Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/dismiss-stale-permissions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---

Dismiss stale permission prompts when another view has already answered them.
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,16 @@ export function recoverablePermissions(perms: RecoverablePermission[], tracked:
}

function isNotFoundError(error: unknown): boolean {
if (!error || typeof error !== "object") return false
const obj = error as Record<string, unknown>
if (obj.name === "NotFoundError") return true
if (typeof obj.status === "number" && obj.status === 404) return true
const data = obj.data as Record<string, unknown> | undefined
return data?.name === "NotFoundError"
const record = (value: unknown) =>
value && typeof value === "object" ? (value as Record<string, unknown>) : undefined
const obj = record(error)
if (!obj) return false

const cause = record(obj.cause)
const body = record(cause?.body)
return [obj, record(obj.data), cause, body, record(body?.data)].some(
(value) => value?.name === "NotFoundError" || value?.status === 404,
)
}

/**
Expand Down
62 changes: 59 additions & 3 deletions packages/kilo-vscode/tests/unit/permission-recovery.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, it, expect } from "bun:test"
import { describe, it, expect, spyOn } from "bun:test"
import {
fetchAndSendPendingPermissions,
handlePermissionResponse,
Expand Down Expand Up @@ -26,6 +26,7 @@ function permissionClient(
queries: string[],
saves: unknown[] = [],
replies: unknown[] = [],
errors?: { save?: unknown; reply?: unknown },
) {
return {
permission: {
Expand All @@ -36,10 +37,12 @@ function permissionClient(
},
saveAlwaysRules: async (args: unknown) => {
saves.push(args)
if (errors?.save) throw errors.save
return { data: true }
},
reply: async (args: unknown) => {
replies.push(args)
if (errors?.reply) throw errors.reply
return { data: true }
},
},
Expand All @@ -51,22 +54,24 @@ function client(
queries: string[],
saves: unknown[] = [],
replies: unknown[] = [],
errors?: { save?: unknown; reply?: unknown },
): PermissionContext["client"] {
return permissionClient(permsPerDir, queries, saves, replies) as unknown as PermissionContext["client"]
return permissionClient(permsPerDir, queries, saves, replies, errors) as unknown as PermissionContext["client"]
}

function ctx(opts: {
tracked: string[]
dirs?: Map<string, string>
permsPerDir?: Record<string, ReturnType<typeof pending>[]>
workspace?: string
errors?: { save?: unknown; reply?: unknown }
}) {
const messages: unknown[] = []
const queries: string[] = []
const saves: unknown[] = []
const replies: unknown[] = []
const perms = opts.permsPerDir ?? {}
const sdk = client(perms, queries, saves, replies)
const sdk = client(perms, queries, saves, replies, opts.errors)

const permDirs = new Map<string, string>()
const fake: PermissionContext = {
Expand Down Expand Up @@ -136,6 +141,57 @@ describe("handlePermissionResponse", () => {
])
expect(replies).toEqual([{ requestID: "p1", reply: "reject", directory: "/workspace/.kilo/worktrees/feature" }])
})

it("treats an SDK-wrapped 404 while saving rules as stale", async () => {
const error = new Error("Permission request not found: p1", {
cause: { status: 404, body: { name: "NotFoundError" } },
})
const { fake, messages, saves, replies, permDirs } = ctx({ tracked: ["s1"], errors: { save: error } })
permDirs.set("p1", "/workspace/.kilo/worktrees/feature")

await handlePermissionResponse(fake, "p1", "s1", "once", ["bun *"], [])

expect(saves).toEqual([
{
requestID: "p1",
directory: "/workspace/.kilo/worktrees/feature",
approvedAlways: ["bun *"],
deniedAlways: [],
},
])
expect(replies).toEqual([])
expect(permDirs.has("p1")).toBe(false)
expect(messages).toEqual([{ type: "permissionError", permissionID: "p1", stale: true }])
})

it("treats an SDK-wrapped 404 while replying as stale", async () => {
const error = new Error("Permission request not found: p1", {
cause: { status: 404, body: { name: "NotFoundError" } },
})
const { fake, messages, replies, permDirs } = ctx({ tracked: ["s1"], errors: { reply: error } })
permDirs.set("p1", "/workspace/.kilo/worktrees/feature")

await handlePermissionResponse(fake, "p1", "s1", "once", [], [])

expect(replies).toEqual([{ requestID: "p1", reply: "once", directory: "/workspace/.kilo/worktrees/feature" }])
expect(permDirs.has("p1")).toBe(false)
expect(messages).toEqual([{ type: "permissionError", permissionID: "p1", stale: true }])
})

it("does not treat other SDK-wrapped errors as stale", async () => {
const error = new Error("Internal server error", {
cause: { status: 500, body: { name: "InternalServerError" } },
})
const { fake, messages, permDirs } = ctx({ tracked: ["s1"], errors: { reply: error } })
const spy = spyOn(console, "error").mockImplementation(() => {})
permDirs.set("p1", "/workspace/.kilo/worktrees/feature")

await handlePermissionResponse(fake, "p1", "s1", "once", [], [])
spy.mockRestore()

expect(permDirs.has("p1")).toBe(true)
expect(messages).toEqual([{ type: "permissionError", permissionID: "p1" }])
})
})

describe("recoverablePermissions", () => {
Expand Down
Loading