From 039d2336791ec32a14e3399fa1ef328110d56053 Mon Sep 17 00:00:00 2001 From: cmanu Date: Mon, 3 Aug 2026 11:23:50 -0700 Subject: [PATCH 01/73] feat(pr-viewer): View PR with mock data in AM --- .../src/agent-manager/PRStatusPoller.ts | 112 +++++---- .../kilo-vscode/src/agent-manager/types.ts | 8 + .../agent-manager/AgentManagerApp.tsx | 31 ++- .../webview-ui/agent-manager/PRPanel.tsx | 210 +++++++++++++++++ .../webview-ui/agent-manager/TabBar.tsx | 19 +- .../agent-manager/agent-manager.css | 221 ++++++++++++++++++ .../src/types/messages/agent-manager.ts | 8 + packages/ui/src/components/icon.tsx | 1 + 8 files changed, 566 insertions(+), 44 deletions(-) create mode 100644 packages/kilo-vscode/webview-ui/agent-manager/PRPanel.tsx diff --git a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts index 155bd410bba..c45cd992beb 100644 --- a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts +++ b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts @@ -1,6 +1,6 @@ import type { ExecFileOptionsWithStringEncoding } from "child_process" import type { Worktree } from "./WorktreeStateManager" -import type { PRStatus, PRCheck, PRComment, CheckStatus, AggregateCheckStatus, PRState, ReviewDecision } from "./types" +import type { PRStatus, PRCheck, PRComment, PRReviewer, CheckStatus, AggregateCheckStatus, PRState, ReviewDecision } from "./types" import { execWithShellEnv } from "./shell-env" import { classifyPRError } from "./git-import" import type { Semaphore } from "./semaphore" @@ -244,11 +244,12 @@ export class PRStatusPoller { const status: PRStatus = { number: pr.number, title: pr.title, + body: pr.body, url: pr.url, state: pr.state, review: pr.review, checks, - ...(comments && { comments }), + ...(comments && { comments, reviewers: comments.reviewers }), additions: pr.additions, deletions: pr.deletions, files: pr.files, @@ -283,7 +284,7 @@ export class PRStatusPoller { } private static readonly PR_JSON_FIELDS = - "number,title,url,state,isDraft,reviewDecision,additions,deletions,changedFiles,headRefName,headRefOid" + "number,title,body,url,state,isDraft,reviewDecision,additions,deletions,changedFiles,headRefName,headRefOid" /** Return a cached PR lookup if still fresh, otherwise fetch and cache. * Keyed by branch name so multiple worktrees on the same branch share @@ -420,10 +421,9 @@ export class PRStatusPoller { private async fetchComments( prNumber: number, cwd: string, - ): Promise<{ total: number; unresolved: number; items: PRComment[] }> { + ): Promise<{ total: number; unresolved: number; items: PRComment[]; reviewers: PRReviewer[] }> { try { const repo = await this.getRepoInfo(cwd) - const query = `query($owner: String!, $repo: String!, $number: Int!) { repository(owner: $owner, name: $repo) { pullRequest(number: $number) { @@ -443,52 +443,27 @@ export class PRStatusPoller { } } } + reviewRequests(first: 20) { + nodes { requestedReviewer { ... on User { login avatarUrl } } } + } + reviews(first: 20, states: [APPROVED, CHANGES_REQUESTED, COMMENTED]) { + nodes { author { login avatarUrl } state } + } } } }` - const { stdout } = await this.shell( "gh", - [ - "api", - "graphql", - "-f", - `query=${query}`, - "-F", - `owner=${repo.owner}`, - "-F", - `repo=${repo.name}`, - "-F", - `number=${prNumber}`, - ], + ["api", "graphql", "-f", `query=${query}`, "-F", `owner=${repo.owner}`, "-F", `repo=${repo.name}`, "-F", `number=${prNumber}`], { cwd, timeout: 15_000 }, ) - const result = JSON.parse(stdout) - const threads = result?.data?.repository?.pullRequest?.reviewThreads?.nodes ?? [] - - const items: PRComment[] = [] - for (const thread of threads) { - const first = thread.comments?.nodes?.[0] - if (!first) continue - items.push({ - id: first.id, - author: first.author?.login ?? "unknown", - avatar: first.author?.avatarUrl, - body: first.body ?? "", - file: first.path, - line: first.line, - url: first.url, - resolved: thread.isResolved ?? false, - createdAt: first.createdAt ? new Date(first.createdAt).getTime() : undefined, - }) - } - - const total = items.length - const unresolved = items.filter((c) => !c.resolved).length - return { total, unresolved, items } + const pr = JSON.parse(stdout)?.data?.repository?.pullRequest + const items = parseComments(pr?.reviewThreads?.nodes ?? []) + const reviewers = parseReviewers(pr?.reviewRequests?.nodes ?? [], pr?.reviews?.nodes ?? []) + return { total: items.length, unresolved: items.filter((c) => !c.resolved).length, items, reviewers } } catch (err) { this.options.log("Failed to fetch PR comments:", err) - return { total: 0, unresolved: 0, items: [] } + return { total: 0, unresolved: 0, items: [], reviewers: [] } } } } @@ -496,6 +471,7 @@ export class PRStatusPoller { interface PRResult { number: number title: string + body: string url: string state: PRState review: ReviewDecision | null @@ -510,6 +486,7 @@ function parsePRResult(json: string): PRResult | null { return { number: data.number, title: data.title ?? "", + body: data.body ?? "", url: data.url ?? "", state: parsePRState(data.isDraft, data.state), review: parseReviewDecision(data.reviewDecision), @@ -564,6 +541,57 @@ function formatCheckDuration(startedAt?: string, completedAt?: string): string | return secs < 60 ? `${secs}s` : `${Math.floor(secs / 60)}m ${secs % 60}s` } +function parseComments(threads: Record[]): PRComment[] { + const items: PRComment[] = [] + for (const thread of threads) { + const nodes = (thread.comments as { nodes?: unknown[] } | undefined)?.nodes + const first = nodes?.[0] as Record | undefined + if (!first) continue + const author = first.author as Record | undefined + items.push({ + id: first.id as string, + author: (author?.login as string) ?? "unknown", + avatar: author?.avatarUrl as string | undefined, + body: (first.body as string) ?? "", + file: first.path as string | undefined, + line: first.line as number | undefined, + url: first.url as string | undefined, + resolved: (thread.isResolved as boolean) ?? false, + createdAt: first.createdAt ? new Date(first.createdAt as string).getTime() : undefined, + }) + } + return items +} + +function parseReviewers( + requests: Record[], + reviews: Record[], +): PRReviewer[] { + const map = new Map() + for (const node of requests) { + const user = node.requestedReviewer as Record | undefined + if (!user?.login) continue + map.set(user.login as string, { login: user.login as string, avatar: user.avatarUrl as string | undefined, state: "pending" }) + } + for (const node of reviews) { + const author = node.author as Record | undefined + const login = author?.login as string | undefined + if (!login) continue + const state = REVIEWER_STATE[node.state as string] ?? "pending" + // Keep most significant: approved/changes_requested > commented > pending + if (!map.has(login) || state !== "commented") { + map.set(login, { login, avatar: author?.avatarUrl as string | undefined, state }) + } + } + return [...map.values()] +} + +const REVIEWER_STATE: Record = { + APPROVED: "approved", + CHANGES_REQUESTED: "changes_requested", + COMMENTED: "commented", +} + /** Run async thunks with bounded concurrency, returning settled results. */ async function settled(thunks: (() => Promise)[], concurrency: number): Promise[]> { const results: PromiseSettledResult[] = new Array(thunks.length) diff --git a/packages/kilo-vscode/src/agent-manager/types.ts b/packages/kilo-vscode/src/agent-manager/types.ts index 3a2b555e355..44e2c3eee68 100644 --- a/packages/kilo-vscode/src/agent-manager/types.ts +++ b/packages/kilo-vscode/src/agent-manager/types.ts @@ -71,9 +71,16 @@ export interface PRComment { createdAt?: number } +export interface PRReviewer { + login: string + avatar?: string + state: "approved" | "changes_requested" | "pending" | "commented" +} + export interface PRStatus { number: number title: string + body?: string url: string state: PRState review: ReviewDecision | null @@ -90,6 +97,7 @@ export interface PRStatus { unresolved: number items: PRComment[] } + reviewers?: PRReviewer[] additions: number deletions: number files: number diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index 939a8e505be..b10f83e6d6f 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -141,6 +141,7 @@ import { import { focusCurrentTab, renderTab, renderTerminalLayer, renderNewTabButton } from "./tab-rendering" import { useTabScroll } from "./tab-scroll" import { DiffPanel } from "./DiffPanel" +import { PRPanel } from "./PRPanel" import { createRevertFile } from "./revert-file" import { FullScreenDiffView } from "../diff-viewer/FullScreenDiffView" import { createApplyToLocal } from "./apply-to-local" @@ -308,6 +309,7 @@ const AgentManagerContent: Component = () => { const [history, setHistory] = createSignal(false) const [sidePanel, setSidePanel] = createSignal(null) const diffOpen = () => sidePanel() === "diff" + const prOpen = () => sidePanel() === "pr" const diffs = createWorktreeDiffs(vscode) const diffDatas = diffs.diffDatas const diffLoading = diffs.diffLoading @@ -553,7 +555,13 @@ const AgentManagerContent: Component = () => { const sel = selection() if (!sel || sel === LOCAL || !prStatuses()[sel]) return metrics.track("open_pull_request", "keyboard_shortcut") - vscode.postMessage({ type: "agentManager.openPR", worktreeId: sel }) + togglePRPanel() + } + + const togglePRPanel = () => { + setHistory(false) + setReviewActive(false) + setSidePanel((prev) => (prev === "pr" ? null : "pr")) } const runWorktree = (id: string, destination: TerminalDestination) => { @@ -2448,6 +2456,13 @@ const AgentManagerContent: Component = () => { reviewActive={reviewActive} onToggleDiff={toggleDiffPanel} onToggleReview={metrics.click("fullscreen_review", "tab_toolbar", toggleReviewTab)} + prStatus={() => { + const sel = selection() + if (!sel || sel === LOCAL) return undefined + return prStatuses()[sel] + }} + prOpen={prOpen} + onTogglePR={togglePRPanel} terminalDestination={sideCtl.destination} terminalDestinationActive={() => sidePanel() === "terminal"} terminalKeybind={() => kb().showTerminal ?? ""} @@ -2686,6 +2701,20 @@ const AgentManagerContent: Component = () => { activeTerminalId={terms.activeId()} /> + + {(() => { + const sel = selection() + const pr = sel && sel !== LOCAL ? prStatuses()[sel] : undefined + if (!pr) return null + return ( + setSidePanel(null)} + onOpenExternal={() => vscode.postMessage({ type: "agentManager.openPR", worktreeId: sel as string })} + /> + ) + })()} + void + onOpenExternal: () => void +} + +const STATE_LABEL: Record = { + open: "Open", + draft: "Draft", + merged: "Merged", + closed: "Closed", +} + +const REVIEW_LABEL: Partial, string>> = { + approved: "Approved", + changes_requested: "Changes Requested", + pending: "Review Pending", +} + +const CHECK_ICON: Record = { + success: "circle-check", + failure: "circle-x", + cancelled: "circle-x", + skipped: "circle-x", + pending: "play", +} + +const CHECK_LABEL: Record = { + success: "Passed", + failure: "Failed", + pending: "Running", + skipped: "Skipped", + cancelled: "Cancelled", +} + +const REVIEWER_STATE_ICON: Record = { + approved: "circle-check", + changes_requested: "refresh", + commented: "edit", + pending: "clock", +} + +const REVIEWER_STATE_LABEL: Record = { + approved: "Approved", + changes_requested: "Changes requested", + commented: "Commented", + pending: "Awaiting", +} + +export const PRPanel: Component = (props) => { + const accent = () => prAccentColor(props.pr) + const indicator = () => prBadgeIndicator(props.pr) + const pulsing = () => prChecksRunning(props.pr) + + return ( +
+
+
+ + + #{props.pr.number} + + {props.pr.title} +
+
+ + + + + + +
+
+ +
+ {/* State + review */} +
+
+ Status + {STATE_LABEL[props.pr.state]} +
+ + {(review) => ( +
+ Review + {REVIEW_LABEL[review()]} +
+ )} +
+
+ + {/* Description */} + +
+
+
{props.pr.body}
+
+ + + {/* Reviewers */} + 0}> +
+
+
Reviewers
+
+ + {(reviewer) => ( +
+ + + {REVIEWER_STATE_LABEL[reviewer.state]} +
+ )} +
+
+
+ + + {/* Diff stats */} + 0 || props.pr.additions > 0 || props.pr.deletions > 0}> +
+
+
+ Changes + + 0}> + {props.pr.files}f + + 0}> + +{props.pr.additions} + + 0}> + −{props.pr.deletions} + + +
+
+ + + {/* Checks */} + 0}> +
+
+
+ Checks + + {props.pr.checks.passed}/{props.pr.checks.total} passed + +
+
+ + {(check) => ( +
+ + {check.name} + {CHECK_LABEL[check.status]} + + {check.duration} + + + e.preventDefault()}> + + + +
+ )} +
+
+
+ + + {/* Comments */} + 0}> +
+
+
+ Comments + + {props.pr.comments!.total} + 0}> + {" "} + ({props.pr.comments!.unresolved} unresolved) + + +
+
+ +
+
+ ) +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/TabBar.tsx b/packages/kilo-vscode/webview-ui/agent-manager/TabBar.tsx index fc1a36b462b..f3c1ddda1c3 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/TabBar.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/TabBar.tsx @@ -13,7 +13,7 @@ import { closestCenter, type DragEvent, } from "@thisbeyond/solid-dnd" -import type { LocalGitStats, RunStatus, WorktreeGitStats } from "../src/types/messages" +import type { LocalGitStats, RunStatus, WorktreeGitStats, PRStatus } from "../src/types/messages" import type { LanguageContextValue } from "../src/context/language" import { LOCAL } from "./navigate" import { ConstrainDragYAxis } from "../src/components/chat/TabDnd" @@ -55,6 +55,9 @@ export interface TabBarProps { reviewActive: () => boolean onToggleDiff: () => void onToggleReview: () => void + prStatus: () => PRStatus | null | undefined + prOpen: () => boolean + onTogglePR: () => void terminalDestination: () => TerminalDestination terminalDestinationActive: () => boolean terminalKeybind: () => string @@ -241,6 +244,20 @@ export const TabBar: Component = (props) => ( ) })()} + + {(pr) => ( + + + + )} + `, warning: ``, reset: ``, + refresh: ``, link: ``, providers: ``, layers: ``, From 0a78fedd28eadcac51661c8c81983d8b16176ce0 Mon Sep 17 00:00:00 2001 From: cmanu Date: Mon, 3 Aug 2026 13:16:00 -0700 Subject: [PATCH 02/73] feat(pr-viewer): Update styles and add markdown renderer for PR description --- .../src/agent-manager/pr-status-bridge.ts | 4 +- .../kilo-vscode/src/agent-manager/types.ts | 1 + .../agent-manager/AgentManagerApp.tsx | 2 +- .../webview-ui/agent-manager/PRPanel.tsx | 85 +++++++++---- .../agent-manager/agent-manager.css | 119 ++++++++++++++++-- .../src/types/messages/webview-messages.ts | 1 + 6 files changed, 173 insertions(+), 39 deletions(-) diff --git a/packages/kilo-vscode/src/agent-manager/pr-status-bridge.ts b/packages/kilo-vscode/src/agent-manager/pr-status-bridge.ts index cbbb1bffda8..2a216c30796 100644 --- a/packages/kilo-vscode/src/agent-manager/pr-status-bridge.ts +++ b/packages/kilo-vscode/src/agent-manager/pr-status-bridge.ts @@ -78,8 +78,8 @@ export class PRStatusBridge { return true } if (m.type === "agentManager.openPR") { - const wt = this.host.getWorktrees().find((w: Worktree) => w.id === m.worktreeId) - if (wt?.prUrl) this.host.openExternal(wt.prUrl) + const url = (m.url as string | undefined) ?? this.host.getWorktrees().find((w: Worktree) => w.id === m.worktreeId)?.prUrl + if (url) this.host.openExternal(url) return true } return false diff --git a/packages/kilo-vscode/src/agent-manager/types.ts b/packages/kilo-vscode/src/agent-manager/types.ts index 44e2c3eee68..cb2f761ed10 100644 --- a/packages/kilo-vscode/src/agent-manager/types.ts +++ b/packages/kilo-vscode/src/agent-manager/types.ts @@ -739,6 +739,7 @@ interface OpenPRIn { type: "agentManager.openPR" projectId?: string worktreeId: string + url?: string } interface OpenSessionsIn { diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index b10f83e6d6f..d00471b8bd6 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -2710,7 +2710,7 @@ const AgentManagerContent: Component = () => { setSidePanel(null)} - onOpenExternal={() => vscode.postMessage({ type: "agentManager.openPR", worktreeId: sel as string })} + onOpenExternal={() => vscode.postMessage({ type: "agentManager.openPR", worktreeId: sel as string, url: pr.url })} /> ) })()} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/PRPanel.tsx b/packages/kilo-vscode/webview-ui/agent-manager/PRPanel.tsx index 8a5a011ea98..6bb89ddb585 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/PRPanel.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/PRPanel.tsx @@ -3,6 +3,8 @@ import { Component, For, Show } from "solid-js" import { Icon } from "@kilocode/kilo-ui/icon" import { IconButton } from "@kilocode/kilo-ui/icon-button" import { Tooltip } from "@kilocode/kilo-ui/tooltip" +import { Markdown } from "@kilocode/kilo-ui/markdown" +import { MarkedProvider } from "@kilocode/kilo-ui/context/marked" import type { PRStatus, CheckStatus, PRReviewer } from "../src/types/messages" import { prAccentColor, prBadgeIndicator, prChecksRunning } from "./WorktreeItem" @@ -45,7 +47,7 @@ const REVIEWER_STATE_ICON: Record = { approved: "circle-check", changes_requested: "refresh", commented: "edit", - pending: "clock", + pending: "dash", } const REVIEWER_STATE_LABEL: Record = { @@ -55,7 +57,13 @@ const REVIEWER_STATE_LABEL: Record = { pending: "Awaiting", } -export const PRPanel: Component = (props) => { +export const PRPanel: Component = (props) => ( + + + +) + +const PRPanelInner: Component = (props) => { const accent = () => prAccentColor(props.pr) const indicator = () => prBadgeIndicator(props.pr) const pulsing = () => prChecksRunning(props.pr) @@ -94,6 +102,7 @@ export const PRPanel: Component = (props) => {
{/* State + review */}
+
Overview
Status {STATE_LABEL[props.pr.state]} @@ -108,19 +117,11 @@ export const PRPanel: Component = (props) => {
- {/* Description */} - -
-
-
{props.pr.body}
-
- - {/* Reviewers */} 0}>
-
Reviewers
+
Reviewers
{(reviewer) => ( @@ -135,13 +136,24 @@ export const PRPanel: Component = (props) => {
+ {/* Description */} + +
+
+
Description
+
+ +
+
+ + {/* Diff stats */} 0 || props.pr.additions > 0 || props.pr.deletions > 0}>
-
- Changes - +
+ File Changes + 0}> {props.pr.files}f @@ -160,9 +172,9 @@ export const PRPanel: Component = (props) => { 0}>
-
- Checks - +
+ Checks + {props.pr.checks.passed}/{props.pr.checks.total} passed
@@ -192,15 +204,36 @@ export const PRPanel: Component = (props) => { 0}>
-
- Comments - - {props.pr.comments!.total} - 0}> - {" "} - ({props.pr.comments!.unresolved} unresolved) - - +
+ Comments + 0}> + + {props.pr.comments!.unresolved} unresolved + + +
+
+ + {(comment) => ( +
+
+ {comment.author} + + + {comment.file} + {`:${comment.line}`} + + + + Resolved + +
+
+ +
+
+ )} +
diff --git a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css index 9f9403b046b..e4c19fe49d7 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css @@ -4944,7 +4944,7 @@ body.vscode-high-contrast-light { .am-pr-panel-title { font-size: var(--kilo-font-size-14); - color: var(--text-base); + color: var(--vscode-foreground); font-weight: 500; overflow: hidden; text-overflow: ellipsis; @@ -4968,6 +4968,26 @@ body.vscode-high-contrast-light { padding: 4px 12px; } +.am-pr-panel-section-heading { + display: flex; + align-items: center; + justify-content: space-between; + font-size: var(--font-size-small); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--text-weak); + padding-bottom: 6px; +} + +.am-pr-panel-section-count { + font-size: var(--kilo-font-size-11); + font-weight: 400; + text-transform: none; + letter-spacing: 0; + color: var(--text-weak); +} + .am-pr-panel-divider { height: 1px; background: var(--vscode-panel-border); @@ -4984,14 +5004,17 @@ body.vscode-high-contrast-light { } .am-pr-panel-label { - font-size: var(--kilo-font-size-12); - color: var(--text-weak); + font-size: var(--kilo-font-size-11); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--text-weaker); flex-shrink: 0; } .am-pr-panel-value { font-size: var(--kilo-font-size-12); - color: var(--text-base); + color: var(--vscode-foreground); text-align: right; } @@ -5014,14 +5037,90 @@ body.vscode-high-contrast-light { color: #fbbf24; } +/* PR comment list */ +.am-pr-panel-comment-list { + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 6px; +} + +.am-pr-panel-comment { + border: 1px solid var(--vscode-panel-border); + border-radius: 4px; + padding: 8px; + opacity: 1; +} + +.am-pr-panel-comment-resolved { + opacity: 0.5; +} + +.am-pr-panel-comment-header { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 4px; + flex-wrap: wrap; +} + +.am-pr-panel-comment-author { + font-size: var(--kilo-font-size-12); + font-weight: 600; + color: var(--vscode-foreground); +} + +.am-pr-panel-comment-file { + font-size: var(--kilo-font-size-11); + color: var(--text-weak); + font-family: var(--font-mono, monospace); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 180px; +} + +.am-pr-panel-comment-resolved-badge { + font-size: var(--kilo-font-size-10); + color: var(--text-weaker); + border: 1px solid var(--vscode-panel-border); + border-radius: 3px; + padding: 1px 4px; + margin-left: auto; +} + +.am-pr-panel-comment-body [data-component="markdown"] { + font-size: var(--kilo-font-size-12); + color: var(--vscode-foreground); +} + .am-pr-panel-description { - font-size: var(--kilo-font-size-14); - color: var(--text-base); - white-space: pre-wrap; line-height: 1.5; padding: 2px 0 4px; } +.am-pr-panel-description [data-component="markdown"], +.am-pr-panel-comment-body [data-component="markdown"] { + font-size: var(--kilo-font-size-13); + color: var(--vscode-foreground); +} + +.am-pr-panel-description [data-component="markdown"] h1, +.am-pr-panel-description [data-component="markdown"] h2, +.am-pr-panel-description [data-component="markdown"] h3, +.am-pr-panel-description [data-component="markdown"] h4, +.am-pr-panel-description [data-component="markdown"] h5, +.am-pr-panel-description [data-component="markdown"] h6 { + font-weight: 600; + color: var(--vscode-foreground); + margin-top: 0.75em; + margin-bottom: 0.25em; +} + +.am-pr-panel-description [data-component="markdown"] h1 { font-size: var(--kilo-font-size-16); } +.am-pr-panel-description [data-component="markdown"] h2 { font-size: var(--kilo-font-size-14); } +.am-pr-panel-description [data-component="markdown"] h3 { font-size: var(--kilo-font-size-13); } + /* PR Reviewers */ .am-pr-panel-reviewers { display: flex; @@ -5042,13 +5141,13 @@ body.vscode-high-contrast-light { } .am-pr-panel-reviewer[data-state="approved"] .am-pr-reviewer-icon { color: #34d399; } -.am-pr-panel-reviewer[data-state="changes_requested"] .am-pr-reviewer-icon { color: #f87171; } +.am-pr-panel-reviewer[data-state="changes_requested"] .am-pr-reviewer-icon { color: #fbbf24; } .am-pr-panel-reviewer[data-state="commented"] .am-pr-reviewer-icon { color: var(--text-weak); } .am-pr-panel-reviewer[data-state="pending"] .am-pr-reviewer-icon { color: var(--text-weaker); } .am-pr-reviewer-login { flex: 1; - color: var(--text-base); + color: var(--vscode-foreground); } .am-pr-reviewer-state { @@ -5083,7 +5182,7 @@ body.vscode-high-contrast-light { .am-pr-check-name { flex: 1; - color: var(--text-base); + color: var(--vscode-foreground); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts index 4f11b99d7c2..db195e4ad4c 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts @@ -973,6 +973,7 @@ export interface OpenPRMessage { type: "agentManager.openPR" projectId?: string worktreeId: string + url?: string } export interface ApplyWorktreeDiffMessage { From eb74496b3b55683e0ead9326c42aa74493dcee6c Mon Sep 17 00:00:00 2001 From: cmanu Date: Mon, 3 Aug 2026 13:27:26 -0700 Subject: [PATCH 03/73] feat(pr-viewer): Update styles section headers --- .../agent-manager/AgentManagerApp.tsx | 24 ++- .../webview-ui/agent-manager/PRPanel.tsx | 203 +++++++++++------- .../agent-manager/agent-manager.css | 49 ++++- packages/ui/src/components/icon.tsx | 1 + 4 files changed, 186 insertions(+), 91 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index d00471b8bd6..25c22878f9b 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -310,6 +310,13 @@ const AgentManagerContent: Component = () => { const [sidePanel, setSidePanel] = createSignal(null) const diffOpen = () => sidePanel() === "diff" const prOpen = () => sidePanel() === "pr" + const activePR = createMemo(() => { + const selected = selection() + if (!selected || selected === LOCAL) return undefined + const pr = prStatuses()[selected] + if (!pr) return undefined + return { pr, selected, wt: worktrees().find((w) => w.id === selected) } + }) const diffs = createWorktreeDiffs(vscode) const diffDatas = diffs.diffDatas const diffLoading = diffs.diffLoading @@ -2456,11 +2463,7 @@ const AgentManagerContent: Component = () => { reviewActive={reviewActive} onToggleDiff={toggleDiffPanel} onToggleReview={metrics.click("fullscreen_review", "tab_toolbar", toggleReviewTab)} - prStatus={() => { - const sel = selection() - if (!sel || sel === LOCAL) return undefined - return prStatuses()[sel] - }} + prStatus={() => activePR()?.pr} prOpen={prOpen} onTogglePR={togglePRPanel} terminalDestination={sideCtl.destination} @@ -2701,16 +2704,15 @@ const AgentManagerContent: Component = () => { activeTerminalId={terms.activeId()} /> - + {(() => { - const sel = selection() - const pr = sel && sel !== LOCAL ? prStatuses()[sel] : undefined - if (!pr) return null + const data = activePR()! return ( setSidePanel(null)} - onOpenExternal={() => vscode.postMessage({ type: "agentManager.openPR", worktreeId: sel as string, url: pr.url })} + onOpenExternal={() => vscode.postMessage({ type: "agentManager.openPR", worktreeId: data.selected, url: data.pr.url })} /> ) })()} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/PRPanel.tsx b/packages/kilo-vscode/webview-ui/agent-manager/PRPanel.tsx index 6bb89ddb585..e97cfca42b7 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/PRPanel.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/PRPanel.tsx @@ -1,15 +1,16 @@ /** @jsxImportSource solid-js */ -import { Component, For, Show } from "solid-js" +import { Component, For, Show, createSignal } from "solid-js" import { Icon } from "@kilocode/kilo-ui/icon" import { IconButton } from "@kilocode/kilo-ui/icon-button" import { Tooltip } from "@kilocode/kilo-ui/tooltip" import { Markdown } from "@kilocode/kilo-ui/markdown" import { MarkedProvider } from "@kilocode/kilo-ui/context/marked" -import type { PRStatus, CheckStatus, PRReviewer } from "../src/types/messages" +import type { PRStatus, CheckStatus, PRReviewer, WorktreeState } from "../src/types/messages" import { prAccentColor, prBadgeIndicator, prChecksRunning } from "./WorktreeItem" interface PRPanelProps { pr: PRStatus + worktree?: WorktreeState onClose: () => void onOpenExternal: () => void } @@ -29,9 +30,9 @@ const REVIEW_LABEL: Partial, string>> = { const CHECK_ICON: Record = { success: "circle-check", - failure: "circle-x", - cancelled: "circle-x", - skipped: "circle-x", + failure: "circle-x-outline", + cancelled: "circle-x-outline", + skipped: "circle-x-outline", pending: "play", } @@ -57,6 +58,26 @@ const REVIEWER_STATE_LABEL: Record = { pending: "Awaiting", } +function SectionHeading(props: { + title: string + open: boolean + onToggle: () => void + count?: string + countClass?: string +}) { + return ( + + ) +} + export const PRPanel: Component = (props) => ( @@ -68,6 +89,11 @@ const PRPanelInner: Component = (props) => { const indicator = () => prBadgeIndicator(props.pr) const pulsing = () => prChecksRunning(props.pr) + const [reviewersOpen, setReviewersOpen] = createSignal(true) + const [descOpen, setDescOpen] = createSignal(true) + const [checksOpen, setChecksOpen] = createSignal(true) + const [commentsOpen, setCommentsOpen] = createSignal(true) + return (
@@ -100,9 +126,20 @@ const PRPanelInner: Component = (props) => {
- {/* State + review */} + {/* Overview: branch→base + status */}
-
Overview
+ + {(wt) => ( +
+ Branch + + {wt().branch} + + {wt().parentBranch} + +
+ )} +
Status {STATE_LABEL[props.pr.state]} @@ -121,18 +158,20 @@ const PRPanelInner: Component = (props) => { 0}>
-
Reviewers
-
- - {(reviewer) => ( -
- - - {REVIEWER_STATE_LABEL[reviewer.state]} -
- )} -
-
+ setReviewersOpen((v) => !v)} /> + +
+ + {(reviewer) => ( +
+ + + {REVIEWER_STATE_LABEL[reviewer.state]} +
+ )} +
+
+
@@ -140,14 +179,16 @@ const PRPanelInner: Component = (props) => {
-
Description
-
- -
+ setDescOpen((v) => !v)} /> + +
+ +
+
- {/* Diff stats */} + {/* File Changes */} 0 || props.pr.additions > 0 || props.pr.deletions > 0}>
@@ -172,31 +213,34 @@ const PRPanelInner: Component = (props) => { 0}>
-
- Checks - - {props.pr.checks.passed}/{props.pr.checks.total} passed - -
-
- - {(check) => ( -
- - {check.name} - {CHECK_LABEL[check.status]} - - {check.duration} - - - e.preventDefault()}> - - - -
- )} -
-
+ setChecksOpen((v) => !v)} + count={`${props.pr.checks.passed}/${props.pr.checks.total} passed`} + countClass={`am-pr-checks-count-${props.pr.checks.status}`} + /> + +
+ + {(check) => ( +
+ + {check.name} + {CHECK_LABEL[check.status]} + + {check.duration} + + + e.preventDefault()}> + + + +
+ )} +
+
+
@@ -204,37 +248,38 @@ const PRPanelInner: Component = (props) => { 0}>
-
- Comments - 0}> - - {props.pr.comments!.unresolved} unresolved - - -
-
- - {(comment) => ( -
-
- {comment.author} - - - {comment.file} - {`:${comment.line}`} - - - - Resolved - + setCommentsOpen((v) => !v)} + count={props.pr.comments!.unresolved > 0 ? `${props.pr.comments!.unresolved} unresolved` : undefined} + countClass="am-pr-panel-unresolved" + /> + +
+ + {(comment) => ( +
+
+ {comment.author} + + + {comment.file} + {`:${comment.line}`} + + + + Resolved + +
+
+ +
-
- -
-
- )} - -
+ )} + +
+
diff --git a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css index e4c19fe49d7..21305369f5b 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css @@ -4980,6 +4980,54 @@ body.vscode-high-contrast-light { padding-bottom: 6px; } +.am-pr-panel-section-toggle { + width: 100%; + background: none; + border: none; + cursor: pointer; + padding: 0; + text-align: left; + border-radius: 3px; +} + +.am-pr-panel-section-toggle:hover { + color: var(--vscode-foreground); +} + +.am-pr-panel-section-heading-left { + display: flex; + align-items: center; + gap: 4px; +} + +.am-pr-section-chevron { + flex-shrink: 0; + opacity: 0.6; +} + +/* Branch → base row */ +.am-pr-panel-branch { + display: flex; + align-items: center; + gap: 4px; + flex-wrap: wrap; +} + +.am-pr-branch-name { + font-family: var(--font-mono, monospace); + font-size: var(--kilo-font-size-11); +} + +.am-pr-branch-arrow { + opacity: 0.5; + flex-shrink: 0; +} + +/* Check count color by status */ +.am-pr-checks-count-success { color: #34d399; } +.am-pr-checks-count-failure { color: #f87171; } +.am-pr-checks-count-pending { color: #fbbf24; } + .am-pr-panel-section-count { font-size: var(--kilo-font-size-11); font-weight: 400; @@ -5032,7 +5080,6 @@ body.vscode-high-contrast-light { align-items: center; gap: 6px; } - .am-pr-panel-unresolved { color: #fbbf24; } diff --git a/packages/ui/src/components/icon.tsx b/packages/ui/src/components/icon.tsx index bdf68e12228..e70de73a831 100644 --- a/packages/ui/src/components/icon.tsx +++ b/packages/ui/src/components/icon.tsx @@ -18,6 +18,7 @@ const icons = { "chevron-grabber-vertical": ``, "chevron-double-right": ``, "circle-x": ``, + "circle-x-outline": ``, close: ``, "close-small": ``, checklist: ``, From 291ac0887e4874dd4904e582118ebb3aa22edb87 Mon Sep 17 00:00:00 2001 From: cmanu Date: Mon, 3 Aug 2026 13:38:21 -0700 Subject: [PATCH 04/73] fix(pr-view): Move toggle pr panel function --- .../webview-ui/agent-manager/AgentManagerApp.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index 25c22878f9b..a49531d7994 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -359,7 +359,6 @@ const AgentManagerContent: Component = () => { // Per-worktree git stats (diff additions/deletions, commits missing from origin) const worktreeStats = () => registry.active().worktreeStats() - // Per-worktree PR status data const prStatuses = () => registry.active().prStatuses() const runStatuses = () => registry.active().runStatuses() @@ -558,6 +557,12 @@ const AgentManagerContent: Component = () => { } const openWindow = metrics.click("open_worktree_window", "tab_toolbar", openWorktreeDirectory) + const togglePRPanel = () => { + setHistory(false) + setReviewActive(false) + setSidePanel((prev) => (prev === "pr" ? null : "pr")) + } + const openSelectedPR = () => { const sel = selection() if (!sel || sel === LOCAL || !prStatuses()[sel]) return @@ -565,12 +570,6 @@ const AgentManagerContent: Component = () => { togglePRPanel() } - const togglePRPanel = () => { - setHistory(false) - setReviewActive(false) - setSidePanel((prev) => (prev === "pr" ? null : "pr")) - } - const runWorktree = (id: string, destination: TerminalDestination) => { const state = runStatuses()[id]?.state ?? "idle" if (state === "running" || state === "stopping") { From 8663e5baf6a7a5f5ff60ad925cae3bc5702854f7 Mon Sep 17 00:00:00 2001 From: cmanu Date: Mon, 3 Aug 2026 14:09:02 -0700 Subject: [PATCH 05/73] fix(pr-view): Organizing files and components, and improving types --- .../src/agent-manager/PRStatusPoller.ts | 28 +- .../kilo-vscode/src/agent-manager/types.ts | 6 +- .../agent-manager/AgentManagerApp.tsx | 2 +- .../webview-ui/agent-manager/PRPanel.tsx | 288 ------------- .../webview-ui/agent-manager/TabBar.tsx | 2 +- .../webview-ui/agent-manager/WorktreeItem.tsx | 23 +- .../agent-manager/agent-manager.css | 390 +----------------- .../webview-ui/agent-manager/pr/PRBadge.tsx | 30 ++ .../webview-ui/agent-manager/pr/PRChecks.tsx | 59 +++ .../agent-manager/pr/PRComments.tsx | 48 +++ .../agent-manager/pr/PRDescription.tsx | 21 + .../agent-manager/pr/PRFileChanges.tsx | 20 + .../agent-manager/pr/PROverview.tsx | 49 +++ .../webview-ui/agent-manager/pr/PRPanel.tsx | 61 +++ .../agent-manager/pr/PRReviewers.tsx | 44 ++ .../agent-manager/pr/SectionHeading.tsx | 23 ++ .../webview-ui/agent-manager/pr/pr-panel.css | 343 +++++++++++++++ .../webview-ui/agent-manager/pr/pr-types.ts | 32 ++ .../src/stories/agent-manager.stories.tsx | 2 +- .../src/stories/section-header.stories.tsx | 14 +- .../src/types/messages/agent-manager.ts | 39 +- 21 files changed, 793 insertions(+), 731 deletions(-) delete mode 100644 packages/kilo-vscode/webview-ui/agent-manager/PRPanel.tsx create mode 100644 packages/kilo-vscode/webview-ui/agent-manager/pr/PRBadge.tsx create mode 100644 packages/kilo-vscode/webview-ui/agent-manager/pr/PRChecks.tsx create mode 100644 packages/kilo-vscode/webview-ui/agent-manager/pr/PRComments.tsx create mode 100644 packages/kilo-vscode/webview-ui/agent-manager/pr/PRDescription.tsx create mode 100644 packages/kilo-vscode/webview-ui/agent-manager/pr/PRFileChanges.tsx create mode 100644 packages/kilo-vscode/webview-ui/agent-manager/pr/PROverview.tsx create mode 100644 packages/kilo-vscode/webview-ui/agent-manager/pr/PRPanel.tsx create mode 100644 packages/kilo-vscode/webview-ui/agent-manager/pr/PRReviewers.tsx create mode 100644 packages/kilo-vscode/webview-ui/agent-manager/pr/SectionHeading.tsx create mode 100644 packages/kilo-vscode/webview-ui/agent-manager/pr/pr-panel.css create mode 100644 packages/kilo-vscode/webview-ui/agent-manager/pr/pr-types.ts diff --git a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts index c45cd992beb..01875607f00 100644 --- a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts +++ b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts @@ -249,7 +249,9 @@ export class PRStatusPoller { state: pr.state, review: pr.review, checks, - ...(comments && { comments, reviewers: comments.reviewers }), + ...(comments && { + comments: { total: comments.total, unresolved: comments.unresolved, comments: comments.comments, reviewers: comments.reviewers }, + }), additions: pr.additions, deletions: pr.deletions, files: pr.files, @@ -367,7 +369,7 @@ export class PRStatusPoller { passed: number failed: number pending: number - items: PRCheck[] + checks: PRCheck[] }> { try { const { stdout } = await this.shell( @@ -383,24 +385,24 @@ export class PRStatusPoller { completedAt?: string }> - const items: PRCheck[] = data.map((c) => ({ + const checks: PRCheck[] = data.map((c) => ({ name: c.name, status: mapCheckStatus(c.state), url: c.link, duration: formatCheckDuration(c.startedAt, c.completedAt), })) - const total = items.length - const passed = items.filter((c) => c.status === "success").length - const failed = items.filter((c) => c.status === "failure").length - const pending = items.filter((c) => c.status === "pending").length + const total = checks.length + const passed = checks.filter((c) => c.status === "success").length + const failed = checks.filter((c) => c.status === "failure").length + const pending = checks.filter((c) => c.status === "pending").length const status: AggregateCheckStatus = total === 0 ? "none" : failed > 0 ? "failure" : pending > 0 ? "pending" : "success" - return { status, total, passed, failed, pending, items } + return { status, total, passed, failed, pending, checks } } catch { - return { status: "none", total: 0, passed: 0, failed: 0, pending: 0, items: [] } + return { status: "none", total: 0, passed: 0, failed: 0, pending: 0, checks: [] } } } @@ -421,7 +423,7 @@ export class PRStatusPoller { private async fetchComments( prNumber: number, cwd: string, - ): Promise<{ total: number; unresolved: number; items: PRComment[]; reviewers: PRReviewer[] }> { + ): Promise<{ total: number; unresolved: number; comments: PRComment[]; reviewers: PRReviewer[] }> { try { const repo = await this.getRepoInfo(cwd) const query = `query($owner: String!, $repo: String!, $number: Int!) { @@ -458,12 +460,12 @@ export class PRStatusPoller { { cwd, timeout: 15_000 }, ) const pr = JSON.parse(stdout)?.data?.repository?.pullRequest - const items = parseComments(pr?.reviewThreads?.nodes ?? []) + const comments = parseComments(pr?.reviewThreads?.nodes ?? []) const reviewers = parseReviewers(pr?.reviewRequests?.nodes ?? [], pr?.reviews?.nodes ?? []) - return { total: items.length, unresolved: items.filter((c) => !c.resolved).length, items, reviewers } + return { total: comments.length, unresolved: comments.filter((c) => !c.resolved).length, comments, reviewers } } catch (err) { this.options.log("Failed to fetch PR comments:", err) - return { total: 0, unresolved: 0, items: [], reviewers: [] } + return { total: 0, unresolved: 0, comments: [], reviewers: [] } } } } diff --git a/packages/kilo-vscode/src/agent-manager/types.ts b/packages/kilo-vscode/src/agent-manager/types.ts index cb2f761ed10..38bf209d547 100644 --- a/packages/kilo-vscode/src/agent-manager/types.ts +++ b/packages/kilo-vscode/src/agent-manager/types.ts @@ -90,14 +90,14 @@ export interface PRStatus { passed: number failed: number pending: number - items: PRCheck[] + checks: PRCheck[] } comments?: { total: number unresolved: number - items: PRComment[] + comments: PRComment[] + reviewers: PRReviewer[] } - reviewers?: PRReviewer[] additions: number deletions: number files: number diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index a49531d7994..ea77a6c64b8 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -141,7 +141,7 @@ import { import { focusCurrentTab, renderTab, renderTerminalLayer, renderNewTabButton } from "./tab-rendering" import { useTabScroll } from "./tab-scroll" import { DiffPanel } from "./DiffPanel" -import { PRPanel } from "./PRPanel" +import { PRPanel } from "./pr/PRPanel" import { createRevertFile } from "./revert-file" import { FullScreenDiffView } from "../diff-viewer/FullScreenDiffView" import { createApplyToLocal } from "./apply-to-local" diff --git a/packages/kilo-vscode/webview-ui/agent-manager/PRPanel.tsx b/packages/kilo-vscode/webview-ui/agent-manager/PRPanel.tsx deleted file mode 100644 index e97cfca42b7..00000000000 --- a/packages/kilo-vscode/webview-ui/agent-manager/PRPanel.tsx +++ /dev/null @@ -1,288 +0,0 @@ -/** @jsxImportSource solid-js */ -import { Component, For, Show, createSignal } from "solid-js" -import { Icon } from "@kilocode/kilo-ui/icon" -import { IconButton } from "@kilocode/kilo-ui/icon-button" -import { Tooltip } from "@kilocode/kilo-ui/tooltip" -import { Markdown } from "@kilocode/kilo-ui/markdown" -import { MarkedProvider } from "@kilocode/kilo-ui/context/marked" -import type { PRStatus, CheckStatus, PRReviewer, WorktreeState } from "../src/types/messages" -import { prAccentColor, prBadgeIndicator, prChecksRunning } from "./WorktreeItem" - -interface PRPanelProps { - pr: PRStatus - worktree?: WorktreeState - onClose: () => void - onOpenExternal: () => void -} - -const STATE_LABEL: Record = { - open: "Open", - draft: "Draft", - merged: "Merged", - closed: "Closed", -} - -const REVIEW_LABEL: Partial, string>> = { - approved: "Approved", - changes_requested: "Changes Requested", - pending: "Review Pending", -} - -const CHECK_ICON: Record = { - success: "circle-check", - failure: "circle-x-outline", - cancelled: "circle-x-outline", - skipped: "circle-x-outline", - pending: "play", -} - -const CHECK_LABEL: Record = { - success: "Passed", - failure: "Failed", - pending: "Running", - skipped: "Skipped", - cancelled: "Cancelled", -} - -const REVIEWER_STATE_ICON: Record = { - approved: "circle-check", - changes_requested: "refresh", - commented: "edit", - pending: "dash", -} - -const REVIEWER_STATE_LABEL: Record = { - approved: "Approved", - changes_requested: "Changes requested", - commented: "Commented", - pending: "Awaiting", -} - -function SectionHeading(props: { - title: string - open: boolean - onToggle: () => void - count?: string - countClass?: string -}) { - return ( - - ) -} - -export const PRPanel: Component = (props) => ( - - - -) - -const PRPanelInner: Component = (props) => { - const accent = () => prAccentColor(props.pr) - const indicator = () => prBadgeIndicator(props.pr) - const pulsing = () => prChecksRunning(props.pr) - - const [reviewersOpen, setReviewersOpen] = createSignal(true) - const [descOpen, setDescOpen] = createSignal(true) - const [checksOpen, setChecksOpen] = createSignal(true) - const [commentsOpen, setCommentsOpen] = createSignal(true) - - return ( -
-
-
- - - #{props.pr.number} - - {props.pr.title} -
-
- - - - - - -
-
- -
- {/* Overview: branch→base + status */} -
- - {(wt) => ( -
- Branch - - {wt().branch} - - {wt().parentBranch} - -
- )} -
-
- Status - {STATE_LABEL[props.pr.state]} -
- - {(review) => ( -
- Review - {REVIEW_LABEL[review()]} -
- )} -
-
- - {/* Reviewers */} - 0}> -
-
- setReviewersOpen((v) => !v)} /> - -
- - {(reviewer) => ( -
- - - {REVIEWER_STATE_LABEL[reviewer.state]} -
- )} -
-
-
-
- - - {/* Description */} - -
-
- setDescOpen((v) => !v)} /> - -
- -
-
-
- - - {/* File Changes */} - 0 || props.pr.additions > 0 || props.pr.deletions > 0}> -
-
-
- File Changes - - 0}> - {props.pr.files}f - - 0}> - +{props.pr.additions} - - 0}> - −{props.pr.deletions} - - -
-
- - - {/* Checks */} - 0}> -
-
- setChecksOpen((v) => !v)} - count={`${props.pr.checks.passed}/${props.pr.checks.total} passed`} - countClass={`am-pr-checks-count-${props.pr.checks.status}`} - /> - -
- - {(check) => ( -
- - {check.name} - {CHECK_LABEL[check.status]} - - {check.duration} - - - e.preventDefault()}> - - - -
- )} -
-
-
-
- - - {/* Comments */} - 0}> -
-
- setCommentsOpen((v) => !v)} - count={props.pr.comments!.unresolved > 0 ? `${props.pr.comments!.unresolved} unresolved` : undefined} - countClass="am-pr-panel-unresolved" - /> - -
- - {(comment) => ( -
-
- {comment.author} - - - {comment.file} - {`:${comment.line}`} - - - - Resolved - -
-
- -
-
- )} -
-
-
-
- -
-
- ) -} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/TabBar.tsx b/packages/kilo-vscode/webview-ui/agent-manager/TabBar.tsx index f3c1ddda1c3..ab0a888f30e 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/TabBar.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/TabBar.tsx @@ -55,7 +55,7 @@ export interface TabBarProps { reviewActive: () => boolean onToggleDiff: () => void onToggleReview: () => void - prStatus: () => PRStatus | null | undefined + prStatus: () => PRStatus | undefined prOpen: () => boolean onTogglePR: () => void terminalDestination: () => TerminalDestination diff --git a/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx b/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx index a7fc15a6cf4..3af617b8599 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx @@ -10,7 +10,8 @@ import { Tooltip, TooltipKeybind } from "@kilocode/kilo-ui/tooltip" import { HoverCard } from "@kilocode/kilo-ui/hover-card" import { ContextMenu } from "@kilocode/kilo-ui/context-menu" import { Button } from "@kilocode/kilo-ui/button" -import type { WorktreeState, WorktreeGitStats, PRStatus, SectionState, RunStatus } from "../src/types/messages" +import type { WorktreeState, WorktreeGitStats, SectionState, RunStatus } from "../src/types/messages" +import type { PRStatus } from "../src/types/messages" import { colorCss } from "./section-colors" import { useLanguage } from "../src/context/language" import { formatRelativeDate } from "../src/utils/date" @@ -57,7 +58,7 @@ interface WorktreeItemProps { /** Keybinding string for the open-in-vscode action. */ openKeybind: string /** PR status for this worktree's branch, or null if no PR. */ - pr?: PRStatus | null + pr?: PRStatus runStatus?: RunStatus /** Callback when the PR badge is clicked. */ onOpenPR?: () => void @@ -94,12 +95,13 @@ const hasStats = (s: WorktreeGitStats | undefined): s is WorktreeGitStats => * and review results are conveyed by a separate status icon (see prBadgeIndicator) * so a failing check is not mistaken for a closed PR. */ -export function prAccentColor(pr: PRStatus): string { - if (pr.state === "draft") return "var(--text-weaker)" - if (pr.state === "merged") return "#a78bfa" - if (pr.state === "closed") return "#f87171" - if (pr.checks.status === "pending") return "#fbbf24" - return "#34d399" +/** Derive the badge accent key used as `data-accent` for CSS color styling. */ +export function prBadgeAccent(pr: PRStatus): "open" | "pending" | "draft" | "merged" | "closed" { + if (pr.state === "draft") return "draft" + if (pr.state === "merged") return "merged" + if (pr.state === "closed") return "closed" + if (pr.checks.status === "pending") return "pending" + return "open" } /** True while an open PR's checks are still running — drives the pulsing amber badge. */ @@ -349,13 +351,12 @@ export const WorktreeItem: Component = (props) => { } > {(pr) => { - const accent = () => prAccentColor(pr()) const indicator = () => prBadgeIndicator(pr()) return ( }> diff --git a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css index 21305369f5b..4f2958ab18b 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css @@ -846,7 +846,18 @@ button.am-section-toggle:hover .am-section-label { } /* PR pill badge — second row inside am-wt-row2. - Uses --pr-accent custom property (set inline) to derive all colors. */ + Uses data-accent attribute to derive all colors via CSS variables. */ +.am-pr-badge[data-accent="open"], +.am-pr-panel-badge[data-accent="open"] { --pr-accent: var(--vscode-testing-iconPassed, #34d399); } +.am-pr-badge[data-accent="pending"], +.am-pr-panel-badge[data-accent="pending"] { --pr-accent: var(--vscode-charts-yellow, #fbbf24); } +.am-pr-badge[data-accent="draft"], +.am-pr-panel-badge[data-accent="draft"] { --pr-accent: var(--text-weaker); } +.am-pr-badge[data-accent="merged"], +.am-pr-panel-badge[data-accent="merged"] { --pr-accent: var(--vscode-charts-purple, #a78bfa); } +.am-pr-badge[data-accent="closed"], +.am-pr-panel-badge[data-accent="closed"] { --pr-accent: var(--vscode-testing-iconFailed, #f87171); } + .am-pr-badge { display: inline-flex; align-items: center; @@ -881,17 +892,17 @@ button.am-section-toggle:hover .am-section-label { /* Status icon overlaid on the badge — colored by CI/review result, independent of the badge's PR-state accent color. */ .am-pr-badge-status[data-status="failure"] { - color: #ef4444; + color: var(--vscode-testing-iconFailed, #f87171); } .am-pr-badge-status[data-status="changes"] { - color: #fbbf24; + color: var(--vscode-charts-yellow, #fbbf24); } .am-pr-badge-status[data-status="approved"] { - color: #34d399; + color: var(--vscode-testing-iconPassed, #34d399); } /* Checks still running — pulse the whole amber badge. */ -.am-pr-badge[data-pending] { +.am-pr-badge.am-pr-badge-pending { animation: am-pr-pulse 1.5s ease-in-out infinite; } @keyframes am-pr-pulse { @@ -905,7 +916,7 @@ button.am-section-toggle:hover .am-section-label { } @media (prefers-reduced-motion: reduce) { - .am-pr-badge[data-pending] { + .am-pr-badge.am-pr-badge-pending { animation: none; opacity: 0.9; } @@ -4897,370 +4908,3 @@ body.vscode-high-contrast-light { .am-terminal-host [class~="xterm-viewport"] { background: transparent !important; } - -/* PR Panel */ -.am-pr-panel { - display: flex; - flex-direction: column; - height: 100%; - overflow: hidden; -} - -.am-pr-panel-header { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 8px; - padding: 10px 12px 8px; - border-bottom: 1px solid var(--vscode-panel-border); - flex-shrink: 0; -} - -.am-pr-panel-title-row { - display: flex; - align-items: center; - gap: 8px; - min-width: 0; -} - -.am-pr-panel-badge { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 2px 6px 2px 4px; - border-radius: 4px; - font-size: var(--kilo-font-size-10); - font-variant-numeric: tabular-nums; - background: color-mix(in srgb, var(--pr-accent, #34d399) 15%, transparent); - color: var(--pr-accent, #34d399); - border: 1px solid color-mix(in srgb, var(--pr-accent, #34d399) 40%, transparent); - white-space: nowrap; - flex-shrink: 0; -} - -.am-pr-panel-badge[data-pending] { - animation: am-pr-badge-pulse 1.5s ease-in-out infinite; -} - -.am-pr-panel-title { - font-size: var(--kilo-font-size-14); - color: var(--vscode-foreground); - font-weight: 500; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.am-pr-panel-actions { - display: flex; - align-items: center; - gap: 2px; - flex-shrink: 0; -} - -.am-pr-panel-body { - flex: 1; - overflow-y: auto; - padding: 8px 0; -} - -.am-pr-panel-section { - padding: 4px 12px; -} - -.am-pr-panel-section-heading { - display: flex; - align-items: center; - justify-content: space-between; - font-size: var(--font-size-small); - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; - color: var(--text-weak); - padding-bottom: 6px; -} - -.am-pr-panel-section-toggle { - width: 100%; - background: none; - border: none; - cursor: pointer; - padding: 0; - text-align: left; - border-radius: 3px; -} - -.am-pr-panel-section-toggle:hover { - color: var(--vscode-foreground); -} - -.am-pr-panel-section-heading-left { - display: flex; - align-items: center; - gap: 4px; -} - -.am-pr-section-chevron { - flex-shrink: 0; - opacity: 0.6; -} - -/* Branch → base row */ -.am-pr-panel-branch { - display: flex; - align-items: center; - gap: 4px; - flex-wrap: wrap; -} - -.am-pr-branch-name { - font-family: var(--font-mono, monospace); - font-size: var(--kilo-font-size-11); -} - -.am-pr-branch-arrow { - opacity: 0.5; - flex-shrink: 0; -} - -/* Check count color by status */ -.am-pr-checks-count-success { color: #34d399; } -.am-pr-checks-count-failure { color: #f87171; } -.am-pr-checks-count-pending { color: #fbbf24; } - -.am-pr-panel-section-count { - font-size: var(--kilo-font-size-11); - font-weight: 400; - text-transform: none; - letter-spacing: 0; - color: var(--text-weak); -} - -.am-pr-panel-divider { - height: 1px; - background: var(--vscode-panel-border); - margin: 6px 0; -} - -.am-pr-panel-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - padding: 3px 0; - min-height: 22px; -} - -.am-pr-panel-label { - font-size: var(--kilo-font-size-11); - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.04em; - color: var(--text-weaker); - flex-shrink: 0; -} - -.am-pr-panel-value { - font-size: var(--kilo-font-size-12); - color: var(--vscode-foreground); - text-align: right; -} - -.am-pr-panel-value[data-pr-state="open"] { color: #34d399; } -.am-pr-panel-value[data-pr-state="merged"] { color: #a78bfa; } -.am-pr-panel-value[data-pr-state="closed"] { color: #f87171; } -.am-pr-panel-value[data-pr-state="draft"] { color: var(--text-weaker); } - -.am-pr-panel-value[data-checks="failure"] { color: #f87171; } -.am-pr-panel-value[data-checks="pending"] { color: #fbbf24; } -.am-pr-panel-value[data-checks="success"] { color: #34d399; } - -.am-pr-panel-diff { - display: flex; - align-items: center; - gap: 6px; -} -.am-pr-panel-unresolved { - color: #fbbf24; -} - -/* PR comment list */ -.am-pr-panel-comment-list { - display: flex; - flex-direction: column; - gap: 8px; - margin-top: 6px; -} - -.am-pr-panel-comment { - border: 1px solid var(--vscode-panel-border); - border-radius: 4px; - padding: 8px; - opacity: 1; -} - -.am-pr-panel-comment-resolved { - opacity: 0.5; -} - -.am-pr-panel-comment-header { - display: flex; - align-items: center; - gap: 6px; - margin-bottom: 4px; - flex-wrap: wrap; -} - -.am-pr-panel-comment-author { - font-size: var(--kilo-font-size-12); - font-weight: 600; - color: var(--vscode-foreground); -} - -.am-pr-panel-comment-file { - font-size: var(--kilo-font-size-11); - color: var(--text-weak); - font-family: var(--font-mono, monospace); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - max-width: 180px; -} - -.am-pr-panel-comment-resolved-badge { - font-size: var(--kilo-font-size-10); - color: var(--text-weaker); - border: 1px solid var(--vscode-panel-border); - border-radius: 3px; - padding: 1px 4px; - margin-left: auto; -} - -.am-pr-panel-comment-body [data-component="markdown"] { - font-size: var(--kilo-font-size-12); - color: var(--vscode-foreground); -} - -.am-pr-panel-description { - line-height: 1.5; - padding: 2px 0 4px; -} - -.am-pr-panel-description [data-component="markdown"], -.am-pr-panel-comment-body [data-component="markdown"] { - font-size: var(--kilo-font-size-13); - color: var(--vscode-foreground); -} - -.am-pr-panel-description [data-component="markdown"] h1, -.am-pr-panel-description [data-component="markdown"] h2, -.am-pr-panel-description [data-component="markdown"] h3, -.am-pr-panel-description [data-component="markdown"] h4, -.am-pr-panel-description [data-component="markdown"] h5, -.am-pr-panel-description [data-component="markdown"] h6 { - font-weight: 600; - color: var(--vscode-foreground); - margin-top: 0.75em; - margin-bottom: 0.25em; -} - -.am-pr-panel-description [data-component="markdown"] h1 { font-size: var(--kilo-font-size-16); } -.am-pr-panel-description [data-component="markdown"] h2 { font-size: var(--kilo-font-size-14); } -.am-pr-panel-description [data-component="markdown"] h3 { font-size: var(--kilo-font-size-13); } - -/* PR Reviewers */ -.am-pr-panel-reviewers { - display: flex; - flex-direction: column; - gap: 2px; -} - -.am-pr-panel-reviewer { - display: flex; - align-items: center; - gap: 6px; - padding: 3px 0; - font-size: var(--kilo-font-size-12); -} - -.am-pr-reviewer-icon { - flex-shrink: 0; -} - -.am-pr-panel-reviewer[data-state="approved"] .am-pr-reviewer-icon { color: #34d399; } -.am-pr-panel-reviewer[data-state="changes_requested"] .am-pr-reviewer-icon { color: #fbbf24; } -.am-pr-panel-reviewer[data-state="commented"] .am-pr-reviewer-icon { color: var(--text-weak); } -.am-pr-panel-reviewer[data-state="pending"] .am-pr-reviewer-icon { color: var(--text-weaker); } - -.am-pr-reviewer-login { - flex: 1; - color: var(--vscode-foreground); -} - -.am-pr-reviewer-state { - color: var(--text-weak); - flex-shrink: 0; -} - -.am-pr-panel-checks { - display: flex; - flex-direction: column; - gap: 2px; - margin-top: 4px; -} - -.am-pr-panel-check-item { - display: flex; - align-items: center; - gap: 6px; - padding: 3px 0; - font-size: var(--kilo-font-size-12); -} - -.am-pr-check-icon { - flex-shrink: 0; -} - -.am-pr-panel-check-item[data-status="success"] .am-pr-check-icon { color: #34d399; } -.am-pr-panel-check-item[data-status="failure"] .am-pr-check-icon { color: #f87171; } -.am-pr-panel-check-item[data-status="cancelled"] .am-pr-check-icon { color: var(--text-weaker); } -.am-pr-panel-check-item[data-status="skipped"] .am-pr-check-icon { color: var(--text-weaker); } -.am-pr-panel-check-item[data-status="pending"] .am-pr-check-icon { color: #fbbf24; } - -.am-pr-check-name { - flex: 1; - color: var(--vscode-foreground); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.am-pr-check-status { - color: var(--text-weak); - flex-shrink: 0; -} - -.am-pr-check-duration { - color: var(--text-weaker); - font-size: var(--kilo-font-size-10); - flex-shrink: 0; -} - -.am-pr-check-link { - color: var(--text-weak); - display: flex; - align-items: center; - flex-shrink: 0; - cursor: pointer; - text-decoration: none; -} - -.am-pr-check-link:hover { - color: var(--text-base); -} - -/* PR toggle button in tab bar */ -.am-pr-toggle-number { - font-size: var(--kilo-font-size-10); - font-variant-numeric: tabular-nums; -} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRBadge.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRBadge.tsx new file mode 100644 index 00000000000..b80b9495152 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRBadge.tsx @@ -0,0 +1,30 @@ +/** @jsxImportSource solid-js */ +import { Icon } from "@kilocode/kilo-ui/icon" +import type { PRStatus } from "../../src/types/messages" +import { prBadgeAccent, prBadgeIndicator, prChecksRunning } from "../WorktreeItem" + +const INDICATOR_ICON: Record = { + failure: "circle-x", + changes: "warning", + approved: "circle-check", + none: "branch", +} + +export function PRBadge(props: { pr: PRStatus }) { + const indicator = () => prBadgeIndicator(props.pr) + return ( + + + #{props.pr.number} + + ) +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRChecks.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRChecks.tsx new file mode 100644 index 00000000000..e76a760b275 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRChecks.tsx @@ -0,0 +1,59 @@ +/** @jsxImportSource solid-js */ +import { For, Show, createSignal } from "solid-js" +import { Icon } from "@kilocode/kilo-ui/icon" +import type { PRStatus } from "../../src/types/messages" +import type { PRCheck, CheckStatus } from "./pr-types" +import { SectionHeading } from "./SectionHeading" + +const CHECK_ICON: Record = { + success: "circle-check", + failure: "circle-x-outline", + cancelled: "circle-x-outline", + skipped: "circle-x-outline", + pending: "play", +} + +const CHECK_LABEL: Record = { + success: "Passed", + failure: "Failed", + pending: "Running", + skipped: "Skipped", + cancelled: "Cancelled", +} + +export function PRChecks(props: { checks: PRStatus["checks"] }) { + const [open, setOpen] = createSignal(true) + return ( + <> +
+
+ setOpen((v) => !v)} + count={`${props.checks.passed}/${props.checks.total} passed`} + countClass={`am-pr-checks-count-${props.checks.status}`} + /> + +
+ + {(check: PRCheck) => ( +
+ + {check.name} + {CHECK_LABEL[check.status]} + {check.duration} + + e.preventDefault()}> + + + +
+ )} +
+
+
+
+ + ) +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRComments.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRComments.tsx new file mode 100644 index 00000000000..d3f3057e6c0 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRComments.tsx @@ -0,0 +1,48 @@ +/** @jsxImportSource solid-js */ +import { For, Show, createSignal } from "solid-js" +import { Markdown } from "@kilocode/kilo-ui/markdown" +import type { PRStatus } from "../../src/types/messages" +import type { PRComment } from "./pr-types" +import { SectionHeading } from "./SectionHeading" + +export function PRComments(props: { comments: NonNullable }) { + const [open, setOpen] = createSignal(true) + return ( + <> +
+
+ setOpen((v) => !v)} + count={props.comments.unresolved > 0 ? `${props.comments.unresolved} unresolved` : undefined} + countClass="am-pr-panel-unresolved" + /> + +
+ + {(comment: PRComment) => ( +
+
+ {comment.author} + + + {comment.file}{`:${comment.line}`} + + + + Resolved + +
+
+ +
+
+ )} +
+
+
+
+ + ) +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRDescription.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRDescription.tsx new file mode 100644 index 00000000000..524be50e2ec --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRDescription.tsx @@ -0,0 +1,21 @@ +/** @jsxImportSource solid-js */ +import { Show, createSignal } from "solid-js" +import { Markdown } from "@kilocode/kilo-ui/markdown" +import { SectionHeading } from "./SectionHeading" + +export function PRDescription(props: { body: string }) { + const [open, setOpen] = createSignal(true) + return ( + <> +
+
+ setOpen((v) => !v)} /> + +
+ +
+
+
+ + ) +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRFileChanges.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRFileChanges.tsx new file mode 100644 index 00000000000..b4e558d67b0 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRFileChanges.tsx @@ -0,0 +1,20 @@ +/** @jsxImportSource solid-js */ +import { Show } from "solid-js" + +export function PRFileChanges(props: { files: number; additions: number; deletions: number }) { + return ( + <> +
+
+
+ File Changes + + 0}>{props.files}f + 0}>+{props.additions} + 0}>−{props.deletions} + +
+
+ + ) +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/PROverview.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PROverview.tsx new file mode 100644 index 00000000000..6c1fc863739 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PROverview.tsx @@ -0,0 +1,49 @@ +/** @jsxImportSource solid-js */ +import { Show } from "solid-js" +import { Icon } from "@kilocode/kilo-ui/icon" +import type { WorktreeState } from "../../src/types/messages" +import type { PRStatus } from "../../src/types/messages" + +const STATE_LABEL: Record = { + open: "Open", + draft: "Draft", + merged: "Merged", + closed: "Closed", +} + +const REVIEW_LABEL: Partial, string>> = { + approved: "Approved", + changes_requested: "Changes Requested", + pending: "Review Pending", +} + +export function PROverview(props: { pr: PRStatus; worktree?: WorktreeState }) { + return ( +
+ + {(wt) => ( +
+ Branch + + {wt().branch} + + {wt().parentBranch} + +
+ )} +
+
+ Status + {STATE_LABEL[props.pr.state]} +
+ + {(review) => ( +
+ Review + {REVIEW_LABEL[review()]} +
+ )} +
+
+ ) +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRPanel.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRPanel.tsx new file mode 100644 index 00000000000..e69890b5e9f --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRPanel.tsx @@ -0,0 +1,61 @@ +/** @jsxImportSource solid-js */ +import { Component, Show } from "solid-js" +import { IconButton } from "@kilocode/kilo-ui/icon-button" +import { Tooltip } from "@kilocode/kilo-ui/tooltip" +import { MarkedProvider } from "@kilocode/kilo-ui/context/marked" +import type { WorktreeState } from "../../src/types/messages" +import type { PRStatus } from "../../src/types/messages" +import { PRBadge } from "./PRBadge" +import { PROverview } from "./PROverview" +import { PRReviewers } from "./PRReviewers" +import { PRDescription } from "./PRDescription" +import { PRFileChanges } from "./PRFileChanges" +import { PRChecks } from "./PRChecks" +import { PRComments } from "./PRComments" +import "./pr-panel.css" + +interface PRPanelProps { + pr: PRStatus + worktree?: WorktreeState + onClose: () => void + onOpenExternal: () => void +} + +export const PRPanel: Component = (props) => ( + +
+
+
+ + {props.pr.title} +
+
+ + + + + + +
+
+
+ + + + + + + + 0 || props.pr.additions > 0 || props.pr.deletions > 0}> + + + 0}> + + + 0}> + + +
+
+
+) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRReviewers.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRReviewers.tsx new file mode 100644 index 00000000000..85206562958 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRReviewers.tsx @@ -0,0 +1,44 @@ +/** @jsxImportSource solid-js */ +import { For, Show, createSignal } from "solid-js" +import { Icon } from "@kilocode/kilo-ui/icon" +import type { PRReviewer } from "./pr-types" +import { SectionHeading } from "./SectionHeading" + +const REVIEWER_ICON: Record = { + approved: "circle-check", + changes_requested: "refresh", + commented: "edit", + pending: "dash", +} + +const REVIEWER_LABEL: Record = { + approved: "Approved", + changes_requested: "Changes requested", + commented: "Commented", + pending: "Awaiting", +} + +export function PRReviewers(props: { reviewers: PRReviewer[] }) { + const [open, setOpen] = createSignal(true) + return ( + <> +
+
+ setOpen((v) => !v)} /> + +
+ + {(reviewer) => ( +
+ + + {REVIEWER_LABEL[reviewer.state]} +
+ )} +
+
+
+
+ + ) +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/SectionHeading.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/SectionHeading.tsx new file mode 100644 index 00000000000..1de9bba802e --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/SectionHeading.tsx @@ -0,0 +1,23 @@ +/** @jsxImportSource solid-js */ +import { Show } from "solid-js" +import { Icon } from "@kilocode/kilo-ui/icon" + +export function SectionHeading(props: { + title: string + open: boolean + onToggle: () => void + count?: string + countClass?: string +}) { + return ( + + ) +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-panel.css b/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-panel.css new file mode 100644 index 00000000000..ba73dd76ae8 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-panel.css @@ -0,0 +1,343 @@ +/* PR Panel */ +.am-pr-panel { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; +} + +/* Shared flex utilities scoped to the PR panel */ +.am-pr-row { display: flex; align-items: center; } +.am-pr-col { display: flex; flex-direction: column; } + +.am-pr-panel-header { + align-items: flex-start; + justify-content: space-between; + gap: 8px; + padding: 10px 12px 8px; + border-bottom: 1px solid var(--vscode-panel-border); + flex-shrink: 0; +} + +.am-pr-panel-title-row { + gap: 8px; + min-width: 0; +} + +.am-pr-panel-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 6px 2px 4px; + border-radius: 4px; + font-size: var(--kilo-font-size-10); + font-variant-numeric: tabular-nums; + background: color-mix(in srgb, var(--pr-accent) 15%, transparent); + color: var(--pr-accent); + border: 1px solid color-mix(in srgb, var(--pr-accent) 40%, transparent); + white-space: nowrap; + flex-shrink: 0; +} + +.am-pr-panel-badge.am-pr-badge-pending { + animation: am-pr-badge-pulse 1.5s ease-in-out infinite; +} + +.am-pr-panel-title { + font-size: var(--kilo-font-size-14); + color: var(--vscode-foreground); + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.am-pr-panel-actions { + gap: 2px; + flex-shrink: 0; +} + +.am-pr-panel-body { + flex: 1; + overflow-y: auto; + padding: 8px 0; +} + +.am-pr-panel-section { + padding: 4px 12px; +} + +.am-pr-panel-section-heading { + justify-content: space-between; + font-size: var(--font-size-small); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--text-weak); + padding-bottom: 6px; +} + +.am-pr-panel-section-toggle { + width: 100%; + background: none; + border: none; + cursor: pointer; + padding: 0; + text-align: left; + border-radius: 3px; +} + +.am-pr-panel-section-toggle:hover { + color: var(--vscode-foreground); +} + +.am-pr-panel-section-heading-left { + gap: 4px; +} + +.am-pr-section-chevron { + flex-shrink: 0; + opacity: 0.6; +} + +/* Branch → base row */ +.am-pr-panel-branch { + gap: 4px; + flex-wrap: wrap; +} + +.am-pr-branch-name { + font-family: var(--font-mono, monospace); + font-size: var(--kilo-font-size-11); +} + +.am-pr-branch-arrow { + opacity: 0.5; + flex-shrink: 0; +} + +/* Check count color by status */ +.am-pr-checks-count-success { color: var(--vscode-testing-iconPassed, #34d399); } +.am-pr-checks-count-failure { color: var(--vscode-testing-iconFailed, #f87171); } +.am-pr-checks-count-pending { color: var(--vscode-charts-yellow, #fbbf24); } + +.am-pr-panel-section-count { + font-size: var(--kilo-font-size-11); + font-weight: 400; + text-transform: none; + letter-spacing: 0; + color: var(--text-weak); +} + +.am-pr-panel-divider { + height: 1px; + background: var(--vscode-panel-border); + margin: 6px 0; +} + +.am-pr-panel-row { + justify-content: space-between; + gap: 8px; + padding: 3px 0; + min-height: 22px; +} + +.am-pr-panel-label { + font-size: var(--kilo-font-size-11); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--text-weaker); + flex-shrink: 0; +} + +.am-pr-panel-value { + font-size: var(--kilo-font-size-12); + color: var(--vscode-foreground); + text-align: right; +} + +.am-pr-panel-value[data-pr-state="open"] { color: var(--vscode-testing-iconPassed, #34d399); } +.am-pr-panel-value[data-pr-state="merged"] { color: var(--vscode-charts-purple, #a78bfa); } +.am-pr-panel-value[data-pr-state="closed"] { color: var(--vscode-testing-iconFailed, #f87171); } +.am-pr-panel-value[data-pr-state="draft"] { color: var(--text-weaker); } + +.am-pr-panel-value[data-checks="failure"] { color: var(--vscode-testing-iconFailed, #f87171); } +.am-pr-panel-value[data-checks="pending"] { color: var(--vscode-charts-yellow, #fbbf24); } +.am-pr-panel-value[data-checks="success"] { color: var(--vscode-testing-iconPassed, #34d399); } + +.am-pr-panel-diff { + gap: 6px; +} +.am-pr-panel-unresolved { + color: var(--vscode-charts-yellow, #fbbf24); +} + +/* PR comment list */ +.am-pr-panel-comment-list { + gap: 8px; + margin-top: 6px; +} + +.am-pr-panel-comment { + border: 1px solid var(--vscode-panel-border); + border-radius: 4px; + padding: 8px; + opacity: 1; +} + +.am-pr-panel-comment-resolved { + opacity: 0.5; +} + +.am-pr-panel-comment-header { + gap: 6px; + margin-bottom: 4px; + flex-wrap: wrap; +} + +.am-pr-panel-comment-author { + font-size: var(--kilo-font-size-12); + font-weight: 600; + color: var(--vscode-foreground); +} + +.am-pr-panel-comment-file { + font-size: var(--kilo-font-size-11); + color: var(--text-weak); + font-family: var(--font-mono, monospace); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 180px; +} + +.am-pr-panel-comment-resolved-badge { + font-size: var(--kilo-font-size-10); + color: var(--text-weaker); + border: 1px solid var(--vscode-panel-border); + border-radius: 3px; + padding: 1px 4px; + margin-left: auto; +} + +.am-pr-panel-comment-body [data-component="markdown"] { + font-size: var(--kilo-font-size-12); + color: var(--vscode-foreground); +} + +.am-pr-panel-description { + line-height: 1.5; + padding: 2px 0 4px; +} + +.am-pr-panel-description [data-component="markdown"], +.am-pr-panel-comment-body [data-component="markdown"] { + font-size: var(--kilo-font-size-13); + color: var(--vscode-foreground); +} + +.am-pr-panel-description [data-component="markdown"] h1, +.am-pr-panel-description [data-component="markdown"] h2, +.am-pr-panel-description [data-component="markdown"] h3, +.am-pr-panel-description [data-component="markdown"] h4, +.am-pr-panel-description [data-component="markdown"] h5, +.am-pr-panel-description [data-component="markdown"] h6 { + font-weight: 600; + color: var(--vscode-foreground); + margin-top: 0.75em; + margin-bottom: 0.25em; +} + +.am-pr-panel-description [data-component="markdown"] h1 { font-size: var(--kilo-font-size-16); } +.am-pr-panel-description [data-component="markdown"] h2 { font-size: var(--kilo-font-size-14); } +.am-pr-panel-description [data-component="markdown"] h3 { font-size: var(--kilo-font-size-13); } + +/* PR Reviewers */ +.am-pr-panel-reviewers { + gap: 2px; +} + +.am-pr-panel-reviewer { + gap: 6px; + padding: 3px 0; + font-size: var(--kilo-font-size-12); +} + +.am-pr-reviewer-icon { + flex-shrink: 0; +} + +.am-pr-panel-reviewer[data-state="approved"] .am-pr-reviewer-icon { color: var(--vscode-testing-iconPassed, #34d399); } +.am-pr-panel-reviewer[data-state="changes_requested"] .am-pr-reviewer-icon { color: var(--vscode-charts-yellow, #fbbf24); } +.am-pr-panel-reviewer[data-state="commented"] .am-pr-reviewer-icon { color: var(--text-weak); } +.am-pr-panel-reviewer[data-state="pending"] .am-pr-reviewer-icon { color: var(--text-weaker); } + +.am-pr-reviewer-login { + flex: 1; + color: var(--vscode-foreground); +} + +.am-pr-reviewer-state { + color: var(--text-weak); + flex-shrink: 0; +} + +.am-pr-panel-checks { + gap: 2px; + margin-top: 4px; +} + +.am-pr-panel-check-item { + gap: 6px; + padding: 3px 0; + font-size: var(--kilo-font-size-12); +} + +.am-pr-check-icon { + flex-shrink: 0; +} + +.am-pr-panel-check-item[data-status="success"] .am-pr-check-icon { color: var(--vscode-testing-iconPassed, #34d399); } +.am-pr-panel-check-item[data-status="failure"] .am-pr-check-icon { color: var(--vscode-testing-iconFailed, #f87171); } +.am-pr-panel-check-item[data-status="cancelled"] .am-pr-check-icon { color: var(--text-weaker); } +.am-pr-panel-check-item[data-status="skipped"] .am-pr-check-icon { color: var(--text-weaker); } +.am-pr-panel-check-item[data-status="pending"] .am-pr-check-icon { color: var(--vscode-charts-yellow, #fbbf24); } + +.am-pr-check-name { + flex: 1; + color: var(--vscode-foreground); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.am-pr-check-status { + color: var(--text-weak); + flex-shrink: 0; +} + +.am-pr-check-duration { + color: var(--text-weaker); + font-size: var(--kilo-font-size-10); + flex-shrink: 0; +} + +.am-pr-check-link { + color: var(--text-weak); + display: flex; + align-items: center; + flex-shrink: 0; + cursor: pointer; + text-decoration: none; +} + +.am-pr-check-link:hover { + color: var(--text-base); +} + +/* PR toggle button in tab bar */ +.am-pr-toggle-number { + font-size: var(--kilo-font-size-10); + font-variant-numeric: tabular-nums; +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-types.ts b/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-types.ts new file mode 100644 index 00000000000..8d88e131557 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-types.ts @@ -0,0 +1,32 @@ +// PR sub-types — source of truth for all PR-related types used in the PR panel. +// PRStatus lives in src/types/messages/agent-manager.ts for broad consumption. + +export type PRState = "open" | "draft" | "merged" | "closed" +export type ReviewDecision = "approved" | "changes_requested" | "pending" +export type CheckStatus = "success" | "failure" | "pending" | "skipped" | "cancelled" +export type AggregateCheckStatus = "success" | "failure" | "pending" | "none" + +export interface PRCheck { + name: string + status: CheckStatus + url?: string + duration?: string +} + +export interface PRComment { + id: string + author: string + avatar?: string + body: string + file?: string + line?: number + url?: string + resolved: boolean + createdAt?: number +} + +export interface PRReviewer { + login: string + avatar?: string + state: "approved" | "changes_requested" | "pending" | "commented" +} diff --git a/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx index 9ebef0f8766..629818c1c25 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx @@ -604,7 +604,7 @@ const basePR: PRStatus = { url: "https://github.com/org/repo/pull/8594", state: "open", review: null, - checks: { status: "success", total: 5, passed: 5, failed: 0, pending: 0, items: [] }, + checks: { status: "success", total: 5, passed: 5, failed: 0, pending: 0, checks: [] }, additions: 978, deletions: 202, files: 12, diff --git a/packages/kilo-vscode/webview-ui/src/stories/section-header.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/section-header.stories.tsx index cf348a4d339..23938282a84 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/section-header.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/section-header.stories.tsx @@ -593,7 +593,7 @@ export const WithPRBadges: Story = { additions: 120, deletions: 30, files: 5, - checks: { status: "success", total: 5, passed: 5, failed: 0, pending: 0, items: [] }, + checks: { status: "success", total: 5, passed: 5, failed: 0, pending: 0, checks: [] }, }} /> {/* Open + failing checks → green badge, red ✗ (no longer confusable with closed) */} @@ -611,7 +611,7 @@ export const WithPRBadges: Story = { additions: 15, deletions: 8, files: 3, - checks: { status: "failure", total: 5, passed: 3, failed: 2, pending: 0, items: [] }, + checks: { status: "failure", total: 5, passed: 3, failed: 2, pending: 0, checks: [] }, }} /> {/* Open + changes requested → green badge, amber warning */} @@ -629,7 +629,7 @@ export const WithPRBadges: Story = { additions: 60, deletions: 12, files: 4, - checks: { status: "success", total: 5, passed: 5, failed: 0, pending: 0, items: [] }, + checks: { status: "success", total: 5, passed: 5, failed: 0, pending: 0, checks: [] }, }} /> {/* Open + checks running → pulsing amber badge (animation disabled in snapshots) */} @@ -647,7 +647,7 @@ export const WithPRBadges: Story = { additions: 200, deletions: 0, files: 8, - checks: { status: "pending", total: 5, passed: 2, failed: 0, pending: 3, items: [] }, + checks: { status: "pending", total: 5, passed: 2, failed: 0, pending: 3, checks: [] }, }} /> {/* Draft → gray badge */} @@ -665,7 +665,7 @@ export const WithPRBadges: Story = { additions: 30, deletions: 5, files: 2, - checks: { status: "none", total: 0, passed: 0, failed: 0, pending: 0, items: [] }, + checks: { status: "none", total: 0, passed: 0, failed: 0, pending: 0, checks: [] }, }} /> {/* Merged → purple badge, no status icon */} @@ -683,7 +683,7 @@ export const WithPRBadges: Story = { additions: 90, deletions: 20, files: 6, - checks: { status: "success", total: 5, passed: 5, failed: 0, pending: 0, items: [] }, + checks: { status: "success", total: 5, passed: 5, failed: 0, pending: 0, checks: [] }, }} /> {/* Closed → red badge, no status icon (distinct from a failing open PR) */} @@ -701,7 +701,7 @@ export const WithPRBadges: Story = { additions: 10, deletions: 4, files: 1, - checks: { status: "failure", total: 5, passed: 1, failed: 4, pending: 0, items: [] }, + checks: { status: "failure", total: 5, passed: 1, failed: 4, pending: 0, checks: [] }, }} />
diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/agent-manager.ts b/packages/kilo-vscode/webview-ui/src/types/messages/agent-manager.ts index 5bdbe3cfafc..23c0395a07d 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/agent-manager.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/agent-manager.ts @@ -45,32 +45,11 @@ export interface SectionState { } // --------------------------------------------------------------------------- -// PR status types (mirrored from extension types.ts) +// PR status types — sub-types live in agent-manager/pr/pr-types.ts // --------------------------------------------------------------------------- -export type PRState = "open" | "draft" | "merged" | "closed" -export type ReviewDecision = "approved" | "changes_requested" | "pending" -export type CheckStatus = "success" | "failure" | "pending" | "skipped" | "cancelled" -export type AggregateCheckStatus = "success" | "failure" | "pending" | "none" - -export interface PRCheck { - name: string - status: CheckStatus - url?: string - duration?: string -} - -export interface PRComment { - id: string - author: string - avatar?: string - body: string - file?: string - line?: number - url?: string - resolved: boolean - createdAt?: number -} +import type { PRState, ReviewDecision, AggregateCheckStatus, PRCheck, PRComment, PRReviewer } from "../../../agent-manager/pr/pr-types" +export type { PRState, ReviewDecision, CheckStatus, AggregateCheckStatus, PRCheck, PRComment, PRReviewer } from "../../../agent-manager/pr/pr-types" export interface PRStatus { number: number @@ -85,25 +64,19 @@ export interface PRStatus { passed: number failed: number pending: number - items: PRCheck[] + checks: PRCheck[] } comments?: { total: number unresolved: number - items: PRComment[] + comments: PRComment[] + reviewers: PRReviewer[] } - reviewers?: PRReviewer[] additions: number deletions: number files: number } -export interface PRReviewer { - login: string - avatar?: string - state: "approved" | "changes_requested" | "pending" | "commented" -} - export type RunState = "idle" | "running" | "stopping" export interface RunStatus { From f01d5fdbb1120aa10881ef483cfa00729063e468 Mon Sep 17 00:00:00 2001 From: cmanu Date: Mon, 3 Aug 2026 14:24:03 -0700 Subject: [PATCH 06/73] feat(pr-view): Improve agent code practices --- .../src/agent-manager/pr-status-bridge.ts | 2 +- .../webview-ui/agent-manager/WorktreeItem.tsx | 19 +++++++-------- .../agent-manager/agent-manager.css | 23 +++++++++--------- .../webview-ui/agent-manager/pr/PRBadge.tsx | 12 +++++++--- .../webview-ui/agent-manager/pr/PRChecks.tsx | 24 +++++++------------ .../webview-ui/agent-manager/pr/PRPanel.tsx | 14 +++++------ 6 files changed, 43 insertions(+), 51 deletions(-) diff --git a/packages/kilo-vscode/src/agent-manager/pr-status-bridge.ts b/packages/kilo-vscode/src/agent-manager/pr-status-bridge.ts index 2a216c30796..c64da3971eb 100644 --- a/packages/kilo-vscode/src/agent-manager/pr-status-bridge.ts +++ b/packages/kilo-vscode/src/agent-manager/pr-status-bridge.ts @@ -78,7 +78,7 @@ export class PRStatusBridge { return true } if (m.type === "agentManager.openPR") { - const url = (m.url as string | undefined) ?? this.host.getWorktrees().find((w: Worktree) => w.id === m.worktreeId)?.prUrl + const url = (m.url as string) ?? this.host.getWorktrees().find((w: Worktree) => w.id === m.worktreeId)?.prUrl if (url) this.host.openExternal(url) return true } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx b/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx index 3af617b8599..d33b0e0a61d 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx @@ -95,15 +95,6 @@ const hasStats = (s: WorktreeGitStats | undefined): s is WorktreeGitStats => * and review results are conveyed by a separate status icon (see prBadgeIndicator) * so a failing check is not mistaken for a closed PR. */ -/** Derive the badge accent key used as `data-accent` for CSS color styling. */ -export function prBadgeAccent(pr: PRStatus): "open" | "pending" | "draft" | "merged" | "closed" { - if (pr.state === "draft") return "draft" - if (pr.state === "merged") return "merged" - if (pr.state === "closed") return "closed" - if (pr.checks.status === "pending") return "pending" - return "open" -} - /** True while an open PR's checks are still running — drives the pulsing amber badge. */ export function prChecksRunning(pr: PRStatus): boolean { return pr.state === "open" && pr.checks.status === "pending" @@ -355,8 +346,14 @@ export const WorktreeItem: Component = (props) => { return ( }> diff --git a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css index 4f2958ab18b..79e0fee011a 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css @@ -845,18 +845,17 @@ button.am-section-toggle:hover .am-section-label { color: #f87171; } -/* PR pill badge — second row inside am-wt-row2. - Uses data-accent attribute to derive all colors via CSS variables. */ -.am-pr-badge[data-accent="open"], -.am-pr-panel-badge[data-accent="open"] { --pr-accent: var(--vscode-testing-iconPassed, #34d399); } -.am-pr-badge[data-accent="pending"], -.am-pr-panel-badge[data-accent="pending"] { --pr-accent: var(--vscode-charts-yellow, #fbbf24); } -.am-pr-badge[data-accent="draft"], -.am-pr-panel-badge[data-accent="draft"] { --pr-accent: var(--text-weaker); } -.am-pr-badge[data-accent="merged"], -.am-pr-panel-badge[data-accent="merged"] { --pr-accent: var(--vscode-charts-purple, #a78bfa); } -.am-pr-badge[data-accent="closed"], -.am-pr-panel-badge[data-accent="closed"] { --pr-accent: var(--vscode-testing-iconFailed, #f87171); } +/* PR badge accent colors via classList */ +.am-pr-accent-open, +.am-pr-panel-badge.am-pr-accent-open { --pr-accent: var(--vscode-testing-iconPassed, #34d399); } +.am-pr-accent-pending, +.am-pr-panel-badge.am-pr-accent-pending { --pr-accent: var(--vscode-charts-yellow, #fbbf24); } +.am-pr-accent-draft, +.am-pr-panel-badge.am-pr-accent-draft { --pr-accent: var(--text-weaker); } +.am-pr-accent-merged, +.am-pr-panel-badge.am-pr-accent-merged { --pr-accent: var(--vscode-charts-purple, #a78bfa); } +.am-pr-accent-closed, +.am-pr-panel-badge.am-pr-accent-closed { --pr-accent: var(--vscode-testing-iconFailed, #f87171); } .am-pr-badge { display: inline-flex; diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRBadge.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRBadge.tsx index b80b9495152..9eaec93baef 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRBadge.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRBadge.tsx @@ -1,7 +1,7 @@ /** @jsxImportSource solid-js */ import { Icon } from "@kilocode/kilo-ui/icon" import type { PRStatus } from "../../src/types/messages" -import { prBadgeAccent, prBadgeIndicator, prChecksRunning } from "../WorktreeItem" +import { prBadgeIndicator, prChecksRunning } from "../WorktreeItem" const INDICATOR_ICON: Record = { failure: "circle-x", @@ -15,8 +15,14 @@ export function PRBadge(props: { pr: PRStatus }) { return ( = { - success: "circle-check", - failure: "circle-x-outline", - cancelled: "circle-x-outline", - skipped: "circle-x-outline", - pending: "play", -} - -const CHECK_LABEL: Record = { - success: "Passed", - failure: "Failed", - pending: "Running", - skipped: "Skipped", - cancelled: "Cancelled", +const CHECK: Record = { + success: { icon: "circle-check", label: "Passed" }, + failure: { icon: "circle-x-outline", label: "Failed" }, + cancelled: { icon: "circle-x-outline", label: "Cancelled" }, + skipped: { icon: "circle-x-outline", label: "Skipped" }, + pending: { icon: "play", label: "Running" }, } export function PRChecks(props: { checks: PRStatus["checks"] }) { @@ -39,9 +31,9 @@ export function PRChecks(props: { checks: PRStatus["checks"] }) { {(check: PRCheck) => (
- - + + {(comments) => } - - - 0 || props.pr.additions > 0 || props.pr.deletions > 0}> - + {(body) => } + 0}> - 0}> - + + {(comments) => }
From 6e1d7e64f3f53f6874e91ce7263cfe123d8db263 Mon Sep 17 00:00:00 2001 From: cmanu Date: Mon, 3 Aug 2026 14:37:31 -0700 Subject: [PATCH 07/73] feat(pr-view): Improve agent code practices, add types and styles --- .../src/agent-manager/PRStatusPoller.ts | 51 +++++++++---------- ...agent-manager-orchestration-domain.test.ts | 4 +- .../webview-ui/agent-manager/pr/PRChecks.tsx | 8 +-- .../webview-ui/agent-manager/pr/PRPanel.tsx | 2 +- .../webview-ui/agent-manager/pr/pr-panel.css | 3 -- 5 files changed, 32 insertions(+), 36 deletions(-) diff --git a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts index 01875607f00..a2a6ddd51d6 100644 --- a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts +++ b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts @@ -460,8 +460,8 @@ export class PRStatusPoller { { cwd, timeout: 15_000 }, ) const pr = JSON.parse(stdout)?.data?.repository?.pullRequest - const comments = parseComments(pr?.reviewThreads?.nodes ?? []) - const reviewers = parseReviewers(pr?.reviewRequests?.nodes ?? [], pr?.reviews?.nodes ?? []) + const comments = parseComments((pr?.reviewThreads?.nodes ?? []) as GQLThread[]) + const reviewers = parseReviewers((pr?.reviewRequests?.nodes ?? []) as GQLReviewRequest[], (pr?.reviews?.nodes ?? []) as GQLReview[]) return { total: comments.length, unresolved: comments.filter((c) => !c.resolved).length, comments, reviewers } } catch (err) { this.options.log("Failed to fetch PR comments:", err) @@ -543,46 +543,45 @@ function formatCheckDuration(startedAt?: string, completedAt?: string): string | return secs < 60 ? `${secs}s` : `${Math.floor(secs / 60)}m ${secs % 60}s` } -function parseComments(threads: Record[]): PRComment[] { +interface GQLAuthor { login?: string; avatarUrl?: string } +interface GQLComment { id: string; author?: GQLAuthor; body?: string; path?: string; line?: number; url?: string; createdAt?: string } +interface GQLThread { isResolved?: boolean; comments?: { nodes?: GQLComment[] } } +interface GQLReviewRequest { requestedReviewer?: GQLAuthor } +interface GQLReview { author?: GQLAuthor; state?: string } + +function parseComments(threads: GQLThread[]): PRComment[] { const items: PRComment[] = [] for (const thread of threads) { - const nodes = (thread.comments as { nodes?: unknown[] } | undefined)?.nodes - const first = nodes?.[0] as Record | undefined + const first = thread.comments?.nodes?.[0] if (!first) continue - const author = first.author as Record | undefined items.push({ - id: first.id as string, - author: (author?.login as string) ?? "unknown", - avatar: author?.avatarUrl as string | undefined, - body: (first.body as string) ?? "", - file: first.path as string | undefined, - line: first.line as number | undefined, - url: first.url as string | undefined, - resolved: (thread.isResolved as boolean) ?? false, - createdAt: first.createdAt ? new Date(first.createdAt as string).getTime() : undefined, + id: first.id, + author: first.author?.login ?? "unknown", + avatar: first.author?.avatarUrl, + body: first.body ?? "", + file: first.path, + line: first.line, + url: first.url, + resolved: thread.isResolved ?? false, + createdAt: first.createdAt ? new Date(first.createdAt).getTime() : undefined, }) } return items } -function parseReviewers( - requests: Record[], - reviews: Record[], -): PRReviewer[] { +function parseReviewers(requests: GQLReviewRequest[], reviews: GQLReview[]): PRReviewer[] { const map = new Map() for (const node of requests) { - const user = node.requestedReviewer as Record | undefined + const user = node.requestedReviewer if (!user?.login) continue - map.set(user.login as string, { login: user.login as string, avatar: user.avatarUrl as string | undefined, state: "pending" }) + map.set(user.login, { login: user.login, avatar: user.avatarUrl, state: "pending" }) } for (const node of reviews) { - const author = node.author as Record | undefined - const login = author?.login as string | undefined + const login = node.author?.login if (!login) continue - const state = REVIEWER_STATE[node.state as string] ?? "pending" - // Keep most significant: approved/changes_requested > commented > pending + const state = REVIEWER_STATE[node.state ?? ""] ?? "pending" if (!map.has(login) || state !== "commented") { - map.set(login, { login, avatar: author?.avatarUrl as string | undefined, state }) + map.set(login, { login, avatar: node.author?.avatarUrl, state }) } } return [...map.values()] diff --git a/packages/kilo-vscode/tests/unit/agent-manager-orchestration-domain.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-orchestration-domain.test.ts index e7a6e0fb177..68173c61e3a 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-orchestration-domain.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-orchestration-domain.test.ts @@ -86,8 +86,8 @@ describe("Agent Manager orchestration domain", () => { url: "https://example.com/pr/42", state: "open", review: "approved", - checks: { status: "success", total: 1, passed: 1, failed: 0, pending: 0, items: [] }, - comments: { total: 2, unresolved: 1, items: [] }, + checks: { status: "success", total: 1, passed: 1, failed: 0, pending: 0, checks: [] }, + comments: { total: 2, unresolved: 1, comments: [], reviewers: [] }, additions: 10, deletions: 2, files: 1, diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRChecks.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRChecks.tsx index 1e6b82654f0..235124dc720 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRChecks.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRChecks.tsx @@ -6,11 +6,11 @@ import type { PRCheck, CheckStatus } from "./pr-types" import { SectionHeading } from "./SectionHeading" const CHECK: Record = { - success: { icon: "circle-check", label: "Passed" }, - failure: { icon: "circle-x-outline", label: "Failed" }, + success: { icon: "circle-check", label: "Passed" }, + failure: { icon: "circle-x-outline", label: "Failed" }, cancelled: { icon: "circle-x-outline", label: "Cancelled" }, - skipped: { icon: "circle-x-outline", label: "Skipped" }, - pending: { icon: "play", label: "Running" }, + skipped: { icon: "circle-x-outline", label: "Skipped" }, + pending: { icon: "play", label: "Running" }, } export function PRChecks(props: { checks: PRStatus["checks"] }) { 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 a10bd6d8a3b..d3c68a71924 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRPanel.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRPanel.tsx @@ -23,7 +23,7 @@ interface PRPanelProps { export const PRPanel: Component = (props) => ( -
+
diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-panel.css b/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-panel.css index ba73dd76ae8..f218582cdda 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-panel.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-panel.css @@ -1,12 +1,9 @@ /* PR Panel */ .am-pr-panel { - display: flex; - flex-direction: column; height: 100%; overflow: hidden; } -/* Shared flex utilities scoped to the PR panel */ .am-pr-row { display: flex; align-items: center; } .am-pr-col { display: flex; flex-direction: column; } From ffacdc60dde5e9295edfa9bf37242dac774003a3 Mon Sep 17 00:00:00 2001 From: cmanu Date: Mon, 3 Aug 2026 15:12:20 -0700 Subject: [PATCH 08/73] feat(pr-view): PR summary --- .../webview-ui/agent-manager/pr/PRPanel.tsx | 75 ++++++++++--------- .../webview-ui/agent-manager/pr/PRSummary.tsx | 67 +++++++++++++++++ .../webview-ui/agent-manager/pr/pr-panel.css | 49 ++++++++++++ 3 files changed, 157 insertions(+), 34 deletions(-) create mode 100644 packages/kilo-vscode/webview-ui/agent-manager/pr/PRSummary.tsx 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 d3c68a71924..aa92f03e8a9 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRPanel.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRPanel.tsx @@ -1,5 +1,5 @@ /** @jsxImportSource solid-js */ -import { Component, Show } from "solid-js" +import { Component, Show, createSignal } from "solid-js" import { IconButton } from "@kilocode/kilo-ui/icon-button" import { Tooltip } from "@kilocode/kilo-ui/tooltip" import { MarkedProvider } from "@kilocode/kilo-ui/context/marked" @@ -12,6 +12,7 @@ import { PRDescription } from "./PRDescription" import { PRFileChanges } from "./PRFileChanges" import { PRChecks } from "./PRChecks" import { PRComments } from "./PRComments" +import { PRSummary } from "./PRSummary" import "./pr-panel.css" interface PRPanelProps { @@ -21,39 +22,45 @@ interface PRPanelProps { onOpenExternal: () => void } -export const PRPanel: Component = (props) => ( - -
-
-
- - {props.pr.title} +export const PRPanel: Component = (props) => { + const [summaryVisible, setSummaryVisible] = createSignal(true) + return ( + +
+
+
+ + {props.pr.title} +
+
+ + + + + + +
-
- - - - - - + + setSummaryVisible(false)} /> + +
+ + + {(comments) => } + + + {(body) => } + + + 0}> + + + + {(comments) => } +
-
- - - {(comments) => } - - - {(body) => } - - - 0}> - - - - {(comments) => } - -
-
-
-) + + ) +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRSummary.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRSummary.tsx new file mode 100644 index 00000000000..2eb405c7765 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRSummary.tsx @@ -0,0 +1,67 @@ +/** @jsxImportSource solid-js */ +import { Component, Show, createSignal } from "solid-js" +import { Icon } from "@kilocode/kilo-ui/icon" +import { IconButton } from "@kilocode/kilo-ui/icon-button" +import type { PRStatus } from "../../src/types/messages" + +interface PRSummaryProps { + pr: PRStatus + onDismiss: () => void +} + +function summaryRows(pr: PRStatus): Array<{ icon: string; label: string; status: string }> { + const rows = [] + + if (pr.checks.total > 0) { + const { passed, total, status } = pr.checks + rows.push({ + icon: status === "success" ? "circle-check" : status === "failure" ? "circle-x-outline" : "play", + label: status === "success" ? "All checks passing" : `${passed}/${total} checks passed`, + status, + }) + } + + const reviewers = pr.comments?.reviewers + if (reviewers && reviewers.length > 0) { + const approved = reviewers.filter((r) => r.state === "approved").length + const changes = reviewers.filter((r) => r.state === "changes_requested").length + const status = approved === reviewers.length ? "success" : changes > 0 ? "failure" : "pending" + rows.push({ + icon: status === "success" ? "circle-check" : status === "failure" ? "circle-x-outline" : "play", + label: approved === reviewers.length ? "All reviewers approved" : `${approved}/${reviewers.length} approvals`, + status, + }) + } + + if (pr.comments && pr.comments.unresolved > 0) { + rows.push({ + icon: "comment", + label: `${pr.comments.unresolved} unresolved comment${pr.comments.unresolved > 1 ? "s" : ""}`, + status: "warning", + }) + } + + return rows +} + +export function PRSummary(props: PRSummaryProps) { + const rows = () => summaryRows(props.pr) + return ( + 0}> +
+
+ PR Summary + +
+
+ {rows().map((row) => ( +
+ + {row.label} +
+ ))} +
+
+
+ ) +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-panel.css b/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-panel.css index f218582cdda..9507c8c5e0f 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-panel.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-panel.css @@ -338,3 +338,52 @@ font-size: var(--kilo-font-size-10); font-variant-numeric: tabular-nums; } + +/* PR Summary banner */ +.am-pr-summary { + margin: 8px 12px; + border: 1px solid var(--vscode-panel-border); + border-radius: 6px; + overflow: hidden; + flex-shrink: 0; +} + +.am-pr-summary-header { + justify-content: space-between; + padding: 6px 6px 6px 10px; + border-bottom: 1px solid var(--vscode-panel-border); + background: var(--vscode-editor-background); +} + +.am-pr-summary-title { + font-size: var(--font-size-small); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--text-weak); +} + +.am-pr-summary-rows { + padding: 6px 10px; + gap: 4px; + background: var(--vscode-editor-background); +} + +.am-pr-summary-row { + gap: 7px; + padding: 2px 0; + font-size: var(--kilo-font-size-12); +} + +.am-pr-summary-icon { + flex-shrink: 0; +} + +.am-pr-summary-row[data-status="success"] .am-pr-summary-icon { color: var(--vscode-testing-iconPassed, #34d399); } +.am-pr-summary-row[data-status="failure"] .am-pr-summary-icon { color: var(--vscode-testing-iconFailed, #f87171); } +.am-pr-summary-row[data-status="pending"] .am-pr-summary-icon { color: var(--vscode-charts-yellow, #fbbf24); } +.am-pr-summary-row[data-status="warning"] .am-pr-summary-icon { color: var(--vscode-charts-yellow, #fbbf24); } + +.am-pr-summary-label { + color: var(--vscode-foreground); +} From c7213b2a9be5dc017abef417dc24fa0899da844b Mon Sep 17 00:00:00 2001 From: cmanu Date: Tue, 4 Aug 2026 08:53:34 -0700 Subject: [PATCH 09/73] feat(pr-view): Use PR icon on badges and update summary to remove dismiss --- .../webview-ui/agent-manager/TabBar.tsx | 2 +- .../webview-ui/agent-manager/pr/PRBadge.tsx | 18 ++++++++++-------- .../webview-ui/agent-manager/pr/PRPanel.tsx | 8 +++----- .../webview-ui/agent-manager/pr/PRSummary.tsx | 5 +---- .../webview-ui/agent-manager/pr/pr-panel.css | 7 +++++++ packages/ui/src/components/icon.tsx | 1 + 6 files changed, 23 insertions(+), 18 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/TabBar.tsx b/packages/kilo-vscode/webview-ui/agent-manager/TabBar.tsx index ab0a888f30e..c21ca2f4705 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/TabBar.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/TabBar.tsx @@ -252,7 +252,7 @@ export const TabBar: Component = (props) => ( onClick={props.onTogglePR} title={`PR #${pr().number}`} > - + #{pr().number} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRBadge.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRBadge.tsx index 9eaec93baef..cb54b3ee913 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRBadge.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRBadge.tsx @@ -1,4 +1,5 @@ /** @jsxImportSource solid-js */ +import { Show } from "solid-js" import { Icon } from "@kilocode/kilo-ui/icon" import type { PRStatus } from "../../src/types/messages" import { prBadgeIndicator, prChecksRunning } from "../WorktreeItem" @@ -7,7 +8,6 @@ const INDICATOR_ICON: Record = { failure: "circle-x", changes: "warning", approved: "circle-check", - none: "branch", } export function PRBadge(props: { pr: PRStatus }) { @@ -24,13 +24,15 @@ export function PRBadge(props: { pr: PRStatus }) { "am-pr-badge-pending": prChecksRunning(props.pr), }} > - - #{props.pr.number} + + + + ) } 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 aa92f03e8a9..dda904744ec 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRPanel.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRPanel.tsx @@ -1,5 +1,5 @@ /** @jsxImportSource solid-js */ -import { Component, Show, createSignal } from "solid-js" +import { Component, Show } from "solid-js" import { IconButton } from "@kilocode/kilo-ui/icon-button" import { Tooltip } from "@kilocode/kilo-ui/tooltip" import { MarkedProvider } from "@kilocode/kilo-ui/context/marked" @@ -23,7 +23,6 @@ interface PRPanelProps { } export const PRPanel: Component = (props) => { - const [summaryVisible, setSummaryVisible] = createSignal(true) return (
@@ -31,6 +30,7 @@ export const PRPanel: Component = (props) => {
{props.pr.title} + #{props.pr.number}
@@ -41,9 +41,7 @@ export const PRPanel: Component = (props) => {
- - setSummaryVisible(false)} /> - +
diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRSummary.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRSummary.tsx index 2eb405c7765..c04e89a5fe4 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRSummary.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRSummary.tsx @@ -1,12 +1,10 @@ /** @jsxImportSource solid-js */ -import { Component, Show, createSignal } from "solid-js" +import { Show } from "solid-js" import { Icon } from "@kilocode/kilo-ui/icon" -import { IconButton } from "@kilocode/kilo-ui/icon-button" import type { PRStatus } from "../../src/types/messages" interface PRSummaryProps { pr: PRStatus - onDismiss: () => void } function summaryRows(pr: PRStatus): Array<{ icon: string; label: string; status: string }> { @@ -51,7 +49,6 @@ export function PRSummary(props: PRSummaryProps) {
PR Summary -
{rows().map((row) => ( diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-panel.css b/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-panel.css index 9507c8c5e0f..897f1874a6e 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-panel.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-panel.css @@ -49,6 +49,13 @@ white-space: nowrap; } +.am-pr-panel-number { + font-size: var(--kilo-font-size-12); + color: var(--vscode-descriptionForeground); + font-variant-numeric: tabular-nums; + flex-shrink: 0; +} + .am-pr-panel-actions { gap: 2px; flex-shrink: 0; diff --git a/packages/ui/src/components/icon.tsx b/packages/ui/src/components/icon.tsx index e70de73a831..e2170c0772e 100644 --- a/packages/ui/src/components/icon.tsx +++ b/packages/ui/src/components/icon.tsx @@ -91,6 +91,7 @@ const icons = { download: ``, menu: ``, server: ``, + "pull-request": ``, branch: ``, edit: ``, history: ``, From 93fde67c76cad60fdd021905b5e16a8d53693944 Mon Sep 17 00:00:00 2001 From: cmanu Date: Tue, 4 Aug 2026 08:56:13 -0700 Subject: [PATCH 10/73] feat(pr-view): Use PR icon on worktree item and remove pr number --- .../kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx b/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx index d33b0e0a61d..b6fa2d92898 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx @@ -356,7 +356,8 @@ export const WorktreeItem: Component = (props) => { }} onClick={handleOpenPR} > - }> + + @@ -372,7 +373,6 @@ export const WorktreeItem: Component = (props) => { /> - #{pr().number} ) }} From bd67e85902e56eb9cc8ac00045209838c04b046f Mon Sep 17 00:00:00 2001 From: cmanu Date: Tue, 4 Aug 2026 09:08:12 -0700 Subject: [PATCH 11/73] feat(pr-view): Styling and copy/paste on PR comments --- .../agent-manager/agent-manager.css | 2 +- .../agent-manager/pr/CopyButton.tsx | 22 +++++++++++++++++++ .../agent-manager/pr/PRComments.tsx | 2 ++ .../webview-ui/agent-manager/pr/pr-panel.css | 6 ++++- 4 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 packages/kilo-vscode/webview-ui/agent-manager/pr/CopyButton.tsx diff --git a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css index 79e0fee011a..ee101e834e9 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css @@ -861,7 +861,7 @@ button.am-section-toggle:hover .am-section-label { display: inline-flex; align-items: center; gap: 2px; - padding: 2px 7px 2px 4px; + padding: 2px 4px; border-radius: 6px; border: none; background: color-mix(in srgb, var(--pr-accent) 12%, transparent); diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/CopyButton.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/CopyButton.tsx new file mode 100644 index 00000000000..4a856df8d5b --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/CopyButton.tsx @@ -0,0 +1,22 @@ +/** @jsxImportSource solid-js */ +import { createSignal } from "solid-js" +import { IconButton } from "@kilocode/kilo-ui/icon-button" + +export function CopyButton(props: { text: string; label?: string; class?: string }) { + const [copied, setCopied] = createSignal(false) + const copy = () => { + navigator.clipboard.writeText(props.text) + setCopied(true) + setTimeout(() => setCopied(false), 1500) + } + return ( + + ) +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRComments.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRComments.tsx index d3f3057e6c0..6745a0522c3 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRComments.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRComments.tsx @@ -4,6 +4,7 @@ import { Markdown } from "@kilocode/kilo-ui/markdown" import type { PRStatus } from "../../src/types/messages" import type { PRComment } from "./pr-types" import { SectionHeading } from "./SectionHeading" +import { CopyButton } from "./CopyButton" export function PRComments(props: { comments: NonNullable }) { const [open, setOpen] = createSignal(true) @@ -33,6 +34,7 @@ export function PRComments(props: { comments: NonNullable Resolved +
diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-panel.css b/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-panel.css index 897f1874a6e..4a1183bb125 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-panel.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-panel.css @@ -25,7 +25,7 @@ display: inline-flex; align-items: center; gap: 4px; - padding: 2px 6px 2px 4px; + padding: 2px 4px; border-radius: 4px; font-size: var(--kilo-font-size-10); font-variant-numeric: tabular-nums; @@ -225,6 +225,10 @@ margin-left: auto; } +.am-pr-copy-btn { + margin-left: auto; +} + .am-pr-panel-comment-body [data-component="markdown"] { font-size: var(--kilo-font-size-12); color: var(--vscode-foreground); From 70c37d66d8d11322ba82e7d17d783945d054f0b3 Mon Sep 17 00:00:00 2001 From: cmanu Date: Tue, 4 Aug 2026 16:05:08 -0700 Subject: [PATCH 12/73] feat(pr-view): show code -X/+Y on code diff and remove PR number --- .../webview-ui/agent-manager/TabBar.tsx | 46 ++++++++++--------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/TabBar.tsx b/packages/kilo-vscode/webview-ui/agent-manager/TabBar.tsx index c21ca2f4705..c315cc3a7a1 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/TabBar.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/TabBar.tsx @@ -218,6 +218,19 @@ export const TabBar: Component = (props) => ( ) })()} + + {(pr) => ( + + + + )} + = (props) => ( title={props.t("agentManager.diff.toggle")} > - - - 0}> - {stats()!.files}f + + {(pr) => ( + 0 || pr().deletions > 0}> + + 0}> + +{pr().additions} + + 0}> + −{pr().deletions} + + - +{stats()!.additions} - −{stats()!.deletions} - + )} @@ -244,20 +262,6 @@ export const TabBar: Component = (props) => ( ) })()} - - {(pr) => ( - - - - )} - Date: Tue, 4 Aug 2026 16:15:47 -0700 Subject: [PATCH 13/73] feat(pr-view): fix styles to add variables and light theme fix for svg color. Make summary unfixed. --- .../agent-manager/agent-manager.css | 52 +++++++++++-------- .../webview-ui/agent-manager/pr/PRPanel.tsx | 2 +- .../webview-ui/agent-manager/pr/pr-panel.css | 1 - 3 files changed, 30 insertions(+), 25 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css index d27fc1dca86..88eccf3932c 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css @@ -796,7 +796,7 @@ html[data-theme="kilo-vscode"] } .am-run-badge[data-run-state="stopping"] { - --am-run-accent: #fbbf24; + --am-run-accent: var(--vscode-charts-yellow, #fbbf24); } .am-run-badge[data-run-state="success"] { @@ -804,7 +804,7 @@ html[data-theme="kilo-vscode"] } .am-run-badge[data-run-state="error"] { - --am-run-accent: #f87171; + --am-run-accent: var(--vscode-testing-iconFailed, #f87171); } @keyframes am-run-pulse { @@ -854,11 +854,11 @@ html[data-theme="kilo-vscode"] } .am-stat-additions { - color: #34d399; + color: var(--vscode-testing-iconPassed, #34d399); } .am-stat-deletions { - color: #f87171; + color: var(--vscode-testing-iconFailed, #f87171); } /* PR badge accent colors via classList */ @@ -962,35 +962,35 @@ html[data-theme="kilo-vscode"] } .am-worktree-item-active .am-stat-files { - color: rgba(255, 255, 255, 0.7); + color: var(--text-weak); } .am-worktree-item-active .am-stat-additions { - color: #6ee7b7; + color: var(--vscode-testing-iconPassed, #6ee7b7); } .am-worktree-item-active .am-stat-deletions { - color: #fca5a5; + color: var(--vscode-testing-iconFailed, #fca5a5); } .am-worktree-commits { - color: #34d399; + color: var(--vscode-testing-iconPassed, #34d399); } .am-worktree-behind { - color: #f87171; + color: var(--vscode-testing-iconFailed, #f87171); } .am-worktree-item-active .am-worktree-commits { - color: #6ee7b7; + color: var(--vscode-testing-iconPassed, #6ee7b7); } .am-worktree-item-active .am-worktree-behind { - color: #fca5a5; + color: var(--vscode-testing-iconFailed, #fca5a5); } .am-local-item-active .am-worktree-behind { - color: #fca5a5; + color: var(--vscode-testing-iconFailed, #fca5a5); } .am-local-item:hover .am-worktree-stats, @@ -1664,7 +1664,7 @@ body.am-wt-dragging-active * { } .am-sidebar-search-status { - color: #fbbf24; + color: var(--vscode-charts-yellow, #fbbf24); text-transform: uppercase; } @@ -1759,9 +1759,10 @@ body.am-wt-dragging-active * { color: var(--text-base); } -.am-diff-toggle-btn.am-tab-diff-btn-active { - background: var(--surface-interactive-base) !important; - color: var(--text-base) !important; +.am-diff-toggle-btn.am-tab-diff-btn-active, +.am-diff-toggle-btn.am-tab-diff-btn-active:hover { + background: var(--surface-interactive-base); + color: var(--text-on-interactive-base); } .am-diff-toggle-btn.am-diff-toggle-has-changes { @@ -1775,20 +1776,24 @@ body.am-wt-dragging-active * { } .am-diff-toggle-btn.am-tab-diff-btn-active .am-stat-files { - color: rgba(255, 255, 255, 0.7); + color: var(--text-weak); } .am-diff-toggle-btn.am-tab-diff-btn-active .am-stat-additions { - color: #6ee7b7; + color: var(--vscode-testing-iconPassed, #6ee7b7); } .am-diff-toggle-btn.am-tab-diff-btn-active .am-stat-deletions { - color: #fca5a5; + color: var(--vscode-testing-iconFailed, #fca5a5); } .am-tab-diff-btn-active { background: var(--surface-interactive-base) !important; - color: var(--text-base) !important; + color: var(--text-on-interactive-base) !important; +} + +.am-tab-diff-btn-active [data-slot="icon-svg"] { + color: var(--text-on-interactive-base) !important; } .am-detail-content { @@ -2899,9 +2904,10 @@ body.am-wt-dragging-active * { color: var(--text-base); } -.am-nv-pill-active { - background: var(--border-focus, #007fd4) !important; - color: #fff !important; +.am-nv-pill-active, +.am-nv-pill-active:hover { + background: var(--border-focus, #007fd4); + color: var(--text-on-interactive-base); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); } 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 dda904744ec..7bf8aef9f7b 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRPanel.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRPanel.tsx @@ -41,8 +41,8 @@ export const PRPanel: Component = (props) => {
-
+ {(comments) => } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-panel.css b/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-panel.css index 4a1183bb125..6f1cc8cc260 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-panel.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-panel.css @@ -356,7 +356,6 @@ border: 1px solid var(--vscode-panel-border); border-radius: 6px; overflow: hidden; - flex-shrink: 0; } .am-pr-summary-header { From cc900b277781b787f19d74e6b940acd6795e5470 Mon Sep 17 00:00:00 2001 From: cmanu Date: Wed, 5 Aug 2026 10:16:04 -0700 Subject: [PATCH 14/73] feat(pr-view): Organizing code; Adjust colors for themes; Improve types and icon consistency --- .../src/agent-manager/PRStatusPoller.ts | 133 +----------------- .../src/agent-manager/am-pr-utils.ts | 119 ++++++++++++++++ .../agent-manager/AgentManagerApp.tsx | 40 +++--- .../webview-ui/agent-manager/WorktreeItem.tsx | 2 +- .../agent-manager/agent-manager.css | 2 +- .../webview-ui/agent-manager/pr/PRBadge.tsx | 2 +- .../agent-manager/side-panel-layout.ts | 6 + .../agent-manager/terminal/ambient.ts | 3 +- 8 files changed, 156 insertions(+), 151 deletions(-) create mode 100644 packages/kilo-vscode/src/agent-manager/am-pr-utils.ts diff --git a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts index a2a6ddd51d6..813c79f25d9 100644 --- a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts +++ b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts @@ -1,9 +1,11 @@ import type { ExecFileOptionsWithStringEncoding } from "child_process" import type { Worktree } from "./WorktreeStateManager" -import type { PRStatus, PRCheck, PRComment, PRReviewer, CheckStatus, AggregateCheckStatus, PRState, ReviewDecision } from "./types" +import type { PRStatus, PRCheck, PRComment, PRReviewer, AggregateCheckStatus } from "./types" import { execWithShellEnv } from "./shell-env" import { classifyPRError } from "./git-import" import type { Semaphore } from "./semaphore" +import { parsePRResult, checkStatus, formatCheckDuration, parseComments, parseReviewers } from "./am-pr-utils" +import type { PRResult, GhThread, GhReviewRequest, GhReview } from "./am-pr-utils" interface PRStatusPollerOptions { getWorktrees: () => Worktree[] @@ -387,7 +389,7 @@ export class PRStatusPoller { const checks: PRCheck[] = data.map((c) => ({ name: c.name, - status: mapCheckStatus(c.state), + status: checkStatus(c.state), url: c.link, duration: formatCheckDuration(c.startedAt, c.completedAt), })) @@ -460,8 +462,8 @@ export class PRStatusPoller { { cwd, timeout: 15_000 }, ) const pr = JSON.parse(stdout)?.data?.repository?.pullRequest - const comments = parseComments((pr?.reviewThreads?.nodes ?? []) as GQLThread[]) - const reviewers = parseReviewers((pr?.reviewRequests?.nodes ?? []) as GQLReviewRequest[], (pr?.reviews?.nodes ?? []) as GQLReview[]) + const comments = parseComments((pr?.reviewThreads?.nodes ?? []) as GhThread[]) + const reviewers = parseReviewers((pr?.reviewRequests?.nodes ?? []) as GhReviewRequest[], (pr?.reviews?.nodes ?? []) as GhReview[]) return { total: comments.length, unresolved: comments.filter((c) => !c.resolved).length, comments, reviewers } } catch (err) { this.options.log("Failed to fetch PR comments:", err) @@ -470,129 +472,6 @@ export class PRStatusPoller { } } -interface PRResult { - number: number - title: string - body: string - url: string - state: PRState - review: ReviewDecision | null - additions: number - deletions: number - files: number -} - -function parsePRResult(json: string): PRResult | null { - const data = JSON.parse(json) - if (!data.number) return null - return { - number: data.number, - title: data.title ?? "", - body: data.body ?? "", - url: data.url ?? "", - state: parsePRState(data.isDraft, data.state), - review: parseReviewDecision(data.reviewDecision), - additions: data.additions ?? 0, - deletions: data.deletions ?? 0, - files: data.changedFiles ?? 0, - } -} - -function parsePRState(isDraft: boolean, ghState: string): PRState { - if (isDraft) return "draft" - if (ghState === "MERGED") return "merged" - if (ghState === "CLOSED") return "closed" - return "open" -} - -function parseReviewDecision(decision: string | undefined): ReviewDecision | null { - if (decision === "APPROVED") return "approved" - if (decision === "CHANGES_REQUESTED") return "changes_requested" - if (decision === "REVIEW_REQUIRED") return "pending" - return null -} - -function mapCheckStatus(state: string): CheckStatus { - switch (state.toUpperCase()) { - case "SUCCESS": - return "success" - case "FAILURE": - case "ERROR": - return "failure" - case "PENDING": - case "QUEUED": - case "IN_PROGRESS": - case "REQUESTED": - case "WAITING": - return "pending" - case "SKIPPED": - return "skipped" - case "CANCELLED": - case "TIMED_OUT": - case "STALE": - case "STARTUP_FAILURE": - return "cancelled" - default: - return "pending" - } -} - -function formatCheckDuration(startedAt?: string, completedAt?: string): string | undefined { - if (!startedAt || !completedAt) return undefined - const secs = Math.round((new Date(completedAt).getTime() - new Date(startedAt).getTime()) / 1000) - return secs < 60 ? `${secs}s` : `${Math.floor(secs / 60)}m ${secs % 60}s` -} - -interface GQLAuthor { login?: string; avatarUrl?: string } -interface GQLComment { id: string; author?: GQLAuthor; body?: string; path?: string; line?: number; url?: string; createdAt?: string } -interface GQLThread { isResolved?: boolean; comments?: { nodes?: GQLComment[] } } -interface GQLReviewRequest { requestedReviewer?: GQLAuthor } -interface GQLReview { author?: GQLAuthor; state?: string } - -function parseComments(threads: GQLThread[]): PRComment[] { - const items: PRComment[] = [] - for (const thread of threads) { - const first = thread.comments?.nodes?.[0] - if (!first) continue - items.push({ - id: first.id, - author: first.author?.login ?? "unknown", - avatar: first.author?.avatarUrl, - body: first.body ?? "", - file: first.path, - line: first.line, - url: first.url, - resolved: thread.isResolved ?? false, - createdAt: first.createdAt ? new Date(first.createdAt).getTime() : undefined, - }) - } - return items -} - -function parseReviewers(requests: GQLReviewRequest[], reviews: GQLReview[]): PRReviewer[] { - const map = new Map() - for (const node of requests) { - const user = node.requestedReviewer - if (!user?.login) continue - map.set(user.login, { login: user.login, avatar: user.avatarUrl, state: "pending" }) - } - for (const node of reviews) { - const login = node.author?.login - if (!login) continue - const state = REVIEWER_STATE[node.state ?? ""] ?? "pending" - if (!map.has(login) || state !== "commented") { - map.set(login, { login, avatar: node.author?.avatarUrl, state }) - } - } - return [...map.values()] -} - -const REVIEWER_STATE: Record = { - APPROVED: "approved", - CHANGES_REQUESTED: "changes_requested", - COMMENTED: "commented", -} - /** Run async thunks with bounded concurrency, returning settled results. */ async function settled(thunks: (() => Promise)[], concurrency: number): Promise[]> { const results: PromiseSettledResult[] = new Array(thunks.length) diff --git a/packages/kilo-vscode/src/agent-manager/am-pr-utils.ts b/packages/kilo-vscode/src/agent-manager/am-pr-utils.ts new file mode 100644 index 00000000000..976b486db80 --- /dev/null +++ b/packages/kilo-vscode/src/agent-manager/am-pr-utils.ts @@ -0,0 +1,119 @@ +import type { PRState, ReviewDecision, CheckStatus, PRComment, PRReviewer } from "./types" + +// Raw shapes returned by `gh pr view --json` + +export interface GhAuthor { login?: string; avatarUrl?: string } +export interface GhComment { id: string; author?: GhAuthor; body?: string; path?: string; line?: number; url?: string; createdAt?: string } +export interface GhThread { isResolved?: boolean; comments?: { nodes?: GhComment[] } } +export interface GhReviewRequest { requestedReviewer?: GhAuthor } +export interface GhReview { author?: GhAuthor; state?: string } + +export interface PRResult { + number: number + title: string + body: string + url: string + state: PRState + review: ReviewDecision | null + additions: number + deletions: number + files: number +} + +export function parsePRResult(json: string): PRResult | null { + const data = JSON.parse(json) + if (!data.number) return null + const state: PRState = data.isDraft ? "draft" : (data.state?.toLowerCase() as PRState) ?? "open" + const decision = data.reviewDecision as string | undefined + const review: ReviewDecision | null = + decision === "APPROVED" ? "approved" + : decision === "CHANGES_REQUESTED" ? "changes_requested" + : decision === "REVIEW_REQUIRED" ? "pending" + : null + return { + number: data.number, + title: data.title ?? "", + body: data.body ?? "", + url: data.url ?? "", + state, + review, + additions: data.additions ?? 0, + deletions: data.deletions ?? 0, + files: data.changedFiles ?? 0, + } +} + +export function checkStatus(state: string): CheckStatus { + switch (state.toUpperCase()) { + case "SUCCESS": + return "success" + case "FAILURE": + case "ERROR": + return "failure" + case "PENDING": + case "QUEUED": + case "IN_PROGRESS": + case "REQUESTED": + case "WAITING": + return "pending" + case "SKIPPED": + return "skipped" + case "CANCELLED": + case "TIMED_OUT": + case "STALE": + case "STARTUP_FAILURE": + return "cancelled" + default: + return "pending" + } +} + +export function formatCheckDuration(startedAt?: string, completedAt?: string): string | undefined { + if (!startedAt || !completedAt) return undefined + const secs = Math.round((new Date(completedAt).getTime() - new Date(startedAt).getTime()) / 1000) + return secs < 60 ? `${secs}s` : `${Math.floor(secs / 60)}m ${secs % 60}s` +} + +const REVIEWER_STATE: Record = { + APPROVED: "approved", + CHANGES_REQUESTED: "changes_requested", + COMMENTED: "commented", +} + +export function parseComments(threads: GhThread[]): PRComment[] { + const items: PRComment[] = [] + for (const thread of threads) { + const first = thread.comments?.nodes?.[0] + if (!first) continue + items.push({ + id: first.id, + author: first.author?.login ?? "unknown", + avatar: first.author?.avatarUrl, + body: first.body ?? "", + file: first.path, + line: first.line, + url: first.url, + resolved: thread.isResolved ?? false, + createdAt: first.createdAt ? new Date(first.createdAt).getTime() : undefined, + }) + } + return items +} + +export function parseReviewers(requests: GhReviewRequest[], reviews: GhReview[]): PRReviewer[] { + const map = new Map() + for (const node of requests) { + const user = node.requestedReviewer + if (!user?.login) continue + map.set(user.login, { login: user.login, avatar: user.avatarUrl, state: "pending" }) + } + for (const node of reviews) { + const login = node.author?.login + if (!login) continue + const state = REVIEWER_STATE[node.state ?? ""] ?? "pending" + if (!map.has(login) || state !== "commented") { + map.set(login, { login, avatar: node.author?.avatarUrl, state }) + } + } + return [...map.values()] +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index 09b36c3439d..842a33b2540 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -175,7 +175,7 @@ import { createMarkdownRender } from "./review-preferences" import { createSidebarCollapse } from "./sidebar-collapse" import { SidebarToggleButton } from "./SidebarToggleButton" import { setTabWidths } from "./tab-widths" -import { clampPanelWidth, maxPanelWidth, minPanelWidth } from "./side-panel-layout" +import { clampPanelWidth, maxPanelWidth, minPanelWidth, SidePanel } from "./side-panel-layout" import { buildShortcutCategories } from "./shortcuts" import { tracker } from "./telemetry" import { createChatFocus, hasQuestionOption } from "./focus" @@ -195,7 +195,7 @@ interface SetupState { /** Sidebar selection: LOCAL for local repo, worktree ID for a worktree, or null for an unassigned session. */ type SidebarSelection = typeof LOCAL | string | null -type SidePanel = "diff" | "pr" | "terminal" | null +export type SidePanelState = SidePanel | null const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent) // Fallback keybindings before extension sends resolved ones const MAX_JUMP_INDEX = 9 @@ -309,9 +309,9 @@ const AgentManagerContent: Component = () => { let pendingSideWidth: number | undefined const [history, setHistory] = createSignal(false) - const [sidePanel, setSidePanel] = createSignal(null) - const diffOpen = () => sidePanel() === "diff" - const prOpen = () => sidePanel() === "pr" + const [sidePanel, setSidePanel] = createSignal(null) + const diffOpen = () => sidePanel() === SidePanel.Diff + const prOpen = () => sidePanel() === SidePanel.PR const activePR = createMemo(() => { const selected = selection() if (!selected || selected === LOCAL) return undefined @@ -338,7 +338,7 @@ const AgentManagerContent: Component = () => { const showSideTerminal = () => { setHistory(false) setReviewActive(false) - setSidePanel("terminal") + setSidePanel(SidePanel.Terminal) } const [reviewOpenByContext, setReviewOpenByContext] = createSignal>({}) @@ -423,7 +423,7 @@ const AgentManagerContent: Component = () => { const rememberPromptFocus = (focused: boolean) => { if (focused) focusMemory.set(focusKey(), "prompt") } - const terminalVisible = () => sidePanel() === "terminal" && !history() && !reviewActive() + const terminalVisible = () => sidePanel() === SidePanel.Terminal && !history() && !reviewActive() const focusOnDraftChange = () => { const key = focusKey() const owner = focusMemory.get(key) @@ -551,7 +551,7 @@ const AgentManagerContent: Component = () => { const togglePRPanel = () => { setHistory(false) setReviewActive(false) - setSidePanel((prev) => (prev === "pr" ? null : "pr")) + setSidePanel((prev) => (prev === SidePanel.PR ? null : SidePanel.PR)) } const openSelectedPR = () => { @@ -1187,8 +1187,8 @@ const AgentManagerContent: Component = () => { } else if (msg.action === "toggleDiff") { if (reviewActive()) { closeReviewTab() - setSidePanel("diff") - } else setSidePanel((prev) => (prev === "diff" ? null : "diff")) + setSidePanel(SidePanel.Diff) + } else setSidePanel((prev) => (prev === SidePanel.Diff ? null : SidePanel.Diff)) } else if (msg.action === "newTab") handleNewTabForCurrentSelection() else if (msg.action === "closeTab") closeActiveTab() else if (msg.action === "newWorktree") showNewWorktreeDialog() @@ -1350,7 +1350,7 @@ const AgentManagerContent: Component = () => { onSideCreated: (contextKey, terminalId, focus) => { // Focus only when the user is still looking at this panel — // a slow create landing after a mode switch must not steal it. - if (focus && sidePanel() === "terminal" && !history() && !reviewActive() && terms.sideKey() === contextKey) { + if (focus && sidePanel() === SidePanel.Terminal && !history() && !reviewActive() && terms.sideKey() === contextKey) { terms.requestFocus(terminalId) } }, @@ -1419,7 +1419,7 @@ const AgentManagerContent: Component = () => { if (!isActivePayload(ev.projectId)) return // Close diff/review panels — nothing to show during setup. // Terminal panels keep live setup output, so they stay open. - if (sidePanel() === "diff") setSidePanel(null) + if (sidePanel() === SidePanel.Diff) setSidePanel(null) setReviewActive(false) setSetup({ active: true, message: ev.message, branch: ev.branch, worktreeId: ev.worktreeId }) } @@ -2090,7 +2090,7 @@ const AgentManagerContent: Component = () => { const sideCtl = createSideTerminal({ handlers: termHandlers, - visible: () => sidePanel() === "terminal" && !history() && !reviewActive(), + visible: () => sidePanel() === SidePanel.Terminal && !history() && !reviewActive(), focusedId: () => terms.sideFocusedId(), hide: () => { cancelAmbientSetup() @@ -2217,7 +2217,7 @@ const AgentManagerContent: Component = () => { // A focused side terminal owns Cmd+W while its panel is visible — // closing a chat tab out from under the user's cursor would be // surprising. Only that terminal dies; the panel keeps the rest. - if (sidePanel() === "terminal" && terms.sideFocusedId()) { + if (sidePanel() === SidePanel.Terminal && terms.sideFocusedId()) { if (sideCtl.close()) return } if (termHandlers.closeActive()) { @@ -2269,10 +2269,10 @@ const AgentManagerContent: Component = () => { }) if (reviewActive()) { closeReviewTab() - setSidePanel("diff") + setSidePanel(SidePanel.Diff) return } - setSidePanel((prev) => (prev === "diff" ? null : "diff")) + setSidePanel((prev) => (prev === SidePanel.Diff ? null : SidePanel.Diff)) } const renderTabById = (id: string) => @@ -2470,7 +2470,7 @@ const AgentManagerContent: Component = () => { prOpen={prOpen} onTogglePR={togglePRPanel} terminalDestination={sideCtl.destination} - terminalDestinationActive={() => sidePanel() === "terminal"} + terminalDestinationActive={() => sidePanel() === SidePanel.Terminal} terminalKeybind={() => kb().showTerminal ?? ""} onTerminalDestinationOpen={() => { cancelAmbientSetup() @@ -2658,7 +2658,7 @@ const AgentManagerContent: Component = () => { />
- + { activeTerminalId={terms.activeId()} /> - + {(() => { const data = activePR()! return ( @@ -2709,7 +2709,7 @@ const AgentManagerContent: Component = () => { sidePanel() === "terminal"} + visible={() => sidePanel() === SidePanel.Terminal} onSelect={(id) => termHandlers.selectSide(id)} onClose={(id) => { cancelAmbientSetup() diff --git a/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx b/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx index b6fa2d92898..69378459144 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx @@ -359,7 +359,7 @@ export const WorktreeItem: Component = (props) => { - + diff --git a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css index 88eccf3932c..75953b058b5 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css @@ -869,7 +869,7 @@ html[data-theme="kilo-vscode"] .am-pr-accent-draft, .am-pr-panel-badge.am-pr-accent-draft { --pr-accent: var(--text-weaker); } .am-pr-accent-merged, -.am-pr-panel-badge.am-pr-accent-merged { --pr-accent: var(--vscode-charts-purple, #a78bfa); } +.am-pr-panel-badge.am-pr-accent-merged { --pr-accent: var(--vscode-charts-purple, var(--vscode-foreground)); } .am-pr-accent-closed, .am-pr-panel-badge.am-pr-accent-closed { --pr-accent: var(--vscode-testing-iconFailed, #f87171); } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRBadge.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRBadge.tsx index cb54b3ee913..d77213de726 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRBadge.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRBadge.tsx @@ -5,7 +5,7 @@ import type { PRStatus } from "../../src/types/messages" import { prBadgeIndicator, prChecksRunning } from "../WorktreeItem" const INDICATOR_ICON: Record = { - failure: "circle-x", + failure: "circle-x-outline", changes: "warning", approved: "circle-check", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/side-panel-layout.ts b/packages/kilo-vscode/webview-ui/agent-manager/side-panel-layout.ts index 8cfaafe4f9e..ed5cd0097a1 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/side-panel-layout.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/side-panel-layout.ts @@ -2,6 +2,12 @@ export const MIN_PANEL_WIDTH = 360 const DEFAULT_PANEL_WIDTH_RATIO = 0.5 const MAX_PANEL_WIDTH_RATIO = 0.8 +export enum SidePanel { + Diff = "diff", + PR = "pr", + Terminal = "terminal", +} + function viewportWidth(viewport: number): number { return Number.isFinite(viewport) && viewport > 0 ? viewport : MIN_PANEL_WIDTH } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/terminal/ambient.ts b/packages/kilo-vscode/webview-ui/agent-manager/terminal/ambient.ts index aac693f50e7..c674c80d79f 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/terminal/ambient.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/terminal/ambient.ts @@ -13,11 +13,12 @@ import { createEffect, createSignal, type Accessor } from "solid-js" import type { ScriptTerminalStatus, TerminalStateControls } from "./state" import type { TerminalTabStateWithContext } from "./state" +import type { SidePanelState } from "../AgentManagerApp" interface AmbientSetupDeps { terms: TerminalStateControls selection: Accessor - sidePanel: Accessor + sidePanel: Accessor setSidePanel(panel: null): void } From bddce1a49dd78c9673e971cdedd3665758378ac6 Mon Sep 17 00:00:00 2001 From: Christiaan Arnoldus Date: Thu, 6 Aug 2026 12:28:30 +0200 Subject: [PATCH 15/73] fix: infer custom provider reasoning efforts --- .changeset/broad-custom-provider-efforts.md | 6 + .../kilo-vscode/src/shared/custom-provider.ts | 9 +- .../custom-provider-dialog-validate.test.ts | 28 ++ .../tests/unit/custom-provider.test.ts | 29 ++ .../tests/unit/provider-actions-save.test.ts | 21 ++ .../settings/CustomProviderDialog.tsx | 38 +-- .../settings/CustomProviderModelCard.tsx | 306 +----------------- .../settings/CustomProviderValidation.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/ar.ts | 38 --- .../kilo-vscode/webview-ui/src/i18n/br.ts | 39 --- .../kilo-vscode/webview-ui/src/i18n/bs.ts | 39 --- .../kilo-vscode/webview-ui/src/i18n/da.ts | 39 --- .../kilo-vscode/webview-ui/src/i18n/de.ts | 39 --- .../kilo-vscode/webview-ui/src/i18n/en.ts | 39 --- .../kilo-vscode/webview-ui/src/i18n/es.ts | 39 --- .../kilo-vscode/webview-ui/src/i18n/fa.ts | 39 --- .../kilo-vscode/webview-ui/src/i18n/fr.ts | 39 --- .../kilo-vscode/webview-ui/src/i18n/it.ts | 39 --- .../kilo-vscode/webview-ui/src/i18n/ja.ts | 38 --- .../kilo-vscode/webview-ui/src/i18n/ko.ts | 38 --- .../kilo-vscode/webview-ui/src/i18n/nl.ts | 39 --- .../kilo-vscode/webview-ui/src/i18n/no.ts | 39 --- .../kilo-vscode/webview-ui/src/i18n/pl.ts | 39 --- .../kilo-vscode/webview-ui/src/i18n/ru.ts | 39 --- .../kilo-vscode/webview-ui/src/i18n/th.ts | 39 --- .../kilo-vscode/webview-ui/src/i18n/tr.ts | 39 --- .../kilo-vscode/webview-ui/src/i18n/uk.ts | 39 --- .../kilo-vscode/webview-ui/src/i18n/zh.ts | 38 --- .../kilo-vscode/webview-ui/src/i18n/zht.ts | 38 --- .../src/kilocode/provider/provider.ts | 33 ++ packages/opencode/src/provider/provider.ts | 10 +- .../kilocode/custom-provider-variants.test.ts | 46 +++ .../provider-reasoning-options.test.ts | 53 +++ 33 files changed, 230 insertions(+), 1164 deletions(-) create mode 100644 .changeset/broad-custom-provider-efforts.md create mode 100644 packages/opencode/test/kilocode/custom-provider-variants.test.ts diff --git a/.changeset/broad-custom-provider-efforts.md b/.changeset/broad-custom-provider-efforts.md new file mode 100644 index 00000000000..2b275555aa1 --- /dev/null +++ b/.changeset/broad-custom-provider-efforts.md @@ -0,0 +1,6 @@ +--- +"@kilocode/cli": patch +"kilo-code": patch +--- + +Automatically expose broad reasoning effort options for custom provider models instead of requiring manual variant configuration. diff --git a/packages/kilo-vscode/src/shared/custom-provider.ts b/packages/kilo-vscode/src/shared/custom-provider.ts index 757897d1c16..3ac8ddb917b 100644 --- a/packages/kilo-vscode/src/shared/custom-provider.ts +++ b/packages/kilo-vscode/src/shared/custom-provider.ts @@ -12,14 +12,7 @@ export const EnvSchema = z .trim() .regex(/^[A-Z_][A-Z0-9_]*$/, INVALID_ENV) -const VariantConfigSchema = z.object({ - enable_thinking: z.boolean().optional(), - thinking: z.object({ type: z.enum(["enabled", "disabled", "adaptive"]) }).optional(), - reasoning_split: z.boolean().optional(), - reasoningEffort: z.enum(["none", "minimal", "low", "medium", "high", "xhigh"]).optional(), - effort: z.enum(["low", "medium", "high", "xhigh", "max"]).optional(), - chat_template_args: z.object({ enable_thinking: z.boolean() }).optional(), -}) +const VariantConfigSchema = z.record(z.string(), z.unknown()) export type VariantConfig = z.infer diff --git a/packages/kilo-vscode/tests/unit/custom-provider-dialog-validate.test.ts b/packages/kilo-vscode/tests/unit/custom-provider-dialog-validate.test.ts index be1f69c0504..ff405481c06 100644 --- a/packages/kilo-vscode/tests/unit/custom-provider-dialog-validate.test.ts +++ b/packages/kilo-vscode/tests/unit/custom-provider-dialog-validate.test.ts @@ -206,6 +206,34 @@ describe("validateCustomProvider – variant name validation", () => { }) }) + it("preserves opaque variant options after the editor controls are removed", () => { + const form = base() + const raw = { + thinking: { type: "adaptive", display: "summarized" }, + reasoningSummary: "auto", + include: ["reasoning.encrypted_content"], + customOption: { enabled: true }, + } + form.models[0].reasoning = true + form.models[0].variants = [ + { + name: "high", + raw, + enableThinking: undefined, + thinking: "adaptive", + splitReasoning: undefined, + outputEffort: undefined, + reasoningEffort: undefined, + chatTemplateArgs: undefined, + }, + ] + + const out = validateCustomProvider(args(form)) + expect(out.result).toBeDefined() + const saved = out.result!.config.models["model-1"] as Record + expect(saved.variants).toEqual({ high: raw }) + }) + it("serializes image modality when supportsImages is set", () => { const form = base() form.models[0].supportsImages = true diff --git a/packages/kilo-vscode/tests/unit/custom-provider.test.ts b/packages/kilo-vscode/tests/unit/custom-provider.test.ts index b4869706e43..30b420abb95 100644 --- a/packages/kilo-vscode/tests/unit/custom-provider.test.ts +++ b/packages/kilo-vscode/tests/unit/custom-provider.test.ts @@ -162,6 +162,35 @@ describe("sanitizeCustomProviderConfig", () => { }) }) + it("preserves opaque options on existing variants", () => { + const variant = { + thinking: { type: "adaptive", display: "summarized" }, + reasoningEffort: "max", + reasoningSummary: "auto", + include: ["reasoning.encrypted_content"], + customOption: { enabled: true }, + } + const result = sanitizeCustomProviderConfig({ + name: "Thinking Provider", + options: { baseURL: "https://example.com/v1" }, + models: { + "model-1": { + name: "Model One", + variants: { high: variant }, + }, + }, + }) + + expect(result).toEqual({ + value: { + npm: "@ai-sdk/openai-compatible", + name: "Thinking Provider", + options: { baseURL: "https://example.com/v1" }, + models: { "model-1": { name: "Model One", variants: { high: variant } } }, + }, + }) + }) + it("preserves core custom model modalities", () => { const result = sanitizeCustomProviderConfig({ name: "Media Provider", diff --git a/packages/kilo-vscode/tests/unit/provider-actions-save.test.ts b/packages/kilo-vscode/tests/unit/provider-actions-save.test.ts index a6499bab1c6..8f0e10b60a8 100644 --- a/packages/kilo-vscode/tests/unit/provider-actions-save.test.ts +++ b/packages/kilo-vscode/tests/unit/provider-actions-save.test.ts @@ -206,6 +206,27 @@ describe("saveCustomProvider", () => { expect(calls.set).toEqual([{ providerID: "myprovider", auth: { type: "api", key: "sk-test" } }]) }) + it("preserves opaque existing variant options through the save boundary", async () => { + const variant = { + thinking: { type: "adaptive", display: "summarized" }, + reasoningEffort: "custom", + reasoningSummary: "auto", + include: ["reasoning.encrypted_content"], + customOption: { enabled: true }, + } + const saved = { + ...createSavedProvider(), + models: { "model-1": { name: "Model One", reasoning: true, variants: { high: variant } } }, + } + const existing = { disabled_providers: [], provider: { myprovider: saved } } + const { ctx, calls, setCachedConfig } = createCtx(existing) + + await saveCustomProvider(ctx, "req", "myprovider", saved, undefined, false, null, setCachedConfig) + + const provider = (calls.config[0]?.config.provider as Record).myprovider + expect(provider.models["model-1"].variants.high).toEqual(variant) + }) + // Regression tests for https://github.com/Kilo-Org/kilocode/issues/9186 // // The CLI's config.update endpoint deep-merges its payload with the existing diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderDialog.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderDialog.tsx index 1041664e41c..e81910eadf7 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderDialog.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderDialog.tsx @@ -89,6 +89,7 @@ function modes(raw: unknown): Modalities { function parseVariant([name, cfg]: [string, Record]): VariantEntry { return { name, + raw: cfg, enableThinking: typeof cfg.enable_thinking === "boolean" ? cfg.enable_thinking : undefined, thinking: typeof cfg.thinking === "object" && cfg.thinking !== null @@ -460,25 +461,6 @@ const CustomProviderDialog = (props: CustomProviderDialogProps) => { setErrors("headers", (v) => v.filter((_, i) => i !== index)) } - function addVariant(mi: number) { - const blank: VariantEntry = { - name: "", - enableThinking: undefined, - thinking: undefined, - splitReasoning: undefined, - reasoningEffort: undefined, - outputEffort: undefined, - chatTemplateArgs: undefined, - } - setForm("models", mi, "variants", (v) => [...v, blank]) - setErrors("models", mi, "variants", (v) => [...(v ?? []), {}]) - } - - function removeVariant(mi: number, vi: number) { - setForm("models", mi, "variants", (v) => v.filter((_, i) => i !== vi)) - setErrors("models", mi, "variants", (v) => (v ?? []).filter((_, i) => i !== vi)) - } - function validate() { const output = validateCustomProvider({ form, @@ -673,7 +655,6 @@ const CustomProviderDialog = (props: CustomProviderDialogProps) => { {(m, i) => ( 1} @@ -682,23 +663,6 @@ const CustomProviderDialog = (props: CustomProviderDialogProps) => { onChangeReasoning={(v) => setForm("models", i(), "reasoning", v)} onChangeSupportsImages={(v) => setForm("models", i(), "supportsImages", v)} onRemove={() => removeModel(i())} - onAddVariant={() => addVariant(i())} - onRemoveVariant={(vi) => removeVariant(i(), vi)} - onChangeVariantName={(vi, val) => setForm("models", i(), "variants", vi, "name", val)} - onChangeVariantEnableThinking={(vi, val) => - setForm("models", i(), "variants", vi, "enableThinking", val) - } - onChangeVariantThinking={(vi, val) => setForm("models", i(), "variants", vi, "thinking", val)} - onChangeVariantSplitReasoning={(vi, val) => - setForm("models", i(), "variants", vi, "splitReasoning", val) - } - onChangeVariantReasoningEffort={(vi, val) => - setForm("models", i(), "variants", vi, "reasoningEffort", val) - } - onChangeVariantOutputEffort={(vi, val) => setForm("models", i(), "variants", vi, "outputEffort", val)} - onChangeVariantChatTemplateArgs={(vi, val) => - setForm("models", i(), "variants", vi, "chatTemplateArgs", val) - } /> )} diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderModelCard.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderModelCard.tsx index d1b4ea13f3d..fa440816f31 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderModelCard.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderModelCard.tsx @@ -1,8 +1,5 @@ -import { Button } from "@kilocode/kilo-ui/button" import { IconButton } from "@kilocode/kilo-ui/icon-button" -import { Select } from "@kilocode/kilo-ui/select" import { TextField } from "@kilocode/kilo-ui/text-field" -import { For, Show } from "solid-js" import { useLanguage } from "../../context/language" export type Translator = ReturnType["t"] @@ -23,6 +20,7 @@ export type Modalities = { export type VariantEntry = { name: string + raw?: Record enableThinking: EnableThinkingValue thinking: ThinkingTypeValue splitReasoning: SplitReasoningValue @@ -40,265 +38,9 @@ export type ModelEntry = { variants: VariantEntry[] } -type SelectOption = { value: T; labelKey: string } - -const ENABLE_THINKING_OPTIONS: SelectOption[] = [ - { value: undefined, labelKey: "provider.custom.models.variants.option.unset" }, - { value: true, labelKey: "provider.custom.models.variants.enableThinking.true" }, - { value: false, labelKey: "provider.custom.models.variants.enableThinking.false" }, -] - -const THINKING_OPTIONS: SelectOption[] = [ - { value: undefined, labelKey: "provider.custom.models.variants.option.unset" }, - { value: "enabled", labelKey: "provider.custom.models.variants.thinking.enabled" }, - { value: "disabled", labelKey: "provider.custom.models.variants.thinking.disabled" }, - { value: "adaptive", labelKey: "provider.custom.models.variants.thinking.adaptive" }, -] - -const SPLIT_REASONING_OPTIONS: SelectOption[] = [ - { value: undefined, labelKey: "provider.custom.models.variants.option.unset" }, - { value: true, labelKey: "provider.custom.models.variants.splitReasoning.true" }, - { value: false, labelKey: "provider.custom.models.variants.splitReasoning.false" }, -] - -const CHAT_TEMPLATE_ARGS_OPTIONS: SelectOption[] = [ - { value: undefined, labelKey: "provider.custom.models.variants.option.unset" }, - { value: true, labelKey: "provider.custom.models.variants.chatTemplateArgs.true" }, - { value: false, labelKey: "provider.custom.models.variants.chatTemplateArgs.false" }, -] - -const REASONING_EFFORT_OPTIONS: SelectOption[] = [ - { value: undefined, labelKey: "provider.custom.models.variants.option.unset" }, - { value: "none", labelKey: "provider.custom.models.variants.reasoningEffort.none" }, - { value: "minimal", labelKey: "provider.custom.models.variants.reasoningEffort.minimal" }, - { value: "low", labelKey: "provider.custom.models.variants.reasoningEffort.low" }, - { value: "medium", labelKey: "provider.custom.models.variants.reasoningEffort.medium" }, - { value: "high", labelKey: "provider.custom.models.variants.reasoningEffort.high" }, - { value: "xhigh", labelKey: "provider.custom.models.variants.reasoningEffort.xhigh" }, -] - -const OUTPUT_EFFORT_OPTIONS: SelectOption[] = [ - { value: undefined, labelKey: "provider.custom.models.variants.option.unset" }, - { value: "low", labelKey: "provider.custom.models.variants.outputEffort.low" }, - { value: "medium", labelKey: "provider.custom.models.variants.outputEffort.medium" }, - { value: "high", labelKey: "provider.custom.models.variants.outputEffort.high" }, - { value: "xhigh", labelKey: "provider.custom.models.variants.outputEffort.xhigh" }, - { value: "max", labelKey: "provider.custom.models.variants.outputEffort.max" }, -] - -type VariantRowProps = { - v: VariantEntry - vi: () => number - isFirst: () => boolean - error: { name?: string } | undefined - t: Translator - onChangeName: (val: string) => void - onChangeEnableThinking: (val: EnableThinkingValue) => void - onChangeThinking: (val: ThinkingTypeValue) => void - onChangeSplitReasoning: (val: SplitReasoningValue) => void - onChangeReasoningEffort: (val: ReasoningEffortValue) => void - onChangeOutputEffort: (val: OutputEffortValue) => void - onChangeChatTemplateArgs: (val: ChatTemplateArgsValue) => void - onRemove: () => void -} - -function VariantRow(props: VariantRowProps) { - return ( -
- -
- -
-
- -
-
- - o.value === props.v.thinking)} - value={(o) => String(o.value)} - label={(o) => props.t(o.labelKey)} - onSelect={(o) => props.onChangeThinking(o?.value)} - placeholder={props.t("provider.custom.models.variants.thinking.placeholder")} - variant="secondary" - size="small" - triggerVariant="settings" - /> -
-
- - o.value === props.v.reasoningEffort)} - value={(o) => String(o.value)} - label={(o) => props.t(o.labelKey)} - onSelect={(o) => props.onChangeReasoningEffort(o?.value)} - placeholder={props.t("provider.custom.models.variants.reasoningEffort.placeholder")} - variant="secondary" - size="small" - triggerVariant="settings" - /> -
-
- - o.value === props.v.chatTemplateArgs)} - value={(o) => String(o.value)} - label={(o) => props.t(o.labelKey)} - onSelect={(o) => props.onChangeChatTemplateArgs(o?.value)} - placeholder={props.t("provider.custom.models.variants.chatTemplateArgs.placeholder")} - variant="secondary" - size="small" - triggerVariant="settings" - /> -
- -
-
- ) -} - type ModelCardProps = { m: ModelEntry - i: () => number - errors: { id?: string; name?: string; variants?: Array<{ name?: string }> } + errors: { id?: string; name?: string } t: Translator canRemove: boolean onChangeId: (val: string) => void @@ -306,15 +48,6 @@ type ModelCardProps = { onChangeReasoning: (val: boolean) => void onChangeSupportsImages: (val: boolean) => void onRemove: () => void - onAddVariant: () => void - onRemoveVariant: (vi: number) => void - onChangeVariantName: (vi: number, val: string) => void - onChangeVariantEnableThinking: (vi: number, val: EnableThinkingValue) => void - onChangeVariantThinking: (vi: number, val: ThinkingTypeValue) => void - onChangeVariantSplitReasoning: (vi: number, val: SplitReasoningValue) => void - onChangeVariantReasoningEffort: (vi: number, val: ReasoningEffortValue) => void - onChangeVariantOutputEffort: (vi: number, val: OutputEffortValue) => void - onChangeVariantChatTemplateArgs: (vi: number, val: ChatTemplateArgsValue) => void } export function ModelCard(props: ModelCardProps) { @@ -398,41 +131,6 @@ export function ModelCard(props: ModelCardProps) { /> {props.t("provider.custom.models.modalities.image")} - - {/* Variants — only available when reasoning is enabled */} - - 0}> -
- - - {(v, vi) => ( - vi() === 0} - error={props.errors.variants?.[vi()]} - t={props.t} - onChangeName={(val) => props.onChangeVariantName(vi(), val)} - onChangeEnableThinking={(val) => props.onChangeVariantEnableThinking(vi(), val)} - onChangeThinking={(val) => props.onChangeVariantThinking(vi(), val)} - onChangeSplitReasoning={(val) => props.onChangeVariantSplitReasoning(vi(), val)} - onChangeReasoningEffort={(val) => props.onChangeVariantReasoningEffort(vi(), val)} - onChangeOutputEffort={(val) => props.onChangeVariantOutputEffort(vi(), val)} - onChangeChatTemplateArgs={(val) => props.onChangeVariantChatTemplateArgs(vi(), val)} - onRemove={() => props.onRemoveVariant(vi())} - /> - )} - -
-
- -
) } diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderValidation.ts b/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderValidation.ts index 2331d295435..1032e6cf91d 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderValidation.ts +++ b/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderValidation.ts @@ -105,6 +105,7 @@ function checkProviderID(id: string, editing: boolean, disabled: string[], exist } function serializeVariant(v: VariantEntry): [string, Record] { + if (v.raw) return [v.name.trim(), v.raw] const cfg: Record = {} if (v.enableThinking !== undefined) cfg.enable_thinking = v.enableThinking if (v.thinking !== undefined) cfg.thinking = { type: v.thinking } diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index 6c425940a8f..be67beb1093 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -433,44 +433,6 @@ export const dict = { "provider.custom.models.name.placeholder": "الاسم المعروض", "provider.custom.models.reasoning.label": "الاستدلال", "provider.custom.models.modalities.image": "صورة", - "provider.custom.models.variants.label": "المتغيرات", - "provider.custom.models.variants.add": "إضافة متغير", - "provider.custom.models.variants.remove": "إزالة المتغير", - "provider.custom.models.variants.name.label": "الاسم", - "provider.custom.models.variants.name.placeholder": "على سبيل المثال: thinking", - "provider.custom.models.variants.option.unset": "(غير محدد)", - "provider.custom.models.variants.enableThinking.label": "تمكين التفكير (مثل Alibaba)", - "provider.custom.models.variants.enableThinking.placeholder": "enable_thinking", - "provider.custom.models.variants.enableThinking.true": "true", - "provider.custom.models.variants.enableThinking.false": "false", - "provider.custom.models.variants.thinking.label": "نوع التفكير (مثل Z.ai)", - "provider.custom.models.variants.thinking.placeholder": "thinking", - "provider.custom.models.variants.thinking.enabled": "enabled", - "provider.custom.models.variants.thinking.disabled": "disabled", - "provider.custom.models.variants.thinking.adaptive": "adaptive", - "provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)", - "provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split", - "provider.custom.models.variants.splitReasoning.true": "true", - "provider.custom.models.variants.splitReasoning.false": "false", - "provider.custom.models.variants.chatTemplateArgs.label": "تفعيل التفكير عبر وسائط قالب الدردشة (مثل Hugging Face)", - "provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args", - "provider.custom.models.variants.chatTemplateArgs.true": "true", - "provider.custom.models.variants.chatTemplateArgs.false": "false", - "provider.custom.models.variants.reasoningEffort.label": "جهد الاستدلال", - "provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort", - "provider.custom.models.variants.reasoningEffort.none": "none", - "provider.custom.models.variants.reasoningEffort.minimal": "minimal", - "provider.custom.models.variants.reasoningEffort.low": "low", - "provider.custom.models.variants.reasoningEffort.medium": "medium", - "provider.custom.models.variants.reasoningEffort.high": "high", - "provider.custom.models.variants.reasoningEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)", - "provider.custom.models.variants.outputEffort.placeholder": "effort", - "provider.custom.models.variants.outputEffort.low": "low", - "provider.custom.models.variants.outputEffort.medium": "medium", - "provider.custom.models.variants.outputEffort.high": "high", - "provider.custom.models.variants.outputEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.max": "max", "provider.custom.models.remove": "إزالة النموذج", "provider.custom.models.add": "إضافة نموذج", "provider.custom.models.fetch.authError": "فشلت المصادقة. تحقق من مفتاح API أعلاه وحاول مرة أخرى.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index 1a7d9ae17ba..3a066b96b81 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -444,45 +444,6 @@ export const dict = { "provider.custom.models.name.placeholder": "Nome de Exibição", "provider.custom.models.reasoning.label": "Raciocínio", "provider.custom.models.modalities.image": "Imagem", - "provider.custom.models.variants.label": "Variantes", - "provider.custom.models.variants.add": "Adicionar variante", - "provider.custom.models.variants.remove": "Remover variante", - "provider.custom.models.variants.name.label": "Nome", - "provider.custom.models.variants.name.placeholder": "ex. thinking", - "provider.custom.models.variants.option.unset": "(não definido)", - "provider.custom.models.variants.enableThinking.label": "Ativar pensamento (ex. Alibaba)", - "provider.custom.models.variants.enableThinking.placeholder": "enable_thinking", - "provider.custom.models.variants.enableThinking.true": "true", - "provider.custom.models.variants.enableThinking.false": "false", - "provider.custom.models.variants.thinking.label": "Tipo de pensamento (ex. Z.ai)", - "provider.custom.models.variants.thinking.placeholder": "thinking", - "provider.custom.models.variants.thinking.enabled": "enabled", - "provider.custom.models.variants.thinking.disabled": "disabled", - "provider.custom.models.variants.thinking.adaptive": "adaptive", - "provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)", - "provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split", - "provider.custom.models.variants.splitReasoning.true": "true", - "provider.custom.models.variants.splitReasoning.false": "false", - "provider.custom.models.variants.chatTemplateArgs.label": - "Ativar pensamento via args do template de chat (ex. Hugging Face)", - "provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args", - "provider.custom.models.variants.chatTemplateArgs.true": "true", - "provider.custom.models.variants.chatTemplateArgs.false": "false", - "provider.custom.models.variants.reasoningEffort.label": "Esforço de raciocínio", - "provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort", - "provider.custom.models.variants.reasoningEffort.none": "none", - "provider.custom.models.variants.reasoningEffort.minimal": "minimal", - "provider.custom.models.variants.reasoningEffort.low": "low", - "provider.custom.models.variants.reasoningEffort.medium": "medium", - "provider.custom.models.variants.reasoningEffort.high": "high", - "provider.custom.models.variants.reasoningEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)", - "provider.custom.models.variants.outputEffort.placeholder": "effort", - "provider.custom.models.variants.outputEffort.low": "low", - "provider.custom.models.variants.outputEffort.medium": "medium", - "provider.custom.models.variants.outputEffort.high": "high", - "provider.custom.models.variants.outputEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.max": "max", "provider.custom.models.remove": "Remover modelo", "provider.custom.models.add": "Adicionar modelo", "provider.custom.models.fetch.authError": "Falha na autenticação. Verifique a chave de API acima e tente novamente.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index 509f0265201..e478af80f20 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -486,45 +486,6 @@ export const dict = { "provider.custom.models.name.placeholder": "Naziv za prikaz", "provider.custom.models.reasoning.label": "Zaključivanje", "provider.custom.models.modalities.image": "Slika", - "provider.custom.models.variants.label": "Varijante", - "provider.custom.models.variants.add": "Dodaj varijantu", - "provider.custom.models.variants.remove": "Ukloni varijantu", - "provider.custom.models.variants.name.label": "Ime", - "provider.custom.models.variants.name.placeholder": "npr. thinking", - "provider.custom.models.variants.option.unset": "(nije postavljeno)", - "provider.custom.models.variants.enableThinking.label": "Omogući razmišljanje (npr. Alibaba)", - "provider.custom.models.variants.enableThinking.placeholder": "enable_thinking", - "provider.custom.models.variants.enableThinking.true": "true", - "provider.custom.models.variants.enableThinking.false": "false", - "provider.custom.models.variants.thinking.label": "Vrsta razmišljanja (npr. Z.ai)", - "provider.custom.models.variants.thinking.placeholder": "thinking", - "provider.custom.models.variants.thinking.enabled": "enabled", - "provider.custom.models.variants.thinking.disabled": "disabled", - "provider.custom.models.variants.thinking.adaptive": "adaptive", - "provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)", - "provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split", - "provider.custom.models.variants.splitReasoning.true": "true", - "provider.custom.models.variants.splitReasoning.false": "false", - "provider.custom.models.variants.chatTemplateArgs.label": - "Omogući razmišljanje preko argumenata chat predloška (npr. Hugging Face)", - "provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args", - "provider.custom.models.variants.chatTemplateArgs.true": "true", - "provider.custom.models.variants.chatTemplateArgs.false": "false", - "provider.custom.models.variants.reasoningEffort.label": "Napor zaključivanja", - "provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort", - "provider.custom.models.variants.reasoningEffort.none": "none", - "provider.custom.models.variants.reasoningEffort.minimal": "minimal", - "provider.custom.models.variants.reasoningEffort.low": "low", - "provider.custom.models.variants.reasoningEffort.medium": "medium", - "provider.custom.models.variants.reasoningEffort.high": "high", - "provider.custom.models.variants.reasoningEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)", - "provider.custom.models.variants.outputEffort.placeholder": "effort", - "provider.custom.models.variants.outputEffort.low": "low", - "provider.custom.models.variants.outputEffort.medium": "medium", - "provider.custom.models.variants.outputEffort.high": "high", - "provider.custom.models.variants.outputEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.max": "max", "provider.custom.models.remove": "Ukloni model", "provider.custom.models.add": "Dodaj model", "provider.custom.models.fetch.authError": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index 4f1d6c9034c..7ef37266d21 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -484,45 +484,6 @@ export const dict = { "provider.custom.models.name.placeholder": "Visningsnavn", "provider.custom.models.reasoning.label": "Ræsonnement", "provider.custom.models.modalities.image": "Billede", - "provider.custom.models.variants.label": "Varianter", - "provider.custom.models.variants.add": "Tilføj variant", - "provider.custom.models.variants.remove": "Fjern variant", - "provider.custom.models.variants.name.label": "Navn", - "provider.custom.models.variants.name.placeholder": "f.eks. thinking", - "provider.custom.models.variants.option.unset": "(ikke angivet)", - "provider.custom.models.variants.enableThinking.label": "Aktivér tænkning (f.eks. Alibaba)", - "provider.custom.models.variants.enableThinking.placeholder": "enable_thinking", - "provider.custom.models.variants.enableThinking.true": "true", - "provider.custom.models.variants.enableThinking.false": "false", - "provider.custom.models.variants.thinking.label": "Tænkningstype (f.eks. Z.ai)", - "provider.custom.models.variants.thinking.placeholder": "thinking", - "provider.custom.models.variants.thinking.enabled": "enabled", - "provider.custom.models.variants.thinking.disabled": "disabled", - "provider.custom.models.variants.thinking.adaptive": "adaptive", - "provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)", - "provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split", - "provider.custom.models.variants.splitReasoning.true": "true", - "provider.custom.models.variants.splitReasoning.false": "false", - "provider.custom.models.variants.chatTemplateArgs.label": - "Aktivér tænkning via chat-skabelonargs (f.eks. Hugging Face)", - "provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args", - "provider.custom.models.variants.chatTemplateArgs.true": "true", - "provider.custom.models.variants.chatTemplateArgs.false": "false", - "provider.custom.models.variants.reasoningEffort.label": "Ræsonnementsindsats", - "provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort", - "provider.custom.models.variants.reasoningEffort.none": "none", - "provider.custom.models.variants.reasoningEffort.minimal": "minimal", - "provider.custom.models.variants.reasoningEffort.low": "low", - "provider.custom.models.variants.reasoningEffort.medium": "medium", - "provider.custom.models.variants.reasoningEffort.high": "high", - "provider.custom.models.variants.reasoningEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)", - "provider.custom.models.variants.outputEffort.placeholder": "effort", - "provider.custom.models.variants.outputEffort.low": "low", - "provider.custom.models.variants.outputEffort.medium": "medium", - "provider.custom.models.variants.outputEffort.high": "high", - "provider.custom.models.variants.outputEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.max": "max", "provider.custom.models.remove": "Fjern model", "provider.custom.models.add": "Tilføj model", "provider.custom.models.fetch.authError": "Godkendelse mislykkedes. Kontrollér API-nøglen ovenfor, og prøv igen.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index 7c1366c7261..8eaedf2be46 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -494,45 +494,6 @@ export const dict = { "provider.custom.models.name.placeholder": "Anzeigename", "provider.custom.models.reasoning.label": "Schlussfolgerung", "provider.custom.models.modalities.image": "Bild", - "provider.custom.models.variants.label": "Varianten", - "provider.custom.models.variants.add": "Variante hinzufügen", - "provider.custom.models.variants.remove": "Variante entfernen", - "provider.custom.models.variants.name.label": "Name", - "provider.custom.models.variants.name.placeholder": "z.B. thinking", - "provider.custom.models.variants.option.unset": "(nicht festgelegt)", - "provider.custom.models.variants.enableThinking.label": "Nachdenken aktivieren (z.B. Alibaba)", - "provider.custom.models.variants.enableThinking.placeholder": "enable_thinking", - "provider.custom.models.variants.enableThinking.true": "true", - "provider.custom.models.variants.enableThinking.false": "false", - "provider.custom.models.variants.thinking.label": "Art des Nachdenkens (z.B. Z.ai)", - "provider.custom.models.variants.thinking.placeholder": "thinking", - "provider.custom.models.variants.thinking.enabled": "enabled", - "provider.custom.models.variants.thinking.disabled": "disabled", - "provider.custom.models.variants.thinking.adaptive": "adaptive", - "provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)", - "provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split", - "provider.custom.models.variants.splitReasoning.true": "true", - "provider.custom.models.variants.splitReasoning.false": "false", - "provider.custom.models.variants.chatTemplateArgs.label": - "Nachdenken über Chat-Vorlagenargumente aktivieren (z.B. Hugging Face)", - "provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args", - "provider.custom.models.variants.chatTemplateArgs.true": "true", - "provider.custom.models.variants.chatTemplateArgs.false": "false", - "provider.custom.models.variants.reasoningEffort.label": "Reasoning-Aufwand", - "provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort", - "provider.custom.models.variants.reasoningEffort.none": "none", - "provider.custom.models.variants.reasoningEffort.minimal": "minimal", - "provider.custom.models.variants.reasoningEffort.low": "low", - "provider.custom.models.variants.reasoningEffort.medium": "medium", - "provider.custom.models.variants.reasoningEffort.high": "high", - "provider.custom.models.variants.reasoningEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)", - "provider.custom.models.variants.outputEffort.placeholder": "effort", - "provider.custom.models.variants.outputEffort.low": "low", - "provider.custom.models.variants.outputEffort.medium": "medium", - "provider.custom.models.variants.outputEffort.high": "high", - "provider.custom.models.variants.outputEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.max": "max", "provider.custom.models.remove": "Modell entfernen", "provider.custom.models.add": "Modell hinzufügen", "provider.custom.models.fetch.authError": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index 5752f6b2a8c..35ab3e7e9fb 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -406,45 +406,6 @@ export const dict = { "provider.custom.models.name.placeholder": "Display Name", "provider.custom.models.reasoning.label": "Reasoning", "provider.custom.models.modalities.image": "Image", - "provider.custom.models.variants.label": "Variants", - "provider.custom.models.variants.add": "Add variant", - "provider.custom.models.variants.remove": "Remove variant", - "provider.custom.models.variants.name.label": "Name", - "provider.custom.models.variants.name.placeholder": "e.g. thinking", - "provider.custom.models.variants.option.unset": "(not set)", - "provider.custom.models.variants.enableThinking.label": "Enable thinking (e.g. Alibaba)", - "provider.custom.models.variants.enableThinking.placeholder": "enable_thinking", - "provider.custom.models.variants.enableThinking.true": "true", - "provider.custom.models.variants.enableThinking.false": "false", - "provider.custom.models.variants.thinking.label": "Thinking type (e.g. Z.ai)", - "provider.custom.models.variants.thinking.placeholder": "thinking", - "provider.custom.models.variants.thinking.enabled": "enabled", - "provider.custom.models.variants.thinking.disabled": "disabled", - "provider.custom.models.variants.thinking.adaptive": "adaptive", - "provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)", - "provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split", - "provider.custom.models.variants.splitReasoning.true": "true", - "provider.custom.models.variants.splitReasoning.false": "false", - "provider.custom.models.variants.chatTemplateArgs.label": - "Enable thinking via chat template args (e.g. Hugging Face)", - "provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args", - "provider.custom.models.variants.chatTemplateArgs.true": "true", - "provider.custom.models.variants.chatTemplateArgs.false": "false", - "provider.custom.models.variants.reasoningEffort.label": "Reasoning effort", - "provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort", - "provider.custom.models.variants.reasoningEffort.none": "none", - "provider.custom.models.variants.reasoningEffort.minimal": "minimal", - "provider.custom.models.variants.reasoningEffort.low": "low", - "provider.custom.models.variants.reasoningEffort.medium": "medium", - "provider.custom.models.variants.reasoningEffort.high": "high", - "provider.custom.models.variants.reasoningEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)", - "provider.custom.models.variants.outputEffort.placeholder": "effort", - "provider.custom.models.variants.outputEffort.low": "low", - "provider.custom.models.variants.outputEffort.medium": "medium", - "provider.custom.models.variants.outputEffort.high": "high", - "provider.custom.models.variants.outputEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.max": "max", "provider.custom.models.remove": "Remove model", "provider.custom.models.add": "Add model", "provider.custom.models.fetch.authError": "Authentication failed. Check the API key above and try again.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index 2b7ed694b0b..db2015b6543 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -487,45 +487,6 @@ export const dict = { "provider.custom.models.name.placeholder": "Nombre para mostrar", "provider.custom.models.reasoning.label": "Razonamiento", "provider.custom.models.modalities.image": "Imagen", - "provider.custom.models.variants.label": "Variantes", - "provider.custom.models.variants.add": "Añadir variante", - "provider.custom.models.variants.remove": "Eliminar variante", - "provider.custom.models.variants.name.label": "Nombre", - "provider.custom.models.variants.name.placeholder": "p. ej. thinking", - "provider.custom.models.variants.option.unset": "(no establecido)", - "provider.custom.models.variants.enableThinking.label": "Habilitar pensamiento (p. ej. Alibaba)", - "provider.custom.models.variants.enableThinking.placeholder": "enable_thinking", - "provider.custom.models.variants.enableThinking.true": "true", - "provider.custom.models.variants.enableThinking.false": "false", - "provider.custom.models.variants.thinking.label": "Tipo de pensamiento (p. ej. Z.ai)", - "provider.custom.models.variants.thinking.placeholder": "thinking", - "provider.custom.models.variants.thinking.enabled": "enabled", - "provider.custom.models.variants.thinking.disabled": "disabled", - "provider.custom.models.variants.thinking.adaptive": "adaptive", - "provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)", - "provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split", - "provider.custom.models.variants.splitReasoning.true": "true", - "provider.custom.models.variants.splitReasoning.false": "false", - "provider.custom.models.variants.chatTemplateArgs.label": - "Activar pensamiento mediante args de plantilla de chat (ej. Hugging Face)", - "provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args", - "provider.custom.models.variants.chatTemplateArgs.true": "true", - "provider.custom.models.variants.chatTemplateArgs.false": "false", - "provider.custom.models.variants.reasoningEffort.label": "Esfuerzo de razonamiento", - "provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort", - "provider.custom.models.variants.reasoningEffort.none": "none", - "provider.custom.models.variants.reasoningEffort.minimal": "minimal", - "provider.custom.models.variants.reasoningEffort.low": "low", - "provider.custom.models.variants.reasoningEffort.medium": "medium", - "provider.custom.models.variants.reasoningEffort.high": "high", - "provider.custom.models.variants.reasoningEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)", - "provider.custom.models.variants.outputEffort.placeholder": "effort", - "provider.custom.models.variants.outputEffort.low": "low", - "provider.custom.models.variants.outputEffort.medium": "medium", - "provider.custom.models.variants.outputEffort.high": "high", - "provider.custom.models.variants.outputEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.max": "max", "provider.custom.models.remove": "Eliminar modelo", "provider.custom.models.add": "Añadir modelo", "provider.custom.models.fetch.authError": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fa.ts b/packages/kilo-vscode/webview-ui/src/i18n/fa.ts index 71b97a2d3ff..85eb22ea3ba 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fa.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fa.ts @@ -407,45 +407,6 @@ export const dict = { "provider.custom.models.name.placeholder": "نام نمایشی", "provider.custom.models.reasoning.label": "استدلال", "provider.custom.models.modalities.image": "تصویر", - "provider.custom.models.variants.label": "نسخه‌های متغیر", - "provider.custom.models.variants.add": "افزودن نسخه متغیر", - "provider.custom.models.variants.remove": "حذف نسخه متغیر", - "provider.custom.models.variants.name.label": "نام", - "provider.custom.models.variants.name.placeholder": "مثلاً thinking", - "provider.custom.models.variants.option.unset": "(تنظیم نشده)", - "provider.custom.models.variants.enableThinking.label": "فعال‌سازی تفکر (مثلاً Alibaba)", - "provider.custom.models.variants.enableThinking.placeholder": "enable_thinking", - "provider.custom.models.variants.enableThinking.true": "true", - "provider.custom.models.variants.enableThinking.false": "false", - "provider.custom.models.variants.thinking.label": "نوع تفکر (مثلاً Z.ai)", - "provider.custom.models.variants.thinking.placeholder": "thinking", - "provider.custom.models.variants.thinking.enabled": "فعال", - "provider.custom.models.variants.thinking.disabled": "غیرفعال", - "provider.custom.models.variants.thinking.adaptive": "تطبیقی", - "provider.custom.models.variants.splitReasoning.label": "تقسیم استدلال (لازم برای مثلاً MiniMax)", - "provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split", - "provider.custom.models.variants.splitReasoning.true": "true", - "provider.custom.models.variants.splitReasoning.false": "false", - "provider.custom.models.variants.chatTemplateArgs.label": - "فعال‌سازی تفکر از طریق آرگومان‌های قالب چت (مثلاً Hugging Face)", - "provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args", - "provider.custom.models.variants.chatTemplateArgs.true": "true", - "provider.custom.models.variants.chatTemplateArgs.false": "false", - "provider.custom.models.variants.reasoningEffort.label": "سطح استدلال", - "provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort", - "provider.custom.models.variants.reasoningEffort.none": "هیچ", - "provider.custom.models.variants.reasoningEffort.minimal": "حداقل", - "provider.custom.models.variants.reasoningEffort.low": "کم", - "provider.custom.models.variants.reasoningEffort.medium": "متوسط", - "provider.custom.models.variants.reasoningEffort.high": "زیاد", - "provider.custom.models.variants.reasoningEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.label": "تلاش خروجی (مثلاً Anthropic)", - "provider.custom.models.variants.outputEffort.placeholder": "تلاش", - "provider.custom.models.variants.outputEffort.low": "کم", - "provider.custom.models.variants.outputEffort.medium": "متوسط", - "provider.custom.models.variants.outputEffort.high": "زیاد", - "provider.custom.models.variants.outputEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.max": "حداکثر", "provider.custom.models.remove": "حذف مدل", "provider.custom.models.add": "افزودن مدل", "provider.custom.models.fetch.authError": "احراز هویت ناموفق بود. کلید API بالا را بررسی کرده و دوباره امتحان کنید.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index 3fd9956d9de..cec3e454773 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -488,45 +488,6 @@ export const dict = { "provider.custom.models.name.placeholder": "Nom d'affichage", "provider.custom.models.reasoning.label": "Raisonnement", "provider.custom.models.modalities.image": "Image", - "provider.custom.models.variants.label": "Variantes", - "provider.custom.models.variants.add": "Ajouter une variante", - "provider.custom.models.variants.remove": "Supprimer la variante", - "provider.custom.models.variants.name.label": "Nom", - "provider.custom.models.variants.name.placeholder": "ex. thinking", - "provider.custom.models.variants.option.unset": "(non défini)", - "provider.custom.models.variants.enableThinking.label": "Activer la réflexion (ex. Alibaba)", - "provider.custom.models.variants.enableThinking.placeholder": "enable_thinking", - "provider.custom.models.variants.enableThinking.true": "true", - "provider.custom.models.variants.enableThinking.false": "false", - "provider.custom.models.variants.thinking.label": "Type de réflexion (ex. Z.ai)", - "provider.custom.models.variants.thinking.placeholder": "thinking", - "provider.custom.models.variants.thinking.enabled": "enabled", - "provider.custom.models.variants.thinking.disabled": "disabled", - "provider.custom.models.variants.thinking.adaptive": "adaptive", - "provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)", - "provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split", - "provider.custom.models.variants.splitReasoning.true": "true", - "provider.custom.models.variants.splitReasoning.false": "false", - "provider.custom.models.variants.chatTemplateArgs.label": - "Activer la réflexion via les args du modèle de chat (ex. Hugging Face)", - "provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args", - "provider.custom.models.variants.chatTemplateArgs.true": "true", - "provider.custom.models.variants.chatTemplateArgs.false": "false", - "provider.custom.models.variants.reasoningEffort.label": "Effort de raisonnement", - "provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort", - "provider.custom.models.variants.reasoningEffort.none": "none", - "provider.custom.models.variants.reasoningEffort.minimal": "minimal", - "provider.custom.models.variants.reasoningEffort.low": "low", - "provider.custom.models.variants.reasoningEffort.medium": "medium", - "provider.custom.models.variants.reasoningEffort.high": "high", - "provider.custom.models.variants.reasoningEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)", - "provider.custom.models.variants.outputEffort.placeholder": "effort", - "provider.custom.models.variants.outputEffort.low": "low", - "provider.custom.models.variants.outputEffort.medium": "medium", - "provider.custom.models.variants.outputEffort.high": "high", - "provider.custom.models.variants.outputEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.max": "max", "provider.custom.models.remove": "Supprimer le modèle", "provider.custom.models.add": "Ajouter un modèle", "provider.custom.models.fetch.authError": "Échec de l'authentification. Vérifiez la clé API ci-dessus et réessayez.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/it.ts b/packages/kilo-vscode/webview-ui/src/i18n/it.ts index b8ec52156f4..4270684cd22 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/it.ts @@ -316,45 +316,6 @@ export const dict = { "provider.custom.models.name.placeholder": "Nome visualizzato", "provider.custom.models.reasoning.label": "Reasoning", "provider.custom.models.modalities.image": "Immagine", - "provider.custom.models.variants.label": "Variants", - "provider.custom.models.variants.add": "Aggiungi variante", - "provider.custom.models.variants.remove": "Rimuovi variante", - "provider.custom.models.variants.name.label": "Nome", - "provider.custom.models.variants.name.placeholder": "es. thinking", - "provider.custom.models.variants.option.unset": "(non impostato)", - "provider.custom.models.variants.enableThinking.label": "Abilita thinking (es. Alibaba)", - "provider.custom.models.variants.enableThinking.placeholder": "enable_thinking", - "provider.custom.models.variants.enableThinking.true": "true", - "provider.custom.models.variants.enableThinking.false": "false", - "provider.custom.models.variants.thinking.label": "Tipo thinking (es. Z.ai)", - "provider.custom.models.variants.thinking.placeholder": "thinking", - "provider.custom.models.variants.thinking.enabled": "enabled", - "provider.custom.models.variants.thinking.disabled": "disabled", - "provider.custom.models.variants.thinking.adaptive": "adaptive", - "provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)", - "provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split", - "provider.custom.models.variants.splitReasoning.true": "true", - "provider.custom.models.variants.splitReasoning.false": "false", - "provider.custom.models.variants.chatTemplateArgs.label": - "Enable thinking via chat template args (e.g. Hugging Face)", - "provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args", - "provider.custom.models.variants.chatTemplateArgs.true": "true", - "provider.custom.models.variants.chatTemplateArgs.false": "false", - "provider.custom.models.variants.reasoningEffort.label": "Sforzo di ragionamento", - "provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort", - "provider.custom.models.variants.reasoningEffort.none": "none", - "provider.custom.models.variants.reasoningEffort.minimal": "minimal", - "provider.custom.models.variants.reasoningEffort.low": "low", - "provider.custom.models.variants.reasoningEffort.medium": "medium", - "provider.custom.models.variants.reasoningEffort.high": "high", - "provider.custom.models.variants.reasoningEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)", - "provider.custom.models.variants.outputEffort.placeholder": "effort", - "provider.custom.models.variants.outputEffort.low": "low", - "provider.custom.models.variants.outputEffort.medium": "medium", - "provider.custom.models.variants.outputEffort.high": "high", - "provider.custom.models.variants.outputEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.max": "max", "provider.custom.models.remove": "Rimuovi modello", "provider.custom.models.add": "Aggiungi modello", "provider.custom.models.fetch.authError": "Autenticazione non riuscita. Controlla l'API key sopra e riprova.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index fa1eda6f6b0..fe0e5b26a80 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -480,44 +480,6 @@ export const dict = { "provider.custom.models.name.placeholder": "表示名", "provider.custom.models.reasoning.label": "推論", "provider.custom.models.modalities.image": "画像", - "provider.custom.models.variants.label": "バリアント", - "provider.custom.models.variants.add": "バリアントを追加", - "provider.custom.models.variants.remove": "バリアントを削除", - "provider.custom.models.variants.name.label": "名前", - "provider.custom.models.variants.name.placeholder": "例: thinking", - "provider.custom.models.variants.option.unset": "(未設定)", - "provider.custom.models.variants.enableThinking.label": "思考を有効にする (例: Alibaba)", - "provider.custom.models.variants.enableThinking.placeholder": "enable_thinking", - "provider.custom.models.variants.enableThinking.true": "true", - "provider.custom.models.variants.enableThinking.false": "false", - "provider.custom.models.variants.thinking.label": "思考タイプ (例: Z.ai)", - "provider.custom.models.variants.thinking.placeholder": "thinking", - "provider.custom.models.variants.thinking.enabled": "enabled", - "provider.custom.models.variants.thinking.disabled": "disabled", - "provider.custom.models.variants.thinking.adaptive": "adaptive", - "provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)", - "provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split", - "provider.custom.models.variants.splitReasoning.true": "true", - "provider.custom.models.variants.splitReasoning.false": "false", - "provider.custom.models.variants.chatTemplateArgs.label": "チャットテンプレート引数で思考を有効化 (例: Hugging Face)", - "provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args", - "provider.custom.models.variants.chatTemplateArgs.true": "true", - "provider.custom.models.variants.chatTemplateArgs.false": "false", - "provider.custom.models.variants.reasoningEffort.label": "推論エフォート", - "provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort", - "provider.custom.models.variants.reasoningEffort.none": "none", - "provider.custom.models.variants.reasoningEffort.minimal": "minimal", - "provider.custom.models.variants.reasoningEffort.low": "low", - "provider.custom.models.variants.reasoningEffort.medium": "medium", - "provider.custom.models.variants.reasoningEffort.high": "high", - "provider.custom.models.variants.reasoningEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)", - "provider.custom.models.variants.outputEffort.placeholder": "effort", - "provider.custom.models.variants.outputEffort.low": "low", - "provider.custom.models.variants.outputEffort.medium": "medium", - "provider.custom.models.variants.outputEffort.high": "high", - "provider.custom.models.variants.outputEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.max": "max", "provider.custom.models.remove": "モデルを削除", "provider.custom.models.add": "モデルを追加", "provider.custom.models.fetch.authError": "認証に失敗しました。上記のAPIキーを確認して再試行してください。", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index 360ed69cb00..5d4a35c614e 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -441,44 +441,6 @@ export const dict = { "provider.custom.models.name.placeholder": "표시 이름", "provider.custom.models.reasoning.label": "추론", "provider.custom.models.modalities.image": "이미지", - "provider.custom.models.variants.label": "변형", - "provider.custom.models.variants.add": "변형 추가", - "provider.custom.models.variants.remove": "변형 제거", - "provider.custom.models.variants.name.label": "이름", - "provider.custom.models.variants.name.placeholder": "예: thinking", - "provider.custom.models.variants.option.unset": "(설정되지 않음)", - "provider.custom.models.variants.enableThinking.label": "사고 활성화 (예: Alibaba)", - "provider.custom.models.variants.enableThinking.placeholder": "enable_thinking", - "provider.custom.models.variants.enableThinking.true": "true", - "provider.custom.models.variants.enableThinking.false": "false", - "provider.custom.models.variants.thinking.label": "사고 유형 (예: Z.ai)", - "provider.custom.models.variants.thinking.placeholder": "thinking", - "provider.custom.models.variants.thinking.enabled": "enabled", - "provider.custom.models.variants.thinking.disabled": "disabled", - "provider.custom.models.variants.thinking.adaptive": "adaptive", - "provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)", - "provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split", - "provider.custom.models.variants.splitReasoning.true": "true", - "provider.custom.models.variants.splitReasoning.false": "false", - "provider.custom.models.variants.chatTemplateArgs.label": "채팅 템플릿 인수로 사고 활성화 (예: Hugging Face)", - "provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args", - "provider.custom.models.variants.chatTemplateArgs.true": "true", - "provider.custom.models.variants.chatTemplateArgs.false": "false", - "provider.custom.models.variants.reasoningEffort.label": "추론 노력", - "provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort", - "provider.custom.models.variants.reasoningEffort.none": "none", - "provider.custom.models.variants.reasoningEffort.minimal": "minimal", - "provider.custom.models.variants.reasoningEffort.low": "low", - "provider.custom.models.variants.reasoningEffort.medium": "medium", - "provider.custom.models.variants.reasoningEffort.high": "high", - "provider.custom.models.variants.reasoningEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)", - "provider.custom.models.variants.outputEffort.placeholder": "effort", - "provider.custom.models.variants.outputEffort.low": "low", - "provider.custom.models.variants.outputEffort.medium": "medium", - "provider.custom.models.variants.outputEffort.high": "high", - "provider.custom.models.variants.outputEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.max": "max", "provider.custom.models.remove": "모델 제거", "provider.custom.models.add": "모델 추가", "provider.custom.models.fetch.authError": "인증에 실패했습니다. 위의 API 키를 확인하고 다시 시도하세요.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts index 64f70685b4c..c3d0b3d28e0 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts @@ -437,45 +437,6 @@ export const dict = { "provider.custom.models.name.placeholder": "Weergavenaam", "provider.custom.models.reasoning.label": "Redeneren", "provider.custom.models.modalities.image": "Afbeelding", - "provider.custom.models.variants.label": "Varianten", - "provider.custom.models.variants.add": "Variant toevoegen", - "provider.custom.models.variants.remove": "Variant verwijderen", - "provider.custom.models.variants.name.label": "Naam", - "provider.custom.models.variants.name.placeholder": "bijv. thinking", - "provider.custom.models.variants.option.unset": "(niet ingesteld)", - "provider.custom.models.variants.enableThinking.label": "Denken inschakelen (bijv. Alibaba)", - "provider.custom.models.variants.enableThinking.placeholder": "enable_thinking", - "provider.custom.models.variants.enableThinking.true": "true", - "provider.custom.models.variants.enableThinking.false": "false", - "provider.custom.models.variants.thinking.label": "Denktype (bijv. Z.ai)", - "provider.custom.models.variants.thinking.placeholder": "thinking", - "provider.custom.models.variants.thinking.enabled": "enabled", - "provider.custom.models.variants.thinking.disabled": "disabled", - "provider.custom.models.variants.thinking.adaptive": "adaptive", - "provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)", - "provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split", - "provider.custom.models.variants.splitReasoning.true": "true", - "provider.custom.models.variants.splitReasoning.false": "false", - "provider.custom.models.variants.chatTemplateArgs.label": - "Nadenken inschakelen via chat template args (bijv. Hugging Face)", - "provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args", - "provider.custom.models.variants.chatTemplateArgs.true": "true", - "provider.custom.models.variants.chatTemplateArgs.false": "false", - "provider.custom.models.variants.reasoningEffort.label": "Redeneerinspanning", - "provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort", - "provider.custom.models.variants.reasoningEffort.none": "none", - "provider.custom.models.variants.reasoningEffort.minimal": "minimal", - "provider.custom.models.variants.reasoningEffort.low": "low", - "provider.custom.models.variants.reasoningEffort.medium": "medium", - "provider.custom.models.variants.reasoningEffort.high": "high", - "provider.custom.models.variants.reasoningEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)", - "provider.custom.models.variants.outputEffort.placeholder": "effort", - "provider.custom.models.variants.outputEffort.low": "low", - "provider.custom.models.variants.outputEffort.medium": "medium", - "provider.custom.models.variants.outputEffort.high": "high", - "provider.custom.models.variants.outputEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.max": "max", "provider.custom.models.remove": "Model verwijderen", "provider.custom.models.add": "Model toevoegen", "provider.custom.models.fetch.authError": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index 103de2edff6..27a1abb7719 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -447,45 +447,6 @@ export const dict = { "provider.custom.models.name.placeholder": "Visningsnavn", "provider.custom.models.reasoning.label": "Resonnering", "provider.custom.models.modalities.image": "Bilde", - "provider.custom.models.variants.label": "Varianter", - "provider.custom.models.variants.add": "Legg til variant", - "provider.custom.models.variants.remove": "Fjern variant", - "provider.custom.models.variants.name.label": "Navn", - "provider.custom.models.variants.name.placeholder": "f.eks. thinking", - "provider.custom.models.variants.option.unset": "(ikke angitt)", - "provider.custom.models.variants.enableThinking.label": "Aktiver tenkning (f.eks. Alibaba)", - "provider.custom.models.variants.enableThinking.placeholder": "enable_thinking", - "provider.custom.models.variants.enableThinking.true": "true", - "provider.custom.models.variants.enableThinking.false": "false", - "provider.custom.models.variants.thinking.label": "Tenkningstype (f.eks. Z.ai)", - "provider.custom.models.variants.thinking.placeholder": "thinking", - "provider.custom.models.variants.thinking.enabled": "enabled", - "provider.custom.models.variants.thinking.disabled": "disabled", - "provider.custom.models.variants.thinking.adaptive": "adaptive", - "provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)", - "provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split", - "provider.custom.models.variants.splitReasoning.true": "true", - "provider.custom.models.variants.splitReasoning.false": "false", - "provider.custom.models.variants.chatTemplateArgs.label": - "Aktiver tenkning via chat-malargumenter (f.eks. Hugging Face)", - "provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args", - "provider.custom.models.variants.chatTemplateArgs.true": "true", - "provider.custom.models.variants.chatTemplateArgs.false": "false", - "provider.custom.models.variants.reasoningEffort.label": "Resonneringsinnsats", - "provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort", - "provider.custom.models.variants.reasoningEffort.none": "none", - "provider.custom.models.variants.reasoningEffort.minimal": "minimal", - "provider.custom.models.variants.reasoningEffort.low": "low", - "provider.custom.models.variants.reasoningEffort.medium": "medium", - "provider.custom.models.variants.reasoningEffort.high": "high", - "provider.custom.models.variants.reasoningEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)", - "provider.custom.models.variants.outputEffort.placeholder": "effort", - "provider.custom.models.variants.outputEffort.low": "low", - "provider.custom.models.variants.outputEffort.medium": "medium", - "provider.custom.models.variants.outputEffort.high": "high", - "provider.custom.models.variants.outputEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.max": "max", "provider.custom.models.remove": "Fjern modell", "provider.custom.models.add": "Legg til modell", "provider.custom.models.fetch.authError": "Autentisering mislyktes. Sjekk API-nøkkelen ovenfor og prøv igjen.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index 18830cde9be..d869a869989 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -442,45 +442,6 @@ export const dict = { "provider.custom.models.name.placeholder": "Nazwa wyświetlana", "provider.custom.models.reasoning.label": "Rozumowanie", "provider.custom.models.modalities.image": "Obraz", - "provider.custom.models.variants.label": "Warianty", - "provider.custom.models.variants.add": "Dodaj wariant", - "provider.custom.models.variants.remove": "Usuń wariant", - "provider.custom.models.variants.name.label": "Nazwa", - "provider.custom.models.variants.name.placeholder": "np. thinking", - "provider.custom.models.variants.option.unset": "(nie ustawiono)", - "provider.custom.models.variants.enableThinking.label": "Włącz myślenie (np. Alibaba)", - "provider.custom.models.variants.enableThinking.placeholder": "enable_thinking", - "provider.custom.models.variants.enableThinking.true": "true", - "provider.custom.models.variants.enableThinking.false": "false", - "provider.custom.models.variants.thinking.label": "Typ myślenia (np. Z.ai)", - "provider.custom.models.variants.thinking.placeholder": "thinking", - "provider.custom.models.variants.thinking.enabled": "enabled", - "provider.custom.models.variants.thinking.disabled": "disabled", - "provider.custom.models.variants.thinking.adaptive": "adaptive", - "provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)", - "provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split", - "provider.custom.models.variants.splitReasoning.true": "true", - "provider.custom.models.variants.splitReasoning.false": "false", - "provider.custom.models.variants.chatTemplateArgs.label": - "Włącz myślenie przez argumenty szablonu czatu (np. Hugging Face)", - "provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args", - "provider.custom.models.variants.chatTemplateArgs.true": "true", - "provider.custom.models.variants.chatTemplateArgs.false": "false", - "provider.custom.models.variants.reasoningEffort.label": "Wysiłek rozumowania", - "provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort", - "provider.custom.models.variants.reasoningEffort.none": "none", - "provider.custom.models.variants.reasoningEffort.minimal": "minimal", - "provider.custom.models.variants.reasoningEffort.low": "low", - "provider.custom.models.variants.reasoningEffort.medium": "medium", - "provider.custom.models.variants.reasoningEffort.high": "high", - "provider.custom.models.variants.reasoningEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)", - "provider.custom.models.variants.outputEffort.placeholder": "effort", - "provider.custom.models.variants.outputEffort.low": "low", - "provider.custom.models.variants.outputEffort.medium": "medium", - "provider.custom.models.variants.outputEffort.high": "high", - "provider.custom.models.variants.outputEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.max": "max", "provider.custom.models.remove": "Usuń model", "provider.custom.models.add": "Dodaj model", "provider.custom.models.fetch.authError": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index 5eca69def5d..e0efa575acd 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -481,45 +481,6 @@ export const dict = { "provider.custom.models.name.placeholder": "Отображаемое имя", "provider.custom.models.reasoning.label": "Рассуждение", "provider.custom.models.modalities.image": "Изображение", - "provider.custom.models.variants.label": "Варианты", - "provider.custom.models.variants.add": "Добавить вариант", - "provider.custom.models.variants.remove": "Удалить вариант", - "provider.custom.models.variants.name.label": "Имя", - "provider.custom.models.variants.name.placeholder": "напр. thinking", - "provider.custom.models.variants.option.unset": "(не задано)", - "provider.custom.models.variants.enableThinking.label": "Включить мышление (напр. Alibaba)", - "provider.custom.models.variants.enableThinking.placeholder": "enable_thinking", - "provider.custom.models.variants.enableThinking.true": "true", - "provider.custom.models.variants.enableThinking.false": "false", - "provider.custom.models.variants.thinking.label": "Тип мышления (напр. Z.ai)", - "provider.custom.models.variants.thinking.placeholder": "thinking", - "provider.custom.models.variants.thinking.enabled": "enabled", - "provider.custom.models.variants.thinking.disabled": "disabled", - "provider.custom.models.variants.thinking.adaptive": "adaptive", - "provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)", - "provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split", - "provider.custom.models.variants.splitReasoning.true": "true", - "provider.custom.models.variants.splitReasoning.false": "false", - "provider.custom.models.variants.chatTemplateArgs.label": - "Включить размышление через аргументы шаблона чата (напр. Hugging Face)", - "provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args", - "provider.custom.models.variants.chatTemplateArgs.true": "true", - "provider.custom.models.variants.chatTemplateArgs.false": "false", - "provider.custom.models.variants.reasoningEffort.label": "Усилие рассуждения", - "provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort", - "provider.custom.models.variants.reasoningEffort.none": "none", - "provider.custom.models.variants.reasoningEffort.minimal": "minimal", - "provider.custom.models.variants.reasoningEffort.low": "low", - "provider.custom.models.variants.reasoningEffort.medium": "medium", - "provider.custom.models.variants.reasoningEffort.high": "high", - "provider.custom.models.variants.reasoningEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)", - "provider.custom.models.variants.outputEffort.placeholder": "effort", - "provider.custom.models.variants.outputEffort.low": "low", - "provider.custom.models.variants.outputEffort.medium": "medium", - "provider.custom.models.variants.outputEffort.high": "high", - "provider.custom.models.variants.outputEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.max": "max", "provider.custom.models.remove": "Удалить модель", "provider.custom.models.add": "Добавить модель", "provider.custom.models.fetch.authError": "Ошибка аутентификации. Проверьте API-ключ выше и попробуйте снова.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index c4dbc68bcf5..2393f6c3eed 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -478,45 +478,6 @@ export const dict = { "provider.custom.models.name.placeholder": "ชื่อที่แสดง", "provider.custom.models.reasoning.label": "การใช้เหตุผล", "provider.custom.models.modalities.image": "รูปภาพ", - "provider.custom.models.variants.label": "รูปแบบ", - "provider.custom.models.variants.add": "เพิ่มรูปแบบ", - "provider.custom.models.variants.remove": "ลบรูปแบบ", - "provider.custom.models.variants.name.label": "ชื่อ", - "provider.custom.models.variants.name.placeholder": "เช่น thinking", - "provider.custom.models.variants.option.unset": "(ไม่ได้ตั้งค่า)", - "provider.custom.models.variants.enableThinking.label": "เปิดใช้งานการคิด (เช่น Alibaba)", - "provider.custom.models.variants.enableThinking.placeholder": "enable_thinking", - "provider.custom.models.variants.enableThinking.true": "true", - "provider.custom.models.variants.enableThinking.false": "false", - "provider.custom.models.variants.thinking.label": "ประเภทการคิด (เช่น Z.ai)", - "provider.custom.models.variants.thinking.placeholder": "thinking", - "provider.custom.models.variants.thinking.enabled": "enabled", - "provider.custom.models.variants.thinking.disabled": "disabled", - "provider.custom.models.variants.thinking.adaptive": "adaptive", - "provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)", - "provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split", - "provider.custom.models.variants.splitReasoning.true": "true", - "provider.custom.models.variants.splitReasoning.false": "false", - "provider.custom.models.variants.chatTemplateArgs.label": - "เปิดใช้งานการคิดผ่านอาร์กิวเมนต์เทมเพลตแชท (เช่น Hugging Face)", - "provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args", - "provider.custom.models.variants.chatTemplateArgs.true": "true", - "provider.custom.models.variants.chatTemplateArgs.false": "false", - "provider.custom.models.variants.reasoningEffort.label": "ระดับการใช้เหตุผล", - "provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort", - "provider.custom.models.variants.reasoningEffort.none": "none", - "provider.custom.models.variants.reasoningEffort.minimal": "minimal", - "provider.custom.models.variants.reasoningEffort.low": "low", - "provider.custom.models.variants.reasoningEffort.medium": "medium", - "provider.custom.models.variants.reasoningEffort.high": "high", - "provider.custom.models.variants.reasoningEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)", - "provider.custom.models.variants.outputEffort.placeholder": "effort", - "provider.custom.models.variants.outputEffort.low": "low", - "provider.custom.models.variants.outputEffort.medium": "medium", - "provider.custom.models.variants.outputEffort.high": "high", - "provider.custom.models.variants.outputEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.max": "max", "provider.custom.models.remove": "ลบโมเดล", "provider.custom.models.add": "เพิ่มโมเดล", "provider.custom.models.fetch.authError": "การยืนยันตัวตนล้มเหลว ตรวจสอบคีย์ API ด้านบนแล้วลองอีกครั้ง", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts index 0761b27b94a..4f76d9c8778 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts @@ -432,45 +432,6 @@ export const dict = { "provider.custom.models.name.placeholder": "Görünen Ad", "provider.custom.models.reasoning.label": "Akıl Yürütme", "provider.custom.models.modalities.image": "Görüntü", - "provider.custom.models.variants.label": "Varyantlar", - "provider.custom.models.variants.add": "Varyant ekle", - "provider.custom.models.variants.remove": "Varyantı kaldır", - "provider.custom.models.variants.name.label": "Ad", - "provider.custom.models.variants.name.placeholder": "örn. thinking", - "provider.custom.models.variants.option.unset": "(ayarlanmadı)", - "provider.custom.models.variants.enableThinking.label": "Düşünmeyi etkinleştir (örn. Alibaba)", - "provider.custom.models.variants.enableThinking.placeholder": "enable_thinking", - "provider.custom.models.variants.enableThinking.true": "true", - "provider.custom.models.variants.enableThinking.false": "false", - "provider.custom.models.variants.thinking.label": "Düşünme türü (örn. Z.ai)", - "provider.custom.models.variants.thinking.placeholder": "thinking", - "provider.custom.models.variants.thinking.enabled": "enabled", - "provider.custom.models.variants.thinking.disabled": "disabled", - "provider.custom.models.variants.thinking.adaptive": "adaptive", - "provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)", - "provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split", - "provider.custom.models.variants.splitReasoning.true": "true", - "provider.custom.models.variants.splitReasoning.false": "false", - "provider.custom.models.variants.chatTemplateArgs.label": - "Sohbet şablonu argümanları ile düşünmeyi etkinleştir (ör. Hugging Face)", - "provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args", - "provider.custom.models.variants.chatTemplateArgs.true": "true", - "provider.custom.models.variants.chatTemplateArgs.false": "false", - "provider.custom.models.variants.reasoningEffort.label": "Akıl yürütme çabası", - "provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort", - "provider.custom.models.variants.reasoningEffort.none": "none", - "provider.custom.models.variants.reasoningEffort.minimal": "minimal", - "provider.custom.models.variants.reasoningEffort.low": "low", - "provider.custom.models.variants.reasoningEffort.medium": "medium", - "provider.custom.models.variants.reasoningEffort.high": "high", - "provider.custom.models.variants.reasoningEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)", - "provider.custom.models.variants.outputEffort.placeholder": "effort", - "provider.custom.models.variants.outputEffort.low": "low", - "provider.custom.models.variants.outputEffort.medium": "medium", - "provider.custom.models.variants.outputEffort.high": "high", - "provider.custom.models.variants.outputEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.max": "max", "provider.custom.models.remove": "Modeli kaldır", "provider.custom.models.add": "Model ekle", "provider.custom.models.fetch.authError": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts index b4b8c529d24..9b47e1a3a24 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts @@ -436,45 +436,6 @@ export const dict = { "provider.custom.models.name.placeholder": "Відображувана назва", "provider.custom.models.reasoning.label": "Міркування", "provider.custom.models.modalities.image": "Зображення", - "provider.custom.models.variants.label": "Варіанти", - "provider.custom.models.variants.add": "Додати варіант", - "provider.custom.models.variants.remove": "Видалити варіант", - "provider.custom.models.variants.name.label": "Ім'я", - "provider.custom.models.variants.name.placeholder": "напр. thinking", - "provider.custom.models.variants.option.unset": "(не встановлено)", - "provider.custom.models.variants.enableThinking.label": "Увімкнути мислення (напр. Alibaba)", - "provider.custom.models.variants.enableThinking.placeholder": "enable_thinking", - "provider.custom.models.variants.enableThinking.true": "true", - "provider.custom.models.variants.enableThinking.false": "false", - "provider.custom.models.variants.thinking.label": "Тип мислення (напр. Z.ai)", - "provider.custom.models.variants.thinking.placeholder": "thinking", - "provider.custom.models.variants.thinking.enabled": "enabled", - "provider.custom.models.variants.thinking.disabled": "disabled", - "provider.custom.models.variants.thinking.adaptive": "adaptive", - "provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)", - "provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split", - "provider.custom.models.variants.splitReasoning.true": "true", - "provider.custom.models.variants.splitReasoning.false": "false", - "provider.custom.models.variants.chatTemplateArgs.label": - "Увімкнути мислення через аргументи шаблону чату (напр. Hugging Face)", - "provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args", - "provider.custom.models.variants.chatTemplateArgs.true": "true", - "provider.custom.models.variants.chatTemplateArgs.false": "false", - "provider.custom.models.variants.reasoningEffort.label": "Зусилля міркування", - "provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort", - "provider.custom.models.variants.reasoningEffort.none": "none", - "provider.custom.models.variants.reasoningEffort.minimal": "minimal", - "provider.custom.models.variants.reasoningEffort.low": "low", - "provider.custom.models.variants.reasoningEffort.medium": "medium", - "provider.custom.models.variants.reasoningEffort.high": "high", - "provider.custom.models.variants.reasoningEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)", - "provider.custom.models.variants.outputEffort.placeholder": "effort", - "provider.custom.models.variants.outputEffort.low": "low", - "provider.custom.models.variants.outputEffort.medium": "medium", - "provider.custom.models.variants.outputEffort.high": "high", - "provider.custom.models.variants.outputEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.max": "max", "provider.custom.models.remove": "Видалити модель", "provider.custom.models.add": "Додати модель", "provider.custom.models.fetch.authError": "Автентифікація не вдалася. Перевірте API-ключ вище і спробуйте ще раз.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index 041c72d116b..eadac1499ae 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -463,44 +463,6 @@ export const dict = { "provider.custom.models.name.placeholder": "显示名称", "provider.custom.models.reasoning.label": "推理", "provider.custom.models.modalities.image": "图片", - "provider.custom.models.variants.label": "变体", - "provider.custom.models.variants.add": "添加变体", - "provider.custom.models.variants.remove": "移除变体", - "provider.custom.models.variants.name.label": "名称", - "provider.custom.models.variants.name.placeholder": "例如 thinking", - "provider.custom.models.variants.option.unset": "(未设置)", - "provider.custom.models.variants.enableThinking.label": "启用思考 (例如 Alibaba)", - "provider.custom.models.variants.enableThinking.placeholder": "enable_thinking", - "provider.custom.models.variants.enableThinking.true": "true", - "provider.custom.models.variants.enableThinking.false": "false", - "provider.custom.models.variants.thinking.label": "思考类型 (例如 Z.ai)", - "provider.custom.models.variants.thinking.placeholder": "thinking", - "provider.custom.models.variants.thinking.enabled": "enabled", - "provider.custom.models.variants.thinking.disabled": "disabled", - "provider.custom.models.variants.thinking.adaptive": "adaptive", - "provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)", - "provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split", - "provider.custom.models.variants.splitReasoning.true": "true", - "provider.custom.models.variants.splitReasoning.false": "false", - "provider.custom.models.variants.chatTemplateArgs.label": "通过聊天模板参数启用思考(如 Hugging Face)", - "provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args", - "provider.custom.models.variants.chatTemplateArgs.true": "true", - "provider.custom.models.variants.chatTemplateArgs.false": "false", - "provider.custom.models.variants.reasoningEffort.label": "推理强度", - "provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort", - "provider.custom.models.variants.reasoningEffort.none": "none", - "provider.custom.models.variants.reasoningEffort.minimal": "minimal", - "provider.custom.models.variants.reasoningEffort.low": "low", - "provider.custom.models.variants.reasoningEffort.medium": "medium", - "provider.custom.models.variants.reasoningEffort.high": "high", - "provider.custom.models.variants.reasoningEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)", - "provider.custom.models.variants.outputEffort.placeholder": "effort", - "provider.custom.models.variants.outputEffort.low": "low", - "provider.custom.models.variants.outputEffort.medium": "medium", - "provider.custom.models.variants.outputEffort.high": "high", - "provider.custom.models.variants.outputEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.max": "max", "provider.custom.models.remove": "移除模型", "provider.custom.models.add": "添加模型", "provider.custom.models.fetch.authError": "认证失败。请检查上方的 API 密钥后重试。", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index 52ecca5fa9e..8cec3278a7a 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -423,44 +423,6 @@ export const dict = { "provider.custom.models.name.placeholder": "顯示名稱", "provider.custom.models.reasoning.label": "推理", "provider.custom.models.modalities.image": "圖片", - "provider.custom.models.variants.label": "變體", - "provider.custom.models.variants.add": "新增變體", - "provider.custom.models.variants.remove": "移除變體", - "provider.custom.models.variants.name.label": "名稱", - "provider.custom.models.variants.name.placeholder": "例如 thinking", - "provider.custom.models.variants.option.unset": "(未設定)", - "provider.custom.models.variants.enableThinking.label": "啟用思考 (例如 Alibaba)", - "provider.custom.models.variants.enableThinking.placeholder": "enable_thinking", - "provider.custom.models.variants.enableThinking.true": "true", - "provider.custom.models.variants.enableThinking.false": "false", - "provider.custom.models.variants.thinking.label": "思考類型 (例如 Z.ai)", - "provider.custom.models.variants.thinking.placeholder": "thinking", - "provider.custom.models.variants.thinking.enabled": "enabled", - "provider.custom.models.variants.thinking.disabled": "disabled", - "provider.custom.models.variants.thinking.adaptive": "adaptive", - "provider.custom.models.variants.splitReasoning.label": "Split reasoning (required for e.g. MiniMax)", - "provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split", - "provider.custom.models.variants.splitReasoning.true": "true", - "provider.custom.models.variants.splitReasoning.false": "false", - "provider.custom.models.variants.chatTemplateArgs.label": "透過聊天範本參數啟用思考(如 Hugging Face)", - "provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args", - "provider.custom.models.variants.chatTemplateArgs.true": "true", - "provider.custom.models.variants.chatTemplateArgs.false": "false", - "provider.custom.models.variants.reasoningEffort.label": "推理強度", - "provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort", - "provider.custom.models.variants.reasoningEffort.none": "none", - "provider.custom.models.variants.reasoningEffort.minimal": "minimal", - "provider.custom.models.variants.reasoningEffort.low": "low", - "provider.custom.models.variants.reasoningEffort.medium": "medium", - "provider.custom.models.variants.reasoningEffort.high": "high", - "provider.custom.models.variants.reasoningEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.label": "Output effort (e.g. Anthropic)", - "provider.custom.models.variants.outputEffort.placeholder": "effort", - "provider.custom.models.variants.outputEffort.low": "low", - "provider.custom.models.variants.outputEffort.medium": "medium", - "provider.custom.models.variants.outputEffort.high": "high", - "provider.custom.models.variants.outputEffort.xhigh": "xhigh", - "provider.custom.models.variants.outputEffort.max": "max", "provider.custom.models.remove": "移除模型", "provider.custom.models.add": "新增模型", "provider.custom.models.fetch.authError": "驗證失敗。請檢查上方的 API 金鑰後重試。", diff --git a/packages/opencode/src/kilocode/provider/provider.ts b/packages/opencode/src/kilocode/provider/provider.ts index cc8a452fc96..13ec8bdddd2 100644 --- a/packages/opencode/src/kilocode/provider/provider.ts +++ b/packages/opencode/src/kilocode/provider/provider.ts @@ -15,6 +15,8 @@ import { ProviderError } from "@/provider/error" import { Effect, Schema } from "effect" import type { LanguageModelV3 } from "@ai-sdk/provider" import { mapValues, omit, pickBy } from "remeda" +import { reasoningSummary } from "./reasoning-summary" +import type { Provider } from "@/provider/provider" /** Default timeout (ms) for provider HTTP requests (connection phase). */ export const REQUEST_TIMEOUT_MS = 300_000 // 5 minutes @@ -95,6 +97,37 @@ export function patchConfigModel(cfg: any, existing: any) { } } +const CUSTOM_PROVIDER_PACKAGES = new Set(["@ai-sdk/openai-compatible", "@ai-sdk/openai", "@ai-sdk/anthropic"]) +const FALLBACK_EFFORTS = ["none", "low", "medium", "high", "xhigh", "max"] +type Variants = NonNullable +type Generate = (model: Provider.Model) => Variants + +export function customProviderVariants(model: Provider.Model, npm: unknown, generate: Generate): Variants { + const supported = typeof npm === "string" && CUSTOM_PROVIDER_PACKAGES.has(npm) && model.api.npm === npm + const variants = generate(supported ? { ...model, variants: {} } : model) + if (Object.keys(variants).length > 0) return variants + if (!model.capabilities.reasoning || !supported) return variants + + return Object.fromEntries( + FALLBACK_EFFORTS.map((effort) => { + if (npm === "@ai-sdk/anthropic") { + return [effort, effort === "none" ? { thinking: { type: "disabled" } } : { effort }] + } + if (npm === "@ai-sdk/openai") { + return [ + effort, + { + reasoningEffort: effort, + reasoningSummary: reasoningSummary(model), + include: ["reasoning.encrypted_content"], + }, + ] + } + return [effort, { reasoningEffort: effort }] + }), + ) +} + // --------------------------------------------------------------------------- // Custom loaders (new or fully-replaced loaders) // --------------------------------------------------------------------------- diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 897e8a6880f..e8f134f2871 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -37,6 +37,7 @@ import { KILO_MODEL_SCHEMA_EXTENSIONS, patchModelsDevModel as patchKiloModel, patchConfigModel as patchKiloConfigModel, + customProviderVariants, patchCustomLoaderResult, patchKiloProviderPrivacy, kiloSmallModelPriority, @@ -1526,7 +1527,14 @@ const layer = Layer.effect( // variants: {}, // kilocode_change, moved into patchKiloConfigModel ...patchKiloConfigModel(model, existingModel), // kilocode_change } - const merged = mergeDeep(ProviderTransform.variants(parsedModel), model.variants ?? {}) + // kilocode_change start + const generated = customProviderVariants( + parsedModel, + model.provider?.npm ?? provider.npm, + ProviderTransform.variants, + ) + const merged = mergeDeep(generated, model.variants ?? {}) + // kilocode_change end parsedModel.variants = mapValues( pickBy(merged, (v): v is NonNullable => !!v && !v.disabled), // kilocode_change - drop null delete sentinels (v) => omit(v, ["disabled"]), diff --git a/packages/opencode/test/kilocode/custom-provider-variants.test.ts b/packages/opencode/test/kilocode/custom-provider-variants.test.ts new file mode 100644 index 00000000000..e3a6e6be179 --- /dev/null +++ b/packages/opencode/test/kilocode/custom-provider-variants.test.ts @@ -0,0 +1,46 @@ +import { expect } from "bun:test" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" +import { ProviderV2 } from "@opencode-ai/core/provider" +import { Effect } from "effect" +import { Env } from "@/env" +import { Plugin } from "@/plugin" +import { Provider } from "@/provider/provider" +import { testEffect } from "../lib/effect" + +const it = testEffect(LayerNode.compile(LayerNode.group([Provider.node, Env.node, Plugin.node]))) + +it.instance( + "adds fallback efforts to configured reasoning models and merges explicit variants", + () => + Effect.gen(function* () { + const providers = yield* Provider.use.list() + const model = providers[ProviderV2.ID.make("custom")]?.models["qwen-custom"] + + expect(Object.keys(model?.variants ?? {})).toEqual(["none", "low", "medium", "xhigh", "max", "custom"]) + expect(model?.variants?.none).toEqual({ reasoningEffort: "none" }) + expect(model?.variants?.high).toBeUndefined() + expect(model?.variants?.custom).toEqual({ reasoningEffort: "custom" }) + }), + { + config: { + provider: { + custom: { + name: "Custom", + npm: "@ai-sdk/openai-compatible", + options: { apiKey: "test" }, + models: { + "qwen-custom": { + name: "Qwen Custom", + reasoning: true, + limit: { context: 128_000, output: 16_000 }, + variants: { + high: { disabled: true }, + custom: { reasoningEffort: "custom" }, + }, + }, + }, + }, + }, + }, + }, +) diff --git a/packages/opencode/test/kilocode/provider-reasoning-options.test.ts b/packages/opencode/test/kilocode/provider-reasoning-options.test.ts index 711820ede3b..268d27f4399 100644 --- a/packages/opencode/test/kilocode/provider-reasoning-options.test.ts +++ b/packages/opencode/test/kilocode/provider-reasoning-options.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "bun:test" import { ProviderTransform } from "../../src/provider/transform" import { Provider } from "../../src/provider/provider" +import { customProviderVariants } from "../../src/kilocode/provider/provider" import type * as ModelsDev from "@opencode-ai/core/models-dev" function mockModel(overrides: Partial = {}): any { @@ -138,3 +139,55 @@ describe("ProviderTransform.reasoningVariants - models.dev reasoning_options", ( expect(Object.keys(gpt5.variants ?? {})).toEqual(["minimal", "low", "medium", "high"]) }) }) + +describe("custom provider fallback reasoning efforts", () => { + const efforts = ["none", "low", "medium", "high", "xhigh", "max"] + + for (const npm of ["@ai-sdk/openai-compatible", "@ai-sdk/openai", "@ai-sdk/anthropic"]) { + test(`${npm} exposes broad efforts after heuristics fail`, () => { + const model = mockModel({ id: "qwen-custom", api: { id: "qwen-custom", url: "https://api.test.com", npm } }) + const generated = ProviderTransform.variants({ ...model, variants: {} }) + expect(generated).toEqual({}) + + const result = customProviderVariants(model, npm, ProviderTransform.variants) + + expect(Object.keys(result)).toEqual(efforts) + if (npm === "@ai-sdk/anthropic") { + expect(result.none).toEqual({ thinking: { type: "disabled" } }) + expect(result.max).toEqual({ effort: "max" }) + return + } + expect(result.none?.reasoningEffort).toBe("none") + expect(result.max?.reasoningEffort).toBe("max") + }) + } + + test("preserves successful heuristics", () => { + const model = mockModel({ api: { id: "custom", url: "https://api.test.com", npm: "@ai-sdk/openai-compatible" } }) + const generated = { low: { reasoningEffort: "low" }, high: { reasoningEffort: "high" } } + expect(customProviderVariants(model, model.api.npm, () => generated)).toBe(generated) + }) + + test("requires a reasoning model with an explicitly configured supported package", () => { + const npm = "@ai-sdk/openai-compatible" + const plain = mockModel({ + api: { id: "custom", url: "https://api.test.com", npm }, + capabilities: { ...mockModel().capabilities, reasoning: false }, + }) + expect(customProviderVariants(plain, npm, () => ({}))).toEqual({}) + expect( + customProviderVariants( + mockModel({ api: { id: "custom", url: "https://api.test.com", npm } }), + undefined, + () => ({}), + ), + ).toEqual({}) + expect( + customProviderVariants( + mockModel({ api: { id: "custom", url: "https://api.test.com", npm: "unrelated-provider" } }), + "unrelated-provider", + () => ({}), + ), + ).toEqual({}) + }) +}) From 71a1c6bc3d571aa464376117080680dcaf5588f2 Mon Sep 17 00:00:00 2001 From: Christiaan Arnoldus Date: Thu, 6 Aug 2026 13:37:59 +0200 Subject: [PATCH 16/73] fix(vscode): surface custom variant validation --- .../settings/CustomProviderModelCard.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderModelCard.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderModelCard.tsx index fa440816f31..a33984738c3 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderModelCard.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderModelCard.tsx @@ -1,5 +1,6 @@ import { IconButton } from "@kilocode/kilo-ui/icon-button" import { TextField } from "@kilocode/kilo-ui/text-field" +import { Show } from "solid-js" import { useLanguage } from "../../context/language" export type Translator = ReturnType["t"] @@ -40,7 +41,7 @@ export type ModelEntry = { type ModelCardProps = { m: ModelEntry - errors: { id?: string; name?: string } + errors: { id?: string; name?: string; variants?: Array<{ name?: string }> } t: Translator canRemove: boolean onChangeId: (val: string) => void @@ -51,6 +52,8 @@ type ModelCardProps = { } export function ModelCard(props: ModelCardProps) { + const issue = () => props.errors.variants?.find((error) => error.name)?.name + return (
{props.t("provider.custom.models.modalities.image")} + + + {(error) => ( + + {error()} + + )} +
) } From 2dc24c1cfdd6271e253e1a7623639b043f33fd6f Mon Sep 17 00:00:00 2001 From: Christiaan Arnoldus Date: Thu, 6 Aug 2026 14:03:12 +0200 Subject: [PATCH 17/73] fix(vscode): identify invalid custom variants --- .../tests/unit/custom-provider-dialog-validate.test.ts | 6 +++--- .../src/components/settings/CustomProviderValidation.ts | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/kilo-vscode/tests/unit/custom-provider-dialog-validate.test.ts b/packages/kilo-vscode/tests/unit/custom-provider-dialog-validate.test.ts index ff405481c06..814b2147e0b 100644 --- a/packages/kilo-vscode/tests/unit/custom-provider-dialog-validate.test.ts +++ b/packages/kilo-vscode/tests/unit/custom-provider-dialog-validate.test.ts @@ -93,7 +93,7 @@ describe("validateCustomProvider – variant name validation", () => { ] const out = validateCustomProvider(args(form)) expect(out.result).toBeUndefined() - expect(out.errors.models[0].variants?.[0]?.name).toBe("provider.custom.error.required") + expect(out.errors.models[0].variants?.[0]?.name).toBe('variants[""]: provider.custom.error.required') }) it("blocks submit and reports error when reasoning is enabled with a whitespace-only variant name", () => { @@ -112,7 +112,7 @@ describe("validateCustomProvider – variant name validation", () => { ] const out = validateCustomProvider(args(form)) expect(out.result).toBeUndefined() - expect(out.errors.models[0].variants?.[0]?.name).toBe("provider.custom.error.required") + expect(out.errors.models[0].variants?.[0]?.name).toBe('variants[" "]: provider.custom.error.required') }) it("blocks submit and reports duplicate error for two variants with the same name", () => { @@ -140,7 +140,7 @@ describe("validateCustomProvider – variant name validation", () => { ] const out = validateCustomProvider(args(form)) expect(out.result).toBeUndefined() - expect(out.errors.models[0].variants?.[1]?.name).toBe("provider.custom.error.duplicate") + expect(out.errors.models[0].variants?.[1]?.name).toBe('variants["fast"]: provider.custom.error.duplicate') }) it("ignores variants entirely when reasoning is disabled, even if they have empty names", () => { diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderValidation.ts b/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderValidation.ts index 1032e6cf91d..2300e9fe910 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderValidation.ts +++ b/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderValidation.ts @@ -57,8 +57,9 @@ const PROVIDER_ID = /^[a-z0-9][a-z0-9-_]*$/ function checkVariant(v: VariantEntry, seen: Set, t: Translator) { const n = v.name.trim() - if (!n) return { name: t("provider.custom.error.required") } - if (seen.has(n)) return { name: t("provider.custom.error.duplicate") } + const path = `variants[${JSON.stringify(v.name)}]` + if (!n) return { name: `${path}: ${t("provider.custom.error.required")}` } + if (seen.has(n)) return { name: `${path}: ${t("provider.custom.error.duplicate")}` } seen.add(n) return { name: undefined } } From 907a5f65863241e27244716c4e2197a24f4ba7bc Mon Sep 17 00:00:00 2001 From: cmanu Date: Thu, 6 Aug 2026 10:55:19 -0700 Subject: [PATCH 18/73] feat(pr-view): Handle gh auth errors in the ui --- .../src/agent-manager/PRStatusPoller.ts | 4 +++- .../src/agent-manager/pr-status-bridge.ts | 15 +++++++++++++++ packages/kilo-vscode/src/agent-manager/types.ts | 6 ++++++ .../webview-ui/agent-manager/AgentManagerApp.tsx | 6 ++++++ .../webview-ui/agent-manager/i18n/en.ts | 4 ++++ .../src/types/messages/extension-messages.ts | 6 ++++++ 6 files changed, 40 insertions(+), 1 deletion(-) diff --git a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts index 813c79f25d9..a4fa6ac7aa0 100644 --- a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts +++ b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts @@ -18,6 +18,7 @@ interface PRStatusPollerOptions { } const GH_PROBE_TTL = 300_000 // 5 minutes — gh installation state rarely changes at runtime +const GH_PROBE_FAILURE_TTL = 30_000 // 30 seconds — retry faster after a failed probe const MAX_BACKOFF = 120_000 // 2 minutes — cap for exponential backoff on repeated errors const BACKOFF_MULTIPLIER = 2 const PR_LOOKUP_TTL = 10_000 // 10 seconds — short TTL; only the active worktree polls so this stays cheap @@ -164,7 +165,8 @@ export class PRStatusPoller { private async probeGh(): Promise { const now = Date.now() - if (this.ghAvailable !== undefined && now - this.ghProbeTime < GH_PROBE_TTL) { + const ttl = this.ghAvailable === false ? GH_PROBE_FAILURE_TTL : GH_PROBE_TTL + if (this.ghAvailable !== undefined && now - this.ghProbeTime < ttl) { return this.ghAvailable } try { diff --git a/packages/kilo-vscode/src/agent-manager/pr-status-bridge.ts b/packages/kilo-vscode/src/agent-manager/pr-status-bridge.ts index c64da3971eb..6eb742fbd4e 100644 --- a/packages/kilo-vscode/src/agent-manager/pr-status-bridge.ts +++ b/packages/kilo-vscode/src/agent-manager/pr-status-bridge.ts @@ -31,6 +31,7 @@ export class PRStatusBridge { readonly poller: PRStatusPoller private readonly cache = new Map() private readonly host: PRBridgeHost + private lastErrorNotified: "gh_missing" | "gh_auth" | "fetch_failed" | undefined constructor(host: PRBridgeHost) { this.host = host @@ -61,6 +62,8 @@ export class PRStatusBridge { /** Replay cached PR statuses to a freshly-connected webview. */ replay(): void { this.cache.forEach((msg) => this.host.postToWebview(msg)) + if (this.lastErrorNotified === "gh_auth" || this.lastErrorNotified === "gh_missing") + this.host.postToWebview({ type: "agentManager.prError", error: this.lastErrorNotified } as AgentManagerOutMessage) } snapshot(): Map { @@ -93,6 +96,13 @@ export class PRStatusBridge { reset(): void { this.poller.stop() this.cache.clear() + this.lastErrorNotified = undefined + } + + notifyError(err: "gh_missing" | "gh_auth" | "fetch_failed"): void { + if (this.lastErrorNotified === err) return + this.lastErrorNotified = err + this.host.postToWebview({ type: "agentManager.prError", error: err } as AgentManagerOutMessage) } } @@ -115,6 +125,11 @@ function bridgePollerOpts(bridge: PRStatusBridge, host: PRBridgeHost) { pr: null, error: err, } as AgentManagerOutMessage) + // Always forward auth/missing errors so the webview can show a toast, + // regardless of whether prior data exists. Deduplicate per error type + // so multiple failing worktrees don't produce multiple toasts. + if (err === "gh_auth" || err === "gh_missing") + bridge.notifyError(err) return } const msg = { type: "agentManager.prStatus", worktreeId: id, pr, error: err } as AgentManagerOutMessage diff --git a/packages/kilo-vscode/src/agent-manager/types.ts b/packages/kilo-vscode/src/agent-manager/types.ts index 186ac19f2ec..39f2844ab6d 100644 --- a/packages/kilo-vscode/src/agent-manager/types.ts +++ b/packages/kilo-vscode/src/agent-manager/types.ts @@ -385,6 +385,11 @@ interface PRStatusOutMessage { error?: "gh_missing" | "gh_auth" | "fetch_failed" } +interface PRErrorOutMessage { + type: "agentManager.prError" + error: "gh_missing" | "gh_auth" | "fetch_failed" +} + interface ActionOutMessage { type: "action" action: string @@ -424,6 +429,7 @@ export type AgentManagerOutMessage = | RevertWorktreeFileResultMessage | DiffBranchesMessage | PRStatusOutMessage + | PRErrorOutMessage | ActionOutMessage | RunStatusMessage | TerminalCreatedMessage diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index 842a33b2540..2c822a35e38 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -37,6 +37,7 @@ import type { RunStatus, PRStatus, AgentManagerPRStatusMessage, + AgentManagerPRErrorMessage, AgentManagerProjectsMessage, AgentProjectSnapshot, ManagedSessionState, @@ -1566,6 +1567,11 @@ const AgentManagerContent: Component = () => { managedSession: focusManagedSession, }) + if (msg.type === "agentManager.prError") { + const ev = msg as AgentManagerPRErrorMessage + showToast({ variant: "error", title: t(`agentManager.pr.error.${ev.error}.title`), description: t(`agentManager.pr.error.${ev.error}.description`) }) + } + if (projectLive.apply(msg)) return }) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts index 56954f2c7ac..382bddd0451 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts @@ -214,4 +214,8 @@ export const dict = { "agentManager.import.noBranchesFound": "No branches found.", "agentManager.import.noBranchesHint": "Paste a PR URL above or create a new worktree.", "agentManager.import.failed": "Import failed", + "agentManager.pr.error.gh_auth.title": "GitHub authentication required", + "agentManager.pr.error.gh_auth.description": "Run 'gh auth login' in your terminal to restore PR status.", + "agentManager.pr.error.gh_missing.title": "GitHub CLI not installed", + "agentManager.pr.error.gh_missing.description": "Install the GitHub CLI to enable PR status.", } diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts index d0fb0339252..a3e3b16eea8 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts @@ -1034,6 +1034,11 @@ export interface AgentManagerPRStatusMessage { error?: "gh_missing" | "gh_auth" | "fetch_failed" } +export interface AgentManagerPRErrorMessage { + type: "agentManager.prError" + error: "gh_missing" | "gh_auth" | "fetch_failed" +} + // Sidebar: Live worktree diff stats (extension → webview) export interface WorktreeStatsLoadedMessage { type: "worktreeStatsLoaded" @@ -1397,6 +1402,7 @@ export type ExtensionMessage = | AgentManagerWorktreeStatsMessage | AgentManagerLocalStatsMessage | AgentManagerPRStatusMessage + | AgentManagerPRErrorMessage | AgentManagerTerminalCreatedMessage | AgentManagerTerminalRestartedMessage | AgentManagerTerminalFontChangedMessage From 7415a47248684eb0693270c496028ddf684d7b9d Mon Sep 17 00:00:00 2001 From: cmanu Date: Thu, 6 Aug 2026 14:33:52 -0700 Subject: [PATCH 19/73] fix(pr-view): merge 'main' causes line count linter error so need to move code into new files --- .../agent-manager/AgentManagerApp.tsx | 113 ++-------------- .../agent-manager/default-base-branch.tsx | 121 ++++++++++++++++++ .../webview-ui/src/context/session-merge.ts | 41 ++++++ .../webview-ui/src/context/session.tsx | 40 +----- 4 files changed, 178 insertions(+), 137 deletions(-) create mode 100644 packages/kilo-vscode/webview-ui/agent-manager/default-base-branch.tsx create mode 100644 packages/kilo-vscode/webview-ui/src/context/session-merge.ts diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index 7d6aa4195be..2e6b55063d5 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -21,7 +21,6 @@ import type { AgentManagerKeybindingsMessage, AgentManagerMultiVersionProgressMessage, AgentManagerSendInitialMessage, - AgentManagerBranchesMessage, AgentManagerWorktreeDiffMessage, AgentManagerWorktreeDiffFileMessage, AgentManagerWorktreeDiffLoadingMessage, @@ -44,7 +43,6 @@ import type { SectionState, SessionInfo, SessionCreatedMessage, - BranchInfo, TerminalDestination, TerminalFont, } from "../src/types/messages" @@ -154,8 +152,10 @@ import type { ReviewComment } from "../diff-viewer/review-comments" import { clearReviewComposer, createReviewComposer } from "../diff-viewer/review-annotations" import type { SidebarSearchMenuRef } from "./SidebarSearchMenu" import { createSidebarSearch, type SidebarSearchItem } from "./sidebar-search" -import { BranchSelect } from "../src/components/shared/BranchSelect" import { randomColor } from "./section-colors" +import { createMarkdownRender } from "./review-preferences" +import { createChangeDefaultBaseBranch } from "./default-base-branch" +import { createSidebarCollapse } from "./sidebar-collapse" import { createNewTaskDrafts } from "./new-task-drafts" import { buildTopLevelItems, @@ -174,8 +174,6 @@ import { DiffScopeControls } from "../diff-viewer/DiffScopeControls" import { scopeCapabilities } from "./diff-scope-state" import { createDiffReviewScope } from "./diff-review-scope" import { initialMessage, seedInitialVariant } from "./initial-message" -import { createMarkdownRender } from "./review-preferences" -import { createSidebarCollapse } from "./sidebar-collapse" import { SidebarToggleButton } from "./SidebarToggleButton" import { setTabWidths } from "./tab-widths" import { clampPanelWidth, maxPanelWidth, minPanelWidth, SidePanel } from "./side-panel-layout" @@ -1793,101 +1791,16 @@ const AgentManagerContent: Component = () => { } const setupScript = metrics.click("configure_setup_script", "worktree_settings", handleConfigureSetupScript) - const handleChangeDefaultBaseBranch = () => { - const [search, setSearch] = createSignal("") - const [branches, setBranches] = createSignal([]) - const [loading, setLoading] = createSignal(true) - const [highlighted, setHighlighted] = createSignal(-1) - - const unsub = vscode.onMessage((msg) => { - if (msg.type === "agentManager.branches") { - const ev = msg as AgentManagerBranchesMessage - setBranches(ev.branches) - if (ev.defaultBranch) setRepoDetectedBranch(ev.defaultBranch) - setLoading(false) - } - }) - - vscode.postMessage({ type: "agentManager.requestBranches" }) - - const filtered = createMemo(() => { - const s = search().toLowerCase() - if (!s) return branches() - return branches().filter((b) => b.name.toLowerCase().includes(s)) - }) - - const selectBranch = (name: string | undefined) => { - vscode.postMessage({ type: "agentManager.setDefaultBaseBranch", branch: name }) - setDefaultBaseBranch(name) - dialog.close() - } - - const handleKeyDown = (e: KeyboardEvent) => { - const items = filtered() - // offset by 1 for auto-detect option (-1 = auto-detect) - const total = items.length + 1 - if (e.key === "ArrowDown") { - e.preventDefault() - e.stopPropagation() - setHighlighted((prev) => Math.min(prev + 1, total - 2)) - } else if (e.key === "ArrowUp") { - e.preventDefault() - e.stopPropagation() - setHighlighted((prev) => Math.max(prev - 1, -1)) - } else if (e.key === "Enter") { - e.preventDefault() - e.stopPropagation() - const idx = highlighted() - if (idx === -1) { - selectBranch(undefined) - } else { - const branch = items[idx] - if (branch) selectBranch(branch.name) - } - } else if (e.key === "Escape") { - e.preventDefault() - e.stopPropagation() - dialog.close() - } - } - - dialog.show(() => { - onCleanup(unsub) - return ( - -
- { - setSearch(v) - setHighlighted(-1) - }} - onSelect={(b) => selectBranch(b.name)} - onSearchKeyDown={handleKeyDown} - selected={defaultBaseBranch()} - highlighted={highlighted()} - onHighlight={setHighlighted} - searchPlaceholder={t("agentManager.dialog.searchBranches")} - emptyLabel={t("agentManager.import.noMatchingBranches")} - loadingLabel={t("agentManager.import.loadingBranches")} - defaultLabel={t("agentManager.dialog.branchBadge.default")} - remoteLabel={t("agentManager.dialog.branchBadge.remote")} - defaultName={defaultBaseBranch()} - autoOption={{ - label: t("agentManager.worktree.defaultBaseBranchAuto"), - hint: repoDetectedBranch(), - active: !hasConfiguredBranch(), - highlighted: highlighted() === -1, - onSelect: () => selectBranch(undefined), - }} - /> -
-
- ) - }) - } + const handleChangeDefaultBaseBranch = createChangeDefaultBaseBranch({ + vscode, + dialog, + t, + defaultBaseBranch, + setDefaultBaseBranch, + setRepoDetectedBranch, + repoDetectedBranch, + hasConfiguredBranch, + }) const handleShowKeyboardShortcuts = () => { const categories = buildShortcutCategories(kb(), t) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/default-base-branch.tsx b/packages/kilo-vscode/webview-ui/agent-manager/default-base-branch.tsx new file mode 100644 index 00000000000..53c9b77443d --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/default-base-branch.tsx @@ -0,0 +1,121 @@ +/** @jsxImportSource solid-js */ + +import { createSignal, createMemo, onCleanup } from "solid-js" +import type { AgentManagerBranchesMessage, BranchInfo } from "../src/types/messages" +import { Dialog } from "@kilocode/kilo-ui/dialog" +import { useDialog } from "@kilocode/kilo-ui/context/dialog" +import { BranchSelect } from "../src/components/shared/BranchSelect" +import type { useVSCode } from "../src/context/vscode" +import type { useLanguage } from "../src/context/language" +import type { Accessor, Setter } from "solid-js" + +interface Deps { + vscode: ReturnType + dialog: ReturnType + t: ReturnType["t"] + defaultBaseBranch: Accessor + setDefaultBaseBranch: (v: Parameters>[0]) => void + setRepoDetectedBranch: Setter + repoDetectedBranch: Accessor + hasConfiguredBranch: Accessor +} + +export function createChangeDefaultBaseBranch(deps: Deps) { + const { vscode, dialog, t, defaultBaseBranch, setDefaultBaseBranch, setRepoDetectedBranch, repoDetectedBranch, hasConfiguredBranch } = deps + + return () => { + const [search, setSearch] = createSignal("") + const [branches, setBranches] = createSignal([]) + const [loading, setLoading] = createSignal(true) + const [highlighted, setHighlighted] = createSignal(-1) + + const unsub = vscode.onMessage((msg) => { + if (msg.type === "agentManager.branches") { + const ev = msg as AgentManagerBranchesMessage + setBranches(ev.branches) + if (ev.defaultBranch) setRepoDetectedBranch(ev.defaultBranch) + setLoading(false) + } + }) + + vscode.postMessage({ type: "agentManager.requestBranches" }) + + const filtered = createMemo(() => { + const s = search().toLowerCase() + if (!s) return branches() + return branches().filter((b) => b.name.toLowerCase().includes(s)) + }) + + const selectBranch = (name: string | undefined) => { + vscode.postMessage({ type: "agentManager.setDefaultBaseBranch", branch: name }) + setDefaultBaseBranch(name) + dialog.close() + } + + const handleKeyDown = (e: KeyboardEvent) => { + const items = filtered() + // offset by 1 for auto-detect option (-1 = auto-detect) + const total = items.length + 1 + if (e.key === "ArrowDown") { + e.preventDefault() + e.stopPropagation() + setHighlighted((prev) => Math.min(prev + 1, total - 2)) + } else if (e.key === "ArrowUp") { + e.preventDefault() + e.stopPropagation() + setHighlighted((prev) => Math.max(prev - 1, -1)) + } else if (e.key === "Enter") { + e.preventDefault() + e.stopPropagation() + const idx = highlighted() + if (idx === -1) { + selectBranch(undefined) + } else { + const branch = items[idx] + if (branch) selectBranch(branch.name) + } + } else if (e.key === "Escape") { + e.preventDefault() + e.stopPropagation() + dialog.close() + } + } + + dialog.show(() => { + onCleanup(unsub) + return ( + +
+ { + setSearch(v) + setHighlighted(-1) + }} + onSelect={(b) => selectBranch(b.name)} + onSearchKeyDown={handleKeyDown} + selected={defaultBaseBranch()} + highlighted={highlighted()} + onHighlight={setHighlighted} + searchPlaceholder={t("agentManager.dialog.searchBranches")} + emptyLabel={t("agentManager.import.noMatchingBranches")} + loadingLabel={t("agentManager.import.loadingBranches")} + defaultLabel={t("agentManager.dialog.branchBadge.default")} + remoteLabel={t("agentManager.dialog.branchBadge.remote")} + defaultName={defaultBaseBranch()} + autoOption={{ + label: t("agentManager.worktree.defaultBaseBranchAuto"), + hint: repoDetectedBranch(), + active: !hasConfiguredBranch(), + highlighted: highlighted() === -1, + onSelect: () => selectBranch(undefined), + }} + /> +
+
+ ) + }) + } +} diff --git a/packages/kilo-vscode/webview-ui/src/context/session-merge.ts b/packages/kilo-vscode/webview-ui/src/context/session-merge.ts new file mode 100644 index 00000000000..ac9264cf238 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/context/session-merge.ts @@ -0,0 +1,41 @@ +import type { Message, MessageLoadMode, Part } from "../types/messages" +import { sameParts } from "./session-parts" + +export function mergeMessages(current: Message[], incoming: Message[], mode: Exclude) { + if (mode === "reconcile") { + // Tail reconcile: incoming is the authoritative newest-N snapshot. + // Local state may already hold some of those IDs and may also hold + // newer optimistic entries created after the fetch was taken. Merge + // by id (server wins on collision) then sort by createdAt so new + // server messages land in the right position and optimistic tail + // entries stay at the end. + const byId = new Map() + for (const msg of current) byId.set(msg.id, msg) + for (const msg of incoming) byId.set(msg.id, msg) + return [...byId.values()].sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()) + } + const seen = new Set() + const source = mode === "prepend" ? [...incoming, ...current] : incoming + return source.filter((msg) => { + if (seen.has(msg.id)) return false + seen.add(msg.id) + return true + }) +} + +// Cheap tail check: same ids in the same order and no visible streamed-part +// correction to apply. It skips store churn when SSE already matches the +// snapshot, but lets reconcile heal part removals and finalized text. +export function sameReconcileShape( + current: Message[], + incoming: Message[], + getParts: (messageID: string) => Part[] | undefined, +): boolean { + if (current.length !== incoming.length) return false + for (const [i, n] of incoming.entries()) { + const c = current[i]! + if (c.id !== n.id) return false + if (!sameParts(getParts(c.id) ?? c.parts, n.parts)) return false + } + return true +} diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx index 839b0dc3f50..302d1c8727f 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -72,7 +72,8 @@ import { getAgentModel } from "./session-model-store" import { resolveMessagePrefs } from "./session-preferences" import { errorIDs } from "./session-errors" import { PartStash } from "./part-stash" -import { mergeParts, sameParts } from "./session-parts" +import { mergeParts } from "./session-parts" +import { mergeMessages, sameReconcileShape } from "./session-merge" import { state as todoState } from "./todo-revert" import { getAgentVariant, @@ -1423,28 +1424,6 @@ export const SessionProvider: ParentComponent = (props) => { setPages(sessionID, { ...(pages[sessionID] ?? emptyPageState), ...patch }) } - function mergeMessages(current: Message[], incoming: Message[], mode: Exclude) { - if (mode === "reconcile") { - // Tail reconcile: incoming is the authoritative newest-N snapshot. - // Local state may already hold some of those IDs and may also hold - // newer optimistic entries created after the fetch was taken. Merge - // by id (server wins on collision) then sort by createdAt so new - // server messages land in the right position and optimistic tail - // entries stay at the end. - const byId = new Map() - for (const msg of current) byId.set(msg.id, msg) - for (const msg of incoming) byId.set(msg.id, msg) - return [...byId.values()].sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()) - } - const seen = new Set() - const source = mode === "prepend" ? [...incoming, ...current] : incoming - return source.filter((msg) => { - if (seen.has(msg.id)) return false - seen.add(msg.id) - return true - }) - } - function recoverPrefs(sessionID: string, messages: Message[], names = agentNames()) { const prefs = resolveMessagePrefs(messages, names) if (prefs.agent && !store.agentSelections[sessionID]) { @@ -1469,19 +1448,6 @@ export const SessionProvider: ParentComponent = (props) => { return [...messages, ...orphans] } - // Cheap tail check: same ids in the same order and no visible streamed-part - // correction to apply. It skips store churn when SSE already matches the - // snapshot, but lets reconcile heal part removals and finalized text. - function sameReconcileShape(current: Message[], incoming: Message[]): boolean { - if (current.length !== incoming.length) return false - for (const [i, n] of incoming.entries()) { - const c = current[i]! - if (c.id !== n.id) return false - if (!sameParts(store.parts[c.id] ?? c.parts, n.parts)) return false - } - return true - } - function setTools(sessionID: string, tools: ToolPart[]) { setStore("toolParts", sessionID, reconcileSessionToolParts(tools)) } @@ -1530,7 +1496,7 @@ export const SessionProvider: ParentComponent = (props) => { // Reconcile fast-path: if the tail matches local state shape-wise, every // message+part-count already agrees with the server. Skip the reactive // store churn entirely — virtualizer and rendering stay untouched. - if (mode === "reconcile" && sameReconcileShape(store.messages[sessionID] ?? [], messages)) { + if (mode === "reconcile" && sameReconcileShape(store.messages[sessionID] ?? [], messages, (id) => store.parts[id])) { const parts = messageParts(messages) for (const msg of messages) { if (store.parts[msg.id]) delete parts[msg.id] From 0979a576e2d78d4cc2990adb2e1af42d04a04c9c Mon Sep 17 00:00:00 2001 From: cmanu Date: Thu, 6 Aug 2026 14:53:24 -0700 Subject: [PATCH 20/73] feat(pr-view): organize types --- .../src/agent-manager/PRStatusPoller.ts | 2 +- .../src/agent-manager/am-pr-types.ts | 21 +++++++++++++++ .../src/agent-manager/am-pr-utils.ts | 27 +++---------------- 3 files changed, 26 insertions(+), 24 deletions(-) create mode 100644 packages/kilo-vscode/src/agent-manager/am-pr-types.ts diff --git a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts index 077552ccf38..6ce5e6b748d 100644 --- a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts +++ b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts @@ -6,7 +6,7 @@ import { execGhRead } from "./gh" import { classifyPRError } from "./git-import" import type { Semaphore } from "./semaphore" import { parsePRResult, checkStatus, formatCheckDuration, parseComments, parseReviewers } from "./am-pr-utils" -import type { PRResult, GhThread, GhReviewRequest, GhReview } from "./am-pr-utils" +import type { PRResult, GhThread, GhReviewRequest, GhReview } from "./am-pr-types" interface PRStatusPollerOptions { getWorktrees: () => Worktree[] diff --git a/packages/kilo-vscode/src/agent-manager/am-pr-types.ts b/packages/kilo-vscode/src/agent-manager/am-pr-types.ts new file mode 100644 index 00000000000..59f4604c96b --- /dev/null +++ b/packages/kilo-vscode/src/agent-manager/am-pr-types.ts @@ -0,0 +1,21 @@ +import type { PRState, ReviewDecision } from "./types" + +// Raw shapes returned by `gh pr view --json` + +export interface GhAuthor { login?: string; avatarUrl?: string } +export interface GhComment { id: string; author?: GhAuthor; body?: string; path?: string; line?: number; url?: string; createdAt?: string } +export interface GhThread { isResolved?: boolean; comments?: { nodes?: GhComment[] } } +export interface GhReviewRequest { requestedReviewer?: GhAuthor } +export interface GhReview { author?: GhAuthor; state?: string } + +export interface PRResult { + number: number + title: string + body: string + url: string + state: PRState + review: ReviewDecision | null + additions: number + deletions: number + files: number +} diff --git a/packages/kilo-vscode/src/agent-manager/am-pr-utils.ts b/packages/kilo-vscode/src/agent-manager/am-pr-utils.ts index 976b486db80..8a45349bf61 100644 --- a/packages/kilo-vscode/src/agent-manager/am-pr-utils.ts +++ b/packages/kilo-vscode/src/agent-manager/am-pr-utils.ts @@ -1,31 +1,12 @@ -import type { PRState, ReviewDecision, CheckStatus, PRComment, PRReviewer } from "./types" - -// Raw shapes returned by `gh pr view --json` - -export interface GhAuthor { login?: string; avatarUrl?: string } -export interface GhComment { id: string; author?: GhAuthor; body?: string; path?: string; line?: number; url?: string; createdAt?: string } -export interface GhThread { isResolved?: boolean; comments?: { nodes?: GhComment[] } } -export interface GhReviewRequest { requestedReviewer?: GhAuthor } -export interface GhReview { author?: GhAuthor; state?: string } - -export interface PRResult { - number: number - title: string - body: string - url: string - state: PRState - review: ReviewDecision | null - additions: number - deletions: number - files: number -} +import type { CheckStatus, PRComment, PRReviewer } from "./types" +import type { PRResult, GhThread, GhReviewRequest, GhReview } from "./am-pr-types" export function parsePRResult(json: string): PRResult | null { const data = JSON.parse(json) if (!data.number) return null - const state: PRState = data.isDraft ? "draft" : (data.state?.toLowerCase() as PRState) ?? "open" + const state = data.isDraft ? "draft" : (data.state?.toLowerCase() ?? "open") const decision = data.reviewDecision as string | undefined - const review: ReviewDecision | null = + const review = decision === "APPROVED" ? "approved" : decision === "CHANGES_REQUESTED" ? "changes_requested" : decision === "REVIEW_REQUIRED" ? "pending" From 0c22c49d438841917f8a1829970f9e3c18338c5a Mon Sep 17 00:00:00 2001 From: cmanu Date: Thu, 6 Aug 2026 15:21:12 -0700 Subject: [PATCH 21/73] feat(pr-view): add unit tests for status bridge and pr utils --- .../tests/unit/am-pr-status-bridge.test.ts | 186 +++++++++++++++ .../tests/unit/am-pr-utils.test.ts | 214 ++++++++++++++++++ 2 files changed, 400 insertions(+) create mode 100644 packages/kilo-vscode/tests/unit/am-pr-status-bridge.test.ts create mode 100644 packages/kilo-vscode/tests/unit/am-pr-utils.test.ts diff --git a/packages/kilo-vscode/tests/unit/am-pr-status-bridge.test.ts b/packages/kilo-vscode/tests/unit/am-pr-status-bridge.test.ts new file mode 100644 index 00000000000..4752844023e --- /dev/null +++ b/packages/kilo-vscode/tests/unit/am-pr-status-bridge.test.ts @@ -0,0 +1,186 @@ +import { describe, expect, it } from "bun:test" +import { PRStatusBridge } from "../../src/agent-manager/pr-status-bridge" +import type { AgentManagerOutMessage, PRStatus } from "../../src/agent-manager/types" + +const pr: PRStatus = { + number: 1, + title: "my PR", + url: "https://github.com/x/y/pull/1", + state: "open", + review: null, + checks: { status: "none", total: 0, passed: 0, failed: 0, pending: 0, checks: [] }, + additions: 0, + deletions: 0, + files: 0, +} + +function harness(opts: { hasPersisted?: boolean } = {}) { + const sent: AgentManagerOutMessage[] = [] + const worktrees: { id: string; prUrl?: string }[] = [] + const bridge = PRStatusBridge.create({ + getWorktrees: () => worktrees as never, + getWorkspaceRoot: () => "/repo", + postToWebview: (msg) => sent.push(msg), + updateWorktreePR: () => {}, + hasPersistedPR: () => opts.hasPersisted ?? false, + openExternal: () => {}, + log: () => {}, + }) + const onStatus = (bridge.poller as unknown as { options: { onStatus: (...a: unknown[]) => void } }).options.onStatus + return { bridge, sent, onStatus } +} + +// --- error deduplication --- + +describe("PRStatusBridge.notifyError", () => { + it("sends the first error notification", () => { + const { bridge, sent } = harness() + bridge.notifyError("gh_missing") + expect(sent).toHaveLength(1) + expect(sent[0]).toEqual(expect.objectContaining({ type: "agentManager.prError", error: "gh_missing" })) + }) + + it("deduplicates the same error type", () => { + const { bridge, sent } = harness() + bridge.notifyError("gh_auth") + bridge.notifyError("gh_auth") + expect(sent).toHaveLength(1) + }) + + it("sends again when error type changes", () => { + const { bridge, sent } = harness() + bridge.notifyError("gh_missing") + bridge.notifyError("gh_auth") + expect(sent).toHaveLength(2) + }) +}) + +// --- onStatus cache suppression --- + +describe("PRStatusBridge onStatus", () => { + it("forwards a successful status to the webview", () => { + const { sent, onStatus } = harness() + onStatus("wt1", pr) + expect(sent).toHaveLength(1) + expect(sent[0]).toEqual(expect.objectContaining({ type: "agentManager.prStatus", worktreeId: "wt1", pr })) + }) + + it("forwards pr:null error when no cache entry and no persisted PR", () => { + const { sent, onStatus } = harness() + onStatus("wt1", null, "fetch_failed") + expect(sent).toHaveLength(1) + expect(sent[0]).toEqual(expect.objectContaining({ type: "agentManager.prStatus", worktreeId: "wt1", pr: null })) + }) + + it("suppresses pr:null error when cache entry exists", () => { + const { sent, onStatus } = harness() + onStatus("wt1", pr) + sent.length = 0 + onStatus("wt1", null, "fetch_failed") + expect(sent).toHaveLength(0) + }) + + it("suppresses pr:null error when persisted PR exists", () => { + const { sent, onStatus } = harness({ hasPersisted: true }) + onStatus("wt1", null, "fetch_failed") + expect(sent).toHaveLength(0) + }) + + it("forwards gh_auth error even when cache entry exists", () => { + const { sent, onStatus } = harness() + onStatus("wt1", pr) + sent.length = 0 + onStatus("wt1", null, "gh_auth") + const errorMsg = sent.find((m) => m.type === "agentManager.prError") + expect(errorMsg).toEqual(expect.objectContaining({ error: "gh_auth" })) + }) + + it("forwards gh_missing error even when cache entry exists", () => { + const { sent, onStatus } = harness() + onStatus("wt1", pr) + sent.length = 0 + onStatus("wt1", null, "gh_missing") + const errorMsg = sent.find((m) => m.type === "agentManager.prError") + expect(errorMsg).toEqual(expect.objectContaining({ error: "gh_missing" })) + }) +}) + +// --- replay --- + +describe("PRStatusBridge.replay", () => { + it("replays cached status messages", () => { + const { bridge, sent, onStatus } = harness() + onStatus("wt1", pr) + sent.length = 0 + bridge.replay() + expect(sent).toHaveLength(1) + expect(sent[0]).toEqual(expect.objectContaining({ type: "agentManager.prStatus", worktreeId: "wt1" })) + }) + + it("replays the last auth error on reconnect", () => { + const { bridge, sent, onStatus } = harness() + onStatus("wt1", null, "gh_auth") + sent.length = 0 + bridge.replay() + expect(sent.some((m) => m.type === "agentManager.prError" && (m as never as { error: string }).error === "gh_auth")).toBe(true) + }) + + it("does not replay fetch_failed errors", () => { + const { bridge, sent, onStatus } = harness() + onStatus("wt1", null, "fetch_failed") + sent.length = 0 + bridge.replay() + expect(sent).toHaveLength(0) + }) +}) + +// --- snapshot --- + +describe("PRStatusBridge.snapshot", () => { + it("returns only entries with a non-null pr", () => { + const { bridge, onStatus } = harness() + onStatus("wt1", pr) + onStatus("wt2", pr) + expect(bridge.snapshot().size).toBe(2) + }) + + it("excludes entries where pr was null", () => { + const { bridge, onStatus } = harness({ hasPersisted: true }) + onStatus("wt1", null, "fetch_failed") + expect(bridge.snapshot().size).toBe(0) + }) +}) + +// --- remove / reset --- + +describe("PRStatusBridge.remove", () => { + it("removes a cached entry so it is no longer replayed", () => { + const { bridge, sent, onStatus } = harness() + onStatus("wt1", pr) + bridge.remove("wt1") + sent.length = 0 + bridge.replay() + expect(sent).toHaveLength(0) + }) +}) + +describe("PRStatusBridge.reset", () => { + it("clears cache and error state so replay sends nothing", () => { + const { bridge, sent, onStatus } = harness() + onStatus("wt1", pr) + bridge.notifyError("gh_auth") + bridge.reset() + sent.length = 0 + bridge.replay() + expect(sent).toHaveLength(0) + }) + + it("allows the same error to be sent again after reset", () => { + const { bridge, sent } = harness() + bridge.notifyError("gh_auth") + bridge.reset() + sent.length = 0 + bridge.notifyError("gh_auth") + expect(sent).toHaveLength(1) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/am-pr-utils.test.ts b/packages/kilo-vscode/tests/unit/am-pr-utils.test.ts new file mode 100644 index 00000000000..e16ab9c259d --- /dev/null +++ b/packages/kilo-vscode/tests/unit/am-pr-utils.test.ts @@ -0,0 +1,214 @@ +import { describe, expect, it } from "bun:test" +import { + parsePRResult, + checkStatus, + formatCheckDuration, + parseComments, + parseReviewers, +} from "../../src/agent-manager/am-pr-utils" +import type { GhThread, GhReviewRequest, GhReview } from "../../src/agent-manager/am-pr-types" + +// --- parsePRResult --- + +describe("parsePRResult", () => { + it("returns null when number is missing", () => { + expect(parsePRResult(JSON.stringify({ title: "foo" }))).toBeNull() + }) + + it("parses an open PR", () => { + const raw = { number: 42, title: "my PR", body: "desc", url: "https://github.com/x/y/pull/42", state: "OPEN", isDraft: false, reviewDecision: null, additions: 10, deletions: 3, changedFiles: 2 } + expect(parsePRResult(JSON.stringify(raw))).toEqual({ + number: 42, + title: "my PR", + body: "desc", + url: "https://github.com/x/y/pull/42", + state: "open", + review: null, + additions: 10, + deletions: 3, + files: 2, + }) + }) + + it("maps isDraft to draft state regardless of gh state field", () => { + const raw = { number: 1, title: "", body: "", url: "", state: "OPEN", isDraft: true, reviewDecision: null, additions: 0, deletions: 0, changedFiles: 0 } + expect(parsePRResult(JSON.stringify(raw))?.state).toBe("draft") + }) + + it("maps MERGED state", () => { + const raw = { number: 1, title: "", body: "", url: "", state: "MERGED", isDraft: false, reviewDecision: null, additions: 0, deletions: 0, changedFiles: 0 } + expect(parsePRResult(JSON.stringify(raw))?.state).toBe("merged") + }) + + it("maps CLOSED state", () => { + const raw = { number: 1, title: "", body: "", url: "", state: "CLOSED", isDraft: false, reviewDecision: null, additions: 0, deletions: 0, changedFiles: 0 } + expect(parsePRResult(JSON.stringify(raw))?.state).toBe("closed") + }) + + it("maps APPROVED review decision", () => { + const raw = { number: 1, title: "", body: "", url: "", state: "OPEN", isDraft: false, reviewDecision: "APPROVED", additions: 0, deletions: 0, changedFiles: 0 } + expect(parsePRResult(JSON.stringify(raw))?.review).toBe("approved") + }) + + it("maps CHANGES_REQUESTED review decision", () => { + const raw = { number: 1, title: "", body: "", url: "", state: "OPEN", isDraft: false, reviewDecision: "CHANGES_REQUESTED", additions: 0, deletions: 0, changedFiles: 0 } + expect(parsePRResult(JSON.stringify(raw))?.review).toBe("changes_requested") + }) + + it("maps REVIEW_REQUIRED review decision to pending", () => { + const raw = { number: 1, title: "", body: "", url: "", state: "OPEN", isDraft: false, reviewDecision: "REVIEW_REQUIRED", additions: 0, deletions: 0, changedFiles: 0 } + expect(parsePRResult(JSON.stringify(raw))?.review).toBe("pending") + }) + + it("returns null review for unknown decision", () => { + const raw = { number: 1, title: "", body: "", url: "", state: "OPEN", isDraft: false, reviewDecision: "SOMETHING_ELSE", additions: 0, deletions: 0, changedFiles: 0 } + expect(parsePRResult(JSON.stringify(raw))?.review).toBeNull() + }) + + it("defaults missing fields to empty strings and zeros", () => { + const result = parsePRResult(JSON.stringify({ number: 5 })) + expect(result).toEqual(expect.objectContaining({ title: "", body: "", url: "", additions: 0, deletions: 0, files: 0 })) + }) +}) + +// --- checkStatus --- + +describe("checkStatus", () => { + it("maps SUCCESS", () => expect(checkStatus("SUCCESS")).toBe("success")) + it("maps FAILURE", () => expect(checkStatus("FAILURE")).toBe("failure")) + it("maps ERROR to failure", () => expect(checkStatus("ERROR")).toBe("failure")) + it("maps PENDING", () => expect(checkStatus("PENDING")).toBe("pending")) + it("maps QUEUED to pending", () => expect(checkStatus("QUEUED")).toBe("pending")) + it("maps IN_PROGRESS to pending", () => expect(checkStatus("IN_PROGRESS")).toBe("pending")) + it("maps REQUESTED to pending", () => expect(checkStatus("REQUESTED")).toBe("pending")) + it("maps WAITING to pending", () => expect(checkStatus("WAITING")).toBe("pending")) + it("maps SKIPPED", () => expect(checkStatus("SKIPPED")).toBe("skipped")) + it("maps CANCELLED", () => expect(checkStatus("CANCELLED")).toBe("cancelled")) + it("maps TIMED_OUT to cancelled", () => expect(checkStatus("TIMED_OUT")).toBe("cancelled")) + it("maps STALE to cancelled", () => expect(checkStatus("STALE")).toBe("cancelled")) + it("maps STARTUP_FAILURE to cancelled", () => expect(checkStatus("STARTUP_FAILURE")).toBe("cancelled")) + it("maps unknown state to pending", () => expect(checkStatus("WHATEVER")).toBe("pending")) + it("is case-insensitive", () => expect(checkStatus("success")).toBe("success")) +}) + +// --- formatCheckDuration --- + +describe("formatCheckDuration", () => { + it("returns undefined when startedAt is missing", () => { + expect(formatCheckDuration(undefined, "2024-01-01T00:01:00Z")).toBeUndefined() + }) + + it("returns undefined when completedAt is missing", () => { + expect(formatCheckDuration("2024-01-01T00:00:00Z", undefined)).toBeUndefined() + }) + + it("formats sub-minute durations in seconds", () => { + expect(formatCheckDuration("2024-01-01T00:00:00Z", "2024-01-01T00:00:45Z")).toBe("45s") + }) + + it("formats durations over a minute as m/s", () => { + expect(formatCheckDuration("2024-01-01T00:00:00Z", "2024-01-01T00:02:30Z")).toBe("2m 30s") + }) + + it("formats exactly 60 seconds as 1m 0s", () => { + expect(formatCheckDuration("2024-01-01T00:00:00Z", "2024-01-01T00:01:00Z")).toBe("1m 0s") + }) +}) + +// --- parseComments --- + +describe("parseComments", () => { + it("returns empty array for empty threads", () => { + expect(parseComments([])).toEqual([]) + }) + + it("skips threads with no comments", () => { + const threads: GhThread[] = [{ isResolved: false, comments: { nodes: [] } }] + expect(parseComments(threads)).toHaveLength(0) + }) + + it("parses a resolved thread", () => { + const threads: GhThread[] = [{ + isResolved: true, + comments: { nodes: [{ id: "c1", author: { login: "alice", avatarUrl: "https://avatar" }, body: "looks good", path: "src/foo.ts", line: 10, url: "https://url", createdAt: "2024-01-01T00:00:00Z" }] }, + }] + expect(parseComments(threads)).toEqual([{ + id: "c1", + author: "alice", + avatar: "https://avatar", + body: "looks good", + file: "src/foo.ts", + line: 10, + url: "https://url", + resolved: true, + createdAt: new Date("2024-01-01T00:00:00Z").getTime(), + }]) + }) + + it("defaults missing author to 'unknown'", () => { + const threads: GhThread[] = [{ isResolved: false, comments: { nodes: [{ id: "c2", body: "note" }] } }] + expect(parseComments(threads)[0]?.author).toBe("unknown") + }) + + it("only uses the first comment of each thread", () => { + const threads: GhThread[] = [{ + isResolved: false, + comments: { nodes: [ + { id: "first", body: "first comment" }, + { id: "second", body: "second comment" }, + ]}, + }] + const result = parseComments(threads) + expect(result).toHaveLength(1) + expect(result[0]?.id).toBe("first") + }) +}) + +// --- parseReviewers --- + +describe("parseReviewers", () => { + it("returns empty array with no requests or reviews", () => { + expect(parseReviewers([], [])).toEqual([]) + }) + + it("adds pending reviewer from request", () => { + const requests: GhReviewRequest[] = [{ requestedReviewer: { login: "alice", avatarUrl: "https://avatar" } }] + expect(parseReviewers(requests, [])).toEqual([{ login: "alice", avatar: "https://avatar", state: "pending" }]) + }) + + it("skips review requests without a login", () => { + const requests: GhReviewRequest[] = [{ requestedReviewer: {} }] + expect(parseReviewers(requests, [])).toHaveLength(0) + }) + + it("adds reviewer from review when not in requests", () => { + const reviews: GhReview[] = [{ author: { login: "bob" }, state: "APPROVED" }] + expect(parseReviewers([], reviews)).toEqual([{ login: "bob", avatar: undefined, state: "approved" }]) + }) + + it("upgrades pending request to approved when review arrives", () => { + const requests: GhReviewRequest[] = [{ requestedReviewer: { login: "alice" } }] + const reviews: GhReview[] = [{ author: { login: "alice" }, state: "APPROVED" }] + expect(parseReviewers(requests, reviews)).toEqual([{ login: "alice", avatar: undefined, state: "approved" }]) + }) + + it("does not downgrade approved to commented", () => { + const requests: GhReviewRequest[] = [{ requestedReviewer: { login: "alice" } }] + const reviews: GhReview[] = [ + { author: { login: "alice" }, state: "APPROVED" }, + { author: { login: "alice" }, state: "COMMENTED" }, + ] + expect(parseReviewers(requests, reviews)[0]?.state).toBe("approved") + }) + + it("does upgrade pending to changes_requested", () => { + const requests: GhReviewRequest[] = [{ requestedReviewer: { login: "alice" } }] + const reviews: GhReview[] = [{ author: { login: "alice" }, state: "CHANGES_REQUESTED" }] + expect(parseReviewers(requests, reviews)[0]?.state).toBe("changes_requested") + }) + + it("skips reviews without a login", () => { + const reviews: GhReview[] = [{ author: {}, state: "APPROVED" }] + expect(parseReviewers([], reviews)).toHaveLength(0) + }) +}) From d70ebd319174b795412d1afcec97b32113801fc0 Mon Sep 17 00:00:00 2001 From: Christiaan Arnoldus Date: Fri, 7 Aug 2026 09:47:38 +0200 Subject: [PATCH 22/73] fix(cli): prefer configured custom variants --- packages/opencode/src/kilocode/provider/provider.ts | 4 +++- packages/opencode/src/provider/provider.ts | 8 +++----- .../test/kilocode/custom-provider-variants.test.ts | 5 ++--- .../test/kilocode/provider-reasoning-options.test.ts | 12 ++++++++++++ 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/kilocode/provider/provider.ts b/packages/opencode/src/kilocode/provider/provider.ts index 13ec8bdddd2..21345881ee9 100644 --- a/packages/opencode/src/kilocode/provider/provider.ts +++ b/packages/opencode/src/kilocode/provider/provider.ts @@ -103,8 +103,10 @@ type Variants = NonNullable type Generate = (model: Provider.Model) => Variants export function customProviderVariants(model: Provider.Model, npm: unknown, generate: Generate): Variants { + if (model.variants && Object.keys(model.variants).length > 0) return model.variants + const supported = typeof npm === "string" && CUSTOM_PROVIDER_PACKAGES.has(npm) && model.api.npm === npm - const variants = generate(supported ? { ...model, variants: {} } : model) + const variants = generate(model) if (Object.keys(variants).length > 0) return variants if (!model.capabilities.reasoning || !supported) return variants diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index e8f134f2871..c830ad8bb78 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -1528,11 +1528,9 @@ const layer = Layer.effect( ...patchKiloConfigModel(model, existingModel), // kilocode_change } // kilocode_change start - const generated = customProviderVariants( - parsedModel, - model.provider?.npm ?? provider.npm, - ProviderTransform.variants, - ) + const generated = Object.keys(model.variants ?? {}).length + ? {} + : customProviderVariants(parsedModel, model.provider?.npm ?? provider.npm, ProviderTransform.variants) const merged = mergeDeep(generated, model.variants ?? {}) // kilocode_change end parsedModel.variants = mapValues( diff --git a/packages/opencode/test/kilocode/custom-provider-variants.test.ts b/packages/opencode/test/kilocode/custom-provider-variants.test.ts index e3a6e6be179..7de3036fd7a 100644 --- a/packages/opencode/test/kilocode/custom-provider-variants.test.ts +++ b/packages/opencode/test/kilocode/custom-provider-variants.test.ts @@ -10,14 +10,13 @@ import { testEffect } from "../lib/effect" const it = testEffect(LayerNode.compile(LayerNode.group([Provider.node, Env.node, Plugin.node]))) it.instance( - "adds fallback efforts to configured reasoning models and merges explicit variants", + "uses configured variants instead of inferred reasoning efforts", () => Effect.gen(function* () { const providers = yield* Provider.use.list() const model = providers[ProviderV2.ID.make("custom")]?.models["qwen-custom"] - expect(Object.keys(model?.variants ?? {})).toEqual(["none", "low", "medium", "xhigh", "max", "custom"]) - expect(model?.variants?.none).toEqual({ reasoningEffort: "none" }) + expect(Object.keys(model?.variants ?? {})).toEqual(["custom"]) expect(model?.variants?.high).toBeUndefined() expect(model?.variants?.custom).toEqual({ reasoningEffort: "custom" }) }), diff --git a/packages/opencode/test/kilocode/provider-reasoning-options.test.ts b/packages/opencode/test/kilocode/provider-reasoning-options.test.ts index 268d27f4399..5987819421d 100644 --- a/packages/opencode/test/kilocode/provider-reasoning-options.test.ts +++ b/packages/opencode/test/kilocode/provider-reasoning-options.test.ts @@ -168,6 +168,18 @@ describe("custom provider fallback reasoning efforts", () => { expect(customProviderVariants(model, model.api.npm, () => generated)).toBe(generated) }) + test("prefers configured variants to inference", () => { + const variants = { custom: { reasoningEffort: "custom" } } + for (const npm of ["@ai-sdk/openai-compatible", "@ai-sdk/openai", "@ai-sdk/anthropic"]) { + const model = mockModel({ api: { id: "custom", url: "https://api.test.com", npm }, variants }) + expect( + customProviderVariants(model, npm, () => { + throw new Error("inference should not run") + }), + ).toBe(variants) + } + }) + test("requires a reasoning model with an explicitly configured supported package", () => { const npm = "@ai-sdk/openai-compatible" const plain = mockModel({ From ed63f985d92f36f7e196d42136103875b71339c0 Mon Sep 17 00:00:00 2001 From: Christiaan Arnoldus Date: Fri, 7 Aug 2026 09:58:35 +0200 Subject: [PATCH 23/73] feat(vscode): link custom providers to config --- .changeset/broad-custom-provider-efforts.md | 2 +- .../tests/unit/open-config-message.test.ts | 14 ++++++++ .../settings/CustomProviderDialog.tsx | 14 ++++++++ .../src/components/settings/Settings.tsx | 30 ++--------------- .../kilo-vscode/webview-ui/src/i18n/ar.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/br.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/bs.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/da.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/de.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/en.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/es.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/fa.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/fr.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/it.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/ja.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/ko.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/nl.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/no.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/pl.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/ru.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/th.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/tr.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/uk.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/zh.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/zht.ts | 1 + .../webview-ui/src/utils/open-config.ts | 32 +++++++++++++++++++ 26 files changed, 84 insertions(+), 29 deletions(-) create mode 100644 packages/kilo-vscode/tests/unit/open-config-message.test.ts create mode 100644 packages/kilo-vscode/webview-ui/src/utils/open-config.ts diff --git a/.changeset/broad-custom-provider-efforts.md b/.changeset/broad-custom-provider-efforts.md index 2b275555aa1..c0746956176 100644 --- a/.changeset/broad-custom-provider-efforts.md +++ b/.changeset/broad-custom-provider-efforts.md @@ -3,4 +3,4 @@ "kilo-code": patch --- -Automatically expose broad reasoning effort options for custom provider models instead of requiring manual variant configuration. +Automatically expose broad reasoning effort options for custom provider models and link saved providers to advanced JSON configuration. diff --git a/packages/kilo-vscode/tests/unit/open-config-message.test.ts b/packages/kilo-vscode/tests/unit/open-config-message.test.ts new file mode 100644 index 00000000000..f16f0ee458d --- /dev/null +++ b/packages/kilo-vscode/tests/unit/open-config-message.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from "bun:test" +import { configMessage } from "../../webview-ui/src/utils/open-config" + +describe("configMessage", () => { + it("builds a global config request with localized labels", () => { + const message = configMessage("global", (key, params) => `${key}:${params?.scope ?? ""}`) + + expect(message.type).toBe("openConfigFile") + expect(message.scope).toBe("global") + expect(message.labels.scope).toBe("settings.config.scope.global:") + expect(message.labels.title).toBe("settings.config.title:settings.config.scope.global:") + expect(message.labels.openFailed).toBe("settings.config.openFailed:settings.config.scope.global:") + }) +}) diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderDialog.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderDialog.tsx index e81910eadf7..c548b06c76d 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderDialog.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderDialog.tsx @@ -15,6 +15,7 @@ import { useProvider } from "../../context/provider" import { useVSCode } from "../../context/vscode" import type { ExtensionMessage, ProviderAuthState, ProviderConfig } from "../../types/messages" import { createProviderAction } from "../../utils/provider-action" +import { configMessage } from "../../utils/open-config" import { MASKED_CUSTOM_PROVIDER_KEY, resolveCustomProviderKey } from "../../../../src/shared/custom-provider" import { CUSTOM_PROVIDER_PACKAGE, @@ -563,6 +564,19 @@ const CustomProviderDialog = (props: CustomProviderDialogProps) => { {language.t("provider.custom.description.link")} {language.t("provider.custom.description.suffix")} + +
+
diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/Settings.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/Settings.tsx index 8bd39f14a44..ab8a5e99fba 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/Settings.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/Settings.tsx @@ -28,6 +28,7 @@ import SandboxingTab from "./SandboxingTab" import * as Sandboxing from "./sandboxing" import { useServer } from "../../context/server" import type { MigrationSource } from "../../types/messages" +import { configMessage } from "../../utils/open-config" export interface SettingsProps { tab?: string @@ -66,34 +67,7 @@ const Settings: Component = (props) => { } const open = (scope: "local" | "global") => { - const label = - scope === "global" ? language.t("settings.config.scope.global") : language.t("settings.config.scope.local") - vscode.postMessage({ - type: "openConfigFile", - scope, - labels: { - scope: label, - statusLoaded: language.t("settings.config.status.loaded"), - statusLoadedLegacy: language.t("settings.config.status.loadedLegacy"), - statusNotLoaded: language.t("settings.config.status.notLoaded"), - statusCreate: language.t("settings.config.status.create"), - title: language.t("settings.config.title", { scope: label }), - placeholder: language.t("settings.config.placeholder"), - noWorkspace: language.t("settings.config.noWorkspace"), - openFailed: language.t("settings.config.openFailed", { scope: label, message: "{{message}}" }), - sourceXdg: language.t("settings.config.source.xdg"), - sourceHomeKilo: language.t("settings.config.source.homeKilo"), - sourceHomeKilocode: language.t("settings.config.source.homeKilocode"), - sourceHomeOpencode: language.t("settings.config.source.homeOpencode"), - sourceEnvFile: language.t("settings.config.source.envFile"), - sourceEnvDir: language.t("settings.config.source.envDir"), - sourceEnvContent: language.t("settings.config.source.envContent"), - sourceProjectKilo: language.t("settings.config.source.projectKilo"), - sourceProjectRoot: language.t("settings.config.source.projectRoot"), - sourceProjectKilocode: language.t("settings.config.source.projectKilocode"), - sourceProjectOpencode: language.t("settings.config.source.projectOpencode"), - }, - }) + vscode.postMessage(configMessage(scope, language.t)) } // Sync when the parent changes the tab prop (e.g. via navigate message) diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index 71ac3559bce..42dde00f517 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -455,6 +455,7 @@ export const dict = { "provider.custom.models.fetch.search": "البحث في النماذج\u2026", "provider.custom.models.fetch.add": "إضافة {{count}} نموذج(نماذج)", "provider.custom.edit.title": "تعديل المزود", + "provider.custom.edit.advanced": "تحرير الإعدادات المتقدمة في ملف إعداد JSON", "provider.custom.headers.label": "الرؤوس (اختياري)", "provider.custom.headers.key.label": "الرأس", "provider.custom.headers.key.placeholder": "Header-Name", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index 589a8b38714..42334d5c232 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -466,6 +466,7 @@ export const dict = { "provider.custom.models.fetch.search": "Pesquisar modelos\u2026", "provider.custom.models.fetch.add": "Adicionar {{count}} modelo(s)", "provider.custom.edit.title": "Editar provedor", + "provider.custom.edit.advanced": "Editar configurações avançadas no arquivo de configuração JSON", "provider.custom.headers.label": "Headers (opcional)", "provider.custom.headers.key.label": "Cabeçalho", "provider.custom.headers.key.placeholder": "Header-Name", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index d737c969906..a8f4e547c09 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -509,6 +509,7 @@ export const dict = { "provider.custom.models.fetch.search": "Pretraži modele\u2026", "provider.custom.models.fetch.add": "Dodaj {{count}} model(a)", "provider.custom.edit.title": "Uredi provajdera", + "provider.custom.edit.advanced": "Uredite napredne postavke u JSON konfiguracijskoj datoteci", "provider.custom.headers.label": "Zaglavlja (opcionalno)", "provider.custom.headers.key.label": "Zaglavlje", "provider.custom.headers.key.placeholder": "Header-Name", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index bf71f652cf9..66d4104056d 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -506,6 +506,7 @@ export const dict = { "provider.custom.models.fetch.search": "Søg modeller\u2026", "provider.custom.models.fetch.add": "Tilføj {{count}} model(ler)", "provider.custom.edit.title": "Rediger udbyder", + "provider.custom.edit.advanced": "Rediger avancerede indstillinger i JSON-konfigurationsfilen", "provider.custom.headers.label": "Headers (valgfrit)", "provider.custom.headers.key.label": "Header", "provider.custom.headers.key.placeholder": "Header-Name", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index 12d30679fac..221b11d3e14 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -517,6 +517,7 @@ export const dict = { "provider.custom.models.fetch.search": "Modelle suchen\u2026", "provider.custom.models.fetch.add": "{{count}} Modell(e) hinzufügen", "provider.custom.edit.title": "Anbieter bearbeiten", + "provider.custom.edit.advanced": "Erweiterte Einstellungen in der JSON-Konfigurationsdatei bearbeiten", "provider.custom.headers.label": "Header (optional)", "provider.custom.headers.key.label": "Kopfzeile", "provider.custom.headers.key.placeholder": "Header-Name", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index c5f19bdb6b0..cbcd4ea4223 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -420,6 +420,7 @@ export const dict = { "provider.custom.models.fetch.search": "Search models\u2026", "provider.custom.models.fetch.add": "Add {{count}} model(s)", "provider.custom.edit.title": "Edit provider", + "provider.custom.edit.advanced": "Edit advanced settings in the JSON config file", "provider.custom.headers.label": "Headers (optional)", "provider.custom.headers.key.label": "Header", "provider.custom.headers.key.placeholder": "Header-Name", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index bee7bb09d13..929494b6b87 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -510,6 +510,7 @@ export const dict = { "provider.custom.models.fetch.search": "Buscar modelos\u2026", "provider.custom.models.fetch.add": "Añadir {{count}} modelo(s)", "provider.custom.edit.title": "Editar proveedor", + "provider.custom.edit.advanced": "Editar la configuración avanzada en el archivo de configuración JSON", "provider.custom.headers.label": "Headers (opcional)", "provider.custom.headers.key.label": "Header", "provider.custom.headers.key.placeholder": "Header-Name", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fa.ts b/packages/kilo-vscode/webview-ui/src/i18n/fa.ts index 1d18abd83dc..b18b51b7ae1 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fa.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fa.ts @@ -421,6 +421,7 @@ export const dict = { "provider.custom.models.fetch.search": "جستجوی مدل‌ها…", "provider.custom.models.fetch.add": "افزودن {{count}} مدل", "provider.custom.edit.title": "ویرایش ارائه‌دهنده", + "provider.custom.edit.advanced": "ویرایش تنظیمات پیشرفته در فایل پیکربندی JSON", "provider.custom.headers.label": "هدرها (اختیاری)", "provider.custom.headers.key.label": "هدر", "provider.custom.headers.key.placeholder": "Header-Name", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index dcf4a43dc37..f63a277f928 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -510,6 +510,7 @@ export const dict = { "provider.custom.models.fetch.search": "Rechercher des modèles\u2026", "provider.custom.models.fetch.add": "Ajouter {{count}} modèle(s)", "provider.custom.edit.title": "Modifier le fournisseur", + "provider.custom.edit.advanced": "Modifier les paramètres avancés dans le fichier de configuration JSON", "provider.custom.headers.label": "En-têtes (optionnel)", "provider.custom.headers.key.label": "En-tête", "provider.custom.headers.key.placeholder": "Header-Name", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/it.ts b/packages/kilo-vscode/webview-ui/src/i18n/it.ts index 84a1a7f4be0..01fa7e835ff 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/it.ts @@ -330,6 +330,7 @@ export const dict = { "provider.custom.models.fetch.search": "Cerca modelli...", "provider.custom.models.fetch.add": "Aggiungi {{count}} modelli", "provider.custom.edit.title": "Modifica provider", + "provider.custom.edit.advanced": "Modifica le impostazioni avanzate nel file di configurazione JSON", "provider.custom.headers.label": "Header (opzionali)", "provider.custom.headers.key.label": "Header", "provider.custom.headers.key.placeholder": "Nome-Header", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index 96f9f032d6d..dffecedbd80 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -502,6 +502,7 @@ export const dict = { "provider.custom.models.fetch.search": "モデルを検索\u2026", "provider.custom.models.fetch.add": "{{count}}個のモデルを追加", "provider.custom.edit.title": "プロバイダーを編集", + "provider.custom.edit.advanced": "JSON 設定ファイルで詳細設定を編集", "provider.custom.headers.label": "ヘッダー(任意)", "provider.custom.headers.key.label": "ヘッダー", "provider.custom.headers.key.placeholder": "Header-Name", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index 69cb64653ff..fac9f03cea6 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -463,6 +463,7 @@ export const dict = { "provider.custom.models.fetch.search": "모델 검색\u2026", "provider.custom.models.fetch.add": "{{count}}개 모델 추가", "provider.custom.edit.title": "공급자 편집", + "provider.custom.edit.advanced": "JSON 구성 파일에서 고급 설정 편집", "provider.custom.headers.label": "헤더 (선택사항)", "provider.custom.headers.key.label": "헤더", "provider.custom.headers.key.placeholder": "Header-Name", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts index ae0a6b22bfe..c1485ff655b 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts @@ -460,6 +460,7 @@ export const dict = { "provider.custom.models.fetch.search": "Modellen zoeken\u2026", "provider.custom.models.fetch.add": "{{count}} model(len) toevoegen", "provider.custom.edit.title": "Provider bewerken", + "provider.custom.edit.advanced": "Geavanceerde instellingen bewerken in het JSON-configuratiebestand", "provider.custom.headers.label": "Headers (optioneel)", "provider.custom.headers.key.label": "Header", "provider.custom.headers.key.placeholder": "Header-Naam", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index 6cb0b4bb955..dd6da3fe63e 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -469,6 +469,7 @@ export const dict = { "provider.custom.models.fetch.search": "Søk etter modeller\u2026", "provider.custom.models.fetch.add": "Legg til {{count}} modell(er)", "provider.custom.edit.title": "Rediger leverandør", + "provider.custom.edit.advanced": "Rediger avanserte innstillinger i JSON-konfigurasjonsfilen", "provider.custom.headers.label": "Headere (valgfritt)", "provider.custom.headers.key.label": "Header", "provider.custom.headers.key.placeholder": "Header-Name", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index 8e6744d6aa8..2114eb158f6 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -465,6 +465,7 @@ export const dict = { "provider.custom.models.fetch.search": "Szukaj modeli\u2026", "provider.custom.models.fetch.add": "Dodaj {{count}} model(i)", "provider.custom.edit.title": "Edytuj dostawcę", + "provider.custom.edit.advanced": "Edytuj ustawienia zaawansowane w pliku konfiguracji JSON", "provider.custom.headers.label": "Nagłówki (opcjonalnie)", "provider.custom.headers.key.label": "Nagłówek", "provider.custom.headers.key.placeholder": "Header-Name", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index 91b22b70430..4e20af74782 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -503,6 +503,7 @@ export const dict = { "provider.custom.models.fetch.search": "Поиск моделей\u2026", "provider.custom.models.fetch.add": "Добавить {{count}} модель(ей)", "provider.custom.edit.title": "Редактировать провайдера", + "provider.custom.edit.advanced": "Изменить расширенные настройки в файле конфигурации JSON", "provider.custom.headers.label": "Заголовки (необязательно)", "provider.custom.headers.key.label": "Заголовок", "provider.custom.headers.key.placeholder": "Header-Name", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index 764eef584ee..66924828bcb 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -500,6 +500,7 @@ export const dict = { "provider.custom.models.fetch.search": "ค้นหาโมเดล\u2026", "provider.custom.models.fetch.add": "เพิ่ม {{count}} โมเดล", "provider.custom.edit.title": "แก้ไขผู้ให้บริการ", + "provider.custom.edit.advanced": "แก้ไขการตั้งค่าขั้นสูงในไฟล์การกำหนดค่า JSON", "provider.custom.headers.label": "ส่วนหัว (ไม่จำเป็น)", "provider.custom.headers.key.label": "ส่วนหัว", "provider.custom.headers.key.placeholder": "Header-Name", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts index 9aa92a001be..8ee41b0e18f 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts @@ -455,6 +455,7 @@ export const dict = { "provider.custom.models.fetch.search": "Model ara\u2026", "provider.custom.models.fetch.add": "{{count}} model ekle", "provider.custom.edit.title": "Sağlayıcıyı düzenle", + "provider.custom.edit.advanced": "JSON yapılandırma dosyasında gelişmiş ayarları düzenle", "provider.custom.headers.label": "Başlıklar (isteğe bağlı)", "provider.custom.headers.key.label": "Başlık", "provider.custom.headers.key.placeholder": "Başlık-Adı", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts index 9acb616a6e4..12944f68a4b 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts @@ -458,6 +458,7 @@ export const dict = { "provider.custom.models.fetch.search": "Пошук моделей\u2026", "provider.custom.models.fetch.add": "Додати {{count}} моделей", "provider.custom.edit.title": "Редагувати провайдера", + "provider.custom.edit.advanced": "Редагувати розширені налаштування у файлі конфігурації JSON", "provider.custom.headers.label": "Заголовки (необов'язково)", "provider.custom.headers.key.label": "Заголовок", "provider.custom.headers.key.placeholder": "Назва-заголовка", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index 1d48e7777b4..78d21c59126 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -485,6 +485,7 @@ export const dict = { "provider.custom.models.fetch.search": "搜索模型\u2026", "provider.custom.models.fetch.add": "添加 {{count}} 个模型", "provider.custom.edit.title": "编辑提供商", + "provider.custom.edit.advanced": "在 JSON 配置文件中编辑高级设置", "provider.custom.headers.label": "请求头(可选)", "provider.custom.headers.key.label": "请求头", "provider.custom.headers.key.placeholder": "Header-Name", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index 97cdf54edf0..1414c7f47d5 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -445,6 +445,7 @@ export const dict = { "provider.custom.models.fetch.search": "搜尋模型\u2026", "provider.custom.models.fetch.add": "新增 {{count}} 個模型", "provider.custom.edit.title": "編輯供應商", + "provider.custom.edit.advanced": "在 JSON 設定檔中編輯進階設定", "provider.custom.headers.label": "標頭(選填)", "provider.custom.headers.key.label": "標頭", "provider.custom.headers.key.placeholder": "Header-Name", diff --git a/packages/kilo-vscode/webview-ui/src/utils/open-config.ts b/packages/kilo-vscode/webview-ui/src/utils/open-config.ts new file mode 100644 index 00000000000..9ae1ed38393 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/utils/open-config.ts @@ -0,0 +1,32 @@ +import type { LanguageContextValue } from "../context/language" +import type { OpenConfigFileRequest } from "../types/messages" + +export function configMessage(scope: "local" | "global", t: LanguageContextValue["t"]): OpenConfigFileRequest { + const label = t(scope === "global" ? "settings.config.scope.global" : "settings.config.scope.local") + return { + type: "openConfigFile", + scope, + labels: { + scope: label, + statusLoaded: t("settings.config.status.loaded"), + statusLoadedLegacy: t("settings.config.status.loadedLegacy"), + statusNotLoaded: t("settings.config.status.notLoaded"), + statusCreate: t("settings.config.status.create"), + title: t("settings.config.title", { scope: label }), + placeholder: t("settings.config.placeholder"), + noWorkspace: t("settings.config.noWorkspace"), + openFailed: t("settings.config.openFailed", { scope: label, message: "{{message}}" }), + sourceXdg: t("settings.config.source.xdg"), + sourceHomeKilo: t("settings.config.source.homeKilo"), + sourceHomeKilocode: t("settings.config.source.homeKilocode"), + sourceHomeOpencode: t("settings.config.source.homeOpencode"), + sourceEnvFile: t("settings.config.source.envFile"), + sourceEnvDir: t("settings.config.source.envDir"), + sourceEnvContent: t("settings.config.source.envContent"), + sourceProjectKilo: t("settings.config.source.projectKilo"), + sourceProjectRoot: t("settings.config.source.projectRoot"), + sourceProjectKilocode: t("settings.config.source.projectKilocode"), + sourceProjectOpencode: t("settings.config.source.projectOpencode"), + }, + } +} From ead46f43415a121bacc28780bc47fe43b1b736ba Mon Sep 17 00:00:00 2001 From: niefangxin_yfmx Date: Fri, 7 Aug 2026 16:57:47 +0800 Subject: [PATCH 24/73] fix(ui): make Mermaid Copy PNG/SVG put images on the clipboard Copy PNG called fetch() on a data URL, which VS Code webview CSP rejects, so the copy silently did nothing. Decode the data URL locally instead. Copy SVG used clipboard.writeText() with SVG markup, so paste targets got source text instead of an image. Write SVG/PNG/text representations when supported, and fall back to PNG on hosts that reject multi-type items. --- .changeset/mermaid-copy-clipboard-images.md | 5 +++ .../src/kilocode/markdown-mermaid-actions.tsx | 13 ++++-- .../markdown-mermaid-data-url.test.ts | 24 ++++++++++ .../src/kilocode/markdown-mermaid-data-url.ts | 12 +++++ packages/ui/src/kilocode/markdown-mermaid.ts | 44 ++++++++++++++++--- 5 files changed, 87 insertions(+), 11 deletions(-) create mode 100644 .changeset/mermaid-copy-clipboard-images.md create mode 100644 packages/ui/src/kilocode/markdown-mermaid-data-url.test.ts create mode 100644 packages/ui/src/kilocode/markdown-mermaid-data-url.ts diff --git a/.changeset/mermaid-copy-clipboard-images.md b/.changeset/mermaid-copy-clipboard-images.md new file mode 100644 index 00000000000..c9c0a405f9c --- /dev/null +++ b/.changeset/mermaid-copy-clipboard-images.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Fix Mermaid Copy PNG/SVG in VS Code webviews so they put images on the clipboard instead of failing or copying SVG markup. diff --git a/packages/ui/src/kilocode/markdown-mermaid-actions.tsx b/packages/ui/src/kilocode/markdown-mermaid-actions.tsx index 4f5756dbe1b..26fe5e639df 100644 --- a/packages/ui/src/kilocode/markdown-mermaid-actions.tsx +++ b/packages/ui/src/kilocode/markdown-mermaid-actions.tsx @@ -49,10 +49,15 @@ function Item(props: { label: string; onSelect: () => void }) { export function MermaidActions(props: Props) { const [copied, setCopied] = createSignal(false) const copy = (run: () => Promise) => { - void run().then(() => { - setCopied(true) - setTimeout(() => setCopied(false), 1500) - }) + void run() + .then(() => { + setCopied(true) + setTimeout(() => setCopied(false), 1500) + }) + .catch((err) => { + // Avoid unhandledrejection; Copy PNG used to fail silently under webview CSP. + console.warn("Mermaid copy failed", err) + }) } return ( diff --git a/packages/ui/src/kilocode/markdown-mermaid-data-url.test.ts b/packages/ui/src/kilocode/markdown-mermaid-data-url.test.ts new file mode 100644 index 00000000000..592a53213bc --- /dev/null +++ b/packages/ui/src/kilocode/markdown-mermaid-data-url.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, test } from "bun:test" +import { dataUrlToBlob } from "./markdown-mermaid-data-url" + +describe("dataUrlToBlob", () => { + test("decodes base64 payload and preserves mime type", async () => { + const bytes = Uint8Array.from([0x89, 0x50, 0x4e, 0x47]) + const dataUrl = `data:image/png;base64,${Buffer.from(bytes).toString("base64")}` + const blob = dataUrlToBlob(dataUrl) + + expect(blob.type).toBe("image/png") + expect(blob.size).toBe(bytes.length) + expect(new Uint8Array(await blob.arrayBuffer())).toEqual(bytes) + }) + + test("defaults mime when the data URL omits it", async () => { + const blob = dataUrlToBlob(`data:;base64,${Buffer.from("hi").toString("base64")}`) + expect(blob.type).toBe("application/octet-stream") + expect(await blob.text()).toBe("hi") + }) + + test("rejects data URLs without a comma separator", () => { + expect(() => dataUrlToBlob("data:image/png;base64")).toThrow("Unable to export Mermaid diagram.") + }) +}) diff --git a/packages/ui/src/kilocode/markdown-mermaid-data-url.ts b/packages/ui/src/kilocode/markdown-mermaid-data-url.ts new file mode 100644 index 00000000000..d9eda408f28 --- /dev/null +++ b/packages/ui/src/kilocode/markdown-mermaid-data-url.ts @@ -0,0 +1,12 @@ +// VS Code webview CSP blocks fetch(data:...); decode locally for clipboard/export helpers. +export function dataUrlToBlob(dataUrl: string): Blob { + const comma = dataUrl.indexOf(",") + if (comma < 0) throw new Error("Unable to export Mermaid diagram.") + const header = dataUrl.slice(0, comma) + const data = dataUrl.slice(comma + 1) + const mime = /^data:([^;,]*)/.exec(header)?.[1] || "application/octet-stream" + const binary = atob(data) + const bytes = new Uint8Array(binary.length) + for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i) + return new Blob([bytes], { type: mime }) +} diff --git a/packages/ui/src/kilocode/markdown-mermaid.ts b/packages/ui/src/kilocode/markdown-mermaid.ts index 33f3e091783..0edeef600d5 100644 --- a/packages/ui/src/kilocode/markdown-mermaid.ts +++ b/packages/ui/src/kilocode/markdown-mermaid.ts @@ -1,6 +1,7 @@ import DOMPurify from "dompurify" import { fnv1a } from "../context/marked" import { mountMermaidActions } from "./markdown-mermaid-actions" +import { dataUrlToBlob } from "./markdown-mermaid-data-url" // DOMPurify >= 3.1.7 dropped foreignObject from the default HTML integration // points, which caused the inner
/ /

labels Mermaid renders @@ -303,14 +304,43 @@ async function copyText(text: string) { await navigator.clipboard.writeText(text) } -async function copyPng(svg: SVGSVGElement) { - const url = await png(svg) - const blob = await (await fetch(url)).blob() +function supportsClipboardType(type: string) { + return typeof ClipboardItem !== "undefined" && "supports" in ClipboardItem && ClipboardItem.supports(type) +} + +async function writeClipboard(items: Record) { if (typeof ClipboardItem === "undefined") { - await navigator.clipboard.writeText(serialize(svg)) - return + throw new Error("ClipboardItem is unavailable.") } - await navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]) + await navigator.clipboard.write([new ClipboardItem(items)]) +} + +async function writePngClipboard(svg: SVGSVGElement) { + await writeClipboard({ "image/png": dataUrlToBlob(await png(svg)) }) +} + +async function copySvg(svg: SVGSVGElement) { + const markup = serialize(svg) + // Prefer a rich ClipboardItem so paste targets can pick SVG, PNG, or text. + // Previous writeText(serialize(svg)) always pasted markup instead of an image. + const pngBlob = dataUrlToBlob(await png(svg)) + const items: Record = { + "image/png": pngBlob, + "text/plain": new Blob([markup], { type: "text/plain" }), + } + if (supportsClipboardType("image/svg+xml")) { + items["image/svg+xml"] = new Blob([markup], { type: "image/svg+xml" }) + } + try { + await writeClipboard(items) + } catch { + // Some hosts reject multi-type items; keep an image on the clipboard. + await writeClipboard({ "image/png": pngBlob }) + } +} + +async function copyPng(svg: SVGSVGElement) { + await writePngClipboard(svg) } function renderActions(el: HTMLDivElement, pre: HTMLPreElement, source: string, labels: MermaidLabels) { @@ -329,7 +359,7 @@ function renderActions(el: HTMLDivElement, pre: HTMLPreElement, source: string, mountMermaidActions(el, { labels, onCopySource: () => copyText(sourceText), - onCopySvg: () => copyText(sourceSvg()), + onCopySvg: () => copySvg(svg), onCopyPng: () => copyPng(svg), onDownloadSvg: () => save(sourceSvgUrl(), "mermaid-diagram.svg"), onDownloadPng: async () => save(await png(svg), "mermaid-diagram.png"), From 910f0f24d2b38ff04b43dee694f6968608f54eb1 Mon Sep 17 00:00:00 2001 From: Christiaan Arnoldus Date: Fri, 7 Aug 2026 16:45:50 +0200 Subject: [PATCH 25/73] fix(cli): disable suggest tool in non-interactive runs --- ...disable-suggest-in-non-interactive-runs.md | 5 +++ .../opencode/src/cli/cmd/github.handler.ts | 7 ++++ packages/opencode/src/cli/cmd/run.ts | 17 +++++++- packages/opencode/src/kilocode/tool/task.ts | 1 + packages/opencode/test/cli/auto-mode.test.ts | 15 ++++++- .../opencode/test/cli/run/run-process.test.ts | 41 +++++++++++++++++++ .../test/kilocode/task-nesting.test.ts | 5 +++ 7 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 .changeset/disable-suggest-in-non-interactive-runs.md diff --git a/.changeset/disable-suggest-in-non-interactive-runs.md b/.changeset/disable-suggest-in-non-interactive-runs.md new file mode 100644 index 00000000000..d75f2b540ee --- /dev/null +++ b/.changeset/disable-suggest-in-non-interactive-runs.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Disable the suggest tool and auto-dismiss pending suggestions in non-interactive CLI runs to prevent hanging on benchmarks and automated pipelines. diff --git a/packages/opencode/src/cli/cmd/github.handler.ts b/packages/opencode/src/cli/cmd/github.handler.ts index 9727a76c2a0..1a94e86ebb1 100644 --- a/packages/opencode/src/cli/cmd/github.handler.ts +++ b/packages/opencode/src/cli/cmd/github.handler.ts @@ -516,6 +516,13 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?: action: "deny", pattern: "*", }, + // kilocode_change start + { + permission: "suggest", + action: "deny", + pattern: "*", + }, + // kilocode_change end ], }), ) diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 1ae73d53c62..36bcabfba86 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -472,7 +472,12 @@ export const RunCommand = effectCmd({ action: "deny", pattern: "*", }, - // kilocode_change start - non-interactive runs cannot take over a terminal + // kilocode_change start - non-interactive runs cannot answer suggestions or take over a terminal + { + permission: "suggest", + action: "deny", + pattern: "*", + }, { permission: "interactive_terminal", action: "deny", @@ -904,6 +909,16 @@ export const RunCommand = effectCmd({ break } + // kilocode_change start - non-interactive runs dismiss suggestions so they don't block + if (event.type === "suggestion.shown") { + const suggestion = event.properties + if (suggestion.sessionID === sessionID || KiloRunAuto.allowed(tracked, suggestion.sessionID)) { + await client.suggestion.dismiss({ requestID: suggestion.id }).catch(() => {}) + } + continue + } + // kilocode_change end + if (event.type === "permission.asked") { const permission = event.properties // kilocode_change start - skill shell batches need an interactive human decision. The server ignores diff --git a/packages/opencode/src/kilocode/tool/task.ts b/packages/opencode/src/kilocode/tool/task.ts index f5d094df7bc..3337423c8d9 100644 --- a/packages/opencode/src/kilocode/tool/task.ts +++ b/packages/opencode/src/kilocode/tool/task.ts @@ -80,6 +80,7 @@ export namespace KiloTask { return [ { permission: "task", pattern: "*", action: "deny" }, { permission: "question", pattern: "*", action: "deny" }, + { permission: "suggest", pattern: "*", action: "deny" }, { permission: "interactive_terminal", pattern: "*", action: "deny" }, ...rules, ] diff --git a/packages/opencode/test/cli/auto-mode.test.ts b/packages/opencode/test/cli/auto-mode.test.ts index eef67fcc56c..598d4017859 100644 --- a/packages/opencode/test/cli/auto-mode.test.ts +++ b/packages/opencode/test/cli/auto-mode.test.ts @@ -18,6 +18,11 @@ describe("Auto mode flag", () => { action: "deny" as const, pattern: "*", }, + { + permission: "suggest", + action: "deny" as const, + pattern: "*", + }, { permission: "interactive_terminal", action: "deny" as const, @@ -25,7 +30,7 @@ describe("Auto mode flag", () => { }, ] - expect(autoPermissions).toHaveLength(3) + expect(autoPermissions).toHaveLength(4) // First rule: allow all expect(autoPermissions[0].permission).toBe("*") @@ -36,9 +41,12 @@ describe("Auto mode flag", () => { expect(autoPermissions[1].permission).toBe("question") expect(autoPermissions[1].action).toBe("deny") expect(autoPermissions[1].pattern).toBe("*") - expect(autoPermissions[2].permission).toBe("interactive_terminal") + expect(autoPermissions[2].permission).toBe("suggest") expect(autoPermissions[2].action).toBe("deny") expect(autoPermissions[2].pattern).toBe("*") + expect(autoPermissions[3].permission).toBe("interactive_terminal") + expect(autoPermissions[3].action).toBe("deny") + expect(autoPermissions[3].pattern).toBe("*") }) test("non-auto mode should not set allow-all permissions", () => { @@ -55,6 +63,7 @@ describe("Auto mode flag", () => { const autoPermissions = [ { permission: "*", action: "allow" as const, pattern: "*" }, { permission: "question", action: "deny" as const, pattern: "*" }, + { permission: "suggest", action: "deny" as const, pattern: "*" }, { permission: "interactive_terminal", action: "deny" as const, pattern: "*" }, ] @@ -71,6 +80,8 @@ describe("Auto mode flag", () => { // Test that "question" permission resolves to "deny" const questionRule = findLastMatch("question") expect(questionRule?.action).toBe("deny") + const suggestRule = findLastMatch("suggest") + expect(suggestRule?.action).toBe("deny") const terminalRule = findLastMatch("interactive_terminal") expect(terminalRule?.action).toBe("deny") diff --git a/packages/opencode/test/cli/run/run-process.test.ts b/packages/opencode/test/cli/run/run-process.test.ts index 2dce9e8cb21..70fcbe5bea8 100644 --- a/packages/opencode/test/cli/run/run-process.test.ts +++ b/packages/opencode/test/cli/run/run-process.test.ts @@ -412,4 +412,45 @@ describe("opencode run (non-interactive subprocess)", () => { }), 60_000, // kilocode_change ) + + // kilocode_change start - non-interactive runs exclude human-driven tools like suggest + cliIt.concurrent( + "kilo run --auto excludes suggest tool from LLM request", + ({ llm, opencode }) => + Effect.gen(function* () { + yield* llm.text("done") + const result = yield* opencode.run("do work", { extraArgs: ["--auto"] }) + opencode.expectExit(result, 0) + expect(result.stdout).toBe("done\n") + const inputs = yield* llm.inputs + const tools = (inputs[0]?.body as any)?.tools as Array<{ function?: { name?: string } }> | undefined + const toolNames = tools?.map((t) => t.function?.name).filter(Boolean) ?? [] + expect(toolNames).not.toContain("suggest") + expect(toolNames).not.toContain("question") + expect(toolNames).not.toContain("interactive_terminal") + }), + 60_000, + ) + + cliIt.concurrent( + "kilo run auto-dismisses suggestion and exits cleanly if suggest tool is invoked", + ({ llm, opencode }) => + Effect.gen(function* () { + yield* llm.push( + reply().tool("suggest", { + suggest: "Run checks?", + actions: [{ label: "Run checks", prompt: "Run checks" }], + }), + ) + yield* llm.text("completed after dismissal") + const result = yield* opencode.run("do work", { + permission: { suggest: "allow" }, + extraArgs: ["--auto"], + }) + opencode.expectExit(result, 0) + expect(result.stdout).toContain("completed after dismissal") + }), + 60_000, + ) + // kilocode_change end }) diff --git a/packages/opencode/test/kilocode/task-nesting.test.ts b/packages/opencode/test/kilocode/task-nesting.test.ts index 4d566155130..a57b682905c 100644 --- a/packages/opencode/test/kilocode/task-nesting.test.ts +++ b/packages/opencode/test/kilocode/task-nesting.test.ts @@ -279,6 +279,11 @@ describe("Kilo task nesting", () => { pattern: "*", action: "deny", }, + { + permission: "suggest", + pattern: "*", + action: "deny", + }, { permission: "interactive_terminal", pattern: "*", From 82be537f18985223d23612477fa3f4ce30ed57aa Mon Sep 17 00:00:00 2001 From: Christiaan Arnoldus Date: Fri, 7 Aug 2026 17:07:31 +0200 Subject: [PATCH 26/73] test(cli): verify suggestion auto-dismissal in attached run session --- .../opencode/test/cli/run/run-process.test.ts | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/packages/opencode/test/cli/run/run-process.test.ts b/packages/opencode/test/cli/run/run-process.test.ts index 70fcbe5bea8..5841afa6043 100644 --- a/packages/opencode/test/cli/run/run-process.test.ts +++ b/packages/opencode/test/cli/run/run-process.test.ts @@ -5,6 +5,7 @@ // `KILO_CONFIG_CONTENT` providing the test provider config inline. import { describe, expect } from "bun:test" import { Effect } from "effect" +import { createKiloClient } from "@kilocode/sdk/v2" // kilocode_change import { reply } from "../../lib/llm-server" import { cliIt } from "../../lib/cli-process" @@ -432,10 +433,20 @@ describe("opencode run (non-interactive subprocess)", () => { 60_000, ) - cliIt.concurrent( - "kilo run auto-dismisses suggestion and exits cleanly if suggest tool is invoked", + cliIt.live( + "kilo run auto-dismisses suggestion and exits cleanly if suggest tool is invoked in attached session", ({ llm, opencode }) => Effect.gen(function* () { + const server = yield* opencode.serve() + const client = createKiloClient({ baseUrl: server.url }) + const session = yield* Effect.promise(() => + client.session.create({ + permission: [{ permission: "suggest", action: "allow", pattern: "*" }], + }), + ) + const sessionID = session.data?.id + expect(sessionID).toBeDefined() + yield* llm.push( reply().tool("suggest", { suggest: "Run checks?", @@ -443,12 +454,17 @@ describe("opencode run (non-interactive subprocess)", () => { }), ) yield* llm.text("completed after dismissal") + const result = yield* opencode.run("do work", { - permission: { suggest: "allow" }, - extraArgs: ["--auto"], + extraArgs: ["--attach", server.url, "--session", sessionID!, "--auto"], }) opencode.expectExit(result, 0) - expect(result.stdout).toContain("completed after dismissal") + + const messages = yield* Effect.promise(() => client.session.messages({ sessionID: sessionID! })) + const assistant = messages.data?.findLast((m) => m.info.role === "assistant") + const toolPart = assistant?.parts.find((p) => p.type === "tool" && p.tool === "suggest") + expect(toolPart).toBeDefined() + expect((toolPart as any)?.state?.metadata?.dismissed).toBe(true) }), 60_000, ) From e87633400bf1b7be8a54fb48bb7af4a161468eda Mon Sep 17 00:00:00 2001 From: cmanu Date: Fri, 7 Aug 2026 08:53:14 -0700 Subject: [PATCH 27/73] feat(pr-view): Re-add pr number on worktree item --- packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx b/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx index 69378459144..2b07b00269c 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx @@ -357,6 +357,7 @@ export const WorktreeItem: Component = (props) => { onClick={handleOpenPR} > + #{pr().number} From d0585faaeb8fc5e0a889e81f4d7f46526a441ca0 Mon Sep 17 00:00:00 2001 From: cmanu Date: Fri, 7 Aug 2026 09:43:52 -0700 Subject: [PATCH 28/73] fix(pr-view): unit tests --- packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts index 7d0b8b1c132..0486cc70822 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts @@ -38,6 +38,7 @@ const TSX_FILES = [ path.join(ROOT, "webview-ui/agent-manager/MultiModelSelector.tsx"), path.join(ROOT, "webview-ui/agent-manager/ApplyDialog.tsx"), path.join(ROOT, "webview-ui/agent-manager/WorktreeItem.tsx"), + path.join(ROOT, "webview-ui/agent-manager/pr/PRBadge.tsx"), path.join(ROOT, "webview-ui/agent-manager/SectionHeader.tsx"), path.join(ROOT, "webview-ui/agent-manager/SidebarSectionHeader.tsx"), path.join(ROOT, "webview-ui/agent-manager/SidebarSearchMenu.tsx"), From d6e4182606c57a82361d427f25e36eb1de6fb13e Mon Sep 17 00:00:00 2001 From: "kilo-maintainer[bot]" Date: Fri, 7 Aug 2026 16:46:55 +0000 Subject: [PATCH 29/73] chore: update kilo-vscode visual regression baselines --- .../pr-badge-approved-checks-failing-chromium-linux.png | 4 ++-- .../agentmanager/pr-badge-checks-failing-chromium-linux.png | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-approved-checks-failing-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-approved-checks-failing-chromium-linux.png index 4720473ebb2..25035a43351 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-approved-checks-failing-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-approved-checks-failing-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f2349f8c0d7049811702e5b8cbeb5fc2f402bdda8630d1a5210a970a13284197 -size 3694 +oid sha256:33ee6eb7070237487d1931f32ac77a2549e18e664b993895e9a9d6619527d0aa +size 4063 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-checks-failing-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-checks-failing-chromium-linux.png index 4720473ebb2..25035a43351 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-checks-failing-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-checks-failing-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f2349f8c0d7049811702e5b8cbeb5fc2f402bdda8630d1a5210a970a13284197 -size 3694 +oid sha256:33ee6eb7070237487d1931f32ac77a2549e18e664b993895e9a9d6619527d0aa +size 4063 From 1a0cc860e821001d7023577971105680062ddf05 Mon Sep 17 00:00:00 2001 From: cmanu Date: Fri, 7 Aug 2026 10:05:58 -0700 Subject: [PATCH 30/73] fix(pr-view): update reviewers summary and types --- packages/kilo-vscode/src/agent-manager/am-pr-utils.ts | 4 ++-- packages/kilo-vscode/src/agent-manager/types.ts | 4 +++- .../webview-ui/agent-manager/pr/PRReviewers.tsx | 6 +++--- .../webview-ui/agent-manager/pr/PRSummary.tsx | 9 +++------ .../kilo-vscode/webview-ui/agent-manager/pr/pr-types.ts | 4 +++- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/kilo-vscode/src/agent-manager/am-pr-utils.ts b/packages/kilo-vscode/src/agent-manager/am-pr-utils.ts index 8a45349bf61..25291c2a5db 100644 --- a/packages/kilo-vscode/src/agent-manager/am-pr-utils.ts +++ b/packages/kilo-vscode/src/agent-manager/am-pr-utils.ts @@ -1,4 +1,4 @@ -import type { CheckStatus, PRComment, PRReviewer } from "./types" +import type { CheckStatus, PRComment, PRReviewer, ReviewerState } from "./types" import type { PRResult, GhThread, GhReviewRequest, GhReview } from "./am-pr-types" export function parsePRResult(json: string): PRResult | null { @@ -55,7 +55,7 @@ export function formatCheckDuration(startedAt?: string, completedAt?: string): s return secs < 60 ? `${secs}s` : `${Math.floor(secs / 60)}m ${secs % 60}s` } -const REVIEWER_STATE: Record = { +const REVIEWER_STATE: Record = { APPROVED: "approved", CHANGES_REQUESTED: "changes_requested", COMMENTED: "commented", diff --git a/packages/kilo-vscode/src/agent-manager/types.ts b/packages/kilo-vscode/src/agent-manager/types.ts index 8e503e7db9f..836b50471a7 100644 --- a/packages/kilo-vscode/src/agent-manager/types.ts +++ b/packages/kilo-vscode/src/agent-manager/types.ts @@ -71,10 +71,12 @@ export interface PRComment { createdAt?: number } +export type ReviewerState = "approved" | "changes_requested" | "pending" | "commented" + export interface PRReviewer { login: string avatar?: string - state: "approved" | "changes_requested" | "pending" | "commented" + state: ReviewerState } export interface PRStatus { diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRReviewers.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRReviewers.tsx index 85206562958..071da1f119d 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRReviewers.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRReviewers.tsx @@ -1,17 +1,17 @@ /** @jsxImportSource solid-js */ import { For, Show, createSignal } from "solid-js" import { Icon } from "@kilocode/kilo-ui/icon" -import type { PRReviewer } from "./pr-types" +import type { PRReviewer, ReviewerState } from "./pr-types" import { SectionHeading } from "./SectionHeading" -const REVIEWER_ICON: Record = { +const REVIEWER_ICON: Record = { approved: "circle-check", changes_requested: "refresh", commented: "edit", pending: "dash", } -const REVIEWER_LABEL: Record = { +const REVIEWER_LABEL: Record = { approved: "Approved", changes_requested: "Changes requested", commented: "Commented", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRSummary.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRSummary.tsx index c04e89a5fe4..bd7dbf36d0e 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRSummary.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRSummary.tsx @@ -19,14 +19,11 @@ function summaryRows(pr: PRStatus): Array<{ icon: string; label: string; status: }) } - const reviewers = pr.comments?.reviewers - if (reviewers && reviewers.length > 0) { - const approved = reviewers.filter((r) => r.state === "approved").length - const changes = reviewers.filter((r) => r.state === "changes_requested").length - const status = approved === reviewers.length ? "success" : changes > 0 ? "failure" : "pending" + if (pr.review) { + const status = pr.review === "approved" ? "success" : pr.review === "changes_requested" ? "failure" : "pending" rows.push({ icon: status === "success" ? "circle-check" : status === "failure" ? "circle-x-outline" : "play", - label: approved === reviewers.length ? "All reviewers approved" : `${approved}/${reviewers.length} approvals`, + label: status === "success" ? "Approved" : status === "failure" ? "Changes requested" : "Review pending", status, }) } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-types.ts b/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-types.ts index 8d88e131557..d5d0a89d6c6 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-types.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-types.ts @@ -25,8 +25,10 @@ export interface PRComment { createdAt?: number } +export type ReviewerState = "approved" | "changes_requested" | "pending" | "commented" + export interface PRReviewer { login: string avatar?: string - state: "approved" | "changes_requested" | "pending" | "commented" + state: ReviewerState } From c875fb02be3962a8a3195da70cb4d94fd6acce34 Mon Sep 17 00:00:00 2001 From: cmanu Date: Fri, 7 Aug 2026 10:19:54 -0700 Subject: [PATCH 31/73] fix(pr-view): split reviewers out of comments; fix pr summary to show review decision; show reviewers for all pr statuses --- .../src/agent-manager/PRStatusPoller.ts | 47 ++++++++++++++----- .../kilo-vscode/src/agent-manager/types.ts | 2 +- .../webview-ui/agent-manager/pr/PRPanel.tsx | 4 +- .../src/stories/agent-manager.stories.tsx | 1 + .../src/stories/section-header.stories.tsx | 7 +++ .../src/types/messages/agent-manager.ts | 2 +- 6 files changed, 46 insertions(+), 17 deletions(-) diff --git a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts index 6ce5e6b748d..5a2e1e51ce0 100644 --- a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts +++ b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts @@ -248,8 +248,9 @@ export class PRStatusPoller { return } - const [checks, comments] = await Promise.all([ + const [checks, reviewers, comments] = await Promise.all([ this.fetchChecks(pr.number, wt.path), + this.fetchReviewers(pr.number, wt.path), this.activeWorktreeId === worktreeId ? this.fetchComments(pr.number, wt.path) : undefined, ]) if (this.stale(generation)) return @@ -262,15 +263,16 @@ export class PRStatusPoller { state: pr.state, review: pr.review, checks, + reviewers, ...(comments && { - comments: { total: comments.total, unresolved: comments.unresolved, comments: comments.comments, reviewers: comments.reviewers }, + comments: { total: comments.total, unresolved: comments.unresolved, comments: comments.comments }, }), additions: pr.additions, deletions: pr.deletions, files: pr.files, } - const hash = `${worktreeId}:${pr.number}:${pr.state}:${pr.review}:${checks.status}:${checks.passed}/${checks.total}:${comments?.total ?? ""}:${comments?.unresolved ?? ""}` + const hash = `${worktreeId}:${pr.number}:${pr.state}:${pr.review}:${checks.status}:${checks.passed}/${checks.total}:${reviewers.length}:${comments?.total ?? ""}:${comments?.unresolved ?? ""}` if (this.lastHash.get(worktreeId) === hash) return this.lastHash.set(worktreeId, hash) @@ -431,10 +433,36 @@ export class PRStatusPoller { return info } + private async fetchReviewers(prNumber: number, cwd: string): Promise { + try { + const repo = await this.getRepoInfo(cwd) + const query = `query($owner: String!, $repo: String!, $number: Int!) { + repository(owner: $owner, name: $repo) { + pullRequest(number: $number) { + reviewRequests(first: 20) { + nodes { requestedReviewer { ... on User { login avatarUrl } } } + } + reviews(first: 20, states: [APPROVED, CHANGES_REQUESTED, COMMENTED]) { + nodes { author { login avatarUrl } state } + } + } + } + }` + const { stdout } = await this.gh( + ["api", "graphql", "-f", `query=${query}`, "-F", `owner=${repo.owner}`, "-F", `repo=${repo.name}`, "-F", `number=${prNumber}`], + { cwd, timeout: 15_000 }, + ) + const pr = JSON.parse(stdout)?.data?.repository?.pullRequest + return parseReviewers((pr?.reviewRequests?.nodes ?? []) as GhReviewRequest[], (pr?.reviews?.nodes ?? []) as GhReview[]) + } catch { + return [] + } + } + private async fetchComments( prNumber: number, cwd: string, - ): Promise<{ total: number; unresolved: number; comments: PRComment[]; reviewers: PRReviewer[] }> { + ): Promise<{ total: number; unresolved: number; comments: PRComment[] }> { try { const repo = await this.getRepoInfo(cwd) const query = `query($owner: String!, $repo: String!, $number: Int!) { @@ -456,12 +484,6 @@ export class PRStatusPoller { } } } - reviewRequests(first: 20) { - nodes { requestedReviewer { ... on User { login avatarUrl } } } - } - reviews(first: 20, states: [APPROVED, CHANGES_REQUESTED, COMMENTED]) { - nodes { author { login avatarUrl } state } - } } } }` @@ -483,11 +505,10 @@ export class PRStatusPoller { ) const pr = JSON.parse(stdout)?.data?.repository?.pullRequest const comments = parseComments((pr?.reviewThreads?.nodes ?? []) as GhThread[]) - const reviewers = parseReviewers((pr?.reviewRequests?.nodes ?? []) as GhReviewRequest[], (pr?.reviews?.nodes ?? []) as GhReview[]) - return { total: comments.length, unresolved: comments.filter((c) => !c.resolved).length, comments, reviewers } + return { total: comments.length, unresolved: comments.filter((c) => !c.resolved).length, comments } } catch (err) { this.options.log("Failed to fetch PR comments:", err) - return { total: 0, unresolved: 0, comments: [], reviewers: [] } + return { total: 0, unresolved: 0, comments: [] } } } } diff --git a/packages/kilo-vscode/src/agent-manager/types.ts b/packages/kilo-vscode/src/agent-manager/types.ts index 836b50471a7..01c88ff1581 100644 --- a/packages/kilo-vscode/src/agent-manager/types.ts +++ b/packages/kilo-vscode/src/agent-manager/types.ts @@ -94,11 +94,11 @@ export interface PRStatus { pending: number checks: PRCheck[] } + reviewers: PRReviewer[] comments?: { total: number unresolved: number comments: PRComment[] - reviewers: PRReviewer[] } additions: number deletions: number 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 7bf8aef9f7b..20382b02972 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRPanel.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRPanel.tsx @@ -44,8 +44,8 @@ export const PRPanel: Component = (props) => {

- - {(comments) => } + 0}> + {(body) => } diff --git a/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx index d308663f8bf..817310e1bb9 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx @@ -613,6 +613,7 @@ const basePR: PRStatus = { state: "open", review: null, checks: { status: "success", total: 5, passed: 5, failed: 0, pending: 0, checks: [] }, + reviewers: [], additions: 978, deletions: 202, files: 12, diff --git a/packages/kilo-vscode/webview-ui/src/stories/section-header.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/section-header.stories.tsx index 23938282a84..a359eefea5f 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/section-header.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/section-header.stories.tsx @@ -593,6 +593,7 @@ export const WithPRBadges: Story = { additions: 120, deletions: 30, files: 5, + reviewers: [], checks: { status: "success", total: 5, passed: 5, failed: 0, pending: 0, checks: [] }, }} /> @@ -611,6 +612,7 @@ export const WithPRBadges: Story = { additions: 15, deletions: 8, files: 3, + reviewers: [], checks: { status: "failure", total: 5, passed: 3, failed: 2, pending: 0, checks: [] }, }} /> @@ -629,6 +631,7 @@ export const WithPRBadges: Story = { additions: 60, deletions: 12, files: 4, + reviewers: [], checks: { status: "success", total: 5, passed: 5, failed: 0, pending: 0, checks: [] }, }} /> @@ -647,6 +650,7 @@ export const WithPRBadges: Story = { additions: 200, deletions: 0, files: 8, + reviewers: [], checks: { status: "pending", total: 5, passed: 2, failed: 0, pending: 3, checks: [] }, }} /> @@ -665,6 +669,7 @@ export const WithPRBadges: Story = { additions: 30, deletions: 5, files: 2, + reviewers: [], checks: { status: "none", total: 0, passed: 0, failed: 0, pending: 0, checks: [] }, }} /> @@ -683,6 +688,7 @@ export const WithPRBadges: Story = { additions: 90, deletions: 20, files: 6, + reviewers: [], checks: { status: "success", total: 5, passed: 5, failed: 0, pending: 0, checks: [] }, }} /> @@ -701,6 +707,7 @@ export const WithPRBadges: Story = { additions: 10, deletions: 4, files: 1, + reviewers: [], checks: { status: "failure", total: 5, passed: 1, failed: 4, pending: 0, checks: [] }, }} /> diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/agent-manager.ts b/packages/kilo-vscode/webview-ui/src/types/messages/agent-manager.ts index 23c0395a07d..f76fb185c7e 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/agent-manager.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/agent-manager.ts @@ -66,11 +66,11 @@ export interface PRStatus { pending: number checks: PRCheck[] } + reviewers: PRReviewer[] comments?: { total: number unresolved: number comments: PRComment[] - reviewers: PRReviewer[] } additions: number deletions: number From c20ccad2d7fb60ad8da9ccd943c0b4934aede1b7 Mon Sep 17 00:00:00 2001 From: cmanu Date: Fri, 7 Aug 2026 10:22:18 -0700 Subject: [PATCH 32/73] fix(pr-view): close pr window if wt has not pr --- .../kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index 48d364feb36..234625e073b 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -499,6 +499,7 @@ const AgentManagerContent: Component = () => { } createEffect(on(selection, () => cancelPendingDelete(), { defer: true })) createEffect(on(selection, () => clearReviewComposer(reviewComposer), { defer: true })) + createEffect(on(selection, () => { if (!activePR()) setSidePanel((p) => p === SidePanel.PR ? null : p) }, { defer: true })) onCleanup(() => clearTimeout(pendingDeleteTimer)) // Per-context tab memory lives in the active project's store: maps sidebar From e1f064387d8a0162524fee0e60888f3a65d2c582 Mon Sep 17 00:00:00 2001 From: cmanu Date: Fri, 7 Aug 2026 10:28:23 -0700 Subject: [PATCH 33/73] feat(pr-view): move file changes --- packages/kilo-vscode/webview-ui/agent-manager/pr/PRPanel.tsx | 2 -- .../kilo-vscode/webview-ui/agent-manager/pr/PRSummary.tsx | 5 +++++ 2 files changed, 5 insertions(+), 2 deletions(-) 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 20382b02972..7e4e804371c 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRPanel.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRPanel.tsx @@ -9,7 +9,6 @@ import { PRBadge } from "./PRBadge" import { PROverview } from "./PROverview" import { PRReviewers } from "./PRReviewers" import { PRDescription } from "./PRDescription" -import { PRFileChanges } from "./PRFileChanges" import { PRChecks } from "./PRChecks" import { PRComments } from "./PRComments" import { PRSummary } from "./PRSummary" @@ -50,7 +49,6 @@ export const PRPanel: Component = (props) => { {(body) => } - 0}> diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRSummary.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRSummary.tsx index bd7dbf36d0e..5257014a3b0 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRSummary.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRSummary.tsx @@ -46,6 +46,11 @@ export function PRSummary(props: PRSummaryProps) {
PR Summary + + 0}>{props.pr.files}f + 0}>+{props.pr.additions} + 0}>−{props.pr.deletions} +
{rows().map((row) => ( From a357c7123045fd1b7b0739158c5710aa4758bece Mon Sep 17 00:00:00 2001 From: cmanu Date: Fri, 7 Aug 2026 10:36:11 -0700 Subject: [PATCH 34/73] feat(pr-view): Update pr button style --- .../kilo-vscode/webview-ui/agent-manager/TabBar.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/TabBar.tsx b/packages/kilo-vscode/webview-ui/agent-manager/TabBar.tsx index ad0461db397..3f8cba105e7 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/TabBar.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/TabBar.tsx @@ -221,13 +221,14 @@ export const TabBar: Component = (props) => ( {(pr) => ( - + /> )} From 05301da2d23114b6a09caa2e3c9a1c551e685eb2 Mon Sep 17 00:00:00 2001 From: cmanu Date: Fri, 7 Aug 2026 13:01:24 -0700 Subject: [PATCH 35/73] fix(pr-view): Formatting and update css variable name for unit test --- .../src/agent-manager/PRStatusPoller.ts | 18 +- .../src/agent-manager/am-pr-types.ts | 29 ++- .../src/agent-manager/am-pr-utils.ts | 11 +- .../src/agent-manager/pr-status-bridge.ts | 3 +- .../tests/unit/am-pr-status-bridge.test.ts | 4 +- .../tests/unit/am-pr-utils.test.ts | 172 ++++++++++++++---- .../agent-manager/AgentManagerApp.tsx | 24 ++- .../webview-ui/agent-manager/WorktreeItem.tsx | 7 +- .../agent-manager/agent-manager.css | 34 ++-- .../agent-manager/default-base-branch.tsx | 11 +- .../webview-ui/agent-manager/pr/PRBadge.tsx | 7 +- .../webview-ui/agent-manager/pr/PRChecks.tsx | 4 +- .../agent-manager/pr/PRComments.tsx | 3 +- .../agent-manager/pr/PRFileChanges.tsx | 12 +- .../agent-manager/pr/PROverview.tsx | 4 +- .../webview-ui/agent-manager/pr/PRPanel.tsx | 12 +- .../webview-ui/agent-manager/pr/PRSummary.tsx | 12 +- .../webview-ui/agent-manager/pr/pr-panel.css | 120 ++++++++---- .../webview-ui/src/context/session.tsx | 5 +- .../src/types/messages/agent-manager.ts | 19 +- 20 files changed, 396 insertions(+), 115 deletions(-) diff --git a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts index 5a2e1e51ce0..afe6a3e3854 100644 --- a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts +++ b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts @@ -449,11 +449,25 @@ export class PRStatusPoller { } }` const { stdout } = await this.gh( - ["api", "graphql", "-f", `query=${query}`, "-F", `owner=${repo.owner}`, "-F", `repo=${repo.name}`, "-F", `number=${prNumber}`], + [ + "api", + "graphql", + "-f", + `query=${query}`, + "-F", + `owner=${repo.owner}`, + "-F", + `repo=${repo.name}`, + "-F", + `number=${prNumber}`, + ], { cwd, timeout: 15_000 }, ) const pr = JSON.parse(stdout)?.data?.repository?.pullRequest - return parseReviewers((pr?.reviewRequests?.nodes ?? []) as GhReviewRequest[], (pr?.reviews?.nodes ?? []) as GhReview[]) + return parseReviewers( + (pr?.reviewRequests?.nodes ?? []) as GhReviewRequest[], + (pr?.reviews?.nodes ?? []) as GhReview[], + ) } catch { return [] } diff --git a/packages/kilo-vscode/src/agent-manager/am-pr-types.ts b/packages/kilo-vscode/src/agent-manager/am-pr-types.ts index 59f4604c96b..4cd818d7a09 100644 --- a/packages/kilo-vscode/src/agent-manager/am-pr-types.ts +++ b/packages/kilo-vscode/src/agent-manager/am-pr-types.ts @@ -2,11 +2,30 @@ import type { PRState, ReviewDecision } from "./types" // Raw shapes returned by `gh pr view --json` -export interface GhAuthor { login?: string; avatarUrl?: string } -export interface GhComment { id: string; author?: GhAuthor; body?: string; path?: string; line?: number; url?: string; createdAt?: string } -export interface GhThread { isResolved?: boolean; comments?: { nodes?: GhComment[] } } -export interface GhReviewRequest { requestedReviewer?: GhAuthor } -export interface GhReview { author?: GhAuthor; state?: string } +export interface GhAuthor { + login?: string + avatarUrl?: string +} +export interface GhComment { + id: string + author?: GhAuthor + body?: string + path?: string + line?: number + url?: string + createdAt?: string +} +export interface GhThread { + isResolved?: boolean + comments?: { nodes?: GhComment[] } +} +export interface GhReviewRequest { + requestedReviewer?: GhAuthor +} +export interface GhReview { + author?: GhAuthor + state?: string +} export interface PRResult { number: number diff --git a/packages/kilo-vscode/src/agent-manager/am-pr-utils.ts b/packages/kilo-vscode/src/agent-manager/am-pr-utils.ts index 25291c2a5db..9cf563d0aef 100644 --- a/packages/kilo-vscode/src/agent-manager/am-pr-utils.ts +++ b/packages/kilo-vscode/src/agent-manager/am-pr-utils.ts @@ -7,10 +7,13 @@ export function parsePRResult(json: string): PRResult | null { const state = data.isDraft ? "draft" : (data.state?.toLowerCase() ?? "open") const decision = data.reviewDecision as string | undefined const review = - decision === "APPROVED" ? "approved" - : decision === "CHANGES_REQUESTED" ? "changes_requested" - : decision === "REVIEW_REQUIRED" ? "pending" - : null + decision === "APPROVED" + ? "approved" + : decision === "CHANGES_REQUESTED" + ? "changes_requested" + : decision === "REVIEW_REQUIRED" + ? "pending" + : null return { number: data.number, title: data.title ?? "", diff --git a/packages/kilo-vscode/src/agent-manager/pr-status-bridge.ts b/packages/kilo-vscode/src/agent-manager/pr-status-bridge.ts index 6eb742fbd4e..af496281966 100644 --- a/packages/kilo-vscode/src/agent-manager/pr-status-bridge.ts +++ b/packages/kilo-vscode/src/agent-manager/pr-status-bridge.ts @@ -128,8 +128,7 @@ function bridgePollerOpts(bridge: PRStatusBridge, host: PRBridgeHost) { // Always forward auth/missing errors so the webview can show a toast, // regardless of whether prior data exists. Deduplicate per error type // so multiple failing worktrees don't produce multiple toasts. - if (err === "gh_auth" || err === "gh_missing") - bridge.notifyError(err) + if (err === "gh_auth" || err === "gh_missing") bridge.notifyError(err) return } const msg = { type: "agentManager.prStatus", worktreeId: id, pr, error: err } as AgentManagerOutMessage diff --git a/packages/kilo-vscode/tests/unit/am-pr-status-bridge.test.ts b/packages/kilo-vscode/tests/unit/am-pr-status-bridge.test.ts index 4752844023e..734b3128f6e 100644 --- a/packages/kilo-vscode/tests/unit/am-pr-status-bridge.test.ts +++ b/packages/kilo-vscode/tests/unit/am-pr-status-bridge.test.ts @@ -122,7 +122,9 @@ describe("PRStatusBridge.replay", () => { onStatus("wt1", null, "gh_auth") sent.length = 0 bridge.replay() - expect(sent.some((m) => m.type === "agentManager.prError" && (m as never as { error: string }).error === "gh_auth")).toBe(true) + expect( + sent.some((m) => m.type === "agentManager.prError" && (m as never as { error: string }).error === "gh_auth"), + ).toBe(true) }) it("does not replay fetch_failed errors", () => { diff --git a/packages/kilo-vscode/tests/unit/am-pr-utils.test.ts b/packages/kilo-vscode/tests/unit/am-pr-utils.test.ts index e16ab9c259d..ba2d8d80070 100644 --- a/packages/kilo-vscode/tests/unit/am-pr-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/am-pr-utils.test.ts @@ -16,7 +16,18 @@ describe("parsePRResult", () => { }) it("parses an open PR", () => { - const raw = { number: 42, title: "my PR", body: "desc", url: "https://github.com/x/y/pull/42", state: "OPEN", isDraft: false, reviewDecision: null, additions: 10, deletions: 3, changedFiles: 2 } + const raw = { + number: 42, + title: "my PR", + body: "desc", + url: "https://github.com/x/y/pull/42", + state: "OPEN", + isDraft: false, + reviewDecision: null, + additions: 10, + deletions: 3, + changedFiles: 2, + } expect(parsePRResult(JSON.stringify(raw))).toEqual({ number: 42, title: "my PR", @@ -31,43 +42,122 @@ describe("parsePRResult", () => { }) it("maps isDraft to draft state regardless of gh state field", () => { - const raw = { number: 1, title: "", body: "", url: "", state: "OPEN", isDraft: true, reviewDecision: null, additions: 0, deletions: 0, changedFiles: 0 } + const raw = { + number: 1, + title: "", + body: "", + url: "", + state: "OPEN", + isDraft: true, + reviewDecision: null, + additions: 0, + deletions: 0, + changedFiles: 0, + } expect(parsePRResult(JSON.stringify(raw))?.state).toBe("draft") }) it("maps MERGED state", () => { - const raw = { number: 1, title: "", body: "", url: "", state: "MERGED", isDraft: false, reviewDecision: null, additions: 0, deletions: 0, changedFiles: 0 } + const raw = { + number: 1, + title: "", + body: "", + url: "", + state: "MERGED", + isDraft: false, + reviewDecision: null, + additions: 0, + deletions: 0, + changedFiles: 0, + } expect(parsePRResult(JSON.stringify(raw))?.state).toBe("merged") }) it("maps CLOSED state", () => { - const raw = { number: 1, title: "", body: "", url: "", state: "CLOSED", isDraft: false, reviewDecision: null, additions: 0, deletions: 0, changedFiles: 0 } + const raw = { + number: 1, + title: "", + body: "", + url: "", + state: "CLOSED", + isDraft: false, + reviewDecision: null, + additions: 0, + deletions: 0, + changedFiles: 0, + } expect(parsePRResult(JSON.stringify(raw))?.state).toBe("closed") }) it("maps APPROVED review decision", () => { - const raw = { number: 1, title: "", body: "", url: "", state: "OPEN", isDraft: false, reviewDecision: "APPROVED", additions: 0, deletions: 0, changedFiles: 0 } + const raw = { + number: 1, + title: "", + body: "", + url: "", + state: "OPEN", + isDraft: false, + reviewDecision: "APPROVED", + additions: 0, + deletions: 0, + changedFiles: 0, + } expect(parsePRResult(JSON.stringify(raw))?.review).toBe("approved") }) it("maps CHANGES_REQUESTED review decision", () => { - const raw = { number: 1, title: "", body: "", url: "", state: "OPEN", isDraft: false, reviewDecision: "CHANGES_REQUESTED", additions: 0, deletions: 0, changedFiles: 0 } + const raw = { + number: 1, + title: "", + body: "", + url: "", + state: "OPEN", + isDraft: false, + reviewDecision: "CHANGES_REQUESTED", + additions: 0, + deletions: 0, + changedFiles: 0, + } expect(parsePRResult(JSON.stringify(raw))?.review).toBe("changes_requested") }) it("maps REVIEW_REQUIRED review decision to pending", () => { - const raw = { number: 1, title: "", body: "", url: "", state: "OPEN", isDraft: false, reviewDecision: "REVIEW_REQUIRED", additions: 0, deletions: 0, changedFiles: 0 } + const raw = { + number: 1, + title: "", + body: "", + url: "", + state: "OPEN", + isDraft: false, + reviewDecision: "REVIEW_REQUIRED", + additions: 0, + deletions: 0, + changedFiles: 0, + } expect(parsePRResult(JSON.stringify(raw))?.review).toBe("pending") }) it("returns null review for unknown decision", () => { - const raw = { number: 1, title: "", body: "", url: "", state: "OPEN", isDraft: false, reviewDecision: "SOMETHING_ELSE", additions: 0, deletions: 0, changedFiles: 0 } + const raw = { + number: 1, + title: "", + body: "", + url: "", + state: "OPEN", + isDraft: false, + reviewDecision: "SOMETHING_ELSE", + additions: 0, + deletions: 0, + changedFiles: 0, + } expect(parsePRResult(JSON.stringify(raw))?.review).toBeNull() }) it("defaults missing fields to empty strings and zeros", () => { const result = parsePRResult(JSON.stringify({ number: 5 })) - expect(result).toEqual(expect.objectContaining({ title: "", body: "", url: "", additions: 0, deletions: 0, files: 0 })) + expect(result).toEqual( + expect.objectContaining({ title: "", body: "", url: "", additions: 0, deletions: 0, files: 0 }), + ) }) }) @@ -128,21 +218,37 @@ describe("parseComments", () => { }) it("parses a resolved thread", () => { - const threads: GhThread[] = [{ - isResolved: true, - comments: { nodes: [{ id: "c1", author: { login: "alice", avatarUrl: "https://avatar" }, body: "looks good", path: "src/foo.ts", line: 10, url: "https://url", createdAt: "2024-01-01T00:00:00Z" }] }, - }] - expect(parseComments(threads)).toEqual([{ - id: "c1", - author: "alice", - avatar: "https://avatar", - body: "looks good", - file: "src/foo.ts", - line: 10, - url: "https://url", - resolved: true, - createdAt: new Date("2024-01-01T00:00:00Z").getTime(), - }]) + const threads: GhThread[] = [ + { + isResolved: true, + comments: { + nodes: [ + { + id: "c1", + author: { login: "alice", avatarUrl: "https://avatar" }, + body: "looks good", + path: "src/foo.ts", + line: 10, + url: "https://url", + createdAt: "2024-01-01T00:00:00Z", + }, + ], + }, + }, + ] + expect(parseComments(threads)).toEqual([ + { + id: "c1", + author: "alice", + avatar: "https://avatar", + body: "looks good", + file: "src/foo.ts", + line: 10, + url: "https://url", + resolved: true, + createdAt: new Date("2024-01-01T00:00:00Z").getTime(), + }, + ]) }) it("defaults missing author to 'unknown'", () => { @@ -151,13 +257,17 @@ describe("parseComments", () => { }) it("only uses the first comment of each thread", () => { - const threads: GhThread[] = [{ - isResolved: false, - comments: { nodes: [ - { id: "first", body: "first comment" }, - { id: "second", body: "second comment" }, - ]}, - }] + const threads: GhThread[] = [ + { + isResolved: false, + comments: { + nodes: [ + { id: "first", body: "first comment" }, + { id: "second", body: "second comment" }, + ], + }, + }, + ] const result = parseComments(threads) expect(result).toHaveLength(1) expect(result[0]?.id).toBe("first") diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index 234625e073b..2dad2ea0209 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -499,7 +499,15 @@ const AgentManagerContent: Component = () => { } createEffect(on(selection, () => cancelPendingDelete(), { defer: true })) createEffect(on(selection, () => clearReviewComposer(reviewComposer), { defer: true })) - createEffect(on(selection, () => { if (!activePR()) setSidePanel((p) => p === SidePanel.PR ? null : p) }, { defer: true })) + createEffect( + on( + selection, + () => { + if (!activePR()) setSidePanel((p) => (p === SidePanel.PR ? null : p)) + }, + { defer: true }, + ), + ) onCleanup(() => clearTimeout(pendingDeleteTimer)) // Per-context tab memory lives in the active project's store: maps sidebar @@ -1581,7 +1589,11 @@ const AgentManagerContent: Component = () => { if (msg.type === "agentManager.prError") { const ev = msg as AgentManagerPRErrorMessage - showToast({ variant: "error", title: t(`agentManager.pr.error.${ev.error}.title`), description: t(`agentManager.pr.error.${ev.error}.description`) }) + showToast({ + variant: "error", + title: t(`agentManager.pr.error.${ev.error}.title`), + description: t(`agentManager.pr.error.${ev.error}.description`), + }) } if (projectLive.apply(msg)) return @@ -2656,7 +2668,13 @@ const AgentManagerContent: Component = () => { pr={data.pr} worktree={data.wt} onClose={() => setSidePanel(null)} - onOpenExternal={() => vscode.postMessage({ type: "agentManager.openPR", worktreeId: data.selected, url: data.pr.url })} + onOpenExternal={() => + vscode.postMessage({ + type: "agentManager.openPR", + worktreeId: data.selected, + url: data.pr.url, + }) + } /> ) })()} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx b/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx index 2b07b00269c..c44b767cffd 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx @@ -360,7 +360,12 @@ export const WorktreeItem: Component = (props) => { #{pr().number} - + diff --git a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css index 332fe65426d..5017d37a669 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css @@ -907,15 +907,25 @@ html[data-theme="kilo-vscode"] /* PR badge accent colors via classList */ .am-pr-accent-open, -.am-pr-panel-badge.am-pr-accent-open { --pr-accent: var(--vscode-testing-iconPassed, #34d399); } +.am-pr-panel-badge.am-pr-accent-open { + --am-pr-accent: var(--vscode-testing-iconPassed, #34d399); +} .am-pr-accent-pending, -.am-pr-panel-badge.am-pr-accent-pending { --pr-accent: var(--vscode-charts-yellow, #fbbf24); } +.am-pr-panel-badge.am-pr-accent-pending { + --am-pr-accent: var(--vscode-charts-yellow, #fbbf24); +} .am-pr-accent-draft, -.am-pr-panel-badge.am-pr-accent-draft { --pr-accent: var(--text-weaker); } +.am-pr-panel-badge.am-pr-accent-draft { + --am-pr-accent: var(--text-weaker); +} .am-pr-accent-merged, -.am-pr-panel-badge.am-pr-accent-merged { --pr-accent: var(--vscode-charts-purple, var(--vscode-foreground)); } +.am-pr-panel-badge.am-pr-accent-merged { + --am-pr-accent: var(--vscode-charts-purple, var(--vscode-foreground)); +} .am-pr-accent-closed, -.am-pr-panel-badge.am-pr-accent-closed { --pr-accent: var(--vscode-testing-iconFailed, #f87171); } +.am-pr-panel-badge.am-pr-accent-closed { + --am-pr-accent: var(--vscode-testing-iconFailed, #f87171); +} .am-pr-badge { display: inline-flex; @@ -924,8 +934,8 @@ html[data-theme="kilo-vscode"] padding: 2px 4px; border-radius: 6px; border: none; - background: color-mix(in srgb, var(--pr-accent) 12%, transparent); - color: var(--pr-accent); + background: color-mix(in srgb, var(--am-pr-accent) 12%, transparent); + color: var(--am-pr-accent); cursor: pointer; font-size: var(--kilo-font-size-11); font-weight: 500; @@ -934,19 +944,19 @@ html[data-theme="kilo-vscode"] margin-left: auto; } .am-pr-badge:hover { - background: color-mix(in srgb, var(--pr-accent) 25%, transparent); + background: color-mix(in srgb, var(--am-pr-accent) 25%, transparent); } .am-pr-badge [data-component="icon"] { width: 12px; height: 12px; - color: var(--pr-accent); + color: var(--am-pr-accent); } .am-pr-badge-number { line-height: 1; color: var(--text-muted); } .am-pr-badge:hover .am-pr-badge-number { - color: var(--pr-accent); + color: var(--am-pr-accent); } /* Status icon overlaid on the badge — colored by CI/review result, independent of the badge's PR-state accent color. */ @@ -982,10 +992,10 @@ html[data-theme="kilo-vscode"] } .am-worktree-item-active .am-pr-badge { - background: color-mix(in srgb, var(--pr-accent) 18%, transparent); + background: color-mix(in srgb, var(--am-pr-accent) 18%, transparent); } .am-worktree-item-active .am-pr-badge:hover { - background: color-mix(in srgb, var(--pr-accent) 30%, transparent); + background: color-mix(in srgb, var(--am-pr-accent) 30%, transparent); } /* Clickable link icon in hover card to open PR on GitHub */ diff --git a/packages/kilo-vscode/webview-ui/agent-manager/default-base-branch.tsx b/packages/kilo-vscode/webview-ui/agent-manager/default-base-branch.tsx index 53c9b77443d..421c17c8bf3 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/default-base-branch.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/default-base-branch.tsx @@ -21,7 +21,16 @@ interface Deps { } export function createChangeDefaultBaseBranch(deps: Deps) { - const { vscode, dialog, t, defaultBaseBranch, setDefaultBaseBranch, setRepoDetectedBranch, repoDetectedBranch, hasConfiguredBranch } = deps + const { + vscode, + dialog, + t, + defaultBaseBranch, + setDefaultBaseBranch, + setRepoDetectedBranch, + repoDetectedBranch, + hasConfiguredBranch, + } = deps return () => { const [search, setSearch] = createSignal("") diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRBadge.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRBadge.tsx index d77213de726..55bdea2165a 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRBadge.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRBadge.tsx @@ -26,12 +26,7 @@ export function PRBadge(props: { pr: PRStatus }) { > - + ) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRChecks.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRChecks.tsx index 235124dc720..b010d221f71 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRChecks.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRChecks.tsx @@ -34,7 +34,9 @@ export function PRChecks(props: { checks: PRStatus["checks"] }) { {check.name} {CHECK[check.status].label} - {check.duration} + + {check.duration} + e.preventDefault()}> diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRComments.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRComments.tsx index 6745a0522c3..b73c915f02d 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRComments.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRComments.tsx @@ -28,7 +28,8 @@ export function PRComments(props: { comments: NonNullable {comment.author} - {comment.file}{`:${comment.line}`} + {comment.file} + {`:${comment.line}`} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRFileChanges.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRFileChanges.tsx index b4e558d67b0..971948d8e28 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRFileChanges.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRFileChanges.tsx @@ -9,9 +9,15 @@ export function PRFileChanges(props: { files: number; additions: number; deletio
File Changes - 0}>{props.files}f - 0}>+{props.additions} - 0}>−{props.deletions} + 0}> + {props.files}f + + 0}> + +{props.additions} + + 0}> + −{props.deletions} +
diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/PROverview.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PROverview.tsx index 6c1fc863739..577dd4a8e7d 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/PROverview.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PROverview.tsx @@ -34,7 +34,9 @@ export function PROverview(props: { pr: PRStatus; worktree?: WorktreeState }) {
Status - {STATE_LABEL[props.pr.state]} + + {STATE_LABEL[props.pr.state]} +
{(review) => ( 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 7e4e804371c..c0241f05120 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRPanel.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRPanel.tsx @@ -33,7 +33,13 @@ export const PRPanel: Component = (props) => {
- + @@ -46,9 +52,7 @@ export const PRPanel: Component = (props) => { 0}> - - {(body) => } - + {(body) => } 0}> diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRSummary.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRSummary.tsx index 5257014a3b0..48d9d2ee2e8 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRSummary.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRSummary.tsx @@ -47,9 +47,15 @@ export function PRSummary(props: PRSummaryProps) {
PR Summary - 0}>{props.pr.files}f - 0}>+{props.pr.additions} - 0}>−{props.pr.deletions} + 0}> + {props.pr.files}f + + 0}> + +{props.pr.additions} + + 0}> + −{props.pr.deletions} +
diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-panel.css b/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-panel.css index 6f1cc8cc260..e505989acee 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-panel.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-panel.css @@ -4,8 +4,14 @@ overflow: hidden; } -.am-pr-row { display: flex; align-items: center; } -.am-pr-col { display: flex; flex-direction: column; } +.am-pr-row { + display: flex; + align-items: center; +} +.am-pr-col { + display: flex; + flex-direction: column; +} .am-pr-panel-header { align-items: flex-start; @@ -29,9 +35,9 @@ border-radius: 4px; font-size: var(--kilo-font-size-10); font-variant-numeric: tabular-nums; - background: color-mix(in srgb, var(--pr-accent) 15%, transparent); - color: var(--pr-accent); - border: 1px solid color-mix(in srgb, var(--pr-accent) 40%, transparent); + background: color-mix(in srgb, var(--am-pr-accent) 15%, transparent); + color: var(--am-pr-accent); + border: 1px solid color-mix(in srgb, var(--am-pr-accent) 40%, transparent); white-space: nowrap; flex-shrink: 0; } @@ -121,9 +127,15 @@ } /* Check count color by status */ -.am-pr-checks-count-success { color: var(--vscode-testing-iconPassed, #34d399); } -.am-pr-checks-count-failure { color: var(--vscode-testing-iconFailed, #f87171); } -.am-pr-checks-count-pending { color: var(--vscode-charts-yellow, #fbbf24); } +.am-pr-checks-count-success { + color: var(--vscode-testing-iconPassed, #34d399); +} +.am-pr-checks-count-failure { + color: var(--vscode-testing-iconFailed, #f87171); +} +.am-pr-checks-count-pending { + color: var(--vscode-charts-yellow, #fbbf24); +} .am-pr-panel-section-count { font-size: var(--kilo-font-size-11); @@ -161,14 +173,28 @@ text-align: right; } -.am-pr-panel-value[data-pr-state="open"] { color: var(--vscode-testing-iconPassed, #34d399); } -.am-pr-panel-value[data-pr-state="merged"] { color: var(--vscode-charts-purple, #a78bfa); } -.am-pr-panel-value[data-pr-state="closed"] { color: var(--vscode-testing-iconFailed, #f87171); } -.am-pr-panel-value[data-pr-state="draft"] { color: var(--text-weaker); } +.am-pr-panel-value[data-pr-state="open"] { + color: var(--vscode-testing-iconPassed, #34d399); +} +.am-pr-panel-value[data-pr-state="merged"] { + color: var(--vscode-charts-purple, #a78bfa); +} +.am-pr-panel-value[data-pr-state="closed"] { + color: var(--vscode-testing-iconFailed, #f87171); +} +.am-pr-panel-value[data-pr-state="draft"] { + color: var(--text-weaker); +} -.am-pr-panel-value[data-checks="failure"] { color: var(--vscode-testing-iconFailed, #f87171); } -.am-pr-panel-value[data-checks="pending"] { color: var(--vscode-charts-yellow, #fbbf24); } -.am-pr-panel-value[data-checks="success"] { color: var(--vscode-testing-iconPassed, #34d399); } +.am-pr-panel-value[data-checks="failure"] { + color: var(--vscode-testing-iconFailed, #f87171); +} +.am-pr-panel-value[data-checks="pending"] { + color: var(--vscode-charts-yellow, #fbbf24); +} +.am-pr-panel-value[data-checks="success"] { + color: var(--vscode-testing-iconPassed, #34d399); +} .am-pr-panel-diff { gap: 6px; @@ -257,9 +283,15 @@ margin-bottom: 0.25em; } -.am-pr-panel-description [data-component="markdown"] h1 { font-size: var(--kilo-font-size-16); } -.am-pr-panel-description [data-component="markdown"] h2 { font-size: var(--kilo-font-size-14); } -.am-pr-panel-description [data-component="markdown"] h3 { font-size: var(--kilo-font-size-13); } +.am-pr-panel-description [data-component="markdown"] h1 { + font-size: var(--kilo-font-size-16); +} +.am-pr-panel-description [data-component="markdown"] h2 { + font-size: var(--kilo-font-size-14); +} +.am-pr-panel-description [data-component="markdown"] h3 { + font-size: var(--kilo-font-size-13); +} /* PR Reviewers */ .am-pr-panel-reviewers { @@ -276,10 +308,18 @@ flex-shrink: 0; } -.am-pr-panel-reviewer[data-state="approved"] .am-pr-reviewer-icon { color: var(--vscode-testing-iconPassed, #34d399); } -.am-pr-panel-reviewer[data-state="changes_requested"] .am-pr-reviewer-icon { color: var(--vscode-charts-yellow, #fbbf24); } -.am-pr-panel-reviewer[data-state="commented"] .am-pr-reviewer-icon { color: var(--text-weak); } -.am-pr-panel-reviewer[data-state="pending"] .am-pr-reviewer-icon { color: var(--text-weaker); } +.am-pr-panel-reviewer[data-state="approved"] .am-pr-reviewer-icon { + color: var(--vscode-testing-iconPassed, #34d399); +} +.am-pr-panel-reviewer[data-state="changes_requested"] .am-pr-reviewer-icon { + color: var(--vscode-charts-yellow, #fbbf24); +} +.am-pr-panel-reviewer[data-state="commented"] .am-pr-reviewer-icon { + color: var(--text-weak); +} +.am-pr-panel-reviewer[data-state="pending"] .am-pr-reviewer-icon { + color: var(--text-weaker); +} .am-pr-reviewer-login { flex: 1; @@ -306,11 +346,21 @@ flex-shrink: 0; } -.am-pr-panel-check-item[data-status="success"] .am-pr-check-icon { color: var(--vscode-testing-iconPassed, #34d399); } -.am-pr-panel-check-item[data-status="failure"] .am-pr-check-icon { color: var(--vscode-testing-iconFailed, #f87171); } -.am-pr-panel-check-item[data-status="cancelled"] .am-pr-check-icon { color: var(--text-weaker); } -.am-pr-panel-check-item[data-status="skipped"] .am-pr-check-icon { color: var(--text-weaker); } -.am-pr-panel-check-item[data-status="pending"] .am-pr-check-icon { color: var(--vscode-charts-yellow, #fbbf24); } +.am-pr-panel-check-item[data-status="success"] .am-pr-check-icon { + color: var(--vscode-testing-iconPassed, #34d399); +} +.am-pr-panel-check-item[data-status="failure"] .am-pr-check-icon { + color: var(--vscode-testing-iconFailed, #f87171); +} +.am-pr-panel-check-item[data-status="cancelled"] .am-pr-check-icon { + color: var(--text-weaker); +} +.am-pr-panel-check-item[data-status="skipped"] .am-pr-check-icon { + color: var(--text-weaker); +} +.am-pr-panel-check-item[data-status="pending"] .am-pr-check-icon { + color: var(--vscode-charts-yellow, #fbbf24); +} .am-pr-check-name { flex: 1; @@ -389,10 +439,18 @@ flex-shrink: 0; } -.am-pr-summary-row[data-status="success"] .am-pr-summary-icon { color: var(--vscode-testing-iconPassed, #34d399); } -.am-pr-summary-row[data-status="failure"] .am-pr-summary-icon { color: var(--vscode-testing-iconFailed, #f87171); } -.am-pr-summary-row[data-status="pending"] .am-pr-summary-icon { color: var(--vscode-charts-yellow, #fbbf24); } -.am-pr-summary-row[data-status="warning"] .am-pr-summary-icon { color: var(--vscode-charts-yellow, #fbbf24); } +.am-pr-summary-row[data-status="success"] .am-pr-summary-icon { + color: var(--vscode-testing-iconPassed, #34d399); +} +.am-pr-summary-row[data-status="failure"] .am-pr-summary-icon { + color: var(--vscode-testing-iconFailed, #f87171); +} +.am-pr-summary-row[data-status="pending"] .am-pr-summary-icon { + color: var(--vscode-charts-yellow, #fbbf24); +} +.am-pr-summary-row[data-status="warning"] .am-pr-summary-icon { + color: var(--vscode-charts-yellow, #fbbf24); +} .am-pr-summary-label { color: var(--vscode-foreground); diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx index d0402d48e82..80435d4cc70 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -1453,7 +1453,10 @@ export const SessionProvider: ParentComponent = (props) => { // Reconcile fast-path: if the tail matches local state shape-wise, every // message+part-count already agrees with the server. Skip the reactive // store churn entirely — virtualizer and rendering stay untouched. - if (mode === "reconcile" && sameReconcileShape(store.messages[sessionID] ?? [], messages, (id) => store.parts[id])) { + if ( + mode === "reconcile" && + sameReconcileShape(store.messages[sessionID] ?? [], messages, (id) => store.parts[id]) + ) { const parts = messageParts(messages) for (const msg of messages) { if (store.parts[msg.id]) delete parts[msg.id] diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/agent-manager.ts b/packages/kilo-vscode/webview-ui/src/types/messages/agent-manager.ts index f76fb185c7e..4ccb83c0745 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/agent-manager.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/agent-manager.ts @@ -48,8 +48,23 @@ export interface SectionState { // PR status types — sub-types live in agent-manager/pr/pr-types.ts // --------------------------------------------------------------------------- -import type { PRState, ReviewDecision, AggregateCheckStatus, PRCheck, PRComment, PRReviewer } from "../../../agent-manager/pr/pr-types" -export type { PRState, ReviewDecision, CheckStatus, AggregateCheckStatus, PRCheck, PRComment, PRReviewer } from "../../../agent-manager/pr/pr-types" +import type { + PRState, + ReviewDecision, + AggregateCheckStatus, + PRCheck, + PRComment, + PRReviewer, +} from "../../../agent-manager/pr/pr-types" +export type { + PRState, + ReviewDecision, + CheckStatus, + AggregateCheckStatus, + PRCheck, + PRComment, + PRReviewer, +} from "../../../agent-manager/pr/pr-types" export interface PRStatus { number: number From db1331511a7045998aa025deeae6a77ece10157c Mon Sep 17 00:00:00 2001 From: cmanu Date: Fri, 7 Aug 2026 13:01:44 -0700 Subject: [PATCH 36/73] feat(pr-view): i18n for agent manager pr messaging --- .../webview-ui/agent-manager/i18n/ar.ts | 11 ++++++++--- .../webview-ui/agent-manager/i18n/br.ts | 11 ++++++++--- .../webview-ui/agent-manager/i18n/bs.ts | 13 +++++++++---- .../webview-ui/agent-manager/i18n/da.ts | 15 ++++++++++----- .../webview-ui/agent-manager/i18n/de.ts | 14 ++++++++++---- .../webview-ui/agent-manager/i18n/es.ts | 14 ++++++++++---- .../webview-ui/agent-manager/i18n/fa.ts | 11 ++++++++--- .../webview-ui/agent-manager/i18n/fr.ts | 14 ++++++++++---- .../webview-ui/agent-manager/i18n/it.ts | 8 +++++++- .../webview-ui/agent-manager/i18n/ja.ts | 15 +++++++++++---- .../webview-ui/agent-manager/i18n/ko.ts | 13 +++++++++---- .../webview-ui/agent-manager/i18n/nl.ts | 9 +++++++-- .../webview-ui/agent-manager/i18n/no.ts | 15 ++++++++++----- .../webview-ui/agent-manager/i18n/pl.ts | 11 ++++++++--- .../webview-ui/agent-manager/i18n/ru.ts | 11 ++++++++--- .../webview-ui/agent-manager/i18n/th.ts | 15 ++++++++++----- .../webview-ui/agent-manager/i18n/tr.ts | 8 +++++++- .../webview-ui/agent-manager/i18n/uk.ts | 7 ++++++- .../webview-ui/agent-manager/i18n/zh.ts | 13 +++++++++---- .../webview-ui/agent-manager/i18n/zht.ts | 13 +++++++++---- 20 files changed, 174 insertions(+), 67 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts index 726e132d709..ae7aeceab7f 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts @@ -14,7 +14,7 @@ export const dict = { "agentManager.worktree.new": "Worktree جديد", "agentManager.worktree.setupScript": "سكربت إعداد Worktree", "agentManager.worktree.delete": "حذف Worktree", - "agentManager.worktree.confirmDelete": "Delete?", + "agentManager.worktree.confirmDelete": "حذف؟", "agentManager.worktree.stale": "قديم", "agentManager.worktree.staleTooltip": "مفقود على القرص أو لم يعد git worktree يتتبعه", "agentManager.worktree.removeStale": "إزالة Worktree القديم", @@ -104,8 +104,8 @@ export const dict = { "agentManager.run.options": "خيارات التشغيل", "agentManager.run.configure": "تكوين سكربت التشغيل", "agentManager.shortcuts.openAgentManager": "فتح Agent Manager", - "agentManager.shortcuts.cycleAgentMode": "Next agent mode", - "agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode", + "agentManager.shortcuts.cycleAgentMode": "وضع الوكيل التالي", + "agentManager.shortcuts.cyclePreviousAgentMode": "وضع الوكيل السابق", "agentManager.shortcuts.showShortcuts": "إظهار اختصارات لوحة المفاتيح", "agentManager.dialog.removeStaleWorktree.title": "إزالة Worktree القديم", "agentManager.dialog.removeStaleWorktree.messagePre": "إزالة Worktree القديم ", @@ -211,4 +211,9 @@ export const dict = { "agentManager.import.noBranchesFound": "لم يتم العثور على فروع.", "agentManager.import.noBranchesHint": "الصق رابط PR أعلاه أو أنشئ Worktree جديدًا.", "agentManager.import.failed": "فشل الاستيراد", + + "agentManager.pr.error.gh_auth.title": "مصادقة GitHub مطلوبة", + "agentManager.pr.error.gh_auth.description": "شغّل 'gh auth login' في الطرفية لاستعادة حالة PR.", + "agentManager.pr.error.gh_missing.title": "لم يتم تثبيت GitHub CLI", + "agentManager.pr.error.gh_missing.description": "قم بتثبيت GitHub CLI لتفعيل حالة PR.", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts index 8ff91cfdff6..843a6c86e4b 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts @@ -14,7 +14,7 @@ export const dict = { "agentManager.worktree.new": "Novo Worktree", "agentManager.worktree.setupScript": "Script de configuração do Worktree", "agentManager.worktree.delete": "Excluir Worktree", - "agentManager.worktree.confirmDelete": "Delete?", + "agentManager.worktree.confirmDelete": "Excluir?", "agentManager.worktree.stale": "Obsoleto", "agentManager.worktree.staleTooltip": "Ausente no disco ou não é mais rastreado pelo git worktree", "agentManager.worktree.removeStale": "Remover Worktree obsoleto", @@ -106,8 +106,8 @@ export const dict = { "agentManager.run.options": "Opções de execução", "agentManager.run.configure": "Configurar script de execução", "agentManager.shortcuts.openAgentManager": "Abrir Agent Manager", - "agentManager.shortcuts.cycleAgentMode": "Next agent mode", - "agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode", + "agentManager.shortcuts.cycleAgentMode": "Próximo modo de agente", + "agentManager.shortcuts.cyclePreviousAgentMode": "Modo de agente anterior", "agentManager.shortcuts.showShortcuts": "Mostrar atalhos de teclado", "agentManager.dialog.removeStaleWorktree.title": "Remover Worktree obsoleto", "agentManager.dialog.removeStaleWorktree.messagePre": "Remover Worktree obsoleto ", @@ -215,4 +215,9 @@ export const dict = { "agentManager.import.noBranchesFound": "Nenhum branch encontrado.", "agentManager.import.noBranchesHint": "Cole uma URL de PR acima ou crie um novo Worktree.", "agentManager.import.failed": "Falha na importação", + + "agentManager.pr.error.gh_auth.title": "Autenticação do GitHub necessária", + "agentManager.pr.error.gh_auth.description": "Execute 'gh auth login' no terminal para restaurar o status do PR.", + "agentManager.pr.error.gh_missing.title": "GitHub CLI não instalado", + "agentManager.pr.error.gh_missing.description": "Instale o GitHub CLI para habilitar o status do PR.", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts index 10cd591c258..7ff68e5a77b 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts @@ -14,7 +14,7 @@ export const dict = { "agentManager.worktree.new": "Novi Worktree", "agentManager.worktree.setupScript": "Skripta za postavljanje Worktree-a", "agentManager.worktree.delete": "Obriši Worktree", - "agentManager.worktree.confirmDelete": "Delete?", + "agentManager.worktree.confirmDelete": "Izbrisati?", "agentManager.worktree.stale": "Zastario", "agentManager.worktree.staleTooltip": "Nedostaje na disku ili ga git worktree više ne prati", "agentManager.worktree.removeStale": "Ukloni zastarjeli Worktree", @@ -106,8 +106,8 @@ export const dict = { "agentManager.run.options": "Opcije pokretanja", "agentManager.run.configure": "Konfiguriši skriptu za pokretanje", "agentManager.shortcuts.openAgentManager": "Otvori Agent Manager", - "agentManager.shortcuts.cycleAgentMode": "Next agent mode", - "agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode", + "agentManager.shortcuts.cycleAgentMode": "Sljedeći način rada agenta", + "agentManager.shortcuts.cyclePreviousAgentMode": "Prethodni način rada agenta", "agentManager.shortcuts.showShortcuts": "Prikaži prečice na tastaturi", "agentManager.dialog.removeStaleWorktree.title": "Ukloni zastarjeli Worktree", "agentManager.dialog.removeStaleWorktree.messagePre": "Ukloni zastarjeli Worktree ", @@ -203,7 +203,7 @@ export const dict = { "agentManager.review.imageUnavailable": "Pregled slike nije dostupan za ovaj snimak sesije.", "agentManager.review.endOfLongDiff": "Došli ste do kraja!", - "agentManager.import.pullRequest": "Pull Request", + "agentManager.import.pullRequest": "Zahtjev za povlačenje", "agentManager.import.pastePrUrl": "Zalijepite PR URL...", "agentManager.import.open": "Otvori", "agentManager.import.branches": "Branchevi", @@ -214,4 +214,9 @@ export const dict = { "agentManager.import.noBranchesFound": "Nisu pronađeni branchevi.", "agentManager.import.noBranchesHint": "Zalijepite PR URL iznad ili kreirajte novi Worktree.", "agentManager.import.failed": "Uvoz neuspješan", + + "agentManager.pr.error.gh_auth.title": "Potrebna GitHub autentikacija", + "agentManager.pr.error.gh_auth.description": "Pokrenite 'gh auth login' u terminalu da vratite status PR-a.", + "agentManager.pr.error.gh_missing.title": "GitHub CLI nije instaliran", + "agentManager.pr.error.gh_missing.description": "Instalirajte GitHub CLI da omogućite status PR-a.", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts index d715e408088..d89a28452a7 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts @@ -14,7 +14,7 @@ export const dict = { "agentManager.worktree.new": "Nyt Worktree", "agentManager.worktree.setupScript": "Worktree-opsætningsscript", "agentManager.worktree.delete": "Slet Worktree", - "agentManager.worktree.confirmDelete": "Delete?", + "agentManager.worktree.confirmDelete": "Slet?", "agentManager.worktree.stale": "Forældet", "agentManager.worktree.staleTooltip": "Mangler på disken eller spores ikke længere af git worktree", "agentManager.worktree.removeStale": "Fjern forældet Worktree", @@ -107,8 +107,8 @@ export const dict = { "agentManager.run.options": "Kørselsindstillinger", "agentManager.run.configure": "Konfigurer kørselsscript", "agentManager.shortcuts.openAgentManager": "Åbn Agent Manager", - "agentManager.shortcuts.cycleAgentMode": "Next agent mode", - "agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode", + "agentManager.shortcuts.cycleAgentMode": "Næste agenttilstand", + "agentManager.shortcuts.cyclePreviousAgentMode": "Forrige agenttilstand", "agentManager.shortcuts.showShortcuts": "Vis tastaturgenveje", "agentManager.dialog.removeStaleWorktree.title": "Fjern forældet Worktree", "agentManager.dialog.removeStaleWorktree.messagePre": "Fjern forældet Worktree ", @@ -205,10 +205,10 @@ export const dict = { "Forhåndsvisning af billedet er ikke tilgængelig for dette snapshot af sessionen.", "agentManager.review.endOfLongDiff": "Du nåede slutningen!", - "agentManager.import.pullRequest": "Pull Request", + "agentManager.import.pullRequest": "Pull request", "agentManager.import.pastePrUrl": "Indsæt PR URL...", "agentManager.import.open": "Åbn", - "agentManager.import.branches": "Branches", + "agentManager.import.branches": "Grene", "agentManager.import.selectBranch": "Vælg branch...", "agentManager.import.loading": "Indlæser...", "agentManager.import.loadingBranches": "Indlæser branches...", @@ -216,4 +216,9 @@ export const dict = { "agentManager.import.noBranchesFound": "Ingen branches fundet.", "agentManager.import.noBranchesHint": "Indsæt en PR URL ovenfor eller opret et nyt Worktree.", "agentManager.import.failed": "Import mislykkedes", + + "agentManager.pr.error.gh_auth.title": "GitHub-godkendelse påkrævet", + "agentManager.pr.error.gh_auth.description": "Kør 'gh auth login' i din terminal for at gendanne PR-status.", + "agentManager.pr.error.gh_missing.title": "GitHub CLI er ikke installeret", + "agentManager.pr.error.gh_missing.description": "Installer GitHub CLI for at aktivere PR-status.", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts index f60caf8070b..0875d75f794 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts @@ -14,7 +14,7 @@ export const dict = { "agentManager.worktree.new": "Neuer Worktree", "agentManager.worktree.setupScript": "Worktree-Einrichtungsskript", "agentManager.worktree.delete": "Worktree löschen", - "agentManager.worktree.confirmDelete": "Delete?", + "agentManager.worktree.confirmDelete": "Löschen?", "agentManager.worktree.stale": "Veraltet", "agentManager.worktree.staleTooltip": "Fehlt auf der Festplatte oder wird nicht mehr von git worktree verfolgt", "agentManager.worktree.removeStale": "Veralteten Worktree entfernen", @@ -108,8 +108,8 @@ export const dict = { "agentManager.run.options": "Ausführungsoptionen", "agentManager.run.configure": "Ausführungsskript konfigurieren", "agentManager.shortcuts.openAgentManager": "Agent Manager öffnen", - "agentManager.shortcuts.cycleAgentMode": "Next agent mode", - "agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode", + "agentManager.shortcuts.cycleAgentMode": "Nächster Agentenmodus", + "agentManager.shortcuts.cyclePreviousAgentMode": "Vorheriger Agentenmodus", "agentManager.shortcuts.showShortcuts": "Tastenkürzel anzeigen", "agentManager.dialog.removeStaleWorktree.title": "Veralteten Worktree entfernen", "agentManager.dialog.removeStaleWorktree.messagePre": "Veralteten Worktree entfernen ", @@ -208,7 +208,7 @@ export const dict = { "agentManager.import.pullRequest": "Pull Request", "agentManager.import.pastePrUrl": "PR-URL einfügen...", "agentManager.import.open": "Öffnen", - "agentManager.import.branches": "Branches", + "agentManager.import.branches": "Zweige", "agentManager.import.selectBranch": "Branch auswählen...", "agentManager.import.loading": "Laden...", "agentManager.import.loadingBranches": "Branches werden geladen...", @@ -216,4 +216,10 @@ export const dict = { "agentManager.import.noBranchesFound": "Keine Branches gefunden.", "agentManager.import.noBranchesHint": "Fügen Sie oben eine PR-URL ein oder erstellen Sie einen neuen Worktree.", "agentManager.import.failed": "Import fehlgeschlagen", + + "agentManager.pr.error.gh_auth.title": "GitHub-Authentifizierung erforderlich", + "agentManager.pr.error.gh_auth.description": + "Führen Sie 'gh auth login' in Ihrem Terminal aus, um den PR-Status wiederherzustellen.", + "agentManager.pr.error.gh_missing.title": "GitHub CLI nicht installiert", + "agentManager.pr.error.gh_missing.description": "Installieren Sie die GitHub CLI, um den PR-Status zu aktivieren.", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts index f0173a673fd..6b17280918d 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts @@ -14,7 +14,7 @@ export const dict = { "agentManager.worktree.new": "Nuevo Worktree", "agentManager.worktree.setupScript": "Script de configuración de Worktree", "agentManager.worktree.delete": "Eliminar Worktree", - "agentManager.worktree.confirmDelete": "Delete?", + "agentManager.worktree.confirmDelete": "¿Eliminar?", "agentManager.worktree.stale": "Obsoleto", "agentManager.worktree.staleTooltip": "No está en disco o ya no está seguido por git worktree", "agentManager.worktree.removeStale": "Eliminar Worktree obsoleto", @@ -107,8 +107,8 @@ export const dict = { "agentManager.run.options": "Opciones de ejecución", "agentManager.run.configure": "Configurar script de ejecución", "agentManager.shortcuts.openAgentManager": "Abrir Agent Manager", - "agentManager.shortcuts.cycleAgentMode": "Next agent mode", - "agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode", + "agentManager.shortcuts.cycleAgentMode": "Siguiente modo de agente", + "agentManager.shortcuts.cyclePreviousAgentMode": "Modo de agente anterior", "agentManager.shortcuts.showShortcuts": "Mostrar atajos de teclado", "agentManager.dialog.removeStaleWorktree.title": "Eliminar Worktree obsoleto", "agentManager.dialog.removeStaleWorktree.messagePre": "¿Eliminar Worktree obsoleto ", @@ -208,7 +208,7 @@ export const dict = { "agentManager.import.pullRequest": "Pull Request", "agentManager.import.pastePrUrl": "Pegar URL del PR...", "agentManager.import.open": "Abrir", - "agentManager.import.branches": "Branches", + "agentManager.import.branches": "Ramas", "agentManager.import.selectBranch": "Seleccionar branch...", "agentManager.import.loading": "Cargando...", "agentManager.import.loadingBranches": "Cargando branches...", @@ -216,4 +216,10 @@ export const dict = { "agentManager.import.noBranchesFound": "No se encontraron branches.", "agentManager.import.noBranchesHint": "Pega una URL de PR arriba o crea un nuevo Worktree.", "agentManager.import.failed": "Error en la importación", + + "agentManager.pr.error.gh_auth.title": "Se requiere autenticación de GitHub", + "agentManager.pr.error.gh_auth.description": + "Ejecuta 'gh auth login' en tu terminal para restaurar el estado del PR.", + "agentManager.pr.error.gh_missing.title": "GitHub CLI no está instalado", + "agentManager.pr.error.gh_missing.description": "Instala GitHub CLI para habilitar el estado del PR.", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fa.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fa.ts index b5d50a2b430..01aa40d7c65 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fa.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fa.ts @@ -3,8 +3,8 @@ export const dict = { "agentManager.sidebar.collapse": "بستن نوار کناری", "agentManager.sidebar.expand": "نمایش نوار کناری", "agentManager.section.worktrees": "WORKTREES", - "agentManager.section.sessions": "SESSIONS", - "agentManager.projects": "PROJECTS", + "agentManager.section.sessions": "جلسات", + "agentManager.projects": "پروژه‌ها", "agentManager.project.add": "افزودن پروژه", "agentManager.project.remove": "حذف از Agent Manager", "agentManager.project.trust": "اعتماد", @@ -207,7 +207,7 @@ export const dict = { "agentManager.review.imageUnavailable": "پیش‌نمایش تصویر برای این نمونه جلسه در دسترس نیست.", "agentManager.review.endOfLongDiff": "به انتها رسیدید!", - "agentManager.import.pullRequest": "Pull Request", + "agentManager.import.pullRequest": "درخواست ادغام", "agentManager.import.pastePrUrl": "URL درخواست PR را وارد کنید...", "agentManager.import.open": "باز کردن", "agentManager.import.branches": "شاخه‌ها", @@ -218,4 +218,9 @@ export const dict = { "agentManager.import.noBranchesFound": "هیچ شاخه‌ای یافت نشد.", "agentManager.import.noBranchesHint": "یک URL درخواست ادغام را در بالا جای‌گذاری کنید یا یک worktree جدید بسازید.", "agentManager.import.failed": "وارد کردن ناموفق بود", + + "agentManager.pr.error.gh_auth.title": "احراز هویت GitHub لازم است", + "agentManager.pr.error.gh_auth.description": "برای بازیابی وضعیت PR، دستور 'gh auth login' را در ترمینال اجرا کنید.", + "agentManager.pr.error.gh_missing.title": "GitHub CLI نصب نشده است", + "agentManager.pr.error.gh_missing.description": "برای فعال کردن وضعیت PR، GitHub CLI را نصب کنید.", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts index 544b5007a2c..3d8f7279b9f 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts @@ -14,7 +14,7 @@ export const dict = { "agentManager.worktree.new": "Nouveau Worktree", "agentManager.worktree.setupScript": "Script de configuration du Worktree", "agentManager.worktree.delete": "Supprimer le Worktree", - "agentManager.worktree.confirmDelete": "Delete?", + "agentManager.worktree.confirmDelete": "Supprimer ?", "agentManager.worktree.stale": "Obsolète", "agentManager.worktree.staleTooltip": "Absent du disque ou n'est plus suivi par git worktree", "agentManager.worktree.removeStale": "Supprimer le Worktree obsolète", @@ -60,7 +60,7 @@ export const dict = { "agentManager.sidebarSearch.label": "Rechercher des Worktrees et des sessions", "agentManager.sidebarSearch.scope": "Recherche dans l'espace de travail local, les sessions locales, les Worktrees et leurs sessions", - "agentManager.sidebarSearch.contexts": "LOCAL & WORKTREES", + "agentManager.sidebarSearch.contexts": "LOCAL ET WORKTREES", "agentManager.terminal.new": "Nouvel onglet de terminal", "agentManager.terminal.ended": "terminal terminé — fermez l'onglet pour ignorer", @@ -107,8 +107,8 @@ export const dict = { "agentManager.run.options": "Options d'exécution", "agentManager.run.configure": "Configurer le script d'exécution", "agentManager.shortcuts.openAgentManager": "Ouvrir Agent Manager", - "agentManager.shortcuts.cycleAgentMode": "Next agent mode", - "agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode", + "agentManager.shortcuts.cycleAgentMode": "Mode d'agent suivant", + "agentManager.shortcuts.cyclePreviousAgentMode": "Mode d'agent précédent", "agentManager.shortcuts.showShortcuts": "Afficher les raccourcis clavier", "agentManager.dialog.removeStaleWorktree.title": "Supprimer le Worktree obsolète", "agentManager.dialog.removeStaleWorktree.messagePre": "Supprimer le Worktree obsolète ", @@ -216,4 +216,10 @@ export const dict = { "agentManager.import.noBranchesFound": "Aucune branche trouvée.", "agentManager.import.noBranchesHint": "Collez une URL de PR ci-dessus ou créez un nouveau Worktree.", "agentManager.import.failed": "Échec de l'importation", + + "agentManager.pr.error.gh_auth.title": "Authentification GitHub requise", + "agentManager.pr.error.gh_auth.description": + "Exécutez 'gh auth login' dans votre terminal pour restaurer le statut de la PR.", + "agentManager.pr.error.gh_missing.title": "GitHub CLI non installé", + "agentManager.pr.error.gh_missing.description": "Installez GitHub CLI pour activer le statut de la PR.", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts index 59f6ea340c6..684ea283f4f 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts @@ -36,7 +36,7 @@ export const dict = { "agentManager.section.moveUp": "Sposta su", "agentManager.section.moveDown": "Sposta giù", - "agentManager.hoverCard.branch": "BRANCH", + "agentManager.hoverCard.branch": "Ramo", "agentManager.hoverCard.worktree": "Worktree", "agentManager.hoverCard.base": "Base", "agentManager.hoverCard.sessions": "Sessioni", @@ -222,4 +222,10 @@ export const dict = { "agentManager.import.noBranchesFound": "Nessun branch trovato.", "agentManager.import.noBranchesHint": "Incolla un URL PR sopra o crea un nuovo worktree.", "agentManager.import.failed": "Importazione non riuscita", + + "agentManager.pr.error.gh_auth.title": "Autenticazione GitHub richiesta", + "agentManager.pr.error.gh_auth.description": + "Esegui 'gh auth login' nel terminale per ripristinare lo stato della PR.", + "agentManager.pr.error.gh_missing.title": "GitHub CLI non installato", + "agentManager.pr.error.gh_missing.description": "Installa GitHub CLI per abilitare lo stato della PR.", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts index 5e4dd806ef5..c6ef13f8cb0 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts @@ -14,7 +14,7 @@ export const dict = { "agentManager.worktree.new": "新しいWorktree", "agentManager.worktree.setupScript": "Worktreeセットアップスクリプト", "agentManager.worktree.delete": "Worktreeを削除", - "agentManager.worktree.confirmDelete": "Delete?", + "agentManager.worktree.confirmDelete": "削除しますか?", "agentManager.worktree.stale": "無効", "agentManager.worktree.staleTooltip": "ディスク上に存在しないか、git worktree で追跡されていません", "agentManager.worktree.removeStale": "無効な Worktree を削除", @@ -107,8 +107,8 @@ export const dict = { "agentManager.run.options": "実行オプション", "agentManager.run.configure": "実行スクリプトを設定", "agentManager.shortcuts.openAgentManager": "Agent Managerを開く", - "agentManager.shortcuts.cycleAgentMode": "Next agent mode", - "agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode", + "agentManager.shortcuts.cycleAgentMode": "次のエージェントモード", + "agentManager.shortcuts.cyclePreviousAgentMode": "前のエージェントモード", "agentManager.shortcuts.showShortcuts": "キーボードショートカットを表示", "agentManager.dialog.removeStaleWorktree.title": "無効な Worktree を削除", "agentManager.dialog.removeStaleWorktree.messagePre": "無効な Worktree を削除 ", @@ -204,7 +204,7 @@ export const dict = { "agentManager.review.imageUnavailable": "このセッションスナップショットでは画像プレビューを利用できません。", "agentManager.review.endOfLongDiff": "最後まで到達しました!", - "agentManager.import.pullRequest": "Pull Request", + "agentManager.import.pullRequest": "プルリクエスト", "agentManager.import.pastePrUrl": "PR URLを貼り付け...", "agentManager.import.open": "開く", "agentManager.import.branches": "ブランチ", @@ -215,4 +215,11 @@ export const dict = { "agentManager.import.noBranchesFound": "ブランチが見つかりません。", "agentManager.import.noBranchesHint": "上にPR URLを貼り付けるか、新しいWorktreeを作成してください。", "agentManager.import.failed": "インポートに失敗しました", + + "agentManager.pr.error.gh_auth.title": "GitHub認証が必要です", + "agentManager.pr.error.gh_auth.description": + "PRステータスを復元するには、ターミナルで 'gh auth login' を実行してください。", + "agentManager.pr.error.gh_missing.title": "GitHub CLIがインストールされていません", + "agentManager.pr.error.gh_missing.description": + "PRステータスを有効にするには、GitHub CLIをインストールしてください。", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts index b729493b1d3..e8fa9677984 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts @@ -14,7 +14,7 @@ export const dict = { "agentManager.worktree.new": "새 Worktree", "agentManager.worktree.setupScript": "Worktree 설정 스크립트", "agentManager.worktree.delete": "Worktree 삭제", - "agentManager.worktree.confirmDelete": "Delete?", + "agentManager.worktree.confirmDelete": "삭제하시겠습니까?", "agentManager.worktree.stale": "오래됨", "agentManager.worktree.staleTooltip": "디스크에 없거나 더 이상 git worktree에서 추적되지 않습니다", "agentManager.worktree.removeStale": "오래된 Worktree 제거", @@ -105,8 +105,8 @@ export const dict = { "agentManager.run.options": "실행 옵션", "agentManager.run.configure": "실행 스크립트 구성", "agentManager.shortcuts.openAgentManager": "Agent Manager 열기", - "agentManager.shortcuts.cycleAgentMode": "Next agent mode", - "agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode", + "agentManager.shortcuts.cycleAgentMode": "다음 에이전트 모드", + "agentManager.shortcuts.cyclePreviousAgentMode": "이전 에이전트 모드", "agentManager.shortcuts.showShortcuts": "키보드 단축키 표시", "agentManager.dialog.removeStaleWorktree.title": "오래된 Worktree 제거", "agentManager.dialog.removeStaleWorktree.messagePre": "오래된 Worktree 제거 ", @@ -202,7 +202,7 @@ export const dict = { "agentManager.review.imageUnavailable": "이 세션 스냅샷에서는 이미지 미리보기를 사용할 수 없습니다.", "agentManager.review.endOfLongDiff": "끝까지 도달했습니다!", - "agentManager.import.pullRequest": "Pull Request", + "agentManager.import.pullRequest": "풀 리퀘스트", "agentManager.import.pastePrUrl": "PR URL 붙여넣기...", "agentManager.import.open": "열기", "agentManager.import.branches": "브랜치", @@ -213,4 +213,9 @@ export const dict = { "agentManager.import.noBranchesFound": "브랜치를 찾을 수 없습니다.", "agentManager.import.noBranchesHint": "위에 PR URL을 붙여넣거나 새 Worktree를 만드세요.", "agentManager.import.failed": "가져오기 실패", + + "agentManager.pr.error.gh_auth.title": "GitHub 인증 필요", + "agentManager.pr.error.gh_auth.description": "PR 상태를 복원하려면 터미널에서 'gh auth login'을 실행하세요.", + "agentManager.pr.error.gh_missing.title": "GitHub CLI가 설치되지 않음", + "agentManager.pr.error.gh_missing.description": "PR 상태를 활성화하려면 GitHub CLI를 설치하세요.", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts index 3ffc6c7dce5..89171ad1b81 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts @@ -138,7 +138,7 @@ export const dict = { "agentManager.dialog.baseBranch": "Basis branch", "agentManager.dialog.searchBranches": "Zoeken naar branches...", "agentManager.dialog.branchBadge.default": "standaard", - "agentManager.dialog.branchBadge.remote": "remote", + "agentManager.dialog.branchBadge.remote": "extern", "agentManager.dialog.versions": "Versies", "agentManager.dialog.versionHint": "{{count}} worktrees zullen parallel draaien", "agentManager.dialog.compareModels": "Modellen vergelijken", @@ -209,7 +209,7 @@ export const dict = { "agentManager.review.imageUnavailable": "Afbeeldingsvoorbeeld is niet beschikbaar voor deze sessiesnapshot.", "agentManager.review.endOfLongDiff": "Je hebt het einde bereikt!", - "agentManager.import.pullRequest": "Pull Request", + "agentManager.import.pullRequest": "Pull request", "agentManager.import.pastePrUrl": "Plak PR URL...", "agentManager.import.open": "Openen", "agentManager.import.branches": "Branches", @@ -220,4 +220,9 @@ export const dict = { "agentManager.import.noBranchesFound": "Geen branches gevonden.", "agentManager.import.noBranchesHint": "Plak hierboven een PR URL of maak een nieuwe worktree.", "agentManager.import.failed": "Importeren mislukt", + + "agentManager.pr.error.gh_auth.title": "GitHub-authenticatie vereist", + "agentManager.pr.error.gh_auth.description": "Voer 'gh auth login' uit in je terminal om de PR-status te herstellen.", + "agentManager.pr.error.gh_missing.title": "GitHub CLI niet geïnstalleerd", + "agentManager.pr.error.gh_missing.description": "Installeer de GitHub CLI om de PR-status in te schakelen.", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts index d5dfa02a817..5807661c9d3 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts @@ -14,7 +14,7 @@ export const dict = { "agentManager.worktree.new": "Nytt Worktree", "agentManager.worktree.setupScript": "Worktree-oppsettskript", "agentManager.worktree.delete": "Slett Worktree", - "agentManager.worktree.confirmDelete": "Delete?", + "agentManager.worktree.confirmDelete": "Slette?", "agentManager.worktree.stale": "Utdatert", "agentManager.worktree.staleTooltip": "Mangler på disken eller spores ikke lenger av git worktree", "agentManager.worktree.removeStale": "Fjern utdatert Worktree", @@ -105,8 +105,8 @@ export const dict = { "agentManager.run.options": "Kjøringsalternativer", "agentManager.run.configure": "Konfigurer kjøreskript", "agentManager.shortcuts.openAgentManager": "Åpne Agent Manager", - "agentManager.shortcuts.cycleAgentMode": "Next agent mode", - "agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode", + "agentManager.shortcuts.cycleAgentMode": "Neste agentmodus", + "agentManager.shortcuts.cyclePreviousAgentMode": "Forrige agentmodus", "agentManager.shortcuts.showShortcuts": "Vis tastatursnarveier", "agentManager.dialog.removeStaleWorktree.title": "Fjern utdatert Worktree", "agentManager.dialog.removeStaleWorktree.messagePre": "Fjern utdatert Worktree ", @@ -202,10 +202,10 @@ export const dict = { "Forhåndsvisning av bildet er ikke tilgjengelig for dette øyeblikksbildet av økten.", "agentManager.review.endOfLongDiff": "Du nådde slutten!", - "agentManager.import.pullRequest": "Pull Request", + "agentManager.import.pullRequest": "Pull request", "agentManager.import.pastePrUrl": "Lim inn PR URL...", "agentManager.import.open": "Åpne", - "agentManager.import.branches": "Branches", + "agentManager.import.branches": "Grener", "agentManager.import.selectBranch": "Velg branch...", "agentManager.import.loading": "Laster...", "agentManager.import.loadingBranches": "Laster branches...", @@ -213,4 +213,9 @@ export const dict = { "agentManager.import.noBranchesFound": "Ingen branches funnet.", "agentManager.import.noBranchesHint": "Lim inn en PR URL ovenfor eller opprett et nytt Worktree.", "agentManager.import.failed": "Import mislyktes", + + "agentManager.pr.error.gh_auth.title": "GitHub-autentisering kreves", + "agentManager.pr.error.gh_auth.description": "Kjør 'gh auth login' i terminalen for å gjenopprette PR-status.", + "agentManager.pr.error.gh_missing.title": "GitHub CLI er ikke installert", + "agentManager.pr.error.gh_missing.description": "Installer GitHub CLI for å aktivere PR-status.", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts index 9fb33a5c980..ddd54169991 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts @@ -14,7 +14,7 @@ export const dict = { "agentManager.worktree.new": "Nowy Worktree", "agentManager.worktree.setupScript": "Skrypt konfiguracji Worktree", "agentManager.worktree.delete": "Usuń Worktree", - "agentManager.worktree.confirmDelete": "Delete?", + "agentManager.worktree.confirmDelete": "Usunąć?", "agentManager.worktree.stale": "Nieaktualny", "agentManager.worktree.staleTooltip": "Brak na dysku lub nie jest już śledzony przez git worktree", "agentManager.worktree.removeStale": "Usuń nieaktualny Worktree", @@ -107,8 +107,8 @@ export const dict = { "agentManager.run.options": "Opcje uruchamiania", "agentManager.run.configure": "Konfiguruj skrypt uruchamiania", "agentManager.shortcuts.openAgentManager": "Otwórz Agent Manager", - "agentManager.shortcuts.cycleAgentMode": "Next agent mode", - "agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode", + "agentManager.shortcuts.cycleAgentMode": "Następny tryb agenta", + "agentManager.shortcuts.cyclePreviousAgentMode": "Poprzedni tryb agenta", "agentManager.shortcuts.showShortcuts": "Pokaż skróty klawiszowe", "agentManager.dialog.removeStaleWorktree.title": "Usuń nieaktualny Worktree", "agentManager.dialog.removeStaleWorktree.messagePre": "Usunąć nieaktualny Worktree ", @@ -214,4 +214,9 @@ export const dict = { "agentManager.import.noBranchesFound": "Nie znaleziono branchy.", "agentManager.import.noBranchesHint": "Wklej URL PR powyżej lub utwórz nowy Worktree.", "agentManager.import.failed": "Import nie powiódł się", + + "agentManager.pr.error.gh_auth.title": "Wymagana autoryzacja GitHub", + "agentManager.pr.error.gh_auth.description": "Uruchom 'gh auth login' w terminalu, aby przywrócić status PR.", + "agentManager.pr.error.gh_missing.title": "GitHub CLI nie jest zainstalowane", + "agentManager.pr.error.gh_missing.description": "Zainstaluj GitHub CLI, aby włączyć status PR.", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts index 74afb2c7aa3..90ae939cbdc 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts @@ -14,7 +14,7 @@ export const dict = { "agentManager.worktree.new": "Новый Worktree", "agentManager.worktree.setupScript": "Скрипт настройки Worktree", "agentManager.worktree.delete": "Удалить Worktree", - "agentManager.worktree.confirmDelete": "Delete?", + "agentManager.worktree.confirmDelete": "Удалить?", "agentManager.worktree.stale": "Устаревший", "agentManager.worktree.staleTooltip": "Отсутствует на диске или больше не отслеживается git worktree", "agentManager.worktree.removeStale": "Удалить устаревший Worktree", @@ -107,8 +107,8 @@ export const dict = { "agentManager.run.options": "Параметры запуска", "agentManager.run.configure": "Настроить скрипт запуска", "agentManager.shortcuts.openAgentManager": "Открыть Agent Manager", - "agentManager.shortcuts.cycleAgentMode": "Next agent mode", - "agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode", + "agentManager.shortcuts.cycleAgentMode": "Следующий режим агента", + "agentManager.shortcuts.cyclePreviousAgentMode": "Предыдущий режим агента", "agentManager.shortcuts.showShortcuts": "Показать сочетания клавиш", "agentManager.dialog.removeStaleWorktree.title": "Удалить устаревший Worktree", "agentManager.dialog.removeStaleWorktree.messagePre": "Удалить устаревший Worktree ", @@ -215,4 +215,9 @@ export const dict = { "agentManager.import.noBranchesFound": "Ветки не найдены.", "agentManager.import.noBranchesHint": "Вставьте URL PR выше или создайте новый Worktree.", "agentManager.import.failed": "Ошибка импорта", + + "agentManager.pr.error.gh_auth.title": "Требуется аутентификация GitHub", + "agentManager.pr.error.gh_auth.description": "Выполните 'gh auth login' в терминале, чтобы восстановить статус PR.", + "agentManager.pr.error.gh_missing.title": "GitHub CLI не установлен", + "agentManager.pr.error.gh_missing.description": "Установите GitHub CLI, чтобы включить статус PR.", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts index b65655f053c..0a395b5ff8f 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts @@ -14,7 +14,7 @@ export const dict = { "agentManager.worktree.new": "Worktree ใหม่", "agentManager.worktree.setupScript": "สคริปต์ตั้งค่า Worktree", "agentManager.worktree.delete": "ลบ Worktree", - "agentManager.worktree.confirmDelete": "Delete?", + "agentManager.worktree.confirmDelete": "ลบ?", "agentManager.worktree.stale": "ล้าสมัย", "agentManager.worktree.staleTooltip": "ไม่พบบนดิสก์หรือไม่ได้ถูกติดตามโดย git worktree อีกต่อไป", "agentManager.worktree.removeStale": "ลบ Worktree ที่ล้าสมัย", @@ -102,8 +102,8 @@ export const dict = { "agentManager.run.options": "ตัวเลือกการเรียกใช้", "agentManager.run.configure": "กำหนดค่าสคริปต์การเรียกใช้", "agentManager.shortcuts.openAgentManager": "เปิด Agent Manager", - "agentManager.shortcuts.cycleAgentMode": "Next agent mode", - "agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode", + "agentManager.shortcuts.cycleAgentMode": "โหมดเอเจนต์ถัดไป", + "agentManager.shortcuts.cyclePreviousAgentMode": "โหมดเอเจนต์ก่อนหน้า", "agentManager.shortcuts.showShortcuts": "แสดงปุ่มลัดแป้นพิมพ์", "agentManager.dialog.removeStaleWorktree.title": "ลบ Worktree ที่ล้าสมัย", "agentManager.dialog.removeStaleWorktree.messagePre": "ลบ Worktree ที่ล้าสมัย ", @@ -198,10 +198,10 @@ export const dict = { "agentManager.review.imageUnavailable": "ไม่สามารถแสดงตัวอย่างรูปภาพสำหรับสแนปช็อตของเซสชันนี้ได้", "agentManager.review.endOfLongDiff": "คุณมาถึงท้ายสุดแล้ว!", - "agentManager.import.pullRequest": "Pull Request", + "agentManager.import.pullRequest": "คำขอรวมโค้ด", "agentManager.import.pastePrUrl": "วาง URL ของ PR...", "agentManager.import.open": "เปิด", - "agentManager.import.branches": "Branches", + "agentManager.import.branches": "สาขา", "agentManager.import.selectBranch": "เลือก Branch...", "agentManager.import.loading": "กำลังโหลด...", "agentManager.import.loadingBranches": "กำลังโหลด Branches...", @@ -209,4 +209,9 @@ export const dict = { "agentManager.import.noBranchesFound": "ไม่พบ Branch", "agentManager.import.noBranchesHint": "วาง URL ของ PR ด้านบนหรือสร้าง Worktree ใหม่", "agentManager.import.failed": "นำเข้าล้มเหลว", + + "agentManager.pr.error.gh_auth.title": "ต้องยืนยันตัวตน GitHub", + "agentManager.pr.error.gh_auth.description": "รันคำสั่ง 'gh auth login' ในเทอร์มินัลเพื่อกู้คืนสถานะ PR", + "agentManager.pr.error.gh_missing.title": "ไม่ได้ติดตั้ง GitHub CLI", + "agentManager.pr.error.gh_missing.description": "ติดตั้ง GitHub CLI เพื่อเปิดใช้งานสถานะ PR", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts index 43b033d32a9..ed3c98980c7 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts @@ -211,7 +211,7 @@ export const dict = { "agentManager.review.imageUnavailable": "Bu oturum anlık görüntüsü için görsel önizlemesi kullanılamıyor.", "agentManager.review.endOfLongDiff": "Sonuna geldiniz!", - "agentManager.import.pullRequest": "Pull Request", + "agentManager.import.pullRequest": "Çekme İsteği", "agentManager.import.pastePrUrl": "PR URL'sini yapıştırın...", "agentManager.import.open": "Aç", "agentManager.import.branches": "Dallar", @@ -222,4 +222,10 @@ export const dict = { "agentManager.import.noBranchesFound": "Hiçbir dal bulunamadı.", "agentManager.import.noBranchesHint": "Yukarıya bir PR URL'si yapıştırın veya yeni bir worktree oluşturun.", "agentManager.import.failed": "İçe aktarma başarısız oldu", + + "agentManager.pr.error.gh_auth.title": "GitHub kimlik doğrulaması gerekli", + "agentManager.pr.error.gh_auth.description": + "PR durumunu geri yüklemek için terminalinizde 'gh auth login' komutunu çalıştırın.", + "agentManager.pr.error.gh_missing.title": "GitHub CLI yüklü değil", + "agentManager.pr.error.gh_missing.description": "PR durumunu etkinleştirmek için GitHub CLI'yi yükleyin.", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts index 0242498ddf5..a6c8f9dc53e 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts @@ -212,7 +212,7 @@ export const dict = { "agentManager.review.imageUnavailable": "Попередній перегляд зображення недоступний для цього знімка сеансу.", "agentManager.review.endOfLongDiff": "Ви дійшли до кінця!", - "agentManager.import.pullRequest": "Pull Request", + "agentManager.import.pullRequest": "Пул-реквест", "agentManager.import.pastePrUrl": "Вставте URL PR...", "agentManager.import.open": "Відкрити", "agentManager.import.branches": "Гілки", @@ -223,4 +223,9 @@ export const dict = { "agentManager.import.noBranchesFound": "Гілок не знайдено.", "agentManager.import.noBranchesHint": "Вставте URL PR вище або створіть нове робоче дерево.", "agentManager.import.failed": "Імпортування не вдалося", + + "agentManager.pr.error.gh_auth.title": "Потрібна автентифікація GitHub", + "agentManager.pr.error.gh_auth.description": "Виконайте 'gh auth login' у терміналі, щоб відновити статус PR.", + "agentManager.pr.error.gh_missing.title": "GitHub CLI не встановлено", + "agentManager.pr.error.gh_missing.description": "Встановіть GitHub CLI, щоб увімкнути статус PR.", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts index 8bbcfd45263..8010f67b7d7 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts @@ -14,7 +14,7 @@ export const dict = { "agentManager.worktree.new": "新建 Worktree", "agentManager.worktree.setupScript": "Worktree 设置脚本", "agentManager.worktree.delete": "删除 Worktree", - "agentManager.worktree.confirmDelete": "Delete?", + "agentManager.worktree.confirmDelete": "删除?", "agentManager.worktree.stale": "已失效", "agentManager.worktree.staleTooltip": "在磁盘上缺失,或不再由 git worktree 跟踪", "agentManager.worktree.removeStale": "移除失效 Worktree", @@ -102,8 +102,8 @@ export const dict = { "agentManager.run.options": "运行选项", "agentManager.run.configure": "配置运行脚本", "agentManager.shortcuts.openAgentManager": "打开 Agent Manager", - "agentManager.shortcuts.cycleAgentMode": "Next agent mode", - "agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode", + "agentManager.shortcuts.cycleAgentMode": "下一个代理模式", + "agentManager.shortcuts.cyclePreviousAgentMode": "上一个代理模式", "agentManager.shortcuts.showShortcuts": "显示键盘快捷键", "agentManager.dialog.removeStaleWorktree.title": "移除失效 Worktree", "agentManager.dialog.removeStaleWorktree.messagePre": "移除失效 Worktree ", @@ -196,7 +196,7 @@ export const dict = { "agentManager.review.imageUnavailable": "此会话快照无法提供图片预览。", "agentManager.review.endOfLongDiff": "你已经到末尾了!", - "agentManager.import.pullRequest": "Pull Request", + "agentManager.import.pullRequest": "拉取请求", "agentManager.import.pastePrUrl": "粘贴 PR URL...", "agentManager.import.open": "打开", "agentManager.import.branches": "分支", @@ -207,4 +207,9 @@ export const dict = { "agentManager.import.noBranchesFound": "未找到分支。", "agentManager.import.noBranchesHint": "在上方粘贴 PR URL 或创建新的 Worktree。", "agentManager.import.failed": "导入失败", + + "agentManager.pr.error.gh_auth.title": "需要 GitHub 身份验证", + "agentManager.pr.error.gh_auth.description": "在终端中运行 'gh auth login' 以恢复 PR 状态。", + "agentManager.pr.error.gh_missing.title": "未安装 GitHub CLI", + "agentManager.pr.error.gh_missing.description": "安装 GitHub CLI 以启用 PR 状态。", } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts index 0acadafdbfe..ddb93a8804a 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts @@ -14,7 +14,7 @@ export const dict = { "agentManager.worktree.new": "新建 Worktree", "agentManager.worktree.setupScript": "Worktree 設定指令碼", "agentManager.worktree.delete": "刪除 Worktree", - "agentManager.worktree.confirmDelete": "Delete?", + "agentManager.worktree.confirmDelete": "刪除?", "agentManager.worktree.stale": "已失效", "agentManager.worktree.staleTooltip": "磁碟上不存在,或不再由 git worktree 追蹤", "agentManager.worktree.removeStale": "移除失效 Worktree", @@ -102,8 +102,8 @@ export const dict = { "agentManager.run.options": "執行選項", "agentManager.run.configure": "設定執行指令碼", "agentManager.shortcuts.openAgentManager": "開啟 Agent Manager", - "agentManager.shortcuts.cycleAgentMode": "Next agent mode", - "agentManager.shortcuts.cyclePreviousAgentMode": "Previous agent mode", + "agentManager.shortcuts.cycleAgentMode": "下一個代理模式", + "agentManager.shortcuts.cyclePreviousAgentMode": "上一個代理模式", "agentManager.shortcuts.showShortcuts": "顯示鍵盤快捷鍵", "agentManager.dialog.removeStaleWorktree.title": "移除失效 Worktree", "agentManager.dialog.removeStaleWorktree.messagePre": "移除失效 Worktree ", @@ -196,7 +196,7 @@ export const dict = { "agentManager.review.imageUnavailable": "此工作階段快照無法預覽圖片。", "agentManager.review.endOfLongDiff": "你已經到最後了!", - "agentManager.import.pullRequest": "Pull Request", + "agentManager.import.pullRequest": "提取請求", "agentManager.import.pastePrUrl": "貼上 PR URL...", "agentManager.import.open": "開啟", "agentManager.import.branches": "分支", @@ -207,4 +207,9 @@ export const dict = { "agentManager.import.noBranchesFound": "找不到分支。", "agentManager.import.noBranchesHint": "在上方貼上 PR URL 或建立新的 Worktree。", "agentManager.import.failed": "匯入失敗", + + "agentManager.pr.error.gh_auth.title": "需要 GitHub 驗證", + "agentManager.pr.error.gh_auth.description": "在終端機中執行 'gh auth login' 以還原 PR 狀態。", + "agentManager.pr.error.gh_missing.title": "未安裝 GitHub CLI", + "agentManager.pr.error.gh_missing.description": "安裝 GitHub CLI 以啟用 PR 狀態。", } From baf568564efc1fcff9bf8057fc09e053005df508 Mon Sep 17 00:00:00 2001 From: cmanu Date: Fri, 7 Aug 2026 13:19:31 -0700 Subject: [PATCH 37/73] fix(pr-view): don't include skipped prs in pr summary and also remove unused files causing checks to fail --- .../src/agent-manager/PRStatusPoller.ts | 2 +- .../src/agent-manager/am-pr-utils.ts | 4 +- .../agent-manager/default-base-branch.tsx | 130 ------------------ .../agent-manager/pr/PRFileChanges.tsx | 26 ---- 4 files changed, 4 insertions(+), 158 deletions(-) delete mode 100644 packages/kilo-vscode/webview-ui/agent-manager/default-base-branch.tsx delete mode 100644 packages/kilo-vscode/webview-ui/agent-manager/pr/PRFileChanges.tsx diff --git a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts index afe6a3e3854..b66c42ba434 100644 --- a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts +++ b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts @@ -405,7 +405,7 @@ export class PRStatusPoller { duration: formatCheckDuration(c.startedAt, c.completedAt), })) - const total = checks.length + const total = checks.filter((c) => c.status !== "skipped").length const passed = checks.filter((c) => c.status === "success").length const failed = checks.filter((c) => c.status === "failure").length const pending = checks.filter((c) => c.status === "pending").length diff --git a/packages/kilo-vscode/src/agent-manager/am-pr-utils.ts b/packages/kilo-vscode/src/agent-manager/am-pr-utils.ts index 9cf563d0aef..5877d4ab021 100644 --- a/packages/kilo-vscode/src/agent-manager/am-pr-utils.ts +++ b/packages/kilo-vscode/src/agent-manager/am-pr-utils.ts @@ -30,9 +30,11 @@ export function parsePRResult(json: string): PRResult | null { export function checkStatus(state: string): CheckStatus { switch (state.toUpperCase()) { case "SUCCESS": + case "NEUTRAL": return "success" case "FAILURE": case "ERROR": + case "ACTION_REQUIRED": return "failure" case "PENDING": case "QUEUED": @@ -48,7 +50,7 @@ export function checkStatus(state: string): CheckStatus { case "STARTUP_FAILURE": return "cancelled" default: - return "pending" + return "cancelled" } } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/default-base-branch.tsx b/packages/kilo-vscode/webview-ui/agent-manager/default-base-branch.tsx deleted file mode 100644 index 421c17c8bf3..00000000000 --- a/packages/kilo-vscode/webview-ui/agent-manager/default-base-branch.tsx +++ /dev/null @@ -1,130 +0,0 @@ -/** @jsxImportSource solid-js */ - -import { createSignal, createMemo, onCleanup } from "solid-js" -import type { AgentManagerBranchesMessage, BranchInfo } from "../src/types/messages" -import { Dialog } from "@kilocode/kilo-ui/dialog" -import { useDialog } from "@kilocode/kilo-ui/context/dialog" -import { BranchSelect } from "../src/components/shared/BranchSelect" -import type { useVSCode } from "../src/context/vscode" -import type { useLanguage } from "../src/context/language" -import type { Accessor, Setter } from "solid-js" - -interface Deps { - vscode: ReturnType - dialog: ReturnType - t: ReturnType["t"] - defaultBaseBranch: Accessor - setDefaultBaseBranch: (v: Parameters>[0]) => void - setRepoDetectedBranch: Setter - repoDetectedBranch: Accessor - hasConfiguredBranch: Accessor -} - -export function createChangeDefaultBaseBranch(deps: Deps) { - const { - vscode, - dialog, - t, - defaultBaseBranch, - setDefaultBaseBranch, - setRepoDetectedBranch, - repoDetectedBranch, - hasConfiguredBranch, - } = deps - - return () => { - const [search, setSearch] = createSignal("") - const [branches, setBranches] = createSignal([]) - const [loading, setLoading] = createSignal(true) - const [highlighted, setHighlighted] = createSignal(-1) - - const unsub = vscode.onMessage((msg) => { - if (msg.type === "agentManager.branches") { - const ev = msg as AgentManagerBranchesMessage - setBranches(ev.branches) - if (ev.defaultBranch) setRepoDetectedBranch(ev.defaultBranch) - setLoading(false) - } - }) - - vscode.postMessage({ type: "agentManager.requestBranches" }) - - const filtered = createMemo(() => { - const s = search().toLowerCase() - if (!s) return branches() - return branches().filter((b) => b.name.toLowerCase().includes(s)) - }) - - const selectBranch = (name: string | undefined) => { - vscode.postMessage({ type: "agentManager.setDefaultBaseBranch", branch: name }) - setDefaultBaseBranch(name) - dialog.close() - } - - const handleKeyDown = (e: KeyboardEvent) => { - const items = filtered() - // offset by 1 for auto-detect option (-1 = auto-detect) - const total = items.length + 1 - if (e.key === "ArrowDown") { - e.preventDefault() - e.stopPropagation() - setHighlighted((prev) => Math.min(prev + 1, total - 2)) - } else if (e.key === "ArrowUp") { - e.preventDefault() - e.stopPropagation() - setHighlighted((prev) => Math.max(prev - 1, -1)) - } else if (e.key === "Enter") { - e.preventDefault() - e.stopPropagation() - const idx = highlighted() - if (idx === -1) { - selectBranch(undefined) - } else { - const branch = items[idx] - if (branch) selectBranch(branch.name) - } - } else if (e.key === "Escape") { - e.preventDefault() - e.stopPropagation() - dialog.close() - } - } - - dialog.show(() => { - onCleanup(unsub) - return ( - -
- { - setSearch(v) - setHighlighted(-1) - }} - onSelect={(b) => selectBranch(b.name)} - onSearchKeyDown={handleKeyDown} - selected={defaultBaseBranch()} - highlighted={highlighted()} - onHighlight={setHighlighted} - searchPlaceholder={t("agentManager.dialog.searchBranches")} - emptyLabel={t("agentManager.import.noMatchingBranches")} - loadingLabel={t("agentManager.import.loadingBranches")} - defaultLabel={t("agentManager.dialog.branchBadge.default")} - remoteLabel={t("agentManager.dialog.branchBadge.remote")} - defaultName={defaultBaseBranch()} - autoOption={{ - label: t("agentManager.worktree.defaultBaseBranchAuto"), - hint: repoDetectedBranch(), - active: !hasConfiguredBranch(), - highlighted: highlighted() === -1, - onSelect: () => selectBranch(undefined), - }} - /> -
-
- ) - }) - } -} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRFileChanges.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRFileChanges.tsx deleted file mode 100644 index 971948d8e28..00000000000 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRFileChanges.tsx +++ /dev/null @@ -1,26 +0,0 @@ -/** @jsxImportSource solid-js */ -import { Show } from "solid-js" - -export function PRFileChanges(props: { files: number; additions: number; deletions: number }) { - return ( - <> -
-
-
- File Changes - - 0}> - {props.files}f - - 0}> - +{props.additions} - - 0}> - −{props.deletions} - - -
-
- - ) -} From c313dc47879218e89515765255316cf75ead61cf Mon Sep 17 00:00:00 2001 From: cmanu Date: Fri, 7 Aug 2026 13:44:09 -0700 Subject: [PATCH 38/73] fix(pr-view): default pr status state --- packages/kilo-vscode/src/agent-manager/am-pr-utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kilo-vscode/src/agent-manager/am-pr-utils.ts b/packages/kilo-vscode/src/agent-manager/am-pr-utils.ts index 5877d4ab021..5fe6e0fad5a 100644 --- a/packages/kilo-vscode/src/agent-manager/am-pr-utils.ts +++ b/packages/kilo-vscode/src/agent-manager/am-pr-utils.ts @@ -50,7 +50,7 @@ export function checkStatus(state: string): CheckStatus { case "STARTUP_FAILURE": return "cancelled" default: - return "cancelled" + return "pending" } } From 2dc7442ab611c1070a8c8f6d17f956c229f9c84b Mon Sep 17 00:00:00 2001 From: cmanu Date: Fri, 7 Aug 2026 14:05:18 -0700 Subject: [PATCH 39/73] fix(pr-view): add kilocode change --- packages/ui/src/components/icon.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/components/icon.tsx b/packages/ui/src/components/icon.tsx index e2170c0772e..8bd13c7599f 100644 --- a/packages/ui/src/components/icon.tsx +++ b/packages/ui/src/components/icon.tsx @@ -18,7 +18,7 @@ const icons = { "chevron-grabber-vertical": ``, "chevron-double-right": ``, "circle-x": ``, - "circle-x-outline": ``, + "circle-x-outline": ``, // kilocode_change close: ``, "close-small": ``, checklist: ``, @@ -91,7 +91,7 @@ const icons = { download: ``, menu: ``, server: ``, - "pull-request": ``, + "pull-request": ``, // kilocode_change branch: ``, edit: ``, history: ``, @@ -108,7 +108,7 @@ const icons = { "star-filled": ``, warning: ``, reset: ``, - refresh: ``, + refresh: ``, // kilocode_change link: ``, providers: ``, layers: ``, From 9dbf276adec8a6b88e451eb13422272e16435cbe Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Sat, 8 Aug 2026 17:26:07 +0000 Subject: [PATCH 40/73] feat(llm): set explicit prompt cache breakpoints on stable prefix for GPT-5.6+ --- ...penai-explicit-prompt-cache-breakpoints.md | 5 ++ packages/llm/src/cache-policy.ts | 14 ++- packages/llm/src/protocols/openai-chat.ts | 87 ++++++++++++++++--- .../llm/src/protocols/openai-responses.ts | 69 +++++++++++++-- packages/llm/test/cache-policy.test.ts | 84 +++++++++++++++++- packages/opencode/src/provider/transform.ts | 35 +++++++- .../opencode/test/provider/transform.test.ts | 64 ++++++++++++++ 7 files changed, 335 insertions(+), 23 deletions(-) create mode 100644 .changeset/openai-explicit-prompt-cache-breakpoints.md diff --git a/.changeset/openai-explicit-prompt-cache-breakpoints.md b/.changeset/openai-explicit-prompt-cache-breakpoints.md new file mode 100644 index 00000000000..4ba3d97b450 --- /dev/null +++ b/.changeset/openai-explicit-prompt-cache-breakpoints.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Set explicit prompt cache breakpoints on stable prefixes for OpenAI GPT-5.6+ models. diff --git a/packages/llm/src/cache-policy.ts b/packages/llm/src/cache-policy.ts index 60f96dc69aa..3387c3a67d0 100644 --- a/packages/llm/src/cache-policy.ts +++ b/packages/llm/src/cache-policy.ts @@ -36,10 +36,16 @@ const resolve = (policy: CachePolicy | undefined): CachePolicyObject => { return policy } -// Protocols whose wire format ignores inline cache markers (OpenAI's implicit -// prefix caching, Gemini's implicit + out-of-band CachedContent). Skip the -// whole policy pass for these — emitting hints would be harmless but pointless. -const RESPECTS_INLINE_HINTS = new Set(["anthropic-messages", "bedrock-converse"]) +// kilocode_change start - Protocols whose wire format supports inline cache markers / explicit breakpoints. +// Gemini uses out-of-band CachedContent. +const RESPECTS_INLINE_HINTS = new Set([ + "anthropic-messages", + "bedrock-converse", + "openai-responses", + "openai-chat", + "openai-compatible-chat", +]) +// kilocode_change end const makeHint = (ttlSeconds: number | undefined): CacheHint => ttlSeconds !== undefined ? new CacheHint({ type: "ephemeral", ttlSeconds }) : new CacheHint({ type: "ephemeral" }) diff --git a/packages/llm/src/protocols/openai-chat.ts b/packages/llm/src/protocols/openai-chat.ts index c6ac16c7b3b..7d672563e5a 100644 --- a/packages/llm/src/protocols/openai-chat.ts +++ b/packages/llm/src/protocols/openai-chat.ts @@ -28,6 +28,24 @@ const IMAGE_MIMES = new Set(ProviderShared.IMAGE_MIMES) export const DEFAULT_BASE_URL = "https://api.openai.com/v1" export const PATH = "/chat/completions" +// kilocode_change start - explicit prompt cache breakpoints for GPT-5.6+ +const supportsBreakpoint = (modelId: string) => { + const match = modelId.match(/gpt-(\d+)\.(\d+)/) + if (match) { + const major = Number(match[1]) + const minor = Number(match[2]) + if (major > 5 || (major === 5 && minor >= 6)) return true + } + const majorMatch = modelId.match(/gpt-(\d+)/) + if (majorMatch && Number(majorMatch[1]) >= 6) return true + return false +} + +const OpenAIChatPromptCacheBreakpoint = Schema.Struct({ + mode: Schema.Literal("explicit"), +}) +// kilocode_change end + // ============================================================================= // Request Body Schema // ============================================================================= @@ -57,15 +75,29 @@ const OpenAIChatAssistantToolCall = Schema.Struct({ type OpenAIChatAssistantToolCall = Schema.Schema.Type const OpenAIChatUserContent = Schema.Union([ - Schema.Struct({ type: Schema.Literal("text"), text: Schema.String }), + Schema.Struct({ + type: Schema.Literal("text"), + text: Schema.String, + prompt_cache_breakpoint: Schema.optional(OpenAIChatPromptCacheBreakpoint), // kilocode_change + }), Schema.Struct({ type: Schema.Literal("image_url"), image_url: Schema.Struct({ url: Schema.String }), + prompt_cache_breakpoint: Schema.optional(OpenAIChatPromptCacheBreakpoint), // kilocode_change }), ]) const OpenAIChatMessage = Schema.Union([ - Schema.Struct({ role: Schema.Literal("system"), content: Schema.String }), + // kilocode_change start - support content block array for system/developer messages + Schema.Struct({ + role: Schema.Literal("system"), + content: Schema.Union([Schema.String, Schema.Array(OpenAIChatUserContent)]), + }), + Schema.Struct({ + role: Schema.Literal("developer"), + content: Schema.Union([Schema.String, Schema.Array(OpenAIChatUserContent)]), + }), + // kilocode_change end Schema.Struct({ role: Schema.Literal("user"), content: Schema.Union([Schema.String, Schema.Array(OpenAIChatUserContent)]), @@ -210,21 +242,33 @@ const lowerMedia = Effect.fn("OpenAIChat.lowerMedia")(function* (part: MediaPart const openAICompatibleReasoningContent = (native: unknown) => isRecord(native) && typeof native.reasoning_content === "string" ? native.reasoning_content : undefined -const lowerUserMessage = Effect.fn("OpenAIChat.lowerUserMessage")(function* (message: OpenAIChatRequestMessage) { +const lowerUserMessage = Effect.fn("OpenAIChat.lowerUserMessage")(function* ( + message: OpenAIChatRequestMessage, + modelId: string, // kilocode_change +) { const content: Array> = [] for (const part of message.content) { + // kilocode_change start + const breakpoint = + "cache" in part && part.cache && supportsBreakpoint(modelId) + ? { prompt_cache_breakpoint: { mode: "explicit" as const } } + : {} if (part.type === "text") { - content.push({ type: "text", text: part.text }) + content.push({ type: "text", text: part.text, ...breakpoint }) continue } if (part.type === "media") { - content.push(yield* lowerMedia(part)) + content.push({ ...(yield* lowerMedia(part)), ...breakpoint }) continue } + // kilocode_change end return yield* ProviderShared.unsupportedContent("OpenAI Chat", "user", ["text", "media"]) } - if (content.every((part) => part.type === "text")) - return { role: "user" as const, content: content.map((part) => part.text).join("\n") } // kilocode_change + // kilocode_change start + const hasBreakpoint = content.some((part) => "prompt_cache_breakpoint" in part && part.prompt_cache_breakpoint) + if (!hasBreakpoint && content.every((part) => part.type === "text")) + return { role: "user" as const, content: content.map((part) => (part as { text: string }).text).join("\n") } + // kilocode_change end return { role: "user" as const, content } }) @@ -284,15 +328,36 @@ const lowerToolMessages = Effect.fn("OpenAIChat.lowerToolMessages")(function* (m return { messages, images } }) -const lowerMessage = Effect.fn("OpenAIChat.lowerMessage")(function* (message: OpenAIChatRequestMessage) { - if (message.role === "user") return [yield* lowerUserMessage(message)] +const lowerMessage = Effect.fn("OpenAIChat.lowerMessage")(function* ( + message: OpenAIChatRequestMessage, + modelId: string, // kilocode_change +) { + if (message.role === "user") return [yield* lowerUserMessage(message, modelId)] // kilocode_change if (message.role === "assistant") return [yield* lowerAssistantMessage(message)] return (yield* lowerToolMessages(message)).messages }) const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request: LLMRequest) { + // kilocode_change start + const hasSystemCache = request.system.some((part) => part.cache) && supportsBreakpoint(request.model.id) const system: OpenAIChatMessage[] = - request.system.length === 0 ? [] : [{ role: "system", content: ProviderShared.joinText(request.system) }] + request.system.length === 0 + ? [] + : hasSystemCache + ? [ + { + role: "system", + content: [ + { + type: "text", + text: ProviderShared.joinText(request.system), + prompt_cache_breakpoint: { mode: "explicit" }, + }, + ], + }, + ] + : [{ role: "system", content: ProviderShared.joinText(request.system) }] + // kilocode_change end const messages = [...system] const pendingImages: Array> = [] const flushImages = () => { @@ -324,7 +389,7 @@ const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request: continue } flushImages() - messages.push(...(yield* lowerMessage(message))) + messages.push(...(yield* lowerMessage(message, request.model.id))) // kilocode_change } flushImages() return messages diff --git a/packages/llm/src/protocols/openai-responses.ts b/packages/llm/src/protocols/openai-responses.ts index 2e6b67aec72..cdbb27965f7 100644 --- a/packages/llm/src/protocols/openai-responses.ts +++ b/packages/llm/src/protocols/openai-responses.ts @@ -29,16 +29,36 @@ const ADAPTER = "openai-responses" export const DEFAULT_BASE_URL = "https://api.openai.com/v1" export const PATH = "/responses" +// kilocode_change start - explicit prompt cache breakpoints for GPT-5.6+ +const supportsBreakpoint = (modelId: string) => { + const match = modelId.match(/gpt-(\d+)\.(\d+)/) + if (match) { + const major = Number(match[1]) + const minor = Number(match[2]) + if (major > 5 || (major === 5 && minor >= 6)) return true + } + const majorMatch = modelId.match(/gpt-(\d+)/) + if (majorMatch && Number(majorMatch[1]) >= 6) return true + return false +} + +const OpenAIResponsesPromptCacheBreakpoint = Schema.Struct({ + mode: Schema.Literal("explicit"), +}) +// kilocode_change end + // ============================================================================= // Request Body Schema // ============================================================================= const OpenAIResponsesInputText = Schema.Struct({ type: Schema.tag("input_text"), text: Schema.String, + prompt_cache_breakpoint: Schema.optional(OpenAIResponsesPromptCacheBreakpoint), // kilocode_change }) const OpenAIResponsesInputImage = Schema.Struct({ type: Schema.tag("input_image"), image_url: Schema.String, + prompt_cache_breakpoint: Schema.optional(OpenAIResponsesPromptCacheBreakpoint), // kilocode_change }) const OpenAIResponsesInputContent = Schema.Union([OpenAIResponsesInputText, OpenAIResponsesInputImage]) type OpenAIResponsesInputContent = Schema.Schema.Type @@ -76,7 +96,16 @@ const OpenAIResponsesFunctionCallOutput = Schema.Union([ ]) const OpenAIResponsesInputItem = Schema.Union([ - Schema.Struct({ role: Schema.tag("system"), content: Schema.String }), + // kilocode_change start - support content block array for system/developer messages + Schema.Struct({ + role: Schema.Literal("system"), + content: Schema.Union([Schema.String, Schema.Array(OpenAIResponsesInputContent)]), + }), + Schema.Struct({ + role: Schema.Literal("developer"), + content: Schema.Union([Schema.String, Schema.Array(OpenAIResponsesInputContent)]), + }), + // kilocode_change end Schema.Struct({ role: Schema.tag("user"), content: Schema.Array(OpenAIResponsesInputContent) }), Schema.Struct({ role: Schema.tag("assistant"), content: Schema.Array(OpenAIResponsesOutputText) }), OpenAIResponsesReasoningItem, @@ -307,16 +336,23 @@ const hostedToolItemID = (part: ToolResultPart) => { const lowerUserContent = Effect.fn("OpenAIResponses.lowerUserContent")(function* ( part: LLMRequest["messages"][number]["content"][number], + modelId: string, // kilocode_change ) { - if (part.type === "text") return { type: "input_text" as const, text: part.text } + // kilocode_change start + const breakpoint = + "cache" in part && part.cache && supportsBreakpoint(modelId) + ? { prompt_cache_breakpoint: { mode: "explicit" as const } } + : {} + if (part.type === "text") return { type: "input_text" as const, text: part.text, ...breakpoint } if (part.type === "media") { const media = yield* ProviderShared.validateMedia( "OpenAI Responses", part, new Set(ProviderShared.IMAGE_MIMES), ) - return { type: "input_image" as const, image_url: media.dataUrl } + return { type: "input_image" as const, image_url: media.dataUrl, ...breakpoint } } + // kilocode_change end return yield* ProviderShared.unsupportedContent("OpenAI Responses", "user", ["text", "media"]) }) @@ -344,8 +380,26 @@ const lowerToolResultOutput = Effect.fn("OpenAIResponses.lowerToolResultOutput") }) const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (request: LLMRequest) { + // kilocode_change start + const hasSystemCache = request.system.some((part) => part.cache) && supportsBreakpoint(request.model.id) const system: OpenAIResponsesInputItem[] = - request.system.length === 0 ? [] : [{ role: "system", content: ProviderShared.joinText(request.system) }] + request.system.length === 0 + ? [] + : hasSystemCache + ? [ + { + role: "system", + content: [ + { + type: "input_text", + text: ProviderShared.joinText(request.system), + prompt_cache_breakpoint: { mode: "explicit" }, + }, + ], + }, + ] + : [{ role: "system", content: ProviderShared.joinText(request.system) }] + // kilocode_change end const input: OpenAIResponsesInputItem[] = [...system] const store = OpenAIOptions.store(request) @@ -363,7 +417,12 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ } if (message.role === "user") { - input.push({ role: "user", content: yield* Effect.forEach(message.content, lowerUserContent) }) + // kilocode_change start + input.push({ + role: "user", + content: yield* Effect.forEach(message.content, (part) => lowerUserContent(part, request.model.id)), + }) + // kilocode_change end continue } diff --git a/packages/llm/test/cache-policy.test.ts b/packages/llm/test/cache-policy.test.ts index a126d9502c5..91a6712d108 100644 --- a/packages/llm/test/cache-policy.test.ts +++ b/packages/llm/test/cache-policy.test.ts @@ -6,6 +6,9 @@ import { AmazonBedrock } from "../src/providers" import * as AnthropicMessages from "../src/protocols/anthropic-messages" import * as Gemini from "../src/protocols/gemini" import * as OpenAIChat from "../src/protocols/openai-chat" +// kilocode_change start +import * as OpenAIResponses from "../src/protocols/openai-responses" +// kilocode_change end import { applyCachePolicy } from "../src/cache-policy" import { it } from "./lib/effect" @@ -21,6 +24,16 @@ const openaiModel = OpenAIChat.route .with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") }) .model({ id: "gpt-4o-mini" }) +// kilocode_change start +const openaiGpt56ResponsesModel = OpenAIResponses.route + .with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") }) + .model({ id: "gpt-5.6" }) + +const openaiGpt56ChatModel = OpenAIChat.route + .with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") }) + .model({ id: "gpt-5.6" }) +// kilocode_change end + const geminiModel = Gemini.route .with({ endpoint: { baseURL: "https://generativelanguage.test/v1beta/" }, @@ -79,7 +92,8 @@ describe("applyCachePolicy", () => { }), ) - it.effect("'auto' is a no-op on OpenAI (implicit caching protocol)", () => + // kilocode_change start + it.effect("'auto' does not emit explicit breakpoints on pre-5.6 OpenAI models", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( LLM.request({ @@ -91,13 +105,79 @@ describe("applyCachePolicy", () => { ) const body = prepared.body as { messages: Array<{ content: unknown }> } - // OpenAI doesn't accept cache_control on messages — policy must skip. + // Older OpenAI models reject prompt_cache_breakpoint — policy must skip. const flat = JSON.stringify(body) + expect(flat).not.toContain("prompt_cache_breakpoint") expect(flat).not.toContain("cache_control") expect(flat).not.toContain("cachePoint") }), ) + it.effect("'auto' emits prompt_cache_breakpoint on stable system prefix and latest user on GPT-5.6 Responses", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + model: openaiGpt56ResponsesModel, + system: "System instructions", + messages: [ + Message.user("first question"), + Message.assistant("assistant reply"), + Message.user("latest question"), + ], + cache: "auto", + }), + ) + + expect(prepared.body).toMatchObject({ + input: [ + { + role: "system", + content: [{ type: "input_text", text: "System instructions", prompt_cache_breakpoint: { mode: "explicit" } }], + }, + { role: "user", content: [{ type: "input_text", text: "first question" }] }, + { role: "assistant", content: [{ type: "output_text", text: "assistant reply" }] }, + { + role: "user", + content: [{ type: "input_text", text: "latest question", prompt_cache_breakpoint: { mode: "explicit" } }], + }, + ], + }) + }), + ) + + it.effect("'auto' emits prompt_cache_breakpoint on stable system prefix and latest user on GPT-5.6 Chat", () => + Effect.gen(function* () { + const prepared = yield* LLMClient.prepare( + LLM.request({ + model: openaiGpt56ChatModel, + system: "System instructions", + messages: [ + Message.user("first question"), + Message.assistant("assistant reply"), + Message.user("latest question"), + ], + cache: "auto", + }), + ) + + expect(prepared.body).toMatchObject({ + messages: [ + { + role: "system", + content: [{ type: "text", text: "System instructions", prompt_cache_breakpoint: { mode: "explicit" } }], + }, + { role: "user", content: "first question" }, + { role: "assistant", content: "assistant reply" }, + { + role: "user", + content: [{ type: "text", text: "latest question", prompt_cache_breakpoint: { mode: "explicit" } }], + }, + ], + }) + }), + ) + // kilocode_change end + it.effect("'auto' is a no-op on Gemini (out-of-band caching protocol)", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 8112996020f..4b1c28e6f5f 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -328,6 +328,20 @@ function normalizeMessages( return msgs } +// kilocode_change start - explicit prompt cache breakpoints for GPT-5.6+ +function supportsOpenAICacheBreakpoint(modelId: string): boolean { + const match = modelId.match(/gpt-(\d+)\.(\d+)/) + if (match) { + const major = Number(match[1]) + const minor = Number(match[2]) + if (major > 5 || (major === 5 && minor >= 6)) return true + } + const majorMatch = modelId.match(/gpt-(\d+)/) + if (majorMatch && Number(majorMatch[1]) >= 6) return true + return false +} +// kilocode_change end + function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] { const system = msgs.filter((msg) => msg.role === "system").slice(0, 2) const final = msgs.filter((msg) => msg.role !== "system").slice(-2) @@ -351,6 +365,18 @@ function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage alibaba: { cacheControl: { type: "ephemeral" }, }, + // kilocode_change start + ...(supportsOpenAICacheBreakpoint(model.api.id) + ? { + openai: { + promptCacheBreakpoint: { mode: "explicit" }, + }, + azure: { + promptCacheBreakpoint: { mode: "explicit" }, + }, + } + : {}), + // kilocode_change end } for (const msg of unique([...system, ...final])) { @@ -438,6 +464,7 @@ function mapProviderOptions( export function message(msgs: ModelMessage[], model: Provider.Model, options: Record) { msgs = unsupportedParts(msgs, model) msgs = normalizeMessages(msgs, model, options) + // kilocode_change start - apply caching for anthropic, alibaba, and GPT-5.6+ openai/azure if ( (model.providerID === "anthropic" || model.providerID === "google-vertex-anthropic" || @@ -446,11 +473,17 @@ export function message(msgs: ModelMessage[], model: Provider.Model, options: Re model.id.includes("anthropic") || model.id.includes("claude") || model.api.npm === "@ai-sdk/anthropic" || - model.api.npm === "@ai-sdk/alibaba") && + model.api.npm === "@ai-sdk/alibaba" || + ((model.api.npm === "@ai-sdk/openai" || + model.api.npm === "@ai-sdk/azure" || + model.providerID === "openai" || + model.providerID === "azure") && + supportsOpenAICacheBreakpoint(model.api.id))) && model.api.npm !== "@ai-sdk/gateway" ) { msgs = applyCaching(msgs, model) } + // kilocode_change end // Remap providerOptions keys from stored providerID to expected SDK key const key = sdkKey(model.api.npm) diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index dfa901724b1..14542cdaa41 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -3029,6 +3029,70 @@ describe("ProviderTransform.message - cache control on gateway", () => { }, }) }) + + // kilocode_change start + test("openai gpt-5.6 applies promptCacheBreakpoint", () => { + const model = createModel({ + providerID: "openai", + api: { + id: "gpt-5.6", + url: "https://api.openai.com/v1", + npm: "@ai-sdk/openai", + }, + id: "gpt-5.6", + }) + const msgs = [ + { + role: "system", + content: "You are a helpful assistant", + }, + { + role: "user", + content: "Hello", + }, + ] as any[] + + const result = ProviderTransform.message(msgs, model, {}) as any[] + + expect(result[0].providerOptions.openai).toEqual({ + promptCacheBreakpoint: { + mode: "explicit", + }, + }) + expect(result[1].providerOptions.openai).toEqual({ + promptCacheBreakpoint: { + mode: "explicit", + }, + }) + }) + + test("openai pre-5.6 does not apply promptCacheBreakpoint", () => { + const model = createModel({ + providerID: "openai", + api: { + id: "gpt-4o", + url: "https://api.openai.com/v1", + npm: "@ai-sdk/openai", + }, + id: "gpt-4o", + }) + const msgs = [ + { + role: "system", + content: "You are a helpful assistant", + }, + { + role: "user", + content: "Hello", + }, + ] as any[] + + const result = ProviderTransform.message(msgs, model, {}) as any[] + + expect(result[0].providerOptions?.openai?.promptCacheBreakpoint).toBeUndefined() + expect(result[1].providerOptions?.openai?.promptCacheBreakpoint).toBeUndefined() + }) + // kilocode_change end }) describe("ProviderTransform.temperature - Cohere North", () => { From 435c029f8cad49af70be2dd1857a85636d4f406b Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Sat, 8 Aug 2026 17:39:22 +0000 Subject: [PATCH 41/73] refactor(llm): extract shared supportsPromptCacheBreakpoint and restrict inline hints to native openai routes Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- packages/llm/src/cache-policy.ts | 1 - packages/llm/src/index.ts | 1 + packages/llm/src/protocols/openai-chat.ts | 16 ++-------------- .../llm/src/protocols/openai-responses.ts | 16 ++-------------- .../llm/src/protocols/utils/openai-options.ts | 14 ++++++++++++++ packages/opencode/src/provider/transform.ts | 19 +++---------------- 6 files changed, 22 insertions(+), 45 deletions(-) diff --git a/packages/llm/src/cache-policy.ts b/packages/llm/src/cache-policy.ts index 3387c3a67d0..e1caf0d67e5 100644 --- a/packages/llm/src/cache-policy.ts +++ b/packages/llm/src/cache-policy.ts @@ -43,7 +43,6 @@ const RESPECTS_INLINE_HINTS = new Set([ "bedrock-converse", "openai-responses", "openai-chat", - "openai-compatible-chat", ]) // kilocode_change end diff --git a/packages/llm/src/index.ts b/packages/llm/src/index.ts index 735520ff77c..1a9fd1fd3de 100644 --- a/packages/llm/src/index.ts +++ b/packages/llm/src/index.ts @@ -26,6 +26,7 @@ export type { ToolToModelOutput, } from "./tool" export * as LLM from "./llm" +export { supportsPromptCacheBreakpoint } from "./protocols/utils/openai-options" // kilocode_change export type { Definition as ProviderDefinition, ModelFactory as ProviderModelFactory, diff --git a/packages/llm/src/protocols/openai-chat.ts b/packages/llm/src/protocols/openai-chat.ts index 7d672563e5a..9db95915669 100644 --- a/packages/llm/src/protocols/openai-chat.ts +++ b/packages/llm/src/protocols/openai-chat.ts @@ -29,18 +29,6 @@ export const DEFAULT_BASE_URL = "https://api.openai.com/v1" export const PATH = "/chat/completions" // kilocode_change start - explicit prompt cache breakpoints for GPT-5.6+ -const supportsBreakpoint = (modelId: string) => { - const match = modelId.match(/gpt-(\d+)\.(\d+)/) - if (match) { - const major = Number(match[1]) - const minor = Number(match[2]) - if (major > 5 || (major === 5 && minor >= 6)) return true - } - const majorMatch = modelId.match(/gpt-(\d+)/) - if (majorMatch && Number(majorMatch[1]) >= 6) return true - return false -} - const OpenAIChatPromptCacheBreakpoint = Schema.Struct({ mode: Schema.Literal("explicit"), }) @@ -250,7 +238,7 @@ const lowerUserMessage = Effect.fn("OpenAIChat.lowerUserMessage")(function* ( for (const part of message.content) { // kilocode_change start const breakpoint = - "cache" in part && part.cache && supportsBreakpoint(modelId) + "cache" in part && part.cache && OpenAIOptions.supportsPromptCacheBreakpoint(modelId) ? { prompt_cache_breakpoint: { mode: "explicit" as const } } : {} if (part.type === "text") { @@ -339,7 +327,7 @@ const lowerMessage = Effect.fn("OpenAIChat.lowerMessage")(function* ( const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request: LLMRequest) { // kilocode_change start - const hasSystemCache = request.system.some((part) => part.cache) && supportsBreakpoint(request.model.id) + const hasSystemCache = request.system.some((part) => part.cache) && OpenAIOptions.supportsPromptCacheBreakpoint(request.model.id) const system: OpenAIChatMessage[] = request.system.length === 0 ? [] diff --git a/packages/llm/src/protocols/openai-responses.ts b/packages/llm/src/protocols/openai-responses.ts index cdbb27965f7..74b4524518d 100644 --- a/packages/llm/src/protocols/openai-responses.ts +++ b/packages/llm/src/protocols/openai-responses.ts @@ -30,18 +30,6 @@ export const DEFAULT_BASE_URL = "https://api.openai.com/v1" export const PATH = "/responses" // kilocode_change start - explicit prompt cache breakpoints for GPT-5.6+ -const supportsBreakpoint = (modelId: string) => { - const match = modelId.match(/gpt-(\d+)\.(\d+)/) - if (match) { - const major = Number(match[1]) - const minor = Number(match[2]) - if (major > 5 || (major === 5 && minor >= 6)) return true - } - const majorMatch = modelId.match(/gpt-(\d+)/) - if (majorMatch && Number(majorMatch[1]) >= 6) return true - return false -} - const OpenAIResponsesPromptCacheBreakpoint = Schema.Struct({ mode: Schema.Literal("explicit"), }) @@ -340,7 +328,7 @@ const lowerUserContent = Effect.fn("OpenAIResponses.lowerUserContent")(function* ) { // kilocode_change start const breakpoint = - "cache" in part && part.cache && supportsBreakpoint(modelId) + "cache" in part && part.cache && OpenAIOptions.supportsPromptCacheBreakpoint(modelId) ? { prompt_cache_breakpoint: { mode: "explicit" as const } } : {} if (part.type === "text") return { type: "input_text" as const, text: part.text, ...breakpoint } @@ -381,7 +369,7 @@ const lowerToolResultOutput = Effect.fn("OpenAIResponses.lowerToolResultOutput") const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (request: LLMRequest) { // kilocode_change start - const hasSystemCache = request.system.some((part) => part.cache) && supportsBreakpoint(request.model.id) + const hasSystemCache = request.system.some((part) => part.cache) && OpenAIOptions.supportsPromptCacheBreakpoint(request.model.id) const system: OpenAIResponsesInputItem[] = request.system.length === 0 ? [] diff --git a/packages/llm/src/protocols/utils/openai-options.ts b/packages/llm/src/protocols/utils/openai-options.ts index 59ac7abc874..708dc07d28c 100644 --- a/packages/llm/src/protocols/utils/openai-options.ts +++ b/packages/llm/src/protocols/utils/openai-options.ts @@ -101,4 +101,18 @@ export const instructions = (request: LLMRequest) => { return typeof value === "string" ? value : undefined } +// kilocode_change start - explicit prompt cache breakpoints for GPT-5.6+ +export const supportsPromptCacheBreakpoint = (modelId: string): boolean => { + const match = modelId.match(/gpt-(\d+)\.(\d+)/) + if (match) { + const major = Number(match[1]) + const minor = Number(match[2]) + if (major > 5 || (major === 5 && minor >= 6)) return true + } + const majorMatch = modelId.match(/gpt-(\d+)/) + if (majorMatch && Number(majorMatch[1]) >= 6) return true + return false +} +// kilocode_change end + export * as OpenAIOptions from "./openai-options" diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 4b1c28e6f5f..dbe3f58da25 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -1,6 +1,7 @@ import type { ModelMessage, ToolResultPart } from "ai" import { mergeDeep, unique } from "remeda" import type { JSONSchema7 } from "@ai-sdk/provider" +import { supportsPromptCacheBreakpoint } from "@opencode-ai/llm" // kilocode_change import type * as Provider from "./provider" import type * as ModelsDev from "@opencode-ai/core/models-dev" import { iife } from "@/util/iife" @@ -328,20 +329,6 @@ function normalizeMessages( return msgs } -// kilocode_change start - explicit prompt cache breakpoints for GPT-5.6+ -function supportsOpenAICacheBreakpoint(modelId: string): boolean { - const match = modelId.match(/gpt-(\d+)\.(\d+)/) - if (match) { - const major = Number(match[1]) - const minor = Number(match[2]) - if (major > 5 || (major === 5 && minor >= 6)) return true - } - const majorMatch = modelId.match(/gpt-(\d+)/) - if (majorMatch && Number(majorMatch[1]) >= 6) return true - return false -} -// kilocode_change end - function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] { const system = msgs.filter((msg) => msg.role === "system").slice(0, 2) const final = msgs.filter((msg) => msg.role !== "system").slice(-2) @@ -366,7 +353,7 @@ function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage cacheControl: { type: "ephemeral" }, }, // kilocode_change start - ...(supportsOpenAICacheBreakpoint(model.api.id) + ...(supportsPromptCacheBreakpoint(model.api.id) ? { openai: { promptCacheBreakpoint: { mode: "explicit" }, @@ -478,7 +465,7 @@ export function message(msgs: ModelMessage[], model: Provider.Model, options: Re model.api.npm === "@ai-sdk/azure" || model.providerID === "openai" || model.providerID === "azure") && - supportsOpenAICacheBreakpoint(model.api.id))) && + supportsPromptCacheBreakpoint(model.api.id))) && model.api.npm !== "@ai-sdk/gateway" ) { msgs = applyCaching(msgs, model) From 944ab18304ff4b26574daa546ef745b2c6337efe Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Sat, 8 Aug 2026 18:17:49 +0000 Subject: [PATCH 42/73] fix(llm): place prompt cache breakpoint before trailing environment details Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- packages/llm/src/cache-policy.ts | 21 +++++++-- packages/llm/test/cache-policy.test.ts | 46 +++++++++++++++++++ packages/opencode/src/provider/transform.ts | 28 ++++++++--- .../opencode/test/provider/transform.test.ts | 35 ++++++++++++++ 4 files changed, 120 insertions(+), 10 deletions(-) diff --git a/packages/llm/src/cache-policy.ts b/packages/llm/src/cache-policy.ts index e1caf0d67e5..96cd7b38bdb 100644 --- a/packages/llm/src/cache-policy.ts +++ b/packages/llm/src/cache-policy.ts @@ -66,15 +66,28 @@ const markLastSystem = (system: LLMRequest["system"], hint: CacheHint): LLMReque const lastIndexOfRole = (messages: ReadonlyArray, role: Message["role"]): number => messages.findLastIndex((m) => m.role === role) -// Mark the last text part of `messages[index]`. If no text part exists, mark -// the last content part regardless of type — that's the breakpoint position -// in tool-result-only messages too. +// kilocode_change start - mark the last stable text part of `messages[index]` (skipping trailing ephemeral details) +const isEphemeralPart = (part: ContentPart): boolean => { + if (part.type === "text" && (part.text.startsWith("") || part.text.includes(""))) + return true + if ("metadata" in part && part.metadata && Boolean((part.metadata as Record).synthetic)) + return true + return false +} + const markMessageAt = (messages: ReadonlyArray, index: number, hint: CacheHint): ReadonlyArray => { if (index < 0 || index >= messages.length) return messages const target = messages[index]! if (target.content.length === 0) return messages + const lastStableTextIndex = target.content.findLastIndex((part) => part.type === "text" && !isEphemeralPart(part)) const lastTextIndex = target.content.findLastIndex((part) => part.type === "text") - const markAt = lastTextIndex >= 0 ? lastTextIndex : target.content.length - 1 + const markAt = + lastStableTextIndex >= 0 + ? lastStableTextIndex + : lastTextIndex >= 0 + ? lastTextIndex + : target.content.length - 1 + // kilocode_change end const existing = target.content[markAt]! if ("cache" in existing && existing.cache) return messages const nextContent = target.content.map((part, i) => (i === markAt ? ({ ...part, cache: hint } as ContentPart) : part)) diff --git a/packages/llm/test/cache-policy.test.ts b/packages/llm/test/cache-policy.test.ts index 91a6712d108..b0f3256e988 100644 --- a/packages/llm/test/cache-policy.test.ts +++ b/packages/llm/test/cache-policy.test.ts @@ -176,6 +176,52 @@ describe("applyCachePolicy", () => { }) }), ) + + it.effect("places prompt_cache_breakpoint BEFORE trailing part", () => + Effect.gen(function* () { + const envBlock = "\nCurrent time: 2026-08-08T18:00:00+00:00\n" + const prepared = yield* LLMClient.prepare( + LLM.request({ + model: openaiGpt56ResponsesModel, + system: "System instructions", + messages: [ + Message.user([ + { type: "text", text: "Please inspect the codebase" }, + { type: "text", text: envBlock }, + ]), + ], + cache: "auto", + }), + ) + + expect(prepared.body).toMatchObject({ + input: [ + { + role: "system", + content: [{ type: "input_text", text: "System instructions", prompt_cache_breakpoint: { mode: "explicit" } }], + }, + { + role: "user", + content: [ + { + type: "input_text", + text: "Please inspect the codebase", + prompt_cache_breakpoint: { mode: "explicit" }, + }, + { + type: "input_text", + text: envBlock, + }, + ], + }, + ], + }) + + const userContent = (prepared.body as any).input[1].content + expect(userContent[0].prompt_cache_breakpoint).toEqual({ mode: "explicit" }) + expect(userContent[1].prompt_cache_breakpoint).toBeUndefined() + }), + ) // kilocode_change end it.effect("'auto' is a no-op on Gemini (out-of-band caching protocol)", () => diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index dbe3f58da25..f0173b50303 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -371,20 +371,36 @@ function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage model.providerID === "anthropic" || model.providerID.includes("bedrock") || model.api.npm === "@ai-sdk/amazon-bedrock" + // kilocode_change start - place caching breakpoint on stable content before trailing const shouldUseContentOptions = !useMessageLevelOptions && Array.isArray(msg.content) && msg.content.length > 0 if (shouldUseContentOptions) { - const lastContent = msg.content[msg.content.length - 1] + const parts = msg.content as any[] + const targetIndex = parts.findLastIndex( + (part) => + part && + typeof part === "object" && + part.type !== "tool-approval-request" && + part.type !== "tool-approval-response" && + !( + part.type === "text" && + typeof part.text === "string" && + (part.text.startsWith("") || part.text.includes("")) + ) && + !part.synthetic, + ) + const targetContent = targetIndex >= 0 ? parts[targetIndex] : parts[parts.length - 1] if ( - lastContent && - typeof lastContent === "object" && - lastContent.type !== "tool-approval-request" && - lastContent.type !== "tool-approval-response" + targetContent && + typeof targetContent === "object" && + targetContent.type !== "tool-approval-request" && + targetContent.type !== "tool-approval-response" ) { - lastContent.providerOptions = mergeDeep(lastContent.providerOptions ?? {}, providerOptions) + targetContent.providerOptions = mergeDeep(targetContent.providerOptions ?? {}, providerOptions) continue } } + // kilocode_change end msg.providerOptions = mergeDeep(msg.providerOptions ?? {}, providerOptions) } diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 14542cdaa41..2f8c7d23b18 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -3092,6 +3092,41 @@ describe("ProviderTransform.message - cache control on gateway", () => { expect(result[0].providerOptions?.openai?.promptCacheBreakpoint).toBeUndefined() expect(result[1].providerOptions?.openai?.promptCacheBreakpoint).toBeUndefined() }) + + test("openai gpt-5.6 places promptCacheBreakpoint before trailing environment_details", () => { + const model = createModel({ + providerID: "openai", + api: { + id: "gpt-5.6", + url: "https://api.openai.com/v1", + npm: "@ai-sdk/openai", + }, + id: "gpt-5.6", + }) + const envBlock = "\nCurrent time: 2026-08-08T18:00:00+00:00\n" + const msgs = [ + { + role: "system", + content: "You are a helpful assistant", + }, + { + role: "user", + content: [ + { type: "text", text: "Please review the changes" }, + { type: "text", text: envBlock }, + ], + }, + ] as any[] + + const result = ProviderTransform.message(msgs, model, {}) as any[] + + expect(result[1].content[0].providerOptions.openai).toEqual({ + promptCacheBreakpoint: { + mode: "explicit", + }, + }) + expect(result[1].content[1].providerOptions?.openai?.promptCacheBreakpoint).toBeUndefined() + }) // kilocode_change end }) From 2f72a8b72dc16b226f937384c8fd11a140262e1e Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Sat, 8 Aug 2026 18:22:35 +0000 Subject: [PATCH 43/73] refactor(cli): limit explicit prompt cache breakpoint changes to Vercel AI SDK path Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- packages/llm/src/cache-policy.ts | 34 ++--- packages/llm/src/index.ts | 1 - packages/llm/src/protocols/openai-chat.ts | 75 ++-------- .../llm/src/protocols/openai-responses.ts | 57 +------- .../llm/src/protocols/utils/openai-options.ts | 14 -- packages/llm/test/cache-policy.test.ts | 130 +----------------- packages/opencode/src/provider/transform.ts | 15 +- 7 files changed, 40 insertions(+), 286 deletions(-) diff --git a/packages/llm/src/cache-policy.ts b/packages/llm/src/cache-policy.ts index 96cd7b38bdb..60f96dc69aa 100644 --- a/packages/llm/src/cache-policy.ts +++ b/packages/llm/src/cache-policy.ts @@ -36,15 +36,10 @@ const resolve = (policy: CachePolicy | undefined): CachePolicyObject => { return policy } -// kilocode_change start - Protocols whose wire format supports inline cache markers / explicit breakpoints. -// Gemini uses out-of-band CachedContent. -const RESPECTS_INLINE_HINTS = new Set([ - "anthropic-messages", - "bedrock-converse", - "openai-responses", - "openai-chat", -]) -// kilocode_change end +// Protocols whose wire format ignores inline cache markers (OpenAI's implicit +// prefix caching, Gemini's implicit + out-of-band CachedContent). Skip the +// whole policy pass for these — emitting hints would be harmless but pointless. +const RESPECTS_INLINE_HINTS = new Set(["anthropic-messages", "bedrock-converse"]) const makeHint = (ttlSeconds: number | undefined): CacheHint => ttlSeconds !== undefined ? new CacheHint({ type: "ephemeral", ttlSeconds }) : new CacheHint({ type: "ephemeral" }) @@ -66,28 +61,15 @@ const markLastSystem = (system: LLMRequest["system"], hint: CacheHint): LLMReque const lastIndexOfRole = (messages: ReadonlyArray, role: Message["role"]): number => messages.findLastIndex((m) => m.role === role) -// kilocode_change start - mark the last stable text part of `messages[index]` (skipping trailing ephemeral details) -const isEphemeralPart = (part: ContentPart): boolean => { - if (part.type === "text" && (part.text.startsWith("") || part.text.includes(""))) - return true - if ("metadata" in part && part.metadata && Boolean((part.metadata as Record).synthetic)) - return true - return false -} - +// Mark the last text part of `messages[index]`. If no text part exists, mark +// the last content part regardless of type — that's the breakpoint position +// in tool-result-only messages too. const markMessageAt = (messages: ReadonlyArray, index: number, hint: CacheHint): ReadonlyArray => { if (index < 0 || index >= messages.length) return messages const target = messages[index]! if (target.content.length === 0) return messages - const lastStableTextIndex = target.content.findLastIndex((part) => part.type === "text" && !isEphemeralPart(part)) const lastTextIndex = target.content.findLastIndex((part) => part.type === "text") - const markAt = - lastStableTextIndex >= 0 - ? lastStableTextIndex - : lastTextIndex >= 0 - ? lastTextIndex - : target.content.length - 1 - // kilocode_change end + const markAt = lastTextIndex >= 0 ? lastTextIndex : target.content.length - 1 const existing = target.content[markAt]! if ("cache" in existing && existing.cache) return messages const nextContent = target.content.map((part, i) => (i === markAt ? ({ ...part, cache: hint } as ContentPart) : part)) diff --git a/packages/llm/src/index.ts b/packages/llm/src/index.ts index 1a9fd1fd3de..735520ff77c 100644 --- a/packages/llm/src/index.ts +++ b/packages/llm/src/index.ts @@ -26,7 +26,6 @@ export type { ToolToModelOutput, } from "./tool" export * as LLM from "./llm" -export { supportsPromptCacheBreakpoint } from "./protocols/utils/openai-options" // kilocode_change export type { Definition as ProviderDefinition, ModelFactory as ProviderModelFactory, diff --git a/packages/llm/src/protocols/openai-chat.ts b/packages/llm/src/protocols/openai-chat.ts index 9db95915669..c6ac16c7b3b 100644 --- a/packages/llm/src/protocols/openai-chat.ts +++ b/packages/llm/src/protocols/openai-chat.ts @@ -28,12 +28,6 @@ const IMAGE_MIMES = new Set(ProviderShared.IMAGE_MIMES) export const DEFAULT_BASE_URL = "https://api.openai.com/v1" export const PATH = "/chat/completions" -// kilocode_change start - explicit prompt cache breakpoints for GPT-5.6+ -const OpenAIChatPromptCacheBreakpoint = Schema.Struct({ - mode: Schema.Literal("explicit"), -}) -// kilocode_change end - // ============================================================================= // Request Body Schema // ============================================================================= @@ -63,29 +57,15 @@ const OpenAIChatAssistantToolCall = Schema.Struct({ type OpenAIChatAssistantToolCall = Schema.Schema.Type const OpenAIChatUserContent = Schema.Union([ - Schema.Struct({ - type: Schema.Literal("text"), - text: Schema.String, - prompt_cache_breakpoint: Schema.optional(OpenAIChatPromptCacheBreakpoint), // kilocode_change - }), + Schema.Struct({ type: Schema.Literal("text"), text: Schema.String }), Schema.Struct({ type: Schema.Literal("image_url"), image_url: Schema.Struct({ url: Schema.String }), - prompt_cache_breakpoint: Schema.optional(OpenAIChatPromptCacheBreakpoint), // kilocode_change }), ]) const OpenAIChatMessage = Schema.Union([ - // kilocode_change start - support content block array for system/developer messages - Schema.Struct({ - role: Schema.Literal("system"), - content: Schema.Union([Schema.String, Schema.Array(OpenAIChatUserContent)]), - }), - Schema.Struct({ - role: Schema.Literal("developer"), - content: Schema.Union([Schema.String, Schema.Array(OpenAIChatUserContent)]), - }), - // kilocode_change end + Schema.Struct({ role: Schema.Literal("system"), content: Schema.String }), Schema.Struct({ role: Schema.Literal("user"), content: Schema.Union([Schema.String, Schema.Array(OpenAIChatUserContent)]), @@ -230,33 +210,21 @@ const lowerMedia = Effect.fn("OpenAIChat.lowerMedia")(function* (part: MediaPart const openAICompatibleReasoningContent = (native: unknown) => isRecord(native) && typeof native.reasoning_content === "string" ? native.reasoning_content : undefined -const lowerUserMessage = Effect.fn("OpenAIChat.lowerUserMessage")(function* ( - message: OpenAIChatRequestMessage, - modelId: string, // kilocode_change -) { +const lowerUserMessage = Effect.fn("OpenAIChat.lowerUserMessage")(function* (message: OpenAIChatRequestMessage) { const content: Array> = [] for (const part of message.content) { - // kilocode_change start - const breakpoint = - "cache" in part && part.cache && OpenAIOptions.supportsPromptCacheBreakpoint(modelId) - ? { prompt_cache_breakpoint: { mode: "explicit" as const } } - : {} if (part.type === "text") { - content.push({ type: "text", text: part.text, ...breakpoint }) + content.push({ type: "text", text: part.text }) continue } if (part.type === "media") { - content.push({ ...(yield* lowerMedia(part)), ...breakpoint }) + content.push(yield* lowerMedia(part)) continue } - // kilocode_change end return yield* ProviderShared.unsupportedContent("OpenAI Chat", "user", ["text", "media"]) } - // kilocode_change start - const hasBreakpoint = content.some((part) => "prompt_cache_breakpoint" in part && part.prompt_cache_breakpoint) - if (!hasBreakpoint && content.every((part) => part.type === "text")) - return { role: "user" as const, content: content.map((part) => (part as { text: string }).text).join("\n") } - // kilocode_change end + if (content.every((part) => part.type === "text")) + return { role: "user" as const, content: content.map((part) => part.text).join("\n") } // kilocode_change return { role: "user" as const, content } }) @@ -316,36 +284,15 @@ const lowerToolMessages = Effect.fn("OpenAIChat.lowerToolMessages")(function* (m return { messages, images } }) -const lowerMessage = Effect.fn("OpenAIChat.lowerMessage")(function* ( - message: OpenAIChatRequestMessage, - modelId: string, // kilocode_change -) { - if (message.role === "user") return [yield* lowerUserMessage(message, modelId)] // kilocode_change +const lowerMessage = Effect.fn("OpenAIChat.lowerMessage")(function* (message: OpenAIChatRequestMessage) { + if (message.role === "user") return [yield* lowerUserMessage(message)] if (message.role === "assistant") return [yield* lowerAssistantMessage(message)] return (yield* lowerToolMessages(message)).messages }) const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request: LLMRequest) { - // kilocode_change start - const hasSystemCache = request.system.some((part) => part.cache) && OpenAIOptions.supportsPromptCacheBreakpoint(request.model.id) const system: OpenAIChatMessage[] = - request.system.length === 0 - ? [] - : hasSystemCache - ? [ - { - role: "system", - content: [ - { - type: "text", - text: ProviderShared.joinText(request.system), - prompt_cache_breakpoint: { mode: "explicit" }, - }, - ], - }, - ] - : [{ role: "system", content: ProviderShared.joinText(request.system) }] - // kilocode_change end + request.system.length === 0 ? [] : [{ role: "system", content: ProviderShared.joinText(request.system) }] const messages = [...system] const pendingImages: Array> = [] const flushImages = () => { @@ -377,7 +324,7 @@ const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request: continue } flushImages() - messages.push(...(yield* lowerMessage(message, request.model.id))) // kilocode_change + messages.push(...(yield* lowerMessage(message))) } flushImages() return messages diff --git a/packages/llm/src/protocols/openai-responses.ts b/packages/llm/src/protocols/openai-responses.ts index 74b4524518d..2e6b67aec72 100644 --- a/packages/llm/src/protocols/openai-responses.ts +++ b/packages/llm/src/protocols/openai-responses.ts @@ -29,24 +29,16 @@ const ADAPTER = "openai-responses" export const DEFAULT_BASE_URL = "https://api.openai.com/v1" export const PATH = "/responses" -// kilocode_change start - explicit prompt cache breakpoints for GPT-5.6+ -const OpenAIResponsesPromptCacheBreakpoint = Schema.Struct({ - mode: Schema.Literal("explicit"), -}) -// kilocode_change end - // ============================================================================= // Request Body Schema // ============================================================================= const OpenAIResponsesInputText = Schema.Struct({ type: Schema.tag("input_text"), text: Schema.String, - prompt_cache_breakpoint: Schema.optional(OpenAIResponsesPromptCacheBreakpoint), // kilocode_change }) const OpenAIResponsesInputImage = Schema.Struct({ type: Schema.tag("input_image"), image_url: Schema.String, - prompt_cache_breakpoint: Schema.optional(OpenAIResponsesPromptCacheBreakpoint), // kilocode_change }) const OpenAIResponsesInputContent = Schema.Union([OpenAIResponsesInputText, OpenAIResponsesInputImage]) type OpenAIResponsesInputContent = Schema.Schema.Type @@ -84,16 +76,7 @@ const OpenAIResponsesFunctionCallOutput = Schema.Union([ ]) const OpenAIResponsesInputItem = Schema.Union([ - // kilocode_change start - support content block array for system/developer messages - Schema.Struct({ - role: Schema.Literal("system"), - content: Schema.Union([Schema.String, Schema.Array(OpenAIResponsesInputContent)]), - }), - Schema.Struct({ - role: Schema.Literal("developer"), - content: Schema.Union([Schema.String, Schema.Array(OpenAIResponsesInputContent)]), - }), - // kilocode_change end + Schema.Struct({ role: Schema.tag("system"), content: Schema.String }), Schema.Struct({ role: Schema.tag("user"), content: Schema.Array(OpenAIResponsesInputContent) }), Schema.Struct({ role: Schema.tag("assistant"), content: Schema.Array(OpenAIResponsesOutputText) }), OpenAIResponsesReasoningItem, @@ -324,23 +307,16 @@ const hostedToolItemID = (part: ToolResultPart) => { const lowerUserContent = Effect.fn("OpenAIResponses.lowerUserContent")(function* ( part: LLMRequest["messages"][number]["content"][number], - modelId: string, // kilocode_change ) { - // kilocode_change start - const breakpoint = - "cache" in part && part.cache && OpenAIOptions.supportsPromptCacheBreakpoint(modelId) - ? { prompt_cache_breakpoint: { mode: "explicit" as const } } - : {} - if (part.type === "text") return { type: "input_text" as const, text: part.text, ...breakpoint } + if (part.type === "text") return { type: "input_text" as const, text: part.text } if (part.type === "media") { const media = yield* ProviderShared.validateMedia( "OpenAI Responses", part, new Set(ProviderShared.IMAGE_MIMES), ) - return { type: "input_image" as const, image_url: media.dataUrl, ...breakpoint } + return { type: "input_image" as const, image_url: media.dataUrl } } - // kilocode_change end return yield* ProviderShared.unsupportedContent("OpenAI Responses", "user", ["text", "media"]) }) @@ -368,26 +344,8 @@ const lowerToolResultOutput = Effect.fn("OpenAIResponses.lowerToolResultOutput") }) const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (request: LLMRequest) { - // kilocode_change start - const hasSystemCache = request.system.some((part) => part.cache) && OpenAIOptions.supportsPromptCacheBreakpoint(request.model.id) const system: OpenAIResponsesInputItem[] = - request.system.length === 0 - ? [] - : hasSystemCache - ? [ - { - role: "system", - content: [ - { - type: "input_text", - text: ProviderShared.joinText(request.system), - prompt_cache_breakpoint: { mode: "explicit" }, - }, - ], - }, - ] - : [{ role: "system", content: ProviderShared.joinText(request.system) }] - // kilocode_change end + request.system.length === 0 ? [] : [{ role: "system", content: ProviderShared.joinText(request.system) }] const input: OpenAIResponsesInputItem[] = [...system] const store = OpenAIOptions.store(request) @@ -405,12 +363,7 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ } if (message.role === "user") { - // kilocode_change start - input.push({ - role: "user", - content: yield* Effect.forEach(message.content, (part) => lowerUserContent(part, request.model.id)), - }) - // kilocode_change end + input.push({ role: "user", content: yield* Effect.forEach(message.content, lowerUserContent) }) continue } diff --git a/packages/llm/src/protocols/utils/openai-options.ts b/packages/llm/src/protocols/utils/openai-options.ts index 708dc07d28c..59ac7abc874 100644 --- a/packages/llm/src/protocols/utils/openai-options.ts +++ b/packages/llm/src/protocols/utils/openai-options.ts @@ -101,18 +101,4 @@ export const instructions = (request: LLMRequest) => { return typeof value === "string" ? value : undefined } -// kilocode_change start - explicit prompt cache breakpoints for GPT-5.6+ -export const supportsPromptCacheBreakpoint = (modelId: string): boolean => { - const match = modelId.match(/gpt-(\d+)\.(\d+)/) - if (match) { - const major = Number(match[1]) - const minor = Number(match[2]) - if (major > 5 || (major === 5 && minor >= 6)) return true - } - const majorMatch = modelId.match(/gpt-(\d+)/) - if (majorMatch && Number(majorMatch[1]) >= 6) return true - return false -} -// kilocode_change end - export * as OpenAIOptions from "./openai-options" diff --git a/packages/llm/test/cache-policy.test.ts b/packages/llm/test/cache-policy.test.ts index b0f3256e988..a126d9502c5 100644 --- a/packages/llm/test/cache-policy.test.ts +++ b/packages/llm/test/cache-policy.test.ts @@ -6,9 +6,6 @@ import { AmazonBedrock } from "../src/providers" import * as AnthropicMessages from "../src/protocols/anthropic-messages" import * as Gemini from "../src/protocols/gemini" import * as OpenAIChat from "../src/protocols/openai-chat" -// kilocode_change start -import * as OpenAIResponses from "../src/protocols/openai-responses" -// kilocode_change end import { applyCachePolicy } from "../src/cache-policy" import { it } from "./lib/effect" @@ -24,16 +21,6 @@ const openaiModel = OpenAIChat.route .with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") }) .model({ id: "gpt-4o-mini" }) -// kilocode_change start -const openaiGpt56ResponsesModel = OpenAIResponses.route - .with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") }) - .model({ id: "gpt-5.6" }) - -const openaiGpt56ChatModel = OpenAIChat.route - .with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") }) - .model({ id: "gpt-5.6" }) -// kilocode_change end - const geminiModel = Gemini.route .with({ endpoint: { baseURL: "https://generativelanguage.test/v1beta/" }, @@ -92,8 +79,7 @@ describe("applyCachePolicy", () => { }), ) - // kilocode_change start - it.effect("'auto' does not emit explicit breakpoints on pre-5.6 OpenAI models", () => + it.effect("'auto' is a no-op on OpenAI (implicit caching protocol)", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( LLM.request({ @@ -105,125 +91,13 @@ describe("applyCachePolicy", () => { ) const body = prepared.body as { messages: Array<{ content: unknown }> } - // Older OpenAI models reject prompt_cache_breakpoint — policy must skip. + // OpenAI doesn't accept cache_control on messages — policy must skip. const flat = JSON.stringify(body) - expect(flat).not.toContain("prompt_cache_breakpoint") expect(flat).not.toContain("cache_control") expect(flat).not.toContain("cachePoint") }), ) - it.effect("'auto' emits prompt_cache_breakpoint on stable system prefix and latest user on GPT-5.6 Responses", () => - Effect.gen(function* () { - const prepared = yield* LLMClient.prepare( - LLM.request({ - model: openaiGpt56ResponsesModel, - system: "System instructions", - messages: [ - Message.user("first question"), - Message.assistant("assistant reply"), - Message.user("latest question"), - ], - cache: "auto", - }), - ) - - expect(prepared.body).toMatchObject({ - input: [ - { - role: "system", - content: [{ type: "input_text", text: "System instructions", prompt_cache_breakpoint: { mode: "explicit" } }], - }, - { role: "user", content: [{ type: "input_text", text: "first question" }] }, - { role: "assistant", content: [{ type: "output_text", text: "assistant reply" }] }, - { - role: "user", - content: [{ type: "input_text", text: "latest question", prompt_cache_breakpoint: { mode: "explicit" } }], - }, - ], - }) - }), - ) - - it.effect("'auto' emits prompt_cache_breakpoint on stable system prefix and latest user on GPT-5.6 Chat", () => - Effect.gen(function* () { - const prepared = yield* LLMClient.prepare( - LLM.request({ - model: openaiGpt56ChatModel, - system: "System instructions", - messages: [ - Message.user("first question"), - Message.assistant("assistant reply"), - Message.user("latest question"), - ], - cache: "auto", - }), - ) - - expect(prepared.body).toMatchObject({ - messages: [ - { - role: "system", - content: [{ type: "text", text: "System instructions", prompt_cache_breakpoint: { mode: "explicit" } }], - }, - { role: "user", content: "first question" }, - { role: "assistant", content: "assistant reply" }, - { - role: "user", - content: [{ type: "text", text: "latest question", prompt_cache_breakpoint: { mode: "explicit" } }], - }, - ], - }) - }), - ) - - it.effect("places prompt_cache_breakpoint BEFORE trailing part", () => - Effect.gen(function* () { - const envBlock = "\nCurrent time: 2026-08-08T18:00:00+00:00\n" - const prepared = yield* LLMClient.prepare( - LLM.request({ - model: openaiGpt56ResponsesModel, - system: "System instructions", - messages: [ - Message.user([ - { type: "text", text: "Please inspect the codebase" }, - { type: "text", text: envBlock }, - ]), - ], - cache: "auto", - }), - ) - - expect(prepared.body).toMatchObject({ - input: [ - { - role: "system", - content: [{ type: "input_text", text: "System instructions", prompt_cache_breakpoint: { mode: "explicit" } }], - }, - { - role: "user", - content: [ - { - type: "input_text", - text: "Please inspect the codebase", - prompt_cache_breakpoint: { mode: "explicit" }, - }, - { - type: "input_text", - text: envBlock, - }, - ], - }, - ], - }) - - const userContent = (prepared.body as any).input[1].content - expect(userContent[0].prompt_cache_breakpoint).toEqual({ mode: "explicit" }) - expect(userContent[1].prompt_cache_breakpoint).toBeUndefined() - }), - ) - // kilocode_change end - it.effect("'auto' is a no-op on Gemini (out-of-band caching protocol)", () => Effect.gen(function* () { const prepared = yield* LLMClient.prepare( diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index f0173b50303..6009891d05e 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -1,7 +1,6 @@ import type { ModelMessage, ToolResultPart } from "ai" import { mergeDeep, unique } from "remeda" import type { JSONSchema7 } from "@ai-sdk/provider" -import { supportsPromptCacheBreakpoint } from "@opencode-ai/llm" // kilocode_change import type * as Provider from "./provider" import type * as ModelsDev from "@opencode-ai/core/models-dev" import { iife } from "@/util/iife" @@ -329,6 +328,20 @@ function normalizeMessages( return msgs } +// kilocode_change start - explicit prompt cache breakpoints for GPT-5.6+ +function supportsPromptCacheBreakpoint(modelId: string): boolean { + const match = modelId.match(/gpt-(\d+)\.(\d+)/) + if (match) { + const major = Number(match[1]) + const minor = Number(match[2]) + if (major > 5 || (major === 5 && minor >= 6)) return true + } + const majorMatch = modelId.match(/gpt-(\d+)/) + if (majorMatch && Number(majorMatch[1]) >= 6) return true + return false +} +// kilocode_change end + function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] { const system = msgs.filter((msg) => msg.role === "system").slice(0, 2) const final = msgs.filter((msg) => msg.role !== "system").slice(-2) From 1b0f94ec74e66331a02bcc06c337ed662e1e6412 Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Sat, 8 Aug 2026 18:25:29 +0000 Subject: [PATCH 44/73] feat(cli): enable explicit prompt cache options for OpenAI models on Kilo Gateway Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- packages/opencode/src/provider/transform.ts | 6 ++-- .../opencode/test/provider/transform.test.ts | 35 +++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 6009891d05e..efb6ef760ad 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -480,7 +480,7 @@ function mapProviderOptions( export function message(msgs: ModelMessage[], model: Provider.Model, options: Record) { msgs = unsupportedParts(msgs, model) msgs = normalizeMessages(msgs, model, options) - // kilocode_change start - apply caching for anthropic, alibaba, and GPT-5.6+ openai/azure + // kilocode_change start - apply caching for anthropic, alibaba, and GPT-5.6+ openai/azure/kilo-gateway if ( (model.providerID === "anthropic" || model.providerID === "google-vertex-anthropic" || @@ -492,8 +492,10 @@ export function message(msgs: ModelMessage[], model: Provider.Model, options: Re model.api.npm === "@ai-sdk/alibaba" || ((model.api.npm === "@ai-sdk/openai" || model.api.npm === "@ai-sdk/azure" || + model.api.npm === "@kilocode/kilo-gateway" || model.providerID === "openai" || - model.providerID === "azure") && + model.providerID === "azure" || + model.providerID === "kilo") && supportsPromptCacheBreakpoint(model.api.id))) && model.api.npm !== "@ai-sdk/gateway" ) { diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 2f8c7d23b18..74ef174ade8 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -3127,6 +3127,41 @@ describe("ProviderTransform.message - cache control on gateway", () => { }) expect(result[1].content[1].providerOptions?.openai?.promptCacheBreakpoint).toBeUndefined() }) + + test("kilo gateway with openai gpt-5.6 applies caching options", () => { + const model = createModel({ + providerID: "kilo", + api: { + id: "openai/gpt-5.6", + url: "https://api.kilo.ai/api/gateway", + npm: "@kilocode/kilo-gateway", + }, + id: "openai/gpt-5.6", + }) + const msgs = [ + { + role: "system", + content: "You are a helpful assistant", + }, + { + role: "user", + content: "Hello", + }, + ] as any[] + + const result = ProviderTransform.message(msgs, model, {}) as any[] + + expect(result[0].providerOptions.openrouter).toEqual({ + cacheControl: { + type: "ephemeral", + }, + }) + expect(result[1].providerOptions.openrouter).toEqual({ + cacheControl: { + type: "ephemeral", + }, + }) + }) // kilocode_change end }) From bca459832456bdba75e48c675122c324568870ea Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Sat, 8 Aug 2026 18:28:46 +0000 Subject: [PATCH 45/73] refactor(cli): simplify provider checks and narrow kilocode_change markers in transform.ts Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- packages/opencode/src/provider/transform.ts | 31 ++++++++------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index efb6ef760ad..d366119a5f3 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -384,10 +384,10 @@ function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage model.providerID === "anthropic" || model.providerID.includes("bedrock") || model.api.npm === "@ai-sdk/amazon-bedrock" - // kilocode_change start - place caching breakpoint on stable content before trailing const shouldUseContentOptions = !useMessageLevelOptions && Array.isArray(msg.content) && msg.content.length > 0 if (shouldUseContentOptions) { + // kilocode_change start - place caching breakpoint on stable content before trailing const parts = msg.content as any[] const targetIndex = parts.findLastIndex( (part) => @@ -395,25 +395,21 @@ function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage typeof part === "object" && part.type !== "tool-approval-request" && part.type !== "tool-approval-response" && - !( - part.type === "text" && - typeof part.text === "string" && - (part.text.startsWith("") || part.text.includes("")) - ) && + !(part.type === "text" && typeof part.text === "string" && part.text.startsWith("")) && !part.synthetic, ) - const targetContent = targetIndex >= 0 ? parts[targetIndex] : parts[parts.length - 1] + const lastContent = targetIndex >= 0 ? parts[targetIndex] : parts[parts.length - 1] + // kilocode_change end if ( - targetContent && - typeof targetContent === "object" && - targetContent.type !== "tool-approval-request" && - targetContent.type !== "tool-approval-response" + lastContent && + typeof lastContent === "object" && + lastContent.type !== "tool-approval-request" && + lastContent.type !== "tool-approval-response" ) { - targetContent.providerOptions = mergeDeep(targetContent.providerOptions ?? {}, providerOptions) + lastContent.providerOptions = mergeDeep(lastContent.providerOptions ?? {}, providerOptions) continue } } - // kilocode_change end msg.providerOptions = mergeDeep(msg.providerOptions ?? {}, providerOptions) } @@ -480,7 +476,6 @@ function mapProviderOptions( export function message(msgs: ModelMessage[], model: Provider.Model, options: Record) { msgs = unsupportedParts(msgs, model) msgs = normalizeMessages(msgs, model, options) - // kilocode_change start - apply caching for anthropic, alibaba, and GPT-5.6+ openai/azure/kilo-gateway if ( (model.providerID === "anthropic" || model.providerID === "google-vertex-anthropic" || @@ -490,18 +485,16 @@ export function message(msgs: ModelMessage[], model: Provider.Model, options: Re model.id.includes("claude") || model.api.npm === "@ai-sdk/anthropic" || model.api.npm === "@ai-sdk/alibaba" || + // kilocode_change start ((model.api.npm === "@ai-sdk/openai" || model.api.npm === "@ai-sdk/azure" || - model.api.npm === "@kilocode/kilo-gateway" || - model.providerID === "openai" || - model.providerID === "azure" || - model.providerID === "kilo") && + model.api.npm === "@kilocode/kilo-gateway") && supportsPromptCacheBreakpoint(model.api.id))) && + // kilocode_change end model.api.npm !== "@ai-sdk/gateway" ) { msgs = applyCaching(msgs, model) } - // kilocode_change end // Remap providerOptions keys from stored providerID to expected SDK key const key = sdkKey(model.api.npm) From 7c7c11e33400a4640ad7b6cb031921964c7fe70b Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Sat, 8 Aug 2026 18:34:00 +0000 Subject: [PATCH 46/73] fix(cli): include modified alibaba line in kilocode_change block Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- packages/opencode/src/provider/transform.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index d366119a5f3..cf876f0c56c 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -484,8 +484,8 @@ export function message(msgs: ModelMessage[], model: Provider.Model, options: Re model.id.includes("anthropic") || model.id.includes("claude") || model.api.npm === "@ai-sdk/anthropic" || - model.api.npm === "@ai-sdk/alibaba" || // kilocode_change start + model.api.npm === "@ai-sdk/alibaba" || ((model.api.npm === "@ai-sdk/openai" || model.api.npm === "@ai-sdk/azure" || model.api.npm === "@kilocode/kilo-gateway") && From d3548d179bf9814e16cd4acdeb91411bd046aaac Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Sat, 8 Aug 2026 18:52:47 +0000 Subject: [PATCH 47/73] refactor(cli): remove part.synthetic and any-cast from applyCaching Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- packages/opencode/src/provider/transform.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index cf876f0c56c..d8b04db4662 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -388,18 +388,15 @@ function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage if (shouldUseContentOptions) { // kilocode_change start - place caching breakpoint on stable content before trailing - const parts = msg.content as any[] - const targetIndex = parts.findLastIndex( + const target = msg.content.findLast( (part) => - part && typeof part === "object" && + part !== null && part.type !== "tool-approval-request" && part.type !== "tool-approval-response" && - !(part.type === "text" && typeof part.text === "string" && part.text.startsWith("")) && - !part.synthetic, + !(part.type === "text" && part.text.startsWith("")), ) - const lastContent = targetIndex >= 0 ? parts[targetIndex] : parts[parts.length - 1] - // kilocode_change end + const lastContent = target ?? msg.content[msg.content.length - 1] if ( lastContent && typeof lastContent === "object" && @@ -409,6 +406,7 @@ function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage lastContent.providerOptions = mergeDeep(lastContent.providerOptions ?? {}, providerOptions) continue } + // kilocode_change end } msg.providerOptions = mergeDeep(msg.providerOptions ?? {}, providerOptions) @@ -476,6 +474,7 @@ function mapProviderOptions( export function message(msgs: ModelMessage[], model: Provider.Model, options: Record) { msgs = unsupportedParts(msgs, model) msgs = normalizeMessages(msgs, model, options) + // kilocode_change start - apply caching for anthropic, alibaba, and GPT-5.6+ openai/azure/kilo-gateway if ( (model.providerID === "anthropic" || model.providerID === "google-vertex-anthropic" || @@ -484,17 +483,16 @@ export function message(msgs: ModelMessage[], model: Provider.Model, options: Re model.id.includes("anthropic") || model.id.includes("claude") || model.api.npm === "@ai-sdk/anthropic" || - // kilocode_change start model.api.npm === "@ai-sdk/alibaba" || ((model.api.npm === "@ai-sdk/openai" || model.api.npm === "@ai-sdk/azure" || model.api.npm === "@kilocode/kilo-gateway") && supportsPromptCacheBreakpoint(model.api.id))) && - // kilocode_change end model.api.npm !== "@ai-sdk/gateway" ) { msgs = applyCaching(msgs, model) } + // kilocode_change end // Remap providerOptions keys from stored providerID to expected SDK key const key = sdkKey(model.api.npm) From 71abd522b1fd8988046df726f08f9e804e8f45ff Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Sat, 8 Aug 2026 19:06:53 +0000 Subject: [PATCH 48/73] fix(cli): use typed loop and narrow kilocode_change block in applyCaching Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- packages/opencode/src/provider/transform.ts | 25 +++++++++++++-------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index d8b04db4662..ff534bf107f 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -386,17 +386,24 @@ function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage model.api.npm === "@ai-sdk/amazon-bedrock" const shouldUseContentOptions = !useMessageLevelOptions && Array.isArray(msg.content) && msg.content.length > 0 - if (shouldUseContentOptions) { - // kilocode_change start - place caching breakpoint on stable content before trailing - const target = msg.content.findLast( - (part) => + // kilocode_change start - place caching breakpoint on stable content before trailing + if (shouldUseContentOptions && Array.isArray(msg.content)) { + const parts = msg.content + let targetIndex = -1 + for (let i = parts.length - 1; i >= 0; i--) { + const part = parts[i] + if ( + part && typeof part === "object" && - part !== null && part.type !== "tool-approval-request" && part.type !== "tool-approval-response" && - !(part.type === "text" && part.text.startsWith("")), - ) - const lastContent = target ?? msg.content[msg.content.length - 1] + !(part.type === "text" && part.text.startsWith("")) + ) { + targetIndex = i + break + } + } + const lastContent = targetIndex >= 0 ? parts[targetIndex] : parts[parts.length - 1] if ( lastContent && typeof lastContent === "object" && @@ -406,8 +413,8 @@ function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage lastContent.providerOptions = mergeDeep(lastContent.providerOptions ?? {}, providerOptions) continue } - // kilocode_change end } + // kilocode_change end msg.providerOptions = mergeDeep(msg.providerOptions ?? {}, providerOptions) } From 4ff8f5e1105652b98f49210608e9f28e98a92f8b Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Sun, 9 Aug 2026 15:44:33 +0000 Subject: [PATCH 49/73] feat(vscode): show Auto model routes for every kilo-auto model Show the underlying model list in the selector whenever an Auto model includes autoRouting, not only kilo-auto/efficient. Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .changeset/auto-model-route-choices.md | 5 ++++ .../model-selector-accessibility.spec.ts | 14 ++++++++++ .../tests/unit/model-selector-utils.test.ts | 26 ++++++++++++++++--- .../components/shared/model-selector-utils.ts | 7 +---- .../webview-ui/src/stories/shared.stories.tsx | 13 +++++++++- 5 files changed, 55 insertions(+), 10 deletions(-) create mode 100644 .changeset/auto-model-route-choices.md diff --git a/.changeset/auto-model-route-choices.md b/.changeset/auto-model-route-choices.md new file mode 100644 index 00000000000..6edfcabe146 --- /dev/null +++ b/.changeset/auto-model-route-choices.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Show underlying models for every Auto model in the model selector when the catalog includes them. diff --git a/packages/kilo-vscode/tests/model-selector-accessibility.spec.ts b/packages/kilo-vscode/tests/model-selector-accessibility.spec.ts index 785bf9faf2f..de54f3b5e00 100644 --- a/packages/kilo-vscode/tests/model-selector-accessibility.spec.ts +++ b/packages/kilo-vscode/tests/model-selector-accessibility.spec.ts @@ -80,6 +80,20 @@ test("auto efficient details show server description and model choices", async ( await expect(preview).not.toContainText("openai/gpt-5.5") }) +test("auto frontier details show model choices when routes are present", async ({ page }) => { + await load(page, "shared--model-selector-accessible") + + await page.getByRole("button", { name: "Review model: Alpha" }).click() + await page.getByRole("treeitem", { name: /Kilo Auto Frontier/ }).click() + + const preview = page.locator(".model-selector-preview") + await expect(preview).toContainText("Routes each request to the strongest available models.") + await expect(preview).toContainText("Model choices") + await expect(preview).toContainText("openai/gpt-5.5") + await expect(preview).toContainText("anthropic/claude-opus-4.6") + await expect(preview).not.toContainText("google/gemini-2.5-flash") +}) + test("search uses a flat relevance-ranked result list with provider labels", async ({ page }) => { await load(page, "shared--model-selector-accessible") diff --git a/packages/kilo-vscode/tests/unit/model-selector-utils.test.ts b/packages/kilo-vscode/tests/unit/model-selector-utils.test.ts index d1c681f89fc..59956de5ef1 100644 --- a/packages/kilo-vscode/tests/unit/model-selector-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/model-selector-utils.test.ts @@ -128,7 +128,7 @@ describe("isAuto", () => { }) describe("autoChoices", () => { - it("uses backend Auto Efficient routes and resolves names when available", () => { + it("uses backend Auto routes and resolves names when available", () => { expect( autoChoices( { @@ -144,12 +144,32 @@ describe("autoChoices", () => { ]) }) - it("ignores missing routes and non-efficient Auto models", () => { + it("shows routes for any Auto model when present", () => { + expect( + autoChoices( + { + providerID: KILO_GATEWAY_ID, + id: "kilo-auto/frontier", + autoRouting: { models: ["provider/model"] }, + }, + [{ id: "provider/model", name: "Provider: Model" }], + ), + ).toEqual([{ id: "provider/model", name: "Model" }]) + expect( + autoChoices({ + providerID: KILO_GATEWAY_ID, + id: "kilo-auto/free", + autoRouting: { models: ["provider/model"] }, + }), + ).toEqual([{ id: "provider/model", name: "provider/model" }]) + }) + + it("ignores missing routes and non-Auto models", () => { expect(autoChoices({ providerID: KILO_GATEWAY_ID, id: "kilo-auto/efficient" })).toEqual([]) expect( autoChoices({ providerID: KILO_GATEWAY_ID, - id: "kilo-auto/frontier", + id: "anthropic/claude-sonnet", autoRouting: { models: ["provider/model"] }, }), ).toEqual([]) diff --git a/packages/kilo-vscode/webview-ui/src/components/shared/model-selector-utils.ts b/packages/kilo-vscode/webview-ui/src/components/shared/model-selector-utils.ts index d93d142e37b..80d70c9cf8c 100644 --- a/packages/kilo-vscode/webview-ui/src/components/shared/model-selector-utils.ts +++ b/packages/kilo-vscode/webview-ui/src/components/shared/model-selector-utils.ts @@ -10,7 +10,6 @@ import { export { KILO_GATEWAY_ID, PROVIDER_ORDER } export const KILO_AUTO_SMALL_IDS = new Set(["kilo-auto/small", "auto-small"]) -export const KILO_AUTO_EFFICIENT_ID = "kilo-auto/efficient" const AUTO_FALLBACK = "Routes requests automatically." interface Choice { @@ -24,15 +23,11 @@ export function isAuto(model: Pick): boolean ) } -export function isAutoEfficient(model: Pick): boolean { - return model.providerID === KILO_GATEWAY_ID && model.id === KILO_AUTO_EFFICIENT_ID -} - export function autoChoices( model: Pick, catalog: readonly Pick[] = [], ): readonly Choice[] { - if (!isAutoEfficient(model)) return [] + if (!isAuto(model)) return [] const ids = model.autoRouting?.models if (!ids?.length) return [] const names = new Map(catalog.map((item) => [item.id, stripSubProviderPrefix(sanitizeName(item.name))])) diff --git a/packages/kilo-vscode/webview-ui/src/stories/shared.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/shared.stories.tsx index 48d6bd94a8a..8753cb96ae1 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/shared.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/shared.stories.tsx @@ -72,7 +72,18 @@ const ACCESSIBLE_MODELS: EnrichedModel[] = [ }, autoRouting: { models: ["google/gemini-2.5-flash", "anthropic/claude-sonnet-4.6"] }, }, - { id: "omega", name: "Omega", providerID: "openai", providerName: "OpenAI", recommendedIndex: 1 }, + { + id: "kilo-auto/frontier", + name: "Kilo Auto Frontier", + providerID: "kilo", + providerName: "Kilo", + recommendedIndex: 1, + options: { + description: "Routes each request to the strongest available models.", + }, + autoRouting: { models: ["openai/gpt-5.5", "anthropic/claude-opus-4.6"] }, + }, + { id: "omega", name: "Omega", providerID: "openai", providerName: "OpenAI", recommendedIndex: 2 }, { id: "alpha", name: "Alpha", providerID: "kilo", providerName: "Kilo" }, { id: "bravo", name: "Bravo", providerID: "kilo", providerName: "Kilo" }, { id: "charlie", name: "Charlie", providerID: "kilo", providerName: "Kilo" }, From 33a3eab7510f111cf12d16607d7b0bde9876318b Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Sun, 9 Aug 2026 14:29:39 -0400 Subject: [PATCH 50/73] docs(kilo-docs): correct session paths and scope --- .../pages/code-with-ai/agents/context-mentions.md | 4 ++-- .../pages/code-with-ai/agents/session-history.md | 12 ++++++------ .../troubleshooting/troubleshooting-extension.md | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/kilo-docs/pages/code-with-ai/agents/context-mentions.md b/packages/kilo-docs/pages/code-with-ai/agents/context-mentions.md index 0e433cca870..b0a7ec85091 100644 --- a/packages/kilo-docs/pages/code-with-ai/agents/context-mentions.md +++ b/packages/kilo-docs/pages/code-with-ai/agents/context-mentions.md @@ -31,7 +31,7 @@ Selecting a suggestion inserts the mention and highlights it in the input. File ### Referencing Past Chats -Choosing **Past chats** from the `@` menu opens a searchable picker of sessions in the current workspace, ordered by recency. Search matches session titles and workspace names. Selecting a session inserts a highlighted mention token; when you send the message, that session's current chat history is added as context so the agent can build on the earlier conversation. Clicking the mention token opens the referenced session. +Choosing **Past chats** from the `@` menu opens a searchable picker of sessions in the current workspace and related Git worktrees, including Agent Manager worktrees, ordered by recency. Search matches session titles and worktree names. Selecting a session inserts a highlighted mention token; when you send the message, that session's current chat history is added as context so the agent can build on the earlier conversation. Clicking the mention token opens the referenced session. Very long chat histories are truncated, keeping the beginning and end, so a single mention cannot fill the context window. @@ -77,7 +77,7 @@ This means the agent can explore your entire project as needed, rather than bein | **Mention files when helpful** | If you know the exact file, mention its path to save the agent a search step | | **Keep editor tabs relevant** | Open tabs are passed as context, so keep relevant files open | | **Trust the agent's tools** | The agent can search, read, and explore your codebase — let it do the discovery work | -| **Reference a past chat** | Type `@` and choose **Past chats** to search sessions in the current workspace. Selecting a session adds its current chat history as context when you send the message. | +| **Reference a past chat** | Type `@` and choose **Past chats** to search the current workspace and related Git worktrees, including Agent Manager worktrees. Selecting a session adds its current chat history as context when you send the message. | {% /tab %} {% tab label="CLI" %} diff --git a/packages/kilo-docs/pages/code-with-ai/agents/session-history.md b/packages/kilo-docs/pages/code-with-ai/agents/session-history.md index 4b78b52ae28..02f1c0787ea 100644 --- a/packages/kilo-docs/pages/code-with-ai/agents/session-history.md +++ b/packages/kilo-docs/pages/code-with-ai/agents/session-history.md @@ -21,9 +21,9 @@ Different search surfaces cover different content and scopes: | [JetBrains History](#search-session-history-in-jetbrains) | Session titles | Sessions loaded in the selected Local or Cloud history view | | [CLI Past chats](#reference-a-past-chat-in-the-cli-tui) | Session titles | Current workspace | | [`kilo session list --search`](#filter-session-titles) | Session titles | Current workspace, or every local workspace with `--all` | -| [Ask Kilo to recall past chats](#ask-kilo-to-search-past-chats) | Titles and high-signal chat content | Current workspace | +| [Ask Kilo to recall past chats](#ask-kilo-to-search-past-chats) | Titles and high-signal chat content | Current workspace and related Git worktrees | | [VS Code chat search](#search-the-open-chat-session) | Rendered content in the open session | Current chat session | -| [VS Code Past chats](#add-a-past-chat-to-the-current-session) | Session titles and workspace names | Current workspace | +| [VS Code Past chats](#add-a-past-chat-to-the-current-session) | Session titles and worktree names | Current workspace and related Git worktrees, including Agent Manager worktrees | | [SQLite query](#query-sqlite-directly) | Any stored field you select | Entire selected local database | History and CLI list searches do **not** search message content. To find a string inside prompts or replies, ask Kilo to search past chats or use a direct SQLite query. @@ -58,7 +58,7 @@ Kilo loads older messages while the search is active so the search covers the fu ### Add a past chat to the current session -Type `@` in the chat input, select **Past chats**, and search by session title or workspace name. Selecting a result adds that past chat's content as context when you send the message from your current session. It does not switch or reopen sessions. Past chats includes sessions in the current workspace. +Type `@` in the chat input, select **Past chats**, and search by session title or worktree name. Selecting a result adds that past chat's content as context when you send the message from your current session. It does not switch or reopen sessions. Past chats includes sessions in the current workspace and related Git worktrees, including Agent Manager worktrees. {% /tab %} {% tab label="JetBrains" %} @@ -98,7 +98,7 @@ The simplest way to search message content across past local sessions is to ask Search my local sessions for "database disk image is malformed" and summarize the matching conversations. ``` -Kilo's local recall search covers session titles, user and assistant text, file references, and failed tool errors in the current workspace. Every query term must occur somewhere in a matching session; exact phrases and user-authored matches rank higher. +Kilo's local recall search covers session titles, user and assistant text, file references, and failed tool errors in the current workspace and related Git worktrees. Every query term must occur somewhere in a matching session; exact phrases and user-authored matches rank higher. The search includes archived and child sessions. It excludes reasoning, synthetic or ignored text, successful tool output, file contents, and other metadata. Kilo can then read the full chat history for a selected result. @@ -144,8 +144,8 @@ Stable installations normally use these paths: | Environment | Default path | |---|---| -| Windows | `%LOCALAPPDATA%\kilo\kilo.db` | -| macOS | `~/Library/Application Support/kilo/kilo.db` | +| Windows | `%USERPROFILE%\.local\share\kilo\kilo.db` | +| macOS | `~/.local/share/kilo/kilo.db` | | Linux | `~/.local/share/kilo/kilo.db` | | VS Code Remote SSH | `~/.local/share/kilo/kilo.db` on the remote machine | diff --git a/packages/kilo-docs/pages/getting-started/troubleshooting/troubleshooting-extension.md b/packages/kilo-docs/pages/getting-started/troubleshooting/troubleshooting-extension.md index 368d5ac134d..075843369e8 100644 --- a/packages/kilo-docs/pages/getting-started/troubleshooting/troubleshooting-extension.md +++ b/packages/kilo-docs/pages/getting-started/troubleshooting/troubleshooting-extension.md @@ -33,8 +33,8 @@ The default database location depends on where Kilo Code is running: | Environment | Database path | |---|---| -| Windows | `%LOCALAPPDATA%\kilo\kilo.db` | -| macOS | `~/Library/Application Support/kilo/kilo.db` | +| Windows | `%USERPROFILE%\.local\share\kilo\kilo.db` | +| macOS | `~/.local/share/kilo/kilo.db` | | Linux | `~/.local/share/kilo/kilo.db` | | VS Code Remote SSH | `~/.local/share/kilo/kilo.db` on the remote machine | From 48c4a4af227572011bf44c172ab0ae86e0c2a429 Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Mon, 10 Aug 2026 09:20:37 +0000 Subject: [PATCH 51/73] fix(gateway): ignore negative prices in model catalog Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .changeset/ignore-negative-model-prices.md | 7 +++ packages/kilo-gateway/src/api/models.ts | 2 +- packages/kilo-gateway/test/api/models.test.ts | 63 +++++++++++++++++++ .../tests/unit/model-price-format.test.ts | 5 ++ .../components/shared/model-preview-utils.ts | 1 + .../components/model-info-panel-utils.ts | 1 + .../kilocode/model-info-panel-utils.test.ts | 5 ++ 7 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 .changeset/ignore-negative-model-prices.md diff --git a/.changeset/ignore-negative-model-prices.md b/.changeset/ignore-negative-model-prices.md new file mode 100644 index 00000000000..629d5fcc02c --- /dev/null +++ b/.changeset/ignore-negative-model-prices.md @@ -0,0 +1,7 @@ +--- +"@kilocode/kilo-gateway": patch +"@kilocode/cli": patch +"kilo-code": patch +--- + +Ignore negative pricing entries from model catalogs and handle unpriced models gracefully in UI price formatting. diff --git a/packages/kilo-gateway/src/api/models.ts b/packages/kilo-gateway/src/api/models.ts index 8188c1f825b..3ccb4ff2534 100644 --- a/packages/kilo-gateway/src/api/models.ts +++ b/packages/kilo-gateway/src/api/models.ts @@ -76,7 +76,7 @@ type OpenRouterModel = z.infer function parseApiPrice(price: string | null | undefined): number | undefined { if (!price) return undefined const parsed = parseFloat(price) - if (isNaN(parsed)) return undefined + if (isNaN(parsed) || parsed < 0) return undefined return parsed * 1_000_000 // Convert $/token → $/M tokens } diff --git a/packages/kilo-gateway/test/api/models.test.ts b/packages/kilo-gateway/test/api/models.test.ts index 0833ec11f30..7d73b3d1b5e 100644 --- a/packages/kilo-gateway/test/api/models.test.ts +++ b/packages/kilo-gateway/test/api/models.test.ts @@ -383,3 +383,66 @@ test("keeps organization catalog errors from silently falling back to personal m expect(calls[0]).toContain("/api/gateway/transcription-models") expect(headers[0]?.get("X-KILOCODE-ORGANIZATIONID")).toBe("org-1") }) + +test("omits cost when pricing contains negative values (dynamic/auto-routed pricing)", async () => { + const orig = globalThis.fetch + stubFetch( + async () => + new Response( + JSON.stringify({ + data: [ + { + id: "openrouter/auto", + name: "Auto Router", + context_length: 128000, + max_completion_tokens: 16384, + architecture: { + input_modalities: ["text"], + output_modalities: ["text"], + }, + supported_parameters: ["tools"], + pricing: { + prompt: "-1", + completion: "-1", + }, + }, + { + id: "test/fixed-price", + name: "Fixed Price Model", + context_length: 128000, + max_completion_tokens: 16384, + architecture: { + input_modalities: ["text"], + output_modalities: ["text"], + }, + supported_parameters: ["tools"], + pricing: { + prompt: "0.000003", + completion: "0.000015", + input_cache_read: "0.0000003", + input_cache_write: "-1", + }, + }, + ], + }), + { + status: 200, + headers: { "content-type": "application/json" }, + }, + ), + ) + + const result = await fetchKiloModels({}) + + ;(globalThis as any).fetch = orig + + expect(result.error).toBeUndefined() + expect(result.models["openrouter/auto"]).toBeDefined() + expect(result.models["openrouter/auto"].cost).toBeUndefined() + expect(result.models["test/fixed-price"].cost).toEqual({ + input: 3, + output: 15, + cache_read: 0.3, + }) +}) + diff --git a/packages/kilo-vscode/tests/unit/model-price-format.test.ts b/packages/kilo-vscode/tests/unit/model-price-format.test.ts index d5aab2d6729..8a01b838094 100644 --- a/packages/kilo-vscode/tests/unit/model-price-format.test.ts +++ b/packages/kilo-vscode/tests/unit/model-price-format.test.ts @@ -18,6 +18,11 @@ describe("fmtPrice", () => { expect(fmtPrice(0)).toBe("Free") }) + it("returns dash for negative price", () => { + expect(fmtPrice(-1)).toBe("—") + expect(fmtPrice(-1000000)).toBe("—") + }) + it("uses 4 decimal places for sub-cent prices", () => { expect(fmtPrice(0.005)).toBe("$0.0050/1M") }) diff --git a/packages/kilo-vscode/webview-ui/src/components/shared/model-preview-utils.ts b/packages/kilo-vscode/webview-ui/src/components/shared/model-preview-utils.ts index 980ab78f368..10cd1f44d76 100644 --- a/packages/kilo-vscode/webview-ui/src/components/shared/model-preview-utils.ts +++ b/packages/kilo-vscode/webview-ui/src/components/shared/model-preview-utils.ts @@ -3,6 +3,7 @@ * Expects `n` in $/M tokens (as stored in model.cost.input / model.cost.output). */ export function fmtPrice(n: number): string { + if (n < 0) return "—" if (n === 0) return "Free" if (n < 0.01) return `$${n.toFixed(4)}/1M` return `$${n.toFixed(2)}/1M` diff --git a/packages/opencode/src/kilocode/components/model-info-panel-utils.ts b/packages/opencode/src/kilocode/components/model-info-panel-utils.ts index 1c7bb828b56..5cfc69d7b70 100644 --- a/packages/opencode/src/kilocode/components/model-info-panel-utils.ts +++ b/packages/opencode/src/kilocode/components/model-info-panel-utils.ts @@ -8,6 +8,7 @@ interface Cost { } export function fmtPrice(n: number): string { + if (n < 0) return "—" if (n === 0) return "Free" if (n < 0.01) return `$${n.toFixed(4)}/1M` return `$${n.toFixed(2)}/1M` diff --git a/packages/opencode/test/kilocode/model-info-panel-utils.test.ts b/packages/opencode/test/kilocode/model-info-panel-utils.test.ts index 8a07fcb2291..f24f2b3ba85 100644 --- a/packages/opencode/test/kilocode/model-info-panel-utils.test.ts +++ b/packages/opencode/test/kilocode/model-info-panel-utils.test.ts @@ -6,6 +6,11 @@ describe("model info panel price formatting", () => { expect(fmtPrice(0)).toBe("Free") }) + test("fmtPrice returns dash for negative values", () => { + expect(fmtPrice(-1)).toBe("—") + expect(fmtPrice(-1000000)).toBe("—") + }) + test("fmtPrice uses four decimals for very small prices", () => { expect(fmtPrice(0.0095)).toBe("$0.0095/1M") }) From 08f8c5fc5937063f1e01e73526ce224bb09a71fb Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Mon, 10 Aug 2026 09:28:01 +0000 Subject: [PATCH 52/73] fix(ui): hide pricing section for unpriced models Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .changeset/ignore-negative-model-prices.md | 1 + .../session/ui/model/ModelDetailsPanel.kt | 6 +-- .../src/components/shared/ModelPreview.tsx | 9 +++-- .../kilocode/components/model-info-panel.tsx | 37 ++++++++++--------- 4 files changed, 29 insertions(+), 24 deletions(-) diff --git a/.changeset/ignore-negative-model-prices.md b/.changeset/ignore-negative-model-prices.md index 629d5fcc02c..66bcf01a6dd 100644 --- a/.changeset/ignore-negative-model-prices.md +++ b/.changeset/ignore-negative-model-prices.md @@ -2,6 +2,7 @@ "@kilocode/kilo-gateway": patch "@kilocode/cli": patch "kilo-code": patch +"@kilocode/kilo-jetbrains": patch --- Ignore negative pricing entries from model catalogs and handle unpriced models gracefully in UI price formatting. diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelDetailsPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelDetailsPanel.kt index eb83cf748e6..b6541f267a6 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelDetailsPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelDetailsPanel.kt @@ -157,14 +157,14 @@ internal class ModelDetailsPanel( return buildList { item.releaseDate?.let { add(KiloBundle.message("model.picker.details.released") to date(it)) } if (!item.free) { - item.cost?.let { cost -> + item.cost?.takeIf { it.input > 0.0 || it.output > 0.0 }?.let { cost -> add(KiloBundle.message("model.picker.details.input") to price(cost.input)) add(KiloBundle.message("model.picker.details.output") to price(cost.output)) add(KiloBundle.message("model.picker.details.cached") to cached(cost.input, cost.cache?.read)) add(KiloBundle.message("model.picker.details.average") to price(average(cost.input, cost.output, cost.cache?.read))) } ?: run { - item.inputPrice?.let { add(KiloBundle.message("model.picker.details.input") to price(it)) } - item.outputPrice?.let { add(KiloBundle.message("model.picker.details.output") to price(it)) } + item.inputPrice?.takeIf { it > 0.0 }?.let { add(KiloBundle.message("model.picker.details.input") to price(it)) } + item.outputPrice?.takeIf { it > 0.0 }?.let { add(KiloBundle.message("model.picker.details.output") to price(it)) } } } ctx?.let { add(KiloBundle.message("model.picker.details.context") to context(it)) } diff --git a/packages/kilo-vscode/webview-ui/src/components/shared/ModelPreview.tsx b/packages/kilo-vscode/webview-ui/src/components/shared/ModelPreview.tsx index a88bc752668..163cd5adf30 100644 --- a/packages/kilo-vscode/webview-ui/src/components/shared/ModelPreview.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/shared/ModelPreview.tsx @@ -50,12 +50,13 @@ export const ModelPreview: Component = (props) => { {(model) => { const cost = () => model().cost + const hasPricing = () => cost() && (cost()!.input > 0 || cost()!.output > 0) const bench = () => model().terminalBench const cachedText = () => { - if (!cost()) return "" + if (!cost() || !hasPricing()) return "" return fmtCachedPrice(cost()!) ?? language.t("model.preview.value.notSupported") } - const avg = () => (cost() ? avgPrice(cost()!) : undefined) + const avg = () => (cost() && hasPricing() ? avgPrice(cost()!) : undefined) const freeLabel = () => language.t("model.tag.free") const dataLabel = () => freeDataLabel(language.t("model.tag.free"), language.t("model.tag.dataCollected")) const autoLabel = () => autoSummary(model()) @@ -139,8 +140,8 @@ export const ModelPreview: Component = (props) => { {fmtDate(model().releaseDate!)} - {/* Pricing — hidden for free models */} - + {/* Pricing — hidden for free models or models without fixed pricing */} + {language.t("model.preview.label.input")} {fmtPrice(cost()!.input)} {language.t("model.preview.label.output")} diff --git a/packages/opencode/src/kilocode/components/model-info-panel.tsx b/packages/opencode/src/kilocode/components/model-info-panel.tsx index e7fa1893499..8ed65dda997 100644 --- a/packages/opencode/src/kilocode/components/model-info-panel.tsx +++ b/packages/opencode/src/kilocode/components/model-info-panel.tsx @@ -27,8 +27,9 @@ export function ModelInfoPanel(props: Props) { const maxHeight = createMemo(() => Math.floor(dimensions().height / 2) - 3) const cost = () => m().cost - const cached = () => (cost() ? fmtCachedPrice(cost()) : null) - const avg = () => (cost() ? avgPrice(cost()) : undefined) + const hasPricing = () => m().cost?.input > 0 || m().cost?.output > 0 + const cached = () => (cost() && hasPricing() ? fmtCachedPrice(cost()) : null) + const avg = () => (cost() && hasPricing() ? avgPrice(cost()) : undefined) const caps = () => m().capabilities const inputs = () => caps()?.input const outputs = () => caps()?.output @@ -99,25 +100,27 @@ export function ModelInfoPanel(props: Props) { - - Input - {m() ? fmtPrice(m().cost.input) : "—"} - - - Output - {m() ? fmtPrice(m().cost.output) : "—"} - - + - Cached - {cached()} + Input + {m() ? fmtPrice(m().cost.input) : "—"} - - - Avg Cost - {m() ? fmtPrice(avg()!) : "—"} + Output + {m() ? fmtPrice(m().cost.output) : "—"} + + + Cached + {cached()} + + + + + Avg Cost + {m() ? fmtPrice(avg()!) : "—"} + + Context From 73844ae68d27d095681c2369a8093156b106a453 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Mon, 10 Aug 2026 12:15:30 +0100 Subject: [PATCH 53/73] feat(cli): answer sessionless model catalog requests (#13014) * feat(cli): answer sessionless model catalog requests * feat(cli): add sessionless catalog changeset * chore(cli): format remote sender files * test(cli): simplify sessionless catalog coverage * fix(test): extend config overlay timeout The legacy overlay test can exceed 30 seconds on a cold Windows runner while its assertions remain valid. --- .changeset/remote-instance-catalog.md | 5 ++ .../src/kilo-sessions/remote-sender.ts | 27 +++---- .../kilocode/server/config-overlay.test.ts | 4 +- .../kilocode/sessions/remote-sender.test.ts | 79 ++++++++++++++++--- 4 files changed, 90 insertions(+), 25 deletions(-) create mode 100644 .changeset/remote-instance-catalog.md diff --git a/.changeset/remote-instance-catalog.md b/.changeset/remote-instance-catalog.md new file mode 100644 index 00000000000..bed43e8cd31 --- /dev/null +++ b/.changeset/remote-instance-catalog.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Support browsing an instance's model catalog before a session starts. diff --git a/packages/opencode/src/kilo-sessions/remote-sender.ts b/packages/opencode/src/kilo-sessions/remote-sender.ts index 288be081c43..d49babd8699 100644 --- a/packages/opencode/src/kilo-sessions/remote-sender.ts +++ b/packages/opencode/src/kilo-sessions/remote-sender.ts @@ -345,9 +345,7 @@ export namespace RemoteSender { // bus listener count from inflating for senders that never handle // attachments (the count would otherwise show up in unrelated tests // that assert it stays at 0). - const attachments = - options.attachments ?? - ((sessionID: SessionID) => RemoteAttachments.create({ sessionID })) + const attachments = options.attachments ?? ((sessionID: SessionID) => RemoteAttachments.create({ sessionID })) const attachmentCache = new Map() const pending = new Map() const retired = new Map() @@ -886,27 +884,27 @@ export namespace RemoteSender { return } // kilocode_change end + // kilocode_change start - sessionless list_models for the pre-session instance picker if (msg.command === "list_models") { const parsed = RemoteModelCatalog.Request.safeParse(msg.data) - const session = msg.sessionId ? decodeSessionID(msg.sessionId) : Option.none() - if (!parsed.success || Option.isNone(session)) { - options.conn.send({ - type: "response", - id: msg.id, - error: "invalid list_models command", - }) + // Accept an absent sessionId (the mobile instance-picker path asks for the + // instance's catalog before a session exists). A present but undecodable + // sessionId is still invalid. + const target = msg.sessionId ? decodeSessionID(msg.sessionId) : Option.none() + if (!parsed.success || (msg.sessionId && Option.isNone(target))) { + options.conn.send({ type: "response", id: msg.id, error: "invalid list_models command" }) return } const run = options.provide ?? provide void (async () => { try { - const info = await catalog.get(session.value) + const info = Option.isSome(target) ? await catalog.get(target.value) : undefined const result = await run({ - directory: info.directory, + directory: info?.directory ?? options.directory, fn: async () => { const [providers, messages, fallback] = await Promise.all([ catalog.providers(), - catalog.messages(info.id), + info ? catalog.messages(info.id) : Promise.resolve([]), catalog.default().catch((err) => { options.log.warn("default model lookup failed", { error: String(err) }) return undefined @@ -914,7 +912,7 @@ export namespace RemoteSender { ]) return RemoteModelCatalog.build({ providers, - session: info, + session: info ?? {}, messages, defaultModel: fallback, }) @@ -928,6 +926,7 @@ export namespace RemoteSender { })() return } + // kilocode_change end if (msg.command === "send_message") { const parsed = getRemotePromptInput().safeParse(msg.data) if (!parsed.success) { diff --git a/packages/opencode/test/kilocode/server/config-overlay.test.ts b/packages/opencode/test/kilocode/server/config-overlay.test.ts index f87246fb360..50c8e1667a5 100644 --- a/packages/opencode/test/kilocode/server/config-overlay.test.ts +++ b/packages/opencode/test/kilocode/server/config-overlay.test.ts @@ -911,7 +911,9 @@ describe("config overlay routes", () => { Permission.evaluate("edit", "*", after.find((item) => item.name === "code")?.permission ?? []).action, ).toBe("allow") }, - 30_000, + // Cold Windows CI runs take ~32s (observed timeout at 30s); give the two + // instance create/dispose cycles of each iteration real headroom. + 90_000, ) } }) diff --git a/packages/opencode/test/kilocode/sessions/remote-sender.test.ts b/packages/opencode/test/kilocode/sessions/remote-sender.test.ts index e8683fe49e8..44504702f37 100644 --- a/packages/opencode/test/kilocode/sessions/remote-sender.test.ts +++ b/packages/opencode/test/kilocode/sessions/remote-sender.test.ts @@ -1084,7 +1084,7 @@ describe("RemoteSender", () => { expect(JSON.stringify(sent)).not.toContain("api-key") }) - test("list_models rejects unsupported versions and missing session IDs", () => { + test("list_models rejects unsupported versions and undecodable session IDs", () => { const { conn, sent } = fakeConn() const sender = RemoteSender.create({ conn, @@ -1100,12 +1100,6 @@ describe("RemoteSender", () => { sessionId: "ses_models", data: { protocolVersion: 2 }, }) - sender.handle({ - type: "command", - id: "req_models_missing_session", - command: "list_models", - data: { protocolVersion: 1 }, - }) sender.handle({ type: "command", id: "req_models_invalid_session", @@ -1116,11 +1110,72 @@ describe("RemoteSender", () => { expect(sent).toEqual([ { type: "response", id: "req_models_v2", error: "invalid list_models command" }, - { type: "response", id: "req_models_missing_session", error: "invalid list_models command" }, { type: "response", id: "req_models_invalid_session", error: "invalid list_models command" }, ]) }) + test("list_models without a sessionId returns the instance catalog", async () => { + const { conn, sent } = fakeConn() + const dirs: string[] = [] + const sender = RemoteSender.create({ + conn, + directory: "/tmp/process-default", + log: nolog, + subscribe: fakeBus().subscribe, + provide: async (input: { directory: string; init?: Effect.Effect; fn: () => R }) => { + dirs.push(input.directory) + return input.fn() + }, + catalog: { + get: async () => { + throw new Error("catalog.get must not be called without a sessionId") + }, + messages: async () => { + throw new Error("catalog.messages must not be called without a sessionId") + }, + providers: async () => + ({ + custom: { + id: ProviderV2.ID.make("custom"), + name: "Custom Provider", + source: "config", + env: ["PRIVATE_API_KEY"], + key: "must-not-leak", + options: { apiKey: "must-not-leak" }, + models: { + "deployment/model": catalogModel("custom", "deployment/model", "Deployment Model", true), + }, + }, + }) as any, + default: async () => ({ + providerID: ProviderV2.ID.make("custom"), + modelID: ModelV2.ID.make("deployment/model"), + }), + }, + }) + + sender.handle({ + type: "command", + id: "req_models_sessionless", + command: "list_models", + data: { protocolVersion: 1 }, + }) + + await new Promise((resolve) => setTimeout(resolve, 0)) + + expect(dirs).toEqual(["/tmp/process-default"]) + expect(sent).toHaveLength(1) + expect(sent[0]?.type).toBe("response") + expect(sent[0]?.id).toBe("req_models_sessionless") + const result = sent[0]?.result as RemoteModelCatalog.Response + expect(result.protocolVersion).toBe(1) + expect(result.all).toHaveLength(1) + expect(result.all[0]?.id).toBe("custom") + expect(result.defaultModel).toEqual({ providerID: "custom", modelID: "deployment/model" }) + expect(result).not.toHaveProperty("currentModel") + expect(JSON.stringify(result)).not.toContain("must-not-leak") + }) + test("send_message with agent is accepted", async () => { const { conn, sent } = fakeConn() let resolveProvide: () => void @@ -3066,7 +3121,9 @@ describe("RemoteSender slash commands", () => { removeCalls.push(id) }, }, - attachSession: async () => { throw new Error("attach failed") }, + attachSession: async () => { + throw new Error("attach failed") + }, }) const response = expectResponse(conn, sent, "req_spawn_failed") @@ -3107,7 +3164,9 @@ describe("RemoteSender slash commands", () => { throw new Error("cleanup secondary failure") }, }, - attachSession: async () => { throw new Error("attach failed") }, + attachSession: async () => { + throw new Error("attach failed") + }, }) const response = expectResponse(conn, sent, "req_spawn_then_cleanup_fail") From fdc46654c5860c01276d7034ba715f779b163bca Mon Sep 17 00:00:00 2001 From: Johnny Eric Amancio Date: Thu, 6 Aug 2026 18:08:52 +0200 Subject: [PATCH 54/73] fix(memory): quiet transient capture timeouts and stop same-turn retries --- .../fix-memory-model-timeout-warnings.md | 7 + .../kilo-memory/src/capture/operations.ts | 9 +- packages/kilo-memory/src/capture/outcome.ts | 73 +++++++++- packages/kilo-memory/src/effect/capture.ts | 122 +++-------------- packages/kilo-memory/src/effect/service.ts | 23 +--- packages/kilo-memory/src/memory-notice.ts | 24 +--- packages/kilo-memory/src/memory.ts | 59 -------- packages/kilo-memory/src/recall/indexer.ts | 1 - packages/kilo-memory/src/recall/shared.ts | 1 + packages/kilo-memory/src/schema.ts | 6 + packages/kilo-memory/src/storage/audit.ts | 44 +----- packages/kilo-memory/src/storage/state.ts | 11 +- packages/kilo-memory/src/tool.ts | 129 ++---------------- packages/kilo-memory/test/capture.test.ts | 84 ++++++++++-- packages/kilo-memory/test/core.test.ts | 16 +++ .../kilo-memory/test/effect-capture.test.ts | 127 ++++++++++++++++- packages/kilo-vscode/src/KiloProvider.ts | 7 +- .../unit/kilo-provider-memory-events.test.ts | 37 +++++ .../src/kilocode/cli/cmd/tui/memory-events.ts | 3 + .../opencode/src/kilocode/memory/ports.ts | 3 +- .../cli/cmd/tui/memory-command.test.ts | 13 +- .../test/kilocode/memory/memory-ports.test.ts | 48 ++++++- 22 files changed, 446 insertions(+), 401 deletions(-) create mode 100644 .changeset/fix-memory-model-timeout-warnings.md diff --git a/.changeset/fix-memory-model-timeout-warnings.md b/.changeset/fix-memory-model-timeout-warnings.md new file mode 100644 index 00000000000..5a0c8700162 --- /dev/null +++ b/.changeset/fix-memory-model-timeout-warnings.md @@ -0,0 +1,7 @@ +--- +"@kilocode/cli": patch +"@kilocode/kilo-memory": patch +"kilo-code": patch +--- + +Reduce noisy memory timeout warnings and retry transient background consolidation failures once. diff --git a/packages/kilo-memory/src/capture/operations.ts b/packages/kilo-memory/src/capture/operations.ts index f4703fdd160..53a1dcf345f 100644 --- a/packages/kilo-memory/src/capture/operations.ts +++ b/packages/kilo-memory/src/capture/operations.ts @@ -126,7 +126,7 @@ export namespace MemoryOperations { const adds = input.ops .filter((item): item is Add => item.action === "add") // Redact rejected text too: this filter runs before the secret one, so a rejected op never - // reaches it, and skips flow into the persistent decisions audit (/memory/show, TUI). + // reaches it, and skips remain visible in the operation result. .filter((op) => { const item = reject(op) if (!item) return true @@ -314,7 +314,7 @@ export namespace MemoryOperations { } } - async function persist(input: { root: string; state: MemorySchema.State; count: number; removed: number }) { + async function persist(input: { root: string; state: MemorySchema.State; count: number }) { const index = await MemoryIndexer.rebuild({ root: input.root, state: input.state }) await MemoryFiles.writeState(input.root, { ...input.state, @@ -323,7 +323,6 @@ export namespace MemoryOperations { lastOperationCount: input.count, }, }) - await MemoryFiles.append(input.root, `apply ops=${input.count} removed=${input.removed}`) return index } @@ -365,9 +364,9 @@ export namespace MemoryOperations { const prepared = prepare({ state, ops: input.ops, max: state.limits.maxLineChars }) const removes = input.ops.filter((item): item is Remove => item.action === "remove") const plan = planOps({ docs, inventory, removes, adds: prepared.adds, now: Date.now() }) - // Commit (IO): write changed documents, then rebuild the index, persist state, and audit. + // Commit (IO): write changed documents, then rebuild the index and persist state. await writeDocs({ root: input.root, plan }) - const index = await persist({ root: input.root, state, count: plan.count, removed: plan.removed }) + const index = await persist({ root: input.root, state, count: plan.count }) return { operationCount: plan.count, added: plan.added, diff --git a/packages/kilo-memory/src/capture/outcome.ts b/packages/kilo-memory/src/capture/outcome.ts index 9ee4be803d7..ce65e172862 100644 --- a/packages/kilo-memory/src/capture/outcome.ts +++ b/packages/kilo-memory/src/capture/outcome.ts @@ -1,9 +1,12 @@ import { MemoryOperations } from "./operations" import { MemoryRedact } from "./redact" import { MemoryShared } from "../recall/shared" +import { TRANSIENT } from "../schema" import type { MemoryFiles } from "../storage/store" import type { CaptureSkip } from "./parse" +export { TRANSIENT } + export type CaptureSourceItem = { id: string text: string @@ -69,14 +72,75 @@ export function errorReason(err: unknown) { return MemoryShared.brief(MemoryRedact.text(parts.join(" ")), 500) } -export function guardReason(input: string) { - const value = input.toLowerCase() - if (/\b(429|rate[_ -]?limit|too many requests)\b/.test(value)) return "rate_limit_guard" - if (/\b(insufficient[_ -]?quota|quota exceeded|exceeded your quota|billing|credits?|credit balance)\b/.test(value)) +const RATE_TEXT = /\b(429|rate[_ -]?limit|too many requests)\b/ +const QUOTA_TEXT = /\b(insufficient[_ -]?quota|quota exceeded|exceeded your quota|billing|credits?|credit balance)\b/ +const TIMEOUT_TEXT = + /\b(timeouterror|etimedout|deadline[ _-]?exceeded|timed out|(connect|headers|body|gateway)[ _-]?time[ -]?out)\b|^timeout$/ +const TIMEOUT_CODES = new Set(["ETIMEDOUT", "UND_ERR_CONNECT_TIMEOUT", "UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT"]) + +/** Structural timeout detection over an error's name/status/code, its message text, and its nested + * cause/errors chains (cycle-safe). Message matching is limited to `message` — never the serialized + * body/data blobs — so response payloads that merely mention "timeout" cannot classify as transient. */ +function timedOut(item: unknown, seen = new WeakSet()): boolean { + if (!item || typeof item !== "object" || seen.has(item)) return false + seen.add(item) + const err = item as { + name?: unknown + code?: unknown + status?: unknown + statusCode?: unknown + message?: unknown + cause?: unknown + errors?: unknown + } + if (err.name === "TimeoutError") return true + if (err.status === 504 || err.statusCode === 504) return true + if (typeof err.code === "string" && TIMEOUT_CODES.has(err.code)) return true + if (typeof err.message === "string" && TIMEOUT_TEXT.test(err.message.toLowerCase())) return true + if (timedOut(err.cause, seen)) return true + return Array.isArray(err.errors) && err.errors.some((entry) => timedOut(entry, seen)) +} + +function rateGuarded(item: unknown, seen = new WeakSet()): boolean { + if (!item || typeof item !== "object" || seen.has(item)) return false + seen.add(item) + const err = item as { + status?: unknown + statusCode?: unknown + message?: unknown + cause?: unknown + errors?: unknown + } + if (err.status === 429 || err.statusCode === 429) return true + if (typeof err.message === "string" && RATE_TEXT.test(err.message.toLowerCase())) return true + if (rateGuarded(err.cause, seen)) return true + return Array.isArray(err.errors) && err.errors.some((entry) => rateGuarded(entry, seen)) +} + +function quotaGuarded(item: unknown, seen = new WeakSet()): boolean { + if (!item || typeof item !== "object" || seen.has(item)) return false + seen.add(item) + const err = item as { + message?: unknown + cause?: unknown + errors?: unknown + } + if (typeof err.message === "string" && QUOTA_TEXT.test(err.message.toLowerCase())) return true + if (quotaGuarded(err.cause, seen)) return true + return Array.isArray(err.errors) && err.errors.some((entry) => quotaGuarded(entry, seen)) +} + +export function guardReason(input: unknown) { + const value = (typeof input === "string" ? input : errorReason(input)).toLowerCase() + if (typeof input === "string" ? RATE_TEXT.test(value) : rateGuarded(input) || RATE_TEXT.test(value)) + return "rate_limit_guard" + if (typeof input === "string" ? QUOTA_TEXT.test(value) : quotaGuarded(input) || QUOTA_TEXT.test(value)) return "quota_guard" + if (typeof input === "string" ? TIMEOUT_TEXT.test(value) : timedOut(input)) return TRANSIENT return undefined } +/** @deprecated Memory audit persistence was removed. */ export function skipped(input: { sessionID: string; reason: string }): MemoryFiles.Decision { return { kind: "typed", @@ -94,6 +158,7 @@ export function skipped(input: { sessionID: string; reason: string }): MemoryFil } } +/** @deprecated Memory audit persistence was removed. */ export function auditOps(ops: MemoryOperations.Op[]) { return MemoryShared.audit(ops) } diff --git a/packages/kilo-memory/src/effect/capture.ts b/packages/kilo-memory/src/effect/capture.ts index fda14a1a19a..cd0e1308d20 100644 --- a/packages/kilo-memory/src/effect/capture.ts +++ b/packages/kilo-memory/src/effect/capture.ts @@ -1,6 +1,5 @@ import { Cause, Effect } from "effect" import { - auditOps, cap, capturePlan, digestPrompt, @@ -18,9 +17,9 @@ import { parseJson, parseOps, salvageTyped, - skipped, summarize, summarizeDiffs, + TRANSIENT, typedPrompt, usage, verifySkips, @@ -105,11 +104,16 @@ export namespace MemoryCapture { yield* memory.prepare({ root }) const state = yield* memory.state({ root }) const reported = new Set() - const fail = (reason: string) => + const fail = (reason: string, detail?: string) => Effect.promise(async () => { const safe = MemoryRedact.text(reason) if (reported.has(safe)) return reported.add(safe) + if (reason === TRANSIENT) + MemoryLog.warn("memory capture transient failure", { + reason: safe, + detail: MemoryShared.brief(MemoryRedact.text(detail ?? safe), 160), + }) await MemoryEvents.publish({ event: "error", payload: MemoryEvents.status({ @@ -123,7 +127,6 @@ export namespace MemoryCapture { }) const skip = (reason: string, opts?: { idleFlush?: boolean }) => Effect.gen(function* () { - if (state.enabled) yield* memory.decide({ root, decision: skipped({ sessionID: input.sessionID, reason }) }) yield* Effect.promise(() => MemoryEvents.publish({ event: "status", @@ -203,23 +206,6 @@ export namespace MemoryCapture { tokens: 0, fallback: true, }) - yield* memory.decide({ - root, - decision: { - kind: "digest", - trigger: "turn-close", - sessionID: input.sessionID, - result: "fallback", - llm: false, - parsed: false, - fallback: true, - reason: input.reason, - tokens: 0, - operationCount: 1, - skippedCount: 0, - summary: `session digest fallback on ${input.reason ?? "close"}`, - }, - }) } return yield* skip(plan.skipReason, plan.idleFlush ? { idleFlush: true } : undefined) } @@ -232,22 +218,12 @@ export namespace MemoryCapture { const model = digestDue || typedCall - ? yield* Effect.gen(function* () { - const resolution = yield* input.model.resolve({ + ? ( + yield* input.model.resolve({ configured: input.memoryModel, session: view.sessionModel, }) - if (resolution.fallback) { - yield* memory.append({ - root, - text: `memory_model_config reason=${MemoryShared.brief( - MemoryRedact.text(resolution.fallback.reason), - 160, - )} fallback=1`, - }) - } - return resolution.handle - }) + ).handle : undefined const digestEffect = digestDue ? Effect.gen(function* () { @@ -277,9 +253,8 @@ export namespace MemoryCapture { Effect.gen(function* () { if (signal.aborted) return { ok: false as const, reason: "cancelled" } const raw = errorReason(err) - const reason = MemoryRedact.text(guardReason(raw) ?? raw) - yield* fail(reason) - yield* memory.append({ root, text: `digest error=${MemoryShared.brief(reason, 160)} fallback=1` }) + const reason = MemoryRedact.text(guardReason(err) ?? raw) + yield* fail(reason, raw) return { ok: false as const, reason } }), ), @@ -296,14 +271,9 @@ export namespace MemoryCapture { try: () => parseJson(digestSchema, result.result.text), catch: (error) => error, }).pipe( - Effect.catch((err: unknown) => + Effect.catch(() => Effect.gen(function* () { - const reason = MemoryRedact.text(errorReason(err)) yield* fail("digest parse_error") - yield* memory.append({ - root, - text: `digest parse_error=${MemoryShared.brief(reason, 160)} fallback=1`, - }) return undefined }), ), @@ -402,9 +372,8 @@ export namespace MemoryCapture { Effect.gen(function* () { if (signal.aborted) return { ok: false as const, reason: "cancelled" } const raw = errorReason(err) - const reason = MemoryRedact.text(guardReason(raw) ?? raw) - yield* fail(reason) - yield* memory.append({ root, text: `consolidate error=${MemoryShared.brief(reason, 160)}` }) + const reason = MemoryRedact.text(guardReason(err) ?? raw) + yield* fail(reason, raw) return { ok: false as const, reason } }), ), @@ -423,11 +392,9 @@ export namespace MemoryCapture { try: () => salvageTyped(result.result.text), catch: (error) => error, }).pipe( - Effect.catch((err: unknown) => + Effect.catch(() => Effect.gen(function* () { - const reason = MemoryRedact.text(errorReason(err)) yield* fail("consolidate parse_error") - yield* memory.append({ root, text: `consolidate parse_error=${MemoryShared.brief(reason, 160)}` }) return undefined }), ), @@ -475,30 +442,6 @@ export namespace MemoryCapture { fallback: Boolean(digest.reason), }) } - if (digestDue) { - yield* memory.decide({ - root, - decision: { - kind: "digest", - trigger: "turn-close", - sessionID: input.sessionID, - result: digest.reason ? "fallback" : digest.summary ? "saved" : "skipped", - llm: true, - parsed: Boolean(digest.summary && !digest.reason), - fallback: Boolean(digest.reason), - reason: digest.reason, - tokens: digest.tokens, - operationCount: digest.summary ? 1 : 0, - skippedCount: digest.summary ? 0 : 1, - summary: digest.reason - ? `session digest used fallback after ${digest.reason}` - : digest.summary - ? "session digest saved" - : "session digest skipped", - }, - }) - } - // Apply adds only: a same-key add supersedes/updates an existing fact in place. reconcile also // surfaces exact-key auto-removes, but V0 keeps hard removes explicit-only — auto-capture never // deletes memory it merely paraphrased (or wrongly flags), so reconciled.removes is not applied. @@ -506,39 +449,11 @@ export namespace MemoryCapture { const ops = reconciled.ops.slice(0, state.capture.maxOpsPerRun) const project = ops.length > 0 ? yield* memory.apply({ root, ops, trigger: "turn-close", tokens: generated.tokens }) : undefined - // Apply-time skips (content gate + secret, both redacted at creation) surface in the typed audit - // record alongside the model's own declared skips. const applied: CaptureSkip[] = [...generated.skipped, ...(project?.skipped ?? [])] const count = project?.operationCount ?? 0 - if (typedCall) { - yield* memory.decide({ - root, - decision: { - kind: "typed", - trigger: "turn-close", - sessionID: input.sessionID, - result: generated.fallback ? "fallback" : count > 0 ? "saved" : "skipped", - llm: true, - parsed: !generated.fallback, - fallback: generated.fallback, - reason: generated.reason, - tokens: generated.tokens, - operationCount: count, - skippedCount: applied.length, - skipped: applied, - operations: auditOps(ops), - files: [...new Set(ops.flatMap((item) => (item.action === "add" && item.file ? [item.file] : [])))], - summary: generated.fallback - ? `typed consolidation skipped after ${generated.reason ?? "model failure"}` - : count > 0 - ? `typed consolidation saved ${count} ops` - : `typed consolidation skipped ${applied.length} candidates`, - }, - }) - } const tokens = digest.tokens + generated.tokens if (!digest.summary && !typedCall && count === 0) return yield* skip("no_ops") - if ((digestDue || typedCall || count > 0) && (!typedCall || !generated.fallback)) { + if (digestDue || typedCall || count > 0) { yield* memory.commit({ root, now, @@ -546,8 +461,7 @@ export namespace MemoryCapture { tokens, count, digest: Boolean(digest.summary), - typed: typedCall, - skipped: applied, + typed: typedCall && !generated.fallback, }) } const updated = yield* memory.state({ root }) diff --git a/packages/kilo-memory/src/effect/service.ts b/packages/kilo-memory/src/effect/service.ts index 9f705499360..026553a61a3 100644 --- a/packages/kilo-memory/src/effect/service.ts +++ b/packages/kilo-memory/src/effect/service.ts @@ -1,5 +1,4 @@ import { Context, Effect, Layer, Semaphore } from "effect" -import { skipLine, type CaptureSkip } from "../capture/capture" import type { Memory } from "../memory" import type { MemoryOperations } from "../capture/operations" import { MemoryRecall } from "../recall/recall" @@ -104,7 +103,6 @@ type CommitInput = RootInput & { // shared typed-interval clock (lastTypedConsolidationAt); a digest-only commit must leave it untouched so a // digest in one session cannot throttle another session's typed capture. typed: boolean - skipped: CaptureSkip[] cost?: number } @@ -156,10 +154,12 @@ export namespace MemoryService { readonly recent: ( input: RecentInput, ) => Effect.Effect>, Failure> + /** @deprecated Memory audit persistence was removed. */ readonly append: (input: AppendInput) => Effect.Effect readonly index: (input: RootInput) => Effect.Effect readonly commit: (input: CommitInput) => Effect.Effect readonly recordRecall: (input: RecordRecallInput) => Effect.Effect + /** @deprecated Memory audit persistence was removed. */ readonly decide: (input: DecideInput) => Effect.Effect readonly readSource: (input: ReadSourceInput) => Effect.Effect readonly turnLock: (sessionID: SessionID) => Semaphore.Semaphore @@ -200,7 +200,7 @@ export namespace MemoryService { return Object.fromEntries(entries) as Sources }), recent: (input) => bridge(() => MemoryFiles.recentSessions(input.root, input.limit, input.max)), - append: (input) => bridge(() => MemoryFiles.append(input.root, input.text)), + append: () => Effect.void, index: (input) => bridge(async () => { const text = await MemoryFiles.readIndex(input.root) @@ -219,20 +219,11 @@ export namespace MemoryService { lastSessionSavedAt: input.digest ? input.now : state.stats.lastSessionSavedAt, lastConsolidatedMessageID: input.messageID, lastConsolidationCost: input.cost ?? state.stats.lastConsolidationCost, - lastConsolidationTokens: input.tokens, - lastOperationCount: input.count, + lastConsolidationTokens: + input.typed || input.digest ? input.tokens : state.stats.lastConsolidationTokens, + lastOperationCount: input.typed ? input.count : state.stats.lastOperationCount, }, }) - const skip = skipLine(input.skipped) - await MemoryFiles.append( - input.root, - [ - `consolidate trigger=turn-close digest=${input.digest ? 1 : 0} ops=${input.count} tokens=${input.tokens}`, - skip, - ] - .filter(Boolean) - .join(" "), - ) }), ), recordRecall: (input) => @@ -266,7 +257,7 @@ export namespace MemoryService { }), }) }), - decide: (input) => bridge(() => MemoryFiles.decide(input.root, input.decision)), + decide: () => Effect.void, readSource: (input) => bridge(() => MemoryFiles.readSource(input.root, input.file)), // Ref-counted so every acquirer — in-flight or queued behind `withPermits` — shares one // semaphore. Each call must be balanced by exactly one `dropLock`. diff --git a/packages/kilo-memory/src/memory-notice.ts b/packages/kilo-memory/src/memory-notice.ts index f96bca8eec7..983b4613879 100644 --- a/packages/kilo-memory/src/memory-notice.ts +++ b/packages/kilo-memory/src/memory-notice.ts @@ -1,23 +1,12 @@ import { MemoryShared } from "./recall/shared" import type { MemoryOperations } from "./capture/operations" -import { MemoryRedact } from "./capture/redact" -/** Human-facing messages and audit views describing an explicit apply result. */ +/** Human-facing messages describing an explicit apply result. */ export namespace MemoryNotice { export function saved(input: { added: number; removed: number }) { return input.removed > 0 || input.added > 0 } - export function summary(input: { added: number; removed: number; count: number }) { - if (input.added > 0 && input.removed > 0) { - return `explicit memory operation saved ${input.added} and removed ${input.removed}` - } - if (input.added > 0) return `explicit memory operation saved ${input.added} ops` - if (input.removed > 0) return `explicit memory operation removed ${input.removed} entries` - if (input.count > 0) return "explicit memory operation matched no source memory" - return "explicit memory operation had no accepted ops" - } - export function message(input: { ops: MemoryOperations.Op[]; added: number; removed: number; count: number }) { const refs = MemoryShared.refs(input.ops) if (input.added > 0 && input.removed > 0) return `Memory updated · ${input.added} saved, ${input.removed} removed` @@ -25,15 +14,4 @@ export namespace MemoryNotice { if (input.removed > 0) return `Memory updated · ${input.removed} removed` return `Memory unchanged · ${input.count} ops` } - - export function skip(input: MemoryOperations.Rejection[]) { - return input.map((item) => (item.reason === "out_of_scope" ? { reason: item.reason } : item)) - } - - export function ops(input: { ops: MemoryOperations.Op[]; skipped: MemoryOperations.Rejection[] }) { - const blocked = new Set(input.skipped.filter((item) => item.reason === "out_of_scope").map((item) => item.text)) - return MemoryShared.audit( - input.ops.filter((item) => item.action !== "add" || !blocked.has(MemoryRedact.text(item.text))), - ) - } } diff --git a/packages/kilo-memory/src/memory.ts b/packages/kilo-memory/src/memory.ts index 8bd14dbf421..267b92093af 100644 --- a/packages/kilo-memory/src/memory.ts +++ b/packages/kilo-memory/src/memory.ts @@ -8,7 +8,6 @@ import { MemorySchema } from "./schema" import { MemoryShared } from "./recall/shared" import { MemoryToken } from "./recall/token" import { MemorySlug } from "./slug" -import { MemoryRedact } from "./capture/redact" /** Root-bound package facade. External Kilo surfaces should derive root from workspace context first. */ export namespace Memory { @@ -97,7 +96,6 @@ export namespace Memory { const state = await MemoryFiles.readState(input.root) const next = { ...state, enabled: false } await MemoryFiles.writeState(input.root, next) - await MemoryFiles.append(input.root, `disable ${next.scope} source=command`) return { root: input.root, state: next } }) } @@ -124,16 +122,6 @@ export namespace Memory { ...(input.settings.verbose === undefined ? {} : { verbose: input.settings.verbose }), } await MemoryFiles.writeState(input.root, next) - await MemoryFiles.append( - input.root, - [ - `settings ${next.scope}`, - input.settings.autoConsolidate === undefined ? "" : `autoConsolidate=${next.autoConsolidate}`, - input.settings.verbose === undefined ? "" : `verbose=${next.verbose}`, - ] - .filter(Boolean) - .join(" "), - ) return { root: input.root, state: next } }) } @@ -216,24 +204,6 @@ export namespace Memory { } const state = await MemoryFiles.readState(input.root) const ok = MemoryNotice.saved({ added: result.added, removed: result.removed }) - if (trigger === "explicit") { - await MemoryFiles.decide(input.root, { - kind: "typed", - trigger, - sessionID: input.sessionID, - result: ok ? "saved" : "skipped", - llm: false, - parsed: true, - fallback: false, - tokens: input.tokens ?? 0, - operationCount: result.operationCount, - skippedCount: result.skipped.length || (ok ? 0 : 1), - skipped: MemoryNotice.skip(result.skipped), - operations: MemoryNotice.ops({ ops: accepted, skipped: result.skipped }), - files: MemoryShared.files(accepted), - summary: MemoryNotice.summary({ added: result.added, removed: result.removed, count: result.operationCount }), - }) - } return { root: input.root, state, @@ -318,31 +288,6 @@ export namespace Memory { const hits = result?.hits ?? [] const files = [...new Set(hits.map((hit) => hit.source))] const topics = [...new Set(hits.flatMap((hit) => (hit.topics?.length ? hit.topics : [hit.kind])))] - await MemoryFiles.decide(input.root, { - kind: "recall", - trigger: "targeted-recall", - sessionID: input.sessionID, - result: result ? "recalled" : "skipped", - llm: false, - parsed: false, - fallback: false, - reason: result ? undefined : "no_matches", - query: MemoryShared.brief(MemoryRedact.text(input.query), 240), - topics, - files, - tokens: result?.tokens ?? 0, - operationCount: hits.length, - skippedCount: result ? 0 : 1, - summary: result ? `targeted recall matched ${hits.length} memories` : "targeted recall found no matches", - }) - if (result) { - await MemoryFiles.queue(input.root, async () => { - await MemoryFiles.append( - input.root, - `recall session=${input.sessionID ?? ""} hits=${result.hits.length} tokens=${result.tokens} files=${files.join(",")}`, - ) - }) - } return { root: input.root, state, result, hits, files, topics } } @@ -368,10 +313,6 @@ export namespace Memory { }) await MemoryFiles.pruneSessions(input.root, state.limits.maxSessionFiles) const index = await MemoryIndexer.rebuild({ root: input.root, state }) - await MemoryFiles.append( - input.root, - `session digest session=${input.sessionID} tokens=${input.tokens ?? 0} indexTokens=${index.tokens}`, - ) return { root: input.root, state, skipped: false as const, index } }) } diff --git a/packages/kilo-memory/src/recall/indexer.ts b/packages/kilo-memory/src/recall/indexer.ts index e4c219cbb01..1f906d2867b 100644 --- a/packages/kilo-memory/src/recall/indexer.ts +++ b/packages/kilo-memory/src/recall/indexer.ts @@ -226,7 +226,6 @@ export namespace MemoryIndexer { return MemoryFiles.queue(input.root, async () => { const result = await build(input) await MemoryFiles.writeIndex(input.root, result.text) - await MemoryFiles.append(input.root, `regenerate index.kmem bytes=${result.bytes} tokens=${result.tokens}`) return result }) } diff --git a/packages/kilo-memory/src/recall/shared.ts b/packages/kilo-memory/src/recall/shared.ts index b24829b2f46..b38f215e6ac 100644 --- a/packages/kilo-memory/src/recall/shared.ts +++ b/packages/kilo-memory/src/recall/shared.ts @@ -94,6 +94,7 @@ export namespace MemoryShared { ] } + /** @deprecated Memory audit persistence was removed. */ export function audit(ops: MemoryOperations.Op[]) { return ops.map((item) => item.action === "add" diff --git a/packages/kilo-memory/src/schema.ts b/packages/kilo-memory/src/schema.ts index 59436a8c144..0ca36e7616e 100644 --- a/packages/kilo-memory/src/schema.ts +++ b/packages/kilo-memory/src/schema.ts @@ -1,3 +1,9 @@ +/** Wire contract for suppressible background-capture failures: guardReason emits this as the + * `memory.error` reason, and every client surface (TUI toast filter, VS Code provider) matches + * against this same constant rather than a local literal. Lives here (dependency-free module) + * so clients can import it without pulling the capture pipeline into their bundles. */ +export const TRANSIENT = "transient" as const + export namespace MemorySchema { export const VERSION = 1 export const maxStoredDigestSummary = 4_000 diff --git a/packages/kilo-memory/src/storage/audit.ts b/packages/kilo-memory/src/storage/audit.ts index 70c7c42bf3a..58fe9f496b7 100644 --- a/packages/kilo-memory/src/storage/audit.ts +++ b/packages/kilo-memory/src/storage/audit.ts @@ -1,15 +1,5 @@ -import z from "zod" -import { MemoryFs } from "./fs" - +/** Compatibility facade for the removed memory audit log. */ export namespace MemoryAudit { - const Log = z - .object({ - kind: z.literal("log"), - summary: z.string(), - time: z.string().optional(), - }) - .passthrough() - export type Decision = | { kind: "log" @@ -43,18 +33,14 @@ export namespace MemoryAudit { }[] } - function audit(root: string, input: Decision) { - void root - void input - return Promise.resolve() - } - export async function append(root: string, text: string) { - await audit(root, { kind: "log", result: "logged", summary: text }) + void root + void text } export async function decide(root: string, input: Decision) { - await audit(root, input) + void root + void input } export async function readDecisions(root: string) { @@ -62,24 +48,8 @@ export namespace MemoryAudit { return "" } - function record(input: string) { - try { - const data = JSON.parse(input) - const parsed = Log.safeParse(data) - return parsed.success ? parsed.data : undefined - } catch (error) { - if (MemoryFs.parse(error)) return undefined - throw error - } - } - export async function readChanges(root: string) { - const lines = (await readDecisions(root)).split("\n").flatMap((line) => { - const data = record(line) - if (!data) return [] - const time = data.time ?? "" - return [`${time} ${data.summary}`.trim()] - }) - return lines.join("\n") + void root + return "" } } diff --git a/packages/kilo-memory/src/storage/state.ts b/packages/kilo-memory/src/storage/state.ts index bb373e24610..136a83a3e5e 100644 --- a/packages/kilo-memory/src/storage/state.ts +++ b/packages/kilo-memory/src/storage/state.ts @@ -1,6 +1,5 @@ import { readdir, rm } from "fs/promises" import path from "path" -import { MemoryAudit } from "./audit" import { MemoryFs } from "./fs" import { MemoryMarkdown } from "./markdown" import { MemoryPaths } from "./paths" @@ -19,13 +18,10 @@ export namespace MemoryState { "corrections.md": "# Corrective Memory\n\n## Corrections\n", } - async function recover(root: string, file: string, error: unknown) { + async function recover(root: string, file: string) { await MemoryFs.backup(file) const state = MemorySchema.missing() await writeState(root, state) - await MemoryAudit.append(root, `recover state.json error=${MemoryFs.brief(error)}`).catch((err: unknown) => - MemoryFs.warn("failed to audit memory state recovery", { err, root }), - ) return state } @@ -33,14 +29,14 @@ export namespace MemoryState { const file = MemoryPaths.files(root).state const data = await MemoryFs.json(file).catch(async (error: unknown) => { if (MemoryFs.miss(error)) return undefined - if (MemoryFs.parse(error)) return recover(root, file, error) + if (MemoryFs.parse(error)) return recover(root, file) throw error }) if (data === undefined) return MemorySchema.missing() return Promise.resolve() .then(() => MemorySchema.parse(data)) .catch((error: unknown) => { - if (MemoryFs.parse(error)) return recover(root, file, error) + if (MemoryFs.parse(error)) return recover(root, file) throw error }) } @@ -199,7 +195,6 @@ export namespace MemoryState { ? { ...(await readState(root)), enabled: true, autoInject: true } : { ...MemorySchema.create(), enabled: true } await writeState(root, state) - await MemoryAudit.append(root, "enable project source=command") return state } diff --git a/packages/kilo-memory/src/tool.ts b/packages/kilo-memory/src/tool.ts index e87fb85d0f5..cef71179d35 100644 --- a/packages/kilo-memory/src/tool.ts +++ b/packages/kilo-memory/src/tool.ts @@ -4,7 +4,6 @@ import { MemoryError, type MemoryError as Failure } from "./effect/errors" import { MemoryPaths } from "./effect/paths" import { MemoryService } from "./effect/service" import { MemoryRecall } from "./recall/recall" -import { MemoryToken } from "./recall/token" import { MemorySchema } from "./schema" import recallDescription from "./prompts/tool-memory-recall.txt" import saveDescription from "./prompts/tool-memory-save.txt" @@ -115,50 +114,6 @@ export namespace MemoryTool { } } - function audit( - memory: MemoryService.Interface, - input: { - root: string - params: RecallParams - current: string - hits: MemoryRecall.Hit[] - skipped?: string - output: string - }, - ) { - const files = [...new Set(input.hits.map((hit) => hit.source))] - const topics = [...new Set(input.hits.flatMap((hit) => (hit.topics?.length ? hit.topics : [hit.kind])))] - const query = - input.params.query ?? - (input.params.sessionID - ? `sessionID=${input.params.sessionID}` - : input.params.mode === "digest" - ? "recent digests" - : undefined) - return memory.decide({ - root: input.root, - decision: { - kind: "recall", - trigger: "targeted-recall", - sessionID: input.current, - result: input.hits.length ? "recalled" : "skipped", - llm: false, - parsed: false, - fallback: false, - reason: input.skipped, - query, - topics, - files, - tokens: MemoryToken.estimate(input.output), - operationCount: input.hits.length, - skippedCount: input.hits.length ? 0 : 1, - summary: input.hits.length - ? `memory recall returned ${input.hits.length} ${input.params.mode} hits` - : `memory recall found no ${input.params.mode} hits`, - }, - }) - } - function miss(input: { params: RecallParams; current: string }) { const self = input.params.sessionID === input.current if (self && input.params.mode === "digest") { @@ -239,35 +194,6 @@ export namespace MemoryTool { }) } - function catalogAudit( - memory: MemoryService.Interface, - input: { - root: string - current: string - query: string - result: { output: string; count: number; files: string[] } - }, - ) { - return memory.decide({ - root: input.root, - decision: { - kind: "recall", - trigger: "targeted-recall", - sessionID: input.current, - result: input.result.count ? "recalled" : "skipped", - llm: false, - parsed: false, - fallback: false, - query: input.query || "all keys", - files: input.result.files, - tokens: MemoryToken.estimate(input.result.output), - operationCount: input.result.count, - skippedCount: input.result.count ? 0 : 1, - summary: `memory catalog listed ${input.result.count} entries`, - }, - }) - } - function approvalRecall(input: Recall) { return input.ask({ permission: "kilo_memory_recall", @@ -285,7 +211,6 @@ export namespace MemoryTool { return Effect.gen(function* () { const result = yield* catalog(input.memory, { root: live.root, query }) const safe = { ...result, output: block(result.output) } - yield* catalogAudit(input.memory, { root: live.root, current: live.current, query, result: safe }) yield* input.memory.recordRecall({ root: live.root, sessionID: live.current, now: Date.now(), count: result.count }) return { title: `Kilo memory catalog: ${result.count} entr${result.count === 1 ? "y" : "ies"}`, @@ -296,22 +221,13 @@ export namespace MemoryTool { } function recallQuery(input: Recall, live: Live) { - return Effect.gen(function* () { - const output = "Provide a topic query for typed/search memory recall." - yield* audit(input.memory, { - root: live.root, - params: input.params, - current: live.current, - hits: [], - skipped: "missing_query", - output, - }) - return { + return Effect.succeed( + { title: `Kilo memory ${input.params.mode}: no query`, - output, + output: "Provide a topic query for typed/search memory recall.", metadata: { sources: [], count: 0 }, - } satisfies Result - }) + } satisfies Result, + ) } function recallSearch(input: Recall, live: Live, query: string, mode: MemoryRecall.Mode) { @@ -327,15 +243,7 @@ export namespace MemoryTool { limit, }) const hits = result?.hits ?? [] - const self = input.params.sessionID === live.current - const skipped = - input.params.sessionID && input.params.mode === "digest" && hits.length === 0 - ? self - ? "current_session_digest" - : "missing_session_digest" - : undefined const output = hits.length ? result!.block : miss({ params: input.params, current: live.current }) - yield* audit(input.memory, { root: live.root, params: input.params, current: live.current, hits, skipped, output }) yield* input.memory.recordRecall({ root: live.root, sessionID: live.current, now: Date.now(), count: hits.length }) if (hits.length === 0) { @@ -440,29 +348,8 @@ export namespace MemoryTool { return saved(input) } - function skip(input: Base & { params: SaveParams }, root: string) { - return Effect.gen(function* () { - const reason = input.params.reason ?? "out_of_scope" - yield* input.memory.decide({ - root, - decision: { - kind: "typed", - trigger: "explicit", - sessionID: input.sessionID, - result: "skipped", - llm: false, - parsed: true, - fallback: false, - reason, - tokens: 0, - operationCount: 0, - skippedCount: 1, - skipped: [{ reason }], - summary: `explicit memory save skipped: ${reason}`, - }, - }) - return skipped({ reason }) - }) + function skip(input: Base & { params: SaveParams }) { + return Effect.succeed(skipped({ reason: input.params.reason ?? "out_of_scope" })) } function forget(input: Save, root: string) { @@ -497,7 +384,7 @@ export namespace MemoryTool { const state = yield* input.memory.state({ root }) if (!state.enabled) return disabled() if (input.params.action === "forget") return yield* forget(input, root) - if (input.params.action === "skip") return yield* skip(input, root) + if (input.params.action === "skip") return yield* skip(input) return yield* write(input, root) }) } diff --git a/packages/kilo-memory/test/capture.test.ts b/packages/kilo-memory/test/capture.test.ts index a1dcdc609c2..516ed3a65d3 100644 --- a/packages/kilo-memory/test/capture.test.ts +++ b/packages/kilo-memory/test/capture.test.ts @@ -1,8 +1,8 @@ import { describe, expect, test } from "bun:test" import { - auditOps, capturePlan, duplicateOps, + errorReason, fallbackDigest, guardReason, hasSubstantialDiff, @@ -128,7 +128,7 @@ describe("memory capture parsing", () => { expect(() => salvageTyped(`{"op":"upsert_project_fact","key":"good_one","value":"Keep this fact."}`)).toThrow() }) - test("redacts secrets in salvaged unsupported ops before they reach the audit", () => { + test("redacts secrets in salvaged unsupported ops before they reach callers", () => { const parsed = salvageTyped( `{"operations":[{"op":"not_a_real_op","key":"leak","value":"key is sk-abcdefghijklmnopqrstuvwxyz"}],"skipped":[]}`, ) @@ -169,17 +169,6 @@ describe("memory capture parsing", () => { expect(JSON.stringify(parsed.skipped)).not.toContain(secret.slice(0, 20)) }) - test("redacts remove audit queries before truncating", () => { - const secret = "sk-" + "a".repeat(40) - const query = "x".repeat(100) + secret - const audit = auditOps([{ action: "remove", query }]) - const text = JSON.stringify(audit) - - expect(text).toContain("[redacted]") - expect(text).not.toContain(secret) - expect(text).not.toContain(secret.slice(0, 20)) - }) - test("truncates typed batches beyond the op cap instead of failing", () => { const ops = Array.from( { length: 20 }, @@ -332,6 +321,11 @@ describe("memory capture parsing", () => { input: base, expected: { session: true, digestDue: true, typedCall: true, typedWork: true, skipReason: undefined }, }, + { + name: "expected work: a persisted null typed clock does not throttle capture", + input: { ...base, lastTypedConsolidationAt: null }, + expected: { session: true, digestDue: true, typedCall: true, typedWork: true, skipReason: undefined }, + }, { name: "expected idle flush: completed turn inside interval skips now", input: { ...base, priorTime: 900, lastTypedConsolidationAt: 900 }, @@ -650,6 +644,70 @@ describe("memory capture parsing", () => { ) expect(guardReason("429 too many requests")).toBe("rate_limit_guard") expect(guardReason("billing credits exhausted")).toBe("quota_guard") + expect(guardReason("request timed out")).toBe("transient") + expect(guardReason("deadline exceeded")).toBe("transient") + expect(guardReason("DeadlineExceeded")).toBe("transient") + expect(guardReason('cause={"code":"ETIMEDOUT"}')).toBe("transient") + expect(guardReason("504 Gateway Timeout")).toBe("transient") + expect(guardReason("Connect Timeout Error")).toBe("transient") + expect(guardReason("Headers Timeout Error")).toBe("transient") + expect(guardReason("Body Timeout Error")).toBe("transient") + expect(guardReason("connect_timeout")).toBe("transient") + expect(guardReason('status=400 body={"error":"invalid parameter: timeout"}')).toBeUndefined() + expect(guardReason("set request timeout to 30000")).toBeUndefined() + const timeout = new Error("request aborted") + timeout.name = "TimeoutError" + expect(guardReason(timeout)).toBe("transient") + expect(errorReason(timeout)).toBe("request aborted") + expect(guardReason(Object.assign(new Error("request failed"), { status: 504 }))).toBe("transient") + expect( + guardReason(Object.assign(new Error("request failed"), { cause: { code: "UND_ERR_CONNECT_TIMEOUT" } })), + ).toBe("transient") + expect( + guardReason(Object.assign(new Error("request failed"), { cause: { code: "UND_ERR_HEADERS_TIMEOUT" } })), + ).toBe("transient") + expect(guardReason(Object.assign(new Error("request failed"), { cause: { code: "ETIMEDOUT" } }))).toBe( + "transient", + ) + expect( + guardReason(Object.assign(new Error("failed after 2 attempts"), { errors: [{ statusCode: 504 }] })), + ).toBe("transient") + expect(guardReason(new Error("set request timeout to 30000"))).toBeUndefined() + expect(guardReason(new Error("request timed out"))).toBe("transient") + expect(guardReason(Object.assign(new Error("request failed"), { cause: new Error("connect timeout") }))).toBe( + "transient", + ) + expect( + guardReason( + Object.assign(new Error("failed after 2 attempts"), { + errors: [Object.assign(new Error("rate limit reached"), { status: 429 }), timeout], + }), + ), + ).toBe("rate_limit_guard") + expect( + guardReason( + Object.assign(new Error("failed after 2 attempts"), { + errors: [new Error("insufficient quota available"), timeout], + }), + ), + ).toBe("quota_guard") + expect( + guardReason( + Object.assign(new Error("request failed"), { + cause: Object.assign(new Error("rate limit reached"), { status: 429 }), + }), + ), + ).toBe("rate_limit_guard") + expect( + guardReason( + Object.assign(new Error("timed out"), { + name: "TimeoutError", + cause: new Error("billing credit balance exhausted"), + }), + ), + ).toBe("quota_guard") + expect(guardReason("timeout after 429 too many requests")).toBe("rate_limit_guard") + expect(guardReason("timeout after quota exceeded")).toBe("quota_guard") }) test("redacts common secret token shapes", () => { diff --git a/packages/kilo-memory/test/core.test.ts b/packages/kilo-memory/test/core.test.ts index 04b52a03624..9c029e6e211 100644 --- a/packages/kilo-memory/test/core.test.ts +++ b/packages/kilo-memory/test/core.test.ts @@ -50,6 +50,22 @@ describe("memory core package", () => { }) }) + test("legacy audit compatibility APIs remain no-op", async () => { + await use(async (t) => { + await Memory.enable({ root: t.root }) + await MemoryFiles.append(t.root, "provider error with sensitive detail") + await MemoryFiles.decide(t.root, { + kind: "typed", + result: "error", + reason: "provider error with sensitive detail", + }) + + expect(await MemoryFiles.readChanges(t.root)).toBe("") + expect(await MemoryFiles.readDecisions(t.root)).toBe("") + expect(await Bun.file(path.join(t.root, "decisions.jsonl")).exists()).toBe(false) + }) + }) + test("prepare removes legacy decisions once from owned memory roots", async () => { await use(async (t) => { await Memory.enable({ root: t.root }) diff --git a/packages/kilo-memory/test/effect-capture.test.ts b/packages/kilo-memory/test/effect-capture.test.ts index 91e0ec84e04..50a86b0f1f4 100644 --- a/packages/kilo-memory/test/effect-capture.test.ts +++ b/packages/kilo-memory/test/effect-capture.test.ts @@ -7,6 +7,7 @@ import { digestPrompt, typedPrompt } from "../src/capture/capture" import { MemoryCapture } from "../src/effect/capture" import { MemoryEvents } from "../src/effect/events" import { KiloMemory } from "../src/effect/index" +import { MemoryLog } from "../src/effect/log" import type { MemoryPorts } from "../src/effect/ports" import { MemoryService } from "../src/effect/service" import { MemoryTimers } from "../src/effect/timers" @@ -49,11 +50,18 @@ function session(turn: MemoryPorts.TurnView | undefined): MemoryPorts.SessionPor /** Model port that answers digest/typed calls from canned JSON, keyed by system prompt so it is * order-independent (digest and typed run concurrently). */ -function model(input: { digest: string; typed: string; fallback?: string; onRun?: (system: string) => void }): MemoryPorts.ModelPort { +function model(input: { + digest: string + typed: string + fallback?: string + fail?: Error + onRun?: (system: string) => void +}): MemoryPorts.ModelPort { return { resolve: () => Effect.succeed({ handle: {}, ...(input.fallback ? { fallback: { reason: input.fallback } } : {}) }), run: async ({ system }) => { input.onRun?.(system) + if (system === typedPrompt && input.fail) throw input.fail const text = system === digestPrompt ? input.digest : system === typedPrompt ? input.typed : "{}" return { text, usage: USAGE } }, @@ -66,6 +74,7 @@ function run(input: { model: MemoryPorts.ModelPort memoryModel?: string reason?: "completed" | "interrupted" | "error" + bypassInterval?: boolean }) { return Effect.runPromise( MemoryCapture.turn({ @@ -75,6 +84,7 @@ function run(input: { model: input.model, memoryModel: input.memoryModel, reason: input.reason ?? "completed", + bypassInterval: input.bypassInterval, }).pipe(Effect.provideService(MemoryService.Service, MemoryService.make())), ) } @@ -107,6 +117,121 @@ describe("MemoryCapture (fake ports)", () => { } }) + test("typed timeout preserves digest progress without advancing the typed clock", async () => { + const t = await tmp() + const events: MemoryEvents.Status[] = [] + const logs: string[] = [] + try { + await KiloMemory.enable({ root: t.root }) + await KiloMemory.configure({ root: t.root, settings: { autoConsolidate: true } }) + MemoryLog.setWarn((message, meta) => logs.push(`${message}:${meta?.reason}:${meta?.detail}`)) + MemoryEvents.setSink((input) => { + events.push(input.payload) + }) + + const result = await run({ + root: t.root, + session: session(view()), + model: model({ + digest: '{"topic":"repo setup","summary":"Explored repo setup commands. Next step: verify memory tests."}', + typed: "{}", + fail: new DOMException("memory model timed out", "TimeoutError"), + }), + }) + + expect(result).toMatchObject({ skipped: false, operationCount: 0 }) + const state = await MemoryFiles.readState(t.root) + expect(state.stats.lastTypedConsolidationAt).toBeNull() + expect(state.stats.lastSessionSavedAt).toEqual(expect.any(Number)) + expect(state.stats.lastConsolidatedMessageID).toBe("msg_assistant") + expect(events.find((item) => item.state === "error")?.reason).toBe("transient") + expect(logs).toEqual(["memory capture transient failure:transient:memory model timed out"]) + + const calls: string[] = [] + const retry = await run({ + root: t.root, + session: session(view()), + model: model({ + digest: "{}", + typed: '{"operations":[],"skipped":[]}', + onRun: (system) => calls.push(system), + }), + bypassInterval: true, + }) + expect(retry).toMatchObject({ skipped: true, reason: "no_new_content" }) + expect(calls).toEqual([]) + + const next = await run({ + root: t.root, + session: session( + view({ + assistant: "Use bun install, then run the package tests and typecheck.", + lastAssistantID: "msg_assistant_next", + }), + ), + model: model({ + digest: "{}", + typed: '{"operations":[],"skipped":[]}', + onRun: (system) => calls.push(system), + }), + }) + expect(next).toMatchObject({ skipped: false, operationCount: 0 }) + expect(calls).toEqual([typedPrompt]) + const updated = await MemoryFiles.readState(t.root) + expect(updated.stats.lastTypedConsolidationAt).toEqual(expect.any(Number)) + expect(updated.stats.lastConsolidatedMessageID).toBe("msg_assistant_next") + } finally { + MemoryLog.setWarn(() => {}) + MemoryEvents.setSink(() => {}) + await t.done() + } + }) + + test("fallback commit preserves metrics from the prior successful typed consolidation", async () => { + const t = await tmp() + try { + await KiloMemory.enable({ root: t.root }) + await KiloMemory.configure({ root: t.root, settings: { autoConsolidate: true } }) + + const first = await run({ + root: t.root, + session: session(view()), + model: model({ + digest: '{"topic":"repo setup","summary":"Explored repo setup."}', + typed: + '{"operations":[{"op":"upsert_environment_fact","section":"Commands","key":"test_cmd","value":"bun test"}],"skipped":[]}', + }), + }) + expect(first).toMatchObject({ skipped: false, operationCount: 1 }) + const initial = await MemoryFiles.readState(t.root) + expect(initial.stats.lastOperationCount).toBe(1) + expect(initial.stats.lastTypedConsolidationAt).toEqual(expect.any(Number)) + + const second = await run({ + root: t.root, + session: session( + view({ + assistant: "Investigated a timeout edge case.", + lastAssistantID: "msg_assistant_timeout", + }), + ), + model: model({ + digest: '{"topic":"investigation","summary":"Investigated timeouts."}', + typed: "{}", + fail: new DOMException("memory model timed out", "TimeoutError"), + }), + bypassInterval: true, + }) + expect(second).toMatchObject({ skipped: false, operationCount: 0 }) + const preserved = await MemoryFiles.readState(t.root) + expect(preserved.stats.lastOperationCount).toBe(1) + expect(preserved.stats.lastTypedConsolidationAt).toBe(initial.stats.lastTypedConsolidationAt) + expect(preserved.stats.lastConsolidatedMessageID).toBe("msg_assistant_timeout") + } finally { + await t.done() + } + }) + test("turn-close skips a secret-like op and applies the rest of the batch", async () => { const t = await tmp() const events: MemoryEvents.Status[] = [] diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 4660904c00a..918b0a0afc3 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -1,6 +1,7 @@ import * as path from "path" import { existsSync } from "fs" import * as vscode from "vscode" +import { TRANSIENT as MEMORY_TRANSIENT } from "@kilocode/kilo-memory/schema" import type { KiloClient, Session, @@ -4307,8 +4308,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper for (const sid of trackedByDir) targets.add(sid) if (local && active) targets.add(active) if (targets.size === 0 && local) targets.add(undefined) - const detail = - props.detail && typeof props.detail === "object" + const transient = event.type === "memory.error" && props.reason === MEMORY_TRANSIENT + const detail = transient + ? undefined + : props.detail && typeof props.detail === "object" ? props.detail : event.type === "memory.error" && typeof props.reason === "string" ? { type: "error", message: props.reason, reason: props.reason } diff --git a/packages/kilo-vscode/tests/unit/kilo-provider-memory-events.test.ts b/packages/kilo-vscode/tests/unit/kilo-provider-memory-events.test.ts index 274cb8cc60d..815ab32f8bd 100644 --- a/packages/kilo-vscode/tests/unit/kilo-provider-memory-events.test.ts +++ b/packages/kilo-vscode/tests/unit/kilo-provider-memory-events.test.ts @@ -127,6 +127,43 @@ describe("KiloProvider memory events", () => { expect(calls).toEqual(["/repo", "/repo"]) }) + it("refreshes status without forwarding transient memory errors", async () => { + const calls: string[] = [] + const posts: unknown[] = [] + const client = { + memory: { + status: async (input: { directory: string }) => { + calls.push(input.directory) + return { data: status(input.directory) } + }, + }, + } as unknown as KiloClient + const provider = new KiloProvider( + {} as never, + { + getClient: () => client, + } as never, + ) + const item = provider as unknown as Internals + item.webview = { postMessage: async (message) => posts.push(message) } + item.currentSession = { id: "ses_active" } + item.trackedSessionIds.add("ses_active") + provider.setSessionDirectory("ses_active", "/repo") + + item.handleEvent( + { + type: "memory.error", + properties: { sessionID: "ses_active", reason: "transient" }, + }, + "/repo", + ) + await item.memory.idle() + + expect(posts).not.toContainEqual(expect.objectContaining({ type: "memoryEvent" })) + expect(posts).toContainEqual(expect.objectContaining({ type: "memoryLoaded", sessionID: "ses_active" })) + expect(calls).toEqual(["/repo"]) + }) + it("uses the project directory when toggling memory", async () => { const calls: unknown[] = [] const client = { diff --git a/packages/opencode/src/kilocode/cli/cmd/tui/memory-events.ts b/packages/opencode/src/kilocode/cli/cmd/tui/memory-events.ts index c388231b922..293feee20cf 100644 --- a/packages/opencode/src/kilocode/cli/cmd/tui/memory-events.ts +++ b/packages/opencode/src/kilocode/cli/cmd/tui/memory-events.ts @@ -1,3 +1,5 @@ +import { TRANSIENT } from "@kilocode/kilo-memory/schema" + type Event = { on(type: "memory.error", fn: (event: MemoryEvent) => void): void | (() => void) } @@ -22,6 +24,7 @@ export namespace MemoryTuiEvents { }) { const handler = (event: MemoryEvent) => { if (event.properties.sessionID && event.properties.sessionID !== input.sessionID) return + if (event.properties.reason === TRANSIENT) return const detail = event.properties.detail if (!detail || typeof detail !== "object") { input.toast.show({ diff --git a/packages/opencode/src/kilocode/memory/ports.ts b/packages/opencode/src/kilocode/memory/ports.ts index 03d26d54674..9b38ceac89d 100644 --- a/packages/opencode/src/kilocode/memory/ports.ts +++ b/packages/opencode/src/kilocode/memory/ports.ts @@ -191,6 +191,7 @@ async function memoryText(input: { temperature: input.temperature, topP: input.topP, topK: input.topK, + maxRetries: 1, } const work = async () => { if (!openai) return generateText(common) @@ -210,7 +211,7 @@ async function memoryText(input: { const timeout = new Promise((_, reject) => { timer = setTimeout(() => { ctl.abort() - reject(new Error("memory model timed out")) + reject(new DOMException("memory model timed out", "TimeoutError")) }, ms) }) try { diff --git a/packages/opencode/test/kilocode/cli/cmd/tui/memory-command.test.ts b/packages/opencode/test/kilocode/cli/cmd/tui/memory-command.test.ts index 42df5344333..7448077955b 100644 --- a/packages/opencode/test/kilocode/cli/cmd/tui/memory-command.test.ts +++ b/packages/opencode/test/kilocode/cli/cmd/tui/memory-command.test.ts @@ -276,7 +276,7 @@ describe("memory TUI events", () => { expect(handlers).toEqual({ "memory.error": [expect.any(Function)] }) }) - test("keeps generic and detailed errors visible", async () => { + test("suppresses transient errors while keeping generic and detailed errors visible", async () => { const shown: string[] = [] const handlers: Record = {} MemoryTuiEvents.attach({ @@ -298,6 +298,17 @@ describe("memory TUI events", () => { fn({ properties: { sessionID: "ses_tui_memory", reason: "model failed" } }), ), ) + await Promise.all( + (handlers["memory.error"] ?? []).map((fn) => + fn({ + properties: { + sessionID: "ses_tui_memory", + reason: "transient", + detail: { message: "Memory model timed out" }, + }, + }), + ), + ) await Promise.all( (handlers["memory.error"] ?? []).map((fn) => fn({ properties: { sessionID: "ses_tui_memory", detail: { message: "Memory save failed" } } }), diff --git a/packages/opencode/test/kilocode/memory/memory-ports.test.ts b/packages/opencode/test/kilocode/memory/memory-ports.test.ts index 8016c1ab00f..8b39e8e99c0 100644 --- a/packages/opencode/test/kilocode/memory/memory-ports.test.ts +++ b/packages/opencode/test/kilocode/memory/memory-ports.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test" import type { LanguageModelV3 } from "@ai-sdk/provider" +import { APICallError } from "ai" import { Effect } from "effect" import { ModelNotFoundError, type Provider } from "../../../src/provider/provider" import { ProviderV2 } from "@opencode-ai/core/provider" @@ -31,15 +32,22 @@ function mdl(id = mid): Provider.Model { } as unknown as Provider.Model } -function lang(outputs = ["{}"]): LanguageModelV3 { +function lang(outputs: (string | Error)[] = ["{}"], calls?: unknown[], hang?: boolean): LanguageModelV3 { let idx = 0 + const next = () => { + const item = outputs[idx++] ?? outputs.at(-1) ?? "{}" + if (item instanceof Error) throw item + return item + } return { specificationVersion: "v3", provider: "test", modelId: "fake-memory-model", supportedUrls: {}, - doGenerate: async () => { - const text = outputs[idx++] ?? outputs.at(-1) ?? "{}" + doGenerate: async (...args: Parameters) => { + calls?.push(args[0]) + if (hang) return new Promise(() => {}) + const text = next() return { content: [{ type: "text", text }], finishReason: { unified: "stop" }, @@ -57,7 +65,9 @@ function lang(outputs = ["{}"]): LanguageModelV3 { } as unknown as LanguageModelV3 } -function provider(input: { outputs?: string[]; seen?: string[] } = {}): Provider.Interface { +function provider( + input: { outputs?: (string | Error)[]; seen?: string[]; calls?: unknown[]; hang?: boolean } = {}, +): Provider.Interface { const base = mdl() const mem = mdl(ModelV2.ID.make("memory-config-model")) const info = { @@ -78,7 +88,7 @@ function provider(input: { outputs?: string[]; seen?: string[] } = {}): Provider }, getLanguage: (model) => { input.seen?.push(model.id) - return Effect.succeed(lang(input.outputs)) + return Effect.succeed(lang(input.outputs, input.calls, input.hang)) }, closest: () => Effect.succeed({ providerID: pid, modelID: base.id }), getSmallModel: () => Effect.succeed(mem), @@ -280,6 +290,34 @@ describe("memory ports", () => { expect(seen).toEqual(["memory-config-model", "fake-memory-model"]) }) + test("model port retries a transient provider failure once", async () => { + const calls: unknown[] = [] + const err = new APICallError({ + message: "temporarily unavailable", + url: "https://example.com/v1/generate", + requestBodyValues: {}, + statusCode: 503, + responseHeaders: {}, + responseBody: '{"error":"temporarily unavailable"}', + isRetryable: true, + }) + const port = MemoryModel.port({ provider: provider({ outputs: [err, "{}"], calls }) }) + const resolved = await Effect.runPromise(port.resolve({ session: ref })) + + await port.run({ handle: resolved.handle, system: "system", prompt: "prompt", timeoutMs: 30_000 }) + + expect(calls).toHaveLength(2) + }) + + test("model port emits a structured timeout error", async () => { + const port = MemoryModel.port({ provider: provider({ hang: true }) }) + const resolved = await Effect.runPromise(port.resolve({ session: ref })) + + await expect( + port.run({ handle: resolved.handle, system: "system", prompt: "prompt", timeoutMs: 1 }), + ).rejects.toMatchObject({ name: "TimeoutError", message: "memory model timed out" }) + }) + test("model port clears its timeout after successful output", async () => { const set = globalThis.setTimeout const clear = globalThis.clearTimeout From 2e5199a40d8791e99dc304ad722bf6baa707f09c Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Mon, 10 Aug 2026 12:49:48 +0000 Subject: [PATCH 55/73] feat(cli): exclude ChatGPT subscriptions from explicit prompt cache breakpoints --- ...de-chatgpt-from-prompt-cache-breakpoint.md | 5 ++ packages/opencode/src/provider/transform.ts | 19 ++++-- .../opencode/test/provider/transform.test.ts | 64 +++++++++++++++++++ 3 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 .changeset/exclude-chatgpt-from-prompt-cache-breakpoint.md diff --git a/.changeset/exclude-chatgpt-from-prompt-cache-breakpoint.md b/.changeset/exclude-chatgpt-from-prompt-cache-breakpoint.md new file mode 100644 index 00000000000..6362bdd7ff1 --- /dev/null +++ b/.changeset/exclude-chatgpt-from-prompt-cache-breakpoint.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Exclude ChatGPT subscriptions from explicit prompt cache breakpoints. diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index ff534bf107f..7a9659b25d2 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -328,15 +328,22 @@ function normalizeMessages( return msgs } -// kilocode_change start - explicit prompt cache breakpoints for GPT-5.6+ -function supportsPromptCacheBreakpoint(modelId: string): boolean { - const match = modelId.match(/gpt-(\d+)\.(\d+)/) +// kilocode_change start - explicit prompt cache breakpoints for GPT-5.6+ (excluding ChatGPT subscriptions) +function isChatGPTSubscription(model: Provider.Model): boolean { + if (model.providerID === "openai" && model.cost?.input === 0 && model.cost?.output === 0) return true + if (typeof model.api.url === "string" && model.api.url.includes("chatgpt.com")) return true + return false +} + +function supportsPromptCacheBreakpoint(model: Provider.Model): boolean { + if (isChatGPTSubscription(model)) return false + const match = model.api.id.match(/gpt-(\d+)\.(\d+)/) if (match) { const major = Number(match[1]) const minor = Number(match[2]) if (major > 5 || (major === 5 && minor >= 6)) return true } - const majorMatch = modelId.match(/gpt-(\d+)/) + const majorMatch = model.api.id.match(/gpt-(\d+)/) if (majorMatch && Number(majorMatch[1]) >= 6) return true return false } @@ -366,7 +373,7 @@ function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage cacheControl: { type: "ephemeral" }, }, // kilocode_change start - ...(supportsPromptCacheBreakpoint(model.api.id) + ...(supportsPromptCacheBreakpoint(model) ? { openai: { promptCacheBreakpoint: { mode: "explicit" }, @@ -494,7 +501,7 @@ export function message(msgs: ModelMessage[], model: Provider.Model, options: Re ((model.api.npm === "@ai-sdk/openai" || model.api.npm === "@ai-sdk/azure" || model.api.npm === "@kilocode/kilo-gateway") && - supportsPromptCacheBreakpoint(model.api.id))) && + supportsPromptCacheBreakpoint(model))) && model.api.npm !== "@ai-sdk/gateway" ) { msgs = applyCaching(msgs, model) diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 74ef174ade8..508f6a8cd0b 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -3162,6 +3162,70 @@ describe("ProviderTransform.message - cache control on gateway", () => { }, }) }) + + test("openai gpt-5.6 with ChatGPT subscription (zero cost) does not apply promptCacheBreakpoint", () => { + const model = createModel({ + providerID: "openai", + api: { + id: "gpt-5.6", + url: "https://api.openai.com/v1", + npm: "@ai-sdk/openai", + }, + id: "gpt-5.6", + cost: { + input: 0, + output: 0, + cache: { read: 0, write: 0 }, + }, + }) + const msgs = [ + { + role: "system", + content: "You are a helpful assistant", + }, + { + role: "user", + content: "Hello", + }, + ] as any[] + + const result = ProviderTransform.message(msgs, model, {}) as any[] + + expect(result[0].providerOptions?.openai?.promptCacheBreakpoint).toBeUndefined() + expect(result[1].providerOptions?.openai?.promptCacheBreakpoint).toBeUndefined() + }) + + test("openai gpt-5.6 with ChatGPT backend endpoint does not apply promptCacheBreakpoint", () => { + const model = createModel({ + providerID: "openai", + api: { + id: "gpt-5.6", + url: "https://chatgpt.com/backend-api/codex/responses", + npm: "@ai-sdk/openai", + }, + id: "gpt-5.6", + cost: { + input: 0.002, + output: 0.008, + cache: { read: 0.0005, write: 0.002 }, + }, + }) + const msgs = [ + { + role: "system", + content: "You are a helpful assistant", + }, + { + role: "user", + content: "Hello", + }, + ] as any[] + + const result = ProviderTransform.message(msgs, model, {}) as any[] + + expect(result[0].providerOptions?.openai?.promptCacheBreakpoint).toBeUndefined() + expect(result[1].providerOptions?.openai?.promptCacheBreakpoint).toBeUndefined() + }) // kilocode_change end }) From 9b820d2f9d153baf0c8e8840d83c9de98b93ab58 Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Mon, 10 Aug 2026 13:04:02 +0000 Subject: [PATCH 56/73] refactor(cli): simplify ChatGPT subscription detection to zero-cost heuristic Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- packages/opencode/src/provider/transform.ts | 8 ++--- .../opencode/test/provider/transform.test.ts | 34 +------------------ 2 files changed, 4 insertions(+), 38 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 7a9659b25d2..310c9d0c83c 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -329,14 +329,12 @@ function normalizeMessages( } // kilocode_change start - explicit prompt cache breakpoints for GPT-5.6+ (excluding ChatGPT subscriptions) -function isChatGPTSubscription(model: Provider.Model): boolean { - if (model.providerID === "openai" && model.cost?.input === 0 && model.cost?.output === 0) return true - if (typeof model.api.url === "string" && model.api.url.includes("chatgpt.com")) return true - return false +function isLikelyChatGPTSubscription(model: Provider.Model): boolean { + return model.providerID === "openai" && model.cost?.input === 0 && model.cost?.output === 0 } function supportsPromptCacheBreakpoint(model: Provider.Model): boolean { - if (isChatGPTSubscription(model)) return false + if (isLikelyChatGPTSubscription(model)) return false const match = model.api.id.match(/gpt-(\d+)\.(\d+)/) if (match) { const major = Number(match[1]) diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 508f6a8cd0b..e834ec326eb 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -3163,7 +3163,7 @@ describe("ProviderTransform.message - cache control on gateway", () => { }) }) - test("openai gpt-5.6 with ChatGPT subscription (zero cost) does not apply promptCacheBreakpoint", () => { + test("openai gpt-5.6 with ChatGPT subscription (zero cost heuristic) does not apply promptCacheBreakpoint", () => { const model = createModel({ providerID: "openai", api: { @@ -3194,38 +3194,6 @@ describe("ProviderTransform.message - cache control on gateway", () => { expect(result[0].providerOptions?.openai?.promptCacheBreakpoint).toBeUndefined() expect(result[1].providerOptions?.openai?.promptCacheBreakpoint).toBeUndefined() }) - - test("openai gpt-5.6 with ChatGPT backend endpoint does not apply promptCacheBreakpoint", () => { - const model = createModel({ - providerID: "openai", - api: { - id: "gpt-5.6", - url: "https://chatgpt.com/backend-api/codex/responses", - npm: "@ai-sdk/openai", - }, - id: "gpt-5.6", - cost: { - input: 0.002, - output: 0.008, - cache: { read: 0.0005, write: 0.002 }, - }, - }) - const msgs = [ - { - role: "system", - content: "You are a helpful assistant", - }, - { - role: "user", - content: "Hello", - }, - ] as any[] - - const result = ProviderTransform.message(msgs, model, {}) as any[] - - expect(result[0].providerOptions?.openai?.promptCacheBreakpoint).toBeUndefined() - expect(result[1].providerOptions?.openai?.promptCacheBreakpoint).toBeUndefined() - }) // kilocode_change end }) From 00dc8d0242f787a48e0cf44396f6a743b75b45e0 Mon Sep 17 00:00:00 2001 From: Christiaan Arnoldus Date: Mon, 10 Aug 2026 15:05:35 +0200 Subject: [PATCH 57/73] fix(vscode): keep session errors visible --- .changeset/show-vscode-session-errors.md | 5 +++ .../kilo-vscode/src/kilo-provider-utils.ts | 3 +- .../tests/unit/kilo-provider-utils.test.ts | 25 +++++++++++++ .../tests/unit/session-errors.test.ts | 36 +++++++++++++++++- .../webview-ui/src/context/session-errors.ts | 21 +++++++++++ .../webview-ui/src/context/session.tsx | 37 ++++++++++--------- .../src/types/messages/extension-messages.ts | 1 + .../webview-ui/src/types/messages/sessions.ts | 1 + 8 files changed, 109 insertions(+), 20 deletions(-) create mode 100644 .changeset/show-vscode-session-errors.md diff --git a/.changeset/show-vscode-session-errors.md b/.changeset/show-vscode-session-errors.md new file mode 100644 index 00000000000..5302a13a707 --- /dev/null +++ b/.changeset/show-vscode-session-errors.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Keep model and provider errors visible in VS Code when chat history refreshes. diff --git a/packages/kilo-vscode/src/kilo-provider-utils.ts b/packages/kilo-vscode/src/kilo-provider-utils.ts index c4bd2f9f74e..c1284b1d481 100644 --- a/packages/kilo-vscode/src/kilo-provider-utils.ts +++ b/packages/kilo-vscode/src/kilo-provider-utils.ts @@ -450,7 +450,7 @@ export type WebviewMessage = | { type: "sessionUpdated"; session: ReturnType } | { type: "sessionDeleted"; sessionID: string } | { type: "messageRemoved"; sessionID: string; messageID: string } - | { type: "sessionError"; sessionID?: string; error?: unknown } + | { type: "sessionError"; eventID: string; sessionID?: string; error?: unknown } | { type: "sandboxStatus" sessionID: string @@ -620,6 +620,7 @@ export function mapSSEEventToWebviewMessage(event: StreamEvent, sessionID: strin case "session.error": { return { type: "sessionError", + eventID: event.id, sessionID: event.properties.sessionID, error: event.properties.error, } diff --git a/packages/kilo-vscode/tests/unit/kilo-provider-utils.test.ts b/packages/kilo-vscode/tests/unit/kilo-provider-utils.test.ts index 3a9213d887f..75ed68671e5 100644 --- a/packages/kilo-vscode/tests/unit/kilo-provider-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/kilo-provider-utils.test.ts @@ -21,6 +21,7 @@ import type { Event, EventSessionStatus, EventSessionTurnClose, + EventSessionError, EventSandboxStatusChanged, EventPermissionAsked, EventPermissionReplied, @@ -375,6 +376,30 @@ describe("mapSSEEventToWebviewMessage", () => { expect(msg).toEqual({ type: "sessionTurnClosed", sessionID: "sess-1", reason: "interrupted" }) }) + it("maps session errors with their event identity and message", () => { + const event: EventSessionError = { + id: "evt-error", + type: "session.error", + properties: { + sessionID: "sess-1", + error: { + name: "APIError", + data: { + message: "prompt_cache_breakpoint is not supported on this model", + isRetryable: false, + }, + }, + }, + } + + expect(mapSSEEventToWebviewMessage(event, "sess-1")).toEqual({ + type: "sessionError", + eventID: "evt-error", + sessionID: "sess-1", + error: event.properties.error, + }) + }) + it("maps permission.asked to permissionRequest", () => { const event: EventPermissionAsked = { type: "permission.asked", diff --git a/packages/kilo-vscode/tests/unit/session-errors.test.ts b/packages/kilo-vscode/tests/unit/session-errors.test.ts index c24ed0297a8..53eb01d8152 100644 --- a/packages/kilo-vscode/tests/unit/session-errors.test.ts +++ b/packages/kilo-vscode/tests/unit/session-errors.test.ts @@ -1,5 +1,10 @@ import { describe, expect, it } from "bun:test" -import { errorIDs, visibleError } from "../../webview-ui/src/context/session-errors" +import { + errorIDs, + preserveSessionErrors, + visibleError, + withoutResolvedSessionErrors, +} from "../../webview-ui/src/context/session-errors" import type { Message } from "../../webview-ui/src/types/messages" const base = { @@ -45,3 +50,32 @@ describe("visibleError", () => { expect(visibleError(messages, () => false)).toBeUndefined() }) }) + +describe("session error reconciliation", () => { + const error = { + name: "APIError", + data: { message: "prompt_cache_breakpoint is not supported on this model", isRetryable: false }, + } + + it("preserves transient errors across stale history replacements", () => { + const user = { ...base, id: "message_1", role: "user" as const } + const transient = { ...assistant("message_2", error), parentID: user.id, sessionErrorID: "event_1" } + + expect(preserveSessionErrors([user, transient], [user])).toEqual([user, transient]) + }) + + it("replaces a transient error with its persisted assistant message", () => { + const transient = { ...assistant("message_2", error), parentID: "message_1", sessionErrorID: "event_1" } + const persisted = { ...assistant("message_3", error), parentID: "message_1" } + + expect(preserveSessionErrors([transient], [persisted])).toEqual([persisted]) + expect(withoutResolvedSessionErrors([transient], [persisted])).toEqual([]) + }) + + it("deduplicates repeated delivery of the same session error event", () => { + const first = { ...assistant("message_2", error), parentID: "message_1", sessionErrorID: "event_1" } + const repeat = { ...assistant("message_3", error), parentID: "message_1", sessionErrorID: "event_1" } + + expect(withoutResolvedSessionErrors([first], [repeat])).toEqual([]) + }) +}) diff --git a/packages/kilo-vscode/webview-ui/src/context/session-errors.ts b/packages/kilo-vscode/webview-ui/src/context/session-errors.ts index a5fd388726f..7d7f56aa072 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session-errors.ts +++ b/packages/kilo-vscode/webview-ui/src/context/session-errors.ts @@ -3,6 +3,27 @@ import type { Message } from "../types/messages" type Entry = { id: string; error?: Message["error"] } type Error = NonNullable +function sameError(a: Message, b: Message) { + if (!a.error || !b.error || a.error.name !== b.error.name) return false + if (a.parentID !== b.parentID) return false + return JSON.stringify(a.error.data) === JSON.stringify(b.error.data) +} + +export function withoutResolvedSessionErrors(current: Message[], incoming: Message[]) { + const events = new Set(incoming.map((msg) => msg.sessionErrorID).filter((id): id is string => !!id)) + return current.filter((msg) => { + if (!msg.sessionErrorID) return true + if (events.has(msg.sessionErrorID)) return false + return !incoming.some((next) => !next.sessionErrorID && sameError(msg, next)) + }) +} + +export function preserveSessionErrors(current: Message[], incoming: Message[]) { + const ids = new Set(incoming.map((msg) => msg.id)) + const errors = withoutResolvedSessionErrors(current, incoming).filter((msg) => msg.sessionErrorID && !ids.has(msg.id)) + return [...incoming, ...errors] +} + export function errorIDs(messages: Entry[]) { return messages.filter((msg) => !!msg.error).map((msg) => msg.id) } diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx index 8831d7edfb0..42fe0c86cd9 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -70,7 +70,7 @@ import { Identifier } from "../utils/id" import { resolveModelSelection } from "./model-selection" import { getAgentModel } from "./session-model-store" import { resolveMessagePrefs } from "./session-preferences" -import { errorIDs } from "./session-errors" +import { errorIDs, preserveSessionErrors, withoutResolvedSessionErrors } from "./session-errors" import { PartStash } from "./part-stash" import { mergeParts, sameParts } from "./session-parts" import { state as todoState } from "./todo-revert" @@ -1175,7 +1175,7 @@ export const SessionProvider: ParentComponent = (props) => { break case "sessionError": { - if (message.error?.name === "MessageAbortedError") break + if (!message.error || message.error.name === "MessageAbortedError") break const sid = message.sessionID ?? currentSessionID() if (!sid) break // Find the last user message in this session to use as parentID @@ -1188,6 +1188,7 @@ export const SessionProvider: ParentComponent = (props) => { createdAt: new Date().toISOString(), parentID: parent?.id, error: message.error, + sessionErrorID: message.eventID, } handleMessageCreated(errorMsg) break @@ -1381,20 +1382,17 @@ export const SessionProvider: ParentComponent = (props) => { } function mergeMessages(current: Message[], incoming: Message[], mode: Exclude) { + const kept = withoutResolvedSessionErrors(current, incoming) if (mode === "reconcile") { - // Tail reconcile: incoming is the authoritative newest-N snapshot. - // Local state may already hold some of those IDs and may also hold - // newer optimistic entries created after the fetch was taken. Merge - // by id (server wins on collision) then sort by createdAt so new - // server messages land in the right position and optimistic tail - // entries stay at the end. + // Merge the authoritative newest-N snapshot by ID, then sort so newer + // optimistic entries stay at the end. const byId = new Map() - for (const msg of current) byId.set(msg.id, msg) + for (const msg of kept) byId.set(msg.id, msg) for (const msg of incoming) byId.set(msg.id, msg) return [...byId.values()].sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()) } const seen = new Set() - const source = mode === "prepend" ? [...incoming, ...current] : incoming + const source = mode === "prepend" ? [...incoming, ...kept] : incoming return source.filter((msg) => { if (seen.has(msg.id)) return false seen.add(msg.id) @@ -1418,12 +1416,13 @@ export const SessionProvider: ParentComponent = (props) => { } function withPending(sessionID: string, messages: Message[]) { - const pending = pendingOptimistic.get(sessionID) - if (!pending || pending.size === 0) return messages - const ids = new Set(messages.map((msg) => msg.id)) const current = store.messages[sessionID] ?? [] + const merged = preserveSessionErrors(current, messages) + const pending = pendingOptimistic.get(sessionID) + if (!pending || pending.size === 0) return merged + const ids = new Set(merged.map((msg) => msg.id)) const orphans = current.filter((msg) => pending.has(msg.id) && !ids.has(msg.id)) - return [...messages, ...orphans] + return [...merged, ...orphans] } // Cheap tail check: same ids in the same order and no visible streamed-part @@ -1601,16 +1600,18 @@ export const SessionProvider: ParentComponent = (props) => { const exists = (store.messages[message.sessionID] ?? []).some((msg) => msg.id === message.id) setStore("messages", message.sessionID, (msgs = []) => { + const current = withoutResolvedSessionErrors(msgs, [message]) + if (message.sessionErrorID && current.some((msg) => msg.sessionErrorID === message.sessionErrorID)) return current // Check if message already exists (optimistic or update case). // Since we now use the same messageID for optimistic and server messages, // this naturally handles the optimistic→real transition. - const idx = msgs.findIndex((m) => m.id === message.id) + const idx = current.findIndex((m) => m.id === message.id) if (idx >= 0) { - const updated = [...msgs] - updated[idx] = { ...msgs[idx], ...message } + const updated = [...current] + updated[idx] = { ...current[idx], ...message } return updated } - return [...msgs, message] + return [...current, message] }) patchPage(message.sessionID, { lastMutation: exists ? "update" : "append" }) diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts index ed8aa570f7f..222b45fee6a 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts @@ -148,6 +148,7 @@ export interface SessionTurnClosedMessage { export interface SessionErrorMessage { type: "sessionError" + eventID: string sessionID?: string error?: { name: string; data?: Record } } diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/sessions.ts b/packages/kilo-vscode/webview-ui/src/types/messages/sessions.ts index 05477b126a8..eec557fe1e7 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/sessions.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/sessions.ts @@ -22,6 +22,7 @@ export interface Message { parentID?: string path?: { cwd: string; root: string } error?: { name: string; data?: Record } + sessionErrorID?: string summary?: { title?: string; body?: string; diffs?: unknown[] } | boolean cost?: number tokens?: TokenUsage From 981600921ae695decac7f80449d765cae67cd83d Mon Sep 17 00:00:00 2001 From: Christiaan Arnoldus Date: Mon, 10 Aug 2026 15:17:17 +0200 Subject: [PATCH 58/73] feat(vscode): improve custom provider dialog layout and model toggles --- ...ustom-provider-edit-screen-improvements.md | 6 + packages/kilo-ui/src/components/dialog.css | 18 +++ .../custom-provider-dialog-validate.test.ts | 16 +++ .../settings/CustomProviderDialog.tsx | 132 ++++++++++++------ .../settings/CustomProviderModelCard.tsx | 72 +++++----- .../kilo-vscode/webview-ui/src/i18n/ar.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/br.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/bs.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/da.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/de.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/en.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/es.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/fa.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/fr.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/it.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/ja.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/ko.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/nl.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/no.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/pl.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/ru.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/th.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/tr.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/uk.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/zh.ts | 2 + .../kilo-vscode/webview-ui/src/i18n/zht.ts | 2 + 26 files changed, 207 insertions(+), 79 deletions(-) create mode 100644 .changeset/custom-provider-edit-screen-improvements.md diff --git a/.changeset/custom-provider-edit-screen-improvements.md b/.changeset/custom-provider-edit-screen-improvements.md new file mode 100644 index 00000000000..b4b383ff25f --- /dev/null +++ b/.changeset/custom-provider-edit-screen-improvements.md @@ -0,0 +1,6 @@ +--- +"kilo-code": patch +"@kilocode/kilo-ui": patch +--- + +Improve custom provider edit dialog layout, make advanced configuration action prominent, and add bulk toggle buttons for reasoning and image modalities across all models. diff --git a/packages/kilo-ui/src/components/dialog.css b/packages/kilo-ui/src/components/dialog.css index d80f2b0d117..fd9018f9dea 100644 --- a/packages/kilo-ui/src/components/dialog.css +++ b/packages/kilo-ui/src/components/dialog.css @@ -12,6 +12,24 @@ border: 1px solid var(--border-weak-base); } + &[data-size="large"] [data-slot="dialog-container"] { + width: min(calc(100vw - 32px), 800px); + height: min(calc(100vh - 32px), 720px); + } + + &[data-size="large"] [data-slot="dialog-content"] { + max-width: 800px; + } + + &[data-size="x-large"] [data-slot="dialog-container"] { + width: min(calc(100vw - 32px), 980px); + height: min(calc(100vh - 32px), 800px); + } + + &[data-size="x-large"] [data-slot="dialog-content"] { + max-width: 980px; + } + [data-slot="dialog-header"] { font-size: var(--kilo-font-size-14); } diff --git a/packages/kilo-vscode/tests/unit/custom-provider-dialog-validate.test.ts b/packages/kilo-vscode/tests/unit/custom-provider-dialog-validate.test.ts index 814b2147e0b..788fa6615ac 100644 --- a/packages/kilo-vscode/tests/unit/custom-provider-dialog-validate.test.ts +++ b/packages/kilo-vscode/tests/unit/custom-provider-dialog-validate.test.ts @@ -292,4 +292,20 @@ describe("validateCustomProvider – variant name validation", () => { const saved = out.result!.config.models["model-1"] as Record expect(saved.modalities).toEqual({ input: ["text", "audio", "video", "pdf"], output: ["text", "audio"] }) }) + + it("handles multiple models with reasoning and images toggled", () => { + const form = base() + form.models = [ + { id: "m1", name: "Model 1", reasoning: true, supportsImages: true, modalities: {}, variants: [] }, + { id: "m2", name: "Model 2", reasoning: true, supportsImages: false, modalities: {}, variants: [] }, + ] + const out = validateCustomProvider(args(form)) + expect(out.result).toBeDefined() + const m1 = out.result!.config.models["m1"] as Record + const m2 = out.result!.config.models["m2"] as Record + expect(m1.reasoning).toBe(true) + expect(m1.modalities).toEqual({ input: ["text", "image"] }) + expect(m2.reasoning).toBe(true) + expect(m2.modalities).toBeUndefined() + }) }) diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderDialog.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderDialog.tsx index c548b06c76d..5d966e9c578 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderDialog.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderDialog.tsx @@ -451,6 +451,18 @@ const CustomProviderDialog = (props: CustomProviderDialogProps) => { setErrors("models", (v) => v.filter((_, i) => i !== index)) } + function toggleAllReasoning() { + const all = form.models.length > 0 && form.models.every((m) => m.reasoning) + const target = !all + form.models.forEach((_, i) => setForm("models", i, "reasoning", target)) + } + + function toggleAllImages() { + const all = form.models.length > 0 && form.models.every((m) => m.supportsImages) + const target = !all + form.models.forEach((_, i) => setForm("models", i, "supportsImages", target)) + } + function addHeader() { setForm("headers", (v) => [...v, { key: "", value: "" }]) setErrors("headers", (v) => [...v, {}]) @@ -515,6 +527,7 @@ const CustomProviderDialog = (props: CustomProviderDialogProps) => { return ( { style={{ display: "flex", "flex-direction": "column", - gap: "24px", - padding: "0 10px 12px 10px", + gap: "20px", + padding: "0 16px 16px 16px", "overflow-y": "auto", - "max-height": "60vh", + flex: 1, + width: "100%", + "box-sizing": "border-box", }} > -
+
{
-
-
- {language.t("provider.custom.description.prefix")} - { - e.preventDefault() - vscode.postMessage({ - type: "openExternal", - url: "https://kilo.ai/docs/ai-providers#custom-provider", - }) - }} - > - {language.t("provider.custom.description.link")} - - {language.t("provider.custom.description.suffix")} + +
+ - @@ -651,19 +665,49 @@ const CustomProviderDialog = (props: CustomProviderDialogProps) => { {/* Models */}
-
- - - - +
+
+ + + + +
+
+ + +
{(m, i) => ( diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderModelCard.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderModelCard.tsx index a33984738c3..49338afe000 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderModelCard.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderModelCard.tsx @@ -98,42 +98,44 @@ export function ModelCard(props: ModelCardProps) { />
- {/* Reasoning toggle */} - + {/* Reasoning and Image toggles */} +
+ - + +
{(error) => ( diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index 4e8295ad13d..4c5496bc988 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -443,6 +443,8 @@ export const dict = { "provider.custom.models.name.placeholder": "الاسم المعروض", "provider.custom.models.reasoning.label": "الاستدلال", "provider.custom.models.modalities.image": "صورة", + "provider.custom.models.toggleReasoning": "تبديل التفكير للكل", + "provider.custom.models.toggleImages": "تبديل الصور للكل", "provider.custom.models.remove": "إزالة النموذج", "provider.custom.models.add": "إضافة نموذج", "provider.custom.models.fetch.authError": "فشلت المصادقة. تحقق من مفتاح API أعلاه وحاول مرة أخرى.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index 8e825534964..f7412b3e7ea 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -454,6 +454,8 @@ export const dict = { "provider.custom.models.name.placeholder": "Nome de Exibição", "provider.custom.models.reasoning.label": "Raciocínio", "provider.custom.models.modalities.image": "Imagem", + "provider.custom.models.toggleReasoning": "Alternar raciocínio para todos", + "provider.custom.models.toggleImages": "Alternar imagem para todos", "provider.custom.models.remove": "Remover modelo", "provider.custom.models.add": "Adicionar modelo", "provider.custom.models.fetch.authError": "Falha na autenticação. Verifique a chave de API acima e tente novamente.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index 36817205996..f372413ef25 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -496,6 +496,8 @@ export const dict = { "provider.custom.models.name.placeholder": "Naziv za prikaz", "provider.custom.models.reasoning.label": "Zaključivanje", "provider.custom.models.modalities.image": "Slika", + "provider.custom.models.toggleReasoning": "Uključi/isključi rezonovanje za sve", + "provider.custom.models.toggleImages": "Uključi/isključi slike za sve", "provider.custom.models.remove": "Ukloni model", "provider.custom.models.add": "Dodaj model", "provider.custom.models.fetch.authError": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index af7a402f188..2c5df219a56 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -494,6 +494,8 @@ export const dict = { "provider.custom.models.name.placeholder": "Visningsnavn", "provider.custom.models.reasoning.label": "Ræsonnement", "provider.custom.models.modalities.image": "Billede", + "provider.custom.models.toggleReasoning": "Slå ræsonnement til/fra for alle", + "provider.custom.models.toggleImages": "Slå billede til/fra for alle", "provider.custom.models.remove": "Fjern model", "provider.custom.models.add": "Tilføj model", "provider.custom.models.fetch.authError": "Godkendelse mislykkedes. Kontrollér API-nøglen ovenfor, og prøv igen.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index 3bd2234ee43..9e83f815f8c 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -504,6 +504,8 @@ export const dict = { "provider.custom.models.name.placeholder": "Anzeigename", "provider.custom.models.reasoning.label": "Schlussfolgerung", "provider.custom.models.modalities.image": "Bild", + "provider.custom.models.toggleReasoning": "Schlussfolgerung für alle umschalten", + "provider.custom.models.toggleImages": "Bild für alle umschalten", "provider.custom.models.remove": "Modell entfernen", "provider.custom.models.add": "Modell hinzufügen", "provider.custom.models.fetch.authError": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index 637305d8f22..cf4a857e1af 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -408,6 +408,8 @@ export const dict = { "provider.custom.models.name.placeholder": "Display Name", "provider.custom.models.reasoning.label": "Reasoning", "provider.custom.models.modalities.image": "Image", + "provider.custom.models.toggleReasoning": "Toggle reasoning for all", + "provider.custom.models.toggleImages": "Toggle image for all", "provider.custom.models.remove": "Remove model", "provider.custom.models.add": "Add model", "provider.custom.models.fetch.authError": "Authentication failed. Check the API key above and try again.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index 60816982138..644800a5221 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -497,6 +497,8 @@ export const dict = { "provider.custom.models.name.placeholder": "Nombre para mostrar", "provider.custom.models.reasoning.label": "Razonamiento", "provider.custom.models.modalities.image": "Imagen", + "provider.custom.models.toggleReasoning": "Alternar razonamiento para todos", + "provider.custom.models.toggleImages": "Alternar imagen para todos", "provider.custom.models.remove": "Eliminar modelo", "provider.custom.models.add": "Añadir modelo", "provider.custom.models.fetch.authError": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fa.ts b/packages/kilo-vscode/webview-ui/src/i18n/fa.ts index 2a6ac366bb9..3b1530349db 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fa.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fa.ts @@ -409,6 +409,8 @@ export const dict = { "provider.custom.models.name.placeholder": "نام نمایشی", "provider.custom.models.reasoning.label": "استدلال", "provider.custom.models.modalities.image": "تصویر", + "provider.custom.models.toggleReasoning": "تغییر وضعیت استدلال برای همه", + "provider.custom.models.toggleImages": "تغییر وضعیت تصویر برای همه", "provider.custom.models.remove": "حذف مدل", "provider.custom.models.add": "افزودن مدل", "provider.custom.models.fetch.authError": "احراز هویت ناموفق بود. کلید API بالا را بررسی کرده و دوباره امتحان کنید.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index e1957ba7500..5ee7c64be6b 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -498,6 +498,8 @@ export const dict = { "provider.custom.models.name.placeholder": "Nom d'affichage", "provider.custom.models.reasoning.label": "Raisonnement", "provider.custom.models.modalities.image": "Image", + "provider.custom.models.toggleReasoning": "Basculer le raisonnement pour tous", + "provider.custom.models.toggleImages": "Basculer l'image pour tous", "provider.custom.models.remove": "Supprimer le modèle", "provider.custom.models.add": "Ajouter un modèle", "provider.custom.models.fetch.authError": "Échec de l'authentification. Vérifiez la clé API ci-dessus et réessayez.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/it.ts b/packages/kilo-vscode/webview-ui/src/i18n/it.ts index 3f804c4b4dd..833f568ff41 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/it.ts @@ -318,6 +318,8 @@ export const dict = { "provider.custom.models.name.placeholder": "Nome visualizzato", "provider.custom.models.reasoning.label": "Reasoning", "provider.custom.models.modalities.image": "Immagine", + "provider.custom.models.toggleReasoning": "Attiva/disattiva reasoning per tutti", + "provider.custom.models.toggleImages": "Attiva/disattiva immagine per tutti", "provider.custom.models.remove": "Rimuovi modello", "provider.custom.models.add": "Aggiungi modello", "provider.custom.models.fetch.authError": "Autenticazione non riuscita. Controlla l'API key sopra e riprova.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index 36b3bb91cb3..511b5034aa8 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -490,6 +490,8 @@ export const dict = { "provider.custom.models.name.placeholder": "表示名", "provider.custom.models.reasoning.label": "推論", "provider.custom.models.modalities.image": "画像", + "provider.custom.models.toggleReasoning": "すべてのモデルの推論を切り替え", + "provider.custom.models.toggleImages": "すべてのモデルの画像を切り替え", "provider.custom.models.remove": "モデルを削除", "provider.custom.models.add": "モデルを追加", "provider.custom.models.fetch.authError": "認証に失敗しました。上記のAPIキーを確認して再試行してください。", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index ed69706294b..aad05fc7ca1 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -451,6 +451,8 @@ export const dict = { "provider.custom.models.name.placeholder": "표시 이름", "provider.custom.models.reasoning.label": "추론", "provider.custom.models.modalities.image": "이미지", + "provider.custom.models.toggleReasoning": "모든 모델의 추론 전환", + "provider.custom.models.toggleImages": "모든 모델의 이미지 전환", "provider.custom.models.remove": "모델 제거", "provider.custom.models.add": "모델 추가", "provider.custom.models.fetch.authError": "인증에 실패했습니다. 위의 API 키를 확인하고 다시 시도하세요.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts index 71ad69b7f7c..5d1a26d21f9 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts @@ -447,6 +447,8 @@ export const dict = { "provider.custom.models.name.placeholder": "Weergavenaam", "provider.custom.models.reasoning.label": "Redeneren", "provider.custom.models.modalities.image": "Afbeelding", + "provider.custom.models.toggleReasoning": "Redeneren voor alle modellen in-/uitschakelen", + "provider.custom.models.toggleImages": "Afbeelding voor alle modellen in-/uitschakelen", "provider.custom.models.remove": "Model verwijderen", "provider.custom.models.add": "Model toevoegen", "provider.custom.models.fetch.authError": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index f2c3e5425e2..eb98954aba1 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -457,6 +457,8 @@ export const dict = { "provider.custom.models.name.placeholder": "Visningsnavn", "provider.custom.models.reasoning.label": "Resonnering", "provider.custom.models.modalities.image": "Bilde", + "provider.custom.models.toggleReasoning": "Veksle resonnering for alle", + "provider.custom.models.toggleImages": "Veksle bilde for alle", "provider.custom.models.remove": "Fjern modell", "provider.custom.models.add": "Legg til modell", "provider.custom.models.fetch.authError": "Autentisering mislyktes. Sjekk API-nøkkelen ovenfor og prøv igjen.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index e2048370847..d711cff1355 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -452,6 +452,8 @@ export const dict = { "provider.custom.models.name.placeholder": "Nazwa wyświetlana", "provider.custom.models.reasoning.label": "Rozumowanie", "provider.custom.models.modalities.image": "Obraz", + "provider.custom.models.toggleReasoning": "Przełącz wnioskowanie dla wszystkich", + "provider.custom.models.toggleImages": "Przełącz obraz dla wszystkich", "provider.custom.models.remove": "Usuń model", "provider.custom.models.add": "Dodaj model", "provider.custom.models.fetch.authError": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index b022005b489..eae378a40f7 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -491,6 +491,8 @@ export const dict = { "provider.custom.models.name.placeholder": "Отображаемое имя", "provider.custom.models.reasoning.label": "Рассуждение", "provider.custom.models.modalities.image": "Изображение", + "provider.custom.models.toggleReasoning": "Переключить рассуждения для всех", + "provider.custom.models.toggleImages": "Переключить изображения для всех", "provider.custom.models.remove": "Удалить модель", "provider.custom.models.add": "Добавить модель", "provider.custom.models.fetch.authError": "Ошибка аутентификации. Проверьте API-ключ выше и попробуйте снова.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index 189ae8e5b5e..4dba9385c68 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -488,6 +488,8 @@ export const dict = { "provider.custom.models.name.placeholder": "ชื่อที่แสดง", "provider.custom.models.reasoning.label": "การใช้เหตุผล", "provider.custom.models.modalities.image": "รูปภาพ", + "provider.custom.models.toggleReasoning": "สลับการให้เหตุผลสำหรับทุกโมเดล", + "provider.custom.models.toggleImages": "สลับรูปภาพสำหรับทุกโมเดล", "provider.custom.models.remove": "ลบโมเดล", "provider.custom.models.add": "เพิ่มโมเดล", "provider.custom.models.fetch.authError": "การยืนยันตัวตนล้มเหลว ตรวจสอบคีย์ API ด้านบนแล้วลองอีกครั้ง", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts index 7c569cf1db9..e8767e36004 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts @@ -442,6 +442,8 @@ export const dict = { "provider.custom.models.name.placeholder": "Görünen Ad", "provider.custom.models.reasoning.label": "Akıl Yürütme", "provider.custom.models.modalities.image": "Görüntü", + "provider.custom.models.toggleReasoning": "Tüm modeller için akıl yürütmeyi aç/kapat", + "provider.custom.models.toggleImages": "Tüm modeller için görüntüyü aç/kapat", "provider.custom.models.remove": "Modeli kaldır", "provider.custom.models.add": "Model ekle", "provider.custom.models.fetch.authError": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts index ea30dd59d27..84672e9f904 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts @@ -446,6 +446,8 @@ export const dict = { "provider.custom.models.name.placeholder": "Відображувана назва", "provider.custom.models.reasoning.label": "Міркування", "provider.custom.models.modalities.image": "Зображення", + "provider.custom.models.toggleReasoning": "Перемкнути міркування для всіх", + "provider.custom.models.toggleImages": "Перемкнути зображення для всіх", "provider.custom.models.remove": "Видалити модель", "provider.custom.models.add": "Додати модель", "provider.custom.models.fetch.authError": "Автентифікація не вдалася. Перевірте API-ключ вище і спробуйте ще раз.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index 11bbdef8350..28fcf881cd9 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -473,6 +473,8 @@ export const dict = { "provider.custom.models.name.placeholder": "显示名称", "provider.custom.models.reasoning.label": "推理", "provider.custom.models.modalities.image": "图片", + "provider.custom.models.toggleReasoning": "切换所有模型的推理", + "provider.custom.models.toggleImages": "切换所有模型的图像", "provider.custom.models.remove": "移除模型", "provider.custom.models.add": "添加模型", "provider.custom.models.fetch.authError": "认证失败。请检查上方的 API 密钥后重试。", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index d679c55c7fe..e6842c06da4 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -433,6 +433,8 @@ export const dict = { "provider.custom.models.name.placeholder": "顯示名稱", "provider.custom.models.reasoning.label": "推理", "provider.custom.models.modalities.image": "圖片", + "provider.custom.models.toggleReasoning": "切換所有模型的推理", + "provider.custom.models.toggleImages": "切換所有模型的圖像", "provider.custom.models.remove": "移除模型", "provider.custom.models.add": "新增模型", "provider.custom.models.fetch.authError": "驗證失敗。請檢查上方的 API 金鑰後重試。", From 1b9fedbbe6b16aabcf8cadb0b900ba261e7c65b6 Mon Sep 17 00:00:00 2001 From: Christiaan Arnoldus Date: Mon, 10 Aug 2026 15:17:54 +0200 Subject: [PATCH 59/73] fix(vscode): stabilize session error deduplication --- packages/kilo-vscode/webview-ui/src/context/session.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx index 42fe0c86cd9..0451e708583 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -1600,8 +1600,8 @@ export const SessionProvider: ParentComponent = (props) => { const exists = (store.messages[message.sessionID] ?? []).some((msg) => msg.id === message.id) setStore("messages", message.sessionID, (msgs = []) => { + if (message.sessionErrorID && msgs.some((msg) => msg.sessionErrorID === message.sessionErrorID)) return msgs const current = withoutResolvedSessionErrors(msgs, [message]) - if (message.sessionErrorID && current.some((msg) => msg.sessionErrorID === message.sessionErrorID)) return current // Check if message already exists (optimistic or update case). // Since we now use the same messageID for optimistic and server messages, // this naturally handles the optimistic→real transition. From 3a9f5a69e78111b59b1a6066f3761260c06a917d Mon Sep 17 00:00:00 2001 From: Christiaan Arnoldus Date: Mon, 10 Aug 2026 15:23:48 +0200 Subject: [PATCH 60/73] test(vscode): add storybook stories for custom provider dialog --- .../src/stories/settings.stories.tsx | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/packages/kilo-vscode/webview-ui/src/stories/settings.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/settings.stories.tsx index 7b510d6a644..34738b5943f 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/settings.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/settings.stories.tsx @@ -17,6 +17,8 @@ import ModeEditView from "../components/settings/ModeEditView" import McpEditView from "../components/settings/McpEditView" import type { AgentConfig, CommandConfig, Config } from "../types/messages" import IndexingTab from "../components/settings/IndexingTab" +import CustomProviderDialog from "../components/settings/CustomProviderDialog" +import { useDialog } from "@kilocode/kilo-ui/context/dialog" import { SidebarEmptyState } from "../components/chat/SidebarEmptyState" import { WorkStyleContext, type WorkStyleContextValue } from "../context/work-style" @@ -695,3 +697,44 @@ export const IndexingKiloCatalogLoading: Story = { ) }, } + +function CustomProviderDialogMount(props: { existing?: Parameters[0]["existing"] }) { + const dialog = useDialog() + onMount(() => dialog.show(() => )) + return null +} + +export const CustomProviderCreateDialog: Story = { + name: "Custom Provider — create dialog", + render: () => ( + + + + ), +} + +export const CustomProviderEditDialog: Story = { + name: "Custom Provider — edit dialog", + render: () => ( + + + + ), +} From 97d0c97626b49a34f9ef7e67d49e63831dce5316 Mon Sep 17 00:00:00 2001 From: "kilo-maintainer[bot]" Date: Mon, 10 Aug 2026 13:27:08 +0000 Subject: [PATCH 61/73] chore: update kilo-vscode visual regression baselines --- .../settings/custom-provider-create-dialog-chromium-linux.png | 3 +++ .../settings/custom-provider-edit-dialog-chromium-linux.png | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/custom-provider-create-dialog-chromium-linux.png create mode 100644 packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/custom-provider-edit-dialog-chromium-linux.png diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/custom-provider-create-dialog-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/custom-provider-create-dialog-chromium-linux.png new file mode 100644 index 00000000000..038643cd74a --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/custom-provider-create-dialog-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72a0a206e978250d41b42f3c0585691a78cbe2635c12d320bcefe61cd938ced5 +size 280 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/custom-provider-edit-dialog-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/custom-provider-edit-dialog-chromium-linux.png new file mode 100644 index 00000000000..038643cd74a --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/custom-provider-edit-dialog-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72a0a206e978250d41b42f3c0585691a78cbe2635c12d320bcefe61cd938ced5 +size 280 From d074e0eb4b7ebea1a6c4473cf2d732b184bbcf37 Mon Sep 17 00:00:00 2001 From: cmanu Date: Mon, 10 Aug 2026 08:11:27 -0700 Subject: [PATCH 62/73] fix(pr-view): Update mergeMessages to main --- .../kilo-vscode/webview-ui/src/context/session-merge.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/context/session-merge.ts b/packages/kilo-vscode/webview-ui/src/context/session-merge.ts index ac9264cf238..2a59bf0d0ad 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session-merge.ts +++ b/packages/kilo-vscode/webview-ui/src/context/session-merge.ts @@ -1,7 +1,9 @@ import type { Message, MessageLoadMode, Part } from "../types/messages" import { sameParts } from "./session-parts" +import { withoutResolvedSessionErrors } from "./session-errors" export function mergeMessages(current: Message[], incoming: Message[], mode: Exclude) { + const kept = withoutResolvedSessionErrors(current, incoming) if (mode === "reconcile") { // Tail reconcile: incoming is the authoritative newest-N snapshot. // Local state may already hold some of those IDs and may also hold @@ -10,12 +12,12 @@ export function mergeMessages(current: Message[], incoming: Message[], mode: Exc // server messages land in the right position and optimistic tail // entries stay at the end. const byId = new Map() - for (const msg of current) byId.set(msg.id, msg) + for (const msg of kept) byId.set(msg.id, msg) for (const msg of incoming) byId.set(msg.id, msg) return [...byId.values()].sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()) } const seen = new Set() - const source = mode === "prepend" ? [...incoming, ...current] : incoming + const source = mode === "prepend" ? [...incoming, ...kept] : incoming return source.filter((msg) => { if (seen.has(msg.id)) return false seen.add(msg.id) From cc397bb885c78426889e8aa781e3705bad7bc12a Mon Sep 17 00:00:00 2001 From: cmanu Date: Mon, 10 Aug 2026 09:20:41 -0700 Subject: [PATCH 63/73] feat(pr-view): new icons in kilo-ui for circle x, pull request, and refresh --- packages/kilo-ui/src/components/icon.tsx | 12 ++++++++++++ packages/ui/src/components/icon.tsx | 3 --- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/kilo-ui/src/components/icon.tsx b/packages/kilo-ui/src/components/icon.tsx index 96e8621531e..f58623ade7d 100644 --- a/packages/kilo-ui/src/components/icon.tsx +++ b/packages/kilo-ui/src/components/icon.tsx @@ -2,6 +2,18 @@ import { Icon as Upstream, type IconProps as Props } from "@opencode-ai/ui/icon" import { splitProps } from "solid-js" const icons: Record = { + "circle-x-outline": { + viewBox: "0 0 20 20", + path: ``, + }, + "pull-request": { + viewBox: "0 0 20 20", + path: ``, + }, + refresh: { + viewBox: "0 0 20 20", + path: ``, + }, memory: { viewBox: "0 0 24 24", path: ``, diff --git a/packages/ui/src/components/icon.tsx b/packages/ui/src/components/icon.tsx index 8bd13c7599f..95180b8d65b 100644 --- a/packages/ui/src/components/icon.tsx +++ b/packages/ui/src/components/icon.tsx @@ -18,7 +18,6 @@ const icons = { "chevron-grabber-vertical": ``, "chevron-double-right": ``, "circle-x": ``, - "circle-x-outline": ``, // kilocode_change close: ``, "close-small": ``, checklist: ``, @@ -91,7 +90,6 @@ const icons = { download: ``, menu: ``, server: ``, - "pull-request": ``, // kilocode_change branch: ``, edit: ``, history: ``, @@ -108,7 +106,6 @@ const icons = { "star-filled": ``, warning: ``, reset: ``, - refresh: ``, // kilocode_change link: ``, providers: ``, layers: ``, From cf490b9eadad6ca2564e96689c9dcade1ae7e241 Mon Sep 17 00:00:00 2001 From: cmanu Date: Mon, 10 Aug 2026 09:52:28 -0700 Subject: [PATCH 64/73] fix(pr-view): Update hash for pr status poller to include reviewers state change --- packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts index b66c42ba434..4b1098777cd 100644 --- a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts +++ b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts @@ -272,7 +272,8 @@ export class PRStatusPoller { files: pr.files, } - const hash = `${worktreeId}:${pr.number}:${pr.state}:${pr.review}:${checks.status}:${checks.passed}/${checks.total}:${reviewers.length}:${comments?.total ?? ""}:${comments?.unresolved ?? ""}` + const reviewersSig = reviewers.map((r) => `${r.login}:${r.state}`).join(",") + const hash = `${worktreeId}:${pr.number}:${pr.state}:${pr.review}:${checks.status}:${checks.passed}/${checks.total}:${reviewersSig}:${pr.body?.length ?? 0}:${comments?.total ?? ""}:${comments?.unresolved ?? ""}` if (this.lastHash.get(worktreeId) === hash) return this.lastHash.set(worktreeId, hash) From 95c361e2e25897a602dad804f2cd499106574cec Mon Sep 17 00:00:00 2001 From: cmanu Date: Mon, 10 Aug 2026 09:54:58 -0700 Subject: [PATCH 65/73] fix(pr-view): clear error on successful status received --- packages/kilo-vscode/src/agent-manager/pr-status-bridge.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/kilo-vscode/src/agent-manager/pr-status-bridge.ts b/packages/kilo-vscode/src/agent-manager/pr-status-bridge.ts index af496281966..42370a5e721 100644 --- a/packages/kilo-vscode/src/agent-manager/pr-status-bridge.ts +++ b/packages/kilo-vscode/src/agent-manager/pr-status-bridge.ts @@ -133,6 +133,7 @@ function bridgePollerOpts(bridge: PRStatusBridge, host: PRBridgeHost) { } const msg = { type: "agentManager.prStatus", worktreeId: id, pr, error: err } as AgentManagerOutMessage bridge["cache"].set(id, msg) + bridge["lastErrorNotified"] = undefined host.postToWebview(msg) host.updateWorktreePR(id, pr?.number, pr?.url, pr?.state) }, From 1b2ab46479f71a3e4375b763f57d8abe7b48126c Mon Sep 17 00:00:00 2001 From: cmanu Date: Mon, 10 Aug 2026 10:00:48 -0700 Subject: [PATCH 66/73] fix(pr-view): Adjust link out pattern to use vs code post message --- .../webview-ui/agent-manager/pr/PRChecks.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRChecks.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRChecks.tsx index b010d221f71..57be374882f 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRChecks.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRChecks.tsx @@ -1,9 +1,11 @@ /** @jsxImportSource solid-js */ import { For, Show, createSignal } from "solid-js" import { Icon } from "@kilocode/kilo-ui/icon" +import { Tooltip } from "@kilocode/kilo-ui/tooltip" import type { PRStatus } from "../../src/types/messages" import type { PRCheck, CheckStatus } from "./pr-types" import { SectionHeading } from "./SectionHeading" +import { useVSCode } from "../../src/context/vscode" const CHECK: Record = { success: { icon: "circle-check", label: "Passed" }, @@ -14,6 +16,7 @@ const CHECK: Record = { } export function PRChecks(props: { checks: PRStatus["checks"] }) { + const vscode = useVSCode() const [open, setOpen] = createSignal(true) return ( <> @@ -38,9 +41,15 @@ export function PRChecks(props: { checks: PRStatus["checks"] }) { {check.duration} - e.preventDefault()}> - - + + +
)} From a1daaadd182c4c784f362efc505dfad54f81865a Mon Sep 17 00:00:00 2001 From: cmanu Date: Mon, 10 Aug 2026 10:04:50 -0700 Subject: [PATCH 67/73] feat(pr-view): remove excess markedprovider on pr --- packages/kilo-vscode/webview-ui/agent-manager/pr/PRPanel.tsx | 5 +---- .../kilo-vscode/webview-ui/src/context/provider-shell.tsx | 2 ++ 2 files changed, 3 insertions(+), 4 deletions(-) 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 c0241f05120..8c3906262e0 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRPanel.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRPanel.tsx @@ -2,7 +2,6 @@ import { Component, Show } from "solid-js" import { IconButton } from "@kilocode/kilo-ui/icon-button" import { Tooltip } from "@kilocode/kilo-ui/tooltip" -import { MarkedProvider } from "@kilocode/kilo-ui/context/marked" import type { WorktreeState } from "../../src/types/messages" import type { PRStatus } from "../../src/types/messages" import { PRBadge } from "./PRBadge" @@ -23,8 +22,7 @@ interface PRPanelProps { export const PRPanel: Component = (props) => { return ( - -
+
@@ -61,6 +59,5 @@ export const PRPanel: Component = (props) => {
- ) } diff --git a/packages/kilo-vscode/webview-ui/src/context/provider-shell.tsx b/packages/kilo-vscode/webview-ui/src/context/provider-shell.tsx index 48ecd641c7c..d457f5e4f00 100644 --- a/packages/kilo-vscode/webview-ui/src/context/provider-shell.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/provider-shell.tsx @@ -52,6 +52,8 @@ const Root: ParentComponent = (props) => ( + {/* MarkedProvider is required here for all markdown consumers in the tree, + including PRPanel's PRDescription and PRComments components. Do not remove. */} From 7bf1e546eb0bfe1bb03ba2028dea08ccb8373e98 Mon Sep 17 00:00:00 2001 From: cmanu Date: Mon, 10 Aug 2026 10:09:53 -0700 Subject: [PATCH 68/73] feat(pr-view): close review tab when pr panel opens --- .../kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index 2dad2ea0209..622f0fe96f5 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -567,7 +567,7 @@ const AgentManagerContent: Component = () => { const togglePRPanel = () => { setHistory(false) - setReviewActive(false) + if (reviewActive()) closeReviewTab() setSidePanel((prev) => (prev === SidePanel.PR ? null : SidePanel.PR)) } From 1afd4e7c2a436b61e8dbbab223078d28cead5d2a Mon Sep 17 00:00:00 2001 From: cmanu Date: Mon, 10 Aug 2026 10:15:10 -0700 Subject: [PATCH 69/73] format(pr-view): prettier --- .../webview-ui/agent-manager/pr/PRPanel.tsx | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) 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 8c3906262e0..ec37c45940a 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/pr/PRPanel.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRPanel.tsx @@ -23,41 +23,41 @@ interface PRPanelProps { export const PRPanel: Component = (props) => { return (
-
-
- - {props.pr.title} - #{props.pr.number} -
-
- - - - - - -
+
+
+ + {props.pr.title} + #{props.pr.number}
-
- - - 0}> - - - {(body) => } - 0}> - - - - {(comments) => } - +
+ + + + + +
+
+ + + 0}> + + + {(body) => } + 0}> + + + + {(comments) => } + +
+
) } From b87f4da23943194ed755ac463ff34579e1f39daa Mon Sep 17 00:00:00 2001 From: cmanu Date: Mon, 10 Aug 2026 10:16:44 -0700 Subject: [PATCH 70/73] feat(pr-view): add title to pr hash --- packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts index 4b1098777cd..d71b1200e25 100644 --- a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts +++ b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts @@ -273,7 +273,7 @@ export class PRStatusPoller { } const reviewersSig = reviewers.map((r) => `${r.login}:${r.state}`).join(",") - const hash = `${worktreeId}:${pr.number}:${pr.state}:${pr.review}:${checks.status}:${checks.passed}/${checks.total}:${reviewersSig}:${pr.body?.length ?? 0}:${comments?.total ?? ""}:${comments?.unresolved ?? ""}` + const hash = `${worktreeId}:${pr.number}:${pr.title}:${pr.state}:${pr.review}:${checks.status}:${checks.passed}/${checks.total}:${reviewersSig}:${pr.body ?? ""}:${comments?.total ?? ""}:${comments?.unresolved ?? ""}` if (this.lastHash.get(worktreeId) === hash) return this.lastHash.set(worktreeId, hash) From e62e92d368747e6b878c8a628eabcb8e5ed8ddea Mon Sep 17 00:00:00 2001 From: cmanu Date: Mon, 10 Aug 2026 10:19:03 -0700 Subject: [PATCH 71/73] fix(pr-view): fetch the most recent 20 reviews --- packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts index d71b1200e25..743c85a4c4b 100644 --- a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts +++ b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts @@ -443,7 +443,7 @@ export class PRStatusPoller { reviewRequests(first: 20) { nodes { requestedReviewer { ... on User { login avatarUrl } } } } - reviews(first: 20, states: [APPROVED, CHANGES_REQUESTED, COMMENTED]) { + reviews(last: 20, states: [APPROVED, CHANGES_REQUESTED, COMMENTED]) { nodes { author { login avatarUrl } state } } } From e532519947bb87e1b753212442900c29895076ad Mon Sep 17 00:00:00 2001 From: cmanu Date: Mon, 10 Aug 2026 10:24:14 -0700 Subject: [PATCH 72/73] fix(pr-view): error handling for pr reviewer fetch --- packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts index 743c85a4c4b..d94a4511fc2 100644 --- a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts +++ b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts @@ -469,7 +469,8 @@ export class PRStatusPoller { (pr?.reviewRequests?.nodes ?? []) as GhReviewRequest[], (pr?.reviews?.nodes ?? []) as GhReview[], ) - } catch { + } catch (err) { + this.options.log("Failed to fetch PR reviewers:", err) return [] } } From dcf9c5a0be9bdd4ccfeeea531ceafb07c74d0286 Mon Sep 17 00:00:00 2001 From: dev_Hakaze Date: Tue, 11 Aug 2026 00:45:45 +0700 Subject: [PATCH 73/73] fix(config): expand MCP header env refs without wiping MCP set (#12554) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(config): expand MCP header env refs without wiping MCP set Project kilo.jsonc with {env:…} in MCP headers caused the whole config load to fail since 7.4.5, silently removing every MCP. Leave {env:} literal during untrusted text substitution, expand only remote MCP headers after parse, and drop individual MCPs with blocked env refs instead of the full set. Fixes #12319 * fix(config): ban {file:} during MCP header env expansion Post-parse header expand used trusted substitute without fileScope, so a decoy in-repo {file:} plus any {env:} could read arbitrary paths into remote MCP headers. Expand only {env:} and reject residual {file:}. * fix(config): ban env secret expansion in project MCP headers Untrusted project MCP headers must not read process.env or authEnv. Reject {env:} and {file:} per entry with a warning so sibling MCPs still load. * fix(config): drop global MCP headers on project URL retarget When a project config overrides a same-named global remote MCP with a new URL and does not restate headers, do not inherit expanded secret headers from the global entry. Also fix MCP union narrowing in tests and stop saying "header env expansion failed" when only validating. * fix(config): do not mutate caller's patch in mergeConfig Shallow-copy patch before delete p.mcp so probe-then-write updateProjectConfig paths keep mcp on the original object. * fix(config): secure project MCP header loading * Update packages/opencode/src/config/config.ts Co-authored-by: kilo-code-bot[bot] <240665456+kilo-code-bot[bot]@users.noreply.github.com> * fix(config): secure layered MCP transitions --------- Co-authored-by: arimu1 <19286898+arimu1@users.noreply.github.com> Co-authored-by: Johnny Eric Amancio Co-authored-by: kilo-code-bot[bot] <240665456+kilo-code-bot[bot]@users.noreply.github.com> --- .changeset/fix-mcp-env-header-expansion.md | 5 + packages/opencode/src/config/config.ts | 39 ++- .../opencode/src/kilocode/config/config.ts | 71 +++- .../src/kilocode/config/mcp-headers.ts | 33 ++ .../opencode/src/kilocode/config/overlay.ts | 15 +- .../test/kilocode/config/config.test.ts | 222 +++++++++++++ .../test/kilocode/config/mcp-headers.test.ts | 303 ++++++++++++++++++ 7 files changed, 672 insertions(+), 16 deletions(-) create mode 100644 .changeset/fix-mcp-env-header-expansion.md create mode 100644 packages/opencode/src/kilocode/config/mcp-headers.ts create mode 100644 packages/opencode/test/kilocode/config/mcp-headers.test.ts diff --git a/.changeset/fix-mcp-env-header-expansion.md b/.changeset/fix-mcp-env-header-expansion.md new file mode 100644 index 00000000000..051924f1dfb --- /dev/null +++ b/.changeset/fix-mcp-env-header-expansion.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Prevent project MCP configs from resolving variable-backed headers or inheriting trusted headers when changing endpoints, while preserving unaffected servers. diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 0d5e87b9328..ad043e3826c 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -42,6 +42,7 @@ import z from "zod" // kilocode_change - Kilo config compatibility schemas // kilocode_change start import { ZodOverride } from "@opencode-ai/core/effect-zod" import { KilocodeConfig } from "../kilocode/config/config" +import { sanitizeProjectMcpHeaders } from "../kilocode/config/mcp-headers" import { primaryPaths } from "../kilocode/primary-worktree" import { Git } from "@/git" import { KilocodeDefaultPlugins } from "@/kilocode/config/default-plugins" @@ -66,8 +67,9 @@ function mergeConfig(target: Info, source: Info): Info { return mergeDeep(target, source) as Info } -function mergeConfigConcatArrays(target: Info, source: Info): Info { - const merged = mergeConfig(target, source) +function mergeConfigConcatArrays(target: Info, source: Info, trusted = true): Info { + // kilocode_change + const merged = trusted ? mergeConfig(target, source) : KilocodeConfig.mergeProject(target, source) if (target.instructions && source.instructions) { merged.instructions = Array.from(new Set([...target.instructions, ...source.instructions])) } @@ -310,7 +312,7 @@ const layer = Layer.effect( const loadConfig = Effect.fnUntraced(function* ( text: string, - options: { path: string } | { dir: string; source: string }, + options: { path: string; original?: string } | { dir: string; source: string }, // kilocode_change env?: Record, // kilocode_change start - trusted allows {env:}; fileScope confines untrusted {file:} reads to a root trusted?: boolean, @@ -333,12 +335,13 @@ const layer = Layer.effect( if (!data.$schema) { // kilocode_change start data.$schema = "https://app.kilo.ai/config.json" - const edits = modify(text, ["$schema"], "https://app.kilo.ai/config.json", { + const original = options.original ?? text + const edits = modify(original, ["$schema"], "https://app.kilo.ai/config.json", { formattingOptions: { insertSpaces: true, tabSize: 2 }, getInsertionIndex: () => 0, }) - const updated = applyEdits(text, edits) - if (updated !== text) { + const updated = applyEdits(original, edits) + if (updated !== original) { yield* fs.writeFileString(options.path, updated).pipe(Effect.catch(() => Effect.void)) } // kilocode_change end @@ -351,11 +354,25 @@ const layer = Layer.effect( env?: Record, trusted?: boolean, // kilocode_change fileScope?: ConfigVariable.FileScope, // kilocode_change + configWarnings?: Warning[], // kilocode_change - collect MCP header expansion warnings ) { yield* Effect.logInfo("loading", { path: filepath }) const text = yield* readConfigFile(filepath) if (!text) return {} as Info - return yield* loadConfig(text, { path: filepath }, env, trusted, fileScope) // kilocode_change + // kilocode_change start - remove variable-bearing project MCP headers before generic substitution can read them + const sanitized = + trusted === false ? sanitizeProjectMcpHeaders(ConfigParse.jsonc(text, filepath), filepath) : undefined + const content = sanitized ? (JSON.stringify(sanitized.config) ?? text) : text + if (sanitized && configWarnings) configWarnings.push(...sanitized.warnings) + const data = yield* loadConfig( + content, + { path: filepath, original: text }, + trusted === false ? undefined : env, + trusted, + fileScope, + ) + // kilocode_change end + return data }) let globalStamp = "" // kilocode_change @@ -550,7 +567,7 @@ const layer = Layer.effect( const scope = kind ?? (yield* pluginScopeForSource(source)) const trusted = sourceTrusted ?? scope === "global" const scoped = KilocodeConfig.scopeIndexing(SandboxConfig.scope(next, scope), scope) - result = mergeConfigConcatArrays(result, scoped) + result = mergeConfigConcatArrays(result, scoped, trusted) // kilocode_change if (scoped.agent) configuredAgents = mergeDeep(configuredAgents, scoped.agent) if (next.instructions?.length) { result.instruction_origins = origins(result.instruction_origins, next.instructions, trusted, source) @@ -670,8 +687,8 @@ const layer = Layer.effect( for (const file of yield* ConfigPaths.files(name, ctx.directory, ctx.worktree).pipe(Effect.orDie)) { yield* merge( file, - // kilocode_change - project config is untrusted: {env:} rejected, {file:} confined to projectRoot - yield* loadFile(file, authEnv, false, { root: projectRoot, source: file }).pipe( + // kilocode_change - project config is untrusted: {env:} rejected by substitution; MCP entries with variable-bearing headers dropped pre-substitution, {file:} confined to projectRoot + yield* loadFile(file, authEnv, false, { root: projectRoot, source: file }, warnings).pipe( Effect.catchDefect((err: unknown) => { caughtWarning(warnings, file, err) return Effect.succeed({} as Info) @@ -723,7 +740,7 @@ const layer = Layer.effect( const fileScope = dirTrusted ? undefined : { root: projectRoot, source } yield* merge( source, - yield* loadFile(source, authEnv, dirTrusted, fileScope).pipe( + yield* loadFile(source, authEnv, dirTrusted, fileScope, dirTrusted ? undefined : warnings).pipe( // kilocode_change Effect.catchDefect((err: unknown) => { caughtWarning(warnings, source, err) diff --git a/packages/opencode/src/kilocode/config/config.ts b/packages/opencode/src/kilocode/config/config.ts index d47a9a9a2b1..91498443b6f 100644 --- a/packages/opencode/src/kilocode/config/config.ts +++ b/packages/opencode/src/kilocode/config/config.ts @@ -519,13 +519,24 @@ export namespace KilocodeConfig { * 3. Strip null delete sentinels */ export function mergeConfig(existing: Config.Info, patch: Config.Info): Config.Info { + return merge(existing, patch, true) + } + + /** Merge an untrusted project layer without changing generic config merge semantics. */ + export function mergeProject(existing: Config.Info, patch: Config.Info): Config.Info { + return merge(existing, patch, false) + } + + function merge(existing: Config.Info, patch: Config.Info, clean: boolean): Config.Info { const e = { ...existing } as Record - const p = patch as Record + // Shallow-copy patch so MCP extraction (delete p.mcp) never mutates the caller's object. + // Callers may probe with mergeConfig({}, patch) then reuse the same patch for a write. + const p = { ...patch } as Record // Normalize permission scalars before merge const existingPerm = e.permission const patchPerm = p.permission - if (isRecord(existingPerm) && isRecord(patchPerm)) { + if (clean && isRecord(existingPerm) && isRecord(patchPerm)) { const cloned = { ...existingPerm } for (const [key, value] of Object.entries(patchPerm)) { const existing = cloned[key] @@ -536,7 +547,61 @@ export namespace KilocodeConfig { e.permission = cloned } - return stripNulls(mergeDeep(e, p) as Record) as Config.Info + // MCP servers merge by name; project URL retargets must not inherit base headers. + const existingMcp = e.mcp + const patchMcp = p.mcp + if (!isRecord(existingMcp) && !isRecord(patchMcp)) { + return (clean ? stripNulls(mergeDeep(e, p) as Record) : mergeDeep(e, p)) as Config.Info + } + + delete e.mcp + delete p.mcp + const merged = (clean ? stripNulls(mergeDeep(e, p) as Record) : mergeDeep(e, p)) as Config.Info + const baseMcp = isRecord(existingMcp) ? (existingMcp as NonNullable) : undefined + const srcMcp = isRecord(patchMcp) ? (patchMcp as NonNullable) : undefined + if (!srcMcp) { + if (baseMcp) merged.mcp = baseMcp + return merged + } + if (!baseMcp) { + merged.mcp = srcMcp + return merged + } + + const out: NonNullable = { ...baseMcp } + for (const [name, src] of Object.entries(srcMcp)) { + const base = baseMcp[name] + if (!isRecord(src) || !isRecord(base)) { + out[name] = src + continue + } + + const kind = "type" in base && (base.type === "local" || base.type === "remote") ? base.type : undefined + const next = "type" in src && (src.type === "local" || src.type === "remote") ? src.type : undefined + const changed = next !== undefined && next !== kind + const seed = changed + ? { + ...("enabled" in base ? { enabled: base.enabled } : {}), + ...("timeout" in base ? { timeout: base.timeout } : {}), + } + : base + const entry = mergeDeep(seed, src) as (typeof out)[string] + const srcUrl = "url" in src && typeof src.url === "string" ? src.url : undefined + const baseUrl = "url" in base && typeof base.url === "string" ? base.url : undefined + const retargeted = + kind === "remote" && next !== "local" && srcUrl !== undefined && baseUrl !== undefined && srcUrl !== baseUrl + if (!retargeted || !isRecord(entry)) { + out[name] = entry + continue + } + + const { headers: _headers, oauth: _oauth, ...rest } = entry as Record + if ("headers" in src) rest.headers = src.headers + if ("oauth" in src) rest.oauth = src.oauth + out[name] = rest as (typeof out)[string] + } + merged.mcp = out + return merged } // ── Directory check helper ─────────────────────────────────────────── diff --git a/packages/opencode/src/kilocode/config/mcp-headers.ts b/packages/opencode/src/kilocode/config/mcp-headers.ts new file mode 100644 index 00000000000..4fa26621c8b --- /dev/null +++ b/packages/opencode/src/kilocode/config/mcp-headers.ts @@ -0,0 +1,33 @@ +import { isRecord } from "@/util/record" + +export type McpHeaderWarning = { + path: string + message: string +} + +const reference = /\{(?:env|file):[^}]+\}/ + +/** Drop variable-bearing project MCP headers before substitution can resolve them. */ +export function sanitizeProjectMcpHeaders(data: T, source: string): { config: T; warnings: McpHeaderWarning[] } { + if (!isRecord(data) || !isRecord(data.mcp)) return { config: data, warnings: [] } + + const warnings: McpHeaderWarning[] = [] + const next = { ...data.mcp } + + for (const [name, mcp] of Object.entries(data.mcp)) { + if (!isRecord(mcp) || !isRecord(mcp.headers)) continue + const token = Object.entries(mcp.headers) + .flatMap(([key, value]) => [key, value]) + .find((value): value is string => typeof value === "string" && reference.test(value)) + ?.match(reference)?.[0] + if (!token) continue + + delete next[name] + warnings.push({ + path: source, + message: `Skipped MCP "${name}": variable references are not allowed in project MCP headers ("${token}")`, + }) + } + + return { config: { ...data, mcp: next } as T, warnings } +} diff --git a/packages/opencode/src/kilocode/config/overlay.ts b/packages/opencode/src/kilocode/config/overlay.ts index fce5d9dee02..566dcfafeb6 100644 --- a/packages/opencode/src/kilocode/config/overlay.ts +++ b/packages/opencode/src/kilocode/config/overlay.ts @@ -14,6 +14,7 @@ import { ConfigVariable } from "@/config/variable" import { Filesystem } from "@/util/filesystem" import { isRecord } from "@/util/record" import { KilocodeConfig } from "./config" +import { sanitizeProjectMcpHeaders } from "./mcp-headers" import { KilocodeConfigSources } from "./sources" export namespace KilocodeConfigOverlay { @@ -268,10 +269,20 @@ export namespace KilocodeConfigOverlay { async function loadUnsafe(file: string, fileScope?: ConfigVariable.FileScope): Promise { // kilocode_change end const text = await Bun.file(file).text() - // kilocode_change - overlay reads project config files: {env:} rejected, {file:} confined to fileScope.root - const expanded = await ConfigVariable.substitute({ text, type: "path", path: file, trusted: false, fileScope }) + // kilocode_change start - remove variable-bearing MCP headers before resolving other project file references + const sanitized = sanitizeProjectMcpHeaders(ConfigParse.jsonc(text, file), file) + const content = JSON.stringify(sanitized.config) ?? text + const expanded = await ConfigVariable.substitute({ + text: content, + type: "path", + path: file, + trusted: false, + fileScope, + }) const parsed = ConfigParse.jsonc(expanded, file) if (!isRecord(parsed)) return {} + for (const warning of sanitized.warnings) log.warn(warning.message, { path: warning.path }) + // kilocode_change end return ConfigParse.schema(Config.Info, parsed, file) as Config.Info } diff --git a/packages/opencode/test/kilocode/config/config.test.ts b/packages/opencode/test/kilocode/config/config.test.ts index 2b2d7eeba0d..6bfe7b6d74e 100644 --- a/packages/opencode/test/kilocode/config/config.test.ts +++ b/packages/opencode/test/kilocode/config/config.test.ts @@ -188,6 +188,228 @@ describe("global config updates", () => { }) }) +describe("project MCP trust boundaries", () => { + test("does not inherit global headers when a project changes the remote URL", async () => { + await using globalTmp = await tmpdir() + await using tmp = await tmpdir({ git: true }) + const prev = Global.Path.config + ;(Global.Path as { config: string }).config = globalTmp.path + await clear() + await disposeAllInstances() + + try { + await writeConfig(globalTmp.path, { + $schema: "https://app.kilo.ai/config.json", + mcp: { + plain: { + type: "remote", + url: "https://trusted.example.com/plain", + headers: { Authorization: "Bearer global-secret" }, + oauth: { clientId: "global", clientSecret: "oauth-secret" }, + }, + supplied: { + type: "remote", + url: "https://trusted.example.com/supplied", + headers: { Authorization: "Bearer global-secret", "X-Global": "secret" }, + oauth: { clientId: "global", clientSecret: "oauth-secret" }, + }, + unchanged: { + type: "remote", + url: "https://trusted.example.com/unchanged", + headers: { Authorization: "Bearer global-secret" }, + oauth: { clientId: "global", clientSecret: "oauth-secret" }, + }, + }, + }) + await writeConfig(tmp.path, { + mcp: { + plain: { type: "remote", url: "https://project.example.com/plain" }, + supplied: { + type: "remote", + url: "https://project.example.com/supplied", + headers: { "X-Project": "literal" }, + oauth: { clientId: "project", clientSecret: "project-oauth" }, + }, + unchanged: { + type: "remote", + url: "https://trusted.example.com/unchanged", + enabled: false, + }, + }, + }) + + await provideTestInstance({ + directory: tmp.path, + fn: async () => { + const config = await load() + expect(config.mcp?.plain).toEqual({ type: "remote", url: "https://project.example.com/plain" }) + expect(config.mcp?.supplied).toEqual({ + type: "remote", + url: "https://project.example.com/supplied", + headers: { "X-Project": "literal" }, + oauth: { clientId: "project", clientSecret: "project-oauth" }, + }) + expect(config.mcp?.unchanged).toEqual({ + type: "remote", + url: "https://trusted.example.com/unchanged", + headers: { Authorization: "Bearer global-secret" }, + oauth: { clientId: "global", clientSecret: "oauth-secret" }, + enabled: false, + }) + }, + }) + } finally { + ;(Global.Path as { config: string }).config = prev + await clear() + await disposeAllInstances() + } + }) + + test("drops file-backed project MCP headers before reading them", async () => { + await using globalTmp = await tmpdir() + await using tmp = await tmpdir({ git: true }) + const prev = Global.Path.config + ;(Global.Path as { config: string }).config = globalTmp.path + await clear() + await disposeAllInstances() + + try { + await Filesystem.write(path.join(tmp.path, "secret.txt"), "project secret") + await writeConfig(tmp.path, { + mcp: { + unsafe: { + type: "remote", + url: "https://project.example.com/unsafe", + headers: { Authorization: "Bearer {file:secret.txt}" }, + }, + sibling: { type: "remote", url: "https://project.example.com/sibling" }, + }, + }) + + await provideTestInstance({ + directory: tmp.path, + fn: async () => { + const config = await load() + const warnings = await Effect.runPromise( + Config.Service.use((svc) => svc.warnings()).pipe(Effect.scoped, Effect.provide(layer)), + ) + expect(config.mcp?.unsafe).toBeUndefined() + expect(config.mcp?.sibling).toEqual({ type: "remote", url: "https://project.example.com/sibling" }) + expect(JSON.stringify(config)).not.toContain("project secret") + expect(warnings.some((warning) => warning.message.includes('Skipped MCP "unsafe"'))).toBe(true) + }, + }) + } finally { + ;(Global.Path as { config: string }).config = prev + await clear() + await disposeAllInstances() + } + }) + + test("drops env-backed project MCP headers without dropping static siblings", async () => { + await using globalTmp = await tmpdir() + await using tmp = await tmpdir({ git: true }) + const prev = Global.Path.config + const secret = process.env.KILO_PROJECT_MCP_SECRET + ;(Global.Path as { config: string }).config = globalTmp.path + process.env.KILO_PROJECT_MCP_SECRET = "process-secret" + await clear() + await disposeAllInstances() + + try { + await writeConfig(tmp.path, { + mcp: { + unsafe: { + type: "remote", + url: "https://project.example.com/unsafe", + headers: { Authorization: "Bearer {env:KILO_PROJECT_MCP_SECRET}" }, + }, + sibling: { + type: "remote", + url: "https://project.example.com/sibling", + headers: { "X-Project": "literal" }, + }, + }, + }) + + await provideTestInstance({ + directory: tmp.path, + fn: async () => { + const config = await load() + const warnings = await Effect.runPromise( + Config.Service.use((svc) => svc.warnings()).pipe(Effect.scoped, Effect.provide(layer)), + ) + expect(config.mcp?.unsafe).toBeUndefined() + expect(config.mcp?.sibling).toEqual({ + type: "remote", + url: "https://project.example.com/sibling", + headers: { "X-Project": "literal" }, + }) + expect(JSON.stringify(config)).not.toContain("process-secret") + expect(warnings.some((warning) => warning.message.includes('Skipped MCP "unsafe"'))).toBe(true) + }, + }) + } finally { + if (secret === undefined) delete process.env.KILO_PROJECT_MCP_SECRET + else process.env.KILO_PROJECT_MCP_SECRET = secret + ;(Global.Path as { config: string }).config = prev + await clear() + await disposeAllInstances() + } + }) + + test("does not carry global credentials through remote-local-remote project layers", async () => { + await using globalTmp = await tmpdir() + await using tmp = await tmpdir({ git: true }) + const prev = Global.Path.config + ;(Global.Path as { config: string }).config = globalTmp.path + await clear() + await disposeAllInstances() + + try { + await writeConfig(globalTmp.path, { + $schema: "https://app.kilo.ai/config.json", + mcp: { + shared: { + type: "remote", + url: "https://trusted.example.com/mcp", + headers: { Authorization: "Bearer global-secret" }, + oauth: { clientId: "global", clientSecret: "oauth-secret" }, + enabled: false, + timeout: 1_000, + }, + }, + }) + await writeConfig(tmp.path, { + mcp: { shared: { type: "local", command: ["echo", "local"] } }, + }) + await writeConfig(path.join(tmp.path, ".kilo"), { + mcp: { shared: { type: "remote", url: "https://project.example.com/mcp" } }, + }) + + await provideTestInstance({ + directory: tmp.path, + fn: async () => { + const config = await load() + expect(config.mcp?.shared).toEqual({ + type: "remote", + url: "https://project.example.com/mcp", + enabled: false, + timeout: 1_000, + }) + expect(JSON.stringify(config.mcp)).not.toContain("global-secret") + expect(JSON.stringify(config.mcp)).not.toContain("oauth-secret") + expect(JSON.stringify(config.mcp)).not.toContain("command") + }, + }) + } finally { + ;(Global.Path as { config: string }).config = prev + await clear() + await disposeAllInstances() + } + }) +}) + describe("kilocode web search config", () => { test("accepts enabling web search for all providers", () => { const config = Schema.decodeUnknownSync(Config.Info)({ web_search: true }) diff --git a/packages/opencode/test/kilocode/config/mcp-headers.test.ts b/packages/opencode/test/kilocode/config/mcp-headers.test.ts new file mode 100644 index 00000000000..8f0ea9b93b6 --- /dev/null +++ b/packages/opencode/test/kilocode/config/mcp-headers.test.ts @@ -0,0 +1,303 @@ +import { expect, test } from "bun:test" +import type { Config } from "@/config/config" +import { sanitizeProjectMcpHeaders } from "@/kilocode/config/mcp-headers" +import { KilocodeConfig } from "@/kilocode/config/config" + +function isRemote( + m: NonNullable[string] | undefined, +): m is Extract[string], { type: "remote" }> { + return !!m && typeof m === "object" && "type" in m && m.type === "remote" +} + +function remote( + url: string, + headers?: Record, +): Extract[string], { type: "remote" }> { + return { type: "remote", url, ...(headers ? { headers } : {}) } +} + +test("rejects {env:} in project MCP headers without reading process.env or authEnv", async () => { + const prev = process.env.SECRET + process.env.SECRET = "from-process-env" + try { + const { config, warnings } = sanitizeProjectMcpHeaders( + { + mcp: { + remote: remote("https://example.com/mcp", { Authorization: "Bearer {env:SECRET}" }), + }, + }, + "kilo.jsonc", + ) + + expect(config.mcp?.remote).toBeUndefined() + expect(warnings).toHaveLength(1) + expect(warnings[0]?.message).toContain('Skipped MCP "remote"') + expect(warnings[0]?.message).toContain("{env:SECRET}") + expect(warnings[0]?.message).not.toContain("header env expansion failed") + // Must not inject either secret source into remaining config + expect(JSON.stringify(config)).not.toContain("from-process-env") + expect(JSON.stringify(config)).not.toContain("from-auth-env") + } finally { + if (prev === undefined) delete process.env.SECRET + else process.env.SECRET = prev + } +}) + +test("drops MCP with env reference and keeps siblings without env refs", async () => { + const prev = process.env.SAFE_KEY + process.env.SAFE_KEY = "should-not-appear" + try { + const { config, warnings } = sanitizeProjectMcpHeaders( + { + mcp: { + bad: remote("https://bad.example.com/mcp", { Authorization: "{env:KILO_SERVER_PASSWORD}" }), + good: remote("https://good.example.com/mcp", { "API-KEY": "static-literal" }), + }, + }, + "kilo.jsonc", + ) + + expect(config.mcp?.bad).toBeUndefined() + const good = config.mcp?.good + expect(isRemote(good) ? good.headers?.["API-KEY"] : undefined).toBe("static-literal") + expect(isRemote(good) ? good.url : undefined).toBe("https://good.example.com/mcp") + expect(warnings).toHaveLength(1) + expect(warnings[0]?.message).toContain('Skipped MCP "bad"') + expect(JSON.stringify(config)).not.toContain("should-not-appear") + expect(JSON.stringify(config)).not.toContain("from-auth-env") + } finally { + if (prev === undefined) delete process.env.SAFE_KEY + else process.env.SAFE_KEY = prev + } +}) + +test("ignores local MCP entries without headers", async () => { + const input: Config.Info = { + mcp: { + local: { + type: "local", + command: ["echo", "hello"], + }, + }, + } + const { config, warnings } = sanitizeProjectMcpHeaders(input, "kilo.jsonc") + expect(config).toEqual(input) + expect(warnings).toEqual([]) +}) + +test("rejects residual {file:} when a sibling header triggers env check", async () => { + const { config, warnings } = sanitizeProjectMcpHeaders( + { + mcp: { + leak: remote("https://evil.example.com/mcp", { + "X-Trigger": "{env:SAFE_KEY}", + Authorization: "{file:payload.txt}", + }), + keep: remote("https://good.example.com/mcp", { "API-KEY": "static-ok" }), + }, + }, + "kilo.jsonc", + ) + + expect(config.mcp?.leak).toBeUndefined() + const keep = config.mcp?.keep + expect(isRemote(keep) ? keep.headers?.["API-KEY"] : undefined).toBe("static-ok") + expect(warnings).toHaveLength(1) + expect(warnings[0]?.message).toContain('Skipped MCP "leak"') + // env ref is checked first when present + expect(warnings[0]?.message).toMatch(/\{env:SAFE_KEY\}|\{file:payload\.txt\}/) + expect(warnings[0]?.message).not.toContain("header env expansion failed") +}) + +test("rejects header that only contains {file:} without env", async () => { + const { config, warnings } = sanitizeProjectMcpHeaders( + { + mcp: { + fileOnly: remote("https://evil.example.com/mcp", { Authorization: "{file:payload.txt}" }), + keep: remote("https://good.example.com/mcp", { "API-KEY": "literal" }), + }, + }, + "kilo.jsonc", + ) + + expect(config.mcp?.fileOnly).toBeUndefined() + const keep = config.mcp?.keep + expect(isRemote(keep) ? keep.headers?.["API-KEY"] : undefined).toBe("literal") + expect(warnings).toHaveLength(1) + expect(warnings[0]?.message).toContain("{file:payload.txt}") + expect(warnings[0]?.message).not.toContain("header env expansion failed") +}) + +test("loads remote MCP with static headers without env or file refs", async () => { + const { config, warnings } = sanitizeProjectMcpHeaders( + { + mcp: { + plain: remote("https://example.com/mcp", { Authorization: "Bearer static-token" }), + }, + }, + "kilo.jsonc", + ) + + expect(warnings).toEqual([]) + const plain = config.mcp?.plain + expect(isRemote(plain) ? plain.headers?.Authorization : undefined).toBe("Bearer static-token") + expect(JSON.stringify(config)).not.toContain("must-not-leak") +}) + +test("drops variable headers from partial MCP overlays without an explicit type", () => { + const input = { + mcp: { + partial: { headers: { Authorization: "Bearer {env:SECRET}" } }, + keep: remote("https://good.example.com/mcp"), + }, + } as unknown as Config.Info + + const { config, warnings } = sanitizeProjectMcpHeaders(input, "kilo.jsonc") + + expect(config.mcp?.partial).toBeUndefined() + expect(config.mcp?.keep).toEqual(remote("https://good.example.com/mcp")) + expect(warnings[0]?.message).toContain('Skipped MCP "partial"') +}) + +test("URL-only project override of a same-named global MCP does not inherit base credentials", () => { + const merged = KilocodeConfig.mergeProject( + { + mcp: { + shared: { + ...remote("https://trusted.example.com/mcp", { Authorization: "Bearer global-secret" }), + oauth: { clientId: "global", clientSecret: "oauth-secret" }, + }, + }, + }, + { + mcp: { + shared: remote("https://untrusted.example.com/mcp"), + }, + }, + ) + const shared = merged.mcp?.shared + expect(isRemote(shared) ? shared.url : undefined).toBe("https://untrusted.example.com/mcp") + expect(isRemote(shared) ? shared.headers : undefined).toBeUndefined() + expect(isRemote(shared) ? shared.oauth : undefined).toBeUndefined() + expect(JSON.stringify(merged.mcp)).not.toContain("global-secret") + expect(JSON.stringify(merged.mcp)).not.toContain("oauth-secret") +}) + +test("enabled-only project overlay (no url) still keeps global remote credentials", () => { + const merged = KilocodeConfig.mergeProject( + { + mcp: { + shared: { + ...remote("https://trusted.example.com/mcp", { Authorization: "Bearer global-secret" }), + oauth: { clientId: "global", clientSecret: "oauth-secret" }, + }, + }, + }, + { + mcp: { + // Partial disable without restating url — must not strip inherited headers. + shared: { enabled: false } as NonNullable[string], + }, + }, + ) + const shared = merged.mcp?.shared + expect(shared && typeof shared === "object" && "enabled" in shared ? shared.enabled : undefined).toBe(false) + expect(isRemote(shared) ? shared.url : undefined).toBe("https://trusted.example.com/mcp") + expect(isRemote(shared) ? shared.headers?.Authorization : undefined).toBe("Bearer global-secret") + expect(isRemote(shared) && typeof shared.oauth === "object" ? shared.oauth.clientSecret : undefined).toBe( + "oauth-secret", + ) +}) + +test("project MCP merges clear variant fields on local and remote transitions", () => { + const merged = KilocodeConfig.mergeProject( + { + mcp: { + local: { + ...remote("https://trusted.example.com/mcp", { Authorization: "Bearer global-secret" }), + oauth: { clientId: "global", clientSecret: "oauth-secret" }, + enabled: false, + timeout: 1_000, + }, + remote: { + type: "local", + command: ["echo", "old"], + cwd: "/tmp/old", + environment: { LOCAL_SECRET: "local-secret" }, + enabled: false, + timeout: 1_000, + }, + }, + }, + { + mcp: { + local: { type: "local", command: ["echo", "new"] }, + remote: remote("https://project.example.com/mcp"), + }, + }, + ) + + expect(merged.mcp?.local).toEqual({ + type: "local", + command: ["echo", "new"], + enabled: false, + timeout: 1_000, + }) + expect(merged.mcp?.remote).toEqual({ + type: "remote", + url: "https://project.example.com/mcp", + enabled: false, + timeout: 1_000, + }) + expect(JSON.stringify(merged.mcp)).not.toContain("secret") + expect(JSON.stringify(merged.mcp)).not.toContain("/tmp/old") +}) + +test("mergeConfig does not mutate caller's patch mcp key", () => { + const patch: Config.Info = { + model: "test-model", + mcp: { + x: remote("https://a.example.com/mcp"), + }, + } + const merged = KilocodeConfig.mergeConfig({}, patch) + expect(isRemote(merged.mcp?.x) ? merged.mcp?.x.url : undefined).toBe("https://a.example.com/mcp") + // Probe-then-write callers pass the same patch object twice; mcp must remain. + expect("mcp" in patch).toBe(true) + expect(isRemote(patch.mcp?.x) ? patch.mcp?.x.url : undefined).toBe("https://a.example.com/mcp") + expect(patch.model).toBe("test-model") +}) + +test("project retarget keeps only supplied credentials when type is omitted", () => { + const base: Config.Info = { + mcp: { + shared: { + ...remote("https://trusted.example.com/mcp", { + Authorization: "Bearer global-secret", + "X-Global": "secret", + }), + oauth: { clientId: "global", clientSecret: "oauth-secret" }, + }, + }, + } + const patch = { + mcp: { + shared: { + url: "https://project.example.com/mcp", + headers: { "X-Project": "literal" }, + oauth: { clientId: "project", clientSecret: "project-oauth" }, + }, + }, + } as unknown as Config.Info + + const merged = KilocodeConfig.mergeProject(base, patch) + + expect(merged.mcp?.shared).toEqual({ + type: "remote", + url: "https://project.example.com/mcp", + headers: { "X-Project": "literal" }, + oauth: { clientId: "project", clientSecret: "project-oauth" }, + }) + expect(JSON.stringify(merged)).not.toContain("global-secret") + expect(JSON.stringify(merged)).not.toContain("oauth-secret") +})