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/autocomplete-not-set-default.md b/.changeset/autocomplete-not-set-default.md new file mode 100644 index 00000000000..68888282766 --- /dev/null +++ b/.changeset/autocomplete-not-set-default.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Add a "Not set (use server default)" option to the autocomplete model picker so users can follow the recommended default automatically. Users who previously had the default model pinned only because it was the only thing visible in the dropdown are migrated to "Not set" once. diff --git a/.changeset/breezy-models-float.md b/.changeset/breezy-models-float.md deleted file mode 100644 index 84b355eb520..00000000000 --- a/.changeset/breezy-models-float.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Improve the JetBrains model picker with sections, favorites, and smart filtering. 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/center-history-delete.md b/.changeset/center-history-delete.md new file mode 100644 index 00000000000..db671cea3b1 --- /dev/null +++ b/.changeset/center-history-delete.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Center local session history delete buttons within their rows. diff --git a/.changeset/chilly-dancers-build.md b/.changeset/chilly-dancers-build.md deleted file mode 100644 index 3f8b568bc5d..00000000000 --- a/.changeset/chilly-dancers-build.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Add a JetBrains tool window action for starting new sessions and let recent sessions reopen in the side panel without duplicate views. 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/editor-sized-jetbrains-chat.md b/.changeset/editor-sized-jetbrains-chat.md deleted file mode 100644 index 221bd814f6e..00000000000 --- a/.changeset/editor-sized-jetbrains-chat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Scale JetBrains chat transcript fonts from the editor font settings. diff --git a/.changeset/favorite-picker-index.md b/.changeset/favorite-picker-index.md deleted file mode 100644 index 602a32bfe03..00000000000 --- a/.changeset/favorite-picker-index.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Keep the JetBrains model picker at the same row position when models are favorited or unfavorited. 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/fuzzy-berries-press.md b/.changeset/fuzzy-berries-press.md deleted file mode 100644 index 0c48bad5166..00000000000 --- a/.changeset/fuzzy-berries-press.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Show JetBrains session connection status inline above the prompt, cap expanded connection details, and keep empty sessions in the shared chat scroll area. diff --git a/.changeset/green-planes-reply.md b/.changeset/green-planes-reply.md deleted file mode 100644 index f9b3517d9c4..00000000000 --- a/.changeset/green-planes-reply.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Show detailed JetBrains session connection and configuration issues in the connection panel, and make retry recover app-side problems more reliably. diff --git a/.changeset/history-context-menu-size.md b/.changeset/history-context-menu-size.md new file mode 100644 index 00000000000..80a0fb90dd8 --- /dev/null +++ b/.changeset/history-context-menu-size.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Improve the size and readability of the local History session context menu. diff --git a/.changeset/isolate-indexing-worker.md b/.changeset/isolate-indexing-worker.md new file mode 100644 index 00000000000..34f2c9fb109 --- /dev/null +++ b/.changeset/isolate-indexing-worker.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Keep the extension responsive while semantic indexing processes large workspaces. diff --git a/.changeset/jetbrains-active-badges.md b/.changeset/jetbrains-active-badges.md new file mode 100644 index 00000000000..89e19d5f707 --- /dev/null +++ b/.changeset/jetbrains-active-badges.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Show question, permission, plan, and login-required badges for active JetBrains sessions in recent and history lists. diff --git a/.changeset/jetbrains-auto-approve.md b/.changeset/jetbrains-auto-approve.md new file mode 100644 index 00000000000..fbedb802c55 --- /dev/null +++ b/.changeset/jetbrains-auto-approve.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": minor +--- + +Support toggling auto-approve for permission prompts from the JetBrains chat input. diff --git a/.changeset/jetbrains-chat-renderers.md b/.changeset/jetbrains-chat-renderers.md deleted file mode 100644 index 0a099733c1b..00000000000 --- a/.changeset/jetbrains-chat-renderers.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Match VS Code-style reasoning and tool run rendering in JetBrains chat. diff --git a/.changeset/jetbrains-empty-panel-layout.md b/.changeset/jetbrains-empty-panel-layout.md deleted file mode 100644 index 3f13739eeb8..00000000000 --- a/.changeset/jetbrains-empty-panel-layout.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Center description text and remove the fixed-width cap from the empty session panel so recent sessions can stretch naturally. diff --git a/.changeset/jetbrains-existing-session-scroll.md b/.changeset/jetbrains-existing-session-scroll.md deleted file mode 100644 index 534092cc3f9..00000000000 --- a/.changeset/jetbrains-existing-session-scroll.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Open existing JetBrains sessions scrolled to the latest message after history loads. diff --git a/.changeset/jetbrains-header-tooltips.md b/.changeset/jetbrains-header-tooltips.md deleted file mode 100644 index eabe4154aa9..00000000000 --- a/.changeset/jetbrains-header-tooltips.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Remember the JetBrains session header metrics expansion preference and show file names in read/write timeline tooltips. diff --git a/.changeset/jetbrains-history-navigation.md b/.changeset/jetbrains-history-navigation.md deleted file mode 100644 index 92f361fc775..00000000000 --- a/.changeset/jetbrains-history-navigation.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Improve JetBrains history navigation with Back support and a Show History action in recent sessions. diff --git a/.changeset/jetbrains-history-refresh.md b/.changeset/jetbrains-history-refresh.md new file mode 100644 index 00000000000..bf87fff84ec --- /dev/null +++ b/.changeset/jetbrains-history-refresh.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Refresh JetBrains history and recent-session rows when active session titles change, and keep pending inactive sessions alive when switching views. diff --git a/.changeset/jetbrains-history-tabs.md b/.changeset/jetbrains-history-tabs.md deleted file mode 100644 index 859a598e3e8..00000000000 --- a/.changeset/jetbrains-history-tabs.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Improve the JetBrains history panel with native tabs, independent local and cloud search, and date-grouped cloud sessions. diff --git a/.changeset/jetbrains-mode-picker.md b/.changeset/jetbrains-mode-picker.md deleted file mode 100644 index 7d3b6b2011d..00000000000 --- a/.changeset/jetbrains-mode-picker.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Improve the JetBrains mode picker with descriptions, deprecated badges, and native IntelliJ selection behavior. diff --git a/.changeset/jetbrains-question-scroll-overlay.md b/.changeset/jetbrains-question-scroll-overlay.md new file mode 100644 index 00000000000..ae3d2a9633f --- /dev/null +++ b/.changeset/jetbrains-question-scroll-overlay.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Show a question indicator on the JetBrains session scroll overlay when user input is needed. diff --git a/.changeset/jetbrains-resizing-prompt.md b/.changeset/jetbrains-resizing-prompt.md new file mode 100644 index 00000000000..858eccf6a96 --- /dev/null +++ b/.changeset/jetbrains-resizing-prompt.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Resize the JetBrains prompt editor as prompt lines are added or removed. diff --git a/.changeset/jetbrains-scroll-bottom.md b/.changeset/jetbrains-scroll-bottom.md deleted file mode 100644 index b98469fac73..00000000000 --- a/.changeset/jetbrains-scroll-bottom.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Keep the JetBrains chat transcript pinned to bottom reliably while responses stream. diff --git a/.changeset/jetbrains-scroll-button.md b/.changeset/jetbrains-scroll-button.md deleted file mode 100644 index 31762b7e48c..00000000000 --- a/.changeset/jetbrains-scroll-button.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Show a scroll-to-bottom button when JetBrains chat sessions are scrolled away from the latest message. diff --git a/.changeset/jetbrains-send-action.md b/.changeset/jetbrains-send-action.md deleted file mode 100644 index 6f6f30a3d41..00000000000 --- a/.changeset/jetbrains-send-action.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Support rebinding the JetBrains prompt Send action in the IntelliJ keymap. diff --git a/.changeset/jetbrains-session-header.md b/.changeset/jetbrains-session-header.md deleted file mode 100644 index 9c13aa6c9d5..00000000000 --- a/.changeset/jetbrains-session-header.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Show collapsible session metadata, context usage, token usage, and compaction controls in the JetBrains session header. diff --git a/.changeset/jetbrains-session-history.md b/.changeset/jetbrains-session-history.md deleted file mode 100644 index 8aa827208cc..00000000000 --- a/.changeset/jetbrains-session-history.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Add a native session history panel to the JetBrains plugin. diff --git a/.changeset/jetbrains-session-hover.md b/.changeset/jetbrains-session-hover.md new file mode 100644 index 00000000000..181ad76ac4a --- /dev/null +++ b/.changeset/jetbrains-session-hover.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Refine JetBrains session transcript styling with subtler tool rows, prompt-styled user messages, and underlined read file links that open files in the IDE. diff --git a/.changeset/jetbrains-session-layout-padding.md b/.changeset/jetbrains-session-layout-padding.md deleted file mode 100644 index 2fbf40c42d8..00000000000 --- a/.changeset/jetbrains-session-layout-padding.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Control JetBrains chat transcript spacing from the session layout. diff --git a/.changeset/jetbrains-session-links.md b/.changeset/jetbrains-session-links.md new file mode 100644 index 00000000000..63de324a815 --- /dev/null +++ b/.changeset/jetbrains-session-links.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Support opening links in JetBrains session markdown transcripts. diff --git a/.changeset/jetbrains-session-loading-state.md b/.changeset/jetbrains-session-loading-state.md deleted file mode 100644 index bc930ebcbca..00000000000 --- a/.changeset/jetbrains-session-loading-state.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Show loading immediately when opening JetBrains sessions without showing duplicate progress for new sessions. diff --git a/.changeset/jetbrains-session-scroll-layout.md b/.changeset/jetbrains-session-scroll-layout.md deleted file mode 100644 index ea065c67885..00000000000 --- a/.changeset/jetbrains-session-scroll-layout.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Reduce redundant JetBrains chat scroll layout work while keeping bottom-follow behavior. diff --git a/.changeset/jetbrains-session-scroll.md b/.changeset/jetbrains-session-scroll.md new file mode 100644 index 00000000000..d91e0ec1f90 --- /dev/null +++ b/.changeset/jetbrains-session-scroll.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Keep JetBrains chat scrolled to the latest prompt and question updates when following the bottom. diff --git a/.changeset/jetbrains-step-finish-timeline.md b/.changeset/jetbrains-step-finish-timeline.md deleted file mode 100644 index 6afba10a4bc..00000000000 --- a/.changeset/jetbrains-step-finish-timeline.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Show completed assistant steps in the JetBrains session timeline. diff --git a/.changeset/jetbrains-stop-action.md b/.changeset/jetbrains-stop-action.md deleted file mode 100644 index 34bdae579dc..00000000000 --- a/.changeset/jetbrains-stop-action.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Support stopping active JetBrains sessions from the prompt action button. diff --git a/.changeset/kilo-embedding-model-presets.md b/.changeset/kilo-embedding-model-presets.md new file mode 100644 index 00000000000..ae3e3f912e4 --- /dev/null +++ b/.changeset/kilo-embedding-model-presets.md @@ -0,0 +1,8 @@ +--- +"kilo-code": patch +"@kilocode/cli": patch +"@kilocode/kilo-indexing": patch +"@kilocode/sdk": patch +--- + +Use supported hosted model presets for Kilo indexing and clear obsolete model and dimension overrides. diff --git a/.changeset/mercury-next-edit.md b/.changeset/mercury-next-edit.md new file mode 100644 index 00000000000..976b7990df4 --- /dev/null +++ b/.changeset/mercury-next-edit.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Add Mercury Next Edit as an opt-in autocomplete mode. Predicts multi-line edits beyond the cursor (including off-cursor and pure-insertion edits) and surfaces them with a Tab-to-jump / Tab-to-apply affordance. Select "Mercury Next Edit" under the autocomplete model setting to enable it (requires an Inception API key). Thanks [@tfiras](https://github.com/tfiras)! diff --git a/.changeset/model-picker-natural-size.md b/.changeset/model-picker-natural-size.md deleted file mode 100644 index 5fa0954e274..00000000000 --- a/.changeset/model-picker-natural-size.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Size the JetBrains model picker from its rendered model rows instead of fixed popup dimensions. diff --git a/.changeset/nextedit-via-kilo-gateway.md b/.changeset/nextedit-via-kilo-gateway.md new file mode 100644 index 00000000000..7e6f8ad503c --- /dev/null +++ b/.changeset/nextedit-via-kilo-gateway.md @@ -0,0 +1,6 @@ +--- +"@kilocode/kilo-gateway": minor +"kilo-code": minor +--- + +Support Mercury Next Edit through the Kilo Gateway. The new "Mercury Next Edit via Kilo Gateway" autocomplete model routes Next Edit predictions through your Kilo account (no separate Inception API key required). 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/quiet-agents-launch.md b/.changeset/quiet-agents-launch.md new file mode 100644 index 00000000000..0fed5274876 --- /dev/null +++ b/.changeset/quiet-agents-launch.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Make the Agent Manager tool available by default in VS Code. diff --git a/.changeset/quiet-indexing-startup.md b/.changeset/quiet-indexing-startup.md new file mode 100644 index 00000000000..5a0260139b7 --- /dev/null +++ b/.changeset/quiet-indexing-startup.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Keep the extension usable on fresh startup when semantic indexing is enabled globally. diff --git a/.changeset/quiet-otters-reason.md b/.changeset/quiet-otters-reason.md deleted file mode 100644 index ad7b31eb35f..00000000000 --- a/.changeset/quiet-otters-reason.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Support reasoning effort selection and resetting JetBrains chat model overrides to the default model. diff --git a/.changeset/reasoning-picker-align.md b/.changeset/reasoning-picker-align.md deleted file mode 100644 index 4a21b9e9762..00000000000 --- a/.changeset/reasoning-picker-align.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Align checked and unchecked reasoning picker rows in the JetBrains plugin. diff --git a/.changeset/rename-session-titles.md b/.changeset/rename-session-titles.md new file mode 100644 index 00000000000..03d77bce3fb --- /dev/null +++ b/.changeset/rename-session-titles.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Allow renaming sessions with a consistent inline editor in the active chat header and History, using safe bounded titles. 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/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/slow-terms-dance.md b/.changeset/slow-terms-dance.md deleted file mode 100644 index 777d5bec8e7..00000000000 --- a/.changeset/slow-terms-dance.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Keep the JetBrains session connection status above the prompt without shifting the chat layout. diff --git a/.changeset/soft-diff-highlights.md b/.changeset/soft-diff-highlights.md new file mode 100644 index 00000000000..1687eb5e886 --- /dev/null +++ b/.changeset/soft-diff-highlights.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Restore readable diff highlighting and collapsed unchanged sections in VS Code themes. diff --git a/.changeset/soft-garlic-care.md b/.changeset/soft-garlic-care.md deleted file mode 100644 index ee6f55a6d61..00000000000 --- a/.changeset/soft-garlic-care.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Refine JetBrains session loading UI and settings actions. diff --git a/.changeset/steady-crabs-scroll.md b/.changeset/steady-crabs-scroll.md deleted file mode 100644 index 9688855539f..00000000000 --- a/.changeset/steady-crabs-scroll.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Preserve JetBrains chat scroll position when reviewing earlier session output. diff --git a/.changeset/steady-indexing-provider.md b/.changeset/steady-indexing-provider.md new file mode 100644 index 00000000000..9afaebdf819 --- /dev/null +++ b/.changeset/steady-indexing-provider.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Prevent saved global indexing provider changes from temporarily reverting in active workspaces. 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/tidy-otters-wait.md b/.changeset/tidy-otters-wait.md deleted file mode 100644 index 151fcfe79af..00000000000 --- a/.changeset/tidy-otters-wait.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/kilo-jetbrains": patch ---- - -Avoid flickering transient loading and connection error states in JetBrains sessions. diff --git a/.changeset/visible-turn-outcomes.md b/.changeset/visible-turn-outcomes.md new file mode 100644 index 00000000000..9697024077b --- /dev/null +++ b/.changeset/visible-turn-outcomes.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Warn when a chat turn stops unexpectedly or ends while tracked to-dos remain unfinished. 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/.changeset/witty-cabin.md b/.changeset/witty-cabin.md new file mode 100644 index 00000000000..47dbc193d64 --- /dev/null +++ b/.changeset/witty-cabin.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Show running badges on active sessions in JetBrains recent and history lists. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 2b7e92a9d70..a8415564209 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,3 +1,9 @@ +## Issue + + + +Fixes # + ## Context @@ -6,11 +12,13 @@ -## Screenshots +## Screenshots / Video + + | before | after | |---|---| @@ -18,18 +26,49 @@ Some description of HOW you achieved it. Perhaps give a high level description o ## How to Test +### Manual/local verification + +- + +### Reviewer test steps + + + +- + +### Blocked checks and substitute verification + + + +- + + + +## Checklist + +- [ ] Issue linked above, or exception explained +- [ ] Tests/verification described +- [ ] Screenshots/video included for visual changes, or marked N/A +- [ ] Changeset considered for user-facing changes +- [ ] I personally reviewed the diff and can explain the changes, including any AI-assisted work. + ## Get in Touch diff --git a/.github/workflows/check-forbidden-strings.yml b/.github/workflows/check-forbidden-strings.yml new file mode 100644 index 00000000000..e199ab4b46e --- /dev/null +++ b/.github/workflows/check-forbidden-strings.yml @@ -0,0 +1,21 @@ +# kilocode_change - new file +name: Check forbidden strings + +on: + pull_request: + workflow_dispatch: + +jobs: + check: + name: Check forbidden strings + if: github.repository == 'Kilo-Org/kilocode' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 # kilocode_change + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + + - uses: oven-sh/setup-bun@v2 + + - name: Run check + run: bun run script/check-forbidden-strings.ts diff --git a/.github/workflows/check-opencode-annotations.yml b/.github/workflows/check-opencode-annotations.yml index 810371d6d3e..e30997d8483 100644 --- a/.github/workflows/check-opencode-annotations.yml +++ b/.github/workflows/check-opencode-annotations.yml @@ -5,7 +5,6 @@ on: paths: - ".github/**" - "github/**" - - "sdks/vscode/**" - "packages/extensions/**" - "packages/opencode/**" - "packages/script/**" @@ -39,6 +38,9 @@ jobs: fi # kilocode_change start + - name: Check Effect Promise facade allowlist + run: bun run script/check-opencode-promise-facades.ts + - name: Check workflow allowlist run: bun run script/check-workflows.ts # kilocode_change end diff --git a/.github/workflows/disabled/daily-issues-recap.yml.disabled b/.github/workflows/disabled/daily-issues-recap.yml.disabled deleted file mode 100644 index 789712ef519..00000000000 --- a/.github/workflows/disabled/daily-issues-recap.yml.disabled +++ /dev/null @@ -1,170 +0,0 @@ -name: daily-issues-recap - -on: - schedule: - # Run at 6 PM EST (23:00 UTC, or 22:00 UTC during daylight saving) - - cron: "0 23 * * *" - workflow_dispatch: # Allow manual trigger for testing - -jobs: - daily-recap: - runs-on: blacksmith-4vcpu-ubuntu-2404 - permissions: - contents: read - issues: read - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - uses: ./.github/actions/setup-bun - - - name: Install opencode - run: curl -fsSL https://kilo.ai/install | bash - - - name: Generate daily issues recap - id: recap - env: - OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - KILO_PERMISSION: | - { - "bash": { - "*": "deny", - "gh issue*": "allow", - "gh search*": "allow" - }, - "webfetch": "deny", - "edit": "deny", - "write": "deny" - } - run: | - # Get today's date range - TODAY=$(date -u +%Y-%m-%d) - - opencode run -m opencode/claude-sonnet-4-5 "Generate a daily issues recap for the Kilo repository. - - TODAY'S DATE: ${TODAY} - - STEP 1: Gather today's issues - Search for all OPEN issues created today (${TODAY}) using: - gh issue list --repo ${{ github.repository }} --state open --search \"created:${TODAY}\" --json number,title,body,labels,state,comments,createdAt,author --limit 500 - - IMPORTANT: EXCLUDE all issues authored by Anomaly team members. Filter out issues where the author login matches ANY of these: - adamdotdevin, Brendonovich, fwang, Hona, iamdavidhill, jayair, kitlangton, kommander, MrMushrooooom, R44VC0RP, rekram1-node, thdxr - This recap is specifically for COMMUNITY (external) issues only. - - STEP 2: Analyze and categorize - For each issue created today, categorize it: - - **Severity Assessment:** - - CRITICAL: Crashes, data loss, security issues, blocks major functionality - - HIGH: Significant bugs affecting many users, important features broken - - MEDIUM: Bugs with workarounds, minor features broken - - LOW: Minor issues, cosmetic, nice-to-haves - - **Activity Assessment:** - - Note issues with high comment counts or engagement - - Note issues from repeat reporters (check if author has filed before) - - STEP 3: Cross-reference with existing issues - For issues that seem like feature requests or recurring bugs: - - Search for similar older issues to identify patterns - - Note if this is a frequently requested feature - - Identify any issues that are duplicates of long-standing requests - - STEP 4: Generate the recap - Create a structured recap with these sections: - - ===DISCORD_START=== - **Daily Issues Recap - ${TODAY}** - - **Summary Stats** - - Total issues opened today: [count] - - By category: [bugs/features/questions] - - **Critical/High Priority Issues** - [List any CRITICAL or HIGH severity issues with brief descriptions and issue numbers] - - **Most Active/Discussed** - [Issues with significant engagement or from active community members] - - **Trending Topics** - [Patterns noticed - e.g., 'Multiple reports about X', 'Continued interest in Y feature'] - - **Duplicates & Related** - [Issues that relate to existing open issues] - ===DISCORD_END=== - - STEP 5: Format for Discord - Format the recap as a Discord-compatible message: - - Use Discord markdown (**, __, etc.) - - BE EXTREMELY CONCISE - this is an EOD summary, not a detailed report - - Use hyperlinked issue numbers with suppressed embeds: [#1234]() - - Group related issues on single lines where possible - - Add emoji sparingly for critical items only - - HARD LIMIT: Keep under 1800 characters total - - Skip sections that have nothing notable (e.g., if no critical issues, omit that section) - - Prioritize signal over completeness - only surface what matters - - OUTPUT: Output ONLY the content between ===DISCORD_START=== and ===DISCORD_END=== markers. Include the markers so I can extract it." > /tmp/recap_raw.txt - - # Extract only the Discord message between markers - sed -n '/===DISCORD_START===/,/===DISCORD_END===/p' /tmp/recap_raw.txt | grep -v '===DISCORD' > /tmp/recap.txt - - echo "recap_file=/tmp/recap.txt" >> $GITHUB_OUTPUT - - - name: Post to Discord - env: - DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_ISSUES_WEBHOOK_URL }} - run: | - if [ -z "$DISCORD_WEBHOOK_URL" ]; then - echo "Warning: DISCORD_ISSUES_WEBHOOK_URL secret not set, skipping Discord post" - cat /tmp/recap.txt - exit 0 - fi - - # Read the recap - RECAP_RAW=$(cat /tmp/recap.txt) - RECAP_LENGTH=${#RECAP_RAW} - - echo "Recap length: ${RECAP_LENGTH} chars" - - # Function to post a message to Discord - post_to_discord() { - local msg="$1" - local content=$(echo "$msg" | jq -Rs '.') - curl -s -H "Content-Type: application/json" \ - -X POST \ - -d "{\"content\": ${content}}" \ - "$DISCORD_WEBHOOK_URL" - sleep 1 - } - - # If under limit, send as single message - if [ "$RECAP_LENGTH" -le 1950 ]; then - post_to_discord "$RECAP_RAW" - else - echo "Splitting into multiple messages..." - remaining="$RECAP_RAW" - while [ ${#remaining} -gt 0 ]; do - if [ ${#remaining} -le 1950 ]; then - post_to_discord "$remaining" - break - else - chunk="${remaining:0:1900}" - last_newline=$(echo "$chunk" | grep -bo $'\n' | tail -1 | cut -d: -f1) - if [ -n "$last_newline" ] && [ "$last_newline" -gt 500 ]; then - chunk="${remaining:0:$last_newline}" - remaining="${remaining:$((last_newline+1))}" - else - chunk="${remaining:0:1900}" - remaining="${remaining:1900}" - fi - post_to_discord "$chunk" - fi - done - fi - - echo "Posted daily recap to Discord" diff --git a/.github/workflows/disabled/daily-pr-recap.yml.disabled b/.github/workflows/disabled/daily-pr-recap.yml.disabled deleted file mode 100644 index ff026917f81..00000000000 --- a/.github/workflows/disabled/daily-pr-recap.yml.disabled +++ /dev/null @@ -1,173 +0,0 @@ -name: daily-pr-recap - -on: - schedule: - # Run at 5pm EST (22:00 UTC, or 21:00 UTC during daylight saving) - - cron: "0 22 * * *" - workflow_dispatch: # Allow manual trigger for testing - -jobs: - pr-recap: - runs-on: blacksmith-4vcpu-ubuntu-2404 - permissions: - contents: read - pull-requests: read - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - uses: ./.github/actions/setup-bun - - - name: Install opencode - run: curl -fsSL https://kilo.ai/install | bash - - - name: Generate daily PR recap - id: recap - env: - OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - KILO_PERMISSION: | - { - "bash": { - "*": "deny", - "gh pr*": "allow", - "gh search*": "allow" - }, - "webfetch": "deny", - "edit": "deny", - "write": "deny" - } - run: | - TODAY=$(date -u +%Y-%m-%d) - - opencode run -m opencode/claude-sonnet-4-5 "Generate a daily PR activity recap for the Kilo repository. - - TODAY'S DATE: ${TODAY} - - STEP 1: Gather PR data - Run these commands to gather PR information. ONLY include OPEN PRs created or updated TODAY (${TODAY}): - - # Open PRs created today - gh pr list --repo ${{ github.repository }} --state open --search \"created:${TODAY}\" --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft,additions,deletions --limit 100 - - # Open PRs with activity today (updated today) - gh pr list --repo ${{ github.repository }} --state open --search \"updated:${TODAY}\" --json number,title,author,labels,createdAt,updatedAt,reviewDecision,isDraft,additions,deletions --limit 100 - - IMPORTANT: EXCLUDE all PRs authored by Anomaly team members. Filter out PRs where the author login matches ANY of these: - adamdotdevin, Brendonovich, fwang, Hona, iamdavidhill, jayair, kitlangton, kommander, MrMushrooooom, R44VC0RP, rekram1-node, thdxr - This recap is specifically for COMMUNITY (external) contributions only. - - - - STEP 2: For high-activity PRs, check comment counts - For promising PRs, run: - gh pr view [NUMBER] --repo ${{ github.repository }} --json comments --jq '[.comments[] | select(.author.login != \"copilot-pull-request-reviewer\" and .author.login != \"github-actions\")] | length' - - IMPORTANT: When counting comments/activity, EXCLUDE these bot accounts: - - copilot-pull-request-reviewer - - github-actions - - STEP 3: Identify what matters (ONLY from today's PRs) - - **Bug Fixes From Today:** - - PRs with 'fix' or 'bug' in title created/updated today - - Small bug fixes (< 100 lines changed) that are easy to review - - Bug fixes from community contributors - - **High Activity Today:** - - PRs with significant human comments today (excluding bots listed above) - - PRs with back-and-forth discussion today - - **Quick Wins:** - - Small PRs (< 50 lines) that are approved or nearly approved - - PRs that just need a final review - - STEP 4: Generate the recap - Create a structured recap: - - ===DISCORD_START=== - **Daily PR Recap - ${TODAY}** - - **New PRs Today** - [PRs opened today - group by type: bug fixes, features, etc.] - - **Active PRs Today** - [PRs with activity/updates today - significant discussion] - - **Quick Wins** - [Small PRs ready to merge] - ===DISCORD_END=== - - STEP 5: Format for Discord - - Use Discord markdown (**, __, etc.) - - BE EXTREMELY CONCISE - surface what we might miss - - Use hyperlinked PR numbers with suppressed embeds: [#1234]() - - Include PR author: [#1234]() (@author) - - For bug fixes, add brief description of what it fixes - - Show line count for quick wins: \"(+15/-3 lines)\" - - HARD LIMIT: Keep under 1800 characters total - - Skip empty sections - - Focus on PRs that need human eyes - - OUTPUT: Output ONLY the content between ===DISCORD_START=== and ===DISCORD_END=== markers. Include the markers so I can extract it." > /tmp/pr_recap_raw.txt - - # Extract only the Discord message between markers - sed -n '/===DISCORD_START===/,/===DISCORD_END===/p' /tmp/pr_recap_raw.txt | grep -v '===DISCORD' > /tmp/pr_recap.txt - - echo "recap_file=/tmp/pr_recap.txt" >> $GITHUB_OUTPUT - - - name: Post to Discord - env: - DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_ISSUES_WEBHOOK_URL }} - run: | - if [ -z "$DISCORD_WEBHOOK_URL" ]; then - echo "Warning: DISCORD_ISSUES_WEBHOOK_URL secret not set, skipping Discord post" - cat /tmp/pr_recap.txt - exit 0 - fi - - # Read the recap - RECAP_RAW=$(cat /tmp/pr_recap.txt) - RECAP_LENGTH=${#RECAP_RAW} - - echo "Recap length: ${RECAP_LENGTH} chars" - - # Function to post a message to Discord - post_to_discord() { - local msg="$1" - local content=$(echo "$msg" | jq -Rs '.') - curl -s -H "Content-Type: application/json" \ - -X POST \ - -d "{\"content\": ${content}}" \ - "$DISCORD_WEBHOOK_URL" - sleep 1 - } - - # If under limit, send as single message - if [ "$RECAP_LENGTH" -le 1950 ]; then - post_to_discord "$RECAP_RAW" - else - echo "Splitting into multiple messages..." - remaining="$RECAP_RAW" - while [ ${#remaining} -gt 0 ]; do - if [ ${#remaining} -le 1950 ]; then - post_to_discord "$remaining" - break - else - chunk="${remaining:0:1900}" - last_newline=$(echo "$chunk" | grep -bo $'\n' | tail -1 | cut -d: -f1) - if [ -n "$last_newline" ] && [ "$last_newline" -gt 500 ]; then - chunk="${remaining:0:$last_newline}" - remaining="${remaining:$((last_newline+1))}" - else - chunk="${remaining:0:1900}" - remaining="${remaining:1900}" - fi - post_to_discord "$chunk" - fi - done - fi - - echo "Posted daily PR recap to Discord" 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/publish.yml b/.github/workflows/publish.yml index 34f35c22ce0..93f79d63cb9 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -175,6 +175,7 @@ jobs: cli_version: ${{ needs.version.outputs.version }} secrets: inherit # kilocode_change end + publish: needs: - version @@ -208,6 +209,7 @@ jobs: with: node-version: "24" registry-url: "https://registry.npmjs.org" + package-manager-cache: false # kilocode_change - name: Install @vscode/vsce run: bun install -g @vscode/vsce @@ -265,7 +267,6 @@ jobs: GH_REPO: ${{ github.repository }} AUR_KEY: ${{ secrets.AUR_KEY }} GITHUB_TOKEN: ${{ steps.committer.outputs.token }} - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NPM_CONFIG_PROVENANCE: true VSCE_PAT: ${{ secrets.VSCE_TOKEN }} OPENVSX_TOKEN: ${{ secrets.OVSX_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 559d65f0a9f..b7db6f7ac68 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 @@ -64,7 +74,7 @@ jobs: turbo-${{ runner.os }}- - name: Run unit tests - run: bun turbo test:ci + run: bun turbo test:ci --filter='!@kilocode/kilo-jetbrains' env: KILO_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }} @@ -88,14 +98,69 @@ jobs: retention-days: 7 path: packages/*/.artifacts/unit/junit.xml + # kilocode_change start + jetbrains: + name: jetbrains + runs-on: blacksmith-4vcpu-ubuntu-2404 + defaults: + run: + shell: bash + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - 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 + with: + cache-read-only: ${{ github.ref != 'refs/heads/main' }} + + - name: Run JetBrains unit tests + run: bun script/test-ci.ts + working-directory: packages/kilo-jetbrains + + - name: Publish JetBrains unit reports + if: always() + uses: mikepenz/action-junit-report@v6 + with: + report_paths: packages/kilo-jetbrains/.artifacts/unit/junit.xml + check_name: "unit results (jetbrains)" + detailed_summary: true + include_time_in_summary: true + fail_on_failure: false + + - name: Upload JetBrains unit artifacts + if: always() + uses: actions/upload-artifact@v7 + with: + name: unit-jetbrains-${{ github.run_attempt }} + include-hidden-files: true + if-no-files-found: ignore + retention-days: 7 + path: packages/kilo-jetbrains/.artifacts/unit/junit.xml + # kilocode_change end + required: name: test (linux) runs-on: blacksmith-4vcpu-ubuntu-2404 needs: - unit + - jetbrains if: always() steps: - name: Verify upstream test jobs passed run: | echo "unit=${{ needs.unit.result }}" + echo "jetbrains=${{ needs.jetbrains.result }}" test "${{ needs.unit.result }}" = "success" + test "${{ needs.jetbrains.result }}" = "success" diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 6c7ae420953..b4d0da57a9f 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -8,7 +8,8 @@ on: workflow_dispatch: jobs: - typecheck: + typecheck-js: + name: typecheck-js runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - name: Checkout repository @@ -17,5 +18,49 @@ jobs: - name: Setup Bun uses: ./.github/actions/setup-bun - - name: Run typecheck - run: bun typecheck + # kilocode_change start + - name: Run TypeScript typecheck + run: bun turbo typecheck --filter='!@kilocode/kilo-jetbrains' + # kilocode_change end + + # kilocode_change start + typecheck-jetbrains: + name: typecheck-jetbrains + runs-on: blacksmith-4vcpu-ubuntu-2404 + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - 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 + with: + cache-read-only: ${{ github.ref != 'refs/heads/main' }} + + - name: Run JetBrains typecheck + run: ./gradlew typecheck + working-directory: packages/kilo-jetbrains + + required: + name: typecheck + runs-on: blacksmith-4vcpu-ubuntu-2404 + needs: + - typecheck-js + - typecheck-jetbrains + if: always() + steps: + - name: Verify typecheck jobs passed + run: | + echo "typecheck-js=${{ needs.typecheck-js.result }}" + echo "typecheck-jetbrains=${{ needs.typecheck-jetbrains.result }}" + test "${{ needs.typecheck-js.result }}" = "success" + test "${{ needs.typecheck-jetbrains.result }}" = "success" + # kilocode_change end diff --git a/.gitignore b/.gitignore index 91bf7230c02..812dece5848 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .husky/_ .DS_Store +.secrets/ node_modules .worktrees .sst @@ -33,9 +34,11 @@ a.out target .scripts .direnv/ +packages/kilo-jetbrains/build-tasks/.gradle/ # Local dev files opencode-dev +.kilo-dev/ UPCOMING_CHANGELOG.md logs/ *.bun-build diff --git a/.kilo/agent/upstream-merge.md b/.kilo/agent/upstream-merge.md index d8aff1fd6c0..559f59e890a 100644 --- a/.kilo/agent/upstream-merge.md +++ b/.kilo/agent/upstream-merge.md @@ -44,6 +44,12 @@ permission: Resolve the manual part of an upstream merge. +**Do not load the `kilocode-merge-minimizer` skill.** That skill is for +authoring new Kilo changes against shared upstream files; during an upstream +merge it gives the wrong guidance (it nudges toward extracting Kilo logic out +of conflict regions, which is exactly the opposite of what merge resolution +needs). Follow the rules in this agent file instead. + The user will provide the upstream version (for example `v1.1.50` or `1.1.50`) in their first message. If they don't, infer it from the current branch name, from `upstream-merge-report-.md`, or from the newest relevant report @@ -243,7 +249,15 @@ be broken. Check every auto-merged file for: files changed. Note that this tool compares against the merge base via `HEAD` and will be silent until the merge commit lands - other CI guards that touched files imply (knip for `kilo-vscode/`, - `check-kilocode-change`, source-links, visual regression) + `check-kilocode-change`, source-links, visual regression, + `script/check-forbidden-strings.ts`) +- if you encounter a hardcoded upstream URL, repo path, or attribution string + during conflict resolution that obviously shouldn't ship in Kilo (e.g. another + `https://opencode.ai/...` link, an `anomalyco/opencode` reference, an + attribution header naming "opencode"), suggest adding a literal pattern for + it to `script/check-forbidden-strings.ts` in the merge summary so future + merges catch it automatically. Don't add it silently mid-merge — flag it for + the user. ### 9. Commit with the standard message diff --git a/.kilo/command/review-upstream-merge.md b/.kilo/command/review-upstream-merge.md new file mode 100644 index 00000000000..36237d81c1c --- /dev/null +++ b/.kilo/command/review-upstream-merge.md @@ -0,0 +1,69 @@ +--- +description: Review an upstream merge PR and write reports +--- + +Review $1. + +Branch off so we have all the code for the reviewed PR. Then run the following reviews in parallel, using one subagent per review, and save each result in the named markdown file at the repository root. A human will read those files, so if in doubt add a finding for human verification. + +When all subagents are done, commit the report files and create a draft PR with the reviewed PR branch as base. + +Do not include exhaustive per-file checklists in the report files. Summarize the scope and methodology, then list only findings, notable non-findings, command outputs, and limitations. + +## KILOCODE_CHANGE_MARKERS.md + +Review carefully, file by file, whether we accidentally removed any `kilocode_change` marker. + +Get the full list of files changed in the reviewed PR. Check each changed file. For each file, compare both our `main` branch and the upstream-merged version in the reviewed PR. Determine whether any marker removal, marker move, or Kilo-specific change makes sense, and comment on that. + +Do not include a full "Files Checked" section. Mention the number of changed files checked, but only list files that have findings or need human verification. + +## INFRASTRUCTURE_CHANGE.md + +Review whether this PR adds, removes, or changes any infrastructure, such as GitHub Actions, CI config, release/deploy scripts, Docker/build infrastructure, package manager/workspace infrastructure, repository automation, issue templates, changelog automation, or generated SDK/build automation. + +We want to merge upstream code but keep our own infrastructure, so flag anything infrastructure-related. When in doubt, add a finding and say a human should check manually. + +Do not include a full "Files Checked" section. Only list infrastructure-relevant files and findings. + +## OPENCODE_MENTIONS.md + +Check whether the merge now mentions OpenCode somewhere user-facing instead of Kilo, or links to OpenCode web properties. + +Focus on UI strings, docs, help text, package metadata shown to users, URLs, CLI output, config docs, generated SDK/OpenAPI descriptions, and error messages. + +## UNNECESSARY_MARKERS.md + +Check whether any merged files now use `kilocode_change` markers without any actual difference to upstream. + +Use `script/upstream/find-reset-candidates.ts --dry-run` to see whether any files changed in the PR are actually now identical to upstream. If you find candidates, verify them with `script/upstream/reset-to-upstream.ts --dry-run`. + +## BROKEN_PIPELINE_CHAINS.md + +Review this PR for broken end-to-end chains where our custom functionality requires changes across multiple files or layers, but the merge may have removed or altered an intermediate step. The code may still compile, so these issues can be silent. + +Look for cases like: + +- A parameter that is set but never read. +- A field that is populated but never passed through. +- An event that is emitted but no longer handled. +- A config option that is defined but never propagated to where it is used. +- A type definition extended on one side but not consumed on the other. + +For each `kilocode_change` in this PR, trace the full chain: where the value or behavior is introduced, where it needs to flow through, and where it is ultimately consumed. Verify every link in the chain still exists after the merge. + +Pay special attention to: + +- Props or parameters passed through multiple component or function layers. +- Values written to state, context, or storage that are read elsewhere. +- Message types, events, or IPC handlers where sender and receiver must match. +- Configuration or feature flags defined in one place and checked in another. +- Type definitions extended on one side but consumed on the other. + +When in doubt, add a finding. A human will verify it. Compiling code is not proof the chain is intact. + +## TESTS.md + +Check whether this PR removed any Kilo-specific tests. + +Kilo-specific tests may live in paths containing `kilo` or `kilocode`, or may include Kilo-specific assertions, fixtures, or `kilocode_change` markers. diff --git a/.opencode-version b/.opencode-version index 9866d345648..2aa995bb4eb 100644 --- a/.opencode-version +++ b/.opencode-version @@ -1 +1 @@ -v1.14.33 +v1.14.41 diff --git a/.opencode/agent/triage.md b/.opencode/agent/triage.md index d85586c6678..69ab7ac86b6 100644 --- a/.opencode/agent/triage.md +++ b/.opencode/agent/triage.md @@ -1,7 +1,7 @@ --- mode: primary hidden: true -model: kilo/minimax/minimax-m2.5 +model: openai/gpt-5-nano color: "#44BA81" tools: "*": false @@ -14,95 +14,38 @@ Use your github-triage tool to triage issues. This file is the source of truth for ownership/routing rules. -## Labels +Assign issues by choosing the team with the strongest overlap. The github-triage tool will assign a random member from that team. -### windows +Do not add labels to issues. Only assign an owner. -Use for any issue that mentions Windows (the OS). Be sure they are saying that they are on Windows. +When calling github-triage, pass one of these team values: tui, desktop_web, core, inference, windows, jetbrains, extension -- Use if they mention WSL too +## Teams -#### perf +### TUI -Performance-related issues: +Terminal UI issues, including rendering, keybindings, scrolling, terminal compatibility, SSH behavior, crashes in the TUI, and low-level TUI performance. -- Slow performance -- High RAM usage -- High CPU usage +### Desktop / Web -**Only** add if it's likely a RAM or CPU issue. **Do not** add for LLM slowness. +Desktop application and browser-based app issues, including `opencode web`, desktop-specific UI behavior, packaging, and web view problems. -#### nix +### Core -**Only** add if the issue explicitly mentions nix. +Core opencode server and harness issues, including sqlite, snapshots, memory, API behavior, agent context construction, tool execution, provider integrations, model behavior, documentation, and larger architectural features. -If the issue does not mention nix, do not add nix. +### Inference -If the issue mentions nix, assign to `catrielmuller`. +Kilo Provider, Provider implementation, and billing issues. -#### core +### Windows -Use for core server issues in `packages/opencode/`, excluding `packages/opencode/src/cli/cmd/tui/`. +Windows-specific issues, including native Windows behavior, WSL interactions, path handling, shell compatibility, and installation or runtime problems that only happen on Windows. -Examples: +### Jetbrains -- LSP server behavior -- Harness behavior (agent + tools) -- Feature requests for server behavior -- Agent context construction -- API endpoints -- Provider integration issues -- New, broken, or poor-quality models +Jetbrains extension related issues. -#### vscode +### Extension -Use for issues related to the VS Code extension in `packages/kilo-vscode/`. - -#### gateway - -Use for issues related to the Kilo Gateway in `packages/kilo-gateway/`. - -When assigning to people here are the following rules: - -Nix: -ONLY assign if the issue will have the "nix" label. - -- catrielmuller - -Models / Providers: -Use for issues about model quality, provider integrations, or broken/new models. - -- chrarnoldus - -Cloud Agents: -Use for issues about cloud agent behavior or infrastructure. - -- pandemicsyn -- eshurakov - -Core (`packages/opencode/...`): - -- kevinvandijk -- marius-kilocode -- catrielmuller - -VSCode Extension (`packages/kilo-vscode/...`): - -- markijbema - -Kilo Gateway (`packages/kilo-gateway/...`): - -- jrf0110 - -Windows: - -- catrielmuller (assign any issue that mentions Windows or is likely Windows-specific) - -Determinism rules: - -- If "nix" label is added but title + body does not mention nix/nixos, the tool will drop "nix" -- If title + body mentions nix/nixos, assign to `catrielmuller` -- If "vscode" label is added, assign to `markijbema` -- If "gateway" label is added, assign to `jrf0110` - -In all other cases, choose the team/section with the most overlap with the issue and assign a member from that team at random. +VSCode Extension related issues. diff --git a/.opencode/command/changelog.md b/.opencode/command/changelog.md index 4cd30a704a4..b28d963d005 100644 --- a/.opencode/command/changelog.md +++ b/.opencode/command/changelog.md @@ -18,9 +18,12 @@ Do not use `git log` or author metadata when deciding attribution. Rules: -- Write the final file with sections in this order: +- Write the final file with release sections in this order: `## Core`, `## TUI`, `## Desktop`, `## SDK`, `## Extensions` - Only include sections that have at least one notable entry +- Within each release section, keep bug fixes grouped under `### Bugfixes` +- Keep other notable entries under `### Improvements` when a section has bug fixes too +- Omit empty subsections - Keep one bullet per commit you keep - Skip commits that are entirely internal, CI, tests, refactors, or otherwise not user-facing - Start each bullet with a capital letter diff --git a/.opencode/tool/github-triage.ts b/.opencode/tool/github-triage.ts index 8f80f868577..ce07fdebc60 100644 --- a/.opencode/tool/github-triage.ts +++ b/.opencode/tool/github-triage.ts @@ -1,15 +1,16 @@ /// import { tool } from "@kilocode/plugin" -const TEAM = { - desktop: ["adamdotdevin", "iamdavidhill", "Brendonovich", "nexxeln"], - zen: ["fwang", "MrMushrooooom"], - tui: ["kommander", "rekram1-node", "simonklee"], - core: ["kitlangton", "rekram1-node", "jlongster"], - docs: ["R44VC0RP"], - windows: ["Hona"], -} as const -const ASSIGNEES = [...new Set(Object.values(TEAM).flat())] +// kilocode_change start +const TEAM = { + tui: ["catrielmuller"], + desktop_web: ["markijbema"], + core: ["markijbema", "marius-kilocode", "catrielmuller", "chrarnoldus", "imanolmzd-svg"], + jetbrains: ["kirillk"], + inference: ["chrarnoldus", "markijbema"], + windows: ["catrielmuller"], +} as const +// kilocode_change end function pick(items: readonly T[]) { return items[Math.floor(Math.random() * items.length)]! @@ -38,70 +39,27 @@ async function githubFetch(endpoint: string, options: RequestInit = {}) { } export default tool({ - description: `Use this tool to assign and/or label a GitHub issue. + description: `Use this tool to assign a GitHub issue. -Choose labels and assignee using the current triage policy and ownership rules. -Pick the most fitting labels for the issue and assign one owner. - -If unsure, choose the team/section with the most overlap with the issue and assign a member from that team at random.`, +Provide the team that should own the issue. This tool picks a random assignee from that team and does not apply labels.`, args: { - assignee: tool.schema - .enum(["thdxr", "adamdotdevin", "rekram1-node", "fwang", "jayair", "kommander"]) - .describe("The username of the assignee") - .default("rekram1-node"), - labels: tool.schema - .array(tool.schema.enum(["nix", "opentui", "perf", "desktop", "zen", "docs", "windows"])) - .describe("The labels(s) to add to the issue") - .default([]), + team: tool.schema + .enum(Object.keys(TEAM) as [keyof typeof TEAM, ...(keyof typeof TEAM)[]]) + .describe("The owning team"), }, async execute(args) { const issue = getIssueNumber() - // const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }) - const owner = "anomalyco" - const repo = "opencode" + // kilocode_change start + const owner = "Kilo-Org" + const repo = "kilocode" + // kilocode_change end + const assignee = pick(TEAM[args.team]) - const results: string[] = [] - - if (args.assignee === "adamdotdevin" && !args.labels.includes("desktop")) { - throw new Error("Only desktop issues should be assigned to adamdotdevin") - } - - if (args.assignee === "fwang" && !args.labels.includes("zen")) { - throw new Error("Only zen issues should be assigned to fwang") - } - - if (args.assignee === "kommander" && !args.labels.includes("opentui")) { - throw new Error("Only opentui issues should be assigned to kommander") - } - - // await octokit.rest.issues.addAssignees({ - // owner, - // repo, - // issue_number: issue, - // assignees: [args.assignee], - // }) await githubFetch(`/repos/${owner}/${repo}/issues/${issue}/assignees`, { method: "POST", - body: JSON.stringify({ assignees: [args.assignee] }), + body: JSON.stringify({ assignees: [assignee] }), }) - results.push(`Assigned @${args.assignee} to issue #${issue}`) - const labels: string[] = args.labels.map((label) => (label === "desktop" ? "web" : label)) - - if (labels.length > 0) { - // await octokit.rest.issues.addLabels({ - // owner, - // repo, - // issue_number: issue, - // labels, - // }) - await githubFetch(`/repos/${owner}/${repo}/issues/${issue}/labels`, { - method: "POST", - body: JSON.stringify({ labels }), - }) - results.push(`Added labels: ${args.labels.join(", ")}`) - } - - return results.join("\n") + return `Assigned @${assignee} from ${args.team} to issue #${issue}` }, }) diff --git a/AGENTS.md b/AGENTS.md index 34fec96cc6a..e1fc8dc1238 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,7 +12,7 @@ Kilo CLI is an open source AI coding agent that generates code from natural lang - **Dev**: `bun run dev` (runs from root) or `bun run --cwd packages/opencode --conditions=browser src/index.ts` - **Dev with params**: `bun dev -- help` - **Extension**: `bun run extension` (build + launch VS Code with the extension in dev mode). Pass `--no-build` to skip the build. -- **Typecheck**: `bun turbo typecheck` (uses `tsgo`, not `tsc`) +- **Typecheck**: `bun turbo typecheck` (uses `tsgo`, not `tsc`). Includes the JetBrains plugin — requires Java 21. Check with `java -version` before running. If missing, install via SDKMAN: `sdk install java 21-tem && sdk use java 21-tem`. If SDKMAN is not installed, see https://sdkman.io/install. - **Test**: `bun test` from `packages/opencode/` (NOT from root -- root blocks tests) - **Single test**: `bun test ./test/tool/tool-define.test.ts` from `packages/opencode/` - **CLI build artifact size check**: after `bun run script/build.ts --single --skip-install` in `packages/opencode/`, use `du -h dist/*/*/bin/kilo` (scoped package output lives under `dist/@kilocode/`) @@ -21,6 +21,7 @@ Kilo CLI is an open source AI coding agent that generates code from natural lang - **Source links**: After adding or changing URLs in `packages/kilo-vscode/`, `packages/kilo-vscode/webview-ui/`, or `packages/opencode/src/`, run `bun run script/extract-source-links.ts` from the repo root and commit the updated `packages/kilo-docs/source-links.md`. CI runs this check — the build fails if the file is stale. - **kilocode_change check**: `bun run check-kilocode-change` from `packages/kilo-vscode/`. CI runs this — `kilocode_change` is a marker for upstream merge conflicts and must not appear in `packages/kilo-vscode/` or `packages/kilo-ui/` (these are entirely Kilo Code additions). Remove the markers before pushing. - **opencode annotation check**: `bun run script/check-opencode-annotations.ts` from repo root. CI runs this on PRs touching `packages/opencode/` — every Kilo-specific change in shared opencode files must be annotated with `kilocode_change` markers. Exempt paths (no markers needed): `packages/opencode/src/kilocode/`, `packages/opencode/test/kilocode/`, and any path containing `kilocode` in the name. +- **Effect facade ratchet**: Do not add runtime-backed Promise facades to shared `packages/opencode/src` Effect services; use service dependencies, `AppRuntime`, or Kilo-owned boundaries. Run `bun run script/check-opencode-promise-facades.ts` when touching service adapters. - **workflow allowlist**: `bun run script/check-workflows.ts` from repo root. CI runs this as part of the annotations workflow — any `.yml` / `.yaml` file added to or removed from `.github/workflows/` must be reflected in the hardcoded list in `script/check-workflows.ts`. Prevents upstream-merged workflows from silently starting to run in our CI. - **Backend/SDK programmatic testing**: see [TESTING.md](./TESTING.md) for spawning the local main-branch backend (`bun dev serve`) and driving it via `curl` — use this instead of `kilo serve` (prod binary) when testing backend fixes. @@ -34,6 +35,7 @@ Before saying an implementation is ready, run the smallest relevant checks that | CLI | From `packages/opencode/`: `bun run typecheck`, `bun test` or targeted `bun test ./path/to/file.test.ts` | | VS Code extension | From `packages/kilo-vscode/`: `bun run typecheck`, `bun run lint`, `bun run test:unit` or `bun run test` | | Extension build/package | From `packages/kilo-vscode/`: `bun run compile` or `bun run package` when touching build, packaging, SDK, or webview integration paths | +| JetBrains plugin | From `packages/kilo-jetbrains/`: `./gradlew typecheck`, `./gradlew test`. Requires Java 21 — check first with `java -version`. Install via SDKMAN if missing: `sdk install java 21-tem && sdk use java 21-tem`. | | CI-only guards | Run affected guards documented above, such as `bun run knip`, `bun run check-kilocode-change`, `bun run script/check-opencode-annotations.ts`, or source link extraction | Never run root `bun test`; the root script prints `do not run tests from root` and exits with code 1. Use package-level tests instead. @@ -53,8 +55,7 @@ Extension-specific settings should live in the Kilo extension settings, not defa ## Package Instructions -- When a task primarily touches `packages/kilo-jetbrains/`, read `packages/kilo-jetbrains/AGENTS.md` before planning or editing. -- For JetBrains Kotlin/Swing UI work, also apply `packages/kilo-jetbrains/.kilo/skills/jetbrains-ui-style/SKILL.md`. +- When a task primarily touches `packages/kilo-jetbrains/`, read `packages/kilo-jetbrains/AGENTS.md` before planning or editing. It covers split-mode architecture, IntelliJ source lookup, threading fundamentals, UI guidelines, and session component architecture. ## Monorepo Structure @@ -89,17 +90,6 @@ Prefer `const`. Good: -### Naming Enforcement (Read This) - -THIS RULE IS MANDATORY FOR AGENT WRITTEN CODE. - -- Use single word names by default for new locals, params, and helper functions. -- Multi-word names are allowed only when a single word would be unclear or ambiguous. -- Do not introduce new camelCase compounds when a short single-word alternative is clear. -- Before finishing edits, review touched lines and shorten newly introduced identifiers where possible. -- Good short names to prefer: `pid`, `cfg`, `err`, `opts`, `dir`, `root`, `child`, `state`, `timeout`. -- Examples to avoid unless truly required: `inputPID`, `existingClient`, `connectTimeout`, `workerPath`. - ```ts const foo = condition ? 1 : 2 ``` @@ -113,6 +103,17 @@ if (condition) foo = 1 else foo = 2 ``` +### Naming Enforcement (Read This) + +THIS RULE IS MANDATORY FOR AGENT WRITTEN CODE. + +- Use single word names by default for new locals, params, and helper functions. +- Multi-word names are allowed only when a single word would be unclear or ambiguous. +- Do not introduce new camelCase compounds when a short single-word alternative is clear. +- Before finishing edits, review touched lines and shorten newly introduced identifiers where possible. +- Good short names to prefer: `pid`, `cfg`, `err`, `opts`, `dir`, `root`, `child`, `state`, `timeout`. +- Examples to avoid unless truly required: `inputPID`, `existingClient`, `connectTimeout`, `workerPath`. + ### Avoid else statements Prefer early returns or using an `iife` to avoid else statements. @@ -219,7 +220,7 @@ Changeset descriptions appear directly in release notes and are read by end user ## Pull Requests -PR descriptions should be 2-3 lines covering **what** changed and **why**. Focus on intent and context a reviewer can't get from the diff — skip file-by-file inventories, test result summaries, and anything obvious from the code itself. +PR descriptions should explain **what** changed, **why** the change is needed, and the intent or constraints a reviewer cannot infer from the diff alone. Keep simple PRs brief, but give non-trivial changes enough context to stand on their own. Skip file-by-file inventories, test result summaries, and anything obvious from the code itself. ## GitHub Issues diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9bb4a775994..41ecbcb24ea 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,16 +14,101 @@ There are lots of ways to contribute to the project: The Kilo Community is [on Discord](https://kilo.ai/discord). +## Prerequisites + +- **Bun 1.3.13+** — required for all packages. +- **Java 21** — required by the JetBrains plugin. The root `bun turbo typecheck` and `bun turbo test:ci` commands include `@kilocode/kilo-jetbrains` and will fail without Java 21. + + The preferred way to install Java is via [SDKMAN](https://sdkman.io/install): + + ```bash + # Install SDKMAN (if not already installed) + curl -s "https://get.sdkman.io" | bash + + # Install and activate Java 21 (Eclipse Temurin) + sdk install java 21-tem + sdk use java 21-tem + + # Verify + java -version + ``` + + If you don't plan to work on the JetBrains plugin, you can still run non-JetBrains checks directly: + + ```bash + bun turbo typecheck --filter=!@kilocode/kilo-jetbrains + ``` + ## Developing Kilo CLI -- **Requirements:** Bun 1.3.13+ -- Install dependencies and start the dev server from the repo root: +- **Requirements:** Bun 1.3.13+, Java 21 (see [Prerequisites](#prerequisites) above) +- Install dependencies and start the CLI from the repo root: ```bash bun install bun dev ``` + `bun dev` and `bun run dev` both run the local CLI. For the VS Code extension, use `bun run extension`. + +## Common Checks + +From the repo root: + +```bash +bun install +bun run lint +bun run typecheck +``` + +`bun run typecheck` wraps `bun turbo typecheck`. Use `bun turbo typecheck --force` if you need to bypass the Turbo cache. + +Do **not** run `bun test` from the repo root. The root test script intentionally exits with failure so tests run from the package that owns them. + +### CLI checks + +From `packages/opencode/`: + +```bash +bun run typecheck +bun test +bun test ./path/to/file.test.ts +``` + +For backend/API validation, see [`TESTING.md`](./TESTING.md). It covers starting the local backend with `bun dev serve` and making `curl` requests against it. After changing server endpoints in `packages/opencode/src/server/`, run `./script/generate.ts` from the repo root to regenerate `packages/sdk/js/`. + +### VS Code extension checks + +From `packages/kilo-vscode/`: + +```bash +bun run typecheck +bun run lint +bun run test:unit +bun run test +bun run compile +bun run package +``` + +### Documentation checks + +From the repo root: + +```bash +bun run --filter @kilocode/kilo-docs test +bun run --filter @kilocode/kilo-docs build +bun run --filter @kilocode/kilo-docs dev +``` + +For manual docs validation, run the docs site locally, preview the affected page, and check changed links and rendered content. + +### Guardrails + +- User-facing changes usually need a changeset (`bunx changeset add` or a file under `.changeset/`). +- After changing server endpoints, regenerate the SDK with `./script/generate.ts`. +- After adding or changing guarded URLs in `packages/kilo-vscode/`, `packages/kilo-vscode/webview-ui/`, or `packages/opencode/src/`, run `bun run script/extract-source-links.ts` from the repo root. +- When editing shared `packages/opencode/` files, keep Kilo changes small and mark Kilo-only edits with `// kilocode_change` for a single line or `// kilocode_change start` / `// kilocode_change end` for a block. Do not add these markers inside `kilocode`-named paths. + ### Developing the VS Code Extension Build and launch the extension in an isolated VS Code instance: @@ -34,6 +119,23 @@ bun run extension # Build + launch in dev mode This auto-detects VS Code on macOS, Linux, and Windows. Override with `--app-path PATH` or `VSCODE_EXEC_PATH`. Use `--insiders` to prefer Insiders, `--workspace PATH` to open a specific folder, or `--clean` to reset cached state. +### Developing the JetBrains Plugin + +Requires Java 21 (see [Prerequisites](#prerequisites)). From `packages/kilo-jetbrains/`: + +```bash +./gradlew typecheck # Compile-check all Kotlin sources +./gradlew test # Run all tests (backend + frontend) +./gradlew runIde # Launch sandboxed IntelliJ with the plugin +``` + +Or via the root turbo filter to run only JetBrains checks from the repo root: + +```bash +bun turbo typecheck --filter=@kilocode/kilo-jetbrains +bun turbo test:ci --filter=@kilocode/kilo-jetbrains +``` + ### Running against a different directory By default, `bun dev` runs Kilo CLI in the `packages/opencode` directory. To run it against a different directory or repository: @@ -146,14 +248,69 @@ Current required fields by issue type: ## Pull Request Expectations +Contributor guidance exists to protect maintainer review time and keep reviews focused on work that is ready to evaluate. + - **UI Changes:** Include screenshots or videos (before/after). - **Logic Changes:** Explain how you verified it works. -## Issue First Policy +### Contribution Ownership and AI Assistance -All pull requests must reference an existing issue. +AI and coding agents are allowed, but contributors own the work they submit. Before requesting review, make sure you personally understand the change, have tested it appropriately, can explain the diff, and understand how it interacts with the affected packages and the rest of the repo. -This helps reviewers understand the problem statement, discussion, and intended scope before reviewing the code change. +If you use an agent, start it from the repo root so the root `AGENTS.md` is available. When your change touches a package with its own guidance, read and follow that package's `AGENTS.md` or contributor docs too. + +Maintainers may close PRs that appear to be submitted without credible contributor ownership or understanding, including AI-assisted work that the contributor cannot explain or has not meaningfully reviewed. + +### Tracker Use and Automation + +Do not submit batches of agent-generated, untested, or weakly reviewed PRs. + +Please keep concurrent PRs focused and limited. As a rule, open no more than three PRs at a time, especially if you are a new contributor. Prioritize high-impact or high-priority issues first instead of opening many speculative fixes. If a contributor opens a large batch of low-value or duplicative PRs, maintainers may close the batch and ask the contributor to choose one PR to reopen, focus, and bring up to the documented review bar before submitting more. + +For issues, do not mass-create tickets through automation or agents. Search existing issues first, open issues only when you have enough context for someone to act, and prioritize the most important reports instead of filing every possible finding. Maintainers may close duplicate, low-signal, automated, or weakly reviewed issues without action. + +Repeated disregard of this contribution guide, or high-volume automated or agent-generated tracker spam across issues or PRs, may result in maintainers blocking the responsible account. + +### Bug Bounties + +Kilo has bug bounties. To be eligible, make sure your GitHub account is connected in your Kilo account. + +### Testing Evidence + +Every PR marked ready for review must include testing evidence. A bare `Not tested` or `N/A` answer is not sufficient. + +Choose checks that match the files touched. Include command results and manual/local verification; for visual CLI or extension changes, include screenshots or videos. Docs-only, config-only, and similar changes still need concrete evidence, such as a relevant command check or preview. + +If you cannot complete a relevant command, include all of the following in the PR: + +- The command you attempted or would normally run +- The blocker or failure that prevented completion +- The substitute verification you performed instead + +See [Testing Evidence for Pull Requests](packages/kilo-docs/pages/contributing/development-environment.md#testing-evidence-for-pull-requests) for more examples. Agent limitations, local resource constraints, OOM constraints, or an agent prompt that says to skip tests do not waive this requirement. Draft PRs may be incomplete until they are marked ready for review. Maintainers may still defer or close review at their discretion. + +Our issue-first policy asks contributors to reference an existing issue when opening a PR. This helps reviewers understand the problem statement, discussion, and intended scope before reviewing the code change. + +A review-ready PR description should explain: + +- What problem is being solved +- Why the change is needed +- Important implementation choices or tradeoffs reviewers cannot infer from the diff +- How the change was tested or verified + +Keep the description focused on context reviewers cannot infer from the diff. Skip file-by-file summaries, placeholders, and other filler. + +For visual UI changes, include screenshots or video showing the relevant before/after or resulting state. + +Maintainers may close or decline review of PRs presented as review-ready at their discretion when they lack: + +- Linked issue context +- A clear what/why explanation +- Credible testing evidence +- Credible contributor ownership of AI-assisted work +- Relevant UI proof for visual UI changes + +When a PR is close to this bar, addresses important work, or would benefit from further shaping, maintainers may ask for specific fixes instead of closing or declining review. Contributors may reopen or resubmit once the PR meets the documented bar. ## PR Titles @@ -170,6 +327,8 @@ Use conventional commit style PR titles such as: To keep our backlog manageable, we automatically close inactive issues and PRs after a period of inactivity. This isn't a judgment on quality — older items tend to lose context over time and we'd rather start fresh if they're still relevant. Feel free to reopen or create a new issue/PR if you're still working on something! +Maintainers may also close issues or PRs that disregard the contribution guide, bypass required context, or lack credible contributor ownership of AI-assisted work. + ## Style Preferences - **Functions:** Keep logic within a single function unless breaking it out adds clear reuse. 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/REVIEW.md b/REVIEW.md new file mode 100644 index 00000000000..72876657091 --- /dev/null +++ b/REVIEW.md @@ -0,0 +1,90 @@ +# REVIEW.md + +Guidance for the automated reviewer (kilo-code-bot) on PRs in this repo. + +The goal of the review is to catch things CI **cannot** catch: bugs, design issues, and judgment calls about style and fork hygiene. Be helpful, not pedantic — frame everything as a suggestion the human can accept or reject. + +## Don't duplicate CI + +CI already runs and will report failures directly. Do **not** comment on: + +- Lint, formatting, or typecheck errors (root `lint`, `turbo typecheck`) +- Test failures (CLI tests, vscode tests) +- `knip` unused exports +- `kilocode_change` marker rules — both directions: + - Missing markers on shared opencode files (`script/check-opencode-annotations.ts`) + - Markers present in kilo-only paths like `packages/kilo-vscode/`, `packages/kilo-ui/`, `packages/opencode/src/kilocode/` (`bun run check-kilocode-change`) +- Workflow allowlist drift (`script/check-workflows.ts`) +- Stale `packages/kilo-docs/source-links.md` (`script/extract-source-links.ts`) +- Markdown table padding (`script/check-md-table-padding.ts`) +- Visual regression snapshots (CI generates baselines on Linux) +- SDK regeneration drift (`generate.yml`) +- Generated artifact freshness (`check-kilo-generated-artifacts.yml`) +- Docs link checks, nix evals, container builds + +If the only issue you'd raise is one of the above, just say `lgtm`. + +## What to focus on + +### 1. Bugs and correctness + +Read enough of the surrounding file to actually understand the change — diffs alone hide context. Look for: + +- Logic errors, off-by-one, wrong conditions, swapped arguments +- Unhandled error paths, swallowed promises, missing `await` +- Race conditions, especially around session/process lifecycle in the CLI and Agent Manager +- Resource leaks (unclosed file handles, child processes, subscriptions) +- Inputs that aren't validated where they cross trust boundaries (server routes, IPC, config loading) + +### 2. Style guide judgment calls + +The full guide is in `AGENTS.md`. Don't be a zealot — only flag actual violations, and recognize when the existing code already complies through a different mechanism. + +- **No `let`**: prefer `const` with ternary or IIFE (`packages/opencode/src/util/iife.ts`). But `let` is fine when it's genuinely the simplest option; don't demand IIFE rewrites for trivial cases. +- **No `else`**: prefer early returns. Don't complain about `else` if the code already uses early returns elsewhere. You **may** flag excessive nesting regardless. +- **No empty `catch`**: always flag — empty catches hide bugs. +- **Avoid `try`/`catch` where possible**: if a try/catch is added, consider whether it's needed at all. +- **Avoid `any`**: flag new `any` usage unless there's a clear reason. +- **Single-word names**: prefer `cfg`, `pid`, `dir`, `opts`, `err` over `inputPID`, `connectTimeout`. Only flag newly introduced multi-word names where a clear single-word alternative exists. +- **Avoid unnecessary destructuring**: prefer `obj.a` over `const { a } = obj` to preserve context. +- **Bun APIs**: prefer `Bun.file()` etc. over node equivalents in CLI code. +- **Type inference**: avoid explicit annotations unless needed for exports/clarity. + +When suggesting fixes, ensure the suggestion is valid TypeScript (matched braces, correct syntax). Prefer prose comments over `suggestion` blocks unless the fix is trivially mechanical. + +### 3. Fork merge hygiene + +Kilo CLI is a fork of opencode. Minimizing diff against upstream is a top priority. + +- If a change modifies a shared opencode file (anything under `packages/opencode/` not in a path containing `kilocode`), ask whether the logic could live in a Kilo-only directory instead (`packages/opencode/src/kilocode/`, `packages/kilo-gateway/`, etc.) or be reduced to a smaller hook. +- Refactors or reorganizations of upstream code are a red flag — flag them unless clearly justified. +- See `.kilo/skills/kilocode-merge-minimizer/SKILL.md` for the decision rules. + +### 4. Cloud config schema mirror + +When `Config.Info` in `packages/opencode/src/config/config.ts` gains a new `kilocode_change` field, the matching JSON Schema entry must also be added in the cloud repo (`apps/web/src/app/config.json/extras.ts`). CI does **not** check this — flag it as a reminder if you see a new config field added. + +### 5. Test quality + +- Tests should exercise real implementation, not duplicate logic into the test. +- Mocks should be avoided where reasonable; flag mock-heavy tests that look like they're testing the mock rather than the code. +- New behavior in `packages/opencode/` should generally come with a test under `packages/opencode/test/`. + +### 6. User-facing changes + +- Features, bug fixes, and breaking changes should include a changeset (`.changeset/*.md`). If a PR clearly changes user-visible behavior and has no changeset, mention it. +- Changeset descriptions are read by end users — if one is present but written as implementation notes ("Add a new export handler that serializes…"), suggest a user-facing rewrite ("Support exporting conversations as markdown"). +- PR descriptions should explain **why**, not enumerate files. Skip file-by-file inventories. + +### 7. UI changes + +For changes under `packages/kilo-vscode/webview-ui/`: + +- Significant visual or layout changes should have a Storybook story added under `webview-ui/src/stories/`. Minor tweaks and i18n-only changes don't need one. +- Don't ask for locally generated baseline PNGs — those must come from Linux CI. + +## How to comment + +- Leave comments on the exact line via `gh api .../pulls/{n}/comments`. +- Make it clear suggestions are suggestions; the human decides. +- If the PR is clean against the above, comment `lgtm` and nothing else. diff --git a/bun.lock b/bun.lock index 6ed1c5069dd..38e8f9cea06 100644 --- a/bun.lock +++ b/bun.lock @@ -28,12 +28,12 @@ "prettier": "3.6.2", "semver": "^7.6.0", "sst": "3.18.10", - "turbo": "2.8.13", + "turbo": "2.9.14", }, }, "packages/core": { "name": "@opencode-ai/core", - "version": "7.2.52", + "version": "7.3.12", "bin": { "opencode": "./bin/opencode", }, @@ -88,7 +88,7 @@ }, "packages/kilo-docs": { "name": "@kilocode/kilo-docs", - "version": "7.2.52", + "version": "7.3.12", "dependencies": { "@docsearch/css": "^4", "@docsearch/js": "^4", @@ -97,7 +97,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", @@ -118,7 +118,7 @@ }, "packages/kilo-gateway": { "name": "@kilocode/kilo-gateway", - "version": "7.2.52", + "version": "7.3.12", "dependencies": { "@ai-sdk/alibaba": "1.0.17", "@ai-sdk/anthropic": "3.0.71", @@ -154,7 +154,7 @@ }, "packages/kilo-i18n": { "name": "@kilocode/kilo-i18n", - "version": "7.2.52", + "version": "7.3.12", "devDependencies": { "@tsconfig/node22": "catalog:", "@types/bun": "catalog:", @@ -164,7 +164,7 @@ }, "packages/kilo-indexing": { "name": "@kilocode/kilo-indexing", - "version": "7.2.52", + "version": "7.3.12", "dependencies": { "@aws-sdk/client-bedrock-runtime": "3.1005.0", "@aws-sdk/credential-provider-ini": "3.972.31", @@ -174,6 +174,7 @@ "@qdrant/js-client-rest": "1.17.0", "async-mutex": "0.5.0", "chokidar": "4.0.3", + "effect": "catalog:", "glob": "13.0.6", "hono": "catalog:", "hono-openapi": "catalog:", @@ -195,10 +196,11 @@ }, "packages/kilo-jetbrains": { "name": "@kilocode/kilo-jetbrains", + "version": "7.3.12", }, "packages/kilo-telemetry": { "name": "@kilocode/kilo-telemetry", - "version": "7.2.52", + "version": "7.3.12", "dependencies": { "@kilocode/kilo-gateway": "workspace:*", "posthog-node": "4.4.0", @@ -212,7 +214,7 @@ }, "packages/kilo-ui": { "name": "@kilocode/kilo-ui", - "version": "7.2.52", + "version": "7.3.12", "dependencies": { "@kilocode/sdk": "workspace:*", "@kobalte/core": "0.13.11", @@ -249,7 +251,7 @@ }, "packages/kilo-vscode": { "name": "kilo-code", - "version": "7.2.52", + "version": "7.3.12", "dependencies": { "@anthropic-ai/sdk": "^0.39.0", "@kilocode/kilo-gateway": "workspace:*", @@ -327,7 +329,7 @@ }, "packages/opencode": { "name": "@kilocode/cli", - "version": "7.2.52", + "version": "7.3.12", "bin": { "kilo": "./bin/kilo", "kilocode": "./bin/kilo", @@ -335,7 +337,7 @@ "dependencies": { "@actions/core": "1.11.1", "@actions/github": "6.0.1", - "@agentclientprotocol/sdk": "0.16.1", + "@agentclientprotocol/sdk": "0.21.0", "@ai-sdk/alibaba": "1.0.17", "@ai-sdk/amazon-bedrock": "4.0.96", "@ai-sdk/anthropic": "3.0.71", @@ -485,7 +487,7 @@ }, "packages/plugin": { "name": "@kilocode/plugin", - "version": "7.2.52", + "version": "7.3.12", "dependencies": { "@kilocode/sdk": "workspace:*", "effect": "catalog:", @@ -510,7 +512,7 @@ }, "packages/script": { "name": "@opencode-ai/script", - "version": "7.2.52", + "version": "7.3.12", "dependencies": { "semver": "^7.6.3", }, @@ -521,7 +523,7 @@ }, "packages/sdk/js": { "name": "@kilocode/sdk", - "version": "7.2.52", + "version": "7.3.12", "dependencies": { "cross-spawn": "catalog:", }, @@ -536,7 +538,7 @@ }, "packages/storybook": { "name": "@opencode-ai/storybook", - "version": "7.2.52", + "version": "7.3.12", "devDependencies": { "@opencode-ai/ui": "workspace:*", "@solidjs/meta": "catalog:", @@ -559,7 +561,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "7.2.52", + "version": "7.3.12", "dependencies": { "@kilocode/sdk": "workspace:*", "@kobalte/core": "catalog:", @@ -581,7 +583,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", @@ -609,6 +611,13 @@ }, }, }, + "trustedDependencies": [ + "esbuild", + "tree-sitter-powershell", + "protobufjs", + "web-tree-sitter", + "tree-sitter-bash", + ], "patchedDependencies": { "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", "@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.patch", @@ -641,7 +650,7 @@ "@openauthjs/openauth": "0.0.0-20250322224806", "@opentui/core": "0.2.2", "@opentui/solid": "0.2.2", - "@pierre/diffs": "1.1.0-beta.18", + "@pierre/diffs": "1.1.22", "@playwright/test": "1.59.1", "@solid-primitives/storage": "4.3.3", "@solidjs/meta": "0.29.4", @@ -653,7 +662,7 @@ "@types/bun": "1.3.12", "@types/cross-spawn": "6.0.6", "@types/luxon": "3.7.1", - "@types/node": "22.13.9", + "@types/node": "24.12.2", "@types/semver": "7.7.1", "@typescript/native-preview": "7.0.0-dev.20260316.1", "ai": "6.0.168", @@ -662,7 +671,7 @@ "dompurify": "3.4.2", "drizzle-kit": "1.0.0-beta.19-d95b7a4", "drizzle-orm": "1.0.0-beta.19-d95b7a4", - "effect": "4.0.0-beta.57", + "effect": "4.0.0-beta.59", "fuzzysort": "3.1.0", "hono": "4.12.12", "hono-openapi": "1.1.2", @@ -699,7 +708,7 @@ "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="], - "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.16.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-1ad+Sc/0sCtZGHthxxvgEUo5Wsbw16I+aF+YwdiLnPwkZG8KAGUEAPK6LM6Pf69lCyJPt1Aomk1d+8oE3C4ZEw=="], + "@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.21.0", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-ONj+Q8qOdNQp5XbH5jnMwzT9IKZJsSN0p0lkceS4GtUtNOPVLpNzSS8gqQdGMKfBvA0ESbkL8BTaSN1Rc9miEw=="], "@ai-sdk/alibaba": ["@ai-sdk/alibaba@1.0.17", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZbE+U5bWz2JBc5DERLowx5+TKbjGBE93LqKZAWvuEn7HOSQMraxFMZuc0ST335QZJAyfBOzh7m1mPQ+y7EaaoA=="], @@ -1003,15 +1012,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=="], @@ -1387,7 +1388,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=="], @@ -1683,9 +1684,9 @@ "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="], - "@pierre/diffs": ["@pierre/diffs@1.1.0-beta.18", "", { "dependencies": { "@pierre/theme": "0.0.22", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-7ZF3YD9fxdbYsPnltz5cUqHacN7ztp8RX/fJLxwv8wIEORpP4+7dHz1h/qx3o4EW2xUrIhmbM8ImywLasB787Q=="], + "@pierre/diffs": ["@pierre/diffs@1.1.22", "", { "dependencies": { "@pierre/theme": "0.0.28", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-1Iv7kdl6OABFCd1n2HQbGUiRHouXPaHoIjcb7Lwg8zeJKY5ph+cESFcGEyIiwW0NCbKGtYS2bTnXmI+Eze5dwg=="], - "@pierre/theme": ["@pierre/theme@0.0.22", "", {}, "sha512-ePUIdQRNGjrveELTU7fY89Xa7YGHHEy5Po5jQy/18lm32eRn96+tnYJEtFooGdffrx55KBUtOXfvVy/7LDFFhA=="], + "@pierre/theme": ["@pierre/theme@0.0.28", "", {}, "sha512-1j/H/fECBuc9dEvntdWI+l435HZapw+RCJTlqCA6BboQ5TjlnE005j/ROWutXIs8aq5OAc82JI2Kwk4A1WWBgw=="], "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], @@ -2067,6 +2068,18 @@ "@tufjs/models": ["@tufjs/models@4.1.0", "", { "dependencies": { "@tufjs/canonical-json": "2.0.0", "minimatch": "^10.1.1" } }, "sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww=="], + "@turbo/darwin-64": ["@turbo/darwin-64@2.9.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-t7QiPflaEyBE4oayeZtSmu4mEfjgIrcNlNNl1z1dmIVPqEdtA7+CfTf8d7KXsOGPh6aNgWjKxyvQg9uGfDQF+A=="], + + "@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.9.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-d23147mC9BsCPA9mJ0h/ubcpbRgcJBXbcG3+Vq7YLhjz3IXuvQsJ1UXH8f4MD76ZjJ4m/E4aRdJV+MW88CDfbw=="], + + "@turbo/linux-64": ["@turbo/linux-64@2.9.14", "", { "os": "linux", "cpu": "x64" }, "sha512-P3ZKB5tuUDdDQWuAsACGUR1qv9W7BNWxdxqVJ0kZNuNNPRaVYTPPikLcp79+GiEcW3npsR+KyP38lnQiBc5aSA=="], + + "@turbo/linux-arm64": ["@turbo/linux-arm64@2.9.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZRTlzcUMrrPv9ZuDzRF9n60Ym13bKeG9jDB8WjxyLhWNzV+AJQN+zdpIk3NJYf2zQsGUm1mNar2P0elRzLw25g=="], + + "@turbo/windows-64": ["@turbo/windows-64@2.9.14", "", { "os": "win32", "cpu": "x64" }, "sha512-exanwN6sIduZwykYeiTQj8kCmOhazP5WOz3bvXMcYtjhL6Z3iRWLewKrXCBq0bqwSP3iBMb/AerRCnHI4lx46A=="], + + "@turbo/windows-arm64": ["@turbo/windows-arm64@2.9.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-fVdCsnmYoKICsycbWuuGp6Jvi51/3G/UluFWuAUCvR8PIW5IJkAk5BM9UF8PSm0Q2IphWHFZjYEgjHsh3B9y/g=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], @@ -2191,7 +2204,7 @@ "@types/mssql": ["@types/mssql@9.1.11", "", { "dependencies": { "@types/node": "*", "tarn": "^3.0.1", "tedious": "*" } }, "sha512-vcujgrDbDezCxNDO4KY6gjwduLYOKfrexpRUwhoysRvcXZ3+IgZ/PMYFDgh8c3cQIxZ6skAwYo+H6ibMrBWPjQ=="], - "@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="], + "@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="], @@ -2593,10 +2606,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=="], @@ -2781,7 +2790,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=="], @@ -2875,7 +2884,7 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "effect": ["effect@4.0.0-beta.57", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.6.0", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.9", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^13.0.0", "yaml": "^2.8.3" } }, "sha512-rg32VgXnLKaPRs9tbRDaZ5jxmzNY7ojXt85gSHGUTwdlbWH5Ik+OCUY2q14TXliygPGoHwCAvNWS4bQJOqf00g=="], + "effect": ["effect@4.0.0-beta.59", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.6.0", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.9", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^13.0.0", "yaml": "^2.8.3" } }, "sha512-xyUDLeHSe8d6lWGOvR6Fgn2HL6gYeTZ/S4Jzk9uc4ZUxMPPsNZlNXrvk0C7/utQFzeX7uAWcVnG2BjbA0SRoAA=="], "electron-to-chromium": ["electron-to-chromium@1.5.340", "", {}, "sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA=="], @@ -2915,6 +2924,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=="], @@ -3401,8 +3412,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=="], @@ -3533,7 +3542,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=="], @@ -4341,19 +4350,7 @@ "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], - "turbo": ["turbo@2.8.13", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.13", "turbo-darwin-arm64": "2.8.13", "turbo-linux-64": "2.8.13", "turbo-linux-arm64": "2.8.13", "turbo-windows-64": "2.8.13", "turbo-windows-arm64": "2.8.13" }, "bin": { "turbo": "bin/turbo" } }, "sha512-nyM99hwFB9/DHaFyKEqatdayGjsMNYsQ/XBNO6MITc7roncZetKb97MpHxWf3uiU+LB9c9HUlU3Jp2Ixei2k1A=="], - - "turbo-darwin-64": ["turbo-darwin-64@2.8.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-PmOvodQNiOj77+Zwoqku70vwVjKzL34RTNxxoARjp5RU5FOj/CGiC6vcDQhNtFPUOWSAaogHF5qIka9TBhX4XA=="], - - "turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kI+anKcLIM4L8h+NsM7mtAUpElkCOxv5LgiQVQR8BASyDFfc8Efj5kCk3cqxuxOvIqx0sLfCX7atrHQ2kwuNJQ=="], - - "turbo-linux-64": ["turbo-linux-64@2.8.13", "", { "os": "linux", "cpu": "x64" }, "sha512-j29KnQhHyzdzgCykBFeBqUPS4Wj7lWMnZ8CHqytlYDap4Jy70l4RNG46pOL9+lGu6DepK2s1rE86zQfo0IOdPw=="], - - "turbo-linux-arm64": ["turbo-linux-arm64@2.8.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-OEl1YocXGZDRDh28doOUn49QwNe82kXljO1HXApjU0LapkDiGpfl3jkAlPKxEkGDSYWc8MH5Ll8S16Rf5tEBYg=="], - - "turbo-windows-64": ["turbo-windows-64@2.8.13", "", { "os": "win32", "cpu": "x64" }, "sha512-717bVk1+Pn2Jody7OmWludhEirEe0okoj1NpRbSm5kVZz/yNN/jfjbxWC6ilimXMz7xoMT3IDfQFJsFR3PMANA=="], - - "turbo-windows-arm64": ["turbo-windows-arm64@2.8.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-R819HShLIT0Wj6zWVnIsYvSNtRNj1q9VIyaUz0P24SMcLCbQZIm1sV09F4SDbg+KCCumqD2lcaR2UViQ8SnUJA=="], + "turbo": ["turbo@2.9.14", "", { "optionalDependencies": { "@turbo/darwin-64": "2.9.14", "@turbo/darwin-arm64": "2.9.14", "@turbo/linux-64": "2.9.14", "@turbo/linux-arm64": "2.9.14", "@turbo/windows-64": "2.9.14", "@turbo/windows-arm64": "2.9.14" }, "bin": { "turbo": "bin/turbo" } }, "sha512-BQqXRr4UoWI3UPFrtznCLykYHxwxWh53iCB57x092jPMjIlW1wnm3N895g5irpiXmnxUhREBB0n6+y8BHhs4nw=="], "turndown": ["turndown@7.2.0", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A=="], @@ -4383,7 +4380,7 @@ "undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], - "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], @@ -4461,16 +4458,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=="], @@ -4605,6 +4594,8 @@ "@antfu/install-pkg/tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="], + "@anthropic-ai/sdk/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="], + "@aws-crypto/sha1-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], "@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], @@ -4709,6 +4700,8 @@ "@kilocode/kilo-indexing/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], + "@manypkg/find-root/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="], + "@manypkg/find-root/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], "@manypkg/find-root/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], @@ -4765,6 +4758,8 @@ "@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="], + "@opencode-ai/plugin/effect": ["effect@4.0.0-beta.57", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.6.0", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.9", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^13.0.0", "yaml": "^2.8.3" } }, "sha512-rg32VgXnLKaPRs9tbRDaZ5jxmzNY7ojXt85gSHGUTwdlbWH5Ik+OCUY2q14TXliygPGoHwCAvNWS4bQJOqf00g=="], + "@opencode-ai/storybook/@storybook/addon-a11y": ["@storybook/addon-a11y@10.3.5", "", { "dependencies": { "@storybook/global": "^5.0.0", "axe-core": "^4.2.0" }, "peerDependencies": { "storybook": "^10.3.5" } }, "sha512-5k6lpgfIeLxvNhE8v3wEzdiu73ONKjF4gmH1AHvfqYd8kIVzQJai0KCDxgvqNncXHQhIWkaf1fg6+9hKaYJyaw=="], "@opencode-ai/storybook/@storybook/addon-docs": ["@storybook/addon-docs@10.3.5", "", { "dependencies": { "@mdx-js/react": "^3.0.0", "@storybook/csf-plugin": "10.3.5", "@storybook/icons": "^2.0.1", "@storybook/react-dom-shim": "10.3.5", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^10.3.5" } }, "sha512-WuHbxia/o5TX4Rg/IFD0641K5qId/Nk0dxhmAUNoFs5L0+yfZUwh65XOBbzXqrkYmYmcVID4v7cgDRmzstQNkA=="], @@ -4777,8 +4772,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=="], @@ -4813,6 +4806,10 @@ "@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=="], + "@standard-community/standard-json/effect": ["effect@4.0.0-beta.57", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.6.0", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.9", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^13.0.0", "yaml": "^2.8.3" } }, "sha512-rg32VgXnLKaPRs9tbRDaZ5jxmzNY7ojXt85gSHGUTwdlbWH5Ik+OCUY2q14TXliygPGoHwCAvNWS4bQJOqf00g=="], + + "@standard-community/standard-openapi/effect": ["effect@4.0.0-beta.57", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.6.0", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.9", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^13.0.0", "yaml": "^2.8.3" } }, "sha512-rg32VgXnLKaPRs9tbRDaZ5jxmzNY7ojXt85gSHGUTwdlbWH5Ik+OCUY2q14TXliygPGoHwCAvNWS4bQJOqf00g=="], + "@storybook/addon-links/storybook": ["storybook@10.3.5", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0", "open": "^10.2.0", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.18.0" }, "peerDependencies": { "prettier": "^2 || ^3" }, "optionalPeers": ["prettier"], "bin": "./dist/bin/dispatcher.js" }, "sha512-uBSZu/GZa9aEIW3QMGvdQPMZWhGxSe4dyRWU8B3/Vd47Gy/XLC7tsBxRr13txmmPOEDHZR94uLuq0H50fvuqBw=="], "@storybook/addon-onboarding/storybook": ["storybook@10.3.5", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "@webcontainer/env": "^1.1.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0", "open": "^10.2.0", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.18.0" }, "peerDependencies": { "prettier": "^2 || ^3" }, "optionalPeers": ["prettier"], "bin": "./dist/bin/dispatcher.js" }, "sha512-uBSZu/GZa9aEIW3QMGvdQPMZWhGxSe4dyRWU8B3/Vd47Gy/XLC7tsBxRr13txmmPOEDHZR94uLuq0H50fvuqBw=="], @@ -4853,6 +4850,30 @@ "@textlint/linter-formatter/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@types/cacache/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="], + + "@types/cross-spawn/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="], + + "@types/mssql/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="], + + "@types/node-fetch/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="], + + "@types/npm-registry-fetch/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="], + + "@types/npmcli__arborist/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="], + + "@types/npmlog/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="], + + "@types/pacote/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="], + + "@types/qrcode/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="], + + "@types/readable-stream/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="], + + "@types/ssri/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="], + + "@types/ws/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="], + "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], "@vscode/ripgrep/yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], @@ -4889,6 +4910,8 @@ "ai-gateway-provider/@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.5.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-r1fJL1Cb3gQDa2MpWH/sfx1BsEW0uzlRriJM6eihaKqbtKDmZoBisF32VcVaQYassighX7NGCkF68EsrZA43uQ=="], + "apache-arrow/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="], + "archiver-utils/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], "archiver-utils/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], @@ -4923,6 +4946,8 @@ "buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "bun-types/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="], + "c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], "c12/dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], @@ -5015,6 +5040,8 @@ "gray-matter/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], + "image-q/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="], + "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], "isomorphic-git/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], @@ -5055,8 +5082,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=="], @@ -5139,6 +5164,8 @@ "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + "protobufjs/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="], + "proxy-addr/ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], "qrcode/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], @@ -5205,6 +5232,8 @@ "tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + "tedious/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="], + "tedious/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], "test-exclude/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], @@ -5225,8 +5254,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=="], @@ -5265,6 +5292,8 @@ "@ai-sdk/vercel/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@anthropic-ai/sdk/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], @@ -5303,10 +5332,14 @@ "@kilocode/kilo-gateway/@opentui/solid/babel-preset-solid": ["babel-preset-solid@1.9.9", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.1" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.8" }, "optionalPeers": ["solid-js"] }, "sha512-pCnxWrciluXCeli/dj5PIEHgbNzim3evtTn12snjqqg8QZWJNMjH1AWIp4iG/tbVjqQ72aBEymMSagvmgxubXw=="], + "@manypkg/find-root/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + "@manypkg/find-root/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], "@morphllm/morphsdk/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.95", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZmUNNbZl3V42xwQzPaNUi+s8eqR2lnrxf0bvB6YbLXpLjHYv0k2Y78t12cNOfY0bxGeuVVTLyk856uLuQIuXEQ=="], + "@morphllm/morphsdk/openai/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="], + "@morphllm/morphsdk/openai/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], @@ -5341,6 +5374,10 @@ "@octokit/rest/@octokit/core/before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="], + "@opencode-ai/plugin/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@opencode-ai/plugin/effect/uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="], + "@opencode-ai/storybook/@storybook/addon-docs/@storybook/csf-plugin": ["@storybook/csf-plugin@10.3.5", "", { "dependencies": { "unplugin": "^2.3.5" }, "peerDependencies": { "esbuild": "*", "rollup": "*", "storybook": "^10.3.5", "vite": "*", "webpack": "*" }, "optionalPeers": ["esbuild", "rollup", "vite", "webpack"] }, "sha512-qlEzNKxOjq86pvrbuMwiGD/bylnsXk1dg7ve0j77YFjEEchqtl7qTlrXvFdNaLA89GhW6D/EV6eOCu/eobPDgw=="], "@opencode-ai/storybook/@storybook/addon-docs/@storybook/react-dom-shim": ["@storybook/react-dom-shim@10.3.5", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.3.5" } }, "sha512-Gw8R7XZm0zSUH0XAuxlQJhmizsLzyD6x00KOlP6l7oW9eQHXGfxg3seNDG3WrSAcW07iP1/P422kuiriQlOv7g=="], @@ -5351,12 +5388,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=="], @@ -5365,6 +5396,14 @@ "@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@standard-community/standard-json/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@standard-community/standard-json/effect/uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="], + + "@standard-community/standard-openapi/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@standard-community/standard-openapi/effect/uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="], + "@storybook/addon-links/storybook/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], "@storybook/addon-onboarding/storybook/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], @@ -5407,6 +5446,30 @@ "@textlint/linter-formatter/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "@types/cacache/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + + "@types/cross-spawn/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + + "@types/mssql/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + + "@types/node-fetch/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + + "@types/npm-registry-fetch/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + + "@types/npmcli__arborist/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + + "@types/npmlog/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + + "@types/pacote/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + + "@types/qrcode/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + + "@types/readable-stream/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + + "@types/ssri/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + + "@types/ws/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + "@vscode/ripgrep/yauzl/buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], "@vscode/test-cli/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -5449,6 +5512,8 @@ "ai-gateway-provider/@ai-sdk/xai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + "apache-arrow/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + "archiver-utils/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], "archiver-utils/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], @@ -5479,6 +5544,8 @@ "bl/buffer/ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "bun-types/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + "c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], "c8/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], @@ -5583,10 +5650,14 @@ "gray-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + "image-q/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + "jszip/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "jszip/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + "kilo-code/openai/@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="], + "lazystream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], @@ -5649,6 +5720,8 @@ "posthog-js/@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@2.7.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-DT12SXVwV2eoJrGf4nnsvZojxxeQo+LlNAsoYGRRObPWTeN6APiqZ2+nqDCQDvQX40eLi1AePONS0onoASp3yQ=="], + "protobufjs/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + "qrcode/yargs/cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], "qrcode/yargs/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], @@ -5679,6 +5752,8 @@ "tar-fs/tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "tedious/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + "test-exclude/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], "test-exclude/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], @@ -5711,6 +5786,8 @@ "@morphllm/morphsdk/ai/@ai-sdk/gateway/@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="], + "@morphllm/morphsdk/openai/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + "@octokit/graphql/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@octokit/rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.3", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag=="], @@ -5823,6 +5900,8 @@ "gray-matter/js-yaml/argparse/sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + "kilo-code/openai/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + "mocha/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], "mocha/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], 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/install b/install index c6d0e37e0b6..f78cbc8d1b6 100755 --- a/install +++ b/install @@ -348,6 +348,10 @@ download_and_install() { # kilocode_change start mv "$tmp_dir/kilo" "$INSTALL_DIR" + rm -rf "${INSTALL_DIR}/tree-sitter" + if [ -d "$tmp_dir/tree-sitter" ]; then + mv "$tmp_dir/tree-sitter" "$INSTALL_DIR" + fi chmod 755 "${INSTALL_DIR}/kilo" # kilocode_change end rm -rf "$tmp_dir" @@ -517,10 +521,46 @@ if [ -n "${GITHUB_ACTIONS-}" ] && [ "${GITHUB_ACTIONS}" == "true" ]; then fi # kilocode_change start +logo_supports() { + case "${KILO_UNICODE_LOGO:-}" in + 1|true|TRUE|yes|YES|on|ON) + return 0 + ;; + 0|false|FALSE|no|NO|off|OFF) + return 1 + ;; + esac + + if [ "${TERM:-}" = "dumb" ]; then + return 1 + fi + + if [ -n "${SSH_TTY:-}" ] || [ -n "${SSH_CLIENT:-}" ] || [ -n "${SSH_CONNECTION:-}" ]; then + return 1 + fi + + if [ -n "${ConEmuPID:-}" ] || [ -n "${ANSICON:-}" ]; then + return 1 + fi + + return 0 +} + +print_logo() { + if logo_supports; then + echo -e "██ ██ ██🬺🬏 ██ ██ ██🬺🬏 ████ ██ ██🬺🬏 " + echo -e "████🬺🬏 ██ ██ ██ ██ ██ ██ ██ " + echo -e "██ ██ ██████ 🬁🬬████ 🬁🬬██ 🬁🬬████ 🬁🬬████ ██████ " + return + fi + + echo -e "██ ██ ████ ██ ██ ███ ████ ██ ████ " + echo -e "████ ██ ██ ██ ██ ██ ██ ██ " + echo -e "██ ██ ██████ ██████ ██ ████ ██████ ██████ " +} + echo -e "" -echo -e "██ ▄█▀ ██ ██ ▄████▄ ▄█████ ██ ██ " -echo -e "████ ██ ██ ██ ██ ██ ██ ██ " -echo -e "██ ▀█▄ ██ ██████ ▀████▀ ▀█████ ██████ ██ " +print_logo echo -e "" echo -e "" echo -e "${MUTED}Kilo includes free models so you can start immediately:${NC}" diff --git a/nix/hashes.json b/nix/hashes.json index 3aa8ec21fe0..e8171c93a62 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-EbXkvlbexL1Gyy4vvGG5j+JWjv7SsVk6T5YnDfo3AQY=", + "aarch64-linux": "sha256-+MuJE4XGr0dArD/HuozaoK9Oymfgvx1CisE/Sm4ZstY=", + "aarch64-darwin": "sha256-Qdk+tZLydGld471ApL1cxfd85QQNFelTfqq7uAznfK4=", + "x86_64-darwin": "sha256-Fo6W65MfAcXaulBGCaE7+GfHtq3VVJyVDJpIENvR7lQ=" } } diff --git a/package.json b/package.json index f0e3e0d9508..76b61df1dd3 100644 --- a/package.json +++ b/package.json @@ -7,16 +7,17 @@ "packageManager": "bun@1.3.13", "scripts": { "dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts", - "dev-setup": "bun run --cwd packages/opencode --conditions=browser src/index.ts dev-setup", "dev:storybook": "bun --cwd packages/storybook storybook", "lint": "oxlint", "typecheck": "bun turbo typecheck", + "test:ci": "bun turbo test:ci", "postinstall": "bun run --cwd packages/opencode fix-node-pty && bun run script/setup-git.ts", "prepare": "husky", "random": "echo 'Random script'", "hello": "echo 'Hello World!'", "test": "echo 'do not run tests from root' && exit 1", - "extension": "bun --cwd packages/kilo-vscode script/launch.ts" + "extension": "bun --cwd packages/kilo-vscode script/launch.ts", + "dev-setup": "bun run --cwd packages/opencode --conditions=browser src/index.ts dev-setup" }, "workspaces": { "packages": [ @@ -36,13 +37,13 @@ "ulid": "3.0.1", "@kobalte/core": "0.13.11", "@types/luxon": "3.7.1", - "@types/node": "22.13.9", + "@types/node": "24.12.2", "@types/semver": "7.7.1", "@tsconfig/node22": "22.0.2", "@tsconfig/bun": "1.0.9", "@cloudflare/workers-types": "4.20251008.0", "@openauthjs/openauth": "0.0.0-20250322224806", - "@pierre/diffs": "1.1.0-beta.18", + "@pierre/diffs": "1.1.22", "opentui-spinner": "0.0.6", "@solid-primitives/storage": "4.3.3", "@tailwindcss/vite": "4.1.11", @@ -50,7 +51,7 @@ "dompurify": "3.4.2", "drizzle-kit": "1.0.0-beta.19-d95b7a4", "drizzle-orm": "1.0.0-beta.19-d95b7a4", - "effect": "4.0.0-beta.57", + "effect": "4.0.0-beta.59", "ai": "6.0.168", "cross-spawn": "7.0.6", "hono": "4.12.12", @@ -91,7 +92,7 @@ "prettier": "3.6.2", "semver": "^7.6.0", "sst": "3.18.10", - "turbo": "2.8.13", + "turbo": "2.9.14", "@types/bun": "catalog:", "@changesets/changelog-github": "^0.5.1", "@changesets/cli": "^2.27.10" @@ -114,6 +115,16 @@ "semi": false, "printWidth": 120 }, + "trustedDependencies": [ + "esbuild", + "node-pty", + "protobufjs", + "tree-sitter", + "tree-sitter-bash", + "tree-sitter-powershell", + "web-tree-sitter", + "electron" + ], "overrides": { "@types/bun": "catalog:", "@types/node": "catalog:", @@ -135,6 +146,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.12", "peerDependencies": {} } diff --git a/packages/core/package.json b/packages/core/package.json index 32ffc2df5ba..3936f230288 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.12", "name": "@opencode-ai/core", "type": "module", "license": "MIT", diff --git a/packages/core/src/filesystem.ts b/packages/core/src/filesystem.ts index bc149d586e6..2e7a450d9b4 100644 --- a/packages/core/src/filesystem.ts +++ b/packages/core/src/filesystem.ts @@ -46,6 +46,7 @@ export namespace AppFileSystem { readonly isDir: (path: string) => Effect.Effect readonly isFile: (path: string) => Effect.Effect readonly existsSafe: (path: string) => Effect.Effect + readonly readFileStringSafe: (path: string) => Effect.Effect readonly readJson: (path: string) => Effect.Effect readonly writeJson: (path: string, data: unknown, mode?: number) => Effect.Effect readonly ensureDir: (path: string) => Effect.Effect @@ -69,6 +70,12 @@ export namespace AppFileSystem { return yield* fs.exists(path).pipe(Effect.orElseSucceed(() => false)) }) + const readFileStringSafe = Effect.fn("FileSystem.readFileStringSafe")(function* (path: string) { + return yield* fs + .readFileString(path) + .pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(undefined))) + }) + const isDir = Effect.fn("FileSystem.isDir")(function* (path: string) { const info = yield* fs.stat(path).pipe(Effect.catch(() => Effect.void)) return info?.type === "Directory" @@ -195,6 +202,7 @@ export namespace AppFileSystem { return Service.of({ ...fs, existsSafe, + readFileStringSafe, isDir, isFile, readDirectoryEntries, diff --git a/packages/core/src/flag/flag.ts b/packages/core/src/flag/flag.ts index ff01b402d0b..6cbc94e43a9 100644 --- a/packages/core/src/flag/flag.ts +++ b/packages/core/src/flag/flag.ts @@ -1,4 +1,5 @@ import { Config } from "effect" +import { InstallationChannel } from "../installation/version" function truthy(key: string) { const value = process.env[key]?.toLowerCase() @@ -10,6 +11,10 @@ function falsy(key: string) { return value === "false" || value === "0" } +// Channels that default to the new effect-httpapi server backend. The legacy +// hono backend remains the default for stable (`prod`/`latest`) installs. +const HTTPAPI_DEFAULT_ON_CHANNELS = new Set(["dev", "beta", "local"]) + function number(key: string) { const value = process.env[key] if (!value) return undefined @@ -46,7 +51,7 @@ export const Flag = { KILO_DISABLE_CLAUDE_CODE, KILO_DISABLE_CLAUDE_CODE_PROMPT: KILO_DISABLE_CLAUDE_CODE || truthy("KILO_DISABLE_CLAUDE_CODE_PROMPT"), KILO_DISABLE_CLAUDE_CODE_SKILLS, - KILO_DISABLE_EXTERNAL_SKILLS: KILO_DISABLE_CLAUDE_CODE_SKILLS || truthy("KILO_DISABLE_EXTERNAL_SKILLS"), + KILO_DISABLE_EXTERNAL_SKILLS: truthy("KILO_DISABLE_EXTERNAL_SKILLS"), // kilocode_change KILO_FAKE_VCS: process.env["KILO_FAKE_VCS"], KILO_SERVER_PASSWORD: process.env["KILO_SERVER_PASSWORD"], KILO_SERVER_USERNAME: process.env["KILO_SERVER_USERNAME"], @@ -78,8 +83,16 @@ export const Flag = { KILO_STRICT_CONFIG_DEPS: truthy("KILO_STRICT_CONFIG_DEPS"), KILO_WORKSPACE_ID: process.env["KILO_WORKSPACE_ID"], - KILO_EXPERIMENTAL_HTTPAPI: truthy("KILO_EXPERIMENTAL_HTTPAPI"), + // Defaults to true on dev/beta/local channels so internal users exercise the + // new effect-httpapi server backend. Stable (`prod`/`latest`) installs stay + // on the legacy hono backend until the rollout is complete. An explicit env + // var ("true"/"1" or "false"/"0") always wins, providing an opt-in for + // stable users and an escape hatch for dev/beta users. + KILO_EXPERIMENTAL_HTTPAPI: + truthy("KILO_EXPERIMENTAL_HTTPAPI") || + (!falsy("KILO_EXPERIMENTAL_HTTPAPI") && HTTPAPI_DEFAULT_ON_CHANNELS.has(InstallationChannel)), KILO_EXPERIMENTAL_WORKSPACES: KILO_EXPERIMENTAL || truthy("KILO_EXPERIMENTAL_WORKSPACES"), + KILO_EXPERIMENTAL_EVENT_SYSTEM: KILO_EXPERIMENTAL || truthy("KILO_EXPERIMENTAL_EVENT_SYSTEM"), // Evaluated at access time (not module load) because tests, the CLI, and // external tooling set these env vars at runtime. diff --git a/packages/core/src/global.ts b/packages/core/src/global.ts index 645c1a976f3..223322300fa 100644 --- a/packages/core/src/global.ts +++ b/packages/core/src/global.ts @@ -87,6 +87,8 @@ export const layer = Layer.effect( Effect.sync(() => Service.of(make())), ) +export const defaultLayer = layer + export const layerWith = (input: Partial) => Layer.effect( Service, diff --git a/packages/core/src/util/log.ts b/packages/core/src/util/log.ts index 1181317e082..85ffbd915dc 100644 --- a/packages/core/src/util/log.ts +++ b/packages/core/src/util/log.ts @@ -1,3 +1,5 @@ +export * as Log from "./log" + import path from "path" import { existsSync, writeFileSync } from "fs" // kilocode_change import fs from "fs/promises" diff --git a/packages/core/test/filesystem/filesystem.test.ts b/packages/core/test/filesystem/filesystem.test.ts index b77f4e356fa..1d9405333da 100644 --- a/packages/core/test/filesystem/filesystem.test.ts +++ b/packages/core/test/filesystem/filesystem.test.ts @@ -65,6 +65,34 @@ describe("AppFileSystem", () => { ) }) + describe("readFileStringSafe", () => { + it( + "returns file contents when file exists", + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const filesys = yield* FileSystem.FileSystem + const tmp = yield* filesys.makeTempDirectoryScoped() + const file = path.join(tmp, "exists.txt") + yield* filesys.writeFileString(file, "hello") + + const result = yield* fs.readFileStringSafe(file) + expect(result).toBe("hello") + }), + ) + + it( + "returns undefined for missing file (NotFound)", + Effect.gen(function* () { + const fs = yield* AppFileSystem.Service + const filesys = yield* FileSystem.FileSystem + const tmp = yield* filesys.makeTempDirectoryScoped() + + const result = yield* fs.readFileStringSafe(path.join(tmp, "does-not-exist.txt")) + expect(result).toBeUndefined() + }), + ) + }) + describe("readJson / writeJson", () => { it( "round-trips JSON data", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 2a67ecece50..ccdb1a18d60 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.12" 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.12/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.12/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.12/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.12/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.12/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/kilo-docs/components/BrowserFrame/index.tsx b/packages/kilo-docs/components/BrowserFrame/index.tsx index bc96def1cf3..d1a80ae074b 100644 --- a/packages/kilo-docs/components/BrowserFrame/index.tsx +++ b/packages/kilo-docs/components/BrowserFrame/index.tsx @@ -87,7 +87,23 @@ export function BrowserFrame({ )} {/* Content */} -
{children}
+ {/* + * The `browser-frame-content` class strips the default vertical margin + * applied by descendant `Image` figures (1.5rem top/bottom). The + * BrowserFrame already provides its own outer spacing via the `figure` + * wrapper, and the inner image should sit flush against the chrome. + */} +
+ {children} +
+ {caption && (
({ + padding: "10px 18px", + borderRadius: "8px", + fontSize: "12px", + fontFamily: "'JetBrains Mono', monospace", + fontWeight: 600, + textAlign: "center" as const, + minWidth: "140px", + background: `rgba(${rgb},0.1)`, + border: `1px solid rgba(${rgb},0.5)`, + color, +}) + +const gt = laneStyle("59,130,246", "#3b82f6") +const wl = laneStyle("248,160,32", "#f8a020") +const val = laneStyle("167,139,250", "#a78bfa") +const rep = laneStyle("34,197,94", "#22c55e") + +const nodes: Node[] = [ + { + id: "browse", + position: { x: 0, y: 0 }, + data: { label: "Mayor browse" }, + sourcePosition: "right" as Position, + targetPosition: "left" as Position, + style: gt, + }, + { + id: "wl-return", + position: { x: 250, y: 0 }, + data: { label: "Wasteland returns\nwanted items" }, + sourcePosition: "bottom" as Position, + targetPosition: "left" as Position, + style: wl, + }, + { + id: "claim", + position: { x: 0, y: 100 }, + data: { label: "Mayor claim" }, + sourcePosition: "right" as Position, + targetPosition: "left" as Position, + style: gt, + }, + { + id: "wl-lock", + position: { x: 250, y: 100 }, + data: { label: "Wasteland locks\nitem → DoltHub PR" }, + sourcePosition: "bottom" as Position, + targetPosition: "left" as Position, + style: wl, + }, + { + id: "work", + position: { x: 0, y: 210 }, + data: { label: "Polecats work\n→ push branch" }, + sourcePosition: "right" as Position, + targetPosition: "top" as Position, + style: gt, + }, + { + id: "done", + position: { x: 0, y: 320 }, + data: { label: "Mayor done\n(evidence)" }, + sourcePosition: "right" as Position, + targetPosition: "left" as Position, + style: gt, + }, + { + id: "wl-attach", + position: { x: 250, y: 320 }, + data: { label: "Wasteland attaches\nevidence to PR" }, + sourcePosition: "bottom" as Position, + targetPosition: "left" as Position, + style: wl, + }, + { + id: "review", + position: { x: 500, y: 320 }, + data: { label: "Validator\nreviews PR" }, + sourcePosition: "bottom" as Position, + targetPosition: "left" as Position, + style: val, + }, + { + id: "stamp", + position: { x: 500, y: 450 }, + data: { label: "Stamps:\nquality · reliability\n· creativity" }, + sourcePosition: "bottom" as Position, + targetPosition: "top" as Position, + style: val, + }, + { + id: "reputation", + position: { x: 250, y: 560 }, + data: { label: "Reputation ledger\nupdates" }, + sourcePosition: "bottom" as Position, + targetPosition: "top" as Position, + style: rep, + }, + { + id: "identity", + position: { x: 0, y: 560 }, + data: { label: "Portable\nidentity" }, + sourcePosition: "top" as Position, + targetPosition: "right" as Position, + style: rep, + }, +] + +const labelStyle = { fontSize: 10, fontFamily: "'JetBrains Mono', monospace", fill: "#888" } + +const edges: Edge[] = [ + { + id: "browse-return", + source: "browse", + target: "wl-return", + type: "smoothstep", + animated: true, + label: "1 browse", + labelStyle, + style: { strokeWidth: 2, stroke: "#3b82f6" }, + }, + { + id: "claim-lock", + source: "claim", + target: "wl-lock", + type: "smoothstep", + animated: true, + label: "2 claim", + labelStyle, + style: { strokeWidth: 2, stroke: "#3b82f6" }, + }, + { + id: "lock-work", + source: "wl-lock", + target: "work", + type: "smoothstep", + label: "3 work", + labelStyle, + style: { strokeWidth: 2, stroke: "#f8a020" }, + }, + { + id: "work-done", + source: "work", + target: "done", + type: "smoothstep", + animated: true, + style: { strokeWidth: 2, stroke: "#3b82f6" }, + }, + { + id: "done-attach", + source: "done", + target: "wl-attach", + type: "smoothstep", + animated: true, + label: "4 evidence", + labelStyle, + style: { strokeWidth: 2, stroke: "#3b82f6" }, + }, + { + id: "attach-review", + source: "wl-attach", + target: "review", + type: "smoothstep", + animated: true, + label: "5 review", + labelStyle, + style: { strokeWidth: 2, stroke: "#a78bfa" }, + }, + { + id: "review-stamp", + source: "review", + target: "stamp", + type: "smoothstep", + animated: true, + style: { strokeWidth: 2, stroke: "#a78bfa" }, + }, + { + id: "stamp-rep", + source: "stamp", + target: "reputation", + type: "smoothstep", + animated: true, + label: "6 reputation", + labelStyle, + style: { strokeWidth: 2, stroke: "#22c55e" }, + }, + { + id: "rep-identity", + source: "reputation", + target: "identity", + type: "smoothstep", + animated: true, + style: { strokeWidth: 2, stroke: "#22c55e" }, + }, + { + id: "return-claim", + source: "wl-return", + target: "wl-lock", + type: "smoothstep", + style: { strokeWidth: 1, stroke: "#f8a020", strokeDasharray: "4 4" }, + }, +] + +export const claimToStamp: DiagramDefinition = { + nodes, + edges, + caption: + "Claim-to-stamp flow — from browsing wanted items through claiming, working, submitting evidence, and earning stamps", +} diff --git a/packages/kilo-docs/components/FlowDiagram/diagrams/index.ts b/packages/kilo-docs/components/FlowDiagram/diagrams/index.ts index 547f5b8b83a..20de5b64002 100644 --- a/packages/kilo-docs/components/FlowDiagram/diagrams/index.ts +++ b/packages/kilo-docs/components/FlowDiagram/diagrams/index.ts @@ -2,6 +2,9 @@ import type { Node, Edge } from "@xyflow/react" import { beadLifecycle } from "./bead-lifecycle" import { adversarialLoop } from "./adversarial-loop" import { convoyExecution } from "./convoy-execution" +import { wastelandFederation } from "./wasteland-federation" +import { wantedLifecycle } from "./wanted-lifecycle" +import { claimToStamp } from "./claim-to-stamp" export interface DiagramDefinition { nodes: Node[] @@ -13,4 +16,7 @@ export const diagrams: Record = { "bead-lifecycle": beadLifecycle, "adversarial-loop": adversarialLoop, "convoy-execution": convoyExecution, + "wasteland-federation": wastelandFederation, + "wanted-lifecycle": wantedLifecycle, + "claim-to-stamp": claimToStamp, } diff --git a/packages/kilo-docs/components/FlowDiagram/diagrams/wanted-lifecycle.ts b/packages/kilo-docs/components/FlowDiagram/diagrams/wanted-lifecycle.ts new file mode 100644 index 00000000000..54c7574bec2 --- /dev/null +++ b/packages/kilo-docs/components/FlowDiagram/diagrams/wanted-lifecycle.ts @@ -0,0 +1,143 @@ +import type { Node, Edge, Position } from "@xyflow/react" +import type { DiagramDefinition } from "./index" + +const stateStyle = (color: string, rgb: string) => ({ + padding: "12px 20px", + borderRadius: "8px", + fontSize: "13px", + fontFamily: "'JetBrains Mono', monospace", + fontWeight: 600, + textAlign: "center" as const, + minWidth: "120px", + background: `rgba(${rgb},0.1)`, + border: `1px solid rgba(${rgb},0.5)`, + color, +}) + +const nodes: Node[] = [ + { + id: "posted", + position: { x: 0, y: 0 }, + data: { label: "Posted" }, + sourcePosition: "right" as Position, + targetPosition: "top" as Position, + style: stateStyle("#22c55e", "34,197,94"), + }, + { + id: "claimed", + position: { x: 200, y: 0 }, + data: { label: "Claimed" }, + sourcePosition: "right" as Position, + targetPosition: "left" as Position, + style: stateStyle("#3b82f6", "59,130,246"), + }, + { + id: "in_progress", + position: { x: 400, y: 0 }, + data: { label: "In progress" }, + sourcePosition: "right" as Position, + targetPosition: "left" as Position, + style: stateStyle("#f8a020", "248,160,32"), + }, + { + id: "evidence_submitted", + position: { x: 600, y: 0 }, + data: { label: "Evidence submitted" }, + sourcePosition: "right" as Position, + targetPosition: "left" as Position, + style: stateStyle("#a78bfa", "167,139,250"), + }, + { + id: "stamped", + position: { x: 850, y: 0 }, + data: { label: "Stamped" }, + sourcePosition: "top" as Position, + targetPosition: "left" as Position, + style: stateStyle("#22c55e", "34,197,94"), + }, + { + id: "rejected", + position: { x: 600, y: 160 }, + data: { label: "Rejected" }, + sourcePosition: "bottom" as Position, + targetPosition: "top" as Position, + style: stateStyle("#ef4444", "239,68,68"), + }, + { + id: "abandoned", + position: { x: 200, y: 160 }, + data: { label: "Abandoned" }, + sourcePosition: "top" as Position, + targetPosition: "top" as Position, + style: stateStyle("#888", "255,255,255"), + }, +] + +const labelStyle = { fontSize: 10, fontFamily: "'JetBrains Mono', monospace", fill: "#888" } + +const edges: Edge[] = [ + { + id: "posted-claimed", + source: "posted", + target: "claimed", + type: "smoothstep", + animated: true, + style: { strokeWidth: 2, stroke: "#3b82f6" }, + }, + { + id: "claimed-progress", + source: "claimed", + target: "in_progress", + type: "smoothstep", + animated: true, + style: { strokeWidth: 2, stroke: "#f8a020" }, + }, + { + id: "progress-evidence", + source: "in_progress", + target: "evidence_submitted", + type: "smoothstep", + animated: true, + style: { strokeWidth: 2, stroke: "#a78bfa" }, + }, + { + id: "evidence-stamped", + source: "evidence_submitted", + target: "stamped", + type: "smoothstep", + animated: true, + style: { strokeWidth: 2, stroke: "#22c55e" }, + }, + { + id: "evidence-rejected", + source: "evidence_submitted", + target: "rejected", + type: "smoothstep", + label: "rejects", + labelStyle, + style: { strokeWidth: 2, stroke: "#ef4444", strokeDasharray: "5 3" }, + }, + { + id: "rejected-progress", + source: "rejected", + target: "in_progress", + type: "smoothstep", + label: "rework", + labelStyle, + style: { strokeWidth: 2, stroke: "#f8a020", strokeDasharray: "5 3" }, + }, + + { + id: "claimed-abandoned", + source: "claimed", + target: "abandoned", + type: "smoothstep", + style: { strokeWidth: 2, stroke: "#888", strokeDasharray: "5 3" }, + }, +] + +export const wantedLifecycle: DiagramDefinition = { + nodes, + edges, + caption: "Wanted item lifecycle — from posted through claimed, in-progress, evidence, and stamped (or rejected)", +} diff --git a/packages/kilo-docs/components/FlowDiagram/diagrams/wasteland-federation.ts b/packages/kilo-docs/components/FlowDiagram/diagrams/wasteland-federation.ts new file mode 100644 index 00000000000..5d3ec3d4c97 --- /dev/null +++ b/packages/kilo-docs/components/FlowDiagram/diagrams/wasteland-federation.ts @@ -0,0 +1,186 @@ +import type { Node, Edge, Position } from "@xyflow/react" +import type { DiagramDefinition } from "./index" + +const townStyle = { + padding: "10px 16px", + borderRadius: "8px", + fontSize: "12px", + fontFamily: "'JetBrains Mono', monospace", + fontWeight: 600, + textAlign: "center" as const, + minWidth: "130px", + background: "rgba(59,130,246,0.1)", + border: "1px solid rgba(59,130,246,0.5)", + color: "#3b82f6", +} + +const wastelandStyle = { + padding: "14px 24px", + borderRadius: "10px", + fontSize: "13px", + fontFamily: "'JetBrains Mono', monospace", + fontWeight: 700, + textAlign: "center" as const, + minWidth: "180px", + background: "rgba(248,160,32,0.1)", + border: "1px solid rgba(248,160,32,0.5)", + color: "#f8a020", +} + +const dbStyle = { + padding: "10px 18px", + borderRadius: "6px", + fontSize: "11px", + fontFamily: "'JetBrains Mono', monospace", + fontWeight: 600, + textAlign: "center" as const, + minWidth: "140px", + background: "rgba(139,92,246,0.08)", + border: "1px solid rgba(139,92,246,0.4)", + color: "#8b5cf6", +} + +const nodes: Node[] = [ + { + id: "town-you", + position: { x: -60, y: 0 }, + data: { label: "Your Town" }, + sourcePosition: "bottom" as Position, + targetPosition: "top" as Position, + style: townStyle, + }, + { + id: "town-acme", + position: { x: 140, y: 0 }, + data: { label: "Acme Eng Town" }, + sourcePosition: "bottom" as Position, + targetPosition: "top" as Position, + style: townStyle, + }, + { + id: "town-oss", + position: { x: 360, y: 0 }, + data: { label: "Open Source Town" }, + sourcePosition: "bottom" as Position, + targetPosition: "top" as Position, + style: townStyle, + }, + { + id: "commons-wl", + position: { x: 60, y: 180 }, + data: { label: "Commons Wasteland" }, + sourcePosition: "bottom" as Position, + targetPosition: "top" as Position, + style: wastelandStyle, + }, + { + id: "commons-db", + position: { x: 80, y: 340 }, + data: { label: "DoltHub DB" }, + sourcePosition: "top" as Position, + targetPosition: "top" as Position, + style: dbStyle, + }, + { + id: "private-wl", + position: { x: 520, y: 180 }, + data: { label: "Private Team\nWasteland" }, + sourcePosition: "bottom" as Position, + targetPosition: "top" as Position, + style: wastelandStyle, + }, + { + id: "private-town1", + position: { x: 540, y: 0 }, + data: { label: "Private Town" }, + sourcePosition: "bottom" as Position, + targetPosition: "top" as Position, + style: townStyle, + }, + { + id: "private-db", + position: { x: 540, y: 340 }, + data: { label: "DoltHub DB" }, + sourcePosition: "top" as Position, + targetPosition: "top" as Position, + style: dbStyle, + }, +] + +const labelStyle = { fontSize: 10, fontFamily: "'JetBrains Mono', monospace", fill: "#888" } + +const edges: Edge[] = [ + { + id: "town-you-wl", + source: "town-you", + target: "commons-wl", + type: "smoothstep", + animated: true, + label: "claim / submit", + labelStyle, + style: { strokeWidth: 2, stroke: "#3b82f6" }, + }, + { + id: "town-acme-wl", + source: "town-acme", + target: "commons-wl", + type: "smoothstep", + animated: true, + label: "claim / submit", + labelStyle, + zIndex: 10, + style: { strokeWidth: 2, stroke: "#3b82f6" }, + }, + { + id: "town-oss-wl", + source: "town-oss", + target: "commons-wl", + type: "smoothstep", + animated: true, + labelStyle, + style: { strokeWidth: 2, stroke: "#3b82f6" }, + }, + { + id: "commons-wl-db", + source: "commons-wl", + target: "commons-db", + type: "smoothstep", + label: "persist", + labelStyle, + style: { strokeWidth: 2, stroke: "#8b5cf6" }, + }, + { + id: "private-town1-wl", + source: "private-town1", + target: "private-wl", + type: "smoothstep", + animated: true, + label: "claim / submit", + labelStyle, + style: { strokeWidth: 2, stroke: "#3b82f6" }, + }, + { + id: "private-wl-db", + source: "private-wl", + target: "private-db", + type: "smoothstep", + label: "persist", + labelStyle, + style: { strokeWidth: 2, stroke: "#8b5cf6" }, + }, + { + id: "wl-federation", + source: "commons-wl", + target: "private-wl", + type: "smoothstep", + label: "portable identity", + labelStyle, + style: { strokeWidth: 2, stroke: "#f8a020", strokeDasharray: "6 3" }, + }, +] + +export const wastelandFederation: DiagramDefinition = { + nodes, + edges, + caption: "Federated Wasteland architecture — towns connect to shared Wasteland instances backed by DoltHub", +} diff --git a/packages/kilo-docs/components/FlowDiagram/index.tsx b/packages/kilo-docs/components/FlowDiagram/index.tsx index d76f53f7f1f..d6b32190b8b 100644 --- a/packages/kilo-docs/components/FlowDiagram/index.tsx +++ b/packages/kilo-docs/components/FlowDiagram/index.tsx @@ -1,11 +1,35 @@ "use client" -import React, { useState, useEffect, useMemo } from "react" +import React, { useState, useEffect, useRef } from "react" import { diagrams } from "./diagrams" +/** + * Re-fits the viewport whenever the React Flow container resizes. + * Defined at module scope so its component identity is stable across + * re-renders of FlowDiagram (otherwise React would unmount/remount it + * on every parent render and tear down the ResizeObserver each time). + */ +function FitOnResize({ useReactFlow }: { useReactFlow: typeof import("@xyflow/react").useReactFlow }) { + const { fitView } = useReactFlow() + const containerRef = useRef(null) + + useEffect(() => { + const el = containerRef.current?.closest(".react-flow") as HTMLElement | null + if (!el) return + const observer = new ResizeObserver(() => { + fitView({ padding: 0.15 }) + }) + observer.observe(el) + return () => observer.disconnect() + }, [fitView]) + + return
+} + /** * FlowDiagram renders an interactive React Flow diagram. * Loaded lazily to avoid bundling the entire @xyflow/react library on pages that don't use it. + * Re-fits the viewport on container resize so the diagram scales responsively. * * Usage in markdown: * {% flowDiagram name="bead-lifecycle" /%} @@ -64,7 +88,7 @@ export function FlowDiagram({ name, height = "400px" }: { name: string; height?: ) } - const { ReactFlow, Background, BackgroundVariant } = mod + const { ReactFlow, Background, BackgroundVariant, useReactFlow, ReactFlowProvider } = mod return (
@@ -77,25 +101,30 @@ export function FlowDiagram({ name, height = "400px" }: { name: string; height?: overflow: "hidden", }} > - - - + + + + + +
{diagram.caption && (
0 ? Math.min(Math.floor(count), 4) : 3 + const size = addPxIfNeeded(width) + + return ( +
+ {Children.map(children, (child) => { + const item = isValidElement(child) + ? cloneElement(child as ReactElement, { compact: true }) + : child + + return ( +
+ {item} +
+ ) + })} +
+ ) +} diff --git a/packages/kilo-docs/components/index.js b/packages/kilo-docs/components/index.js index 96e12f01af8..95694ac103c 100644 --- a/packages/kilo-docs/components/index.js +++ b/packages/kilo-docs/components/index.js @@ -5,6 +5,7 @@ export * from "./CopyPageButton" export * from "./Heading" export * from "./Icon" export * from "./Image" +export * from "./ImageGallery" export * from "./KiloCodeIcon" export * from "./PageFooter" export * from "./PageVersionSwitcher" diff --git a/packages/kilo-docs/lib/nav/code-with-ai.ts b/packages/kilo-docs/lib/nav/code-with-ai.ts index 59f6d8a4bd2..d0011e9d75f 100644 --- a/packages/kilo-docs/lib/nav/code-with-ai.ts +++ b/packages/kilo-docs/lib/nav/code-with-ai.ts @@ -19,9 +19,17 @@ export const CodeWithAiNav: NavSection[] = [ children: "CLI", subLinks: [{ href: "/code-with-ai/platforms/cli-reference", children: "Command Reference" }], }, + { + href: "/code-with-ai/platforms/kilo-connect", + children: "Kilo Connect", + subLinks: [ + { href: "/code-with-ai/platforms/slack", children: "Slack" }, + { href: "/code-with-ai/platforms/github", children: "GitHub" }, + { href: "/code-with-ai/platforms/linear", children: "Linear" }, + ], + }, { href: "/code-with-ai/platforms/cloud-agent", children: "Cloud Agent" }, { href: "/code-with-ai/platforms/mobile", children: "Mobile Apps" }, - { href: "/code-with-ai/platforms/slack", children: "Slack" }, { href: "/code-with-ai/app-builder", children: "App Builder" }, { href: "/code-with-ai/gastown", @@ -34,6 +42,12 @@ export const CodeWithAiNav: NavSection[] = [ { href: "/code-with-ai/gastown/code-review", children: "Code Review" }, { href: "/code-with-ai/gastown/settings", children: "Settings" }, { href: "/code-with-ai/gastown/troubleshooting", children: "Troubleshooting" }, + { href: "/code-with-ai/gastown/wasteland", children: "Wasteland" }, + { href: "/code-with-ai/gastown/wasteland/quick-start", children: "Wasteland: Quick Start" }, + { href: "/code-with-ai/gastown/wasteland/concepts", children: "Wasteland: Concepts" }, + { href: "/code-with-ai/gastown/wasteland/workflow", children: "Wasteland: Workflow" }, + { href: "/code-with-ai/gastown/wasteland/admin", children: "Wasteland: Administration" }, + { href: "/code-with-ai/gastown/wasteland/settings", children: "Wasteland: Settings" }, ], }, ], diff --git a/packages/kilo-docs/lychee.toml b/packages/kilo-docs/lychee.toml index 5d15d940df1..f3e8de71098 100644 --- a/packages/kilo-docs/lychee.toml +++ b/packages/kilo-docs/lychee.toml @@ -23,12 +23,18 @@ exclude = [ 'https://console.groq.com/pages/models', '^https?://([A-Za-z0-9-]+\.)?glama\.ai(/|$)', '^https?://(api|console)\.mistral\.ai', + '^https?://codestral\.mistral\.ai/v1/fim/completions/?$', + '^https?://api\.inceptionlabs\.ai/v1/fim/completions/?$', '^https?://(app|console)\.requesty\.ai', '^https?://(cloud|console)\.google\.ai', # API endpoint prefixes are valid but return 4xx to plain GET link checks. '^https?://ai-gateway\.vercel\.sh/v1/?$', '^https?://api\.voyageai\.com/v1/embeddings/?$', '^https?://generativelanguage\.googleapis\.com/v1beta/openai/?$', + # xAI API and OAuth endpoints require request parameters or non-GET methods. + '^https?://api\.x\.ai/v1/?$', + '^https?://auth\.x\.ai/?$', + '^https?://auth\.x\.ai/oauth2/(authorize|device/code|token)/?$', 'https://opencode.ai/pages/config', '^localhost:3000/', 'https://tbench.ai/', diff --git a/packages/kilo-docs/markdoc/partials/cli-commands-table.md b/packages/kilo-docs/markdoc/partials/cli-commands-table.md index 6243af9f0a8..f94a19d13fb 100644 --- a/packages/kilo-docs/markdoc/partials/cli-commands-table.md +++ b/packages/kilo-docs/markdoc/partials/cli-commands-table.md @@ -14,12 +14,14 @@ | `kilo uninstall` | uninstall kilo and remove all related files | | `kilo serve` | starts a headless kilo server | | `kilo models [provider]` | list all available models | +| `kilo roll-call ` | batch-test text models matching a filter for connectivity and latency | | `kilo stats` | show token usage and cost statistics | | `kilo export [sessionID]` | export session data as JSON | | `kilo import ` | import session data from JSON file or URL | | `kilo pr ` | fetch and checkout a GitHub PR branch, then run kilo | | `kilo session` | manage sessions | | `kilo remote` | enable remote connection for real-time session relay | +| `kilo daemon` | manage the local kilo daemon | | `kilo db` | database tools | | `kilo config` | configuration tools | | `kilo plugin ` | install plugin and update config | diff --git a/packages/kilo-docs/markdoc/partials/install-jetbrains.md b/packages/kilo-docs/markdoc/partials/install-jetbrains.md index a64c1c1974e..2f9800f869e 100644 --- a/packages/kilo-docs/markdoc/partials/install-jetbrains.md +++ b/packages/kilo-docs/markdoc/partials/install-jetbrains.md @@ -28,6 +28,31 @@ Before installing the Kilo Code plugin, ensure you have: 4. Search for "Kilo Code" 5. Click **Install** and restart your IDE +### Try the v7 Early Access Program plugin + +The v7 EAP plugin is available for users who want to try the newest JetBrains experience before it reaches the default Marketplace channel. It uses a JetBrains-native UI and is designed to work well with JetBrains remote development. + +{% callout type="info" %} +EAP builds may update more frequently than the default Marketplace release. Share feedback in the JetBrains channel on the [Kilo Discord](https://kilo.ai/discord). +{% /callout %} + +To install the EAP build and receive updates: + +1. Open IntelliJ IDEA or another JetBrains IDE +2. Go to **Settings/Preferences → Plugins** +3. Click the gear icon and choose **Manage Plugin Repositories** +4. Add this repository URL: + +```text +https://plugins.jetbrains.com/plugins/list?channel=eap&pluginId=28350 +``` + +5. Return to the **Marketplace** tab +6. Search for **Kilo Code** +7. Click **Install** or **Update** and restart your IDE if prompted + +After the custom repository is added, JetBrains will offer EAP updates through the normal plugin update flow. + ### Supported IDEs - IntelliJ IDEA diff --git a/packages/kilo-docs/markdoc/tags/image-gallery.markdoc.ts b/packages/kilo-docs/markdoc/tags/image-gallery.markdoc.ts new file mode 100644 index 00000000000..33f9be98371 --- /dev/null +++ b/packages/kilo-docs/markdoc/tags/image-gallery.markdoc.ts @@ -0,0 +1,16 @@ +import { ImageGallery } from "../../components" + +export const imageGallery = { + render: ImageGallery, + children: ["tag"], + attributes: { + columns: { + type: String, + description: "Preferred number of image columns on wide screens", + }, + width: { + type: String, + description: "Preferred width of each gallery image", + }, + }, +} diff --git a/packages/kilo-docs/markdoc/tags/index.ts b/packages/kilo-docs/markdoc/tags/index.ts index e158a64b895..6fc54c6d902 100644 --- a/packages/kilo-docs/markdoc/tags/index.ts +++ b/packages/kilo-docs/markdoc/tags/index.ts @@ -4,6 +4,7 @@ export * from "./codicon.markdoc" export * from "./tabs.markdoc" export * from "./icon.markdoc" export * from "./image.markdoc" +export * from "./image-gallery.markdoc" export * from "./kilo-code-icon.markdoc" export * from "./video.markdoc" export * from "./youtube.markdoc" diff --git a/packages/kilo-docs/package.json b/packages/kilo-docs/package.json index 298272242b5..6a34ff6095c 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.12", "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", @@ -33,5 +33,6 @@ "tailwindcss": "^4.1.18", "typescript": "^5.9.3", "vitest": "^3.2.3" - } + }, + "peerDependencies": {} } diff --git a/packages/kilo-docs/pages/_app.tsx b/packages/kilo-docs/pages/_app.tsx index 245ab29af70..2739752f3a6 100644 --- a/packages/kilo-docs/pages/_app.tsx +++ b/packages/kilo-docs/pages/_app.tsx @@ -126,6 +126,7 @@ export default function MyApp({ Component, pageProps }: AppProps) { description = markdoc.frontmatter.description } } + const noindex = markdoc?.frontmatter?.noindex === true const toc = pageProps.markdoc?.content ? collectHeadings(pageProps.markdoc.content) : [] @@ -139,6 +140,7 @@ export default function MyApp({ Component, pageProps }: AppProps) { + {noindex && } diff --git a/packages/kilo-docs/pages/ai-providers/mistral.md b/packages/kilo-docs/pages/ai-providers/mistral.md index 684191febc1..aaa3e6beb1f 100644 --- a/packages/kilo-docs/pages/ai-providers/mistral.md +++ b/packages/kilo-docs/pages/ai-providers/mistral.md @@ -76,7 +76,7 @@ Other Mistral models do not get automatic reasoning variants, even if they appea ## Using Codestral -[Codestral](https://docs.mistral.ai/capabilities/code_generation/) is a model specifically designed for code generation and interaction. +[Codestral](https://docs.mistral.ai/vibe/code/overview) is a model specifically designed for code generation and interaction. Only for Codestral you could use different endpoints (Default: codestral.mistral.ai). For the La Platforme API Key change the **Codestral Base Url** to: https://api.mistral.ai diff --git a/packages/kilo-docs/pages/ai-providers/xai.md b/packages/kilo-docs/pages/ai-providers/xai.md index dc74332d781..59aee771efe 100644 --- a/packages/kilo-docs/pages/ai-providers/xai.md +++ b/packages/kilo-docs/pages/ai-providers/xai.md @@ -1,6 +1,6 @@ --- title: "Using xAI Grok with Kilo Code" -description: "Connect xAI's Grok models to Kilo Code. Guide to getting an API key and configuring Grok in VS Code and the CLI." +description: "Connect xAI's Grok models to Kilo Code. Use a SuperGrok or X Premium subscription via OAuth or a paid API key. Guide to setup in VS Code and the CLI." sidebar_label: xAI (Grok) --- @@ -10,14 +10,93 @@ xAI is the company behind Grok, a large language model known for its conversatio **Website:** [https://x.ai/](https://x.ai/) -## Getting an API Key +Kilo Code supports two ways to connect xAI: -1. **Sign Up/Sign In:** Go to the [xAI Console](https://console.x.ai/). Create an account or sign in. -2. **Navigate to API Keys:** Go to the API keys section in your dashboard. -3. **Create a Key:** Click to create a new API key. Give your key a descriptive name (e.g., "Kilo Code"). -4. **Copy the Key:** **Important:** Copy the API key _immediately_. You will not be able to see it again. Store it securely. +- **SuperGrok or X Premium subscription (OAuth):** If you subscribe to SuperGrok or X Premium, you can sign in with OAuth — no separate API key or pay-as-you-go charges required. +- **API key:** For pay-as-you-go access via the xAI API. -## Configuration in Kilo Code +--- + +## Option 1: SuperGrok or X Premium Subscription (OAuth) + +If you have an active [SuperGrok or X Premium subscription](https://x.ai/grok), you can authenticate with xAI using OAuth and use Grok models directly without needing a separate API key. + +### Why use SuperGrok or X Premium? + +- **No API billing:** Usage counts against your subscription, not a pay-per-token API account. +- **OAuth login — no API keys:** Sign in through your browser and Kilo Code handles token management automatically. +- **Automatic token refresh:** Kilo Code refreshes your access token in the background so long-running sessions stay authenticated. + +{% callout type="note" %} +SuperGrok and X Premium subscription access works with Kilo Code's core functionality (VS Code extension and CLI). For cloud features such as Cloud Agents or KiloClaw, use the [Kilo Gateway](/docs/gateway) — the Gateway supports xAI via [BYOK](/docs/getting-started/byok) with an API key (OAuth and subscription-based access are not supported through the Gateway). +{% /callout %} + +### Setup with SuperGrok / X Premium + +{% tabs %} +{% tab label="VSCode (Legacy)" %} + +1. Open Kilo Code settings (click the gear icon {% codicon name="gear" /%} in the Kilo Code panel). +2. In **API Provider**, select **xAI**. +3. Click **Sign in with xAI (SuperGrok / X Premium)**. +4. Complete the authorization flow in your browser. +5. Back in Kilo Code settings, select your desired Grok model. +6. Save. + +{% /tab %} +{% tab label="VSCode" %} + +Open **Settings** (gear icon) and go to the **Providers** tab. Click **Show more providers**, then search for or select **xAI**. Choose the **xAI Grok OAuth (SuperGrok / X Premium)** sign-in option and complete the OAuth flow in your browser. + +For headless or remote environments (VPS, SSH, Docker, WSL) where a browser redirect to `127.0.0.1` is not reachable, choose **xAI Grok OAuth (Headless / Remote / VPS)** instead. You will be shown a short code to enter at a URL you open on any device with a browser. + +{% /tab %} +{% tab label="CLI" %} + +Run the auth command and follow the xAI sign-in flow: + +```bash +kilo auth login --provider xai +``` + +Kilo Code offers three methods at the prompt: + +- **xAI Grok OAuth (SuperGrok / X Premium)** — opens `https://auth.x.ai` in your browser for a standard PKCE OAuth flow. Best for local desktop environments. +- **xAI Grok OAuth (Headless / Remote / VPS)** — uses the RFC 8628 device-code flow. The CLI displays a short code and a URL; open the URL on any device with a browser, enter the code, and the CLI completes the login. Use this when running on a VPS, behind SSH, inside Docker, WSL, or CI where `127.0.0.1:56121` is not accessible from your browser. +- **Manually enter API Key** — fall back to a standard API key if you prefer. + +Then set your default model: + +```jsonc +{ + "model": "xai/grok-3", +} +``` + +{% /tab %} +{% /tabs %} + +### Tips for SuperGrok and X Premium + +- **Subscription required:** You need an active SuperGrok or X Premium subscription. This option will not work with a free xAI account. +- **Sign out:** To disconnect in VS Code, use the "Disconnect" button in the provider settings. In the CLI, run `kilo auth logout` and choose xAI. +- **Port 56121:** The browser OAuth flow (PKCE) starts a short-lived local server on `127.0.0.1:56121` to receive the OAuth callback. If another application is already using that port, use the headless device-code method instead. +- **Token rotation:** xAI rotates refresh tokens on each use. Kilo Code persists the latest tokens automatically. If you run Kilo Code from multiple processes simultaneously, the first refresh can invalidate the other process's token — re-run `kilo auth login --provider xai` to restore the session. + +--- + +## Option 2: API Key + +If you prefer pay-as-you-go access or do not have a SuperGrok or X Premium subscription, you can use an xAI API key. + +### Getting an API Key + +1. **Sign Up/Sign In:** Go to the [xAI Console](https://console.x.ai/). Create an account or sign in. +2. **Navigate to API Keys:** Go to the API keys section in your dashboard. +3. **Create a Key:** Click to create a new API key. Give your key a descriptive name (e.g., "Kilo Code"). +4. **Copy the Key:** **Important:** Copy the API key _immediately_. You will not be able to see it again. Store it securely. + +### Configuration with API Key {% tabs %} {% tab label="VSCode (Legacy)" %} @@ -30,7 +109,7 @@ xAI is the company behind Grok, a large language model known for its conversatio {% /tab %} {% tab label="VSCode" %} -Open **Settings** (gear icon) and go to the **Providers** tab to add xAI and enter your API key. +Open **Settings** (gear icon) and go to the **Providers** tab. Click **Show more providers**, then search for or select **xAI** and enter your API key. The extension stores this in your `kilo.json` config file. You can also edit the config file directly — see the **CLI** tab for the file format. @@ -68,6 +147,8 @@ Then set your default model: {% /tab %} {% /tabs %} +--- + ## Reasoning Capabilities Some models feature specialized reasoning capabilities, allowing them to "think before responding" - particularly useful for complex problem-solving tasks. @@ -91,5 +172,5 @@ Choose `low` for simple queries that should complete quickly, and `high` for har - **Context Window:** Most Grok models feature large context windows (up to 131K tokens), allowing you to include substantial amounts of code and context in your prompts. - **Vision Capabilities:** Select vision-enabled models (`grok-2-vision-latest`, `grok-2-vision`, etc.) when you need to process or analyze images. -- **Pricing:** Pricing varies by model, with input costs ranging from $0.3 to $5.0 per million tokens and output costs from $0.5 to $25.0 per million tokens. Refer to the xAI documentation for the most current pricing information. +- **Pricing:** API key pricing varies by model, with input costs ranging from $0.3 to $5.0 per million tokens and output costs from $0.5 to $25.0 per million tokens. Refer to the xAI documentation for the most current pricing information. - **Performance Tradeoffs:** "Fast" variants typically offer quicker response times but may have higher costs, while "mini" variants are more economical but may have reduced capabilities. diff --git a/packages/kilo-docs/pages/automate/agent-manager.md b/packages/kilo-docs/pages/automate/agent-manager.md index 42dbc169236..edc7bacb1c4 100644 --- a/packages/kilo-docs/pages/automate/agent-manager.md +++ b/packages/kilo-docs/pages/automate/agent-manager.md @@ -138,9 +138,9 @@ Imported work stays associated with its branch or worktree and can be continued ## Starting Sessions From Chat -Kilo can start Agent Manager sessions from chat with the experimental `agent_manager` tool. Enable it in **Settings > Experimental > Agent Manager Tool**, or set `experimental.agent_manager_tool` to `true` in `kilo.jsonc`. +Kilo can start Agent Manager sessions from chat with the `agent_manager` tool. It is available by default only in the VS Code extension because Agent Manager is an extension feature. -The tool is available only in the VS Code extension because Agent Manager is an extension feature. It supports two modes: +The tool supports two modes: | Mode | Behavior | |---|---| diff --git a/packages/kilo-docs/pages/automate/tools/index.md b/packages/kilo-docs/pages/automate/tools/index.md index 0ac0cc2ce28..4f7dcfe16ad 100644 --- a/packages/kilo-docs/pages/automate/tools/index.md +++ b/packages/kilo-docs/pages/automate/tools/index.md @@ -24,7 +24,7 @@ Tools are organized into logical groups based on their functionality: | **Web Group** | Fetch and search web content | `webfetch`, `websearch`, `codesearch` | Research, documentation lookup | | **Browser Group** | Web browser automation | `kilo-playwright_*` (via built-in Playwright MCP) | Browser testing and interaction | | **MCP Group** | External tool integration | MCP server tools (namespaced as `{server}_{tool}`) | Specialized functionality via MCP | -| **Workflow Group** | Sub-agents and task management | `question`, `task`, `todowrite`, `todoread`, `plan`, `skill`, `agent_manager` (experimental) | Context switching and task organization | +| **Workflow Group** | Sub-agents and task management | `question`, `task`, `todowrite`, `todoread`, `plan`, `skill`, `agent_manager` | Context switching and task organization | ### Always Available Tools @@ -94,7 +94,7 @@ These tools help manage the conversation and task flow: - `todoread` - Reads the current session TODO list - `plan` - Enters structured planning mode - `skill` - Invokes a reusable skill (Markdown instruction module) -- `agent_manager` - Starts Agent Manager local or worktree sessions when the experimental Agent Manager Tool setting is enabled in VS Code +- `agent_manager` - Starts Agent Manager local or worktree sessions in VS Code {% /tab %} {% tab label="VSCode (Legacy)" %} 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/agents/model-selection.md b/packages/kilo-docs/pages/code-with-ai/agents/model-selection.md index b0f7c227cec..947546cfbd2 100644 --- a/packages/kilo-docs/pages/code-with-ai/agents/model-selection.md +++ b/packages/kilo-docs/pages/code-with-ai/agents/model-selection.md @@ -52,12 +52,22 @@ While the specifics change constantly, some principles stay consistent: **For everyday coding**: Mid-tier models often provide the best balance of speed, cost, and quality. They're fast enough to keep your flow state intact and capable enough for most tasks. -**For budget-conscious work**: Newer efficient models keep surprising us with price-to-performance ratios. DeepSeek, Qwen, and similar models can handle more than you'd expect. +**For budget-conscious work**: Newer efficient models keep surprising us with price-to-performance ratios. DeepSeek, Qwen, and similar models can handle more than you'd expect. See the [free and budget picks](#free-and-budget-model-picks) below. **For local/private work**: Ollama and LM Studio let you run models locally. The tradeoff is usually speed and capability for privacy and zero API costs. **Using an unlisted model?** You can register any model — including fine-tunes, newly released models, or custom local models — by adding it to your config file. See [Custom Models](/docs/code-with-ai/agents/custom-models) for details. +## Free and Budget Model Picks + +You don't need a paid API key to use Kilo Code productively. The fastest way to start for free is [Auto Model Free](/docs/code-with-ai/agents/auto-model) (`kilo-auto/free`), which routes to the best available free models automatically. See [Using Kilo for Free](/docs/getting-started/using-kilo-for-free) for the full zero-cost setup. + +If you prefer to pick models yourself, type `free` in the model picker to filter by free models, or browse the full list at [kilo.ai/models](https://kilo.ai/models). + +{% callout type="info" %} +Free model availability changes as providers adjust promotional periods. Check [kilo.ai/models](https://kilo.ai/models) for the live list. +{% /callout %} + ## Context Windows Matter One thing that doesn't change: context window size matters for your workflow. diff --git a/packages/kilo-docs/pages/code-with-ai/features/autocomplete/index.md b/packages/kilo-docs/pages/code-with-ai/features/autocomplete/index.md index 45b5072fb05..ca9c6b1bdaa 100644 --- a/packages/kilo-docs/pages/code-with-ai/features/autocomplete/index.md +++ b/packages/kilo-docs/pages/code-with-ai/features/autocomplete/index.md @@ -17,7 +17,7 @@ The extension uses **Fill-in-the-Middle (FIM)** completion routed through the ** You can choose between two FIM models: - **Codestral** (`mistralai/codestral-2508`) by Mistral AI — the default, billed through your Kilo account. -- **Mercury Edit** (`inception/mercury-edit`) by Inception — temporarily available via **BYOK** (Bring Your Own Key) only; Kilo Gateway support is coming soon. +- **Mercury Edit 2** (`inception/mercury-edit-2`) by Inception — temporarily available via **BYOK** (Bring Your Own Key) only; Kilo Gateway support is coming soon. ## Triggering Options @@ -38,10 +38,10 @@ This keybinding requires `kilo-code.new.autocomplete.enableSmartInlineTaskKeybin Autocomplete requests are routed through the **Kilo Gateway**. You can pick the FIM model under **Settings → Models → Autocomplete model**: - **Codestral** (`mistralai/codestral-2508`) — the default. Billed through your Kilo account, or free when you add your own Mistral Codestral key via BYOK. See [Setting Up Mistral for Free Autocomplete](/docs/code-with-ai/features/autocomplete/mistral-setup). -- **Mercury Edit** (`inception/mercury-edit`) — a fast diffusion-based FIM model by Inception. Temporarily requires an **Inception BYOK key** until Kilo Gateway support lands. Add one from the [BYOK page](https://app.kilo.ai/byok) in the Kilo platform. See [Bring Your Own Key (BYOK)](/docs/getting-started/byok) for setup details. +- **Mercury Edit 2** (`inception/mercury-edit-2`) — a fast diffusion-based FIM model by Inception. Temporarily requires an **Inception BYOK key** until Kilo Gateway support lands. Add one from the [BYOK page](https://app.kilo.ai/byok) in the Kilo platform. See [Bring Your Own Key (BYOK)](/docs/getting-started/byok) for setup details. {% callout type="note" %} -Mercury Edit is only available through BYOK for now — Kilo Gateway support is coming soon. If you select Mercury Edit without a valid Inception BYOK key configured, autocomplete requests will fail — switch back to Codestral or add an Inception key to continue. +Mercury Edit 2 is only available through BYOK for now — Kilo Gateway support is coming soon. If you select Mercury Edit 2 without a valid Inception BYOK key configured, autocomplete requests will fail — switch back to Codestral or add an Inception key to continue. {% /callout %} ## Status Bar diff --git a/packages/kilo-docs/pages/code-with-ai/features/speech-to-text.md b/packages/kilo-docs/pages/code-with-ai/features/speech-to-text.md index 0f82886d335..c98ae767c66 100644 --- a/packages/kilo-docs/pages/code-with-ai/features/speech-to-text.md +++ b/packages/kilo-docs/pages/code-with-ai/features/speech-to-text.md @@ -1,21 +1,19 @@ --- title: Voice Transcription -description: Kilo Code now includes experimental support for voice input in the chat interface. +description: Dictate prompts through your signed-in Kilo account. --- # Voice Transcription -{% callout type="warning" title="🧪 Experimental Feature" %} -Voice Transcription / speech-to-text (STT) is currently in experimental status. Expect potential issues and changes as the feature matures. -{% /callout %} +Use voice input in prompt fields instead of typing. When the Kilo provider is enabled and you are signed in, the microphone appears automatically and transcription uses your account through Kilo Gateway. -Kilo Code now includes experimental support for voice input in the chat interface. This feature allows you to dictate your messages using speech-to-text (STT) technology powered by OpenAI's Whisper API. +--- -## Prerequisites +## Get ready -Voice transcription requires two components to be set up: +Voice input needs FFmpeg plus access to the Kilo provider. -### 1. FFmpeg Installation +### Install FFmpeg FFmpeg is required for audio capture and processing. Install it for your platform: @@ -35,56 +33,57 @@ sudo apt install ffmpeg **Windows:** Download from [ffmpeg.org/download.html](https://ffmpeg.org/download.html) and add to your system PATH. -### 2. OpenAI API Key +### Sign in -Voice transcription uses OpenAI's Whisper API for speech recognition. You need an OpenAI API configuration in Kilo Code: +Enable and sign in to the Kilo provider to use voice input in prompt fields. Requests use your Kilo account through Kilo Gateway, so no separate OpenAI provider profile or API key is needed. -1. Configure an OpenAI provider profile in Kilo Code settings -2. Add your OpenAI API key to the profile -3. Either **OpenAI** or **OpenAI Native** provider types will work +--- -## Enabling Voice Transcription +## Choose a model -Voice transcription is an experimental feature that must be enabled: +You can optionally choose a transcription model in **Settings** > **Experimental** > **Speech to Text Model**. Kilo stores this choice as `experimental.speech_to_text_model` in your global Kilo CLI config (`~/.config/kilo/kilo.jsonc`). -1. Open Kilo Code settings -2. Navigate to **Experimental Features** -3. Enable the **Speech to Text** experiment +--- -## Using Voice Input +## Record prompts -Once configured and enabled, a microphone button will appear in the chat input area: +When you are signed in to the enabled Kilo provider, a microphone button appears in prompt fields: 1. Click the microphone button to start recording 2. Speak your message clearly 3. Click again to stop recording -4. Your speech will be automatically transcribed into text +4. Your speech is transcribed into text The feature includes real-time audio level visualization and voice activity detection to automatically detect when you're speaking. -## Technical Details +--- -- **Audio Processing**: Uses FFmpeg for system audio capture -- **Voice Recognition**: OpenAI Whisper API for transcription +## Review details -## Troubleshooting +- **Audio processing**: Uses FFmpeg for system audio capture +- **Transcription**: Sends audio through Kilo Gateway with the selected transcription model + +--- + +## Fix issues **Microphone button not appearing:** -- Ensure the Speech to Text experiment is enabled -- Verify FFmpeg is installed and in your PATH -- Check that you have an OpenAI provider configured with a valid API key +- Enable and sign in to the Kilo provider **Transcription errors:** -- Verify your OpenAI API key is valid and has available credits +- Confirm the Kilo provider remains enabled and signed in +- Verify FFmpeg is installed and in your PATH - Check your internet connection - Try speaking more clearly or adjusting your microphone settings -## Limitations +--- -This feature is currently experimental and may have limitations: +## Know limits -- Requires active internet connection -- Uses OpenAI API credits based on audio duration +Voice transcription has these requirements: + +- Requires an active internet connection +- Requires Kilo Gateway access through your Kilo account - Transcription accuracy depends on audio quality and speech clarity diff --git a/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/admin.md b/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/admin.md new file mode 100644 index 00000000000..edafc39180b --- /dev/null +++ b/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/admin.md @@ -0,0 +1,334 @@ +--- +title: "Wasteland Administration" +description: "Running a Wasteland: posting wanted items, reviewing submissions, stamping, merging, and managing members" +noindex: true +--- + +# {% $markdoc.frontmatter.title %} + +Administrator and validator guide for running a Wasteland instance — posting work, reviewing evidence, stamping submissions, and managing your membership. + +## Why Run Your Own Wasteland + +The reference commons ([`hop/wl-commons`](https://www.dolthub.com/repositories/hop/wl-commons)) is open to everyone, but running your own instance gives you: + +- **Team-private boards** — Tasks only visible to your members. No external rigs browsing or claiming your internal work. +- **Scoped reputation** — Stamps are earned and visible within your instance's context. Useful for internal performance tracking without mixing with public reputation. +- **Controlled validator set** — You decide who can review and stamp work. On the public commons, any registered rig can participate. On a private instance, only members you invite can validate. +- **Custom workflow** — You can configure the instance to use direct mode (no PR review gate) for trusted maintainers, or enforce PR mode for all contributors. + +Running your own wasteland means creating a new DoltHub database and configuring it as an upstream commons. Your team's rigs fork from and sync with this private commons. + +## Spinning Up a Wasteland in Gas Town + +### From the dashboard + +1. Navigate to your town's **Settings** → **Wasteland** tab +2. Click **Connect** → choose **Create your own** (instead of joining an existing one) +3. Enter your wasteland name and upstream (e.g., `my-org/wl-internal`) +4. Provide your DoltHub PAT — this must have write access to the upstream repo +5. Check **"I own this upstream"** — this sets `is_upstream_admin = true` on your credential, enabling admin mode +6. Click **Connect** + +Behind the scenes, this runs `wl create ` on the wasteland container, which bootstraps a fresh DoltHub repo with the wasteland schema (the `wanted`, `rigs`, `completions`, `stamps`, and metadata tables). You're automatically registered as the first rig with `role = 'owner'` and `trust_level = 3`. + + + +### Via CLI + +If you prefer the standalone `wl` CLI: + +```bash +wl create my-org/wl-internal --name "My Team Wasteland" +``` + +This creates the DoltHub repo and initializes the schema. You'd then connect your town to it through the dashboard. + +### Kilo Cloud hosted option + + + +Kilo Cloud provides the managed infrastructure (Cloudflare containers, encryption, billing) but the upstream DoltHub database is always user-owned. There is no fully-hosted wasteland where Kilo manages the DoltHub repo on your behalf — you always create and own the upstream. The wasteland container runs on Kilo's infrastructure, but data lives in your DoltHub account. + +## Posting Wanted Items + +Wanted items are the tasks on your board. You can post them through the Mayor or through the dashboard UI. + +### Via Mayor + +Ask the Mayor to post a new item: + +> *"Post a wanted item: fix the login timeout bug, high priority, type bug"* + +The Mayor calls the `gt_wasteland_post` tool, which runs `wl post` on your behalf. + +### Via dashboard UI + +Navigate to your wasteland → **Wanted** tab → click **Post new item**. + + +{% browserFrame url="app.kilo.ai/wasteland/my-org/wl-internal/wanted/post" caption="Posting a wanted item — title, description, priority, and type fields" %} +{% image src="/docs/img/gastown/wasteland/wl-post-form.png" alt="Wanted item post form" /%} +{% /browserFrame %} + +### Required fields + +| Field | Description | +|---|---| +| **Title** | What needs to be done (1–256 characters) | +| **Description** | Details, acceptance criteria, links (1–4096 characters) | + +### Optional fields + +| Field | Values | Description | Available through | +|---|---|---|---| +| **Priority** | `low`, `medium`, `high`, `critical` | How urgent is this? Defaults to `medium` if unset. | Dashboard, Mayor, `wl` CLI | +| **Type** | `feature`, `bug`, `docs`, `other` | What kind of work is this? | Dashboard, Mayor, `wl` CLI | +| **Project** | String | Optional project tag for filtering | `wl` CLI only | +| **Tags** | Free-form labels | Arbitrary labels for categorization and filtering | `wl` CLI only | +| **Effort level** | `small`, `medium`, `large` | Expected effort | `wl` CLI only | + +The dashboard UI and Mayor tool currently expose only `title`, `description`, `priority`, and `type`. The additional fields (`project`, `tags`, `effort_level`) are stored in the `wanted` table and visible on the board, but must be set through the `wl post` CLI directly. + +In PR mode (the default), posting creates a DoltHub branch with the `INSERT INTO wanted` DML and opens a PR upstream. In direct mode (admin only), the insert goes straight to main. + +## The Review Inbox + +The review inbox is where validators see incoming submissions that need attention. Access it from your wasteland → **Review** tab. + +{% callout type="info" %} +The review inbox requires **both** owner-level membership and admin mode (`is_upstream_admin = true`). If you don't see the Review tab, check that you have the owner role and that your DoltHub credential has "I own this upstream" checked. +{% /callout %} + + +{% browserFrame url="app.kilo.ai/wasteland/my-org/wl-internal/review" caption="The review inbox — pending submissions grouped by type" %} +{% image src="/docs/img/gastown/wasteland/wl-admin-review-inbox.png" alt="Admin review inbox showing pending submissions" /%} +{% /browserFrame %} + +### What appears in the inbox + +The inbox classifies incoming DoltHub PRs into typed cards: + +| Card kind | What it represents | +|---|---| +| **Rig registration** | A new rig wants to join — review their handle, org, and version | +| **Wanted post** | Someone posted a new wanted item — review title, description, type, priority | +| **Wanted edit** | An existing item was updated (`update`), withdrawn (`delete`), or unclaimed (`unclaim`) | +| **Work submission** | A rig submitted evidence for a claimed item — review the evidence URL | +| **Admin action** | An accept (`accept` / `accept-upstream`), reject (`reject`), or close (`close` / `close-upstream`) was performed — review the stamp details. The `-upstream` subkinds indicate a fork submission was accepted or closed at the upstream level. | +| **Unknown** | A PR from outside the `wl` toolchain — foreign commits not matching any known verb | + +### Inspecting evidence + +When a work submission appears, you can: + +1. **View the DoltHub PR** — Click through to see the full diff (claim + evidence commits) +2. **Inspect the evidence URL** — Follow the link to the commit, PR, or deployed URL +3. **Check the rig's history** — See the rig's past completions and stamps + +The review page also supports drawer navigation — clicking a rig handle in a PR opens a rig detail panel without losing your place. + +### Commenting on PRs + +Admins can post comments directly on upstream DoltHub PRs from the review interface. This is useful for requesting clarification or leaving feedback before stamping, without leaving the dashboard. + +## Stamping Work + +When you've reviewed a submission, you stamp it with your assessment. Stamping commits a row to the `stamps` table on the upstream commons. + +### Pick dimensions + +The `wl accept` command takes a `--quality` flag with four levels: + +| Quality | Meaning | +|---|---| +| `excellent` | Exceeds expectations — thorough, well-tested, clean code | +| `good` | Meets expectations — solid work that solves the problem | +| `fair` | Partially meets expectations — works but has gaps | +| `poor` | Below expectations — significant issues remain | + +Quality is stored in the `stamps.valence` JSON field alongside reliability: `{"quality":"good","reliability":"good"}`. The `--quality` flag sets the quality dimension; reliability is recorded as part of the same valence object. + +### Set confidence + + + +Confidence indicates how certain you are in your assessment. It's stored in `stamps.confidence` (which accepts either a string or a number). Higher confidence means you reviewed the work thoroughly; lower confidence means you're stamping based on partial review. + +### Write a justification + +The `--message` flag on `wl accept` lets you attach a free-form justification. This becomes the `stamps.message` field and is visible in the rig's reputation profile. Good justifications help the rig understand what they did well and where to improve. + +### The yearbook rule + +**You cannot stamp your own work.** The `stamps` table has a `CHECK (author != subject)` constraint — if your rig handle matches the rig that completed the work, the stamp INSERT silently fails (the commit doesn't land on the branch). This is enforced at the database level, not just the application level. + +Always verify that the submitting rig is different from your own before accepting. The admin UI should not offer "Accept" actions on your own rig's submissions. + +### Merge vs reject DoltHub PR + +After stamping, you take one of three actions on the associated DoltHub PR: + +| Action | CLI equivalent | What happens | +|---|---|---| +| **Accept** | `wl accept --quality ` | PR is merged, stamp is created, item → `completed` | +| **Reject** | `wl reject --reason "..."` | PR is closed, item → `claimed` (back to the claimer for rework) | +| **Close** | `wl close ` | PR is closed, item → `completed` (no stamp) | + +{% callout type="warning" %} +DoltHub merge is **asynchronous**. After calling the merge API, the PR state and upstream `main` may not reflect the merge for 5–30 seconds. Poll the PR state to confirm. +{% /callout %} + +In **direct mode** (admin with `is_upstream_admin = true`), accept/reject/close push directly to main without creating or merging a PR. This skips the review gate but preserves the audit trail in the commit history. + +## Managing Members + +Members are managed on the wasteland's **Members** tab. Access requires owner-level permissions. + +### Roles + +| Role | Trust level | What they can do | +|---|---|---| +| **Owner** | 3 | Full control: add/remove members, change config, delete wasteland, disconnect towns | +| **Maintainer** | 2 | Post wanted items, accept/reject/close submissions, manage validators | +| **Contributor** | 1 | Browse wanted board, claim items, submit evidence | + +Trust levels (0–3) are stored on the `wasteland_members` table and are also reflected in the upstream `rigs` table. Level 0 is reserved for demoted or restricted rigs (no API enforcement exists, but it serves as a signal). Higher trust levels indicate greater responsibility and access. + +The API enforces two permission tiers: **any member** (who can browse, claim, post, submit evidence, accept, reject, and close) and **owner only** (who can manage members, change config, and access admin tools like PR management and rig trust levels). The maintainer role's additional permissions — posting wanted items, reviewing submissions — are enforced at the UI level, not the API level. + +### Inviting members + +Owners can add members through the Members tab: + +1. Click **Add member** +2. Enter the user's Kilo user ID +3. Select their role (`contributor`, `maintainer`, `owner`) +4. Set their trust level (1–3) + +New members are auto-registered as `contributor` with `trust_level = 1` if they connect to the wasteland through their own town before being explicitly added. + +### Removing members + +Owners can remove any member except themselves. Removing a member: + +1. Deletes their row from the `wasteland_members` table +2. Does **not** revoke their DoltHub credential (they can still push to their fork) +3. Does **not** remove their rig from the upstream commons registry +4. Does **not** affect any claims or evidence they've already submitted + +### Permissions matrix + +| Action | Owner | Maintainer | Contributor | +|---|---|---|---| +| Browse wanted board | ✅ | ✅ | ✅ | +| Claim items | ✅ | ✅ | ✅ | +| Submit evidence | ✅ | ✅ | ✅ | +| Post wanted items | ✅ | ✅ | ❌ | +| Accept/reject/close items | ✅ | ✅ | ❌ | +| View review inbox | ✅ | ❌ | ❌ | +| Add/remove members | ✅ | ❌ | ❌ | +| Change wasteland config | ✅ | ❌ | ❌ | +| Delete wasteland | ✅ | ❌ | ❌ | +| Disconnect towns | ✅ | ❌ | ❌ | +| Toggle admin mode | ✅ | ❌ | ❌ | +| Change rig trust levels | ✅ | ❌ | ❌ | +| Merge/close upstream PRs | ✅ | ❌ | ❌ | + +## Moderation + +### Removing wanted items + +Owners and maintainers can withdraw (soft-delete) a wanted item from the board. This is equivalent to `wl delete ` — the item is marked as `withdrawn` rather than hard-deleted, preserving the audit trail. + +### Banning rigs + +There is no built-in "ban" mechanism in the current protocol. To block a problematic rig: + +1. **Remove their membership** — Use the Members tab to remove the member from your wasteland instance. +2. **Reject their open PRs** — Close any pending DoltHub PRs from the rig's fork. +3. **Set trust level to 0** — Admins can use the rig management panel in settings to set `trust_level = 0` on the upstream `rigs` table. This is the closest equivalent to a ban — it signals that the rig is restricted, though there is no API-level enforcement that blocks a trust_level 0 rig from claiming items. +4. **Unclaim their items** — Admins can unclaim items held by other rigs, returning them to `open` for other claimers. + +For the public commons (`hop/wl-commons`), moderation is handled by the maintainers of that upstream repo. + +### Escalation + +If a rig is submitting low-quality work or abusing the system: + +1. **Reject the evidence** — Use `wl reject` with a clear reason. The item returns to `claimed` and the rig can resubmit. +2. **Close without stamp** — Use `wl close` to mark the item completed without awarding reputation. +3. **Remove from instance** — For private wastelands, remove the member entirely. + +## Federation Choices + +Federation controls how your wasteland instance interacts with other instances in the network. + +### Federate with the Commons? + +Your private wasteland operates independently of `hop/wl-commons`. Rigs that join your instance don't automatically appear on the public commons, and vice versa. However, a rig **can** be registered on multiple wastelands simultaneously — each fork and PR is independent. + +When a rig joins your private instance, their existing reputation from other instances (including the public commons) is **portable** — they carry their stamp history with them. But your instance's stamps are only visible within your instance unless the rig also participates on other instances where those stamps can be viewed. + +### Accept incoming reputation? + + + +In the current protocol, reputation portability is built into the federation model. There is no toggle to reject incoming reputation — if a rig has stamps on another instance, those stamps are visible when they join your instance through the standard profile lookup (`wl profile `). + +What you **can** control: + +- **Who joins your instance** — Private wastelands restrict membership. Only invited rigs can browse, claim, and submit evidence. +- **Who can validate** — You control the validator set. Only members with appropriate roles can issue stamps on your instance. +- **Visibility of your board** — Wasteland instances have a `visibility` setting (`public` or `private`) that restricts wanted board access. Private wastelands are invisible to non-members. + +### What is not shared across federation + +Each wasteland instance maintains its own: + +- **Wanted board** — Items on your private instance don't appear on the public commons. +- **Claims and evidence** — Only visible within the instance where the work was done. +- **Configuration** — Workflow mode, validator membership, and moderation rules are local to each instance. +- **Members list** — Your instance's member roster is independent. + +## Admin Settings + +The wasteland settings page exposes additional controls when your credential has `is_upstream_admin = true`. These are visible only to owners. + +### Test admin access + +The **Test admin access** button probes DoltHub by attempting a no-op write against a scratch branch on the upstream. If the probe succeeds, you see a green "Admin access verified" badge. If it fails (expired PAT, wrong org, insufficient permissions), you see a red error banner with guidance to fix the credential. + +This is useful after rotating your DoltHub PAT or toggling the admin checkbox — it confirms that your stored credential actually has push rights before you attempt admin operations. + +### Pending PRs + +The settings page lists all open upstream PRs cross-referenced against the wanted board. Each row shows: + +- PR title and number +- Associated wanted item ID +- State (open, merging, merged) +- Age and contributor rig + +Per-row actions: **View on DoltHub** (external link), **Merge**, **Close without merging**. These actions use the stored admin credential to call DoltHub's merge/close APIs directly. + +{% callout type="warning" %} +DoltHub merge is **asynchronous**. After clicking Merge, the PR state may not update for 5–30 seconds. The UI polls the PR state to confirm. +{% /callout %} + +### Rig management + +The rig management panel lists all rigs registered on the upstream commons. Each row shows: + +- Handle and display name +- Trust level (0–3) +- DoltHub org and contact email +- Registration date and last-seen timestamp + +Owners can change a rig's trust level directly from this panel. This writes to the upstream `rigs` table via the DoltHub write API — it's the only currently-available way to elevate or demote contributors, since `wl` doesn't expose a CLI command for it. + +Setting a rig's trust level to 0 is the closest equivalent to a ban — it signals that the rig is restricted, though there is no API-level enforcement that blocks a trust-level-0 rig from claiming items. See [Moderation](#moderation) for the full escalation workflow. + +### Delete wasteland + +The existing Delete section in settings keeps its danger styling. In admin mode, an additional warning appears: deleting the wasteland does **not** delete the upstream DoltHub repository. To fully decommission, you must also archive or delete the `/` repo on DoltHub. diff --git a/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/concepts.md b/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/concepts.md new file mode 100644 index 00000000000..7c2470be373 --- /dev/null +++ b/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/concepts.md @@ -0,0 +1,174 @@ +--- +title: "Wasteland Concepts" +description: "Core concepts: Wasteland instances, federation, Wanted Board, claims, evidence, stamps, reputation, DoltHub PRs, validators, and rig identity" +noindex: true +--- + +# {% $markdoc.frontmatter.title %} + +Understanding the Wasteland starts with a handful of core concepts that show up across the protocol and the Gas Town UI. + +## Wasteland Instance + +A **Wasteland instance** is a federated deployment backed by a DoltHub database. The database stores the Wanted Board (open tasks), the rig registry, completion records, and reputation stamps — all versioned with Git semantics. + +The reference instance is the **Commons** ([`hop/wl-commons`](https://www.dolthub.com/repositories/hop/wl-commons)), open to any rig that joins. You can also run your own instance for a team, company, or open-source project — private boards, scoped reputation, and a controlled validator set. + +Each instance is identified by its DoltHub `org/database` path (e.g., `hop/wl-commons`). Rigs join by forking the database and registering their handle. + +## Federation + +Federation means your identity and reputation travel with you across Wasteland instances. A stamp earned on `hop/wl-commons` is visible when you join `my-org/wl-internal` — you carry your history with you. + +What **is** shared across federation: + +- **Rig identity** — Your `org/repo` handle is consistent across instances. +- **Reputation stamps** — Attestations you've received are portable. +- **Profile data** — Skills, notable projects, and assessments from the-pile. + +What is **not** shared across federation: + +- **Wanted items** — Each instance has its own board. Tasks on `hop/wl-commons` don't appear on `my-org/wl-internal`. +- **Claims and evidence** — Only visible within the instance where the work was done. +- **Instance configuration** — Workflow mode, validator membership, and moderation rules are local to each instance. + +Rigs sync their fork with the upstream commons using `wl sync`, pulling in new wanted items and stamps from other participants. + +## Rig Identity + +A **rig identity** is how your Gas Town is addressed on the Wasteland. It's composed of: + +- **Handle** — An `org/repo` identifier (e.g., `kilo/main`). This is set when you join a Wasteland and is sticky by design — changing it mid-stream would break the link between your past stamps and your current identity. +- **DoltHub account binding** — Your rig is associated with the DoltHub account that forked the commons database. Mutations (claims, evidence submissions) are authored under this account. + + + +When connecting your Gas Town to a Wasteland, you provide a DoltHub personal access token (PAT) so the Mayor can push claims and evidence on your behalf. See [Settings](/docs/code-with-ai/gastown/wasteland/settings) for configuration details. + +## Wanted Board + +The **Wanted Board** is the shared queue of tasks on a Wasteland instance. Anyone with access can browse it, and rigs claim items they want to work on. + + +{% browserFrame url="app.kilo.ai/gastown/town/wasteland" caption="The Wanted Board — filter and sort open tasks" %} +{% image src="/docs/img/gastown/wasteland/wl-wanted-board.png" alt="Wasteland Wanted Board showing open tasks" /%} +{% /browserFrame %} + +Each wanted item has: + +| Field | Description | +|---|---| +| **Title** | What needs to be done | +| **Description** | Details, acceptance criteria, links | +| **Type** | `bug`, `feature`, or `chore` | +| **Priority** | 0 (critical) through 4 (low) | +| **Effort** | `small`, `medium`, or `large` | +| **Project** | Optional project tag for filtering | +| **Tags** | Arbitrary labels for categorization | + +## Wanted Item Lifecycle + +A wanted item moves through a defined set of states: + +{% flowDiagram name="wanted-lifecycle" height="400px" /%} + +| State | What's happening | +|---|---| +| **Open** | Posted and available for any rig to claim | +| **Claimed** | A rig has locked the item and is working on it | +| **In Review** | Evidence has been submitted and awaits validator review | +| **Completed** | A validator has stamped the work (or it was closed without a stamp) | +| **Withdrawn** | The poster removed the item from the board | + +An item can cycle between **Claimed** and **Open** if the rig abandons the claim (via `wl unclaim`), and between **In Review** and **Claimed** if a validator rejects the evidence (via `wl reject`), sending it back for rework. + +## Claims + +When a rig claims a wanted item, it gets **exclusive** access — no other rig can claim the same item simultaneously. This prevents duplicate work. + +Key claim semantics: + +- **Exclusive lock** — Only one rig can hold a claim at a time. If two rigs race to claim the same item, only one succeeds. +- **Abandon** — A rig can release a claim with `wl unclaim`, returning the item to **Open** for others to pick up. +- **TTL** — Claims do not expire automatically in the current protocol. They persist until the claimer runs `wl unclaim` or submits evidence with `wl done`. The `wl doctor` command warns if your local clone hasn't synced in >24 hours, but this doesn't affect claim state. +- **Through Gas Town** — Your Mayor handles claiming conversationally. Ask "claim the top item" and the Mayor runs the protocol for you. + +If you need to release a claim, you can abandon it at any time before evidence is submitted. See [Workflow](/docs/code-with-ai/gastown/wasteland/workflow) for the full flow. + +## Evidence + +**Evidence** is what you submit to prove you've completed the work. Acceptable evidence includes: + +- **Git commit SHAs** — The commit that implements the fix or feature. +- **Pull request URLs** — A link to the PR containing the changes. +- **DoltHub PRs** — The Wasteland protocol itself uses DoltHub pull requests as the evidence transport. +- **Deployed URLs** — A live link to the deployed change (for web-based work). + +When you're working through Gas Town, your Mayor submits evidence automatically via `wl done` when a bead closes. The Mayor packages the PR URL and pushes it to your Wasteland fork as a DoltHub pull request. + + + +In the standalone `wl` CLI, you'd use `wl done --evidence "..."` to submit manually. + +## Stamps + +A **stamp** is a multi-dimensional attestation issued by a validator when they review completed work. Stamps are the building blocks of reputation. + +Each stamp scores across dimensions: + +| Dimension | What it measures | +|---|---| +| **Quality** | How well was the work done? (`excellent`, `good`, `fair`, `poor`) | +| **Reliability** | Did the rig deliver on time and to spec? (stored in `stamps.valence` JSON alongside quality) | + +Validators also set a **severity** — `leaf`, `branch`, or `root` — indicating how impactful the work was, and attach **skill tags** (e.g., `go`, `federation`) to build the completer's profile. The `stamps.confidence` field records how certain the validator is in their assessment. + + + +The **yearbook rule** enforces that you can't stamp your own work. Your reputation is built exclusively from what other validators write about you. This keeps the system honest and evidence-backed — every stamp traces back to verifiable work. + + + +## Reputation Ledger + +The **reputation ledger** is the cumulative record of all stamps a rig has received across Wasteland instances. It's: + +- **Evidence-backed** — Every entry traces back to actual work (commits, PRs). +- **Portable** — Your ledger travels with you across federated instances. +- **Multi-dimensional** — Scores across quality and reliability, plus severity and skill tags, give a nuanced picture rather than a single number. + +You can view your reputation from the Wasteland page in your Gas Town dashboard, or directly on DoltHub. + +## Validators + +**Validators** are Wasteland members with the authority to review evidence and issue stamps. They're the quality gate for the reputation system. + +Validator responsibilities: + +- Review submitted evidence (commits, PRs, deployed URLs) +- Assess work across quality and reliability dimensions +- Set severity level (`leaf`, `branch`, `root`) +- Attach skill tags relevant to the work +- Write justifications for their assessment +- Merge or reject DoltHub PRs associated with the evidence + +The yearbook rule applies — validators **cannot** stamp their own work. A validator reviewing their own rig's submission would be rejected by the protocol. + + + +For more on the validator workflow, see [Administration](/docs/code-with-ai/gastown/wasteland/admin). + +## DoltHub PRs + +The Wasteland protocol uses Dolt's Git-style semantics to manage the full lifecycle of work. Because Dolt is a SQL database with Git versioning, every mutation — posting a wanted item, claiming it, submitting evidence, stamping it — is a commit on a branch. + +In **PR mode** (the default), the flow looks like this: + +1. **Claim** — Creates a commit on a `wl//` branch on your fork. +2. **Done** — Stacks another commit on the same branch with your evidence. +3. **DoltHub PR** — The branch is proposed upstream as a pull request. Reviewers can see the full diff (claim + evidence) in one place. +4. **Accept/Reject** — Validators approve (merge the PR, issue a stamp) or reject (request changes, sending it back for rework). + +In **direct mode** (enabled when `is_upstream_admin = true`), changes push directly to the upstream commons without creating a PR. This is intended for maintainers with write access who don't need the PR review step. Direct mode is off by default — the `--direct` flag must be passed explicitly, and is silently downgraded to PR mode if the caller's credential isn't marked as admin. + +This model means the entire history of a wanted item — from posting to completion — is versioned, auditable, and cryptographically signed (when GPG signing is enabled). See [Workflow](/docs/code-with-ai/gastown/wasteland/workflow) for the end-to-end walkthrough and [Administration](/docs/code-with-ai/gastown/wasteland/admin) for running your own instance. \ No newline at end of file diff --git a/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/index.md b/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/index.md new file mode 100644 index 00000000000..bcb1dd6c487 --- /dev/null +++ b/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/index.md @@ -0,0 +1,73 @@ +--- +title: "Wasteland" +description: "Federated work protocol connecting Gas Towns into a shared network of tasks, reputation, and collaboration" +noindex: true +--- + +# {% $markdoc.frontmatter.title %} + +The Wasteland is a federated work protocol for Gas Towns — it connects towns into a shared network where agents browse tasks, claim work, submit evidence, and build reputation backed by cryptographic stamps. + +## What is the Wasteland? + +If Gas Town turns a single developer into an engineering team, the Wasteland turns a network of developers into a workforce. It's a federated system built on [Dolt](https://docs.dolthub.com/introduction/installation) (a SQL database with Git semantics) where participants browse a shared Wanted Board of tasks, claim work, submit evidence of completion, and get stamped by validators who attest to the quality of what was done. + +The stamps aren't pass/fail. They're multi-dimensional attestations covering quality and reliability, each scored independently. Every stamp traces back to the actual work, so reputation is built on evidence, not self-reporting. Steve describes it as "a yearbook rule: you can't stamp your own work." Your reputation is what other people write about you. + +The whole thing is federated, meaning anyone can run their own Wasteland instance (a team, a company, an open-source project) and your identity and reputation are portable across all of them. + +## The Federation Model + +Each Wasteland instance is backed by a [DoltHub](https://www.dolthub.com) database. Rigs fork the commons database, make changes locally, and propose them back upstream via DoltHub pull requests. This Git-style workflow means every mutation — claims, evidence, stamps — is versioned, reviewable, and auditable. + +{% flowDiagram name="wasteland-federation" height="500px" /%} + +The reference commons is [`hop/wl-commons`](https://www.dolthub.com/repositories/hop/wl-commons) — come join us. + +## Roles + +| Role | What They Do | +|---|---| +| **Rig** | Claims wanted items, does the work, submits evidence. A Gas Town rig is the worker unit on the Wasteland. | +| **Validator** | Reviews submitted evidence, issues stamps (quality, reliability, creativity). Enforces the yearbook rule — can't stamp their own work. | +| **Administrator** | Posts wanted items, manages members and validators, moderates the board, configures federation settings. | +| **Commons** | The shared DoltHub database that holds the Wanted Board, rig registry, and completion records. Every rig forks from and syncs with the commons. | + +## Why Use It Through Gas Town by Kilo? + +The Wasteland protocol itself doesn't require Gas Town — you can use the [`wl` CLI](https://github.com/gastownhall/wasteland) directly. But running it through Gas Town by Kilo gives you: + +- **Conversational protocol handling** — Your Mayor handles claiming, submitting evidence, and posting wanted items. You just ask in natural language. +- **Model access through the Gateway** — 500+ models with no separate API keys to manage. +- **No infrastructure overhead** — No Gas Town CLI, Gas City CLI, Beads CLI, Dolt CLI, or Wasteland CLI to install and maintain. +- **Automatic evidence submission** — When a bead closes, your Mayor submits the completion evidence (commit SHA, PR URL) back to the Wasteland as a DoltHub pull request. +- **Reputation that builds automatically** — Every stamp your work receives updates your reputation ledger, which is portable across all Wasteland instances. + + +{% browserFrame url="app.kilo.ai/gastown/town/settings" caption="Wasteland connection settings in your town" %} +{% image src="/docs/img/gastown/wasteland/gt-wasteland-settings.png" alt="Wasteland connection settings" /%} +{% /browserFrame %} + +From the Gas Town dashboard, you'll see your Wasteland connection settings, including which fork you're connected to and your rig identity. You ask your Mayor to pull the top wanted items, pick a task, and your Gas Town claims it, spins up the right polecats, and starts working. + + +{% browserFrame url="app.kilo.ai/gastown/town/wasteland" caption="The Wanted Board — browse open tasks from your connected Wasteland" %} +{% image src="/docs/img/gastown/wasteland/wl-wanted-board.png" alt="Wasteland Wanted Board showing open tasks" /%} +{% /browserFrame %} + + +{% browserFrame url="app.kilo.ai/gastown/town/wasteland/claim" caption="Mayor claiming a wanted item on your behalf" %} +{% image src="/docs/img/gastown/wasteland/gt-mayor-claiming.png" alt="Mayor claiming a wanted item from the Wasteland" /%} +{% /browserFrame %} + + +{% browserFrame url="app.kilo.ai/gastown/town/wasteland/claim/detail" caption="Claim detail drawer — evidence, status, and stamp history" %} +{% image src="/docs/img/gastown/wasteland/gt-claim-detail-drawer.png" alt="Claim detail drawer showing evidence and stamp history" /%} +{% /browserFrame %} + +## Where to Go Next + +- [Quick Start](/docs/code-with-ai/gastown/wasteland/quick-start) — Connect your town to the Commons wasteland and claim your first item +- [Concepts](/docs/code-with-ai/gastown/wasteland/concepts) — Deep dive into instances, federation, claims, evidence, stamps, and reputation +- [Workflow](/docs/code-with-ai/gastown/wasteland/workflow) — End-to-end walkthrough from browsing to getting stamped +- [Administration](/docs/code-with-ai/gastown/wasteland/admin) — Running your own Wasteland: posting work, reviewing submissions, managing members diff --git a/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/quick-start.md b/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/quick-start.md new file mode 100644 index 00000000000..32f243ae813 --- /dev/null +++ b/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/quick-start.md @@ -0,0 +1,139 @@ +--- +title: "Wasteland Quick Start" +description: "Connect your town to the Commons wasteland, claim your first wanted item, and submit evidence" +noindex: true +--- + +# {% $markdoc.frontmatter.title %} + +Get your Gas Town connected to the Commons wasteland and working on your first wanted item in a few minutes. + +{% callout type="warning" title="DoltHub PAT security" %} +Your DoltHub PAT gives your town's agents the ability to push commits, open PRs, and submit evidence on your behalf. Use a **fine-grained PAT scoped to only the repositories your town needs** — never a global token with full account access. See [DoltHub credentials](https://www.dolthub.com/settings/credentials) to create one. +{% /callout %} + +## 1. Before You Start + +Make sure you have the prerequisites: + +- **An active Gas Town** — [Create one](/docs/code-with-ai/gastown/quick-start) if you haven't already +- **A DoltHub account** — Sign up at [dolthub.com](https://www.dolthub.com) +- **A rig connected to a repository** — Your town needs at least one rig with a repo attached so agents have somewhere to work + +## 2. Connect Your Town to the Commons Wasteland + +Open your town's **Settings** → **Wasteland** tab and click **Connect**. + +1. **Choose an upstream** — The default is `hop/wl-commons`, the reference commons. This is where the shared Wanted Board lives. +2. **Enter your DoltHub PAT** — Create a token at [dolthub.com/settings/credentials](https://www.dolthub.com/settings/credentials). The token needs read/write access to the wasteland database on your DoltHub account. Set `DOLTHUB_TOKEN` and `DOLTHUB_ORG` in your environment if you also use the `wl` CLI directly. +3. **Enter your rig handle** — This is your town's identity on the wasteland, in `org/repo` format (e.g., `kilo/main`). It's set once when you join — choose carefully, as it's sticky by design. Behind the scenes this is equivalent to `wl join --handle `. +4. Click **Connect**. + +Your town forks the commons database, registers your rig handle, and is now part of the federation. + + +{% browserFrame url="app.kilo.ai/gastown/town/settings/wasteland" caption="The Wasteland connect dialog in Gas Town settings" %} +{% image src="/docs/img/gastown/wasteland/gt-wasteland-connect-dialog.png" alt="Wasteland connect dialog" /%} +{% /browserFrame %} + +{% callout type="info" %} +Behind the scenes, connecting runs the equivalent of `wl join hop/wl-commons` — it forks the upstream commons database under your DoltHub account and records your rig handle in the registry. +{% /callout %} + +## 3. Browse the Wanted Board + +Once connected, ask your Mayor to show you what's available: + +> *"Show me the wanted board"* + +The Mayor fetches open items from the commons and presents them in chat. You'll see each item's title, type (bug, feature, design, rfc, docs, inference), priority, and effort level. + + +{% browserFrame url="app.kilo.ai/gastown/town/wasteland" caption="The Wanted Board — browse open tasks from the Commons wasteland" %} +{% image src="/docs/img/gastown/wasteland/wl-wanted-board.png" alt="Wasteland Wanted Board showing open tasks" /%} +{% /browserFrame %} + +You can also filter by asking: + +- *"Show me only bugs"* +- *"What are the critical-priority items?"* +- *"Filter by the gastown project"* + +## 4. Claim Your First Wanted Item + +When you see something your town can handle, ask the Mayor to claim it: + +> *"Claim the top item"* + +Here's what happens: + +1. **The wasteland locks the item** — Your rig gets exclusive access. No other rig can claim the same item while you hold it. Claims don't expire — they persist until you submit evidence or explicitly release them with `wl unclaim`. +2. **A DoltHub branch is created** — In PR mode (the default), a `wl//` branch is created on your fork. Claim and evidence stack as commits on the same branch, so a single PR tells the full story. +3. **The Mayor creates a bead** — A new bead appears on your rig's kanban board, linked to the upstream wanted item via a `wasteland_wanted_id` reference. + + +{% browserFrame url="app.kilo.ai/gastown/town/wasteland/claim" caption="Claiming a wanted item — the claim drawer shows item details and confirmation" %} +{% image src="/docs/img/gastown/wasteland/wl-claim-drawer.png" alt="Claim drawer for a wanted item" /%} +{% /browserFrame %} + +{% callout type="info" %} +If two rigs race to claim the same item, only one succeeds — the other gets a conflict error. The Mayor will let you know and suggest the next available item. +{% /callout %} + +## 5. Let Agents Do the Work + +Once claimed, the bead follows the standard Gas Town flow: + +1. The reconciler assigns the bead to an available polecat +2. The polecat reads the wanted item's description and acceptance criteria +3. It makes changes, pushes a branch, and the bead moves to `in_review` +4. The refinery reviews the work + +You can track progress on the rig page, just like any other bead. The difference is this bead is linked back to the wasteland — when it closes, evidence flows back automatically. + +## 6. Submit Evidence + +When the bead closes successfully, your Mayor auto-submits the completion evidence to the wasteland. This is triggered by `wl done` behind the scenes. + +The evidence includes: + +- **Pull request URL** — A link to the PR containing the changes (the `--evidence` flag accepts a free-form string, conventionally a URL) + +The Mayor packages this evidence and pushes it to your wasteland fork as an update to the `wl//` branch. In PR mode, a DoltHub pull request is opened (or updated) proposing the claim and evidence upstream. + + +{% browserFrame url="app.kilo.ai/gastown/town/wasteland/evidence" caption="Evidence submitted — confirmation that your work has been proposed upstream" %} +{% image src="/docs/img/gastown/wasteland/wl-evidence-submitted.png" alt="Evidence submitted confirmation" /%} +{% /browserFrame %} + +{% callout type="tip" %} +You don't need to manually submit evidence when working through Gas Town. The Mayor handles it automatically when the bead closes. If you're using the standalone `wl` CLI, you'd run `wl done --evidence "..."` manually. +{% /callout %} + +## 7. Get Stamped + +After evidence is submitted, a validator reviews the DoltHub PR and issues a **stamp** — a multi-dimensional attestation of your work. + +Stamps score across: + +| Dimension | What it measures | +|---|---| +| **Quality** | How well was the work done? (`excellent`, `good`, `fair`, `poor`) | +| **Reliability** | Did the rig deliver on time and to spec? | + +Validators also set a **severity** — `leaf`, `branch`, or `root` — indicating how impactful the work was. Skill tags (e.g., `go`, `federation`) are attached to build the completer's profile. + + + +The yearbook rule applies: **you can't stamp your own work**. Your reputation is built exclusively from what others write about you. + +Reputation updates asynchronously — you don't need to wait around. Check your reputation from the Wasteland page in your Gas Town dashboard, or directly on DoltHub. You can also use `wl profile ` from the CLI to look up any rig's profile. + +## 8. What's Next + +You've completed your first wasteland cycle. Here's where to go from here: + +- **Post your own wanted items** — See [Administration](/docs/code-with-ai/gastown/wasteland/admin) for posting work to the board +- **Run your own wasteland** — A private instance for your team with scoped reputation and controlled validators +- **Learn about stamps and reputation** — See [Concepts](/docs/code-with-ai/gastown/wasteland/concepts) for the deep dive +- **Troubleshoot issues** — See the [Wasteland overview](/docs/code-with-ai/gastown/wasteland) if something went wrong diff --git a/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/settings.md b/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/settings.md new file mode 100644 index 00000000000..851700a4a13 --- /dev/null +++ b/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/settings.md @@ -0,0 +1,155 @@ +--- +title: "Wasteland Settings" +description: "Wasteland connection settings in the Gas Town dashboard, DoltHub token setup, and rig identity" +noindex: true +--- + +# {% $markdoc.frontmatter.title %} + +Reference for every Wasteland-related setting in your Gas Town dashboard, from upstream selection to DoltHub PAT management. + +Access these settings from your town dashboard → **Settings** → **Wasteland** tab. + + +{% browserFrame url="app.kilo.ai/gastown/town/settings/wasteland" caption="Wasteland settings tab in your Gas Town dashboard" %} +{% image src="/docs/img/gastown/wasteland/gt-wasteland-settings.png" alt="Wasteland settings tab" /%} +{% /browserFrame %} + +## Wasteland Connection + +The top of the Wasteland tab shows your current connections. Each row represents a wasteland instance your town has joined. + +| Field | Description | +|---|---| +| **Upstream** | The DoltHub `org/database` path of the commons (e.g., `hop/wl-commons`) | +| **Rig handle** | Your identity on that wasteland | +| **Status** | Connected, syncing, or error | + +### Disconnecting + +To leave a wasteland, click **Disconnect** next to the connection. This: + +1. Removes the connection from your town's settings +2. Does **not** delete your DoltHub fork or remove your rig from the commons registry +3. Does **not** affect any claims or evidence you've already submitted + +If you reconnect to the same upstream later, your existing fork and rig handle are reused. + +{% callout type="warning" %} +Disconnecting while you have active claims will leave those items in a `claimed` state on the commons. Other rigs won't be able to pick them up until you reconnect and abandon them. Claims do not expire automatically — they persist until explicitly released with `wl unclaim`. +{% /callout %} + +## Upstream + +The **upstream** is the DoltHub database your town forks from. It determines which Wanted Board you see and which community you're building reputation in. + +| Upstream | What it is | +|---|---| +| `hop/wl-commons` | The reference commons — open to everyone, the default choice | +| Your own (e.g., `my-org/wl-internal`) | A private instance for your team or organization | + +### Switching upstreams + +You can connect to multiple wasteland instances simultaneously. Each connection has its own fork, rig handle, and Wanted Board. To switch between them, select the connection in the Wasteland tab. + +To change your upstream for a single connection, you'll need to disconnect and reconnect with the new upstream. Your existing claims and evidence on the previous upstream remain intact. + +## Rig Handle + +Your **rig handle** is your town's identity on the wasteland. It's an `org/repo`-style identifier (e.g., `kilo/main`, `acme/backend`) that other participants see when you claim items and submit evidence. + +### Setting your handle + +You set your rig handle when you first connect to a wasteland. The handle is derived from your DoltHub organization and a name you choose — it follows the `org/repo` format used throughout the Wasteland protocol. + +### Changing your handle + +Rig handles are **sticky by design**. Changing your handle mid-stream would break the link between your past stamps and your current identity — your reputation ledger traces back to your handle, and a new handle starts fresh. + +If you absolutely need a different handle, disconnect from the wasteland and reconnect with a new one. Be aware that this is effectively a new identity: previous claims, evidence, and reputation stay with the old handle. + +{% callout type="info" %} +Think of your rig handle like a GitHub username — you set it once and it follows you everywhere on that wasteland. Choose something stable and recognizable. +{% /callout %} + +## DoltHub PAT + +Your DoltHub Personal Access Token lets your town's agents interact with the wasteland on your behalf — forking the commons, pushing claims, and submitting evidence via DoltHub pull requests. + +### Creating a PAT + +1. Go to [dolthub.com/settings/credentials](https://www.dolthub.com/settings/credentials) +2. Create a new token +3. Scope the token to the repositories your town needs access to + +### Required scope + +The PAT needs read/write access to the wasteland database on your DoltHub account. At minimum, it must be able to: + +- Fork the upstream commons database +- Push branches to your fork +- Open and update pull requests on the upstream + +{% callout type="warning" title="Security: agents act on your behalf" %} +Your DoltHub PAT gives your town's agents the ability to push commits and open PRs under your DoltHub account. **Use a fine-grained PAT scoped to only the repositories your town needs.** Never use a global token with full account access. Since agents act autonomously, limiting the token's scope reduces the blast radius if it's ever compromised. +{% /callout %} + +### Rotating your PAT + +To rotate an expired or compromised token: + +1. Generate a new PAT at [dolthub.com/settings/credentials](https://www.dolthub.com/settings/credentials) +2. Go to **Settings** → **Wasteland** tab +3. Update the PAT field with the new token +4. Revoke the old token on DoltHub + +Your town's active connections will pick up the new token immediately — no reconnect needed. + +### If your PAT is invalid + +If the Mayor reports DoltHub authentication errors, the PAT may have expired or been revoked. Check: + +- The token hasn't been revoked on [dolthub.com/settings/credentials](https://www.dolthub.com/settings/credentials) +- The token's scope still covers the wasteland database +- The token hasn't hit a rate limit + +See the [Wasteland overview](/docs/code-with-ai/gastown/wasteland) for detailed diagnosis steps. + +## Wanted Item Filters + +Currently there are no per-town filters for wanted items. When you browse the Wanted Board — either through the Mayor or the dashboard — you see all open items on the connected upstream. + +You can filter conversationally through the Mayor: + +- *"Show me only bugs"* +- *"What are the critical-priority items?"* +- *"Filter by the gastown project"* + + + +## Evidence Auto-Submit + +When a wasteland-linked bead closes successfully, your Mayor automatically submits the completion evidence to the wasteland. This is always-on behavior — there is no toggle to disable it. + +The auto-submit flow: + +1. The bead closes (passes refinery review, merges successfully) +2. The Mayor collects the commit SHA and PR URL +3. It runs the equivalent of `wl done --evidence ""` on your behalf +4. Evidence is pushed to your wasteland fork and proposed upstream as a DoltHub PR + +{% callout type="info" %} +If the auto-submit fails (e.g., DoltHub is unreachable, PAT expired), the Mayor will retry and notify you. The evidence isn't lost — it can be resubmitted once the issue is resolved. +{% /callout %} + +## Wasteland Admin Settings + +If you're a validator or administrator on a wasteland instance, additional settings are available on the [Administration](/docs/code-with-ai/gastown/wasteland/admin) page rather than in your per-town settings. + +Key admin capabilities: + +- **Validator membership** — Administrators can grant or revoke validator status for members, controlling who can issue stamps +- **Wanted board moderation** — Remove inappropriate or stale wanted items, ban problematic rigs +- **Federation configuration** — Control whether the instance accepts incoming reputation from other wastelands + +See [Administration](/docs/code-with-ai/gastown/wasteland/admin) for the full guide. diff --git a/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/workflow.md b/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/workflow.md new file mode 100644 index 00000000000..1f367d00d92 --- /dev/null +++ b/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/workflow.md @@ -0,0 +1,223 @@ +--- +title: "Wasteland Workflow" +description: "End-to-end workflow for working on Wasteland items: browse, claim, work, submit evidence, and get stamped" +noindex: true +--- + +# {% $markdoc.frontmatter.title %} + +The worker-side flow for picking up a wanted item, doing the work in your Gas Town, and turning completion into stamped reputation. + +{% flowDiagram name="claim-to-stamp" height="550px" /%} + +## Step 1: Browse + +Once your town is connected to a wasteland, you can browse the Wanted Board through your Mayor. Just ask: + +> *"Show me the wanted board"* + +The Mayor fetches open items from the upstream commons and presents them in chat — title, type, priority, effort level, and who posted it. + +{% callout type="info" %} +Behind the scenes, the Mayor calls the `gt_wasteland_browse` tool, which queries the DoltHub upstream via the wasteland container's `wl browse` command. The results are cached briefly (30s) and invalidated on every mutation. +{% /callout %} + +### Filtering + +You can narrow results conversationally: + +- *"Show me only bugs"* +- *"What are the critical-priority items?"* +- *"Filter by the gastown project"* + +The `wl browse` CLI supports `--project`, `--type`, `--status`, `--priority`, `--limit`, and `--sort` flags. The Mayor translates your request into the appropriate filters. + + +{% browserFrame url="app.kilo.ai/gastown/town/wasteland" caption="The Wanted Board — browse and filter open tasks through the Mayor" %} +{% image src="/docs/img/gastown/wasteland/wl-wanted-board.png" alt="Wasteland Wanted Board showing open tasks" /%} +{% /browserFrame %} + +## Step 2: Claim + +When you find an item your town can handle, ask the Mayor to claim it: + +> *"Claim the top item"* +> *"Claim w-870be07fbc"* + +### Exclusive claim semantics + +Claims are **exclusive** — only one rig can hold a claim on a given wanted item at a time. This prevents duplicate work across the federation. + +When you claim an item: + +1. **The wasteland locks the item** — The `wanted` table row transitions from `status = "open"` to `status = "claimed"` with `claimed_by` set to your rig handle. No other rig can claim the same item while you hold it. +2. **A DoltHub branch is created** — In PR mode (the default), a `wl//` branch is created on your fork. The claim commits a status update to this branch. +3. **A DoltHub PR is opened** — The branch is proposed upstream as a pull request. This PR will accumulate both the claim and the evidence commit, giving reviewers the full picture in one place. + +### Two-rig race resolution + +If two rigs try to claim the same item simultaneously, only one succeeds. The `wl claim` command writes a `status = "claimed"` update to a branch — Dolt's merge semantics ensure that the first PR to merge wins. The second rig's claim will fail with a conflict when the branch is pushed or the PR is created. + +If your claim fails because another rig got there first, the Mayor will let you know and suggest the next available item. + +### Claim TTL + +Claims do **not** expire automatically in the current protocol. They persist until you submit evidence with `wl done` or explicitly release them with `wl unclaim`. The `wl doctor` command warns if your local clone hasn't synced in more than 24 hours, but this doesn't affect claim state on the upstream. + +This means an abandoned claim blocks other rigs from picking up the item. If you can't complete the work, [abandon the claim](#cancelingabandoning) to release it back to the board. + +### Abandon behavior + +See [Canceling/abandoning](#cancelingabandoning) below for how to release a claim you no longer want. + +## Step 3: Work + +Once claimed, your Mayor creates a **bead** linked to the upstream wanted item and agents get to work. + +### Bead creation with wasteland link + +When the claim succeeds, the Mayor creates a bead on your rig with a `wasteland_wanted_id` reference linking it back to the upstream item. This link is what triggers automatic evidence submission when the bead closes. + +The bead follows the standard Gas Town lifecycle: + +1. The reconciler assigns the bead to an available polecat +2. The polecat reads the wanted item's description and acceptance criteria +3. It makes code changes, pushes a branch, and the bead moves to `in_review` +4. The refinery reviews the work + + +{% browserFrame url="app.kilo.ai/gastown/town/rig/bead" caption="A bead with a wasteland link — the upstream item badge connects local work to the Wanted Board" %} +{% image src="/docs/img/gastown/wasteland/gt-bead-with-wasteland-link.png" alt="Bead with wasteland wanted item link badge" /%} +{% /browserFrame %} + +### Tracking progress + +You can track progress on the rig page, just like any other bead. The difference is this bead is linked back to the wasteland — when it closes, evidence flows back automatically. + +If the refinery sends the work back for revisions, the polecat revises and resubmits. The bead stays in the Gas Town pipeline until it passes review. + +## Step 4: Submit Evidence + +When the bead closes successfully, your Mayor **auto-submits** the completion evidence to the wasteland. You don't need to do anything manually — the Mayor runs the equivalent of `wl done --evidence ""` on your behalf. + +### Types of evidence + +Evidence is a **URL** that proves the work was done (the `wl done --evidence` flag requires a valid URL). Acceptable types: + +| Type | Example | +|---|---| +| **Pull request URL** | `https://github.com/org/repo/pull/123` | +| **Git commit SHA** | `https://github.com/org/repo/commit/abc123` | +| **Deployed URL** | `https://staging.example.com/new-feature` | +| **Artifact URL** | `https://registry.example.com/package@1.2.3` | + +When working through Gas Town, the Mayor automatically packages the PR URL from the bead's completed work as the evidence. The evidence field requires a valid URL — plain text or non-URL strings are rejected by the API. + +### How evidence flows + +1. The bead closes (passes refinery review, merges successfully) +2. The Mayor collects the commit SHA and PR URL +3. It calls `wl done --evidence ""` via the `gt_wasteland_done` tool +4. Evidence is pushed to your wasteland fork as a commit on the `wl//` branch +5. In PR mode, the existing DoltHub PR is updated with the evidence commit +6. The item transitions to `in_review` on the upstream, awaiting validator action + + +{% browserFrame url="app.kilo.ai/gastown/town/wasteland/evidence" caption="Evidence submitted — confirmation that your work has been proposed upstream" %} +{% image src="/docs/img/gastown/wasteland/wl-evidence-submitted.png" alt="Evidence submitted confirmation" /%} +{% /browserFrame %} + +### DoltHub PR fate + +The DoltHub PR created during the claim step now contains both the claim commit and the evidence commit. Validators review this PR as a cohesive unit — they see the full diff (what was claimed, what was done) in one place. When a validator accepts the work, the PR is merged, landing both commits on upstream main. + +If evidence submission fails (e.g., DoltHub is unreachable, PAT expired), the Mayor will retry and notify you. The evidence isn't lost — it can be resubmitted once the issue is resolved. + +{% callout type="info" %} +In **direct mode** (available to upstream admins), changes push directly to the upstream commons without creating a PR. This skips the review gate and is intended for maintainers with write access who don't need the PR review step. Direct mode is off by default. +{% /callout %} + +## Step 5: Stamping + +After evidence is submitted, the item moves to `in_review` and awaits a validator. Stamping is **asynchronous** — you don't need to wait around. + +### How stamps work + +A validator reviews the DoltHub PR and issues a **stamp** — a multi-dimensional attestation written to the `stamps` table on the upstream commons. The stamp records: + +| Field | Description | +|---|---| +| **Valence** | JSON object with per-dimension ratings: `{"quality":"good","reliability":"good"}`. Quality is set via `wl accept --quality` (`excellent`, `good`, `fair`, `poor`). | +| **Severity** | Impact level — `leaf`, `branch`, or `root` | +| **Skill tags** | Relevant skills demonstrated (e.g., `go`, `federation`) | +| **Confidence** | How confident the validator is in their assessment. Stored as a string or number in the schema. | +| **Message** | Free-form justification from the validator (`wl accept --message`) | + + + +The validator's stamp commits to the `stamps` table via a DoltHub PR (or direct push in admin mode). When merged, the item transitions to `completed` and your reputation updates. + +### The yearbook rule + +**You can't stamp your own work.** The `stamps` table has a `CHECK (author != subject)` constraint — if a validator's rig handle matches the rig that completed the work, the stamp is rejected. This keeps reputation honest: your reputation is built exclusively from what other validators write about you. + +### Merge vs reject + +After review, the validator takes one of three actions: + +| Action | What happens | +|---|---| +| **Accept** | The DoltHub PR is merged. A stamp row is created. The item moves to `completed`. Your reputation updates. | +| **Reject** | Evidence is cleared and the item goes back to `claimed`. You can resubmit with new evidence. No stamp is issued. | +| **Close** | The item moves to `completed` without a stamp. No reputation change. Used when work is no longer relevant. | + +## Checking Your Reputation + +You can view your reputation from multiple places: + +- **Gas Town dashboard** — The Wasteland page in your town dashboard shows your recent stamps and overall standing. +- **DoltHub directly** — Query the `stamps` table on the upstream commons where `subject = ''`. +- **`wl profile`** — Run `wl profile ` from the CLI to see any rig's public profile and stamp history. + +### Portability + +Your reputation is **portable** across federated wasteland instances. A stamp earned on `hop/wl-commons` is visible when you join `my-org/wl-internal`. Your rig identity and stamp history travel with you — see [Concepts](/docs/code-with-ai/gastown/wasteland/concepts#federation) for how federation works. + +## Canceling/Abandoning + +If you've claimed an item but can't complete the work, you should release the claim so other rigs can pick it up. + +### Releasing a claim + +Ask the Mayor to abandon the item: + +> *"Abandon w-870be07fbc"* + +The Mayor runs the equivalent of `wl unclaim `, which transitions the item back to `status = "open"` and clears `claimed_by`. Other rigs can then claim it. (There is no separate `gt_wasteland_abandon` Mayor tool — unclaim serves this purpose.) + +### Natural expiry + +Claims do **not** expire automatically. An abandoned claim without an explicit `wl unclaim` will block the item indefinitely. Always release claims you can't complete. + +{% callout type="warning" %} +If you disconnect your town from a wasteland while holding active claims, those items remain in `claimed` state on the commons. Other rigs can't pick them up until you reconnect and abandon them. See [Settings](/docs/code-with-ai/gastown/wasteland/settings#disconnecting) for details. +{% /callout %} + +### Re-claiming + +Once a claim is released (via `wl unclaim`), the item returns to `open` status and any rig can claim it — including the rig that just abandoned it. There's no cooldown or penalty for re-claiming. + +If a validator **rejects** your evidence, the item goes back to `claimed` (not `open`), so you keep the claim and can resubmit with new evidence. You don't need to re-claim. + +## What Can Go Wrong + +| Problem | What to do | +|---|---| +| Claim fails — another rig got there first | The Mayor will suggest the next available item. Try claiming a different one. | +| Evidence auto-submit fails | Check your DoltHub PAT is valid. The Mayor will retry. See the [Wasteland overview](/docs/code-with-ai/gastown/wasteland). | +| Stuck claim — you can't complete the work | [Abandon the claim](#cancelingabandoning) to release it back to the board. | +| Item stays in `in_review` forever | Validators may be backlogged. You can check the PR status on DoltHub directly. | +| `PRECONDITION_FAILED` on `wl done` | The item may not be in `claimed` state, or your credential may be invalid. See the [Wasteland overview](/docs/code-with-ai/gastown/wasteland). | +| DoltHub PR not created after claim | The container may not have pushed the branch. Check container status. See the [Wasteland overview](/docs/code-with-ai/gastown/wasteland). | +| Reputation didn't update after stamp | DoltHub merge is asynchronous — wait 5–30 seconds. If still missing, see the [Wasteland overview](/docs/code-with-ai/gastown/wasteland). | +| Can't claim — "rig not found" | Your rig registration PR may not be merged yet. Ask a validator to merge it. | diff --git a/packages/kilo-docs/pages/code-with-ai/platforms/cli-reference.md b/packages/kilo-docs/pages/code-with-ai/platforms/cli-reference.md index 7e58b5f01a1..5a713889fed 100644 --- a/packages/kilo-docs/pages/code-with-ai/platforms/cli-reference.md +++ b/packages/kilo-docs/pages/code-with-ai/platforms/cli-reference.md @@ -154,6 +154,7 @@ Options: --fork fork the session when continuing (use with --continue or --session) [boolean] --cloud-fork fetch session from cloud and continue locally (use with --session) [boolean] -p, --password basic auth password (defaults to KILO_SERVER_PASSWORD) [string] + -u, --username basic auth username (defaults to KILO_SERVER_USERNAME or 'kilo') [string] ``` ## kilo run @@ -165,25 +166,28 @@ Positionals: message message to send [string] [default: []] Options: - --help Show help [boolean] - --version Show version number [boolean] - --command the command to run, use message for args [string] - -c, --continue continue the last session [boolean] - -s, --session session id to continue [string] - --fork fork the session before continuing (requires --continue or --session) [boolean] - --share share the session [boolean] - -m, --model model to use in the format of provider/model [string] - --agent agent to use [string] - --format format: default (formatted) or json (raw JSON events) [string] [choices: "default", "json"] [default: "default"] - -f, --file file(s) to attach to message [array] - --title title for the session (uses truncated prompt if no value provided) [string] - --attach attach to a running opencode server (e.g., http://localhost:4096) [string] - -p, --password basic auth password (defaults to KILO_SERVER_PASSWORD) [string] - --dir directory to run in, path on remote server if attaching [string] - --port port for the local server (defaults to random port if no value provided) [number] - --variant model variant (provider-specific reasoning effort, e.g., high, max, minimal) [string] - --thinking show thinking blocks [boolean] [default: false] - --auto auto-approve all permissions (for autonomous/pipeline usage) [boolean] [default: false] + --help Show help [boolean] + --version Show version number [boolean] + --command the command to run, use message for args [string] + -c, --continue continue the last session [boolean] + -s, --session session id to continue [string] + --fork fork the session before continuing (requires --continue or --session) [boolean] + --cloud-fork fetch session from cloud and continue locally (use with --session) [boolean] + --share share the session [boolean] + -m, --model model to use in the format of provider/model [string] + --agent agent to use [string] + --format format: default (formatted) or json (raw JSON events) [string] [choices: "default", "json"] [default: "default"] + -f, --file file(s) to attach to message [array] + --title title for the session (uses truncated prompt if no value provided) [string] + --attach attach to a running kilo server (e.g., http://localhost:4096) [string] + -p, --password basic auth password (defaults to KILO_SERVER_PASSWORD) [string] + -u, --username basic auth username (defaults to KILO_SERVER_USERNAME or 'kilo') [string] + --dir directory to run in, path on remote server if attaching [string] + --port port for the local server (defaults to random port if no value provided) [number] + --variant model variant (provider-specific reasoning effort, e.g., high, max, minimal) [string] + --thinking show thinking blocks [boolean] [default: false] + --dangerously-skip-permissions auto-approve permissions that are not explicitly denied (dangerous!) [boolean] [default: false] + --auto auto-approve all permissions (for autonomous/pipeline usage) [boolean] [default: false] ``` ## kilo debug @@ -201,6 +205,7 @@ Commands: kilo debug snapshot snapshot debugging utilities kilo debug startup print startup timing kilo debug agent show agent configuration details + kilo debug info show debug information kilo debug paths show global paths (data, config, cache, state) kilo debug wait wait indefinitely (for debugging) @@ -502,6 +507,16 @@ Options: --params Tool params as JSON or a JS object literal [string] ``` +### kilo debug info + +``` +show debug information + +Options: + --help Show help [boolean] + --version Show version number [boolean] +``` + ### kilo debug paths ``` @@ -528,7 +543,7 @@ Options: manage AI providers and credentials Commands: - kilo auth list list providers [aliases: ls] + kilo auth list list providers and credentials [aliases: ls] kilo auth login [url] log in to a provider kilo auth logout log out from a configured provider @@ -540,7 +555,7 @@ Options: ### kilo auth list ``` -list providers +list providers and credentials Options: --help Show help [boolean] @@ -669,6 +684,25 @@ Options: --refresh refresh the models cache from models.dev [boolean] ``` +## kilo roll-call + +``` +batch-test text models matching a filter for connectivity and latency + +Positionals: + filter regex to filter models by provider/modelID (required) [string] + +Options: + --help Show help [boolean] + --version Show version number [boolean] + --prompt Prompt to send to each model [string] [default: "Hello"] + --timeout Timeout for each model call in milliseconds [number] [default: 25000] + --parallel Number of parallel model calls [number] [default: 5] + --verbose Show verbose output [boolean] [default: false] + --quiet Suppress progress and decoration [boolean] [default: false] + --output Output format (table, json, or md) [string] [choices: "table", "json", "md"] [default: "table"] +``` + ## kilo stats ``` @@ -774,6 +808,76 @@ Options: --version Show version number [boolean] ``` +## kilo daemon + +``` +manage the local kilo daemon + +Commands: + kilo daemon start start the local kilo daemon + kilo daemon status show local kilo daemon status + kilo daemon stop stop the local kilo daemon + kilo daemon restart restart the local kilo daemon + +Options: + --help Show help [boolean] + --version Show version number [boolean] +``` + +### kilo daemon start + +``` +start the local kilo daemon + +Options: + --help Show help [boolean] + --version Show version number [boolean] + --port port to listen on [number] [default: 0] + --hostname hostname to listen on [string] [default: "127.0.0.1"] + --mdns enable mDNS service discovery (defaults hostname to 0.0.0.0) [boolean] [default: false] + --mdns-domain custom domain name for mDNS service (default: kilo.local) [string] [default: "kilo.local"] + --cors additional domains to allow for CORS [array] [default: []] + --json print daemon details as JSON [boolean] +``` + +### kilo daemon status + +``` +show local kilo daemon status + +Options: + --help Show help [boolean] + --version Show version number [boolean] + --json print daemon details as JSON [boolean] +``` + +### kilo daemon stop + +``` +stop the local kilo daemon + +Options: + --help Show help [boolean] + --version Show version number [boolean] + --json print daemon details as JSON [boolean] +``` + +### kilo daemon restart + +``` +restart the local kilo daemon + +Options: + --help Show help [boolean] + --version Show version number [boolean] + --port port to listen on [number] [default: 0] + --hostname hostname to listen on [string] [default: "127.0.0.1"] + --mdns enable mDNS service discovery (defaults hostname to 0.0.0.0) [boolean] [default: false] + --mdns-domain custom domain name for mDNS service (default: kilo.local) [string] [default: "kilo.local"] + --cors additional domains to allow for CORS [array] [default: []] + --json print daemon details as JSON [boolean] +``` + ## kilo db ``` diff --git a/packages/kilo-docs/pages/code-with-ai/platforms/github.md b/packages/kilo-docs/pages/code-with-ai/platforms/github.md new file mode 100644 index 00000000000..d275c81d02c --- /dev/null +++ b/packages/kilo-docs/pages/code-with-ai/platforms/github.md @@ -0,0 +1,133 @@ +--- +title: "GitHub" +description: "Using Kilo Code in GitHub issues, pull requests, and review comments" +--- + +# Kilo for GitHub + +**Kilo for GitHub** lets you interact with Kilo directly from your GitHub issues, pull requests, and review comments. Mention the bot and it will: + +- Read the surrounding context +- Access the repository +- Respond with answers, code reviews, or full implementations + +--- + +## What You Can Do + +### Ask questions on review comments + +When you're reviewing a pull request and want a second opinion on a piece of code: + +``` +@kilocode-bot is this true? +``` + +The bot reads the review comment, the surrounding diff, and the relevant code in the repository to give you an informed answer. + +{% image src="/docs/img/connect/github/github-review.png" alt="Asking @kilocode-bot a question on a GitHub pull request review comment" width="800" /%} + +### Fix issues directly from GitHub + +Tag the bot on any issue and ask it to handle the fix: + +``` +@kilocode-bot please fix +``` + +The bot will: + +- Read the issue description +- Analyze the relevant code +- Create a branch with the implementation +- Open a pull request + +{% image src="/docs/img/connect/github/github-issue.png" alt="Asking @kilocode-bot to fix a GitHub issue" width="800" /%} + +### Diagnose bug reports + +When a bug report comes in and you want to understand what's going on before diving in: + +``` +@kilocode-bot what could be the cause of this issue? +``` + +The bot examines the bug report, searches the codebase for related code paths, and shares its analysis directly in the issue thread. + +{% image src="/docs/img/connect/github/github-bug.png" alt="Asking @kilocode-bot to diagnose a bug report on a GitHub issue" width="800" /%} + +--- + +## How It Works + +1. **Mention `@kilocode-bot`** on an issue, pull request, or review comment +2. **Kilo reads the context** — the issue description, PR diff, review thread, and connected repository code +3. **A Cloud Agent spins up** to process the request +4. **Kilo responds** with an answer, analysis, or a pull request containing the implementation + +For implementation requests, the bot creates a new branch, commits the changes, and opens a PR back to the repository. You'll see the PR linked directly in the thread. + +--- + +## Prerequisites + +- A Kilo Code account with available credits +- Your GitHub integration configured via the Integrations tab at [app.kilo.ai](https://app.kilo.ai) +- The Kilo Code Bot GitHub App installed on the relevant repositories + +--- + +## Setup + +1. Go to [app.kilo.ai](https://app.kilo.ai) and navigate to the **Integrations** tab +2. Connect your GitHub account through the **KiloConnect** GitHub App if you haven't already +3. Install the **Kilo Code Bot** GitHub App on your repositories at [github.com/apps/kilo-code-bot](https://github.com/apps/kilo-code-bot/) + +Once installed, `@kilocode-bot` is available on any issue or PR in the repositories you've authorized. + +--- + +## Use Cases + +### Code Review Assistance + +During a PR review, mention the bot on any comment or line of code to get a quick analysis. The bot can: + +- Verify logic +- Suggest improvements +- Explain unfamiliar patterns + +### Issue Triage and Fixes + +When new issues come in, the bot can diagnose probable causes and, if you ask, implement a fix directly. This works best for straightforward bugs where the issue description gives enough context to act. + +### Bulk or Cross-Repo Changes + +Need to bump a dependency or apply a config change across your org? Mention the repos in a single comment and the bot handles each one. + +--- + +## Cost + +Kilo Code credits are consumed the same way as any other Kilo interface. Credit usage depends on the model selected and the complexity of the task. + +--- + +## Tips for Best Results + +- **Be specific about what you want.** "Please fix" works best on well-described issues. For vague issues, ask the bot to diagnose first. +- **Mention specific repositories** if the fix spans multiple repos. +- **Use on review comments** for quick, contextual questions during code review. + +--- + +## Troubleshooting + +**The bot isn't responding to mentions.** +Make sure the Kilo Code Bot GitHub App is installed on the repository and that you're mentioning `@kilocode-bot` (not `@kilo`). + +**The bot can't access the repository.** +Verify that both the KiloConnect App and the Kilo Code Bot App are installed and have access to the repository in question. + +**The implementation doesn't match what I expected.** +Try providing more context in the issue description or comment. The bot works best when the problem and desired outcome are clearly described. diff --git a/packages/kilo-docs/pages/code-with-ai/platforms/kilo-connect.md b/packages/kilo-docs/pages/code-with-ai/platforms/kilo-connect.md new file mode 100644 index 00000000000..afb8928db5b --- /dev/null +++ b/packages/kilo-docs/pages/code-with-ai/platforms/kilo-connect.md @@ -0,0 +1,28 @@ +--- +title: "Kilo Connect" +description: "Use Kilo Code directly from Slack, GitHub, and Linear" +--- + +# Kilo Connect + +**Kilo Connect** brings Kilo Code into the tools your team already uses. Instead of switching to a separate interface, you can trigger implementations, ask questions, and get pull requests opened directly from your chat, issue tracker, or code review workflow. + +--- + +## Supported Integrations + +| Integration | Trigger | What It Can Do | +|---|---|---| +| [Slack](/docs/code-with-ai/platforms/slack) | `@Kilo` in any channel or DM | Ask questions, implement fixes, debug issues | +| [GitHub](/docs/code-with-ai/platforms/github) | `@kilocode-bot` on issues and PRs | Fix issues, review code, cross-repo changes | +| [Linear](/docs/code-with-ai/platforms/linear) | `@kilo` on any issue | Implement fixes, investigate bugs, cross-repo changes | + +--- + +## How to Set Up + +All integrations are configured from the **Integrations** tab at [app.kilo.ai](https://app.kilo.ai). Each integration requires: + +- A Kilo Code account with available credits +- A connected Git provider (GitHub or GitLab) so Kilo can access your repositories +- The specific integration installed and authorized for your workspace diff --git a/packages/kilo-docs/pages/code-with-ai/platforms/linear.md b/packages/kilo-docs/pages/code-with-ai/platforms/linear.md new file mode 100644 index 00000000000..52d3b15a2ed --- /dev/null +++ b/packages/kilo-docs/pages/code-with-ai/platforms/linear.md @@ -0,0 +1,125 @@ +--- +title: "Linear" +description: "Using Kilo Code in Linear" +--- + +# Kilo for Linear + +**Kilo for Linear** connects Kilo Code to your Linear workspace, so you can trigger implementations, debug issues, and investigate bugs directly from your project management tool. Mention `@kilo` on any issue and the bot gets to work. + +--- + +## What You Can Do + +### Fix issues from Linear + +Tag the bot on any Linear issue and tell it to implement the fix: + +``` +@kilo please fix +``` + +The bot will: + +- Read the issue title, description, and comments +- Spin up a Cloud Agent to implement the solution +- Show a thinking/processing animation in Linear while it works +- Link the resulting pull request back to the issue + +{% image src="/docs/img/connect/linear/linear-fix-issue.png" alt="Asking @kilo to fix an issue in Linear" width="800" /%} + +### Apply changes across multiple repositories + +If a fix or upgrade needs to land in several repos at once: + +``` +@kilo please fix this in the cloud, landing, and handbook repos +``` + +The bot handles each repository independently, creating separate branches and pull requests for each. + +{% image src="/docs/img/connect/linear/linear-multi-repo.png" alt="Asking @kilo to apply changes across multiple repositories from Linear" width="800" /%} + +### Get help understanding an issue + +Before jumping into a fix, ask the bot to analyze the problem: + +``` +@kilo what could be the cause of this issue? +``` + +The bot examines the issue context and searches the connected codebase to surface likely causes. + +{% image src="/docs/img/connect/linear/linear-understand-issue.png" alt="Asking @kilo to analyze the cause of a Linear issue" width="800" /%} + +--- + +## How It Works + +1. **Mention `@kilo`** in a comment on a Linear issue +2. **Kilo reads the issue context** — title, description, labels, and comment thread +3. **A Cloud Agent spins up** to process the request (you'll see a thinking animation in Linear while this happens) +4. **Kilo responds** with an answer or opens a pull request with the implementation + +When Kilo creates a pull request, it links back to the Linear issue so everything stays connected. + +--- + +## Prerequisites + +- A Kilo Code account with available credits +- Your GitHub or GitLab integration configured via the Integrations tab at [app.kilo.ai](https://app.kilo.ai) +- Access to a Linear workspace where you can install integrations + +--- + +## Setup + +1. Go to [app.kilo.ai](https://app.kilo.ai) and navigate to the **Integrations** tab +2. Set up the **Linear** integration +3. Authorize Kilo to access your Linear workspace + +Once connected, `@kilo` is available as a mention in any issue across your Linear workspace. + +--- + +## Use Cases + +### Sprint Cleanup + +Have a backlog of well-described bugs? Tag the bot on each one and let it work through them while you focus on higher-priority work. + +### Cross-Repo Upgrades + +Need to upgrade a framework version or apply a config change across several services? Create a single Linear issue describing the change, then tell the bot which repos to update. + +### Issue Investigation + +When a bug report is unclear or the root cause isn't obvious, ask the bot to analyze the issue before committing to a fix. It can search the codebase and surface likely problem areas. + +--- + +## Cost + +Kilo Code credits are consumed the same way as any other Kilo interface. Credit usage depends on the model selected and the complexity of the task. + +--- + +## Tips for Best Results + +- **Write clear issue descriptions.** The bot works best when the issue title and description give enough context to understand the problem and the expected outcome. +- **Mention specific repositories** if the change needs to land in more than one. +- **Start with diagnosis for ambiguous issues.** Ask the bot to analyze the issue before asking it to fix. + +--- + +## Troubleshooting + +**The bot isn't responding to mentions.** +Confirm that the Linear integration is set up in the Integrations tab at [app.kilo.ai](https://app.kilo.ai) and that the bot has access to your workspace. + +**The bot can't access the repository.** +Make sure your GitHub or GitLab integration is configured and that the relevant repositories are authorized. + +**The pull request doesn't match what I expected.** +Provide more detail in the issue description, or comment with additional context before asking the bot to fix. diff --git a/packages/kilo-docs/pages/code-with-ai/platforms/mobile.md b/packages/kilo-docs/pages/code-with-ai/platforms/mobile.md index 76abc8decb2..ec23078ad6e 100644 --- a/packages/kilo-docs/pages/code-with-ai/platforms/mobile.md +++ b/packages/kilo-docs/pages/code-with-ai/platforms/mobile.md @@ -5,20 +5,44 @@ description: "Using Kilo Code on iOS and Android" # Mobile Apps -Kilo Code is coming to mobile! Soon you'll be able to use Kilo Code's powerful AI coding capabilities directly from your iOS or Android device. +Use Kilo Code from your phone to keep coding sessions moving while you are away from your desk. The mobile app connects to Cloud Agents, KiloClaw, and remote sessions from your local CLI or editor extensions. -{% callout type="info" title="Coming Soon" %} -Mobile apps for iOS and Android are currently in development. Sign up to be notified when they launch! +{% callout type="info" title="Android app available now" %} +Install Kilo Code for Android from [Google Play](https://kilocode.onelink.me/ZzZZ/r10ni4zy). {% /callout %} -## iOS App +## What you can do -The Kilo Code iOS app will bring AI-powered coding assistance to your iPhone and iPad. +The mobile app lets you: -[Learn more about the iOS app →](https://kilo.ai/features/ios-app) +- View and manage Kilo Code sessions, including remote CLI and extension sessions running on your local machine. +- Chat with KiloClaw from your phone. +- Spawn Cloud Agents and code directly from the app. +- Monitor and view all non-remote sessions in one place. +- Create, onboard, and manage KiloClaw instances. + +{% imageGallery columns="3" width="220px" %} +{% image src="/docs/img/mobile-apps/home.webp" alt="Kilo Code mobile home screen showing KiloClaw and active agent sessions" caption="Start coding tasks, open KiloClaw, and resume active sessions from the mobile home screen." /%} + +{% image src="/docs/img/mobile-apps/new-session.webp" alt="Kilo Code mobile new session screen with coding mode selector" caption="Create a new Cloud Agent session and choose the right mode for the task." /%} + +{% image src="/docs/img/mobile-apps/session-chat.webp" alt="Kilo Code mobile session chat with an active coding task" caption="Review progress and continue coding conversations from the mobile app." /%} +{% /imageGallery %} + +{% imageGallery columns="2" width="220px" %} +{% image src="/docs/img/mobile-apps/kiloclaw-chat.webp" alt="KiloClaw chat in the Kilo Code mobile app" caption="Chat with KiloClaw on mobile." /%} + +{% image src="/docs/img/mobile-apps/session-filters.webp" alt="Kilo Code mobile session filter panel for Cloud Extension CLI Slack and other platforms" caption="Filter sessions by platform and project, including Cloud, Extension, CLI, Slack, and other sessions." /%} +{% /imageGallery %} ## Android App -The Kilo Code Android app will let you code with AI assistance on your Android phone or tablet. +The Android app is available now on Google Play. -[Learn more about the Android app →](https://kilo.ai/features/android-app) +[Install the Android app →](https://kilocode.onelink.me/ZzZZ/r10ni4zy) + +## iOS App + +The iOS app is in review with the App Store team and will be available soon. You can already sign up for the iOS waitlist to be notified when it launches. + +[Join the iOS app waitlist →](https://kilo.ai/features/ios-app) diff --git a/packages/kilo-docs/pages/code-with-ai/platforms/slack.md b/packages/kilo-docs/pages/code-with-ai/platforms/slack.md index 401b7751202..20691d8c381 100644 --- a/packages/kilo-docs/pages/code-with-ai/platforms/slack.md +++ b/packages/kilo-docs/pages/code-with-ai/platforms/slack.md @@ -5,50 +5,79 @@ description: "Using Kilo Code in Slack" # Kilo for Slack -Kilo for Slack brings the power of Kilo Code directly into your Slack workspace. Ask questions about your repositories, request code implementations, or get help with issues—all without leaving Slack. +**Kilo for Slack** brings Kilo Code directly into your Slack workspace. Ask questions about your repositories, request code implementations, debug issues, and collaborate with your team — all without leaving Slack. + +When you mention `@Kilo` in a thread, the bot: + +- Reads the full conversation +- Accesses your connected repositories +- Answers your question or spins up a Cloud Agent to implement the change +- Creates branches, pushes commits, and opens pull requests on your behalf --- -## What You Can Do With Kilo for Slack +## What You Can Do -- **Ask questions about your repositories** — Get explanations about code, architecture, or implementation details -- **Request code implementations** — Tell the bot to implement fixes or features suggested in Slack threads -- **Get help with debugging** — Share error messages or issues and get AI-powered assistance -- **Collaborate with your team** — Mention the bot in any channel to get help in context +### Ask questions about your codebase + +``` +@Kilo what does the UserService class do in our main backend repo? +``` + +``` +@Kilo how is error handling implemented in the payment processing module? +``` + +{% image src="/docs/img/connect/slack/slackbot-ask-questions.webp" alt="Asking Kilo a question about the codebase in Slack" width="800" /%} + +### Implement fixes and features from Slack discussions + +When your team identifies a bug or improvement in a thread, ask the bot to handle it: + +``` +@Kilo based on this thread, can you implement the fix for the null pointer exception in the order processing service? +``` + +The bot will: + +- Read the thread context +- Understand the proposed solution +- Create a branch with the implementation +- Push a pull request to your repository + +{% image src="/docs/img/connect/slack/slackbot-turn-discussions-into-PRs.webp" alt="Kilo turning a Slack thread discussion into a pull request" width="800" /%} + +### Implement changes across multiple repositories + +If the same change needs to land in several repos, just tell the bot: + +``` +@Kilo please fix this in the cloud, landing, and handbook repos +``` + +{% image src="/docs/img/connect/slack/slackbot-coding.webp" alt="Kilo implementing changes across multiple repositories from Slack" width="800" /%} + +### Debug issues + +Paste an error message or stack trace and ask for help: + +``` +@Kilo I'm seeing this error in production: +[paste error message] +Can you help me understand what's causing it? +``` + +{% image src="/docs/img/connect/slack/slackbot-bugs.webp" alt="Kilo helping debug a production error in Slack" width="800" /%} --- -## Supported Platforms - -| Platform | Integration Type | Details | -|---|---|---| -| GitHub | GitHub App | [GitHub Setup Guide](/docs/automate/integrations#connecting-github) | -| GitLab | OAuth or PAT | [GitLab Setup Guide](/docs/automate/integrations#connecting-gitlab) | - ---- - -## Prerequisites - -Before using Kilo for Slack: - -- You must have a **Kilo Code account** with available credits -- Your **Git provider integration must be configured** via the [Integrations tab](https://app.kilo.ai/integrations) so Kilo can access your repositories - -To install Kilo for Slack, simply go to the integrations menu in the sidebar on https://app.kilo.ai and set up the Slack integration. - ---- - -## How to Interact with Kilo +## How to Interact ### Direct Messages -You can message Kilo directly through Slack DMs for private conversations: +You can DM Kilo directly for private conversations. Find Kilo in your workspace's app list and start a direct message. -1. Find **Kilo** in your Slack workspace's app list -2. Start a direct message conversation -3. Ask your question or describe what you need - -This is ideal for: +Good for: - Private questions about your code - Sensitive debugging sessions @@ -62,114 +91,80 @@ Mention the bot in any channel where it's been added: @Kilo can you explain how the authentication flow works in our backend? ``` -This is great for: +Good for: - Team discussions where AI assistance would help -- Collaborative debugging sessions +- Collaborative debugging - Getting quick answers during code reviews --- -## Use Cases +## Prerequisites -### Ask Questions About Your Repositories +Before using Kilo for Slack: -Get instant answers about your codebase without switching contexts: +- You need a Kilo Code account with available credits +- Your Git provider integration (GitHub or GitLab) must be configured via the Integrations tab at [app.kilo.ai](https://app.kilo.ai) so Kilo can access your repositories -``` -@Kilo what does the UserService class do in our main backend repo? -``` +--- -``` -@Kilo how is error handling implemented in the payment processing module? -``` +## Setup -### Implement Fixes from Slack Discussions +To install Kilo for Slack, go to the Integrations menu in the sidebar at [app.kilo.ai](https://app.kilo.ai) and set up the Slack integration. -When your team identifies an issue or improvement in a Slack thread, ask the bot to implement it: - -``` -@Kilo based on this thread, can you implement the fix for the null pointer exception in the order processing service? -``` - -The bot can: - -- Read the context from the thread -- Understand the proposed solution -- Create a branch with the implementation -- Push the changes to your repository - -### Debug Issues - -Share error messages or stack traces and get help: - -``` -@Kilo I'm seeing this error in production: -[paste error message] -Can you help me understand what's causing it? -``` +| Platform | Integration Type | Details | +|---|---|---| +| GitHub | GitHub App | [GitHub Setup Guide](/docs/automate/integrations#connecting-github) | +| GitLab | OAuth or PAT | [GitLab Setup Guide](/docs/automate/integrations#connecting-gitlab) | --- ## How It Works -1. **Message Kilo** — Either through DMs or by mentioning it in a channel -2. **Kilo processes your request** — Kilo uses your connected repositories to understand context -3. **AI generates a response** — Kilo Code's AI analyzes your request and provides helpful responses -4. **Code changes (if requested)** — For implementation requests, Kilo can create pull or merge requests - ---- - -## Cost - -- **Kilo Code credits are used** when Kilo performs work (model usage, operations, etc.) -- Credit usage is similar to using Kilo Code through other interfaces - ---- - -## Tips for Best Results - -- **Be specific** — The more context you provide, the better the response -- **Reference specific files or functions** — Help the bot understand exactly what you're asking about -- **Use threads** — Keep related conversations in threads for better context -- **Specify the repository** — If you have multiple repos connected, mention which one you're asking about - ---- - -## Limitations - -- Kilo can only access repositories you've connected through the [Integrations](https://app.kilo.ai/integrations) page -- Complex multi-step implementations may require follow-up messages -- Response times may vary based on the complexity of your request +1. **Message Kilo** through a DM or channel mention +2. **Kilo processes your request** using your connected repositories for context +3. **AI generates a response** analyzing your request and providing an answer +4. **Code changes (if requested)** — Kilo creates a branch, commits the changes, and opens a pull or merge request --- ## Changing the Model -You can customize which AI model Kilo uses for generating responses. The model affects the quality, speed, and capabilities of Kilo's responses. +You can customize which AI model Kilo uses for generating responses. -1. Go to your [Kilo Workspace](https://app.kilo.ai/) -2. Navigate to **Integrations** > **Slack** -3. Select your preferred model for Kilo for Slack +1. Go to your Kilo Workspace at [app.kilo.ai](https://app.kilo.ai) +2. Navigate to **Integrations > Slack** +3. Select your preferred model -Kilo will start using the new model immediately for subsequent requests. +Kilo for Slack supports 400+ models across different providers. The new model applies immediately to subsequent requests. -### Available Models +--- -Kilo for Slack supports over 400+ models across different providers. +## Cost + +Kilo Code credits are used when Kilo performs work (model usage, operations, etc.). Credit usage is the same as using Kilo through any other interface. + +--- + +## Tips for Best Results + +- **Be specific.** The more context you provide, the better the response. +- **Reference specific files or functions.** Help the bot understand exactly what you're asking about. +- **Use threads.** Keep related conversations in threads for better context. +- **Specify the repository.** If you have multiple repos connected, mention which one you're asking about. --- ## Troubleshooting -**"Kilo isn't responding."** -Ensure Kilo for Slack is installed in your workspace and has been added to the channel you're using. +**Kilo isn't responding.** +Make sure Kilo for Slack is installed in your workspace and has been added to the channel you're using. -**"Kilo can't access my repository."** -Verify your Git provider integration is configured correctly in the [Integrations tab](https://app.kilo.ai/integrations). +**Kilo can't access my repository.** +Verify your Git provider integration is configured correctly in the Integrations tab. -**"I'm getting incomplete responses."** +**I'm getting incomplete responses.** Try breaking your request into smaller, more specific questions. -**"Kilo doesn't understand my codebase."** -Make sure the repository you're asking about is connected and accessible through your Git provider integration. +**Kilo doesn't understand my codebase.** +Confirm that the repository you're asking about is connected and accessible through your Git provider integration. 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-docs/pages/collaborate/teams/billing.md b/packages/kilo-docs/pages/collaborate/teams/billing.md index 545c8887818..c8b89f50c98 100644 --- a/packages/kilo-docs/pages/collaborate/teams/billing.md +++ b/packages/kilo-docs/pages/collaborate/teams/billing.md @@ -68,7 +68,7 @@ To fill empty seats or remove members ahead of seat deletion, see the [team mana ## Automatic Top-Up -Ensure your team has uninterrupted access to Kilo Code by enabling Automatic Top-Up. This feature keeps your organization's balance funded so you never have to worry about manual recharges. +Ensure your team has uninterrupted access to Kilo Code by enabling [Automatic Top-Up](https://kilo.ai/features/auto-top-ups). This feature keeps your organization's balance funded so you never have to worry about manual recharges. Auto top-up is also available for individual accounts — see [Adding Credits](/docs/getting-started/adding-credits) for details. ### How It Works diff --git a/packages/kilo-docs/pages/contributing/development-environment.md b/packages/kilo-docs/pages/contributing/development-environment.md index 477b4a5e4ee..79bbc09c8f8 100644 --- a/packages/kilo-docs/pages/contributing/development-environment.md +++ b/packages/kilo-docs/pages/contributing/development-environment.md @@ -16,8 +16,9 @@ This document will help you set up your development environment and understand h Before you begin, make sure you have the following installed: 1. **Git** - For version control -2. **Node.js** (version v20.18.1 (See `.nvmrc` for latest) or higher recommended) and npm +2. **Bun 1.3.13+** - Required for installing dependencies and running scripts 3. **Visual Studio Code** - Our recommended IDE for development +4. **Java 21** - Required only when running JetBrains plugin checks or repo-level checks that include `@kilocode/kilo-jetbrains` ## Getting Started @@ -37,10 +38,10 @@ Before you begin, make sure you have the following installed: 1. **Install dependencies**: ```bash - pnpm install + bun install ``` - This command will install dependencies for the main extension, webview UI, and e2e tests. + This command will install dependencies for all workspace packages. 1. **Install VSCode Extensions**: - **Required**: [ESBuild Problem Matchers](https://marketplace.visualstudio.com/items?itemName=connor4312.esbuild-problem-matchers) - Helps display build errors correctly. @@ -52,136 +53,164 @@ While not strictly necessary for running the extension, these extensions are rec The full list of recommended extensions is in `.vscode/extensions.json` +### Using AI and Coding Agents + +AI and coding agents are allowed in this repo. If you use one, start it from the repository root so the root `AGENTS.md` is available, then check package-specific guidance when your change touches a package with its own `AGENTS.md` or contributor docs. + +You remain responsible for the submitted work. Before opening a PR, personally review the diff, test the change, make sure you can explain it, and understand how it interacts with the affected package and the rest of the repo. Do not use agents to submit batches of agent-generated, untested, or weakly reviewed PRs. Keep concurrent PRs limited, generally no more than three at a time, and prioritize high-impact issues first. Do not use automation or agents to mass-create issues without human review and prioritization. + +Kilo has bug bounties. To be eligible, make sure your GitHub account is connected in your Kilo account. + ### Project Structure -The project is organized into several key directories: +The project is organized into several key packages: -- **`src/`** - Core extension code - - **`core/`** - Core functionality and tools - - **`services/`** - Service implementations -- **`webview-ui/`** - Frontend UI code -- **`e2e/`** - End-to-end tests -- **`scripts/`** - Utility scripts -- **`assets/`** - Static assets like images and icons +- **`packages/opencode/`** - Kilo CLI, agent runtime, local HTTP server, session management, and TUI +- **`packages/kilo-vscode/`** - VS Code extension, webview UI, Agent Manager, and extension packaging +- **`packages/sdk/js/`** - Generated TypeScript SDK for the local server API +- **`packages/kilo-docs/`** - Documentation site +- **`packages/kilo-jetbrains/`** - JetBrains plugin ## Development Workflow -### Building the Extension +### Running the CLI -To build the extension: +To run the CLI from the repo root: ```bash -pnpm build +bun dev ``` -This will: +`bun dev` and `bun run dev` are equivalent. Both run the local source in `packages/opencode/`; they do not use a globally installed `kilo` binary. -1. Build the webview UI -2. Compile TypeScript -3. Bundle the extension -4. Create a `.vsix` file in the `bin/` directory +### Backend/API Validation + +For backend and API validation, use the root [TESTING.md](https://github.com/Kilo-Org/kilocode/blob/main/TESTING.md) guide. It covers starting the local backend with: + +```bash +bun dev serve +``` + +and validating behavior with `curl` requests against the local server. + +If you change server endpoints in `packages/opencode/src/server/`, regenerate the SDK from the repo root: + +```bash +./script/generate.ts +``` ### Running the Extension To run the extension in development mode: -1. Press `F5` (or select **Run** → **Start Debugging**) in VSCode -2. This will open a new VSCode window with Kilo Code loaded - -### Hot Reloading - -- **Webview UI changes**: Changes to the webview UI will appear immediately without restarting -- **Core extension changes**: Changes to the core extension code will automatically reload the ext host - -In development mode (NODE_ENV="development"), changing the core code will trigger a `workbench.action.reloadWindow` command, so it is no longer necessary to manually start/stop the debugger and tasks. - -> **Important**: In production builds, when making changes to the core extension, you need to: -> -> 1. Stop the debugging process -> 2. Kill any npm tasks running in the background (see screenshot below) -> 3. Start debugging again - -{% image src="https://github.com/user-attachments/assets/466fb76e-664d-4066-a3f2-0df4d57dd9a4" alt="Stopping background tasks" width="600" /%} - -### Installing the Built Extension - -To install your built extension: - ```bash -code --install-extension "$(ls -1v bin/kilo-code-*.vsix | tail -n1)" +bun run extension ``` -Replace `[version]` with the current version number. +This will build and launch the extension in an isolated VS Code instance. + +### Building the Extension + +From `packages/kilo-vscode/`: + +```bash +bun run compile +bun run package +``` + +Use `bun run compile` when you need a development build and `bun run package` when you need a production extension bundle. ## Testing Kilo Code uses several types of tests to ensure quality: -### Unit Tests +### Repo-Level Checks -Run unit tests with: +From the repo root: ```bash -npm test +bun install +bun run lint +bun run typecheck ``` -This runs both extension and webview tests. +`bun run typecheck` wraps `bun turbo typecheck`. Use `bun turbo typecheck --force` if you need to bypass the Turbo cache. -To run specific test suites: +Do **not** run `bun test` from the repo root. The root test script intentionally exits with failure to prevent accidentally running tests from the wrong package. + +### CLI Checks + +From `packages/opencode/`: ```bash -npm run test:extension # Run only extension tests -npm run test:webview # Run only webview tests +bun run typecheck +bun test +bun test ./path/to/file.test.ts ``` -### End-to-End Tests +Use the root [TESTING.md](https://github.com/Kilo-Org/kilocode/blob/main/TESTING.md) guide for backend/API checks that require `bun dev serve` and `curl`-based requests. -E2E tests verify the extension works correctly within VSCode: +### VS Code Extension Checks -1. Create a `.env.local` file in the root with required API keys: - - ``` - OPENROUTER_API_KEY=sk-or-v1-... - ``` - -2. Run the integration tests: - ```bash - npm run test:integration - ``` - -For more details on E2E tests, see e2e/VSCODE_INTEGRATION_TESTS - -## Linting and Type Checking - -Ensure your code meets our quality standards: +From `packages/kilo-vscode/`: ```bash -npm run lint # Run ESLint -npm run check-types # Run TypeScript type checking +bun run typecheck +bun run lint +bun run test:unit +bun run test +bun run compile +bun run package ``` +### Documentation Checks + +From the repo root: + +```bash +bun run --filter @kilocode/kilo-docs test +bun run --filter @kilocode/kilo-docs build +bun run --filter @kilocode/kilo-docs dev +``` + +For manual documentation validation, run the docs site locally, preview the affected page, and check the changed links and rendered content. + +### Testing Evidence for Pull Requests + +Every PR marked ready for review must include testing evidence. A bare `Not tested` or `N/A` answer is not sufficient. + +Choose checks that match the files touched. Docs-only, config-only, and similar changes may satisfy this rule with concrete manual verification or a relevant command check. + +For CLI and extension changes, useful evidence can include: + +- The relevant command checks from the package you changed +- Manual/local verification of the changed behavior in the CLI or extension +- Screenshots or videos for visual changes, such as a settings page update or changed CLI/extension behavior + +For docs changes, useful evidence can include: + +- `bun run script/check-md-table-padding.ts --fix` +- `bun run --filter @kilocode/kilo-docs test` +- Previewing the changed docs page locally, as described in [Documentation Contributions](/docs/contributing#documentation-contributions) + +If you cannot complete a relevant command, include all of the following in the PR: + +- The command you attempted or would normally run +- The blocker or failure that prevented completion +- The substitute verification you performed instead + +Agent limitations, local resource constraints, OOM constraints, or an agent prompt that says to skip tests do not waive this requirement. Draft PRs may be incomplete until they are marked ready for review. Maintainers may still defer or close review at their discretion. + +## Guardrails + +- User-facing changes usually need a changeset. Run `bunx changeset add` or add a file under `.changeset/`. +- After changing server endpoints, run `./script/generate.ts` from the repo root to regenerate `packages/sdk/js/`. +- After adding or changing guarded URLs in `packages/kilo-vscode/`, `packages/kilo-vscode/webview-ui/`, or `packages/opencode/src/`, run `bun run script/extract-source-links.ts` from the repo root. +- When editing shared `packages/opencode/` files, keep changes small and mark Kilo-only edits with `// kilocode_change` for a single line or `// kilocode_change start` / `// kilocode_change end` for a block. Do not add these markers inside `kilocode`-named paths. + ## Git Hooks -This project uses [Husky](https://typicode.github.io/husky/) to manage Git hooks, which automate certain checks before commits and pushes. The hooks are located in the `.husky/` directory. - -### Pre-commit Hook - -Before a commit is finalized, the `.husky/pre-commit` hook runs: - -1. **Branch Check**: Prevents committing directly to the `main` branch. -2. **Type Generation**: Runs `npm run generate-types`. -3. **Type File Check**: Ensures that any changes made to `src/exports/roo-code.d.ts` by the type generation are staged. -4. **Linting**: Runs `lint-staged` to lint and format staged files. - -### Pre-push Hook - -Before changes are pushed to the remote repository, the `.husky/pre-push` hook runs: - -1. **Branch Check**: Prevents pushing directly to the `main` branch. -2. **Compilation**: Runs `npm run compile` to ensure the project builds successfully. -3. **Changeset Check**: Checks if a changeset file exists in `.changeset/` and reminds you to create one using `npm run changeset` if necessary. - -These hooks help maintain code quality and consistency. If you encounter issues with commits or pushes, check the output from these hooks for error messages. +This project uses [Husky](https://typicode.github.io/husky/) to manage Git hooks. The current pre-push hook checks the Bun version against root `package.json` and runs the repo-level typecheck. ## Troubleshooting @@ -189,7 +218,8 @@ These hooks help maintain code quality and consistency. If you encounter issues 1. **Extension not loading**: Check the VSCode Developer Tools (Help > Toggle Developer Tools) for errors 2. **Webview not updating**: Try reloading the window (Developer: Reload Window) -3. **Build errors**: Make sure all dependencies are installed with `npm run install:all` +3. **Build errors**: Make sure all dependencies are installed with `bun install` +4. **Root tests fail immediately**: This is expected. Run package-level tests instead of root `bun test` ### Debugging Tips diff --git a/packages/kilo-docs/pages/contributing/index.md b/packages/kilo-docs/pages/contributing/index.md index a3f17423b64..c8d3c50f174 100644 --- a/packages/kilo-docs/pages/contributing/index.md +++ b/packages/kilo-docs/pages/contributing/index.md @@ -52,6 +52,14 @@ git checkout -b docs/your-change-description - Include appropriate tests for new features - Update documentation for any user-facing changes +### Contribution Ownership and AI Assistance + +AI and coding agents are welcome in Kilo contributions. Contributors still own the work they submit: you must personally understand the change, test it appropriately, be able to explain the diff, and understand how it interacts with the affected package and the rest of the repo. + +When using an agent, start it from the repository root so the root `AGENTS.md` is available. If you work in a package with its own guidance, check and follow the package-specific `AGENTS.md` or contributor docs too. + +Maintainers may close PRs that appear to be submitted without credible contributor ownership or understanding, including AI-assisted work that has not been meaningfully reviewed by the contributor. + ### Commit Guidelines - Write clear, concise commit messages @@ -85,16 +93,32 @@ Guidelines: Skip the changeset only for internal refactors, CI tweaks, test-only changes, or docs that do not affect users. +### Other Guardrails + +- Regenerate `packages/sdk/js/` with `./script/generate.ts` after changing server endpoints. +- Run `bun run script/extract-source-links.ts` after adding or changing guarded URLs in `packages/kilo-vscode/`, `packages/kilo-vscode/webview-ui/`, or `packages/opencode/src/`. +- When editing shared `packages/opencode/` files, keep Kilo changes small and mark Kilo-only edits with `// kilocode_change` for a single line or `// kilocode_change start` / `// kilocode_change end` for a block. Do not add these markers inside `kilocode`-named paths. + ### Testing Your Changes -- Run the test suite: - ```bash - npm test - ``` -- Manually test your changes in the development extension +Use the current Bun commands in the [Development Environment](/docs/contributing/development-environment) guide for repo-level, CLI, backend/API, VS Code extension, and docs checks. + +Key reminders: + +- Do **not** run `bun test` from the repo root. The root test script intentionally exits with failure so tests run from the package that owns them. +- Use the root [TESTING.md](https://github.com/Kilo-Org/kilocode/blob/main/TESTING.md) guide for backend/API validation with `bun dev serve` and `curl`-based requests. +- Regenerate the SDK with `./script/generate.ts` from the repo root after changing server endpoints. +- Manually verify extension behavior with `bun run extension`. +- For manual documentation validation, preview the affected page and check changed links and rendered content. + +Before marking a PR ready for review, include testing evidence in the PR template. See [Testing Evidence for Pull Requests](/docs/contributing/development-environment#testing-evidence-for-pull-requests) for the full standard, including docs/config-only verification and blocked command fallback requirements. ### Creating a Pull Request +Contributor guidance exists to protect maintainer review time and keep reviews focused on work that is ready to evaluate. + +Follow the issue-first policy by linking the relevant issue when you open a PR. Use `Fixes #123`, `Closes #123`, or equivalent linked issue wording so reviewers can see the problem statement, discussion, and intended scope before reviewing the code change. + 1. Push your changes to your fork: ```bash @@ -108,10 +132,34 @@ Skip the changeset only for internal refactors, CI tweaks, test-only changes, or 4. Select your fork and branch 5. Fill out the PR template with: - - A clear description of the changes - - Any related issues - - Testing steps - - Screenshots (if applicable) + - Related issue link, or an explanation for why there is no existing issue + - What problem is being solved and why the change is needed + - Important implementation choices or tradeoffs reviewers cannot infer from the diff + - Testing evidence, including commands run and results + - Manual/local verification performed + - Any command blocker plus substitute verification + - Screenshots or video for visual UI changes, showing the relevant before/after or resulting state + - Confirmation that you personally reviewed the diff and can explain the changes, including any AI-assisted work + +Keep the description focused on context reviewers cannot infer from the diff. Skip file-by-file summaries, placeholders, and other filler. + +Maintainers may close or decline review of PRs presented as review-ready at their discretion when they lack linked issue context, a clear what/why explanation, credible testing evidence, credible contributor ownership of AI-assisted work, or relevant UI proof for visual UI changes. + +When a PR is close to this bar, addresses important work, or would benefit from further shaping, maintainers may ask for specific fixes instead of closing or declining review. Contributors may reopen or resubmit once the PR meets the documented bar. + +## Tracker Use and Automation + +Please keep the issue and PR trackers useful for maintainers and contributors. Do not submit batches of agent-generated, untested, or weakly reviewed PRs. + +Keep concurrent PRs focused and limited. As a rule, open no more than three PRs at a time, especially if you are a new contributor. Prioritize high-impact or high-priority issues first instead of opening many speculative fixes. If a contributor opens a large batch of low-value or duplicative PRs, maintainers may close the batch and ask the contributor to choose one PR to reopen, focus, and bring up to the documented review bar before submitting more. + +For issues, do not mass-create tickets through automation or agents. Search existing issues first, open issues only when you have enough context for someone to act, and prioritize the most important reports instead of filing every possible finding. Maintainers may close duplicate, low-signal, automated, or weakly reviewed issues without action. + +Maintainers may close issues or PRs that disregard the contribution guide, bypass required context, or lack credible contributor ownership of AI-assisted work. Repeated disregard of this contribution guide, or high-volume automated or agent-generated tracker spam across issues or PRs, may result in maintainers blocking the responsible account. + +## Bug Bounties + +Kilo has bug bounties. To be eligible, make sure your GitHub account is connected in your Kilo account. ## Contributing to the Kilo Marketplace @@ -143,14 +191,16 @@ Documentation improvements are highly valued contributions: - Use absolute paths starting from `/docs/` for internal links (except within the same directory) - Don't include `.md` extensions in links -2. Test your documentation changes by running the docs site locally: +2. Test your documentation changes and run the docs site locally from the repo root: ```bash - cd packages/kilo-docs - pnpm install - pnpm dev + bun run --filter @kilocode/kilo-docs test + bun run --filter @kilocode/kilo-docs build + bun run --filter @kilocode/kilo-docs dev ``` + For manual validation, preview the affected page and check changed links and rendered content. + 3. Submit a PR with your documentation changes ## Community Guidelines diff --git a/packages/kilo-docs/pages/customize/context/context-condensing.md b/packages/kilo-docs/pages/customize/context/context-condensing.md index 9993b4bcca3..c5b173a3e09 100644 --- a/packages/kilo-docs/pages/customize/context/context-condensing.md +++ b/packages/kilo-docs/pages/customize/context/context-condensing.md @@ -36,10 +36,12 @@ This summary replaces older conversation history while Kilo keeps the most recen ### Automatic trigger -Kilo tracks the total token count for the session — input, output, and cached reads and writes — and compares it to the model's context window. Compaction runs when the total fills the window minus a reserved buffer of headroom kept free for the next turn. +Kilo tracks the total token count for the session: input, output, and cached reads and writes. Compaction runs when token usage reaches `compaction.threshold_percent`, or when the remaining window hits the reserved safety buffer, whichever happens first. How the buffer is chosen depends on what the model declares. When the model advertises a separate input limit, the buffer defaults to 20,000 tokens (or the model's maximum output size, whichever is smaller). When the model only declares a single context window, Kilo instead reserves the model's full output cap — up to 32,000 tokens. +`compaction.threshold_percent` is optional. Set it from `1` to `100` to compact at that percentage of the model input or context window. + Custom models that do not declare a context window are not tracked, and auto-compaction does not run for them. ### Context Pruning @@ -59,10 +61,11 @@ You can trigger compaction at any time: | Setting | Default | Effect | |---|---|---| | `compaction.auto` | `true` | Automatically compact when the usable window is reached | +| `compaction.threshold_percent` | unset | Compact when token usage reaches this percentage of the model window | | `compaction.prune` | `true` | Clear old tool outputs beyond the 40K recency window | | `compaction.tail_turns` | `2` | Keep the most recent user turns and their responses verbatim when possible | | `compaction.preserve_recent_tokens` | 25% of usable context, clamped between 2,000 and 8,000 tokens | Token budget for the verbatim recent tail | -| `compaction.reserved` | `min(20,000, model_max_output_tokens)` | Token headroom kept free for the next turn — also defines the compaction trigger point | +| `compaction.reserved` | `min(20,000, model_max_output_tokens)` | Token headroom kept free for the next turn, and a safety trigger if reached before the threshold | ## Configuration @@ -72,6 +75,7 @@ Compaction is configured in your `kilo.jsonc` file: { "compaction": { "auto": true, // Enable or disable automatic compaction + "threshold_percent": 80, // Optional trigger at 80% of the model window "prune": true, // Enable pruning of old tool outputs beyond the recency window "tail_turns": 2, // Recent user turns to keep verbatim during compaction "preserve_recent_tokens": 8000, // Maximum token budget for the recent tail @@ -83,6 +87,7 @@ Compaction is configured in your `kilo.jsonc` file: | Option | Type | Default | Description | |---|---|---|---| | `compaction.auto` | boolean | `true` | Enable or disable automatic compaction when the usable window is reached | +| `compaction.threshold_percent` | number | unset | Optional percentage from 1 to 100. Auto-compaction runs when token usage reaches this share of the model input or context window, unless the reserved safety buffer triggers first. | | `compaction.prune` | boolean | `true` | Enable pruning of old tool outputs outside the 40K token recency window | | `compaction.tail_turns` | number | `2` | Number of recent user turns, including following assistant and tool responses, to keep verbatim during compaction | | `compaction.preserve_recent_tokens` | number | 25% of usable context, clamped between 2,000 and 8,000 tokens | Maximum token budget for recent turns kept verbatim after compaction | @@ -131,10 +136,12 @@ This summary replaces older conversation history while Kilo keeps the most recen ### Automatic trigger -Kilo tracks the total token count for the session — input, output, and cached reads and writes — and compares it to the model's context window. Compaction runs when the total fills the window minus a reserved buffer of headroom kept free for the next turn. +Kilo tracks the total token count for the session: input, output, and cached reads and writes. Compaction runs when token usage reaches `compaction.threshold_percent`, or when the remaining window hits the reserved safety buffer, whichever happens first. How the buffer is chosen depends on what the model declares. When the model advertises a separate input limit, the buffer defaults to 20,000 tokens (or the model's maximum output size, whichever is smaller). When the model only declares a single context window, Kilo instead reserves the model's full output cap — up to 32,000 tokens. +`compaction.threshold_percent` is optional. Set it from `1` to `100` to compact at that percentage of the model input or context window. + [Custom models](/docs/code-with-ai/agents/custom-models) that do not declare a context window are not tracked, and auto-compaction does not run for them. ### Context Pruning @@ -153,10 +160,11 @@ You can trigger compaction at any time: | Setting | Default | Effect | |---|---|---| | `compaction.auto` | `true` | Automatically compact when the usable window is reached | +| `compaction.threshold_percent` | unset | Compact when token usage reaches this percentage of the model window | | `compaction.prune` | `true` | Clear old tool outputs beyond the 40K recency window | | `compaction.tail_turns` | `2` | Keep the most recent user turns and their responses verbatim when possible | | `compaction.preserve_recent_tokens` | 25% of usable context, clamped between 2,000 and 8,000 tokens | Token budget for the verbatim recent tail | -| `compaction.reserved` | `min(20,000, model_max_output_tokens)` | Token headroom kept free for the next turn — also defines the compaction trigger point | +| `compaction.reserved` | `min(20,000, model_max_output_tokens)` | Token headroom kept free for the next turn, and a safety trigger if reached before the threshold | ## Configuration @@ -166,6 +174,7 @@ Compaction is configured in your `kilo.jsonc` file: { "compaction": { "auto": true, // Enable or disable automatic compaction + "threshold_percent": 80, // Optional trigger at 80% of the model window "prune": true, // Enable pruning of old tool outputs beyond the recency window "tail_turns": 2, // Recent user turns to keep verbatim during compaction "preserve_recent_tokens": 8000, // Maximum token budget for the recent tail @@ -177,6 +186,7 @@ Compaction is configured in your `kilo.jsonc` file: | Option | Type | Default | Description | |---|---|---|---| | `compaction.auto` | boolean | `true` | Enable or disable automatic compaction when the usable window is reached | +| `compaction.threshold_percent` | number | unset | Optional percentage from 1 to 100. Auto-compaction runs when token usage reaches this share of the model input or context window, unless the reserved safety buffer triggers first. | | `compaction.prune` | boolean | `true` | Enable pruning of old tool outputs outside the 40K token recency window | | `compaction.tail_turns` | number | `2` | Number of recent user turns, including following assistant and tool responses, to keep verbatim during compaction | | `compaction.preserve_recent_tokens` | number | 25% of usable context, clamped between 2,000 and 8,000 tokens | Maximum token budget for recent turns kept verbatim after compaction | @@ -284,7 +294,11 @@ If the condensed summary doesn't capture important details: - **Before major transitions**: When switching to a different aspect of your project - **When approaching limits**: Run `/compact` manually before hitting the automatic trigger if you want control over _when_ the summary is produced -### Tuning `compaction.reserved` +### Tuning compaction triggers + +Use `compaction.threshold_percent` when you want compaction to happen at a predictable share of the model window, such as `80` for earlier summaries on long tasks. + +The reserved safety buffer still applies and can trigger compaction earlier than the percentage threshold. On models that advertise a separate input limit, the `reserved` value is a trade-off: diff --git a/packages/kilo-docs/pages/customize/skills.md b/packages/kilo-docs/pages/customize/skills.md index 99dbe4dabe1..82ac37af29e 100644 --- a/packages/kilo-docs/pages/customize/skills.md +++ b/packages/kilo-docs/pages/customize/skills.md @@ -80,10 +80,10 @@ your-project/ ### Compatibility Directories -For interoperability with other tools, the CLI also loads skills from: +For interoperability with other tools, Kilo Code also loads skills from: -- `.claude/skills/` — Claude Code compatibility -- `.agents/skills/` — Open agent standard +- `.agents/skills/` — Open agent standard, loaded by default +- `.claude/skills/` — Claude Code compatibility, loaded when Claude Code Compatibility is enabled ### Additional Skill Paths and Remote URLs diff --git a/packages/kilo-docs/pages/gateway/sdks-and-frameworks.md b/packages/kilo-docs/pages/gateway/sdks-and-frameworks.md index 485c9d2c26b..f2ec735f7f4 100644 --- a/packages/kilo-docs/pages/gateway/sdks-and-frameworks.md +++ b/packages/kilo-docs/pages/gateway/sdks-and-frameworks.md @@ -292,6 +292,17 @@ The Kilo AI Gateway works with any framework that supports OpenAI-compatible API | [LlamaIndex](https://www.llamaindex.ai) | Use OpenAI-compatible configuration | | [Haystack](https://haystack.deepset.ai) | Use OpenAI generator with custom URL | | [Semantic Kernel](https://learn.microsoft.com/en-us/semantic-kernel/) | Use OpenAI connector with custom endpoint | +| [Pi](https://pi.dev) | Install the [Kilo provider extension](https://github.com/Kilo-Org/kilo-pi-provider) | + +### Pi coding agent + +Use the Kilo-maintained [Pi provider extension](https://github.com/Kilo-Org/kilo-pi-provider) to access Kilo Gateway models from the [Pi coding agent](https://pi.dev). + +```bash +pi install git:github.com/Kilo-Org/kilo-pi-provider +``` + +Run `/login kilo` in Pi to connect your account, or use supported free models without signing in. See the provider repository for organization configuration and model-specific behavior. ### LangChain example diff --git a/packages/kilo-docs/pages/getting-started/adding-credits.md b/packages/kilo-docs/pages/getting-started/adding-credits.md index f5c89de697c..a8b021abcea 100644 --- a/packages/kilo-docs/pages/getting-started/adding-credits.md +++ b/packages/kilo-docs/pages/getting-started/adding-credits.md @@ -9,9 +9,9 @@ Once you've used any initial free Kilo Credits, you can easily add more: - Subscribe to the [Kilo Pass](https://kilo.ai/features/kilo-pass), the most cost effective way to add credits. - Purchase additional credits as a one-time transaction. -- Enable automatic top-up, which purchases additional credits when your balance is below $5. +- Enable [automatic top-up](https://kilo.ai/features/auto-top-ups), which purchases additional credits when your balance runs low. Auto top-up is available for both individual and organization accounts. -These options are available to purchase from your [personal profile page](https://app.kilo.ai/profile). +These options are available to purchase from your [personal profile page](https://app.kilo.ai/profile). Organization owners can manage credits and auto top-up from the [Organization dashboard](https://app.kilo.ai). You can also use subscriptions or credits you may have purchased directly with an AI provider by adding your keys on the [Bring your own Key (BYOK)](https://app.kilo.ai/byok) settings screen. For setup details and supported providers, see [AI Providers documentation](/docs/ai-providers). If your provider is not yet supported, you can also [directly connect your provider](/docs/getting-started/setup-authentication) in the extension and CLI. diff --git a/packages/kilo-docs/pages/getting-started/settings/auto-approving-actions.md b/packages/kilo-docs/pages/getting-started/settings/auto-approving-actions.md index 693ad0871ed..14d2a6cf6c7 100644 --- a/packages/kilo-docs/pages/getting-started/settings/auto-approving-actions.md +++ b/packages/kilo-docs/pages/getting-started/settings/auto-approving-actions.md @@ -66,7 +66,7 @@ Use the shield button in the prompt controls to toggle runtime auto-approve for Expand **Manage Auto-Approve Rules** to add commands or patterns to your allowed or denied lists. These rules are then appended to the bottom of the approval rules in settings and the config file. -For the experimental `agent_manager` tool, runtime approvals use the requested mode as the pattern: `worktree` or `local`. +For the `agent_manager` tool, runtime approvals use the requested mode as the pattern: `worktree` or `local`. ## MCP Tool Permissions diff --git a/packages/kilo-docs/pages/getting-started/settings/index.md b/packages/kilo-docs/pages/getting-started/settings/index.md index a947eebab69..79faf946cf5 100644 --- a/packages/kilo-docs/pages/getting-started/settings/index.md +++ b/packages/kilo-docs/pages/getting-started/settings/index.md @@ -193,14 +193,24 @@ Use this option only if you are certain you want to remove all Kilo Code data or The new extension exposes experimental features via the **Experimental** tab in Settings (click the gear icon {% codicon name="gear" /%} → Experimental). -Available experimental toggles include: +Available experimental settings include: -- **Share mode** — `manual`, `auto`, or `disabled` session sharing -- **LSP integration** — expose language server diagnostics to the agent -- **Paste summary** — summarize large clipboard pastes before including them -- **Batch tool** — allow the agent to batch multiple tool calls in one step -- **Agent Manager Tool** - allow agents to start Agent Manager local and worktree sessions from chat -- **OpenTelemetry** — enable Kilo telemetry and optional OTLP export when configured +- **Share mode** - `manual`, `auto`, or `disabled` session sharing +- **LSP integration** - expose language server diagnostics to the agent +- **Paste summary** - summarize large clipboard pastes before including them +- **Speech to Text Model** - optionally select the transcription model +- **Batch tool** - allow the agent to batch multiple tool calls in one step +- **OpenTelemetry** - enable Kilo telemetry and optional OTLP export when configured + +Voice input appears automatically when the Kilo provider is enabled and you are signed in. Choosing **Speech to Text Model** stores `experimental.speech_to_text_model` in your global Kilo CLI config (`~/.config/kilo/kilo.jsonc`): + +```json +{ + "experimental": { + "speech_to_text_model": "openai/gpt-4o-mini-transcribe" + } +} +``` Advanced options not exposed in the UI can be configured via the `experimental` key in `kilo.jsonc`: @@ -209,7 +219,6 @@ Advanced options not exposed in the UI can be configured via the `experimental` "experimental": { "codebase_search": true, "batch_tool": false, - "agent_manager_tool": false, "openTelemetry": true, "disable_paste_summary": false, "mcp_timeout": 30000 diff --git a/packages/kilo-docs/pages/kiloclaw/dashboard.md b/packages/kilo-docs/pages/kiloclaw/dashboard.md index 0d6a388a5c7..6661aa069ba 100644 --- a/packages/kilo-docs/pages/kiloclaw/dashboard.md +++ b/packages/kilo-docs/pages/kiloclaw/dashboard.md @@ -189,10 +189,6 @@ Each instance runs on a dedicated machine — there is no shared infrastructure Your storage is region-pinned — once your instance is created in a region (e.g., DFW), it always runs there. OpenClaw config lives at `/root/.openclaw` and the workspace at `/root/clawd`. -{% callout type="info" %} -These are the beta specifications for machines and subject to change without notice. -{% /callout %} - ## Related - [KiloClaw Overview](/docs/kiloclaw/overview) diff --git a/packages/kilo-docs/pages/kiloclaw/faq/pricing.md b/packages/kilo-docs/pages/kiloclaw/faq/pricing.md index e63d9aa99b2..949aa62ffeb 100644 --- a/packages/kilo-docs/pages/kiloclaw/faq/pricing.md +++ b/packages/kilo-docs/pages/kiloclaw/faq/pricing.md @@ -9,10 +9,7 @@ KiloClaw uses Kilo Gateway credits by default — if you route requests through ## Instance Hosting -KiloClaw hosting is **free during the beta period**. Each user gets a dedicated machine (2 shared vCPUs, 3 GB RAM, 10 GB SSD) at no cost. - -> ℹ️ **Info** -> Beta pricing is subject to change. Paid hosting tiers may be introduced after the beta period ends. Any changes will be announced in advance. +Each user gets a dedicated machine (2 shared vCPUs, 3 GB RAM, 10 GB SSD). Visit [kilo.ai/pricing](https://kilo.ai/pricing) for current pricing and plans. ## Model Inference diff --git a/packages/kilo-docs/pages/kiloclaw/overview.md b/packages/kilo-docs/pages/kiloclaw/overview.md index 19d8b695913..7a1c518d10d 100644 --- a/packages/kilo-docs/pages/kiloclaw/overview.md +++ b/packages/kilo-docs/pages/kiloclaw/overview.md @@ -7,7 +7,7 @@ description: "One-click deployment of your personal AI agent with OpenClaw" KiloClaw is Kilo's hosted [OpenClaw](https://openclaw.ai) service — a one-click deployment that gives you a personal AI agent without the complexity of self-hosting. OpenClaw is a 24/7, open source AI agent that connects to chat platforms like Telegram, Discord, and Slack so it can take real actions automatically, not just chat. -KiloClaw is powered by KiloCode. The API key is platform-managed, so you never need to bring your own. KiloClaw is currently in **Beta**. +KiloClaw is powered by KiloCode. The API key is platform-managed, so you never need to bring your own. ## Why KiloClaw? diff --git a/packages/kilo-docs/previous-docs-redirects.js b/packages/kilo-docs/previous-docs-redirects.js index 44fe2ff0fdb..270e4874ccf 100644 --- a/packages/kilo-docs/previous-docs-redirects.js +++ b/packages/kilo-docs/previous-docs-redirects.js @@ -865,4 +865,16 @@ module.exports = [ basePath: false, permanent: true, }, + { + source: "/docs/code-with-ai/gastown/wasteland/troubleshooting", + destination: "/docs/code-with-ai/gastown/wasteland", + basePath: false, + permanent: true, + }, + { + source: "/auto-top-ups", + destination: "https://kilo.ai/features/auto-top-ups", + basePath: false, + permanent: true, + }, ] diff --git a/packages/kilo-docs/public/img/connect/github/github-bug.png b/packages/kilo-docs/public/img/connect/github/github-bug.png new file mode 100644 index 00000000000..1095aa4ab4d Binary files /dev/null and b/packages/kilo-docs/public/img/connect/github/github-bug.png differ diff --git a/packages/kilo-docs/public/img/connect/github/github-issue.png b/packages/kilo-docs/public/img/connect/github/github-issue.png new file mode 100644 index 00000000000..092ce869d21 Binary files /dev/null and b/packages/kilo-docs/public/img/connect/github/github-issue.png differ diff --git a/packages/kilo-docs/public/img/connect/github/github-review.png b/packages/kilo-docs/public/img/connect/github/github-review.png new file mode 100644 index 00000000000..cea6ea000cb Binary files /dev/null and b/packages/kilo-docs/public/img/connect/github/github-review.png differ diff --git a/packages/kilo-docs/public/img/connect/linear/linear-fix-issue.png b/packages/kilo-docs/public/img/connect/linear/linear-fix-issue.png new file mode 100644 index 00000000000..b4862a05703 Binary files /dev/null and b/packages/kilo-docs/public/img/connect/linear/linear-fix-issue.png differ diff --git a/packages/kilo-docs/public/img/connect/linear/linear-multi-repo.png b/packages/kilo-docs/public/img/connect/linear/linear-multi-repo.png new file mode 100644 index 00000000000..13555708901 Binary files /dev/null and b/packages/kilo-docs/public/img/connect/linear/linear-multi-repo.png differ diff --git a/packages/kilo-docs/public/img/connect/linear/linear-understand-issue.png b/packages/kilo-docs/public/img/connect/linear/linear-understand-issue.png new file mode 100644 index 00000000000..ec6743a2436 Binary files /dev/null and b/packages/kilo-docs/public/img/connect/linear/linear-understand-issue.png differ diff --git a/packages/kilo-docs/public/img/connect/slack/slackbot-ask-questions.webp b/packages/kilo-docs/public/img/connect/slack/slackbot-ask-questions.webp new file mode 100644 index 00000000000..74920beddab Binary files /dev/null and b/packages/kilo-docs/public/img/connect/slack/slackbot-ask-questions.webp differ diff --git a/packages/kilo-docs/public/img/connect/slack/slackbot-bugs.webp b/packages/kilo-docs/public/img/connect/slack/slackbot-bugs.webp new file mode 100644 index 00000000000..9efc3e80496 Binary files /dev/null and b/packages/kilo-docs/public/img/connect/slack/slackbot-bugs.webp differ diff --git a/packages/kilo-docs/public/img/connect/slack/slackbot-coding.webp b/packages/kilo-docs/public/img/connect/slack/slackbot-coding.webp new file mode 100644 index 00000000000..cc2f4104a8a Binary files /dev/null and b/packages/kilo-docs/public/img/connect/slack/slackbot-coding.webp differ diff --git a/packages/kilo-docs/public/img/connect/slack/slackbot-turn-discussions-into-PRs.webp b/packages/kilo-docs/public/img/connect/slack/slackbot-turn-discussions-into-PRs.webp new file mode 100644 index 00000000000..64670c4563b Binary files /dev/null and b/packages/kilo-docs/public/img/connect/slack/slackbot-turn-discussions-into-PRs.webp differ diff --git a/packages/kilo-docs/public/img/gastown/wasteland/gt-bead-with-wasteland-link.png b/packages/kilo-docs/public/img/gastown/wasteland/gt-bead-with-wasteland-link.png new file mode 100644 index 00000000000..f3f25d6fb05 Binary files /dev/null and b/packages/kilo-docs/public/img/gastown/wasteland/gt-bead-with-wasteland-link.png differ diff --git a/packages/kilo-docs/public/img/gastown/wasteland/gt-claim-detail-drawer.png b/packages/kilo-docs/public/img/gastown/wasteland/gt-claim-detail-drawer.png new file mode 100644 index 00000000000..d74355c16bb Binary files /dev/null and b/packages/kilo-docs/public/img/gastown/wasteland/gt-claim-detail-drawer.png differ diff --git a/packages/kilo-docs/public/img/gastown/wasteland/gt-mayor-claiming.png b/packages/kilo-docs/public/img/gastown/wasteland/gt-mayor-claiming.png new file mode 100644 index 00000000000..0f4f4b90fb0 Binary files /dev/null and b/packages/kilo-docs/public/img/gastown/wasteland/gt-mayor-claiming.png differ diff --git a/packages/kilo-docs/public/img/gastown/wasteland/gt-wasteland-connect-dialog.png b/packages/kilo-docs/public/img/gastown/wasteland/gt-wasteland-connect-dialog.png new file mode 100644 index 00000000000..1d2a1008d8f Binary files /dev/null and b/packages/kilo-docs/public/img/gastown/wasteland/gt-wasteland-connect-dialog.png differ diff --git a/packages/kilo-docs/public/img/gastown/wasteland/gt-wasteland-settings.png b/packages/kilo-docs/public/img/gastown/wasteland/gt-wasteland-settings.png new file mode 100644 index 00000000000..76b44beef80 Binary files /dev/null and b/packages/kilo-docs/public/img/gastown/wasteland/gt-wasteland-settings.png differ diff --git a/packages/kilo-docs/public/img/gastown/wasteland/wl-admin-review-inbox.png b/packages/kilo-docs/public/img/gastown/wasteland/wl-admin-review-inbox.png new file mode 100644 index 00000000000..12306817e5a Binary files /dev/null and b/packages/kilo-docs/public/img/gastown/wasteland/wl-admin-review-inbox.png differ diff --git a/packages/kilo-docs/public/img/gastown/wasteland/wl-claim-drawer.png b/packages/kilo-docs/public/img/gastown/wasteland/wl-claim-drawer.png new file mode 100644 index 00000000000..394655e9515 Binary files /dev/null and b/packages/kilo-docs/public/img/gastown/wasteland/wl-claim-drawer.png differ diff --git a/packages/kilo-docs/public/img/gastown/wasteland/wl-evidence-submitted.png b/packages/kilo-docs/public/img/gastown/wasteland/wl-evidence-submitted.png new file mode 100644 index 00000000000..0d25c696244 Binary files /dev/null and b/packages/kilo-docs/public/img/gastown/wasteland/wl-evidence-submitted.png differ diff --git a/packages/kilo-docs/public/img/gastown/wasteland/wl-post-form.png b/packages/kilo-docs/public/img/gastown/wasteland/wl-post-form.png new file mode 100644 index 00000000000..2112b4c3279 Binary files /dev/null and b/packages/kilo-docs/public/img/gastown/wasteland/wl-post-form.png differ diff --git a/packages/kilo-docs/public/img/gastown/wasteland/wl-wanted-board.png b/packages/kilo-docs/public/img/gastown/wasteland/wl-wanted-board.png new file mode 100644 index 00000000000..60218619367 Binary files /dev/null and b/packages/kilo-docs/public/img/gastown/wasteland/wl-wanted-board.png differ diff --git a/packages/kilo-docs/public/img/mobile-apps/home.webp b/packages/kilo-docs/public/img/mobile-apps/home.webp new file mode 100644 index 00000000000..8df94c62ca5 Binary files /dev/null and b/packages/kilo-docs/public/img/mobile-apps/home.webp differ diff --git a/packages/kilo-docs/public/img/mobile-apps/kiloclaw-chat.webp b/packages/kilo-docs/public/img/mobile-apps/kiloclaw-chat.webp new file mode 100644 index 00000000000..1df99e23e20 Binary files /dev/null and b/packages/kilo-docs/public/img/mobile-apps/kiloclaw-chat.webp differ diff --git a/packages/kilo-docs/public/img/mobile-apps/new-session.webp b/packages/kilo-docs/public/img/mobile-apps/new-session.webp new file mode 100644 index 00000000000..e8ca7dda341 Binary files /dev/null and b/packages/kilo-docs/public/img/mobile-apps/new-session.webp differ diff --git a/packages/kilo-docs/public/img/mobile-apps/session-chat.webp b/packages/kilo-docs/public/img/mobile-apps/session-chat.webp new file mode 100644 index 00000000000..26a9b82adf6 Binary files /dev/null and b/packages/kilo-docs/public/img/mobile-apps/session-chat.webp differ diff --git a/packages/kilo-docs/public/img/mobile-apps/session-filters.webp b/packages/kilo-docs/public/img/mobile-apps/session-filters.webp new file mode 100644 index 00000000000..a0f684b9cd6 Binary files /dev/null and b/packages/kilo-docs/public/img/mobile-apps/session-filters.webp differ diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/full-screen-diff-with-collapsed-context-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/full-screen-diff-with-collapsed-context-chromium-linux.png new file mode 100644 index 00000000000..bc87566f6c4 --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/full-screen-diff-with-collapsed-context-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f5e9dd9a8b232e960fbc1925a2447a7634b2db241f6fe9b9ba8527665a5ca4c4 +size 14248 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/message-list-tool-to-queued-user-spacing-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/message-list-tool-to-queued-user-spacing-chromium-linux.png index ff195b4d918..edc40570888 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/message-list-tool-to-queued-user-spacing-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/message-list-tool-to-queued-user-spacing-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c3f37193389ad7937c7b2103ad2fe3dd63123a0d4f60a0fbfd65b31d3982c092 -size 12105 +oid sha256:dbaab3a1ff10eac8f710b0ed8ddc99d96188c06a09c227e634ca712bcee12130 +size 11543 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/suggest-bar-review-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/suggest-bar-review-chromium-linux.png index 27e9b8983a1..b26984c98f1 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/suggest-bar-review-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/suggest-bar-review-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eee5f5d61cf2ac9ad0b987a8b220d76c503be0d11447766e6669da4684698c82 -size 6130 +oid sha256:bbdf3df267cb9c32febbaa486b7a63e8561e1d968bda24bb5278634307eac873 +size 5962 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/turn-outcome-failed-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/turn-outcome-failed-chromium-linux.png new file mode 100644 index 00000000000..f011a8544ec --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/turn-outcome-failed-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e6d0ccfb86ad9a6eaab64e1afe455a96cd4c1443f12fbd47698a4730724712b6 +size 931 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/turn-outcome-unknown-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/turn-outcome-unknown-chromium-linux.png new file mode 100644 index 00000000000..00e71e588fb --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/turn-outcome-unknown-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4544323cd000c4f43b88c420bbf2b7cb72317b6d40b0b0f3c690fb9deabaa27d +size 3737 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/components-shell/shell-execution-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/components-shell/shell-execution-chromium-linux.png index 4d612a7ad03..ddcba78b314 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/components-shell/shell-execution-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/components-shell/shell-execution-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74a42ec77a8ac8d5a1c555b46e3d550c9acf3198f45cc87da6582c0ff924d5f9 -size 17677 +oid sha256:7cabfff215cef485ec913b9a082aaac844275e506e88ba0fab112d95ff6de329 +size 18200 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/composite-webview/background-process-tool-cards-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/composite-webview/background-process-tool-cards-chromium-linux.png new file mode 100644 index 00000000000..b557ae59935 --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/composite-webview/background-process-tool-cards-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18d394d726ebee312e54db5d45f55cca3a1728d2b41f9f73bb9b610096bf53e0 +size 41004 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/composite-webview/permission-dock-apply-patch-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/composite-webview/permission-dock-apply-patch-chromium-linux.png index a17e69d6219..db19d08e861 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/composite-webview/permission-dock-apply-patch-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/composite-webview/permission-dock-apply-patch-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7aa0924c6679cbff6e816dd86371a2d483afb7f0840824503ef4ba325c25d6c5 -size 26282 +oid sha256:97728b91ed1c92556abfde08824b91536e87cc7afa4d121bb5b1b488946c1d05 +size 26009 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/composite-webview/permission-dock-edit-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/composite-webview/permission-dock-edit-chromium-linux.png index cdca3764fb9..0aadc161dc2 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/composite-webview/permission-dock-edit-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/composite-webview/permission-dock-edit-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f28a551185ec9d7607d02295803b155e0f7bac378fcefdf0996c6ca51c0afad6 -size 21892 +oid sha256:7b8e9f6e0b9a9a62efc9b1e807936049c7627674ba241239dfd777698f951acc +size 21820 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/history-sessionlist/with-items-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/history-sessionlist/with-items-chromium-linux.png index 74a826d3453..8f2b4c1f3fa 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/history-sessionlist/with-items-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/history-sessionlist/with-items-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:65686e2d0978adcf09ae504d9414e2d81f304eb18ff2275be7a33a9c3579a5e5 -size 18209 +oid sha256:32fa296aeee4f45f1cefe49d8eaca08099a8b3b8c23d4e9cdec803ce7fd1f0a1 +size 18207 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/agents-tab-empty-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/agents-tab-empty-chromium-linux.png new file mode 100644 index 00000000000..fd503e50e61 --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/agents-tab-empty-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7338febdf802b5ce501773be164e9da152fb7903e9b16807bd8751fa38f208ed +size 10310 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/agents-tab-with-installed-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/agents-tab-with-installed-chromium-linux.png new file mode 100644 index 00000000000..071927ae44d --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/agents-tab-with-installed-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f7e075b4961d3a62c8deda6b481f64cb8777d05183a9c10d0dd2373b7972d3ab +size 53678 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/agents-tab-with-items-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/agents-tab-with-items-chromium-linux.png new file mode 100644 index 00000000000..158e7e0bdcc --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/agents-tab-with-items-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b0dd1dc26b28d354ecd14c53dd1b9e6158a161f47152f30299ba4c0827ce1674 +size 50904 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/installed-mode-card-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/installed-agent-card-chromium-linux.png similarity index 100% rename from packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/installed-mode-card-chromium-linux.png rename to packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/installed-agent-card-chromium-linux.png diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/modes-tab-empty-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/modes-tab-empty-chromium-linux.png deleted file mode 100644 index f190c5fac6e..00000000000 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/modes-tab-empty-chromium-linux.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2c925148c349c6ab1f14190a2a379aa24869de35e5b77455fa15d8868eb08343 -size 5943 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/modes-tab-with-installed-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/modes-tab-with-installed-chromium-linux.png deleted file mode 100644 index 180572497e7..00000000000 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/modes-tab-with-installed-chromium-linux.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5094e214a52bac4627f8fe280f4818c986753c49f160bf5295f21122054ea72f -size 53684 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/modes-tab-with-items-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/modes-tab-with-items-chromium-linux.png deleted file mode 100644 index b8b99ed8d51..00000000000 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/modes-tab-with-items-chromium-linux.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:83a70ef1260f77c3017db90a918ee77c332c065bfc047e0f206948bb5dde83b9 -size 50888 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/single-mode-card-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/single-agent-card-chromium-linux.png similarity index 100% rename from packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/single-mode-card-chromium-linux.png rename to packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/marketplace/single-agent-card-chromium-linux.png diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/agent-behaviour-edit-custom-mode-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/agent-behaviour-edit-custom-mode-chromium-linux.png index 19e7ee32a14..bba8ab7e265 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/agent-behaviour-edit-custom-mode-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/agent-behaviour-edit-custom-mode-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b74f0ca7296bb81df65fe6628bc8709d743bf46f99b4c7cda67d5d1ae7c55d22 -size 47647 +oid sha256:72b04f729ebf5b25e54dc9f66aa04fbc2ec888e3396df3e34a9b8f9457117763 +size 47249 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/indexing-kilo-catalog-loading-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/indexing-kilo-catalog-loading-chromium-linux.png new file mode 100644 index 00000000000..6f601a9608e --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/indexing-kilo-catalog-loading-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d70e12fc78142601de74977fea330a91f3662851cc8f56023a6ec819724dbb8 +size 48958 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/indexing-kilo-model-preset-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/indexing-kilo-model-preset-chromium-linux.png new file mode 100644 index 00000000000..7c064e2b69e --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/indexing-kilo-model-preset-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d7ad739b2099b5b35abf3989bca8e8f63c2926906decfddf7530d6b97fb0e6b1 +size 50711 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/models-autocomplete-open-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/models-autocomplete-open-chromium-linux.png index e395b8b062d..01ea6e7ac02 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/models-autocomplete-open-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/models-autocomplete-open-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bdcac95e7969d021ebce19001f37f86b8ab670bd79e39086385c9419617294df -size 27177 +oid sha256:bbfed6606c5e1ccd6a744bdadf34ee597007d5f82c436c8c53e189560e0837d4 +size 30244 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/settings-panel-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/settings-panel-chromium-linux.png index 77079da8613..542e06b36ac 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/settings-panel-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/settings-panel-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:73a34a22b24e42731510f9ecd3ea8827a0e39fa9b335294c45c4e8780df9a1f4 -size 35853 +oid sha256:6bcfc6c12f69c5131dcc8f294f480e26ff53f87e0c5943bf525e69c4c894ad21 +size 43744 diff --git a/packages/kilo-docs/source-links.md b/packages/kilo-docs/source-links.md index 7bfc0a61504..e4b29c05385 100644 --- a/packages/kilo-docs/source-links.md +++ b/packages/kilo-docs/source-links.md @@ -1,13 +1,16 @@ # Source Code Links - +- + - - +- + - - @@ -24,16 +27,28 @@ - +- + +- + +- + +- + - - - +- + - - +- + - - @@ -42,8 +57,6 @@ - -- - - - @@ -68,6 +81,7 @@ - + - - @@ -83,8 +97,10 @@ - + - + - @@ -113,7 +129,6 @@ - - - - diff --git a/packages/kilo-gateway/.gitignore b/packages/kilo-gateway/.gitignore new file mode 100644 index 00000000000..b6f2962c39e --- /dev/null +++ b/packages/kilo-gateway/.gitignore @@ -0,0 +1 @@ +.artifacts diff --git a/packages/kilo-gateway/package.json b/packages/kilo-gateway/package.json index bd51095255b..c50a79a6df4 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.12", "type": "module", "license": "MIT", "description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration", @@ -17,6 +17,10 @@ ], "exports": { ".": "./src/index.ts", + "./autocomplete": "./src/autocomplete.ts", + "./fim": "./src/fim.ts", + "./edit": "./src/edit.ts", + "./edit-prompt": "./src/edit-prompt.ts", "./tui": "./src/tui.ts" }, "files": [ @@ -24,7 +28,8 @@ ], "scripts": { "typecheck": "tsgo --noEmit", - "build": "tsc" + "build": "tsc", + "test:ci": "mkdir -p .artifacts/unit && bun test test --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml" }, "dependencies": { "@kilocode/plugin": "workspace:*", 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/autocomplete.ts b/packages/kilo-gateway/src/autocomplete.ts new file mode 100644 index 00000000000..0d3a8e7b7b4 --- /dev/null +++ b/packages/kilo-gateway/src/autocomplete.ts @@ -0,0 +1,147 @@ +export type AutocompleteProviderID = "kilo" | "mistral" | "inception" +export type DirectAutocompleteProviderID = Exclude + +export interface AutocompleteModelDef { + /** Stable combined value for internal comparisons. */ + readonly id: string + /** Model value stored in settings and sent to the autocomplete API. */ + readonly modelID: string + /** Human-readable label shown in settings. */ + readonly label: string + /** Provider value stored in settings and used by the selector group. */ + readonly providerID: AutocompleteProviderID + /** Provider display name for status bar / telemetry. */ + readonly provider: string + /** Full model ID sent upstream by the autocomplete route. */ + readonly requestModel: string + /** Provider key to use for direct BYOK. Empty means Kilo Gateway. */ + readonly directProvider?: DirectAutocompleteProviderID + /** Request temperature. */ + readonly temperature: number + /** + * Which gateway endpoint this model targets. Defaults to "fim" if omitted + * (back-compat with existing entries). Models with `kind: "edit"` route + * through `/kilo/edit` and use Mercury's Next Edit pipeline. + */ + readonly kind?: "fim" | "edit" +} + +const models: AutocompleteModelDef[] = [ + { + id: "kilo/mistralai/codestral-2508", + modelID: "mistralai/codestral-2508", + label: "Codestral", + providerID: "kilo", + provider: "Kilo Gateway", + requestModel: "mistralai/codestral-2508", + temperature: 0.2, + }, + { + id: "kilo/inception/mercury-edit-2", + modelID: "inception/mercury-edit-2", + label: "Mercury Edit 2", + providerID: "kilo", + provider: "Kilo Gateway", + requestModel: "inception/mercury-edit-2", + temperature: 0, + }, + { + // Same wire-level model as `kilo/inception/mercury-edit-2`, but routed + // through the Kilo Gateway's Next Edit endpoint instead of FIM. Picked by + // users who want multi-line next-edit predictions with the jump-to-edit UX. + id: "kilo/inception/mercury-next-edit", + modelID: "inception/mercury-next-edit", + label: "Mercury Next Edit", + providerID: "kilo", + provider: "Kilo Gateway", + requestModel: "inception/mercury-edit-2", + temperature: 0, + kind: "edit", + }, + { + id: "mistral/codestral-2508", + modelID: "codestral-2508", + label: "Codestral", + providerID: "mistral", + provider: "Mistral", + requestModel: "codestral-2508", + directProvider: "mistral", + temperature: 0.2, + }, + { + id: "inception/mercury-edit-2", + modelID: "mercury-edit-2", + label: "Mercury Edit 2", + providerID: "inception", + provider: "Inception", + requestModel: "mercury-edit-2", + directProvider: "inception", + temperature: 0, + }, + { + // Same wire-level model as `mercury-edit-2`, but routed through the + // Mercury Next Edit endpoint instead of FIM. Picked by users who want + // multi-line next-edit predictions with the jump-to-edit UX. + id: "inception/mercury-next-edit", + modelID: "mercury-next-edit", + label: "Mercury Next Edit", + providerID: "inception", + provider: "Inception", + requestModel: "mercury-edit-2", + directProvider: "inception", + temperature: 0, + kind: "edit", + }, +] + +export const AUTOCOMPLETE_MODELS: readonly AutocompleteModelDef[] = models + +export const DEFAULT_AUTOCOMPLETE_PROVIDER_ID: AutocompleteProviderID = "kilo" +export const DEFAULT_AUTOCOMPLETE_MODEL_ID = "mistralai/codestral-2508" + +export const DEFAULT_AUTOCOMPLETE_MODEL: AutocompleteModelDef = (() => { + const found = models.find( + (m) => m.providerID === DEFAULT_AUTOCOMPLETE_PROVIDER_ID && m.modelID === DEFAULT_AUTOCOMPLETE_MODEL_ID, + ) + if (!found) { + throw new Error( + `DEFAULT_AUTOCOMPLETE_MODEL not found: provider=${DEFAULT_AUTOCOMPLETE_PROVIDER_ID} model=${DEFAULT_AUTOCOMPLETE_MODEL_ID}`, + ) + } + return found +})() + +const aliases: Record = { + "inception/mercury-edit": "inception/mercury-edit-2", +} + +export function getAutocompleteModel(provider?: string, model?: string): AutocompleteModelDef { + // When provider is unset, always default to Kilo Gateway. Direct-provider + // use must be opted into explicitly via the provider setting — never inferred + // from a model name, since the same plain model id can exist on multiple + // providers and we don't want to silently route legacy settings to BYOK. + const pid = provider ?? "kilo" + const mid = aliases[model ?? ""] ?? model + for (const m of models) { + if (m.providerID === pid && m.modelID === mid) return m + } + return DEFAULT_AUTOCOMPLETE_MODEL +} + +export function getAutocompleteModelById(id: string): AutocompleteModelDef { + for (const m of models) { + if (m.id === id) return m + } + return DEFAULT_AUTOCOMPLETE_MODEL +} + +export function validAutocompleteProvider(value: unknown) { + if (typeof value !== "string") return false + return models.some((m) => m.providerID === value) +} + +export function validAutocompleteModel(value: unknown) { + if (typeof value !== "string") return false + const resolved = aliases[value] ?? value + return models.some((m) => m.modelID === resolved) +} diff --git a/packages/kilo-gateway/src/edit-prompt.ts b/packages/kilo-gateway/src/edit-prompt.ts new file mode 100644 index 00000000000..60c3802d8e6 --- /dev/null +++ b/packages/kilo-gateway/src/edit-prompt.ts @@ -0,0 +1,107 @@ +/** + * Mercury Next Edit prompt assembly. Lives in the gateway so every client + * (VS Code, JetBrains, TUI) sends the same structured editor context and the + * Mercury-specific sentinel format is defined in exactly one place. + * + * Tag set is defined by the model and must be reproduced verbatim — see + * https://docs.inceptionlabs.ai/capabilities/next-edit + */ + +const RECENTLY_VIEWED_SNIPPETS_OPEN = "<|recently_viewed_code_snippets|>" +const RECENTLY_VIEWED_SNIPPETS_CLOSE = "<|/recently_viewed_code_snippets|>" +const RECENTLY_VIEWED_SNIPPET_OPEN = "<|recently_viewed_code_snippet|>" +const RECENTLY_VIEWED_SNIPPET_CLOSE = "<|/recently_viewed_code_snippet|>" +const CURRENT_FILE_CONTENT_OPEN = "<|current_file_content|>" +const CURRENT_FILE_CONTENT_CLOSE = "<|/current_file_content|>" +const CODE_TO_EDIT_OPEN = "<|code_to_edit|>" +const CODE_TO_EDIT_CLOSE = "<|/code_to_edit|>" +const EDIT_DIFF_HISTORY_OPEN = "<|edit_diff_history|>" +const EDIT_DIFF_HISTORY_CLOSE = "<|/edit_diff_history|>" +const CURSOR = "<|cursor|>" +/** Trailing token that tells the model this is a next-edit (not chat) request. */ +const UNIQUE_TOKEN = "<|!@#IS_NEXT_EDIT!@#|>" + +export interface MercuryRecentSnippet { + filepath: string + content: string +} + +/** Editor-derived context a client sends; the gateway turns it into a prompt. */ +export interface MercuryEditContext { + currentFilePath: string + currentFileContent: string + cursorLine: number + cursorCharacter: number + editableRegionStartLine: number + editableRegionEndLine: number + recentlyViewedSnippets: MercuryRecentSnippet[] + editDiffHistory: string[] +} + +function insertCursorToken(lines: string[], cursorLine: number, cursorCharacter: number): string[] { + if (cursorLine < 0 || cursorLine >= lines.length) return lines + const line = lines[cursorLine] + const safeChar = Math.min(Math.max(cursorCharacter, 0), line.length) + const next = line.slice(0, safeChar) + CURSOR + line.slice(safeChar) + return [...lines.slice(0, cursorLine), next, ...lines.slice(cursorLine + 1)] +} + +export function recentlyViewedSnippetsBlock(snippets: MercuryRecentSnippet[]): string { + const inner = snippets + .map((s) => + [RECENTLY_VIEWED_SNIPPET_OPEN, `code_snippet_file_path: ${s.filepath}`, s.content, RECENTLY_VIEWED_SNIPPET_CLOSE].join("\n"), + ) + .join("\n") + return [RECENTLY_VIEWED_SNIPPETS_OPEN, inner, RECENTLY_VIEWED_SNIPPETS_CLOSE].join("\n") +} + +export function currentFileContentBlock( + currentFilePath: string, + currentFileContent: string, + editableRegionStartLine: number, + editableRegionEndLine: number, + cursorLine: number, + cursorCharacter: number, +): string { + const rawLines = currentFileContent.split("\n") + const withCursor = insertCursorToken(rawLines, cursorLine, cursorCharacter) + const start = Math.max(0, Math.min(editableRegionStartLine, withCursor.length)) + const end = Math.max(start, Math.min(editableRegionEndLine, withCursor.length - 1)) + const instrumented = [ + ...withCursor.slice(0, start), + CODE_TO_EDIT_OPEN, + ...withCursor.slice(start, end + 1), + CODE_TO_EDIT_CLOSE, + ...withCursor.slice(end + 1), + ] + return [CURRENT_FILE_CONTENT_OPEN, `current_file_path: ${currentFilePath}`, instrumented.join("\n"), CURRENT_FILE_CONTENT_CLOSE].join("\n") +} + +export function editDiffHistoryBlock(diffs: string[]): string { + // Each unidiff from `diff.createPatch` opens with an Index line + separator we + // strip. Diffs are blank-line separated so the model reads them as distinct hunks. + const trimmed = diffs.map((d) => { + const lines = d.split("\n") + return lines.length > 2 ? lines.slice(2).join("\n") : d + }) + return [EDIT_DIFF_HISTORY_OPEN, trimmed.join("\n\n"), EDIT_DIFF_HISTORY_CLOSE].join("\n") +} + +export function buildMercuryEditPrompt(ctx: MercuryEditContext): string { + return [ + recentlyViewedSnippetsBlock(ctx.recentlyViewedSnippets), + "", + currentFileContentBlock( + ctx.currentFilePath, + ctx.currentFileContent, + ctx.editableRegionStartLine, + ctx.editableRegionEndLine, + ctx.cursorLine, + ctx.cursorCharacter, + ), + "", + editDiffHistoryBlock(ctx.editDiffHistory), + "", + UNIQUE_TOKEN, + ].join("\n") +} diff --git a/packages/kilo-gateway/src/edit.ts b/packages/kilo-gateway/src/edit.ts new file mode 100644 index 00000000000..7740ced3f91 --- /dev/null +++ b/packages/kilo-gateway/src/edit.ts @@ -0,0 +1,72 @@ +import { KILO_API_BASE } from "./api/constants.js" +import { getAutocompleteModel, type DirectAutocompleteProviderID } from "./autocomplete.js" + +/** + * Env var(s) consulted as a fallback for BYOK keys when the provider hasn't + * been authenticated via the gateway's Auth store. Mirrors `DIRECT_FIM_ENV`. + */ +export const DIRECT_EDIT_ENV: Record = { + mistral: ["MISTRAL_API_KEY"], + inception: ["INCEPTION_API_KEY"], +} + +export type EditTarget = + | { provider: "inception"; model: string; url: string } + | { provider: "kilo"; model: string; url: string } + +/** Shape of the upstream (Mercury) chat/edit completion response we read from. */ +export interface EditUpstreamResponse { + choices?: Array<{ message?: { content?: string } }> + usage?: { prompt_tokens?: number; completion_tokens?: number } +} + +const INCEPTION_EDIT_URL = "https://api.inceptionlabs.ai/v1/edit/completions" +const KILO_NEXTEDIT_URL = KILO_API_BASE + "/api/edit/completions" + +/** + * Pick the upstream edit endpoint for a (provider, model) pair. Today this is + * either Inception's `/v1/edit/completions` (direct BYOK) or the Kilo Gateway's + * `/api/edit/completions` proxy, which forwards to Inception server-side. + * Mistral does not expose a comparable surface. + */ +export function resolveEditTarget(provider?: string, model?: string): EditTarget { + const info = getAutocompleteModel(provider, model) + if (info.kind === "edit") { + if (info.providerID === "kilo") { + // The gateway expects the upstream model id with the `inception/` prefix + // (it strips it before forwarding to Inception). The kilo entry's + // `requestModel` already carries the prefix. + const m = info.requestModel.includes("/") ? info.requestModel : `inception/${info.requestModel}` + return { provider: "kilo", model: m, url: KILO_NEXTEDIT_URL } + } + if (info.directProvider === "inception") { + return { provider: "inception", model: info.requestModel, url: INCEPTION_EDIT_URL } + } + } + // Non-edit models fall through to a kilo placeholder with no URL so the + // handler can surface a 400 rather than silently routing somewhere unexpected. + return { provider: "kilo", model: info.requestModel, url: "" } +} + +/** + * Mercury wraps the rewritten editable region in a triple-backtick fence, + * sometimes with a language tag and sometimes with `<|code_to_edit|>` sentinels + * inside. Strip all of that down to the bare code. Shared by both the hono and + * the Effect HttpApi edit handlers so the parsing can't drift between them. + */ +export function extractFencedBody(message: string): string { + if (!message) return "" + const fenceOpen = message.indexOf("```") + if (fenceOpen === -1) return message + const afterFenceOpen = message.indexOf("\n", fenceOpen + 3) + if (afterFenceOpen === -1) return "" + // A missing closing fence means the replacement was truncated. Applying a + // partial editable region can delete valid trailing code, so suppress it. + const fenceClose = message.indexOf("```", afterFenceOpen + 1) + if (fenceClose === -1) return "" + let body = message.slice(afterFenceOpen + 1, fenceClose) + if (body.endsWith("\n")) body = body.slice(0, -1) + body = body.replace(/^<\|code_to_edit\|>\n?/, "") + body = body.replace(/\n?<\|\/code_to_edit\|>$/, "") + return body +} diff --git a/packages/kilo-gateway/src/fim.ts b/packages/kilo-gateway/src/fim.ts new file mode 100644 index 00000000000..2c7783080ae --- /dev/null +++ b/packages/kilo-gateway/src/fim.ts @@ -0,0 +1,34 @@ +import { KILO_API_BASE } from "./api/constants.js" +import { getAutocompleteModel, type DirectAutocompleteProviderID } from "./autocomplete.js" + +export { requestMistralFim } from "./mistral-fim-endpoint.js" + +export const DIRECT_FIM_ENV: Record = { + mistral: ["MISTRAL_API_KEY"], + inception: ["INCEPTION_API_KEY"], +} + +export type FimTarget = + | { provider: "kilo"; model: string; url: string } + | { provider: "inception"; model: string; url: string } + | { provider: "mistral"; model: string } + +const KILO_FIM_URL = KILO_API_BASE + "/api/fim/completions" +const INCEPTION_FIM_URL = "https://api.inceptionlabs.ai/v1/fim/completions" + +function kiloTarget(model?: string): FimTarget { + return { provider: "kilo", model: model ?? "mistralai/codestral-2501", url: KILO_FIM_URL } +} + +export function resolveFimTarget(provider?: string, model?: string): FimTarget { + if (!provider || provider === "kilo") return kiloTarget(model) + + const info = getAutocompleteModel(provider, model) + if (info.directProvider === "mistral") { + return { provider: "mistral", model: info.requestModel } + } + if (info.directProvider === "inception") { + return { provider: "inception", model: info.requestModel, url: INCEPTION_FIM_URL } + } + return kiloTarget(model) +} 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..6a1449d9c19 100644 --- a/packages/kilo-gateway/src/index.ts +++ b/packages/kilo-gateway/src/index.ts @@ -41,6 +41,16 @@ export { type KiloEmbeddingModelCatalog, } from "./api/embedding-models.js" export { resolveKiloGatewayBaseUrl, resolveKiloOpenRouterBaseUrl } from "./api/url.js" +export { + AUTOCOMPLETE_MODELS, + DEFAULT_AUTOCOMPLETE_MODEL, + getAutocompleteModel, + getAutocompleteModelById, + validAutocompleteModel, + validAutocompleteProvider, + type AutocompleteModelDef, + type AutocompleteProviderID, +} from "./autocomplete.js" export { fetchOrganizationModes, clearModesCache, @@ -48,11 +58,24 @@ export { type OrganizationModeConfig, } from "./api/modes.js" export { fetchKilocodeNotifications, type KilocodeNotification } from "./api/notifications.js" +export { fetchCloudSession, fetchCloudSessionForImport, importSessionToDb } from "./cloud-sessions.js" // ============================================================================ // Server Routes (optional - requires hono and OpenCode dependencies) // ============================================================================ export { createKiloRoutes } from "./server/routes.js" +export { + GatewayError, + UnauthorizedError, + getOrganizationId, + getClawChatCredentials, + getClawStatus, + getCloudSessions, + getNotifications, + getProfile, + getToken, + setOrganization, +} from "./server/handlers.js" // ============================================================================ // Note: TUI exports moved to separate entry point @@ -88,6 +111,8 @@ export { ENV_KILO_API_URL, DEFAULT_KILO_API_URL, KILO_API_BASE, + KILO_CHAT_URL, + KILO_EVENT_SERVICE_URL, KILO_OPENROUTER_BASE, POLL_INTERVAL_MS, DEFAULT_MODEL, @@ -100,6 +125,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/mistral-fim-endpoint.ts b/packages/kilo-gateway/src/mistral-fim-endpoint.ts new file mode 100644 index 00000000000..ebd5b70fb41 --- /dev/null +++ b/packages/kilo-gateway/src/mistral-fim-endpoint.ts @@ -0,0 +1,30 @@ +export const MISTRAL_FIM_URL = "https://api.mistral.ai/v1/fim/completions" +export const CODESTRAL_FIM_URL = "https://codestral.mistral.ai/v1/fim/completions" + +let preferred: string | undefined + +export function isMistralEndpointMismatch(response: Response) { + return response.status === 401 || response.status === 403 +} + +export function clearMistralFimEndpointCache() { + preferred = undefined +} + +export function getCachedMistralFimEndpoint() { + return preferred +} + +export async function requestMistralFim(request: (url: string) => Promise) { + const firstUrl = preferred ?? MISTRAL_FIM_URL + const secondUrl = firstUrl === MISTRAL_FIM_URL ? CODESTRAL_FIM_URL : MISTRAL_FIM_URL + const first = await request(firstUrl) + + if (first.ok) return first + if (!isMistralEndpointMismatch(first)) return first + + preferred = undefined + const second = await request(secondUrl) + if (second.ok) preferred = secondUrl + return second +} 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/edit.ts b/packages/kilo-gateway/src/server/edit.ts new file mode 100644 index 00000000000..fca7efa44e4 --- /dev/null +++ b/packages/kilo-gateway/src/server/edit.ts @@ -0,0 +1,113 @@ +import { HEADER_FEATURE } from "../api/constants.js" +import { DIRECT_EDIT_ENV, extractFencedBody, resolveEditTarget, type EditTarget, type EditUpstreamResponse } from "../edit.js" +import { buildMercuryEditPrompt, type MercuryEditContext } from "../edit-prompt.js" +import type { DirectAutocompleteProviderID } from "../autocomplete.js" +import { buildKiloHeaders } from "../headers.js" +import type { AuthStore } from "./handlers.js" + +type Auth = Pick + +const EDIT_TIMEOUT_MS = 30_000 +const MAX_TOKENS_DEFAULT = 512 + +async function getProviderKey(Auth: Auth, provider: DirectAutocompleteProviderID): Promise { + const auth = await Auth.get(provider) + if (auth?.type === "api") return auth.key + return DIRECT_EDIT_ENV[provider].map((key) => process.env[key]).find(Boolean) +} + +async function getProxyAuth(Auth: Auth) { + const auth = await Auth.get("kilo") + const token = auth?.type === "api" ? auth.key : auth?.type === "oauth" ? auth.access : undefined + return { + auth, + token, + organizationId: auth?.type === "oauth" ? auth.accountId : undefined, + } +} + +export function createEditHandler(Auth: Auth) { + return async (c: any) => { + const { provider, model, maxTokens, ...context } = c.req.valid("json") + const target = resolveEditTarget(provider, model) + + if (target.provider === "kilo" && !target.url) { + return c.json({ error: "Next Edit currently requires the Inception provider (mercury-edit-2)." }, 400 as any) + } + + const proxy = target.provider === "kilo" ? await getProxyAuth(Auth) : undefined + const token = + target.provider === "kilo" ? proxy?.token : await getProviderKey(Auth, target.provider as DirectAutocompleteProviderID) + + if (target.provider === "kilo" && !proxy?.auth) { + return c.json({ error: "Not authenticated with Kilo Gateway" }, 401 as any) + } + + if (!token) { + return c.json({ error: `Missing ${target.provider} provider API key` }, 401 as any) + } + + // Build the Mercury sentinel prompt here so every client only sends + // structured editor context. + const content = buildMercuryEditPrompt(context as MercuryEditContext) + const signal = AbortSignal.any([c.req.raw.signal, AbortSignal.timeout(EDIT_TIMEOUT_MS)]) + + let response: Response + try { + response = await fetch(target.url, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + ...(target.provider === "kilo" + ? buildKiloHeaders(undefined, { kilocodeOrganizationId: proxy?.organizationId }) + : {}), + ...(target.provider === "kilo" ? { [HEADER_FEATURE]: "autocomplete" } : {}), + }, + signal, + body: JSON.stringify({ + model: target.model, + max_tokens: maxTokens ?? MAX_TOKENS_DEFAULT, + // Mercury rejects role:"system" on this endpoint — must be a single + // user message. See the integration's constants.ts for context. + messages: [{ role: "user", content }], + }), + }) + } catch (err) { + if (err instanceof DOMException && err.name === "TimeoutError") { + return c.json({ error: "Edit request timed out" }, 504 as any) + } + if (signal.aborted) return c.json({ error: "Edit request canceled" }, 499 as any) + throw err + } + + if (!response.ok) { + const text = await safeText(response) + return c.json({ error: `Edit request failed: ${response.status} ${text}` }, response.status as any) + } + + const json = (await response.json()) as EditUpstreamResponse + const replyContent = json.choices?.[0]?.message?.content ?? "" + const body = extractFencedBody(replyContent) + return c.json({ + content: body, + usage: json.usage + ? { + prompt_tokens: json.usage.prompt_tokens, + completion_tokens: json.usage.completion_tokens, + } + : undefined, + }) + } +} + +async function safeText(res: Response): Promise { + try { + return await res.text() + } catch { + return "" + } +} + +// Re-export the target type for tests + the opencode handler +export type { EditTarget } diff --git a/packages/kilo-gateway/src/server/fim.ts b/packages/kilo-gateway/src/server/fim.ts new file mode 100644 index 00000000000..08790784ea0 --- /dev/null +++ b/packages/kilo-gateway/src/server/fim.ts @@ -0,0 +1,120 @@ +import { HEADER_FEATURE } from "../api/constants.js" +import type { DirectAutocompleteProviderID } from "../autocomplete.js" +import { DIRECT_FIM_ENV, requestMistralFim, resolveFimTarget, type FimTarget } from "../fim.js" +import { buildKiloHeaders } from "../headers.js" +import type { AuthStore } from "./handlers.js" + +type Auth = Pick + +const FIM_TIMEOUT_MS = 30_000 + +async function getProxyAuth(Auth: Auth) { + const auth = await Auth.get("kilo") + const token = auth?.type === "api" ? auth.key : auth?.type === "oauth" ? auth.access : undefined + return { + auth, + token, + organizationId: auth?.type === "oauth" ? auth.accountId : undefined, + } +} + +async function getProviderKey(Auth: Auth, provider: DirectAutocompleteProviderID) { + const auth = await Auth.get(provider) + if (auth?.type === "api") return auth.key + return DIRECT_FIM_ENV[provider].map((key) => process.env[key]).find(Boolean) +} + +async function fetchFim( + target: FimTarget, + key: string, + input: { + prefix: string + suffix: string + maxTokens: number + temperature: number + signal: AbortSignal + organizationId?: string + }, +): Promise { + const run = async (url: string) => { + console.info(`[FIM] request provider=${target.provider} model=${target.model} url=${url}`) + return fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${key}`, + ...(target.provider === "kilo" + ? buildKiloHeaders(undefined, { kilocodeOrganizationId: input.organizationId }) + : {}), + ...(target.provider === "kilo" ? { [HEADER_FEATURE]: "autocomplete" } : {}), + }, + signal: input.signal, + body: JSON.stringify({ + model: target.model, + prompt: input.prefix, + suffix: input.suffix, + max_tokens: input.maxTokens, + temperature: input.temperature, + stream: true, + }), + }) + } + + if (target.provider === "mistral") return requestMistralFim(run) + return run(target.url) +} + +export function createFimHandler(Auth: Auth) { + return async (c: any) => { + const { prefix, suffix, provider, model, maxTokens, temperature } = c.req.valid("json") + const target = resolveFimTarget(provider, model) + const fimMaxTokens = maxTokens ?? 256 + const fimTemperature = temperature ?? 0.2 + const proxy = target.provider === "kilo" ? await getProxyAuth(Auth) : undefined + const token = target.provider === "kilo" ? proxy?.token : await getProviderKey(Auth, target.provider) + + if (target.provider === "kilo" && !proxy?.auth) { + return c.json({ error: "Not authenticated with Kilo Gateway" }, 401) + } + + if (target.provider === "kilo" && !token) { + return c.json({ error: "No valid token found" }, 401) + } + + if (!token) { + return c.json({ error: `Missing ${target.provider} provider API key` }, 401) + } + + const signal = AbortSignal.any([c.req.raw.signal, AbortSignal.timeout(FIM_TIMEOUT_MS)]) + + try { + const response = await fetchFim(target, token, { + prefix, + suffix, + maxTokens: fimMaxTokens, + temperature: fimTemperature, + signal, + organizationId: proxy?.organizationId, + }) + + if (!response.ok) { + const text = await response.text() + return c.json({ error: `FIM request failed: ${response.status} ${text}` }, response.status as any) + } + + return new Response(response.body, { + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }, + }) + } catch (err) { + if (err instanceof DOMException && err.name === "TimeoutError") { + return c.json({ error: "FIM request timed out" }, 504 as any) + } + if (signal.aborted) return c.json({ error: "FIM request canceled" }, 499 as any) + throw err + } + } +} diff --git a/packages/kilo-gateway/src/server/handlers.ts b/packages/kilo-gateway/src/server/handlers.ts new file mode 100644 index 00000000000..ff8e7f2114e --- /dev/null +++ b/packages/kilo-gateway/src/server/handlers.ts @@ -0,0 +1,190 @@ +import { fetchBalance, fetchProfile } from "../api/profile.js" +import { fetchKilocodeNotifications } from "../api/notifications.js" +import { clearModesCache } from "../api/modes.js" +import { HEADER_ORGANIZATIONID, KILO_API_BASE, KILO_CHAT_URL, KILO_EVENT_SERVICE_URL } from "../api/constants.js" +import type { KilocodeBalance, KilocodeProfile } from "../types.js" +import { buildKiloHeaders } from "../headers.js" + +export type KiloAuth = + | { type: "api"; key: string } + | { type: "oauth"; access: string; refresh: string; expires: number; accountId?: string } + | { type: "wellknown"; key: string; token: string } + +export interface KiloProfileResult { + profile: KilocodeProfile + balance: KilocodeBalance | null + currentOrgId: string | null +} + +export interface ClawChatCredentials { + token: string + expiresAt: string + kiloChatUrl: string + eventServiceUrl: string +} + +export interface AuthStore { + get(provider: string): Promise + set(provider: string, auth: Extract): Promise +} + +export interface OrganizationDeps { + auth: AuthStore + clear(): void | Promise + dispose(): Promise +} + +export interface CloudSessionsInput { + cursor?: string + limit?: number + gitUrl?: string +} + +export class UnauthorizedError extends Error {} + +export class GatewayError extends Error { + constructor( + message: string, + readonly status: number, + ) { + super(message) + } +} + +export function getToken(auth: KiloAuth | undefined) { + if (auth?.type === "api") return auth.key + if (auth?.type === "oauth") return auth.access + return undefined +} + +export function getOrganizationId(auth: KiloAuth | undefined) { + if (auth?.type === "oauth") return auth.accountId + return undefined +} + +export async function getProfile(auth: AuthStore): Promise { + const info = await auth.get("kilo") + if (!info || info.type !== "oauth") throw new UnauthorizedError("Not authenticated with Kilo Gateway") + + const currentOrgId = info.accountId ?? null + const [profile, balance] = await Promise.all([ + fetchProfile(info.access), + fetchBalance(info.access, currentOrgId ?? undefined), + ]) + return { profile, balance, currentOrgId } +} + +export async function getNotifications(auth: AuthStore) { + const info = await auth.get("kilo") + const token = getToken(info) + if (!token) return [] + + return fetchKilocodeNotifications({ + kilocodeToken: token, + kilocodeOrganizationId: getOrganizationId(info), + }) +} + +export async function setOrganization(deps: OrganizationDeps, organizationId: string | null) { + const info = await deps.auth.get("kilo") + if (!info || info.type !== "oauth") throw new UnauthorizedError("Not authenticated with Kilo Gateway") + + await deps.auth.set("kilo", { + type: "oauth", + refresh: info.refresh, + access: info.access, + expires: info.expires, + ...(organizationId && { accountId: organizationId }), + }) + + await deps.clear() + clearModesCache() + await deps.dispose() + return true +} + +export async function getClawStatus(auth: AuthStore) { + const info = await auth.get("kilo") + const token = getToken(info) + if (!token) throw new UnauthorizedError("No valid token found") + + const headers: Record = { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + } + const org = getOrganizationId(info) + if (org) headers[HEADER_ORGANIZATIONID] = org + + const response = await fetch(`${KILO_API_BASE}/api/kiloclaw/status`, { headers }) + if (!response.ok) throw new GatewayError(await response.text(), response.status) + return response.json() +} + +export async function getClawChatCredentials(auth: AuthStore): Promise { + const info = await auth.get("kilo") + const token = getToken(info) + if (!token) throw new UnauthorizedError("No valid token found") + + const expires = info?.type === "oauth" ? info.expires : Date.now() + 365 * 24 * 60 * 60 * 1000 + return { + token, + expiresAt: new Date(expires).toISOString(), + kiloChatUrl: KILO_CHAT_URL, + eventServiceUrl: KILO_EVENT_SERVICE_URL, + } +} + +export async function getCloudSessions(token: string, input: CloudSessionsInput) { + const query: Record = {} + if (input.cursor) query.cursor = input.cursor + if (input.limit) query.limit = input.limit + if (input.gitUrl) query.gitUrl = input.gitUrl + + const params = new URLSearchParams({ + batch: "1", + input: JSON.stringify({ "0": query }), + }) + + const response = await fetch(`${KILO_API_BASE}/api/trpc/cliSessionsV2.list?${params.toString()}`, { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + ...buildKiloHeaders(), + }, + }) + + if (!response.ok) { + const text = await response.text() + console.error("[Kilo Gateway] cloud-sessions: tRPC request failed", { + status: response.status, + body: text.slice(0, 500), + }) + throw new GatewayError(`Cloud sessions fetch failed: ${response.status}`, response.status) + } + + const raw = await response.text() + const json = JSON.parse(raw) + const data = Array.isArray(json) ? json[0]?.result?.data : null + const result = data?.json ?? data + if (!result) return { cliSessions: [], nextCursor: null } + + const cliSessions = (result.cliSessions ?? []).map((item: any) => ({ + session_id: item.session_id, + title: item.title ?? null, + created_at: + typeof item.created_at === "string" + ? item.created_at + : item.created_at + ? new Date(item.created_at).toISOString() + : new Date().toISOString(), + updated_at: + typeof item.updated_at === "string" + ? item.updated_at + : item.updated_at + ? new Date(item.updated_at).toISOString() + : new Date().toISOString(), + version: item.version ?? 0, + })) + + return { cliSessions, nextCursor: result.nextCursor ?? null } +} diff --git a/packages/kilo-gateway/src/server/routes.ts b/packages/kilo-gateway/src/server/routes.ts index 269427b7f44..c90c4f1253b 100644 --- a/packages/kilo-gateway/src/server/routes.ts +++ b/packages/kilo-gateway/src/server/routes.ts @@ -5,19 +5,24 @@ * This factory function accepts OpenCode dependencies to create Kilo-specific routes */ -import { fetchProfile, fetchBalance } from "../api/profile.js" import { fetchKilocodeNotifications, KilocodeNotificationSchema } from "../api/notifications.js" import { fetchOrganizationModes, clearModesCache } from "../api/modes.js" -import { - KILO_API_BASE, - KILO_CHAT_URL, - KILO_EVENT_SERVICE_URL, - HEADER_FEATURE, - HEADER_ORGANIZATIONID, -} from "../api/constants.js" +import { KILO_API_BASE, HEADER_FEATURE, HEADER_ORGANIZATIONID } from "../api/constants.js" import { buildKiloHeaders } from "../headers.js" import type { ImportDeps, DrizzleDb } from "../cloud-sessions.js" import { fetchCloudSession, fetchCloudSessionForImport, importSessionToDb } from "../cloud-sessions.js" +import { createEditHandler } from "./edit.js" +import { createFimHandler } from "./fim.js" +import { + GatewayError, + UnauthorizedError, + getClawChatCredentials, + getClawStatus, + getCloudSessions, + getNotifications, + getProfile, + setOrganization, +} from "./handlers.js" // Type definitions for OpenCode dependencies (injected at runtime) type Hono = any @@ -26,7 +31,7 @@ type Validator = any type Resolver = any type Errors = any type Auth = any -type ModelCache = { clear: (providerID: string) => void } +type ModelCache = { clear: (providerID: string) => void | Promise } type Z = any interface KiloRoutesDeps extends ImportDeps { @@ -38,11 +43,9 @@ interface KiloRoutesDeps extends ImportDeps { Auth: Auth ModelCache: ModelCache z: Z - InstanceStore: { disposeAllInstances(): Promise } + Instances: { disposeAllInstances(): Promise } } -const FIM_TIMEOUT_MS = 30_000 - /** * Create Kilo Gateway routes with OpenCode dependencies injected * @@ -85,7 +88,7 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { SessionCreatedEvent, Identifier, ModelCache, - InstanceStore, + Instances, } = deps const Organization = z.object({ @@ -110,6 +113,16 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { currentOrgId: z.string().nullable(), }) + const EditCompletionResponse = z.object({ + content: z.string(), + usage: z + .object({ + prompt_tokens: z.number().optional(), + completion_tokens: z.number().optional(), + }) + .optional(), + }) + const FimStreamChunk = z.object({ choices: z .array( @@ -132,6 +145,21 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { cost: z.number().optional(), }) + const TranscriptionResponse = z.object({ + text: z.string(), + usage: z.unknown().optional(), + }) + + const getProxyAuth = async () => { + const auth = await Auth.get("kilo") + const token = auth?.type === "api" ? auth.key : auth?.type === "oauth" ? auth.access : undefined + return { + auth, + token, + organizationId: auth?.type === "oauth" ? auth.accountId : undefined, + } + } + return new Hono() .get( "/profile", @@ -152,24 +180,12 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { }, }), async (c: any) => { - // Get Kilo auth - const auth = await Auth.get("kilo") - - if (!auth || auth.type !== "oauth") { + try { + return c.json(await getProfile(Auth)) + } catch (err) { + if (!(err instanceof UnauthorizedError)) throw err return c.json({ error: "Not authenticated with Kilo Gateway" }, 401) } - - const token = auth.access - const currentOrgId = auth.accountId ?? null - - // Fetch profile and balance in parallel - // Pass organizationId to fetchBalance to get team balance when in org context - const [profile, balance] = await Promise.all([ - fetchProfile(token), - fetchBalance(token, currentOrgId ?? undefined), - ]) - - return c.json({ profile, balance, currentOrgId }) }, ) .post( @@ -199,27 +215,21 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { async (c: any) => { const { organizationId } = c.req.valid("json") - // Get current Kilo auth - const auth = await Auth.get("kilo") - - if (!auth || auth.type !== "oauth") { + try { + return c.json( + await setOrganization( + { + auth: Auth, + clear: () => ModelCache.clear("kilo"), + dispose: () => Instances.disposeAllInstances(), + }, + organizationId, + ), + ) + } catch (err) { + if (!(err instanceof UnauthorizedError)) throw err return c.json({ error: "Not authenticated with Kilo Gateway" }, 401) } - - // Update auth with new organization ID - await Auth.set("kilo", { - type: "oauth", - refresh: auth.refresh, - access: auth.access, - expires: auth.expires, - ...(organizationId && { accountId: organizationId }), - }) - - ModelCache.clear("kilo") - clearModesCache() - await InstanceStore.disposeAllInstances() - - return c.json(true) }, ) .get( @@ -318,74 +328,109 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { z.object({ prefix: z.string(), suffix: z.string(), + provider: z.string().optional(), model: z.string().optional(), maxTokens: z.number().optional(), temperature: z.number().optional(), }), ), + createFimHandler(Auth), + ) + .post( + "/edit", + describeRoute({ + summary: "Next Edit completion", + description: + "Proxy a Mercury-style Next Edit request. The client supplies structured editor " + + "context; the gateway assembles the sentinel-tagged prompt and forwards to the upstream edit endpoint.", + operationId: "kilo.edit", + responses: { + 200: { + description: "Next Edit completion", + content: { + "application/json": { + schema: resolver(EditCompletionResponse), + }, + }, + }, + ...errors(400, 401), + }, + }), + validator( + "json", + z.object({ + provider: z.string().optional(), + model: z.string().optional(), + maxTokens: z.number().optional(), + currentFilePath: z.string(), + currentFileContent: z.string(), + cursorLine: z.number(), + cursorCharacter: z.number(), + editableRegionStartLine: z.number(), + editableRegionEndLine: z.number(), + recentlyViewedSnippets: z.array(z.object({ filepath: z.string(), content: z.string() })), + editDiffHistory: z.array(z.string()), + }), + ), + createEditHandler(Auth), + ) + .post( + "/audio/transcriptions", + describeRoute({ + summary: "Speech to text transcription", + description: "Proxy an audio transcription request to the Kilo Gateway", + operationId: "kilo.audio.transcriptions", + responses: { + 200: { + description: "Transcription response", + content: { + "application/json": { + schema: resolver(TranscriptionResponse), + }, + }, + }, + ...errors(400, 401), + }, + }), + validator( + "json", + z.object({ + model: z.string(), + input_audio: z.object({ + data: z.string(), + format: z.string(), + }), + language: z.string().optional(), + prompt: z.string().optional(), + temperature: z.number().optional(), + }), + ), async (c: any) => { - const auth = await Auth.get("kilo") + const proxy = await getProxyAuth() + if (!proxy.auth) return c.json({ error: "Not authenticated with Kilo Gateway" }, 401) - if (!auth) { - return c.json({ error: "Not authenticated with Kilo Gateway" }, 401) - } - - const token = auth.type === "api" ? auth.key : auth.type === "oauth" ? auth.access : undefined - if (!token) { - return c.json({ error: "No valid token found" }, 401) - } - - const organizationId = auth.type === "oauth" ? auth.accountId : undefined - - const { prefix, suffix, model, maxTokens, temperature } = c.req.valid("json") - const fimModel = model ?? "mistralai/codestral-2501" - const fimMaxTokens = maxTokens ?? 256 - const fimTemperature = temperature ?? 0.2 - - const baseApiUrl = KILO_API_BASE + "/api/" - const endpoint = new URL("fim/completions", baseApiUrl) + if (!proxy.token) return c.json({ error: "No valid token found" }, 401) + const body = c.req.valid("json") const headers = { "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - ...buildKiloHeaders(undefined, { kilocodeOrganizationId: organizationId }), - [HEADER_FEATURE]: "autocomplete", + Authorization: `Bearer ${proxy.token}`, + ...buildKiloHeaders(undefined, { kilocodeOrganizationId: proxy.organizationId }), + [HEADER_FEATURE]: "vscode-extension", } - const signal = AbortSignal.any([c.req.raw.signal, AbortSignal.timeout(FIM_TIMEOUT_MS)]) + const response = await fetch(`${KILO_API_BASE}/api/gateway/v1/audio/transcriptions`, { + method: "POST", + headers, + signal: c.req.raw.signal, + body: JSON.stringify(body), + }) - let response: Response - try { - response = await fetch(endpoint, { - method: "POST", - headers, - signal, - body: JSON.stringify({ - model: fimModel, - prompt: prefix, - suffix, - max_tokens: fimMaxTokens, - temperature: fimTemperature, - stream: true, - }), - }) - } catch (err) { - if (err instanceof DOMException && err.name === "TimeoutError") - return c.json({ error: "FIM request timed out" }, 504 as any) - if (signal.aborted) return c.json({ error: "FIM request canceled" }, 499 as any) - throw err - } - - if (!response.ok) { - const text = await response.text() - return c.json({ error: `FIM request failed: ${response.status} ${text}` }, response.status as any) - } - - return new Response(response.body, { + const text = await response.text() + return new Response(text, { + status: response.status, headers: { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - Connection: "keep-alive", + "Content-Type": response.headers.get("Content-Type") ?? "application/json", }, }) }, @@ -409,19 +454,7 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { }, }), async (c: any) => { - const auth = await Auth.get("kilo") - if (!auth) return c.json([]) - - const token = auth.type === "api" ? auth.key : auth.type === "oauth" ? auth.access : undefined - if (!token) return c.json([]) - - const organizationId = auth.type === "oauth" ? auth.accountId : undefined - const notifications = await fetchKilocodeNotifications({ - kilocodeToken: token, - kilocodeOrganizationId: organizationId, - }) - - return c.json(notifications) + return c.json(await getNotifications(Auth)) }, ) .get( @@ -570,29 +603,11 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { }), async (c: any) => { try { - const auth = await Auth.get("kilo") - if (!auth) return c.json({ error: "Not authenticated with Kilo Gateway" }, 401) - const token = auth.type === "api" ? auth.key : auth.type === "oauth" ? auth.access : undefined - if (!token) return c.json({ error: "No valid token found" }, 401) - - const organizationId = auth.type === "oauth" ? auth.accountId : undefined - const headers: Record = { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - } - if (organizationId) { - headers[HEADER_ORGANIZATIONID] = organizationId - } - - const response = await fetch(`${KILO_API_BASE}/api/kiloclaw/status`, { headers }) - - if (!response.ok) { - const text = await response.text() - return c.json({ error: `KiloClaw request failed: ${response.status} ${text}` }, response.status as any) - } - - return c.json(await response.json()) + return c.json(await getClawStatus(Auth)) } catch (err: any) { + if (err instanceof GatewayError) { + return c.json({ error: `KiloClaw request failed: ${err.status} ${err.message}` }, err.status as any) + } console.error("[Kilo Gateway] claw/status: error", err?.message ?? err) return c.json({ error: "Failed to reach KiloClaw" }, 502) } @@ -629,25 +644,12 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { }, }), async (c: any) => { - const auth = await Auth.get("kilo") - if (!auth) return c.json({ error: "Not authenticated with Kilo Gateway" }, 401) - const token = auth.type === "api" ? auth.key : auth.type === "oauth" ? auth.access : undefined - if (!token) return c.json({ error: "No valid token found" }, 401) - - // For OAuth, expires is a millisecond epoch we already track. For - // API tokens we don't have a verified expiry locally — the JWT is - // signed by the cloud and validated by kilo-chat/event-service on - // every request. Use a far-future placeholder so the client cache - // doesn't refetch unnecessarily; on 401 the client clears the - // cache and prompts re-auth. - const expiresAtMs = auth.type === "oauth" ? auth.expires : Date.now() + 365 * 24 * 60 * 60 * 1000 - - return c.json({ - token, - expiresAt: new Date(expiresAtMs).toISOString(), - kiloChatUrl: KILO_CHAT_URL, - eventServiceUrl: KILO_EVENT_SERVICE_URL, - }) + try { + return c.json(await getClawChatCredentials(Auth)) + } catch (err) { + if (!(err instanceof UnauthorizedError)) throw err + return c.json({ error: "Not authenticated with Kilo Gateway" }, 401) + } }, ) .get( @@ -697,63 +699,9 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { const token = auth.type === "api" ? auth.key : auth.type === "oauth" ? auth.access : undefined if (!token) return c.json({ error: "No valid token found" }, 401) - const { cursor, limit, gitUrl } = c.req.valid("query") - - const input: Record = {} - if (cursor) input.cursor = cursor - if (limit) input.limit = limit - if (gitUrl) input.gitUrl = gitUrl - - const params = new URLSearchParams({ - batch: "1", - input: JSON.stringify({ "0": input }), - }) - - const url = `${KILO_API_BASE}/api/trpc/cliSessionsV2.list?${params.toString()}` - - const response = await fetch(url, { - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - ...buildKiloHeaders(), - }, - }) - - if (!response.ok) { - const text = await response.text() - console.error("[Kilo Gateway] cloud-sessions: tRPC request failed", { - status: response.status, - body: text.slice(0, 500), - }) - return c.json({ error: `Cloud sessions fetch failed: ${response.status}` }, response.status as any) - } - - const raw = await response.text() - const json = JSON.parse(raw) - const data = Array.isArray(json) ? json[0]?.result?.data : null - const result = data?.json ?? data - if (!result) return c.json({ cliSessions: [], nextCursor: null }) - - const sessions = (result.cliSessions ?? []).map((s: any) => ({ - session_id: s.session_id, - title: s.title ?? null, - created_at: - typeof s.created_at === "string" - ? s.created_at - : s.created_at - ? new Date(s.created_at).toISOString() - : new Date().toISOString(), - updated_at: - typeof s.updated_at === "string" - ? s.updated_at - : s.updated_at - ? new Date(s.updated_at).toISOString() - : new Date().toISOString(), - version: s.version ?? 0, - })) - - return c.json({ cliSessions: sessions, nextCursor: result.nextCursor ?? null }) + return c.json(await getCloudSessions(token, c.req.valid("query"))) } catch (err: any) { + if (err instanceof GatewayError) return c.json({ error: err.message }, err.status as any) console.error("[Kilo Gateway] cloud-sessions: unhandled error", err?.message ?? err) return c.json({ error: "Internal error" }, 500) } diff --git a/packages/kilo-gateway/test/autocomplete.test.ts b/packages/kilo-gateway/test/autocomplete.test.ts new file mode 100644 index 00000000000..91ba6226b9d --- /dev/null +++ b/packages/kilo-gateway/test/autocomplete.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, test } from "bun:test" +import { + AUTOCOMPLETE_MODELS, + DEFAULT_AUTOCOMPLETE_MODEL, + DEFAULT_AUTOCOMPLETE_MODEL_ID, + DEFAULT_AUTOCOMPLETE_PROVIDER_ID, +} from "../src/autocomplete" + +describe("DEFAULT_AUTOCOMPLETE_MODEL", () => { + test("resolves to an entry that exists in AUTOCOMPLETE_MODELS", () => { + const match = AUTOCOMPLETE_MODELS.find( + (m) => m.providerID === DEFAULT_AUTOCOMPLETE_PROVIDER_ID && m.modelID === DEFAULT_AUTOCOMPLETE_MODEL_ID, + ) + expect(match).toBeDefined() + expect(DEFAULT_AUTOCOMPLETE_MODEL).toBe(match!) + }) +}) diff --git a/packages/kilo-gateway/test/edit-prompt.test.ts b/packages/kilo-gateway/test/edit-prompt.test.ts new file mode 100644 index 00000000000..6aafce62a69 --- /dev/null +++ b/packages/kilo-gateway/test/edit-prompt.test.ts @@ -0,0 +1,92 @@ +import { describe, expect, test } from "bun:test" +import { + buildMercuryEditPrompt, + currentFileContentBlock, + editDiffHistoryBlock, + recentlyViewedSnippetsBlock, +} from "../src/edit-prompt" + +describe("recentlyViewedSnippetsBlock", () => { + test("wraps in open/close sentinels even when empty", () => { + const out = recentlyViewedSnippetsBlock([]) + expect(out.startsWith("<|recently_viewed_code_snippets|>")).toBe(true) + expect(out.endsWith("<|/recently_viewed_code_snippets|>")).toBe(true) + }) + + test("emits one inner block per snippet with the file-path header", () => { + const out = recentlyViewedSnippetsBlock([ + { filepath: "src/a.ts", content: "const a = 1" }, + { filepath: "src/b.ts", content: "const b = 2" }, + ]) + expect(out).toContain("code_snippet_file_path: src/a.ts") + expect(out).toContain("code_snippet_file_path: src/b.ts") + expect(out).toContain("const a = 1") + expect(out).toContain("const b = 2") + }) +}) + +describe("currentFileContentBlock", () => { + test("inserts <|cursor|> at the right character and wraps the editable region", () => { + const file = ["function foo() {", " return 1", "}"].join("\n") + const out = currentFileContentBlock("src/foo.ts", file, 1, 1, 1, 2) + expect(out).toContain("<|current_file_content|>") + expect(out).toContain("<|/current_file_content|>") + expect(out).toContain("current_file_path: src/foo.ts") + expect(out).toContain(" <|cursor|>return 1") + const openIdx = out.indexOf("<|code_to_edit|>") + const lineIdx = out.indexOf("return 1") + const closeIdx = out.indexOf("<|/code_to_edit|>") + expect(openIdx).toBeGreaterThan(-1) + expect(closeIdx).toBeGreaterThan(openIdx) + expect(lineIdx).toBeGreaterThan(openIdx) + expect(lineIdx).toBeLessThan(closeIdx) + }) + + test("clamps an out-of-range cursor instead of throwing", () => { + const out = currentFileContentBlock("p.ts", "only-line", 0, 0, 0, 9999) + expect(out).toContain("only-line<|cursor|>") + }) +}) + +describe("editDiffHistoryBlock", () => { + test("strips the createPatch index+separator lines from each diff", () => { + const fakeDiff = ["Index: foo.ts", "===", "@@ -1,1 +1,1 @@", "-old", "+new"].join("\n") + const out = editDiffHistoryBlock([fakeDiff]) + expect(out).toContain("@@ -1,1 +1,1 @@") + expect(out).not.toContain("Index: foo.ts") + expect(out.startsWith("<|edit_diff_history|>")).toBe(true) + expect(out.endsWith("<|/edit_diff_history|>")).toBe(true) + }) + + test("separates multiple diffs with a blank line", () => { + const diff1 = ["Index: a.ts", "===", "@@ -1,1 +1,1 @@", "-a", "+aa"].join("\n") + const diff2 = ["Index: b.ts", "===", "@@ -2,1 +2,1 @@", "-b", "+bb"].join("\n") + const out = editDiffHistoryBlock([diff1, diff2]) + const idx1 = out.indexOf("@@ -1,1 +1,1 @@") + const idx2 = out.indexOf("@@ -2,1 +2,1 @@") + expect(idx2).toBeGreaterThan(idx1) + expect(out.slice(idx1, idx2)).toContain("\n\n") + }) +}) + +describe("buildMercuryEditPrompt", () => { + test("assembles the three blocks in order and ends with the NES token", () => { + const out = buildMercuryEditPrompt({ + currentFilePath: "p.ts", + currentFileContent: "a\nb\nc", + cursorLine: 1, + cursorCharacter: 0, + editableRegionStartLine: 1, + editableRegionEndLine: 1, + recentlyViewedSnippets: [], + editDiffHistory: [], + }) + const snippetsIdx = out.indexOf("<|recently_viewed_code_snippets|>") + const fileIdx = out.indexOf("<|current_file_content|>") + const diffIdx = out.indexOf("<|edit_diff_history|>") + expect(snippetsIdx).toBeGreaterThan(-1) + expect(fileIdx).toBeGreaterThan(snippetsIdx) + expect(diffIdx).toBeGreaterThan(fileIdx) + expect(out.endsWith("<|!@#IS_NEXT_EDIT!@#|>")).toBe(true) + }) +}) diff --git a/packages/kilo-gateway/test/edit.test.ts b/packages/kilo-gateway/test/edit.test.ts new file mode 100644 index 00000000000..0d240f70a88 --- /dev/null +++ b/packages/kilo-gateway/test/edit.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, test } from "bun:test" +import { extractFencedBody, resolveEditTarget } from "../src/edit" + +describe("Edit target resolution", () => { + test("routes the Inception next-edit model to Inception's edit endpoint", () => { + expect(resolveEditTarget("inception", "mercury-next-edit")).toEqual({ + provider: "inception", + model: "mercury-edit-2", + url: "https://api.inceptionlabs.ai/v1/edit/completions", + }) + }) + + test("does NOT route the FIM Mercury model to the edit endpoint", () => { + // `mercury-edit-2` (kind: fim) must fall through to the kilo placeholder, + // not the edit endpoint — only `mercury-next-edit` (kind: edit) is NES. + expect(resolveEditTarget("inception", "mercury-edit-2").provider).toBe("kilo") + }) + + test("routes the Kilo Gateway next-edit model to the gateway proxy", () => { + const target = resolveEditTarget("kilo", "inception/mercury-next-edit") + expect(target.provider).toBe("kilo") + expect(target.model).toBe("inception/mercury-edit-2") + expect(target.url).toMatch(/\/api\/edit\/completions$/) + }) + + test("falls back to a kilo placeholder (no upstream) for non-edit models", () => { + expect(resolveEditTarget("kilo", "mistralai/codestral-2508")).toEqual({ + provider: "kilo", + model: "mistralai/codestral-2508", + url: "", + }) + expect(resolveEditTarget()).toMatchObject({ provider: "kilo", url: "" }) + }) +}) + +describe("extractFencedBody", () => { + test("extracts a plain triple-backtick fenced body", () => { + expect(extractFencedBody("```\nconst x = 1\n```")).toBe("const x = 1") + }) + + test("handles a language tag on the opening fence", () => { + expect(extractFencedBody("```typescript\nconst x = 1\n```")).toBe("const x = 1") + }) + + test("strips embedded <|code_to_edit|> sentinels", () => { + expect(extractFencedBody("```\n<|code_to_edit|>\nconst x = 2\n<|/code_to_edit|>\n```")).toBe("const x = 2") + }) + + test("returns the raw message when there is no fence", () => { + expect(extractFencedBody("just text, no fence")).toBe("just text, no fence") + }) + + test("returns the empty string for empty input", () => { + expect(extractFencedBody("")).toBe("") + }) + + test("suppresses a replacement when the closing fence is missing", () => { + expect(extractFencedBody("```\nconst x = 1\nconst y = ")).toBe("") + }) + + test("preserves internal blank lines and indentation", () => { + const body = "def f():\n if True:\n\n return 1" + expect(extractFencedBody("```python\n" + body + "\n```")).toBe(body) + }) +}) diff --git a/packages/kilo-gateway/test/fim.test.ts b/packages/kilo-gateway/test/fim.test.ts new file mode 100644 index 00000000000..86d7e08b556 --- /dev/null +++ b/packages/kilo-gateway/test/fim.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, test } from "bun:test" +import { resolveFimTarget } from "../src/fim" + +describe("FIM target resolution", () => { + test("keeps gateway autocomplete models on Kilo Gateway", () => { + expect(resolveFimTarget("kilo", "mistralai/codestral-2508")).toEqual({ + provider: "kilo", + model: "mistralai/codestral-2508", + url: "https://api.kilo.ai/api/fim/completions", + }) + expect(resolveFimTarget("kilo", "inception/mercury-edit-2")).toEqual({ + provider: "kilo", + model: "inception/mercury-edit-2", + url: "https://api.kilo.ai/api/fim/completions", + }) + }) + + test("routes explicit provider autocomplete models directly", () => { + expect(resolveFimTarget("mistral", "codestral-2508")).toEqual({ + provider: "mistral", + model: "codestral-2508", + }) + expect(resolveFimTarget("inception", "mercury-edit-2")).toEqual({ + provider: "inception", + model: "mercury-edit-2", + url: "https://api.inceptionlabs.ai/v1/fim/completions", + }) + }) + + test("preserves gateway model pass-through behavior", () => { + expect(resolveFimTarget()).toEqual({ + provider: "kilo", + model: "mistralai/codestral-2501", + url: "https://api.kilo.ai/api/fim/completions", + }) + expect(resolveFimTarget(undefined, "mistralai/codestral-2508")).toEqual({ + provider: "kilo", + model: "mistralai/codestral-2508", + url: "https://api.kilo.ai/api/fim/completions", + }) + expect(resolveFimTarget(undefined, "inception/mercury-edit")).toEqual({ + provider: "kilo", + model: "inception/mercury-edit", + url: "https://api.kilo.ai/api/fim/completions", + }) + expect(resolveFimTarget("kilo", "custom/fim-model")).toEqual({ + provider: "kilo", + model: "custom/fim-model", + url: "https://api.kilo.ai/api/fim/completions", + }) + }) +}) diff --git a/packages/kilo-gateway/test/mistral-fim-endpoint.test.ts b/packages/kilo-gateway/test/mistral-fim-endpoint.test.ts new file mode 100644 index 00000000000..d10c6177287 --- /dev/null +++ b/packages/kilo-gateway/test/mistral-fim-endpoint.test.ts @@ -0,0 +1,75 @@ +import { describe, expect, test } from "bun:test" +import { + CODESTRAL_FIM_URL, + MISTRAL_FIM_URL, + clearMistralFimEndpointCache, + getCachedMistralFimEndpoint, + requestMistralFim, +} from "../src/mistral-fim-endpoint" + +function response(status: number) { + return new Response(null, { status }) +} + +describe("Mistral FIM endpoint cache", () => { + test("remembers Codestral endpoint after successful fallback", async () => { + clearMistralFimEndpointCache() + const urls: string[] = [] + const first = await requestMistralFim(async (url) => { + urls.push(url) + return response(url === MISTRAL_FIM_URL ? 401 : 200) + }) + const second = await requestMistralFim(async (url) => { + urls.push(url) + return response(200) + }) + + expect(first.ok).toBe(true) + expect(second.ok).toBe(true) + expect(urls).toEqual([MISTRAL_FIM_URL, CODESTRAL_FIM_URL, CODESTRAL_FIM_URL]) + expect(getCachedMistralFimEndpoint()).toBe(CODESTRAL_FIM_URL) + }) + + test("does not remember fallback for invalid credentials", async () => { + clearMistralFimEndpointCache() + const urls: string[] = [] + const res = await requestMistralFim(async (url) => { + urls.push(url) + return response(401) + }) + + expect(res.status).toBe(401) + expect(urls).toEqual([MISTRAL_FIM_URL, CODESTRAL_FIM_URL]) + expect(getCachedMistralFimEndpoint()).toBeUndefined() + }) + + test("uses one process-local endpoint preference", async () => { + clearMistralFimEndpointCache() + const urls: string[] = [] + await requestMistralFim(async (url) => { + urls.push(url) + return response(url === MISTRAL_FIM_URL ? 403 : 200) + }) + await requestMistralFim(async (url) => { + urls.push(url) + return response(200) + }) + + expect(urls).toEqual([MISTRAL_FIM_URL, CODESTRAL_FIM_URL, CODESTRAL_FIM_URL]) + expect(getCachedMistralFimEndpoint()).toBe(CODESTRAL_FIM_URL) + }) + + test("clears stale preference and probes alternate endpoint", async () => { + clearMistralFimEndpointCache() + const urls: string[] = [] + await requestMistralFim(async (url) => response(url === MISTRAL_FIM_URL ? 401 : 200)) + const res = await requestMistralFim(async (url) => { + urls.push(url) + return response(url === CODESTRAL_FIM_URL ? 401 : 200) + }) + + expect(res.ok).toBe(true) + expect(urls).toEqual([CODESTRAL_FIM_URL, MISTRAL_FIM_URL]) + expect(getCachedMistralFimEndpoint()).toBe(MISTRAL_FIM_URL) + }) +}) 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..d4a38188187 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.12", "type": "module", "license": "MIT", "description": "Kilo-specific i18n translations and overrides", @@ -16,5 +16,7 @@ "@types/bun": "catalog:", "typescript": "catalog:", "@typescript/native-preview": "catalog:" - } + }, + "dependencies": {}, + "peerDependencies": {} } diff --git a/packages/kilo-i18n/src/ar.ts b/packages/kilo-i18n/src/ar.ts index 7dd1f9304fa..87f1c525f82 100644 --- a/packages/kilo-i18n/src/ar.ts +++ b/packages/kilo-i18n/src/ar.ts @@ -19,7 +19,6 @@ export const dict = { // Marketplace "marketplace.tab.skills": "المهارات", "marketplace.tab.mcpServers": "خوادم MCP", - "marketplace.tab.modes": "الأوضاع", "marketplace.category.all": "الكل", "marketplace.placeholder": "سيتم تنفيذه لاحقاً", "marketplace.card.installed": "مثبت", @@ -45,6 +44,7 @@ export const dict = { "marketplace.remove.cancel": "إلغاء", "marketplace.remove.confirm.button": "إزالة", "marketplace.tab.mcp": "MCP", + "marketplace.tab.agents": "وكلاء", "marketplace.search": "بحث...", "marketplace.filter.all": "جميع العناصر", "marketplace.filter.notInstalled": "غير مثبت", @@ -60,7 +60,7 @@ export const dict = { "marketplace.scope.global": "عام", "marketplace.remove.type.mcp": "خادم MCP", "marketplace.remove.type.skill": "مهارة", - "marketplace.remove.type.mode": "وضع", + "marketplace.remove.type.agent": "وكيل", "marketplace.remove.failed": "فشلت إزالة {{name}}", "marketplace.install": "تثبيت", "marketplace.filter.installed": "مثبت", @@ -69,8 +69,10 @@ export const dict = { "marketplace.warning.busyMany": "عدة جلسات تعمل وستتوقف", "marketplace.warning.installAnyway": "تثبيت على أي حال", "marketplace.warning.cancel": "إلغاء", - "marketplace.contribute.prompt": "هل تفتقد مهارة أو وضعًا أو خادم MCP؟", + "marketplace.contribute.prompt": "هل تفتقد مهارة أو وكيلاً أو خادم MCP؟", "marketplace.contribute.cta": "ساهم على GitHub", + "marketplace.migration.notice": + "تم استبدال الأوضاع بالوكلاء. إذا كنت قد قمت بتثبيت أوضاع السوق سابقاً، يرجى إزالتها وإعادة تثبيتها كوكلاء للانتقال إلى التنسيق الجديد.", // Plan follow-up question shown after plan_exit "plan.followup.header": "نفّذ", @@ -91,7 +93,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "أوقف لقطات Kilo لهذا المشروع. ستفقد ميزة التراجع/الإعادة لتعديلات Kilo، لكن git سيستمر في تتبع كل شيء.", + // Edit-tool header and shell-tool section labels "ui.messagePart.openInDiffViewer": "فتح في عارض الفروقات", + "ui.messagePart.shell.command": "الأمر", + "ui.messagePart.shell.output": "المخرجات", + "ui.messagePart.openInEditor": "فتح في المحرر", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "كان هذا مفيدًا", diff --git a/packages/kilo-i18n/src/br.ts b/packages/kilo-i18n/src/br.ts index e79ef39bf30..21864df9338 100644 --- a/packages/kilo-i18n/src/br.ts +++ b/packages/kilo-i18n/src/br.ts @@ -19,7 +19,6 @@ export const dict = { // Marketplace "marketplace.tab.skills": "Skills", "marketplace.tab.mcpServers": "Servidores MCP", - "marketplace.tab.modes": "Modos", "marketplace.category.all": "Todos", "marketplace.placeholder": "A ser implementado", "marketplace.card.installed": "Instalado", @@ -45,6 +44,7 @@ export const dict = { "marketplace.remove.cancel": "Cancelar", "marketplace.remove.confirm.button": "Remover", "marketplace.tab.mcp": "MCP", + "marketplace.tab.agents": "Agentes", "marketplace.search": "Pesquisar...", "marketplace.filter.all": "Todos os Itens", "marketplace.filter.notInstalled": "Não Instalado", @@ -60,7 +60,7 @@ export const dict = { "marketplace.scope.global": "global", "marketplace.remove.type.mcp": "servidor MCP", "marketplace.remove.type.skill": "habilidade", - "marketplace.remove.type.mode": "modo", + "marketplace.remove.type.agent": "agente", "marketplace.remove.failed": "Falha ao remover {{name}}", "marketplace.install": "Instalar", "marketplace.filter.installed": "Instalado", @@ -69,8 +69,10 @@ export const dict = { "marketplace.warning.busyMany": "Várias sessões estão em execução e serão interrompidas", "marketplace.warning.installAnyway": "Instalar mesmo assim", "marketplace.warning.cancel": "Cancelar", - "marketplace.contribute.prompt": "Está faltando uma skill, modo ou servidor MCP?", + "marketplace.contribute.prompt": "Está faltando uma skill, agente ou servidor MCP?", "marketplace.contribute.cta": "Contribuir no GitHub", + "marketplace.migration.notice": + "Os modos foram substituídos por agentes. Se você instalou modos do marketplace anteriormente, remova-os e reinstale-os como agentes para migrar para o novo formato.", // Plan follow-up question shown after plan_exit "plan.followup.header": "Implementar", @@ -91,7 +93,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "Desligue os snapshots do Kilo para este projeto. Você perde desfazer/refazer das mudanças feitas pelo Kilo, mas o git continua rastreando tudo.", + // Edit-tool header and shell-tool section labels "ui.messagePart.openInDiffViewer": "Abrir no Visualizador de Diferenças", + "ui.messagePart.shell.command": "Comando", + "ui.messagePart.shell.output": "Saída", + "ui.messagePart.openInEditor": "Abrir no Editor", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "Isso foi útil", diff --git a/packages/kilo-i18n/src/bs.ts b/packages/kilo-i18n/src/bs.ts index e23e964bbd5..e517c1352f2 100644 --- a/packages/kilo-i18n/src/bs.ts +++ b/packages/kilo-i18n/src/bs.ts @@ -24,7 +24,6 @@ export const dict = { // Marketplace "marketplace.tab.skills": "Vještine", "marketplace.tab.mcpServers": "MCP Serveri", - "marketplace.tab.modes": "Modovi", "marketplace.category.all": "Sve", "marketplace.placeholder": "Biće implementirano", "marketplace.card.installed": "Instalirano", @@ -50,6 +49,7 @@ export const dict = { "marketplace.remove.cancel": "Otkaži", "marketplace.remove.confirm.button": "Ukloni", "marketplace.tab.mcp": "MCP", + "marketplace.tab.agents": "Agenti", "marketplace.search": "Pretraži...", "marketplace.filter.all": "Sve stavke", "marketplace.filter.notInstalled": "Nije instalirano", @@ -65,7 +65,7 @@ export const dict = { "marketplace.scope.global": "globalno", "marketplace.remove.type.mcp": "MCP server", "marketplace.remove.type.skill": "vještina", - "marketplace.remove.type.mode": "režim", + "marketplace.remove.type.agent": "agent", "marketplace.remove.failed": "Uklanjanje {{name}} nije uspjelo", "marketplace.install": "Instaliraj", "marketplace.filter.installed": "Instalirano", @@ -74,8 +74,10 @@ export const dict = { "marketplace.warning.busyMany": "Nekoliko sesija je pokrenuto i bit će prekinuto", "marketplace.warning.installAnyway": "Instaliraj svejedno", "marketplace.warning.cancel": "Otkaži", - "marketplace.contribute.prompt": "Nedostaje vještina, način rada ili MCP server?", + "marketplace.contribute.prompt": "Nedostaje vještina, agent ili MCP server?", "marketplace.contribute.cta": "Doprinesi na GitHub-u", + "marketplace.migration.notice": + "Modovi su zamijenjeni agentima. Ako ste prethodno instalirali marketplace modove, uklonite ih i ponovo instalirajte kao agente da biste prešli na novi format.", // Plan follow-up question shown after plan_exit "plan.followup.header": "Implementiraj", @@ -96,7 +98,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "Isključi Kilo snapshotove za ovaj projekat. Izgubićete poništi/vrati za izmjene koje napravi Kilo, ali git i dalje prati sve.", + // Edit-tool header and shell-tool section labels "ui.messagePart.openInDiffViewer": "Otvori u pregledniku razlika", + "ui.messagePart.shell.command": "Naredba", + "ui.messagePart.shell.output": "Izlaz", + "ui.messagePart.openInEditor": "Otvori u editoru", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "Ovo je bilo korisno", diff --git a/packages/kilo-i18n/src/da.ts b/packages/kilo-i18n/src/da.ts index 2a086970b40..b8189401bd6 100644 --- a/packages/kilo-i18n/src/da.ts +++ b/packages/kilo-i18n/src/da.ts @@ -19,7 +19,6 @@ export const dict = { // Marketplace "marketplace.tab.skills": "Skills", "marketplace.tab.mcpServers": "MCP-servere", - "marketplace.tab.modes": "Tilstande", "marketplace.category.all": "Alle", "marketplace.placeholder": "Skal implementeres", "marketplace.card.installed": "Installeret", @@ -45,6 +44,7 @@ export const dict = { "marketplace.remove.cancel": "Annuller", "marketplace.remove.confirm.button": "Fjern", "marketplace.tab.mcp": "MCP", + "marketplace.tab.agents": "Agenter", "marketplace.search": "Søg...", "marketplace.filter.all": "Alle elementer", "marketplace.filter.notInstalled": "Ikke installeret", @@ -60,7 +60,7 @@ export const dict = { "marketplace.scope.global": "global", "marketplace.remove.type.mcp": "MCP-server", "marketplace.remove.type.skill": "færdighed", - "marketplace.remove.type.mode": "tilstand", + "marketplace.remove.type.agent": "agent", "marketplace.remove.failed": "Kunne ikke fjerne {{name}}", "marketplace.install": "Installer", "marketplace.filter.installed": "Installeret", @@ -69,8 +69,10 @@ export const dict = { "marketplace.warning.busyMany": "Flere sessioner kører og vil blive afbrudt", "marketplace.warning.installAnyway": "Installer alligevel", "marketplace.warning.cancel": "Annuller", - "marketplace.contribute.prompt": "Mangler du en skill, tilstand eller MCP-server?", + "marketplace.contribute.prompt": "Mangler du en skill, agent eller MCP-server?", "marketplace.contribute.cta": "Bidrag på GitHub", + "marketplace.migration.notice": + "Tilstande er blevet erstattet af agenter. Hvis du tidligere har installeret marketplace-tilstande, skal du fjerne dem og geninstallere dem som agenter for at migrere til det nye format.", // Plan follow-up question shown after plan_exit "plan.followup.header": "Implementér", @@ -91,7 +93,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "Slå Kilos snapshots fra for dette projekt. Du mister fortryd/gentag for Kilo-ændringer, men git sporer stadig alt.", - "ui.messagePart.openInDiffViewer": "Åbn i diff-viser", + // Edit-tool header and shell-tool section labels + "ui.messagePart.openInDiffViewer": "Åbn i Diff-visning", + "ui.messagePart.shell.command": "Kommando", + "ui.messagePart.shell.output": "Output", + "ui.messagePart.openInEditor": "Åbn i editor", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "Dette var nyttigt", diff --git a/packages/kilo-i18n/src/de.ts b/packages/kilo-i18n/src/de.ts index 437f5fc649f..a65837305ff 100644 --- a/packages/kilo-i18n/src/de.ts +++ b/packages/kilo-i18n/src/de.ts @@ -19,7 +19,6 @@ export const dict = { // Marketplace "marketplace.tab.skills": "Skills", "marketplace.tab.mcpServers": "MCP-Server", - "marketplace.tab.modes": "Modi", "marketplace.category.all": "Alle", "marketplace.placeholder": "Noch nicht implementiert", "marketplace.card.installed": "Installiert", @@ -45,6 +44,7 @@ export const dict = { "marketplace.remove.cancel": "Abbrechen", "marketplace.remove.confirm.button": "Entfernen", "marketplace.tab.mcp": "MCP", + "marketplace.tab.agents": "Agenten", "marketplace.search": "Suchen...", "marketplace.filter.all": "Alle Elemente", "marketplace.filter.notInstalled": "Nicht installiert", @@ -60,7 +60,7 @@ export const dict = { "marketplace.scope.global": "Global", "marketplace.remove.type.mcp": "MCP-Server", "marketplace.remove.type.skill": "Skill", - "marketplace.remove.type.mode": "Modus", + "marketplace.remove.type.agent": "Agent", "marketplace.remove.failed": "Fehler beim Entfernen von {{name}}", "marketplace.install": "Installieren", "marketplace.filter.installed": "Installiert", @@ -69,8 +69,10 @@ export const dict = { "marketplace.warning.busyMany": "Mehrere Sitzungen laufen und werden unterbrochen", "marketplace.warning.installAnyway": "Trotzdem installieren", "marketplace.warning.cancel": "Abbrechen", - "marketplace.contribute.prompt": "Fehlt ein Skill, Modus oder MCP-Server?", + "marketplace.contribute.prompt": "Fehlt ein Skill, Agent oder MCP-Server?", "marketplace.contribute.cta": "Auf GitHub beitragen", + "marketplace.migration.notice": + "Modi wurden durch Agenten ersetzt. Wenn Sie zuvor Marketplace-Modi installiert haben, entfernen Sie diese bitte und installieren Sie sie als Agenten neu, um zum neuen Format zu migrieren.", // Plan follow-up question shown after plan_exit "plan.followup.header": "Umsetzen", @@ -91,7 +93,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "Kilo-Snapshots für dieses Projekt ausschalten. Rückgängig/Wiederherstellen für Kilo-Änderungen ist nicht mehr möglich, aber git verfolgt weiterhin alles.", + // Edit-tool header and shell-tool section labels "ui.messagePart.openInDiffViewer": "Im Diff-Viewer öffnen", + "ui.messagePart.shell.command": "Befehl", + "ui.messagePart.shell.output": "Ausgabe", + "ui.messagePart.openInEditor": "Im Editor öffnen", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "Das war hilfreich", diff --git a/packages/kilo-i18n/src/en.ts b/packages/kilo-i18n/src/en.ts index bd80d92857d..e2bc7de26d5 100644 --- a/packages/kilo-i18n/src/en.ts +++ b/packages/kilo-i18n/src/en.ts @@ -21,7 +21,6 @@ export const dict = { // Marketplace "marketplace.tab.skills": "Skills", "marketplace.tab.mcpServers": "MCP Servers", - "marketplace.tab.modes": "Modes", "marketplace.category.all": "All", "marketplace.placeholder": "To be implemented", "marketplace.card.installed": "Installed", @@ -47,6 +46,7 @@ export const dict = { "marketplace.remove.cancel": "Cancel", "marketplace.remove.confirm.button": "Remove", "marketplace.tab.mcp": "MCP", + "marketplace.tab.agents": "Agents", "marketplace.search": "Search...", "marketplace.filter.all": "All Items", "marketplace.filter.notInstalled": "Not Installed", @@ -62,7 +62,7 @@ export const dict = { "marketplace.scope.global": "global", "marketplace.remove.type.mcp": "MCP server", "marketplace.remove.type.skill": "skill", - "marketplace.remove.type.mode": "mode", + "marketplace.remove.type.agent": "agent", "marketplace.remove.failed": "Failed to remove {{name}}", "marketplace.install": "Install", "marketplace.filter.installed": "Installed", @@ -71,8 +71,10 @@ export const dict = { "marketplace.warning.busyMany": "Several sessions are running and will be interrupted", "marketplace.warning.installAnyway": "Install anyway", "marketplace.warning.cancel": "Cancel", - "marketplace.contribute.prompt": "Missing a skill, mode, or MCP server?", + "marketplace.contribute.prompt": "Missing a skill, agent, or MCP server?", "marketplace.contribute.cta": "Contribute on GitHub", + "marketplace.migration.notice": + "Modes have been replaced by agents. If you previously installed marketplace modes, please remove and reinstall them as agents to migrate to the new format.", // Plan follow-up question shown after plan_exit. The English strings here must match // the canonical `label`/`header`/`question` sent by the backend — those canonical labels @@ -99,6 +101,10 @@ export const dict = { // Edit-tool header: hover-revealed action opening the diff in a full tab. "ui.messagePart.openInDiffViewer": "Open in Diff Viewer", + // Shell-tool section labels and actions. + "ui.messagePart.shell.command": "Command", + "ui.messagePart.shell.output": "Output", + "ui.messagePart.openInEditor": "Open in Editor", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "This was helpful", diff --git a/packages/kilo-i18n/src/es.ts b/packages/kilo-i18n/src/es.ts index 8b05522cb9f..8312118c26a 100644 --- a/packages/kilo-i18n/src/es.ts +++ b/packages/kilo-i18n/src/es.ts @@ -19,7 +19,6 @@ export const dict = { // Marketplace "marketplace.tab.skills": "Skills", "marketplace.tab.mcpServers": "Servidores MCP", - "marketplace.tab.modes": "Modos", "marketplace.category.all": "Todos", "marketplace.placeholder": "Por implementar", "marketplace.card.installed": "Instalado", @@ -45,6 +44,7 @@ export const dict = { "marketplace.remove.cancel": "Cancelar", "marketplace.remove.confirm.button": "Eliminar", "marketplace.tab.mcp": "MCP", + "marketplace.tab.agents": "Agentes", "marketplace.search": "Buscar...", "marketplace.filter.all": "Todos los elementos", "marketplace.filter.notInstalled": "No instalado", @@ -60,7 +60,7 @@ export const dict = { "marketplace.scope.global": "global", "marketplace.remove.type.mcp": "servidor MCP", "marketplace.remove.type.skill": "habilidad", - "marketplace.remove.type.mode": "modo", + "marketplace.remove.type.agent": "agente", "marketplace.remove.failed": "Error al eliminar {{name}}", "marketplace.install": "Instalar", "marketplace.filter.installed": "Instalado", @@ -69,8 +69,10 @@ export const dict = { "marketplace.warning.busyMany": "Varias sesiones están en ejecución y se interrumpirán", "marketplace.warning.installAnyway": "Instalar de todas formas", "marketplace.warning.cancel": "Cancelar", - "marketplace.contribute.prompt": "¿Falta una skill, modo o servidor MCP?", + "marketplace.contribute.prompt": "¿Falta una skill, agente o servidor MCP?", "marketplace.contribute.cta": "Contribuir en GitHub", + "marketplace.migration.notice": + "Los modos han sido reemplazados por agentes. Si anteriormente instalaste modos del marketplace, elimínalos y reinstálalos como agentes para migrar al nuevo formato.", // Plan follow-up question shown after plan_exit "plan.followup.header": "Implementar", @@ -91,7 +93,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "Apaga las instantáneas de Kilo para este proyecto. Perderás deshacer/rehacer de los cambios de Kilo, pero git seguirá rastreando todo.", + // Edit-tool header and shell-tool section labels "ui.messagePart.openInDiffViewer": "Abrir en el visor de diferencias", + "ui.messagePart.shell.command": "Comando", + "ui.messagePart.shell.output": "Salida", + "ui.messagePart.openInEditor": "Abrir en el editor", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "Esto fue útil", diff --git a/packages/kilo-i18n/src/fr.ts b/packages/kilo-i18n/src/fr.ts index b64ace8f1f9..510c0c7df91 100644 --- a/packages/kilo-i18n/src/fr.ts +++ b/packages/kilo-i18n/src/fr.ts @@ -19,7 +19,6 @@ export const dict = { // Marketplace "marketplace.tab.skills": "Skills", "marketplace.tab.mcpServers": "Serveurs MCP", - "marketplace.tab.modes": "Modes", "marketplace.category.all": "Tout", "marketplace.placeholder": "À implémenter", "marketplace.card.installed": "Installé", @@ -45,6 +44,7 @@ export const dict = { "marketplace.remove.cancel": "Annuler", "marketplace.remove.confirm.button": "Supprimer", "marketplace.tab.mcp": "MCP", + "marketplace.tab.agents": "Agents", "marketplace.search": "Rechercher...", "marketplace.filter.all": "Tous les éléments", "marketplace.filter.notInstalled": "Non installé", @@ -60,7 +60,7 @@ export const dict = { "marketplace.scope.global": "global", "marketplace.remove.type.mcp": "serveur MCP", "marketplace.remove.type.skill": "compétence", - "marketplace.remove.type.mode": "mode", + "marketplace.remove.type.agent": "agent", "marketplace.remove.failed": "Échec de la suppression de {{name}}", "marketplace.install": "Installer", "marketplace.filter.installed": "Installé", @@ -69,8 +69,10 @@ export const dict = { "marketplace.warning.busyMany": "Plusieurs sessions sont en cours et seront interrompues", "marketplace.warning.installAnyway": "Installer quand même", "marketplace.warning.cancel": "Annuler", - "marketplace.contribute.prompt": "Il manque une skill, un mode ou un serveur MCP ?", + "marketplace.contribute.prompt": "Il manque une compétence, un agent ou un serveur MCP ?", "marketplace.contribute.cta": "Contribuer sur GitHub", + "marketplace.migration.notice": + "Les modes ont été remplacés par des agents. Si vous avez précédemment installé des modes depuis le marketplace, veuillez les supprimer et les réinstaller en tant qu'agents pour migrer vers le nouveau format.", // Plan follow-up question shown after plan_exit "plan.followup.header": "Implémenter", @@ -91,7 +93,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "Désactivez les instantanés Kilo pour ce projet. Vous perdez l'annulation/restauration des modifications faites par Kilo, mais git continue de tout suivre.", - "ui.messagePart.openInDiffViewer": "Ouvrir dans la visionneuse de différences", + // Edit-tool header and shell-tool section labels + "ui.messagePart.openInDiffViewer": "Ouvrir dans le visualiseur de différences", + "ui.messagePart.shell.command": "Commande", + "ui.messagePart.shell.output": "Sortie", + "ui.messagePart.openInEditor": "Ouvrir dans l'éditeur", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "C'était utile", diff --git a/packages/kilo-i18n/src/ja.ts b/packages/kilo-i18n/src/ja.ts index 9d5214c7a48..634e4729604 100644 --- a/packages/kilo-i18n/src/ja.ts +++ b/packages/kilo-i18n/src/ja.ts @@ -18,7 +18,6 @@ export const dict = { // Marketplace "marketplace.tab.skills": "スキル", "marketplace.tab.mcpServers": "MCPサーバー", - "marketplace.tab.modes": "モード", "marketplace.category.all": "すべて", "marketplace.placeholder": "未実装", "marketplace.card.installed": "インストール済み", @@ -43,6 +42,7 @@ export const dict = { "marketplace.remove.cancel": "キャンセル", "marketplace.remove.confirm.button": "削除", "marketplace.tab.mcp": "MCP", + "marketplace.tab.agents": "エージェント", "marketplace.search": "検索...", "marketplace.filter.all": "すべてのアイテム", "marketplace.filter.notInstalled": "未インストール", @@ -58,7 +58,7 @@ export const dict = { "marketplace.scope.global": "グローバル", "marketplace.remove.type.mcp": "MCPサーバー", "marketplace.remove.type.skill": "スキル", - "marketplace.remove.type.mode": "モード", + "marketplace.remove.type.agent": "エージェント", "marketplace.remove.failed": "{{name}} の削除に失敗しました", "marketplace.install": "インストール", "marketplace.filter.installed": "インストール済み", @@ -67,8 +67,10 @@ export const dict = { "marketplace.warning.busyMany": "複数のセッションが実行中で中断されます", "marketplace.warning.installAnyway": "それでもインストール", "marketplace.warning.cancel": "キャンセル", - "marketplace.contribute.prompt": "スキル、モード、MCP サーバーが見つかりませんか?", + "marketplace.contribute.prompt": "スキル、エージェント、またはMCPサーバーが見つかりませんか?", "marketplace.contribute.cta": "GitHub で貢献する", + "marketplace.migration.notice": + "モードはエージェントに置き換えられました。以前にマーケットプレイスのモードをインストールしていた場合は、新しい形式に移行するためにそれらを削除してエージェントとして再インストールしてください。", // Plan follow-up question shown after plan_exit "plan.followup.header": "実装", @@ -89,7 +91,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "このプロジェクトでは Kilo のスナップショットを無効にします。Kilo による変更の取り消し/やり直しはできなくなりますが、git は引き続きすべてを追跡します。", - "ui.messagePart.openInDiffViewer": "差分ビューアで開く", + // Edit-tool header and shell-tool section labels + "ui.messagePart.openInDiffViewer": "差分ビューアーで開く", + "ui.messagePart.shell.command": "コマンド", + "ui.messagePart.shell.output": "出力", + "ui.messagePart.openInEditor": "エディタで開く", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "役に立ちました", diff --git a/packages/kilo-i18n/src/ko.ts b/packages/kilo-i18n/src/ko.ts index 6308cf9fc68..1e3c37ee0f5 100644 --- a/packages/kilo-i18n/src/ko.ts +++ b/packages/kilo-i18n/src/ko.ts @@ -18,7 +18,6 @@ export const dict = { // Marketplace "marketplace.tab.skills": "스킬", "marketplace.tab.mcpServers": "MCP 서버", - "marketplace.tab.modes": "모드", "marketplace.category.all": "전체", "marketplace.placeholder": "구현 예정", "marketplace.card.installed": "설치됨", @@ -43,6 +42,7 @@ export const dict = { "marketplace.remove.cancel": "취소", "marketplace.remove.confirm.button": "제거", "marketplace.tab.mcp": "MCP", + "marketplace.tab.agents": "에이전트", "marketplace.search": "검색...", "marketplace.filter.all": "모든 항목", "marketplace.filter.notInstalled": "설치되지 않음", @@ -58,7 +58,7 @@ export const dict = { "marketplace.scope.global": "글로벌", "marketplace.remove.type.mcp": "MCP 서버", "marketplace.remove.type.skill": "스킬", - "marketplace.remove.type.mode": "모드", + "marketplace.remove.type.agent": "에이전트", "marketplace.remove.failed": "{{name}} 제거 실패", "marketplace.install": "설치", "marketplace.filter.installed": "설치됨", @@ -67,8 +67,10 @@ export const dict = { "marketplace.warning.busyMany": "여러 세션이 실행 중이며 중단됩니다", "marketplace.warning.installAnyway": "그래도 설치", "marketplace.warning.cancel": "취소", - "marketplace.contribute.prompt": "스킬, 모드 또는 MCP 서버가 없나요?", + "marketplace.contribute.prompt": "스킬, 에이전트 또는 MCP 서버가 없나요?", "marketplace.contribute.cta": "GitHub에서 기여하기", + "marketplace.migration.notice": + "모드가 에이전트로 대체되었습니다. 이전에 마켓플레이스 모드를 설치한 경우 새 형식으로 마이그레이션하려면 해당 모드를 제거하고 에이전트로 다시 설치하세요.", // Plan follow-up question shown after plan_exit "plan.followup.header": "구현", @@ -89,7 +91,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "이 프로젝트의 Kilo 스냅샷을 끕니다. Kilo 변경에 대한 실행 취소/다시 실행은 사용할 수 없지만 git은 여전히 모든 것을 추적합니다.", - "ui.messagePart.openInDiffViewer": "Diff 뷰어에서 열기", + // Edit-tool header and shell-tool section labels + "ui.messagePart.openInDiffViewer": "차이점 뷰어에서 열기", + "ui.messagePart.shell.command": "명령어", + "ui.messagePart.shell.output": "출력", + "ui.messagePart.openInEditor": "편집기에서 열기", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "도움이 됐어요", diff --git a/packages/kilo-i18n/src/nl.ts b/packages/kilo-i18n/src/nl.ts index 4fb55eddca5..3e42fb7abff 100644 --- a/packages/kilo-i18n/src/nl.ts +++ b/packages/kilo-i18n/src/nl.ts @@ -21,7 +21,6 @@ export const dict = { // Marketplace "marketplace.tab.skills": "Skills", "marketplace.tab.mcpServers": "MCP Servers", - "marketplace.tab.modes": "Modi", "marketplace.category.all": "Alle", "marketplace.placeholder": "Nog te implementeren", "marketplace.card.installed": "Geïnstalleerd", @@ -47,6 +46,7 @@ export const dict = { "marketplace.remove.cancel": "Annuleren", "marketplace.remove.confirm.button": "Verwijderen", "marketplace.tab.mcp": "MCP", + "marketplace.tab.agents": "Agenten", "marketplace.search": "Zoeken...", "marketplace.filter.all": "Alle items", "marketplace.filter.notInstalled": "Niet geïnstalleerd", @@ -62,7 +62,7 @@ export const dict = { "marketplace.scope.global": "globaal", "marketplace.remove.type.mcp": "MCP server", "marketplace.remove.type.skill": "skill", - "marketplace.remove.type.mode": "modus", + "marketplace.remove.type.agent": "agent", "marketplace.remove.failed": "Verwijderen van {{name}} mislukt", "marketplace.install": "Installeren", "marketplace.filter.installed": "Geïnstalleerd", @@ -71,8 +71,10 @@ export const dict = { "marketplace.warning.busyMany": "Er zijn meerdere sessies actief en deze zullen worden onderbroken", "marketplace.warning.installAnyway": "Toch installeren", "marketplace.warning.cancel": "Annuleren", - "marketplace.contribute.prompt": "Mist er een skill, modus of MCP-server?", + "marketplace.contribute.prompt": "Mist u een skill, agent of MCP-server?", "marketplace.contribute.cta": "Bijdragen op GitHub", + "marketplace.migration.notice": + "Modi zijn vervangen door agenten. Als u eerder marketplace-modi hebt geïnstalleerd, verwijder ze dan en installeer ze opnieuw als agenten om naar het nieuwe formaat te migreren.", // Plan follow-up question shown after plan_exit "plan.followup.header": "Implementeren", @@ -93,7 +95,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "Zet Kilo-snapshots uit voor dit project. Je verliest ongedaan maken/opnieuw doen van Kilo-wijzigingen, maar git blijft alles volgen.", - "ui.messagePart.openInDiffViewer": "Openen in diff-viewer", + // Edit-tool header and shell-tool section labels + "ui.messagePart.openInDiffViewer": "Openen in Diff-weergave", + "ui.messagePart.shell.command": "Opdracht", + "ui.messagePart.shell.output": "Uitvoer", + "ui.messagePart.openInEditor": "Openen in editor", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "Dit was nuttig", diff --git a/packages/kilo-i18n/src/no.ts b/packages/kilo-i18n/src/no.ts index 915801390af..c23bf6c3e84 100644 --- a/packages/kilo-i18n/src/no.ts +++ b/packages/kilo-i18n/src/no.ts @@ -19,7 +19,6 @@ export const dict = { // Marketplace "marketplace.tab.skills": "Skills", "marketplace.tab.mcpServers": "MCP-servere", - "marketplace.tab.modes": "Moduser", "marketplace.category.all": "Alle", "marketplace.placeholder": "Skal implementeres", "marketplace.card.installed": "Installert", @@ -45,6 +44,7 @@ export const dict = { "marketplace.remove.cancel": "Avbryt", "marketplace.remove.confirm.button": "Fjern", "marketplace.tab.mcp": "MCP", + "marketplace.tab.agents": "Agenter", "marketplace.search": "Søk...", "marketplace.filter.all": "Alle elementer", "marketplace.filter.notInstalled": "Ikke installert", @@ -60,7 +60,7 @@ export const dict = { "marketplace.scope.global": "global", "marketplace.remove.type.mcp": "MCP-server", "marketplace.remove.type.skill": "ferdighet", - "marketplace.remove.type.mode": "modus", + "marketplace.remove.type.agent": "agent", "marketplace.remove.failed": "Kunne ikke fjerne {{name}}", "marketplace.install": "Installer", "marketplace.filter.installed": "Installert", @@ -69,8 +69,10 @@ export const dict = { "marketplace.warning.busyMany": "Flere økter kjører og vil bli avbrutt", "marketplace.warning.installAnyway": "Installer uansett", "marketplace.warning.cancel": "Avbryt", - "marketplace.contribute.prompt": "Mangler du en skill, modus eller MCP-server?", + "marketplace.contribute.prompt": "Mangler du en skill, agent eller MCP-server?", "marketplace.contribute.cta": "Bidra på GitHub", + "marketplace.migration.notice": + "Modi er erstattet av agenter. Hvis du tidligere har installert marketplace-modi, fjern dem og installer dem på nytt som agenter for å migrere til det nye formatet.", // Plan follow-up question shown after plan_exit "plan.followup.header": "Implementer", @@ -91,7 +93,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "Slå av Kilos snapshots for dette prosjektet. Du mister angre/gjør om for Kilo-endringer, men git fortsetter å spore alt.", - "ui.messagePart.openInDiffViewer": "Åpne i diff-viser", + // Edit-tool header and shell-tool section labels + "ui.messagePart.openInDiffViewer": "Åpne i diff-visning", + "ui.messagePart.shell.command": "Kommando", + "ui.messagePart.shell.output": "Utdata", + "ui.messagePart.openInEditor": "Åpne i editor", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "Dette var nyttig", diff --git a/packages/kilo-i18n/src/pl.ts b/packages/kilo-i18n/src/pl.ts index 67b83e46c29..09cbe56e59b 100644 --- a/packages/kilo-i18n/src/pl.ts +++ b/packages/kilo-i18n/src/pl.ts @@ -19,7 +19,6 @@ export const dict = { // Marketplace "marketplace.tab.skills": "Skills", "marketplace.tab.mcpServers": "Serwery MCP", - "marketplace.tab.modes": "Tryby", "marketplace.category.all": "Wszystkie", "marketplace.placeholder": "Do zaimplementowania", "marketplace.card.installed": "Zainstalowano", @@ -45,6 +44,7 @@ export const dict = { "marketplace.remove.cancel": "Anuluj", "marketplace.remove.confirm.button": "Usuń", "marketplace.tab.mcp": "MCP", + "marketplace.tab.agents": "Agenci", "marketplace.search": "Szukaj...", "marketplace.filter.all": "Wszystkie elementy", "marketplace.filter.notInstalled": "Nie zainstalowano", @@ -60,7 +60,7 @@ export const dict = { "marketplace.scope.global": "globalny", "marketplace.remove.type.mcp": "serwer MCP", "marketplace.remove.type.skill": "umiejętność", - "marketplace.remove.type.mode": "tryb", + "marketplace.remove.type.agent": "agent", "marketplace.remove.failed": "Nie udało się usunąć {{name}}", "marketplace.install": "Zainstaluj", "marketplace.filter.installed": "Zainstalowano", @@ -69,8 +69,10 @@ export const dict = { "marketplace.warning.busyMany": "Kilka sesji jest uruchomionych i zostanie przerwanych", "marketplace.warning.installAnyway": "Zainstaluj mimo to", "marketplace.warning.cancel": "Anuluj", - "marketplace.contribute.prompt": "Brakuje skilla, trybu lub serwera MCP?", + "marketplace.contribute.prompt": "Brakuje umiejętności, agenta lub serwera MCP?", "marketplace.contribute.cta": "Wnieś wkład na GitHubie", + "marketplace.migration.notice": + "Tryby zostały zastąpione przez agentów. Jeśli wcześniej instalowałeś tryby z marketplace, usuń je i zainstaluj ponownie jako agenty, aby przejść na nowy format.", // Plan follow-up question shown after plan_exit "plan.followup.header": "Wdróż", @@ -91,7 +93,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "Wyłącz snapshoty Kilo dla tego projektu. Stracisz cofanie/przywracanie zmian Kilo, ale git nadal śledzi wszystko.", + // Edit-tool header and shell-tool section labels "ui.messagePart.openInDiffViewer": "Otwórz w podglądzie różnic", + "ui.messagePart.shell.command": "Polecenie", + "ui.messagePart.shell.output": "Wyjście", + "ui.messagePart.openInEditor": "Otwórz w edytorze", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "To było pomocne", diff --git a/packages/kilo-i18n/src/ru.ts b/packages/kilo-i18n/src/ru.ts index d9cff803db5..89dc4e409b6 100644 --- a/packages/kilo-i18n/src/ru.ts +++ b/packages/kilo-i18n/src/ru.ts @@ -19,7 +19,6 @@ export const dict = { // Marketplace "marketplace.tab.skills": "Навыки", "marketplace.tab.mcpServers": "MCP-серверы", - "marketplace.tab.modes": "Режимы", "marketplace.category.all": "Все", "marketplace.placeholder": "Будет реализовано", "marketplace.card.installed": "Установлено", @@ -45,6 +44,7 @@ export const dict = { "marketplace.remove.cancel": "Отмена", "marketplace.remove.confirm.button": "Удалить", "marketplace.tab.mcp": "MCP", + "marketplace.tab.agents": "Агенты", "marketplace.search": "Поиск...", "marketplace.filter.all": "Все элементы", "marketplace.filter.notInstalled": "Не установлено", @@ -60,7 +60,7 @@ export const dict = { "marketplace.scope.global": "глобально", "marketplace.remove.type.mcp": "MCP-сервер", "marketplace.remove.type.skill": "навык", - "marketplace.remove.type.mode": "режим", + "marketplace.remove.type.agent": "агент", "marketplace.remove.failed": "Не удалось удалить {{name}}", "marketplace.install": "Установить", "marketplace.filter.installed": "Установлено", @@ -69,8 +69,10 @@ export const dict = { "marketplace.warning.busyMany": "Несколько сеансов выполняются и будут прерваны", "marketplace.warning.installAnyway": "Установить в любом случае", "marketplace.warning.cancel": "Отмена", - "marketplace.contribute.prompt": "Не хватает навыка, режима или MCP-сервера?", + "marketplace.contribute.prompt": "Не хватает навыка, агента или MCP-сервера?", "marketplace.contribute.cta": "Внести вклад на GitHub", + "marketplace.migration.notice": + "Режимы заменены агентами. Если вы ранее устанавливали режимы из магазина, удалите их и переустановите как агенты, чтобы перейти на новый формат.", // Plan follow-up question shown after plan_exit "plan.followup.header": "Реализовать", @@ -91,7 +93,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "Выключите снимки Kilo для этого проекта. Вы потеряете отмену/повтор изменений Kilo, но git по-прежнему отслеживает всё.", - "ui.messagePart.openInDiffViewer": "Открыть в просмотрщике различий", + // Edit-tool header and shell-tool section labels + "ui.messagePart.openInDiffViewer": "Открыть в просмотре различий", + "ui.messagePart.shell.command": "Команда", + "ui.messagePart.shell.output": "Вывод", + "ui.messagePart.openInEditor": "Открыть в редакторе", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "Это было полезно", diff --git a/packages/kilo-i18n/src/th.ts b/packages/kilo-i18n/src/th.ts index be90eddc10a..8affeb54c16 100644 --- a/packages/kilo-i18n/src/th.ts +++ b/packages/kilo-i18n/src/th.ts @@ -19,7 +19,6 @@ export const dict = { // Marketplace "marketplace.tab.skills": "ทักษะ", "marketplace.tab.mcpServers": "เซิร์ฟเวอร์ MCP", - "marketplace.tab.modes": "โหมด", "marketplace.category.all": "ทั้งหมด", "marketplace.placeholder": "ยังไม่ได้ดำเนินการ", "marketplace.card.installed": "ติดตั้งแล้ว", @@ -45,6 +44,7 @@ export const dict = { "marketplace.remove.cancel": "ยกเลิก", "marketplace.remove.confirm.button": "ลบ", "marketplace.tab.mcp": "MCP", + "marketplace.tab.agents": "เอเจนต์", "marketplace.search": "ค้นหา...", "marketplace.filter.all": "รายการทั้งหมด", "marketplace.filter.notInstalled": "ยังไม่ได้ติดตั้ง", @@ -60,7 +60,7 @@ export const dict = { "marketplace.scope.global": "โกลบอล", "marketplace.remove.type.mcp": "เซิร์ฟเวอร์ MCP", "marketplace.remove.type.skill": "ทักษะ", - "marketplace.remove.type.mode": "โหมด", + "marketplace.remove.type.agent": "เอเจนต์", "marketplace.remove.failed": "ไม่สามารถลบ {{name}} ได้", "marketplace.install": "ติดตั้ง", "marketplace.filter.installed": "ติดตั้งแล้ว", @@ -69,8 +69,10 @@ export const dict = { "marketplace.warning.busyMany": "มีหลายเซสชันกำลังทำงานและจะถูกขัดจังหวะ", "marketplace.warning.installAnyway": "ติดตั้งต่อไป", "marketplace.warning.cancel": "ยกเลิก", - "marketplace.contribute.prompt": "ไม่พบทักษะ โหมด หรือเซิร์ฟเวอร์ MCP ที่ต้องการ?", + "marketplace.contribute.prompt": "ขาดสกิล เอเจนต์ หรือเซิร์ฟเวอร์ MCP?", "marketplace.contribute.cta": "ร่วมสมทบบน GitHub", + "marketplace.migration.notice": + "โหมดถูกแทนที่ด้วยเอเจนต์แล้ว หากคุณเคยติดตั้งโหมดจาก marketplace กรุณาลบและติดตั้งใหม่เป็นเอเจนต์เพื่อย้ายไปยังรูปแบบใหม่", // Plan follow-up question shown after plan_exit "plan.followup.header": "ดำเนินการ", @@ -91,7 +93,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "ปิดสแน็ปช็อตของ Kilo สำหรับโปรเจกต์นี้ คุณจะสูญเสียการยกเลิก/ทำซ้ำสำหรับการเปลี่ยนแปลงของ Kilo แต่ git ยังคงติดตามทุกอย่าง", + // Edit-tool header and shell-tool section labels "ui.messagePart.openInDiffViewer": "เปิดในตัวดูความแตกต่าง", + "ui.messagePart.shell.command": "คำสั่ง", + "ui.messagePart.shell.output": "ผลลัพธ์", + "ui.messagePart.openInEditor": "เปิดในตัวแก้ไข", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "สิ่งนี้มีประโยชน์", diff --git a/packages/kilo-i18n/src/tr.ts b/packages/kilo-i18n/src/tr.ts index 0ebf2a73f24..b4e335f0501 100644 --- a/packages/kilo-i18n/src/tr.ts +++ b/packages/kilo-i18n/src/tr.ts @@ -19,7 +19,6 @@ export const dict = { // Marketplace "marketplace.tab.skills": "Yetenekler", "marketplace.tab.mcpServers": "MCP Sunucuları", - "marketplace.tab.modes": "Modlar", "marketplace.category.all": "Tümü", "marketplace.placeholder": "Uygulanacak", "marketplace.card.installed": "Yüklendi", @@ -45,6 +44,7 @@ export const dict = { "marketplace.remove.cancel": "İptal", "marketplace.remove.confirm.button": "Kaldır", "marketplace.tab.mcp": "MCP", + "marketplace.tab.agents": "Ajanlar", "marketplace.search": "Ara...", "marketplace.filter.all": "Tüm Öğeler", "marketplace.filter.notInstalled": "Yüklü Değil", @@ -60,7 +60,7 @@ export const dict = { "marketplace.scope.global": "genel", "marketplace.remove.type.mcp": "MCP sunucusu", "marketplace.remove.type.skill": "yetenek", - "marketplace.remove.type.mode": "mod", + "marketplace.remove.type.agent": "ajan", "marketplace.remove.failed": "{{name}} kaldırılamadı", "marketplace.install": "Yükle", "marketplace.filter.installed": "Yüklendi", @@ -69,8 +69,10 @@ export const dict = { "marketplace.warning.busyMany": "Birden fazla oturum çalışıyor ve kesintiye uğrayacak", "marketplace.warning.installAnyway": "Yine de yükle", "marketplace.warning.cancel": "İptal", - "marketplace.contribute.prompt": "Bir yetenek, mod veya MCP sunucusu mu eksik?", + "marketplace.contribute.prompt": "Bir yetenek, ajan veya MCP sunucusu mu eksik?", "marketplace.contribute.cta": "GitHub'da katkıda bulun", + "marketplace.migration.notice": + "Modlar agentlarla değiştirildi. Daha önce marketplace modları yüklediyseniz, yeni formata geçiş yapmak için bunları kaldırın ve agent olarak yeniden yükleyin.", // Plan follow-up question shown after plan_exit "plan.followup.header": "Uygula", @@ -91,7 +93,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "Bu proje için Kilo anlık görüntülerini kapat. Kilo değişiklikleri için geri alma/yeniden yapmayı kaybedersiniz, ancak git her şeyi izlemeye devam eder.", - "ui.messagePart.openInDiffViewer": "Fark görüntüleyicide aç", + // Edit-tool header and shell-tool section labels + "ui.messagePart.openInDiffViewer": "Fark Görüntüleyicide Aç", + "ui.messagePart.shell.command": "Komut", + "ui.messagePart.shell.output": "Çıktı", + "ui.messagePart.openInEditor": "Düzenleyicide Aç", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "Bu yardımcı oldu", diff --git a/packages/kilo-i18n/src/uk.ts b/packages/kilo-i18n/src/uk.ts index 3e325a33f7d..795fd1398bc 100644 --- a/packages/kilo-i18n/src/uk.ts +++ b/packages/kilo-i18n/src/uk.ts @@ -19,7 +19,6 @@ export const dict = { // Marketplace "marketplace.tab.skills": "Навички", "marketplace.tab.mcpServers": "MCP-сервери", - "marketplace.tab.modes": "Режими", "marketplace.category.all": "Усі", "marketplace.placeholder": "Буде реалізовано", "marketplace.card.installed": "Встановлено", @@ -45,6 +44,7 @@ export const dict = { "marketplace.remove.cancel": "Скасувати", "marketplace.remove.confirm.button": "Видалити", "marketplace.tab.mcp": "MCP", + "marketplace.tab.agents": "Агенти", "marketplace.search": "Пошук...", "marketplace.filter.all": "Усі елементи", "marketplace.filter.notInstalled": "Не встановлено", @@ -60,7 +60,7 @@ export const dict = { "marketplace.scope.global": "глобально", "marketplace.remove.type.mcp": "MCP-сервер", "marketplace.remove.type.skill": "навичка", - "marketplace.remove.type.mode": "режим", + "marketplace.remove.type.agent": "агент", "marketplace.remove.failed": "Не вдалося видалити {{name}}", "marketplace.install": "Встановити", "marketplace.filter.installed": "Встановлено", @@ -69,8 +69,10 @@ export const dict = { "marketplace.warning.busyMany": "Виконується кілька сесій, їх буде перервано", "marketplace.warning.installAnyway": "Встановити все одно", "marketplace.warning.cancel": "Скасувати", - "marketplace.contribute.prompt": "Бракує навички, режиму або MCP-сервера?", + "marketplace.contribute.prompt": "Бракує навички, агента або MCP-сервера?", "marketplace.contribute.cta": "Зробити внесок на GitHub", + "marketplace.migration.notice": + "Режими замінено агентами. Якщо ви раніше встановлювали режими з маркетплейсу, видаліть їх та перевстановіть як агенти для переходу на новий формат.", // Plan follow-up question shown after plan_exit "plan.followup.header": "Реалізувати", @@ -91,7 +93,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "Вимкніть знімки Kilo для цього проєкту. Ви втратите скасування/повторення для змін Kilo, але git продовжить відстежувати все.", - "ui.messagePart.openInDiffViewer": "Відкрити у переглядачі відмінностей", + // Edit-tool header and shell-tool section labels + "ui.messagePart.openInDiffViewer": "Відкрити в переглядачі відмінностей", + "ui.messagePart.shell.command": "Команда", + "ui.messagePart.shell.output": "Вивід", + "ui.messagePart.openInEditor": "Відкрити в редакторі", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "Це було корисно", diff --git a/packages/kilo-i18n/src/zh.ts b/packages/kilo-i18n/src/zh.ts index 29a57c5290d..f23fbb9c66a 100644 --- a/packages/kilo-i18n/src/zh.ts +++ b/packages/kilo-i18n/src/zh.ts @@ -17,7 +17,6 @@ export const dict = { // Marketplace "marketplace.tab.skills": "技能", "marketplace.tab.mcpServers": "MCP 服务器", - "marketplace.tab.modes": "模式", "marketplace.category.all": "全部", "marketplace.placeholder": "待实现", "marketplace.card.installed": "已安装", @@ -42,6 +41,7 @@ export const dict = { "marketplace.remove.cancel": "取消", "marketplace.remove.confirm.button": "移除", "marketplace.tab.mcp": "MCP", + "marketplace.tab.agents": "智能体", "marketplace.search": "搜索...", "marketplace.filter.all": "所有项目", "marketplace.filter.notInstalled": "未安装", @@ -57,7 +57,7 @@ export const dict = { "marketplace.scope.global": "全局", "marketplace.remove.type.mcp": "MCP 服务器", "marketplace.remove.type.skill": "技能", - "marketplace.remove.type.mode": "模式", + "marketplace.remove.type.agent": "智能体", "marketplace.remove.failed": "移除 {{name}} 失败", "marketplace.install": "安装", "marketplace.filter.installed": "已安装", @@ -66,8 +66,10 @@ export const dict = { "marketplace.warning.busyMany": "多个会话正在运行,将被中断", "marketplace.warning.installAnyway": "仍然安装", "marketplace.warning.cancel": "取消", - "marketplace.contribute.prompt": "缺少技能、模式或 MCP 服务器?", + "marketplace.contribute.prompt": "缺少技能、智能体或 MCP 服务器?", "marketplace.contribute.cta": "在 GitHub 上贡献", + "marketplace.migration.notice": + "模式已被智能体取代。如果您之前安装了市场中的模式,请将其删除并重新安装为智能体,以迁移到新格式。", // Plan follow-up question shown after plan_exit "plan.followup.header": "实现", @@ -86,7 +88,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "关闭本项目的 Kilo 快照。你将失去对 Kilo 更改的撤销/重做,但 git 仍会追踪所有内容。", + // Edit-tool header and shell-tool section labels "ui.messagePart.openInDiffViewer": "在差异查看器中打开", + "ui.messagePart.shell.command": "命令", + "ui.messagePart.shell.output": "输出", + "ui.messagePart.openInEditor": "在编辑器中打开", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "这有帮助", diff --git a/packages/kilo-i18n/src/zht.ts b/packages/kilo-i18n/src/zht.ts index 08582812567..930ddcf3ac5 100644 --- a/packages/kilo-i18n/src/zht.ts +++ b/packages/kilo-i18n/src/zht.ts @@ -17,7 +17,6 @@ export const dict = { // Marketplace "marketplace.tab.skills": "技能", "marketplace.tab.mcpServers": "MCP 伺服器", - "marketplace.tab.modes": "模式", "marketplace.category.all": "全部", "marketplace.placeholder": "待實作", "marketplace.card.installed": "已安裝", @@ -42,6 +41,7 @@ export const dict = { "marketplace.remove.cancel": "取消", "marketplace.remove.confirm.button": "移除", "marketplace.tab.mcp": "MCP", + "marketplace.tab.agents": "智能體", "marketplace.search": "搜尋...", "marketplace.filter.all": "所有項目", "marketplace.filter.notInstalled": "未安裝", @@ -57,7 +57,7 @@ export const dict = { "marketplace.scope.global": "全域", "marketplace.remove.type.mcp": "MCP 伺服器", "marketplace.remove.type.skill": "技能", - "marketplace.remove.type.mode": "模式", + "marketplace.remove.type.agent": "智能體", "marketplace.remove.failed": "移除 {{name}} 失敗", "marketplace.install": "安裝", "marketplace.filter.installed": "已安裝", @@ -66,8 +66,10 @@ export const dict = { "marketplace.warning.busyMany": "多個工作階段正在執行,將被中斷", "marketplace.warning.installAnyway": "仍然安裝", "marketplace.warning.cancel": "取消", - "marketplace.contribute.prompt": "缺少技能、模式或 MCP 伺服器?", + "marketplace.contribute.prompt": "缺少技能、智能體或 MCP 伺服器?", "marketplace.contribute.cta": "在 GitHub 上貢獻", + "marketplace.migration.notice": + "模式已被智能體取代。如果您之前安裝了市場中的模式,請將其刪除並重新安裝為智能體,以遷移到新格式。", // Plan follow-up question shown after plan_exit "plan.followup.header": "實作", @@ -86,7 +88,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "關閉本專案的 Kilo 快照。你將失去對 Kilo 變更的撤銷/重做,但 git 仍會追蹤所有內容。", + // Edit-tool header and shell-tool section labels "ui.messagePart.openInDiffViewer": "在差異檢視器中開啟", + "ui.messagePart.shell.command": "命令", + "ui.messagePart.shell.output": "輸出", + "ui.messagePart.openInEditor": "在編輯器中開啟", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "這有幫助", diff --git a/packages/kilo-indexing/package.json b/packages/kilo-indexing/package.json index 0fe3b960876..479f7b7dfe2 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.12", "type": "module", "license": "MIT", "description": "Standalone indexing engine and host helpers for Kilo Code", @@ -40,6 +40,7 @@ "@qdrant/js-client-rest": "1.17.0", "async-mutex": "0.5.0", "chokidar": "4.0.3", + "effect": "catalog:", "glob": "13.0.6", "hono": "catalog:", "hono-openapi": "catalog:", @@ -57,5 +58,6 @@ "@types/node": "catalog:", "@typescript/native-preview": "catalog:", "typescript": "catalog:" - } + }, + "peerDependencies": {} } diff --git a/packages/kilo-indexing/src/config.ts b/packages/kilo-indexing/src/config.ts index 7eafc17b06f..a4477388036 100644 --- a/packages/kilo-indexing/src/config.ts +++ b/packages/kilo-indexing/src/config.ts @@ -1,3 +1,4 @@ +import { Schema } from "effect" import z from "zod" import type { IndexingConfigInput } from "./indexing/config-manager" import type { EmbedderProvider } from "./indexing/interfaces/manager" @@ -14,19 +15,21 @@ const providers = [ "openrouter", "voyage", ] as const satisfies readonly EmbedderProvider[] +const stores = ["lancedb", "qdrant"] as const export const IndexingConfig = z .object({ enabled: z.boolean().optional().describe("Enable codebase indexing"), provider: z.enum(providers).optional().describe("Embedding provider to use for codebase indexing"), - model: z.string().optional().describe("Embedding model ID (uses provider default if omitted)"), + model: z.string().nullable().optional().describe("Embedding model ID (uses provider default if omitted)"), dimension: z .number() .int() .positive() + .nullable() .optional() .describe("Override embedding vector dimension (auto-detected from model if omitted)"), - vectorStore: z.enum(["lancedb", "qdrant"]).optional().describe("Vector store backend (default: qdrant)"), + vectorStore: z.enum(stores).optional().describe("Vector store backend (default: qdrant)"), kilo: z .object({ apiKey: z.string().optional(), @@ -128,6 +131,106 @@ export const IndexingConfig = z export type IndexingConfig = z.infer +const Provider = Schema.Literals(providers) +const Store = Schema.Literals(stores) +const PositiveInt = Schema.Int.check(Schema.isGreaterThan(0)) +const Score = Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(1)) + +export const IndexingSchema = Schema.Struct({ + enabled: Schema.optional(Schema.Boolean).annotate({ description: "Enable codebase indexing" }), + provider: Schema.optional(Provider).annotate({ + description: "Embedding provider to use for codebase indexing", + }), + model: Schema.optional(Schema.NullOr(Schema.String)).annotate({ + description: "Embedding model ID (uses provider default if omitted)", + }), + dimension: Schema.optional(Schema.NullOr(PositiveInt)).annotate({ + description: "Override embedding vector dimension (auto-detected from model if omitted)", + }), + vectorStore: Schema.optional(Store).annotate({ description: "Vector store backend (default: qdrant)" }), + kilo: Schema.optional( + Schema.Struct({ + apiKey: Schema.optional(Schema.String), + baseUrl: Schema.optional(Schema.String), + organizationId: Schema.optional(Schema.String), + }), + ).annotate({ description: "Kilo-hosted embedding provider options" }), + openai: Schema.optional( + Schema.Struct({ + apiKey: Schema.optional(Schema.String), + }), + ).annotate({ description: "OpenAI embedding provider options" }), + ollama: Schema.optional( + Schema.Struct({ + baseUrl: Schema.optional(Schema.String), + }), + ).annotate({ description: "Ollama embedding provider options" }), + "openai-compatible": Schema.optional( + Schema.Struct({ + baseUrl: Schema.optional(Schema.String), + apiKey: Schema.optional(Schema.String), + }), + ).annotate({ description: "OpenAI-compatible embedding provider options" }), + gemini: Schema.optional( + Schema.Struct({ + apiKey: Schema.optional(Schema.String), + }), + ).annotate({ description: "Gemini embedding provider options" }), + mistral: Schema.optional( + Schema.Struct({ + apiKey: Schema.optional(Schema.String), + }), + ).annotate({ description: "Mistral embedding provider options" }), + "vercel-ai-gateway": Schema.optional( + Schema.Struct({ + apiKey: Schema.optional(Schema.String), + }), + ).annotate({ description: "Vercel AI Gateway embedding provider options" }), + bedrock: Schema.optional( + Schema.Struct({ + region: Schema.optional(Schema.String), + profile: Schema.optional(Schema.String), + }), + ).annotate({ description: "AWS Bedrock embedding provider options" }), + openrouter: Schema.optional( + Schema.Struct({ + apiKey: Schema.optional(Schema.String), + specificProvider: Schema.optional(Schema.String), + }), + ).annotate({ description: "OpenRouter embedding provider options" }), + voyage: Schema.optional( + Schema.Struct({ + apiKey: Schema.optional(Schema.String), + }), + ).annotate({ description: "Voyage embedding provider options" }), + qdrant: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + apiKey: Schema.optional(Schema.String), + }), + ).annotate({ description: "Qdrant vector store connection options" }), + lancedb: Schema.optional( + Schema.Struct({ + directory: Schema.optional(Schema.String), + }), + ).annotate({ description: "LanceDB vector store options" }), + searchMinScore: Schema.optional(Score).annotate({ + description: "Minimum similarity score for search results (default: 0.4)", + }), + searchMaxResults: Schema.optional(PositiveInt).annotate({ + description: "Maximum number of search results (default: 50)", + }), + embeddingBatchSize: Schema.optional(PositiveInt).annotate({ + description: "Number of code segments per embedding batch (default: 60)", + }), + scannerMaxBatchRetries: Schema.optional(PositiveInt).annotate({ + description: "Maximum retry attempts for failed embedding batches (default: 3)", + }), +}).annotate({ + identifier: "IndexingConfig", + description: "Codebase indexing configuration", +}) + export function toIndexingConfigInput(cfg: IndexingConfig | undefined): IndexingConfigInput { const provider = cfg?.provider ?? "openai" @@ -135,8 +238,8 @@ export function toIndexingConfigInput(cfg: IndexingConfig | undefined): Indexing enabled: cfg?.enabled ?? false, embedderProvider: provider, vectorStoreProvider: cfg?.vectorStore, - modelId: cfg?.model, - modelDimension: cfg?.dimension, + modelId: cfg?.model ?? undefined, + modelDimension: cfg?.dimension ?? undefined, lancedbVectorStoreDirectory: cfg?.lancedb?.directory, qdrantUrl: cfg?.qdrant?.url, qdrantApiKey: cfg?.qdrant?.apiKey, diff --git a/packages/kilo-indexing/src/indexing/config-manager.ts b/packages/kilo-indexing/src/indexing/config-manager.ts index 1c5124de2dd..fd8c63210d9 100644 --- a/packages/kilo-indexing/src/indexing/config-manager.ts +++ b/packages/kilo-indexing/src/indexing/config-manager.ts @@ -302,10 +302,9 @@ export class CodeIndexConfigManager { } public get currentModelDimension(): number | undefined { + if (this.modelDimension && this.modelDimension > 0) return this.modelDimension const id = this.modelId ?? getDefaultModelId(this.embedderProvider) - const dim = getModelDimension(this.embedderProvider, id) - if (!dim && this.modelDimension && this.modelDimension > 0) return this.modelDimension - return dim + return getModelDimension(this.embedderProvider, id) } public get currentSearchMinScore(): number { diff --git a/packages/kilo-indexing/src/indexing/embedders/openrouter.ts b/packages/kilo-indexing/src/indexing/embedders/openrouter.ts index c055964a5c3..bc8deea083a 100644 --- a/packages/kilo-indexing/src/indexing/embedders/openrouter.ts +++ b/packages/kilo-indexing/src/indexing/embedders/openrouter.ts @@ -25,7 +25,8 @@ interface EmbeddingItem { } interface OpenRouterEmbeddingResponse { - data: EmbeddingItem[] + data?: EmbeddingItem[] + error?: string | { code?: string | number; message?: string } usage?: { prompt_tokens?: number total_tokens?: number @@ -193,10 +194,7 @@ export class OpenRouterEmbedder implements IEmbedder { const requestParams: any = { input: batchTexts, model: model, - // OpenAI package (as of v4.78.1) has a parsing issue that truncates embedding dimensions to 256 - // when processing numeric arrays, which breaks compatibility with models using larger dimensions. - // By requesting base64 encoding, we bypass the package's parser and handle decoding ourselves. - encoding_format: "base64", + encoding_format: "float", } if (this.dimensions !== undefined) { @@ -213,8 +211,24 @@ export class OpenRouterEmbedder implements IEmbedder { } const response = (await this.embeddingsClient.embeddings.create(requestParams)) as OpenRouterEmbeddingResponse + const err = response.error + const msg = typeof err === "string" ? err : err?.message + const code = typeof err === "object" && err ? err.code : undefined + if (!response.data || response.data.length === 0) { + log.warn("OpenRouter embedder batch returned invalid response", { + location: "OpenRouterEmbedder:_embedBatchWithRetries", + model, + dimensions: this.dimensions, + provider: this.specificProvider, + code, + err: msg, + }) + const invalid = new Error(msg ?? "Invalid response from OpenRouter embedding endpoint") as HttpError + invalid.status = typeof code === "number" ? code : 422 + throw invalid + } - // Convert base64 embeddings to float32 arrays + // Normalize base64 embeddings if OpenRouter returns them despite the float request. const processedEmbeddings = response.data.map((item: EmbeddingItem) => { if (typeof item.embedding === "string") { const buffer = Buffer.from(item.embedding, "base64") @@ -292,7 +306,7 @@ export class OpenRouterEmbedder implements IEmbedder { const requestParams: any = { input: testTexts, model: modelToUse, - encoding_format: "base64", + encoding_format: "float", } if (this.dimensions !== undefined) { @@ -315,6 +329,18 @@ export class OpenRouterEmbedder implements IEmbedder { // Check if we got a valid response if (!response?.data || response.data.length === 0) { + const err = response?.error + const msg = typeof err === "string" ? err : err?.message + const code = typeof err === "object" && err ? err.code : undefined + log.warn("OpenRouter embedder validation returned invalid response", { + location: "OpenRouterEmbedder:validateConfiguration", + model: modelToUse, + dimensions: this.dimensions, + provider: this.specificProvider, + dataCount: response?.data?.length ?? 0, + code, + err: msg, + }) return { valid: false, error: "Invalid response from OpenRouter embedding endpoint", diff --git a/packages/kilo-indexing/src/indexing/embedding-profile.ts b/packages/kilo-indexing/src/indexing/embedding-profile.ts index 843288e80a9..9d37d8eacb2 100644 --- a/packages/kilo-indexing/src/indexing/embedding-profile.ts +++ b/packages/kilo-indexing/src/indexing/embedding-profile.ts @@ -20,7 +20,7 @@ export function resolveEmbeddingProfile( modelDimension?: number, ): EmbeddingProfile | undefined { const id = modelId ?? getDefaultModelId(provider) - const dim = getModelDimension(provider, id) ?? parseDimension(modelDimension) + const dim = parseDimension(modelDimension) ?? getModelDimension(provider, id) if (!dim) return undefined return { provider, diff --git a/packages/kilo-indexing/src/indexing/model-registry.ts b/packages/kilo-indexing/src/indexing/model-registry.ts index 12a427d210b..d02b488cbe1 100644 --- a/packages/kilo-indexing/src/indexing/model-registry.ts +++ b/packages/kilo-indexing/src/indexing/model-registry.ts @@ -48,6 +48,7 @@ const profiles: Record> = { openrouter: { "openai/text-embedding-3-small": { dimension: 1536, scoreThreshold: 0.4 }, "openai/text-embedding-3-large": { dimension: 3072, scoreThreshold: 0.4 }, + "google/gemini-embedding-2-preview": { dimension: 3072, scoreThreshold: 0.35 }, }, "openai-compatible": {}, "vercel-ai-gateway": { diff --git a/packages/kilo-indexing/src/tree-sitter/languageParser.ts b/packages/kilo-indexing/src/tree-sitter/languageParser.ts index e7ba0c3f388..dce528004b1 100644 --- a/packages/kilo-indexing/src/tree-sitter/languageParser.ts +++ b/packages/kilo-indexing/src/tree-sitter/languageParser.ts @@ -41,7 +41,7 @@ export interface LanguageParser { } } -function resolveModulePath(specifier: string): string | undefined { +export function resolveModulePath(specifier: string): string | undefined { try { return require.resolve(specifier) } catch { @@ -53,7 +53,7 @@ function uniquePaths(paths: Array): string[] { return [...new Set(paths.filter((item): item is string => !!item))] } -function wasmDirectories(sourceDirectory?: string): string[] { +export function wasmDirectories(sourceDirectory?: string): string[] { const baseDir = sourceDirectory || __dirname const execDir = path.dirname(process.execPath) const envDir = process.env.KILO_TREE_SITTER_WASM_DIR @@ -80,7 +80,7 @@ function resolveFromDirectories(file: string, dirs: string[]): string | undefine } } -function resolveCoreRuntimeWasmPath(sourceDirectory?: string): string | undefined { +export function resolveCoreRuntimeWasmPath(sourceDirectory?: string): string | undefined { const dirs = wasmDirectories(sourceDirectory) const localPath = resolveFromDirectories("tree-sitter.wasm", dirs) if (localPath) { @@ -89,7 +89,7 @@ function resolveCoreRuntimeWasmPath(sourceDirectory?: string): string | undefine return resolveModulePath("web-tree-sitter/tree-sitter.wasm") } -function resolveLanguageWasmPath(langName: string, sourceDirectory?: string) { +export function resolveLanguageWasmPath(langName: string, sourceDirectory?: string) { const dirs = wasmDirectories(sourceDirectory) const fileName = `tree-sitter-${langName}.wasm` const wasmPath = resolveFromDirectories(fileName, dirs) diff --git a/packages/kilo-indexing/test/kilocode/indexing/config-manager.test.ts b/packages/kilo-indexing/test/kilocode/indexing/config-manager.test.ts index cc56020ab7f..4ba3338df49 100644 --- a/packages/kilo-indexing/test/kilocode/indexing/config-manager.test.ts +++ b/packages/kilo-indexing/test/kilocode/indexing/config-manager.test.ts @@ -83,6 +83,20 @@ describe("CodeIndexConfigManager", () => { expect(cfg.currentModelDimension).toBe(2048) }) + test("uses configured dimension before static model metadata", () => { + const cfg = new CodeIndexConfigManager( + createInput({ + embedderProvider: "openrouter", + openAiKey: undefined, + openRouterApiKey: "or-test", + modelId: "google/gemini-embedding-2-preview", + modelDimension: 1536, + }), + ) + + expect(cfg.currentModelDimension).toBe(1536) + }) + describe("loadConfiguration restart checks", () => { test("requires restart when model changes with same dimension", () => { const cfg = new CodeIndexConfigManager(createInput({ modelId: "text-embedding-3-small" })) diff --git a/packages/kilo-indexing/test/kilocode/indexing/embedders/openrouter.test.ts b/packages/kilo-indexing/test/kilocode/indexing/embedders/openrouter.test.ts index 729e5c77875..098dd007508 100644 --- a/packages/kilo-indexing/test/kilocode/indexing/embedders/openrouter.test.ts +++ b/packages/kilo-indexing/test/kilocode/indexing/embedders/openrouter.test.ts @@ -68,14 +68,10 @@ describe("OpenRouterEmbedder", () => { }) test("should create embeddings successfully", async () => { - // Create base64 encoded embedding with values that can be exactly represented in Float32 - const testEmbedding = new Float32Array([0.25, 0.5, 0.75]) - const base64String = Buffer.from(testEmbedding.buffer).toString("base64") - const mockResponse = { data: [ { - embedding: base64String, + embedding: [0.25, 0.5, 0.75], }, ], usage: { @@ -91,7 +87,7 @@ describe("OpenRouterEmbedder", () => { expect(mockEmbeddingsCreate).toHaveBeenCalledWith({ input: ["test text"], model: defaultModel, - encoding_format: "base64", + encoding_format: "float", }) expect(result.embeddings).toHaveLength(1) expect(result.embeddings[0]).toEqual([0.25, 0.5, 0.75]) @@ -99,6 +95,20 @@ describe("OpenRouterEmbedder", () => { expect(result.usage?.totalTokens).toBe(5) }) + test("should not retry invalid responses without embedding data", async () => { + mockEmbeddingsCreate.mockResolvedValue({ + error: { + code: 404, + message: "No successful provider responses.", + }, + }) + + await expect(embedder.createEmbeddings(["test"])).rejects.toThrow( + "Embedding request failed after 3 attempts with status 404: No successful provider responses.", + ) + expect(mockEmbeddingsCreate).toHaveBeenCalledTimes(1) + }) + test("should handle multiple texts", async () => { const embedding1 = new Float32Array([0.25, 0.5]) const embedding2 = new Float32Array([0.75, 1.0]) @@ -156,7 +166,7 @@ describe("OpenRouterEmbedder", () => { expect(mockEmbeddingsCreate).toHaveBeenCalledWith({ input: ["test"], model: customModel, - encoding_format: "base64", + encoding_format: "float", }) }) @@ -187,7 +197,7 @@ describe("OpenRouterEmbedder", () => { expect(mockEmbeddingsCreate).toHaveBeenCalledWith({ input: ["test"], model: defaultModel, - encoding_format: "base64", + encoding_format: "float", provider: { order: [specificProvider], only: [specificProvider], @@ -221,7 +231,7 @@ describe("OpenRouterEmbedder", () => { expect(mockEmbeddingsCreate).toHaveBeenCalledWith({ input: ["test"], model: defaultModel, - encoding_format: "base64", + encoding_format: "float", dimensions: 1024, }) }) @@ -257,7 +267,7 @@ describe("OpenRouterEmbedder", () => { expect(mockEmbeddingsCreate).toHaveBeenCalledWith({ input: ["test"], model: defaultModel, - encoding_format: "base64", + encoding_format: "float", }) }) }) @@ -271,13 +281,10 @@ describe("OpenRouterEmbedder", () => { }) test("should validate configuration successfully", async () => { - const testEmbedding = new Float32Array([0.25, 0.5]) - const base64String = Buffer.from(testEmbedding.buffer).toString("base64") - const mockResponse = { data: [ { - embedding: base64String, + embedding: [0.25, 0.5], }, ], usage: { @@ -296,7 +303,7 @@ describe("OpenRouterEmbedder", () => { { input: ["test"], model: defaultModel, - encoding_format: "base64", + encoding_format: "float", }, { timeout: REMOTE_EMBEDDER_VALIDATION_TIMEOUT_MS, @@ -305,6 +312,20 @@ describe("OpenRouterEmbedder", () => { ) }) + test("should reject responses without embedding data", async () => { + mockEmbeddingsCreate.mockResolvedValue({ + error: { + code: 404, + message: "No successful provider responses.", + }, + }) + + const result = await embedder.validateConfiguration() + + expect(result.valid).toBe(false) + expect(result.error).toBe("Invalid response from OpenRouter embedding endpoint") + }) + test("should handle validation failure", async () => { const authError = new Error("Invalid API key") ;(authError as any).status = 401 @@ -346,7 +367,7 @@ describe("OpenRouterEmbedder", () => { { input: ["test"], model: defaultModel, - encoding_format: "base64", + encoding_format: "float", provider: { order: [specificProvider], only: [specificProvider], @@ -388,7 +409,7 @@ describe("OpenRouterEmbedder", () => { { input: ["test"], model: defaultModel, - encoding_format: "base64", + encoding_format: "float", dimensions: 1024, }, { diff --git a/packages/kilo-indexing/test/kilocode/indexing/service-factory.test.ts b/packages/kilo-indexing/test/kilocode/indexing/service-factory.test.ts index 67aff168c98..86d50ccc4a1 100644 --- a/packages/kilo-indexing/test/kilocode/indexing/service-factory.test.ts +++ b/packages/kilo-indexing/test/kilocode/indexing/service-factory.test.ts @@ -149,11 +149,42 @@ describe("CodeIndexServiceFactory", () => { expect(mockEmbeddingsCreate).toHaveBeenCalledWith({ input: ["hello"], model: "openai/text-embedding-3-small", - encoding_format: "base64", + encoding_format: "float", dimensions: 1024, }) }) + test("creates vector store for OpenRouter Gemini embedding preview", () => { + const factory = createFactory({ + embedderProvider: "openrouter", + openAiKey: undefined, + openRouterApiKey: "or-test", + modelId: "google/gemini-embedding-2-preview", + vectorStoreProvider: "lancedb", + }) + + const store = factory.createVectorStore() as unknown as { vectorSize: number } + + expect(store).toBeDefined() + expect(store.vectorSize).toBe(3072) + }) + + test("uses configured dimension before static model metadata for vector stores", () => { + const factory = createFactory({ + embedderProvider: "openrouter", + openAiKey: undefined, + openRouterApiKey: "or-test", + modelId: "openai/text-embedding-3-small", + modelDimension: 1024, + vectorStoreProvider: "lancedb", + }) + + const store = factory.createVectorStore() as unknown as { vectorSize: number } + + expect(store).toBeDefined() + expect(store.vectorSize).toBe(1024) + }) + test("creates Kilo embedder with Cloud-provided model", async () => { const factory = createFactory({ embedderProvider: "kilo", @@ -177,6 +208,7 @@ describe("CodeIndexServiceFactory", () => { input: ["hello"], model: "mistralai/mistral-embed-2312", encoding_format: "base64", + dimensions: 1024, }) }) }) diff --git a/packages/kilo-indexing/test/kilocode/tree-sitter/wasm-resolution.test.ts b/packages/kilo-indexing/test/kilocode/tree-sitter/wasm-resolution.test.ts new file mode 100644 index 00000000000..d2acef7ffd7 --- /dev/null +++ b/packages/kilo-indexing/test/kilocode/tree-sitter/wasm-resolution.test.ts @@ -0,0 +1,32 @@ +import { afterEach, describe, expect, test } from "bun:test" +import { mkdir, mkdtemp, rm, writeFile } from "fs/promises" +import { join } from "path" +import { tmpdir } from "os" +import { resolveCoreRuntimeWasmPath, resolveLanguageWasmPath } from "../../../src/tree-sitter/languageParser" + +const env = "KILO_TREE_SITTER_WASM_DIR" +const prev = process.env[env] + +describe("tree-sitter WASM resolution", () => { + afterEach(() => { + if (prev === undefined) delete process.env[env] + if (prev !== undefined) process.env[env] = prev + }) + + test("prefers installed CLI tree-sitter resources over module resolution", async () => { + const root = await mkdtemp(join(tmpdir(), "kilo-tree-sitter-wasm-")) + try { + const dir = join(root, "bin", "tree-sitter") + await mkdir(dir, { recursive: true }) + await writeFile(join(dir, "tree-sitter.wasm"), "runtime") + await writeFile(join(dir, "tree-sitter-typescript.wasm"), "language") + + process.env[env] = dir + + expect(resolveCoreRuntimeWasmPath()).toBe(join(dir, "tree-sitter.wasm")) + expect(resolveLanguageWasmPath("typescript").wasmPath).toBe(join(dir, "tree-sitter-typescript.wasm")) + } finally { + await rm(root, { recursive: true, force: true }) + } + }) +}) diff --git a/packages/kilo-jetbrains/.kilo/skills/jetbrains-ui-style/SKILL.md b/packages/kilo-jetbrains/.kilo/skills/jetbrains-ui-style/SKILL.md deleted file mode 100644 index 81667fea96d..00000000000 --- a/packages/kilo-jetbrains/.kilo/skills/jetbrains-ui-style/SKILL.md +++ /dev/null @@ -1,1335 +0,0 @@ ---- -name: jetbrains-ui-style -description: Use when creating, modifying, or reviewing Kotlin/Swing UI code for the Kilo JetBrains plugin. Applies to dialogs, settings pages, tool windows, forms, panels, component layout, sizing, spacing, colors, borders, icons, lists, trees, popups, notifications, and IntelliJ platform UI components. ---- - -# JetBrains UI Style - -Use this skill whenever creating, modifying, or reviewing UI code in `packages/kilo-jetbrains`. - -## Scope - -This skill covers Kotlin/Swing UI code for the Kilo JetBrains plugin, including dialogs, settings pages, tool windows, forms, panels, component layout, sizing, spacing, colors, borders, icons, lists, trees, popups, notifications, and IntelliJ Platform UI components. - -This plugin is a split-mode JetBrains plugin. UI code belongs in `frontend` unless there is a specific split-mode reason to place it elsewhere. - -## IntelliJ Platform Source Lookup - -When looking for IntelliJ Platform API usage, implementation examples, extension points, services, actions, inspections, PSI/VFS/editor behavior, or plugin patterns, prefer real IntelliJ source code over Gradle caches, downloaded jars, generated parser artifacts, or decompiled classes. - -Use this priority order: - -1. Check whether `$INTELLIJ_REPO` is set and points to a readable IntelliJ Community checkout. - - If `$INTELLIJ_REPO` is set, search source files under that directory first. - - Prefer implementation source files from directories such as `platform/`, `plugins/`, `java/`, `xml/`, `json/`, `jvm/`, and related modules. - - Do not assume the IntelliJ checkout is a sibling of the current repo or worktree. -2. If `$INTELLIJ_REPO` is unset, empty, unreadable, or does not appear to contain an IntelliJ source checkout, tell the user to set it up. - - Suggested instruction: `Set INTELLIJ_REPO to the path of a local intellij-community checkout, for example: export INTELLIJ_REPO=/path/to/intellij-community` - - Do not invent or hardcode a machine-specific absolute path. -3. If a local checkout is unavailable, use the public IntelliJ Community repository as the fallback source: - - https://github.com/JetBrains/intellij-community -4. Only inspect Gradle caches, downloaded jars, generated parser jars, decompiled classes, or dependency internals as a last resort when neither a local IntelliJ source checkout nor the public GitHub repository provides the needed information. - -Avoid starting searches in: - -- `~/.gradle/caches` -- `.gradle/` -- downloaded dependency jars -- generated parser artifacts -- decompiled library sources - -## Primary Rules - -- Prefer Kotlin UI DSL v2 whenever the UI is a dialog, settings page, form, options panel, or structured component layout. -- Use manual Swing only for custom rendering, transcript/chat surfaces, highly dynamic panels, virtualized lists, custom painting, or layouts the DSL cannot express cleanly. -- Keep generated UI code minimal. -- Do not set default Swing properties explicitly. For example, avoid `isOpaque = false` unless the component default differs or there is a documented rendering reason. -- Avoid hardcoded dimensions. Prefer layout semantics, component defaults, DSL sizing helpers, and `JBUI` utilities. -- Do not add decorative helper functions, wrappers, or defensive UI code unless they materially improve clarity or correctness. -- Prefer IntelliJ platform components and theme-aware APIs. -- Do not use hardcoded runtime colors in generated UI examples or implementation code. -- Derive UI colors from semantic IntelliJ theme APIs such as `UIUtil`, `JBUI.CurrentTheme`, `NamedColorUtil`, `JBColor.namedColor`, or `JBColor.lazy`. -- Do not use hardcoded font sizes or font families in generated UI examples or implementation code. -- Use component defaults, `JBFont`, `RelativeFont`, or existing component fonts for typography. -- Put user-visible strings in `*.properties` files. - -## Decision Tree - -- Use Kotlin UI DSL v2 for dialogs, settings pages, forms, options panels, and structured component layouts. -- Use Kotlin UI DSL v2 inside tool windows when rendering form-like controls or structured subpanels. -- Use standard Swing with IntelliJ Platform component replacements for tool-window shells, action-driven UI, custom components, transcript surfaces, and custom renderers. -- Use the Action System for menus and toolbars. -- Do not use Kotlin Compose. -- Do not use JCEF. - -| Need | API | -|---|---| -| Dialogs, settings pages, forms, any layout with components | Preferred: Kotlin UI DSL v2 (`com.intellij.ui.dsl.builder`) | -| Tool window panels, action-driven UI, custom components | Standard Swing with IntelliJ Platform component replacements | -| Menus and toolbars | Action System | - -## Minimal Code Rule - -Avoid explicit defaults and manual layout when the DSL can express the UI: - -```kotlin -// Avoid -val panel = JPanel(BorderLayout()).apply { - isOpaque = false - preferredSize = Dimension(420, 260) - border = EmptyBorder(12, 12, 12, 12) -} -``` - -Prefer DSL and platform spacing: - -```kotlin -panel { - row(KiloBundle.message("settings.name")) { - textField() - .align(AlignX.FILL) - .resizableColumn() - } -} -``` - -Avoid explicit default properties: - -```kotlin -// Avoid unless there is a documented rendering reason -component.isOpaque = false -``` - -Prefer omitting the assignment and relying on the component/platform default. - -Avoid raw fixed sizes: - -```kotlin -// Avoid -component.preferredSize = Dimension(300, 40) -``` - -Prefer semantic sizing: - -```kotlin -textField() - .columns(COLUMNS_MEDIUM) - .align(AlignX.FILL) -``` - -If a fixed size is genuinely required, use `JBUI.size(...)` or `JBUI.scale(...)` and keep the reason obvious from context. - -## Do Not Use Kotlin Compose - -Do not use Kotlin Compose or `intellij.platform.compose` in this plugin. The JetBrains modular template uses Compose for its demo tool window, but Kilo should use standard Swing with IntelliJ Platform components only. Keep all plugin UI in the existing Swing-based stack. - -## Do Not Use JCEF - -Do not use JCEF (`JBCefBrowser`) in this plugin. JCEF does not work in JetBrains remote development (split mode): the frontend process runs on the client machine but JCEF requires a display on the host, making it effectively unusable for remote users. Use standard Swing with IntelliJ Platform components for all UI. - -## Kotlin UI DSL v2 - -Use Kotlin UI DSL v2 as the default way to build UI for dialogs, settings pages, forms, and any layout composed of standard components. It produces correct spacing, label alignment, HiDPI scaling, and accessibility automatically. Only fall back to manual Swing layout when you need a fully custom component, such as a canvas, rich list renderer, transcript surface, or tool-window chrome that the DSL cannot express. - -The top-level builder is `panel { }` and returns `DialogPanel`. Structure is `panel -> row -> cells`. Cell factory methods such as `textField()`, `checkBox()`, and `label()` add components. The DSL lives in `com.intellij.ui.dsl.builder`. - -To explore DSL capabilities interactively: Tools -> Internal Actions -> UI -> Kotlin UI DSL -> UI DSL Showcase. This requires internal mode: `-Didea.is.internal=true`. - -### Basics: Panel, Row, Cell - -Rows occupy full width. The last cell in a row takes remaining space. Rows have a `layout` property. - -```kotlin -panel { - row("Row1 label:") { - textField() - label("Some text") - } - row("Row2:") { - label("This text is aligned with previous row") - } -} -``` - -### Row Layout - -Every row uses one of three layouts. Default is `LABEL_ALIGNED` when a label is provided for the row, `INDEPENDENT` otherwise. - -| Layout | Behavior | -|---|---| -| `LABEL_ALIGNED` | Label column and content columns, aligned across rows | -| `INDEPENDENT` | All cells are independent, no cross-row alignment | -| `PARENT_GRID` | Cells align with the parent grid columns across rows | - -```kotlin -panel { - row("PARENT_GRID:") { - label("Col 1") - label("Col 2") - }.layout(RowLayout.PARENT_GRID) - - row("PARENT_GRID:") { - textField() - textField() - }.layout(RowLayout.PARENT_GRID) - - row("LABEL_ALIGNED default with label:") { - textField() - } - - row { - label("INDEPENDENT default without label:") - textField() - } -} -``` - -### Components Reference - -All cell factory methods available inside `row { }`: - -| Method | Description | -|---|---| -| `checkBox("text")` | Checkbox | -| `threeStateCheckBox("text")` | Three-state checkbox | -| `radioButton("text", value)` | Radio button, must be inside `buttonsGroup {}` | -| `button("text") {}` | Push button | -| `actionButton(action)` | Icon button bound to an `AnAction` | -| `actionsButton(action1, action2, ...)` | Dropdown actions button | -| `segmentedButton(items) { text = it }` | Segmented control | -| `tabbedPaneHeader(items)` | Tab header strip | -| `label("text")` | Static label | -| `text("html")` | Rich text with links, icons, line-width control | -| `link("text") {}` | Focusable clickable link | -| `browserLink("text", "url")` | Opens URL in browser | -| `dropDownLink("default", listOf(...))` | Dropdown link selector | -| `icon(AllIcons.*)` | Icon display | -| `contextHelp("description", "title")` | Help icon with popup | -| `textField()` | Text input | -| `passwordField()` | Password input | -| `textFieldWithBrowseButton()` | Text field and browse dialog | -| `expandableTextField()` | Expandable multi-line text field | -| `extendableTextField()` | Text field with extension icons | -| `intTextField(range)` | Integer input with validation | -| `spinner(intRange)` / `spinner(doubleRange, step)` | Numeric spinner | -| `slider(min, max, minorTick, majorTick)` | Slider, use `.labelTable()` for tick labels | -| `textArea()` | Multi-line text, use `.rows(n)` and `.align(AlignX.FILL)` | -| `comboBox(items)` | Combo box / dropdown | -| `comment("text")` | Gray comment text, standalone | -| `cell(component)` | Wrap any arbitrary Swing component | -| `scrollCell(component)` | Wrap component in a scroll pane | -| `cell()` | Empty placeholder cell for grid alignment | - -Key component examples: - -```kotlin -panel { - var mode = "auto" - - buttonsGroup { - row("Mode:") { - radioButton("Automatic", "auto") - radioButton("Manual", "manual") - } - }.bind({ mode }, { mode = it }) - - row("Slider:") { - slider(0, 10, 1, 5) - .labelTable(mapOf( - 0 to JBLabel("0"), - 5 to JBLabel("5"), - 10 to JBLabel("10"), - )) - } - - row { - label("Text area:") - .align(AlignY.TOP) - .gap(RightGap.SMALL) - textArea() - .rows(5) - .align(AlignX.FILL) - }.layout(RowLayout.PARENT_GRID) -} -``` - -### Component Labels - -Labels for modifiable components must be connected via one of two methods. This ensures correct spacing, mnemonic support, and accessibility. - -- Row label: `row("&Label:") { textField() }`, mnemonic via `&`, label in left column -- Cell label: `textField().label("&Label:", LabelPosition.TOP)`, label attached to cell, optionally on top - -```kotlin -panel { - row("&Row label:") { - textField() - textField() - .label("Cell label at &left:") - } - row { - textField() - .label("Cell label at &top:", LabelPosition.TOP) - } -} -``` - -When a row contains a `checkBox` or `radioButton`, the DSL automatically increases space after the row label per IntelliJ UI Guidelines. - -### Comments - -Three types of comments, each with different placement and semantics: - -| Type | Method | Placement | -|---|---|---| -| Cell comment, bottom | `cell.comment("text")` | Below the cell | -| Cell comment, right | `cell.commentRight("text")` | Right of the cell | -| Cell context help | `cell.contextHelp("text", "title")` | Help icon with popup | -| Row comment | `row.rowComment("text")` | Below the entire row | -| Arbitrary comment | `comment("text")` | Standalone gray text | - -```kotlin -panel { - row { - textField() - .comment("Bottom comment") - textField() - .commentRight("Right comment") - textField() - .contextHelp("Help popup text") - } - - row("Label:") { - textField() - }.rowComment("This comment sits below the whole row") - - row { - comment("Standalone comment, supports links and  icons") - } -} -``` - -Comments support HTML with clickable links, bundled icons via ``, and line width control via `maxLineLength`. Use `MAX_LINE_LENGTH_NO_WRAP` to prevent wrapping. - -### Groups and Structure - -| Method | Grid | Description | -|---|---|---| -| `panel {}` | Own grid | Sub-panel occupying full width | -| `rowsRange {}` | Parent grid | Grouped rows sharing parent grid, useful with `enabledIf` | -| `group("Title") {}` | Own grid | Titled section with vertical spacing before/after | -| `groupRowsRange("Title") {}` | Parent grid | Titled section sharing parent grid alignment | -| `collapsibleGroup("Title") {}` | Own grid | Expandable section, Tab-focusable, supports mnemonics | -| `buttonsGroup("Title") {}` | None | Groups `radioButton` or `checkBox` under a title | -| `separator()` | None | Horizontal separator line | -| Row `panel {}` | Own grid | Sub-panel inside a cell | - -```kotlin -panel { - group("Settings") { - row("Name:") { textField() } - row("Path:") { textFieldWithBrowseButton() } - } - - collapsibleGroup("Advanced") { - row("Timeout:") { intTextField(0..1000) } - } - - var enabled = true - buttonsGroup("Mode:") { - row { radioButton("Automatic", true) } - row { radioButton("Manual", false) } - }.bind({ enabled }, { enabled = it }) - - separator() - - row { - label("Nested panels:") - panel { - row("Sub row 1:") { textField() } - row("Sub row 2:") { textField() } - } - } -} -``` - -### Gaps and Spacing - -- Horizontal gaps: `cell.gap(RightGap.SMALL)` between a label-like checkbox and its related field. Medium gap is the default between cells. -- Two-column layout: `twoColumnsRow({}, {})` or `gap(RightGap.COLUMNS)` with `.layout(RowLayout.PARENT_GRID)`. -- Left indent: `indent {}` for indented sub-content. -- Vertical gaps: `.topGap(TopGap.MEDIUM)` / `.bottomGap(BottomGap.MEDIUM)` on rows to separate unrelated groups. Attach gaps to the related row so hiding rows does not break layout. - -Common Kotlin UI DSL spacing values from `IntelliJSpacingConfiguration`: - -| Constant | Unscaled px | Usage | -|---|---|---| -| `RightGap.SMALL` | 6 | Related inline components, such as field to button | -| Default row/cell gap | 16 | Automatic gap between components in a row | -| `RightGap.COLUMNS` | 60 | Logical column separation | -| `TopGap.SMALL` / `BottomGap.SMALL` | 8 | Minor section separation | -| `TopGap.MEDIUM` / `BottomGap.MEDIUM` | 20 | Major section or group separation | -| `verticalComponentGap` | 6 | Automatic gap between rows | - -These are semantic IntelliJ spacing constants. They handle DPI scaling and the IntelliJ spacing model, but they are not theme keys and are not overridden by theme JSON. - -```kotlin -panel { - group("Horizontal Gaps") { - row { - val cb = checkBox("Use mail:") - .gap(RightGap.SMALL) - textField() - .enabledIf(cb.selected) - } - row("Width:") { - textField() - .gap(RightGap.SMALL) - label("pixels") - } - } - - group("Indent") { - row { label("Not indented") } - indent { - row { label("Indented row") } - } - } - - group("Two Columns") { - twoColumnsRow({ - checkBox("First column") - }, { - checkBox("Second column") - }) - } - - group("Vertical Gaps") { - row { checkBox("Option 1") } - row { checkBox("Option 2") } - row { checkBox("Unrelated option") } - .topGap(TopGap.MEDIUM) - } -} -``` - -### Enabled and Visible State - -Bind enabled/visible state to a checkbox or other observable. Works on rows, `indent {}` blocks, `rowsRange {}`, and individual cells. - -```kotlin -panel { - group("Enabled") { - lateinit var cb: Cell - row { cb = checkBox("Enable options") } - indent { - row { checkBox("Option 1") } - row { checkBox("Option 2") } - }.enabledIf(cb.selected) - } - - group("Visible") { - lateinit var cb: Cell - row { cb = checkBox("Show options") } - indent { - row { checkBox("Option 1") } - row { checkBox("Option 2") } - }.visibleIf(cb.selected) - } -} -``` - -### Binding - -Bind component values to model properties. Values are applied on `DialogPanel.apply()`, checked with `.isModified()`, and reverted with `.reset()`. - -| Method | Component | -|---|---| -| `bindSelected(model::prop)` | checkBox | -| `bindText(model::prop)` | textField | -| `bindIntText(model::prop)` | intTextField | -| `bindItem(model::prop.toNullableProperty())` | comboBox | -| `bindValue(model::prop)` | slider | -| `bindIntValue(model::prop)` | spinner | -| `buttonsGroup {}.bind(model::prop)` | radio group | - -```kotlin -enum class Theme { LIGHT, DARK } - -data class Settings( - var name: String = "", - var count: Int = 0, - var enabled: Boolean = false, - var theme: Theme = Theme.LIGHT, -) - -val model = Settings() - -val panel = panel { - row("Name:") { - textField().bindText(model::name) - } - row("Count:") { - intTextField(0..100).bindIntText(model::count) - } - row { - checkBox("Enabled").bindSelected(model::enabled) - } - buttonsGroup("Theme:") { - row { radioButton("Light", Theme.LIGHT) } - row { radioButton("Dark", Theme.DARK) } - }.bind(model::theme) -} - -panel.isModified() -panel.apply() -panel.reset() -``` - -### Validation - -Attach input validation rules to cells. Rules run continuously and display inline error/warning indicators. - -```kotlin -panel { - row("Username:") { - textField() - .columns(COLUMNS_MEDIUM) - .cellValidation { - addInputRule("Must not be empty") { - it.text.isBlank() - } - } - } - row("Port:") { - textField() - .columns(COLUMNS_MEDIUM) - .cellValidation { - addInputRule("Contains non-numeric characters", level = Level.WARNING) { - it.text.contains(Regex("[^0-9]")) - } - } - } -} -``` - -Activate validators by calling `dialogPanel.registerValidators(disposable)` after creating the panel. - -### Tips and Common Patterns - -| Pattern | Usage | -|---|---| -| `.bold()` | Bold text on any cell | -| `.columns(COLUMNS_MEDIUM)` | Set preferred width of textField / comboBox / textArea | -| `.text("initial")` | Set initial text on text components | -| `.resizableColumn()` | Column fills remaining horizontal space | -| `cell()` | Empty placeholder cell for grid alignment | -| `.widthGroup("name")` | Equalize widths across rows, cannot combine with `AlignX.FILL` | -| `.align(AlignX.FILL)` | Stretch component to fill available width | -| `.align(AlignY.TOP)` | Top-align component in its cell | -| `.applyToComponent { }` | Direct access to the underlying Swing component | -| `.selected(true)` | Default-select a radioButton when no bound value matches | -| `.gap(RightGap.COLUMNS)` | Column-level gap for multi-column layouts | - -```kotlin -panel { - row { label("Title").bold() } - - row("Name:") { - textField() - .columns(COLUMNS_MEDIUM) - .resizableColumn() - .align(AlignX.FILL) - } - - row("") { - textField() - }.rowComment("""Use row("") for an empty label column that aligns with labeled rows""") - - row { - text("Comment-colored text") - .applyToComponent { foreground = JBUI.CurrentTheme.ContextHelp.FOREGROUND } - } -} -``` - -## Manual Swing - -Use manual Swing only when Kotlin UI DSL cannot express the UI cleanly. - -Preferred manual Swing rules: - -- Use IntelliJ platform components instead of raw Swing components. -- Use layout managers and component defaults rather than fixed sizes. -- Use `JBUI.Borders.empty(...)`, `JBUI.insets(...)`, `JBUI.size(...)`, and `JBUI.scale(...)` instead of raw AWT/Swing sizing primitives. -- Do not set default properties explicitly. -- Keep custom components small and focused. -- For painting, use theme-aware colors and `JBUI.scale(...)` for pixel values. - -## Swing Component State - -Swing is retained-mode UI, not React-style declarative rendering. For dynamic Swing surfaces such as session cards, transcript parts, hover rows, and collapsible panels, build a stable component tree once and then mutate existing components in response to model or interaction changes. - -- Do not use a React-style `state -> render()` loop for Swing components. -- Avoid card-level `render()` methods that remove/recreate headers, text areas, markdown views, controls, or scroll panes after every click, hover, or model update. -- Give each renderer an `update(model)` method that applies model changes directly to existing UI components. -- In `update(model)`, compare before assigning when practical: label text, icons, foregrounds, fonts, body text, visibility, cursor, and containment. -- Do not duplicate state in booleans when Swing component state already answers the question. -- Derive expanded state from containment, such as `scroll.parent === root`, rather than maintaining an `open` boolean. -- Derive hover state from the current header background or other component property, rather than maintaining a `hover` boolean. -- Expand/collapse should attach or detach the existing body component and update controls only if attachment changed. -- Hover should update only the affected component, usually the header background, and repaint only that component when the effective color changed. -- Lazy-create expensive bodies such as `JBTextArea`, `JBScrollPane`, markdown panes, and HTML panes on first expansion or first direct access. -- Keep source/model text separately only when needed to initialize a lazy component before it exists. -- Store the current style snapshot so lazily-created children receive the latest `SessionStyle` when they are first created. -- Parent containers should refresh for add/remove/reorder operations, not automatically after every delegated child update or streaming delta. -- Child views should call `revalidate()`/`repaint()` only when they changed preferred size, visibility, containment, or paint output. -- Empty deltas, identical text, unchanged styles, repeated hover values, and no-op toggles should not repaint the whole card. -- Private helpers named `render()` in Swing views tend to invite full tree rebuilds. Prefer names that describe the mutation, such as `syncBody()`, `syncArrow()`, `syncHtml()`, or `applyModel()`. -- It is fine for markdown internals to call library APIs such as `renderer.render(...)`; the problem is UI-level rerendering that rebuilds Swing component trees. - -Avoid this React-style pattern: - -```kotlin -private var open = false - -fun toggle() { - open = !open - render() -} - -private fun render() { - root.removeAll() - header.removeAll() - header.add(title) - if (open) root.add(JBScrollPane(JBTextArea(text)), BorderLayout.CENTER) - revalidate() - repaint() -} -``` - -Prefer mutating stable Swing components and deriving state from the UI: - -```kotlin -fun isExpanded(): Boolean = scroll?.parent === root - -fun toggle() { - if (!canExpand()) return - val changed = if (isExpanded()) collapse() else expand() - if (!changed) return - syncArrow() - revalidate() - repaint() -} - -private fun expand(): Boolean { - if (isExpanded()) return false - root.add(scroll(), BorderLayout.CENTER) - return true -} - -private fun collapse(): Boolean { - val pane = scroll ?: return false - if (pane.parent !== root) return false - root.remove(pane) - return true -} - -private fun scroll(): JBScrollPane { - scroll?.let { return it } - val pane = JBScrollPane(body()) - scroll = pane - return pane -} -``` - -For tests around dynamic Swing components, assert retained component behavior instead of only final text: - -- Collapsed components start unattached, and expensive bodies are not created until first expansion. -- First expansion creates the body once, collapse detaches it, and re-expansion reuses the same instance. -- `isExpanded()` agrees with actual containment. -- `update(model)` changes existing labels/body text without duplicating components. -- Updates while collapsed do not eagerly create lazy bodies unless direct body access requires it. -- No-op updates, empty deltas, repeated hover values, and toggling non-expandable cards do not repaint/revalidate the whole view. - -## Platform Spacing and Insets - -Use semantic spacing APIs before inventing numbers. Do not copy fallback values from IntelliJ source into generated UI code; those values are defaults behind theme keys and may change by UI theme, New UI, OS, or IDE version. - -| Need | Preferred source | -|---|---| -| Dialogs, forms, settings | Kotlin UI DSL row/group/indent/gap APIs | -| Component gaps in Kotlin UI DSL | `RightGap.SMALL`, `RightGap.COLUMNS`, `TopGap.MEDIUM`, `BottomGap.MEDIUM`, `.customize(UnscaledGaps(...))` as a last resort | -| Manual Swing empty padding | `JBUI.Borders.empty(...)` | -| Manual Swing insets | `JBUI.insets(...)`, `JBUI.emptyInsets()`, `JBUI.insetsTop(...)`, `JBUI.insetsLeft(...)`, `JBUI.insetsBottom(...)`, `JBUI.insetsRight(...)` | -| Manual Swing dimensions | `JBUI.size(...)`, `JBDimension`, `JBUI.scale(...)` | -| Side separators | `JBUI.Borders.customLineTop(...)`, `customLineBottom(...)`, `customLineLeft(...)`, `customLineRight(...)` | -| Composed borders | `JBUI.Borders.compound(...)`, `JBUI.Borders.merge(...)` | -| Simple `BorderLayout` panels | `JBUI.Panels.simplePanel(...)`, `BorderLayoutPanel` | -| Simple vertical custom Swing groups | `VerticalLayout` | -| Fluent platform panels | `JBPanel.withBorder(...)`, `.andTransparent()`, `.andOpaque()`, `.withBackground(...)` | - -For themeable custom layouts, keep the `JBValue.UIInteger` object and call `.get()` during layout and size calculation. Do not cache the resolved `Int`, because that freezes the value from the current Look and Feel and scale context. - -```kotlin -private val gap = JBValue.UIInteger("KiloComponent.gap", 16) - -override fun doLayout() { - val value = gap.get() - // layout using value -} - -override fun getPreferredSize(): Dimension { - val value = gap.get() - // calculate using value -} -``` - -Avoid resolving the value once in a constructor or property initializer: - -```kotlin -private val gap = JBValue.UIInteger("KiloComponent.gap", 16).get() -``` - -When raw AWT layout constructors are unavoidable, legacy Swing defaults exist but must still be scaled before use: - -| Constant | Value | -|---|---| -| `UIUtil.DEFAULT_HGAP` | 10 | -| `UIUtil.DEFAULT_VGAP` | 4 | -| `UIUtil.LARGE_VGAP` | 12 | - -```kotlin -val panel = JPanel(BorderLayout(JBUI.scale(UIUtil.DEFAULT_HGAP), 0)) -``` - -Use `JBUI.CurrentTheme` for context-specific spacing and dimensions: - -| UI area | Preferred source | -|---|---| -| Action list rows | `JBUI.CurrentTheme.ActionsList.cellPadding()` | -| Action icon/text gaps | `JBUI.CurrentTheme.ActionsList.elementIconGap()` | -| Action mnemonic gaps | `JBUI.CurrentTheme.ActionsList.mnemonicIconGap()` / `mnemonicInsets()` | -| Popup selection interior | `JBUI.CurrentTheme.Popup.Selection.innerInsets()` | -| Popup separators | `JBUI.CurrentTheme.Popup.separatorInsets()` / `separatorLabelInsets()` | -| Popup header/search field | `JBUI.CurrentTheme.Popup.headerInsets()`, `searchFieldBorderInsets()`, `searchFieldInputInsets()` | -| Complex popup headers | `JBUI.CurrentTheme.ComplexPopup.headerInsets()` | -| Complex popup text fields | `JBUI.CurrentTheme.ComplexPopup.textFieldBorderInsets()` / `textFieldInputInsets()` | -| Search Everywhere / big popup header | `JBUI.CurrentTheme.BigPopup.headerBorder()` / `headerToolbarInsets()` / `tabInsets()` | -| Popup advertiser/footer | `JBUI.CurrentTheme.Advertiser.border()` | -| Toolbar buttons | `JBUI.CurrentTheme.Toolbar.toolbarButtonInsets()` / `mainToolbarButtonInsets()` | -| Toolbar containers | `JBUI.CurrentTheme.Toolbar.horizontalToolbarInsets()` / `verticalToolbarInsets()` | -| Tool-window headers | `JBUI.CurrentTheme.ToolWindow.headerLabelLeftRightInsets()` / `headerToolbarLeftRightInsets()` / `headerTabLeftRightInsets()` | -| Lists | `JBUI.CurrentTheme.List.rowHeight()` | -| Trees | `JBUI.CurrentTheme.Tree.rowHeight()` | -| VCS log rows | `JBUI.CurrentTheme.VersionControl.Log.rowHeight()` / `verticalPadding()` | -| Help tooltips | `JBUI.CurrentTheme.HelpTooltip.defaultTextBorderInsets()` / `smallTextBorderInsets()` | -| Navigation bar items | `JBUI.CurrentTheme.NavBar.itemInsets()` | - -When using Kotlin UI DSL, prefer DSL semantics over manual padding: - -```kotlin -panel { - group(KiloBundle.message("settings.section")) { - row(KiloBundle.message("settings.name")) { - textField() - .align(AlignX.FILL) - .resizableColumn() - } - indent { - row { checkBox(KiloBundle.message("settings.enabled")) } - } - } -} -``` - -When manual Swing is necessary, get padding from platform utilities: - -```kotlin -val panel = JBUI.Panels.simplePanel(content) - .withBorder(JBUI.Borders.empty(JBUI.CurrentTheme.Popup.headerInsets())) -``` - -For row heights, prefer platform row height APIs over fixed heights: - -```kotlin -component.preferredSize = JBDimension(0, JBUI.CurrentTheme.Tree.rowHeight()) -``` - -## Generic Platform Utilities - -Use existing platform utility classes instead of hand-rolled labels, renderers, borders, colors, fonts, and validation behavior. - -| Need | Preferred API | -|---|---| -| Concise border layout | `BorderLayoutPanel`, `JBUI.Panels.simplePanel(...)` | -| Platform panel helpers | `JBPanel.withBorder(...)`, `.andTransparent()`, `.andOpaque()` | -| Platform label behavior | `JBLabel` | -| Context help | `ContextHelpLabel` | -| Links | `HyperlinkLabel`, `LinkLabel`, Kotlin UI DSL `link(...)` / `browserLink(...)` | -| Error/validation label in legacy UI | `ErrorLabel` only when an inline validation component is required and DSL validation is not available | -| High-performance rich fragments | `SimpleColoredComponent` | -| List renderers | `ColoredListCellRenderer`, Kotlin `listCellRenderer` / `textListCellRenderer` / `LcrRow` | -| Tree renderers | `ColoredTreeCellRenderer` | -| Renderer text styles | `SimpleTextAttributes` | -| Editable list toolbar | `ToolbarDecorator` | -| Platform list | `JBList` | -| Platform tree | `Tree` | - -For renderer text, prefer `SimpleTextAttributes` over direct color/font mutation: - -```kotlin -append(text, SimpleTextAttributes.ERROR_ATTRIBUTES) -append(detail, SimpleTextAttributes.GRAYED_ATTRIBUTES) -append(link, SimpleTextAttributes.LINK_ATTRIBUTES) -``` - -## Tool Windows - -- Register declaratively in module XML via `com.intellij.toolWindow` extension point. -- Implement `ToolWindowFactory.createToolWindowContent()`, called lazily on first click. -- Use `SimpleToolWindowPanel(vertical = true)` as a convenient base for toolbar and content layout. -- Add tabs via `ToolWindow.contentManager`: create content with `ContentFactory.getInstance().createContent(component, title, isLockable)`, then `contentManager.addContent()`. -- For conditional display, implement `ToolWindowFactory.isApplicableAsync(project)`. -- Always use `ToolWindowManager.invokeLater()` instead of `Application.invokeLater()` for tool-window-related EDT tasks. - -## Dialogs - -- Extend `DialogWrapper`. -- Call `init()` from the constructor. -- Override `createCenterPanel()` to return UI content. -- Prefer Kotlin UI DSL v2 for panel contents. -- Override `getPreferredFocusedComponent()` for initial focus. -- Override `getDimensionServiceKey()` for size persistence when useful. -- Show with `showAndGet()` for modal boolean result, or `show()` and then `getExitCode()`. -- For input validation, call `initValidation()` in the constructor and override `doValidate()` to return `null` if valid or `ValidationInfo(message, component)` if invalid. - -## Platform Components - -Always use IntelliJ platform components instead of raw Swing where an equivalent exists. - -| Instead of | Use | Package | -|---|---|---| -| `JLabel` | `JBLabel` | `com.intellij.ui.components` | -| `JTextField` | `JBTextField` | `com.intellij.ui.components` | -| `JTextArea` | `JBTextArea` | `com.intellij.ui.components` | -| `JList` | `JBList` | `com.intellij.ui.components` | -| `JScrollPane` | `JBScrollPane` | `com.intellij.ui.components` | -| `JTable` | `JBTable` | `com.intellij.ui.table` | -| `JTree` | `Tree` | `com.intellij.ui.treeStructure` | -| `JSplitPane` | `JBSplitter` | `com.intellij.ui` | -| `JTabbedPane` | `JBTabs` | `com.intellij.ui.tabs` | -| `JCheckBox` | `JBCheckBox` | `com.intellij.ui.components` | -| Raw runtime colors | `UIUtil`, `JBUI.CurrentTheme`, `NamedColorUtil`, `JBColor.namedColor`, `JBColor.lazy` | `com.intellij.util.ui`, `com.intellij.ui` | -| `EmptyBorder` | `JBUI.Borders.empty()` | `com.intellij.util.ui` | -| Hardcoded pixel sizes | `JBUI.scale(px)` | `com.intellij.util.ui` | - -Inspection `Plugin DevKit | Code | Undesirable class usage` highlights raw Swing usage where a platform replacement exists. - -## Multi-line and Rich Text - -| Need | Component | -|---|---| -| Rich HTML with modern CSS, icons, shortcuts | `JBHtmlPane` (`com.intellij.ui.components.JBHtmlPane`) | -| Simple multi-line label with HTML | `JBLabel` + `XmlStringUtil.wrapInHtml()` | -| Scrollable / wrapping HTML panel | `SwingHelper.createHtmlViewer()` | -| High-performance colored text fragments in trees/lists/tables | `SimpleColoredComponent` | -| Plain-text newline splitting | `MultiLineLabel`, legacy, do not use in new code | - -- Build HTML programmatically with `HtmlChunk` / `HtmlBuilder` (`com.intellij.openapi.util.text.HtmlChunk`). Avoid raw HTML string concatenation because it risks injection and breaks localization. -- For simple wrapping/escaping, use `XmlStringUtil.wrapInHtml(content)`, `XmlStringUtil.wrapInHtmlLines(lines...)`, and `XmlStringUtil.escapeString(text)`. -- For selectable/copyable label text, use `JBLabel.setCopyable(true)`, which switches internally to `JEditorPane` while preserving label appearance. Use `setAllowAutoWrapping(true)` for auto-wrap. -- When creating a `JEditorPane` manually, always use `HTMLEditorKitBuilder` instead of constructing `HTMLEditorKit` directly: `editorPane.setEditorKit(HTMLEditorKitBuilder.simple())` or `.withWordWrapViewFactory().build()`. -- For single-line overflow/ellipsis, use `SwingTextTrimmer`. Do not manually truncate strings. -- Put all user-visible strings in `*.properties` files. HTML markup in values is acceptable. - -## Theme-Derived Colors - -Do not hardcode runtime colors. IntelliJ UI colors must come from the current theme, component state, editor color scheme, or a centralized semantic named color key. - -Prefer semantic helpers for common UI roles: - -| Need | Preferred API | -|---|---| -| Ordinary label text | `UIUtil.getLabelForeground()` | -| Secondary/help text | `UIUtil.getContextHelpForeground()` | -| Info label text | `UIUtil.getLabelInfoForeground()` | -| Success label text | `UIUtil.getLabelSuccessForeground()` | -| Error label text | `UIUtil.getErrorForeground()` | -| Warning label text | `JBUI.CurrentTheme.Label.warningForeground()` | -| Generic label foreground by state | `JBUI.CurrentTheme.Label.foreground(selected)` / `disabledForeground(selected)` | -| Inactive secondary text | `NamedColorUtil.getInactiveTextColor()` | -| Bounds and standard border color | `NamedColorUtil.getBoundsColor()` / `JBColor.border()` | -| Tooltips | `UIUtil.getToolTipForeground()` / `getToolTipBackground()` / `JBUI.CurrentTheme.Tooltip.*` | -| Links | `JBUI.CurrentTheme.Link.Foreground.ENABLED` / `HOVERED` / `PRESSED` / `VISITED` / `DISABLED` / `SECONDARY` | -| List renderer text/background | `UIUtil.getListForeground(selected, focused)` / `UIUtil.getListBackground(selected, focused)` | -| Tree renderer text/background | `UIUtil.getTreeForeground(selected, focused)` / `UIUtil.getTreeBackground(selected, focused)` | -| Non-selected tree content | `UIUtil.getTreeForeground()` / `UIUtil.getTreeBackground()` | - -Prefer `JBUI.CurrentTheme` for component-area colors and borders: - -| Need | Preferred API | -|---|---| -| Popup background | `JBUI.CurrentTheme.Popup.BACKGROUND` | -| Complex popup header | `JBUI.CurrentTheme.ComplexPopup.HEADER_BACKGROUND` | -| Separators | `JBUI.CurrentTheme.CustomFrameDecorations.separatorForeground()` | -| Advertiser/footer surfaces | `JBUI.CurrentTheme.Advertiser.foreground()` / `background()` / `border()` | -| Links | `JBUI.CurrentTheme.Link.Foreground.ENABLED` | -| Tree selection state | `JBUI.CurrentTheme.Tree.foreground(...)` and selection helpers | -| Validation errors | `JBUI.CurrentTheme.Validator.errorBorderColor()` / `errorBackgroundColor()` | -| Validation warnings | `JBUI.CurrentTheme.Validator.warningBorderColor()` / `warningBackgroundColor()` | -| Focus error/warning outlines | `JBUI.CurrentTheme.Focus.errorColor(active)` / `warningColor(active)` | -| Banner-like surfaces | `JBUI.CurrentTheme.Banner.*` | -| Notification tool-window surfaces | `JBUI.CurrentTheme.NotificationError.*`, `NotificationWarning.*`, `NotificationInfo.*` | -| Icon badges | `JBUI.CurrentTheme.IconBadge.*` | - -Use `JBColor.lazy { ... }` for colors that depend on runtime state or the active editor color scheme: - -```kotlin -val bg = JBColor.lazy { EditorColorsManager.getInstance().globalScheme.defaultBackground } -``` - -Use `JBColor.namedColor("Some.Semantic.Key", fallback)` only when defining or consuming a semantic color key. Prefer a fallback from an existing theme API over numeric RGB values in examples. - -For HTML/CSS snippets, compute the color from a theme API and convert it with `ColorUtil.toHtmlColor(...)`. Do not write hardcoded CSS color literals. - -```kotlin -val fg = ColorUtil.toHtmlColor(UIUtil.getContextHelpForeground()) -val html = HtmlChunk.span("color: $fg").addText(text) -``` - -Recommended examples: - -```kotlin -label.applyToComponent { - foreground = UIUtil.getContextHelpForeground() - font = JBFont.medium() -} -``` - -```kotlin -foreground = if (selected) { - UIUtil.getTreeForeground(true, hasFocus) -} else { - UIUtil.getContextHelpForeground() -} -``` - -Avoid inline `Color(...)`, numeric `JBColor(...)`, `Gray.xNN`, `JBColor.GRAY`, and hex color literals in runtime UI code. The exception is a centralized semantic color definition with a named color key when no existing platform key exists. - -## Theme-Derived Fonts - -Do not hardcode font sizes or font families. IntelliJ fonts must come from component defaults, platform typography helpers, or existing component fonts. - -- Prefer component default fonts when no style change is needed. -- Use `JBFont.h1()`, `JBFont.h2()`, `JBFont.h3()`, and `JBFont.h4()` for headings. -- Use `.asBold()`, `.asItalic()`, and `.asPlain()` for style changes on `JBFont` values. -- Use `JBFont.regular()`, `JBFont.medium()`, and `JBFont.small()` for regular and secondary text. -- Use `UIUtil.getLabelFont(UIUtil.FontSize.SMALL)` / `MINI` only when an API expects a raw `Font` and `JBFont` / `RelativeFont` is not a good fit. -- Use `RelativeFont` when adjusting an existing component font relatively. -- Use `RelativeFont.fromResource(...)` for theme-configurable font offsets when matching platform component patterns. -- Use `JBUI.Fonts.smallFont()` only when matching an existing platform component pattern that expects it. -- If editor-like text needs editor typography, derive it from the editor color scheme or existing editor component instead of hardcoding a font. - -For errors, grayed text, shortcuts, and links in renderers, prefer `SimpleTextAttributes.ERROR_ATTRIBUTES`, `GRAYED_ATTRIBUTES`, `SHORTCUT_ATTRIBUTES`, and `LINK_ATTRIBUTES` rather than manually selecting colors and styles. - -Recommended examples: - -```kotlin -label(title).applyToComponent { - font = JBFont.h3().asBold() -} -``` - -```kotlin -RelativeFont.BOLD.install(label) -``` - -Avoid `Font("...")`, raw font sizes, and `deriveFont(14f)` style examples. - -## Kilo Session Styling - -For session/chat UI under `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/`, keep general UI chrome separate from transcript styling. - -- Use `ai.kilocode.client.ui.UiStyle` for general UI helpers: `Colors`, `Borders`, `Insets`, `Gap`, `Space`, `Size`, and `Buttons`. -- Use `ai.kilocode.client.session.ui.SessionStyle` for session transcript/content styling that follows editor settings or live transcript configuration. -- `SessionStyle` is for user/assistant transcript text, reasoning text, tool output, prompt editor text, and markdown renderer fields such as `font`, `codeFont`, and future link/code-block styles. -- Long-lived session components should implement or propagate `SessionStyleTarget.applyStyle(style)` instead of reading editor globals once in constructors. -- Do not call `EditorColorsManager.getInstance().globalScheme` directly from individual views unless extending `SessionStyle.current()`. -- New transcript renderers must apply the current `SessionStyle` snapshot and ensure child parts created after a style change receive the queued style. -- Keep non-transcript IDE chrome on platform/UI style unless it is intentionally part of the transcript reading experience. - -Use `UiStyle` for the surrounding card chrome and `SessionStyle` for transcript content inside it: - -```kotlin -class ExamplePartView : PartView() { - override fun applyStyle(style: SessionStyle) { - var changed = false - if (md.font != style.transcriptFont) { - md.font = style.transcriptFont - changed = true - } - if (md.codeFont != style.editorFamily) { - md.codeFont = style.editorFamily - changed = true - } - if (!changed) return - revalidate() - repaint() - } -} -``` - -## Borders, Insets, and Spacing - -- Always create borders via `JBUI.Borders.empty(top, left, bottom, right)` and insets via `JBUI.insets()` so they are DPI-aware and auto-update on zoom. -- Use `JBUI.scale(int)` for any pixel dimension to ensure proper HiDPI scaling. -- Prefer Kotlin UI DSL gaps and row layout semantics over manually assigning borders or insets. -- Do not use `EmptyBorder`, raw `Insets`, or raw `Dimension` unless there is no platform alternative and the reason is obvious. - -Theme-dependent borders, insets, colors, and corner arcs must be re-evaluated when the Look and Feel changes. Do not assign a theme-derived border once in a constructor for a long-lived component. - -Prefer overriding `updateUI()` for component-local updates: - -```kotlin -class KiloPanel : JPanel() { - override fun updateUI() { - super.updateUI() - border = JBUI.Borders.customLine( - JBColor.namedColor("KiloPanel.borderColor", JBColor.border()), - 1, - ) - } -} -``` - -For long-lived tool-window panels or components that need explicit Look and Feel handling outside normal Swing updates, subscribe to `LafManagerListener.TOPIC` and reapply theme-dependent borders or cached UI state there. - -Use `JBValue.UIInteger` for themeable arc values, because defaults differ by theme: - -| Key | Use | -|---|---| -| `Component.arc` | General component corners | -| `Button.arc` | Button corners | -| `Popup.Selection.arc` | Popup selection background | -| `TabbedPane.tabSelectionArc` | Tab selection indicator | -| `Tree.Selection.arc` | Tree selection background | - -```kotlin -val arc = JBValue.UIInteger("Component.arc", 5).get() -val button = JBValue.UIInteger("Button.arc", 6).get() -``` - -Only define plugin-owned theme keys when a platform key is not enough. Themeable insets can use `JBUI.insets("KiloPanel.insets", JBUI.insets(4, 8))`; themeable colors should use `JBColor.namedColor("KiloPanel.borderColor", fallback)`. - -Themes can override plugin-owned UI keys through the `ui` section: - -```json -{ - "ui": { - "KiloPanel.insets": "6,12,6,12", - "KiloPanel.gap": 20, - "KiloPanel.borderColor": "#FF0000", - "KiloPanel.arc": 10 - } -} -``` - -Expose intentional custom UI keys to theme authors with the `themeMetadataProvider` extension point and a `*.themeMetadata.json` file. - -```xml - -``` - -```json -{ - "name": "Kilo Code", - "fixed": false, - "ui": [ - { "key": "KiloPanel.borderColor", "description": "Panel border color" }, - { "key": "KiloPanel.gap", "description": "Gap between panel items" } - ] -} -``` - -## Validation and Error UI - -Prefer built-in validation mechanisms over custom error rendering. - -- For Kotlin UI DSL, use `validationOnInput`, `validationOnApply`, `validationInfo`, `errorOnApply`, and `addValidationRule` on cells. -- For DSL dialogs and settings panels, call `DialogPanel.registerValidators(disposable)` when input validation should run continuously. -- Call `DialogPanel.validateAll()` before `apply()` when submitting a DSL panel manually. -- In `DialogWrapper`, use `doValidate()` and return `ValidationInfo(message, component)` for errors. -- Use `ValidationInfo.asWarning()` for warnings and `withOKEnabled()` when a warning should not block submission. -- For hand-built Swing forms, use `ComponentValidator` with `withValidator`, `withFocusValidator`, and `andRegisterOnDocumentListener` instead of custom tooltip/error border logic. -- If custom validation painting is unavoidable, use `JBUI.CurrentTheme.Validator.*` and `JBUI.CurrentTheme.Focus.*` colors. - -Kotlin UI DSL example: - -```kotlin -textField() - .validationOnInput { - if (it.text.isBlank()) error(KiloBundle.message("settings.name.required")) else null - } -``` - -Dialog validation example: - -```kotlin -override fun doValidate(): ValidationInfo? { - if (field.text.isBlank()) return ValidationInfo(KiloBundle.message("settings.name.required"), field) - return null -} -``` - -## Icons - -- Reuse platform icons. Browse at https://intellij-icons.jetbrains.design and access via `AllIcons.*` constants. -- Custom icons belong in `resources/icons/`. -- Load custom icons via `IconLoader.getIcon("/icons/foo.svg", MyClass::class.java)`. -- Organize custom icons in an `icons` package or a `*Icons` object with `@JvmField` on each constant. -- Icon sizing guideline values: actions/nodes = 16x16, tool window = 13x13 classic or 20x20 + 16x16 compact New UI, editor gutter = 12x12 classic or 14x14 New UI. -- Dark variants: `icon.svg` + `icon_dark.svg`. -- HiDPI variants: `icon@2x.svg` + `icon@2x_dark.svg`. -- New UI support: place New UI icons in `expui/`, create `*IconMappings.json`, register via `com.intellij.iconMapper` extension point. -- For SVG asset palette values, follow the IntelliJ icon asset guidelines. Do not copy guideline color literals into runtime Swing UI code. - -### SVG Assets - -IntelliJ does not theme SVG icons with `currentColor`, CSS classes, CSS variables, inherited styles, or ` + + +
+

Mercury Next Edit — Testing Playground

+

+ A walk-through guide for Kilo Code reviewers to validate the new + Next Edit Suggestion integration powered by Mercury Edit 2 from Inception Labs. +

+ +
+

On this page

+ +
+ +

What is Mercury Next Edit?

+

+ Mercury Edit 2 is a code-edit model from Inception Labs. + Unlike FIM completion, it predicts the user's next multi-line edit given the current file, cursor + position, and recent edit history. It typically responds in under 250 ms. +

+

+ This PR adds Mercury Next Edit as a new, opt-in autocomplete option in Kilo Code. It lives + alongside everything that's already shipping — Codestral FIM and Mercury Edit 2 via the Kilo gateway are + bit-for-bit unchanged. Selecting Mercury Next Edit (Inception) from the model dropdown switches + to a separate render pipeline: +

+
    +
  • + Same-line predictions render as inline ghost text (just like FIM — + Tab accepts). +
  • +
  • + Off-cursor predictions render as a decoration (red strikethrough + green ghost annotation) at + the predicted edit location. First Tab teleports the cursor there; second Tab applies. +
  • +
  • + After any accept, the integration immediately re-triggers Mercury so the user can walk a refactor with + repeated Tab presses ("Tab-Tab-Tab"). +
  • +
+ +

Install the PR locally

+

+ You'll need to pull this PR's branch and run the extension in a development VSCode window ("Extension + Development Host"). The whole loop is about 3 minutes once you have the prerequisites. +

+ +

Prerequisites

+
    +
  • VSCode ≥ 1.105.1 (matches kilocode's engines.vscode)
  • +
  • + Bun ≥ 1.3.13 (the build script checks the version) — install via + brew install bun or bun.sh +
  • +
  • GitHub CLI (gh) — optional but makes the PR checkout one command
  • +
  • + An Inception API key — create one at + platform.inceptionlabs.ai if you don't already have one +
  • +
+ +

1. Check out the PR branch

+

From an empty directory:

+
gh repo clone Kilo-Org/kilocode
+cd kilocode
+gh pr checkout 10536
+

Or without gh:

+
git clone https://github.com/Kilo-Org/kilocode.git
+cd kilocode
+git fetch origin pull/10536/head:mercury-next-edit-integration
+git checkout mercury-next-edit-integration
+ +

2. Install dependencies

+
bun install
+

(First install pulls the full monorepo — takes 30–60 seconds.)

+ +

3. Start the dev build

+
cd packages/kilo-vscode
+bun run watch
+

+ Leave that terminal running. It rebuilds the extension on every save and runs the TypeScript compiler in watch + mode. +

+ +

4. Open kilocode in VSCode and launch the Extension Development Host

+

From a separate terminal (or your IDE launcher):

+
code /path/to/kilocode
+

+ Inside that VSCode window, press F5 (or Run → Start Debugging). A second VSCode + window opens, titled [Extension Development Host]. That window has this PR's build of the + kilocode extension loaded. +

+ +
+

+ Pre-push turbo typecheck may fail on packages that need Java (JetBrains plugin). That's + environment, not code — not relevant to the NES feature. Use --no-verify on any local + pushes if you hit it. +

+
+ +

5. Open the test playground

+

+ In the Dev Host window: File → Open Folder… → choose + packages/kilo-vscode/docs/nes-examples/ inside this same repo. That gives you the 20 self-contained + test files described below. +

+ +

6. Configure NES

+

Settings (Cmd+,) in the Dev Host, search kilo-code.new.autocomplete:

+
    +
  • + modelMercury Next Edit (Inception)not "Mercury Edit 2", + which is the classic FIM-via-gateway option +
  • +
  • + nextEdit.apiKey → paste your sk_… Inception API key (or set + INCEPTION_API_KEY env before launching) +
  • +
  • enableAutoTrigger → ✓ (already the default)
  • +
+ +

7. Watch the pipeline live

+

+ In the Dev Host: View → Output → in the dropdown, select + "Kilo Code · Next Edit". Every request, response, and render decision is logged here with + timestamps. Keep this panel visible while testing — it's the single best diagnostic. +

+ +

You're set. Skip to the test cases below.

+ +

Enabling the feature (settings reference)

+

In VSCode Settings (Cmd+,), search kilo-code.new.autocomplete:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
SettingValue
model + Mercury Next Edit (Inception)not "Mercury Edit 2", which is the original + FIM-via-gateway option +
nextEdit.apiKeyyour Inception API key (sk_...); also accepts INCEPTION_API_KEY env var
enableAutoTrigger✓ (default)
nextEdit.baseUrl(optional) override the API base, defaults to https://api.inceptionlabs.ai/v1
nextEdit.debug(optional) mirror diagnostic logs to DevTools console
+

+ To watch the pipeline live: View → Output in the Dev Host, choose the + "Kilo Code · Next Edit" channel. +

+ +

How the integration works

+

+ The AutocompleteServiceManager instantiates both providers up front. Provider + registration with vscode.languages.registerInlineCompletionItemProvider is driven by the + configured model: +

+
    +
  • inception/mercury-next-editNES provider (this PR's new pipeline)
  • +
  • anything else → classic FIM provider (unchanged)
  • +
+

The NES provider, per keystroke:

+
    +
  1. Debounces 250 ms (skipped for explicit invocations).
  2. +
  3. + Builds a Mercury prompt: current file + cursor + an editable region [cursor − 5, cursor + 10] + + 3–5 recently-viewed-snippet ranges (from the shared RecentlyVisitedRangesService) + the last 5 + debounced unidiffs (from a new per-file EditHistoryTracker). +
  4. +
  5. + Sends a single role: "user" message to POST /v1/edit/completions with + max_tokens: 512. +
  6. +
  7. + Parses the triple-backtick fenced reply, strips Mercury's sentinel tokens, computes the minimal line-diff + against the current document. +
  8. +
  9. + Branches: same-line diff → InlineCompletionItem; off-cursor diff → + NextEditSuggestionManager with a decoration + Tab/Esc keybinding gated on a context flag + (kilo-code.nextEdit.hasPendingSuggestion). +
  10. +
+ +

Test cases

+

+ Each test below is a self-contained file at packages/kilo-vscode/docs/nes-examples/ in this repo. + Open that folder in the Extension Development Host (step 5 above), then work through the cases. Place your + cursor where indicated, wait ~300 ms idle, and observe. +

+

+ Tip: keep this page open in a separate window from the Dev Host — the descriptions below would otherwise + leak into Mercury's prompt context and bias the test. +

+ +

Render-path legend:

+

+ same-line ghost appears as inline ghost text at the cursor (Tab + accepts) · off-cursor decoration renders away from the cursor; first + Tab jumps, second Tab applies · + suppressed negative case — nothing should render +

+ +

Python — core tests

+ +
+

01 — Finish a recursive function body same-line

+
def factorial(n):
+    if n <= 1:
+        return 1
+
+
+
Cursor
+

The empty indented line at the end of factorial (column 4).

+
Expected
+

+ Ghost text proposing the recursive case (e.g. return n * factorial(n - 1)). + Tab accepts. +

+
+ +
+

02 — Pattern continuation same-line

+
COLOR_RED = "#ff0000"
+COLOR_GREEN = "#00ff00"
+COLOR_BLUE =
+
Cursor
+

End of line 3 (right after =).

+
Expected
+

Ghost text appending a hex color like "#0000ff".

+
+ +
+

03 — Mid-identifier completion same-line

+
def calculate_total(items):
+    total = 0
+    for item in items:
+        total += item.price
+    return tot
+
Cursor
+

End of file (after return tot).

+
Expected
+

Ghost text completing the identifier (likely altotal).

+
+ +
+

04 — Loop body inference same-line

+
def calculate_total(items):
+    total = 0
+    for item in items:
+
+    return total
+
Cursor
+

The empty indented line inside the for loop (column 8).

+
Expected
+

Ghost text proposing the accumulator update.

+
+ +
+

05 — Sibling method body same-line

+
class Stack:
+    def __init__(self):
+        self.items = []
+
+    def push(self, item):
+        self.items.append(item)
+
+    def pop(self):
+
+
+    def peek(self):
+        return self.items[-1] if self.items else None
+
Cursor
+

Empty indented line inside pop (column 8).

+
Expected
+

Ghost text proposing a body consistent with the symmetric push.

+
+ +

Python — advanced

+ +
+

07 — Multi-line rename refactor off-cursor

+
def compute_user_score(u, w):
+    base = u * 10
+    bonus = w * 5
+    penalty = u - w
+    return base + bonus - penalty
+
+
+def compute_user_score(user_id, weight):
+    base = u * 10
+    bonus = w * 5
+    penalty = u - w
+    return base + bonus - penalty
+
Cursor
+

End of the renamed signature line (def compute_user_score(user_id, weight):).

+
Expected
+

+ Strikethrough on the body lines below + ghost showing the renamed body. First Tab jumps, second + applies. +

+
+ +
+

08 — Mixed insert + replace off-cursor

+
def sum_prices(items):
+    total = 0
+    for item in items:
+    return total
+
Cursor
+

End of total = 0.

+
Expected
+

Decoration on the broken for-loop area showing the corrected body (insertion + replacement combined).

+
+ +
+

10 — Mid-token completion same-line

+
def fibonacci(n):
+    if n <= 1:
+        return n
+    return fibonacci(n - 1) + fibonacci(n - 2)
+
+
+result = fib
+
Cursor
+

End of the file (after result = fib).

+
Expected
+

Ghost text extending the identifier and supplying a call, e.g. onacci(10).

+
+ +
+

11 — Stub method with implemented siblings same-line

+
class Queue:
+    def __init__(self):
+        self.items = []
+
+    def enqueue(self, item):
+        self.items.append(item)
+
+    def peek(self):
+        return self.items[0] if self.items else None
+
+    def size(self):
+        return len(self.items)
+
+    def is_empty(self):
+        return not self.items
+
+    def dequeue(self):
+
+
+    def clear(self):
+        self.items.clear()
+
Cursor
+

Empty indented line inside dequeue (column 8).

+
Expected
+

Ghost text proposing a FIFO pop, e.g. return self.items.pop(0).

+
+ +
+

+ 12 — Type annotation insertion same-line / + off-cursor +

+
def multiply(a: int, b: int) -> int:
+    return a * b
+
+
+def subtract(a: int, b: int) -> int:
+    return a - b
+
+
+def add(a, b):
+    return a + b
+
Cursor
+

End of def add(a, b): (the only un-annotated function).

+
Expected
+

+ Strikethrough on the signature line + ghost showing the typed version (def add(a: int, b: int) -> int:). May render same-line or off-cursor depending on where on the line you clicked. +

+
+ +
+

13 — Docstring generation same-line

+
import datetime
+
+
+def parse_iso_datetime(s):
+    """Parse an ISO 8601 datetime string into a datetime.datetime."""
+    return datetime.datetime.fromisoformat(s)
+
+
+def parse_iso_date(s):
+
+    return datetime.date.fromisoformat(s)
+
Cursor
+

Empty indented line under def parse_iso_date(s): (column 4).

+
Expected
+

Ghost text inserting a one-line docstring matching the sibling's style.

+
+ +
+

14 — No-op suppression suppressed

+
def add(a: int, b: int) -> int:
+    """Return the sum of two integers."""
+    return a + b
+
+
+def multiply(a: int, b: int) -> int:
+    """Return the product of two integers."""
+    return a * b
+
+
+def subtract(a: int, b: int) -> int:
+    """Return a minus b."""
+    return a - b
+
Cursor
+

End of return a + b.

+
Expected
+

+ Nothing. The code is already correct — either Mercury returns an identical reply or our + suppression branch drops the proposal. Channel should show "no-op" or skip lines, never a render. +

+
Failure mode
+

Any visible suggestion that just replays the existing code is a false positive worth reporting.

+
+ +

TypeScript

+ +
+

ts_07 — Array transform completion same-line

+
interface User {
+    id: number;
+    name: string;
+    active: boolean;
+}
+
+function getActiveUserNames(users: User[]): string[] {
+    return users
+}
+
+const sample: User[] = [
+    { id: 1, name: "ada", active: true },
+    { id: 2, name: "lin", active: false },
+    { id: 3, name: "rin", active: true },
+];
+
+console.log(getActiveUserNames(sample));
+
Cursor
+

End of return users inside getActiveUserNames.

+
Expected
+

Ghost text completing the chain, e.g. .filter(u => u.active).map(u => u.name).

+
+ +
+

ts_08 — Param type annotations off-cursor

+
function double(x: number): number {
+    return x * 2;
+}
+
+function add(a, b) {
+    return a + b;
+}
+
+function negate(x: number): number {
+    return -x;
+}
+
+function main(): void {
+    console.log(double(3));
+    console.log(add(2, 4));
+    console.log(negate(7));
+}
+
+main();
+
Cursor
+

End of file (after main();).

+
Expected
+

Decoration on the add(a, b) signature proposing the typed version.

+
+ +
+

ts_09 — React event handler same-line

+
declare const React: {
+    useState: <T>(initial: T) => [T, (next: T) => void];
+};
+
+function Counter(): JSX.Element {
+    const [count, setCount] = React.useState<number>(0);
+
+    function handleClick() {
+
+    }
+
+    return (
+        <div>
+            <p>Count: {count}</p>
+            <button onClick={handleClick}>Increment</button>
+        </div>
+    );
+}
+
+export default Counter;
+
Cursor
+

Empty indented line inside handleClick (column 4).

+
Expected
+

Ghost text incrementing count via setCount.

+
+ +

Go

+ +
+

go_07 — Error handling block same-line

+
package main
+
+import (
+	"fmt"
+	"os"
+)
+
+func loadConfig(path string) ([]byte, error) {
+	data, err := os.ReadFile(path)
+
+	return data, nil
+}
+
+func main() {
+	cfg, err := loadConfig("config.json")
+	if err != nil {
+		fmt.Println("error:", err)
+		return
+	}
+	fmt.Println(string(cfg))
+}
+
Cursor
+

Empty line right after data, err := os.ReadFile(path).

+
Expected
+

Ghost text proposing the canonical if err != nil { return nil, err }.

+
+ +
+

go_08 — Struct method body same-line

+
package main
+
+import "fmt"
+
+type Rectangle struct {
+	Width  float64
+	Height float64
+}
+
+func (r Rectangle) Perimeter() float64 {
+	return 2 * (r.Width + r.Height)
+}
+
+func (r Rectangle) Area() float64 {
+
+}
+
+func main() {
+	r := Rectangle{Width: 3, Height: 4}
+	fmt.Println("perimeter:", r.Perimeter())
+	fmt.Println("area:", r.Area())
+}
+
Cursor
+

Empty indented line inside Area().

+
Expected
+

Ghost text computing area from Width and Height.

+
+ +
+

go_09 — Goroutine + channel same-line

+
package main
+
+import "fmt"
+
+func main() {
+	ch := make(chan int)
+
+	go func() {
+
+	}()
+
+	for v := range ch {
+		fmt.Println("got:", v)
+	}
+}
+
Cursor
+

Empty indented line inside the goroutine.

+
Expected
+

Ghost text producing values onto the channel and closing it.

+
+ +

Rust

+ +
+

rs_07 — Match-arm completion same-line

+
enum Shape {
+    Circle(f64),
+    Square(f64),
+    Rectangle(f64, f64),
+    Triangle(f64, f64),
+}
+
+fn area(s: &Shape) -> f64 {
+    match s {
+        Shape::Circle(r) => std::f64::consts::PI * r * r,
+        Shape::Square(side) => side * side,
+
+    }
+}
+
+fn main() {
+    let shapes = vec![
+        Shape::Circle(1.0),
+        Shape::Rectangle(2.0, 3.0),
+        Shape::Triangle(4.0, 5.0),
+    ];
+    for s in &shapes {
+        println!("area = {}", area(s));
+    }
+}
+
Cursor
+

Empty indented line inside the match body, after the Square arm.

+
Expected
+

Ghost text adding the missing Rectangle and Triangle arms.

+
+ +
+

rs_08 — Result/Option chaining same-line

+
fn parse_int(s: &str) -> Option<i32> {
+    let n = s.trim()
+    Some(n * 2)
+}
+
+fn main() {
+    let inputs = ["  21  ", "not-a-number", "10"];
+    for s in &inputs {
+        match parse_int(s) {
+            Some(v) => println!("{} -> {}", s, v),
+            None => println!("{} -> skipped", s),
+        }
+    }
+}
+
Cursor
+

End of let n = s.trim() (no semicolon yet).

+
Expected
+

Ghost text continuing the chain into a parsed i32.

+
+ +
+

rs_09 — Lifetime annotations off-cursor

+
fn longest(a: &str, b: &str) -> &str {
+    if a.len() >= b.len() {
+        a
+    } else {
+        b
+    }
+}
+
+fn main() {
+    let s1 = String::from("hello world");
+    let s2 = String::from("hi");
+    let out = longest(&s1, &s2);
+    println!("longest = {}", out);
+}
+
Cursor
+

End of file.

+
Expected
+

Decoration on the fn longest signature proposing lifetime annotations.

+
+ +

JavaScript

+ +
+

js_07 — Async/await fetch same-line

+
async function fetchUser(id) {
+    try {
+
+    } catch (err) {
+        console.error("fetchUser failed", err);
+        return null;
+    }
+}
+
+async function main() {
+    const user = await fetchUser(42);
+    console.log("user:", user);
+}
+
+main();
+
Cursor
+

Empty indented line inside the try { block (column 8).

+
Expected
+

Ghost text completing the fetch + json parse.

+
+ +
+

js_08 — Express GET handler same-line

+
const app = {
+    get: (_path, _handler) => app,
+    post: (_path, _handler) => app,
+    listen: (_port, cb) => cb && cb(),
+};
+
+const users = [
+    { id: 1, name: "ada" },
+    { id: 2, name: "lin" },
+];
+
+app.get("/users/:id", (req, res) => {
+
+});
+
+app.post("/users", (req, res) => {
+    const user = { id: users.length + 1, name: req.body.name };
+    users.push(user);
+    res.status(201).json(user);
+});
+
+app.listen(3000, () => console.log("listening on :3000"));
+
Cursor
+

Empty indented line inside the GET handler (column 4).

+
Expected
+

Ghost text proposing a get-by-id (lookup, 404, json response).

+
+ +

SQL

+ +
+

sql_07 — Missing JOIN same-line

+
SELECT
+    c.name,
+    SUM(o.total) AS total_spent
+FROM orders o
+
+WHERE o.created_at >= '2026-01-01'
+GROUP BY c.name
+ORDER BY total_spent DESC
+LIMIT 10;
+
Cursor
+

End of the line FROM orders o.

+
Expected
+

Ghost text completing the JOIN against customers.

+
+ +
+

sql_08 — WHERE filter same-line

+
SELECT id, email
+FROM users
+WHERE
+ORDER BY last_login_at DESC;
+
Cursor
+

End of the bare WHERE line.

+
Expected
+

Ghost text proposing a predicate.

+
+ +

Markdown (negative case)

+ +
+

md_07 — Prose should stay quiet suppressed

+
# Mercury Edit 2 — Quick Notes
+
+Mercury Edit 2 is a small, fast model trained to predict the user's
+next single edit given the current file, cursor position, and recent
+edit history. It targets latency under 200 ms on typical files and
+returns a unified-diff-like patch scoped to a window around the cursor.
+
+Unlike chat-style completions, the model is biased toward minimal,
+local changes — finishing a function body, fixing a typo, propagating
+a rename — rather than generating new files from scratch.
+
Cursor
+

End of the last sentence.

+
Expected
+

+ Nothing. If Mercury does propose a prose continuation it counts as a soft fail — we + don't want a code model writing README content. +

+
+ +

Troubleshooting

+
+

+ If nothing happens when you type, open View → Output → "Kilo Code · Next Edit" and watch the + log. The pipeline is verbose enough that 90% of issues are obvious from the first few lines. +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SymptomLikely causeFix
No log lines at allWrong model selected, or Dev Host wasn't reloaded after rebuild + Cmd+R in the Dev Host; confirm model = Mercury Next Edit (Inception) +
skip — no API key resolvedSetting not savedRe-paste the key in nextEdit.apiKey, press Enter, reload
<- 401 UnauthorizedWrong key or wrong tierVerify the key at platform.inceptionlabs.ai
<- 400 Bad RequestPrompt-shape regression (we shouldn't ship this, but if it happens during dev)Capture the response body from the channel and ping the integration owner
Suggestion shown for a wrong-looking model + Selecting "Mercury Edit 2" routes through the classic FIM provider, not NES — that's by design (the + old behavior is preserved) + Switch to "Mercury Next Edit (Inception)" to use the new pipeline
Inline ghost text never appears, but logs show RENDERAnother extension (Copilot, Tabnine) is winning the inline-completion raceTemporarily disable conflicting extensions in the Dev Host
+ +

Feedback we'd love

+
    +
  • + Where the prediction was wrong but the UX was correct. Note the file + cursor position + what + Mercury proposed. Helps us tune the model. +
  • +
  • + Where the UX got in the way. Tab semantics, decoration appearance, chained-prediction timing, + anything that felt clumsy compared to other NES products you've used. +
  • +
  • + Performance regressions in classic FIM autocomplete. The PR is supposed to leave the classic + path untouched — if Codestral or Mercury Edit 2 (FIM) feel different in this build, that's a regression + we want to know about. +
  • +
  • + Things you tried that aren't in this doc. The 20 tests are a starting point, not a contract. + Real codebases will be different. +
  • +
+ + +
+ + diff --git a/packages/kilo-vscode/docs/nes-examples/01_finish_function_body.py b/packages/kilo-vscode/docs/nes-examples/01_finish_function_body.py new file mode 100644 index 00000000000..939b1044889 --- /dev/null +++ b/packages/kilo-vscode/docs/nes-examples/01_finish_function_body.py @@ -0,0 +1,4 @@ +def factorial(n): + if n <= 1: + return 1 + diff --git a/packages/kilo-vscode/docs/nes-examples/02_pattern_continuation.py b/packages/kilo-vscode/docs/nes-examples/02_pattern_continuation.py new file mode 100644 index 00000000000..87d3616b278 --- /dev/null +++ b/packages/kilo-vscode/docs/nes-examples/02_pattern_continuation.py @@ -0,0 +1,3 @@ +COLOR_RED = "#ff0000" +COLOR_GREEN = "#00ff00" +COLOR_BLUE = diff --git a/packages/kilo-vscode/docs/nes-examples/03_typo_completion.py b/packages/kilo-vscode/docs/nes-examples/03_typo_completion.py new file mode 100644 index 00000000000..932190bfe8b --- /dev/null +++ b/packages/kilo-vscode/docs/nes-examples/03_typo_completion.py @@ -0,0 +1,5 @@ +def calculate_total(items): + total = 0 + for item in items: + total += item.price + return tot diff --git a/packages/kilo-vscode/docs/nes-examples/04_loop_body.py b/packages/kilo-vscode/docs/nes-examples/04_loop_body.py new file mode 100644 index 00000000000..7d7c9629b8a --- /dev/null +++ b/packages/kilo-vscode/docs/nes-examples/04_loop_body.py @@ -0,0 +1,5 @@ +def calculate_total(items): + total = 0 + for item in items: + + return total diff --git a/packages/kilo-vscode/docs/nes-examples/05_class_method.py b/packages/kilo-vscode/docs/nes-examples/05_class_method.py new file mode 100644 index 00000000000..2613e953eb5 --- /dev/null +++ b/packages/kilo-vscode/docs/nes-examples/05_class_method.py @@ -0,0 +1,12 @@ +class Stack: + def __init__(self): + self.items = [] + + def push(self, item): + self.items.append(item) + + def pop(self): + + + def peek(self): + return self.items[-1] if self.items else None diff --git a/packages/kilo-vscode/docs/nes-examples/07_multiline_rename_refactor.py b/packages/kilo-vscode/docs/nes-examples/07_multiline_rename_refactor.py new file mode 100644 index 00000000000..7846ab08f4b --- /dev/null +++ b/packages/kilo-vscode/docs/nes-examples/07_multiline_rename_refactor.py @@ -0,0 +1,12 @@ +def compute_user_score(u, w): + base = u * 10 + bonus = w * 5 + penalty = u - w + return base + bonus - penalty + + +def compute_user_score(user_id, weight): + base = u * 10 + bonus = w * 5 + penalty = u - w + return base + bonus - penalty diff --git a/packages/kilo-vscode/docs/nes-examples/08_mixed_insert_and_replace.py b/packages/kilo-vscode/docs/nes-examples/08_mixed_insert_and_replace.py new file mode 100644 index 00000000000..94d9332e062 --- /dev/null +++ b/packages/kilo-vscode/docs/nes-examples/08_mixed_insert_and_replace.py @@ -0,0 +1,4 @@ +def sum_prices(items): + total = 0 + for item in items: + return total diff --git a/packages/kilo-vscode/docs/nes-examples/10_mid_token_completion.py b/packages/kilo-vscode/docs/nes-examples/10_mid_token_completion.py new file mode 100644 index 00000000000..5139ba755d3 --- /dev/null +++ b/packages/kilo-vscode/docs/nes-examples/10_mid_token_completion.py @@ -0,0 +1,7 @@ +def fibonacci(n): + if n <= 1: + return n + return fibonacci(n - 1) + fibonacci(n - 2) + + +result = fib diff --git a/packages/kilo-vscode/docs/nes-examples/11_fill_sibling_method.py b/packages/kilo-vscode/docs/nes-examples/11_fill_sibling_method.py new file mode 100644 index 00000000000..f2c4845e20e --- /dev/null +++ b/packages/kilo-vscode/docs/nes-examples/11_fill_sibling_method.py @@ -0,0 +1,21 @@ +class Queue: + def __init__(self): + self.items = [] + + def enqueue(self, item): + self.items.append(item) + + def peek(self): + return self.items[0] if self.items else None + + def size(self): + return len(self.items) + + def is_empty(self): + return not self.items + + def dequeue(self): + + + def clear(self): + self.items.clear() diff --git a/packages/kilo-vscode/docs/nes-examples/12_type_annotation_insertion.py b/packages/kilo-vscode/docs/nes-examples/12_type_annotation_insertion.py new file mode 100644 index 00000000000..8013be0a2a8 --- /dev/null +++ b/packages/kilo-vscode/docs/nes-examples/12_type_annotation_insertion.py @@ -0,0 +1,10 @@ +def multiply(a: int, b: int) -> int: + return a * b + + +def subtract(a: int, b: int) -> int: + return a - b + + +def add(a, b): + return a + b diff --git a/packages/kilo-vscode/docs/nes-examples/13_docstring_generation.py b/packages/kilo-vscode/docs/nes-examples/13_docstring_generation.py new file mode 100644 index 00000000000..928cbb322bc --- /dev/null +++ b/packages/kilo-vscode/docs/nes-examples/13_docstring_generation.py @@ -0,0 +1,11 @@ +import datetime + + +def parse_iso_datetime(s): + """Parse an ISO 8601 datetime string into a datetime.datetime.""" + return datetime.datetime.fromisoformat(s) + + +def parse_iso_date(s): + + return datetime.date.fromisoformat(s) diff --git a/packages/kilo-vscode/docs/nes-examples/14_no_op_suppression.py b/packages/kilo-vscode/docs/nes-examples/14_no_op_suppression.py new file mode 100644 index 00000000000..81d91f5554a --- /dev/null +++ b/packages/kilo-vscode/docs/nes-examples/14_no_op_suppression.py @@ -0,0 +1,13 @@ +def add(a: int, b: int) -> int: + """Return the sum of two integers.""" + return a + b + + +def multiply(a: int, b: int) -> int: + """Return the product of two integers.""" + return a * b + + +def subtract(a: int, b: int) -> int: + """Return a minus b.""" + return a - b diff --git a/packages/kilo-vscode/docs/nes-examples/INSTRUCTIONS.md b/packages/kilo-vscode/docs/nes-examples/INSTRUCTIONS.md new file mode 100644 index 00000000000..bc5f8822d0d --- /dev/null +++ b/packages/kilo-vscode/docs/nes-examples/INSTRUCTIONS.md @@ -0,0 +1,201 @@ +# NES Test Playground — Instructions + +These tests are designed so that the source files contain **no hints** about what Mercury is supposed to predict. All cursor placements and expected behaviors live here. Don't open this file inside the Dev Host while testing — keep it in a separate window so the model can't see it. + +## One-time setup + +1. **`bun run watch`** is already running for the kilocode extension. +2. In the kilocode VSCode window, press **F5** → opens the **Extension Development Host**. +3. In the Dev Host: `File → Open Folder…` → `packages/kilo-vscode/docs/nes-examples/` inside this repo. +4. Open Settings (`Cmd+,`), confirm: + - `kilo-code.new.autocomplete.enableAutoTrigger` → ✓ (default true) + - `kilo-code.new.autocomplete.model` → **Mercury Next Edit (Inception)** ← *NOT* "Mercury Edit 2", which is the classic FIM option + - `kilo-code.new.autocomplete.nextEdit.apiKey` → your `sk_...` key + - VSCode global `editor.inlineSuggest.enabled` → ✓ +5. To watch the pipeline live: `View → Output` → pick the **"Kilo Code · Next Edit"** channel. + +## Conventions used below + +- **Cursor placement**: where to click in the file before waiting. +- **Expected (editor)**: what should appear on screen. +- **Expected (channel)**: a stripped-down line you should see in the Next Edit output channel. +- **Path**: which NES rendering path this exercises — same-line ghost / off-cursor replace / off-cursor insert / suppressed. + +After each test, **don't accept** if you want to re-run it — the suggestion will edit the file. Either `Cmd+Z` after accept, or just navigate to the next test file. + +--- + +## Core tests (Python) + +### 01 — Finish a function body *(path: same-line insert)* +- **File**: `01_finish_function_body.py` +- **Cursor**: the empty indented line at the end of `factorial` (column 4). +- **Expected editor**: ghost text proposing the recursive case. +- **Expected channel**: `diff at lines [N..N], cursor at line N`, then `RENDER`. + +### 02 — Pattern continuation *(path: same-line ghost)* +- **File**: `02_pattern_continuation.py` +- **Cursor**: end of the last line (after `COLOR_BLUE = `). +- **Expected editor**: ghost text appending a hex color. +- **Expected channel**: `diff at lines [N..N], cursor at line N`, then `RENDER`. + +### 03 — Mid-identifier completion *(path: same-line ghost)* +- **File**: `03_typo_completion.py` +- **Cursor**: end of the file (after `return tot`). +- **Expected editor**: ghost text completing the identifier. +- **Path**: same-line. + +### 04 — Loop body inference *(path: same-line insert)* +- **File**: `04_loop_body.py` +- **Cursor**: the empty indented line inside the `for` loop (column 8). +- **Expected editor**: ghost text proposing the accumulator update. + +### 05 — Sibling method body *(path: same-line insert)* +- **File**: `05_class_method.py` +- **Cursor**: empty indented line inside `pop` (column 8). +- **Expected editor**: ghost text proposing the pop body. + +--- + +## Advanced Python tests + +### 07 — Multi-line rename refactor *(path: off-cursor replace)* +- **File**: `07_multiline_rename_refactor.py` +- **Cursor**: end of the line with `def compute_user_score(user_id, weight):` (the renamed signature). The body below still uses the old `u` / `w` names. +- **Expected editor**: strikethrough on the body lines + ghost showing the renamed body. +- **Tab**: jump, then apply. + +### 08 — Mixed insert + replace *(path: off-cursor replace, multi-line)* +- **File**: `08_mixed_insert_and_replace.py` +- **Cursor**: end of line `total = 0`. +- **Expected editor**: a decoration spanning the for-loop area showing the corrected accumulator body. The proposed text is longer than the original. + +### 10 — Mid-token completion *(path: same-line ghost)* +- **File**: `10_mid_token_completion.py` +- **Cursor**: end of the file (after `result = fib`). +- **Expected editor**: ghost extending the identifier and supplying a call. + +### 11 — Stub method with implemented siblings *(path: same-line insert)* +- **File**: `11_fill_sibling_method.py` +- **Cursor**: empty indented line inside `dequeue` (column 8). +- **Expected editor**: ghost text filling in the FIFO body. + +### 12 — Type annotation insertion *(path: same-line replace OR off-cursor replace)* +- **File**: `12_type_annotation_insertion.py` +- **Cursor**: on line `def add(a, b):` (anywhere on that line works; end-of-line is easiest). +- **Expected editor**: strikethrough + ghost showing the typed signature. May render as inline ghost depending on where you place the cursor on the line. + +### 13 — Docstring generation *(path: same-line insert)* +- **File**: `13_docstring_generation.py` +- **Cursor**: empty indented line directly under `def parse_iso_date(s):` (column 4). +- **Expected editor**: ghost text starting with `"""` and a one-line description. + +### 14 — No-op suppression (NEGATIVE) *(path: suppressed)* +- **File**: `14_no_op_suppression.py` +- **Cursor**: end of `return a + b`. +- **Expected editor**: NOTHING. No ghost, no decoration. +- **Expected channel**: either `identical replacement — no-op` or no `RENDER` line. +- **Fail mode**: any visible suggestion is a false positive. + +--- + +## TypeScript + +### ts_07 — Array transform *(same-line)* +- **File**: `ts_07_array_transform.ts` +- **Cursor**: end of `return users` inside `getActiveUserNames`. +- **Expected editor**: ghost text completing a `.filter(...).map(...)` chain. + +### ts_08 — Param type annotations *(off-cursor replace)* +- **File**: `ts_08_param_types.ts` +- **Cursor**: end of file (after `main();`). +- **Expected editor**: strikethrough on `add(a, b)` signature + ghost showing the typed version. + +### ts_09 — React event handler *(same-line insert)* +- **File**: `ts_09_jsx_handler.tsx` +- **Cursor**: empty indented line inside `handleClick` (column 4). +- **Expected editor**: ghost text incrementing `count`. + +--- + +## Go + +### go_07 — Error handling *(same-line insert, multi-line)* +- **File**: `go_07_error_handling.go` +- **Cursor**: empty line right after `data, err := os.ReadFile(path)`. +- **Expected editor**: ghost text proposing the canonical `if err != nil { return nil, err }` block. + +### go_08 — Struct method body *(same-line insert)* +- **File**: `go_08_struct_method.go` +- **Cursor**: empty indented line inside `Area()`. +- **Expected editor**: ghost text computing area from `Width` and `Height`. + +### go_09 — Goroutine + channel *(same-line insert, multi-line)* +- **File**: `go_09_goroutine_channel.go` +- **Cursor**: empty indented line inside the goroutine. +- **Expected editor**: ghost text producing values onto the channel and closing it. + +--- + +## Rust + +### rs_07 — Match-arm completion *(same-line)* +- **File**: `rs_07_match_arms.rs` +- **Cursor**: empty indented line inside the `match s {` body, after the `Square` arm (column 8). +- **Expected editor**: ghost text proposing the missing `Rectangle` and `Triangle` arms. + +### rs_08 — Result chaining *(same-line ghost)* +- **File**: `rs_08_result_chain.rs` +- **Cursor**: end of the line `let n = s.trim()` (no semicolon yet). +- **Expected editor**: ghost text continuing the chain into a parsed `i32`. + +### rs_09 — Lifetime annotation *(off-cursor replace)* +- **File**: `rs_09_lifetimes.rs` +- **Cursor**: end of the file (after `main`'s closing `}`). +- **Expected editor**: strikethrough on the `fn longest(...)` signature + ghost showing the lifetime-annotated version. + +--- + +## JavaScript + +### js_07 — Async/await *(same-line insert, multi-line)* +- **File**: `js_07_async_await.js` +- **Cursor**: empty indented line inside the `try {` block (column 8). +- **Expected editor**: ghost text completing fetch + json parse. + +### js_08 — Express route handler *(same-line insert, multi-line)* +- **File**: `js_08_express_route.js` +- **Cursor**: empty indented line inside the GET handler (column 4). +- **Expected editor**: ghost text implementing get-by-id (lookup, 404, json response). + +--- + +## SQL + +### sql_07 — JOIN clause *(same-line ghost)* +- **File**: `sql_07_join.sql` +- **Cursor**: end of the line `FROM orders o`. +- **Expected editor**: ghost text completing the JOIN against `customers`. + +### sql_08 — WHERE filter *(same-line ghost)* +- **File**: `sql_08_where_filter.sql` +- **Cursor**: end of the bare `WHERE` line. +- **Expected editor**: ghost text proposing a predicate. + +--- + +## Markdown (negative) + +### md_07 — Prose, should stay quiet *(suppressed)* +- **File**: `md_07_prose_negative.md` +- **Cursor**: end of the last sentence. +- **Expected editor**: NOTHING (ideally). If Mercury does propose a continuation of the prose, note it as a soft fail — code models writing your README isn't the v0 product. + +--- + +## Troubleshooting + +- **No log lines appearing**: confirm the output channel is "Kilo Code · Next Edit". Also confirm you reloaded the Dev Host after rebuilding. +- **`[NES] skip — no API key resolved`**: setting wasn't saved. Re-paste the key, hit Enter, reload. +- **`[NES] <- 400`**: regression on prompt shape — capture the body in the channel and ping the integration owner. +- **Visible suggestion that's not in this doc**: write it down. Unexpected wins (or false positives) are the most useful signal. diff --git a/packages/kilo-vscode/docs/nes-examples/go_07_error_handling.go b/packages/kilo-vscode/docs/nes-examples/go_07_error_handling.go new file mode 100644 index 00000000000..96d50b1e931 --- /dev/null +++ b/packages/kilo-vscode/docs/nes-examples/go_07_error_handling.go @@ -0,0 +1,21 @@ +package main + +import ( + "fmt" + "os" +) + +func loadConfig(path string) ([]byte, error) { + data, err := os.ReadFile(path) + + return data, nil +} + +func main() { + cfg, err := loadConfig("config.json") + if err != nil { + fmt.Println("error:", err) + return + } + fmt.Println(string(cfg)) +} diff --git a/packages/kilo-vscode/docs/nes-examples/go_08_struct_method.go b/packages/kilo-vscode/docs/nes-examples/go_08_struct_method.go new file mode 100644 index 00000000000..dbfb598a331 --- /dev/null +++ b/packages/kilo-vscode/docs/nes-examples/go_08_struct_method.go @@ -0,0 +1,22 @@ +package main + +import "fmt" + +type Rectangle struct { + Width float64 + Height float64 +} + +func (r Rectangle) Perimeter() float64 { + return 2 * (r.Width + r.Height) +} + +func (r Rectangle) Area() float64 { + +} + +func main() { + r := Rectangle{Width: 3, Height: 4} + fmt.Println("perimeter:", r.Perimeter()) + fmt.Println("area:", r.Area()) +} diff --git a/packages/kilo-vscode/docs/nes-examples/go_09_goroutine_channel.go b/packages/kilo-vscode/docs/nes-examples/go_09_goroutine_channel.go new file mode 100644 index 00000000000..4fe79b9fe87 --- /dev/null +++ b/packages/kilo-vscode/docs/nes-examples/go_09_goroutine_channel.go @@ -0,0 +1,15 @@ +package main + +import "fmt" + +func main() { + ch := make(chan int) + + go func() { + + }() + + for v := range ch { + fmt.Println("got:", v) + } +} diff --git a/packages/kilo-vscode/docs/nes-examples/js_07_async_await.js b/packages/kilo-vscode/docs/nes-examples/js_07_async_await.js new file mode 100644 index 00000000000..f61b734ccb8 --- /dev/null +++ b/packages/kilo-vscode/docs/nes-examples/js_07_async_await.js @@ -0,0 +1,14 @@ +async function fetchUser(id) { + try { + } catch (err) { + console.error("fetchUser failed", err) + return null + } +} + +async function main() { + const user = await fetchUser(42) + console.log("user:", user) +} + +main() diff --git a/packages/kilo-vscode/docs/nes-examples/js_08_express_route.js b/packages/kilo-vscode/docs/nes-examples/js_08_express_route.js new file mode 100644 index 00000000000..83d4c9ce261 --- /dev/null +++ b/packages/kilo-vscode/docs/nes-examples/js_08_express_route.js @@ -0,0 +1,20 @@ +const app = { + get: (_path, _handler) => app, + post: (_path, _handler) => app, + listen: (_port, cb) => cb && cb(), +} + +const users = [ + { id: 1, name: "ada" }, + { id: 2, name: "lin" }, +] + +app.get("/users/:id", (req, res) => {}) + +app.post("/users", (req, res) => { + const user = { id: users.length + 1, name: req.body.name } + users.push(user) + res.status(201).json(user) +}) + +app.listen(3000, () => console.log("listening on :3000")) diff --git a/packages/kilo-vscode/docs/nes-examples/md_07_prose_negative.md b/packages/kilo-vscode/docs/nes-examples/md_07_prose_negative.md new file mode 100644 index 00000000000..3751a203cf2 --- /dev/null +++ b/packages/kilo-vscode/docs/nes-examples/md_07_prose_negative.md @@ -0,0 +1,10 @@ +# Mercury Edit 2 — Quick Notes + +Mercury Edit 2 is a small, fast model trained to predict the user's +next single edit given the current file, cursor position, and recent +edit history. It targets latency under 200 ms on typical files and +returns a unified-diff-like patch scoped to a window around the cursor. + +Unlike chat-style completions, the model is biased toward minimal, +local changes — finishing a function body, fixing a typo, propagating +a rename — rather than generating new files from scratch. diff --git a/packages/kilo-vscode/docs/nes-examples/rs_07_match_arms.rs b/packages/kilo-vscode/docs/nes-examples/rs_07_match_arms.rs new file mode 100644 index 00000000000..e666f246a03 --- /dev/null +++ b/packages/kilo-vscode/docs/nes-examples/rs_07_match_arms.rs @@ -0,0 +1,25 @@ +enum Shape { + Circle(f64), + Square(f64), + Rectangle(f64, f64), + Triangle(f64, f64), +} + +fn area(s: &Shape) -> f64 { + match s { + Shape::Circle(r) => std::f64::consts::PI * r * r, + Shape::Square(side) => side * side, + + } +} + +fn main() { + let shapes = vec![ + Shape::Circle(1.0), + Shape::Rectangle(2.0, 3.0), + Shape::Triangle(4.0, 5.0), + ]; + for s in &shapes { + println!("area = {}", area(s)); + } +} diff --git a/packages/kilo-vscode/docs/nes-examples/rs_08_result_chain.rs b/packages/kilo-vscode/docs/nes-examples/rs_08_result_chain.rs new file mode 100644 index 00000000000..2c51c0c6172 --- /dev/null +++ b/packages/kilo-vscode/docs/nes-examples/rs_08_result_chain.rs @@ -0,0 +1,14 @@ +fn parse_int(s: &str) -> Option { + let n = s.trim() + Some(n * 2) +} + +fn main() { + let inputs = [" 21 ", "not-a-number", "10"]; + for s in &inputs { + match parse_int(s) { + Some(v) => println!("{} -> {}", s, v), + None => println!("{} -> skipped", s), + } + } +} diff --git a/packages/kilo-vscode/docs/nes-examples/rs_09_lifetimes.rs b/packages/kilo-vscode/docs/nes-examples/rs_09_lifetimes.rs new file mode 100644 index 00000000000..86868749707 --- /dev/null +++ b/packages/kilo-vscode/docs/nes-examples/rs_09_lifetimes.rs @@ -0,0 +1,14 @@ +fn longest(a: &str, b: &str) -> &str { + if a.len() >= b.len() { + a + } else { + b + } +} + +fn main() { + let s1 = String::from("hello world"); + let s2 = String::from("hi"); + let out = longest(&s1, &s2); + println!("longest = {}", out); +} diff --git a/packages/kilo-vscode/docs/nes-examples/sql_07_join.sql b/packages/kilo-vscode/docs/nes-examples/sql_07_join.sql new file mode 100644 index 00000000000..b692494f243 --- /dev/null +++ b/packages/kilo-vscode/docs/nes-examples/sql_07_join.sql @@ -0,0 +1,9 @@ +SELECT + c.name, + SUM(o.total) AS total_spent +FROM orders o + +WHERE o.created_at >= '2026-01-01' +GROUP BY c.name +ORDER BY total_spent DESC +LIMIT 10; diff --git a/packages/kilo-vscode/docs/nes-examples/sql_08_where_filter.sql b/packages/kilo-vscode/docs/nes-examples/sql_08_where_filter.sql new file mode 100644 index 00000000000..ed2fd4a6a7f --- /dev/null +++ b/packages/kilo-vscode/docs/nes-examples/sql_08_where_filter.sql @@ -0,0 +1,4 @@ +SELECT id, email +FROM users +WHERE +ORDER BY last_login_at DESC; diff --git a/packages/kilo-vscode/docs/nes-examples/ts_07_array_transform.ts b/packages/kilo-vscode/docs/nes-examples/ts_07_array_transform.ts new file mode 100644 index 00000000000..f939ce4cd25 --- /dev/null +++ b/packages/kilo-vscode/docs/nes-examples/ts_07_array_transform.ts @@ -0,0 +1,17 @@ +interface User { + id: number + name: string + active: boolean +} + +function getActiveUserNames(users: User[]): string[] { + return users +} + +const sample: User[] = [ + { id: 1, name: "ada", active: true }, + { id: 2, name: "lin", active: false }, + { id: 3, name: "rin", active: true }, +] + +console.log(getActiveUserNames(sample)) diff --git a/packages/kilo-vscode/docs/nes-examples/ts_08_param_types.ts b/packages/kilo-vscode/docs/nes-examples/ts_08_param_types.ts new file mode 100644 index 00000000000..5330b68f14a --- /dev/null +++ b/packages/kilo-vscode/docs/nes-examples/ts_08_param_types.ts @@ -0,0 +1,19 @@ +function double(x: number): number { + return x * 2 +} + +function add(a, b) { + return a + b +} + +function negate(x: number): number { + return -x +} + +function main(): void { + console.log(double(3)) + console.log(add(2, 4)) + console.log(negate(7)) +} + +main() diff --git a/packages/kilo-vscode/docs/nes-examples/ts_09_jsx_handler.tsx b/packages/kilo-vscode/docs/nes-examples/ts_09_jsx_handler.tsx new file mode 100644 index 00000000000..5c5ed11bc20 --- /dev/null +++ b/packages/kilo-vscode/docs/nes-examples/ts_09_jsx_handler.tsx @@ -0,0 +1,18 @@ +declare const React: { + useState: (initial: T) => [T, (next: T) => void] +} + +function Counter(): JSX.Element { + const [count, setCount] = React.useState(0) + + function handleClick() {} + + return ( +
+

Count: {count}

+ +
+ ) +} + +export default Counter diff --git a/packages/kilo-vscode/eslint.config.mjs b/packages/kilo-vscode/eslint.config.mjs index 0914b0366f7..c7b65c8aac5 100644 --- a/packages/kilo-vscode/eslint.config.mjs +++ b/packages/kilo-vscode/eslint.config.mjs @@ -38,7 +38,7 @@ export default [ // New code must stay ≤ 20. Do not raise these caps; refactor instead. { files: ["src/KiloProvider.ts"], - rules: { complexity: ["error", 150], "max-lines": ["error", 3600] }, + rules: { complexity: ["error", 150], "max-lines": ["error", 3700] }, }, { files: ["webview-ui/agent-manager/AgentManagerApp.tsx"], diff --git a/packages/kilo-vscode/package.json b/packages/kilo-vscode/package.json index 25b4536daee..bbcff2d2d19 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.12", "icon": "assets/icons/logo-outline-black.png", "galleryBanner": { "color": "#FFFFFF", @@ -31,19 +31,25 @@ ], "keywords": [ "kilo", - "claude", - "dev", - "mcp", - "openrouter", - "coding", - "agent", - "autonomous", - "chatgpt", - "sonnet", - "ai", - "llama", "Kilo Code", - "kilocode" + "kilocode", + "ai", + "agent", + "agentic", + "coding", + "coding-agent", + "coding-assistant", + "autocomplete", + "code-completion", + "pair-programming", + "chat", + "terminal", + "chatgpt", + "claude", + "sonnet", + "anthropic", + "openai", + "zoo-code" ], "activationEvents": [ "onStartupFinished" @@ -125,6 +131,41 @@ "title": "Settings", "icon": "$(settings-gear)" }, + { + "command": "kilo-code.new.sidebarTitle.plusButtonClicked", + "title": "New Task", + "icon": "$(add)" + }, + { + "command": "kilo-code.new.sidebarTitle.agentManagerOpen", + "title": "Agent Manager", + "icon": "$(organization)" + }, + { + "command": "kilo-code.new.sidebarTitle.kiloClawOpen", + "title": "KiloClaw", + "icon": "$(comment-discussion)" + }, + { + "command": "kilo-code.new.sidebarTitle.marketplaceButtonClicked", + "title": "Marketplace", + "icon": "$(extensions)" + }, + { + "command": "kilo-code.new.sidebarTitle.historyButtonClicked", + "title": "History", + "icon": "$(history)" + }, + { + "command": "kilo-code.new.sidebarTitle.profileButtonClicked", + "title": "Profile", + "icon": "$(account)" + }, + { + "command": "kilo-code.new.sidebarTitle.settingsButtonClicked", + "title": "Settings", + "icon": "$(settings-gear)" + }, { "command": "kilo-code.new.openInTab", "title": "Kilo Code", @@ -153,6 +194,16 @@ "title": "Cancel Suggested Edits", "category": "Kilo Code" }, + { + "command": "kilo-code.new.autocomplete.nextEdit.acceptOrJump", + "title": "Next Edit: Accept or Jump to Suggested Edit", + "category": "Kilo Code" + }, + { + "command": "kilo-code.new.autocomplete.nextEdit.dismiss", + "title": "Next Edit: Dismiss Pending Suggestion", + "category": "Kilo Code" + }, { "command": "kilo-code.new.agentManager.previousSession", "title": "Agent Manager: Previous Session", @@ -363,39 +414,69 @@ } ], "menus": { + "commandPalette": [ + { + "command": "kilo-code.new.sidebarTitle.plusButtonClicked", + "when": "false" + }, + { + "command": "kilo-code.new.sidebarTitle.historyButtonClicked", + "when": "false" + }, + { + "command": "kilo-code.new.sidebarTitle.agentManagerOpen", + "when": "false" + }, + { + "command": "kilo-code.new.sidebarTitle.kiloClawOpen", + "when": "false" + }, + { + "command": "kilo-code.new.sidebarTitle.marketplaceButtonClicked", + "when": "false" + }, + { + "command": "kilo-code.new.sidebarTitle.profileButtonClicked", + "when": "false" + }, + { + "command": "kilo-code.new.sidebarTitle.settingsButtonClicked", + "when": "false" + } + ], "view/title": [ { - "command": "kilo-code.new.plusButtonClicked", + "command": "kilo-code.new.sidebarTitle.plusButtonClicked", "group": "navigation@0", "when": "view == kilo-code.SidebarProvider" }, { - "command": "kilo-code.new.historyButtonClicked", + "command": "kilo-code.new.sidebarTitle.historyButtonClicked", "group": "navigation@1", "when": "view == kilo-code.SidebarProvider" }, { - "command": "kilo-code.new.agentManagerOpen", + "command": "kilo-code.new.sidebarTitle.agentManagerOpen", "group": "navigation@2", "when": "view == kilo-code.SidebarProvider" }, { - "command": "kilo-code.new.kiloClawOpen", + "command": "kilo-code.new.sidebarTitle.kiloClawOpen", "group": "navigation@3", "when": "view == kilo-code.SidebarProvider" }, { - "command": "kilo-code.new.marketplaceButtonClicked", + "command": "kilo-code.new.sidebarTitle.marketplaceButtonClicked", "group": "navigation@4", "when": "view == kilo-code.SidebarProvider" }, { - "command": "kilo-code.new.profileButtonClicked", + "command": "kilo-code.new.sidebarTitle.profileButtonClicked", "group": "navigation@5", "when": "view == kilo-code.SidebarProvider" }, { - "command": "kilo-code.new.settingsButtonClicked", + "command": "kilo-code.new.sidebarTitle.settingsButtonClicked", "group": "navigation@6", "when": "view == kilo-code.SidebarProvider" } @@ -685,6 +766,16 @@ "key": "ctrl+l", "mac": "cmd+l", "when": "editorTextFocus && !editorTabMovesFocus && !inSnippetMode && kilocode.autocomplete.enableSmartInlineTaskKeybinding && github.copilot.completions.enabled" + }, + { + "command": "kilo-code.new.autocomplete.nextEdit.acceptOrJump", + "key": "tab", + "when": "editorTextFocus && !editorTabMovesFocus && !inSnippetMode && !suggestWidgetVisible && kilo-code.nextEdit.hasPendingSuggestion" + }, + { + "command": "kilo-code.new.autocomplete.nextEdit.dismiss", + "key": "escape", + "when": "editorTextFocus && !editorTabMovesFocus && !inSnippetMode && kilo-code.nextEdit.hasPendingSuggestion" } ], "configuration": { @@ -753,13 +844,35 @@ "type": "string", "enum": [ "mistralai/codestral-2508", - "inception/mercury-edit" + "inception/mercury-edit-2", + "inception/mercury-next-edit", + "codestral-2508", + "mercury-edit-2", + "mercury-next-edit" ], "enumDescriptions": [ - "Codestral by Mistral AI (default)", - "Mercury Edit by Inception" + "Codestral via Kilo Gateway", + "Mercury Edit 2 via Kilo Gateway", + "Mercury Next Edit (multi-line edit predictions with jump-to-edit UX) via Kilo Gateway", + "Codestral via your connected Mistral provider API key", + "Mercury Edit 2 (FIM) via your connected Inception provider API key", + "Mercury Next Edit (multi-line edit predictions with jump-to-edit UX) via your connected Inception provider API key" ], - "description": "Model to use for inline autocomplete suggestions" + "description": "Model to use for inline autocomplete suggestions. If unset, the recommended default is used." + }, + "kilo-code.new.autocomplete.provider": { + "type": "string", + "enum": [ + "kilo", + "mistral", + "inception" + ], + "enumDescriptions": [ + "Use autocomplete models through Kilo Gateway", + "Use autocomplete models through your connected Mistral provider API key", + "Use autocomplete models through your connected Inception provider API key" + ], + "description": "Provider to use for inline autocomplete suggestions. If unset, Kilo Gateway is used." }, "kilo-code.new.autocomplete.enableAutoTrigger": { "type": "boolean", @@ -776,6 +889,15 @@ "default": false, "description": "Enable chat textarea autocomplete" }, + "kilo-code.new.speechToText.enabled": { + "type": "boolean", + "default": false, + "description": "Enable experimental speech-to-text voice input in prompt fields. Uses your Kilo account through the Kilo Gateway." + }, + "kilo-code.new.speechToText.model": { + "type": "string", + "description": "Model to use for experimental speech-to-text voice input. Requires Kilo Gateway." + }, "kilo-code.new.claudeCodeCompat": { "type": "boolean", "default": false, @@ -886,7 +1008,7 @@ "format": "prettier --write .", "format:check": "prettier --check .", "knip": "knip", - "check-kilocode-change": "! grep -rn 'kilocode_change' . ../kilo-ui/ --exclude='package.json' --exclude='*.md' --exclude-dir='node_modules' --exclude-dir='dist' | grep -v '`kilocode_change`'", + "check-kilocode-change": "! grep -rIn 'kilocode_change' . ../kilo-ui/ --exclude='package.json' --exclude='*.md' --exclude-dir='node_modules' --exclude-dir='dist' | grep -v '`kilocode_change`'", "lint": "eslint src webview-ui", "test": "vscode-test", "test:unit": "bun test tests/unit/", @@ -957,5 +1079,6 @@ "web-tree-sitter": "^0.24.7", "yaml": "2.8.3", "zod": "^3.24.2" - } + }, + "peerDependencies": {} } diff --git a/packages/kilo-vscode/script/build.ts b/packages/kilo-vscode/script/build.ts index cb285b327b4..56f65d9f5c0 100644 --- a/packages/kilo-vscode/script/build.ts +++ b/packages/kilo-vscode/script/build.ts @@ -2,6 +2,8 @@ import { $ } from "bun" import { join } from "node:path" import { existsSync, mkdirSync, rmSync, chmodSync } from "node:fs" +import { copyTreeSitterResources } from "../src/services/cli-backend/cli-resources" +import { ensureFfmpegForTarget } from "./ffmpeg-helper" const packageJsonPath = join(import.meta.dir, "..", "package.json") const packageJson = await Bun.file(packageJsonPath).json() @@ -74,6 +76,7 @@ for (const config of targets) { console.log(` 📥 Copying binary from ${config.cliDir}/bin/${config.binary}...`) await $`cp ${sourceBinary} ${targetBinary}` + await copyTreeSitterResources(sourceBinary, targetBinary) if (config.binary !== "kilo.exe") { chmodSync(targetBinary, 0o755) @@ -81,6 +84,9 @@ for (const config of targets) { console.log(` ✅ Binary ready at ${targetBinary}`) + console.log("Adding bundled FFmpeg helper...") + await ensureFfmpegForTarget(config.target, binDir) + console.log(` 📦 Packaging .vsix for ${config.target}${prerelease ? " (pre-release)" : ""}...`) const vsixPath = join(outDir, `kilo-vscode-${config.target}.vsix`) const args = ["--no-dependencies", "--skip-license", "--target", config.target, "-o", vsixPath] diff --git a/packages/kilo-vscode/script/dev-snapshot.ts b/packages/kilo-vscode/script/dev-snapshot.ts index 7637579ea38..edbe590573e 100755 --- a/packages/kilo-vscode/script/dev-snapshot.ts +++ b/packages/kilo-vscode/script/dev-snapshot.ts @@ -41,7 +41,7 @@ console.log("\n📦 Rebuilding SDK...") await $`bun run --cwd ../sdk/js build`.cwd(root) console.log("\n🔧 Preparing CLI binary...") -await $`bun script/local-bin.ts`.cwd(root) +await $`bun script/local-bin.ts --force`.cwd(root) console.log("\n✅ Type-checking...") await $`bun run typecheck`.cwd(root) diff --git a/packages/kilo-vscode/script/ffmpeg-helper.ts b/packages/kilo-vscode/script/ffmpeg-helper.ts new file mode 100644 index 00000000000..c0b5e440ef4 --- /dev/null +++ b/packages/kilo-vscode/script/ffmpeg-helper.ts @@ -0,0 +1,49 @@ +import { $ } from "bun" +import { chmodSync, copyFileSync, existsSync, mkdirSync, rmSync } from "node:fs" +import { join } from "node:path" + +const packages: Record = { + "darwin-x64": "@ffmpeg-installer/darwin-x64@4.1.0", + "darwin-arm64": "@ffmpeg-installer/darwin-arm64@4.1.5", + "linux-x64": "@ffmpeg-installer/linux-x64@4.1.0", + "linux-arm64": "@ffmpeg-installer/linux-arm64@4.1.4", + "alpine-x64": "@ffmpeg-installer/linux-x64@4.1.0", + "alpine-arm64": "@ffmpeg-installer/linux-arm64@4.1.4", + "win32-x64": "@ffmpeg-installer/win32-x64@4.1.0", +} + +export async function ensureFfmpegForTarget(target: string, bin: string): Promise { + if (target === "win32-arm64") { + console.warn("No Windows ARM64 FFmpeg helper package is available; speech input will use system FFmpeg.") + return + } + + const spec = packages[target] + if (!spec) throw new Error(`No FFmpeg helper package configured for target ${target}`) + + const exe = target.startsWith("win32") ? "ffmpeg.exe" : "ffmpeg" + const dest = join(bin, exe) + if (existsSync(dest)) return + + const tmp = join(bin, ".ffmpeg-tmp") + rmSync(tmp, { recursive: true, force: true }) + mkdirSync(tmp, { recursive: true }) + + try { + const packed = await $`npm pack ${spec} --pack-destination ${tmp}`.quiet() + const name = packed.text().trim().split(/\s+/).pop() + if (!name) throw new Error(`npm pack did not return a tarball for ${spec}`) + + await $`tar -xzf ${join(tmp, name)} -C ${tmp}`.quiet() + copyFileSync(join(tmp, "package", exe), dest) + if (!target.startsWith("win32")) chmodSync(dest, 0o755) + } finally { + rmSync(tmp, { recursive: true, force: true }) + } +} + +export function currentFfmpegTarget(): string { + const os = process.platform === "win32" ? "win32" : process.platform + const arch = process.arch === "x64" ? "x64" : process.arch === "arm64" ? "arm64" : process.arch + return `${os}-${arch}` +} diff --git a/packages/kilo-vscode/script/local-bin.ts b/packages/kilo-vscode/script/local-bin.ts index aeb57e81643..951d0f8ce46 100644 --- a/packages/kilo-vscode/script/local-bin.ts +++ b/packages/kilo-vscode/script/local-bin.ts @@ -2,6 +2,8 @@ import { $ } from "bun" import { join, relative, dirname, basename } from "node:path" import { chmodSync, statSync, rmSync, readdirSync, existsSync } from "node:fs" +import { copyTreeSitterResources, hasTreeSitterResources } from "../src/services/cli-backend/cli-resources" +import { currentFfmpegTarget, ensureFfmpegForTarget } from "./ffmpeg-helper" const forceRebuild = process.argv.includes("--force") @@ -21,6 +23,7 @@ const kiloVscodeDir = join(import.meta.dir, "..") const packagesDir = join(kiloVscodeDir, "..") const opencodeDir = join(packagesDir, "opencode") const coreDir = join(packagesDir, "core") +const indexingDir = join(packagesDir, "kilo-indexing") const targetBinDir = join(kiloVscodeDir, "bin") const binName = process.platform === "win32" ? "kilo.exe" : "kilo" @@ -35,7 +38,8 @@ async function cliSourceHash(): Promise { try { const opencodeResult = await $`git log -1 --format=%H -- .`.cwd(opencodeDir).quiet() const coreResult = await $`git log -1 --format=%H -- .`.cwd(coreDir).quiet() - return `${opencodeResult.text().trim()}-${coreResult.text().trim()}` || null + const indexingResult = await $`git log -1 --format=%H -- .`.cwd(indexingDir).quiet() + return `${opencodeResult.text().trim()}-${coreResult.text().trim()}-${indexingResult.text().trim()}` || null } catch { return null } @@ -45,7 +49,12 @@ async function isDirty(): Promise { try { const opencodeResult = await $`git status --porcelain -- .`.cwd(opencodeDir).quiet() const coreResult = await $`git status --porcelain -- .`.cwd(coreDir).quiet() - return opencodeResult.text().trim().length > 0 || coreResult.text().trim().length > 0 + const indexingResult = await $`git status --porcelain -- .`.cwd(indexingDir).quiet() + return ( + opencodeResult.text().trim().length > 0 || + coreResult.text().trim().length > 0 || + indexingResult.text().trim().length > 0 + ) } catch { return false } @@ -82,6 +91,7 @@ async function findKiloBinaryInOpencodeDist(): Promise { const preferred = join(distDir, `@kilocode`, tag, "bin", binName) try { statSync(preferred) + if (!hasTreeSitterResources(preferred)) return null return preferred } catch { // fall through to generic search @@ -107,6 +117,7 @@ async function findKiloBinaryInOpencodeDist(): Promise { continue } if (e.isFile() && (e.name === "kilo" || e.name === "kilo.exe") && basename(dirname(p)) === "bin") { + if (!hasTreeSitterResources(p)) continue return p } } @@ -158,6 +169,7 @@ async function main() { log( `CLI binary already present at ${relative(kiloVscodeDir, targetBinPath)} (${Math.round(st.size / 1024 / 1024)}MB). Use --force to rebuild.`, ) + await ensureFfmpegForTarget(currentFfmpegTarget(), targetBinDir) return } @@ -180,7 +192,9 @@ async function main() { const sourceBinPath = await ensureBuiltBinary() await $`mkdir -p ${targetBinDir}` await $`cp ${sourceBinPath} ${targetBinPath}` + await copyTreeSitterResources(sourceBinPath, targetBinPath) chmodSync(targetBinPath, 0o755) + await ensureFfmpegForTarget(currentFfmpegTarget(), targetBinDir) // Record the CLI source version so future runs detect when a rebuild is needed const hash = await cliSourceHash() diff --git a/packages/kilo-vscode/script/watch-cli.ts b/packages/kilo-vscode/script/watch-cli.ts index d7b9943563f..2a50cf8e7a2 100644 --- a/packages/kilo-vscode/script/watch-cli.ts +++ b/packages/kilo-vscode/script/watch-cli.ts @@ -9,6 +9,7 @@ import { watch, chmodSync } from "node:fs" import { join, relative } from "node:path" import { $ } from "bun" +import { copyTreeSitterResources } from "../src/services/cli-backend/cli-resources" const kiloVscodeDir = join(import.meta.dir, "..") const packagesDir = join(kiloVscodeDir, "..") @@ -57,6 +58,7 @@ async function rebuild() { await $`mkdir -p ${targetBinDir}` await $`cp ${source} ${targetBinPath}` + await copyTreeSitterResources(source, targetBinPath) chmodSync(targetBinPath, 0o755) const elapsed = ((performance.now() - start) / 1000).toFixed(1) diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 96932f34d35..8657c062cf2 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, @@ -39,6 +40,7 @@ import { resolveContextDirectory, resolveNewSessionDirectory, resolveWorkspaceDirectory, + sameDirectory, SessionStreamScheduler, type SessionRefreshContext, } from "./kilo-provider-utils" @@ -46,7 +48,12 @@ import { GitOps } from "./agent-manager/GitOps" import { GitStatsPoller, type LocalStats } from "./agent-manager/GitStatsPoller" import { diffSummary as localDiffSummary } from "./agent-manager/local-diff" import { getWorkspaceRoot } from "./review-utils" -import { MarketplaceService, type MarketplaceItem, type RemoveResult } from "./services/marketplace" +import { + MarketplaceService, + type MarketplaceItem, + type AgentMarketplaceItem, + type RemoveResult, +} from "./services/marketplace" import type { RemoteStatusService } from "./services/RemoteStatusService" import { resolveProjectDirectory } from "./project-directory" import { getBusySessionCount, seedSessionStatuses } from "./session-status" @@ -55,6 +62,7 @@ import { retry } from "./services/cli-backend/retry" import { slimPart, slimParts } from "./kilo-provider/slim-metadata" import { handleSidebarWorktreeMessage } from "./kilo-provider/sidebar-worktree" import { parseMessageFiles, type MessageFile } from "./kilo-provider/message-files" +import { renameSession } from "./kilo-provider/rename-session" import { handleFileSearch } from "./kilo-provider/file-search" import { watchFontSizeConfig } from "./kilo-provider/font-size" import { getTerminalContents } from "./services/terminal/context" @@ -64,14 +72,15 @@ import { matchFollowup, recordFollowup, type Followup } from "./kilo-provider/fo import { clearCommandsCache, loadCommands } from "./kilo-provider/commands" import { fetchMessagePage, MESSAGE_PAGE_LIMIT } from "./kilo-provider/message-page" import { childID } from "./kilo-provider/task-session" +import { VisibleTaskStreams } from "./kilo-provider/visible-task-streams" import { handleNetworkEvent, clearNetworkWaits } from "./kilo-provider/network" import { abortSession } from "./kilo-provider/abort" import { buildAutocompleteSettingsMessage, - routeAutocompleteMessage, validAutocompleteSetting, watchAutocompleteConfig, } from "./services/autocomplete/settings" +import { routeEarlyMessage } from "./kilo-provider/early-message" import * as ModelState from "./kilo-provider/model-state" import { handleForkSession } from "./kilo-provider/fork-session" import { openConfig } from "./kilo-provider/open-config" @@ -112,7 +121,7 @@ import { handleQuestionReject, fetchAndSendPendingQuestions, } from "./kilo-provider/handlers/question" -import { fetchAndSendPendingSuggestions, routeSuggestionWebviewMessage } from "./kilo-provider/handlers/suggestion" +import { fetchAndSendPendingSuggestions } from "./kilo-provider/handlers/suggestion" import { nativeTitle } from "./kilo-provider/native-tab-title" import { @@ -133,8 +142,10 @@ import { configFeatures } from "./features" import { createAutoApproveBridge } from "./kilo-provider/auto-approve" import type { KiloProviderOptions } from "./kilo-provider/options" import { fetchKiloEmbeddingModelCatalog } from "@kilocode/kilo-gateway" +import { stopSessionProcesses } from "./kilo-provider/background-process" 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, @@ -196,21 +207,15 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper private promptRecovery: Promise | null = null private trackedSessionIds: Set = new Set() private syncedChildSessions: Set = new Set() - /** Tracks the latest status for each session, used to warn before destructive config operations. */ - private sessionStatusMap = new Map() - /** Per-session directory overrides (e.g., worktree paths registered by AgentManagerProvider). */ - private sessionDirectories = new Map() + private sessionStatusMap = new Map() // Latest status used for destructive config warnings. + private sessionDirectories = new Map() // Per-session directory overrides, such as Agent Manager worktrees. private permissionDirectories = new Map() - /** Project ID for the current workspace, used to filter out sessions from other repositories. */ - private projectID: string | undefined - /** Abort controller for the current loadMessages request; aborted when a new session is selected. */ - private loadMessagesAbort: AbortController | null = null - /** Per-session last focus-mode reconcile timestamp — throttles rapid tab switching. */ - private lastReconciledAt = new Map() - /** Set when refreshSessions() is called before the client is ready. - * Cleared and retried once the connection transitions to "connected". */ - private pendingSessionRefresh = false + private projectID: string | undefined // Current workspace project ID used to filter sessions. + private loadMessagesAbort: AbortController | null = null // Current load request cancellation. + private lastReconciledAt = new Map() // Per-session focus-mode reconcile timestamp. + private pendingSessionRefresh = false // Refresh requested before the client is ready. private readonly streams = new SessionStreamScheduler((msg) => this.postMessage(msg)) + private readonly visibleTaskStreams = new VisibleTaskStreams((id, visible) => this.streams.setVisible(id, visible)) private readonly confirmations = new MessageConfirmation() private unsubscribeEvent: (() => void) | null = null private unsubscribeState: (() => void) | null = null @@ -287,6 +292,13 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.opts.tabTitle?.(nativeTitle(session)) } + private stopCurrentSessionProcesses(next?: string): void { + const sid = this.contextSessionID ?? this.currentSession?.id + if (!sid || sid === next) return + const session = this.currentSession?.id === sid ? this.currentSession : undefined + void stopSessionProcesses(this.client, sid, this.getSessionDirectory(sid, session)) + } + private sendRemoteStatus(): void { const s = this.remoteService?.getState() if (s) this.postMessage({ type: "remoteStatus", enabled: s.enabled, connected: s.connected }) @@ -297,6 +309,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper else this.connectionService.unregisterFocused(this.instanceId) } + public setStreamVisibility(active: boolean): void { + this.visibleTaskStreams.setActive(active) + } + public setProjectDirectory(directory: string | null): void { if (this.projectDirectory === directory) return this.projectDirectory = directory @@ -433,11 +449,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper _context: vscode.WebviewViewResolveContext, _token: vscode.CancellationToken, ) { - // Store the webview references this.isWebviewReady = false this.webview = webviewView.webview - // Set up webview options webviewView.webview.options = { enableScripts: true, localResourceRoots: [this.extensionUri], @@ -446,10 +460,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) @@ -459,9 +473,13 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.initializeConnection() } - /** - * Resolve a WebviewPanel for displaying the Kilo webview in an editor tab. - */ + private setSidebarVisible(visible: boolean): void { + this.setStreamVisibility(visible) + vscode.commands.executeCommand("setContext", "kilo-code.new.sidebarVisible", visible) + this.opts.onSidebarVisibilityChange?.(visible) + } + + /** Resolve a WebviewPanel for displaying Kilo in an editor tab. */ public resolveWebviewPanel(panel: vscode.WebviewPanel): void { // WebviewPanel can be restored/reloaded; ensure we don't treat it as ready prematurely. this.isWebviewReady = false @@ -476,7 +494,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.setupWebviewMessageHandler(panel.webview) this.viewStateDisposable?.dispose() - this.viewStateDisposable = panel.onDidChangeViewState(() => + this.viewStateDisposable = this.visibleTaskStreams.bindPanel(panel, () => this.focusSession(panel.active ? this.currentSession?.id : undefined), ) this.initializeConnection() @@ -484,6 +502,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper /** Register a session created externally and notify the webview. */ public registerSession(session: Session): void { + this.stopCurrentSessionProcesses(session.id) this.setCurrentSession(session) this.contextSessionID = session.id this.trackedSessionIds.add(session.id) @@ -604,9 +623,19 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper if (intercepted === null) return message = intercepted - await routeSuggestionWebviewMessage(this.questionCtx, message) - if (await ModelState.handleMessage(message.type, message, this.client, (msg) => this.postMessage(msg))) return - if (await routeAutocompleteMessage(message, (msg) => this.postMessage(msg))) return + if ( + await routeEarlyMessage(message, { + question: this.questionCtx, + client: this.client, + connection: this.connectionService, + dir: this.getWorkspaceDirectory(this.currentSession?.id), + post: (msg) => this.postMessage(msg), + exportTranscript: (sessionID) => this.handleExportSessionTranscript(sessionID), + }) + ) { + return + } + if (this.handleEditorOpenMessage(message)) return if ( await handleSidebarWorktreeMessage(message, { post: (msg) => this.postMessage(msg), @@ -623,17 +652,19 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper ) { return } + this.visibleTaskStreams.handle(message) switch (message.type) { case "webviewReady": console.log("[Kilo New] KiloProvider: ✅ webviewReady received") this.isWebviewReady = true + this.visibleTaskStreams.clear() await this.syncWebviewState("webviewReady") this.flushPendingReviewComments() this.recoverPendingPrompts() 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, @@ -643,13 +674,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, @@ -660,8 +692,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 } @@ -693,7 +726,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper await this.handleCreateSession() break case "clearSession": - this.contextSessionID = this.currentSession?.id ?? this.contextSessionID + this.stopCurrentSessionProcesses() + this.contextSessionID = undefined this.setCurrentSession(null) this.focusSession() break @@ -771,11 +805,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper break case "saveImage": return saveImage(this.getWorkspaceDirectory(this.currentSession?.id), message) - case "openFile": - if (message.filePath) { - this.handleOpenFile(message.filePath, message.line, message.column) - } - break case "requestProviders": this.fetchAndSendProviders().catch((e) => console.error("[Kilo New] fetchAndSendProviders failed:", e)) break @@ -808,8 +837,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper console.error("[Kilo New] removeSkill failed:", e), ) break - case "removeMode": - this.handleRemoveMode(message.name).catch((e) => console.error("[Kilo New] handleRemoveMode failed:", e)) + case "removeAgent": + this.handleRemoveAgent(message.name).catch((e) => console.error("[Kilo New] handleRemoveAgent failed:", e)) break case "removeMcp": this.handleRemoveMcp(message.name).catch((e) => console.error("[Kilo New] handleRemoveMcp failed:", e)) @@ -1072,13 +1101,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": { @@ -1111,6 +1134,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper }) break } + case "dismissAgentMigrationBanner": { + await this.extensionContext?.globalState.update("kilo.agentMigrationBannerDismissed", true) + break + } } }) this.webviewMessageDisposable = watchFontSizeConfig((msg) => this.postMessage(msg), this.webviewMessageDisposable) @@ -1128,6 +1155,15 @@ 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) + const dismissed = this.extensionContext?.globalState.get("kilo.agentMigrationBannerDismissed") ?? false + this.postMessage({ type: "marketplaceData", ...data, showAgentMigrationBanner: !dismissed }) + } + /** * Initialize connection to the CLI backend server. * Subscribes to the shared KiloConnectionService. @@ -1336,7 +1372,11 @@ 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.stopCurrentSessionProcesses(session.id) this.setCurrentSession(session) this.contextSessionID = session.id this.trackDirectory(session.id, workspaceDir) @@ -1362,7 +1402,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.client.session .get({ sessionID, directory: dir }) .then((r) => { - if (r.data && !signal?.aborted) { + if (r.data && !signal?.aborted && this.contextSessionID === sessionID) { this.setCurrentSession(r.data) this.contextSessionID = r.data.id } @@ -1391,7 +1431,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper options: { mode?: MessageLoadMode; before?: string; limit?: number } = {}, ): Promise { const mode = options.mode ?? "replace" - if (mode !== "prepend") { + if (mode === "replace" || mode === "focus") { + this.stopCurrentSessionProcesses(sessionID) this.trackedSessionIds.add(sessionID) this.focusSession(sessionID) this.contextSessionID = sessionID @@ -1415,6 +1456,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.loadMessagesAbort = abort this.refreshSessionDetails(sessionID, dir, abort.signal) } + const since = mode === "reconcile" ? Date.now() : undefined try { const page = await fetchMessagePage(this.client, { sessionID, @@ -1446,6 +1488,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper mode, cursor: page.cursor, hasMore: Boolean(page.cursor), + since, }) // Recover any prompts missed while the webview was loading or during an SSE reconnection. this.recoverPendingPrompts() @@ -1526,6 +1569,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), } @@ -1594,15 +1638,21 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper } try { - const workspaceDir = this.getWorkspaceDirectory(sessionID) + const workspaceDir = this.getSessionDirectory( + sessionID, + this.currentSession?.id === sessionID ? this.currentSession : undefined, + ) + await stopSessionProcesses(this.client, sessionID, workspaceDir) await this.client.session.delete({ sessionID, directory: workspaceDir }, { throwOnError: true }) this.trackedSessionIds.delete(sessionID) this.streams.drop(sessionID) + this.visibleTaskStreams.delete(sessionID) this.syncedChildSessions.delete(sessionID) this.sessionDirectories.delete(sessionID) this.lastReconciledAt.delete(sessionID) this.connectionService.pruneSession(sessionID) if (this.currentSession?.id === sessionID) { + this.contextSessionID = undefined this.setCurrentSession(null) this.focusSession(undefined) } @@ -1620,26 +1670,41 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper * Handle renaming a session. */ private async handleRenameSession(sessionID: string, title: string): Promise { + try { + const updated = await renameSession({ + client: this.client, + sessionID, + title, + directory: this.getWorkspaceDirectory(sessionID), + }) + if (this.currentSession?.id === sessionID) this.setCurrentSession(updated) + this.postMessage({ type: "sessionUpdated", session: this.sessionToWebview(updated) }) + } catch (error) { + console.error("[Kilo New] KiloProvider: Failed to rename session:", error) + this.postMessage({ type: "error", message: getErrorMessage(error) || "Failed to rename session" }) + } + } + + /** + * 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 workspaceDir = this.getWorkspaceDirectory(sessionID) - const { data: updated } = await this.client.session.update( - { sessionID, directory: workspaceDir, title }, - { throwOnError: true }, - ) - if (this.currentSession?.id === sessionID) { - this.setCurrentSession(updated) - } - this.postMessage({ type: "sessionUpdated", session: this.sessionToWebview(updated) }) + 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 rename session:", error) + console.error("[Kilo New] KiloProvider: Failed to export session transcript:", error) this.postMessage({ type: "error", - message: getErrorMessage(error) || "Failed to rename session", + message: getErrorMessage(error) || "Failed to export session transcript", }) } } @@ -1890,32 +1955,28 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper return true } - /** - * Remove a custom mode via the CLI backend (deletes from disk + refreshes state). - * The webview optimistically removes the mode from its list before this runs. - * On failure, re-fetches agents so the webview reverts to the authoritative state. - */ - private async handleRemoveMode(name: string): Promise { + /** Remove an agent via CLI, falling back to kilo.json removal. */ + private async handleRemoveAgent(name: string): Promise { if (!this.client) return - - // 1. Try CLI removal (handles .md files and legacy .kilocodemodes) try { - const dir = this.getWorkspaceDirectory() - const result = await this.client.kilocode.removeAgent({ name, directory: dir }) + const result = await this.client.kilocode.removeAgent({ name, directory: this.getWorkspaceDirectory() }) if (!result.error) { this.cachedAgentsMessage = null await this.fetchAndSendAgents() return } } catch { - // CLI removal failed — agent may be in kilo.json instead + // fall through to kilo.json removal } - - // 2. Try removing from kilo.json (handles marketplace-installed modes) - const stub = { id: name, type: "mode" as const, name, description: "", content: "" } - const removed = await this.removeMarketplaceItemFromAllScopes(stub) - if (!removed) { - console.error("[Kilo New] KiloProvider: Failed to remove mode:", name) + const stub: AgentMarketplaceItem = { + id: name, + type: "agent", + name, + description: "", + content: { mode: "primary", description: "", prompt: "" }, + } + if (!(await this.removeMarketplaceItemFromAllScopes(stub))) { + console.error("[Kilo New] KiloProvider: Failed to remove agent:", name) } } @@ -2425,7 +2486,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({ @@ -2433,12 +2494,17 @@ 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.stopCurrentSessionProcesses(session.id) this.setCurrentSession(session) this.contextSessionID = session.id this.trackDirectory(session.id, dir) @@ -2542,6 +2608,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper variant?: string, files?: MessageFile[], context?: string, + contextDirectory?: string, ): Promise { if (!this.client) { this.postMessage({ @@ -2558,7 +2625,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) { @@ -2619,6 +2686,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper variant?: string, files?: MessageFile[], context?: string, + contextDirectory?: string, ): Promise { if (!this.client) { this.postMessage({ @@ -2635,7 +2703,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) @@ -2812,6 +2880,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper return self.currentSession }, set currentSession(session) { + self.stopCurrentSessionProcesses(session?.id) self.setCurrentSession(session) if (session) self.contextSessionID = session.id }, @@ -2897,6 +2966,35 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper .then(open, (err) => console.error("[Kilo New] KiloProvider: Failed to preview image:", err)) } + private handleEditorOpenMessage(message: { + type?: string + filePath?: string + line?: number + column?: number + content?: string + language?: string + }): boolean { + if (message.type === "openFile") { + if (message.filePath) this.handleOpenFile(message.filePath, message.line, message.column) + return true + } + if (message.type === "openContent") { + if (message.content) this.handleOpenContent(message.content, message.language) + return true + } + return false + } + + /** + * Handle openContent request - open arbitrary text in an untitled VS Code editor tab. + */ + private handleOpenContent(content: string, language?: string): void { + vscode.workspace.openTextDocument({ content, language: language || "log" }).then( + (doc) => vscode.window.showTextDocument(doc, { preview: true }), + (err) => console.error("[Kilo New] KiloProvider: Failed to open content:", err), + ) + } + /** * Handle openFile request from the webview — open a file in the VS Code editor. * Resolves relative paths against the current session's directory (which may be @@ -2907,17 +3005,26 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper const uri = isAbsolutePath(filePath) ? vscode.Uri.file(filePath) : vscode.Uri.joinPath(vscode.Uri.file(this.getWorkspaceDirectory(this.currentSession?.id)), filePath) - vscode.workspace.openTextDocument(uri).then( - (doc) => { - const options: vscode.TextDocumentShowOptions = { preview: true } - if (line !== undefined && line > 0) { - const col = column !== undefined && column > 0 ? column - 1 : 0 - const pos = new vscode.Position(line - 1, col) - options.selection = new vscode.Range(pos, pos) + vscode.workspace.fs.stat(uri).then( + (stat) => { + if (stat.type & vscode.FileType.Directory) { + vscode.commands.executeCommand("revealInExplorer", uri) + return } - vscode.window.showTextDocument(doc, options) + vscode.workspace.openTextDocument(uri).then( + (doc) => { + const options: vscode.TextDocumentShowOptions = { preview: true } + if (line !== undefined && line > 0) { + const col = column !== undefined && column > 0 ? column - 1 : 0 + const pos = new vscode.Position(line - 1, col) + options.selection = new vscode.Range(pos, pos) + } + vscode.window.showTextDocument(doc, options) + }, + (err) => console.error("[Kilo New] KiloProvider: Failed to open file:", uri.fsPath, err), + ) }, - (err) => console.error("[Kilo New] KiloProvider: Failed to open file:", uri.fsPath, err), + (err) => console.error("[Kilo New] KiloProvider: Path does not exist:", uri.fsPath, err), ) } @@ -2929,7 +3036,11 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper const { section, leaf } = buildSettingPath(key) if (section === "autocomplete" && !validAutocompleteSetting(leaf, value)) return const config = vscode.workspace.getConfiguration(`kilo-code.new${section ? `.${section}` : ""}`) - await config.update(leaf, value, vscode.ConfigurationTarget.Global) + // Normalize a webview-side clear to `undefined` so VS Code removes the + // key from settings.json rather than persisting a literal `null`. This + // lets the runtime fall back to the resolved default. + const next = value === null ? undefined : value + await config.update(leaf, next, vscode.ConfigurationTarget.Global) } /** @@ -2965,6 +3076,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper await this.extensionContext?.globalState.update("variantSelections", undefined) await this.extensionContext?.globalState.update("recentModels", undefined) await this.extensionContext?.globalState.update("kilo.dismissedNotificationIds", undefined) + await this.extensionContext?.globalState.update("kilo.agentMigrationBannerDismissed", undefined) // Re-send all settings to the webview so the UI reflects the reset this.postMessage(buildAutocompleteSettingsMessage()) @@ -3093,7 +3205,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper if (event.type === "server.instance.disposed") { const props = event.properties as Record | null const dir = typeof props?.directory === "string" ? props.directory : undefined - if (dir && path.resolve(dir) !== path.resolve(this.getWorkspaceDirectory())) return + if (dir && !sameDirectory(dir, this.getWorkspaceDirectory())) return void this.reloadAfterAuthChange() return } @@ -3140,8 +3252,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper handleNetworkEvent(event.type as string, event.properties as any, this.client, (s) => this.getWorkspaceDirectory(s)) if (event.type === "indexing.status" && directory) { - const current = path.resolve(this.getWorkspaceDirectory(this.currentSession?.id)) - if (path.resolve(directory) !== current) return + if (!sameDirectory(directory, this.getWorkspaceDirectory(this.currentSession?.id))) return } const msg = mapSSEEventToWebviewMessage(event, sessionID) @@ -3313,6 +3424,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper }) } + private getSessionDirectory(sessionId: string, session?: Session): string { + return this.sessionDirectories.get(sessionId) ?? session?.directory ?? this.getRootDirectory() + } + private getContextDirectory(): string { return resolveContextDirectory({ currentSessionID: this.currentSession?.id, @@ -3471,6 +3586,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.autocompleteConfigDisposable?.dispose() this.telemetryStateDisposable?.dispose() this.autoApproveBridge?.dispose() + this.visibleTaskStreams.clear() this.streams.dispose() this.isWebviewReady = false this.promptRecoveryQueued = false diff --git a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts index 77237b86975..d14dcbaf8ec 100644 --- a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts +++ b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts @@ -33,6 +33,7 @@ import { recordPromotionHandoff } from "./promotion-handoff" import { restoreWorktrees } from "./state-recovery" import { diffSummary as localDiffSummary, diffFile as localDiffFile } from "./local-diff" import { parseToolRequest, startFromTool, type ToolRequest } from "./tool-start" +import { stopSessionProcesses } from "../kilo-provider/background-process" import { buildKeybindingMap } from "./format-keybinding" import { resolveVersionModels, buildInitialMessages, type CreatedVersion } from "./multi-version" @@ -71,6 +72,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 @@ -120,7 +122,6 @@ export class AgentManagerProvider implements Disposable { getState: () => this.getStateManager(), getRoot: () => this.getRoot(), getStateReady: () => this.stateReady, - getClient: () => this.connectionService.getClient(), git: this.gitOps, localDiff: (dir, base) => localDiffSummary(this.gitOps, dir, base, (...args) => this.log(...args)), localDiffFile: (dir, base, file) => localDiffFile(this.gitOps, dir, base, file, (...args) => this.log(...args)), @@ -182,10 +183,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 { @@ -213,6 +219,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) }) @@ -234,6 +241,7 @@ export class AgentManagerProvider implements Disposable { this.prBridge.poller.stop() this.diffs.stop() this.panel = undefined + this.onVisibilityChange?.(false) } ctx.sessions.dispose() }) @@ -379,7 +387,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) } @@ -419,6 +427,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 @@ -529,6 +546,10 @@ export class AgentManagerProvider implements Disposable { this.state?.setSessionsCollapsed(m.collapsed) return null } + if (m.type === "agentManager.setSidebarCollapsed") { + this.state?.setSidebarCollapsed(m.collapsed) + return null + } if (this.handleSection(m)) return null if (m.type === "agentManager.setReviewDiffStyle") { this.state?.setReviewDiffStyle(m.style) @@ -855,6 +876,7 @@ export class AgentManagerProvider implements Disposable { case "agentManager.setTabOrder": case "agentManager.setWorktreeOrder": case "agentManager.setSessionsCollapsed": + case "agentManager.setSidebarCollapsed": case "agentManager.setReviewDiffStyle": case "agentManager.setDefaultBaseBranch": case "agentManager.createSection": @@ -1045,7 +1067,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() @@ -1064,6 +1086,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( @@ -1134,7 +1176,16 @@ export class AgentManagerProvider implements Disposable { const state = this.getStateManager() if (!state) return null + const dirs = this.panel?.sessions.getSessionDirectories() + const dir = state.directoryFor(sessionId) ?? dirs?.get(sessionId) ?? this.getRoot() ?? process.cwd() + try { + await stopSessionProcesses(this.connectionService.getClient(), sessionId, dir) + } catch (err) { + this.log("onCloseSession: client not available:", err) + } + state.removeSession(sessionId) + this.panel?.sessions.clearSessionDirectory(sessionId) this.pushState() this.log(`Closed session ${sessionId}`) return null @@ -1497,6 +1548,7 @@ export class AgentManagerProvider implements Disposable { tabOrder: state.getTabOrder(), worktreeOrder: state.getWorktreeOrder(), sessionsCollapsed: state.getSessionsCollapsed(), + sidebarCollapsed: state.getSidebarCollapsed(), reviewDiffStyle: state.getReviewDiffStyle(), reviewMarkdownRender: getDiffMarkdownRender(), isGitRepo: true, @@ -1643,6 +1695,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/WorktreeStateManager.ts b/packages/kilo-vscode/src/agent-manager/WorktreeStateManager.ts index 74fea89b555..352932fde03 100644 --- a/packages/kilo-vscode/src/agent-manager/WorktreeStateManager.ts +++ b/packages/kilo-vscode/src/agent-manager/WorktreeStateManager.ts @@ -71,6 +71,7 @@ interface StateFile { tabOrder?: Record worktreeOrder?: string[] sessionsCollapsed?: boolean + sidebarCollapsed?: boolean reviewDiffStyle?: "unified" | "split" defaultBaseBranch?: string } @@ -99,6 +100,7 @@ export class WorktreeStateManager { private tabOrder: Record = {} private worktreeOrder: string[] = [] private collapsed = false + private sidebar = false private reviewDiffStyle: "unified" | "split" = "unified" private defaultBase: string | undefined private readonly log: (msg: string) => void @@ -522,6 +524,19 @@ export class WorktreeStateManager { void this.save() } + // --------------------------------------------------------------------------- + // Sidebar collapsed + // --------------------------------------------------------------------------- + + getSidebarCollapsed(): boolean { + return this.sidebar + } + + setSidebarCollapsed(value: boolean): void { + this.sidebar = value + void this.save() + } + // --------------------------------------------------------------------------- // Review diff style // --------------------------------------------------------------------------- @@ -636,6 +651,7 @@ export class WorktreeStateManager { } const repaired = this.setNormalizedWorktreeOrder(this.worktreeOrder) this.collapsed = data.sessionsCollapsed ?? false + this.sidebar = data.sidebarCollapsed ?? false if (data.reviewDiffStyle === "split") { this.reviewDiffStyle = "split" } @@ -741,6 +757,9 @@ export class WorktreeStateManager { if (this.collapsed) { data.sessionsCollapsed = true } + if (this.sidebar) { + data.sidebarCollapsed = true + } if (this.reviewDiffStyle === "split") { data.reviewDiffStyle = "split" } 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 ef52f68ad15..f6f312fe466 100644 --- a/packages/kilo-vscode/src/agent-manager/types.ts +++ b/packages/kilo-vscode/src/agent-manager/types.ts @@ -126,6 +126,7 @@ interface StateMessage { tabOrder?: Record worktreeOrder?: string[] sessionsCollapsed?: boolean + sidebarCollapsed?: boolean reviewDiffStyle?: "unified" | "split" reviewMarkdownRender?: boolean isGitRepo?: boolean @@ -347,6 +348,7 @@ interface OpenLocallyIn { interface AddSessionToWorktreeIn { type: "agentManager.addSessionToWorktree" worktreeId: string + sessionId?: string } interface CloseSessionIn { @@ -456,6 +458,11 @@ interface SetSessionsCollapsedIn { collapsed: boolean } +interface SetSidebarCollapsedIn { + type: "agentManager.setSidebarCollapsed" + collapsed: boolean +} + interface SetReviewDiffStyleIn { type: "agentManager.setReviewDiffStyle" style: "unified" | "split" @@ -600,6 +607,7 @@ interface SendMessageIn { variant?: string files?: Array<{ mime: string; url: string; filename?: string; source?: FileSourceIn }> agentManagerContext?: string + contextDirectory?: string } interface SendCommandIn { @@ -615,6 +623,7 @@ interface SendCommandIn { variant?: string files?: Array<{ mime: string; url: string; filename?: string; source?: FileSourceIn }> agentManagerContext?: string + contextDirectory?: string } interface RequestTerminalContextIn { @@ -736,6 +745,7 @@ export type AgentManagerInMessage = | SetTabOrderIn | SetWorktreeOrderIn | SetSessionsCollapsedIn + | SetSidebarCollapsedIn | SetReviewDiffStyleIn | SetReviewMarkdownRenderIn | SetDefaultBaseBranchIn diff --git a/packages/kilo-vscode/src/agent-manager/vscode-host.ts b/packages/kilo-vscode/src/agent-manager/vscode-host.ts index 222ea03857d..94f28cba3b1 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) @@ -93,6 +99,10 @@ export class VscodeHost implements Host { provider.attachToWebview(panel.webview, { onBeforeMessage: opts.onBeforeMessage, }) + provider.setStreamVisibility(panel.active && panel.visible) + const streams = panel.onDidChangeViewState((event) => + provider.setStreamVisibility(event.webviewPanel.active && event.webviewPanel.visible), + ) if (this.autoApprove) provider.setAutoApproveController(this.autoApprove) const sessions: SessionProvider = { @@ -141,6 +151,7 @@ export class VscodeHost implements Host { return panel.onDidDispose(cb) }, dispose() { + streams.dispose() provider.dispose() panel.dispose() }, diff --git a/packages/kilo-vscode/src/agent-manager/worktree-diff-controller.ts b/packages/kilo-vscode/src/agent-manager/worktree-diff-controller.ts index 94625e0544e..79694070a7e 100644 --- a/packages/kilo-vscode/src/agent-manager/worktree-diff-controller.ts +++ b/packages/kilo-vscode/src/agent-manager/worktree-diff-controller.ts @@ -1,7 +1,6 @@ -import type { KiloClient } from "@kilocode/sdk/v2/client" import { SourceController } from "../diff/SourceController" import { resolveLocalDiffTarget } from "../diff/shared/target" -import { WorktreeDiffClient } from "../diff/shared/client" +import { WorktreeDiffReverter, type StatusResolver } from "../diff/shared/reverter" import type { DiffFile } from "../diff/types" import type { DiffSource, DiffSourceDescriptor, DiffSourceFetch } from "../diff/sources/types" import type { ApplyConflict, GitOps } from "./GitOps" @@ -20,12 +19,9 @@ export interface WorktreeDiffControllerContext { getRoot: () => string | undefined getStateReady: () => Promise | undefined /** - * SDK client — used by `revert()` via `WorktreeDiffClient` for the one-shot - * file-status lookup. Hot polling paths (`request`, `requestFile`, polling) - * deliberately bypass the client and go through `localDiff`/`localDiffFile` - * to keep git spawns out of the Bun `kilo serve` process (see oven-sh/bun#18265). + * In-process diff paths deliberately bypass the SDK client to keep git spawns + * out of the Bun `kilo serve` process (see oven-sh/bun#18265). */ - getClient: () => KiloClient git: GitOps /** In-process diff summary (replaces client.worktree.diffSummary). */ localDiff: (dir: string, base: string) => Promise @@ -272,7 +268,11 @@ export class WorktreeDiffController { if (!target) return { ok: false, message: "Could not resolve diff target" } try { - const diff = new WorktreeDiffClient(this.ctx.getClient(), this.ctx.git, (...args) => this.ctx.log(...args)) + const status: StatusResolver = async (current, item) => { + const diff = await this.ctx.localDiffFile(current.directory, current.baseBranch, item) + return diff?.status + } + const diff = new WorktreeDiffReverter(this.ctx.git, status, (...args) => this.ctx.log(...args)) return await diff.revertFile(target, file) } catch (error) { const msg = error instanceof Error ? error.message : String(error) diff --git a/packages/kilo-vscode/src/diff/SourceController.ts b/packages/kilo-vscode/src/diff/SourceController.ts index 47d1a771471..11a51ed3449 100644 --- a/packages/kilo-vscode/src/diff/SourceController.ts +++ b/packages/kilo-vscode/src/diff/SourceController.ts @@ -181,8 +181,12 @@ export class SourceController { return } const diff = await source.fetchFile(file).catch(() => null) - // Drop the response if the source has been disposed/swapped while we waited. - if (this.epoch !== epoch) return + // Discard stale content after disposal/swap, but still complete the request + // so consumers can clear per-file loading state. + if (this.epoch !== epoch) { + this.send(this.messages.diffFile(source, file, null)) + return + } this.send(this.messages.diffFile(source, file, diff)) } diff --git a/packages/kilo-vscode/src/diff/shared/client.ts b/packages/kilo-vscode/src/diff/shared/reverter.ts similarity index 51% rename from packages/kilo-vscode/src/diff/shared/client.ts rename to packages/kilo-vscode/src/diff/shared/reverter.ts index 18922a77a20..84900a4ccf7 100644 --- a/packages/kilo-vscode/src/diff/shared/client.ts +++ b/packages/kilo-vscode/src/diff/shared/reverter.ts @@ -1,39 +1,34 @@ -import type { KiloClient } from "@kilocode/sdk/v2/client" import type { GitOps } from "../../agent-manager/GitOps" /** - * A worktree diff target: the working directory and the base branch we diff + * A worktree target: the working directory and the base branch we diff * against (usually the tracking branch). */ export type DiffTarget = { directory: string; baseBranch: string } -type Status = "added" | "deleted" | "modified" +export type DiffStatus = "added" | "deleted" | "modified" +export type StatusResolver = (target: DiffTarget, file: string) => Promise /** - * Thin coordinator that wraps (KiloClient, GitOps, DiffTarget) and exposes the - * small set of operations used by the Changes panel's WorktreeDiffSource and - * the agent manager's WorktreeDiffController. + * Thin coordinator that wraps local diff status lookup with GitOps revert + * behavior used by the Changes panel and Agent Manager. */ -export class WorktreeDiffClient { +export class WorktreeDiffReverter { constructor( - private readonly client: KiloClient, private readonly git: GitOps, + private readonly status: StatusResolver, private readonly log: (...args: unknown[]) => void, ) {} /** * Look up the diff status for a single file. Used by revert flows to pick - * the right git strategy (added → delete, modified/deleted → checkout). + * the right git strategy (added means delete, modified/deleted means checkout). * Returns `undefined` on error so callers can still attempt a best-effort - * revert — `GitOps.revertFile` defaults to a modified-file strategy. + * revert, `GitOps.revertFile` defaults to a modified-file strategy. */ - async fileStatus(target: DiffTarget, file: string): Promise { + async fileStatus(target: DiffTarget, file: string): Promise { try { - const { data } = await this.client.worktree.diffFile( - { directory: target.directory, base: target.baseBranch, file }, - { throwOnError: true }, - ) - return data?.status + return await this.status(target, file) } catch (err) { this.log("Failed to look up file status for revert:", err) return undefined @@ -41,7 +36,7 @@ export class WorktreeDiffClient { } /** - * Revert a single file in the worktree. Composes `fileStatus` + `GitOps.revertFile`. + * Revert a single file in the worktree. Composes `fileStatus` and `GitOps.revertFile`. * Returns a normalized result; callers handle UI/messaging. */ async revertFile(target: DiffTarget, file: string): Promise<{ ok: boolean; message: string }> { diff --git a/packages/kilo-vscode/src/diff/sources/catalog.ts b/packages/kilo-vscode/src/diff/sources/catalog.ts index 2c29b3536f0..fa36aaecf2d 100644 --- a/packages/kilo-vscode/src/diff/sources/catalog.ts +++ b/packages/kilo-vscode/src/diff/sources/catalog.ts @@ -91,7 +91,7 @@ export class DiffSourceCatalog implements vscode.Disposable { build(id: string, ctx: PanelContext): DiffSource { if (id === WORKSPACE_SOURCE_ID) { - return createWorktreeDiffSource(this.connection, { baseBranchOverride: ctx.baseBranchOverride }) + return createWorktreeDiffSource({ baseBranchOverride: ctx.baseBranchOverride }) } if (id === STAGED_SOURCE_ID) return createStagedDiffSource() diff --git a/packages/kilo-vscode/src/diff/sources/worktree.ts b/packages/kilo-vscode/src/diff/sources/worktree.ts index fdb2ebe9328..fdeea76a44a 100644 --- a/packages/kilo-vscode/src/diff/sources/worktree.ts +++ b/packages/kilo-vscode/src/diff/sources/worktree.ts @@ -1,9 +1,8 @@ import * as vscode from "vscode" -import type { KiloConnectionService } from "../../services/cli-backend" import { GitOps } from "../../agent-manager/GitOps" import { diffSummary, diffFile } from "../../agent-manager/local-diff" import type { WorktreeDiffEntry } from "../../agent-manager/types" -import { WorktreeDiffClient, type DiffTarget } from "../shared/client" +import { WorktreeDiffReverter, type DiffTarget, type StatusResolver } from "../shared/reverter" import { resolveLocalDiffTarget } from "../shared/target" import { appendOutput, getWorkspaceRoot } from "../../review-utils" import type { DiffFile } from "../types" @@ -32,10 +31,7 @@ export interface WorktreeDiffSourceOptions { * `before`/`after` per file on demand via `fetchFile`. Runs entirely in the * extension host — no `kilo serve` round-trip. */ -export function createWorktreeDiffSource( - connection: KiloConnectionService, - opts: WorktreeDiffSourceOptions = {}, -): DiffSource { +export function createWorktreeDiffSource(opts: WorktreeDiffSourceOptions = {}): DiffSource { const output = vscode.window.createOutputChannel("Kilo Diff: Workspace") const log = (...args: unknown[]) => appendOutput(output, "WorktreeDiffSource", ...args) const git = new GitOps({ log }) @@ -65,6 +61,11 @@ export function createWorktreeDiffSource( return target } + const status: StatusResolver = async (current, file) => { + const entry = await diffFile(git, current.directory, current.baseBranch, file, log) + return entry?.status + } + return { descriptor: WORKSPACE_DESCRIPTOR, @@ -98,8 +99,7 @@ export function createWorktreeDiffSource( if (!current) return { ok: false, message: "Could not resolve diff target" } try { - const client = connection.getClient() - const diff = new WorktreeDiffClient(client, git, log) + const diff = new WorktreeDiffReverter(git, status, log) return await diff.revertFile(current, file) } catch (err) { const message = err instanceof Error ? err.message : String(err) diff --git a/packages/kilo-vscode/src/extension.ts b/packages/kilo-vscode/src/extension.ts index 8c2ae51cdad..e45d49bbe33 100644 --- a/packages/kilo-vscode/src/extension.ts +++ b/packages/kilo-vscode/src/extension.ts @@ -14,7 +14,7 @@ import { registerAutocompleteProvider } from "./services/autocomplete" import { ensureBackendForAutocomplete } from "./services/autocomplete/ensure-backend" import { AutocompleteServiceManager } from "./services/autocomplete/AutocompleteServiceManager" import { BrowserAutomationService } from "./services/browser-automation" -import { TelemetryProxy } from "./services/telemetry" +import { TelemetryEventName, TelemetryProxy } from "./services/telemetry" import { registerCommitMessageService } from "./services/commit-message" import { registerCodeActions, registerTerminalActions, KiloCodeActionProvider } from "./services/code-actions" import { registerToggleAutoApprove } from "./commands/toggle-auto-approve" @@ -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() @@ -278,8 +305,39 @@ export function activate(context: vscode.ExtensionContext) { }), ) + // Sidebar menus use wrapper commands so this event measures real title button presses, + // not programmatic opens, shortcuts, or editor title commands. + const track = (button: string, command: string) => { + TelemetryProxy.capture(TelemetryEventName.TITLE_BUTTON_CLICKED, { + button, + surface: "sidebar_title", + }) + void vscode.commands.executeCommand(command) + } + // Register toolbar button command handlers context.subscriptions.push( + vscode.commands.registerCommand("kilo-code.new.sidebarTitle.plusButtonClicked", () => { + track("new_task", "kilo-code.new.plusButtonClicked") + }), + vscode.commands.registerCommand("kilo-code.new.sidebarTitle.historyButtonClicked", () => { + track("history", "kilo-code.new.historyButtonClicked") + }), + vscode.commands.registerCommand("kilo-code.new.sidebarTitle.agentManagerOpen", () => { + track("agent_manager", "kilo-code.new.agentManagerOpen") + }), + vscode.commands.registerCommand("kilo-code.new.sidebarTitle.kiloClawOpen", () => { + track("kiloclaw", "kilo-code.new.kiloClawOpen") + }), + vscode.commands.registerCommand("kilo-code.new.sidebarTitle.marketplaceButtonClicked", () => { + track("marketplace", "kilo-code.new.marketplaceButtonClicked") + }), + vscode.commands.registerCommand("kilo-code.new.sidebarTitle.profileButtonClicked", () => { + track("profile", "kilo-code.new.profileButtonClicked") + }), + vscode.commands.registerCommand("kilo-code.new.sidebarTitle.settingsButtonClicked", () => { + track("settings", "kilo-code.new.settingsButtonClicked") + }), vscode.commands.registerCommand("kilo-code.new.plusButtonClicked", () => { const tab = activeTabProvider() if (tab) tab.postMessage({ type: "action", action: "plusButtonClicked" }) @@ -429,7 +487,7 @@ export function activate(context: vscode.ExtensionContext) { ) // Register autocomplete provider - registerAutocompleteProvider(context, connectionService) + void registerAutocompleteProvider(context, connectionService) // Register commit message generation registerCommitMessageService(context, connectionService) @@ -452,6 +510,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 +520,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..c8ceef403ba 100644 --- a/packages/kilo-vscode/src/kilo-provider-utils.ts +++ b/packages/kilo-vscode/src/kilo-provider-utils.ts @@ -3,6 +3,7 @@ import { prettifyError } from "zod/v4" import type { CloudSessionMessage, IndexingStatus } from "./services/cli-backend/types" import type { PartBatch, PartUpdate } from "./kilo-provider/session-stream-scheduler" import type { PartRemove } from "./shared/stream-messages" +import * as path from "path" export { SessionStreamScheduler } from "./kilo-provider/session-stream-scheduler" @@ -222,6 +223,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 +247,7 @@ export async function loadSessions(ctx: SessionRefreshContext): Promise() const extra = await Promise.all( [...worktreeDirs].map((dir) => @@ -339,6 +341,7 @@ export function resolveNewSessionDirectory(input: { currentSessionID?: string contextSessionID?: string agentManagerContext?: string + contextDirectory?: string sessionDirectories: Map workspaceDirectory: string }) { @@ -350,6 +353,8 @@ export function resolveNewSessionDirectory(input: { }) } + if (input.contextDirectory) return input.contextDirectory + return resolveContextDirectory({ currentSessionID: input.currentSessionID, contextSessionID: input.contextSessionID, @@ -359,6 +364,17 @@ export function resolveNewSessionDirectory(input: { }) } +export function sameDirectory(a: string, b: string): boolean { + if (!a || !b) return false + + const left = path.resolve(a) + const right = path.resolve(b) + if (path.relative(left, right) === "") return true + + if (process.platform !== "win32") return false + return path.relative(left.toLowerCase(), right.toLowerCase()) === "" +} + export type WebviewMessage = | PartUpdate | PartBatch @@ -372,6 +388,7 @@ export type WebviewMessage = message: Record } | { type: "sessionStatus"; sessionID: string; status: string; attempt?: number; message?: string; next?: number } + | { type: "sessionTurnClosed"; sessionID: string; reason: "completed" | "error" | "interrupted" } | { type: "permissionRequest" permission: { @@ -493,6 +510,12 @@ export function mapSSEEventToWebviewMessage(event: Event, sessionID: string | un ...extra, } } + case "session.turn.close": + return { + type: "sessionTurnClosed", + sessionID: event.properties.sessionID, + reason: event.properties.reason, + } case "permission.asked": return { type: "permissionRequest", diff --git a/packages/kilo-vscode/src/kilo-provider/background-process.ts b/packages/kilo-vscode/src/kilo-provider/background-process.ts new file mode 100644 index 00000000000..1e59a3a8baf --- /dev/null +++ b/packages/kilo-vscode/src/kilo-provider/background-process.ts @@ -0,0 +1,12 @@ +import type { KiloClient } from "@kilocode/sdk/v2/client" + +export async function stopSessionProcesses( + client: KiloClient | null, + sessionID: string, + directory: string, +): Promise { + if (!client) return + await client.backgroundProcess + .stopSession({ sessionID, directory }) + .catch((err: unknown) => console.warn("[Kilo New] KiloProvider: Failed to stop background processes:", err)) +} diff --git a/packages/kilo-vscode/src/kilo-provider/early-message.ts b/packages/kilo-vscode/src/kilo-provider/early-message.ts new file mode 100644 index 00000000000..4ff6c787b3d --- /dev/null +++ b/packages/kilo-vscode/src/kilo-provider/early-message.ts @@ -0,0 +1,26 @@ +import { routeSuggestionWebviewMessage } from "./handlers/suggestion" +import * as ModelState from "./model-state" +import { routeInputToolMessage } from "../services/input-tools" +import type { KiloConnectionService } from "../services/cli-backend/connection-service" +import type { SuggestionContext } from "./handlers/suggestion" +import type { KiloClient } from "@kilocode/sdk/v2/client" + +type Ctx = { + question: SuggestionContext + client: KiloClient | null + 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/kilo-provider/rename-session.ts b/packages/kilo-vscode/src/kilo-provider/rename-session.ts new file mode 100644 index 00000000000..62b06a04637 --- /dev/null +++ b/packages/kilo-vscode/src/kilo-provider/rename-session.ts @@ -0,0 +1,18 @@ +import type { KiloClient, Session } from "@kilocode/sdk/v2/client" +import { parseSessionTitle } from "../shared/session-title" + +export async function renameSession(input: { + client: KiloClient | null + sessionID: string + title: unknown + directory: string +}): Promise { + if (!input.client) throw new Error("Not connected to CLI backend") + const result = parseSessionTitle(input.title) + if ("error" in result) throw new Error("Invalid session title") + const { data } = await input.client.session.update( + { sessionID: input.sessionID, directory: input.directory, title: result.value }, + { throwOnError: true }, + ) + return data +} diff --git a/packages/kilo-vscode/src/kilo-provider/session-stream-scheduler.ts b/packages/kilo-vscode/src/kilo-provider/session-stream-scheduler.ts index 1f3c91e8bae..3209d3c6e9d 100644 --- a/packages/kilo-vscode/src/kilo-provider/session-stream-scheduler.ts +++ b/packages/kilo-vscode/src/kilo-provider/session-stream-scheduler.ts @@ -17,6 +17,7 @@ export type StreamSchedulerStats = { emitted: number batches: number active: number + visible: number background: number } @@ -32,6 +33,8 @@ export type StreamSchedulerOptions = { backgroundStepMs?: number /** Hard cap for the background cadence. Defaults to 400ms. */ backgroundMaxMs?: number + /** Flush cadence for visible inline child sessions. Defaults to 50ms. */ + visibleMs?: number } // Scheduler tuning — rationale: @@ -71,6 +74,7 @@ export type StreamSchedulerOptions = { // ~500ms the UI starts feeling disconnected; below 300ms the many-agent // backoff stops providing meaningful throttling. const DEFAULT_ACTIVE_MS = 16 +const DEFAULT_VISIBLE_MS = 50 const DEFAULT_BG_BASE_MS = 150 const DEFAULT_BG_STEP_MS = 20 const DEFAULT_BG_MAX_MS = 400 @@ -110,10 +114,14 @@ function mergePartUpdate(prev: PartUpdate | undefined, msg: PartUpdate): PartUpd export class SessionStreamScheduler { private active: string | undefined private atimer: ReturnType | null = null + private vtimer: ReturnType | null = null private btimer: ReturnType | null = null private bgFirstQueuedAt = 0 + private visibleFirstQueuedAt = 0 private readonly queues = new Map>() + private readonly visible = new Set() private readonly activeMs: number + private readonly visibleMs: number private readonly bgBase: number private readonly bgStep: number private readonly bgMax: number @@ -122,6 +130,7 @@ export class SessionStreamScheduler { emitted: 0, batches: 0, active: 0, + visible: 0, background: 0, } @@ -130,6 +139,7 @@ export class SessionStreamScheduler { opts?: StreamSchedulerOptions, ) { this.activeMs = opts?.activeMs ?? DEFAULT_ACTIVE_MS + this.visibleMs = opts?.visibleMs ?? DEFAULT_VISIBLE_MS this.bgBase = opts?.backgroundBaseMs ?? DEFAULT_BG_BASE_MS this.bgStep = opts?.backgroundStepMs ?? DEFAULT_BG_STEP_MS this.bgMax = opts?.backgroundMaxMs ?? DEFAULT_BG_MAX_MS @@ -143,10 +153,26 @@ export class SessionStreamScheduler { this.atimer = null } this.active = sessionID - if (prev && this.queues.get(prev)?.size) this.scheduleBackground() + if (prev && this.queues.get(prev)?.size) this.schedule(prev) if (sessionID) this.flush(sessionID) } + setVisible(sessionID: string, visible: boolean): void { + const changed = visible ? !this.visible.has(sessionID) : this.visible.has(sessionID) + if (!changed) return + if (visible) this.visible.add(sessionID) + else this.visible.delete(sessionID) + if (this.queues.get(sessionID)?.size) this.schedule(sessionID) + if (this.vtimer && !this.hasVisible()) { + clearTimeout(this.vtimer) + this.vtimer = null + } + if (this.btimer && !this.hasBackground()) { + clearTimeout(this.btimer) + this.btimer = null + } + } + push(msg: PartUpdate): void { this.counters.received++ const key = partUpdateKey(msg) @@ -183,6 +209,11 @@ export class SessionStreamScheduler { this.emit(this.take(sessionID)) + if (this.vtimer && !this.hasVisible()) { + clearTimeout(this.vtimer) + this.vtimer = null + } + if (this.btimer && !this.hasBackground()) { clearTimeout(this.btimer) this.btimer = null @@ -200,6 +231,10 @@ export class SessionStreamScheduler { */ drop(sessionID: string): void { this.queues.delete(sessionID) + if (this.vtimer && !this.hasVisible()) { + clearTimeout(this.vtimer) + this.vtimer = null + } if (this.btimer && !this.hasBackground()) { clearTimeout(this.btimer) this.btimer = null @@ -209,6 +244,7 @@ export class SessionStreamScheduler { dispose(): void { this.clearTimers() this.queues.clear() + this.visible.clear() } stats(): Readonly { @@ -230,9 +266,23 @@ export class SessionStreamScheduler { this.atimer = setTimeout(() => this.flushActive(), this.activeMs) return } + if (this.visible.has(sessionID)) { + this.scheduleVisible() + return + } this.scheduleBackground() } + private scheduleVisible(): void { + if (!this.hasVisible()) return + const now = Date.now() + if (!this.vtimer) this.visibleFirstQueuedAt = now + const elapsed = now - this.visibleFirstQueuedAt + const remaining = Math.max(0, this.visibleMs - elapsed) + if (this.vtimer) clearTimeout(this.vtimer) + this.vtimer = setTimeout(() => this.flushVisible(), remaining) + } + private scheduleBackground(): void { const count = this.backgroundCount() if (count === 0) return @@ -259,6 +309,12 @@ export class SessionStreamScheduler { this.emit(this.takeBackground()) } + private flushVisible(): void { + this.vtimer = null + this.visibleFirstQueuedAt = 0 + this.emit(this.takeVisible()) + } + private take(sessionID: string): PartUpdate[] { const queue = this.queues.get(sessionID) if (!queue) return [] @@ -276,20 +332,43 @@ export class SessionStreamScheduler { const updates: PartUpdate[] = [] for (const [sid, queue] of this.queues) { if (sid === this.active) continue + if (this.visible.has(sid)) continue updates.push(...queue.values()) this.queues.delete(sid) } return updates } + private takeVisible(): PartUpdate[] { + const updates: PartUpdate[] = [] + for (const [sid, queue] of this.queues) { + if (sid === this.active || !this.visible.has(sid)) continue + updates.push(...queue.values()) + this.queues.delete(sid) + } + return updates + } + + private visibleCount(): number { + let n = 0 + for (const [sid, queue] of this.queues) { + if (sid !== this.active && this.visible.has(sid) && queue.size > 0) n++ + } + return n + } + private backgroundCount(): number { let n = 0 for (const [sid, queue] of this.queues) { - if (sid !== this.active && queue.size > 0) n++ + if (sid !== this.active && !this.visible.has(sid) && queue.size > 0) n++ } return n } + private hasVisible(): boolean { + return this.visibleCount() > 0 + } + private hasBackground(): boolean { return this.backgroundCount() > 0 } @@ -315,12 +394,15 @@ export class SessionStreamScheduler { private countLane(sessionID: string): void { if (sessionID === this.active) this.counters.active++ + else if (this.visible.has(sessionID)) this.counters.visible++ else this.counters.background++ } private clearTimers(): void { if (this.atimer) clearTimeout(this.atimer) this.atimer = null + if (this.vtimer) clearTimeout(this.vtimer) + this.vtimer = null if (this.btimer) clearTimeout(this.btimer) this.btimer = null } diff --git a/packages/kilo-vscode/src/kilo-provider/visible-task-streams.ts b/packages/kilo-vscode/src/kilo-provider/visible-task-streams.ts new file mode 100644 index 00000000000..83b97c3103e --- /dev/null +++ b/packages/kilo-vscode/src/kilo-provider/visible-task-streams.ts @@ -0,0 +1,53 @@ +import type * as vscode from "vscode" + +type Message = { type?: unknown; sessionID?: unknown; visible?: unknown } + +function parse(message: unknown): Message | undefined { + if (!message || typeof message !== "object") return undefined + const msg = message as Message + if (msg.type !== "streamSessionVisible") return undefined + return msg +} + +export class VisibleTaskStreams { + private readonly refs = new Map() + private active = true + + constructor(private readonly set: (id: string, visible: boolean) => void) {} + + clear(): void { + for (const id of this.refs.keys()) this.set(id, false) + this.refs.clear() + } + + delete(id: string): void { + this.set(id, false) + this.refs.delete(id) + } + + setActive(active: boolean): void { + if (this.active === active) return + this.active = active + for (const id of this.refs.keys()) this.set(id, active) + } + + bindPanel(panel: vscode.WebviewPanel, focus: () => void): vscode.Disposable { + this.setActive(panel.active) + return panel.onDidChangeViewState(() => { + this.setActive(panel.active) + focus() + }) + } + + handle(message: unknown): boolean { + const msg = parse(message) + if (!msg) return false + if (typeof msg.sessionID !== "string" || typeof msg.visible !== "boolean") return true + const count = this.refs.get(msg.sessionID) ?? 0 + const next = msg.visible ? count + 1 : Math.max(0, count - 1) + if (next === 0) this.refs.delete(msg.sessionID) + else this.refs.set(msg.sessionID, next) + this.set(msg.sessionID, this.active && next > 0) + return true + } +} diff --git a/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts b/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts index a272f677372..5c24286e782 100644 --- a/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts +++ b/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts @@ -6,6 +6,10 @@ import { AutocompleteStatusBar } from "./AutocompleteStatusBar" import { AutocompleteCodeActionProvider } from "./AutocompleteCodeActionProvider" import { AutocompleteInlineCompletionProvider } from "./classic-auto-complete/AutocompleteInlineCompletionProvider" import { AutocompleteTelemetry } from "./classic-auto-complete/AutocompleteTelemetry" +import { NextEditInlineCompletionProvider } from "./next-edit/NextEditInlineCompletionProvider" +import { disposeLog } from "./next-edit/log" +import { NextEditSuggestionManager } from "./next-edit/NextEditSuggestionManager" +import { toAllowedMercuryRecentSnippets } from "./next-edit/recentSnippetsAdapter" import type { KiloConnectionService } from "../cli-backend" import { hasValidCredentials } from "./fim" import { DEFAULT_AUTOCOMPLETE_MODEL, getAutocompleteModel } from "../../shared/autocomplete-models" @@ -23,11 +27,13 @@ export interface AutocompleteServiceSettings { function readSettings(): AutocompleteServiceSettings { const config = vscode.workspace.getConfiguration(CONFIG_SECTION) + const info = getAutocompleteModel(config.get("provider"), config.get("model")) return { enableAutoTrigger: config.get("enableAutoTrigger") ?? true, enableSmartInlineTaskKeybinding: config.get("enableSmartInlineTaskKeybinding") ?? true, enableChatAutocomplete: config.get("enableChatAutocomplete") ?? true, - model: getAutocompleteModel(config.get("model") ?? "").id, + provider: info.providerID, + model: info.modelID, snoozeUntil: config.get("snoozeUntil"), } } @@ -59,9 +65,15 @@ export class AutocompleteServiceManager { // VSCode Providers public readonly codeActionProvider: AutocompleteCodeActionProvider public readonly inlineCompletionProvider: AutocompleteInlineCompletionProvider + public readonly nextEditProvider: NextEditInlineCompletionProvider + public readonly nextEditSuggestionManager: NextEditSuggestionManager private inlineCompletionProviderDisposable: vscode.Disposable | null = null + private inlineCompletionProviderKind: "classic" | "next-edit" | null = null private unsubscribeState: (() => void) | null = null private unsubscribeEvent: (() => void) | null = null + // Resolved copy of the classic provider's ignore controller for synchronous + // snippet filtering. Null until the async initialize() resolves. + private ignoreControllerSync: { validateAccess(fsPath: string): boolean } | null = null constructor(context: vscode.ExtensionContext, connectionService: KiloConnectionService) { if (AutocompleteServiceManager._instance) { @@ -88,6 +100,52 @@ export class AutocompleteServiceManager { new AutocompleteTelemetry(), (status) => this.handleFatalAutocompleteError(status), ) + // Cache the resolved ignore controller for synchronous snippet filtering. + void this.inlineCompletionProvider.ignoreController.then((ic) => { + this.ignoreControllerSync = ic + }) + + this.nextEditSuggestionManager = new NextEditSuggestionManager() + this.nextEditProvider = new NextEditInlineCompletionProvider({ + connectionService, + suggestionManager: this.nextEditSuggestionManager, + getModelSelection: () => { + const info = getAutocompleteModel(this.settings?.provider, this.settings?.model) + return { providerId: info.providerID, modelId: info.modelID } + }, + isFileAllowed: async (fsPath) => { + const ignore = await this.inlineCompletionProvider.ignoreController + return ignore.validateAccess(fsPath) + }, + getRecentlyViewedSnippets: () => { + // Reuse the LRU populated by the classic provider — keeps a single + // RecentlyVisitedRangesService instance instead of double-tracking. + // Suppress snippets until access checks are available, then include + // only content explicitly approved by the ignore controller. + const raw = this.inlineCompletionProvider.recentlyVisitedRangesService.getSnippets() + const ignore = this.ignoreControllerSync + if (!ignore) return [] + return toAllowedMercuryRecentSnippets(raw, (path) => ignore.validateAccess(path)) + }, + onFatalError: (status) => this.handleFatalAutocompleteError(status), + onSuggestion: (event) => { + const eventName = + event.status === "error" + ? TelemetryEventName.AUTOCOMPLETE_LLM_REQUEST_FAILED + : event.shown + ? TelemetryEventName.AUTOCOMPLETE_LLM_SUGGESTION_RETURNED + : TelemetryEventName.AUTOCOMPLETE_LLM_REQUEST_COMPLETED + TelemetryProxy.capture(eventName, { + mode: "next-edit", + model: getAutocompleteModel(this.settings?.provider, this.settings?.model).id, + latencyMs: event.latencyMs, + inputTokens: event.inputTokens, + outputTokens: event.outputTokens, + shown: event.shown, + errorStatus: event.errorStatus, + }) + }, + }) // Reload when CLI backend connection state changes so autocomplete // picks up the connected state even if it wasn't ready at startup. @@ -119,9 +177,7 @@ export class AutocompleteServiceManager { public async load() { this.settings = readSettings() - if (this.settings.model) { - this.inlineCompletionProvider.setModel(this.settings.model) - } + this.inlineCompletionProvider.setModel(getAutocompleteModel(this.settings.provider, this.settings.model).id) await this.updateGlobalContext() this.updateStatusBar() @@ -136,25 +192,47 @@ export class AutocompleteServiceManager { */ private async ensureInlineCompletionProviderRegistration() { const shouldBeRegistered = (this.settings?.enableAutoTrigger ?? false) && !this.isSnoozed() - const isRegistered = this.inlineCompletionProviderDisposable !== null + const info = getAutocompleteModel(this.settings?.provider, this.settings?.model) + const desiredKind: "classic" | "next-edit" = info.kind === "edit" ? "next-edit" : "classic" - // Already in the correct state — nothing to do - if (shouldBeRegistered === isRegistered) { - return + // Mode change while still enabled requires a swap: tear down the old + // registration so the new provider takes over. + if ( + shouldBeRegistered && + this.inlineCompletionProviderKind !== null && + this.inlineCompletionProviderKind !== desiredKind + ) { + if (this.inlineCompletionProviderKind === "next-edit") this.nextEditSuggestionManager.clear() + this.inlineCompletionProviderDisposable?.dispose() + this.inlineCompletionProviderDisposable = null + this.inlineCompletionProviderKind = null } + if (!shouldBeRegistered && this.inlineCompletionProviderKind === "next-edit") { + this.nextEditSuggestionManager.clear() + } + const isRegistered = this.inlineCompletionProviderDisposable !== null + if (shouldBeRegistered === isRegistered) return + if (!shouldBeRegistered) { this.inlineCompletionProviderDisposable!.dispose() this.inlineCompletionProviderDisposable = null + this.inlineCompletionProviderKind = null return } - // Register classic provider (tracked via this.inlineCompletionProviderDisposable, - // not context.subscriptions, so re-registration on reconnect doesn't leak) + const provider: vscode.InlineCompletionItemProvider = + desiredKind === "next-edit" ? this.nextEditProvider : this.inlineCompletionProvider this.inlineCompletionProviderDisposable = vscode.languages.registerInlineCompletionItemProvider( { scheme: "file" }, - this.inlineCompletionProvider, + provider, ) + this.inlineCompletionProviderKind = desiredKind + } + + /** Which provider is currently registered (`null` if none). */ + public get currentMode(): "classic" | "next-edit" | null { + return this.inlineCompletionProviderKind } public async disable() { @@ -319,11 +397,13 @@ export class AutocompleteServiceManager { } private getCurrentModelName(): string { - return this.inlineCompletionProvider.getModelId() + const info = getAutocompleteModel(this.settings?.provider, this.settings?.model) + return info.label } private getCurrentProviderName(): string { - return getAutocompleteModel(this.inlineCompletionProvider.getModelId()).provider + const info = getAutocompleteModel(this.settings?.provider, this.settings?.model) + return info.provider } private hasNoUsableProvider(): boolean { @@ -408,10 +488,17 @@ export class AutocompleteServiceManager { if (this.inlineCompletionProviderDisposable) { this.inlineCompletionProviderDisposable.dispose() this.inlineCompletionProviderDisposable = null + this.inlineCompletionProviderKind = null } // Dispose inline completion provider resources this.inlineCompletionProvider.dispose() + this.nextEditProvider.dispose() + this.nextEditSuggestionManager.dispose() + + // Drop the dedicated Next Edit OutputChannel so it doesn't leak across + // extension reloads. + disposeLog() // Clear singleton instance AutocompleteServiceManager._instance = null diff --git a/packages/kilo-vscode/src/services/autocomplete/__tests__/settings.spec.ts b/packages/kilo-vscode/src/services/autocomplete/__tests__/settings.spec.ts index 4cc2eb0a535..43bf1438ae4 100644 --- a/packages/kilo-vscode/src/services/autocomplete/__tests__/settings.spec.ts +++ b/packages/kilo-vscode/src/services/autocomplete/__tests__/settings.spec.ts @@ -24,36 +24,63 @@ describe("autocomplete settings", () => { update.mockClear() }) - it("includes the configured model in loaded settings", async () => { - state.set("model", "inception/mercury-edit") + it("includes the configured direct provider model in loaded settings", async () => { + state.set("provider", "inception") + state.set("model", "mercury-edit-2") const { buildAutocompleteSettingsMessage } = await import("../settings") - expect(buildAutocompleteSettingsMessage().settings.model).toBe("inception/mercury-edit") + expect(buildAutocompleteSettingsMessage().settings.provider).toBe("inception") + expect(buildAutocompleteSettingsMessage().settings.model).toBe("mercury-edit-2") }) - it("defaults to codestral when no model is set", async () => { + it("passes a bare model setting through unchanged so the webview can render it as-is", async () => { + // The webview now distinguishes "no explicit setting" (null) from "user + // picked something." We don't try to interpret a bare `model` here — + // resolving it to a default happens at the runtime layer, not in the + // settings message. + state.set("model", "mercury-edit-2") const { buildAutocompleteSettingsMessage } = await import("../settings") - expect(buildAutocompleteSettingsMessage().settings.model).toBe("mistralai/codestral-2508") + expect(buildAutocompleteSettingsMessage().settings.provider).toBeNull() + expect(buildAutocompleteSettingsMessage().settings.model).toBe("mercury-edit-2") }) - it("defaults to codestral when stored model is no longer supported", async () => { + it("returns null for both keys when nothing is set (let the webview render 'Not set')", async () => { + const { buildAutocompleteSettingsMessage } = await import("../settings") + + expect(buildAutocompleteSettingsMessage().settings.provider).toBeNull() + expect(buildAutocompleteSettingsMessage().settings.model).toBeNull() + }) + + it("preserves an unsupported stored model verbatim — runtime fallback handles resolution", async () => { state.set("model", "some/removed-model") const { buildAutocompleteSettingsMessage } = await import("../settings") - expect(buildAutocompleteSettingsMessage().settings.model).toBe("mistralai/codestral-2508") + expect(buildAutocompleteSettingsMessage().settings.provider).toBeNull() + expect(buildAutocompleteSettingsMessage().settings.model).toBe("some/removed-model") }) it("validates supported model updates", async () => { const { validAutocompleteSetting } = await import("../settings") - expect(validAutocompleteSetting("model", "inception/mercury-edit")).toBe(true) + expect(validAutocompleteSetting("model", "mercury-edit-2")).toBe(true) + expect(validAutocompleteSetting("provider", "inception")).toBe(true) }) - it("rejects unsupported model updates", async () => { + it("accepts null/undefined for provider and model so users can clear the setting", async () => { + const { validAutocompleteSetting } = await import("../settings") + + expect(validAutocompleteSetting("provider", null)).toBe(true) + expect(validAutocompleteSetting("provider", undefined)).toBe(true) + expect(validAutocompleteSetting("model", null)).toBe(true) + expect(validAutocompleteSetting("model", undefined)).toBe(true) + }) + + it("rejects unsupported autocomplete updates", async () => { const { validAutocompleteSetting } = await import("../settings") expect(validAutocompleteSetting("model", "other/model")).toBe(false) + expect(validAutocompleteSetting("provider", "openrouter")).toBe(false) }) it("rejects non-boolean toggle updates", async () => { diff --git a/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/ChatTextAreaAutocomplete.ts b/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/ChatTextAreaAutocomplete.ts index 3971e785a11..9d2c0d00823 100644 --- a/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/ChatTextAreaAutocomplete.ts +++ b/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/ChatTextAreaAutocomplete.ts @@ -77,7 +77,7 @@ export class ChatTextAreaAutocomplete { async getCompletion(userText: string, visibleCodeContext?: VisibleCodeContext): Promise<{ suggestion: string }> { const cfg = vscode.workspace.getConfiguration("kilo-code.new.autocomplete") - const entry = getAutocompleteModel(cfg.get("model") ?? "") + const entry = getAutocompleteModel(cfg.get("provider"), cfg.get("model")) const startTime = Date.now() // Build context for telemetry diff --git a/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/AutocompleteInlineCompletionProvider.ts b/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/AutocompleteInlineCompletionProvider.ts index 78c16079f52..6e6eef24c77 100644 --- a/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/AutocompleteInlineCompletionProvider.ts +++ b/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/AutocompleteInlineCompletionProvider.ts @@ -22,7 +22,7 @@ import { import { FimPromptBuilder } from "./FillInTheMiddle" import { hasValidCredentials } from "../fim" import type { KiloConnectionService } from "../../cli-backend" -import { getAutocompleteModel } from "../../../shared/autocomplete-models" +import { getAutocompleteModelById } from "../../../shared/autocomplete-models" import { ContextRetrievalService } from "../continuedev/core/autocomplete/context/ContextRetrievalService" import { VsCodeIde } from "../continuedev/core/vscode-test-harness/src/VSCodeIde" import { RecentlyVisitedRangesService } from "../continuedev/core/vscode-test-harness/src/autocomplete/RecentlyVisitedRangesService" @@ -111,13 +111,13 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple private connectionService: KiloConnectionService private costTrackingCallback: CostTrackingCallback private getSettings: () => AutocompleteServiceSettings | null - private recentlyVisitedRangesService: RecentlyVisitedRangesService + public readonly recentlyVisitedRangesService: RecentlyVisitedRangesService private recentlyEditedTracker: RecentlyEditedTracker private debounceTimer: NodeJS.Timeout | null = null /** The pending request associated with the current debounce timer (if any) */ private debouncedPendingRequest: PendingRequest | null = null private isFirstCall: boolean = true - private ignoreController: Promise + public readonly ignoreController: Promise /** Abort controller for the current in-flight FIM request */ private fimAbortController: AbortController | null = null private acceptedCommand: vscode.Disposable | null = null @@ -345,7 +345,7 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple const telemetryContext: AutocompleteContext = { languageId: document.languageId, modelId: this.contextProvider.modelId, - provider: getAutocompleteModel(this.contextProvider.modelId).provider, + provider: getAutocompleteModelById(this.contextProvider.modelId).provider, } this.telemetry?.captureSuggestionRequested(telemetryContext) @@ -586,7 +586,7 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple const telemetryContext: AutocompleteContext = { languageId, modelId: this.contextProvider.modelId, - provider: getAutocompleteModel(this.contextProvider.modelId).provider, + provider: getAutocompleteModelById(this.contextProvider.modelId).provider, } // Defense-in-depth: credentials may become invalid between the provider gate and the actual diff --git a/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/FillInTheMiddle.ts b/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/FillInTheMiddle.ts index 6a36ae3ab32..564423b2f18 100644 --- a/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/FillInTheMiddle.ts +++ b/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/FillInTheMiddle.ts @@ -65,36 +65,14 @@ export class FimPromptBuilder { signal?: AbortSignal, ): Promise { const { formattedPrefix, prunedSuffix, autocompleteInput } = prompt - let perflog = "" - const logtime = (() => { - let timestamp = performance.now() - return (msg: string) => { - const baseline = timestamp - timestamp = performance.now() - perflog += `${msg}: ${timestamp - baseline}\n` - } - })() - - logtime("snippets") - - console.log("[FIM] formattedPrefix:", formattedPrefix) - let response = "" const onChunk = (text: string) => { response += text } - logtime("prep fim") const usageInfo = await generateFim(connection, modelId, formattedPrefix, prunedSuffix, onChunk, signal) - logtime("fim network") - console.log("[FIM] response:", response) const fillInAtCursorSuggestion = processSuggestion(response) - if (fillInAtCursorSuggestion.text) { - console.info("Final FIM suggestion:", fillInAtCursorSuggestion) - } - logtime("processSuggestion") - console.log(perflog + `lengths: ${formattedPrefix.length + prunedSuffix.length}\n`) return { suggestion: fillInAtCursorSuggestion, cost: usageInfo.cost, diff --git a/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/__tests__/uselessSuggestionFilter.test.ts b/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/__tests__/uselessSuggestionFilter.test.ts index b2cd98dbfce..0e0902e2371 100644 --- a/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/__tests__/uselessSuggestionFilter.test.ts +++ b/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/__tests__/uselessSuggestionFilter.test.ts @@ -47,7 +47,7 @@ describe("postprocessAutocompleteSuggestion", () => { suggestion: "const x = 42", prefix: "const x = ", suffix: "", - model: "inception/mercury-edit", + model: "inception/mercury-edit-2", }) expect(result).toBe("42") }) diff --git a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/templating/AutocompleteTemplate.ts b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/templating/AutocompleteTemplate.ts index 93c4de15e63..229fdbf5e35 100644 --- a/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/templating/AutocompleteTemplate.ts +++ b/packages/kilo-vscode/src/services/autocomplete/continuedev/core/autocomplete/templating/AutocompleteTemplate.ts @@ -1,6 +1,6 @@ // Fill in the middle prompts // -// We only expose Codestral and Mercury Edit as autocomplete models — every +// We only expose Codestral and Mercury Edit 2 as autocomplete models — every // other FIM template in the upstream continuedev list is unreachable. import { CompletionOptions } from "../../index.js" diff --git a/packages/kilo-vscode/src/services/autocomplete/fim.ts b/packages/kilo-vscode/src/services/autocomplete/fim.ts index 8e5fcb429c6..d45ecd65b9d 100644 --- a/packages/kilo-vscode/src/services/autocomplete/fim.ts +++ b/packages/kilo-vscode/src/services/autocomplete/fim.ts @@ -1,6 +1,8 @@ import { ResponseMetaData } from "./types" import type { KiloConnectionService } from "../cli-backend" -import { getAutocompleteModel } from "../../shared/autocomplete-models" +import { getAutocompleteModelById } from "../../shared/autocomplete-models" + +const FIM_MAX_TOKENS = 256 /** * Generate a FIM (Fill-in-the-Middle) completion via the CLI backend. @@ -17,7 +19,7 @@ export async function generateFim( signal?: AbortSignal, ): Promise { const client = await connectionService.getClientAsync() - + const info = getAutocompleteModelById(modelId) let cost = 0 let inputTokens = 0 let outputTokens = 0 @@ -27,15 +29,16 @@ export async function generateFim( // ends the stream. Without this, errors never reach ErrorBackoff. let sseError: Error | undefined - const temp = getAutocompleteModel(modelId).temperature + console.info(`[FIM] request provider=${info.providerID} model=${info.requestModel} url=/kilo/fim`) const { stream } = await client.kilo.fim( { prefix, suffix, - model: modelId, - maxTokens: 256, - temperature: temp, + provider: info.providerID, + model: info.modelID, + maxTokens: FIM_MAX_TOKENS, + temperature: info.temperature, }, { signal, diff --git a/packages/kilo-vscode/src/services/autocomplete/index.ts b/packages/kilo-vscode/src/services/autocomplete/index.ts index 9b8a22eba1f..7b106dc03c1 100644 --- a/packages/kilo-vscode/src/services/autocomplete/index.ts +++ b/packages/kilo-vscode/src/services/autocomplete/index.ts @@ -1,12 +1,22 @@ import * as vscode from "vscode" import { AutocompleteServiceManager } from "./AutocompleteServiceManager" import { ensureBackendForAutocomplete } from "./ensure-backend" +import { migrateDefaultAutocompleteSettings } from "./migrate-default" +import { nesLog } from "./next-edit/log" +import { INLINE_COMPLETION_ACCEPTED_COMMAND as NEXT_EDIT_ACCEPTED_COMMAND } from "./next-edit/NextEditInlineCompletionProvider" +import { chainNextPrediction } from "./next-edit/NextEditSuggestionManager" import type { KiloConnectionService } from "../cli-backend" -export const registerAutocompleteProvider = ( +export const registerAutocompleteProvider = async ( context: vscode.ExtensionContext, connectionService: KiloConnectionService, ) => { + // Run before constructing the manager so its initial readSettings() sees + // the cleared state and behaves as "Not set." Awaited because the manager's + // constructor synchronously kicks off readSettings() via load(), which would + // otherwise race with the migration. + await migrateDefaultAutocompleteSettings(context) + const autocompleteManager = new AutocompleteServiceManager(context, connectionService) context.subscriptions.push(autocompleteManager) @@ -42,6 +52,27 @@ export const registerAutocompleteProvider = ( await autocompleteManager.disable() }), ) + // Fired by VSCode when the user accepts a Next Edit same-line ghost. Chains + // the next prediction so users can walk a refactor with repeated Tabs. + context.subscriptions.push( + vscode.commands.registerCommand(NEXT_EDIT_ACCEPTED_COMMAND, () => { + nesLog("suggestion accepted") + if (autocompleteManager.currentMode === "next-edit") chainNextPrediction() + }), + ) + // Tab handler for off-cursor pending suggestions: first press teleports the + // cursor to the predicted edit, second press applies. + context.subscriptions.push( + vscode.commands.registerCommand("kilo-code.new.autocomplete.nextEdit.acceptOrJump", async () => { + await autocompleteManager.nextEditSuggestionManager.acceptOrJump() + }), + ) + // Esc handler: dismiss the pending suggestion without applying. + context.subscriptions.push( + vscode.commands.registerCommand("kilo-code.new.autocomplete.nextEdit.dismiss", () => { + autocompleteManager.nextEditSuggestionManager.clear() + }), + ) // Register AutocompleteServiceManager Code Actions context.subscriptions.push( diff --git a/packages/kilo-vscode/src/services/autocomplete/migrate-default.ts b/packages/kilo-vscode/src/services/autocomplete/migrate-default.ts new file mode 100644 index 00000000000..9a4eeed4145 --- /dev/null +++ b/packages/kilo-vscode/src/services/autocomplete/migrate-default.ts @@ -0,0 +1,40 @@ +import * as vscode from "vscode" +import { DEFAULT_AUTOCOMPLETE_MODEL } from "../../shared/autocomplete-models" + +const FLAG = "kilo.autocomplete.defaultClearMigrationV1" + +/** + * One-time migration: clear `kilo-code.new.autocomplete.{provider,model}` when + * they exactly match the current `DEFAULT_AUTOCOMPLETE_MODEL`. Many users have + * the default explicitly stored only because it was the only thing visible in + * the dropdown — leaving it pinned would block them from picking up future + * default changes (e.g. a switch to Mercury Next Edit) silently. After the + * migration runs they show up as "Not set" and follow the resolved default. + * + * Users who picked a different model are untouched. The migration runs once + * per machine and is gated on a globalState flag. + * + * TODO(2026-09): remove this migration. By September 2026 the cohort that + * needed it will either have migrated already or be fine staying pinned. + */ +export async function migrateDefaultAutocompleteSettings(context: vscode.ExtensionContext): Promise { + if (context.globalState.get(FLAG)) return + + const config = vscode.workspace.getConfiguration("kilo-code.new.autocomplete") + // Read the user/global scope specifically. `get()` returns the merged + // effective value (workspace > global > default), which would let a + // workspace-level pin or the schema default falsely look like a stored + // global default and cause us to no-op while still flipping the flag. + const provider = config.inspect("provider")?.globalValue + const model = config.inspect("model")?.globalValue + + const matchesDefault = + provider === DEFAULT_AUTOCOMPLETE_MODEL.providerID && model === DEFAULT_AUTOCOMPLETE_MODEL.modelID + + if (matchesDefault) { + await config.update("provider", undefined, vscode.ConfigurationTarget.Global) + await config.update("model", undefined, vscode.ConfigurationTarget.Global) + } + + await context.globalState.update(FLAG, true) +} diff --git a/packages/kilo-vscode/src/services/autocomplete/next-edit/MercuryEditProvider.ts b/packages/kilo-vscode/src/services/autocomplete/next-edit/MercuryEditProvider.ts new file mode 100644 index 00000000000..e1886b8639b --- /dev/null +++ b/packages/kilo-vscode/src/services/autocomplete/next-edit/MercuryEditProvider.ts @@ -0,0 +1,111 @@ +import type { KiloConnectionService } from "../../cli-backend" +import { nesLog, nesWarn } from "./log" +import type { MercuryEditRequestContext, MercuryEditSuggestion } from "./types" + +const MERCURY_MAX_TOKENS = 512 +const DEFAULT_PROVIDER_ID = "inception" +const DEFAULT_MODEL_ID = "mercury-next-edit" + +type EditResponseData = { content?: string; usage?: { prompt_tokens?: number; completion_tokens?: number } } + +export interface MercuryEditProviderOptions { + connectionService: KiloConnectionService + /** Provider id to send to the gateway (e.g. `"kilo"` or `"inception"`). */ + providerId?: string + /** Model id to send to the gateway (e.g. `"inception/mercury-next-edit"`). */ + modelId?: string + /** AbortSignal for cancellation (cursor moves, escape, etc.). */ + signal?: AbortSignal +} + +/** + * Thin wrapper around the SDK's `client.kilo.edit(...)` endpoint (non-streaming). + * The gateway (`packages/kilo-gateway/src/server/edit.ts`) handles auth, routing + * to Mercury's `/v1/edit/completions`, and unwrapping the triple-backtick fence — + * so the VSCode side only deals in already-parsed code. + */ +export class MercuryEditProvider { + constructor(private readonly options: MercuryEditProviderOptions) {} + + async suggest(ctx: MercuryEditRequestContext): Promise { + const start = Date.now() + const provider = this.options.providerId ?? DEFAULT_PROVIDER_ID + const model = this.options.modelId ?? DEFAULT_MODEL_ID + nesLog( + `-> /kilo/edit provider=${provider} model=${model} region=[${ctx.editableRegionStartLine},${ctx.editableRegionEndLine}] diffs=${ctx.editDiffHistory.length} snippets=${ctx.recentlyViewedSnippets.length}`, + ) + + const client = await this.options.connectionService.getClientAsync() + try { + // Send structured editor context; the gateway assembles the Mercury prompt. + const { data, error, response } = await client.kilo.edit( + { + provider, + model, + maxTokens: MERCURY_MAX_TOKENS, + currentFilePath: ctx.currentFilePath, + currentFileContent: ctx.currentFileContent, + cursorLine: ctx.cursorLine, + cursorCharacter: ctx.cursorCharacter, + editableRegionStartLine: ctx.editableRegionStartLine, + editableRegionEndLine: ctx.editableRegionEndLine, + recentlyViewedSnippets: ctx.recentlyViewedSnippets, + editDiffHistory: ctx.editDiffHistory, + }, + { signal: this.options.signal, throwOnError: false }, + ) + const latencyMs = Date.now() - start + if (error) { + // HTTP status lives on the Response object, not the parsed error body. + const status = typeof response?.status === "number" ? response.status : null + nesWarn(`<- error ${status ?? "?"} (${latencyMs}ms): ${safeStringify(error)}`) + throw new MercuryEditError(`Edit request failed: ${status ?? "?"} ${safeStringify(error)}`, status) + } + return this.parseSuccess(ctx, data, latencyMs) + } catch (err) { + if ((err as Error)?.name === "AbortError") throw err + if (err instanceof MercuryEditError) throw err + const msg = err instanceof Error ? err.message : String(err) + nesWarn(`<- transport error: ${msg}`) + throw new MercuryEditError(`Edit request failed: ${msg}`, null) + } + } + + private parseSuccess( + ctx: MercuryEditRequestContext, + data: EditResponseData | undefined, + latencyMs: number, + ): MercuryEditSuggestion | null { + const replacement = data?.content ?? null + const usage = data?.usage + nesLog(`<- ok (${latencyMs}ms) tokens=${usage?.completion_tokens ?? "?"} parsedChars=${replacement?.length ?? 0}`) + if (replacement === null || replacement.length === 0) return null + return { + replacement, + editableRegionStartLine: ctx.editableRegionStartLine, + editableRegionEndLine: ctx.editableRegionEndLine, + latencyMs, + inputTokens: usage?.prompt_tokens, + outputTokens: usage?.completion_tokens, + } + } +} + +export class MercuryEditError extends Error { + constructor( + message: string, + public readonly status: number | null, + ) { + super(message) + this.name = "MercuryEditError" + } +} + +function safeStringify(value: unknown): string { + try { + if (typeof value === "string") return value + return JSON.stringify(value) + } catch { + return String(value) + } +} diff --git a/packages/kilo-vscode/src/services/autocomplete/next-edit/NextEditInlineCompletionProvider.ts b/packages/kilo-vscode/src/services/autocomplete/next-edit/NextEditInlineCompletionProvider.ts new file mode 100644 index 00000000000..5d613465c5b --- /dev/null +++ b/packages/kilo-vscode/src/services/autocomplete/next-edit/NextEditInlineCompletionProvider.ts @@ -0,0 +1,397 @@ +import * as vscode from "vscode" +import type { KiloConnectionService } from "../../cli-backend" +import { computeEditableRegion } from "./editableRegion" +import { EditHistoryTracker } from "./editHistoryTracker" +import { nesLog } from "./log" +import { MercuryEditError, MercuryEditProvider } from "./MercuryEditProvider" +import type { NextEditSuggestionManager } from "./NextEditSuggestionManager" +import type { MercuryEditRequestContext, MercuryRecentSnippet } from "./types" + +const INLINE_COMPLETION_ACCEPTED_COMMAND = "kilo-code.new.autocomplete.nextEdit.accepted" +const DEFAULT_DEBOUNCE_MS = 250 + +export interface NextEditProviderDeps { + /** Routes Mercury calls through the local Kilo gateway (handles auth + BYOK). */ + connectionService: KiloConnectionService + /** Optional source of recently-viewed snippets (kilocode's VisibleCodeTracker can adapt to this). */ + getRecentlyViewedSnippets?: (document: vscode.TextDocument) => MercuryRecentSnippet[] + /** Returns false for files that must not be sent to a server (.env etc). */ + isFileAllowed: (fsPath: string) => Promise + /** Telemetry hook fired on every suggestion result. */ + onSuggestion?: (event: NextEditSuggestionEvent) => void + onFatalError?: (status: number | null) => void + /** Stash for diffs that don't land on the cursor's line — rendered as a jump affordance. */ + suggestionManager?: NextEditSuggestionManager + /** Resolves the currently selected (provider, model) at request time. */ + getModelSelection?: () => { providerId: string; modelId: string } +} + +export interface NextEditSuggestionEvent { + shown: boolean + latencyMs: number + status: "ok" | "no-replacement" | "error" + errorStatus?: number + inputTokens?: number + outputTokens?: number +} + +/** A parsed Mercury suggestion plus the editable region it targets. */ +type SuggestionResult = { + replacement: string + editableRegionStartLine: number + editableRegionEndLine: number + latencyMs: number + inputTokens?: number + outputTokens?: number +} + +export class NextEditInlineCompletionProvider implements vscode.InlineCompletionItemProvider, vscode.Disposable { + private readonly editHistoryTracker: EditHistoryTracker + private debounceTimer: NodeJS.Timeout | null = null + private currentAbort: AbortController | null = null + + constructor(private readonly deps: NextEditProviderDeps) { + this.editHistoryTracker = new EditHistoryTracker({ isFileAllowed: deps.isFileAllowed }) + } + + dispose(): void { + this.editHistoryTracker.dispose() + if (this.debounceTimer) clearTimeout(this.debounceTimer) + this.currentAbort?.abort() + } + + async provideInlineCompletionItems( + document: vscode.TextDocument, + position: vscode.Position, + context: vscode.InlineCompletionContext, + token: vscode.CancellationToken, + ): Promise { + if (document.uri.scheme !== "file") return undefined + if (this.deps.suggestionManager?.isPending()) return undefined + + // Never send a file unless the access policy explicitly approves it. + if (!(await this.allowed(document.uri.fsPath))) return undefined + + const isExplicit = context.triggerKind === vscode.InlineCompletionTriggerKind.Invoke + if (!isExplicit) { + await this.debounce(DEFAULT_DEBOUNCE_MS, token) + if (token.isCancellationRequested) return undefined + } + + const abort = this.swapAbortController(token) + const ctx = await this.buildRequestContext(document, position) + const sel = this.deps.getModelSelection?.() + const provider = new MercuryEditProvider({ + connectionService: this.deps.connectionService, + providerId: sel?.providerId, + modelId: sel?.modelId, + signal: abort.signal, + }) + + try { + const suggestion = await provider.suggest(ctx) + if (!suggestion || token.isCancellationRequested) { + this.deps.onSuggestion?.({ shown: false, latencyMs: 0, status: "no-replacement" }) + return undefined + } + return this.toCompletionItems(document, position, suggestion) + } catch (err) { + return this.handleError(err) + } + } + + private async allowed(path: string): Promise { + const allow = this.deps.isFileAllowed + if (!allow) return false + return allow(path).catch(() => false) + } + + private swapAbortController(token: vscode.CancellationToken): AbortController { + this.currentAbort?.abort() + const abort = new AbortController() + this.currentAbort = abort + token.onCancellationRequested(() => abort.abort()) + return abort + } + + private async buildRequestContext( + document: vscode.TextDocument, + position: vscode.Position, + ): Promise { + const { startLine, endLine } = computeEditableRegion({ + cursorLine: position.line, + totalLines: document.lineCount, + }) + await this.editHistoryTracker.flush(document) + return { + // Mirror classic autocomplete's policy: never send an absolute fsPath upstream. + // Mercury only needs the path for language/context hints, and the workspace-relative + // form is what `recentlyViewedSnippets` already uses (see recentSnippetsAdapter.ts). + currentFilePath: vscode.workspace.asRelativePath(document.uri, false), + currentFileContent: document.getText(), + cursorLine: position.line, + cursorCharacter: position.character, + editableRegionStartLine: startLine, + editableRegionEndLine: endLine, + recentlyViewedSnippets: this.deps.getRecentlyViewedSnippets?.(document) ?? [], + editDiffHistory: await this.editHistoryTracker.getRecentDiffs(), + } + } + + private toCompletionItems( + document: vscode.TextDocument, + position: vscode.Position, + suggestion: SuggestionResult, + ): vscode.InlineCompletionItem[] | undefined { + const endLine = Math.min(suggestion.editableRegionEndLine, document.lineCount - 1) + const fullRange = new vscode.Range( + new vscode.Position(suggestion.editableRegionStartLine, 0), + document.lineAt(endLine).range.end, + ) + const currentText = document.getText(fullRange) + if (currentText === suggestion.replacement) { + this.emitNotShown(suggestion) + return undefined + } + + // Trim to minimal diff: skip identical leading and trailing lines. + const currentLines = currentText.split("\n") + const proposedLines = suggestion.replacement.split("\n") + let prefixLines = 0 + while ( + prefixLines < currentLines.length && + prefixLines < proposedLines.length && + currentLines[prefixLines] === proposedLines[prefixLines] + ) + prefixLines++ + let suffixLines = 0 + while ( + suffixLines < currentLines.length - prefixLines && + suffixLines < proposedLines.length - prefixLines && + currentLines[currentLines.length - 1 - suffixLines] === proposedLines[proposedLines.length - 1 - suffixLines] + ) + suffixLines++ + + const diffStartLineInFile = suggestion.editableRegionStartLine + prefixLines + const diffEndLineInFile = suggestion.editableRegionStartLine + currentLines.length - 1 - suffixLines + const trimmedLines = proposedLines.slice(prefixLines, proposedLines.length - suffixLines) + const trimmedReplacement = trimmedLines.join("\n") + + nesLog( + `diff at lines [${diffStartLineInFile}..${diffEndLineInFile}], cursor at line ${position.line}, ${trimmedReplacement.length} chars`, + ) + + // VSCode's inline ghost text only renders when the diff starts on the cursor's line. + // For off-cursor diffs, stash the suggestion in the manager — it renders a + // decoration-based "jump to next edit" affordance and Tab handles the move/apply. + const isPureInsertion = diffEndLineInFile < diffStartLineInFile + const removesLines = trimmedLines.length === 0 + if (isPureInsertion || removesLines || diffStartLineInFile !== position.line) { + this.stashOffCursorSuggestion( + document, + diffStartLineInFile, + diffEndLineInFile, + trimmedReplacement, + isPureInsertion, + removesLines, + suggestion, + ) + return undefined + } + // Same-line diff: clear any prior off-cursor pending state so we don't render + // two competing affordances. + this.deps.suggestionManager?.clear() + return this.renderSameLineItem( + document, + position, + proposedLines, + prefixLines, + suffixLines, + diffStartLineInFile, + diffEndLineInFile, + trimmedReplacement, + suggestion, + ) + } + + /** Build the cursor-position ghost-text item for a same-line diff. */ + private renderSameLineItem( + document: vscode.TextDocument, + position: vscode.Position, + proposedLines: string[], + prefixLines: number, + suffixLines: number, + diffStartLine: number, + diffEndLine: number, + trimmedReplacement: string, + suggestion: SuggestionResult, + ): vscode.InlineCompletionItem[] | undefined { + const cursorLineText = document.lineAt(position.line).text + const cursorLineCurrent = cursorLineText.slice(position.character) + const cursorLineProposed = proposedLines[prefixLines] + // A pure deletion at the trim seam has no cursor-line replacement to render. + if (cursorLineProposed === undefined) { + this.emitNotShown(suggestion) + return undefined + } + // Native ghost text cannot alter text before the cursor; present that edit + // through the decoration/apply flow rather than silently discarding it. + if (!cursorLineProposed.startsWith(cursorLineText.slice(0, position.character))) { + this.stashOffCursorSuggestion(document, diffStartLine, diffEndLine, trimmedReplacement, false, false, suggestion) + return undefined + } + const insertText = [ + cursorLineProposed.slice(position.character), + ...proposedLines.slice(prefixLines + 1, proposedLines.length - suffixLines), + ].join("\n") + const renderEndLine = pickRenderEndLine(document, position.line, diffEndLine, insertText) + // A single-line insert spanning non-blank lines below the cursor can't be + // represented as inline ghost text — route it to the decoration path. + if (renderEndLine > position.line && !insertText.includes("\n")) { + this.stashOffCursorSuggestion(document, diffStartLine, diffEndLine, trimmedReplacement, false, false, suggestion) + return undefined + } + const renderRange = new vscode.Range( + position, + new vscode.Position(renderEndLine, document.lineAt(renderEndLine).range.end.character), + ) + if (document.getText(renderRange) === cursorLineCurrent && cursorLineCurrent === insertText) return undefined + + const item = new vscode.InlineCompletionItem(insertText, renderRange, { + command: INLINE_COMPLETION_ACCEPTED_COMMAND, + title: "Next Edit Accepted", + }) + nesLog( + `RENDER range=[${renderRange.start.line}:${renderRange.start.character}..${renderRange.end.line}:${renderRange.end.character}] insertChars=${insertText.length}`, + ) + this.deps.onSuggestion?.({ + shown: true, + latencyMs: suggestion.latencyMs, + status: "ok", + inputTokens: suggestion.inputTokens, + outputTokens: suggestion.outputTokens, + }) + return [item] + } + + private emitNotShown(suggestion: SuggestionResult): void { + this.deps.onSuggestion?.({ + shown: false, + latencyMs: suggestion.latencyMs, + status: "no-replacement", + inputTokens: suggestion.inputTokens, + outputTokens: suggestion.outputTokens, + }) + } + + private stashOffCursorSuggestion( + document: vscode.TextDocument, + diffStartLine: number, + diffEndLine: number, + trimmedReplacement: string, + isPureInsertion: boolean, + removesLines: boolean, + suggestion: SuggestionResult, + ): void { + const mgr = this.deps.suggestionManager + if (!mgr) { + // Manager wasn't wired — fall through silently. The classic path + // already covers same-line completions; this branch only matters in + // tests or misconfigured embeds. + this.emitNotShown(suggestion) + return + } + if (isPureInsertion) { + // The original text we snapshot must come from the line VSCode will see + // when the user later accepts. For mid-file inserts that's `diffStartLine` + // (the line that gets pushed down). For EOF inserts (diffStartLine === + // lineCount) there is no such line; fall back to lineCount-1 (the last + // line, which will sit just above the inserted content). The + // SuggestionManager's drift guard knows to compare against this anchor. + const isEof = diffStartLine >= document.lineCount + const anchorLine = isEof + ? Math.max(0, document.lineCount - 1) + : Math.max(0, Math.min(diffStartLine, document.lineCount - 1)) + mgr.setPending({ + kind: "insert", + document, + diffStartLine, + diffEndLine: diffStartLine, + replacement: trimmedReplacement + "\n", + originalText: document.lineAt(anchorLine).text, + }) + nesLog( + `insert suggestion stashed at line ${diffStartLine} (anchor=${anchorLine}, eof=${isEof}, ${trimmedReplacement.length} chars)`, + ) + } else { + const originalRange = new vscode.Range( + new vscode.Position(diffStartLine, 0), + new vscode.Position(diffEndLine, document.lineAt(diffEndLine).range.end.character), + ) + mgr.setPending({ + kind: "replace", + document, + diffStartLine, + diffEndLine, + replacement: trimmedReplacement, + removesLines, + originalText: document.getText(originalRange), + }) + nesLog(`replace suggestion stashed at lines [${diffStartLine}..${diffEndLine}]`) + } + this.deps.onSuggestion?.({ + shown: true, + latencyMs: suggestion.latencyMs, + status: "ok", + inputTokens: suggestion.inputTokens, + outputTokens: suggestion.outputTokens, + }) + } + + private handleError(err: unknown): undefined { + if ((err as Error)?.name === "AbortError") return undefined + const status = err instanceof MercuryEditError ? err.status : null + this.deps.onSuggestion?.({ + shown: false, + latencyMs: 0, + status: "error", + errorStatus: status ?? undefined, + }) + if (status === 401 || status === 402) this.deps.onFatalError?.(status) + return undefined + } + + private debounce(ms: number, token: vscode.CancellationToken): Promise { + if (this.debounceTimer) clearTimeout(this.debounceTimer) + return new Promise((resolve) => { + this.debounceTimer = setTimeout(resolve, ms) + token.onCancellationRequested(() => { + if (this.debounceTimer) clearTimeout(this.debounceTimer) + resolve() + }) + }) + } +} + +export { INLINE_COMPLETION_ACCEPTED_COMMAND } + +/** + * VSCode's inline ghost text silently fails to render when the completion's + * range crosses a line boundary but the insert text has no newline (typical + * when Mercury implicitly drops a trailing blank line as file-end + * normalization). When that happens — and the lines past the cursor are + * blank — cap the range at the cursor's line so the ghost renders cleanly. + */ +function pickRenderEndLine( + document: vscode.TextDocument, + cursorLine: number, + diffEndLine: number, + insertText: string, +): number { + if (diffEndLine <= cursorLine) return diffEndLine + if (insertText.includes("\n")) return diffEndLine + for (let l = cursorLine + 1; l <= diffEndLine; l++) { + if (document.lineAt(l).text.trim() !== "") return diffEndLine + } + return cursorLine +} diff --git a/packages/kilo-vscode/src/services/autocomplete/next-edit/NextEditSuggestionManager.ts b/packages/kilo-vscode/src/services/autocomplete/next-edit/NextEditSuggestionManager.ts new file mode 100644 index 00000000000..a8ba61f16b8 --- /dev/null +++ b/packages/kilo-vscode/src/services/autocomplete/next-edit/NextEditSuggestionManager.ts @@ -0,0 +1,352 @@ +import * as vscode from "vscode" +import { nesLog } from "./log" +import { planInsertion, planReplacement } from "./pendingEdit" + +const PENDING_CONTEXT_KEY = "kilo-code.nextEdit.hasPendingSuggestion" +const CHAIN_DELAY_MS = 60 + +export type PendingNextEdit = + | { + kind: "replace" + document: vscode.TextDocument + /** Inclusive start line of the lines being replaced. */ + diffStartLine: number + /** Inclusive end line of the lines being replaced. */ + diffEndLine: number + /** New text to substitute for [diffStartLine, diffEndLine]. */ + replacement: string + /** Whether the suggestion omits complete lines rather than rewriting one as blank. */ + removesLines: boolean + /** Snapshot of the original text — used to detect drift. */ + originalText: string + } + | { + kind: "insert" + document: vscode.TextDocument + /** Existing line before insertion, or `lineCount` when appending at EOF. */ + diffStartLine: number + /** Same as diffStartLine for hint/jump-target purposes. */ + diffEndLine: number + /** Lines to insert. Must end with a newline so existing content gets pushed down. */ + replacement: string + /** Snapshot of the surrounding (single) line — used as a soft drift guard. */ + originalText: string + } + +/** + * Holds the currently-pending out-of-cursor NES suggestion and renders a + * jump-to-next-edit affordance via editor decorations. Same-line diffs are + * still handled by `InlineCompletionItem` (faster, native ghost text) — this + * manager is for everything else. + * + * Lifecycle: at most one pending suggestion at a time. A pending suggestion + * is cleared when the user accepts, dismisses, edits inside the diff range, + * or moves to a different document. + */ +export class NextEditSuggestionManager implements vscode.Disposable { + private pending: PendingNextEdit | null = null + private readonly subscriptions: vscode.Disposable[] = [] + + private readonly removedLineDecoration: vscode.TextEditorDecorationType + private readonly proposedLineDecoration: vscode.TextEditorDecorationType + private readonly hintDecoration: vscode.TextEditorDecorationType + + constructor() { + // Tints + strikethrough on the lines that will be replaced or removed. + this.removedLineDecoration = vscode.window.createTextEditorDecorationType({ + isWholeLine: true, + backgroundColor: new vscode.ThemeColor("diffEditor.removedLineBackground"), + overviewRulerColor: new vscode.ThemeColor("editorInfo.foreground"), + overviewRulerLane: vscode.OverviewRulerLane.Left, + textDecoration: "line-through; opacity: 0.65;", + }) + // Inline `after` text showing the proposed replacement line. + this.proposedLineDecoration = vscode.window.createTextEditorDecorationType({ + after: { + margin: "0 0 0 2em", + color: new vscode.ThemeColor("editorInfo.foreground"), + fontStyle: "italic", + }, + }) + // The one-line user-facing hint. + this.hintDecoration = vscode.window.createTextEditorDecorationType({ + after: { + margin: "0 0 0 2em", + color: new vscode.ThemeColor("editorCodeLens.foreground"), + fontStyle: "italic", + }, + }) + + // Dismiss when the document or selection moves in ways that invalidate + // the prediction. + this.subscriptions.push( + vscode.workspace.onDidChangeTextDocument((e) => { + const p = this.pending + if (!p) return + if (e.document !== p.document) return + // For "insert" we just confirm the anchor line is still there with its + // original content; for "replace" we re-check the full range. + let stillValid = true + try { + if (p.kind === "replace") { + const text = e.document.getText( + new vscode.Range( + new vscode.Position(p.diffStartLine, 0), + new vscode.Position(p.diffEndLine, e.document.lineAt(p.diffEndLine).range.end.character), + ), + ) + stillValid = text === p.originalText + } else { + // Insert mode: only invalidate if the anchor line shifted. + const anchorLine = Math.min(p.diffStartLine, e.document.lineCount - 1) + const anchorText = e.document.lineAt(anchorLine).text + stillValid = anchorText === p.originalText + } + } catch { + stillValid = false + } + if (!stillValid) this.clear() + }), + vscode.window.onDidChangeActiveTextEditor(() => this.clear()), + // When the cursor moves (e.g., post-jump), refresh the hint so it + // flips between "Tab to jump" and "Tab to apply". + vscode.window.onDidChangeTextEditorSelection((e) => { + if (!this.pending) return + if (e.textEditor.document !== this.pending.document) return + this.renderDecorations(this.pending) + }), + ) + } + + public isPending(): boolean { + return this.pending !== null + } + + public getPending(): PendingNextEdit | null { + return this.pending + } + + public setPending(p: PendingNextEdit): void { + this.clearDecorations() + this.pending = p + void vscode.commands.executeCommand("setContext", PENDING_CONTEXT_KEY, true) + // Hide any in-flight inline suggestion so it can't compete with our Tab handler. + void vscode.commands.executeCommand("editor.action.inlineSuggest.hide") + this.renderDecorations(p) + } + + public clear(): void { + if (!this.pending) return + this.pending = null + this.clearDecorations() + void vscode.commands.executeCommand("setContext", PENDING_CONTEXT_KEY, false) + } + + /** Tab handler — accept if cursor near the diff, else jump. */ + public async acceptOrJump(): Promise { + const p = this.pending + if (!p) return + const editor = vscode.window.activeTextEditor + if (!editor || editor.document !== p.document) { + this.clear() + return + } + const cursor = editor.selection.active + const inside = + p.kind === "replace" + ? cursor.line >= p.diffStartLine && cursor.line <= p.diffEndLine + : cursor.line === p.diffStartLine || cursor.line === p.diffStartLine - 1 + if (inside) { + await this.applyPending() + } else { + const targetLine = Math.min(p.diffStartLine, Math.max(0, p.document.lineCount - 1)) + const targetChar = p.document.lineAt(targetLine).firstNonWhitespaceCharacterIndex + const target = new vscode.Position(targetLine, targetChar) + editor.selection = new vscode.Selection(target, target) + editor.revealRange(new vscode.Range(target, target), vscode.TextEditorRevealType.InCenterIfOutsideViewport) + nesLog(`jumped cursor ${cursor.line} -> ${target.line} (pending diff at [${p.diffStartLine}..${p.diffEndLine}])`) + // Refresh hint immediately so "Tab to apply" is shown. + this.renderDecorations(p) + } + } + + private async applyPending(): Promise { + const p = this.pending + if (!p) return + const editor = vscode.window.activeTextEditor + if (!editor || editor.document !== p.document) { + this.clear() + return + } + // Snapshot what we're about to do, then nuke pending state so the upcoming + // document change doesn't re-enter via the invalidation listener. + this.clearDecorations() + this.pending = null + void vscode.commands.executeCommand("setContext", PENDING_CONTEXT_KEY, false) + + let ok = false + if (p.kind === "insert") { + // Re-validate before applying: the anchor line must still hold its + // original text. Without this, edits between the anchor and the insertion + // point can shift line numbers and land the insert in the wrong place. + const anchorLine = Math.min(p.diffStartLine, editor.document.lineCount - 1) + const anchorText = anchorLine >= 0 ? editor.document.lineAt(anchorLine).text : undefined + if (anchorText !== p.originalText) { + nesLog(`document drifted since suggestion was made — dropping insert at line ${p.diffStartLine}`) + return + } + const edit = planInsertion(p, { + lineCount: editor.document.lineCount, + end: (line) => editor.document.lineAt(line).range.end.character, + }) + const pos = new vscode.Position(edit.line, edit.character) + ok = await editor.edit((b) => b.insert(pos, edit.text)) + nesLog(`applied insert at line ${pos.line} (${edit.text.length} chars, ok=${ok})`) + } else { + const range = new vscode.Range( + new vscode.Position(p.diffStartLine, 0), + new vscode.Position(p.diffEndLine, p.document.lineAt(p.diffEndLine).range.end.character), + ) + const currentInDoc = editor.document.getText(range) + if (currentInDoc !== p.originalText) { + nesLog(`document drifted since suggestion was made — dropping range [${p.diffStartLine}..${p.diffEndLine}]`) + return + } + const edit = planReplacement(p, { + lineCount: editor.document.lineCount, + end: (line) => editor.document.lineAt(line).range.end.character, + }) + const target = new vscode.Range( + new vscode.Position(edit.start.line, edit.start.character), + new vscode.Position(edit.end.line, edit.end.character), + ) + ok = await editor.edit((b) => b.replace(target, edit.text)) + nesLog(`applied replace at lines [${p.diffStartLine}..${p.diffEndLine}] (ok=${ok})`) + } + if (ok) chainNextPrediction() + } + + private renderDecorations(p: PendingNextEdit): void { + // Same document can be open in multiple splits — paint all of them so the + // user sees the decoration regardless of which split has focus. + const editors = vscode.window.visibleTextEditors.filter((e) => e.document === p.document) + if (editors.length === 0) return + + const removedRanges: vscode.Range[] = [] + const proposedAnnotations: vscode.DecorationOptions[] = [] + + if (p.kind === "replace") { + const originalLines = p.originalText.split("\n") + const proposedLines = p.replacement.split("\n") + const minLen = Math.min(originalLines.length, proposedLines.length) + for (let i = 0; i < minLen; i++) { + if (originalLines[i] === proposedLines[i]) continue + const lineNo = p.diffStartLine + i + const lineRange = p.document.lineAt(lineNo).range + removedRanges.push(lineRange) + proposedAnnotations.push({ + range: new vscode.Range(lineRange.end, lineRange.end), + renderOptions: { after: { contentText: `→ ${visualize(proposedLines[i])}` } }, + }) + } + // Pure deletions inside a replace + for (let i = minLen; i < originalLines.length; i++) { + const lineNo = p.diffStartLine + i + const lineRange = p.document.lineAt(lineNo).range + removedRanges.push(lineRange) + proposedAnnotations.push({ + range: new vscode.Range(lineRange.end, lineRange.end), + renderOptions: { after: { contentText: `→ (removed)` } }, + }) + } + // Additions inside a replace — anchor on last shared line + if (proposedLines.length > originalLines.length) { + const tailLineNo = p.diffStartLine + originalLines.length - 1 + const safeLine = Math.max(p.diffStartLine, Math.min(tailLineNo, p.diffEndLine)) + const tailRange = p.document.lineAt(safeLine).range + const added = proposedLines.slice(originalLines.length).map(visualize).join(" ⏎ ") + proposedAnnotations.push({ + range: new vscode.Range(tailRange.end, tailRange.end), + renderOptions: { after: { contentText: `+ ${added}` } }, + }) + } + } else { + // Pure insertion: anchor the ghost text on the existing line, no strikethrough. + const anchorLine = Math.min(p.diffStartLine, p.document.lineCount - 1) + const safeAnchor = Math.max(0, anchorLine) + const anchorRange = p.document.lineAt(safeAnchor).range + // Strip the trailing \n we appended for insertion semantics, then show each + // inserted line collapsed with a small separator. + const lines = p.replacement.replace(/\n$/, "").split("\n").map(visualize) + const inserted = lines.join(" ⏎ ") + proposedAnnotations.push({ + range: new vscode.Range(anchorRange.end, anchorRange.end), + renderOptions: { after: { contentText: `+ ${inserted}` } }, + }) + } + + // Hint anchor + cursor check use the active editor if it's one of ours, + // else fall back to the first visible editor for this document. + const active = vscode.window.activeTextEditor + const referenceEditor = active && editors.includes(active) ? active : editors[0] + const hintAnchor = Math.min(p.diffStartLine, p.document.lineCount - 1) + const hintLineEnd = p.document.lineAt(Math.max(0, hintAnchor)).range.end + const cursor = referenceEditor.selection.active + const cursorAtDiff = + p.kind === "replace" + ? cursor.line >= p.diffStartLine && cursor.line <= p.diffEndLine + : cursor.line === p.diffStartLine || cursor.line === p.diffStartLine - 1 + const hintText = cursorAtDiff ? " ↳ Tab to apply · Esc to dismiss" : " ↳ Tab to jump here · Esc to dismiss" + const hintOptions: vscode.DecorationOptions[] = [ + { + range: new vscode.Range(hintLineEnd, hintLineEnd), + renderOptions: { after: { contentText: hintText } }, + }, + ] + + for (const editor of editors) { + editor.setDecorations(this.removedLineDecoration, removedRanges) + editor.setDecorations(this.proposedLineDecoration, proposedAnnotations) + editor.setDecorations(this.hintDecoration, hintOptions) + } + } + + private clearDecorations(): void { + for (const editor of vscode.window.visibleTextEditors) { + editor.setDecorations(this.removedLineDecoration, []) + editor.setDecorations(this.proposedLineDecoration, []) + editor.setDecorations(this.hintDecoration, []) + } + } + + public dispose(): void { + this.clear() + for (const s of this.subscriptions) s.dispose() + this.subscriptions.length = 0 + this.removedLineDecoration.dispose() + this.proposedLineDecoration.dispose() + this.hintDecoration.dispose() + } +} + +/** + * Re-invoke VSCode's inline-suggest UI after an accept so the provider fires + * again and surfaces the next prediction without the user having to type. + * This is the "Tab-Tab-Tab" walk-through-a-refactor UX from Cursor. + * + * A short delay lets the document change settle before we re-enter + * `provideInlineCompletionItems`, and gives the user a moment to abandon the + * chain by typing or moving the cursor. + */ +export function chainNextPrediction(delayMs = CHAIN_DELAY_MS): void { + setTimeout(() => { + void vscode.commands.executeCommand("editor.action.inlineSuggest.trigger") + }, delayMs) +} + +function visualize(line: string): string { + // VSCode after-text decorations don't support newlines — collapse just in case. + // Also surface leading whitespace explicitly so it isn't visually swallowed. + const collapsed = line.replace(/\s+$/g, "").replace(/^\t+/, (t) => " ".repeat(t.length)) + return collapsed.length > 120 ? collapsed.slice(0, 117) + "…" : collapsed +} diff --git a/packages/kilo-vscode/src/services/autocomplete/next-edit/constants.ts b/packages/kilo-vscode/src/services/autocomplete/next-edit/constants.ts new file mode 100644 index 00000000000..cf7ec8d4e09 --- /dev/null +++ b/packages/kilo-vscode/src/services/autocomplete/next-edit/constants.ts @@ -0,0 +1,9 @@ +/** + * Editable-region sizing for Next Edit. Per the Mercury docs, region size + * dominates output latency; centering [-5, +10] around the cursor is the + * recommended starting point. (The Mercury prompt sentinel tokens live in the + * gateway — see packages/kilo-gateway/src/edit-prompt.ts.) + */ +export const DEFAULT_EDITABLE_REGION_TOP_MARGIN = 5 +export const DEFAULT_EDITABLE_REGION_BOTTOM_MARGIN = 10 +export const MAX_EDITABLE_REGION_LINES = 25 diff --git a/packages/kilo-vscode/src/services/autocomplete/next-edit/editHistoryTracker.ts b/packages/kilo-vscode/src/services/autocomplete/next-edit/editHistoryTracker.ts new file mode 100644 index 00000000000..9edebc298f6 --- /dev/null +++ b/packages/kilo-vscode/src/services/autocomplete/next-edit/editHistoryTracker.ts @@ -0,0 +1,168 @@ +import { createPatch } from "diff" +import * as vscode from "vscode" + +const DEFAULT_DEBOUNCE_MS = 1500 +const DEFAULT_MAX_DIFFS = 5 + +type Options = { + debounceMs?: number + maxDiffs?: number + isFileAllowed: (fsPath: string) => Promise +} + +type Diff = { + key: string + patch: string +} + +/** + * Tracks per-file snapshots and emits a workspace-wide chronological stream + * of range-based unidiffs after a short idle window. Cross-file history is + * intentional: Mercury uses recent edits from any file to infer user intent. + * + * Diffs are produced lazily; the tracker holds the previously-emitted state + * per file and computes the diff against the current document content when + * the debounce fires. + */ +export class EditHistoryTracker implements vscode.Disposable { + private readonly snapshots = new Map() + private readonly pendingTimers = new Map() + private readonly diffs: Diff[] = [] + private readonly subscriptions: vscode.Disposable[] = [] + + constructor(private readonly options: Options) { + const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS + + // Seed snapshots on open so the FIRST edit in a freshly-opened file is + // captured in the diff history (otherwise the common "open, type, trigger" + // flow ships an empty edit-history block). Access checks happen before + // reading text so ignored documents are never retained as edit context. + this.subscriptions.push( + vscode.workspace.onDidOpenTextDocument((doc) => { + if (doc.uri.scheme !== "file") return + void this.seed(doc) + }), + ) + for (const doc of vscode.workspace.textDocuments) { + if (doc.uri.scheme === "file") void this.seed(doc) + } + this.subscriptions.push( + vscode.workspace.onDidChangeTextDocument((event) => { + if (event.document.uri.scheme !== "file") return + if (event.contentChanges.length === 0) return + void this.scheduleSnapshotDiff(event.document, debounceMs) + }), + ) + this.subscriptions.push( + vscode.workspace.onDidCloseTextDocument((doc) => { + const key = doc.uri.fsPath + const t = this.pendingTimers.get(key) + if (t) clearTimeout(t) + this.pendingTimers.delete(key) + this.snapshots.delete(key) + }), + ) + } + + /** + * Force the pending diff (if any) for `document` to be emitted now. Call + * this immediately before building a request so the freshest user edit + * makes it into the prompt. + */ + public async flush(document: vscode.TextDocument): Promise { + const key = document.uri.fsPath + if (!(await this.allowed(key))) { + this.reject(key) + return + } + const t = this.pendingTimers.get(key) + if (t) clearTimeout(t) + this.pendingTimers.delete(key) + await this.emitDiffNow(document) + } + + /** Workspace-wide oldest to newest, matching the Mercury prompt-history convention. */ + public async getRecentDiffs(): Promise { + const kept = ( + await Promise.all(this.diffs.map(async (diff) => ((await this.allowed(diff.key)) ? diff : undefined))) + ).filter((diff): diff is Diff => diff !== undefined) + this.diffs.splice(0, this.diffs.length, ...kept) + return kept.map((diff) => diff.patch) + } + + public dispose(): void { + for (const t of this.pendingTimers.values()) clearTimeout(t) + this.pendingTimers.clear() + for (const s of this.subscriptions) s.dispose() + this.subscriptions.length = 0 + } + + private async seed(document: vscode.TextDocument): Promise { + const key = document.uri.fsPath + if (this.snapshots.has(key)) return + if (!(await this.allowed(key))) { + this.reject(key) + return + } + if (!this.snapshots.has(key)) this.snapshots.set(key, document.getText()) + } + + private async scheduleSnapshotDiff(document: vscode.TextDocument, debounceMs: number): Promise { + const key = document.uri.fsPath + if (!(await this.allowed(key))) { + this.reject(key) + return + } + if (!this.snapshots.has(key)) { + // Fallback seed for documents we never saw open (e.g. opened before the + // tracker existed). The triggering change is lost, but subsequent edits + // produce useful diffs. + this.snapshots.set(key, document.getText()) + return + } + const existing = this.pendingTimers.get(key) + if (existing) clearTimeout(existing) + const timer = setTimeout(() => { + this.pendingTimers.delete(key) + void this.emitDiffNow(document) + }, debounceMs) + this.pendingTimers.set(key, timer) + } + + private async emitDiffNow(document: vscode.TextDocument): Promise { + const key = document.uri.fsPath + if (!(await this.allowed(key))) { + this.reject(key) + return + } + const previous = this.snapshots.get(key) + if (previous === undefined) return + const current = document.getText() + if (current === previous) return + + const filename = vscode.workspace.asRelativePath(document.uri, false) + const patch = createPatch(filename, previous, current, undefined, undefined, { context: 1 }) + // `createPatch` returns "" for identical inputs; guard anyway. + if (patch && patch.trim().length > 0) { + this.diffs.push({ key, patch }) + const maxDiffs = this.options.maxDiffs ?? DEFAULT_MAX_DIFFS + if (this.diffs.length > maxDiffs) this.diffs.shift() + } + this.snapshots.set(key, current) + } + + private async allowed(key: string): Promise { + const allow = this.options.isFileAllowed + if (!allow) return false + return allow(key).catch(() => false) + } + + private reject(key: string): void { + const timer = this.pendingTimers.get(key) + if (timer) clearTimeout(timer) + this.pendingTimers.delete(key) + this.snapshots.delete(key) + const kept = this.diffs.filter((diff) => diff.key !== key) + this.diffs.splice(0, this.diffs.length, ...kept) + } +} diff --git a/packages/kilo-vscode/src/services/autocomplete/next-edit/editableRegion.ts b/packages/kilo-vscode/src/services/autocomplete/next-edit/editableRegion.ts new file mode 100644 index 00000000000..16be4a992b5 --- /dev/null +++ b/packages/kilo-vscode/src/services/autocomplete/next-edit/editableRegion.ts @@ -0,0 +1,43 @@ +import { + DEFAULT_EDITABLE_REGION_BOTTOM_MARGIN, + DEFAULT_EDITABLE_REGION_TOP_MARGIN, + MAX_EDITABLE_REGION_LINES, +} from "./constants" + +export interface EditableRegionInputs { + cursorLine: number + totalLines: number + topMargin?: number + bottomMargin?: number +} + +export interface EditableRegion { + startLine: number + endLine: number +} + +/** + * Editable region selection per the Mercury docs: center [-top, +bottom] around + * the cursor, clipped to file bounds. Capped to MAX_EDITABLE_REGION_LINES (~25) + * because output tokens dominate latency. + */ +export function computeEditableRegion({ + cursorLine, + totalLines, + topMargin = DEFAULT_EDITABLE_REGION_TOP_MARGIN, + bottomMargin = DEFAULT_EDITABLE_REGION_BOTTOM_MARGIN, +}: EditableRegionInputs): EditableRegion { + if (totalLines <= 0) return { startLine: 0, endLine: 0 } + + const lastLine = totalLines - 1 + let start = Math.max(0, cursorLine - topMargin) + let end = Math.min(lastLine, cursorLine + bottomMargin) + + const span = end - start + 1 + if (span > MAX_EDITABLE_REGION_LINES) { + const overflow = span - MAX_EDITABLE_REGION_LINES + // Prefer trimming below the cursor, where we have less semantic context. + end = Math.max(start, end - overflow) + } + return { startLine: start, endLine: end } +} diff --git a/packages/kilo-vscode/src/services/autocomplete/next-edit/log.ts b/packages/kilo-vscode/src/services/autocomplete/next-edit/log.ts new file mode 100644 index 00000000000..7e7c3239ba3 --- /dev/null +++ b/packages/kilo-vscode/src/services/autocomplete/next-edit/log.ts @@ -0,0 +1,37 @@ +import * as vscode from "vscode" + +const CHANNEL_NAME = "Kilo Code · Next Edit" + +let channel: vscode.OutputChannel | null = null + +function getChannel(): vscode.OutputChannel { + if (!channel) channel = vscode.window.createOutputChannel(CHANNEL_NAME) + return channel +} + +function debugEnabled(): boolean { + // Toggled via env only — deliberately not a VSCode setting, to avoid adding + // new autocomplete config (config is migrating to the backend). + return process.env.KILO_NES_DEBUG === "1" +} + +/** + * Append a single log line to the dedicated NES output channel. Always goes to + * the channel (so a user troubleshooting can flip it on without rebuilding); + * `console.log` is mirrored only when the debug setting is enabled. + */ +export function nesLog(message: string): void { + getChannel().appendLine(`[${new Date().toISOString()}] ${message}`) + if (debugEnabled()) console.log(`[NES] ${message}`) +} + +/** Equivalent of `console.warn` for the channel. */ +export function nesWarn(message: string): void { + getChannel().appendLine(`[${new Date().toISOString()}] WARN ${message}`) + if (debugEnabled()) console.warn(`[NES] ${message}`) +} + +export function disposeLog(): void { + channel?.dispose() + channel = null +} diff --git a/packages/kilo-vscode/src/services/autocomplete/next-edit/pendingEdit.ts b/packages/kilo-vscode/src/services/autocomplete/next-edit/pendingEdit.ts new file mode 100644 index 00000000000..bea2c9c1d86 --- /dev/null +++ b/packages/kilo-vscode/src/services/autocomplete/next-edit/pendingEdit.ts @@ -0,0 +1,43 @@ +type Document = { + lineCount: number + end(line: number): number +} + +type Insertion = { + diffStartLine: number + replacement: string +} + +type Replacement = Insertion & { + diffEndLine: number + removesLines: boolean +} + +export function planInsertion(input: Insertion, document: Document) { + if (input.diffStartLine < document.lineCount) { + return { line: input.diffStartLine, character: 0, text: input.replacement } + } + const line = Math.max(0, document.lineCount - 1) + const text = input.replacement.endsWith("\n") ? input.replacement.slice(0, -1) : input.replacement + return { line, character: document.end(line), text: `\n${text}` } +} + +export function planReplacement(input: Replacement, document: Document) { + const end = { line: input.diffEndLine, character: document.end(input.diffEndLine) } + if (!input.removesLines) { + return { start: { line: input.diffStartLine, character: 0 }, end, text: input.replacement } + } + if (input.diffEndLine < document.lineCount - 1) { + return { + start: { line: input.diffStartLine, character: 0 }, + end: { line: input.diffEndLine + 1, character: 0 }, + text: input.replacement, + } + } + if (input.diffStartLine === 0) return { start: { line: 0, character: 0 }, end, text: input.replacement } + return { + start: { line: input.diffStartLine - 1, character: document.end(input.diffStartLine - 1) }, + end, + text: input.replacement, + } +} diff --git a/packages/kilo-vscode/src/services/autocomplete/next-edit/recentSnippetsAdapter.ts b/packages/kilo-vscode/src/services/autocomplete/next-edit/recentSnippetsAdapter.ts new file mode 100644 index 00000000000..b7295ded353 --- /dev/null +++ b/packages/kilo-vscode/src/services/autocomplete/next-edit/recentSnippetsAdapter.ts @@ -0,0 +1,52 @@ +import type { AutocompleteCodeSnippet } from "../continuedev/core/autocomplete/types" +import * as vscode from "vscode" +import type { MercuryRecentSnippet } from "./types" + +const MAX_SNIPPET_LINES = 20 +const MAX_SNIPPETS = 5 + +/** + * Convert kilocode's already-collected `RecentlyVisitedRangesService` output + * into the shape Mercury Edit expects for the `<|recently_viewed_code_snippets|>` + * block. Per docs: 3–5 snippets × ~20 lines, oldest → newest, excluding the + * currently active file (the service already filters that out). + * + * `RecentlyVisitedRangesService.getSnippets()` returns snippets newest→oldest; + * we reverse so Mercury sees them in chronological order. + */ +export function toMercuryRecentSnippets( + snippets: ReadonlyArray>, +): MercuryRecentSnippet[] { + return snippets + .slice(0, MAX_SNIPPETS) + .reverse() + .map((s) => ({ + filepath: shortenPath(s.filepath), + content: trimToLines(s.content, MAX_SNIPPET_LINES), + })) +} + +export function toAllowedMercuryRecentSnippets( + snippets: ReadonlyArray>, + allowed: (filepath: string) => boolean, +): MercuryRecentSnippet[] { + return toMercuryRecentSnippets(snippets.filter((snippet) => allowed(snippet.filepath))) +} + +function trimToLines(content: string, maxLines: number): string { + const lines = content.split("\n") + if (lines.length <= maxLines) return content + // Center the trim window — keep the most semantically meaningful core. + const start = Math.floor((lines.length - maxLines) / 2) + return lines.slice(start, start + maxLines).join("\n") +} + +function shortenPath(uri: string): string { + // Convert file:// URI strings to workspace-relative paths so the prompt is compact. + try { + const parsed = vscode.Uri.parse(uri) + return vscode.workspace.asRelativePath(parsed, false) + } catch { + return uri + } +} diff --git a/packages/kilo-vscode/src/services/autocomplete/next-edit/types.ts b/packages/kilo-vscode/src/services/autocomplete/next-edit/types.ts new file mode 100644 index 00000000000..a88e346a360 --- /dev/null +++ b/packages/kilo-vscode/src/services/autocomplete/next-edit/types.ts @@ -0,0 +1,26 @@ +export interface MercuryRecentSnippet { + filepath: string + content: string +} + +export interface MercuryEditRequestContext { + currentFilePath: string + currentFileContent: string + cursorLine: number + cursorCharacter: number + editableRegionStartLine: number + editableRegionEndLine: number + recentlyViewedSnippets: MercuryRecentSnippet[] + editDiffHistory: string[] +} + +export interface MercuryEditSuggestion { + /** The replacement text for lines [editableRegionStartLine, editableRegionEndLine]. */ + replacement: string + editableRegionStartLine: number + editableRegionEndLine: number + /** Latency in milliseconds from request send to response parse. */ + latencyMs: number + inputTokens?: number + outputTokens?: number +} diff --git a/packages/kilo-vscode/src/services/autocomplete/settings.ts b/packages/kilo-vscode/src/services/autocomplete/settings.ts index 9ec341406dc..0a3f189d58b 100644 --- a/packages/kilo-vscode/src/services/autocomplete/settings.ts +++ b/packages/kilo-vscode/src/services/autocomplete/settings.ts @@ -1,5 +1,5 @@ import * as vscode from "vscode" -import { AUTOCOMPLETE_MODELS, getAutocompleteModel } from "../../shared/autocomplete-models" +import { validAutocompleteModel, validAutocompleteProvider } from "../../shared/autocomplete-models" type Message = { type: string @@ -18,13 +18,17 @@ export async function routeAutocompleteMessage(message: Message, post: Post): Pr export function buildAutocompleteSettingsMessage() { const config = vscode.workspace.getConfiguration("kilo-code.new.autocomplete") + // Pass through provider/model as-is (null when unset) so the webview can + // distinguish "user hasn't picked" from "user picked the current default." + // The runtime resolves null → DEFAULT_AUTOCOMPLETE_MODEL via getAutocompleteModel(). return { type: "autocompleteSettingsLoaded" as const, settings: { enableAutoTrigger: config.get("enableAutoTrigger", true), enableSmartInlineTaskKeybinding: config.get("enableSmartInlineTaskKeybinding", false), enableChatAutocomplete: config.get("enableChatAutocomplete", false), - model: getAutocompleteModel(config.get("model") ?? "").id, + provider: config.get("provider") ?? null, + model: config.get("model") ?? null, }, } } @@ -40,8 +44,14 @@ export function watchAutocompleteConfig(post: Post): vscode.Disposable { export function validAutocompleteSetting(key: string, value: unknown) { if (key === "model") { - if (typeof value !== "string") return false - return AUTOCOMPLETE_MODELS.some((m) => m.id === value) + // Allow clearing back to the server-side default. + if (value === null || value === undefined) return true + return validAutocompleteModel(value) + } + + if (key === "provider") { + if (value === null || value === undefined) return true + return validAutocompleteProvider(value) } if (key === "enableAutoTrigger") return typeof value === "boolean" diff --git a/packages/kilo-vscode/src/services/cli-backend/cli-resources.ts b/packages/kilo-vscode/src/services/cli-backend/cli-resources.ts new file mode 100644 index 00000000000..29b53f093da --- /dev/null +++ b/packages/kilo-vscode/src/services/cli-backend/cli-resources.ts @@ -0,0 +1,39 @@ +import * as fs from "fs" +import * as path from "path" + +const dir = "tree-sitter" +const runtime = "tree-sitter.wasm" + +function paths(file: string) { + if (/^[a-z]:[\\/]/i.test(file) || file.includes("\\")) return path.win32 + return path +} + +export function treeSitterDirForBinary(file: string): string { + const p = paths(file) + return p.join(p.dirname(file), dir) +} + +export function treeSitterDirForExtension(root: string): string { + return paths(root).join(root, "bin", dir) +} + +export function resolveTreeSitterEnv(root: string): Record { + return { KILO_TREE_SITTER_WASM_DIR: treeSitterDirForExtension(root) } +} + +export function hasTreeSitterResources(file: string): boolean { + return fs.existsSync(path.join(treeSitterDirForBinary(file), runtime)) +} + +export async function copyTreeSitterResources(source: string, target: string): Promise { + const from = treeSitterDirForBinary(source) + const to = treeSitterDirForBinary(target) + + if (!fs.existsSync(path.join(from, runtime))) { + throw new Error(`CLI tree-sitter resources not found at ${from}`) + } + + await fs.promises.rm(to, { recursive: true, force: true }) + await fs.promises.cp(from, to, { recursive: true }) +} diff --git a/packages/kilo-vscode/src/services/cli-backend/connection-service.ts b/packages/kilo-vscode/src/services/cli-backend/connection-service.ts index 3b5a9e0d950..768f6bb6c78 100644 --- a/packages/kilo-vscode/src/services/cli-backend/connection-service.ts +++ b/packages/kilo-vscode/src/services/cli-backend/connection-service.ts @@ -584,7 +584,8 @@ export class KiloConnectionService { this.sseClient = new SdkSSEAdapter(this.client) - // Wait until SSE actually reaches a terminal state before resolving connect(). + // Wait until SSE yields its first server event before resolving connect(). + // Initial stream failures are handled by the adapter reconnect loop. let resolveConnected: (() => void) | null = null let rejectConnected: ((error: Error) => void) | null = null const connectedPromise = new Promise((resolve, reject) => { @@ -601,11 +602,8 @@ export class KiloConnectionService { } }) - this.sseClient.onError((error) => { + this.sseClient.onError(() => { this.setState("error") - rejectConnected?.(error) - resolveConnected = null - rejectConnected = null }) // Wire SSE state → broadcast to all registered state listeners diff --git a/packages/kilo-vscode/src/services/cli-backend/connection-utils.ts b/packages/kilo-vscode/src/services/cli-backend/connection-utils.ts index 787311ed03b..1b0a1dcfa62 100644 --- a/packages/kilo-vscode/src/services/cli-backend/connection-utils.ts +++ b/packages/kilo-vscode/src/services/cli-backend/connection-utils.ts @@ -16,6 +16,7 @@ export function resolveEventSessionId( case "session.updated": return event.properties.info.id case "session.status": + case "session.turn.close": case "session.idle": case "session.error": case "todo.updated": diff --git a/packages/kilo-vscode/src/services/cli-backend/sdk-sse-adapter.ts b/packages/kilo-vscode/src/services/cli-backend/sdk-sse-adapter.ts index 299a4a9453c..f2ba427c83f 100644 --- a/packages/kilo-vscode/src/services/cli-backend/sdk-sse-adapter.ts +++ b/packages/kilo-vscode/src/services/cli-backend/sdk-sse-adapter.ts @@ -15,9 +15,8 @@ export type SSEStateHandler = (state: "connecting" | "connected" | "disconnected * * In this VS Code extension context the connection is localhost (extension ↔ * child-process server), so zombie-connection scenarios are less likely than in - * a browser client (which goes through proxies/CDNs). We keep the heartbeat for - * consistency with the original strategy but use a generous 90 s timeout to - * avoid false-positive reconnections during idle periods. + * a browser client (which goes through proxies/CDNs). We still keep a heartbeat + * grace window so dead local streams recover without waiting indefinitely. * * NOTE on event coalescing: * The app batches rapid events into 16 ms windows before flushing to the UI. @@ -37,6 +36,7 @@ export class SdkSSEAdapter { // Reduced from 90s: with 90s a dead connection could linger for ~1.5 minutes. private static readonly HEARTBEAT_TIMEOUT_MS = 15_000 private static readonly RECONNECT_DELAY_MS = 250 + private static readonly MAX_RECONNECT_DELAY_MS = 5_000 constructor(private readonly client: KiloClient) {} @@ -126,8 +126,11 @@ export class SdkSSEAdapter { * Main reconnection loop — mirrors the pattern in `global-sdk.tsx`. */ private async consumeLoop(signal: AbortSignal): Promise { + let delay = SdkSSEAdapter.RECONNECT_DELAY_MS + while (!signal.aborted) { const attempt = new AbortController() + let ready = false // Forward the outer abort to the per-attempt controller so // `disconnect()` cancels the current fetch immediately. @@ -159,8 +162,7 @@ export class SdkSSEAdapter { }, }) - console.log("[Kilo New] SSE: ✅ Stream opened successfully") - this.notifyState("connected") + console.log("[Kilo New] SSE: ⏳ Waiting for first stream event") this.resetHeartbeat(attempt) for await (const event of events.stream) { @@ -170,6 +172,13 @@ export class SdkSSEAdapter { this.resetHeartbeat(attempt) + if (!ready) { + ready = true + delay = SdkSSEAdapter.RECONNECT_DELAY_MS + console.log("[Kilo New] SSE: ✅ Stream opened successfully") + this.notifyState("connected") + } + // The SDK yields GlobalEvent = { directory, payload: Event }. const globalEvent = event as GlobalEvent const type = (globalEvent.payload as { type: string }).type @@ -179,7 +188,9 @@ export class SdkSSEAdapter { this.notifyEvent(globalEvent.payload as Event, globalEvent.directory) } - console.log("[Kilo New] SSE: 📭 Stream ended normally") + console.log( + ready ? "[Kilo New] SSE: 📭 Stream ended normally" : "[Kilo New] SSE: 📭 Stream ended before first event", + ) } catch (error) { // Suppress AbortErrors — they are expected when the heartbeat timer // or reconnect() aborts the per-attempt controller. @@ -198,9 +209,11 @@ export class SdkSSEAdapter { break } - console.log(`[Kilo New] SSE: 🔄 Reconnecting in ${SdkSSEAdapter.RECONNECT_DELAY_MS}ms...`) + const wait = delay + delay = ready ? SdkSSEAdapter.RECONNECT_DELAY_MS : Math.min(delay * 2, SdkSSEAdapter.MAX_RECONNECT_DELAY_MS) + console.log(`[Kilo New] SSE: 🔄 Reconnecting in ${wait}ms...`) this.notifyState("connecting") - await new Promise((resolve) => setTimeout(resolve, SdkSSEAdapter.RECONNECT_DELAY_MS)) + await new Promise((resolve) => setTimeout(resolve, wait)) } this.notifyState("disconnected") diff --git a/packages/kilo-vscode/src/services/cli-backend/server-manager.ts b/packages/kilo-vscode/src/services/cli-backend/server-manager.ts index 18c176c389a..40a132e8eaa 100644 --- a/packages/kilo-vscode/src/services/cli-backend/server-manager.ts +++ b/packages/kilo-vscode/src/services/cli-backend/server-manager.ts @@ -4,6 +4,7 @@ import * as crypto from "crypto" import * as fs from "fs" import * as path from "path" import * as vscode from "vscode" +import { resolveTreeSitterEnv } from "./cli-resources" import { t } from "./i18n" import { parseServerPort } from "./server-utils" @@ -131,6 +132,7 @@ export class ServerManager { KILO_VSCODE_VERSION: vscode.version, KILOCODE_EDITOR_NAME: `${vscode.env.appName} ${vscode.version}`, ...(!claudeCompat && { KILO_DISABLE_CLAUDE_CODE: "true" }), + ...resolveTreeSitterEnv(this.context.extensionPath), }, stdio: ["ignore", "pipe", "pipe"], detached: true, diff --git a/packages/kilo-vscode/src/services/input-tools.ts b/packages/kilo-vscode/src/services/input-tools.ts new file mode 100644 index 00000000000..7d722b684ed --- /dev/null +++ b/packages/kilo-vscode/src/services/input-tools.ts @@ -0,0 +1,43 @@ +import type { KiloConnectionService } from "./cli-backend/connection-service" +import { routeAutocompleteMessage } from "./autocomplete/settings" +import { handleSpeechToTextCancel, handleSpeechToTextStart, handleSpeechToTextStop } from "../speech-to-text/handler" + +type Msg = { + type: string + requestId?: string + model?: string + language?: string +} + +type Ctx = { + connection: KiloConnectionService + dir: string + post: (msg: unknown) => void +} + +export async function routeInputToolMessage(message: Msg, ctx: Ctx): Promise { + if (await routeAutocompleteMessage(message, ctx.post)) return true + + if (message.type === "speechToTextStart") { + if (!message.requestId) return true + handleSpeechToTextStart( + { requestId: message.requestId, model: message.model, language: message.language }, + ctx.post, + ) + return true + } + + if (message.type === "speechToTextStop") { + if (!message.requestId) return true + handleSpeechToTextStop(ctx.connection, { requestId: message.requestId }, ctx.dir, ctx.post) + return true + } + + if (message.type === "speechToTextCancel") { + if (!message.requestId) return true + handleSpeechToTextCancel({ requestId: message.requestId }, ctx.post) + return true + } + + return false +} diff --git a/packages/kilo-vscode/src/services/marketplace/api.ts b/packages/kilo-vscode/src/services/marketplace/api.ts index c687d9f5997..5c1bd7c3c4b 100644 --- a/packages/kilo-vscode/src/services/marketplace/api.ts +++ b/packages/kilo-vscode/src/services/marketplace/api.ts @@ -1,5 +1,5 @@ import { parse as parseYaml } from "yaml" -import type { MarketplaceItem, McpMarketplaceItem, ModeMarketplaceItem, SkillMarketplaceItem, RawSkill } from "./types" +import type { MarketplaceItem, McpMarketplaceItem, AgentMarketplaceItem, SkillMarketplaceItem, RawSkill } from "./types" const BASE_URL = "https://api.kilo.ai/api/marketplace" const CACHE_TTL = 300_000 @@ -76,18 +76,6 @@ export class MarketplaceApiClient { this.cache.set(key, { data, timestamp: Date.now() }) } - private async fetchModes(): Promise { - const cached = this.getCached("modes") - if (cached) return cached as ModeMarketplaceItem[] - - const text = await fetchWithRetry(`${BASE_URL}/modes`) - const parsed = parseResponse(text) as { items?: unknown[] } - const items = (parsed.items ?? []) as Array> - const result = items.map((item) => ({ ...item, type: "mode" as const }) as ModeMarketplaceItem) - this.setCache("modes", result) - return result - } - private async fetchMcps(): Promise { const cached = this.getCached("mcps") if (cached) return cached as McpMarketplaceItem[] @@ -100,6 +88,18 @@ export class MarketplaceApiClient { return result } + private async fetchAgents(): Promise { + const cached = this.getCached("agents") + if (cached) return cached as AgentMarketplaceItem[] + + const text = await fetchWithRetry(`${BASE_URL}/agents`) + const parsed = parseResponse(text) as { items?: unknown[] } + const items = (parsed.items ?? []) as Array> + const result = items.map((item) => ({ ...item, type: "agent" as const }) as AgentMarketplaceItem) + this.setCache("agents", result) + return result + } + private async fetchSkills(): Promise { const cached = this.getCached("skills") if (cached) return cached as SkillMarketplaceItem[] @@ -116,9 +116,9 @@ export class MarketplaceApiClient { const errors: string[] = [] const settled = await Promise.all([ - this.fetchModes().catch((err: unknown) => { - errors.push(`Failed to fetch modes: ${err instanceof Error ? err.message : String(err)}`) - return [] as ModeMarketplaceItem[] + this.fetchAgents().catch((err: unknown) => { + errors.push(`Failed to fetch agents: ${err instanceof Error ? err.message : String(err)}`) + return [] as AgentMarketplaceItem[] }), this.fetchMcps().catch((err: unknown) => { errors.push(`Failed to fetch mcps: ${err instanceof Error ? err.message : String(err)}`) diff --git a/packages/kilo-vscode/src/services/marketplace/detection.ts b/packages/kilo-vscode/src/services/marketplace/detection.ts index 9f203d3ceb1..fb2030beb7d 100644 --- a/packages/kilo-vscode/src/services/marketplace/detection.ts +++ b/packages/kilo-vscode/src/services/marketplace/detection.ts @@ -16,6 +16,7 @@ export class InstallationDetector { /** * Detect installed marketplace items. * + * Agents are detected from .kilo/agents/*.md files. * MCP servers and modes are detected from kilo.json config files. * Skills come from the CLI backend (via GET /skill), which is the * authoritative source — it scans all skill directories. @@ -23,12 +24,14 @@ export class InstallationDetector { async detect(workspace?: string, skills?: CliSkill[]): Promise { const project = workspace ? Object.fromEntries([ + ...(await this.detectAgentFiles("project", workspace)), ...(await this.detectFromConfig(this.paths.configPath("project", workspace))), ...this.skillEntries(skills, workspace, true), ]) : {} const global = Object.fromEntries([ + ...(await this.detectAgentFiles("global")), ...(await this.detectFromConfig(this.paths.configPath("global"))), ...this.skillEntries(skills, workspace, false), ]) @@ -52,6 +55,20 @@ export class InstallationDetector { .map((s) => [s.name, { type: "skill" }]) } + /** Scan .kilo/agents/*.md files to detect installed marketplace agents. */ + private async detectAgentFiles(scope: "project" | "global", workspace?: string): Promise { + const dir = this.paths.agentsDir(scope, workspace) + try { + const files = await fs.readdir(dir) + return files.filter((f) => f.endsWith(".md")).map((f) => [path.basename(f, ".md"), { type: "agent" }] as Entry) + } catch (err) { + if ((err as NodeJS.ErrnoException).code !== "ENOENT") { + console.warn(`Failed to detect agent files from ${dir}:`, err) + } + return [] + } + } + /** Read mcp and agent entries from a kilo.json config file. */ private async detectFromConfig(filepath: string): Promise { try { @@ -67,7 +84,7 @@ export class InstallationDetector { if (parsed?.agent && typeof parsed.agent === "object") { for (const key of Object.keys(parsed.agent)) { - entries.push([key, { type: "mode" }]) + entries.push([key, { type: "agent" }]) } } diff --git a/packages/kilo-vscode/src/services/marketplace/index.ts b/packages/kilo-vscode/src/services/marketplace/index.ts index cb2272e7168..148ec9e6d89 100644 --- a/packages/kilo-vscode/src/services/marketplace/index.ts +++ b/packages/kilo-vscode/src/services/marketplace/index.ts @@ -65,6 +65,7 @@ export class MarketplaceService { export type { MarketplaceItem, + AgentMarketplaceItem, InstallMarketplaceItemOptions, MarketplaceDataResponse, InstallResult, diff --git a/packages/kilo-vscode/src/services/marketplace/installer.ts b/packages/kilo-vscode/src/services/marketplace/installer.ts index eff254f1a06..6ec35af7620 100644 --- a/packages/kilo-vscode/src/services/marketplace/installer.ts +++ b/packages/kilo-vscode/src/services/marketplace/installer.ts @@ -7,7 +7,7 @@ import type { MarketplaceItem, SkillMarketplaceItem, McpMarketplaceItem, - ModeMarketplaceItem, + AgentMarketplaceItem, McpInstallationMethod, InstallMarketplaceItemOptions, InstallResult, @@ -26,7 +26,7 @@ export class MarketplaceInstaller { const scope = options.target ?? "project" if (item.type === "skill") return this.installSkill(item, scope, workspace) if (item.type === "mcp") return this.installMcp(item, options, scope, workspace) - return this.installMode(item, scope, workspace) + return this.installAgent(item, scope, workspace) } // ── MCP ───────────────────────────────────────────────────────────── @@ -81,10 +81,10 @@ export class MarketplaceInstaller { return normalizeMcpEntry(raw) } - // ── Mode ──────────────────────────────────────────────────────────── + // ── Agent ─────────────────────────────────────────────────────────── - async installMode( - item: ModeMarketplaceItem, + async installAgent( + item: AgentMarketplaceItem, scope: "project" | "global", workspace?: string, ): Promise { @@ -92,16 +92,72 @@ export class MarketplaceInstaller { return { success: false, slug: item.id, error: "No workspace directory for project-scope install" } } - const config = await this.readConfig(scope, workspace) - if (!config.agent) config.agent = {} - - if (config.agent[item.id]) { - return { success: false, slug: item.id, error: "Mode already installed. Remove it first." } + if (!isSafeId(item.id)) { + return { success: false, slug: item.id, error: "Invalid agent id" } } - config.agent[item.id] = convertModeToAgent(item.content) + const dir = this.paths.agentsDir(scope, workspace) + await fs.mkdir(dir, { recursive: true }) + + const filepath = path.join(dir, `${item.id}.md`) + if (!path.resolve(filepath).startsWith(path.resolve(dir))) { + return { success: false, slug: item.id, error: "Invalid agent id" } + } + + try { + await fs.access(filepath) + return { success: false, slug: item.id, error: "Agent already installed. Remove it first." } + } catch (err) { + if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err + } + + const { prompt, ...front } = item.content + const frontmatter = yaml.stringify(front).trimEnd() + const content = `---\n${frontmatter}\n---\n\n${prompt}\n` + await fs.writeFile(filepath, content, "utf-8") + + // Migration: remove stale kilo.json agent entry with same id if present + const config = await this.readConfig(scope, workspace) + if (config.agent?.[item.id]) { + delete (config.agent as Record)[item.id] + if (Object.keys(config.agent as object).length === 0) delete config.agent + await this.writeConfig(scope, workspace, config) + } + + return { success: true, slug: item.id, filePath: filepath, line: 1 } + } + + async removeAgent( + item: AgentMarketplaceItem, + scope: "project" | "global", + workspace?: string, + ): Promise { + if (!isSafeId(item.id)) { + return { success: false, slug: item.id, error: "Invalid agent id" } + } + + const dir = this.paths.agentsDir(scope, workspace) + const filepath = path.join(dir, `${item.id}.md`) + if (!path.resolve(filepath).startsWith(path.resolve(dir))) { + return { success: false, slug: item.id, error: "Invalid agent id" } + } + + try { + await fs.unlink(filepath) + } catch (err) { + if ((err as NodeJS.ErrnoException).code !== "ENOENT") { + return { success: false, slug: item.id, error: String(err) } + } + } + + // Also clean up any stale kilo.json agent entry + const config = await this.readConfig(scope, workspace) + if (config.agent?.[item.id]) { + delete (config.agent as Record)[item.id] + if (Object.keys(config.agent as object).length === 0) delete config.agent + await this.writeConfig(scope, workspace, config) + } - await this.writeConfig(scope, workspace, config) return { success: true, slug: item.id } } @@ -191,7 +247,7 @@ export class MarketplaceInstaller { async remove(item: MarketplaceItem, scope: "project" | "global", workspace?: string): Promise { if (item.type === "skill") return this.removeSkill(item, scope, workspace) if (item.type === "mcp") return this.removeMcp(item, scope, workspace) - return this.removeMode(item, scope, workspace) + return this.removeAgent(item, scope, workspace) } async removeMcp(item: McpMarketplaceItem, scope: "project" | "global", workspace?: string): Promise { @@ -205,17 +261,6 @@ export class MarketplaceInstaller { return { success: true, slug: item.id } } - async removeMode(item: ModeMarketplaceItem, scope: "project" | "global", workspace?: string): Promise { - const config = await this.readConfig(scope, workspace) - if (!config.agent?.[item.id]) { - return { success: true, slug: item.id } - } - delete config.agent[item.id] - if (Object.keys(config.agent).length === 0) delete config.agent - await this.writeConfig(scope, workspace, config) - return { success: true, slug: item.id } - } - async removeSkill( item: SkillMarketplaceItem, scope: "project" | "global", @@ -318,47 +363,6 @@ function isSafeId(id: string): boolean { return /^[\w\-@.]+$/.test(id) } -// Group name → opencode permission key mapping (mirrors ModesMigrator) -const GROUP_PERMISSIONS: Record = { - read: "read", - edit: "edit", - browser: "bash", - command: "bash", - mcp: "mcp", -} -const ALL_PERMISSIONS = ["read", "edit", "bash", "mcp"] - -function convertModeToAgent(content: string): Record { - const mode = yaml.parse(content) as Record - const groups = (mode.groups ?? []) as Array]> - - const permission: Record = {} - const allowed = new Set() - for (const group of groups) { - if (typeof group === "string") { - const key = GROUP_PERMISSIONS[group] ?? group - allowed.add(key) - permission[key] = "allow" - } else if (Array.isArray(group)) { - const [name, cfg] = group - const key = GROUP_PERMISSIONS[name] ?? name - allowed.add(key) - permission[key] = cfg?.fileRegex ? { [String(cfg.fileRegex)]: "allow", "*": "deny" } : "allow" - } - } - for (const perm of ALL_PERMISSIONS) { - if (!allowed.has(perm)) permission[perm] = "deny" - } - - const prompt = [mode.roleDefinition, mode.customInstructions].filter(Boolean).join("\n\n") - return { - mode: "primary", - description: mode.description ?? mode.whenToUse ?? mode.name, - prompt, - permission, - } -} - function escapeJsonValue(raw: string): string { return raw .replace(/\\/g, "\\\\") diff --git a/packages/kilo-vscode/src/services/marketplace/paths.ts b/packages/kilo-vscode/src/services/marketplace/paths.ts index 112c6cad23d..d7c8c19582b 100644 --- a/packages/kilo-vscode/src/services/marketplace/paths.ts +++ b/packages/kilo-vscode/src/services/marketplace/paths.ts @@ -17,6 +17,12 @@ export class MarketplacePaths { return path.join(globalConfigDir(), "kilo.json") } + /** Agent install directory (where marketplace agents are written as .md files). */ + agentsDir(scope: "project" | "global", workspace?: string): string { + if (scope === "project") return path.join(workspace!, ".kilo", "agents") + return path.join(globalConfigDir(), "agents") + } + /** Skill install directory (where the marketplace installer writes to). */ skillsDir(scope: "project" | "global", workspace?: string): string { if (scope === "project") return path.join(workspace!, ".kilo", "skills") diff --git a/packages/kilo-vscode/src/services/marketplace/types.ts b/packages/kilo-vscode/src/services/marketplace/types.ts index c35ad472419..bfb20a6bd18 100644 --- a/packages/kilo-vscode/src/services/marketplace/types.ts +++ b/packages/kilo-vscode/src/services/marketplace/types.ts @@ -29,9 +29,17 @@ export interface McpMarketplaceItem extends MarketplaceItemBase { parameters?: McpParameter[] } -export interface ModeMarketplaceItem extends MarketplaceItemBase { - type: "mode" - content: string +export interface AgentContent { + mode: "primary" | "subagent" | "all" + description: string + prompt: string + options?: Record + permission?: Record +} + +export interface AgentMarketplaceItem extends MarketplaceItemBase { + type: "agent" + content: AgentContent } export interface RawSkill { @@ -51,7 +59,7 @@ export interface SkillMarketplaceItem extends MarketplaceItemBase { displayCategory: string } -export type MarketplaceItem = McpMarketplaceItem | ModeMarketplaceItem | SkillMarketplaceItem +export type MarketplaceItem = McpMarketplaceItem | AgentMarketplaceItem | SkillMarketplaceItem export interface InstallMarketplaceItemOptions { target?: "global" | "project" diff --git a/packages/kilo-vscode/src/shared/autocomplete-models.ts b/packages/kilo-vscode/src/shared/autocomplete-models.ts index 203be8daea5..e67350a1313 100644 --- a/packages/kilo-vscode/src/shared/autocomplete-models.ts +++ b/packages/kilo-vscode/src/shared/autocomplete-models.ts @@ -1,44 +1,10 @@ -/** - * Single source of truth for autocomplete FIM model definitions. - * - * Shared between extension code (src/) and webview code (webview-ui/). - * When adding a new model, update ONLY this file and package.json's - * `kilo-code.new.autocomplete.model` enum. - */ - -export interface AutocompleteModelDef { - /** Full model ID sent to the gateway, e.g. "mistralai/codestral-2508" */ - readonly id: string - /** Human-readable label shown in the settings dropdown */ - readonly label: string - /** Provider display name for status bar / telemetry */ - readonly provider: string - /** FIM request temperature */ - readonly temperature: number -} - -const models: AutocompleteModelDef[] = [ - { - id: "mistralai/codestral-2508", - label: "Codestral (Mistral AI)", - provider: "Mistral AI", - temperature: 0.2, - }, - { - id: "inception/mercury-edit", - label: "Mercury Edit (Inception)", - provider: "Inception", - temperature: 0, - }, -] - -export const AUTOCOMPLETE_MODELS: readonly AutocompleteModelDef[] = models - -export const DEFAULT_AUTOCOMPLETE_MODEL: AutocompleteModelDef = models[0]! - -export function getAutocompleteModel(id: string): AutocompleteModelDef { - for (const m of models) { - if (m.id === id) return m - } - return DEFAULT_AUTOCOMPLETE_MODEL -} +export { + AUTOCOMPLETE_MODELS, + DEFAULT_AUTOCOMPLETE_MODEL, + getAutocompleteModel, + getAutocompleteModelById, + validAutocompleteModel, + validAutocompleteProvider, + type AutocompleteModelDef, + type AutocompleteProviderID, +} from "@kilocode/kilo-gateway/autocomplete" diff --git a/packages/kilo-vscode/src/shared/custom-provider.ts b/packages/kilo-vscode/src/shared/custom-provider.ts index f75cfcd7319..b8ed2dfa294 100644 --- a/packages/kilo-vscode/src/shared/custom-provider.ts +++ b/packages/kilo-vscode/src/shared/custom-provider.ts @@ -14,7 +14,7 @@ export const EnvSchema = z const VariantConfigSchema = z.object({ enable_thinking: z.boolean().optional(), thinking: z.object({ type: z.enum(["enabled", "disabled"]) }).optional(), - reasoningEffort: z.enum(["none", "minimal", "low", "medium", "high"]).optional(), + reasoningEffort: z.enum(["none", "minimal", "low", "medium", "high", "xhigh"]).optional(), chat_template_args: z.object({ enable_thinking: z.boolean() }).optional(), }) diff --git a/packages/kilo-vscode/src/shared/session-title.ts b/packages/kilo-vscode/src/shared/session-title.ts new file mode 100644 index 00000000000..a86e01360d7 --- /dev/null +++ b/packages/kilo-vscode/src/shared/session-title.ts @@ -0,0 +1,16 @@ +export const SESSION_TITLE_LIMIT = 200 + +// Block terminal/display controls and bidi marks that can visually spoof a title. +const unsafe = /[\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069]/u + +type SessionTitleIssue = "invalid" | "required" | "too_long" | "control" +type SessionTitleResult = { value: string } | { error: SessionTitleIssue } + +export function parseSessionTitle(raw: unknown): SessionTitleResult { + if (typeof raw !== "string") return { error: "invalid" } + const value = raw.trim() + if (!value) return { error: "required" } + if (value.length > SESSION_TITLE_LIMIT) return { error: "too_long" } + if (unsafe.test(value)) return { error: "control" } + return { value } +} diff --git a/packages/kilo-vscode/src/speech-to-text/capture.ts b/packages/kilo-vscode/src/speech-to-text/capture.ts new file mode 100644 index 00000000000..92f127dc2c9 --- /dev/null +++ b/packages/kilo-vscode/src/speech-to-text/capture.ts @@ -0,0 +1,345 @@ +import { existsSync } from "fs" +import { readFile, stat, unlink } from "fs/promises" +import * as os from "os" +import * as path from "path" +import type { ChildProcess } from "child_process" +import { exec, spawn } from "../util/process" + +type Input = { + requestId: string + model: string + language?: string +} + +type Recording = Input & { + file: string + proc: ChildProcess + stderr: string[] + stopped: boolean + exit?: { + code: number | null + signal: string | null + } +} + +type Audio = { + data: string + format: "wav" + model: string + language?: string +} + +type Args = { + pipe?: string[] + input: string[] +} + +let active: Recording | undefined +let starting: string | undefined + +export async function startSpeechCapture(input: Input): Promise { + if (active || starting) throw new Error("Speech recording is already in progress") + + starting = input.requestId + try { + const bin = await findFFmpeg() + const file = path.join(os.tmpdir(), `kilo-stt-${process.pid}-${Date.now()}.wav`) + const state = await startWithArgs(bin, file, input, await inputArgSets(bin)) + return !state.stopped + } finally { + if (starting === input.requestId) starting = undefined + } +} + +export async function stopSpeechCapture(requestId: string): Promise