Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions dashboard/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1056,6 +1056,8 @@
"dockFileListCount": "{{count}} files",
"dockFileListHint": "This list only shows files produced during execution. They are not guaranteed to be kept, and the model may delete them after processing finishes.",
"dockFileMaybeDeleted": "This file may have been a temporary artifact during processing and has already been deleted.",
"dockFileDelete": "Delete file",
"dockFileDeleteConfirm": "Delete this file? This cannot be undone.",
"remoteBrowserTitle": "Remote Browser",
"dockTerminalTitle": "Terminal",
"loadEarlierMessages": "Load earlier messages"
Expand Down
2 changes: 2 additions & 0 deletions dashboard/src/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -1055,6 +1055,8 @@
"dockFileListCount": "{{count}} 个文件",
"dockFileListHint": "当前仅列出执行过程中生成的文件,不代表最终一定存储,可能在处理结束后被大模型删除。",
"dockFileMaybeDeleted": "该文件可能为处理过程中的临时文件,当前已经被删除。",
"dockFileDelete": "删除文件",
"dockFileDeleteConfirm": "确定删除该文件吗?删除后不可恢复。",
"remoteBrowserTitle": "远程浏览器",
"dockTerminalTitle": "终端",
"loadEarlierMessages": "加载更早的消息"
Expand Down
31 changes: 26 additions & 5 deletions dashboard/src/pages/Chat/chatBrowserPanel.partial.less
Original file line number Diff line number Diff line change
Expand Up @@ -392,12 +392,20 @@
white-space: nowrap;
}

.dockFileTreeDownload {
.dockFileTreeActions {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
flex-shrink: 0;
margin-left: auto;
}

.dockFileTreeDownload,
.dockFileTreeDelete {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 28px;
height: 28px;
padding: 0;
Expand Down Expand Up @@ -434,15 +442,28 @@
}
}

/* Desktop: hide download until row hover / keyboard focus. */
.dockFileTreeDelete:hover:not(:disabled) {
color: var(--fn-color-danger);
border-color: color-mix(in srgb, var(--fn-color-danger) 35%, transparent);
background: color-mix(
in srgb,
var(--fn-color-danger) 8%,
var(--fn-bg-primary)
);
}

