diff --git a/CHANGELOG.md b/CHANGELOG.md index 776cb6f4..8146c037 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - 插件工具使用中文等非 ASCII 名称时 LLM 调用失败:主流 API 要求工具名匹配 `^[a-zA-Z0-9_-]{1,64}$`,现自动将非法名称转写为合法拼音名(`pypinyin` 缺失时退回下划线替换),冲突追加 `_2`/`_3` 后缀,并在工具描述前缀 `[原名: …]` 保留原名映射;`config_json.plugins` 配置键与插件内部仍使用原始名称,路由不受影响 - 修复聊天页在"生成中"时于输入框持续打字导致消息列表上下轻微抖动的问题:输入框高度测量改为在离屏克隆节点上进行,不再瞬态改变页面布局 - 修复个性化「通道」面板在页面放大后不出现纵向滚动条、被挤出的通道卡片无法查看的问题:工具栏固定、卡片网格改为内部滚动区(与技能面板一致),移动端仍整页滚动 +- 修复聊天页「编辑文件」卡片只展开文件树、不定位到所生成文件的问题:点击后直接打开该轮最近写入文件的编辑标签页,同时保留文件列表标签页,便于继续浏览其他文件 ## [0.9.24] - 2026-08-15 diff --git a/dashboard/src/pages/Chat/components/AssistantTurnView.tsx b/dashboard/src/pages/Chat/components/AssistantTurnView.tsx index 17a3117a..71b22a5b 100644 --- a/dashboard/src/pages/Chat/components/AssistantTurnView.tsx +++ b/dashboard/src/pages/Chat/components/AssistantTurnView.tsx @@ -11,6 +11,7 @@ import { } from "../utils/messageContent"; import { layoutAssistantTurnHitl } from "../utils/layoutAssistantTurnHitl"; import { useAgent } from "../../../context/AgentContext"; +import { lastWriteToolPath } from "../hooks/useChatFileDetection"; import { TodoProgressPanel } from "../../../components/TodoProgressPanel"; import { collectWriteTodosFromMessages, @@ -35,7 +36,7 @@ interface AssistantTurnViewProps { decisions: Array<{ type: string; message?: string }>, ) => void; onOpenBrowser?: () => void; - onEditFile?: () => void; + onEditFile?: (path?: string | null) => void; onRunShellCommand?: (code: string) => void; shellCommandDisabled?: boolean; shellCommandDisabledTitle?: string; @@ -102,6 +103,10 @@ export default function AssistantTurnView({ const showOpenBrowser = usedBrowser && !!onOpenBrowser; const usedFileTool = turnUsedFileTool(fullSplit); const showEditFile = usedFileTool && !!onEditFile; + const lastFilePath = useMemo( + () => lastWriteToolPath(messages, agentId), + [messages, agentId], + ); const hasToolMedia = toolMedia.images.length > 0 || toolMedia.videos.length > 0 || @@ -233,7 +238,7 @@ export default function AssistantTurnView({ className={`${styles.openBrowserPrompt} ${ turnStreaming ? styles.openBrowserPromptActive : "" }`} - onClick={onEditFile} + onClick={() => onEditFile(lastFilePath)} aria-label={t("chat.editFileCard", "编辑文件")} > , ) => void; onOpenBrowser?: () => void; - onEditFile?: () => void; + onEditFile?: (path?: string | null) => void; onRunShellCommand?: (code: string) => void; shellCommandDisabled?: boolean; shellCommandDisabledTitle?: string; @@ -128,7 +128,7 @@ interface GroupRenderContext { decisions: Array<{ type: string; message?: string }>, ) => void; onOpenBrowser?: () => void; - onEditFile?: () => void; + onEditFile?: (path?: string | null) => void; onRunShellCommand?: (code: string) => void; shellCommandDisabled?: boolean; shellCommandDisabledTitle?: string; diff --git a/dashboard/src/pages/Chat/hooks/useChatDockPanel.test.ts b/dashboard/src/pages/Chat/hooks/useChatDockPanel.test.ts index c89e1828..db047cb7 100644 --- a/dashboard/src/pages/Chat/hooks/useChatDockPanel.test.ts +++ b/dashboard/src/pages/Chat/hooks/useChatDockPanel.test.ts @@ -32,6 +32,26 @@ describe("useChatDockPanel tabs", () => { expect(result.current.openTabs.map((t) => t.id)).toEqual([fileId]); }); + it("openFileListAt opens the files tab together with the focused file tab", () => { + const { result } = renderHook(() => useChatDockPanel(false, "main")); + act(() => { + result.current.openFileListAt("/outbound/a.txt"); + }); + const fileId = dockFileTabId("/outbound/a.txt", "main"); + expect(result.current.dockOpen).toBe(true); + expect(result.current.openTabs.map((t) => t.id)).toEqual(["files", fileId]); + expect(result.current.activeTabId).toBe(fileId); + }); + + it("openFileListAt without a path falls back to the files tab", () => { + const { result } = renderHook(() => useChatDockPanel(false, "main")); + act(() => { + result.current.openFileListAt(null); + }); + expect(result.current.openTabs.map((t) => t.id)).toEqual(["files"]); + expect(result.current.activeTabId).toBe("files"); + }); + it("toggleBrowserPanel opens browser tab then closes dock when active", () => { const { result } = renderHook(() => useChatDockPanel(false)); act(() => { diff --git a/dashboard/src/pages/Chat/hooks/useChatDockPanel.ts b/dashboard/src/pages/Chat/hooks/useChatDockPanel.ts index 8be9d2eb..3770aced 100644 --- a/dashboard/src/pages/Chat/hooks/useChatDockPanel.ts +++ b/dashboard/src/pages/Chat/hooks/useChatDockPanel.ts @@ -156,6 +156,18 @@ export function useChatDockPanel(isMobile: boolean, agentId?: string | null) { [agentId, openDock, openFileList], ); + /** + * Edit-file card: open the file list tab alongside the generated file tab so + * the tree stays reachable while landing directly on the file itself. + */ + const openFileListAt = useCallback( + (path?: string | null) => { + openFileList(); + openFileAt(path); + }, + [openFileList, openFileAt], + ); + const openBrowserTab = useCallback(() => { setOpenTabs((prev) => { if (prev.some((t) => t.id === "browser")) return prev; @@ -259,6 +271,7 @@ export function useChatDockPanel(isMobile: boolean, agentId?: string | null) { handleModeChange, openFileAt, openFileList, + openFileListAt, openBrowserTab, toggleBrowserPanel, openTerminalTab, diff --git a/dashboard/src/pages/Chat/hooks/useChatFileDetection.ts b/dashboard/src/pages/Chat/hooks/useChatFileDetection.ts index 39526873..06245899 100644 --- a/dashboard/src/pages/Chat/hooks/useChatFileDetection.ts +++ b/dashboard/src/pages/Chat/hooks/useChatFileDetection.ts @@ -128,6 +128,24 @@ function addPath( paths.push(normalizeDockFilePath(path!) || key); } +/** + * Path of the most recent workspace write/edit tool call in this turn, so the + * "edit file" card can jump straight to the generated file (falls back to the + * file list when no listable path is found). + */ +export function lastWriteToolPath( + messages: ChatMessage[], + agentId?: string | null, +): string | null { + for (let i = messages.length - 1; i >= 0; i--) { + const raw = extractWriteToolPath(messages[i]); + if (!raw) continue; + const key = canonicalizeDockFilePath(raw, agentId); + if (key && isDockListablePath(key)) return raw; + } + return null; +} + /** * Collect workspace file paths from the active thread so the docked file * panel can switch among written / previewable / attached files together. diff --git a/dashboard/src/pages/Chat/index.tsx b/dashboard/src/pages/Chat/index.tsx index f385f544..feae4786 100644 --- a/dashboard/src/pages/Chat/index.tsx +++ b/dashboard/src/pages/Chat/index.tsx @@ -278,6 +278,7 @@ function ChatPageInner() { handleClose: handleDockClose, handleModeChange: handleDockModeChange, openFileList, + openFileListAt, openFileAt, openBrowserTab, toggleBrowserPanel, @@ -839,7 +840,7 @@ function ChatPageInner() { } onEditFile={ !sharedExpertViewer && panelFilePaths.length > 0 && !isMobile - ? openFileList + ? openFileListAt : undefined } />