fix(vscode): render heredoc approvals as plain text (#12304)

* fix(vscode): render heredoc approvals as plain text

* test: handle heredoc approvals across platforms

* chore: update kilo-vscode visual regression baselines

---------

Co-authored-by: kilo-maintainer[bot] <kilo-maintainer[bot]@users.noreply.github.com>
This commit is contained in:
Marius
2026-07-20 09:51:05 +02:00
committed by GitHub
parent 1687d42c44
commit 79fe75745f
9 changed files with 67 additions and 7 deletions
+6
View File
@@ -0,0 +1,6 @@
---
"@kilocode/cli": patch
"kilo-code": patch
---
Display here-document content as plain text in terminal approval prompts.
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:31a447c3602b2959fde3da003baa05901d4d68e935f245c63d7080a574ef7b53
size 22366
oid sha256:e7905a190dff49bc7e23af1773ea93ac79cc71659336469745561355cba5f413
size 22602
@@ -10,7 +10,7 @@ import { Icon } from "@kilocode/kilo-ui/icon"
import { Tooltip } from "@kilocode/kilo-ui/tooltip"
import { useLanguage } from "../../context/language"
export const PermissionCommand: Component<{ command: string }> = (props) => {
export const PermissionCommand: Component<{ command: string; plain?: boolean }> = (props) => {
const language = useLanguage()
const [copied, setCopied] = createSignal(false)
const state = { signal: { aborted: false } }
@@ -23,10 +23,11 @@ export const PermissionCommand: Component<{ command: string }> = (props) => {
const pre = document.createElement("pre")
const code = document.createElement("code")
code.dataset.lang = "shellscript"
if (!props.plain) code.dataset.lang = "shellscript"
code.textContent = command
pre.append(code)
ref.replaceChildren(pre)
if (props.plain) return
const signal = { aborted: false }
state.signal = signal
@@ -269,7 +269,9 @@ export const PermissionDock: Component<{
}
>
<Show when={cmdDescription()}>{(desc) => <div data-slot="permission-hint">{desc()}</div>}</Show>
<Show when={command()}>{(cmd) => <PermissionCommand command={cmd()} />}</Show>
<Show when={command()}>
{(cmd) => <PermissionCommand command={cmd()} plain={props.request.args.heredoc === true} />}
</Show>
{(() => {
const desc = description()
@@ -1199,6 +1199,7 @@ print(f"Entries with audio_file set: {found_audio}")
print(f"Missing audio_file: {len(expected) - found_audio}")
EOF`,
rules: ["python3 *"],
heredoc: true,
},
tool: { messageID: ASST_MSG_ID, callID: "call-heredoc-001" },
}
@@ -43,6 +43,7 @@ export interface PermissionRequest {
filediff?: PermissionFileDiff
files?: PermissionPatchFile[]
description?: string
heredoc?: boolean
}
message?: string
tool?: { messageID: string; callID: string }
@@ -0,0 +1,7 @@
import type { ShellID } from "@/tool/shell/id"
import type { Node } from "web-tree-sitter"
export function heredocs(root: Node, kind: ShellID.Kind) {
if (kind !== "bash") return {}
return root.descendantsOfType("heredoc_redirect").length > 0 ? { heredoc: true } : {}
}
+6 -2
View File
@@ -19,6 +19,7 @@ import * as Truncate from "./truncate"
import { Plugin } from "@/plugin"
import { normalizeUrls } from "@/kilocode/util/url" // kilocode_change
import { CommandTimeout } from "@/kilocode/command-timeout" // kilocode_change
import { heredocs } from "@/kilocode/tool/shell-heredoc" // kilocode_change
import { ChildProcess } from "effect/unstable/process"
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
import { ShellPrompt, type Parameters } from "./shell/prompt"
@@ -282,6 +283,7 @@ const ask = Effect.fn("ShellTool.ask")(function* (
ctx: Tool.Context,
scan: Scan,
command: string,
metadata: ReturnType<typeof heredocs>, // kilocode_change
description?: string, // kilocode_change
) {
// kilocode_change
@@ -302,6 +304,7 @@ const ask = Effect.fn("ShellTool.ask")(function* (
directories,
patterns: globs,
...(scan.access === "read" ? { access: "read" as const } : {}),
...metadata,
},
// kilocode_change end
})
@@ -312,7 +315,7 @@ const ask = Effect.fn("ShellTool.ask")(function* (
permission: ShellID.ToolID,
patterns: Array.from(scan.patterns),
always: Array.from(scan.always),
metadata: { command: normalizeUrls(command), ...(description ? { description } : {}) }, // kilocode_change
metadata: { command: normalizeUrls(command), ...(description ? { description } : {}), ...metadata }, // kilocode_change
})
})
@@ -411,11 +414,12 @@ export const ShellPermission = Effect.gen(function* () {
Effect.gen(function* () {
const tree = yield* Effect.acquireRelease(parse(input.command, ps), (tree) => Effect.sync(() => tree.delete()))
const scan = yield* collect(tree.rootNode, input.cwd, ps, input.shell, instance)
const metadata = heredocs(tree.rootNode, ShellID.toKind(Shell.name(input.shell))) // kilocode_change
if (!containsPath(input.cwd, instance)) {
scan.dirs.add(input.cwd)
scan.access = "unknown"
}
yield* ask(ctx, scan, input.command, input.description)
yield* ask(ctx, scan, input.command, metadata, input.description) // kilocode_change
}),
)
})
@@ -65,4 +65,42 @@ describe("bash permission metadata.command", () => {
},
})
})
test.skipIf(process.platform === "win32").each([
["single quoted", "cat << 'EOF'\n$HOME\nEOF"],
["double quoted", 'cat << "EOF"\n$HOME\nEOF'],
["escaped", "cat << \\EOF\n$HOME\nEOF"],
["unquoted", "cat << EOF\n$HOME\nEOF"],
] as const)("marks %s heredocs", async (_, command) => {
await using tmp = await tmpdir()
await provideTestInstance({
directory: tmp.path,
fn: async () => {
const bash = await runtime.runPromise(ShellTool.pipe(Effect.flatMap((info) => info.init())))
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
await Effect.runPromise(bash.execute({ command }, capture(requests)))
const req = requests.find((item) => item.permission === "bash")
expect(req?.metadata.heredoc).toBe(true)
expect(req?.metadata.command).toBe(command)
expect(req?.patterns).toEqual([command])
expect(req?.always).toEqual(["cat *"])
},
})
})
test("omits heredoc metadata for ordinary commands", async () => {
await using tmp = await tmpdir()
await provideTestInstance({
directory: tmp.path,
fn: async () => {
const bash = await runtime.runPromise(ShellTool.pipe(Effect.flatMap((info) => info.init())))
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
await Effect.runPromise(bash.execute({ command: "echo hello" }, capture(requests)))
const req = requests.find((item) => item.permission === "bash")
expect(req?.metadata.heredoc).toBeUndefined()
},
})
})
})