diff --git a/.changeset/agent-manager-sidebar-density.md b/.changeset/agent-manager-sidebar-density.md new file mode 100644 index 00000000000..08eb1cf9c18 --- /dev/null +++ b/.changeset/agent-manager-sidebar-density.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Fix the Agent Manager sidebar keyboard shortcut badge so it appears on the right edge of local rows and only while hovered or holding the jump modifier, give worktree titles more room by no longer reserving space for hidden row actions, align project names and section headings with the row icons below them, and show which worktree a session belongs to in the search palette. diff --git a/.changeset/agent-manager-terminal-focus.md b/.changeset/agent-manager-terminal-focus.md new file mode 100644 index 00000000000..d88b418a295 --- /dev/null +++ b/.changeset/agent-manager-terminal-focus.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Improve Agent Manager terminal focus and keyboard navigation. `Cmd+/` now focuses a visible embedded terminal before hiding it, `Cmd+Shift+T` creates a side terminal only while that terminal area has focus, and `Cmd+Shift+[` / `]` switch terminal tabs. `Cmd+Shift+M` focuses the Agent Manager prompt instead of opening VS Code Problems. `Cmd+W` hides the last side terminal instead of stopping its shell. diff --git a/.changeset/align-agent-manager-project-actions.md b/.changeset/align-agent-manager-project-actions.md new file mode 100644 index 00000000000..b2d36e56cea --- /dev/null +++ b/.changeset/align-agent-manager-project-actions.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Align multi-project Agent Manager header actions with the worktree controls. diff --git a/.changeset/bright-model-search.md b/.changeset/bright-model-search.md new file mode 100644 index 00000000000..6bdf91a54df --- /dev/null +++ b/.changeset/bright-model-search.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Improve model search relevance with provider-aware results and personalized usage suggestions. diff --git a/.changeset/bright-project-picker.md b/.changeset/bright-project-picker.md new file mode 100644 index 00000000000..18458aaa856 --- /dev/null +++ b/.changeset/bright-project-picker.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Let Agent Manager users choose the repository when creating or importing a worktree in multi-project mode. diff --git a/.changeset/cache-worktree-dialog-selections.md b/.changeset/cache-worktree-dialog-selections.md new file mode 100644 index 00000000000..f13128f7b9c --- /dev/null +++ b/.changeset/cache-worktree-dialog-selections.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Remember Agent Manager worktree dialog model, variant, mode, and sandbox selections when reopened. diff --git a/.changeset/calm-agent-manager-git-polling.md b/.changeset/calm-agent-manager-git-polling.md new file mode 100644 index 00000000000..5f3314267ab --- /dev/null +++ b/.changeset/calm-agent-manager-git-polling.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Reduce Agent Manager background Git polling load across large worktree sets. diff --git a/.changeset/calm-skill-examples.md b/.changeset/calm-skill-examples.md new file mode 100644 index 00000000000..a1ac64ae30e --- /dev/null +++ b/.changeset/calm-skill-examples.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Prevent built-in skill documentation examples from triggering shell permission prompts. diff --git a/.changeset/command-model-selector-sync.md b/.changeset/command-model-selector-sync.md new file mode 100644 index 00000000000..cd64ab6f90e --- /dev/null +++ b/.changeset/command-model-selector-sync.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Update the active model, mode, and thinking selectors when executing a slash command with configured overrides. diff --git a/.changeset/commands-settings-jetbrains.md b/.changeset/commands-settings-jetbrains.md new file mode 100644 index 00000000000..0c451ea9f32 --- /dev/null +++ b/.changeset/commands-settings-jetbrains.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": minor +--- + +Add kilocode command-file endpoints so clients can list editable command/workflow files, inspect model and reasoning variant metadata, and remove them. diff --git a/.changeset/compressed-speech-to-text-audio.md b/.changeset/compressed-speech-to-text-audio.md new file mode 100644 index 00000000000..fee2a9166d1 --- /dev/null +++ b/.changeset/compressed-speech-to-text-audio.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Compress speech-to-text audio input to AAC format across macOS, Linux, and Windows to prevent payload size errors on long recordings. diff --git a/.changeset/faster-agent-manager-worktrees.md b/.changeset/faster-agent-manager-worktrees.md new file mode 100644 index 00000000000..259a5413f73 --- /dev/null +++ b/.changeset/faster-agent-manager-worktrees.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Start Agent Manager worktree sessions faster by prefetching base branches, reducing workspace file-watcher load, and overlapping independent multi-session setup. diff --git a/.changeset/fix-agent-manager-terminal-toggle.md b/.changeset/fix-agent-manager-terminal-toggle.md new file mode 100644 index 00000000000..a93825d5957 --- /dev/null +++ b/.changeset/fix-agent-manager-terminal-toggle.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Fix the Agent Manager terminal toolbar button to toggle panel visibility directly. diff --git a/.changeset/fix-gh-windows-console.md b/.changeset/fix-gh-windows-console.md new file mode 100644 index 00000000000..f9fc3a1e5cc --- /dev/null +++ b/.changeset/fix-gh-windows-console.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Prevent extension-managed GitHub CLI commands from opening transient Windows Terminal windows. diff --git a/.changeset/fix-interactive-terminal-input.md b/.changeset/fix-interactive-terminal-input.md new file mode 100644 index 00000000000..b9d53915489 --- /dev/null +++ b/.changeset/fix-interactive-terminal-input.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Restore keyboard input for interactive terminal prompts when the CLI session uses a workspace. diff --git a/.changeset/fix-multi-project-agent-manager-tool.md b/.changeset/fix-multi-project-agent-manager-tool.md new file mode 100644 index 00000000000..bb4e6304f63 --- /dev/null +++ b/.changeset/fix-multi-project-agent-manager-tool.md @@ -0,0 +1,6 @@ +--- +"kilo-code": patch +"@kilocode/cli": patch +--- + +Route Agent Manager tool-launched sessions to the project that owns the tool event directory, keep sandboxed worktree sessions inside their active worktree, and wait for busy managed sessions before prompting them. diff --git a/.changeset/fix-multi-project-session-scope.md b/.changeset/fix-multi-project-session-scope.md index d56c4c93018..0f32e3327ac 100644 --- a/.changeset/fix-multi-project-session-scope.md +++ b/.changeset/fix-multi-project-session-scope.md @@ -2,4 +2,4 @@ "kilo-code": patch --- -Scope Agent Manager session events and Git status to the active project, including edits inside nested repositories. +Speed up local VS Code snapshot installs and scope Agent Manager session events and Git status to the active project, including edits inside nested repositories. diff --git a/.changeset/fix-secondary-sidebar-nav-bar.md b/.changeset/fix-secondary-sidebar-nav-bar.md new file mode 100644 index 00000000000..7905945a1ee --- /dev/null +++ b/.changeset/fix-secondary-sidebar-nav-bar.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Fix the sidebar navigation bar (New Task, History, Agent Manager, KiloClaw, Marketplace, Profile, Settings) disappearing in Cursor when the Kilo Code view is docked in the Secondary Side Bar. Cursor now renders the navigation inside the webview itself so it stays visible regardless of dock location. VS Code is unaffected — it continues to use its native title bar toolbar, which already worked correctly everywhere. diff --git a/.changeset/fix-session-scroll-flicker.md b/.changeset/fix-session-scroll-flicker.md new file mode 100644 index 00000000000..7fe8232c1e5 --- /dev/null +++ b/.changeset/fix-session-scroll-flicker.md @@ -0,0 +1,6 @@ +--- +"@kilocode/kilo-ui": patch +"kilo-code": patch +--- + +Fix flickering and sticky scrolling when scrolling up in Agent Manager and chat sessions. diff --git a/.changeset/fix-tool-approval-source-display.md b/.changeset/fix-tool-approval-source-display.md new file mode 100644 index 00000000000..3308ed0827d --- /dev/null +++ b/.changeset/fix-tool-approval-source-display.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Move the "why was this tool call approved" line to after the tool output instead of between the header and body, add an icon to it, and add a Display setting to hide it. diff --git a/.changeset/forked-subagent-resume.md b/.changeset/forked-subagent-resume.md new file mode 100644 index 00000000000..2a4f8cd4837 --- /dev/null +++ b/.changeset/forked-subagent-resume.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Allow subagent tasks to be resumed after their parent session is forked. diff --git a/.changeset/friendly-llamas-manage.md b/.changeset/friendly-llamas-manage.md new file mode 100644 index 00000000000..494e5cc0658 --- /dev/null +++ b/.changeset/friendly-llamas-manage.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Support the Agent Manager tool with llama.cpp servers that reject prefix-only JSON Schema patterns. diff --git a/.changeset/green-greps-settle.md b/.changeset/green-greps-settle.md new file mode 100644 index 00000000000..9f7cacabf79 --- /dev/null +++ b/.changeset/green-greps-settle.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Add bounded, context-aware grep controls without leaving agents waiting on completed searches. diff --git a/.changeset/jetbrains-revert-diff-card.md b/.changeset/jetbrains-revert-diff-card.md new file mode 100644 index 00000000000..fe8f75e3d3b --- /dev/null +++ b/.changeset/jetbrains-revert-diff-card.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Improve JetBrains session transcript layout, icons, reverted-change summaries, and multi-hunk diff rendering. diff --git a/.changeset/jetbrains-slash-completion.md b/.changeset/jetbrains-slash-completion.md new file mode 100644 index 00000000000..e2ec3c49969 --- /dev/null +++ b/.changeset/jetbrains-slash-completion.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Improve slash command completion to match separators, camel-case humps, and contained command names. diff --git a/.changeset/narrow-agent-manager-terminal.md b/.changeset/narrow-agent-manager-terminal.md new file mode 100644 index 00000000000..5091714bdb5 --- /dev/null +++ b/.changeset/narrow-agent-manager-terminal.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Keep Agent Manager terminals aligned and correctly wrapped when the terminal panel is narrow. diff --git a/.changeset/narrow-sidebar-recent.md b/.changeset/narrow-sidebar-recent.md new file mode 100644 index 00000000000..fcd56b0f73f --- /dev/null +++ b/.changeset/narrow-sidebar-recent.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Keep recent sessions and the Show History action inset and usable in narrow VS Code sidebars. diff --git a/.changeset/optimize-review-slash-commands.md b/.changeset/optimize-review-slash-commands.md new file mode 100644 index 00000000000..8e104765c23 --- /dev/null +++ b/.changeset/optimize-review-slash-commands.md @@ -0,0 +1,6 @@ +--- +"kilo-code": minor +"@kilocode/cli": minor +--- + +Add nested slash command suggestions for `/review` in VS Code and support `staged`, `unpushed`, and `quick` review modes. diff --git a/.changeset/preserve-model-variants.md b/.changeset/preserve-model-variants.md new file mode 100644 index 00000000000..10941827bb8 --- /dev/null +++ b/.changeset/preserve-model-variants.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Preserve the selected reasoning effort when switching to a model that supports the same or nearest available variant. diff --git a/.changeset/quick-git-launches.md b/.changeset/quick-git-launches.md new file mode 100644 index 00000000000..6c29101ce1f --- /dev/null +++ b/.changeset/quick-git-launches.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Reduce Agent Manager Git polling overhead by reusing the validated Git executable and bypassing the macOS developer-tool launcher when safe. diff --git a/.changeset/quiet-embedded-logo.md b/.changeset/quiet-embedded-logo.md new file mode 100644 index 00000000000..7623cf0e739 --- /dev/null +++ b/.changeset/quiet-embedded-logo.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Use a compatible Kilo wordmark in embedded Agent Manager terminals. diff --git a/.changeset/quiet-tui-config-reloads.md b/.changeset/quiet-tui-config-reloads.md new file mode 100644 index 00000000000..8c21c818bb9 --- /dev/null +++ b/.changeset/quiet-tui-config-reloads.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Prevent TUI config reload logs from corrupting the interactive terminal. diff --git a/.changeset/quiet-tui-disposal-aborts.md b/.changeset/quiet-tui-disposal-aborts.md new file mode 100644 index 00000000000..7431897d318 --- /dev/null +++ b/.changeset/quiet-tui-disposal-aborts.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Avoid printing an error when closing the TUI cancels in-flight startup refreshes. diff --git a/.changeset/remove-web-command.md b/.changeset/remove-web-command.md new file mode 100644 index 00000000000..8dd97b02feb --- /dev/null +++ b/.changeset/remove-web-command.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Remove unsupported `kilo web` CLI command. diff --git a/.changeset/safe-credential-reconciliation.md b/.changeset/safe-credential-reconciliation.md new file mode 100644 index 00000000000..abb8dd5f36a --- /dev/null +++ b/.changeset/safe-credential-reconciliation.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Prevent concurrent Kilo startups from rewriting unchanged credentials, retry transient database locks, and redact bound values from database errors. diff --git a/.changeset/sandbox-live-settings.md b/.changeset/sandbox-live-settings.md new file mode 100644 index 00000000000..f356683b997 --- /dev/null +++ b/.changeset/sandbox-live-settings.md @@ -0,0 +1,6 @@ +--- +"kilo-code": patch +"@kilocode/cli": patch +--- + +Apply saved sandbox settings to existing sessions and use the latest settings when enabling sandboxing diff --git a/.changeset/surface-backend-exit-diagnostics.md b/.changeset/surface-backend-exit-diagnostics.md new file mode 100644 index 00000000000..146a00c7424 --- /dev/null +++ b/.changeset/surface-backend-exit-diagnostics.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Surface process exit signals and structured spawn failure details in server startup diagnostics diff --git a/.changeset/sync-inspector-width.md b/.changeset/sync-inspector-width.md index 02eeb4e7239..9372f7ace4b 100644 --- a/.changeset/sync-inspector-width.md +++ b/.changeset/sync-inspector-width.md @@ -2,4 +2,4 @@ "kilo-code": patch --- -Persist the Agent Manager inspector width and share it between the terminal and diff viewer. +Persist the Agent Manager inspector width, share it between the terminal and diff viewer, and keep resizing responsive. diff --git a/.changeset/terminal-shortcut-translations.md b/.changeset/terminal-shortcut-translations.md new file mode 100644 index 00000000000..c0b45c5c4cc --- /dev/null +++ b/.changeset/terminal-shortcut-translations.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Translate the Agent Manager terminal focus shortcut label in all supported locales. diff --git a/.changeset/vscode-skills-tooltip-wrap.md b/.changeset/vscode-skills-tooltip-wrap.md new file mode 100644 index 00000000000..a4a5ecec038 --- /dev/null +++ b/.changeset/vscode-skills-tooltip-wrap.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Long skill folder paths and URLs shown in the tooltip on the Skills settings page now wrap inside the viewport instead of overflowing on a single line. diff --git a/.github/workflows/check-opencode-annotations.yml b/.github/workflows/check-opencode-annotations.yml index f494bbb4c8e..51d40efb260 100644 --- a/.github/workflows/check-opencode-annotations.yml +++ b/.github/workflows/check-opencode-annotations.yml @@ -41,6 +41,9 @@ jobs: - name: Check Effect Promise facade allowlist run: bun run script/check-opencode-promise-facades.ts + - name: Check domain architecture boundaries and ratchets + run: bun run script/check-architecture.ts + - name: Check model tool network boundary run: bun run script/check-model-tool-network.ts diff --git a/.kilo/plans/agent-manager-multi-project-implementation-handoff.md b/.kilo/plans/agent-manager-multi-project-implementation-handoff.md deleted file mode 100644 index 510a5e1c380..00000000000 --- a/.kilo/plans/agent-manager-multi-project-implementation-handoff.md +++ /dev/null @@ -1,459 +0,0 @@ -# Agent Manager multi-project implementation handoff - -**Status:** Implementation-ready handoff from the current branch - -**Date:** 2026-07-22 - -This document tells the next implementation model exactly how to continue from branch `abalone-bactrosaurus` at commit `8f48da2278` plus the uncommitted plan files. It is the execution checklist. Detailed decisions live in: - -- [`agent-manager-multi-project-configuration.md`](./agent-manager-multi-project-configuration.md) for configuration ownership, bindings, revisions, indexing consent, and settings tests; -- [`agent-manager-multi-project-uniform-ui.md`](./agent-manager-multi-project-uniform-ui.md) for runtime, routing, lifecycle, uniform sidebar UI, and acceptance criteria. - -If this handoff conflicts with either architecture document, stop and resolve the contradiction before coding. - -## Goal - -Finish the current prototype into a safe, complete multi-project Agent Manager behind a default-off VS Code experimental flag. - -The feature is done only when: - -- multiple expanded projects show the same full Agent Manager sidebar behavior; -- every action uses explicit project/worktree/session ownership; -- Settings cannot write the wrong project's config; -- indexing requires machine-local consent per canonical project; -- project identity is canonical, Git-root-aware, common-dir-safe, and remote-authority-aware; -- single-project behavior remains unchanged when the flag is off. - -## Current branch baseline - -### Implemented and worth keeping - -- persistent additional-project registry with serialized/re-read mutations; -- separate `ProjectContext` objects and per-project Agent Manager state files; -- project-stamped state, stats, PR, and session payloads; -- canonical worktree presence comparison for symlink/`/tmp` aliases; -- per-project session listing from project root and worktree directories; -- partial production `ProjectRouteService` integration and raw-session ambiguity rejection; -- project-qualified sidebar DOM IDs and cross-project previous/next navigation; -- atomic selection validation and same-project activation fast path; -- flag-off return to pinned project; -- Experimental Settings toggle and translations; -- focused unit tests for registry, contexts, paths, pollers, routing, sessions, selection, and navigation. - -### Not implemented or incomplete - -- immutable/revisioned Settings bindings; -- machine-local indexing consent; -- canonical pinned Git toplevel, `commonGitDir`, URI scheme/authority identity; -- strict project envelopes for all Agent Manager operations; -- complete route-service use for every session/terminal/diff/share/permission operation; -- one full sidebar body shared by single- and multi-project modes; -- uniform per-context poller and stale-state ownership; -- all repository mutations through `ProjectContext.run()`; -- complete real two-project E2E verification. - -### Known diff cleanup - -- remove unrelated `packages/opencode/package.json` ordering/dependency drift; -- restore unrelated `bun.lock` ordering/version drift unless required by a retained change; -- remove `experimental.multi_project` from the CLI config schema and generated SDK once the VS Code flag is the sole source of truth; -- rewrite the changeset after final behavior is complete. - -Do not reset or discard unrelated user changes. Inspect every cleanup diff before applying it. - -## Non-negotiable invariants - -1. Agent Manager activation is not a Settings write target. -2. No identified operation falls back to the active project or workspace root in multi-project mode. -3. A project config write targets the explicitly selected registered project root, never an active worktree. -4. Runtime config remains directory-correct: project Local uses root, worktree sessions use exact worktree directory. -5. Repository config cannot grant indexing consent. -6. New projects default to indexing disabled on this machine. -7. Provider/API credentials and indexing infrastructure never enter project config through Settings. -8. Raw session/worktree IDs are never sufficient UI/runtime identity when multiple projects are exposed. -9. The full existing sidebar behavior is reused; do not maintain a simplified multi-project copy. -10. Do not raise `AgentManagerProvider.ts` or `AgentManagerApp.tsx` line caps. - -## Work order - -Complete slices in order. Do not start the next slice while the current slice's tests or checks fail. - -## Slice 0A: consolidate the feature flag - -### Required result - -Use one source of truth: VS Code application setting `kilo-code.new.experimental.multiProject`. - -### Required changes - -1. Remove `experimental.multi_project` from: - - `packages/core/src/v1/config/config.ts`; - - `packages/sdk/js/src/v2/gen/types.gen.ts` by regenerating the SDK after schema removal; - - `packages/kilo-vscode/webview-ui/src/types/messages/config.ts`. -2. Do not hand-edit generated SDK files. Run `./script/generate.ts` from repository root after endpoint/schema changes. -3. Change `ExperimentalTab.tsx` so the toggle reads the VS Code setting delivered in `configLoaded.settings`, not `config().experimental`. -4. Add `multiProject` to the extension settings payload sent by `KiloProvider.fetchAndSendConfig()`. -5. Toggling it sends only `updateSetting("experimental.multiProject", checked)`. -6. Keep `VscodeHost.multiProject()` and its change listener reading the same VS Code setting. - -### Tests - -- toggle initial state reflects VS Code setting when CLI config disagrees; -- toggling updates the VS Code setting and Agent Manager reacts; -- no CLI/project config file gains `experimental.multi_project`. - -### Stop gate - -Run from `packages/kilo-vscode/`: - -```sh -bun run format -bun run format:check -bun run typecheck -bun run lint -bun run test:unit -``` - -Do not continue if any command fails. - -## Slice 0B: machine-local indexing consent - -### Required result - -Indexing enablement is explicit machine-local consent keyed by canonical `ProjectId`, default false. Project config controls indexing rules but cannot enable indexing. - -### Required changes - -1. Add a versioned machine-local consent store in the extension, not repository config and not synced across machines. -2. Use canonical project identity as the key. The pinned project and registered projects use the same identity resolver. -3. Remove hidden routing of `indexing.enabled` to project config from `webview-ui/src/utils/config-scope.ts`. -4. Remove project/global inheritance logic for `indexing.enabled` from `indexing-tab-state.ts`. -5. The Indexing UI requires an explicit project selector for enablement and reads/writes consent through dedicated messages. -6. Keep these in User config only: - - provider; - - model and dimension; - - credentials/API keys/base URLs; - - vector-store type and connection/storage settings; - - machine/infrastructure tuning defaults. -7. Keep these available in explicit Project scope: - - file extensions; - - repository include/ignore rules; - - deliberate repository chunking/tuning overrides. -8. Indexing startup/status must require both valid User indexing configuration and consent for the routed project. -9. A repository containing `indexing.enabled: true` must not enable indexing. - -### Suggested files - -- new VS Code-free consent store under `src/indexing/` or `src/agent-manager/`; -- `src/KiloProvider.ts` only as a thin protocol adapter; -- `webview-ui/src/components/settings/IndexingTab.tsx`; -- `webview-ui/src/components/settings/indexing-tab-state.ts`; -- `webview-ui/src/context/config.tsx` only if necessary; -- focused tests beside existing indexing tests. - -### Tests - -- unknown project defaults off; -- A enabled does not enable B; -- symlink/path alias resolves to the same consent; -- repository config cannot grant consent; -- removing/re-adding behavior follows the documented store policy; -- no credentials/storage settings are emitted in a Project-scope patch. - -## Slice 0C: revisioned config overlay backend - -### Required result - -Config reads return authoritative target descriptors and revisions. Writes use compare-and-swap and cannot overwrite external changes or a changed target. - -### Required changes - -1. Extend Kilo-owned config overlay code: - - `packages/opencode/src/kilocode/config/overlay.ts`; - - a new Kilo-owned writer under `packages/opencode/src/kilocode/config/`; - - `packages/opencode/src/kilocode/server/httpapi/groups/config-console.ts`; - - `packages/opencode/src/kilocode/server/httpapi/handlers/config-console.ts`. -2. Read response includes for global and project targets: - - canonical path; - - scope; - - exists/writable; - - SHA-256 revision of canonical path, existence marker, and exact bytes; - - raw parsed target layer; - - effective config and source metadata. -3. Write request accepts exactly one scope, `set`, `unset`, and expected path/revision. -4. Server re-resolves the authoritative target. Never trust client path as a destination. -5. Lock by canonical target path, re-read inside lock, compare revision, patch raw JSONC, validate, and atomically replace. -6. Return a fresh authoritative snapshot. -7. Return typed 409 conflicts for target/revision changes; preserve drafts client-side. -8. Keep Kilo logic in `packages/opencode/src/kilocode/`. Shared upstream files get minimal marked delegation only when unavoidable. -9. Regenerate the SDK after endpoint changes. - -### Tests - -Add/extend: - -- `packages/opencode/test/kilocode/server/config-overlay.test.ts`; -- `packages/opencode/test/kilocode/project-config-update.test.ts`. - -Cover: - -- exact raw target layer; -- comment-only external edit conflict; -- missing-file target revision; -- newly created higher-priority target conflict; -- concurrent writers: one success, one 409; -- symlink escape rejection; -- global and project writes remain separate; -- atomic write failure never exposes partial content. - -### Stop gate - -From `packages/opencode/`: - -```sh -bun run typecheck -bun test test/kilocode/server/config-overlay.test.ts -bun test test/kilocode/project-config-update.test.ts -``` - -From repository root: - -```sh -./script/generate.ts -bun run script/check-opencode-annotations.ts -bun run script/check-opencode-promise-facades.ts -``` - -## Slice 0D: immutable Settings bindings - -### Required result - -Settings has explicit `User | Project` scope. Project scope requires an explicit trusted project selector. Drafts and saves never follow Agent Manager activation. - -### Required changes - -1. Split runtime-effective config from Settings editor config. -2. Add a binding controller under `packages/kilo-vscode/src/kilo-provider/`; keep `KiloProvider.ts` as a thin adapter. -3. Replace unqualified config protocol with: - - read request carrying request ID, scope, and optional project ID; - - snapshot carrying opaque binding ID and source/target metadata; - - write request carrying request ID, binding ID, set, and unset. -4. The extension stores authoritative bindings. The webview never supplies a writable path. -5. Project binding resolution: - - resolve registered `ProjectContext.root`; - - require project existence, matching generation, and trust; - - never use active worktree/session or `getWorkspaceDirectory()` after binding creation. -6. Bindings expire on successful save, reconnect, project removal, generation change, or trust revocation. -7. Separate drafts by scope/project binding. -8. Dirty selector changes prompt Save, Discard, or Stay. -9. Out-of-order reads/writes update only matching request/binding. -10. External config events refresh clean drafts and mark dirty drafts stale without overwriting them. -11. Remove `splitConfigByScope()` after all controls declare scope explicitly. -12. Audit direct config mutators outside the save bar, including provider disconnect, imports/resets, custom providers, work styles, Permission Dock, MCP actions, and indexing. -13. Open Project Config takes explicit `ProjectRef`, checks trust, and resolves the registered root. - -### UI scope policy - -Use the table in `agent-manager-multi-project-configuration.md`. Do not invent another policy. - -### Blocking tests - -- load A, activate B, save A: only A target changes; -- same while selecting worktrees/sessions; -- User save changes only user config; -- Project save changes only selected trusted project's root config; -- dirty draft cannot migrate to another project; -- project removal/trust revocation expires binding; -- external edit returns conflict and preserves draft; -- no save-time active-directory lookup occurs. - -## Slice 1: canonical project identity - -### Required result - -Every project is a canonical Git repository root with URI authority and common Git directory identity. - -### Required changes - -1. Replace path-only project descriptors with: - -```ts -{ - id, - uri, - scheme, - authority, - root, - commonGitDir, - label, - order, - trusted, - addedAt -} -``` - -2. Pinned and added projects use the same resolver: - - validate URI host scope; - - `git rev-parse --show-toplevel`; - - `git rev-parse --path-format=absolute --git-common-dir`; - - realpath/normalize both; - - derive ID from scheme, authority, and canonical root. -3. Opening VS Code in a repository subdirectory still uses the Git toplevel and root state file. -4. Reject another exposed project sharing canonical `commonGitDir`. -5. Key Git mutation locks by common Git directory. -6. Preserve remote URI scheme/authority in picker, storage, and open-folder operations. -7. Migrate or safely read the current registry version without dropping valid entries. - -### Tests - -- workspace subdirectory resolves to repository root; -- symlink aliases dedupe; -- linked worktree/common-dir duplicate rejected; -- other remote authority hidden but preserved; -- missing/non-Git entry remains removable and never falls back to pinned root. - -## Slice 2: strict routing and lifecycle - -### Required result - -Every repository-bound operation is explicitly project-qualified once multiple projects are exposed. - -### Required changes - -1. Introduce one project envelope at the Agent Manager boundary with project ID and generation. -2. Compatibility adapter injects pinned identity only in single-project mode. -3. Multi-project mode rejects missing/mismatched identity with typed errors. -4. Wire `ProjectRouteService` into every existing-session operation: - - transcript/messages; - - prompt/abort; - - share/unshare; - - fork/continuation; - - permission/question responses; - - terminal; - - file/context operations; - - diff/apply/revert. -5. Route global SSE events once by exact directory/session ownership. -6. Preserve non-Agent-Manager KiloProvider behavior. -7. Execute repository mutations through `ctx.run()` and generation-check commits. -8. Removal waits for mutation queue, stops pollers/watchers, flushes state, detaches routes, then removes registry entry. -9. Flag-off activates pinned Local and suspends secondaries without aborting sessions. - -### Tests - -- same raw session ID across projects is ambiguous without qualifier; -- B Local/share/unshare/permission never routes through A; -- unknown IDs produce no terminal/file/diff operation; -- project switch during mutation cannot change operation ownership; -- late completion after removal cannot mutate replacement context. - -## Slice 3: uniform context-owned state and polling - -### Required result - -Every initialized expanded project owns the same live-state services. Active selection changes cadence/detail only, not ownership. - -### Required changes - -1. Move stats and PR pollers into `ProjectContext`; remove active singleton/background split. -2. Each context owns stale/presence state, cached stats, local stats, PR state, run/setup state, and generation. -3. Background presence updates project stale state rather than emitting empty arrays. -4. All project outputs include project ID and generation; webview discards stale generations. -5. Session-created/deleted/updated events refresh the owning project store without active-project filtering. -6. Panel visibility and expansion uniformly control polling. - -### Tests - -- two expanded projects both receive stats/presence/PR updates; -- collapse stops only that project's pollers; -- stale late poll result is dropped; -- background session deletion updates only its project. - -## Slice 4: one full sidebar body - -### Required result - -Single- and multi-project modes render the same full `ProjectSidebarBody` implementation. - -### Required changes - -1. Extract the current full `renderBody()` from `AgentManagerApp.tsx` intact. -2. Parameterize it with one project store and project-qualified actions. -3. Preserve: - - busy state; - - drag/drop and grouping; - - section actions; - - search and keyboard hints; - - run/setup state; - - New Worktree dialog; - - worktree menus and PR actions; - - complete managed/unassigned session behavior. -4. Delete the simplified duplicate body path. -5. Render project rows keyed by stable project ID and keep bodies mounted across active changes. -6. New Worktree receives explicit target project, defaulting to Settings/detail selection policy as specified. - -### Tests - -- single-project behavior unchanged with flag off; -- two expanded projects render two full bodies; -- active selection changes no body mount identity; -- drag/drop, sections, worktree actions, sessions, and keyboard navigation work across projects. - -## Slice 5: cleanup and release validation - -1. Remove unrelated manifest/lockfile drift. -2. Update changeset to final user-visible behavior. -3. Run all extension and affected CLI checks. -4. Build/package the extension. -5. Use the VS Code self-test fixture with two repositories and multiple worktrees/sessions. -6. Keep feature default off until every blocking test passes. - -### Required extension checks - -From `packages/kilo-vscode/`: - -```sh -bun run format -bun run format:check -bun run typecheck -bun run lint -bun run test:unit -bun run knip -bun run check-kilocode-change -bun run compile -``` - -From repository root: - -```sh -bun run script/check-opencode-annotations.ts -bun run script/check-opencode-promise-facades.ts -bun run script/check-md-table-padding.ts -``` - -### Manual scenarios - -1. Flag off: current single-project Agent Manager is unchanged. -2. Enable flag in Experimental Settings; restart; value persists without CLI/project config changes. -3. Add/trust/expand two repositories; restart; pinned remains first. -4. Both projects show Local, worktrees, sessions, stats, PR state, and full actions. -5. Navigate previous/next across projects. -6. Create/delete/rename worktrees in both projects. -7. Send prompts and answer permission/question requests in both projects. -8. Share/unshare B while A is active; exact B directory is used. -9. Load Project Settings A, activate B, save A; B is unchanged. -10. Modify A config externally while draft is dirty; conflict preserves draft. -11. Indexing defaults off for a new project; enabling A does not enable B; repo config cannot enable either. -12. Disable flag while B is active; pinned Local becomes active and B remains registered/suspended. -13. Repeat key routing and identity checks in a remote VS Code window. - -## Rules for the implementation model - -- Work on one slice at a time. -- Read the referenced architecture section before editing. -- Add focused tests before or with behavior changes. -- Use `apply_patch` for manual edits. -- Do not hand-edit generated SDK files. -- Do not raise file-size caps. -- Do not add fallback compatibility code unless the plan explicitly requires the single-project adapter. -- Do not claim a slice complete until its stop gate passes. -- If a required API or ownership decision is missing, stop and update this plan instead of guessing. diff --git a/.kilo/plans/agent-manager-multi-project-shipping-gaps.md b/.kilo/plans/agent-manager-multi-project-shipping-gaps.md deleted file mode 100644 index 8301929bbdf..00000000000 --- a/.kilo/plans/agent-manager-multi-project-shipping-gaps.md +++ /dev/null @@ -1,210 +0,0 @@ -# Agent Manager Multi-Project — Shipping Gaps - -Status as of 2026-07-27. The multi-project sidebar renders and works end to end in the -self-test harness (both projects list worktrees/sessions/sections, section creation -persists, worktree delete works, project switching restores selection, live session -upsert verified). Full unit suite green (3444 pass / 0 fail), typecheck, lint, knip, -arch caps all pass. Everything is uncommitted in the worktree. - -This document lists what is still missing, ranked by whether it should block shipping. - -Two audiences matter for the blocking decision: - -- **All users**: the branch changes shared surfaces (legacy sidebar refactor, config - write path, indexing consent, pollers). Regressions here ship to everyone, flag off - or not. -- **Experimental users**: multi-project mode is gated behind - `kilo-code.new.experimental.multiProject`, default off. Rough edges here are - acceptable if they cannot corrupt data. - ---- - -## 1. Blocks shipping (all-user surfaces) - -### 1.0 Empty-state skeleton regression (fixed, must stay fixed) - -`initializeState` and `onRequestState` only called `refreshSessions()` when the -managed state contained at least one session. With zero managed sessions the -backend listing never ran, `sessionsLoaded` never reached the webview, and both -the WORKTREES and SESSIONS sections stayed on skeletons forever (the worktree -gate requires `worktreesLoaded() && sessionsLoaded()`). Any user whose state file -lost its sessions — exactly what the earlier session-persist bugs caused — hit a -permanently empty-looking sidebar. Reproduced in the harness on a worktree-root -workspace and fixed by making both refreshes unconditional. Root-caused via -stage-by-stage init logging on 2026-07-27; do not reintroduce a content-based -guard here. - -### 1.1 Config write path can fail or wipe drafts for existing callers — P0-6 / P1-7 - -The config binding rework requires every `updateConfig` sender to carry a binding id, -and `configUpdateFailed` currently wipes the draft for the failed scope. Any existing -sender without a binding (permission dock, model picker, auto-approve, onboarding) -now fails or loses the user's unsaved edits. - -- Backend half is done: `expected` is optional in the overlay schema, writer, and - handler, so a client without a binding writes unconditionally again instead of - getting a 400. The webview half (draft retention on `configUpdateFailed`, and the - audit of every `updateConfig` sender) is still open. -- Work: the code change is small; the real work is auditing every webview - `updateConfig` sender and classifying it. -- Also: this change is orthogonal to multi-project. Split it into its own commit - (`feat(vscode): config write revision bindings`) so it can be reverted alone. - -### 1.2 Indexing status read silently revokes consent — P0-3 / P0-4 - -Partially addressed: untrusted projects are now filtered out of the consent list -(P0-4), and config scope switching plus project-scoped `indexing.enabled` writes -were restored after the rework had hardcoded the tab to global scope (the earlier -P1-8 gap). The remaining blocker is the read path below. - - -`fetchAndSendIndexingStatus` issues `PUT /indexing/consent` (a write) on a plain -status refresh, defaulting to `enabled: false` for any project not in local -`globalState`. On a fresh machine/profile, the first status read turns indexing off -for users who had it on. A refresh can also target the wrong project -(`requestIndexingStatus` resolves from the current session's directory). Separately, -the Indexing tab lists untrusted projects and would let a user enable indexing for a -repo the trust system has not approved. - -- Fix: read status with a GET; PUT only from the explicit setter; seed consent from - the effective config on first read instead of defaulting false; require an explicit - project on refresh; filter the project list to trusted projects. -- Dependency: needs a read-only status endpoint. If none exists, this escapes into - `packages/opencode` (shared upstream code, needs `kilocode_change` markers) or the - cloud repo. -- Also orthogonal to multi-project; split into its own commit - (`feat(vscode): per-project indexing consent`). - -### 1.3 Land the current work as separate, revertable commits - -Everything currently sits uncommitted in one worktree. The review's recommendation -stands: `git reset --soft origin/main` and stage by path into three commits — -multi-project, config bindings, indexing consent. The multi-project commit message -must not promise per-project config/indexing behavior that lands in the other two. - ---- - -## 2. Should fix soon after (experimental surface, data-integrity relevant) - -### 2.1 Same repo can register twice — P0-5 - -`projectIdFor` hashes the canonical path verbatim while `samePath` folds case, so two -casings of one repo produce two project ids, two contexts, and two state managers -racing to write the same `.kilo/agent-manager.json` (last write wins, worktrees and -sessions vanish). - -- Fix: fold case inside `projectIdFor` on darwin/win32; reject `addProject` when - `samePath` matches any registered root. -- Migration: existing registry keys were built from the old hash, so re-key them on - load or dual-lookup on read. Note `canonicalizePath` already realpaths existing - paths; the fallback branch must fold case too. -- Why not blocking: requires an unusual casing mismatch at registration time, and the - feature is flag-gated. - -### 2.2 Route service is shared across panels but versioned per context — P0-7 - -Two VS Code windows each have their own `ProjectContexts` with independent generation -counters feeding one shared `ProjectRouteService`. Panel B registering a project at -generation 0 while panel A is at 2 unregisters panel A's routes; closing one panel -drops routes the other still needs. - -- Fix: panel-qualified keys (`panelId + projectId + sessionId`), generations issued - by the service, ambiguity computed across all panels. -- Why not blocking: needs two windows running Agent Manager against the same repos. - The fallback already refuses ambiguous raw ids instead of resolving them wrong. -- Note: no two-panel test harness exists today; this fix should create one. - -### 2.3 `gh pr` noise in repos without remotes — Bug 5 - -`gh pr view` fails every 15s per worktree forever, logs before the dedupe, and -`pollOnce` rejections are unhandled. - -- Fix: log after the `lastHash` dedupe; `void this.pollOnce().catch(log)` in - `schedule`; per-root remote probe with a single error emission; skip PR pollers for - remote-less projects in `ProjectPollers.sync`. - ---- - -## 3. UX papercuts in experimental mode (fix opportunistically) - -- **Legacy tabs orphaned on upgrade** (P1-1): `createLocalTabs` migrates persisted - `localSessionIDs` into a `single` bucket that `tabKey()` never reads once the - catalog arrives. Migrate `single` to the pinned project id on first state apply. -- **`restoreProjectTarget` skips tab bookkeeping** (P1-3): a restored session has no - tab. Call `selectLocal()` first, add the tab, then select. -- **`ensurePendingTab` runs before restore** (P1-4): switch adds a "New Session" - draft that restore may contradict. Move it after restore. -- **Per-keystroke state saves**: `setActiveTarget` writes - `.kilo/agent-manager.json` on every selection change. Debounce or persist on - deactivation/dispose only. -- **Untranslated Indexing tab strings** (P1-9), **unused-ish composite id schemes** - (P1-12), **stats messages tagged at emit time** (P1-14), **no presence sync for - background projects** (P1-15), **registry read cache** (P1-16/17), **realpath - syscall churn** (P1-18), **`resolveProjectRoot` process spawning** (P1-19). - ---- - -## 4. Structural debt (schedule, don't block) - -### 4.1 Two sidebar implementations — Arch 1.1 - -Tracked as #12685 (section parity) and #12686 (sidebar drag-and-drop). - -`AgentManagerApp` keeps the legacy `renderBody` (now `SidebarBody.tsx`) and -`ProjectSidebarBody.tsx` as a reduced reimplementation. The reimplementation already -caused one full outage of multi-project mode (missing `DragDropProvider` crashed the -webview render). Missing versus legacy: worktree ordering, drag-and-drop reorder and -move-to-section, grouping, busy/navHint/shortcut badges, section auto-rename, stats -skeletons. - -- Direction: single-project becomes the degenerate case of multi-project (one - implicit pinned project, header row hidden), `SidebarBody.tsx` is deleted. -- Do this last: it churns the same message-stamping code as 2.2, needs per-project - DnD state, and must keep legacy pixel-identical for the default-off population. - Use the visual-regression skill to cover both modes before merging it. - -### 4.2 Ambient project scope via AsyncLocalStorage — Arch 1.3 - -`ProjectScope` plus the `this.state`/`this.context` getters make the target project -invisible at call sites; any continuation escaping ALS silently falls back to the -active project. `provider-lifecycle.ts` shows the intended end state (explicit deps). -Keep threading `ctx` explicitly into the remaining handler groups; add a dev-mode log -in the `context` getter when a project-stamped message resolves without a scope. - -### 4.3 Per-project webview store — Arch 1.2 - -`worktrees()`, `managedSessions()`, `selection()` etc. are single-valued with -`memKey()`/`tabKey()` and two "current project" accessors that disagree during the -switch window. Long-term: one `createProjectStore(projectId)` per project. For now -the gating added to the remember effect contains the known race. - ---- - -## 5. Verification gaps to close before the PR - -- **SSE session upsert** is unit-tested (`upsertSession`, `byDirectory`, fresh-skip - re-post) but not E2E-verified: the harness backend's basic-auth credentials could - not be re-extracted after a window reload, so external session creation was not - exercised live. Verify manually: `kilo` a new session in a registered repo from a - terminal and watch it appear in that project's sidebar. -- **Tab isolation across projects** (per-project buckets) is unit-tested but not - E2E-verified with real sessions open in two projects. -- **Legacy mode parity**: legacy sidebar and tab bar were smoke-tested (render, - select, section create + inline rename), but not the full matrix (delete, DnD - reorder, move-to-section, review tab, terminals). The extraction moved ~700 lines - of JSX; a visual-regression pass over `SidebarBody`/`TabBar` stories is the - cheapest safety net. -- **Harness instability**: the isolated VS Code window crashed repeatedly during this - work. If it keeps failing on the next pass, say so in the PR rather than claiming - coverage that does not exist. - ---- - -## Proposed landing sequence - -1. Quick independent fixes: 2.3 (gh noise), P0-4 (trust filter), 1.1's audit + 1.2's - fallback (config), 1.2's indexing read/write split if the GET endpoint exists. -2. 2.1 (case-fold + registry migration). -3. 2.2 (route service, with a two-panel test harness). -4. 1.1's config/indexing commits split out and merged separately. -5. Arch 1.1 (sidebar collapse) with visual-regression coverage; then the P1 batch. diff --git a/.kilo/plans/agent-manager-multi-project-sidebar-density.md b/.kilo/plans/agent-manager-multi-project-sidebar-density.md new file mode 100644 index 00000000000..28790c6a3bf --- /dev/null +++ b/.kilo/plans/agent-manager-multi-project-sidebar-density.md @@ -0,0 +1,437 @@ +# Plan: Agent Manager sidebar — shortcut badge fix + reclaim title width + +Worktree: `/Users/marius/Documents/git/kilocode/.kilo/worktrees/mewing-profit` +All paths below are relative to `packages/kilo-vscode/`. + +Rules: + +- Do exactly these edits. Do not refactor anything else. +- Solid.js, not React: `class=`, not `className=`. +- Do **not** add `kilocode_change` markers. This package is Kilo-owned and CI fails if you do. +- Do **not** add new i18n strings. None are needed. +- Out of scope: the per-project `SESSIONS` list in the tree (tracked in + https://github.com/Kilo-Org/kilocode/issues/12928). Do not touch `UnassignedSessionsSection.tsx`. + +Do task A, verify, then task B, verify, then task C. + +## Task A — Shortcut badge on the right, hidden until hover or ⌘ held + +The `⌘1` badge on "local" rows is always visible, and in multi-project mode it renders on the +*left*, between the icon and the label. Worktree rows already behave correctly. Cause: +`.am-shortcut-badge` has no default `opacity: 0` — worktree badges are only hidden because their +container `.am-wt-hover-actions` is hidden, and local rows never got that container. + +### A1 — `webview-ui/agent-manager/ProjectSidebarBody.tsx` + +Replace the whole ` +``` + +The `−` in `am-stat-deletions` is U+2212 MINUS SIGN, not a hyphen. Copy it verbatim. + +### A2 — `webview-ui/agent-manager/SidebarBody.tsx` + +Same bug in legacy single-project mode, but the badge is already last, so only the wrapper is +missing. + +Insert **before** line **127** (``): + +```tsx +
+``` + +Replace line **182**: + +```tsx + {isMac ? "⌘" : "Ctrl+"}1 +``` + +with: + +```tsx +
+ {isMac ? "⌘" : "Ctrl+"}1 +
+
+``` + +Result: one `am-wt-actions-cell` div containing the skeleton ``, the stats ``, and the +hover-actions div, closing before ``. Prettier fixes indentation in task D. + +### A3 — `webview-ui/agent-manager/agent-manager.css` + +Delete lines **569-575** and replace with the rule that actually works: + +```css +.am-local-item .am-shortcut-badge { + right: 8px; +} + +.am-local-item:hover .am-shortcut-badge { + opacity: 1; +} +``` + +becomes: + +```css +.am-local-item:hover .am-wt-hover-actions { + opacity: 1; + visibility: visible; +} +``` + +(`right: 8px` never applied — the badge is `position: static`. The `opacity: 1` never did anything +because nothing set `opacity: 0` first.) + +### A4 — same file, delete lines **609-611** entirely, add nothing: + +```css +.am-show-shortcuts .am-local-item .am-shortcut-badge { + opacity: 1; +} +``` + +The rule at lines 597-600 is not scoped to worktree items, so after A1/A2 it already covers local +rows. + +### A5 — same file, lines **986-989**, add `visibility: hidden` to match worktree behaviour: + +```css +.am-local-item:hover .am-worktree-stats, +.am-local-item:hover .am-worktree-stats-skeleton { + opacity: 0; + visibility: hidden; +} +``` + +### Verify A + +```bash +cd packages/kilo-vscode && bun run typecheck && bun run lint +``` + +## Task B — Stop reserving width for invisible content + +`.am-wt-actions-cell` is a grid whose children all stack in cell 1/1, so the cell is permanently as +wide as its widest child. `visibility: hidden` does not remove layout, so the invisible hover +actions (~42px) and the loading skeleton (~48px) reserve width on every row forever. That is why +titles truncate ~40px before the right edge. + +All edits in `webview-ui/agent-manager/agent-manager.css`. + +### B1 — Take hover actions out of grid flow + +Lines **475-482**. Add the four `position` lines at the top, keep everything else: + +```css +.am-wt-hover-actions { + position: absolute; + top: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 2px; + opacity: 0; + visibility: hidden; +} +``` + +Absolutely positioned children never size grid tracks. `.am-wt-actions-cell` is already +`position: relative` (line 464). Do not edit the `.am-wt-actions-cell > *` rule at lines 469-472. + +### B2 — Same for the loading skeleton + +Add immediately after the rule from B1: + +```css +/* Skeleton is a placeholder, not real content — it must not reserve title width. */ +.am-wt-actions-cell > .am-worktree-stats-skeleton { + position: absolute; + top: 0; + right: 0; + bottom: 0; +} +``` + +Do not edit the base `.am-worktree-stats-skeleton` rule (lines 744-748) or `.am-pr-badge-skeleton`. + +After B1+B2 the cell is sized only by `.am-worktree-stats` and `.am-worktree-delete-hint`, which +are the only things that should reserve space. + +### B3 — Fade the title under the overlaying actions + +The actions now overlay the row instead of sitting in reserved space, so a long title would render +behind them. Same fix the codebase already uses for the local branch at lines 577-580. Add after +B2: + +```css +.am-worktree-item:hover .am-worktree-branch { + mask-image: linear-gradient(to right, black calc(100% - 48px), transparent 100%); + -webkit-mask-image: linear-gradient(to right, black calc(100% - 48px), transparent 100%); +} +``` + +### B4 — Remove one layer of nested padding + +Lines **365-370**, change `padding: 0 6px;` to `padding: 0;`: + +```css +.am-project-body { + display: flex; + flex-direction: column; + min-height: 0; + padding: 0; +} +``` + +Gains 6px per side on every row in a project. Rows keep their own 10px inset +(`.am-local-item` line 106, `.am-worktree-item` line 425) so they stay indented under the project +header. Do not change those, and leave `.am-project-body .am-section-header` (lines 273-275) alone. + +### B5 — Remove the remaining outer list gutters in multi-project mode + +The sidebar keeps 8px horizontal padding for the header controls, but the project list should use +the full width up to its scrollbar. In the same CSS file, extend `.am-projects-list` with: + +```css +.am-projects-list { + display: flex; + flex-direction: column; + min-height: 0; + overflow-y: auto; + margin-inline: -8px; +} +``` + +Do not remove padding from `.am-sidebar` itself. This makes project cards and rows reach the +scrollbar without moving the `PROJECTS` header and its controls to the edge. + +### Verify B + +```bash +cd packages/kilo-vscode && bun run typecheck && bun run lint +``` + +## Task C — Show where a palette session lives + +In the ⌘F search palette, multi-project session results show only the project name, so you cannot +tell whether a session is in a worktree or at the project root. + +File: `webview-ui/agent-manager/ProjectList.tsx`. In the session loop (lines **90-106**), add the +two `const` lines and change `meta` and `search`: + +```tsx + for (const session of props.sessions[project.id] ?? []) { + const wt = session.worktreeId ? state.worktrees.find((item) => item.id === session.worktreeId) : undefined + const where = wt ? wt.label || wt.branch : props.t("agentManager.local") + items.push({ + key: `${project.id}:session:${session.id}`, + projectId: project.id, + kind: "session", + group: "sessions", + title: session.title || props.t("agentManager.session.untitled"), + meta: [project.label, where], + search: [project.label, where, wt?.branch, session.title, session.id].filter(Boolean).join(" "), + updatedAt: session.updatedAt, + state: "idle", + visible: project.expanded, + sessionId: session.id, + location: session.worktreeId ? "worktree" : "local", + worktreeId: session.worktreeId ?? undefined, + }) + } +``` + +`state` is already in scope (line 55) and already null-checked (line 56). `meta` is joined with +` · ` by the renderer (`SidebarSearchMenu.tsx:143`), so no separator work is needed. + +## Task E — Align the project heading with the row icons + +Every row in the projects tree used a different left inset, which made the sidebar look busy and +indented for no reason. Measured against a full-bleed `.am-projects-list` (starting at x=0): + +| Element | Before | After | +|---|---|---| +| `PROJECTS` label | 16px | 10px | +| Project chevron | 12px | 10px | +| `WORKTREES` / `SESSIONS` label | 6px | 10px | +| `.am-local-item` icon | 10px | 10px | +| `.am-worktree-item` icon | 10px | 10px | + +Changes in `agent-manager.css`: + +- `.am-local-item` padding `8px 10px` → `8px 6px` +- `.am-worktree-item` padding `6px 10px` → `6px 6px` +- `.am-project-item` padding `6px 8px 6px 12px` → `6px 6px` +- `.am-project-body .am-section-header` padding-left `6px` → unchanged at `6px` +- `.am-projects > .am-section-header` gets `padding-left: 2px`, because that heading sits inside + `.am-sidebar`'s own 8px padding rather than in the pulled-out list + +The leading columns carry no padding of their own (`.am-sidebar-header-toggle` and +`.am-sidebar-header-chevron` are bare 16px boxes), so row padding is the only lever. + +### Symmetric gutter + +`.am-projects-list` uses `margin-left: -4px; margin-right: -4px`, pulling out of `.am-sidebar`'s +8px padding to an even 4px gutter. An earlier attempt used `-8px` on the left for true full bleed, +but that is wrong twice over: the resize handle's inner half then covered every card, so card +clicks started a resize, and a selected row's `border-radius: var(--radius-sm)` background clipped +flat against the window edge while still being inset on the right, which read as a bar bleeding off +the sidebar rather than a card. + +4px is also the most reclaimable on the right. The handle's hit area is 8px wide centered on the +border, reaching 5px back into the content area (255-263 in a 260px sidebar). At this gutter the +row's hover actions end at 249, leaving 6px of clearance; anything tighter puts row buttons under +the handle and turns clicks into resize drags. + +### Two tab stops + +Reducing the insets exposed that labels sat at four different offsets: the non-collapsible +`WORKTREES` heading at 6px (it passes no `onToggle`, so `SidebarSectionHeader` renders no chevron), +collapsible headings at 28px, worktree card text at 30px, and local card text at 32px because +`.am-local-icon` was 18px wide while `.am-wt-icon` held a 16px glyph. + +Normalized to one 16px leading column with an 8px gap, giving exactly two tab stops: + +- `.am-sidebar-header-main` gap `6px` → `8px`, matching the card icon gap +- new `.am-project-body .am-sidebar-header:not(.am-sidebar-header-toggleable) .am-sidebar-header-main { padding-left: 24px }` + so a heading with no chevron still reserves the column +- `.am-local-icon` `18px` → `16px` +- `.am-wt-icon` gains `width: 16px` and `justify-content: center` + +Measured result at 260px: + +| Tab stop | Elements | +|---|---| +| 10px | `PROJECTS` label, project chevron, local icon, worktree icon, `SESSIONS` chevron | +| 34px | project name, `WORKTREES` label, `SESSIONS` label, local text, worktree text | + +``` +cardLeftGap=4 | cardRightGap=4 | actionsRight=249 | handleZoneFrom=255 +``` + +`PROJECTS` intentionally stays at the 10px glyph stop rather than being pushed to 34px: it is the +root heading, so indenting it further than the projects beneath it would invert the hierarchy. + +### Rejected: a per-project icon in the heading + +Superset shows a GitHub **owner** avatar per project (`https://github.com/{owner}.png?size=64`, +falling back to the project's initial). Our webview CSP already allows `https:` images, so it was +feasible, but it does not fit this repo set: of the local projects, ~17 are `Kilo-Org` remotes and +would share one identical Kilo logo, and ~20 have no git remote at all and would show nothing. +An owner avatar answers "who owns this", which is not the question the sidebar needs answered. +Superset's repo-file scanner (`favicon-discovery.ts`) is dead code there, so it was not an option +worth copying either. Left out entirely as out of scope. + +## Task D — Final checks + +From `packages/kilo-vscode`: + +```bash +bun run format +bun run typecheck +bun run lint +bun run test:unit +bun run check-kilocode-change +bun run compile +``` + +Then create `.changeset/agent-manager-sidebar-density.md`: + +```md +--- +"kilo-code": patch +--- + +Fix the Agent Manager sidebar keyboard shortcut badge so it appears on the right edge of local rows and only while hovered or holding the jump modifier, give worktree titles more room by no longer reserving space for hidden row actions, and show which worktree a session belongs to in the search palette. +``` + +## Visual regression baselines + +Task A and B change existing snapshots: `WorktreeItemDefault`, `WorktreeItemActive`, +`WorktreeItemPendingDelete`, `WorktreeItemStale`, `WorktreeItemWithStats`, `WorktreeItemGrouped`, +`SidebarSearchOpen`, `MultiProjectSidebar` in `webview-ui/src/stories/agent-manager.stories.tsx`. + +**Do not run or update visual regression tests.** `tests/visual-regression.spec.ts` is skipped on +macOS, so you cannot produce valid Linux baselines locally. CI regenerates them and may push a +baseline commit. If a push is then rejected, do **not** `git pull --rebase`; run +`git fetch && git push --force-with-lease`. + +## Manual test + +Enable `kilo-code.new.experimental.multiProject` in Kilo Settings → Experimental, add a second +project, open Agent Manager (`Cmd+Shift+M`). + +1. Nothing hovered: no `⌘N` badge visible anywhere. +2. Hover a `local` row: badge appears at the **right** edge, git stats fade out. +3. Hold ⌘: badges appear on all local and worktree rows, all right-aligned. Release: all gone. +4. Worktree rows with no git changes show noticeably longer titles than before. Hover one with a + long title: it fades under the badge and trash button instead of colliding. +5. ⌘F: session results read ` · `. +6. Turn the experimental setting off: the `local` row's `⌘1` badge is hidden until hover. + +## Known issue, not fixed here + +`⌘1`–`⌘9` index the full nav order, which includes session rows that render no badge +(`navigate.ts:214-218` + `section-helpers.ts:147-153`). So with 4 worktrees you see `⌘1`–`⌘5` and +`⌘6`–`⌘9` silently land on unlabelled session rows. Deliberately left alone: fixing it means +changing jump semantics and rewriting `tests/unit/navigate.test.ts:745-757`, and it becomes moot +once #12928 moves sessions out of the tree. diff --git a/.kilo/plans/agent-manager-new-worktree-project-selector.md b/.kilo/plans/agent-manager-new-worktree-project-selector.md new file mode 100644 index 00000000000..5f98fd086f7 --- /dev/null +++ b/.kilo/plans/agent-manager-new-worktree-project-selector.md @@ -0,0 +1,834 @@ +# Agent Manager — New Worktree Project Selector + +Status: implemented 2026-08-06. The implementation is uncommitted. + +This is Slice 4 item 6 ("Add project-aware New Worktree targeting") from +`agent-manager-multi-project-uniform-ui.md`, the last unimplemented item of that slice. +`agent-manager-multi-project-runtime.md:29` deferred it out of the backend-first scope. +Everything the extension side needs already exists; this is almost entirely a webview +change. + +Implementation notes: + +- The project catalog is passed into the dialog as an accessor so the picker reflects + registry changes while it remains open. +- The per-project default-base resolver returns `undefined` when the project has no + configured/local branch, allowing the backend-detected branch response to remain the + fallback instead of being replaced by a hardcoded `main`. +- Branch, import-result, and worktree-ready messages carry `projectId` in multi-project + mode, which makes fast project changes and cross-project creation activation safe. +- The slash-command hook accepts optional caller-owned commands; `/project` is scoped to + this dialog and is hidden when multi-project mode is unavailable. + +--- + +## Problem + +With `kilo-code.new.experimental.multiProject` enabled, the New Worktree dialog has no +notion of which repository it targets, and the user cannot see or change it. + +`Cmd+N` opens the dialog with no project at all: + +```tsx +// AgentManagerApp.tsx:1870-1876 +const showNewWorktreeDialog = () => { + if (!loaded()) return + expandSidebar() + dialog.show(() => ( + dialog.close()} defaultBaseBranch={repoDefaultBranch()} /> + )) +} +``` + +`projectId` is `undefined`, so every message the dialog sends omits it +(`agentManager.requestBranches` at `NewWorktreeDialog.tsx:321`, +`agentManager.createMultiVersion` at `:373`, `agentManager.importFromPR` at `:566`, +`agentManager.importFromBranch` at `:575`). The extension then silently falls back to the +active project in `messageProject()` (`AgentManagerProvider.ts:474`) before running the +message inside `ProjectScope`. + +The result is correct but opaque: + +- The dialog never shows which repository the worktree lands in. +- The only way to target a specific project is the per-project `+` button + (`ProjectList.tsx:136-146`), which does pass `projectId` explicitly. +- `defaultBaseBranch` is resolved from the *active* project + (`AgentManagerApp.tsx:261,272`), so even Advanced options' base-branch list and default + badge are implicitly single-project. + +The desired behavior, per the original request: the dialog should show the assigned +project and let the user change it. Defaulting to the last selected project is fine as a +default; it just must be visible and overridable. + +--- + +## Design decisions + +### Placement: inline with the tab switcher + +The selector renders inside the New/Import pill row (`NewWorktreeDialog.tsx:620-699`), +right-aligned with a constrained width, so it is shared by both tabs without adding a +full-width form row. + +``` +┌─ New Worktree ──────────────────────────────┐ +│ [ New ] [ Import ] [ folder kilocode ▾ ] │ ← inline, multiProject only +│ [ Worktree name (optional) ] │ +│ [ prompt … ] Code ▾ GPT-5.6 ▾ None ▾ │ +│ › Advanced options │ +│ VERSIONS 1 2 3 4 ⧉ Compare Models │ +│ [ Create Worktree ] │ +└──────────────────────────────────────────────┘ +``` + +Rejected alternatives and why: + +- **Inside Advanced options.** Wrong category. Advanced options holds refinements of a + known target (branch name, base branch). The project *is* the target: changing it + invalidates the branch list, base branch, default-branch badge, and setup scripts. + Hiding it also fails the stated requirement of seeing which project is assigned. +- **A full-width row directly beneath the tabs.** It covered both tabs, but consumed + unnecessary vertical space and made the project control look like a primary form field. +- **In the dialog title** (`New Worktree in [kilocode ▾]`). Reads nicely but requires + widening `Dialog`'s `title` prop to accept JSX, which touches kilo-ui for one caller. + +### Visibility + +Render the row only when `multiProject` is true. With the flag off (the default) the +dialog stays byte-identical to today, so there is no regression surface for the +all-user path. When the flag is on but only the pinned project exists, still render it: +showing the target is informative and the requirement is explicitly about seeing the +assignment. + +### Default value + +`props.projectId ?? activeProjectId()`. `activeProjectId()` already exists at +`AgentManagerApp.tsx:268` (`projectList().find((p) => p.active)?.id ?? currentProjectId()`). + +No new persistence. The active project is already the durable "last selected" state +(persisted per project as `activeTarget` in each repo's `.kilo/agent-manager.json`, plus +the registry's ordering). Adding a separate "last dialog project" key would create a +second source of truth that can disagree with the sidebar. + +### Reuse, no new CSS + +The row uses the existing `am-advanced-field` + `am-nv-config-label` + +`am-selector-wrapper` + `am-selector-trigger` markup, i.e. exactly the structure the base +branch selector already uses at `NewWorktreeDialog.tsx:813-905`, with `DeferredPopover` +(already imported) instead of `BranchSelectPopover`. + +### Component extraction + +`NewWorktreeDialog.tsx` is already 1136 lines. The selector and its popover list go into a +new `webview-ui/agent-manager/ProjectSelect.tsx` (roughly `BranchSelect.tsx`'s role): +presentational, takes `projects`, `value`, `onSelect`, and labels, and owns nothing but +its own list rendering. The dialog keeps only the signal, the popover trigger, and the +effects. + +Note: `webview-ui/agent-manager/NewWorktreeDialog.tsx` is not under a `maxLines` cap +(`tests/unit/agent-manager-arch.test.ts` caps `src/agent-manager/*.ts` only), but the file +is on the arch test's watched list and the caps exist to discourage exactly this kind of +growth. + +--- + +## Changes + +### 1. `webview-ui/agent-manager/ProjectSelect.tsx` (new) + +Presentational popover body listing projects: + +- Row = project label + dimmed root path (tooltip on the full root, matching + `ProjectsSection.tsx:60`). +- Check mark on the selected project. +- Untrusted and missing projects are disabled and carry the same affordances the accordion + uses: `lock` icon + trust hint, `warning` icon + missing hint + (`ProjectsSection.tsx:67-75`). Selecting them is not possible; trust happens in the + sidebar, not in this dialog. Keeps the dialog free of trust-flow branching. +- There is intentionally no Add project action in this picker. Project registration and + trust management stay in the Agent Manager Projects toolbar. + +### 2. `webview-ui/agent-manager/NewWorktreeDialog.tsx` + +Props change: + +```ts +export const NewWorktreeDialog: Component<{ + onClose: () => void + projectId?: string // now: initial value, not fixed target + projects?: AgentProjectSnapshot[] // omitted / empty => single-project, row hidden + activeProjectId?: string + defaultBase?: (projectId: string) => string // replaces defaultBaseBranch?: string + mode: ModeRouter +}> +``` + +`defaultBaseBranch?: string` must become a per-project lookup because each project has its +own configured default and its own local branch. `ProjectList.tsx:142` already computes +that expression (`state?.defaultBaseBranch ?? props.local[projectId]?.branch`); hoist it +into the callback so both call sites share it. + +New state and derived values: + +```ts +const [project, setProject] = createSignal(props.projectId ?? props.activeProjectId) +const [projectOpen, setProjectOpen] = createSignal(false) +const selectable = () => (props.projects ?? []).filter((p) => p.trusted && !p.missing) +const showProject = () => (props.projects?.length ?? 0) > 0 +``` + +Every outbound message switches from `props.projectId` to `project()`: +`:321` `requestBranches`, `:373` `createMultiVersion`, `:566` `importFromPR`, +`:575` `importFromBranch`. + +Reload branch data on project change, replacing the one-shot `onMount` request at `:319-321`: + +```ts +createEffect( + on(project, (id) => { + setBranches([]) + setBranchSearch("") + setHighlightedIndex(0) + setBaseBranch(null) // custom base is project-specific + setDefaultBranch(props.defaultBase?.(id) ?? "main") + setBranchesLoading(true) + vscode.postMessage({ type: "agentManager.requestBranches", projectId: id }) + }), +) +``` + +Drop stale branch replies in the `agentManager.branches` handler (`:520-525`). +`AgentManagerBranchesMessage` **already declares an optional `projectId`** +(`extension-messages.ts:939-945`, `src/agent-manager/types.ts:293-298`); the field is +simply never populated or read today. Without this guard, switching projects twice quickly +can race a wrong branch list into the base-branch popover: + +```ts +if (ev.projectId && ev.projectId !== project()) return +``` + +Also replace the `if (!props.defaultBaseBranch) setDefaultBranch(ev.defaultBranch)` guard +at `:523` — with a per-project lookup, the guard must consult +`props.defaultBase?.(project())` instead of a fixed prop. + +Preserved across a project change (all project-agnostic): prompt text and its +`advancedDialogPrompt` persistence, images, name, agent, model, variant, versions, compare +allocations, sandbox override. + +Keyboard: add `project` to `WORKTREE_PROMPT_COMMANDS` so `/project` opens the popover, +consistent with mode/model/variant/sandbox already being reachable from the dialog's slash +menu (`:302-314`). Hide it from the list when `showProject()` is false, using the same +`hidden` set mechanism already used for `agents` / `variant` / `sandbox`. + +### 3. `webview-ui/agent-manager/AgentManagerApp.tsx` + +`showNewWorktreeDialog` (`:1870-1876`) passes the catalog and a per-project default +resolver instead of a single branch string: + +```tsx + dialog.close()} + projects={multiProject() ? projectList() : undefined} + activeProjectId={activeProjectId()} + defaultBase={defaultBase} +/> +``` + +where `defaultBase(id)` reads `registry.ensure(id).defaultBaseBranch() ?? registry.ensure(id).localStats()?.branch ?? repoDetectedBranch() ?? "main"`. +`registry.ensure` and both store fields already exist +(`project/registry.ts:39`, `project/store.ts:62,67,113,123`). + +### 4. `webview-ui/agent-manager/ProjectList.tsx` + +`newWorktree(projectId)` (`:136-146`) passes the same `projects` / `activeProjectId` / +`defaultBase` props with `projectId` as the initial value, so the per-project `+` button +opens the dialog pre-scoped but still switchable. Its current inline +`state?.defaultBaseBranch ?? props.local[projectId]?.branch` expression is replaced by the +shared resolver passed down from `AgentManagerApp`. + +### 5. `src/agent-manager/worktree-importer.ts` + +Stamp `projectId` on all three `agentManager.branches` posts (`:27`, `:48`, `:54`). The +field is already in the type; the value is available from the ambient `ProjectScope` +context the message runs in (`AgentManagerProvider.ts:479`). Without this the stale-reply +guard in the webview is inert. + +### 6. Activate the created worktree when the project differs + +Creating in a non-active project currently leaves the sidebar where it is: +`createMultiVersion` never activates (no activation call in `provider-multi-version.ts`), +and the new worktree just appears in that project's accordion. That is right for the +per-project `+` button, but for `Cmd+N` where the user deliberately switched projects, +landing in the new worktree is what the flow implies. + +Post an `agentManager.activateSelection` for the first created worktree when the chosen +project differs from the active one. `activateSelection` already handles readiness, trust, +and stale-target fallback (`project/messages.ts:76-99`), so this is one message, not new +machinery. Hook it to the existing `agentManager.worktreeSetup` / `multiVersionProgress` +handling in `AgentManagerApp.tsx:1453-1471`, which already carries `projectId`. + +### 7. i18n + +New keys in `webview-ui/agent-manager/i18n/en.ts` (near the existing +`agentManager.dialog.*` block at `:130`): + +- `agentManager.dialog.project.select` — "Select project" +- `agentManager.dialog.project.untrusted` — "Trust this project in the sidebar first" +- `agentManager.dialog.project.missing` — "Repository not found" + +Then translate the four keys into the other 20 locale files in that directory via the +`translator` subagent. + +--- + +## Implementation order + +1. `ProjectSelect.tsx` with the presentational list, plus i18n keys in `en.ts`. +2. Dialog: `project` signal, prop rename to `defaultBase`, route all four outbound + messages through `project()`, render the row behind `showProject()`. +3. Dialog: `createEffect(on(project, …))` branch reload, base-branch reset, stale-reply + guard. +4. Call-site updates in `AgentManagerApp.tsx` and `ProjectList.tsx`, shared `defaultBase` + resolver. +5. `projectId` stamp in `worktree-importer.ts`. +6. `/project` slash command. +7. Post-create activation when the target project differs. +8. Locale fan-out. +9. Changeset (`minor`, user-facing): worktree creation targets an explicit project. + +--- + +## Tests + +Existing source-text unit tests already assert against this dialog and will need to stay +green: `tests/unit/new-worktree-dialog-sandbox.test.ts`, +`tests/unit/prompt-input-bidirectional.test.ts`, and the dialog entry in +`tests/unit/agent-manager-arch.test.ts`. + +New coverage: + +- The dialog posts `createMultiVersion` / `requestBranches` / `importFromBranch` / + `importFromPR` with the *selected* project id, not the prop, after a project change. +- A `agentManager.branches` reply carrying a non-current `projectId` does not mutate the + branch list (the race guard). +- Changing project clears `baseBranch` and re-derives `defaultBranch` from `defaultBase`. +- Prompt text survives a project change (no accidental reset through the shared + `advancedDialogPrompt` cache). +- The row does not render when `projects` is empty, so the single-project dialog is + unchanged. +- Untrusted and missing projects are not selectable. + +Checks to run before declaring done, from `packages/kilo-vscode/`: +`bun run typecheck`, `bun run lint`, `bun run test:unit`, `bun run knip`. + +--- + +## Manual verification + +In the isolated harness (`bun run extension:isolated`) with +`kilo-code.new.experimental.multiProject` enabled and two repositories registered: + +1. `Cmd+N` from project A shows "Project: A". Switch to B, create, and confirm the + worktree lands in B's accordion and the sidebar activates it. +2. Switch project with Advanced options open and confirm the base-branch list and default + badge follow the new project rather than showing A's branches. +3. Switch project rapidly back and forth and confirm the branch list matches the selected + project (the race guard). +4. Type a prompt, switch project, confirm the prompt is retained. +5. Use the Import tab after switching project and confirm branches and PR import target + the selected repository. +6. Turn the flag off and confirm the dialog is visually identical to today. + +--- + +## Out of scope + +- Trusting or removing a project from inside the dialog. Trust stays in the sidebar; the + dialog only disables untrusted entries. +- Any change to how the active project is persisted. +- The quick-create path (`Cmd+Shift+N` → `agentManager.createWorktree`, + `AgentManagerApp.tsx:1863-1867`). It has no dialog, so it keeps targeting the active + project. Worth revisiting only if the explicit-target rule should apply there too. +- Per-project setup-script or agent selection in the dialog. + +## Risks + +- **Stale branch data** is the only real correctness risk, and it is why the `projectId` + stamp plus the reply guard are mandatory rather than optional polish. +- **Prop signature change** (`defaultBaseBranch: string` → `defaultBase: (id) => string`) + touches both call sites; a partial migration would silently show one project's default + branch while creating in another. +- **Dialog file growth**; mitigated by extracting `ProjectSelect.tsx`. + +--- + +# Appendix: exact UI and styling specification + +Everything below is copy-paste ready. Class names, tokens, and icon names are all verified +against the current tree. Do not invent new tokens or new class names beyond the ones +listed here. + +## A. Visual layout + +``` +┌─ New Worktree ─────────────────────────────────────────── X ─┐ +│ │ +│ [ New ][ Import ] [ 📁 kilocode ⌃⌄ ] │ +│ └────────────────────────────────────────────┘ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ Worktree name (optional) │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ prompt … │ │ +│ │ Code ▾ OpenAI / GPT-5.6 ▾ None ▾ ✨ 🔒 🎤 │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ › Advanced options │ +│ VERSIONS [1][2][3][4] [⧉ Compare Models] │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ Create Worktree │ │ +│ └──────────────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────┘ +``` + +Open dropdown (anchored under the trigger, same width as the trigger): + +``` + ┌────────────────────────────────────────────┐ + │ 📁 kilocode ~/Documents/git/kilocode ✓│ ← .am-project-option-active + │ 📁 cloud ~/Documents/git/cloud │ + │ 🔒 sample-app ~/dev/sample-app │ ← disabled, 50% opacity + │ ⚠ old-repo ~/dev/old-repo │ ← disabled, 50% opacity + └────────────────────────────────────────────┘ +``` + +Rules: + +- The selector is inline with the New/Import buttons inside the tab-switcher flex row, so + it applies to New and Import alike. +- The project name and folder icon identify the scope without a separate visible label. +- The trigger is the same control as the Advanced options base-branch trigger + (`.am-selector-trigger`), so the dialog has one visual language for "pick a thing". +- The row is **not rendered at all** when `props.projects` is empty or undefined. That is + the single-project / flag-off case, which must stay pixel-identical to today. + +## B. Exact icon names + +Only these, from `packages/ui/src/components/icon.tsx`: + +| Where | `Icon name` | Notes | +|---|---|---| +| Trigger left | `folder` | Always, regardless of project state. | +| Trigger right | `selector` | Same as every other `.am-selector-trigger`. | +| Option row, normal | `folder` | | +| Option row, untrusted | `lock` | Matches the sidebar accordion affordance. | +| Option row, missing | `warning` | Matches the sidebar accordion affordance. | +| Option row, selected | `check-small` | Right-aligned. | + +All at `size="small"`. Do not use `folder-add-left`, `check`, or `plus`. + +## C. New file: `webview-ui/agent-manager/ProjectSelect.tsx` + +```tsx +// Project picker list for the New Worktree dialog + +/** @jsxImportSource solid-js */ + +import { For, Show, type Component } from "solid-js" +import { Icon } from "@kilocode/kilo-ui/icon" +import type { AgentProjectSnapshot } from "../src/types/messages" + +interface ProjectSelectProps { + projects: AgentProjectSnapshot[] + selected?: string + onSelect: (id: string) => void + labels: { untrusted: string; missing: string } +} + +export const ProjectSelect: Component = (props) => ( +
+ + {(project) => { + const blocked = () => !project.trusted || project.missing + const hint = () => { + if (project.missing) return props.labels.missing + if (!project.trusted) return props.labels.untrusted + return project.root + } + const icon = () => { + if (project.missing) return "warning" as const + if (!project.trusted) return "lock" as const + return "folder" as const + } + return ( + + ) + }} + +
+) +``` + +Notes for the implementer: + +- Wrap in `.am-dropdown-list`, not a bare fragment: that class supplies the scroll cap and + 4px padding, and `.am-dropdown [data-slot="popover-body"]` zeroes the popover padding. +- No search input. A project list is short; adding one would need keyboard nav plumbing for + no benefit. +- `props.labels` is passed in rather than calling `useLanguage()` here, matching how + `BranchSelect` and `SidebarSearchMenu` take label props. + +## D. Exact JSX inserted into `NewWorktreeDialog.tsx` + +### D.1 Imports + +Add to the existing type import block at lines 6-12: + +```ts + AgentProjectSnapshot, +``` + +Add after line 48 (`import { BranchSelect, BranchSelectPopover } …`): + +```ts +import { ProjectSelect } from "./ProjectSelect" +``` + +`Icon`, `Show`, `DeferredPopover`, `createSignal`, `createEffect` are already imported. +`on` from `solid-js` must be added to the line 5 import list. + +### D.2 Props + +Replace the component signature at lines 84-89 with: + +```tsx +export const NewWorktreeDialog: Component<{ + onClose: () => void + /** Resolves the default base branch for one project. */ + defaultBase?: (projectId: string) => string | undefined + /** Initial target project. The user can change it while the dialog is open. */ + projectId?: string + /** Full project catalog. Empty or undefined hides the project row entirely. */ + projects?: () => AgentProjectSnapshot[] + /** Project the sidebar currently has active; used as the default target. */ + activeProjectId?: string + mode: ModeRouter +}> = (props) => { +``` + +### D.3 State + +Immediately after line 101 (`const [tab, setTab] = createSignal("new")`): + +```tsx +const [project, setProject] = createSignal(props.projectId ?? props.activeProjectId) +const [projectOpen, setProjectOpen] = createSignal(false) +const projects = () => props.projects?.() ?? [] +const showProject = () => projects().length > 0 +const projectLabel = () => projects().find((p) => p.id === project())?.label ?? "" +``` + +`defaultBranch` (line 106) changes from `props.defaultBaseBranch ?? "main"` to: + +```tsx +const [defaultBranch, setDefaultBranch] = createSignal( + (project() && props.defaultBase?.(project()!)) || "main", +) +``` + +### D.4 The inline selector + +Insert inside the tab switcher after the Import button: + +```tsx +{/* Project scope — applies to both tabs. Hidden unless multi-project is on. */} + +
+
+ + + + + {t("agentManager.dialog.project.select")} + + } + > + {projectLabel()} + + + + + + + } + > + { + track("project_select", { changed: id !== props.activeProjectId }) + setProject(id) + setProjectOpen(false) + }} + labels={{ + untrusted: t("agentManager.dialog.project.untrusted"), + missing: t("agentManager.dialog.project.missing"), + }} + /> + +
+
+
+``` + +Critical details, in order of how easily they get wrong: + +1. `placement="bottom-start"`, **not** `top-start`. The rest of this dialog uses + `top-start` because those triggers sit near the bottom of the panel. This one sits at + the top, so it must open downward. +2. `portal={false}` plus the escape CSS in section E.3. Do not switch to a portal unless + the clipping fallback in E.3 is needed. +3. `sameWidth` so the dropdown matches the trigger width, consistent with the base-branch + and compare-models popovers. +4. Never send the project label, root, or id as a telemetry property. `track` takes only + the boolean shown above. + +### D.5 Reactive reload on project change + +Delete the one-shot request at lines 319-321 inside `onMount` and replace it with an effect +placed next to the other `createEffect` calls: + +```tsx +// Project scope owns the branch data, the base branch, and the default badge. +// Prompt, name, model, agent, versions and attachments are project-agnostic and survive. +createEffect( + on(project, (id) => { + if (!id) return + setBranches([]) + setBranchSearch("") + setHighlightedIndex(0) + setBaseBranch(null) + setDefaultBranch(props.defaultBase?.(id) ?? "main") + setBranchesLoading(true) + vscode.postMessage({ type: "agentManager.requestBranches", projectId: id }) + }), +) +``` + +`on(project, …)` without `{ defer: true }` runs immediately, which replaces the removed +`onMount` request. Keep the textarea focus logic in `onMount` untouched. + +In the `agentManager.branches` handler (lines 520-525), replace the body with: + +```tsx +if (msg.type === "agentManager.branches") { + const ev = msg as AgentManagerBranchesMessage + if (ev.projectId && ev.projectId !== project()) return + setBranches(ev.branches) + const id = project() + if (!id || !props.defaultBase?.(id)) setDefaultBranch(ev.defaultBranch) + setBranchesLoading(false) +} +``` + +### D.6 Outbound project id + +Four call sites change from `props.projectId` to `project()`: + +| Line | Message | +|---|---| +| 321 (now inside the effect) | `agentManager.requestBranches` | +| 373 | `agentManager.createMultiVersion` | +| 566 | `agentManager.importFromPR` | +| 575 | `agentManager.importFromBranch` | + +Grep afterwards: `props.projectId` must appear exactly once in the file, in the `project` +signal initializer. + +## E. Exact CSS + +All of it goes into `webview-ui/agent-manager/agent-manager.css`. No changes to kilo-ui. + +### E.1 The inline selector + +Insert directly after the `.am-tab-switcher-pill-active` rule (agent-manager.css:3614-3617), +before the `/* Import tab layout */` comment at line 3619: + +```css +/* Project scope selector — inline with the New/Import tabs */ + +.am-nv-project-inline { + display: flex; + align-items: center; + flex-shrink: 0; + flex: 0 1 260px; + min-width: 0; + margin-left: auto; +} + +.am-nv-project-inline .am-selector-wrapper { + width: 100%; + min-width: 0; +} +``` + +The `flex: 0 1 260px` cap keeps the project control compact while allowing long project +names to truncate. `margin-left: auto` keeps it aligned to the right of the New/Import +buttons. + +### E.2 The dropdown rows + +Insert after the `.am-dropdown-empty` rule (agent-manager.css:3863-3868), before the +`/* Import empty state */` comment at line 3870: + +```css +/* Project option rows in the New Worktree project dropdown. + Deliberately distinct from .am-project-item, which styles the sidebar accordion. */ + +.am-project-option { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + width: 100%; + padding: 6px 8px; + border: none; + border-radius: var(--radius-sm); + background: none; + color: var(--text-base); + font-size: var(--font-size-base); + font-family: inherit; + text-align: left; + cursor: pointer; +} + +.am-project-option:hover:not(:disabled) { + background: var(--surface-inset-base-hover); +} + +.am-project-option-active { + background: var(--surface-inset-base); +} + +.am-project-option:disabled { + opacity: 0.5; + cursor: default; +} + +.am-project-option-left { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + flex: 1; +} + +.am-project-option-left [data-component="icon"] { + color: var(--text-weaker); + flex-shrink: 0; +} + +.am-project-option-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex-shrink: 0; + max-width: 45%; +} + +.am-project-option-root { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; + font-size: var(--kilo-font-size-11); + color: var(--text-weaker); +} + +``` + +Why not reuse `.am-branch-item`: it is defined twice (lines 3211 and 3810) and the earlier +definition sets `font-family: var(--font-mono, monospace)` on `.am-branch-item-name`, which +would render project labels in monospace. Reusing it also couples project rows to future +branch-row changes. `.am-project-item` is likewise off limits: it already styles the +sidebar project accordion header (line 325). + +### E.3 Popover clipping escape + +`[data-slot="dialog-body"]` is `overflow: hidden` in `packages/ui/src/components/dialog.css:99-105`, +and `[data-slot="dialog-content"]` is `overflow: auto` (line 38). The existing escape rules +at agent-manager.css:2822-2828 only match popovers **inside** `.am-nv-dialog`, and this row +is deliberately outside it. Without the following, the dropdown is clipped by the dialog. + +Add to that same rule group (extend the existing selector list rather than duplicating the +declaration): + +```css +[data-component="dialog"]:has(.am-nv-project-inline [data-component="popover-content"]) [data-slot="dialog-content"], +[data-component="dialog"]:has(.am-nv-project-inline [data-component="popover-content"]) [data-slot="dialog-body"] { + overflow: visible; +} +``` + +Verification step, not optional: open the dropdown with four or more projects registered +and confirm no row is cut off and no inner scrollbar appears on the dialog. If it still +clips, the documented fallback is to drop `portal={false}` from the `DeferredPopover` in +D.4 and delete this rule; the dialog already sets `overflow: visible` on +`[data-slot="dialog-content"]` for portal-based dropdowns (agent-manager.css:2755-2760). + +## F. i18n + +Add to `webview-ui/agent-manager/i18n/en.ts`, immediately after +`"agentManager.dialog.namePlaceholder"`: + +```ts +"agentManager.dialog.project.select": "Select project", +"agentManager.dialog.project.untrusted": "Trust this project in the sidebar first", +"agentManager.dialog.project.missing": "Repository not found", +``` + +Then add the same three keys to all 20 sibling locale files in that directory (`ar bs br da +de es fa fr it ja ko nl no pl ru th tr uk zh zht`) via the `translator` subagent. + +## G. What must not change + +- No new CSS variables or tokens. Only the ones listed above, all already in use in this + file. +- No edits to `packages/kilo-ui/` or `packages/ui/`. +- No change to `.am-project-item`, `.am-branch-item`, `.am-selector-trigger`, + `.am-nv-config-label`, or any other existing rule. The only existing rule touched is the + `overflow: visible` selector group in E.3, and only by adding selectors to it. +- No new message types. `agentManager.requestBranches`, `agentManager.createMultiVersion`, + `agentManager.importFromBranch`, and `agentManager.importFromPR` all already exist and + already accept what is needed. +- With `props.projects` empty, the rendered dialog markup must be identical to before the + change. Verify by toggling `kilo-code.new.experimental.multiProject` off. diff --git a/bun.lock b/bun.lock index 1a09d995841..50998be7c4d 100644 --- a/bun.lock +++ b/bun.lock @@ -345,7 +345,7 @@ }, "packages/kilo-jetbrains": { "name": "@kilocode/kilo-jetbrains", - "version": "7.4.17", + "version": "7.4.20", }, "packages/kilo-memory": { "name": "@kilocode/kilo-memory", @@ -444,6 +444,7 @@ "@opencode-ai/ui": "workspace:*", "@pierre/diffs": "catalog:", "@thisbeyond/solid-dnd": "0.7.5", + "@vscode/codicons": "^0.0.44", "@xterm/addon-clipboard": "0.2.0", "@xterm/addon-fit": "0.11.0", "@xterm/addon-unicode-graphemes": "0.4.0", @@ -469,6 +470,8 @@ }, "devDependencies": { "@axe-core/playwright": "4.11.3", + "@babel/core": "^7.28.4", + "@babel/preset-typescript": "^7.27.1", "@playwright/test": "1.57.0", "@storybook/addon-a11y": "10.2.10", "@storybook/addon-docs": "10.2.10", @@ -480,6 +483,7 @@ "@vscode/test-cli": "^0.0.12", "@vscode/test-electron": "^2.5.2", "@vscode/vsce": "^3.7.1", + "babel-preset-solid": "^1.9.9", "esbuild": "^0.27.2", "esbuild-plugin-solid": "^0.6.0", "eslint": "^9.39.2", @@ -983,23 +987,23 @@ }, }, "trustedDependencies": [ - "esbuild", - "protobufjs", "web-tree-sitter", + "esbuild", "tree-sitter-bash", + "protobufjs", ], "patchedDependencies": { - "@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch", - "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", + "virtua@0.49.1": "patches/virtua@0.49.1.patch", + "mammoth@1.12.0": "patches/mammoth@1.12.0.patch", "@ff-labs/fff-bun@0.9.4": "patches/@ff-labs%2Ffff-bun@0.9.4.patch", - "pacote@21.5.1": "patches/pacote@21.5.1.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", + "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", + "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", + "pacote@21.5.1": "patches/pacote@21.5.1.patch", + "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", "effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch", "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch", - "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", - "virtua@0.49.1": "patches/virtua@0.49.1.patch", - "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", - "mammoth@1.12.0": "patches/mammoth@1.12.0.patch", + "@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch", }, "overrides": { "@effect/platform-node-shared": "4.0.0-beta.74", @@ -5442,10 +5446,6 @@ "@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.74", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-Yx+Kh12U+i2FmjwEfKs+ePFmpMd43RPD1oGqc/VraSS9bYzvF0Ff3PojwEFEVEewp8xc92Uxu28gTspU4qyvHA=="], - - "@standard-community/standard-openapi/effect": ["effect@4.0.0-beta.74", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-Yx+Kh12U+i2FmjwEfKs+ePFmpMd43RPD1oGqc/VraSS9bYzvF0Ff3PojwEFEVEewp8xc92Uxu28gTspU4qyvHA=="], - "@storybook/addon-links/storybook": ["storybook@10.4.4", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2", "@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", "oxc-parser": "^0.127.0", "oxc-resolver": "^11.19.1", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.18.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "prettier": "^2 || ^3", "vite-plus": "^0.1.15" }, "optionalPeers": ["@types/react", "prettier", "vite-plus"], "bin": "./dist/bin/dispatcher.js" }, "sha512-Nn0qFRxU5fyABa6dGRftfL3lz0Y+HkKOaAkfytF8S4Q2K6Szwwq7TwPAEs3Wsj8hBQbYhsobrKADcPsyXQpJaA=="], "@storybook/addon-onboarding/storybook": ["storybook@10.4.4", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.2", "@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", "oxc-parser": "^0.127.0", "oxc-resolver": "^11.19.1", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", "ws": "^8.18.0" }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "prettier": "^2 || ^3", "vite-plus": "^0.1.15" }, "optionalPeers": ["@types/react", "prettier", "vite-plus"], "bin": "./dist/bin/dispatcher.js" }, "sha512-Nn0qFRxU5fyABa6dGRftfL3lz0Y+HkKOaAkfytF8S4Q2K6Szwwq7TwPAEs3Wsj8hBQbYhsobrKADcPsyXQpJaA=="], @@ -6152,14 +6152,6 @@ "@smithy/util-stream/@smithy/core/@smithy/types": ["@smithy/types@4.14.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YupL0ZWmFtJexUN2cHzkvvF/b9pKrtAIfT1o7/oY/Ppu8IYeZ+lDPM5vZdQJaSeA132dJCqojjGC9NhXeF71VQ=="], - "@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/yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], - - "@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/yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], - "@storybook/addon-links/storybook/@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], "@storybook/addon-links/storybook/@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], diff --git a/nix/hashes.json b/nix/hashes.json index 9ee5b06cf5e..52d88513d86 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-XCtBeP2R+Tx2S4LEXn1RRZWoquOUdIFB+8O58n2krHI=", - "aarch64-linux": "sha256-gnf+k+mI7JaqoCrVzFeEVBmKELUfZ9D6XpqzlBXKYzI=", - "aarch64-darwin": "sha256-j9nQgeurmHs00dGOIuxGoV1W/tQ/DJnWrfllr66y9yg=", - "x86_64-darwin": "sha256-+arUGf3HZvSRumkfX082dM0wErzzDexOz3zZcVDMlSI=" + "x86_64-linux": "sha256-y6PZR6BsVZcsK/qJ0XvJBevukYzVJ8NRLHRbNSuSzb8=", + "aarch64-linux": "sha256-0ozGLgGTHajlHqofixtjJv/dggq25MhWYIqmfmb+6Z0=", + "aarch64-darwin": "sha256-IEYJothLBDT20BmLamBqM+pciFMQtWy8Um1YjFjbKz4=", + "x86_64-darwin": "sha256-k2bQGKTkvIZmB2+I2Fy3TPm/NZ0BbGjxp9esxtGuUbI=" } } diff --git a/package.json b/package.json index 7d22cb0b8e9..ee3d5953d90 100644 --- a/package.json +++ b/package.json @@ -6,10 +6,11 @@ "type": "module", "packageManager": "bun@1.3.14", "scripts": { - "dev": "bun run --cwd packages/opencode --conditions=node src/index.ts", + "dev": "KILO_CLIENT=cli bun run --cwd packages/opencode --conditions=node src/index.ts", "dev:stats": "bun sst shell --stage=production -- bun run --cwd packages/stats/app dev", "dev:storybook": "bun --cwd packages/storybook storybook", "lint": "oxlint", + "check:architecture": "bun run script/check-architecture.ts", "typecheck": "bun turbo typecheck", "upgrade-opentui": "bun run script/upgrade-opentui.ts", "postinstall": "bun run --cwd packages/core fix-node-pty && bun run script/setup-git.ts", diff --git a/packages/client/src/generated-effect/client.ts b/packages/client/src/generated-effect/client.ts index 024c9782806..5eac8611d0e 100644 --- a/packages/client/src/generated-effect/client.ts +++ b/packages/client/src/generated-effect/client.ts @@ -531,6 +531,7 @@ type Endpoint14_1Input = { readonly cwd?: Endpoint14_1Request["payload"]["cwd"] readonly title?: Endpoint14_1Request["payload"]["title"] readonly env?: Endpoint14_1Request["payload"]["env"] + readonly size?: Endpoint14_1Request["payload"]["size"] } const Endpoint14_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint14_1Input) => raw["pty.create"]({ @@ -541,6 +542,7 @@ const Endpoint14_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint14_1Inpu cwd: input?.["cwd"], title: input?.["title"], env: input?.["env"], + size: input?.["size"], }, }).pipe(Effect.mapError(mapClientError)) diff --git a/packages/client/src/generated/client.ts b/packages/client/src/generated/client.ts index 5f6943c53e6..b17a7ebd9fe 100644 --- a/packages/client/src/generated/client.ts +++ b/packages/client/src/generated/client.ts @@ -840,6 +840,7 @@ export function make(options: ClientOptions) { cwd: input?.["cwd"], title: input?.["title"], env: input?.["env"], + size: input?.["size"], }, successStatus: 200, declaredStatuses: [400, 401], diff --git a/packages/client/src/generated/types.ts b/packages/client/src/generated/types.ts index 9d918786679..a96ecb5d5ee 100644 --- a/packages/client/src/generated/types.ts +++ b/packages/client/src/generated/types.ts @@ -2632,6 +2632,7 @@ export type PtysCreateInput = { readonly cwd?: string readonly title?: string readonly env?: { readonly [x: string]: string } + readonly size?: { readonly rows: number; readonly cols: number } }["command"] readonly args?: { readonly command?: string @@ -2639,6 +2640,7 @@ export type PtysCreateInput = { readonly cwd?: string readonly title?: string readonly env?: { readonly [x: string]: string } + readonly size?: { readonly rows: number; readonly cols: number } }["args"] readonly cwd?: { readonly command?: string @@ -2646,6 +2648,7 @@ export type PtysCreateInput = { readonly cwd?: string readonly title?: string readonly env?: { readonly [x: string]: string } + readonly size?: { readonly rows: number; readonly cols: number } }["cwd"] readonly title?: { readonly command?: string @@ -2653,6 +2656,7 @@ export type PtysCreateInput = { readonly cwd?: string readonly title?: string readonly env?: { readonly [x: string]: string } + readonly size?: { readonly rows: number; readonly cols: number } }["title"] readonly env?: { readonly command?: string @@ -2660,7 +2664,16 @@ export type PtysCreateInput = { readonly cwd?: string readonly title?: string readonly env?: { readonly [x: string]: string } + readonly size?: { readonly rows: number; readonly cols: number } }["env"] + readonly size?: { + readonly command?: string + readonly args?: ReadonlyArray + readonly cwd?: string + readonly title?: string + readonly env?: { readonly [x: string]: string } + readonly size?: { readonly rows: number; readonly cols: number } + }["size"] } export type PtysCreateOutput = { diff --git a/packages/core/src/credential.ts b/packages/core/src/credential.ts index bed9bb25314..104b9897eb4 100644 --- a/packages/core/src/credential.ts +++ b/packages/core/src/credential.ts @@ -15,6 +15,7 @@ import { Global } from "./global" import { DataMigrationTable } from "./data-migration.sql" import path from "path" import { parse as parseKiloAccounts } from "./kilocode/credential-migration" +import { isBusy } from "./kilocode/sqlite-error" import { NonNegativeInt } from "./schema" // kilocode_change end @@ -170,6 +171,17 @@ export const legacyImportLayer = Layer.effectDiscard( const integration = Integration.ID.make(integrationID.replace(/\/+$/, "")) return [{ integration, value: legacyValue(integration, decoded.value) }] }) + const migrated = yield* db.select().from(DataMigrationTable).where(eq(DataMigrationTable.name, name)).get() + const existing = yield* db.select().from(CredentialTable).orderBy(desc(CredentialTable.time_created)).all() + const same = (left: Value, right: Value) => JSON.stringify(left) === JSON.stringify(right) + if ( + migrated && + values.every((item) => { + const current = existing.find((row) => row.integration_id === item.integration) + return current !== undefined && same(current.value, item.value) + }) + ) + return yield* db.transaction((tx) => Effect.gen(function* () { for (const item of values) { @@ -181,7 +193,12 @@ export const legacyImportLayer = Layer.effectDiscard( .orderBy(desc(CredentialTable.time_created)) // kilocode_change - reconcile the active imported account .get() if (current) { - yield* tx.update(CredentialTable).set({ value: item.value }).where(eq(CredentialTable.id, current.id)).run() + if (!same(current.value, item.value)) + yield* tx + .update(CredentialTable) + .set({ value: item.value }) + .where(eq(CredentialTable.id, current.id)) + .run() continue } yield* tx.insert(CredentialTable).values({ @@ -194,7 +211,15 @@ export const legacyImportLayer = Layer.effectDiscard( yield* tx.insert(DataMigrationTable).values({ name, time_completed: Date.now() }).onConflictDoNothing().run() }), ) - }).pipe(Effect.orDie), + }).pipe( + Effect.retry({ while: isBusy, times: 2 }), + Effect.catch((error) => + isBusy(error) + ? Effect.logWarning("legacy credential reconciliation deferred because the database is busy") + : Effect.fail(error), + ), + Effect.orDie, + ), ) // kilocode_change end diff --git a/packages/core/src/cross-spawn-spawner.ts b/packages/core/src/cross-spawn-spawner.ts index 5888693f014..58fb29912ee 100644 --- a/packages/core/src/cross-spawn-spawner.ts +++ b/packages/core/src/cross-spawn-spawner.ts @@ -3,6 +3,7 @@ import { NodeFileSystem, NodeSink, NodeStream } from "@effect/platform-node" import * as NodePath from "@effect/platform-node/NodePath" import { prepareCommand as prepareSandbox } from "@kilocode/sandbox" // kilocode_change import { tap as tapStdio, tapped } from "./kilocode/stdio-tap" // kilocode_change - Bun drops buffered stdio on close +import * as SpawnExit from "./kilocode/spawn-exit" // kilocode_change import * as SpawnValidation from "./kilocode/spawn-validation" // kilocode_change import { settle } from "./kilocode/exit-code" // kilocode_change - settle signal termination as 128 + signum import * as Deferred from "effect/Deferred" @@ -268,7 +269,11 @@ export const make = Effect.gen(function* () { return { stdout, stderr, all: Stream.merge(stdout, stderr) } } - const spawn = (command: ChildProcess.StandardCommand, opts: NodeChildProcess.SpawnOptions) => + const spawn = ( + command: ChildProcess.StandardCommand, + opts: NodeChildProcess.SpawnOptions, + direct: boolean, // kilocode_change - avoid shadowing settle + ) => Effect.callback((resume) => { const signal = Deferred.makeUnsafe() const proc = launch(command.command, command.args, opts) @@ -280,6 +285,7 @@ export const make = Effect.gen(function* () { }) proc.on("exit", (...args) => { exit = args + if (direct) Deferred.doneUnsafe(signal, Exit.succeed(args)) // kilocode_change - bounded grep must not await inherited pipes }) proc.on("close", (...args) => { if (end) return @@ -326,6 +332,18 @@ export const make = Effect.gen(function* () { return Effect.fail(toPlatformError("kill", new Error("Failed to kill child process"), command)) }) + // kilocode_change start - inspect descendants owned by commands that settle on direct exit + const groupAlive = (proc: NodeChildProcess.ChildProcess) => { + if (process.platform === "win32") return false + try { + process.kill(-proc.pid!, 0) + return true + } catch { + return false + } + } + // kilocode_change end + const timeout = ( proc: NodeChildProcess.ChildProcess, @@ -370,6 +388,7 @@ export const make = Effect.gen(function* () { switch (command._tag) { case "StandardCommand": { const validation = SpawnValidation.take(command) // kilocode_change - retain target validation through preparation + const direct = SpawnExit.take(command) // kilocode_change - opt selected commands into direct-exit settlement const dir = yield* cwd(command.options) // kilocode_change start - prepare agent-scoped commands through the selected sandbox backend const target = yield* prepareSandbox(command, dir, env(command.options)) @@ -396,21 +415,33 @@ export const make = Effect.gen(function* () { const [proc, signal] = yield* Effect.acquireRelease( // kilocode_change start - spawn the prepared command and options - spawn(target, { - cwd: dir, - env: env(target.options), - stdio: stdios(sin, sout, serr, extra), - detached: target.options.detached ?? process.platform !== "win32", - shell: target.options.shell, - // kilocode_change end - windowsHide: process.platform === "win32", - }), + spawn( + target, + { + cwd: dir, + env: env(target.options), + stdio: stdios(sin, sout, serr, extra), + detached: target.options.detached ?? process.platform !== "win32", + shell: target.options.shell, + // kilocode_change end + windowsHide: process.platform === "win32", + }, + direct, // kilocode_change + ), Effect.fnUntraced(function* ([proc, signal]) { const done = yield* Deferred.isDone(signal) const kill = timeout(proc, command, target.options) // kilocode_change if (done) { const [code] = yield* Deferred.await(signal) if (process.platform === "win32") return yield* Effect.void + // kilocode_change start - clean up only descendants owned by direct-settling commands + if (direct && groupAlive(proc)) { + yield* Effect.ignore(killGroup(command, proc, target.options.killSignal ?? "SIGTERM")) + yield* Effect.sleep("100 millis") + if (groupAlive(proc)) yield* Effect.ignore(killGroup(command, proc, "SIGKILL")) + return yield* Effect.void + } + // kilocode_change end if (code !== 0 && Predicate.isNotNull(code)) return yield* Effect.ignore(kill(killGroup)) return yield* Effect.void } diff --git a/packages/core/src/kilocode/database-compat.ts b/packages/core/src/kilocode/database-compat.ts index 9982d3d7285..733b2eece88 100644 --- a/packages/core/src/kilocode/database-compat.ts +++ b/packages/core/src/kilocode/database-compat.ts @@ -4,19 +4,29 @@ import type { Database } from "../database/database" type Db = Database.Interface["db"] export function ensure(db: Db) { - return db.transaction( - (tx) => - Effect.gen(function* () { - const rows = yield* tx.all<{ name: string }>("PRAGMA table_info('session_context_epoch')") - const names = new Set(rows.map((row) => row.name)) + const load = db.all<{ name: string }>("PRAGMA table_info('session_context_epoch')") + const ready = (rows: { name: string }[]) => { + const names = new Set(rows.map((row) => row.name)) + return ["agent", "replacement_seq", "revision"].every((name) => names.has(name)) + } + return load.pipe( + Effect.flatMap((rows) => { + if (ready(rows)) return Effect.void + return db.transaction( + (tx) => + Effect.gen(function* () { + const current = yield* tx.all<{ name: string }>("PRAGMA table_info('session_context_epoch')") + const names = new Set(current.map((row) => row.name)) - if (!names.has("agent")) - yield* tx.run("ALTER TABLE `session_context_epoch` ADD `agent` text DEFAULT 'build' NOT NULL") - if (!names.has("replacement_seq")) - yield* tx.run("ALTER TABLE `session_context_epoch` ADD `replacement_seq` integer") - if (!names.has("revision")) - yield* tx.run("ALTER TABLE `session_context_epoch` ADD `revision` integer DEFAULT 0 NOT NULL") - }), - { behavior: "immediate" }, + if (!names.has("agent")) + yield* tx.run("ALTER TABLE `session_context_epoch` ADD `agent` text DEFAULT 'build' NOT NULL") + if (!names.has("replacement_seq")) + yield* tx.run("ALTER TABLE `session_context_epoch` ADD `replacement_seq` integer") + if (!names.has("revision")) + yield* tx.run("ALTER TABLE `session_context_epoch` ADD `revision` integer DEFAULT 0 NOT NULL") + }), + { behavior: "immediate" }, + ) + }), ) } diff --git a/packages/core/src/kilocode/ripgrep-grep.ts b/packages/core/src/kilocode/ripgrep-grep.ts new file mode 100644 index 00000000000..cac163c7f5d --- /dev/null +++ b/packages/core/src/kilocode/ripgrep-grep.ts @@ -0,0 +1,50 @@ +import type { Match } from "@opencode-ai/schema/filesystem" + +export interface Options { + readonly context?: number + readonly literal?: boolean + readonly ignoreCase?: boolean +} + +export type GrepMatch = Match & { + readonly context: boolean + readonly textTruncated: boolean +} + +export const flags = (input: Options) => [ + ...(input.literal ? ["--fixed-strings"] : []), + ...(input.ignoreCase ? ["--ignore-case"] : []), + ...(input.context ? [`--context=${input.context}`] : []), +] + +export const stop = (limit: number) => { + let matches = 0 + return (row: { readonly context: boolean }) => !row.context && ++matches > limit +} + +export const select = < + A extends { + readonly context: boolean + readonly path: { readonly text: string } + readonly line_number: number + }, +>( + input: { readonly limit: number; readonly context?: number }, + items: readonly A[], +) => { + let count = 0 + const overflow = items.findIndex((row) => !row.context && ++count > input.limit) + const selected = items.slice(0, overflow === -1 ? items.length : overflow) + const matches = selected.filter((row) => !row.context) + return selected.filter( + (row) => + !row.context || + matches.some( + (match) => + match.path.text === row.path.text && Math.abs(match.line_number - row.line_number) <= (input.context ?? 0), + ), + ) +} + +export const decorate = (match: Match, context: boolean, textTruncated: boolean): GrepMatch => + Object.assign(match, { context, textTruncated }) diff --git a/packages/core/src/kilocode/spawn-exit.ts b/packages/core/src/kilocode/spawn-exit.ts new file mode 100644 index 00000000000..3bd4c0f88a9 --- /dev/null +++ b/packages/core/src/kilocode/spawn-exit.ts @@ -0,0 +1,14 @@ +import type { ChildProcess } from "effect/unstable/process" + +const commands = new WeakSet() + +export function attach(command: ChildProcess.StandardCommand) { + commands.add(command) + return command +} + +export function take(command: ChildProcess.StandardCommand) { + const found = commands.has(command) + commands.delete(command) + return found +} diff --git a/packages/core/src/kilocode/sqlite-error.ts b/packages/core/src/kilocode/sqlite-error.ts new file mode 100644 index 00000000000..60f52b531c0 --- /dev/null +++ b/packages/core/src/kilocode/sqlite-error.ts @@ -0,0 +1,10 @@ +import { Cause, Option } from "effect" +import { isSqlError } from "effect/unstable/sql/SqlError" + +export function isBusy(error: unknown): boolean { + if (isSqlError(error)) return error.reason._tag === "LockTimeoutError" + if (typeof error !== "object" || error === null || !("cause" in error) || error.cause === error) return false + if (!Cause.isCause(error.cause)) return isBusy(error.cause) + const failure = Cause.findErrorOption(error.cause) + return Option.isSome(failure) && isBusy(failure.value) +} diff --git a/packages/core/src/pty.ts b/packages/core/src/pty.ts index 44d95cb5085..97f41bcd54b 100644 --- a/packages/core/src/pty.ts +++ b/packages/core/src/pty.ts @@ -214,7 +214,17 @@ const layer = Layer.effect( } yield* Effect.logInfo("creating session", { id, cmd: command, args, cwd }) const { spawn } = yield* Effect.promise(() => pty()) - const proc = yield* Effect.sync(() => spawn(command, args, { name: "xterm-256color", cwd, env })) + // kilocode_change start - spawn with initial terminal dimensions + const proc = yield* Effect.sync(() => + spawn(command, args, { + name: "xterm-256color", + cwd, + env, + cols: input.size?.cols, + rows: input.size?.rows, + }), + ) + // kilocode_change end const info: Info = { id, title: input.title || `Terminal ${id.slice(-4)}`, diff --git a/packages/core/src/ripgrep.ts b/packages/core/src/ripgrep.ts index 5361c083a43..71ee912e249 100644 --- a/packages/core/src/ripgrep.ts +++ b/packages/core/src/ripgrep.ts @@ -1,9 +1,11 @@ export * as Ripgrep from "./ripgrep" -import { Context, Effect, Fiber, Layer, Schema, Stream } from "effect" +import { Context, Duration, Effect, Fiber, Layer, Schema, Stream } from "effect" import { ChildProcess } from "effect/unstable/process" import { makeGlobalNode } from "./effect/app-node" import { Entry, Match } from "@opencode-ai/schema/filesystem" +import * as KiloGrep from "./kilocode/ripgrep-grep" // kilocode_change +import * as SpawnExit from "./kilocode/spawn-exit" // kilocode_change import * as SpawnValidation from "./kilocode/spawn-validation" // kilocode_change import { AppProcess, collectStream, waitForAbort } from "./process" import { NonNegativeInt, PositiveInt, RelativePath } from "./schema" @@ -21,7 +23,7 @@ const MAX_RECORD_BYTES = 64 * 1024 const MAX_SUBMATCHES = 100 const RawMatch = Schema.Struct({ - type: Schema.Literal("match"), + type: Schema.Literals(["match", "context"]), // kilocode_change - retain requested context records data: Schema.Struct({ path: Schema.Struct({ text: Schema.String }), lines: Schema.Struct({ text: Schema.String }), @@ -37,7 +39,7 @@ const RawMatch = Schema.Struct({ }), }) -type RawMatchData = (typeof RawMatch.Type)["data"] +type RawMatchData = (typeof RawMatch.Type)["data"] & { readonly context: boolean } // kilocode_change export class Error extends Schema.TaggedErrorClass()("Ripgrep.Error", { message: Schema.String, @@ -69,7 +71,8 @@ export interface GlobInput { readonly validate?: Effect.Effect // kilocode_change - bind approved searches at spawn } -export interface GrepInput { +export interface GrepInput extends KiloGrep.Options { + // kilocode_change readonly cwd: string readonly pattern: string readonly file?: string @@ -82,7 +85,7 @@ export interface GrepInput { export interface Interface { readonly find: (input: FindInput) => Effect.Effect readonly glob: (input: GlobInput) => Effect.Effect, Error> // kilocode_change - readonly grep: (input: GrepInput) => Effect.Effect, Error | InvalidPatternError> // kilocode_change + readonly grep: (input: GrepInput) => Effect.Effect, Error | InvalidPatternError> // kilocode_change } // kilocode_change start - retain truncation state through model-facing tools @@ -114,6 +117,7 @@ const layer = Layer.effect( readonly parse: (line: string) => Effect.Effect readonly pattern?: string readonly onItem?: (item: A) => Effect.Effect + readonly stop?: (item: A) => boolean // kilocode_change - stop bounded searches at the overflow match readonly validate?: Effect.Effect // kilocode_change - spawn-bound target validation }) => { const program = Effect.scoped( @@ -124,16 +128,24 @@ const layer = Layer.effect( cwd: input.cwd, extendEnv: true, stdin: "ignore", + forceKillAfter: input.stop ? Duration.seconds(1) : undefined, // kilocode_change - bound grep interruption }) - const handle = yield* process.spawn( - input.validate ? SpawnValidation.attach(command, input.validate) : command, - ) + const validated = input.validate ? SpawnValidation.attach(command, input.validate) : command + const spawned = input.stop ? SpawnExit.attach(validated) : validated // kilocode_change + const handle = yield* process.spawn(spawned) // kilocode_change end const stderrFiber = yield* collectStream(handle.stderr, ERROR_BYTES).pipe( Effect.map((output) => output.buffer.toString("utf8")), Effect.forkScoped, ) let observed = 0 + let stopped = false // kilocode_change + const take = input.stop // kilocode_change start + ? Stream.takeUntil((row) => { + stopped = input.stop?.(row) ?? false + return stopped + }) + : Stream.take(input.limit + 1) // kilocode_change end const rows = yield* Stream.decodeText(handle.stdout).pipe( Stream.splitLines, Stream.filter((line) => line.length > 0), @@ -143,11 +155,12 @@ const layer = Layer.effect( if (!input.onItem || observed++ >= input.limit) return Effect.void return input.onItem(row) }), - Stream.take(input.limit + 1), + take, // kilocode_change Stream.runCollect, Effect.map((chunk) => [...chunk]), ) - const truncated = rows.length > input.limit + if (stopped) return { items: rows, truncated: true, partial: false } // kilocode_change + const truncated = input.stop ? false : rows.length > input.limit // kilocode_change - custom stop owns truncation if (truncated) return { items: rows.slice(0, input.limit), truncated, partial: false } const code = yield* handle.exitCode @@ -244,11 +257,13 @@ const layer = Layer.effect( grep: (input) => run({ ...input, + stop: KiloGrep.stop(input.limit), // kilocode_change args: [ "--no-config", "--json", "--hidden", "--no-messages", + ...KiloGrep.flags(input), // kilocode_change ...(input.include ? [`--glob=${input.include}`] : []), "--glob=!**/.git/**", "--", @@ -264,13 +279,19 @@ const layer = Layer.effect( }) ).pipe( Effect.flatMap((json) => { - if (!json || typeof json !== "object" || !("type" in json) || json.type !== "match") + if ( + !json || + typeof json !== "object" || + !("type" in json) || + (json.type !== "match" && json.type !== "context") // kilocode_change + ) return Effect.succeed(undefined) return Schema.decodeUnknownEffect(RawMatch)(json).pipe( Effect.map((match) => ({ ...match.data, path: { text: match.data.path.text.replace(/^\.[\\/]/, "") }, submatches: match.data.submatches.slice(0, MAX_SUBMATCHES), + context: match.type === "context", // kilocode_change })), Effect.mapError((cause) => failure("Invalid ripgrep match output", cause)), ) @@ -280,12 +301,12 @@ const layer = Layer.effect( // kilocode_change start - retain spawn metadata after mapping matches Effect.map((result) => ({ ...result, - items: result.items.map((match) => { + items: KiloGrep.select(input, result.items).map((match) => { const relative = match.path.text .replace(/^(?:\.[\\/])+/u, "") .replace(/^[\\/]+/u, "") .replaceAll("\\", "/") - return Match.make({ + const item = Match.make({ entry: Entry.make({ path: RelativePath.make(relative), type: "file", @@ -299,6 +320,7 @@ const layer = Layer.effect( end: submatch.end, })), }) + return KiloGrep.decorate(item, match.context, match.lines.text.length > 2_000) }), })), // kilocode_change end diff --git a/packages/core/test/credential.test.ts b/packages/core/test/credential.test.ts index e6cbf820bb1..164f297cbc7 100644 --- a/packages/core/test/credential.test.ts +++ b/packages/core/test/credential.test.ts @@ -1,11 +1,15 @@ import path from "path" +import { Database as SQLite } from "bun:sqlite" // kilocode_change import { describe, expect } from "bun:test" +import { eq } from "drizzle-orm" // kilocode_change import { Effect, Layer } from "effect" import { Credential } from "@opencode-ai/core/credential" +import { CredentialTable } from "@opencode-ai/core/credential/sql" // kilocode_change import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { Integration } from "@opencode-ai/core/integration" // kilocode_change start import { Database } from "@opencode-ai/core/database/database" +import { FSUtil } from "@opencode-ai/core/fs-util" import { Global } from "@opencode-ai/core/global" // kilocode_change end import { tmpdir } from "./fixture/tmpdir" @@ -20,6 +24,16 @@ function localLayer(directory: string) { ) } +// kilocode_change start +function importer(dir: string, store: Database.Interface) { + return Credential.legacyImportLayer.pipe( + Layer.provide(Layer.succeed(Database.Service, store)), + Layer.provide(FSUtil.defaultLayer), + Layer.provide(Global.layerWith({ data: dir })), + ) +} +// kilocode_change end + describe("Credential", () => { it.live("stores, updates, lists, and removes credentials", () => Effect.acquireUseRelease( @@ -196,6 +210,70 @@ describe("Credential", () => { ), ) + it.live("skips unchanged legacy writes and defers locked reconciliation", () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => { + const file = path.join(tmp.path, "credential.db") + const auth = path.join(tmp.path, "auth.json") + const write = (key: string) => + Effect.promise(() => Bun.write(auth, JSON.stringify({ kilo: { type: "api", key } }))) + return Effect.gen(function* () { + yield* write("first") + const store = yield* Database.Service + const layer = importer(tmp.path, store) + yield* Layer.build(Layer.fresh(layer)) + + const before = yield* store.db + .select() + .from(CredentialTable) + .where(eq(CredentialTable.integration_id, Integration.ID.make("kilo"))) + .get() + yield* Layer.build(Layer.fresh(layer)) + const unchanged = yield* store.db + .select() + .from(CredentialTable) + .where(eq(CredentialTable.integration_id, Integration.ID.make("kilo"))) + .get() + expect(unchanged?.time_updated).toBe(before?.time_updated) + + yield* write("second") + yield* store.db.run("PRAGMA busy_timeout = 0") + yield* Effect.acquireUseRelease( + Effect.sync(() => { + const holder = new SQLite(file) + holder.run("PRAGMA busy_timeout = 0") + holder.run("BEGIN IMMEDIATE") + return holder + }), + () => Layer.build(Layer.fresh(layer)), + (holder) => + Effect.sync(() => { + if (holder.inTransaction) holder.run("ROLLBACK") + holder.close() + }), + ) + + const stale = yield* store.db + .select() + .from(CredentialTable) + .where(eq(CredentialTable.integration_id, Integration.ID.make("kilo"))) + .get() + expect(stale?.value).toMatchObject({ type: "key", key: "first" }) + + yield* Layer.build(Layer.fresh(layer)) + const reconciled = yield* store.db + .select() + .from(CredentialTable) + .where(eq(CredentialTable.integration_id, Integration.ID.make("kilo"))) + .get() + expect(reconciled?.value).toMatchObject({ type: "key", key: "second" }) + }).pipe(Effect.provide(Database.layerFromPath(file)), Effect.scoped) + }, + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), + ) + it.live("dual-writes stored credentials for released auth.json readers", () => Effect.acquireRelease( Effect.promise(() => tmpdir()), diff --git a/packages/core/test/kilocode/database-migration-compat.test.ts b/packages/core/test/kilocode/database-migration-compat.test.ts index 8546df16b37..83d35cf1666 100644 --- a/packages/core/test/kilocode/database-migration-compat.test.ts +++ b/packages/core/test/kilocode/database-migration-compat.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test" +import { Database as SQLite } from "bun:sqlite" import { SqliteClient } from "@effect/sql-sqlite-bun" import { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite" import { DatabaseMigration } from "@opencode-ai/core/database/migration" @@ -159,6 +160,22 @@ describe("database migration compatibility", () => { sql`SELECT agent, replacement_seq AS replacementSeq, revision FROM session_context_epoch WHERE session_id = 'session'`, ), ).toEqual({ agent: "build", replacementSeq: 4, revision: 1 }) + + yield* db.run("PRAGMA busy_timeout = 0") + yield* Effect.acquireUseRelease( + Effect.sync(() => { + const holder = new SQLite(filename) + holder.run("PRAGMA busy_timeout = 0") + holder.run("BEGIN IMMEDIATE") + return holder + }), + () => ensure(db), + (holder) => + Effect.sync(() => { + if (holder.inTransaction) holder.run("ROLLBACK") + holder.close() + }), + ) }), ).pipe(Effect.provide(Database.layerFromPath(filename)), Effect.scoped), ) diff --git a/packages/core/test/kilocode/ripgrep-settlement.test.ts b/packages/core/test/kilocode/ripgrep-settlement.test.ts new file mode 100644 index 00000000000..955aa424a08 --- /dev/null +++ b/packages/core/test/kilocode/ripgrep-settlement.test.ts @@ -0,0 +1,208 @@ +import { describe, expect } from "bun:test" +import fs from "node:fs/promises" +import path from "node:path" +import { Effect, Fiber, Layer } from "effect" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" +import { Ripgrep } from "@opencode-ai/core/ripgrep" +import { RipgrepBinary } from "@opencode-ai/core/ripgrep/binary" +import { tmpdir } from "../fixture/tmpdir" +import { it } from "../lib/effect" + +const record = (type: "match" | "context", line: number, text: string) => + JSON.stringify({ + type, + data: { + path: { text: "fixture.ts" }, + lines: { text: `${text}\n` }, + line_number: line, + absolute_offset: line * 10, + submatches: type === "match" ? [{ match: { text: "NEEDLE.*" }, start: 0, end: 8 }] : [], + }, + }) + +const alive = (pid: number) => { + try { + process.kill(pid, 0) + return true + } catch { + return false + } +} + +const read = async (file: string) => { + const deadline = Date.now() + 1_000 + while (Date.now() < deadline) { + const value = await fs.readFile(file, "utf8").catch(() => undefined) + if (value) return value + await Bun.sleep(10) + } + throw new Error(`Timed out waiting for ${file}`) +} + +const cleanup = async (file: string) => { + const pid = Number(await fs.readFile(file, "utf8").catch(() => "")) + if (!pid || !alive(pid)) return + try { + process.kill(pid, "SIGKILL") + } catch (err) { + if (alive(pid)) throw err + } +} + +const gone = async (pid: number) => { + const deadline = Date.now() + 1_000 + while (Date.now() < deadline) { + if (!alive(pid)) return true + await Bun.sleep(10) + } + return !alive(pid) +} + +const fixture = async (dir: string, source: string) => { + if (process.platform !== "win32") { + const binary = path.join(dir, "rg") + await fs.writeFile(binary, `#!${process.execPath}\n${source}`, { mode: 0o755 }) + return binary + } + + const script = path.join(dir, "fake-rg.cjs") + const binary = path.join(dir, "rg.cmd") + await fs.writeFile(script, source) + await fs.writeFile(binary, `@echo off\r\n"${process.execPath}" "%~dp0fake-rg.cjs" %*\r\n`) + return binary +} + +const layer = (binary: string) => + LayerNode.compile(Ripgrep.node, [ + [ + RipgrepBinary.node, + Layer.succeed(RipgrepBinary.Service, RipgrepBinary.Service.of({ filepath: Effect.succeed(binary) })), + ], + ] as const) + +describe("Kilo ripgrep settlement", () => { + it.live( + "settles a bounded parameterized grep when inherited output stays open", + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => + Effect.gen(function* () { + const retained = path.join(tmp.path, "retained.pid") + const owned = path.join(tmp.path, "owned.pid") + const args = path.join(tmp.path, "args.json") + const output = + [ + record("context", 1, "before"), + record("match", 2, "NEEDLE.*"), + record("context", 3, "after"), + record("context", 5, "later before"), + record("match", 6, "NEEDLE.*"), + ].join("\n") + "\n" + const source = `const { spawn } = require("node:child_process") +const { writeFileSync, writeSync } = require("node:fs") +const retained = spawn(process.execPath, ["-e", "setTimeout(() => process.exit(0), 30_000)"], { + detached: true, + stdio: ["ignore", "inherit", "inherit"], +}) +retained.unref() +${ + process.platform === "win32" + ? "" + : `const owned = spawn(process.execPath, ["-e", "setTimeout(() => process.exit(0), 30_000)"], { + stdio: "ignore", +}) +owned.unref()` +} +writeFileSync(${JSON.stringify(retained)}, String(retained.pid)) +${process.platform === "win32" ? "" : `writeFileSync(${JSON.stringify(owned)}, String(owned.pid))`} +writeFileSync(${JSON.stringify(args)}, JSON.stringify(process.argv.slice(2))) +writeSync(1, ${JSON.stringify(output)}) +` + const binary = yield* Effect.promise(() => fixture(tmp.path, source)) + + const result = yield* Ripgrep.Service.pipe( + Effect.flatMap((ripgrep) => + ripgrep.grep({ + cwd: tmp.path, + pattern: "NEEDLE.*", + include: "*.ts", + context: 1, + limit: 1, + literal: true, + ignoreCase: true, + }), + ), + Effect.provide(layer(binary)), + Effect.timeout("5 seconds"), + ) + const pid = Number(yield* Effect.promise(() => read(retained))) + const passed: unknown = JSON.parse(yield* Effect.promise(() => read(args))) + if (!Array.isArray(passed) || !passed.every((arg) => typeof arg === "string")) { + throw new Error("Fake ripgrep did not capture string arguments") + } + + expect(result.truncated).toBe(true) + expect(result.items.map((item) => [item.context, item.line, item.text.trim()])).toEqual([ + [true, 1, "before"], + [false, 2, "NEEDLE.*"], + [true, 3, "after"], + ]) + expect(passed).toContain("--fixed-strings") + expect(passed).toContain("--ignore-case") + expect(passed).toContain("--context=1") + expect(passed).toContain("--glob=*.ts") + expect(alive(pid)).toBe(true) + if (process.platform !== "win32") { + const child = Number(yield* Effect.promise(() => read(owned))) + expect(yield* Effect.promise(() => gone(child))).toBe(true) + } + }), + (tmp) => + Effect.promise(async () => { + await cleanup(path.join(tmp.path, "retained.pid")) + await cleanup(path.join(tmp.path, "owned.pid")) + await tmp[Symbol.asyncDispose]() + }), + ), + 10_000, + ) + + it.live( + "force kills a bounded grep that does not exit after cancellation", + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => + Effect.gen(function* () { + const ready = path.join(tmp.path, "ready.pid") + const source = `const { writeFileSync } = require("node:fs") +if (process.platform !== "win32") process.on("SIGTERM", () => {}) +writeFileSync(${JSON.stringify(ready)}, String(process.pid)) +setInterval(() => {}, 10_000) +` + const binary = yield* Effect.promise(() => fixture(tmp.path, source)) + + const controller = new AbortController() + const fiber = yield* Ripgrep.Service.pipe( + Effect.flatMap((ripgrep) => + ripgrep.grep({ cwd: tmp.path, pattern: "needle", context: 1, limit: 1, signal: controller.signal }), + ), + Effect.provide(layer(binary)), + Effect.exit, + Effect.forkScoped, + ) + const pid = Number(yield* Effect.promise(() => read(ready))) + controller.abort() + const exit = yield* Fiber.join(fiber).pipe(Effect.timeout("5 seconds")) + + expect(exit._tag).toBe("Failure") + expect(alive(pid)).toBe(false) + }), + (tmp) => + Effect.promise(async () => { + await cleanup(path.join(tmp.path, "ready.pid")) + await tmp[Symbol.asyncDispose]() + }), + ), + 10_000, + ) +}) diff --git a/packages/effect-drizzle-sqlite/src/sqlite-core/effect/session.ts b/packages/effect-drizzle-sqlite/src/sqlite-core/effect/session.ts index 15a56f2ca7d..535b232ac4b 100644 --- a/packages/effect-drizzle-sqlite/src/sqlite-core/effect/session.ts +++ b/packages/effect-drizzle-sqlite/src/sqlite-core/effect/session.ts @@ -279,7 +279,13 @@ export class SQLiteEffectPreparedQuery< assertUnreachable(cacheStrat) }).pipe( Effect.catch((e) => { - return Effect.fail(new EffectDrizzleQueryError({ query: queryString, params, cause: Cause.fail(e) })) + return Effect.fail( + new EffectDrizzleQueryError({ + query: queryString, + params: params.map(() => ""), // kilocode_change - bound values may contain credentials + cause: Cause.fail(e), + }), + ) }), ) } diff --git a/packages/effect-drizzle-sqlite/test/sqlite.test.ts b/packages/effect-drizzle-sqlite/test/sqlite.test.ts index 5303ee069ac..0148a1b9d4d 100644 --- a/packages/effect-drizzle-sqlite/test/sqlite.test.ts +++ b/packages/effect-drizzle-sqlite/test/sqlite.test.ts @@ -130,6 +130,24 @@ test("preserves failed transaction begin errors", async () => { } }) +// kilocode_change start - query errors must never expose bound credential values +test("redacts bound values from query errors", async () => { + await run( + Effect.gen(function* () { + const db = yield* makeDb + const secret = "must-not-leak" + yield* db.insert(users).values({ id: 1, name: "Ada" }) + + const error = yield* db.insert(users).values({ id: 1, name: secret }).pipe(Effect.flip) + + expect(error.message).not.toContain(secret) + expect(error.params).not.toContain(secret) + expect(error.params.every((param) => param === "")).toBe(true) + }), + ) +}) +// kilocode_change end + test("supports returning and rejects empty update sets", async () => { await run( Effect.gen(function* () { diff --git a/packages/kilo-docs/markdoc/partials/cli-commands-table.md b/packages/kilo-docs/markdoc/partials/cli-commands-table.md index 0ac95fd5548..d8653a898a6 100644 --- a/packages/kilo-docs/markdoc/partials/cli-commands-table.md +++ b/packages/kilo-docs/markdoc/partials/cli-commands-table.md @@ -13,7 +13,6 @@ | `kilo upgrade [target]` | upgrade kilo to the latest or a specific version | | `kilo uninstall` | uninstall kilo and remove all related files | | `kilo serve` | starts a headless kilo server | -| `kilo web` | start kilo server and open web interface | | `kilo models [provider]` | list all available models | | `kilo roll-call ` | batch-test text models matching a filter for connectivity and latency | | `kilo profile` | show Kilo account profile | diff --git a/packages/kilo-docs/markdoc/partials/install-jetbrains.md b/packages/kilo-docs/markdoc/partials/install-jetbrains.md index b8f163a95e6..7cf7e70ea9d 100644 --- a/packages/kilo-docs/markdoc/partials/install-jetbrains.md +++ b/packages/kilo-docs/markdoc/partials/install-jetbrains.md @@ -14,6 +14,31 @@ The JetBrains plugin provides the best native JetBrains UX for working with an A {% image src="/docs/img/jetbrains/plugin-auto-updates.png" alt="JetBrains Updates settings with Update plugins automatically enabled" width="900" caption="Enable automatic plugin updates to receive Kilo Code fixes and improvements." /%} +### Install with bundled Kilo Core + +The Marketplace build is best for most users. Use the bundled Kilo Core build when your IDE cannot download the Kilo Core runtime after installation, such as on locked-down corporate networks, behind strict proxy or TLS inspection, in offline development environments, or where corporate policy blocks applications from downloading executables at runtime. + +The bundled build ships the JetBrains plugin with Kilo Core included. The install is larger, but first launch does not need a separate runtime download. + +1. Open **Settings → Plugins** +2. Click the gear icon and choose **Manage Plugin Repositories...** +3. Click **+** and add the Kilo Code repository URL: + + ```text + https://kilo-org.github.io/kilocode/jetbrains/updatePlugins.xml + ``` + +4. Click **OK**, then install or update **Kilo Code** from **Settings → Plugins** +5. Restart the IDE if prompted + +{% image src="/docs/img/jetbrains/plugin-custom-repository-menu.png" alt="JetBrains Plugins settings with Manage Plugin Repositories selected from the gear menu" width="900" caption="Open Manage Plugin Repositories from the Plugins settings gear menu." /%} + +{% image src="/docs/img/jetbrains/plugin-custom-repository-url.png" alt="JetBrains Custom Plugin Repositories dialog with the Kilo Code repository URL added" width="700" caption="Add the Kilo Code custom plugin repository URL." /%} + +After restart, open the **Kilo Code** tool window and choose **... → Core**. The menu footer should show **Bundled Core** with the version and architecture. + +{% image src="/docs/img/jetbrains/plugin-bundled-core.png" alt="Kilo Code tool window Core menu showing Bundled Core and the current architecture" width="900" caption="Confirm that the plugin is using Bundled Core." /%} + ### If you used the v7 EAP {% #jetbrains-early-access %} {% callout type="info" %} diff --git a/packages/kilo-docs/pages/automate/agent-manager.md b/packages/kilo-docs/pages/automate/agent-manager.md index 753f9dd3421..aab5652ec91 100644 --- a/packages/kilo-docs/pages/automate/agent-manager.md +++ b/packages/kilo-docs/pages/automate/agent-manager.md @@ -232,7 +232,7 @@ See [Agent Manager Workflows](/docs/automate/agent-manager-workflows#merging-wor ## Terminals -Each session has a dedicated terminal rooted in the session's worktree directory. Press `Cmd+/` (macOS) / `Ctrl+/` (Windows/Linux) to focus the terminal for the active session. +Each session has a dedicated terminal rooted in the session's worktree directory. Press `Cmd+/` (macOS) / `Ctrl+/` (Windows/Linux) to focus the terminal for the active session. If the embedded terminal is already visible but the prompt has focus, the same shortcut focuses the terminal without hiding it. Press it again while the terminal has focus to hide the panel. ### Choosing the Terminal Destination @@ -243,11 +243,18 @@ The toolbar's terminal button is a split button: click it to open a terminal, or The dropdown choice is remembered per panel and becomes the default for new panels. You can also set the default directly with the `kilo-code.new.agentManager.terminalButtonDestination` setting (`vscode` or `agentManager`). The `Cmd+/` (macOS) / `Ctrl+/` (Windows/Linux) shortcut follows the same destination. -With the **Agent Manager panel** destination, the terminal works like the diff panel: press `Cmd+/` to reveal it and press again to hide it. Hiding never stops the terminal — scrollback and running processes continue in the background, and focus returns to the chat input. A terminal stops only when you close its tab in the panel. +With the **Agent Manager panel** destination, the terminal works like the diff panel: press `Cmd+/` to reveal and focus it, press it while the panel is visible but another control has focus to move focus into the terminal, and press it again from the terminal to hide it. Hiding never stops the terminal — scrollback and running processes continue in the background, and focus returns to the chat input. A terminal stops only when you click its close button or type `exit` in the shell. ### Multiple Terminals -The side panel hosts multiple terminals per context (the local workspace or a worktree). The panel header is a tab strip: click a tab to switch, click **+** to open another terminal, and click **X** (or middle-click) to close a single terminal. Drag tabs to reorder them. Closing a terminal no longer hides the panel — closing the last one lands on the empty state. Pressing `Cmd+W` (macOS) / `Ctrl+W` (Windows/Linux) with a focused side terminal closes exactly that terminal. +Agent Manager has two separate terminal tab strips: + +- **Main terminal tabs** appear alongside the agent session tabs. With the prompt or a main terminal focused, press `Cmd+Shift+T` / `Ctrl+Shift+T` to create another main terminal tab. +- **Side terminal tabs** appear in the terminal panel. Focus a side terminal, then press `Cmd+Shift+T` / `Ctrl+Shift+T` to create another side terminal. You can also click **+** in the side-terminal strip. + +The shortcut follows terminal focus, not panel visibility. A visible side panel with the prompt focused still creates a main terminal tab. Press `Cmd+Shift+[` / `Ctrl+Shift+[` for the previous terminal or `Cmd+Shift+]` / `Ctrl+Shift+]` for the next terminal in the focused terminal strip. Drag tabs to reorder them. Pressing `Cmd+W` / `Ctrl+W` with a focused side terminal closes that terminal when other terminals remain. On the last side terminal, it hides the panel and keeps the shell alive; use its close button or type `exit` to stop it. + +`Cmd+T` / `Ctrl+T` always creates a new agent session tab. It never creates a terminal. New terminals are named "Terminal N" using the lowest free number, and tabs pick up the live title from the shell or running program, so a dev server or editor names its own tab. @@ -256,7 +263,7 @@ New terminals are named "Terminal N" using the lowest free number, and tabs pick A common workflow is letting the agent work, then switching to the terminal to run tests or inspect the worktree, then switching back to control the agent: 1. **Agent Manager → Terminal:** Press `Cmd+/` (macOS) / `Ctrl+/` (Windows/Linux) to open and focus the terminal for the current session. The terminal runs inside the session's worktree, so commands like `npm test` or `git status` operate on the agent's isolated branch. -2. **Terminal → Agent Manager:** Press `Cmd+Shift+M` (macOS) / `Ctrl+Shift+M` (Windows/Linux) to bring focus back to the Agent Manager panel and its prompt input. This works from anywhere in VS Code — the terminal, another editor tab, or the sidebar. +2. **Terminal → Agent Manager:** Press `Cmd+Shift+M` (macOS) / `Ctrl+Shift+M` (Windows/Linux) to bring focus back to the Agent Manager panel and its prompt input. This explicit shortcut always targets the prompt and works from anywhere in VS Code — the terminal, another editor tab, or the sidebar. Returning to the panel by clicking its editor tab or switching windows restores the last focused control instead. ## Setup Scripts @@ -392,11 +399,13 @@ Closing a managed worktree removes it from Agent Manager, deletes its `.kilo/wor | `Cmd+Shift+N` | `Ctrl+Shift+N` | Create a new worktree immediately | | `Cmd+Shift+O` | `Ctrl+Shift+O` | Import/open worktree | | `Cmd+Shift+W` | `Ctrl+Shift+W` | Close current worktree | -| `Cmd+T` | `Ctrl+T` | New tab (session) in worktree | -| `Cmd+W` | `Ctrl+W` | Close current tab | +| `Cmd+T` | `Ctrl+T` | New agent session tab in worktree | +| `Cmd+W` | `Ctrl+W` | Close the focused tab or terminal; the last side terminal hides instead of stopping | | `Cmd+Alt+Up` / `Down` | `Ctrl+Alt+Up` / `Down` | Previous / next worktree | | `Cmd+Alt+Left` / `Right` | `Ctrl+Alt+Left` / `Right` | Previous / next tab in worktree | -| `Cmd+/` | `Ctrl+/` | Focus terminal for current session | +| `Cmd+/` | `Ctrl+/` | Focus terminal, or hide it when it already has focus | +| `Cmd+Shift+T` | `Ctrl+Shift+T` | New side terminal when a side terminal is focused; otherwise new main terminal tab | +| `Cmd+Shift+[` / `]` | `Ctrl+Shift+[` / `]` | Previous / next terminal | | `Cmd+D` | `Ctrl+D` | Toggle diff panel | | `Cmd+E` | `Ctrl+E` | Run / stop run script | | `Cmd+Shift+/` | `Ctrl+Shift+/` | Show keyboard shortcuts | diff --git a/packages/kilo-docs/pages/code-with-ai/agents/auto-model.md b/packages/kilo-docs/pages/code-with-ai/agents/auto-model.md index a287cf094c6..d09392976dc 100644 --- a/packages/kilo-docs/pages/code-with-ai/agents/auto-model.md +++ b/packages/kilo-docs/pages/code-with-ai/agents/auto-model.md @@ -10,7 +10,6 @@ Auto Model is a smart routing system that selects an underlying model for each r | Tier | Best For | Pricing | |---|---|---| | `kilo-auto/frontier` | Maximum capability with the best available models | Paid | -| `kilo-auto/balanced` | Strong performance at a lower cost | Paid | | `kilo-auto/efficient` | Lowest cost per task, with capability matched to difficulty | Paid | | `kilo-auto/free` | The best free models available | Free | @@ -31,8 +30,7 @@ The underlying models behind each Auto Model tier are updated server-side as bet ## Tiers - **Frontier** — Routes to the latest and most capable paid models. Uses different models for reasoning-heavy tasks (planning, architecture, debugging) versus implementation tasks (coding, building, exploring), pairing the right capability to each type of work. -- **Balanced** — Routes to a cost-effective model for all modes. The specific model is selected based on the API interface in use, but does not vary by mode. A good default for most developers who want strong AI assistance without paying frontier prices. -- **Efficient** — Session-aware routing that classifies the difficulty of each request in real time and routes it to the cheapest model proven accurate enough for that task, based on Kilo's continuously-run benchmarks. Routine work stays lean while harder tasks get a more capable model. Because it watches your session in context, it keeps using a model across related turns and only switches when a cheaper option is clearly worth it. If a routing decision can't be made, it falls back to the Balanced tier, so quality never drops below Balanced. +- **Efficient** — Session-aware routing that classifies the difficulty of each request in real time and routes it to the cheapest model proven accurate enough for that task, based on Kilo's continuously-run benchmarks. Routine work stays lean while harder tasks get a more capable model. Because it watches your session in context, it keeps using a model across related turns and only switches when a cheaper option is clearly worth it. If a routing decision can't be made, it falls back to a fixed, cost-effective baseline model, so quality never drops below that baseline. A good default for most developers who want strong AI assistance without paying frontier prices. - **Free** — Routes to the best available free models on OpenRouter, splitting traffic across them. Because free model availability shifts over time as providers change promotional periods, the mapping is updated server-side — you always get the best free option without having to track what's currently available. Quality will be lower than paid tiers, and the models may change over time. ### How Auto Efficient routing works @@ -61,7 +59,7 @@ New entries are benchmarked on demand before they can serve traffic. Each entry | Failed | Benchmarking failed — retry the entry | | Unavailable | The model or variant is no longer in your catalog — remove the entry | -Routing decides only among ready entries. If no pool entry can serve a request, the request falls back to the Balanced tier, so quality never drops below Balanced. +Routing decides only among ready entries. If no pool entry can serve a request, the request falls back to a fixed, cost-effective baseline model, so quality never drops below that baseline. {% callout type="note" %} You can benchmark up to 10 new or retried pairs per owner per rolling 24 hours. Entries that are already ready or benchmarking don't count against this limit. @@ -85,7 +83,7 @@ No need to manually switch models when changing modes. Auto Model handles routin ### Flexible Cost Control -Pick the tier that fits your budget. Frontier gives you the best models for demanding work; Balanced offers capable models at a fraction of the cost; Efficient minimizes cost per task by matching model capability to task difficulty; Free costs nothing. +Pick the tier that fits your budget. Frontier gives you the best models for demanding work; Efficient minimizes cost per task by matching model capability to task difficulty, at a fraction of Frontier's cost; Free costs nothing. ## Requirements @@ -101,7 +99,7 @@ Select an Auto Model tier from the model dropdown in the Kilo Code chat interfac 1. Open Kilo Code in VS Code or JetBrains 2. Click the model selector dropdown -3. Choose an Auto Model such as `kilo-auto/frontier` or `kilo-auto/balanced` +3. Choose an Auto Model such as `kilo-auto/frontier` or `kilo-auto/efficient` 4. Start chatting - the right model is selected automatically based on your current mode ## When to Use Auto Model diff --git a/packages/kilo-docs/pages/code-with-ai/gastown/index.md b/packages/kilo-docs/pages/code-with-ai/gastown/index.md index 55f665f5896..b7bba0b60e2 100644 --- a/packages/kilo-docs/pages/code-with-ai/gastown/index.md +++ b/packages/kilo-docs/pages/code-with-ai/gastown/index.md @@ -5,7 +5,7 @@ description: "Autonomous AI agent orchestration for your codebase" # {% $markdoc.frontmatter.title %} -Gastown by Kilo is an autonomous agent orchestration platform that manages teams of AI agents working on your codebase. Built on [Gastown](https://gastown.dev) — the open protocol for agent orchestration — Kilo's implementation coordinates coding agents, a code review agent, and a conversational coordinator to ship features, fix bugs, and maintain your projects with minimal human intervention. +Gastown by Kilo is an autonomous agent orchestration platform that manages teams of AI agents working on your codebase. Built on [Gastown](https://github.com/gastownhall/gastown) — the open protocol for agent orchestration — Kilo's implementation coordinates coding agents, a code review agent, and a conversational coordinator to ship features, fix bugs, and maintain your projects with minimal human intervention. You describe the work. Agents figure out how to do it, write the code, review each other's output, and land clean PRs — while you stay in control of what ships. diff --git a/packages/kilo-docs/pages/code-with-ai/gastown/settings.md b/packages/kilo-docs/pages/code-with-ai/gastown/settings.md index d95fa6bf147..7512e72e6d6 100644 --- a/packages/kilo-docs/pages/code-with-ai/gastown/settings.md +++ b/packages/kilo-docs/pages/code-with-ai/gastown/settings.md @@ -17,7 +17,7 @@ The primary model used by all agents (polecats, refinery, mayor). This affects q Popular choices: - **Kilo Auto Frontier** — highest quality models, best results (recommended) -- **Kilo Auto Balanced** — good balance of quality and cost (minimum for Gas Town) +- **Kilo Auto Efficient** — cheapest model proven accurate enough for each task, with capability matched to difficulty (minimum for Gas Town) ### Role-Specific Models diff --git a/packages/kilo-docs/pages/code-with-ai/gastown/troubleshooting.md b/packages/kilo-docs/pages/code-with-ai/gastown/troubleshooting.md index 62067e340f1..63b3fcc7880 100644 --- a/packages/kilo-docs/pages/code-with-ai/gastown/troubleshooting.md +++ b/packages/kilo-docs/pages/code-with-ai/gastown/troubleshooting.md @@ -124,7 +124,7 @@ Beads automatically escalate after 3 failed review cycles. If a bead is genuinel **Fix:** 1. Review failed bead descriptions — make them more specific 2. Ensure the repo builds cleanly (agents struggle with pre-existing broken builds) -3. Consider upgrading the model (Auto Balanced → Auto Frontier for complex work) +3. Consider upgrading the model (Auto Efficient → Auto Frontier for complex work) 4. Add custom instructions to guide agents: test commands, build steps, conventions ## Getting Help 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 0047db658f8..9a4ec19735b 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 @@ -659,21 +659,6 @@ Options: --cors additional domains to allow for CORS [array] [default: []] ``` -## kilo web - -``` -start kilo server and open web interface - -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: []] -``` - ## kilo models ``` 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 20691d8c381..469b135b7fd 100644 --- a/packages/kilo-docs/pages/code-with-ai/platforms/slack.md +++ b/packages/kilo-docs/pages/code-with-ai/platforms/slack.md @@ -110,7 +110,19 @@ Before using Kilo for Slack: ## Setup -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. +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: + +{% image src="/docs/img/connect/slack/slackbot-integrations.webp" alt="Kilo Integrations page where you install the Slack integration" width="800" /%} + +Then hit the Configure button, which will take you to this page: +{% image src="/docs/img/connect/slack/slackbot-integrations-2.webp" alt="Kilo Integrations page where you install the Slack integration, step 2" width="800" /%} + +Once you press "Connect with Slack", the Slack OAuth authorization page appears: +{% image src="/docs/img/connect/slack/slackbot-slack-authorize.webp" alt="Slack OAuth authorization page" width="800" /%} + +Once this is completed you're good to go! + +To make proper use of all of the Kilo Slack bot's capabilities, it's important that you also connect your source control provider with Kilo: | Platform | Integration Type | Details | |---|---|---| diff --git a/packages/kilo-docs/pages/deploy-secure/security-reviews.md b/packages/kilo-docs/pages/deploy-secure/security-reviews.md index 68d6677aa08..22c25c8fdc7 100644 --- a/packages/kilo-docs/pages/deploy-secure/security-reviews.md +++ b/packages/kilo-docs/pages/deploy-secure/security-reviews.md @@ -243,9 +243,9 @@ General settings include: |---|---|---| | Security Agent enabled | Off until you turn it on | Turning it on queues an initial sync for the selected repository scope. | | Repository selection | Selected repositories during setup | Choose all accessible repositories or selected repositories. | -| Triage model | Kilo Balanced | Used for initial triage and exploitability recommendations. | -| Analysis model | Kilo Balanced | Used for sandbox analysis and result extraction. | -| Remediation model | Kilo Balanced | Used by Cloud Agent for remediation PR work. | +| Triage model | Kilo Efficient | Used for initial triage and exploitability recommendations. | +| Analysis model | Kilo Efficient | Used for sandbox analysis and result extraction. | +| Remediation model | Kilo Efficient | Used by Cloud Agent for remediation PR work. | | Analysis mode | Auto | Auto, Shallow, or Deep. | #### Turn Security Agent on or off @@ -271,7 +271,7 @@ Security Agent uses a separate model for each stage: - The Analysis model runs sandbox analysis and extracts the result. - The Remediation model is used by Cloud Agent to prepare remediation pull requests. -Kilo Balanced is the default for all three stages. You can change each model independently. The model recorded in finding details is the model used when that analysis or remediation attempt ran. AI triage, sandbox analysis, and remediation consume Kilo Code credits. +Kilo Efficient is the default for all three stages. You can change each model independently. The model recorded in finding details is the model used when that analysis or remediation attempt ran. AI triage, sandbox analysis, and remediation consume Kilo Code credits. #### Choose an analysis mode diff --git a/packages/kilo-docs/pages/gateway/models-and-providers.md b/packages/kilo-docs/pages/gateway/models-and-providers.md index 922706b984b..b7a78df9ca0 100644 --- a/packages/kilo-docs/pages/gateway/models-and-providers.md +++ b/packages/kilo-docs/pages/gateway/models-and-providers.md @@ -73,7 +73,7 @@ For NVIDIA free endpoints (Super/Ultra/etc): Trial use only - do not submit pers ## Auto models -Auto virtual models select an underlying model using tier-specific routing. Frontier uses the `x-kilocode-mode` request header. Balanced uses the API interface, Free uses deterministic affinity across available candidates, and Small uses account balance. +Auto virtual models select an underlying model using tier-specific routing. Frontier uses the `x-kilocode-mode` request header. Efficient classifies task difficulty in session context and falls back to the API interface for its baseline model, Free uses deterministic affinity across available candidates, and Small uses account balance. {% callout type="info" title="Underlying models can change" %} The mappings below reflect the current routing. The underlying models behind each `kilo-auto/*` tier are updated server-side as better options become available or as providers change pricing and availability — the tier IDs themselves remain stable. @@ -89,9 +89,9 @@ Highest performance and capability for any task. Frontier requests are sent with | `build`, `explore`, `code` | `anthropic/claude-sonnet-4.6` | | Default (no / unknown mode) | `anthropic/claude-sonnet-4.6` | -### `kilo-auto/balanced` +### `kilo-auto/efficient` -Great balance of price and capability. The resolved model depends on the API interface used by the client. +Session-aware routing that classifies each request by difficulty and routes to the cheapest model proven accurate enough for the task. When no confident routing decision can be made, requests fall back to a baseline model resolved by the API interface used by the client. | API interface | Resolved Model | Reasoning effort | |---|---|---| @@ -132,5 +132,5 @@ curl -X POST "https://api.kilo.ai/api/gateway/chat/completions" \ -H "Authorization: Bearer $KILO_API_KEY" \ -H "x-kilocode-mode: plan" \ -H "Content-Type: application/json" \ - -d '{"model": "kilo-auto/balanced", "messages": [{"role": "user", "content": "Design a database schema"}]}' + -d '{"model": "kilo-auto/efficient", "messages": [{"role": "user", "content": "Design a database schema"}]}' ``` diff --git a/packages/kilo-docs/pages/getting-started/rate-limits-and-costs.md b/packages/kilo-docs/pages/getting-started/rate-limits-and-costs.md index 8b49b32ea69..9850e877655 100644 --- a/packages/kilo-docs/pages/getting-started/rate-limits-and-costs.md +++ b/packages/kilo-docs/pages/getting-started/rate-limits-and-costs.md @@ -14,7 +14,6 @@ Auto Model is Kilo's smart routing system. Instead of selecting a specific provi | Tier | Name | Best For | Cost | |---|---|---|---| | `kilo-auto/frontier` | Auto Frontier | Maximum capability — routes to top-tier models for planning/architect/debug and high-quality models for coding | Paid (highest) | -| `kilo-auto/balanced` | Auto Balanced | Strong performance at a predictably lower cost — routes every request to one fixed high-quality model | Paid | | `kilo-auto/efficient` | Auto Efficient | Lowest cost per task — classifies each request by difficulty and routes to the cheapest benchmark-proven model for that task | Paid (lowest) | | `kilo-auto/free` | Auto Free | No credits required — rotates through available free models | Free | @@ -22,20 +21,14 @@ Auto Model is Kilo's smart routing system. Instead of selecting a specific provi The underlying models behind each tier are updated server-side as better options become available or as providers change pricing. See [kilo.ai/models](https://kilo.ai/models) for current model assignments and live pricing. {% /callout %} -## Balanced vs Efficient — What's the Difference? +## What Makes Auto Efficient Efficient? -Both tiers are paid, but they optimize for different things. +**Auto Efficient** observes your coding session in context, classifies the difficulty of each request in real time, and routes it to the *cheapest model proven accurate enough* for that specific task, based on Kilo's continuously running benchmarks. Routine tasks (small edits, lookups, quick explanations) are handled by leaner models; harder tasks (architecture, debugging, complex refactors) automatically get a more capable model. -**Auto Balanced** routes every request to a single, fixed high-quality model. You get consistent, strong results with predictable cost — a reliable default for most developers. - -**Auto Efficient** goes further. It observes your coding session in context, classifies the difficulty of each request in real time, and routes it to the *cheapest model proven accurate enough* for that specific task, based on Kilo's continuously running benchmarks. Routine tasks (small edits, lookups, quick explanations) are handled by leaner models; harder tasks (architecture, debugging, complex refactors) automatically get a more capable model. - -Efficient is also session-aware: it stays with a model across related turns and only switches when a cheaper option is clearly worth it. If it cannot make a routing decision with confidence, it falls back to Balanced — so quality never drops below Balanced. - -Think of Efficient as Balanced with an intelligent cost optimizer layered on top. +Efficient is also session-aware: it stays with a model across related turns and only switches when a cheaper option is clearly worth it. If it cannot make a routing decision with confidence, it falls back to a fixed, high-quality baseline model — so quality never drops below that baseline. You get consistent, strong results with predictable cost, plus an intelligent cost optimizer layered on top. {% callout type="tip" %} -For everyday coding tasks, start with **Auto Efficient** or **Auto Balanced**. Switch to **Auto Frontier** for complex architecture sessions or deep debugging where maximum capability matters. +For everyday coding tasks, start with **Auto Efficient**. Switch to **Auto Frontier** for complex architecture sessions or deep debugging where maximum capability matters. {% /callout %} ## How to Switch Auto Models diff --git a/packages/kilo-docs/pages/getting-started/settings/sandboxing.md b/packages/kilo-docs/pages/getting-started/settings/sandboxing.md index 72f1029bc4c..462a582ab71 100644 --- a/packages/kilo-docs/pages/getting-started/settings/sandboxing.md +++ b/packages/kilo-docs/pages/getting-started/settings/sandboxing.md @@ -161,9 +161,9 @@ A configured destination is an egress route, not tenant, organization, repositor The config setting supplies the initial default for new sessions that do not have a saved preference. Use the lock button in the VS Code prompt or `/sandbox` in the CLI to change the current session. Your latest choice is saved as the default for future sessions in that project, takes precedence over the config default, and persists across restarts. -Each initialized session snapshots its network mode, allowed destinations, and additional writable paths. Changing config affects new sessions. The prompt control or `/sandbox` can change the current session's enabled state, but it cannot change these authority lists, and they never expand during an active session. +Each session preserves its enabled or disabled choice. Saving changes through Kilo settings to network mode, allowed destinations, or additional writable paths refreshes existing session policies before their next tool execution. Enabling sandboxing also reads the latest settings. A tool that is already running keeps the policy it started with. -Forked sessions retain the source session's confinement. Subagents inherit the stricter combination of parent and child settings: sandboxing remains enabled if either requires it, deny-all wins over destination exceptions, destination lists intersect, and additional writable paths intersect. +At creation, forked sessions retain the source session's confinement and subagents inherit the stricter combination of parent and child settings: sandboxing remains enabled if either requires it, deny-all wins over destination exceptions, destination lists intersect, and additional writable paths intersect. Later trusted sandbox settings replace those network and writable-path limits before the affected session's next tool execution. Cloud sessions do not expose the local sandbox control because their tools do not run in your local sandbox. diff --git a/packages/kilo-docs/pages/kiloclaw/end-to-end.md b/packages/kilo-docs/pages/kiloclaw/end-to-end.md index 2ff8e011726..901f76dab82 100644 --- a/packages/kilo-docs/pages/kiloclaw/end-to-end.md +++ b/packages/kilo-docs/pages/kiloclaw/end-to-end.md @@ -158,6 +158,6 @@ Or ask your Claw to build a custom skill from scratch — it has a built-in skil ## Manage inference -**Model picker:** Balanced is a good starting point. Frontier is more capable but significantly more expensive. +**Model picker:** Efficient is a good starting point. Frontier is more capable but significantly more expensive. You can also use your [Kilo Pass](https://kilo.ai/pricing/kilo-pass) credits — find this under **Profile** in the dashboard. diff --git a/packages/kilo-docs/pages/kiloclaw/overview.md b/packages/kilo-docs/pages/kiloclaw/overview.md index a55e43395fa..8af8b3ef184 100644 --- a/packages/kilo-docs/pages/kiloclaw/overview.md +++ b/packages/kilo-docs/pages/kiloclaw/overview.md @@ -37,7 +37,7 @@ Depending on your setup, you can also use: {% image src="/docs/img/kiloclaw/profile-claw-nav.png" alt="Profile page showing Claw navigation" width="400" caption="Claw navigation in profile sidebar" /%} 3. Click **Create Instance** -4. Your instance will use **Kilo Auto Balanced** as the default model. You can optionally select a different model from the dropdown — see all available models at the [Kilo Leaderboard](https://kilo.ai/leaderboard#all-models). +4. Your instance will use **Kilo Auto Efficient** as the default model. You can optionally select a different model from the dropdown — see all available models at the [Kilo Leaderboard](https://kilo.ai/leaderboard#all-models). {% image src="/docs/img/kiloclaw/create-instance.png" alt="Create instance modal with model selection" width="600" caption="Model selection during instance creation" /%} diff --git a/packages/kilo-docs/pnpm-lock.yaml b/packages/kilo-docs/pnpm-lock.yaml new file mode 100644 index 00000000000..6ca1dd11e8d --- /dev/null +++ b/packages/kilo-docs/pnpm-lock.yaml @@ -0,0 +1,2786 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@docsearch/css': + specifier: ^4 + version: 4.7.0 + '@docsearch/js': + specifier: ^4 + version: 4.7.0 + '@markdoc/markdoc': + specifier: ^0.5.4 + version: 0.5.9(@types/react@19.2.18)(react@19.2.8) + '@markdoc/next.js': + specifier: ^0.5.0 + version: 0.5.0(@markdoc/markdoc@0.5.9(@types/react@19.2.18)(react@19.2.8))(next@16.3.0(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8) + '@vscode/codicons': + specifier: ^0.0.44 + version: 0.0.44 + '@xyflow/react': + specifier: 12.10.2 + version: 12.10.2(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + js-yaml: + specifier: ^4.1.0 + version: 4.3.1 + mermaid: + specifier: 11.15.0 + version: 11.15.0 + next: + specifier: ^16.1.5 + version: 16.3.0(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + posthog-js: + specifier: ^1.335.3 + version: 1.413.3 + prismjs: + specifier: ^1.30.0 + version: 1.30.0 + react: + specifier: ^19.2.4 + version: 19.2.8 + react-dom: + specifier: ^19.2.4 + version: 19.2.8(react@19.2.8) + devDependencies: + '@tailwindcss/postcss': + specifier: ^4.1.18 + version: 4.3.3 + '@types/node': + specifier: ^25.0.10 + version: 25.9.5 + '@types/react': + specifier: ^19.2.9 + version: 19.2.18 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.4(@types/react@19.2.18) + autoprefixer: + specifier: ^10.4.23 + version: 10.5.4(postcss@8.5.26) + postcss: + specifier: ^8.5.6 + version: 8.5.26 + tailwindcss: + specifier: ^4.1.18 + version: 4.3.3 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vitest: + specifier: 4.1.0 + version: 4.1.0(@types/node@25.9.5)(vite@8.2.1(@types/node@25.9.5)(jiti@2.7.0)) + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@antfu/install-pkg@1.1.0': + resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + + '@braintree/sanitize-url@7.1.2': + resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} + + '@chevrotain/types@11.1.2': + resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==} + + '@docsearch/css@4.7.0': + resolution: {integrity: sha512-Sk5xkdRFeE7PeWjG9l4AfTwdvMfr9wHiwNNCpHXT4v4SNyNMKdHGvEILc31BgaVFGDDNbv5u/a73tofRiwbEZw==} + + '@docsearch/js@4.7.0': + resolution: {integrity: sha512-x5lCqu1tetgsJFkjQ6VSocbHldsRkGEgwg5N98Vx21sq/V5wcmj4u226PY9k+TEpIgQ772zlYbPLTPicWyGnpA==} + + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@3.1.4': + resolution: {integrity: sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw==} + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.35.3': + resolution: {integrity: sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.35.3': + resolution: {integrity: sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [darwin] + + '@img/sharp-freebsd-wasm32@0.35.3': + resolution: {integrity: sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==} + engines: {node: '>=20.9.0'} + os: [freebsd] + + '@img/sharp-libvips-darwin-arm64@1.3.2': + resolution: {integrity: sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.3.2': + resolution: {integrity: sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.3.2': + resolution: {integrity: sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.3.2': + resolution: {integrity: sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.3.2': + resolution: {integrity: sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.3.2': + resolution: {integrity: sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.3.2': + resolution: {integrity: sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.3.2': + resolution: {integrity: sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.3.2': + resolution: {integrity: sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.3.2': + resolution: {integrity: sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.35.3': + resolution: {integrity: sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.35.3': + resolution: {integrity: sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==} + engines: {node: '>=20.9.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.35.3': + resolution: {integrity: sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==} + engines: {node: '>=20.9.0'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.35.3': + resolution: {integrity: sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==} + engines: {node: '>=20.9.0'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.35.3': + resolution: {integrity: sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==} + engines: {node: '>=20.9.0'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.35.3': + resolution: {integrity: sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.35.3': + resolution: {integrity: sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.35.3': + resolution: {integrity: sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.35.3': + resolution: {integrity: sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.3': + resolution: {integrity: sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==} + engines: {node: '>=20.9.0'} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.35.3': + resolution: {integrity: sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.35.3': + resolution: {integrity: sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==} + engines: {node: ^20.9.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.35.3': + resolution: {integrity: sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@markdoc/markdoc@0.5.9': + resolution: {integrity: sha512-req3Wpi1xXIs7C+n2nsKv7oGeC0xREkpd+D+JRGZ/pIMXEuQSlT+s/I760fqdmiysvkx10EC3MGlScokRRcaSQ==} + engines: {node: '>=14.7.0'} + peerDependencies: + '@types/react': '*' + react: '*' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + + '@markdoc/next.js@0.5.0': + resolution: {integrity: sha512-gzWvRc2dlxArQn1mDS4SHed46vkAoKBbXkELihQRYv1eVSA0u9eoNEdJXNuBL4Ajn9j2dPpwZp6nAVjN4nzZlA==} + peerDependencies: + '@markdoc/markdoc': '*' + next: '*' + react: '*' + + '@mermaid-js/parser@1.2.0': + resolution: {integrity: sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA==} + + '@next/env@16.3.0': + resolution: {integrity: sha512-o9r1S0BNiNreHP9Vs+Qnqd9kviDkJh8xIACY7UFZSmiGbbQRzPBBosvHzAU4TULHOIuOj/18RSsyz2qrREmIFw==} + + '@next/swc-darwin-arm64@16.3.0': + resolution: {integrity: sha512-55hpqq18bEVAlxedlTt3tFqZmKg2nUXT1kn1G/BGEy0R13h3LwtwHPVzzjG6P4LLeOHE32PFDQUVaJEWvBEZBw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@16.3.0': + resolution: {integrity: sha512-SOi96kSaF5T+0wW4koiM1bWzSPwjzTesC1p3df+FjdOi5LIQkBK/blxh7HdoKnNuI4PURF1OO7TZqtfnbWDSgw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@16.3.0': + resolution: {integrity: sha512-P0gZAoPMF4dyTRzhmkV4PrqVzSOB6t4mC1oI3c4dqijJ+OVEVx5clIXAKR4/uQpsqw2KKM/0D5tVumcR2r5blg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-arm64-musl@16.3.0': + resolution: {integrity: sha512-tXXGKJw0m37O0eKJARVTX/TheKPhz0QFVtVVZXmOig+9YKLQOSP6hvf2pxv5DO7CLEJyTHx3Pg043CDQkv1G4Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@next/swc-linux-x64-gnu@16.3.0': + resolution: {integrity: sha512-pjGxK5EY7yWml78ALejFkWmgHsU7wbFQrISiugpH6FbUJhgEvw3xFZ/EBAtLl7QtL0WdQKiG9eWJ3mOKGTukHw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-x64-musl@16.3.0': + resolution: {integrity: sha512-sjo++Xx+lomlPs3HRsHWhVDyGG6ms1kGW5EtHLERdII8AyG1i+f6aq68xHREO6AEMlhjTNEWBSmfJfqm9orf7g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@next/swc-win32-arm64-msvc@16.3.0': + resolution: {integrity: sha512-C5JSgiO54wURdaxdEUIXqkz04uMqC9UmPX1gtDrV/5Tf1UowdWYI8uA5hfFbPolTlp0q4KZ60xlHePNibf0VIw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@16.3.0': + resolution: {integrity: sha512-fDOggsweNb5SSw0ZKVk6U+gxSyGFFlIBY/LBc1r8GUj4u/6t6oArL+Pmkg0MBnsgR+KkdsURilVH4F3GXUGepA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@oxc-project/types@0.143.0': + resolution: {integrity: sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==} + + '@posthog/browser-common@0.4.0': + resolution: {integrity: sha512-W9DCGVks15docUMPvJ2nd8NS16Gn74bsGWuaeg31beEKFSjdW8wvnQ1ETY6WSql5pYxZb3GdJmEUZVVstKSrBQ==} + + '@posthog/core@1.46.9': + resolution: {integrity: sha512-EXO6y5ih+jBkTCpUCuYgQmajuDZuvy6vMvflkub6pLQyi0GPlCPWVSvZZkOeQw9e2MxoD5GteeGCt9R8+UJ/yQ==} + + '@posthog/types@1.402.2': + resolution: {integrity: sha512-ZZTiS4dLwF4/D0YTzS3gGSLFnhuNOY5yu4d9VGS9trGe5GW6FjIXo20p18K5BPW2RZSYSld8sdnSAY3AUXleUQ==} + + '@rolldown/binding-android-arm64@1.2.3': + resolution: {integrity: sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.3': + resolution: {integrity: sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.3': + resolution: {integrity: sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.3': + resolution: {integrity: sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.3': + resolution: {integrity: sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.3': + resolution: {integrity: sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.2.3': + resolution: {integrity: sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.2.3': + resolution: {integrity: sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.2.3': + resolution: {integrity: sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.2.3': + resolution: {integrity: sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.2.3': + resolution: {integrity: sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.2.3': + resolution: {integrity: sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.3': + resolution: {integrity: sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.3': + resolution: {integrity: sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + + '@tailwindcss/node@4.3.3': + resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} + + '@tailwindcss/oxide-android-arm64@4.3.3': + resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.3': + resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.3': + resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==} + engines: {node: '>= 20'} + + '@tailwindcss/postcss@4.3.3': + resolution: {integrity: sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-axis@3.0.6': + resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} + + '@types/d3-brush@3.0.6': + resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} + + '@types/d3-chord@3.0.6': + resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-contour@3.0.6': + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + + '@types/d3-delaunay@6.0.4': + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + + '@types/d3-dispatch@3.0.7': + resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-dsv@3.0.7': + resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-fetch@3.0.7': + resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} + + '@types/d3-force@3.0.10': + resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} + + '@types/d3-format@3.0.4': + resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} + + '@types/d3-geo@3.1.1': + resolution: {integrity: sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w==} + + '@types/d3-hierarchy@3.1.7': + resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-polygon@3.0.2': + resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} + + '@types/d3-quadtree@3.0.6': + resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} + + '@types/d3-random@3.0.4': + resolution: {integrity: sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA==} + + '@types/d3-scale-chromatic@3.1.0': + resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time-format@4.0.3': + resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + + '@types/d3@7.4.3': + resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + + '@types/linkify-it@3.0.5': + resolution: {integrity: sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==} + + '@types/markdown-it@12.2.3': + resolution: {integrity: sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + + '@types/node@25.9.5': + resolution: {integrity: sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==} + + '@types/react-dom@19.2.4': + resolution: {integrity: sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.18': + resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@upsetjs/venn.js@2.0.0': + resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==} + + '@vitest/expect@4.1.0': + resolution: {integrity: sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==} + + '@vitest/mocker@4.1.0': + resolution: {integrity: sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.0': + resolution: {integrity: sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==} + + '@vitest/runner@4.1.0': + resolution: {integrity: sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==} + + '@vitest/snapshot@4.1.0': + resolution: {integrity: sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==} + + '@vitest/spy@4.1.0': + resolution: {integrity: sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==} + + '@vitest/utils@4.1.0': + resolution: {integrity: sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==} + + '@vscode/codicons@0.0.44': + resolution: {integrity: sha512-F7qPRumUK3EHjNdopfICLGRf3iNPoZQt+McTHAn4AlOWPB3W2kL4H0S7uqEqbyZ6rCxaeDjpAn3MCUnwTu/VJQ==} + + '@xyflow/react@12.10.2': + resolution: {integrity: sha512-CgIi6HwlcHXwlkTpr0fxLv/0sRVNZ8IdwKLzzeCscaYBwpvfcH1QFOCeaTCuEn1FQEs/B8CjnTSjhs8udgmBgQ==} + peerDependencies: + react: '>=17' + react-dom: '>=17' + + '@xyflow/system@0.0.76': + resolution: {integrity: sha512-hvwvnRS1B3REwVDlWexsq7YQaPZeG3/mKo1jv38UmnpWmxihp14bW6VtEOuHEwJX2FvzFw8k77LyKSk/wiZVNA==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + autoprefixer@10.5.4: + resolution: {integrity: sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + baseline-browser-mapping@2.11.12: + resolution: {integrity: sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==} + engines: {node: '>=6.0.0'} + hasBin: true + + browserslist@4.28.7: + resolution: {integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + caniuse-lite@1.0.30001807: + resolution: {integrity: sha512-daRXJ9EB/rdRgu7kV+TTl1YUKtlsMWblPl2sLnpg9DZae16QCegol6A1SmCE31Lm9mXC1sRWGt/krouH+/dl7Q==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + classcat@5.0.5: + resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + core-js@3.50.0: + resolution: {integrity: sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw==} + + cose-base@1.0.3: + resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} + + cose-base@2.2.0: + resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + cytoscape-cose-bilkent@4.1.0: + resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape-fcose@2.2.0: + resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape@3.34.0: + resolution: {integrity: sha512-62rNSrioXw93uliKFBwjukeQyeWwH2PqDrTac31r2P6464u3AUvTk0xS4LVvT251g7IgkFunrI48ZEZGjywSOg==} + engines: {node: '>=0.10'} + + d3-array@2.12.1: + resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@1.0.9: + resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-sankey@0.12.3: + resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-shape@1.3.7: + resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + + dagre-d3-es@7.0.14: + resolution: {integrity: sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==} + + dayjs@1.11.21: + resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==} + + delaunator@5.1.0: + resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dompurify@3.4.13: + resolution: {integrity: sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==} + + electron-to-chromium@1.5.402: + resolution: {integrity: sha512-/oOpMaPT6Yg+6/1XQhyIPlzgj7Ye9zf+nNM2Uh6OcE2G2oNptWazFa+qB2Pdqqbsc9KnIDzgAntoYN0dbwOXwA==} + + enhanced-resolve@5.24.5: + resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==} + engines: {node: '>=10.13.0'} + + es-module-lexer@2.3.1: + resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} + + es-toolkit@1.50.0: + resolution: {integrity: sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fflate@0.4.9: + resolution: {integrity: sha512-zdxgIEddhfsyCaWpJ2SdXEP8ZMrKJ6+5jl4OupODcywU0IhRk6gdXuVGcPICyfx2H97hVK7xmJtRLPjkxAX8Vw==} + + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + hachure-fill@0.5.2: + resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + import-meta-resolve@4.2.0: + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + + internmap@1.0.1: + resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + js-yaml@4.3.1: + resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} + hasBin: true + + katex@0.16.47: + resolution: {integrity: sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==} + hasBin: true + + khroma@2.1.0: + resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} + + layout-base@1.0.2: + resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} + + layout-base@2.0.1: + resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + marked@16.4.2: + resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==} + engines: {node: '>= 20'} + hasBin: true + + mermaid@11.15.0: + resolution: {integrity: sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw==} + + nanoid@3.3.17: + resolution: {integrity: sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + next@16.3.0: + resolution: {integrity: sha512-NEdGOzH+08eTXMUp9UYkA99Nhi5N6Thrhc1jgFOQgfgnGK/dA2hRwBpXep+exdFQrnwlRf/3Wixyp8lLBUpE2A==} + engines: {node: '>=20.9.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + + node-releases@2.0.53: + resolution: {integrity: sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==} + engines: {node: '>=18'} + + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} + + package-manager-detector@1.8.0: + resolution: {integrity: sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==} + + path-data-parser@0.1.0: + resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + points-on-curve@0.2.0: + resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} + + points-on-path@0.2.1: + resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.23: + resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} + engines: {node: ^10 || ^12 || >=14} + + posthog-js@1.413.3: + resolution: {integrity: sha512-UoAymtZYVr84Ldp0ITXmlK4Q3GE8vU7GW8wC+aCput5RrKVJ8wkAPc12n3ElmKRlvLL+oD+Fotjc7n3wYG0RGQ==} + + preact@10.29.8: + resolution: {integrity: sha512-ej2aVZ+vZ8WO7tvlQWRM9N63A0KzF9q4mWJfDUHgYaIofWY9hu74QdnQrjoPMmZi2/nZ5gN0bJCQF49xQqx09Q==} + peerDependencies: + preact-render-to-string: '>=5' + peerDependenciesMeta: + preact-render-to-string: + optional: true + + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} + engines: {node: '>=6'} + + query-selector-shadow-dom@1.0.1: + resolution: {integrity: sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==} + + react-dom@19.2.8: + resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==} + peerDependencies: + react: ^19.2.8 + + react@19.2.8: + resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} + engines: {node: '>=0.10.0'} + + robust-predicates@3.0.3: + resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} + + rolldown@1.2.3: + resolution: {integrity: sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + roughjs@4.6.6: + resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} + + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.35.3: + resolution: {integrity: sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==} + engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + stylis@4.4.0: + resolution: {integrity: sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==} + + tailwindcss@4.3.3: + resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.3.0: + resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.1: + resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} + engines: {node: '>=14.0.0'} + + ts-dedent@2.3.0: + resolution: {integrity: sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==} + engines: {node: '>=6.10'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + uuid@14.0.1: + resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==} + hasBin: true + + vite@8.2.1: + resolution: {integrity: sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.4.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.0: + resolution: {integrity: sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.0 + '@vitest/browser-preview': 4.1.0 + '@vitest/browser-webdriverio': 4.1.0 + '@vitest/ui': 4.1.0 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + web-vitals@5.3.0: + resolution: {integrity: sha512-q6LWsLatGYZp5VGBIOvbTj6JBV2nOmC8KvWztXBmwJcfFAzhwKwbOxhUH306XY3CcaZDUlSmSuNPBsCn0bFu+g==} + + web-vitals@6.0.0: + resolution: {integrity: sha512-Guaibvy/+uNtL6Bsu4jmMJGzuSl91oeRH5iO9pPRbYftnFUr3yqT1TUNX/OE4o9HexuEMU3Kb/Wg7iKhlffZUA==} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + zustand@4.5.7: + resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@antfu/install-pkg@1.1.0': + dependencies: + package-manager-detector: 1.8.0 + tinyexec: 1.3.0 + + '@braintree/sanitize-url@7.1.2': {} + + '@chevrotain/types@11.1.2': {} + + '@docsearch/css@4.7.0': {} + + '@docsearch/js@4.7.0': {} + + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@iconify/types@2.0.0': {} + + '@iconify/utils@3.1.4': + dependencies: + '@antfu/install-pkg': 1.1.0 + '@iconify/types': 2.0.0 + import-meta-resolve: 4.2.0 + + '@img/colour@1.1.0': + optional: true + + '@img/sharp-darwin-arm64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.3.2 + optional: true + + '@img/sharp-darwin-x64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.3.2 + optional: true + + '@img/sharp-freebsd-wasm32@0.35.3': + dependencies: + '@img/sharp-wasm32': 0.35.3 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.3.2': + optional: true + + '@img/sharp-libvips-darwin-x64@1.3.2': + optional: true + + '@img/sharp-libvips-linux-arm64@1.3.2': + optional: true + + '@img/sharp-libvips-linux-arm@1.3.2': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.3.2': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.3.2': + optional: true + + '@img/sharp-libvips-linux-s390x@1.3.2': + optional: true + + '@img/sharp-libvips-linux-x64@1.3.2': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.3.2': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.3.2': + optional: true + + '@img/sharp-linux-arm64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.3.2 + optional: true + + '@img/sharp-linux-arm@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.3.2 + optional: true + + '@img/sharp-linux-ppc64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.3.2 + optional: true + + '@img/sharp-linux-riscv64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.3.2 + optional: true + + '@img/sharp-linux-s390x@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.3.2 + optional: true + + '@img/sharp-linux-x64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.3.2 + optional: true + + '@img/sharp-linuxmusl-arm64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 + optional: true + + '@img/sharp-linuxmusl-x64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.3.2 + optional: true + + '@img/sharp-wasm32@0.35.3': + dependencies: + '@emnapi/runtime': 1.11.3 + optional: true + + '@img/sharp-webcontainers-wasm32@0.35.3': + dependencies: + '@img/sharp-wasm32': 0.35.3 + optional: true + + '@img/sharp-win32-arm64@0.35.3': + optional: true + + '@img/sharp-win32-ia32@0.35.3': + optional: true + + '@img/sharp-win32-x64@0.35.3': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@markdoc/markdoc@0.5.9(@types/react@19.2.18)(react@19.2.8)': + optionalDependencies: + '@types/linkify-it': 3.0.5 + '@types/markdown-it': 12.2.3 + '@types/react': 19.2.18 + react: 19.2.8 + + '@markdoc/next.js@0.5.0(@markdoc/markdoc@0.5.9(@types/react@19.2.18)(react@19.2.8))(next@16.3.0(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)': + dependencies: + '@markdoc/markdoc': 0.5.9(@types/react@19.2.18)(react@19.2.8) + js-yaml: 4.3.1 + next: 16.3.0(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + + '@mermaid-js/parser@1.2.0': + dependencies: + '@chevrotain/types': 11.1.2 + + '@next/env@16.3.0': {} + + '@next/swc-darwin-arm64@16.3.0': + optional: true + + '@next/swc-darwin-x64@16.3.0': + optional: true + + '@next/swc-linux-arm64-gnu@16.3.0': + optional: true + + '@next/swc-linux-arm64-musl@16.3.0': + optional: true + + '@next/swc-linux-x64-gnu@16.3.0': + optional: true + + '@next/swc-linux-x64-musl@16.3.0': + optional: true + + '@next/swc-win32-arm64-msvc@16.3.0': + optional: true + + '@next/swc-win32-x64-msvc@16.3.0': + optional: true + + '@oxc-project/types@0.143.0': {} + + '@posthog/browser-common@0.4.0': + dependencies: + '@posthog/core': 1.46.9 + '@posthog/types': 1.402.2 + + '@posthog/core@1.46.9': + dependencies: + '@posthog/types': 1.402.2 + + '@posthog/types@1.402.2': {} + + '@rolldown/binding-android-arm64@1.2.3': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.3': + optional: true + + '@rolldown/binding-darwin-x64@1.2.3': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.3': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.3': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.3': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.3': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.3': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.3': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.3': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.3': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.3': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.3': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.3': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@standard-schema/spec@1.1.0': {} + + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + + '@tailwindcss/node@4.3.3': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.24.5 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.3 + + '@tailwindcss/oxide-android-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide@4.3.3': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-x64': 4.3.3 + '@tailwindcss/oxide-freebsd-x64': 4.3.3 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.3 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.3 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-x64-musl': 4.3.3 + '@tailwindcss/oxide-wasm32-wasi': 4.3.3 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 + + '@tailwindcss/postcss@4.3.3': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.3.3 + '@tailwindcss/oxide': 4.3.3 + postcss: 8.5.26 + tailwindcss: 4.3.3 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/d3-array@3.2.2': {} + + '@types/d3-axis@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-brush@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-chord@3.0.6': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-contour@3.0.6': + dependencies: + '@types/d3-array': 3.2.2 + '@types/geojson': 7946.0.16 + + '@types/d3-delaunay@6.0.4': {} + + '@types/d3-dispatch@3.0.7': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-dsv@3.0.7': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-fetch@3.0.7': + dependencies: + '@types/d3-dsv': 3.0.7 + + '@types/d3-force@3.0.10': {} + + '@types/d3-format@3.0.4': {} + + '@types/d3-geo@3.1.1': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/d3-hierarchy@3.1.7': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-polygon@3.0.2': {} + + '@types/d3-quadtree@3.0.6': {} + + '@types/d3-random@3.0.4': {} + + '@types/d3-scale-chromatic@3.1.0': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-selection@3.0.11': {} + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time-format@4.0.3': {} + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + + '@types/d3@7.4.3': + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-axis': 3.0.6 + '@types/d3-brush': 3.0.6 + '@types/d3-chord': 3.0.6 + '@types/d3-color': 3.1.3 + '@types/d3-contour': 3.0.6 + '@types/d3-delaunay': 6.0.4 + '@types/d3-dispatch': 3.0.7 + '@types/d3-drag': 3.0.7 + '@types/d3-dsv': 3.0.7 + '@types/d3-ease': 3.0.2 + '@types/d3-fetch': 3.0.7 + '@types/d3-force': 3.0.10 + '@types/d3-format': 3.0.4 + '@types/d3-geo': 3.1.1 + '@types/d3-hierarchy': 3.1.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-path': 3.1.1 + '@types/d3-polygon': 3.0.2 + '@types/d3-quadtree': 3.0.6 + '@types/d3-random': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-scale-chromatic': 3.1.0 + '@types/d3-selection': 3.0.11 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-time-format': 4.0.3 + '@types/d3-timer': 3.0.2 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/geojson@7946.0.16': {} + + '@types/linkify-it@3.0.5': + optional: true + + '@types/markdown-it@12.2.3': + dependencies: + '@types/linkify-it': 3.0.5 + '@types/mdurl': 2.0.0 + optional: true + + '@types/mdurl@2.0.0': + optional: true + + '@types/node@25.9.5': + dependencies: + undici-types: 7.24.6 + + '@types/react-dom@19.2.4(@types/react@19.2.18)': + dependencies: + '@types/react': 19.2.18 + + '@types/react@19.2.18': + dependencies: + csstype: 3.2.3 + + '@types/trusted-types@2.0.7': + optional: true + + '@upsetjs/venn.js@2.0.0': + optionalDependencies: + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + '@vitest/expect@4.1.0': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.0 + '@vitest/utils': 4.1.0 + chai: 6.2.2 + tinyrainbow: 3.1.1 + + '@vitest/mocker@4.1.0(vite@8.2.1(@types/node@25.9.5)(jiti@2.7.0))': + dependencies: + '@vitest/spy': 4.1.0 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.2.1(@types/node@25.9.5)(jiti@2.7.0) + + '@vitest/pretty-format@4.1.0': + dependencies: + tinyrainbow: 3.1.1 + + '@vitest/runner@4.1.0': + dependencies: + '@vitest/utils': 4.1.0 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.0': + dependencies: + '@vitest/pretty-format': 4.1.0 + '@vitest/utils': 4.1.0 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.0': {} + + '@vitest/utils@4.1.0': + dependencies: + '@vitest/pretty-format': 4.1.0 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.1 + + '@vscode/codicons@0.0.44': {} + + '@xyflow/react@12.10.2(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@xyflow/system': 0.0.76 + classcat: 5.0.5 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + zustand: 4.5.7(@types/react@19.2.18)(react@19.2.8) + transitivePeerDependencies: + - '@types/react' + - immer + + '@xyflow/system@0.0.76': + dependencies: + '@types/d3-drag': 3.0.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-zoom: 3.0.0 + + argparse@2.0.1: {} + + assertion-error@2.0.1: {} + + autoprefixer@10.5.4(postcss@8.5.26): + dependencies: + browserslist: 4.28.7 + caniuse-lite: 1.0.30001807 + fraction.js: 5.3.4 + picocolors: 1.1.1 + postcss: 8.5.26 + postcss-value-parser: 4.2.0 + + baseline-browser-mapping@2.11.12: {} + + browserslist@4.28.7: + dependencies: + baseline-browser-mapping: 2.11.12 + caniuse-lite: 1.0.30001807 + electron-to-chromium: 1.5.402 + node-releases: 2.0.53 + update-browserslist-db: 1.2.3(browserslist@4.28.7) + + caniuse-lite@1.0.30001807: {} + + chai@6.2.2: {} + + classcat@5.0.5: {} + + client-only@0.0.1: {} + + commander@7.2.0: {} + + commander@8.3.0: {} + + convert-source-map@2.0.0: {} + + core-js@3.50.0: {} + + cose-base@1.0.3: + dependencies: + layout-base: 1.0.2 + + cose-base@2.2.0: + dependencies: + layout-base: 2.0.1 + + csstype@3.2.3: {} + + cytoscape-cose-bilkent@4.1.0(cytoscape@3.34.0): + dependencies: + cose-base: 1.0.3 + cytoscape: 3.34.0 + + cytoscape-fcose@2.2.0(cytoscape@3.34.0): + dependencies: + cose-base: 2.2.0 + cytoscape: 3.34.0 + + cytoscape@3.34.0: {} + + d3-array@2.12.1: + dependencies: + internmap: 1.0.1 + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-axis@3.0.0: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + + d3-color@3.1.0: {} + + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.1.0 + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-ease@3.0.1: {} + + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.2: {} + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@1.0.9: {} + + d3-path@3.1.0: {} + + d3-polygon@3.0.1: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-sankey@0.12.3: + dependencies: + d3-array: 2.12.1 + d3-shape: 1.3.7 + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-shape@1.3.7: + dependencies: + d3-path: 1.0.9 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3@7.9.0: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.2 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + + dagre-d3-es@7.0.14: + dependencies: + d3: 7.9.0 + lodash-es: 4.18.1 + + dayjs@1.11.21: {} + + delaunator@5.1.0: + dependencies: + robust-predicates: 3.0.3 + + detect-libc@2.1.2: {} + + dompurify@3.4.13: + optionalDependencies: + '@types/trusted-types': 2.0.7 + + electron-to-chromium@1.5.402: {} + + enhanced-resolve@5.24.5: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + es-module-lexer@2.3.1: {} + + es-toolkit@1.50.0: {} + + escalade@3.2.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.4.0: {} + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + fflate@0.4.9: {} + + fraction.js@5.3.4: {} + + fsevents@2.3.3: + optional: true + + graceful-fs@4.2.11: {} + + hachure-fill@0.5.2: {} + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + import-meta-resolve@4.2.0: {} + + internmap@1.0.1: {} + + internmap@2.0.3: {} + + jiti@2.7.0: {} + + js-yaml@4.3.1: + dependencies: + argparse: 2.0.1 + + katex@0.16.47: + dependencies: + commander: 8.3.0 + + khroma@2.1.0: {} + + layout-base@1.0.2: {} + + layout-base@2.0.1: {} + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + lodash-es@4.18.1: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + marked@16.4.2: {} + + mermaid@11.15.0: + dependencies: + '@braintree/sanitize-url': 7.1.2 + '@iconify/utils': 3.1.4 + '@mermaid-js/parser': 1.2.0 + '@types/d3': 7.4.3 + '@upsetjs/venn.js': 2.0.0 + cytoscape: 3.34.0 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.34.0) + cytoscape-fcose: 2.2.0(cytoscape@3.34.0) + d3: 7.9.0 + d3-sankey: 0.12.3 + dagre-d3-es: 7.0.14 + dayjs: 1.11.21 + dompurify: 3.4.13 + es-toolkit: 1.50.0 + katex: 0.16.47 + khroma: 2.1.0 + marked: 16.4.2 + roughjs: 4.6.6 + stylis: 4.4.0 + ts-dedent: 2.3.0 + uuid: 14.0.1 + + nanoid@3.3.17: {} + + next@16.3.0(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + '@next/env': 16.3.0 + '@swc/helpers': 0.5.15 + baseline-browser-mapping: 2.11.12 + caniuse-lite: 1.0.30001807 + postcss: 8.5.23 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + styled-jsx: 5.1.6(react@19.2.8) + optionalDependencies: + '@next/swc-darwin-arm64': 16.3.0 + '@next/swc-darwin-x64': 16.3.0 + '@next/swc-linux-arm64-gnu': 16.3.0 + '@next/swc-linux-arm64-musl': 16.3.0 + '@next/swc-linux-x64-gnu': 16.3.0 + '@next/swc-linux-x64-musl': 16.3.0 + '@next/swc-win32-arm64-msvc': 16.3.0 + '@next/swc-win32-x64-msvc': 16.3.0 + sharp: 0.35.3(@types/node@25.9.5) + transitivePeerDependencies: + - '@babel/core' + - '@types/node' + - babel-plugin-macros + + node-releases@2.0.53: {} + + obug@2.1.4: {} + + package-manager-detector@1.8.0: {} + + path-data-parser@0.1.0: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.5: {} + + points-on-curve@0.2.0: {} + + points-on-path@0.2.1: + dependencies: + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.23: + dependencies: + nanoid: 3.3.17 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.26: + dependencies: + nanoid: 3.3.17 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + posthog-js@1.413.3: + dependencies: + '@posthog/browser-common': 0.4.0 + '@posthog/core': 1.46.9 + '@posthog/types': 1.402.2 + core-js: 3.50.0 + dompurify: 3.4.13 + fflate: 0.4.9 + preact: 10.29.8 + query-selector-shadow-dom: 1.0.1 + web-vitals: 5.3.0 + web-vitals-soft-navs: web-vitals@6.0.0 + transitivePeerDependencies: + - preact-render-to-string + + preact@10.29.8: {} + + prismjs@1.30.0: {} + + query-selector-shadow-dom@1.0.1: {} + + react-dom@19.2.8(react@19.2.8): + dependencies: + react: 19.2.8 + scheduler: 0.27.0 + + react@19.2.8: {} + + robust-predicates@3.0.3: {} + + rolldown@1.2.3: + dependencies: + '@oxc-project/types': 0.143.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.2.3 + '@rolldown/binding-darwin-arm64': 1.2.3 + '@rolldown/binding-darwin-x64': 1.2.3 + '@rolldown/binding-freebsd-x64': 1.2.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.3 + '@rolldown/binding-linux-arm64-gnu': 1.2.3 + '@rolldown/binding-linux-arm64-musl': 1.2.3 + '@rolldown/binding-linux-ppc64-gnu': 1.2.3 + '@rolldown/binding-linux-s390x-gnu': 1.2.3 + '@rolldown/binding-linux-x64-gnu': 1.2.3 + '@rolldown/binding-linux-x64-musl': 1.2.3 + '@rolldown/binding-openharmony-arm64': 1.2.3 + '@rolldown/binding-win32-arm64-msvc': 1.2.3 + '@rolldown/binding-win32-x64-msvc': 1.2.3 + + roughjs@4.6.6: + dependencies: + hachure-fill: 0.5.2 + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + points-on-path: 0.2.1 + + rw@1.3.3: {} + + safer-buffer@2.1.2: {} + + scheduler@0.27.0: {} + + semver@7.8.5: + optional: true + + sharp@0.35.3(@types/node@25.9.5): + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.35.3 + '@img/sharp-darwin-x64': 0.35.3 + '@img/sharp-freebsd-wasm32': 0.35.3 + '@img/sharp-libvips-darwin-arm64': 1.3.2 + '@img/sharp-libvips-darwin-x64': 1.3.2 + '@img/sharp-libvips-linux-arm': 1.3.2 + '@img/sharp-libvips-linux-arm64': 1.3.2 + '@img/sharp-libvips-linux-ppc64': 1.3.2 + '@img/sharp-libvips-linux-riscv64': 1.3.2 + '@img/sharp-libvips-linux-s390x': 1.3.2 + '@img/sharp-libvips-linux-x64': 1.3.2 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 + '@img/sharp-libvips-linuxmusl-x64': 1.3.2 + '@img/sharp-linux-arm': 0.35.3 + '@img/sharp-linux-arm64': 0.35.3 + '@img/sharp-linux-ppc64': 0.35.3 + '@img/sharp-linux-riscv64': 0.35.3 + '@img/sharp-linux-s390x': 0.35.3 + '@img/sharp-linux-x64': 0.35.3 + '@img/sharp-linuxmusl-arm64': 0.35.3 + '@img/sharp-linuxmusl-x64': 0.35.3 + '@img/sharp-webcontainers-wasm32': 0.35.3 + '@img/sharp-win32-arm64': 0.35.3 + '@img/sharp-win32-ia32': 0.35.3 + '@img/sharp-win32-x64': 0.35.3 + '@types/node': 25.9.5 + optional: true + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@4.2.0: {} + + styled-jsx@5.1.6(react@19.2.8): + dependencies: + client-only: 0.0.1 + react: 19.2.8 + + stylis@4.4.0: {} + + tailwindcss@4.3.3: {} + + tapable@2.3.3: {} + + tinybench@2.9.0: {} + + tinyexec@1.3.0: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + tinyrainbow@3.1.1: {} + + ts-dedent@2.3.0: {} + + tslib@2.8.1: {} + + typescript@5.9.3: {} + + undici-types@7.24.6: {} + + update-browserslist-db@1.2.3(browserslist@4.28.7): + dependencies: + browserslist: 4.28.7 + escalade: 3.2.0 + picocolors: 1.1.1 + + use-sync-external-store@1.6.0(react@19.2.8): + dependencies: + react: 19.2.8 + + uuid@14.0.1: {} + + vite@8.2.1(@types/node@25.9.5)(jiti@2.7.0): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.5 + postcss: 8.5.26 + rolldown: 1.2.3 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 25.9.5 + fsevents: 2.3.3 + jiti: 2.7.0 + + vitest@4.1.0(@types/node@25.9.5)(vite@8.2.1(@types/node@25.9.5)(jiti@2.7.0)): + dependencies: + '@vitest/expect': 4.1.0 + '@vitest/mocker': 4.1.0(vite@8.2.1(@types/node@25.9.5)(jiti@2.7.0)) + '@vitest/pretty-format': 4.1.0 + '@vitest/runner': 4.1.0 + '@vitest/snapshot': 4.1.0 + '@vitest/spy': 4.1.0 + '@vitest/utils': 4.1.0 + es-module-lexer: 2.3.1 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.4 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.3.0 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.1 + vite: 8.2.1(@types/node@25.9.5)(jiti@2.7.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.9.5 + transitivePeerDependencies: + - msw + + web-vitals@5.3.0: {} + + web-vitals@6.0.0: {} + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + zustand@4.5.7(@types/react@19.2.18)(react@19.2.8): + dependencies: + use-sync-external-store: 1.6.0(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + react: 19.2.8 diff --git a/packages/kilo-docs/pnpm-workspace.yaml b/packages/kilo-docs/pnpm-workspace.yaml new file mode 100644 index 00000000000..55f54edf1dd --- /dev/null +++ b/packages/kilo-docs/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +allowBuilds: + core-js: true diff --git a/packages/kilo-docs/public/img/connect/slack/slackbot-integrations-2.webp b/packages/kilo-docs/public/img/connect/slack/slackbot-integrations-2.webp new file mode 100644 index 00000000000..6c4d4b96c00 Binary files /dev/null and b/packages/kilo-docs/public/img/connect/slack/slackbot-integrations-2.webp differ diff --git a/packages/kilo-docs/public/img/connect/slack/slackbot-integrations.webp b/packages/kilo-docs/public/img/connect/slack/slackbot-integrations.webp new file mode 100644 index 00000000000..25eabb2d312 Binary files /dev/null and b/packages/kilo-docs/public/img/connect/slack/slackbot-integrations.webp differ diff --git a/packages/kilo-docs/public/img/connect/slack/slackbot-slack-authorize.webp b/packages/kilo-docs/public/img/connect/slack/slackbot-slack-authorize.webp new file mode 100644 index 00000000000..45d49a17c40 Binary files /dev/null and b/packages/kilo-docs/public/img/connect/slack/slackbot-slack-authorize.webp differ diff --git a/packages/kilo-docs/public/img/jetbrains/plugin-bundled-core.png b/packages/kilo-docs/public/img/jetbrains/plugin-bundled-core.png new file mode 100644 index 00000000000..2085550ae77 Binary files /dev/null and b/packages/kilo-docs/public/img/jetbrains/plugin-bundled-core.png differ diff --git a/packages/kilo-docs/public/img/jetbrains/plugin-custom-repository-menu.png b/packages/kilo-docs/public/img/jetbrains/plugin-custom-repository-menu.png new file mode 100644 index 00000000000..e435eac4521 Binary files /dev/null and b/packages/kilo-docs/public/img/jetbrains/plugin-custom-repository-menu.png differ diff --git a/packages/kilo-docs/public/img/jetbrains/plugin-custom-repository-url.png b/packages/kilo-docs/public/img/jetbrains/plugin-custom-repository-url.png new file mode 100644 index 00000000000..2fabd81c2c8 Binary files /dev/null and b/packages/kilo-docs/public/img/jetbrains/plugin-custom-repository-url.png differ diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/all-colors-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/all-colors-chromium-linux.png index eadcbe9dbf5..239ee516fc2 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/all-colors-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/all-colors-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e9241e89fae82f69f1fab0371423ab45c905b54ff0c60ec1408b54d60f6c4c6c -size 19636 +oid sha256:785cb43d5208977a69f03056bf16e0bbc32b29aeba1d14257848a32d19590bd7 +size 19702 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/collapsed-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/collapsed-chromium-linux.png index b24dac2c741..0753c0ae837 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/collapsed-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/collapsed-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a96dd287b6ffdbdc0360baa9af99d09c3b709a5adaef18f5e7b7e43a83e44746 -size 3134 +oid sha256:950cf0a3053a283651b2232b4a4e2cfdced3c6feb2f053b0a5976f8d169cfcf1 +size 3193 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/default-color-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/default-color-chromium-linux.png index e1edce07827..78a5302b80d 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/default-color-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/default-color-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60daae227133cedd35f93753128453177400f83233268ae19af984bc9a72ad4b -size 5203 +oid sha256:c02aed8f52a7b22d14d2068e1f87b65bb42ed6737115dadf89615ff5a4c870e5 +size 5214 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/dense-sidebar-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/dense-sidebar-chromium-linux.png index de65b45f226..0e8d89dc2b8 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/dense-sidebar-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/dense-sidebar-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4cda12827fa0de0590119ab707f6403fc98291843701ab0d4300e493b4b284aa -size 14965 +oid sha256:faae43fcbe8c42aef80184b4a815bbe6a024e0345243b5b8eb1be04d62b8611f +size 15184 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/empty-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/empty-chromium-linux.png index d6ae3eb676f..9b33fc4b7ed 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/empty-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/empty-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8099695f3cb2f79a54f6ada28eaf30a4e31afc5e660e293853d1ff34a2afaed1 -size 1605 +oid sha256:82ee97e40bd03a7c126e154c1644d878e374f3a573d81b22daf602a8e74ed840 +size 1596 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/expanded-with-items-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/expanded-with-items-chromium-linux.png index 7c85edd3e20..a05c8b3d286 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/expanded-with-items-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/expanded-with-items-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0bee9b58fb35c2484a9da4bffcae07f98a942c1097df33b549e13de781790824 -size 8026 +oid sha256:4d7fa00cb51b6ab591a6f25f6f863d17b78cfcd8166cf09d179ab485e394a084 +size 8149 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/first-and-last-section-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/first-and-last-section-chromium-linux.png index 9bec9f013a3..b21483ccfaa 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/first-and-last-section-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/first-and-last-section-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf6056375e5bf9d9ad78487d58a7fc1fb2d570333d057d412f1d70893a73e5df -size 6176 +oid sha256:1d841a283f99962a7aa3e0ecc1b24aba5a5351534e74ebe3a6794f79e6d9f374 +size 6204 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/long-section-name-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/long-section-name-chromium-linux.png index 71e123b86a3..e351fd7ab0d 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/long-section-name-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/long-section-name-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1ebd138c8d5e59bc09ce0d8d571d5598622fb61631cf5a698c80380872bef0a4 -size 7031 +oid sha256:adb4fa97f6e2dfbf1d5dd7aa58181d548c99cc0ca170f8618af72d3e6f252974 +size 7008 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/multiple-sections-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/multiple-sections-chromium-linux.png index b0039f2efd0..1eaa9c12566 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/multiple-sections-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/multiple-sections-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b629ad25ef4f3da8bd8a9e59c64cc05279dfc262db2d8d9d25c3697f48ef8393 -size 12834 +oid sha256:8caa6d6b2d7e1b0bbab8e47a486e27bcbfd7716ef84ad8771980a658c8cc41ab +size 13002 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/with-active-worktree-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/with-active-worktree-chromium-linux.png index a227600e9c2..c48be143e8a 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/with-active-worktree-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/with-active-worktree-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b78544f9c306dbcb0278502b1f929f0af5389a90b73362abbea6f76612c5def9 -size 7761 +oid sha256:089e30f789f0f592e0ee3794dfe736fc503290d05e300830fa720a5b75046894 +size 7768 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/with-busy-worktree-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/with-busy-worktree-chromium-linux.png index e269e186d42..caea07cf963 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/with-busy-worktree-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/with-busy-worktree-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4785426a34c06b0c74fe62c117528bb16ea579b2d677902c839336f6a29a380e -size 5926 +oid sha256:3792bcfd82e37fd2ae9664f0abfbd50a6823be2da55b96640557d3696b490ab9 +size 6018 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/with-pr-badges-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/with-pr-badges-chromium-linux.png index 42e7b3b4b6a..66d76c1e33b 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/with-pr-badges-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/with-pr-badges-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e2b861de27fb8e6d64becd50bb4b3b05d748fba74b98aafaf5cb2a182ff74655 -size 26259 +oid sha256:f65d353dac1fd4bf4ddaee9f7171e348b19bb279cb220b7f745651c3d65ad821 +size 26237 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/with-stale-worktree-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/with-stale-worktree-chromium-linux.png index 3e2363c6477..41f77fa6094 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/with-stale-worktree-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/with-stale-worktree-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:afa789c231b370c2ba874029a8a0597b5ca7ffff5624ddd343fcd54139a94012 -size 6132 +oid sha256:4a133a9d0b13ba7effcd1dab38a1843526b7aac99a9be5df9647d1ba7fb8d858 +size 6140 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/with-versions-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/with-versions-chromium-linux.png index 136ca2845b2..d8de11ca4b2 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/with-versions-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager-sections/with-versions-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0f11ac60e7dd3e393739e85d279bca7e53dc22af01bade22a741e5267d96300 -size 11053 +oid sha256:aad33864d63a6d5eb7868daaa7989e3271806ddd385b4847a5a4221d189c4d5c +size 11063 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/multi-project-sidebar-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/multi-project-sidebar-chromium-linux.png index c5f0c077fcb..9a31bdf34b3 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/multi-project-sidebar-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/multi-project-sidebar-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:94227c822d88aa83f286136c05d92c8209b2cb08eaf6870cf79172aa004e8402 -size 39173 +oid sha256:d1c6647a2cb4da318b9ec60761c248345c2a6fdc6e95a0dc5e03c31a939ace4c +size 38950 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/new-worktree-project-dropdown-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/new-worktree-project-dropdown-chromium-linux.png new file mode 100644 index 00000000000..3f0dd7a5811 --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/new-worktree-project-dropdown-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:88a4490322f4f70694f7b785cff41c5900b4ef8c14da104396fe55d264a624b4 +size 14535 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-approved-checks-failing-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-approved-checks-failing-chromium-linux.png index 5b2f303660f..4720473ebb2 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-approved-checks-failing-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-approved-checks-failing-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:facae793453671cf34b950352dc19041d420d096fac2a7358d1a4550e05a8033 -size 3383 +oid sha256:f2349f8c0d7049811702e5b8cbeb5fc2f402bdda8630d1a5210a970a13284197 +size 3694 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-approved-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-approved-chromium-linux.png index 784bf935344..b481fb3ef0a 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-approved-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-approved-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc81d91792781a9784775db3af44ea8058e87df9e029c2d7fc5acf1548fa7ec7 -size 3423 +oid sha256:311bb5062867449b9d5e2de41e77b399be1953fa6663097fa730218bed64a30a +size 3753 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-changes-requested-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-changes-requested-chromium-linux.png index ebf4954c6bf..87cf0cf6a55 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-changes-requested-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-changes-requested-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2923838fbc9ce24851e2b8ed6c5d4c2a098db9667fc528d1c00162595640bc05 -size 3550 +oid sha256:52cd6565a96bee5f55872fad4faf1c88030bec70d223187e51942bb70c2da253 +size 3687 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-checks-failing-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-checks-failing-chromium-linux.png index fdd58cf7fc4..4720473ebb2 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-checks-failing-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-checks-failing-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c087f8e2b50bf43f9ca03ab31e8955e995304473688bf8db88fb6901d743a1d4 -size 3541 +oid sha256:f2349f8c0d7049811702e5b8cbeb5fc2f402bdda8630d1a5210a970a13284197 +size 3694 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-checks-pending-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-checks-pending-chromium-linux.png index ebf4954c6bf..7db717a6fac 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-checks-pending-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-checks-pending-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2923838fbc9ce24851e2b8ed6c5d4c2a098db9667fc528d1c00162595640bc05 -size 3550 +oid sha256:4e29131b2a6e68fc26d032396c872698d1882eacc3830fe4059fca810981915c +size 3679 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-closed-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-closed-chromium-linux.png index 05c387099b1..4380385eb52 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-closed-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-closed-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6c13cc44cec6092d8294a535a3e6057d6e3a2b2d59cb18e09b0a073576972f80 -size 3582 +oid sha256:62b2731184acdc022a75338779296b8d217d5abe565d8d763075bfc68939b2e8 +size 3695 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-draft-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-draft-chromium-linux.png index fc5b1dc4911..481d4f5cc97 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-draft-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-draft-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:79f5495b171fc9de3d04b7e15cc30ac2f5082c6d7db3818b3e288ce9c3ba41f3 -size 3461 +oid sha256:b36b2ab3912ec70139285c19a226e02c35cb982a321e3cd140d84a29756d817e +size 3590 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-merged-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-merged-chromium-linux.png index e819a16198a..2cda01638ff 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-merged-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-merged-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dda50cfb4ebebd29e6f0791ec13ee9e16e48c9fa6f6d6be03c78b1d900516427 -size 3604 +oid sha256:1b787fe5310a002e7c5ac2bf8101964c0e4576d16db973e77a5e893b0db1fb66 +size 3728 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-no-review-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-no-review-chromium-linux.png index 8858ea455c7..3b1b3de84d1 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-no-review-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-no-review-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2680e8f97baee13ab92ac6fb3bcce56eb9cbb7e0c5fb0f76a76251abc016eac3 -size 3567 +oid sha256:e2bcc9c6e0dbf398194431bdbca27138608304e95c6de5874aeff2ca06fe73e6 +size 3684 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-pending-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-pending-chromium-linux.png index 8858ea455c7..3b1b3de84d1 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-pending-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/pr-badge-pending-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2680e8f97baee13ab92ac6fb3bcce56eb9cbb7e0c5fb0f76a76251abc016eac3 -size 3567 +oid sha256:e2bcc9c6e0dbf398194431bdbca27138608304e95c6de5874aeff2ca06fe73e6 +size 3684 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/worktree-item-active-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/worktree-item-active-chromium-linux.png index b657e55ee74..bfe0176bc25 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/worktree-item-active-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/worktree-item-active-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:270e0dafc2601996e7d048873e55db4680e8803d6800dc44b1fa6ce6239827cc -size 2017 +oid sha256:769935237790bbf85293a260a4dded22ad0f33dcffb41cbcd0d7695136775bd2 +size 2215 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/worktree-item-default-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/worktree-item-default-chromium-linux.png index bc4f471b091..4a0f64a1775 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/worktree-item-default-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/worktree-item-default-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5feb854ee2693b655ab504b8e2ed8794af77a357c3d716e2deb9668a08f3aa0c -size 1847 +oid sha256:6905b74776e20e3bdf0ca2ba6a89888e9160f5055d2f1719bc0cafa59f808134 +size 2056 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/worktree-item-pending-delete-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/worktree-item-pending-delete-chromium-linux.png index 2ec70d4ede2..7301e8f0b84 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/worktree-item-pending-delete-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/worktree-item-pending-delete-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1d9935cc07b9eefe0c1eca1852b00ac2b39b76cdf12ee2f01c52e4ac0771b39e -size 2910 +oid sha256:8cb40c61ea8472804f6a067d64cec84a6c492756abf386acded8e2df57868653 +size 3012 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/worktree-item-stale-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/worktree-item-stale-chromium-linux.png index 9cdd496d4a1..72a6a34103c 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/worktree-item-stale-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/worktree-item-stale-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:54e7142d591391c6e99631c9bf87b011321e52f17a8ee39e5aeeaa9b1350f2f9 -size 1831 +oid sha256:3ed27d7e73679fe2ccbe267cf6ca52143882ae72026d01beb8862e6add661531 +size 2207 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/worktree-item-with-stats-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/worktree-item-with-stats-chromium-linux.png index 43f94d6b804..460be84ca5c 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/worktree-item-with-stats-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/worktree-item-with-stats-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e4a37b5b7d71e0eb7f043176f410030385a8a61f3913bd6419b1d96c6cad2de -size 2422 +oid sha256:71034449e848b4d6f2fe88624694842cfddd8a1890996f6cbc66499cbefafb33 +size 2547 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/sidebar-top-bar-default-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/sidebar-top-bar-default-chromium-linux.png new file mode 100644 index 00000000000..a3d13087253 --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/sidebar-top-bar-default-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d49870048a42ea9c0166143fbb364b0d9ee45d786e3b1346fcd0eda76c533bdb +size 2414 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/work-style-onboarding-default-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/work-style-onboarding-default-chromium-linux.png index 2597a60c84d..13b431ad977 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/work-style-onboarding-default-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/work-style-onboarding-default-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e3f86dabc39700b3a86508c1e6499a546eeabf720832b62c9ce12f2d97378ebd -size 34649 +oid sha256:00092414080e2f70fb9b3166e8d06a1692af93e33717d0ab8fea5fe96e6a6982 +size 34468 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/shared/model-selector-most-used-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/shared/model-selector-most-used-chromium-linux.png new file mode 100644 index 00000000000..f6894817821 --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/shared/model-selector-most-used-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:89fa6a619fc2089fd0bb03dabe191394d3dcf3423ba066f1e8f2e2c6aac38838 +size 1085 diff --git a/packages/kilo-docs/source-links.md b/packages/kilo-docs/source-links.md index f6b84e3d290..e8471869def 100644 --- a/packages/kilo-docs/source-links.md +++ b/packages/kilo-docs/source-links.md @@ -72,6 +72,8 @@ - +- + - - diff --git a/packages/kilo-jetbrains/CHANGELOG.md b/packages/kilo-jetbrains/CHANGELOG.md index d000b8245ae..168b62f5eac 100644 --- a/packages/kilo-jetbrains/CHANGELOG.md +++ b/packages/kilo-jetbrains/CHANGELOG.md @@ -128,6 +128,66 @@ ## [Unreleased] +## [7.0.14] - 2026-08-06 + +### Fixed +- Improve slash command matching in the JetBrains plugin so typed commands resolve more reliably. +- Avoid startup crashes when the Kilo CLI database is temporarily locked by another process. + +## [7.0.13] - 2026-08-05 + +### Added + +- Show the pinned Kilo Core version and whether JetBrains is using a downloaded or bundled CLI build. + +### Fixed + +- Avoid GitHub checksum API rate limits when JetBrains verifies downloaded Kilo Core CLI assets. +- Add dropped files as JetBrains file references so attachments are available to Kilo reliably. +- Stop eager Kilo Core file watchers when running from JetBrains to reduce unnecessary background work. +- Improve JetBrains session diff rendering, including full-file editor diffs, multi-hunk diffs, fallback handling, gutter line numbers, and session-scoped diff paths. +- Speed up local recall searches in Kilo Core. +- Omit persona details from generated session names. +- Make invalid tool-argument errors clearer and more actionable to the model. +- Handle SQLite lock errors more gracefully. + +### Changed + +- Bump the JetBrains CLI pin to Kilo CLI v7.4.20. +- Include upstream OpenCode updates through v1.17.13. +- Adopt upstream reasoning variant metadata from OpenCode v1.18.11. + +## [7.0.13-rc.1] - 2026-08-05 + +### Added + +- Show the pinned Kilo Core version and whether JetBrains is using a downloaded or bundled CLI build. +- Add JetBrains developer tooling for pinning, unpinning, and updating the bundled Kilo Core CLI used by the plugin. +- Support resuming Claude and Codex sessions through the bundled Kilo Core runtime. +- Add remote CLI file delivery support for attachment flows. + +### Fixed + +- Avoid GitHub checksum API rate limits when JetBrains verifies downloaded Kilo Core CLI assets. +- Add dropped files as JetBrains file references so attachments are available to Kilo reliably. +- Stop eager Kilo Core file watchers when running from JetBrains to reduce unnecessary background work. +- Improve JetBrains session diff rendering, including full-file editor diffs, multi-hunk diffs, fallback handling, gutter line numbers, and session-scoped diff paths. +- Preserve configured subagent routing in Kilo Core. +- Defer threshold compaction during active tool loops so long-running sessions do not compact at unsafe points. +- Speed up local recall searches in Kilo Core. +- Stop inline skill-shell documentation examples from triggering permission prompts. +- Omit persona details from generated session names. +- Skip Kilo Core startup work for informational commands. +- Make invalid tool-argument errors clearer and more actionable to the model. +- Allow explicit external markdown sources in Kilo Core. +- Handle SQLite lock errors more gracefully. + +### Changed + +- Bump the JetBrains CLI pin to Kilo CLI v7.4.20. +- Include upstream OpenCode updates through v1.17.13. +- Adopt upstream reasoning variant metadata from OpenCode v1.18.11. + ## [7.0.12] - 2026-08-01 ### Added diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendSessionManager.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendSessionManager.kt index 5d0067db9c0..8cfe58b3f37 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendSessionManager.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendSessionManager.kt @@ -360,8 +360,16 @@ class KiloBackendSessionManager( files = count(files), ) - private fun revertDto(s: ai.kilocode.jetbrains.api.model.SessionRevert?) = s?.let { - revertDto(it.messageID, it.partID, it.snapshot, it.diff) + private fun revertDto(s: Any?) = when (s) { + null -> null + is ai.kilocode.jetbrains.api.model.SessionRevert -> revertDto(s.messageID, s.partID, s.snapshot, s.diff) + else -> runCatching { + val cls = s.javaClass + fun str(name: String) = cls.methods.firstOrNull { it.name == name && it.parameterCount == 0 }?.invoke(s) as? String + val message = str("getMessageID") + ?: return@runCatching null.also { log.info("revertDto reflective getMessageID missing on ${cls.name}") } + revertDto(message, str("getPartID"), str("getSnapshot"), str("getDiff")) + }.onFailure { log.info("revertDto reflective decode failed for ${s.javaClass.name}: ${it.message}") }.getOrNull() } private fun revertDto(message: String, part: String?, snapshot: String?, diff: String?) = diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt index d18a8c0dee4..09e112dca15 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt @@ -17,6 +17,7 @@ import ai.kilocode.rpc.dto.ChatEventDto import ai.kilocode.rpc.dto.CloudSessionDto import ai.kilocode.rpc.dto.CloudSessionListDto import ai.kilocode.rpc.dto.CommandDto +import ai.kilocode.rpc.dto.CommandFileDto import ai.kilocode.rpc.dto.ConfigDto import ai.kilocode.rpc.dto.ConfigPatchDto import ai.kilocode.rpc.dto.ConfigUpdateDto @@ -630,8 +631,12 @@ object KiloCliDataParser { CommandInfo( name = obj.str("name") ?: "", description = obj.str("description"), + agent = obj.str("agent"), + model = obj.str("model"), + variant = obj.str("variant"), source = obj.str("source"), hints = obj["hints"]?.jsonArray?.mapNotNull { it.jsonPrimitive.contentOrNull } ?: emptyList(), + subtask = obj.flagOrNull("subtask"), ) } @@ -662,9 +667,34 @@ object KiloCliDataParser { CommandDto( name = name, description = obj.str("description"), + agent = obj.str("agent"), + model = obj.str("model"), + variant = obj.str("variant"), source = obj.str("source"), hints = obj["hints"].arr()?.mapNotNull { it.jsonPrimitive.contentOrNull } ?: emptyList(), template = obj.str("template"), + subtask = obj.flagOrNull("subtask"), + ) + } + + fun parseAgentBehaviorCommandFiles(raw: String): List = + raw.array().mapNotNull { item -> + val obj = item.obj() ?: return@mapNotNull null + val name = obj.str("name") ?: return@mapNotNull null + val location = obj.str("location") ?: return@mapNotNull null + CommandFileDto( + name = name, + description = obj.str("description"), + agent = obj.str("agent"), + model = obj.str("model"), + variant = obj.str("variant"), + source = obj.str("source"), + builtin = obj.bool("builtin"), + location = location, + editable = obj.bool("editable"), + content = obj.str("content"), + subtask = obj.flagOrNull("subtask"), + hints = obj["hints"].arr()?.mapNotNull { it.jsonPrimitive.contentOrNull } ?: emptyList(), ) } @@ -713,6 +743,16 @@ object KiloCliDataParser { return if (prim.isString) prim.content else null } + fun parsePathConfig(raw: String): String? { + val prim = runCatching { tryParseObject(raw)?.get("config")?.jsonPrimitive }.getOrNull() ?: return null + return if (prim.isString) prim.content else null + } + + fun parsePathHome(raw: String): String? { + val prim = runCatching { tryParseObject(raw)?.get("home")?.jsonPrimitive }.getOrNull() ?: return null + return if (prim.isString) prim.content else null + } + fun parseModelState(raw: String): ModelStateDto { val obj = tryParseObject(raw) ?: return ModelStateDto() return ModelStateDto( @@ -1516,14 +1556,71 @@ object KiloCliDataParser { private fun parseRevert(obj: JsonObject?): SessionRevertDto? { if (obj == null) return null val message = obj.str("messageID") ?: return null + val diff = obj.str("diff") return SessionRevertDto( messageID = message, partID = obj.str("partID"), snapshot = obj.str("snapshot"), - diff = obj.str("diff"), + diff = diff, + diffs = parseUnifiedDiff(diff), ) } + private fun parseUnifiedDiff(diff: String?): List { + if (diff.isNullOrBlank()) return emptyList() + val lines = diff.lines() + val starts = lines.mapIndexedNotNull { index, line -> if (line.startsWith("diff --git ")) index else null } + if (starts.isEmpty()) return emptyList() + return starts.mapIndexedNotNull { index, start -> + val end = starts.getOrNull(index + 1) ?: lines.size + parseUnifiedBlock(lines.subList(start, end).joinToString("\n")) + } + } + + private fun parseUnifiedBlock(block: String): DiffFileDto? { + val lines = block.lines() + val file = unifiedFile(lines) ?: return null + return DiffFileDto( + file = file, + additions = lines.count { it.startsWith("+") && !it.startsWith("+++") }, + deletions = lines.count { it.startsWith("-") && !it.startsWith("---") }, + patch = block, + status = unifiedStatus(lines), + ) + } + + private fun unifiedFile(lines: List): String? { + val next = lines.firstOrNull { it.startsWith("+++ ") }?.removePrefix("+++ ") + val prev = lines.firstOrNull { it.startsWith("--- ") }?.removePrefix("--- ") + val path = sequenceOf(next, prev) + .filterNotNull() + .firstOrNull { it != "/dev/null" } + ?: lines.firstOrNull()?.let(::gitDiffTarget) + return path?.let(::cleanDiffPath) + } + + private fun unifiedStatus(lines: List): String = when { + lines.any { it == "new file mode" || it.startsWith("new file mode ") } -> "added" + lines.any { it == "deleted file mode" || it.startsWith("deleted file mode ") } -> "deleted" + lines.any { it.startsWith("--- /dev/null") } -> "added" + lines.any { it.startsWith("+++ /dev/null") } -> "deleted" + else -> "modified" + } + + private fun gitDiffTarget(line: String): String? { + val match = Regex("^diff --git a/(.*) b/(.*)$").find(line) ?: return null + return match.groupValues.getOrNull(2) + } + + private fun cleanDiffPath(path: String): String { + val text = path.trim().trim('"') + return when { + text.startsWith("a/") -> text.removePrefix("a/") + text.startsWith("b/") -> text.removePrefix("b/") + else -> text + } + } + // ================================================================ // Internal — status parsing // ================================================================ diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/diff/DiffFullReconstruct.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/diff/DiffFullReconstruct.kt new file mode 100644 index 00000000000..0b1f063f759 --- /dev/null +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/diff/DiffFullReconstruct.kt @@ -0,0 +1,86 @@ +package ai.kilocode.backend.diff + +/** + * Rebuilds the full "before" content of a modified file by reverse-applying a unified diff hunk + * patch to the current working-tree content. This lets the JetBrains diff editor show a whole-file + * diff (with collapsible unchanged regions) from the limited-context patch the CLI already returns — + * no CLI change required. + * + * Added and deleted files are intentionally rejected: their patches already carry every line, so the + * frontend reconstructs those full sides directly. Binary patches and any drift between the patch's + * after side and the real file (a stale/historical turn) also return null so the caller can fall back + * to the hunk-only view instead of rendering a wrong diff. + * + * Known limitation: `\ No newline at end of file` markers are dropped rather than tracked per side, so + * the reconstructed `before` inherits the after side's trailing-newline state. When exactly one side + * lacks a trailing newline, the whole-file fallback view will not surface that EOF-newline change. This + * is cosmetic and rare (the scoped hunk view still shows the marker); tracking it per side would require + * remembering which side the marker followed. + */ +internal object DiffFullReconstruct { + private val HUNK = Regex("^@@ -\\d+(?:,\\d+)? \\+(\\d+)(?:,(\\d+))? @@") + + fun before(after: String, patch: String?): String? { + if (patch.isNullOrBlank() || binary(patch) || added(patch) || deleted(patch)) return null + + val lines = if (after.isEmpty()) emptyList() else after.split("\n") + val out = ArrayList(lines.size) + var cursor = 0 // next after-line index still to emit (0-based) + var open = false + var start = 0 // 0-based after index where the current hunk begins + val afterBody = ArrayList() + val beforeBody = ArrayList() + + fun flush(): Boolean { + if (!open) return true + if (start < cursor) return false // overlapping or out-of-order hunks + while (cursor < start) { + if (cursor >= lines.size) return false + out.add(lines[cursor]); cursor++ + } + for (i in afterBody.indices) { + val idx = start + i + if (idx >= lines.size || lines[idx] != afterBody[i]) return false // working tree drifted + } + out.addAll(beforeBody) + cursor = start + afterBody.size + afterBody.clear(); beforeBody.clear() + open = false + return true + } + + // git patches are newline-terminated; drop the trailing split artifact so it is not read as a + // blank context line. Real blank context lines are " " (space-prefixed), never "". + for (raw in patch.split("\n").dropLastWhile { it.isEmpty() }) { + if (raw.startsWith("@@")) { + if (!flush()) return null + val match = HUNK.find(raw) ?: return null + val newStart = match.groupValues[1].toIntOrNull() ?: return null + val newLen = match.groupValues[2].ifEmpty { "1" }.toInt() + // For a zero-length new range git reports the line preceding the removed block, so the + // removed lines are reinserted at `newStart`; otherwise the region starts at newStart-1. + start = if (newLen == 0) newStart else newStart - 1 + open = true + continue + } + if (!open) continue // skip file headers (diff/index/---/+++) + if (raw.startsWith("\\")) continue // "\ No newline at end of file" + when (raw.firstOrNull()) { + ' ' -> { val body = raw.substring(1); afterBody.add(body); beforeBody.add(body) } + '+' -> afterBody.add(raw.substring(1)) + '-' -> beforeBody.add(raw.substring(1)) + null -> { afterBody.add(""); beforeBody.add("") } + else -> return null + } + } + if (!flush()) return null + while (cursor < lines.size) { out.add(lines[cursor]); cursor++ } + return out.joinToString("\n") + } + + fun added(patch: String): Boolean = patch.lineSequence().any { it == "--- /dev/null" } + + fun deleted(patch: String): Boolean = patch.lineSequence().any { it == "+++ /dev/null" } + + private fun binary(patch: String): Boolean = patch.lineSequence().any { it.startsWith("Binary files ") } +} diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImpl.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImpl.kt index 95185f2f204..244e5b2f2da 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImpl.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImpl.kt @@ -10,6 +10,7 @@ import ai.kilocode.rpc.KiloAgentBehaviorRpcApi import ai.kilocode.rpc.dto.AgentCreateDto import ai.kilocode.rpc.dto.AgentDetailDto import ai.kilocode.jetbrains.api.model.AgentBuilderSaveRequest +import ai.kilocode.rpc.dto.CommandFileDto import ai.kilocode.rpc.dto.ConfigPatchDto import ai.kilocode.rpc.dto.McpConfigDto import ai.kilocode.rpc.dto.McpServerConfigDto @@ -75,7 +76,7 @@ class KiloAgentBehaviorRpcApiImpl(private val backend: KiloBackendAppService? = override suspend fun reloadSkills(directory: String): Boolean { LOG.info("Skills reload requested dir=$directory") - if (hasActiveSession(directory)) { + if (hasActiveSession(directory, "Skills")) { LOG.warn("Skills reload blocked by active session dir=$directory") return false } @@ -136,6 +137,45 @@ class KiloAgentBehaviorRpcApiImpl(private val backend: KiloBackendAppService? = override suspend fun commands(directory: String) = KiloCliDataParser.parseAgentBehaviorCommands(request(directory, "/command", null)) + override suspend fun commandFiles(directory: String): List = + KiloCliDataParser.parseAgentBehaviorCommandFiles(request(directory, "/kilocode/command/files", null)) + + override suspend fun removeCommand(directory: String, location: String): Boolean = + post(directory, "/kilocode/command/remove", JsonObject(mapOf("location" to JsonPrimitive(location)))) + + override suspend fun reloadCommands(directory: String): Boolean { + LOG.info("Commands reload requested dir=$directory") + if (hasActiveSession(directory, "Commands")) { + LOG.warn("Commands reload blocked by active session dir=$directory") + return false + } + runCatching { post(directory, "/instance/reload") }.onFailure { err -> + LOG.warn("Commands reload failed dir=$directory", err) + }.getOrThrow() + LOG.info("Commands reload succeeded dir=$directory") + return true + } + + override suspend fun saveCommands(directory: String, edits: Map): Boolean { + LOG.info("Commands save requested dir=$directory count=${edits.size}") + app.requireReady() + val known = knownCommands(directory) + val roots = commandRoots(directory) + val paths = edits.map { (location, content) -> + val path = writableCommandPath(directory, location, known, roots) ?: return false + path to content + } + withContext(Dispatchers.IO) { + for ((path, content) in paths) { + Files.createDirectories(path.parent) + Files.writeString(path, content, StandardCharsets.UTF_8) + } + } + LOG.info("Command files saved dir=$directory count=${paths.size}") + LOG.info("Commands save reload deferred dir=$directory count=${paths.size}") + return true + } + override suspend fun mcpStatus(directory: String) = KiloCliDataParser.parseMcpStatus(request(directory, "/mcp", null)).also { items -> LOG.info("MCP status returned dir=$directory count=${items.size}") } @@ -193,24 +233,24 @@ class KiloAgentBehaviorRpcApiImpl(private val backend: KiloBackendAppService? = return true } - private fun hasActiveSession(directory: String): Boolean { + private fun hasActiveSession(directory: String, label: String): Boolean { val active = app.sessions.statuses.value.filterValues { it.type != "idle" } if (active.isNotEmpty()) { - LOG.info("Skills reload active statuses dir=$directory count=${active.size} types=${active.values.map { it.type }.distinct()}") + LOG.info("$label reload active statuses dir=$directory count=${active.size} types=${active.values.map { it.type }.distinct()}") return true } val permissions = runCatching { app.chat.pendingPermissions(directory) }.onFailure { err -> - LOG.warn("Skills reload pending permission check failed dir=$directory", err) + LOG.warn("$label reload pending permission check failed dir=$directory", err) }.getOrDefault(emptyList()) if (permissions.isNotEmpty()) { - LOG.info("Skills reload pending permissions dir=$directory count=${permissions.size}") + LOG.info("$label reload pending permissions dir=$directory count=${permissions.size}") return true } val questions = runCatching { app.chat.pendingQuestions(directory) }.onFailure { err -> - LOG.warn("Skills reload pending question check failed dir=$directory", err) + LOG.warn("$label reload pending question check failed dir=$directory", err) }.getOrDefault(emptyList()) if (questions.isNotEmpty()) { - LOG.info("Skills reload pending questions dir=$directory count=${questions.size}") + LOG.info("$label reload pending questions dir=$directory count=${questions.size}") return true } return false @@ -238,6 +278,11 @@ class KiloAgentBehaviorRpcApiImpl(private val backend: KiloBackendAppService? = return items.mapNotNull { item -> resolveEditablePath(item) }.toSet() } + private suspend fun knownCommands(directory: String): Set { + val items = commandFiles(directory) + return items.mapNotNull { item -> resolveEditableCommandPath(item) }.toSet() + } + private fun writablePath(directory: String, location: String, known: Set): Path? { val path = resolveSkillPath(location) if (path == null) { @@ -251,12 +296,28 @@ class KiloAgentBehaviorRpcApiImpl(private val backend: KiloBackendAppService? = return path } + private fun writableCommandPath(directory: String, location: String, known: Set, roots: Set): Path? { + val path = resolveCommandPath(location) + if (path == null) { + LOG.warn("Command save rejected: invalid location dir=$directory location=$location") + return null + } + if (path in known || newCommandPath(path, roots)) return path + LOG.warn("Command save rejected: unknown command dir=$directory path=$path") + return null + } + private fun resolveEditablePath(skill: SkillDto): Path? { val path = resolveSkillPath(skill.location) ?: return null if (urlCached(path)) return null return path } + private fun resolveEditableCommandPath(command: CommandFileDto): Path? { + if (!command.editable) return null + return resolveCommandPath(command.location) + } + private fun resolveSkillPath(location: String): Path? { val raw = normalizeWorkspacePath(location) ?: return null val path = try { @@ -268,6 +329,59 @@ class KiloAgentBehaviorRpcApiImpl(private val backend: KiloBackendAppService? = return path } + private fun resolveCommandPath(location: String): Path? { + val raw = normalizeWorkspacePath(location) ?: return null + val path = try { + Path.of(raw).normalize() + } catch (_: InvalidPathException) { + return null + } + if (!path.isAbsolute || path.fileName?.toString()?.endsWith(".md") != true) return null + return path + } + + private suspend fun commandRoots(directory: String): Set = buildSet { + addProjectCommandRoots(this, directory) + val paths = runCatching { request(directory, "/path", null) }.getOrNull() + val config = paths?.let(KiloCliDataParser::parsePathConfig) + if (config != null) addConfigCommandRoots(this, config) + val home = paths?.let(KiloCliDataParser::parsePathHome) + if (home != null) addHomeCommandRoots(this, home) + } + + private fun addProjectCommandRoots(roots: MutableSet, dir: String) { + val base = try { + Path.of(dir).normalize() + } catch (_: InvalidPathException) { + return + } + for (cfg in listOf(".kilo", ".kilocode")) { + for (name in listOf("command", "commands")) roots.add(base.resolve(cfg).resolve(name).normalize()) + } + } + + private fun addConfigCommandRoots(roots: MutableSet, dir: String) { + val base = try { + Path.of(dir).normalize() + } catch (_: InvalidPathException) { + return + } + for (name in listOf("command", "commands")) roots.add(base.resolve(name).normalize()) + } + + private fun addHomeCommandRoots(roots: MutableSet, home: String) { + val base = try { + Path.of(home).normalize() + } catch (_: InvalidPathException) { + return + } + for (cfg in listOf(".kilo", ".kilocode")) addConfigCommandRoots(roots, base.resolve(cfg).toString()) + } + + private fun newCommandPath(path: Path, roots: Set): Boolean { + return roots.any { root -> path.startsWith(root) } + } + private fun urlCached(path: Path): Boolean { return cacheRoots().any { root -> path.startsWith(root.resolve("kilo").resolve("skills").normalize()) } } diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloSessionRpcApiImpl.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloSessionRpcApiImpl.kt index fad5fb24b94..8b83922d823 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloSessionRpcApiImpl.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloSessionRpcApiImpl.kt @@ -35,6 +35,16 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onStart +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.Request +import ai.kilocode.backend.diff.DiffFullReconstruct +import java.nio.file.Files +import java.nio.file.Path /** * Backend implementation of [KiloSessionRpcApi]. @@ -166,6 +176,76 @@ class KiloSessionRpcApiImpl internal constructor( } } + override suspend fun diffSides(sessionId: String?, directory: String, file: DiffFileDto, messageId: String?): DiffFileDto? { + val patch = file.patch + if (patch.isNullOrBlank()) return null + log.info("diffSides start file=${file.file} session=${!sessionId.isNullOrBlank()} message=${!messageId.isNullOrBlank()} patch=${patch.length}") + // 1) Authoritative: a CLI with full/file support returns whole before/after from the snapshot, + // correct even for historical turns. Older CLIs ignore the params, so we detect the missing + // content and fall through to local reconstruction. + if (!sessionId.isNullOrBlank()) authoritative(sessionId, directory, file, messageId)?.let { + log.info("diffSides authoritative file=${file.file} before=${it.before?.length ?: 0} after=${it.after?.length ?: 0}") + return it + } + // 2) Fallback: read the working-tree file and reverse-apply the hunk patch to recover the whole + // "before". No CLI round-trip, so this works against any pinned CLI. + return withContext(Dispatchers.IO) { + val path = resolve(directory, file.file) + val after = path?.let { runCatching { Files.readString(it) }.getOrNull() } + val before = after?.let { DiffFullReconstruct.before(it, patch) } + log.info("diffSides fallback file=${file.file} path=${path ?: ""} after=${after?.length ?: 0} before=${before?.length ?: 0}") + if (after != null && before != null) file.copy(before = before, after = after) else null + } + } + + private fun resolve(directory: String, file: String): Path? { + val direct = Path.of(directory).resolve(file).normalize() + if (Files.isRegularFile(direct)) return direct + // dev-only: a stored diff may reference another worktree (relative, or absolute into a sibling + // worktree that isn't checked out here). Re-root onto the running worktree by trying progressively + // shorter path suffixes until one exists, so full-file diffs work across dev worktrees. + val root = System.getProperty("kilo.dev.worktree.root")?.takeIf { it.isNotBlank() }?.let(Path::of) ?: return null + val segs = Path.of(file).toList() + for (i in segs.indices) { + val candidate = segs.drop(i).fold(root) { acc, seg -> acc.resolve(seg) }.normalize() + if (Files.isRegularFile(candidate)) return candidate + } + return null + } + + // Ask the CLI for full before/after via GET /session/:id/diff?full=true&file=...; returns null when + // the pinned CLI lacks full/file support (it omits before/after) so the caller falls back locally. + private suspend fun authoritative(sessionId: String, directory: String, file: DiffFileDto, messageId: String?): DiffFileDto? { + val api = app.api ?: return null + return withContext(Dispatchers.IO) { + runCatching { + val url = (api.baseUrl.trimEnd('/') + "/").toHttpUrlOrNull() + ?.newBuilder() + ?.addPathSegment("session") + ?.addPathSegment(sessionId) + ?.addPathSegment("diff") + ?.addQueryParameter("directory", directory) + ?.addQueryParameter("full", "true") + ?.addQueryParameter("file", file.file) + ?.apply { if (!messageId.isNullOrBlank()) addQueryParameter("messageID", messageId) } + ?.build() + ?: return@runCatching null + api.client.newCall(Request.Builder().url(url).get().build()).execute().use { response -> + if (!response.isSuccessful) { + log.info("diffSides authoritative file=${file.file} http=${response.code} messageID=${messageId ?: "none"}") + return@runCatching null + } + val arr = Json.parseToJsonElement(response.body?.string().orEmpty()).jsonArray + val item = arr.firstOrNull { it.jsonObject["file"]?.jsonPrimitive?.contentOrNull == file.file }?.jsonObject + val before = item?.get("before")?.jsonPrimitive?.contentOrNull + val after = item?.get("after")?.jsonPrimitive?.contentOrNull + log.info("diffSides authoritative file=${file.file} items=${arr.size} matched=${item != null} before=${before?.length ?: 0} after=${after?.length ?: 0}") + if (before != null && after != null) file.copy(before = before, after = after) else null + } + }.onFailure { log.info("diffSides authoritative file=${file.file} error=${it.message}") }.getOrNull() + } + } + override suspend fun attachmentPart(id: String, directory: String, messageId: String, partId: String, attachmentKey: String?): PartDto? = ready { chat.attachmentPart(id, directory, messageId, partId, attachmentKey) } diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceDtoMapper.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceDtoMapper.kt index 981ea1ad28b..ad89fb9be0f 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceDtoMapper.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceDtoMapper.kt @@ -56,8 +56,12 @@ internal object KiloWorkspaceDtoMapper { fun command(c: CommandInfo) = CommandDto( name = c.name, description = c.description, + agent = c.agent, + model = c.model, + variant = c.variant, source = c.source, hints = c.hints, + subtask = c.subtask, ) fun skill(s: SkillInfo) = SkillDto( diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloWorkspaceState.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloWorkspaceState.kt index 37c58d853d6..3d926aa84da 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloWorkspaceState.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloWorkspaceState.kt @@ -135,8 +135,12 @@ data class AgentInfo( data class CommandInfo( val name: String, val description: String?, + val agent: String?, + val model: String?, + val variant: String?, val source: String?, val hints: List, + val subtask: Boolean?, ) data class SkillInfo( diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt index 93514a7a750..dab1d5b7138 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt @@ -691,7 +691,7 @@ class KiloCliDataParserTest { "messageID": "msg_rollback", "partID": "prt_rollback", "snapshot": "snap_rollback", - "diff": "diff --git a/file b/file" + "diff": "diff --git a/src/A.kt b/src/A.kt\n--- a/src/A.kt\n+++ b/src/A.kt\n@@ -1 +1,2 @@\n-old\n+new\n+more\ndiff --git a/src/Old.kt b/src/Old.kt\ndeleted file mode 100644\n--- a/src/Old.kt\n+++ /dev/null\n@@ -1 +0,0 @@\n-gone" } } } @@ -705,6 +705,13 @@ class KiloCliDataParserTest { assertEquals(2, result.session.summary?.files) assertEquals("msg_rollback", result.session.revert?.messageID) assertEquals("prt_rollback", result.session.revert?.partID) + assertEquals(2, result.session.revert?.diffs?.size) + assertEquals("src/A.kt", result.session.revert?.diffs?.get(0)?.file) + assertEquals(2, result.session.revert?.diffs?.get(0)?.additions) + assertEquals(1, result.session.revert?.diffs?.get(0)?.deletions) + assertEquals("modified", result.session.revert?.diffs?.get(0)?.status) + assertEquals("src/Old.kt", result.session.revert?.diffs?.get(1)?.file) + assertEquals("deleted", result.session.revert?.diffs?.get(1)?.status) } @Test @@ -1817,7 +1824,7 @@ class KiloCliDataParserTest { @Test fun `parseCommands - maps name, description, source, and hints`() { val raw = """[ - {"name":"init","description":"guided AGENTS.md setup","template":"static body","hints":["${'$'}ARGUMENTS"],"source":"command"}, + {"name":"init","description":"guided AGENTS.md setup","agent":"reviewer","model":"anthropic/claude-sonnet-4-6","variant":"high","template":"static body","hints":["${'$'}ARGUMENTS"],"source":"command","subtask":true}, {"name":"mcp-tool","template":"","hints":["${'$'}1","${'$'}2"],"source":"mcp"} ]""" @@ -1826,8 +1833,12 @@ class KiloCliDataParserTest { assertEquals(2, result.size) assertEquals("init", result[0].name) assertEquals("guided AGENTS.md setup", result[0].description) + assertEquals("reviewer", result[0].agent) + assertEquals("anthropic/claude-sonnet-4-6", result[0].model) + assertEquals("high", result[0].variant) assertEquals("command", result[0].source) assertEquals(listOf("\$ARGUMENTS"), result[0].hints) + assertEquals(true, result[0].subtask) assertEquals("mcp", result[1].source) assertEquals(listOf("\$1", "\$2"), result[1].hints) } @@ -1878,6 +1889,8 @@ class KiloCliDataParserTest { fun `parsePathState - extracts state from valid path response`() { val raw = """{"home":"/home/user","state":"/home/user/.local/state/kilo","config":"/home/user/.config/kilo","worktree":"/project","directory":"/project"}""" assertEquals("/home/user/.local/state/kilo", KiloCliDataParser.parsePathState(raw)) + assertEquals("/home/user/.config/kilo", KiloCliDataParser.parsePathConfig(raw)) + assertEquals("/home/user", KiloCliDataParser.parsePathHome(raw)) } @Test diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/diff/DiffFullReconstructTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/diff/DiffFullReconstructTest.kt new file mode 100644 index 00000000000..993ec8928e4 --- /dev/null +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/diff/DiffFullReconstructTest.kt @@ -0,0 +1,55 @@ +package ai.kilocode.backend.diff + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class DiffFullReconstructTest { + + @Test + fun `reconstructs before across multiple hunks keeping unchanged regions`() { + // Ten-line file; two separated single-line edits. The patch only carries 3-context hunks, so + // the unchanged gap between them must come from the working-tree content. + val after = (1..10).joinToString("\n") { if (it == 2) "TWO" else if (it == 9) "NINE" else "l$it" } + "\n" + val patch = buildString { + append("--- a/f\n+++ b/f\n") + append("@@ -1,4 +1,4 @@\n l1\n-l2\n+TWO\n l3\n l4\n") + append("@@ -7,4 +7,4 @@\n l7\n l8\n-l9\n+NINE\n l10\n") + } + + val before = DiffFullReconstruct.before(after, patch) + + assertEquals((1..10).joinToString("\n") { "l$it" } + "\n", before) + } + + @Test + fun `reconstructs before for a deletion-only hunk`() { + val after = "a\nc\n" + val patch = "--- a/f\n+++ b/f\n@@ -1,3 +1,2 @@\n a\n-b\n c\n" + + assertEquals("a\nb\nc\n", DiffFullReconstruct.before(after, patch)) + } + + @Test + fun `preserves files without a trailing newline`() { + val after = "a\nB" + val patch = "--- a/f\n+++ b/f\n@@ -1,2 +1,2 @@\n a\n-b\n+B\n\\ No newline at end of file\n" + + assertEquals("a\nb", DiffFullReconstruct.before(after, patch)) + } + + @Test + fun `returns null when context does not match the working tree`() { + val patch = "--- a/f\n+++ b/f\n@@ -1,2 +1,2 @@\n a\n-b\n+B\n" + + assertNull(DiffFullReconstruct.before("x\nB\n", patch)) + } + + @Test + fun `returns null for added deleted binary and blank patches`() { + assertNull(DiffFullReconstruct.before("hello\n", "--- /dev/null\n+++ b/f\n@@ -0,0 +1 @@\n+hello\n")) + assertNull(DiffFullReconstruct.before("", "--- a/f\n+++ /dev/null\n@@ -1 +0,0 @@\n-gone\n")) + assertNull(DiffFullReconstruct.before("x", "Binary files a/f and b/f differ\n")) + assertNull(DiffFullReconstruct.before("x", "")) + } +} diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImplTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImplTest.kt index 880e6cba4cd..6daa8b0096f 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImplTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImplTest.kt @@ -119,6 +119,76 @@ class KiloAgentBehaviorRpcApiImplTest { assertEquals(1, mock.requestCount("/instance/reload")) } + @Test + fun `command files and remove command call CLI endpoints`() = runBlocking { + val dir = Files.createTempDirectory("kilo-command-test") + val file = Files.createDirectories(dir.resolve("command")).resolve("review.md") + Files.writeString(file, "---\ndescription: Review code\n---\n\nReview $" + "ARGUMENTS") + mock.commandFiles = """[ + {"name":"review","description":"Review code","agent":"reviewer","model":"anthropic/claude-sonnet-4-6","variant":"high","source":"command","builtin":false,"location":"$file","editable":true,"content":"Review","subtask":true}, + {"name":"init","source":"command","builtin":true,"location":"builtin","editable":false,"content":"Init"} + ]""".trimIndent() + val rpc = rpc() + + val commands = rpc.commandFiles("/test project") + assertEquals(listOf("review", "init"), commands.map { it.name }) + assertEquals(true, commands.single { it.name == "review" }.editable) + assertEquals("reviewer", commands.single { it.name == "review" }.agent) + assertEquals("anthropic/claude-sonnet-4-6", commands.single { it.name == "review" }.model) + assertEquals("high", commands.single { it.name == "review" }.variant) + assertEquals(true, commands.single { it.name == "review" }.subtask) + assertEquals(false, commands.single { it.name == "init" }.editable) + + assertTrue(rpc.removeCommand("/test project", file.toString())) + assertEquals("{\"location\":\"$file\"}", mock.lastCommandRemoveBody) + assertEquals(1, mock.requestCount("/kilocode/command/remove")) + + mock.commandRemoveStatus = 400 + val err = assertFailsWith { + rpc.removeCommand("/test", "/tmp/missing.md") + } + assertContains(err.message.orEmpty(), "HTTP 400") + + assertTrue(rpc.reloadCommands("/test project")) + assertEquals(1, mock.requestCount("/instance/reload")) + } + + @Test + fun `save commands validates known and new project command paths`() = runBlocking { + val project = Files.createTempDirectory("kilo-command-project") + val known = Files.createDirectories(project.resolve(".kilo/command")).resolve("known.md") + val added = project.resolve(".kilo/commands/new.md") + val other = Files.createTempFile("kilo-command-other", ".md") + Files.writeString(known, "old") + Files.writeString(other, "old") + mock.commandFiles = """[ + {"name":"known","source":"command","builtin":false,"location":"$known","editable":true,"content":"old"} + ]""".trimIndent() + val rpc = rpc() + + assertTrue(rpc.saveCommands(project.toString(), mapOf(known.toString() to "new", added.toString() to "created"))) + assertEquals("new", Files.readString(known)) + assertEquals("created", Files.readString(added)) + assertEquals(1, mock.requestCount("/kilocode/command/files")) + + assertFalse(rpc.saveCommands(project.toString(), mapOf(other.toString() to "nope"))) + assertEquals("old", Files.readString(other)) + } + + @Test + fun `save commands validates new global command paths`() = runBlocking { + val project = Files.createTempDirectory("kilo-command-project") + val config = Files.createTempDirectory("kilo-command-config") + val added = config.resolve("commands/global.md") + mock.path = """{"home":"/tmp","state":"/tmp","config":"$config","worktree":"$project","directory":"$project"}""" + val rpc = rpc() + + assertTrue(rpc.saveCommands(project.toString(), mapOf(added.toString() to "global command"))) + + assertEquals("global command", Files.readString(added)) + assertEquals(1, mock.requestCount("/path")) + } + @Test fun `url cached skills are read only`() = runBlocking { val cache = Path.of(System.getProperty("user.home"), ".cache", "kilo", "skills", "remote") diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloSessionRpcApiImplTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloSessionRpcApiImplTest.kt index 4e0290a3f4e..19ab196b892 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloSessionRpcApiImplTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloSessionRpcApiImplTest.kt @@ -6,6 +6,7 @@ import ai.kilocode.backend.testing.FakeCliServer import ai.kilocode.backend.testing.MockCliServer import ai.kilocode.backend.testing.TestLog import ai.kilocode.rpc.dto.ChatEventDto +import ai.kilocode.rpc.dto.DiffFileDto import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -18,9 +19,16 @@ import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeout +import kotlinx.coroutines.withTimeoutOrNull +import java.nio.file.Files +import kotlin.io.path.createTempDirectory import kotlin.test.AfterTest import kotlin.test.Test +import kotlin.test.assertEquals import kotlin.test.assertFailsWith +import kotlin.test.assertIs +import kotlin.test.assertNotNull +import kotlin.test.assertNull import kotlin.test.assertTrue class KiloSessionRpcApiImplTest { @@ -29,7 +37,7 @@ class KiloSessionRpcApiImplTest { private val apps = mutableListOf() @AfterTest - fun tearDown() { + fun tearDown() = runBlocking { apps.forEach { it.dispose() } apps.clear() scope.cancel() @@ -106,4 +114,119 @@ class KiloSessionRpcApiImplTest { assertTrue(log.messages.any { it.contains("delete session: id=ses_deleted") }, log.messages.joinToString("\n")) } + + @Test + fun `diffSides rebuilds full before by reverse-applying the patch to the working tree`() = runBlocking(Dispatchers.Default) { + val dir = createTempDirectory("kilo-diff") + try { + val file = "src/Main.kt" + Files.createDirectories(dir.resolve("src")) + Files.writeString(dir.resolve(file), "a\nB2\nc\n") + val patch = "--- a/$file\n+++ b/$file\n@@ -1,3 +1,3 @@\n a\n-b2\n+B2\n c\n" + + val diff = KiloSessionRpcApiImpl().diffSides(null, dir.toString(), DiffFileDto(file, 1, 1, patch, "modified"), null) + + assertNotNull(diff) + assertEquals("a\nb2\nc\n", diff.before) + assertEquals("a\nB2\nc\n", diff.after) + } finally { + delete(dir) + } + } + + @Test + fun `diffSides returns null when the working tree drifted from the patch`() = runBlocking(Dispatchers.Default) { + val dir = createTempDirectory("kilo-diff") + try { + val file = "src/Main.kt" + Files.createDirectories(dir.resolve("src")) + Files.writeString(dir.resolve(file), "a\nUNRELATED\nc\n") + val patch = "--- a/$file\n+++ b/$file\n@@ -1,3 +1,3 @@\n a\n-b2\n+B2\n c\n" + + assertNull(KiloSessionRpcApiImpl().diffSides(null, dir.toString(), DiffFileDto(file, 1, 1, patch, "modified"), null)) + } finally { + delete(dir) + } + } + + @Test + fun `diffSides returns null for added files and missing patches`() = runBlocking(Dispatchers.Default) { + val dir = createTempDirectory("kilo-diff") + try { + Files.writeString(dir.resolve("new.kt"), "hello\n") + val added = "--- /dev/null\n+++ b/new.kt\n@@ -0,0 +1 @@\n+hello\n" + + assertNull(KiloSessionRpcApiImpl().diffSides(null, dir.toString(), DiffFileDto("new.kt", 1, 0, added, "added"), null)) + assertNull(KiloSessionRpcApiImpl().diffSides(null, dir.toString(), DiffFileDto("new.kt", 1, 0, null, "added"), null)) + } finally { + delete(dir) + } + } + + @Test + fun `diffSides prefers authoritative CLI content over local reconstruction`() = runBlocking(Dispatchers.Default) { + val server = MockCliServer() + try { + server.sessionDiff = + """[{"file":"src/Main.kt","additions":1,"deletions":1,"status":"modified","patch":"p","before":"OLD\n","after":"NEW\n"}]""" + val api = KiloSessionRpcApiImpl(app(server)) + + // No working-tree file exists here, so a non-null result can only come from the CLI path. + val diff = api.diffSides("ses_test", "/does-not-exist", DiffFileDto("src/Main.kt", 1, 1, "p", "modified"), "msg1") + + assertNotNull(diff) + assertEquals("OLD\n", diff.before) + assertEquals("NEW\n", diff.after) + val path = assertNotNull(server.lastSessionDiffPath) + assertTrue(path.contains("full=true"), path) + assertTrue(path.contains("file=src%2FMain.kt"), path) + assertTrue(path.contains("messageID=msg1"), path) + } finally { + server.close() + } + } + + @Test + fun `diffSides falls back to local reconstruction when the CLI omits full content`() = runBlocking(Dispatchers.Default) { + val server = MockCliServer() + val dir = createTempDirectory("kilo-diff") + try { + // A CLI without full/file support returns the file entry but no before/after. + server.sessionDiff = """[{"file":"src/Main.kt","additions":1,"deletions":1,"status":"modified","patch":"p"}]""" + Files.createDirectories(dir.resolve("src")) + Files.writeString(dir.resolve("src/Main.kt"), "a\nB2\nc\n") + val patch = "--- a/src/Main.kt\n+++ b/src/Main.kt\n@@ -1,3 +1,3 @@\n a\n-b2\n+B2\n c\n" + val api = KiloSessionRpcApiImpl(app(server)) + + val diff = api.diffSides("ses_test", dir.toString(), DiffFileDto("src/Main.kt", 1, 1, patch, "modified"), "msg1") + + assertNotNull(diff) + assertEquals("a\nb2\nc\n", diff.before) + assertEquals("a\nB2\nc\n", diff.after) + } finally { + delete(dir) + server.close() + } + } + + private suspend fun app(server: MockCliServer): KiloBackendAppService { + val app = KiloBackendAppService.create(scope, FakeCliServer(server), TestLog()).also { apps.add(it) } + app.connect() + val state = assertNotNull( + withTimeoutOrNull(35_000) { + app.appState.first { + it is KiloAppState.Ready || it is KiloAppState.Error || it is KiloAppState.MigrationRequired + } + }, + "App startup timed out in ${app.appState.value}", + ) + assertIs(state, "App startup failed") + return app + } + + private fun delete(dir: java.nio.file.Path) { + Files.walk(dir).use { paths -> + paths.sorted(Comparator.reverseOrder()).forEach { Files.deleteIfExists(it) } + } + } } diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/MockCliServer.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/MockCliServer.kt index d016709a685..1d35de360a2 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/MockCliServer.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/MockCliServer.kt @@ -68,10 +68,12 @@ class MockCliServer : AutoCloseable { @Volatile var mcpStatus = 200 @Volatile var mcpActionStatus = 200 @Volatile var agentRemoveStatus = 200 + @Volatile var commandRemoveStatus = 200 @Volatile var skillRemoveStatus = 200 @Volatile var agentBuilderStatus = 200 @Volatile var lastMcpActionPath: String? = null @Volatile var lastAgentRemoveBody: String? = null + @Volatile var lastCommandRemoveBody: String? = null @Volatile var lastSkillRemoveBody: String? = null @Volatile var lastAgentBuilderPath: String? = null @Volatile var lastAgentBuilderBody: String? = null @@ -83,11 +85,13 @@ class MockCliServer : AutoCloseable { @Volatile var providersAfterAuthPut: String? = null @Volatile var agents = "[]" @Volatile var commands = "[]" + @Volatile var commandFiles = "[]" @Volatile var skills = "[]" @Volatile var providersStatus = 200 @Volatile var providerAuthStatus = 200 @Volatile var agentsStatus = 200 @Volatile var commandsStatus = 200 + @Volatile var commandFilesStatus = 200 @Volatile var skillsStatus = 200 // File search responses @@ -101,6 +105,8 @@ class MockCliServer : AutoCloseable { @Volatile var recentSessions = "[]" @Volatile var sessionCreate = """{"id":"ses_test","slug":"test","projectID":"prj_test","directory":"/test","title":"New Session","version":"1.0.0","time":{"created":1000,"updated":1000}}""" @Volatile var sessionStatuses = "{}" + @Volatile var sessionDiff = "[]" + @Volatile var lastSessionDiffPath: String? = null @Volatile var summarizeResponse = "true" @Volatile var sessionsStatus = 200 @Volatile var recentSessionsStatus = 200 @@ -368,7 +374,7 @@ class MockCliServer : AutoCloseable { respond(output, organizationSetStatus, "true") } path == "/global/event" -> handleSse(output, latch) - path == "/path" -> respond(output, 200, this.path) + bare == "/path" -> respond(output, 200, this.path) bare == "/provider" -> respond(output, providersStatus, providers) bare == "/provider/auth" -> respond(output, providerAuthStatus, providerAuth) bare == "/agent" -> respond(output, agentsStatus, agents) @@ -382,6 +388,11 @@ class MockCliServer : AutoCloseable { lastAgentRemoveBody = body respond(output, agentRemoveStatus, if (agentRemoveStatus == 200) "true" else """{"error":"Agent not found"}""") } + bare == "/kilocode/command/files" -> respond(output, commandFilesStatus, commandFiles) + bare == "/kilocode/command/remove" && method == "POST" -> { + lastCommandRemoveBody = body + respond(output, commandRemoveStatus, if (commandRemoveStatus == 200) "true" else """{"error":"Command not found"}""") + } bare == "/kilocode/skill/remove" && method == "POST" -> { lastSkillRemoveBody = body respond(output, skillRemoveStatus, if (skillRemoveStatus == 200) "true" else """{"error":"Skill not found"}""") @@ -431,6 +442,10 @@ class MockCliServer : AutoCloseable { lastSessionRenameMethod = method respond(output, sessionRenameStatus, sessionRenameResponse) } + bare.matches(Regex("/session/ses_[^/]+/diff")) && method == "GET" -> { + lastSessionDiffPath = path + respond(output, 200, sessionDiff) + } bare.matches(Regex("/session/ses_[^/]+/summarize")) && method == "POST" -> { lastSummarizePath = path lastSummarizeBody = body diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloSessionService.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloSessionService.kt index d5f86b95a2a..5ce4f747c07 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloSessionService.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloSessionService.kt @@ -226,6 +226,9 @@ class KiloSessionService internal constructor( suspend fun diff(id: String, dir: String): List = call { diff(id, dir) } + suspend fun diffSides(sessionId: String?, dir: String, file: DiffFileDto, messageId: String?): DiffFileDto? = + call { diffSides(sessionId, dir, file, messageId) } + suspend fun attachmentPart(id: String, dir: String, message: String, part: String, key: String?): PartDto? = call { attachmentPart(id, dir, message, part, key) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/diff/DiffBlocks.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/diff/DiffBlocks.kt index 538a1686ace..6b8b8d0f0f4 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/diff/DiffBlocks.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/diff/DiffBlocks.kt @@ -3,12 +3,15 @@ package ai.kilocode.client.diff import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.rpc.dto.DiffFileDto import com.intellij.diff.DiffContentFactory +import com.intellij.diff.contents.DocumentContent import com.intellij.diff.requests.DiffRequest import com.intellij.diff.requests.SimpleDiffRequest import com.intellij.diff.util.DiffUserDataKeys +import com.intellij.diff.util.DiffUserDataKeysEx import com.intellij.openapi.fileTypes.FileTypeManager import com.intellij.openapi.project.Project import com.intellij.openapi.vcs.FileStatus +import java.util.function.IntUnaryOperator internal fun diffRequest( project: Project, @@ -22,15 +25,18 @@ internal fun diffRequest( val status = fileStatus(dto) val patch = dto.patch?.takeIf { it.isNotBlank() } val fallback = patch ?: KiloBundle.message("diff.editor.patch.unavailable") + val full = dto.before != null && dto.after != null val left = when { + full -> factory.create(project, dto.before.orEmpty(), type) DiffPatchReconstruct.added(dto.patch) -> factory.createEmpty() - sides.renderable -> factory.create(project, sides.before, type) + sides.renderable -> factory.create(project, sides.before, type).numbered(sides.leftLines) status == FileStatus.DELETED -> factory.create(project, fallback, type) else -> factory.createEmpty() } val right = when { + full -> factory.create(project, dto.after.orEmpty(), type) DiffPatchReconstruct.deleted(dto.patch) -> factory.createEmpty() - sides.renderable -> factory.create(project, sides.after, type) + sides.renderable -> factory.create(project, sides.after, type).numbered(sides.rightLines) status == FileStatus.DELETED -> factory.createEmpty() else -> factory.create(project, fallback, type) } @@ -43,3 +49,10 @@ internal fun diffTitle(file: String, branch: String?): String { val name = branch.takeIf { !it.isNullOrBlank() } ?: return file return KiloBundle.message("diff.editor.file.title", file, name) } + +// The hunk-fallback content is a concatenation of hunk bodies, so its own document lines restart at 1. +// Remap each document line to its real source-file line (0-based; the platform adds 1) so the gutter +// matches the file instead of showing 1..N. Lines with no mapping return -1 and the platform hides them. +private fun DocumentContent.numbered(lines: List): DocumentContent = apply { + if (lines.isNotEmpty()) putUserData(DiffUserDataKeysEx.LINE_NUMBER_CONVERTOR, IntUnaryOperator { lines.getOrElse(it) { -1 } }) +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/diff/DiffPatchReconstruct.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/diff/DiffPatchReconstruct.kt index 654ef86da03..b29812ca12d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/diff/DiffPatchReconstruct.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/diff/DiffPatchReconstruct.kt @@ -6,21 +6,29 @@ internal data class DiffSides( val before: String, val after: String, val renderable: Boolean, + // 0-based source-file line for each reconstructed document line, so the diff editor gutter shows + // real file positions (and jumps across the elided inter-hunk gaps) instead of restarting at 1. + val leftLines: List = emptyList(), + val rightLines: List = emptyList(), ) internal object DiffPatchReconstruct { - private val HUNK = Regex("^@@ -\\d+(?:,(\\d+))? \\+\\d+(?:,(\\d+))? @@") + private val HUNK = Regex("^@@ -(\\d+)(?:,(\\d+))? \\+(\\d+)(?:,(\\d+))? @@") fun sides(dto: DiffFileDto): DiffSides { val patch = dto.patch if (patch.isNullOrBlank() || binary(patch)) return DiffSides("", "", false) val before = StringBuilder() val after = StringBuilder() + val leftLines = mutableListOf() + val rightLines = mutableListOf() var hunks = 0 var oldLen = 0 var newLen = 0 var oldSeen = 0 var newSeen = 0 + var oldLine = 1 + var newLine = 1 // Drop the trailing empty element that split('\n') yields for a newline-terminated patch (the // usual case for git output). Counting it as a body line would inflate oldSeen/newSeen past the // header lengths and wrongly reject every full-context diff. Mirrors DiffLineNumbers' edge trim; @@ -29,8 +37,10 @@ internal object DiffPatchReconstruct { if (line.startsWith("@@")) { hunks += 1 HUNK.find(line)?.let { match -> - oldLen += match.groupValues[1].ifEmpty { "1" }.toInt() - newLen += match.groupValues[2].ifEmpty { "1" }.toInt() + oldLine = match.groupValues[1].toInt() + newLine = match.groupValues[3].toInt() + oldLen += match.groupValues[2].ifEmpty { "1" }.toInt() + newLen += match.groupValues[4].ifEmpty { "1" }.toInt() } continue } @@ -38,29 +48,40 @@ internal object DiffPatchReconstruct { if (line.startsWith("\\")) continue when (line.firstOrNull()) { ' ' -> { - before.appendLine(line.substring(1)) - after.appendLine(line.substring(1)) + before.appendLine(line.substring(1)); leftLines.add(oldLine++ - 1) + after.appendLine(line.substring(1)); rightLines.add(newLine++ - 1) oldSeen += 1 newSeen += 1 } - '-' -> { before.appendLine(line.substring(1)); oldSeen += 1 } - '+' -> { after.appendLine(line.substring(1)); newSeen += 1 } + '-' -> { before.appendLine(line.substring(1)); leftLines.add(oldLine++ - 1); oldSeen += 1 } + '+' -> { after.appendLine(line.substring(1)); rightLines.add(newLine++ - 1); newSeen += 1 } else -> { - before.appendLine("") - after.appendLine("") + before.appendLine(""); leftLines.add(oldLine++ - 1) + after.appendLine(""); rightLines.add(newLine++ - 1) oldSeen += 1 newSeen += 1 } } } - // Both producers (CLI snapshot and branchDiff) emit a single full-context hunk. A patch with - // several hunks, or one whose header lengths don't match the reconstructed body, has elided - // context: reconstructing would place every line at the wrong number, so fall back to the - // raw-patch view (renderable = false) instead of showing a misaligned side-by-side diff. - if (hunks != 1 || oldSeen != oldLen || newSeen != newLen) return DiffSides("", "", false) - val left = if (added(patch)) "" else before.toString().removeSuffix("\n") - val right = if (deleted(patch)) "" else after.toString().removeSuffix("\n") - return DiffSides(left, right, true) + // A patch may carry several hunks (limited-context git output) or a single full-context hunk. + // We concatenate every hunk body into contiguous before/after text: unchanged context lines + // anchor each region so the resulting side-by-side still colors adds/removes correctly. The + // elided gaps between hunks collapse (line numbers restart at 1), which is acceptable for a + // "what changed" view and far better than the all-green raw-patch fallback. We still bail when + // there is no hunk, or when the header lengths don't match the reconstructed body (truncated + // context), because that would place lines against the wrong side. + if (hunks < 1 || oldSeen != oldLen || newSeen != newLen) return DiffSides("", "", false) + val added = added(patch) + val deleted = deleted(patch) + val left = if (added) "" else before.toString().removeSuffix("\n") + val right = if (deleted) "" else after.toString().removeSuffix("\n") + return DiffSides( + left, + right, + true, + if (added) emptyList() else leftLines, + if (deleted) emptyList() else rightLines, + ) } fun added(patch: String?): Boolean = patch?.lineSequence()?.any { it == "--- /dev/null" } == true diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/diff/KiloDiffEditorKind.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/diff/KiloDiffEditorKind.kt index f8d61745f8e..e257e62d971 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/diff/KiloDiffEditorKind.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/diff/KiloDiffEditorKind.kt @@ -150,17 +150,58 @@ internal class KiloDiffEditorService( val dir = params["directory"].takeIfPresent() ?: return DiffEditorData.Empty val workspace = service() val store = project.service() + val session = project.service() val files = when (params["source"]) { // branch is authoritative here (no store seeding): recompute on every load/refresh so a // re-open or Refresh always reflects the current worktree instead of a stale click seed. "branch" -> workspace.branchDiff(dir) "inline" -> store.get(params["token"].orEmpty()).orEmpty() - else -> project.service().diff(params["sessionId"].orEmpty(), dir) + else -> session.diff(params["sessionId"].orEmpty(), dir) } if (files.isEmpty()) return DiffEditorData.Empty val branch = params["branch"].takeIfPresent() ?: if (params["source"] == "branch") workspace.branchName(dir) else null - return DiffEditorData.Files(files, branch) + return DiffEditorData.Files(detail(params, dir, files, session), branch) + } + + // Enrich modified files with full before/after content so the editor shows whole-file diffs. + // Added/deleted/binary files already render fully from their patch, so they skip the round-trip; + // a null result (working tree drifted from the patch) falls back to the hunk view. + private suspend fun detail( + params: Map, + dir: String, + files: List, + session: KiloSessionService, + ): List { + // Revert diffs already carry range-scoped patches from the CLI's `revert.diff`. Whole-file + // enrichment has no per-message scope for a revert here, so the authoritative endpoint would + // return the whole-session before/after and splice in changes from kept turns. Render the + // scoped hunk patches directly instead. + if (params["token"].takeIfPresent()?.startsWith("revert:") == true) return files + val sessionId = params["sessionId"].takeIfPresent() + val message = message(params) + LOG.info("diff editor detail source=${params["source"]} files=${files.size} session=${!sessionId.isNullOrBlank()} message=${!message.isNullOrBlank()}") + return files.map { file -> + val patch = file.patch + if (patch.isNullOrBlank() || DiffPatchReconstruct.added(patch) || DiffPatchReconstruct.deleted(patch)) { + LOG.info("diff editor detail skip file=${file.file} patch=${!patch.isNullOrBlank()} status=${file.status}") + file + } else { + val detail = runCatching { session.diffSides(sessionId, dir, file, message) } + .onFailure { LOG.warn("diff editor detail failed file=${file.file}", it) } + .getOrNull() + LOG.info("diff editor detail file=${file.file} full=${detail?.before != null && detail?.after != null} before=${detail?.before?.length ?: 0} after=${detail?.after?.length ?: 0}") + detail ?: file + } + } + } + + // Turn diffs carry "turn::" and single-edit diffs carry "tool::"; + // the third segment is the message the CLI scopes the authoritative snapshot diff to. Other sources + // (session, branch) have no per-message scope. + private fun message(params: Map): String? { + val parts = params["token"].takeIfPresent()?.split(":", limit = 3) ?: return null + return if (parts.size == 3 && (parts[0] == "turn" || parts[0] == "tool")) parts[2].takeIfPresent() else null } private companion object { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt index d4b62d08fbd..de437e63a45 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt @@ -822,15 +822,17 @@ class SessionUi( } private fun openInlineDiff(files: List, title: String, key: String) { + val dir = controller.sessionDirectory cs.launch { - val branch = workspaces.branchName(workspace.directory) + val branch = workspaces.branchName(dir) val label = branch?.let { KiloBundle.message("diff.editor.inline.title.named", title, it) } ?: title + LOG.info("open inline diff session=${controller.id ?: "pending"} dir=${ChatLogSummary.dir(dir)} files=${files.size}") withContext(Dispatchers.Main) { ensureDiffEditorKind() project.service().put(key, files) project.service().open( KiloDiffEditorKind.ID, - diffParams("inline", workspace.directory, controller.id, label, token = key), + diffParams("inline", dir, controller.id, label, token = key), ) Telemetry.send("Diff Editor Opened", mapOf("source" to "inline")) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt index 6373645a785..22f933d6e0e 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt @@ -190,6 +190,7 @@ class SessionController( val autoApprove: Boolean get() = KiloPluginSettings.getAutoApprove() internal val blank: Boolean get() = ref == null && model.isEmpty() && !model.showSession internal val id: String? get() = sid + internal val sessionDirectory: String get() = model.session?.directory ?: (ref as? SessionRef.Local)?.session?.directory ?: directory internal val refKey: String? get() = ref?.key internal val refType: SessionRef.Type? get() = ref?.type @@ -1012,6 +1013,7 @@ class SessionController( } } recoverPending(id) + seedRevertDiff(id) runEdt { if (disposed) return@runEdt if (sid != id) return@runEdt @@ -1061,6 +1063,7 @@ class SessionController( } } recoverPending(session.id) + seedRevertDiff(session.id) runEdt { if (disposed) return@runEdt subscribeEvents() @@ -1092,6 +1095,25 @@ class SessionController( } } + /** + * Seed [SessionModel.diff] when opening a reverted session. The rolled-back file list in + * [ai.kilocode.client.session.ui.RevertBanner] falls back to `model.diff` when the CLI does not + * attach a diff to the revert marker. On a live revert a `session.diff` event seeds that; on + * reload nothing does, so fetch the persisted session diff once here. Skipped for sessions + * without a revert or once a diff is already present (e.g. a concurrent `session.diff` event). + */ + private suspend fun seedRevertDiff(id: String) { + var fetch = false + runEdt { fetch = !disposed && sid == id && model.revert() != null && model.diff.isEmpty() } + if (!fetch) return + val diffs = runCatching { sessions.diff(id, directory) }.getOrNull()?.takeIf { it.isNotEmpty() } ?: return + runEdt { + if (disposed || sid != id) return@runEdt + if (model.revert() == null || model.diff.isNotEmpty()) return@runEdt + updateModel { model.setDiff(diffs) } + } + } + private fun startSessionLoading(token: SessionLoadState.Loading) { assertEdt() setSessionLoadState(token) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/Message.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/Message.kt index 7eeaa951500..beee188bd70 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/Message.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/Message.kt @@ -77,6 +77,8 @@ class FileAttachment(id: String) : Content(id) { /** Tool invocation with lifecycle state. */ class Tool(id: String, val name: String, var kind: ToolKind) : Content(id) { + /** Owning message id. The CLI scopes authoritative snapshot diffs by message, not part, id. */ + var messageID: String? = null var state: ToolExecState = ToolExecState.PENDING var callId: String? = null var title: String? = null diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionModel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionModel.kt index 14f7b8c0271..1d918c70bcd 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionModel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionModel.kt @@ -554,6 +554,7 @@ class SessionModel { source = dto.source } "tool" -> Tool(dto.id, dto.tool ?: "unknown", toolKind(dto.tool)).apply { + messageID = dto.messageID state = parseToolState(dto.state) callId = dto.callID title = dto.title diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/RevertBanner.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/RevertBanner.kt index 7eb0106b7c5..e1273bdfc11 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/RevertBanner.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/RevertBanner.kt @@ -1,22 +1,36 @@ package ai.kilocode.client.session.ui import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.client.session.SessionDiffOpener import ai.kilocode.client.session.model.SessionModel import ai.kilocode.client.session.model.SessionState import ai.kilocode.client.session.ui.style.SessionEditorStyle import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget +import ai.kilocode.client.session.views.SessionViewIcons import ai.kilocode.client.session.views.base.BaseQuestionView +import ai.kilocode.client.session.views.base.PartHeader import ai.kilocode.client.ui.DiffStatBadge +import ai.kilocode.client.ui.ToolbarButtonAction import ai.kilocode.client.ui.UiStyle import ai.kilocode.client.ui.layout.Stack +import ai.kilocode.client.ui.toolbarButton +import ai.kilocode.rpc.dto.DiffFileDto import com.intellij.icons.AllIcons import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBScrollPane import com.intellij.util.concurrency.annotations.RequiresEdt import com.intellij.util.ui.JBFont +import com.intellij.util.ui.JBUI import com.intellij.util.ui.UIUtil import com.intellij.util.ui.components.BorderLayoutPanel import java.awt.BorderLayout +import java.awt.Component +import java.awt.Container +import java.awt.Dimension +import java.nio.file.Path +import javax.swing.JComponent import javax.swing.JPanel +import javax.swing.ScrollPaneConstants class RevertBanner( private val model: SessionModel, @@ -24,15 +38,54 @@ class RevertBanner( private val redoAllAction: () -> Unit, private val cancelAction: () -> Unit, focus: (() -> Unit)? = null, + private var openDiff: SessionDiffOpener = { _, _, _ -> }, + private var sessionId: String? = null, ) : BorderLayoutPanel(), SessionView, SessionEditorStyleTarget { override val sessionViewKind = SessionView.Kind.Default + companion object { + /** Cap the reverted-file list to this many rows before scrolling. */ + const val MAX_FILE_ROWS = 10 + } + private val card = BaseQuestionView(focus = focus) + private val title = JBLabel() + + private val diff = toolbarButton( + ToolbarButtonAction(SessionViewIcons.openDiff, KiloBundle.message("session.part.tool.openDiff"), ::openDiffViewer), + ).apply { + isEnabled = false + isVisible = false + } + + private val header = PartHeader().apply { + leading(JBLabel(AllIcons.Actions.Back)) + left(title) + left(PartHeader.centered(diff)) + } + private val body = Stack.vertical(UiStyle.Gap.lg()) private val files = Stack.vertical(UiStyle.Gap.xs()) + private val scroll = object : JBScrollPane( + files, + ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, + ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED, + ) { + override fun getPreferredSize(): Dimension { + val size = super.getPreferredSize() + val cap = rowCap() + return Dimension(size.width, if (cap > 0) minOf(size.height, cap) else size.height) + } + }.apply { + border = JBUI.Borders.empty() + viewportBorder = JBUI.Borders.empty() + isOpaque = false + viewport.isOpaque = false + } + private val rows = LinkedHashMap() private var progress: RevertProgress? = null @@ -46,8 +99,8 @@ class RevertBanner( init { isOpaque = false - card.setHeaderIcon(AllIcons.Actions.Back, KiloBundle.message("revert.message.rollback")) - body.next(files).next(hint).next(notice) + card.setTopPanel(header) + body.next(scroll).next(hint).next(notice) card.setContent(body) card.setActions(listOf( BaseQuestionView.Action("redo", KiloBundle.message("revert.banner.redo"), primary = false) { redoAction() }, @@ -58,22 +111,33 @@ class RevertBanner( update() } + @RequiresEdt + fun setDiffOpener(openDiff: SessionDiffOpener, sessionId: String?) { + this.openDiff = openDiff + this.sessionId = sessionId + } + @RequiresEdt fun update() { val revert = model.revert() isVisible = revert != null if (revert == null) return val total = model.revertedCount() - card.setHeader(KiloBundle.message(if (total == 1) "revert.banner.count.one" else "revert.banner.count.other", total)) + title.text = KiloBundle.message(if (total == 1) "revert.banner.count.one" else "revert.banner.count.other", total) card.setActionVisible("all", total > 1) notice.isVisible = revert.snapshot == null - val keep = model.diff.mapTo(LinkedHashSet()) { it.file } + val diffs = resolveDiffs(revert) + val names = disambiguate(diffs.map { it.file }) + diff.isVisible = diffs.isNotEmpty() + diff.isEnabled = diffs.isNotEmpty() + val keep = diffs.mapTo(LinkedHashSet()) { it.file } rows.entries.removeIf { it.key !in keep } - val order = model.diff.map { item -> + scroll.isVisible = diffs.isNotEmpty() + val order = diffs.map { item -> val row = rows.getOrPut(item.file) { - Row(item.file) + Row(item) } - row.update(item.file, item.additions, item.deletions) + row.update(item, names[item.file] ?: item.file, absolute(item.file)) row.panel } if (files.components.toList() != order) { @@ -105,30 +169,92 @@ class RevertBanner( override fun applyStyle(style: SessionEditorStyle) { card.applyStyle(style) + title.font = style.headerFont + title.foreground = UiStyle.Colors.fg() progress?.applyStyle(style) hint.foreground = UIUtil.getLabelForeground() notice.foreground = UIUtil.getContextHelpForeground() rows.values.forEach { it.applyStyle() } } - private class Row(file: String) { - private val label = JBLabel(file) - private val badge = DiffStatBadge(0, 0) + /** + * The rolled-back diff to render. Prefer the snapshot diff the CLI attaches to the revert; fall + * back to the session's current file diff when the pinned CLI doesn't provide it. Empty when no + * snapshot exists (files were not restored, so there is nothing to diff). + */ + private fun resolveDiffs(revert: ai.kilocode.rpc.dto.SessionRevertDto): List = + if (revert.snapshot == null) emptyList() else revert.diffs.ifEmpty { model.diff } + + private fun openDiffViewer() { + val revert = model.revert() ?: return + val diffs = resolveDiffs(revert) + if (diffs.isEmpty()) return + openDiff(diffs, KiloBundle.message("revert.banner.openDiff.title"), "revert:${sessionId ?: "pending"}:${revert.messageID}") + } + + private fun absolute(file: String): String { + val path = runCatching { Path.of(file) }.getOrNull() ?: return file + if (path.isAbsolute) return path.normalize().toString() + val root = model.session?.directory + ?.takeIf { it.isNotBlank() } + ?.let { runCatching { Path.of(it) }.getOrNull() } + return root?.resolve(path)?.normalize()?.toString() ?: file + } + + /** Height that fits at most [MAX_FILE_ROWS] rows, or 0 when the list is short enough to show in full. */ + private fun rowCap(): Int { + val comps = files.components + if (comps.size <= MAX_FILE_ROWS) return 0 + val gap = UiStyle.Gap.xs() + return (0 until MAX_FILE_ROWS).sumOf { comps[it].preferredSize.height } + gap * (MAX_FILE_ROWS - 1) + } + + private class Row(item: DiffFileDto) { + private val label = JBLabel(item.file).apply { + toolTipText = item.file + } + private val badge = DiffStatBadge(item.additions, item.deletions) val panel: JPanel = Stack.horizontal(UiStyle.Gap.sm()) .next(label) .next(badge) init { applyStyle() + tip(item.file) } - fun update(file: String, additions: Int, deletions: Int) { - if (label.text != file) label.text = file - badge.update(additions, deletions) + fun update(item: DiffFileDto, text: String, tip: String) { + if (label.text != text) label.text = text + if (panel.toolTipText != tip) tip(tip) + badge.update(item.additions, item.deletions) } fun applyStyle() { label.foreground = UIUtil.getLabelForeground() } + + private fun tip(text: String) { + tip(panel, text) + } + + private fun tip(node: Component, text: String) { + if (node is JComponent && node.toolTipText != text) node.toolTipText = text + if (node is Container) node.components.forEach { tip(it, text) } + } } } + +internal fun disambiguate(paths: List): Map { + val parts = paths.associateWith { split(it) } + return paths.groupBy { parts[it]?.lastOrNull().orEmpty() }.values.flatMap { group -> + if (group.size == 1) return@flatMap listOf(group.first() to (parts[group.first()]?.lastOrNull() ?: group.first())) + val depth = (1..(group.maxOf { parts[it]?.size ?: 1 })).firstOrNull { count -> + group.map { suffix(parts[it].orEmpty(), count) }.toSet().size == group.size + } ?: group.maxOf { parts[it]?.size ?: 1 } + group.map { it to suffix(parts[it].orEmpty(), depth) } + }.toMap() +} + +private fun split(path: String): List = path.split('/', '\\').filter { it.isNotEmpty() } + +private fun suffix(parts: List, depth: Int): String = parts.takeLast(depth).joinToString("/") diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionLayout.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionLayout.kt index 94d57811264..c4b3d8e6f41 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionLayout.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionLayout.kt @@ -152,6 +152,33 @@ open class SessionLayoutPanel( layout = SessionLayout(gap, pad) } + override fun doLayout() { + super.doLayout() + reconcileValidateRoot() + } + + /** + * Keep the parent [SessionLayout]'s cached height honest across validate-root boundaries. + * + * A validate root, such as a settled [ai.kilocode.client.session.views.TurnView], can be laid out + * independently by `RepaintManager`. Its `isValid` flag can flip back to `true` before the parent + * transcript remeasures it, so [SessionLayout] may otherwise keep stacking it at a stale cached + * height until some unrelated resize changes the cache key. + * + * Once this root has laid out its own content, its rendered height should match its preferred + * height. If it does not, the parent cache is stale: drop this entry and revalidate the parent so + * the outer transcript geometry follows the content. Non-roots are skipped because their + * invalidation already propagates to the parent and keeps `isValid` an honest cache signal. + */ + private fun reconcileValidateRoot() { + if (!isValidateRoot()) return + val host = parent ?: return + val layout = host.layout as? SessionLayout ?: return + if (preferredSize.height == height) return + layout.forget(this) + (host as? javax.swing.JComponent)?.revalidate() + } + override fun getScrollableTracksViewportWidth() = true override fun getScrollableTracksViewportHeight() = false override fun getPreferredScrollableViewportSize(): Dimension = preferredSize diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanel.kt index 5f60ef388b3..8e6edd1ed6d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanel.kt @@ -214,6 +214,7 @@ class SessionMessageListPanel( fun setDiffOpener(openDiff: SessionDiffOpener, sessionId: String?) { this.openDiff = openDiff this.sessionId = sessionId + banner?.setDiffOpener(openDiff, sessionId) turnViews.values.forEach { it.setDiffOpener(openDiff, sessionId) } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/KiloPromptCompletionProvider.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/KiloPromptCompletionProvider.kt index a2b6331bbba..f7f5f3617d6 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/KiloPromptCompletionProvider.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/prompt/KiloPromptCompletionProvider.kt @@ -20,7 +20,10 @@ import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.fileTypes.FileTypeManager import com.intellij.openapi.project.DumbAware import com.intellij.openapi.progress.runBlockingCancellable +import com.intellij.psi.codeStyle.MinusculeMatcher +import com.intellij.psi.codeStyle.NameUtil import com.intellij.util.textCompletion.TextCompletionProvider +import com.intellij.util.text.matching.MatchingMode import java.util.Collections import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -163,12 +166,18 @@ class KiloPromptCompletionProvider( private fun slash(prefix: String, result: CompletionResultSet) { result.restartCompletionOnAnyPrefixChange() val out = result.withPrefixMatcher(PlainPrefixMatcher.ALWAYS_TRUE) + val rank = Ranker(prefix) val names = clientTokens() - val clients = actions.filter { action -> matches(prefix, action.name, action.hints) } - clients.forEach { action -> out.addElement(client(action)) } + val clients = actions.mapNotNull { action -> + rank.score(action.name, action.hints)?.let { Hit(client(action), it) } + } val commands = workspace.state.value.commands - .filter { it.name !in names && matches(prefix, it.name, it.hints) } - commands.forEach { command -> out.addElement(server(command)) } + .mapNotNull { command -> + if (command.name in names) return@mapNotNull null + rank.score(command.name, command.hints)?.let { Hit(server(command), it) } + } + val hits = (clients + commands).sortedByDescending { it.score } + hits.forEach { hit -> out.addElement(weight(hit.item, hit.score)) } if (clients.isNotEmpty() || commands.isNotEmpty()) return result.withPrefixMatcher(PlainPrefixMatcher.ALWAYS_TRUE) .addElement(info(prefix, KiloBundle.message("prompt.completion.noMatches"))) @@ -178,7 +187,8 @@ class KiloPromptCompletionProvider( result.restartCompletionOnAnyPrefixChange() val out = result.withPrefixMatcher(PlainPrefixMatcher.ALWAYS_TRUE) val search = search(prefix) - val known = mentions.filter { action -> matches(prefix, action.name, action.hints) && action.available(search) } + val rank = Ranker(prefix) + val known = mentions.filter { action -> rank.matches(action.name, action.hints) && action.available(search) } known.forEach { action -> out.addElement(prioritize(resource(action))) } if (search.indexing) { val msg = KiloBundle.message("prompt.mention.indexing") @@ -214,8 +224,37 @@ class KiloPromptCompletionProvider( } .withAutoCompletionPolicy(AutoCompletionPolicy.NEVER_AUTOCOMPLETE) - private fun matches(prefix: String, name: String, hints: List): Boolean = - (listOf(name) + hints).any { it.startsWith(prefix, ignoreCase = true) } + private class Ranker(prefix: String) { + private val start = matcher(prefix) + private val middle = if (prefix.any { separator(it) }) null else matcher("*$prefix") + + fun matches(name: String, hints: List): Boolean = score(name, hints) != null + + fun score(name: String, hints: List): Int? = (listOf(name) + hints).maxOfOrNull { value -> + score(value) ?: Int.MIN_VALUE + }?.takeIf { it != Int.MIN_VALUE } + + private fun score(value: String): Int? { + val exact = start.match(value) + if (exact != null) return START + start.matchingDegree(value, true, exact) + val fallback = middle ?: return null + val fuzzy = fallback.match(value) ?: return null + return fallback.matchingDegree(value, false, fuzzy) + } + + private companion object { + const val START = 10_000 + + fun matcher(prefix: String): MinusculeMatcher = NameUtil.buildMatcher(prefix) + .withMatchingMode(MatchingMode.IGNORE_CASE) + .build() + + fun separator(c: Char): Boolean = when (c) { + '_', '-', ':', '+', '.' -> true + else -> c.isWhitespace() + } + } + } private fun commandName(text: String): String? { val raw = text.trimStart() @@ -251,6 +290,11 @@ class KiloPromptCompletionProvider( private fun prioritize(element: LookupElement): LookupElement = PrioritizedLookupElement.withGrouping(PrioritizedLookupElement.withPriority(element, 100.0), 100) + private fun weight(element: LookupElement, score: Int): LookupElement = + PrioritizedLookupElement.withPriority(element, score.toDouble()) + + private data class Hit(val item: LookupElement, val score: Int) + private fun file(file: WorkspaceFileDto): LookupElement = LookupElementBuilder.create(file.path) .withPresentableText("@${file.path}") .withTailText(parent(file.path), true) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/base/BaseQuestionView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/base/BaseQuestionView.kt index 5d39e41feb1..113a1c7b924 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/base/BaseQuestionView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/base/BaseQuestionView.kt @@ -93,7 +93,9 @@ class BaseQuestionView( } private val headerText: JBTextArea = makeText("", UiStyle.Colors.fg(), bold = true) - private val descriptionText: JBTextArea = makeText("", UiStyle.Colors.weak(), bold = false) + private val descriptionText: JBTextArea = makeText("", UiStyle.Colors.weak(), bold = false).apply { + isVisible = false + } private var top: JComponent? = null private var content: JComponent? = null @@ -131,6 +133,7 @@ class BaseQuestionView( fun setHeader(text: String, description: String? = null) { headerText.text = text setDescription(description) + syncNorth() } /** @@ -141,6 +144,7 @@ class BaseQuestionView( fun setDescription(text: String?) { descriptionText.text = text ?: "" descriptionText.isVisible = !text.isNullOrBlank() + syncNorth() } // ---- public slot API ---- @@ -170,8 +174,7 @@ class BaseQuestionView( if (icon == null && attached) header.remove(this.icon) this.icon.revalidate() this.icon.repaint() - header.revalidate() - header.repaint() + syncNorth() } /** @@ -297,12 +300,14 @@ class BaseQuestionView( private fun syncNorth() { north.removeAll() top?.let { north.next(it) } - north.next(header) + if (hasHeader()) north.next(header) if (content != null) north.fill(gap) north.revalidate() north.repaint() } + private fun hasHeader() = icon.icon != null || headerText.text.isNotBlank() || descriptionText.isVisible + private fun syncFooter() { val layout = footer.layout as BorderLayout val west = layout.getLayoutComponent(BorderLayout.WEST) @@ -331,7 +336,8 @@ class BaseQuestionView( private fun makeText(value: String, color: Color, bold: Boolean): JBTextArea { val area = object : JBTextArea(value) { - override fun getPreferredSize() = withWidth(super.getPreferredSize().height) + override fun getPreferredSize() = + withWidth(super.getPreferredSize().height) override fun getMaximumSize(): Dimension { val size = preferredSize diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/tool/EditToolView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/tool/EditToolView.kt index 77fb712763a..831bfb6ea19 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/tool/EditToolView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/tool/EditToolView.kt @@ -254,7 +254,9 @@ class EditToolView( private fun openDiffViewer() { val files = toDiffFiles(item) if (files.isEmpty()) return - opener(files, diffTitle(item), "tool:${sessionId ?: "pending"}:${item.id}") + // The CLI scopes the authoritative snapshot diff by message id, so carry the owning message id + // (not the tool part id) in the token; otherwise the per-message lookup never matches. + opener(files, diffTitle(item), "tool:${sessionId ?: "pending"}:${item.messageID ?: item.id}") } private fun syncFilesTag(count: Int): Boolean { diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/icons/copy.svg b/packages/kilo-jetbrains/frontend/src/main/resources/icons/copy.svg index 7ee8dc0f842..f73b6c541c5 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/icons/copy.svg +++ b/packages/kilo-jetbrains/frontend/src/main/resources/icons/copy.svg @@ -1,3 +1,3 @@ - - + + diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/icons/copy_dark.svg b/packages/kilo-jetbrains/frontend/src/main/resources/icons/copy_dark.svg index fcbadd38270..b578567ed8a 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/icons/copy_dark.svg +++ b/packages/kilo-jetbrains/frontend/src/main/resources/icons/copy_dark.svg @@ -1,3 +1,3 @@ - - + + diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/icons/views/brain.svg b/packages/kilo-jetbrains/frontend/src/main/resources/icons/views/brain.svg index 304a93ea9c5..72b3551622d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/icons/views/brain.svg +++ b/packages/kilo-jetbrains/frontend/src/main/resources/icons/views/brain.svg @@ -1,3 +1,5 @@ - - + + + + diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/icons/views/brain_dark.svg b/packages/kilo-jetbrains/frontend/src/main/resources/icons/views/brain_dark.svg index 894515662bf..38e3f872ec2 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/icons/views/brain_dark.svg +++ b/packages/kilo-jetbrains/frontend/src/main/resources/icons/views/brain_dark.svg @@ -1,3 +1,5 @@ - - + + + + diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties index afa26e6f45d..84710269275 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties @@ -48,6 +48,7 @@ revert.banner.redo=Redo revert.banner.redo.all=Redo All revert.banner.hint=You can redo these changes until you send a new message revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. +revert.banner.openDiff.title=Rolled back changes revert.message.rollback=Rollback to this message session.status.rollingback=Rolling back\u2026 session.status.redoing=Redoing\u2026 diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties index a3ad22de392..42610a0aa1d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties @@ -428,6 +428,7 @@ settings.agentBehavior.skills.sources.editPath.title=تعديل مسار الم settings.agentBehavior.skills.sources.editUrl.title=تعديل URL المهارات session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. +revert.banner.openDiff.title=Rolled back changes revert.banner.count.one=تم التراجع عن رسالة واحدة revert.banner.count.other=تم التراجع عن {0} رسائل revert.banner.redo=إعادة diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties index f865d877598..91e01f95141 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties @@ -428,6 +428,7 @@ settings.agentBehavior.skills.sources.editPath.title=Uredi putanju vještina settings.agentBehavior.skills.sources.editUrl.title=Uredi URL vještina session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. +revert.banner.openDiff.title=Rolled back changes revert.banner.count.one={0} poruka vraćena revert.banner.count.other={0} poruka vraćeno revert.banner.redo=Ponovi diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties index aa558608cf1..1383c8f9c44 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties @@ -428,6 +428,7 @@ settings.agentBehavior.skills.sources.editPath.title=Rediger færdighedssti settings.agentBehavior.skills.sources.editUrl.title=Rediger færdigheds-URL session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. +revert.banner.openDiff.title=Rolled back changes revert.banner.count.one={0} besked rullet tilbage revert.banner.count.other={0} beskeder rullet tilbage revert.banner.redo=Gentag diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties index 0aaaa9b1063..598b2c11b77 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties @@ -428,6 +428,7 @@ settings.agentBehavior.skills.sources.editPath.title=Skill-Pfad bearbeiten settings.agentBehavior.skills.sources.editUrl.title=Skill-URL bearbeiten session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. +revert.banner.openDiff.title=Rolled back changes revert.banner.count.one={0} Nachricht zurückgesetzt revert.banner.count.other={0} Nachrichten zurückgesetzt revert.banner.redo=Wiederholen diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties index 9ad869fc591..344de8e9467 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties @@ -428,6 +428,7 @@ settings.agentBehavior.skills.sources.editPath.title=Editar ruta de habilidades settings.agentBehavior.skills.sources.editUrl.title=Editar URL de habilidades session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. +revert.banner.openDiff.title=Rolled back changes revert.banner.count.one={0} mensaje revertido revert.banner.count.other={0} mensajes revertidos revert.banner.redo=Rehacer diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties index dfe14978821..c09dce01edb 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties @@ -428,6 +428,7 @@ settings.agentBehavior.skills.sources.editPath.title=Modifier le chemin des comp settings.agentBehavior.skills.sources.editUrl.title=Modifier l’URL des compétences session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. +revert.banner.openDiff.title=Rolled back changes revert.banner.count.one={0} message annulé revert.banner.count.other={0} messages annulés revert.banner.redo=Rétablir diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties index 1c8e84104d9..ae011ad81d3 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties @@ -428,6 +428,7 @@ settings.agentBehavior.skills.sources.editPath.title=スキルパスを編集 settings.agentBehavior.skills.sources.editUrl.title=スキル URL を編集 session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. +revert.banner.openDiff.title=Rolled back changes revert.banner.count.one={0} 件のメッセージをロールバックしました revert.banner.count.other={0} 件のメッセージをロールバックしました revert.banner.redo=やり直し diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties index 5594fdf3199..0afc7549a47 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties @@ -428,6 +428,7 @@ settings.agentBehavior.skills.sources.editPath.title=스킬 경로 편집 settings.agentBehavior.skills.sources.editUrl.title=스킬 URL 편집 session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. +revert.banner.openDiff.title=Rolled back changes revert.banner.count.one={0}개 메시지가 롤백됨 revert.banner.count.other={0}개 메시지가 롤백됨 revert.banner.redo=다시 실행 diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties index d96e7241085..ebd3c697179 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties @@ -428,6 +428,7 @@ settings.agentBehavior.skills.sources.editPath.title=Vaardigheidspad bewerken settings.agentBehavior.skills.sources.editUrl.title=Vaardigheids-URL bewerken session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. +revert.banner.openDiff.title=Rolled back changes revert.banner.count.one={0} bericht teruggedraaid revert.banner.count.other={0} berichten teruggedraaid revert.banner.redo=Opnieuw uitvoeren diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties index 57502a00bea..6a03c40232a 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties @@ -428,6 +428,7 @@ settings.agentBehavior.skills.sources.editPath.title=Rediger ferdighetssti settings.agentBehavior.skills.sources.editUrl.title=Rediger ferdighets-URL session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. +revert.banner.openDiff.title=Rolled back changes revert.banner.count.one={0} melding rullet tilbake revert.banner.count.other={0} meldinger rullet tilbake revert.banner.redo=Gjør om diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties index 4b9bf11f462..9495b5a224d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties @@ -428,6 +428,7 @@ settings.agentBehavior.skills.sources.editPath.title=Edytuj ścieżkę umiejętn settings.agentBehavior.skills.sources.editUrl.title=Edytuj URL umiejętności session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. +revert.banner.openDiff.title=Rolled back changes revert.banner.count.one=Cofnięto {0} wiadomość revert.banner.count.other=Cofnięto {0} wiadomości revert.banner.redo=Ponów diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties index 08de02c275d..3b6a69e414d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties @@ -428,6 +428,7 @@ settings.agentBehavior.skills.sources.editPath.title=Editar caminho de habilidad settings.agentBehavior.skills.sources.editUrl.title=Editar URL de habilidades session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. +revert.banner.openDiff.title=Rolled back changes revert.banner.count.one={0} mensagem revertida revert.banner.count.other={0} mensagens revertidas revert.banner.redo=Refazer diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties index 90a13ed0ed9..50920770978 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties @@ -428,6 +428,7 @@ settings.agentBehavior.skills.sources.editPath.title=Изменить путь settings.agentBehavior.skills.sources.editUrl.title=Изменить URL навыков session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. +revert.banner.openDiff.title=Rolled back changes revert.banner.count.one=Отменено сообщений: {0} revert.banner.count.other=Отменено сообщений: {0} revert.banner.redo=Повторить diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties index ee10c59c9aa..0d5b6c363ff 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties @@ -428,6 +428,7 @@ settings.agentBehavior.skills.sources.editPath.title=แก้ไขเส้น settings.agentBehavior.skills.sources.editUrl.title=แก้ไข URL ทักษะ session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. +revert.banner.openDiff.title=Rolled back changes revert.banner.count.one=ย้อนกลับข้อความ {0} รายการแล้ว revert.banner.count.other=ย้อนกลับข้อความ {0} รายการแล้ว revert.banner.redo=ทำซ้ำ diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties index 15c992fac3f..f3a1c986195 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties @@ -428,6 +428,7 @@ settings.agentBehavior.skills.sources.editPath.title=Beceri Yolunu Düzenle settings.agentBehavior.skills.sources.editUrl.title=Beceri URL'sini Düzenle session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. +revert.banner.openDiff.title=Rolled back changes revert.banner.count.one={0} mesaj geri alındı revert.banner.count.other={0} mesaj geri alındı revert.banner.redo=Yinele diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties index b69ddc88618..7ab35b6970f 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties @@ -428,6 +428,7 @@ settings.agentBehavior.skills.sources.editPath.title=Редагувати шля settings.agentBehavior.skills.sources.editUrl.title=Редагувати URL навичок session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. +revert.banner.openDiff.title=Rolled back changes revert.banner.count.one=Відкочено повідомлень: {0} revert.banner.count.other=Відкочено повідомлень: {0} revert.banner.redo=Повторити diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties index f16bb2266b8..7905d906475 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties @@ -428,6 +428,7 @@ settings.agentBehavior.skills.sources.editPath.title=编辑技能路径 settings.agentBehavior.skills.sources.editUrl.title=编辑技能 URL session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. +revert.banner.openDiff.title=Rolled back changes revert.banner.count.one=已回滚 {0} 条消息 revert.banner.count.other=已回滚 {0} 条消息 revert.banner.redo=重做 diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties index 6eec893d1af..1f42649ba93 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties @@ -428,6 +428,7 @@ settings.agentBehavior.skills.sources.editPath.title=編輯技能路徑 settings.agentBehavior.skills.sources.editUrl.title=編輯技能 URL session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. +revert.banner.openDiff.title=Rolled back changes revert.banner.count.one=已回復 {0} 則訊息 revert.banner.count.other=已回復 {0} 則訊息 revert.banner.redo=重做 diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/diff/DiffBlocksTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/diff/DiffBlocksTest.kt new file mode 100644 index 00000000000..2d68628ea9f --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/diff/DiffBlocksTest.kt @@ -0,0 +1,53 @@ +package ai.kilocode.client.diff + +import ai.kilocode.rpc.dto.DiffFileDto +import com.intellij.diff.contents.DocumentContent +import com.intellij.diff.requests.SimpleDiffRequest +import com.intellij.diff.util.DiffUserDataKeysEx +import com.intellij.testFramework.fixtures.BasePlatformTestCase + +class DiffBlocksTest : BasePlatformTestCase() { + fun `test diffRequest uses full sides when available`() { + val request = diffRequest( + project, + DiffFileDto( + file = "src/Main.kt", + additions = 1, + deletions = 1, + patch = "@@ -1 +1 @@\n-old\n+new\n", + status = "modified", + before = "old\nkeep\n", + after = "new\nkeep\n", + ), + ) as SimpleDiffRequest + + assertEquals("old\nkeep\n", (request.contents[0] as DocumentContent).document.text) + assertEquals("new\nkeep\n", (request.contents[1] as DocumentContent).document.text) + // Full content already starts at line 1, so no gutter remap is installed. + assertNull((request.contents[0] as DocumentContent).getUserData(DiffUserDataKeysEx.LINE_NUMBER_CONVERTOR)) + assertNull((request.contents[1] as DocumentContent).getUserData(DiffUserDataKeysEx.LINE_NUMBER_CONVERTOR)) + } + + fun `test diffRequest remaps hunk fallback gutter to real file lines`() { + val request = diffRequest( + project, + DiffFileDto( + file = "src/Main.kt", + additions = 1, + deletions = 1, + patch = "--- a/src/Main.kt\n+++ b/src/Main.kt\n@@ -126,3 +126,3 @@\n one\n-two\n+TWO\n three\n", + status = "modified", + ), + ) as SimpleDiffRequest + + val left = (request.contents[0] as DocumentContent).getUserData(DiffUserDataKeysEx.LINE_NUMBER_CONVERTOR)!! + val right = (request.contents[1] as DocumentContent).getUserData(DiffUserDataKeysEx.LINE_NUMBER_CONVERTOR)!! + // Document line 0 (0-based) maps to file line 125 (0-based); the platform renders it as 126. + assertEquals(125, left.applyAsInt(0)) + assertEquals(127, left.applyAsInt(2)) + assertEquals(125, right.applyAsInt(0)) + assertEquals(127, right.applyAsInt(2)) + // Out-of-range document lines are hidden. + assertEquals(-1, left.applyAsInt(9)) + } +} diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/diff/DiffPatchReconstructTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/diff/DiffPatchReconstructTest.kt index 24ab5b67c5a..b55ef75ae72 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/diff/DiffPatchReconstructTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/diff/DiffPatchReconstructTest.kt @@ -133,7 +133,98 @@ class DiffPatchReconstructTest { } @Test - fun `multi hunk partial context patch is not renderable`() { + fun `multi hunk patch reconstructs concatenated changed regions`() { + // Turn/tool diffs are ordinary limited-context git output with several hunks. The reconstruction + // stitches each hunk body into contiguous before/after text so the diff editor colors the + // changes instead of dumping the raw patch as all-added lines. Inter-hunk gaps collapse. + val dto = DiffFileDto( + file = "src/A.kt", + additions = 2, + deletions = 2, + patch = """ + diff --git a/src/A.kt b/src/A.kt + --- a/src/A.kt + +++ b/src/A.kt + @@ -1,3 +1,3 @@ + one + -two + +TWO + three + @@ -20,3 +20,3 @@ + twenty + -x + +X + z + """.trimIndent(), + ) + + val sides = DiffPatchReconstruct.sides(dto) + + assertTrue(sides.renderable) + assertEquals("one\ntwo\nthree\ntwenty\nx\nz", sides.before) + assertEquals("one\nTWO\nthree\ntwenty\nX\nz", sides.after) + } + + @Test + fun `line maps carry real file positions across hunk gaps`() { + // Regression: the fallback content restarts at line 1, so the diff editor gutter showed 1..N + // instead of real file positions. The per-side maps (0-based; platform adds 1) must follow the + // @@ headers and jump across the elided gap between the two hunks. + val dto = DiffFileDto( + file = "src/A.kt", + additions = 2, + deletions = 2, + patch = """ + diff --git a/src/A.kt b/src/A.kt + --- a/src/A.kt + +++ b/src/A.kt + @@ -126,3 +126,3 @@ + one + -two + +TWO + three + @@ -220,3 +221,3 @@ + twenty + -x + +X + z + """.trimIndent(), + ) + + val sides = DiffPatchReconstruct.sides(dto) + + // before doc lines: one/two/three (126,127,128) then twenty/x/z (220,221,222) -> 0-based + assertEquals(listOf(125, 126, 127, 219, 220, 221), sides.leftLines) + // after doc lines: one/TWO/three (126,127,128) then twenty/X/z (221,222,223) -> 0-based + assertEquals(listOf(125, 126, 127, 220, 221, 222), sides.rightLines) + } + + @Test + fun `added file has no left line map and after side maps from one`() { + val dto = DiffFileDto( + file = "src/A.kt", + additions = 2, + deletions = 0, + patch = """ + diff --git a/src/A.kt b/src/A.kt + --- /dev/null + +++ b/src/A.kt + @@ -0,0 +1,2 @@ + +one + +two + """.trimIndent(), + ) + + val sides = DiffPatchReconstruct.sides(dto) + + assertEquals(emptyList(), sides.leftLines) + assertEquals(listOf(0, 1), sides.rightLines) + } + + @Test + fun `multi hunk patch with truncated context is not renderable`() { + // header claims 3 old / 3 new lines per hunk but the body carries only 2 of each: reconstructing + // would misalign the sides, so fall back to the raw-patch view. val dto = DiffFileDto( file = "src/A.kt", additions = 2, diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/diff/KiloDiffEditorContentTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/diff/KiloDiffEditorContentTest.kt index 7bf4d3c12bd..de63af8b033 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/diff/KiloDiffEditorContentTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/diff/KiloDiffEditorContentTest.kt @@ -291,6 +291,31 @@ class KiloDiffEditorContentTest : BasePlatformTestCase() { assertEquals("hello\nworld", contents[1]) } + fun `test diff request reconstructs multi hunk modified patch`() { + // Regression: multi-hunk modified diffs used to fall back to an empty original + raw patch on + // the right, rendering every line as added. They now reconstruct into a real side-by-side diff. + val patch = """ + diff --git a/src/App.kt b/src/App.kt + --- a/src/App.kt + +++ b/src/App.kt + @@ -1,3 +1,3 @@ + one + -two + +TWO + three + @@ -20,3 +20,3 @@ + twenty + -x + +X + z + """.trimIndent() + val request = diffRequest(project, file("src/App.kt", 2, 2, patch = patch)) as SimpleDiffRequest + val contents = request.contents.map(::content) + + assertEquals("one\ntwo\nthree\ntwenty\nx\nz", contents[0]) + assertEquals("one\nTWO\nthree\ntwenty\nX\nz", contents[1]) + } + fun `test tree displays absolute files relative to workspace`() { val parent = Disposer.newDisposable() try { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/RevertDiffLoadingTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/RevertDiffLoadingTest.kt new file mode 100644 index 00000000000..e2d831baa19 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/RevertDiffLoadingTest.kt @@ -0,0 +1,38 @@ +package ai.kilocode.client.session.controller + +import ai.kilocode.rpc.dto.DiffFileDto +import ai.kilocode.rpc.dto.MessageWithPartsDto +import ai.kilocode.rpc.dto.SessionRevertDto + +/** + * A session reverted in a previous run has no `session.diff` event to replay on open, and the + * pinned CLI may not attach a diff to the revert marker. [SessionController.seedRevertDiff] fetches + * the persisted session diff so the reverted-files banner has something to render. + */ +class RevertDiffLoadingTest : SessionControllerTestBase() { + + fun `test opening a reverted session seeds model diff from the diff rpc`() { + rpc.session = session("ses_test").copy(revert = SessionRevertDto(messageID = "msg1", snapshot = "snap")) + rpc.history.add(MessageWithPartsDto(msg("msg1", "ses_test", "user"), emptyList())) + rpc.history.add(MessageWithPartsDto(msg("msg2", "ses_test", "assistant"), emptyList())) + rpc.diffs["ses_test"] = mutableListOf(DiffFileDto("src/A.kt", 2, 1, "@@ patch")) + + val c = controller("ses_test") + flush() + + assertEquals("msg1", c.model.revert()?.messageID) + assertEquals(listOf("src/A.kt"), c.model.diff.map { it.file }) + } + + fun `test opening a session without a revert does not fetch the diff`() { + rpc.session = session("ses_test") + rpc.history.add(MessageWithPartsDto(msg("msg1", "ses_test", "user"), emptyList())) + rpc.diffs["ses_test"] = mutableListOf(DiffFileDto("src/A.kt", 2, 1, "@@ patch")) + + val c = controller("ses_test") + flush() + + assertNull(c.model.revert()) + assertTrue(c.model.diff.isEmpty()) + } +} diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/model/SessionModelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/model/SessionModelTest.kt index 0c7fce27165..9025368f4cc 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/model/SessionModelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/model/SessionModelTest.kt @@ -376,6 +376,7 @@ class SessionModelTest : BasePlatformTestCase() { ) val p = model.message("m1")!!.parts["p1"] as Tool + assertEquals("m1", p.messageID) assertEquals("git log", p.input["command"]) assertEquals("Show history", p.input["description"]) assertEquals("state", p.metadata["source"]) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionLayoutTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionLayoutTest.kt index 6e39e42c605..ced62e863ba 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionLayoutTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionLayoutTest.kt @@ -361,6 +361,34 @@ class SessionLayoutTest : BasePlatformTestCase() { assertEquals(sCount + 1, second.count) } + fun `test validate root child reconciles stale parent cache after self layout`() { + val p = panel(width = 300) + val child = rootProbe(root = true) + p.add(child) + p.doLayout() + child.markValid() + child.preferred = 80 + + child.doLayout() + p.doLayout() + + assertEquals(80, child.height) + } + + fun `test non validate root child keeps parent cache until it invalidates upward`() { + val p = panel(width = 300) + val child = rootProbe(root = false) + p.add(child) + p.doLayout() + child.markValid() + child.preferred = 80 + + child.doLayout() + p.doLayout() + + assertEquals(20, child.height) + } + // ---- helpers ------ /** A fixed-height JLabel. The width is reported as 0 until layout sets it. */ @@ -394,4 +422,24 @@ class SessionLayoutTest : BasePlatformTestCase() { return Dimension(0, height) } } + + private fun rootProbe(root: Boolean) = object : SessionLayoutPanel() { + var preferred = 20 + private var valid = false + + override fun isValid() = valid + + override fun invalidate() { + valid = false + super.invalidate() + } + + fun markValid() { + valid = true + } + + override fun isValidateRoot() = root + + override fun getPreferredSize(): Dimension = Dimension(0, preferred) + } } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanelTest.kt index ab7f800934b..95ff55bf948 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanelTest.kt @@ -15,6 +15,7 @@ import ai.kilocode.client.session.ui.style.SessionUiStyle import ai.kilocode.client.session.views.LoginRequiredView import ai.kilocode.client.session.views.PlanExitView import ai.kilocode.client.session.views.base.BaseQuestionView +import ai.kilocode.client.session.views.base.PartHeader import ai.kilocode.client.session.views.permission.PermissionView import ai.kilocode.client.session.views.question.QuestionResultView import ai.kilocode.client.session.views.question.QuestionView @@ -29,6 +30,7 @@ import ai.kilocode.client.session.views.tool.ToolView import ai.kilocode.client.session.views.todo.TodoWriteView import ai.kilocode.client.ui.DiffStatBadge import ai.kilocode.client.ui.HoverIcon +import ai.kilocode.client.ui.UiStyle import ai.kilocode.client.ui.layout.Stack import ai.kilocode.rpc.dto.DiffFileDto import ai.kilocode.rpc.dto.MessageDto @@ -37,6 +39,8 @@ import ai.kilocode.rpc.dto.MessageTimeDto import ai.kilocode.rpc.dto.MessageWithPartsDto import ai.kilocode.rpc.dto.PartDto import ai.kilocode.rpc.dto.SessionRevertDto +import ai.kilocode.rpc.dto.SessionDto +import ai.kilocode.rpc.dto.SessionTimeDto import ai.kilocode.rpc.dto.TodoDto import com.intellij.ide.ui.laf.darcula.ui.DarculaButtonUI import com.intellij.openapi.Disposable @@ -60,6 +64,7 @@ import javax.swing.JButton import javax.swing.JComponent import javax.swing.JPanel import javax.swing.RepaintManager +import javax.swing.ScrollPaneConstants import javax.swing.SwingUtilities import javax.swing.border.Border @@ -1080,8 +1085,9 @@ class SessionMessageListPanelTest : BasePlatformTestCase() { banner.update() assertNotNull(find(banner)) + assertNotNull(components(banner).filterIsInstance().singleOrNull()) - val buttons = components(banner).filterIsInstance() + val buttons = components(banner).filterIsInstance().filter { it.text.isNotEmpty() } assertEquals( listOf(KiloBundle.message("revert.banner.redo"), KiloBundle.message("revert.banner.redo.all")), buttons.map { it.text }, @@ -1098,7 +1104,7 @@ class SessionMessageListPanelTest : BasePlatformTestCase() { fun `test rollback banner reuses file rows across updates`() { val banner = RevertBanner(model, {}, {}, {}) model.upsertMessage(msg("u1", "user")) - model.setRevert(SessionRevertDto("u1")) + model.setRevert(SessionRevertDto("u1", snapshot = "snap1")) model.setDiff(listOf(DiffFileDto("src/A.kt", 1, 0), DiffFileDto("src/B.kt", 2, 1))) banner.update() val rows = components(banner).filterIsInstance().filter { stack -> @@ -1122,6 +1128,127 @@ class SessionMessageListPanelTest : BasePlatformTestCase() { assertEquals("-2", badges[1].removedLabelForTest().text) } + fun `test rollback banner caps file list with scroll pane`() { + val banner = RevertBanner(model, {}, {}, {}) + model.upsertMessage(msg("u1", "user")) + model.setRevert(SessionRevertDto("u1", snapshot = "snap1")) + model.setDiff((1..80).map { DiffFileDto("src/file-$it.kt", it, 0) }) + + banner.update() + + val scroll = components(banner).filterIsInstance().single() + assertTrue(scroll.verticalScrollBarPolicy == ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED) + assertTrue(scroll.horizontalScrollBarPolicy == ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED) + val rows = rowLabels(banner).mapNotNull { it.parent } + val rowHeight = rows.first().preferredSize.height + val cap = rowHeight * RevertBanner.MAX_FILE_ROWS + UiStyle.Gap.xs() * (RevertBanner.MAX_FILE_ROWS - 1) + assertEquals(cap, scroll.preferredSize.height) + } + + fun `test rollback banner shortens duplicate file names with parents`() { + val banner = RevertBanner(model, {}, {}, {}) + model.upsertMessage(msg("u1", "user")) + model.setRevert(SessionRevertDto("u1", snapshot = "snap1")) + model.setDiff(listOf( + DiffFileDto("apps/main/src/App.kt", 1, 0), + DiffFileDto("packages/ui/src/App.kt", 2, 1), + DiffFileDto("packages/ui/src/Button.kt", 3, 0), + )) + + banner.update() + + val labels = rowLabels(banner).map { it.text to it.toolTipText } + assertTrue(labels.contains("main/src/App.kt" to "apps/main/src/App.kt")) + assertTrue(labels.contains("ui/src/App.kt" to "packages/ui/src/App.kt")) + assertTrue(labels.contains("Button.kt" to "packages/ui/src/Button.kt")) + } + + fun `test rollback banner uses full path tooltip for entire file row`() { + val banner = RevertBanner(model, {}, {}, {}) + model.setSession(SessionDto( + id = "ses", + projectID = "proj", + directory = "/workspace/root", + title = "Session", + version = "1", + time = SessionTimeDto(0.0, 0.0), + )) + model.upsertMessage(msg("u1", "user")) + model.setRevert(SessionRevertDto("u1", snapshot = "snap1")) + model.setDiff(listOf( + DiffFileDto("project/dir1/shared-alpha.txt", 0, 4), + DiffFileDto("project/dir2/shared-alpha.txt", 0, 4), + )) + + banner.update() + + val label = rowLabels(banner).first { it.text == "dir1/shared-alpha.txt" } + val row = label.parent as JComponent + assertEquals("/workspace/root/project/dir1/shared-alpha.txt", row.toolTipText) + assertTrue(components(row).filterIsInstance().all { it.toolTipText == "/workspace/root/project/dir1/shared-alpha.txt" }) + } + + fun `test rollback banner opens rolled back diff`() { + val diff = DiffFileDto("src/A.kt", 1, 0, PATCH, "modified") + val opened = mutableListOf>() + val titles = mutableListOf() + val keys = mutableListOf() + val banner = RevertBanner(model, {}, {}, {}) + banner.setDiffOpener({ files, title, key -> + opened.add(files) + titles.add(title) + keys.add(key) + }, "ses_1") + model.upsertMessage(msg("u1", "user")) + model.setRevert(SessionRevertDto("u1", snapshot = "snap1", diffs = listOf(diff))) + + banner.update() + + val button = components(banner).filterIsInstance() + .first { it.toolTipText == KiloBundle.message("session.part.tool.openDiff") } + assertTrue(button.isVisible) + assertTrue(button.isEnabled) + button.doClick() + + assertEquals(listOf(diff), opened.single()) + assertEquals(KiloBundle.message("revert.banner.openDiff.title"), titles.single()) + assertEquals("revert:ses_1:u1", keys.single()) + } + + fun `test rollback banner hides open diff without a snapshot`() { + val banner = RevertBanner(model, {}, {}, {}) + model.upsertMessage(msg("u1", "user")) + model.setRevert(SessionRevertDto("u1", snapshot = null)) + model.setDiff(listOf(DiffFileDto("src/A.kt", 1, 0, PATCH))) + + banner.update() + + val button = components(banner).filterIsInstance() + .first { it.toolTipText == KiloBundle.message("session.part.tool.openDiff") } + assertFalse(button.isVisible) + assertFalse(button.isEnabled) + } + + fun `test rollback banner opens session diff when revert diff is absent`() { + val diff = DiffFileDto("src/A.kt", 1, 0, PATCH, "modified") + val opened = mutableListOf>() + val banner = RevertBanner(model, {}, {}, {}) + banner.setDiffOpener({ files, _, _ -> opened.add(files) }, "ses_1") + model.upsertMessage(msg("u1", "user")) + model.setRevert(SessionRevertDto("u1", snapshot = "snap1")) + model.setDiff(listOf(diff)) + + banner.update() + + val button = components(banner).filterIsInstance() + .first { it.toolTipText == KiloBundle.message("session.part.tool.openDiff") } + assertTrue(button.isVisible) + assertTrue(button.isEnabled) + button.doClick() + + assertEquals(listOf(diff), opened.single()) + } + fun `test rollback banner shows redo all only for multiple reverted messages`() { val banner = RevertBanner(model, {}, {}, {}) model.upsertMessage(msg("u1", "user")) @@ -1168,7 +1295,7 @@ class SessionMessageListPanelTest : BasePlatformTestCase() { banner.setReverting(SessionState.Reverting("Rolling back...", SessionState.Reverting.Kind.ROLLBACK, "u1")) - val buttons = components(banner).filterIsInstance() + val buttons = components(banner).filterIsInstance().filter { it.text.isNotEmpty() } assertTrue(buttons.filter { it.text == KiloBundle.message("revert.banner.redo") }.all { !it.isEnabled }) assertTrue(buttons.filter { it.text == KiloBundle.message("revert.banner.redo.all") }.all { !it.isEnabled }) val progress = components(banner).filterIsInstance().single() @@ -1521,6 +1648,11 @@ class SessionMessageListPanelTest : BasePlatformTestCase() { return out } + private fun rowLabels(root: Component): List = components(root) + .filterIsInstance() + .filter { stack -> stack.components.any { it is DiffStatBadge } } + .mapNotNull { stack -> components(stack).filterIsInstance().firstOrNull() } + private fun taskText(view: TaskToolView): List { val scroll = components(view).filterIsInstance().single() val stack = components(scroll.viewport.view).filterIsInstance().single() diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/prompt/KiloPromptCompletionProviderTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/prompt/KiloPromptCompletionProviderTest.kt index 76077daeabb..b61398ca28b 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/prompt/KiloPromptCompletionProviderTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/prompt/KiloPromptCompletionProviderTest.kt @@ -174,6 +174,69 @@ class KiloPromptCompletionProviderTest : BasePlatformTestCase() { assertFalse(myFixture.lookupElementStrings.orEmpty().contains(noMatches())) } + fun `test slash completion ranks prefix matches before contains matches`() { + rpc.state.value = KiloWorkspaceStateDto( + KiloWorkspaceStatusDto.READY, + commands = listOf( + CommandDto("three-jetbrains"), + CommandDto("jetbrains-one"), + CommandDto("jetbrains-two"), + ), + ) + waitFor { provider.serverCommand("/jetbrains-one") != null } + + complete("/jet") + + assertOrder("jetbrains-one", "jetbrains-two", "three-jetbrains") + } + + fun `test slash completion matches prefix across separators`() { + rpc.state.value = KiloWorkspaceStateDto( + KiloWorkspaceStatusDto.READY, + commands = listOf( + CommandDto("three-jetbrains"), + CommandDto("jetbrains-one"), + CommandDto("jetbrains-two"), + CommandDto("jetbrains_two"), + CommandDto("jetbrains.three"), + CommandDto("jetbrains:four"), + CommandDto("jetbrains+five"), + ), + ) + waitFor { provider.serverCommand("/jetbrains-one") != null } + + complete("/j-") + assertEquals( + listOf("jetbrains-one", "jetbrains-two"), + matches("jetbrains-one", "jetbrains-two", "jetbrains_two", "three-jetbrains"), + ) + + complete("/j_") + assertEquals(listOf("jetbrains_two"), matches("jetbrains-one", "jetbrains_two", "three-jetbrains")) + + complete("/j.") + assertEquals(listOf("jetbrains.three"), matches("jetbrains.three", "jetbrains:four", "jetbrains+five")) + + complete("/j:") + assertEquals(listOf("jetbrains:four"), matches("jetbrains.three", "jetbrains:four", "jetbrains+five")) + + complete("/j+") + assertEquals(listOf("jetbrains+five"), matches("jetbrains.three", "jetbrains:four", "jetbrains+five")) + } + + fun `test slash completion matches camel humps with capitals`() { + rpc.state.value = KiloWorkspaceStateDto( + KiloWorkspaceStatusDto.READY, + commands = listOf(CommandDto("jetBrainsSkill"), CommandDto("jetbrains-skill")), + ) + waitFor { provider.serverCommand("/jetBrainsSkill") != null } + + complete("/JBS") + + assertContainsElements(myFixture.lookupElementStrings.orEmpty(), "jetBrainsSkill") + assertFalse(myFixture.lookupElementStrings.orEmpty().contains(noMatches())) + } + fun `test blank mention completion includes special and root entries`() { rpc.searchResult = FileSearchResultDto( files = listOf(file("src", directory = true), file("README.md")), @@ -380,6 +443,19 @@ class KiloPromptCompletionProviderTest : BasePlatformTestCase() { private fun item(value: String) = myFixture.lookupElements.orEmpty().first { it.lookupString == value } + private fun assertOrder(vararg values: String) { + val items = myFixture.lookupElementStrings.orEmpty() + assertContainsElements(items, *values) + values.toList().zipWithNext().forEach { (left, right) -> + assertTrue("Expected $left before $right in $items", items.indexOf(left) < items.indexOf(right)) + } + } + + private fun matches(vararg values: String): List { + val items = myFixture.lookupElementStrings.orEmpty() + return values.filter { it in items } + } + private fun file(path: String, directory: Boolean = false) = WorkspaceFileDto( path = path, name = path.substringAfterLast('/'), diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/base/BaseQuestionViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/base/BaseQuestionViewTest.kt index ffc0a1b97cf..522e7b41b6a 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/base/BaseQuestionViewTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/base/BaseQuestionViewTest.kt @@ -22,12 +22,11 @@ class BaseQuestionViewTest : BasePlatformTestCase() { // ------ initial state ------ - fun `test header and description text areas are in the component tree by default`() { + fun `test empty card does not render a header row by default`() { edt { val panel = BaseQuestionView() assertTrue("Root layout should be BorderLayout", panel.layout is BorderLayout) - val areas = findAll(panel) - assertTrue("Should have at least 2 text areas (header + description)", areas.size >= 2) + assertNull("Header row should be omitted until header content exists", headerRow(panel)) } } @@ -88,6 +87,7 @@ class BaseQuestionViewTest : BasePlatformTestCase() { fun `test setTopPanel adds component before header`() { edt { val panel = BaseQuestionView() + panel.setHeader("Title") val top = JLabel("top") panel.setTopPanel(top) @@ -281,9 +281,10 @@ class BaseQuestionViewTest : BasePlatformTestCase() { } } - fun `test header row has no west icon gap by default`() { + fun `test header row has no west icon gap without icon`() { edt { val panel = BaseQuestionView() + panel.setHeader("Title") val header = headerRow(panel)!! val west = (header.layout as BorderLayout).getLayoutComponent(BorderLayout.WEST) assertNull("header should not reserve icon space when icon is absent", west) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeAgentBehaviorRpcApi.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeAgentBehaviorRpcApi.kt index 87f13a939c4..c74b6132c04 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeAgentBehaviorRpcApi.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeAgentBehaviorRpcApi.kt @@ -3,6 +3,7 @@ package ai.kilocode.client.testing import ai.kilocode.rpc.KiloAgentBehaviorRpcApi import ai.kilocode.rpc.dto.AgentCreateDto import ai.kilocode.rpc.dto.AgentDetailDto +import ai.kilocode.rpc.dto.CommandFileDto import ai.kilocode.rpc.dto.CommandDto import ai.kilocode.rpc.dto.McpConfigDto import ai.kilocode.rpc.dto.McpServerConfigDto @@ -12,6 +13,7 @@ import ai.kilocode.rpc.dto.SkillDto class FakeAgentBehaviorRpcApi : KiloAgentBehaviorRpcApi { var agents = emptyList() var skills = emptyList() + var commandFiles = emptyList() var mcps = emptyList() var mcpConfigs = emptyMap() val agentCalls = mutableListOf() @@ -19,6 +21,10 @@ class FakeAgentBehaviorRpcApi : KiloAgentBehaviorRpcApi { val skillRemovals = mutableListOf>() val skillReloads = mutableListOf() val skillSaves = mutableListOf>() + val commandCalls = mutableListOf() + val commandRemovals = mutableListOf>() + val commandReloads = mutableListOf() + val commandSaves = mutableListOf>() val mcpCalls = mutableListOf() val mcpConfigCalls = mutableListOf() val mcpSaves = mutableListOf>() @@ -33,6 +39,7 @@ class FakeAgentBehaviorRpcApi : KiloAgentBehaviorRpcApi { var afterMcpConnect: (suspend (String, String) -> Unit)? = null var createError: Exception? = null var skillsError: Exception? = null + var commandFilesError: Exception? = null var removeError: Exception? = null var removeSkillError: Exception? = null var saveSkillError: Exception? = null @@ -42,6 +49,9 @@ class FakeAgentBehaviorRpcApi : KiloAgentBehaviorRpcApi { var removeSkillResult = true var reloadSkillResult = true var saveSkillResult = true + var removeCommandResult = true + var reloadCommandResult = true + var saveCommandResult = true var mcpConnectResult = true var mcpDisconnectResult = true var mcpAuthenticateResult = true @@ -123,6 +133,35 @@ class FakeAgentBehaviorRpcApi : KiloAgentBehaviorRpcApi { return emptyList() } + override suspend fun commandFiles(directory: String): List { + assertNotEdt("agentBehavior.commandFiles") + commandFilesError?.let { throw it } + commandCalls.add(directory) + return commandFiles + } + + override suspend fun removeCommand(directory: String, location: String): Boolean { + assertNotEdt("agentBehavior.removeCommand") + commandRemovals.add(directory to location) + if (removeCommandResult) commandFiles = commandFiles.filterNot { it.location == location } + return removeCommandResult + } + + override suspend fun reloadCommands(directory: String): Boolean { + assertNotEdt("agentBehavior.reloadCommands") + commandReloads.add(directory) + return reloadCommandResult + } + + override suspend fun saveCommands(directory: String, edits: Map): Boolean { + assertNotEdt("agentBehavior.saveCommands") + for ((location, content) in edits) commandSaves.add(Triple(directory, location, content)) + if (saveCommandResult) commandFiles = commandFiles.map { command -> + edits[command.location]?.let { command.copy(content = it) } ?: command + } + return saveCommandResult + } + override suspend fun mcpStatus(directory: String): List { assertNotEdt("agentBehavior.mcpStatus") mcpStatusError?.let { throw it } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeSessionRpcApi.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeSessionRpcApi.kt index 51f7e504f22..d5efdd9bc00 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeSessionRpcApi.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeSessionRpcApi.kt @@ -50,6 +50,7 @@ class FakeSessionRpcApi : KiloSessionRpcApi { val history = mutableListOf() val histories = mutableMapOf>() val diffs = mutableMapOf>() + val diffSides = mutableMapOf() var historyGate: CompletableDeferred? = null var historyCalls = 0 private set @@ -270,6 +271,11 @@ class FakeSessionRpcApi : KiloSessionRpcApi { return diffs[id]?.toList().orEmpty() } + override suspend fun diffSides(sessionId: String?, directory: String, file: DiffFileDto, messageId: String?): DiffFileDto? { + assertNotEdt("diffSides") + return diffSides[file.file] + } + override suspend fun attachmentPart(id: String, directory: String, messageId: String, partId: String, attachmentKey: String?): PartDto? { assertNotEdt("attachmentPart") attachmentParts.add(AttachmentCall(id, directory, messageId, partId, attachmentKey)) diff --git a/packages/kilo-jetbrains/gradle.properties b/packages/kilo-jetbrains/gradle.properties index 22bcfa17240..21e1e5e45bb 100644 --- a/packages/kilo-jetbrains/gradle.properties +++ b/packages/kilo-jetbrains/gradle.properties @@ -1,5 +1,5 @@ kotlin.stdlib.default.dependency=false -kilo.jetbrains.version=7.0.12 +kilo.jetbrains.version=7.0.14 # When true (default) the JetBrains plugin uses the pinned CLI release from package.json. # Set to false ONLY for local dev: generate the client from local source + bundle the local binary. # false is NOT releasable -- production builds fail unless this is true. diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloAgentBehaviorRpcApi.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloAgentBehaviorRpcApi.kt index b13c03d526a..ac82b9de313 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloAgentBehaviorRpcApi.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloAgentBehaviorRpcApi.kt @@ -2,6 +2,7 @@ package ai.kilocode.rpc import ai.kilocode.rpc.dto.AgentDetailDto import ai.kilocode.rpc.dto.AgentCreateDto +import ai.kilocode.rpc.dto.CommandFileDto import ai.kilocode.rpc.dto.CommandDto import ai.kilocode.rpc.dto.McpConfigDto import ai.kilocode.rpc.dto.McpServerConfigDto @@ -38,6 +39,14 @@ interface KiloAgentBehaviorRpcApi : RemoteApi { suspend fun commands(directory: String): List + suspend fun commandFiles(directory: String): List + + suspend fun removeCommand(directory: String, location: String): Boolean + + suspend fun reloadCommands(directory: String): Boolean + + suspend fun saveCommands(directory: String, edits: Map): Boolean + suspend fun mcpStatus(directory: String): List suspend fun mcpConfig(directory: String): Map diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloSessionRpcApi.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloSessionRpcApi.kt index dcd940cab02..ae3851c2b2e 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloSessionRpcApi.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloSessionRpcApi.kt @@ -107,6 +107,15 @@ interface KiloSessionRpcApi : RemoteApi { /** Load cumulative file changes for a session. */ suspend fun diff(id: String, directory: String): List + /** + * Full before/after content for one changed file so the diff editor can show a whole-file diff. + * Prefers authoritative snapshot content from a CLI that supports it (correct even for historical + * turns); falls back to rebuilding locally from the working tree + hunk patch against any pinned + * CLI. Returns null when neither is available (fall back to the hunk view). Added/deleted files + * return null because the frontend reconstructs those directly. + */ + suspend fun diffSides(sessionId: String?, directory: String, file: DiffFileDto, messageId: String?): DiffFileDto? + /** Load one attachment part from a session without returning full history to the frontend. */ suspend fun attachmentPart(id: String, directory: String, messageId: String, partId: String, attachmentKey: String?): PartDto? diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt index 4d31c3a5517..ecf0da6a410 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt @@ -410,6 +410,8 @@ data class DiffFileDto( val deletions: Int, val patch: String? = null, val status: String? = null, + val before: String? = null, + val after: String? = null, ) // --- Config Update --- diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/CommandDto.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/CommandDto.kt index 4d0e6540aac..f14eab4aaa5 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/CommandDto.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/CommandDto.kt @@ -6,7 +6,11 @@ import kotlinx.serialization.Serializable data class CommandDto( val name: String, val description: String? = null, + val agent: String? = null, + val model: String? = null, + val variant: String? = null, val source: String? = null, val hints: List = emptyList(), val template: String? = null, + val subtask: Boolean? = null, ) diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/CommandFileDto.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/CommandFileDto.kt new file mode 100644 index 00000000000..9c4267dcf03 --- /dev/null +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/CommandFileDto.kt @@ -0,0 +1,19 @@ +package ai.kilocode.rpc.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class CommandFileDto( + val name: String, + val description: String? = null, + val agent: String? = null, + val model: String? = null, + val variant: String? = null, + val source: String? = null, + val builtin: Boolean = false, + val location: String, + val editable: Boolean = false, + val content: String? = null, + val subtask: Boolean? = null, + val hints: List = emptyList(), +) diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/SessionDto.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/SessionDto.kt index 41386c87b93..c8058d5b408 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/SessionDto.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/SessionDto.kt @@ -21,6 +21,7 @@ data class SessionRevertDto( val partID: String? = null, val snapshot: String? = null, val diff: String? = null, + val diffs: List = emptyList(), ) @Serializable diff --git a/packages/kilo-telemetry/src/__tests__/telemetry.test.ts b/packages/kilo-telemetry/src/__tests__/telemetry.test.ts index c86a249f51f..9246facb48c 100644 --- a/packages/kilo-telemetry/src/__tests__/telemetry.test.ts +++ b/packages/kilo-telemetry/src/__tests__/telemetry.test.ts @@ -126,4 +126,15 @@ describe("Telemetry", () => { expect(typeof Telemetry.trackSuggestionShown).toBe("function") expect(typeof Telemetry.trackSuggestionAccepted).toBe("function") }) + + test("trackToolUsed sends Tool Used event with tool name and sessionId", () => { + const capture = spyOn(Client, "capture").mockImplementation(() => {}) + + try { + Telemetry.trackToolUsed("chart", "session-123") + expect(capture).toHaveBeenCalledWith(TelemetryEvent.TOOL_USED, expect.objectContaining({ tool: "chart", sessionId: "session-123" })) + } finally { + capture.mockRestore() + } + }) }) diff --git a/packages/kilo-ui/src/components/basic-tool.css b/packages/kilo-ui/src/components/basic-tool.css index a386323f999..9d7ff5acc76 100644 --- a/packages/kilo-ui/src/components/basic-tool.css +++ b/packages/kilo-ui/src/components/basic-tool.css @@ -507,21 +507,25 @@ html[data-theme="kilo-vscode"] [data-component="tool-part-wrapper"][data-part-ty } } -/* "why was this allowed" line inside a tool's expanded body */ +/* "why was this allowed" line inside a tool's expanded body. Styled like + [data-component="tool-hint"] (muted, italic) so it reads as ambient + context rather than a call to action, and recedes the way reasoning text does. */ [data-slot="tool-approval-line"] { display: flex; flex-wrap: wrap; - align-items: baseline; + align-items: center; gap: 4px; padding: 4px 0 6px; font-family: var(--font-family-sans); font-size: var(--font-size-small); + font-style: italic; line-height: var(--line-height-normal); color: var(--text-weak); + opacity: 0.9; - [data-slot="tool-approval-decision"] { - font-weight: var(--font-weight-medium); - color: var(--text-strong); + svg { + flex-shrink: 0; + color: var(--text-weak); } [data-slot="tool-approval-rule"] { diff --git a/packages/kilo-ui/src/components/basic-tool.tsx b/packages/kilo-ui/src/components/basic-tool.tsx index 7dbaeee9395..ac225cd73b0 100644 --- a/packages/kilo-ui/src/components/basic-tool.tsx +++ b/packages/kilo-ui/src/components/basic-tool.tsx @@ -44,18 +44,24 @@ export function BasicTool(props: BasicToolProps) { writeToolOpen(key(), open) props.onOpenChange?.(open) } + // Renders after the body/tool list, not before — it's context about what + // happened, not part of the header. const details = () => (
- {(value) => } {props.children} + {(value) => }
) - if (!("children" in props) && !inBody()) { - return - } + // A , not a plain `if`: inBody() tracks the visibility toggle, which can + // flip after mount (Settings), so the branch must stay reactive. return ( - - {details()} - + } + > + + {details()} + + ) } diff --git a/packages/kilo-ui/src/components/message-part.tsx b/packages/kilo-ui/src/components/message-part.tsx index 4281174d125..bce8f1c8917 100644 --- a/packages/kilo-ui/src/components/message-part.tsx +++ b/packages/kilo-ui/src/components/message-part.tsx @@ -49,7 +49,7 @@ import { Tooltip } from "./tooltip" import { IconButton } from "./icon-button" import { TextShimmer } from "@opencode-ai/ui/text-shimmer" import { ToolApprovalProvider, resolveToolApproval } from "./tool-approval" -export { ToolApprovalProvider, resolveToolApproval } from "./tool-approval" +export { ToolApprovalProvider, resolveToolApproval, ToolApprovalVisibilityProvider } from "./tool-approval" import { GrowBox } from "./grow-box" import { COLLAPSIBLE_SPRING } from "./motion" import { busy, createThrottledValue, useToolFade, useContextToolPending } from "./tool-utils" diff --git a/packages/kilo-ui/src/components/tool-approval.tsx b/packages/kilo-ui/src/components/tool-approval.tsx index d555e105df3..9fc6e102693 100644 --- a/packages/kilo-ui/src/components/tool-approval.tsx +++ b/packages/kilo-ui/src/components/tool-approval.tsx @@ -1,4 +1,5 @@ import { createContext, useContext, Show, type Accessor, type ParentProps } from "solid-js" +import { Icon } from "./icon" /** * Explains why a tool call was auto-approved, inside the expanded tool row. @@ -30,8 +31,23 @@ export function ToolApprovalProvider(props: ParentProps<{ value: Accessor{props.children} } +/** + * Whether the approval line should render at all. Hosts that expose a "hide + * auto-approval reason" display setting wrap their tree in + * `ToolApprovalVisibilityProvider`; without one, the line stays visible. + */ +const VisibilityContext = createContext>(() => true) + +export function ToolApprovalVisibilityProvider(props: ParentProps<{ value: Accessor }>) { + return {props.children} +} + +/** Read the approval for the tool row below, gated by the visibility toggle + * here (not per call site) so a new `ToolApprovalProvider` usage can't forget it. */ export function useToolApproval() { - return useContext(Context) + const value = useContext(Context) + const visible = useContext(VisibilityContext) + return () => (visible() ? value() : undefined) } /** Read the raw approval payload off a tool part's metadata, if present. */ @@ -79,6 +95,7 @@ export function ToolApprovalLine(props: { display: ToolApprovalDisplay }) { const manual = () => props.display.approval.source === "manual" return (
+ {props.display.decision} {(text) => {text()}} diff --git a/packages/kilo-ui/src/hooks/create-auto-scroll.test.tsx b/packages/kilo-ui/src/hooks/create-auto-scroll.test.tsx index 08df06b7fce..6f1ec9e3542 100644 --- a/packages/kilo-ui/src/hooks/create-auto-scroll.test.tsx +++ b/packages/kilo-ui/src/hooks/create-auto-scroll.test.tsx @@ -216,4 +216,50 @@ describe("createAutoScroll non-scrollable layouts", () => { expect(ctx.el.scrollTop).toBe(300) ctx.dispose() }) + + test("does not snap to bottom on content resize after user scrolls up while idle", () => { + const ctx = setup({ working: false }) + ctx.el.scrollHeight = 1000 + ctx.el.clientHeight = 200 + ctx.el.scrollTop = 800 // at bottom + + // User wheels up + const event = new FakeWheelEvent(-50, ctx.el) + ctx.el.fire("wheel", event as unknown as Event) + ctx.el.scrollTop = 750 + ctx.scroll.handleScroll() + + expect(ctx.scroll.userScrolled()).toBe(true) + + // Virtual list re-measures / resizes content + ctx.el.scrollHeight = 1100 + ctx.resize() + + // Must NOT snap to bottom (1100), must remain at user scroll position (750) + expect(ctx.scroll.userScrolled()).toBe(true) + expect(ctx.el.scrollTop).toBe(750) + ctx.dispose() + }) + + test("does not snap to bottom when dragging scrollbar up while idle", () => { + const ctx = setup({ working: false }) + ctx.el.scrollHeight = 1000 + ctx.el.clientHeight = 200 + ctx.el.scrollTop = 800 // at bottom + + // User presses pointerdown on scrollbar and drags up + ctx.el.fire("pointerdown", new Event("pointerdown")) + ctx.el.scrollTop = 600 + ctx.scroll.handleScroll() + + expect(ctx.scroll.userScrolled()).toBe(true) + + // Content resize during drag + ctx.el.scrollHeight = 1050 + ctx.resize() + + expect(ctx.scroll.userScrolled()).toBe(true) + expect(ctx.el.scrollTop).toBe(600) + ctx.dispose() + }) }) diff --git a/packages/kilo-ui/src/hooks/create-auto-scroll.tsx b/packages/kilo-ui/src/hooks/create-auto-scroll.tsx index a84b246f572..09ab785423c 100644 --- a/packages/kilo-ui/src/hooks/create-auto-scroll.tsx +++ b/packages/kilo-ui/src/hooks/create-auto-scroll.tsx @@ -4,7 +4,6 @@ import { createResizeObserver } from "@solid-primitives/resize-observer" import { canScroll, distanceFromBottom } from "./auto-scroll" import { createUserActivity } from "./scroll-user-activity" -const DEBOUNCE_MS = 100 // Grace window after a real pointer/key/touch interaction during which a // ResizeObserver or non-user scroll event must not snap the view back to the // bottom. Upward wheel intent pauses immediately in its capture handler. @@ -14,6 +13,7 @@ export interface AutoScrollOptions { working: () => boolean onUserInteracted?: () => void bottomThreshold?: number + overflowAnchor?: "none" | "auto" | "dynamic" } export function createAutoScroll(options: AutoScrollOptions) { @@ -24,7 +24,6 @@ export function createAutoScroll(options: AutoScrollOptions) { let scroll: HTMLElement | undefined let settling = false let settleTimer: ReturnType | undefined - let stopTimer: ReturnType | undefined let cleanup: (() => void) | undefined const [store, setStore] = createStore({ @@ -100,7 +99,7 @@ export function createAutoScroll(options: AutoScrollOptions) { const handleScroll = () => { if (!scroll) return - const input = userActivity.consumeScroll() + userActivity.consumeScroll() const distance = distanceFromBottom(scroll) if (!canScroll(scroll)) return @@ -110,52 +109,25 @@ export function createAutoScroll(options: AutoScrollOptions) { return } - if (!store.userScrolled && !input) { - // Only explicit user input can pause following. Treat unclassified - // scroll events from virtualization or layout changes as programmatic. - if (userActivity.isRecent()) { - stop() - } else { - bottom() - } - return - } - - // Debounce to avoid layout-induced scroll shifts (e.g. images loading, - // virtual-list reflows) from incorrectly breaking auto-follow. - if (stopTimer) clearTimeout(stopTimer) - stopTimer = setTimeout(() => { - stopTimer = undefined - if (!scroll) return - if (distanceFromBottom(scroll) < threshold()) return - stop() - }, DEBOUNCE_MS) + stop() } const onContentResize = () => { - if (scroll && !canScroll(scroll)) return - if (!active()) { - if (!store.userScrolled && scroll && distanceFromBottom(scroll) > threshold()) { - bottom() - return - } - return - } - if (store.userScrolled) { - return - } - // Virtualized lists (virtua) re-measure items during user scroll, firing - // resize events that race ahead of handleScroll's DEBOUNCE_MS window. - // If the user just interacted with the scroller and is no longer near - // the bottom, treat the resize as a layout reflow on top of their - // scroll — pause auto-follow instead of snapping back to the bottom. - if (scroll && userActivity.isRecent() && distanceFromBottom(scroll) > threshold()) { + if (!scroll || !canScroll(scroll)) return + if (store.userScrolled) return + + if (userActivity.isRecent() && distanceFromBottom(scroll) > threshold()) { stop() return } - // ResizeObserver fires after layout, before paint. - // Keep the bottom locked in the same frame to avoid visible - // "jump up then catch up" artifacts while streaming content. + + if (!active()) { + if (!userActivity.isRecent() && distanceFromBottom(scroll) > threshold()) { + bottom() + } + return + } + follow() } @@ -173,6 +145,15 @@ export function createAutoScroll(options: AutoScrollOptions) { createResizeObserver(() => store.contentRef, onContentResize) createResizeObserver(() => store.scrollRef, onViewportResize) + createEffect( + on( + () => store.userScrolled, + () => { + if (scroll) updateOverflowAnchor(scroll) + }, + ), + ) + createEffect( on(options.working, (working: boolean) => { settling = false @@ -195,6 +176,19 @@ export function createAutoScroll(options: AutoScrollOptions) { // Lifecycle // --------------------------------------------------------------------------- + const updateOverflowAnchor = (el: HTMLElement) => { + const mode = options.overflowAnchor ?? "none" + if (mode === "none") { + el.style.overflowAnchor = "none" + return + } + if (mode === "auto") { + el.style.overflowAnchor = "auto" + return + } + el.style.overflowAnchor = store.userScrolled ? "auto" : "none" + } + const setScroll = (el: HTMLElement | undefined) => { if (cleanup) { cleanup() @@ -206,13 +200,12 @@ export function createAutoScroll(options: AutoScrollOptions) { if (!el) return - el.style.overflowAnchor = "auto" + updateOverflowAnchor(el) cleanup = userActivity.listen(el) } onCleanup(() => { if (settleTimer) clearTimeout(settleTimer) - if (stopTimer) clearTimeout(stopTimer) if (cleanup) cleanup() }) diff --git a/packages/kilo-ui/src/hooks/scroll-user-activity.ts b/packages/kilo-ui/src/hooks/scroll-user-activity.ts index a0b640c32d0..a8a33fc8a66 100644 --- a/packages/kilo-ui/src/hooks/scroll-user-activity.ts +++ b/packages/kilo-ui/src/hooks/scroll-user-activity.ts @@ -18,13 +18,16 @@ export const createUserActivity = (options: UserActivityOptions) => { // do not get mistaken for the user leaving auto-follow mode. const mark = (event: Event) => { if (!isPotentialScrollInput(event)) return + if (scroll && scroll.scrollHeight - scroll.clientHeight <= 1) return marked = true time = performance.now() } const handleWheel = (event: WheelEvent) => { - if (event.deltaY >= 0 || !scroll || scroll.scrollTop <= 0) return - time = performance.now() + if (!isPotentialScrollInput(event)) return + if (!scroll || scroll.scrollHeight - scroll.clientHeight <= 1) return + mark(event) + if (event.deltaY >= 0 || scroll.scrollTop <= 0) return options.onWheelUp() } diff --git a/packages/kilo-ui/src/lucide.ts b/packages/kilo-ui/src/lucide.ts index e715ec841d7..ec7ab9c17f5 100644 --- a/packages/kilo-ui/src/lucide.ts +++ b/packages/kilo-ui/src/lucide.ts @@ -3,4 +3,4 @@ * Only add icons here that are actually used — esbuild/Vite will * tree-shake unused exports but explicit re-exports keep the API small. */ -export { WandSparkles } from "lucide-solid" +export { default as WandSparkles } from "lucide-solid/icons/wand-sparkles" diff --git a/packages/kilo-vscode/AGENTS.md b/packages/kilo-vscode/AGENTS.md index 15fac1d6be2..71986c85bee 100644 --- a/packages/kilo-vscode/AGENTS.md +++ b/packages/kilo-vscode/AGENTS.md @@ -168,6 +168,12 @@ Agent Manager local worktree sessions use the current shared `kilo serve` proces Extension-side code lives in `src/agent-manager/`, webview code in `webview-ui/agent-manager/`. The webview reuses the sidebar's provider chain and `ChatView` component, adding a `WorktreeModeProvider` and a split layout. +### Multi-project migration + +Multi-project Agent Manager is an incremental migration behind the application-scoped `kilo-code.new.experimental.multiProject` flag (default `false`); flag-off behavior must remain unchanged. The project registry/contexts, per-project state and session routing, project sidebar, sections and drag-and-drop, progress/persistence, and project-targeted worktree creation are implemented. + +It is not yet a complete convergence: audit every operation for explicit project/worktree/session routing, finish immutable project-bound Settings and machine-local indexing consent, harden canonical Git identity and multi-window route ownership, and replace the duplicate `SidebarBody`/`ProjectSidebarBody` implementations with one shared body. Full two-project E2E and legacy-parity coverage is still incomplete. + ## Webview UI (kilo-ui) New webview features must use **`@kilocode/kilo-ui`** components instead of raw HTML elements with inline styles. This is a Solid.js component library built on `@kobalte/core`. @@ -221,6 +227,8 @@ import { spawn, exec } from "../util/process" The `spawn` wrapper covers long-lived processes (e.g. `kilo serve`). The `exec` wrapper covers short commands (e.g. `git`, `tar`). If you need the raw callback form of `execFile` for some reason, pass `windowsHide: true` explicitly in the options object. +Agent Manager uses read-only `gh` commands for PR status and PR import. Call `execGhRead` from `src/agent-manager/gh.ts` for those commands; on Windows it supplies `TZ=UTC` when no timezone is configured, preventing older `gh` releases from launching `tzutil.exe` in a visible console. + ## Style Follow monorepo root AGENTS.md style guide: diff --git a/packages/kilo-vscode/esbuild.js b/packages/kilo-vscode/esbuild.js index eed8e2475b2..2fa1876afdb 100644 --- a/packages/kilo-vscode/esbuild.js +++ b/packages/kilo-vscode/esbuild.js @@ -1,10 +1,96 @@ const esbuild = require("esbuild") const path = require("path") -const { solidPlugin } = require("esbuild-plugin-solid") +const fs = require("fs") +const crypto = require("crypto") +const core = require("@babel/core") +const solid = require("babel-preset-solid") +const ts = require("@babel/preset-typescript") const production = process.argv.includes("--production") const watch = process.argv.includes("--watch") +/** + * Cache transformed Solid JSX files in memory and on disk to avoid + * re-parsing and re-transforming unchanged files across builds and webviews. + */ +const solidCacheDir = path.join(__dirname, "node_modules", ".cache", "esbuild-solid") +const solidMemCache = new Map() +const buildScriptHash = crypto + .createHash("sha256") + .update(fs.readFileSync(__filename, "utf8")) + .update(require("babel-preset-solid/package.json").version || "") + .update(require("@babel/preset-typescript/package.json").version || "") + .digest("hex") + .slice(0, 8) + +if (!fs.existsSync(solidCacheDir)) { + try { + fs.mkdirSync(solidCacheDir, { recursive: true }) + } catch (err) { + console.warn("[esbuild] could not create solid cache directory", err) + } +} + +const cachedSolidPlugin = { + name: "esbuild:solid-cached", + setup(build) { + build.onLoad({ filter: /\.(t|j)sx$/ }, async (args) => { + let mtime = 0 + let size = 0 + try { + const st = fs.statSync(args.path) + mtime = st.mtimeMs + size = st.size + } catch (err) { + console.warn("[esbuild] could not stat source file for cache key", args.path, err) + } + + const cacheKey = `${args.path}:${mtime}:${size}:${buildScriptHash}` + const memHit = solidMemCache.get(cacheKey) + if (memHit) return { contents: memHit, loader: "js" } + + const diskKey = crypto.createHash("sha256").update(cacheKey).digest("hex") + ".js" + const diskPath = path.join(solidCacheDir, diskKey) + + if (fs.existsSync(diskPath)) { + try { + const diskCode = fs.readFileSync(diskPath, "utf8") + solidMemCache.set(cacheKey, diskCode) + return { contents: diskCode, loader: "js" } + } catch (err) { + console.warn("[esbuild] cache read failed, rebuilding", diskPath, err) + } + } + + const source = fs.readFileSync(args.path, "utf8") + const { name, ext } = path.parse(args.path) + const filename = name + ext + const result = await core.transformAsync(source, { + presets: [ + [solid, {}], + [ts, {}], + ], + filename, + sourceMaps: "inline", + }) + + if (result?.code === void 0 || result.code === null) { + throw new Error("No result was provided from Babel") + } + + if (solidMemCache.size > 2000) solidMemCache.clear() + solidMemCache.set(cacheKey, result.code) + try { + fs.writeFileSync(diskPath, result.code) + } catch (err) { + console.warn("[esbuild] cache write failed", diskPath, err) + } + + return { contents: result.code, loader: "js" } + }) + }, +} + /** * Force all solid-js imports (from kilo-ui and the webview) to resolve to * the **same** copy so SolidJS contexts are shared across packages. @@ -123,7 +209,7 @@ const svgSpritePlugin = { name: "svg-sprite-inline", setup(build) { build.onLoad({ filter: /sprite\.svg$/ }, (args) => { - const content = require("fs").readFileSync(args.path, "utf8") + const content = fs.readFileSync(args.path, "utf8") return { contents: ` const svg = ${JSON.stringify(content)}; @@ -160,69 +246,8 @@ const cssPackageResolvePlugin = { }, } -function createBrowserWebviewContext(entryPoint, outfile) { - return esbuild.context({ - entryPoints: [entryPoint], - bundle: true, - format: "iife", - minify: production, - sourcemap: !production, - sourcesContent: false, - platform: "browser", - outfile, - logLevel: "silent", - loader: { - ".woff": "file", - ".woff2": "file", - ".ttf": "file", - }, - plugins: [ - solidDedupePlugin, - pierreWorkerAliasPlugin, - markdownWorkerUrlPlugin, - svgSpritePlugin, - cssPackageResolvePlugin, - solidPlugin(), - esbuildProblemMatcherPlugin, - ], - }) -} - -// Bundle Pierre's Shiki worker into a single self-contained asset that the -// webviews load off the main thread for syntax highlighting. -function createShikiWorkerContext() { - return esbuild.context({ - entryPoints: ["kilo-shiki-worker"], - bundle: true, - format: "iife", - minify: production, - sourcemap: !production, - sourcesContent: false, - platform: "browser", - outfile: "dist/shiki-worker.js", - logLevel: "silent", - plugins: [shikiWorkerEntryPlugin, esbuildProblemMatcherPlugin], - }) -} - -function createMarkdownShikiWorkerContext() { - return esbuild.context({ - entryPoints: [path.join(__dirname, "..", "ui", "src", "components", "markdown-shiki.worker.ts")], - bundle: true, - format: "esm", - minify: production, - sourcemap: !production, - sourcesContent: false, - platform: "browser", - outfile: "dist/markdown-shiki-worker.js", - logLevel: "silent", - plugins: [esbuildProblemMatcherPlugin], - }) -} - -async function main() { - // Build extension - const extensionCtx = await esbuild.context({ +function getExtensionConfig() { + return { entryPoints: ["src/extension.ts"], bundle: true, format: "cjs", @@ -239,68 +264,101 @@ async function main() { outfile: "dist/extension.js", external: ["vscode"], logLevel: "silent", - plugins: [esbuildProblemMatcherPlugin], - }) + plugins: watch ? [esbuildProblemMatcherPlugin] : [], + } +} - // Build Agent Manager webview (SolidJS, shares components with sidebar) - const agentManagerCtx = await createBrowserWebviewContext( - "webview-ui/agent-manager/index.tsx", - "dist/agent-manager.js", - ) +function getWebviewsConfig() { + return { + entryPoints: { + "agent-manager": "webview-ui/agent-manager/index.tsx", + kiloclaw: "webview-ui/kiloclaw/index.tsx", + marketplace: "webview-ui/marketplace/index.tsx", + "diff-viewer": "webview-ui/diff-viewer/index.tsx", + "diff-virtual": "webview-ui/diff-virtual/index.tsx", + webview: "webview-ui/src/index.tsx", + }, + outdir: "dist", + bundle: true, + format: "iife", + minify: production, + sourcemap: !production, + sourcesContent: false, + platform: "browser", + logLevel: "silent", + loader: { + ".woff": "file", + ".woff2": "file", + ".ttf": "file", + }, + plugins: [ + solidDedupePlugin, + pierreWorkerAliasPlugin, + markdownWorkerUrlPlugin, + svgSpritePlugin, + cssPackageResolvePlugin, + cachedSolidPlugin, + ...(watch ? [esbuildProblemMatcherPlugin] : []), + ], + } +} - // Build KiloClaw webview (SolidJS, standalone chat panel) - const kiloClawCtx = await createBrowserWebviewContext("webview-ui/kiloclaw/index.tsx", "dist/kiloclaw.js") +function getShikiWorkerConfig() { + return { + entryPoints: ["kilo-shiki-worker"], + bundle: true, + format: "iife", + minify: production, + sourcemap: !production, + sourcesContent: false, + platform: "browser", + outfile: "dist/shiki-worker.js", + logLevel: "silent", + plugins: [shikiWorkerEntryPlugin, ...(watch ? [esbuildProblemMatcherPlugin] : [])], + } +} - // Build Marketplace webview (SolidJS, standalone catalog panel) - const marketplaceCtx = await createBrowserWebviewContext("webview-ui/marketplace/index.tsx", "dist/marketplace.js") +function getMarkdownShikiWorkerConfig() { + return { + entryPoints: [path.join(__dirname, "..", "ui", "src", "components", "markdown-shiki.worker.ts")], + bundle: true, + format: "esm", + minify: production, + sourcemap: !production, + sourcesContent: false, + platform: "browser", + outfile: "dist/markdown-shiki-worker.js", + logLevel: "silent", + plugins: watch ? [esbuildProblemMatcherPlugin] : [], + } +} - // Build Diff Viewer webview (SolidJS, reuses Agent Manager diff components) - const diffViewerCtx = await createBrowserWebviewContext("webview-ui/diff-viewer/index.tsx", "dist/diff-viewer.js") - - // Build Diff Virtual webview (lightweight single-file diff for permission approval) - const diffVirtualCtx = await createBrowserWebviewContext("webview-ui/diff-virtual/index.tsx", "dist/diff-virtual.js") - - // Build webview - const webviewCtx = await createBrowserWebviewContext("webview-ui/src/index.tsx", "dist/webview.js") - - // Build the shared Shiki highlighting worker asset - const shikiWorkerCtx = await createShikiWorkerContext() - const markdownShikiWorkerCtx = await createMarkdownShikiWorkerContext() +async function main() { + const extensionConfig = getExtensionConfig() + const webviewsConfig = getWebviewsConfig() + const shikiWorkerConfig = getShikiWorkerConfig() + const markdownShikiWorkerConfig = getMarkdownShikiWorkerConfig() if (watch) { + const [extensionCtx, webviewsCtx, shikiWorkerCtx, markdownShikiWorkerCtx] = await Promise.all([ + esbuild.context(extensionConfig), + esbuild.context(webviewsConfig), + esbuild.context(shikiWorkerConfig), + esbuild.context(markdownShikiWorkerConfig), + ]) + await Promise.all([ extensionCtx.watch(), - webviewCtx.watch(), - agentManagerCtx.watch(), - diffViewerCtx.watch(), - diffVirtualCtx.watch(), - kiloClawCtx.watch(), - marketplaceCtx.watch(), + webviewsCtx.watch(), shikiWorkerCtx.watch(), markdownShikiWorkerCtx.watch(), ]) } else { await Promise.all([ - extensionCtx.rebuild(), - webviewCtx.rebuild(), - agentManagerCtx.rebuild(), - kiloClawCtx.rebuild(), - marketplaceCtx.rebuild(), - diffViewerCtx.rebuild(), - diffVirtualCtx.rebuild(), - shikiWorkerCtx.rebuild(), - markdownShikiWorkerCtx.rebuild(), - ]) - await Promise.all([ - extensionCtx.dispose(), - webviewCtx.dispose(), - agentManagerCtx.dispose(), - diffViewerCtx.dispose(), - diffVirtualCtx.dispose(), - kiloClawCtx.dispose(), - marketplaceCtx.dispose(), - shikiWorkerCtx.dispose(), - markdownShikiWorkerCtx.dispose(), + esbuild.build(extensionConfig), + esbuild.build(webviewsConfig), + esbuild.build(shikiWorkerConfig), + esbuild.build(markdownShikiWorkerConfig), ]) } } diff --git a/packages/kilo-vscode/eslint.config.mjs b/packages/kilo-vscode/eslint.config.mjs index 8aedaac465c..06624301c3c 100644 --- a/packages/kilo-vscode/eslint.config.mjs +++ b/packages/kilo-vscode/eslint.config.mjs @@ -44,8 +44,9 @@ export default [ { files: ["webview-ui/agent-manager/AgentManagerApp.tsx"], // Lowered 3210 → 2800 after extracting the sidebar body (SidebarBody.tsx) - // and the tab bar (TabBar.tsx) into components. Keep shrinking as more - // logic moves out; do not raise. + // and the tab bar (TabBar.tsx) into components. The keybinding defaults + // (keybind-defaults.ts) extraction offsets the terminal-ux additions; keep + // shrinking as more logic moves out; do not raise. rules: { complexity: ["error", 74], "max-lines": ["error", 2800] }, }, { diff --git a/packages/kilo-vscode/package.json b/packages/kilo-vscode/package.json index 26d6af3ffad..2721db62a0a 100644 --- a/packages/kilo-vscode/package.json +++ b/packages/kilo-vscode/package.json @@ -59,6 +59,12 @@ ], "main": "./dist/extension.js", "contributes": { + "configurationDefaults": { + "files.watcherExclude": { + "**/.kilo/worktrees/**": true, + "**/.kilocode/worktrees/**": true + } + }, "taskDefinitions": [ { "type": "kilo-worktree-setup", @@ -245,6 +251,16 @@ "title": "Agent Manager: Next Tab", "category": "Kilo Code" }, + { + "command": "kilo-code.new.agentManager.previousTerminal", + "title": "Agent Manager: Previous Terminal", + "category": "Kilo Code" + }, + { + "command": "kilo-code.new.agentManager.nextTerminal", + "title": "Agent Manager: Next Terminal", + "category": "Kilo Code" + }, { "command": "kilo-code.new.agentManager.search", "title": "Agent Manager: Search Worktrees and Sessions", @@ -498,37 +514,37 @@ { "command": "kilo-code.new.sidebarTitle.plusButtonClicked", "group": "navigation@0", - "when": "view == kilo-code.SidebarProvider" + "when": "view == kilo-code.SidebarProvider && !kilo-code.new.isCursor" }, { "command": "kilo-code.new.sidebarTitle.historyButtonClicked", "group": "navigation@1", - "when": "view == kilo-code.SidebarProvider" + "when": "view == kilo-code.SidebarProvider && !kilo-code.new.isCursor" }, { "command": "kilo-code.new.sidebarTitle.agentManagerOpen", "group": "navigation@2", - "when": "view == kilo-code.SidebarProvider" + "when": "view == kilo-code.SidebarProvider && !kilo-code.new.isCursor" }, { "command": "kilo-code.new.sidebarTitle.kiloClawOpen", "group": "navigation@3", - "when": "view == kilo-code.SidebarProvider" + "when": "view == kilo-code.SidebarProvider && !kilo-code.new.isCursor" }, { "command": "kilo-code.new.sidebarTitle.marketplaceButtonClicked", "group": "navigation@4", - "when": "view == kilo-code.SidebarProvider" + "when": "view == kilo-code.SidebarProvider && !kilo-code.new.isCursor" }, { "command": "kilo-code.new.sidebarTitle.profileButtonClicked", "group": "navigation@5", - "when": "view == kilo-code.SidebarProvider" + "when": "view == kilo-code.SidebarProvider && !kilo-code.new.isCursor" }, { "command": "kilo-code.new.sidebarTitle.settingsButtonClicked", "group": "navigation@6", - "when": "view == kilo-code.SidebarProvider" + "when": "view == kilo-code.SidebarProvider && !kilo-code.new.isCursor" } ], "scm/title": [ @@ -554,22 +570,22 @@ { "command": "kilo-code.new.plusButtonClicked", "group": "navigation@0", - "when": "activeWebviewPanelId == kilo-code.new.TabPanel" + "when": "activeWebviewPanelId == kilo-code.new.TabPanel && !kilo-code.new.isCursor" }, { "command": "kilo-code.new.historyButtonClicked", "group": "navigation@1", - "when": "activeWebviewPanelId == kilo-code.new.TabPanel" + "when": "activeWebviewPanelId == kilo-code.new.TabPanel && !kilo-code.new.isCursor" }, { "command": "kilo-code.new.profileButtonClicked", "group": "navigation@2", - "when": "activeWebviewPanelId == kilo-code.new.TabPanel" + "when": "activeWebviewPanelId == kilo-code.new.TabPanel && !kilo-code.new.isCursor" }, { "command": "kilo-code.new.settingsButtonClicked", "group": "navigation@3", - "when": "activeWebviewPanelId == kilo-code.new.TabPanel" + "when": "activeWebviewPanelId == kilo-code.new.TabPanel && !kilo-code.new.isCursor" } ], "editor/context": [ @@ -633,6 +649,11 @@ "key": "ctrl+shift+g", "mac": "cmd+shift+g" }, + { + "command": "-workbench.actions.view.problems", + "key": "ctrl+shift+m", + "mac": "cmd+shift+m" + }, { "command": "kilo-code.new.agentManagerOpen", "key": "ctrl+shift+m", @@ -668,6 +689,18 @@ "mac": "cmd+alt+right", "when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'" }, + { + "command": "kilo-code.new.agentManager.previousTerminal", + "key": "ctrl+shift+[", + "mac": "cmd+shift+[", + "when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'" + }, + { + "command": "kilo-code.new.agentManager.nextTerminal", + "key": "ctrl+shift+]", + "mac": "cmd+shift+]", + "when": "activeWebviewPanelId == 'kilo-code.new.AgentManagerPanel'" + }, { "command": "kilo-code.new.agentManager.search", "key": "ctrl+f", @@ -1158,6 +1191,11 @@ "default": false, "description": "Show tokens-per-second (prompt-processing / text-generation) badges on assistant messages and the task header" }, + "kilo-code.new.showAutoApprovalReason": { + "type": "boolean", + "default": true, + "description": "Show a line on tool calls explaining why they were auto-approved (matched rule, agent default, YOLO mode, etc.)" + }, "kilo-code.new.chat.shiftTabCyclesVariant": { "type": "boolean", "default": true, @@ -1193,12 +1231,12 @@ "prepare:cli-binary": "bun script/local-bin.ts", "prepare:sdk": "bun script/prepare-sdk.ts", "build:launch": "bun run prepare:cli-binary && bun run prepare:sdk && bun run build:check:production", - "compile": "bun run prepare:cli-binary -- --force && bun run rebuild-sdk && bun run build:check", - "watch": "bun run rebuild-sdk && bun run --parallel watch:esbuild watch:tsc", + "compile": "bun run prepare:cli-binary && bun run prepare:sdk && bun run build:check", + "watch": "bun run prepare:sdk && bun run --parallel watch:esbuild watch:tsc", "watch:esbuild": "bun run prepare:cli-binary && node esbuild.js --watch", "watch:tsc": "tsc --noEmit --watch --project tsconfig.json", "watch:cli": "bun script/watch-cli.ts", - "package": "bun run prepare:cli-binary && bun run rebuild-sdk && bun run build:check:production", + "package": "bun run prepare:cli-binary && bun run prepare:sdk && bun run build:check:production", "build:check": "bun run --parallel check-types check-types:webview lint bundle", "build:check:production": "bun run --parallel check-types check-types:webview lint bundle:production", "bundle": "bun esbuild.js", @@ -1230,6 +1268,8 @@ }, "devDependencies": { "@axe-core/playwright": "4.11.3", + "@babel/core": "^7.28.4", + "@babel/preset-typescript": "^7.27.1", "@playwright/test": "1.57.0", "@storybook/addon-a11y": "10.2.10", "@storybook/addon-docs": "10.2.10", @@ -1241,6 +1281,7 @@ "@vscode/test-cli": "^0.0.12", "@vscode/test-electron": "^2.5.2", "@vscode/vsce": "^3.7.1", + "babel-preset-solid": "^1.9.9", "esbuild": "^0.27.2", "esbuild-plugin-solid": "^0.6.0", "eslint": "^9.39.2", @@ -1271,6 +1312,7 @@ "@opencode-ai/ui": "workspace:*", "@pierre/diffs": "catalog:", "@thisbeyond/solid-dnd": "0.7.5", + "@vscode/codicons": "^0.0.44", "@xterm/addon-clipboard": "0.2.0", "@xterm/addon-fit": "0.11.0", "@xterm/addon-unicode-graphemes": "0.4.0", diff --git a/packages/kilo-vscode/script/dev-snapshot.ts b/packages/kilo-vscode/script/dev-snapshot.ts index edbe590573e..14ed0ee64c3 100755 --- a/packages/kilo-vscode/script/dev-snapshot.ts +++ b/packages/kilo-vscode/script/dev-snapshot.ts @@ -1,5 +1,6 @@ #!/usr/bin/env bun import { $ } from "bun" +import { createRequire } from "node:module" import { join, dirname } from "node:path" import { tmpdir } from "node:os" import { rmSync, mkdirSync, existsSync } from "node:fs" @@ -26,37 +27,53 @@ console.log(`Commit: ${sha}`) console.log(`Mode: ${mode}\n`) console.log("🧹 Cleaning build directories...") -for (const dir of ["bin", "dist"]) { - const dirPath = join(root, dir) - if (existsSync(dirPath)) { - rmSync(dirPath, { recursive: true, force: true }) - console.log(` ✓ Cleaned ${dir}/`) - } +const dist = join(root, "dist") +if (existsSync(dist)) { + rmSync(dist, { recursive: true, force: true }) + console.log(" ✓ Cleaned dist/") } const outDir = join(tmpdir(), "kilo-vscode-snapshots") mkdirSync(outDir, { recursive: true }) -console.log("\n📦 Rebuilding SDK...") -await $`bun run --cwd ../sdk/js build`.cwd(root) +console.log("\n📦 Preparing SDK...") +await $`bun run prepare:sdk`.cwd(root) -console.log("\n🔧 Preparing CLI binary...") -await $`bun script/local-bin.ts --force`.cwd(root) - -console.log("\n✅ Type-checking...") -await $`bun run typecheck`.cwd(root) - -console.log("\n🔍 Linting...") -await $`bun run lint`.cwd(root) - -console.log("\n🏗️ Building extension...") -await $`node ${join(root, "esbuild.js")} --production`.cwd(root) +console.log("\n🔧 Preparing CLI binary and validating extension...") +await $`bun script/local-bin.ts --compiled`.cwd(root) +await $`bun run build:check:production`.cwd(root) console.log("\n📦 Packaging VSIX...") const vsixPath = join(outDir, `kilo-vscode-snapshot-${sha}-${user}.vsix`) -await $`bunx vsce package ${snapshotVersion} --no-update-package-json --no-dependencies --skip-license -o ${vsixPath}`.cwd( - root, -) +const require = createRequire(import.meta.url) +const vsceRequire = createRequire(require.resolve("@vscode/vsce")) +if (shouldInstall) { + // Local installs favor fast packaging and extraction over archive size. + type Options = Record + type Zip = { + addFile(path: string, name: string, options?: Options): void + addBuffer(data: Uint8Array, name: string, options?: Options): void + } + const yazl = vsceRequire("yazl") as { ZipFile: { prototype: Zip } } + const zip = yazl.ZipFile.prototype + const file = zip.addFile + const buffer = zip.addBuffer + zip.addFile = function (this: Zip, path, name, options) { + return file.call(this, path, name, { ...options, compress: false }) + } + zip.addBuffer = function (this: Zip, data, name, options) { + return buffer.call(this, data, name, { ...options, compress: false }) + } +} +const { createVSIX } = await import("@vscode/vsce") +await createVSIX({ + cwd: root, + packagePath: vsixPath, + version: snapshotVersion, + updatePackageJson: false, + dependencies: false, + skipLicense: true, +}) if (shouldInstall) { const execPath = process.env.VSCODE_EXEC_PATH ?? "" diff --git a/packages/kilo-vscode/script/ffmpeg-helper.ts b/packages/kilo-vscode/script/ffmpeg-helper.ts index c0b5e440ef4..eaf604a9d0e 100644 --- a/packages/kilo-vscode/script/ffmpeg-helper.ts +++ b/packages/kilo-vscode/script/ffmpeg-helper.ts @@ -23,7 +23,8 @@ export async function ensureFfmpegForTarget(target: string, bin: string): Promis const exe = target.startsWith("win32") ? "ffmpeg.exe" : "ffmpeg" const dest = join(bin, exe) - if (existsSync(dest)) return + const marker = join(bin, "..", "node_modules", ".kilo-ffmpeg-target") + if (existsSync(dest) && existsSync(marker) && (await Bun.file(marker).text()).trim() === target) return const tmp = join(bin, ".ffmpeg-tmp") rmSync(tmp, { recursive: true, force: true }) @@ -37,6 +38,7 @@ export async function ensureFfmpegForTarget(target: string, bin: string): Promis await $`tar -xzf ${join(tmp, name)} -C ${tmp}`.quiet() copyFileSync(join(tmp, "package", exe), dest) if (!target.startsWith("win32")) chmodSync(dest, 0o755) + await Bun.write(marker, `${target}\n`) } finally { rmSync(tmp, { recursive: true, force: true }) } diff --git a/packages/kilo-vscode/script/local-bin.ts b/packages/kilo-vscode/script/local-bin.ts index 9a2f11f550b..0ea79671b0e 100644 --- a/packages/kilo-vscode/script/local-bin.ts +++ b/packages/kilo-vscode/script/local-bin.ts @@ -1,5 +1,6 @@ #!/usr/bin/env bun import { $ } from "bun" +import { createHash } from "node:crypto" import { join, relative, dirname, basename } from "node:path" import { chmodSync, statSync, rmSync, readdirSync, existsSync } from "node:fs" import { @@ -15,6 +16,7 @@ import { currentBwrapTarget, ensureBwrapForTarget } from "./bwrap-helper" import { currentFfmpegTarget, ensureFfmpegForTarget } from "./ffmpeg-helper" const forceRebuild = process.argv.includes("--force") +const compiledOnly = process.argv.includes("--compiled") /** * Ensures the VS Code extension has a CLI binary at `packages/kilo-vscode/bin/kilo`. @@ -32,64 +34,154 @@ const kiloVscodeDir = join(import.meta.dir, "..") const packagesDir = join(kiloVscodeDir, "..") const repoDir = join(packagesDir, "..") const opencodeDir = join(packagesDir, "opencode") -const coreDir = join(packagesDir, "core") -const gatewayDir = join(packagesDir, "kilo-gateway") -const indexingDir = join(packagesDir, "kilo-indexing") const sandboxDir = join(packagesDir, "kilo-sandbox") +const rootFile = join(repoDir, "package.json") const targetBinDir = join(kiloVscodeDir, "bin") const binName = process.platform === "win32" ? "kilo.exe" : "kilo" const targetBinPath = join(targetBinDir, binName) -const versionFile = join(targetBinDir, ".cli-version") +const versionFile = join(kiloVscodeDir, "node_modules", ".kilo-cli-version") function log(msg: string) { console.log(`[local-bin] ${msg}`) } -async function cliSourceHash(): Promise { +type Package = { + name?: string + workspaces?: string[] | { packages?: string[] } + dependencies?: Record + devDependencies?: Record + optionalDependencies?: Record + peerDependencies?: Record +} + +async function cliInputs() { + const root: Package = await Bun.file(rootFile).json() + const workspaces = Array.isArray(root.workspaces) ? root.workspaces : (root.workspaces?.packages ?? []) + const files = ( + await Promise.all( + workspaces.map((pattern) => + Array.fromAsync(new Bun.Glob(`${pattern}/package.json`).scan({ cwd: repoDir, onlyFiles: true })), + ), + ) + ).flat() + const entries = await Promise.all( + files.map(async (file) => ({ file, pkg: (await Bun.file(join(repoDir, file)).json()) as Package })), + ) + const packages = new Map(entries.flatMap((entry) => (entry.pkg.name ? [[entry.pkg.name, entry] as const] : []))) + const dirs = new Set() + + function visit(name: string) { + const entry = packages.get(name) + if (!entry) return + const dir = dirname(entry.file) + if (dirs.has(dir)) return + dirs.add(dir) + const deps = { + ...entry.pkg.dependencies, + ...entry.pkg.devDependencies, + ...entry.pkg.optionalDependencies, + ...entry.pkg.peerDependencies, + } + for (const dep of Object.keys(deps)) visit(dep) + } + + // The CLI build embeds the console even though it is not a package dependency. + for (const dir of [opencodeDir, join(packagesDir, "kilo-console")]) { + const pkg: Package = await Bun.file(join(dir, "package.json")).json() + if (!pkg.name) throw new Error(`Workspace package at ${dir} has no name`) + visit(pkg.name) + } + + return [ + relative(repoDir, rootFile), + "bun.lock", + "patches", + ...[...dirs].sort(), + "packages/kilo-vscode/script/bwrap-helper.ts", + "packages/kilo-vscode/script/ffmpeg-helper.ts", + "packages/kilo-vscode/script/local-bin.ts", + "packages/kilo-vscode/src/services/cli-backend/cli-resources.ts", + ] +} + +async function cliSourceHash() { try { - const opencodeResult = await $`git log -1 --format=%H -- .`.cwd(opencodeDir).quiet() - const coreResult = await $`git log -1 --format=%H -- .`.cwd(coreDir).quiet() - const gatewayResult = await $`git log -1 --format=%H -- .`.cwd(gatewayDir).quiet() - const indexingResult = await $`git log -1 --format=%H -- .`.cwd(indexingDir).quiet() - const sandboxResult = await $`git log -1 --format=%H -- .`.cwd(sandboxDir).quiet() - return `${opencodeResult.text().trim()}-${coreResult.text().trim()}-${gatewayResult.text().trim()}-${indexingResult.text().trim()}-${sandboxResult.text().trim()}` - } catch { + const inputs = await cliInputs() + const [tree, diff, extra, branch] = await Promise.all([ + $`git ls-tree -r HEAD -- ${inputs}`.cwd(repoDir).quiet(), + $`git diff --binary HEAD -- ${inputs}`.cwd(repoDir).quiet(), + $`git ls-files --others --exclude-standard -z -- ${inputs}`.cwd(repoDir).quiet(), + $`git branch --show-current`.cwd(repoDir).quiet(), + ]) + const env = Object.fromEntries( + [ + "GH_REPO", + "KILO_BUMP", + "KILO_BWRAP_CACHE", + "KILO_CHANNEL", + "KILO_MODELS_URL", + "KILO_PRE_RELEASE", + "KILO_RELEASE", + "KILO_SKIP_BUNDLED_BWRAP", + "KILO_VERSION", + "MODELS_DEV_API_JSON", + "ZIG", + ].map((key) => [key, process.env[key] ?? ""]), + ) + const hash = createHash("sha256") + .update(tree.text()) + .update(diff.text()) + .update(branch.text()) + .update(JSON.stringify(env)) + const files = extra.text().split("\0").filter(Boolean).sort() + + for (const file of files) { + hash.update(file) + hash.update(new Uint8Array(await Bun.file(join(repoDir, file)).arrayBuffer())) + } + + const models = process.env.MODELS_DEV_API_JSON + if (models) hash.update(new Uint8Array(await Bun.file(models).arrayBuffer())) + return hash.digest("hex") + } catch (err) { + log(`Could not determine CLI source hash: ${err instanceof Error ? err.message : String(err)}`) return null } } -async function isDirty(): Promise { - try { - const opencodeResult = await $`git status --porcelain -- .`.cwd(opencodeDir).quiet() - const coreResult = await $`git status --porcelain -- .`.cwd(coreDir).quiet() - const gatewayResult = await $`git status --porcelain -- .`.cwd(gatewayDir).quiet() - const indexingResult = await $`git status --porcelain -- .`.cwd(indexingDir).quiet() - const sandboxResult = await $`git status --porcelain -- .`.cwd(sandboxDir).quiet() - return ( - opencodeResult.text().trim().length > 0 || - coreResult.text().trim().length > 0 || - gatewayResult.text().trim().length > 0 || - indexingResult.text().trim().length > 0 || - sandboxResult.text().trim().length > 0 - ) - } catch { - return false - } -} - -async function isStale(): Promise { - if (await isDirty()) return true +async function isStale() { const hash = await cliSourceHash() - if (!hash) return false // can't determine — assume fresh + if (!hash) { + if (!compiledOnly) return false + try { + const stored: unknown = await Bun.file(versionFile).json() + return Reflect.get(stored, "kind") !== "compiled" + } catch { + return true + } + } try { - const stored = (await Bun.file(versionFile).text()).trim() - return stored !== hash + const stored: unknown = await Bun.file(versionFile).json() + if (!stored || typeof stored !== "object") return true + const input = Reflect.get(stored, "input") + const target = Reflect.get(stored, "target") + const kind = Reflect.get(stored, "kind") + return input !== hash || target !== platformTag() || (compiledOnly && kind !== "compiled") } catch { return true // no version file — treat as stale } } +async function writeVersion(kind: "compiled" | "wrapper") { + const input = await cliSourceHash() + if (!input) { + rmSync(versionFile, { force: true }) + return + } + await Bun.write(versionFile, JSON.stringify({ input, target: platformTag(), kind }) + "\n") +} + function platformTag(): string { const os = process.platform === "win32" ? "windows" : process.platform return `cli-${os}-${process.arch}` @@ -115,6 +207,8 @@ async function findKiloBinaryInOpencodeDist(): Promise { // fall through to generic search } + if (compiledOnly) return null + // Fallback: find any dist/**/bin/kilo or kilo.exe const queue = [distDir] while (queue.length) { @@ -158,13 +252,15 @@ async function ensureBuiltBinary(): Promise { ) } - // Use the repository-pinned Bun version throughout. Newer canaries can fail compilation - // and must not cause packaged snapshots to fall back to the browser-mode source wrapper. const pkg = await Bun.file(join(repoDir, "package.json")).json() const bun = String(pkg.packageManager) - log("Installing dependencies in opencode package...") - await $`bunx ${bun} install --frozen-lockfile`.cwd(opencodeDir) - await $`bunx ${bun} run build --single --skip-install`.cwd(opencodeDir) + log("Building CLI binary...") + try { + await $`bunx ${bun} run build --single --skip-install`.cwd(opencodeDir) + } catch (err) { + log(`Pinned bunx build failed (${err}), running via active bun runtime...`) + await $`bun run script/build.ts --single --skip-install`.cwd(opencodeDir) + } const built = await findKiloBinaryInOpencodeDist() if (!built) { @@ -214,14 +310,16 @@ async function writeSourceWrapper() { await bundleKiloSandboxWorker() await ensureLocalHelpers() - const hash = await cliSourceHash() - if (hash) await Bun.write(versionFile, hash + "\n") + await writeVersion("wrapper") log( `Compiled CLI build failed; wrote source wrapper at ${relative(kiloVscodeDir, targetBinPath)} for local development.`, ) } async function main() { + for (const file of [join(targetBinDir, ".cli-version"), join(targetBinDir, ".ffmpeg-target")]) { + rmSync(file, { force: true }) + } const targetFile = Bun.file(targetBinPath) const exists = await targetFile.exists() const ready = exists && hasTreeSitterResources(targetBinPath) && hasKiloSandboxWorker(targetBinPath) @@ -238,7 +336,7 @@ async function main() { return } - if (forceRebuild && !exists) { + if ((forceRebuild || compiledOnly) && !ready) { removeDist() } @@ -256,7 +354,7 @@ async function main() { } const sourceBinPath = await ensureBuiltBinary().catch(async (err) => { - if (forceRebuild) throw err + if (forceRebuild || compiledOnly) throw err await writeSourceWrapper() log(`Wrapper fallback reason: ${err instanceof Error ? err.message : String(err)}`) return null @@ -270,8 +368,7 @@ async function main() { chmodSync(targetBinPath, 0o755) await ensureLocalHelpers() - const hash = await cliSourceHash() - if (hash) await Bun.write(versionFile, hash + "\n") + await writeVersion("compiled") log(`Copied CLI binary from ${relative(packagesDir, sourceBinPath)} -> ${relative(kiloVscodeDir, targetBinPath)}`) } diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index c04d818b4b8..4660904c00a 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -17,7 +17,7 @@ import type { EditorContext, IndexingStatus } from "./services/cli-backend/types import { FileIgnoreController } from "./services/autocomplete/shims/FileIgnoreController" import { ChatTextAreaAutocomplete } from "./services/autocomplete/chat-autocomplete/ChatTextAreaAutocomplete" import { notebookUri } from "./services/autocomplete/continuedev/core/autocomplete/notebook" -import { buildWebviewHtml, getWebviewFontSize } from "./utils" +import { buildWebviewHtml, getWebviewFontSize, isCursorHost } from "./utils" import { saveImage } from "./kilo-provider/save-image" import { handleEditorAction } from "./kilo-provider/editor-actions" import { exportTranscript } from "./kilo-provider/export-transcript" @@ -49,7 +49,6 @@ import { } from "./kilo-provider-utils" import { GitOps } from "./agent-manager/GitOps" import { GitStatsPoller, type LocalStats } from "./agent-manager/GitStatsPoller" -import { diffSummary as localDiffSummary } from "./agent-manager/local-diff" import { createMarketplaceRemover, removeMcp } from "./kilo-provider/remove-config-item" import { AgentRequirementsController } from "./kilo-provider/agent-requirements-controller" import type { RemoteStatusService } from "./services/RemoteStatusService" @@ -90,6 +89,7 @@ import { } from "./services/autocomplete/settings" import { routeEarlyMessage } from "./kilo-provider/early-message" import * as ModelState from "./kilo-provider/model-state" +import { handleModelUsageMessage } from "./kilo-provider/model-usage" import { handleForkSession } from "./kilo-provider/fork-session" import { openConfig } from "./kilo-provider/open-config" import { @@ -186,6 +186,10 @@ import { import { canonicalizePath, projectIdFor, samePath } from "./agent-manager/project/paths" import { validChatSetting, watchChatConfig } from "./kilo-provider/chat-settings" import { buildThroughputSettingMessage, watchThroughputConfig } from "./kilo-provider/throughput-settings" +import { + buildAutoApprovalReasonSettingMessage, + watchAutoApprovalReasonConfig, +} from "./kilo-provider/auto-approval-reason-settings" let maxCost = 0 @@ -419,6 +423,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper private indexingConfigDisposable: vscode.Disposable | null = null private chatConfigDisposable: vscode.Disposable | null = null private throughputConfigDisposable: vscode.Disposable | null = null + private autoApprovalReasonConfigDisposable: vscode.Disposable | null = null private telemetryStateDisposable: vscode.Disposable | null = null private viewStateDisposable: vscode.Disposable | null = null private visibilityDisposable: vscode.Disposable | null = null @@ -442,6 +447,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper private cachedGitRepo = false private cachedGitDirectory: string | undefined private gitStatusRevision = 0 + private sessionRefreshRevision = 0 private onBeforeMessage: ((msg: Record) => Promise | null>) | null = null @@ -1001,6 +1007,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.chatConfigDisposable = watchChatConfig((msg) => this.postMessage(msg)) this.throughputConfigDisposable?.dispose() this.throughputConfigDisposable = watchThroughputConfig((msg) => this.postMessage(msg)) + this.autoApprovalReasonConfigDisposable?.dispose() + this.autoApprovalReasonConfigDisposable = watchAutoApprovalReasonConfig((msg) => this.postMessage(msg)) this.telemetryStateDisposable?.dispose() this.telemetryStateDisposable = watchTelemetryState((msg) => this.postMessage(msg)) this.webviewMessageDisposable = webview.onDidReceiveMessage(async (message) => { @@ -1025,6 +1033,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper copy: (text) => vscode.env.clipboard.writeText(text), openSessions: (ids) => this.trackOpenSessions(ids), speechToTextModels: () => this.fetchAndSendSpeechToTextModels(), + modelUsage: (msg) => handleModelUsageMessage(msg, this.extensionContext, (value) => this.postMessage(value)), }) ) { return @@ -1046,6 +1055,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper openAdvancedWorktree: () => vscode.commands.executeCommand("kilo-code.new.agentManager.advancedWorktree"), openChanges: (sessionId?: string, turnId?: string) => vscode.commands.executeCommand("kilo-code.new.showChanges", { sessionId, turnId }), + openProfile: () => vscode.commands.executeCommand("kilo-code.new.profileButtonClicked"), currentSessionId: this.currentSession?.id, createWorktree: async (baseBranch, branchName) => { await this.createWorktreeHandler?.(baseBranch, branchName) @@ -1665,7 +1675,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper // Subscribe to SSE events for this webview (filtered by tracked sessions) this.unsubscribeEvent = this.connectionService.onEventFiltered( (payload, directory) => { - if (directory && !this.isCurrentProjectDirectory(directory)) return false + if (directory && directory !== "global" && !this.isCurrentProjectDirectory(directory)) return false if (!directory && isEventFromForeignProject(payload, this.projectID)) return false const event = unwrapSyncEvent(payload) if (!event) return false @@ -1829,6 +1839,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.sendNotificationSettings() this.sendTimelineSetting() this.postMessage(buildThroughputSettingMessage()) + this.postMessage(buildAutoApprovalReasonSettingMessage()) this.postMessage({ type: "extensionDataReady" }) console.log("[Kilo New] KiloProvider: ✅ initializeConnection completed successfully") @@ -2085,7 +2096,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper /** * Build the context object used by the extracted session-refresh helpers. */ - private get sessionRefreshContext(): SessionRefreshContext { + private getSessionRefreshContext(revision: number): SessionRefreshContext { const client = this.client return { pendingSessionRefresh: this.pendingSessionRefresh, @@ -2097,6 +2108,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper sessionDirectories: this.sessionDirectories, worktreeDirectories: this.opts.worktreeDirectories, workspaceDirectory: this.getWorkspaceDirectory(), + isCurrent: () => revision === this.sessionRefreshRevision, postMessage: (msg: unknown) => this.postMessage(msg), } } @@ -2107,9 +2119,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper private async flushPendingSessionRefresh(reason: string): Promise { if (!this.pendingSessionRefresh) return console.log("[Kilo New] KiloProvider: 🔄 Flushing deferred sessions refresh", { reason }) + const revision = ++this.sessionRefreshRevision const scope = this.opts.projectQualifier?.()?.projectId if (scope !== undefined) this.projectID = undefined - const ctx = this.sessionRefreshContext + const ctx = this.getSessionRefreshContext(revision) try { const resolved = await flushPendingSessionRefreshUtil(ctx) if (resolved && scope === this.opts.projectQualifier?.()?.projectId) this.projectID = resolved @@ -2123,9 +2136,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper * Handle loading all sessions. */ private async handleLoadSessions(): Promise { + const revision = ++this.sessionRefreshRevision const scope = this.opts.projectQualifier?.()?.projectId if (scope !== undefined) this.projectID = undefined - const ctx = this.sessionRefreshContext + const ctx = this.getSessionRefreshContext(revision) try { const resolved = await loadSessionsUtil(ctx) if (resolved && scope === this.opts.projectQualifier?.()?.projectId) this.projectID = resolved @@ -4054,6 +4068,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper // Clear globalState items that are not part of the configuration await this.extensionContext?.globalState.update("variantSelections", undefined) await this.extensionContext?.globalState.update("recentModels", undefined) + await this.extensionContext?.globalState.update("modelUsage", undefined) await this.extensionContext?.globalState.update("kilo.dismissedNotificationIds", undefined) await this.extensionContext?.globalState.update("kilo.agentMigrationBannerDismissed", undefined) await this.extensionContext?.globalState.update("kilo.marketplace.dismissedSuggestions", undefined) @@ -4065,12 +4080,14 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.sendNotificationSettings() this.sendTimelineSetting() this.postMessage(buildThroughputSettingMessage()) + this.postMessage(buildAutoApprovalReasonSettingMessage()) this.sendWorkStyle() await ModelState.reset(this.client, (msg) => this.postMessage(msg)) // Re-send globalState items to the webview this.postMessage({ type: "variantsLoaded", variants: {} }) this.postMessage({ type: "recentsLoaded", recents: [] }) + this.postMessage({ type: "modelUsageLoaded", usage: {} }) // Re-fetch notifications to reflect cleared dismissed IDs await this.fetchAndSendNotifications() @@ -4312,7 +4329,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper // Drop session events from other projects before any tracking logic. // This must come first: the trackedSessionIds guard below would otherwise // let a foreign session through if it was accidentally tracked. - if (directory && !this.isCurrentProjectDirectory(directory)) return + if (directory && directory !== "global" && !this.isCurrentProjectDirectory(directory)) return if ( this.projectID && (!this.opts.projectQualifier || !directory) && @@ -4754,6 +4771,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper private isCurrentProjectSession(sessionID: string): boolean { if (!this.opts.projectQualifier || !this.opts.routeService) return true + if (this.isSessionRouteAmbiguous(sessionID)) return false const directory = this.opts.routeService.trySessionDirectory(sessionID) return !directory || this.isCurrentProjectDirectory(directory) } @@ -4798,7 +4816,14 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper if (revision !== this.gitStatusRevision) return const found = repo || root !== undefined const target = root ?? directory - if (!this.cachedGitDirectory || !sameDirectory(this.cachedGitDirectory, target)) this.cachedStats = null + const changed = !this.cachedGitDirectory || !sameDirectory(this.cachedGitDirectory, target) + if (changed) { + this.cachedStats = null + this.statsPoller?.stop() + this.statsPoller = null + this.statsGitOps?.dispose() + this.statsGitOps = null + } this.cachedGitDirectory = target this.cachedGitRepo = found this.postMessage({ type: "gitStatus", repo: found }) @@ -4893,6 +4918,11 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper title: "Kilo Code", port: this.connectionService.getServerInfo()?.port, extraStyles: `.container { height: 100vh; }`, + // Dedicated single-purpose panels (Settings, Profile, Sub-Agent Viewer) + // never show the bar. Sidebar and "Open in Tab" only need it in Cursor — + // VS Code's native toolbar (restored in package.json) works everywhere. + topBar: this.opts.hideTopBar !== true && isCursorHost(), + topBarSurface: this.opts.topBarSurface === "tab" ? "tab_title" : "sidebar_title", }) } @@ -4929,7 +4959,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.statsPoller = new GitStatsPoller({ getWorktrees: () => [], getWorkspaceRoot: () => this.cachedGitDirectory ?? this.getWorkspaceDirectory(this.currentSession?.id), - localDiff: (dir, base) => localDiffSummary(git, dir, base), git, onStats: () => {}, onLocalStats: (stats: LocalStats) => { @@ -4981,6 +5010,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.indexingConfigDisposable?.dispose() this.chatConfigDisposable?.dispose() this.throughputConfigDisposable?.dispose() + this.autoApprovalReasonConfigDisposable?.dispose() this.telemetryStateDisposable?.dispose() this.autoApproveBridge?.dispose() this.visibleTaskStreams.clear() diff --git a/packages/kilo-vscode/src/MarketplacePanelProvider.ts b/packages/kilo-vscode/src/MarketplacePanelProvider.ts index 0c704ac5805..32fd768b236 100644 --- a/packages/kilo-vscode/src/MarketplacePanelProvider.ts +++ b/packages/kilo-vscode/src/MarketplacePanelProvider.ts @@ -6,7 +6,7 @@ import { watchFontSizeConfig } from "./kilo-provider/font-size" import { mapSSEEventToWebviewMessage } from "./kilo-provider-utils" import { resolvePanelProjectDirectory } from "./project-directory" import { seedSessionStatuses } from "./session-status" -import type { KiloConnectionService } from "./services/cli-backend" +import { type KiloConnectionService, ServerStartupError } from "./services/cli-backend" import { MarketplaceService } from "./services/marketplace" import { fetchMarketplaceData, @@ -123,7 +123,15 @@ export class MarketplacePanelProvider implements vscode.Disposable { ) this.subscriptions.push( this.connection.onStateChange((state, err) => { - this.post({ type: "connectionState", state, ...(err ? { error: err.message } : {}) }) + this.post({ + type: "connectionState", + state, + ...(err ? { error: err.message } : {}), + ...(err instanceof ServerStartupError && { + userMessage: err.userMessage, + userDetails: err.userDetails, + }), + }) if (state === "connected") void this.sync(false) }), this.connection.onLanguageChanged((locale) => this.post({ type: "languageChanged", locale })), @@ -155,7 +163,15 @@ export class MarketplacePanelProvider implements vscode.Disposable { await this.connection.connect(this.directory()) await this.sync(this.statuses.size === 0) } catch (err) { - this.post({ type: "connectionState", state: "error", error: err instanceof Error ? err.message : String(err) }) + this.post({ + type: "connectionState", + state: "error", + error: err instanceof Error ? err.message : String(err), + ...(err instanceof ServerStartupError && { + userMessage: err.userMessage, + userDetails: err.userDetails, + }), + }) } } diff --git a/packages/kilo-vscode/src/SettingsEditorProvider.ts b/packages/kilo-vscode/src/SettingsEditorProvider.ts index 84dc741d6d2..f03657921a8 100644 --- a/packages/kilo-vscode/src/SettingsEditorProvider.ts +++ b/packages/kilo-vscode/src/SettingsEditorProvider.ts @@ -103,6 +103,7 @@ export class SettingsEditorProvider implements vscode.Disposable { // backend connectivity (config, providers, agents, profile, auth). const provider = new KiloProvider(this.extensionUri, this.connectionService, this.context, { projectDirectory, + hideTopBar: true, }) if (this.remoteService) { provider.setRemoteService(this.remoteService) diff --git a/packages/kilo-vscode/src/SubAgentViewerProvider.ts b/packages/kilo-vscode/src/SubAgentViewerProvider.ts index e1693cc18bb..11dfd75e8ef 100644 --- a/packages/kilo-vscode/src/SubAgentViewerProvider.ts +++ b/packages/kilo-vscode/src/SubAgentViewerProvider.ts @@ -41,7 +41,7 @@ export class SubAgentViewerProvider implements vscode.Disposable { dark: vscode.Uri.joinPath(this.extensionUri, "assets", "icons", "kilo-dark.svg"), } - const provider = new KiloProvider(this.extensionUri, this.connectionService, this.context) + const provider = new KiloProvider(this.extensionUri, this.connectionService, this.context, { hideTopBar: true }) // Start accepting this session's SSE events as soon as the panel subscribes. // Reasoning deltas are not persisted until the reasoning part finishes. provider.trackSession(sessionID) diff --git a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts index ebfe467e076..46bce229897 100644 --- a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts +++ b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts @@ -27,6 +27,7 @@ import { GitStatsPoller, type LocalStats, type WorktreePresenceResult, type Work import { PRStatusBridge } from "./pr-status-bridge" import { createPollers, type ProjectPollers } from "./project/pollers" import { GitOps } from "./GitOps" +import type { GitExecutable } from "../util/git-executable" import { versionedName } from "./branch-name" import { BranchNamingController } from "./branch-naming" import { SetupScriptService } from "./SetupScriptService" @@ -52,8 +53,10 @@ import { import { initContextState, pushProjectSessions, reactivateProject, registerProjectSessions } from "./project/init" import { createLocalDiff } from "./local-diff" import { parseToolRequest, startFromTool, type ToolRequest } from "./tool-start" +import { handleToolEvent } from "./tool-project" import { sandboxSessionMetadata } from "../shared/sandbox-session" -import { AgentManagerOrchestrationBridge } from "./orchestration-bridge" +import { createOrchestrationBridge } from "./orchestration-setup" +import type { AgentManagerOrchestrationBridge } from "./orchestration-bridge" import { pruneSubagents } from "./prune-subagents" import { startSession } from "./mcp-warmup" import { readTerminalFont, watchTerminalFont } from "./terminal-font" @@ -72,7 +75,7 @@ import { createProjectWiring } from "./project/wiring" import { ProjectScope } from "./project/scope" import type { AgentManagerOutMessage, AgentManagerInMessage } from "./types" import type { Host, PanelContext, OutputHandle, Disposable } from "./host" - +import { focusPanelPrompt } from "./focus-panel" export class AgentManagerProvider implements Disposable { public static readonly viewType = "kilo-code.new.AgentManagerPanel" private panel: PanelContext | undefined @@ -111,6 +114,7 @@ export class AgentManagerProvider implements Disposable { private onVisibilityChange: ((visible: boolean) => void) | undefined // Tracks sessions owned by this panel until they are explicitly closed. private panelSessions = new Set() + private busySessions = new Set() /** Session ID most recently loaded via `loadMessages`; updated synchronously. */ private activeSessionId: string | undefined @@ -122,6 +126,7 @@ export class AgentManagerProvider implements Disposable { constructor( private readonly host: Host, private readonly connectionService: KiloConnectionService, + binary: GitExecutable = () => Promise.resolve("git"), ) { this.outputChannel = host.createOutput("Kilo Agent Manager") this.terminalManager = new SessionTerminalManager( @@ -175,7 +180,7 @@ export class AgentManagerProvider implements Disposable { log: (...args) => this.log(...args), }) const semaphore = new Semaphore(3) - this.gitOps = new GitOps({ log: (...args) => this.log(...args), semaphore }) + this.gitOps = new GitOps({ log: (...args) => this.log(...args), semaphore, binary }) const wiring = createProjectWiring({ host: this.host, git: this.gitOps, @@ -219,6 +224,24 @@ export class AgentManagerProvider implements Disposable { state: () => this.state, root: () => this.getRoot(), activeId: () => this.contexts.active()?.id, + hot: () => { + const ids = new Set() + const target = this.state?.getActiveTarget() + if (target?.kind === "worktree") ids.add(target.worktreeId) + if (target?.kind === "session") { + const id = this.state?.getSession(target.sessionId)?.worktreeId + if (id) ids.add(id) + } + for (const status of this.run.state().runStatuses) { + if (status.state === "running" || status.state === "stopping") ids.add(status.worktreeId) + } + for (const sid of this.busySessions) { + const owner = this.contexts.byLiveSession(sid) + const id = owner?.peekState()?.getSession(sid)?.worktreeId ?? this.state?.getSession(sid)?.worktreeId + if (id) ids.add(id) + } + return ids + }, visible: () => this.panel?.visible ?? false, post: (msg) => this.postToWebview(msg), cache: (msg) => { @@ -232,22 +255,20 @@ export class AgentManagerProvider implements Disposable { this.statsPoller = pollers.stats this.prBridge = pollers.pr this.projectPollers = pollers.projects - this.orchestration = new AgentManagerOrchestrationBridge(this.connectionService, { - root: () => this.getRoot(), - state: () => this.state, - ready: async () => { - this.stateReady ??= this.initializeState() - await this.stateReady - return this.state - }, - stats: () => this.statsPoller.snapshot(), - prs: () => this.prBridge.snapshot(), - push: () => this.pushState(), - managed: (id) => this.panelSessions.has(id) || !!this.state?.getSession(id), - close: async (id) => { - await this.onCloseSession(id) - this.postToWebview({ type: "agentManager.sessionClosed", sessionId: id }) - }, + this.orchestration = createOrchestrationBridge({ + connectionService: this.connectionService, + contexts: this.contexts, + projectScope: this.projectScope, + getRoot: () => this.getRoot(), + getState: () => this.state, + getStateReady: () => this.stateReady, + initStateReady: () => (this.stateReady = this.initializeState()), + getStats: () => this.statsPoller.snapshot(), + getPrs: () => this.prBridge.snapshot(), + pushState: (ctx) => this.pushState(ctx), + hasPanelSession: (id) => this.panelSessions.has(id), + closeSession: (id) => this.onCloseSession(id), + postSessionClosed: (id) => this.postToWebview({ type: "agentManager.sessionClosed", sessionId: id }), log: (...args) => this.log(...args), }) this.unsubTool = this.connectionService.onEventFiltered( @@ -261,7 +282,12 @@ export class AgentManagerProvider implements Disposable { this.unsubSessions = this.connectionService.onEventFiltered( (event) => { const type = (event as { type?: string }).type - return type === "session.created" || type === "session.updated" || type === "session.deleted" + return ( + type === "session.created" || + type === "session.updated" || + type === "session.deleted" || + type === "session.error" + ) }, (event) => this.onSessionLifecycle(event), ) @@ -274,9 +300,14 @@ export class AgentManagerProvider implements Disposable { */ private onSessionLifecycle(event: unknown): void { const ev = event as { type?: string; properties?: { info?: Session; sessionID?: string } } + if (ev.type === "session.error") { + if (ev.properties?.sessionID) this.busySessions.delete(ev.properties.sessionID) + return + } if (ev.type === "session.deleted") { const id = ev.properties?.sessionID if (!id) return + this.busySessions.delete(id) const ctx = this.contexts.byLiveSession(id) if (!ctx) return ctx.removeLiveSession(id) @@ -307,8 +338,13 @@ export class AgentManagerProvider implements Disposable { const sid = props?.sessionID const type = props?.status?.type if (!sid || !type) return - if (type === "idle") this.naming.idle(sid) - else this.naming.busy(sid) + if (type === "idle") { + this.busySessions.delete(sid) + this.naming.idle(sid) + return + } + this.busySessions.add(sid) + this.naming.busy(sid) } private log(...args: unknown[]) { @@ -320,22 +356,22 @@ export class AgentManagerProvider implements Disposable { if (this.panel) { this.log("Panel already open, revealing") this.panel.reveal(preserveFocus) - if (!preserveFocus) this.postToWebview({ type: "action", action: "focusInput" }) + if (!preserveFocus) + focusPanelPrompt(this.panel, this.waitForPanelReady(this.panel), this.waitForPanelActive(this.panel)) return } this.log("Opening Agent Manager panel") this.host.capture("Agent Manager Opened", { source: PLATFORM }) - this.attachPanel( - this.host.openPanel({ - onBeforeMessage: (msg) => this.onMessage(msg), - worktreeDirectories: () => this.getWorktreeDirectories(), - workspaceRoot: () => this.getRoot(), - projectId: () => this.contexts.active()?.id, - }), - ) + const panel = this.host.openPanel({ + onBeforeMessage: (msg) => this.onMessage(msg), + worktreeDirectories: () => this.getWorktreeDirectories(), + workspaceRoot: () => this.getRoot(), + projectId: () => this.contexts.active()?.id, + }) + this.attachPanel(panel) + if (!preserveFocus) focusPanelPrompt(panel, this.waitForPanelReady(panel), this.waitForPanelActive(panel)) } - public onPanelVisibilityChange(cb: (visible: boolean) => void): void { this.onVisibilityChange = cb } @@ -393,6 +429,7 @@ export class AgentManagerProvider implements Disposable { const ids = [...this.panelSessions] if (this.activeSessionId) ids.push(this.activeSessionId) this.panelSessions.clear() + this.busySessions.clear() void ctx.sessions.abortSessions(ids).catch((err) => this.log("Failed to abort sessions on panel close:", err)) this.statsPoller.stop() this.projectPollers.dispose() @@ -774,15 +811,15 @@ export class AgentManagerProvider implements Disposable { private onImportMessage(m: AgentManagerInMessage): Record | null | undefined { if (m.type === "agentManager.requestBranches") { - void this.importer.branches() + void this.importer.branches(m.projectId) return null } if (m.type === "agentManager.importFromBranch") { - void this.importer.branch(m.branch) + void this.importer.branch(m.branch, m.projectId) return null } if (m.type === "agentManager.importFromPR") { - void this.importer.pr(m.url) + void this.importer.pr(m.url, m.projectId) return null } } @@ -1018,6 +1055,7 @@ export class AgentManagerProvider implements Disposable { this.pushState() this.postToWebview({ type: "agentManager.worktreeSetup", + projectId: this.host.multiProject() ? this.context?.id : undefined, status: "ready", message: "Worktree ready", sessionId, @@ -1045,14 +1083,16 @@ export class AgentManagerProvider implements Disposable { } private onToolEvent(event: unknown, directory?: string): void { - const properties = (event as { properties?: unknown }).properties - const req = parseToolRequest(properties) - if (!req) return - if (directory) { - req.directory = directory - req.projectId ??= this.contexts.byDirectory(directory)?.id - } - void this.startToolRequest(req) + handleToolEvent( + event, + directory, + { + byDirectory: (value) => this.contexts.byDirectory(value), + usable: (id) => this.contexts.usable(id), + }, + this.projectScope, + (req) => this.startToolRequest(req), + ) } private async startToolRequest(req: ToolRequest): Promise { @@ -1557,6 +1597,7 @@ export class AgentManagerProvider implements Disposable { this.statsPoller.stop() this.prBridge.reset() this.activeSessionId = undefined + this.busySessions.clear() this.cachedWorktreeStats = this.cachedLocalStats = undefined void this.sendRepoInfo() if (!reactivateProject(ctx, this.panel?.sessions, (c) => this.pushState(c))) @@ -1654,11 +1695,11 @@ export class AgentManagerProvider implements Disposable { * Used for the keyboard shortcut to switch back from terminal. */ public focusPanel(): void { - if (!this.panel) return - this.panel.reveal(false) - this.postToWebview({ type: "action", action: "focusInput" }) + const panel = this.panel + if (!panel) return + panel.reveal(false) + focusPanelPrompt(panel, this.waitForPanelReady(panel), this.waitForPanelActive(panel)) } - public isActive(): boolean { return this.panel?.active === true } @@ -1790,19 +1831,15 @@ export class AgentManagerProvider implements Disposable { public async createFromSidebar(baseBranch?: string, branchName?: string): Promise { this.openPanel() - const panel = this.panel - if (!panel) return - if (!(await this.waitForPanelReady(panel))) return + if (!this.panel || !(await this.waitForPanelReady(this.panel))) return await this.waitForStateReady("createFromSidebar") await this.onCreateWorktree(baseBranch, branchName) } public async openAdvancedWorktree(): Promise { this.openPanel() - const panel = this.panel - if (!panel) return - if (!(await this.waitForPanelActive(panel))) return - if (!(await this.waitForPanelReady(panel))) return + if (!this.panel || !(await this.waitForPanelActive(this.panel)) || !(await this.waitForPanelReady(this.panel))) + return await this.waitForStateReady("openAdvancedWorktree") queueMicrotask(() => this.postToWebview({ type: "action", action: "advancedWorktree" })) } diff --git a/packages/kilo-vscode/src/agent-manager/GitOps.ts b/packages/kilo-vscode/src/agent-manager/GitOps.ts index ea26906b893..d33799e0c06 100644 --- a/packages/kilo-vscode/src/agent-manager/GitOps.ts +++ b/packages/kilo-vscode/src/agent-manager/GitOps.ts @@ -2,6 +2,7 @@ import * as nodePath from "path" import * as os from "os" import * as fs from "fs/promises" import { spawn } from "../util/process" +import type { GitExecutable } from "../util/git-executable" import simpleGit from "simple-git" import { parseWorktreeList, @@ -18,6 +19,8 @@ interface GitOpsOptions { runGit?: (args: string[], cwd: string) => Promise /** Shared concurrency gate for child process spawning. */ semaphore?: Semaphore + /** Validated Git executable shared by Agent Manager operations. */ + binary?: GitExecutable } export interface ApplyConflict { @@ -96,6 +99,9 @@ export class GitOps { private readonly runGit: (args: string[], cwd: string) => Promise private readonly controller = new AbortController() private readonly semaphore: Semaphore | undefined + private readonly binary: GitExecutable + private readonly injected: boolean + private executableCache: Promise | undefined private readonly resolutionCache = new Map() private static readonly CACHE_TTL_MS = 60000 private static readonly MAX_CACHE_SIZE = 100 @@ -107,12 +113,19 @@ export class GitOps { constructor(options: GitOpsOptions) { this.log = options.log this.semaphore = options.semaphore + this.binary = options.binary ?? (() => Promise.resolve("git")) + this.injected = options.runGit !== undefined this.runGit = options.runGit ?? - ((args, cwd) => - simpleGit(cwd, { abort: this.controller.signal }) + (async (args, cwd) => { + const binary = await this.executable() + return simpleGit(cwd, { + abort: this.controller.signal, + binary, + }) .raw(args) - .then((out) => out.trim())) + .then((out) => out.trim()) + }) } dispose(): void { @@ -148,22 +161,28 @@ export class GitOps { private raw(args: string[], cwd: string): Promise { const signal = this.controller.signal if (signal.aborted) return Promise.reject(new Error("GitOps disposed")) - const invoke = () => - new Promise((resolve, reject) => { - const onAbort = () => reject(new Error("GitOps disposed")) - signal.addEventListener("abort", onAbort, { once: true }) - this.runGit(args, cwd).then( - (value) => { - signal.removeEventListener("abort", onAbort) - resolve(value) - }, - (err) => { - signal.removeEventListener("abort", onAbort) - reject(err) - }, - ) - }) - return this.semaphore ? this.semaphore.run(invoke) : invoke() + return this.executable().then(() => { + if (signal.aborted) throw new Error("GitOps disposed") + const invoke = () => { + const pending = this.runGit(args, cwd) + if (!this.injected) return pending + return new Promise((resolve, reject) => { + const onAbort = () => reject(new Error("GitOps disposed")) + signal.addEventListener("abort", onAbort, { once: true }) + pending.then( + (value) => { + signal.removeEventListener("abort", onAbort) + resolve(value) + }, + (err) => { + signal.removeEventListener("abort", onAbort) + reject(err) + }, + ) + }) + } + return this.semaphore ? this.semaphore.run(invoke) : invoke() + }) } /** Return the name of the currently checked-out branch, or `"HEAD"` if detached. */ @@ -551,43 +570,79 @@ export class GitOps { return { code: result.code, stdout: result.stdout.toString("utf8"), stderr: result.stderr } } - private execBuffer(args: string[], cwd: string, options?: ExecOptions): Promise { + private async execBuffer(args: string[], cwd: string, options?: ExecOptions): Promise { + if (this.controller.signal.aborted) { + return { code: 1, stdout: Buffer.alloc(0), stderr: "GitOps disposed" } + } + const cmd = await this.executable().catch(() => undefined) + if (!cmd || this.controller.signal.aborted) { + return { code: 1, stdout: Buffer.alloc(0), stderr: "GitOps disposed" } + } + const invoke = () => this.invoke(cmd, args, cwd, options) + return this.semaphore ? this.semaphore.run(invoke) : invoke() + } + + private executable(): Promise { + const signal = this.controller.signal + if (signal.aborted) return Promise.reject(new Error("GitOps disposed")) + + return new Promise((resolve, reject) => { + const onAbort = () => reject(new Error("GitOps disposed")) + signal.addEventListener("abort", onAbort, { once: true }) + const cache = (this.executableCache ??= Promise.resolve().then(() => this.binary())) + cache.then( + (value) => { + signal.removeEventListener("abort", onAbort) + resolve(value) + }, + (err) => { + signal.removeEventListener("abort", onAbort) + if (this.executableCache === cache) this.executableCache = undefined + reject(err) + }, + ) + }) + } + + private invoke(cmd: string, args: string[], cwd: string, options?: ExecOptions): Promise { if (this.controller.signal.aborted) { return Promise.resolve({ code: 1, stdout: Buffer.alloc(0), stderr: "GitOps disposed" }) } - const invoke = () => - new Promise((resolve) => { - const child = spawn("git", args, { - cwd, - env: options?.env, - signal: this.controller.signal, - stdio: ["pipe", "pipe", "pipe"], - }) - if (options?.stdin !== undefined) { - if (!child.stdin) { - resolve({ code: 1, stdout: Buffer.alloc(0), stderr: "stdin not available for git process" }) - return - } - child.stdin.end(options.stdin) - } + return new Promise((resolve) => { + const child = spawn(cmd, args, { + cwd, + env: options?.env, + stdio: ["pipe", "pipe", "pipe"], + }) + const out: Buffer[] = [] + const err: Buffer[] = [] + let failure: string | undefined + const abort = () => child.kill("SIGTERM") - const out: Buffer[] = [] - const err: Buffer[] = [] - child.stdout?.on("data", (chunk: Buffer) => out.push(chunk)) - child.stderr?.on("data", (chunk: Buffer) => err.push(chunk)) + this.controller.signal.addEventListener("abort", abort, { once: true }) + child.stdout?.on("data", (chunk: Buffer) => out.push(chunk)) + child.stderr?.on("data", (chunk: Buffer) => err.push(chunk)) - child.on("error", (error) => { - resolve({ code: 1, stdout: Buffer.alloc(0), stderr: error.message }) - }) - child.on("close", (code) => { - resolve({ - code: code ?? 1, - stdout: Buffer.concat(out), - stderr: Buffer.concat(err).toString("utf8"), - }) + child.on("error", (error) => { + failure = error.message + }) + child.on("close", (code) => { + this.controller.signal.removeEventListener("abort", abort) + resolve({ + code: code ?? 1, + stdout: Buffer.concat(out), + stderr: failure ?? Buffer.concat(err).toString("utf8"), }) }) - return this.semaphore ? this.semaphore.run(invoke) : invoke() + + if (options?.stdin === undefined) return + if (child.stdin) { + child.stdin.end(options.stdin) + return + } + failure = "stdin not available for git process" + child.kill("SIGTERM") + }) } } diff --git a/packages/kilo-vscode/src/agent-manager/GitStatsPoller.ts b/packages/kilo-vscode/src/agent-manager/GitStatsPoller.ts index 188885497f0..11e513f32c5 100644 --- a/packages/kilo-vscode/src/agent-manager/GitStatsPoller.ts +++ b/packages/kilo-vscode/src/agent-manager/GitStatsPoller.ts @@ -4,7 +4,14 @@ import { remoteRef, type Worktree } from "./WorktreeStateManager" import type { GitOps } from "./GitOps" import type { Semaphore } from "./semaphore" import { findTrackedBranch } from "./project/paths" -import type { WorktreeDiffEntry } from "./types" +import { + GitStatsSnapshot, + refOID, + shortRef, + type DiffStats, + type RefSnapshot, + type GitStatsSource, +} from "./git-stats-snapshot" export interface WorktreeStats { worktreeId: string @@ -39,12 +46,9 @@ export interface WorktreePresenceResult { interface GitStatsPollerOptions { getWorktrees: () => Worktree[] getWorkspaceRoot: () => string | undefined - /** - * Compute diff summaries locally (in the extension host) rather than over - * HTTP to `kilo serve`. Keeps git spawning out of the Bun process, which - * leaks native memory on Windows (oven-sh/bun#18265). - */ - localDiff: (dir: string, base: string) => Promise + getHotWorktreeIds?: () => Set + /** Override the real Git source in scheduler and failure-path tests. */ + source?: GitStatsSource git: GitOps onStats: (stats: WorktreeStats[]) => void onLocalStats: (stats: LocalStats) => void @@ -54,6 +58,7 @@ interface GitStatsPollerOptions { /** Shared concurrency gate for child process spawning. */ semaphore?: Semaphore hiddenIntervalMs?: number + dormantIntervalMs?: number } export class GitStatsPoller { @@ -66,15 +71,22 @@ export class GitStatsPoller { private lastStats: Record = {} private readonly intervalMs: number private readonly hiddenIntervalMs: number + private readonly dormantIntervalMs: number private readonly git: GitOps + private readonly snapshots: GitStatsSource + private readonly cache = new Map() + private localCache: CachedStats | undefined private skipWorktreeIds = new Set() private visible = true private generation = 0 + private cursor = 0 constructor(private readonly options: GitStatsPollerOptions) { this.intervalMs = options.intervalMs ?? 5000 this.hiddenIntervalMs = options.hiddenIntervalMs ?? 60000 + this.dormantIntervalMs = options.dormantIntervalMs ?? 30000 this.git = options.git + this.snapshots = options.source ?? new GitStatsSnapshot(options.git) } setVisible(visible: boolean): void { @@ -129,9 +141,22 @@ export class GitStatsPoller { this.lastLocalHash = undefined this.lastLocalStats = undefined this.lastStats = {} + this.cache.clear() + this.localCache = undefined + this.cursor = 0 } - async snapshot(): Promise<{ worktrees: WorktreeStats[]; local?: LocalStats }> { + async snapshot(refresh = false): Promise<{ worktrees: WorktreeStats[]; local?: LocalStats }> { + if (refresh && !this.busy) { + this.busy = true + const refs = await this.fetchRefs() + await Promise.all([ + this.fetchWorktreeStats(true, this.generation, refs), + this.fetchLocalStats(this.generation, refs, true), + ]).finally(() => { + this.busy = false + }) + } return { worktrees: Object.values(this.lastStats), ...(this.lastLocalStats ? { local: this.lastLocalStats } : {}), @@ -164,10 +189,15 @@ export class GitStatsPoller { } private async fetch(generation = this.generation): Promise { - await Promise.all([this.fetchWorktreeStats(generation), this.fetchLocalStats(generation)]) + const refs = await this.fetchRefs() + await Promise.all([this.fetchWorktreeStats(false, generation, refs), this.fetchLocalStats(generation, refs)]) } - private async fetchWorktreeStats(generation = this.generation): Promise { + private async fetchWorktreeStats( + includeSkipped = false, + generation = this.generation, + refs?: RefSnapshot, + ): Promise { const worktrees = this.options.getWorktrees() if (worktrees.length === 0) return @@ -181,9 +211,13 @@ export class GitStatsPoller { const available = worktrees.filter((wt) => !missing.has(wt.id)) const ids = new Set(available.map((wt) => wt.id)) for (const id of Object.keys(this.lastStats)) { - if (!ids.has(id)) delete this.lastStats[id] + if (!ids.has(id)) { + delete this.lastStats[id] + this.cache.delete(id) + } } - const active = available.filter((wt) => !this.skipWorktreeIds.has(wt.id)) + const candidates = includeSkipped ? available : available.filter((wt) => !this.skipWorktreeIds.has(wt.id)) + const active = includeSkipped ? candidates : this.select(candidates) if (active.length === 0) { if (available.length > 0) return if (this.lastHash === "") return @@ -193,29 +227,7 @@ export class GitStatsPoller { return } - // localDiff runs in-process via GitOps.execGit() which already acquires - // the shared semaphore internally; same goes for aheadBehind via - // GitOps.raw(). Wrapping either again here would deadlock. - const stats = ( - await Promise.all( - active.map(async (wt) => { - try { - const base = remoteRef(wt) - const [diffs, ab] = await Promise.all([ - this.options.localDiff(wt.path, base), - this.git.aheadBehind(wt.path, base), - ]) - const files = diffs.length - const additions = diffs.reduce((sum, diff) => sum + diff.additions, 0) - const deletions = diffs.reduce((sum, diff) => sum + diff.deletions, 0) - return { worktreeId: wt.id, files, additions, deletions, ahead: ab.ahead, behind: ab.behind } - } catch (err) { - this.options.log(`Failed to fetch worktree stats for ${wt.branch} (${wt.path}):`, err) - return this.lastStats[wt.id] - } - }), - ) - ).filter((item): item is WorktreeStats => !!item) + const stats = await this.fetchOptimized(active, refs, includeSkipped) if (generation !== this.generation) return for (const item of stats) this.lastStats[item.worktreeId] = item @@ -229,6 +241,70 @@ export class GitStatsPoller { this.options.onStats(visible) } + private async fetchOptimized(worktrees: Worktree[], refs: RefSnapshot | undefined, refresh: boolean) { + const rows = await Promise.all( + worktrees.map(async (wt) => { + const base = remoteRef(wt) + const baseOID = refOID(refs, base) + try { + const status = await this.snapshots.status(wt.path) + const cached = this.cache.get(wt.id) + const same = + !refresh && + !!baseOID && + cached?.base === base && + cached.baseOID === baseOID && + cached.fingerprint === status.fingerprint + const diff = same ? cached.diff : await this.snapshots.diff(wt.path, base, status.untracked) + const ahead = + !refresh && baseOID && cached?.head === status.head && cached.baseOID === baseOID + ? cached.ahead + : await this.git.aheadBehind(wt.path, base) + return { wt, base, baseOID, status, diff, ahead } + } catch (err) { + this.options.log(`Failed to fetch worktree stats for ${wt.branch} (${wt.path}):`, err) + return { wt, prior: this.lastStats[wt.id] } + } + }), + ) + return rows + .map((row) => { + if ("prior" in row) return row.prior + const stats = { worktreeId: row.wt.id, ...row.diff, ...row.ahead } + this.cache.set(row.wt.id, { + base: row.base, + baseOID: row.baseOID, + head: row.status.head, + fingerprint: row.status.fingerprint, + diff: row.diff, + dirty: row.status.dirty, + clean: row.status.dirty ? 0 : (this.cache.get(row.wt.id)?.clean ?? 0) + 1, + ahead: row.ahead, + }) + return stats + }) + .filter((item): item is WorktreeStats => !!item) + } + + private select(worktrees: Worktree[]): Worktree[] { + if (!this.visible || worktrees.length === 0) return worktrees + const hot = this.options.getHotWorktreeIds?.() ?? new Set() + const selected = worktrees.filter((wt) => { + const cached = this.cache.get(wt.id) + return hot.has(wt.id) || !cached || cached.dirty || cached.clean < 2 + }) + const ids = new Set(selected.map((wt) => wt.id)) + const dormant = worktrees.filter((wt) => !ids.has(wt.id)) + if (dormant.length === 0) return selected + const ticks = Math.max(1, Math.ceil(this.dormantIntervalMs / this.intervalMs)) + const count = Math.max(1, Math.ceil(dormant.length / ticks)) + for (let i = 0; i < count; i++) { + selected.push(dormant[(this.cursor + i) % dormant.length]!) + } + this.cursor = (this.cursor + count) % dormant.length + return selected + } + private hash(stats: WorktreeStats[]): string { return stats .map( @@ -267,60 +343,95 @@ export class GitStatsPoller { return { worktrees: worktreeStatuses, degraded: false } } - private async fetchLocalStats(generation = this.generation): Promise { + private async fetchLocalStats(generation = this.generation, refs?: RefSnapshot, refresh = false): Promise { const root = this.options.getWorkspaceRoot() if (!root) return try { - const branch = await this.git.currentBranch(root) + const status = await this.snapshots.status(root) + const branch = status.branch if (!branch || branch === "HEAD") return - - const tracking = await this.git.resolveTrackingBranch(root, branch) - const base = tracking ?? (await this.git.resolveDefaultBranch(root, branch)) - - let files: number - let additions: number - let deletions: number - let ahead: number - let behind: number - try { - if (base) { - this.options.log(`Local stats: using localDiff with base=${base}`) - const [diffs, ab] = await Promise.all([this.options.localDiff(root, base), this.git.aheadBehind(root, base)]) - files = diffs.length - additions = diffs.reduce((sum, d) => sum + d.additions, 0) - deletions = diffs.reduce((sum, d) => sum + d.deletions, 0) - ahead = ab.ahead - behind = ab.behind - } else { - this.options.log(`Local stats: fallback to workingTreeStats (no base branch)`) - const wt = await this.git.workingTreeStats(root) - files = wt.files - additions = wt.additions - deletions = wt.deletions - ahead = 0 - behind = 0 - } - } catch (err) { + const stats = await this.local(root, branch, status, refs, refresh).catch((err) => { this.options.log("Failed to fetch local diff stats:", err) - if (this.lastLocalStats && this.lastLocalStats.branch === branch) return - return - } + return undefined + }) + if (!stats) return if (generation !== this.generation) return - const hash = `local:${branch}:${files}:${additions}:${deletions}:${ahead}:${behind}` + const hash = `local:${branch}:${stats.files}:${stats.additions}:${stats.deletions}:${stats.ahead}:${stats.behind}` if (hash === this.lastLocalHash) { this.options.log(`Local stats: unchanged (${hash})`) return } this.lastLocalHash = hash - this.options.log(`Local stats: emitting files=${files} +${additions} -${deletions} ↑${ahead} ↓${behind}`) - const stats: LocalStats = { branch, files, additions, deletions, ahead, behind } + this.options.log( + `Local stats: emitting files=${stats.files} +${stats.additions} -${stats.deletions} ↑${stats.ahead} ↓${stats.behind}`, + ) this.lastLocalStats = stats this.options.onLocalStats(stats) } catch (err) { this.options.log("Failed to fetch local stats:", err) } } + + private async local( + root: string, + branch: string, + status: Awaited>, + refs: RefSnapshot | undefined, + refresh: boolean, + ): Promise { + const trackingRef = refs?.upstreams.get(`refs/heads/${branch}`) + const tracking = trackingRef ? shortRef(trackingRef) : await this.git.resolveTrackingBranch(root, branch) + const base = tracking ?? (await this.git.resolveDefaultBranch(root, branch)) + if (!base) { + const stats = await this.git.workingTreeStats(root) + return { branch, ...stats, ahead: 0, behind: 0 } + } + const baseOID = refOID(refs, base) + const cached = this.localCache + const same = + !refresh && + !!baseOID && + cached?.base === base && + cached.baseOID === baseOID && + cached.fingerprint === status.fingerprint + const diff = same ? cached.diff : await this.snapshots.diff(root, base, status.untracked) + const ahead = + !refresh && baseOID && cached?.head === status.head && cached.baseOID === baseOID + ? cached.ahead + : await this.git.aheadBehind(root, base) + this.localCache = { + base, + baseOID, + head: status.head, + fingerprint: status.fingerprint, + diff, + dirty: status.dirty, + clean: status.dirty ? 0 : (cached?.clean ?? 0) + 1, + ahead, + } + return { branch, ...diff, ...ahead } + } + + private async fetchRefs(): Promise { + const root = this.options.getWorkspaceRoot() + if (!root) return undefined + return this.snapshots.refs(root).catch((err) => { + this.options.log("Failed to read project refs:", err) + return undefined + }) + } +} + +interface CachedStats { + base: string + baseOID?: string + head: string + fingerprint: string + diff: DiffStats + dirty: boolean + clean: number + ahead: { ahead: number; behind: number } } diff --git a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts index 155bd410bba..56ced904d57 100644 --- a/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts +++ b/packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts @@ -2,6 +2,7 @@ import type { ExecFileOptionsWithStringEncoding } from "child_process" import type { Worktree } from "./WorktreeStateManager" import type { PRStatus, PRCheck, PRComment, CheckStatus, AggregateCheckStatus, PRState, ReviewDecision } from "./types" import { execWithShellEnv } from "./shell-env" +import { execGhRead } from "./gh" import { classifyPRError } from "./git-import" import type { Semaphore } from "./semaphore" @@ -59,6 +60,14 @@ export class PRStatusPoller { return this.semaphore ? this.semaphore.run(invoke) : invoke() } + private gh( + args: string[], + options?: Omit, + ): Promise<{ stdout: string; stderr: string }> { + const invoke = () => execGhRead(args, options) + return this.semaphore ? this.semaphore.run(invoke) : invoke() + } + setEnabled(enabled: boolean): void { if (enabled) { if (this.active) return @@ -166,7 +175,7 @@ export class PRStatusPoller { return this.ghAvailable } try { - await this.shell("gh", ["--version"], { timeout: 5_000 }) + await this.gh(["--version"], { timeout: 5_000 }) this.ghAvailable = true } catch { this.ghAvailable = false @@ -311,7 +320,7 @@ export class PRStatusPoller { if (branch) args.push(branch) args.push("--json", PRStatusPoller.PR_JSON_FIELDS) - const { stdout } = await this.shell("gh", args, { cwd, timeout: 15_000 }) + const { stdout } = await this.gh(args, { cwd, timeout: 15_000 }) return parsePRResult(stdout) } catch (err) { const msg = err instanceof Error ? err.message : String(err) @@ -327,8 +336,7 @@ export class PRStatusPoller { const head = sha.trim() if (!head) return null - const { stdout } = await this.shell( - "gh", + const { stdout } = await this.gh( [ "pr", "list", @@ -369,8 +377,7 @@ export class PRStatusPoller { items: PRCheck[] }> { try { - const { stdout } = await this.shell( - "gh", + const { stdout } = await this.gh( ["pr", "checks", String(prNumber), "--json", "name,state,link,startedAt,completedAt"], { cwd, timeout: 15_000 }, ) @@ -407,7 +414,7 @@ export class PRStatusPoller { if (this.cachedRepo && this.cachedRepo.cwd === cwd) { return this.cachedRepo } - const { stdout } = await this.shell("gh", ["repo", "view", "--json", "owner,name"], { + const { stdout } = await this.gh(["repo", "view", "--json", "owner,name"], { cwd, timeout: 10_000, }) @@ -447,8 +454,7 @@ export class PRStatusPoller { } }` - const { stdout } = await this.shell( - "gh", + const { stdout } = await this.gh( [ "api", "graphql", diff --git a/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts b/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts index 32f2fb1b9f9..a28e157a4ea 100644 --- a/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts +++ b/packages/kilo-vscode/src/agent-manager/WorktreeManager.ts @@ -13,6 +13,7 @@ import simpleGit, { type SimpleGit } from "simple-git" import { generateBranchName, sanitizeBranchName } from "./branch-name" import { type GitOps, isKiloOwnedSshCommand, nonInteractiveEnv } from "./GitOps" import { execWithShellEnv } from "./shell-env" +import { execGhRead } from "./gh" import { markNoIndex } from "../util/spotlight" import { parsePRUrl, @@ -120,6 +121,8 @@ export class WorktreeManager { // Key: `${root}:${remote}:${branch}`, Value: timestamp when fetch was done private static fetchCache = new Map() private static readonly FETCH_CACHE_TTL = 60_000 // 1 minute + private static gitAvailable = false + private static lfsAvailable: boolean | undefined private withGitLock(fn: () => Promise): Promise { const key = this.root @@ -149,6 +152,13 @@ export class WorktreeManager { return this.withGitLock(() => this.createWorktreeImpl(params)) } + /** Start the remote base refresh before creation reaches the git mutex. */ + async prefetchBase(branch?: string): Promise { + await this.ensureMigrated() + const base = branch || (await this.defaultBranch()) + await this.withGitLock(() => this.refreshBase(base)) + } + async renameBranch(worktreePath: string, current: string, requested: string): Promise { await this.ensureMigrated() return this.withGitLock(() => this.renameBranchImpl(worktreePath, current, requested)) @@ -175,9 +185,12 @@ export class WorktreeManager { } private async ensureGitAvailable(): Promise { + if (WorktreeManager.gitAvailable) return try { await execWithShellEnv("git", ["--version"]) + WorktreeManager.gitAvailable = true } catch (error) { + WorktreeManager.gitAvailable = false if (error instanceof Error && "code" in error && (error as NodeJS.ErrnoException).code === "ENOENT") { throw new Error( "Git is not installed or not found in PATH. Please install Git (https://git-scm.com) and restart VS Code.", @@ -761,20 +774,14 @@ export class WorktreeManager { source: "remote", } } + WorktreeManager.fetchCache.delete(cacheKey) } // Either not cached or cache is stale - do the fetch. // Use non-interactive env to prevent SSH passphrase popups. onProgress?.("fetching", `Fetching ${remote}/${branch}...`) try { - // Only opt into simple-git's allowUnsafeSshCommand when the SSH command - // is the fixed value Kilo injects — never for an inherited one, which - // could be attacker-controlled. - const env = nonInteractiveEnv() - await simpleGit(this.root, { unsafe: { allowUnsafeSshCommand: isKiloOwnedSshCommand(env) } }) - .env(env) - .fetch(remote, branch, { "--quiet": null, "--no-tags": null }) - WorktreeManager.fetchCache.set(cacheKey, Date.now()) + await this.refreshBase(branch, remote) if (await this.refExistsLocally(`${remote}/${branch}`)) { return { ref: `${remote}/${branch}`, @@ -829,6 +836,23 @@ export class WorktreeManager { throw new Error(`Could not resolve start point for branch "${branch}"`) } + private async refreshBase(branch: string, requested?: string): Promise { + const remote = requested ?? (await this.resolveRemote()) + if (!remote) return + const key = `${this.root}:${remote}:${branch}` + const cached = WorktreeManager.fetchCache.get(key) + if (cached && Date.now() - cached < WorktreeManager.FETCH_CACHE_TTL) return + + // Only opt into simple-git's allowUnsafeSshCommand when the SSH command + // is the fixed value Kilo injects — never for an inherited one, which + // could be attacker-controlled. + const env = nonInteractiveEnv() + await simpleGit(this.root, { unsafe: { allowUnsafeSshCommand: isKiloOwnedSshCommand(env) } }) + .env(env) + .fetch(remote, branch, { "--quiet": null, "--no-tags": null }) + WorktreeManager.fetchCache.set(key, Date.now()) + } + /** * Resolve the primary remote name for this repo. * Uses `GitOps.resolveRemote` when available, otherwise checks for "origin". @@ -893,10 +917,13 @@ export class WorktreeManager { } async checkLfsAvailable(): Promise { + if (WorktreeManager.lfsAvailable) return true try { await execWithShellEnv("git", ["lfs", "version"], { cwd: this.root, timeout: 5000 }) + WorktreeManager.lfsAvailable = true return true } catch { + WorktreeManager.lfsAvailable = false // git-lfs not installed return false } @@ -1043,8 +1070,7 @@ export class WorktreeManager { private async fetchPRInfo(parsed: { owner: string; repo: string; number: number }): Promise { try { - const json = await this.exec( - "gh", + const json = await this.gh( [ "pr", "view", @@ -1100,6 +1126,11 @@ export class WorktreeManager { return stdout } + private async gh(args: string[], timeout = 120000): Promise { + const { stdout } = await execGhRead(args, { cwd: this.root, timeout }) + return stdout + } + private async gitExec(args: string[]): Promise { await this.exec("git", args) } diff --git a/packages/kilo-vscode/src/agent-manager/focus-panel.ts b/packages/kilo-vscode/src/agent-manager/focus-panel.ts new file mode 100644 index 00000000000..231ff5d49e5 --- /dev/null +++ b/packages/kilo-vscode/src/agent-manager/focus-panel.ts @@ -0,0 +1,8 @@ +import type { PanelContext } from "./host" + +export function focusPanelPrompt(panel: PanelContext, ready: Promise, active: Promise): void { + void Promise.all([ready, active]).then(([ready, active]) => { + if (!ready || !active) return + panel.postMessage({ type: "action", action: "focusInput" }) + }) +} diff --git a/packages/kilo-vscode/src/agent-manager/format-keybinding.ts b/packages/kilo-vscode/src/agent-manager/format-keybinding.ts index c6a4b05f150..9ac58043dcf 100644 --- a/packages/kilo-vscode/src/agent-manager/format-keybinding.ts +++ b/packages/kilo-vscode/src/agent-manager/format-keybinding.ts @@ -71,6 +71,9 @@ export function buildKeybindingMap( if (!bindings.runScript) bindings.runScript = formatKeybinding(mac ? "cmd+e" : "ctrl+e", mac) if (!bindings.toggleDiff) bindings.toggleDiff = formatKeybinding(mac ? "cmd+d" : "ctrl+d", mac) if (!bindings.showShortcuts) bindings.showShortcuts = formatKeybinding(mac ? "cmd+shift+/" : "ctrl+shift+/", mac) + if (!bindings.previousTerminal) + bindings.previousTerminal = formatKeybinding(mac ? "cmd+shift+[" : "ctrl+shift+[", mac) + if (!bindings.nextTerminal) bindings.nextTerminal = formatKeybinding(mac ? "cmd+shift+]" : "ctrl+shift+]", mac) return bindings } diff --git a/packages/kilo-vscode/src/agent-manager/gh.ts b/packages/kilo-vscode/src/agent-manager/gh.ts new file mode 100644 index 00000000000..5794f2f2b91 --- /dev/null +++ b/packages/kilo-vscode/src/agent-manager/gh.ts @@ -0,0 +1,18 @@ +import type { ExecFileOptionsWithStringEncoding } from "child_process" +import { execWithShellEnv } from "./shell-env" + +function env(options?: Omit): NodeJS.ProcessEnv { + const result = options?.env ? { ...options.env } : { ...process.env } + const tz = Object.keys(result).find((key) => key.toLowerCase() === "tz") + if (!tz) result.TZ = "UTC" + return result +} + +/** Run read-only gh queries without tzutil console windows flashing on Windows. */ +export function execGhRead( + args: string[], + options?: Omit, +): Promise<{ stdout: string; stderr: string }> { + if (process.platform !== "win32") return execWithShellEnv("gh", args, options) + return execWithShellEnv("gh", args, { ...options, env: env(options) }) +} diff --git a/packages/kilo-vscode/src/agent-manager/git-stats-snapshot.ts b/packages/kilo-vscode/src/agent-manager/git-stats-snapshot.ts new file mode 100644 index 00000000000..bfe2f02b8ed --- /dev/null +++ b/packages/kilo-vscode/src/agent-manager/git-stats-snapshot.ts @@ -0,0 +1,222 @@ +import { createHash } from "crypto" +import * as fs from "fs/promises" +import { binaryFile } from "../diff/shared/binary" +import { resolveInside } from "../diff/shared/path" +import type { GitOps } from "./GitOps" + +const MAX_BYTES = 1_000_000 + +export interface DiffStats { + files: number + additions: number + deletions: number +} + +export interface StatusSnapshot { + branch: string + dirty: boolean + head: string + fingerprint: string + untracked: string[] +} + +export interface RefSnapshot { + oids: Map + upstreams: Map +} + +export interface GitStatsSource { + status(dir: string): Promise + refs(root: string): Promise + diff(dir: string, base: string, untracked: string[]): Promise +} + +interface PathState { + file: string + missing: boolean +} + +function tail(record: string, fields: number): string | undefined { + let offset = 0 + for (let i = 0; i < fields; i++) { + const next = record.indexOf(" ", offset) + if (next === -1) return undefined + offset = next + 1 + } + return record.slice(offset) +} + +function records(raw: Buffer): { branch: string; head: string; paths: PathState[]; untracked: string[] } { + const items = raw.toString("utf8").split("\0") + const paths: PathState[] = [] + const untracked: string[] = [] + let branch = "" + let head = "" + + for (let i = 0; i < items.length; i++) { + const item = items[i] + if (!item) continue + if (item.startsWith("# branch.oid ")) { + head = item.slice(13) + continue + } + if (item.startsWith("# branch.head ")) { + const value = item.slice(14) + branch = value === "(detached)" ? "HEAD" : value + continue + } + if (item.startsWith("? ")) { + const file = item.slice(2) + untracked.push(file) + paths.push({ file, missing: false }) + continue + } + if (item.startsWith("1 ")) { + const file = tail(item, 8) + if (file) paths.push({ file, missing: item.slice(2, 4).includes("D") }) + continue + } + if (item.startsWith("2 ")) { + const file = tail(item, 9) + if (file) paths.push({ file, missing: false }) + i++ + continue + } + if (item.startsWith("u ")) { + const file = tail(item, 10) + if (file) paths.push({ file, missing: true }) + } + } + + return { branch, head, paths, untracked } +} + +async function fingerprint(dir: string, raw: Buffer, paths: PathState[]): Promise { + const hash = createHash("sha256").update(raw) + const unique = new Map(paths.map((item) => [item.file, item])) + const files = [...unique.values()].sort((a, b) => a.file.localeCompare(b.file)) + + for (const item of files) { + const full = resolveInside(dir, item.file) + if (!full) return undefined + const stat = await fs.lstat(full, { bigint: true }).catch(() => undefined) + if (!stat) { + if (!item.missing) return undefined + hash.update(`\0${item.file}\0missing`) + continue + } + hash.update(`\0${item.file}\0${stat.dev}:${stat.ino}:${stat.mode}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`) + } + return hash.digest("hex") +} + +function numstat(raw: Buffer): DiffStats { + const result = { files: 0, additions: 0, deletions: 0 } + for (const item of raw.toString("utf8").split("\0")) { + if (!item) continue + const first = item.indexOf("\t") + const second = item.indexOf("\t", first + 1) + if (first === -1 || second === -1) continue + result.files++ + const additions = item.slice(0, first) + const deletions = item.slice(first + 1, second) + if (additions !== "-") result.additions += parseInt(additions, 10) || 0 + if (deletions !== "-") result.deletions += parseInt(deletions, 10) || 0 + } + return result +} + +async function lines(file: string): Promise { + const stat = await fs.lstat(file).catch(() => undefined) + if (!stat || stat.size === 0 || stat.size > MAX_BYTES) return 0 + if (await binaryFile(file)) return 0 + const content = stat.isSymbolicLink() + ? await fs.readlink(file).catch(() => "") + : await fs.readFile(file, "utf8").catch(() => "") + if (!content) return 0 + return content.endsWith("\n") ? content.split("\n").length - 1 : content.split("\n").length +} + +export function refOID(refs: RefSnapshot | undefined, ref: string): string | undefined { + if (!refs) return undefined + if (ref.startsWith("refs/")) return refs.oids.get(ref) + return refs.oids.get(`refs/remotes/${ref}`) ?? refs.oids.get(`refs/heads/${ref}`) ?? refs.oids.get(ref) +} + +export function shortRef(ref: string): string { + if (ref.startsWith("refs/remotes/")) return ref.slice(13) + if (ref.startsWith("refs/heads/")) return ref.slice(11) + return ref +} + +export class GitStatsSnapshot implements GitStatsSource { + constructor(private readonly git: GitOps) {} + + async status(dir: string): Promise { + const result = await this.git.execGitBuffer( + [ + "--no-optional-locks", + "status", + "--porcelain=v2", + "--branch", + "-z", + "--no-ahead-behind", + "--untracked-files=all", + "--no-renames", + ], + dir, + ) + if (result.code !== 0) throw new Error(result.stderr.trim() || "git status failed") + const parsed = records(result.stdout) + if (!parsed.head || !parsed.branch) throw new Error("git status returned incomplete branch data") + const stamp = await fingerprint(dir, result.stdout, parsed.paths) + if (!stamp) throw new Error("worktree changed while status was being sampled") + return { + branch: parsed.branch, + dirty: parsed.paths.length > 0, + head: parsed.head, + fingerprint: stamp, + untracked: parsed.untracked, + } + } + + async refs(root: string): Promise { + const result = await this.git.execGitBuffer( + ["for-each-ref", "--format=%(refname)%00%(objectname)%00%(upstream)%00", "refs/heads", "refs/remotes"], + root, + ) + if (result.code !== 0) throw new Error(result.stderr.trim() || "git for-each-ref failed") + const oids = new Map() + const upstreams = new Map() + for (const line of result.stdout.toString("utf8").split("\n")) { + if (!line) continue + const [ref, oid, upstream] = line.split("\0") + if (!ref || !oid) continue + oids.set(ref, oid) + if (upstream) upstreams.set(ref, upstream) + } + return { oids, upstreams } + } + + async diff(dir: string, base: string, untracked: string[]): Promise { + const ancestor = await this.git.execGit(["merge-base", "HEAD", base], dir) + if (ancestor.code !== 0) throw new Error(ancestor.stderr.trim() || "git merge-base failed") + const result = await this.git.execGitBuffer( + ["-c", "core.quotepath=false", "diff", "--numstat", "-z", "--no-renames", ancestor.stdout.trim()], + dir, + ) + if (result.code !== 0) throw new Error(result.stderr.trim() || "git diff failed") + const stats = numstat(result.stdout) + const counts = await Promise.all( + untracked.map(async (file) => { + const full = resolveInside(dir, file) + return full ? lines(full) : 0 + }), + ) + return { + files: stats.files + untracked.length, + additions: stats.additions + counts.reduce((sum, count) => sum + count, 0), + deletions: stats.deletions, + } + } +} diff --git a/packages/kilo-vscode/src/agent-manager/orchestration-bridge.ts b/packages/kilo-vscode/src/agent-manager/orchestration-bridge.ts index e36948c449a..e4e5d05360d 100644 --- a/packages/kilo-vscode/src/agent-manager/orchestration-bridge.ts +++ b/packages/kilo-vscode/src/agent-manager/orchestration-bridge.ts @@ -41,14 +41,15 @@ interface Failure { } interface Options { - root(): string | undefined - ready(): Promise - state(): WorktreeStateManager | undefined - stats(): Promise<{ worktrees: WorktreeStats[]; local?: LocalStats }> - prs(): Map - push(): void - managed(sessionID: string): boolean - close(sessionID: string): Promise + root(directory?: string): string | undefined + ready(directory?: string): Promise + state(directory?: string): WorktreeStateManager | undefined + stats(directory?: string): Promise<{ worktrees: WorktreeStats[]; local?: LocalStats }> + prs(directory?: string): Map + push(directory?: string): void + managed(sessionID: string, directory?: string): boolean + close(sessionID: string, directory?: string): Promise + directories?(): string[] log(...args: unknown[]): void } @@ -108,6 +109,7 @@ export class AgentManagerOrchestrationBridge { }) }) this.unsubscribeDirectories = connection.registerDirectoryProvider(() => { + if (this.options.directories) return this.options.directories() const root = this.options.root() const dirs = this.options @@ -181,8 +183,8 @@ export class AgentManagerOrchestrationBridge { } private async admit(request: Request, directory: string): Promise { - const state = await this.options.ready() - const root = this.options.root() + const state = await this.options.ready(directory) + const root = this.options.root(directory) if (this.disposed || this.settled.has(request.id)) return if (!state || !root) { const accepted = await this.reject(request.id, directory, { @@ -232,7 +234,7 @@ export class AgentManagerOrchestrationBridge { private async run(request: Request, origin: Origin, active: Active): Promise { try { - const outcome = this.outcomes.get(request.id) ?? (await this.execute(request, active)) + const outcome = this.outcomes.get(request.id) ?? (await this.execute(request, origin, active)) if (!outcome || this.disposed || active.cancelled) return this.rememberOutcome(request.id, outcome) const accepted = @@ -248,10 +250,10 @@ export class AgentManagerOrchestrationBridge { } } - private async execute(request: Request, active: Active): Promise { + private async execute(request: Request, origin: Origin, active: Active): Promise { try { - const state = await this.options.ready() - const root = this.options.root() + const state = await this.options.ready(origin.directory) + const root = this.options.root(origin.directory) if (!state || !root) throw new OrchestrationError("workspace_unavailable", "Agent Manager requires an open workspace") if (this.disposed || active.cancelled) return @@ -260,7 +262,7 @@ export class AgentManagerOrchestrationBridge { // Git stats are refreshed by the poller independently. A forced refresh // here can spawn one diff/ahead-behind pair per worktree and exceed the // host request timeout before the overview can return its IDs. - const stats = await this.options.stats() + const stats = await this.options.stats(origin.directory) if (this.disposed || active.cancelled) return const result = await overview({ client, @@ -269,7 +271,7 @@ export class AgentManagerOrchestrationBridge { titles: this.titles, filter: request.filter, stats, - prs: this.options.prs(), + prs: this.options.prs(origin.directory), }) return { result: { operation: "overview", overview: result } } } @@ -288,7 +290,7 @@ export class AgentManagerOrchestrationBridge { } if (request.operation === "move") { move({ state, sessionID: request.targetSessionID, sectionID: request.sectionID }) - this.options.push() + this.options.push(origin.directory) if (this.disposed || active.cancelled) return return { result: { @@ -299,10 +301,10 @@ export class AgentManagerOrchestrationBridge { }, } } - if (!this.options.managed(request.targetSessionID)) { + if (!this.options.managed(request.targetSessionID, origin.directory)) { throw new OrchestrationError("unknown_session", "The session is not managed by this Agent Manager workspace") } - await this.options.close(request.targetSessionID) + await this.options.close(request.targetSessionID, origin.directory) if (this.disposed || active.cancelled) return return { result: { operation: "stop", sessionID: request.targetSessionID, stopped: true } } } catch (error) { diff --git a/packages/kilo-vscode/src/agent-manager/orchestration-domain.ts b/packages/kilo-vscode/src/agent-manager/orchestration-domain.ts index b8b08282716..f1b7849c329 100644 --- a/packages/kilo-vscode/src/agent-manager/orchestration-domain.ts +++ b/packages/kilo-vscode/src/agent-manager/orchestration-domain.ts @@ -319,6 +319,7 @@ export async function prompt(input: { text: string messageID: string signal?: AbortSignal + idleTimeoutMs?: number }): Promise { if (input.signal?.aborted) return const managed = input.state.getSession(input.sessionID) @@ -342,15 +343,7 @@ export async function prompt(input: { if (!(await sameManagedDirectory(response.data.directory, dir))) { throw new OrchestrationError("cross_workspace", "The managed session belongs to a different workspace directory") } - const status = await input.client.session.status({ directory: dir }) - if (status.error) throw new OrchestrationError("host_error", "The managed session status could not be read") - const activity = status.data?.[input.sessionID]?.type ?? "idle" - if (activity !== "idle") { - throw new OrchestrationError( - "unavailable_session", - `The managed session is ${activity}; only idle sessions can be prompted`, - ) - } + await waitForIdle(input.client, dir, input.sessionID, input.signal, input.idleTimeoutMs ?? 30_000) if (input.signal?.aborted) return await input.client.session.promptAsync( { @@ -364,6 +357,29 @@ export async function prompt(input: { ) } +async function waitForIdle( + client: KiloClient, + directory: string, + sessionID: string, + signal: AbortSignal | undefined, + timeout: number, + start = Date.now(), +): Promise { + if (signal?.aborted) return + const status = await client.session.status({ directory }) + if (status.error) throw new OrchestrationError("host_error", "The managed session status could not be read") + const activity = status.data?.[sessionID]?.type ?? "idle" + if (activity === "idle") return + if (Date.now() - start >= timeout) { + throw new OrchestrationError( + "unavailable_session", + `The managed session is still ${activity}; only idle sessions can be prompted`, + ) + } + await new Promise((resolve) => setTimeout(resolve, 250)) + return waitForIdle(client, directory, sessionID, signal, timeout, start) +} + export function move(input: { state: WorktreeStateManager; sessionID: string; sectionID: string | null }): void { const session = input.state.getSession(input.sessionID) if (!session) diff --git a/packages/kilo-vscode/src/agent-manager/orchestration-setup.ts b/packages/kilo-vscode/src/agent-manager/orchestration-setup.ts new file mode 100644 index 00000000000..1f24b314ead --- /dev/null +++ b/packages/kilo-vscode/src/agent-manager/orchestration-setup.ts @@ -0,0 +1,78 @@ +import type { KiloConnectionService } from "../services/cli-backend/connection-service" +import { AgentManagerOrchestrationBridge } from "./orchestration-bridge" +import type { ProjectContexts } from "./project/contexts" +import type { ProjectContext } from "./project/context" +import type { ProjectScope } from "./project/scope" +import type { WorktreeStateManager } from "./WorktreeStateManager" +import type { WorktreeStats, LocalStats } from "./GitStatsPoller" +import type { PRStatus } from "./types" +import { initContextState } from "./project/init" + +export interface OrchestrationBridgeDeps { + connectionService: KiloConnectionService + contexts: ProjectContexts + projectScope: ProjectScope + getRoot: () => string | undefined + getState: () => WorktreeStateManager | undefined + getStateReady: () => Promise | undefined + initStateReady: () => Promise + getStats: () => Promise<{ worktrees: WorktreeStats[]; local?: LocalStats }> + getPrs: () => Map + pushState: (ctx?: ProjectContext) => void + hasPanelSession: (id: string) => boolean + closeSession: (id: string) => Promise + postSessionClosed: (id: string) => void + log: (...args: unknown[]) => void +} + +export function createOrchestrationBridge(deps: OrchestrationBridgeDeps): AgentManagerOrchestrationBridge { + return new AgentManagerOrchestrationBridge(deps.connectionService, { + root: (dir) => (dir ? deps.contexts.byDirectory(dir)?.root : undefined) ?? deps.getRoot(), + state: (dir) => (dir ? deps.contexts.byDirectory(dir)?.peekState() : undefined) ?? deps.getState(), + ready: async (dir) => { + const ctx = dir ? deps.contexts.byDirectory(dir) : undefined + if (ctx && ctx.id !== deps.contexts.active()?.id) { + await initContextState(ctx, (...args) => deps.log(...args)) + return ctx.stateManager() + } + const ready = deps.getStateReady() ?? deps.initStateReady() + await ready + return deps.getState() + }, + stats: () => deps.getStats(), + prs: () => deps.getPrs(), + push: (dir) => { + const ctx = dir ? deps.contexts.byDirectory(dir) : undefined + deps.pushState(ctx) + }, + managed: (id, dir) => { + const ctx = dir ? deps.contexts.byDirectory(dir) : undefined + if (ctx) return ctx.hasLiveSession(id) || !!ctx.peekState()?.getSession(id) + return deps.hasPanelSession(id) || !!deps.getState()?.getSession(id) + }, + close: async (id, dir) => { + const ctx = dir ? deps.contexts.byDirectory(dir) : undefined + if (ctx) { + await deps.projectScope.run(ctx, () => deps.closeSession(id)) + } else { + await deps.closeSession(id) + } + deps.postSessionClosed(id) + }, + directories: () => { + const all: string[] = [] + for (const ctx of deps.contexts.values()) { + all.push(ctx.root) + for (const wt of ctx.peekState()?.getWorktrees() ?? []) { + if (wt.path) all.push(wt.path) + } + } + if (all.length === 0) { + const root = deps.getRoot() + if (root) all.push(root) + } + return all + }, + log: (...args) => deps.log(...args), + }) +} diff --git a/packages/kilo-vscode/src/agent-manager/project/contexts.ts b/packages/kilo-vscode/src/agent-manager/project/contexts.ts index 95a16d7050a..310ee240472 100644 --- a/packages/kilo-vscode/src/agent-manager/project/contexts.ts +++ b/packages/kilo-vscode/src/agent-manager/project/contexts.ts @@ -100,6 +100,10 @@ export class ProjectContexts { return this.contexts.get(id) } + values(): IterableIterator { + return this.contexts.values() + } + /** The context that owns a directory: its root or one of its worktree paths. */ byDirectory(dir: string): ProjectContext | undefined { for (const ctx of this.contexts.values()) { diff --git a/packages/kilo-vscode/src/agent-manager/project/pollers.ts b/packages/kilo-vscode/src/agent-manager/project/pollers.ts index 04bc8f023b6..56384b72063 100644 --- a/packages/kilo-vscode/src/agent-manager/project/pollers.ts +++ b/packages/kilo-vscode/src/agent-manager/project/pollers.ts @@ -13,13 +13,12 @@ import type { GitOps } from "../GitOps" import { GitStatsPoller, type LocalStats, type WorktreePresenceResult, type WorktreeStats } from "../GitStatsPoller" -import { diffSummary } from "../local-diff" import { PRStatusBridge } from "../pr-status-bridge" import type { PRStatus } from "../types" import type { ProjectContext } from "./context" import type { ProjectContexts } from "./contexts" import type { Semaphore } from "../semaphore" -import type { AgentManagerOutMessage, WorktreeDiffEntry } from "../types" +import type { AgentManagerOutMessage } from "../types" import type { WorktreeStateManager } from "../WorktreeStateManager" export interface PollerPair { @@ -37,20 +36,31 @@ type StatsMessage = Extract Promise + hot?: () => Set post: (msg: StatsOutMessage) => void openExternal: (url: string) => void visible: () => boolean log: (...args: unknown[]) => void } +function hot(state: WorktreeStateManager | undefined): Set { + const result = new Set() + const target = state?.getActiveTarget() + if (target?.kind === "worktree") result.add(target.worktreeId) + if (target?.kind === "session") { + const id = state?.getSession(target.sessionId)?.worktreeId + if (id) result.add(id) + } + return result +} + /** Create the real poller pair for one project context. */ function createPollerPair(ctx: ProjectContext, deps: PollerDeps): PollerPair { const state = () => ctx.peekState() const stats = new GitStatsPoller({ getWorktrees: () => state()?.getWorktrees() ?? [], getWorkspaceRoot: () => ctx.root, - localDiff: deps.localDiff, + getHotWorktreeIds: deps.hot ?? (() => hot(state())), git: deps.git, semaphore: deps.semaphore, log: deps.log, @@ -135,12 +145,12 @@ export function createPollers(opts: { presence: (result: WorktreePresenceResult) => void openExternal: (url: string) => void log: (...args: unknown[]) => void + hot?: () => Set }): { stats: GitStatsPoller; pr: PRStatusBridge; projects: ProjectPollers } { - const localDiff = (dir: string, base: string) => diffSummary(opts.git, dir, base, opts.log) const stats = new GitStatsPoller({ getWorktrees: () => opts.state()?.getWorktrees() ?? [], getWorkspaceRoot: opts.root, - localDiff, + getHotWorktreeIds: opts.hot ?? (() => hot(opts.state())), semaphore: opts.semaphore, onStats: (stats) => { const msg = { type: "agentManager.worktreeStats" as const, projectId: opts.activeId(), stats } @@ -169,7 +179,7 @@ export function createPollers(opts: { const projects = new ProjectPollers({ git: opts.git, semaphore: opts.semaphore, - localDiff, + hot: opts.hot, post: opts.post, openExternal: opts.openExternal, visible: opts.visible, diff --git a/packages/kilo-vscode/src/agent-manager/provider-multi-version.ts b/packages/kilo-vscode/src/agent-manager/provider-multi-version.ts index 41cc02049eb..59b6aba1afd 100644 --- a/packages/kilo-vscode/src/agent-manager/provider-multi-version.ts +++ b/packages/kilo-vscode/src/agent-manager/provider-multi-version.ts @@ -6,6 +6,9 @@ import { versionedName } from "./branch-name" import { resolveVersionModels, buildInitialMessages, type CreatedVersion } from "./multi-version" import { ensureSandbox } from "./sandbox-bootstrap" import type { LifecycleHost } from "./provider-lifecycle" +import { Semaphore } from "./semaphore" + +const PROVISION_CONCURRENCY = 2 /** * Multi-version creation needs the lifecycle capabilities plus three provider @@ -19,8 +22,8 @@ export interface MultiVersionHost extends LifecycleHost { } /** - * Create N worktrees with one session each (optionally one model per version), - * then fan the initial prompt out to every created session. State is reached + * Create N worktrees, provision their sessions with bounded concurrency, then + * send each initial prompt as soon as that session is ready. State is reached * through the project context; everything else goes through the host. */ export async function createMultiVersion( @@ -57,26 +60,49 @@ export async function createMultiVersion( groupId, }) - // Phase 1: Create all worktrees + sessions first + // Phase 1: finish every shared-repository Git mutation before setup scripts + // or agents can run their own Git commands in the new worktrees. const created: CreatedVersion[] = [] - for (let i = 0; i < versions; i++) { - const version = await createVersion(ctx, host, { - index: i, - versions, - groupId, - baseBranch, - branchName, - worktreeName, + const specs = Array.from({ length: versions }, (_, index) => ({ + index, + versions, + groupId, + baseBranch, + branchName, + worktreeName, + models, + providerID, + modelID, + sandbox: msg.sandbox, + })) + const prepared: PreparedVersion[] = [] + for (const spec of specs) { + const version = await prepareVersion(host, spec) + if (version) prepared.push(version) + } + + // Phase 2: Git creation is complete, so independent setup/session pipelines + // can overlap without racing the shared worktree metadata mutation. + const provision = async (version: PreparedVersion) => { + const ready = await provisionVersion(ctx, host, version) + if (!ready) return + created.push(ready) + + sendInitialPrompt( + host, + ctx.id, + ready, models, - providerID, - modelID, - sandbox: msg.sandbox, - }) - if (!version) continue - created.push(version) + { providerID, modelID }, + { + text, + agent, + variant: msg.variant, + files, + }, + ) - // Update progress host.post({ type: "agentManager.multiVersionProgress", projectId: ctx.id, @@ -87,15 +113,8 @@ export async function createMultiVersion( }) } - // Phase 2: Send the initial prompt to all sessions, or clear busy state if no text. - await sendInitialPrompts( - host, - ctx.id, - created, - models, - { providerID, modelID }, - { text, agent, variant: msg.variant, files }, - ) + const gate = new Semaphore(PROVISION_CONCURRENCY) + await Promise.all(prepared.map((version) => gate.run(() => provision(version)))) // Notify completion host.post({ @@ -128,12 +147,13 @@ interface VersionSpec { sandbox: boolean | undefined } -/** Create one version's worktree + session and wire it into state and the webview. */ -async function createVersion( - ctx: ProjectContext, - host: MultiVersionHost, - spec: VersionSpec, -): Promise { +interface PreparedVersion { + spec: VersionSpec + wt: NonNullable>> +} + +/** Create one version's worktree while the shared-repository Git barrier is active. */ +async function prepareVersion(host: MultiVersionHost, spec: VersionSpec): Promise { host.log(`Creating worktree ${spec.index + 1}/${spec.versions}`) const version = versionedName(spec.branchName || spec.worktreeName, spec.index, spec.versions) @@ -148,6 +168,16 @@ async function createVersion( host.log(`Failed to create worktree for version ${spec.index + 1}`) return null } + return { spec, wt } +} + +/** Set up one prepared worktree, create its session, and expose it to the UI. */ +async function provisionVersion( + ctx: ProjectContext, + host: MultiVersionHost, + prepared: PreparedVersion, +): Promise { + const { spec, wt } = prepared await host.runSetup(wt.result.path, wt.result.branch, wt.worktree.id) @@ -240,11 +270,11 @@ async function reconcileSandbox( } } -/** Fan the initial prompt out to every created session, throttled between sends. */ -async function sendInitialPrompts( +/** Send one version's initial prompt as soon as its session is ready. */ +function sendInitialPrompt( host: MultiVersionHost, projectId: string, - created: CreatedVersion[], + created: CreatedVersion, models: VersionSpec["models"], resolved: { providerID: string | undefined; modelID: string | undefined }, input: { @@ -253,22 +283,16 @@ async function sendInitialPrompts( variant: string | undefined files: Extract["files"] }, -): Promise { - const messages = buildInitialMessages(created, models, resolved, input.text, input.agent, input.variant, input.files) - for (let i = 0; i < messages.length; i++) { - const msg = messages[i]! - if (input.text) { - host.log(`Sending initial message to version ${i + 1} (session=${msg.sessionId})`) - host.promptName({ - sessionID: msg.sessionId, - text: input.text, - providerID: msg.providerID, - modelID: msg.modelID, - }) - } - host.post({ type: "agentManager.sendInitialMessage", projectId, ...msg }) - if (input.text && i < messages.length - 1) { - await new Promise((resolve) => setTimeout(resolve, 300)) - } +): void { + const msg = buildInitialMessages([created], models, resolved, input.text, input.agent, input.variant, input.files)[0]! + if (input.text) { + host.log(`Sending initial message to version ${created.versionIndex + 1} (session=${msg.sessionId})`) + host.promptName({ + sessionID: msg.sessionId, + text: input.text, + providerID: msg.providerID, + modelID: msg.modelID, + }) } + host.post({ type: "agentManager.sendInitialMessage", projectId, ...msg }) } diff --git a/packages/kilo-vscode/src/agent-manager/terminal-manager.ts b/packages/kilo-vscode/src/agent-manager/terminal-manager.ts index 24f916542e5..26677dc2d3e 100644 --- a/packages/kilo-vscode/src/agent-manager/terminal-manager.ts +++ b/packages/kilo-vscode/src/agent-manager/terminal-manager.ts @@ -15,6 +15,8 @@ import type { KiloClient } from "@kilocode/sdk/v2/client" +const env = { KILO_UNICODE_LOGO: "0" } + /** * Everything the manager needs from the surrounding AgentManagerProvider. * @@ -49,6 +51,7 @@ interface Entry { export class TerminalManager { private readonly entries = new Map() private readonly restarts = new Map>() + private readonly pending = new Map() constructor(private readonly deps: TerminalManagerDeps) {} @@ -65,12 +68,23 @@ export class TerminalManager { worktreeId: string | null cwd: string title: string + cols?: number + rows?: number }): Promise<{ terminalId: string; worktreeId: string | null; title: string; wsUrl: string }> { + const initial = + this.pending.get(params.terminalId) ?? + (params.cols !== undefined && params.rows !== undefined ? { cols: params.cols, rows: params.rows } : undefined) + this.pending.delete(params.terminalId) + const client = this.deps.getClient() const { data, error } = await client.pty.create({ directory: params.cwd, cwd: params.cwd, title: params.title, + // xterm's DOM renderer cannot draw the Unicode sextant glyphs used by + // Kilo's modern wordmark, so use the compatible logo in embedded tabs. + env, + size: initial, }) if (error || !data) { const err = error instanceof Error ? error.message : String(error ?? "unknown error") @@ -84,15 +98,39 @@ export class TerminalManager { title: data.title ?? params.title, } this.entries.set(params.terminalId, entry) + // If a resize arrived while pty.create was in flight that differed from `initial`, apply it now. + const latest = this.pending.get(params.terminalId) + if (latest && (latest.cols !== initial?.cols || latest.rows !== initial?.rows)) { + this.pending.delete(params.terminalId) + const { error: resizeErr } = await client.pty.update({ + directory: entry.cwd, + ptyID: entry.ptyID, + size: latest, + }) + if (resizeErr) { + const err = resizeErr instanceof Error ? resizeErr.message : String(resizeErr) + this.deps.log(`Initial terminal resize failed (${params.terminalId}): ${err}`) + } + } const wsUrl = this.deps.buildWsUrl(entry.ptyID, entry.cwd) this.deps.log(`Terminal created: ${params.terminalId} -> pty ${entry.ptyID} cwd=${entry.cwd}`) return { terminalId: params.terminalId, worktreeId: entry.worktreeId, title: entry.title, wsUrl } } - /** Forward a resize event to the backend PTY. Missing terminals are a no-op. */ + /** + * Forward a resize event to the backend PTY. + * + * If the terminal creation is still in flight, dimensions are queued into + * `pending` and applied during PTY initialization before the WebSocket + * URL is returned. + */ async resize(terminalId: string, cols: number, rows: number): Promise { const entry = this.entries.get(terminalId) - if (!entry) return + if (!entry) { + this.pending.set(terminalId, { cols, rows }) + return + } + this.pending.delete(terminalId) const client = this.deps.getClient() const { error } = await client.pty.update({ directory: entry.cwd, @@ -121,6 +159,7 @@ export class TerminalManager { * failed delete would be silently logged as a successful close and * the server-side PTY would linger until `kilo serve` exits. */ async close(terminalId: string): Promise { + this.pending.delete(terminalId) const entry = this.entries.get(terminalId) if (!entry) return this.entries.delete(terminalId) @@ -182,6 +221,7 @@ export class TerminalManager { * is sampled mid-shutdown. */ async dispose(): Promise { + this.pending.clear() const snapshot = [...this.entries.values()] if (snapshot.length === 0) { this.entries.clear() @@ -237,6 +277,7 @@ export class TerminalManager { directory: entry.cwd, cwd: entry.cwd, title: entry.title, + env, }) const info = created.data if (created.error || !info) diff --git a/packages/kilo-vscode/src/agent-manager/terminal-routing.ts b/packages/kilo-vscode/src/agent-manager/terminal-routing.ts index 95fe5c86328..7a4a74520f8 100644 --- a/packages/kilo-vscode/src/agent-manager/terminal-routing.ts +++ b/packages/kilo-vscode/src/agent-manager/terminal-routing.ts @@ -88,7 +88,7 @@ export class TerminalRouter { handle(m: AgentManagerInMessage): boolean { if (!isTerminalMessage(m)) return false if (m.type === "agentManager.terminal.create") { - void this.handleCreate(m.createId, m.placement, m.worktreeId) + void this.handleCreate(m.createId, m.placement, m.worktreeId, m.cols, m.rows) return true } if (m.type === "agentManager.terminal.close") { @@ -132,7 +132,13 @@ export class TerminalRouter { return manager.dispose() } - private async handleCreate(createId: string, placement: TerminalPlacement, worktreeId: string | null): Promise { + private async handleCreate( + createId: string, + placement: TerminalPlacement, + worktreeId: string | null, + cols?: number, + rows?: number, + ): Promise { const generation = this.generation const manager = this.manager const cwd = this.resolveCwd(worktreeId) @@ -155,7 +161,7 @@ export class TerminalRouter { // Join the shared backend connection instead of racing its synchronous // client accessor when this is the first Kilo action in the window. await this.deps.getClientAsync() - const created = await manager.create({ terminalId: createId, worktreeId, cwd, title }) + const created = await manager.create({ terminalId: createId, worktreeId, cwd, title, cols, rows }) if (generation !== this.generation) { await manager.close(created.terminalId) return diff --git a/packages/kilo-vscode/src/agent-manager/tool-project.ts b/packages/kilo-vscode/src/agent-manager/tool-project.ts new file mode 100644 index 00000000000..f6959107bd8 --- /dev/null +++ b/packages/kilo-vscode/src/agent-manager/tool-project.ts @@ -0,0 +1,32 @@ +import type { ToolRequest } from "./tool-start" +import { parseToolRequest } from "./tool-start" + +export function routeToolRequest( + input: T, + directory: string | undefined, + deps: { byDirectory: (value: string) => C | undefined; usable: (id: string) => C | undefined }, +): { request: T; owner?: C } { + const request = directory ? { ...input, directory } : input + const owner = + (directory && deps.byDirectory(directory)) ?? (request.projectId ? deps.usable(request.projectId) : undefined) + if (!owner) return { request } + return { request: { ...request, projectId: owner.id }, owner } +} + +export function handleToolEvent( + event: unknown, + directory: string | undefined, + contexts: { byDirectory: (value: string) => C | undefined; usable: (id: string) => C | undefined }, + scope: { run: (owner: C, fn: () => Promise) => Promise }, + start: (req: ToolRequest) => Promise, +): void { + const properties = (event as { properties?: unknown }).properties + const req = parseToolRequest(properties) + if (!req) return + const routed = routeToolRequest(req, directory, contexts) + if (routed.owner) { + void scope.run(routed.owner, () => start(routed.request)) + return + } + void start(routed.request) +} diff --git a/packages/kilo-vscode/src/agent-manager/types.ts b/packages/kilo-vscode/src/agent-manager/types.ts index 76389d4f288..7e6792f10b2 100644 --- a/packages/kilo-vscode/src/agent-manager/types.ts +++ b/packages/kilo-vscode/src/agent-manager/types.ts @@ -299,6 +299,7 @@ interface BranchesMessage { interface ImportResultMessage { type: "agentManager.importResult" + projectId?: string success: boolean message: string errorCode?: WorktreeSetupErrorCode @@ -945,6 +946,8 @@ interface TerminalCreateIn { placement: TerminalPlacement /** null for LOCAL, worktree id otherwise */ worktreeId: string | null + cols?: number + rows?: number } interface TerminalCloseIn { diff --git a/packages/kilo-vscode/src/agent-manager/worktree-importer.ts b/packages/kilo-vscode/src/agent-manager/worktree-importer.ts index f589a7c4ebe..0324ce6e1bd 100644 --- a/packages/kilo-vscode/src/agent-manager/worktree-importer.ts +++ b/packages/kilo-vscode/src/agent-manager/worktree-importer.ts @@ -21,23 +21,26 @@ export class WorktreeImporter { constructor(private readonly host: WorktreeImporterHost) {} - async branches(): Promise { + async branches(projectId?: string): Promise { const manager = this.host.manager() if (!manager) { - this.host.post({ type: "agentManager.branches", branches: [], defaultBranch: "main" }) + this.host.post({ type: "agentManager.branches", projectId, branches: [], defaultBranch: "main" }) return } try { const result = await manager.listBranches() + const state = this.host.state() + const configured = state?.getDefaultBaseBranch() + const base = + configured && result.branches.some((branch) => branch.name === configured) ? configured : result.defaultBranch + void manager.prefetchBase(base).catch((err) => this.host.log("Failed to prefetch base branch:", err)) const checked = await manager.checkedOutBranches() const branches = result.branches.map((branch) => ({ ...branch, isCheckedOut: checked.has(branch.name), })) - const state = this.host.state() - const configured = state?.getDefaultBaseBranch() if (state && configured && !branches.some((branch) => branch.name === configured)) { this.host.log(`Default base branch "${configured}" no longer exists, clearing`) state.setDefaultBaseBranch(undefined) @@ -46,31 +49,32 @@ export class WorktreeImporter { this.host.post({ type: "agentManager.branches", + projectId, branches, defaultBranch: result.defaultBranch, }) } catch (error) { this.host.log(`Failed to list branches: ${error}`) - this.host.post({ type: "agentManager.branches", branches: [], defaultBranch: "main" }) + this.host.post({ type: "agentManager.branches", projectId, branches: [], defaultBranch: "main" }) } } - async branch(branch: string): Promise { - await this.run({ branch }) + async branch(branch: string, projectId?: string): Promise { + await this.run({ branch }, projectId) } - async pr(url: string): Promise { - await this.run({ url }) + async pr(url: string, projectId?: string): Promise { + await this.run({ url }, projectId) } - private async run(target: { branch: string } | { url: string }): Promise { + private async run(target: { branch: string } | { url: string }, projectId?: string): Promise { const manager = this.host.manager() const state = this.host.state() if (!manager || !state) { - this.host.post({ type: "agentManager.importResult", success: false, message: "Not a git repository" }) + this.host.post({ type: "agentManager.importResult", projectId, success: false, message: "Not a git repository" }) return } - if (this.busy()) return + if (this.busy(projectId)) return this.importing = true const branch = "branch" in target const creating = branch ? "Creating worktree from branch..." : "Resolving PR..." @@ -79,7 +83,7 @@ export class WorktreeImporter { ? `Branch "${target.branch}" is already checked out in another worktree` : "This PR's branch is already checked out in another worktree" try { - const progress = { type: "agentManager.worktreeSetup", status: "creating" } as const + const progress = { type: "agentManager.worktreeSetup", projectId, status: "creating" } as const this.host.post({ ...progress, message: creating }) const result = branch ? await manager.createWorktree({ existingBranch: target.branch }) @@ -102,7 +106,7 @@ export class WorktreeImporter { state.addSession(session.id, worktree.id) this.host.register(session.id, result.path) this.host.ready(session.id, result, worktree.id) - this.host.post({ type: "agentManager.importResult", success: true, message: success }) + this.host.post({ type: "agentManager.importResult", projectId, success: true, message: success }) this.host.log(`${log} as worktree ${worktree.id}`) } catch (error) { state.removeWorktree(worktree.id) @@ -111,27 +115,28 @@ export class WorktreeImporter { throw error } } catch (error) { - this.importError(error, duplicate) + this.importError(error, duplicate, projectId) } finally { this.importing = false } } - private busy(): boolean { + private busy(projectId?: string): boolean { if (!this.importing) return false this.host.post({ type: "agentManager.importResult", + projectId, success: false, message: "Another import is already in progress", }) return true } - private importError(error: unknown, duplicate: string): void { + private importError(error: unknown, duplicate: string, projectId?: string): void { const raw = error instanceof Error ? error.message : String(error) const message = raw.includes("already used by worktree") || raw.includes("already checked out") ? duplicate : raw const code = classifyWorktreeError(message) - this.host.post({ type: "agentManager.worktreeSetup", status: "error", message, errorCode: code }) - this.host.post({ type: "agentManager.importResult", success: false, message, errorCode: code }) + this.host.post({ type: "agentManager.worktreeSetup", projectId, status: "error", message, errorCode: code }) + this.host.post({ type: "agentManager.importResult", projectId, success: false, message, errorCode: code }) } } diff --git a/packages/kilo-vscode/src/extension.ts b/packages/kilo-vscode/src/extension.ts index b1549e3c479..e5b4e92c8f4 100644 --- a/packages/kilo-vscode/src/extension.ts +++ b/packages/kilo-vscode/src/extension.ts @@ -25,6 +25,8 @@ import { registerHeapSnapshot } from "./commands/heap-snapshot" import { RemoteStatusService } from "./services/RemoteStatusService" import { markWorkspace } from "./util/spotlight" import { createNotebookBridge } from "./services/notebook" +import { createGitExecutable } from "./util/git-executable" +import { isCursorHost } from "./utils" let agentManager: AgentManagerProvider | undefined let shuttingDown = false @@ -47,6 +49,10 @@ export function activate(context: vscode.ExtensionContext) { console.log("Kilo Code extension is now active") shuttingDown = false + // Drives the "!kilo-code.new.isCursor" guards on the native view/title and + // editor/title menu contributions — see isCursorHost() for why. + void vscode.commands.executeCommand("setContext", "kilo-code.new.isCursor", isCursorHost()) + const telemetry = TelemetryProxy.getInstance() // Create shared connection service (one server for all webviews) @@ -139,7 +145,12 @@ export function activate(context: vscode.ExtensionContext) { // The terminal intercepts all keystrokes unless the command is listed in // terminal.integrated.commandsToSkipShell, which only contains built-in // commands by default. - const skip = ["kilo-code.new.agentManagerOpen", "kilo-code.new.agentManager.showTerminal"] + const skip = [ + "kilo-code.new.agentManagerOpen", + "kilo-code.new.agentManager.showTerminal", + "kilo-code.new.agentManager.previousTerminal", + "kilo-code.new.agentManager.nextTerminal", + ] if (process.platform === "darwin") skip.push("kilo-code.new.agentManager.runScript") ensureCommandsSkipShell(skip) @@ -149,7 +160,10 @@ export function activate(context: vscode.ExtensionContext) { // Create Agent Manager provider for editor panel const agentManagerHost = new VscodeHost(context.extensionUri, connectionService, context, remoteService) - const agentManagerProvider = new AgentManagerProvider(agentManagerHost, connectionService) + const git = createGitExecutable({ + log: (message) => console.warn(`[Kilo New] ${message}`), + }) + const agentManagerProvider = new AgentManagerProvider(agentManagerHost, connectionService, git) agentManagerProvider.onPanelVisibilityChange((visible) => remember({ agentManager: visible })) agentManager = agentManagerProvider context.subscriptions.push(agentManagerProvider) @@ -228,6 +242,7 @@ export function activate(context: vscode.ExtensionContext) { deserializeWebviewPanel(panel: vscode.WebviewPanel) { const tabProvider = new KiloProvider(context.extensionUri, connectionService, context, { tabTitle: panelTitleHandler(panel), + topBarSurface: "tab", }) tabProvider.setRemoteService(remoteService) tabProvider.setAutoApproveController(autoApprove) @@ -472,6 +487,12 @@ export function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand("kilo-code.new.agentManager.nextTab", () => { agentManagerProvider.postMessage({ type: "action", action: "tabNext" }) }), + vscode.commands.registerCommand("kilo-code.new.agentManager.previousTerminal", () => { + agentManagerProvider.postMessage({ type: "action", action: "terminalPrevious" }) + }), + vscode.commands.registerCommand("kilo-code.new.agentManager.nextTerminal", () => { + agentManagerProvider.postMessage({ type: "action", action: "terminalNext" }) + }), vscode.commands.registerCommand("kilo-code.new.agentManager.search", () => { agentManagerProvider.postMessage({ type: "action", action: "search" }) }), @@ -623,6 +644,7 @@ function openKiloInNewTab( const tabProvider = new KiloProvider(context.extensionUri, connectionService, context, { tabTitle: panelTitleHandler(panel), + topBarSurface: "tab", }) tabProvider.setRemoteService(remoteService) tabProvider.setAutoApproveController(autoApprove) diff --git a/packages/kilo-vscode/src/kilo-provider-utils.ts b/packages/kilo-vscode/src/kilo-provider-utils.ts index 41ea85380de..c4bd2f9f74e 100644 --- a/packages/kilo-vscode/src/kilo-provider-utils.ts +++ b/packages/kilo-vscode/src/kilo-provider-utils.ts @@ -234,6 +234,7 @@ export interface SessionRefreshContext { sessionDirectories: Map worktreeDirectories?: () => string[] workspaceDirectory: string + isCurrent?: () => boolean postMessage(message: unknown): void } @@ -256,7 +257,7 @@ export async function loadSessions(ctx: SessionRefreshContext): Promise() const extra = await Promise.all( [...worktreeDirs].map((dir) => @@ -276,6 +277,8 @@ export async function loadSessions(ctx: SessionRefreshContext): Promise void + +export function buildAutoApprovalReasonSettingMessage() { + const config = vscode.workspace.getConfiguration("kilo-code.new") + return { + type: "autoApprovalReasonSettingLoaded" as const, + visible: config.get("showAutoApprovalReason", true), + } +} + +export function watchAutoApprovalReasonConfig(post: Post): vscode.Disposable { + return vscode.workspace.onDidChangeConfiguration((event) => { + if (event.affectsConfiguration("kilo-code.new.showAutoApprovalReason")) { + post(buildAutoApprovalReasonSettingMessage()) + } + }) +} diff --git a/packages/kilo-vscode/src/kilo-provider/commands.ts b/packages/kilo-vscode/src/kilo-provider/commands.ts index d641fff1f38..822ef45ba2e 100644 --- a/packages/kilo-vscode/src/kilo-provider/commands.ts +++ b/packages/kilo-vscode/src/kilo-provider/commands.ts @@ -13,12 +13,18 @@ export async function loadCommands(client: KiloClient, dir: string): Promise client.command.list({ directory: dir }, { throwOnError: true })).then(({ data }) => ({ type: "commandsLoaded", - commands: data.map((cmd) => ({ - name: cmd.name, - description: cmd.description, - source: cmd.source, - hints: cmd.hints, - })), + commands: data.map((cmd) => { + const item = cmd as typeof cmd & { variant?: string } + return { + name: item.name, + description: item.description, + agent: item.agent, + model: item.model, + variant: item.variant, + source: item.source, + hints: item.hints, + } + }), })) promises.set(dir, promise) diff --git a/packages/kilo-vscode/src/kilo-provider/early-message.ts b/packages/kilo-vscode/src/kilo-provider/early-message.ts index abf79b49a02..49240367cb9 100644 --- a/packages/kilo-vscode/src/kilo-provider/early-message.ts +++ b/packages/kilo-vscode/src/kilo-provider/early-message.ts @@ -6,6 +6,8 @@ import type { SuggestionContext } from "./handlers/suggestion" import type { KiloClient } from "@kilocode/sdk/v2/client" import { buildChatSettingsMessage } from "./chat-settings" import { buildThroughputSettingMessage } from "./throughput-settings" +import { buildAutoApprovalReasonSettingMessage } from "./auto-approval-reason-settings" +import { handleModelUsageMessage, type ModelUsageMessage } from "./model-usage" type Ctx = { question: SuggestionContext @@ -18,6 +20,7 @@ type Ctx = { copy: (text: string) => PromiseLike openSessions: (ids: string[]) => void speechToTextModels: () => Promise + modelUsage: (message: ModelUsageMessage) => Promise } export async function routeEarlyMessage( @@ -42,6 +45,10 @@ export async function routeEarlyMessage( ) return true } + if (message.type === "recordModelUsage" || message.type === "requestModelUsage") { + await ctx.modelUsage(message as ModelUsageMessage) + return true + } await routeSuggestionWebviewMessage(ctx.question, message) if (await ModelState.handleMessage(message.type, message, ctx.client, ctx.post)) return true if (message.type === "exportSessionTranscript") { @@ -65,6 +72,10 @@ export async function routeEarlyMessage( ctx.post(buildThroughputSettingMessage()) return true } + if (message.type === "requestAutoApprovalReasonSetting") { + ctx.post(buildAutoApprovalReasonSettingMessage()) + return true + } if (message.type === "requestSpeechToTextModels") { await ctx.speechToTextModels() return true diff --git a/packages/kilo-vscode/src/kilo-provider/model-usage.ts b/packages/kilo-vscode/src/kilo-provider/model-usage.ts new file mode 100644 index 00000000000..e27d6c735d0 --- /dev/null +++ b/packages/kilo-vscode/src/kilo-provider/model-usage.ts @@ -0,0 +1,65 @@ +const LIMIT = 200 +export type ModelUsageMap = Record +export type ModelUsageMessage = + | { type: "recordModelUsage"; providerID: string; modelID: string } + | { type: "requestModelUsage" } + +function valid(value: unknown): value is { count: number; lastUsed: number } { + if (!value || typeof value !== "object" || Array.isArray(value)) return false + const item = value as Record + const count = item.count + const lastUsed = item.lastUsed + return ( + typeof count === "number" && + Number.isFinite(count) && + count > 0 && + typeof lastUsed === "number" && + Number.isFinite(lastUsed) + ) +} + +export function validateModelUsage(raw: unknown): ModelUsageMap { + if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {} + const entries = Object.entries(raw as Record).flatMap(([key, value]) => + valid(value) ? [[key, value] as const] : [], + ) + return Object.fromEntries( + entries + .sort(([, a], [, b]) => b.lastUsed - a.lastUsed) + .slice(0, LIMIT) + .map(([key, value]) => [ + key, + { + count: Math.floor(value.count), + lastUsed: value.lastUsed, + }, + ]), + ) +} + +export function recordModelUsage(raw: unknown, providerID: unknown, modelID: unknown, now = Date.now()): ModelUsageMap { + if (typeof providerID !== "string" || !providerID || typeof modelID !== "string" || !modelID) { + return validateModelUsage(raw) + } + const usage = validateModelUsage(raw) + const key = `${providerID}/${modelID}` + const current = usage[key] ?? { count: 0, lastUsed: 0 } + usage[key] = { count: current.count + 1, lastUsed: now } + return validateModelUsage(usage) +} + +export async function handleModelUsageMessage( + message: ModelUsageMessage, + context: + | { globalState: { get: (key: string) => unknown; update: (key: string, value: unknown) => Thenable } } + | undefined, + post: (message: unknown) => void, +): Promise { + const current = context?.globalState.get("modelUsage") + const usage = + message.type === "recordModelUsage" + ? recordModelUsage(current, message.providerID, message.modelID) + : validateModelUsage(current) + if (message.type === "recordModelUsage") await context?.globalState.update("modelUsage", usage) + post({ type: "modelUsageLoaded", usage }) +} diff --git a/packages/kilo-vscode/src/kilo-provider/options.ts b/packages/kilo-vscode/src/kilo-provider/options.ts index fcd8481bbe5..0e23e9d8dbc 100644 --- a/packages/kilo-vscode/src/kilo-provider/options.ts +++ b/packages/kilo-vscode/src/kilo-provider/options.ts @@ -32,4 +32,13 @@ export type KiloProviderOptions = { * is ambiguous. */ projectQualifier?: () => { projectId: string } | undefined + /** + * Hides the in-webview sidebar top bar (New Task, History, Agent Manager, + * etc.) for dedicated single-purpose panels — Settings, Profile, and the + * Sub-Agent Viewer — where it doesn't apply and would let users navigate + * away from the panel's one job. Sidebar and "Open in Tab" leave this unset. + */ + hideTopBar?: boolean + /** Reports "Open in Tab" as the top bar's telemetry surface instead of the sidebar default. */ + topBarSurface?: "tab" } diff --git a/packages/kilo-vscode/src/kilo-provider/sidebar-worktree.ts b/packages/kilo-vscode/src/kilo-provider/sidebar-worktree.ts index dc4d54fe5b0..10963338214 100644 --- a/packages/kilo-vscode/src/kilo-provider/sidebar-worktree.ts +++ b/packages/kilo-vscode/src/kilo-provider/sidebar-worktree.ts @@ -15,6 +15,7 @@ interface Ctx { openAgentManager: () => Thenable openAdvancedWorktree: () => Thenable openChanges: (sessionId?: string, turnId?: string) => Thenable + openProfile: () => Thenable currentSessionId?: string createWorktree?: (baseBranch?: string, branchName?: string) => Promise continueInWorktree?: ( @@ -59,6 +60,11 @@ export async function handleSidebarWorktreeMessage(message: Msg, ctx: Ctx) { return true } + if (message.type === "openProfilePanel") { + await ctx.openProfile() + return true + } + if (message.type !== "continueInWorktree") return false handleContinueInWorktree({ sessionId: message.sessionId, diff --git a/packages/kilo-vscode/src/services/cli-backend/connection-service.test.ts b/packages/kilo-vscode/src/services/cli-backend/connection-service.test.ts index 17420f7bc6a..262b2d64bf8 100644 --- a/packages/kilo-vscode/src/services/cli-backend/connection-service.test.ts +++ b/packages/kilo-vscode/src/services/cli-backend/connection-service.test.ts @@ -282,3 +282,25 @@ describe("KiloConnectionService drainPendingPrompts", () => { expect(cleared).toBe(0) }) }) + +describe("KiloConnectionService server exit handling", () => { + test("reports signal name when process is killed by signal", () => { + const service = new KiloConnectionService({} as any) + let stateErr: Error | undefined + service.onStateChange((state, err) => { + if (state === "error") stateErr = err + }) + ;(service as any).handleServerExit(null, "SIGSEGV") + expect(stateErr?.message).toBe("CLI background process exited with signal SIGSEGV. Retry to reconnect.") + }) + + test("reports exit code when process exits normally with code", () => { + const service = new KiloConnectionService({} as any) + let stateErr: Error | undefined + service.onStateChange((state, err) => { + if (state === "error") stateErr = err + }) + ;(service as any).handleServerExit(1, null) + expect(stateErr?.message).toBe("CLI background process exited with code 1. Retry to reconnect.") + }) +}) 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 7a7e1209659..d202eeedba9 100644 --- a/packages/kilo-vscode/src/services/cli-backend/connection-service.ts +++ b/packages/kilo-vscode/src/services/cli-backend/connection-service.ts @@ -138,7 +138,7 @@ export class KiloConnectionService { update: async () => undefined, } satisfies Pick) this.sandboxPreference = new SandboxPreference(state) - this.serverManager = new ServerManager(context, (code) => this.handleServerExit(code)) + this.serverManager = new ServerManager(context, (code, signal) => this.handleServerExit(code, signal)) this.active = vscode.window.state.focused this.windowStateDisposable = vscode.window.onDidChangeWindowState((ws) => { this.active = ws.focused @@ -790,13 +790,11 @@ export class KiloConnectionService { this.questionRevision += 1 } - private handleServerExit(code: number | null): void { - console.warn("[Kilo New] ConnectionService: CLI background process exited:", code) + private handleServerExit(code: number | null, signal: NodeJS.Signals | null): void { + const reason = signal ? `signal ${signal}` : `code ${code ?? "unknown"}` + console.warn(`[Kilo New] ConnectionService: CLI background process exited with ${reason}`) this.resetConnection() - this.setState( - "error", - new Error(`CLI background process exited with code ${code ?? "unknown"}. Retry to reconnect.`), - ) + this.setState("error", new Error(`CLI background process exited with ${reason}. Retry to reconnect.`)) } private async doConnect(workspaceDir: string): Promise { diff --git a/packages/kilo-vscode/src/services/cli-backend/i18n/ar.ts b/packages/kilo-vscode/src/services/cli-backend/i18n/ar.ts index 1221fb0de36..c938afc5e41 100644 --- a/packages/kilo-vscode/src/services/cli-backend/i18n/ar.ts +++ b/packages/kilo-vscode/src/services/cli-backend/i18n/ar.ts @@ -1,6 +1,8 @@ export const dict = { - "server.processExited": "انتهت عملية CLI بالرمز {{code}} قبل بدء الخادم", - "server.startupTimeout": "انتهت مهلة بدء تشغيل الخادم بعد {{seconds}} ثانية", + "server.processExited": "تم إنهاء عملية CLI بالرمز {{code}} قبل بدء الخادم", + "server.processSignaled": "تم إنهاء عملية CLI بواسطة الإشارة {{signal}} قبل بدء الخادم", + "server.spawnFailed": "فشل في تشغيل الملف الثنائي لـ CLI ({{code}})", + "server.startupTimeout": "انتهت مهلة بدء تشغيل الخادم بعد {{seconds}} ثوانٍ", "remote.connected": "Kilo Remote: متصل", "remote.connecting": "Kilo Remote: جارٍ الاتصال\u2026", } as const diff --git a/packages/kilo-vscode/src/services/cli-backend/i18n/br.ts b/packages/kilo-vscode/src/services/cli-backend/i18n/br.ts index c2cb93e6ef1..31c166b9c29 100644 --- a/packages/kilo-vscode/src/services/cli-backend/i18n/br.ts +++ b/packages/kilo-vscode/src/services/cli-backend/i18n/br.ts @@ -1,6 +1,8 @@ export const dict = { - "server.processExited": "O processo da CLI foi encerrado com o código {{code}} antes que o servidor fosse iniciado", - "server.startupTimeout": "Tempo limite de inicialização do servidor esgotado após {{seconds}} segundos", + "server.processExited": "O processo da CLI foi encerrado com o código {{code}} antes da inicialização do servidor", + "server.processSignaled": "O processo da CLI foi finalizado pelo sinal {{signal}} antes da inicialização do servidor", + "server.spawnFailed": "Falha ao gerar o binário da CLI ({{code}})", + "server.startupTimeout": "Tempo limite de inicialização do servidor atingido após {{seconds}} segundos", "remote.connected": "Kilo Remote: Conectado", "remote.connecting": "Kilo Remote: Conectando\u2026", } as const diff --git a/packages/kilo-vscode/src/services/cli-backend/i18n/bs.ts b/packages/kilo-vscode/src/services/cli-backend/i18n/bs.ts index 87de30b377c..6cf1847da96 100644 --- a/packages/kilo-vscode/src/services/cli-backend/i18n/bs.ts +++ b/packages/kilo-vscode/src/services/cli-backend/i18n/bs.ts @@ -1,6 +1,8 @@ export const dict = { - "server.processExited": "CLI proces je izašao sa kodom {{code}} prije nego što se server pokrenuo", - "server.startupTimeout": "Vrijeme pokretanja servera je isteklo nakon {{seconds}} sekundi", + "server.processExited": "CLI proces je izašao s kodom {{code}} prije nego što je server pokrenut", + "server.processSignaled": "CLI proces je prekinut signalom {{signal}} prije pokretanja servera", + "server.spawnFailed": "Neuspjelo pokretanje CLI binarne datoteke ({{code}})", + "server.startupTimeout": "Isteklo je vrijeme za pokretanje servera nakon {{seconds}} sekundi", "remote.connected": "Kilo Remote: Povezano", "remote.connecting": "Kilo Remote: Povezivanje\u2026", } as const diff --git a/packages/kilo-vscode/src/services/cli-backend/i18n/da.ts b/packages/kilo-vscode/src/services/cli-backend/i18n/da.ts index 59881a81fec..9bd6d68e93d 100644 --- a/packages/kilo-vscode/src/services/cli-backend/i18n/da.ts +++ b/packages/kilo-vscode/src/services/cli-backend/i18n/da.ts @@ -1,6 +1,8 @@ export const dict = { - "server.processExited": "CLI-processen afsluttede med kode {{code}} før serveren startede", - "server.startupTimeout": "Serverens opstartstid udløb efter {{seconds}} sekunder", + "server.processExited": "CLI-processen afsluttedes med kode {{code}}, før serveren startede", + "server.processSignaled": "CLI-processen blev afbrudt af signal {{signal}}, før serveren startede", + "server.spawnFailed": "Kunne ikke starte CLI-binærfil ({{code}})", + "server.startupTimeout": "Timeout for serverstart efter {{seconds}} sekunder", "remote.connected": "Kilo Remote: Forbundet", "remote.connecting": "Kilo Remote: Forbinder\u2026", } as const diff --git a/packages/kilo-vscode/src/services/cli-backend/i18n/de.ts b/packages/kilo-vscode/src/services/cli-backend/i18n/de.ts index df45e47ea27..fc3d7b8d5e0 100644 --- a/packages/kilo-vscode/src/services/cli-backend/i18n/de.ts +++ b/packages/kilo-vscode/src/services/cli-backend/i18n/de.ts @@ -1,5 +1,8 @@ export const dict = { "server.processExited": "Der CLI-Prozess wurde mit dem Code {{code}} beendet, bevor der Server gestartet wurde", + "server.processSignaled": + "Der CLI-Prozess wurde durch das Signal {{signal}} beendet, bevor der Server gestartet wurde", + "server.spawnFailed": "Fehler beim Starten der CLI-Binärdatei ({{code}})", "server.startupTimeout": "Zeitüberschreitung beim Serverstart nach {{seconds}} Sekunden", "remote.connected": "Kilo Remote: Verbunden", "remote.connecting": "Kilo Remote: Verbindung wird hergestellt\u2026", diff --git a/packages/kilo-vscode/src/services/cli-backend/i18n/en.ts b/packages/kilo-vscode/src/services/cli-backend/i18n/en.ts index 0b59af3f34f..ccecde0d245 100644 --- a/packages/kilo-vscode/src/services/cli-backend/i18n/en.ts +++ b/packages/kilo-vscode/src/services/cli-backend/i18n/en.ts @@ -1,5 +1,7 @@ export const dict = { "server.processExited": "CLI process exited with code {{code}} before server started", + "server.processSignaled": "CLI process terminated by signal {{signal}} before server started", + "server.spawnFailed": "Failed to spawn CLI binary ({{code}})", "server.startupTimeout": "Server startup timeout after {{seconds}} seconds", "remote.connected": "Kilo Remote: Connected", "remote.connecting": "Kilo Remote: Connecting\u2026", diff --git a/packages/kilo-vscode/src/services/cli-backend/i18n/es.ts b/packages/kilo-vscode/src/services/cli-backend/i18n/es.ts index a744fe33455..6491eed1e8e 100644 --- a/packages/kilo-vscode/src/services/cli-backend/i18n/es.ts +++ b/packages/kilo-vscode/src/services/cli-backend/i18n/es.ts @@ -1,6 +1,8 @@ export const dict = { - "server.processExited": "El proceso de la CLI finalizó con el código {{code}} antes de que se iniciara el servidor", - "server.startupTimeout": "Tiempo de espera de inicio del servidor agotado después de {{seconds}} segundos", + "server.processExited": "El proceso CLI finalizó con el código {{code}} antes de que se iniciara el servidor", + "server.processSignaled": "El proceso CLI fue terminado por la señal {{signal}} antes de que se iniciara el servidor", + "server.spawnFailed": "Error al generar el binario CLI ({{code}})", + "server.startupTimeout": "Tiempo de espera agotado para el inicio del servidor después de {{seconds}} segundos", "remote.connected": "Kilo Remote: Conectado", "remote.connecting": "Kilo Remote: Conectando\u2026", } as const diff --git a/packages/kilo-vscode/src/services/cli-backend/i18n/fa.ts b/packages/kilo-vscode/src/services/cli-backend/i18n/fa.ts index 3aa4dc74830..19729aa5c71 100644 --- a/packages/kilo-vscode/src/services/cli-backend/i18n/fa.ts +++ b/packages/kilo-vscode/src/services/cli-backend/i18n/fa.ts @@ -1,6 +1,8 @@ export const dict = { - "server.processExited": "فرآیند CLI با کد {{code}} قبل از راه‌اندازی سرور خاتمه یافت", + "server.processExited": "فرایند CLI قبل از شروع سرور با کد {{code}} خارج شد", + "server.processSignaled": "فرایند CLI با سیگنال {{signal}} قبل از شروع سرور متوقف شد", + "server.spawnFailed": "اجرای فایل باینری CLI ناموفق بود ({{code}})", "server.startupTimeout": "زمان راه‌اندازی سرور پس از {{seconds}} ثانیه به پایان رسید", "remote.connected": "Kilo Remote: متصل شد", - "remote.connecting": "Kilo Remote: در حال اتصال…", + "remote.connecting": "Kilo Remote: در حال اتصال\u2026", } as const diff --git a/packages/kilo-vscode/src/services/cli-backend/i18n/fr.ts b/packages/kilo-vscode/src/services/cli-backend/i18n/fr.ts index 03f7a3024e9..dafcad4e76d 100644 --- a/packages/kilo-vscode/src/services/cli-backend/i18n/fr.ts +++ b/packages/kilo-vscode/src/services/cli-backend/i18n/fr.ts @@ -1,6 +1,8 @@ export const dict = { - "server.processExited": "Le processus CLI s'est terminé avec le code {{code}} avant le démarrage du serveur", - "server.startupTimeout": "Délai de démarrage du serveur dépassé après {{seconds}} secondes", - "remote.connected": "Kilo Remote\u00a0: Connecté", - "remote.connecting": "Kilo Remote\u00a0: Connexion\u2026", + "server.processExited": "Le processus CLI a quitté avec le code {{code}} avant le démarrage du serveur", + "server.processSignaled": "Le processus CLI a été arrêté par le signal {{signal}} avant le démarrage du serveur", + "server.spawnFailed": "Échec du lancement du binaire CLI ({{code}})", + "server.startupTimeout": "Délai d'attente du démarrage du serveur dépassé après {{seconds}} secondes", + "remote.connected": "Kilo Remote : Connecté", + "remote.connecting": "Kilo Remote : Connexion en cours\u2026", } as const diff --git a/packages/kilo-vscode/src/services/cli-backend/i18n/it.ts b/packages/kilo-vscode/src/services/cli-backend/i18n/it.ts index f032870a431..dc4b1d05ffe 100644 --- a/packages/kilo-vscode/src/services/cli-backend/i18n/it.ts +++ b/packages/kilo-vscode/src/services/cli-backend/i18n/it.ts @@ -1,6 +1,8 @@ export const dict = { - "server.processExited": "Il processo CLI è uscito con codice {{code}} prima dell'avvio del server", + "server.processExited": "Il processo CLI è terminato con il codice {{code}} prima dell'avvio del server", + "server.processSignaled": "Il processo CLI è stato terminato dal segnale {{signal}} prima dell'avvio del server", + "server.spawnFailed": "Impossibile avviare il binario CLI ({{code}})", "server.startupTimeout": "Timeout di avvio del server dopo {{seconds}} secondi", - "remote.connected": "Kilo Remote: connesso", - "remote.connecting": "Kilo Remote: connessione...", + "remote.connected": "Kilo Remote: Connesso", + "remote.connecting": "Kilo Remote: Connessione in corso\u2026", } as const diff --git a/packages/kilo-vscode/src/services/cli-backend/i18n/ja.ts b/packages/kilo-vscode/src/services/cli-backend/i18n/ja.ts index 58e9c3ee9bd..966c46fc3ca 100644 --- a/packages/kilo-vscode/src/services/cli-backend/i18n/ja.ts +++ b/packages/kilo-vscode/src/services/cli-backend/i18n/ja.ts @@ -1,5 +1,7 @@ export const dict = { "server.processExited": "サーバーが起動する前に、CLI プロセスがコード {{code}} で終了しました", + "server.processSignaled": "サーバーが起動する前に、CLI プロセスがシグナル {{signal}} で終了しました", + "server.spawnFailed": "CLI バイナリの起動に失敗しました ({{code}})", "server.startupTimeout": "サーバーの起動が {{seconds}} 秒後にタイムアウトしました", "remote.connected": "Kilo Remote: 接続済み", "remote.connecting": "Kilo Remote: 接続中\u2026", diff --git a/packages/kilo-vscode/src/services/cli-backend/i18n/ko.ts b/packages/kilo-vscode/src/services/cli-backend/i18n/ko.ts index c48eed062e9..61ca3601dde 100644 --- a/packages/kilo-vscode/src/services/cli-backend/i18n/ko.ts +++ b/packages/kilo-vscode/src/services/cli-backend/i18n/ko.ts @@ -1,6 +1,8 @@ export const dict = { - "server.processExited": "서버가 시작되기 전에 CLI 프로세스가 코드 {{code}}로 종료되었습니다", - "server.startupTimeout": "{{seconds}}초 후 서버 시작 시간이 초과되었습니다", + "server.processExited": "서버가 시작되기 전에 CLI 프로세스가 코드 {{code}}(으)로 종료되었습니다", + "server.processSignaled": "서버가 시작되기 전에 CLI 프로세스가 신호 {{signal}}(으)로 종료되었습니다", + "server.spawnFailed": "CLI 바이너리를 생성하지 못했습니다 ({{code}})", + "server.startupTimeout": "{{seconds}}초 후 서버 시작 시간 초과", "remote.connected": "Kilo Remote: 연결됨", "remote.connecting": "Kilo Remote: 연결 중\u2026", } as const diff --git a/packages/kilo-vscode/src/services/cli-backend/i18n/nl.ts b/packages/kilo-vscode/src/services/cli-backend/i18n/nl.ts index c9cfc6a96e2..87336c1c3ba 100644 --- a/packages/kilo-vscode/src/services/cli-backend/i18n/nl.ts +++ b/packages/kilo-vscode/src/services/cli-backend/i18n/nl.ts @@ -1,5 +1,7 @@ export const dict = { "server.processExited": "CLI-proces is afgesloten met code {{code}} voordat de server is gestart", + "server.processSignaled": "CLI-proces is beëindigd door signaal {{signal}} voordat de server is gestart", + "server.spawnFailed": "Kan CLI-binair bestand niet starten ({{code}})", "server.startupTimeout": "Time-out bij opstarten van server na {{seconds}} seconden", "remote.connected": "Kilo Remote: Verbonden", "remote.connecting": "Kilo Remote: Verbinden\u2026", diff --git a/packages/kilo-vscode/src/services/cli-backend/i18n/no.ts b/packages/kilo-vscode/src/services/cli-backend/i18n/no.ts index a24895f79e4..e1148e928e4 100644 --- a/packages/kilo-vscode/src/services/cli-backend/i18n/no.ts +++ b/packages/kilo-vscode/src/services/cli-backend/i18n/no.ts @@ -1,5 +1,7 @@ export const dict = { "server.processExited": "CLI-prosessen avsluttet med kode {{code}} før serveren startet", + "server.processSignaled": "CLI-prosessen ble avsluttet av signal {{signal}} før serveren startet", + "server.spawnFailed": "Kunne ikke starte CLI-binærfil ({{code}})", "server.startupTimeout": "Tidsavbrudd for serveroppstart etter {{seconds}} sekunder", "remote.connected": "Kilo Remote: Tilkoblet", "remote.connecting": "Kilo Remote: Kobler til\u2026", diff --git a/packages/kilo-vscode/src/services/cli-backend/i18n/pl.ts b/packages/kilo-vscode/src/services/cli-backend/i18n/pl.ts index 7f7415c1378..7f5c54e96f5 100644 --- a/packages/kilo-vscode/src/services/cli-backend/i18n/pl.ts +++ b/packages/kilo-vscode/src/services/cli-backend/i18n/pl.ts @@ -1,5 +1,7 @@ export const dict = { - "server.processExited": "Proces CLI zakończył się z kodem {{code}} przed uruchomieniem serwera", + "server.processExited": "Proces CLI zakończył działanie z kodem {{code}} przed uruchomieniem serwera", + "server.processSignaled": "Proces CLI został przerwany przez sygnał {{signal}} przed uruchomieniem serwera", + "server.spawnFailed": "Nie udało się uruchomić pliku binarnego CLI ({{code}})", "server.startupTimeout": "Przekroczono limit czasu uruchamiania serwera po {{seconds}} sekundach", "remote.connected": "Kilo Remote: Połączono", "remote.connecting": "Kilo Remote: Łączenie\u2026", diff --git a/packages/kilo-vscode/src/services/cli-backend/i18n/ru.ts b/packages/kilo-vscode/src/services/cli-backend/i18n/ru.ts index b9e721e60bf..287091788be 100644 --- a/packages/kilo-vscode/src/services/cli-backend/i18n/ru.ts +++ b/packages/kilo-vscode/src/services/cli-backend/i18n/ru.ts @@ -1,5 +1,7 @@ export const dict = { "server.processExited": "Процесс CLI завершился с кодом {{code}} до запуска сервера", + "server.processSignaled": "Процесс CLI был завершен сигналом {{signal}} до запуска сервера", + "server.spawnFailed": "Не удалось запустить исполняемый файл CLI ({{code}})", "server.startupTimeout": "Время ожидания запуска сервера истекло через {{seconds}} секунд", "remote.connected": "Kilo Remote: Подключено", "remote.connecting": "Kilo Remote: Подключение\u2026", diff --git a/packages/kilo-vscode/src/services/cli-backend/i18n/th.ts b/packages/kilo-vscode/src/services/cli-backend/i18n/th.ts index aff4ea8f378..05be10cd554 100644 --- a/packages/kilo-vscode/src/services/cli-backend/i18n/th.ts +++ b/packages/kilo-vscode/src/services/cli-backend/i18n/th.ts @@ -1,6 +1,8 @@ export const dict = { - "server.processExited": "กระบวนการ CLI ออกด้วยรหัส {{code}} ก่อนที่เซิร์ฟเวอร์จะเริ่มทำงาน", - "server.startupTimeout": "หมดเวลาการเริ่มต้นเซิร์ฟเวอร์หลังจาก {{seconds}} วินาที", + "server.processExited": "กระบวนการ CLI ออกด้วยรหัส {{code}} ก่อนที่เซิร์ฟเวอร์จะเริ่มต้น", + "server.processSignaled": "กระบวนการ CLI ถูกยุติโดยสัญญาณ {{signal}} ก่อนที่เซิร์ฟเวอร์จะเริ่มต้น", + "server.spawnFailed": "ไม่สามารถเริ่มต้นไบนารี CLI ({{code}})", + "server.startupTimeout": "หมดเวลาเริ่มต้นเซิร์ฟเวอร์หลังจาก {{seconds}} วินาที", "remote.connected": "Kilo Remote: เชื่อมต่อแล้ว", "remote.connecting": "Kilo Remote: กำลังเชื่อมต่อ\u2026", } as const diff --git a/packages/kilo-vscode/src/services/cli-backend/i18n/tr.ts b/packages/kilo-vscode/src/services/cli-backend/i18n/tr.ts index 6be397e7a36..4530412f19a 100644 --- a/packages/kilo-vscode/src/services/cli-backend/i18n/tr.ts +++ b/packages/kilo-vscode/src/services/cli-backend/i18n/tr.ts @@ -1,5 +1,7 @@ export const dict = { - "server.processExited": "CLI işlemi sunucu başlamadan önce {{code}} koduyla çıktı", + "server.processExited": "CLI işlemi sunucu başlatılmadan önce {{code}} koduyla çıktı", + "server.processSignaled": "CLI işlemi sunucu başlatılmadan önce {{signal}} sinyali ile sonlandırıldı", + "server.spawnFailed": "CLI ikili dosyası başlatılamadı ({{code}})", "server.startupTimeout": "{{seconds}} saniye sonra sunucu başlatma zaman aşımı", "remote.connected": "Kilo Remote: Bağlandı", "remote.connecting": "Kilo Remote: Bağlanıyor\u2026", diff --git a/packages/kilo-vscode/src/services/cli-backend/i18n/uk.ts b/packages/kilo-vscode/src/services/cli-backend/i18n/uk.ts index 315d12fa96d..5b89093555e 100644 --- a/packages/kilo-vscode/src/services/cli-backend/i18n/uk.ts +++ b/packages/kilo-vscode/src/services/cli-backend/i18n/uk.ts @@ -1,6 +1,8 @@ export const dict = { "server.processExited": "Процес CLI завершився з кодом {{code}} до запуску сервера", - "server.startupTimeout": "Час очікування запуску сервера вичерпано після {{seconds}} секунд", + "server.processSignaled": "Процес CLI був завершений сигналом {{signal}} до запуску сервера", + "server.spawnFailed": "Не вдалося запустити двійковий файл CLI ({{code}})", + "server.startupTimeout": "Час очікування запуску сервера минув через {{seconds}} секунд", "remote.connected": "Kilo Remote: Підключено", "remote.connecting": "Kilo Remote: Підключення\u2026", } as const diff --git a/packages/kilo-vscode/src/services/cli-backend/i18n/zh.ts b/packages/kilo-vscode/src/services/cli-backend/i18n/zh.ts index 72160d167fe..b050faebf43 100644 --- a/packages/kilo-vscode/src/services/cli-backend/i18n/zh.ts +++ b/packages/kilo-vscode/src/services/cli-backend/i18n/zh.ts @@ -1,6 +1,8 @@ export const dict = { - "server.processExited": "在服务器启动之前,CLI 进程已退出,代码为 {{code}}", - "server.startupTimeout": "服务器启动在 {{seconds}} 秒后超时", + "server.processExited": "CLI 进程在服务器启动前以代码 {{code}} 退出", + "server.processSignaled": "CLI 进程在服务器启动前被信号 {{signal}} 终止", + "server.spawnFailed": "生成 CLI 二进制文件失败 ({{code}})", + "server.startupTimeout": "{{seconds}} 秒后服务器启动超时", "remote.connected": "Kilo Remote: 已连接", "remote.connecting": "Kilo Remote: 正在连接\u2026", } as const diff --git a/packages/kilo-vscode/src/services/cli-backend/i18n/zht.ts b/packages/kilo-vscode/src/services/cli-backend/i18n/zht.ts index 61baae8020f..799bb17a8f6 100644 --- a/packages/kilo-vscode/src/services/cli-backend/i18n/zht.ts +++ b/packages/kilo-vscode/src/services/cli-backend/i18n/zht.ts @@ -1,6 +1,8 @@ export const dict = { - "server.processExited": "在伺服器啟動之前,CLI 處理程序已退出,代碼為 {{code}}", - "server.startupTimeout": "伺服器啟動在 {{seconds}} 秒後逾時", + "server.processExited": "CLI 程序在伺服器啟動前以代碼 {{code}} 退出", + "server.processSignaled": "CLI 程序在伺服器啟動前被信號 {{signal}} 終止", + "server.spawnFailed": "生成 CLI 二進位檔案失敗 ({{code}})", + "server.startupTimeout": "{{seconds}} 秒後伺服器啟動逾時", "remote.connected": "Kilo Remote: 已連線", "remote.connecting": "Kilo Remote: 正在連線\u2026", } as const 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 76cb5125719..9edea2476c8 100644 --- a/packages/kilo-vscode/src/services/cli-backend/server-manager.ts +++ b/packages/kilo-vscode/src/services/cli-backend/server-manager.ts @@ -17,7 +17,7 @@ export interface ServerInstance { const STARTUP_TIMEOUT_SECONDS = 30 type WorkspaceFolderLike = { uri: { fsPath: string } } -type ServerExitListener = (code: number | null) => void +type ServerExitListener = (code: number | null, signal: NodeJS.Signals | null) => void export function resolveServerCwd(folders: readonly WorkspaceFolderLike[] | undefined, storage: string): string { return folders?.[0]?.uri.fsPath ?? storage @@ -182,25 +182,36 @@ export class ServerManager { stderrLines.push(errorOutput) }) - serverProcess.on("error", (error) => { - console.error("[Kilo New] ServerManager: ❌ Process error:", error) + serverProcess.on("error", (err: NodeJS.ErrnoException) => { + console.error("[Kilo New] ServerManager: ❌ Process error:", err) if (!resolved) { - reject(error) + const spawnErr = err as NodeJS.ErrnoException & { spawnargs?: string[] } + const code = err.code || err.name || "UNKNOWN" + const header = t("server.spawnFailed", { code }) + const lines = [ + `Error: ${err.message}`, + ...(err.code ? [`Code: ${err.code}`] : []), + ...(err.errno !== undefined ? [`Errno: ${err.errno}`] : []), + ...(err.syscall ? [`Syscall: ${err.syscall}`] : []), + ...(err.path ? [`Path: ${err.path}`] : []), + ...(Array.isArray(spawnErr.spawnargs) ? [`Spawn args: ${JSON.stringify(spawnErr.spawnargs)}`] : []), + ] + const { userMessage, userDetails } = toErrorMessage(header, [...lines, ...stderrLines], cliPath) + reject(new ServerStartupError(userMessage, userDetails)) } }) - serverProcess.on("exit", (code) => { - console.log("[Kilo New] ServerManager: 🛑 Process exited with code:", code) + serverProcess.on("exit", (code, signal) => { + console.warn("[Kilo New] ServerManager: 🛑 Process exited:", { code, signal }) if (this.instance?.process === serverProcess) { this.instance = null - this.onExit?.(code) + this.onExit?.(code, signal) } if (!resolved) { - const { userMessage, userDetails } = toErrorMessage( - t("server.processExited", { code: code ?? "null" }), - stderrLines, - cliPath, - ) + const msg = signal + ? t("server.processSignaled", { signal }) + : t("server.processExited", { code: code ?? "unknown" }) + const { userMessage, userDetails } = toErrorMessage(msg, stderrLines, cliPath) reject(new ServerStartupError(userMessage, userDetails)) } }) diff --git a/packages/kilo-vscode/src/speech-to-text/capture.ts b/packages/kilo-vscode/src/speech-to-text/capture.ts index cbf5c54848a..4b354158426 100644 --- a/packages/kilo-vscode/src/speech-to-text/capture.ts +++ b/packages/kilo-vscode/src/speech-to-text/capture.ts @@ -24,7 +24,7 @@ type Recording = Input & { type Audio = { data: string - format: "wav" + format: "m4a" model: string language?: string } @@ -39,16 +39,17 @@ ObjC.import("AVFoundation") ObjC.import("Foundation") function run(args) { const settings = $.NSMutableDictionary.alloc.init - settings.setObjectForKey($.NSNumber.numberWithUnsignedInt(1819304813), $.AVFormatIDKey) + settings.setObjectForKey($.NSNumber.numberWithUnsignedInt(1633772320), $.AVFormatIDKey) settings.setObjectForKey($.NSNumber.numberWithDouble(16000), $.AVSampleRateKey) settings.setObjectForKey($.NSNumber.numberWithInt(1), $.AVNumberOfChannelsKey) - settings.setObjectForKey($.NSNumber.numberWithInt(16), $.AVLinearPCMBitDepthKey) - settings.setObjectForKey($.NSNumber.numberWithBool(false), $.AVLinearPCMIsFloatKey) - settings.setObjectForKey($.NSNumber.numberWithBool(false), $.AVLinearPCMIsBigEndianKey) + settings.setObjectForKey($.NSNumber.numberWithInt(24000), $.AVEncoderBitRateKey) const error = Ref() const url = $.NSURL.fileURLWithPath(args[0]) const recorder = $.AVAudioRecorder.alloc.initWithURLSettingsError(url, settings, error) - if (!recorder || !recorder.prepareToRecord || !recorder.record) throw new Error("Could not start recording") + if (!recorder || !recorder.prepareToRecord || !recorder.record) { + const description = error[0] && error[0].localizedDescription + throw new Error(description ? description.js : "Could not start recording") + } console.log("ready") $.NSFileHandle.fileHandleWithStandardInput.readDataToEndOfFile recorder.stop @@ -68,7 +69,7 @@ export async function startSpeechCapture(input: Input): Promise { starting = input.requestId try { - const file = path.join(os.tmpdir(), `kilo-stt-${process.pid}-${Date.now()}.wav`) + const file = path.join(os.tmpdir(), `kilo-stt-${process.pid}-${Date.now()}.m4a`) if (useMacCapture(process.platform, process.env)) { const result = await startMac(file, input).catch((err: unknown) => { console.warn("[Kilo New] Native macOS speech capture failed, falling back to FFmpeg", err) @@ -105,7 +106,7 @@ export async function stopSpeechCapture(requestId: string): Promise - - {(shortcut) => ( - - {isMac ? "⌘" : "Ctrl+"} - {shortcut()} - - )} -
{props.t("agentManager.local")} {props.local!.branch}
- -
- - ↓{props.local!.behind} - - - ↑{props.local!.ahead} - - - +{props.local!.additions} - - - −{props.local!.deletions} +
+ +
+ + ↓{props.local!.behind} + + + ↑{props.local!.ahead} + + + +{props.local!.additions} + + + −{props.local!.deletions} + +
+
+
+ + {(shortcut) => ( + + {isMac ? "⌘" : "Ctrl+"} + {shortcut()} + + )}
- +
diff --git a/packages/kilo-vscode/webview-ui/agent-manager/ProjectsSection.tsx b/packages/kilo-vscode/webview-ui/agent-manager/ProjectsSection.tsx index 1e2dcb8fb8a..36359ab5f20 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/ProjectsSection.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/ProjectsSection.tsx @@ -36,7 +36,6 @@ export const ProjectsSection: Component = (props) => ( label={} actions={
- {props.tools} = (props) => ( label={props.t("agentManager.project.add")} onClick={props.onAdd} /> + {props.tools}
} /> diff --git a/packages/kilo-vscode/webview-ui/agent-manager/SidebarBody.tsx b/packages/kilo-vscode/webview-ui/agent-manager/SidebarBody.tsx index 840ec8cfba6..8f58fb07d44 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/SidebarBody.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/SidebarBody.tsx @@ -124,62 +124,66 @@ export const SidebarBody: Component = (props) => { {props.repoBranch()}
- -
-
-
+
+ +
+
+
+
+ + 0 || + props.localStats()!.additions > 0 || + props.localStats()!.deletions > 0 || + props.localStats()!.ahead > 0 || + props.localStats()!.behind > 0) + } + > +
+ 0 || props.localStats()!.deletions > 0} + fallback={ + 0}> + {props.localStats()!.files}f + + } + > +
+ 0}> + +{props.localStats()!.additions} + + 0}> + + {"−"} + {props.localStats()!.deletions} + + +
+
+ 0 || props.localStats()!.behind > 0}> +
+ 0}> + + {"↑"} + {props.localStats()!.ahead} + + + 0}> + + {"↓"} + {props.localStats()!.behind} + + +
+
+
+
+
+ {isMac ? "⌘" : "Ctrl+"}1
- - 0 || - props.localStats()!.additions > 0 || - props.localStats()!.deletions > 0 || - props.localStats()!.ahead > 0 || - props.localStats()!.behind > 0) - } - > -
- 0 || props.localStats()!.deletions > 0} - fallback={ - 0}> - {props.localStats()!.files}f - - } - > -
- 0}> - +{props.localStats()!.additions} - - 0}> - - {"−"} - {props.localStats()!.deletions} - - -
-
- 0 || props.localStats()!.behind > 0}> -
- 0}> - - {"↑"} - {props.localStats()!.ahead} - - - 0}> - - {"↓"} - {props.localStats()!.behind} - - -
-
-
-
- {isMac ? "⌘" : "Ctrl+"}1 +
{/* WORKTREES section */} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/TabBar.tsx b/packages/kilo-vscode/webview-ui/agent-manager/TabBar.tsx index fc1a36b462b..359ad99e4cb 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/TabBar.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/TabBar.tsx @@ -253,9 +253,9 @@ export const TabBar: Component = (props) => ( {/* Terminal destination split button: the primary action - follows the user's setting (VS Code integrated terminal - or the embedded side panel), the dropdown picks which. - Cmd+Shift+T still creates an xterm tab via the `+` menu. */} + follows the user's setting (VS Code integrated terminal + or the embedded side panel), the dropdown picks which. + Cmd+Shift+T creates a terminal in the active terminal container. */} .am-section-header { + padding-left: 2px; + padding-right: 2px; } .am-section-label { @@ -301,13 +318,23 @@ html[data-theme="kilo-vscode"] flex-direction: column; min-height: 0; overflow-y: auto; + /* Pull out of .am-sidebar's 8px padding to an even 4px gutter on both sides, + so a selected row's rounded background is inset symmetrically. 4px on the + right is also the most we can reclaim before the resize handle's hit area + (8px wide, centered on the border) would cover the row's own controls. */ + margin-left: -4px; + margin-right: -4px; } .am-projects-tools, .am-project-actions { display: flex; align-items: center; - gap: 2px; + justify-content: space-between; + /* The project row needs 20px + 24px + 20px; the worktree row uses the + remaining space for its split-button and settings gap. */ + width: 64px; + gap: 0; } .am-project { @@ -324,7 +351,9 @@ html[data-theme="kilo-vscode"] .am-project-item { min-height: 34px; - padding: 6px 8px 6px 12px; + /* Leading column aligns with the card icons below it (.am-local-item and + .am-worktree-item both use a 6px inset), so the tree reads as one line. */ + padding: 6px 6px; box-sizing: border-box; } @@ -366,7 +395,7 @@ html[data-theme="kilo-vscode"] display: flex; flex-direction: column; min-height: 0; - padding: 0 6px; + padding: 0; } .am-project-body > .am-local-item { @@ -422,7 +451,7 @@ html[data-theme="kilo-vscode"] display: flex; align-items: flex-start; gap: 8px; - padding: 6px 10px; + padding: 6px 6px; min-height: 36px; box-sizing: border-box; border-radius: var(--radius-sm); @@ -439,6 +468,8 @@ html[data-theme="kilo-vscode"] flex-shrink: 0; display: flex; align-items: center; + justify-content: center; + width: 16px; height: 20px; } @@ -474,6 +505,10 @@ html[data-theme="kilo-vscode"] /* Hover actions (shortcut badge + close button) — hidden by default, shown on hover */ .am-wt-hover-actions { + position: absolute; + top: 0; + right: 0; + bottom: 0; display: flex; align-items: center; justify-content: flex-end; @@ -492,6 +527,20 @@ html[data-theme="kilo-vscode"] visibility: hidden; } +/* Placeholder content must not reserve title width. */ +.am-wt-actions-cell > .am-worktree-stats-skeleton { + position: absolute; + top: 0; + right: 0; + bottom: 0; +} + +.am-worktree-item:hover .am-worktree-branch, +.am-show-shortcuts .am-worktree-item:has(.am-shortcut-badge) .am-worktree-branch { + mask-image: linear-gradient(to right, black calc(100% - 48px), transparent 100%); + -webkit-mask-image: linear-gradient(to right, black calc(100% - 48px), transparent 100%); +} + /* Row 2: always present for consistent height; PR badge right-aligned */ .am-wt-row2 { display: flex; @@ -566,15 +615,13 @@ html[data-theme="kilo-vscode"] white-space: nowrap; } -.am-local-item .am-shortcut-badge { - right: 8px; -} - -.am-local-item:hover .am-shortcut-badge { +.am-local-item:hover .am-wt-hover-actions { opacity: 1; + visibility: visible; } -.am-local-item:hover .am-local-branch { +.am-local-item:hover .am-local-branch, +.am-show-shortcuts .am-local-item:has(.am-shortcut-badge) .am-local-branch { mask-image: linear-gradient(to right, black calc(100% - 40px), transparent 100%); -webkit-mask-image: linear-gradient(to right, black calc(100% - 40px), transparent 100%); } @@ -606,9 +653,6 @@ html[data-theme="kilo-vscode"] opacity: 0; visibility: hidden; } -.am-show-shortcuts .am-local-item .am-shortcut-badge { - opacity: 1; -} .am-worktree-item:has(.am-worktree-rename-input) .am-wt-row2 { display: none; @@ -986,6 +1030,7 @@ html[data-theme="kilo-vscode"] .am-local-item:hover .am-worktree-stats, .am-local-item:hover .am-worktree-stats-skeleton { opacity: 0; + visibility: hidden; } /* User-defined sections — collapsible, color-coded groups */ @@ -2828,6 +2873,11 @@ body.am-wt-dragging-active * { overflow: visible; } +[data-component="dialog"]:has(.am-nv-project-inline [data-component="popover-content"]) [data-slot="dialog-content"], +[data-component="dialog"]:has(.am-nv-project-inline [data-component="popover-content"]) [data-slot="dialog-body"] { + overflow: visible; +} + .am-slash-command-dropdown { min-width: 0; max-width: none; @@ -3616,6 +3666,22 @@ body.am-wt-dragging-active * { color: var(--text-on-interactive-base) !important; } +/* Project scope selector — inline with the New/Import tabs */ + +.am-nv-project-inline { + display: flex; + align-items: center; + flex-shrink: 0; + flex: 0 1 260px; + min-width: 0; + margin-left: auto; +} + +.am-nv-project-inline .am-selector-wrapper { + width: 100%; + min-width: 0; +} + /* Import tab layout */ .am-import-tab { @@ -3867,6 +3933,68 @@ body.am-wt-dragging-active * { color: var(--text-weaker); } +/* Project option rows in the New Worktree project dropdown. */ + +.am-project-option { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + width: 100%; + padding: 6px 8px; + border: none; + border-radius: var(--radius-sm); + background: none; + color: var(--text-base); + font-size: var(--font-size-base); + font-family: inherit; + text-align: left; + cursor: pointer; +} + +.am-project-option:hover:not(:disabled) { + background: var(--surface-inset-base-hover); +} + +.am-project-option-active { + background: var(--surface-inset-base); +} + +.am-project-option:disabled { + opacity: 0.5; + cursor: default; +} + +.am-project-option-left { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + flex: 1; +} + +.am-project-option-left [data-component="icon"] { + color: var(--text-weaker); + flex-shrink: 0; +} + +.am-project-option-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex-shrink: 0; + max-width: 45%; +} + +.am-project-option-root { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; + font-size: var(--kilo-font-size-11); + color: var(--text-weaker); +} + /* Import empty state */ .am-import-empty { @@ -4632,6 +4760,10 @@ body.vscode-high-contrast-light { outline: 1px solid var(--vscode-contrastActiveBorder, var(--vscode-focusBorder)); outline-offset: -1px; } + + .am-terminal-host:focus-within { + box-shadow: inset 0 0 0 1px var(--vscode-contrastActiveBorder, var(--vscode-focusBorder)); + } } /* Context menu — restyle to match dropdown-menu visuals. @@ -4677,6 +4809,10 @@ body.vscode-high-contrast-light { opacity: 0.7; } +.am-tab-terminal-focused { + background: var(--surface-base-hover); +} + /* * Stacking layout for xterm terminal tabs. * @@ -4879,6 +5015,10 @@ body.vscode-high-contrast-light { background: var(--vscode-terminal-background, #1e1e1e); } +.am-terminal-host:focus-within { + box-shadow: inset 0 0 0 1px var(--border-focus, var(--vscode-focusBorder)); +} + /* Third-party xterm classes — addressed by attribute selector so the agent-manager "am-* prefix" architecture test does not flag them. FitAddon subtracts padding from xterm itself, not its parent host. */ diff --git a/packages/kilo-vscode/webview-ui/agent-manager/focus.ts b/packages/kilo-vscode/webview-ui/agent-manager/focus.ts index 0553003b306..91167be26f7 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/focus.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/focus.ts @@ -12,7 +12,7 @@ export function createChatFocus(deps: { review: () => boolean }) { const focus = (force: boolean) => { - if ((!force && !document.hasFocus()) || deps.term() || deps.history() || deps.review()) return + if ((!force && (!document.hasFocus() || deps.term())) || deps.history() || deps.review()) return if (preservesTextFocus(document.activeElement)) return if (!force && document.activeElement?.matches('[role="tab"]')) return if (!force && document.activeElement?.closest('[data-component="question-dock"]')) return diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts index 586079b908d..726e132d709 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts @@ -99,7 +99,7 @@ export const dict = { "agentManager.shortcuts.nextTab": "علامة التبويب التالية", "agentManager.shortcuts.newTab": "علامة تبويب جديدة", "agentManager.shortcuts.closeTab": "إغلاق علامة التبويب", - "agentManager.shortcuts.toggleTerminal": "تبديل الطرفية", + "agentManager.shortcuts.toggleTerminal": "التركيز على الطرفية / إخفاء الطرفية", "agentManager.shortcuts.runScript": "تشغيل السكربت", "agentManager.run.options": "خيارات التشغيل", "agentManager.run.configure": "تكوين سكربت التشغيل", @@ -114,6 +114,9 @@ export const dict = { "agentManager.dialog.removeStaleWorktree.cancel": "إلغاء", "agentManager.dialog.removeStaleWorktree.confirm": "إزالة Worktree القديم", + "agentManager.dialog.project.select": "اختيار مشروع", + "agentManager.dialog.project.untrusted": "يُرجى الوثوق بهذا المشروع من الشريط الجانبي أولًا", + "agentManager.dialog.project.missing": "المستودع غير موجود", "agentManager.dialog.openWorktree": "شجرة عمل جديدة", "agentManager.dialog.configureWorktree": "تكوين Worktree جديد...", "agentManager.dialog.tab.new": "جديد", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts index 64ad7cc61a9..8ff91cfdff6 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts @@ -101,7 +101,7 @@ export const dict = { "agentManager.shortcuts.nextTab": "Próxima aba", "agentManager.shortcuts.newTab": "Nova aba", "agentManager.shortcuts.closeTab": "Fechar aba", - "agentManager.shortcuts.toggleTerminal": "Alternar terminal", + "agentManager.shortcuts.toggleTerminal": "Focar / ocultar terminal", "agentManager.shortcuts.runScript": "Executar script", "agentManager.run.options": "Opções de execução", "agentManager.run.configure": "Configurar script de execução", @@ -116,6 +116,9 @@ export const dict = { "agentManager.dialog.removeStaleWorktree.cancel": "Cancelar", "agentManager.dialog.removeStaleWorktree.confirm": "Remover Worktree obsoleto", + "agentManager.dialog.project.select": "Selecionar projeto", + "agentManager.dialog.project.untrusted": "Primeiro, confie neste projeto na barra lateral", + "agentManager.dialog.project.missing": "Repositório não encontrado", "agentManager.dialog.openWorktree": "Novo Worktree", "agentManager.dialog.configureWorktree": "Configurar Novo Worktree...", "agentManager.dialog.tab.new": "Novo", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts index a42401b977e..10cd591c258 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts @@ -101,7 +101,7 @@ export const dict = { "agentManager.shortcuts.nextTab": "Sljedeća kartica", "agentManager.shortcuts.newTab": "Nova kartica", "agentManager.shortcuts.closeTab": "Zatvori karticu", - "agentManager.shortcuts.toggleTerminal": "Prebaci terminal", + "agentManager.shortcuts.toggleTerminal": "Fokusiraj / sakrij terminal", "agentManager.shortcuts.runScript": "Pokreni skriptu", "agentManager.run.options": "Opcije pokretanja", "agentManager.run.configure": "Konfiguriši skriptu za pokretanje", @@ -116,6 +116,9 @@ export const dict = { "agentManager.dialog.removeStaleWorktree.cancel": "Otkaži", "agentManager.dialog.removeStaleWorktree.confirm": "Ukloni zastarjeli Worktree", + "agentManager.dialog.project.select": "Odaberi projekat", + "agentManager.dialog.project.untrusted": "Prvo vjeruj ovom projektu na bočnoj traci", + "agentManager.dialog.project.missing": "Repozitorij nije pronađen", "agentManager.dialog.openWorktree": "Novi worktree", "agentManager.dialog.configureWorktree": "Konfiguriši Novi Worktree...", "agentManager.dialog.tab.new": "Novo", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts index 705758f1b94..d715e408088 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts @@ -102,7 +102,7 @@ export const dict = { "agentManager.shortcuts.nextTab": "Næste fane", "agentManager.shortcuts.newTab": "Ny fane", "agentManager.shortcuts.closeTab": "Luk fane", - "agentManager.shortcuts.toggleTerminal": "Skift terminal", + "agentManager.shortcuts.toggleTerminal": "Fokusér / skjul terminalen", "agentManager.shortcuts.runScript": "Kør script", "agentManager.run.options": "Kørselsindstillinger", "agentManager.run.configure": "Konfigurer kørselsscript", @@ -117,6 +117,9 @@ export const dict = { "agentManager.dialog.removeStaleWorktree.cancel": "Annuller", "agentManager.dialog.removeStaleWorktree.confirm": "Fjern forældet Worktree", + "agentManager.dialog.project.select": "Vælg projekt", + "agentManager.dialog.project.untrusted": "Godkend først dette projekt i sidepanelet", + "agentManager.dialog.project.missing": "Repository ikke fundet", "agentManager.dialog.openWorktree": "Ny Worktree", "agentManager.dialog.configureWorktree": "Konfigurer Nyt Worktree...", "agentManager.dialog.tab.new": "Ny", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts index 234e551f1aa..f60caf8070b 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts @@ -103,7 +103,7 @@ export const dict = { "agentManager.shortcuts.nextTab": "Nächster Tab", "agentManager.shortcuts.newTab": "Neuer Tab", "agentManager.shortcuts.closeTab": "Tab schließen", - "agentManager.shortcuts.toggleTerminal": "Terminal umschalten", + "agentManager.shortcuts.toggleTerminal": "Terminal fokussieren / ausblenden", "agentManager.shortcuts.runScript": "Skript ausführen", "agentManager.run.options": "Ausführungsoptionen", "agentManager.run.configure": "Ausführungsskript konfigurieren", @@ -118,6 +118,9 @@ export const dict = { "agentManager.dialog.removeStaleWorktree.cancel": "Abbrechen", "agentManager.dialog.removeStaleWorktree.confirm": "Veralteten Worktree entfernen", + "agentManager.dialog.project.select": "Projekt auswählen", + "agentManager.dialog.project.untrusted": "Vertrauen Sie diesem Projekt zuerst in der Seitenleiste", + "agentManager.dialog.project.missing": "Repository nicht gefunden", "agentManager.dialog.openWorktree": "Neuer Worktree", "agentManager.dialog.configureWorktree": "Neuen Worktree konfigurieren...", "agentManager.dialog.tab.new": "Neu", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts index 56954f2c7ac..db68e9bd7dc 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts @@ -105,7 +105,7 @@ export const dict = { "agentManager.shortcuts.nextTab": "Next tab", "agentManager.shortcuts.newTab": "New tab", "agentManager.shortcuts.closeTab": "Close tab", - "agentManager.shortcuts.toggleTerminal": "Toggle terminal", + "agentManager.shortcuts.toggleTerminal": "Focus / hide terminal", "agentManager.shortcuts.runScript": "Run script", "agentManager.run.options": "Run options", "agentManager.run.configure": "Configure run script", @@ -125,6 +125,9 @@ export const dict = { "agentManager.dialog.tab.new": "New", "agentManager.dialog.tab.import": "Import", "agentManager.dialog.namePlaceholder": "Worktree name (optional)", + "agentManager.dialog.project.select": "Select project", + "agentManager.dialog.project.untrusted": "Trust this project in the sidebar first", + "agentManager.dialog.project.missing": "Repository not found", "agentManager.dialog.promptPlaceholder.mac": "Type a message (\u2318Enter to send)", "agentManager.dialog.promptPlaceholder.other": "Type a message (Ctrl+Enter to send)", "agentManager.dialog.advancedOptions": "Advanced options", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts index f1ec88f48c3..f0173a673fd 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts @@ -102,7 +102,7 @@ export const dict = { "agentManager.shortcuts.nextTab": "Siguiente pestaña", "agentManager.shortcuts.newTab": "Nueva pestaña", "agentManager.shortcuts.closeTab": "Cerrar pestaña", - "agentManager.shortcuts.toggleTerminal": "Alternar terminal", + "agentManager.shortcuts.toggleTerminal": "Enfocar / ocultar la terminal", "agentManager.shortcuts.runScript": "Ejecutar script", "agentManager.run.options": "Opciones de ejecución", "agentManager.run.configure": "Configurar script de ejecución", @@ -117,6 +117,9 @@ export const dict = { "agentManager.dialog.removeStaleWorktree.cancel": "Cancelar", "agentManager.dialog.removeStaleWorktree.confirm": "Eliminar Worktree obsoleto", + "agentManager.dialog.project.select": "Seleccionar proyecto", + "agentManager.dialog.project.untrusted": "Confía primero en este proyecto desde la barra lateral", + "agentManager.dialog.project.missing": "Repositorio no encontrado", "agentManager.dialog.openWorktree": "Nuevo Worktree", "agentManager.dialog.configureWorktree": "Configurar Nuevo Worktree...", "agentManager.dialog.tab.new": "Nuevo", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fa.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fa.ts index e128e95a674..b5d50a2b430 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fa.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fa.ts @@ -105,7 +105,7 @@ export const dict = { "agentManager.shortcuts.nextTab": "تب بعدی", "agentManager.shortcuts.newTab": "تب جدید", "agentManager.shortcuts.closeTab": "بستن تب", - "agentManager.shortcuts.toggleTerminal": "نمایش/پنهان کردن ترمینال", + "agentManager.shortcuts.toggleTerminal": "تمرکز روی ترمینال / پنهان کردن ترمینال", "agentManager.shortcuts.runScript": "اجرای اسکریپت", "agentManager.run.options": "گزینه‌های اجرا", "agentManager.run.configure": "پیکربندی اسکریپت اجرا", @@ -121,6 +121,9 @@ export const dict = { "agentManager.dialog.removeStaleWorktree.cancel": "لغو", "agentManager.dialog.removeStaleWorktree.confirm": "حذف Worktree قدیمی", + "agentManager.dialog.project.select": "انتخاب پروژه", + "agentManager.dialog.project.untrusted": "ابتدا در نوار کناری به این پروژه اعتماد کنید", + "agentManager.dialog.project.missing": "مخزن یافت نشد", "agentManager.dialog.openWorktree": "Worktree جدید", "agentManager.dialog.tab.new": "جدید", "agentManager.dialog.tab.import": "وارد کردن", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts index 3a2f7871314..544b5007a2c 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts @@ -102,7 +102,7 @@ export const dict = { "agentManager.shortcuts.nextTab": "Onglet suivant", "agentManager.shortcuts.newTab": "Nouvel onglet", "agentManager.shortcuts.closeTab": "Fermer l'onglet", - "agentManager.shortcuts.toggleTerminal": "Basculer le terminal", + "agentManager.shortcuts.toggleTerminal": "Focaliser / masquer le terminal", "agentManager.shortcuts.runScript": "Exécuter le script", "agentManager.run.options": "Options d'exécution", "agentManager.run.configure": "Configurer le script d'exécution", @@ -117,6 +117,9 @@ export const dict = { "agentManager.dialog.removeStaleWorktree.cancel": "Annuler", "agentManager.dialog.removeStaleWorktree.confirm": "Supprimer le Worktree obsolète", + "agentManager.dialog.project.select": "Sélectionner un projet", + "agentManager.dialog.project.untrusted": "Approuvez d'abord ce projet dans la barre latérale", + "agentManager.dialog.project.missing": "Dépôt introuvable", "agentManager.dialog.openWorktree": "Nouveau worktree", "agentManager.dialog.configureWorktree": "Configurer un Nouveau Worktree...", "agentManager.dialog.tab.new": "Nouveau", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts index 8f0cfabc3d1..59f6ea340c6 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts @@ -107,7 +107,7 @@ export const dict = { "agentManager.shortcuts.nextTab": "Scheda successiva", "agentManager.shortcuts.newTab": "Nuova scheda", "agentManager.shortcuts.closeTab": "Chiudi scheda", - "agentManager.shortcuts.toggleTerminal": "Mostra/nascondi terminale", + "agentManager.shortcuts.toggleTerminal": "Metti a fuoco / nascondi il terminale", "agentManager.shortcuts.runScript": "Esegui script", "agentManager.run.options": "Opzioni di esecuzione", "agentManager.run.configure": "Configura script di esecuzione", @@ -123,6 +123,9 @@ export const dict = { "agentManager.dialog.removeStaleWorktree.cancel": "Annulla", "agentManager.dialog.removeStaleWorktree.confirm": "Rimuovi worktree obsoleto", + "agentManager.dialog.project.select": "Seleziona progetto", + "agentManager.dialog.project.untrusted": "Prima, fidati di questo progetto nella barra laterale", + "agentManager.dialog.project.missing": "Repository non trovata", "agentManager.dialog.openWorktree": "Nuovo worktree", "agentManager.dialog.tab.new": "Nuovo", "agentManager.dialog.tab.import": "Importa", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts index c689b6ac005..5e4dd806ef5 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts @@ -102,7 +102,7 @@ export const dict = { "agentManager.shortcuts.nextTab": "次のタブ", "agentManager.shortcuts.newTab": "新しいタブ", "agentManager.shortcuts.closeTab": "タブを閉じる", - "agentManager.shortcuts.toggleTerminal": "ターミナルの切り替え", + "agentManager.shortcuts.toggleTerminal": "ターミナルにフォーカス / 非表示にする", "agentManager.shortcuts.runScript": "スクリプトを実行", "agentManager.run.options": "実行オプション", "agentManager.run.configure": "実行スクリプトを設定", @@ -117,6 +117,9 @@ export const dict = { "agentManager.dialog.removeStaleWorktree.cancel": "キャンセル", "agentManager.dialog.removeStaleWorktree.confirm": "無効な Worktree を削除", + "agentManager.dialog.project.select": "プロジェクトを選択", + "agentManager.dialog.project.untrusted": "まずサイドバーでこのプロジェクトを信頼してください", + "agentManager.dialog.project.missing": "リポジトリが見つかりません", "agentManager.dialog.openWorktree": "新規ワークツリー", "agentManager.dialog.configureWorktree": "新規 Worktree の構成...", "agentManager.dialog.tab.new": "新規", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts index 28855e0bb7b..b729493b1d3 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts @@ -100,7 +100,7 @@ export const dict = { "agentManager.shortcuts.nextTab": "다음 탭", "agentManager.shortcuts.newTab": "새 탭", "agentManager.shortcuts.closeTab": "탭 닫기", - "agentManager.shortcuts.toggleTerminal": "터미널 전환", + "agentManager.shortcuts.toggleTerminal": "터미널에 포커스 / 터미널 숨기기", "agentManager.shortcuts.runScript": "스크립트 실행", "agentManager.run.options": "실행 옵션", "agentManager.run.configure": "실행 스크립트 구성", @@ -115,6 +115,9 @@ export const dict = { "agentManager.dialog.removeStaleWorktree.cancel": "취소", "agentManager.dialog.removeStaleWorktree.confirm": "오래된 Worktree 제거", + "agentManager.dialog.project.select": "프로젝트 선택", + "agentManager.dialog.project.untrusted": "먼저 사이드바에서 이 프로젝트를 신뢰하세요", + "agentManager.dialog.project.missing": "저장소를 찾을 수 없음", "agentManager.dialog.openWorktree": "새 워크트리", "agentManager.dialog.configureWorktree": "새 Worktree 구성...", "agentManager.dialog.tab.new": "새로 만들기", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts index 81a003f9f03..3ffc6c7dce5 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts @@ -106,7 +106,7 @@ export const dict = { "agentManager.shortcuts.nextTab": "Volgend tabblad", "agentManager.shortcuts.newTab": "Nieuw tabblad", "agentManager.shortcuts.closeTab": "Tabblad sluiten", - "agentManager.shortcuts.toggleTerminal": "Terminal in-/uitschakelen", + "agentManager.shortcuts.toggleTerminal": "Terminal focussen / verbergen", "agentManager.shortcuts.runScript": "Script uitvoeren", "agentManager.run.options": "Uitvoeropties", "agentManager.run.configure": "Uitvoerscript configureren", @@ -122,6 +122,9 @@ export const dict = { "agentManager.dialog.removeStaleWorktree.cancel": "Annuleren", "agentManager.dialog.removeStaleWorktree.confirm": "Verouderde worktree verwijderen", + "agentManager.dialog.project.select": "Project selecteren", + "agentManager.dialog.project.untrusted": "Vertrouw dit project eerst in de zijbalk", + "agentManager.dialog.project.missing": "Repository niet gevonden", "agentManager.dialog.openWorktree": "Nieuwe worktree", "agentManager.dialog.configureWorktree": "Nieuwe Worktree Configureren...", "agentManager.dialog.tab.new": "Nieuw", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts index 4a0946996f5..d5dfa02a817 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts @@ -100,7 +100,7 @@ export const dict = { "agentManager.shortcuts.nextTab": "Neste fane", "agentManager.shortcuts.newTab": "Ny fane", "agentManager.shortcuts.closeTab": "Lukk fane", - "agentManager.shortcuts.toggleTerminal": "Veksle terminal", + "agentManager.shortcuts.toggleTerminal": "Fokuser / skjul terminalen", "agentManager.shortcuts.runScript": "Kjør skript", "agentManager.run.options": "Kjøringsalternativer", "agentManager.run.configure": "Konfigurer kjøreskript", @@ -115,6 +115,9 @@ export const dict = { "agentManager.dialog.removeStaleWorktree.cancel": "Avbryt", "agentManager.dialog.removeStaleWorktree.confirm": "Fjern utdatert Worktree", + "agentManager.dialog.project.select": "Velg prosjekt", + "agentManager.dialog.project.untrusted": "Stol på dette prosjektet i sidepanelet først", + "agentManager.dialog.project.missing": "Repository ikke funnet", "agentManager.dialog.openWorktree": "Ny worktree", "agentManager.dialog.configureWorktree": "Konfigurer Nytt Worktree...", "agentManager.dialog.tab.new": "Ny", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts index 47ba2cbffbe..9fb33a5c980 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts @@ -102,7 +102,7 @@ export const dict = { "agentManager.shortcuts.nextTab": "Następna karta", "agentManager.shortcuts.newTab": "Nowa karta", "agentManager.shortcuts.closeTab": "Zamknij kartę", - "agentManager.shortcuts.toggleTerminal": "Przełącz terminal", + "agentManager.shortcuts.toggleTerminal": "Przenieś fokus do terminala / ukryj terminal", "agentManager.shortcuts.runScript": "Uruchom skrypt", "agentManager.run.options": "Opcje uruchamiania", "agentManager.run.configure": "Konfiguruj skrypt uruchamiania", @@ -117,6 +117,9 @@ export const dict = { "agentManager.dialog.removeStaleWorktree.cancel": "Anuluj", "agentManager.dialog.removeStaleWorktree.confirm": "Usuń nieaktualny Worktree", + "agentManager.dialog.project.select": "Wybierz projekt", + "agentManager.dialog.project.untrusted": "Najpierw zaufaj temu projektowi na pasku bocznym", + "agentManager.dialog.project.missing": "Nie znaleziono repozytorium", "agentManager.dialog.openWorktree": "Nowy Worktree", "agentManager.dialog.configureWorktree": "Skonfiguruj Nowe Worktree...", "agentManager.dialog.tab.new": "Nowy", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts index 52cf8082e0c..74afb2c7aa3 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts @@ -102,7 +102,7 @@ export const dict = { "agentManager.shortcuts.nextTab": "Следующая вкладка", "agentManager.shortcuts.newTab": "Новая вкладка", "agentManager.shortcuts.closeTab": "Закрыть вкладку", - "agentManager.shortcuts.toggleTerminal": "Переключить терминал", + "agentManager.shortcuts.toggleTerminal": "Перевести фокус на терминал / скрыть терминал", "agentManager.shortcuts.runScript": "Запустить скрипт", "agentManager.run.options": "Параметры запуска", "agentManager.run.configure": "Настроить скрипт запуска", @@ -117,6 +117,9 @@ export const dict = { "agentManager.dialog.removeStaleWorktree.cancel": "Отмена", "agentManager.dialog.removeStaleWorktree.confirm": "Удалить устаревший Worktree", + "agentManager.dialog.project.select": "Выбрать проект", + "agentManager.dialog.project.untrusted": "Сначала подтвердите доверие к этому проекту на боковой панели", + "agentManager.dialog.project.missing": "Репозиторий не найден", "agentManager.dialog.openWorktree": "Новый worktree", "agentManager.dialog.configureWorktree": "Настроить новое Worktree...", "agentManager.dialog.tab.new": "Новый", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts index 320a1bebbc3..b65655f053c 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts @@ -97,7 +97,7 @@ export const dict = { "agentManager.shortcuts.nextTab": "แท็บถัดไป", "agentManager.shortcuts.newTab": "แท็บใหม่", "agentManager.shortcuts.closeTab": "ปิดแท็บ", - "agentManager.shortcuts.toggleTerminal": "สลับเทอร์มินัล", + "agentManager.shortcuts.toggleTerminal": "โฟกัสเทอร์มินัล / ซ่อนเทอร์มินัล", "agentManager.shortcuts.runScript": "เรียกใช้สคริปต์", "agentManager.run.options": "ตัวเลือกการเรียกใช้", "agentManager.run.configure": "กำหนดค่าสคริปต์การเรียกใช้", @@ -112,6 +112,9 @@ export const dict = { "agentManager.dialog.removeStaleWorktree.cancel": "ยกเลิก", "agentManager.dialog.removeStaleWorktree.confirm": "ลบ Worktree ที่ล้าสมัย", + "agentManager.dialog.project.select": "เลือกโปรเจกต์", + "agentManager.dialog.project.untrusted": "โปรดเชื่อถือโปรเจกต์นี้ในแถบด้านข้างก่อน", + "agentManager.dialog.project.missing": "ไม่พบ Repository", "agentManager.dialog.openWorktree": "Worktree ใหม่", "agentManager.dialog.configureWorktree": "กำหนดค่า Worktree ใหม่...", "agentManager.dialog.tab.new": "ใหม่", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts index d02a01e95e7..43b033d32a9 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts @@ -107,7 +107,7 @@ export const dict = { "agentManager.shortcuts.nextTab": "Sonraki sekme", "agentManager.shortcuts.newTab": "Yeni sekme", "agentManager.shortcuts.closeTab": "Sekmeyi kapat", - "agentManager.shortcuts.toggleTerminal": "Terminali aç/kapat", + "agentManager.shortcuts.toggleTerminal": "Terminale odaklan / terminali gizle", "agentManager.shortcuts.runScript": "Betiği çalıştır", "agentManager.run.options": "Çalıştırma seçenekleri", "agentManager.run.configure": "Çalıştırma betiğini yapılandır", @@ -123,6 +123,9 @@ export const dict = { "agentManager.dialog.removeStaleWorktree.cancel": "İptal", "agentManager.dialog.removeStaleWorktree.confirm": "Eskimiş worktree'yi kaldır", + "agentManager.dialog.project.select": "Proje seç", + "agentManager.dialog.project.untrusted": "Önce kenar çubuğunda bu projeye güvenin", + "agentManager.dialog.project.missing": "Depo bulunamadı", "agentManager.dialog.openWorktree": "Yeni Worktree", "agentManager.dialog.configureWorktree": "Yeni Worktree Yapılandır...", "agentManager.dialog.tab.new": "Yeni", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts index 54587c0c96a..0242498ddf5 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts @@ -108,7 +108,7 @@ export const dict = { "agentManager.shortcuts.nextTab": "Наступна вкладка", "agentManager.shortcuts.newTab": "Нова вкладка", "agentManager.shortcuts.closeTab": "Закрити вкладку", - "agentManager.shortcuts.toggleTerminal": "Перемкнути термінал", + "agentManager.shortcuts.toggleTerminal": "Перейти до термінала / приховати термінал", "agentManager.shortcuts.runScript": "Запустити скрипт", "agentManager.run.options": "Параметри запуску", "agentManager.run.configure": "Налаштувати скрипт запуску", @@ -124,6 +124,9 @@ export const dict = { "agentManager.dialog.removeStaleWorktree.cancel": "Скасувати", "agentManager.dialog.removeStaleWorktree.confirm": "Видалити застаріле робоче дерево", + "agentManager.dialog.project.select": "Вибрати проєкт", + "agentManager.dialog.project.untrusted": "Спочатку підтвердьте, що довіряєте цьому проєкту, на бічній панелі", + "agentManager.dialog.project.missing": "Репозиторій не знайдено", "agentManager.dialog.openWorktree": "Нове робоче дерево", "agentManager.dialog.configureWorktree": "Налаштувати нове Worktree...", "agentManager.dialog.tab.new": "Нове", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts index 4732e36dd68..8bbcfd45263 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts @@ -97,7 +97,7 @@ export const dict = { "agentManager.shortcuts.nextTab": "下一个标签页", "agentManager.shortcuts.newTab": "新建标签页", "agentManager.shortcuts.closeTab": "关闭标签页", - "agentManager.shortcuts.toggleTerminal": "切换终端", + "agentManager.shortcuts.toggleTerminal": "聚焦终端 / 隐藏终端", "agentManager.shortcuts.runScript": "运行脚本", "agentManager.run.options": "运行选项", "agentManager.run.configure": "配置运行脚本", @@ -111,6 +111,9 @@ export const dict = { "agentManager.dialog.removeStaleWorktree.cancel": "取消", "agentManager.dialog.removeStaleWorktree.confirm": "移除失效 Worktree", + "agentManager.dialog.project.select": "选择项目", + "agentManager.dialog.project.untrusted": "请先在侧边栏中信任此项目", + "agentManager.dialog.project.missing": "未找到仓库", "agentManager.dialog.openWorktree": "新建工作树", "agentManager.dialog.configureWorktree": "配置新 Worktree...", "agentManager.dialog.tab.new": "新建", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts index 0357ac87df4..0acadafdbfe 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts @@ -97,7 +97,7 @@ export const dict = { "agentManager.shortcuts.nextTab": "下一個分頁", "agentManager.shortcuts.newTab": "新建分頁", "agentManager.shortcuts.closeTab": "關閉分頁", - "agentManager.shortcuts.toggleTerminal": "切換終端機", + "agentManager.shortcuts.toggleTerminal": "聚焦終端機 / 隱藏終端機", "agentManager.shortcuts.runScript": "執行指令碼", "agentManager.run.options": "執行選項", "agentManager.run.configure": "設定執行指令碼", @@ -111,6 +111,9 @@ export const dict = { "agentManager.dialog.removeStaleWorktree.cancel": "取消", "agentManager.dialog.removeStaleWorktree.confirm": "移除失效 Worktree", + "agentManager.dialog.project.select": "選擇專案", + "agentManager.dialog.project.untrusted": "請先在側邊欄信任此專案", + "agentManager.dialog.project.missing": "找不到儲存庫", "agentManager.dialog.openWorktree": "新建工作樹", "agentManager.dialog.configureWorktree": "配置新 Worktree...", "agentManager.dialog.tab.new": "新建", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/keybind-defaults.ts b/packages/kilo-vscode/webview-ui/agent-manager/keybind-defaults.ts new file mode 100644 index 00000000000..1890624bd7e --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/keybind-defaults.ts @@ -0,0 +1,32 @@ +const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent) + +const MAX_JUMP_INDEX = 9 + +/** Fallback keybindings before the extension sends resolved ones. */ +export const defaultBindings: Record = { + previousSession: isMac ? "⌘⌥↑" : "Ctrl+Alt+↑", + nextSession: isMac ? "⌘⌥↓" : "Ctrl+Alt+↓", + previousTab: isMac ? "⌘⌥←" : "Ctrl+Alt+←", + nextTab: isMac ? "⌘⌥→" : "Ctrl+Alt+→", + previousTerminal: isMac ? "⌘⇧[" : "Ctrl+Shift+[", + nextTerminal: isMac ? "⌘⇧]" : "Ctrl+Shift+]", + search: isMac ? "⌘F" : "Ctrl+F", + showTerminal: isMac ? "⌘/" : "Ctrl+/", + newTerminal: isMac ? "⌘⇧T" : "Ctrl+Shift+T", + runScript: isMac ? "⌘E" : "Ctrl+E", + toggleDiff: isMac ? "⌘D" : "Ctrl+D", + showShortcuts: isMac ? "⌘⇧/" : "Ctrl+Shift+/", + newTab: isMac ? "⌘T" : "Ctrl+T", + closeTab: isMac ? "⌘W" : "Ctrl+W", + newWorktree: isMac ? "⌘N" : "Ctrl+N", + quickWorktree: isMac ? "⌘⇧N" : "Ctrl+Shift+N", + closeWorktree: isMac ? "⌘⇧W" : "Ctrl+Shift+W", + openWorktree: isMac ? "⌘⇧O" : "Ctrl+Shift+O", + openPR: isMac ? "⌘⇧R" : "Ctrl+Shift+R", + agentManagerOpen: isMac ? "⌘⇧M" : "Ctrl+Shift+M", + cycleAgentMode: isMac ? "⌘." : "Ctrl+.", + cyclePreviousAgentMode: isMac ? "⌘⇧." : "Ctrl+Shift+.", + ...Object.fromEntries( + Array.from({ length: MAX_JUMP_INDEX }, (_, i) => [`jumpTo${i + 1}`, isMac ? `⌘${i + 1}` : `Ctrl+${i + 1}`]), + ), +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/pending-create.ts b/packages/kilo-vscode/webview-ui/agent-manager/pending-create.ts new file mode 100644 index 00000000000..4918e8f3fe2 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/pending-create.ts @@ -0,0 +1,34 @@ +import { createSignal } from "solid-js" + +/** + * Tracks a cross-project worktree creation so the target project is activated + * once its worktree is ready, and abandons the pending activation when the + * creation fails or completes through another flow. + */ +export function usePendingCreate( + active: () => string | undefined, + activate: (projectId: string, worktreeId: string) => void, +) { + const [pending, setPending] = createSignal<{ projectId: string }>() + + const schedule = (projectId: string) => { + if (projectId === active()) return + if (pending()) return + setPending({ projectId }) + } + + const abandon = (projectId?: string) => { + if (pending()?.projectId === projectId) setPending(undefined) + } + + const setup = (ev: { status: string; projectId?: string; worktreeId?: string }) => { + if (pending()?.projectId !== ev.projectId) return + if (ev.status === "ready" && ev.projectId && ev.worktreeId) { + setPending(undefined) + activate(ev.projectId, ev.worktreeId) + } + if (ev.status === "error") setPending(undefined) + } + + return { schedule, abandon, setup } +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/shortcuts.ts b/packages/kilo-vscode/webview-ui/agent-manager/shortcuts.ts index 6dca13dc229..356e57b3593 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/shortcuts.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/shortcuts.ts @@ -12,15 +12,16 @@ export function buildShortcutCategories( bindings: Record, t: (key: string, params?: Record) => string, ): ShortcutCategory[] { + const bind = (key: string) => bindings[key] ?? "" return [ { title: t("agentManager.shortcuts.category.quickSwitch"), shortcuts: [ - { label: t("agentManager.sidebarSearch.label"), binding: bindings.search ?? "" }, + { label: t("agentManager.sidebarSearch.label"), binding: bind("search") }, { label: t("agentManager.shortcuts.jumpToItem"), binding: (() => { - const first = bindings.jumpTo1 ?? "" + const first = bind("jumpTo1") const prefix = first.replace(/\d+$/, "") return prefix ? `${prefix}1-9` : "" })(), @@ -30,39 +31,48 @@ export function buildShortcutCategories( { title: t("agentManager.shortcuts.category.sidebar"), shortcuts: [ - { label: t("agentManager.shortcuts.previousItem"), binding: bindings.previousSession ?? "" }, - { label: t("agentManager.shortcuts.nextItem"), binding: bindings.nextSession ?? "" }, + { label: t("agentManager.shortcuts.previousItem"), binding: bind("previousSession") }, + { label: t("agentManager.shortcuts.nextItem"), binding: bind("nextSession") }, { label: t("agentManager.shortcuts.advancedWorktree"), binding: bindings.newWorktree ?? "" }, { label: t("agentManager.shortcuts.newWorktree"), binding: bindings.quickWorktree ?? "" }, - { label: t("agentManager.shortcuts.deleteWorktree"), binding: bindings.closeWorktree ?? "" }, - { label: t("agentManager.shortcuts.openWorktree"), binding: bindings.openWorktree ?? "" }, - { label: t("agentManager.shortcuts.openPR"), binding: bindings.openPR ?? "" }, + { label: t("agentManager.shortcuts.deleteWorktree"), binding: bind("closeWorktree") }, + { label: t("agentManager.shortcuts.openWorktree"), binding: bind("openWorktree") }, + { label: t("agentManager.shortcuts.openPR"), binding: bind("openPR") }, ], }, { title: t("agentManager.shortcuts.category.tabs"), shortcuts: [ - { label: t("agentManager.shortcuts.previousTab"), binding: bindings.previousTab ?? "" }, - { label: t("agentManager.shortcuts.nextTab"), binding: bindings.nextTab ?? "" }, - { label: t("agentManager.shortcuts.newTab"), binding: bindings.newTab ?? "" }, - { label: t("agentManager.shortcuts.closeTab"), binding: bindings.closeTab ?? "" }, + { label: t("agentManager.shortcuts.previousTab"), binding: bind("previousTab") }, + { label: t("agentManager.shortcuts.nextTab"), binding: bind("nextTab") }, + { label: t("agentManager.shortcuts.newTab"), binding: bind("newTab") }, + { label: t("agentManager.shortcuts.closeTab"), binding: bind("closeTab") }, ], }, { title: t("agentManager.shortcuts.category.terminal"), shortcuts: [ - { label: t("agentManager.shortcuts.toggleTerminal"), binding: bindings.showTerminal ?? "" }, - { label: t("agentManager.shortcuts.runScript"), binding: bindings.runScript ?? "" }, - { label: t("agentManager.shortcuts.toggleDiff"), binding: bindings.toggleDiff ?? "" }, + { label: t("agentManager.shortcuts.toggleTerminal"), binding: bind("showTerminal") }, + { label: t("agentManager.terminal.add"), binding: bind("newTerminal") }, + { + label: `${t("agentManager.shortcuts.previousTab")} (${t("agentManager.tab.terminal")})`, + binding: bind("previousTerminal"), + }, + { + label: `${t("agentManager.shortcuts.nextTab")} (${t("agentManager.tab.terminal")})`, + binding: bind("nextTerminal"), + }, + { label: t("agentManager.shortcuts.runScript"), binding: bind("runScript") }, + { label: t("agentManager.shortcuts.toggleDiff"), binding: bind("toggleDiff") }, ], }, { title: t("agentManager.shortcuts.category.global"), shortcuts: [ - { label: t("agentManager.shortcuts.openAgentManager"), binding: bindings.agentManagerOpen ?? "" }, - { label: t("agentManager.shortcuts.cycleAgentMode"), binding: bindings.cycleAgentMode ?? "" }, - { label: t("agentManager.shortcuts.cyclePreviousAgentMode"), binding: bindings.cyclePreviousAgentMode ?? "" }, - { label: t("agentManager.shortcuts.showShortcuts"), binding: bindings.showShortcuts ?? "" }, + { label: t("agentManager.shortcuts.openAgentManager"), binding: bind("agentManagerOpen") }, + { label: t("agentManager.shortcuts.cycleAgentMode"), binding: bind("cycleAgentMode") }, + { label: t("agentManager.shortcuts.cyclePreviousAgentMode"), binding: bind("cyclePreviousAgentMode") }, + { label: t("agentManager.shortcuts.showShortcuts"), binding: bind("showShortcuts") }, ].filter((s) => s.binding), }, ].filter((c) => c.shortcuts.length > 0) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/terminal/SideTerminalPanel.tsx b/packages/kilo-vscode/webview-ui/agent-manager/terminal/SideTerminalPanel.tsx index 772cf76e208..b5986463466 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/terminal/SideTerminalPanel.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/terminal/SideTerminalPanel.tsx @@ -30,7 +30,7 @@ import type { DragEvent } from "@thisbeyond/solid-dnd" import { IconButton } from "@kilocode/kilo-ui/icon-button" import { Button } from "@kilocode/kilo-ui/button" import { Spinner } from "@kilocode/kilo-ui/spinner" -import { Tooltip } from "@kilocode/kilo-ui/tooltip" +import { Tooltip, TooltipKeybind } from "@kilocode/kilo-ui/tooltip" import { useLanguage } from "../../src/context/language" import { ConstrainDragYAxis } from "../../src/components/chat/TabDnd" import { useTabScroll } from "../../src/utils/tab-scroll" @@ -57,8 +57,11 @@ interface Props { onCloseOthers: (terminalId: string) => void /** Create a new side terminal for this context. */ onStart: () => void + nextKeybind: string + closeKeybind: string /** Deliberately stop a running script terminal. */ onStop: (terminalId: string) => void + onFocusPrompt: () => void } export const SideTerminalPanel: Component = (props) => { @@ -170,7 +173,10 @@ export const SideTerminalPanel: Component = (props) => { label={props.state.title(term.id) ?? term.title} tooltip={props.state.title(term.id) ?? term.title} status={props.state.scriptStatus(term.id)} + keybind={active() === term.id ? "" : props.nextKeybind} + closeKeybind={props.closeKeybind} active={active() === term.id} + focused={props.state.sideFocusedId() === term.id} role="tab" selected={active() === term.id} tabIndex={active() === term.id ? 0 : -1} @@ -226,7 +232,12 @@ export const SideTerminalPanel: Component = (props) => {
- {renderSideTerminalLayer({ state: props.state, contextKey: props.contextKey, visible: props.visible })} + {renderSideTerminalLayer({ + state: props.state, + contextKey: props.contextKey, + visible: props.visible, + onFocusPrompt: props.onFocusPrompt, + })}
diff --git a/packages/kilo-vscode/webview-ui/agent-manager/terminal/SortableTerminalTab.tsx b/packages/kilo-vscode/webview-ui/agent-manager/terminal/SortableTerminalTab.tsx index 03d6fdaec83..99b25001d2d 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/terminal/SortableTerminalTab.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/terminal/SortableTerminalTab.tsx @@ -27,6 +27,7 @@ export const TerminalTabChrome: Component<{ status?: ScriptTerminalStatus keybind?: string closeKeybind?: string + focused?: boolean active: boolean role?: "tab" selected?: boolean @@ -46,7 +47,9 @@ export const TerminalTabChrome: Component<{ return "console" } return ( -
+
void + /** Handle the Agent Manager prompt shortcut locally because xterm's + * textarea does not reliably forward custom commands to the workbench. */ + onFocusPrompt?: () => void /** Reports OSC window-title escape codes (`ESC ] 0/1/2 ; title BEL`) * sent by the shell or running programs — fish sets it to the active * command, oh-my-zsh to user@host:cwd, vim to the file name. The @@ -131,7 +134,7 @@ function isAgentManagerShortcut(e: KeyboardEvent): boolean { const key = e.key.toLowerCase() if (e.altKey && ["arrowleft", "arrowright", "arrowup", "arrowdown"].includes(key)) return true if (["t", "w", "n", "d", "e", "f"].includes(key)) return true - if (e.shiftKey && ["w", "n", "o", "r", "m", "/", "?"].includes(key)) return true + if (e.shiftKey && ["t", "w", "n", "o", "r", "m", "[", "]", "/", "?"].includes(key)) return true if (/^[1-9]$/.test(key)) return true if (key === "/") return true return false @@ -149,8 +152,12 @@ export const TerminalTab: Component = (props) => { onMount(() => { const term = new Terminal({ - convertEol: true, + // PTY output already contains terminal line endings. Converting LF to + // CRLF here corrupts raw PTY output and is especially visible when a + // narrow terminal wraps and redraws the prompt. + convertEol: false, cursorBlink: true, + cursorInactiveStyle: "outline", fontFamily: props.font.fontFamily, fontSize: props.font.fontSize, scrollback: 5000, @@ -160,21 +167,23 @@ export const TerminalTab: Component = (props) => { const fit = new FitAddon() term.loadAddon(fit) term.open(host) - // Fit on the next frame — `host` might still have 0px dimensions - // during the initial layout pass otherwise. - requestAnimationFrame(() => { - try { - fit.fit() - } catch (err) { - // Host still detached at mount time. ResizeObserver will retry - // once layout kicks in. Logged so regressions don't hide. - log("initial fit() threw", err) - } - }) + // Unicode width must be configured before the first PTY bytes are parsed. + // Loading it later can leave already-wrapped graphemes with stale cell + // widths, which moves the cursor in narrow terminals. + term.loadAddon(new UnicodeGraphemesAddon()) + term.unicode.activeVersion = "15-graphemes" - // Pass agent-manager hotkeys through to the parent key handler so - // ⌘T / ⌘W / ⌘⌥← etc. still work while the terminal is focused. - term.attachCustomKeyEventHandler((event) => !isAgentManagerShortcut(event)) + // Pass Agent Manager hotkeys through to the parent key handler so + // ⌘T / ⌘⇧T / ⌘W / terminal cycling / ⌘⌥← still work while focused. + term.attachCustomKeyEventHandler((event) => { + const prompt = + (event.metaKey || event.ctrlKey) && event.shiftKey && !event.altKey && event.key.toLowerCase() === "m" + if (prompt) { + if (event.type === "keydown") props.onFocusPrompt?.() + return false + } + return !isAgentManagerShortcut(event) + }) // Track DOM focus so the state layer knows which terminal holds the // cursor (drives Cmd+W targeting). focusout is ignored when focus @@ -344,11 +353,9 @@ export const TerminalTab: Component = (props) => { } const disposeData = term.onData(send) const disposeBinary = term.onBinary(send) - open(props.wsUrl) - // These addons are not needed to paint the initial prompt. Defer them - // until after the first frame so their startup work, especially the - // Unicode 15 width tables, does not delay the shell connection. + // until after the first frame so their startup work does not delay the + // shell connection. const loadAddons = () => { deferred = undefined if (closed) return @@ -363,30 +370,24 @@ export const TerminalTab: Component = (props) => { ) // OSC 52 clipboard support for shell programs such as tmux and neovim. term.loadAddon(new ClipboardAddon()) - // Use grapheme-aware width tables for newer emoji and ZWJ sequences. - term.loadAddon(new UnicodeGraphemesAddon()) - term.unicode.activeVersion = "15-graphemes" term.refresh(0, Math.max(0, term.rows - 1)) } - frame = requestAnimationFrame(() => { - frame = undefined - deferred = requestAnimationFrame(loadAddons) - }) - const restarted = (url: string) => { open(url) } - // Resize: fit on any host size change and forward new cols/rows to - // the backend PTY. Debounced because a user drag can fire dozens of - // resize events per second. + // Resize the visible terminal and forward new cols/rows to the backend + // PTY. Hidden terminals refit when activated, avoiding scrollback reflow + // for every mounted terminal during an inspector drag. let resizeTimer: ReturnType | undefined let lastCols = term.cols let lastRows = term.rows - const syncSize = () => { - if (term.cols === lastCols && term.rows === lastRows) return + let synced = false + const syncSize = (force = false) => { + if (!force && synced && term.cols === lastCols && term.rows === lastRows) return lastCols = term.cols lastRows = term.rows + synced = true vscode.postMessage({ type: "agentManager.terminal.resize", terminalId: props.terminalId, @@ -394,7 +395,18 @@ export const TerminalTab: Component = (props) => { rows: term.rows, }) } + const fitNow = () => { + try { + fit.fit() + if (props.active) syncSize(true) + } catch (err) { + // Host still detached at mount time. ResizeObserver will retry + // once layout kicks in. Logged so regressions don't hide. + log("fit() threw", err) + } + } const ro = new ResizeObserver(() => { + if (!props.active) return try { fit.fit() } catch (err) { @@ -409,6 +421,16 @@ export const TerminalTab: Component = (props) => { resizeTimer = setTimeout(syncSize, RESIZE_DEBOUNCE_MS) }) ro.observe(host) + // Wait for the first committed layout before attaching the socket. This + // prevents the shell from emitting its first prompt at xterm's default + // 80 columns, which is most visible in a narrow side panel. + frame = requestAnimationFrame(() => { + frame = undefined + if (closed) return + fitNow() + open(props.wsUrl) + deferred = requestAnimationFrame(loadAddons) + }) // ---- Repaint recovery ---- // @@ -439,7 +461,7 @@ export const TerminalTab: Component = (props) => { if (!isRenderable()) return try { fit.fit() - syncSize() + syncSize(!synced) } catch (err) { // Layout not settled yet; ResizeObserver retries on next change. log("repaint fit() threw", err) @@ -472,6 +494,9 @@ export const TerminalTab: Component = (props) => { if (message.terminalId === props.terminalId && !ws) { term.options.fontFamily = message.font.fontFamily term.options.fontSize = message.font.fontSize + // Optimistic side terminals can fit before their backend PTY exists; + // force the first resize again once the created response arrives. + fitNow() scheduleRepaint() open(message.wsUrl) } diff --git a/packages/kilo-vscode/webview-ui/agent-manager/terminal/render.tsx b/packages/kilo-vscode/webview-ui/agent-manager/terminal/render.tsx index fa83bd7e939..611f9d20139 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/terminal/render.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/terminal/render.tsx @@ -51,6 +51,7 @@ export function renderTerminalTab(deps: TerminalTabRenderDeps): JSX.Element { status={deps.terms.scriptStatus(deps.id)} keybind={isActive() ? "" : deps.keybind()} closeKeybind={deps.closeKeybind()} + focused={deps.terms.focusedId() === deps.id} active={isActive()} role={deps.role} selected={deps.selected} @@ -93,7 +94,7 @@ export function renderTerminalTab(deps: TerminalTabRenderDeps): JSX.Element { * exists; that boundary never flips under a live xterm, since removing * the last terminal disposes its instance first. */ -export function renderTerminalLayer(props: { state: TerminalStateControls }): JSX.Element { +export function renderTerminalLayer(props: { state: TerminalStateControls; onFocusPrompt: () => void }): JSX.Element { const layerActive = () => props.state.activeId() !== undefined const slotVisible = (termId: string, contextKey: string) => props.state.activeId() === termId && props.state.currentKey() === contextKey @@ -113,6 +114,7 @@ export function renderTerminalLayer(props: { state: TerminalStateControls }): JS focusSerial={focusSerial(props.state, term.id)} font={term.font} onFocusChange={(focused) => props.state.setFocusedId(focused ? term.id : undefined)} + onFocusPrompt={props.onFocusPrompt} onTitleChange={(title) => props.state.setTitle(term.id, title)} />
@@ -138,6 +140,7 @@ export function renderSideTerminalLayer(props: { state: TerminalStateControls contextKey: Accessor visible: Accessor + onFocusPrompt: () => void }): JSX.Element { return (
@@ -159,6 +162,7 @@ export function renderSideTerminalLayer(props: { status={() => props.state.scriptStatus(term.id)} restartable={term.kind === undefined} onFocusChange={(focused) => props.state.setFocusedId(focused ? term.id : undefined)} + onFocusPrompt={props.onFocusPrompt} onTitleChange={(title) => props.state.setTitle(term.id, title)} />
diff --git a/packages/kilo-vscode/webview-ui/agent-manager/terminal/side.ts b/packages/kilo-vscode/webview-ui/agent-manager/terminal/side.ts index 907625dd165..08f70bb699f 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/terminal/side.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/terminal/side.ts @@ -3,10 +3,9 @@ * * Extracted from AgentManagerApp.tsx to keep that file under the * `max-lines` lint cap. Owns the destination preference plus the toggle - * semantics of the toolbar button / `Cmd/Ctrl+/` shortcut, so the - * embedded terminal behaves like the diff panel: press once to reveal, - * press again to hide. Hiding never kills the terminal — only the - * explicit close action (or `Cmd+W` while it holds focus) does. + * semantics: the toolbar button toggles visibility, while `Cmd/Ctrl+/` + * reveals, focuses when unfocused, and hides when focused. + * Hiding never kills the terminal — only the explicit close action does. * * ## Destination state ownership * @@ -71,6 +70,10 @@ export interface SideTerminalDeps { visible: Accessor /** Id of the side terminal holding DOM focus, if any. */ focusedId: Accessor + /** Number of side terminals in the visible context. */ + count: Accessor + /** Whether the focused terminal is provider-owned Run/Setup output. */ + isScript: (terminalId: string) => boolean /** Leave terminal mode; the terminal stays alive in the background. */ hide: () => void /** Move focus back to the chat composer. */ @@ -104,8 +107,12 @@ export function createSideTerminal(deps: SideTerminalDeps) { if (wasFocused) deps.refocus() } - const toggle = () => { + const toggle = (trigger: "keyboard_shortcut" | "tab_toolbar" = "keyboard_shortcut") => { if (deps.visible()) { + if (trigger === "keyboard_shortcut" && !deps.focusedId()) { + deps.handlers.requestSide() + return + } const was = deps.focusedId() !== undefined deps.hide() handoff(was) @@ -122,15 +129,18 @@ export function createSideTerminal(deps: SideTerminalDeps) { }) } - /** Kill the focused side terminal (Cmd/Ctrl+W). The panel stays open - * on the remaining terminals, or on the empty state when this was - * the last one. */ + /** Leave the focused side terminal without killing its shell when it is + * the last terminal or provider-owned script output. */ const close = (): boolean => { const id = deps.focusedId() if (!id) return false - const done = deps.handlers.closeSide(id) - if (done) handoff(true) - return done + const only = deps.count() === 1 + if (only || deps.isScript(id)) { + deps.hide() + handoff(true) + return true + } + return deps.handlers.closeSide(id) } /** Toolbar button and `Cmd/Ctrl+/`: follow the user's destination. */ @@ -138,7 +148,7 @@ export function createSideTerminal(deps: SideTerminalDeps) { const target = destination() deps.track("terminal", trigger, { destination: target }) if (target === "agentManager") { - toggle() + toggle(trigger) return } deps.openVscode() diff --git a/packages/kilo-vscode/webview-ui/agent-manager/terminal/state.ts b/packages/kilo-vscode/webview-ui/agent-manager/terminal/state.ts index 2702421223c..dde770b4d48 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/terminal/state.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/terminal/state.ts @@ -628,10 +628,32 @@ function newId(): string { } /** - * Build the close-terminal handler the main component wires to the - * close button. Picks the next visible tab before dropping the entry - * so focus flows naturally; notifies the extension last. + * Estimate initial terminal geometry from the current DOM container. + * Provides best-effort columns and rows so PTY spawn avoids the default + * 80-column line width before the first xterm fit pass commits. */ +function measureInitialDimensions( + placement: TerminalPlacement, + font: TerminalFont, +): { cols: number; rows: number } | undefined { + if (typeof document === "undefined") return undefined + const selector = + placement === "side" + ? ".am-side-terminal-layer, .am-side-terminal, .am-diff-panel-wrapper" + : ".am-terminal-layer, .am-detail-stack" + const host = document.querySelector(selector) as HTMLElement | null + const rect = host?.getBoundingClientRect() + if (!rect || rect.width <= 0 || rect.height <= 0) return undefined + const cellWidth = font.fontSize > 0 ? font.fontSize * 0.6 : 7.2 + const cellHeight = font.fontSize > 0 ? font.fontSize * 1.2 : 14.4 + const availableWidth = Math.max(0, rect.width - 30) + const availableHeight = Math.max(0, rect.height - 16) + return { + cols: Math.max(10, Math.floor(availableWidth / cellWidth)), + rows: Math.max(3, Math.floor(availableHeight / cellHeight)), + } +} + export function createTerminalHandlers(deps: TerminalHandlerDeps) { const activate = (id: string) => { deps.state.setActiveId(id) @@ -645,11 +667,15 @@ export function createTerminalHandlers(deps: TerminalHandlerDeps) { const requestNew = () => { const sel = deps.getSelection() if (sel === null) return + const font = deps.getFont() + const dims = measureInitialDimensions("tab", font) deps.postMessage({ type: "agentManager.terminal.create", createId: newId(), placement: "tab", worktreeId: sel === deps.LOCAL ? null : sel, + cols: dims?.cols, + rows: dims?.rows, }) } @@ -659,12 +685,14 @@ export function createTerminalHandlers(deps: TerminalHandlerDeps) { // project-namespaced state key and must not leak into the message. const sel = deps.getSelection() const id = newId() + const font = deps.getFont() + const dims = measureInitialDimensions("side", font) deps.state.beginSide(key, id) deps.state.add(key === deps.LOCAL ? null : key, { id, title: "Terminal", wsUrl: "", - font: deps.getFont(), + font, placement: "side", }) deps.state.setSideActive(key, id) @@ -674,6 +702,8 @@ export function createTerminalHandlers(deps: TerminalHandlerDeps) { createId: id, placement: "side", worktreeId: sel === null || sel === deps.LOCAL ? null : sel, + cols: dims?.cols, + rows: dims?.rows, }) } @@ -746,8 +776,6 @@ export function createTerminalHandlers(deps: TerminalHandlerDeps) { const target = deps.findTab(nextId) if (target) deps.selectSessionTab(target.id, deps.isPendingId(target.id)) } - } else { - deps.clearSession() } } deps.postMessage({ type: "agentManager.terminal.close", terminalId }) @@ -764,6 +792,14 @@ export function createTerminalHandlers(deps: TerminalHandlerDeps) { // unmount its xterm while the backend PTY leaks (no close sent). const term = deps.state.sides().find((t) => t.id === terminalId) if (!term) return false + const key = term.contextKey + const active = deps.state.sideActiveFor(key) === terminalId + const rest = active ? deps.state.sidesForContext(key).filter((item) => item.id !== terminalId) : [] + const survivor = rest[rest.length - 1] + if (survivor) { + deps.state.setSideActive(key, survivor.id) + deps.state.requestFocus(survivor.id) + } if (term.kind) { deps.postMessage({ type: "agentManager.terminal.close", terminalId }) return true @@ -820,6 +856,33 @@ export function createTerminalHandlers(deps: TerminalHandlerDeps) { return true } + /** Close the main terminal that actually owns DOM focus, not just the active tab. */ + const closeFocused = () => { + const id = deps.state.focusedId() + if (!id || !deps.state.current().some((term) => term.id === id)) return false + closeTerminal(id) + return true + } + + /** Cycle terminals within one placement, wrapping at either end. */ + const cycle = (direction: "previous" | "next", placement: "side" | "tab") => { + const key = deps.state.sideKey() + const list = placement === "side" ? deps.state.sidesForContext(key) : deps.state.current() + if (list.length === 0) return false + const current = placement === "side" ? deps.state.sideActiveFor(key) : deps.state.activeId() + const index = list.findIndex((term) => term.id === current) + const start = index === -1 ? (direction === "next" ? -1 : list.length) : index + const offset = direction === "next" ? 1 : -1 + const next = list[(start + offset + list.length) % list.length]! + if (placement === "side") { + deps.state.setSideActive(key, next.id) + deps.state.requestFocus(next.id) + return true + } + activate(next.id) + return true + } + return { closeTerminal, closeSide, @@ -834,6 +897,8 @@ export function createTerminalHandlers(deps: TerminalHandlerDeps) { ensureSide, addSide, closeActive, + closeFocused, + cycle, } } diff --git a/packages/kilo-vscode/webview-ui/src/App.tsx b/packages/kilo-vscode/webview-ui/src/App.tsx index 7fa356e8482..e8dafa01c5c 100644 --- a/packages/kilo-vscode/webview-ui/src/App.tsx +++ b/packages/kilo-vscode/webview-ui/src/App.tsx @@ -11,6 +11,7 @@ import { LocalTabsProvider, useLocalTabs } from "./context/local-tabs" import { ProviderShell } from "./context/provider-shell" import { ChatView } from "./components/chat" import { SidebarEmptyState } from "./components/chat/SidebarEmptyState" +import { SidebarTopBar } from "./components/chat/SidebarTopBar" import { registerExpandedTaskTool } from "./components/chat/TaskToolExpanded" import { registerVscodeToolOverrides } from "./components/chat/VscodeToolOverrides" @@ -308,8 +309,26 @@ const AppContent: Component = () => { setCurrentView("history")} /> ) + // Set synchronously in the webview HTML by KiloProvider so it's available + // before this component ever mounts (see buildWebviewHtml/_getHtmlForWebview). + // False for dedicated single-purpose panels (Settings, Profile, Sub-Agent + // Viewer) always, and for the Sidebar/"Open in Tab" outside Cursor — real + // VS Code's native title bar toolbar already covers those. Defaults to + // true only when unset entirely (e.g. Storybook, which doesn't render the + // real page HTML). + const host = window as { KILO_TOP_BAR?: boolean; KILO_TOP_BAR_SURFACE?: string } + const showTopBar = host.KILO_TOP_BAR !== false + const topBarSurface = host.KILO_TOP_BAR_SURFACE ?? "sidebar_title" + return (
+ + handleViewAction("plusButtonClicked")} + onHistory={() => handleViewAction("historyButtonClicked")} + surface={topBarSurface} + /> + {/* legacy-migration start — state-driven overlay, independent of currentView */} = (props) => { pendingId, context, origin ?? null, + { + agent: matched.agent, + model: matched.model, + variant: matched.variant, + }, ) } else { session.sendMessage(message, sel?.providerID, sel?.modelID, attachments, pendingId, context, data, origin ?? null) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/SidebarTopBar.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/SidebarTopBar.tsx new file mode 100644 index 00000000000..0a0de70c929 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/components/chat/SidebarTopBar.tsx @@ -0,0 +1,88 @@ +/** + * Renders New Task, History, Agent Manager, KiloClaw, Marketplace, Profile, and + * Settings inside the webview, as a fallback for Cursor only (see isCursorHost() + * in src/utils.ts). Cursor's Secondary Side Bar support is unreliable for + * extension-contributed `view/title` toolbars, which render outside the webview + * DOM with no API to detect or work around the failure. Real VS Code renders the + * native toolbar fine everywhere, so it keeps using that instead of this bar. + */ + +import { Component, For } from "solid-js" +import { Tooltip } from "@kilocode/kilo-ui/tooltip" +import { useVSCode } from "../../context/vscode" +import { useLanguage } from "../../context/language" +import { TelemetryEventName } from "../../../../src/services/telemetry/types" +import "@vscode/codicons/dist/codicon.css" + +export interface SidebarTopBarProps { + onNewTask: () => void + onHistory: () => void + /** Telemetry surface — distinguishes the sidebar from the "Open in Tab" panel, which shares this component. */ + surface: string +} + +/** Codicon names used below. */ +type Codicon = "add" | "history" | "organization" | "comment-discussion" | "extensions" | "account" | "settings-gear" + +interface Action { + key: string + codicon: Codicon + button: string + run: () => void +} + +export const SidebarTopBar: Component = (props) => { + const vscode = useVSCode() + const language = useLanguage() + + // Mirrors the telemetry the native toolbar buttons used to record, so analytics aren't lost. + const track = (button: string) => + vscode.postMessage({ + type: "telemetry", + event: TelemetryEventName.TITLE_BUTTON_CLICKED, + properties: { button, surface: props.surface }, + }) + + const open = ( + type: "openAgentManager" | "openKiloClaw" | "openMarketplacePanel" | "openProfilePanel" | "openSettingsPanel", + ) => vscode.postMessage({ type }) + + const actions: Action[] = [ + { key: "newTask", codicon: "add", button: "new_task", run: () => props.onNewTask() }, + { key: "history", codicon: "history", button: "history", run: () => props.onHistory() }, + { key: "agentManager", codicon: "organization", button: "agent_manager", run: () => open("openAgentManager") }, + { key: "kiloClaw", codicon: "comment-discussion", button: "kiloclaw", run: () => open("openKiloClaw") }, + { key: "marketplace", codicon: "extensions", button: "marketplace", run: () => open("openMarketplacePanel") }, + { key: "profile", codicon: "account", button: "profile", run: () => open("openProfilePanel") }, + { key: "settings", codicon: "settings-gear", button: "settings", run: () => open("openSettingsPanel") }, + ] + + return ( +