From f539f59210e79a1e028ea6b2021283f7f9f89bf2 Mon Sep 17 00:00:00 2001 From: Imanol Maiztegui Date: Thu, 7 May 2026 15:55:10 +0200 Subject: [PATCH] fix(kilo-ui): add diff viewer action to write tool header (#10008) Extend the "Open in Diff Viewer" icon button to appear on Write tool calls, not just Edit. Refactor shared CSS selectors to use generic data-slot names (tool-trigger-actions) so both triggers share styling. --- .changeset/open-write-diff-viewer.md | 5 ++ .../kilo-ui/src/components/message-part.css | 14 +++-- .../kilo-ui/src/components/message-part.tsx | 56 +++++++++++++------ .../src/stories/message-part.stories.tsx | 43 +++++++++++++- 4 files changed, 95 insertions(+), 23 deletions(-) create mode 100644 .changeset/open-write-diff-viewer.md diff --git a/.changeset/open-write-diff-viewer.md b/.changeset/open-write-diff-viewer.md new file mode 100644 index 00000000000..c9ce4d18efc --- /dev/null +++ b/.changeset/open-write-diff-viewer.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Show an "Open in Diff Viewer" icon on Write tool calls (matching the Edit tool), so the changes can be inspected in a full diff tab. diff --git a/packages/kilo-ui/src/components/message-part.css b/packages/kilo-ui/src/components/message-part.css index 942e77c45d9..bb6f99c515a 100644 --- a/packages/kilo-ui/src/components/message-part.css +++ b/packages/kilo-ui/src/components/message-part.css @@ -40,12 +40,14 @@ } } -/* Hover-revealed "Open in Diff Viewer" icon button in the edit-tool header */ -[data-component="tool-trigger"] [data-slot="basic-tool-tool-info"]:has([data-component="edit-trigger"]) { +/* Hover-revealed "Open in Diff Viewer" icon button in edit/write tool headers */ +[data-component="tool-trigger"] [data-slot="basic-tool-tool-info"]:has([data-component="edit-trigger"]), +[data-component="tool-trigger"] [data-slot="basic-tool-tool-info"]:has([data-component="write-trigger"]) { flex: 1 1 auto; } -[data-component="edit-trigger"] { +[data-component="edit-trigger"], +[data-component="write-trigger"] { display: flex; align-items: center; gap: 8px; @@ -58,7 +60,7 @@ overflow: hidden; } - [data-slot="edit-trigger-actions"] { + [data-slot="tool-trigger-actions"] { flex-shrink: 0; margin-left: auto; display: inline-flex; @@ -68,8 +70,8 @@ } } -[data-component="tool-trigger"]:hover [data-slot="edit-trigger-actions"], -[data-slot="edit-trigger-actions"]:focus-within { +[data-component="tool-trigger"]:hover [data-slot="tool-trigger-actions"], +[data-slot="tool-trigger-actions"]:focus-within { opacity: 1; } diff --git a/packages/kilo-ui/src/components/message-part.tsx b/packages/kilo-ui/src/components/message-part.tsx index 9ff461092d7..aeb10d3dd43 100644 --- a/packages/kilo-ui/src/components/message-part.tsx +++ b/packages/kilo-ui/src/components/message-part.tsx @@ -2185,7 +2185,7 @@ ToolRegistry.register({ - + !!data.openDiff && !!props.input.filePath && (!!view() || !!props.input.content) + const canOpenFile = () => !!data.openFile && !!props.input.filePath + + const openDiff = () => { + if (!data.openDiff || !props.input.filePath) return + const v = view() + data.openDiff({ + file: props.metadata?.filediff?.file || props.input.filePath, + before: v?.before ?? "", + after: v?.after ?? props.input.content ?? "", + additions: props.metadata?.filediff?.additions ?? 0, + deletions: props.metadata?.filediff?.deletions ?? 0, + }) + } const handleFileClick = (e: MouseEvent) => { e.stopPropagation() - if (data.openDiff && view()) { - data.openDiff({ - file: props.metadata?.filediff?.file || props.input.filePath, - before: view()!.before, - after: view()!.after, - additions: props.metadata?.filediff?.additions ?? 0, - deletions: props.metadata?.filediff?.deletions ?? 0, - }) + if (canOpenDiff()) { + openDiff() return } - if (!data.openFile || !props.input.filePath) return - data.openFile(props.input.filePath) + if (canOpenFile()) { + data.openFile!(props.input.filePath!) + } + } + + const handleOpenDiffClick = (e: MouseEvent) => { + e.stopPropagation() + openDiff() } return ( @@ -2286,16 +2300,26 @@ ToolRegistry.register({ path={props.input.filePath?.includes("/") ? getDirectory(props.input.filePath!) : undefined} changes={props.metadata.filediff} animate={reveal()} - onClick={ - (view() && data.openDiff) || (data.openFile && props.input.filePath) - ? handleFileClick - : undefined - } + onClick={canOpenDiff() || canOpenFile() ? handleFileClick : undefined} /> )} + + + + e.preventDefault()} + onClick={handleOpenDiffClick} + aria-label={i18n.t("ui.messagePart.openInDiffViewer")} + /> + + + } > diff --git a/packages/kilo-ui/src/stories/message-part.stories.tsx b/packages/kilo-ui/src/stories/message-part.stories.tsx index 8c256487b12..5e1912f3aff 100644 --- a/packages/kilo-ui/src/stories/message-part.stories.tsx +++ b/packages/kilo-ui/src/stories/message-part.stories.tsx @@ -196,6 +196,35 @@ const editCompletedPart: ToolPart = { }, } +// Completed write tool that creates a new file — exercises canOpenDiff() via +// `props.input.content` so the "Open in Diff Viewer" icon button renders even +// when metadata.filediff has no diff payload. +const writeCompletedPart: ToolPart = { + id: "part-tool-write-done", + sessionID: SESSION_ID, + messageID: ASST_MSG_ID, + type: "tool", + callID: "call-write-done", + tool: "write", + state: { + status: "completed", + input: { + filePath: "src/greet.ts", + content: "export function greet(name: string) {\n return `Hello, ${name}!`\n}\n", + }, + output: "File written successfully", + title: "Write file", + metadata: { + filediff: { + file: "src/greet.ts", + additions: 3, + deletions: 0, + }, + }, + time: { start: now - 4000, end: now - 3500 }, + }, +} + // --- Reasoning part --- const reasoningPart: ReasoningPart = { @@ -243,6 +272,7 @@ const mockDataBash = createMockData([bashCompleted]) const mockDataContextGroup = createMockData([completedToolPart, grepCompleted, globCompleted, textPart]) // Completed edit tool with filediff — exercises the "Open in Diff Viewer" button path const mockDataEdit = createMockData([editCompletedPart]) +const mockDataWrite = createMockData([writeCompletedPart]) function AllProviders(props: { children: any; data?: MockData; onOpenDiff?: () => void }) { return ( @@ -402,7 +432,18 @@ export const WithEditToolOpenDiffAction: Story = { name: "WithEditTool (open-diff action visible)", render: () => ( {}}> - + + + + ), +} + +// --- Completed write tool with content → "Open in Diff Viewer" icon visible --- +export const WithWriteToolOpenDiffAction: Story = { + name: "WithWriteTool (open-diff action visible)", + render: () => ( + {}}> + ),