diff --git a/webview-ui/src/components/chat/ChangeCard.tsx b/webview-ui/src/components/chat/ChangeCard.tsx index efae3394c7..709b4e6fb9 100644 --- a/webview-ui/src/components/chat/ChangeCard.tsx +++ b/webview-ui/src/components/chat/ChangeCard.tsx @@ -146,6 +146,13 @@ export const ChangeCard = ({ message }: { message: ClineMessage }) => { setStepRollback({ status: "pending" }) } + // Open the changed file in the editor. The extension host resolves relative + // paths against the current cwd (webviewMessageHandler "openFile"), so + // normalize the "./" prefix the same way FileChangesPanel does. + const openFileInEditor = (path: string) => { + vscode.postMessage({ type: "openFile", text: path.startsWith("./") ? path : "./" + path }) + } + const diffBadges = (additions: number, deletions: number) => additions > 0 || deletions > 0 ? ( @@ -306,6 +313,7 @@ export const ChangeCard = ({ message }: { message: ClineMessage }) => { language="diff" isExpanded={expandedFiles.has(file.path)} onToggleExpand={() => toggleFile(file.path)} + onJumpToFile={() => openFileInEditor(file.path)} diffStats={{ added: file.additions, removed: file.deletions }} /> ) : ( @@ -315,6 +323,22 @@ export const ChangeCard = ({ message }: { message: ClineMessage }) => { {diffBadges(file.additions, file.deletions)} + {/* Native button (not a span) so keyboard users can open the + file from a compact row: Enter/Space activate it for free. */} + )} diff --git a/webview-ui/src/components/chat/__tests__/ChangeCard.spec.tsx b/webview-ui/src/components/chat/__tests__/ChangeCard.spec.tsx index d5cc3804aa..b8718b05d1 100644 --- a/webview-ui/src/components/chat/__tests__/ChangeCard.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ChangeCard.spec.tsx @@ -27,6 +27,7 @@ vi.mock("react-i18next", () => ({ "chat:changeCard.rolledBack": "Rolled back", "chat:changeCard.stepRolledBack": "Step rolled back", "chat:changeCard.rollbackFailed": "Rollback failed", + "chat:changeCard.openFile": "Open file", } return map[key] || key }, @@ -341,6 +342,67 @@ describe("ChangeCard", () => { expect(screen.getByTestId("change-card-step-rollback")).toBeInTheDocument() expect(mockPostMessage).not.toHaveBeenCalledWith(expect.objectContaining({ type: "checkpointRollbackStep" })) }) + + it("posts an openFile message from both the diff-row jump icon and the no-diff-row button", () => { + renderWithExtensionState( + , + ) + + // Diff row: the CodeAccordion header jump icon (own aria-label). + fireEvent.click(screen.getByLabelText("Open file: src/a.ts")) + expect(mockPostMessage).toHaveBeenCalledWith({ type: "openFile", text: "./src/a.ts" }) + mockPostMessage.mockClear() + + // No-diff row: the open control on the plain path row. + fireEvent.click(screen.getByTestId("change-card-file-open-1")) + expect(mockPostMessage).toHaveBeenCalledWith({ type: "openFile", text: "./src/b.ts" }) + }) + + it("does not double-prefix paths that already carry the ./ marker", () => { + renderWithExtensionState( + , + ) + + fireEvent.click(screen.getByTestId("change-card-file-open-0")) + expect(mockPostMessage).toHaveBeenCalledWith({ type: "openFile", text: "./src/c.ts" }) + }) + + it("renders the no-diff row open control as a native button so keyboard users can activate it", () => { + renderWithExtensionState( + , + ) + + // A native