mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 11:05:31 +08:00
fix(vscode): show apply_patch diffs before approval (#9691)
* fix(vscode): show apply patch permission diffs * fix(vscode): avoid caching permission diff patches * chore: update kilo-vscode visual regression baselines --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Show apply_patch diffs in the permission prompt before approval.
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7aa0924c6679cbff6e816dd86371a2d483afb7f0840824503ef4ba325c25d6c5
|
||||
size 26282
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:36d371115e898752d566502cae5815c9a9719be64d9b5bd9f6612c0ed8a1d114
|
||||
size 15546
|
||||
oid sha256:f28a551185ec9d7607d02295803b155e0f7bac378fcefdf0996c6ca51c0afad6
|
||||
size 21892
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { permissionDiffs } from "../../webview-ui/src/components/chat/permission-diff-utils"
|
||||
import type { PermissionRequest } from "../../webview-ui/src/types/messages"
|
||||
|
||||
function perm(args: PermissionRequest["args"]): PermissionRequest {
|
||||
return {
|
||||
id: "perm",
|
||||
sessionID: "ses",
|
||||
toolName: "edit",
|
||||
patterns: ["*"],
|
||||
always: ["*"],
|
||||
args,
|
||||
}
|
||||
}
|
||||
|
||||
describe("permissionDiffs", () => {
|
||||
test("uses filediff metadata for edit and write permissions", () => {
|
||||
const diffs = permissionDiffs(
|
||||
perm({ filediff: { file: "src/app.ts", patch: "patch", additions: 1, deletions: 0 } }),
|
||||
)
|
||||
|
||||
expect(diffs).toEqual([{ file: "src/app.ts", patch: "patch", additions: 1, deletions: 0 }])
|
||||
})
|
||||
|
||||
test("uses apply_patch files metadata", () => {
|
||||
const diffs = permissionDiffs(
|
||||
perm({
|
||||
files: [
|
||||
{ relativePath: "src/a.ts", type: "update", patch: "a", additions: 1, deletions: 1 },
|
||||
{ relativePath: "src/b.ts", type: "add", patch: "b", additions: 2, deletions: 0 },
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
expect(diffs).toEqual([
|
||||
{ file: "src/a.ts", patch: "a", additions: 1, deletions: 1 },
|
||||
{ file: "src/b.ts", patch: "b", additions: 2, deletions: 0 },
|
||||
])
|
||||
})
|
||||
|
||||
test("falls back to raw diff metadata", () => {
|
||||
const diffs = permissionDiffs(perm({ filepath: "src/a.ts", diff: "Index: src/a.ts" }))
|
||||
|
||||
expect(diffs).toEqual([{ file: "src/a.ts", patch: "Index: src/a.ts", additions: 0, deletions: 0 }])
|
||||
})
|
||||
|
||||
test("returns no diffs for command-only permissions", () => {
|
||||
expect(permissionDiffs(perm({ command: "git status" }))).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -1,9 +1,9 @@
|
||||
import { type Component, createMemo } from "solid-js"
|
||||
import { Show, type Component, createMemo } from "solid-js"
|
||||
import { Diff } from "@kilocode/kilo-ui/diff"
|
||||
import { DiffChanges } from "@kilocode/kilo-ui/diff-changes"
|
||||
import { IconButton } from "@kilocode/kilo-ui/icon-button"
|
||||
import { Tooltip } from "@kilocode/kilo-ui/tooltip"
|
||||
import { normalize, text } from "@kilocode/kilo-ui/session-diff"
|
||||
import { parsePatch } from "diff"
|
||||
import type { PermissionFileDiff } from "../../types/messages"
|
||||
import { useVSCode } from "../../context/vscode"
|
||||
|
||||
@@ -11,6 +11,29 @@ interface PermissionDiffProps {
|
||||
filediff: PermissionFileDiff
|
||||
}
|
||||
|
||||
function patchText(patch: string) {
|
||||
const parsed = parsePatch(patch)[0]
|
||||
if (!parsed) return { before: "", after: "" }
|
||||
|
||||
const before: string[] = []
|
||||
const after: string[] = []
|
||||
for (const hunk of parsed.hunks) {
|
||||
for (const line of hunk.lines) {
|
||||
if (line.startsWith("-")) {
|
||||
before.push(line.slice(1))
|
||||
continue
|
||||
}
|
||||
if (line.startsWith("+")) {
|
||||
after.push(line.slice(1))
|
||||
continue
|
||||
}
|
||||
before.push(line.slice(1))
|
||||
after.push(line.slice(1))
|
||||
}
|
||||
}
|
||||
return { before: before.join("\n"), after: after.join("\n") }
|
||||
}
|
||||
|
||||
export const PermissionDiff: Component<PermissionDiffProps> = (props) => {
|
||||
const vscode = useVSCode()
|
||||
const filename = createMemo(() => {
|
||||
@@ -27,13 +50,15 @@ export const PermissionDiff: Component<PermissionDiffProps> = (props) => {
|
||||
const resolved = createMemo(() => {
|
||||
const fd = props.filediff
|
||||
if (fd.before !== undefined || fd.after !== undefined) return { before: fd.before ?? "", after: fd.after ?? "" }
|
||||
if (fd.patch) {
|
||||
const view = normalize(fd)
|
||||
return { before: text(view, "deletions"), after: text(view, "additions") }
|
||||
}
|
||||
if (fd.patch) return patchText(fd.patch)
|
||||
return { before: "", after: "" }
|
||||
})
|
||||
|
||||
const empty = createMemo(() => {
|
||||
const diff = resolved()
|
||||
return diff.before === "" && diff.after === ""
|
||||
})
|
||||
|
||||
const openInTab = () => {
|
||||
const { before, after } = resolved()
|
||||
vscode.postMessage({
|
||||
@@ -77,11 +102,16 @@ export const PermissionDiff: Component<PermissionDiffProps> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
<div data-slot="permission-diff-content">
|
||||
<Diff
|
||||
before={{ name: props.filediff.file, contents: resolved().before }}
|
||||
after={{ name: props.filediff.file, contents: resolved().after }}
|
||||
diffStyle="unified"
|
||||
/>
|
||||
<Show
|
||||
when={!empty()}
|
||||
fallback={<div data-slot="permission-diff-empty">Diff preview unavailable for this file.</div>}
|
||||
>
|
||||
<Diff
|
||||
before={{ name: props.filediff.file, contents: resolved().before }}
|
||||
after={{ name: props.filediff.file, contents: resolved().after }}
|
||||
diffStyle="unified"
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -20,6 +20,7 @@ import { useConfig } from "../../context/config"
|
||||
import { describePatterns, resolveLabel, savedRuleStates, type RuleDecision } from "./permission-dock-utils"
|
||||
import { PermissionCommand } from "./PermissionCommand"
|
||||
import { PermissionDiff } from "./PermissionDiff"
|
||||
import { permissionDiffs } from "./permission-diff-utils"
|
||||
import type { PermissionRequest } from "../../types/messages"
|
||||
|
||||
let rulesExpandedPreference = false
|
||||
@@ -47,12 +48,7 @@ export const PermissionDock: Component<{
|
||||
command() ? null : describePatterns(props.request.toolName, props.request.patterns, language.t),
|
||||
)
|
||||
|
||||
const filediff = () => {
|
||||
if (props.request.toolName !== "edit" && props.request.toolName !== "write") return null
|
||||
const fd = props.request.args?.filediff
|
||||
if (!fd || typeof fd !== "object") return null
|
||||
return fd as NonNullable<PermissionRequest["args"]["filediff"]>
|
||||
}
|
||||
const diffs = createMemo(() => permissionDiffs(props.request))
|
||||
|
||||
// Pre-populate toggle states from existing config rules so previously
|
||||
// approved/denied patterns show their saved state immediately.
|
||||
@@ -247,7 +243,11 @@ export const PermissionDock: Component<{
|
||||
)
|
||||
})()}
|
||||
|
||||
<Show when={filediff()}>{(fd) => <PermissionDiff filediff={fd()} />}</Show>
|
||||
<Show when={diffs().length > 0}>
|
||||
<div data-slot="permission-diffs" data-count={diffs().length}>
|
||||
<For each={diffs()}>{(diff) => <PermissionDiff filediff={diff} />}</For>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div data-slot="permission-actions">
|
||||
<Button
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { PermissionFileDiff, PermissionRequest } from "../../types/messages"
|
||||
|
||||
type File = {
|
||||
filePath?: unknown
|
||||
relativePath?: unknown
|
||||
type?: unknown
|
||||
patch?: unknown
|
||||
additions?: unknown
|
||||
deletions?: unknown
|
||||
}
|
||||
|
||||
function num(value: unknown) {
|
||||
return typeof value === "number" ? value : 0
|
||||
}
|
||||
|
||||
function text(value: unknown) {
|
||||
return typeof value === "string" ? value : undefined
|
||||
}
|
||||
|
||||
function clean(diff: unknown): PermissionFileDiff | undefined {
|
||||
if (!diff || typeof diff !== "object") return
|
||||
const item = diff as Record<string, unknown>
|
||||
const file = text(item.file)
|
||||
if (!file) return
|
||||
return {
|
||||
file,
|
||||
...(text(item.patch) !== undefined ? { patch: text(item.patch) } : {}),
|
||||
...(text(item.before) !== undefined ? { before: text(item.before) } : {}),
|
||||
...(text(item.after) !== undefined ? { after: text(item.after) } : {}),
|
||||
additions: num(item.additions),
|
||||
deletions: num(item.deletions),
|
||||
}
|
||||
}
|
||||
|
||||
function file(item: File): PermissionFileDiff | undefined {
|
||||
const name = text(item.relativePath) ?? text(item.filePath)
|
||||
if (!name) return
|
||||
return {
|
||||
file: name,
|
||||
...(text(item.patch) !== undefined ? { patch: text(item.patch) } : {}),
|
||||
additions: num(item.additions),
|
||||
deletions: num(item.deletions),
|
||||
}
|
||||
}
|
||||
|
||||
export function permissionDiffs(request: PermissionRequest): PermissionFileDiff[] {
|
||||
const direct = clean(request.args?.filediff)
|
||||
if (direct) return [direct]
|
||||
|
||||
const files = request.args?.files
|
||||
if (Array.isArray(files)) {
|
||||
return files.flatMap((item) => {
|
||||
const diff = file(item as File)
|
||||
return diff ? [diff] : []
|
||||
})
|
||||
}
|
||||
|
||||
const patch = text(request.args?.diff)
|
||||
if (!patch) return []
|
||||
const name = text(request.args?.filepath) ?? "patch"
|
||||
return [{ file: name, patch, additions: 0, deletions: 0 }]
|
||||
}
|
||||
@@ -677,10 +677,48 @@ const editPermission: PermissionRequest = {
|
||||
toolName: "edit",
|
||||
patterns: ["src/components/App.tsx", "src/utils/helpers.ts"],
|
||||
always: ["*"],
|
||||
args: {},
|
||||
args: {
|
||||
filediff: {
|
||||
file: "src/components/App.tsx",
|
||||
patch:
|
||||
'===================================================================\n--- src/components/App.tsx\n+++ src/components/App.tsx\n@@ -1,3 +1,4 @@\n import { Button } from "@kilocode/kilo-ui/button"\n+import { Card } from "@kilocode/kilo-ui/card"\n \n export function App() {\n',
|
||||
additions: 1,
|
||||
deletions: 0,
|
||||
},
|
||||
},
|
||||
tool: { messageID: ASST_MSG_ID, callID: "call-edit-001" },
|
||||
}
|
||||
|
||||
const applyPatchPermission: PermissionRequest = {
|
||||
id: "perm-patch-001",
|
||||
sessionID: SESSION_ID,
|
||||
toolName: "edit",
|
||||
patterns: ["src/components/App.tsx", "src/utils/helpers.ts"],
|
||||
always: ["*"],
|
||||
args: {
|
||||
filepath: "src/components/App.tsx, src/utils/helpers.ts",
|
||||
files: [
|
||||
{
|
||||
relativePath: "src/components/App.tsx",
|
||||
type: "update",
|
||||
patch:
|
||||
'===================================================================\n--- src/components/App.tsx\n+++ src/components/App.tsx\n@@ -1,3 +1,4 @@\n import { Button } from "@kilocode/kilo-ui/button"\n+import { Card } from "@kilocode/kilo-ui/card"\n \n export function App() {\n',
|
||||
additions: 1,
|
||||
deletions: 0,
|
||||
},
|
||||
{
|
||||
relativePath: "src/utils/helpers.ts",
|
||||
type: "update",
|
||||
patch:
|
||||
"===================================================================\n--- src/utils/helpers.ts\n+++ src/utils/helpers.ts\n@@ -1,3 +1,3 @@\n export function label(value: string) {\n- return value\n+ return value.trim()\n }\n",
|
||||
additions: 1,
|
||||
deletions: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
tool: { messageID: ASST_MSG_ID, callID: "call-patch-001" },
|
||||
}
|
||||
|
||||
export const PermissionDockEdit: Story = {
|
||||
name: "Permission Dock — edit",
|
||||
render: () => {
|
||||
@@ -701,6 +739,26 @@ export const PermissionDockEdit: Story = {
|
||||
},
|
||||
}
|
||||
|
||||
export const PermissionDockApplyPatch: Story = {
|
||||
name: "Permission Dock - apply patch",
|
||||
render: () => {
|
||||
const perms = [applyPatchPermission]
|
||||
const session = {
|
||||
...mockSessionValue({ id: SESSION_ID, status: "busy", permissions: perms }),
|
||||
messages: () => [{ id: "msg-001" }] as any[],
|
||||
}
|
||||
return (
|
||||
<StoryProviders permissions={perms} sessionID={SESSION_ID} status="busy" noPadding>
|
||||
<SessionContext.Provider value={session as any}>
|
||||
<div style={{ width: "100%", height: "420px", display: "flex", "flex-direction": "column" }}>
|
||||
<ChatView />
|
||||
</div>
|
||||
</SessionContext.Provider>
|
||||
</StoryProviders>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 13. Permission dock — websearch tool
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -110,6 +110,15 @@
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
[data-slot="permission-diffs"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin: 8px 0;
|
||||
max-height: 420px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
[data-slot="permission-diff"] {
|
||||
margin: 8px 0;
|
||||
border: 1px solid var(--border-weak-base);
|
||||
@@ -205,6 +214,13 @@
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
[data-slot="permission-diff-empty"] {
|
||||
padding: 12px;
|
||||
font-size: 12px;
|
||||
color: var(--text-weak, var(--vscode-descriptionForeground));
|
||||
background: var(--vscode-editor-background, #1e1e1e);
|
||||
}
|
||||
|
||||
[data-slot="permission-rules"] {
|
||||
margin: 0;
|
||||
max-height: 160px;
|
||||
|
||||
@@ -22,6 +22,16 @@ export interface PermissionFileDiff {
|
||||
deletions: number
|
||||
}
|
||||
|
||||
export interface PermissionPatchFile {
|
||||
filePath?: string
|
||||
relativePath?: string
|
||||
type?: "add" | "update" | "delete" | "move"
|
||||
patch?: string
|
||||
additions?: number
|
||||
deletions?: number
|
||||
movePath?: string
|
||||
}
|
||||
|
||||
export interface PermissionRequest {
|
||||
id: string
|
||||
sessionID: string
|
||||
@@ -33,6 +43,7 @@ export interface PermissionRequest {
|
||||
diff?: string
|
||||
filepath?: string
|
||||
filediff?: PermissionFileDiff
|
||||
files?: PermissionPatchFile[]
|
||||
}
|
||||
message?: string
|
||||
tool?: { messageID: string; callID: string }
|
||||
|
||||
Reference in New Issue
Block a user