diff --git a/.changeset/agent-manager-collapsible-sidebar.md b/.changeset/agent-manager-collapsible-sidebar.md deleted file mode 100644 index 921f175b225..00000000000 --- a/.changeset/agent-manager-collapsible-sidebar.md +++ /dev/null @@ -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. diff --git a/.changeset/agent-manager-prompt-picker.md b/.changeset/agent-manager-prompt-picker.md new file mode 100644 index 00000000000..5d88c9825ad --- /dev/null +++ b/.changeset/agent-manager-prompt-picker.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Keep long New Worktree prompts clear of selector controls and show inline model picker options without clipping. diff --git a/.changeset/agent-manager-state-exclude.md b/.changeset/agent-manager-state-exclude.md deleted file mode 100644 index 43f9c067e14..00000000000 --- a/.changeset/agent-manager-state-exclude.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Keep Agent Manager state hidden from Git before creating local sessions. diff --git a/.changeset/bright-checkpoints-graph.md b/.changeset/bright-checkpoints-graph.md deleted file mode 100644 index c9812a3ca69..00000000000 --- a/.changeset/bright-checkpoints-graph.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Keep VS Code session timeline, token, context, and cost stats in sync after restoring a checkpoint. diff --git a/.changeset/bright-searches-count.md b/.changeset/bright-searches-count.md new file mode 100644 index 00000000000..d518bc898d4 --- /dev/null +++ b/.changeset/bright-searches-count.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Capture aggregate usage telemetry for experimental Morph-backed codebase search. diff --git a/.changeset/codex-chatgpt-reauth.md b/.changeset/codex-chatgpt-reauth.md deleted file mode 100644 index 50ba4d3fd1c..00000000000 --- a/.changeset/codex-chatgpt-reauth.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"kilo-code": patch -"@kilocode/cli": patch ---- - -Show ChatGPT sign-in again when Codex authentication expires. diff --git a/.changeset/continue-worktree-sessions.md b/.changeset/continue-worktree-sessions.md new file mode 100644 index 00000000000..95a49a2b807 --- /dev/null +++ b/.changeset/continue-worktree-sessions.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Keep continued Agent Manager sessions in the selected worktree instead of moving them to Local. diff --git a/.changeset/copy-tooltip-top.md b/.changeset/copy-tooltip-top.md deleted file mode 100644 index ded0f1a640d..00000000000 --- a/.changeset/copy-tooltip-top.md +++ /dev/null @@ -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. diff --git a/.changeset/fix-tui-diff-hunk-headers.md b/.changeset/fix-tui-diff-hunk-headers.md deleted file mode 100644 index 2406a74b89c..00000000000 --- a/.changeset/fix-tui-diff-hunk-headers.md +++ /dev/null @@ -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. diff --git a/.changeset/fresh-snapshot-cli.md b/.changeset/fresh-snapshot-cli.md deleted file mode 100644 index 4bdd7e6cf27..00000000000 --- a/.changeset/fresh-snapshot-cli.md +++ /dev/null @@ -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. diff --git a/.changeset/hide-subagent-sessions.md b/.changeset/hide-subagent-sessions.md deleted file mode 100644 index eb7a95948e5..00000000000 --- a/.changeset/hide-subagent-sessions.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Hide Task tool subagent sessions from Agent Manager session lists. diff --git a/.changeset/inline-review-speech.md b/.changeset/inline-review-speech.md new file mode 100644 index 00000000000..30cf6eeb87b --- /dev/null +++ b/.changeset/inline-review-speech.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Support speech-to-text voice input in Agent Manager inline review comments. diff --git a/.changeset/local-worktree-revert-diff.md b/.changeset/local-worktree-revert-diff.md deleted file mode 100644 index 26c094336b3..00000000000 --- a/.changeset/local-worktree-revert-diff.md +++ /dev/null @@ -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. diff --git a/.changeset/manual-compact-limit.md b/.changeset/manual-compact-limit.md deleted file mode 100644 index c4d823ca617..00000000000 --- a/.changeset/manual-compact-limit.md +++ /dev/null @@ -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. diff --git a/.changeset/markdown-review-voice.md b/.changeset/markdown-review-voice.md new file mode 100644 index 00000000000..d883a488ca4 --- /dev/null +++ b/.changeset/markdown-review-voice.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Keep inline review voice capture stable in rendered Markdown diffs. diff --git a/.changeset/mermaid-security-fixes.md b/.changeset/mermaid-security-fixes.md new file mode 100644 index 00000000000..4a9947d7248 --- /dev/null +++ b/.changeset/mermaid-security-fixes.md @@ -0,0 +1,6 @@ +--- +"@kilocode/cli": patch +"@kilocode/kilo-docs": patch +--- + +Harden Mermaid diagram rendering with upstream security fixes. diff --git a/.changeset/model-picker-search-focus.md b/.changeset/model-picker-search-focus.md deleted file mode 100644 index 33e7b6a635f..00000000000 --- a/.changeset/model-picker-search-focus.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Keep the New Worktree model picker open when interacting with its search field. diff --git a/.changeset/persist-chat-tool-cards.md b/.changeset/persist-chat-tool-cards.md deleted file mode 100644 index 4e88b114edf..00000000000 --- a/.changeset/persist-chat-tool-cards.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"kilo-code": patch -"@kilocode/kilo-ui": patch ---- - -Keep chat tool cards open or closed when streaming updates remount them. diff --git a/.changeset/placeholder-vscode-token.md b/.changeset/placeholder-vscode-token.md deleted file mode 100644 index 4c4ed7a085b..00000000000 --- a/.changeset/placeholder-vscode-token.md +++ /dev/null @@ -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.). diff --git a/.changeset/reset-reverted-todos.md b/.changeset/reset-reverted-todos.md deleted file mode 100644 index 03cfcb3a4b3..00000000000 --- a/.changeset/reset-reverted-todos.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Reset the To-dos tab when reverting a session snapshot. diff --git a/.changeset/shell-syntax-highlighting.md b/.changeset/shell-syntax-highlighting.md deleted file mode 100644 index 038f51dd685..00000000000 --- a/.changeset/shell-syntax-highlighting.md +++ /dev/null @@ -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. diff --git a/.changeset/shiny-sessions-export.md b/.changeset/shiny-sessions-export.md new file mode 100644 index 00000000000..57a07807cae --- /dev/null +++ b/.changeset/shiny-sessions-export.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Export full VS Code session transcripts as Markdown files. diff --git a/.changeset/silent-mermaid-labels.md b/.changeset/silent-mermaid-labels.md deleted file mode 100644 index 0bb5b38ac2d..00000000000 --- a/.changeset/silent-mermaid-labels.md +++ /dev/null @@ -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. diff --git a/.changeset/smart-views-restore.md b/.changeset/smart-views-restore.md new file mode 100644 index 00000000000..8cfb0295f83 --- /dev/null +++ b/.changeset/smart-views-restore.md @@ -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. diff --git a/.changeset/speech-input-gateway.md b/.changeset/speech-input-gateway.md deleted file mode 100644 index cf48a6b19bb..00000000000 --- a/.changeset/speech-input-gateway.md +++ /dev/null @@ -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. diff --git a/.changeset/swift-voices-send.md b/.changeset/swift-voices-send.md new file mode 100644 index 00000000000..6ee05869f9b --- /dev/null +++ b/.changeset/swift-voices-send.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Send recorded voice prompts directly when the prompt send button is pressed. diff --git a/.changeset/tame-responses-items.md b/.changeset/tame-responses-items.md deleted file mode 100644 index c08aa6f029a..00000000000 --- a/.changeset/tame-responses-items.md +++ /dev/null @@ -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. diff --git a/.changeset/terminals-split-diff-sidebar.md b/.changeset/terminals-split-diff-sidebar.md deleted file mode 100644 index 8474c11fc35..00000000000 --- a/.changeset/terminals-split-diff-sidebar.md +++ /dev/null @@ -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. diff --git a/.changeset/vscode-mode-model-sync.md b/.changeset/vscode-mode-model-sync.md deleted file mode 100644 index 142de1d15ab..00000000000 --- a/.changeset/vscode-mode-model-sync.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Keep configured mode models in sync when switching modes in VS Code. diff --git a/.github/workflows/publish-jetbrains.yml b/.github/workflows/publish-jetbrains.yml new file mode 100644 index 00000000000..4db34d33be8 --- /dev/null +++ b/.github/workflows/publish-jetbrains.yml @@ -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." >&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 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 559d65f0a9f..0af05b88e17 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 diff --git a/README.md b/README.md index afbb34ef673..81bc35bbd55 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/bun.lock b/bun.lock index 76cd6a758fc..2314b1d6c8a 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], diff --git a/bunfig.toml b/bunfig.toml index 0fde17500d1..25923129a8c 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -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" diff --git a/nix/hashes.json b/nix/hashes.json index 3aa8ec21fe0..585b1d7461e 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -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=" } } diff --git a/package.json b/package.json index f0e3e0d9508..9d074301c1b 100644 --- a/package.json +++ b/package.json @@ -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": {} } diff --git a/packages/core/package.json b/packages/core/package.json index 32ffc2df5ba..9f1fd76ff76 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 2a67ecece50..6087a11fe55 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "kilo" name = "Kilo" description = "The open source coding agent." -version = "7.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"] diff --git a/packages/kilo-docs/package.json b/packages/kilo-docs/package.json index 298272242b5..86b1b174dad 100644 --- a/packages/kilo-docs/package.json +++ b/packages/kilo-docs/package.json @@ -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", diff --git a/packages/kilo-docs/pages/code-with-ai/agents/chat-interface.md b/packages/kilo-docs/pages/code-with-ai/agents/chat-interface.md index 04481e37130..3708f9c8095 100644 --- a/packages/kilo-docs/pages/code-with-ai/agents/chat-interface.md +++ b/packages/kilo-docs/pages/code-with-ai/agents/chat-interface.md @@ -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" %} diff --git a/packages/kilo-docs/pages/code-with-ai/platforms/vscode/index.md b/packages/kilo-docs/pages/code-with-ai/platforms/vscode/index.md index 6bc15d57ba0..9c3cfb5c07d 100644 --- a/packages/kilo-docs/pages/code-with-ai/platforms/vscode/index.md +++ b/packages/kilo-docs/pages/code-with-ai/platforms/vscode/index.md @@ -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 diff --git a/packages/kilo-gateway/package.json b/packages/kilo-gateway/package.json index bd51095255b..6aef452b3c4 100644 --- a/packages/kilo-gateway/package.json +++ b/packages/kilo-gateway/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/kilo-gateway", - "version": "7.2.52", + "version": "7.3.0", "type": "module", "license": "MIT", "description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration", diff --git a/packages/kilo-gateway/src/api/constants.ts b/packages/kilo-gateway/src/api/constants.ts index 44acecfa75d..8c556af15f0 100644 --- a/packages/kilo-gateway/src/api/constants.ts +++ b/packages/kilo-gateway/src/api/constants.ts @@ -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" diff --git a/packages/kilo-gateway/src/headers.ts b/packages/kilo-gateway/src/headers.ts index e2446ae2353..aaddc274cb0 100644 --- a/packages/kilo-gateway/src/headers.ts +++ b/packages/kilo-gateway/src/headers.ts @@ -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 diff --git a/packages/kilo-gateway/src/index.ts b/packages/kilo-gateway/src/index.ts index cfd97e06e90..e84cb0c7dcf 100644 --- a/packages/kilo-gateway/src/index.ts +++ b/packages/kilo-gateway/src/index.ts @@ -100,6 +100,7 @@ export { MODELS_FETCH_TIMEOUT_MS, HEADER_ORGANIZATIONID, HEADER_TASKID, + HEADER_PARENT_TASKID, HEADER_PROJECTID, HEADER_TESTER, HEADER_EDITORNAME, diff --git a/packages/kilo-gateway/src/provider-debug.ts b/packages/kilo-gateway/src/provider-debug.ts index 57d779f7f38..f97bcbb7cdc 100644 --- a/packages/kilo-gateway/src/provider-debug.ts +++ b/packages/kilo-gateway/src/provider-debug.ts @@ -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) { diff --git a/packages/kilo-gateway/src/provider.ts b/packages/kilo-gateway/src/provider.ts index 21fc818cc26..c36551469e6 100644 --- a/packages/kilo-gateway/src/provider.ts +++ b/packages/kilo-gateway/src/provider.ts @@ -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, 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}`) diff --git a/packages/kilo-gateway/src/server/routes.ts b/packages/kilo-gateway/src/server/routes.ts index f2cd17498fa..f92c00f3f31 100644 --- a/packages/kilo-gateway/src/server/routes.ts +++ b/packages/kilo-gateway/src/server/routes.ts @@ -429,6 +429,7 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { format: z.string(), }), language: z.string().optional(), + prompt: z.string().optional(), temperature: z.number().optional(), }), ), diff --git a/packages/kilo-gateway/test/provider.test.ts b/packages/kilo-gateway/test/provider.test.ts new file mode 100644 index 00000000000..34f468a80b0 --- /dev/null +++ b/packages/kilo-gateway/test/provider.test.ts @@ -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") + }) +}) diff --git a/packages/kilo-i18n/package.json b/packages/kilo-i18n/package.json index 9e86f83af9e..581dd55bb85 100644 --- a/packages/kilo-i18n/package.json +++ b/packages/kilo-i18n/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/kilo-i18n", - "version": "7.2.52", + "version": "7.3.0", "type": "module", "license": "MIT", "description": "Kilo-specific i18n translations and overrides", diff --git a/packages/kilo-indexing/package.json b/packages/kilo-indexing/package.json index 0fe3b960876..da79c2e85db 100644 --- a/packages/kilo-indexing/package.json +++ b/packages/kilo-indexing/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/kilo-indexing", - "version": "7.2.52", + "version": "7.3.0", "type": "module", "license": "MIT", "description": "Standalone indexing engine and host helpers for Kilo Code", diff --git a/packages/kilo-jetbrains/README.md b/packages/kilo-jetbrains/README.md index 56fbf578a11..87e89b664c5 100644 --- a/packages/kilo-jetbrains/README.md +++ b/packages/kilo-jetbrains/README.md @@ -52,6 +52,12 @@ The built plugin archive is at `build/distributions/kilo.jetbrains-.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. diff --git a/packages/kilo-jetbrains/RELEASE_TODO.md b/packages/kilo-jetbrains/RELEASE_TODO.md new file mode 100644 index 00000000000..1f8efae536f --- /dev/null +++ b/packages/kilo-jetbrains/RELEASE_TODO.md @@ -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. diff --git a/packages/kilo-jetbrains/RELEASING.md b/packages/kilo-jetbrains/RELEASING.md new file mode 100644 index 00000000000..a5d223cb501 --- /dev/null +++ b/packages/kilo-jetbrains/RELEASING.md @@ -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..-rc.` + +``` +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. diff --git a/packages/kilo-jetbrains/build.gradle.kts b/packages/kilo-jetbrains/build.gradle.kts index 00b4bd5ea2b..975f360bae2 100644 --- a/packages/kilo-jetbrains/build.gradle.kts +++ b/packages/kilo-jetbrains/build.gradle.kts @@ -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 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) } } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt index cb316ab2d93..f30a434ee5d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt @@ -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 diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt index b3ad33485d8..7945be7ee2b 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt @@ -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() { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/scroll/ScrollButtonIcon.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/scroll/ScrollButtonIcon.kt new file mode 100644 index 00000000000..323e32f6db3 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/scroll/ScrollButtonIcon.kt @@ -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", + ) +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionScroll.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/scroll/SessionScroll.kt similarity index 76% rename from packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionScroll.kt rename to packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/scroll/SessionScroll.kt index eef78776d83..48c8068068d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionScroll.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/scroll/SessionScroll.kt @@ -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) { - 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, 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()}" - } - } - }) -} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/PickerRow.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/PickerRow.kt index e5e24fb567d..3fd2887b184 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/PickerRow.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/PickerRow.kt @@ -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 } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPicker.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPicker.kt index 4423679e9ef..6913142e533 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPicker.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPicker.kt @@ -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 diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt index 0a51cbaee15..1dc0ea9a5ca 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt @@ -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) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt index ac4bc25f6e2..9ba3f57bfd4 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/PromptPanel.kt @@ -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) } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/SvgIconColorizer.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/SvgIconColorizer.kt new file mode 100644 index 00000000000..f51a7d9b880 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/SvgIconColorizer.kt @@ -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) { + 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, 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 diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/HistorySessionActionsTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/HistorySessionActionsTest.kt index 9165e8d50ef..012734473bd 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/HistorySessionActionsTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/HistorySessionActionsTest.kt @@ -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) } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/NewSessionActionTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/NewSessionActionTest.kt index 3de571b0d8f..9d18dac4407 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/NewSessionActionTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/NewSessionActionTest.kt @@ -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) } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/SendPromptActionTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/SendPromptActionTest.kt index a6198250f4d..d3a670bf208 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/SendPromptActionTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/SendPromptActionTest.kt @@ -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) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/StopSessionActionTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/StopSessionActionTest.kt index f6670ed10ba..f74bb0910c0 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/StopSessionActionTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/StopSessionActionTest.kt @@ -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) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiTestBase.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiTestBase.kt index a1d28529c71..13591100a75 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiTestBase.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiTestBase.kt @@ -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 diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/model/ModelPickerTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/model/ModelPickerTest.kt index 56dac1a9553..c1ad8e5f07f 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/model/ModelPickerTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/model/ModelPickerTest.kt @@ -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 diff --git a/packages/kilo-telemetry/package.json b/packages/kilo-telemetry/package.json index 08eeeca07eb..3e68970c207 100644 --- a/packages/kilo-telemetry/package.json +++ b/packages/kilo-telemetry/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/kilo-telemetry", - "version": "7.2.52", + "version": "7.3.0", "type": "module", "license": "MIT", "description": "Telemetry for Kilo CLI - PostHog analytics integration", diff --git a/packages/kilo-ui/package.json b/packages/kilo-ui/package.json index 27a64548719..ce5c61a3024 100644 --- a/packages/kilo-ui/package.json +++ b/packages/kilo-ui/package.json @@ -1,6 +1,6 @@ { "name": "@kilocode/kilo-ui", - "version": "7.2.52", + "version": "7.3.0", "type": "module", "license": "MIT", "exports": { diff --git a/packages/kilo-vscode/CHANGELOG.md b/packages/kilo-vscode/CHANGELOG.md index 461e4d8fe1c..547d154ca65 100644 --- a/packages/kilo-vscode/CHANGELOG.md +++ b/packages/kilo-vscode/CHANGELOG.md @@ -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 diff --git a/packages/kilo-vscode/package.json b/packages/kilo-vscode/package.json index d073c0dde9d..2a297381b6c 100644 --- a/packages/kilo-vscode/package.json +++ b/packages/kilo-vscode/package.json @@ -2,7 +2,7 @@ "name": "kilo-code", "displayName": "Kilo Code: AI Coding Agent, Copilot, and Autocomplete", "description": "Open Source AI coding agent that generates code from natural language, automates tasks, and runs terminal commands. Features inline autocomplete, browser automation, automated refactoring, and custom modes for planning, coding, and debugging. Supports 500+ AI models including Claude (Anthropic), Gemini, Grok, GPT, Codex and GLM.", - "version": "7.2.52", + "version": "7.3.0", "icon": "assets/icons/logo-outline-black.png", "galleryBanner": { "color": "#FFFFFF", diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index a3ed7bd2215..4556b4fcbc6 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -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 { + 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 { + 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 { 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 { 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 = [] if (files) { @@ -2633,6 +2678,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper variant?: string, files?: MessageFile[], context?: string, + contextDirectory?: string, ): Promise { 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) diff --git a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts index f88e32e770c..8f7cad3014a 100644 --- a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts +++ b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts @@ -71,6 +71,7 @@ export class AgentManagerProvider implements Disposable { private cachedLocalStats: { type: "agentManager.localStats"; stats: LocalStats } | undefined private unsubTool: (() => void) | undefined private closing: Promise | 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 { + private async onAddSessionToWorktree(worktreeId: string, sessionId?: string): Promise { 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. diff --git a/packages/kilo-vscode/src/agent-manager/__tests__/AgentManagerProvider.spec.ts b/packages/kilo-vscode/src/agent-manager/__tests__/AgentManagerProvider.spec.ts index 0c4dd458fcf..bf1f2dbd434 100644 --- a/packages/kilo-vscode/src/agent-manager/__tests__/AgentManagerProvider.spec.ts +++ b/packages/kilo-vscode/src/agent-manager/__tests__/AgentManagerProvider.spec.ts @@ -101,6 +101,7 @@ function createHarness() { prBridge: { handleMessage: ReturnType } activeSessionId: string | undefined stateReady: Promise | undefined + contextTarget: ReturnType createWorktreeOnDisk: ReturnType runSetupScriptForWorktree: ReturnType createSessionInWorktree: ReturnType @@ -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", + }) + }) }) diff --git a/packages/kilo-vscode/src/agent-manager/host.ts b/packages/kilo-vscode/src/agent-manager/host.ts index 92970ecac71..079b610f590 100644 --- a/packages/kilo-vscode/src/agent-manager/host.ts +++ b/packages/kilo-vscode/src/agent-manager/host.ts @@ -93,6 +93,7 @@ export interface Host { */ openPanel(opts: { onBeforeMessage: (msg: Record) => Promise | null> + worktreeDirectories?: () => string[] }): PanelContext /** Get the workspace/project root path. */ diff --git a/packages/kilo-vscode/src/agent-manager/types.ts b/packages/kilo-vscode/src/agent-manager/types.ts index 4a74212b7a4..f6f312fe466 100644 --- a/packages/kilo-vscode/src/agent-manager/types.ts +++ b/packages/kilo-vscode/src/agent-manager/types.ts @@ -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 { diff --git a/packages/kilo-vscode/src/agent-manager/vscode-host.ts b/packages/kilo-vscode/src/agent-manager/vscode-host.ts index 222ea03857d..54e2b6ce5b3 100644 --- a/packages/kilo-vscode/src/agent-manager/vscode-host.ts +++ b/packages/kilo-vscode/src/agent-manager/vscode-host.ts @@ -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) => Promise | 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) => Promise | 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) => Promise | 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) diff --git a/packages/kilo-vscode/src/extension.ts b/packages/kilo-vscode/src/extension.ts index 8c2ae51cdad..4bf19364f81 100644 --- a/packages/kilo-vscode/src/extension.ts +++ b/packages/kilo-vscode/src/extension.ts @@ -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(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() } diff --git a/packages/kilo-vscode/src/kilo-provider-utils.ts b/packages/kilo-vscode/src/kilo-provider-utils.ts index caa1f565e18..115cb69d15d 100644 --- a/packages/kilo-vscode/src/kilo-provider-utils.ts +++ b/packages/kilo-vscode/src/kilo-provider-utils.ts @@ -222,6 +222,7 @@ export interface SessionRefreshContext { connectionState: "connecting" | "connected" | "disconnected" | "error" listSessions: ((dir: string) => Promise) | null sessionDirectories: Map + worktreeDirectories?: () => string[] workspaceDirectory: string postMessage(message: unknown): void } @@ -245,7 +246,7 @@ export async function loadSessions(ctx: SessionRefreshContext): Promise() 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 workspaceDirectory: string }) { @@ -350,6 +352,8 @@ export function resolveNewSessionDirectory(input: { }) } + if (input.contextDirectory) return input.contextDirectory + return resolveContextDirectory({ currentSessionID: input.currentSessionID, contextSessionID: input.contextSessionID, diff --git a/packages/kilo-vscode/src/kilo-provider/early-message.ts b/packages/kilo-vscode/src/kilo-provider/early-message.ts index 74b73a14955..4ff6c787b3d 100644 --- a/packages/kilo-vscode/src/kilo-provider/early-message.ts +++ b/packages/kilo-vscode/src/kilo-provider/early-message.ts @@ -11,10 +11,16 @@ type Ctx = { connection: KiloConnectionService dir: string post: (msg: unknown) => void + exportTranscript: (sessionID: string) => Promise } export async function routeEarlyMessage(message: { type: string }, ctx: Ctx): Promise { 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 }) } diff --git a/packages/kilo-vscode/src/kilo-provider/export-transcript.ts b/packages/kilo-vscode/src/kilo-provider/export-transcript.ts new file mode 100644 index 00000000000..6592d509735 --- /dev/null +++ b/packages/kilo-vscode/src/kilo-provider/export-transcript.ts @@ -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 "" +} diff --git a/packages/kilo-vscode/src/kilo-provider/options.ts b/packages/kilo-vscode/src/kilo-provider/options.ts index 5889a6ea5e2..8fad1c57c48 100644 --- a/packages/kilo-vscode/src/kilo-provider/options.ts +++ b/packages/kilo-vscode/src/kilo-provider/options.ts @@ -1,5 +1,8 @@ export type KiloProviderOptions = { projectDirectory?: string | null + platform?: string slimEditMetadata?: boolean tabTitle?: (title: string) => void + onSidebarVisibilityChange?: (visible: boolean) => void + worktreeDirectories?: () => string[] } diff --git a/packages/kilo-vscode/src/speech-to-text/capture.ts b/packages/kilo-vscode/src/speech-to-text/capture.ts index 3ae3005601b..92f127dc2c9 100644 --- a/packages/kilo-vscode/src/speech-to-text/capture.ts +++ b/packages/kilo-vscode/src/speech-to-text/capture.ts @@ -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 { }) } -async function startWithArgs(bin: string, file: string, input: Input, args: string[][]): Promise { +async function startWithArgs(bin: string, file: string, input: Input, args: Args[]): Promise { 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 { 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 { - if (process.platform === "darwin") return [["-f", "avfoundation", "-i", ":default"]] - if (process.platform === "linux") +async function inputArgSets(bin: string): Promise { + 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 { +async function windowsInputArgSets(bin: string): Promise { 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 { @@ -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 { await unlink(file).catch((err: unknown) => { console.warn("[Kilo New] Failed to remove speech recording", err) diff --git a/packages/kilo-vscode/src/speech-to-text/models.ts b/packages/kilo-vscode/src/speech-to-text/models.ts index 5936641366f..be872d81c96 100644 --- a/packages/kilo-vscode/src/speech-to-text/models.ts +++ b/packages/kilo-vscode/src/speech-to-text/models.ts @@ -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", diff --git a/packages/kilo-vscode/src/speech-to-text/transcribe.ts b/packages/kilo-vscode/src/speech-to-text/transcribe.ts index 10cbb422f1a..1e978bf028c 100644 --- a/packages/kilo-vscode/src/speech-to-text/transcribe.ts +++ b/packages/kilo-vscode/src/speech-to-text/transcribe.ts @@ -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 } : {}), }), }) diff --git a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts index c1ce05633dd..fc4de9e19f8 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts @@ -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"), diff --git a/packages/kilo-vscode/tests/unit/export-transcript.test.ts b/packages/kilo-vscode/tests/unit/export-transcript.test.ts new file mode 100644 index 00000000000..f93698ac960 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/export-transcript.test.ts @@ -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") + }) +}) diff --git a/packages/kilo-vscode/tests/unit/kilo-provider-worktree-context.test.ts b/packages/kilo-vscode/tests/unit/kilo-provider-worktree-context.test.ts index 8e0a9afbacc..017b305cc94 100644 --- a/packages/kilo-vscode/tests/unit/kilo-provider-worktree-context.test.ts +++ b/packages/kilo-vscode/tests/unit/kilo-provider-worktree-context.test.ts @@ -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", diff --git a/packages/kilo-vscode/tests/unit/markdown-annotation-layer.test.ts b/packages/kilo-vscode/tests/unit/markdown-annotation-layer.test.ts new file mode 100644 index 00000000000..3354e61943c --- /dev/null +++ b/packages/kilo-vscode/tests/unit/markdown-annotation-layer.test.ts @@ -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 { + 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) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/plan-exit.test.ts b/packages/kilo-vscode/tests/unit/plan-exit.test.ts new file mode 100644 index 00000000000..11042c38fa6 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/plan-exit.test.ts @@ -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()") + // { }) }) +// ── 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", () => { diff --git a/packages/kilo-vscode/tests/unit/speech-to-text-capture.test.ts b/packages/kilo-vscode/tests/unit/speech-to-text-capture.test.ts index 2524f732b7a..c840008d9b7 100644 --- a/packages/kilo-vscode/tests/unit/speech-to-text-capture.test.ts +++ b/packages/kilo-vscode/tests/unit/speech-to-text-capture.test.ts @@ -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", + ) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/use-speech-to-text.test.ts b/packages/kilo-vscode/tests/unit/use-speech-to-text.test.ts new file mode 100644 index 00000000000..52ac796d67a --- /dev/null +++ b/packages/kilo-vscode/tests/unit/use-speech-to-text.test.ts @@ -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() + }) +}) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index 8cc186ff2ea..9dbfaa1b336 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -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 = { ), } -/** 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, - t: (key: string, params?: Record) => 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 = () => { { + if (addSessionToCurrentWorktree(id)) return setHistory(false) if (localSessionIDs().includes(id)) { saveTabMemory() @@ -3021,6 +2975,7 @@ const AgentManagerContent: Component = () => {
{ + if (addSessionToCurrentWorktree(id)) return if (localSessionIDs().includes(id)) { session.selectSession(id) if (selection() === null) setSelection(LOCAL) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx b/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx index 72de493aabd..f7b01b37690 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx @@ -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 = (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 = (props) => { null, ) const [editing, setEditing] = createSignal(null) + const speechKeys = createMemo(() => { + const keys = new Set() + 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 = (props) => { cancelDraft, labels: labels(), activeTerminalId: props.activeTerminalId, + speech: reviewSpeech, }) } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/FullScreenDiffView.tsx b/packages/kilo-vscode/webview-ui/agent-manager/FullScreenDiffView.tsx index 6f8b8828f77..e9ad869f4a8 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/FullScreenDiffView.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/FullScreenDiffView.tsx @@ -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 = (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 = (props) => null, ) const [editing, setEditing] = createSignal(null) + const speechKeys = createMemo(() => { + const keys = new Set() + 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(null) const [treeWidth, setTreeWidth] = createSignal(240) let nextId = 0 @@ -322,6 +353,7 @@ export const FullScreenDiffView: Component = (props) => cancelDraft, labels: labels(), activeTerminalId: props.activeTerminalId, + speech: reviewSpeech, }) } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/MarkdownAnnotationLayer.tsx b/packages/kilo-vscode/webview-ui/agent-manager/MarkdownAnnotationLayer.tsx index bdd04b2b525..fb1fa8d3787 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/MarkdownAnnotationLayer.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/MarkdownAnnotationLayer.tsx @@ -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, 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 = 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 }) }) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css index 305359b3dc7..fa1a843c6ce 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css @@ -2146,6 +2146,11 @@ body.am-wt-dragging-active * { gap: 6px; } +.am-annotation-speech { + display: flex; + align-items: center; +} + .am-annotation-btn { padding: 2px 10px; font-size: inherit; @@ -2553,12 +2558,25 @@ body.am-wt-dragging-active * { .am-nv-dialog .am-prompt-input-wrapper { flex: 1; min-height: 0; + overflow-x: hidden; + overflow-y: auto; +} + +.am-nv-dialog .am-prompt-input-container:has([class~="model-selector-popover"][data-component="popover-content"]), +.am-nv-dialog-content:has([class~="model-selector-popover"][data-component="popover-content"]), +[data-component="dialog"]:has(.am-nv-dialog [class~="model-selector-popover"][data-component="popover-content"]) + [data-slot="dialog-body"] { + overflow: visible; } .am-nv-dialog .am-prompt-input-ghost-wrapper { height: 100%; } +.am-nv-dialog [class~="prompt-input-hint"] { + flex-shrink: 0; +} + .am-nv-dialog .am-prompt-input { color: var(--vscode-input-foreground, var(--text-base)); resize: none; diff --git a/packages/kilo-vscode/webview-ui/agent-manager/markdown-annotation-mutation.ts b/packages/kilo-vscode/webview-ui/agent-manager/markdown-annotation-mutation.ts new file mode 100644 index 00000000000..6fcdf582a83 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/markdown-annotation-mutation.ts @@ -0,0 +1,13 @@ +const selector = ".am-markdown-inline-annotations, .am-markdown-list-annotation, .am-markdown-table-annotation" + +// Keep host insertion observable, only nested annotation UI updates are safe to ignore. +export function isAnnotationMutation(mutation: Pick): boolean { + const target = mutation.target + if (!("closest" in target)) return false + if (typeof target.closest !== "function") return false + return target.closest(selector) !== null +} + +export function annotationSelector(): string { + return selector +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/review-annotation-speech.tsx b/packages/kilo-vscode/webview-ui/agent-manager/review-annotation-speech.tsx new file mode 100644 index 00000000000..48c67f73d28 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/review-annotation-speech.tsx @@ -0,0 +1,138 @@ +import { createEffect, createRoot, createSignal, onCleanup, Show, type Accessor } from "solid-js" +import { render as renderSolid } from "solid-js/web" +import { SpeechToTextButton } from "../src/components/speech-to-text/SpeechToTextButton" +import { insertSpacedText } from "../src/components/chat/prompt-input-utils" +import type { SpeechState, SpeechToText } from "../src/components/speech-to-text/useSpeechToText" +import { reviewAnnotationSpeechKey, type AnnotationMeta } from "./review-annotations" + +type Props = { + speech: SpeechToText + enabled: Accessor + model: Accessor + label: (key: string) => string + keys: Accessor> +} + +type Node = { + host: HTMLDivElement + dispose: VoidFunction + setTextarea: (textarea: HTMLTextAreaElement) => void +} + +function insertReviewSpeechText(textarea: HTMLTextAreaElement, value: string): void { + const text = textarea.value + const start = textarea.selectionStart ?? text.length + const end = textarea.selectionEnd ?? start + const result = insertSpacedText(text, value, start, end) + + textarea.value = result.text + textarea.setSelectionRange(result.pos, result.pos) + textarea.focus() +} + +export function createReviewAnnotationSpeechRenderer(props: Props) { + const nodes = new Map() + const [owner, setOwner] = createSignal() + + const mount = (key: string, textarea: HTMLTextAreaElement): Node | undefined => { + if (typeof document === "undefined") return undefined + + const host = document.createElement("div") + host.className = "am-annotation-speech" + let field = textarea + const mine = () => owner() === key + const state = (): SpeechState => (mine() ? props.speech.state() : "idle") + const start = (model: string) => { + setOwner(key) + props.speech.start({ + model, + insert: (value) => insertReviewSpeechText(field, value), + }) + } + const speech: SpeechToText = { + state, + error: () => (mine() ? props.speech.error() : undefined), + active: () => mine() && props.speech.active(), + start: (opts) => start(opts.model), + stop: () => { + if (!mine()) return + props.speech.stop() + }, + cancel: () => { + if (!mine()) return + props.speech.cancel() + setOwner(undefined) + }, + clear: () => { + if (!mine()) return + props.speech.clear() + setOwner(undefined) + }, + } + const blocked = () => props.speech.active() && !mine() + + const dispose = createRoot((root) => { + const view = renderSolid( + () => ( + + start(props.model())} + label={props.label} + /> + + ), + host, + ) + onCleanup(view) + return root + }) + + const node = { + host, + dispose, + setTextarea: (next: HTMLTextAreaElement) => { + field = next + }, + } + nodes.set(key, node) + return node + } + + const render = (meta: AnnotationMeta, textarea: HTMLTextAreaElement): HTMLElement | undefined => { + const key = reviewAnnotationSpeechKey(meta) + if (!key) return undefined + const node = nodes.get(key) ?? mount(key, textarea) + if (!node) return undefined + node.setTextarea(textarea) + return node.host + } + + createEffect(() => { + const keys = props.keys() + const current = owner() + if (current && !keys.has(current)) { + if (props.speech.active()) props.speech.cancel() + if (props.speech.state() === "error") props.speech.clear() + setOwner(undefined) + } + + for (const [key, node] of nodes) { + if (keys.has(key)) continue + node.dispose() + nodes.delete(key) + } + }) + + onCleanup(() => { + if (owner() && props.speech.active()) props.speech.cancel() + for (const [, node] of nodes) node.dispose() + nodes.clear() + }) + + return { + active: props.speech.active, + render, + } +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/review-annotations.ts b/packages/kilo-vscode/webview-ui/agent-manager/review-annotations.ts index 3c46aac071c..380a501998e 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/review-annotations.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/review-annotations.ts @@ -26,6 +26,22 @@ export interface AnnotationMeta { editing?: boolean } +type SpeechDraft = Pick + +export function reviewDraftSpeechKey(draft: SpeechDraft): string { + return `draft:${draft.file}:${draft.side}:${draft.line}:${draft.endLine ?? draft.line}` +} + +export function reviewEditSpeechKey(id: string): string { + return `edit:${id}` +} + +export function reviewAnnotationSpeechKey(meta: AnnotationMeta): string | undefined { + if (meta.type === "draft") return reviewDraftSpeechKey(meta) + if (!meta.editing || !meta.comment) return undefined + return reviewEditSpeechKey(meta.comment.id) +} + interface AnnotationHandlers { diffs: WorktreeFileDiff[] editing: string | null @@ -36,6 +52,10 @@ interface AnnotationHandlers { cancelDraft: () => void labels: AnnotationLabels activeTerminalId?: string + speech?: { + active: () => boolean + render: (meta: AnnotationMeta, textarea: HTMLTextAreaElement) => HTMLElement | undefined + } } function focusWhenConnected(el: HTMLTextAreaElement): void { @@ -150,6 +170,8 @@ export function buildReviewAnnotation( submitButton.className = "am-annotation-btn am-annotation-btn-submit" submitButton.textContent = handlers.labels.comment + const speech = handlers.speech?.render(meta, textarea) + if (speech) actions.appendChild(speech) actions.appendChild(cancelButton) actions.appendChild(submitButton) wrapper.appendChild(header) @@ -159,6 +181,7 @@ export function buildReviewAnnotation( focusWhenConnected(textarea) const submit = () => { + if (handlers.speech?.active()) return const text = textarea.value.trim() if (!text) return const diff = handlers.diffs.find((item) => item.file === meta.file) @@ -216,6 +239,8 @@ export function buildReviewAnnotation( saveButton.className = "am-annotation-btn am-annotation-btn-submit" saveButton.textContent = handlers.labels.save + const speech = handlers.speech?.render(meta, textarea) + if (speech) actions.appendChild(speech) actions.appendChild(cancelButton) actions.appendChild(saveButton) wrapper.appendChild(header) @@ -231,6 +256,7 @@ export function buildReviewAnnotation( saveButton.addEventListener("click", (event) => { event.stopPropagation() + if (handlers.speech?.active()) return const text = textarea.value.trim() if (!text) return handlers.updateComment(comment.id, text) @@ -244,6 +270,7 @@ export function buildReviewAnnotation( } if (event.key === "Enter" && !event.shiftKey) { event.preventDefault() + if (handlers.speech?.active()) return const text = textarea.value.trim() if (!text) return handlers.updateComment(comment.id, text) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/shortcuts.ts b/packages/kilo-vscode/webview-ui/agent-manager/shortcuts.ts new file mode 100644 index 00000000000..8f21d698a11 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/shortcuts.ts @@ -0,0 +1,67 @@ +export interface ShortcutEntry { + label: string + binding: string +} + +export interface ShortcutCategory { + title: string + shortcuts: ShortcutEntry[] +} + +export function buildShortcutCategories( + bindings: Record, + t: (key: string, params?: Record) => 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) +} diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx index 91754c748fa..92fb514d580 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx @@ -21,7 +21,10 @@ import { useData } from "@kilocode/kilo-ui/context/data" import { useSession } from "../../context/session" import { useDisplay } from "../../context/display" import { useConfig } from "../../context/config" +import { useLanguage } from "../../context/language" +import { useServer } from "../../context/server" import { snapshotProgress } from "../../context/session-utils" +import { planDisplayPath } from "../../utils/plan-path" import { QuestionDock } from "./QuestionDock" import { SuggestBar } from "./SuggestBar" @@ -30,6 +33,50 @@ import { SuggestBar } from "./SuggestBar" // so the user can see what the AI set up. export const UPSTREAM_SUPPRESSED_TOOLS = new Set(["todowrite", "todoread"]) +/** Extract plan path from a completed plan_exit tool part. */ +function planExitInfo(part: SDKPart): { plan: string } | undefined { + if (part.type !== "tool") return undefined + const tp = part as unknown as ToolPart + if (tp.tool !== "plan_exit") return undefined + if (tp.state?.status !== "completed") return undefined + const meta = (tp.state as { metadata?: Record }).metadata ?? {} + const plan = typeof meta.plan === "string" ? meta.plan : undefined + if (!plan) return undefined + return { plan } +} + +function PlanExitCard(props: { part: ToolPart }) { + const language = useLanguage() + const server = useServer() + const data = useData() + const info = createMemo(() => planExitInfo(props.part as unknown as SDKPart)) + const display = createMemo(() => { + const i = info() + if (!i) return "" + return planDisplayPath(i.plan, server.workspaceDirectory()) + }) + const label = createMemo(() => { + if (!info()) return "" + return language.t("plan.exit.ready") + }) + const open = (e: MouseEvent) => { + e.preventDefault() + const i = info() + if (!i || !data.openFile) return + data.openFile(i.plan) + } + return ( + +
+ {label()}{" "} + + {display()} + +
+
+ ) +} + function isRenderable(part: SDKPart): boolean { if (part.type === "tool") { const tool = (part as SDKPart & { tool: string }).tool @@ -156,10 +203,24 @@ export const AssistantMessage: Component = (props) => { if (tool.state?.status === "error") return return part }) + const planExit = createMemo(() => { + if (part.type !== "tool") return + const tp = part as unknown as ToolPart + if (tp.tool !== "plan_exit") return + if (tp.state?.status !== "completed") return + return tp + }) return (
= (props) => { when={activeSuggestion()} fallback={ } - /> + > + + } > - + {(tool) => } } > - {(tool) => } + {(tp) => } } > diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx index 27d984c56a0..cbd3feb548d 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx @@ -329,14 +329,30 @@ export const PromptInput: Component = (props) => { window.addEventListener("compactSession", onCompact) onCleanup(() => window.removeEventListener("compactSession", onCompact)) + const onExport = () => { + const id = session.currentSessionID() + if (id) session.exportSessionTranscript(id) + } + window.addEventListener("exportSessionTranscript", onExport) + onCleanup(() => window.removeEventListener("exportSessionTranscript", onExport)) + const isBusy = () => isPromptBusy(session.status(), !!props.suggesting?.(), !!props.questioning?.()) const isDisabled = () => !server.isConnected() const canUseSpeech = () => canUseSpeechToText(settings(), config(), provider.connected(), server.profileData()) const speechModel = () => selectedSpeechToTextModel(settings()) const hasInput = () => text().trim().length > 0 || imageAttach.images().length > 0 || reviewComments().length > 0 const canSend = () => - hasInput() && !isDisabled() && !speech.active() && !terminal.pending() && !git.pending() && !props.blocked?.() - const showStop = () => isBusy() && !hasInput() + !isDisabled() && + !terminal.pending() && + !git.pending() && + !props.blocked?.() && + (speech.state() === "recording" || (hasInput() && !speech.active())) + const sendLabel = () => { + if (props.blocked?.()) return language.t("prompt.action.send.blocked") + if (speech.state() === "recording") return language.t("prompt.action.send.recording") + return language.t("prompt.action.send") + } + const showStop = () => isBusy() && !hasInput() && speech.state() !== "recording" const isAtEnd = () => textareaRef ? atEnd(textareaRef.selectionStart, textareaRef.selectionEnd, textareaRef.value.length) : false const highlightMentions = () => { @@ -675,6 +691,33 @@ export const PromptInput: Component = (props) => { speech.start({ model: speechModel(), insert: insertSpeechText }) } + const transcribeAndSend = () => { + const key = draftKey() + const id = sid() + const context = ctx() + const value = text() + const comments = reviewComments() + const images = imageAttach.images() + speech.stop({ + done: () => void handleSend(), + ready: () => + draftKey() === key && + sid() === id && + ctx() === context && + text() === value && + reviewComments() === comments && + imageAttach.images() === images, + }) + } + + const handleSendClick = () => { + if (speech.state() !== "recording" || !canSend()) { + void handleSend() + return + } + transcribeAndSend() + } + const handleSend = async () => { const draft = text().trim() @@ -721,6 +764,8 @@ export const PromptInput: Component = (props) => { const pendingId = props.pendingSessionID ?? session.draftSessionID() const id = sid() const sel = session.selected(id) + const context = ctx() + const key = draftKey() const terminalFile = await terminal.resolveAttachment(message, id).catch((err: Error) => { showToast({ variant: "error", title: "Terminal context unavailable", description: err.message }) @@ -742,16 +787,20 @@ export const PromptInput: Component = (props) => { ] const attachments = allFiles.length > 0 ? allFiles : undefined - const key = draftKey() // Server-side slash command (cmdMatch/matched already computed above) if (matched) { const rest = draft.slice(cmdMatch![0].length).trim() const args = review && rest ? `${review}\n\n${rest}` : rest || review - session.sendCommand(matched.name, args, sel?.providerID, sel?.modelID, attachments, pendingId, ctx()) + session.sendCommand(matched.name, args, sel?.providerID, sel?.modelID, attachments, pendingId, context) } else { - session.sendMessage(message, sel?.providerID, sel?.modelID, attachments, pendingId, ctx()) + session.sendMessage(message, sel?.providerID, sel?.modelID, attachments, pendingId, context) } + drafts.delete(key) + reviewDrafts.delete(key) + imageDrafts.delete(key) + if (draftKey() !== key) return + history.append(draft) history.reset() setText("") @@ -759,9 +808,6 @@ export const PromptInput: Component = (props) => { imageAttach.clear() mention.closeMention() slash.close() - drafts.delete(key) - reviewDrafts.delete(key) - imageDrafts.delete(key) if (textareaRef) textareaRef.style.height = "auto" } @@ -1085,16 +1131,13 @@ export const PromptInput: Component = (props) => { +