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} /> )}