diff --git a/dashboard/src/locales/en.json b/dashboard/src/locales/en.json
index be84f34d..4e157fd4 100644
--- a/dashboard/src/locales/en.json
+++ b/dashboard/src/locales/en.json
@@ -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"
diff --git a/dashboard/src/locales/zh.json b/dashboard/src/locales/zh.json
index 97ea00e9..1f8a9622 100644
--- a/dashboard/src/locales/zh.json
+++ b/dashboard/src/locales/zh.json
@@ -1055,6 +1055,8 @@
"dockFileListCount": "{{count}} 个文件",
"dockFileListHint": "当前仅列出执行过程中生成的文件,不代表最终一定存储,可能在处理结束后被大模型删除。",
"dockFileMaybeDeleted": "该文件可能为处理过程中的临时文件,当前已经被删除。",
+ "dockFileDelete": "删除文件",
+ "dockFileDeleteConfirm": "确定删除该文件吗?删除后不可恢复。",
"remoteBrowserTitle": "远程浏览器",
"dockTerminalTitle": "终端",
"loadEarlierMessages": "加载更早的消息"
diff --git a/dashboard/src/pages/Chat/chatBrowserPanel.partial.less b/dashboard/src/pages/Chat/chatBrowserPanel.partial.less
index bc26bfbd..c7233aff 100644
--- a/dashboard/src/pages/Chat/chatBrowserPanel.partial.less
+++ b/dashboard/src/pages/Chat/chatBrowserPanel.partial.less
@@ -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;
@@ -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;
}
diff --git a/dashboard/src/pages/Chat/components/ChatDockFileList.tsx b/dashboard/src/pages/Chat/components/ChatDockFileList.tsx
index e5d98a99..3c6c9c0e 100644
--- a/dashboard/src/pages/Chat/components/ChatDockFileList.tsx
+++ b/dashboard/src/pages/Chat/components/ChatDockFileList.tsx
@@ -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,
@@ -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({
@@ -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 (
-
-
-
+
+
+ onDelete()}
+ disabled={downloading || deleting}
+ >
+
+
+
+
+
+
+
);
}
@@ -117,8 +152,10 @@ function TreeNodes({
expanded,
toggle,
downloading,
+ deleting,
onOpenFile,
onDownload,
+ onDelete,
downloadLabel,
}: {
nodes: DockPathTreeNode[];
@@ -126,8 +163,10 @@ function TreeNodes({
expanded: Set;
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 (
@@ -150,8 +189,10 @@ function TreeNodes({
expanded={expanded}
toggle={toggle}
downloading={downloading}
+ deleting={deleting}
onOpenFile={onOpenFile}
onDownload={onDownload}
+ onDelete={onDelete}
downloadLabel={downloadLabel}
/>
) : null}
@@ -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}
/>
);
@@ -181,11 +224,17 @@ 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>(() => 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),
@@ -193,6 +242,7 @@ export default function ChatDockFileList({
);
const [expanded, setExpanded] = useState>(() => new Set());
const [downloading, setDownloading] = useState(null);
+ const [deleting, setDeleting] = useState(null);
const seenFoldersRef = useRef>(new Set());
// Expand newly appeared folders only; keep user collapse state.
@@ -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 = (
void handleDownload(p)}
+ onDelete={(p) => void handleDelete(p)}
downloadLabel={downloadLabel}
/>
diff --git a/dashboard/src/pages/Chat/components/ChatDockPanel.tsx b/dashboard/src/pages/Chat/components/ChatDockPanel.tsx
index fd8ad064..3172450c 100644
--- a/dashboard/src/pages/Chat/components/ChatDockPanel.tsx
+++ b/dashboard/src/pages/Chat/components/ChatDockPanel.tsx
@@ -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";
@@ -128,6 +128,14 @@ const ChatDockPanel: React.FC = ({
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;
@@ -254,6 +262,7 @@ const ChatDockPanel: React.FC = ({
agentId={agentId}
filePaths={filePaths}
onOpenFile={onOpenFile}
+ onCloseFile={handleCloseFile}
/>
)}