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.
This commit is contained in:
Imanol Maiztegui
2026-05-07 15:55:10 +02:00
committed by GitHub
parent 8699edd5a3
commit f539f59210
4 changed files with 95 additions and 23 deletions
+5
View File
@@ -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.
@@ -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;
}
@@ -2185,7 +2185,7 @@ ToolRegistry.register({
</div>
</div>
<Show when={canOpenDiff()}>
<span data-slot="edit-trigger-actions">
<span data-slot="tool-trigger-actions">
<Tooltip value={i18n.t("ui.messagePart.openInDiffViewer")} placement="top" gutter={4}>
<IconButton
icon="square-arrow-top-right"
@@ -2249,21 +2249,35 @@ ToolRegistry.register({
if (!diff?.patch) return
return contents(diff)
})
const canOpenDiff = () => !!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}
/>
)}
</Show>
</div>
</div>
<Show when={canOpenDiff()}>
<span data-slot="tool-trigger-actions">
<Tooltip value={i18n.t("ui.messagePart.openInDiffViewer")} placement="top" gutter={4}>
<IconButton
icon="square-arrow-top-right"
size="small"
variant="ghost"
onMouseDown={(e) => e.preventDefault()}
onClick={handleOpenDiffClick}
aria-label={i18n.t("ui.messagePart.openInDiffViewer")}
/>
</Tooltip>
</span>
</Show>
</div>
}
>
@@ -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: () => (
<AllProviders data={mockDataEdit} onOpenDiff={() => {}}>
<style>{`[data-slot="edit-trigger-actions"] { opacity: 1 !important; }`}</style>
<style>{`[data-slot="tool-trigger-actions"] { opacity: 1 !important; }`}</style>
<AssistantParts messages={[mockAssistantMessage]} />
</AllProviders>
),
}
// --- Completed write tool with content → "Open in Diff Viewer" icon visible ---
export const WithWriteToolOpenDiffAction: Story = {
name: "WithWriteTool (open-diff action visible)",
render: () => (
<AllProviders data={mockDataWrite} onOpenDiff={() => {}}>
<style>{`[data-slot="tool-trigger-actions"] { opacity: 1 !important; }`}</style>
<AssistantParts messages={[mockAssistantMessage]} />
</AllProviders>
),