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
22 changes: 22 additions & 0 deletions packages/kilo-vscode/tests/unit/context-requests.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { expect, test } from "bun:test"
import { createContextRequests } from "../../webview-ui/src/hooks/context-requests"

test("context requests settle by ID, time out, and preserve cleanup semantics", async () => {
const ctx = createContextRequests("context", 5, "Timed out")
const first = ctx.request(() => {}).catch((err: Error) => err.message)
const second = ctx.request((id) => ctx.settle(id, (req) => req.resolve("second")))
await expect(second).resolves.toBe("second")
ctx.settle("context-2", () => {
throw new Error("Late settlement")
})
expect(ctx.pending()).toBe(true)
await expect(first).resolves.toBe("Timed out")
expect(ctx.pending()).toBe(false)

for (const reset of [false, true]) {
const result = ctx.request(() => {}).catch((err: Error) => err.message)
ctx.dispose("Cancelled", reset)
await expect(result).resolves.toBe("Cancelled")
expect(ctx.pending()).toBe(!reset)
}
})
49 changes: 49 additions & 0 deletions packages/kilo-vscode/webview-ui/src/hooks/context-requests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { createSignal } from "solid-js"

type Pending = {
resolve: (content: string) => void
reject: (err: Error) => void
timer: ReturnType<typeof setTimeout>
}

export function createContextRequests(prefix: string, timeout: number, expired: string) {
const [pending, setPending] = createSignal(false)
const requests = new Map<string, Pending>()
let counter = 0

const settle = (id: string, run: (req: Pending) => void) => {
const req = requests.get(id)
if (!req) return
clearTimeout(req.timer)
requests.delete(id)
setPending(requests.size > 0)
run(req)
}

const request = (send: (id: string) => void) => {
const id = `${prefix}-${++counter}`
const deferred = Promise.withResolvers<string>()
const timer = setTimeout(() => {
settle(id, (req) => req.reject(new Error(expired)))
}, timeout)
requests.set(id, { resolve: deferred.resolve, reject: deferred.reject, timer })
setPending(true)
try {
send(id)
} catch (err) {
deferred.reject(err)
}
return deferred.promise
}

const dispose = (message: string, reset = false) => {
for (const req of requests.values()) {
clearTimeout(req.timer)
req.reject(new Error(message))
}
requests.clear()
if (reset) setPending(false)
}

return { pending, settle, request, dispose }
}
57 changes: 11 additions & 46 deletions packages/kilo-vscode/webview-ui/src/hooks/useGitChangesContext.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
import { createSignal, onCleanup } from "solid-js"
import { onCleanup } from "solid-js"
import type { Accessor } from "solid-js"
import type { ExtensionMessage, FileAttachment, WebviewMessage } from "../types/messages"
import { buildGitChangesAttachment, hasGitChangesMention } from "./git-changes-context-utils"

const GIT_CHANGES_TIMEOUT_MS = 15_000

type Pending = {
resolve: (content: string) => void
reject: (err: Error) => void
timer: ReturnType<typeof setTimeout>
}
import { createContextRequests } from "./context-requests"

