diff --git a/.changeset/adaptive-opus-five.md b/.changeset/adaptive-opus-five.md deleted file mode 100644 index 9d8cf6c1b3..0000000000 --- a/.changeset/adaptive-opus-five.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Support adaptive thinking levels for Claude Opus and Sonnet 5 and later. diff --git a/.changeset/agent-manager-side-terminal.md b/.changeset/agent-manager-side-terminal.md deleted file mode 100644 index 533da3233c..0000000000 --- a/.changeset/agent-manager-side-terminal.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": minor ---- - -Let users open Agent Manager terminals in the VS Code terminal or an embedded side panel. The terminal button's dropdown picks the destination; the side panel shares the right-hand inspector with the diff view and keeps running in the background when hidden. diff --git a/.changeset/atomic-session-revert.md b/.changeset/atomic-session-revert.md deleted file mode 100644 index ed922d1a6e..0000000000 --- a/.changeset/atomic-session-revert.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@kilocode/cli": patch -"kilo-code": patch ---- - -Keep conversations and workspace files unchanged when a checkpoint cannot be fully restored. diff --git a/.changeset/calm-cursors-fit.md b/.changeset/calm-cursors-fit.md new file mode 100644 index 0000000000..d35d8ee2d1 --- /dev/null +++ b/.changeset/calm-cursors-fit.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Keep the Agent Manager terminal cursor visible on the bottom row. diff --git a/.changeset/console-headless-credentials.md b/.changeset/console-headless-credentials.md deleted file mode 100644 index 6c6102f467..0000000000 --- a/.changeset/console-headless-credentials.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Include basic-auth credentials in the Local and Network Console URLs printed by `kilo console`, so users on headless hosts (no `DISPLAY`/`WAYLAND_DISPLAY`, SSH sessions, CI runners) can open the URL in a browser on another machine and reach the Console. \ No newline at end of file diff --git a/.changeset/fix-nix-bun-pin.md b/.changeset/fix-nix-bun-pin.md deleted file mode 100644 index 5facef0667..0000000000 --- a/.changeset/fix-nix-bun-pin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Keep Nix builds on the Bun version required by the repository. diff --git a/.changeset/fix-scoped-mode-cycling.md b/.changeset/fix-scoped-mode-cycling.md deleted file mode 100644 index d71009746d..0000000000 --- a/.changeset/fix-scoped-mode-cycling.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Update the visible agent mode when cycling modes in Kilo sidebars and pending session tabs. diff --git a/.changeset/fix-vscode-settings-save.md b/.changeset/fix-vscode-settings-save.md deleted file mode 100644 index c7ed178ad9..0000000000 --- a/.changeset/fix-vscode-settings-save.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Fix settings changes sometimes failing to save and apply in VS Code. diff --git a/.changeset/fresh-rc-upgrades.md b/.changeset/fresh-rc-upgrades.md new file mode 100644 index 0000000000..61a54ea4e2 --- /dev/null +++ b/.changeset/fresh-rc-upgrades.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Keep RC installations up to date when a newer stable CLI release is published. diff --git a/.changeset/fuzzy-tildes-smile.md b/.changeset/fuzzy-tildes-smile.md deleted file mode 100644 index 39c76209f8..0000000000 --- a/.changeset/fuzzy-tildes-smile.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Preserve parenthesized tilde expressions as literal text in rendered chat messages. diff --git a/.changeset/ingest-shutdown-flush.md b/.changeset/ingest-shutdown-flush.md deleted file mode 100644 index 94ff86a5b1..0000000000 --- a/.changeset/ingest-shutdown-flush.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Fix session transcripts losing their final messages when the CLI exits — pending uploads are now flushed on shutdown and as soon as a session closes. diff --git a/.changeset/instant-prompt-tooltips.md b/.changeset/instant-prompt-tooltips.md deleted file mode 100644 index d34f46dc04..0000000000 --- a/.changeset/instant-prompt-tooltips.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Show prompt input toggle tooltips instantly on hover instead of after a delay. diff --git a/.changeset/jetbrains-bundled-cli.md b/.changeset/jetbrains-bundled-cli.md deleted file mode 100644 index d0239ff176..0000000000 --- a/.changeset/jetbrains-bundled-cli.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": minor ---- - -Publish a signed GitHub-hosted JetBrains plugin build with the CLI bundled for offline installation. diff --git a/.changeset/jetbrains-queued-prompts.md b/.changeset/jetbrains-queued-prompts.md deleted file mode 100644 index b22d8c8353..0000000000 --- a/.changeset/jetbrains-queued-prompts.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Allow sending prompts while a session is busy and show queued prompts with a remove action. diff --git a/.changeset/opencode-v1-17-5-to-v1-17-9.md b/.changeset/opencode-v1-17-5-to-v1-17-9.md deleted file mode 100644 index 5a019637c3..0000000000 --- a/.changeset/opencode-v1-17-5-to-v1-17-9.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -"@kilocode/cli": patch -"kilo-code": patch ---- - -Changes from opencode v1.17.5 to v1.17.9 upstream: - -- Core Bugfixes: Improved MCP server compatibility by declaring Kilo's supported client capabilities. -- Core Bugfixes: Plugin client requests now reuse the active server instead of assuming the default local port. -- Core Bugfixes: ACP shell tool calls now show the command and working directory from the start. -- Core Bugfixes: Plugin-provided shell environment variables now apply to PTY sessions. -- Core Bugfixes: OpenAI-compatible providers now accept MCP tool schemas that previously failed validation. (@jquense) -- Core Bugfixes: Cloudflare AI Gateway now receives the configured API key correctly. (@keefetang) -- Core Bugfixes: MCP tools without declared schema properties now work with providers that expect object properties. -- Core Bugfixes: Long-running MCP tools now keep their timeout alive when they report progress. (@Nomadcxx) -- Core Bugfixes: The MCP OAuth callback server now shuts down once authorization finishes or is cancelled. -- Core Bugfixes: MCP tool failures now surface the server's error text instead of a generic failure. -- Core Bugfixes: MCP OAuth error pages now escape provider error text correctly. -- Core Bugfixes: Honor configured agent step limits by forcing a final text response instead of failing mid-run. -- Core Bugfixes: Queue steering prompts before dismissing pending questions so the previous turn cannot resume first. -- Core Bugfixes: Prevent local server credentials from leaking into spawned PTY processes. -- Core Bugfixes: Fix Devstral model detection when provider IDs use different casing. (@Robin1987China) -- Core Bugfixes: Pass configured custom headers to Copilot model requests. -- Core Improvements: MCP servers can now receive the current workspace as a client root. -- Core Improvements: Session timelines load much faster and avoid flicker or scroll jumps. -- Core Improvements: Add `high` and `max` thinking variants for GLM-5.2 across supported providers. (@imranshaiedi-byte) -- Core Improvements: Stop wrapping follow-up user messages in a steering reminder so prompt caching stays effective. -- TUI Bugfixes: MCP debug now uses the SDK's latest protocol version. -- TUI Bugfixes: Only show the background subagent shortcut when the server supports it. -- UI Bugfixes: Render completed Mermaid blocks from diagram source instead of fenced Markdown. diff --git a/.changeset/persian-language.md b/.changeset/persian-language.md new file mode 100644 index 0000000000..9e07d9e8b8 --- /dev/null +++ b/.changeset/persian-language.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Add Persian (Farsi) as a UI language, including right-to-left layout. Contributed by Babak Safabahar. diff --git a/.changeset/pwsh-permission-fail-closed.md b/.changeset/pwsh-permission-fail-closed.md deleted file mode 100644 index cb140f33cd..0000000000 --- a/.changeset/pwsh-permission-fail-closed.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Fix bash permission rules being bypassed on PowerShell for commands containing a bare `--` such as `git checkout -- `. Commands the shell parser cannot parse now get checked against their raw command text instead of executing without a permission check. diff --git a/.changeset/quiet-json-events.md b/.changeset/quiet-json-events.md deleted file mode 100644 index bc7f513506..0000000000 --- a/.changeset/quiet-json-events.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Emit each agent event once from `kilo run --format json`. diff --git a/.changeset/quiet-vscode-watchers.md b/.changeset/quiet-vscode-watchers.md deleted file mode 100644 index 0f44b7cfda..0000000000 --- a/.changeset/quiet-vscode-watchers.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@kilocode/cli": patch -"kilo-code": patch ---- - -Prevent the VS Code backend from eagerly starting native file watchers for every Agent Manager worktree. diff --git a/.changeset/reliable-vscode-message-copy.md b/.changeset/reliable-vscode-message-copy.md deleted file mode 100644 index 566918cf93..0000000000 --- a/.changeset/reliable-vscode-message-copy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Keep message and response copy buttons working after switching focus away from VS Code. diff --git a/.changeset/safe-windows-snapshot-diffs.md b/.changeset/safe-windows-snapshot-diffs.md deleted file mode 100644 index 55dd9e61bc..0000000000 --- a/.changeset/safe-windows-snapshot-diffs.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@kilocode/cli": patch -"kilo-code": patch ---- - -Keep Windows snapshot diffs parseable and preserve valid files when a stored patch is malformed. diff --git a/.changeset/stalled-provider-first-byte.md b/.changeset/stalled-provider-first-byte.md deleted file mode 100644 index 2d11921fd6..0000000000 --- a/.changeset/stalled-provider-first-byte.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Bound the wait for a provider's first response byte by the request timeout. A provider that accepts a request and returns headers but never sends body data now fails and retries instead of leaving the turn hanging after a tool call completes. The same `timeout` value now covers both the connection phase and the wait for the first byte as a single deadline; streaming responses that have already produced data are unaffected. diff --git a/.changeset/steady-cli-subprocess-tests.md b/.changeset/steady-cli-subprocess-tests.md deleted file mode 100644 index e95a9d4c73..0000000000 --- a/.changeset/steady-cli-subprocess-tests.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Stabilize cross-platform CLI subprocess tests under constrained CI runners diff --git a/.changeset/steady-editor-tabs.md b/.changeset/steady-editor-tabs.md deleted file mode 100644 index 84e5f2b0b5..0000000000 --- a/.changeset/steady-editor-tabs.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Open Kilo chats, settings, and files as tabs in the selected editor pane without creating, locking, or resizing editor panes. diff --git a/.changeset/tidy-am-i18n-keys.md b/.changeset/tidy-am-i18n-keys.md deleted file mode 100644 index 85c3ca1581..0000000000 --- a/.changeset/tidy-am-i18n-keys.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Remove unused translation keys from the Agent Manager, sidebar webview, shared kilo-i18n, and autocomplete dictionaries across all locales, and add a conservative lint test for unreferenced, unprotected dictionary keys. diff --git a/.changeset/tidy-mice-report.md b/.changeset/tidy-mice-report.md new file mode 100644 index 0000000000..3ba5fbb6e9 --- /dev/null +++ b/.changeset/tidy-mice-report.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-telemetry": patch +--- + +Include the host operating system name, version, and architecture in telemetry events. diff --git a/.changeset/tui-variant-shortcut-hint.md b/.changeset/tui-variant-shortcut-hint.md deleted file mode 100644 index 8891876bdb..0000000000 --- a/.changeset/tui-variant-shortcut-hint.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Show the `Ctrl+T` variant cycling shortcut in the TUI prompt hint row whenever the active model exposes reasoning variants, as the first hint before the agent and command palette hints diff --git a/bun.lock b/bun.lock index fd599605c4..d98ffedb22 100644 --- a/bun.lock +++ b/bun.lock @@ -32,7 +32,7 @@ }, "packages/core": { "name": "@opencode-ai/core", - "version": "7.4.16", + "version": "7.4.17", "bin": { "opencode": "./bin/opencode", }, @@ -127,7 +127,7 @@ }, "packages/effect-drizzle-sqlite": { "name": "@opencode-ai/effect-drizzle-sqlite", - "version": "7.4.16", + "version": "7.4.17", "dependencies": { "drizzle-orm": "catalog:", "effect": "catalog:", @@ -141,7 +141,7 @@ }, "packages/effect-sqlite-node": { "name": "@opencode-ai/effect-sqlite-node", - "version": "7.4.16", + "version": "7.4.17", "dependencies": { "effect": "catalog:", }, @@ -153,7 +153,7 @@ }, "packages/http-recorder": { "name": "@opencode-ai/http-recorder", - "version": "7.4.16", + "version": "7.4.17", "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.16", + "version": "7.4.17", "dependencies": { "@kilocode/kilo-indexing": "workspace:*", "@kilocode/kilo-web-ui": "workspace:*", @@ -197,7 +197,7 @@ }, "packages/kilo-docs": { "name": "@kilocode/kilo-docs", - "version": "7.4.16", + "version": "7.4.17", "dependencies": { "@docsearch/css": "^4", "@docsearch/js": "^4", @@ -227,7 +227,7 @@ }, "packages/kilo-gateway": { "name": "@kilocode/kilo-gateway", - "version": "7.4.16", + "version": "7.4.17", "dependencies": { "@ai-sdk/alibaba": "1.0.17", "@ai-sdk/anthropic": "3.0.71", @@ -263,7 +263,7 @@ }, "packages/kilo-i18n": { "name": "@kilocode/kilo-i18n", - "version": "7.4.16", + "version": "7.4.17", "devDependencies": { "@tsconfig/node22": "catalog:", "@types/bun": "catalog:", @@ -273,7 +273,7 @@ }, "packages/kilo-indexing": { "name": "@kilocode/kilo-indexing", - "version": "7.4.16", + "version": "7.4.17", "dependencies": { "@aws-sdk/client-bedrock-runtime": "3.1005.0", "@aws-sdk/credential-provider-ini": "3.972.31", @@ -305,11 +305,11 @@ }, "packages/kilo-jetbrains": { "name": "@kilocode/kilo-jetbrains", - "version": "7.4.15", + "version": "7.4.16", }, "packages/kilo-memory": { "name": "@kilocode/kilo-memory", - "version": "7.4.16", + "version": "7.4.17", "dependencies": { "effect": "catalog:", "zod": "catalog:", @@ -323,7 +323,7 @@ }, "packages/kilo-sandbox": { "name": "@kilocode/sandbox", - "version": "7.4.16", + "version": "7.4.17", "dependencies": { "@anthropic-ai/sandbox-runtime": "catalog:", "effect": "catalog:", @@ -338,7 +338,7 @@ }, "packages/kilo-telemetry": { "name": "@kilocode/kilo-telemetry", - "version": "7.4.16", + "version": "7.4.17", "dependencies": { "@kilocode/kilo-gateway": "workspace:*", "posthog-node": "4.4.0", @@ -352,7 +352,7 @@ }, "packages/kilo-ui": { "name": "@kilocode/kilo-ui", - "version": "7.4.16", + "version": "7.4.17", "dependencies": { "@kilocode/sdk": "workspace:*", "@kobalte/core": "0.13.11", @@ -389,7 +389,7 @@ }, "packages/kilo-vscode": { "name": "kilo-code", - "version": "7.4.16", + "version": "7.4.17", "dependencies": { "@anthropic-ai/sdk": "^0.39.0", "@kilocode/kilo-gateway": "workspace:*", @@ -458,7 +458,7 @@ }, "packages/kilo-web-ui": { "name": "@kilocode/kilo-web-ui", - "version": "7.4.16", + "version": "7.4.17", "dependencies": { "@kilocode/kilo-ui": "workspace:*", "@kobalte/core": "catalog:", @@ -475,7 +475,7 @@ }, "packages/llm": { "name": "@opencode-ai/llm", - "version": "7.4.16", + "version": "7.4.17", "dependencies": { "@smithy/eventstream-codec": "4.2.14", "@smithy/util-utf8": "4.2.2", @@ -493,7 +493,7 @@ }, "packages/opencode": { "name": "@kilocode/cli", - "version": "7.4.16", + "version": "7.4.17", "bin": { "kilo": "./bin/kilo", "kilocode": "./bin/kilo", @@ -660,7 +660,7 @@ }, "packages/plugin": { "name": "@kilocode/plugin", - "version": "7.4.16", + "version": "7.4.17", "dependencies": { "@kilocode/sdk": "workspace:*", "effect": "catalog:", @@ -688,7 +688,7 @@ }, "packages/plugin-atomic-chat": { "name": "@kilocode/plugin-atomic-chat", - "version": "7.4.16", + "version": "7.4.17", "dependencies": { "@kilocode/plugin": "workspace:*", }, @@ -702,7 +702,7 @@ }, "packages/script": { "name": "@opencode-ai/script", - "version": "7.4.16", + "version": "7.4.17", "dependencies": { "semver": "^7.6.3", }, @@ -713,7 +713,7 @@ }, "packages/sdk/js": { "name": "@kilocode/sdk", - "version": "7.4.16", + "version": "7.4.17", "dependencies": { "cross-spawn": "catalog:", }, @@ -728,7 +728,7 @@ }, "packages/server": { "name": "@opencode-ai/server", - "version": "7.4.16", + "version": "7.4.17", "dependencies": { "@opencode-ai/core": "workspace:*", "drizzle-orm": "catalog:", @@ -742,7 +742,7 @@ }, "packages/storybook": { "name": "@opencode-ai/storybook", - "version": "7.4.16", + "version": "7.4.17", "devDependencies": { "@opencode-ai/ui": "workspace:*", "@solidjs/meta": "catalog:", @@ -765,7 +765,7 @@ }, "packages/tui": { "name": "@opencode-ai/tui", - "version": "7.4.16", + "version": "7.4.17", "dependencies": { "@kilocode/plugin": "workspace:*", "@kilocode/sdk": "workspace:*", @@ -792,7 +792,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "7.4.16", + "version": "7.4.17", "dependencies": { "@kilocode/sdk": "workspace:*", "@kobalte/core": "catalog:", @@ -845,22 +845,22 @@ }, }, "trustedDependencies": [ - "web-tree-sitter", "esbuild", - "tree-sitter-bash", "protobufjs", + "web-tree-sitter", + "tree-sitter-bash", ], "patchedDependencies": { - "virtua@0.49.1": "patches/virtua@0.49.1.patch", - "mammoth@1.12.0": "patches/mammoth@1.12.0.patch", - "@ff-labs/fff-bun@0.9.4": "patches/@ff-labs%2Ffff-bun@0.9.4.patch", - "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", - "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", - "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", - "pacote@21.5.1": "patches/pacote@21.5.1.patch", - "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", - "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch", "@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch", + "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", + "@ff-labs/fff-bun@0.9.4": "patches/@ff-labs%2Ffff-bun@0.9.4.patch", + "pacote@21.5.1": "patches/pacote@21.5.1.patch", + "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", + "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch", + "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", + "virtua@0.49.1": "patches/virtua@0.49.1.patch", + "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", + "mammoth@1.12.0": "patches/mammoth@1.12.0.patch", }, "overrides": { "@effect/platform-node-shared": "4.0.0-beta.74", diff --git a/package.json b/package.json index 2cbfccad00..d50ed093f2 100644 --- a/package.json +++ b/package.json @@ -171,6 +171,6 @@ "pacote@21.5.1": "patches/pacote@21.5.1.patch", "mammoth@1.12.0": "patches/mammoth@1.12.0.patch" }, - "version": "7.4.16", + "version": "7.4.17", "peerDependencies": {} } diff --git a/packages/core/package.json b/packages/core/package.json index ddb4d33d35..e64a66062d 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.16", + "version": "7.4.17", "name": "@opencode-ai/core", "type": "module", "license": "MIT", diff --git a/packages/effect-drizzle-sqlite/package.json b/packages/effect-drizzle-sqlite/package.json index 920418a989..bdc5267013 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.16", + "version": "7.4.17", "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 66a2274823..c5863bfad0 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.16", + "version": "7.4.17", "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 d31c75a3d8..a912fd5940 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.16" +version = "7.4.17" 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.16/opencode-darwin-arm64.zip" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.17/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.16/opencode-darwin-x64.zip" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.17/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.16/opencode-linux-arm64.tar.gz" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.17/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.16/opencode-linux-x64.tar.gz" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.17/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.16/opencode-windows-x64.zip" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.17/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/http-recorder/package.json b/packages/http-recorder/package.json index fec3e374d0..46de980c7a 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.16", + "version": "7.4.17", "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 82ed878f47..53ac288500 100755 --- a/packages/kilo-console/package.json +++ b/packages/kilo-console/package.json @@ -1,6 +1,6 @@ { "name": "@kilocode/kilo-console", - "version": "7.4.16", + "version": "7.4.17", "private": true, "type": "module", "scripts": { diff --git a/packages/kilo-docs/package.json b/packages/kilo-docs/package.json index cff09854e6..742763dd4d 100644 --- a/packages/kilo-docs/package.json +++ b/packages/kilo-docs/package.json @@ -1,6 +1,6 @@ { "name": "@kilocode/kilo-docs", - "version": "7.4.16", + "version": "7.4.17", "private": true, "scripts": { "dev": "next dev --webpack --port 3002", diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/prompt-rail-many-prompts-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/prompt-rail-many-prompts-chromium-linux.png new file mode 100644 index 0000000000..df4e9c7068 --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/prompt-rail-many-prompts-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:51adb9e31ce0bc82981b0f20f895ce4ede3f92828546115f929942ef93bf0812 +size 11204 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/prompt-rail-sidebar-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/prompt-rail-sidebar-chromium-linux.png new file mode 100644 index 0000000000..94024986cd --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/prompt-rail-sidebar-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c5658ed9e5266311c4239b7cd470d2f772d1cc0b3fd6c6b00337d4f3141a3a4d +size 11950 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/prompt-rail-wide-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/prompt-rail-wide-chromium-linux.png new file mode 100644 index 0000000000..07e3e945ef --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/prompt-rail-wide-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:095ed97dc46498be6fd75483b24357ac0740b6f2c1544f6c103a1dac360e1a7e +size 11202 diff --git a/packages/kilo-gateway/package.json b/packages/kilo-gateway/package.json index 37461419a9..15fac962bb 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.16", + "version": "7.4.17", "type": "module", "license": "MIT", "description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration", diff --git a/packages/kilo-i18n/package.json b/packages/kilo-i18n/package.json index f690fe2f5f..e50df8969b 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.16", + "version": "7.4.17", "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 eac32eee09..fd56507373 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.16", + "version": "7.4.17", "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 7c48f6085e..90eda933de 100644 --- a/packages/kilo-jetbrains/CHANGELOG.md +++ b/packages/kilo-jetbrains/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 7.5.0 + +### Minor Changes + +- [#12518](https://github.com/Kilo-Org/kilocode/pull/12518) [`452d0eb`](https://github.com/Kilo-Org/kilocode/commit/452d0eb55f740e951cfd906375e22cf97250144c) - Publish a signed GitHub-hosted JetBrains plugin build with the CLI bundled for offline installation. + +### Patch Changes + +- [#12571](https://github.com/Kilo-Org/kilocode/pull/12571) [`9950739`](https://github.com/Kilo-Org/kilocode/commit/9950739e36b40a682c0a25173e62f5236e60f81a) - Allow sending prompts while a session is busy and show queued prompts with a remove action. + ## 7.4.16 ### Patch Changes @@ -134,7 +144,9 @@ ### Changed - Update the JetBrains CLI pin from Kilo Core 7.4.15 to 7.4.16. + ## [7.0.10] - 2026-07-24 + ## [7.0.10] - 2026-07-24 ### Added diff --git a/packages/kilo-memory/package.json b/packages/kilo-memory/package.json index df2adc3325..d48bcabe87 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.16", + "version": "7.4.17", "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 ff29edd756..b57728afed 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.16", + "version": "7.4.17", "type": "module", "license": "MIT", "private": true, diff --git a/packages/kilo-telemetry/package.json b/packages/kilo-telemetry/package.json index 7b9e81497a..36243e82c9 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.16", + "version": "7.4.17", "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 ff87bf6909..939bb5dd24 100644 --- a/packages/kilo-telemetry/src/__tests__/telemetry.test.ts +++ b/packages/kilo-telemetry/src/__tests__/telemetry.test.ts @@ -1,4 +1,6 @@ -import { describe, test, expect, beforeEach } from "bun:test" +import { arch, platform, release } from "node:os" +import { describe, test, expect, beforeEach, spyOn } from "bun:test" +import { Client } from "../client.js" import { Identity } from "../identity.js" import { TelemetryEvent } from "../events.js" import { Telemetry } from "../telemetry.js" @@ -83,6 +85,22 @@ describe("TelemetryEvent", () => { }) describe("Telemetry", () => { + test("includes host OS properties", () => { + const capture = spyOn(Client, "capture").mockImplementation(() => {}) + + Telemetry.track(TelemetryEvent.CLI_START) + + expect(capture).toHaveBeenCalledWith( + TelemetryEvent.CLI_START, + expect.objectContaining({ + os_name: platform(), + os_version: release(), + os_arch: arch(), + }), + ) + capture.mockRestore() + }) + test("indexing helpers are exposed", () => { expect(typeof Telemetry.trackIndexingStarted).toBe("function") expect(typeof Telemetry.trackIndexingCompleted).toBe("function") @@ -96,4 +114,3 @@ describe("Telemetry", () => { expect(typeof Telemetry.trackSuggestionAccepted).toBe("function") }) }) - diff --git a/packages/kilo-telemetry/src/telemetry.ts b/packages/kilo-telemetry/src/telemetry.ts index 3ac0d42e39..4d16051c2d 100644 --- a/packages/kilo-telemetry/src/telemetry.ts +++ b/packages/kilo-telemetry/src/telemetry.ts @@ -1,3 +1,4 @@ +import { release } from "node:os" import { Client } from "./client.js" import { Identity } from "./identity.js" import { TelemetryEvent } from "./events.js" @@ -6,6 +7,9 @@ export interface TelemetryProperties { appName: string appVersion: string platform: string + os_name: string + os_version: string + os_arch: string editorName?: string vscodeVersion?: string } @@ -58,6 +62,9 @@ export namespace Telemetry { appName: "kilo-cli", appVersion: "unknown", platform: process.platform, + os_name: process.platform, + os_version: release(), + os_arch: process.arch, } export async function init(options: { dataPath: string; version: string; enabled: boolean }): Promise { @@ -109,6 +116,9 @@ export namespace Telemetry { appName: props.appName, appVersion: props.appVersion, platform: props.platform, + os_name: props.os_name, + os_version: props.os_version, + os_arch: props.os_arch, }) // Link the anonymous machineId to the authenticated email diff --git a/packages/kilo-ui/package.json b/packages/kilo-ui/package.json index 3043aedbab..25e78d8ac0 100644 --- a/packages/kilo-ui/package.json +++ b/packages/kilo-ui/package.json @@ -1,6 +1,6 @@ { "name": "@kilocode/kilo-ui", - "version": "7.4.16", + "version": "7.4.17", "type": "module", "license": "MIT", "exports": { diff --git a/packages/kilo-vscode/CHANGELOG.md b/packages/kilo-vscode/CHANGELOG.md index d41e4a8c94..8e0e1d920c 100644 --- a/packages/kilo-vscode/CHANGELOG.md +++ b/packages/kilo-vscode/CHANGELOG.md @@ -1,5 +1,78 @@ # kilo-code +## 7.4.17 + +### Minor Changes + +- [#12631](https://github.com/Kilo-Org/kilocode/pull/12631) [`3321216`](https://github.com/Kilo-Org/kilocode/commit/3321216c0157e1a8a1829b0c8e0a1cae8d2f2ad2) - Show the ⌘1-9 (Ctrl+1-9 on Windows/Linux) shortcut badges on every Agent Manager sidebar card while the modifier key is held, making it easy to see which number jumps to which worktree before pressing it. + +- [#12598](https://github.com/Kilo-Org/kilocode/pull/12598) [`c6711fc`](https://github.com/Kilo-Org/kilocode/commit/c6711fcf6cea9fdbe78a04b95276d09a2faabfa7) - Let users open Agent Manager terminals in the VS Code terminal or an embedded side panel. The terminal button's dropdown picks the destination; the side panel shares the right-hand inspector with the diff view and keeps running in the background when hidden. + +- [#12633](https://github.com/Kilo-Org/kilocode/pull/12633) [`23039c0`](https://github.com/Kilo-Org/kilocode/commit/23039c0fb1e5b32704119ddde10a6a28ccd6bff3) - Support multiple Agent Manager side-panel terminals per context. The panel header is now a tab strip that reuses the main tab bar's terminal tabs: click to switch, drag to reorder, X to close a single terminal, and + to open another one. Terminal numbers fill gaps left by closed terminals, and tabs pick up the live title from the shell or running program (OSC escape codes), so a dev server or build names its own tab. + +- [#12632](https://github.com/Kilo-Org/kilocode/pull/12632) [`0d853df`](https://github.com/Kilo-Org/kilocode/commit/0d853df3ec338ac99e025939f74136dec6d9daa1) - Add a prompt navigator rail to the chat transcript. A thin rail of ticks on the left edge shows one tick per prompt you sent; hovering or focusing it expands a card listing those prompts with a short preview of the answer, and clicking jumps the transcript to that turn. It appears in the sidebar, Kilo editor tabs, the sub-agent viewer, and Agent Manager, and never changes the readable width of the chat. + +### Patch Changes + +- [#12629](https://github.com/Kilo-Org/kilocode/pull/12629) [`0a1c140`](https://github.com/Kilo-Org/kilocode/commit/0a1c14073a4bf14f8ad4e3c8295dc6ae6bfbfdaf) - Keep each Agent Manager panel's terminal destination consistent. A dropdown pick is now remembered per panel and no longer flips when another window rewrites the shared terminal destination setting, so the terminal shortcut keeps opening the terminal type that panel is actually using. The shortcut also no longer dead-ends on worktrees without an active session, and terminals left over from a reloaded webview are cleaned up instead of leaking. + +- [#12587](https://github.com/Kilo-Org/kilocode/pull/12587) [`16f8e7e`](https://github.com/Kilo-Org/kilocode/commit/16f8e7ef7fbd47755395539e7df54af3baae0c63) - Keep conversations and workspace files unchanged when a checkpoint cannot be fully restored. + +- [#12333](https://github.com/Kilo-Org/kilocode/pull/12333) [`290a5af`](https://github.com/Kilo-Org/kilocode/commit/290a5af56e6ddccd8b4a459883625e30f2ae0344) Thanks [@IamCoder18](https://github.com/IamCoder18)! - Include basic-auth credentials in the Local and Network Console URLs printed by `kilo console`, so users on headless hosts (no `DISPLAY`/`WAYLAND_DISPLAY`, SSH sessions, CI runners) can open the URL in a browser on another machine and reach the Console. + +- [#12630](https://github.com/Kilo-Org/kilocode/pull/12630) [`a7f972f`](https://github.com/Kilo-Org/kilocode/commit/a7f972f63bc948d70b73a36a5beab6d694316037) - Open Agent Manager terminals faster and avoid delayed shell prompts. + +- [#12560](https://github.com/Kilo-Org/kilocode/pull/12560) [`65c5e9d`](https://github.com/Kilo-Org/kilocode/commit/65c5e9d2c03cea152b140710228075edf9156def) - Update the visible agent mode when cycling modes in Kilo sidebars and pending session tabs. + +- [#12561](https://github.com/Kilo-Org/kilocode/pull/12561) [`44f5963`](https://github.com/Kilo-Org/kilocode/commit/44f596366931d5336f1cd4dfdd97ef54e0f2fa4c) - Fix settings changes sometimes failing to save and apply in VS Code. + +- [#12540](https://github.com/Kilo-Org/kilocode/pull/12540) [`2da8949`](https://github.com/Kilo-Org/kilocode/commit/2da89498138e49f857c354924fdecac85337e742) Thanks [@Githubguy132010](https://github.com/Githubguy132010)! - Preserve parenthesized tilde expressions as literal text in rendered chat messages. + +- [#12591](https://github.com/Kilo-Org/kilocode/pull/12591) [`625d2b9`](https://github.com/Kilo-Org/kilocode/commit/625d2b974de1381b4d475808c9215becb263d1f0) - Show prompt input toggle tooltips instantly on hover instead of after a delay. + +- [#12460](https://github.com/Kilo-Org/kilocode/pull/12460) [`51d8031`](https://github.com/Kilo-Org/kilocode/commit/51d8031c9997bd5478bcde715562169f732d04d4) - Changes from opencode v1.17.5 to v1.17.9 upstream: + - Core Bugfixes: Improved MCP server compatibility by declaring Kilo's supported client capabilities. + - Core Bugfixes: Plugin client requests now reuse the active server instead of assuming the default local port. + - Core Bugfixes: ACP shell tool calls now show the command and working directory from the start. + - Core Bugfixes: Plugin-provided shell environment variables now apply to PTY sessions. + - Core Bugfixes: OpenAI-compatible providers now accept MCP tool schemas that previously failed validation. (@jquense) + - Core Bugfixes: Cloudflare AI Gateway now receives the configured API key correctly. (@keefetang) + - Core Bugfixes: MCP tools without declared schema properties now work with providers that expect object properties. + - Core Bugfixes: Long-running MCP tools now keep their timeout alive when they report progress. (@Nomadcxx) + - Core Bugfixes: The MCP OAuth callback server now shuts down once authorization finishes or is cancelled. + - Core Bugfixes: MCP tool failures now surface the server's error text instead of a generic failure. + - Core Bugfixes: MCP OAuth error pages now escape provider error text correctly. + - Core Bugfixes: Honor configured agent step limits by forcing a final text response instead of failing mid-run. + - Core Bugfixes: Queue steering prompts before dismissing pending questions so the previous turn cannot resume first. + - Core Bugfixes: Prevent local server credentials from leaking into spawned PTY processes. + - Core Bugfixes: Fix Devstral model detection when provider IDs use different casing. (@Robin1987China) + - Core Bugfixes: Pass configured custom headers to Copilot model requests. + - Core Improvements: MCP servers can now receive the current workspace as a client root. + - Core Improvements: Session timelines load much faster and avoid flicker or scroll jumps. + - Core Improvements: Add `high` and `max` thinking variants for GLM-5.2 across supported providers. (@imranshaiedi-byte) + - Core Improvements: Stop wrapping follow-up user messages in a steering reminder so prompt caching stays effective. + - TUI Bugfixes: MCP debug now uses the SDK's latest protocol version. + - TUI Bugfixes: Only show the background subagent shortcut when the server supports it. + - UI Bugfixes: Render completed Mermaid blocks from diagram source instead of fenced Markdown. + +- [#11984](https://github.com/Kilo-Org/kilocode/pull/11984) [`26dac19`](https://github.com/Kilo-Org/kilocode/commit/26dac197fe28294c391c8d437abf06e18e2d22bd) Thanks [@sylwester-liljegren](https://github.com/sylwester-liljegren)! - Mention `@` file references in the chat input placeholder so users know they can add file mentions. Translated across all supported languages. + +- [#12593](https://github.com/Kilo-Org/kilocode/pull/12593) [`160b066`](https://github.com/Kilo-Org/kilocode/commit/160b06661acc5f04b21221ab6578c468325f64c5) - Prevent the VS Code backend from eagerly starting native file watchers for every Agent Manager worktree. + +- [#12123](https://github.com/Kilo-Org/kilocode/pull/12123) [`3075d35`](https://github.com/Kilo-Org/kilocode/commit/3075d35f13ba9738446ac28fa2eebf054097f2f5) Thanks [@mjnaderi](https://github.com/mjnaderi)! - Keep message and response copy buttons working after switching focus away from VS Code. + +- [#12583](https://github.com/Kilo-Org/kilocode/pull/12583) [`1310c12`](https://github.com/Kilo-Org/kilocode/commit/1310c1200ab613b316f27fe4fd59e23e262df02f) Thanks [@noobezlol](https://github.com/noobezlol)! - Keep Windows snapshot diffs parseable and preserve valid files when a stored patch is malformed. + +- [#12410](https://github.com/Kilo-Org/kilocode/pull/12410) [`85d65a3`](https://github.com/Kilo-Org/kilocode/commit/85d65a3137ecadfcbda8255bfb4a40daf1f155fb) - Open Kilo chats, settings, and files as tabs in the selected editor pane without creating, locking, or resizing editor panes. + +- [#12639](https://github.com/Kilo-Org/kilocode/pull/12639) [`8a47d8b`](https://github.com/Kilo-Org/kilocode/commit/8a47d8b78885fa8fd14c73b3aecdb57e1fc96c9c) - Stop flashing a "Turn interrupted" warning when a follow-up message is queued while the assistant is still working. The running turn now closes with a dedicated "superseded" reason instead of "interrupted" when it hands off to the queued prompt, so the premature-stop warning only appears for real interruptions. + +- [#12602](https://github.com/Kilo-Org/kilocode/pull/12602) [`5d87ca5`](https://github.com/Kilo-Org/kilocode/commit/5d87ca598c4c66328f0e476cb1be3fc9b26d05aa) - Remove unused translation keys from the Agent Manager, sidebar webview, shared kilo-i18n, and autocomplete dictionaries across all locales, and add a conservative lint test for unreferenced, unprotected dictionary keys. + +- [#12463](https://github.com/Kilo-Org/kilocode/pull/12463) [`1f3383c`](https://github.com/Kilo-Org/kilocode/commit/1f3383cf3de37327b02e0fc2a1c5ac176ca9134f) Thanks [@IamCoder18](https://github.com/IamCoder18)! - Show the `Ctrl+T` variant cycling shortcut in the TUI prompt hint row whenever the active model exposes reasoning variants, as the first hint before the agent and command palette hints + +- [#12634](https://github.com/Kilo-Org/kilocode/pull/12634) [`4c5c242`](https://github.com/Kilo-Org/kilocode/commit/4c5c2428927f26c4c818f23a650cfaf5723b7641) - Show the worktree directory name on the Agent Manager worktree hover card + ## 7.4.16 ### Minor Changes diff --git a/packages/kilo-vscode/eslint.config.mjs b/packages/kilo-vscode/eslint.config.mjs index d19df68b45..97181f7f60 100644 --- a/packages/kilo-vscode/eslint.config.mjs +++ b/packages/kilo-vscode/eslint.config.mjs @@ -43,17 +43,13 @@ export default [ }, { files: ["webview-ui/agent-manager/AgentManagerApp.tsx"], - // Raised from 3100 → 3200 for the experimental terminal tabs feature. - // ~600 lines of terminal logic were extracted to ./terminal/* and - // ./tab-rendering.tsx; the remaining ~75 lines are signal bindings, - // a stacking-container wrapper required by the hydration invariant - // (canvases must never leave the paint tree — see render.tsx), and - // render-call wiring that must live at the top of - // `AgentManagerContent` alongside the existing selection/session state. - // Raised from 3200 → 3210 for the per-message feedback `FeedbackProvider` - // wiring, which sits inside the provider chain and cannot be extracted - // without adding an intermediate wrapper component. - rules: { complexity: ["error", 74], "max-lines": ["error", 3210] }, + // Complexity stays exempt: the top of `AgentManagerContent` wires many + // selection/session handlers that can't be split without threading shared + // reactive state. Line count needs no override — the apply-to-local + // workflow is extracted to ./apply-to-local.tsx, keeping the file under + // the global 3000-line default. Do not add a max-lines override back; + // extract cohesive domains out of the file instead. + rules: { complexity: ["error", 74] }, }, { files: ["src/agent-manager/AgentManagerProvider.ts"], diff --git a/packages/kilo-vscode/package.json b/packages/kilo-vscode/package.json index 45d6fbeb52..df9e75a70b 100644 --- a/packages/kilo-vscode/package.json +++ b/packages/kilo-vscode/package.json @@ -2,7 +2,7 @@ "name": "kilo-code", "displayName": "Kilo Code: AI Coding Agent, Copilot, and Autocomplete", "description": "Open Source AI coding agent that generates code from natural language, automates tasks, and runs terminal commands. Features inline autocomplete, browser automation, automated refactoring, and custom modes for planning, coding, and debugging. Supports 500+ AI models including Claude (Anthropic), Gemini, Grok, GPT, Codex and GLM.", - "version": "7.4.16", + "version": "7.4.17", "icon": "assets/icons/logo-outline-black.png", "galleryBanner": { "color": "#FFFFFF", @@ -868,7 +868,8 @@ "tr", "nl", "uk", - "it" + "it", + "fa" ], "enumDescriptions": [ "Auto (VS Code language)", @@ -891,7 +892,8 @@ "Türkçe", "Nederlands", "Українська", - "Italiano" + "Italiano", + "فارسی" ] }, "kilo-code.new.languageCommitMessage": { @@ -919,7 +921,8 @@ "tr", "nl", "uk", - "it" + "it", + "fa" ], "enumDescriptions": [ "跟随界面语言 (Sync with UI language)", @@ -942,7 +945,8 @@ "Türkçe", "Nederlands", "Українська", - "Italiano" + "Italiano", + "فارسی" ] }, "kilo-code.new.model.providerID": { @@ -1028,7 +1032,7 @@ "Open or focus the VS Code integrated terminal.", "Open or focus an embedded terminal in the Agent Manager side panel." ], - "description": "Choose where the Agent Manager terminal button and Focus Terminal keyboard shortcut open a terminal." + "description": "Default destination for the Agent Manager terminal button and Focus Terminal keyboard shortcut. The terminal button's dropdown remembers its own choice per Agent Manager panel; this setting only applies to panels that never picked one." }, "kilo-code.new.indexing.showButtonWhenDisabled": { "type": "boolean", diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 48817cf901..83c41aaf09 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -161,6 +161,7 @@ import { AnacondaDesktopBridge } from "./anaconda-desktop/bridge" import { fetchOpenAIModels, FetchModelsError } from "./shared/fetch-models" import type { Agent } from "@kilocode/sdk/v2/client" import { configFeatures } from "./features" +import { fetchSnapshot } from "./kilo-provider/config-snapshot" import { createAutoApproveBridge } from "./kilo-provider/auto-approve" import type { KiloProviderOptions } from "./kilo-provider/options" import { fetchKiloEmbeddingModelCatalog } from "@kilocode/kilo-gateway" @@ -2562,24 +2563,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper } try { - const workspaceDir = this.getWorkspaceDirectory() - const [{ data: config }, { data: global }, { data: overlay }] = await Promise.all([ - retry(() => this.client!.config.get({ directory: workspaceDir }, { throwOnError: true })), - this.client.global.config.get({ throwOnError: true }), - this.client.config.overlay({ directory: workspaceDir, scope: "project" }, { throwOnError: true }), - ]) - this.cachedGlobalConfig = global ?? null - - const message = { - type: "configLoaded", - config, - globalConfig: global, - projectConfig: overlay?.project, - settings: { maxCost: this.maxCostSetting(), languageCommitMessage: this.commitMessageLanguageSetting() }, - features: configFeatures(config), - } - this.cachedConfigMessage = message - this.postMessage(message) + await this.refreshConfig("configLoaded") } catch (error) { console.error("[Kilo New] KiloProvider: Failed to fetch config:", error) } @@ -2673,29 +2657,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper private async fetchAndSendConfigUpdated(): Promise { if (!this.client || this.connectionState !== "connected") return try { - const dir = this.getWorkspaceDirectory() - const [{ data: config }, { data: global }, { data: overlay }] = await Promise.all([ - retry(() => this.client!.config.get({ directory: dir }, { throwOnError: true })), - this.client.global.config.get({ throwOnError: true }), - this.client.config.overlay({ directory: dir, scope: "project" }, { throwOnError: true }), - ]) - this.cachedGlobalConfig = global ?? null - this.cachedConfigMessage = { - type: "configLoaded", - config, - globalConfig: global, - projectConfig: overlay?.project, - settings: { maxCost: this.maxCostSetting(), languageCommitMessage: this.commitMessageLanguageSetting() }, - features: configFeatures(config), - } - this.postMessage({ - type: "configUpdated", - config, - globalConfig: global, - projectConfig: overlay?.project, - settings: { maxCost: this.maxCostSetting(), languageCommitMessage: this.commitMessageLanguageSetting() }, - features: configFeatures(config), - }) + await this.refreshConfig("configUpdated") } catch (error) { console.error("[Kilo New] KiloProvider: Failed to fetch config after update:", error) } @@ -3064,28 +3026,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper } try { - const [{ data: merged }, { data: global }, { data: overlay }] = await Promise.all([ - retry(() => this.client!.config.get({ directory: dir }, { throwOnError: true })), - this.client.global.config.get({ throwOnError: true }), - this.client.config.overlay({ directory: dir, scope: "project" }, { throwOnError: true }), - ]) - this.cachedGlobalConfig = global ?? null - this.cachedConfigMessage = { - type: "configLoaded", - config: merged, - globalConfig: global, - projectConfig: overlay?.project, - settings: { maxCost: this.maxCostSetting(), languageCommitMessage: this.commitMessageLanguageSetting() }, - features: configFeatures(merged), - } - this.postMessage({ - type: "configUpdated", - config: merged, - globalConfig: global, - projectConfig: overlay?.project, - settings: { maxCost: this.maxCostSetting(), languageCommitMessage: this.commitMessageLanguageSetting() }, - features: configFeatures(merged), - }) + await this.refreshConfig("configUpdated", dir) this.requirements.clear() await Promise.all([ refreshProviders ? this.fetchAndSendProviders() : Promise.resolve(), @@ -3113,6 +3054,17 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.pending-- } } + + private async refreshConfig(type: "configLoaded" | "configUpdated", dir = this.getWorkspaceDirectory()) { + const snapshot = await fetchSnapshot(this.client!, dir, () => ({ + maxCost: this.maxCostSetting(), + languageCommitMessage: this.commitMessageLanguageSetting(), + })) + this.cachedGlobalConfig = snapshot.globalConfig ?? null + this.cachedConfigMessage = { type: "configLoaded", ...snapshot } + this.postMessage({ type, ...snapshot }) + } + private postConfigFailure(error: unknown): void { console.error("[Kilo New] KiloProvider: Failed to update config:", error) this.postMessage({ diff --git a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts index bf72af74ab..e8e668b4b5 100644 --- a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts +++ b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts @@ -103,6 +103,7 @@ export class AgentManagerProvider implements Disposable { ) this.terminalRouter = new TerminalRouter({ getClient: () => this.connectionService.getClient(), + getClientAsync: () => this.connectionService.getClientAsync(this.getRoot()), getServerConfig: () => this.connectionService.getServerConfig() ?? undefined, getRoot: () => this.getRoot(), getWorktreePath: (id) => this.getStateManager()?.getWorktree(id)?.path, @@ -593,6 +594,10 @@ export class AgentManagerProvider implements Disposable { this.terminalManager.showLocalTerminal() return null } + if (m.type === "agentManager.showWorktreeTerminal") { + this.terminalManager.showWorktreeTerminal(m.worktreeId, this.state) + return null + } if (m.type === "agentManager.openWorktree") { this.openWorktreeDirectory(m.worktreeId) return null @@ -720,6 +725,13 @@ export class AgentManagerProvider implements Disposable { } private onRequestState(): void { + // requestState fires from the webview's onMount, and a freshly mounted + // webview has no terminal records — any PTYs the router still tracks + // belong to a previous webview instance (reload or crash) and are + // unreachable orphans. Kill them here rather than leaking shells until + // the panel itself is disposed. In-flight creates from the dying + // instance are reaped by the router's generation guard. + void this.terminalRouter.dispose() void this.stateReady ?.then(() => { // When the folder is not a git repo (or has no folder open), @@ -906,8 +918,9 @@ export class AgentManagerProvider implements Disposable { case "agentManager.toggleSectionCollapsed": case "agentManager.moveToSection": case "agentManager.moveSection": - case "agentManager.terminal.create": return true + case "agentManager.terminal.create": + return m.worktreeId !== null default: return false } diff --git a/packages/kilo-vscode/src/agent-manager/SessionTerminalManager.ts b/packages/kilo-vscode/src/agent-manager/SessionTerminalManager.ts index 6120ba6a1c..3fc41c1170 100644 --- a/packages/kilo-vscode/src/agent-manager/SessionTerminalManager.ts +++ b/packages/kilo-vscode/src/agent-manager/SessionTerminalManager.ts @@ -34,6 +34,8 @@ export interface Disposable { */ export class SessionTerminalManager { private static readonly LOCAL_KEY = "__local__" + private static readonly SESSION_PREFIX = "session:" + private static readonly WORKTREE_PREFIX = "worktree:" private terminals = new Map() private disposables: Disposable[] = [] @@ -47,10 +49,10 @@ export class SessionTerminalManager { ) { this.disposables.push( host.onTerminalClosed((terminal) => { - for (const [sessionId, entry] of this.terminals) { + for (const [key, entry] of this.terminals) { if (entry.terminal !== terminal) continue - this.terminals.delete(sessionId) - this.log(`Removed terminal mapping for session ${sessionId} (terminal closed)`) + this.terminals.delete(key) + this.log(`Removed terminal mapping for ${key} (terminal closed)`) break } this.updateContextKey() @@ -86,7 +88,8 @@ export class SessionTerminalManager { */ showTerminal(sessionId: string, state: WorktreeStateManager | undefined): void { // If terminal already exists, just focus it - if (this.showExisting(sessionId, false)) return + const key = SessionTerminalManager.sessionKey(sessionId) + if (this.showExistingKey(key, false)) return const repoPath = this.host.repoPath() const worktreePath = state?.directoryFor(sessionId) @@ -102,7 +105,7 @@ export class SessionTerminalManager { const worktree = session?.worktreeId ? state?.getWorktree(session.worktreeId) : undefined const name = worktree ? `Agent: ${worktree.branch}` : "Agent: local" - this.showOrCreate(sessionId, cwd, name) + this.showOrCreate(key, cwd, name) } /** @@ -110,7 +113,7 @@ export class SessionTerminalManager { * Used when the user triggers a terminal in local mode without an active session. */ showLocalTerminal(): void { - if (this.showExisting(SessionTerminalManager.LOCAL_KEY, false)) return + if (this.showExistingKey(SessionTerminalManager.LOCAL_KEY, false)) return const cwd = this.host.repoPath() if (!cwd) { @@ -122,11 +125,32 @@ export class SessionTerminalManager { this.showOrCreate(SessionTerminalManager.LOCAL_KEY, cwd, "Agent: local") } + /** + * Show (or create) a terminal rooted at a worktree directory. Used when + * the worktree has no session to key the terminal off (e.g. all of its + * sessions were closed) so the shortcut never dead-ends on a sessionless + * worktree. + */ + showWorktreeTerminal(worktreeId: string, state: WorktreeStateManager | undefined): void { + const key = SessionTerminalManager.worktreeKey(worktreeId) + if (this.showExistingKey(key, false)) return + + const worktree = state?.getWorktree(worktreeId) + const cwd = worktree?.path ?? this.host.repoPath() + if (!cwd) { + this.log(`showWorktreeTerminal: no cwd resolved for worktree ${worktreeId}`) + this.host.showWarning("Open a folder that contains a git repository to use worktrees") + return + } + + this.showOrCreate(key, cwd, worktree ? `Agent: ${worktree.branch}` : "Agent: worktree") + } + /** * Show the existing local terminal if one was previously created (used on context switch). */ showExistingLocal(): boolean { - return this.showExisting(SessionTerminalManager.LOCAL_KEY) + return this.showExistingKey(SessionTerminalManager.LOCAL_KEY) } /** @@ -159,34 +183,38 @@ export class SessionTerminalManager { * Pass preserveFocus=true to keep focus on the current editor (default for session switching). */ showExisting(sessionId: string, preserveFocus = true): boolean { - const entry = this.terminals.get(sessionId) + return this.showExistingKey(SessionTerminalManager.sessionKey(sessionId), preserveFocus) + } + + private showExistingKey(key: string, preserveFocus = true): boolean { + const entry = this.terminals.get(key) if (!entry) return false if (entry.terminal.exitStatus !== undefined) { - this.terminals.delete(sessionId) - this.log(`showExisting: terminal exited for session ${sessionId}, clearing`) + this.terminals.delete(key) + this.log(`showExisting: terminal exited for ${key}, clearing`) return false } entry.terminal.show(preserveFocus) this.panelOpen = true - this.log(`showExisting: revealed terminal for session ${sessionId}`) + this.log(`showExisting: revealed terminal for ${key}`) return true } - activeSession(): string | undefined { + private activeKey(): string | undefined { const active = this.host.activeTerminal() if (!active) return undefined - for (const [id, entry] of this.terminals) { - if (entry.terminal === active && entry.terminal.exitStatus === undefined) return id + for (const [key, entry] of this.terminals) { + if (entry.terminal === active && entry.terminal.exitStatus === undefined) return key } return undefined } prepareContext(sessionId: string): boolean { if (this.showExisting(sessionId)) return true - const active = this.activeSession() - return !active || active === sessionId + const active = this.activeKey() + return !active || active === SessionTerminalManager.sessionKey(sessionId) } dispose(): void { @@ -254,32 +282,40 @@ export class SessionTerminalManager { void this.host.setContext("kilo-code.agentTerminalFocus", managed) } - private showOrCreate(sessionId: string, cwd: string, name: string): void { - let entry = this.terminals.get(sessionId) + private showOrCreate(key: string, cwd: string, name: string): void { + let entry = this.terminals.get(key) // Clean up exited terminals if (entry && entry.terminal.exitStatus !== undefined) { - this.terminals.delete(sessionId) + this.terminals.delete(key) entry = undefined } // Recreate if CWD changed if (entry && entry.cwd !== cwd) { entry.terminal.dispose() - this.terminals.delete(sessionId) + this.terminals.delete(key) entry = undefined - this.log(`showTerminal: cwd changed for session ${sessionId}, recreating`) + this.log(`showTerminal: cwd changed for ${key}, recreating`) } if (!entry) { const terminal = this.host.createTerminal({ cwd, name }) entry = { terminal, cwd } - this.terminals.set(sessionId, entry) - this.log(`showTerminal: created terminal for session ${sessionId} (cwd=${cwd})`) + this.terminals.set(key, entry) + this.log(`showTerminal: created terminal for ${key} (cwd=${cwd})`) } entry.terminal.show(false) this.panelOpen = true this.updateContextKey() } + + private static sessionKey(id: string): string { + return `${SessionTerminalManager.SESSION_PREFIX}${id}` + } + + private static worktreeKey(id: string): string { + return `${SessionTerminalManager.WORKTREE_PREFIX}${id}` + } } diff --git a/packages/kilo-vscode/src/agent-manager/__tests__/AgentManagerProvider.spec.ts b/packages/kilo-vscode/src/agent-manager/__tests__/AgentManagerProvider.spec.ts index 045a108faf..7766476185 100644 --- a/packages/kilo-vscode/src/agent-manager/__tests__/AgentManagerProvider.spec.ts +++ b/packages/kilo-vscode/src/agent-manager/__tests__/AgentManagerProvider.spec.ts @@ -39,6 +39,7 @@ vi.mock("../SessionTerminalManager", () => ({ SessionTerminalManager: class { showTerminal() {} showLocalTerminal() {} + showWorktreeTerminal() {} syncLocalOnSessionSwitch() {} syncOnSessionSwitch() { return false @@ -216,6 +217,20 @@ describe("AgentManagerProvider worktree creation", () => { expect(manager.createWorktreeOnDisk).toHaveBeenCalledTimes(1) }) + it("disposes orphaned terminals when a freshly mounted webview requests state", async () => { + const manager = createHarness() + const dispose = vi.fn().mockResolvedValue(undefined) + manager.terminalRouter = { handle: vi.fn().mockReturnValue(false), dispose } as unknown as { + handle: ReturnType + } + // Avoid the vscode-backed pushEmptyState; the disposal under test is synchronous. + ;(manager as unknown as Record).pushEmptyState = vi.fn() + + await manager.onMessage({ type: "agentManager.requestState" }) + + expect(dispose).toHaveBeenCalledTimes(1) + }) + it("routes file search through the active worktree session", async () => { const manager = createHarness() manager.activeSessionId = "session-wt" diff --git a/packages/kilo-vscode/src/agent-manager/terminal-manager.ts b/packages/kilo-vscode/src/agent-manager/terminal-manager.ts index c1662d92b3..9f3d030728 100644 --- a/packages/kilo-vscode/src/agent-manager/terminal-manager.ts +++ b/packages/kilo-vscode/src/agent-manager/terminal-manager.ts @@ -113,6 +113,16 @@ export class TerminalManager { } } + /** Titles of every live terminal in a context — used by the router to + * pick the lowest free "Terminal N" ordinal. */ + titles(worktreeId: string | null): string[] { + const out: string[] = [] + for (const entry of this.entries.values()) { + if (entry.worktreeId === worktreeId) out.push(entry.title) + } + return out + } + /** Kill a single terminal. Best-effort — we always drop our bookkeeping. * The SDK's `pty.remove` returns `{ data, error }` without throwing * on 4xx/5xx, so we have to check `error` ourselves; otherwise a diff --git a/packages/kilo-vscode/src/agent-manager/terminal-routing.ts b/packages/kilo-vscode/src/agent-manager/terminal-routing.ts index 566952f9b2..6ca6491c7a 100644 --- a/packages/kilo-vscode/src/agent-manager/terminal-routing.ts +++ b/packages/kilo-vscode/src/agent-manager/terminal-routing.ts @@ -26,6 +26,8 @@ interface ServerConfig { export interface TerminalRoutingDeps { /** Shared SDK client. Throws when the CLI backend is not connected. */ getClient(): KiloClient + /** Shared SDK client, connecting the CLI backend when needed. */ + getClientAsync(): Promise /** Loopback URL + basic-auth password for the running `kilo serve`. */ getServerConfig(): ServerConfig | undefined /** Workspace root — used as cwd fallback when no worktree is selected (LOCAL). */ @@ -53,7 +55,9 @@ function isTerminalMessage( export class TerminalRouter { private manager: TerminalManager - private readonly ordinals = new Map() + /** Ordinals reserved by in-flight creates, per context — prevents two + * concurrent creates from grabbing the same "Terminal N" title. */ + private readonly reserved = new Map>() private generation = 0 constructor(private readonly deps: TerminalRoutingDeps) { @@ -100,6 +104,7 @@ export class TerminalRouter { this.generation++ const manager = this.manager this.manager = this.createManager() + this.reserved.clear() return manager.dispose() } @@ -117,8 +122,12 @@ export class TerminalRouter { }) return } - const title = `Terminal ${this.nextOrdinal(worktreeId)}` + const ordinal = this.reserveOrdinal(worktreeId) + const title = `Terminal ${ordinal}` try { + // Join the shared backend connection instead of racing its synchronous + // client accessor when this is the first Kilo action in the window. + await this.deps.getClientAsync() const created = await manager.create({ worktreeId, cwd, title }) if (generation !== this.generation) { await manager.close(created.terminalId) @@ -139,6 +148,11 @@ export class TerminalRouter { const message = err instanceof Error ? err.message : String(err) this.deps.log(`Terminal create failed: ${message}`) this.deps.post({ type: "agentManager.terminal.error", createId, message }) + } finally { + // Only a current-generation create may release: dispose() already + // cleared this create's reservation, and releasing here would + // delete a *new* panel's reservation for the same number. + if (generation === this.generation) this.releaseOrdinal(worktreeId, ordinal) } } @@ -156,15 +170,40 @@ export class TerminalRouter { return this.deps.getWorktreePath(worktreeId) } - /** Per-context counter so default titles are "Terminal 1", "Terminal 2"… - * Not persisted; a webview reload resets counts. */ - private nextOrdinal(worktreeId: string | null): number { + /** + * Pick the lowest "Terminal N" ordinal not used by a live terminal or + * an in-flight create in this context, and reserve it until the + * create settles. Gap-filling keeps numbering consistent: closing + * "Terminal 1" of three frees 1 for the next terminal, instead of + * drifting to ever-higher numbers. Not persisted; a webview reload + * resets the live set. + */ + private reserveOrdinal(worktreeId: string | null): number { const key = worktreeId ?? "__local__" - const next = (this.ordinals.get(key) ?? 0) + 1 - this.ordinals.set(key, next) + const used = new Set() + for (const title of this.manager.titles(worktreeId)) { + const match = /^Terminal (\d+)$/.exec(title) + if (match) used.add(Number(match[1])) + } + const pending = this.reserved.get(key) + if (pending) for (const n of pending) used.add(n) + let next = 1 + while (used.has(next)) next++ + const set = pending ?? new Set() + set.add(next) + this.reserved.set(key, set) return next } + /** Return an in-flight create's reservation. */ + private releaseOrdinal(worktreeId: string | null, ordinal: number) { + const key = worktreeId ?? "__local__" + const set = this.reserved.get(key) + if (!set) return + set.delete(ordinal) + if (set.size === 0) this.reserved.delete(key) + } + /** * Build the WebSocket URL for a given PTY. * @@ -182,6 +221,9 @@ export class TerminalRouter { const token = Buffer.from(`kilo:${config.password}`).toString("base64") const dir = encodeURIComponent(cwd) const auth = encodeURIComponent(token) - return `${base}/pty/${encodeURIComponent(ptyID)}/connect?directory=${dir}&cursor=-1&auth_token=${auth}` + // A new terminal has one initial attachment. Replay its retained startup + // bytes so xterm can answer shell capability queries emitted before the + // WebSocket connected; tailing from -1 can make shells wait for a timeout. + return `${base}/pty/${encodeURIComponent(ptyID)}/connect?directory=${dir}&cursor=0&auth_token=${auth}` } } diff --git a/packages/kilo-vscode/src/agent-manager/types.ts b/packages/kilo-vscode/src/agent-manager/types.ts index 8950c68d22..d386e36acc 100644 --- a/packages/kilo-vscode/src/agent-manager/types.ts +++ b/packages/kilo-vscode/src/agent-manager/types.ts @@ -414,6 +414,11 @@ interface ShowLocalTerminalIn { type: "agentManager.showLocalTerminal" } +interface ShowWorktreeTerminalIn { + type: "agentManager.showWorktreeTerminal" + worktreeId: string +} + interface OpenWorktreeIn { type: "agentManager.openWorktree" worktreeId: string @@ -780,6 +785,7 @@ export type AgentManagerInMessage = | StopRunScriptIn | ShowTerminalIn | ShowLocalTerminalIn + | ShowWorktreeTerminalIn | OpenWorktreeIn | CopyToClipboardIn | ShowExistingLocalTerminalIn diff --git a/packages/kilo-vscode/src/agent-manager/worktree-importer.ts b/packages/kilo-vscode/src/agent-manager/worktree-importer.ts index d840f67eb5..f589a7c4eb 100644 --- a/packages/kilo-vscode/src/agent-manager/worktree-importer.ts +++ b/packages/kilo-vscode/src/agent-manager/worktree-importer.ts @@ -56,63 +56,14 @@ export class WorktreeImporter { } async branch(branch: string): Promise { - const manager = this.host.manager() - const state = this.host.state() - if (!manager || !state) { - this.host.post({ type: "agentManager.importResult", success: false, message: "Not a git repository" }) - return - } - if (this.busy()) return - - this.importing = true - try { - this.host.post({ - type: "agentManager.worktreeSetup", - status: "creating", - message: "Creating worktree from branch...", - }) - const result = await manager.createWorktree({ existingBranch: branch }) - const worktree = state.addWorktree({ - branch: result.branch, - path: result.path, - parentBranch: result.parentBranch, - remote: result.remote, - branchOwned: false, - }) - this.host.push() - - try { - this.host.post({ - type: "agentManager.worktreeSetup", - status: "creating", - message: "Running setup script...", - branch: result.branch, - worktreeId: worktree.id, - }) - await this.host.setup(result.path, result.branch, worktree.id) - - const session = await this.host.session(result.path, result.branch, worktree.id) - if (!session) throw new Error("Failed to create session") - - state.addSession(session.id, worktree.id) - this.host.register(session.id, result.path) - this.host.ready(session.id, result, worktree.id) - this.host.post({ type: "agentManager.importResult", success: true, message: `Opened branch ${branch}` }) - this.host.log(`Imported branch ${branch} as worktree ${worktree.id}`) - } catch (error) { - state.removeWorktree(worktree.id) - await manager.removeWorktree(result.path) - this.host.push() - throw error - } - } catch (error) { - this.importError(error, `Branch "${branch}" is already checked out in another worktree`) - } finally { - this.importing = false - } + await this.run({ branch }) } async pr(url: string): Promise { + await this.run({ url }) + } + + private async run(target: { branch: string } | { url: string }): Promise { const manager = this.host.manager() const state = this.host.state() if (!manager || !state) { @@ -120,11 +71,21 @@ export class WorktreeImporter { return } if (this.busy()) return - this.importing = true + const branch = "branch" in target + const creating = branch ? "Creating worktree from branch..." : "Resolving PR..." + const setup = branch ? "Running setup script..." : "Setting up worktree..." + const duplicate = branch + ? `Branch "${target.branch}" is already checked out in another worktree` + : "This PR's branch is already checked out in another worktree" try { - this.host.post({ type: "agentManager.worktreeSetup", status: "creating", message: "Resolving PR..." }) - const result = await manager.createFromPR(url) + const progress = { type: "agentManager.worktreeSetup", status: "creating" } as const + this.host.post({ ...progress, message: creating }) + const result = branch + ? await manager.createWorktree({ existingBranch: target.branch }) + : await manager.createFromPR(target.url) + const success = branch ? `Opened branch ${target.branch}` : `Opened PR branch ${result.branch}` + const log = branch ? `Imported branch ${target.branch}` : `Imported PR ${target.url}` const worktree = state.addWorktree({ branch: result.branch, path: result.path, @@ -133,29 +94,16 @@ export class WorktreeImporter { branchOwned: false, }) this.host.push() - try { - this.host.post({ - type: "agentManager.worktreeSetup", - status: "creating", - message: "Setting up worktree...", - branch: result.branch, - worktreeId: worktree.id, - }) + this.host.post({ ...progress, message: setup, branch: result.branch, worktreeId: worktree.id }) await this.host.setup(result.path, result.branch, worktree.id) - const session = await this.host.session(result.path, result.branch, worktree.id) if (!session) throw new Error("Failed to create session") - state.addSession(session.id, worktree.id) this.host.register(session.id, result.path) this.host.ready(session.id, result, worktree.id) - this.host.post({ - type: "agentManager.importResult", - success: true, - message: `Opened PR branch ${result.branch}`, - }) - this.host.log(`Imported PR ${url} as worktree ${worktree.id}`) + this.host.post({ type: "agentManager.importResult", success: true, message: success }) + this.host.log(`${log} as worktree ${worktree.id}`) } catch (error) { state.removeWorktree(worktree.id) await manager.removeWorktree(result.path) @@ -163,7 +111,7 @@ export class WorktreeImporter { throw error } } catch (error) { - this.importError(error, "This PR's branch is already checked out in another worktree") + this.importError(error, duplicate) } finally { this.importing = false } diff --git a/packages/kilo-vscode/src/kilo-provider-utils.ts b/packages/kilo-vscode/src/kilo-provider-utils.ts index e5dbd40e44..c9825cb771 100644 --- a/packages/kilo-vscode/src/kilo-provider-utils.ts +++ b/packages/kilo-vscode/src/kilo-provider-utils.ts @@ -408,7 +408,7 @@ export type WebviewMessage = message: Record } | { type: "sessionStatus"; sessionID: string; status: string; attempt?: number; message?: string; next?: number } - | { type: "sessionTurnClosed"; sessionID: string; reason: "completed" | "error" | "interrupted" } + | { type: "sessionTurnClosed"; sessionID: string; reason: "completed" | "error" | "interrupted" | "superseded" } | { type: "permissionRequest" permission: { diff --git a/packages/kilo-vscode/src/kilo-provider/config-snapshot.ts b/packages/kilo-vscode/src/kilo-provider/config-snapshot.ts new file mode 100644 index 0000000000..209d5c4e42 --- /dev/null +++ b/packages/kilo-vscode/src/kilo-provider/config-snapshot.ts @@ -0,0 +1,20 @@ +import type { KiloClient } from "@kilocode/sdk/v2/client" +import { configFeatures } from "../features" +import { retry } from "../services/cli-backend/retry" + +type Client = Pick +type Settings = { maxCost: number; languageCommitMessage: string } +export async function fetchSnapshot(client: Client, dir: string, settings: () => Settings) { + const [{ data: config }, { data: global }, { data: overlay }] = await Promise.all([ + retry(() => client.config.get({ directory: dir }, { throwOnError: true })), + client.global.config.get({ throwOnError: true }), + client.config.overlay({ directory: dir, scope: "project" }, { throwOnError: true }), + ]) + return { + config, + globalConfig: global, + projectConfig: overlay?.project, + settings: settings(), + features: configFeatures(config), + } +} diff --git a/packages/kilo-vscode/src/services/cli-backend/i18n/fa.ts b/packages/kilo-vscode/src/services/cli-backend/i18n/fa.ts new file mode 100644 index 0000000000..3aa4dc7483 --- /dev/null +++ b/packages/kilo-vscode/src/services/cli-backend/i18n/fa.ts @@ -0,0 +1,6 @@ +export const dict = { + "server.processExited": "فرآیند CLI با کد {{code}} قبل از راه‌اندازی سرور خاتمه یافت", + "server.startupTimeout": "زمان راه‌اندازی سرور پس از {{seconds}} ثانیه به پایان رسید", + "remote.connected": "Kilo Remote: متصل شد", + "remote.connecting": "Kilo Remote: در حال اتصال…", +} as const diff --git a/packages/kilo-vscode/src/services/cli-backend/i18n/index.ts b/packages/kilo-vscode/src/services/cli-backend/i18n/index.ts index 82c8c9358a..d41b11bd2c 100644 --- a/packages/kilo-vscode/src/services/cli-backend/i18n/index.ts +++ b/packages/kilo-vscode/src/services/cli-backend/i18n/index.ts @@ -6,6 +6,7 @@ import { dict as da } from "./da" import { dict as de } from "./de" import { dict as en } from "./en" import { dict as es } from "./es" +import { dict as fa } from "./fa" import { dict as fr } from "./fr" import { dict as it } from "./it" import { dict as ja } from "./ja" @@ -29,6 +30,7 @@ const bundles: Record> = { de, en, es, + fa, fr, it, ja, diff --git a/packages/kilo-vscode/src/services/i18n/autocomplete/fa.ts b/packages/kilo-vscode/src/services/i18n/autocomplete/fa.ts new file mode 100644 index 0000000000..593c89eb68 --- /dev/null +++ b/packages/kilo-vscode/src/services/i18n/autocomplete/fa.ts @@ -0,0 +1,27 @@ +// English runtime translations for autocomplete (kilocode:autocomplete.* namespace) +// Source: src/i18n/locales/en/kilocode.json → "autocomplete" section + +export const dict = { + "kilocode:autocomplete.statusBar.enabled": "$(kilo-logo) تکمیل خودکار", + "kilocode:autocomplete.statusBar.snoozed": "به تعویق افتاده", + "kilocode:autocomplete.statusBar.warning": "$(warning) تکمیل خودکار", + "kilocode:autocomplete.statusBar.tooltip.basic": "تکمیل خودکار Kilo Code", + "kilocode:autocomplete.statusBar.tooltip.noUsableProvider": + "**هیچ مدل تکمیل خودکاری پیکربندی نشده است**\n\nبرای فعال‌سازی تکمیل خودکار، یک پروفایل با یکی از ارائه‌دهندگان پشتیبانی‌شده زیر اضافه کنید: {{providers}}.\n\n[باز کردن تنظیمات]({{command}})", + "kilocode:autocomplete.statusBar.tooltip.completionSummary": + "{{count}} تکمیل بین {{startTime}} و {{endTime}} انجام شد، با هزینه کل {{cost}}.", + "kilocode:autocomplete.statusBar.tooltip.providerInfo": + "تکمیل خودکار توسط {{model}} از طریق {{provider}} ارائه می‌شود.", + "kilocode:autocomplete.statusBar.cost.zero": "۰.۰۰$", + "kilocode:autocomplete.statusBar.cost.lessThanCent": "<۰.۰۱$", + "kilocode:autocomplete.codeAction.title": "Kilo Code: ویرایش‌های پیشنهادی", + "kilocode:autocomplete.incompatibilityExtensionPopup.message": + "تکمیل خودکار Kilo Code به دلیل تعارض با GitHub Copilot مسدود شده است. برای رفع این مشکل، باید پیشنهادات درون‌خطی Copilot را غیرفعال کنید.", + "kilocode:autocomplete.incompatibilityExtensionPopup.disableCopilot": "غیرفعال کردن Copilot", + "kilocode:autocomplete.incompatibilityExtensionPopup.disableInlineAssist": "غیرفعال کردن تکمیل خودکار", + "kilocode:autocomplete.creditsExhausted.message": + "تکمیل خودکار Kilo Code متوقف شده است. دلایل احتمالی: حساب Kilo شما اعتبار کافی ندارد، یا کلید API پیکربندی‌شده (BYOK) به سقف مجاز خود رسیده است. برای از سرگیری تکمیل خودکار، اعتبار Kilo اضافه کنید یا تنظیمات کلید API خود را بررسی کنید.", + "kilocode:autocomplete.creditsExhausted.addCredits": "افزودن اعتبار", + "kilocode:autocomplete.authError.message": + "تکمیل خودکار Kilo Code به دلیل مشکل احراز هویت متوقف شده است. دلایل احتمالی: وارد Kilo نشده‌اید، یا کلید API (BYOK) شما نامعتبر یا وارد نشده است. لطفاً دوباره وارد شوید یا تنظیمات کلید API ارائه‌دهنده خود را بررسی کنید.", +} diff --git a/packages/kilo-vscode/src/services/i18n/fa.ts b/packages/kilo-vscode/src/services/i18n/fa.ts new file mode 100644 index 0000000000..e8daa62e10 --- /dev/null +++ b/packages/kilo-vscode/src/services/i18n/fa.ts @@ -0,0 +1,7 @@ +import { dict as autocompleteDict } from "./autocomplete/fa" + +export { autocompleteDict } + +export const dict = { + ...autocompleteDict, +} as const diff --git a/packages/kilo-vscode/src/services/i18n/index.ts b/packages/kilo-vscode/src/services/i18n/index.ts index 4da8dda292..1090b84b39 100644 --- a/packages/kilo-vscode/src/services/i18n/index.ts +++ b/packages/kilo-vscode/src/services/i18n/index.ts @@ -6,6 +6,7 @@ import { dict as de } from "./de" import { dict as en } from "./en" import { type dict as enDict } from "./en" import { dict as es } from "./es" +import { dict as fa } from "./fa" import { dict as fr } from "./fr" import { dict as it } from "./it" import { dict as ja } from "./ja" @@ -28,6 +29,7 @@ const bundles: Record> = { de, en, es, + fa, fr, it, ja, diff --git a/packages/kilo-vscode/tests/package.json b/packages/kilo-vscode/tests/package.json index 38105c8a8f..52e607fbd4 100644 --- a/packages/kilo-vscode/tests/package.json +++ b/packages/kilo-vscode/tests/package.json @@ -1,6 +1,6 @@ { "type": "module", - "version": "7.4.16", + "version": "7.4.17", "dependencies": {}, "devDependencies": {}, "peerDependencies": {} diff --git a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts index 804471f9bb..ef69f9f317 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts @@ -11,6 +11,7 @@ import { describe, it, expect } from "bun:test" import fs from "node:fs" import path from "node:path" import { Project, SyntaxKind } from "ts-morph" +import { WorktreeImporter } from "../../src/agent-manager/worktree-importer" const ROOT = path.resolve(import.meta.dir, "../..") const KILO_PROVIDER_FILE = path.join(ROOT, "src/KiloProvider.ts") @@ -411,6 +412,7 @@ describe("Agent Manager Provider — onMessage routing", () => { "agentManager.stopRunScript", "agentManager.showTerminal", "agentManager.showLocalTerminal", + "agentManager.showWorktreeTerminal", "agentManager.showExistingLocalTerminal", "agentManager.requestRepoInfo", "agentManager.requestState", @@ -577,11 +579,61 @@ describe("Agent Manager Provider — onMessage routing", () => { it("worktree import behavior lives in the cohesive importer", () => { const text = importer() - const providerText = body("onImportMessage") - expect(text).toContain("class WorktreeImporter") - expect(text).toContain("createFromPR") - expect(text).toContain("createWorktree") - expect(providerText).toContain("this.importer") + for (const value of ["createFromPR", "createWorktree", "this.busy()"]) expect(text).toContain(value) + expect(body("onImportMessage")).toContain("this.importer") + }) + + it("preserves branch and PR import ordering and rollback", async () => { + const run = async (kind: "branch" | "pr", fail?: "setup" | "duplicate") => { + const events: string[] = [] + const create = async () => { + events.push("create") + if (fail === "duplicate") throw new Error("already checked out") + return { branch: "topic", path: "/repo/topic", parentBranch: "main" } + } + const importer = new WorktreeImporter({ + manager: () => + ({ createWorktree: create, createFromPR: create, removeWorktree: async () => events.push("disk") }) as never, + state: () => + ({ + addWorktree: (input: { branchOwned: boolean }) => ({ + id: events.push(`add:${input.branchOwned}`) ? "worktree" : "", + }), + addSession: () => events.push("state-session"), + removeWorktree: () => events.push("state-remove"), + }) as never, + post: (msg) => events.push("message" in msg ? String(msg.message) : msg.type), + push: () => events.push("push"), + setup: async () => { + events.push("setup") + if (fail === "setup") throw new Error("setup failed") + }, + session: async () => (events.push("session"), { id: "session" }) as never, + register: () => events.push("register"), + ready: () => events.push("ready"), + log: () => events.push("log"), + }) + const action = () => (kind === "branch" ? importer.branch("topic") : importer.pr("https://example.test/pull/1")) + await action() + if (fail === "setup") await action() + return events.join("|") + } + for (const kind of ["branch", "pr"] as const) { + const branch = kind === "branch" + const creating = branch ? "Creating worktree from branch..." : "Resolving PR..." + const setup = branch ? "Running setup script..." : "Setting up worktree..." + const success = branch ? "Opened branch topic" : "Opened PR branch topic" + expect(await run(kind)).toBe( + `${creating}|create|add:false|push|${setup}|setup|session|state-session|register|ready|${success}|log`, + ) + expect(await run(kind, "setup")).toBe( + `${creating}|create|add:false|push|${setup}|setup|state-remove|disk|push|setup failed|setup failed|${creating}|create|add:false|push|${setup}|setup|state-remove|disk|push|setup failed|setup failed`, + ) + const duplicate = branch + ? 'Branch "topic" is already checked out in another worktree' + : "This PR's branch is already checked out in another worktree" + expect(await run(kind, "duplicate")).toBe(`${creating}|create|${duplicate}|${duplicate}`) + } }) }) @@ -891,70 +943,73 @@ describe("Agent Manager — VS Code import boundary", () => { }) }) -// --------------------------------------------------------------------------- -// Provider chain parity — sidebar App.tsx vs AgentManagerApp.tsx -// -// The agent manager reuses ChatView (and therefore MessageList, etc.) from the -// sidebar. Any context provider that ChatView's tree may call useXxx() on must -// also be present in the agent manager's provider chain. A missing provider -// crashes the entire SolidJS component tree silently. -// -// Regression: PR #7473 moved KiloNotifications into MessageList. It calls -// useNotifications(), but NotificationsProvider was only in App.tsx — the agent -// manager rendered a blank screen. -// --------------------------------------------------------------------------- - const APP_FILE = path.join(ROOT, "webview-ui/src/App.tsx") const AGENT_MANAGER_APP_FILE = path.join(ROOT, "webview-ui/agent-manager/AgentManagerApp.tsx") +const PROVIDER_SHELL_FILE = path.join(ROOT, "webview-ui/src/context/provider-shell.tsx") -describe("Agent Manager — provider chain parity with sidebar", () => { - /** - * Extract provider component names used as JSX elements in a file. - * Matches `` patterns, returning the names. - */ - function extractProviders(content: string): string[] { - const matches = [...content.matchAll(/<(\w+Provider)\b/g)] - return [...new Set(matches.map((m) => m[1]!))] +describe("Shared webview provider shell", () => { + function ordered(source: string, names: string[]) { + const positions = names.map((name) => source.indexOf(`<${name}`)) + expect( + positions.every((position) => position >= 0), + `Missing provider from ${names.join(" -> ")}`, + ).toBe(true) + expect(positions).toEqual([...positions].sort((a, b) => a - b)) } - /** - * Providers that the agent manager intentionally omits because it does not - * use the components that depend on them. If a shared component (ChatView, - * MessageList, etc.) starts using one of these, the test will fail and - * force the developer to add the provider to AgentManagerApp.tsx. - */ - const KNOWN_EXCLUSIONS: string[] = [ - // These are wrapped by LanguageBridge and DataBridge respectively, - // which the agent manager already includes in its provider chain. - "LanguageProvider", - "DataProvider", - // Agent Manager owns its local session tabs and ChatView only reads this - // optional context in the standard sidebar/editor webview. - "LocalTabsProvider", - // Work-style onboarding is injected only into the sidebar empty state. - "WorkStyleProvider", - ] + it("owns the common provider order and bridges", () => { + const source = fs.readFileSync(PROVIDER_SHELL_FILE, "utf-8") + ordered(source, [ + "ThemeProvider", + "DialogProvider", + "VSCodeProvider", + "MermaidDownloadBridge", + "ServerProvider", + "LanguageBridge", + "MarkedProvider", + "DiffComponentProvider", + "CodeComponentProvider", + "FileComponentProvider", + "ProviderProvider", + "ConfigProvider", + "SpeechToTextPrewarm", + "DisplayProvider", + "IndexingProvider", + "KiloEmbeddingModelsProvider", + "ImageModelsProvider", + "NotificationsProvider", + "SessionProvider", + "AgentRequirementsProvider", + "MemoryProvider", + "FeedbackProvider", + ]) + expect(source.indexOf("")) + }) - it("agent manager includes all context providers from sidebar App.tsx", () => { - const sidebar = fs.readFileSync(APP_FILE, "utf-8") - const agent = fs.readFileSync(AGENT_MANAGER_APP_FILE, "utf-8") + it("keeps sidebar-only providers in the sidebar root", () => { + const source = fs.readFileSync(APP_FILE, "utf-8") + ordered(source, [ + "ProviderShell.Root", + "WorkStyleProvider", + "ProviderShell.Session", + "LocalTabsProvider", + "ProviderShell.Chat", + "DataBridge", + "AppContent", + ]) + expect(fs.readFileSync(PROVIDER_SHELL_FILE, "utf-8")).not.toMatch(/WorkStyleProvider|LocalTabsProvider/) + }) - const sidebarProviders = extractProviders(sidebar) - const agentProviders = extractProviders(agent) - const agentSet = new Set(agentProviders) - const excluded = new Set(KNOWN_EXCLUSIONS) - - const missing = sidebarProviders.filter((p) => !agentSet.has(p) && !excluded.has(p)) - - expect( - missing, - `These providers are in App.tsx but missing from AgentManagerApp.tsx.\n` + - `The agent manager reuses ChatView — any provider that ChatView's component\n` + - `tree depends on must be present in both provider chains.\n\n` + - `Missing providers:\n` + - missing.map((p) => ` - ${p}`).join("\n") + - `\n\nFix: add the missing <${missing[0]}> to AgentManagerApp.tsx's provider chain,\n` + - `or add it to KNOWN_EXCLUSIONS with a justification if it's truly unused.`, - ).toEqual([]) + it("keeps worktree mode in the Agent Manager root", () => { + const source = fs.readFileSync(AGENT_MANAGER_APP_FILE, "utf-8") + ordered(source, [ + "ProviderShell.Root", + "ProviderShell.Session", + "ProviderShell.Chat", + "WorktreeModeProvider", + "DataBridge", + "AgentManagerContent", + ]) + expect(fs.readFileSync(PROVIDER_SHELL_FILE, "utf-8")).not.toContain("WorktreeModeProvider") }) }) diff --git a/packages/kilo-vscode/tests/unit/agent-manager-i18n-split.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-i18n-split.test.ts index ab0965fa7e..3b7477aafc 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-i18n-split.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-i18n-split.test.ts @@ -39,6 +39,7 @@ import { dict as amTr } from "../../webview-ui/agent-manager/i18n/tr" import { dict as amNl } from "../../webview-ui/agent-manager/i18n/nl" import { dict as amUk } from "../../webview-ui/agent-manager/i18n/uk" import { dict as amIt } from "../../webview-ui/agent-manager/i18n/it" +import { dict as amFa } from "../../webview-ui/agent-manager/i18n/fa" const PREFIX = "agentManager." @@ -63,6 +64,7 @@ const locales = { nl: amNl, uk: amUk, it: amIt, + fa: amFa, } const appLocales = { diff --git a/packages/kilo-vscode/tests/unit/agent-manager-terminal-font.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-terminal-font.test.ts index 64504d2600..02b7fffe27 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-terminal-font.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-terminal-font.test.ts @@ -53,6 +53,7 @@ describe("Agent Manager terminal font", () => { const message = new Promise((resolve) => { const router = new TerminalRouter({ getClient: () => client, + getClientAsync: async () => client, getServerConfig: () => ({ baseUrl: "http://127.0.0.1:4096", password: "secret" }), getRoot: () => "/workspace", getWorktreePath: () => undefined, @@ -77,6 +78,7 @@ describe("Agent Manager terminal font", () => { expect(created.font).toEqual(font) expect(created.worktreeId).toBeNull() expect(created.wsUrl).toContain("/pty/pty-1/connect") + expect(created.wsUrl).toContain("cursor=0") }) it("keeps the created font in terminal state", () => { diff --git a/packages/kilo-vscode/tests/unit/agent-manager-terminal-layout.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-terminal-layout.test.ts new file mode 100644 index 0000000000..f30ba800e5 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/agent-manager-terminal-layout.test.ts @@ -0,0 +1,15 @@ +import { expect, test } from "bun:test" +import { readFileSync } from "node:fs" +import { resolve } from "node:path" + +const css = readFileSync(resolve(import.meta.dir, "../../webview-ui/agent-manager/agent-manager.css"), "utf8") + +test("xterm owns the padding used by FitAddon", () => { + const host = css.match(/\.am-terminal-host\s*\{([^}]*)\}/)?.[1] + const term = css.match(/\.am-terminal-host \[class~="xterm"\]\s*\{([^}]*)\}/)?.[1] + + expect(host).toBeDefined() + expect(term).toBeDefined() + expect(host).not.toMatch(/\bpadding\s*:/) + expect(term).toMatch(/\bpadding\s*:\s*8px\s*;/) +}) diff --git a/packages/kilo-vscode/tests/unit/agent-manager-terminal-routing.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-terminal-routing.test.ts index bfa243d1fc..ef9bfcd6c4 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-terminal-routing.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-terminal-routing.test.ts @@ -21,6 +21,7 @@ describe("Agent Manager terminal routing", () => { } as unknown as KiloClient const router = new TerminalRouter({ getClient: () => client, + getClientAsync: async () => client, getServerConfig: () => ({ baseUrl: "http://127.0.0.1:4096", password: "secret" }), getRoot: () => "/workspace", getWorktreePath: (id) => (id === "wt-1" ? "/workspace/wt-1" : undefined), @@ -78,6 +79,7 @@ describe("Agent Manager terminal routing", () => { } as unknown as KiloClient const router = new TerminalRouter({ getClient: () => client, + getClientAsync: async () => client, getServerConfig: () => ({ baseUrl: "http://127.0.0.1:4096", password: "secret" }), getRoot: () => "/workspace", getWorktreePath: () => undefined, @@ -108,4 +110,174 @@ describe("Agent Manager terminal routing", () => { await router.dispose() expect(removed).toContain("pty-new") }) + + it("fills numbering gaps left by closed terminals", async () => { + const messages: AgentManagerOutMessage[] = [] + const titles: string[] = [] + let seq = 0 + const client = { + pty: { + create: async ({ title }: { title: string }) => { + titles.push(title) + seq++ + return { data: { id: `pty-${seq}`, title } } + }, + remove: async () => ({ data: true }), + update: async () => ({ data: true }), + }, + } as unknown as KiloClient + const router = new TerminalRouter({ + getClient: () => client, + getClientAsync: async () => client, + getServerConfig: () => ({ baseUrl: "http://127.0.0.1:4096", password: "secret" }), + getRoot: () => "/workspace", + getWorktreePath: () => undefined, + log: () => undefined, + post: (message) => messages.push(message), + getTerminalFont: () => font, + }) + const create = (createId: string) => + router.handle({ type: "agentManager.terminal.create", createId, placement: "side", worktreeId: null }) + + create("one") + await wait() + create("two") + await wait() + expect(titles).toEqual(["Terminal 1", "Terminal 2"]) + + // Close "Terminal 1"; the next create reuses the freed number. + const first = messages.find((m) => m.type === "agentManager.terminal.created" && m.createId === "one") + if (first?.type !== "agentManager.terminal.created") throw new Error("missing created message") + router.handle({ type: "agentManager.terminal.close", terminalId: first.terminalId }) + await wait() + + create("three") + await wait() + expect(titles).toEqual(["Terminal 1", "Terminal 2", "Terminal 1"]) + await router.dispose() + }) + + it("hands out distinct numbers to concurrent creates", async () => { + const messages: AgentManagerOutMessage[] = [] + const titles: string[] = [] + const resolvers: Array<(value: { data: { id: string; title: string } }) => void> = [] + const client = { + pty: { + create: ({ title }: { title: string }) => + new Promise<{ data: { id: string; title: string } }>((resolve) => { + titles.push(title) + resolvers.push(resolve) + }), + remove: async () => ({ data: true }), + update: async () => ({ data: true }), + }, + } as unknown as KiloClient + const router = new TerminalRouter({ + getClient: () => client, + getClientAsync: async () => client, + getServerConfig: () => ({ baseUrl: "http://127.0.0.1:4096", password: "secret" }), + getRoot: () => "/workspace", + getWorktreePath: () => undefined, + log: () => undefined, + post: (message) => messages.push(message), + getTerminalFont: () => font, + }) + + // Two creates before either settles must not share an ordinal. The + // backend-connection await defers the PTY creates to a microtask. + router.handle({ type: "agentManager.terminal.create", createId: "a", placement: "side", worktreeId: null }) + router.handle({ type: "agentManager.terminal.create", createId: "b", placement: "side", worktreeId: null }) + await wait() + expect(titles).toEqual(["Terminal 1", "Terminal 2"]) + resolvers[0]?.({ data: { id: "pty-a", title: titles[0]! } }) + resolvers[1]?.({ data: { id: "pty-b", title: titles[1]! } }) + await wait() + + // A failed create releases its reservation for the next attempt. + await router.dispose() + }) + + it("does not let a stale create release a new generation's reservation", async () => { + const titles: string[] = [] + const resolvers: Array<(value: { data: { id: string; title: string } }) => void> = [] + const client = { + pty: { + create: ({ title }: { title: string }) => + new Promise<{ data: { id: string; title: string } }>((resolve) => { + titles.push(title) + resolvers.push(resolve) + }), + remove: async () => ({ data: true }), + update: async () => ({ data: true }), + }, + } as unknown as KiloClient + const router = new TerminalRouter({ + getClient: () => client, + getClientAsync: async () => client, + getServerConfig: () => ({ baseUrl: "http://127.0.0.1:4096", password: "secret" }), + getRoot: () => "/workspace", + getWorktreePath: () => undefined, + log: () => undefined, + post: () => undefined, + getTerminalFont: () => font, + }) + + // Create A starts before the panel is recreated; its reservation dies + // with dispose(). Create B of the new generation reserves the same + // free number. When A's late completion settles, its release must not + // wipe B's reservation — otherwise create C would duplicate B's title. + router.handle({ type: "agentManager.terminal.create", createId: "a", placement: "side", worktreeId: null }) + await router.dispose() + router.handle({ type: "agentManager.terminal.create", createId: "b", placement: "side", worktreeId: null }) + await wait() + expect(titles).toEqual(["Terminal 1", "Terminal 1"]) + resolvers[0]?.({ data: { id: "pty-a", title: titles[0]! } }) + await wait() + router.handle({ type: "agentManager.terminal.create", createId: "c", placement: "side", worktreeId: null }) + await wait() + expect(titles).toEqual(["Terminal 1", "Terminal 1", "Terminal 2"]) + await router.dispose() + }) + + it("awaits the shared backend connection before creating a terminal", async () => { + let connected = false + const client = { + pty: { + create: async () => ({ data: { id: "pty-1", title: "Terminal 1" } }), + remove: async () => ({ data: true }), + update: async () => ({ data: true }), + }, + } as unknown as KiloClient + const messages: AgentManagerOutMessage[] = [] + const router = new TerminalRouter({ + getClient: () => { + if (!connected) throw new Error("Not connected") + return client + }, + getClientAsync: async () => { + await wait() + connected = true + return client + }, + getServerConfig: () => ({ baseUrl: "http://127.0.0.1:4096", password: "secret" }), + getRoot: () => "/workspace", + getWorktreePath: () => undefined, + log: () => undefined, + post: (message) => messages.push(message), + getTerminalFont: () => font, + }) + + router.handle({ + type: "agentManager.terminal.create", + createId: "real", + placement: "tab", + worktreeId: null, + }) + expect(messages).toHaveLength(0) + await wait() + await wait() + + expect(messages[0]).toMatchObject({ type: "agentManager.terminal.created", createId: "real" }) + await router.dispose() + }) }) diff --git a/packages/kilo-vscode/tests/unit/agent-manager-terminal-side.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-terminal-side.test.ts index 199d3adca2..9a071e5c15 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-terminal-side.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-terminal-side.test.ts @@ -1,32 +1,44 @@ import { describe, expect, it } from "bun:test" -import { createSideTerminal } from "../../webview-ui/agent-manager/terminal/side" +import { + createSideTerminal, + readSavedDestination, + resolveVscodeTerminalRequest, +} from "../../webview-ui/agent-manager/terminal/side" -function scene(opts: { destination?: "vscode" | "agentManager"; visible?: boolean; focused?: boolean } = {}) { +function scene( + opts: { + destination?: "vscode" | "agentManager" + saved?: "vscode" | "agentManager" + visible?: boolean + focusedId?: string + } = {}, +) { const calls = { requestSide: 0, - closeSide: 0, + closed: [] as string[], hide: 0, refocus: 0, openVscode: 0, + persisted: [] as string[], posted: [] as Array>, tracked: [] as string[], } let visible = opts.visible ?? false - let focused = opts.focused ?? false + let focusedId = opts.focusedId as string | undefined const ctl = createSideTerminal({ handlers: { requestSide: () => { calls.requestSide++ visible = true }, - closeSide: () => { - calls.closeSide++ - visible = false + closeSide: (terminalId) => { + calls.closed.push(terminalId) + focusedId = undefined return true }, }, visible: () => visible, - focused: () => focused, + focusedId: () => focusedId, hide: () => { calls.hide++ visible = false @@ -35,19 +47,21 @@ function scene(opts: { destination?: "vscode" | "agentManager"; visible?: boolea postMessage: (msg) => calls.posted.push(msg as Record), track: (button) => calls.tracked.push(button), openVscode: () => calls.openVscode++, + saved: opts.saved, + save: (destination) => calls.persisted.push(destination), }) - if (opts.destination) ctl.setDestination(opts.destination) + if (opts.destination) ctl.syncDefault(opts.destination) return { ctl, calls } } describe("Agent Manager side terminal controller", () => { it("toggles the panel and hands focus to the chat only when the terminal had it", () => { - const focused = scene({ destination: "agentManager", visible: true, focused: true }) + const focused = scene({ destination: "agentManager", visible: true, focusedId: "terminal:side" }) focused.ctl.toggle() expect(focused.calls.hide).toBe(1) expect(focused.calls.refocus).toBe(1) - const elsewhere = scene({ destination: "agentManager", visible: true, focused: false }) + const elsewhere = scene({ destination: "agentManager", visible: true }) elsewhere.ctl.toggle() expect(elsewhere.calls.hide).toBe(1) expect(elsewhere.calls.refocus).toBe(0) @@ -58,15 +72,18 @@ describe("Agent Manager side terminal controller", () => { expect(hidden.calls.hide).toBe(0) }) - it("refocuses the chat after killing a focused terminal, not otherwise", () => { - const focused = scene({ focused: true }) + it("kills the focused terminal and refocuses the chat", () => { + const focused = scene({ focusedId: "terminal:two" }) expect(focused.ctl.close()).toBe(true) - expect(focused.calls.closeSide).toBe(1) + expect(focused.calls.closed).toEqual(["terminal:two"]) expect(focused.calls.refocus).toBe(1) + }) - const elsewhere = scene({ focused: false }) - expect(elsewhere.ctl.close()).toBe(true) - expect(elsewhere.calls.refocus).toBe(0) + it("does nothing on close without a focused terminal", () => { + const item = scene() + expect(item.ctl.close()).toBe(false) + expect(item.calls.closed).toEqual([]) + expect(item.calls.refocus).toBe(0) }) it("routes the primary action by destination", () => { @@ -88,5 +105,84 @@ describe("Agent Manager side terminal controller", () => { expect(item.calls.posted).toEqual([ { type: "updateSetting", key: "agentManager.terminalButtonDestination", value: "agentManager" }, ]) + expect(item.calls.persisted).toEqual(["agentManager"]) + }) + + it("follows the remote default while the panel has no explicit choice", () => { + const item = scene() + item.ctl.syncDefault("agentManager") + expect(item.ctl.destination()).toBe("agentManager") + item.ctl.syncDefault("vscode") + expect(item.ctl.destination()).toBe("vscode") + }) + + it("keeps the panel's explicit choice when another window rewrites the shared setting", () => { + const item = scene() + item.ctl.choose("agentManager") + // Echo of the application-scoped setting being rewritten elsewhere: + // worktree window B picked the VS Code terminal, which must not flip + // this panel's routing. + item.ctl.syncDefault("vscode") + expect(item.ctl.destination()).toBe("agentManager") + item.ctl.openPreferred("keyboard_shortcut") + expect(item.calls.requestSide).toBe(1) + expect(item.calls.openVscode).toBe(0) + }) + + it("restores a saved panel choice and ignores remote defaults", () => { + const item = scene({ saved: "agentManager" }) + expect(item.ctl.destination()).toBe("agentManager") + item.ctl.syncDefault("vscode") + expect(item.ctl.destination()).toBe("agentManager") + }) +}) + +describe("readSavedDestination", () => { + it("reads a valid choice and rejects anything else", () => { + expect(readSavedDestination({ terminalDestination: "agentManager" })).toBe("agentManager") + expect(readSavedDestination({ terminalDestination: "vscode" })).toBe("vscode") + expect(readSavedDestination({ terminalDestination: "bogus" })).toBeUndefined() + expect(readSavedDestination({})).toBeUndefined() + expect(readSavedDestination(undefined)).toBeUndefined() + }) +}) + +describe("resolveVscodeTerminalRequest", () => { + const sessions = new Map([ + ["wt-1", "session-a"], + ["wt-2", "session-b"], + ]) + const forWorktree = (id: string) => sessions.get(id) + + it("prefers the current session", () => { + expect(resolveVscodeTerminalRequest("wt-1", "session-current", forWorktree)).toEqual({ + type: "agentManager.showTerminal", + sessionId: "session-current", + }) + }) + + it("falls back to a session of the selected worktree when the current session is cleared", () => { + // Terminal tab activation clears the current session; the shortcut + // must still open a terminal for the worktree, not dead-end. + expect(resolveVscodeTerminalRequest("wt-2", undefined, forWorktree)).toEqual({ + type: "agentManager.showTerminal", + sessionId: "session-b", + }) + }) + + it("opens a worktree-rooted terminal for sessionless worktrees", () => { + expect(resolveVscodeTerminalRequest("wt-3", undefined, forWorktree)).toEqual({ + type: "agentManager.showWorktreeTerminal", + worktreeId: "wt-3", + }) + }) + + it("opens the local terminal for the local context and unassigned selections", () => { + expect(resolveVscodeTerminalRequest("local", undefined, forWorktree)).toEqual({ + type: "agentManager.showLocalTerminal", + }) + expect(resolveVscodeTerminalRequest(null, undefined, forWorktree)).toEqual({ + type: "agentManager.showLocalTerminal", + }) }) }) diff --git a/packages/kilo-vscode/tests/unit/agent-manager-terminal-state.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-terminal-state.test.ts index 432eed1fb6..db0acb769f 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-terminal-state.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-terminal-state.test.ts @@ -14,7 +14,7 @@ function scene(initial: string | null = LOCAL) { const [selection, setSelection] = createSignal(initial) const state = createTerminalState(selection) const posted: Array> = [] - const events = { activated: [] as string[], selected: [] as string[], saved: 0, shown: [] as string[], hidden: 0 } + const events = { activated: [] as string[], selected: [] as string[], saved: 0, shown: [] as string[], errors: 0 } const tabs = () => state.current().map((term) => term.id) const handlers = createTerminalHandlers({ state, @@ -27,7 +27,6 @@ function scene(initial: string | null = LOCAL) { findTab: () => undefined, postMessage: (message) => posted.push(message as Record), onShowSide: (key) => events.shown.push(key), - onHideSide: () => events.hidden++, getSelection: selection, LOCAL, REVIEW_TAB_ID: "review", @@ -40,12 +39,25 @@ function scene(initial: string | null = LOCAL) { events.selected.push(value) setSelection(value) }, - showError: () => undefined, + showError: () => events.errors++, postMessage: (message) => posted.push(message as Record), }) return { state, selection, setSelection, posted, events, handlers, dispatch } } +function createdSide(createId: string, terminalId: string, title = "Terminal 1") { + return { + type: "agentManager.terminal.created", + createId, + placement: "side", + worktreeId: null, + terminalId, + title, + wsUrl: `ws://${terminalId}`, + font, + } satisfies ExtensionMessage +} + describe("Agent Manager terminal state", () => { it("keeps side terminals out of the tab state and shares root context with unassigned sessions", () => { createRoot((dispose) => { @@ -64,21 +76,22 @@ describe("Agent Manager terminal state", () => { font, placement: "side", }) + item.state.setSideActive(LOCAL, "terminal:side") expect(item.state.current().map((term) => term.id)).toEqual(["terminal:tab"]) expect(item.state.all().map((term) => term.id)).toEqual(["terminal:tab"]) expect(item.state.sides().map((term) => term.id)).toEqual(["terminal:side"]) - expect(item.state.side()?.id).toBe("terminal:side") + expect(item.state.sideActiveFor(LOCAL)).toBe("terminal:side") item.setSelection(null) expect(item.state.current()).toEqual([]) expect(item.state.sideKey()).toBe(LOCAL) - expect(item.state.side()?.id).toBe("terminal:side") + expect(item.state.sideActiveFor(LOCAL)).toBe("terminal:side") dispose() }) }) - it("deduplicates side creation and reuses the terminal without tab side effects", () => { + it("deduplicates an in-flight reveal and focuses the active terminal on repeat", () => { createRoot((dispose) => { const item = scene() item.handlers.requestSide() @@ -88,18 +101,8 @@ describe("Agent Manager terminal state", () => { const request = item.posted[0]! expect(request).toMatchObject({ type: "agentManager.terminal.create", placement: "side", worktreeId: null }) const createId = String(request.createId) - const created = { - type: "agentManager.terminal.created", - createId, - placement: "side", - worktreeId: null, - terminalId: "terminal:side", - title: "Terminal 1", - wsUrl: "ws://side", - font, - } satisfies ExtensionMessage - expect(item.dispatch(created)).toBe(true) - expect(item.state.side()?.id).toBe("terminal:side") + expect(item.dispatch(createdSide(createId, "terminal:side"))).toBe(true) + expect(item.state.sideActiveFor(LOCAL)).toBe("terminal:side") expect(item.events.activated).toEqual([]) expect(item.events.selected).toEqual([]) expect(item.events.saved).toBe(0) @@ -111,6 +114,74 @@ describe("Agent Manager terminal state", () => { }) }) + it("supports several side terminals per context with newest active", () => { + createRoot((dispose) => { + const item = scene() + item.handlers.addSide() + item.handlers.addSide() + expect(item.posted).toHaveLength(2) + const first = String(item.posted[0]!.createId) + const second = String(item.posted[1]!.createId) + + item.dispatch(createdSide(first, "terminal:one", "Terminal 1")) + expect(item.state.sidesForContext(LOCAL).map((term) => term.id)).toEqual(["terminal:one"]) + expect(item.state.sideActiveFor(LOCAL)).toBe("terminal:one") + + item.dispatch(createdSide(second, "terminal:two", "Terminal 2")) + expect(item.state.sidesForContext(LOCAL).map((term) => term.id)).toEqual(["terminal:one", "terminal:two"]) + expect(item.state.sideActiveFor(LOCAL)).toBe("terminal:two") + dispose() + }) + }) + + it("switches the active side terminal on select", () => { + createRoot((dispose) => { + const item = scene() + item.state.add(null, { id: "terminal:one", title: "Terminal 1", wsUrl: "ws://one", font, placement: "side" }) + item.state.add(null, { id: "terminal:two", title: "Terminal 2", wsUrl: "ws://two", font, placement: "side" }) + item.state.setSideActive(LOCAL, "terminal:two") + + item.handlers.selectSide("terminal:one") + expect(item.state.sideActiveFor(LOCAL)).toBe("terminal:one") + expect(item.state.focusRequest()?.id).toBe("terminal:one") + dispose() + }) + }) + + it("moves activation to the last remaining side terminal on close", () => { + createRoot((dispose) => { + const item = scene() + item.state.add(null, { id: "terminal:one", title: "Terminal 1", wsUrl: "ws://one", font, placement: "side" }) + item.state.add(null, { id: "terminal:two", title: "Terminal 2", wsUrl: "ws://two", font, placement: "side" }) + item.state.setSideActive(LOCAL, "terminal:two") + + expect(item.handlers.closeSide("terminal:two")).toBe(true) + expect(item.state.sideActiveFor(LOCAL)).toBe("terminal:one") + expect(item.posted).toEqual([{ type: "agentManager.terminal.close", terminalId: "terminal:two" }]) + + expect(item.handlers.closeSide("terminal:one")).toBe(true) + expect(item.state.sideActiveFor(LOCAL)).toBeUndefined() + expect(item.state.sidesForContext(LOCAL)).toEqual([]) + + // Closing an unknown or non-side id is a no-op. + expect(item.handlers.closeSide("terminal:gone")).toBe(false) + expect(item.posted).toHaveLength(2) + dispose() + }) + }) + + it("closes a stale side answer whose create request is unknown", () => { + createRoot((dispose) => { + const item = scene() + // A created message for a createId the webview never sent (e.g. it + // reloaded while the PTY was starting) must not leak the PTY. + item.dispatch(createdSide("stale-id", "terminal:stale")) + expect(item.state.sidesForContext(LOCAL)).toEqual([]) + expect(item.posted).toEqual([{ type: "agentManager.terminal.close", terminalId: "terminal:stale" }]) + dispose() + }) + }) + it("creates explicit terminal tabs independently of the side destination", () => { createRoot((dispose) => { const item = scene("wt-1") @@ -124,44 +195,91 @@ describe("Agent Manager terminal state", () => { }) }) - it("cancels a side terminal that is still starting", () => { + it("routes side creates of a worktree context to that worktree", () => { createRoot((dispose) => { - const item = scene() - item.handlers.requestSide() - const request = item.posted[0]! - expect(item.handlers.closeSide()).toBe(true) - expect(item.state.pendingSide(LOCAL)).toBeUndefined() - - item.dispatch({ - type: "agentManager.terminal.created", - createId: String(request.createId), + const item = scene("wt-1") + item.handlers.addSide() + expect(item.posted[0]).toMatchObject({ + type: "agentManager.terminal.create", placement: "side", - worktreeId: null, - terminalId: "terminal:late", - title: "Terminal 1", - wsUrl: "ws://late", - font, + worktreeId: "wt-1", }) - expect(item.state.side()).toBeUndefined() - expect(item.posted.at(-1)).toEqual({ type: "agentManager.terminal.close", terminalId: "terminal:late" }) dispose() }) }) - it("closes a side terminal without changing the active chat tab", () => { + it("tracks OSC titles per terminal without touching the terminal records", () => { createRoot((dispose) => { const item = scene() - item.state.add(null, { - id: "terminal:side", - title: "Terminal 1", - wsUrl: "ws://side", - font, - placement: "side", - }) - expect(item.handlers.closeSide()).toBe(true) - expect(item.state.side()).toBeUndefined() - expect(item.posted).toEqual([{ type: "agentManager.terminal.close", terminalId: "terminal:side" }]) - expect(item.events.hidden).toBe(1) + item.state.add(null, { id: "terminal:one", title: "Terminal 1", wsUrl: "ws://one", font, placement: "side" }) + const before = item.state.sidesForContext(LOCAL)[0]! + + item.state.setTitle("terminal:one", "npm run dev") + expect(item.state.title("terminal:one")).toBe("npm run dev") + // Reference stability: the stored record is untouched so does + // not remount the xterm instance on a title change. + expect(item.state.sidesForContext(LOCAL)[0]).toBe(before) + + // Empty titles are ignored; removal drops the override. + item.state.setTitle("terminal:one", " ") + expect(item.state.title("terminal:one")).toBe("npm run dev") + item.state.remove("terminal:one") + expect(item.state.title("terminal:one")).toBeUndefined() + dispose() + }) + }) + + it("reorders side terminals within their context via drag", () => { + createRoot((dispose) => { + const item = scene() + item.state.add(null, { id: "terminal:one", title: "Terminal 1", wsUrl: "ws://one", font, placement: "side" }) + item.state.add(null, { id: "terminal:two", title: "Terminal 2", wsUrl: "ws://two", font, placement: "side" }) + item.state.add(null, { id: "terminal:three", title: "Terminal 3", wsUrl: "ws://three", font, placement: "side" }) + item.state.add(null, { id: "terminal:tab", title: "Terminal 4", wsUrl: "ws://tab", font, placement: "tab" }) + + // Drag the first side terminal onto the third position. + expect(item.state.reorderSideDrag(LOCAL, "terminal:one", "terminal:three")).toBe(true) + expect(item.state.sidesForContext(LOCAL).map((term) => term.id)).toEqual([ + "terminal:two", + "terminal:three", + "terminal:one", + ]) + // Tab terminals are untouched. + expect(item.state.current().map((term) => term.id)).toEqual(["terminal:tab"]) + + // The order survives switching to another context and back. + item.setSelection("wt-1") + expect(item.state.sidesForContext(LOCAL).map((term) => term.id)).toEqual([ + "terminal:two", + "terminal:three", + "terminal:one", + ]) + item.setSelection(LOCAL) + expect(item.state.sidesForContext(LOCAL).map((term) => term.id)).toEqual([ + "terminal:two", + "terminal:three", + "terminal:one", + ]) + + // Unknown ids, tab-placement ids, and foreign contexts are rejected. + expect(item.state.reorderSideDrag(LOCAL, "terminal:gone", "terminal:two")).toBe(false) + expect(item.state.reorderSideDrag(LOCAL, "terminal:tab", "terminal:two")).toBe(false) + expect(item.state.reorderSideDrag("wt-1", "terminal:two", "terminal:three")).toBe(false) + dispose() + }) + }) + + it("reports the focused side terminal only for the current context", () => { + createRoot((dispose) => { + const item = scene() + item.state.add(null, { id: "terminal:side", title: "Terminal 1", wsUrl: "ws://side", font, placement: "side" }) + item.state.add(null, { id: "terminal:tab", title: "Terminal 2", wsUrl: "ws://tab", font, placement: "tab" }) + + expect(item.state.sideFocusedId()).toBeUndefined() + item.state.setFocusedId("terminal:tab") + expect(item.state.sideFocusedId()).toBeUndefined() + item.state.setFocusedId("terminal:side") + expect(item.state.sideFocusedId()).toBe("terminal:side") dispose() }) }) diff --git a/packages/kilo-vscode/tests/unit/agent-manager-worktree-diffs.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-worktree-diffs.test.ts new file mode 100644 index 0000000000..5d2e5bc3e1 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/agent-manager-worktree-diffs.test.ts @@ -0,0 +1,103 @@ +import { describe, expect, it } from "bun:test" +import { createRoot } from "solid-js" +import { createWorktreeDiffs } from "../../webview-ui/agent-manager/worktree-diffs" +import type { WorktreeFileDiff } from "../../webview-ui/src/types/messages" + +const diff = (file: string, additions = 1): WorktreeFileDiff => ({ + file, + before: "", + after: "", + additions, + deletions: 0, +}) + +interface Sent { + type: string + sessionId?: string + file?: string +} + +// Only `postMessage` is exercised by the diff workflow, so a recording stub is +// enough — the signals and merge/pending logic under test are the real thing. +const vscode = (sent: Sent[]) => + ({ postMessage: (msg: Sent) => sent.push(msg) }) as unknown as Parameters[0] + +const withDiffs = (fn: (diffs: ReturnType, sent: Sent[]) => void) => { + createRoot((dispose) => { + const sent: Sent[] = [] + fn(createWorktreeDiffs(vscode(sent)), sent) + dispose() + }) +} + +describe("createWorktreeDiffs", () => { + it("stores full diffs per session", () => { + withDiffs((diffs) => { + diffs.onWorktreeDiff({ type: "agentManager.worktreeDiff", sessionId: "s1", diffs: [diff("a.ts")] }) + expect(diffs.diffDatas()["s1"]).toHaveLength(1) + }) + }) + + it("does not replace state when an update produces an identical diff list", () => { + withDiffs((diffs) => { + diffs.onWorktreeDiff({ type: "agentManager.worktreeDiff", sessionId: "s1", diffs: [diff("a.ts")] }) + const before = diffs.diffDatas() + diffs.onWorktreeDiff({ type: "agentManager.worktreeDiff", sessionId: "s1", diffs: [diff("a.ts")] }) + expect(diffs.diffDatas()).toBe(before) + }) + }) + + it("replaces a single file on a diffFile message and clears its pending flag", () => { + withDiffs((diffs) => { + diffs.onWorktreeDiff({ type: "agentManager.worktreeDiff", sessionId: "s1", diffs: [diff("a.ts", 1)] }) + diffs.onWorktreeDiffFile({ + type: "agentManager.worktreeDiffFile", + sessionId: "s1", + file: "a.ts", + diff: diff("a.ts", 9), + }) + expect(diffs.diffDatas()["s1"]![0]!.additions).toBe(9) + expect(diffs.diffFileLoadingFor(() => "s1").size).toBe(0) + }) + }) + + it("tracks panel loading via diffLoading", () => { + withDiffs((diffs) => { + diffs.onWorktreeDiffLoading({ type: "agentManager.worktreeDiffLoading", sessionId: "s1", loading: true }) + expect(diffs.diffLoading()).toBe(true) + diffs.onWorktreeDiffLoading({ type: "agentManager.worktreeDiffLoading", sessionId: "s1", loading: false }) + expect(diffs.diffLoading()).toBe(false) + }) + }) + + it("requestDiffFile marks a file pending, posts once, and ignores repeats", () => { + withDiffs((diffs, sent) => { + diffs.requestDiffFile("s1", "a.ts") + diffs.requestDiffFile("s1", "a.ts") + expect(sent.filter((m) => m.type === "agentManager.requestWorktreeDiffFile")).toHaveLength(1) + expect(diffs.diffFileLoadingFor(() => "s1").has("a.ts")).toBe(true) + }) + }) + + it("refreshStaleDiffs requests only files not already loading", () => { + withDiffs((diffs, sent) => { + diffs.requestDiffFile("s1", "a.ts") + diffs.refreshStaleDiffs("s1", new Set(["a.ts", "b.ts"])) + const files = sent.filter((m) => m.type === "agentManager.requestWorktreeDiffFile").map((m) => m.file) + expect(files).toEqual(["a.ts", "b.ts"]) + }) + }) + + it("clears the session key once its last pending file resolves", () => { + withDiffs((diffs) => { + diffs.requestDiffFile("s1", "a.ts") + diffs.onWorktreeDiffFile({ + type: "agentManager.worktreeDiffFile", + sessionId: "s1", + file: "a.ts", + diff: diff("a.ts"), + }) + expect(diffs.diffFileLoadingFor(() => "s1").size).toBe(0) + }) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/i18n-keys.test.ts b/packages/kilo-vscode/tests/unit/i18n-keys.test.ts index 11c8c277e3..1aa3db8111 100644 --- a/packages/kilo-vscode/tests/unit/i18n-keys.test.ts +++ b/packages/kilo-vscode/tests/unit/i18n-keys.test.ts @@ -41,6 +41,7 @@ import { dict as appTr } from "../../webview-ui/src/i18n/tr" import { dict as appNl } from "../../webview-ui/src/i18n/nl" import { dict as appUk } from "../../webview-ui/src/i18n/uk" import { dict as appIt } from "../../webview-ui/src/i18n/it" +import { dict as appFa } from "../../webview-ui/src/i18n/fa" // Layer 2: upstream UI (@opencode-ai/ui re-exported via @kilocode/kilo-ui) import { dict as uiEn } from "../../../ui/src/i18n/en" @@ -114,6 +115,7 @@ import { dict as cliTr } from "../../src/services/cli-backend/i18n/tr" import { dict as cliNl } from "../../src/services/cli-backend/i18n/nl" import { dict as cliUk } from "../../src/services/cli-backend/i18n/uk" import { dict as cliIt } from "../../src/services/cli-backend/i18n/it" +import { dict as cliFa } from "../../src/services/cli-backend/i18n/fa" import { dict as hostEn } from "../../src/services/i18n/en" import { dict as hostZh } from "../../src/services/i18n/zh" @@ -135,6 +137,7 @@ import { dict as hostTr } from "../../src/services/i18n/tr" import { dict as hostNl } from "../../src/services/i18n/nl" import { dict as hostUk } from "../../src/services/i18n/uk" import { dict as hostIt } from "../../src/services/i18n/it" +import { dict as hostFa } from "../../src/services/i18n/fa" // ── Locale maps ───────────────────────────────────────────────────────────── @@ -161,6 +164,7 @@ const appLocales: Record> = { nl: appNl, uk: appUk, it: appIt, + fa: appFa, } const kiloLocales: Record> = { @@ -230,6 +234,7 @@ const cliLocales: Record> = { nl: cliNl, uk: cliUk, it: cliIt, + fa: cliFa, } const hostLocales: Record> = { @@ -253,6 +258,7 @@ const hostLocales: Record> = { nl: hostNl, uk: hostUk, it: hostIt, + fa: hostFa, } // Merge webview dictionaries in the same priority order as language.tsx diff --git a/packages/kilo-vscode/tests/unit/kilo-provider-indexing-refresh.test.ts b/packages/kilo-vscode/tests/unit/kilo-provider-indexing-refresh.test.ts index 31ed93fd4f..cc698f3e0f 100644 --- a/packages/kilo-vscode/tests/unit/kilo-provider-indexing-refresh.test.ts +++ b/packages/kilo-vscode/tests/unit/kilo-provider-indexing-refresh.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "bun:test" import type { Config } from "@kilocode/sdk/v2/client" +import { fetchSnapshot } from "../../src/kilo-provider/config-snapshot" // vscode mock is provided by the shared preload (tests/setup/vscode-mock.ts) const { KiloProvider } = await import("../../src/KiloProvider") @@ -17,6 +18,7 @@ type Internals = { projectUnset?: string[][], ) => Promise fetchAndSendConfig: () => Promise + fetchAndSendConfigUpdated: () => Promise fetchAndSendProviders: () => Promise fetchAndSendAgents: () => Promise fetchAndSendSkills: () => Promise @@ -28,17 +30,18 @@ type Internals = { function createConnection() { let drains = 0 const patches: unknown[] = [] + const config = { snapshot: true } + const global = { snapshot: false } + const project = { default_agent: "code" } const client = { global: { config: { - get: async () => ({ data: {} }), - update: async () => ({ data: {} }), + get: async () => ({ data: global }), }, }, config: { - get: async () => ({ data: {} }), - update: async () => ({ data: {} }), - overlay: async () => ({ data: { project: {} } }), + get: async () => ({ data: config }), + overlay: async () => ({ data: { project } }), overlayUpdate: async (patch: unknown) => { patches.push(patch) return { data: {} } @@ -47,6 +50,7 @@ function createConnection() { } return { + client, drains: () => drains, patches: () => patches, service: { @@ -59,6 +63,29 @@ function createConnection() { } describe("KiloProvider indexing refresh", () => { + it("shares snapshot payloads across load, SSE refresh, and post-save refresh", async () => { + const conn = createConnection() + const settings = () => ({ maxCost: 0, languageCommitMessage: "sync" }) + const snapshot = await fetchSnapshot(conn.client as never, "/repo", settings) + const provider = new KiloProvider({} as never, conn.service as never) + const internal = provider as unknown as Internals + const sent: unknown[] = [] + provider.postMessage = (message) => void sent.push(message) + Object.assign(internal, { connectionState: "connected", commitMessageLanguageSetting: () => "sync" }) + await internal.fetchAndSendConfig() + await internal.fetchAndSendConfigUpdated() + await internal.handleUpdateConfig({}) + + expect(sent).toEqual([ + { type: "configLoaded", ...snapshot }, + { type: "configUpdated", ...snapshot }, + { type: "configUpdated", ...snapshot }, + ]) + sent.length = 0 + internal.connectionState = "disconnected" + await internal.fetchAndSendConfig() + expect(sent).toEqual([{ type: "configLoaded", ...snapshot }]) + }) it("reloadAfterAuthChange fetches config first, then indexing status", async () => { const provider = new KiloProvider({} as never, {} as never) const internal = provider as unknown as Internals diff --git a/packages/kilo-vscode/tests/unit/language-utils.test.ts b/packages/kilo-vscode/tests/unit/language-utils.test.ts index cc1a8c286b..b4dc8e0aee 100644 --- a/packages/kilo-vscode/tests/unit/language-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/language-utils.test.ts @@ -33,6 +33,11 @@ describe("normalizeLocale", () => { expect(normalizeLocale("ko-KR")).toBe("ko") }) + it("returns 'fa' for Persian", () => { + expect(normalizeLocale("fa")).toBe("fa") + expect(normalizeLocale("fa-IR")).toBe("fa") + }) + it("returns 'no' for Norwegian Bokmål", () => { expect(normalizeLocale("nb")).toBe("no") expect(normalizeLocale("nb-NO")).toBe("no") diff --git a/packages/kilo-vscode/tests/unit/prompt-rail.test.ts b/packages/kilo-vscode/tests/unit/prompt-rail.test.ts new file mode 100644 index 0000000000..d9854b5226 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/prompt-rail.test.ts @@ -0,0 +1,192 @@ +import { describe, expect, it } from "bun:test" +import { messageTurns } from "../../webview-ui/src/context/session-queue" +import { transcriptRows } from "../../webview-ui/src/context/transcript-rows" +import type { Message, Part, TextPart } from "../../webview-ui/src/types/messages" +import { capacity, previewText, promptItems, railItems } from "../../webview-ui/src/components/chat/prompt-rail" + +const base = { + sessionID: "session", + createdAt: "2026-01-01T00:00:00.000Z", + time: { created: 1 }, +} + +const user = (id: string, opts: Partial = {}): Message => ({ ...base, id, role: "user", ...opts }) +const assistant = (id: string, parentID: string, opts: Partial = {}): Message => ({ + ...base, + id, + parentID, + role: "assistant", + ...opts, +}) +const text = (id: string, messageID: string, value: string, opts: Partial = {}): Part => ({ + id, + messageID, + type: "text", + text: value, + ...opts, +}) +const tool = (id: string, messageID: string, title: string): Part => ({ + id, + messageID, + type: "tool", + tool: "bash", + state: { status: "completed", input: {}, output: "", title }, +}) +const lookup = (values: Record) => (id: string) => values[id] ?? [] + +describe("previewText", () => { + it("collapses whitespace and keeps plain text", () => { + expect(previewText(" hello\n\nworld ")).toBe("hello world") + }) + + it("drops fenced code blocks and inline code", () => { + expect(previewText("before `const x = 1` after\n```\nconst y = 2\n```\nend")).toBe("before after end") + }) + + it("keeps link labels but drops URLs and images", () => { + expect(previewText("see [the docs](https://example.com) and ![shot](img.png) now")).toBe("see the docs and now") + }) + + it("keeps bracket text inside inline code literal", () => { + expect(previewText("echo `[label](url)` verbatim")).toBe("echo verbatim") + }) + + it("strips heading, list, and quote markers", () => { + expect(previewText("# Title\n- one\n> two\nthree")).toBe("Title one two three") + }) +}) + +describe("promptItems", () => { + it("emits one item per non-partial user turn with prompt and answer", () => { + const u1 = user("u1") + const a1 = assistant("a1", "u1") + const u2 = user("u2") + const a2 = assistant("a2", "u2") + const parts = { + u1: [text("up1", "u1", "add authentication")], + a1: [text("ap1", "a1", "Done, auth is wired up.")], + u2: [text("up2", "u2", "now fix the bug")], + a2: [text("ap2", "a2", "Fixed it.")], + } + const rows = transcriptRows(messageTurns([u1, a1, u2, a2]), lookup(parts)) + + const items = promptItems(rows) + + expect(items).toEqual([ + { key: "u1:user", turn: "u1", queued: false, prompt: "add authentication", answer: "Done, auth is wired up." }, + { key: "u2:user", turn: "u2", queued: false, prompt: "now fix the bug", answer: "Fixed it." }, + ]) + }) + + it("keeps assistant chunks of one turn joined", () => { + const u1 = user("u1") + const a1 = assistant("a1", "u1") + const parts = { + u1: [text("up1", "u1", "go")], + a1: [text("ap1", "a1", "First."), text("ap2", "a1", "Second.")], + } + const rows = transcriptRows(messageTurns([u1, a1]), lookup(parts)) + + expect(promptItems(rows)[0]?.answer).toBe("First. Second.") + }) + + it("skips synthetic parts and leaves an empty answer for tool-only turns", () => { + const u1 = user("u1") + const a1 = assistant("a1", "u1") + const parts = { + u1: [text("up1", "u1", "run it"), text("up2", "u1", "internal", { synthetic: true })], + a1: [tool("at1", "a1", "Run tests"), text("at2", "a1", "hidden", { synthetic: true })], + } + const rows = transcriptRows(messageTurns([u1, a1]), lookup(parts)) + + expect(promptItems(rows)[0]).toMatchObject({ prompt: "run it", answer: "" }) + }) + + it("marks queued rows", () => { + const u1 = user("u1") + const u2 = user("u2") + const rows = transcriptRows(messageTurns([u1, u2]), lookup({}), { queued: new Set(["u2"]) }) + + expect(promptItems(rows).map((item) => item.queued)).toEqual([false, true]) + }) + + it("truncates long prompts and answers", () => { + const u1 = user("u1") + const a1 = assistant("a1", "u1") + const parts = { + u1: [text("up1", "u1", "x".repeat(400))], + a1: [text("ap1", "a1", "y".repeat(400))], + } + const rows = transcriptRows(messageTurns([u1, a1]), lookup(parts)) + + const [item] = promptItems(rows) + expect(item!.prompt.length).toBe(160) + expect(item!.prompt.endsWith("…")).toBe(true) + expect(item!.answer.length).toBe(220) + expect(item!.answer.endsWith("…")).toBe(true) + }) + + it("omits partial turns (assistant-only leads)", () => { + const a1 = assistant("a1", "u1") + const rows = transcriptRows(messageTurns([a1]), lookup({ a1: [text("p1", "a1", "hello")] })) + + expect(promptItems(rows)).toEqual([]) + }) + + it("promotes the answer when the prompt carries no text (image-only message)", () => { + const u1 = user("u1") + const a1 = assistant("a1", "u1") + const parts = { + u1: [{ id: "up1", messageID: "u1", type: "file", mime: "image/png", url: "data:," } as Part], + a1: [text("ap1", "a1", "That screenshot shows the rail overlapping the gutter.")], + } + const rows = transcriptRows(messageTurns([u1, a1]), lookup(parts)) + + expect(promptItems(rows)[0]).toMatchObject({ + prompt: "That screenshot shows the rail overlapping the gutter.", + answer: "", + }) + }) + + it("leaves both empty when neither prompt nor answer has text", () => { + const u1 = user("u1") + const rows = transcriptRows(messageTurns([u1]), lookup({})) + + expect(promptItems(rows)[0]).toMatchObject({ prompt: "", answer: "" }) + }) +}) + +describe("capacity", () => { + it("counts how many worst-case rows fit the transcript height", () => { + expect(capacity(24 + 76 * 5)).toBe(5) + expect(capacity(100)).toBe(1) + }) + + it("returns nothing usable for unmeasured or tiny transcripts", () => { + expect(capacity(0)).toBeLessThan(1) + expect(capacity(99)).toBeLessThan(1) + }) +}) + +describe("railItems", () => { + const items = Array.from({ length: 5 }, (_, i) => ({ + key: `k${i}`, + turn: `t${i}`, + queued: false, + prompt: `p${i}`, + answer: `a${i}`, + })) + + it("passes through when everything fits", () => { + expect(railItems(items, 5)).toEqual(items) + expect(railItems(items, 10)).toEqual(items) + }) + + it("keeps the newest items when capacity is smaller", () => { + expect(railItems(items, 2)).toEqual(items.slice(-2)) + }) + + it("returns nothing at zero capacity", () => { + expect(railItems(items, 0)).toEqual([]) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/session-outcome.test.ts b/packages/kilo-vscode/tests/unit/session-outcome.test.ts index 8dd06b6dd6..93b8475e5f 100644 --- a/packages/kilo-vscode/tests/unit/session-outcome.test.ts +++ b/packages/kilo-vscode/tests/unit/session-outcome.test.ts @@ -138,6 +138,13 @@ describe("terminal", () => { ).toBe("error") }) + it("hides superseded turns that handed off to a queued follow-up", () => { + expect( + terminal({ reason: "superseded", messages: [message("tool-calls")], todos: [todo("pending")] }), + ).toBeUndefined() + expect(terminal({ reason: "superseded", messages: [message("unknown")], todos: [] })).toBeUndefined() + }) + it("reports only the latest assistant finish reason", () => { const user: Message = { id: "u1", sessionID: "s1", role: "user", createdAt: new Date(1).toISOString() } expect(terminal({ reason: "completed", messages: [message("length"), user], todos: [] })?.finish).toBeUndefined() diff --git a/packages/kilo-vscode/tests/unit/session-terminal-manager.test.ts b/packages/kilo-vscode/tests/unit/session-terminal-manager.test.ts index 4e763196bf..5669acaeac 100644 --- a/packages/kilo-vscode/tests/unit/session-terminal-manager.test.ts +++ b/packages/kilo-vscode/tests/unit/session-terminal-manager.test.ts @@ -9,6 +9,7 @@ import { describe, it, expect } from "bun:test" import path from "node:path" import { Project, SyntaxKind } from "ts-morph" import { SessionTerminalManager, type TerminalHost } from "../../src/agent-manager/SessionTerminalManager" +import type { WorktreeStateManager } from "../../src/agent-manager/WorktreeStateManager" const ROOT = path.resolve(import.meta.dir, "../..") const FILE = path.join(ROOT, "src/agent-manager/SessionTerminalManager.ts") @@ -130,8 +131,8 @@ describe("SessionTerminalManager structure", () => { expect(text).toContain("panel command registration skipped") }) - it("resolves the session that owns the active managed terminal", () => { - const text = body("activeSession") + it("resolves the key that owns the active managed terminal", () => { + const text = body("activeKey") expect(text).toContain("this.host.activeTerminal()") expect(text).toContain("entry.terminal === active") }) @@ -139,7 +140,8 @@ describe("SessionTerminalManager structure", () => { it("rejects context capture from another managed session", () => { const text = body("prepareContext") expect(text).toContain("this.showExisting(sessionId)") - expect(text).toContain("this.activeSession()") + expect(text).toContain("this.activeKey()") + expect(text).toContain("SessionTerminalManager.sessionKey(sessionId)") }) }) @@ -162,3 +164,74 @@ describe("SessionTerminalManager command restoration", () => { state.manager.dispose() }) }) + +describe("SessionTerminalManager worktree terminals", () => { + function scene(opts: { worktreePath?: string; repoPath?: string } = {}) { + const created: Array<{ cwd: string; name: string }> = [] + const warnings: string[] = [] + let shown = 0 + const host: TerminalHost = { + createTerminal(o) { + created.push(o) + return { + show: () => shown++, + dispose() {}, + exitStatus: undefined, + } + }, + activeTerminal: () => undefined, + repoPath: () => opts.repoPath, + showWarning: (msg) => warnings.push(msg), + setContext() {}, + onTerminalClosed: () => ({ dispose() {} }), + onActiveTerminalChanged: () => ({ dispose() {} }), + registerCommand: () => ({ dispose() {} }), + executeCommand: () => Promise.resolve(), + } + const state = { + getWorktree: (id: string) => + opts.worktreePath ? { id, path: opts.worktreePath, branch: "feature/x" } : undefined, + } as unknown as WorktreeStateManager + const manager = new SessionTerminalManager(() => {}, host) + return { manager, state, created, warnings, shown: () => shown } + } + + it("creates a terminal rooted at the worktree path", () => { + const s = scene({ worktreePath: "/repo/.kilo/worktrees/wt-1", repoPath: "/repo" }) + s.manager.showWorktreeTerminal("wt-1", s.state) + expect(s.created).toEqual([{ cwd: "/repo/.kilo/worktrees/wt-1", name: "Agent: feature/x" }]) + expect(s.shown()).toBe(1) + }) + + it("reuses the live terminal on repeat calls", () => { + const s = scene({ worktreePath: "/repo/.kilo/worktrees/wt-1" }) + s.manager.showWorktreeTerminal("wt-1", s.state) + s.manager.showWorktreeTerminal("wt-1", s.state) + expect(s.created).toHaveLength(1) + expect(s.shown()).toBe(2) + }) + + it("keeps session and worktree terminal keys in separate namespaces", () => { + const s = scene({ worktreePath: "/repo/.kilo/worktrees/wt-1", repoPath: "/repo" }) + s.manager.showTerminal("worktree:wt-1", undefined) + s.manager.showWorktreeTerminal("wt-1", s.state) + expect(s.created).toEqual([ + { cwd: "/repo", name: "Agent: local" }, + { cwd: "/repo/.kilo/worktrees/wt-1", name: "Agent: feature/x" }, + ]) + expect(s.shown()).toBe(2) + }) + + it("falls back to the repo root when the worktree is unknown", () => { + const s = scene({ repoPath: "/repo" }) + s.manager.showWorktreeTerminal("gone", s.state) + expect(s.created).toEqual([{ cwd: "/repo", name: "Agent: worktree" }]) + }) + + it("warns and creates nothing when no cwd resolves", () => { + const s = scene({}) + s.manager.showWorktreeTerminal("gone", s.state) + expect(s.created).toHaveLength(0) + expect(s.warnings).toHaveLength(1) + }) +}) diff --git a/packages/kilo-vscode/tests/visual-regression.spec.mts b/packages/kilo-vscode/tests/visual-regression.spec.mts index 3f210bb7cb..437ac4bd85 100644 --- a/packages/kilo-vscode/tests/visual-regression.spec.mts +++ b/packages/kilo-vscode/tests/visual-regression.spec.mts @@ -75,9 +75,12 @@ async function settle(page: Page) { // Spinner animation captures at an indeterminate frame, causing flaky diffs. // Permission dock config-preloaded has non-deterministic toggle rendering. // Sandboxing rows can settle at different scroll heights after settings context updates. +// Side terminal tabs mount live xterm instances whose websocket error text +// lands at indeterminate times. const SKIP = new Set([ "agentmanager--worktree-item-busy", "agentmanager--full-screen-diff-agent-edit-scroll", + "agentmanager--side-terminal-panel-tabs", "composite-webview--permission-dock-config-preloaded", "settings--sandboxing-allowlist", "settings--sandboxing-panel", diff --git a/packages/kilo-vscode/tests/visual-regression.spec.ts b/packages/kilo-vscode/tests/visual-regression.spec.ts index 3f210bb7cb..437ac4bd85 100644 --- a/packages/kilo-vscode/tests/visual-regression.spec.ts +++ b/packages/kilo-vscode/tests/visual-regression.spec.ts @@ -75,9 +75,12 @@ async function settle(page: Page) { // Spinner animation captures at an indeterminate frame, causing flaky diffs. // Permission dock config-preloaded has non-deterministic toggle rendering. // Sandboxing rows can settle at different scroll heights after settings context updates. +// Side terminal tabs mount live xterm instances whose websocket error text +// lands at indeterminate times. const SKIP = new Set([ "agentmanager--worktree-item-busy", "agentmanager--full-screen-diff-agent-edit-scroll", + "agentmanager--side-terminal-panel-tabs", "composite-webview--permission-dock-config-preloaded", "settings--sandboxing-allowlist", "settings--sandboxing-panel", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index ce6f795941..8397077e5d 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -24,8 +24,6 @@ import type { AgentManagerWorktreeDiffFileMessage, AgentManagerWorktreeDiffLoadingMessage, AgentManagerApplyWorktreeDiffResultMessage, - AgentManagerApplyWorktreeDiffStatus, - AgentManagerApplyWorktreeDiffConflict, AgentManagerWorktreeStatsMessage, AgentManagerLocalStatsMessage, WorktreeFileDiff, @@ -41,7 +39,6 @@ import type { SessionCreatedMessage, BranchInfo, } from "../src/types/messages" -import { IndexingProvider } from "../src/context/indexing" import { DragDropProvider, DragDropSensors, @@ -51,43 +48,24 @@ import { createSortable, } from "@thisbeyond/solid-dnd" import type { DragEvent } from "@thisbeyond/solid-dnd" -import { ThemeProvider } from "@kilocode/kilo-ui/theme" -import { DialogProvider, useDialog } from "@kilocode/kilo-ui/context/dialog" +import { useDialog } from "@kilocode/kilo-ui/context/dialog" import { Dialog } from "@kilocode/kilo-ui/dialog" import { DropdownMenu } from "@kilocode/kilo-ui/dropdown-menu" -import { MarkedProvider } from "@kilocode/kilo-ui/context/marked" -import { CodeComponentProvider } from "@kilocode/kilo-ui/context/code" -import { DiffComponentProvider } from "@kilocode/kilo-ui/context/diff" -import { FileComponentProvider } from "@kilocode/kilo-ui/context/file" -import { Code } from "@kilocode/kilo-ui/code" -import { Diff } from "@kilocode/kilo-ui/diff" -import { File } from "@kilocode/kilo-ui/file" -import { Toast, showToast } from "@kilocode/kilo-ui/toast" +import { showToast } from "@kilocode/kilo-ui/toast" import { ResizeHandle } from "@kilocode/kilo-ui/resize-handle" import { Icon } from "@kilocode/kilo-ui/icon" import { Button } from "@kilocode/kilo-ui/button" import { IconButton } from "@kilocode/kilo-ui/icon-button" import { Spinner } from "@kilocode/kilo-ui/spinner" import { Tooltip, TooltipKeybind } from "@kilocode/kilo-ui/tooltip" -import { VSCodeProvider, useVSCode } from "../src/context/vscode" -import { ServerProvider } from "../src/context/server" -import { ProviderProvider } from "../src/context/provider" -import { ConfigProvider } from "../src/context/config" -import { DisplayProvider } from "../src/context/display" -import { KiloEmbeddingModelsProvider } from "../src/context/kilo-embedding-models" -import { ImageModelsProvider } from "../src/context/image-models" -import { NotificationsProvider } from "../src/context/notifications" -import { FeedbackProvider } from "../src/context/feedback" -import { MemoryProvider } from "../src/context/memory" -import { SessionProvider, useSession } from "../src/context/session" -import { AgentRequirementsProvider } from "../src/context/agent-requirements" +import { useVSCode } from "../src/context/vscode" +import { useSession } from "../src/context/session" import { WorktreeModeProvider } from "../src/context/worktree-mode" +import { ProviderShell } from "../src/context/provider-shell" import { ChatView } from "../src/components/chat" -import { SpeechToTextPrewarm } from "../src/components/speech-to-text/SpeechToTextPrewarm" import HistoryView from "../src/components/history/HistoryView" import { NewWorktreeDialog } from "./NewWorktreeDialog" -import { DataBridge, MermaidDownloadBridge } from "../src/App" -import { LanguageBridge } from "../src/context/language-bridge" +import { DataBridge } from "../src/App" import { useLanguage } from "../src/context/language" import { createTabFocus } from "../src/utils/tab-navigation" import { @@ -126,14 +104,16 @@ import { createTerminalHandlers, createTerminalMessageHandler, createSideTerminal, + readSavedDestination, + resolveVscodeTerminalRequest, } from "./terminal" import { focusCurrentTab, renderTab, renderTerminalLayer, renderNewTabButton } from "./tab-rendering" import { useTabScroll } from "./tab-scroll" import { DiffPanel } from "./DiffPanel" import { createRevertFile } from "./revert-file" import { FullScreenDiffView } from "../diff-viewer/FullScreenDiffView" -import { ApplyDialog } from "./ApplyDialog" -import { groupApplyConflicts } from "./apply-conflicts" +import { createApplyToLocal } from "./apply-to-local" +import { createWorktreeDiffs } from "./worktree-diffs" import type { ReviewComment } from "../diff-viewer/review-comments" import { clearReviewComposer, createReviewComposer } from "../diff-viewer/review-annotations" import type { SidebarSearchMenuRef } from "./SidebarSearchMenu" @@ -156,7 +136,6 @@ import { } from "./section-helpers" import { sectionAwareDetector } from "./section-dnd" import { ConstrainDragXAxis } from "./constrain-drag-x" -import { mergeWorktreeDiffs } from "../diff-viewer/diff-state" import { initialMessage, seedInitialVariant } from "./initial-message" import { createMarkdownRender } from "./review-preferences" import { createSidebarCollapse } from "./sidebar-collapse" @@ -183,12 +162,6 @@ interface WorktreeBusyState { message?: string branch?: string } - -interface ApplyState { - status: AgentManagerApplyWorktreeDiffStatus - message: string - conflicts: AgentManagerApplyWorktreeDiffConflict[] -} /** Sidebar selection: LOCAL for local repo, worktree ID for a worktree, or null for an unassigned session. */ type SidebarSelection = typeof LOCAL | string | null type SidePanel = "diff" | "pr" | "terminal" | null @@ -241,6 +214,8 @@ const AgentManagerContent: Component = () => { const [repoBranch, setRepoBranch] = createSignal() const [busyWorktrees, setBusyWorktrees] = createSignal>(new Map()) const [staleWorktreeIds, setStaleWorktreeIds] = createSignal>(new Set()) + /** True while the ⌘/Ctrl jump modifier is held — reveals the ⌘1-9 badges on all sidebar items. */ + const [held, setHeld] = createSignal(false) const [worktreesLoaded, setWorktreesLoaded] = createSignal(false) const [sessionsLoaded, setSessionsLoaded] = createSignal(false) const [isGitRepo, setIsGitRepo] = createSignal(true) @@ -282,9 +257,10 @@ const AgentManagerContent: Component = () => { const [history, setHistory] = createSignal(false) const [sidePanel, setSidePanel] = createSignal(null) const diffOpen = () => sidePanel() === "diff" - const [diffDatas, setDiffDatas] = createSignal>({}) - const [diffLoading, setDiffLoading] = createSignal(false) - const [diffFileLoading, setDiffFileLoading] = createSignal>>({}) + const diffs = createWorktreeDiffs(vscode) + const diffDatas = diffs.diffDatas + const diffLoading = diffs.diffLoading + const setDiffLoading = diffs.setDiffLoading // The diff and terminal panels each remember their own width: a diff // benefits from half the window, a terminal only needs about a third. const TERMINAL_MIN_WIDTH = 360 @@ -331,12 +307,6 @@ const AgentManagerContent: Component = () => { // Local repo git stats (branch name, diff additions/deletions, commits) const [localStats, setLocalStats] = createSignal() - // Per-worktree apply-to-local status - const [applyStates, setApplyStates] = createSignal>({}) - const [applyTarget, setApplyTarget] = createSignal() - const [applySelectedFiles, setApplySelectedFiles] = createSignal([]) - const [applySelectionTouched, setApplySelectionTouched] = createSignal(false) - const PENDING_PREFIX = "pending:" const closedDrafts = new Set() const [activePendingId, setActivePendingId] = createSignal() @@ -391,12 +361,6 @@ const AgentManagerContent: Component = () => { setReviewCommentsByContext((prev) => ({ ...prev, [sel]: comments })) } - const applyStateForSelection = createMemo(() => { - const sel = selection() - if (!sel || sel === LOCAL) return undefined - return applyStates()[sel] - }) - const resolveWorktreeSessionId = (worktreeId: string) => { const id = session.currentSessionID() if (id) { @@ -406,157 +370,19 @@ const AgentManagerContent: Component = () => { return managedSessions().find((entry) => entry.worktreeId === worktreeId)?.id } - const applyTargetSessionId = createMemo(() => { - const target = applyTarget() - if (!target) return undefined - return resolveWorktreeSessionId(target) + const apply = createApplyToLocal({ + vscode, + dialog, + t, + selection, + local: LOCAL, + worktrees, + diffDatas, + diffLoading, + resolveWorktreeSessionId, + track: metrics.track, }) - - const applyDiffs = createMemo(() => { - const target = applyTarget() - if (!target) return [] as WorktreeFileDiff[] - const data = diffDatas() - const current = applyTargetSessionId() - if (current && data[current]) return data[current]! - const ids = managedSessions() - .filter((entry) => entry.worktreeId === target) - .map((entry) => entry.id) - for (const id of ids) { - if (data[id]) return data[id]! - } - return [] as WorktreeFileDiff[] - }) - - const applyStateForTarget = createMemo(() => { - const target = applyTarget() - if (!target) return undefined - return applyStates()[target] - }) - - const applyBusyForTarget = createMemo(() => { - const state = applyStateForTarget() - if (!state) return false - return state.status === "checking" || state.status === "applying" - }) - - const applySelectedSet = createMemo(() => new Set(applySelectedFiles())) - - const applySelectionStats = createMemo(() => { - const set = applySelectedSet() - const selected = applyDiffs().filter((diff) => set.has(diff.file)) - const additions = selected.reduce((sum, diff) => sum + diff.additions, 0) - const deletions = selected.reduce((sum, diff) => sum + diff.deletions, 0) - return { - total: applyDiffs().length, - selected: selected.length, - additions, - deletions, - } - }) - - const applyHasSelection = createMemo(() => applySelectionStats().selected > 0) - - const applyConflictRows = createMemo(() => groupApplyConflicts(applyStateForTarget()?.conflicts ?? [])) - - const applyToLocal = (worktreeId: string, selectedFiles: string[]) => { - setApplyStates((prev) => ({ - ...prev, - [worktreeId]: { - status: "checking", - message: t("agentManager.apply.checking"), - conflicts: [], - }, - })) - vscode.postMessage({ type: "agentManager.applyWorktreeDiff", worktreeId, selectedFiles }) - } - - const resetApplyDialog = () => { - setApplyTarget(undefined) - setApplySelectedFiles([]) - setApplySelectionTouched(false) - } - - const closeApplyDialog = () => { - resetApplyDialog() - dialog.close() - } - - const applySelectAll = () => { - setApplySelectionTouched(true) - setApplySelectedFiles(applyDiffs().map((diff) => diff.file)) - } - - const applySelectNone = () => { - setApplySelectionTouched(true) - setApplySelectedFiles([]) - } - - const applyToggleFile = (file: string, checked: boolean) => { - setApplySelectionTouched(true) - setApplySelectedFiles((prev) => { - if (checked) { - if (prev.includes(file)) return prev - const set = new Set(prev) - set.add(file) - return applyDiffs() - .map((diff) => diff.file) - .filter((path) => set.has(path)) - } - if (!prev.includes(file)) return prev - return prev.filter((path) => path !== file) - }) - } - - const triggerApply = () => { - const target = applyTarget() - if (!target) return - if (!applyHasSelection()) return - if (applyBusyForTarget()) return - metrics.track("apply_to_local", "apply_dialog", { fileCount: applySelectedFiles().length }) - applyToLocal(target, applySelectedFiles()) - } - - const openApplyDialog = () => { - const sel = selection() - if (!sel || sel === LOCAL) return - setApplyStates((prev) => { - if (!prev[sel]) return prev - const next = { ...prev } - delete next[sel] - return next - }) - setApplyTarget(sel) - setApplySelectionTouched(false) - setApplySelectedFiles([]) - const sid = resolveWorktreeSessionId(sel) - if (sid) vscode.postMessage({ type: "agentManager.requestWorktreeDiff", sessionId: sid }) - - setApplySelectedFiles(applyDiffs().map((diff) => diff.file)) - - dialog.show( - () => ( - - ), - resetApplyDialog, - ) - } + const openApplyDialog = apply.openApplyDialog const openWorktreeDirectory = () => { const sel = selection() @@ -588,31 +414,6 @@ const AgentManagerContent: Component = () => { if (sel) runWorktree(sel) } - createEffect( - on( - () => [applyTarget(), applyDiffs(), applySelectionTouched()] as const, - ([target, diffs, touched]) => { - if (!target) return - const files = diffs.map((diff) => diff.file) - if (files.length === 0) { - if (!touched) setApplySelectedFiles([]) - return - } - - if (!touched) { - setApplySelectedFiles(files) - return - } - - const current = applySelectedFiles() - const set = new Set(current) - const next = files.filter((file) => set.has(file)) - const same = next.length === current.length && next.every((file, index) => file === current[index]) - if (!same) setApplySelectedFiles(next) - }, - ), - ) - const isPending = (id: string) => id.startsWith(PENDING_PREFIX) reportRemoteSessions(vscode, localSessionIDs, managedSessions, isPending) @@ -728,14 +529,6 @@ const AgentManagerContent: Component = () => { if (Object.keys(next).length === Object.keys(prev).length) return prev return next }) - setApplyStates((prev) => { - const next = Object.fromEntries(Object.entries(prev).filter(([id]) => ids.has(id))) - if (Object.keys(next).length === Object.keys(prev).length) return prev - return next - }) - - const target = applyTarget() - if (target && !ids.has(target)) closeApplyDialog() }) const worktreeSessionIds = createMemo( @@ -1207,6 +1000,18 @@ const AgentManagerContent: Component = () => { } window.addEventListener("keydown", deleteKeyHandler) + // Reveal the ⌘/Ctrl+1-9 jump badges on all sidebar items while the modifier is held. + // Capture phase so the terminal's key handlers can't swallow them; blur resets state + // when the keyup is lost (e.g. Cmd+Tab away). + const modifier = isMac ? "Meta" : "Control" + const modTrack = (e: KeyboardEvent) => { + if (e.key === modifier) setHeld(e.type === "keydown") + } + const modReset = () => setHeld(false) + window.addEventListener("keydown", modTrack, true) + window.addEventListener("keyup", modTrack, true) + window.addEventListener("blur", modReset) + // When the panel regains focus (e.g. returning from terminal), focus the prompt // and clear any stale body styles left by Kobalte modal overlays (dropdowns/dialogs // set pointer-events:none and overflow:hidden on body, but cleanup never runs if @@ -1283,7 +1088,7 @@ const AgentManagerContent: Component = () => { // a slow create landing after a mode switch must not steal it. if (sidePanel() === "terminal" && terms.sideKey() === contextKey) terms.requestFocus(terminalId) }, - onDestinationChanged: (destination) => sideCtl.setDestination(destination), + onDestinationChanged: (destination) => sideCtl.syncDefault(destination), }) const unsubTerminals = vscode.onMessage((msg) => { terminalDispatch(msg) @@ -1492,65 +1297,19 @@ const AgentManagerContent: Component = () => { } if (msg.type === "agentManager.worktreeDiff") { - const ev = msg as AgentManagerWorktreeDiffMessage - let staleFiles: Set | undefined - setDiffDatas((prev) => { - const existing = prev[ev.sessionId] - const merged = existing - ? mergeWorktreeDiffs(existing, ev.diffs) - : { diffs: ev.diffs, stale: new Set() } - staleFiles = merged.stale - const next = merged.diffs - if (existing && existing.length === next.length && existing.every((old, i) => old === next[i])) return prev - return { ...prev, [ev.sessionId]: next } - }) - if (staleFiles) refreshStaleDiffs(ev.sessionId, staleFiles) + diffs.onWorktreeDiff(msg as AgentManagerWorktreeDiffMessage) } if (msg.type === "agentManager.worktreeDiffFile") { - const ev = msg as AgentManagerWorktreeDiffFileMessage - if (ev.diff) { - setDiffDatas((prev) => { - const existing = prev[ev.sessionId] ?? [] - const next = existing.map((item) => (item.file === ev.diff!.file ? ev.diff! : item)) - return { ...prev, [ev.sessionId]: next } - }) - setDiffFilePending(ev.sessionId, ev.diff.file, false) - return - } - setDiffFilePending(ev.sessionId, ev.file, false) + diffs.onWorktreeDiffFile(msg as AgentManagerWorktreeDiffFileMessage) } if (msg.type === "agentManager.worktreeDiffLoading") { - const ev = msg as AgentManagerWorktreeDiffLoadingMessage - setDiffLoading(ev.loading) + diffs.onWorktreeDiffLoading(msg as AgentManagerWorktreeDiffLoadingMessage) } if (msg.type === "agentManager.applyWorktreeDiffResult") { - const ev = msg as AgentManagerApplyWorktreeDiffResultMessage - const files = new Set((ev.conflicts ?? []).map((entry) => entry.file).filter(Boolean)).size - const count = ev.conflicts?.length ?? 0 - setApplyStates((prev) => ({ - ...prev, - [ev.worktreeId]: { - status: ev.status, - message: ev.message, - conflicts: ev.conflicts ?? [], - }, - })) - - if (ev.status === "success") { - showToast({ variant: "success", title: t("agentManager.apply.success"), description: ev.message }) - if (applyTarget() === ev.worktreeId) closeApplyDialog() - } - if (ev.status === "conflict") { - const summary = - count > 0 ? t("agentManager.apply.conflictToast", { count, files: Math.max(files, 1) }) : ev.message - showToast({ variant: "error", title: t("agentManager.apply.conflict"), description: summary }) - } - if (ev.status === "error") { - showToast({ variant: "error", title: t("agentManager.apply.error"), description: ev.message }) - } + apply.onApplyResult(msg as AgentManagerApplyWorktreeDiffResultMessage) } if (msg.type === "agentManager.revertWorktreeFileResult") revertCtl.onResult(msg as never) @@ -1578,6 +1337,9 @@ const AgentManagerContent: Component = () => { window.removeEventListener("message", handler) window.removeEventListener("keydown", preventDefaults, true) window.removeEventListener("keydown", deleteKeyHandler) + window.removeEventListener("keydown", modTrack, true) + window.removeEventListener("keyup", modTrack, true) + window.removeEventListener("blur", modReset) window.removeEventListener("focus", onWindowFocus) window.removeEventListener("newTaskRequest", newTaskHandler, true) drafts.cleanup() @@ -1704,54 +1466,13 @@ const AgentManagerContent: Component = () => { vscode.postMessage({ type: "agentManager.setReviewDiffStyle", style }) } - const setDiffFilePending = (sessionId: string, file: string, value: boolean) => { - setDiffFileLoading((prev) => { - const session = prev[sessionId] ?? {} - if (value) { - if (session[file]) return prev - return { - ...prev, - [sessionId]: { ...session, [file]: true }, - } - } - - if (!session[file]) return prev - const next = { ...session } - delete next[file] - if (Object.keys(next).length === 0) { - const result = { ...prev } - delete result[sessionId] - return result - } - return { - ...prev, - [sessionId]: next, - } - }) - } - const requestDiffFile = (file: string) => { const sessionId = currentDiffSessionId() if (!sessionId) return - if (diffFileLoading()[sessionId]?.[file]) return - setDiffFilePending(sessionId, file, true) - vscode.postMessage({ type: "agentManager.requestWorktreeDiffFile", sessionId, file }) + diffs.requestDiffFile(sessionId, file) } - const refreshStaleDiffs = (sessionId: string, files: Set) => { - const loading = diffFileLoading()[sessionId] ?? {} - for (const file of files) { - if (loading[file]) continue - setDiffFilePending(sessionId, file, true) - vscode.postMessage({ type: "agentManager.requestWorktreeDiffFile", sessionId, file }) - } - } - - const diffFileLoadingForCurrent = createMemo(() => { - const sessionId = currentDiffSessionId() - if (!sessionId) return new Set() - return new Set(Object.keys(diffFileLoading()[sessionId] ?? {})) - }) + const diffFileLoadingForCurrent = createMemo(() => diffs.diffFileLoadingFor(currentDiffSessionId)) const revertCtl = createRevertFile(currentDiffSessionId, vscode, showToast, t) @@ -2092,9 +1813,6 @@ const AgentManagerContent: Component = () => { postMessage: (msg) => vscode.postMessage(msg as never), onRemove: freezeTabs, onShowSide: showSideTerminal, - onHideSide: () => { - if (sidePanel() === "terminal") setSidePanel(null) - }, getSelection: selection, LOCAL, REVIEW_TAB_ID, @@ -2103,16 +1821,22 @@ const AgentManagerContent: Component = () => { const sideCtl = createSideTerminal({ handlers: termHandlers, visible: () => sidePanel() === "terminal", - focused: () => terms.focusedId() !== undefined && terms.focusedId() === terms.side()?.id, + focusedId: () => terms.sideFocusedId(), hide: () => setSidePanel(null), refocus: () => window.dispatchEvent(new Event("focusPrompt")), postMessage: (msg) => vscode.postMessage(msg as never), track: (button, surface, properties) => metrics.track(button, surface, properties), - openVscode: () => { - const id = session.currentSessionID() - if (id) vscode.postMessage({ type: "agentManager.showTerminal", sessionId: id }) - else if (selection() === LOCAL) vscode.postMessage({ type: "agentManager.showLocalTerminal" }) - }, + // Panel-local pick, immune to cross-window setting echoes (see side.ts). + saved: readSavedDestination(vscode.getState>()), + save: (d) => vscode.setState({ ...vscode.getState>(), terminalDestination: d }), + openVscode: () => + vscode.postMessage( + resolveVscodeTerminalRequest( + selection(), + session.currentSessionID(), + (wt) => managedSessions().find((ms) => ms.worktreeId === wt)?.id, + ) as never, + ), }) const handleReviewTabMouseDown = (e: MouseEvent) => { @@ -2186,8 +1910,8 @@ const AgentManagerContent: Component = () => { if (!id) return undefined if (id === REVIEW_TAB_ID) return { id, title: t("session.tab.review") } if (isTerminalTabId(id)) { - const term = terms.lookup().get(id) - return term ? { id, title: term.title } : undefined + const title = terms.title(id) + return title ? { id, title } : undefined } return activeTabs().find((s) => s.id === id) }) @@ -2215,8 +1939,8 @@ const AgentManagerContent: Component = () => { const closeActiveTab = () => { // A focused side terminal owns Cmd+W while its panel is visible — // closing a chat tab out from under the user's cursor would be - // surprising. - if (sidePanel() === "terminal" && terms.focusedId() && terms.focusedId() === terms.side()?.id) { + // surprising. Only that terminal dies; the panel keeps the rest. + if (sidePanel() === "terminal" && terms.sideFocusedId()) { if (sideCtl.close()) return } if (termHandlers.closeActive()) { @@ -2269,7 +1993,7 @@ const AgentManagerContent: Component = () => { >
@@ -2741,11 +2465,7 @@ const AgentManagerContent: Component = () => { const s = stats() return s && (s.files > 0 || s.additions > 0 || s.deletions > 0) } - const applyBusy = () => { - const state = applyStateForSelection() - if (!state) return false - return state.status === "checking" || state.status === "applying" - } + const applyBusy = apply.applyBusyForSelection return ( <> @@ -3092,8 +2812,9 @@ const AgentManagerContent: Component = () => { state={terms} contextKey={terms.sideKey} visible={() => sidePanel() === "terminal"} - onClose={() => sideCtl.close()} - onStart={() => termHandlers.requestSide()} + onSelect={(id) => termHandlers.selectSide(id)} + onClose={(id) => termHandlers.closeSide(id)} + onStart={() => termHandlers.addSide()} />
@@ -3139,53 +2860,16 @@ const AgentManagerContent: Component = () => { export const AgentManagerApp: Component = () => { return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + ) } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx b/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx index a349d0fe20..82488b9de2 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx @@ -167,6 +167,13 @@ export const WorktreeItem: Component = (props) => { props.onOpenPR?.() } + /** Worktree directory basename shown in the hover card (e.g. "decorous-taker"). */ + const name = () => { + const p = props.worktree.path.replace(/[\\/]+$/, "") + const i = Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\")) + return i >= 0 ? p.slice(i + 1) : p + } + return ( <> @@ -387,6 +394,11 @@ export const WorktreeItem: Component = (props) => { {props.navHint} +
+
+ {t("agentManager.hoverCard.worktree")} + {name()} +
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 6cfde9cf3b..6fe20d7175 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css @@ -426,6 +426,25 @@ button.am-section-toggle:hover .am-section-label { color: var(--text-weak); } +/* Jump modifier held (⌘/Ctrl): reveal shortcut badges on every item, not just the + hovered one. Scoped via :has so items without a badge (beyond ⌘9) keep their stats. + The close button stays hidden so holding the modifier never exposes the delete action. */ +.am-show-shortcuts .am-wt-hover-actions:has(.am-shortcut-badge) { + opacity: 1; + visibility: visible; +} +.am-show-shortcuts .am-wt-hover-actions .am-worktree-close { + display: none; +} +.am-show-shortcuts .am-wt-actions-cell:has(.am-shortcut-badge) > .am-worktree-stats, +.am-show-shortcuts .am-wt-actions-cell:has(.am-shortcut-badge) > .am-worktree-stats-skeleton { + opacity: 0; + visibility: hidden; +} +.am-show-shortcuts .am-local-item .am-shortcut-badge { + opacity: 1; +} + .am-worktree-item:has(.am-worktree-rename-input) .am-wt-row2 { display: none; } @@ -4599,6 +4618,59 @@ body.vscode-high-contrast-light { color: var(--text-weak); } +/* Side terminal tab strip — one row of tabs reusing the top bar's + .am-tab chrome, plus the "+" action. Height matches .am-diff-header + (32px: 4px padding + 24px content) so switching inspector modes does + not shift the panel chrome. The strip itself never scrolls; the tab + list does, so a narrow panel never pushes the "+" action out of view + (same split as .am-tab-list-wrap / .am-tab-add-wrap). */ +.am-side-terminal-tabs { + display: flex; + align-items: stretch; + height: 32px; + padding: 4px 4px 0; + gap: 2px; + flex-shrink: 0; + border-bottom: 1px solid var(--border-weak-base); + background: var(--surface-base); + position: relative; + z-index: 20; +} + +.am-side-terminal-tablist { + display: flex; + align-items: stretch; + gap: 2px; + flex: 1; + min-width: 0; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scrollbar-width: none; +} + +.am-side-terminal-tablist::-webkit-scrollbar { + display: none; +} + +/* Each tab shares the available width and shrinks with ellipsis. + touch-action unlocks pointer-based drag reordering (same as + .am-tab-sortable). */ +.am-side-terminal-tab { + display: flex; + flex: 0 1 140px; + min-width: 64px; + height: 100%; + touch-action: none; +} + +.am-side-terminal-add { + display: flex; + align-items: center; + flex-shrink: 0; + padding: 0 2px; +} + /* Hidden-but-alive side panel host: taken out of the flow so the chat reclaims the width, but kept painted so hidden side terminals keep streaming. Anchored to .am-detail-stack (position: relative). */ @@ -4615,14 +4687,15 @@ body.vscode-high-contrast-light { flex: 1; min-height: 0; min-width: 0; - padding: 8px; background: var(--vscode-terminal-background, #1e1e1e); } /* Third-party xterm classes — addressed by attribute selector so the - agent-manager "am-* prefix" architecture test does not flag them. */ -.am-terminal-host [class="xterm"] { + agent-manager "am-* prefix" architecture test does not flag them. + FitAddon subtracts padding from xterm itself, not its parent host. */ +.am-terminal-host [class~="xterm"] { height: 100%; + padding: 8px; } .am-terminal-host [class~="xterm-viewport"] { diff --git a/packages/kilo-vscode/webview-ui/agent-manager/apply-to-local.tsx b/packages/kilo-vscode/webview-ui/agent-manager/apply-to-local.tsx new file mode 100644 index 0000000000..202427e8e7 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/apply-to-local.tsx @@ -0,0 +1,285 @@ +/** @jsxImportSource solid-js */ + +/** + * Apply-to-local workflow for Agent Manager. + * + * Owns everything about applying a selected worktree's diff into the local + * repo: the per-worktree apply status, the dialog's file selection state, the + * derived memos that drive `ApplyDialog`, and the result/toast handling for + * the backend's `applyWorktreeDiffResult` message. Extracted from + * `AgentManagerApp.tsx` so the app component only wires the workflow in. + */ + +import { createEffect, createMemo, createSignal, on, type Accessor } from "solid-js" +import { showToast } from "@kilocode/kilo-ui/toast" +import { groupApplyConflicts } from "./apply-conflicts" +import { ApplyDialog } from "./ApplyDialog" +import type { tracker } from "./telemetry" +import type { useDialog } from "@kilocode/kilo-ui/context/dialog" +import type { useLanguage } from "../src/context/language" +import type { useVSCode } from "../src/context/vscode" +import type { AgentManagerApplyWorktreeDiffResultMessage, WorktreeFileDiff } from "../src/types/messages" + +interface ApplyState { + status: AgentManagerApplyWorktreeDiffResultMessage["status"] + message: string + conflicts: NonNullable +} + +interface ApplyToLocalOptions { + vscode: ReturnType + dialog: ReturnType + t: ReturnType["t"] + /** Current sidebar selection (LOCAL, a worktree id, or null). */ + selection: Accessor + /** Sentinel id for the local repo selection. */ + local: string + worktrees: Accessor<{ id: string }[]> + diffDatas: Accessor> + diffLoading: Accessor + resolveWorktreeSessionId: (worktreeId: string) => string | undefined + /** Telemetry: metrics.track(name, surface, data). */ + track: ReturnType["track"] +} + +export function createApplyToLocal(opts: ApplyToLocalOptions) { + const { vscode, dialog, t, selection, local, worktrees, diffDatas, diffLoading } = opts + + const [applyStates, setApplyStates] = createSignal>({}) + const [applyTarget, setApplyTarget] = createSignal() + const [applySelectedFiles, setApplySelectedFiles] = createSignal([]) + const [applySelectionTouched, setApplySelectionTouched] = createSignal(false) + + const applyStateForSelection = createMemo(() => { + const sel = selection() + if (!sel || sel === local) return undefined + return applyStates()[sel] + }) + + const applyBusyForSelection = createMemo(() => { + const state = applyStateForSelection() + if (!state) return false + return state.status === "checking" || state.status === "applying" + }) + + const applyTargetSessionId = createMemo(() => { + const target = applyTarget() + if (!target) return undefined + return opts.resolveWorktreeSessionId(target) + }) + + const applyDiffs = createMemo(() => { + const target = applyTarget() + if (!target) return [] as WorktreeFileDiff[] + const data = diffDatas() + const current = applyTargetSessionId() + if (current && data[current]) return data[current]! + return [] as WorktreeFileDiff[] + }) + + const applyStateForTarget = createMemo(() => { + const target = applyTarget() + if (!target) return undefined + return applyStates()[target] + }) + + const applyBusyForTarget = createMemo(() => { + const state = applyStateForTarget() + if (!state) return false + return state.status === "checking" || state.status === "applying" + }) + + const applySelectedSet = createMemo(() => new Set(applySelectedFiles())) + + const applySelectionStats = createMemo(() => { + const set = applySelectedSet() + const selected = applyDiffs().filter((diff) => set.has(diff.file)) + const additions = selected.reduce((sum, diff) => sum + diff.additions, 0) + const deletions = selected.reduce((sum, diff) => sum + diff.deletions, 0) + return { + total: applyDiffs().length, + selected: selected.length, + additions, + deletions, + } + }) + + const applyHasSelection = createMemo(() => applySelectionStats().selected > 0) + + const applyConflictRows = createMemo(() => groupApplyConflicts(applyStateForTarget()?.conflicts ?? [])) + + const applyToLocal = (worktreeId: string, selectedFiles: string[]) => { + setApplyStates((prev) => ({ + ...prev, + [worktreeId]: { + status: "checking", + message: t("agentManager.apply.checking"), + conflicts: [], + }, + })) + vscode.postMessage({ type: "agentManager.applyWorktreeDiff", worktreeId, selectedFiles }) + } + + const resetApplyDialog = () => { + setApplyTarget(undefined) + setApplySelectedFiles([]) + setApplySelectionTouched(false) + } + + const closeApplyDialog = () => { + resetApplyDialog() + dialog.close() + } + + const applySelectAll = () => { + setApplySelectionTouched(true) + setApplySelectedFiles(applyDiffs().map((diff) => diff.file)) + } + + const applySelectNone = () => { + setApplySelectionTouched(true) + setApplySelectedFiles([]) + } + + const applyToggleFile = (file: string, checked: boolean) => { + setApplySelectionTouched(true) + setApplySelectedFiles((prev) => { + if (checked) { + if (prev.includes(file)) return prev + const set = new Set(prev) + set.add(file) + return applyDiffs() + .map((diff) => diff.file) + .filter((path) => set.has(path)) + } + if (!prev.includes(file)) return prev + return prev.filter((path) => path !== file) + }) + } + + const triggerApply = () => { + const target = applyTarget() + if (!target) return + if (!applyHasSelection()) return + if (applyBusyForTarget()) return + opts.track("apply_to_local", "apply_dialog", { fileCount: applySelectedFiles().length }) + applyToLocal(target, applySelectedFiles()) + } + + const openApplyDialog = () => { + const sel = selection() + if (!sel || sel === local) return + setApplyStates((prev) => { + if (!prev[sel]) return prev + const next = { ...prev } + delete next[sel] + return next + }) + setApplyTarget(sel) + setApplySelectionTouched(false) + setApplySelectedFiles([]) + const sid = opts.resolveWorktreeSessionId(sel) + if (sid) vscode.postMessage({ type: "agentManager.requestWorktreeDiff", sessionId: sid }) + + setApplySelectedFiles(applyDiffs().map((diff) => diff.file)) + + dialog.show( + () => ( + + ), + resetApplyDialog, + ) + } + + // Keep the dialog selection in step with the diff set: select everything on + // first load, then drop files that disappear from the diff without clobbering + // a selection the user has already made. + createEffect( + on( + () => [applyTarget(), applyDiffs(), applySelectionTouched()] as const, + ([target, diffs, touched]) => { + if (!target) return + const files = diffs.map((diff) => diff.file) + if (files.length === 0) { + if (!touched) setApplySelectedFiles([]) + return + } + + if (!touched) { + setApplySelectedFiles(files) + return + } + + const current = applySelectedFiles() + const set = new Set(current) + const next = files.filter((file) => set.has(file)) + const same = next.length === current.length && next.every((file, index) => file === current[index]) + if (!same) setApplySelectedFiles(next) + }, + ), + ) + + // Drop apply state for worktrees that no longer exist, and close a dialog + // whose target disappeared. + createEffect(() => { + const ids = new Set(worktrees().map((wt) => wt.id)) + setApplyStates((prev) => { + const next = Object.fromEntries(Object.entries(prev).filter(([id]) => ids.has(id))) + if (Object.keys(next).length === Object.keys(prev).length) return prev + return next + }) + + const target = applyTarget() + if (target && !ids.has(target)) closeApplyDialog() + }) + + // Backend `applyWorktreeDiffResult` message: record the new status and toast. + const onApplyResult = (ev: AgentManagerApplyWorktreeDiffResultMessage) => { + const files = new Set((ev.conflicts ?? []).map((entry) => entry.file).filter(Boolean)).size + const count = ev.conflicts?.length ?? 0 + setApplyStates((prev) => ({ + ...prev, + [ev.worktreeId]: { + status: ev.status, + message: ev.message, + conflicts: ev.conflicts ?? [], + }, + })) + + if (ev.status === "success") { + showToast({ variant: "success", title: t("agentManager.apply.success"), description: ev.message }) + if (applyTarget() === ev.worktreeId) closeApplyDialog() + } + if (ev.status === "conflict") { + const summary = + count > 0 ? t("agentManager.apply.conflictToast", { count, files: Math.max(files, 1) }) : ev.message + showToast({ variant: "error", title: t("agentManager.apply.conflict"), description: summary }) + } + if (ev.status === "error") { + showToast({ variant: "error", title: t("agentManager.apply.error"), description: ev.message }) + } + } + + return { + applyBusyForSelection, + openApplyDialog, + onApplyResult, + } +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts index 79d4b74c51..8d46b3932c 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts @@ -30,6 +30,7 @@ export const dict = { "agentManager.section.moveUp": "تحريك لأعلى", "agentManager.section.moveDown": "تحريك لأسفل", "agentManager.hoverCard.branch": "الفرع", + "agentManager.hoverCard.worktree": "Worktree", "agentManager.hoverCard.base": "الأساس", "agentManager.hoverCard.sessions": "الجلسات", "agentManager.hoverCard.files": "الملفات", @@ -58,7 +59,7 @@ export const dict = { "agentManager.terminal.new": "علامة تبويب جديدة للمحطة الطرفية", "agentManager.terminal.ended": "انتهت المحطة الطرفية — أغلق علامة التبويب للإخفاء", "agentManager.terminal.connectionError": "خطأ في اتصال المحطة الطرفية", - "agentManager.terminal.kill": "إنهاء المحطة الطرفية", + "agentManager.terminal.add": "محطة طرفية جديدة", "agentManager.terminal.empty": "لا توجد محطة طرفية هنا بعد", "agentManager.terminal.start": "بدء المحطة الطرفية", "agentManager.terminal.destination": "اختر ما الذي يفتحه زر المحطة الطرفية", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts index 67cdb9bcf3..25b1644060 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts @@ -30,6 +30,7 @@ export const dict = { "agentManager.section.moveUp": "Mover para Cima", "agentManager.section.moveDown": "Mover para Baixo", "agentManager.hoverCard.branch": "BRANCH", + "agentManager.hoverCard.worktree": "Worktree", "agentManager.hoverCard.base": "Base", "agentManager.hoverCard.sessions": "Sessões", "agentManager.hoverCard.files": "Arquivos", @@ -59,7 +60,7 @@ export const dict = { "agentManager.terminal.new": "Nova aba de terminal", "agentManager.terminal.ended": "terminal encerrado — feche a aba para dispensar", "agentManager.terminal.connectionError": "erro de conexão do terminal", - "agentManager.terminal.kill": "Encerrar terminal", + "agentManager.terminal.add": "Novo terminal", "agentManager.terminal.empty": "Ainda não há terminal aqui", "agentManager.terminal.start": "Iniciar terminal", "agentManager.terminal.destination": "Escolha o que o botão do terminal abre", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts index f6fddfd612..52a5f1e690 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts @@ -30,6 +30,7 @@ export const dict = { "agentManager.section.moveUp": "Pomjeri gore", "agentManager.section.moveDown": "Pomjeri dolje", "agentManager.hoverCard.branch": "BRANCH", + "agentManager.hoverCard.worktree": "Worktree", "agentManager.hoverCard.base": "Baza", "agentManager.hoverCard.sessions": "Sesije", "agentManager.hoverCard.files": "Datoteke", @@ -58,7 +59,7 @@ export const dict = { "agentManager.terminal.new": "Nova kartica terminala", "agentManager.terminal.ended": "terminal je završen — zatvorite karticu da biste odbacili", "agentManager.terminal.connectionError": "greška u vezi terminala", - "agentManager.terminal.kill": "Prekini terminal", + "agentManager.terminal.add": "Novi terminal", "agentManager.terminal.empty": "Ovdje još nema terminala", "agentManager.terminal.start": "Pokreni terminal", "agentManager.terminal.destination": "Odaberite šta otvara dugme terminala", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts index beb827b1c9..1e0894f828 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts @@ -30,6 +30,7 @@ export const dict = { "agentManager.section.moveUp": "Flyt op", "agentManager.section.moveDown": "Flyt ned", "agentManager.hoverCard.branch": "BRANCH", + "agentManager.hoverCard.worktree": "Worktree", "agentManager.hoverCard.base": "Base", "agentManager.hoverCard.sessions": "Sessioner", "agentManager.hoverCard.files": "Filer", @@ -60,7 +61,7 @@ export const dict = { "agentManager.terminal.new": "Ny terminalfane", "agentManager.terminal.ended": "terminal afsluttet — luk fanen for at fjerne", "agentManager.terminal.connectionError": "forbindelsesfejl til terminal", - "agentManager.terminal.kill": "Afslut terminal", + "agentManager.terminal.add": "Ny terminal", "agentManager.terminal.empty": "Ingen terminal her endnu", "agentManager.terminal.start": "Start terminal", "agentManager.terminal.destination": "Vælg, hvad terminalknappen åbner", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts index b9691ecbaf..4290c9c299 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts @@ -30,6 +30,7 @@ export const dict = { "agentManager.section.moveUp": "Nach oben verschieben", "agentManager.section.moveDown": "Nach unten verschieben", "agentManager.hoverCard.branch": "BRANCH", + "agentManager.hoverCard.worktree": "Worktree", "agentManager.hoverCard.base": "Basis", "agentManager.hoverCard.sessions": "Sitzungen", "agentManager.hoverCard.files": "Dateien", @@ -59,7 +60,7 @@ export const dict = { "agentManager.terminal.new": "Neuer Terminal-Tab", "agentManager.terminal.ended": "Terminal beendet — Tab schließen zum Verwerfen", "agentManager.terminal.connectionError": "Verbindungsfehler im Terminal", - "agentManager.terminal.kill": "Terminal beenden", + "agentManager.terminal.add": "Neues Terminal", "agentManager.terminal.empty": "Hier ist noch kein Terminal", "agentManager.terminal.start": "Terminal starten", "agentManager.terminal.destination": "Auswählen, was die Terminal-Schaltfläche öffnet", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts index 236f18423a..2b1700a395 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts @@ -32,6 +32,7 @@ export const dict = { "agentManager.section.moveDown": "Move Down", "agentManager.hoverCard.branch": "BRANCH", + "agentManager.hoverCard.worktree": "Worktree", "agentManager.hoverCard.base": "Base", "agentManager.hoverCard.sessions": "Sessions", "agentManager.hoverCard.files": "Files", @@ -60,9 +61,9 @@ export const dict = { "agentManager.sidebarSearch.contexts": "LOCAL & WORKTREES", "agentManager.terminal.new": "New Terminal Tab", + "agentManager.terminal.add": "New terminal", "agentManager.terminal.ended": "terminal ended — close tab to dismiss", "agentManager.terminal.connectionError": "terminal connection error", - "agentManager.terminal.kill": "Kill terminal", "agentManager.terminal.empty": "No terminal here yet", "agentManager.terminal.start": "Start terminal", "agentManager.terminal.destination": "Choose what the terminal button opens", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts index a75de2ce0c..8889c83770 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts @@ -30,6 +30,7 @@ export const dict = { "agentManager.section.moveUp": "Mover hacia arriba", "agentManager.section.moveDown": "Mover hacia abajo", "agentManager.hoverCard.branch": "BRANCH", + "agentManager.hoverCard.worktree": "Worktree", "agentManager.hoverCard.base": "Base", "agentManager.hoverCard.sessions": "Sesiones", "agentManager.hoverCard.files": "Archivos", @@ -59,7 +60,7 @@ export const dict = { "agentManager.terminal.new": "Nueva pestaña de terminal", "agentManager.terminal.ended": "terminal finalizado — cierra la pestaña para descartar", "agentManager.terminal.connectionError": "error de conexión del terminal", - "agentManager.terminal.kill": "Terminar terminal", + "agentManager.terminal.add": "Nuevo terminal", "agentManager.terminal.empty": "Aún no hay ningún terminal aquí", "agentManager.terminal.start": "Iniciar terminal", "agentManager.terminal.destination": "Elegir qué abre el botón del terminal", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fa.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fa.ts new file mode 100644 index 0000000000..0f7db226a4 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fa.ts @@ -0,0 +1,207 @@ +export const dict = { + "agentManager.local": "محلی", + "agentManager.sidebar.collapse": "بستن نوار کناری", + "agentManager.sidebar.expand": "نمایش نوار کناری", + "agentManager.section.worktrees": "WORKTREES", + "agentManager.section.sessions": "SESSIONS", + "agentManager.notGitRepo": "این یک مخزن git نیست", + + "agentManager.worktree.settings": "تنظیمات Worktree", + "agentManager.worktree.new": "Worktree جدید", + "agentManager.worktree.setupScript": "اسکریپت راه‌اندازی Worktree", + "agentManager.worktree.delete": "حذف Worktree", + "agentManager.worktree.confirmDelete": "حذف شود؟", + "agentManager.worktree.stale": "منسوخ", + "agentManager.worktree.staleTooltip": "روی دیسک وجود ندارد یا دیگر توسط git worktree ردیابی نمی‌شود", + "agentManager.worktree.removeStale": "حذف worktree منسوخ", + "agentManager.worktree.doubleClickRename": "برای تغییر نام دوبار کلیک کنید", + "agentManager.worktree.versions": "{{count}} نسخه", + "agentManager.worktree.advancedOptions": "گزینه‌های پیشرفته worktree", + "agentManager.worktree.defaultBaseBranch": "شاخه پایه پیش‌فرض", + "agentManager.worktree.defaultBaseBranchAuto": "تشخیص خودکار", + "agentManager.worktree.copyPath": "کپی مسیر", + "agentManager.worktree.openInVscode": "باز کردن در VS Code", + "agentManager.worktree.rename": "تغییر نام", + "agentManager.worktree.newSection": "بخش جدید", + "agentManager.worktree.ungrouped": "بدون گروه", + "agentManager.section.rename": "تغییر نام بخش", + "agentManager.section.setColor": "تنظیم رنگ", + "agentManager.section.delete": "حذف بخش", + "agentManager.section.defaultName": "بخش جدید", + "agentManager.section.moveUp": "انتقال به بالا", + "agentManager.section.moveDown": "انتقال به پایین", + + "agentManager.hoverCard.branch": "شاخه", + "agentManager.hoverCard.worktree": "Worktree", + "agentManager.hoverCard.base": "پایه", + "agentManager.hoverCard.sessions": "جلسات", + "agentManager.hoverCard.files": "فایل‌ها", + "agentManager.hoverCard.changes": "تغییرات", + "agentManager.hoverCard.commits": "کامیت‌ها", + + "agentManager.session.new": "جلسه جدید", + "agentManager.session.untitled": "بدون عنوان", + "agentManager.session.newSession": "جلسه جدید", + "agentManager.session.openInWorktree": "باز کردن در worktree", + "agentManager.session.openLocally": "باز کردن به صورت محلی", + "agentManager.session.readonly": "جلسه فقط‌خواندنی", + "agentManager.session.noSessions": "هیچ جلسه‌ای باز نیست", + + "agentManager.tab.close": "بستن", + "agentManager.tab.closeOthers": "بستن بقیه", + "agentManager.tab.closeTab": "بستن برگه", + "agentManager.tab.forkSession": "انشعاب جلسه", + "agentManager.tab.terminal": "ترمینال", + "agentManager.tab.openTerminal": "باز کردن ترمینال", + "agentManager.tab.newOptions": "گزینه‌های بیشتر برای تب جدید", + "agentManager.tabsMenu.status.waiting": "انتظار", + "agentManager.tabsMenu.status.retry": "تلاش مجدد", + "agentManager.sidebarSearch.label": "جستجوی worktree‌ها و جلسات", + "agentManager.sidebarSearch.scope": "جستجو در فضای کاری محلی، جلسات محلی، worktree‌ها و جلسات آن‌ها", + "agentManager.sidebarSearch.contexts": "محلی و WORKTREE‌ها", + + "agentManager.terminal.new": "تب ترمینال جدید", + "agentManager.terminal.add": "ترمینال جدید", + "agentManager.terminal.ended": "ترمینال پایان یافت — برای بستن، تب را ببندید", + "agentManager.terminal.connectionError": "خطای اتصال ترمینال", + "agentManager.terminal.empty": "هنوز ترمینالی اینجا وجود ندارد", + "agentManager.terminal.start": "شروع ترمینال", + "agentManager.terminal.destination": "انتخاب کنید دکمه ترمینال چه چیزی را باز کند", + "agentManager.terminal.openInVscode": "ترمینال VS Code", + "agentManager.terminal.openInPanel": "پنل Agent Manager", + "agentManager.terminal.errorTitle": "خطای ترمینال", + + "agentManager.setup.failed": "راه‌اندازی Worktree ناموفق بود", + "agentManager.setup.settingUp": "در حال راه‌اندازی Worktree", + "agentManager.setup.error.git_not_found": + "Git نصب نشده یا در PATH یافت نشد. لطفاً Git را نصب کرده و VS Code را مجدداً راه‌اندازی کنید.", + "agentManager.setup.error.not_git_repo": "برای استفاده از Worktree، پوشه‌ای که حاوی یک مخزن git است را باز کنید.", + "agentManager.setup.error.lfs_missing": + "این مخزن از Git LFS استفاده می‌کند، اما git-lfs یافت نشد. لطفاً Git LFS را نصب کنید.", + "agentManager.setup.error.no_commits": + "این مخزن هنوز هیچ کامیتی ندارد. قبل از استفاده از Worktree، یک کامیت اولیه ایجاد کنید.", + "agentManager.shortcuts.title": "میانبرهای صفحه‌کلید", + "agentManager.shortcuts.category.sidebar": "نوار کناری", + "agentManager.shortcuts.category.tabs": "تب‌ها", + "agentManager.shortcuts.category.terminal": "ترمینال", + "agentManager.shortcuts.category.global": "سراسری", + "agentManager.shortcuts.previousItem": "مورد قبلی", + "agentManager.shortcuts.nextItem": "مورد بعدی", + "agentManager.shortcuts.newWorktree": "Worktree جدید", + "agentManager.shortcuts.advancedWorktree": "پیکربندی Worktree جدید", + "agentManager.shortcuts.openWorktree": "باز کردن Worktree", + "agentManager.shortcuts.openPR": "باز کردن pull request", + "agentManager.shortcuts.deleteWorktree": "حذف Worktree", + "agentManager.shortcuts.previousTab": "تب قبلی", + "agentManager.shortcuts.nextTab": "تب بعدی", + "agentManager.shortcuts.newTab": "تب جدید", + "agentManager.shortcuts.closeTab": "بستن تب", + "agentManager.shortcuts.toggleTerminal": "نمایش/پنهان کردن ترمینال", + "agentManager.shortcuts.runScript": "اجرای اسکریپت", + "agentManager.run.options": "گزینه‌های اجرا", + "agentManager.run.configure": "پیکربندی اسکریپت اجرا", + "agentManager.shortcuts.openAgentManager": "باز کردن Agent Manager", + "agentManager.shortcuts.cycleAgentMode": "حالت عامل بعدی", + "agentManager.shortcuts.cyclePreviousAgentMode": "حالت عامل قبلی", + "agentManager.shortcuts.showShortcuts": "نمایش میانبرهای صفحه‌کلید", + + "agentManager.dialog.removeStaleWorktree.title": "حذف Worktree قدیمی", + "agentManager.dialog.removeStaleWorktree.messagePre": "حذف Worktree قدیمی ", + "agentManager.dialog.removeStaleWorktree.messagePost": + "؟ این عملیات فقط نگاشت Agent Manager را حذف می‌کند و فایل‌های روی دیسک دست‌نخورده باقی می‌مانند.", + "agentManager.dialog.removeStaleWorktree.cancel": "لغو", + "agentManager.dialog.removeStaleWorktree.confirm": "حذف Worktree قدیمی", + + "agentManager.dialog.openWorktree": "Worktree جدید", + "agentManager.dialog.tab.new": "جدید", + "agentManager.dialog.tab.import": "وارد کردن", + "agentManager.dialog.namePlaceholder": "نام Worktree (اختیاری)", + "agentManager.dialog.promptPlaceholder.mac": "پیامی بنویسید (⌘Enter برای ارسال)", + "agentManager.dialog.promptPlaceholder.other": "پیامی بنویسید (Ctrl+Enter برای ارسال)", + "agentManager.dialog.advancedOptions": "گزینه‌های پیشرفته", + "agentManager.dialog.branchName": "نام شاخه", + "agentManager.dialog.branchNamePlaceholder": "تولید خودکار", + "agentManager.dialog.baseBranch": "شاخه پایه", + "agentManager.dialog.searchBranches": "جستجوی شاخه‌ها...", + "agentManager.dialog.branchBadge.default": "پیش‌فرض", + "agentManager.dialog.branchBadge.remote": "راه دور", + "agentManager.dialog.versions": "نسخه‌ها", + "agentManager.dialog.versionHint": "{{count}} worktree به صورت موازی اجرا خواهند شد", + "agentManager.dialog.compareModels": "مقایسه مدل‌ها", + "agentManager.dialog.compareModels.tooltip": "اجرای عامل‌ها روی مدل‌های مختلف به صورت موازی برای مقایسه نتایج", + "agentManager.dialog.compareModels.searchModels": "جستجوی مدل‌ها...", + "agentManager.dialog.compareModels.selectModels": "انتخاب مدل‌ها...", + "agentManager.dialog.compareModels.effort": "میزان استدلال", + "agentManager.dialog.compareModels.effortDefault": "پیش‌فرض", + "agentManager.dialog.creating": "در حال ایجاد...", + "agentManager.dialog.createWorktree": "ایجاد Worktree", + "agentManager.dialog.removeImage": "حذف تصویر", + "agentManager.dialog.configureWorktree": "پیکربندی Worktree جدید...", + + "agentManager.diff.toggle": "تغییر وضعیت diff", + "agentManager.diff.openFile": "باز کردن فایل", + "agentManager.diff.revertFile": "بازگردانی فایل", + "agentManager.diff.revertSuccess": "فایل بازگردانی شد", + "agentManager.diff.revertError": "بازگردانی ناموفق بود", + "agentManager.open.button": "باز کردن", + "agentManager.open.tooltip": "باز کردن این Worktree در VS Code", + "agentManager.apply.globalButton": "اعمال", + "agentManager.apply.tooltip": "اعمال تغییرات worktree انتخاب‌شده به شاخه محلی", + "agentManager.apply.dialogTitle": "اعمال تغییرات به شاخه محلی", + "agentManager.apply.confirm": "همین الان اعمال کن", + "agentManager.apply.selectionSummary": "{{selected}} از {{total}} فایل انتخاب‌شده", + "agentManager.apply.selectAll": "انتخاب همه", + "agentManager.apply.selectNone": "هیچ‌کدام را انتخاب نکن", + "agentManager.apply.checking": "در حال بررسی...", + "agentManager.apply.applying": "در حال اعمال...", + "agentManager.apply.success": "تغییرات اعمال شد", + "agentManager.apply.conflict": "اعمال دارای تعارض است", + "agentManager.apply.conflictToast": "{{count}} تعارض در {{files}} فایل. جزئیات را در Apply بررسی کنید.", + "agentManager.apply.error": "اعمال ناموفق بود", + "agentManager.apply.conflictsTitle": "تعارض‌ها", + "agentManager.apply.unknownFile": "فایل نامشخص", + "agentManager.apply.reason.index": "فایل محلی با نسخه مورد انتظار تفاوت دارد", + "agentManager.apply.reason.patch": "پچ به‌درستی قابل اعمال نیست", + "agentManager.apply.reason.contents": "خواندن محتوای فایل محلی فعلی ممکن نیست", + "agentManager.apply.reason.unknown": "تعارض شناسایی شد", + "agentManager.shortcuts.toggleDiff": "نمایش/پنهان کردن پنل diff", + "agentManager.shortcuts.category.quickSwitch": "تغییر سریع", + "agentManager.shortcuts.jumpToItem": "پرش به مورد ۱–۹", + "agentManager.review.sendAllToChat": "ارسال همه به چت", + "agentManager.review.sendAllToChatWithCount": "ارسال همه به چت ({{count}})", + "agentManager.review.sendAllShortcut.mac": "⌘Enter", + "agentManager.review.sendAllShortcut.other": "Ctrl+Enter", + "agentManager.review.modalTitle": "بررسی نظر", + "agentManager.review.inlineCount": "نظرات بررسی ({{count}})", + "agentManager.review.clearAll": "پاک کردن همه", + "agentManager.review.commentOnLine": "نظر در خط {{line}}", + "agentManager.review.editCommentOnLine": "ویرایش نظر در خط {{line}}", + "agentManager.review.commentPlaceholder": "یک نظر بگذارید...", + "agentManager.review.commentAction": "نظر", + "agentManager.review.sendToChat": "ارسال به چت", + "agentManager.review.metaFile": "فایل", + "agentManager.review.metaLine": "خط", + "agentManager.review.metaComment": "نظر کاربر", + "agentManager.review.collapsedOnly": "{{count}} جمع‌شده", + "agentManager.review.collapsedWithLarge": "{{collapsed}} جمع‌شده، {{large}} بزرگ", + "agentManager.review.largeFileCollapsed": "فایل بزرگ (جمع‌شده)", + "agentManager.review.image": "تصویر", + "agentManager.review.imageBefore": "قبل", + "agentManager.review.imageAfter": "بعد", + "agentManager.review.imageTooLarge": "تصویر برای پیش‌نمایش خیلی بزرگ است ({{size}}).", + "agentManager.review.imageUnreadable": "این تصویر قابل نمایش نیست.", + "agentManager.review.imageUnavailable": "پیش‌نمایش تصویر برای این نمونه جلسه در دسترس نیست.", + "agentManager.review.endOfLongDiff": "به انتها رسیدید!", + + "agentManager.import.pullRequest": "Pull Request", + "agentManager.import.pastePrUrl": "URL درخواست PR را وارد کنید...", + "agentManager.import.open": "باز کردن", + "agentManager.import.branches": "شاخه‌ها", + "agentManager.import.selectBranch": "انتخاب شاخه...", + "agentManager.import.loading": "در حال بارگذاری...", + "agentManager.import.loadingBranches": "در حال بارگذاری شاخه‌ها...", + "agentManager.import.noMatchingBranches": "شاخه‌ای مطابق یافت نشد", + "agentManager.import.noBranchesFound": "هیچ شاخه‌ای یافت نشد.", + "agentManager.import.noBranchesHint": "یک URL درخواست ادغام را در بالا جای‌گذاری کنید یا یک worktree جدید بسازید.", + "agentManager.import.failed": "وارد کردن ناموفق بود", +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts index d8579c9cfd..2f96cdb0e6 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts @@ -30,6 +30,7 @@ export const dict = { "agentManager.section.moveUp": "Déplacer vers le haut", "agentManager.section.moveDown": "Déplacer vers le bas", "agentManager.hoverCard.branch": "BRANCHE", + "agentManager.hoverCard.worktree": "Worktree", "agentManager.hoverCard.base": "Base", "agentManager.hoverCard.sessions": "Sessions", "agentManager.hoverCard.files": "Fichiers", @@ -59,7 +60,7 @@ export const dict = { "agentManager.terminal.new": "Nouvel onglet de terminal", "agentManager.terminal.ended": "terminal terminé — fermez l'onglet pour ignorer", "agentManager.terminal.connectionError": "erreur de connexion du terminal", - "agentManager.terminal.kill": "Tuer le terminal", + "agentManager.terminal.add": "Nouveau terminal", "agentManager.terminal.empty": "Aucun terminal ici pour l'instant", "agentManager.terminal.start": "Démarrer le terminal", "agentManager.terminal.destination": "Choisir ce que le bouton Terminal ouvre", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts index 1192807424..0bf5fbbb68 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts @@ -32,6 +32,7 @@ export const dict = { "agentManager.section.moveDown": "Sposta giù", "agentManager.hoverCard.branch": "BRANCH", + "agentManager.hoverCard.worktree": "Worktree", "agentManager.hoverCard.base": "Base", "agentManager.hoverCard.sessions": "Sessioni", "agentManager.hoverCard.files": "File", @@ -63,7 +64,7 @@ export const dict = { "agentManager.terminal.new": "Nuova scheda terminale", "agentManager.terminal.ended": "terminale terminato - chiudi la scheda per nasconderlo", "agentManager.terminal.connectionError": "errore di connessione del terminale", - "agentManager.terminal.kill": "Termina terminale", + "agentManager.terminal.add": "Nuovo terminale", "agentManager.terminal.empty": "Qui non c'è ancora un terminale", "agentManager.terminal.start": "Avvia terminale", "agentManager.terminal.destination": "Scegli cosa apre il pulsante del terminale", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts index d855af8e15..b8fc3757dd 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts @@ -30,6 +30,7 @@ export const dict = { "agentManager.section.moveUp": "上に移動", "agentManager.section.moveDown": "下に移動", "agentManager.hoverCard.branch": "ブランチ", + "agentManager.hoverCard.worktree": "Worktree", "agentManager.hoverCard.base": "ベース", "agentManager.hoverCard.sessions": "セッション", "agentManager.hoverCard.files": "ファイル", @@ -59,7 +60,7 @@ export const dict = { "agentManager.terminal.new": "新しいターミナルタブ", "agentManager.terminal.ended": "ターミナルが終了しました — タブを閉じて破棄", "agentManager.terminal.connectionError": "ターミナル接続エラー", - "agentManager.terminal.kill": "ターミナルを終了", + "agentManager.terminal.add": "新しいターミナル", "agentManager.terminal.empty": "ここにはまだターミナルがありません", "agentManager.terminal.start": "ターミナルを開始", "agentManager.terminal.destination": "ターミナルボタンで開く場所を選択", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts index ae87aaf993..5b2cc3574b 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts @@ -30,6 +30,7 @@ export const dict = { "agentManager.section.moveUp": "위로 이동", "agentManager.section.moveDown": "아래로 이동", "agentManager.hoverCard.branch": "브랜치", + "agentManager.hoverCard.worktree": "Worktree", "agentManager.hoverCard.base": "베이스", "agentManager.hoverCard.sessions": "세션", "agentManager.hoverCard.files": "파일", @@ -58,7 +59,7 @@ export const dict = { "agentManager.terminal.new": "새 터미널 탭", "agentManager.terminal.ended": "터미널 종료됨 — 탭을 닫아 해제", "agentManager.terminal.connectionError": "터미널 연결 오류", - "agentManager.terminal.kill": "터미널 종료", + "agentManager.terminal.add": "새 터미널", "agentManager.terminal.empty": "아직 여기에 터미널이 없습니다", "agentManager.terminal.start": "터미널 시작", "agentManager.terminal.destination": "터미널 버튼으로 열 위치 선택", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts index 1965204a47..bb0d4b1bf9 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts @@ -32,6 +32,7 @@ export const dict = { "agentManager.section.moveDown": "Omlaag verplaatsen", "agentManager.hoverCard.branch": "BRANCH", + "agentManager.hoverCard.worktree": "Worktree", "agentManager.hoverCard.base": "Basis", "agentManager.hoverCard.sessions": "Sessies", "agentManager.hoverCard.files": "Bestanden", @@ -62,7 +63,7 @@ export const dict = { "agentManager.terminal.new": "Nieuw terminaltabblad", "agentManager.terminal.ended": "terminal beëindigd — sluit tabblad om te negeren", "agentManager.terminal.connectionError": "terminalverbindingsfout", - "agentManager.terminal.kill": "Terminal beëindigen", + "agentManager.terminal.add": "Nieuwe terminal", "agentManager.terminal.empty": "Hier is nog geen terminal", "agentManager.terminal.start": "Terminal starten", "agentManager.terminal.destination": "Kies wat de terminalknop opent", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts index 44efed03a5..f45875dfcc 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts @@ -30,6 +30,7 @@ export const dict = { "agentManager.section.moveUp": "Flytt opp", "agentManager.section.moveDown": "Flytt ned", "agentManager.hoverCard.branch": "BRANCH", + "agentManager.hoverCard.worktree": "Worktree", "agentManager.hoverCard.base": "Base", "agentManager.hoverCard.sessions": "Økter", "agentManager.hoverCard.files": "Filer", @@ -58,7 +59,7 @@ export const dict = { "agentManager.terminal.new": "Ny terminalfane", "agentManager.terminal.ended": "terminal avsluttet — lukk fanen for å avvise", "agentManager.terminal.connectionError": "tilkoblingsfeil for terminal", - "agentManager.terminal.kill": "Avslutt terminal", + "agentManager.terminal.add": "Ny terminal", "agentManager.terminal.empty": "Ingen terminal her ennå", "agentManager.terminal.start": "Start terminal", "agentManager.terminal.destination": "Velg hva terminalknappen åpner", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts index 137bf34132..38b90e8a62 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts @@ -30,6 +30,7 @@ export const dict = { "agentManager.section.moveUp": "Przenieś w górę", "agentManager.section.moveDown": "Przenieś w dół", "agentManager.hoverCard.branch": "BRANCH", + "agentManager.hoverCard.worktree": "Worktree", "agentManager.hoverCard.base": "Baza", "agentManager.hoverCard.sessions": "Sesje", "agentManager.hoverCard.files": "Pliki", @@ -59,7 +60,7 @@ export const dict = { "agentManager.terminal.new": "Nowa karta terminala", "agentManager.terminal.ended": "terminal zakończony — zamknij kartę, aby zamknąć", "agentManager.terminal.connectionError": "błąd połączenia terminala", - "agentManager.terminal.kill": "Zakończ terminal", + "agentManager.terminal.add": "Nowy terminal", "agentManager.terminal.empty": "Nie ma tu jeszcze terminala", "agentManager.terminal.start": "Uruchom terminal", "agentManager.terminal.destination": "Wybierz, co otwiera przycisk terminala", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts index c0df2ab3c9..cdacacf807 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts @@ -30,6 +30,7 @@ export const dict = { "agentManager.section.moveUp": "Переместить вверх", "agentManager.section.moveDown": "Переместить вниз", "agentManager.hoverCard.branch": "ВЕТКА", + "agentManager.hoverCard.worktree": "Worktree", "agentManager.hoverCard.base": "Основа", "agentManager.hoverCard.sessions": "Сессии", "agentManager.hoverCard.files": "Файлы", @@ -59,7 +60,7 @@ export const dict = { "agentManager.terminal.new": "Новая вкладка терминала", "agentManager.terminal.ended": "терминал завершен — закройте вкладку, чтобы скрыть", "agentManager.terminal.connectionError": "ошибка подключения к терминалу", - "agentManager.terminal.kill": "Завершить терминал", + "agentManager.terminal.add": "Новый терминал", "agentManager.terminal.empty": "Здесь пока нет терминала", "agentManager.terminal.start": "Запустить терминал", "agentManager.terminal.destination": "Выберите, где будет открываться терминал", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts index 012b3776ba..3bf828ec65 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts @@ -30,6 +30,7 @@ export const dict = { "agentManager.section.moveUp": "เลื่อนขึ้น", "agentManager.section.moveDown": "เลื่อนลง", "agentManager.hoverCard.branch": "BRANCH", + "agentManager.hoverCard.worktree": "Worktree", "agentManager.hoverCard.base": "ฐาน", "agentManager.hoverCard.sessions": "เซสชัน", "agentManager.hoverCard.files": "ไฟล์", @@ -58,7 +59,7 @@ export const dict = { "agentManager.terminal.new": "แท็บเทอร์มินัลใหม่", "agentManager.terminal.ended": "เทอร์มินัลสิ้นสุด — ปิดแท็บเพื่อยกเลิก", "agentManager.terminal.connectionError": "ข้อผิดพลาดการเชื่อมต่อเทอร์มินัล", - "agentManager.terminal.kill": "หยุดเทอร์มินัล", + "agentManager.terminal.add": "เทอร์มินัลใหม่", "agentManager.terminal.empty": "ยังไม่มีเทอร์มินัลที่นี่", "agentManager.terminal.start": "เริ่มเทอร์มินัล", "agentManager.terminal.destination": "เลือกว่าปุ่มเทอร์มินัลจะเปิดอะไร", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts index e18f88694b..60da940237 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts @@ -32,6 +32,7 @@ export const dict = { "agentManager.section.moveDown": "Aşağı Taşı", "agentManager.hoverCard.branch": "DAL", + "agentManager.hoverCard.worktree": "Worktree", "agentManager.hoverCard.base": "Temel", "agentManager.hoverCard.sessions": "Oturumlar", "agentManager.hoverCard.files": "Dosyalar", @@ -63,7 +64,7 @@ export const dict = { "agentManager.terminal.new": "Yeni Terminal Sekmesi", "agentManager.terminal.ended": "terminal sona erdi — kapatmak için sekmeyi kapatın", "agentManager.terminal.connectionError": "terminal bağlantı hatası", - "agentManager.terminal.kill": "Terminali sonlandır", + "agentManager.terminal.add": "Yeni terminal", "agentManager.terminal.empty": "Burada henüz terminal yok", "agentManager.terminal.start": "Terminali başlat", "agentManager.terminal.destination": "Terminal düğmesinin ne açacağını seçin", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts index 1c1c1cbf22..6a022b0dc7 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts @@ -32,6 +32,7 @@ export const dict = { "agentManager.section.moveDown": "Перемістити вниз", "agentManager.hoverCard.branch": "ГІЛКА", + "agentManager.hoverCard.worktree": "Worktree", "agentManager.hoverCard.base": "База", "agentManager.hoverCard.sessions": "Сесії", "agentManager.hoverCard.files": "Файли", @@ -63,7 +64,7 @@ export const dict = { "agentManager.terminal.new": "Нова вкладка термінала", "agentManager.terminal.ended": "термінал завершено — закрийте вкладку, щоб відхилити", "agentManager.terminal.connectionError": "помилка з'єднання термінала", - "agentManager.terminal.kill": "Завершити термінал", + "agentManager.terminal.add": "Новий термінал", "agentManager.terminal.empty": "Тут ще немає термінала", "agentManager.terminal.start": "Запустити термінал", "agentManager.terminal.destination": "Виберіть, що відкриватиме кнопка термінала", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts index 2e0c7f5568..59f227c736 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts @@ -30,6 +30,7 @@ export const dict = { "agentManager.section.moveUp": "上移", "agentManager.section.moveDown": "下移", "agentManager.hoverCard.branch": "分支", + "agentManager.hoverCard.worktree": "Worktree", "agentManager.hoverCard.base": "基础", "agentManager.hoverCard.sessions": "会话", "agentManager.hoverCard.files": "文件", @@ -58,7 +59,7 @@ export const dict = { "agentManager.terminal.new": "新建终端标签页", "agentManager.terminal.ended": "终端已结束 — 关闭标签页以消除", "agentManager.terminal.connectionError": "终端连接错误", - "agentManager.terminal.kill": "终止终端", + "agentManager.terminal.add": "新建终端", "agentManager.terminal.empty": "此处尚无终端", "agentManager.terminal.start": "启动终端", "agentManager.terminal.destination": "选择终端按钮的打开目标", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts index e4403c5aaf..b0cb8f6c5e 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts @@ -30,6 +30,7 @@ export const dict = { "agentManager.section.moveUp": "上移", "agentManager.section.moveDown": "下移", "agentManager.hoverCard.branch": "分支", + "agentManager.hoverCard.worktree": "Worktree", "agentManager.hoverCard.base": "基底", "agentManager.hoverCard.sessions": "工作階段", "agentManager.hoverCard.files": "檔案", @@ -58,7 +59,7 @@ export const dict = { "agentManager.terminal.new": "新增終端分頁", "agentManager.terminal.ended": "終端已結束 — 關閉分頁以消除", "agentManager.terminal.connectionError": "終端連線錯誤", - "agentManager.terminal.kill": "終止終端機", + "agentManager.terminal.add": "新增終端機", "agentManager.terminal.empty": "此處尚無終端機", "agentManager.terminal.start": "啟動終端機", "agentManager.terminal.destination": "選擇終端機按鈕的開啟目標", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/terminal/SideTerminalPanel.tsx b/packages/kilo-vscode/webview-ui/agent-manager/terminal/SideTerminalPanel.tsx index bf42185b83..517965d88d 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/terminal/SideTerminalPanel.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/terminal/SideTerminalPanel.tsx @@ -3,22 +3,33 @@ * * Lives inside the shared `.am-diff-panel-wrapper` host next to the diff * and PR panels, so all three inspector modes share one resize handle - * and one width. The header intentionally reuses the `.am-diff-header` - * structure and metrics so switching modes does not shift the chrome. + * and one width. + * + * A context can own several side terminals. The header is a tab strip + * that reuses the top tab bar's `TerminalTabChrome` (same `am-tab*` + * structure, same X close button) plus a `+` action to add terminals. + * Tabs are drag-sortable via the same `@thisbeyond/solid-dnd` stack as + * the top tab bar; the order lives in the terminal state, so it is + * preserved across sidebar context switches for the webview's lifetime. + * The strip stays visible even when empty so the `+` action is always + * reachable. * * Visibility is opacity-based, never unmount: the xterm render loop * dies when its subtree leaves the paint tree (see `render.tsx`). */ import type { Accessor, Component } from "solid-js" -import { Show, createEffect } from "solid-js" -import { Icon } from "@kilocode/kilo-ui/icon" +import { For, Show, createEffect, createSignal } from "solid-js" +import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd" +import type { DragEvent } from "@thisbeyond/solid-dnd" import { IconButton } from "@kilocode/kilo-ui/icon-button" import { Button } from "@kilocode/kilo-ui/button" import { Spinner } from "@kilocode/kilo-ui/spinner" import { Tooltip } from "@kilocode/kilo-ui/tooltip" import { useLanguage } from "../../src/context/language" +import { ConstrainDragYAxis, SortableTabContainer } from "../../src/components/chat/TabDnd" import { renderSideTerminalLayer } from "./render" +import { TerminalTabChrome } from "./SortableTerminalTab" import type { TerminalStateControls } from "./state" interface Props { @@ -27,9 +38,11 @@ interface Props { contextKey: Accessor /** True while the inspector is in terminal mode. */ visible: Accessor - /** Kill the terminal (or cancel its create) and hide. */ - onClose: () => void - /** Empty-state action: create a side terminal for this context. */ + /** Make a terminal the visible one in the strip. */ + onSelect: (terminalId: string) => void + /** Kill one terminal. */ + onClose: (terminalId: string) => void + /** Create a new side terminal for this context. */ onStart: () => void } @@ -39,8 +52,26 @@ export const SideTerminalPanel: Component = (props) => { createEffect(() => { panel.inert = !props.visible() }) - const side = () => props.state.side() - const pending = () => props.state.pendingSide(props.contextKey()) !== undefined + const [dragging, setDragging] = createSignal<{ id: string; width: number } | undefined>() + const sides = () => props.state.sidesForContext(props.contextKey()) + const ids = () => sides().map((term) => term.id) + const pending = () => props.state.pendingSide(props.contextKey()) + const onDragStart = (event: DragEvent) => { + const id = event.draggable?.id + if (typeof id !== "string") return + // Pin the overlay to the tab's width: the overlay container uses + // min-width, so a long OSC title would otherwise overflow it and + // shift the visual center off the cursor (the "drag offset" bug). + const width = event.draggable?.layout.width ?? event.draggable?.node.getBoundingClientRect().width + setDragging({ id, width }) + } + const onDragEnd = () => setDragging(undefined) + const onDragOver = (event: DragEvent) => { + const from = event.draggable?.id + const to = event.droppable?.id + if (typeof from !== "string" || typeof to !== "string") return + props.state.reorderSideDrag(props.contextKey(), from, to) + } return (
= (props) => { aria-label={t("agentManager.tab.terminal")} aria-hidden={!props.visible()} > -
-
- - {side()?.title ?? t("agentManager.tab.terminal")} -
-
- +
+ + + + {/* Scrollable tab list — mirrors the top bar's .am-tab-list split + so the "+" action never scrolls away. role="tablist" only + when tabs exist: axe aria-required-children rejects an empty + tablist (and non-tab children like the add button). */} +
0 ? "tablist" : undefined} + aria-label={sides().length > 0 ? t("agentManager.tab.terminal") : undefined} + > + + + {(term) => ( + + props.onSelect(term.id)} + onMiddleClick={(e: MouseEvent) => { + if (e.button !== 1) return + e.preventDefault() + e.stopPropagation() + props.onClose(term.id) + }} + onClose={(e: MouseEvent) => { + e.stopPropagation() + props.onClose(term.id) + }} + /> + + )} + + +
+ {/* Cursor-following clone of the dragged tab (same pattern as + the top tab bar). The overlay is what makes the in-list + original use solid-dnd's slot-compensated transform, so the + dragged tab tracks the cursor without a jump/offset. The + original stays dimmed in its slot via .am-tab-dragging. */} + + + {(tab) => ( +
+ {props.state.title(tab().id) ?? t("agentManager.tab.terminal")} +
+ )} +
+
+
+
+
{renderSideTerminalLayer({ state: props.state, contextKey: props.contextKey, visible: props.visible })} - +
{t("common.loading")}
- +
{t("agentManager.terminal.empty")}
) @@ -126,7 +129,8 @@ export function renderTerminalLayer(props: { state: TerminalStateControls }): JS * terminal stays mounted, visibility is toggled via `opacity` / * `pointer-events` / `inert` only. The layer is scoped to * `contextKey` — side terminals from other contexts stay composed in - * the background and never refit. + * the background and never refit — and within a context only the + * active strip tab's terminal is shown. */ export function renderSideTerminalLayer(props: { state: TerminalStateControls @@ -137,7 +141,10 @@ export function renderSideTerminalLayer(props: {
{(term) => { - const active = () => props.visible() && term.contextKey === props.contextKey() + const active = () => + props.visible() && + term.contextKey === props.contextKey() && + props.state.sideActiveFor(term.contextKey) === term.id return (
props.state.setFocusedId(focused ? term.id : undefined)} + onTitleChange={(title) => props.state.setTitle(term.id, title)} />
) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/terminal/side.ts b/packages/kilo-vscode/webview-ui/agent-manager/terminal/side.ts index 3c7fe39145..d0420b9916 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/terminal/side.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/terminal/side.ts @@ -7,23 +7,64 @@ * embedded terminal behaves like the diff panel: press once to reveal, * press again to hide. Hiding never kills the terminal — only the * explicit close action (or `Cmd+W` while it holds focus) does. + * + * ## Destination state ownership + * + * The VS Code setting is application-scoped, so one value is shared by + * every window and echoed back via `terminal.destinationChanged` + * whenever ANY window rewrites it. Two windows can therefore fight: + * picking "VS Code terminal" in worktree window B would silently flip + * the routing of the panel in window A. To keep each panel consistent, + * an explicit dropdown pick is stored per panel (webview state) and + * wins over remote echoes; the setting only drives panels that never + * picked a destination themselves (it stays the default for new ones). */ import { createSignal } from "solid-js" import type { Accessor } from "solid-js" import type { TerminalDestination } from "../../src/types/messages/agent-manager" +import { LOCAL } from "../navigate" + +/** Read the panel-local destination choice from raw webview state. */ +export function readSavedDestination(state: Record | undefined): TerminalDestination | undefined { + const value = state?.terminalDestination + return value === "agentManager" || value === "vscode" ? value : undefined +} + +export type VscodeTerminalRequest = + | { type: "agentManager.showTerminal"; sessionId: string } + | { type: "agentManager.showWorktreeTerminal"; worktreeId: string } + | { type: "agentManager.showLocalTerminal" } + +/** + * Pick the message the terminal button / Focus Terminal shortcut sends + * when the destination is the VS Code integrated terminal. The fallback + * chain exists so the shortcut never dead-ends: activating a terminal + * tab clears the current session, and a worktree may have no sessions + * at all. Extracted from AgentManagerApp.tsx (max-lines cap). + */ +export function resolveVscodeTerminalRequest( + selection: string | null, + currentSessionID: string | undefined, + sessionForWorktree: (worktreeId: string) => string | undefined, +): VscodeTerminalRequest { + const id = currentSessionID ?? (selection && selection !== LOCAL ? sessionForWorktree(selection) : undefined) + if (id) return { type: "agentManager.showTerminal", sessionId: id } + if (selection && selection !== LOCAL) return { type: "agentManager.showWorktreeTerminal", worktreeId: selection } + return { type: "agentManager.showLocalTerminal" } +} interface Handlers { requestSide(): void - closeSide(): boolean + closeSide(terminalId: string): boolean } export interface SideTerminalDeps { handlers: Handlers /** True while the right-side inspector shows the terminal. */ visible: Accessor - /** True while the side terminal itself holds DOM focus. */ - focused: Accessor + /** Id of the side terminal holding DOM focus, if any. */ + focusedId: Accessor /** Leave terminal mode; the terminal stays alive in the background. */ hide: () => void /** Move focus back to the chat composer. */ @@ -32,10 +73,16 @@ export interface SideTerminalDeps { track: (button: string, surface: string, properties: Record) => void /** Open or focus the VS Code integrated terminal for the active context. */ openVscode: () => void + /** Panel-local choice restored from webview state, if the user ever + * picked one in this panel. */ + saved: TerminalDestination | undefined + /** Persist the panel-local choice so it survives webview reloads. */ + save: (destination: TerminalDestination) => void } export function createSideTerminal(deps: SideTerminalDeps) { - const [destination, setDestination] = createSignal("vscode") + const [local, setLocal] = createSignal(deps.saved) + const [destination, setDestination] = createSignal(deps.saved ?? "vscode") /** * Hiding while the terminal holds focus would strand the cursor on @@ -49,7 +96,7 @@ export function createSideTerminal(deps: SideTerminalDeps) { const toggle = () => { if (deps.visible()) { - const was = deps.focused() + const was = deps.focusedId() !== undefined deps.hide() handoff(was) return @@ -57,12 +104,14 @@ export function createSideTerminal(deps: SideTerminalDeps) { deps.handlers.requestSide() } - /** Kill the current context's side terminal (or cancel its in-flight - * create) and hide the panel. */ + /** Kill the focused side terminal (Cmd/Ctrl+W). The panel stays open + * on the remaining terminals, or on the empty state when this was + * the last one. */ const close = (): boolean => { - const was = deps.focused() - const done = deps.handlers.closeSide() - if (done) handoff(was) + const id = deps.focusedId() + if (!id) return false + const done = deps.handlers.closeSide(id) + if (done) handoff(true) return done } @@ -78,17 +127,32 @@ export function createSideTerminal(deps: SideTerminalDeps) { } /** - * Dropdown pick. Applied locally right away so the button reacts - * without a round trip, then persisted as a VS Code setting; the - * extension echoes it back via `terminal.destinationChanged`. + * Dropdown pick. The choice is panel-local and sticky: it is kept in + * webview state and beats later `terminal.destinationChanged` echoes + * caused by other windows rewriting the shared application-scoped + * setting. The setting is still written so it stays the default for + * panels that never picked a destination (and new panels). * The key is relative to the `kilo-code.new` section, matching every * other `updateSetting` sender. */ const choose = (target: TerminalDestination) => { deps.track("terminal_destination", "tab_toolbar", { destination: target }) + setLocal(target) setDestination(target) + deps.save(target) deps.postMessage({ type: "updateSetting", key: "agentManager.terminalButtonDestination", value: target }) } - return { destination, setDestination, toggle, close, openPreferred, choose } + /** + * Apply a remote default (initial `agentManager.state` payload or a + * live `terminal.destinationChanged` echo). Ignored once the user + * picked a destination in this panel — their choice wins over every + * echo, including ones triggered by this panel's own `choose` write. + */ + const syncDefault = (target: TerminalDestination) => { + if (local()) return + setDestination(target) + } + + return { destination, syncDefault, toggle, close, openPreferred, choose } } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/terminal/state.ts b/packages/kilo-vscode/webview-ui/agent-manager/terminal/state.ts index 3e9fba7af1..4dff179435 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/terminal/state.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/terminal/state.ts @@ -50,11 +50,9 @@ export interface TerminalFocusRequest { } /** A create request for a side terminal that has not been answered yet. - * `cancelled` is set when the user closes the panel while the PTY is - * still starting; the late `created` answer is then closed again. */ + * Multiple creates can be in flight for the same context at once. */ interface SideRequest { contextKey: string - cancelled: boolean } export interface TerminalStateControls { @@ -75,10 +73,14 @@ export interface TerminalStateControls { all: Accessor /** Every side terminal across every context (for the side-panel layer). */ sides: Accessor - /** The side terminal of the current context, if any. */ - side: Accessor - /** The side terminal of an arbitrary context, if any. */ - sideForContext(contextKey: string): TerminalTabStateWithContext | undefined + /** Every side terminal of the given context, in creation order. */ + sidesForContext(contextKey: string): TerminalTabStateWithContext[] + /** Id of the active side terminal for a context. */ + sideActiveFor(contextKey: string): string | undefined + /** Mark a side terminal as the visible one for its context. */ + setSideActive(contextKey: string, terminalId: string): void + /** Id of the side terminal holding DOM focus in the current context, if any. */ + sideFocusedId(): string | undefined /** Context key for the current sidebar selection, or `undefined` when nothing is selected. */ currentKey: Accessor /** Context key for the side panel: like `currentKey` but unassigned @@ -96,6 +98,13 @@ export interface TerminalStateControls { requestFocus(id: string): void /** True when the given remembered tab id points to a live terminal for the given selection. */ hasRemembered(selection: string | null, remembered: string | undefined): boolean + /** Live display title for a terminal: the OSC-provided title when the + * shell/program set one, otherwise the create-time title. */ + title(terminalId: string): string | undefined + /** Record an OSC title change for a terminal. Kept outside the + * terminal records so `` reference stability (and therefore the + * mounted xterm instances) is preserved. */ + setTitle(terminalId: string, title: string): void /** * Persist a new order for a context's terminals (webview-memory only — * terminals are ephemeral and never round-trip through the extension @@ -109,12 +118,16 @@ export interface TerminalStateControls { * was applied, false otherwise so the caller can fall through. */ reorderDrag(from: string, to: string): boolean - /** Request id of the in-flight side-terminal create for a context. */ - pendingSide(contextKey: string): string | undefined + /** + * Apply a drag-over reorder within a context's side terminals (the + * side-panel strip). Returns true when both ends are side terminals + * of that context. + */ + reorderSideDrag(contextKey: string, from: string, to: string): boolean + /** Request ids of the in-flight side-terminal creates for a context. */ + pendingSide(contextKey: string): boolean /** Mark a side-terminal create as in flight for a context. */ beginSide(contextKey: string, createId: string): void - /** Cancel the in-flight create; returns true when one was pending. */ - cancelSide(contextKey: string): boolean /** Settle a create request; returns it so the caller can validate. */ completeSide(createId: string): SideRequest | undefined } @@ -149,10 +162,17 @@ export function createTerminalState(selection: Accessor): Termina const [activeId, setActiveId] = createSignal() const [focusedId, setFocusedId] = createSignal() const [focusRequest, setFocusRequest] = createSignal() + // OSC-provided titles, keyed by terminal id. Separate from the terminal + // records on purpose: replacing a record would remount its xterm via + // reference inequality (see the module comment above). + const [titles, setTitles] = createSignal>({}) + // Active side terminal per context. + const [actives, setActives] = createSignal>({}) let focusSerial = 0 // In-flight side-terminal creates, keyed both ways: per context (what - // the panel shows) and per request id (what the answer carries). - const [pending, setPending] = createSignal>({}) + // the panel shows) and per request id (what the answer carries). A + // context can have several creates in flight at once. + const [pending, setPending] = createSignal>({}) const requests = new Map() const currentKey = (): string | undefined => { @@ -195,8 +215,31 @@ export function createTerminalState(selection: Accessor): Termina return out } - const sideForContext = (key: string) => terminalsByContext()[key]?.find((t) => t.placement === "side") - const side = () => sideForContext(sideKey()) + const sidesForContext = (key: string) => (terminalsByContext()[key] ?? []).filter((t) => t.placement === "side") + const sideActiveFor = (key: string) => actives()[key] + const sideFocusedId = () => { + const id = focusedId() + if (!id) return undefined + return sidesForContext(sideKey()).some((t) => t.id === id) ? id : undefined + } + + const setSideActive = (key: string, terminalId: string) => { + setActives((prev) => (prev[key] === terminalId ? prev : { ...prev, [key]: terminalId })) + } + + const title = (terminalId: string): string | undefined => { + const live = titles()[terminalId] + if (live) return live + const key = contextFor(terminalId) + if (!key) return undefined + return terminalsByContext()[key]?.find((t) => t.id === terminalId)?.title + } + + const setTitle = (terminalId: string, next: string) => { + const trimmed = next.trim() + if (!trimmed) return + setTitles((prev) => (prev[terminalId] === trimmed ? prev : { ...prev, [terminalId]: trimmed })) + } const lookup = () => new Map(current().map((t) => [t.id, t])) @@ -218,9 +261,6 @@ export function createTerminalState(selection: Accessor): Termina setTerminalsByContext((prev) => { const list = prev[key] ?? [] if (list.some((t) => t.id === term.id)) return prev - // One side terminal per context; the message handler dedupes via - // pending requests, this guard covers stale double answers. - if (term.placement === "side" && list.some((t) => t.placement === "side")) return prev const enriched: TerminalTabStateWithContext = { ...term, contextKey: key } return { ...prev, [key]: [...list, enriched] } }) @@ -238,6 +278,24 @@ export function createTerminalState(selection: Accessor): Termina return next }) if (focusedId() === terminalId) setFocusedId(undefined) + // A removed active side terminal hands activation to the last + // remaining one of its context, so the panel never shows a dead slot. + if (removed?.placement === "side" && actives()[key] === terminalId) { + const rest = sidesForContext(key) + setActives((prev) => { + const next = { ...prev } + if (rest.length === 0) delete next[key] + else next[key] = rest[rest.length - 1]!.id + return next + }) + } + if (titles()[terminalId] !== undefined) { + setTitles((prev) => { + const next = { ...prev } + delete next[terminalId] + return next + }) + } return removed } @@ -299,34 +357,58 @@ export function createTerminalState(selection: Accessor): Termina return true } - const pendingSide = (key: string) => pending()[key] - - const beginSide = (key: string, createId: string) => { - requests.set(createId, { contextKey: key, cancelled: false }) - setPending((prev) => ({ ...prev, [key]: createId })) - } - - const cancelSide = (key: string): boolean => { - const id = pending()[key] - if (!id) return false - const request = requests.get(id) - if (request) request.cancelled = true - setPending((prev) => { - const next = { ...prev } - delete next[key] - return next + /** + * Reorder the side terminals of a context by moving `from` to `to`'s + * position (side-panel strip drag-and-drop). Tab terminals keep their + * leading positions; only the side subset is reshuffled. The order + * lives in the same `terminalsByContext` list, so it survives sidebar + * context switches for the lifetime of the webview. + */ + const reorderSideDrag = (key: string, from: string, to: string): boolean => { + const order = sidesForContext(key).map((t) => t.id) + const fi = order.indexOf(from) + const ti = order.indexOf(to) + if (fi === -1 || ti === -1 || fi === ti) return false + const next = [...order] + next.splice(fi, 1) + next.splice(ti, 0, from) + setTerminalsByContext((prev) => { + const list = prev[key] + if (!list || list.length === 0) return prev + const tabs = list.filter((t) => t.placement === "tab") + const sides = list.filter((t) => t.placement === "side") + const byId = new Map(sides.map((t) => [t.id, t])) + const moved: TerminalTabStateWithContext[] = [] + for (const id of next) { + const t = byId.get(id) + if (t) moved.push(t) + } + // Fresh terminals that appeared mid-drag keep their tail position. + for (const t of sides) if (!moved.includes(t)) moved.push(t) + const ordered = [...tabs, ...moved] + if (ordered.length === list.length && ordered.every((t, i) => t.id === list[i]!.id)) return prev + return { ...prev, [key]: ordered } }) return true } + const pendingSide = (key: string) => (pending()[key]?.length ?? 0) > 0 + + const beginSide = (key: string, createId: string) => { + requests.set(createId, { contextKey: key }) + setPending((prev) => ({ ...prev, [key]: [...(prev[key] ?? []), createId] })) + } + const completeSide = (createId: string): SideRequest | undefined => { const request = requests.get(createId) if (!request) return undefined requests.delete(createId) setPending((prev) => { - if (prev[request.contextKey] !== createId) return prev + const list = (prev[request.contextKey] ?? []).filter((id) => id !== createId) + if (list.length === (prev[request.contextKey]?.length ?? 0)) return prev const next = { ...prev } - delete next[request.contextKey] + if (list.length === 0) delete next[request.contextKey] + else next[request.contextKey] = list return next }) return request @@ -341,8 +423,10 @@ export function createTerminalState(selection: Accessor): Termina current, all, sides, - side, - sideForContext, + sidesForContext, + sideActiveFor, + setSideActive, + sideFocusedId, currentKey, sideKey, activeId, @@ -352,11 +436,13 @@ export function createTerminalState(selection: Accessor): Termina focusRequest, requestFocus, hasRemembered, + title, + setTitle, reorder, reorderDrag, + reorderSideDrag, pendingSide, beginSide, - cancelSide, completeSide, } } @@ -376,8 +462,6 @@ export interface TerminalHandlerDeps { onRemove?: () => void /** Reveal the right-side inspector in terminal mode. */ onShowSide: (contextKey: string) => void - /** Leave terminal mode without killing the terminal. */ - onHideSide: () => void /** Resolve the current sidebar selection for the new-terminal helper. */ getSelection: () => string | null /** Sentinel value for the LOCAL sidebar selection. */ @@ -418,31 +502,42 @@ export function createTerminalHandlers(deps: TerminalHandlerDeps) { } /** - * Reveal the side panel and create-or-focus the context's side - * terminal. Reuses the existing terminal when one is alive, dedupes - * against an in-flight create, and never touches the tab strip or - * the chat session. + * Always create a fresh side terminal for the current context (the + * panel's `+` action and empty state). Multiple creates may be in + * flight at once; each lands as its own tab in the panel strip. */ - const requestSide = () => { + const addSide = () => { const key = deps.state.sideKey() deps.onShowSide(key) - const existing = deps.state.sideForContext(key) - if (existing) { - deps.state.requestFocus(existing.id) - return - } - if (deps.state.pendingSide(key)) return const id = newId() deps.state.beginSide(key, id) - const sel = deps.getSelection() deps.postMessage({ type: "agentManager.terminal.create", createId: id, placement: "side", - worktreeId: sel === null || sel === deps.LOCAL ? null : sel, + worktreeId: key === deps.LOCAL ? null : key, }) } + /** + * Reveal the side panel and focus the context's active side terminal, + * creating one when the context has none. Never touches the tab strip + * or the chat session. + */ + const requestSide = () => { + const key = deps.state.sideKey() + deps.onShowSide(key) + const existing = deps.state.sidesForContext(key) + if (existing.length > 0) { + const active = deps.state.sideActiveFor(key) ?? existing[existing.length - 1]!.id + deps.state.setSideActive(key, active) + deps.state.requestFocus(active) + return + } + if (deps.state.pendingSide(key)) return + addSide() + } + const closeTerminal = (terminalId: string) => { deps.onRemove?.() const ids = deps.tabIds() @@ -477,19 +572,29 @@ export function createTerminalHandlers(deps: TerminalHandlerDeps) { } /** - * Kill the current context's side terminal and hide the panel. With - * a create still in flight, cancels it instead — the late answer is - * closed by the message handler. + * Kill one side terminal. The panel stays open on the remaining + * terminals (or the empty state when this was the last one) — hiding + * is the toggle's job, not the close button's. Active-tab fallback + * is handled by the state layer. */ - const closeSide = () => { - const term = deps.state.side() - deps.onHideSide() - if (!term) return deps.state.cancelSide(deps.state.sideKey()) - deps.state.remove(term.id) - deps.postMessage({ type: "agentManager.terminal.close", terminalId: term.id }) + const closeSide = (terminalId: string): boolean => { + // Validate before mutating: dropping a non-side record here would + // unmount its xterm while the backend PTY leaks (no close sent). + const term = deps.state.sides().find((t) => t.id === terminalId) + if (!term) return false + deps.state.remove(terminalId) + deps.postMessage({ type: "agentManager.terminal.close", terminalId }) return true } + /** Make a side terminal the visible one in its panel and focus it. */ + const selectSide = (terminalId: string) => { + const key = deps.state.contextFor(terminalId) + if (!key) return + deps.state.setSideActive(key, terminalId) + deps.state.requestFocus(terminalId) + } + const middleClick = (terminalId: string, e: MouseEvent) => { if (e.button !== 1) return e.preventDefault() @@ -504,7 +609,18 @@ export function createTerminalHandlers(deps: TerminalHandlerDeps) { return true } - return { closeTerminal, closeSide, middleClick, activate, deactivate, requestNew, requestSide, closeActive } + return { + closeTerminal, + closeSide, + selectSide, + middleClick, + activate, + deactivate, + requestNew, + requestSide, + addSide, + closeActive, + } } export interface TerminalMessageHandlerDeps { @@ -545,14 +661,17 @@ function handleCreated(deps: TerminalMessageHandlerDeps, msg: CreatedMessage) { } if (msg.placement === "side") { // Side terminals are answered to a specific pending request. A - // missing, cancelled, or context-mismatched request means the user - // already moved on — close the PTY again instead of leaking it. + // missing or context-mismatched request means the webview was + // reloaded (or the context is gone) — close the PTY again instead + // of leaking it. const request = deps.state.completeSide(msg.createId) - if (!request || request.cancelled || request.contextKey !== contextKey) { + if (!request || request.contextKey !== contextKey) { deps.postMessage({ type: "agentManager.terminal.close", terminalId: msg.terminalId }) return } deps.state.add(msg.worktreeId, term) + // The newest terminal becomes the visible one in its panel. + deps.state.setSideActive(contextKey, msg.terminalId) deps.onSideCreated?.(contextKey, msg.terminalId) return } @@ -583,8 +702,6 @@ export function createTerminalMessageHandler(deps: TerminalMessageHandlerDeps) { } if (msg.type === "agentManager.terminal.error") { const request = msg.createId ? deps.state.completeSide(msg.createId) : undefined - // Errors for requests the user already cancelled are noise. - if (request?.cancelled) return true if (request) deps.onSideError?.(request.contextKey) deps.showError(msg.message) return true diff --git a/packages/kilo-vscode/webview-ui/agent-manager/worktree-diffs.ts b/packages/kilo-vscode/webview-ui/agent-manager/worktree-diffs.ts new file mode 100644 index 0000000000..fd56c87167 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/worktree-diffs.ts @@ -0,0 +1,118 @@ +/** + * Worktree diff data for Agent Manager. + * + * Owns the per-session diff map, the panel loading flag, and the per-file + * pending set, plus the backend message handlers that fill them and the + * helpers that request individual files. Extracted from `AgentManagerApp.tsx` + * so the app component only routes the diff messages and reads the signals. + */ + +import { createSignal, type Accessor } from "solid-js" +import { mergeWorktreeDiffs } from "../diff-viewer/diff-state" +import type { useVSCode } from "../src/context/vscode" +import type { + AgentManagerWorktreeDiffFileMessage, + AgentManagerWorktreeDiffLoadingMessage, + AgentManagerWorktreeDiffMessage, + WorktreeFileDiff, +} from "../src/types/messages" + +export function createWorktreeDiffs(vscode: ReturnType) { + const [diffDatas, setDiffDatas] = createSignal>({}) + const [diffLoading, setDiffLoading] = createSignal(false) + const [diffFileLoading, setDiffFileLoading] = createSignal>>({}) + + const setDiffFilePending = (sessionId: string, file: string, value: boolean) => { + setDiffFileLoading((prev) => { + const session = prev[sessionId] ?? {} + if (value) { + if (session[file]) return prev + return { + ...prev, + [sessionId]: { ...session, [file]: true }, + } + } + + if (!session[file]) return prev + const next = { ...session } + delete next[file] + if (Object.keys(next).length === 0) { + const result = { ...prev } + delete result[sessionId] + return result + } + return { + ...prev, + [sessionId]: next, + } + }) + } + + /** Lazily load a single file's full diff for the current session. */ + const requestDiffFile = (sessionId: string, file: string) => { + if (diffFileLoading()[sessionId]?.[file]) return + setDiffFilePending(sessionId, file, true) + vscode.postMessage({ type: "agentManager.requestWorktreeDiffFile", sessionId, file }) + } + + /** Files the backend flagged as stale in a merged update need a fresh fetch. */ + const refreshStaleDiffs = (sessionId: string, files: Set) => { + const loading = diffFileLoading()[sessionId] ?? {} + for (const file of files) { + if (loading[file]) continue + setDiffFilePending(sessionId, file, true) + vscode.postMessage({ type: "agentManager.requestWorktreeDiffFile", sessionId, file }) + } + } + + /** Files currently being fetched for a session, for per-file spinners. */ + const diffFileLoadingFor = (sessionId: Accessor) => { + const id = sessionId() + if (!id) return new Set() + return new Set(Object.keys(diffFileLoading()[id] ?? {})) + } + + // Backend messages. + + const onWorktreeDiff = (ev: AgentManagerWorktreeDiffMessage) => { + let staleFiles: Set | undefined + setDiffDatas((prev) => { + const existing = prev[ev.sessionId] + const merged = existing ? mergeWorktreeDiffs(existing, ev.diffs) : { diffs: ev.diffs, stale: new Set() } + staleFiles = merged.stale + const next = merged.diffs + if (existing && existing.length === next.length && existing.every((old, i) => old === next[i])) return prev + return { ...prev, [ev.sessionId]: next } + }) + if (staleFiles) refreshStaleDiffs(ev.sessionId, staleFiles) + } + + const onWorktreeDiffFile = (ev: AgentManagerWorktreeDiffFileMessage) => { + if (ev.diff) { + setDiffDatas((prev) => { + const existing = prev[ev.sessionId] ?? [] + const next = existing.map((item) => (item.file === ev.diff!.file ? ev.diff! : item)) + return { ...prev, [ev.sessionId]: next } + }) + setDiffFilePending(ev.sessionId, ev.diff.file, false) + return + } + setDiffFilePending(ev.sessionId, ev.file, false) + } + + const onWorktreeDiffLoading = (ev: AgentManagerWorktreeDiffLoadingMessage) => { + setDiffLoading(ev.loading) + } + + return { + diffDatas, + diffLoading, + setDiffLoading, + requestDiffFile, + refreshStaleDiffs, + diffFileLoadingFor, + onWorktreeDiff, + onWorktreeDiffFile, + onWorktreeDiffLoading, + } +} diff --git a/packages/kilo-vscode/webview-ui/kiloclaw/context/language.tsx b/packages/kilo-vscode/webview-ui/kiloclaw/context/language.tsx index e4e079638f..4a64ce3c7e 100644 --- a/packages/kilo-vscode/webview-ui/kiloclaw/context/language.tsx +++ b/packages/kilo-vscode/webview-ui/kiloclaw/context/language.tsx @@ -27,6 +27,7 @@ import { dict as tr } from "../i18n/tr" import { dict as zh } from "../i18n/zh" import { dict as uk } from "../i18n/uk" import { dict as zht } from "../i18n/zht" +import { dict as fa } from "../i18n/fa" const dicts: Record> = { en, @@ -49,6 +50,7 @@ const dicts: Record> = { uk: { ...en, ...uk }, zh: { ...en, ...zh }, zht: { ...en, ...zht }, + fa: { ...en, ...fa }, } type LanguageCtx = { diff --git a/packages/kilo-vscode/webview-ui/kiloclaw/i18n/fa.ts b/packages/kilo-vscode/webview-ui/kiloclaw/i18n/fa.ts new file mode 100644 index 0000000000..1c63633715 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/kiloclaw/i18n/fa.ts @@ -0,0 +1,95 @@ +export const dict = { + // App + "kiloClaw.loading": "در حال بارگذاری KiloClaw...", + + // Chat + "kiloClaw.chat.connecting": "در حال اتصال...", + "kiloClaw.chat.notRunning": "نمونه در حال اجرا نیست", + "kiloClaw.chat.placeholder": "پیامی بنویسید... (Enter برای ارسال)", + "kiloClaw.chat.online": "آنلاین", + "kiloClaw.chat.offline": "آفلاین", + "kiloClaw.chat.idle": "بیکار", + "kiloClaw.chat.unknown": "نامشخص", + "kiloClaw.chat.empty": "هنوز پیامی وجود ندارد. سلام کنید!", + "kiloClaw.chat.emptyWithBot": "هنوز پیامی وجود ندارد. به {bot} سلام کنید!", + "kiloClaw.chat.send": "ارسال", + "kiloClaw.chat.waitingBotStatus": "در انتظار وضعیت ربات...", + "kiloClaw.chat.botOffline": "ربات آفلاین است", + + // Conversations + "kiloClaw.conversations.title": "مکالمات", + "kiloClaw.conversations.new": "مکالمه جدید", + "kiloClaw.conversations.empty": "هنوز مکالمه‌ای وجود ندارد", + "kiloClaw.conversations.untitled": "بدون عنوان", + "kiloClaw.conversations.rename": "تغییر نام", + "kiloClaw.conversations.leave": "خروج", + "kiloClaw.conversations.selectOne": "یک مکالمه را برای شروع گفتگو انتخاب کنید", + "kiloClaw.conversations.groupToday": "امروز", + "kiloClaw.conversations.groupYesterday": "دیروز", + "kiloClaw.conversations.groupThisWeek": "این هفته", + "kiloClaw.conversations.groupOlder": "قدیمی‌تر", + + // Typing + "kiloClaw.typing.one": "{name} در حال تایپ است...", + "kiloClaw.typing.many": "{count} نفر در حال تایپ هستند...", + + // Messages + "kiloClaw.message.bot": "KiloClaw", + "kiloClaw.message.you": "شما", + "kiloClaw.message.thinking": "در حال فکر کردن...", + "kiloClaw.message.deleted": "[پیام حذف شد]", + "kiloClaw.message.replyDeleted": "پیام اصلی حذف شد", + "kiloClaw.message.notDelivered": "ارسال نشد", + "kiloClaw.message.edited": "(ویرایش شده)", + "kiloClaw.message.reply": "پاسخ", + "kiloClaw.message.replyTo": "در حال پاسخ به", + "kiloClaw.message.cancelReply": "لغو پاسخ", + "kiloClaw.message.edit": "ویرایش", + "kiloClaw.message.delete": "حذف", + "kiloClaw.message.confirmDelete": "این پیام حذف شود؟", + "kiloClaw.message.cancel": "انصراف", + "kiloClaw.message.save": "ذخیره", + "kiloClaw.message.react": "واکنش", + "kiloClaw.message.removeReaction": "حذف واکنش", + "kiloClaw.message.copy": "کپی", + "kiloClaw.message.copied": "در کلیپ‌بورد کپی شد", + "kiloClaw.message.copyFailed": "کپی انجام نشد", + + // Error + "kiloClaw.error.retry": "تلاش مجدد", + + // Setup + "kiloClaw.setup.title": "KiloClaw", + "kiloClaw.setup.subtitle": "هوش مصنوعی شخصی برای زندگی روزمره", + "kiloClaw.setup.description1": + "KiloClaw یک هوش مصنوعی شخصی در اختیار شما قرار می‌دهد که ایمیل‌ها را می‌خواند، تقویم شما را مدیریت می‌کند، پروژه‌هایتان را رصد می‌کند و در Telegram، Slack — هر چیزی که از قبل استفاده می‌کنید — در دسترس است.", + "kiloClaw.setup.description2": + "نیازی به نصب برنامه نیست. رابط جدیدی برای یادگیری وجود ندارد. فقط مثل یک دوست برایش پیام بفرستید.", + "kiloClaw.setup.learnMore": "بیشتر بدانید", + "kiloClaw.setup.tryKiloClaw": "امتحان KiloClaw", + + // Upgrade + "kiloClaw.upgrade.title": "KiloClaw Chat نیاز به ارتقا دارد", + "kiloClaw.upgrade.description1": "این نمونه قبل از فعال‌سازی چت راه‌اندازی شده است.", + "kiloClaw.upgrade.description2.before": "از دکمه ", + "kiloClaw.upgrade.description2.bold": "ارتقا به آخرین نسخه", + "kiloClaw.upgrade.description2.after": " در داشبورد KiloClaw برای فعال‌سازی چت زنده با ربات خود استفاده کنید.", + "kiloClaw.upgrade.openDashboard": "باز کردن داشبورد", + + // Sidebar + "kiloClaw.sidebar.title": "KiloClaw", + "kiloClaw.sidebar.instance": "نمونه", + "kiloClaw.sidebar.unknown": "ناشناخته", + "kiloClaw.sidebar.bot": "ربات", + "kiloClaw.sidebar.botStatus": "وضعیت ربات", + "kiloClaw.sidebar.context": "زمینه", + "kiloClaw.sidebar.used": "استفاده‌شده", + "kiloClaw.sidebar.tokens": "توکن‌ها", + "kiloClaw.sidebar.model": "مدل", + "kiloClaw.sidebar.provider": "ارائه‌دهنده", + "kiloClaw.sidebar.details": "جزئیات", + "kiloClaw.sidebar.region": "منطقه", + "kiloClaw.sidebar.version": "نسخه", + "kiloClaw.sidebar.channels": "کانال‌ها", + "kiloClaw.sidebar.noData": "داده‌ای برای نمونه وجود ندارد", +} diff --git a/packages/kilo-vscode/webview-ui/src/App.tsx b/packages/kilo-vscode/webview-ui/src/App.tsx index 1aa6768823..7fa356e848 100644 --- a/packages/kilo-vscode/webview-ui/src/App.tsx +++ b/packages/kilo-vscode/webview-ui/src/App.tsx @@ -1,34 +1,18 @@ import { Component, createSignal, createMemo, Switch, Match, Show, onMount, onCleanup } from "solid-js" -import { ThemeProvider } from "@kilocode/kilo-ui/theme" -import { DialogProvider } from "@kilocode/kilo-ui/context/dialog" -import { MarkedProvider } from "@kilocode/kilo-ui/context/marked" -import { CodeComponentProvider } from "@kilocode/kilo-ui/context/code" -import { DiffComponentProvider } from "@kilocode/kilo-ui/context/diff" -import { FileComponentProvider } from "@kilocode/kilo-ui/context/file" -import { Code } from "@kilocode/kilo-ui/code" -import { Diff } from "@kilocode/kilo-ui/diff" -import { File } from "@kilocode/kilo-ui/file" import { DataProvider } from "@kilocode/kilo-ui/context/data" -import { Toast } from "@kilocode/kilo-ui/toast" import Settings from "./components/settings/Settings" import ProfileView from "./components/profile/ProfileView" -import { VSCodeProvider, useVSCode } from "./context/vscode" -import { ServerProvider, useServer } from "./context/server" -import { ProviderProvider, useProvider } from "./context/provider" -import { ConfigProvider } from "./context/config" -import { DisplayProvider } from "./context/display" +import { useVSCode } from "./context/vscode" +import { useServer } from "./context/server" +import { useProvider } from "./context/provider" import { WorkStyleProvider } from "./context/work-style" -import { IndexingProvider } from "./context/indexing" -import { AgentRequirementsProvider } from "./context/agent-requirements" -import { MemoryProvider } from "./context/memory" -import { SessionProvider, useSession } from "./context/session" +import { useSession } from "./context/session" import { LocalTabsProvider, useLocalTabs } from "./context/local-tabs" -import { LanguageBridge } from "./context/language-bridge" +import { ProviderShell } from "./context/provider-shell" import { ChatView } from "./components/chat" import { SidebarEmptyState } from "./components/chat/SidebarEmptyState" import { registerExpandedTaskTool } from "./components/chat/TaskToolExpanded" import { registerVscodeToolOverrides } from "./components/chat/VscodeToolOverrides" -import { SpeechToTextPrewarm } from "./components/speech-to-text/SpeechToTextPrewarm" // Override the upstream "task" tool renderer with the fully-expanded version // that shows child session parts inline in the VS Code sidebar. @@ -37,10 +21,6 @@ registerExpandedTaskTool() registerVscodeToolOverrides() import HistoryView from "./components/history/HistoryView" import { MigrationWizard } from "./components/migration" // legacy-migration -import { NotificationsProvider } from "./context/notifications" -import { FeedbackProvider } from "./context/feedback" -import { KiloEmbeddingModelsProvider } from "./context/kilo-embedding-models" -import { ImageModelsProvider } from "./context/image-models" import type { Message as SDKMessage, Part as SDKPart } from "@kilocode/sdk/v2" import { cycleAgent as cycle } from "./context/session-agent" import "./styles/chat.css" @@ -210,27 +190,6 @@ export const DataBridge: Component<{ children: any }> = (props) => { ) } -type MermaidImageEvent = CustomEvent<{ dataUrl: string; filename: string }> - -export const MermaidDownloadBridge: Component = () => { - const vscode = useVSCode() - - onMount(() => { - const save = (event: Event) => { - const detail = (event as MermaidImageEvent).detail - if (!detail?.dataUrl || !detail.filename) return - event.preventDefault() - vscode.postMessage({ type: "saveImage", dataUrl: detail.dataUrl, filename: detail.filename }) - } - window.addEventListener("kilo:save-image", save) - onCleanup(() => { - window.removeEventListener("kilo:save-image", save) - }) - }) - - return null -} - // Inner app component that uses the contexts const AppContent: Component = () => { const [currentView, setCurrentView] = createSignal("newTask") @@ -412,59 +371,21 @@ const AppContent: Component = () => { ) } -// Main App component with context providers const App: Component = () => { return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + ) } diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx index 40e0b72876..d2daa6389b 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx @@ -70,6 +70,8 @@ import { type TranscriptHold, type TranscriptRow, } from "../../context/transcript-rows" +import { PromptRail } from "./PromptRail" +import { capacity, promptItems, railItems, type PromptRailItem } from "./prompt-rail" import { onTimelineHighlight, type TimelineHighlight } from "../../utils/timeline/highlight" import { useTranscriptSearch, type SearchMatch } from "../../context/transcript-search" import { applyTranscriptHighlights, clearTranscriptHighlights } from "./transcript-search-highlight" @@ -164,6 +166,8 @@ export const MessageList: Component = (props) => { const [scrollEl, setScrollEl] = createSignal() const [virtualizer, setVirtualizer] = createSignal() const [layout, setLayout] = createSignal("") + // Transcript height, kept reactive so the prompt rail re-caps on resize. + const [height, setHeight] = createSignal(0) const revert = () => session.revert() ?? undefined const turns = createMemo((prev: MessageTurn[] | undefined) => @@ -944,6 +948,21 @@ export const MessageList: Component = (props) => { const keys = createMemo(() => partition().virtual.map((row) => row.key)) const fingerprint = createMemo(() => rowFingerprint(keys())) + // Scrolls the transcript to a row by key. Virtualized rows jump through + // the virtualizer; direct/live/queued rows are mounted, so they use + // scrollIntoView. Pauses auto-follow first so the jump isn't snapped back. + const jump = (key: string) => { + autoScroll.pause() + const index = keys().indexOf(key) + if (index >= 0) { + virtualizer()?.scrollToIndex(index, { align: "start" }) + return + } + const el = scrollEl() + const target = el?.querySelector(`[data-row-key="${CSS.escape(key)}"]`) + target?.scrollIntoView({ block: "start" }) + } + // Clicking a bar in the task timeline scrolls the transcript to that message. // Jumps land instantly (no smooth animation): while pinned at the bottom, a // smooth scroll's initial frames sit within createAutoScroll's near-bottom @@ -956,19 +975,50 @@ export const MessageList: Component = (props) => { // actually contains the clicked part, not just the message's first chunk. const row = matches.find((r) => r.type === "assistant" && r.parts.some((p) => p.id === detail.partId)) ?? matches[0] if (!row) return - autoScroll.pause() - const index = keys().indexOf(row.key) - if (index >= 0) { - virtualizer()?.scrollToIndex(index, { align: "start" }) - return - } - const el = scrollEl() - const target = el?.querySelector(`[data-row-key="${CSS.escape(row.key)}"]`) - target?.scrollIntoView({ block: "start" }) + jump(row.key) } window.addEventListener("scrollToMessage", onScrollToMessage) onCleanup(() => window.removeEventListener("scrollToMessage", onScrollToMessage)) + // Prompt rail: one tick per user prompt, positioned to the left of the + // readable lane, opening a card of prompt/answer previews on hover. + const items = createMemo(() => promptItems(rows())) + // Until the transcript is measured there is no height to cap against, and + // rendering every prompt would spill ticks past the rail on long sessions. + const shown = createMemo(() => railItems(items(), capacity(height()))) + const [activeTurn, setActiveTurn] = createSignal() + const railActiveKey = createMemo(() => shown().find((item) => item.turn === activeTurn())?.key) + + const trackActive = () => { + const list = shown() + if (list.length === 0) return setActiveTurn(undefined) + const handle = virtualizer() + const offset = handle?.scrollOffset + if (handle && offset !== undefined && offset > 1) { + const row = partition().virtual[handle.findItemIndex(offset)] + if (row) return setActiveTurn(row.turn) + } + setActiveTurn(list.at(-1)?.turn) + } + let activeFrame: number | undefined + const scheduleActive = () => { + if (activeFrame !== undefined) return + activeFrame = requestAnimationFrame(() => { + activeFrame = undefined + trackActive() + }) + } + onCleanup(() => { + if (activeFrame !== undefined) cancelAnimationFrame(activeFrame) + }) + // Re-derive the active turn whenever the transcript changes so the rail + // reflects a newly started turn even before any scrolling happens. + createEffect(() => { + shown() + partition() + scheduleActive() + }) + // Highlights the part behind the currently hovered/focused timeline bar // (dispatched by TaskTimeline) so the two stay visually correlated. const [highlight, setHighlight] = createSignal() @@ -1018,6 +1068,7 @@ export const MessageList: Component = (props) => { const handleScroll = () => { autoScroll.handleScroll() maybeLoadOlder() + scheduleActive() if (search.active()) scheduleHighlight() } @@ -1026,6 +1077,7 @@ export const MessageList: Component = (props) => { const el = scrollEl() if (!el) return const style = getComputedStyle(el) + setHeight(el.clientHeight) setLayout( layoutFingerprint({ width: Math.round(el.clientWidth), @@ -1210,6 +1262,17 @@ export const MessageList: Component = (props) => {
+ railActiveKey()} + onSelect={(item: PromptRailItem) => jump(item.key)} + onWheel={(deltaY: number) => { + const el = scrollEl() + if (el) el.scrollTop += deltaY + }} + height={height} + /> + + )} + + + + + {(position) => ( + + + + )} + + + ) +} diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/QuestionDock.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/QuestionDock.tsx index 736c6355a0..74fa8f1ba7 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/QuestionDock.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/QuestionDock.tsx @@ -348,7 +348,9 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) =>
{summary()}
-
{questionText()}
+
+ {questionText()} +
e.stopPropagation()}> @@ -391,7 +393,9 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) =>
-
{questionText()}
+
+ {questionText()} +
{language.t("ui.question.singleHint")}
}>
{language.t("ui.question.multiHint")}
@@ -419,9 +423,13 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) => - {localized.label()} + + {localized.label()} + - {localized.description()} + + {localized.description()} + @@ -498,8 +506,10 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) => const answered = () => Boolean(value()) return (
- {tr(language.t, q.questionKey, q.question)} - + + {tr(language.t, q.questionKey, q.question)} + + {answered() ? value() : language.t("ui.question.review.notAnswered")}
diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/SuggestBar.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/SuggestBar.tsx index 2eac797698..63cc677ad6 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/SuggestBar.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/SuggestBar.tsx @@ -30,7 +30,9 @@ export const SuggestBar: Component<{ request: SuggestionRequest }> = (props) => - {props.request.text} + + {props.request.text} +
diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/TabDnd.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/TabDnd.tsx index 37c650a228..bd86ada3d5 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/TabDnd.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/TabDnd.tsx @@ -27,13 +27,13 @@ export const ConstrainDragYAxis: Component = () => { return null } -export const SortableTabContainer: ParentComponent<{ id: string }> = (props) => { +export const SortableTabContainer: ParentComponent<{ id: string; class?: string }> = (props) => { const sortable = createSortable(props.id) void sortable return (
diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/prompt-rail.ts b/packages/kilo-vscode/webview-ui/src/components/chat/prompt-rail.ts new file mode 100644 index 0000000000..e20de18cfc --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/components/chat/prompt-rail.ts @@ -0,0 +1,98 @@ +import type { Part } from "../../types/messages" +import type { TranscriptRow } from "../../context/transcript-rows" + +export interface PromptRailItem { + key: string + turn: string + queued: boolean + prompt: string + answer: string +} + +const PROMPT_LIMIT = 160 +const ANSWER_LIMIT = 220 + +/** + * Height of the tallest card row (padding + a one-line prompt + a two-line + * answer), and the unit the fit cap is measured in. Deliberately the worst + * case rather than an average: "only show what fits" should stay true for a + * card whose rows all wrap, not just for a lucky mix of short ones. + */ +export const ROW_HEIGHT = 76 +/** Vertical padding reserved at the top and bottom of the rail. */ +export const RAIL_INSET = 24 + +/** + * How many prompts fit the available transcript height. The card and the rail + * always render the same set, so this one number drives both. + */ +export function capacity(height: number): number { + return Math.floor((height - RAIL_INSET) / ROW_HEIGHT) +} + +// The card never renders markdown — user message text shows literally, and +// assistant text should too. Code spans (inline and fenced) are dropped, and +// link URLs / images are stripped rather than parsed (mirrors MessageList's +// stripMarkdownLinkUrls split so bracket text inside inline code stays out, +// not half-stripped). +export function previewText(raw: string): string { + const segments = raw.split(/(```[\s\S]*?```|`[^`\n]*`)/g) + const text = segments.map((segment, i) => (i % 2 === 1 ? "" : stripLinks(segment))).join(" ") + return text + .replace(/^#{1,6}\s+/gm, "") + .replace(/^\s*[-*>+]\s+/gm, "") + .replace(/\s+/g, " ") + .trim() +} + +function stripLinks(text: string) { + return text.replace(/!\[[^\]]*\]\([^)]*\)/g, "").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1") +} + +function text(parts: Part[], limit: number): string { + const joined = parts + .filter((part) => part.type === "text" && !part.synthetic && part.text.trim()) + .map((part) => (part.type === "text" ? part.text : "")) + .join("\n") + return truncate(previewText(joined), limit) +} + +function truncate(value: string, limit: number) { + return value.length <= limit ? value : `${value.slice(0, limit - 1)}…` +} + +export function promptItems(rows: TranscriptRow[]): PromptRailItem[] { + const items: PromptRailItem[] = [] + for (const row of rows) { + if (row.type !== "user") continue + items.push({ key: row.key, turn: row.turn, queued: row.queued, prompt: text(row.parts, PROMPT_LIMIT), answer: "" }) + } + // Answer text is grouped by turn so one pass fills every item; assistant + // rows follow their user row and carry the same `turn` id. + let index = 0 + for (const row of rows) { + if (row.type === "user") { + index += 1 + continue + } + if (row.type !== "assistant" || index === 0) continue + const item = items[index - 1]! + if (item.turn !== row.turn || item.answer) continue + const value = text(row.parts, ANSWER_LIMIT) + if (value) item.answer = value + } + // A prompt can carry no text at all (image-only or file-only message). Rather + // than render a blank row, promote the answer into the label so the row still + // says something; if neither has text the card falls back to its placeholder. + for (const item of items) { + if (item.prompt || !item.answer) continue + item.prompt = truncate(item.answer, PROMPT_LIMIT) + item.answer = "" + } + return items +} + +export function railItems(items: PromptRailItem[], capacity: number): PromptRailItem[] { + if (capacity < 1) return [] + return items.slice(-capacity) +} diff --git a/packages/kilo-vscode/webview-ui/src/context/language-utils.ts b/packages/kilo-vscode/webview-ui/src/context/language-utils.ts index af837c7e92..247a5da01e 100644 --- a/packages/kilo-vscode/webview-ui/src/context/language-utils.ts +++ b/packages/kilo-vscode/webview-ui/src/context/language-utils.ts @@ -19,9 +19,10 @@ export type Locale = | "nl" | "uk" | "it" + | "fa" /** Locales that use right-to-left script. */ -export const RTL_LOCALES = new Set(["ar"]) +export const RTL_LOCALES = new Set(["ar", "fa"]) /** Map internal locale IDs to valid BCP 47 language tags for the HTML lang attribute. */ export const LOCALE_BCP47: Partial> = { @@ -55,6 +56,7 @@ export const LOCALES: readonly Locale[] = [ "nl", "uk", "it", + "fa", ] /** diff --git a/packages/kilo-vscode/webview-ui/src/context/language.tsx b/packages/kilo-vscode/webview-ui/src/context/language.tsx index 7dad79270d..709e0a05cf 100644 --- a/packages/kilo-vscode/webview-ui/src/context/language.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/language.tsx @@ -49,6 +49,7 @@ import { dict as appTr } from "../i18n/tr" import { dict as appNl } from "../i18n/nl" import { dict as appUk } from "../i18n/uk" import { dict as appIt } from "../i18n/it" +import { dict as appFa } from "../i18n/fa" import { dict as amEn } from "../../agent-manager/i18n/en" import { dict as amZh } from "../../agent-manager/i18n/zh" import { dict as amZht } from "../../agent-manager/i18n/zht" @@ -69,6 +70,7 @@ import { dict as amTr } from "../../agent-manager/i18n/tr" import { dict as amNl } from "../../agent-manager/i18n/nl" import { dict as amUk } from "../../agent-manager/i18n/uk" import { dict as amIt } from "../../agent-manager/i18n/it" +import { dict as amFa } from "../../agent-manager/i18n/fa" import { dict as kiloEn } from "@kilocode/kilo-i18n/en" import { dict as kiloZh } from "@kilocode/kilo-i18n/zh" import { dict as kiloZht } from "@kilocode/kilo-i18n/zht" @@ -118,6 +120,7 @@ export const LOCALE_LABELS: Record = { nl: "Nederlands", uk: "Українська", it: "Italiano", + fa: "فارسی", } // Merge 4 dict layers: app + ui + kilo + agent manager (kilo and agent manager override last) @@ -143,6 +146,9 @@ const dicts: Record> = { nl: { ...base, ...appNl, ...uiNl, ...kiloNl, ...amEn, ...amNl }, uk: { ...base, ...appUk, ...uiUk, ...kiloUk, ...amEn, ...amUk }, it: { ...base, ...appIt, ...uiIt, ...kiloIt, ...amEn, ...amIt }, + // Persian (Kilo fork addition). Only app + agent-manager layers are localized; + // the upstream ui/kilo layers fall back to English via `base`. + fa: { ...base, ...appFa, ...amEn, ...amFa }, } function normalizeLocale(lang: string): Locale { diff --git a/packages/kilo-vscode/webview-ui/src/context/provider-shell.tsx b/packages/kilo-vscode/webview-ui/src/context/provider-shell.tsx new file mode 100644 index 0000000000..7a89205249 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/context/provider-shell.tsx @@ -0,0 +1,96 @@ +import { onCleanup, onMount, type Component, type ParentComponent } from "solid-js" +import { ThemeProvider } from "@kilocode/kilo-ui/theme" +import { DialogProvider } from "@kilocode/kilo-ui/context/dialog" +import { MarkedProvider } from "@kilocode/kilo-ui/context/marked" +import { CodeComponentProvider } from "@kilocode/kilo-ui/context/code" +import { DiffComponentProvider } from "@kilocode/kilo-ui/context/diff" +import { FileComponentProvider } from "@kilocode/kilo-ui/context/file" +import { Code } from "@kilocode/kilo-ui/code" +import { Diff } from "@kilocode/kilo-ui/diff" +import { File } from "@kilocode/kilo-ui/file" +import { Toast } from "@kilocode/kilo-ui/toast" +import { VSCodeProvider, useVSCode } from "./vscode" +import { ServerProvider } from "./server" +import { ProviderProvider } from "./provider" +import { ConfigProvider } from "./config" +import { DisplayProvider } from "./display" +import { IndexingProvider } from "./indexing" +import { AgentRequirementsProvider } from "./agent-requirements" +import { MemoryProvider } from "./memory" +import { SessionProvider } from "./session" +import { LanguageBridge } from "./language-bridge" +import { NotificationsProvider } from "./notifications" +import { FeedbackProvider } from "./feedback" +import { KiloEmbeddingModelsProvider } from "./kilo-embedding-models" +import { ImageModelsProvider } from "./image-models" +import { SpeechToTextPrewarm } from "../components/speech-to-text/SpeechToTextPrewarm" + +type MermaidImageEvent = CustomEvent<{ dataUrl: string; filename: string }> + +const MermaidDownloadBridge: Component = () => { + const vscode = useVSCode() + + onMount(() => { + const save = (event: Event) => { + const detail = (event as MermaidImageEvent).detail + if (!detail?.dataUrl || !detail.filename) return + event.preventDefault() + vscode.postMessage({ type: "saveImage", dataUrl: detail.dataUrl, filename: detail.filename }) + } + window.addEventListener("kilo:save-image", save) + onCleanup(() => window.removeEventListener("kilo:save-image", save)) + }) + + return null +} + +const Root: ParentComponent = (props) => ( + + + + + + + + + + + + + + {props.children} + + + + + + + + + + + + +) + +const Session: ParentComponent = (props) => ( + + + + + {props.children} + + + + +) + +const Chat: ParentComponent = (props) => ( + + + {props.children} + + +) + +export const ProviderShell = { Root, Session, Chat } diff --git a/packages/kilo-vscode/webview-ui/src/context/session-outcome.ts b/packages/kilo-vscode/webview-ui/src/context/session-outcome.ts index e17f548ef8..eaf9b6bf76 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session-outcome.ts +++ b/packages/kilo-vscode/webview-ui/src/context/session-outcome.ts @@ -41,6 +41,9 @@ function identifiers( export function terminal(input: Input): TerminalState | undefined { if (!input.reason) return undefined + // A superseded turn handed off to a queued follow-up; it is not a premature + // stop, and the follow-up turn closes with its own reason afterwards. + if (input.reason === "superseded") return undefined const last = input.messages[input.messages.length - 1] const finish = last?.role === "assistant" ? last.finish : undefined const ids = identifiers(last, input.parts) diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index 592ae22dcf..bc448d821e 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -705,6 +705,10 @@ export const dict = { "session.messages.scrollToBottom": "التمرير إلى الأسفل", "session.messages.initializing": "جاري التهيئة...", "session.messages.taskStarting": "جاري البدء...", + "session.prompts.navLabel": "مستعرض المطالبات", + "session.prompts.tick": "المطالبة {{index}} من {{total}}: {{prompt}}", + "session.prompts.noAnswer": "لا توجد استجابة بعد", + "session.prompts.queued": "في قائمة الانتظار", "session.status.writingResponse": "...جارٍ كتابة الرد", "session.status.retry": "جارٍ إعادة المحاولة…", "session.status.working": "...جارٍ العمل", @@ -732,7 +736,7 @@ export const dict = { "prompt.placeholder.connecting": "جارٍ الاتصال بالخادم...", "prompt.placeholder.error": "فشل الاتصال. تحقق من لوحة الإخراج أو أعد تشغيل الإضافة.", - "prompt.placeholder.default": "اكتب رسالة... (Enter للإرسال، Shift+Enter لسطر جديد)", + "prompt.placeholder.default": "اكتب رسالة، @ للإشارة إلى الملفات... (Enter للإرسال، Shift+Enter لسطر جديد)", "context.usage.sessionCost": "تكلفة الجلسة", "context.usage.olderSessions": "{{count}} جلسات أقدم", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index 03055f3543..6db946631d 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -723,6 +723,10 @@ export const dict = { "session.messages.scrollToBottom": "Rolar para o final", "session.messages.initializing": "O teraouiñ...", "session.messages.taskStarting": "O kregiñ...", + "session.prompts.navLabel": "Navegador de prompts", + "session.prompts.tick": "Prompt {{index}} de {{total}}: {{prompt}}", + "session.prompts.noAnswer": "Sem resposta ainda", + "session.prompts.queued": "Na fila", "session.status.writingResponse": "Escrevendo resposta…", "session.status.retry": "Tentando novamente…", "session.status.working": "Trabalhando…", @@ -750,7 +754,8 @@ export const dict = { "prompt.placeholder.connecting": "Conectando ao servidor...", "prompt.placeholder.error": "Conexão falhou. Verifique o painel de saída ou reinicie a extensão.", - "prompt.placeholder.default": "Digite uma mensagem... (Enter para enviar, Shift+Enter para nova linha)", + "prompt.placeholder.default": + "Digite uma mensagem, @ para mencionar arquivos... (Enter para enviar, Shift+Enter para nova linha)", "context.usage.sessionCost": "Custo da sessão", "context.usage.olderSessions": "{{count}} sessões anteriores", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index 5575a758b8..8f0484c02d 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -723,6 +723,10 @@ export const dict = { "session.messages.scrollToBottom": "Pomakni se na dno", "session.messages.initializing": "Inicijalizacija...", "session.messages.taskStarting": "Pokretanje...", + "session.prompts.navLabel": "Navigator upita", + "session.prompts.tick": "Upit {{index}} od {{total}}: {{prompt}}", + "session.prompts.noAnswer": "Još nema odgovora", + "session.prompts.queued": "Na čekanju", "session.status.writingResponse": "Pisanje odgovora…", "session.status.retry": "Ponovni pokušaj…", "session.status.working": "Radim…", @@ -750,7 +754,8 @@ export const dict = { "prompt.placeholder.connecting": "Povezivanje na server...", "prompt.placeholder.error": "Povezivanje nije uspjelo. Provjerite panel za izlaz ili ponovo pokrenite ekstenziju.", - "prompt.placeholder.default": "Unesite poruku... (Enter za slanje, Shift+Enter za novi red)", + "prompt.placeholder.default": + "Unesite poruku, @ za spominjanje datoteka... (Enter za slanje, Shift+Enter za novi red)", "context.usage.sessionCost": "Cijena sesije", "context.usage.olderSessions": "{{count}} starijih sesija", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index 90ff025087..c2bfa6932b 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -721,6 +721,10 @@ export const dict = { "session.messages.scrollToBottom": "Rul til bunden", "session.messages.initializing": "Initialiserer...", "session.messages.taskStarting": "Starter...", + "session.prompts.navLabel": "Promptnavigator", + "session.prompts.tick": "Prompt {{index}} af {{total}}: {{prompt}}", + "session.prompts.noAnswer": "Intet svar endnu", + "session.prompts.queued": "I kø", "session.status.writingResponse": "Skriver svar…", "session.status.retry": "Prøver igen…", "session.status.working": "Arbejder…", @@ -748,7 +752,8 @@ export const dict = { "prompt.placeholder.connecting": "Opretter forbindelse til server...", "prompt.placeholder.error": "Forbindelse mislykkedes. Tjek outputpanelet eller genstart udvidelsen.", - "prompt.placeholder.default": "Skriv en besked... (Enter for at sende, Shift+Enter for ny linje)", + "prompt.placeholder.default": + "Skriv en besked, @ for at nævne filer... (Enter for at sende, Shift+Enter for ny linje)", "context.usage.sessionCost": "Sessionsomkostning", "context.usage.olderSessions": "{{count}} ældre sessioner", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index 811b9ee0fd..7e1bd48f0c 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -734,6 +734,10 @@ export const dict = { "session.messages.scrollToBottom": "Nach unten scrollen", "session.messages.initializing": "Initialisierung...", "session.messages.taskStarting": "Wird gestartet...", + "session.prompts.navLabel": "Prompt-Navigation", + "session.prompts.tick": "Prompt {{index}} von {{total}}: {{prompt}}", + "session.prompts.noAnswer": "Noch keine Antwort", + "session.prompts.queued": "In Warteschlange", "session.status.writingResponse": "Antwort wird geschrieben…", "session.status.retry": "Erneuter Versuch…", "session.status.working": "Wird bearbeitet…", @@ -762,7 +766,8 @@ export const dict = { "prompt.placeholder.connecting": "Verbindung zum Server wird hergestellt...", "prompt.placeholder.error": "Verbindung fehlgeschlagen. Überprüfen Sie das Ausgabepanel oder starten Sie die Erweiterung neu.", - "prompt.placeholder.default": "Nachricht eingeben... (Enter zum Senden, Shift+Enter für neue Zeile)", + "prompt.placeholder.default": + "Nachricht eingeben, @ um Dateien zu erwähnen... (Enter zum Senden, Shift+Enter für neue Zeile)", "context.usage.sessionCost": "Sitzungskosten", "context.usage.olderSessions": "{{count}} ältere Sitzungen", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index 7cc55121fe..ecdc5500bf 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -675,6 +675,10 @@ export const dict = { "session.messages.scrollToBottom": "Scroll to bottom", "session.messages.initializing": "Initializing...", "session.messages.taskStarting": "Starting...", + "session.prompts.navLabel": "Prompt navigator", + "session.prompts.tick": "Prompt {{index}} of {{total}}: {{prompt}}", + "session.prompts.noAnswer": "No response yet", + "session.prompts.queued": "Queued", "session.status.writingResponse": "Writing response...", "session.status.retry": "Retrying…", "session.status.working": "Working...", @@ -724,7 +728,7 @@ export const dict = { "dialog.model.noProviders": "No providers", "prompt.placeholder.connecting": "Connecting to server...", - "prompt.placeholder.default": "Type a message... (Enter to send, Shift+Enter for new line)", + "prompt.placeholder.default": "Type a message, @ to mention files... (Enter to send, Shift+Enter for new line)", "prompt.placeholder.error": "Connection failed. Check the output panel or restart the extension.", "context.usage.sessionCost": "Session cost", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index 7981f2a551..4eb5fe4255 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -728,6 +728,10 @@ export const dict = { "session.messages.scrollToBottom": "Desplazar al final", "session.messages.initializing": "Inicializando...", "session.messages.taskStarting": "Iniciando...", + "session.prompts.navLabel": "Navegador de prompts", + "session.prompts.tick": "Prompt {{index}} de {{total}}: {{prompt}}", + "session.prompts.noAnswer": "Aún no hay respuesta", + "session.prompts.queued": "En cola", "session.status.writingResponse": "Escribiendo respuesta…", "session.status.retry": "Reintentando…", "session.status.working": "Trabajando…", @@ -755,7 +759,8 @@ export const dict = { "prompt.placeholder.connecting": "Conectando al servidor...", "prompt.placeholder.error": "Conexión fallida. Revisa el panel de salida o reinicia la extensión.", - "prompt.placeholder.default": "Escribe un mensaje... (Enter para enviar, Shift+Enter para nueva línea)", + "prompt.placeholder.default": + "Escribe un mensaje, @ para mencionar archivos... (Enter para enviar, Shift+Enter para nueva línea)", "context.usage.sessionCost": "Coste de la sesión", "context.usage.olderSessions": "{{count}} sesiones anteriores", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fa.ts b/packages/kilo-vscode/webview-ui/src/i18n/fa.ts new file mode 100644 index 0000000000..02a2bf9ab5 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/i18n/fa.ts @@ -0,0 +1,1288 @@ +export const anacondaDesktopDict = { + "provider.anaconda.title.connect": "اتصال به Anaconda Desktop", + "provider.anaconda.title.manage": "مدیریت Anaconda Desktop", + "provider.anaconda.status.checking": "در حال بررسی Anaconda Desktop...", + "provider.anaconda.status.opening": "در حال باز کردن Anaconda Desktop...", + "provider.anaconda.status.syncing": "در حال به‌روزرسانی مدل‌های ارائه‌دهنده...", + "provider.anaconda.status.ready": "آماده اتصال", + "provider.anaconda.status.waiting": "در انتظار Desktop", + "provider.anaconda.status.attention": "نیاز به توجه", + "provider.anaconda.status.unavailable": "در دسترس نیست", + "provider.anaconda.state.unsupported": "Anaconda Desktop در {{platform}} پشتیبانی نمی‌شود.", + "provider.anaconda.state.notInstalled": + "Anaconda Desktop را روی این دستگاه نصب کنید، سپس به اینجا بازگردید. Kilo نصب‌کننده را برای شما اجرا نمی‌کند.", + "provider.anaconda.state.notRunning": + "Anaconda Desktop را باز کنید، راه‌اندازی را تکمیل کرده و وارد شوید، سپس «بررسی مجدد» را انتخاب کنید.", + "provider.anaconda.state.invalidConfig": + "راه‌اندازی Anaconda Desktop ناقص است. Desktop را باز کنید، راه‌اندازی را تکمیل کنید و در صورت نیاز آن را مجدداً راه‌اندازی کنید.", + "provider.anaconda.state.signedOut": "قبل از اتصال Kilo، Anaconda Desktop را باز کرده و وارد شوید.", + "provider.anaconda.state.unauthorized": + "Kilo نتوانست به Anaconda Desktop دسترسی پیدا کند. Desktop را باز کنید، دوباره وارد شوید و در صورت نیاز آن را مجدداً راه‌اندازی کنید.", + "provider.anaconda.state.unavailable": + "Anaconda Desktop هنوز پاسخ نمی‌دهد. آن را باز کنید و منتظر بمانید تا برنامه کاملاً راه‌اندازی شود.", + "provider.anaconda.state.noModel": + "در Anaconda Desktop، یک مدل تولید متن دانلود کنید. در صورت امکان مدلی با قابلیت فراخوانی ابزار انتخاب کنید، سپس سرور آن را راه‌اندازی کنید.", + "provider.anaconda.state.noServer_one": + "۱ مدل تولید متن دانلودشده در دسترس است. در Anaconda Desktop، یک سرور مدل راه‌اندازی کنید. مدل‌های دارای پشتیبانی از فراخوانی ابزار به شدت توصیه می‌شوند.", + "provider.anaconda.state.noServer_other": + "{{count}} مدل تولید متن دانلودشده در دسترس است. در Anaconda Desktop، یک سرور مدل راه‌اندازی کنید. مدل‌های دارای پشتیبانی از فراخوانی ابزار به شدت توصیه می‌شوند.", + "provider.anaconda.state.unhealthy": + "سرور استنتاج فعال هنوز سالم نیست. آن را در Anaconda Desktop بررسی کنید و در صورت نیاز سرور را مجدداً راه‌اندازی کنید.", + "provider.anaconda.state.ready": + "Kilo یک سرور تولید متن محلی سالم پیدا کرد و می‌تواند تنظیمات اتصال فعلی آن را وارد کند.", + "provider.anaconda.server": "سرور استنتاج فعال", + "provider.anaconda.context": "پنجره زمینه", + "provider.anaconda.contextValue": "{{count}} توکن", + "provider.anaconda.tools": "فراخوانی ابزار", + "provider.anaconda.tools.supported": "پشتیبانی می‌شود", + "provider.anaconda.tools.unsupported": "فعال نیست", + "provider.anaconda.tools.unknown": "نامشخص", + "provider.anaconda.warning.title": "پشتیبانی از ابزار محدود است", + "provider.anaconda.warning.description": + "این سرور فراخوانی ابزار را تأیید نمی‌کند. اقدامات عامل کدنویسی ممکن است با شکست مواجه شوند یا در دسترس نباشند. تنها در صورت پذیرش این محدودیت‌ها ادامه دهید.", + "provider.anaconda.action.download": "دانلود Anaconda Desktop", + "provider.anaconda.action.open": "باز کردن Anaconda Desktop", + "provider.anaconda.action.checkAgain": "بررسی مجدد", + "provider.anaconda.action.continue": "ادامه به هر حال", + "provider.anaconda.action.manage": "مدیریت / بازنشانی", + "provider.anaconda.toast.refreshed.title": "Anaconda Desktop بازنشانی شد", + "provider.anaconda.toast.refreshed.description": "سرور محلی فعال و مدل‌ها در Kilo به‌روز هستند.", + "settings.providers.note.anacondaDesktop": "یک مدل ارائه‌شده به‌صورت محلی توسط Anaconda Desktop را اجرا کنید.", + "settings.providers.tag.local": "محلی", +} as const + +export const dict = { + ...anacondaDesktopDict, + + "command.provider.connect": "اتصال به ارائه‌دهنده", + + "command.session.new": "جلسه جدید", + "command.session.show.changes": "نمایش تغییرات", + "command.review.toggle": "نمایش/پنهان کردن بررسی", + "revert.banner.count_one": "{{count}} پیام بازگردانده شد", + "revert.banner.count_other": "{{count}} پیام بازگردانده شد", + "revert.banner.redo": "بازانجام", + "revert.banner.redo.all": "بازانجام همه", + "revert.banner.hint": "می‌توانید این تغییرات را تا زمانی که پیام جدیدی ارسال نکرده‌اید بازانجام دهید", + "revert.banner.workspace.snapshotsDisabled": + "مکالمه بازگردانده شد. تغییرات فایل بازیابی نشدند زیرا عکس‌برداری غیرفعال است.", + "revert.banner.workspace.unavailable": + "مکالمه بازگردانده شد. هیچ نقطه بازیابی فایلی موجود نبود، بنابراین تغییرات فضای کاری بازیابی نشدند.", + "revert.banner.workspace.legacy": + "مکالمه بازگردانده شد. وضعیت بازیابی فضای کاری برای این بازگردانی قدیمی‌تر در دسترس نیست.", + "revert.banner.workspace.enableSnapshots": "فعال‌سازی اسنپ‌شات‌ها", + "revert.disabled.agentBusy": "منتظر بمانید تا عامل کارش تمام شود", + "command.session.compact": "فشرده‌سازی جلسه", + "command.session.export": "صدور رونوشت جلسه", + + "agentRequirements.skill.installed": "نصب شده", + "agentRequirements.skill.checkFailed": "بررسی مهارت ناموفق بود", + "agentRequirements.skill.missing": "نصب نشده", + "agentRequirements.mcp.connected": "متصل", + "agentRequirements.mcp.checkFailed": "بررسی MCP ناموفق بود", + "agentRequirements.mcp.missing": "متصل نیست", + "agentRequirements.extension.installed": "نصب شده", + "agentRequirements.extension.checkFailed": "بررسی افزونه VS Code ناموفق بود", + "agentRequirements.extension.missing": "نصب نشده", + "agentRequirements.extension.description": "افزونه‌های مفقود را در VS Code نصب کنید.", + "agentRequirements.group.skills": "مهارت‌ها", + "agentRequirements.group.mcps": "MCPs", + "agentRequirements.group.extensions": "افزونه‌های VS Code", + "agentRequirements.blocked.title": "پیش‌نیازهای عامل {{agent}}", + "agentRequirements.blocked.description": "این عامل پیش از اجرا به ابزارهای زیر نیاز دارد.", + "agentRequirements.prompt.blocked": "ابتدا بررسی‌های لازم را تکمیل کنید تا بتوانید از این عامل استفاده کنید", + "agentRequirements.action.openMarketplace": "باز کردن Marketplace", + "agentRequirements.error.unknownAgent": "عامل انتخاب‌شده یافت نشد.", + "agentRequirements.error.malformedDeclaration": "این عامل دارای اعلان پیش‌نیاز نامعتبر است.", + "agentRequirements.error.discoveryFailed": "Kilo نتوانست مهارت‌های موجود را بررسی کند.", + "agentRequirements.error.mcpStatusFailed": "Kilo نتوانست وضعیت سرور MCP را بررسی کند.", + "agentRequirements.error.scopeMismatch": "این بررسی نیازمندی‌های عامل دیگر فعال نیست.", + "agentRequirements.error.requestFailed": "Kilo نتوانست نیازمندی‌های عامل را بررسی کند.", + + "dialog.provider.search.placeholder": "جستجوی ارائه‌دهندگان", + "dialog.provider.empty": "ارائه‌دهنده‌ای یافت نشد", + "dialog.provider.group.other": "سایر", + "dialog.provider.tag.recommended": "پیشنهادی", + + "dialog.model.select.title": "انتخاب مدل", + "dialog.model.search.placeholder": "جستجوی مدل‌ها", + "dialog.model.empty": "نتیجه‌ای یافت نشد", + "dialog.model.select": "انتخاب", + "dialog.model.expand": "گسترش", + "dialog.model.collapse": "جمع‌کردن", + + "dialog.provider.viewAll": "نمایش ارائه‌دهندگان بیشتر", + + "provider.connect.title": "اتصال به {{provider}}", + "provider.connect.selectMethod": "روش ورود را برای {{provider}} انتخاب کنید.", + "provider.connect.method.apiKey": "کلید API", + "provider.connect.status.inProgress": "در حال احراز هویت...", + "provider.connect.status.waiting": "در انتظار احراز هویت...", + "provider.connect.status.failed": "احراز هویت ناموفق بود: {{error}}", + "provider.connect.apiKey.description": + "کلید API {{provider}} خود را وارد کنید تا حساب‌تان متصل شود و از مدل‌های {{provider}} در Kilo استفاده کنید.", + "provider.connect.apiKey.description.local": + "به سرور محلی {{provider}} خود متصل شوید. اگر سرور نیازی به کلید API ندارد، آن را خالی بگذارید (پیش‌فرض برای localhost).", + "provider.connect.atomicChat.description": + "به Atomic Chat روی دستگاه خود متصل شوید (پیش‌فرض http://127.0.0.1:1337). برای سرور محلی نیازی به کلید API نیست — Atomic Chat را راه‌اندازی کنید، یک مدل بارگذاری کنید، سپس متصل شوید.", + "provider.connect.apiKey.label": "کلید API {{provider}}", + "provider.connect.apiKey.label.optional": "کلید API {{provider}} (اختیاری)", + "provider.connect.apiKey.placeholder": "کلید API", + "provider.connect.apiKey.placeholder.optional": "برای سرور محلی خالی بگذارید", + "provider.connect.apiKey.required": "کلید API الزامی است", + "provider.connect.prompt.required": "{{field}} الزامی است", + "provider.connect.azure.endpointType.label": "پیکربندی endpoint Azure را انتخاب کنید", + "provider.connect.azure.endpointType.resourceName.label": "نام منبع", + "provider.connect.azure.endpointType.resourceName.hint": "نقطه پایانی را از نام منبع Azure خود بسازید", + "provider.connect.azure.endpointType.baseURL.label": "URL کامل نقطه پایانی", + "provider.connect.azure.endpointType.baseURL.hint": "از یک نقطه پایانی سفارشی Azure OpenAI استفاده کنید", + "provider.connect.azure.resourceName.label": "نام منبع Azure", + "provider.connect.azure.resourceName.placeholder": "مثلاً my-models", + "provider.connect.azure.baseURL.label": "URL نقطه پایانی Azure OpenAI", + "provider.connect.azure.baseURL.placeholder": "مثلاً https://my-models.openai.azure.com/openai", + "provider.connect.oauth.code.visit.prefix": "به ", + "provider.connect.oauth.code.visit.link": "این لینک", + "provider.connect.oauth.code.visit.suffix": + " مراجعه کنید تا کد مجوز خود را دریافت کرده، حساب خود را متصل کنید و از مدل‌های {{provider}} در Kilo استفاده کنید.", + "provider.connect.oauth.code.label": "کد مجوز {{method}}", + "provider.connect.oauth.code.placeholder": "کد مجوز", + "provider.connect.oauth.code.required": "کد مجوز الزامی است", + "provider.connect.oauth.auto.visit.prefix": "به ", + "provider.connect.oauth.auto.visit.link": "این لینک", + "provider.connect.oauth.auto.visit.suffix": + " مراجعه کنید و کد زیر را وارد کنید تا حساب خود را متصل کرده و از مدل‌های {{provider}} در Kilo استفاده کنید.", + "provider.connect.oauth.auto.confirmationCode": "کد تأیید", + "provider.connect.toast.connected.title": "{{provider}} متصل شد", + "provider.connect.toast.connected.description": "مدل‌های {{provider}} اکنون در دسترس هستند.", + + "provider.disconnect.toast.disconnected.title": "{{provider}} قطع شد", + "provider.disconnect.toast.disconnected.description": "مدل‌های {{provider}} دیگر در دسترس نیستند.", + + "model.tag.free": "رایگان", + "model.tag.dataCollected": "ممکن است داده‌ها برای آموزش استفاده شوند", + "model.group.auto": "مدل‌های خودکار", + "model.group.recommended": "پیشنهادی", + "model.group.favorites": "موردعلاقه‌ها", + "model.favorite.add": "افزودن به موردعلاقه‌ها", + "model.favorite.remove": "حذف از موردعلاقه‌ها", + "model.preview.label.released": "منتشر شده", + "model.preview.label.input": "ورودی", + "model.preview.label.output": "خروجی", + "model.preview.label.cached": "کش شده", + "model.preview.label.average": "هزینه تخمینی میانگین", + "model.preview.label.context": "زمینه", + "model.preview.group.terminalBench": "Terminal Bench 2.0", + "model.preview.group.autoEfficientChoices": "انتخاب‌های مدل", + "model.preview.label.completion": "تکمیل", + "model.preview.label.costAttempt": "هزینه / تلاش", + "model.preview.value.notSupported": "پشتیبانی نمی‌شود", + "model.preview.tooltip.average": + "میانگین هزینه تخمینی بر اساس نسبت معمول توکن‌های ورودی، خروجی و خواندن کش محاسبه می‌شود.", + "model.preview.badge.reasoning": "استدلال", + "model.preview.modality.text": "متن", + "model.preview.modality.image": "تصاویر", + "model.preview.modality.audio": "صدا", + "model.preview.modality.video": "ویدیو", + "model.preview.modality.pdf": "PDF", + + "common.goBack": "بازگشت", + "common.loading": "در حال بارگذاری", + "common.cancel": "لغو", + "common.connect": "اتصال", + "common.disconnect": "قطع اتصال", + "common.submit": "ارسال", + "common.save": "ذخیره", + "common.saving": "در حال ذخیره...", + "common.default": "پیش‌فرض", + + "prompt.thinking.tooltip": "میزان استدلال", + "prompt.action.send": "ارسال", + "prompt.action.send.blocked": "ابتدا به سؤال در انتظار پاسخ دهید یا آن را رد کنید", + "prompt.action.send.recording": "رونویسی و ارسال", + "prompt.action.stop": "توقف", + "prompt.action.enhance": "بهبود پرامپت", + "prompt.action.indexing": "تنظیمات ایندکس‌گذاری", + "prompt.action.autoApprove.enable": "فعال‌سازی تأیید خودکار", + "prompt.action.autoApprove.disable": "غیرفعال‌سازی تأیید خودکار", + "prompt.action.autoApprove.enabled": "تأیید خودکار فعال است. درخواست‌های مجوز به‌صورت خودکار تأیید می‌شوند.", + "prompt.action.autoApprove.disabled": "تأیید خودکار غیرفعال است. برای تأیید خودکار درخواست‌های مجوز کلیک کنید.", + "prompt.action.sandbox.enable": "فعال‌سازی سندباکس", + "prompt.action.sandbox.disable": "غیرفعال‌سازی سندباکس", + "prompt.action.sandbox.enabled": "سندباکس فعال است. دستورات شل عامل به پوشه‌های پروژه و Kilo محدود شده‌اند.", + "prompt.action.sandbox.disabled": + "سندباکس غیرفعال است. برای محدود کردن نوشتن دستورات شل عامل به پوشه‌های پروژه و Kilo کلیک کنید.", + "prompt.action.sandbox.status.enabled": "Sandbox فعال", + "prompt.action.sandbox.status.disabled": "Sandbox غیرفعال", + "prompt.action.sandbox.filesystem": "سیستم فایل", + "prompt.action.sandbox.network": "شبکه", + "prompt.action.sandbox.filesystem.restricted": "محدود", + "prompt.action.sandbox.network.blocked": "مسدود", + "prompt.action.sandbox.network.allowed": "مجاز", + "prompt.action.sandbox.unrestricted": "بدون محدودیت", + "prompt.action.sandbox.description.enabled": "نوشتن‌ها به پوشه‌های پروژه و Kilo محدود شده‌اند.", + "prompt.action.sandbox.description.disabled": "برای محدود کردن نوشتن در سیستم فایل و دسترسی به شبکه کلیک کنید.", + "prompt.action.sandbox.description.disabledNetworkAllowed": + "برای محدود کردن نوشتن در سیستم فایل کلیک کنید. دسترسی به شبکه طبق تنظیمات sandbox شما مجاز است.", + "prompt.action.resetModel": "بازنشانی مدل به حالت پیش‌فرض", + "prompt.action.enhanceDescription": + "دکمه «بهبود پرامپت» با ارائه زمینه بیشتر، توضیح یا بازنویسی، به بهتر کردن پرامپت شما کمک می‌کند. یک پرامپت تایپ کنید و دوباره روی دکمه کلیک کنید تا نحوه عملکرد آن را ببینید.", + "speechToText.tooltip.start": "شروع ورودی صوتی با Kilo Gateway", + "speechToText.tooltip.starting": "در حال راه‌اندازی میکروفون... منتظر بمانید.", + "speechToText.tooltip.stop": "در حال ضبط. برای توقف کلیک کنید.", + "speechToText.tooltip.transcribing": "در حال رونویسی... برای لغو کلیک کنید.", + "speechToText.tooltip.error": "ورودی صوتی ناموفق بود. برای پاک کردن کلیک کنید.", + "speechToText.error.title": "ورودی صوتی ناموفق بود", + "speechToText.error.loginRequired": "برای استفاده از ورودی صوتی وارد Kilo شوید.", + "speechToText.error.emptyTranscript": "هیچ گفتاری شناسایی نشد.", + + "prompt.toast.promptSendFailed.title": "ارسال پرامپت ناموفق بود", + + "mcp.status.connected": "متصل", + "mcp.status.failed": "ناموفق", + "mcp.status.needs_auth": "نیاز به احراز هویت", + "mcp.status.needs_registration": "نیاز به ثبت کلاینت", + "mcp.status.disabled": "غیرفعال", + + "toast.session.rename.invalid.title": "عنوان جلسه نامعتبر است", + + "error.startup.title": "اتصال به سرور ناموفق بود", + + "error.paidModel.title": "برای استفاده از این مدل باید وارد شوید", + "error.paidModel.description": + "برای دسترسی به بیش از ۵۰۰ مدل، استفاده از اعتبار با هزینه واقعی یا استفاده از کلید خودتان، وارد شوید یا حساب کاربری بسازید.", + "error.paidModel.action": "ورود", + "error.promotionLimit.title": "برای ادامه باید ثبت‌نام کنید", + "error.promotionLimit.description": + "برای ادامه و دسترسی به ۵۰۰ مدل دیگر، رایگان ثبت‌نام کنید. تنها ۲ دقیقه طول می‌کشد و نیازی به کارت اعتباری نیست. یا بعداً برگردید.", + "error.promotionLimit.action": "ثبت‌نام", + "error.providerAuth.title": "{{provider}} شما را خارج کرد", + "error.providerAuth.description": "{{provider}} را دوباره متصل کنید، سپس پیام خود را مجدداً ارسال کنید.", + "error.providerAuth.chatgpt.title": "OpenAI شما را خارج کرد", + "error.providerAuth.chatgpt.description": + "دوباره با ChatGPT وارد شوید، سپس پیام خود را مجدداً ارسال کنید تا از مدل‌های Codex استفاده کنید.", + + "notification.permission.title": "مجوز لازم است", + "notification.permission.titleSubagent": "مجوز مورد نیاز است (زیرعامل)", + "ui.permission.manageAutoApprove": "مدیریت قوانین تأیید خودکار", + "ui.permission.doomLoop.prompt": "حلقه احتمالی برای ابزار {{tool}} شناسایی شد. ادامه می‌دهید؟", + "ui.permission.doomLoop.rule": "ادامه فراخوانی‌های {{tool}}", + "ui.permission.rule.addToAllowed": "افزودن به لیست مجاز", + "ui.permission.rule.removeFromAllowed": "حذف از لیست مجاز", + "ui.permission.rule.addToDenied": "افزودن به لیست مسدود", + "ui.permission.rule.removeFromDenied": "حذف از لیست مسدود", + "ui.permission.toolLabel.read": "خواندن", + "ui.permission.toolLabel.edit": "ویرایش", + "ui.permission.toolLabel.write": "نوشتن", + "ui.permission.toolLabel.patch": "وصله", + "ui.permission.toolLabel.globSearch": "جستجوی Glob", + "ui.permission.toolLabel.grepSearch": "جستجوی Grep", + "ui.permission.toolLabel.webSearch": "جستجوی وب", + "ui.permission.toolLabel.list": "فهرست", + "ui.permission.toolLabel.externalDirectory": "دسترسی به پوشه خارجی", + "ui.permission.toolLabel.webFetch": "دریافت از وب", + "ui.permission.toolLabel.task": "وظیفه", + "ui.permission.toolLabel.skill": "مهارت", + "ui.permission.toolLabel.lsp": "LSP", + "ui.permission.toolLabel.bash": "Bash", + "ui.permission.toolLabel.todoRead": "خواندن Todo", + "ui.permission.toolLabel.todoWrite": "نوشتن Todo", + "ui.permission.toolLabel.codeSearch": "جستجوی کد", + "ui.permission.copyCommand": "کپی", + "ui.approval.auto": "تأیید خودکار", + "ui.approval.manual": "تأیید شده توسط شما", + "ui.approval.rule": "با قانون `{{pattern}}` از `{{permission}}` مطابقت داشت", + "ui.approval.source.agent": "توسط عامل {{agent}}", + "ui.approval.source.agent.default": "توسط عامل", + "ui.approval.source.global": "توسط تنظیمات سراسری شما", + "ui.approval.source.project": "توسط تنظیمات پروژه", + "ui.approval.source.yolo": "توسط حالت تأیید خودکار (YOLO)", + "ui.approval.source.session": "توسط قانون تأیید خودکار جلسه", + "ui.approval.source.default": "به‌طور پیش‌فرض", + + "session.tab.review": "بررسی", + "session.review.filesChanged": "{{count}} فایل تغییر یافته", + "session.review.change.other": "تغییرات", + "session.review.loadingChanges": "در حال بارگذاری تغییرات...", + "session.review.noChanges": "بدون تغییر", + + "session.messages.loadingEarlier": "در حال بارگذاری پیام‌های قبلی...", + "session.messages.loadEarlier": "بارگذاری پیام‌های قبلی", + "session.messages.loading": "در حال بارگذاری پیام‌ها...", + + "common.closeTab": "بستن برگه", + "common.signIn": "ورود", + "common.dismiss": "رد کردن", + "common.requestFailed": "درخواست ناموفق بود", + "common.rename": "تغییر نام", + "common.delete": "حذف", + "common.close": "بستن", + "common.edit": "ویرایش", + "common.loadMore": "بارگذاری بیشتر", + + "sidebar.settings": "تنظیمات", + + "sound.option.alert01": "هشدار ۰۱", + "sound.option.alert02": "هشدار ۰۲", + "sound.option.alert03": "هشدار ۰۳", + "sound.option.alert04": "هشدار ۰۴", + "sound.option.alert05": "هشدار ۰۵", + "sound.option.alert06": "هشدار ۰۶", + "sound.option.alert07": "هشدار ۰۷", + "sound.option.alert08": "هشدار ۰۸", + "sound.option.alert09": "هشدار ۰۹", + "sound.option.alert10": "هشدار ۱۰", + "sound.option.bipbop01": "بیپ-باپ ۰۱", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Nope 01", + "sound.option.nope02": "Nope 02", + "sound.option.nope03": "Nope 03", + "sound.option.nope04": "Nope 04", + "sound.option.nope05": "نه ۰۵", + "sound.option.nope06": "نه ۰۶", + "sound.option.nope07": "نه ۰۷", + "sound.option.nope08": "نه ۰۸", + "sound.option.nope09": "نه ۰۹", + "sound.option.nope10": "نه ۱۰", + "sound.option.nope11": "نه ۱۱", + "sound.option.nope12": "نه ۱۲", + "sound.option.yup01": "بله ۰۱", + "sound.option.yup02": "بله ۰۲", + "sound.option.yup03": "Yup 03", + "sound.option.yup04": "Yup 04", + "sound.option.yup05": "Yup 05", + "sound.option.yup06": "Yup 06", + + "settings.providers.title": "ارائه‌دهندگان", + "settings.providers.section.connected": "ارائه‌دهندگان متصل", + "settings.providers.connected.empty": "هیچ ارائه‌دهنده متصلی وجود ندارد", + "settings.providers.section.popular": "ارائه‌دهندگان محبوب", + "settings.providers.tag.gateway": "Gateway", + "settings.providers.tag.environment": "محیط", + "settings.providers.tag.config": "پیکربندی", + "settings.providers.tag.chatgpt": "ChatGPT", + "settings.providers.tag.custom": "سفارشی", + "settings.providers.tag.customProvider": "ارائه‌دهنده سفارشی", + "settings.providers.tag.other": "سایر", + "settings.providers.connected.environmentDescription": "از متغیرهای محیطی شما متصل شده است", + "settings.providers.action.signInChatGPT": "ورود با ChatGPT", + "settings.providers.custom.description": "یک ارائه‌دهنده سفارشی از طریق URL پایه اضافه کنید.", + + "provider.custom.title": "ارائه‌دهنده سفارشی", + "provider.custom.description.prefix": "یک ارائه‌دهنده سفارشی پیکربندی کنید. به ", + "provider.custom.description.link": "مستندات پیکربندی ارائه‌دهنده", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "شناسه ارائه‌دهنده", + "provider.custom.field.providerID.placeholder": "myprovider", + "provider.custom.field.providerID.description": "حروف کوچک، اعداد، خط تیره یا زیرخط", + "provider.custom.field.name.label": "نام نمایشی", + "provider.custom.field.name.placeholder": "ارائه‌دهنده هوش مصنوعی من", + "provider.custom.field.package.label": "API ارائه‌دهنده", + "provider.custom.field.baseURL.label": "URL پایه", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "کلید API", + "provider.custom.field.apiKey.placeholder": "کلید API", + "provider.custom.field.apiKey.description": "اختیاری. اگر احراز هویت را از طریق هدرها مدیریت می‌کنید، خالی بگذارید.", + "provider.custom.models.label": "مدل‌ها", + "provider.custom.models.id.label": "شناسه", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "نام", + "provider.custom.models.name.placeholder": "نام نمایشی", + "provider.custom.models.reasoning.label": "استدلال", + "provider.custom.models.modalities.image": "تصویر", + "provider.custom.models.variants.label": "نسخه‌های متغیر", + "provider.custom.models.variants.add": "افزودن نسخه متغیر", + "provider.custom.models.variants.remove": "حذف نسخه متغیر", + "provider.custom.models.variants.name.label": "نام", + "provider.custom.models.variants.name.placeholder": "مثلاً thinking", + "provider.custom.models.variants.option.unset": "(تنظیم نشده)", + "provider.custom.models.variants.enableThinking.label": "فعال‌سازی تفکر (مثلاً Alibaba)", + "provider.custom.models.variants.enableThinking.placeholder": "enable_thinking", + "provider.custom.models.variants.enableThinking.true": "true", + "provider.custom.models.variants.enableThinking.false": "false", + "provider.custom.models.variants.thinking.label": "نوع تفکر (مثلاً Z.ai)", + "provider.custom.models.variants.thinking.placeholder": "thinking", + "provider.custom.models.variants.thinking.enabled": "فعال", + "provider.custom.models.variants.thinking.disabled": "غیرفعال", + "provider.custom.models.variants.thinking.adaptive": "تطبیقی", + "provider.custom.models.variants.splitReasoning.label": "تقسیم استدلال (لازم برای مثلاً MiniMax)", + "provider.custom.models.variants.splitReasoning.placeholder": "reasoning_split", + "provider.custom.models.variants.splitReasoning.true": "true", + "provider.custom.models.variants.splitReasoning.false": "false", + "provider.custom.models.variants.chatTemplateArgs.label": + "فعال‌سازی تفکر از طریق آرگومان‌های قالب چت (مثلاً Hugging Face)", + "provider.custom.models.variants.chatTemplateArgs.placeholder": "chat_template_args", + "provider.custom.models.variants.chatTemplateArgs.true": "true", + "provider.custom.models.variants.chatTemplateArgs.false": "false", + "provider.custom.models.variants.reasoningEffort.label": "سطح استدلال", + "provider.custom.models.variants.reasoningEffort.placeholder": "reasoningEffort", + "provider.custom.models.variants.reasoningEffort.none": "هیچ", + "provider.custom.models.variants.reasoningEffort.minimal": "حداقل", + "provider.custom.models.variants.reasoningEffort.low": "کم", + "provider.custom.models.variants.reasoningEffort.medium": "متوسط", + "provider.custom.models.variants.reasoningEffort.high": "زیاد", + "provider.custom.models.variants.reasoningEffort.xhigh": "xhigh", + "provider.custom.models.variants.outputEffort.label": "تلاش خروجی (مثلاً Anthropic)", + "provider.custom.models.variants.outputEffort.placeholder": "تلاش", + "provider.custom.models.variants.outputEffort.low": "کم", + "provider.custom.models.variants.outputEffort.medium": "متوسط", + "provider.custom.models.variants.outputEffort.high": "زیاد", + "provider.custom.models.variants.outputEffort.xhigh": "xhigh", + "provider.custom.models.variants.outputEffort.max": "حداکثر", + "provider.custom.models.remove": "حذف مدل", + "provider.custom.models.add": "افزودن مدل", + "provider.custom.models.fetch.authError": "احراز هویت ناموفق بود. کلید API بالا را بررسی کرده و دوباره امتحان کنید.", + "provider.custom.models.fetch.empty": "هیچ مدلی در این سرور یافت نشد.", + "provider.custom.models.fetch.added": "{{count}} مدل اضافه شد.", + "provider.custom.models.fetch.allExist": "تمام مدل‌های دریافت‌شده قبلاً اضافه شده‌اند.", + "provider.custom.models.fetch.selectAll": "انتخاب همه", + "provider.custom.models.fetch.deselectAll": "لغو انتخاب همه", + "provider.custom.models.fetch.found": "{{count}} مدل یافت شد", + "provider.custom.models.fetch.showing": "نمایش {{shown}} از {{total}}", + "provider.custom.models.fetch.search": "جستجوی مدل‌ها…", + "provider.custom.models.fetch.add": "افزودن {{count}} مدل", + "provider.custom.edit.title": "ویرایش ارائه‌دهنده", + "provider.custom.headers.label": "هدرها (اختیاری)", + "provider.custom.headers.key.label": "هدر", + "provider.custom.headers.key.placeholder": "Header-Name", + "provider.custom.headers.value.label": "مقدار", + "provider.custom.headers.value.placeholder": "value", + "provider.custom.headers.remove": "حذف هدر", + "provider.custom.headers.add": "افزودن هدر", + "provider.custom.error.providerID.required": "شناسه ارائه‌دهنده الزامی است", + "provider.custom.error.providerID.format": "از حروف کوچک، اعداد، خط تیره یا زیرخط استفاده کنید", + "provider.custom.error.providerID.exists": "این شناسه ارائه‌دهنده از قبل وجود دارد", + "provider.custom.error.name.required": "نام نمایشی الزامی است", + "provider.custom.error.baseURL.required": "URL پایه الزامی است", + "provider.custom.error.baseURL.format": "باید با http:// یا https:// شروع شود", + "provider.custom.error.required": "الزامی", + "provider.custom.error.duplicate": "تکراری", + "settings.openLocalConfig": "پیکربندی محلی", + "settings.openGlobalConfig": "پیکربندی سراسری", + "settings.config.scope.local": "محلی", + "settings.config.scope.global": "سراسری", + "settings.config.status.loaded": "بارگذاری شد", + "settings.config.status.loadedLegacy": "پیکربندی قدیمی بارگذاری شد", + "settings.config.status.notLoaded": "بارگذاری نشد", + "settings.config.status.create": "یافت نشد - این فایل را ایجاد کنید", + "settings.config.title": "باز کردن فایل پیکربندی {{scope}} Kilo", + "settings.config.placeholder": + "فایل‌های پیکربندی به ترتیب ادغام می‌شوند؛ فایل‌های علامت‌گذاری‌شده به‌عنوان بارگذاری‌شده در حال حاضر روی تنظیمات تأثیر می‌گذارند.", + "settings.config.noWorkspace": "یک پوشه کاری باز کنید تا فایل پیکربندی محلی Kilo را ویرایش کنید.", + "settings.config.openFailed": "باز کردن فایل پیکربندی {{scope}} Kilo با شکست مواجه شد: {{message}}", + "settings.config.source.xdg": "پیکربندی سراسری XDG", + "settings.config.source.homeKilo": "پیکربندی .kilo در پوشه خانگی", + "settings.config.source.homeKilocode": "پیکربندی .kilocode در پوشه خانگی", + "settings.config.source.homeOpencode": "پیکربندی .opencode در پوشه خانگی", + "settings.config.source.envFile": "فایل محیطی KILO_CONFIG", + "settings.config.source.envDir": "KILO_CONFIG_DIR", + "settings.config.source.envContent": "پیکربندی محیطی درون‌خطی", + "settings.config.source.projectKilo": "پیکربندی .kilo پروژه", + "settings.config.source.projectRoot": "پیکربندی ریشه پروژه", + "settings.config.source.projectKilocode": "پیکربندی قدیمی .kilocode", + "settings.config.source.projectOpencode": "پیکربندی قدیمی .opencode", + "settings.models.title": "مدل‌ها", + + "settings.permissions.toast.updateFailed.title": "به‌روزرسانی مجوزها ناموفق بود", + + "settings.permissions.tool.read.title": "خواندن", + "settings.permissions.tool.read.description": "خواندن یک فایل (با مسیر فایل تطابق دارد)", + "settings.permissions.tool.edit.title": "ویرایش", + "settings.permissions.tool.edit.description": "تغییر فایل‌ها، شامل ویرایش، نوشتن، وصله‌گذاری و ویرایش‌های چندگانه", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "تطبیق فایل‌ها با استفاده از الگوهای glob", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "جستجوی محتوای فایل‌ها با استفاده از عبارات منظم", + "settings.permissions.tool.list.title": "فهرست", + "settings.permissions.tool.list.description": "فهرست کردن فایل‌های درون یک پوشه", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "اجرای دستورات شل", + "settings.permissions.tool.task.title": "وظیفه", + "settings.permissions.tool.task.description": "راه‌اندازی زیر-عامل‌ها", + "settings.permissions.tool.skill.title": "مهارت", + "settings.permissions.tool.skill.description": "بارگذاری یک مهارت با نام", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "اجرای پرس‌وجوهای سرور زبان", + "settings.permissions.tool.todoread.title": "خواندن وظایف", + "settings.permissions.tool.todoread.description": "خواندن فهرست وظایف", + "settings.permissions.tool.todowrite.title": "نوشتن وظایف", + "settings.permissions.tool.todowrite.description": "به‌روزرسانی فهرست وظایف", + "settings.permissions.tool.webfetch.title": "دریافت وب", + "settings.permissions.tool.webfetch.description": "دریافت محتوا از یک URL", + "settings.permissions.tool.websearch.title": "جستجوی وب", + "settings.permissions.tool.websearch.description": "جستجو در وب", + "settings.permissions.tool.codesearch.title": "جستجوی کد", + "settings.permissions.tool.codesearch.description": "جستجوی کد در وب", + "settings.permissions.tool.external_directory.title": "پوشه خارجی", + "settings.permissions.tool.external_directory.description": "دسترسی به فایل‌های خارج از پوشه پروژه", + "settings.permissions.tool.doom_loop.title": "حلقه بی‌پایان", + "settings.permissions.tool.doom_loop.description": "تشخیص فراخوانی‌های تکراری ابزار با ورودی یکسان", + + "session.delete.title": "حذف جلسه", + "session.delete.confirm": "حذف جلسه «{{name}}»؟", + "session.delete.button": "حذف جلسه", + "session.untitled": "بدون عنوان", + "session.current": "جلسه فعلی", + "session.recent": "اخیر", + "session.showHistory": "نمایش تاریخچه", + "session.history.sources": "منبع تاریخچه", + "session.search.placeholder": "جستجوی جلسات...", + "session.empty": "هنوز جلسه‌ای وجود ندارد. برای شروع مکالمه جدید روی + کلیک کنید.", + "session.tabs.switcher.open": "نمایش تب‌های باز", + "session.tabs.switcher.search": "جستجو در تب‌های باز...", + "session.tabs.switcher.current": "فعلی", + "session.tabs.switcher.pending": "جدید", + "session.tabs.switcher.busy": "در حال کار", + "session.tab.local": "محلی", + "session.tab.cloud": "Cloud", + "session.tab.worktree": "Worktree", + "session.cloud.repoOnly": "فقط این مخزن", + "session.cloud.import": "وارد کردن جلسه", + "feedback.button": "بازخورد و پشتیبانی", + "feedback.dialog.message": "خوشحال می‌شویم نظرات شما را بشنویم یا در رفع مشکلاتتان کمک کنیم.", + "feedback.dialog.github": "گزارش مشکل در GitHub", + "feedback.dialog.discord": "پیوستن به جامعه Discord ما", + "feedback.dialog.support": "پشتیبانی مشتریان", + "workStyle.onboarding.welcome": "به Kilo خوش آمدید", + "workStyle.onboarding.title": "نحوه کار خود را انتخاب کنید", + "workStyle.onboarding.settingsNote": "می‌توانید این گزینه‌ها را هر زمان در", + "workStyle.onboarding.settings": "تنظیمات تغییر دهید.", + "workStyle.toast.saved.title": "حالت با موفقیت ذخیره شد", + "workStyle.choice.permissions": "مجوزها", + "workStyle.choice.bash": "Bash", + "workStyle.choice.visibility": "نمایان‌پذیری", + "workStyle.choice.human-in-the-loop.eyebrow": "انسان در حلقه", + "workStyle.choice.human-in-the-loop.title": "ابتدا بررسی کنید", + "workStyle.choice.human-in-the-loop.description": "Kilo در حین کار مکث می‌کند و برنامه خود را به شما نشان می‌دهد.", + "workStyle.choice.human-in-the-loop.permissions": "قبل از ویرایش فایل‌ها یا اجرای دستورات اجازه می‌گیرد.", + "workStyle.choice.human-in-the-loop.bash": "هنگام اجرای تمام دستورات ترمینال اجازه می‌گیرد.", + "workStyle.choice.human-in-the-loop.visibility": "جزئیات کامل مکالمه، از جمله استدلال، را نمایش می‌دهد.", + "workStyle.choice.autonomous.eyebrow": "وقفه‌های کمتر", + "workStyle.choice.autonomous.title": "استقلال بالا", + "workStyle.choice.autonomous.description": "وقفه‌های کمتر، رابط کاربری ساده‌تر.", + "workStyle.choice.autonomous.permissions": + "بدون درخواست اجازه، فایل‌ها را ویرایش می‌کند و دستورات را در فضای کاری اجرا می‌کند.", + "workStyle.choice.autonomous.bash": "می‌تواند بدون تأیید، دستورات ترمینال را در فضای کاری اجرا کند.", + "workStyle.choice.autonomous.visibility": "جزئیات تا زمانی که آن‌ها را باز کنید، جمع‌شده باقی می‌مانند.", + "session.cloud.import.title": "وارد کردن جلسه", + "session.cloud.import.placeholder": "شناسه جلسه، URL، یا دستور kilo import", + "session.cloud.import.button": "وارد کردن", + "session.cloud.import.invalid": "فرمت شناسه جلسه نامعتبر است", + "session.cloud.import.legacy": "به نظر می‌رسد این یک جلسه قدیمی است که دیگر پشتیبانی نمی‌شود.", + "session.cloud.import.failed": "وارد کردن جلسه ابری ناموفق بود", + + "deviceAuth.toast.urlCopied": "URL در کلیپ‌بورد کپی شد", + "deviceAuth.toast.codeCopied": "کد در کلیپ‌بورد کپی شد", + "deviceAuth.toast.errorCopied": "خطا در کلیپ‌بورد کپی شد", + "deviceAuth.status.initiating": "در حال شروع ورود...", + "deviceAuth.title": "ورود به Kilo Code", + "deviceAuth.step1": "مرحله ۱: این URL را باز کنید", + "deviceAuth.action.copyUrl": "کپی URL", + "deviceAuth.action.openBrowser": "باز کردن مرورگر", + "deviceAuth.qrCode.alt": "کد QR", + "deviceAuth.step2": "مرحله ۲: این کد را وارد کنید", + "deviceAuth.action.clickToCopy": "برای کپی کلیک کنید", + "deviceAuth.status.waiting": "در انتظار تأیید...", + "deviceAuth.status.success": "ورود موفقیت‌آمیز بود!", + "deviceAuth.status.failed": "ورود ناموفق بود", + "deviceAuth.status.cancelled": "ورود لغو شد", + "deviceAuth.action.copyError": "خطا در کپی", + "deviceAuth.action.showDetails": "مشاهده جزئیات", + "deviceAuth.action.tryAgain": "تلاش مجدد", + "deviceAuth.error.detailsTitle": "جزئیات خطای ورود", + + "common.retry": "تلاش مجدد", + "common.refresh": "بازخوانی", + "common.reload": "بارگذاری مجدد", + "common.reloadDescription": "بارگذاری مجدد تنظیمات، مهارت‌ها، عوامل و دستورات از دیسک", + + "profile.title": "پروفایل", + "profile.notLoggedIn": "وارد نشده‌اید", + "profile.action.login": "ورود با Kilo Code", + "profile.balance.title": "موجودی", + "profile.balance.refresh": "بازخوانی موجودی", + "profile.action.dashboard": "داشبورد", + "profile.action.topUp": "شارژ کردن", + "profile.pass.subscribe": "Kilo Pass را دریافت کنید تا اعتبار اضافه کنید و پاداش کسب کنید", + "profile.pass.bonus": "پاداش", + "profile.pass.renews": "تمدید می‌شود", + "profile.action.logout": "خروج", + + "settings.agentBehaviour.title": "رفتار عامل", + "settings.autoApprove.title": "تأیید خودکار", + "settings.browser.title": "مرورگر", + "settings.checkpoints.title": "نقاط بازیابی", + "settings.display.title": "نمایش", + "settings.autocomplete.title": "تکمیل خودکار", + "settings.notifications.title": "اعلان‌ها", + "settings.context.title": "زمینه", + "settings.indexing.title": "ایندکس‌گذاری", + "settings.indexing.status.title": "وضعیت", + "settings.indexing.enable.title": "فعال‌سازی ایندکس‌گذاری", + "settings.indexing.enable.description": "ایندکس‌گذاری معنایی پایگاه کد را روشن یا خاموش کنید.", + "settings.indexing.showButton.title": "نمایش دکمه هنگامی که ایندکس‌گذاری خاموش است", + "settings.indexing.showButton.description": + "دکمه ایندکس‌گذاری را در زیر پرامپت هنگامی که ایندکس‌گذاری خاموش است نشان دهید. در صورت پنهان بودن، برای فعال‌سازی ایندکس‌گذاری به تنظیمات > ایندکس‌گذاری بروید.", + "settings.indexing.globalEnable.title": "فعال‌سازی سراسری", + "settings.indexing.globalEnable.description": "فهرست‌سازی را برای همه فضاهای کاری فعال کنید.", + "settings.indexing.projectEnable.title": "فعال‌سازی برای این پروژه", + "settings.indexing.projectEnable.description": + "هنگامی که فهرست‌سازی سراسری غیرفعال است، فهرست‌سازی را برای این فضای کاری فعال کنید.", + "settings.indexing.provider.title": "ارائه‌دهنده جاسازی", + "settings.indexing.provider.description": + "ارائه‌دهنده مورد استفاده برای تولید جاسازی‌ها در جستجوی معنایی را انتخاب کنید.", + "settings.indexing.kiloModel.title": "پیش‌تنظیم مدل Kilo", + "settings.indexing.kiloModel.description": "یک مدل جاسازی میزبانی‌شده توسط Kilo را انتخاب کنید.", + "settings.indexing.kiloSignIn.title": "ورود به Kilo الزامی است", + "settings.indexing.kiloSignIn.description": "برای استفاده از جاسازی‌های میزبانی‌شده، وارد Kilo شوید.", + "settings.indexing.model.title": "مدل جاسازی", + "settings.indexing.model.description": "مدل جاسازی پیش‌فرض برای ارائه‌دهنده انتخاب‌شده را بازنویسی کنید.", + "settings.indexing.dimension.title": "بُعد برداری", + "settings.indexing.dimension.description": "برای تشخیص خودکار بُعد جاسازی از مدل، خالی بگذارید.", + "settings.indexing.dimension.placeholder": "خودکار", + "settings.indexing.providerField.description": "تنظیم اتصال مخصوص ارائه‌دهنده.", + "settings.indexing.vectorStore.title": "فروشگاه برداری", + "settings.indexing.vectorStore.description": "محل ذخیره‌سازی جاسازی‌های ایندکس‌شده را انتخاب کنید.", + "settings.indexing.lancedbDirectory.title": "پوشه LanceDB", + "settings.indexing.lancedbDirectory.description": "پوشه اختیاری برای ذخیره‌گاه محلی LanceDB.", + "settings.indexing.lancedbDirectory.placeholder": "برای پیش‌فرض خالی بگذارید", + "settings.indexing.qdrantUrl.title": "آدرس URL Qdrant", + "settings.indexing.qdrantUrl.description": "آدرس سرور برای نمونه Qdrant.", + "settings.indexing.qdrantApiKey.title": "کلید API Qdrant", + "settings.indexing.qdrantApiKey.description": "کلید API اختیاری برای نمونه Qdrant.", + "settings.indexing.qdrantApiKey.placeholder": "کلید API اختیاری", + "settings.indexing.fileExtensions.title": "پسوندهای فایل", + "settings.indexing.fileExtensions.description": + "فهرست مجاز با جداکننده کاما. برای استفاده از پیش‌فرض‌های داخلی خالی بگذارید.", + "settings.indexing.fileExtensions.invalid": "پسوند نامعتبر: {{extension}}", + "settings.indexing.tuning.description": "پارامتر پیشرفته جستجو و دسته‌بندی.", + "settings.experimental.title": "آزمایشی", + "settings.language.title": "زبان", + "settings.aboutKiloCode.title": "درباره Kilo Code", + + "session.messages.welcome": + "Kilo Code یک دستیار هوش مصنوعی برای کدنویسی است. از آن بخواهید ویژگی‌ها بسازد، باگ‌ها را رفع کند یا کدبیس شما را توضیح دهد.", + "session.messages.scrollToBottom": "رفتن به پایین", + "session.messages.initializing": "در حال راه‌اندازی...", + "session.messages.taskStarting": "در حال شروع...", + "session.prompts.navLabel": "ناوبر پرامپت", + "session.prompts.tick": "پرامپت {{index}} از {{total}}: {{prompt}}", + "session.prompts.noAnswer": "هنوز پاسخی وجود ندارد", + "session.prompts.queued": "در صف انتظار", + "session.status.writingResponse": "در حال نوشتن پاسخ...", + "session.status.retry": "در حال تلاش مجدد…", + "session.status.working": "در حال پردازش...", + "session.status.offline": "اتصال شبکه قطع شد — در حال اتصال مجدد...", + "session.outcome.incomplete": "نوبت با {{count}} کار باقی‌مانده پایان یافت.", + "session.outcome.limit": "پاسخ به محدودیت خروجی رسید و ممکن است ناقص باشد.", + "session.outcome.unknown": "پاسخ بدون دلیل پایان یافت و ممکن است ناقص باشد.", + "session.outcome.filtered": "ارائه‌دهنده این پاسخ را به دلیل فیلتر محتوا متوقف کرد.", + "session.outcome.unexpected": "پاسخ به‌طور غیرمنتظره‌ای پایان یافت و ممکن است ناقص باشد.", + "session.outcome.generationId": "شناسه تولید: {{id}}", + "session.outcome.interrupted": "نوبت قطع شد.", + "session.outcome.error": "نوبت با شکست مواجه شد.", + "session.outcome.finish": "دلیل فنی پایان: {{reason}}", + "session.costAlert.header": "هشدار هزینه جلسه", + "session.costAlert.continue": "ادامه", + "session.costAlert.question": "هزینه این جلسه از آستانه هشدار {{limit}} شما فراتر رفت و {{cost}} شد. ادامه می‌دهید؟", + "session.costAlert.stop": "توقف", + "sidebar.session.newSession": "جلسه جدید", + "sidebar.session.newSession.tooltip": "یک مکالمه تازه شروع کنید و جلسه فعلی را دست‌نخورده نگه دارید.", + "sidebar.session.newWorktree": "Worktree جدید", + "sidebar.session.newWorktree.tooltip": + "یک worktree گیت ایزوله ایجاد کنید تا به‌صورت امن آزمایش کنید، تغییرات را جدا نگه دارید و بدون اختلال در شاخه فعلی، جلسات موازی اجرا کنید.", + "sidebar.session.configureWorktree.tooltip": + "برای پیکربندی یک worktree جدید قبل از ایجاد آن، دیالوگ worktree در Agent Manager را باز کنید.", + "sidebar.session.newWorktree.from": "Worktree جدید از", + "sidebar.session.currentBranch": "شاخه فعلی", + "sidebar.session.moveToWorktree": "انتقال به Worktree", + "sidebar.session.moveToWorktree.tooltip.empty": + "این مکالمه و تغییرات محلی فعلی شما را به یک worktree اختصاصی برای کار پیگیری ایزوله منتقل کنید.", + "sidebar.session.moveToWorktree.tooltip.one": + "این مکالمه و ۱ فایل تغییریافته را به یک worktree اختصاصی برای کار پیگیری ایزوله منتقل کنید.", + "sidebar.session.moveToWorktree.tooltip.other": + "این مکالمه و {{files}} فایل تغییریافته را به یک worktree اختصاصی برای کار پیگیری ایزوله منتقل کنید.", + "sidebar.session.showChanges.tooltip.empty": "نمای تغییرات را برای بررسی working tree فعلی باز کنید.", + "sidebar.session.progress.capturing": "در حال ثبت تغییرات...", + "sidebar.session.progress.creating": "در حال ایجاد worktree...", + "sidebar.session.progress.setup": "در حال اجرای راه‌اندازی...", + "sidebar.session.progress.transferring": "در حال انتقال تغییرات...", + "sidebar.session.progress.forking": "در حال شروع جلسه...", + "sidebar.session.progress.failed": "ادامه در worktree با شکست مواجه شد", + + "ui.sessionTurn.cancel": "لغو", + "ui.sessionTurn.status.thinking": "در حال فکر کردن...", + "ui.sessionTurn.status.consideringNextSteps": "در حال بررسی مراحل بعدی...", + + "dialog.model.noProviders": "هیچ ارائه‌دهنده‌ای وجود ندارد", + + "prompt.placeholder.connecting": "در حال اتصال به سرور...", + "prompt.placeholder.default": "پیامی بنویسید... (Enter برای ارسال، Shift+Enter برای خط جدید)", + "prompt.placeholder.error": "اتصال ناموفق بود. پنل خروجی را بررسی کنید یا افزونه را مجدداً راه‌اندازی کنید.", + + "context.usage.sessionCost": "هزینه جلسه", + "context.usage.olderSessions": "{{count}} جلسه قدیمی‌تر", + "context.stats.thisSession": "این جلسه", + + "time.today": "امروز", + "time.yesterday": "دیروز", + "time.thisWeek": "این هفته", + "time.thisMonth": "این ماه", + "time.older": "قدیمی‌تر", + + "settings.aboutKiloCode.status.connected": "متصل", + "settings.aboutKiloCode.status.connecting": "در حال اتصال...", + "settings.aboutKiloCode.status.disconnected": "قطع شده", + "settings.aboutKiloCode.status.error": "خطا", + "settings.aboutKiloCode.cliServer": "سرور CLI", + "settings.aboutKiloCode.status.label": "وضعیت:", + "settings.aboutKiloCode.port.label": "پورت:", + "settings.aboutKiloCode.versionInfo": "اطلاعات نسخه", + "settings.aboutKiloCode.version.label": "نسخه:", + "settings.aboutKiloCode.community": "جامعه و پشتیبانی", + "settings.aboutKiloCode.feedback.prefix": "اگر سؤال یا بازخوردی دارید، می‌توانید یک issue در", + "settings.aboutKiloCode.feedback.or": "یا", + "settings.aboutKiloCode.support.prefix": "برای سؤالات مربوط به صورت‌حساب یا حساب کاربری، با پشتیبانی مشتریان در", + "settings.aboutKiloCode.resetSettings.title": "بازنشانی تنظیمات", + "settings.aboutKiloCode.resetSettings.description": + "این گزینه فقط تنظیمات مخصوص افزونه VS Code را به مقادیر پیش‌فرض بازنشانی می‌کند. تنظیمات مشترک با CLI، مانند حالت‌ها و قوانین تأیید خودکار، در پیکربندی CLI ذخیره می‌شوند و بازنشانی نخواهند شد.", + "settings.aboutKiloCode.resetSettings.button": "بازنشانی همه تنظیمات", + "settings.aboutKiloCode.resetSettings.notificationsButton": "بازنشانی اعلان‌های خوانده‌شده", + "settings.aboutKiloCode.settingsTransfer.title": "انتقال تنظیمات", + "settings.aboutKiloCode.settingsTransfer.description": + "تنظیمات خود را برای انتقال بین نمونه‌های VS Code صادر یا وارد کنید.", + "settings.aboutKiloCode.exportSettings": "صادر کردن", + "settings.aboutKiloCode.importSettings": "وارد کردن", + "settings.aboutKiloCode.importSettings.invalidJson": + "فایل JSON نامعتبر است. لطفاً یک فایل تنظیمات معتبر انتخاب کنید.", + "settings.aboutKiloCode.importSettings.invalidConfig": "فایل حاوی تنظیمات معتبر Kilo نیست.", + "settings.aboutKiloCode.importSettings.tooLarge": + "فایل بیش از حد بزرگ است. فایل‌های تنظیمات باید کمتر از ۱ مگابایت باشند.", + "settings.aboutKiloCode.importSettings.newerVersion": + "این فایل از نسخه جدیدتری از Kilo صادر شده است. برخی تنظیمات ممکن است نادیده گرفته شوند.", + "settings.aboutKiloCode.importSettings.success": + "تنظیمات وارد شد. تغییرات بالا را بررسی کنید، سپس روی ذخیره کلیک کنید.", + + "settings.aboutKiloCode.telemetry.title": "تله‌متری", + "settings.aboutKiloCode.telemetry.description": + "تله‌متری توسط تنظیمات داخلی تله‌متری VS Code کنترل می‌شود. برای غیرفعال کردن آن، به Settings > Telemetry > Telemetry Level بروید و آن را روی «off» تنظیم کنید. VS Code را مجدداً راه‌اندازی کنید تا تغییر اعمال شود.", + "settings.aboutKiloCode.telemetry.openSettings": "باز کردن تنظیمات تله‌متری", + + "settings.agentBehaviour.subtab.agents": "عوامل", + "settings.agentBehaviour.subtab.mcpServers": "MCP Servers", + "settings.agentBehaviour.subtab.rules": "قوانین", + "settings.agentBehaviour.subtab.workflows": "گردش‌های کاری", + "settings.agentBehaviour.subtab.skills": "مهارت‌ها", + + "settings.browser.description": + "وقتی فعال است، عامل هوش مصنوعی می‌تواند با صفحات وب تعامل داشته باشد — پیمایش، کلیک، تایپ و گرفتن اسکرین‌شات. یک پنجره Chrome باز می‌شود تا بتوانید عملکرد عامل را مشاهده کنید.", + "settings.browser.enable.title": "فعال‌سازی اتوماسیون مرورگر", + "settings.browser.enable.description": "سرور Playwright MCP را با بک‌اند CLI ثبت کنید.", + "settings.browser.systemChrome.title": "استفاده از Chrome سیستم", + "settings.browser.systemChrome.description": + "به جای یک نمونه Chromium جداگانه، از مرورگر Chrome نصب‌شده شما استفاده کنید.", + "settings.browser.headless.title": "حالت Headless", + "settings.browser.headless.description": "در حالت headless اجرا شود (بدون پنجره مرورگر قابل مشاهده).", + + "settings.language.description": + "زبان رابط کاربری Kilo Code را انتخاب کنید. «Auto» از زبان نمایشی VS Code شما استفاده می‌کند.", + "settings.language.auto": "خودکار (زبان VS Code)", + "settings.language.current": "فعلی:", + + "common.add": "افزودن", + + "settings.autocomplete.model.title": "مدل تکمیل خودکار", + "settings.autocomplete.model.description": "مدل مورد استفاده برای تکمیل‌های درون‌خطی کد را انتخاب کنید", + "settings.autocomplete.autoTrigger.title": "فعال‌سازی تکمیل‌های خودکار درون‌خطی", + "settings.autocomplete.autoTrigger.description": "پیشنهادهای تکمیل درون‌خطی را هنگام تایپ به‌صورت خودکار نمایش بده", + "settings.autocomplete.smartKeybinding.title": "فعال‌سازی میانبر هوشمند وظایف درون‌خطی", + "settings.autocomplete.smartKeybinding.description": "از یک میانبر هوشمند برای فعال‌سازی وظایف درون‌خطی استفاده کن", + "settings.autocomplete.chatAutocomplete.title": "فعال‌سازی تکمیل خودکار متن چت", + "settings.autocomplete.chatAutocomplete.description": "نمایش پیشنهادات تکمیل خودکار در کادر متنی چت", + "settings.autocomplete.modelsHint": "برای انتخاب مدل مورد استفاده در تکمیل خودکار، به تنظیمات مدل‌ها مراجعه کنید.", + + "settings.notifications.sounds": "صداها", + "settings.notifications.enable.title": "فعال‌سازی اعلان‌های صوتی", + "settings.notifications.enable.description": "پخش صدا هنگام تکمیل جلسات، بروز خطا یا نیاز به ورودی شما", + "settings.notifications.testSound": "آزمایش", + "settings.notifications.sound.default": "پیش‌فرض", + "settings.notifications.sound.system": "سیستم", + "settings.notifications.sound.description": + "پیش‌فرض از صداهای مختلف برای تکمیل، ورودی و خطاها استفاده می‌کند. سایر گزینه‌ها از یک صدا برای همه رویدادها استفاده می‌کنند.", + + "settings.experimental.share.title": "حالت اشتراک‌گذاری", + "settings.experimental.share.description": "نحوه رفتار اشتراک‌گذاری جلسه", + "settings.experimental.share.manual": "دستی", + "settings.experimental.share.auto": "خودکار", + "settings.experimental.share.disabled": "غیرفعال", + "settings.experimental.formatter.title": "قالب‌بند", + "settings.experimental.formatter.description": "فعال‌سازی قالب‌بند خودکار کد", + "settings.experimental.lsp.title": "LSP", + "settings.experimental.lsp.description": "فعال‌سازی یکپارچه‌سازی پروتکل سرور زبان", + "settings.experimental.batch.title": "ابزار دسته‌ای", + "settings.experimental.batch.description": "فعال‌سازی دسته‌بندی چندین فراخوانی ابزار", + "settings.experimental.codebaseSearch.title": "جستجوی پایگاه کد", + "settings.experimental.codebaseSearch.description": + "فعال‌سازی جستجوی زبان طبیعی مبتنی بر هوش مصنوعی در سراسر پایگاه کد", + "settings.experimental.imageGeneration.title": "تولید تصویر", + "settings.experimental.imageGeneration.description": "فعال‌سازی تولید تصویر با هوش مصنوعی", + "settings.experimental.imageGenerationModel.title": "مدل تصویر", + "settings.experimental.imageGenerationModel.description": "مدل تولید تصویر", + "settings.experimental.imageGenerationModel.placeholder": "پیش‌فرض (مسیریاب خودکار)", + + "settings.models.speechToText.disabledDescription": + "برای استفاده از تبدیل گفتار به متن، ارائه‌دهنده Kilo را فعال کرده و وارد شوید. تبدیل گفتار به متن در حال حاضر فقط از طریق Kilo Gateway پشتیبانی می‌شود.", + "settings.models.speechToTextModel.title": "مدل تبدیل گفتار به متن", + "settings.models.speechToTextModel.description": "مدل رونویسی Kilo Gateway را برای ورودی صوتی انتخاب کنید.", + "settings.experimental.nativeNotebookTools.title": "ابزارهای بومی Notebook", + "settings.experimental.nativeNotebookTools.description": + "ابزارهای آزمایشی برای خواندن، ویرایش و اجرای VS Code notebooks را فعال کنید", + "settings.experimental.continueOnDeny.title": "ادامه در صورت رد", + "settings.experimental.continueOnDeny.description": "حلقه عامل را هنگام رد شدن یک مجوز ادامه دهید", + "settings.sandboxing.enabled.title": "Sandbox", + "settings.sandboxing.enabled.description": + "اجرای دستورات شل عامل در یک Sandbox سطح سیستم‌عامل که نوشتن را به پوشه‌های پروژه و وضعیت Kilo محدود می‌کند", + "settings.sandboxing.title": "Sandboxing", + "settings.sandboxing.network.title": "محدود کردن دسترسی شبکه", + "settings.sandboxing.network.description": + "مسدود کردن دسترسی مستقیم خروجی از دستورات مدل و ابزارهای HTTP. ابزارهای MCP محلی و راه‌دور در حین محدودیت در دسترس نیستند. ترافیک ارائه‌دهنده و هوک‌های پلاگین مورد اعتماد خارج از این محدودیت باقی می‌مانند.", + "settings.sandboxing.allowedHosts.title": "مقصدهای شبکه مجاز", + "settings.sandboxing.allowedHosts.description": + "مقصدهای DNS هاست و پورت برای ترافیک پروکسی HTTP و HTTPS در Sandbox. GitHub CLI و Git از طریق HTTPS معمولاً به github.com:443 و api.github.com:443 نیاز دارند. تغییرات در نشست‌های جدید اعمال می‌شوند.", + "settings.sandboxing.writablePaths.title": "مسیرهای قابل نوشتن اضافی", + "settings.sandboxing.writablePaths.description": + "مسیرهای فایل‌سیستم اضافی که Sandbox اجازه نوشتن به آن‌ها را می‌دهد (مثلاً /tmp، /var/log). این مسیرها هنگام فعال بودن Sandbox با مسیرهای قابل نوشتن پیش‌فرض ادغام می‌شوند.", + "settings.experimental.swePruner.title": "SWE-Pruner", + "settings.experimental.swePruner.description": + "فعال‌سازی SWE-Pruner: هرس آگاه از وظیفه برای خروجی‌های بزرگ ابزارهای خواندن، جستجو و پوسته، هدایت‌شده توسط یک سؤال تمرکز از عامل", + "settings.experimental.swePrunerModel.title": "مدل SWE-Pruner", + "settings.experimental.swePrunerModel.description": + "مدل مورد استفاده برای مرور سریع خروجی‌های ابزار؛ به‌طور پیش‌فرض از مدل کوچک پیکربندی‌شده استفاده می‌کند", + "settings.experimental.mcpTimeout.title": "زمان‌وقفه MCP (میلی‌ثانیه)", + "settings.experimental.mcpTimeout.description": "زمان‌وقفه برای درخواست‌های سرور MCP بر حسب میلی‌ثانیه", + "settings.experimental.remote.title": "کنترل از راه دور", + "settings.experimental.remote.description": + "فعال‌سازی کنترل از راه دور جلسات از طریق Kilo Cloud. این تنظیم بر CLIهای این دستگاه نیز تأثیر می‌گذارد.", + "settings.experimental.remote.current": "وضعیت فعلی:", + "settings.experimental.remote.startup": "فعال‌سازی خودکار هنگام راه‌اندازی:", + "settings.experimental.remote.active": "فعال", + "settings.experimental.remote.inactive": "غیرفعال", + "settings.experimental.remote.hint": "برای تغییر وضعیت از /remote در چت استفاده کنید", + "settings.experimental.toolToggles": "تنظیمات ابزارها", + + "settings.agentBehaviour.defaultAgent.title": "عامل پیش‌فرض", + "settings.agentBehaviour.defaultAgent.description": "عاملی که در صورت عدم تعیین استفاده می‌شود", + "settings.agentBehaviour.availableAgents": "عامل‌های موجود", + "settings.agentBehaviour.modelOverride.title": "جایگزینی مدل", + "settings.agentBehaviour.modelOverride.description": "مدل پیش‌فرض این عامل را بازنویسی کنید", + "settings.agentBehaviour.variantOverride.title": "بازنویسی نوع", + "settings.agentBehaviour.variantOverride.description": "نوع مدل این عامل را بازنویسی کنید", + "settings.agentBehaviour.temperature.title": "دما", + "settings.agentBehaviour.temperature.description": + "کنترل می‌کند که پاسخ‌های هوش مصنوعی چقدر تصادفی باشند (۰–۲). مقادیر پایین‌تر (مثلاً ۰.۲) خروجی متمرکز و یکنواخت تولید می‌کنند. مقادیر بالاتر (مثلاً ۱.۰) پاسخ‌های متنوع‌تر و خلاقانه‌تری ایجاد می‌کنند. برای استفاده از مقدار پیش‌فرض مدل، خالی بگذارید.", + "settings.agentBehaviour.topP.title": "Top P", + "settings.agentBehaviour.topP.description": + "آستانه نمونه‌برداری هسته‌ای (۰–۱). انتخاب توکن‌ها را به کوچک‌ترین مجموعه‌ای محدود می‌کند که احتمال تجمعی آن به P برسد. مقادیر پایین‌تر خروجی را متمرکزتر می‌کنند؛ مقادیر بالاتر تنوع بیشتری را مجاز می‌دانند. برای استفاده از مقدار پیش‌فرض مدل، خالی بگذارید.", + "settings.agentBehaviour.maxSteps.title": "حداکثر مراحل", + "settings.agentBehaviour.maxSteps.description": + "حداکثر تعداد گام‌های عامل. در این محدودیت، به عامل دستور داده می‌شود که استفاده از ابزارها را متوقف کرده و پاسخ نهایی ارائه دهد. برای وظایف پیچیده چندمرحله‌ای افزایش دهید؛ برای پاسخ‌های کوتاه‌تر و قابل پیش‌بینی‌تر کاهش دهید.", + "settings.agentBehaviour.hidden.title": "پنهان", + "settings.agentBehaviour.hidden.description": "این عامل را از انتخابگر حالت در ورودی چت پنهان کنید", + "settings.agentBehaviour.disable.title": "غیرفعال", + "settings.agentBehaviour.disable.description": "این عامل را کاملاً غیرفعال کنید — در هیچ جایی نمایش داده نخواهد شد", + "settings.agentBehaviour.badge.hidden": "پنهان", + "settings.agentBehaviour.badge.disabled": "غیرفعال", + "settings.agentBehaviour.badge.deprecated": "منسوخ", + "settings.agentBehaviour.discoveredSkills": "مهارت‌های کشف‌شده", + "settings.agentBehaviour.noSkillsFound": + "هیچ مهارتی کشف نشد. مسیرهای پوشه مهارت یا URL را در زیر اضافه کنید تا مهارت‌ها در دسترس قرار گیرند.", + "settings.agentBehaviour.noAgentsFound": "هیچ عاملی یافت نشد.", + "settings.agentBehaviour.removeAgent.title": "حذف عامل", + "settings.agentBehaviour.removeAgent.confirm": + "ایجنت «{{name}}» حذف شود؟ این کار با به‌روزرسانی پیکربندی شما، ایجنت را غیرفعال می‌کند.", + "settings.agentBehaviour.removeAgent.button": "حذف", + "settings.agentBehaviour.skillPaths": "مسیرهای پوشه مهارت", + "settings.agentBehaviour.skillUrls": "URL های مهارت", + "settings.agentBehaviour.removeSkill.title": "حذف مهارت", + "settings.agentBehaviour.removeSkill.confirm": + "مهارت «{{name}}» حذف شود؟ این کار فایل‌های مهارت را از دیسک حذف می‌کند.", + "settings.agentBehaviour.removeSkill.button": "حذف", + "settings.agentBehaviour.rules.description": + "قوانین، فایل‌های دستورالعملی هستند که رفتار عامل را هدایت می‌کنند. این قوانین در پرامپت سیستم برای هر مکالمه گنجانده می‌شوند. برای افزودن قوانین بیشتر، مسیرهای فایل را در زیر وارد کنید.", + "settings.agentBehaviour.instructionFiles": "فایل‌های دستورالعمل اضافی", + "settings.agentBehaviour.instructionFiles.description": + "مسیرهای فایل‌های دستورالعمل اضافی که در پرامپت سیستم گنجانده می‌شوند", + "settings.agentBehaviour.claudeCompat.heading": "سازگاری با Claude Code", + "settings.agentBehaviour.claudeCompat.title": "بارگذاری فایل‌های Claude Code", + "settings.agentBehaviour.claudeCompat.description": + "دستورالعمل‌ها و مهارت‌های CLAUDE.md را از پوشه پیکربندی Claude Code شما در جلسات بارگذاری می‌کند. اگر می‌خواهید Kilo از دستورالعمل‌ها و مهارت‌های Claude Code شما استفاده کند، این گزینه را فعال کنید. نیاز به راه‌اندازی مجدد دارد.", + "settings.agentBehaviour.removeMcp.title": "حذف سرور MCP", + "settings.agentBehaviour.removeMcp.confirm": "سرور MCP «{{name}}» حذف شود؟ این کار آن را از پیکربندی شما حذف می‌کند.", + "settings.agentBehaviour.removeMcp.button": "حذف", + "settings.agentBehaviour.mcpDetail.command": "دستور", + "settings.agentBehaviour.mcpDetail.args": "آرگومان‌ها", + "settings.agentBehaviour.mcpDetail.env": "محیط", + "settings.agentBehaviour.editMcp": "ویرایش سرور MCP", + "settings.agentBehaviour.editMcp.transportLocal": "سرور محلی (انتقال stdio)", + "settings.agentBehaviour.editMcp.transportRemote": "سرور راه‌دور (انتقال SSE/HTTP)", + "settings.agentBehaviour.editMcp.env": "متغیرهای محیطی", + "settings.agentBehaviour.editMcp.env.help": "متغیرهایی که به فرآیند سرور MCP ارسال می‌شوند.", + "settings.agentBehaviour.addMcp.command": "دستور", + "settings.agentBehaviour.addMcp.command.placeholder": "مثلاً npx", + "settings.agentBehaviour.addMcp.args": "آرگومان‌ها", + "settings.agentBehaviour.addMcp.args.help": "یک آرگومان در هر خط. مسیرهای دارای فاصله عیناً حفظ می‌شوند.", + "settings.agentBehaviour.addMcp.args.placeholder": "مثلاً\n-y\n@modelcontextprotocol/server-filesystem\n/tmp", + "settings.agentBehaviour.addMcp.url": "URL سرور", + "settings.agentBehaviour.addMcp.url.placeholder": "مثلاً http://localhost:3000/sse", + "settings.agentBehaviour.mcpBrowseMarketplace": "مرور Marketplace", + "settings.agentBehaviour.mcpEmpty": + "هیچ سرور MCP پیکربندی نشده است. سرورهای MCP را در kilo.jsonc اضافه کنید، یا از agent بخواهید آن‌ها را برایتان اضافه کند.", + "settings.agentBehaviour.workflows.description": + "Workflow‌ها دستورات slash سفارشی هستند که در پیکربندی شما تعریف شده‌اند. برای فراخوانی آن‌ها /command-name را در چت تایپ کنید. دستورات در opencode.json زیر بخش 'command' پیکربندی می‌شوند.", + "settings.agentBehaviour.workflows.empty": + "هیچ دستور سفارشی پیکربندی نشده است. دستورات را به opencode.json خود اضافه کنید تا اینجا نمایش داده شوند.", + "settings.agentBehaviour.workflows.detail.description": "توضیحات", + "settings.agentBehaviour.workflows.detail.template": "قالب", + + "settings.agentBehaviour.createMode": "ایجاد حالت جدید", + "settings.agentBehaviour.createMode.name": "نام", + "settings.agentBehaviour.createMode.name.placeholder": "مثلاً reviewer", + "settings.agentBehaviour.createMode.name.description": + "شناسه یکتا برای حالت. فقط از حروف کوچک، اعداد و خط تیره استفاده کنید.", + "settings.agentBehaviour.createMode.description": "توضیحات", + "settings.agentBehaviour.createMode.description.placeholder": "مثلاً کد را از نظر کیفیت و بهترین روش‌ها بررسی می‌کند", + "settings.agentBehaviour.createMode.description.help": "توضیح کوتاهی از عملکرد این حالت.", + "settings.agentBehaviour.createMode.prompt": "پرامپت سیستم", + "settings.agentBehaviour.createMode.prompt.placeholder": + "مثلاً شما یک بازبین کد هستید. بر کیفیت کد، بهترین روش‌ها و باگ‌های احتمالی تمرکز کنید.", + "settings.agentBehaviour.createMode.prompt.help": "دستورالعمل‌های عامل هوش مصنوعی هنگام استفاده از این حالت.", + "settings.agentBehaviour.createMode.button": "ایجاد", + "settings.agentBehaviour.createMode.cancel": "لغو", + "settings.agentBehaviour.createMode.nameRequired": "نام الزامی است", + "settings.agentBehaviour.createMode.nameInvalid": + "نام باید با یک حرف کوچک شروع شود و فقط شامل حروف کوچک، اعداد و خط تیره باشد", + "settings.agentBehaviour.createMode.nameTaken": "یک حالت با این نام از قبل وجود دارد", + "settings.agentBehaviour.importMode": "وارد کردن", + "settings.agentBehaviour.importMode.invalidName": + "نام حالت در فایل نامعتبر است. نام باید با یک حرف کوچک شروع شود و فقط شامل حروف کوچک، اعداد و خط تیره باشد.", + "settings.agentBehaviour.importMode.nameTaken": "یک حالت با این نام از قبل وجود دارد.", + "settings.agentBehaviour.importMode.invalidJson": + "فایل JSON نامعتبر است. لطفاً یک فایل تعریف عامل معتبر انتخاب کنید.", + "settings.agentBehaviour.importMode.tooLarge": "فایل بیش از حد بزرگ است. تعریف عامل‌ها باید کمتر از ۱ مگابایت باشند.", + "settings.agentBehaviour.exportMode": "صادر کردن تعریف عامل", + "settings.agentBehaviour.editMode": "حالت ویرایش", + "settings.agentBehaviour.editMode.description": "توضیحات", + "settings.agentBehaviour.editMode.prompt": "پرامپت سیستم", + "settings.agentBehaviour.editMode.save": "انجام شد", + "settings.agentBehaviour.editMode.back": "بازگشت به فهرست", + "settings.agentBehaviour.editMode.native": + "این یک حالت داخلی است. تعریف پایه آن قابل تغییر نیست، اما می‌توانید تنظیمات سفارشی را در زیر پیکربندی کنید.", + "settings.agentBehaviour.editMode.promptOverride": "پرامپت سفارشی جایگزین برای این حالت داخلی", + "settings.agentBehaviour.badge.subagent": "زیرعامل", + "settings.agentBehaviour.permissions.title": "مجوزهای محاسبه‌شده", + "settings.agentBehaviour.permissions.count": "{{count}} قانون", + "settings.agentBehaviour.permissions.effective": "مؤثر (wildcard):", + "settings.agentBehaviour.permissions.col.tool": "ابزار", + "settings.agentBehaviour.permissions.col.pattern": "الگو", + "settings.agentBehaviour.permissions.col.action": "عملکرد", + "settings.agentBehaviour.permissions.copy": "کپی مجوزها به‌صورت JSON", + "settings.agentBehaviour.permissions.hint": + "قوانین به ترتیب ارزیابی می‌شوند — آخرین قانون منطبق اعمال می‌شود. این مجموعه قوانین حل‌شده از backend CLI است.", + + "settings.autoApprove.description": + "نحوه اجرای ابزارها را تعریف کنید. اکثر ابزارها به‌طور پیش‌فرض روی Allow هستند. doom_loop و external_directory به‌طور پیش‌فرض روی Ask هستند.", + "settings.autoApprove.maxCost.title": "هشدار هزینه جلسه", + "settings.autoApprove.maxCost.description": + "هنگامی که هزینه یک جلسه از این مقدار دلاری فراتر رفت، یک هشدار غیرمسدودکننده نمایش داده شود. از اعداد صحیح استفاده کنید؛ برای غیرفعال کردن خالی بگذارید.", + "settings.autoApprove.level.allow": "اجازه", + "settings.autoApprove.level.ask": "پرسش", + "settings.autoApprove.level.deny": "رد کردن", + "settings.autoApprove.wildcardLabel.commands": "همه دستورات (*)", + "settings.autoApprove.wildcardLabel.paths": "همه مسیرها (*)", + "settings.autoApprove.exceptions": "استثناها", + "settings.autoApprove.addCommand": "افزودن دستور", + "settings.autoApprove.addPath": "افزودن مسیر", + "settings.autoApprove.placeholder.command": "مثال: git *", + "settings.autoApprove.placeholder.path": "مثال: *.env", + "settings.autoApprove.tool.external_directory": + "دسترسی به فایل‌های خارج از فضای کاری. هنگام دسترسی به فایل‌های خارج از پوشه پروژه جاری فعال می‌شود.", + "settings.autoApprove.tool.bash": "اجرای دستورات ترمینال. اجازه اجرای دستورات شل را می‌دهد (مثلاً git status).", + "settings.autoApprove.tool.read": "خواندن فایل‌ها. به عامل اجازه می‌دهد فایل‌های منطبق با مسیر مشخص‌شده را بخواند.", + "settings.autoApprove.tool.edit": + "ویرایش فایل‌ها. به عامل اجازه می‌دهد فایل‌ها را ایجاد یا ویرایش کند، از جمله پچ‌ها و به‌روزرسانی‌های چندفایلی.", + "settings.autoApprove.tool.glob": + "تطبیق فایل‌ها با الگو. اجازه تطبیق فایل با استفاده از الگوهای glob را می‌دهد (مثلاً src/**/*.ts).", + "settings.autoApprove.tool.grep": "جستجو در محتوای فایل‌ها. اجازه جستجوی مبتنی بر regex درون فایل‌ها را می‌دهد.", + "settings.autoApprove.tool.list": "فهرست محتوای پوشه. اجازه مشاهده فایل‌ها و پوشه‌های داخل یک دایرکتوری را می‌دهد.", + "settings.autoApprove.tool.task": "راه‌اندازی زیرعامل‌ها. اجازه شروع زیرعامل‌های تخصصی برای وظایف خاص را می‌دهد.", + "settings.autoApprove.tool.skill": + "بارگذاری مهارت‌ها. اجازه بارگذاری مهارت‌های از پیش تعریف‌شده بر اساس نام را می‌دهد.", + "settings.autoApprove.tool.lsp": "پرس‌وجو از سرور زبان. اجازه اجرای پرس‌وجوهای LSP برای هوشمندی کد را می‌دهد.", + "settings.autoApprove.tool.todoreadwrite": + "مدیریت فهرست وظایف. اجازه خواندن و به‌روزرسانی فهرست وظایف داخلی را می‌دهد.", + "settings.autoApprove.tool.webfetch": "دریافت یک URL. امکان بازیابی محتوا از یک URL مشخص را فراهم می‌کند.", + "settings.autoApprove.tool.websearch": "جستجو در وب. امکان انجام جستجوهای خارجی در وب را فراهم می‌کند.", + "settings.autoApprove.tool.doom_loop": + "جلوگیری از اقدامات تکراری یکسان. زمانی فعال می‌شود که همان فراخوانی ابزار با ورودی یکسان تکرار شود.", + + "settings.checkpoints.enable.title": "فعال‌سازی اسنپ‌شات‌ها", + "settings.checkpoints.enable.description": + "قبل از ویرایش فایل‌ها نقاط بازیابی ایجاد کنید تا بتوانید به حالت‌های قبلی بازگردید", + + "settings.context.autoCompaction.title": "فشرده‌سازی خودکار", + "settings.context.autoCompaction.description": "قبل از رسیدن به محدودیت، زمینه را به‌طور خودکار فشرده کنید", + "settings.context.compaction.title": "فشرده‌سازی", + "settings.context.compactionLimit.title": "محدودیت فشرده‌سازی خودکار", + "settings.context.compactionLimit.description": + "زمانی فشرده‌سازی انجام شود که زمینه به این درصد از پنجره مدل برسد. برای استفاده تنها از بافر ایمنی، خالی بگذارید.", + "settings.context.prune.title": "حذف خروجی‌های قدیمی", + "settings.context.prune.description": "حذف خروجی‌های قدیمی ابزار در حین فشرده‌سازی", + "settings.context.watcherPatterns": "الگوهای نادیده‌گیری ناظر فایل", + "settings.context.watcherPatterns.description": "الگوهای Glob برای فایل‌هایی که ناظر باید نادیده بگیرد", + "settings.context.memory.title": "حافظه", + "settings.context.memory.project.title": "حافظه پروژه", + "settings.context.memory.autoSave.title": "ذخیره خودکار حافظه پروژه", + "settings.context.memory.autoSave.description": + "هنگامی که حافظه فعال است، اطلاعات پایدار پروژه از نوبت‌های تکمیل‌شده به‌طور خودکار ذخیره می‌شوند.", + "settings.context.memory.storage.title": "ذخیره‌سازی", + "settings.context.memory.status.notLoaded": "بارگذاری نشده", + "settings.context.memory.status.disabled": "غیرفعال", + "settings.context.memory.status.enabledTokens": "فعال - ~{{tokens}} توکن ذخیره‌شده", + "settings.context.memory.storage.path": "{{path}}", + "settings.context.memory.storage.enable": "حافظه را فعال کنید تا فایل‌های حافظه پروژه ایجاد شوند.", + "settings.context.memory.inspect": "بررسی", + "chat.memory.project.disabled": "حافظه پروژه غیرفعال است", + "chat.memory.project.empty": "این پروژه هنوز هیچ حافظه‌ای ندارد. پس از استفاده از Kilo نمایش داده خواهد شد.", + "chat.memory.command.failed": "دستور حافظه ناموفق بود", + "chat.memory.updated": "حافظه به‌روزرسانی شد", + "chat.memory.rebuild": "ایندکس حافظه بازسازی شد", + + "settings.commitMessage.title": "پیام Commit", + "settings.commitMessage.override.title": "استفاده از Prompt سفارشی", + "settings.commitMessage.override.description": + "جایگزینی Prompt پیش‌فرض پیام commit. در صورت فعال‌سازی، Prompt سفارشی شما به‌طور کامل جایگزین Prompt داخلی conventional commits می‌شود.", + "settings.commitMessage.prompt.title": "پرامپت سفارشی", + "settings.commitMessage.prompt.description": + "پرامپت سیستمی که هنگام تولید پیام‌های کامیت به هوش مصنوعی ارسال می‌شود. این گزینه پرامپت پیش‌فرض را به‌طور کامل جایگزین می‌کند.", + "settings.commitMessage.prompt.placeholder": + "مثال: پیام‌های کامیت را به زبان اسپانیایی و با فرمت conventional commits تولید کن. فقط پیام کامیت را برگردان.", + + "settings.commitMessage.language.sync": "همگام‌سازی با زبان رابط کاربری", + "settings.commitMessage.language.description": + "زبان مورد استفاده برای پیام‌های کامیت تولیدشده توسط هوش مصنوعی را انتخاب کنید:", + + "settings.display.username.title": "نام کاربری", + "settings.display.username.description": "نام کاربری سفارشی که در مکالمات نمایش داده می‌شود", + "settings.display.fontSize.title": "اندازه قلم", + "settings.display.fontSize.description": "اندازه قلم رابط کاربری وب‌ویو Kilo را مستقل از VS Code تنظیم کنید.", + "settings.display.reasoningAutoCollapse.title": "جمع‌شدن خودکار استدلال", + "settings.display.reasoningAutoCollapse.description": + "بلوک‌های استدلال را پس از اتمام نوشتن توسط عامل جمع می‌کند. برای نگه داشتن استدلال در حالت باز، این گزینه را خاموش بگذارید مگر اینکه خودتان آن را جمع کنید.", + "settings.display.shiftTabCycle.title": "چرخش سطح استدلال با Shift+Tab", + "settings.display.shiftTabCycle.description": + "در ورودی پرامپت، Shift+Tab را فشار دهید تا به سطح تلاش استدلال بعدی بروید. برای حفظ عملکرد Shift+Tab جهت ناوبری فوکوس صفحه‌کلید، این گزینه را غیرفعال کنید.", + "settings.display.terminalCommand.title": "بلوک‌های دستور ترمینال", + "settings.display.terminalCommand.description": + "انتخاب کنید که بلوک‌های دستور ترمینال در حالت باز یا بسته شروع شوند.", + "settings.display.terminalCommand.expanded": "باز", + "settings.display.terminalCommand.collapsed": "بسته", + "settings.display.codeEdit.title": "بلوک‌های ویرایش کد", + "settings.display.codeEdit.description": "انتخاب کنید که بلوک‌های ویرایش کد و تفاوت در حالت باز یا بسته شروع شوند.", + "settings.display.codeEdit.expanded": "گسترش‌یافته", + "settings.display.codeEdit.collapsed": "جمع‌شده", + "settings.display.tokenThroughput.title": "نمایش توان عملیاتی توکن", + "settings.display.tokenThroughput.description": + "نرخ تولید متن (توکن/ثانیه) را در آخرین پیام دستیار و در سربرگ وظیفه نمایش می‌دهد. به‌طور پیش‌فرض پنهان است تا چت شلوغ نشود.", + + "chat.throughput.tooltip": + "میانگین {{speed}} توکن/ثانیه برای این نوبت. شامل توکن‌های خروجی و استدلال می‌شود؛ زمان اجرای ابزار و انتظار را شامل نمی‌شود.", + "chat.throughput.tooltip.missing": "معیارهای توان عملیاتی برای این نوبت در دسترس نیست.", + + "settings.providers.defaultModel.title": "مدل پیش‌فرض", + "settings.providers.defaultModel.description": "مدل اصلی برای مکالمات", + "settings.providers.smallModel.title": "مدل سبک", + "settings.providers.smallModel.description": + "مدل سبک‌وزن برای تولید عنوان، تولید پیام کامیت، بهبود پرامپت و سایر وظایف سریع", + "settings.providers.subagentModel.title": "مدل زیرعامل", + "settings.providers.subagentModel.description": + "مدل پیش‌فرض و میزان استدلال برای زیرعامل‌های ابزار-وظیفه. برای به ارث بردن مدل عامل فراخواننده، خالی بگذارید.", + "settings.models.hidePromptTraining.title": "پنهان کردن مدل‌های آموزش پرامپت", + "settings.models.hidePromptTraining.description": + "مدل‌های Kilo Gateway را که ارائه‌دهندگانشان ممکن است از پرامپت‌های شما برای آموزش استفاده کنند پنهان کنید.", + "settings.providers.modeModels": "مدل به ازای حالت", + "settings.providers.modeModels.description": + "مدل پیش‌فرض را برای حالت‌های خاص بازنویسی کنید. در صورت عدم تنظیم، از مدل پیش‌فرض سراسری استفاده می‌شود.", + "settings.providers.disabled": "ارائه‌دهندگان غیرفعال", + "settings.providers.disabled.description": "ارائه‌دهندگانی که از فهرست پنهان می‌شوند", + "settings.providers.disabled.enable": "فعال‌سازی", + "settings.providers.notSet": "تنظیم نشده (از پیش‌فرض سرور استفاده شود)", + "settings.providers.select.placeholder": "انتخاب ارائه‌دهنده...", + + "dialog.model.notSet": "تنظیم نشده", + + "profile.personalAccount": "حساب شخصی", + "profile.switchingAccount": "در حال تغییر حساب…", + + // Agent Manager strings live in webview-ui/agent-manager/i18n/en.ts + + "question.summary": "{{n}} از {{total}} سؤال", + "common.review": "بررسی", + + // legacy-migration start + "settings.legacyMigration.link": "انتقال از نسخه قدیمی", + "settings.aboutKiloCode.legacyMigration.title": "انتقال از نسخه قدیمی", + "settings.aboutKiloCode.legacyMigration.description": + "تنظیمات را از نصب قبلی Kilo Code منتقل کنید، از جمله کلیدهای API ارائه‌دهنده و مدل پیش‌فرض.", + "settings.aboutKiloCode.rooImport.description": "تاریخچه مکالمات را از یک نصب Roo Code وارد کنید.", + "settings.aboutKiloCode.rooImport.button": "وارد کردن جلسات از Roo Code", + + // Screen 1 — What's New + "migration.whatsNew.title": "تازه‌های Kilo Code", + "migration.whatsNew.subtitle": "افزونه را بر پایه‌ای سریع‌تر و کارآمدتر بازسازی کرده‌ایم.", + "migration.whatsNew.features.performance.title": "عملکرد سریع‌تر عامل", + "migration.whatsNew.features.performance.detail": + "فراخوانی ابزارهای موازی و زیرعامل‌ها به عامل شما امکان می‌دهند کارهای بیشتری را همزمان انجام دهد — تا زمان کمتری صرف نظارت کنید و زمان بیشتری برای ارسال داشته باشید.", + "migration.whatsNew.features.interface.title": "رابط کاربری ساده‌تر", + "migration.whatsNew.features.interface.detail": "حواس‌پرتی کمتر، خواندن آسان‌تر و سریع‌تر.", + "migration.whatsNew.features.agentManager.title": "Agent Manager", + "migration.whatsNew.features.agentManager.detail": + "یک رابط یکپارچه برای اجرای چندین عامل به‌صورت موازی، هر کدام در worktree مخصوص خود — پیشرفت را رصد کنید، بین زمینه‌ها جابه‌جا شوید و تغییرات را در یک مکان بررسی کنید.", + "migration.whatsNew.features.foundation.title": "پایه مشترک", + "migration.whatsNew.features.foundation.detail": + "یک هسته کوچک و کارآمد در تمام محصولات Kilo. تجربه‌ای آشنا، هر طور که انتخاب کنید کار کنید.", + "migration.whatsNew.blogLink": "خواندن اعلامیه کامل", + "migration.whatsNew.docsLink": "چه چیزی جدید است و سؤالات متداول", + "migration.whatsNew.continue": "ادامه", + + // Screen 2 — Migrate Settings + "migration.migrate.title": "تنظیمات خود را منتقل کنید", + "migration.migrate.subtitle": "تنظیماتی از نصب قبلی شما یافتیم. این‌ها چیزهایی هستند که می‌توانیم منتقل کنیم.", + "migration.migrate.selectLabel": "انتخاب موارد برای انتقال", + "migration.migrate.chatHistory": "جلسات و تاریخچه چت", + "migration.migrate.button": "انتقال تنظیمات", + "migration.migrate.skip": "رد کردن", + "migration.migrate.keysDetected": "{{count}} کلید شناسایی شد", + "migration.migrate.serversConfigured": "{{count}} سرور پیکربندی شده", + "migration.migrate.modesFound": "{{count}} حالت یافت شد", + "migration.migrate.sessionsDetected": "{{count}} نشست شناسایی شد", + "migration.migrate.nothingToMigrate": "هیچ موردی برای انتقال در تنظیمات قدیمی یافت نشد.", + + // Migrate — item labels (reused from old select keys) + "migration.select.providers": "کلیدهای API ارائه‌دهنده", + "migration.select.mcpServers": "سرورهای MCP", + "migration.select.customModes": "حالت‌ها / عوامل سفارشی", + "migration.select.defaultModel": "مدل پیش‌فرض", + "migration.select.autoApproval": "تأیید خودکار", + "migration.select.language": "زبان رابط کاربری", + "migration.select.autocomplete": "تنظیمات تکمیل خودکار", + + // Migrate — completion + "migration.complete.summary": "{{success}} از {{total}} مورد با موفقیت منتقل شد.", + "migration.complete.cleanup": "حذف داده‌های تنظیمات قدیمی", + "migration.complete.cleanupDescription": + "این گزینه تنظیمات قدیمی را از حافظه VS Code حذف می‌کند. پس از این کار، امکان اجرای مجدد این انتقال وجود نخواهد داشت.", + "migration.complete.done": "انجام شد", + "migration.error.continue": "ادامه", + "migration.sessionSummary.title": "خلاصه:", + "migration.sessionSummary.copy": "کپی گزارش", + "migration.sessionSummary.toast.copied": "گزارش کپی شد", + "migration.sessionSummary.successful": "موفق", + "migration.sessionSummary.skipped": "رد شده", + "migration.sessionSummary.alreadyMigrated": "قبلاً منتقل شده", + "migration.sessionSummary.errored": "با خطا مواجه شد", + "migration.sessionSummary.none": "هیچ‌کدام", + "migration.forceReimport.title": "وارد کردن مجدد اجباری", + "migration.forceReimport.description": + "وارد کردن مجدد {{target}} آن‌ها را بازنویسی کرده و هر پیام جدیدی که در آن نشست‌ها ایجاد شده را حذف می‌کند.", + "migration.forceReimport.target.one": "این نشست", + "migration.forceReimport.target.many": "این {{count}} نشست", + "migration.forceReimport.button": "وارد کردن مجدد اجباری", + "migration.forceReimport.all": "وارد کردن مجدد همه", + "migration.forceReimport.proceed": "ادامه", + "migration.forceReimport.toast.started": "وارد کردن مجدد اجباری آغاز شد", + "migration.running.title": "انتقال در حال انجام است", + "migration.running.description.line1": "در حالی که هنوز نشست‌هایی در حال انتقال هستند، می‌خواهید پایان دهید.", + "migration.running.description.line2": "اگر اکنون خارج شوید، برخی جلسات ممکن است ناتمام بمانند.", + "migration.running.stay": "ماندن", + "migration.running.proceed": "ادامه", + "migration.sessionProgress.preparing": "در حال آماده‌سازی جلسه", + "migration.sessionProgress.storing": "در حال ذخیره جلسه", + "migration.sessionProgress.skipped": "جلسه رد شد", + "migration.sessionProgress.header": "در حال انتقال {{current}} از {{total}}", + "migration.sessionFormat.unknownDate": "تاریخ نامشخص", + "migration.sessionFormat.unknown": "نامشخص", + "migration.sessionFormat.unknownError": "خطای نامشخص", + // legacy-migration end + + "error.details.show": "جزئیات", + + "task.todos.progress": "{{done}}/{{total}} کار انجام شد", + "task.todos.allDone": "{{count}} کار انجام شد", + + "settings.saveBar.unsavedChanges": "تغییرات ذخیره‌نشده", + "settings.saveBar.discard": "رد کردن", + "settings.saveBar.save": "ذخیره", + "settings.saveBar.saving": "در حال ذخیره…", + "settings.saveBar.warning.one": "یک جلسه در حال اجرا است و قطع خواهد شد", + "settings.saveBar.warning.many": "چند جلسه در حال اجرا هستند و قطع خواهند شد", + "settings.saveBar.saveAnyway": "در هر صورت ذخیره کن", + "settings.saveBar.cancel": "لغو", + "settings.saveBar.saveFailed": "ذخیره تنظیمات ممکن نشد", + + "notifications.action.next": "بعدی", + "notifications.action.close": "بستن", + "notifications.action.tryModel": "امتحان {{model}}", + "notifications.action.tryModelGeneric": "امتحان مدل", + + "diffViewer.source.workspace.label": "شاخه", + "diffViewer.source.workspace.tooltip": + "تمام تغییرات این شاخه در مقایسه با شاخه پایه. شامل فایل‌های کامیت‌نشده (staged، unstaged، untracked) و کامیت‌های محلی که هنوز در شاخه پایه نیستند.", + "diffViewer.source.staged.label": "آماده‌سازی‌شده", + "diffViewer.source.staged.tooltip": + "فایل‌هایی با تغییراتی که به ناحیه staging گیت اضافه کرده‌اید (`git add`)، همان‌طور که در کامیت بعدی ظاهر خواهند شد.", + "diffViewer.source.unstaged.label": "مرحله‌بندی‌نشده", + "diffViewer.source.unstaged.tooltip": + "فایل‌های تغییر یافته در working tree شما که هنوز stage نشده‌اند، به علاوه فایل‌های ردیابی‌نشده (جدید).", + "diffViewer.source.session.label": "جلسه", + "diffViewer.source.session.tooltip": + "فایل‌هایی که توسط Kilo در جلسه جاری تغییر کرده‌اند، بر اساس عکس‌های فوری هر نوبت. با شروع جلسه جدید بازنشانی می‌شود.", + "diffViewer.group.session": "جلسه", + "diffViewer.group.git": "Git", + "diffViewer.notice.snapshotsDisabled": + "عکس‌های فوری برای این مخزن غیرفعال هستند. لطفاً فایل‌های پیکربندی خود را ویرایش کنید تا تغییرات جلسه نمایش داده شوند.", + + "diffViewer.baseBranch.auto": "پیش‌فرض", + "diffViewer.baseBranch.default": "پیش‌فرض", + "diffViewer.baseBranch.remote": "راه دور", + "diffViewer.baseBranch.search": "جستجوی شاخه‌ها", + "diffViewer.baseBranch.empty": "هیچ شاخه‌ای یافت نشد", + "diffViewer.baseBranch.loading": "در حال بارگذاری شاخه‌ها…", + "diffViewer.baseBranch.none": "—", + + "plan.exit.ready": "طرح آماده است:", + "chat.search.placeholder": "جستجو در چت…", + "chat.search.toggle": "جستجو در چت", + "chat.search.matchCase": "تطابق حروف بزرگ و کوچک", + "chat.search.matchWholeWord": "تطابق کلمه کامل", + "chat.search.useRegex": "استفاده از عبارت منظم", + "chat.search.previousMatch": "تطابق قبلی", + "chat.search.nextMatch": "تطابق بعدی", + "chat.search.close": "بستن جستجو", + "chat.search.invalidRegex": "عبارت منظم نامعتبر", + "chat.search.noResults": "نتیجه‌ای یافت نشد", + "chat.search.searchingHistory": "در حال جستجو در پیام‌های قبلی…", +} diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index 451b0a1bc6..de3da09df3 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -734,6 +734,10 @@ export const dict = { "session.messages.scrollToBottom": "Défiler vers le bas", "session.messages.initializing": "Initialisation...", "session.messages.taskStarting": "Démarrage...", + "session.prompts.navLabel": "Navigateur de prompts", + "session.prompts.tick": "Prompt {{index}} sur {{total}} : {{prompt}}", + "session.prompts.noAnswer": "Pas encore de réponse", + "session.prompts.queued": "En attente", "session.status.writingResponse": "Rédaction de la réponse…", "session.status.retry": "Nouvelle tentative…", "session.status.working": "En cours…", @@ -761,7 +765,8 @@ export const dict = { "prompt.placeholder.connecting": "Connexion au serveur...", "prompt.placeholder.error": "Échec de la connexion. Vérifiez le panneau de sortie ou redémarrez l'extension.", - "prompt.placeholder.default": "Tapez un message... (Entrée pour envoyer, Maj+Entrée pour un saut de ligne)", + "prompt.placeholder.default": + "Tapez un message, @ pour mentionner des fichiers... (Entrée pour envoyer, Maj+Entrée pour un saut de ligne)", "context.usage.sessionCost": "Coût de la session", "context.usage.olderSessions": "{{count}} sessions précédentes", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/it.ts b/packages/kilo-vscode/webview-ui/src/i18n/it.ts index 066cd30098..4f78744e77 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/it.ts @@ -574,6 +574,10 @@ export const dict = { "session.messages.scrollToBottom": "Scorri in fondo", "session.messages.initializing": "Inizializzazione...", "session.messages.taskStarting": "Avvio...", + "session.prompts.navLabel": "Navigatore dei prompt", + "session.prompts.tick": "Prompt {{index}} di {{total}}: {{prompt}}", + "session.prompts.noAnswer": "Nessuna risposta ancora", + "session.prompts.queued": "In coda", "session.status.writingResponse": "Scrittura risposta...", "session.status.retry": "Riprovo...", "session.status.working": "Al lavoro...", @@ -606,7 +610,8 @@ export const dict = { "ui.sessionTurn.status.consideringNextSteps": "Valutazione prossimi passi...", "dialog.model.noProviders": "Nessun provider", "prompt.placeholder.connecting": "Connessione al server...", - "prompt.placeholder.default": "Scrivi un messaggio... (Invio per inviare, Maiusc+Invio per nuova riga)", + "prompt.placeholder.default": + "Scrivi un messaggio, @ per menzionare i file... (Invio per inviare, Maiusc+Invio per nuova riga)", "prompt.placeholder.error": "Connessione non riuscita. Controlla il pannello output o riavvia l'estensione.", "context.usage.sessionCost": "Costo sessione", "context.usage.olderSessions": "{{count}} sessioni precedenti", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index b338abd210..983bc27a35 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -715,6 +715,10 @@ export const dict = { "session.messages.scrollToBottom": "下にスクロール", "session.messages.initializing": "初期化中...", "session.messages.taskStarting": "開始中...", + "session.prompts.navLabel": "プロンプトナビゲーター", + "session.prompts.tick": "プロンプト {{index}}/{{total}}: {{prompt}}", + "session.prompts.noAnswer": "まだ応答がありません", + "session.prompts.queued": "キューに追加済み", "session.status.writingResponse": "応答を作成中…", "session.status.retry": "再試行中…", "session.status.working": "作業中…", @@ -742,7 +746,7 @@ export const dict = { "prompt.placeholder.connecting": "サーバーに接続中...", "prompt.placeholder.error": "接続に失敗しました。出力パネルを確認するか、拡張機能を再起動してください。", - "prompt.placeholder.default": "メッセージを入力... (Enterで送信、Shift+Enterで改行)", + "prompt.placeholder.default": "メッセージを入力、@ でファイルを参照... (Enterで送信、Shift+Enterで改行)", "context.usage.sessionCost": "セッションコスト", "context.usage.olderSessions": "{{count}} 件の古いセッション", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index 20fc092585..0f3c269a6a 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -716,6 +716,10 @@ export const dict = { "session.messages.scrollToBottom": "하단으로 스크롤", "session.messages.initializing": "초기화 중...", "session.messages.taskStarting": "시작 중...", + "session.prompts.navLabel": "프롬프트 탐색기", + "session.prompts.tick": "프롬프트 {{index}}/{{total}}: {{prompt}}", + "session.prompts.noAnswer": "아직 응답이 없습니다", + "session.prompts.queued": "대기 중", "session.status.writingResponse": "응답 작성 중...", "session.status.retry": "재시도 중…", "session.status.working": "작업 중...", @@ -742,7 +746,7 @@ export const dict = { "prompt.placeholder.connecting": "서버에 연결 중...", "prompt.placeholder.error": "연결에 실패했습니다. 출력 패널을 확인하거나 확장 프로그램을 다시 시작하세요.", - "prompt.placeholder.default": "메시지를 입력하세요... (Enter로 전송, Shift+Enter로 줄 바꿈)", + "prompt.placeholder.default": "메시지를 입력하세요, @로 파일 언급... (Enter로 전송, Shift+Enter로 줄 바꿈)", "context.usage.sessionCost": "세션 비용", "context.usage.olderSessions": "{{count}}개의 이전 세션", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts index 5f0d90da4b..3a7f9f75fe 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts @@ -713,6 +713,10 @@ export const dict = { "session.messages.scrollToBottom": "Scroll naar beneden", "session.messages.initializing": "Initialiseren...", "session.messages.taskStarting": "Starten...", + "session.prompts.navLabel": "Promptnavigator", + "session.prompts.tick": "Prompt {{index}} van {{total}}: {{prompt}}", + "session.prompts.noAnswer": "Nog geen reactie", + "session.prompts.queued": "In wachtrij", "session.status.writingResponse": "Antwoord schrijven...", "session.status.retry": "Opnieuw proberen...", "session.status.working": "Bezig...", @@ -739,7 +743,8 @@ export const dict = { "dialog.model.noProviders": "Geen providers", "prompt.placeholder.connecting": "Verbinden met server...", - "prompt.placeholder.default": "Typ een bericht... (Enter om te verzenden, Shift+Enter voor nieuwe regel)", + "prompt.placeholder.default": + "Typ een bericht, @ om bestanden te vermelden... (Enter om te verzenden, Shift+Enter voor nieuwe regel)", "prompt.placeholder.error": "Verbinding mislukt. Controleer het uitvoerpaneel of herstart de extensie.", "context.usage.sessionCost": "Sessiekosten", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index f9a0d5f096..37e7266c76 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -683,6 +683,10 @@ export const dict = { "session.messages.scrollToBottom": "Rull til bunnen", "session.messages.initializing": "Initialiserer...", "session.messages.taskStarting": "Starter...", + "session.prompts.navLabel": "Ledetekstnavigering", + "session.prompts.tick": "Ledetekst {{index}} av {{total}}: {{prompt}}", + "session.prompts.noAnswer": "Ingen svar ennå", + "session.prompts.queued": "I kø", "session.status.writingResponse": "Skriver svar…", "session.status.retry": "Prøver på nytt…", "session.status.working": "Arbeider…", @@ -710,7 +714,8 @@ export const dict = { "prompt.placeholder.connecting": "Kobler til server...", "prompt.placeholder.error": "Tilkobling mislyktes. Sjekk utdatapanelet eller start utvidelsen på nytt.", - "prompt.placeholder.default": "Skriv en melding... (Enter for å sende, Shift+Enter for ny linje)", + "prompt.placeholder.default": + "Skriv en melding, @ for å nevne filer... (Enter for å sende, Shift+Enter for ny linje)", "context.usage.sessionCost": "Sesjonskostnad", "context.usage.olderSessions": "{{count}} eldre sesjoner", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index 99741deb92..4ab4ee01b1 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -679,6 +679,10 @@ export const dict = { "session.messages.scrollToBottom": "Przewiń na dół", "session.messages.initializing": "Inicjalizacja...", "session.messages.taskStarting": "Uruchamianie...", + "session.prompts.navLabel": "Nawigator promptów", + "session.prompts.tick": "Prompt {{index}} z {{total}}: {{prompt}}", + "session.prompts.noAnswer": "Jeszcze brak odpowiedzi", + "session.prompts.queued": "W kolejce", "session.status.writingResponse": "Pisanie odpowiedzi…", "session.status.retry": "Ponawianie…", "session.status.working": "Pracuję…", @@ -706,7 +710,8 @@ export const dict = { "prompt.placeholder.connecting": "Łączenie z serwerem...", "prompt.placeholder.error": "Połączenie nie powiodło się. Sprawdź panel wyjściowy lub uruchom ponownie rozszerzenie.", - "prompt.placeholder.default": "Wpisz wiadomość... (Enter, aby wysłać, Shift+Enter dla nowej linii)", + "prompt.placeholder.default": + "Wpisz wiadomość, @ aby wspomnieć pliki... (Enter, aby wysłać, Shift+Enter dla nowej linii)", "context.usage.sessionCost": "Koszt sesji", "context.usage.olderSessions": "{{count}} starszych sesji", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index 649c5b84d1..aa5519268c 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -720,6 +720,10 @@ export const dict = { "session.messages.scrollToBottom": "Прокрутить вниз", "session.messages.initializing": "Инициализация...", "session.messages.taskStarting": "Запуск...", + "session.prompts.navLabel": "Навигатор промптов", + "session.prompts.tick": "Промпт {{index}} из {{total}}: {{prompt}}", + "session.prompts.noAnswer": "Ответа пока нет", + "session.prompts.queued": "В очереди", "session.status.writingResponse": "Пишу ответ…", "session.status.retry": "Повторная попытка…", "session.status.working": "Работаю…", @@ -747,7 +751,8 @@ export const dict = { "prompt.placeholder.connecting": "Подключение к серверу...", "prompt.placeholder.error": "Не удалось подключиться. Проверьте панель вывода или перезапустите расширение.", - "prompt.placeholder.default": "Введите сообщение... (Enter для отправки, Shift+Enter для новой строки)", + "prompt.placeholder.default": + "Введите сообщение, @ чтобы упомянуть файлы... (Enter для отправки, Shift+Enter для новой строки)", "context.usage.sessionCost": "Стоимость сессии", "context.usage.olderSessions": "{{count}} предыдущих сессий", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index a6f5e657ee..7b9e4f3e4a 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -712,6 +712,10 @@ export const dict = { "session.messages.scrollToBottom": "เลื่อนไปด้านล่าง", "session.messages.initializing": "กำลังเริ่มต้น...", "session.messages.taskStarting": "กำลังเริ่มทำงาน...", + "session.prompts.navLabel": "ตัวนำทางพรอมต์", + "session.prompts.tick": "พรอมต์ {{index}} จาก {{total}}: {{prompt}}", + "session.prompts.noAnswer": "ยังไม่มีการตอบกลับ", + "session.prompts.queued": "อยู่ในคิว", "session.status.writingResponse": "กำลังเขียนคำตอบ...", "session.status.retry": "กำลังลองใหม่…", "session.status.working": "กำลังทำงาน...", @@ -739,7 +743,7 @@ export const dict = { "prompt.placeholder.connecting": "กำลังเชื่อมต่อกับเซิร์ฟเวอร์...", "prompt.placeholder.error": "การเชื่อมต่อล้มเหลว ตรวจสอบแผงเอาต์พุตหรือรีสตาร์ทส่วนขยาย", - "prompt.placeholder.default": "พิมพ์ข้อความ... (Enter เพื่อส่ง, Shift+Enter เพื่อขึ้นบรรทัดใหม่)", + "prompt.placeholder.default": "พิมพ์ข้อความ, @ เพื่ออ้างถึงไฟล์... (Enter เพื่อส่ง, Shift+Enter เพื่อขึ้นบรรทัดใหม่)", "context.usage.sessionCost": "ค่าใช้จ่ายเซสชัน", "context.usage.olderSessions": "{{count}} เซสชันก่อนหน้า", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts index 9a9da245a6..233e43fec9 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts @@ -707,6 +707,10 @@ export const dict = { "session.messages.scrollToBottom": "En alta kaydır", "session.messages.initializing": "Başlatılıyor...", "session.messages.taskStarting": "Başlıyor...", + "session.prompts.navLabel": "Komut gezgini", + "session.prompts.tick": "Komut {{index}} / {{total}}: {{prompt}}", + "session.prompts.noAnswer": "Henüz yanıt yok", + "session.prompts.queued": "Sırada", "session.status.writingResponse": "Yanıt yazılıyor...", "session.status.retry": "Yeniden deneniyor…", "session.status.working": "Çalışıyor...", @@ -733,7 +737,8 @@ export const dict = { "dialog.model.noProviders": "Sağlayıcı yok", "prompt.placeholder.connecting": "Sunucuya bağlanılıyor...", - "prompt.placeholder.default": "Bir mesaj yazın... (Göndermek için Enter, yeni satır için Shift+Enter)", + "prompt.placeholder.default": + "Bir mesaj yazın, dosyaları belirtmek için @ kullanın... (Göndermek için Enter, yeni satır için Shift+Enter)", "prompt.placeholder.error": "Bağlantı başarısız. Çıktı panelini kontrol edin veya uzantıyı yeniden başlatın.", "context.usage.sessionCost": "Oturum maliyeti", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts index f80fdf0f99..b2d8655b2d 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts @@ -709,6 +709,10 @@ export const dict = { "session.messages.scrollToBottom": "Прокрутити до низу", "session.messages.initializing": "Ініціалізація...", "session.messages.taskStarting": "Запуск...", + "session.prompts.navLabel": "Навігатор запитів", + "session.prompts.tick": "Запит {{index}} з {{total}}: {{prompt}}", + "session.prompts.noAnswer": "Відповіді ще немає", + "session.prompts.queued": "У черзі", "session.status.writingResponse": "Пишу відповідь...", "session.status.retry": "Повторна спроба…", "session.status.working": "Працює...", @@ -735,7 +739,8 @@ export const dict = { "dialog.model.noProviders": "Немає провайдерів", "prompt.placeholder.connecting": "Підключення до сервера...", - "prompt.placeholder.default": "Напишіть повідомлення... (Enter для надсилання, Shift+Enter для нового рядка)", + "prompt.placeholder.default": + "Напишіть повідомлення, @ щоб згадати файли... (Enter для надсилання, Shift+Enter для нового рядка)", "prompt.placeholder.error": "Підключення не вдалося. Перевірте панель виводу або перезапустіть розширення.", "context.usage.sessionCost": "Вартість сесії", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index ae31c74807..2bfc2279e4 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -696,6 +696,10 @@ export const dict = { "session.messages.scrollToBottom": "滚动到底部", "session.messages.initializing": "初始化中...", "session.messages.taskStarting": "启动中...", + "session.prompts.navLabel": "提示词导航", + "session.prompts.tick": "提示词 {{index}}/{{total}}:{{prompt}}", + "session.prompts.noAnswer": "暂无响应", + "session.prompts.queued": "已排队", "session.status.writingResponse": "正在撰写回复…", "session.status.retry": "正在重试…", "session.status.working": "处理中…", @@ -722,7 +726,7 @@ export const dict = { "prompt.placeholder.connecting": "正在连接服务器...", "prompt.placeholder.error": "连接失败。请检查输出面板或重启扩展。", - "prompt.placeholder.default": "输入消息... (Enter 发送,Shift+Enter 换行)", + "prompt.placeholder.default": "输入消息,用 @ 提及文件... (Enter 发送,Shift+Enter 换行)", "context.usage.sessionCost": "会话费用", "context.usage.olderSessions": "{{count}} 个较早的会话", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index 097657622d..fcdd8fbbf9 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -656,6 +656,10 @@ export const dict = { "session.messages.scrollToBottom": "捲動至底部", "session.messages.initializing": "初始化中...", "session.messages.taskStarting": "啟動中...", + "session.prompts.navLabel": "提示詞導覽", + "session.prompts.tick": "提示詞 {{index}}/{{total}}:{{prompt}}", + "session.prompts.noAnswer": "尚無回應", + "session.prompts.queued": "已排入佇列", "session.status.writingResponse": "正在撰寫回覆…", "session.status.retry": "正在重試…", "session.status.working": "處理中…", @@ -682,7 +686,7 @@ export const dict = { "prompt.placeholder.connecting": "正在連線至伺服器...", "prompt.placeholder.error": "連線失敗。請檢查輸出面板或重新啟動擴充功能。", - "prompt.placeholder.default": "輸入訊息... (Enter 送出,Shift+Enter 換行)", + "prompt.placeholder.default": "輸入訊息,用 @ 提及檔案... (Enter 送出,Shift+Enter 換行)", "context.usage.sessionCost": "工作階段費用", "context.usage.olderSessions": "{{count}} 個較早的工作階段", diff --git a/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx index 009b72bb17..7494a81d51 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx @@ -933,9 +933,9 @@ export const TabBarSingleTab: Story = { } // Side terminal panel inside the real inspector host chain, empty state — -// no live PTY, so the start affordance renders. The header reuses the -// .am-diff-header metrics so the a11y/screenshot baseline also guards the -// alignment against the diff panel chrome. +// no live PTY, so the start affordance renders. The tab strip header keeps +// the .am-diff-header height so the a11y/screenshot baseline also guards +// the alignment against the diff panel chrome. export const SideTerminalPanelEmpty: Story = { name: "Side terminal panel — empty", render: () => { @@ -953,6 +953,47 @@ export const SideTerminalPanelEmpty: Story = { state={state} contextKey={() => LOCAL} visible={() => true} + onSelect={() => undefined} + onClose={() => undefined} + onStart={() => undefined} + /> +
+
+
+
+ + ) + }, +} + +// Tab strip with several side terminals: the active one shows the X close +// button, the others reveal it on hover. Terminals point at a dead port — +// xterm renders its connection-error notice inside the panel, which keeps +// the story self-contained without a live PTY. +export const SideTerminalPanelTabs: Story = { + name: "Side terminal panel — tabs", + render: () => { + const state = createTerminalState(() => LOCAL) + const font = { fontFamily: "monospace", fontSize: 12 } + state.add(null, { id: "terminal:one", title: "Terminal 1", wsUrl: "ws://127.0.0.1:1/a", font, placement: "side" }) + state.add(null, { id: "terminal:two", title: "Terminal 2", wsUrl: "ws://127.0.0.1:1/b", font, placement: "side" }) + state.add(null, { id: "terminal:three", title: "Terminal 3", wsUrl: "ws://127.0.0.1:1/c", font, placement: "side" }) + state.setSideActive(LOCAL, "terminal:two") + state.setTitle("terminal:two", "npm run dev") + return ( + +
+
+
+ Agent session stays visible beside the terminal. +
+
+
+ LOCAL} + visible={() => true} + onSelect={(id) => state.setSideActive(LOCAL, id)} onClose={() => undefined} onStart={() => undefined} /> diff --git a/packages/kilo-vscode/webview-ui/src/stories/chat.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/chat.stories.tsx index 7d5e6705e9..4eb2cecdf5 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/chat.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/chat.stories.tsx @@ -617,6 +617,141 @@ export const ChatViewReadable420: Story = { render: () => renderReadableChat("busy"), } +// --------------------------------------------------------------------------- +// PromptRail — the left-edge tick rail and its hover card +// Several turns so the rail and card are populated: a long prompt, a short +// low-signal follow-up, a tool-only answer (empty preview), and a queued one. +// --------------------------------------------------------------------------- + +const railNow = 1_700_000_200_000 +const railTurn = (i: number, prompt: string, answer: string | undefined, queued = false) => { + const userID = `rail-user-${i}` + const assistantID = `rail-asst-${i}` + const messages: any[] = [{ id: userID, sessionID: SESSION_ID, role: "user", time: { created: railNow + i * 100 } }] + if (!queued) { + messages.push({ + id: assistantID, + sessionID: SESSION_ID, + role: "assistant", + parentID: userID, + time: { created: railNow + i * 100 + 50 }, + modelID: "claude-sonnet-4-20250514", + providerID: "anthropic", + mode: "default", + agent: "default", + path: { cwd: "/project", root: "/project" }, + }) + } + const parts: Record = { + [userID]: [{ id: `rail-part-user-${i}`, sessionID: SESSION_ID, messageID: userID, type: "text", text: prompt }], + } + if (!queued) { + parts[assistantID] = answer + ? [{ id: `rail-part-asst-${i}`, sessionID: SESSION_ID, messageID: assistantID, type: "text", text: answer }] + : [ + { + id: `rail-part-asst-${i}`, + sessionID: SESSION_ID, + messageID: assistantID, + type: "tool", + callID: `rail-call-${i}`, + tool: "bash", + state: { + status: "completed", + input: { command: "ls", description: "List files" }, + output: "a.ts b.ts", + title: "ls", + metadata: {}, + time: { start: railNow + i * 100 + 50, end: railNow + i * 100 + 80 }, + }, + }, + ] + } + return { messages, parts } +} + +const railTurns = [ + railTurn( + 1, + "Add a prompt navigator rail to the left edge of the chat that expands into a card of prompt and answer previews when I hover it, without shrinking the readable lane", + "Added PromptRail with a tick per prompt and a hover card; the lane width is untouched.", + ), + railTurn(2, "yes", "Confirmed — wiring it into MessageList next."), + railTurn(3, "run the tests", undefined), + railTurn( + 4, + "now do the same in the Agent Manager chat", + "ChatView → MessageList is shared, so the rail appears there automatically; no Agent Manager specific code needed.", + ), + railTurn(5, "looks good, ship it", "", true), +] +const railMessages = railTurns.flatMap((turn) => turn.messages) +const railParts = Object.assign({}, ...railTurns.map((turn) => turn.parts)) +const railData = { + ...defaultMockData, + message: { [SESSION_ID]: railMessages }, + part: railParts, +} + +const renderRailChat = (status: "idle" | "busy" = "idle") => { + const session = { + ...mockSessionValue({ id: SESSION_ID, status }), + messages: () => railMessages, + userMessages: () => railMessages.filter((msg) => msg.role === "user"), + } + return ( + + +
+ +
+
+
+ ) +} + +export const PromptRailWide: Story = { + name: "PromptRail - wide editor tab", + render: () => renderRailChat(), +} + +export const PromptRailSidebar: Story = { + name: "PromptRail - narrow sidebar", + render: () => renderRailChat("busy"), +} + +// Long session: more prompts than fit the transcript height, so the rail and +// the card both cap to the newest ones that fit. +const manyTurns = Array.from({ length: 40 }, (_, i) => + railTurn(100 + i, `Prompt number ${i + 1} in a long running session`, `Answer number ${i + 1}.`), +) +const manyMessages = manyTurns.flatMap((turn) => turn.messages) +const manyData = { + ...defaultMockData, + message: { [SESSION_ID]: manyMessages }, + part: Object.assign({}, ...manyTurns.map((turn) => turn.parts)), +} + +export const PromptRailManyPrompts: Story = { + name: "PromptRail - long session caps to what fits", + render: () => { + const session = { + ...mockSessionValue({ id: SESSION_ID, status: "idle" }), + messages: () => manyMessages, + userMessages: () => manyMessages.filter((msg) => msg.role === "user"), + } + return ( + + +
+ +
+
+
+ ) + }, +} + export const MessageListToolToQueuedUserSpacing: Story = { name: "MessageList — queued users stay at bottom", render: () => { diff --git a/packages/kilo-vscode/webview-ui/src/styles/chat.css b/packages/kilo-vscode/webview-ui/src/styles/chat.css index 9bd9a1b166..6f75206919 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/chat.css +++ b/packages/kilo-vscode/webview-ui/src/styles/chat.css @@ -10,6 +10,7 @@ @import "./search-menu.css"; @import "./session-tabs.css"; @import "./chat-layout.css"; +@import "./prompt-rail.css"; @import "./banners.css"; @import "./session-actions.css"; @import "./welcome.css"; diff --git a/packages/kilo-vscode/webview-ui/src/styles/prompt-rail.css b/packages/kilo-vscode/webview-ui/src/styles/prompt-rail.css new file mode 100644 index 0000000000..0e8392619d --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/styles/prompt-rail.css @@ -0,0 +1,213 @@ +/* ============================================ + Prompt Rail + ============================================ */ + +.prompt-rail { + --prompt-rail-width: 16px; + --prompt-rail-gap: 8px; + --prompt-rail-step: 14px; + --prompt-rail-ease: cubic-bezier(0.22, 1, 0.36, 1); + + position: absolute; + inset-inline-start: max( + 0px, + calc(50% - var(--chat-readable-width) / 2 - var(--prompt-rail-width) - var(--prompt-rail-gap)) + ); + top: 0; + bottom: 0; + width: var(--prompt-rail-width); + display: flex; + flex-direction: column; + justify-content: center; + /* Guards the rail against ever painting outside its own column, whatever + the tick count or panel height. */ + overflow: hidden; + padding: 12px 0; + pointer-events: none; + z-index: 2; + opacity: 0.5; + transition: opacity 0.25s var(--prompt-rail-ease); +} + +.message-list-container:hover .prompt-rail, +.prompt-rail:focus-within { + opacity: 1; +} + +.prompt-rail-tick { + pointer-events: auto; + display: flex; + align-items: center; + justify-content: flex-start; + height: var(--prompt-rail-step); + min-height: 4px; + width: 100%; + border: none; + background: none; + padding: 0; + cursor: pointer; + /* No outline box: the focus ring is drawn on the line itself, so a focused + tick reads as part of the rail instead of a rectangle floating over it. */ + outline: none; +} + +.prompt-rail-tick-line { + display: block; + height: 1.5px; + width: 9px; + border-radius: 1px; + background: var(--icon-base); + opacity: 0.7; + transition: + width 0.24s var(--prompt-rail-ease), + opacity 0.2s var(--prompt-rail-ease), + background-color 0.2s var(--prompt-rail-ease), + box-shadow 0.2s var(--prompt-rail-ease); +} + +.prompt-rail-tick[data-queued] .prompt-rail-tick-line { + background: var(--icon-weaker); + opacity: 0.5; +} + +/* Scroll position: subtle, always-on cue. */ +.prompt-rail-tick--active .prompt-rail-tick-line { + width: 13px; + opacity: 1; + background: var(--icon-strong-base); +} + +/* Pointer or keyboard target: the loud state, full width. */ +.prompt-rail-tick:hover .prompt-rail-tick-line, +.prompt-rail-tick--open .prompt-rail-tick-line { + width: 100%; + height: 2px; + opacity: 1; + background: var(--icon-strong-base); +} + +.prompt-rail-tick:focus-visible .prompt-rail-tick-line { + width: 100%; + height: 2px; + opacity: 1; + background: var(--icon-strong-base); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--vscode-focusBorder) 50%, transparent); +} + +/* ============================================ + Prompt Rail card + ============================================ */ + +.prompt-rail-card { + position: fixed; + z-index: 1000; + box-sizing: border-box; + width: min(360px, calc(100vw - var(--prompt-rail-width, 16px) - 40px)); + max-height: calc(100vh - 24px); + overflow-y: auto; + padding: 6px; + border-radius: 12px; + /* Reads as floating glass rather than a panel pasted over the transcript: + the text behind stays faintly perceptible through the blur, so the card + feels like it belongs to the same surface. Falls back to an opaque fill + where backdrop-filter is unavailable. */ + background: color-mix(in srgb, var(--surface-float-base) 78%, transparent); + backdrop-filter: blur(20px) saturate(1.6); + -webkit-backdrop-filter: blur(20px) saturate(1.6); + box-shadow: + inset 0 0 0 1px color-mix(in srgb, var(--text-strong) 7%, transparent), + 0 2px 6px -2px rgba(0, 0, 0, 0.22), + 0 12px 32px -8px rgba(0, 0, 0, 0.4); + animation: prompt-rail-in 0.22s var(--prompt-rail-ease, cubic-bezier(0.22, 1, 0.36, 1)); + transform-origin: left center; + scrollbar-width: thin; +} + +@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) { + .prompt-rail-card { + background: var(--surface-float-base); + } +} + +@keyframes prompt-rail-in { + from { + opacity: 0; + transform: translateX(-6px) scale(0.98); + } + to { + opacity: 1; + transform: translateX(0) scale(1); + } +} + +.prompt-rail-row { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; + width: 100%; + box-sizing: border-box; + border: none; + background: none; + padding: 9px 11px; + border-radius: 8px; + text-align: start; + font: inherit; + cursor: pointer; + outline: none; + transition: background-color 0.16s var(--prompt-rail-ease, ease); +} + +.prompt-rail-row--hover, +.prompt-rail-row:hover { + background: color-mix(in srgb, var(--text-strong) 8%, transparent); +} + +.prompt-rail-row:active { + background: color-mix(in srgb, var(--text-strong) 12%, transparent); +} + +.prompt-rail-row-prompt { + font-size: var(--kilo-font-size-13); + line-height: var(--line-height-normal); + color: var(--text-strong); + font-weight: var(--font-weight-medium, 500); + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + min-width: 0; + width: 100%; +} + +.prompt-rail-row-prompt[data-queued] { + color: var(--text-weak); +} + +.prompt-rail-row-status { + color: var(--text-weaker); + font-weight: 400; +} + +.prompt-rail-row-answer { + font-size: var(--kilo-font-size-12); + line-height: var(--line-height-large, 1.5); + color: var(--text-weak); + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + min-width: 0; + width: 100%; +} + +@media (prefers-reduced-motion: reduce) { + .prompt-rail, + .prompt-rail-tick-line, + .prompt-rail-row { + transition: none; + } + .prompt-rail-card { + animation: none; + } +} diff --git a/packages/kilo-vscode/webview-ui/src/styles/question-dock.css b/packages/kilo-vscode/webview-ui/src/styles/question-dock.css index 2d25d339d7..d31af72f2c 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/question-dock.css +++ b/packages/kilo-vscode/webview-ui/src/styles/question-dock.css @@ -29,7 +29,8 @@ align-items: center; justify-content: space-between; gap: 6px; - padding: 6px 8px 4px 12px; + padding-block: 6px 4px; + padding-inline: 12px 8px; cursor: pointer; } @@ -176,7 +177,7 @@ background-color: transparent; border: none; border-radius: 6px; - text-align: left; + text-align: start; width: 100%; cursor: pointer; outline: none; diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/sessions.ts b/packages/kilo-vscode/webview-ui/src/types/messages/sessions.ts index 90a5733027..1322349e28 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/sessions.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/sessions.ts @@ -3,7 +3,7 @@ import type { Part, TokenUsage } from "./parts" export type SessionModelUsage = KilocodeSessionModelUsageResponse -export type SessionCloseReason = "completed" | "error" | "interrupted" +export type SessionCloseReason = "completed" | "error" | "interrupted" | "superseded" // Message structure (simplified for webview) export interface Message { diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts index 16fcfe59e5..f5dc4a2035 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts @@ -695,6 +695,12 @@ export interface ShowLocalTerminalRequest { type: "agentManager.showLocalTerminal" } +// Show a terminal rooted at a worktree directory (worktree has no session) +export interface ShowWorktreeTerminalRequest { + type: "agentManager.showWorktreeTerminal" + worktreeId: string +} + // Open a worktree directory in VS Code export interface OpenWorktreeRequest { type: "agentManager.openWorktree" @@ -1336,6 +1342,7 @@ export type WebviewMessage = | StopRunScriptRequest | ShowTerminalRequest | ShowLocalTerminalRequest + | ShowWorktreeTerminalRequest | OpenWorktreeRequest | CopyToClipboardRequest | ShowExistingLocalTerminalRequest diff --git a/packages/kilo-web-ui/package.json b/packages/kilo-web-ui/package.json index 94abbda32a..a831a4ea50 100644 --- a/packages/kilo-web-ui/package.json +++ b/packages/kilo-web-ui/package.json @@ -1,6 +1,6 @@ { "name": "@kilocode/kilo-web-ui", - "version": "7.4.16", + "version": "7.4.17", "type": "module", "license": "MIT", "exports": { diff --git a/packages/llm/package.json b/packages/llm/package.json index 892d40dce2..fe0a6c4e55 100644 --- a/packages/llm/package.json +++ b/packages/llm/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "7.4.16", + "version": "7.4.17", "name": "@opencode-ai/llm", "type": "module", "license": "MIT", diff --git a/packages/opencode/CHANGELOG.md b/packages/opencode/CHANGELOG.md index 707c839036..13eef058d4 100644 --- a/packages/opencode/CHANGELOG.md +++ b/packages/opencode/CHANGELOG.md @@ -1,5 +1,64 @@ # @kilocode/cli +## 7.4.17 + +### Patch Changes + +- [#12544](https://github.com/Kilo-Org/kilocode/pull/12544) [`b8d83fb`](https://github.com/Kilo-Org/kilocode/commit/b8d83fb537040afd6632a6d893acc412395832e4) - Support adaptive thinking levels for Claude Opus and Sonnet 5 and later. + +- [#12587](https://github.com/Kilo-Org/kilocode/pull/12587) [`16f8e7e`](https://github.com/Kilo-Org/kilocode/commit/16f8e7ef7fbd47755395539e7df54af3baae0c63) - Keep conversations and workspace files unchanged when a checkpoint cannot be fully restored. + +- [#12444](https://github.com/Kilo-Org/kilocode/pull/12444) [`92076e7`](https://github.com/Kilo-Org/kilocode/commit/92076e7071084b4bf2ce87d90eb6d45a502c836c) Thanks [@IamCoder18](https://github.com/IamCoder18)! - Add a `/auto-approve` slash command in the TUI for toggling auto-approve mode, with aliases `/autoapprove`, `/approve-all`, and `/approveall`. The command dispatches the existing palette entry, so behavior matches the Ctrl+P "Enable/Disable auto-approve mode" toggle. + +- [#11986](https://github.com/Kilo-Org/kilocode/pull/11986) [`0abe474`](https://github.com/Kilo-Org/kilocode/commit/0abe474b6d5c5d482ce950abfd9033bf0c5af3b4) Thanks [@IamCoder18](https://github.com/IamCoder18)! - Let the Context section in the TUI session sidebar collapse and expand on header click, matching the existing collapsible pattern used by Token Usage, Models, and Terminal Bench 2.0. When collapsed, the header shows a one-line summary of percent used and total cost. + +- [#12601](https://github.com/Kilo-Org/kilocode/pull/12601) [`dab2e79`](https://github.com/Kilo-Org/kilocode/commit/dab2e79d6ecc24acfd8737a10dfdf8ef02765b30) - Exclude GPT-5.6 from models available through ChatGPT subscriptions while retaining access to variants such as GPT-5.6 Sol. + +- [#12592](https://github.com/Kilo-Org/kilocode/pull/12592) [`8c88048`](https://github.com/Kilo-Org/kilocode/commit/8c880487818728f41ffc3087d27d6ce6b4591b53) Thanks [@noobezlol](https://github.com/noobezlol)! - Keep Nix builds on the Bun version required by the repository. + +- [#12545](https://github.com/Kilo-Org/kilocode/pull/12545) [`b2735bf`](https://github.com/Kilo-Org/kilocode/commit/b2735bfbc9df170274a12ec4786106dacb61090f) - Fix session transcripts losing their final messages when the CLI exits — pending uploads are now flushed on shutdown and as soon as a session closes. + +- [#12470](https://github.com/Kilo-Org/kilocode/pull/12470) [`c0ebf98`](https://github.com/Kilo-Org/kilocode/commit/c0ebf987789ab6fa070106219ebc8c46cd0105af) Thanks [@IamCoder18](https://github.com/IamCoder18)! - Route the websearch tool's Exa requests through the Kilo proxy when signed into Kilo. The MCP-Exa transport is preserved as a fallback for users who set `EXA_API_KEY` or are not authenticated. A new `KILO_WEBSEARCH_PROVIDER=kilo-exa` env override forces the Kilo proxy path. Results are capped at 10. + +- [#12460](https://github.com/Kilo-Org/kilocode/pull/12460) [`51d8031`](https://github.com/Kilo-Org/kilocode/commit/51d8031c9997bd5478bcde715562169f732d04d4) - Changes from opencode v1.17.5 to v1.17.9 upstream: + - Core Bugfixes: Improved MCP server compatibility by declaring Kilo's supported client capabilities. + - Core Bugfixes: Plugin client requests now reuse the active server instead of assuming the default local port. + - Core Bugfixes: ACP shell tool calls now show the command and working directory from the start. + - Core Bugfixes: Plugin-provided shell environment variables now apply to PTY sessions. + - Core Bugfixes: OpenAI-compatible providers now accept MCP tool schemas that previously failed validation. (@jquense) + - Core Bugfixes: Cloudflare AI Gateway now receives the configured API key correctly. (@keefetang) + - Core Bugfixes: MCP tools without declared schema properties now work with providers that expect object properties. + - Core Bugfixes: Long-running MCP tools now keep their timeout alive when they report progress. (@Nomadcxx) + - Core Bugfixes: The MCP OAuth callback server now shuts down once authorization finishes or is cancelled. + - Core Bugfixes: MCP tool failures now surface the server's error text instead of a generic failure. + - Core Bugfixes: MCP OAuth error pages now escape provider error text correctly. + - Core Bugfixes: Honor configured agent step limits by forcing a final text response instead of failing mid-run. + - Core Bugfixes: Queue steering prompts before dismissing pending questions so the previous turn cannot resume first. + - Core Bugfixes: Prevent local server credentials from leaking into spawned PTY processes. + - Core Bugfixes: Fix Devstral model detection when provider IDs use different casing. (@Robin1987China) + - Core Bugfixes: Pass configured custom headers to Copilot model requests. + - Core Improvements: MCP servers can now receive the current workspace as a client root. + - Core Improvements: Session timelines load much faster and avoid flicker or scroll jumps. + - Core Improvements: Add `high` and `max` thinking variants for GLM-5.2 across supported providers. (@imranshaiedi-byte) + - Core Improvements: Stop wrapping follow-up user messages in a steering reminder so prompt caching stays effective. + - TUI Bugfixes: MCP debug now uses the SDK's latest protocol version. + - TUI Bugfixes: Only show the background subagent shortcut when the server supports it. + - UI Bugfixes: Render completed Mermaid blocks from diagram source instead of fenced Markdown. + +- [#12585](https://github.com/Kilo-Org/kilocode/pull/12585) [`a0a760e`](https://github.com/Kilo-Org/kilocode/commit/a0a760e00e915a800125f03db7e08381ddc63e2a) - Fix bash permission rules being bypassed on PowerShell for commands containing a bare `--` such as `git checkout -- `. Commands the shell parser cannot parse now get checked against their raw command text instead of executing without a permission check. + +- [#12505](https://github.com/Kilo-Org/kilocode/pull/12505) [`bcf8b8b`](https://github.com/Kilo-Org/kilocode/commit/bcf8b8b9a852969ee842783e33a7fe32f9b3c3b8) - Emit each agent event once from `kilo run --format json`. + +- [#12593](https://github.com/Kilo-Org/kilocode/pull/12593) [`160b066`](https://github.com/Kilo-Org/kilocode/commit/160b06661acc5f04b21221ab6578c468325f64c5) - Prevent the VS Code backend from eagerly starting native file watchers for every Agent Manager worktree. + +- [#12583](https://github.com/Kilo-Org/kilocode/pull/12583) [`1310c12`](https://github.com/Kilo-Org/kilocode/commit/1310c1200ab613b316f27fe4fd59e23e262df02f) Thanks [@noobezlol](https://github.com/noobezlol)! - Keep Windows snapshot diffs parseable and preserve valid files when a stored patch is malformed. + +- [#12588](https://github.com/Kilo-Org/kilocode/pull/12588) [`deddf00`](https://github.com/Kilo-Org/kilocode/commit/deddf0012fe36c5bb8072f4378abd444fbd134fe) - Bound the wait for a provider's first response byte by the request timeout. A provider that accepts a request and returns headers but never sends body data now fails and retries instead of leaving the turn hanging after a tool call completes. The same `timeout` value now covers both the connection phase and the wait for the first byte as a single deadline; streaming responses that have already produced data are unaffected. + +- [#12514](https://github.com/Kilo-Org/kilocode/pull/12514) [`a33493e`](https://github.com/Kilo-Org/kilocode/commit/a33493e7222857a5c9f5e2c09a17312781567b3f) - Stabilize cross-platform CLI subprocess tests under constrained CI runners + +- [#12639](https://github.com/Kilo-Org/kilocode/pull/12639) [`8a47d8b`](https://github.com/Kilo-Org/kilocode/commit/8a47d8b78885fa8fd14c73b3aecdb57e1fc96c9c) - Stop flashing a "Turn interrupted" warning when a follow-up message is queued while the assistant is still working. The running turn now closes with a dedicated "superseded" reason instead of "interrupted" when it hands off to the queued prompt, so the premature-stop warning only appears for real interruptions. + ## 7.4.16 ### Minor Changes diff --git a/packages/opencode/package.json b/packages/opencode/package.json index ba63712656..e05ad63b16 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "7.4.16", + "version": "7.4.17", "name": "@kilocode/cli", "type": "module", "license": "MIT", diff --git a/packages/opencode/script/kilocode/npm-publish.ts b/packages/opencode/script/kilocode/npm-publish.ts index 2744ff675e..8f07ed2e75 100644 --- a/packages/opencode/script/kilocode/npm-publish.ts +++ b/packages/opencode/script/kilocode/npm-publish.ts @@ -3,6 +3,10 @@ export namespace NpmPublish { const base = 10_000 const jitter = 5_000 + export function aliases(channel: string) { + return channel === "latest" ? ["rc"] : [] + } + export async function retry(input: { name: string version: string diff --git a/packages/opencode/script/publish.ts b/packages/opencode/script/publish.ts index bd94c7c0e3..cfc99cacab 100755 --- a/packages/opencode/script/publish.ts +++ b/packages/opencode/script/publish.ts @@ -16,18 +16,21 @@ async function publish(dir: string, name: string, version: string) { // GitHub artifact downloads can drop the executable bit, and Docker uses the // unpacked dist binaries directly rather than the published tarball. if (process.platform !== "win32") await $`chmod -R 755 .`.cwd(dir) - if (await published(name, version)) { - console.log(`already published ${name}@${version}`) - return - } - await $`bun pm pack`.cwd(dir) // kilocode_change start - await NpmPublish.retry({ - name, - version, - run: () => $`npm publish *.tgz --access public --tag ${Script.channel} --provenance`.cwd(dir), - exists: () => published(name, version), - }) + const exists = await published(name, version) + if (exists) { + console.log(`already published ${name}@${version}`) + } + if (!exists) { + await $`bun pm pack`.cwd(dir) + await NpmPublish.retry({ + name, + version, + run: () => $`npm publish *.tgz --access public --tag ${Script.channel} --provenance`.cwd(dir), + exists: () => published(name, version), + }) + } + for (const tag of NpmPublish.aliases(Script.channel)) await $`npm dist-tag add ${name}@${version} ${tag}` // kilocode_change end } diff --git a/packages/opencode/src/kilocode/cli/cmd/tui/app.tsx b/packages/opencode/src/kilocode/cli/cmd/tui/app.tsx index b3c337c09c..543750842f 100644 --- a/packages/opencode/src/kilocode/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/kilocode/cli/cmd/tui/app.tsx @@ -282,7 +282,10 @@ export function init() { ? "Disable auto-approve mode" : "Enable auto-approve mode" }, + desc: "Toggle auto-approve for all permission prompts, saved to global config", category: "System", + slashName: "auto-approve", + slashAliases: ["autoapprove", "approve-all", "approveall"], run: async () => { const enabled = isAllowEverything(sync.data.config.permission) const result = await sdk.client.permission.allowEverything({ enable: !enabled }) diff --git a/packages/opencode/src/kilocode/memory/turn.ts b/packages/opencode/src/kilocode/memory/turn.ts index c41068c0e3..d9fdc79ae7 100644 --- a/packages/opencode/src/kilocode/memory/turn.ts +++ b/packages/opencode/src/kilocode/memory/turn.ts @@ -78,7 +78,9 @@ export namespace MemoryLifecycle { if (!enabled) return yield* MemoryTurn.close({ sessionID: evt.properties.sessionID, - reason: evt.properties.reason, + // A superseded turn handed off to a queued follow-up after draining + // its step; for digest purposes it was cut short like an interrupt. + reason: evt.properties.reason === "superseded" ? "interrupted" : evt.properties.reason, sessions: input.sessions, summary: input.summary, provider: input.provider, diff --git a/packages/opencode/src/kilocode/session/event.ts b/packages/opencode/src/kilocode/session/event.ts index 47025588a8..e8a851211e 100644 --- a/packages/opencode/src/kilocode/session/event.ts +++ b/packages/opencode/src/kilocode/session/event.ts @@ -2,7 +2,10 @@ import { BusEvent } from "@/bus/bus-event" import { MessageID, SessionID } from "@/session/schema" import { Schema } from "effect" -const CloseReason = Schema.Literals(["completed", "error", "interrupted"]) +// "superseded": the turn handed off to a queued follow-up after draining its +// current step. Distinct from "interrupted" so clients do not surface a +// premature-stop warning for a deliberate queue handoff. +const CloseReason = Schema.Literals(["completed", "error", "interrupted", "superseded"]) export const KiloSessionEvent = { TurnOpen: BusEvent.define( diff --git a/packages/opencode/src/kilocode/tool/websearch-kilo-exa.ts b/packages/opencode/src/kilocode/tool/websearch-kilo-exa.ts new file mode 100644 index 0000000000..1e590b6c18 --- /dev/null +++ b/packages/opencode/src/kilocode/tool/websearch-kilo-exa.ts @@ -0,0 +1,74 @@ +// kilocode_change - new file +import { Duration, Effect, Schema } from "effect" +import { HttpClient, HttpClientRequest } from "effect/unstable/http" +import { KILO_API_BASE } from "@kilocode/kilo-gateway" + +export const KILO_EXA_URL = `${KILO_API_BASE}/api/exa/search` +export const MAX_KILO_EXA_RESULTS = 10 + +const ExaResult = Schema.Struct({ + title: Schema.optional(Schema.String), + url: Schema.String, + publishedDate: Schema.optional(Schema.String), + author: Schema.optional(Schema.String), + highlights: Schema.optional(Schema.Array(Schema.String)), +}) + +const ExaResponse = Schema.Struct({ + results: Schema.Array(ExaResult), +}) + +const NO_RESULTS = "No search results found. Please try a different query." + +const formatResults = (data: Schema.Schema.Type): string => { + if (data.results.length === 0) return NO_RESULTS + return data.results + .map((r, i) => { + const head = `[${i + 1}] ${r.title ?? r.url}\n${r.url}${r.publishedDate ? ` (${r.publishedDate})` : ""}` + const hl = r.highlights?.length ? `\n${r.highlights.map((h) => `> ${h}`).join("\n")}` : "" + return `${head}${hl}` + }) + .join("\n\n") +} + +export type KiloExaParams = { + query: string + type?: string + numResults?: number +} + +export const callKiloExa = Effect.fn("WebSearchKiloExa.call")(function* ( + http: HttpClient.HttpClient, + params: KiloExaParams, + kiloToken: string, +) { + const numResults = Math.min(params.numResults ?? MAX_KILO_EXA_RESULTS, MAX_KILO_EXA_RESULTS) + const request = yield* HttpClientRequest.post(KILO_EXA_URL).pipe( + HttpClientRequest.bearerToken(kiloToken), + HttpClientRequest.acceptJson, + HttpClientRequest.bodyJson({ + query: params.query, + type: params.type ?? "auto", + numResults, + contents: { highlights: true }, + }), + ) + const response = yield* http.execute(request).pipe( + Effect.timeoutOrElse({ + duration: Duration.seconds(25), + orElse: () => Effect.die(new Error("kilo exa request timed out")), + }), + ) + const status = response.status + if (status === 401 || status === 403) { + return yield* Effect.die(new Error(`Kilo exa request unauthorized (${status}); sign in with \`kilo auth login\``)) + } + if (status < 200 || status >= 300) { + const body = yield* response.text + return yield* Effect.die(new Error(`Kilo exa request failed (${status}): ${body.slice(0, 200)}`)) + } + const data = yield* response.json + const decode = Schema.decodeUnknownEffect(ExaResponse) + const parsed = yield* decode(data).pipe(Effect.orDie) + return formatResults(parsed) +}) diff --git a/packages/opencode/src/plugin/openai/codex.ts b/packages/opencode/src/plugin/openai/codex.ts index 262882669f..5a3a6fc9f9 100644 --- a/packages/opencode/src/plugin/openai/codex.ts +++ b/packages/opencode/src/plugin/openai/codex.ts @@ -29,7 +29,7 @@ const ALLOWED_MODELS = new Set([ // kilocode_change end ]) // kilocode_change start -const DISALLOWED_MODELS = new Set(["gpt-5.5-pro"]) +const DISALLOWED_MODELS = new Set(["gpt-5.5-pro", "gpt-5.6"]) // kilocode_change end interface PkceCodes { diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index f1520cc2e6..7bee943356 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1848,9 +1848,11 @@ export const layer = Layer.effect( // kilocode_change start — break out so a newer queued prompt can take over // instead of starting another LLM step for the now-superseded turn. The // current handle.process has fully drained (tokens + inline tool calls) by - // the time we get here, so nothing is cut off. + // the time we get here, so nothing is cut off. The close reason is + // "superseded", not "interrupted": this is a deliberate queue handoff, + // not a premature stop, so clients must not flash an interruption warning. if (KiloSessionPromptQueue.hasFollowup(sessionID)) { - closeReasons.set(sessionID, "interrupted") + closeReasons.set(sessionID, "superseded") return "break" as const } // kilocode_change end diff --git a/packages/opencode/src/tool/websearch.ts b/packages/opencode/src/tool/websearch.ts index 12da7fbb3a..9a812cfc62 100644 --- a/packages/opencode/src/tool/websearch.ts +++ b/packages/opencode/src/tool/websearch.ts @@ -1,16 +1,20 @@ -import { Effect, Schema } from "effect" +import { Effect, Option, Schema } from "effect" // kilocode_change - Option added for kilo-exa transport dispatch import { HttpClient } from "effect/unstable/http" import * as Tool from "./tool" import * as McpWebSearch from "./mcp-websearch" +import * as KiloExa from "@/kilocode/tool/websearch-kilo-exa" // kilocode_change - Kilo-REST Exa transport import DESCRIPTION from "./websearch.txt" import { checksum } from "@opencode-ai/core/util/encode" import { InstallationVersion } from "@opencode-ai/core/installation/version" import { RuntimeFlags } from "@/effect/runtime-flags" +import { Auth } from "@/auth" // kilocode_change - source Kilo bearer for Kilo-REST transport + +const MAX_RESULTS = 10 // kilocode_change - cap numResults across all transports export const Parameters = Schema.Struct({ query: Schema.String.annotate({ description: "Websearch query" }), numResults: Schema.optional(Schema.Number).annotate({ - description: "Number of search results to return (default: 8)", + description: "Number of search results to return (default: 8, maximum: 10)", // kilocode_change - note MAX_RESULTS cap }), livecrawl: Schema.optional(Schema.Literals(["fallback", "preferred"])).annotate({ description: @@ -24,12 +28,12 @@ export const Parameters = Schema.Struct({ }), }) -const WebSearchProviderSchema = Schema.Literals(["exa", "parallel"]) +const WebSearchProviderSchema = Schema.Literals(["exa", "parallel", "kilo-exa"]) // kilocode_change - kilo-exa env override export type WebSearchProvider = Schema.Schema.Type export function selectWebSearchProvider(sessionID: string, flags = { exa: false, parallel: false }): WebSearchProvider { const override = process.env.KILO_WEBSEARCH_PROVIDER - if (override === "exa" || override === "parallel") return override + if (override === "exa" || override === "parallel" || override === "kilo-exa") return override // kilocode_change - kilo-exa env override if (flags.parallel) return "parallel" if (flags.exa) return "exa" @@ -38,7 +42,7 @@ export function selectWebSearchProvider(sessionID: string, flags = { exa: false, export function webSearchProviderLabel(provider: unknown) { if (provider === "parallel") return "Parallel Web Search" - if (provider === "exa") return "Exa Web Search" + if (provider === "exa" || provider === "kilo-exa") return "Exa Web Search" // kilocode_change - kilo-exa shares label return "Web Search" } @@ -88,7 +92,7 @@ function callProvider( { query: params.query, type: params.type || "auto", - numResults: params.numResults || 8, + numResults: Math.min(params.numResults || 8, MAX_RESULTS), // kilocode_change - cap at MAX_RESULTS livecrawl: params.livecrawl || "fallback", contextMaxCharacters: params.contextMaxCharacters, }, @@ -101,6 +105,7 @@ export const WebSearchTool = Tool.define( Effect.gen(function* () { const http = yield* HttpClient.HttpClient const flags = yield* RuntimeFlags.Service + const authSvc = yield* Auth.Service // kilocode_change - source Kilo bearer for Kilo-REST transport return { get description() { @@ -114,7 +119,36 @@ export const WebSearchTool = Tool.define( parallel: flags.enableParallel, }) const title = webSearchProviderLabel(provider) - yield* ctx.metadata({ title: `${title} "${params.query}"`, metadata: { provider } }) + // kilocode_change start - Kilo-REST Exa transport + // Precedence: + // provider="kilo-exa" -> kilo-rest (auth required) + // provider="exa" + EXA_API_KEY -> mcp-exa-byok (BYOK wins) + // provider="exa" + Kilo auth -> kilo-rest (new default for authed users) + // provider="exa" + no auth -> mcp-exa-unauth (preserves current fallback) + // provider="parallel" -> mcp-parallel (unchanged) + const kiloToken = yield* Effect.gen(function* () { + if (provider !== "exa" && provider !== "kilo-exa") return undefined as string | undefined + const info = yield* authSvc.get("kilo") + if (!info) return undefined + return info.type === "api" ? info.key : info.type === "oauth" ? info.access : undefined + }) + const transport = + provider === "kilo-exa" + ? "kilo-rest" + : provider === "parallel" + ? "mcp-parallel" + : provider === "exa" && process.env.EXA_API_KEY + ? "mcp-exa-byok" + : provider === "exa" && kiloToken + ? "kilo-rest" + : "mcp-exa-unauth" + // kilocode_change end + // kilocode_change start - add transport to metadata + yield* ctx.metadata({ + title: `${title} "${params.query}"`, + metadata: { provider, transport }, + }) + // kilocode_change end yield* ctx.ask({ permission: "websearch", @@ -130,12 +164,26 @@ export const WebSearchTool = Tool.define( }, }) - const result = yield* callProvider(http, provider, params, ctx) + // kilocode_change start - dispatch Kilo-REST transport + const result = yield* transport === "kilo-rest" + ? kiloToken + ? KiloExa.callKiloExa( + http, + { + query: params.query, + type: params.type, + numResults: params.numResults, + }, + kiloToken, + ) + : Effect.die(new Error("KILO_WEBSEARCH_PROVIDER=kilo-exa requires Kilo auth; run `kilo auth login`")) + : callProvider(http, provider, params, ctx) + // kilocode_change end return { output: result ?? "No search results found. Please try a different query.", title: `${title}: ${params.query}`, - metadata: { provider }, + metadata: { provider, transport }, // kilocode_change - add transport } }).pipe(Effect.orDie), } diff --git a/packages/opencode/test/kilocode/npm-publish.test.ts b/packages/opencode/test/kilocode/npm-publish.test.ts index 62e9434986..33318925ce 100644 --- a/packages/opencode/test/kilocode/npm-publish.test.ts +++ b/packages/opencode/test/kilocode/npm-publish.test.ts @@ -1,6 +1,16 @@ import { describe, expect, test } from "bun:test" import { NpmPublish } from "../../script/kilocode/npm-publish" +describe("npm publish aliases", () => { + test("promotes stable releases to the rc channel", () => { + expect(NpmPublish.aliases("latest")).toEqual(["rc"]) + }) + + test("does not promote prereleases", () => { + expect(NpmPublish.aliases("rc")).toEqual([]) + }) +}) + describe("npm publish retry", () => { test("returns after the first successful attempt", async () => { const calls = { run: 0, exists: 0, sleep: 0 } diff --git a/packages/opencode/test/kilocode/session-prompt-queue.test.ts b/packages/opencode/test/kilocode/session-prompt-queue.test.ts index dd5f90a017..d6a6178802 100644 --- a/packages/opencode/test/kilocode/session-prompt-queue.test.ts +++ b/packages/opencode/test/kilocode/session-prompt-queue.test.ts @@ -549,6 +549,96 @@ describe("session prompt queue", () => { } }) + test("closes a queued-handoff turn as superseded, not interrupted", async () => { + const ready = Promise.withResolvers() + const release = Promise.withResolvers() + const server = Bun.serve({ + port: 0, + async fetch(req) { + const url = new URL(req.url) + if (!url.pathname.endsWith("/chat/completions")) return new Response("not found", { status: 404 }) + + // Hold every stream open until the follow-up prompt is queued, so + // runLoop deterministically takes the hasFollowup break once its + // current step drains. Forked title/summary calls get held too; they + // are Effect.ignore'd and drain once released. + ready.resolve() + const stream = reply({ text: "reply", wait: release.promise }) + return new Response(stream, { + status: 200, + headers: { "Content-Type": "text/event-stream" }, + }) + }, + }) + + try { + await using tmp = await tmpdir({ + git: true, + init: async (dir) => { + await Bun.write(path.join(dir, "opencode.json"), JSON.stringify(providerCfg(server.url.origin))) + }, + }) + + await provideTestInstance({ + directory: tmp.path, + fn: async () => + scoped(tmp.path, async (prompt) => { + const closed: KiloSession.CloseReason[] = [] + const unsubscribe = Bus.subscribe(KiloSession.Event.TurnClose, (event) => { + closed.push(event.properties.reason) + }) + + const session = await sessions.create({ title: "Superseded close reason" }) + const first = Effect.runPromise( + prompt.prompt({ + sessionID: session.id, + agent: "code", + parts: [{ type: "text", text: "first prompt" }], + }), + ) + + // A request reaching the mock implies the turn loop is running + // (forked title/summary calls fire from step 1 of the loop). + await ready.promise + const second = Effect.runPromise( + prompt.prompt({ + sessionID: session.id, + agent: "code", + parts: [{ type: "text", text: "second prompt" }], + }), + ) + + // Wait until the follow-up is actually queued behind the in-flight + // turn, then let the first stream drain so runLoop hands off. + await Effect.runPromise( + pollWithTimeout( + Effect.sync(() => (KiloSessionPromptQueue.hasFollowup(session.id) ? (true as const) : undefined)), + "follow-up prompt never queued behind the in-flight turn", + "3 seconds", + ), + ) + release.resolve() + + expect((await first).info.role).toBe("assistant") + expect((await second).info.role).toBe("assistant") + // Bus delivery is a microtask chain; flush a macrotask so the last + // TurnClose callback lands before asserting. + await new Promise((resolve) => setTimeout(resolve, 0)) + unsubscribe() + + expect(closed).toHaveLength(2) + // The first turn drained its stream cleanly and handed off to the + // queued follow-up; it must not look like a user interruption to + // clients (they flash a "Turn interrupted" warning on that reason). + expect(closed[0]).toBe("superseded") + expect(closed[1]).toBe("completed") + }), + }) + } finally { + server.stop(true) + } + }, 20_000) + test("bridges legacy instance context for prompts after a completed turn", async () => { const calls: number[] = [] const server = Bun.serve({ diff --git a/packages/opencode/test/kilocode/tool/websearch-kilo-exa.test.ts b/packages/opencode/test/kilocode/tool/websearch-kilo-exa.test.ts new file mode 100644 index 0000000000..5756e7c671 --- /dev/null +++ b/packages/opencode/test/kilocode/tool/websearch-kilo-exa.test.ts @@ -0,0 +1,180 @@ +// kilocode_change - new file +import { describe, expect, test } from "bun:test" +import { Effect, Exit, Layer } from "effect" +import { HttpBody, HttpClient, HttpClientResponse } from "effect/unstable/http" +import { + KILO_EXA_URL, + MAX_KILO_EXA_RESULTS, + type KiloExaParams, + callKiloExa, +} from "../../../src/kilocode/tool/websearch-kilo-exa" + +type Recorded = { + url?: string + method?: string + authorization?: string + body?: string +} + +const readBody = async (body: HttpBody.HttpBody): Promise => { + if (body._tag === "Uint8Array") return new TextDecoder().decode(body.body) + if (body._tag === "Raw") return JSON.stringify(body.body) + return "" +} + +const jsonResponse = (status: number, body: unknown): Response => + new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" }, + }) + +const okJson = (body: unknown) => jsonResponse(200, body) + +const fakeHttp = (respond: (status: number) => Response, recorded?: Recorded): HttpClient.HttpClient => + HttpClient.make((request) => + Effect.gen(function* () { + const url = request.url + const method = request.method + const authorization = request.headers["authorization"] + const body = yield* Effect.promise(() => readBody(request.body)) + if (recorded) { + recorded.url = url + recorded.method = method + recorded.authorization = authorization + recorded.body = body + } + return HttpClientResponse.fromWeb( + request as unknown as Parameters[0], + respond(200), + ) + }), + ) + +const runCall = async ( + params: KiloExaParams, + respond: (status: number) => Response, + recorded?: Recorded, + kiloToken = "kilo-test-token", +) => + Effect.runPromiseExit( + Effect.gen(function* () { + const http = fakeHttp(respond, recorded) + return yield* callKiloExa(http, params, kiloToken) + }), + ) + +describe("callKiloExa request shape", () => { + test("posts to KILO_EXA_URL with bearer token and highlights-only contents", async () => { + const recorded: Recorded = {} + const exit = await runCall({ query: "drone" }, () => okJson({ results: [] }), recorded) + expect(Exit.isSuccess(exit)).toBe(true) + expect(recorded.url).toContain("/api/exa/search") + expect(recorded.method).toBe("POST") + expect(recorded.authorization).toBe("Bearer kilo-test-token") + const parsed = JSON.parse(recorded.body!) + expect(parsed.query).toBe("drone") + expect(parsed.type).toBe("auto") + expect(parsed.numResults).toBe(MAX_KILO_EXA_RESULTS) + expect(parsed.contents).toEqual({ highlights: true }) + }) + + test("uses caller numResults when below cap", async () => { + const recorded: Recorded = {} + await runCall({ query: "x", numResults: 3 }, () => okJson({ results: [] }), recorded) + expect(JSON.parse(recorded.body!).numResults).toBe(3) + }) + + test("clamps numResults at MAX_KILO_EXA_RESULTS", async () => { + const recorded: Recorded = {} + await runCall({ query: "x", numResults: 25 }, () => okJson({ results: [] }), recorded) + expect(JSON.parse(recorded.body!).numResults).toBe(MAX_KILO_EXA_RESULTS) + }) + + test("passes through caller type", async () => { + const recorded: Recorded = {} + await runCall({ query: "x", type: "deep" }, () => okJson({ results: [] }), recorded) + expect(JSON.parse(recorded.body!).type).toBe("deep") + }) + + test("KILO_EXA_URL is built from KILO_API_BASE", () => { + expect(KILO_EXA_URL).toMatch(/\/api\/exa\/search$/) + }) +}) + +describe("callKiloExa response formatting", () => { + const okValue = (exit: Exit.Exit): string => { + if (Exit.isFailure(exit)) throw new Error("expected success") + return (exit as Extract).value as string + } + + test("formats results with title, url, date and highlights", async () => { + const exit = await runCall({ query: "x" }, () => + okJson({ + results: [ + { + title: "A drone", + url: "https://example.com/a", + publishedDate: "2025-01-02T00:00:00.000Z", + highlights: ["first", "second"], + }, + ], + }), + ) + const text = okValue(exit) + expect(text).toContain("[1] A drone") + expect(text).toContain("https://example.com/a") + expect(text).toContain("(2025-01-02T00:00:00.000Z)") + expect(text).toContain("> first") + expect(text).toContain("> second") + }) + + test("falls back to url when title is missing", async () => { + const exit = await runCall({ query: "x" }, () => okJson({ results: [{ url: "https://example.com/no-title" }] })) + expect(okValue(exit)).toContain("[1] https://example.com/no-title") + }) + + test("returns NO_RESULTS message on empty results", async () => { + const exit = await runCall({ query: "x" }, () => okJson({ results: [] })) + expect(okValue(exit)).toBe("No search results found. Please try a different query.") + }) + + test("ignores costDollars on the response (cost accounting out of scope)", async () => { + const exit = await runCall({ query: "x" }, () => + okJson({ + results: [{ url: "https://example.com" }], + costDollars: { total: 0.007, search: { neural: 0.007 } }, + requestId: "req-123", + }), + ) + expect(Exit.isSuccess(exit)).toBe(true) + }) +}) + +describe("callKiloExa error handling", () => { + test("dies with auth-required message on 401", async () => { + const exit = await runCall({ query: "x" }, () => jsonResponse(401, {})) + expect(Exit.isFailure(exit)).toBe(true) + if (Exit.isSuccess(exit)) return + expect(String((exit as Extract).cause)).toContain("unauthorized") + expect(String((exit as Extract).cause)).toContain("kilo auth login") + }) + + test("dies with auth-required message on 403", async () => { + const exit = await runCall({ query: "x" }, () => jsonResponse(403, {})) + expect(Exit.isFailure(exit)).toBe(true) + if (Exit.isSuccess(exit)) return + expect(String((exit as Extract).cause)).toContain("unauthorized") + }) + + test("dies with status code on other non-2xx", async () => { + const exit = await runCall({ query: "x" }, () => jsonResponse(500, { error: "boom" })) + expect(Exit.isFailure(exit)).toBe(true) + if (Exit.isSuccess(exit)) return + expect(String((exit as Extract).cause)).toContain("500") + }) + + test("dies when response body is not valid ExaResponse shape", async () => { + const exit = await runCall({ query: "x" }, () => okJson({ nope: true })) + expect(Exit.isFailure(exit)).toBe(true) + }) +}) diff --git a/packages/opencode/test/plugin/codex.test.ts b/packages/opencode/test/plugin/codex.test.ts index a40b995358..a264bd376a 100644 --- a/packages/opencode/test/plugin/codex.test.ts +++ b/packages/opencode/test/plugin/codex.test.ts @@ -148,6 +148,14 @@ describe("plugin.codex", () => { id: "gpt-5.5", api: { id: "gpt-5.5" }, } as never, + "gpt-5.6": { + id: "gpt-5.6", + api: { id: "gpt-5.6" }, + } as never, + "gpt-5.6-sol": { + id: "gpt-5.6-sol", + api: { id: "gpt-5.6-sol" }, + } as never, "gpt-5.4-mini": { id: "gpt-5.4-mini", api: { id: "gpt-5.4-mini" }, @@ -164,6 +172,8 @@ describe("plugin.codex", () => { } as never, { auth: { type: "oauth" } } as never) expect(provider).not.toHaveProperty(["gpt-5.5-pro"]) expect(provider).toHaveProperty(["gpt-5.5"]) + expect(provider).not.toHaveProperty(["gpt-5.6"]) + expect(provider).toHaveProperty(["gpt-5.6-sol"]) expect(provider).toHaveProperty(["gpt-5.4-mini"]) expect(provider).toHaveProperty(["gpt-5.1-codex"]) expect(provider).not.toHaveProperty(["other-model"]) diff --git a/packages/opencode/test/tool/__snapshots__/parameters.test.ts.snap b/packages/opencode/test/tool/__snapshots__/parameters.test.ts.snap index f4951dfa51..368225175b 100644 --- a/packages/opencode/test/tool/__snapshots__/parameters.test.ts.snap +++ b/packages/opencode/test/tool/__snapshots__/parameters.test.ts.snap @@ -462,7 +462,7 @@ exports[`tool parameters JSON Schema (wire shape) websearch 1`] = ` "type": "string", }, "numResults": { - "description": "Number of search results to return (default: 8)", + "description": "Number of search results to return (default: 8, maximum: 10)", "type": "number", }, "query": { diff --git a/packages/plugin-atomic-chat/package.json b/packages/plugin-atomic-chat/package.json index e3ee0bab74..2409e9af79 100644 --- a/packages/plugin-atomic-chat/package.json +++ b/packages/plugin-atomic-chat/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/plugin-atomic-chat", - "version": "7.4.16", + "version": "7.4.17", "description": "Kilo Code plugin for Atomic Chat: auto-detection and dynamic model discovery (OpenAI-compatible local API)", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index f68b8ce481..455be0c020 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/plugin", - "version": "7.4.16", + "version": "7.4.17", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/script/package.json b/packages/script/package.json index 879b34567f..a4b2c4545b 100644 --- a/packages/script/package.json +++ b/packages/script/package.json @@ -12,6 +12,6 @@ "exports": { ".": "./src/index.ts" }, - "version": "7.4.16", + "version": "7.4.17", "peerDependencies": {} } diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 8988da2029..4446c5f1de 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/sdk", - "version": "7.4.16", + "version": "7.4.17", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 61953cc620..f5c44c1930 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -3557,7 +3557,7 @@ export type EventSessionTurnClose = { properties: { sessionID: string parentID?: string - reason: "completed" | "error" | "interrupted" + reason: "completed" | "error" | "interrupted" | "superseded" } } diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 53526b5f00..4b5d75a7d5 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -24496,6 +24496,15 @@ { "$ref": "#/components/schemas/EventServerInstanceDisposed" }, + { + "$ref": "#/components/schemas/EventSessionTurnOpen" + }, + { + "$ref": "#/components/schemas/EventSessionTurnClose" + }, + { + "$ref": "#/components/schemas/EventSessionQueueChanged" + }, { "$ref": "#/components/schemas/EventSessionNetworkAsked" }, @@ -24523,12 +24532,6 @@ { "$ref": "#/components/schemas/EventInteractive_terminalDeleted" }, - { - "$ref": "#/components/schemas/EventSessionTurnOpen" - }, - { - "$ref": "#/components/schemas/EventSessionTurnClose" - }, { "$ref": "#/components/schemas/EventSandboxStatusChanged" }, @@ -24557,10 +24560,10 @@ "$ref": "#/components/schemas/EventKilocodeNotebookCancelled" }, { - "$ref": "#/components/schemas/EventLspClientDiagnostics" + "$ref": "#/components/schemas/EventKilo-sessionsRemote-status-changed" }, { - "$ref": "#/components/schemas/EventKilo-sessionsRemote-status-changed" + "$ref": "#/components/schemas/EventLspClientDiagnostics" }, { "$ref": "#/components/schemas/EventMemoryStatus1" @@ -24818,10 +24821,10 @@ "$ref": "#/components/schemas/EventProjectUpdated" }, { - "$ref": "#/components/schemas/EventLspUpdated" + "$ref": "#/components/schemas/EventVcsBranchUpdated" }, { - "$ref": "#/components/schemas/EventVcsBranchUpdated" + "$ref": "#/components/schemas/EventLspUpdated" }, { "$ref": "#/components/schemas/EventWorkspaceReady" @@ -26838,7 +26841,7 @@ "type": "object", "properties": { "start": { - "type": "number", + "type": "integer", "minimum": 0 } }, @@ -26914,11 +26917,11 @@ "type": "object", "properties": { "start": { - "type": "number", + "type": "integer", "minimum": 0 }, "end": { - "type": "number", + "type": "integer", "minimum": 0 }, "elapsed": { @@ -27686,6 +27689,15 @@ { "$ref": "#/components/schemas/EventServerInstanceDisposed" }, + { + "$ref": "#/components/schemas/EventSessionTurnOpen" + }, + { + "$ref": "#/components/schemas/EventSessionTurnClose" + }, + { + "$ref": "#/components/schemas/EventSessionQueueChanged" + }, { "$ref": "#/components/schemas/EventSessionNetworkAsked" }, @@ -27713,12 +27725,6 @@ { "$ref": "#/components/schemas/EventInteractive_terminalDeleted" }, - { - "$ref": "#/components/schemas/EventSessionTurnOpen" - }, - { - "$ref": "#/components/schemas/EventSessionTurnClose" - }, { "$ref": "#/components/schemas/EventSandboxStatusChanged" }, @@ -27747,10 +27753,10 @@ "$ref": "#/components/schemas/EventKilocodeNotebookCancelled" }, { - "$ref": "#/components/schemas/EventLspClientDiagnostics" + "$ref": "#/components/schemas/EventKilo-sessionsRemote-status-changed" }, { - "$ref": "#/components/schemas/EventKilo-sessionsRemote-status-changed" + "$ref": "#/components/schemas/EventLspClientDiagnostics" }, { "$ref": "#/components/schemas/EventMemoryStatus" @@ -28008,10 +28014,10 @@ "$ref": "#/components/schemas/EventProjectUpdated" }, { - "$ref": "#/components/schemas/EventLspUpdated" + "$ref": "#/components/schemas/EventVcsBranchUpdated" }, { - "$ref": "#/components/schemas/EventVcsBranchUpdated" + "$ref": "#/components/schemas/EventLspUpdated" }, { "$ref": "#/components/schemas/EventWorkspaceReady" @@ -35218,6 +35224,96 @@ "required": ["id", "type", "properties"], "additionalProperties": false }, + "EventSessionTurnOpen": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["session.turn.open"] + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "pattern": "^ses" + } + }, + "required": ["sessionID"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "EventSessionTurnClose": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["session.turn.close"] + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "pattern": "^ses" + }, + "parentID": { + "type": "string", + "pattern": "^ses" + }, + "reason": { + "type": "string", + "enum": ["completed", "error", "interrupted", "superseded"] + } + }, + "required": ["sessionID", "reason"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "EventSessionQueueChanged": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["session.queue.changed"] + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "type": "string", + "pattern": "^ses" + }, + "queued": { + "type": "array", + "items": { + "type": "string", + "pattern": "^msg" + } + } + }, + "required": ["sessionID", "queued"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, "EventSessionNetworkAsked": { "type": "object", "properties": { @@ -35470,64 +35566,6 @@ "required": ["id", "type", "properties"], "additionalProperties": false }, - "EventSessionTurnOpen": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["session.turn.open"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - } - }, - "required": ["sessionID"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventSessionTurnClose": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["session.turn.close"] - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string", - "pattern": "^ses" - }, - "parentID": { - "type": "string", - "pattern": "^ses" - }, - "reason": { - "type": "string", - "enum": ["completed", "error", "interrupted"] - } - }, - "required": ["sessionID", "reason"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, "EventSandboxStatusChanged": { "type": "object", "properties": { @@ -35837,33 +35875,6 @@ "required": ["id", "type", "properties"], "additionalProperties": false }, - "EventLspClientDiagnostics": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["lsp.client.diagnostics"] - }, - "properties": { - "type": "object", - "properties": { - "serverID": { - "type": "string" - }, - "path": { - "type": "string" - } - }, - "required": ["serverID", "path"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, "EventKilo-sessionsRemote-status-changed": { "type": "object", "properties": { @@ -35891,6 +35902,33 @@ "required": ["id", "type", "properties"], "additionalProperties": false }, + "EventLspClientDiagnostics": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["lsp.client.diagnostics"] + }, + "properties": { + "type": "object", + "properties": { + "serverID": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "required": ["serverID", "path"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, "EventMemoryStatus": { "type": "object", "properties": { @@ -39854,24 +39892,6 @@ "required": ["id", "type", "properties"], "additionalProperties": false }, - "EventLspUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["lsp.updated"] - }, - "properties": { - "type": "object", - "properties": {} - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, "EventVcsBranchUpdated": { "type": "object", "properties": { @@ -39895,6 +39915,24 @@ "required": ["id", "type", "properties"], "additionalProperties": false }, + "EventLspUpdated": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["lsp.updated"] + }, + "properties": { + "type": "object", + "properties": {} + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, "EventWorkspaceReady": { "type": "object", "properties": { diff --git a/packages/server/package.json b/packages/server/package.json index 7458002a84..1dc884c72e 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/server", - "version": "7.4.16", + "version": "7.4.17", "private": true, "type": "module", "license": "MIT", diff --git a/packages/storybook/package.json b/packages/storybook/package.json index 22fe404a5f..163306ec46 100644 --- a/packages/storybook/package.json +++ b/packages/storybook/package.json @@ -26,7 +26,7 @@ "typescript": "catalog:", "vite": "catalog:" }, - "version": "7.4.16", + "version": "7.4.17", "dependencies": {}, "peerDependencies": {} } diff --git a/packages/tui/package.json b/packages/tui/package.json index 344179e4d0..87fe7949af 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/tui", - "version": "7.4.16", + "version": "7.4.17", "private": true, "type": "module", "license": "MIT", diff --git a/packages/tui/src/feature-plugins/sidebar/context.tsx b/packages/tui/src/feature-plugins/sidebar/context.tsx index e5bd16e8af..fd1c3bb39b 100644 --- a/packages/tui/src/feature-plugins/sidebar/context.tsx +++ b/packages/tui/src/feature-plugins/sidebar/context.tsx @@ -1,7 +1,7 @@ import type { AssistantMessage } from "@kilocode/sdk/v2" import type { TuiPlugin, TuiPluginApi } from "@kilocode/plugin/tui" import type { BuiltinTuiPlugin } from "../builtins" -import { createMemo } from "solid-js" +import { createMemo, createSignal, Show } from "solid-js" // kilocode_change const id = "internal:sidebar-context" @@ -11,6 +11,9 @@ const money = new Intl.NumberFormat("en-US", { }) function View(props: { api: TuiPluginApi; session_id: string }) { + // kilocode_change start + const [open, setOpen] = createSignal(true) + // kilocode_change end const theme = () => props.api.theme.current const msg = createMemo(() => props.api.state.session.messages(props.session_id)) const session = createMemo(() => props.api.state.session.get(props.session_id)) @@ -44,12 +47,25 @@ function View(props: { api: TuiPluginApi; session_id: string }) { return ( - - Context - - {state().tokens.toLocaleString()} tokens - {state().percent ?? 0}% used - {money.format(cost())} spent + {/* kilocode_change start */} + setOpen((x) => !x)}> + {open() ? "▼" : "▶"} + + Context + + + {" "} + ({state().percent ?? 0}% · {money.format(cost())}) + + + + + + {state().tokens.toLocaleString()} tokens + {state().percent ?? 0}% used + {money.format(cost())} spent + + {/* kilocode_change end */} ) } diff --git a/packages/ui/package.json b/packages/ui/package.json index d5b27305ac..bb4a10d842 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "7.4.16", + "version": "7.4.17", "type": "module", "license": "MIT", "exports": { diff --git a/script/upstream/package.json b/script/upstream/package.json index 254cd0b36e..753ce3fa52 100644 --- a/script/upstream/package.json +++ b/script/upstream/package.json @@ -1,6 +1,6 @@ { "name": "@kilocode/upstream-merge", - "version": "7.4.16", + "version": "7.4.17", "private": true, "type": "module", "description": "Scripts for automating upstream opencode merges into Kilo",