Merge branch 'main' into nvidia-nim-origin-header

This commit is contained in:
Joshua Lambert
2026-05-15 16:56:04 -04:00
committed by GitHub
168 changed files with 3683 additions and 713 deletions
@@ -1,5 +0,0 @@
---
"kilo-code": minor
---
Add a collapsible sidebar in the Agent Manager. The toggle button sits left of the tab title and the collapsed state persists across reloads and restarts. Starting a new session or worktree automatically reopens the sidebar so the new entry is visible.
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Keep long New Worktree prompts clear of selector controls and show inline model picker options without clipping.
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Keep Agent Manager state hidden from Git before creating local sessions.
-5
View File
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Keep VS Code session timeline, token, context, and cost stats in sync after restoring a checkpoint.
+5
View File
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---
Capture aggregate usage telemetry for experimental Morph-backed codebase search.
-6
View File
@@ -1,6 +0,0 @@
---
"kilo-code": patch
"@kilocode/cli": patch
---
Show ChatGPT sign-in again when Codex authentication expires.
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Keep continued Agent Manager sessions in the selected worktree instead of moving them to Local.
-5
View File
@@ -1,5 +0,0 @@
---
"@kilocode/kilo-ui": patch
---
Show the assistant message copy tooltip above the button so it no longer overlaps the thumbs up/down buttons.
-6
View File
@@ -1,6 +0,0 @@
---
"@kilocode/cli": patch
"kilo-code": patch
---
Fix TUI diff rendering when header-like content lines appear inside a unified diff hunk.
-5
View File
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Ensure snapshot installs package a fresh CLI backend so new extension features are available in local VSIX builds.
-5
View File
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Hide Task tool subagent sessions from Agent Manager session lists.
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": minor
---
Support speech-to-text voice input in Agent Manager inline review comments.
-5
View File
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Move worktree file revert diff status checks into the VS Code extension host so Kilo no longer asks the CLI server to run git diff commands through Bun during workspace and Agent Manager reverts.
-7
View File
@@ -1,7 +0,0 @@
---
"@kilocode/cli": minor
"@kilocode/sdk": minor
"kilo-code": minor
---
Support setting an auto-compaction threshold percentage so long sessions can compact before the context window is full.
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Keep inline review voice capture stable in rendered Markdown diffs.
+6
View File
@@ -0,0 +1,6 @@
---
"@kilocode/cli": patch
"@kilocode/kilo-docs": patch
---
Harden Mermaid diagram rendering with upstream security fixes.
-5
View File
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Keep the New Worktree model picker open when interacting with its search field.
-6
View File
@@ -1,6 +0,0 @@
---
"kilo-code": patch
"@kilocode/kilo-ui": patch
---
Keep chat tool cards open or closed when streaming updates remount them.
-5
View File
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Use the VS Code-native placeholder color for input fields so placeholder text is consistently distinct from values you have typed (Settings, Agent Manager, prompt input, model search, etc.).
-5
View File
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Reset the To-dos tab when reverting a session snapshot.
-5
View File
@@ -1,5 +0,0 @@
---
"kilo-code": minor
---
Add syntax highlighting to shell command output with Shiki, labeled "Command" and "Output" sections, per-section copy buttons, and an "Open in Editor" action that opens the full untruncated output in a VS Code editor tab.
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": minor
---
Export full VS Code session transcripts as Markdown files.
-6
View File
@@ -1,6 +0,0 @@
---
"kilo-code": patch
"@kilocode/cli": patch
---
Fix Mermaid diagrams rendering with empty text inside every shape by restoring the `foreignObject` HTML integration point that DOMPurify dropped in 3.1.7.
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Restore only the Kilo Code sidebar, Agent Manager, or both on VS Code restart based on which views were open before shutdown.
-6
View File
@@ -1,6 +0,0 @@
---
"kilo-code": minor
"@kilocode/kilo-gateway": minor
---
Support experimental speech-to-text voice input in VS Code prompt fields through Kilo Gateway.
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Send recorded voice prompts directly when the prompt send button is pressed.
-6
View File
@@ -1,6 +0,0 @@
---
"@kilocode/cli": patch
"@kilocode/kilo-gateway": patch
---
Prevent Kilo Gateway Responses requests from replaying transient provider item IDs when request storage is disabled.
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Support viewing Agent Manager terminal tabs alongside the right diff sidebar, and send review comments to the active terminal.
-5
View File
@@ -1,5 +0,0 @@
---
"kilo-code": patch
---
Keep configured mode models in sync when switching modes in VS Code.
+167
View File
@@ -0,0 +1,167 @@
# kilocode_change - new file
name: publish-jetbrains
on:
push:
tags:
- "jetbrains/*"
concurrency:
group: publish-jetbrains-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
publish:
if: github.repository == 'Kilo-Org/kilocode'
runs-on: blacksmith-8vcpu-ubuntu-2404
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "24"
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Install build tools
run: |
sudo apt-get update
sudo apt-get install -y patchelf zip
- name: Validate version tag
id: version
run: |
tag="$GITHUB_REF_NAME"
if [[ "$tag" != jetbrains/v* ]]; then
echo "Unsupported tag '$tag'. Expected jetbrains/v<version>." >&2
exit 1
fi
version="${tag#jetbrains/v}"
if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then
{
echo "version=$version"
echo "kind=rc"
echo "marketplace_channel=eap"
echo "cli_channel=rc"
} >> "$GITHUB_OUTPUT"
exit 0
fi
if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
{
echo "version=$version"
echo "kind=stable"
echo "marketplace_channel=default"
echo "cli_channel=latest"
} >> "$GITHUB_OUTPUT"
echo "Stable JetBrains Marketplace publishing is implemented but intentionally disabled; use an rc tag such as jetbrains/v7.0.1-rc.1." >&2
exit 1
fi
echo "Unsupported JetBrains plugin version '$version'. Expected jetbrains/vx.y.z-rc.n or jetbrains/vx.y.z." >&2
exit 1
- name: Validate publishing secrets
run: |
missing=0
for name in JETBRAINS_MARKETPLACE_TOKEN JETBRAINS_CERTIFICATE_CHAIN JETBRAINS_PRIVATE_KEY JETBRAINS_PRIVATE_KEY_PASSWORD; do
if [[ -z "${!name}" ]]; then
echo "Missing required secret: $name" >&2
missing=1
fi
done
exit "$missing"
env:
JETBRAINS_MARKETPLACE_TOKEN: ${{ secrets.JETBRAINS_MARKETPLACE_TOKEN }}
JETBRAINS_CERTIFICATE_CHAIN: ${{ secrets.JETBRAINS_CERTIFICATE_CHAIN }}
JETBRAINS_PRIVATE_KEY: ${{ secrets.JETBRAINS_PRIVATE_KEY }}
JETBRAINS_PRIVATE_KEY_PASSWORD: ${{ secrets.JETBRAINS_PRIVATE_KEY_PASSWORD }}
- name: Prepare CLI resources
working-directory: packages/kilo-jetbrains
run: bun script/build.ts --production --prepare-cli
env:
KILO_VERSION: ${{ steps.version.outputs.version }}
KILO_CHANNEL: ${{ steps.version.outputs.cli_channel }}
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
- name: Verify plugin
working-directory: packages/kilo-jetbrains
run: ./gradlew verifyPlugin -Pproduction=true -Pkilo.channel="$CHANNEL"
env:
CHANNEL: ${{ steps.version.outputs.marketplace_channel }}
- name: Publish to JetBrains Marketplace
working-directory: packages/kilo-jetbrains
run: ./gradlew publishPlugin -Pproduction=true -Pkilo.channel="$CHANNEL"
env:
CHANNEL: ${{ steps.version.outputs.marketplace_channel }}
JETBRAINS_MARKETPLACE_TOKEN: ${{ secrets.JETBRAINS_MARKETPLACE_TOKEN }}
JETBRAINS_CERTIFICATE_CHAIN: ${{ secrets.JETBRAINS_CERTIFICATE_CHAIN }}
JETBRAINS_PRIVATE_KEY: ${{ secrets.JETBRAINS_PRIVATE_KEY }}
JETBRAINS_PRIVATE_KEY_PASSWORD: ${{ secrets.JETBRAINS_PRIVATE_KEY_PASSWORD }}
- name: Resolve plugin archive
id: archive
run: |
mapfile -t signed < <(compgen -G "packages/kilo-jetbrains/build/distributions/*-signed.zip")
if [[ "${#signed[@]}" -eq 1 ]]; then
echo "path=${signed[0]}" >> "$GITHUB_OUTPUT"
exit 0
fi
if [[ "${#signed[@]}" -gt 1 ]]; then
echo "Expected exactly one signed JetBrains plugin ZIP, found ${#signed[@]}." >&2
printf '%s\n' "${signed[@]}" >&2
exit 1
fi
mapfile -t files < <(compgen -G "packages/kilo-jetbrains/build/distributions/*.zip")
if [[ "${#files[@]}" -ne 1 ]]; then
echo "Expected exactly one JetBrains plugin ZIP, found ${#files[@]}." >&2
printf '%s\n' "${files[@]}" >&2
exit 1
fi
echo "path=${files[0]}" >> "$GITHUB_OUTPUT"
- name: Upload to GitHub Release
run: |
tag="$GITHUB_REF_NAME"
title="JetBrains $VERSION"
notes="JetBrains plugin $VERSION."
if gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
gh release upload "$tag" "$ARCHIVE" --clobber --repo "$GITHUB_REPOSITORY"
exit 0
fi
gh release create "$tag" "$ARCHIVE" --title "$title" --notes "$notes" --prerelease --repo "$GITHUB_REPOSITORY"
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ steps.version.outputs.version }}
ARCHIVE: ${{ steps.archive.outputs.path }}
- name: Upload workflow artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: kilo-jetbrains-${{ steps.version.outputs.version }}
path: packages/kilo-jetbrains/build/distributions/*.zip
if-no-files-found: ignore
+11 -1
View File
@@ -41,11 +41,21 @@ jobs:
with:
token: ${{ secrets.GITHUB_TOKEN }}
# kilocode_change start
- name: Setup Node
uses: actions/setup-node@v6 # kilocode_change
id: setup-node
continue-on-error: ${{ runner.os == 'Windows' }}
uses: actions/setup-node@v6
with:
node-version: "24"
- name: Retry Setup Node on Windows
if: runner.os == 'Windows' && steps.setup-node.outcome == 'failure'
uses: actions/setup-node@v6
with:
node-version: "24"
# kilocode_change end
- name: Setup Bun
uses: ./.github/actions/setup-bun
+3 -1
View File
@@ -102,7 +102,9 @@ kilo run --auto "run tests and fix any failures"
We welcome contributions from developers, writers, and enthusiasts!
To get started, please read our [Contributing Guide](/CONTRIBUTING.md). It includes details on setting up your environment, coding standards, types of contribution and how to submit pull requests.
See [RELEASING.md](RELEASING.md) for the release process.
See [RELEASING.md](RELEASING.md) for the VS Code extension and CLI release process.
See [packages/kilo-jetbrains/RELEASING.md](packages/kilo-jetbrains/RELEASING.md) for the JetBrains plugin release process.
## Code of Conduct
+22 -54
View File
@@ -33,7 +33,7 @@
},
"packages/core": {
"name": "@opencode-ai/core",
"version": "7.2.52",
"version": "7.3.0",
"bin": {
"opencode": "./bin/opencode",
},
@@ -68,7 +68,7 @@
},
"packages/kilo-docs": {
"name": "@kilocode/kilo-docs",
"version": "7.2.52",
"version": "7.3.0",
"dependencies": {
"@docsearch/css": "^4",
"@docsearch/js": "^4",
@@ -77,7 +77,7 @@
"@vscode/codicons": "^0.0.44",
"@xyflow/react": "12.10.2",
"js-yaml": "^4.1.0",
"mermaid": "11.12.3",
"mermaid": "11.15.0",
"next": "^16.1.5",
"posthog-js": "^1.335.3",
"prismjs": "^1.30.0",
@@ -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:",
@@ -546,7 +546,7 @@
"marked": "catalog:",
"marked-katex-extension": "5.1.6",
"marked-shiki": "catalog:",
"mermaid": "11.14.0",
"mermaid": "11.15.0",
"morphdom": "2.7.8",
"motion": "12.34.5",
"motion-dom": "12.34.3",
@@ -968,15 +968,7 @@
"@changesets/write": ["@changesets/write@0.4.0", "", { "dependencies": { "@changesets/types": "^6.1.0", "fs-extra": "^7.0.1", "human-id": "^4.1.1", "prettier": "^2.7.1" } }, "sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q=="],
"@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@12.0.0", "", { "dependencies": { "@chevrotain/gast": "12.0.0", "@chevrotain/types": "12.0.0" } }, "sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg=="],
"@chevrotain/gast": ["@chevrotain/gast@12.0.0", "", { "dependencies": { "@chevrotain/types": "12.0.0" } }, "sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ=="],
"@chevrotain/regexp-to-ast": ["@chevrotain/regexp-to-ast@12.0.0", "", {}, "sha512-p+EW9MaJwgaHguhoqwOtx/FwuGr+DnNn857sXWOi/mClXIkPGl3rn7hGNWvo31HA3vyeQxjqe+H36yZJwYU8cA=="],
"@chevrotain/types": ["@chevrotain/types@12.0.0", "", {}, "sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA=="],
"@chevrotain/utils": ["@chevrotain/utils@12.0.0", "", {}, "sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA=="],
"@chevrotain/types": ["@chevrotain/types@11.1.2", "", {}, "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw=="],
"@clack/core": ["@clack/core@1.0.0-alpha.1", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-rFbCU83JnN7l3W1nfgCqqme4ZZvTTgsiKQ6FM0l+r0P+o2eJpExcocBUWUIwnDzL76Aca9VhUdWmB2MbUv+Qyg=="],
@@ -1348,7 +1340,7 @@
"@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="],
"@mermaid-js/parser": ["@mermaid-js/parser@1.1.0", "", { "dependencies": { "langium": "^4.0.0" } }, "sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw=="],
"@mermaid-js/parser": ["@mermaid-js/parser@1.1.1", "", { "dependencies": { "@chevrotain/types": "~11.1.1" } }, "sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw=="],
"@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="],
@@ -2554,10 +2546,6 @@
"cheerio-select": ["cheerio-select@2.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", "css-what": "^6.1.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="],
"chevrotain": ["chevrotain@12.0.0", "", { "dependencies": { "@chevrotain/cst-dts-gen": "12.0.0", "@chevrotain/gast": "12.0.0", "@chevrotain/regexp-to-ast": "12.0.0", "@chevrotain/types": "12.0.0", "@chevrotain/utils": "12.0.0" } }, "sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ=="],
"chevrotain-allstar": ["chevrotain-allstar@0.4.1", "", { "dependencies": { "lodash-es": "^4.17.21" }, "peerDependencies": { "chevrotain": "^12.0.0" } }, "sha512-PvVJm3oGqrveUVW2Vt/eZGeiAIsJszYweUcYwcskg9e+IubNYKKD+rHHem7A6XVO22eDAL+inxNIGAzZ/VIWlA=="],
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
@@ -2742,7 +2730,7 @@
"d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="],
"dagre-d3-es": ["dagre-d3-es@7.0.13", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q=="],
"dagre-d3-es": ["dagre-d3-es@7.0.14", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg=="],
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
@@ -2876,6 +2864,8 @@
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
"es-toolkit": ["es-toolkit@1.46.1", "", {}, "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ=="],
"esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="],
"esbuild-plugin-solid": ["esbuild-plugin-solid@0.6.0", "", { "dependencies": { "@babel/core": "^7.20.12", "@babel/preset-typescript": "^7.18.6", "babel-preset-solid": "^1.6.9" }, "peerDependencies": { "esbuild": ">=0.20", "solid-js": ">= 1.0" } }, "sha512-V1FvDALwLDX6K0XNYM9CMRAnMzA0+Ecu55qBUT9q/eAJh1KIDsTMFoOzMSgyHqbOfvrVfO3Mws3z7TW2GVnIZA=="],
@@ -3360,8 +3350,6 @@
"kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="],
"langium": ["langium@4.2.2", "", { "dependencies": { "@chevrotain/regexp-to-ast": "~12.0.0", "chevrotain": "~12.0.0", "chevrotain-allstar": "~0.4.1", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.11", "vscode-uri": "~3.1.0" } }, "sha512-JUshTRAfHI4/MF9dH2WupvjSXyn8JBuUEWazB8ZVJUtXutT0doDlAv1XKbZ1Pb5sMexa8FF4CFBc0iiul7gbUQ=="],
"layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="],
"lazystream": ["lazystream@1.0.1", "", { "dependencies": { "readable-stream": "^2.0.5" } }, "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw=="],
@@ -3492,7 +3480,7 @@
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
"mermaid": ["mermaid@11.12.3", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.1", "@mermaid-js/parser": "^1.0.0", "@types/d3": "^7.4.3", "cytoscape": "^3.29.3", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.13", "dayjs": "^1.11.18", "dompurify": "^3.2.5", "katex": "^0.16.22", "khroma": "^2.1.0", "lodash-es": "^4.17.23", "marked": "^16.2.1", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0" } }, "sha512-wN5ZSgJQIC+CHJut9xaKWsknLxaFBwCPwPkGTSUYrTiHORWvpT8RxGk849HPnpUAQ+/9BPRqYb80jTpearrHzQ=="],
"mermaid": ["mermaid@11.15.0", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.1.1", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.33.1", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.19", "dompurify": "^3.3.1", "es-toolkit": "^1.45.1", "katex": "^0.16.25", "khroma": "^2.1.0", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" } }, "sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw=="],
"methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="],
@@ -4420,16 +4408,8 @@
"vscode-jsonrpc": ["vscode-jsonrpc@8.2.1", "", {}, "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ=="],
"vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="],
"vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.5", "", { "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg=="],
"vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="],
"vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="],
"vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="],
"walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="],
"web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="],
@@ -4736,8 +4716,6 @@
"@opencode-ai/ui/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="],
"@opencode-ai/ui/mermaid": ["mermaid@11.14.0", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.1.0", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.33.1", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.19", "dompurify": "^3.3.1", "katex": "^0.16.25", "khroma": "^2.1.0", "lodash-es": "^4.17.23", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0" } }, "sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g=="],
"@opencode-ai/ui/tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="],
"@opentelemetry/exporter-logs-otlp-http/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="],
@@ -5014,8 +4992,6 @@
"mermaid/marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="],
"mermaid/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="],
"minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
@@ -5184,8 +5160,6 @@
"vitest/why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
"vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="],
"whatwg-encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
@@ -5310,12 +5284,6 @@
"@opencode-ai/ui/@solid-primitives/resize-observer/@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-N8cIDAHbWcLahNRLr0knAAQvXyEdEMoAZvIMZKmhNb1mlx9e2UOv9BRD5YNwQUJwbNoYVhhLwFOEOcVXFx0HqA=="],
"@opencode-ai/ui/mermaid/dagre-d3-es": ["dagre-d3-es@7.0.14", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg=="],
"@opencode-ai/ui/mermaid/marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="],
"@opencode-ai/ui/mermaid/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="],
"@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="],
"@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="],
+1
View File
@@ -1,6 +1,7 @@
[install]
exact = true
minimumReleaseAge = 410520 # seconds (~4.75 days / ~114 hours)
minimumReleaseAgeExcludes = ["mermaid", "@mermaid-js/parser"]
[test]
root = "./do-not-run-tests-from-root"
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-n8qDAnmhHLO3PICGFAL6UzPi6k+Dwtl9Wg87PKqa/vY=",
"aarch64-linux": "sha256-XBCHg6BYL1J3M5JCUZda9e8ZKi8EEpI2hez6w3ICOYs=",
"aarch64-darwin": "sha256-3K35v/UghrM8CSpWvbvJVLeoF94MALWVA7F5B++Po00=",
"x86_64-darwin": "sha256-aTjNOPvGygJ34A1Q5+qziyyuSFa0w6WMUjUcsXSLtBU="
"x86_64-linux": "sha256-NZqcbv0MX1WV/BkAT1poERcDEFiA4s1Gsf7W4p5wjlk=",
"aarch64-linux": "sha256-WSl5114O35I9Fae7+BbYXWznKSApPXN/Ripjqr6NO5U=",
"aarch64-darwin": "sha256-VemzuMnNbZo7vgHS5UiHVFGPVy9VL3gVwm+cw6ij1cg=",
"x86_64-darwin": "sha256-tg2Zt8qE5EZWCVNLxKzXtaygvzAkO0IFysMfFhIfZBs="
}
}
+1 -1
View File
@@ -135,6 +135,6 @@
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch"
},
"version": "7.2.52",
"version": "7.3.0",
"peerDependencies": {}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "7.2.52",
"version": "7.3.0",
"name": "@opencode-ai/core",
"type": "module",
"license": "MIT",
+6 -6
View File
@@ -1,7 +1,7 @@
id = "kilo"
name = "Kilo"
description = "The open source coding agent."
version = "7.2.52"
version = "7.3.0"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/Kilo-Org/kilocode"
@@ -11,26 +11,26 @@ name = "Kilo"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.52/opencode-darwin-arm64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.0/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.52/opencode-darwin-x64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.0/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.52/opencode-linux-arm64.tar.gz"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.0/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.52/opencode-linux-x64.tar.gz"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.0/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.2.52/opencode-windows-x64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.0/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@kilocode/kilo-docs",
"version": "7.2.52",
"version": "7.3.0",
"private": true,
"scripts": {
"dev": "next dev --webpack --port 3002",
@@ -16,7 +16,7 @@
"@vscode/codicons": "^0.0.44",
"@xyflow/react": "12.10.2",
"js-yaml": "^4.1.0",
"mermaid": "11.12.3",
"mermaid": "11.15.0",
"next": "^16.1.5",
"posthog-js": "^1.335.3",
"prismjs": "^1.30.0",
@@ -76,6 +76,12 @@ Find the Kilo Code icon ({% kiloCodeIcon /%}) in VS Code's Primary Side Bar. Cli
The extension automatically passes context from your editor, including your open tabs and active file. You can type `@` in the chat input to get file and terminal autocomplete suggestions — use `@filename` to attach a file or `@terminal` to include your active terminal output. You can also mention file paths naturally in your message (e.g., "update src/utils.ts to add a helper function"). The agent can also discover files on its own using its built-in tools.
**Exporting local transcripts:**
Run `/export` in chat, or open a local session's **History** context menu and choose **Export session transcript**. The save dialog lets you choose the Markdown (`.md`) destination.
Kilo builds the export from the complete local session history, not only the messages currently loaded in the chat view.
{% /tab %}
{% tab label="CLI" %}
@@ -33,6 +33,7 @@ Key features include:
- **[Skills](/docs/customize/skills)** — Load specialized domain knowledge from SKILL.md files
- **[Custom Subagents](/docs/customize/custom-subagents)** — Define specialized sub-agents for the `task` tool
- **Open in Tab** — Pop the chat out into a full editor tab
- **Transcript export:** Save complete local session transcripts as Markdown files
- **Sub-Agent Viewer** — Read-only panels for viewing child agent sessions
- **Legacy Migration** — Automatic migration wizard for VSCode extension settings
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-gateway",
"version": "7.2.52",
"version": "7.3.0",
"type": "module",
"license": "MIT",
"description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration",
@@ -65,6 +65,7 @@ export const MODELS_FETCH_TIMEOUT_MS = 10 * 1000
*/
export const HEADER_ORGANIZATIONID = "X-KILOCODE-ORGANIZATIONID"
export const HEADER_TASKID = "X-KILOCODE-TASKID"
export const HEADER_PARENT_TASKID = "X-KILOCODE-PARENT-TASKID"
export const HEADER_PROJECTID = "X-KILOCODE-PROJECTID"
export const HEADER_TESTER = "X-KILOCODE-TESTER"
export const HEADER_EDITORNAME = "X-KILOCODE-EDITORNAME"
+2
View File
@@ -1,6 +1,7 @@
import {
HEADER_ORGANIZATIONID,
HEADER_TASKID,
HEADER_PARENT_TASKID,
HEADER_PROJECTID,
HEADER_TESTER,
HEADER_EDITORNAME,
@@ -21,6 +22,7 @@ import {
*/
export const X_KILOCODE_ORGANIZATIONID = HEADER_ORGANIZATIONID
export const X_KILOCODE_TASKID = HEADER_TASKID
export const X_KILOCODE_PARENT_TASKID = HEADER_PARENT_TASKID
export const X_KILOCODE_PROJECTID = HEADER_PROJECTID
export const X_KILOCODE_TESTER = HEADER_TESTER
export const X_KILOCODE_EDITORNAME = HEADER_EDITORNAME
+1
View File
@@ -100,6 +100,7 @@ export {
MODELS_FETCH_TIMEOUT_MS,
HEADER_ORGANIZATIONID,
HEADER_TASKID,
HEADER_PARENT_TASKID,
HEADER_PROJECTID,
HEADER_TESTER,
HEADER_EDITORNAME,
+2 -6
View File
@@ -5,6 +5,7 @@ import { getApiKey } from "./auth/token.js"
import { buildKiloHeaders, getDefaultHeaders } from "./headers.js"
import { ANONYMOUS_API_KEY } from "./api/constants.js"
import { resolveKiloOpenRouterBaseUrl } from "./api/url.js"
import { buildRequestHeaders } from "./provider.js"
/**
* Debug version of createKilo with extensive logging
@@ -40,12 +41,7 @@ export function createKiloDebug(options: KiloProviderOptions = {}): SDK {
console.log(" - URL:", String(input))
console.log(" - Method:", init?.method || "GET")
const headers = new Headers(init?.headers)
// Add custom headers
Object.entries(customHeaders).forEach(([key, value]) => {
headers.set(key, value)
})
const headers = buildRequestHeaders(customHeaders, init?.headers)
// Add authorization if API key exists
if (apiKey) {
+9 -6
View File
@@ -10,6 +10,14 @@ import { ANONYMOUS_API_KEY } from "./api/constants.js"
import { resolveKiloOpenRouterBaseUrl } from "./api/url.js"
import { sanitizeResponsesBody } from "./responses.js"
export function buildRequestHeaders(defaultHeaders: Record<string, string>, requestHeaders?: HeadersInit): Headers {
const headers = new Headers(defaultHeaders)
new Headers(requestHeaders).forEach((value, key) => {
headers.set(key, value)
})
return headers
}
/**
* Create a KiloCode provider instance
*
@@ -45,14 +53,9 @@ export function createKilo(options: KiloProviderOptions = {}): KiloProvider {
// Create custom fetch wrapper to add dynamic headers
const originalFetch = options.fetch ?? fetch
const wrappedFetch = async (input: string | URL | Request, init?: RequestInit) => {
const headers = new Headers(init?.headers)
const headers = buildRequestHeaders(customHeaders, init?.headers)
const body = sanitizeResponsesBody(input, init?.body)
// Add custom headers
Object.entries(customHeaders).forEach(([key, value]) => {
headers.set(key, value)
})
// Add authorization if API key exists
if (apiKey) {
headers.set("Authorization", `Bearer ${apiKey}`)
@@ -429,6 +429,7 @@ export function createKiloRoutes(deps: KiloRoutesDeps) {
format: z.string(),
}),
language: z.string().optional(),
prompt: z.string().optional(),
temperature: z.number().optional(),
}),
),
@@ -0,0 +1,23 @@
import { describe, expect, test } from "bun:test"
import { buildRequestHeaders } from "../src/provider"
describe("Kilo provider request headers", () => {
test("request headers override provider defaults", () => {
const headers = buildRequestHeaders(
{
"content-type": "application/json",
"x-kilocode-feature": "vscode-extension",
"x-default-only": "kept",
},
{
"x-kilocode-feature": "agent-manager",
"x-request-only": "kept-too",
},
)
expect(headers.get("content-type")).toBe("application/json")
expect(headers.get("x-kilocode-feature")).toBe("agent-manager")
expect(headers.get("x-default-only")).toBe("kept")
expect(headers.get("x-request-only")).toBe("kept-too")
})
})
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-i18n",
"version": "7.2.52",
"version": "7.3.0",
"type": "module",
"license": "MIT",
"description": "Kilo-specific i18n translations and overrides",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-indexing",
"version": "7.2.52",
"version": "7.3.0",
"type": "module",
"license": "MIT",
"description": "Standalone indexing engine and host helpers for Kilo Code",
+6
View File
@@ -52,6 +52,12 @@ The built plugin archive is at `build/distributions/kilo.jetbrains-<version>.zip
---
## Releasing
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.
---
## Run the plugin
Use the `runIde` Gradle task (available in the Gradle tool window or via the "Run JetBrains Plugin" run configuration) to launch a sandboxed IntelliJ instance with the plugin installed.
+44
View File
@@ -0,0 +1,44 @@
# JetBrains Release Todo
## One-Time Marketplace Setup
- Confirm `ai.kilocode.jetbrains` is the final permanent Marketplace plugin ID before the first upload.
- Build a production ZIP locally or in CI for the first Marketplace version.
- Manually upload the first plugin version in JetBrains Marketplace if the plugin has not been published before.
- Confirm the plugin is owned by the correct Kilo vendor or organization.
- Confirm the `eap` custom channel exists or is accepted on the first RC upload.
- Confirm Marketplace signing requirements before publishing RC builds.
## One-Time GitHub Setup
- Create a JetBrains Marketplace permanent token from Marketplace `My Tokens`.
- Add `JETBRAINS_MARKETPLACE_TOKEN` to GitHub Actions secrets or the protected environment.
- Confirm `GITHUB_TOKEN` has `contents: write` permission for creating and updating GitHub Releases from `jetbrains/v*` tags.
- Optionally create a protected `jetbrains-marketplace` GitHub Environment with required reviewers.
- If using an environment, move the Marketplace and signing secrets there and set the workflow job environment.
## One-Time Signing Certificate Setup
- Generate or locate the JetBrains plugin signing private key and certificate chain.
- Add `JETBRAINS_CERTIFICATE_CHAIN` with the full certificate chain PEM content.
- Add `JETBRAINS_PRIVATE_KEY` with the private key PEM content.
- Add `JETBRAINS_PRIVATE_KEY_PASSWORD` with the private key password.
- Keep PEM files out of git, local logs, and workflow output.
- If GitHub secret input mangles multiline PEM values, store base64-encoded values and verify the Gradle signing task decodes them correctly.
## Per-RC Release
- Choose an RC version in the form `x.y.z-rc.n`.
- Push tag `jetbrains/vx.y.z-rc.n`, for example `jetbrains/v7.0.1-rc.1`.
- Watch the `publish-jetbrains` workflow.
- Download and retain the workflow artifact if needed.
- Confirm the update appears on the JetBrains Marketplace `eap` channel.
- Confirm the GitHub Release for the `jetbrains/vx.y.z-rc.n` tag exists and contains the JetBrains plugin ZIP asset.
- Share `https://plugins.jetbrains.com/plugins/eap/list` with testers.
## Stable Release Guard
- Stable tags like `jetbrains/vx.y.z` are intentionally rejected for now.
- Before enabling stable releases, remove the workflow stable guard.
- Verify `kilo.channel=default` publishes to the default Marketplace channel.
- Update this checklist before stable releases are enabled.
+62
View File
@@ -0,0 +1,62 @@
# Releasing the JetBrains Plugin
## RC releases (currently the only supported flow)
Stable release tags (`jetbrains/vx.y.z`) are recognized by the workflow but intentionally rejected. Only RC tags are accepted right now.
### 1. Create and push a tag
Tag format: `jetbrains/v<major>.<minor>.<patch>-rc.<n>`
```
git tag jetbrains/v7.0.1-rc.1
git push origin jetbrains/v7.0.1-rc.1
```
### 2. Watch the workflow
The `publish-jetbrains` workflow starts automatically on tag push. Follow progress at:
[https://github.com/Kilo-Org/kilocode/actions/workflows/publish-jetbrains.yml](https://github.com/Kilo-Org/kilocode/actions/workflows/publish-jetbrains.yml)
The workflow:
1. Validates the tag format and required secrets.
2. Downloads CLI binaries for all 6 platforms from the matching GitHub Release.
3. Verifies and signs the plugin with `./gradlew verifyPlugin publishPlugin -Pproduction=true`.
4. Publishes the signed ZIP to the JetBrains Marketplace `eap` channel.
5. Uploads the signed ZIP to a GitHub prerelease for the tag.
### 3. Verify on the Marketplace
Once the workflow succeeds, the new version should appear in the plugin's version list:
[https://plugins.jetbrains.com/plugin/28350-kilo-code/edit/versions](https://plugins.jetbrains.com/plugin/28350-kilo-code/edit/versions)
---
## Installing RC builds via the custom plugin repository
RC builds are published to the `eap` channel, not the default channel. To get them in IntelliJ IDEA:
1. Open **Settings > Plugins**.
2. Click the gear icon and choose **Manage Plugin Repositories**.
3. Add the following URL:
```
https://plugins.jetbrains.com/plugins/list?channel=eap&pluginId=28350
```
4. Search for **Kilo Code** in the Marketplace tab — the latest RC version will appear and update automatically.
---
## Required GitHub Actions secrets
| Secret | Purpose |
|---|---|
| `JETBRAINS_MARKETPLACE_TOKEN` | Marketplace API token for publishing |
| `JETBRAINS_CERTIFICATE_CHAIN` | PEM certificate chain for plugin signing |
| `JETBRAINS_PRIVATE_KEY` | PEM private key for plugin signing |
| `JETBRAINS_PRIVATE_KEY_PASSWORD` | Password for the private key |
Before the first publish, complete `RELEASE_TODO.md` to set up these secrets and the Marketplace plugin entry.
+57 -2
View File
@@ -4,7 +4,32 @@ import org.jetbrains.intellij.platform.gradle.tasks.RunIdeTask
import org.jetbrains.intellij.platform.gradle.tasks.aware.SplitModeAware.SplitModeTarget
group = "ai.kilocode.jetbrains"
version = "7.0.1"
fun checked(value: String): String {
if (value == "0.0.0-dev") return value
require(Regex("^[0-9]+\\.[0-9]+\\.[0-9]+(-rc\\.[0-9]+)?$").matches(value)) {
"Invalid JetBrains plugin version: $value"
}
return value
}
fun gitTag(): String? {
val text = providers.exec {
commandLine("git", "tag", "--points-at", "HEAD")
}.standardOutput.asText.get()
return text.lineSequence().map { it.trim() }.firstOrNull { it.startsWith("jetbrains/v") }
}
val release = providers.gradleProperty("production").map { it.toBoolean() }.orElse(false).get()
val ver = if (release) checked(
gitTag()?.removePrefix("jetbrains/v")
?: error("Missing JetBrains plugin version. Publish builds must run from a jetbrains/v<version> tag."),
) else checked(gitTag()?.removePrefix("jetbrains/v") ?: "0.0.0-dev")
val notes = providers.gradleProperty("kilo.changeNotes").orElse("Release candidate build.")
val channel = providers.gradleProperty("kilo.channel").map { it.trim() }.orElse("default")
version = ver
plugins {
application
@@ -59,9 +84,39 @@ intellijPlatform {
splitMode = true
splitModeTarget = SplitModeTarget.BOTH
pluginConfiguration {
id = "ai.kilocode.jetbrains"
name = "Kilo Code"
version = provider { ver }
changeNotes = notes
ideaVersion {
untilBuild = provider { null }
}
vendor {
name = "Kilo Code"
url = "https://kilo.ai"
}
}
publishing {
token = providers.environmentVariable("JETBRAINS_MARKETPLACE_TOKEN")
channels = channel.map { value ->
if (value.isBlank() || value == "default") return@map listOf("default")
listOf(value)
}
}
signing {
certificateChain = providers.environmentVariable("JETBRAINS_CERTIFICATE_CHAIN")
privateKey = providers.environmentVariable("JETBRAINS_PRIVATE_KEY")
password = providers.environmentVariable("JETBRAINS_PRIVATE_KEY_PASSWORD")
}
pluginVerification {
ides {
create(IntelliJPlatformType.IntellijIdeaCommunity, libs.versions.intellij.platform)
create(IntelliJPlatformType.IntellijIdea, libs.versions.intellij.platform)
}
}
}
@@ -5,6 +5,7 @@ import ai.kilocode.client.app.KiloSessionService
import ai.kilocode.client.app.Workspace
import ai.kilocode.client.session.model.SessionModelEvent
import ai.kilocode.client.session.model.SessionState
import ai.kilocode.client.session.scroll.SessionScroll
import ai.kilocode.client.session.ui.ConnectionPanel
import ai.kilocode.client.session.ui.EmptySessionPanel
import ai.kilocode.client.session.ui.LoadingPanel
@@ -5,6 +5,8 @@ import ai.kilocode.client.session.SessionManager
import ai.kilocode.client.session.ui.LoadingPanel
import ai.kilocode.client.session.ui.style.SessionEditorStyle
import ai.kilocode.client.ui.UiStyle
import ai.kilocode.client.ui.HoverIcon
import ai.kilocode.client.ui.iconButton
import com.intellij.icons.AllIcons
import com.intellij.ide.ui.LafManagerListener
import com.intellij.openapi.application.ApplicationManager
@@ -12,11 +14,7 @@ import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.ActionGroup
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.DataProvider
import com.intellij.openapi.actionSystem.Presentation
import com.intellij.openapi.actionSystem.impl.ActionButton
import com.intellij.openapi.util.Disposer
import com.intellij.ui.DocumentAdapter
import com.intellij.ui.PopupHandler
@@ -180,12 +178,12 @@ class HistoryPanel(
private fun back(): BorderLayoutPanel {
val label = KiloBundle.message("history.back")
val action = object : AnAction(label, null, AllIcons.Actions.Back) {
override fun actionPerformed(e: AnActionEvent) = nav()
}
val presentation = Presentation(label).apply { icon = AllIcons.Actions.Back }
val btn = ActionButton(action, presentation, ActionPlaces.TOOLBAR, JBUI.size(16)).apply {
val btn = HoverIcon().apply {
icon = AllIcons.Actions.Back
toolTipText = label
accessibleContext.accessibleName = label
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
addActionListener { nav() }
}
return BorderLayoutPanel().apply {
add(btn, BorderLayout.WEST)
@@ -379,17 +377,17 @@ class HistoryPanel(
internal fun backText(): String? {
val view = activeInfo().foreSideComponent ?: return null
return UIUtil.uiTraverser(view).filter(ActionButton::class.java).firstOrNull()?.presentation?.text
return UIUtil.uiTraverser(view).filter(HoverIcon::class.java).firstOrNull()?.toolTipText
}
internal fun backCursor(): Int? {
val view = activeInfo().foreSideComponent ?: return null
return UIUtil.uiTraverser(view).filter(ActionButton::class.java).firstOrNull()?.cursor?.type
return UIUtil.uiTraverser(view).filter(HoverIcon::class.java).firstOrNull()?.cursor?.type
}
internal fun clickBack() {
val view = activeInfo().foreSideComponent ?: return
UIUtil.uiTraverser(view).filter(ActionButton::class.java).firstOrNull()?.click()
UIUtil.uiTraverser(view).filter(HoverIcon::class.java).firstOrNull()?.doClick()
}
internal fun clickDelete() {
@@ -0,0 +1,17 @@
package ai.kilocode.client.session.scroll
import ai.kilocode.client.ui.colorizeIfPossible
import com.intellij.openapi.util.IconLoader
import com.intellij.util.ui.JBUI
import javax.swing.Icon
internal object ScrollButtonIcon {
private val icon = IconLoader.getIcon("/icons/scroll-bottom.svg", ScrollButtonIcon::class.java)
fun create(): Icon = icon.colorizeIfPossible(
fillColor = JBUI.CurrentTheme.Button.defaultButtonColorStart(),
borderColor = JBUI.CurrentTheme.Button.defaultButtonForeground(),
fillId = "ScrollButton.Background",
strokeId = "ScrollButton.Foreground",
)
}
@@ -1,4 +1,4 @@
package ai.kilocode.client.session
package ai.kilocode.client.session.scroll
import ai.kilocode.client.plugin.KiloBundle
import ai.kilocode.client.session.ui.SessionMessageListPanel
@@ -7,27 +7,18 @@ import ai.kilocode.client.session.ui.style.SessionEditorStyle
import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget
import ai.kilocode.client.ui.UiStyle
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.util.IconLoader
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.JBScrollPane
import com.intellij.ui.icons.CachedImageIcon
import com.intellij.ui.svg.SvgAttributePatcher
import com.intellij.util.SVGLoader
import com.intellij.util.ui.JBUI
import java.awt.Color
import java.awt.Cursor
import java.awt.Point
import java.awt.Rectangle
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import javax.swing.Icon
import javax.swing.JComponent
import javax.swing.JPanel
import javax.swing.JScrollBar
private const val ICON_DIGEST = 0x5c011b0bb17L
private const val OPAQUE_ALPHA = 255
internal class SessionScroll(
private val root: SessionRootPanel,
private val host: JPanel,
@@ -35,7 +26,6 @@ internal class SessionScroll(
body: JPanel,
) {
companion object {
private val ICON = IconLoader.getIcon("/icons/scroll-bottom.svg", SessionScroll::class.java)
private const val THRESHOLD = 32
private const val OPEN_PASSES = 12
}
@@ -58,7 +48,7 @@ internal class SessionScroll(
private var seq = 0
init {
jump = JBLabel(patchedIcon(ICON)).apply {
jump = JBLabel(ScrollButtonIcon.create()).apply {
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
toolTipText = KiloBundle.message("session.scroll.bottom")
isVisible = false
@@ -130,7 +120,7 @@ internal class SessionScroll(
fun applyStyle(style: SessionEditorStyle) {
this.style = style
jump.icon = patchedIcon(ICON)
jump.icon = ScrollButtonIcon.create()
messages.applyStyle(style)
val view = component.viewport.view
if (view !== messages) (view as? SessionEditorStyleTarget)?.applyStyle(style)
@@ -231,34 +221,3 @@ internal class SessionScroll(
root.overlay.repaint()
}
}
private fun patchedIcon(icon: Icon): Icon {
val cached = icon as? CachedImageIcon ?: return icon
return cached.createWithPatcher(object : SVGLoader.SvgElementColorPatcherProvider, SvgAttributePatcher {
override fun digest(): LongArray {
val bg = JBUI.CurrentTheme.Button.defaultButtonColorStart().rgb.toLong()
val fg = JBUI.CurrentTheme.Button.defaultButtonForeground().rgb.toLong()
return longArrayOf(bg, fg, ICON_DIGEST)
}
override fun attributeForPath(path: String) = this
override fun patchColors(attributes: MutableMap<String, String>) {
when (attributes["id"]) {
"ScrollButton.Background" ->
set(attributes, "fill", JBUI.CurrentTheme.Button.defaultButtonColorStart())
"ScrollButton.Foreground" ->
set(attributes, "stroke", JBUI.CurrentTheme.Button.defaultButtonForeground())
}
}
private fun set(attributes: MutableMap<String, String>, key: String, color: Color) {
if (!attributes.containsKey(key) || attributes[key] == "none") return
attributes[key] = "rgb(${color.red},${color.green},${color.blue})"
if (color.alpha != OPAQUE_ALPHA) {
attributes["$key-opacity"] = "${color.alpha / OPAQUE_ALPHA.toFloat()}"
}
}
})
}
@@ -1,7 +1,7 @@
package ai.kilocode.client.session.ui
import com.intellij.openapi.ui.popup.util.PopupUtil
import com.intellij.ui.ExperimentalUI
import com.intellij.ui.NewUI
import com.intellij.ui.popup.list.SelectablePanel
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
@@ -23,7 +23,7 @@ internal class PickerRow : SelectablePanel() {
fun update(list: JList<*>, selected: Boolean, focused: Boolean) {
background = list.background
selectionColor = if (selected) UIUtil.getListBackground(true, focused) else null
if (ExperimentalUI.isNewUI()) {
if (NewUI.isEnabled()) {
PopupUtil.configListRendererFlexibleHeight(this)
return
}
@@ -9,8 +9,8 @@ import com.intellij.openapi.ui.popup.PopupShowOptions
import com.intellij.openapi.ui.popup.util.PopupUtil
import com.intellij.ui.CollectionListModel
import com.intellij.ui.DocumentAdapter
import com.intellij.ui.ExperimentalUI
import com.intellij.ui.JBColor
import com.intellij.ui.NewUI
import com.intellij.ui.ListUtil
import com.intellij.ui.SearchTextField
import com.intellij.ui.ScrollPaneFactory
@@ -38,7 +38,7 @@ import javax.swing.SwingUtilities
import javax.swing.event.DocumentEvent
private val popupBackground: Color
get() = if (ExperimentalUI.isNewUI()) JBUI.CurrentTheme.Popup.BACKGROUND else UIUtil.getListBackground()
get() = if (NewUI.isEnabled()) JBUI.CurrentTheme.Popup.BACKGROUND else UIUtil.getListBackground()
private const val MODEL_PICKER_MIN_WIDTH = 420
private const val MODEL_PICKER_MAX_WIDTH = 760
@@ -5,8 +5,8 @@ import ai.kilocode.client.session.ui.PickerRow
import ai.kilocode.client.ui.UiStyle
import com.intellij.icons.AllIcons
import com.intellij.ui.CollectionListModel
import com.intellij.ui.ExperimentalUI
import com.intellij.ui.GroupHeaderSeparator
import com.intellij.ui.NewUI
import com.intellij.ui.JBColor
import com.intellij.ui.SimpleColoredComponent
import com.intellij.ui.SimpleTextAttributes
@@ -23,7 +23,7 @@ import java.awt.Graphics2D
import java.awt.Point
import java.awt.Rectangle
import java.awt.RenderingHints
import java.awt.Toolkit
import java.awt.font.FontRenderContext
import javax.swing.Icon
import javax.swing.JList
import javax.swing.JPanel
@@ -53,7 +53,7 @@ internal class ModelPickerRenderer(
}
private fun favoriteInset(list: JList<*>): Int {
if (!ExperimentalUI.isNewUI()) return 0
if (!NewUI.isEnabled()) return 0
val inner = JBUI.CurrentTheme.Popup.Selection.innerInsets()
val edge = JBUI.CurrentTheme.Popup.Selection.LEFT_RIGHT_INSET.get()
return edge + if (list.componentOrientation.isLeftToRight) inner.right else inner.left
@@ -162,11 +162,9 @@ internal class ModelPickerRenderer(
private val text = KiloBundle.message("model.picker.free")
override fun getIconWidth(): Int {
val fm = JBFont.small().let { font ->
@Suppress("DEPRECATION")
Toolkit.getDefaultToolkit().getFontMetrics(font)
}
return fm.stringWidth(text) + JBUI.scale(12)
val font = JBFont.small()
val w = font.getStringBounds(text, FontRenderContext(null, true, true)).width.toInt()
return w + JBUI.scale(12)
}
override fun getIconHeight(): Int = JBUI.scale(16)
@@ -117,7 +117,7 @@ class PromptPanel(
?: return@addActionListener
val ctx = DataManager.getInstance().getDataContext(button)
val event = AnActionEvent.createEvent(action, ctx, null, ActionPlaces.UNKNOWN, ActionUiKind.NONE, null)
action.update(event)
ActionUtil.updateAction(action, event)
ActionUtil.performAction(action, event)
}
}
@@ -0,0 +1,46 @@
package ai.kilocode.client.ui
import com.intellij.ui.icons.CachedImageIcon
import com.intellij.ui.svg.SvgAttributePatcher
import com.intellij.util.SVGLoader
import java.awt.Color
import javax.swing.Icon
private const val OPAQUE_ALPHA = 255
internal fun Icon.colorizeIfPossible(
fillColor: Color,
borderColor: Color = fillColor,
fillId: String? = null,
strokeId: String? = null,
): Icon = (this as? CachedImageIcon)?.createWithPatcher(
colorPatcher = object : SVGLoader.SvgElementColorPatcherProvider, SvgAttributePatcher {
private val digest = longArrayOf(0L, 440413911775177385)
override fun digest(): LongArray {
digest[0] = toLong(fillColor.rgb, borderColor.rgb)
return digest
}
override fun patchColors(attributes: MutableMap<String, String>) {
val id = attributes["id"]
if (fillId == null || id == fillId) setAttribute(attributes, "fill", fillColor)
if (strokeId == null || id == strokeId) setAttribute(attributes, "stroke", borderColor)
}
override fun attributeForPath(path: String) = this
private fun setAttribute(attributes: MutableMap<String, String>, key: String, color: Color) {
if (!attributes.containsKey(key) || attributes[key] == "none") return
attributes[key] = "rgb(${color.red},${color.green},${color.blue})"
val alpha = color.alpha
if (alpha != OPAQUE_ALPHA) {
attributes["$key-opacity"] = "${alpha / OPAQUE_ALPHA.toFloat()}"
}
}
private fun toLong(high: Int, low: Int): Long {
return (high.toLong() shl 32) or (low.toLong() and 0xFFFFFFFFL)
}
},
) ?: this
@@ -20,6 +20,7 @@ import ai.kilocode.rpc.dto.KiloWorkspaceStatusDto
import ai.kilocode.rpc.dto.SessionDto
import ai.kilocode.rpc.dto.SessionTimeDto
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.ex.ActionUtil
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.actionSystem.Presentation
@@ -71,7 +72,7 @@ class HistorySessionActionsTest : BasePlatformTestCase() {
val local = localItem("ses_1")
val event = event(action, manager, selection(HistorySource.LOCAL, listOf(local)), controller)
action.update(event)
ActionUtil.updateAction(action, event)
assertTrue(event.presentation.isEnabledAndVisible)
}
@@ -81,7 +82,7 @@ class HistorySessionActionsTest : BasePlatformTestCase() {
val item = cloudItem("cloud_1")
val event = event(action, manager, selection(HistorySource.CLOUD, emptyList(), listOf(item)), controller)
action.update(event)
ActionUtil.updateAction(action, event)
assertTrue(event.presentation.isEnabledAndVisible)
}
@@ -91,7 +92,7 @@ class HistorySessionActionsTest : BasePlatformTestCase() {
val local = localItem("ses_1")
val event = event(action, null, selection(HistorySource.LOCAL, listOf(local)), controller)
action.update(event)
ActionUtil.updateAction(action, event)
assertFalse(event.presentation.isEnabledAndVisible)
}
@@ -100,7 +101,7 @@ class HistorySessionActionsTest : BasePlatformTestCase() {
val action = OpenSessionAction()
val event = event(action, manager, selection(HistorySource.LOCAL, emptyList()), controller)
action.update(event)
ActionUtil.updateAction(action, event)
assertFalse(event.presentation.isEnabledAndVisible)
}
@@ -110,7 +111,7 @@ class HistorySessionActionsTest : BasePlatformTestCase() {
val items = listOf(localItem("ses_1"), localItem("ses_2"))
val event = event(action, manager, selection(HistorySource.LOCAL, items), controller)
action.update(event)
ActionUtil.updateAction(action, event)
assertFalse(event.presentation.isEnabledAndVisible)
}
@@ -160,7 +161,7 @@ class HistorySessionActionsTest : BasePlatformTestCase() {
val local = localItem("ses_1")
val event = event(action, manager, selection(HistorySource.LOCAL, listOf(local)), controller)
action.update(event)
ActionUtil.updateAction(action, event)
assertTrue(event.presentation.isEnabledAndVisible)
}
@@ -170,7 +171,7 @@ class HistorySessionActionsTest : BasePlatformTestCase() {
val item = cloudItem("cloud_1")
val event = event(action, manager, selection(HistorySource.CLOUD, emptyList(), listOf(item)), controller)
action.update(event)
ActionUtil.updateAction(action, event)
assertFalse(event.presentation.isEnabledAndVisible)
}
@@ -180,7 +181,7 @@ class HistorySessionActionsTest : BasePlatformTestCase() {
val local = localItem("ses_1")
val event = event(action, null, selection(HistorySource.LOCAL, listOf(local)), controller)
action.update(event)
ActionUtil.updateAction(action, event)
assertFalse(event.presentation.isEnabledAndVisible)
}
@@ -189,7 +190,7 @@ class HistorySessionActionsTest : BasePlatformTestCase() {
val action = DeleteSessionAction()
val event = event(action, manager, selection(HistorySource.LOCAL, emptyList()), controller)
action.update(event)
ActionUtil.updateAction(action, event)
assertFalse(event.presentation.isEnabledAndVisible)
}
@@ -260,7 +261,7 @@ class HistorySessionActionsTest : BasePlatformTestCase() {
val local = localItem("ses_1")
val event = event(action, manager, selection(HistorySource.LOCAL, listOf(local)), controller)
action.update(event)
ActionUtil.updateAction(action, event)
assertTrue(event.presentation.isEnabledAndVisible)
}
@@ -269,7 +270,7 @@ class HistorySessionActionsTest : BasePlatformTestCase() {
val action = RenameSessionAction()
val event = event(action, manager, selection(HistorySource.LOCAL, emptyList()), controller)
action.update(event)
ActionUtil.updateAction(action, event)
assertFalse(event.presentation.isEnabledAndVisible)
}
@@ -279,7 +280,7 @@ class HistorySessionActionsTest : BasePlatformTestCase() {
val items = listOf(localItem("ses_1"), localItem("ses_2"))
val event = event(action, manager, selection(HistorySource.LOCAL, items), controller)
action.update(event)
ActionUtil.updateAction(action, event)
assertFalse(event.presentation.isEnabledAndVisible)
}
@@ -289,7 +290,7 @@ class HistorySessionActionsTest : BasePlatformTestCase() {
val item = cloudItem("cloud_1")
val event = event(action, manager, selection(HistorySource.CLOUD, emptyList(), listOf(item)), controller)
action.update(event)
ActionUtil.updateAction(action, event)
assertFalse(event.presentation.isEnabledAndVisible)
}
@@ -5,6 +5,7 @@ import ai.kilocode.client.session.SessionRef
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.actionSystem.Presentation
import com.intellij.openapi.actionSystem.ex.ActionUtil
import com.intellij.testFramework.fixtures.BasePlatformTestCase
@Suppress("UnstableApiUsage")
@@ -14,7 +15,7 @@ class NewSessionActionTest : BasePlatformTestCase() {
val action = NewSessionAction()
val event = event(manager)
action.update(event)
ActionUtil.updateAction(action, event)
action.actionPerformed(event)
assertEquals(1, manager.created)
@@ -28,7 +29,7 @@ class NewSessionActionTest : BasePlatformTestCase() {
val action = NewSessionAction()
val presentation = Presentation().apply { copyFrom(action.templatePresentation) }
action.update(AnActionEvent.createFromDataContext("", presentation) { null })
ActionUtil.updateAction(action, AnActionEvent.createFromDataContext("", presentation) { null })
assertFalse(presentation.isEnabled)
}
@@ -3,6 +3,7 @@ package ai.kilocode.client.actions
import ai.kilocode.client.session.ui.prompt.PromptDataKeys
import ai.kilocode.client.session.ui.prompt.SendPromptContext
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.ex.ActionUtil
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.actionSystem.Presentation
import com.intellij.testFramework.fixtures.BasePlatformTestCase
@@ -14,7 +15,7 @@ class SendPromptActionTest : BasePlatformTestCase() {
val action = SendPromptAction()
val event = event(action, ctx)
action.update(event)
ActionUtil.updateAction(action, event)
action.actionPerformed(event)
assertTrue(event.presentation.isEnabled)
@@ -27,7 +28,7 @@ class SendPromptActionTest : BasePlatformTestCase() {
val action = SendPromptAction()
val event = event(action, null)
action.update(event)
ActionUtil.updateAction(action, event)
assertFalse(event.presentation.isEnabled)
}
@@ -37,7 +38,7 @@ class SendPromptActionTest : BasePlatformTestCase() {
val action = SendPromptAction()
val event = event(action, ctx)
action.update(event)
ActionUtil.updateAction(action, event)
action.actionPerformed(event)
assertFalse(event.presentation.isEnabled)
@@ -3,6 +3,7 @@ package ai.kilocode.client.actions
import ai.kilocode.client.session.ui.prompt.PromptDataKeys
import ai.kilocode.client.session.ui.prompt.SendPromptContext
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.ex.ActionUtil
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.actionSystem.Presentation
import com.intellij.testFramework.fixtures.BasePlatformTestCase
@@ -14,7 +15,7 @@ class StopSessionActionTest : BasePlatformTestCase() {
val action = StopSessionAction()
val event = event(action, ctx)
action.update(event)
ActionUtil.updateAction(action, event)
action.actionPerformed(event)
assertTrue(event.presentation.isEnabled)
@@ -27,7 +28,7 @@ class StopSessionActionTest : BasePlatformTestCase() {
val action = StopSessionAction()
val event = event(action, null)
action.update(event)
ActionUtil.updateAction(action, event)
assertFalse(event.presentation.isEnabled)
}
@@ -37,7 +38,7 @@ class StopSessionActionTest : BasePlatformTestCase() {
val action = StopSessionAction()
val event = event(action, ctx)
action.update(event)
ActionUtil.updateAction(action, event)
action.actionPerformed(event)
assertFalse(event.presentation.isEnabled)
@@ -11,6 +11,7 @@ import ai.kilocode.client.testing.FakeAppRpcApi
import ai.kilocode.client.testing.FakeSessionRpcApi
import ai.kilocode.client.testing.FakeWorkspaceRpcApi
import ai.kilocode.client.session.SessionRef
import ai.kilocode.client.session.scroll.SessionScroll
import ai.kilocode.rpc.dto.ChatEventDto
import ai.kilocode.rpc.dto.KiloAppStateDto
import ai.kilocode.rpc.dto.KiloAppStatusDto
@@ -3,7 +3,7 @@ package ai.kilocode.client.session.ui.model
import ai.kilocode.rpc.dto.ModelSelectionDto
import com.intellij.icons.AllIcons
import com.intellij.ui.CollectionListModel
import com.intellij.ui.ExperimentalUI
import com.intellij.ui.NewUI
import com.intellij.ui.components.JBList
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import com.intellij.util.ui.EmptyIcon
@@ -273,7 +273,7 @@ class ModelPickerTest : BasePlatformTestCase() {
) = ModelPicker.Item(id, display, provider, name, index, free = free)
private fun favoriteInset(list: JBList<*>): Int {
if (!ExperimentalUI.isNewUI()) return 0
if (!NewUI.isEnabled()) return 0
val inner = JBUI.CurrentTheme.Popup.Selection.innerInsets()
val edge = JBUI.CurrentTheme.Popup.Selection.LEFT_RIGHT_INSET.get()
return edge + if (list.componentOrientation.isLeftToRight) inner.right else inner.left
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-telemetry",
"version": "7.2.52",
"version": "7.3.0",
"type": "module",
"license": "MIT",
"description": "Telemetry for Kilo CLI - PostHog analytics integration",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kilocode/kilo-ui",
"version": "7.2.52",
"version": "7.3.0",
"type": "module",
"license": "MIT",
"exports": {
+53
View File
@@ -1,5 +1,58 @@
# kilo-code
## 7.3.0
## 7.2.54
### Minor Changes
- [#10230](https://github.com/Kilo-Org/kilocode/pull/10230) [`26f5393`](https://github.com/Kilo-Org/kilocode/commit/26f5393be272a31390f6a317555099ed2addd1b0) - Add a collapsible sidebar in the Agent Manager. The toggle button sits left of the tab title and the collapsed state persists across reloads and restarts. Starting a new session or worktree automatically reopens the sidebar so the new entry is visible.
- [#10218](https://github.com/Kilo-Org/kilocode/pull/10218) [`4860e65`](https://github.com/Kilo-Org/kilocode/commit/4860e654ca1cc46c4e99acc3f40d4f1302e34944) - Support setting an auto-compaction threshold percentage so long sessions can compact before the context window is full.
- [#10228](https://github.com/Kilo-Org/kilocode/pull/10228) [`911e7ce`](https://github.com/Kilo-Org/kilocode/commit/911e7ce73383cfd5780ec80ff8e8b8187061a18a) - Add syntax highlighting to shell command output with Shiki, labeled "Command" and "Output" sections, per-section copy buttons, and an "Open in Editor" action that opens the full untruncated output in a VS Code editor tab.
- [#10258](https://github.com/Kilo-Org/kilocode/pull/10258) [`1af7973`](https://github.com/Kilo-Org/kilocode/commit/1af79731a8ed925f1f69aa536ba90a53b89e8dfb) - Support experimental speech-to-text voice input in VS Code prompt fields through Kilo Gateway.
### Patch Changes
- [#10196](https://github.com/Kilo-Org/kilocode/pull/10196) [`174d467`](https://github.com/Kilo-Org/kilocode/commit/174d467a4967c53fddfc97ae9663ebf17c0fafbc) - Keep Agent Manager state hidden from Git before creating local sessions.
- [#10156](https://github.com/Kilo-Org/kilocode/pull/10156) [`e805d2f`](https://github.com/Kilo-Org/kilocode/commit/e805d2f756b785776cb1f60b97a253e3ae4c837c) - Keep VS Code session timeline, token, context, and cost stats in sync after restoring a checkpoint.
- [#10136](https://github.com/Kilo-Org/kilocode/pull/10136) [`8af638e`](https://github.com/Kilo-Org/kilocode/commit/8af638e7e20c645b22d96da5e30665e8e9cbf6ad) - Show ChatGPT sign-in again when Codex authentication expires.
- [#8754](https://github.com/Kilo-Org/kilocode/pull/8754) [`e498c02`](https://github.com/Kilo-Org/kilocode/commit/e498c02f7acc5c228bbd45f9e4f294bf5def21ca) Thanks [@shssoichiro](https://github.com/shssoichiro)! - Fix TUI diff rendering when header-like content lines appear inside a unified diff hunk.
- [#10265](https://github.com/Kilo-Org/kilocode/pull/10265) [`900ba85`](https://github.com/Kilo-Org/kilocode/commit/900ba85b838e035952ea6ca9adf20cbc923e8d99) - Ensure snapshot installs package a fresh CLI backend so new extension features are available in local VSIX builds.
- [#10227](https://github.com/Kilo-Org/kilocode/pull/10227) [`e1e2454`](https://github.com/Kilo-Org/kilocode/commit/e1e2454b4285109f1d4ef34b93d5d47f612faaea) - Hide Task tool subagent sessions from Agent Manager session lists.
- [#10229](https://github.com/Kilo-Org/kilocode/pull/10229) [`41e3cda`](https://github.com/Kilo-Org/kilocode/commit/41e3cda971c37d155de443e58ae2aeafdaf928b1) - Move worktree file revert diff status checks into the VS Code extension host so Kilo no longer asks the CLI server to run git diff commands through Bun during workspace and Agent Manager reverts.
- [#10240](https://github.com/Kilo-Org/kilocode/pull/10240) [`09f0156`](https://github.com/Kilo-Org/kilocode/commit/09f0156bfc66643870835251eef873d6e62f2791) - Keep the New Worktree model picker open when interacting with its search field.
- [#10243](https://github.com/Kilo-Org/kilocode/pull/10243) [`47c22a2`](https://github.com/Kilo-Org/kilocode/commit/47c22a28e98a50955bd9470a5b3f78bce5d5b4ba) - Keep chat tool cards open or closed when streaming updates remount them.
- [#10159](https://github.com/Kilo-Org/kilocode/pull/10159) [`64e45ea`](https://github.com/Kilo-Org/kilocode/commit/64e45eaba30641dd7b36fb65f79ddee3c0e14cf1) - Use the VS Code-native placeholder color for input fields so placeholder text is consistently distinct from values you have typed (Settings, Agent Manager, prompt input, model search, etc.).
- [#10206](https://github.com/Kilo-Org/kilocode/pull/10206) [`177f7df`](https://github.com/Kilo-Org/kilocode/commit/177f7df04937e13b3ea5abc5a9c79a56c9814ab6) - Reset the To-dos tab when reverting a session snapshot.
- [#10158](https://github.com/Kilo-Org/kilocode/pull/10158) [`d8245a0`](https://github.com/Kilo-Org/kilocode/commit/d8245a0ceb0989b8596c5a5d17fd1095ba9521be) - Fix Mermaid diagrams rendering with empty text inside every shape by restoring the `foreignObject` HTML integration point that DOMPurify dropped in 3.1.7.
- [#10256](https://github.com/Kilo-Org/kilocode/pull/10256) [`da9292f`](https://github.com/Kilo-Org/kilocode/commit/da9292fbccad9f4c20f439e240790da3ab3a92d3) - Support viewing Agent Manager terminal tabs alongside the right diff sidebar, and send review comments to the active terminal.
- [#10275](https://github.com/Kilo-Org/kilocode/pull/10275) [`f5dc95b`](https://github.com/Kilo-Org/kilocode/commit/f5dc95b99394c17ad7140bb034bc15a0f9de60b6) - Keep GPT-4o voice transcriptions closer to the words that were spoken.
- [#10095](https://github.com/Kilo-Org/kilocode/pull/10095) [`710c9dc`](https://github.com/Kilo-Org/kilocode/commit/710c9dca1e9d5f522fa3fe1495dc41ec1eea4b66) - Keep configured mode models in sync when switching modes in VS Code.
- Updated dependencies [[`262c2d6`](https://github.com/Kilo-Org/kilocode/commit/262c2d670474f0da50aa0c900e49aad93863acaf), [`4860e65`](https://github.com/Kilo-Org/kilocode/commit/4860e654ca1cc46c4e99acc3f40d4f1302e34944), [`47c22a2`](https://github.com/Kilo-Org/kilocode/commit/47c22a28e98a50955bd9470a5b3f78bce5d5b4ba), [`1af7973`](https://github.com/Kilo-Org/kilocode/commit/1af79731a8ed925f1f69aa536ba90a53b89e8dfb), [`1ea86fb`](https://github.com/Kilo-Org/kilocode/commit/1ea86fb6e15cbe486cb0af6f26995d0b1b2745a2), [`f5dc95b`](https://github.com/Kilo-Org/kilocode/commit/f5dc95b99394c17ad7140bb034bc15a0f9de60b6)]:
- @kilocode/kilo-ui@7.2.53
- @kilocode/sdk@7.3.0
- @kilocode/kilo-gateway@7.3.0
- @opencode-ai/ui@7.2.53
- @kilocode/kilo-indexing@7.2.53
## 7.2.51
### Patch Changes
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "kilo-code",
"displayName": "Kilo Code: AI Coding Agent, Copilot, and Autocomplete",
"description": "Open Source AI coding agent that generates code from natural language, automates tasks, and runs terminal commands. Features inline autocomplete, browser automation, automated refactoring, and custom modes for planning, coding, and debugging. Supports 500+ AI models including Claude (Anthropic), Gemini, Grok, GPT, Codex and GLM.",
"version": "7.2.52",
"version": "7.3.0",
"icon": "assets/icons/logo-outline-black.png",
"galleryBanner": {
"color": "#FFFFFF",
+64 -18
View File
@@ -17,6 +17,7 @@ import { FileIgnoreController } from "./services/autocomplete/shims/FileIgnoreCo
import { ChatTextAreaAutocomplete } from "./services/autocomplete/chat-autocomplete/ChatTextAreaAutocomplete"
import { buildWebviewHtml, getWebviewFontSize } from "./utils"
import { saveImage } from "./kilo-provider/save-image"
import { exportTranscript } from "./kilo-provider/export-transcript"
import {
TelemetryProxy,
type TelemetryPropertiesProvider,
@@ -137,6 +138,7 @@ import type { KiloProviderOptions } from "./kilo-provider/options"
import { fetchKiloEmbeddingModelCatalog } from "@kilocode/kilo-gateway"
type MessageLoadMode = "replace" | "prepend" | "focus" | "reconcile"
type ContextMessage = { contextDirectory?: unknown }
// Helper to map agent data to the subset of fields sent to the webview
const mapAgent = (a: Agent) => ({
name: a.name,
@@ -449,10 +451,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
webviewView.webview.html = this._getHtmlForWebview(webviewView.webview)
this.setupWebviewMessageHandler(webviewView.webview)
vscode.commands.executeCommand("setContext", "kilo-code.new.sidebarVisible", webviewView.visible)
this.setSidebarVisible(webviewView.visible)
this.visibilityDisposable?.dispose()
this.visibilityDisposable = webviewView.onDidChangeVisibility(() => {
vscode.commands.executeCommand("setContext", "kilo-code.new.sidebarVisible", webviewView.visible)
this.setSidebarVisible(webviewView.visible)
if (this.statsPoller) {
this.statsPoller.setEnabled(webviewView.visible)
this.statsPoller.setVisible(webviewView.visible)
@@ -462,6 +464,11 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.initializeConnection()
}
private setSidebarVisible(visible: boolean): void {
vscode.commands.executeCommand("setContext", "kilo-code.new.sidebarVisible", visible)
this.opts.onSidebarVisibilityChange?.(visible)
}
/**
* Resolve a WebviewPanel for displaying the Kilo webview in an editor tab.
*/
@@ -616,6 +623,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
connection: this.connectionService,
dir: this.getWorkspaceDirectory(this.currentSession?.id),
post: (msg) => this.postMessage(msg),
exportTranscript: (sessionID) => this.handleExportSessionTranscript(sessionID),
})
) {
return
@@ -647,7 +655,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.readyResolvers.splice(0).forEach((r) => r())
break
case "sendMessage": {
const files = parseMessageFiles(message.files)
const msg = message as typeof message & ContextMessage
await this.handleSendMessage(
message.text,
typeof message.messageID === "string" ? message.messageID : undefined,
@@ -657,13 +665,14 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
message.modelID,
message.agent,
message.variant,
files,
parseMessageFiles(message.files),
typeof message.agentManagerContext === "string" ? message.agentManagerContext : undefined,
typeof msg.contextDirectory === "string" ? msg.contextDirectory : undefined,
)
break
}
case "sendCommand": {
const files = parseMessageFiles(message.files)
const msg = message as typeof message & ContextMessage
await this.handleSendCommand(
message.command,
message.arguments,
@@ -674,8 +683,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
message.modelID,
message.agent,
message.variant,
files,
parseMessageFiles(message.files),
typeof message.agentManagerContext === "string" ? message.agentManagerContext : undefined,
typeof msg.contextDirectory === "string" ? msg.contextDirectory : undefined,
)
break
}
@@ -1081,13 +1091,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
break
}
case "fetchMarketplaceData": {
const workspace = this.getProjectDirectory(this.currentSession?.id)
const mp = this.getMarketplace()
// Fetch skills from CLI backend (authoritative source) so the
// marketplace doesn't need to duplicate the CLI's skill scanning.
const skills = await this.fetchCliSkills()
const data = await mp.fetchData(workspace, skills)
this.postMessage({ type: "marketplaceData", ...data })
await this.handleFetchMarketplaceData()
break
}
case "filterMarketplaceItems": {
@@ -1137,6 +1141,14 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.diffVirtualProvider.open(d)
}
private async handleFetchMarketplaceData(): Promise<void> {
const workspace = this.getProjectDirectory(this.currentSession?.id)
const mp = this.getMarketplace()
const skills = await this.fetchCliSkills()
const data = await mp.fetchData(workspace, skills)
this.postMessage({ type: "marketplaceData", ...data })
}
/**
* Initialize connection to the CLI backend server.
* Subscribes to the shared KiloConnectionService.
@@ -1346,7 +1358,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
try {
const workspaceDir = this.getContextDirectory()
const { data: session } = await this.client.session.create({ directory: workspaceDir }, { throwOnError: true })
const { data: session } = await this.client.session.create(
{ directory: workspaceDir, platform: this.opts.platform },
{ throwOnError: true },
)
this.setCurrentSession(session)
this.contextSessionID = session.id
this.trackDirectory(session.id, workspaceDir)
@@ -1536,6 +1551,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
client.session.list({ directory: dir, roots: true }, { throwOnError: true }).then(({ data }) => data)
: null,
sessionDirectories: this.sessionDirectories,
worktreeDirectories: this.opts.worktreeDirectories,
workspaceDirectory: this.getWorkspaceDirectory(),
postMessage: (msg: unknown) => this.postMessage(msg),
}
@@ -1654,6 +1670,30 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
}
}
/**
* Export a full session transcript as Markdown.
*/
private async handleExportSessionTranscript(sessionID: string): Promise<void> {
if (!this.client) {
this.postMessage({ type: "error", message: "Not connected to CLI backend" })
return
}
try {
const saved = await exportTranscript(this.client, {
sessionID,
dir: this.getWorkspaceDirectory(sessionID),
})
if (saved) void vscode.window.showInformationMessage("Session transcript exported as Markdown.")
} catch (error) {
console.error("[Kilo New] KiloProvider: Failed to export session transcript:", error)
this.postMessage({
type: "error",
message: getErrorMessage(error) || "Failed to export session transcript",
})
}
}
/** Fetch providers and send to webview. Coalesced: at most one in-flight + one queued. */
private async fetchAndSendProviders(): Promise<void> {
const next = ++this.providersGeneration
@@ -2439,7 +2479,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
details: getConfigErrorDetails(error),
})
}
private async resolveSession(sessionID?: string, draftID?: string, context?: string) {
private async resolveSession(sessionID?: string, draftID?: string, context?: string, contextDirectory?: string) {
if (!this.client) return undefined
const dir = resolveNewSessionDirectory({
@@ -2447,12 +2487,16 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
currentSessionID: this.currentSession?.id,
contextSessionID: this.contextSessionID,
agentManagerContext: context,
contextDirectory,
sessionDirectories: this.sessionDirectories,
workspaceDirectory: this.getRootDirectory(),
})
if (!sessionID && !this.currentSession) {
const { data: session } = await this.client.session.create({ directory: dir }, { throwOnError: true })
const { data: session } = await this.client.session.create(
{ directory: dir, platform: this.opts.platform },
{ throwOnError: true },
)
this.setCurrentSession(session)
this.contextSessionID = session.id
this.trackDirectory(session.id, dir)
@@ -2556,6 +2600,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
variant?: string,
files?: MessageFile[],
context?: string,
contextDirectory?: string,
): Promise<void> {
if (!this.client) {
this.postMessage({
@@ -2572,7 +2617,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
let resolved: { sid: string; dir: string } | undefined
try {
resolved = await this.resolveSession(sessionID, draftID, context)
resolved = await this.resolveSession(sessionID, draftID, context, contextDirectory)
const parts: Array<TextPartInput | FilePartInput> = []
if (files) {
@@ -2633,6 +2678,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
variant?: string,
files?: MessageFile[],
context?: string,
contextDirectory?: string,
): Promise<void> {
if (!this.client) {
this.postMessage({
@@ -2649,7 +2695,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
let resolved: { sid: string; dir: string } | undefined
try {
resolved = await this.resolveSession(sessionID, draftID, context)
resolved = await this.resolveSession(sessionID, draftID, context, contextDirectory)
if (messageID) {
this.connectionService.recordMessageSessionId(messageID, resolved!.sid)
@@ -71,6 +71,7 @@ export class AgentManagerProvider implements Disposable {
private cachedLocalStats: { type: "agentManager.localStats"; stats: LocalStats } | undefined
private unsubTool: (() => void) | undefined
private closing: Promise<void> | undefined
private onVisibilityChange: ((visible: boolean) => void) | undefined
/** Session ID most recently loaded via a `loadMessages` message from the webview.
* Updated synchronously — unlike the session provider's currentSession which depends on
@@ -181,10 +182,15 @@ export class AgentManagerProvider implements Disposable {
this.attachPanel(
this.host.openPanel({
onBeforeMessage: (msg) => this.onMessage(msg),
worktreeDirectories: () => this.getWorktreeDirectories(),
}),
)
}
public onPanelVisibilityChange(cb: (visible: boolean) => void): void {
this.onVisibilityChange = cb
}
/** Restore the Agent Manager panel from a previously serialized state.
* The caller (extension.ts / vscode-host.ts) wraps the raw panel before passing it. */
public deserializePanel(ctx: PanelContext): void {
@@ -212,6 +218,7 @@ export class AgentManagerProvider implements Disposable {
this.panel = ctx
this.statsPoller.setVisible(ctx.visible)
this.onVisibilityChange?.(ctx.visible)
ctx.onDidChangeVisibility((visible) => {
this.statsPoller.setVisible(visible)
})
@@ -233,6 +240,7 @@ export class AgentManagerProvider implements Disposable {
this.prBridge.poller.stop()
this.diffs.stop()
this.panel = undefined
this.onVisibilityChange?.(false)
}
ctx.sessions.dispose()
})
@@ -378,7 +386,7 @@ export class AgentManagerProvider implements Disposable {
if (m.type === "agentManager.deleteWorktree") return this.onDeleteWorktree(m.worktreeId)
if (m.type === "agentManager.removeStaleWorktree") return this.onRemoveStaleWorktree(m.worktreeId)
if (m.type === "agentManager.promoteSession") return this.onPromoteSession(m.sessionId)
if (m.type === "agentManager.addSessionToWorktree") return this.onAddSessionToWorktree(m.worktreeId)
if (m.type === "agentManager.addSessionToWorktree") return this.onAddSessionToWorktree(m.worktreeId, m.sessionId)
if (m.type === "agentManager.forkSession") return this.onForkSession(m.sessionId, m.worktreeId, m.messageId)
if (m.type === "agentManager.closeSession") return this.onCloseSession(m.sessionId)
}
@@ -418,6 +426,15 @@ export class AgentManagerProvider implements Disposable {
return null
}
if ((m.type === "sendMessage" || m.type === "sendCommand") && !m.sessionID) {
const ctx = typeof m.agentManagerContext === "string" ? m.agentManagerContext : undefined
const worktree = ctx && ctx !== "local" ? this.getStateManager()?.getWorktree(ctx) : undefined
if (worktree) {
if (m.draftID) this.activeSessionId = m.draftID
return { ...msg, contextDirectory: worktree.path }
}
}
if ((m.type === "sendMessage" || m.type === "sendCommand") && m.draftID && !m.sessionID) {
this.activeSessionId = m.draftID
return msg
@@ -1049,7 +1066,7 @@ export class AgentManagerProvider implements Disposable {
}
/** Add a new session to an existing worktree. */
private async onAddSessionToWorktree(worktreeId: string): Promise<null> {
private async onAddSessionToWorktree(worktreeId: string, sessionId?: string): Promise<null> {
let client: KiloClient
try {
client = this.connectionService.getClient()
@@ -1068,6 +1085,26 @@ export class AgentManagerProvider implements Disposable {
return null
}
if (sessionId) {
if (state.getSession(sessionId)) state.moveSession(sessionId, worktreeId)
else state.addSession(sessionId, worktreeId)
this.registerWorktreeSession(sessionId, worktree.path)
this.pushState()
this.postToWebview({
type: "agentManager.sessionAdded",
sessionId,
worktreeId,
})
this.host.capture("Agent Manager Session Started", {
source: PLATFORM,
sessionId,
worktreeId,
existing: true,
})
this.log(`Added existing session ${sessionId} to worktree ${worktreeId}`)
return null
}
let session: Session
try {
const { data } = await client.session.create(
@@ -1648,6 +1685,14 @@ export class AgentManagerProvider implements Disposable {
return this.panel?.sessions.getSessionDirectories() ?? new Map()
}
public getWorktreeDirectories(): string[] {
return (
this.getStateManager()
?.getWorktrees()
.map((wt) => wt.path) ?? []
)
}
/**
* Continue a sidebar session in a new worktree.
* Captures git state, creates worktree, applies state, forks session.
@@ -101,6 +101,7 @@ function createHarness() {
prBridge: { handleMessage: ReturnType<typeof vi.fn> }
activeSessionId: string | undefined
stateReady: Promise<void> | undefined
contextTarget: ReturnType<typeof vi.fn>
createWorktreeOnDisk: ReturnType<typeof vi.fn>
runSetupScriptForWorktree: ReturnType<typeof vi.fn>
createSessionInWorktree: ReturnType<typeof vi.fn>
@@ -121,6 +122,7 @@ function createHarness() {
manager.prBridge = { handleMessage: vi.fn().mockReturnValue(false) }
manager.activeSessionId = undefined
manager.stateReady = Promise.resolve()
manager.contextTarget = vi.fn()
manager.createWorktreeOnDisk = vi.fn()
manager.runSetupScriptForWorktree = vi.fn().mockResolvedValue(undefined)
manager.createSessionInWorktree = vi.fn()
@@ -202,4 +204,28 @@ describe("AgentManagerProvider worktree creation", () => {
expect(result).toEqual({ type: "requestFileSearch", query: "src", requestId: "r1", sessionID: "session-wt" })
})
it("resolves new sends to the selected worktree directory", async () => {
const manager = createHarness()
const state = {
getWorktree: vi.fn().mockReturnValue({ id: "wt-1", path: "/repo/.kilo/worktrees/wt-1" }),
}
manager.getStateManager.mockReturnValue(state)
manager.contextTarget.mockResolvedValue(undefined)
const result = await manager.onMessage({
type: "sendMessage",
text: "continue",
agentManagerContext: "wt-1",
draftID: "draft-1",
})
expect(result).toEqual({
type: "sendMessage",
text: "continue",
agentManagerContext: "wt-1",
draftID: "draft-1",
contextDirectory: "/repo/.kilo/worktrees/wt-1",
})
})
})
@@ -93,6 +93,7 @@ export interface Host {
*/
openPanel(opts: {
onBeforeMessage: (msg: Record<string, unknown>) => Promise<Record<string, unknown> | null>
worktreeDirectories?: () => string[]
}): PanelContext
/** Get the workspace/project root path. */
@@ -348,6 +348,7 @@ interface OpenLocallyIn {
interface AddSessionToWorktreeIn {
type: "agentManager.addSessionToWorktree"
worktreeId: string
sessionId?: string
}
interface CloseSessionIn {
@@ -606,6 +607,7 @@ interface SendMessageIn {
variant?: string
files?: Array<{ mime: string; url: string; filename?: string; source?: FileSourceIn }>
agentManagerContext?: string
contextDirectory?: string
}
interface SendCommandIn {
@@ -621,6 +623,7 @@ interface SendCommandIn {
variant?: string
files?: Array<{ mime: string; url: string; filename?: string; source?: FileSourceIn }>
agentManagerContext?: string
contextDirectory?: string
}
interface RequestTerminalContextIn {
@@ -9,6 +9,7 @@ import * as vscode from "vscode"
import type { Host, PanelContext, OutputHandle, SessionProvider, Disposable } from "./host"
import type { KiloConnectionService } from "../services/cli-backend"
import { KiloProvider } from "../KiloProvider"
import { PLATFORM } from "./constants"
import { DiffVirtualProvider } from "../DiffVirtualProvider"
import { buildWebviewHtml } from "../utils"
import { openFileInEditor, getWorkspaceRoot } from "../review-utils"
@@ -35,6 +36,7 @@ export class VscodeHost implements Host {
openPanel(opts: {
onBeforeMessage: (msg: Record<string, unknown>) => Promise<Record<string, unknown> | null>
worktreeDirectories?: () => string[]
}): PanelContext {
const panel = vscode.window.createWebviewPanel(
"kilo-code.new.AgentManagerPanel",
@@ -54,6 +56,7 @@ export class VscodeHost implements Host {
panel: vscode.WebviewPanel,
opts: {
onBeforeMessage: (msg: Record<string, unknown>) => Promise<Record<string, unknown> | null>
worktreeDirectories?: () => string[]
},
): PanelContext {
return this.wirePanel(panel, opts)
@@ -63,6 +66,7 @@ export class VscodeHost implements Host {
panel: vscode.WebviewPanel,
opts: {
onBeforeMessage: (msg: Record<string, unknown>) => Promise<Record<string, unknown> | null>
worktreeDirectories?: () => string[]
},
): PanelContext {
panel.webview.options = {
@@ -85,7 +89,9 @@ export class VscodeHost implements Host {
})
const provider = new KiloProvider(this.extensionUri, this.connectionService, this.context, {
platform: PLATFORM,
slimEditMetadata: true,
worktreeDirectories: () => opts.worktreeDirectories?.() ?? [],
})
if (this.diffVirtual) {
provider.setDiffVirtualProvider(this.diffVirtual)
+30 -1
View File
@@ -23,6 +23,14 @@ import { RemoteStatusService } from "./services/RemoteStatusService"
import { markWorkspace } from "./util/spotlight"
let agentManager: AgentManagerProvider | undefined
let shuttingDown = false
const RESTORE_KEY = "kilo.workbench.restore"
type RestoreState = {
sidebar?: boolean
agentManager?: boolean
}
const panelTitleHandler = (panel: vscode.WebviewPanel) => (title: string) => {
panel.title = title || EXTENSION_DISPLAY_NAME
@@ -34,11 +42,21 @@ const panelTitleHandler = (panel: vscode.WebviewPanel) => (title: string) => {
// it starts lazily when a webview connects or when ensureBackendForAutocomplete() triggers it.
export function activate(context: vscode.ExtensionContext) {
console.log("Kilo Code extension is now active")
shuttingDown = false
const telemetry = TelemetryProxy.getInstance()
// Create shared connection service (one server for all webviews)
const connectionService = new KiloConnectionService(context)
let restore = context.workspaceState.get<RestoreState>(RESTORE_KEY) ?? {}
const closeSidebar = restore.sidebar === false
const remember = (patch: RestoreState) => {
const next = { ...restore, ...patch }
if (shuttingDown && patch.sidebar === false) next.sidebar = restore.sidebar
if (shuttingDown && patch.agentManager === false) next.agentManager = restore.agentManager
restore = next
void context.workspaceState.update(RESTORE_KEY, restore)
}
// Create browser automation service (manages Playwright MCP registration)
const browserAutomationService = new BrowserAutomationService(connectionService)
@@ -105,7 +123,9 @@ export function activate(context: vscode.ExtensionContext) {
}
// Create the provider with shared service
const provider = new KiloProvider(context.extensionUri, connectionService, context)
const provider = new KiloProvider(context.extensionUri, connectionService, context, {
onSidebarVisibilityChange: (visible) => remember({ sidebar: visible }),
})
provider.setRemoteService(remoteService)
// Register the webview view provider for the sidebar.
@@ -115,6 +135,7 @@ export function activate(context: vscode.ExtensionContext) {
webviewOptions: { retainContextWhenHidden: true },
}),
)
if (closeSidebar) void vscode.commands.executeCommand("workbench.action.closeSidebar")
// Ensure Agent Manager navigation keybindings work when a VS Code terminal has focus.
// The terminal intercepts all keystrokes unless the command is listed in
@@ -131,6 +152,7 @@ export function activate(context: vscode.ExtensionContext) {
// Create Agent Manager provider for editor panel
const agentManagerHost = new VscodeHost(context.extensionUri, connectionService, context)
const agentManagerProvider = new AgentManagerProvider(agentManagerHost, connectionService)
agentManagerProvider.onPanelVisibilityChange((visible) => remember({ agentManager: visible }))
agentManager = agentManagerProvider
context.subscriptions.push(agentManagerProvider)
@@ -169,8 +191,13 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.window.registerWebviewPanelSerializer(AgentManagerProvider.viewType, {
deserializeWebviewPanel(panel: vscode.WebviewPanel) {
if (restore.agentManager === false) {
panel.dispose()
return Promise.resolve()
}
const ctx = agentManagerHost.wrapExistingPanel(panel, {
onBeforeMessage: (msg) => agentManagerProvider.handleMessage(msg),
worktreeDirectories: () => agentManagerProvider.getWorktreeDirectories(),
})
agentManagerProvider.deserializePanel(ctx)
return Promise.resolve()
@@ -452,6 +479,7 @@ export function activate(context: vscode.ExtensionContext) {
// Dispose services when extension deactivates (kills the server)
context.subscriptions.push({
dispose: () => {
shuttingDown = true
unsubscribeStateChange()
browserAutomationService.dispose()
provider.dispose()
@@ -461,6 +489,7 @@ export function activate(context: vscode.ExtensionContext) {
}
export async function deactivate() {
shuttingDown = true
await agentManager?.shutdown()
TelemetryProxy.getInstance().shutdown()
}
@@ -222,6 +222,7 @@ export interface SessionRefreshContext {
connectionState: "connecting" | "connected" | "disconnected" | "error"
listSessions: ((dir: string) => Promise<Session[]>) | null
sessionDirectories: Map<string, string>
worktreeDirectories?: () => string[]
workspaceDirectory: string
postMessage(message: unknown): void
}
@@ -245,7 +246,7 @@ export async function loadSessions(ctx: SessionRefreshContext): Promise<string |
const sessions = await list(ctx.workspaceDirectory)
const projectID = sessions[0]?.projectID
const worktreeDirs = new Set(ctx.sessionDirectories.values())
const worktreeDirs = new Set([...(ctx.worktreeDirectories?.() ?? []), ...ctx.sessionDirectories.values()])
const failed = new Set<string>()
const extra = await Promise.all(
[...worktreeDirs].map((dir) =>
@@ -339,6 +340,7 @@ export function resolveNewSessionDirectory(input: {
currentSessionID?: string
contextSessionID?: string
agentManagerContext?: string
contextDirectory?: string
sessionDirectories: Map<string, string>
workspaceDirectory: string
}) {
@@ -350,6 +352,8 @@ export function resolveNewSessionDirectory(input: {
})
}
if (input.contextDirectory) return input.contextDirectory
return resolveContextDirectory({
currentSessionID: input.currentSessionID,
contextSessionID: input.contextSessionID,
@@ -11,10 +11,16 @@ type Ctx = {
connection: KiloConnectionService
dir: string
post: (msg: unknown) => void
exportTranscript: (sessionID: string) => Promise<void>
}
export async function routeEarlyMessage(message: { type: string }, ctx: Ctx): Promise<boolean> {
await routeSuggestionWebviewMessage(ctx.question, message)
if (await ModelState.handleMessage(message.type, message, ctx.client, ctx.post)) return true
if (message.type === "exportSessionTranscript") {
const input = message as { sessionID?: unknown }
if (typeof input.sessionID === "string") await ctx.exportTranscript(input.sessionID)
return true
}
return await routeInputToolMessage(message, { connection: ctx.connection, dir: ctx.dir, post: ctx.post })
}
@@ -0,0 +1,58 @@
import * as path from "path"
import * as vscode from "vscode"
import type { KiloClient, Message, Part, Session } from "@kilocode/sdk/v2/client"
import { fetchMessagePage } from "./message-page"
type Item = {
info: Message
parts: Part[]
}
export async function exportTranscript(
client: KiloClient,
input: {
sessionID: string
dir: string
},
) {
const [{ data: session }, page] = await Promise.all([
client.session.get({ sessionID: input.sessionID, directory: input.dir }, { throwOnError: true }),
fetchMessagePage(client, { sessionID: input.sessionID, workspaceDir: input.dir, limit: 0 }),
])
const text = formatTranscript(session, page.items)
const uri = await vscode.window.showSaveDialog({
defaultUri: vscode.Uri.file(path.join(input.dir, `session-${session.id.slice(0, 8)}.md`)),
filters: { Markdown: ["md", "markdown"] },
saveLabel: "Export",
})
if (!uri) return false
await vscode.workspace.fs.writeFile(uri, Buffer.from(text, "utf8"))
return true
}
export function formatTranscript(session: Session, items: Item[]): string {
const head = [
`# ${session.title}`,
"",
`**Session ID:** ${session.id}`,
`**Created:** ${new Date(session.time.created).toLocaleString()}`,
`**Updated:** ${new Date(session.time.updated).toLocaleString()}`,
"",
"---",
"",
"",
].join("\n")
const body = items.map((item) => formatMessage(item)).join("---\n\n")
return `${head}${body}${items.length > 0 ? "---\n\n" : ""}`
}
function formatMessage(item: Item): string {
const head = item.info.role === "user" ? "## User\n\n" : "## Assistant\n\n"
return `${head}${item.parts.map((part) => formatPart(part)).join("")}`
}
function formatPart(part: Part): string {
if (part.type === "text" && !part.synthetic) return `${part.text}\n\n`
if (part.type === "tool") return `**Tool: ${part.tool}**\n\n`
return ""
}
@@ -1,5 +1,8 @@
export type KiloProviderOptions = {
projectDirectory?: string | null
platform?: string
slimEditMetadata?: boolean
tabTitle?: (title: string) => void
onSidebarVisibilityChange?: (visible: boolean) => void
worktreeDirectories?: () => string[]
}
@@ -29,6 +29,11 @@ type Audio = {
language?: string
}
type Args = {
pipe?: string[]
input: string[]
}
let active: Recording | undefined
let starting: string | undefined
@@ -119,13 +124,15 @@ async function waitForStart(state: Recording): Promise<void> {
})
}
async function startWithArgs(bin: string, file: string, input: Input, args: string[][]): Promise<Recording> {
async function startWithArgs(bin: string, file: string, input: Input, args: Args[]): Promise<Recording> {
const [first, ...rest] = args
if (!first) throw new Error(`Unsupported platform for speech input: ${process.platform}`)
const proc = spawn(bin, ["-y", ...first, "-acodec", "pcm_s16le", "-ar", "16000", "-ac", "1", "-f", "wav", file], {
stdio: ["pipe", "ignore", "pipe"],
})
const proc = first.pipe
? pipeProcess(first.pipe, bin, file)
: spawn(bin, ["-y", ...first.input, "-acodec", "pcm_s16le", "-ar", "16000", "-ac", "1", "-f", "wav", file], {
stdio: ["pipe", "ignore", "pipe"],
})
const state: Recording = { ...input, file, proc, stderr: [], stopped: false }
active = state
@@ -151,6 +158,48 @@ async function startWithArgs(bin: string, file: string, input: Input, args: stri
}
}
function pipeProcess(pipe: string[], bin: string, file: string): ChildProcess {
const source = spawn("pw-record", pipe, { stdio: ["ignore", "pipe", "pipe"] })
const proc = spawn(
bin,
[
"-y",
"-f",
"s16le",
"-ar",
"16000",
"-ac",
"1",
"-i",
"pipe:0",
"-acodec",
"pcm_s16le",
"-ar",
"16000",
"-ac",
"1",
"-f",
"wav",
file,
],
{
stdio: ["pipe", "ignore", "pipe"],
},
)
if (source.stdout && proc.stdin) source.stdout.pipe(proc.stdin)
source.on("error", (err) => proc.emit("error", err))
source.stderr?.on("data", (data: Buffer) => proc.stderr?.emit("data", data))
source.once("exit", () => {
if (proc.stdin?.writable) proc.stdin.end()
})
proc.once("exit", () => {
if (!source.killed) source.kill("SIGTERM")
})
return proc
}
async function stopProcess(state: Recording): Promise<void> {
if (state.proc.exitCode !== null || state.proc.signalCode) return
@@ -226,24 +275,33 @@ function platformPaths(): string[] {
return ["/usr/bin/ffmpeg", "/usr/local/bin/ffmpeg", "/snap/bin/ffmpeg", "/home/linuxbrew/.linuxbrew/bin/ffmpeg"]
}
async function inputArgSets(bin: string): Promise<string[][]> {
if (process.platform === "darwin") return [["-f", "avfoundation", "-i", ":default"]]
if (process.platform === "linux")
async function inputArgSets(bin: string): Promise<Args[]> {
if (process.platform === "darwin") return [{ input: ["-f", "avfoundation", "-i", ":default"] }]
if (process.platform === "linux") {
const device = process.env.KILO_FFMPEG_AUDIO_DEVICE
if (device)
return [
{ pipe: ["--target", device, "--format", "s16", "--rate", "16000", "--channels", "1", "-"], input: [] },
{ input: ["-f", "pulse", "-i", device] },
{ input: ["-f", "alsa", "-i", device] },
]
return [
["-f", "pulse", "-i", "default"],
["-f", "alsa", "-i", "default"],
{ pipe: ["--format", "s16", "--rate", "16000", "--channels", "1", "-"], input: [] },
{ input: ["-f", "pulse", "-i", "default"] },
{ input: ["-f", "alsa", "-i", "default"] },
]
}
if (process.platform === "win32") return await windowsInputArgSets(bin)
return []
}
async function windowsInputArgSets(bin: string): Promise<string[][]> {
async function windowsInputArgSets(bin: string): Promise<Args[]> {
const configured = process.env.KILO_FFMPEG_AUDIO_DEVICE
if (configured) return [["-f", "dshow", "-i", `audio=${configured}`]]
if (configured) return [{ input: ["-f", "dshow", "-i", `audio=${configured}`] }]
const devices = await listDshowAudioDevices(bin)
if (devices.length === 0) throw new Error("No Windows audio input devices found for speech input")
return devices.map((device) => ["-f", "dshow", "-i", `audio=${device}`])
return devices.map((device) => ({ input: ["-f", "dshow", "-i", `audio=${device}`] }))
}
async function listDshowAudioDevices(bin: string): Promise<string[]> {
@@ -266,11 +324,20 @@ function processOutput(err: unknown): string {
}
function summary(state: Recording, fallback: string): string {
const stderr = state.stderr.join("\n").trim()
const stderr = cleanOutput(state.stderr.join("\n"))
if (!stderr) return fallback
return `${fallback}: ${stderr.slice(-800)}`
}
export function cleanOutput(raw: string): string {
const lines = raw
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
.filter((line) => !/^(ffmpeg version|built with|configuration:|lib[a-z]+\s+\d)/i.test(line))
return lines.join("\n").trim()
}
async function removeFile(file: string): Promise<void> {
await unlink(file).catch((err: unknown) => {
console.warn("[Kilo New] Failed to remove speech recording", err)
@@ -2,6 +2,7 @@ export interface SpeechToTextModelDef {
readonly id: string
readonly label: string
readonly provider: string
readonly verbatim?: boolean
}
const models: SpeechToTextModelDef[] = [
@@ -9,11 +10,13 @@ const models: SpeechToTextModelDef[] = [
id: "openai/gpt-4o-mini-transcribe",
label: "GPT-4o Mini Transcribe",
provider: "OpenAI",
verbatim: true,
},
{
id: "openai/gpt-4o-transcribe",
label: "GPT-4o Transcribe",
provider: "OpenAI",
verbatim: true,
},
{
id: "openai/whisper-1",
@@ -3,6 +3,8 @@ import { getErrorMessage } from "../kilo-provider-utils"
import { getSpeechToTextModel } from "./models"
const PATH = "/kilo/audio/transcriptions"
const PROMPT =
"Transcribe exactly what is spoken. Do not paraphrase, summarize, infer intent, or rewrite for clarity. Preserve the speaker's original wording as closely as possible, including incomplete phrases and unusual wording when audible."
type Req = {
model?: string
@@ -39,6 +41,8 @@ export async function transcribeSpeech(
const auth = Buffer.from(`kilo:${cfg.password}`).toString("base64")
const url = new URL(PATH, cfg.baseUrl)
const model = getSpeechToTextModel(input.model)
const prompt = model.verbatim ? PROMPT : undefined
if (dir) url.searchParams.set("directory", dir)
try {
@@ -50,12 +54,13 @@ export async function transcribeSpeech(
"Content-Type": "application/json",
},
body: JSON.stringify({
model: getSpeechToTextModel(input.model).id,
model: model.id,
input_audio: {
data: input.data,
format: input.format,
},
...(input.language ? { language: input.language } : {}),
...(prompt ? { prompt } : {}),
}),
})
@@ -30,6 +30,7 @@ const TSX_FILES = [
path.join(ROOT, "webview-ui/agent-manager/DiffEndMarker.tsx"),
path.join(ROOT, "webview-ui/agent-manager/FileTree.tsx"),
path.join(ROOT, "webview-ui/agent-manager/review-annotations.ts"),
path.join(ROOT, "webview-ui/agent-manager/review-annotation-speech.tsx"),
path.join(ROOT, "webview-ui/agent-manager/MultiModelSelector.tsx"),
path.join(ROOT, "webview-ui/agent-manager/ApplyDialog.tsx"),
path.join(ROOT, "webview-ui/agent-manager/WorktreeItem.tsx"),
@@ -0,0 +1,58 @@
import { describe, expect, it } from "bun:test"
import { formatTranscript } from "../../src/kilo-provider/export-transcript"
describe("formatTranscript", () => {
it("formats complete Markdown session exports and skips hidden text", () => {
const text = formatTranscript(
{
id: "ses_123456789",
slug: "export-test",
projectID: "project",
directory: "/repo",
title: "Export test",
version: "1",
time: { created: 1_000, updated: 2_000 },
} as never,
[
{
info: { role: "user" },
parts: [
{ type: "text", text: "Please export this." },
{ type: "text", text: "hidden", synthetic: true },
],
},
{
info: { role: "assistant" },
parts: [
{ type: "tool", tool: "read" },
{ type: "text", text: "Saved as Markdown." },
],
},
] as never,
)
expect(text).toContain("# Export test")
expect(text).toContain("**Session ID:** ses_123456789")
expect(text).toContain("## User\n\nPlease export this.")
expect(text).toContain("## Assistant\n\n**Tool: read**\n\nSaved as Markdown.")
expect(text).not.toContain("hidden")
})
it("keeps an empty session export readable", () => {
const text = formatTranscript(
{
id: "ses_empty",
slug: "empty",
projectID: "project",
directory: "/repo",
title: "Empty",
version: "1",
time: { created: 1_000, updated: 2_000 },
} as never,
[],
)
expect(text).toContain("# Empty")
expect(text).toEndWith("---\n\n")
})
})
@@ -93,6 +93,17 @@ describe("resolveNewSessionDirectory", () => {
expect(dir).toBe("/repo/.kilo/worktrees/feature")
})
it("uses explicit Agent Manager worktree context after the active session was cleared", () => {
const dir = resolveNewSessionDirectory({
agentManagerContext: "wt_feature",
contextDirectory: "/repo/.kilo/worktrees/feature",
sessionDirectories: new Map(),
workspaceDirectory: "/repo",
})
expect(dir).toBe("/repo/.kilo/worktrees/feature")
})
it("creates local Agent Manager sessions in the workspace root after a worktree was selected", () => {
const dir = resolveNewSessionDirectory({
contextSessionID: "ses_worktree",
@@ -0,0 +1,20 @@
import { describe, expect, it } from "bun:test"
import { isAnnotationMutation } from "../../webview-ui/agent-manager/markdown-annotation-mutation"
function mutation(target: Node): Pick<MutationRecord, "target"> {
return { target }
}
describe("isAnnotationMutation", () => {
it("matches nested annotation DOM mutations", () => {
const target = { closest: () => ({}) } as unknown as Node
expect(isAnnotationMutation(mutation(target))).toBe(true)
})
it("keeps external Markdown DOM mutations observable", () => {
const markdown = { closest: () => null } as unknown as Node
const plain = {} as Node
expect(isAnnotationMutation(mutation(markdown))).toBe(false)
expect(isAnnotationMutation(mutation(plain))).toBe(false)
})
})
@@ -0,0 +1,105 @@
/**
* Tests for plan_exit webview helpers:
* - planDisplayPath: relative/absolute path display logic
* - plan_exit renderer uses openFile, not openDiff
*/
import { describe, expect, it } from "bun:test"
import { planDisplayPath } from "../../webview-ui/src/utils/plan-path"
import fs from "node:fs"
import path from "node:path"
describe("planDisplayPath", () => {
it("returns a relative path unchanged", () => {
expect(planDisplayPath(".kilo/plans/my-plan.md", "/repo")).toBe(".kilo/plans/my-plan.md")
})
it("returns absolute path inside repo as repo-relative", () => {
expect(planDisplayPath("/repo/.kilo/plans/my-plan.md", "/repo")).toBe(".kilo/plans/my-plan.md")
})
it("returns absolute path inside repo with trailing slash on root as repo-relative", () => {
expect(planDisplayPath("/repo/.kilo/plans/my-plan.md", "/repo/")).toBe(".kilo/plans/my-plan.md")
})
it("returns absolute path outside repo unchanged", () => {
expect(planDisplayPath("/other/path/plan.md", "/repo")).toBe("/other/path/plan.md")
})
it("handles Windows absolute paths inside root", () => {
expect(planDisplayPath("C:\\repo\\.kilo\\plans\\plan.md", "C:\\repo")).toBe(".kilo\\plans\\plan.md")
})
it("handles Windows absolute paths outside root", () => {
expect(planDisplayPath("D:\\other\\plan.md", "C:\\repo")).toBe("D:\\other\\plan.md")
})
it("returns empty string unchanged", () => {
expect(planDisplayPath("", "/repo")).toBe("")
})
it("path equal to root returns original", () => {
// Edge: plan path IS the root directory itself — fall back to original
expect(planDisplayPath("/repo", "/repo")).toBe("/repo")
})
})
describe("plan_exit renderer uses openFile not openDiff (source)", () => {
const ROOT = path.resolve(import.meta.dir, "../..")
const FILE = path.join(ROOT, "webview-ui/src/components/chat/AssistantMessage.tsx")
const TURN_FILE = path.join(ROOT, "webview-ui/src/components/chat/VscodeSessionTurn.tsx")
const src = fs.readFileSync(FILE, "utf-8")
const turnSrc = fs.readFileSync(TURN_FILE, "utf-8")
it("PlanExitCard calls data.openFile", () => {
expect(src).toContain("data.openFile")
})
it("uses a safe inert anchor href", () => {
expect(src).toContain('href="#"')
expect(src).not.toContain("href={display()}")
})
it("always uses the generic ready label", () => {
expect(src).toContain('language.t("plan.exit.ready")')
expect(src).not.toContain('language.t("plan.exit.readyUpdated")')
expect(src).not.toContain('language.t("plan.exit.readyNew")')
})
it("does not infer status from tool history", () => {
expect(src).not.toContain("function inferPlanStatus")
expect(src).not.toContain("function patchUpdatedPlan")
expect(src).not.toContain("function toolTouchesPlan")
expect(src).not.toContain("toolDeletions")
expect(src).not.toContain("readyUpdated")
expect(src).not.toContain("readyNew")
expect(src).not.toContain("Object.values(data.store.part ?? {}).flat()")
expect(src).not.toContain("[...props.parts, ...all()]")
expect(src).not.toContain("turnParts")
expect(turnSrc).not.toContain("assistantMessages().flatMap")
expect(turnSrc).not.toContain("turnParts={assistantParts()}")
})
it("does not depend on opencode-provided plan status metadata", () => {
expect(src).not.toContain('meta.status === "updated" || meta.status === "new"')
expect(src).not.toContain("meta.status")
})
it("PlanExitCard does not call openDiffVirtual", () => {
// Extract just the PlanExitCard function body to scope the assertion
const start = src.indexOf("function PlanExitCard")
const end = src.indexOf("\nfunction ", start + 1)
const block = end === -1 ? src.slice(start) : src.slice(start, end)
expect(block).not.toContain("openDiffVirtual")
expect(block).not.toContain("openDiff")
})
it("plan_exit tool is handled before generic Part renderer", () => {
const planExitIdx = src.indexOf("planExit()")
// <Part may be followed by newline or space
const partIdx = src.search(/<Part[\s\n]/)
expect(planExitIdx).toBeGreaterThan(0)
expect(partIdx).toBeGreaterThan(0)
expect(planExitIdx).toBeLessThan(partIdx)
})
})
@@ -8,6 +8,11 @@ import {
type ReviewComment,
} from "../../webview-ui/agent-manager/review-comments"
import { markdownCommentBlocks } from "../../webview-ui/agent-manager/markdown-comment-ranges"
import {
reviewAnnotationSpeechKey,
reviewDraftSpeechKey,
reviewEditSpeechKey,
} from "../../webview-ui/agent-manager/review-annotations"
import type { WorktreeFileDiff } from "../../webview-ui/src/types/messages"
function diff(file: string, before: string, after: string): WorktreeFileDiff {
@@ -236,6 +241,53 @@ describe("markdownCommentBlocks", () => {
})
})
// ── review annotation speech keys ───────────────────────────────────────────
describe("review annotation speech keys", () => {
it("keeps draft keys scoped to the selected review location", () => {
expect(reviewDraftSpeechKey({ file: "src/a.ts", side: "additions", line: 4 })).toBe("draft:src/a.ts:additions:4:4")
expect(reviewDraftSpeechKey({ file: "src/a.ts", side: "deletions", line: 4, endLine: 8 })).toBe(
"draft:src/a.ts:deletions:4:8",
)
})
it("keeps edit keys scoped to the review comment id", () => {
expect(reviewEditSpeechKey("c-123")).toBe("edit:c-123")
})
it("only exposes annotation speech keys for active draft and edit composers", () => {
const current = comment({ file: "src/a.ts", line: 7 })
expect(
reviewAnnotationSpeechKey({
type: "draft",
comment: null,
file: "src/a.ts",
side: "additions",
line: 7,
}),
).toBe("draft:src/a.ts:additions:7:7")
expect(
reviewAnnotationSpeechKey({
type: "comment",
comment: current,
file: current.file,
side: current.side,
line: current.line,
editing: true,
}),
).toBe(`edit:${current.id}`)
expect(
reviewAnnotationSpeechKey({
type: "comment",
comment: current,
file: current.file,
side: current.side,
line: current.line,
}),
).toBeUndefined()
})
})
// ── getDirectory / getFilename ──────────────────────────────────────────────
describe("getDirectory", () => {
@@ -1,5 +1,5 @@
import { describe, expect, it } from "bun:test"
import { parseDshowAudioDevices } from "../../src/speech-to-text/capture"
import { cleanOutput, parseDshowAudioDevices } from "../../src/speech-to-text/capture"
describe("parseDshowAudioDevices", () => {
it("extracts Windows dshow audio device names", () => {
@@ -20,3 +20,20 @@ describe("parseDshowAudioDevices", () => {
expect(parseDshowAudioDevices(raw)).toEqual(["Microphone"])
})
})
describe("cleanOutput", () => {
it("removes ffmpeg build noise from capture errors", () => {
const raw = `
ffmpeg version 4.2.7 Copyright (c) 2000-2022 the FFmpeg developers
built with gcc 9 (Ubuntu 9.4.0-1ubuntu1~20.04.2)
configuration: --enable-libopus --enable-libx264
libavutil 56. 31.100 / 56. 31.100
ALSA lib ../../../src/pcm/pcm.c:2477:(snd_pcm_open_conf) Unknown field libs
default: Input/output error
`
expect(cleanOutput(raw)).toBe(
"ALSA lib ../../../src/pcm/pcm.c:2477:(snd_pcm_open_conf) Unknown field libs\ndefault: Input/output error",
)
})
})
@@ -0,0 +1,89 @@
import { describe, expect, it, mock } from "bun:test"
import { createRoot } from "solid-js"
import type { ExtensionMessage, WebviewMessage } from "../../webview-ui/src/types/messages"
mock.module("@kilocode/kilo-ui/toast", () => ({
showToast: () => undefined,
}))
const { useSpeechToText } = await import("../../webview-ui/src/components/speech-to-text/useSpeechToText")
function setup() {
const sent: WebviewMessage[] = []
let handler: ((message: ExtensionMessage) => void) | undefined
const root = createRoot((dispose) => ({
dispose,
speech: useSpeechToText(
{
postMessage: (message) => sent.push(message),
onMessage: (next) => {
handler = next
return () => {
handler = undefined
}
},
},
{ profileData: () => ({}), goToLogin: () => {} },
{ t: (key) => key },
),
}))
const fire = (message: ExtensionMessage) => handler?.(message)
return { ...root, fire, sent }
}
describe("useSpeechToText", () => {
it("runs the stop completion after inserting a transcript", () => {
const ctx = setup()
const text: string[] = []
let done = 0
ctx.speech.start({ model: "scribe", insert: (value) => text.push(value) })
const start = ctx.sent[0]
if (start?.type !== "speechToTextStart") throw new Error("speech start message missing")
ctx.speech.stop({ done: () => done++ })
ctx.fire({ type: "speechToTextResult", requestId: start.requestId, text: "Recorded prompt" })
expect(text).toEqual(["Recorded prompt"])
expect(done).toBe(1)
expect(ctx.speech.state()).toBe("idle")
ctx.dispose()
})
it("drops the stop completion when transcription is cancelled", () => {
const ctx = setup()
let done = 0
ctx.speech.start({ model: "scribe", insert: () => {} })
const start = ctx.sent[0]
if (start?.type !== "speechToTextStart") throw new Error("speech start message missing")
ctx.speech.stop({ done: () => done++ })
ctx.speech.cancel()
ctx.fire({ type: "speechToTextResult", requestId: start.requestId, text: "Ignored prompt" })
expect(done).toBe(0)
expect(ctx.speech.state()).toBe("idle")
ctx.dispose()
})
it("drops the stop completion when the send context changes", () => {
const ctx = setup()
const text: string[] = []
let done = 0
ctx.speech.start({ model: "scribe", insert: (value) => text.push(value) })
const start = ctx.sent[0]
if (start?.type !== "speechToTextStart") throw new Error("speech start message missing")
ctx.speech.stop({ done: () => done++, ready: () => false })
ctx.fire({ type: "speechToTextResult", requestId: start.requestId, text: "Keep as draft" })
expect(text).toEqual(["Keep as draft"])
expect(done).toBe(0)
expect(ctx.speech.state()).toBe("idle")
ctx.dispose()
})
})
@@ -130,6 +130,7 @@ import { createMarkdownRender } from "./review-preferences"
import { createSidebarCollapse } from "./sidebar-collapse"
import { SidebarToggleButton } from "./SidebarToggleButton"
import { setTabWidths } from "./tab-widths"
import { buildShortcutCategories } from "./shortcuts"
import "./agent-manager.css"
import "./agent-manager-review.css"
const REVIEW_TAB_ID = "review"
@@ -185,76 +186,6 @@ const defaultBindings: Record<string, string> = {
),
}
/** Shortcut category definition for the keyboard shortcuts dialog */
interface ShortcutEntry {
label: string
binding: string
}
interface ShortcutCategory {
title: string
shortcuts: ShortcutEntry[]
}
/** Build the categorized list of keyboard shortcuts from the current bindings */
function buildShortcutCategories(
bindings: Record<string, string>,
t: (key: string, params?: Record<string, string | number>) => string,
): ShortcutCategory[] {
return [
{
title: t("agentManager.shortcuts.category.quickSwitch"),
shortcuts: [
{
label: t("agentManager.shortcuts.jumpToItem"),
binding: (() => {
const first = bindings.jumpTo1 ?? ""
const prefix = first.replace(/\d+$/, "")
return prefix ? `${prefix}1-9` : ""
})(),
},
],
},
{
title: t("agentManager.shortcuts.category.sidebar"),
shortcuts: [
{ label: t("agentManager.shortcuts.previousItem"), binding: bindings.previousSession ?? "" },
{ label: t("agentManager.shortcuts.nextItem"), binding: bindings.nextSession ?? "" },
{ label: t("agentManager.shortcuts.newWorktree"), binding: bindings.newWorktree ?? "" },
{ label: t("agentManager.shortcuts.advancedWorktree"), binding: bindings.advancedWorktree ?? "" },
{ label: t("agentManager.shortcuts.deleteWorktree"), binding: bindings.closeWorktree ?? "" },
{ label: t("agentManager.shortcuts.openWorktree"), binding: bindings.openWorktree ?? "" },
],
},
{
title: t("agentManager.shortcuts.category.tabs"),
shortcuts: [
{ label: t("agentManager.shortcuts.previousTab"), binding: bindings.previousTab ?? "" },
{ label: t("agentManager.shortcuts.nextTab"), binding: bindings.nextTab ?? "" },
{ label: t("agentManager.shortcuts.newTab"), binding: bindings.newTab ?? "" },
{ label: t("agentManager.shortcuts.closeTab"), binding: bindings.closeTab ?? "" },
],
},
{
title: t("agentManager.shortcuts.category.terminal"),
shortcuts: [
{ label: t("agentManager.shortcuts.toggleTerminal"), binding: bindings.showTerminal ?? "" },
{ label: t("agentManager.shortcuts.runScript"), binding: bindings.runScript ?? "" },
{ label: t("agentManager.shortcuts.toggleDiff"), binding: bindings.toggleDiff ?? "" },
],
},
{
title: t("agentManager.shortcuts.category.global"),
shortcuts: [
{ label: t("agentManager.shortcuts.openAgentManager"), binding: bindings.agentManagerOpen ?? "" },
{ label: t("agentManager.shortcuts.cycleAgentMode"), binding: bindings.cycleAgentMode ?? "" },
{ label: t("agentManager.shortcuts.cyclePreviousAgentMode"), binding: bindings.cyclePreviousAgentMode ?? "" },
{ label: t("agentManager.shortcuts.showShortcuts"), binding: bindings.showShortcuts ?? "" },
].filter((s) => s.binding),
},
].filter((c) => c.shortcuts.length > 0)
}
import { parseBindingTokens } from "./keybind-tokens"
const AgentManagerContent: Component = () => {
@@ -1003,6 +934,27 @@ const AgentManagerContent: Component = () => {
setReviewActive(remembered === REVIEW_TAB_ID && reviewOpenByContext()[worktreeId] === true)
}
const addSessionToCurrentWorktree = (sid: string) => {
const sel = selection()
if (!sel || sel === LOCAL) return false
const current = managedSessions().find((entry) => entry.id === sid)
if (current?.worktreeId) return focusManagedSession(current.worktreeId, sid)
saveTabMemory()
setHistory(false)
setReviewActive(false)
appendToTabOrder(sel, sid)
evictLocal(sid)
vscode.postMessage({ type: "agentManager.addSessionToWorktree", worktreeId: sel, sessionId: sid })
return true
}
const focusManagedSession = (worktreeId: string, sid: string) => {
selectWorktree(worktreeId)
setHistory(false)
session.selectSession(sid)
return true
}
const cycleAgent = (direction: 1 | -1) => {
const available = session.agents().filter((a) => a.mode !== "subagent" && !a.hidden)
if (available.length <= 1) return
@@ -1220,6 +1172,7 @@ const AgentManagerContent: Component = () => {
saveTabMemory()
appendToTabOrder(ev.worktreeId, ev.sessionId)
setSelection(ev.worktreeId)
evictLocal(ev.sessionId)
drafts.apply(ev.worktreeId, ev.sessionId)
session.selectSession(ev.sessionId)
}
@@ -2988,6 +2941,7 @@ const AgentManagerContent: Component = () => {
<Show when={history()}>
<HistoryView
onSelectSession={(id) => {
if (addSessionToCurrentWorktree(id)) return
setHistory(false)
if (localSessionIDs().includes(id)) {
saveTabMemory()
@@ -3021,6 +2975,7 @@ const AgentManagerContent: Component = () => {
<div class="am-chat-wrapper">
<ChatView
onSelectSession={(id) => {
if (addSessionToCurrentWorktree(id)) return
if (localSessionIDs().includes(id)) {
session.selectSession(id)
if (selection() === null) setSelection(LOCAL)
@@ -14,13 +14,22 @@ import type { DiffLineAnnotation, AnnotationSide, SelectedLineRange } from "@pie
import type { WorktreeFileDiff } from "../src/types/messages"
import { KILO_FILE_PATH_MIME } from "../src/utils/path-mentions"
import { useLanguage } from "../src/context/language"
import { useVSCode } from "../src/context/vscode"
import { useServer } from "../src/context/server"
import { useProvider } from "../src/context/provider"
import { useConfig } from "../src/context/config"
import { canUseSpeechToText, selectedSpeechToTextModel } from "../src/components/speech-to-text/availability"
import { useSpeechToText } from "../src/components/speech-to-text/useSpeechToText"
import { getDirectory, getFilename, lineCount, sanitizeReviewComments, type ReviewComment } from "./review-comments"
import {
buildFileAnnotations,
buildReviewAnnotation,
reviewDraftSpeechKey,
reviewEditSpeechKey,
type AnnotationLabels,
type AnnotationMeta,
} from "./review-annotations"
import { createReviewAnnotationSpeechRenderer } from "./review-annotation-speech"
import {
LONG_DIFF_MARKER_FILE_COUNT,
allOpenFiles,
@@ -59,6 +68,13 @@ interface DiffPanelProps {
export const DiffPanel: Component<DiffPanelProps> = (props) => {
const { t } = useLanguage()
const vscode = useVSCode()
const server = useServer()
const provider = useProvider()
const { config, settings } = useConfig()
const speech = useSpeechToText(vscode, server, { t })
const canUseSpeech = () => canUseSpeechToText(settings(), config(), provider.connected(), server.profileData())
const speechModel = () => selectedSpeechToTextModel(settings())
const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent)
const sendAllKeybind = () =>
isMac ? t("agentManager.review.sendAllShortcut.mac") : t("agentManager.review.sendAllShortcut.other")
@@ -78,6 +94,21 @@ export const DiffPanel: Component<DiffPanelProps> = (props) => {
null,
)
const [editing, setEditing] = createSignal<string | null>(null)
const speechKeys = createMemo(() => {
const keys = new Set<string>()
const current = draft()
const edit = editing()
if (current) keys.add(reviewDraftSpeechKey(current))
if (edit) keys.add(reviewEditSpeechKey(edit))
return keys
})
const reviewSpeech = createReviewAnnotationSpeechRenderer({
speech,
enabled: canUseSpeech,
model: speechModel,
label: t,
keys: speechKeys,
})
let nextId = 0
// Tracks the session key for which initial open state has already run. When the
// key changes (different worktree) we expand reviewable files. Within the same key,
@@ -307,6 +338,7 @@ export const DiffPanel: Component<DiffPanelProps> = (props) => {
cancelDraft,
labels: labels(),
activeTerminalId: props.activeTerminalId,
speech: reviewSpeech,
})
}
@@ -20,15 +20,24 @@ import type { DiffLineAnnotation, AnnotationSide, SelectedLineRange } from "@pie
import type { WorktreeFileDiff } from "../src/types/messages"
import { KILO_FILE_PATH_MIME } from "../src/utils/path-mentions"
import { useLanguage } from "../src/context/language"
import { useVSCode } from "../src/context/vscode"
import { useServer } from "../src/context/server"
import { useProvider } from "../src/context/provider"
import { useConfig } from "../src/context/config"
import { canUseSpeechToText, selectedSpeechToTextModel } from "../src/components/speech-to-text/availability"
import { useSpeechToText } from "../src/components/speech-to-text/useSpeechToText"
import { FileTree } from "./FileTree"
import { treeOrder } from "./file-tree-utils"
import { getDirectory, getFilename, lineCount, sanitizeReviewComments, type ReviewComment } from "./review-comments"
import {
buildFileAnnotations,
buildReviewAnnotation,
reviewDraftSpeechKey,
reviewEditSpeechKey,
type AnnotationLabels,
type AnnotationMeta,
} from "./review-annotations"
import { createReviewAnnotationSpeechRenderer } from "./review-annotation-speech"
import {
LONG_DIFF_MARKER_FILE_COUNT,
allOpenFiles,
@@ -69,6 +78,13 @@ interface FullScreenDiffViewProps {
export const FullScreenDiffView: Component<FullScreenDiffViewProps> = (props) => {
const { t } = useLanguage()
const vscode = useVSCode()
const server = useServer()
const provider = useProvider()
const { config, settings } = useConfig()
const speech = useSpeechToText(vscode, server, { t })
const canUseSpeech = () => canUseSpeechToText(settings(), config(), provider.connected(), server.profileData())
const speechModel = () => selectedSpeechToTextModel(settings())
const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent)
const sendAllKeybind = () =>
isMac ? t("agentManager.review.sendAllShortcut.mac") : t("agentManager.review.sendAllShortcut.other")
@@ -88,6 +104,21 @@ export const FullScreenDiffView: Component<FullScreenDiffViewProps> = (props) =>
null,
)
const [editing, setEditing] = createSignal<string | null>(null)
const speechKeys = createMemo(() => {
const keys = new Set<string>()
const current = draft()
const edit = editing()
if (current) keys.add(reviewDraftSpeechKey(current))
if (edit) keys.add(reviewEditSpeechKey(edit))
return keys
})
const reviewSpeech = createReviewAnnotationSpeechRenderer({
speech,
enabled: canUseSpeech,
model: speechModel,
label: t,
keys: speechKeys,
})
const [activeFile, setActiveFile] = createSignal<string | null>(null)
const [treeWidth, setTreeWidth] = createSignal(240)
let nextId = 0
@@ -322,6 +353,7 @@ export const FullScreenDiffView: Component<FullScreenDiffViewProps> = (props) =>
cancelDraft,
labels: labels(),
activeTerminalId: props.activeTerminalId,
speech: reviewSpeech,
})
}
@@ -2,6 +2,7 @@ import { type Component, createEffect, createMemo, onCleanup } from "solid-js"
import type { AnnotationSide, DiffLineAnnotation, SelectedLineRange } from "@pierre/diffs"
import { markdownCommentBlocks, type MarkdownRange } from "./markdown-comment-ranges"
import type { AnnotationMeta } from "./review-annotations"
import { annotationSelector, isAnnotationMutation } from "./markdown-annotation-mutation"
type Insert = "after" | "list" | "table"
@@ -88,11 +89,11 @@ function matches(annotation: DiffLineAnnotation<AnnotationMeta>, anchor: Anchor,
return true
}
const selector = annotationSelector()
function removeInserted(root: HTMLElement, layer: HTMLElement): void {
layer.replaceChildren()
root
.querySelectorAll(".am-markdown-inline-annotations, .am-markdown-list-annotation, .am-markdown-table-annotation")
.forEach((node) => node.remove())
root.querySelectorAll(selector).forEach((node) => node.remove())
}
function insertHost(anchor: Anchor, host: HTMLElement): void {
@@ -206,7 +207,10 @@ export const MarkdownAnnotationLayer: Component<MarkdownAnnotationLayerProps> =
const root = props.root()
if (!root) return
observer?.disconnect()
observer = new MutationObserver(schedule)
observer = new MutationObserver((mutations) => {
if (mutations.every(isAnnotationMutation)) return
schedule()
})
observer.observe(root, { childList: true, subtree: true })
})

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