interface VSCodeContext {
postMessage: (message: WebviewMessage) => void
Expand All @@ -26,66 +19,38 @@ export function useGitChangesContext(
context?: Accessor<string | undefined>,
git?: Accessor<boolean>,
): GitChangesContext {
const [pending, setPending] = createSignal(false)
const requests = new Map<string, Pending>()
let counter = 0

const settle = (requestId: string, run: (req: Pending) => void) => {
const req = requests.get(requestId)
if (!req) return

clearTimeout(req.timer)
requests.delete(requestId)
setPending(requests.size > 0)
run(req)
}
const requests = createContextRequests("git-changes-context", 15_000, "Timed out while reading git changes")

const unsubscribe = vscode.onMessage((message) => {
if (message.type === "gitChangesContextResult") {
settle(message.requestId, (req) => req.resolve(message.content))
requests.settle(message.requestId, (req) => req.resolve(message.content))
return
}

if (message.type === "gitChangesContextError") {
settle(message.requestId, (req) => req.reject(new Error(message.error)))
requests.settle(message.requestId, (req) => req.reject(new Error(message.error)))
}
})

onCleanup(() => {
unsubscribe()
for (const req of requests.values()) {
clearTimeout(req.timer)
req.reject(new Error("Git changes context request cancelled"))
}
requests.clear()
setPending(false)
requests.dispose("Git changes context request cancelled", true)
})

const request = (sessionID?: string, scope?: string) =>
new Promise<string>((resolve, reject) => {
counter++
const requestId = `git-changes-context-${counter}`
const timer = setTimeout(() => {
settle(requestId, (req) => req.reject(new Error("Timed out while reading git changes")))
}, GIT_CHANGES_TIMEOUT_MS)
const resolveAttachment = async (text: string, sessionID?: string, scope?: string) => {
if (!hasGitChangesMention(text)) return undefined
if (git?.() === false) return undefined

requests.set(requestId, { resolve, reject, timer })
setPending(true)
const content = await requests.request((requestId) => {
vscode.postMessage({
type: "requestGitChangesContext",
requestId,
sessionID,
agentManagerContext: scope ?? context?.(),
})
})

const resolveAttachment = async (text: string, sessionID?: string, scope?: string) => {
if (!hasGitChangesMention(text)) return undefined
if (git?.() === false) return undefined

const content = await request(sessionID, scope)
return buildGitChangesAttachment(text, content)
}

return { pending, resolveAttachment }
return { pending: requests.pending, resolveAttachment }
}
58 changes: 12 additions & 46 deletions packages/kilo-vscode/webview-ui/src/hooks/useTerminalContext.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,9 @@
import { createSignal, onCleanup } from "solid-js"
import { onCleanup } from "solid-js"
import type { Accessor } from "solid-js"
import type { FileAttachment } from "../types/messages"
import { useVSCode } from "../context/vscode"
import { buildTerminalAttachment, hasTerminalMention } from "./terminal-context-utils"

const TERMINAL_CONTEXT_TIMEOUT_MS = 10_000

type Pending = {
resolve: (content: string) => void
reject: (err: Error) => void
timer: ReturnType<typeof setTimeout>
}
import { createContextRequests } from "./context-requests"

type EmbeddedResolver = (context?: string) => Promise<string | undefined>

Expand All @@ -21,50 +14,28 @@ export interface TerminalContext {

export function useTerminalContext(embedded?: EmbeddedResolver): TerminalContext {
const vscode = useVSCode()
const [pending, setPending] = createSignal(false)
const requests = new Map<string, Pending>()
let counter = 0

const settle = (requestId: string, run: (req: Pending) => void) => {
const req = requests.get(requestId)
if (!req) return

clearTimeout(req.timer)
requests.delete(requestId)
setPending(requests.size > 0)
run(req)
}
const requests = createContextRequests("terminal-context", 10_000, "Timed out while reading terminal output")

const unsubscribe = vscode.onMessage((message) => {
if (message.type === "terminalContextResult") {
settle(message.requestId, (req) => req.resolve(message.content))
requests.settle(message.requestId, (req) => req.resolve(message.content))
return
}

if (message.type === "terminalContextError") {
settle(message.requestId, (req) => req.reject(new Error(message.error)))
requests.settle(message.requestId, (req) => req.reject(new Error(message.error)))
}
})

onCleanup(() => {
unsubscribe()
for (const req of requests.values()) {
clearTimeout(req.timer)
req.reject(new Error("Terminal context request cancelled"))
}
requests.clear()
requests.dispose("Terminal context request cancelled")
})

const request = (sessionID?: string, context?: string) =>
new Promise<string>((resolve, reject) => {
counter++
const requestId = `terminal-context-${counter}`
const timer = setTimeout(() => {
settle(requestId, (req) => req.reject(new Error("Timed out while reading terminal output")))
}, TERMINAL_CONTEXT_TIMEOUT_MS)
const resolveAttachment = async (text: string, sessionID?: string, context?: string) => {
if (!hasTerminalMention(text)) return undefined

requests.set(requestId, { resolve, reject, timer })
setPending(true)
const content = await requests.request((requestId) => {
if (!embedded) {
vscode.postMessage({ type: "requestTerminalContext", requestId, sessionID, agentManagerContext: context })
return
Expand All @@ -75,21 +46,16 @@ export function useTerminalContext(embedded?: EmbeddedResolver): TerminalContext
vscode.postMessage({ type: "requestTerminalContext", requestId, sessionID, agentManagerContext: context })
return
}
settle(requestId, (req) => req.resolve(content))
requests.settle(requestId, (req) => req.resolve(content))
},
(error: unknown) => {
settle(requestId, (req) => req.reject(error instanceof Error ? error : new Error(String(error))))
requests.settle(requestId, (req) => req.reject(error instanceof Error ? error : new Error(String(error))))
},
)
})

const resolveAttachment = async (text: string, sessionID?: string, context?: string) => {
if (!hasTerminalMention(text)) return undefined

const content = await request(sessionID, context)
if (!content.trim()) throw new Error("No terminal content available")
return buildTerminalAttachment(text, content)
}

return { pending, resolveAttachment }
return { pending: requests.pending, resolveAttachment }
}
2 changes: 1 addition & 1 deletion packages/kilo-vscode/webview-ui/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"lib": ["ES2023", "ES2024.Promise", "DOM", "DOM.Iterable"],
"jsx": "preserve",
"jsxImportSource": "solid-js",
"strict": true,
Expand Down
12 changes: 0 additions & 12 deletions script/kilocode-duplication-allowlist.json
Original file line number Diff line number Diff line change
Expand Up @@ -305,18 +305,6 @@
"owner": "kilo-vscode",
"reason": "Existing duplication before the ratchet; remove through a focused, behavior-preserving extraction."
},
{
"files": [
"packages/kilo-vscode/webview-ui/src/hooks/useGitChangesContext.ts",
"packages/kilo-vscode/webview-ui/src/hooks/useTerminalContext.ts"
],
"fingerprint": "3474d577ea4814b0",
"maxMatches": 1,
"maxTokens": 105,
"kind": "legacy",
"owner": "kilo-vscode",
"reason": "Existing duplication before the ratchet; remove through a focused, behavior-preserving extraction."
},
{
"files": ["packages/opencode/src/kilocode/agent/index.ts", "packages/opencode/src/kilocode/agent/index.ts"],
"fingerprint": "9044b540f896a694",
Expand Down
Loading