diff --git a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts index bdd1473cbc..d49d360b92 100644 --- a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts +++ b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts @@ -249,7 +249,7 @@ export class PRStatusPoller { const pr = await this.cachedFetchPR(wt.branch, wt.path) if (!pr || this.stale(generation)) { if (this.stale(generation)) return - const hash = `${worktreeId}:none` + const hash = `${worktreeId}:${wt.branch}:none` if (this.lastHash.get(worktreeId) === hash) return this.lastHash.set(worktreeId, hash) this.options.onStatus(worktreeId, null) diff --git a/packages/kilo-vscode/src/agent-manager/pr/pr-comment-context.ts b/packages/kilo-vscode/src/agent-manager/pr/pr-comment-context.ts index 5a2d8af710..6862aaabeb 100644 --- a/packages/kilo-vscode/src/agent-manager/pr/pr-comment-context.ts +++ b/packages/kilo-vscode/src/agent-manager/pr/pr-comment-context.ts @@ -4,7 +4,7 @@ * the file, so the lines below the comment are read from disk and attached to * the thread. */ -import { readFile, stat } from "node:fs/promises" +import { readFile, realpath, stat } from "node:fs/promises" import path from "node:path" import type { PRComment } from "../types" @@ -32,7 +32,11 @@ function anchor(hunk: string): string | undefined { } async function lines(dir: string, file: string): Promise { - const full = path.join(dir, file) + const root = await realpath(dir).catch(() => undefined) + const full = await realpath(path.resolve(dir, file)).catch(() => undefined) + if (!root || !full) return undefined + const rel = path.relative(root, full) + if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) return undefined const info = await stat(full).catch(() => undefined) if (!info?.isFile() || info.size > SIZE) return undefined const hit = cache.get(full) diff --git a/packages/kilo-vscode/tests/unit/pr-comment-context.test.ts b/packages/kilo-vscode/tests/unit/pr-comment-context.test.ts index 0e452042f5..c815b2bdaf 100644 --- a/packages/kilo-vscode/tests/unit/pr-comment-context.test.ts +++ b/packages/kilo-vscode/tests/unit/pr-comment-context.test.ts @@ -99,6 +99,14 @@ describe("withContext", () => { expect(items.map((item) => item.after)).toEqual([undefined, undefined, undefined, undefined]) }) + it("rejects a comment path outside the worktree", async () => { + const dir = await repo() + await writeFile(path.join(path.dirname(dir), "outside.ts"), SOURCE.join("\n")) + const [item] = await withContext(dir, [thread({ file: "../outside.ts" })]) + + expect(item!.after).toBeUndefined() + }) + it("keeps every thread, in order, whatever the files say", async () => { const dir = await repo() const items = await withContext(dir, [thread({ threadId: "a" }), thread({ threadId: "b", outdated: true })]) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRPanel.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRPanel.tsx index d7a1facd45..35708e592f 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRPanel.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRPanel.tsx @@ -102,7 +102,7 @@ export const PRPanel: Component = (props) => { // expanded card and the scroll position, so the last list for this PR stays. const comments = createMemo<{ number: number; value: NonNullable } | undefined>((prev) => { const next = props.pr.comments - if (next?.total) return { number: props.pr.number, value: next } + if (next) return { number: props.pr.number, value: next } if (prev && prev.number === props.pr.number) return prev return undefined })