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/.changeset/broad-custom-provider-efforts.md b/.changeset/broad-custom-provider-efforts.md new file mode 100644 index 00000000000..c0746956176 --- /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 and link saved providers to advanced JSON configuration. 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/.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/.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/.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/.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/.changeset/ignore-negative-model-prices.md b/.changeset/ignore-negative-model-prices.md new file mode 100644 index 00000000000..66bcf01a6dd --- /dev/null +++ b/.changeset/ignore-negative-model-prices.md @@ -0,0 +1,8 @@ +--- +"@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/.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/.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/.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/.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-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 | 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 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 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-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-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-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-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/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/src/agent-manager/PRStatusPoller.ts b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts index 56ced904d57..d94a4511fc2 100644 --- a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts +++ b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts @@ -1,10 +1,12 @@ 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, AggregateCheckStatus } from "./types" import { execWithShellEnv } from "./shell-env" 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-types" interface PRStatusPollerOptions { getWorktrees: () => Worktree[] @@ -17,6 +19,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 @@ -171,7 +174,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 { @@ -244,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 @@ -253,17 +258,22 @@ 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 }), + reviewers, + ...(comments && { + 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 reviewersSig = reviewers.map((r) => `${r.login}:${r.state}`).join(",") + 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) @@ -292,7 +302,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 @@ -374,7 +384,7 @@ export class PRStatusPoller { passed: number failed: number pending: number - items: PRCheck[] + checks: PRCheck[] }> { try { const { stdout } = await this.gh( @@ -389,24 +399,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), + status: checkStatus(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.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 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: [] } } } @@ -424,13 +434,53 @@ 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(last: 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 (err) { + this.options.log("Failed to fetch PR reviewers:", err) + return [] + } + } + private async fetchComments( prNumber: number, cwd: string, - ): Promise<{ total: number; unresolved: number; items: PRComment[] }> { + ): Promise<{ total: number; unresolved: number; comments: PRComment[] }> { try { const repo = await this.getRepoInfo(cwd) - const query = `query($owner: String!, $repo: String!, $number: Int!) { repository(owner: $owner, name: $repo) { pullRequest(number: $number) { @@ -469,107 +519,16 @@ export class PRStatusPoller { ], { 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 comments = parseComments((pr?.reviewThreads?.nodes ?? []) as GhThread[]) + 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, items: [] } + return { total: 0, unresolved: 0, comments: [] } } } } -interface PRResult { - number: number - title: 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 ?? "", - 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` -} - /** 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-types.ts b/packages/kilo-vscode/src/agent-manager/am-pr-types.ts new file mode 100644 index 00000000000..4cd818d7a09 --- /dev/null +++ b/packages/kilo-vscode/src/agent-manager/am-pr-types.ts @@ -0,0 +1,40 @@ +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 new file mode 100644 index 00000000000..5fe6e0fad5a --- /dev/null +++ b/packages/kilo-vscode/src/agent-manager/am-pr-utils.ts @@ -0,0 +1,105 @@ +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 { + const data = JSON.parse(json) + if (!data.number) return 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 + 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": + case "NEUTRAL": + return "success" + case "FAILURE": + case "ERROR": + case "ACTION_REQUIRED": + 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/src/agent-manager/pr-status-bridge.ts b/packages/kilo-vscode/src/agent-manager/pr-status-bridge.ts index cbbb1bffda8..42370a5e721 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 { @@ -78,8 +81,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) ?? this.host.getWorktrees().find((w: Worktree) => w.id === m.worktreeId)?.prUrl + if (url) this.host.openExternal(url) return true } return false @@ -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,10 +125,15 @@ 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 bridge["cache"].set(id, msg) + bridge["lastErrorNotified"] = undefined host.postToWebview(msg) host.updateWorktreePR(id, pr?.number, pr?.url, pr?.state) }, diff --git a/packages/kilo-vscode/src/agent-manager/types.ts b/packages/kilo-vscode/src/agent-manager/types.ts index 7e6792f10b2..01c88ff1581 100644 --- a/packages/kilo-vscode/src/agent-manager/types.ts +++ b/packages/kilo-vscode/src/agent-manager/types.ts @@ -71,9 +71,18 @@ export interface PRComment { createdAt?: number } +export type ReviewerState = "approved" | "changes_requested" | "pending" | "commented" + +export interface PRReviewer { + login: string + avatar?: string + state: ReviewerState +} + export interface PRStatus { number: number title: string + body?: string url: string state: PRState review: ReviewDecision | null @@ -83,12 +92,13 @@ export interface PRStatus { passed: number failed: number pending: number - items: PRCheck[] + checks: PRCheck[] } + reviewers: PRReviewer[] comments?: { total: number unresolved: number - items: PRComment[] + comments: PRComment[] } additions: number deletions: number @@ -379,6 +389,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 @@ -418,6 +433,7 @@ export type AgentManagerOutMessage = | RevertWorktreeFileResultMessage | DiffBranchesMessage | PRStatusOutMessage + | PRErrorOutMessage | ActionOutMessage | RunStatusMessage | TerminalCreatedMessage @@ -739,6 +755,7 @@ interface OpenPRIn { type: "agentManager.openPR" projectId?: string worktreeId: string + url?: string } interface OpenSessionsIn { 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/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/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/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"), 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 74197ac0492..11d92fda5c4 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/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..734b3128f6e --- /dev/null +++ b/packages/kilo-vscode/tests/unit/am-pr-status-bridge.test.ts @@ -0,0 +1,188 @@ +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..ba2d8d80070 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/am-pr-utils.test.ts @@ -0,0 +1,324 @@ +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) + }) +}) 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..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 @@ -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", () => { @@ -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 @@ -264,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/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/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/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/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/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/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/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/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/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index 5487c25b066..622f0fe96f5 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -36,6 +36,7 @@ import type { RunStatus, PRStatus, AgentManagerPRStatusMessage, + AgentManagerPRErrorMessage, AgentManagerProjectsMessage, AgentProjectSnapshot, ManagedSessionState, @@ -143,6 +144,7 @@ import { import { focusCurrentTab, renderTab, renderTerminalLayer, renderNewTabButton } from "./tab-rendering" import { useTabScroll } from "./tab-scroll" import { DiffPanel } from "./DiffPanel" +import { PRPanel } from "./pr/PRPanel" import { createRevertFile } from "./revert-file" import { FullScreenDiffView } from "../diff-viewer/FullScreenDiffView" import { createApplyToLocal } from "./apply-to-local" @@ -152,6 +154,8 @@ import { clearReviewComposer, createReviewComposer } from "../diff-viewer/review import type { SidebarSearchMenuRef } from "./SidebarSearchMenu" import { createSidebarSearch, type SidebarSearchItem } from "./sidebar-search" import { randomColor } from "./section-colors" +import { createMarkdownRender } from "./review-preferences" +import { createSidebarCollapse } from "./sidebar-collapse" import { createNewTaskDrafts } from "./new-task-drafts" import { buildTopLevelItems, @@ -170,11 +174,9 @@ 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 } 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 +197,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) const SIDE_RESIZE_INTERVAL_MS = 32 @@ -297,8 +299,16 @@ const AgentManagerContent: Component = () => { let sideResizeTime = 0 const [history, setHistory] = createSignal(false) - const [sidePanel, setSidePanel] = createSignal(null) - const diffOpen = () => sidePanel() === "diff" + 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 + 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 @@ -324,7 +334,7 @@ const AgentManagerContent: Component = () => { const showSideTerminal = () => { setHistory(false) setReviewActive(false) - setSidePanel("terminal") + setSidePanel(SidePanel.Terminal) } const [reviewOpenByContext, setReviewOpenByContext] = createSignal>({}) @@ -336,7 +346,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() @@ -421,7 +430,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) @@ -490,6 +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 }, + ), + ) onCleanup(() => clearTimeout(pendingDeleteTimer)) // Per-context tab memory lives in the active project's store: maps sidebar @@ -547,11 +565,17 @@ const AgentManagerContent: Component = () => { } const openWindow = metrics.click("open_worktree_window", "tab_toolbar", openWorktreeDirectory) + const togglePRPanel = () => { + setHistory(false) + if (reviewActive()) closeReviewTab() + setSidePanel((prev) => (prev === SidePanel.PR ? null : SidePanel.PR)) + } + const openSelectedPR = () => { 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 runWorktree = (id: string, destination: TerminalDestination) => { @@ -1183,8 +1207,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() @@ -1345,6 +1369,7 @@ const AgentManagerContent: Component = () => { showToast({ variant: "error", title: t("agentManager.terminal.errorTitle"), description: message }), postMessage: (message) => vscode.postMessage(message as never), onCreated: (contextKey, terminalId) => appendToTabOrder(contextKey, terminalId), + onSideClosed: (_contextKey, terminalId) => forgetTerminalFocus(terminalId), onScriptRunning: (contextKey, terminalId) => { if (terms.sideKey() !== contextKey) return @@ -1412,7 +1437,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 }) } @@ -1562,6 +1587,15 @@ 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 }) @@ -2011,7 +2045,7 @@ const AgentManagerContent: Component = () => { const sideCtl = createSideTerminal({ handlers: termHandlers, - visible: () => sidePanel() === "terminal" && !history() && !reviewActive(), + visible: () => sidePanel() === SidePanel.Terminal && !history() && !reviewActive(), focusedId: () => terms.sideFocusedId(), count: () => terms.sidesForContext(terms.sideKey()).length, isScript: terms.isScript, @@ -2144,7 +2178,7 @@ const AgentManagerContent: Component = () => { const closeActiveTab = () => { // 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. - if (sidePanel() === "terminal" && terms.sideFocusedId()) { + if (sidePanel() === SidePanel.Terminal && terms.sideFocusedId()) { if (sideCtl.close()) return } if (termHandlers.closeFocused()) { @@ -2200,10 +2234,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) => @@ -2399,8 +2433,11 @@ const AgentManagerContent: Component = () => { reviewActive={reviewActive} onToggleDiff={toggleDiffPanel} onToggleReview={metrics.click("fullscreen_review", "tab_toolbar", toggleReviewTab)} + prStatus={() => activePR()?.pr} + prOpen={prOpen} + onTogglePR={togglePRPanel} terminalDestination={sideCtl.destination} - terminalDestinationActive={() => sidePanel() === "terminal"} + terminalDestinationActive={() => sidePanel() === SidePanel.Terminal} terminalKeybind={() => kb().showTerminal ?? ""} onTerminalDestinationOpen={() => { cancelAmbientSetup() @@ -2588,7 +2625,7 @@ const AgentManagerContent: Component = () => { />
- + { activeTerminalId={terms.activeId()} /> + + {(() => { + const data = activePR()! + return ( + setSidePanel(null)} + onOpenExternal={() => + vscode.postMessage({ + type: "agentManager.openPR", + worktreeId: data.selected, + url: data.pr.url, + }) + } + /> + ) + })()} + sidePanel() === "terminal"} + visible={() => sidePanel() === SidePanel.Terminal} nextKeybind={kb().nextTerminal ?? ""} closeKeybind={kb().closeTab ?? ""} onFocusPrompt={focusPrompt} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/TabBar.tsx b/packages/kilo-vscode/webview-ui/agent-manager/TabBar.tsx index 359ad99e4cb..3f8cba105e7 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 | undefined + prOpen: () => boolean + onTogglePR: () => void terminalDestination: () => TerminalDestination terminalDestinationActive: () => boolean terminalKeybind: () => string @@ -215,6 +218,20 @@ 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} - + )} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx b/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx index a7fc15a6cf4..c44b767cffd 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,14 +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. */ -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" -} - /** 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" @@ -349,18 +342,30 @@ export const WorktreeItem: Component = (props) => { } > {(pr) => { - const accent = () => prAccentColor(pr()) const indicator = () => prBadgeIndicator(pr()) return ( - }> + + #{pr().number} + - + @@ -374,7 +379,6 @@ 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 88adf062704..5017d37a669 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css @@ -840,7 +840,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"] { @@ -848,7 +848,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 { @@ -898,24 +898,44 @@ 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 */ +.am-pr-accent-open, +.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 { + --am-pr-accent: var(--vscode-charts-yellow, #fbbf24); +} +.am-pr-accent-draft, +.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 { + --am-pr-accent: var(--vscode-charts-purple, var(--vscode-foreground)); +} +.am-pr-accent-closed, +.am-pr-panel-badge.am-pr-accent-closed { + --am-pr-accent: var(--vscode-testing-iconFailed, #f87171); } -/* PR pill badge — second row inside am-wt-row2. - Uses --pr-accent custom property (set inline) to derive all colors. */ .am-pr-badge { 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); - 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; @@ -924,34 +944,34 @@ 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. */ .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 { @@ -965,17 +985,17 @@ html[data-theme="kilo-vscode"] } @media (prefers-reduced-motion: reduce) { - .am-pr-badge[data-pending] { + .am-pr-badge.am-pr-badge-pending { animation: none; opacity: 0.9; } } .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 */ @@ -996,35 +1016,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, @@ -1699,7 +1719,7 @@ body.am-wt-dragging-active * { } .am-sidebar-search-status { - color: #fbbf24; + color: var(--vscode-charts-yellow, #fbbf24); text-transform: uppercase; } @@ -1794,9 +1814,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 { @@ -1810,20 +1831,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 { @@ -2947,9 +2972,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/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/en.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts index db68e9bd7dc..ca2526380c8 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts @@ -217,4 +217,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/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 狀態。", } 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/PRBadge.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRBadge.tsx new file mode 100644 index 00000000000..55bdea2165a --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRBadge.tsx @@ -0,0 +1,33 @@ +/** @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" + +const INDICATOR_ICON: Record = { + failure: "circle-x-outline", + changes: "warning", + approved: "circle-check", +} + +export function PRBadge(props: { pr: PRStatus }) { + const indicator = () => prBadgeIndicator(props.pr) + return ( + + + + + + + ) +} 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..57be374882f --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRChecks.tsx @@ -0,0 +1,62 @@ +/** @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" }, + 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"] }) { + const vscode = useVSCode() + 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[check.status].label} + + {check.duration} + + + + + + +
+ )} +
+
+
+
+ + ) +} 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..b73c915f02d --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRComments.tsx @@ -0,0 +1,51 @@ +/** @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" +import { CopyButton } from "./CopyButton" + +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/PROverview.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PROverview.tsx new file mode 100644 index 00000000000..577dd4a8e7d --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PROverview.tsx @@ -0,0 +1,51 @@ +/** @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..ec37c45940a --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRPanel.tsx @@ -0,0 +1,63 @@ +/** @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 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 { PRChecks } from "./PRChecks" +import { PRComments } from "./PRComments" +import { PRSummary } from "./PRSummary" +import "./pr-panel.css" + +interface PRPanelProps { + pr: PRStatus + worktree?: WorktreeState + onClose: () => void + onOpenExternal: () => void +} + +export const PRPanel: Component = (props) => { + return ( +
+
+
+ + {props.pr.title} + #{props.pr.number} +
+
+ + + + + + +
+
+
+ + + 0}> + + + {(body) => } + 0}> + + + + {(comments) => } + +
+
+ ) +} 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..071da1f119d --- /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, ReviewerState } 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/PRSummary.tsx b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRSummary.tsx new file mode 100644 index 00000000000..48d9d2ee2e8 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/PRSummary.tsx @@ -0,0 +1,72 @@ +/** @jsxImportSource solid-js */ +import { Show } from "solid-js" +import { Icon } from "@kilocode/kilo-ui/icon" +import type { PRStatus } from "../../src/types/messages" + +interface PRSummaryProps { + pr: PRStatus +} + +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, + }) + } + + 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: status === "success" ? "Approved" : status === "failure" ? "Changes requested" : "Review pending", + 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 + + 0}> + {props.pr.files}f + + 0}> + +{props.pr.additions} + + 0}> + −{props.pr.deletions} + + +
+
+ {rows().map((row) => ( +
+ + {row.label} +
+ ))} +
+
+
+ ) +} 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..e505989acee --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-panel.css @@ -0,0 +1,457 @@ +/* PR Panel */ +.am-pr-panel { + height: 100%; + overflow: hidden; +} + +.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 4px; + border-radius: 4px; + font-size: var(--kilo-font-size-10); + font-variant-numeric: tabular-nums; + 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; +} + +.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-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; +} + +.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-copy-btn { + 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; +} + +/* PR Summary banner */ +.am-pr-summary { + margin: 8px 12px; + border: 1px solid var(--vscode-panel-border); + border-radius: 6px; + overflow: hidden; +} + +.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); +} 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..d5d0a89d6c6 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/pr/pr-types.ts @@ -0,0 +1,34 @@ +// 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 type ReviewerState = "approved" | "changes_requested" | "pending" | "commented" + +export interface PRReviewer { + login: string + avatar?: string + state: ReviewerState +} 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 } 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..5d966e9c578 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, @@ -89,6 +90,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 @@ -449,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, {}]) @@ -460,25 +474,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, @@ -532,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")} + +
+ + +
+ +
+
@@ -655,25 +665,54 @@ const CustomProviderDialog = (props: CustomProviderDialogProps) => { {/* Models */}
-
- - - - +
+
+ + + + +
+
+ + +
{(m, i) => ( 1} @@ -682,23 +721,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..49338afe000 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,6 @@ -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 { Show } from "solid-js" import { useLanguage } from "../../context/language" export type Translator = ReturnType["t"] @@ -23,6 +21,7 @@ export type Modalities = { export type VariantEntry = { name: string + raw?: Record enableThinking: EnableThinkingValue thinking: ThinkingTypeValue splitReasoning: SplitReasoningValue @@ -40,264 +39,8 @@ 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 }> } t: Translator canRemove: boolean @@ -306,18 +49,11 @@ 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) { + const issue = () => props.errors.variants?.find((error) => error.name)?.name + return (
- {/* Reasoning toggle */} - + {/* Reasoning and Image toggles */} +
+ - + +
- {/* 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())} - /> - )} - -
-
- + + {(error) => ( + + {error()} + + )}
) 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..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 } } @@ -105,6 +106,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/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/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/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/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/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. */} 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-merge.ts b/packages/kilo-vscode/webview-ui/src/context/session-merge.ts new file mode 100644 index 00000000000..2a59bf0d0ad --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/context/session-merge.ts @@ -0,0 +1,43 @@ +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 + // 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 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, ...kept] : 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 8831d7edfb0..f5a6d8d8903 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -70,9 +70,10 @@ 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 { mergeParts } from "./session-parts" +import { mergeMessages, sameReconcileShape } from "./session-merge" import { state as todoState } from "./todo-revert" import { sessionVariantKeys, transferVariants, variantKey } from "./session-variant-store" import { createSessionVariants } from "./session-variants" @@ -1175,7 +1176,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 +1189,7 @@ export const SessionProvider: ParentComponent = (props) => { createdAt: new Date().toISOString(), parentID: parent?.id, error: message.error, + sessionErrorID: message.eventID, } handleMessageCreated(errorMsg) break @@ -1380,28 +1382,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]) { @@ -1418,25 +1398,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] - } - - // 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 + return [...merged, ...orphans] } function setTools(sessionID: string, tools: ToolPart[]) { @@ -1487,7 +1455,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)) { + 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] @@ -1601,16 +1572,18 @@ 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]) // 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/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index 9a3310b6555..4c5496bc988 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -443,44 +443,8 @@ 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.toggleReasoning": "تبديل التفكير للكل", + "provider.custom.models.toggleImages": "تبديل الصور للكل", "provider.custom.models.remove": "إزالة النموذج", "provider.custom.models.add": "إضافة نموذج", "provider.custom.models.fetch.authError": "فشلت المصادقة. تحقق من مفتاح API أعلاه وحاول مرة أخرى.", @@ -494,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": "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 0df91b651c9..f7412b3e7ea 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -454,45 +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.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.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.", @@ -506,6 +469,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 db0f4590145..f372413ef25 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -496,45 +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.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.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": @@ -549,6 +512,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 6f36adc3d33..2c5df219a56 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -494,45 +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.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.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.", @@ -546,6 +509,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 4066d9396d9..9e83f815f8c 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -504,45 +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.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.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": @@ -557,6 +520,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 80f804d969d..cf4a857e1af 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -408,45 +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.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.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.", @@ -460,6 +423,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 558eb4a26ae..644800a5221 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -497,45 +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.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.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": @@ -550,6 +513,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 cda714862bc..3b1530349db 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fa.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fa.ts @@ -409,45 +409,8 @@ 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.toggleReasoning": "تغییر وضعیت استدلال برای همه", + "provider.custom.models.toggleImages": "تغییر وضعیت تصویر برای همه", "provider.custom.models.remove": "حذف مدل", "provider.custom.models.add": "افزودن مدل", "provider.custom.models.fetch.authError": "احراز هویت ناموفق بود. کلید API بالا را بررسی کرده و دوباره امتحان کنید.", @@ -461,6 +424,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 0292d8767a9..5ee7c64be6b 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -498,45 +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.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.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.", @@ -550,6 +513,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 10ef2fd2dcb..833f568ff41 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/it.ts @@ -318,45 +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.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.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.", @@ -370,6 +333,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 67c60731ef5..511b5034aa8 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -490,44 +490,8 @@ 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.toggleReasoning": "すべてのモデルの推論を切り替え", + "provider.custom.models.toggleImages": "すべてのモデルの画像を切り替え", "provider.custom.models.remove": "モデルを削除", "provider.custom.models.add": "モデルを追加", "provider.custom.models.fetch.authError": "認証に失敗しました。上記のAPIキーを確認して再試行してください。", @@ -541,6 +505,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 e7d0664e7b7..aad05fc7ca1 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -451,44 +451,8 @@ 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.toggleReasoning": "모든 모델의 추론 전환", + "provider.custom.models.toggleImages": "모든 모델의 이미지 전환", "provider.custom.models.remove": "모델 제거", "provider.custom.models.add": "모델 추가", "provider.custom.models.fetch.authError": "인증에 실패했습니다. 위의 API 키를 확인하고 다시 시도하세요.", @@ -502,6 +466,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 40169d8672d..5d1a26d21f9 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts @@ -447,45 +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.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.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": @@ -500,6 +463,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 bea79dee720..eb98954aba1 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -457,45 +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.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.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.", @@ -509,6 +472,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 701c94122f9..d711cff1355 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -452,45 +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.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.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": @@ -505,6 +468,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 9f64e84a176..eae378a40f7 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -491,45 +491,8 @@ 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.toggleReasoning": "Переключить рассуждения для всех", + "provider.custom.models.toggleImages": "Переключить изображения для всех", "provider.custom.models.remove": "Удалить модель", "provider.custom.models.add": "Добавить модель", "provider.custom.models.fetch.authError": "Ошибка аутентификации. Проверьте API-ключ выше и попробуйте снова.", @@ -543,6 +506,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 5b0e38ddf14..4dba9385c68 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -488,45 +488,8 @@ 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.toggleReasoning": "สลับการให้เหตุผลสำหรับทุกโมเดล", + "provider.custom.models.toggleImages": "สลับรูปภาพสำหรับทุกโมเดล", "provider.custom.models.remove": "ลบโมเดล", "provider.custom.models.add": "เพิ่มโมเดล", "provider.custom.models.fetch.authError": "การยืนยันตัวตนล้มเหลว ตรวจสอบคีย์ API ด้านบนแล้วลองอีกครั้ง", @@ -540,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/tr.ts b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts index 83a9d8f16ce..e8767e36004 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts @@ -442,45 +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.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.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": @@ -495,6 +458,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 e773ee2d54a..84672e9f904 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts @@ -446,45 +446,8 @@ 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.toggleReasoning": "Перемкнути міркування для всіх", + "provider.custom.models.toggleImages": "Перемкнути зображення для всіх", "provider.custom.models.remove": "Видалити модель", "provider.custom.models.add": "Додати модель", "provider.custom.models.fetch.authError": "Автентифікація не вдалася. Перевірте API-ключ вище і спробуйте ще раз.", @@ -498,6 +461,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 249a332bd1b..28fcf881cd9 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -473,44 +473,8 @@ 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.toggleReasoning": "切换所有模型的推理", + "provider.custom.models.toggleImages": "切换所有模型的图像", "provider.custom.models.remove": "移除模型", "provider.custom.models.add": "添加模型", "provider.custom.models.fetch.authError": "认证失败。请检查上方的 API 密钥后重试。", @@ -524,6 +488,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 93746129e74..e6842c06da4 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -433,44 +433,8 @@ 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.toggleReasoning": "切換所有模型的推理", + "provider.custom.models.toggleImages": "切換所有模型的圖像", "provider.custom.models.remove": "移除模型", "provider.custom.models.add": "新增模型", "provider.custom.models.fetch.authError": "驗證失敗。請檢查上方的 API 金鑰後重試。", @@ -484,6 +448,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/stories/agent-manager.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx index e74cac8b0fb..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 @@ -612,7 +612,8 @@ 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: [] }, + 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 cf348a4d339..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,7 +593,8 @@ export const WithPRBadges: Story = { additions: 120, deletions: 30, files: 5, - checks: { status: "success", total: 5, passed: 5, failed: 0, pending: 0, items: [] }, + reviewers: [], + 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 +612,8 @@ export const WithPRBadges: Story = { additions: 15, deletions: 8, files: 3, - checks: { status: "failure", total: 5, passed: 3, failed: 2, pending: 0, items: [] }, + reviewers: [], + checks: { status: "failure", total: 5, passed: 3, failed: 2, pending: 0, checks: [] }, }} /> {/* Open + changes requested → green badge, amber warning */} @@ -629,7 +631,8 @@ export const WithPRBadges: Story = { additions: 60, deletions: 12, files: 4, - checks: { status: "success", total: 5, passed: 5, failed: 0, pending: 0, items: [] }, + reviewers: [], + checks: { status: "success", total: 5, passed: 5, failed: 0, pending: 0, checks: [] }, }} /> {/* Open + checks running → pulsing amber badge (animation disabled in snapshots) */} @@ -647,7 +650,8 @@ export const WithPRBadges: Story = { additions: 200, deletions: 0, files: 8, - checks: { status: "pending", total: 5, passed: 2, failed: 0, pending: 3, items: [] }, + reviewers: [], + checks: { status: "pending", total: 5, passed: 2, failed: 0, pending: 3, checks: [] }, }} /> {/* Draft → gray badge */} @@ -665,7 +669,8 @@ export const WithPRBadges: Story = { additions: 30, deletions: 5, files: 2, - checks: { status: "none", total: 0, passed: 0, failed: 0, pending: 0, items: [] }, + reviewers: [], + checks: { status: "none", total: 0, passed: 0, failed: 0, pending: 0, checks: [] }, }} /> {/* Merged → purple badge, no status icon */} @@ -683,7 +688,8 @@ export const WithPRBadges: Story = { additions: 90, deletions: 20, files: 6, - checks: { status: "success", total: 5, passed: 5, failed: 0, pending: 0, items: [] }, + reviewers: [], + 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 +707,8 @@ export const WithPRBadges: Story = { additions: 10, deletions: 4, files: 1, - checks: { status: "failure", total: 5, passed: 1, failed: 4, pending: 0, items: [] }, + reviewers: [], + checks: { status: "failure", total: 5, passed: 1, failed: 4, pending: 0, checks: [] }, }} />
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: () => ( + + + + ), +} 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" }, 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 231731b1bdf..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 @@ -45,36 +45,31 @@ 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 title: string + body?: string url: string state: PRState review: ReviewDecision | null @@ -84,12 +79,13 @@ export interface PRStatus { passed: number failed: number pending: number - items: PRCheck[] + checks: PRCheck[] } + reviewers: PRReviewer[] comments?: { total: number unresolved: number - items: PRComment[] + comments: PRComment[] } additions: number deletions: number 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..4008e5d1ea0 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 } } @@ -1046,6 +1047,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" @@ -1411,6 +1417,7 @@ export type ExtensionMessage = | AgentManagerWorktreeStatsMessage | AgentManagerLocalStatsMessage | AgentManagerPRStatusMessage + | AgentManagerPRErrorMessage | AgentManagerTerminalCreatedMessage | AgentManagerTerminalRestartedMessage | AgentManagerTerminalFontChangedMessage 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 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 98de565b812..d444f7fb881 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 @@ -988,6 +988,7 @@ export interface OpenPRMessage { type: "agentManager.openPR" projectId?: string worktreeId: string + url?: string } export interface ApplyWorktreeDiffMessage { 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"), + }, + } +} 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/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/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/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/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/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 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/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/src/kilocode/provider/provider.ts b/packages/opencode/src/kilocode/provider/provider.ts index cc8a452fc96..21345881ee9 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,39 @@ 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 { + 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(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/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/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 897e8a6880f..c830ad8bb78 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,12 @@ 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 = 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( pickBy(merged, (v): v is NonNullable => !!v && !v.disabled), // kilocode_change - drop null delete sentinels (v) => omit(v, ["disabled"]), diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 8112996020f..310c9d0c83c 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -328,6 +328,25 @@ function normalizeMessages( return msgs } +// kilocode_change start - explicit prompt cache breakpoints for GPT-5.6+ (excluding ChatGPT subscriptions) +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 (isLikelyChatGPTSubscription(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 = model.api.id.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 +370,18 @@ function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage alibaba: { cacheControl: { type: "ephemeral" }, }, + // kilocode_change start + ...(supportsPromptCacheBreakpoint(model) + ? { + openai: { + promptCacheBreakpoint: { mode: "explicit" }, + }, + azure: { + promptCacheBreakpoint: { mode: "explicit" }, + }, + } + : {}), + // kilocode_change end } for (const msg of unique([...system, ...final])) { @@ -360,8 +391,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) { - const lastContent = msg.content[msg.content.length - 1] + // 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.type !== "tool-approval-request" && + part.type !== "tool-approval-response" && + !(part.type === "text" && part.text.startsWith("")) + ) { + targetIndex = i + break + } + } + const lastContent = targetIndex >= 0 ? parts[targetIndex] : parts[parts.length - 1] if ( lastContent && typeof lastContent === "object" && @@ -372,6 +419,7 @@ function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage continue } } + // kilocode_change end msg.providerOptions = mergeDeep(msg.providerOptions ?? {}, providerOptions) } @@ -438,6 +486,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" || @@ -446,11 +495,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" || - 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.api.npm === "@kilocode/kilo-gateway") && + supportsPromptCacheBreakpoint(model))) && 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/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..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" @@ -412,4 +413,60 @@ 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.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?", + actions: [{ label: "Run checks", prompt: "Run checks" }], + }), + ) + yield* llm.text("completed after dismissal") + + const result = yield* opencode.run("do work", { + extraArgs: ["--attach", server.url, "--session", sessionID!, "--auto"], + }) + opencode.expectExit(result, 0) + + 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, + ) + // kilocode_change end }) 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/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") +}) 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..7de3036fd7a --- /dev/null +++ b/packages/opencode/test/kilocode/custom-provider-variants.test.ts @@ -0,0 +1,45 @@ +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( + "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(["custom"]) + 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/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 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") }) diff --git a/packages/opencode/test/kilocode/provider-reasoning-options.test.ts b/packages/opencode/test/kilocode/provider-reasoning-options.test.ts index 711820ede3b..5987819421d 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,67 @@ 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("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({ + 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({}) + }) +}) 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") 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: "*", diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 47051f6ed30..e92389ee2a3 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -1536,14 +1536,13 @@ it.instance( ) it.instance( - "variant config merges with generated variants", + "configured variants remain authoritative", // kilocode_change Effect.gen(function* () { yield* set("ANTHROPIC_API_KEY", "test-api-key") const providers = yield* list const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-20250514"] expect(model.variants!["high"]).toBeDefined() - // Should have both the generated thinking config and the custom option - expect(model.variants!["high"].thinking).toBeDefined() + expect(model.variants!["high"].thinking).toBeUndefined() // kilocode_change expect(model.variants!["high"].extraOption).toBe("custom-value") }), { diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index dfa901724b1..e834ec326eb 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -3029,6 +3029,172 @@ 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() + }) + + 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() + }) + + 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", + }, + }) + }) + + test("openai gpt-5.6 with ChatGPT subscription (zero cost heuristic) 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() + }) + // kilocode_change end }) describe("ProviderTransform.temperature - Cohere North", () => { 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"),