Merge branch 'main' into unleashed-romano

This commit is contained in:
Kirill Kalishev
2026-05-15 10:49:23 -04:00
committed by GitHub
141 changed files with 2198 additions and 557 deletions
@@ -1,5 +0,0 @@
---
"kilo-code": minor
---
Add a collapsible sidebar in the Agent Manager. The toggle button sits left of the tab title and the collapsed state persists across reloads and restarts. Starting a new session or worktree automatically reopens the sidebar so the new entry is visible.
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Keep long New Worktree prompts clear of selector controls and show inline model picker options without clipping.
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Keep Agent Manager state hidden from Git before creating local sessions.
-5
View File
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Keep VS Code session timeline, token, context, and cost stats in sync after restoring a checkpoint.
+5
View File
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---
Capture aggregate usage telemetry for experimental Morph-backed codebase search.
-6
View File
@@ -1,6 +0,0 @@
---
"kilo-code": patch
"@kilocode/cli": patch
---
Show ChatGPT sign-in again when Codex authentication expires.
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Keep continued Agent Manager sessions in the selected worktree instead of moving them to Local.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-ui": patch
---
Show the assistant message copy tooltip above the button so it no longer overlaps the thumbs up/down buttons.
-6
View File
@@ -1,6 +0,0 @@
---
"@kilocode/cli": patch
"kilo-code": patch
---
Fix TUI diff rendering when header-like content lines appear inside a unified diff hunk.
-5
View File
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Ensure snapshot installs package a fresh CLI backend so new extension features are available in local VSIX builds.
-5
View File
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Hide Task tool subagent sessions from Agent Manager session lists.
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": minor
---
Support speech-to-text voice input in Agent Manager inline review comments.
-5
View File
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Move worktree file revert diff status checks into the VS Code extension host so Kilo no longer asks the CLI server to run git diff commands through Bun during workspace and Agent Manager reverts.
-7
View File
@@ -1,7 +0,0 @@
---
"@kilocode/cli": minor
"@kilocode/sdk": minor
"kilo-code": minor
---
Support setting an auto-compaction threshold percentage so long sessions can compact before the context window is full.
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Keep inline review voice capture stable in rendered Markdown diffs.
-5
View File
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Keep the New Worktree model picker open when interacting with its search field.
-6
View File
@@ -1,6 +0,0 @@
---
"kilo-code": patch
"@kilocode/kilo-ui": patch
---
Keep chat tool cards open or closed when streaming updates remount them.
-5
View File
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Use the VS Code-native placeholder color for input fields so placeholder text is consistently distinct from values you have typed (Settings, Agent Manager, prompt input, model search, etc.).
-5
View File
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Reset the To-dos tab when reverting a session snapshot.
-5
View File
@@ -1,5 +0,0 @@
---
"kilo-code": minor
---
Add syntax highlighting to shell command output with Shiki, labeled "Command" and "Output" sections, per-section copy buttons, and an "Open in Editor" action that opens the full untruncated output in a VS Code editor tab.
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": minor
---
Export full VS Code session transcripts as Markdown files.
-6
View File
@@ -1,6 +0,0 @@
---
"kilo-code": patch
"@kilocode/cli": patch
---
Fix Mermaid diagrams rendering with empty text inside every shape by restoring the `foreignObject` HTML integration point that DOMPurify dropped in 3.1.7.
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Restore only the Kilo Code sidebar, Agent Manager, or both on VS Code restart based on which views were open before shutdown.
-6
View File
@@ -1,6 +0,0 @@
---
"kilo-code": minor
"@kilocode/kilo-gateway": minor
---
Support experimental speech-to-text voice input in VS Code prompt fields through Kilo Gateway.
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Send recorded voice prompts directly when the prompt send button is pressed.
-6
View File
@@ -1,6 +0,0 @@
---
"@kilocode/cli": patch
"@kilocode/kilo-gateway": patch
---
Prevent Kilo Gateway Responses requests from replaying transient provider item IDs when request storage is disabled.
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Support viewing Agent Manager terminal tabs alongside the right diff sidebar, and send review comments to the active terminal.
-5
View File
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Keep configured mode models in sync when switching modes in VS Code.
+11 -1
View File
@@ -41,11 +41,21 @@ jobs:
with:
token: ${{ secrets.GITHUB_TOKEN }}
# kilocode_change start
- name: Setup Node
uses: actions/setup-node@v6 # kilocode_change
id: setup-node
continue-on-error: ${{ runner.os == 'Windows' }}
uses: actions/setup-node@v6
with:
node-version: "24"
- name: Retry Setup Node on Windows
if: runner.os == 'Windows' && steps.setup-node.outcome == 'failure'
uses: actions/setup-node@v6
with:
node-version: "24"
# kilocode_change end
- name: Setup Bun
uses: ./.github/actions/setup-bun
+3 -1
View File
@@ -102,7 +102,9 @@ kilo run --auto "run tests and fix any failures"
We welcome contributions from developers, writers, and enthusiasts!
To get started, please read our [Contributing Guide](/CONTRIBUTING.md). It includes details on setting up your environment, coding standards, types of contribution and how to submit pull requests.
See [RELEASING.md](RELEASING.md) for the release process.
See [RELEASING.md](RELEASING.md) for the VS Code extension and CLI release process.
See [packages/kilo-jetbrains/RELEASING.md](packages/kilo-jetbrains/RELEASING.md) for the JetBrains plugin release process.
## Code of Conduct
+14 -14
View File
@@ -33,7 +33,7 @@
},
"packages/core": {
"name": "@opencode-ai/core",
"version": "7.2.52",
"version": "7.3.0",
"bin": {
"opencode": "./bin/opencode",
},
@@ -68,7 +68,7 @@
},
"packages/kilo-docs": {
"name": "@kilocode/kilo-docs",
"version": "7.2.52",
"version": "7.3.0",
"dependencies": {
"@docsearch/css": "^4",
"@docsearch/js": "^4",
@@ -98,7 +98,7 @@
},
"packages/kilo-gateway": {
"name": "@kilocode/kilo-gateway",
"version": "7.2.52",
"version": "7.3.0",
"dependencies": {
"@ai-sdk/alibaba": "1.0.17",
"@ai-sdk/anthropic": "3.0.71",
@@ -134,7 +134,7 @@
},
"packages/kilo-i18n": {
"name": "@kilocode/kilo-i18n",
"version": "7.2.52",
"version": "7.3.0",
"devDependencies": {
"@tsconfig/node22": "catalog:",
"@types/bun": "catalog:",
@@ -144,7 +144,7 @@
},
"packages/kilo-indexing": {
"name": "@kilocode/kilo-indexing",
"version": "7.2.52",
"version": "7.3.0",
"dependencies": {
"@aws-sdk/client-bedrock-runtime": "3.1005.0",
"@aws-sdk/credential-provider-ini": "3.972.31",
@@ -178,7 +178,7 @@
},
"packages/kilo-telemetry": {
"name": "@kilocode/kilo-telemetry",
"version": "7.2.52",
"version": "7.3.0",
"dependencies": {
"@kilocode/kilo-gateway": "workspace:*",
"posthog-node": "4.4.0",
@@ -192,7 +192,7 @@
},
"packages/kilo-ui": {
"name": "@kilocode/kilo-ui",
"version": "7.2.52",
"version": "7.3.0",
"dependencies": {
"@kilocode/sdk": "workspace:*",
"@kobalte/core": "0.13.11",
@@ -229,7 +229,7 @@
},
"packages/kilo-vscode": {
"name": "kilo-code",
"version": "7.2.52",
"version": "7.3.0",
"dependencies": {
"@anthropic-ai/sdk": "^0.39.0",
"@kilocode/kilo-gateway": "workspace:*",
@@ -292,7 +292,7 @@
},
"packages/opencode": {
"name": "@kilocode/cli",
"version": "7.2.52",
"version": "7.3.0",
"bin": {
"kilo": "./bin/kilo",
"kilocode": "./bin/kilo",
@@ -450,7 +450,7 @@
},
"packages/plugin": {
"name": "@kilocode/plugin",
"version": "7.2.52",
"version": "7.3.0",
"dependencies": {
"@kilocode/sdk": "workspace:*",
"effect": "catalog:",
@@ -475,7 +475,7 @@
},
"packages/script": {
"name": "@opencode-ai/script",
"version": "7.2.52",
"version": "7.3.0",
"dependencies": {
"semver": "^7.6.3",
},
@@ -486,7 +486,7 @@
},
"packages/sdk/js": {
"name": "@kilocode/sdk",
"version": "7.2.52",
"version": "7.3.0",
"dependencies": {
"cross-spawn": "catalog:",
},
@@ -501,7 +501,7 @@
},
"packages/storybook": {
"name": "@opencode-ai/storybook",
"version": "7.2.52",
"version": "7.3.0",
"devDependencies": {
"@opencode-ai/ui": "workspace:*",
"@solidjs/meta": "catalog:",
@@ -524,7 +524,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "7.2.52",
"version": "7.3.0",
"dependencies": {
"@kilocode/sdk": "workspace:*",
"@kobalte/core": "catalog:",
+1 -1
View File
@@ -135,6 +135,6 @@
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch"
},
"version": "7.2.52",
"version": "7.3.0",
"peerDependencies": {}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "7.2.52",
"version": "7.3.0",
"name": "@opencode-ai/core",
"type": "module",
"license": "MIT",
+6 -6
View File
@@ -1,7 +1,7 @@
id = "kilo"
name = "Kilo"
description = "The open source coding agent."
version = "7.2.52"
version = "7.3.0"
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.2.52/opencode-darwin-arm64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.0/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.2.52/opencode-darwin-x64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.0/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.52/opencode-linux-arm64.tar.gz"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.0/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.2.52/opencode-linux-x64.tar.gz"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.0/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.2.52/opencode-windows-x64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.0/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kilocode/kilo-docs",
"version": "7.2.52",
"version": "7.3.0",
"private": true,
"scripts": {
"dev": "next dev --webpack --port 3002",
@@ -76,6 +76,12 @@ Find the Kilo Code icon ({% kiloCodeIcon /%}) in VS Code's Primary Side Bar. Cli
The extension automatically passes context from your editor, including your open tabs and active file. You can type `@` in the chat input to get file and terminal autocomplete suggestions — use `@filename` to attach a file or `@terminal` to include your active terminal output. You can also mention file paths naturally in your message (e.g., "update src/utils.ts to add a helper function"). The agent can also discover files on its own using its built-in tools.
**Exporting local transcripts:**
Run `/export` in chat, or open a local session's **History** context menu and choose **Export session transcript**. The save dialog lets you choose the Markdown (`.md`) destination.
Kilo builds the export from the complete local session history, not only the messages currently loaded in the chat view.
{% /tab %}
{% tab label="CLI" %}
@@ -33,6 +33,7 @@ Key features include:
- **[Skills](/docs/customize/skills)** — Load specialized domain knowledge from SKILL.md files
- **[Custom Subagents](/docs/customize/custom-subagents)** — Define specialized sub-agents for the `task` tool
- **Open in Tab** — Pop the chat out into a full editor tab
- **Transcript export:** Save complete local session transcripts as Markdown files
- **Sub-Agent Viewer** — Read-only panels for viewing child agent sessions
- **Legacy Migration** — Automatic migration wizard for VSCode extension settings
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-gateway",
"version": "7.2.52",
"version": "7.3.0",
"type": "module",
"license": "MIT",
"description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration",
@@ -65,6 +65,7 @@ export const MODELS_FETCH_TIMEOUT_MS = 10 * 1000
*/
export const HEADER_ORGANIZATIONID = "X-KILOCODE-ORGANIZATIONID"
export const HEADER_TASKID = "X-KILOCODE-TASKID"
export const HEADER_PARENT_TASKID = "X-KILOCODE-PARENT-TASKID"
export const HEADER_PROJECTID = "X-KILOCODE-PROJECTID"
export const HEADER_TESTER = "X-KILOCODE-TESTER"
export const HEADER_EDITORNAME = "X-KILOCODE-EDITORNAME"
+2
View File
@@ -1,6 +1,7 @@
import {
HEADER_ORGANIZATIONID,
HEADER_TASKID,
HEADER_PARENT_TASKID,
HEADER_PROJECTID,
HEADER_TESTER,
HEADER_EDITORNAME,
@@ -21,6 +22,7 @@ import {
*/
export const X_KILOCODE_ORGANIZATIONID = HEADER_ORGANIZATIONID
export const X_KILOCODE_TASKID = HEADER_TASKID
export const X_KILOCODE_PARENT_TASKID = HEADER_PARENT_TASKID
export const X_KILOCODE_PROJECTID = HEADER_PROJECTID
export const X_KILOCODE_TESTER = HEADER_TESTER
export const X_KILOCODE_EDITORNAME = HEADER_EDITORNAME
+1
View File
@@ -100,6 +100,7 @@ export {
MODELS_FETCH_TIMEOUT_MS,
HEADER_ORGANIZATIONID,
HEADER_TASKID,
HEADER_PARENT_TASKID,
HEADER_PROJECTID,
HEADER_TESTER,
HEADER_EDITORNAME,
+2 -6
View File
@@ -5,6 +5,7 @@ import { getApiKey } from "./auth/token.js"
import { buildKiloHeaders, getDefaultHeaders } from "./headers.js"
import { ANONYMOUS_API_KEY } from "./api/constants.js"
import { resolveKiloOpenRouterBaseUrl } from "./api/url.js"
import { buildRequestHeaders } from "./provider.js"
/**
* Debug version of createKilo with extensive logging
@@ -40,12 +41,7 @@ export function createKiloDebug(options: KiloProviderOptions = {}): SDK {
console.log(" - URL:", String(input))
console.log(" - Method:", init?.method || "GET")
const headers = new Headers(init?.headers)
// Add custom headers
Object.entries(customHeaders).forEach(([key, value]) => {
headers.set(key, value)
})
const headers = buildRequestHeaders(customHeaders, init?.headers)
// Add authorization if API key exists
if (apiKey) {
+9 -6
View File
@@ -10,6 +10,14 @@ import { ANONYMOUS_API_KEY } from "./api/constants.js"
import { resolveKiloOpenRouterBaseUrl } from "./api/url.js"
import { sanitizeResponsesBody } from "./responses.js"
export function buildRequestHeaders(defaultHeaders: Record<string, string>, requestHeaders?: HeadersInit): Headers {
const headers = new Headers(defaultHeaders)
new Headers(requestHeaders).forEach((value, key) => {
headers.set(key, value)
})
return headers
}
/**
* Create a KiloCode provider instance
*
@@ -45,14 +53,9 @@ export function createKilo(options: KiloProviderOptions = {}): KiloProvider {
// Create custom fetch wrapper to add dynamic headers
const originalFetch = options.fetch ?? fetch
const wrappedFetch = async (input: string | URL | Request, init?: RequestInit) => {
const headers = new Headers(init?.headers)
const headers = buildRequestHeaders(customHeaders, init?.headers)
const body = sanitizeResponsesBody(input, init?.body)
// Add custom headers
Object.entries(customHeaders).forEach(([key, value]) => {
headers.set(key, value)
})
// Add authorization if API key exists
if (apiKey) {
headers.set("Authorization", `Bearer ${apiKey}`)
@@ -429,6 +429,7 @@ export function createKiloRoutes(deps: KiloRoutesDeps) {
format: z.string(),
}),
language: z.string().optional(),
prompt: z.string().optional(),
temperature: z.number().optional(),
}),
),
@@ -0,0 +1,23 @@
import { describe, expect, test } from "bun:test"
import { buildRequestHeaders } from "../src/provider"
describe("Kilo provider request headers", () => {
test("request headers override provider defaults", () => {
const headers = buildRequestHeaders(
{
"content-type": "application/json",
"x-kilocode-feature": "vscode-extension",
"x-default-only": "kept",
},
{
"x-kilocode-feature": "agent-manager",
"x-request-only": "kept-too",
},
)
expect(headers.get("content-type")).toBe("application/json")
expect(headers.get("x-kilocode-feature")).toBe("agent-manager")
expect(headers.get("x-default-only")).toBe("kept")
expect(headers.get("x-request-only")).toBe("kept-too")
})
})
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-i18n",
"version": "7.2.52",
"version": "7.3.0",
"type": "module",
"license": "MIT",
"description": "Kilo-specific i18n translations and overrides",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-indexing",
"version": "7.2.52",
"version": "7.3.0",
"type": "module",
"license": "MIT",
"description": "Standalone indexing engine and host helpers for Kilo Code",
+2 -19
View File
@@ -52,26 +52,9 @@ The built plugin archive is at `build/distributions/kilo.jetbrains-<version>.zip
---
## Publish release candidates
## Releasing
JetBrains release-candidate builds publish from tags matching `jetbrains/vx.y.z-rc.n`, for example `jetbrains/v7.0.1-rc.1`. The git tag is the source of truth for the Marketplace plugin version; the `v` prefix is stripped before the version is set in the plugin metadata so the Marketplace version remains a plain semver (`x.y.z-rc.n`). Production Gradle builds fail if `HEAD` is not tagged with `jetbrains/v<version>`. Local dev builds use the placeholder `0.0.0-dev` version.
The `publish-jetbrains` workflow publishes RC builds to the JetBrains Marketplace `eap` channel and uploads the same plugin ZIP to a GitHub prerelease for the tag. Stable tags like `jetbrains/vx.y.z` are recognized by the workflow but intentionally rejected until default-channel publishing is enabled.
Testers can install or update EAP builds from this custom plugin repository:
```
https://plugins.jetbrains.com/plugins/eap/list
```
Required GitHub Actions secrets:
- `JETBRAINS_MARKETPLACE_TOKEN`
- `JETBRAINS_CERTIFICATE_CHAIN`
- `JETBRAINS_PRIVATE_KEY`
- `JETBRAINS_PRIVATE_KEY_PASSWORD`
Before the first publish, complete `RELEASE_TODO.md`.
See [RELEASING.md](RELEASING.md) for the full release process, including how to tag and push an RC, where to watch workflow progress, and how to install RC builds via the custom plugin repository.
---
+62
View File
@@ -0,0 +1,62 @@
# Releasing the JetBrains Plugin
## RC releases (currently the only supported flow)
Stable release tags (`jetbrains/vx.y.z`) are recognized by the workflow but intentionally rejected. Only RC tags are accepted right now.
### 1. Create and push a tag
Tag format: `jetbrains/v<major>.<minor>.<patch>-rc.<n>`
```
git tag jetbrains/v7.0.1-rc.1
git push origin jetbrains/v7.0.1-rc.1
```
### 2. Watch the workflow
The `publish-jetbrains` workflow starts automatically on tag push. Follow progress at:
[https://github.com/Kilo-Org/kilocode/actions/workflows/publish-jetbrains.yml](https://github.com/Kilo-Org/kilocode/actions/workflows/publish-jetbrains.yml)
The workflow:
1. Validates the tag format and required secrets.
2. Downloads CLI binaries for all 6 platforms from the matching GitHub Release.
3. Verifies and signs the plugin with `./gradlew verifyPlugin publishPlugin -Pproduction=true`.
4. Publishes the signed ZIP to the JetBrains Marketplace `eap` channel.
5. Uploads the signed ZIP to a GitHub prerelease for the tag.
### 3. Verify on the Marketplace
Once the workflow succeeds, the new version should appear in the plugin's version list:
[https://plugins.jetbrains.com/plugin/28350-kilo-code/edit/versions](https://plugins.jetbrains.com/plugin/28350-kilo-code/edit/versions)
---
## Installing RC builds via the custom plugin repository
RC builds are published to the `eap` channel, not the default channel. To get them in IntelliJ IDEA:
1. Open **Settings > Plugins**.
2. Click the gear icon and choose **Manage Plugin Repositories**.
3. Add the following URL:
```
https://plugins.jetbrains.com/plugins/list?channel=eap&pluginId=28350
```
4. Search for **Kilo Code** in the Marketplace tab — the latest RC version will appear and update automatically.
---
## Required GitHub Actions secrets
| Secret | Purpose |
|---|---|
| `JETBRAINS_MARKETPLACE_TOKEN` | Marketplace API token for publishing |
| `JETBRAINS_CERTIFICATE_CHAIN` | PEM certificate chain for plugin signing |
| `JETBRAINS_PRIVATE_KEY` | PEM private key for plugin signing |
| `JETBRAINS_PRIVATE_KEY_PASSWORD` | Password for the private key |
Before the first publish, complete `RELEASE_TODO.md` to set up these secrets and the Marketplace plugin entry.
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-telemetry",
"version": "7.2.52",
"version": "7.3.0",
"type": "module",
"license": "MIT",
"description": "Telemetry for Kilo CLI - PostHog analytics integration",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kilocode/kilo-ui",
"version": "7.2.52",
"version": "7.3.0",
"type": "module",
"license": "MIT",
"exports": {
+53
View File
@@ -1,5 +1,58 @@
# kilo-code
## 7.3.0
## 7.2.54
### Minor Changes
- [#10230](https://github.com/Kilo-Org/kilocode/pull/10230) [`26f5393`](https://github.com/Kilo-Org/kilocode/commit/26f5393be272a31390f6a317555099ed2addd1b0) - Add a collapsible sidebar in the Agent Manager. The toggle button sits left of the tab title and the collapsed state persists across reloads and restarts. Starting a new session or worktree automatically reopens the sidebar so the new entry is visible.
- [#10218](https://github.com/Kilo-Org/kilocode/pull/10218) [`4860e65`](https://github.com/Kilo-Org/kilocode/commit/4860e654ca1cc46c4e99acc3f40d4f1302e34944) - Support setting an auto-compaction threshold percentage so long sessions can compact before the context window is full.
- [#10228](https://github.com/Kilo-Org/kilocode/pull/10228) [`911e7ce`](https://github.com/Kilo-Org/kilocode/commit/911e7ce73383cfd5780ec80ff8e8b8187061a18a) - Add syntax highlighting to shell command output with Shiki, labeled "Command" and "Output" sections, per-section copy buttons, and an "Open in Editor" action that opens the full untruncated output in a VS Code editor tab.
- [#10258](https://github.com/Kilo-Org/kilocode/pull/10258) [`1af7973`](https://github.com/Kilo-Org/kilocode/commit/1af79731a8ed925f1f69aa536ba90a53b89e8dfb) - Support experimental speech-to-text voice input in VS Code prompt fields through Kilo Gateway.
### Patch Changes
- [#10196](https://github.com/Kilo-Org/kilocode/pull/10196) [`174d467`](https://github.com/Kilo-Org/kilocode/commit/174d467a4967c53fddfc97ae9663ebf17c0fafbc) - Keep Agent Manager state hidden from Git before creating local sessions.
- [#10156](https://github.com/Kilo-Org/kilocode/pull/10156) [`e805d2f`](https://github.com/Kilo-Org/kilocode/commit/e805d2f756b785776cb1f60b97a253e3ae4c837c) - Keep VS Code session timeline, token, context, and cost stats in sync after restoring a checkpoint.
- [#10136](https://github.com/Kilo-Org/kilocode/pull/10136) [`8af638e`](https://github.com/Kilo-Org/kilocode/commit/8af638e7e20c645b22d96da5e30665e8e9cbf6ad) - Show ChatGPT sign-in again when Codex authentication expires.
- [#8754](https://github.com/Kilo-Org/kilocode/pull/8754) [`e498c02`](https://github.com/Kilo-Org/kilocode/commit/e498c02f7acc5c228bbd45f9e4f294bf5def21ca) Thanks [@shssoichiro](https://github.com/shssoichiro)! - Fix TUI diff rendering when header-like content lines appear inside a unified diff hunk.
- [#10265](https://github.com/Kilo-Org/kilocode/pull/10265) [`900ba85`](https://github.com/Kilo-Org/kilocode/commit/900ba85b838e035952ea6ca9adf20cbc923e8d99) - Ensure snapshot installs package a fresh CLI backend so new extension features are available in local VSIX builds.
- [#10227](https://github.com/Kilo-Org/kilocode/pull/10227) [`e1e2454`](https://github.com/Kilo-Org/kilocode/commit/e1e2454b4285109f1d4ef34b93d5d47f612faaea) - Hide Task tool subagent sessions from Agent Manager session lists.
- [#10229](https://github.com/Kilo-Org/kilocode/pull/10229) [`41e3cda`](https://github.com/Kilo-Org/kilocode/commit/41e3cda971c37d155de443e58ae2aeafdaf928b1) - Move worktree file revert diff status checks into the VS Code extension host so Kilo no longer asks the CLI server to run git diff commands through Bun during workspace and Agent Manager reverts.
- [#10240](https://github.com/Kilo-Org/kilocode/pull/10240) [`09f0156`](https://github.com/Kilo-Org/kilocode/commit/09f0156bfc66643870835251eef873d6e62f2791) - Keep the New Worktree model picker open when interacting with its search field.
- [#10243](https://github.com/Kilo-Org/kilocode/pull/10243) [`47c22a2`](https://github.com/Kilo-Org/kilocode/commit/47c22a28e98a50955bd9470a5b3f78bce5d5b4ba) - Keep chat tool cards open or closed when streaming updates remount them.
- [#10159](https://github.com/Kilo-Org/kilocode/pull/10159) [`64e45ea`](https://github.com/Kilo-Org/kilocode/commit/64e45eaba30641dd7b36fb65f79ddee3c0e14cf1) - Use the VS Code-native placeholder color for input fields so placeholder text is consistently distinct from values you have typed (Settings, Agent Manager, prompt input, model search, etc.).
- [#10206](https://github.com/Kilo-Org/kilocode/pull/10206) [`177f7df`](https://github.com/Kilo-Org/kilocode/commit/177f7df04937e13b3ea5abc5a9c79a56c9814ab6) - Reset the To-dos tab when reverting a session snapshot.
- [#10158](https://github.com/Kilo-Org/kilocode/pull/10158) [`d8245a0`](https://github.com/Kilo-Org/kilocode/commit/d8245a0ceb0989b8596c5a5d17fd1095ba9521be) - Fix Mermaid diagrams rendering with empty text inside every shape by restoring the `foreignObject` HTML integration point that DOMPurify dropped in 3.1.7.
- [#10256](https://github.com/Kilo-Org/kilocode/pull/10256) [`da9292f`](https://github.com/Kilo-Org/kilocode/commit/da9292fbccad9f4c20f439e240790da3ab3a92d3) - Support viewing Agent Manager terminal tabs alongside the right diff sidebar, and send review comments to the active terminal.
- [#10275](https://github.com/Kilo-Org/kilocode/pull/10275) [`f5dc95b`](https://github.com/Kilo-Org/kilocode/commit/f5dc95b99394c17ad7140bb034bc15a0f9de60b6) - Keep GPT-4o voice transcriptions closer to the words that were spoken.
- [#10095](https://github.com/Kilo-Org/kilocode/pull/10095) [`710c9dc`](https://github.com/Kilo-Org/kilocode/commit/710c9dca1e9d5f522fa3fe1495dc41ec1eea4b66) - Keep configured mode models in sync when switching modes in VS Code.
- Updated dependencies [[`262c2d6`](https://github.com/Kilo-Org/kilocode/commit/262c2d670474f0da50aa0c900e49aad93863acaf), [`4860e65`](https://github.com/Kilo-Org/kilocode/commit/4860e654ca1cc46c4e99acc3f40d4f1302e34944), [`47c22a2`](https://github.com/Kilo-Org/kilocode/commit/47c22a28e98a50955bd9470a5b3f78bce5d5b4ba), [`1af7973`](https://github.com/Kilo-Org/kilocode/commit/1af79731a8ed925f1f69aa536ba90a53b89e8dfb), [`1ea86fb`](https://github.com/Kilo-Org/kilocode/commit/1ea86fb6e15cbe486cb0af6f26995d0b1b2745a2), [`f5dc95b`](https://github.com/Kilo-Org/kilocode/commit/f5dc95b99394c17ad7140bb034bc15a0f9de60b6)]:
- @kilocode/kilo-ui@7.2.53
- @kilocode/sdk@7.3.0
- @kilocode/kilo-gateway@7.3.0
- @opencode-ai/ui@7.2.53
- @kilocode/kilo-indexing@7.2.53
## 7.2.51
### Patch Changes
+1 -1
View File
@@ -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.2.52",
"version": "7.3.0",
"icon": "assets/icons/logo-outline-black.png",
"galleryBanner": {
"color": "#FFFFFF",
+64 -18
View File
@@ -17,6 +17,7 @@ import { FileIgnoreController } from "./services/autocomplete/shims/FileIgnoreCo
import { ChatTextAreaAutocomplete } from "./services/autocomplete/chat-autocomplete/ChatTextAreaAutocomplete"
import { buildWebviewHtml, getWebviewFontSize } from "./utils"
import { saveImage } from "./kilo-provider/save-image"
import { exportTranscript } from "./kilo-provider/export-transcript"
import {
TelemetryProxy,
type TelemetryPropertiesProvider,
@@ -137,6 +138,7 @@ import type { KiloProviderOptions } from "./kilo-provider/options"
import { fetchKiloEmbeddingModelCatalog } from "@kilocode/kilo-gateway"
type MessageLoadMode = "replace" | "prepend" | "focus" | "reconcile"
type ContextMessage = { contextDirectory?: unknown }
// Helper to map agent data to the subset of fields sent to the webview
const mapAgent = (a: Agent) => ({
name: a.name,
@@ -449,10 +451,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
webviewView.webview.html = this._getHtmlForWebview(webviewView.webview)
this.setupWebviewMessageHandler(webviewView.webview)
vscode.commands.executeCommand("setContext", "kilo-code.new.sidebarVisible", webviewView.visible)
this.setSidebarVisible(webviewView.visible)
this.visibilityDisposable?.dispose()
this.visibilityDisposable = webviewView.onDidChangeVisibility(() => {
vscode.commands.executeCommand("setContext", "kilo-code.new.sidebarVisible", webviewView.visible)
this.setSidebarVisible(webviewView.visible)
if (this.statsPoller) {
this.statsPoller.setEnabled(webviewView.visible)
this.statsPoller.setVisible(webviewView.visible)
@@ -462,6 +464,11 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.initializeConnection()
}
private setSidebarVisible(visible: boolean): void {
vscode.commands.executeCommand("setContext", "kilo-code.new.sidebarVisible", visible)
this.opts.onSidebarVisibilityChange?.(visible)
}
/**
* Resolve a WebviewPanel for displaying the Kilo webview in an editor tab.
*/
@@ -616,6 +623,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
connection: this.connectionService,
dir: this.getWorkspaceDirectory(this.currentSession?.id),
post: (msg) => this.postMessage(msg),
exportTranscript: (sessionID) => this.handleExportSessionTranscript(sessionID),
})
) {
return
@@ -647,7 +655,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.readyResolvers.splice(0).forEach((r) => r())
break
case "sendMessage": {
const files = parseMessageFiles(message.files)
const msg = message as typeof message & ContextMessage
await this.handleSendMessage(
message.text,
typeof message.messageID === "string" ? message.messageID : undefined,
@@ -657,13 +665,14 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
message.modelID,
message.agent,
message.variant,
files,
parseMessageFiles(message.files),
typeof message.agentManagerContext === "string" ? message.agentManagerContext : undefined,
typeof msg.contextDirectory === "string" ? msg.contextDirectory : undefined,
)
break
}
case "sendCommand": {
const files = parseMessageFiles(message.files)
const msg = message as typeof message & ContextMessage
await this.handleSendCommand(
message.command,
message.arguments,
@@ -674,8 +683,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
message.modelID,
message.agent,
message.variant,
files,
parseMessageFiles(message.files),
typeof message.agentManagerContext === "string" ? message.agentManagerContext : undefined,
typeof msg.contextDirectory === "string" ? msg.contextDirectory : undefined,
)
break
}
@@ -1081,13 +1091,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
break
}
case "fetchMarketplaceData": {
const workspace = this.getProjectDirectory(this.currentSession?.id)
const mp = this.getMarketplace()
// Fetch skills from CLI backend (authoritative source) so the
// marketplace doesn't need to duplicate the CLI's skill scanning.
const skills = await this.fetchCliSkills()
const data = await mp.fetchData(workspace, skills)
this.postMessage({ type: "marketplaceData", ...data })
await this.handleFetchMarketplaceData()
break
}
case "filterMarketplaceItems": {
@@ -1137,6 +1141,14 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.diffVirtualProvider.open(d)
}
private async handleFetchMarketplaceData(): Promise<void> {
const workspace = this.getProjectDirectory(this.currentSession?.id)
const mp = this.getMarketplace()
const skills = await this.fetchCliSkills()
const data = await mp.fetchData(workspace, skills)
this.postMessage({ type: "marketplaceData", ...data })
}
/**
* Initialize connection to the CLI backend server.
* Subscribes to the shared KiloConnectionService.
@@ -1346,7 +1358,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
try {
const workspaceDir = this.getContextDirectory()
const { data: session } = await this.client.session.create({ directory: workspaceDir }, { throwOnError: true })
const { data: session } = await this.client.session.create(
{ directory: workspaceDir, platform: this.opts.platform },
{ throwOnError: true },
)
this.setCurrentSession(session)
this.contextSessionID = session.id
this.trackDirectory(session.id, workspaceDir)
@@ -1536,6 +1551,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
client.session.list({ directory: dir, roots: true }, { throwOnError: true }).then(({ data }) => data)
: null,
sessionDirectories: this.sessionDirectories,
worktreeDirectories: this.opts.worktreeDirectories,
workspaceDirectory: this.getWorkspaceDirectory(),
postMessage: (msg: unknown) => this.postMessage(msg),
}
@@ -1654,6 +1670,30 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
}
}
/**
* Export a full session transcript as Markdown.
*/
private async handleExportSessionTranscript(sessionID: string): Promise<void> {
if (!this.client) {
this.postMessage({ type: "error", message: "Not connected to CLI backend" })
return
}
try {
const saved = await exportTranscript(this.client, {
sessionID,
dir: this.getWorkspaceDirectory(sessionID),
})
if (saved) void vscode.window.showInformationMessage("Session transcript exported as Markdown.")
} catch (error) {
console.error("[Kilo New] KiloProvider: Failed to export session transcript:", error)
this.postMessage({
type: "error",
message: getErrorMessage(error) || "Failed to export session transcript",
})
}
}
/** Fetch providers and send to webview. Coalesced: at most one in-flight + one queued. */
private async fetchAndSendProviders(): Promise<void> {
const next = ++this.providersGeneration
@@ -2439,7 +2479,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
details: getConfigErrorDetails(error),
})
}
private async resolveSession(sessionID?: string, draftID?: string, context?: string) {
private async resolveSession(sessionID?: string, draftID?: string, context?: string, contextDirectory?: string) {
if (!this.client) return undefined
const dir = resolveNewSessionDirectory({
@@ -2447,12 +2487,16 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
currentSessionID: this.currentSession?.id,
contextSessionID: this.contextSessionID,
agentManagerContext: context,
contextDirectory,
sessionDirectories: this.sessionDirectories,
workspaceDirectory: this.getRootDirectory(),
})
if (!sessionID && !this.currentSession) {
const { data: session } = await this.client.session.create({ directory: dir }, { throwOnError: true })
const { data: session } = await this.client.session.create(
{ directory: dir, platform: this.opts.platform },
{ throwOnError: true },
)
this.setCurrentSession(session)
this.contextSessionID = session.id
this.trackDirectory(session.id, dir)
@@ -2556,6 +2600,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
variant?: string,
files?: MessageFile[],
context?: string,
contextDirectory?: string,
): Promise<void> {
if (!this.client) {
this.postMessage({
@@ -2572,7 +2617,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
let resolved: { sid: string; dir: string } | undefined
try {
resolved = await this.resolveSession(sessionID, draftID, context)
resolved = await this.resolveSession(sessionID, draftID, context, contextDirectory)
const parts: Array<TextPartInput | FilePartInput> = []
if (files) {
@@ -2633,6 +2678,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
variant?: string,
files?: MessageFile[],
context?: string,
contextDirectory?: string,
): Promise<void> {
if (!this.client) {
this.postMessage({
@@ -2649,7 +2695,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
let resolved: { sid: string; dir: string } | undefined
try {
resolved = await this.resolveSession(sessionID, draftID, context)
resolved = await this.resolveSession(sessionID, draftID, context, contextDirectory)
if (messageID) {
this.connectionService.recordMessageSessionId(messageID, resolved!.sid)
@@ -71,6 +71,7 @@ export class AgentManagerProvider implements Disposable {
private cachedLocalStats: { type: "agentManager.localStats"; stats: LocalStats } | undefined
private unsubTool: (() => void) | undefined
private closing: Promise<void> | undefined
private onVisibilityChange: ((visible: boolean) => void) | undefined
/** Session ID most recently loaded via a `loadMessages` message from the webview.
* Updated synchronously — unlike the session provider's currentSession which depends on
@@ -181,10 +182,15 @@ export class AgentManagerProvider implements Disposable {
this.attachPanel(
this.host.openPanel({
onBeforeMessage: (msg) => this.onMessage(msg),
worktreeDirectories: () => this.getWorktreeDirectories(),
}),
)
}
public onPanelVisibilityChange(cb: (visible: boolean) => void): void {
this.onVisibilityChange = cb
}
/** Restore the Agent Manager panel from a previously serialized state.
* The caller (extension.ts / vscode-host.ts) wraps the raw panel before passing it. */
public deserializePanel(ctx: PanelContext): void {
@@ -212,6 +218,7 @@ export class AgentManagerProvider implements Disposable {
this.panel = ctx
this.statsPoller.setVisible(ctx.visible)
this.onVisibilityChange?.(ctx.visible)
ctx.onDidChangeVisibility((visible) => {
this.statsPoller.setVisible(visible)
})
@@ -233,6 +240,7 @@ export class AgentManagerProvider implements Disposable {
this.prBridge.poller.stop()
this.diffs.stop()
this.panel = undefined
this.onVisibilityChange?.(false)
}
ctx.sessions.dispose()
})
@@ -378,7 +386,7 @@ export class AgentManagerProvider implements Disposable {
if (m.type === "agentManager.deleteWorktree") return this.onDeleteWorktree(m.worktreeId)
if (m.type === "agentManager.removeStaleWorktree") return this.onRemoveStaleWorktree(m.worktreeId)
if (m.type === "agentManager.promoteSession") return this.onPromoteSession(m.sessionId)
if (m.type === "agentManager.addSessionToWorktree") return this.onAddSessionToWorktree(m.worktreeId)
if (m.type === "agentManager.addSessionToWorktree") return this.onAddSessionToWorktree(m.worktreeId, m.sessionId)
if (m.type === "agentManager.forkSession") return this.onForkSession(m.sessionId, m.worktreeId, m.messageId)
if (m.type === "agentManager.closeSession") return this.onCloseSession(m.sessionId)
}
@@ -418,6 +426,15 @@ export class AgentManagerProvider implements Disposable {
return null
}
if ((m.type === "sendMessage" || m.type === "sendCommand") && !m.sessionID) {
const ctx = typeof m.agentManagerContext === "string" ? m.agentManagerContext : undefined
const worktree = ctx && ctx !== "local" ? this.getStateManager()?.getWorktree(ctx) : undefined
if (worktree) {
if (m.draftID) this.activeSessionId = m.draftID
return { ...msg, contextDirectory: worktree.path }
}
}
if ((m.type === "sendMessage" || m.type === "sendCommand") && m.draftID && !m.sessionID) {
this.activeSessionId = m.draftID
return msg
@@ -1049,7 +1066,7 @@ export class AgentManagerProvider implements Disposable {
}
/** Add a new session to an existing worktree. */
private async onAddSessionToWorktree(worktreeId: string): Promise<null> {
private async onAddSessionToWorktree(worktreeId: string, sessionId?: string): Promise<null> {
let client: KiloClient
try {
client = this.connectionService.getClient()
@@ -1068,6 +1085,26 @@ export class AgentManagerProvider implements Disposable {
return null
}
if (sessionId) {
if (state.getSession(sessionId)) state.moveSession(sessionId, worktreeId)
else state.addSession(sessionId, worktreeId)
this.registerWorktreeSession(sessionId, worktree.path)
this.pushState()
this.postToWebview({
type: "agentManager.sessionAdded",
sessionId,
worktreeId,
})
this.host.capture("Agent Manager Session Started", {
source: PLATFORM,
sessionId,
worktreeId,
existing: true,
})
this.log(`Added existing session ${sessionId} to worktree ${worktreeId}`)
return null
}
let session: Session
try {
const { data } = await client.session.create(
@@ -1648,6 +1685,14 @@ export class AgentManagerProvider implements Disposable {
return this.panel?.sessions.getSessionDirectories() ?? new Map()
}
public getWorktreeDirectories(): string[] {
return (
this.getStateManager()
?.getWorktrees()
.map((wt) => wt.path) ?? []
)
}
/**
* Continue a sidebar session in a new worktree.
* Captures git state, creates worktree, applies state, forks session.
@@ -101,6 +101,7 @@ function createHarness() {
prBridge: { handleMessage: ReturnType<typeof vi.fn> }
activeSessionId: string | undefined
stateReady: Promise<void> | undefined
contextTarget: ReturnType<typeof vi.fn>
createWorktreeOnDisk: ReturnType<typeof vi.fn>
runSetupScriptForWorktree: ReturnType<typeof vi.fn>
createSessionInWorktree: ReturnType<typeof vi.fn>
@@ -121,6 +122,7 @@ function createHarness() {
manager.prBridge = { handleMessage: vi.fn().mockReturnValue(false) }
manager.activeSessionId = undefined
manager.stateReady = Promise.resolve()
manager.contextTarget = vi.fn()
manager.createWorktreeOnDisk = vi.fn()
manager.runSetupScriptForWorktree = vi.fn().mockResolvedValue(undefined)
manager.createSessionInWorktree = vi.fn()
@@ -202,4 +204,28 @@ describe("AgentManagerProvider worktree creation", () => {
expect(result).toEqual({ type: "requestFileSearch", query: "src", requestId: "r1", sessionID: "session-wt" })
})
it("resolves new sends to the selected worktree directory", async () => {
const manager = createHarness()
const state = {
getWorktree: vi.fn().mockReturnValue({ id: "wt-1", path: "/repo/.kilo/worktrees/wt-1" }),
}
manager.getStateManager.mockReturnValue(state)
manager.contextTarget.mockResolvedValue(undefined)
const result = await manager.onMessage({
type: "sendMessage",
text: "continue",
agentManagerContext: "wt-1",
draftID: "draft-1",
})
expect(result).toEqual({
type: "sendMessage",
text: "continue",
agentManagerContext: "wt-1",
draftID: "draft-1",
contextDirectory: "/repo/.kilo/worktrees/wt-1",
})
})
})
@@ -93,6 +93,7 @@ export interface Host {
*/
openPanel(opts: {
onBeforeMessage: (msg: Record<string, unknown>) => Promise<Record<string, unknown> | null>
worktreeDirectories?: () => string[]
}): PanelContext
/** Get the workspace/project root path. */
@@ -348,6 +348,7 @@ interface OpenLocallyIn {
interface AddSessionToWorktreeIn {
type: "agentManager.addSessionToWorktree"
worktreeId: string
sessionId?: string
}
interface CloseSessionIn {
@@ -606,6 +607,7 @@ interface SendMessageIn {
variant?: string
files?: Array<{ mime: string; url: string; filename?: string; source?: FileSourceIn }>
agentManagerContext?: string
contextDirectory?: string
}
interface SendCommandIn {
@@ -621,6 +623,7 @@ interface SendCommandIn {
variant?: string
files?: Array<{ mime: string; url: string; filename?: string; source?: FileSourceIn }>
agentManagerContext?: string
contextDirectory?: string
}
interface RequestTerminalContextIn {
@@ -9,6 +9,7 @@ import * as vscode from "vscode"
import type { Host, PanelContext, OutputHandle, SessionProvider, Disposable } from "./host"
import type { KiloConnectionService } from "../services/cli-backend"
import { KiloProvider } from "../KiloProvider"
import { PLATFORM } from "./constants"
import { DiffVirtualProvider } from "../DiffVirtualProvider"
import { buildWebviewHtml } from "../utils"
import { openFileInEditor, getWorkspaceRoot } from "../review-utils"
@@ -35,6 +36,7 @@ export class VscodeHost implements Host {
openPanel(opts: {
onBeforeMessage: (msg: Record<string, unknown>) => Promise<Record<string, unknown> | null>
worktreeDirectories?: () => string[]
}): PanelContext {
const panel = vscode.window.createWebviewPanel(
"kilo-code.new.AgentManagerPanel",
@@ -54,6 +56,7 @@ export class VscodeHost implements Host {
panel: vscode.WebviewPanel,
opts: {
onBeforeMessage: (msg: Record<string, unknown>) => Promise<Record<string, unknown> | null>
worktreeDirectories?: () => string[]
},
): PanelContext {
return this.wirePanel(panel, opts)
@@ -63,6 +66,7 @@ export class VscodeHost implements Host {
panel: vscode.WebviewPanel,
opts: {
onBeforeMessage: (msg: Record<string, unknown>) => Promise<Record<string, unknown> | null>
worktreeDirectories?: () => string[]
},
): PanelContext {
panel.webview.options = {
@@ -85,7 +89,9 @@ export class VscodeHost implements Host {
})
const provider = new KiloProvider(this.extensionUri, this.connectionService, this.context, {
platform: PLATFORM,
slimEditMetadata: true,
worktreeDirectories: () => opts.worktreeDirectories?.() ?? [],
})
if (this.diffVirtual) {
provider.setDiffVirtualProvider(this.diffVirtual)
+30 -1
View File
@@ -23,6 +23,14 @@ import { RemoteStatusService } from "./services/RemoteStatusService"
import { markWorkspace } from "./util/spotlight"
let agentManager: AgentManagerProvider | undefined
let shuttingDown = false
const RESTORE_KEY = "kilo.workbench.restore"
type RestoreState = {
sidebar?: boolean
agentManager?: boolean
}
const panelTitleHandler = (panel: vscode.WebviewPanel) => (title: string) => {
panel.title = title || EXTENSION_DISPLAY_NAME
@@ -34,11 +42,21 @@ const panelTitleHandler = (panel: vscode.WebviewPanel) => (title: string) => {
// it starts lazily when a webview connects or when ensureBackendForAutocomplete() triggers it.
export function activate(context: vscode.ExtensionContext) {
console.log("Kilo Code extension is now active")
shuttingDown = false
const telemetry = TelemetryProxy.getInstance()
// Create shared connection service (one server for all webviews)
const connectionService = new KiloConnectionService(context)
let restore = context.workspaceState.get<RestoreState>(RESTORE_KEY) ?? {}
const closeSidebar = restore.sidebar === false
const remember = (patch: RestoreState) => {
const next = { ...restore, ...patch }
if (shuttingDown && patch.sidebar === false) next.sidebar = restore.sidebar
if (shuttingDown && patch.agentManager === false) next.agentManager = restore.agentManager
restore = next
void context.workspaceState.update(RESTORE_KEY, restore)
}
// Create browser automation service (manages Playwright MCP registration)
const browserAutomationService = new BrowserAutomationService(connectionService)
@@ -105,7 +123,9 @@ export function activate(context: vscode.ExtensionContext) {
}
// Create the provider with shared service
const provider = new KiloProvider(context.extensionUri, connectionService, context)
const provider = new KiloProvider(context.extensionUri, connectionService, context, {
onSidebarVisibilityChange: (visible) => remember({ sidebar: visible }),
})
provider.setRemoteService(remoteService)
// Register the webview view provider for the sidebar.
@@ -115,6 +135,7 @@ export function activate(context: vscode.ExtensionContext) {
webviewOptions: { retainContextWhenHidden: true },
}),
)
if (closeSidebar) void vscode.commands.executeCommand("workbench.action.closeSidebar")
// Ensure Agent Manager navigation keybindings work when a VS Code terminal has focus.
// The terminal intercepts all keystrokes unless the command is listed in
@@ -131,6 +152,7 @@ export function activate(context: vscode.ExtensionContext) {
// Create Agent Manager provider for editor panel
const agentManagerHost = new VscodeHost(context.extensionUri, connectionService, context)
const agentManagerProvider = new AgentManagerProvider(agentManagerHost, connectionService)
agentManagerProvider.onPanelVisibilityChange((visible) => remember({ agentManager: visible }))
agentManager = agentManagerProvider
context.subscriptions.push(agentManagerProvider)
@@ -169,8 +191,13 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.window.registerWebviewPanelSerializer(AgentManagerProvider.viewType, {
deserializeWebviewPanel(panel: vscode.WebviewPanel) {
if (restore.agentManager === false) {
panel.dispose()
return Promise.resolve()
}
const ctx = agentManagerHost.wrapExistingPanel(panel, {
onBeforeMessage: (msg) => agentManagerProvider.handleMessage(msg),
worktreeDirectories: () => agentManagerProvider.getWorktreeDirectories(),
})
agentManagerProvider.deserializePanel(ctx)
return Promise.resolve()
@@ -452,6 +479,7 @@ export function activate(context: vscode.ExtensionContext) {
// Dispose services when extension deactivates (kills the server)
context.subscriptions.push({
dispose: () => {
shuttingDown = true
unsubscribeStateChange()
browserAutomationService.dispose()
provider.dispose()
@@ -461,6 +489,7 @@ export function activate(context: vscode.ExtensionContext) {
}
export async function deactivate() {
shuttingDown = true
await agentManager?.shutdown()
TelemetryProxy.getInstance().shutdown()
}
@@ -222,6 +222,7 @@ export interface SessionRefreshContext {
connectionState: "connecting" | "connected" | "disconnected" | "error"
listSessions: ((dir: string) => Promise<Session[]>) | null
sessionDirectories: Map<string, string>
worktreeDirectories?: () => string[]
workspaceDirectory: string
postMessage(message: unknown): void
}
@@ -245,7 +246,7 @@ export async function loadSessions(ctx: SessionRefreshContext): Promise<string |
const sessions = await list(ctx.workspaceDirectory)
const projectID = sessions[0]?.projectID
const worktreeDirs = new Set(ctx.sessionDirectories.values())
const worktreeDirs = new Set([...(ctx.worktreeDirectories?.() ?? []), ...ctx.sessionDirectories.values()])
const failed = new Set<string>()
const extra = await Promise.all(
[...worktreeDirs].map((dir) =>
@@ -339,6 +340,7 @@ export function resolveNewSessionDirectory(input: {
currentSessionID?: string
contextSessionID?: string
agentManagerContext?: string
contextDirectory?: string
sessionDirectories: Map<string, string>
workspaceDirectory: string
}) {
@@ -350,6 +352,8 @@ export function resolveNewSessionDirectory(input: {
})
}
if (input.contextDirectory) return input.contextDirectory
return resolveContextDirectory({
currentSessionID: input.currentSessionID,
contextSessionID: input.contextSessionID,
@@ -11,10 +11,16 @@ type Ctx = {
connection: KiloConnectionService
dir: string
post: (msg: unknown) => void
exportTranscript: (sessionID: string) => Promise<void>
}
export async function routeEarlyMessage(message: { type: string }, ctx: Ctx): Promise<boolean> {
await routeSuggestionWebviewMessage(ctx.question, message)
if (await ModelState.handleMessage(message.type, message, ctx.client, ctx.post)) return true
if (message.type === "exportSessionTranscript") {
const input = message as { sessionID?: unknown }
if (typeof input.sessionID === "string") await ctx.exportTranscript(input.sessionID)
return true
}
return await routeInputToolMessage(message, { connection: ctx.connection, dir: ctx.dir, post: ctx.post })
}
@@ -0,0 +1,58 @@
import * as path from "path"
import * as vscode from "vscode"
import type { KiloClient, Message, Part, Session } from "@kilocode/sdk/v2/client"
import { fetchMessagePage } from "./message-page"
type Item = {
info: Message
parts: Part[]
}
export async function exportTranscript(
client: KiloClient,
input: {
sessionID: string
dir: string
},
) {
const [{ data: session }, page] = await Promise.all([
client.session.get({ sessionID: input.sessionID, directory: input.dir }, { throwOnError: true }),
fetchMessagePage(client, { sessionID: input.sessionID, workspaceDir: input.dir, limit: 0 }),
])
const text = formatTranscript(session, page.items)
const uri = await vscode.window.showSaveDialog({
defaultUri: vscode.Uri.file(path.join(input.dir, `session-${session.id.slice(0, 8)}.md`)),
filters: { Markdown: ["md", "markdown"] },
saveLabel: "Export",
})
if (!uri) return false
await vscode.workspace.fs.writeFile(uri, Buffer.from(text, "utf8"))
return true
}
export function formatTranscript(session: Session, items: Item[]): string {
const head = [
`# ${session.title}`,
"",
`**Session ID:** ${session.id}`,
`**Created:** ${new Date(session.time.created).toLocaleString()}`,
`**Updated:** ${new Date(session.time.updated).toLocaleString()}`,
"",
"---",
"",
"",
].join("\n")
const body = items.map((item) => formatMessage(item)).join("---\n\n")
return `${head}${body}${items.length > 0 ? "---\n\n" : ""}`
}
function formatMessage(item: Item): string {
const head = item.info.role === "user" ? "## User\n\n" : "## Assistant\n\n"
return `${head}${item.parts.map((part) => formatPart(part)).join("")}`
}
function formatPart(part: Part): string {
if (part.type === "text" && !part.synthetic) return `${part.text}\n\n`
if (part.type === "tool") return `**Tool: ${part.tool}**\n\n`
return ""
}
@@ -1,5 +1,8 @@
export type KiloProviderOptions = {
projectDirectory?: string | null
platform?: string
slimEditMetadata?: boolean
tabTitle?: (title: string) => void
onSidebarVisibilityChange?: (visible: boolean) => void
worktreeDirectories?: () => string[]
}
@@ -29,6 +29,11 @@ type Audio = {
language?: string
}
type Args = {
pipe?: string[]
input: string[]
}
let active: Recording | undefined
let starting: string | undefined
@@ -119,13 +124,15 @@ async function waitForStart(state: Recording): Promise<void> {
})
}
async function startWithArgs(bin: string, file: string, input: Input, args: string[][]): Promise<Recording> {
async function startWithArgs(bin: string, file: string, input: Input, args: Args[]): Promise<Recording> {
const [first, ...rest] = args
if (!first) throw new Error(`Unsupported platform for speech input: ${process.platform}`)
const proc = spawn(bin, ["-y", ...first, "-acodec", "pcm_s16le", "-ar", "16000", "-ac", "1", "-f", "wav", file], {
stdio: ["pipe", "ignore", "pipe"],
})
const proc = first.pipe
? pipeProcess(first.pipe, bin, file)
: spawn(bin, ["-y", ...first.input, "-acodec", "pcm_s16le", "-ar", "16000", "-ac", "1", "-f", "wav", file], {
stdio: ["pipe", "ignore", "pipe"],
})
const state: Recording = { ...input, file, proc, stderr: [], stopped: false }
active = state
@@ -151,6 +158,48 @@ async function startWithArgs(bin: string, file: string, input: Input, args: stri
}
}
function pipeProcess(pipe: string[], bin: string, file: string): ChildProcess {
const source = spawn("pw-record", pipe, { stdio: ["ignore", "pipe", "pipe"] })
const proc = spawn(
bin,
[
"-y",
"-f",
"s16le",
"-ar",
"16000",
"-ac",
"1",
"-i",
"pipe:0",
"-acodec",
"pcm_s16le",
"-ar",
"16000",
"-ac",
"1",
"-f",
"wav",
file,
],
{
stdio: ["pipe", "ignore", "pipe"],
},
)
if (source.stdout && proc.stdin) source.stdout.pipe(proc.stdin)
source.on("error", (err) => proc.emit("error", err))
source.stderr?.on("data", (data: Buffer) => proc.stderr?.emit("data", data))
source.once("exit", () => {
if (proc.stdin?.writable) proc.stdin.end()
})
proc.once("exit", () => {
if (!source.killed) source.kill("SIGTERM")
})
return proc
}
async function stopProcess(state: Recording): Promise<void> {
if (state.proc.exitCode !== null || state.proc.signalCode) return
@@ -226,24 +275,33 @@ function platformPaths(): string[] {
return ["/usr/bin/ffmpeg", "/usr/local/bin/ffmpeg", "/snap/bin/ffmpeg", "/home/linuxbrew/.linuxbrew/bin/ffmpeg"]
}
async function inputArgSets(bin: string): Promise<string[][]> {
if (process.platform === "darwin") return [["-f", "avfoundation", "-i", ":default"]]
if (process.platform === "linux")
async function inputArgSets(bin: string): Promise<Args[]> {
if (process.platform === "darwin") return [{ input: ["-f", "avfoundation", "-i", ":default"] }]
if (process.platform === "linux") {
const device = process.env.KILO_FFMPEG_AUDIO_DEVICE
if (device)
return [
{ pipe: ["--target", device, "--format", "s16", "--rate", "16000", "--channels", "1", "-"], input: [] },
{ input: ["-f", "pulse", "-i", device] },
{ input: ["-f", "alsa", "-i", device] },
]
return [
["-f", "pulse", "-i", "default"],
["-f", "alsa", "-i", "default"],
{ pipe: ["--format", "s16", "--rate", "16000", "--channels", "1", "-"], input: [] },
{ input: ["-f", "pulse", "-i", "default"] },
{ input: ["-f", "alsa", "-i", "default"] },
]
}
if (process.platform === "win32") return await windowsInputArgSets(bin)
return []
}
async function windowsInputArgSets(bin: string): Promise<string[][]> {
async function windowsInputArgSets(bin: string): Promise<Args[]> {
const configured = process.env.KILO_FFMPEG_AUDIO_DEVICE
if (configured) return [["-f", "dshow", "-i", `audio=${configured}`]]
if (configured) return [{ input: ["-f", "dshow", "-i", `audio=${configured}`] }]
const devices = await listDshowAudioDevices(bin)
if (devices.length === 0) throw new Error("No Windows audio input devices found for speech input")
return devices.map((device) => ["-f", "dshow", "-i", `audio=${device}`])
return devices.map((device) => ({ input: ["-f", "dshow", "-i", `audio=${device}`] }))
}
async function listDshowAudioDevices(bin: string): Promise<string[]> {
@@ -266,11 +324,20 @@ function processOutput(err: unknown): string {
}
function summary(state: Recording, fallback: string): string {
const stderr = state.stderr.join("\n").trim()
const stderr = cleanOutput(state.stderr.join("\n"))
if (!stderr) return fallback
return `${fallback}: ${stderr.slice(-800)}`
}
export function cleanOutput(raw: string): string {
const lines = raw
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
.filter((line) => !/^(ffmpeg version|built with|configuration:|lib[a-z]+\s+\d)/i.test(line))
return lines.join("\n").trim()
}
async function removeFile(file: string): Promise<void> {
await unlink(file).catch((err: unknown) => {
console.warn("[Kilo New] Failed to remove speech recording", err)
@@ -2,6 +2,7 @@ export interface SpeechToTextModelDef {
readonly id: string
readonly label: string
readonly provider: string
readonly verbatim?: boolean
}
const models: SpeechToTextModelDef[] = [
@@ -9,11 +10,13 @@ const models: SpeechToTextModelDef[] = [
id: "openai/gpt-4o-mini-transcribe",
label: "GPT-4o Mini Transcribe",
provider: "OpenAI",
verbatim: true,
},
{
id: "openai/gpt-4o-transcribe",
label: "GPT-4o Transcribe",
provider: "OpenAI",
verbatim: true,
},
{
id: "openai/whisper-1",
@@ -3,6 +3,8 @@ import { getErrorMessage } from "../kilo-provider-utils"
import { getSpeechToTextModel } from "./models"
const PATH = "/kilo/audio/transcriptions"
const PROMPT =
"Transcribe exactly what is spoken. Do not paraphrase, summarize, infer intent, or rewrite for clarity. Preserve the speaker's original wording as closely as possible, including incomplete phrases and unusual wording when audible."
type Req = {
model?: string
@@ -39,6 +41,8 @@ export async function transcribeSpeech(
const auth = Buffer.from(`kilo:${cfg.password}`).toString("base64")
const url = new URL(PATH, cfg.baseUrl)
const model = getSpeechToTextModel(input.model)
const prompt = model.verbatim ? PROMPT : undefined
if (dir) url.searchParams.set("directory", dir)
try {
@@ -50,12 +54,13 @@ export async function transcribeSpeech(
"Content-Type": "application/json",
},
body: JSON.stringify({
model: getSpeechToTextModel(input.model).id,
model: model.id,
input_audio: {
data: input.data,
format: input.format,
},
...(input.language ? { language: input.language } : {}),
...(prompt ? { prompt } : {}),
}),
})
@@ -30,6 +30,7 @@ const TSX_FILES = [
path.join(ROOT, "webview-ui/agent-manager/DiffEndMarker.tsx"),
path.join(ROOT, "webview-ui/agent-manager/FileTree.tsx"),
path.join(ROOT, "webview-ui/agent-manager/review-annotations.ts"),
path.join(ROOT, "webview-ui/agent-manager/review-annotation-speech.tsx"),
path.join(ROOT, "webview-ui/agent-manager/MultiModelSelector.tsx"),
path.join(ROOT, "webview-ui/agent-manager/ApplyDialog.tsx"),
path.join(ROOT, "webview-ui/agent-manager/WorktreeItem.tsx"),
@@ -0,0 +1,58 @@
import { describe, expect, it } from "bun:test"
import { formatTranscript } from "../../src/kilo-provider/export-transcript"
describe("formatTranscript", () => {
it("formats complete Markdown session exports and skips hidden text", () => {
const text = formatTranscript(
{
id: "ses_123456789",
slug: "export-test",
projectID: "project",
directory: "/repo",
title: "Export test",
version: "1",
time: { created: 1_000, updated: 2_000 },
} as never,
[
{
info: { role: "user" },
parts: [
{ type: "text", text: "Please export this." },
{ type: "text", text: "hidden", synthetic: true },
],
},
{
info: { role: "assistant" },
parts: [
{ type: "tool", tool: "read" },
{ type: "text", text: "Saved as Markdown." },
],
},
] as never,
)
expect(text).toContain("# Export test")
expect(text).toContain("**Session ID:** ses_123456789")
expect(text).toContain("## User\n\nPlease export this.")
expect(text).toContain("## Assistant\n\n**Tool: read**\n\nSaved as Markdown.")
expect(text).not.toContain("hidden")
})
it("keeps an empty session export readable", () => {
const text = formatTranscript(
{
id: "ses_empty",
slug: "empty",
projectID: "project",
directory: "/repo",
title: "Empty",
version: "1",
time: { created: 1_000, updated: 2_000 },
} as never,
[],
)
expect(text).toContain("# Empty")
expect(text).toEndWith("---\n\n")
})
})
@@ -93,6 +93,17 @@ describe("resolveNewSessionDirectory", () => {
expect(dir).toBe("/repo/.kilo/worktrees/feature")
})
it("uses explicit Agent Manager worktree context after the active session was cleared", () => {
const dir = resolveNewSessionDirectory({
agentManagerContext: "wt_feature",
contextDirectory: "/repo/.kilo/worktrees/feature",
sessionDirectories: new Map(),
workspaceDirectory: "/repo",
})
expect(dir).toBe("/repo/.kilo/worktrees/feature")
})
it("creates local Agent Manager sessions in the workspace root after a worktree was selected", () => {
const dir = resolveNewSessionDirectory({
contextSessionID: "ses_worktree",
@@ -0,0 +1,20 @@
import { describe, expect, it } from "bun:test"
import { isAnnotationMutation } from "../../webview-ui/agent-manager/markdown-annotation-mutation"
function mutation(target: Node): Pick<MutationRecord, "target"> {
return { target }
}
describe("isAnnotationMutation", () => {
it("matches nested annotation DOM mutations", () => {
const target = { closest: () => ({}) } as unknown as Node
expect(isAnnotationMutation(mutation(target))).toBe(true)
})
it("keeps external Markdown DOM mutations observable", () => {
const markdown = { closest: () => null } as unknown as Node
const plain = {} as Node
expect(isAnnotationMutation(mutation(markdown))).toBe(false)
expect(isAnnotationMutation(mutation(plain))).toBe(false)
})
})
@@ -0,0 +1,105 @@
/**
* Tests for plan_exit webview helpers:
* - planDisplayPath: relative/absolute path display logic
* - plan_exit renderer uses openFile, not openDiff
*/
import { describe, expect, it } from "bun:test"
import { planDisplayPath } from "../../webview-ui/src/utils/plan-path"
import fs from "node:fs"
import path from "node:path"
describe("planDisplayPath", () => {
it("returns a relative path unchanged", () => {
expect(planDisplayPath(".kilo/plans/my-plan.md", "/repo")).toBe(".kilo/plans/my-plan.md")
})
it("returns absolute path inside repo as repo-relative", () => {
expect(planDisplayPath("/repo/.kilo/plans/my-plan.md", "/repo")).toBe(".kilo/plans/my-plan.md")
})
it("returns absolute path inside repo with trailing slash on root as repo-relative", () => {
expect(planDisplayPath("/repo/.kilo/plans/my-plan.md", "/repo/")).toBe(".kilo/plans/my-plan.md")
})
it("returns absolute path outside repo unchanged", () => {
expect(planDisplayPath("/other/path/plan.md", "/repo")).toBe("/other/path/plan.md")
})
it("handles Windows absolute paths inside root", () => {
expect(planDisplayPath("C:\\repo\\.kilo\\plans\\plan.md", "C:\\repo")).toBe(".kilo\\plans\\plan.md")
})
it("handles Windows absolute paths outside root", () => {
expect(planDisplayPath("D:\\other\\plan.md", "C:\\repo")).toBe("D:\\other\\plan.md")
})
it("returns empty string unchanged", () => {
expect(planDisplayPath("", "/repo")).toBe("")
})
it("path equal to root returns original", () => {
// Edge: plan path IS the root directory itself — fall back to original
expect(planDisplayPath("/repo", "/repo")).toBe("/repo")
})
})
describe("plan_exit renderer uses openFile not openDiff (source)", () => {
const ROOT = path.resolve(import.meta.dir, "../..")
const FILE = path.join(ROOT, "webview-ui/src/components/chat/AssistantMessage.tsx")
const TURN_FILE = path.join(ROOT, "webview-ui/src/components/chat/VscodeSessionTurn.tsx")
const src = fs.readFileSync(FILE, "utf-8")
const turnSrc = fs.readFileSync(TURN_FILE, "utf-8")
it("PlanExitCard calls data.openFile", () => {
expect(src).toContain("data.openFile")
})
it("uses a safe inert anchor href", () => {
expect(src).toContain('href="#"')
expect(src).not.toContain("href={display()}")
})
it("always uses the generic ready label", () => {
expect(src).toContain('language.t("plan.exit.ready")')
expect(src).not.toContain('language.t("plan.exit.readyUpdated")')
expect(src).not.toContain('language.t("plan.exit.readyNew")')
})
it("does not infer status from tool history", () => {
expect(src).not.toContain("function inferPlanStatus")
expect(src).not.toContain("function patchUpdatedPlan")
expect(src).not.toContain("function toolTouchesPlan")
expect(src).not.toContain("toolDeletions")
expect(src).not.toContain("readyUpdated")
expect(src).not.toContain("readyNew")
expect(src).not.toContain("Object.values(data.store.part ?? {}).flat()")
expect(src).not.toContain("[...props.parts, ...all()]")
expect(src).not.toContain("turnParts")
expect(turnSrc).not.toContain("assistantMessages().flatMap")
expect(turnSrc).not.toContain("turnParts={assistantParts()}")
})
it("does not depend on opencode-provided plan status metadata", () => {
expect(src).not.toContain('meta.status === "updated" || meta.status === "new"')
expect(src).not.toContain("meta.status")
})
it("PlanExitCard does not call openDiffVirtual", () => {
// Extract just the PlanExitCard function body to scope the assertion
const start = src.indexOf("function PlanExitCard")
const end = src.indexOf("\nfunction ", start + 1)
const block = end === -1 ? src.slice(start) : src.slice(start, end)
expect(block).not.toContain("openDiffVirtual")
expect(block).not.toContain("openDiff")
})
it("plan_exit tool is handled before generic Part renderer", () => {
const planExitIdx = src.indexOf("planExit()")
// <Part may be followed by newline or space
const partIdx = src.search(/<Part[\s\n]/)
expect(planExitIdx).toBeGreaterThan(0)
expect(partIdx).toBeGreaterThan(0)
expect(planExitIdx).toBeLessThan(partIdx)
})
})
@@ -8,6 +8,11 @@ import {
type ReviewComment,
} from "../../webview-ui/agent-manager/review-comments"
import { markdownCommentBlocks } from "../../webview-ui/agent-manager/markdown-comment-ranges"
import {
reviewAnnotationSpeechKey,
reviewDraftSpeechKey,
reviewEditSpeechKey,
} from "../../webview-ui/agent-manager/review-annotations"
import type { WorktreeFileDiff } from "../../webview-ui/src/types/messages"
function diff(file: string, before: string, after: string): WorktreeFileDiff {
@@ -236,6 +241,53 @@ describe("markdownCommentBlocks", () => {
})
})
// ── review annotation speech keys ───────────────────────────────────────────
describe("review annotation speech keys", () => {
it("keeps draft keys scoped to the selected review location", () => {
expect(reviewDraftSpeechKey({ file: "src/a.ts", side: "additions", line: 4 })).toBe("draft:src/a.ts:additions:4:4")
expect(reviewDraftSpeechKey({ file: "src/a.ts", side: "deletions", line: 4, endLine: 8 })).toBe(
"draft:src/a.ts:deletions:4:8",
)
})
it("keeps edit keys scoped to the review comment id", () => {
expect(reviewEditSpeechKey("c-123")).toBe("edit:c-123")
})
it("only exposes annotation speech keys for active draft and edit composers", () => {
const current = comment({ file: "src/a.ts", line: 7 })
expect(
reviewAnnotationSpeechKey({
type: "draft",
comment: null,
file: "src/a.ts",
side: "additions",
line: 7,
}),
).toBe("draft:src/a.ts:additions:7:7")
expect(
reviewAnnotationSpeechKey({
type: "comment",
comment: current,
file: current.file,
side: current.side,
line: current.line,
editing: true,
}),
).toBe(`edit:${current.id}`)
expect(
reviewAnnotationSpeechKey({
type: "comment",
comment: current,
file: current.file,
side: current.side,
line: current.line,
}),
).toBeUndefined()
})
})
// ── getDirectory / getFilename ──────────────────────────────────────────────
describe("getDirectory", () => {
@@ -1,5 +1,5 @@
import { describe, expect, it } from "bun:test"
import { parseDshowAudioDevices } from "../../src/speech-to-text/capture"
import { cleanOutput, parseDshowAudioDevices } from "../../src/speech-to-text/capture"
describe("parseDshowAudioDevices", () => {
it("extracts Windows dshow audio device names", () => {
@@ -20,3 +20,20 @@ describe("parseDshowAudioDevices", () => {
expect(parseDshowAudioDevices(raw)).toEqual(["Microphone"])
})
})
describe("cleanOutput", () => {
it("removes ffmpeg build noise from capture errors", () => {
const raw = `
ffmpeg version 4.2.7 Copyright (c) 2000-2022 the FFmpeg developers
built with gcc 9 (Ubuntu 9.4.0-1ubuntu1~20.04.2)
configuration: --enable-libopus --enable-libx264
libavutil 56. 31.100 / 56. 31.100
ALSA lib ../../../src/pcm/pcm.c:2477:(snd_pcm_open_conf) Unknown field libs
default: Input/output error
`
expect(cleanOutput(raw)).toBe(
"ALSA lib ../../../src/pcm/pcm.c:2477:(snd_pcm_open_conf) Unknown field libs\ndefault: Input/output error",
)
})
})
@@ -0,0 +1,89 @@
import { describe, expect, it, mock } from "bun:test"
import { createRoot } from "solid-js"
import type { ExtensionMessage, WebviewMessage } from "../../webview-ui/src/types/messages"
mock.module("@kilocode/kilo-ui/toast", () => ({
showToast: () => undefined,
}))
const { useSpeechToText } = await import("../../webview-ui/src/components/speech-to-text/useSpeechToText")
function setup() {
const sent: WebviewMessage[] = []
let handler: ((message: ExtensionMessage) => void) | undefined
const root = createRoot((dispose) => ({
dispose,
speech: useSpeechToText(
{
postMessage: (message) => sent.push(message),
onMessage: (next) => {
handler = next
return () => {
handler = undefined
}
},
},
{ profileData: () => ({}), goToLogin: () => {} },
{ t: (key) => key },
),
}))
const fire = (message: ExtensionMessage) => handler?.(message)
return { ...root, fire, sent }
}
describe("useSpeechToText", () => {
it("runs the stop completion after inserting a transcript", () => {
const ctx = setup()
const text: string[] = []
let done = 0
ctx.speech.start({ model: "scribe", insert: (value) => text.push(value) })
const start = ctx.sent[0]
if (start?.type !== "speechToTextStart") throw new Error("speech start message missing")
ctx.speech.stop({ done: () => done++ })
ctx.fire({ type: "speechToTextResult", requestId: start.requestId, text: "Recorded prompt" })
expect(text).toEqual(["Recorded prompt"])
expect(done).toBe(1)
expect(ctx.speech.state()).toBe("idle")
ctx.dispose()
})
it("drops the stop completion when transcription is cancelled", () => {
const ctx = setup()
let done = 0
ctx.speech.start({ model: "scribe", insert: () => {} })
const start = ctx.sent[0]
if (start?.type !== "speechToTextStart") throw new Error("speech start message missing")
ctx.speech.stop({ done: () => done++ })
ctx.speech.cancel()
ctx.fire({ type: "speechToTextResult", requestId: start.requestId, text: "Ignored prompt" })
expect(done).toBe(0)
expect(ctx.speech.state()).toBe("idle")
ctx.dispose()
})
it("drops the stop completion when the send context changes", () => {
const ctx = setup()
const text: string[] = []
let done = 0
ctx.speech.start({ model: "scribe", insert: (value) => text.push(value) })
const start = ctx.sent[0]
if (start?.type !== "speechToTextStart") throw new Error("speech start message missing")
ctx.speech.stop({ done: () => done++, ready: () => false })
ctx.fire({ type: "speechToTextResult", requestId: start.requestId, text: "Keep as draft" })
expect(text).toEqual(["Keep as draft"])
expect(done).toBe(0)
expect(ctx.speech.state()).toBe("idle")
ctx.dispose()
})
})
@@ -130,6 +130,7 @@ import { createMarkdownRender } from "./review-preferences"
import { createSidebarCollapse } from "./sidebar-collapse"
import { SidebarToggleButton } from "./SidebarToggleButton"
import { setTabWidths } from "./tab-widths"
import { buildShortcutCategories } from "./shortcuts"
import "./agent-manager.css"
import "./agent-manager-review.css"
const REVIEW_TAB_ID = "review"
@@ -185,76 +186,6 @@ const defaultBindings: Record<string, string> = {
),
}
/** Shortcut category definition for the keyboard shortcuts dialog */
interface ShortcutEntry {
label: string
binding: string
}
interface ShortcutCategory {
title: string
shortcuts: ShortcutEntry[]
}
/** Build the categorized list of keyboard shortcuts from the current bindings */
function buildShortcutCategories(
bindings: Record<string, string>,
t: (key: string, params?: Record<string, string | number>) => string,
): ShortcutCategory[] {
return [
{
title: t("agentManager.shortcuts.category.quickSwitch"),
shortcuts: [
{
label: t("agentManager.shortcuts.jumpToItem"),
binding: (() => {
const first = bindings.jumpTo1 ?? ""
const prefix = first.replace(/\d+$/, "")
return prefix ? `${prefix}1-9` : ""
})(),
},
],
},
{
title: t("agentManager.shortcuts.category.sidebar"),
shortcuts: [
{ label: t("agentManager.shortcuts.previousItem"), binding: bindings.previousSession ?? "" },
{ label: t("agentManager.shortcuts.nextItem"), binding: bindings.nextSession ?? "" },
{ label: t("agentManager.shortcuts.newWorktree"), binding: bindings.newWorktree ?? "" },
{ label: t("agentManager.shortcuts.advancedWorktree"), binding: bindings.advancedWorktree ?? "" },
{ label: t("agentManager.shortcuts.deleteWorktree"), binding: bindings.closeWorktree ?? "" },
{ label: t("agentManager.shortcuts.openWorktree"), binding: bindings.openWorktree ?? "" },
],
},
{
title: t("agentManager.shortcuts.category.tabs"),
shortcuts: [
{ label: t("agentManager.shortcuts.previousTab"), binding: bindings.previousTab ?? "" },
{ label: t("agentManager.shortcuts.nextTab"), binding: bindings.nextTab ?? "" },
{ label: t("agentManager.shortcuts.newTab"), binding: bindings.newTab ?? "" },
{ label: t("agentManager.shortcuts.closeTab"), binding: bindings.closeTab ?? "" },
],
},
{
title: t("agentManager.shortcuts.category.terminal"),
shortcuts: [
{ label: t("agentManager.shortcuts.toggleTerminal"), binding: bindings.showTerminal ?? "" },
{ label: t("agentManager.shortcuts.runScript"), binding: bindings.runScript ?? "" },
{ label: t("agentManager.shortcuts.toggleDiff"), binding: bindings.toggleDiff ?? "" },
],
},
{
title: t("agentManager.shortcuts.category.global"),
shortcuts: [
{ label: t("agentManager.shortcuts.openAgentManager"), binding: bindings.agentManagerOpen ?? "" },
{ label: t("agentManager.shortcuts.cycleAgentMode"), binding: bindings.cycleAgentMode ?? "" },
{ label: t("agentManager.shortcuts.cyclePreviousAgentMode"), binding: bindings.cyclePreviousAgentMode ?? "" },
{ label: t("agentManager.shortcuts.showShortcuts"), binding: bindings.showShortcuts ?? "" },
].filter((s) => s.binding),
},
].filter((c) => c.shortcuts.length > 0)
}
import { parseBindingTokens } from "./keybind-tokens"
const AgentManagerContent: Component = () => {
@@ -1003,6 +934,27 @@ const AgentManagerContent: Component = () => {
setReviewActive(remembered === REVIEW_TAB_ID && reviewOpenByContext()[worktreeId] === true)
}
const addSessionToCurrentWorktree = (sid: string) => {
const sel = selection()
if (!sel || sel === LOCAL) return false
const current = managedSessions().find((entry) => entry.id === sid)
if (current?.worktreeId) return focusManagedSession(current.worktreeId, sid)
saveTabMemory()
setHistory(false)
setReviewActive(false)
appendToTabOrder(sel, sid)
evictLocal(sid)
vscode.postMessage({ type: "agentManager.addSessionToWorktree", worktreeId: sel, sessionId: sid })
return true
}
const focusManagedSession = (worktreeId: string, sid: string) => {
selectWorktree(worktreeId)
setHistory(false)
session.selectSession(sid)
return true
}
const cycleAgent = (direction: 1 | -1) => {
const available = session.agents().filter((a) => a.mode !== "subagent" && !a.hidden)
if (available.length <= 1) return
@@ -1220,6 +1172,7 @@ const AgentManagerContent: Component = () => {
saveTabMemory()
appendToTabOrder(ev.worktreeId, ev.sessionId)
setSelection(ev.worktreeId)
evictLocal(ev.sessionId)
drafts.apply(ev.worktreeId, ev.sessionId)
session.selectSession(ev.sessionId)
}
@@ -2988,6 +2941,7 @@ const AgentManagerContent: Component = () => {
<Show when={history()}>
<HistoryView
onSelectSession={(id) => {
if (addSessionToCurrentWorktree(id)) return
setHistory(false)
if (localSessionIDs().includes(id)) {
saveTabMemory()
@@ -3021,6 +2975,7 @@ const AgentManagerContent: Component = () => {
<div class="am-chat-wrapper">
<ChatView
onSelectSession={(id) => {
if (addSessionToCurrentWorktree(id)) return
if (localSessionIDs().includes(id)) {
session.selectSession(id)
if (selection() === null) setSelection(LOCAL)
@@ -14,13 +14,22 @@ import type { DiffLineAnnotation, AnnotationSide, SelectedLineRange } from "@pie
import type { WorktreeFileDiff } from "../src/types/messages"
import { KILO_FILE_PATH_MIME } from "../src/utils/path-mentions"
import { useLanguage } from "../src/context/language"
import { useVSCode } from "../src/context/vscode"
import { useServer } from "../src/context/server"
import { useProvider } from "../src/context/provider"
import { useConfig } from "../src/context/config"
import { canUseSpeechToText, selectedSpeechToTextModel } from "../src/components/speech-to-text/availability"
import { useSpeechToText } from "../src/components/speech-to-text/useSpeechToText"
import { getDirectory, getFilename, lineCount, sanitizeReviewComments, type ReviewComment } from "./review-comments"
import {
buildFileAnnotations,
buildReviewAnnotation,
reviewDraftSpeechKey,
reviewEditSpeechKey,
type AnnotationLabels,
type AnnotationMeta,
} from "./review-annotations"
import { createReviewAnnotationSpeechRenderer } from "./review-annotation-speech"
import {
LONG_DIFF_MARKER_FILE_COUNT,
allOpenFiles,
@@ -59,6 +68,13 @@ interface DiffPanelProps {
export const DiffPanel: Component<DiffPanelProps> = (props) => {
const { t } = useLanguage()
const vscode = useVSCode()
const server = useServer()
const provider = useProvider()
const { config, settings } = useConfig()
const speech = useSpeechToText(vscode, server, { t })
const canUseSpeech = () => canUseSpeechToText(settings(), config(), provider.connected(), server.profileData())
const speechModel = () => selectedSpeechToTextModel(settings())
const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent)
const sendAllKeybind = () =>
isMac ? t("agentManager.review.sendAllShortcut.mac") : t("agentManager.review.sendAllShortcut.other")
@@ -78,6 +94,21 @@ export const DiffPanel: Component<DiffPanelProps> = (props) => {
null,
)
const [editing, setEditing] = createSignal<string | null>(null)
const speechKeys = createMemo(() => {
const keys = new Set<string>()
const current = draft()
const edit = editing()
if (current) keys.add(reviewDraftSpeechKey(current))
if (edit) keys.add(reviewEditSpeechKey(edit))
return keys
})
const reviewSpeech = createReviewAnnotationSpeechRenderer({
speech,
enabled: canUseSpeech,
model: speechModel,
label: t,
keys: speechKeys,
})
let nextId = 0
// Tracks the session key for which initial open state has already run. When the
// key changes (different worktree) we expand reviewable files. Within the same key,
@@ -307,6 +338,7 @@ export const DiffPanel: Component<DiffPanelProps> = (props) => {
cancelDraft,
labels: labels(),
activeTerminalId: props.activeTerminalId,
speech: reviewSpeech,
})
}
@@ -20,15 +20,24 @@ import type { DiffLineAnnotation, AnnotationSide, SelectedLineRange } from "@pie
import type { WorktreeFileDiff } from "../src/types/messages"
import { KILO_FILE_PATH_MIME } from "../src/utils/path-mentions"
import { useLanguage } from "../src/context/language"
import { useVSCode } from "../src/context/vscode"
import { useServer } from "../src/context/server"
import { useProvider } from "../src/context/provider"
import { useConfig } from "../src/context/config"
import { canUseSpeechToText, selectedSpeechToTextModel } from "../src/components/speech-to-text/availability"
import { useSpeechToText } from "../src/components/speech-to-text/useSpeechToText"
import { FileTree } from "./FileTree"
import { treeOrder } from "./file-tree-utils"
import { getDirectory, getFilename, lineCount, sanitizeReviewComments, type ReviewComment } from "./review-comments"
import {
buildFileAnnotations,
buildReviewAnnotation,
reviewDraftSpeechKey,
reviewEditSpeechKey,
type AnnotationLabels,
type AnnotationMeta,
} from "./review-annotations"
import { createReviewAnnotationSpeechRenderer } from "./review-annotation-speech"
import {
LONG_DIFF_MARKER_FILE_COUNT,
allOpenFiles,
@@ -69,6 +78,13 @@ interface FullScreenDiffViewProps {
export const FullScreenDiffView: Component<FullScreenDiffViewProps> = (props) => {
const { t } = useLanguage()
const vscode = useVSCode()
const server = useServer()
const provider = useProvider()
const { config, settings } = useConfig()
const speech = useSpeechToText(vscode, server, { t })
const canUseSpeech = () => canUseSpeechToText(settings(), config(), provider.connected(), server.profileData())
const speechModel = () => selectedSpeechToTextModel(settings())
const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent)
const sendAllKeybind = () =>
isMac ? t("agentManager.review.sendAllShortcut.mac") : t("agentManager.review.sendAllShortcut.other")
@@ -88,6 +104,21 @@ export const FullScreenDiffView: Component<FullScreenDiffViewProps> = (props) =>
null,
)
const [editing, setEditing] = createSignal<string | null>(null)
const speechKeys = createMemo(() => {
const keys = new Set<string>()
const current = draft()
const edit = editing()
if (current) keys.add(reviewDraftSpeechKey(current))
if (edit) keys.add(reviewEditSpeechKey(edit))
return keys
})
const reviewSpeech = createReviewAnnotationSpeechRenderer({
speech,
enabled: canUseSpeech,
model: speechModel,
label: t,
keys: speechKeys,
})
const [activeFile, setActiveFile] = createSignal<string | null>(null)
const [treeWidth, setTreeWidth] = createSignal(240)
let nextId = 0
@@ -322,6 +353,7 @@ export const FullScreenDiffView: Component<FullScreenDiffViewProps> = (props) =>
cancelDraft,
labels: labels(),
activeTerminalId: props.activeTerminalId,
speech: reviewSpeech,
})
}
@@ -2,6 +2,7 @@ import { type Component, createEffect, createMemo, onCleanup } from "solid-js"
import type { AnnotationSide, DiffLineAnnotation, SelectedLineRange } from "@pierre/diffs"
import { markdownCommentBlocks, type MarkdownRange } from "./markdown-comment-ranges"
import type { AnnotationMeta } from "./review-annotations"
import { annotationSelector, isAnnotationMutation } from "./markdown-annotation-mutation"
type Insert = "after" | "list" | "table"
@@ -88,11 +89,11 @@ function matches(annotation: DiffLineAnnotation<AnnotationMeta>, anchor: Anchor,
return true
}
const selector = annotationSelector()
function removeInserted(root: HTMLElement, layer: HTMLElement): void {
layer.replaceChildren()
root
.querySelectorAll(".am-markdown-inline-annotations, .am-markdown-list-annotation, .am-markdown-table-annotation")
.forEach((node) => node.remove())
root.querySelectorAll(selector).forEach((node) => node.remove())
}
function insertHost(anchor: Anchor, host: HTMLElement): void {
@@ -206,7 +207,10 @@ export const MarkdownAnnotationLayer: Component<MarkdownAnnotationLayerProps> =
const root = props.root()
if (!root) return
observer?.disconnect()
observer = new MutationObserver(schedule)
observer = new MutationObserver((mutations) => {
if (mutations.every(isAnnotationMutation)) return
schedule()
})
observer.observe(root, { childList: true, subtree: true })
})
@@ -2146,6 +2146,11 @@ body.am-wt-dragging-active * {
gap: 6px;
}
.am-annotation-speech {
display: flex;
align-items: center;
}
.am-annotation-btn {
padding: 2px 10px;
font-size: inherit;
@@ -2553,12 +2558,25 @@ body.am-wt-dragging-active * {
.am-nv-dialog .am-prompt-input-wrapper {
flex: 1;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
}
.am-nv-dialog .am-prompt-input-container:has([class~="model-selector-popover"][data-component="popover-content"]),
.am-nv-dialog-content:has([class~="model-selector-popover"][data-component="popover-content"]),
[data-component="dialog"]:has(.am-nv-dialog [class~="model-selector-popover"][data-component="popover-content"])
[data-slot="dialog-body"] {
overflow: visible;
}
.am-nv-dialog .am-prompt-input-ghost-wrapper {
height: 100%;
}
.am-nv-dialog [class~="prompt-input-hint"] {
flex-shrink: 0;
}
.am-nv-dialog .am-prompt-input {
color: var(--vscode-input-foreground, var(--text-base));
resize: none;
@@ -0,0 +1,13 @@
const selector = ".am-markdown-inline-annotations, .am-markdown-list-annotation, .am-markdown-table-annotation"
// Keep host insertion observable, only nested annotation UI updates are safe to ignore.
export function isAnnotationMutation(mutation: Pick<MutationRecord, "target">): boolean {
const target = mutation.target
if (!("closest" in target)) return false
if (typeof target.closest !== "function") return false
return target.closest(selector) !== null
}
export function annotationSelector(): string {
return selector
}
@@ -0,0 +1,138 @@
import { createEffect, createRoot, createSignal, onCleanup, Show, type Accessor } from "solid-js"
import { render as renderSolid } from "solid-js/web"
import { SpeechToTextButton } from "../src/components/speech-to-text/SpeechToTextButton"
import { insertSpacedText } from "../src/components/chat/prompt-input-utils"
import type { SpeechState, SpeechToText } from "../src/components/speech-to-text/useSpeechToText"
import { reviewAnnotationSpeechKey, type AnnotationMeta } from "./review-annotations"
type Props = {
speech: SpeechToText
enabled: Accessor<boolean>
model: Accessor<string>
label: (key: string) => string
keys: Accessor<Set<string>>
}
type Node = {
host: HTMLDivElement
dispose: VoidFunction
setTextarea: (textarea: HTMLTextAreaElement) => void
}
function insertReviewSpeechText(textarea: HTMLTextAreaElement, value: string): void {
const text = textarea.value
const start = textarea.selectionStart ?? text.length
const end = textarea.selectionEnd ?? start
const result = insertSpacedText(text, value, start, end)
textarea.value = result.text
textarea.setSelectionRange(result.pos, result.pos)
textarea.focus()
}
export function createReviewAnnotationSpeechRenderer(props: Props) {
const nodes = new Map<string, Node>()
const [owner, setOwner] = createSignal<string | undefined>()
const mount = (key: string, textarea: HTMLTextAreaElement): Node | undefined => {
if (typeof document === "undefined") return undefined
const host = document.createElement("div")
host.className = "am-annotation-speech"
let field = textarea
const mine = () => owner() === key
const state = (): SpeechState => (mine() ? props.speech.state() : "idle")
const start = (model: string) => {
setOwner(key)
props.speech.start({
model,
insert: (value) => insertReviewSpeechText(field, value),
})
}
const speech: SpeechToText = {
state,
error: () => (mine() ? props.speech.error() : undefined),
active: () => mine() && props.speech.active(),
start: (opts) => start(opts.model),
stop: () => {
if (!mine()) return
props.speech.stop()
},
cancel: () => {
if (!mine()) return
props.speech.cancel()
setOwner(undefined)
},
clear: () => {
if (!mine()) return
props.speech.clear()
setOwner(undefined)
},
}
const blocked = () => props.speech.active() && !mine()
const dispose = createRoot((root) => {
const view = renderSolid(
() => (
<Show when={props.enabled()}>
<SpeechToTextButton
speech={speech}
disabled={blocked()}
start={() => start(props.model())}
label={props.label}
/>
</Show>
),
host,
)
onCleanup(view)
return root
})
const node = {
host,
dispose,
setTextarea: (next: HTMLTextAreaElement) => {
field = next
},
}
nodes.set(key, node)
return node
}
const render = (meta: AnnotationMeta, textarea: HTMLTextAreaElement): HTMLElement | undefined => {
const key = reviewAnnotationSpeechKey(meta)
if (!key) return undefined
const node = nodes.get(key) ?? mount(key, textarea)
if (!node) return undefined
node.setTextarea(textarea)
return node.host
}
createEffect(() => {
const keys = props.keys()
const current = owner()
if (current && !keys.has(current)) {
if (props.speech.active()) props.speech.cancel()
if (props.speech.state() === "error") props.speech.clear()
setOwner(undefined)
}
for (const [key, node] of nodes) {
if (keys.has(key)) continue
node.dispose()
nodes.delete(key)
}
})
onCleanup(() => {
if (owner() && props.speech.active()) props.speech.cancel()
for (const [, node] of nodes) node.dispose()
nodes.clear()
})
return {
active: props.speech.active,
render,
}
}
@@ -26,6 +26,22 @@ export interface AnnotationMeta {
editing?: boolean
}
type SpeechDraft = Pick<AnnotationMeta, "file" | "side" | "line" | "endLine">
export function reviewDraftSpeechKey(draft: SpeechDraft): string {
return `draft:${draft.file}:${draft.side}:${draft.line}:${draft.endLine ?? draft.line}`
}
export function reviewEditSpeechKey(id: string): string {
return `edit:${id}`
}
export function reviewAnnotationSpeechKey(meta: AnnotationMeta): string | undefined {
if (meta.type === "draft") return reviewDraftSpeechKey(meta)
if (!meta.editing || !meta.comment) return undefined
return reviewEditSpeechKey(meta.comment.id)
}
interface AnnotationHandlers {
diffs: WorktreeFileDiff[]
editing: string | null
@@ -36,6 +52,10 @@ interface AnnotationHandlers {
cancelDraft: () => void
labels: AnnotationLabels
activeTerminalId?: string
speech?: {
active: () => boolean
render: (meta: AnnotationMeta, textarea: HTMLTextAreaElement) => HTMLElement | undefined
}
}
function focusWhenConnected(el: HTMLTextAreaElement): void {
@@ -150,6 +170,8 @@ export function buildReviewAnnotation(
submitButton.className = "am-annotation-btn am-annotation-btn-submit"
submitButton.textContent = handlers.labels.comment
const speech = handlers.speech?.render(meta, textarea)
if (speech) actions.appendChild(speech)
actions.appendChild(cancelButton)
actions.appendChild(submitButton)
wrapper.appendChild(header)
@@ -159,6 +181,7 @@ export function buildReviewAnnotation(
focusWhenConnected(textarea)
const submit = () => {
if (handlers.speech?.active()) return
const text = textarea.value.trim()
if (!text) return
const diff = handlers.diffs.find((item) => item.file === meta.file)
@@ -216,6 +239,8 @@ export function buildReviewAnnotation(
saveButton.className = "am-annotation-btn am-annotation-btn-submit"
saveButton.textContent = handlers.labels.save
const speech = handlers.speech?.render(meta, textarea)
if (speech) actions.appendChild(speech)
actions.appendChild(cancelButton)
actions.appendChild(saveButton)
wrapper.appendChild(header)
@@ -231,6 +256,7 @@ export function buildReviewAnnotation(
saveButton.addEventListener("click", (event) => {
event.stopPropagation()
if (handlers.speech?.active()) return
const text = textarea.value.trim()
if (!text) return
handlers.updateComment(comment.id, text)
@@ -244,6 +270,7 @@ export function buildReviewAnnotation(
}
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault()
if (handlers.speech?.active()) return
const text = textarea.value.trim()
if (!text) return
handlers.updateComment(comment.id, text)
@@ -0,0 +1,67 @@
export interface ShortcutEntry {
label: string
binding: string
}
export interface ShortcutCategory {
title: string
shortcuts: ShortcutEntry[]
}
export function buildShortcutCategories(
bindings: Record<string, string>,
t: (key: string, params?: Record<string, string | number>) => string,
): ShortcutCategory[] {
return [
{
title: t("agentManager.shortcuts.category.quickSwitch"),
shortcuts: [
{
label: t("agentManager.shortcuts.jumpToItem"),
binding: (() => {
const first = bindings.jumpTo1 ?? ""
const prefix = first.replace(/\d+$/, "")
return prefix ? `${prefix}1-9` : ""
})(),
},
],
},
{
title: t("agentManager.shortcuts.category.sidebar"),
shortcuts: [
{ label: t("agentManager.shortcuts.previousItem"), binding: bindings.previousSession ?? "" },
{ label: t("agentManager.shortcuts.nextItem"), binding: bindings.nextSession ?? "" },
{ label: t("agentManager.shortcuts.newWorktree"), binding: bindings.newWorktree ?? "" },
{ label: t("agentManager.shortcuts.advancedWorktree"), binding: bindings.advancedWorktree ?? "" },
{ label: t("agentManager.shortcuts.deleteWorktree"), binding: bindings.closeWorktree ?? "" },
{ label: t("agentManager.shortcuts.openWorktree"), binding: bindings.openWorktree ?? "" },
],
},
{
title: t("agentManager.shortcuts.category.tabs"),
shortcuts: [
{ label: t("agentManager.shortcuts.previousTab"), binding: bindings.previousTab ?? "" },
{ label: t("agentManager.shortcuts.nextTab"), binding: bindings.nextTab ?? "" },
{ label: t("agentManager.shortcuts.newTab"), binding: bindings.newTab ?? "" },
{ label: t("agentManager.shortcuts.closeTab"), binding: bindings.closeTab ?? "" },
],
},
{
title: t("agentManager.shortcuts.category.terminal"),
shortcuts: [
{ label: t("agentManager.shortcuts.toggleTerminal"), binding: bindings.showTerminal ?? "" },
{ label: t("agentManager.shortcuts.runScript"), binding: bindings.runScript ?? "" },
{ label: t("agentManager.shortcuts.toggleDiff"), binding: bindings.toggleDiff ?? "" },
],
},
{
title: t("agentManager.shortcuts.category.global"),
shortcuts: [
{ label: t("agentManager.shortcuts.openAgentManager"), binding: bindings.agentManagerOpen ?? "" },
{ label: t("agentManager.shortcuts.cycleAgentMode"), binding: bindings.cycleAgentMode ?? "" },
{ label: t("agentManager.shortcuts.cyclePreviousAgentMode"), binding: bindings.cyclePreviousAgentMode ?? "" },
{ label: t("agentManager.shortcuts.showShortcuts"), binding: bindings.showShortcuts ?? "" },
].filter((s) => s.binding),
},
].filter((c) => c.shortcuts.length > 0)
}
@@ -21,7 +21,10 @@ import { useData } from "@kilocode/kilo-ui/context/data"
import { useSession } from "../../context/session"
import { useDisplay } from "../../context/display"
import { useConfig } from "../../context/config"
import { useLanguage } from "../../context/language"
import { useServer } from "../../context/server"
import { snapshotProgress } from "../../context/session-utils"
import { planDisplayPath } from "../../utils/plan-path"
import { QuestionDock } from "./QuestionDock"
import { SuggestBar } from "./SuggestBar"
@@ -30,6 +33,50 @@ import { SuggestBar } from "./SuggestBar"
// so the user can see what the AI set up.
export const UPSTREAM_SUPPRESSED_TOOLS = new Set(["todowrite", "todoread"])
/** Extract plan path from a completed plan_exit tool part. */
function planExitInfo(part: SDKPart): { plan: string } | undefined {
if (part.type !== "tool") return undefined
const tp = part as unknown as ToolPart
if (tp.tool !== "plan_exit") return undefined
if (tp.state?.status !== "completed") return undefined
const meta = (tp.state as { metadata?: Record<string, unknown> }).metadata ?? {}
const plan = typeof meta.plan === "string" ? meta.plan : undefined
if (!plan) return undefined
return { plan }
}
function PlanExitCard(props: { part: ToolPart }) {
const language = useLanguage()
const server = useServer()
const data = useData()
const info = createMemo(() => planExitInfo(props.part as unknown as SDKPart))
const display = createMemo(() => {
const i = info()
if (!i) return ""
return planDisplayPath(i.plan, server.workspaceDirectory())
})
const label = createMemo(() => {
if (!info()) return ""
return language.t("plan.exit.ready")
})
const open = (e: MouseEvent) => {
e.preventDefault()
const i = info()
if (!i || !data.openFile) return
data.openFile(i.plan)
}
return (
<Show when={info()}>
<div data-component="plan-exit-card">
<span data-slot="plan-exit-label">{label()}</span>{" "}
<a data-slot="plan-exit-link" href="#" onClick={open}>
{display()}
</a>
</div>
</Show>
)
}
function isRenderable(part: SDKPart): boolean {
if (part.type === "tool") {
const tool = (part as SDKPart & { tool: string }).tool
@@ -156,10 +203,24 @@ export const AssistantMessage: Component<AssistantMessageProps> = (props) => {
if (tool.state?.status === "error") return
return part
})
const planExit = createMemo(() => {
if (part.type !== "tool") return
const tp = part as unknown as ToolPart
if (tp.tool !== "plan_exit") return
if (tp.state?.status !== "completed") return
return tp
})
return (
<Show
when={isUpstreamSuppressed || activeQuestion() || activeSuggestion() || bash() || PART_MAPPING[part.type]}
when={
isUpstreamSuppressed ||
activeQuestion() ||
activeSuggestion() ||
bash() ||
planExit() ||
PART_MAPPING[part.type]
}
>
<div data-component="tool-part-wrapper" data-part-type={part.type}>
<Show
@@ -169,30 +230,37 @@ export const AssistantMessage: Component<AssistantMessageProps> = (props) => {
when={activeSuggestion()}
fallback={
<Show
when={bash()}
when={planExit()}
fallback={
<Show
when={isUpstreamSuppressed}
when={bash()}
fallback={
<Part
part={part}
message={props.message as SDKMessage}
showAssistantCopyPartID={props.showAssistantCopyPartID}
reasoningAutoCollapse={display.reasoningAutoCollapse()}
feedback={props.feedback}
animate={
part.type === "tool" &&
((part as unknown as ToolPart).state?.status === "pending" ||
(part as unknown as ToolPart).state?.status === "running")
<Show
when={isUpstreamSuppressed}
fallback={
<Part
part={part}
message={props.message as SDKMessage}
showAssistantCopyPartID={props.showAssistantCopyPartID}
reasoningAutoCollapse={display.reasoningAutoCollapse()}
feedback={props.feedback}
animate={
part.type === "tool" &&
((part as unknown as ToolPart).state?.status === "pending" ||
(part as unknown as ToolPart).state?.status === "running")
}
/>
}
/>
>
<TodoToolCard part={part as unknown as ToolPart} />
</Show>
}
>
<TodoToolCard part={part as unknown as ToolPart} />
{(tool) => <BashToolCard part={tool() as unknown as ToolPart} defaultOpen={open()} />}
</Show>
}
>
{(tool) => <BashToolCard part={tool() as unknown as ToolPart} defaultOpen={open()} />}
{(tp) => <PlanExitCard part={tp()} />}
</Show>
}
>
@@ -329,14 +329,30 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
window.addEventListener("compactSession", onCompact)
onCleanup(() => window.removeEventListener("compactSession", onCompact))
const onExport = () => {
const id = session.currentSessionID()
if (id) session.exportSessionTranscript(id)
}
window.addEventListener("exportSessionTranscript", onExport)
onCleanup(() => window.removeEventListener("exportSessionTranscript", onExport))
const isBusy = () => isPromptBusy(session.status(), !!props.suggesting?.(), !!props.questioning?.())
const isDisabled = () => !server.isConnected()
const canUseSpeech = () => canUseSpeechToText(settings(), config(), provider.connected(), server.profileData())
const speechModel = () => selectedSpeechToTextModel(settings())
const hasInput = () => text().trim().length > 0 || imageAttach.images().length > 0 || reviewComments().length > 0
const canSend = () =>
hasInput() && !isDisabled() && !speech.active() && !terminal.pending() && !git.pending() && !props.blocked?.()
const showStop = () => isBusy() && !hasInput()
!isDisabled() &&
!terminal.pending() &&
!git.pending() &&
!props.blocked?.() &&
(speech.state() === "recording" || (hasInput() && !speech.active()))
const sendLabel = () => {
if (props.blocked?.()) return language.t("prompt.action.send.blocked")
if (speech.state() === "recording") return language.t("prompt.action.send.recording")
return language.t("prompt.action.send")
}
const showStop = () => isBusy() && !hasInput() && speech.state() !== "recording"
const isAtEnd = () =>
textareaRef ? atEnd(textareaRef.selectionStart, textareaRef.selectionEnd, textareaRef.value.length) : false
const highlightMentions = () => {
@@ -675,6 +691,33 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
speech.start({ model: speechModel(), insert: insertSpeechText })
}
const transcribeAndSend = () => {
const key = draftKey()
const id = sid()
const context = ctx()
const value = text()
const comments = reviewComments()
const images = imageAttach.images()
speech.stop({
done: () => void handleSend(),
ready: () =>
draftKey() === key &&
sid() === id &&
ctx() === context &&
text() === value &&
reviewComments() === comments &&
imageAttach.images() === images,
})
}
const handleSendClick = () => {
if (speech.state() !== "recording" || !canSend()) {
void handleSend()
return
}
transcribeAndSend()
}
const handleSend = async () => {
const draft = text().trim()
@@ -721,6 +764,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const pendingId = props.pendingSessionID ?? session.draftSessionID()
const id = sid()
const sel = session.selected(id)
const context = ctx()
const key = draftKey()
const terminalFile = await terminal.resolveAttachment(message, id).catch((err: Error) => {
showToast({ variant: "error", title: "Terminal context unavailable", description: err.message })
@@ -742,16 +787,20 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
]
const attachments = allFiles.length > 0 ? allFiles : undefined
const key = draftKey()
// Server-side slash command (cmdMatch/matched already computed above)
if (matched) {
const rest = draft.slice(cmdMatch![0].length).trim()
const args = review && rest ? `${review}\n\n${rest}` : rest || review
session.sendCommand(matched.name, args, sel?.providerID, sel?.modelID, attachments, pendingId, ctx())
session.sendCommand(matched.name, args, sel?.providerID, sel?.modelID, attachments, pendingId, context)
} else {
session.sendMessage(message, sel?.providerID, sel?.modelID, attachments, pendingId, ctx())
session.sendMessage(message, sel?.providerID, sel?.modelID, attachments, pendingId, context)
}
drafts.delete(key)
reviewDrafts.delete(key)
imageDrafts.delete(key)
if (draftKey() !== key) return
history.append(draft)
history.reset()
setText("")
@@ -759,9 +808,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
imageAttach.clear()
mention.closeMention()
slash.close()
drafts.delete(key)
reviewDrafts.delete(key)
imageDrafts.delete(key)
if (textareaRef) textareaRef.style.height = "auto"
}
@@ -1085,16 +1131,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<Show
when={showStop()}
fallback={
<Tooltip
value={props.blocked?.() ? language.t("prompt.action.send.blocked") : language.t("prompt.action.send")}
placement="top"
>
<Tooltip value={sendLabel()} placement="top">
<Button
variant="ghost"
size="small"
onClick={() => void handleSend()}
onClick={handleSendClick}
aria-disabled={!canSend()}
aria-label={language.t("prompt.action.send")}
aria-label={sendLabel()}
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M1.5 1.5L14.5 8L1.5 14.5V9L10 8L1.5 7V1.5Z" />
@@ -119,6 +119,9 @@ const SessionList: Component<SessionListProps> = (props) => {
<ContextMenu.Item onSelect={() => startRename(item)}>
<ContextMenu.ItemLabel>{language.t("common.rename")}</ContextMenu.ItemLabel>
</ContextMenu.Item>
<ContextMenu.Item onSelect={() => session.exportSessionTranscript(item.id)}>
<ContextMenu.ItemLabel>{language.t("command.session.export")}</ContextMenu.ItemLabel>
</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.Item onSelect={() => confirmDelete(item)}>
<ContextMenu.ItemLabel>{language.t("common.delete")}</ContextMenu.ItemLabel>
@@ -26,12 +26,17 @@ type StartOptions = {
insert: InsertTranscript
}
type StopOptions = {
done?: () => void
ready?: () => boolean
}
export type SpeechToText = {
state: Accessor<SpeechState>
error: Accessor<string | undefined>
active: Accessor<boolean>
start: (opts: StartOptions) => void
stop: () => void
stop: (opts?: StopOptions) => void
cancel: () => void
clear: () => void
}
@@ -45,6 +50,8 @@ export function useSpeechToText(vscode: VSCode, server: Server, lang: Lang): Spe
let request = ""
let counter = 0
let insert: InsertTranscript | undefined
let done: (() => void) | undefined
let ready: (() => boolean) | undefined
const unsub = vscode.onMessage((msg) => {
if (!isSpeechMessage(msg)) return
@@ -73,10 +80,12 @@ export function useSpeechToText(vscode: VSCode, server: Server, lang: Lang): Spe
return
}
const next = ready?.() === false ? undefined : done
insert?.(text)
cleanup()
setState("idle")
setError(undefined)
next?.()
})
onCleanup(() => {
@@ -113,8 +122,10 @@ export function useSpeechToText(vscode: VSCode, server: Server, lang: Lang): Spe
})
}
function stop() {
function stop(opts?: StopOptions) {
if (state() !== "recording") return
done = opts?.done
ready = opts?.ready
setState("transcribing")
vscode.postMessage({ type: "speechToTextStop", requestId: request })
}
@@ -143,6 +154,8 @@ export function useSpeechToText(vscode: VSCode, server: Server, lang: Lang): Spe
function cleanup() {
request = ""
insert = undefined
done = undefined
ready = undefined
}
return { state, error, active, start, stop, cancel, clear }
@@ -248,6 +248,7 @@ interface SessionContextValue {
selectSession: (id: string) => void
deleteSession: (id: string) => void
renameSession: (id: string, title: string) => void
exportSessionTranscript: (id: string) => void
syncSession: (sessionID: string) => void
// Cloud session preview
@@ -2141,6 +2142,18 @@ export const SessionProvider: ParentComponent = (props) => {
vscode.postMessage({ type: "renameSession", sessionID: id, title })
}
function exportSessionTranscript(id: string) {
if (!server.isConnected()) {
console.warn("[Kilo New] Cannot export session transcript: not connected")
return
}
if (id.startsWith("cloud:")) {
console.warn("[Kilo New] Cannot export cloud session transcript")
return
}
vscode.postMessage({ type: "exportSessionTranscript", sessionID: id })
}
// Computed values
const currentSession = () => {
const id = currentSessionID()
@@ -2429,6 +2442,7 @@ export const SessionProvider: ParentComponent = (props) => {
selectSession,
deleteSession,
renameSession,
exportSessionTranscript,
syncSession,
cloudPreviewId,
selectCloudSession,
@@ -99,6 +99,14 @@ export function useSlashCommand(vscode: VSCodeContext, exclude?: Set<string> | A
window.dispatchEvent(new CustomEvent("compactSession"))
},
},
{
name: "export",
description: "Export the current session transcript as Markdown",
hints: ["markdown", "transcript"],
action: () => {
window.dispatchEvent(new CustomEvent("exportSessionTranscript"))
},
},
{
name: "settings",
description: "Open settings",
+3
View File
@@ -92,6 +92,7 @@ export const dict = {
"command.session.share.description": "مشاركة هذه الجلسة ونسخ الرابط إلى الحافظة",
"command.session.unshare": "إلغاء مشاركة الجلسة",
"command.session.unshare.description": "إيقاف مشاركة هذه الجلسة",
"command.session.export": "تصدير سجل الجلسة",
"palette.search.placeholder": "البحث في الملفات والأوامر والجلسات",
"palette.empty": "لا توجد نتائج",
@@ -268,6 +269,7 @@ export const dict = {
"prompt.attachment.remove": "إزالة المرفق",
"prompt.action.send": "إرسال",
"prompt.action.send.blocked": "أجب عن السؤال المعلق أو تجاهله أولاً",
"prompt.action.send.recording": "تفريغ وإرسال",
"prompt.action.stop": "توقف",
"prompt.action.enhance": "تحسين النص",
"prompt.action.autoApprove.enable": "تفعيل الموافقة التلقائية",
@@ -1584,4 +1586,5 @@ export const dict = {
"diffViewer.baseBranch.empty": "لا توجد فروع مطابقة",
"diffViewer.baseBranch.loading": "جارٍ تحميل الفروع…",
"diffViewer.baseBranch.none": "—",
"plan.exit.ready": "الخطة جاهزة:",
}
+3
View File
@@ -92,6 +92,7 @@ export const dict = {
"command.session.share.description": "Compartilhar esta sessão e copiar a URL para a área de transferência",
"command.session.unshare": "Parar de compartilhar sessão",
"command.session.unshare.description": "Parar de compartilhar esta sessão",
"command.session.export": "Exportar transcrição da sessão",
"palette.search.placeholder": "Buscar arquivos, comandos e sessões",
"palette.empty": "Nenhum resultado encontrado",
@@ -269,6 +270,7 @@ export const dict = {
"prompt.attachment.remove": "Remover anexo",
"prompt.action.send": "Enviar",
"prompt.action.send.blocked": "Responda ou feche a pergunta pendente primeiro",
"prompt.action.send.recording": "Transcrever e enviar",
"prompt.action.stop": "Parar",
"prompt.action.enhance": "Melhorar prompt",
"prompt.action.autoApprove.enable": "Ativar aprovação automática",
@@ -1627,4 +1629,5 @@ export const dict = {
"diffViewer.baseBranch.empty": "Nenhum branch correspondente",
"diffViewer.baseBranch.loading": "Carregando branches…",
"diffViewer.baseBranch.none": "—",
"plan.exit.ready": "Plano pronto:",
}
+3
View File
@@ -92,6 +92,7 @@ export const dict = {
"command.session.share.description": "Podijeli ovu sesiju i kopiraj URL u međuspremnik",
"command.session.unshare": "Ukini dijeljenje sesije",
"command.session.unshare.description": "Zaustavi dijeljenje ove sesije",
"command.session.export": "Izvezi transkript sesije",
"palette.search.placeholder": "Pretraži datoteke, komande i sesije",
"palette.empty": "Nema rezultata",
@@ -270,6 +271,7 @@ export const dict = {
"prompt.attachment.remove": "Ukloni prilog",
"prompt.action.send": "Pošalji",
"prompt.action.send.blocked": "Prvo odgovorite ili odbacite pitanje na čekanju",
"prompt.action.send.recording": "Transkribuj i pošalji",
"prompt.action.stop": "Zaustavi",
"prompt.action.enhance": "Poboljšaj prompt",
"prompt.action.autoApprove.enable": "Uključi automatsko odobravanje",
@@ -1623,4 +1625,5 @@ export const dict = {
"diffViewer.baseBranch.empty": "No matching branches",
"diffViewer.baseBranch.loading": "Loading branches…",
"diffViewer.baseBranch.none": "—",
"plan.exit.ready": "Plan je spreman:",
}
+3
View File
@@ -92,6 +92,7 @@ export const dict = {
"command.session.share.description": "Del denne session og kopier URL'en til udklipsholderen",
"command.session.unshare": "Stop deling af session",
"command.session.unshare.description": "Stop med at dele denne session",
"command.session.export": "Eksporter sessionsudskrift",
"palette.search.placeholder": "Søg i filer, kommandoer og sessioner",
"palette.empty": "Ingen resultater fundet",
@@ -269,6 +270,7 @@ export const dict = {
"prompt.attachment.remove": "Fjern vedhæftning",
"prompt.action.send": "Send",
"prompt.action.send.blocked": "Besvar eller afvis det afventende spørgsmål først",
"prompt.action.send.recording": "Transskriber og send",
"prompt.action.stop": "Stop",
"prompt.action.enhance": "Forbedr prompt",
"prompt.action.autoApprove.enable": "Aktiver automatisk godkendelse",
@@ -1612,4 +1614,5 @@ export const dict = {
"diffViewer.baseBranch.empty": "No matching branches",
"diffViewer.baseBranch.loading": "Loading branches…",
"diffViewer.baseBranch.none": "—",
"plan.exit.ready": "Planen er klar:",
}
+3
View File
@@ -96,6 +96,7 @@ export const dict = {
"command.session.share.description": "Diese Sitzung teilen und URL in die Zwischenablage kopieren",
"command.session.unshare": "Teilen der Sitzung aufheben",
"command.session.unshare.description": "Teilen dieser Sitzung beenden",
"command.session.export": "Sitzungsprotokoll exportieren",
"palette.search.placeholder": "Dateien, Befehle und Sitzungen durchsuchen",
"palette.empty": "Keine Ergebnisse gefunden",
@@ -273,6 +274,7 @@ export const dict = {
"prompt.attachment.remove": "Anhang entfernen",
"prompt.action.send": "Senden",
"prompt.action.send.blocked": "Beantworten oder verwerfen Sie zuerst die ausstehende Frage",
"prompt.action.send.recording": "Transkribieren und senden",
"prompt.action.stop": "Stopp",
"prompt.action.enhance": "Prompt verbessern",
"prompt.action.autoApprove.enable": "Automatische Genehmigung aktivieren",
@@ -1643,4 +1645,5 @@ export const dict = {
"diffViewer.baseBranch.empty": "Keine passenden Branches",
"diffViewer.baseBranch.loading": "Branches werden geladen…",
"diffViewer.baseBranch.none": "—",
"plan.exit.ready": "Plan ist bereit:",
} satisfies Partial<Record<Keys, string>>
@@ -92,6 +92,7 @@ export const dict = {
"command.session.share.description": "Share this session and copy the URL to clipboard",
"command.session.unshare": "Unshare session",
"command.session.unshare.description": "Stop sharing this session",
"command.session.export": "Export session transcript",
"palette.search.placeholder": "Search files, commands, and sessions",
"palette.empty": "No results found",
@@ -270,6 +271,7 @@ export const dict = {
"prompt.attachment.remove": "Remove attachment",
"prompt.action.send": "Send",
"prompt.action.send.blocked": "Answer or dismiss the pending question first",
"prompt.action.send.recording": "Transcribe and send",
"prompt.action.stop": "Stop",
"prompt.action.enhance": "Enhance prompt",
"prompt.action.indexing": "Indexing settings",
@@ -1612,4 +1614,6 @@ export const dict = {
"diffViewer.baseBranch.empty": "No matching branches",
"diffViewer.baseBranch.loading": "Loading branches…",
"diffViewer.baseBranch.none": "—",
"plan.exit.ready": "Plan is ready:",
}
+3
View File
@@ -92,6 +92,7 @@ export const dict = {
"command.session.share.description": "Compartir esta sesión y copiar la URL al portapapeles",
"command.session.unshare": "Dejar de compartir sesión",
"command.session.unshare.description": "Dejar de compartir esta sesión",
"command.session.export": "Exportar transcripción de la sesión",
"palette.search.placeholder": "Buscar archivos, comandos y sesiones",
"palette.empty": "No se encontraron resultados",
@@ -270,6 +271,7 @@ export const dict = {
"prompt.attachment.remove": "Eliminar adjunto",
"prompt.action.send": "Enviar",
"prompt.action.send.blocked": "Responda o descarte la pregunta pendiente primero",
"prompt.action.send.recording": "Transcribir y enviar",
"prompt.action.stop": "Detener",
"prompt.action.enhance": "Mejorar prompt",
"prompt.action.autoApprove.enable": "Activar aprobación automática",
@@ -1633,4 +1635,5 @@ export const dict = {
"diffViewer.baseBranch.empty": "Sin ramas que coincidan",
"diffViewer.baseBranch.loading": "Cargando ramas…",
"diffViewer.baseBranch.none": "—",
"plan.exit.ready": "El plan está listo:",
}
+3
View File
@@ -93,6 +93,7 @@ export const dict = {
"command.session.share.description": "Partager cette session et copier l'URL dans le presse-papiers",
"command.session.unshare": "Ne plus partager la session",
"command.session.unshare.description": "Arrêter de partager cette session",
"command.session.export": "Exporter la transcription de la session",
"palette.search.placeholder": "Rechercher des fichiers, des commandes et des sessions",
"palette.empty": "Aucun résultat trouvé",
@@ -271,6 +272,7 @@ export const dict = {
"prompt.attachment.remove": "Supprimer la pièce jointe",
"prompt.action.send": "Envoyer",
"prompt.action.send.blocked": "Répondez ou rejetez d'abord la question en attente",
"prompt.action.send.recording": "Transcrire et envoyer",
"prompt.action.stop": "Arrêter",
"prompt.action.enhance": "Améliorer le prompt",
"prompt.action.autoApprove.enable": "Activer l'approbation automatique",
@@ -1652,4 +1654,5 @@ export const dict = {
"diffViewer.baseBranch.empty": "Aucune branche correspondante",
"diffViewer.baseBranch.loading": "Chargement des branches…",
"diffViewer.baseBranch.none": "—",
"plan.exit.ready": "Le plan est prêt :",
}
+3
View File
@@ -92,6 +92,7 @@ export const dict = {
"command.session.share.description": "このセッションを共有しURLをクリップボードにコピー",
"command.session.unshare": "セッションの共有を停止",
"command.session.unshare.description": "このセッションの共有を停止",
"command.session.export": "セッション記録をエクスポート",
"palette.search.placeholder": "ファイル、コマンド、セッションを検索",
"palette.empty": "結果が見つかりません",
@@ -268,6 +269,7 @@ export const dict = {
"prompt.attachment.remove": "添付ファイルを削除",
"prompt.action.send": "送信",
"prompt.action.send.blocked": "最初に保留中の質問に答えるか、閉じてください",
"prompt.action.send.recording": "文字起こしして送信",
"prompt.action.stop": "停止",
"prompt.action.enhance": "プロンプトを改善",
"prompt.action.autoApprove.enable": "自動承認を有効化",
@@ -1606,4 +1608,5 @@ export const dict = {
"diffViewer.baseBranch.empty": "一致するブランチがありません",
"diffViewer.baseBranch.loading": "ブランチを読み込み中…",
"diffViewer.baseBranch.none": "—",
"plan.exit.ready": "プランの準備ができました:",
}
+3
View File
@@ -96,6 +96,7 @@ export const dict = {
"command.session.share.description": "이 세션을 공유하고 URL을 클립보드에 복사",
"command.session.unshare": "세션 공유 중지",
"command.session.unshare.description": "이 세션 공유 중지",
"command.session.export": "세션 기록 내보내기",
"palette.search.placeholder": "파일, 명령어 및 세션 검색",
"palette.empty": "결과 없음",
@@ -271,6 +272,7 @@ export const dict = {
"prompt.attachment.remove": "첨부 파일 제거",
"prompt.action.send": "전송",
"prompt.action.send.blocked": "먼저 대기 중인 질문에 답하거나 닫아주세요",
"prompt.action.send.recording": "텍스트 변환 및 전송",
"prompt.action.stop": "중지",
"prompt.action.enhance": "프롬프트 개선",
"prompt.action.autoApprove.enable": "자동 승인 사용",
@@ -1591,4 +1593,5 @@ export const dict = {
"diffViewer.baseBranch.empty": "일치하는 브랜치 없음",
"diffViewer.baseBranch.loading": "브랜치 로딩 중…",
"diffViewer.baseBranch.none": "—",
"plan.exit.ready": "계획이 준비되었습니다:",
}

Some files were not shown because too many files have changed in this diff Show More