From d3fde810b46343d194f368635a821d5773d08309 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Sl=C3=ADva?= Date: Fri, 24 Jul 2026 21:13:05 +0200 Subject: [PATCH] feat(chat): enable jump-to-file in regular chat diff accordions (#997) --- webview-ui/src/components/chat/ChatRow.tsx | 10 ++-- .../__tests__/ChatRow.diff-actions.spec.tsx | 52 ++++++++++++++++--- 2 files changed, 51 insertions(+), 11 deletions(-) diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index f973c7929f..640f9fdd22 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -420,12 +420,13 @@ export const ChatRowContent = ({ return (tool.content ?? tool.diff) as string | undefined }, [tool]) - const onJumpToCreatedFile = useMemo(() => { - if (!tool || tool.tool !== "newFileCreated" || !tool.path) { + const onJumpToFile = useMemo(() => { + const path = tool?.path + if (!tool || !path) { return undefined } - return () => vscode.postMessage({ type: "openFile", text: "./" + tool.path }) + return () => vscode.postMessage({ type: "openFile", text: path.startsWith("./") ? path : "./" + path }) }, [tool]) const followUpData = useMemo(() => { @@ -497,7 +498,7 @@ export const ChatRowContent = ({ isLoading={message.partial} isExpanded={isExpanded} onToggleExpand={handleToggleExpand} - onJumpToFile={onJumpToCreatedFile} + onJumpToFile={onJumpToFile} diffStats={tool.diffStats} /> @@ -536,6 +537,7 @@ export const ChatRowContent = ({ isLoading={message.partial} isExpanded={isExpanded} onToggleExpand={handleToggleExpand} + onJumpToFile={onJumpToFile} diffStats={tool.diffStats} /> diff --git a/webview-ui/src/components/chat/__tests__/ChatRow.diff-actions.spec.tsx b/webview-ui/src/components/chat/__tests__/ChatRow.diff-actions.spec.tsx index 7876420959..5675af898e 100644 --- a/webview-ui/src/components/chat/__tests__/ChatRow.diff-actions.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ChatRow.diff-actions.spec.tsx @@ -141,30 +141,68 @@ describe("ChatRow - inline diff stats and actions", () => { expect(screen.getByText("-0")).toBeInTheDocument() }) - it("preserves jump-to-file affordance for newFileCreated", () => { + it.each([ + ["appliedDiff", "@@ -1,1 +1,1 @@\n-old\n+new\n"], + ["editedExistingFile", "@@ -1,1 +1,1 @@\n-old\n+new\n"], + ["newFileCreated", "+new file"], + ["searchAndReplace", "-a\n-b\n+c\n"], + ["search_and_replace", "-a\n-b\n+c\n"], + ["search_replace", "-a\n-b\n+c\n"], + ["edit", "@@ -1,1 +1,1 @@\n-old\n+new\n"], + ["edit_file", "@@ -1,1 +1,1 @@\n-old\n+new\n"], + ["apply_patch", "@@ -1,1 +1,1 @@\n-old\n+new\n"], + ["apply_diff", "@@ -1,1 +1,1 @@\n-old\n+new\n"], + ["insertContent", "@@ -1,1 +1,1 @@\n-old\n+new\n"], + ])("shows jump-to-file affordance for %s", (tool, diff) => { const message = createToolAskMessage({ - tool: "newFileCreated", - path: "src/new-file.ts", - content: "+new file", - diffStats: { added: 1, removed: 0 }, + tool, + path: "src/file.ts", + diff, + lineNumber: 0, + diffStats: { added: 1, removed: 1 }, }) const { container } = renderChatRow(message) + mockPostMessage.mockClear() const openFileIcon = container.querySelector(".codicon-link-external") as HTMLElement | null expect(openFileIcon).toBeInTheDocument() if (!openFileIcon) { - throw new Error("Expected external link icon for newFileCreated") + throw new Error(`Expected external link icon for ${tool}`) } fireEvent.click(openFileIcon) + expect(mockPostMessage).toHaveBeenCalledTimes(1) expect(mockPostMessage).toHaveBeenCalledWith({ type: "openFile", - text: "./src/new-file.ts", + text: "./src/file.ts", }) }) + it("does not show jump-to-file affordance when path is missing", () => { + const message = createToolAskMessage({ + tool: "appliedDiff", + diff: "@@ -1,1 +1,1 @@\n-old\n+new\n", + diffStats: { added: 1, removed: 1 }, + }) + + const { container } = renderChatRow(message) + expect(container.querySelector(".codicon-link-external")).not.toBeInTheDocument() + }) + + it("does not show jump-to-file affordance for non-file tools", () => { + const message = createToolAskMessage({ + tool: "executeCommand", + path: "src/file.ts", + command: "echo hello", + content: "hello", + }) + + const { container } = renderChatRow(message) + expect(container.querySelector(".codicon-link-external")).not.toBeInTheDocument() + }) + it("preserves protected and outside-workspace messaging in unified branch", () => { const outsideWorkspaceMessage = createToolAskMessage({ tool: "searchAndReplace",