diff --git a/.changeset/agent-manager-modal-mode-shortcut.md b/.changeset/agent-manager-modal-mode-shortcut.md deleted file mode 100644 index 4efdaa9bc43..00000000000 --- a/.changeset/agent-manager-modal-mode-shortcut.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Fix Agent Manager mode shortcuts in the New Worktree dialog so the selected mode and its matching model stay in sync. diff --git a/.changeset/bright-files-count.md b/.changeset/bright-files-count.md deleted file mode 100644 index 84e4aa4fa2b..00000000000 --- a/.changeset/bright-files-count.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Show aggregate added and removed line counts for multi-file patch tool calls. diff --git a/.changeset/bright-project-dnd.md b/.changeset/bright-project-dnd.md deleted file mode 100644 index 750341b6225..00000000000 --- a/.changeset/bright-project-dnd.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Restore Agent Manager sections and worktree drag-and-drop when multiple projects are shown, with ordering and section moves scoped to the owning project. diff --git a/.changeset/clean-vscode-sidebar-edge.md b/.changeset/clean-vscode-sidebar-edge.md deleted file mode 100644 index dd798951ac5..00000000000 --- a/.changeset/clean-vscode-sidebar-edge.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Remove the duplicate border along the Kilo Code sidebar edge. diff --git a/.changeset/compaction-threshold-boundary.md b/.changeset/compaction-threshold-boundary.md deleted file mode 100644 index 2e610dbefb0..00000000000 --- a/.changeset/compaction-threshold-boundary.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Prevent configured compaction thresholds from interrupting active tool sequences. diff --git a/.changeset/explain-tool-auto-approval-tui.md b/.changeset/explain-tool-auto-approval-tui.md deleted file mode 100644 index df3e2f5cd03..00000000000 --- a/.changeset/explain-tool-auto-approval-tui.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": minor ---- - -Show why a tool call was auto-approved or denied in the TUI, and record the denial reason on the tool call metadata (visible in `kilo export`) alongside the existing auto-approval reason. diff --git a/.changeset/fair-subagents-route.md b/.changeset/fair-subagents-route.md deleted file mode 100644 index a1d2775fc90..00000000000 --- a/.changeset/fair-subagents-route.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Keep config-defined subagents routable when an installed primary agent uses the same name. diff --git a/.changeset/fast-local-recall.md b/.changeset/fast-local-recall.md deleted file mode 100644 index a2cc81e647c..00000000000 --- a/.changeset/fast-local-recall.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Speed up local session recall searches across large conversation histories. diff --git a/.changeset/faster-macos-speech.md b/.changeset/faster-macos-speech.md deleted file mode 100644 index 2d9bd9cc258..00000000000 --- a/.changeset/faster-macos-speech.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Start voice input faster on macOS with native AVFoundation capture. diff --git a/.changeset/fix-agent-manager-focus.md b/.changeset/fix-agent-manager-focus.md deleted file mode 100644 index f8d5d6c65e3..00000000000 --- a/.changeset/fix-agent-manager-focus.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Restore keyboard focus to the prompt or pending question when switching Agent Manager worktrees and sessions. diff --git a/.changeset/fix-multi-project-navigation.md b/.changeset/fix-multi-project-navigation.md new file mode 100644 index 00000000000..4eddfc5eb17 --- /dev/null +++ b/.changeset/fix-multi-project-navigation.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Fix multi-project Agent Manager keyboard navigation and Cmd/Ctrl shortcut selection when worktrees are grouped in sections. diff --git a/.changeset/fix-multi-project-progress.md b/.changeset/fix-multi-project-progress.md new file mode 100644 index 00000000000..53ccaf57301 --- /dev/null +++ b/.changeset/fix-multi-project-progress.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Fix Agent Manager progress indicators when multiple projects are expanded. diff --git a/.changeset/grep-signal-controls.md b/.changeset/grep-signal-controls.md deleted file mode 100644 index 917e6ed2f59..00000000000 --- a/.changeset/grep-signal-controls.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Add bounded, context-aware signal-to-noise controls to grep searches. diff --git a/.changeset/instant-worktree-hover.md b/.changeset/instant-worktree-hover.md deleted file mode 100644 index e95e1af87ba..00000000000 --- a/.changeset/instant-worktree-hover.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Show and hide Agent Manager worktree hover cards instantly. diff --git a/.changeset/jetbrains-attachment-button-sync.md b/.changeset/jetbrains-attachment-button-sync.md deleted file mode 100644 index dc86abba24e..00000000000 --- a/.changeset/jetbrains-attachment-button-sync.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Keep the JetBrains prompt send/stop button in sync when attachments are added or removed while a session is busy. diff --git a/.changeset/jetbrains-diff-view-fixes.md b/.changeset/jetbrains-diff-view-fixes.md deleted file mode 100644 index 9fc6246c69b..00000000000 --- a/.changeset/jetbrains-diff-view-fixes.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Fix JetBrains diff views to show compact workspace-relative file paths and keep added-file content visible in large branch diffs. diff --git a/.changeset/jetbrains-session-load-crop.md b/.changeset/jetbrains-session-load-crop.md deleted file mode 100644 index 4e515cb3041..00000000000 --- a/.changeset/jetbrains-session-load-crop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Fix JetBrains chat transcripts rendering cropped when opening existing sessions. diff --git a/.changeset/project-local-navigation-hints.md b/.changeset/project-local-navigation-hints.md new file mode 100644 index 00000000000..f6acca80ecd --- /dev/null +++ b/.changeset/project-local-navigation-hints.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Show previous and next navigation hints using each project's own Agent Manager sidebar order. diff --git a/.changeset/prompt-training-indicator-height.md b/.changeset/prompt-training-indicator-height.md deleted file mode 100644 index e1842cbfef4..00000000000 --- a/.changeset/prompt-training-indicator-height.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Keep the prompt controls at a consistent height when the model selector shows the prompt-training indicator. diff --git a/.changeset/quick-model-search.md b/.changeset/quick-model-search.md deleted file mode 100644 index 5b01ee44f97..00000000000 --- a/.changeset/quick-model-search.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Update model search results instantly and keep the active match visible while typing. diff --git a/.changeset/quiet-agent-sections.md b/.changeset/quiet-agent-sections.md deleted file mode 100644 index b6d8d079c04..00000000000 --- a/.changeset/quiet-agent-sections.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Allow Agent Manager sessions to move their worktree between sections or ungroup it through the `agent_manager` tool. diff --git a/.changeset/quiet-metadata-generators.md b/.changeset/quiet-metadata-generators.md deleted file mode 100644 index 67bb87bcbd9..00000000000 --- a/.changeset/quiet-metadata-generators.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Keep Kilo's persona out of generated conversation titles and Agent Manager branch names. diff --git a/.changeset/quiet-speech-shortcut.md b/.changeset/quiet-speech-shortcut.md deleted file mode 100644 index aa18416e3b6..00000000000 --- a/.changeset/quiet-speech-shortcut.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Bind voice input to Cmd/Ctrl+K in Kilo prompt and review comment fields, with hold-to-talk and release-to-send support. diff --git a/.changeset/quiet-terminals-focus.md b/.changeset/quiet-terminals-focus.md deleted file mode 100644 index 22715b263ae..00000000000 --- a/.changeset/quiet-terminals-focus.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Preserve prompt or Agent Manager terminal focus independently for each session when switching sessions. diff --git a/.changeset/quiet-terminals-recover.md b/.changeset/quiet-terminals-recover.md deleted file mode 100644 index 99c87f0de2f..00000000000 --- a/.changeset/quiet-terminals-recover.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Start a fresh shell in the same Agent Manager terminal tab when the user types after the terminal ends. diff --git a/.changeset/remove-project-selection-indicator.md b/.changeset/remove-project-selection-indicator.md new file mode 100644 index 00000000000..50aee2c720f --- /dev/null +++ b/.changeset/remove-project-selection-indicator.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Remove the redundant selected-project indicator from Agent Manager. diff --git a/.changeset/single-agent-manager-empty-state.md b/.changeset/single-agent-manager-empty-state.md new file mode 100644 index 00000000000..f3d9d0d35f6 --- /dev/null +++ b/.changeset/single-agent-manager-empty-state.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Show only one empty-state panel after removing the last Agent Manager worktree. diff --git a/.changeset/skill-shell-inline-docs.md b/.changeset/skill-shell-inline-docs.md deleted file mode 100644 index a0e1e864dfb..00000000000 --- a/.changeset/skill-shell-inline-docs.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Stop treating `` !`cmd` `` shown as an inline code example in skill documentation as a live command, so it no longer triggers a shell permission prompt. diff --git a/.changeset/tui-paste-expand-placeholder.md b/.changeset/tui-paste-expand-placeholder.md deleted file mode 100644 index 07b963ca0bb..00000000000 --- a/.changeset/tui-paste-expand-placeholder.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": minor ---- - -A second identical large paste expands its collapsed prompt placeholder. \ No newline at end of file diff --git a/.changeset/vscode-skills-settings-narrow-overflow.md b/.changeset/vscode-skills-settings-narrow-overflow.md deleted file mode 100644 index bbdb87be496..00000000000 --- a/.changeset/vscode-skills-settings-narrow-overflow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Fix skill folder path and URL rows clipping and pushing the remove (×) button off-screen in narrow Skills settings panels. Long paths and URLs now truncate within their row, and hovering a truncated value shows the full path or URL in a tooltip. diff --git a/.changeset/workflow-model-variants.md b/.changeset/workflow-model-variants.md deleted file mode 100644 index 48b090358ce..00000000000 --- a/.changeset/workflow-model-variants.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@kilocode/cli": minor -"kilo-code": minor ---- - -Configure a model and reasoning variant for each workflow from Agent Behaviour settings. diff --git a/.changeset/worktree-rename-focus.md b/.changeset/worktree-rename-focus.md new file mode 100644 index 00000000000..fef1fe11e23 --- /dev/null +++ b/.changeset/worktree-rename-focus.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Keep multi-project worktree rename inputs focused while selection updates are still settling. diff --git a/bun.lock b/bun.lock index e2b68afdf4f..67811a9f0ef 100644 --- a/bun.lock +++ b/bun.lock @@ -32,7 +32,7 @@ }, "packages/core": { "name": "@opencode-ai/core", - "version": "7.4.18", + "version": "7.4.20", "bin": { "opencode": "./bin/opencode", }, @@ -127,7 +127,7 @@ }, "packages/effect-drizzle-sqlite": { "name": "@opencode-ai/effect-drizzle-sqlite", - "version": "7.4.18", + "version": "7.4.20", "dependencies": { "drizzle-orm": "catalog:", "effect": "catalog:", @@ -141,7 +141,7 @@ }, "packages/effect-sqlite-node": { "name": "@opencode-ai/effect-sqlite-node", - "version": "7.4.18", + "version": "7.4.20", "dependencies": { "effect": "catalog:", }, @@ -153,7 +153,7 @@ }, "packages/http-recorder": { "name": "@opencode-ai/http-recorder", - "version": "7.4.18", + "version": "7.4.20", "dependencies": { "@effect/platform-node": "4.0.0-beta.74", "@effect/platform-node-shared": "4.0.0-beta.74", @@ -174,7 +174,7 @@ }, "packages/kilo-console": { "name": "@kilocode/kilo-console", - "version": "7.4.18", + "version": "7.4.20", "dependencies": { "@kilocode/kilo-indexing": "workspace:*", "@kilocode/kilo-web-ui": "workspace:*", @@ -197,7 +197,7 @@ }, "packages/kilo-docs": { "name": "@kilocode/kilo-docs", - "version": "7.4.18", + "version": "7.4.20", "dependencies": { "@docsearch/css": "^4", "@docsearch/js": "^4", @@ -227,7 +227,7 @@ }, "packages/kilo-gateway": { "name": "@kilocode/kilo-gateway", - "version": "7.4.18", + "version": "7.4.20", "dependencies": { "@ai-sdk/alibaba": "1.0.17", "@ai-sdk/anthropic": "3.0.82", @@ -263,7 +263,7 @@ }, "packages/kilo-i18n": { "name": "@kilocode/kilo-i18n", - "version": "7.4.18", + "version": "7.4.20", "devDependencies": { "@tsconfig/node22": "catalog:", "@types/bun": "catalog:", @@ -273,7 +273,7 @@ }, "packages/kilo-indexing": { "name": "@kilocode/kilo-indexing", - "version": "7.4.18", + "version": "7.4.20", "dependencies": { "@aws-sdk/client-bedrock-runtime": "3.1005.0", "@aws-sdk/credential-provider-ini": "3.972.31", @@ -309,7 +309,7 @@ }, "packages/kilo-memory": { "name": "@kilocode/kilo-memory", - "version": "7.4.18", + "version": "7.4.20", "dependencies": { "effect": "catalog:", "zod": "catalog:", @@ -323,7 +323,7 @@ }, "packages/kilo-sandbox": { "name": "@kilocode/sandbox", - "version": "7.4.18", + "version": "7.4.20", "dependencies": { "@anthropic-ai/sandbox-runtime": "catalog:", "effect": "catalog:", @@ -338,7 +338,7 @@ }, "packages/kilo-telemetry": { "name": "@kilocode/kilo-telemetry", - "version": "7.4.18", + "version": "7.4.20", "dependencies": { "@kilocode/kilo-gateway": "workspace:*", "posthog-node": "4.4.0", @@ -352,7 +352,7 @@ }, "packages/kilo-ui": { "name": "@kilocode/kilo-ui", - "version": "7.4.18", + "version": "7.4.20", "dependencies": { "@kilocode/sdk": "workspace:*", "@kobalte/core": "0.13.11", @@ -390,7 +390,7 @@ }, "packages/kilo-vscode": { "name": "kilo-code", - "version": "7.4.18", + "version": "7.4.20", "dependencies": { "@anthropic-ai/sdk": "^0.39.0", "@kilocode/kilo-gateway": "workspace:*", @@ -459,7 +459,7 @@ }, "packages/kilo-web-ui": { "name": "@kilocode/kilo-web-ui", - "version": "7.4.18", + "version": "7.4.20", "dependencies": { "@kilocode/kilo-ui": "workspace:*", "@kobalte/core": "catalog:", @@ -476,7 +476,7 @@ }, "packages/llm": { "name": "@opencode-ai/llm", - "version": "7.4.18", + "version": "7.4.20", "dependencies": { "@smithy/eventstream-codec": "4.2.14", "@smithy/util-utf8": "4.2.2", @@ -494,7 +494,7 @@ }, "packages/opencode": { "name": "@kilocode/cli", - "version": "7.4.18", + "version": "7.4.20", "bin": { "kilo": "./bin/kilo", "kilocode": "./bin/kilo", @@ -661,7 +661,7 @@ }, "packages/plugin": { "name": "@kilocode/plugin", - "version": "7.4.18", + "version": "7.4.20", "dependencies": { "@kilocode/sdk": "workspace:*", "effect": "catalog:", @@ -689,7 +689,7 @@ }, "packages/plugin-atomic-chat": { "name": "@kilocode/plugin-atomic-chat", - "version": "7.4.18", + "version": "7.4.20", "dependencies": { "@kilocode/plugin": "workspace:*", }, @@ -703,7 +703,7 @@ }, "packages/script": { "name": "@opencode-ai/script", - "version": "7.4.18", + "version": "7.4.20", "dependencies": { "semver": "^7.6.3", }, @@ -714,7 +714,7 @@ }, "packages/sdk/js": { "name": "@kilocode/sdk", - "version": "7.4.18", + "version": "7.4.20", "dependencies": { "cross-spawn": "catalog:", }, @@ -729,7 +729,7 @@ }, "packages/server": { "name": "@opencode-ai/server", - "version": "7.4.18", + "version": "7.4.20", "dependencies": { "@opencode-ai/core": "workspace:*", "drizzle-orm": "catalog:", @@ -743,7 +743,7 @@ }, "packages/storybook": { "name": "@opencode-ai/storybook", - "version": "7.4.18", + "version": "7.4.20", "devDependencies": { "@opencode-ai/ui": "workspace:*", "@solidjs/meta": "catalog:", @@ -766,7 +766,7 @@ }, "packages/tui": { "name": "@opencode-ai/tui", - "version": "7.4.18", + "version": "7.4.20", "dependencies": { "@kilocode/plugin": "workspace:*", "@kilocode/sdk": "workspace:*", @@ -793,7 +793,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "7.4.18", + "version": "7.4.20", "dependencies": { "@kilocode/sdk": "workspace:*", "@kobalte/core": "catalog:", diff --git a/package.json b/package.json index cb7a4a91b07..e55aa0cf85f 100644 --- a/package.json +++ b/package.json @@ -173,6 +173,6 @@ "pacote@21.5.1": "patches/pacote@21.5.1.patch", "mammoth@1.12.0": "patches/mammoth@1.12.0.patch" }, - "version": "7.4.18", + "version": "7.4.20", "peerDependencies": {} } diff --git a/packages/core/package.json b/packages/core/package.json index e07c580a5de..a5e0eb65ff1 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "7.4.18", + "version": "7.4.20", "name": "@opencode-ai/core", "type": "module", "license": "MIT", diff --git a/packages/core/src/kilocode/ripgrep-grep.ts b/packages/core/src/kilocode/ripgrep-grep.ts deleted file mode 100644 index f92cc210f99..00000000000 --- a/packages/core/src/kilocode/ripgrep-grep.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { Match } from "../filesystem/schema" - -export interface Options { - readonly context?: number - readonly literal?: boolean - readonly ignoreCase?: boolean -} - -export type GrepMatch = Match & { - readonly context: boolean - readonly textTruncated: boolean -} - -export const flags = (input: Options) => [ - ...(input.literal ? ["--fixed-strings"] : []), - ...(input.ignoreCase ? ["--ignore-case"] : []), - ...(input.context ? [`--context=${input.context}`] : []), -] - -export const stop = (limit: number) => { - let matches = 0 - return (row: { readonly context: boolean }) => !row.context && ++matches > limit -} - -export const select = < - A extends { - readonly context: boolean - readonly path: { readonly text: string } - readonly line_number: number - }, ->( - input: { readonly limit: number; readonly context?: number }, - items: readonly A[], -) => { - let count = 0 - const overflow = items.findIndex((row) => !row.context && ++count > input.limit) - const selected = items.slice(0, overflow === -1 ? items.length : overflow) - const matches = selected.filter((row) => !row.context) - return selected.filter( - (row) => - !row.context || - matches.some( - (match) => - match.path.text === row.path.text && Math.abs(match.line_number - row.line_number) <= (input.context ?? 0), - ), - ) -} - -export const decorate = (match: Match, context: boolean, textTruncated: boolean): GrepMatch => ({ - ...match, - context, - textTruncated, -}) diff --git a/packages/core/src/ripgrep.ts b/packages/core/src/ripgrep.ts index 5ee202a861d..e1545195606 100644 --- a/packages/core/src/ripgrep.ts +++ b/packages/core/src/ripgrep.ts @@ -6,7 +6,6 @@ import path from "path" import { LayerNode } from "./effect/layer-node" import { Entry, Match } from "./filesystem/schema" import { FSUtil } from "./fs-util" -import * as KiloGrep from "./kilocode/ripgrep-grep" // kilocode_change import * as SpawnValidation from "./kilocode/spawn-validation" // kilocode_change import { AppProcess, collectStream, waitForAbort } from "./process" import { NonNegativeInt, PositiveInt, RelativePath } from "./schema" @@ -24,7 +23,7 @@ const MAX_RECORD_BYTES = 64 * 1024 const MAX_SUBMATCHES = 100 const RawMatch = Schema.Struct({ - type: Schema.Literals(["match", "context"]), // kilocode_change - retain requested context records + type: Schema.Literal("match"), data: Schema.Struct({ path: Schema.Struct({ text: Schema.String }), lines: Schema.Struct({ text: Schema.String }), @@ -40,7 +39,7 @@ const RawMatch = Schema.Struct({ }), }) -type RawMatchData = (typeof RawMatch.Type)["data"] & { readonly context: boolean } // kilocode_change +type RawMatchData = (typeof RawMatch.Type)["data"] export class Error extends Schema.TaggedErrorClass()("Ripgrep.Error", { message: Schema.String, @@ -72,8 +71,7 @@ export interface GlobInput { readonly validate?: Effect.Effect // kilocode_change - bind approved searches at spawn } -export interface GrepInput extends KiloGrep.Options { - // kilocode_change +export interface GrepInput { readonly cwd: string readonly pattern: string readonly file?: string @@ -86,7 +84,7 @@ export interface GrepInput extends KiloGrep.Options { export interface Interface { readonly find: (input: FindInput) => Effect.Effect readonly glob: (input: GlobInput) => Effect.Effect, Error> // kilocode_change - readonly grep: (input: GrepInput) => Effect.Effect, Error | InvalidPatternError> // kilocode_change + readonly grep: (input: GrepInput) => Effect.Effect, Error | InvalidPatternError> // kilocode_change } // kilocode_change start - retain truncation state through model-facing tools @@ -118,7 +116,6 @@ export const layer = Layer.effect( readonly parse: (line: string) => Effect.Effect readonly pattern?: string readonly onItem?: (item: A) => Effect.Effect - readonly stop?: (item: A) => boolean // kilocode_change - stop bounded searches at the overflow match readonly validate?: Effect.Effect // kilocode_change - spawn-bound target validation }) => { const program = Effect.scoped( @@ -139,13 +136,6 @@ export const layer = Layer.effect( Effect.forkScoped, ) let observed = 0 - let stopped = false // kilocode_change - const take = input.stop // kilocode_change start - ? Stream.takeUntil((row) => { - stopped = input.stop?.(row) ?? false - return stopped - }) - : Stream.take(input.limit + 1) // kilocode_change end const rows = yield* Stream.decodeText(handle.stdout).pipe( Stream.splitLines, Stream.filter((line) => line.length > 0), @@ -155,12 +145,11 @@ export const layer = Layer.effect( if (!input.onItem || observed++ >= input.limit) return Effect.void return input.onItem(row) }), - take, // kilocode_change + Stream.take(input.limit + 1), Stream.runCollect, Effect.map((chunk) => [...chunk]), ) - if (stopped) return { items: rows, truncated: true, partial: false } // kilocode_change - const truncated = input.stop ? false : rows.length > input.limit // kilocode_change - custom stop predicates own truncation + const truncated = rows.length > input.limit if (truncated) return { items: rows.slice(0, input.limit), truncated, partial: false } const code = yield* handle.exitCode @@ -260,13 +249,11 @@ export const layer = Layer.effect( grep: (input) => run({ ...input, - stop: KiloGrep.stop(input.limit), // kilocode_change args: [ "--no-config", "--json", "--hidden", "--no-messages", - ...KiloGrep.flags(input), // kilocode_change ...(input.include ? [`--glob=${input.include}`] : []), "--glob=!**/.git/**", "--", @@ -282,19 +269,13 @@ export const layer = Layer.effect( }) ).pipe( Effect.flatMap((json) => { - if ( - !json || - typeof json !== "object" || - !("type" in json) || - (json.type !== "match" && json.type !== "context") // kilocode_change - ) + if (!json || typeof json !== "object" || !("type" in json) || json.type !== "match") return Effect.succeed(undefined) return Schema.decodeUnknownEffect(RawMatch)(json).pipe( Effect.map((match) => ({ ...match.data, path: { text: match.data.path.text.replace(/^\.[\\/]/, "") }, submatches: match.data.submatches.slice(0, MAX_SUBMATCHES), - context: match.type === "context", // kilocode_change })), Effect.mapError((cause) => failure("Invalid ripgrep match output", cause)), ) @@ -304,13 +285,13 @@ export const layer = Layer.effect( // kilocode_change start - retain spawn metadata after mapping matches Effect.map((result) => ({ ...result, - items: KiloGrep.select(input, result.items).map((match) => { + items: result.items.map((match) => { const relative = match.path.text .replace(/^(?:\.[\\/])+/u, "") .replace(/^[\\/]+/u, "") .replaceAll("\\", "/") const absolute = path.resolve(input.cwd, relative) - const item = new Match({ + return new Match({ entry: new Entry({ path: RelativePath.make(relative), type: "file", @@ -325,7 +306,6 @@ export const layer = Layer.effect( end: submatch.end, })), }) - return KiloGrep.decorate(item, match.context, match.lines.text.length > 2_000) }), })), // kilocode_change end diff --git a/packages/core/src/v1/config/permission.ts b/packages/core/src/v1/config/permission.ts index 36d2468bbec..6d6caa02abd 100644 --- a/packages/core/src/v1/config/permission.ts +++ b/packages/core/src/v1/config/permission.ts @@ -25,6 +25,7 @@ const InputObject = Schema.StructWithRest( bash: Schema.optional(Rule), task: Schema.optional(Rule), external_directory: Schema.optional(Rule), + markdown_source: Schema.optional(Rule), // kilocode_change - explicitly authorize external agent/command sources todowrite: Schema.optional(Action), question: Schema.optional(Action), webfetch: Schema.optional(Action), diff --git a/packages/core/test/pty/pty-session.test.ts b/packages/core/test/pty/pty-session.test.ts index f74abf88a80..a238ffb579c 100644 --- a/packages/core/test/pty/pty-session.test.ts +++ b/packages/core/test/pty/pty-session.test.ts @@ -80,17 +80,19 @@ const attachCollecting = Effect.fn("PtySessionTest.attachCollecting")(function* return { attachment, output, ended } }) -const waitForOutput = (output: Queue.Queue, text: string) => - Effect.gen(function* () { - let received = "" +const waitForOutput = (output: Queue.Queue, text: string) => { + let received = "" + const pull = Effect.gen(function* () { while (!received.includes(text)) received += yield* Queue.take(output) return received - }).pipe( + }) + return pull.pipe( Effect.timeoutOrElse({ duration: PTY_TEST_TIMEOUT, // kilocode_change - orElse: () => Effect.fail(new Error(`timeout waiting for output containing ${JSON.stringify(text)}`)), + orElse: () => Effect.fail(new Error(`timeout waiting for output containing ${JSON.stringify(text)}, received ${JSON.stringify(received)}`)), }), ) +} describe("pty", () => { it.live("returns typed not found errors for missing sessions", () => @@ -143,11 +145,13 @@ describe("pty", () => { ) // (script terminals forward raw output to xterm without transcoding). + // The child must outlive its output: an immediate exit can race bun-pty's + // reader thread and drop trailing bytes under load, so print then sleep. ptyTest("round-trips non-ASCII output byte-identically", () => Effect.gen(function* () { const pty = yield* Pty.Service const marker = "café-über-北京-🚀" - const info = yield* createPty("sh", ["-c", "printf 'caf\\303\\251-\\303\\274ber-\\345\\214\\227\\344\\272\\254-\\360\\237\\232\\200\\n'"]) + const info = yield* createPty("sh", ["-c", "printf 'caf\\303\\251-\\303\\274ber-\\345\\214\\227\\344\\272\\254-\\360\\237\\232\\200\\n'; sleep 5"]) const attached = yield* attachCollecting(info.id) expect(yield* waitForOutput(attached.output, marker)).toContain(marker) }), diff --git a/packages/effect-drizzle-sqlite/package.json b/packages/effect-drizzle-sqlite/package.json index 7303d76d7ca..907db1f9a39 100644 --- a/packages/effect-drizzle-sqlite/package.json +++ b/packages/effect-drizzle-sqlite/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "7.4.18", + "version": "7.4.20", "name": "@opencode-ai/effect-drizzle-sqlite", "type": "module", "license": "MIT", diff --git a/packages/effect-sqlite-node/package.json b/packages/effect-sqlite-node/package.json index 2b297b5ecb3..4af05664a09 100644 --- a/packages/effect-sqlite-node/package.json +++ b/packages/effect-sqlite-node/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "7.4.18", + "version": "7.4.20", "name": "@opencode-ai/effect-sqlite-node", "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 5321bca1b32..50fe224ed2a 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "kilo" name = "Kilo" description = "The open source coding agent." -version = "7.4.18" +version = "7.4.20" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/Kilo-Org/kilocode" @@ -11,26 +11,26 @@ name = "Kilo" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.18/opencode-darwin-arm64.zip" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.20/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.18/opencode-darwin-x64.zip" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.20/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.18/opencode-linux-arm64.tar.gz" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.20/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.18/opencode-linux-x64.tar.gz" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.20/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.18/opencode-windows-x64.zip" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.20/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/http-recorder/package.json b/packages/http-recorder/package.json index ff408eb4302..e931dce3e17 100644 --- a/packages/http-recorder/package.json +++ b/packages/http-recorder/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "7.4.18", + "version": "7.4.20", "name": "@opencode-ai/http-recorder", "description": "Record and replay Effect HTTP client traffic with deterministic cassettes", "type": "module", diff --git a/packages/kilo-console/package.json b/packages/kilo-console/package.json index c9097a403ce..2a024ace453 100755 --- a/packages/kilo-console/package.json +++ b/packages/kilo-console/package.json @@ -1,6 +1,6 @@ { "name": "@kilocode/kilo-console", - "version": "7.4.18", + "version": "7.4.20", "private": true, "type": "module", "scripts": { diff --git a/packages/kilo-docs/package.json b/packages/kilo-docs/package.json index 5f296eedeaf..f1c085a55f5 100644 --- a/packages/kilo-docs/package.json +++ b/packages/kilo-docs/package.json @@ -1,6 +1,6 @@ { "name": "@kilocode/kilo-docs", - "version": "7.4.18", + "version": "7.4.20", "private": true, "scripts": { "dev": "next dev --webpack --port 3002", diff --git a/packages/kilo-docs/pages/customize/agents-md.md b/packages/kilo-docs/pages/customize/agents-md.md index 88d231e4ec0..4f2a58235c7 100644 --- a/packages/kilo-docs/pages/customize/agents-md.md +++ b/packages/kilo-docs/pages/customize/agents-md.md @@ -18,7 +18,7 @@ If you'd like to migrate your memory bank content to AGENTS.md: 1. Examine the contents in `.kilocode/rules/memory-bank/` 2. Move that content into your project's `AGENTS.md` file (or ask Kilo to do it for you) - {% /callout %} +{% /callout %} ## What is AGENTS.md? diff --git a/packages/kilo-docs/pages/customize/custom-subagents.md b/packages/kilo-docs/pages/customize/custom-subagents.md index 99c889db7f5..5c00ea8d77c 100644 --- a/packages/kilo-docs/pages/customize/custom-subagents.md +++ b/packages/kilo-docs/pages/customize/custom-subagents.md @@ -93,6 +93,20 @@ Define agents as markdown files with YAML frontmatter. Place them in: The **filename** (without `.md`) becomes the agent name. +If `.kilo/agents/` is a symlink to a directory outside the project, allow that exact source in your global `~/.config/kilo/kilo.jsonc`: + +```jsonc +{ + "permission": { + "markdown_source": { + "/path/to/shared/agents/*": "allow" + } + } +} +``` + +Project configuration cannot grant this permission. External agent files remain untrusted: `{env:...}` substitutions are blocked and `{file:...}` substitutions remain confined to the project. + ```markdown --- description: Reviews code for quality and best practices diff --git a/packages/kilo-docs/pages/customize/workflows.md b/packages/kilo-docs/pages/customize/workflows.md index cb55800c87d..9d6da762ff3 100644 --- a/packages/kilo-docs/pages/customize/workflows.md +++ b/packages/kilo-docs/pages/customize/workflows.md @@ -17,6 +17,20 @@ Workflows are Markdown files stored as **slash commands** in `.kilo/commands/`: - **Global commands**: `~/.config/kilo/commands/` (available in all projects) - **Project commands**: `[project]/.kilo/commands/` (project-specific) +If `.kilo/commands/` is a symlink to a directory outside the project, allow that exact source in your global `~/.config/kilo/kilo.jsonc`: + +```jsonc +{ + "permission": { + "markdown_source": { + "/path/to/shared/commands/*": "allow" + } + } +} +``` + +Project configuration cannot grant this permission. External command files remain untrusted: `{env:...}` substitutions are blocked and `{file:...}` substitutions remain confined to the project. + ### Basic Setup 1. Create a `.md` file with step-by-step instructions diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/labs-tool-call-lab/search-previews-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/labs-tool-call-lab/search-previews-chromium-linux.png index 6a4e3e8ba0b..e571fdb6845 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/labs-tool-call-lab/search-previews-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/labs-tool-call-lab/search-previews-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fd794db4d2c038984f9e753ba70b8b0473d97a16713a9ff7255f0d2cf505d41a -size 758306 +oid sha256:033960b68a915d0b05ce539784998d0f84603f206689dbbd14005771d112faa4 +size 762429 diff --git a/packages/kilo-gateway/package.json b/packages/kilo-gateway/package.json index e55bbbdd5df..bb5e6796df1 100644 --- a/packages/kilo-gateway/package.json +++ b/packages/kilo-gateway/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/kilo-gateway", - "version": "7.4.18", + "version": "7.4.20", "type": "module", "license": "MIT", "description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration", diff --git a/packages/kilo-gateway/src/api/models.ts b/packages/kilo-gateway/src/api/models.ts index d0bb964ca51..8188c1f825b 100644 --- a/packages/kilo-gateway/src/api/models.ts +++ b/packages/kilo-gateway/src/api/models.ts @@ -1,6 +1,7 @@ import { z } from "zod" import { getKiloUrlFromToken } from "../auth/token.js" import { getDefaultHeaders, buildKiloHeaders } from "../headers.js" +import { resolveKiloGatewayBaseUrl } from "./url.js" import { KILO_API_BASE, KILO_OPENROUTER_BASE, MODELS_FETCH_TIMEOUT_MS, PROMPTS, AI_SDK_PROVIDERS } from "./constants.js" export type KiloModelsResult = { @@ -121,6 +122,16 @@ export type KiloImageModelsResult = { error?: { kind: "unauthorized" | "network" | "schema" | "http"; status?: number } } +export type KiloTranscriptionModel = { + id: string + name: string +} + +export type KiloTranscriptionModelsResult = { + models: KiloTranscriptionModel[] + error?: { kind: "unauthorized" | "network" | "schema" | "http"; status?: number } +} + /** * Fetch image-capable models from Kilo API (OpenRouter-compatible endpoint). * Uses the same raw fetch as {@link fetchKiloModels} but keeps only models @@ -145,6 +156,52 @@ export async function fetchKiloImageModels(options?: { return { models } } +export async function fetchKiloTranscriptionModels(options?: { + kilocodeToken?: string + kilocodeOrganizationId?: string + baseURL?: string +}): Promise { + const token = options?.kilocodeToken + const organizationId = options?.kilocodeOrganizationId + const url = new URL("transcription-models", resolveKiloGatewayBaseUrl({ baseURL: options?.baseURL, token })) + const response = await fetch(url, { + headers: { + ...getDefaultHeaders(), + ...buildKiloHeaders(undefined, { kilocodeOrganizationId: organizationId }), + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }, + signal: AbortSignal.timeout(MODELS_FETCH_TIMEOUT_MS), + }).catch((err: unknown) => err as Error) + + if (response instanceof Error) return { models: [], error: { kind: "network" } } + if (!response.ok) { + const kind = response.status === 401 || response.status === 403 ? "unauthorized" : "http" + return { models: [], error: { kind, status: response.status } } + } + + const json = await response.json().catch(() => null) + if (!json || !Array.isArray(json.data)) return { models: [], error: { kind: "schema" } } + + const data: unknown[] = json.data + const models = data.filter(isTranscriptionModel).map((model) => ({ + id: model.id, + name: model.name, + })) + if (models.length === 0) return { models: [], error: { kind: "schema" } } + return { models } +} + +type TranscriptionModelResponse = { + id: string + name: string +} + +function isTranscriptionModel(value: unknown): value is TranscriptionModelResponse { + if (!value || typeof value !== "object") return false + const model = value as Record + return typeof model.id === "string" && typeof model.name === "string" +} + /** * Shared raw fetch + validate used by both {@link fetchKiloModels} and {@link fetchKiloImageModels}. */ diff --git a/packages/kilo-gateway/src/index.ts b/packages/kilo-gateway/src/index.ts index cee10c2f781..4df0e8a7681 100644 --- a/packages/kilo-gateway/src/index.ts +++ b/packages/kilo-gateway/src/index.ts @@ -41,6 +41,9 @@ export { fetchKiloImageModels, type KiloImageModel, type KiloImageModelsResult, + fetchKiloTranscriptionModels, + type KiloTranscriptionModel, + type KiloTranscriptionModelsResult, } from "./api/models.js" export { EMPTY_KILO_EMBEDDING_MODEL_CATALOG, diff --git a/packages/kilo-gateway/test/api/models.test.ts b/packages/kilo-gateway/test/api/models.test.ts index 656d27eaf05..0833ec11f30 100644 --- a/packages/kilo-gateway/test/api/models.test.ts +++ b/packages/kilo-gateway/test/api/models.test.ts @@ -1,7 +1,7 @@ // Verifies fetchKiloModels typed result and 401 fallback behaviour. import { test, expect } from "bun:test" -import { fetchKiloModels } from "../../src/api/models.js" +import { fetchKiloModels, fetchKiloTranscriptionModels } from "../../src/api/models.js" const VALID_RESPONSE = JSON.stringify({ data: [ @@ -330,3 +330,56 @@ test("keeps image-output models with tools and drops models without tools", asyn expect(result.models["test/model-a"]).toBeDefined() expect(result.models["test/no-tools"]).toBeUndefined() }) + +test("fetches and filters the transcription catalog", async () => { + const orig = globalThis.fetch + const calls: string[] = [] + stubFetch(async (input) => { + calls.push(String(input)) + return new Response( + JSON.stringify({ + data: [ + { + id: "fish-audio/transcribe-1", + name: "Fish Audio: Transcribe 1", + }, + ], + }), + { status: 200, headers: { "content-type": "application/json" } }, + ) + }) + + const result = await fetchKiloTranscriptionModels({ kilocodeToken: "token" }) + + ;(globalThis as any).fetch = orig + + expect(result.error).toBeUndefined() + expect(result.models).toEqual([ + { + id: "fish-audio/transcribe-1", + name: "Fish Audio: Transcribe 1", + }, + ]) + expect(calls[0]).toContain("/api/gateway/transcription-models") +}) + +test("keeps organization catalog errors from silently falling back to personal models", async () => { + const orig = globalThis.fetch + const calls: string[] = [] + const headers: Headers[] = [] + stubFetch(async (input, init) => { + calls.push(String(input)) + headers.push(new Headers(init?.headers)) + return new Response("Forbidden", { status: 403 }) + }) + + const result = await fetchKiloTranscriptionModels({ kilocodeToken: "token", kilocodeOrganizationId: "org-1" }) + + ;(globalThis as any).fetch = orig + + expect(result.models).toEqual([]) + expect(result.error?.kind).toBe("unauthorized") + expect(calls).toHaveLength(1) + expect(calls[0]).toContain("/api/gateway/transcription-models") + expect(headers[0]?.get("X-KILOCODE-ORGANIZATIONID")).toBe("org-1") +}) diff --git a/packages/kilo-i18n/package.json b/packages/kilo-i18n/package.json index b74de939f7d..ec845e49b51 100644 --- a/packages/kilo-i18n/package.json +++ b/packages/kilo-i18n/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/kilo-i18n", - "version": "7.4.18", + "version": "7.4.20", "type": "module", "license": "MIT", "description": "Kilo-specific i18n translations and overrides", diff --git a/packages/kilo-indexing/package.json b/packages/kilo-indexing/package.json index 7f2e39eea54..aab1178e92f 100644 --- a/packages/kilo-indexing/package.json +++ b/packages/kilo-indexing/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/kilo-indexing", - "version": "7.4.18", + "version": "7.4.20", "type": "module", "license": "MIT", "description": "Standalone indexing engine and host helpers for Kilo Code", diff --git a/packages/kilo-jetbrains/CHANGELOG.md b/packages/kilo-jetbrains/CHANGELOG.md index 58f12c85b35..d000b8245ae 100644 --- a/packages/kilo-jetbrains/CHANGELOG.md +++ b/packages/kilo-jetbrains/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 7.4.18 + +### Patch Changes + +- [#12746](https://github.com/Kilo-Org/kilocode/pull/12746) [`1a506a7`](https://github.com/Kilo-Org/kilocode/commit/1a506a712c43d317a5a34b250df16845b641eff8) - Keep the JetBrains prompt send/stop button in sync when attachments are added or removed while a session is busy. + +- [#12746](https://github.com/Kilo-Org/kilocode/pull/12746) [`64f0373`](https://github.com/Kilo-Org/kilocode/commit/64f0373056b75546a015816dc0f18b1e380ad93f) - Fix JetBrains diff views to show compact workspace-relative file paths and keep added-file content visible in large branch diffs. + +- [#12746](https://github.com/Kilo-Org/kilocode/pull/12746) [`c1f6a75`](https://github.com/Kilo-Org/kilocode/commit/c1f6a75377b438edfc5c3b5dd85ebdc301302e7a) - Fix JetBrains chat transcripts rendering cropped when opening existing sessions. + ## 7.5.0 ### Minor Changes @@ -150,6 +160,7 @@ ## [7.0.12-rc.4] - 2026-08-01 ### Fixed + - Improve large branch diff performance by capping huge inline diff previews, compacting diff tree paths, and allowing horizontal scrolling for long file names. - Reflow existing long chat sessions after they load so transcripts lay out at the correct width without needing to resize the tool window. - Keep the prompt send/stop button synchronized when attachments are added, removed, or cleared while a session is busy. diff --git a/packages/kilo-memory/package.json b/packages/kilo-memory/package.json index 4989cfc0f2c..24b2e775f62 100644 --- a/packages/kilo-memory/package.json +++ b/packages/kilo-memory/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/kilo-memory", - "version": "7.4.18", + "version": "7.4.20", "type": "module", "license": "MIT", "description": "Project memory storage, indexing, recall, and command helpers for Kilo Code", diff --git a/packages/kilo-sandbox/package.json b/packages/kilo-sandbox/package.json index 1355fc2e076..0f992f992ab 100644 --- a/packages/kilo-sandbox/package.json +++ b/packages/kilo-sandbox/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/sandbox", - "version": "7.4.18", + "version": "7.4.20", "type": "module", "license": "MIT", "private": true, diff --git a/packages/kilo-telemetry/package.json b/packages/kilo-telemetry/package.json index 63a01315655..4ce691dbb55 100644 --- a/packages/kilo-telemetry/package.json +++ b/packages/kilo-telemetry/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/kilo-telemetry", - "version": "7.4.18", + "version": "7.4.20", "type": "module", "license": "MIT", "description": "Telemetry for Kilo CLI - PostHog analytics integration", diff --git a/packages/kilo-telemetry/src/__tests__/telemetry.test.ts b/packages/kilo-telemetry/src/__tests__/telemetry.test.ts index 939bb5dd243..c86a249f51f 100644 --- a/packages/kilo-telemetry/src/__tests__/telemetry.test.ts +++ b/packages/kilo-telemetry/src/__tests__/telemetry.test.ts @@ -85,6 +85,19 @@ describe("TelemetryEvent", () => { }) describe("Telemetry", () => { + test("skips identity updates when disabled", async () => { + const enabled = spyOn(Client, "isEnabled").mockReturnValue(false) + const update = spyOn(Identity, "updateFromKiloAuth").mockResolvedValue() + + try { + await Telemetry.updateIdentity("token") + expect(update).not.toHaveBeenCalled() + } finally { + enabled.mockRestore() + update.mockRestore() + } + }) + test("includes host OS properties", () => { const capture = spyOn(Client, "capture").mockImplementation(() => {}) diff --git a/packages/kilo-telemetry/src/telemetry.ts b/packages/kilo-telemetry/src/telemetry.ts index 496bf1ad4c9..9e819e25519 100644 --- a/packages/kilo-telemetry/src/telemetry.ts +++ b/packages/kilo-telemetry/src/telemetry.ts @@ -105,6 +105,8 @@ export namespace Telemetry { } export async function updateIdentity(token: string | null, accountId?: string): Promise { + if (!isEnabled()) return + const previousId = Identity.getDistinctId() await Identity.updateFromKiloAuth(token, accountId) diff --git a/packages/kilo-ui/package.json b/packages/kilo-ui/package.json index 8724dc548f9..6948cbadbac 100644 --- a/packages/kilo-ui/package.json +++ b/packages/kilo-ui/package.json @@ -1,6 +1,6 @@ { "name": "@kilocode/kilo-ui", - "version": "7.4.18", + "version": "7.4.20", "type": "module", "license": "MIT", "exports": { diff --git a/packages/kilo-ui/src/components/message-part.tsx b/packages/kilo-ui/src/components/message-part.tsx index 0a61e75f6c6..4281174d125 100644 --- a/packages/kilo-ui/src/components/message-part.tsx +++ b/packages/kilo-ui/src/components/message-part.tsx @@ -2799,18 +2799,9 @@ ToolRegistry.register({ > {(file) => { - const active = createMemo(() => expanded().includes(file.filePath)) - const [visible, setVisible] = createSignal(false) - createEffect(() => { - if (!active()) { - setVisible(false) - return - } - requestAnimationFrame(() => { - if (!active()) return - setVisible(true) - }) - }) + // Diff defers its own expensive render; mounting the container + // here avoids dropping the last item during batch expansion. + const active = createMemo(() => allExpanded().includes(file.filePath)) return ( @@ -2864,7 +2855,7 @@ ToolRegistry.register({ - + {(diff) => (
this.handleExportSessionTranscript(sessionID), copy: (text) => vscode.env.clipboard.writeText(text), openSessions: (ids) => this.trackOpenSessions(ids), + speechToTextModels: () => this.fetchAndSendSpeechToTextModels(), }) ) { return @@ -2701,6 +2704,15 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.postMessage(message) } + private async fetchAndSendSpeechToTextModels(): Promise { + const result = await fetchSpeechToTextModels(this.connectionService, this.getWorkspaceDirectory()) + if (!result.ok) { + this.postMessage({ type: "speechToTextModelsLoaded" as const, models: [...SPEECH_TO_TEXT_MODELS] }) + return + } + this.postMessage({ type: "speechToTextModelsLoaded" as const, models: result.models }) + } + /** * Seed sessionStatusMap with current session statuses on connect. * Without this, the Settings panel (which has no tracked sessions) would see @@ -3944,6 +3956,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper disposeGlobal: () => this.disposeGlobal(), fetchAndSendProviders: () => this.fetchAndSendProviders(), fetchAndSendAgents: () => this.fetchAndSendAgents(), + fetchAndSendSpeechToTextModels: () => this.fetchAndSendSpeechToTextModels(), } } diff --git a/packages/kilo-vscode/src/agent-manager/provider-multi-version.ts b/packages/kilo-vscode/src/agent-manager/provider-multi-version.ts index 37a9fe93a14..41cc02049eb 100644 --- a/packages/kilo-vscode/src/agent-manager/provider-multi-version.ts +++ b/packages/kilo-vscode/src/agent-manager/provider-multi-version.ts @@ -50,6 +50,7 @@ export async function createMultiVersion( // Notify webview that multi-version creation has started host.post({ type: "agentManager.multiVersionProgress", + projectId: ctx.id, status: "creating", total: versions, completed: 0, @@ -78,6 +79,7 @@ export async function createMultiVersion( // Update progress host.post({ type: "agentManager.multiVersionProgress", + projectId: ctx.id, status: "creating", total: versions, completed: created.length, @@ -86,11 +88,19 @@ export async function createMultiVersion( } // Phase 2: Send the initial prompt to all sessions, or clear busy state if no text. - await sendInitialPrompts(host, created, models, { providerID, modelID }, { text, agent, variant: msg.variant, files }) + await sendInitialPrompts( + host, + ctx.id, + created, + models, + { providerID, modelID }, + { text, agent, variant: msg.variant, files }, + ) // Notify completion host.post({ type: "agentManager.multiVersionProgress", + projectId: ctx.id, status: "done", total: versions, completed: created.length, @@ -172,6 +182,7 @@ async function createVersion( if (earlyProviderID && earlyModelID) { host.post({ type: "agentManager.setSessionModel", + projectId: ctx.id, sessionId: session.id, providerID: earlyProviderID, modelID: earlyModelID, @@ -232,6 +243,7 @@ async function reconcileSandbox( /** Fan the initial prompt out to every created session, throttled between sends. */ async function sendInitialPrompts( host: MultiVersionHost, + projectId: string, created: CreatedVersion[], models: VersionSpec["models"], resolved: { providerID: string | undefined; modelID: string | undefined }, @@ -254,7 +266,7 @@ async function sendInitialPrompts( modelID: msg.modelID, }) } - host.post({ type: "agentManager.sendInitialMessage", ...msg }) + host.post({ type: "agentManager.sendInitialMessage", projectId, ...msg }) if (input.text && i < messages.length - 1) { await new Promise((resolve) => setTimeout(resolve, 300)) } diff --git a/packages/kilo-vscode/src/agent-manager/types.ts b/packages/kilo-vscode/src/agent-manager/types.ts index 3a2b555e355..5f08325a9fe 100644 --- a/packages/kilo-vscode/src/agent-manager/types.ts +++ b/packages/kilo-vscode/src/agent-manager/types.ts @@ -258,6 +258,8 @@ interface SessionClosedMessage { interface MultiVersionProgressMessage { type: "agentManager.multiVersionProgress" + /** Owning project; absent in single-project mode. */ + projectId?: string status: "creating" | "done" total: number completed: number @@ -266,6 +268,8 @@ interface MultiVersionProgressMessage { interface SetSessionModelMessage { type: "agentManager.setSessionModel" + /** Owning project; absent in single-project mode. */ + projectId?: string sessionId: string providerID: string modelID: string @@ -273,6 +277,8 @@ interface SetSessionModelMessage { interface SendInitialMessage { type: "agentManager.sendInitialMessage" + /** Owning project; absent in single-project mode. */ + projectId?: string sessionId: string worktreeId: string text?: string @@ -791,6 +797,7 @@ interface FileSourceIn { interface SendMessageIn { type: "sendMessage" + projectId?: string text: string messageID?: string sessionID?: string diff --git a/packages/kilo-vscode/src/kilo-provider/early-message.ts b/packages/kilo-vscode/src/kilo-provider/early-message.ts index f67a2144fa1..abf79b49a02 100644 --- a/packages/kilo-vscode/src/kilo-provider/early-message.ts +++ b/packages/kilo-vscode/src/kilo-provider/early-message.ts @@ -17,6 +17,7 @@ type Ctx = { exportTranscript: (sessionID: string) => Promise copy: (text: string) => PromiseLike openSessions: (ids: string[]) => void + speechToTextModels: () => Promise } export async function routeEarlyMessage( @@ -64,6 +65,10 @@ export async function routeEarlyMessage( ctx.post(buildThroughputSettingMessage()) return true } + if (message.type === "requestSpeechToTextModels") { + await ctx.speechToTextModels() + return true + } if (message.type === "requestBrowserSettings") { ctx.browserSettings() return true diff --git a/packages/kilo-vscode/src/kilo-provider/handlers/auth.ts b/packages/kilo-vscode/src/kilo-provider/handlers/auth.ts index db6e9e6d7df..07ae712b902 100644 --- a/packages/kilo-vscode/src/kilo-provider/handlers/auth.ts +++ b/packages/kilo-vscode/src/kilo-provider/handlers/auth.ts @@ -15,6 +15,7 @@ export interface AuthContext { disposeGlobal(): Promise fetchAndSendProviders(): Promise fetchAndSendAgents(): Promise + fetchAndSendSpeechToTextModels(): Promise } /** @@ -137,6 +138,11 @@ export async function handleSetOrganization(ctx: AuthContext, organizationId: st } catch (error) { console.error("[Kilo New] KiloProvider: Failed to refresh agents after org switch:", error) } + try { + await ctx.fetchAndSendSpeechToTextModels() + } catch (error) { + console.error("[Kilo New] KiloProvider: Failed to refresh speech-to-text models after org switch:", error) + } } /** Handle profile refresh request. */ diff --git a/packages/kilo-vscode/src/speech-to-text/catalog.ts b/packages/kilo-vscode/src/speech-to-text/catalog.ts new file mode 100644 index 00000000000..fbb24114f74 --- /dev/null +++ b/packages/kilo-vscode/src/speech-to-text/catalog.ts @@ -0,0 +1,62 @@ +import type { KiloConnectionService } from "../services/cli-backend/connection-service" +import { getErrorMessage } from "../kilo-provider-utils" +import { type SpeechToTextModelDef } from "./models" + +const PATH = "/kilo/models/transcriptions" + +type CatalogModel = { + id: string + name: string +} + +export type SpeechToTextCatalogResult = { ok: true; models: SpeechToTextModelDef[] } | { ok: false; error: string } + +export async function fetchSpeechToTextModels( + connection: KiloConnectionService, + dir: string, + signal?: AbortSignal, +): Promise { + const cfg = connection.getServerConfig() + if (!cfg) return fail("Not connected to the Kilo backend") + + const auth = Buffer.from(`kilo:${cfg.password}`).toString("base64") + const url = new URL(PATH, cfg.baseUrl) + if (dir) url.searchParams.set("directory", dir) + + try { + const res = await fetch(url, { signal, headers: { Authorization: `Basic ${auth}` } }) + if (!res.ok) return fail(`Failed to fetch speech-to-text models (HTTP ${res.status})`) + + const models = parseSpeechToTextCatalog(await res.json()) + if (!models) return fail("Invalid speech-to-text model catalog") + return { ok: true, models } + } catch (err) { + return fail(getErrorMessage(err)) + } +} + +function fail(error: string): SpeechToTextCatalogResult { + return { ok: false, error } +} + +export function parseSpeechToTextCatalog(body: unknown): SpeechToTextModelDef[] | undefined { + if (!Array.isArray(body)) return undefined + const models = body.filter(isCatalogModel).map(toModel) + return models.length > 0 ? models : undefined +} + +function isCatalogModel(value: unknown): value is CatalogModel { + if (!value || typeof value !== "object") return false + const model = value as Record + return typeof model.id === "string" && typeof model.name === "string" +} + +function toModel(model: CatalogModel): SpeechToTextModelDef { + const index = model.name.indexOf(":") + const provider = index === -1 ? model.id.split("/", 1)[0] || "Kilo Gateway" : model.name.slice(0, index).trim() + return { + id: model.id, + label: index === -1 ? model.name : model.name.slice(index + 1).trim(), + provider, + } +} diff --git a/packages/kilo-vscode/src/speech-to-text/transcribe.ts b/packages/kilo-vscode/src/speech-to-text/transcribe.ts index 1e978bf028c..bfa0daf590f 100644 --- a/packages/kilo-vscode/src/speech-to-text/transcribe.ts +++ b/packages/kilo-vscode/src/speech-to-text/transcribe.ts @@ -54,7 +54,7 @@ export async function transcribeSpeech( "Content-Type": "application/json", }, body: JSON.stringify({ - model: model.id, + model: input.model || model.id, input_audio: { data: input.data, format: input.format, diff --git a/packages/kilo-vscode/tests/package.json b/packages/kilo-vscode/tests/package.json index 0219e54e36c..9b3ef8d5815 100644 --- a/packages/kilo-vscode/tests/package.json +++ b/packages/kilo-vscode/tests/package.json @@ -1,6 +1,6 @@ { "type": "module", - "version": "7.4.18", + "version": "7.4.20", "dependencies": {}, "devDependencies": {}, "peerDependencies": {} diff --git a/packages/kilo-vscode/tests/unit/agent-manager-focus.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-focus.test.ts index 1c63c08b4fa..c5c33eea67f 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-focus.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-focus.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "bun:test" import { Window } from "happy-dom" -import { focusQuestionOption, hasQuestionOption } from "../../webview-ui/agent-manager/focus" +import { focusQuestionOption, hasQuestionOption, preservesTextFocus } from "../../webview-ui/agent-manager/focus" +import { isTextControl } from "../../webview-ui/src/utils/focus" describe("Agent Manager focus", () => { it("focuses the first enabled question option", () => { @@ -53,4 +54,21 @@ describe("Agent Manager focus", () => { dock.setAttribute("inert", "") expect(hasQuestionOption(root)).toBe(false) }) + + it("preserves focus for an active editable control", () => { + const window = new Window() + const rename = window.document.createElement("input") + rename.className = "am-worktree-rename-input" + const prompt = window.document.createElement("textarea") + prompt.className = "prompt-input" + const editor = window.document.createElement("div") + editor.contentEditable = "plaintext-only" + const button = window.document.createElement("button") + + expect(isTextControl(rename)).toBe(true) + expect(preservesTextFocus(rename)).toBe(true) + expect(preservesTextFocus(prompt)).toBe(false) + expect(isTextControl(editor)).toBe(true) + expect(isTextControl(button)).toBe(false) + }) }) diff --git a/packages/kilo-vscode/tests/unit/agent-manager-initial-message.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-initial-message.test.ts index cba84607892..87714a5cac6 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-initial-message.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-initial-message.test.ts @@ -5,6 +5,7 @@ describe("Agent Manager initial message", () => { it("forwards the selected variant to sendMessage", () => { const msg = initialMessage({ type: "agentManager.sendInitialMessage", + projectId: "project-a", sessionId: "session-a", worktreeId: "wt-a", text: "Fix it", @@ -16,6 +17,7 @@ describe("Agent Manager initial message", () => { expect(msg).toEqual({ type: "sendMessage", + projectId: "project-a", text: "Fix it", sessionID: "session-a", providerID: "anthropic", diff --git a/packages/kilo-vscode/tests/unit/agent-project-progress.test.ts b/packages/kilo-vscode/tests/unit/agent-project-progress.test.ts new file mode 100644 index 00000000000..21965da9522 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/agent-project-progress.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from "bun:test" +import { createProjectStore } from "../../webview-ui/agent-manager/project/store" +import { clearMultiVersionBusy, markMultiVersionBusy } from "../../webview-ui/agent-manager/project/progress" + +const state = (projectId: string) => ({ + type: "agentManager.state" as const, + projectId, + worktrees: [ + { + id: "same", + branch: `${projectId}-same`, + path: `/repo/${projectId}/same`, + parentBranch: "main", + createdAt: "2026-01-01", + groupId: "group", + }, + ], + sessions: [{ id: `${projectId}-session`, worktreeId: "same", createdAt: "2026-01-01" }], + sections: [], +}) + +describe("multi-project progress state", () => { + it("updates only the owning project's grouped worktrees", () => { + const first = createProjectStore("a") + const second = createProjectStore("b") + first.applyState(state("a")) + second.applyState(state("b")) + first.setBusy(new Map([["same", { reason: "setting-up" as const }]])) + second.setBusy(new Map([["same", { reason: "setting-up" as const }]])) + + clearMultiVersionBusy(second, "group") + + expect(first.busy().has("same")).toBe(true) + expect(second.busy().has("same")).toBe(false) + }) + + it("marks a newly created grouped worktree as busy in its project store", () => { + const store = createProjectStore("a") + store.applyState(state("a")) + + markMultiVersionBusy(store, "a-session") + + expect(store.busy().get("same")?.reason).toBe("setting-up") + }) +}) diff --git a/packages/kilo-vscode/tests/unit/navigate.test.ts b/packages/kilo-vscode/tests/unit/navigate.test.ts index be6e5d816e1..3f23206ec69 100644 --- a/packages/kilo-vscode/tests/unit/navigate.test.ts +++ b/packages/kilo-vscode/tests/unit/navigate.test.ts @@ -401,7 +401,7 @@ describe("buildProjectNavOrder", () => { expect(order.map((e) => e.id)).toEqual([localNavId("A"), worktreeNavId("A", "aw2")]) }) - it("renders section members before ungrouped worktrees (matching the project body)", () => { + it("renders ungrouped worktrees before section members (matching the project body)", () => { const order = buildProjectNavOrder([ project({ id: "A", @@ -411,7 +411,76 @@ describe("buildProjectNavOrder", () => { unassigned: [], }), ]) - expect(order.map((e) => e.id)).toEqual([localNavId("A"), worktreeNavId("A", "aw1"), worktreeNavId("A", "aw2")]) + expect(order.map((e) => e.id)).toEqual([localNavId("A"), worktreeNavId("A", "aw2"), worktreeNavId("A", "aw1")]) + }) + + it("follows persisted top-level section order and worktree order", () => { + const order = buildProjectNavOrder([ + project({ + id: "A", + expanded: true, + worktrees: [{ id: "aw1", sectionId: "s1" }, { id: "aw2" }, { id: "aw3", sectionId: "s2" }], + worktreeOrder: ["aw2", "s2", "s1", "aw3", "aw1"], + sections: [ + { id: "s1", collapsed: false }, + { id: "s2", collapsed: false }, + ], + unassigned: [], + }), + ]) + + expect(order.map((e) => e.id)).toEqual([ + localNavId("A"), + worktreeNavId("A", "aw2"), + worktreeNavId("A", "aw3"), + worktreeNavId("A", "aw1"), + ]) + }) + + it("keeps multi-version worktrees adjacent", () => { + const order = buildProjectNavOrder([ + project({ + id: "A", + expanded: true, + worktrees: [{ id: "aw1", groupId: "g" }, { id: "aw2" }, { id: "aw3", groupId: "g" }], + worktreeOrder: ["aw1", "aw2", "aw3"], + sections: [], + unassigned: [], + }), + ]) + + expect(order.map((e) => e.id)).toEqual([ + localNavId("A"), + worktreeNavId("A", "aw1"), + worktreeNavId("A", "aw3"), + worktreeNavId("A", "aw2"), + ]) + }) + + it("matches raw ungrouped order when sections are present", () => { + const order = buildProjectNavOrder([ + project({ + id: "A", + expanded: true, + worktrees: [ + { id: "aw1", groupId: "g" }, + { id: "aw2" }, + { id: "aw3", groupId: "g" }, + { id: "aw4", sectionId: "s1" }, + ], + worktreeOrder: ["aw1", "aw2", "aw3", "s1", "aw4"], + sections: [{ id: "s1", collapsed: false }], + unassigned: [], + }), + ]) + + expect(order.map((e) => e.id)).toEqual([ + localNavId("A"), + worktreeNavId("A", "aw1"), + worktreeNavId("A", "aw2"), + worktreeNavId("A", "aw3"), + worktreeNavId("A", "aw4"), + ]) }) it("excludes unassigned sessions when the sessions section is collapsed", () => { diff --git a/packages/kilo-vscode/tests/unit/project-local-navigation.test.ts b/packages/kilo-vscode/tests/unit/project-local-navigation.test.ts new file mode 100644 index 00000000000..b20bbc5f30a --- /dev/null +++ b/packages/kilo-vscode/tests/unit/project-local-navigation.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from "bun:test" +import { projectAdjacentHint } from "../../webview-ui/agent-manager/project-local-navigation" + +describe("projectAdjacentHint", () => { + it("does not leak a hint to another project with the same raw ID", () => { + expect(projectAdjacentHint("project-a", "project-a", "shared", "local", ["local", "shared"], "prev", "next")).toBe( + "next", + ) + expect(projectAdjacentHint("project-b", "project-a", "shared", "local", ["local", "shared"], "prev", "next")).toBe( + "", + ) + }) + + it("uses the active project's local sidebar order", () => { + expect(projectAdjacentHint("project-a", "project-a", "shared", "local", ["local", "shared"], "prev", "next")).toBe( + "next", + ) + expect(projectAdjacentHint("project-a", "project-a", "local", "shared", ["local", "shared"], "prev", "next")).toBe( + "prev", + ) + expect( + projectAdjacentHint("project-b", "project-b", "shared", "local", ["local", "other", "shared"], "prev", "next"), + ).toBe("") + }) +}) diff --git a/packages/kilo-vscode/tests/unit/project-store.test.ts b/packages/kilo-vscode/tests/unit/project-store.test.ts index 84dbe9e710c..7f0c7d070e5 100644 --- a/packages/kilo-vscode/tests/unit/project-store.test.ts +++ b/packages/kilo-vscode/tests/unit/project-store.test.ts @@ -42,4 +42,16 @@ describe("project stores", () => { same: { worktreeId: "same", state: "running" }, }) }) + + it("keeps busy worktrees isolated between projects", () => { + const first = createProjectStore("a") + const second = createProjectStore("b") + first.applyState(state("a", ["same"])) + second.applyState(state("b", ["same"])) + + first.setBusy(new Map([["same", { reason: "setting-up" as const }]])) + + expect(first.busy().has("same")).toBe(true) + expect(second.busy().has("same")).toBe(false) + }) }) diff --git a/packages/kilo-vscode/tests/unit/speech-to-text-availability.test.ts b/packages/kilo-vscode/tests/unit/speech-to-text-availability.test.ts index 83848168963..1361f8f5166 100644 --- a/packages/kilo-vscode/tests/unit/speech-to-text-availability.test.ts +++ b/packages/kilo-vscode/tests/unit/speech-to-text-availability.test.ts @@ -23,9 +23,11 @@ describe("speech-to-text availability", () => { }) it("normalizes configured and unknown transcription models", () => { - expect(selectedSpeechToTextModel({ experimental: { speech_to_text_model: "google/chirp-3" } })).toBe( - "google/chirp-3", - ) + expect( + selectedSpeechToTextModel({ experimental: { speech_to_text_model: "google/chirp-3" } }, [ + { id: "google/chirp-3", label: "Chirp 3", provider: "Google" }, + ]), + ).toBe("google/chirp-3") expect(selectedSpeechToTextModel({ experimental: { speech_to_text_model: "unknown/model" } })).toBe( DEFAULT_SPEECH_TO_TEXT_MODEL.id, ) diff --git a/packages/kilo-vscode/tests/unit/speech-to-text-catalog.test.ts b/packages/kilo-vscode/tests/unit/speech-to-text-catalog.test.ts new file mode 100644 index 00000000000..a4e0a8029f8 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/speech-to-text-catalog.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from "bun:test" +import { parseSpeechToTextCatalog } from "../../src/speech-to-text/catalog" +import { DEFAULT_SPEECH_TO_TEXT_MODEL } from "../../src/speech-to-text/models" + +describe("speech-to-text discovery", () => { + it("keeps transcription catalog metadata authoritative and exposes additions", () => { + const models = parseSpeechToTextCatalog([ + { + id: "fish-audio/transcribe-1", + name: "Fish Audio: Transcribe 1", + }, + { + id: "openai/gpt-4o-mini-transcribe", + name: "OpenAI: GPT-4o Mini Transcribe", + }, + { + id: "openai/whisper-1", + name: "Whisper 1", + }, + ]) + + expect(models).toEqual([ + { id: "fish-audio/transcribe-1", label: "Transcribe 1", provider: "Fish Audio" }, + { + id: "openai/gpt-4o-mini-transcribe", + label: "GPT-4o Mini Transcribe", + provider: "OpenAI", + }, + { id: "openai/whisper-1", label: "Whisper 1", provider: "openai" }, + ]) + }) + + it("rejects empty or malformed catalogs so callers can use the static fallback", () => { + expect(parseSpeechToTextCatalog([])).toBeUndefined() + expect(parseSpeechToTextCatalog({ data: [] })).toBeUndefined() + expect(DEFAULT_SPEECH_TO_TEXT_MODEL.id).toBe("openai/whisper-large-v3-turbo") + }) +}) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index 939a8e505be..02a3e9c4dd3 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -89,6 +89,7 @@ import type { WorktreeBusyState } from "./project/store" import { rememberTarget, restoreProjectTarget } from "./project/restore" import { createProjectStateRouter } from "./project/state" import { applyRunStatus } from "./project/run-status" +import { clearMultiVersionBusy, markMultiVersionBusy } from "./project/progress" import { selectLocalAction, selectWorktreeAction } from "./selection-actions" import { DataBridge } from "../src/App" import { LanguageBridge } from "../src/context/language-bridge" @@ -103,7 +104,7 @@ import { focusChatSearch, LOCAL, } from "./navigate" -import { createProjectNav } from "./project-nav" +import { buildProjectNavEntries, createProjectNav } from "./project-nav" import { addPendingTab as addLocalPendingTab, nextTabAfterClose, @@ -865,6 +866,16 @@ const AgentManagerContent: Component = () => { /** True when a local session is actively working. */ const isLocalBusy = (): boolean => isAnySessionBusy(localSessionIDs()) + const projectBusy = (projectId: string, worktreeId: string | null): boolean => { + if (projectId === activeProjectId()) { + return worktreeId === null ? isLocalBusy() : isAgentBusy(worktreeId) + } + const ids = (projectSessionsLive()[projectId] ?? []) + .filter((item) => item.worktreeId === worktreeId) + .map((item) => item.id) + return isAnySessionBusy(ids) + } + const isSessionBusy = (id: string): boolean => isAnySessionBusy([id]) /** Worktrees sorted so that grouped items are always adjacent, respecting custom order if set. */ @@ -882,6 +893,11 @@ const AgentManagerContent: Component = () => { ) /** Map from sidebar item id → 1-based shortcut number (⌘1 for LOCAL, ⌘2 for first worktree, etc.) */ const shortcutMap = createMemo(() => buildShortcutMap(sidebarOrder())) + const projectShortcutMap = createMemo(() => + buildShortcutMap( + buildProjectNavEntries(projectList(), projectStates(), projectLive.sessions()).map((entry) => ({ id: entry.id })), + ), + ) const moveToSection = (ids: string[], sec: string | null) => vscode.postMessage({ type: "agentManager.moveToSection", worktreeIds: ids, sectionId: sec }) @@ -1447,13 +1463,8 @@ const AgentManagerContent: Component = () => { const ev = msg as unknown as AgentManagerMultiVersionProgressMessage if (ev.status === "done" && ev.groupId) { // Clear busy state for all worktrees in this group - setBusyWorktrees((prev) => { - const next = new Map(prev) - for (const wt of worktrees()) { - if (wt.groupId === ev.groupId) next.delete(wt.id) - } - return next - }) + const store = ev.projectId ? registry.ensure(ev.projectId) : registry.active() + clearMultiVersionBusy(store, ev.groupId) } } @@ -1462,11 +1473,8 @@ const AgentManagerContent: Component = () => { if (msg.type === "agentManager.worktreeSetup") { const ev = msg as AgentManagerWorktreeSetupMessage if (ev.status === "ready" && ev.sessionId) { - const ms = managedSessions().find((s) => s.id === ev.sessionId) - const wt = ms?.worktreeId ? worktrees().find((w) => w.id === ms.worktreeId) : undefined - if (wt?.groupId) { - setBusyWorktrees((prev) => new Map([...prev, [wt.id, { reason: "setting-up" as const }]])) - } + const store = ev.projectId ? registry.ensure(ev.projectId) : registry.active() + markMultiVersionBusy(store, ev.sessionId) } } @@ -1503,7 +1511,8 @@ const AgentManagerContent: Component = () => { // Clear busy state — use worktreeId from the message directly // to avoid race condition where managedSessions() hasn't updated yet if (ev.worktreeId) { - setBusyWorktrees((prev) => { + const store = ev.projectId ? registry.ensure(ev.projectId) : registry.active() + store.setBusy((prev) => { const next = new Map(prev) next.delete(ev.worktreeId) return next @@ -2335,6 +2344,9 @@ const AgentManagerContent: Component = () => { projects={projectList()} states={projectStates()} store={(id) => registry.ensure(id)} + busy={(projectId, id) => registry.ensure(projectId).busy().has(id)} + working={(projectId, id) => projectBusy(projectId, id)} + localBusy={(projectId) => projectBusy(projectId, null)} stats={projectLive.stats()} local={projectLive.local()} prs={projectLive.prs()} @@ -2347,6 +2359,7 @@ const AgentManagerContent: Component = () => { t={t} onSearchRef={(ref) => (sidebarSearchMenu = ref)} onShortcuts={handleShowKeyboardShortcuts} + shortcutMap={projectShortcutMap} /> @@ -2459,20 +2472,6 @@ const AgentManagerContent: Component = () => { track={metrics.click} /> - {/* Empty worktree state */} - -
-
- -
-
{t("agentManager.session.noSessions")}
- -
-
- {(state) => (
diff --git a/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx b/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx index 083d892d557..ae15fa3cf6b 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx @@ -21,6 +21,7 @@ import { useProvider } from "../src/context/provider" import { useConfig } from "../src/context/config" import { canUseSpeechToText, selectedSpeechToTextModel } from "../src/components/speech-to-text/availability" import { useSpeechToText } from "../src/components/speech-to-text/useSpeechToText" +import { useSpeechToTextModels } from "../src/context/speech-to-text-models" import { getDirectory, getFilename, @@ -111,8 +112,9 @@ export const DiffPanel: Component = (props) => { const provider = useProvider() const { config } = useConfig() const speech = useSpeechToText(vscode, server, { t }) + const speechModels = useSpeechToTextModels() const canUseSpeech = () => canUseSpeechToText(config(), provider.authStates()) - const speechModel = () => selectedSpeechToTextModel(config()) + const speechModel = () => selectedSpeechToTextModel(config(), speechModels.models()) const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent) const sendAllKeybind = () => isMac ? t("agentManager.review.sendAllShortcut.mac") : t("agentManager.review.sendAllShortcut.other") diff --git a/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx b/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx index bdcba1c8453..72a68c57ef4 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx @@ -39,6 +39,7 @@ import { import { useLanguage } from "../src/context/language" import { useImageAttachments, type ImageAttachment } from "../src/hooks/useImageAttachments" import { useSpeechToText } from "../src/components/speech-to-text/useSpeechToText" +import { useSpeechToTextModels } from "../src/context/speech-to-text-models" import { createSpeechShortcut } from "../src/components/speech-to-text/shortcut" import { convertToMentionPath } from "../src/utils/path-mentions" import { insertSpacedText } from "../src/components/chat/prompt-input-utils" @@ -131,8 +132,9 @@ export const NewWorktreeDialog: Component<{ const sandboxRequestID = crypto.randomUUID() const sandboxVisible = () => features().sandboxControls && globalConfig().sandbox?.enabled === true const speech = useSpeechToText(vscode, server, { t }) + const speechModels = useSpeechToTextModels() const canUseSpeech = () => canUseSpeechToText(config(), provider.authStates()) - const speechModel = () => selectedSpeechToTextModel(config()) + const speechModel = () => selectedSpeechToTextModel(config(), speechModels.models()) let prior: string | null = null let request: string | undefined const cancel = () => { diff --git a/packages/kilo-vscode/webview-ui/agent-manager/ProjectList.tsx b/packages/kilo-vscode/webview-ui/agent-manager/ProjectList.tsx index 3fb1220c166..1575e4e4074 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/ProjectList.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/ProjectList.tsx @@ -34,11 +34,14 @@ interface Props { selection?: string currentSessionID?: () => string | undefined mode: ModeRouter - busy?: (id: string) => boolean + busy?: (projectId: string, id: string) => boolean + working?: (projectId: string, id: string) => boolean + localBusy?: (projectId: string) => boolean bindings: Record t: LanguageContextValue["t"] onSearchRef: (ref: SidebarSearchMenuRef) => void onShortcuts: () => void + shortcutMap?: () => Map } export const ProjectList: Component = (props) => { @@ -211,12 +214,16 @@ export const ProjectList: Component = (props) => { project={project} state={props.states[project.id]} store={props.store?.(project.id)} + busy={(id) => props.busy?.(project.id, id) ?? false} + working={(id) => props.working?.(project.id, id) ?? false} + localBusy={() => props.localBusy?.(project.id) ?? false} stats={props.stats[project.id]} local={props.local[project.id]} prs={props.prs[project.id]} sessions={props.sessions[project.id]} selectedProject={props.selectedProject} selection={props.selection} + currentSessionID={props.currentSessionID} bindings={props.bindings} t={props.t} onSelectLocal={(projectId) => select({ projectId, kind: "local" })} @@ -224,6 +231,7 @@ export const ProjectList: Component = (props) => { onSelectSession={(projectId, sessionId) => select({ projectId, kind: "session", sessionId })} onNewWorktree={newWorktree} onDefaultBranch={defaultBranch} + shortcutMap={props.shortcutMap} /> )} /> diff --git a/packages/kilo-vscode/webview-ui/agent-manager/ProjectSidebarBody.tsx b/packages/kilo-vscode/webview-ui/agent-manager/ProjectSidebarBody.tsx index 2e6bd7b0d3b..3e150730373 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/ProjectSidebarBody.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/ProjectSidebarBody.tsx @@ -20,6 +20,7 @@ import type { } from "../src/types/messages" import type { LanguageContextValue } from "../src/context/language" import { useVSCode } from "../src/context/vscode" +import { projectAdjacentHint, projectSidebarOrder } from "./project-local-navigation" import SectionHeader from "./SectionHeader" import { SidebarSectionHeader } from "./SidebarSectionHeader" import { WorktreeItem } from "./WorktreeItem" @@ -32,17 +33,22 @@ import { ConstrainDragXAxis } from "./constrain-drag-x" import { createProjectStore, type ProjectStore } from "./project/store" import { randomColor } from "./section-colors" +const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent) + interface Props { project: AgentProjectSnapshot state?: AgentManagerStateMessage store?: ProjectStore busy?: (id: string) => boolean + working?: (id: string) => boolean + localBusy?: () => boolean stats?: Record local?: LocalGitStats prs?: Record sessions?: ProjectSessionInfo[] selectedProject?: string selection?: string + currentSessionID?: () => string | undefined bindings: Record t: LanguageContextValue["t"] onSelectLocal: (projectId: string) => void @@ -50,6 +56,7 @@ interface Props { onSelectSession: (projectId: string, sessionId: string) => void onNewWorktree: (projectId: string) => void onDefaultBranch: (projectId: string, selected?: string, detected?: string) => void + shortcutMap?: () => Map } /** Permanent real sidebar body for one expanded project. */ @@ -98,9 +105,23 @@ export const ProjectSidebarBody: Component = (props) => { const members = (sectionId: string) => sorted().filter((wt) => wt.sectionId === sectionId) const ungrouped = createMemo(() => sorted().filter((wt) => !wt.sectionId)) const top = createMemo(() => buildTopLevelItems(sections(), ungrouped(), sorted(), order())) + const sidebarOrder = createMemo(() => + projectSidebarOrder(top(), sorted(), sections(), members, state()?.sessionsCollapsed ? [] : localSessions()), + ) const post = (message: Record) => vscode.postMessage({ ...message, projectId: props.project.id } as never) + const navHint = (id: string) => + projectAdjacentHint( + props.project.id, + props.selectedProject, + id, + props.selection ?? props.currentSessionID?.(), + sidebarOrder(), + props.bindings.previousSession ?? "", + props.bindings.nextSession ?? "", + ) + const scope = (kind: "section" | "worktree", id: string) => `${props.project.id}:${kind}:${id}` const parse = (kind: "section" | "worktree", value: unknown) => { if (typeof value !== "string") return @@ -215,14 +236,16 @@ export const ProjectSidebarBody: Component = (props) => { = (props) => { data-sidebar-id={`${props.project.id}:local`} onClick={() => props.onSelectLocal(props.project.id)} > - - - - - + }> + + + + + + + + {(shortcut) => ( + + {isMac ? "⌘" : "Ctrl+"} + {shortcut()} + + )} +
{props.t("agentManager.local")} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/ProjectsSection.tsx b/packages/kilo-vscode/webview-ui/agent-manager/ProjectsSection.tsx index 7f5e9ea31a5..1e2dcb8fb8a 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/ProjectsSection.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/ProjectsSection.tsx @@ -52,7 +52,7 @@ export const ProjectsSection: Component = (props) => ( {(id) => { const project = () => props.projects.find((item) => item.id === id)! return ( -
+
= (props) => ( } const expanded = !project().expanded props.onExpand(project().id, expanded) - if (!project().active && project().trusted && !project().missing) props.onSelect(project().id) + if (!project().active && project().trusted) props.onSelect(project().id) }} /> 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 a94cf39d149..76c7c6e3b74 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css @@ -322,22 +322,10 @@ html[data-theme="kilo-vscode"] border-bottom: 0; } -.am-project::before { - content: ""; - position: absolute; - inset: 6px auto 6px 0; - width: 2px; - border-radius: 0 2px 2px 0; - background: transparent; -} - -.am-project-active::before { - background: var(--border-interactive-base); -} - .am-project-item { min-height: 34px; padding: 6px 8px 6px 12px; + box-sizing: border-box; } .am-project-item:hover { diff --git a/packages/kilo-vscode/webview-ui/agent-manager/focus.ts b/packages/kilo-vscode/webview-ui/agent-manager/focus.ts index e1acb284611..0553003b306 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/focus.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/focus.ts @@ -1,5 +1,11 @@ +import { isTextControl } from "../src/utils/focus" + const OPTION = '[data-component="question-dock"] button[data-slot="question-option"]' +/** Keep an active editor, such as the worktree rename input, in control. */ +export const preservesTextFocus = (active: Element | null): boolean => + active !== null && isTextControl(active) && !active.classList.contains("prompt-input") + export function createChatFocus(deps: { term: () => string | undefined history: () => boolean @@ -7,6 +13,7 @@ export function createChatFocus(deps: { }) { const focus = (force: boolean) => { if ((!force && !document.hasFocus()) || deps.term() || deps.history() || deps.review()) return + if (preservesTextFocus(document.activeElement)) return if (!force && document.activeElement?.matches('[role="tab"]')) return if (!force && document.activeElement?.closest('[data-component="question-dock"]')) return if (focusQuestionOption()) return diff --git a/packages/kilo-vscode/webview-ui/agent-manager/initial-message.ts b/packages/kilo-vscode/webview-ui/agent-manager/initial-message.ts index 15049eec698..7f210d4a560 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/initial-message.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/initial-message.ts @@ -9,6 +9,7 @@ export function initialMessage(ev: AgentManagerSendInitialMessage): SendMessageR if (!ev.text) return undefined return { type: "sendMessage", + ...(ev.projectId ? { projectId: ev.projectId } : {}), text: ev.text, sessionID: ev.sessionId, providerID: ev.providerID, diff --git a/packages/kilo-vscode/webview-ui/agent-manager/navigate.ts b/packages/kilo-vscode/webview-ui/agent-manager/navigate.ts index 6fad2144f44..8596666cf91 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/navigate.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/navigate.ts @@ -7,6 +7,8 @@ * Returns the action to take: select a session by ID, go to local, or do nothing. */ +import { sortWorktrees } from "./section-helpers" + /** Sentinel value for the local repo selection. */ export const LOCAL = "local" as const @@ -137,8 +139,8 @@ export function focusChatSearch(reset: { history(v: boolean): void; review(v: bo * Multi-project navigation. * * In multi-project mode the sidebar shows an accordion of projects; each - * expanded project renders its own Local item, worktrees (sections first, - * then ungrouped), and an unassigned-sessions list. Keyboard previous/next + * expanded project renders its own Local item, ungrouped worktrees, section + * members, and an unassigned-sessions list. Keyboard previous/next * and numeric shortcuts must traverse every expanded project in visual * order, not just the active one. * @@ -160,7 +162,9 @@ export interface NavEntry { export interface ProjectNavInput { id: string expanded: boolean - worktrees: { id: string; sectionId?: string }[] + worktrees: { id: string; sectionId?: string; groupId?: string }[] + /** Persisted top-level order containing worktree and section IDs. */ + worktreeOrder?: string[] sections: { id: string; collapsed: boolean }[] sessionsCollapsed: boolean /** Visible unassigned (root, no worktree) sessions in render order. */ @@ -174,10 +178,10 @@ export const sessionNavId = (projectId: string, sessionId: string) => `${project /** * Build one global visual order across expanded projects. * - * For each expanded project (in input order): Local, then worktrees in the - * order the multi-project body renders them (each non-collapsed section's - * members, then ungrouped), then visible unassigned sessions (when the - * sessions section is not collapsed). Collapsed projects contribute nothing. + * For each expanded project (in input order): Local, then ungrouped worktrees, + * then members of each non-collapsed section in top-level order, then visible + * unassigned sessions. This matches `buildTopLevelItems` and the project body. + * Collapsed projects contribute nothing. */ export function buildProjectNavOrder(projects: ProjectNavInput[]): NavEntry[] { const order: NavEntry[] = [] @@ -185,19 +189,28 @@ export function buildProjectNavOrder(projects: ProjectNavInput[]): NavEntry[] { if (!p.expanded) continue const pid = p.id order.push({ id: localNavId(pid), target: { projectId: pid, kind: "local" } }) - for (const sec of p.sections) { + const worktrees = sortWorktrees(p.worktrees, p.worktreeOrder ?? []) + const rank = new Map((p.worktreeOrder ?? []).map((id, index) => [id, index] as const)) + const ungrouped = worktrees.filter((w) => !w.sectionId) + if (p.sections.length > 0) { + ungrouped.sort( + (a, b) => (rank.get(a.id) ?? Number.MAX_SAFE_INTEGER) - (rank.get(b.id) ?? Number.MAX_SAFE_INTEGER), + ) + } + const secs = [...p.sections].sort( + (a, b) => (rank.get(a.id) ?? Number.MAX_SAFE_INTEGER) - (rank.get(b.id) ?? Number.MAX_SAFE_INTEGER), + ) + for (const w of ungrouped) { + order.push({ id: worktreeNavId(pid, w.id), target: { projectId: pid, kind: "worktree", worktreeId: w.id } }) + } + for (const sec of secs) { if (sec.collapsed) continue - for (const w of p.worktrees) { + for (const w of worktrees) { if (w.sectionId === sec.id) { order.push({ id: worktreeNavId(pid, w.id), target: { projectId: pid, kind: "worktree", worktreeId: w.id } }) } } } - for (const w of p.worktrees) { - if (!w.sectionId) { - order.push({ id: worktreeNavId(pid, w.id), target: { projectId: pid, kind: "worktree", worktreeId: w.id } }) - } - } if (!p.sessionsCollapsed) { for (const s of p.unassigned) { order.push({ id: sessionNavId(pid, s.id), target: { projectId: pid, kind: "session", sessionId: s.id } }) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/project-local-navigation.ts b/packages/kilo-vscode/webview-ui/agent-manager/project-local-navigation.ts new file mode 100644 index 00000000000..ede97a8e129 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/project-local-navigation.ts @@ -0,0 +1,19 @@ +import { adjacentHint } from "./navigate" +import { buildSidebarOrder } from "./section-helpers" + +export function projectSidebarOrder(...args: Parameters): string[] { + return buildSidebarOrder(...args).map((item) => item.id) +} + +export function projectAdjacentHint( + projectId: string, + activeProjectId: string | undefined, + itemId: string, + activeId: string | undefined, + flatIds: string[], + prev: string, + next: string, +): string { + if (projectId !== activeProjectId) return "" + return adjacentHint(itemId, activeId, flatIds, prev, next) +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/project-nav.ts b/packages/kilo-vscode/webview-ui/agent-manager/project-nav.ts index beae37660cc..2ff2fdbe84f 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/project-nav.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/project-nav.ts @@ -19,7 +19,7 @@ import type { AgentManagerStateMessage, AgentProjectSnapshot, ProjectSessionInfo * * Handles previous/next (⌘⌥↑/↓) and numeric-shortcut (⌘1-9) navigation across * the sidebar. In multi-project mode it builds one global visual order across - * every expanded project — Local, worktrees (sections first, then ungrouped), + * every expanded project — Local, ungrouped worktrees, then section members, * and visible unassigned sessions — using stable project-qualified composite * ids, and activates each target with a single atomic * `agentManager.activateSelection` dispatch. In single-project mode it keeps @@ -48,6 +48,31 @@ export interface ProjectNav { jump: (index: number) => void } +/** Build the same global order used by keyboard navigation and shortcut badges. */ +export function buildProjectNavEntries( + projects: AgentProjectSnapshot[], + states: Record, + sessions: Record, +): NavEntry[] { + return buildProjectNavOrder( + projects.map((p) => { + const st = states[p.id] + if (!st) { + return { id: p.id, expanded: false, worktrees: [], sections: [], sessionsCollapsed: false, unassigned: [] } + } + return { + id: p.id, + expanded: p.expanded, + worktrees: (st.worktrees ?? []).map((w) => ({ id: w.id, sectionId: w.sectionId, groupId: w.groupId })), + worktreeOrder: st.worktreeOrder, + sections: (st.sections ?? []).map((s) => ({ id: s.id, collapsed: s.collapsed })), + sessionsCollapsed: st.sessionsCollapsed === true, + unassigned: (sessions[p.id] ?? []).filter((s) => s.worktreeId === null).map((s) => ({ id: s.id })), + } + }), + ) +} + /** DOM selector for the sidebar element backing a nav target. */ export const navSelector = (target: NavTarget): string => { if (target.kind === "local") return `[data-sidebar-id="${target.projectId}:local"]` @@ -74,24 +99,7 @@ export function createProjectNav( if (!deps.multiProject()) return [] const states = deps.states() const live = deps.sessions() - return buildProjectNavOrder( - deps.projects().map((p) => { - const st = states[p.id] - // Until a project's state payload arrives its body shows a spinner - // (no Local/worktrees/sessions) — exclude it from the nav order. - if (!st) { - return { id: p.id, expanded: false, worktrees: [], sections: [], sessionsCollapsed: false, unassigned: [] } - } - return { - id: p.id, - expanded: p.expanded, - worktrees: (st.worktrees ?? []).map((w) => ({ id: w.id, sectionId: w.sectionId })), - sections: (st.sections ?? []).map((s) => ({ id: s.id, collapsed: s.collapsed })), - sessionsCollapsed: st.sessionsCollapsed === true, - unassigned: (live[p.id] ?? []).filter((s) => s.worktreeId === null).map((s) => ({ id: s.id })), - } - }), - ) + return buildProjectNavEntries(deps.projects(), states, live) }) const currentId = createMemo((): string | undefined => { diff --git a/packages/kilo-vscode/webview-ui/agent-manager/project/progress.ts b/packages/kilo-vscode/webview-ui/agent-manager/project/progress.ts new file mode 100644 index 00000000000..807c7b2be99 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/project/progress.ts @@ -0,0 +1,23 @@ +import type { ProjectStore } from "./store" + +/** Clear the loading indicators for every worktree in one multi-version group. */ +export function clearMultiVersionBusy(store: ProjectStore, groupId: string): void { + const ids = new Set( + store + .worktrees() + .filter((wt) => wt.groupId === groupId) + .map((wt) => wt.id), + ) + if (ids.size === 0) return + store.setBusy((prev) => new Map([...prev].filter(([id]) => !ids.has(id)))) +} + +/** Keep a newly created grouped worktree showing progress until its prompt starts. */ +export function markMultiVersionBusy(store: ProjectStore, sessionId: string): void { + const session = store.managedSessions().find((item) => item.id === sessionId) + const id = session?.worktreeId + if (!id) return + const worktree = store.worktrees().find((item) => item.id === id) + if (!worktree?.groupId) return + store.setBusy((prev) => new Map([...prev, [id, { reason: "setting-up" as const }]])) +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/section-helpers.ts b/packages/kilo-vscode/webview-ui/agent-manager/section-helpers.ts index c79eb3c5378..fba922c053d 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/section-helpers.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/section-helpers.ts @@ -10,11 +10,11 @@ export type TopLevelItem = { kind: "section"; section: SectionState } | { kind: export type SidebarItem = { type: "local" | "wt" | "session"; id: string } /** Apply persisted order while keeping multi-version worktrees adjacent. */ -export function sortWorktrees(all: WorktreeState[], order: string[]): WorktreeState[] { +export function sortWorktrees(all: T[], order: string[]): T[] { const ordered = applyTabOrder(all, order) if (ordered.length === 0) return [] - const groups = new Map() + const groups = new Map() for (const wt of ordered) { if (!wt.groupId) continue const group = groups.get(wt.groupId) ?? [] @@ -22,7 +22,7 @@ export function sortWorktrees(all: WorktreeState[], order: string[]): WorktreeSt groups.set(wt.groupId, group) } - const result: WorktreeState[] = [] + const result: T[] = [] const placed = new Set() for (const wt of ordered) { if (placed.has(wt.id)) continue @@ -145,7 +145,7 @@ export function buildSidebarOrder( } /** Build a map from sidebar item id → 1-based shortcut number (1 for LOCAL, 2+ for worktrees). */ -export function buildShortcutMap(order: SidebarItem[]): Map { +export function buildShortcutMap(order: { id: string }[]): Map { const map = new Map() for (let i = 0; i < order.length && i < 9; i++) { map.set(order[i]!.id, i + 1) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/terminal/TerminalTab.tsx b/packages/kilo-vscode/webview-ui/agent-manager/terminal/TerminalTab.tsx index 8ae231cacb4..606c8fa47ec 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/terminal/TerminalTab.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/terminal/TerminalTab.tsx @@ -158,39 +158,6 @@ export const TerminalTab: Component = (props) => { }) const fit = new FitAddon() term.loadAddon(fit) - // Clickable URLs in terminal output (Cmd/Ctrl+click to open). - // WebLinksAddon's default handler calls `window.open`, which VS Code - // webviews intercept and silently drop — so we pass an explicit - // handler that posts an `openExternal` message. The message falls - // through `AgentManagerProvider.onMessage` to the underlying - // `KiloProvider.handleWebviewMessage` path which already calls - // `vscode.env.openExternal` for sidebar + settings links. - term.loadAddon( - new WebLinksAddon((_event, url) => { - vscode.postMessage({ type: "openExternal", url }) - }), - ) - // OSC 52 clipboard support — lets shell programs (tmux, neovim, etc.) - // copy to the system clipboard via escape sequences. Writes always - // work in the webview; reads require the clipboard-read permission, - // which VS Code does not grant by default, so paste-from-escape - // silently falls back to no-op. Acceptable trade-off. - term.loadAddon(new ClipboardAddon()) - // Unicode 15 grapheme-aware width tables. Fixes cell width for - // emoji introduced in Unicode 12-15 (🫠 melting face, 🫡 salute, - // 🧌 troll, and ~400 others) plus ZWJ grapheme sequences like - // 👨‍👩‍👧‍👦 and 🏳️‍🌈. The older `@xterm/addon-unicode11` (which VS - // Code's integrated terminal still uses) stops at Unicode 11 - // (2018), leaving all post-2020 emoji rendered with wrong width — - // the canvas cuts them off in the DOM renderer and cursor math - // drifts by one cell per emoji. VS Code hides this visually with - // WebGL; in a webview we don't have that fallback, so we fix it - // at the buffer-width layer instead. Addon is marked - // "experimental" in its README but has been stable on npm since - // 2023, is shipped by the same maintainer as the core xterm.js - // package, and has no open bugs as of v0.4.0. - term.loadAddon(new UnicodeGraphemesAddon()) - term.unicode.activeVersion = "15-graphemes" term.open(host) // Fit on the next frame — `host` might still have 0px dimensions // during the initial layout pass otherwise. @@ -232,6 +199,8 @@ export const TerminalTab: Component = (props) => { let fallbackTimer: ReturnType | undefined let streamed = false let socketEnded = false + let frame: number | undefined + let deferred: number | undefined // The failure line must not depend on event ordering: the stream can // close before the exited snapshot lands (fast failures), or stay open // when a background child outlives the script. Write it exactly once, @@ -358,6 +327,34 @@ export const TerminalTab: Component = (props) => { } const disposeData = term.onData(send) open(props.wsUrl) + + // These addons are not needed to paint the initial prompt. Defer them + // until after the first frame so their startup work, especially the + // Unicode 15 width tables, does not delay the shell connection. + const loadAddons = () => { + deferred = undefined + if (closed) return + + // Clickable URLs in terminal output (Cmd/Ctrl+click to open). + // WebLinksAddon's default handler calls `window.open`, which VS Code + // webviews intercept and silently drop, so post an explicit message. + term.loadAddon( + new WebLinksAddon((_event, url) => { + vscode.postMessage({ type: "openExternal", url }) + }), + ) + // OSC 52 clipboard support for shell programs such as tmux and neovim. + term.loadAddon(new ClipboardAddon()) + // Use grapheme-aware width tables for newer emoji and ZWJ sequences. + term.loadAddon(new UnicodeGraphemesAddon()) + term.unicode.activeVersion = "15-graphemes" + term.refresh(0, Math.max(0, term.rows - 1)) + } + frame = requestAnimationFrame(() => { + frame = undefined + deferred = requestAnimationFrame(loadAddons) + }) + const restarted = (url: string) => { open(url) } @@ -525,6 +522,8 @@ export const TerminalTab: Component = (props) => { onCleanup(() => { closed = true if (pendingFrame !== null) cancelAnimationFrame(pendingFrame) + if (frame !== undefined) cancelAnimationFrame(frame) + if (deferred !== undefined) cancelAnimationFrame(deferred) document.removeEventListener("visibilitychange", onVisibilityChange) window.removeEventListener("focus", onWindowFocus) host.removeEventListener("focusin", onFocusIn) diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/DiffViewerApp.tsx b/packages/kilo-vscode/webview-ui/diff-viewer/DiffViewerApp.tsx index 5ab642292e1..08115371429 100644 --- a/packages/kilo-vscode/webview-ui/diff-viewer/DiffViewerApp.tsx +++ b/packages/kilo-vscode/webview-ui/diff-viewer/DiffViewerApp.tsx @@ -24,6 +24,7 @@ import type { DiffViewerNotice } from "../src/types/messages/extension-messages" import { DiffPickerHeader } from "./DiffPickerHeader" import { BaseBranchPicker } from "./BaseBranchPicker" import { SpeechToTextPrewarm } from "../src/components/speech-to-text/SpeechToTextPrewarm" +import { SpeechToTextModelsProvider } from "../src/context/speech-to-text-models" const NOTICE_KEYS: Record = { "snapshots-disabled": "diffViewer.notice.snapshotsDisabled", @@ -306,8 +307,10 @@ export const DiffViewerApp: Component = () => { - - + + + + diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx b/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx index 73ba90e60f5..6f708f8bd91 100644 --- a/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx +++ b/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx @@ -27,6 +27,7 @@ import { useProvider } from "../src/context/provider" import { useConfig } from "../src/context/config" import { canUseSpeechToText, selectedSpeechToTextModel } from "../src/components/speech-to-text/availability" import { useSpeechToText } from "../src/components/speech-to-text/useSpeechToText" +import { useSpeechToTextModels } from "../src/context/speech-to-text-models" import { FileTree } from "./FileTree" import { treeOrder } from "./file-tree-utils" import { getDirectory, getFilename, lineCount, sanitizeReviewComments, type ReviewComment } from "./review-comments" @@ -113,8 +114,9 @@ export const FullScreenDiffView: Component = (props) => const provider = useProvider() const { config } = useConfig() const speech = useSpeechToText(vscode, server, { t }) + const speechModels = useSpeechToTextModels() const canUseSpeech = () => canUseSpeechToText(config(), provider.authStates()) - const speechModel = () => selectedSpeechToTextModel(config()) + const speechModel = () => selectedSpeechToTextModel(config(), speechModels.models()) const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent) const sendAllKeybind = () => isMac ? t("agentManager.review.sendAllShortcut.mac") : t("agentManager.review.sendAllShortcut.other") diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx index 530e4f89874..075b925b73b 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx @@ -10,6 +10,7 @@ import { Tooltip } from "@kilocode/kilo-ui/tooltip" import { FileIcon } from "@kilocode/kilo-ui/file-icon" import { Icon } from "@kilocode/kilo-ui/icon" import { showToast } from "@kilocode/kilo-ui/toast" +import { isTextControl } from "../../utils/focus" import { useSession } from "../../context/session" import { useLocalTabs } from "../../context/local-tabs" import { useServer } from "../../context/server" @@ -34,6 +35,7 @@ import { hasGitChangesMention } from "../../hooks/git-changes-context-utils" import { useSlashCommand } from "../../hooks/useSlashCommand" import { useGhostText } from "../../hooks/useGhostText" import { useSpeechToText } from "../speech-to-text/useSpeechToText" +import { useSpeechToTextModels } from "../../context/speech-to-text-models" import { createSpeechShortcut } from "../speech-to-text/shortcut" import { useImageAttachments, type ImageAttachment } from "../../hooks/useImageAttachments" import { convertToMentionPath } from "../../utils/path-mentions" @@ -348,6 +350,7 @@ export const PromptInput: Component = (props) => { const ghost = useGhostText(vscode, text, () => server.isConnected()) const speech = useSpeechToText(vscode, server, language) + const speechModels = useSpeechToTextModels() const replaceReviewComments = (next: ReviewComment[]) => { setReviewComments(next) @@ -416,8 +419,12 @@ export const PromptInput: Component = (props) => { const onFocusPrompt = (event: Event) => { const defer = () => event instanceof CustomEvent && event.detail?.deferFocusToQuestion && props.deferFocusToQuestion?.() + const ownsFocus = () => { + const active = document.activeElement + return active !== textareaRef && isTextControl(active) + } const focus = () => { - if (defer()) return + if (defer() || ownsFocus()) return const ref = textareaRef if (!ref) return ref.focus({ preventScroll: true }) @@ -425,7 +432,7 @@ export const PromptInput: Component = (props) => { focus() if (!(event instanceof CustomEvent) || !event.detail?.restore) return const restore = () => { - if (defer()) return + if (defer() || ownsFocus()) return window.focus() focus() } @@ -511,7 +518,7 @@ export const PromptInput: Component = (props) => { ) const isDisabled = () => !server.isConnected() const canUseSpeech = () => canUseSpeechToText(config(), provider.authStates()) - const speechModel = () => selectedSpeechToTextModel(config()) + const speechModel = () => selectedSpeechToTextModel(config(), speechModels.models()) const hasInput = () => text().trim().length > 0 || imageAttach.images().length > 0 || reviewComments().length > 0 const canSend = () => !isDisabled() && diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/ModelsTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/ModelsTab.tsx index 81f57c39682..619cf17bd13 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/ModelsTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/ModelsTab.tsx @@ -7,13 +7,14 @@ import { useConfig } from "../../context/config" import { useLanguage } from "../../context/language" import { useProvider } from "../../context/provider" import { useSession } from "../../context/session" +import { useSpeechToTextModels } from "../../context/speech-to-text-models" import { parseModelString } from "../../../../src/shared/provider-model" import { ModelSelectorBase } from "../shared/ModelSelector" import { ThinkingSelectorBase } from "../shared/ThinkingSelector" import SettingsRow from "./SettingsRow" import { DEFAULT_SPEECH_TO_TEXT_MODEL } from "../../../../src/speech-to-text/models" import { hasSpeechToTextAccess, selectedSpeechToTextModel } from "../speech-to-text/availability" -import { SPEECH_TO_TEXT_MODEL_OPTIONS } from "../speech-to-text/model-selector" +import { speechToTextModelOptions } from "../speech-to-text/model-selector" import { AUTOCOMPLETE_SELECTOR_MODELS, getAutocompleteSelection } from "./autocomplete-model-selector" const ModelsTab: Component = () => { @@ -21,6 +22,7 @@ const ModelsTab: Component = () => { const language = useLanguage() const provider = useProvider() const session = useSession() + const speechModels = useSpeechToTextModels() const autocompleteProvider = () => { const v = settings()["autocomplete.provider"] @@ -42,8 +44,9 @@ const ModelsTab: Component = () => { } const subagentModel = createMemo(() => parseModelString(config().subagent_model ?? undefined)) - const speechModel = createMemo(() => selectedSpeechToTextModel(config())) - const speechOption = createMemo(() => SPEECH_TO_TEXT_MODEL_OPTIONS.find((item) => item.value === speechModel())) + const speechModel = createMemo(() => selectedSpeechToTextModel(config(), speechModels.models())) + const speechOptions = createMemo(() => speechToTextModelOptions(speechModels.models())) + const speechOption = createMemo(() => speechOptions().find((item) => item.value === speechModel())) const kiloReady = createMemo(() => hasSpeechToTextAccess(config(), provider.authStates())) const variantKey = createMemo(() => config().subagent_model ?? undefined) const subagentVariants = createMemo(() => Object.keys(provider.findModel(subagentModel())?.variants ?? {})) @@ -190,7 +193,7 @@ const ModelsTab: Component = () => { inactive={kiloReady()} >