mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-29 03:44:06 +08:00
Merge branch 'main' into unleashed-romano
This commit is contained in:
@@ -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.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Keep VS Code session timeline, token, context, and cost stats in sync after restoring a checkpoint.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Capture aggregate usage telemetry for experimental Morph-backed codebase search.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Show ChatGPT sign-in again when Codex authentication expires.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Keep continued Agent Manager sessions in the selected worktree instead of moving them to Local.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Hide Task tool subagent sessions from Agent Manager session lists.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Support speech-to-text voice input in Agent Manager inline review comments.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Keep inline review voice capture stable in rendered Markdown diffs.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Keep the New Worktree model picker open when interacting with its search field.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
"@kilocode/kilo-ui": patch
|
||||
---
|
||||
|
||||
Keep chat tool cards open or closed when streaming updates remount them.
|
||||
@@ -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.).
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Reset the To-dos tab when reverting a session snapshot.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Export full VS Code session transcripts as Markdown files.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Send recorded voice prompts directly when the prompt send button is pressed.
|
||||
@@ -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.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Keep configured mode models in sync when switching modes in VS Code.
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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,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",
|
||||
|
||||
@@ -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,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,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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -100,6 +100,7 @@ export {
|
||||
MODELS_FETCH_TIMEOUT_MS,
|
||||
HEADER_ORGANIZATIONID,
|
||||
HEADER_TASKID,
|
||||
HEADER_PARENT_TASKID,
|
||||
HEADER_PROJECTID,
|
||||
HEADER_TESTER,
|
||||
HEADER_EDITORNAME,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,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,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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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,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,6 +1,6 @@
|
||||
{
|
||||
"name": "@kilocode/kilo-ui",
|
||||
"version": "7.2.52",
|
||||
"version": "7.3.0",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
Reference in New Issue
Block a user