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