diff --git a/.changeset/bright-subagents-reason.md b/.changeset/bright-subagents-reason.md deleted file mode 100644 index 3c86f545762..00000000000 --- a/.changeset/bright-subagents-reason.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@kilocode/cli": patch -"kilo-code": patch ---- - -Support model-specific reasoning overrides for task subagents, including custom subagents with their own model and variant settings. diff --git a/.changeset/clear-rate-limit-errors.md b/.changeset/clear-rate-limit-errors.md new file mode 100644 index 00000000000..7feca1af9cd --- /dev/null +++ b/.changeset/clear-rate-limit-errors.md @@ -0,0 +1,6 @@ +--- +"@kilocode/cli": patch +"kilo-code": patch +--- + +Show a clear, retryable provider rate-limit error instead of raw response JSON in chat. diff --git a/.changeset/close-read-tui-news.md b/.changeset/close-read-tui-news.md new file mode 100644 index 00000000000..ce1fd8845cd --- /dev/null +++ b/.changeset/close-read-tui-news.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Hide TUI news after they have been opened and add a button to close the news dialog. diff --git a/.changeset/fast-regular-snapshots.md b/.changeset/fast-regular-snapshots.md deleted file mode 100644 index 2dcc981012b..00000000000 --- a/.changeset/fast-regular-snapshots.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Accelerate initial snapshots for regular Git sessions while preserving existing changes and asynchronously storing snapshots independently from the source repository. diff --git a/.changeset/fix-kilo-npm-upgrade.md b/.changeset/fix-kilo-npm-upgrade.md new file mode 100644 index 00000000000..307a40eb8f5 --- /dev/null +++ b/.changeset/fix-kilo-npm-upgrade.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Fix upgrades to resolve Kilo CLI packages and releases instead of OpenCode packages and versions. diff --git a/.changeset/fix-session-scope-toggle.md b/.changeset/fix-session-scope-toggle.md new file mode 100644 index 00000000000..38396df275e --- /dev/null +++ b/.changeset/fix-session-scope-toggle.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Show current-worktree sessions by default in the TUI sessions dialog and keep all/current scope toggling working when a scope has no sessions. diff --git a/.changeset/hide-prompt-training-models.md b/.changeset/hide-prompt-training-models.md new file mode 100644 index 00000000000..6221e66b3cd --- /dev/null +++ b/.changeset/hide-prompt-training-models.md @@ -0,0 +1,6 @@ +--- +"kilo-code": patch +"@kilocode/cli": patch +--- + +Add a shared model setting to hide Kilo Gateway models that may train on your prompts across Kilo clients. diff --git a/.changeset/highlight-jetbrains-shell-commands.md b/.changeset/highlight-jetbrains-shell-commands.md new file mode 100644 index 00000000000..394ac4ea35e --- /dev/null +++ b/.changeset/highlight-jetbrains-shell-commands.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Highlight shell tool commands in JetBrains chat transcripts. diff --git a/.changeset/improve-jetbrains-markdown.md b/.changeset/improve-jetbrains-markdown.md new file mode 100644 index 00000000000..ff1a1845e0b --- /dev/null +++ b/.changeset/improve-jetbrains-markdown.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Improve markdown readability in JetBrains chat transcripts. diff --git a/.changeset/jetbrains-hide-read-payloads.md b/.changeset/jetbrains-hide-read-payloads.md new file mode 100644 index 00000000000..6216844f0cd --- /dev/null +++ b/.changeset/jetbrains-hide-read-payloads.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Hide generated read-tool payload lines from JetBrains prompt bubbles while keeping attachments and assistant tool output visible. diff --git a/.changeset/jetbrains-horizontal-attachments.md b/.changeset/jetbrains-horizontal-attachments.md new file mode 100644 index 00000000000..2b167a0198c --- /dev/null +++ b/.changeset/jetbrains-horizontal-attachments.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Show JetBrains prompt attachments in one horizontal scrolling row in session history. diff --git a/.changeset/jetbrains-paste-attachments.md b/.changeset/jetbrains-paste-attachments.md new file mode 100644 index 00000000000..1003291b27a --- /dev/null +++ b/.changeset/jetbrains-paste-attachments.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Support pasting files and images into JetBrains chat prompts as attachments. diff --git a/.changeset/jetbrains-prompt-attachment-preview.md b/.changeset/jetbrains-prompt-attachment-preview.md new file mode 100644 index 00000000000..f5b33305c46 --- /dev/null +++ b/.changeset/jetbrains-prompt-attachment-preview.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Show JetBrains prompt attachments inside the prompt bubble with previews and open embedded attachments in editor tabs. diff --git a/.changeset/jetbrains-prompt-attachments.md b/.changeset/jetbrains-prompt-attachments.md new file mode 100644 index 00000000000..efde8186459 --- /dev/null +++ b/.changeset/jetbrains-prompt-attachments.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Support sending file and image attachments from the JetBrains chat prompt. diff --git a/.changeset/jetbrains-prompt-input-height.md b/.changeset/jetbrains-prompt-input-height.md new file mode 100644 index 00000000000..120f0ff4dd6 --- /dev/null +++ b/.changeset/jetbrains-prompt-input-height.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Limit JetBrains prompt input growth to the session while preserving scrolling for long prompts. diff --git a/.changeset/jetbrains-restartless-unload.md b/.changeset/jetbrains-restartless-unload.md new file mode 100644 index 00000000000..62e2f7941a7 --- /dev/null +++ b/.changeset/jetbrains-restartless-unload.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Stop Kilo backend processes and clear JetBrains UI resources during restartless plugin unloads. diff --git a/.changeset/jetbrains-session-drop-overlay.md b/.changeset/jetbrains-session-drop-overlay.md new file mode 100644 index 00000000000..64be585e380 --- /dev/null +++ b/.changeset/jetbrains-session-drop-overlay.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Support dropping files anywhere in a JetBrains chat session to add them to the prompt. diff --git a/.changeset/jetbrains-session-icons.md b/.changeset/jetbrains-session-icons.md new file mode 100644 index 00000000000..7787da11995 --- /dev/null +++ b/.changeset/jetbrains-session-icons.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Polish session header controls and align session view icons. diff --git a/.changeset/jetbrains-vfs-frontend-handoff.md b/.changeset/jetbrains-vfs-frontend-handoff.md new file mode 100644 index 00000000000..51914683663 --- /dev/null +++ b/.changeset/jetbrains-vfs-frontend-handoff.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Open embedded JetBrains message attachments in frontend-managed Kilo editor tabs with loading and connection retry states. diff --git a/.changeset/kilo-branding-leftovers.md b/.changeset/kilo-branding-leftovers.md new file mode 100644 index 00000000000..b9db0630931 --- /dev/null +++ b/.changeset/kilo-branding-leftovers.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Replace remaining OpenCode-branded CLI and TUI copy with Kilo branding. diff --git a/.changeset/list-agent-manager-worktrees.md b/.changeset/list-agent-manager-worktrees.md new file mode 100644 index 00000000000..2e15a890ad3 --- /dev/null +++ b/.changeset/list-agent-manager-worktrees.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Show Agent Manager and other Git worktrees in the Kilo Console project view. diff --git a/.changeset/native-plan-architect-parity.md b/.changeset/native-plan-architect-parity.md new file mode 100644 index 00000000000..2de5bf1895f --- /dev/null +++ b/.changeset/native-plan-architect-parity.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Make native Plan mode follow Architect-style planning behavior while preserving Plan mode restrictions and repo-root plan files. diff --git a/.changeset/notify-background-sessions.md b/.changeset/notify-background-sessions.md new file mode 100644 index 00000000000..6b5646bcf73 --- /dev/null +++ b/.changeset/notify-background-sessions.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Offer opt-in attention sounds when sessions finish, error, or need input across the sidebar, editor tabs, and Agent Manager. diff --git a/.changeset/plan-timestamp-prefix.md b/.changeset/plan-timestamp-prefix.md new file mode 100644 index 00000000000..d757c9686bf --- /dev/null +++ b/.changeset/plan-timestamp-prefix.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Restore session timestamp prefixes for generated plan filenames while preserving descriptive model-chosen names. diff --git a/.changeset/preserve-console-review-expansion.md b/.changeset/preserve-console-review-expansion.md new file mode 100644 index 00000000000..3b7f19283af --- /dev/null +++ b/.changeset/preserve-console-review-expansion.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Keep expanded Kilo Console file diffs open while resizing the context sidebar. diff --git a/.changeset/preserve-console-terminals.md b/.changeset/preserve-console-terminals.md new file mode 100644 index 00000000000..476e44608f8 --- /dev/null +++ b/.changeset/preserve-console-terminals.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Keep Kilo Console terminal sessions open when changing diff layout and other console preferences. diff --git a/.changeset/render-jetbrains-attachment-cards.md b/.changeset/render-jetbrains-attachment-cards.md new file mode 100644 index 00000000000..fc26c664e3c --- /dev/null +++ b/.changeset/render-jetbrains-attachment-cards.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Render file attachments as attachment cards in JetBrains prompts and session history. diff --git a/.changeset/render-jetbrains-shell-markdown.md b/.changeset/render-jetbrains-shell-markdown.md new file mode 100644 index 00000000000..267e7bd8059 --- /dev/null +++ b/.changeset/render-jetbrains-shell-markdown.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Render JetBrains shell tool output with markdown code blocks. diff --git a/.changeset/route-worktree-questions.md b/.changeset/route-worktree-questions.md deleted file mode 100644 index f0943a4470e..00000000000 --- a/.changeset/route-worktree-questions.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": patch ---- - -Route question responses to the worktree where the question was created. diff --git a/.changeset/secure-external-downloads.md b/.changeset/secure-external-downloads.md deleted file mode 100644 index 561867241db..00000000000 --- a/.changeset/secure-external-downloads.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@kilocode/cli": patch ---- - -Validate GitHub attachments and language server release paths before downloading or executing them. diff --git a/.changeset/silent-comet-attachments.md b/.changeset/silent-comet-attachments.md new file mode 100644 index 00000000000..fb622dba660 --- /dev/null +++ b/.changeset/silent-comet-attachments.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Open embedded transcript attachments in stable Kilo editor tabs. diff --git a/.github/workflows/watch-opencode-releases.yml b/.github/workflows/watch-opencode-releases.yml index 8762e9481de..10da78231b2 100644 --- a/.github/workflows/watch-opencode-releases.yml +++ b/.github/workflows/watch-opencode-releases.yml @@ -12,7 +12,7 @@ permissions: jobs: check-release: - if: vars.OPENCODE_WATCH_ENABLED != 'false' + if: github.repository == 'Kilo-Org/kilocode' && vars.OPENCODE_WATCH_ENABLED != 'false' runs-on: ubuntu-latest timeout-minutes: 5 steps: diff --git a/.kilo/plans/jetbrains-mdview-editor-theme-colors.md b/.kilo/plans/jetbrains-mdview-editor-theme-colors.md new file mode 100644 index 00000000000..20e85b546f8 --- /dev/null +++ b/.kilo/plans/jetbrains-mdview-editor-theme-colors.md @@ -0,0 +1,67 @@ +# JetBrains MdView Editor Theme Colors Plan + +## Goal + +Keep JetBrains markdown layout compact while making markdown colors and backgrounds come from the active editor color scheme, and ensure existing theme/editor-setting listeners refresh all existing `MdView` instances after changes. + +## Findings + +- `SessionUi` already subscribes to `EditorColorsManager.TOPIC` and `LafManagerListener.TOPIC`, then calls `applyStyle(SessionEditorStyle.current())` on the session tree. +- Most markdown consumers already propagate that style through `MdView.applyStyle(style)` via `TextView`, `ReasoningView`, message lists, and session panels. +- `PlanExitView.applyStyle()` sets only font/code font/foreground and does not call `md.applyStyle(style)`, so internal markdown role colors can stay tied to the initial style. +- `MdViewHybrid.applyStyle()` restyles retained HTML panes and code block containers, but retained `CodeField` editors need explicit reapplication of `SessionEditorStyle.applyToEditor()` after creation so syntax highlighting and editor colors follow scheme changes. +- `MdCommon.defaults()` currently mixes editor colors, UI theme colors, and one literal inline-code color fallback. The literal fallback should be removed, and markdown roles should derive from `EditorColorsScheme`/syntax attributes wherever possible. +- `TextView` currently overrides markdown background with `SessionUiStyle.Transcript.bgColor()` (`UiStyle.Colors.bg()`), while prompt markdown already uses `style.editorBackground`. If markdown surfaces should consistently use editor background, normal text views need to use `style.editorBackground` too. + +## Implementation Plan + +1. Keep compact CSS unchanged. + - Do not reintroduce line-height, margin, padding, heading sizing, or other geometry rules. + - Keep the current role color/background selectors only. + +2. Derive markdown role defaults from editor settings. + - Update `MdCommon.defaults(style)` to use `style.editorForeground` and `style.editorBackground` for primary text/background. + - Add small local helper functions for editor attributes, for example foreground/background from `style.editorScheme.getAttributes(key)` and color keys from `style.editorScheme.getColor(key)`. + - Use public IntelliJ editor keys for role colors: + - Links: `CodeInsightColors.HYPERLINK_ATTRIBUTES.foregroundColor`, fallback to platform link color if absent. + - Inline code foreground/background: `DefaultLanguageHighlighterColors.DOC_CODE_INLINE`, fallback to `STRING`, then editor foreground/background. + - Code block foreground/background: `DefaultLanguageHighlighterColors.DOC_CODE_BLOCK`, fallback to editor foreground/background. + - Quote/emphasis/list weak text: comment/doc-comment attributes, fallback to editor foreground or `UIUtil.getContextHelpForeground()` only when the scheme has no useful value. + - Borders/HR/table/code border: editor preview/border color keys such as `EditorColors.PREVIEW_BORDER_COLOR`, fallback to `UiStyle.Colors.contentBorder()`. + - Remove `JBColor(0x...)` or other literal runtime color fallbacks from `MdCommon`. + - Keep public `MdView` API unchanged; role colors stay internal unless a concrete external override need appears. + +3. Ensure style propagation reaches every markdown instance. + - Update `PlanExitView.applyStyle(style)` to call `md.applyStyle(style)` before applying its explicit font/code-font/foreground overrides. + - Audit existing markdown callers after the change; keep using the existing `SessionEditorStyleTarget` propagation path rather than adding per-`MdView` theme listeners. + - Keep explicit foreground overrides in `TextView`, `ReasoningView`, and `PlanExitView` where they intentionally set body text role, but let internal markdown role colors refresh from the new style snapshot. + +4. Ensure retained hybrid code blocks update after editor setting changes. + - In `MdViewHybrid.CodeView.style(opts)`, for retained `CodeField` blocks, reapply `style.applyToEditor(editor)` to the underlying editor if it exists. + - Reapply code editor background/scroll pane/viewport backgrounds from `opts.preBg` in the same path. + - Preserve retained component/editor reuse: do not rebuild code block panes just to update style. + +5. Align markdown backgrounds with editor settings. + - Keep `MdCommon` default background as `style.editorBackground`. + - Change normal `TextView` markdown background to `style.editorBackground` if the intent is that all markdown surfaces use editor background, matching `PromptView` and session root behavior. + - Preserve `transparent` handling: when `md.opaque = false`, background should still be the editor-derived value for child/code surfaces, but the Swing component should remain non-opaque. + +6. Update tests. + - Extend `MdViewTest` to verify markdown role CSS changes when applying a `SessionEditorStyle` backed by a customized editor scheme, especially inline code, code block, link, and border colors. + - Extend `MdViewHybridTest` to assert `applyStyle()` updates retained HTML panes and retained code editors without replacing them, including editor scheme/background changes. + - Add or extend `PlanExitViewTest` so `applyStyle()` refreshes the nested markdown style, not just foreground/font overrides. + - Keep existing compactness expectations: tests should not assert new spacing, padding, margin, line-height, or size rules. + +## Verification + +- From `packages/kilo-jetbrains/`, run `./gradlew frontend:test --tests '*MdView*'`. +- From `packages/kilo-jetbrains/`, run `./gradlew frontend:test --tests '*PlanExitViewTest*'` if the focused test is not covered by the MdView filter. +- From `packages/kilo-jetbrains/`, run `bun run typecheck`. + +## Constraints + +- Do not introduce JCEF, Compose, or Kotlin UI DSL. +- Do not add new theme/editor listeners in `MdView`; use the existing `SessionUi` listener and `SessionEditorStyleTarget` propagation path. +- Avoid hardcoded runtime colors in markdown styling; prefer editor scheme attributes/color keys, then platform/theme APIs as non-literal fallbacks. +- Preserve retained Swing behavior and `MdViewHybrid.sync()` component reuse. +- Keep the public `MdView` override API stable unless implementation proves a new external override is required. diff --git a/.kilo/plans/jetbrains-mdview-vscode-styling.md b/.kilo/plans/jetbrains-mdview-vscode-styling.md new file mode 100644 index 00000000000..89e816d0235 --- /dev/null +++ b/.kilo/plans/jetbrains-mdview-vscode-styling.md @@ -0,0 +1,69 @@ +# JetBrains MdView VS Code Styling Plan + +## Goal + +Improve JetBrains markdown output so assistant/user transcript markdown visually matches the VS Code webview markdown style while keeping the existing Swing/JBHtmlPane + editor-backed code block architecture. + +## Findings + +- VS Code markdown styling is split across `packages/ui/src/components/markdown.css`, `packages/kilo-ui/src/components/markdown.css`, `packages/kilo-ui/src/styles/vscode-bridge.css`, and message-part overrides. +- Base VS Code markdown uses 14px sans text, 160% line height, tight first/last margins, same-size medium headings, 12px paragraph spacing, link-colored anchors, compact lists, weak list markers, weak blockquotes with a 2px left border, invisible HR spacing, bordered/padded code blocks, green inline code, and lightly bordered tables. +- The VS Code theme bridge maps markdown roles to editor/theme tokens: heading/link/list/image use `textLinkForeground`, text/strong/code-block use editor foreground, inline code uses charts/syntax green, quote/emphasis use description foreground, HR uses panel border. +- JetBrains markdown is rendered by `MdViewHybrid` and `MdViewHtmlPane`, with shared CSS from `MdCommon.rules()` and defaults from `MdCommon.defaults()`. +- JetBrains currently styles only broad tag font/color, links, code/pre colors, blockquote border/text color, and table border. It lacks VS Code-equivalent spacing, heading/strong/emphasis/list marker/table cell rules, inline-code foreground, blockquote geometry, HR spacing, and code block surface polish. +- JetBrains fenced code blocks are already stronger than VS Code in one respect: they use `EditorTextField` with real IDE syntax highlighting and streaming retention. Preserve this instead of switching to web/JCEF rendering. + +## Implementation Plan + +1. Expand JetBrains markdown style tokens. + - Add internal fields to `MdStyle` for heading, strong, emphasis, inline code foreground, list marker, HR, table/header, and code block border colors. + - Keep the public `MdView` override API stable unless a new external override is clearly needed. + - Compute defaults in `MdCommon.defaults(style)` from IntelliJ/editor theme sources and centralized Kilo semantic colors where no platform key matches. + - Use `JBColor.namedColor("Kilo.Markdown.*", fallback)` for Kilo-specific markdown palette fallbacks, so themes can override them and runtime code avoids scattered hardcoded colors. + +2. Mirror VS Code markdown CSS in `MdCommon.rules()`. + - Add root/body wrapping rules: max width behavior, break-word wrapping, base line-height, and first/last-child margin trimming where supported by `JBHtmlPane` CSS. + - Add heading rules: same base size, medium/bold weight, role-specific color, line height, and bottom spacing. + - Add paragraph, list, list item, nested list, and marker rules. If Swing HTML does not support `::marker`, fall back to `li { color: ... }` plus child text color reset only if supported; otherwise keep list text normal and document the limitation in tests. + - Add strong/emphasis colors matching VS Code token roles. + - Add anchor styling matching VS Code: themed link color, no forced background, and underline behavior where `JBHtmlPane` supports it. + - Add blockquote geometry: 2px left border, 24px vertical margin, 8px left padding, weak text, and normal style. + - Add table layout rules: collapse borders, full width where possible, 24px vertical margin, 12px cell padding, weak row borders, stronger header text. + - Keep HRs visually hidden but spaced consistently with VS Code if the renderer includes them. `MdViewHybrid` currently filters thematic breaks, so this mainly benefits `MdViewHtmlPane` and future reuse. + - Add inline-code foreground and medium font weight. Avoid inline code backgrounds unless the current `JBHtmlPane` configuration already draws them acceptably. + +3. Polish JetBrains code block containers without losing IDE highlighting. + - Keep `EditorTextField` for fenced/indented blocks and `JBTextArea` fallback. + - Style `JBScrollPane` code blocks to match VS Code’s `markdown-code` wrapper feel: subtle background, subtle border, rounded-ish platform arc if feasible, 12px-ish padding, and thin horizontal scrollbar behavior. + - Use `SessionUiStyle.View.Code` for geometry constants. Add only minimal new constants there if current values cannot represent the VS Code spacing. + - Separate code block border color from table border internally so table styling can change without affecting code boxes. + - Continue applying `SessionEditorStyle.applyToEditor(ed)` so code blocks follow IDE syntax highlighting and editor font changes. + +4. Add file/path affordance parity where safe. + - For markdown links whose `href` looks like a relative file path, keep existing link dispatch so the current caller can open files/URLs appropriately. + - Consider decorating inline code that looks like a path with a `file-link` class only when an `openFile` callback is available through the existing usage path. If the current `MdView` abstraction only has `openUrl`, do not widen it unless the call sites can pass file opening cleanly. + - At minimum, make inline code/path-looking content visually closer to VS Code by using the inline-code foreground and dotted underline for explicit file links where generated HTML contains link/code classes. + +5. Preserve retained Swing behavior. + - Keep `MdViewHybrid.sync()` prefix reuse logic unchanged unless necessary. + - Ensure style updates call `reloadCssStylesheets()` and reassign text only for retained `JBHtmlPane` blocks, not by rebuilding all blocks. + - Keep streaming fenced-code fast path and editor disposal behavior intact. + +6. Add focused tests. + - Extend `MdViewTest` and/or `MdViewHybridTest` to assert `overrideSheet()` contains the new VS Code-equivalent rules for headings, strong/emphasis, links, inline code foreground, list/table/blockquote spacing, HR, and code block/table border separation. + - Add component tests for code block pane styling: background, viewport background, border color, padding, scrollbar policy, and retained editor instance after `applyStyle()`. + - Keep existing stress/leak tests green. Add a small stress assertion only if the implementation changes style application semantics. + - Add a changeset: `@kilocode/kilo-jetbrains` patch with user-facing wording such as `Improve markdown readability in JetBrains chat transcripts.` + +## Verification + +- Run targeted JetBrains markdown tests first from `packages/kilo-jetbrains/`: `./gradlew frontend:test --tests '*MdView*'` if the Gradle module supports it; otherwise run the closest supported targeted Gradle test command. +- Run `bun run typecheck` from `packages/kilo-jetbrains/`. +- If targeted Gradle filtering is unreliable, run `./gradlew test` from `packages/kilo-jetbrains/`. + +## Constraints + +- Do not introduce JCEF, Compose, or Kotlin UI DSL. +- Keep changes inside `packages/kilo-jetbrains/` and `.changeset/` unless a shared Kilo UI source of truth is explicitly required. +- No `kilocode_change` markers are needed for JetBrains or Kilo UI paths. +- Prefer IntelliJ theme APIs and centralized semantic tokens over scattered literal colors. diff --git a/.kilo/plans/jetbrains-session-ui-icons-header.md b/.kilo/plans/jetbrains-session-ui-icons-header.md new file mode 100644 index 00000000000..7ed4ae52502 --- /dev/null +++ b/.kilo/plans/jetbrains-session-ui-icons-header.md @@ -0,0 +1,63 @@ +# JetBrains Session UI Icons And Header Layout Plan + +## Goal +Improve JetBrains session UI icon consistency and reduce accidental header interactions: +- Use Kilo/VS Code-aligned session icons in session views. +- Fix the reasoning header icon. +- Normalize session part collapse/expand chevrons so collapsed/expanded states do not jump between differently sized glyphs. +- Move the session-details toggle away from compaction and place it before the session title. + +## Findings +- Session view icons are centralized in `packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/SessionViewIcons.kt` and loaded from `frontend/src/main/resources/icons/views/*.svg`. +- The JetBrains `views` SVGs already mirror the shared VS Code/UI icon paths from `packages/ui/src/components/icon.tsx` for the audited names, including `brain`, `chevron-down`, `chevron-right`, `checklist`, `console`, `warning`, etc. +- The reasoning view currently renders `SessionViewIcons.eye` in `ReasoningView.kt`; VS Code/shared UI uses the `brain` icon for reasoning/thinking surfaces, and `SessionViewIcons.brain` already exists. +- Standard collapsible session parts use `SessionViewIcons.chevronDown` when expanded and `SessionViewIcons.chevronRight` when collapsed in `AbstractSessionPartView.kt`; `QuestionResultView.kt` repeats this pattern manually. The down/right SVG paths have different visual extents. +- The session header currently places the details toggle next to the compact button in the right-side controls in `SessionHeaderPanel.kt`, making the two actions easy to confuse. + +## Implementation Steps +1. **Keep icon sources aligned with VS Code/shared UI** + - Treat `packages/ui/src/components/icon.tsx` as the source for Kilo web/session glyph shapes. + - Re-check `SessionViewIcons.kt` entries against available JetBrains assets; only update or add SVGs if a session view uses a Kilo icon missing from `frontend/src/main/resources/icons/views/`. + - Preserve JetBrains SVG theming rules: no `currentColor`; use literal palette colors and dark variants where assets are added or changed. + +2. **Fix reasoning icon** + - In `ReasoningView.kt`, change the reasoning header glyph from `SessionViewIcons.eye` to `SessionViewIcons.brain`. + - Add or update test coverage in `ReasoningViewTest.kt` by inspecting the rendered Swing label tree and asserting the reasoning icon is `SessionViewIcons.brain`. + +3. **Normalize collapse/expand chevrons for session parts** + - Stop using the mixed `chevronRight`/`chevronDown` pair for collapsible session content. + - Use a single base Kilo chevron glyph for both states, matching the current custom chevron used by the session header (`/icons/chevron-down.svg` / equivalent `SessionViewIcons.chevronDown`). + - Add a shared rotated icon for the opposite state instead of switching to a differently sized right-facing asset. Prefer a small reusable helper or centralized icon field rather than importing header-specific UI into session views. + - Update `AbstractSessionPartView.kt` and `QuestionResultView.kt` to use the normalized chevron pair. + - Leave `QuestionView.kt` navigation chevrons alone unless auditing shows they are being used for collapse/expand; those are previous/next controls, not expand/collapse controls. + +4. **Relocate and change header show/hide details toggle** + - In `SessionHeaderPanel.kt`, replace the custom header details chevron with platform `AllIcons` arrows/chevrons, e.g. collapsed = `AllIcons.General.ArrowRight`, expanded = `AllIcons.General.ArrowDown`. + - Move the details toggle out of the right-side controls and into `BorderLayout.WEST` of the header row. + - Rebuild the top header as: + - outer border layout + - west: details toggle button + - center: inner border layout + - inner center: session title + - inner east: horizontal stack/row with price/context and compact button + - Remove the details toggle from the right-side row so compaction remains visually separate from show/hide details. + - Keep existing tooltip/accessibility strings and expansion persistence behavior unchanged. + +5. **Tests** + - Update `SessionHeaderPanelTest.kt` to assert: + - collapsed/expanded header details icons use the selected `AllIcons` constants; + - the details toggle persists expansion state as before; + - the details toggle is parented/laid out separately from the compact button. + - Update `AbstractSessionPartViewTest.kt` to assert collapsible parts keep the same icon dimensions across collapsed/expanded states and no longer use the mismatched right/down pair. + - Update `QuestionResultViewTest.kt` similarly because it has its own chevron implementation. + - Update `ReasoningViewTest.kt` for the brain icon. + +6. **Verification** + - Run the smallest relevant JetBrains checks from `packages/kilo-jetbrains/`: + - `./gradlew test --tests "ai.kilocode.client.session.views.ReasoningViewTest" --tests "ai.kilocode.client.session.views.base.AbstractSessionPartViewTest" --tests "ai.kilocode.client.session.views.QuestionResultViewTest" --tests "ai.kilocode.client.session.ui.header.SessionHeaderPanelTest"` + - `./gradlew typecheck` + - If the filtered Gradle test syntax is not accepted by the project, run `./gradlew test` from `packages/kilo-jetbrains/` instead. + +## Notes +- No shared upstream `opencode` files are involved; changes stay under `packages/kilo-jetbrains/`. +- A changeset may be needed because this is user-facing JetBrains UI polish; confirm existing changeset policy for the private JetBrains package during implementation. diff --git a/.opencode-version b/.opencode-version index 2bc9bb1ba3e..fff8b777303 100644 --- a/.opencode-version +++ b/.opencode-version @@ -1 +1 @@ -v1.14.51 +v1.15.4 diff --git a/.opencode/agent/triage.md b/.opencode/agent/triage.md index 69ab7ac86b6..5215fb7c2a9 100644 --- a/.opencode/agent/triage.md +++ b/.opencode/agent/triage.md @@ -1,7 +1,7 @@ --- mode: primary hidden: true -model: openai/gpt-5-nano +model: kilo/openai/gpt-5-nano # kilocode_change color: "#44BA81" tools: "*": false diff --git a/bun.lock b/bun.lock index 63f5b9823d9..b47af1c7e9b 100644 --- a/bun.lock +++ b/bun.lock @@ -33,7 +33,7 @@ }, "packages/core": { "name": "@opencode-ai/core", - "version": "7.3.45", + "version": "7.3.46", "bin": { "opencode": "./bin/opencode", }, @@ -96,7 +96,7 @@ }, "packages/http-recorder": { "name": "@opencode-ai/http-recorder", - "version": "7.3.45", + "version": "7.3.46", "dependencies": { "@effect/platform-node": "catalog:", "effect": "catalog:", @@ -109,7 +109,7 @@ }, "packages/kilo-console": { "name": "@kilocode/kilo-console", - "version": "7.3.45", + "version": "7.3.46", "dependencies": { "@kilocode/kilo-web-ui": "workspace:*", "@kilocode/sdk": "workspace:*", @@ -131,7 +131,7 @@ }, "packages/kilo-docs": { "name": "@kilocode/kilo-docs", - "version": "7.3.45", + "version": "7.3.46", "dependencies": { "@docsearch/css": "^4", "@docsearch/js": "^4", @@ -161,7 +161,7 @@ }, "packages/kilo-gateway": { "name": "@kilocode/kilo-gateway", - "version": "7.3.45", + "version": "7.3.46", "dependencies": { "@ai-sdk/alibaba": "1.0.17", "@ai-sdk/anthropic": "3.0.71", @@ -198,7 +198,7 @@ }, "packages/kilo-i18n": { "name": "@kilocode/kilo-i18n", - "version": "7.3.45", + "version": "7.3.46", "devDependencies": { "@tsconfig/node22": "catalog:", "@types/bun": "catalog:", @@ -208,7 +208,7 @@ }, "packages/kilo-indexing": { "name": "@kilocode/kilo-indexing", - "version": "7.3.45", + "version": "7.3.46", "dependencies": { "@aws-sdk/client-bedrock-runtime": "3.1005.0", "@aws-sdk/credential-provider-ini": "3.972.31", @@ -240,11 +240,11 @@ }, "packages/kilo-jetbrains": { "name": "@kilocode/kilo-jetbrains", - "version": "7.3.45", + "version": "7.3.46", }, "packages/kilo-telemetry": { "name": "@kilocode/kilo-telemetry", - "version": "7.3.45", + "version": "7.3.46", "dependencies": { "@kilocode/kilo-gateway": "workspace:*", "posthog-node": "4.4.0", @@ -258,7 +258,7 @@ }, "packages/kilo-ui": { "name": "@kilocode/kilo-ui", - "version": "7.3.45", + "version": "7.3.46", "dependencies": { "@kilocode/sdk": "workspace:*", "@kobalte/core": "0.13.11", @@ -295,13 +295,14 @@ }, "packages/kilo-vscode": { "name": "kilo-code", - "version": "7.3.45", + "version": "7.3.46", "dependencies": { "@anthropic-ai/sdk": "^0.39.0", "@kilocode/kilo-gateway": "workspace:*", "@kilocode/kilo-i18n": "workspace:*", "@kilocode/kilo-indexing": "workspace:*", "@kilocode/kilo-ui": "workspace:*", + "@kilocode/plugin": "workspace:*", "@kilocode/sdk": "workspace:*", "@opencode-ai/ui": "workspace:*", "@pierre/diffs": "catalog:", @@ -361,7 +362,7 @@ }, "packages/kilo-web-ui": { "name": "@kilocode/kilo-web-ui", - "version": "7.3.45", + "version": "7.3.46", "dependencies": { "@kilocode/kilo-ui": "workspace:*", "@kobalte/core": "catalog:", @@ -376,7 +377,7 @@ }, "packages/llm": { "name": "@opencode-ai/llm", - "version": "7.3.45", + "version": "7.3.46", "dependencies": { "@smithy/eventstream-codec": "4.2.14", "@smithy/util-utf8": "4.2.2", @@ -394,7 +395,7 @@ }, "packages/opencode": { "name": "@kilocode/cli", - "version": "7.3.45", + "version": "7.3.46", "bin": { "kilo": "./bin/kilo", "kilocode": "./bin/kilo", @@ -562,7 +563,7 @@ }, "packages/plugin": { "name": "@kilocode/plugin", - "version": "7.3.45", + "version": "7.3.46", "dependencies": { "@kilocode/sdk": "workspace:*", "effect": "catalog:", @@ -578,9 +579,9 @@ "typescript": "catalog:", }, "peerDependencies": { - "@opentui/core": ">=0.2.10", - "@opentui/keymap": ">=0.2.10", - "@opentui/solid": ">=0.2.10", + "@opentui/core": ">=0.2.11", + "@opentui/keymap": ">=0.2.11", + "@opentui/solid": ">=0.2.11", }, "optionalPeers": [ "@opentui/core", @@ -590,7 +591,7 @@ }, "packages/plugin-atomic-chat": { "name": "@kilocode/plugin-atomic-chat", - "version": "7.3.45", + "version": "7.3.46", "dependencies": { "@kilocode/plugin": "workspace:*", }, @@ -604,7 +605,7 @@ }, "packages/script": { "name": "@opencode-ai/script", - "version": "7.3.45", + "version": "7.3.46", "dependencies": { "semver": "^7.6.3", }, @@ -615,7 +616,7 @@ }, "packages/sdk/js": { "name": "@kilocode/sdk", - "version": "7.3.45", + "version": "7.3.46", "dependencies": { "cross-spawn": "catalog:", }, @@ -630,7 +631,7 @@ }, "packages/storybook": { "name": "@opencode-ai/storybook", - "version": "7.3.45", + "version": "7.3.46", "devDependencies": { "@opencode-ai/ui": "workspace:*", "@solidjs/meta": "catalog:", @@ -653,7 +654,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "7.3.45", + "version": "7.3.46", "dependencies": { "@kilocode/sdk": "workspace:*", "@kobalte/core": "catalog:", @@ -746,9 +747,9 @@ "@npmcli/arborist": "9.4.0", "@octokit/rest": "22.0.0", "@openauthjs/openauth": "0.0.0-20250322224806", - "@opentui/core": "0.2.10", - "@opentui/keymap": "0.2.10", - "@opentui/solid": "0.2.10", + "@opentui/core": "0.2.11", + "@opentui/keymap": "0.2.11", + "@opentui/solid": "0.2.11", "@pierre/diffs": "1.1.22", "@playwright/test": "1.59.1", "@solid-primitives/storage": "4.3.3", @@ -1591,23 +1592,23 @@ "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], - "@opentui/core": ["@opentui/core@0.2.10", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.2.10", "@opentui/core-darwin-x64": "0.2.10", "@opentui/core-linux-arm64": "0.2.10", "@opentui/core-linux-x64": "0.2.10", "@opentui/core-win32-arm64": "0.2.10", "@opentui/core-win32-x64": "0.2.10" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-oviCtx0jYjc7F8X2b8+0IkQLg6WH47Nwl6CFeZo5dU0k6OpSbTbi07ZleObaiECAp+S1YLhAtVdgzHU7hBZlaw=="], + "@opentui/core": ["@opentui/core@0.2.11", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.2.11", "@opentui/core-darwin-x64": "0.2.11", "@opentui/core-linux-arm64": "0.2.11", "@opentui/core-linux-x64": "0.2.11", "@opentui/core-win32-arm64": "0.2.11", "@opentui/core-win32-x64": "0.2.11" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-X0zLmcDEvMrPzWYp769I7VEVb+og38vaete9tGZXu9HnJgu/paPUUplUT+6denBQccr2qx1rBYV6EtgbBpLEyw=="], - "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+lbDDj42Og+UtTZEwlHhGXichmOlkxSqn0J+Jqjat5/Tt5oZykj1NZjFIQ7ZSz4Miz7EmZwgYKE2CyOmmm9MoQ=="], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-h2MXtE2Cu3XlKVoQMXthnbhleO68zGXkoh/r1Q5pCoZh6RuXqns5/94D/aZThXBWwzPuEoyarMlxxR9OqrpvHw=="], - "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-5iAoA0aqMWWAQ93nh8Bb0ipwt9h+tvEFc88+YO9St43uUJ+XrXcmMj3T8wtl6dSu/SN0UoDWNaUMHUmtykiPtg=="], + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-Y0jbPClnOBTPSIy+2THG86MTqIG/jGFlOOKuw4JfCDqEjPBM3pLWIHnJb3WxHRi2LlvfyBxvrUTXWlW6JpI0QQ=="], - "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-EnrkxgH5K76Oi/Br1UHPZblXG5P60snmtySfnxuVaeECNZrbTkV6BV/A0WoBeWshJweGbx1D+eTF+sEEjQCi8w=="], + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-blQyyuTaW4q/OQ3whs7Kt7GCXhBUR5EQHHDdjOqQAr0HYpohUa6sbHMbiBcX2Ehc9ZWwtiaOoWiyZ5YXy2SAvg=="], - "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.10", "", { "os": "linux", "cpu": "x64" }, "sha512-fI+r3kCPqIxsWwPVGpKUQy4zHK8y+jkDRCwa3UbaUy48RQ44jMuf2RhVhmi4xmCvSc8UPJBbYsw1tLuh9kmXjg=="], + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.11", "", { "os": "linux", "cpu": "x64" }, "sha512-0nEB5+MgzQRYiVcQd1vHXPWNPWGh4JEmQTJKyG3OHnTzPaJ1FVSQ/V71ECyRSl3ymY3F+U0eW9cFgw1hCieK2w=="], - "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-8F4z2hIRgkVWcr6CMVeJ9N4+1rmURPt2Pq2GBPko8ch6rxHR+a//KD1MfphyuLTHBS1tJ4vfZSWSoiaESImtrA=="], + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-+KKH77fzm0qF8py9G2pU32DzB1bAgDMfBajrs7gKL5NtSEnknrwfh7hIs/tq41aF6j9zvIzgtykByh26tcjFog=="], - "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.2.10", "", { "os": "win32", "cpu": "x64" }, "sha512-Ki+qNBlIFW5K2wcG/RHrlPp7yEQKXeiNX3mlje25iwX62Ac5w391HBpOmUjbPoq20McPyDRnhbLfbXQSPtickg=="], + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.2.11", "", { "os": "win32", "cpu": "x64" }, "sha512-dMmb9DX0W0HWadLdgciMbonqIc1xdcKiVmaQSYxw5eGCzFRPZIOrKHByesP+2ipkMuLx85W/MJUFal/lW8XSNg=="], - "@opentui/keymap": ["@opentui/keymap@0.2.10", "", { "dependencies": { "@opentui/core": "0.2.10" }, "peerDependencies": { "@opentui/react": "0.2.10", "@opentui/solid": "0.2.10", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-80fU3Lr/98sNIpVYd8PApAeQw8A8D9BemyOGi6jGvTQCl0rxKgvaVBviDRGKxl1INTVjZy9By8UPncc2KJOuWQ=="], + "@opentui/keymap": ["@opentui/keymap@0.2.11", "", { "dependencies": { "@opentui/core": "0.2.11" }, "peerDependencies": { "@opentui/react": "0.2.11", "@opentui/solid": "0.2.11", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-pCrJrY3mTuXdDaaRneId1JsJCtGE+7prTtWihzOLZzVJTJYyYtT38gMI7MpyAoloVDfEL5cTe8C+v7wv+IYREw=="], - "@opentui/solid": ["@opentui/solid@0.2.10", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.2.10", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-+4/MB90yIQiPwg8Y4wY092yva9BvRTsJeeeEO3e2H7P8k8zxYk4G9bzuhqYLxA9mTVQ+zVDlrmFoPQhT7vpIRw=="], + "@opentui/solid": ["@opentui/solid@0.2.11", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.2.11", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-M3WHxBFORHVE0yqMJYpi9PfjXWlnRTw/LYuBhZaJv0HTo+zTs60P/ukGcwnHDWnMpTGf3BH9x0Yi2dIqjHRY6Q=="], "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], @@ -5065,6 +5066,10 @@ "openid-client/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + "opentui-spinner/@opentui/core": ["@opentui/core@0.2.10", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.2.10", "@opentui/core-darwin-x64": "0.2.10", "@opentui/core-linux-arm64": "0.2.10", "@opentui/core-linux-x64": "0.2.10", "@opentui/core-win32-arm64": "0.2.10", "@opentui/core-win32-x64": "0.2.10" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-oviCtx0jYjc7F8X2b8+0IkQLg6WH47Nwl6CFeZo5dU0k6OpSbTbi07ZleObaiECAp+S1YLhAtVdgzHU7hBZlaw=="], + + "opentui-spinner/@opentui/solid": ["@opentui/solid@0.2.10", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.2.10", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-+4/MB90yIQiPwg8Y4wY092yva9BvRTsJeeeEO3e2H7P8k8zxYk4G9bzuhqYLxA9mTVQ+zVDlrmFoPQhT7vpIRw=="], + "ora/cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], "ora/log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="], @@ -5589,6 +5594,20 @@ "opencontrol/@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], + "opentui-spinner/@opentui/core/@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+lbDDj42Og+UtTZEwlHhGXichmOlkxSqn0J+Jqjat5/Tt5oZykj1NZjFIQ7ZSz4Miz7EmZwgYKE2CyOmmm9MoQ=="], + + "opentui-spinner/@opentui/core/@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-5iAoA0aqMWWAQ93nh8Bb0ipwt9h+tvEFc88+YO9St43uUJ+XrXcmMj3T8wtl6dSu/SN0UoDWNaUMHUmtykiPtg=="], + + "opentui-spinner/@opentui/core/@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-EnrkxgH5K76Oi/Br1UHPZblXG5P60snmtySfnxuVaeECNZrbTkV6BV/A0WoBeWshJweGbx1D+eTF+sEEjQCi8w=="], + + "opentui-spinner/@opentui/core/@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.10", "", { "os": "linux", "cpu": "x64" }, "sha512-fI+r3kCPqIxsWwPVGpKUQy4zHK8y+jkDRCwa3UbaUy48RQ44jMuf2RhVhmi4xmCvSc8UPJBbYsw1tLuh9kmXjg=="], + + "opentui-spinner/@opentui/core/@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-8F4z2hIRgkVWcr6CMVeJ9N4+1rmURPt2Pq2GBPko8ch6rxHR+a//KD1MfphyuLTHBS1tJ4vfZSWSoiaESImtrA=="], + + "opentui-spinner/@opentui/core/@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.2.10", "", { "os": "win32", "cpu": "x64" }, "sha512-Ki+qNBlIFW5K2wcG/RHrlPp7yEQKXeiNX3mlje25iwX62Ac5w391HBpOmUjbPoq20McPyDRnhbLfbXQSPtickg=="], + + "opentui-spinner/@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="], + "ora/log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], "p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], @@ -5819,6 +5838,8 @@ "mocha/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "opentui-spinner/@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "pkg-conf/find-up/locate-path/p-locate": ["p-locate@6.0.0", "", { "dependencies": { "p-limit": "^4.0.0" } }, "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw=="], "pkg-up/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="], diff --git a/flake.nix b/flake.nix index 56438ab332e..7383da725db 100644 --- a/flake.nix +++ b/flake.nix @@ -76,8 +76,11 @@ }; kilo-dev = pkgs.writeShellScriptBin "kilo-dev" '' - cd "$KILO_ROOT" - exec ${bun}/bin/bun dev "$@" + set -euo pipefail + + : "''${KILO_ROOT:?KILO_ROOT is not set. Enter the flake dev shell from the repo root.}" + export KILO_DEV_CWD="$PWD" + exec ${bun}/bin/bun --cwd "$KILO_ROOT/packages/opencode" --conditions=browser ./src/index.ts "$@" ''; kilo-install-bin = pkgs.writeShellScriptBin "kilo-install" '' diff --git a/nix/hashes.json b/nix/hashes.json index 153fc112847..d484352aa82 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-Q/F/FbHmJ2To96E8Y/iLL+nILLOk+oB+qpGC3P0T43Q=", - "aarch64-linux": "sha256-wvXhCOmdLwQv8/mshDHPBrqL1slOs+Q1oOCzF1J6ZVs=", - "aarch64-darwin": "sha256-4E77I50PSaI6S+euQOz6SDvasAtJadicXqxlPVBj5nA=", - "x86_64-darwin": "sha256-Ebz3fGdA85Akrbau0PNIr+m+Yu/C0nz7AXNMOOHFhXQ=" + "x86_64-linux": "sha256-jvRriUZ4wVKCs5Lek4b+Ln98WJe3QUZ9xzLX7E6XUG4=", + "aarch64-linux": "sha256-tcr6VJXR0H0iCS4LaRPb8tjapNIt8+WVn1jyWWiwUdg=", + "aarch64-darwin": "sha256-Rwg8EbHvxNo1mDr2RqPUDAX7QR//pFcnmgXfDbIx4oY=", + "x86_64-darwin": "sha256-Wsu1lxKb/gzdi7i0iwok9Uejk9QLEn/K3jaqURjsyT0=" } } diff --git a/package.json b/package.json index af88022f336..9317d05d31d 100644 --- a/package.json +++ b/package.json @@ -32,8 +32,8 @@ "@types/cross-spawn": "6.0.6", "@octokit/rest": "22.0.0", "@hono/zod-validator": "0.4.2", - "@opentui/core": "0.2.10", - "@opentui/solid": "0.2.10", + "@opentui/core": "0.2.11", + "@opentui/solid": "0.2.11", "ulid": "3.0.1", "@kobalte/core": "0.13.11", "@types/luxon": "3.7.1", @@ -78,7 +78,7 @@ "solid-js": "1.9.12", "vite-plugin-solid": "2.11.10", "@lydell/node-pty": "1.2.0-beta.10", - "@opentui/keymap": "0.2.10" + "@opentui/keymap": "0.2.11" } }, "devDependencies": { @@ -153,6 +153,6 @@ "solid-js@1.9.10": "patches/solid-js@1.9.10.patch", "mammoth@1.12.0": "patches/mammoth@1.12.0.patch" }, - "version": "7.3.45", + "version": "7.3.46", "peerDependencies": {} } diff --git a/packages/core/package.json b/packages/core/package.json index 2d0b4fb7ad4..9b2c68a6f6c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "7.3.45", + "version": "7.3.46", "name": "@opencode-ai/core", "type": "module", "license": "MIT", diff --git a/packages/core/src/catalog.ts b/packages/core/src/catalog.ts index 70cdba78563..d27f17bfb87 100644 --- a/packages/core/src/catalog.ts +++ b/packages/core/src/catalog.ts @@ -5,7 +5,8 @@ import { produce, type Draft } from "immer" import { ModelV2 } from "./model" import { PluginV2 } from "./plugin" import { ProviderV2 } from "./provider" -import { Instance } from "./instance" +import { Location } from "./location" +import { EventV2 } from "./event" type ProviderRecord = { provider: ProviderV2.Info @@ -24,6 +25,15 @@ export class ModelNotFoundError extends Schema.TaggedErrorClass Effect.Effect @@ -57,10 +67,11 @@ export class Service extends Context.Service()("@opencode/v2 export const layer = Layer.effect( Service, Effect.gen(function* () { - yield* Instance.Service + yield* Location.Service let records = HashMap.empty() let defaultModel: { providerID: ProviderV2.ID; modelID: ModelV2.ID } | undefined const plugin = yield* PluginV2.Service + const events = yield* EventV2.Service const resolve = (model: ModelV2.Info) => { const provider = Option.getOrThrow(HashMap.get(records, model.providerID)).provider @@ -157,14 +168,12 @@ export const layer = Layer.effect( ) const updated = yield* plugin.trigger("model.update", {}, { model, cancel: false }) if (updated.cancel) return + const next = new ModelV2.Info({ ...updated.model, id: modelID, providerID }) records = HashMap.set(records, providerID, { provider: record.provider, - models: HashMap.set( - record.models, - modelID, - new ModelV2.Info({ ...updated.model, id: modelID, providerID }), - ), + models: HashMap.set(record.models, modelID, next), }) + yield* events.publish(Event.ModelUpdated, { model: resolve(next) }) return }), @@ -257,4 +266,4 @@ export const layer = Layer.effect( const SMALL_MODEL_RE = /\b(nano|flash|lite|mini|haiku|small|fast)\b/ -export const defaultLayer = layer.pipe(Layer.provide(PluginV2.defaultLayer)) +export const defaultLayer = layer.pipe(Layer.provideMerge(EventV2.defaultLayer), Layer.provide(PluginV2.defaultLayer)) diff --git a/packages/core/src/event.ts b/packages/core/src/event.ts new file mode 100644 index 00000000000..e01dc5b0d63 --- /dev/null +++ b/packages/core/src/event.ts @@ -0,0 +1,157 @@ +import { Context, Effect, Layer, Option, PubSub, Schema, Stream } from "effect" +import { Location } from "./location" +import { withStatics } from "./schema" +import { Identifier } from "./util/identifier" + +export const ID = Schema.String.pipe( + Schema.brand("Event.ID"), + withStatics((schema) => ({ create: () => schema.make("evt_" + Identifier.ascending()) })), +) +export type ID = typeof ID.Type + +export type Definition = { + readonly type: Type + readonly version?: number + readonly aggregate?: string + readonly data: DataSchema +} + +export type Data = Schema.Schema.Type + +export type Payload = { + readonly id: ID + readonly type: D["type"] + readonly data: Data + readonly version?: number + readonly location?: Location.Ref + readonly metadata?: Record +} + +export type Sync = (event: Payload) => Effect.Effect + +export const registry = new Map() + +export function define(input: { + readonly type: Type + readonly version?: number + readonly aggregate?: string + readonly schema: Fields +}): Schema.Schema>>> & Definition> { + const Data = Schema.Struct(input.schema) + const Payload = Schema.Struct({ + id: ID, + metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), + type: Schema.Literal(input.type), + version: Schema.optional(Schema.Number), + location: Schema.optional(Location.Ref), + data: Data, + }).annotate({ identifier: input.type }) + + const definition = Object.assign(Payload, { + type: input.type, + ...(input.version === undefined ? {} : { version: input.version }), + ...(input.aggregate === undefined ? {} : { aggregate: input.aggregate }), + data: Data, + }) + registry.set(input.type, definition) + return definition as Schema.Schema>>> & + Definition> +} + +export function definitions() { + return registry.values().toArray() +} + +export interface PublishOptions { + readonly id?: ID + readonly metadata?: Record +} + +export type Unsubscribe = Effect.Effect + +export interface Interface { + readonly publish: ( + definition: D, + data: Data, + options?: PublishOptions, + ) => Effect.Effect> + readonly publishEvent: (event: Payload) => Effect.Effect> + readonly subscribe: (definition: D) => Stream.Stream> + readonly all: () => Stream.Stream + readonly sync: (handler: Sync) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/Event") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const all = yield* PubSub.unbounded() + const typed = new Map>() + const syncHandlers = new Array() + + const getOrCreate = (definition: Definition) => + Effect.gen(function* () { + const existing = typed.get(definition.type) + if (existing) return existing + const pubsub = yield* PubSub.unbounded() + typed.set(definition.type, pubsub) + return pubsub + }) + + yield* Effect.addFinalizer(() => + Effect.gen(function* () { + yield* PubSub.shutdown(all) + yield* Effect.forEach(typed.values(), PubSub.shutdown, { discard: true }) + }), + ) + + function publishEvent(event: Payload) { + return Effect.gen(function* () { + for (const sync of syncHandlers) { + yield* sync(event as Payload) + } + const pubsub = typed.get(event.type) + if (pubsub) yield* PubSub.publish(pubsub, event as Payload) + yield* PubSub.publish(all, event as Payload) + return event + }) + } + + function publish(definition: D, data: Data, options?: PublishOptions) { + return Effect.gen(function* () { + const location = Option.getOrUndefined(yield* Effect.serviceOption(Location.Service)) + const event = { + id: options?.id ?? ID.create(), + ...(options?.metadata ? { metadata: options.metadata } : {}), + type: definition.type, + ...(definition.version === undefined ? {} : { version: definition.version }), + ...(location ? { location } : {}), + data, + } as Payload + return yield* publishEvent(event) + }) + } + + const subscribe = (definition: D): Stream.Stream> => + Stream.unwrap(getOrCreate(definition).pipe(Effect.map((pubsub) => Stream.fromPubSub(pubsub)))).pipe( + Stream.map((event) => event as Payload), + ) + + const streamAll = (): Stream.Stream => Stream.fromPubSub(all) + const sync = (handler: Sync): Effect.Effect => + Effect.sync(() => { + syncHandlers.push(handler) + return Effect.sync(() => { + const index = syncHandlers.indexOf(handler) + if (index >= 0) syncHandlers.splice(index, 1) + }) + }) + + return Service.of({ publish, publishEvent, subscribe, all: streamAll, sync }) + }), +) + +export const defaultLayer = layer + +export * as EventV2 from "./event" diff --git a/packages/core/src/flag/flag.ts b/packages/core/src/flag/flag.ts index ab0647c8af3..0ed6eb47700 100644 --- a/packages/core/src/flag/flag.ts +++ b/packages/core/src/flag/flag.ts @@ -53,7 +53,7 @@ export const Flag = { KILO_DISABLE_CLAUDE_CODE_PROMPT: KILO_DISABLE_CLAUDE_CODE || truthy("KILO_DISABLE_CLAUDE_CODE_PROMPT"), KILO_DISABLE_CLAUDE_CODE_SKILLS, KILO_DISABLE_EXTERNAL_SKILLS: truthy("KILO_DISABLE_EXTERNAL_SKILLS"), - KILO_EXPERIMENTAL_CUSTOMIZE_SKILL: unstableDefault("KILO_EXPERIMENTAL_CUSTOMIZE_SKILL"), + KILO_EXPERIMENTAL_CUSTOMIZE_SKILL: unstableDefault("KILO_EXPERIMENTAL_CUSTOMIZE_SKILL"), // kilocode_change KILO_FAKE_VCS: process.env["KILO_FAKE_VCS"], KILO_SERVER_PASSWORD: process.env["KILO_SERVER_PASSWORD"], KILO_SERVER_USERNAME: process.env["KILO_SERVER_USERNAME"], diff --git a/packages/core/src/instance-layer.ts b/packages/core/src/instance-layer.ts deleted file mode 100644 index 20e6ac1aceb..00000000000 --- a/packages/core/src/instance-layer.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Layer, LayerMap } from "effect" -import { Instance } from "./instance" -import { Catalog } from "./catalog" -import { PluginBoot } from "./plugin/boot" - -export class InstanceServiceMap extends LayerMap.Service()("@opencode/example/InstanceServiceMap", { - lookup: (ref: Instance.Ref) => { - const instance = Layer.succeed(Instance.Service, Instance.Service.of(ref)) - return Layer.mergeAll(Catalog.defaultLayer, PluginBoot.defaultLayer).pipe(Layer.provide(instance)) - }, - idleTimeToLive: "5 minutes", -}) {} diff --git a/packages/core/src/instance.ts b/packages/core/src/instance.ts deleted file mode 100644 index af6de951be4..00000000000 --- a/packages/core/src/instance.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Context } from "effect" - -export * as Instance from "./instance" - -export type Ref = { - readonly directory: string - readonly workspaceID?: string -} - -export class Service extends Context.Service()("@opencode/Instance") {} diff --git a/packages/core/src/location-layer.ts b/packages/core/src/location-layer.ts new file mode 100644 index 00000000000..84dfb3dfae7 --- /dev/null +++ b/packages/core/src/location-layer.ts @@ -0,0 +1,12 @@ +import { Layer, LayerMap } from "effect" +import { Location } from "./location" +import { Catalog } from "./catalog" +import { PluginBoot } from "./plugin/boot" + +export class LocationServiceMap extends LayerMap.Service()("@opencode/example/LocationServiceMap", { + lookup: (ref: Location.Ref) => { + const location = Layer.succeed(Location.Service, Location.Service.of(ref)) + return Layer.mergeAll(Catalog.defaultLayer, PluginBoot.defaultLayer).pipe(Layer.provide(location)) + }, + idleTimeToLive: "5 minutes", +}) {} diff --git a/packages/core/src/location.ts b/packages/core/src/location.ts new file mode 100644 index 00000000000..00ff9cd3ea7 --- /dev/null +++ b/packages/core/src/location.ts @@ -0,0 +1,11 @@ +import { Context, Schema } from "effect" + +export * as Location from "./location" + +export const Ref = Schema.Struct({ + directory: Schema.String, + workspaceID: Schema.optional(Schema.String), +}).annotate({ identifier: "Location.Ref" }) +export type Ref = typeof Ref.Type + +export class Service extends Context.Service()("@opencode/Location") {} diff --git a/packages/opencode/src/v2/session-event.ts b/packages/core/src/session-event.ts similarity index 71% rename from packages/opencode/src/v2/session-event.ts rename to packages/core/src/session-event.ts index 1fd0f909d5a..a98d9cc0514 100644 --- a/packages/opencode/src/v2/session-event.ts +++ b/packages/core/src/session-event.ts @@ -1,12 +1,13 @@ -import { SessionID } from "@/session/schema" -import { NonNegativeInt } from "@opencode-ai/core/schema" -import { EventV2 } from "./event" -import { FileAttachment, Prompt } from "@opencode-ai/core/session-prompt" import { Schema } from "effect" +import { EventV2 } from "./event" +import { ModelV2 } from "./model" +import { NonNegativeInt } from "./schema" +import { Session } from "./session" +import { FileAttachment, Prompt } from "./session-prompt" +import { ToolOutput } from "./tool-output" +import { V2Schema } from "./v2-schema" + export { FileAttachment } -import { ToolOutput } from "@opencode-ai/core/tool-output" -import { V2Schema } from "@opencode-ai/core/v2-schema" -import { ModelV2 } from "@opencode-ai/core/model" export const Source = Schema.Struct({ start: NonNegativeInt, @@ -15,92 +16,94 @@ export const Source = Schema.Struct({ }).annotate({ identifier: "session.next.event.source", }) -export type Source = Schema.Schema.Type +export type Source = typeof Source.Type const Base = { timestamp: V2Schema.DateTimeUtcFromMillis, - sessionID: SessionID, + sessionID: Session.ID, } +const options = { + aggregate: "sessionID", + version: 1, +} as const + export const UnknownError = Schema.Struct({ type: Schema.Literal("unknown"), message: Schema.String, }).annotate({ identifier: "Session.Error.Unknown", }) -export type UnknownError = Schema.Schema.Type +export type UnknownError = typeof UnknownError.Type export const AgentSwitched = EventV2.define({ type: "session.next.agent.switched", - aggregate: "sessionID", - version: 1, + ...options, schema: { ...Base, agent: Schema.String, }, }) -export type AgentSwitched = Schema.Schema.Type +export type AgentSwitched = typeof AgentSwitched.Type export const ModelSwitched = EventV2.define({ type: "session.next.model.switched", - aggregate: "sessionID", - version: 1, + ...options, schema: { ...Base, model: ModelV2.Ref, }, }) -export type ModelSwitched = Schema.Schema.Type +export type ModelSwitched = typeof ModelSwitched.Type export const Prompted = EventV2.define({ type: "session.next.prompted", - aggregate: "sessionID", - version: 1, + ...options, schema: { ...Base, prompt: Prompt, }, }) -export type Prompted = Schema.Schema.Type +export type Prompted = typeof Prompted.Type export const Synthetic = EventV2.define({ type: "session.next.synthetic", - aggregate: "sessionID", + ...options, schema: { ...Base, text: Schema.String, }, }) -export type Synthetic = Schema.Schema.Type +export type Synthetic = typeof Synthetic.Type export namespace Shell { export const Started = EventV2.define({ type: "session.next.shell.started", - aggregate: "sessionID", + ...options, schema: { ...Base, callID: Schema.String, command: Schema.String, }, }) - export type Started = Schema.Schema.Type + export type Started = typeof Started.Type export const Ended = EventV2.define({ type: "session.next.shell.ended", - aggregate: "sessionID", + ...options, schema: { ...Base, callID: Schema.String, output: Schema.String, }, }) - export type Ended = Schema.Schema.Type + export type Ended = typeof Ended.Type } export namespace Step { export const Started = EventV2.define({ type: "session.next.step.started", - aggregate: "sessionID", + ...options, schema: { ...Base, agent: Schema.String, @@ -108,11 +111,11 @@ export namespace Step { snapshot: Schema.String.pipe(Schema.optional), }, }) - export type Started = Schema.Schema.Type + export type Started = typeof Started.Type export const Ended = EventV2.define({ type: "session.next.step.ended", - aggregate: "sessionID", + ...options, schema: { ...Base, finish: Schema.String, @@ -129,123 +132,123 @@ export namespace Step { snapshot: Schema.String.pipe(Schema.optional), }, }) - export type Ended = Schema.Schema.Type + export type Ended = typeof Ended.Type export const Failed = EventV2.define({ type: "session.next.step.failed", - aggregate: "sessionID", + ...options, schema: { ...Base, error: UnknownError, }, }) - export type Failed = Schema.Schema.Type + export type Failed = typeof Failed.Type } export namespace Text { export const Started = EventV2.define({ type: "session.next.text.started", - aggregate: "sessionID", + ...options, schema: { ...Base, }, }) - export type Started = Schema.Schema.Type + export type Started = typeof Started.Type export const Delta = EventV2.define({ type: "session.next.text.delta", - aggregate: "sessionID", + ...options, schema: { ...Base, delta: Schema.String, }, }) - export type Delta = Schema.Schema.Type + export type Delta = typeof Delta.Type export const Ended = EventV2.define({ type: "session.next.text.ended", - aggregate: "sessionID", + ...options, schema: { ...Base, text: Schema.String, }, }) - export type Ended = Schema.Schema.Type + export type Ended = typeof Ended.Type } export namespace Reasoning { export const Started = EventV2.define({ type: "session.next.reasoning.started", - aggregate: "sessionID", + ...options, schema: { ...Base, reasoningID: Schema.String, }, }) - export type Started = Schema.Schema.Type + export type Started = typeof Started.Type export const Delta = EventV2.define({ type: "session.next.reasoning.delta", - aggregate: "sessionID", + ...options, schema: { ...Base, reasoningID: Schema.String, delta: Schema.String, }, }) - export type Delta = Schema.Schema.Type + export type Delta = typeof Delta.Type export const Ended = EventV2.define({ type: "session.next.reasoning.ended", - aggregate: "sessionID", + ...options, schema: { ...Base, reasoningID: Schema.String, text: Schema.String, }, }) - export type Ended = Schema.Schema.Type + export type Ended = typeof Ended.Type } export namespace Tool { export namespace Input { export const Started = EventV2.define({ type: "session.next.tool.input.started", - aggregate: "sessionID", + ...options, schema: { ...Base, callID: Schema.String, name: Schema.String, }, }) - export type Started = Schema.Schema.Type + export type Started = typeof Started.Type export const Delta = EventV2.define({ type: "session.next.tool.input.delta", - aggregate: "sessionID", + ...options, schema: { ...Base, callID: Schema.String, delta: Schema.String, }, }) - export type Delta = Schema.Schema.Type + export type Delta = typeof Delta.Type export const Ended = EventV2.define({ type: "session.next.tool.input.ended", - aggregate: "sessionID", + ...options, schema: { ...Base, callID: Schema.String, text: Schema.String, }, }) - export type Ended = Schema.Schema.Type + export type Ended = typeof Ended.Type } export const Called = EventV2.define({ type: "session.next.tool.called", - aggregate: "sessionID", + ...options, schema: { ...Base, callID: Schema.String, @@ -257,11 +260,11 @@ export namespace Tool { }), }, }) - export type Called = Schema.Schema.Type + export type Called = typeof Called.Type export const Progress = EventV2.define({ type: "session.next.tool.progress", - aggregate: "sessionID", + ...options, schema: { ...Base, callID: Schema.String, @@ -269,11 +272,11 @@ export namespace Tool { content: Schema.Array(ToolOutput.Content), }, }) - export type Progress = Schema.Schema.Type + export type Progress = typeof Progress.Type export const Success = EventV2.define({ type: "session.next.tool.success", - aggregate: "sessionID", + ...options, schema: { ...Base, callID: Schema.String, @@ -285,11 +288,11 @@ export namespace Tool { }), }, }) - export type Success = Schema.Schema.Type + export type Success = typeof Success.Type export const Failed = EventV2.define({ type: "session.next.tool.failed", - aggregate: "sessionID", + ...options, schema: { ...Base, callID: Schema.String, @@ -300,7 +303,7 @@ export namespace Tool { }), }, }) - export type Failed = Schema.Schema.Type + export type Failed = typeof Failed.Type } export const RetryError = Schema.Struct({ @@ -313,49 +316,50 @@ export const RetryError = Schema.Struct({ }).annotate({ identifier: "session.next.retry_error", }) -export type RetryError = Schema.Schema.Type +export type RetryError = typeof RetryError.Type export const Retried = EventV2.define({ type: "session.next.retried", - aggregate: "sessionID", + ...options, schema: { ...Base, attempt: Schema.Finite, error: RetryError, }, }) -export type Retried = Schema.Schema.Type +export type Retried = typeof Retried.Type export namespace Compaction { export const Started = EventV2.define({ type: "session.next.compaction.started", - aggregate: "sessionID", + ...options, schema: { ...Base, reason: Schema.Union([Schema.Literal("auto"), Schema.Literal("manual")]), }, }) - export type Started = Schema.Schema.Type + export type Started = typeof Started.Type export const Delta = EventV2.define({ type: "session.next.compaction.delta", - aggregate: "sessionID", + ...options, schema: { ...Base, text: Schema.String, }, }) + export type Delta = typeof Delta.Type export const Ended = EventV2.define({ type: "session.next.compaction.ended", - aggregate: "sessionID", + ...options, schema: { ...Base, text: Schema.String, include: Schema.String.pipe(Schema.optional), }, }) - export type Ended = Schema.Schema.Type + export type Ended = typeof Ended.Type } export const All = Schema.Union( @@ -392,16 +396,7 @@ export const All = Schema.Union( }, ).pipe(Schema.toTaggedUnion("type")) -// user -// assistant -// assistant -// assistant -// user -// compaction marker -// -> text -// assistant - -export type Event = Schema.Schema.Type +export type Event = typeof All.Type export type Type = Event["type"] export * as SessionEvent from "./session-event" diff --git a/packages/opencode/src/v2/session-message-updater.ts b/packages/core/src/session-message-updater.ts similarity index 100% rename from packages/opencode/src/v2/session-message-updater.ts rename to packages/core/src/session-message-updater.ts diff --git a/packages/opencode/src/v2/session-message.ts b/packages/core/src/session-message.ts similarity index 87% rename from packages/opencode/src/v2/session-message.ts rename to packages/core/src/session-message.ts index fa7c299ae57..73b6dd7da2b 100644 --- a/packages/opencode/src/v2/session-message.ts +++ b/packages/core/src/session-message.ts @@ -1,10 +1,10 @@ import { Schema } from "effect" -import { Prompt } from "@opencode-ai/core/session-prompt" +import { Prompt } from "./session-prompt" import { SessionEvent } from "./session-event" import { EventV2 } from "./event" -import { ToolOutput } from "@opencode-ai/core/tool-output" -import { V2Schema } from "@opencode-ai/core/v2-schema" -import { ModelV2 } from "@opencode-ai/core/model" +import { ToolOutput } from "./tool-output" +import { V2Schema } from "./v2-schema" +import { ModelV2 } from "./model" export const ID = EventV2.ID export type ID = Schema.Schema.Type @@ -20,7 +20,7 @@ const Base = { export class AgentSwitched extends Schema.Class("Session.Message.AgentSwitched")({ ...Base, type: Schema.Literal("agent-switched"), - agent: SessionEvent.AgentSwitched.fields.data.fields.agent, + agent: SessionEvent.AgentSwitched.data.fields.agent, }) {} export class ModelSwitched extends Schema.Class("Session.Message.ModelSwitched")({ @@ -43,16 +43,16 @@ export class User extends Schema.Class("Session.Message.User")({ export class Synthetic extends Schema.Class("Session.Message.Synthetic")({ ...Base, - sessionID: SessionEvent.Synthetic.fields.data.fields.sessionID, - text: SessionEvent.Synthetic.fields.data.fields.text, + sessionID: SessionEvent.Synthetic.data.fields.sessionID, + text: SessionEvent.Synthetic.data.fields.text, type: Schema.Literal("synthetic"), }) {} export class Shell extends Schema.Class("Session.Message.Shell")({ ...Base, type: Schema.Literal("shell"), - callID: SessionEvent.Shell.Started.fields.data.fields.callID, - command: SessionEvent.Shell.Started.fields.data.fields.command, + callID: SessionEvent.Shell.Started.data.fields.callID, + command: SessionEvent.Shell.Started.data.fields.command, output: Schema.String, time: Schema.Struct({ created: V2Schema.DateTimeUtcFromMillis, @@ -130,7 +130,7 @@ export class Assistant extends Schema.Class("Session.Message.Assistan ...Base, type: Schema.Literal("assistant"), agent: Schema.String, - model: SessionEvent.Step.Started.fields.data.fields.model, + model: SessionEvent.Step.Started.data.fields.model, content: AssistantContent.pipe(Schema.Array), snapshot: Schema.Struct({ start: Schema.String.pipe(Schema.optional), @@ -147,7 +147,7 @@ export class Assistant extends Schema.Class("Session.Message.Assistan write: Schema.Finite, }), }).pipe(Schema.optional), - error: SessionEvent.Step.Failed.fields.data.fields.error.pipe(Schema.optional), + error: SessionEvent.Step.Failed.data.fields.error.pipe(Schema.optional), time: Schema.Struct({ created: V2Schema.DateTimeUtcFromMillis, completed: V2Schema.DateTimeUtcFromMillis.pipe(Schema.optional), @@ -156,7 +156,7 @@ export class Assistant extends Schema.Class("Session.Message.Assistan export class Compaction extends Schema.Class("Session.Message.Compaction")({ type: Schema.Literal("compaction"), - reason: SessionEvent.Compaction.Started.fields.data.fields.reason, + reason: SessionEvent.Compaction.Started.data.fields.reason, summary: Schema.String, include: Schema.String.pipe(Schema.optional), ...Base, diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts new file mode 100644 index 00000000000..756531e3280 --- /dev/null +++ b/packages/core/src/session.ts @@ -0,0 +1,13 @@ +export * as Session from "./session" + +import { Schema } from "effect" +import { withStatics } from "./schema" +import { Identifier } from "./util/identifier" + +export const ID = Schema.String.check(Schema.isStartsWith("ses")).pipe( + Schema.brand("SessionID"), + withStatics((schema) => ({ + descending: (id?: string) => schema.make(id ?? "ses_" + Identifier.descending()), + })), +) +export type ID = typeof ID.Type diff --git a/packages/core/test/catalog.test.ts b/packages/core/test/catalog.test.ts index c5ac2c06cc4..594f42d1c8b 100644 --- a/packages/core/test/catalog.test.ts +++ b/packages/core/test/catalog.test.ts @@ -1,14 +1,21 @@ import { describe, expect } from "bun:test" -import { DateTime, Effect, Layer, Option } from "effect" +import { DateTime, Effect, Fiber, Layer, Option, Stream } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { Instance } from "@opencode-ai/core/instance" +import { EventV2 } from "@opencode-ai/core/event" +import { Location } from "@opencode-ai/core/location" import { ModelV2 } from "@opencode-ai/core/model" import { PluginV2 } from "@opencode-ai/core/plugin" import { ProviderV2 } from "@opencode-ai/core/provider" import { testEffect } from "./lib/effect" -const instanceLayer = Layer.succeed(Instance.Service, Instance.Service.of({ directory: "test" })) -const it = testEffect(Catalog.layer.pipe(Layer.provideMerge(PluginV2.defaultLayer), Layer.provide(instanceLayer))) +const locationLayer = Layer.succeed(Location.Service, Location.Service.of({ directory: "test" })) +const it = testEffect( + Catalog.layer.pipe( + Layer.provideMerge(EventV2.defaultLayer), + Layer.provideMerge(PluginV2.defaultLayer), + Layer.provideMerge(locationLayer), + ), +) describe("CatalogV2", () => { it.effect("normalizes provider baseURL into endpoint url", () => @@ -69,6 +76,31 @@ describe("CatalogV2", () => { }), ) + it.effect("publishes model updated events", () => + Effect.gen(function* () { + const catalog = yield* Catalog.Service + const events = yield* EventV2.Service + const providerID = ProviderV2.ID.make("test") + const modelID = ModelV2.ID.make("model") + const fiber = yield* events + .subscribe(Catalog.Event.ModelUpdated) + .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) + + yield* Effect.yieldNow + yield* catalog.provider.update(providerID, () => {}) + yield* catalog.model.update(providerID, modelID, (model) => { + model.name = "Updated Model" + }) + const event = Array.from(yield* Fiber.join(fiber))[0] + + expect(event?.type).toBe("catalog.model.updated") + expect(event?.data.model.providerID).toBe(providerID) + expect(event?.data.model.id).toBe(modelID) + expect(event?.data.model.name).toBe("Updated Model") + expect(event?.location).toEqual({ directory: "test" }) + }), + ) + it.effect("resolves unknown model endpoint from provider endpoint", () => Effect.gen(function* () { const catalog = yield* Catalog.Service diff --git a/packages/core/test/event.test.ts b/packages/core/test/event.test.ts new file mode 100644 index 00000000000..b67b2897a1b --- /dev/null +++ b/packages/core/test/event.test.ts @@ -0,0 +1,132 @@ +import { describe, expect } from "bun:test" +import { Effect, Fiber, Layer, Schema, Stream } from "effect" +import { EventV2 } from "@opencode-ai/core/event" +import { Location } from "@opencode-ai/core/location" +import { testEffect } from "./lib/effect" + +const locationLayer = Layer.succeed( + Location.Service, + Location.Service.of({ directory: "project", workspaceID: "workspace" }), +) +const it = testEffect(EventV2.layer.pipe(Layer.provideMerge(locationLayer))) +const itWithoutLocation = testEffect(EventV2.layer) + +const Message = EventV2.define({ + type: "test.message", + schema: { + text: Schema.String, + }, +}) + +const GlobalMessage = EventV2.define({ + type: "test.global", + schema: { + text: Schema.String, + }, +}) + +const VersionedMessage = EventV2.define({ + type: "test.versioned", + version: 2, + schema: { + text: Schema.String, + }, +}) + +describe("EventV2", () => { + it.effect("publishes events with the current location", () => + Effect.gen(function* () { + const events = yield* EventV2.Service + const fiber = yield* events.subscribe(Message).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) + yield* Effect.yieldNow + const event = yield* events.publish(Message, { text: "hello" }) + const received = Array.from(yield* Fiber.join(fiber)) + + expect(received).toEqual([event]) + expect(event.type).toBe("test.message") + expect(event).not.toHaveProperty("version") + expect(event.data).toEqual({ text: "hello" }) + expect(event.location).toEqual({ directory: "project", workspaceID: "workspace" }) + }), + ) + + itWithoutLocation.effect("omits location when no location is available", () => + Effect.gen(function* () { + const events = yield* EventV2.Service + const event = yield* events.publish(GlobalMessage, { text: "hello" }) + + expect(event).not.toHaveProperty("location") + expect(event.type).toBe("test.global") + }), + ) + + it.effect("publishes definition version", () => + Effect.gen(function* () { + const events = yield* EventV2.Service + const event = yield* events.publish(VersionedMessage, { text: "hello" }) + + expect(event.type).toBe("test.versioned") + expect(event.version).toBe(2) + }), + ) + + it.effect("stores definitions in the exported registry", () => + Effect.sync(() => { + expect(EventV2.registry.get(Message.type)).toBe(Message) + }), + ) + + it.effect("publishes to typed and wildcard subscriptions", () => + Effect.gen(function* () { + const events = yield* EventV2.Service + const typed = yield* events.subscribe(Message).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) + const wildcard = yield* events.all().pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) + yield* Effect.yieldNow + const event = yield* events.publish(Message, { text: "hello" }) + + expect(Array.from(yield* Fiber.join(typed))).toEqual([event]) + expect(Array.from(yield* Fiber.join(wildcard))).toEqual([event]) + }), + ) + + it.effect("runs sync handlers inline", () => + Effect.gen(function* () { + const events = yield* EventV2.Service + const received = new Array() + const unsubscribe = yield* events.sync((event) => + Effect.sync(() => { + received.push(event) + }), + ) + + const event = yield* events.publish(Message, { text: "hello" }) + yield* unsubscribe + yield* events.publish(Message, { text: "after unsubscribe" }) + + expect(received).toEqual([event]) + }), + ) + + it.effect("runs sync handlers before publishing to streams", () => + Effect.gen(function* () { + const events = yield* EventV2.Service + const received = new Array() + const fiber = yield* events.all().pipe( + Stream.take(1), + Stream.runForEach(() => Effect.sync(() => received.push("stream"))), + Effect.forkScoped, + ) + yield* events.sync((event) => + Effect.sync(() => { + received.push(event.type) + }), + ) + + yield* Effect.yieldNow + yield* events.publish(Message, { text: "hello" }) + yield* Fiber.join(fiber) + + expect(received).toEqual([Message.type, "stream"]) + }), + ) +}) diff --git a/packages/core/test/plugin/provider-opencode.test.ts b/packages/core/test/plugin/provider-opencode.test.ts index 8271a4a5c82..ed82686a21c 100644 --- a/packages/core/test/plugin/provider-opencode.test.ts +++ b/packages/core/test/plugin/provider-opencode.test.ts @@ -1,7 +1,7 @@ import { describe, expect } from "bun:test" import { DateTime, Effect, Layer, Option } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { Instance } from "@opencode-ai/core/instance" +import { Location } from "@opencode-ai/core/location" import { ModelV2 } from "@opencode-ai/core/model" import { PluginV2 } from "@opencode-ai/core/plugin" import { OpencodePlugin } from "@opencode-ai/core/plugin/provider/opencode" @@ -9,7 +9,7 @@ import { ProviderV2 } from "@opencode-ai/core/provider" import { it, model, provider, withEnv } from "./provider-helper" const cost = (input: number, output = 0) => [{ input, output, cache: { read: 0, write: 0 } }] -const instanceLayer = Layer.succeed(Instance.Service, Instance.Service.of({ directory: "test" })) +const locationLayer = Layer.succeed(Location.Service, Location.Service.of({ directory: "test" })) describe("OpencodePlugin", () => { it.effect("uses a public key and cancels paid models without credentials", () => @@ -192,6 +192,6 @@ describe("OpencodePlugin", () => { const selected = yield* catalog.model.small(providerID) expect(Option.getOrUndefined(selected)?.id).toBe(ModelV2.ID.make("gpt-5-nano")) - }).pipe(Effect.provide(Catalog.defaultLayer.pipe(Layer.provide(instanceLayer)))), + }).pipe(Effect.provide(Catalog.defaultLayer.pipe(Layer.provide(locationLayer)))), ) }) diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 41a7664d5b1..c0c9940f35e 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "kilo" name = "Kilo" description = "The open source coding agent." -version = "7.3.45" +version = "7.3.46" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/Kilo-Org/kilocode" @@ -11,26 +11,26 @@ name = "Kilo" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.45/opencode-darwin-arm64.zip" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.46/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.45/opencode-darwin-x64.zip" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.46/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.45/opencode-linux-arm64.tar.gz" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.46/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.45/opencode-linux-x64.tar.gz" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.46/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.45/opencode-windows-x64.zip" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.46/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/http-recorder/package.json b/packages/http-recorder/package.json index 4f195e7c6f2..24c96c918d7 100644 --- a/packages/http-recorder/package.json +++ b/packages/http-recorder/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "7.3.45", + "version": "7.3.46", "name": "@opencode-ai/http-recorder", "type": "module", "license": "MIT", diff --git a/packages/kilo-console/package.json b/packages/kilo-console/package.json index 4bf49fd6119..4a9cd9366c7 100644 --- a/packages/kilo-console/package.json +++ b/packages/kilo-console/package.json @@ -1,6 +1,6 @@ { "name": "@kilocode/kilo-console", - "version": "7.3.45", + "version": "7.3.46", "private": true, "type": "module", "scripts": { diff --git a/packages/kilo-console/src/client.ts b/packages/kilo-console/src/client.ts index adca1648a7b..37b3822b2f6 100644 --- a/packages/kilo-console/src/client.ts +++ b/packages/kilo-console/src/client.ts @@ -29,6 +29,7 @@ import type { VcsInfo, Worktree, WorktreeDiffItem, + WorktreeListResponse, } from "@kilocode/sdk/v2/client" export type Scope = "global" | "project" @@ -54,7 +55,7 @@ export type ProjectConsoleSnapshot = { project: ProjectItem config: EffectiveConfig vcs: VcsInfo - worktrees: string[] + worktrees: WorktreeListResponse terminals: ProjectTerminalItem[] } @@ -447,7 +448,7 @@ export async function loadProjectConsole(input: ProjectConsoleQuery): Promise loadProjectTerminals({ url: input.url, dir }, dir)), + [query.dir, ...dirs.map((item) => item.directory)].map((dir) => loadProjectTerminals({ url: input.url, dir }, dir)), ) return { diff --git a/packages/kilo-console/src/routes/projects/ProjectConsoleRoute.tsx b/packages/kilo-console/src/routes/projects/ProjectConsoleRoute.tsx index f4ad90840d8..b4d9e09955b 100644 --- a/packages/kilo-console/src/routes/projects/ProjectConsoleRoute.tsx +++ b/packages/kilo-console/src/routes/projects/ProjectConsoleRoute.tsx @@ -58,6 +58,7 @@ type Context = { dir: string label: string kind: "local" | "worktree" + managed: boolean } type Editor = { kind: "create"; value: string } | { kind: "rename"; item: Context; value: string } @@ -135,6 +136,7 @@ function refreshEvent(event: ProjectConsoleEvent) { if (type.startsWith("permission.")) return true if (type.startsWith("question.")) return true if (type.startsWith("message.")) return true + if (type === "global.config.updated") return true return false } @@ -179,8 +181,14 @@ export function ProjectConsoleRoute() { const data = snap() if (!data) return [] return [ - { id: "local", dir: data.project.worktree, label: "Local", kind: "local" }, - ...data.worktrees.map((dir) => ({ id: dir, dir, label: title(dir), kind: "worktree" as const })), + { id: "local", dir: data.project.worktree, label: "Local", kind: "local", managed: false }, + ...data.worktrees.map((item) => ({ + id: item.directory, + dir: item.directory, + label: title(item.directory), + kind: "worktree" as const, + managed: item.managed, + })), ] }) const terminals = createMemo(() => { @@ -499,8 +507,12 @@ export function ProjectConsoleRoute() { setEditor({ kind: "rename", item, value: displayLabel(item) }) } + function canManage(item: Context | undefined) { + return item?.kind === "worktree" && item.managed + } + function removeWorktree(item: Context) { - if (!projectInput() || item.kind === "local") return + if (!projectInput() || !canManage(item)) return setPending({ kind: "delete", item }) } @@ -512,7 +524,7 @@ export function ProjectConsoleRoute() { function resetSelected() { const item = current() - if (!projectInput() || !item || item.kind === "local") return + if (!projectInput() || !canManage(item)) return setPending({ kind: "reset", item }) } @@ -653,11 +665,14 @@ export function ProjectConsoleRoute() { const base = query() const data = snap() if (!base || !data) return - const dirs = new Set([data.project.worktree, ...data.worktrees]) + const dirs = new Set([data.project.worktree, ...data.worktrees.map((item) => item.directory)]) const stop = subscribeProjectEvents({ url: base.url, dir: data.project.worktree }, (event) => { if (event.directory !== "global" && !dirs.has(event.directory)) return const id = eventSession(event) if (id && messageEvent(event)) markUnread(id) + // Terminal fitting emits pty.updated for every width change. Ignore those refreshes while + // dragging so the controlled review accordion keeps its expanded files mounted. + if (resize.pending && eventType(event) === "pty.updated") return if (refreshEvent(event)) scheduleRefetch() }) onCleanup(stop) @@ -765,19 +780,21 @@ export function ProjectConsoleRoute() { > - + + + @@ -913,7 +930,7 @@ export function ProjectConsoleRoute() { {current()?.dir ?? snap()?.project.worktree ?? project()} - +
- {command() - ? label(rule) - : rule === "*" - ? resolveLabel(props.request.toolName, language.t) - : `${resolveLabel(props.request.toolName, language.t)} ${rule}`} + {command() ? label(rule) : describeRule(props.request.toolName, rule, language.t)} )} diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/permission-dock-utils.ts b/packages/kilo-vscode/webview-ui/src/components/chat/permission-dock-utils.ts index 8d726db2901..efa04e468fb 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/permission-dock-utils.ts +++ b/packages/kilo-vscode/webview-ui/src/components/chat/permission-dock-utils.ts @@ -52,6 +52,17 @@ export function resolveLabel(tool: string, t: (key: string) => string): string { return key ? t(key) : tool } +export function describeRule( + tool: string, + rule: string, + t: (key: string, params?: Record) => string, +): string { + if (tool === "doom_loop") { + return t("ui.permission.doomLoop.rule", { tool: resolveLabel(rule, t) }) + } + return rule === "*" ? resolveLabel(tool, t) : `${resolveLabel(tool, t)} ${rule}` +} + /** * Build a human-readable description for a permission request's patterns. * @@ -62,13 +73,20 @@ export function resolveLabel(tool: string, t: (key: string) => string): string { export function describePatterns( tool: string, patterns: string[], - t: (key: string) => string, + t: (key: string, params?: Record) => string, ): PatternDescription | null { const filtered = patterns.filter((p) => p !== "*") if (filtered.length === 0) return null - const key = TOOL_LABEL_KEYS[tool] - const label = key ? t(key) : tool + // doom-loop requests always contain one repeated tool pattern. + if (tool === "doom_loop") { + return { + kind: "single", + text: t("ui.permission.doomLoop.prompt", { tool: resolveLabel(filtered[0], t) }), + } + } + + const label = resolveLabel(tool, t) if (filtered.length === 1) return { kind: "single", text: `${label} ${filtered[0]}` } return { kind: "multi", title: `${label}:`, paths: filtered } } diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx index dd61fa49405..63d45639c48 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx @@ -43,6 +43,8 @@ interface SelectOption { import SettingsRow from "./SettingsRow" +const builtin = (skill: SkillInfo) => skill.location === "builtin" || skill.location === "" + // View states for the agents subtab type AgentView = "list" | "create" | "edit" @@ -848,10 +850,10 @@ const AgentBehaviourTab: Component = () => { }} >
{skill.description}
- {skill.location !== "builtin" &&
{skill.location}
} + {!builtin(skill) &&
{skill.location}
} - {skill.location !== "builtin" && ( + {!builtin(skill) && ( confirmRemoveSkill(skill)} /> )} diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderDialog.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderDialog.tsx index 2008b57b652..9b24cacf22e 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderDialog.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/CustomProviderDialog.tsx @@ -195,6 +195,12 @@ const CustomProviderDialog = (props: CustomProviderDialogProps) => { const raw = fetchKey().trim() const env = raw.match(/^\{env:([^}]+)\}$/)?.[1]?.trim() const apiKey = raw && !env ? raw : undefined + // When editing an existing provider with the key field untouched, the + // webview has no key to send — keys are stripped before provider data + // reaches it. Send the providerID so the extension can authenticate the + // fetch with the stored key (#10139). Anything typed into the field + // (a key or {env:VAR} syntax) takes precedence. + const providerID = !raw && props.existing ? props.existing.providerID : undefined const existing = new Set(form.models.map((m) => m.id.trim()).filter(Boolean)) const hdrs = form.headers @@ -253,6 +259,7 @@ const CustomProviderDialog = (props: CustomProviderDialogProps) => { requestId: rid, baseURL: url, apiKey, + providerID, headers, }) } diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/ModelsTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/ModelsTab.tsx index 4960afe249a..bd27ab6cc55 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/ModelsTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/ModelsTab.tsx @@ -1,5 +1,6 @@ import { Component, For, Show, createMemo } from "solid-js" import { Card } from "@kilocode/kilo-ui/card" +import { Switch } from "@kilocode/kilo-ui/switch" import { useConfig } from "../../context/config" import { useLanguage } from "../../context/language" import { useProvider } from "../../context/provider" @@ -154,7 +155,6 @@ const ModelsTab: Component = () => { { description={language.t("settings.autocomplete.model.description")} /> + + updateConfig({ hide_prompt_training_models: checked })} + hideLabel + > + {language.t("settings.models.hidePromptTraining.title")} + +

{language.t("settings.providers.modeModels")}

diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/NotificationsTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/NotificationsTab.tsx index e77f10e42a4..632d3ec84a2 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/NotificationsTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/NotificationsTab.tsx @@ -1,4 +1,5 @@ import { Component, createSignal, onCleanup } from "solid-js" +import { Button } from "@kilocode/kilo-ui/button" import { Switch } from "@kilocode/kilo-ui/switch" import { Select } from "@kilocode/kilo-ui/select" import { Card } from "@kilocode/kilo-ui/card" @@ -12,158 +13,96 @@ interface SoundOption { labelKey: string } +const groups = [ + { value: "alert", key: "alert", count: 10 }, + { value: "bip-bop", key: "bipbop", count: 10 }, + { value: "staplebops", key: "staplebops", count: 7 }, + { value: "nope", key: "nope", count: 12 }, + { value: "yup", key: "yup", count: 6 }, +] + const SOUND_OPTIONS: SoundOption[] = [ { value: "default", labelKey: "settings.notifications.sound.default" }, - { value: "none", labelKey: "settings.notifications.sound.none" }, + { value: "system", labelKey: "settings.notifications.sound.system" }, + ...groups.flatMap((group) => + Array.from({ length: group.count }, (_, index) => { + const suffix = String(index + 1).padStart(2, "0") + return { + value: `${group.value}-${suffix}`, + labelKey: `sound.option.${group.key}${suffix}`, + } + }), + ), ] const NotificationsTab: Component = () => { const vscode = useVSCode() const language = useLanguage() - - const [agentNotify, setAgentNotify] = createSignal(true) - const [permNotify, setPermNotify] = createSignal(true) - const [errorNotify, setErrorNotify] = createSignal(true) - const [agentSound, setAgentSound] = createSignal("default") - const [permSound, setPermSound] = createSignal("default") - const [errorSound, setErrorSound] = createSignal("default") + const [enabled, setEnabled] = createSignal(false) + const [sound, setSound] = createSignal("default") const unsubscribe = vscode.onMessage((message: ExtensionMessage) => { - if (message.type !== "notificationSettingsLoaded") { - return - } - const s = message.settings - setAgentNotify(s.notifyAgent) - setPermNotify(s.notifyPermissions) - setErrorNotify(s.notifyErrors) - setAgentSound(s.soundAgent) - setPermSound(s.soundPermissions) - setErrorSound(s.soundErrors) + if (message.type !== "notificationSettingsLoaded") return + setEnabled(message.settings.attentionEnabled) + setSound( + SOUND_OPTIONS.some((option) => option.value === message.settings.attentionSound) + ? message.settings.attentionSound + : "default", + ) }) onCleanup(unsubscribe) vscode.postMessage({ type: "requestNotificationSettings" }) - const save = (key: string, value: unknown) => { - vscode.postMessage({ type: "updateSetting", key, value }) - } - return ( -
- - - { - setAgentNotify(checked) - save("notifications.agent", checked) - }} - hideLabel - > - {language.t("settings.notifications.agent.title")} - - - - { - setPermNotify(checked) - save("notifications.permissions", checked) - }} - hideLabel - > - {language.t("settings.notifications.permissions.title")} - - - - { - setErrorNotify(checked) - save("notifications.errors", checked) - }} - hideLabel - > - {language.t("settings.notifications.errors.title")} - - - - -

{language.t("settings.notifications.sounds")}

- - + + { + setEnabled(checked) + vscode.postMessage({ type: "updateSetting", key: "attention.enabled", value: checked }) + }} + hideLabel > + {language.t("settings.notifications.enable.title")} + + + +
o.value === permSound())} - value={(o) => o.value} - label={(o) => language.t(o.labelKey)} - onSelect={(o) => { - if (o) { - setPermSound(o.value) - save("sounds.permissions", o.value) - } - }} - variant="secondary" +