/* Desktop: hide actions until row hover / keyboard focus. */
@media (hover: hover) and (pointer: fine) {
.dockFileTreeDownload {
.dockFileTreeDownload,
.dockFileTreeDelete {
opacity: 0;
pointer-events: none;
}

.dockFileTreeFile:hover .dockFileTreeDownload,
.dockFileTreeFile:focus-within .dockFileTreeDownload {
.dockFileTreeFile:focus-within .dockFileTreeDownload,
.dockFileTreeFile:hover .dockFileTreeDelete,
.dockFileTreeFile:focus-within .dockFileTreeDelete {
opacity: 1;
pointer-events: auto;
}
Expand Down
138 changes: 120 additions & 18 deletions dashboard/src/pages/Chat/components/ChatDockFileList.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Empty, Tooltip } from "antd";
import { ChevronDown, ChevronRight, Download, Info } from "lucide-react";
import { Empty, Popconfirm, Tooltip } from "antd";
import { ChevronDown, ChevronRight, Download, Info, Trash2 } from "lucide-react";
import { useTranslation } from "react-i18next";
import { message } from "@/utils/antdMessage";
import { requestBlob } from "../../../api/request";
import { workspaceApi } from "../../../api/modules/workspace";
import { isNotFoundApiError } from "../../../utils/apiError";
import { fileTreeIcon } from "../../../utils/fileTreeIcon";
import {
buildDockPathTree,
canonicalizeDockFilePath,
collectDockFolderPaths,
dedupeDockFilePaths,
dockFileBasename,
Expand All @@ -21,6 +23,8 @@ interface ChatDockFileListProps {
agentId: string;
filePaths: string[];
onOpenFile: (path: string) => void;
/** Called after a file is deleted so its viewer tab can be closed. */
onCloseFile?: (path: string) => void;
}

function FolderRow({
Expand Down Expand Up @@ -64,17 +68,23 @@ function FileRow({
node,
depth,
downloading,
deleting,
onOpen,
onDownload,
onDelete,
downloadLabel,
}: {
node: DockPathTreeNode;
depth: number;
downloading: boolean;
deleting: boolean;
onOpen: () => void;
onDownload: () => void;
onDelete: () => void;
downloadLabel: string;
}) {
const { t } = useTranslation();
const deleteLabel = t("chat.dockFileDelete", "删除文件");
return (
<div
className={styles.dockFileTreeFile}
Expand All @@ -93,20 +103,45 @@ function FileRow({
{dockFileBasename(node.path)}
</span>
</button>
<Tooltip title={downloadLabel}>
<button
type="button"
className={styles.dockFileTreeDownload}
onClick={(e) => {
e.stopPropagation();
onDownload();
}}
disabled={downloading}
aria-label={downloadLabel}
>
<Download size={15} strokeWidth={2} />
</button>
</Tooltip>
<div className={styles.dockFileTreeActions}>
<Tooltip title={deleteLabel}>
<Popconfirm
title={t(
"chat.dockFileDeleteConfirm",
"确定删除该文件吗?删除后不可恢复。",
)}
okText={t("common.delete", "删除")}
cancelText={t("common.cancel", "取消")}
okButtonProps={{ danger: true }}
onConfirm={() => onDelete()}
disabled={downloading || deleting}
>
<button
type="button"
className={styles.dockFileTreeDelete}
onClick={(e) => e.stopPropagation()}
disabled={downloading || deleting}
aria-label={deleteLabel}
>
<Trash2 size={15} strokeWidth={2} />
</button>
</Popconfirm>
</Tooltip>
<Tooltip title={downloadLabel}>
<button
type="button"
className={styles.dockFileTreeDownload}
onClick={(e) => {
e.stopPropagation();
onDownload();
}}
disabled={downloading || deleting}
aria-label={downloadLabel}
>
<Download size={15} strokeWidth={2} />
</button>
</Tooltip>
</div>
</div>
);
}
Expand All @@ -117,17 +152,21 @@ function TreeNodes({
expanded,
toggle,
downloading,
deleting,
onOpenFile,
onDownload,
onDelete,
downloadLabel,
}: {
nodes: DockPathTreeNode[];
depth: number;
expanded: Set<string>;
toggle: (path: string) => void;
downloading: string | null;
deleting: string | null;
onOpenFile: (path: string) => void;
onDownload: (path: string) => void;
onDelete: (path: string) => void;
downloadLabel: string;
}) {
return (
Expand All @@ -150,8 +189,10 @@ function TreeNodes({
expanded={expanded}
toggle={toggle}
downloading={downloading}
deleting={deleting}
onOpenFile={onOpenFile}
onDownload={onDownload}
onDelete={onDelete}
downloadLabel={downloadLabel}
/>
) : null}
Expand All @@ -164,8 +205,10 @@ function TreeNodes({
node={node}
depth={depth}
downloading={downloading === node.path}
deleting={deleting === node.path}
onOpen={() => onOpenFile(node.path)}
onDownload={() => onDownload(node.path)}
onDelete={() => onDelete(node.path)}
downloadLabel={downloadLabel}
/>
);
Expand All @@ -181,18 +224,25 @@ export default function ChatDockFileList({
agentId,
filePaths,
onOpenFile,
onCloseFile,
}: ChatDockFileListProps) {
const { t } = useTranslation();
// Deleted files stay in message-derived filePaths; hide them by canonical key.
const [deletedKeys, setDeletedKeys] = useState<Set<string>>(() => new Set());
const paths = useMemo(
() => dedupeDockFilePaths(filePaths, agentId),
[filePaths, agentId],
() =>
dedupeDockFilePaths(filePaths, agentId).filter(
(p) => !deletedKeys.has(canonicalizeDockFilePath(p, agentId)),
),
[filePaths, agentId, deletedKeys],
);
const tree = useMemo(
() => buildDockPathTree(paths, agentId),
[paths, agentId],
);
const [expanded, setExpanded] = useState<Set<string>>(() => new Set());
const [downloading, setDownloading] = useState<string | null>(null);
const [deleting, setDeleting] = useState<string | null>(null);
const seenFoldersRef = useRef<Set<string>>(new Set());

// Expand newly appeared folders only; keep user collapse state.
Expand Down Expand Up @@ -256,6 +306,56 @@ export default function ChatDockFileList({
[agentId, t],
);

/** Hide a path from the list (by canonical key) and close its viewer tab. */
const hideDeletedPath = useCallback(
(path: string) => {
const key = canonicalizeDockFilePath(path, agentId);
if (key) {
setDeletedKeys((prev) => {
if (prev.has(key)) return prev;
const next = new Set(prev);
next.add(key);
return next;
});
}
onCloseFile?.(path);
},
[agentId, onCloseFile],
);

const handleDelete = useCallback(
async (path: string) => {
if (!agentId || !path) return;
setDeleting(path);
try {
await workspaceApi.deleteWorkspaceFile(
agentId,
toDockWorkspaceApiPath(path, agentId),
);
message.success(t("workspace.deleteSuccess", "已删除"));
hideDeletedPath(path);
} catch (err: unknown) {
if (isNotFoundApiError(err)) {
message.warning(
t(
"chat.dockFileMaybeDeleted",
"该文件可能为处理过程中的临时文件,当前已经被删除。",
),
);
hideDeletedPath(path);
return;
}
message.error(
(err instanceof Error ? err.message : String(err)) ||
t("workspace.deleteFailed", "删除失败"),
);
} finally {
setDeleting(null);
}
},
[agentId, hideDeletedPath, t],
);

const listHint = (
<div className={styles.dockFileListHint} role="note">
<Info
Expand Down Expand Up @@ -309,8 +409,10 @@ export default function ChatDockFileList({
expanded={expanded}
toggle={toggle}
downloading={downloading}
deleting={deleting}
onOpenFile={onOpenFile}
onDownload={(p) => void handleDownload(p)}
onDelete={(p) => void handleDelete(p)}
downloadLabel={downloadLabel}
/>
</div>
Expand Down
11 changes: 10 additions & 1 deletion dashboard/src/pages/Chat/components/ChatDockPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import ChatDockPanelShell from "../../../components/BrowserWorkspace/ChatDockPan
import type { DisplayEnvironment } from "../../../api/types/browser";
import { resolveBrowserProfile } from "../../../utils/browserProfile";
import type { DockTab, DockTabId } from "../hooks/useChatDockPanel";
import { dockFileBasename } from "../utils/dockFilePath";
import { dockFileBasename, dockFileTabId } from "../utils/dockFilePath";
import styles from "../index.module.less";
import ChatDockFileList from "./ChatDockFileList";
import FilePanelContent from "./FilePanelContent";
Expand Down Expand Up @@ -128,6 +128,14 @@ const ChatDockPanel: React.FC<ChatDockPanelProps> = ({
browserRefreshRef.current = refresh;
}, []);

/** Close the viewer tab of a deleted file (canonical key matches tab id). */
const handleCloseFile = useCallback(
(path: string) => {
onCloseTab(dockFileTabId(path, agentId));
},
[agentId, onCloseTab],
);

const getFileActionsHandler = useCallback((path: string) => {
const existing = fileActionsHandlersRef.current[path];
if (existing) return existing;
Expand Down Expand Up @@ -254,6 +262,7 @@ const ChatDockPanel: React.FC<ChatDockPanelProps> = ({
agentId={agentId}
filePaths={filePaths}
onOpenFile={onOpenFile}
onCloseFile={handleCloseFile}
/>
</div>
)}
Expand Down
Loading