From 8ef4bee6c6d15d6e586576c8a8d21136f8ff8304 Mon Sep 17 00:00:00 2001 From: aaron Date: Fri, 21 Aug 2026 04:03:58 -0400 Subject: [PATCH] =?UTF-8?q?feat(app-bundle):=20M2=20slice=20(b)=20?= =?UTF-8?q?=E2=80=94=20the=20complete=20app-graph=20overlay;=20M3=20port?= =?UTF-8?q?=20inventory=20machine-derived?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit M2 slice (b) of #451, with an honest scope correction discovered by the build proof: the planned additive-only scope (app+session-ui A-files) does NOT typecheck. ~10 additive files depend on symbol-level additions in modified files (settings.developer, tabs.openPath, model.pin, QuestionInfo.kind — the typed question cards spanning schema/core/sdk), and the debug-bar deletion forces layout.tsx into the overlay (base layout imports the deleted module). The fork delta is a cross-cutting FEATURE delta, not an app-layer delta. Corrected scope: the complete fork-vs-base delta of the app's build graph (packages/{app,ui,session-ui,schema,core,sdk}) — 422 files + 1 deletion, per-package: app 94A/105M/1D, ui 142A/22M, session-ui 8A/16M, schema 2A/5M, core 3A/23M, sdk 0A/2M. The correction yields the M2/M3 hinge artifact: manifest.json now carries the machine-derived M3 CUTOVER PORT INVENTORY — the 35 server-coupled files (schema/core/sdk) whose types/runtime the canonical server will NOT have. Every app feature touching them is a cutover decision (port upstream / extension service / drop). The true overlays ride wholesale (correct for one-push cutover); compose-vs-fork decomposition is recorded as post-cutover maintenance (manifest.true_overlays). Tooling: symlink-aware extraction round-trip + materialization (the amico.svg → ui-asset symlink), archive batching for the larger file set. Proofs (all green): bun install (4,695 pkgs) → schema + session-ui typechecks → ui tsc build → app tsgo -b typecheck → app VITE PRODUCTION BUILD (14.3s, 1,587 assets, Amicode surfaces verified in the emitted bundle) → 103/103 session-ui unit tests on the materialized tree. --- packages/app-bundle/README.md | 37 + packages/app-bundle/manifest.json | 749 +- .../overlay/packages/app/CONTEXT.md | 130 + .../app/docs/adr/0001-chat-first-landing.md | 81 + .../docs/adr/0002-brain-as-chat-background.md | 126 + .../docs/adr/0003-context-tree-top-panel.md | 80 + .../adr/0004-context-tree-to-side-panel.md | 54 + .../app/docs/design/agentic-surfaces.md | 77 + .../e2e/regression/chrome-surfaces.spec.ts | 106 + .../home-open-chat-empty-projects.spec.ts | 33 + .../e2e/regression/pane-boot-watch.spec.ts | 40 + .../project-picker-recent-search.spec.ts | 69 + .../regression/review-line-comment.spec.ts | 171 + .../session-timeline-tool-projection.spec.ts | 103 + .../regression/tab-navigate-mousedown.spec.ts | 164 + .../e2e/regression/workbench-drops.spec.ts | 197 + .../e2e/regression/workbench-panes.spec.ts | 154 + .../app/e2e/smoke/session-timeline.spec.ts | 739 + .../overlay/packages/app/e2e/utils/waits.ts | 11 + .../overlay/packages/app/index.html | 28 + .../overlay/packages/app/playwright.config.ts | 45 + .../overlay/packages/app/public/amico.svg | 1 + .../app/public/assets/Aero-Regular.ttf | Bin 0 -> 44464 bytes .../app/public/assets/JuliaMono-Regular.woff2 | Bin 0 -> 946516 bytes .../public/assets/RacingSansOne-Regular.woff2 | Bin 0 -> 21804 bytes .../packages/app/public/oc-theme-preload.js | 51 + .../amicode/inspector/device-inspector.tsx | 123 + .../src/amicode/inspector/inspector-bridge.ts | 122 + .../amicode/inspector/inspector-context.tsx | 17 + .../src/amicode/inspector/run-inspector.tsx | 124 + .../overlay/packages/app/src/app.tsx | 772 + .../components/amicode-default-model-value.ts | 17 + .../components/amicode-default-model.test.ts | 28 + .../src/components/amicode-default-model.tsx | 106 + .../components/amicode-defaults-capsule.tsx | 475 + .../components/amicode-file-ref-bridge.tsx | 33 + .../app/src/components/connection-banner.tsx | 77 + .../app/src/components/dialog-new-project.tsx | 102 + .../app/src/components/pane-bridge.tsx | 63 + .../prompt-input-clipboard-structure.test.ts | 75 + .../app/src/components/prompt-input-v2.tsx | 606 + .../app/src/components/prompt-input.tsx | 1789 ++ .../components/prompt-input/attachments.ts | 226 + .../prompt-input/clipboard-bridge.test.ts | 124 + .../prompt-input/clipboard-bridge.ts | 128 + .../components/prompt-input/placeholder.ts | 21 + .../components/prompt-project-selector.tsx | 595 + .../app/src/components/report-bug-button.css | 49 + .../components/report-bug-button.stories.tsx | 151 + .../app/src/components/report-bug-button.tsx | 29 + .../src/components/session-context-usage.tsx | 161 + .../session/context-warning-banner.tsx | 51 + .../app/src/components/session/index.ts | 8 + .../app/src/components/session/panel-menu.tsx | 55 + .../session/session-chats-dropdown.test.ts | 235 + .../session/session-context-tab.tsx | 611 + .../src/components/session/session-header.tsx | 1205 ++ .../components/session/session-new-view.tsx | 127 + .../session/session-preview-tab.tsx | 428 + .../components/session/use-context-warning.ts | 49 + .../app/src/components/settings-dialog.tsx | 48 + .../settings-v2/data-storage-controller.ts | 100 + .../components/settings-v2/data-storage.tsx | 98 + .../settings-v2/developer-tools-controller.ts | 213 + .../settings-v2/developer-tools.tsx | 204 + .../settings-v2/dialog-settings-v2.tsx | 149 + .../src/components/settings-v2/general.tsx | 580 + .../components/settings-v2/permissions.tsx | 350 + .../components/settings-v2/settings-v2.css | 941 ++ .../app/src/components/split-frame.tsx | 412 + .../src/components/status-popover-body.tsx | 878 + .../components/status-popover-model.test.ts | 60 + .../src/components/status-popover-model.ts | 51 + .../app/src/components/status-popover.tsx | 294 + .../packages/app/src/components/terminal.tsx | 761 + .../src/components/titlebar-channel.test.ts | 19 + .../app/src/components/titlebar-channel.ts | 12 + .../app/src/components/titlebar-tab-nav.css | 159 + .../app/src/components/titlebar-tab-strip.tsx | 417 + .../packages/app/src/components/titlebar.tsx | 703 + .../components/vault-browser-model.test.ts | 62 + .../app/src/components/vault-browser-model.ts | 38 + .../app/src/components/vault-browser.tsx | 366 + .../app/src/components/vault-panel.tsx | 24 + .../app/src/components/workbench-panel.tsx | 83 + .../packages/app/src/context/command.tsx | 485 + .../app/src/context/directory-sync.ts | 157 + .../src/context/global-sync/bootstrap.test.ts | 331 + .../src/context/global-sync/child-store.ts | 397 + .../context/global-sync/event-reducer.test.ts | 617 + .../src/context/global-sync/event-reducer.ts | 488 + .../context/global-sync/session-cache.test.ts | 108 + .../src/context/global-sync/session-cache.ts | 64 + .../app/src/context/global-sync/types.ts | 138 + .../packages/app/src/context/language.tsx | 247 + .../packages/app/src/context/layout-tabs.ts | 104 + .../packages/app/src/context/layout.tsx | 1099 ++ .../app/src/context/local-agent.test.ts | 44 + .../packages/app/src/context/local-agent.ts | 15 + .../packages/app/src/context/local.tsx | 426 + .../packages/app/src/context/models.tsx | 186 + .../packages/app/src/context/platform.tsx | 154 + .../app/src/context/server-sdk.test.ts | 207 + .../packages/app/src/context/server-sdk.tsx | 466 + .../app/src/context/server-session.ts | 1429 ++ .../packages/app/src/context/server.tsx | 366 + .../packages/app/src/context/settings.tsx | 580 + .../packages/app/src/context/split.tsx | 67 + .../overlay/packages/app/src/context/tabs.tsx | 518 + .../packages/app/src/context/vault-panel.ts | 29 + .../packages/app/src/context/workbench.tsx | 55 + .../app/src/context/zoom-keybind.test.ts | 115 + .../packages/app/src/design-polish.css | 129 + .../overlay/packages/app/src/desktop-menu.ts | 227 + .../overlay/packages/app/src/entry.tsx | 247 + .../overlay/packages/app/src/i18n/ar.ts | 1113 ++ .../overlay/packages/app/src/i18n/br.ts | 1129 ++ .../overlay/packages/app/src/i18n/bs.ts | 1205 ++ .../overlay/packages/app/src/i18n/da.ts | 1197 ++ .../overlay/packages/app/src/i18n/de.ts | 1142 ++ .../overlay/packages/app/src/i18n/en.ts | 1226 ++ .../overlay/packages/app/src/i18n/es.ts | 1213 ++ .../overlay/packages/app/src/i18n/fr.ts | 1142 ++ .../overlay/packages/app/src/i18n/ja.ts | 1121 ++ .../overlay/packages/app/src/i18n/ko.ts | 1120 ++ .../overlay/packages/app/src/i18n/no.ts | 1216 ++ .../overlay/packages/app/src/i18n/pl.ts | 1129 ++ .../overlay/packages/app/src/i18n/ru.ts | 1208 ++ .../overlay/packages/app/src/i18n/th.ts | 1192 ++ .../overlay/packages/app/src/i18n/tr.ts | 1211 ++ .../overlay/packages/app/src/i18n/uk.ts | 1210 ++ .../overlay/packages/app/src/i18n/zh.ts | 1183 ++ .../overlay/packages/app/src/i18n/zht.ts | 1179 ++ .../overlay/packages/app/src/index.css | 382 + .../overlay/packages/app/src/pages/error.tsx | 381 + .../app/src/pages/home-projects.test.ts | 246 + .../packages/app/src/pages/home-projects.ts | 170 + .../src/pages/home-session-archive.test.ts | 127 + .../app/src/pages/home-session-archive.ts | 41 + .../overlay/packages/app/src/pages/home.tsx | 2244 +++ .../app/src/pages/home/home-sessions-view.tsx | 550 + .../packages/app/src/pages/layout-new.tsx | 56 + .../overlay/packages/app/src/pages/layout.tsx | 2424 +++ .../src/pages/layout/session-tab-avatar.tsx | 61 + .../app/src/pages/layout/sidebar-items.tsx | 335 + .../src/pages/layout/sidebar-workspace.tsx | 488 + .../packages/app/src/pages/new-session.tsx | 68 + .../new-session-draft-controller.ts | 91 + .../pages/new-session/new-session-view.tsx | 183 + .../packages/app/src/pages/session.tsx | 2431 +++ .../composer/bug-dock-controller.test.ts | 386 + .../session/composer/bug-dock-controller.ts | 244 + .../src/pages/session/composer/bug-dock.ts | 35 + .../pages/session/composer/report-bug.test.ts | 73 + .../src/pages/session/composer/report-bug.ts | 21 + .../composer/session-bug-dock.stories.tsx | 145 + .../session/composer/session-bug-dock.tsx | 492 + .../composer/session-composer-controls.ts | 140 + .../session-composer-region-controller.ts | 152 + .../composer/session-composer-region.tsx | 199 + .../composer/session-composer-state.ts | 208 + .../session-question-dock.helpers.test.ts | 45 + .../composer/session-question-dock.helpers.ts | 25 + .../composer/session-question-dock.tsx | 735 + .../session/composer/session-revert-dock.tsx | 227 + .../app/src/pages/session/helpers.test.ts | 277 + .../packages/app/src/pages/session/helpers.ts | 224 + .../pages/session/session-panel-width.test.ts | 81 + .../src/pages/session/session-panel-width.ts | 44 + .../session-side-panel-structure.test.ts | 24 + .../src/pages/session/session-side-panel.tsx | 1025 ++ .../timeline/last-prompt-bubble.test.ts | 314 + .../session/timeline/message-timeline.tsx | 2227 +++ .../session/use-amicode-commands.test.ts | 44 + .../pages/session/use-amicode-commands.tsx | 76 + .../pages/session/use-session-commands.tsx | 614 + .../pages/session/v2/accumulate-diffs.test.ts | 94 + .../src/pages/session/v2/accumulate-diffs.ts | 64 + .../session/v2/review-panel-v2-state.test.ts | 20 + .../pages/session/v2/review-panel-v2-state.ts | 53 + .../packages/app/src/utils/amicode-bridge.ts | 15 + .../app/src/utils/amicode-bug-report.test.ts | 44 + .../app/src/utils/amicode-bug-report.ts | 42 + .../packages/app/src/utils/amicode-fetch.ts | 44 + .../src/utils/amicode-hidden-project.test.ts | 14 + .../app/src/utils/amicode-hidden-project.ts | 18 + .../packages/app/src/utils/amicode-pane.ts | 12 + .../app/src/utils/amicode-route-info.ts | 11 + .../packages/app/src/utils/chrome-dropdown.ts | 21 + .../app/src/utils/global-clipboard.test.ts | 698 + .../app/src/utils/global-clipboard.ts | 373 + .../packages/app/src/utils/pane-bridge.ts | 37 + .../app/src/utils/serialize-session.test.ts | 159 + .../app/src/utils/serialize-session.ts | 37 + .../packages/app/src/utils/server-compat.ts | 532 + .../packages/app/src/utils/start-prompt.ts | 32 + .../packages/app/src/utils/web-zoom.test.ts | 30 + .../packages/app/src/utils/web-zoom.ts | 104 + .../app/src/utils/webview-context-menu.ts | 170 + .../app/src/wsl/settings-model.test.ts | 231 + .../packages/app/src/wsl/settings-model.ts | 341 + .../overlay/packages/core/schema.json | 2290 +++ .../overlay/packages/core/src/config.ts | 231 + .../packages/core/src/database/database.ts | 234 + .../core/src/database/migration.gen.ts | 45 + .../20260813162312_shocking_karnak.ts | 25 + .../packages/core/src/database/schema.gen.ts | 289 + .../overlay/packages/core/src/flag/flag.ts | 79 + .../overlay/packages/core/src/models-dev.ts | 269 + .../packages/core/src/observability/otlp.ts | 194 + .../overlay/packages/core/src/permission.ts | 429 + .../core/src/permission/provider-saved.ts | 94 + .../packages/core/src/permission/sql.ts | 43 + .../packages/core/src/provider-permission.ts | 251 + .../overlay/packages/core/src/session.ts | 528 + .../packages/core/src/session/runner/llm.ts | 448 + .../overlay/packages/core/src/tool/bash.ts | 210 + .../overlay/packages/core/src/tool/edit.ts | 225 + .../overlay/packages/core/src/tool/glob.ts | 108 + .../overlay/packages/core/src/tool/grep.ts | 139 + .../packages/core/src/tool/question.ts | 95 + .../overlay/packages/core/src/tool/read.ts | 120 + .../packages/core/src/tool/webfetch.ts | 220 + .../packages/core/src/tool/websearch.ts | 262 + .../overlay/packages/core/src/tool/write.ts | 103 + .../overlay/packages/core/src/v1/session.ts | 70 + .../packages/core/test/tool-question.test.ts | 168 + .../overlay/packages/schema/src/index.ts | 29 + .../overlay/packages/schema/src/prompt.ts | 67 + .../schema/src/provider-permission.ts | 185 + .../overlay/packages/schema/src/question.ts | 92 + .../packages/schema/src/v1/question.ts | 71 + .../overlay/packages/schema/src/v1/session.ts | 710 + .../packages/schema/test/question.test.ts | 54 + .../packages/sdk/js/src/v2/gen/sdk.gen.ts | 7295 ++++++++ .../packages/sdk/js/src/v2/gen/types.gen.ts | 13737 ++++++++++++++++ .../overlay/packages/session-ui/package.json | 72 + .../src/components/group-parts.test.ts | 74 + .../src/components/markdown-file-refs.test.ts | 68 + .../src/components/markdown-file-refs.ts | 69 + .../src/components/markdown-stream.test.ts | 214 + .../src/components/markdown-stream.ts | 132 + .../session-ui/src/components/markdown.css | 390 + .../session-ui/src/components/markdown.tsx | 784 + .../src/components/message-part-css.test.ts | 29 + .../src/components/message-part-groups.ts | 170 + .../src/components/message-part-skill.test.ts | 55 + .../src/components/message-part-skill.ts | 21 + .../src/components/message-part.css | 1688 ++ .../src/components/message-part.tsx | 3103 ++++ .../src/components/session-retry.tsx | 74 + .../src/components/session-turn.css | 252 + .../src/components/tool-error-card.tsx | 163 + .../src/components/tool-status-title.tsx | 131 + .../packages/session-ui/src/util/clipboard.ts | 34 + .../v2/components/prompt-input/attachments.ts | 287 + .../src/v2/components/prompt-input/index.tsx | 724 + .../session-review-file-preview-v2.tsx | 304 + .../src/v2/components/session-review-v2.css | 534 + .../src/v2/components/session-review-v2.tsx | 323 + .../app-bundle/scripts/extract_overlay.mjs | 172 +- packages/app-bundle/scripts/materialize.mjs | 25 +- 262 files changed, 110476 insertions(+), 57 deletions(-) create mode 100644 packages/app-bundle/overlay/packages/app/CONTEXT.md create mode 100644 packages/app-bundle/overlay/packages/app/docs/adr/0001-chat-first-landing.md create mode 100644 packages/app-bundle/overlay/packages/app/docs/adr/0002-brain-as-chat-background.md create mode 100644 packages/app-bundle/overlay/packages/app/docs/adr/0003-context-tree-top-panel.md create mode 100644 packages/app-bundle/overlay/packages/app/docs/adr/0004-context-tree-to-side-panel.md create mode 100644 packages/app-bundle/overlay/packages/app/docs/design/agentic-surfaces.md create mode 100644 packages/app-bundle/overlay/packages/app/e2e/regression/chrome-surfaces.spec.ts create mode 100644 packages/app-bundle/overlay/packages/app/e2e/regression/home-open-chat-empty-projects.spec.ts create mode 100644 packages/app-bundle/overlay/packages/app/e2e/regression/pane-boot-watch.spec.ts create mode 100644 packages/app-bundle/overlay/packages/app/e2e/regression/project-picker-recent-search.spec.ts create mode 100644 packages/app-bundle/overlay/packages/app/e2e/regression/review-line-comment.spec.ts create mode 100644 packages/app-bundle/overlay/packages/app/e2e/regression/session-timeline-tool-projection.spec.ts create mode 100644 packages/app-bundle/overlay/packages/app/e2e/regression/tab-navigate-mousedown.spec.ts create mode 100644 packages/app-bundle/overlay/packages/app/e2e/regression/workbench-drops.spec.ts create mode 100644 packages/app-bundle/overlay/packages/app/e2e/regression/workbench-panes.spec.ts create mode 100644 packages/app-bundle/overlay/packages/app/e2e/smoke/session-timeline.spec.ts create mode 100644 packages/app-bundle/overlay/packages/app/e2e/utils/waits.ts create mode 100644 packages/app-bundle/overlay/packages/app/index.html create mode 100644 packages/app-bundle/overlay/packages/app/playwright.config.ts create mode 120000 packages/app-bundle/overlay/packages/app/public/amico.svg create mode 100644 packages/app-bundle/overlay/packages/app/public/assets/Aero-Regular.ttf create mode 100644 packages/app-bundle/overlay/packages/app/public/assets/JuliaMono-Regular.woff2 create mode 100644 packages/app-bundle/overlay/packages/app/public/assets/RacingSansOne-Regular.woff2 create mode 100644 packages/app-bundle/overlay/packages/app/public/oc-theme-preload.js create mode 100644 packages/app-bundle/overlay/packages/app/src/amicode/inspector/device-inspector.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/amicode/inspector/inspector-bridge.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/amicode/inspector/inspector-context.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/amicode/inspector/run-inspector.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/app.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/components/amicode-default-model-value.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/components/amicode-default-model.test.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/components/amicode-default-model.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/components/amicode-defaults-capsule.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/components/amicode-file-ref-bridge.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/components/connection-banner.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/components/dialog-new-project.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/components/pane-bridge.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/components/prompt-input-clipboard-structure.test.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/components/prompt-input-v2.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/components/prompt-input.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/components/prompt-input/attachments.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/components/prompt-input/clipboard-bridge.test.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/components/prompt-input/clipboard-bridge.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/components/prompt-input/placeholder.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/components/prompt-project-selector.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/components/report-bug-button.css create mode 100644 packages/app-bundle/overlay/packages/app/src/components/report-bug-button.stories.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/components/report-bug-button.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/components/session-context-usage.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/components/session/context-warning-banner.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/components/session/index.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/components/session/panel-menu.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/components/session/session-chats-dropdown.test.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/components/session/session-context-tab.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/components/session/session-header.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/components/session/session-new-view.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/components/session/session-preview-tab.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/components/session/use-context-warning.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/components/settings-dialog.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/components/settings-v2/data-storage-controller.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/components/settings-v2/data-storage.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/components/settings-v2/developer-tools-controller.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/components/settings-v2/developer-tools.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/components/settings-v2/dialog-settings-v2.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/components/settings-v2/general.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/components/settings-v2/permissions.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/components/settings-v2/settings-v2.css create mode 100644 packages/app-bundle/overlay/packages/app/src/components/split-frame.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/components/status-popover-body.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/components/status-popover-model.test.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/components/status-popover-model.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/components/status-popover.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/components/terminal.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/components/titlebar-channel.test.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/components/titlebar-channel.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/components/titlebar-tab-nav.css create mode 100644 packages/app-bundle/overlay/packages/app/src/components/titlebar-tab-strip.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/components/titlebar.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/components/vault-browser-model.test.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/components/vault-browser-model.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/components/vault-browser.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/components/vault-panel.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/components/workbench-panel.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/context/command.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/context/directory-sync.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/context/global-sync/bootstrap.test.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/context/global-sync/child-store.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/context/global-sync/event-reducer.test.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/context/global-sync/event-reducer.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/context/global-sync/session-cache.test.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/context/global-sync/session-cache.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/context/global-sync/types.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/context/language.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/context/layout-tabs.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/context/layout.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/context/local-agent.test.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/context/local-agent.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/context/local.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/context/models.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/context/platform.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/context/server-sdk.test.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/context/server-sdk.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/context/server-session.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/context/server.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/context/settings.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/context/split.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/context/tabs.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/context/vault-panel.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/context/workbench.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/context/zoom-keybind.test.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/design-polish.css create mode 100644 packages/app-bundle/overlay/packages/app/src/desktop-menu.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/entry.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/i18n/ar.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/i18n/br.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/i18n/bs.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/i18n/da.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/i18n/de.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/i18n/en.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/i18n/es.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/i18n/fr.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/i18n/ja.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/i18n/ko.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/i18n/no.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/i18n/pl.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/i18n/ru.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/i18n/th.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/i18n/tr.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/i18n/uk.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/i18n/zh.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/i18n/zht.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/index.css create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/error.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/home-projects.test.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/home-projects.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/home-session-archive.test.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/home-session-archive.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/home.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/home/home-sessions-view.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/layout-new.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/layout.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/layout/session-tab-avatar.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/layout/sidebar-items.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/layout/sidebar-workspace.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/new-session.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/new-session/new-session-draft-controller.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/new-session/new-session-view.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/session.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/session/composer/bug-dock-controller.test.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/session/composer/bug-dock-controller.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/session/composer/bug-dock.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/session/composer/report-bug.test.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/session/composer/report-bug.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/session/composer/session-bug-dock.stories.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/session/composer/session-bug-dock.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/session/composer/session-composer-controls.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/session/composer/session-composer-region-controller.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/session/composer/session-composer-region.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/session/composer/session-composer-state.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/session/composer/session-question-dock.helpers.test.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/session/composer/session-question-dock.helpers.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/session/composer/session-question-dock.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/session/composer/session-revert-dock.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/session/helpers.test.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/session/helpers.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/session/session-panel-width.test.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/session/session-panel-width.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/session/session-side-panel-structure.test.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/session/session-side-panel.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/session/timeline/last-prompt-bubble.test.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/session/timeline/message-timeline.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/session/use-amicode-commands.test.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/session/use-amicode-commands.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/session/use-session-commands.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/session/v2/accumulate-diffs.test.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/session/v2/accumulate-diffs.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/session/v2/review-panel-v2-state.test.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/session/v2/review-panel-v2-state.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/utils/amicode-bridge.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/utils/amicode-bug-report.test.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/utils/amicode-bug-report.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/utils/amicode-fetch.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/utils/amicode-hidden-project.test.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/utils/amicode-hidden-project.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/utils/amicode-pane.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/utils/amicode-route-info.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/utils/chrome-dropdown.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/utils/global-clipboard.test.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/utils/global-clipboard.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/utils/pane-bridge.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/utils/serialize-session.test.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/utils/serialize-session.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/utils/server-compat.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/utils/start-prompt.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/utils/web-zoom.test.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/utils/web-zoom.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/utils/webview-context-menu.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/wsl/settings-model.test.ts create mode 100644 packages/app-bundle/overlay/packages/app/src/wsl/settings-model.ts create mode 100644 packages/app-bundle/overlay/packages/core/schema.json create mode 100644 packages/app-bundle/overlay/packages/core/src/config.ts create mode 100644 packages/app-bundle/overlay/packages/core/src/database/database.ts create mode 100644 packages/app-bundle/overlay/packages/core/src/database/migration.gen.ts create mode 100644 packages/app-bundle/overlay/packages/core/src/database/migration/20260813162312_shocking_karnak.ts create mode 100644 packages/app-bundle/overlay/packages/core/src/database/schema.gen.ts create mode 100644 packages/app-bundle/overlay/packages/core/src/flag/flag.ts create mode 100644 packages/app-bundle/overlay/packages/core/src/models-dev.ts create mode 100644 packages/app-bundle/overlay/packages/core/src/observability/otlp.ts create mode 100644 packages/app-bundle/overlay/packages/core/src/permission.ts create mode 100644 packages/app-bundle/overlay/packages/core/src/permission/provider-saved.ts create mode 100644 packages/app-bundle/overlay/packages/core/src/permission/sql.ts create mode 100644 packages/app-bundle/overlay/packages/core/src/provider-permission.ts create mode 100644 packages/app-bundle/overlay/packages/core/src/session.ts create mode 100644 packages/app-bundle/overlay/packages/core/src/session/runner/llm.ts create mode 100644 packages/app-bundle/overlay/packages/core/src/tool/bash.ts create mode 100644 packages/app-bundle/overlay/packages/core/src/tool/edit.ts create mode 100644 packages/app-bundle/overlay/packages/core/src/tool/glob.ts create mode 100644 packages/app-bundle/overlay/packages/core/src/tool/grep.ts create mode 100644 packages/app-bundle/overlay/packages/core/src/tool/question.ts create mode 100644 packages/app-bundle/overlay/packages/core/src/tool/read.ts create mode 100644 packages/app-bundle/overlay/packages/core/src/tool/webfetch.ts create mode 100644 packages/app-bundle/overlay/packages/core/src/tool/websearch.ts create mode 100644 packages/app-bundle/overlay/packages/core/src/tool/write.ts create mode 100644 packages/app-bundle/overlay/packages/core/src/v1/session.ts create mode 100644 packages/app-bundle/overlay/packages/core/test/tool-question.test.ts create mode 100644 packages/app-bundle/overlay/packages/schema/src/index.ts create mode 100644 packages/app-bundle/overlay/packages/schema/src/prompt.ts create mode 100644 packages/app-bundle/overlay/packages/schema/src/provider-permission.ts create mode 100644 packages/app-bundle/overlay/packages/schema/src/question.ts create mode 100644 packages/app-bundle/overlay/packages/schema/src/v1/question.ts create mode 100644 packages/app-bundle/overlay/packages/schema/src/v1/session.ts create mode 100644 packages/app-bundle/overlay/packages/schema/test/question.test.ts create mode 100644 packages/app-bundle/overlay/packages/sdk/js/src/v2/gen/sdk.gen.ts create mode 100644 packages/app-bundle/overlay/packages/sdk/js/src/v2/gen/types.gen.ts create mode 100644 packages/app-bundle/overlay/packages/session-ui/package.json create mode 100644 packages/app-bundle/overlay/packages/session-ui/src/components/group-parts.test.ts create mode 100644 packages/app-bundle/overlay/packages/session-ui/src/components/markdown-file-refs.test.ts create mode 100644 packages/app-bundle/overlay/packages/session-ui/src/components/markdown-file-refs.ts create mode 100644 packages/app-bundle/overlay/packages/session-ui/src/components/markdown-stream.test.ts create mode 100644 packages/app-bundle/overlay/packages/session-ui/src/components/markdown-stream.ts create mode 100644 packages/app-bundle/overlay/packages/session-ui/src/components/markdown.css create mode 100644 packages/app-bundle/overlay/packages/session-ui/src/components/markdown.tsx create mode 100644 packages/app-bundle/overlay/packages/session-ui/src/components/message-part-css.test.ts create mode 100644 packages/app-bundle/overlay/packages/session-ui/src/components/message-part-groups.ts create mode 100644 packages/app-bundle/overlay/packages/session-ui/src/components/message-part-skill.test.ts create mode 100644 packages/app-bundle/overlay/packages/session-ui/src/components/message-part-skill.ts create mode 100644 packages/app-bundle/overlay/packages/session-ui/src/components/message-part.css create mode 100644 packages/app-bundle/overlay/packages/session-ui/src/components/message-part.tsx create mode 100644 packages/app-bundle/overlay/packages/session-ui/src/components/session-retry.tsx create mode 100644 packages/app-bundle/overlay/packages/session-ui/src/components/session-turn.css create mode 100644 packages/app-bundle/overlay/packages/session-ui/src/components/tool-error-card.tsx create mode 100644 packages/app-bundle/overlay/packages/session-ui/src/components/tool-status-title.tsx create mode 100644 packages/app-bundle/overlay/packages/session-ui/src/util/clipboard.ts create mode 100644 packages/app-bundle/overlay/packages/session-ui/src/v2/components/prompt-input/attachments.ts create mode 100644 packages/app-bundle/overlay/packages/session-ui/src/v2/components/prompt-input/index.tsx create mode 100644 packages/app-bundle/overlay/packages/session-ui/src/v2/components/session-review-file-preview-v2.tsx create mode 100644 packages/app-bundle/overlay/packages/session-ui/src/v2/components/session-review-v2.css create mode 100644 packages/app-bundle/overlay/packages/session-ui/src/v2/components/session-review-v2.tsx diff --git a/packages/app-bundle/README.md b/packages/app-bundle/README.md index 773b375a..118c9d86 100644 --- a/packages/app-bundle/README.md +++ b/packages/app-bundle/README.md @@ -4,6 +4,43 @@ The M2 artifact: the Amicode app surface, carried as an **overlay** on a pinned **canonical opencode** base — the mechanism that retires the fork at cutover while keeping every Amicode surface. +## Slice (b) — the complete app-graph delta — SHIPPED + +**Scope (corrected mid-slice)**: the complete fork-vs-base delta of the +app's build graph — `packages/{app,ui,session-ui,schema,core,sdk}` — 422 +files (per-package: app 94A/105M/1D, ui 142A/22M, session-ui 8A/16M, +schema 2A/5M, core 3A/23M, sdk 0A/2M). + +The planned additive-only scope **does not typecheck** — the finding that +drove the correction: ~10 additive app files depend on symbol-level +additions in modified files (`settings.developer`, `tabs.openPath`, +`model.pin`, `QuestionInfo.kind` — the typed question cards spanning +schema/core/sdk), and the fork's debug-bar deletion forces `layout.tsx` +into the overlay (base layout imports the deleted module). The fork delta +is a cross-cutting FEATURE delta, not an app-layer delta. + +**The M3 cutover port inventory is machine-derived** (manifest: +`server_coupled_port_inventory`, 35 files in schema/core/sdk): every app +feature whose types or runtime live in the fork's server-side packages. +At cutover the canonical server ships NONE of these — the bundle will hit +exactly these gaps — so each is a port-upstream / extension-service / +drop decision. Recorded, not resolved. + +**The true overlays** (big M-files upstream still evolves: home.tsx, +session-header, message-part, timeline, …) ride wholesale — correct for +the one-push cutover. The compose-vs-fork decomposition is deliberate +POST-cutover maintenance (`manifest.true_overlays` is the worklist). + +### Proofs (all green, 2026-08-21) + +1. **Equivalence** — every overlay file byte-identical to the fork's at the + pin (round-trip verified at extraction; manifest hash-verified at + materialization). Symlink-aware (`app/public/amico.svg` → ui asset). +2. **Composition** — `bun install` (4,695 pkgs) → `schema` typecheck → + `session-ui` typecheck + `ui` tsc build → `app` `tsgo -b` typecheck → + `app` **vite production build** (14.3s, 1,587 assets, Amicode surfaces + present in the emitted bundle) → **103/103 session-ui unit tests**. + ## Slice (a) — the complete `packages/ui` delta — SHIPPED **Scope**: every file under `packages/ui` the fork changed vs the upstream diff --git a/packages/app-bundle/manifest.json b/packages/app-bundle/manifest.json index 1743e3e8..0e34ce76 100644 --- a/packages/app-bundle/manifest.json +++ b/packages/app-bundle/manifest.json @@ -1,20 +1,493 @@ { - "schema": 2, - "slice": "a", - "scope": "packages/ui (complete delta)", + "schema": 4, + "slices": "b (complete app-graph delta)", + "scope": "complete fork-vs-base delta of packages/{app,ui,session-ui,schema,core,sdk}", "fork_tag": "v1.18.10-amicode.14", "fork_sha": "1fe58157f78ce62515baaee69a9c1461eed42282", "upstream_base": "v1.18.12", "upstream_base_sha": "0dd6950d1b06958fbcdcadf0ad56258257ab7fdb", - "extracted_at": "2026-08-21T07:33:33.803Z", + "extracted_at": "2026-08-21T08:01:10.830Z", + "per_package": { + "packages/app": { + "A": 94, + "M": 105, + "D": 1 + }, + "packages/core": { + "A": 3, + "M": 23, + "D": 0 + }, + "packages/schema": { + "A": 2, + "M": 5, + "D": 0 + }, + "packages/sdk": { + "A": 0, + "M": 2, + "D": 0 + }, + "packages/session-ui": { + "A": 8, + "M": 16, + "D": 0 + }, + "packages/ui": { + "A": 142, + "M": 22, + "D": 0 + } + }, "counts": { - "added": 142, - "modified": 22, - "deleted": 0, - "unchanged": 1623 + "overlay_total": 422, + "deletions": 1, + "server_coupled": 35 }, - "deletions": [], + "server_coupled_port_inventory": [ + "packages/core/schema.json", + "packages/core/src/config.ts", + "packages/core/src/database/database.ts", + "packages/core/src/database/migration.gen.ts", + "packages/core/src/database/migration/20260813162312_shocking_karnak.ts", + "packages/core/src/database/schema.gen.ts", + "packages/core/src/flag/flag.ts", + "packages/core/src/models-dev.ts", + "packages/core/src/observability/otlp.ts", + "packages/core/src/permission.ts", + "packages/core/src/permission/provider-saved.ts", + "packages/core/src/permission/sql.ts", + "packages/core/src/provider-permission.ts", + "packages/core/src/session.ts", + "packages/core/src/session/runner/llm.ts", + "packages/core/src/tool/bash.ts", + "packages/core/src/tool/edit.ts", + "packages/core/src/tool/glob.ts", + "packages/core/src/tool/grep.ts", + "packages/core/src/tool/question.ts", + "packages/core/src/tool/read.ts", + "packages/core/src/tool/webfetch.ts", + "packages/core/src/tool/websearch.ts", + "packages/core/src/tool/write.ts", + "packages/core/src/v1/session.ts", + "packages/core/test/tool-question.test.ts", + "packages/schema/src/index.ts", + "packages/schema/src/prompt.ts", + "packages/schema/src/provider-permission.ts", + "packages/schema/src/question.ts", + "packages/schema/src/v1/question.ts", + "packages/schema/src/v1/session.ts", + "packages/schema/test/question.test.ts", + "packages/sdk/js/src/v2/gen/sdk.gen.ts", + "packages/sdk/js/src/v2/gen/types.gen.ts" + ], + "true_overlays": [ + "packages/app/e2e/regression/project-picker-recent-search.spec.ts", + "packages/app/e2e/regression/review-line-comment.spec.ts", + "packages/app/e2e/regression/session-timeline-tool-projection.spec.ts", + "packages/app/e2e/regression/tab-navigate-mousedown.spec.ts", + "packages/app/e2e/smoke/session-timeline.spec.ts", + "packages/app/e2e/utils/waits.ts", + "packages/app/index.html", + "packages/app/playwright.config.ts", + "packages/app/public/oc-theme-preload.js", + "packages/app/src/app.tsx", + "packages/app/src/components/prompt-input-v2.tsx", + "packages/app/src/components/prompt-input.tsx", + "packages/app/src/components/prompt-input/attachments.ts", + "packages/app/src/components/prompt-input/placeholder.ts", + "packages/app/src/components/prompt-project-selector.tsx", + "packages/app/src/components/session-context-usage.tsx", + "packages/app/src/components/session/index.ts", + "packages/app/src/components/session/session-context-tab.tsx", + "packages/app/src/components/session/session-header.tsx", + "packages/app/src/components/session/session-new-view.tsx", + "packages/app/src/components/settings-dialog.tsx", + "packages/app/src/components/settings-v2/dialog-settings-v2.tsx", + "packages/app/src/components/settings-v2/general.tsx", + "packages/app/src/components/settings-v2/settings-v2.css", + "packages/app/src/components/status-popover-body.tsx", + "packages/app/src/components/status-popover.tsx", + "packages/app/src/components/terminal.tsx", + "packages/app/src/components/titlebar-tab-nav.css", + "packages/app/src/components/titlebar-tab-strip.tsx", + "packages/app/src/components/titlebar.tsx", + "packages/app/src/context/command.tsx", + "packages/app/src/context/directory-sync.ts", + "packages/app/src/context/global-sync/bootstrap.test.ts", + "packages/app/src/context/global-sync/child-store.ts", + "packages/app/src/context/global-sync/event-reducer.test.ts", + "packages/app/src/context/global-sync/event-reducer.ts", + "packages/app/src/context/global-sync/session-cache.test.ts", + "packages/app/src/context/global-sync/session-cache.ts", + "packages/app/src/context/global-sync/types.ts", + "packages/app/src/context/language.tsx", + "packages/app/src/context/layout-tabs.ts", + "packages/app/src/context/layout.tsx", + "packages/app/src/context/local-agent.test.ts", + "packages/app/src/context/local-agent.ts", + "packages/app/src/context/local.tsx", + "packages/app/src/context/models.tsx", + "packages/app/src/context/platform.tsx", + "packages/app/src/context/server-sdk.test.ts", + "packages/app/src/context/server-sdk.tsx", + "packages/app/src/context/server-session.ts", + "packages/app/src/context/server.tsx", + "packages/app/src/context/settings.tsx", + "packages/app/src/context/tabs.tsx", + "packages/app/src/desktop-menu.ts", + "packages/app/src/entry.tsx", + "packages/app/src/i18n/ar.ts", + "packages/app/src/i18n/br.ts", + "packages/app/src/i18n/bs.ts", + "packages/app/src/i18n/da.ts", + "packages/app/src/i18n/de.ts", + "packages/app/src/i18n/en.ts", + "packages/app/src/i18n/es.ts", + "packages/app/src/i18n/fr.ts", + "packages/app/src/i18n/ja.ts", + "packages/app/src/i18n/ko.ts", + "packages/app/src/i18n/no.ts", + "packages/app/src/i18n/pl.ts", + "packages/app/src/i18n/ru.ts", + "packages/app/src/i18n/th.ts", + "packages/app/src/i18n/tr.ts", + "packages/app/src/i18n/uk.ts", + "packages/app/src/i18n/zh.ts", + "packages/app/src/i18n/zht.ts", + "packages/app/src/index.css", + "packages/app/src/pages/error.tsx", + "packages/app/src/pages/home-session-archive.test.ts", + "packages/app/src/pages/home-session-archive.ts", + "packages/app/src/pages/home.tsx", + "packages/app/src/pages/home/home-sessions-view.tsx", + "packages/app/src/pages/layout-new.tsx", + "packages/app/src/pages/layout.tsx", + "packages/app/src/pages/layout/session-tab-avatar.tsx", + "packages/app/src/pages/layout/sidebar-items.tsx", + "packages/app/src/pages/layout/sidebar-workspace.tsx", + "packages/app/src/pages/new-session.tsx", + "packages/app/src/pages/new-session/new-session-draft-controller.ts", + "packages/app/src/pages/new-session/new-session-view.tsx", + "packages/app/src/pages/session.tsx", + "packages/app/src/pages/session/composer/session-composer-controls.ts", + "packages/app/src/pages/session/composer/session-composer-region-controller.ts", + "packages/app/src/pages/session/composer/session-composer-region.tsx", + "packages/app/src/pages/session/composer/session-composer-state.ts", + "packages/app/src/pages/session/composer/session-question-dock.tsx", + "packages/app/src/pages/session/composer/session-revert-dock.tsx", + "packages/app/src/pages/session/helpers.test.ts", + "packages/app/src/pages/session/helpers.ts", + "packages/app/src/pages/session/session-panel-width.test.ts", + "packages/app/src/pages/session/session-panel-width.ts", + "packages/app/src/pages/session/session-side-panel.tsx", + "packages/app/src/pages/session/timeline/message-timeline.tsx", + "packages/app/src/pages/session/use-session-commands.tsx", + "packages/app/src/pages/session/v2/review-panel-v2-state.ts", + "packages/app/src/utils/server-compat.ts", + "packages/app/src/wsl/settings-model.test.ts", + "packages/app/src/wsl/settings-model.ts", + "packages/session-ui/package.json", + "packages/session-ui/src/components/markdown-stream.test.ts", + "packages/session-ui/src/components/markdown-stream.ts", + "packages/session-ui/src/components/markdown.css", + "packages/session-ui/src/components/markdown.tsx", + "packages/session-ui/src/components/message-part.css", + "packages/session-ui/src/components/message-part.tsx", + "packages/session-ui/src/components/session-retry.tsx", + "packages/session-ui/src/components/session-turn.css", + "packages/session-ui/src/components/tool-error-card.tsx", + "packages/session-ui/src/components/tool-status-title.tsx", + "packages/session-ui/src/v2/components/prompt-input/attachments.ts", + "packages/session-ui/src/v2/components/prompt-input/index.tsx", + "packages/session-ui/src/v2/components/session-review-file-preview-v2.tsx", + "packages/session-ui/src/v2/components/session-review-v2.css", + "packages/session-ui/src/v2/components/session-review-v2.tsx", + "packages/ui/package.json", + "packages/ui/src/assets/favicon/site.webmanifest", + "packages/ui/src/components/dialog.css", + "packages/ui/src/components/dock-surface.css", + "packages/ui/src/components/favicon.tsx", + "packages/ui/src/components/icon.tsx", + "packages/ui/src/components/logo.css", + "packages/ui/src/components/logo.tsx", + "packages/ui/src/components/text-field.tsx", + "packages/ui/src/context/dialog.tsx", + "packages/ui/src/context/marked.tsx", + "packages/ui/src/i18n/en.ts", + "packages/ui/src/styles/index.css", + "packages/ui/src/styles/theme.css", + "packages/ui/src/theme/context.tsx", + "packages/ui/src/theme/themes/oc-2.json", + "packages/ui/src/v2/components/dialog-v2.css", + "packages/ui/src/v2/components/icon-button-v2.tsx", + "packages/ui/src/v2/components/icon.tsx", + "packages/ui/src/v2/components/switch-v2.css", + "packages/ui/src/v2/components/tooltip-v2.css", + "packages/ui/src/v2/components/wordmark-v2.tsx" + ], + "deletions": [ + "packages/app/src/components/debug-bar.tsx" + ], "classification": { + "packages/app/CONTEXT.md": "A", + "packages/app/docs/adr/0001-chat-first-landing.md": "A", + "packages/app/docs/adr/0002-brain-as-chat-background.md": "A", + "packages/app/docs/adr/0003-context-tree-top-panel.md": "A", + "packages/app/docs/adr/0004-context-tree-to-side-panel.md": "A", + "packages/app/docs/design/agentic-surfaces.md": "A", + "packages/app/e2e/regression/chrome-surfaces.spec.ts": "A", + "packages/app/e2e/regression/home-open-chat-empty-projects.spec.ts": "A", + "packages/app/e2e/regression/pane-boot-watch.spec.ts": "A", + "packages/app/e2e/regression/project-picker-recent-search.spec.ts": "M", + "packages/app/e2e/regression/review-line-comment.spec.ts": "M", + "packages/app/e2e/regression/session-timeline-tool-projection.spec.ts": "M", + "packages/app/e2e/regression/tab-navigate-mousedown.spec.ts": "M", + "packages/app/e2e/regression/workbench-drops.spec.ts": "A", + "packages/app/e2e/regression/workbench-panes.spec.ts": "A", + "packages/app/e2e/smoke/session-timeline.spec.ts": "M", + "packages/app/e2e/utils/waits.ts": "M", + "packages/app/index.html": "M", + "packages/app/playwright.config.ts": "M", + "packages/app/public/amico.svg": "A", + "packages/app/public/assets/Aero-Regular.ttf": "A", + "packages/app/public/assets/JuliaMono-Regular.woff2": "A", + "packages/app/public/assets/RacingSansOne-Regular.woff2": "A", + "packages/app/public/oc-theme-preload.js": "M", + "packages/app/src/amicode/inspector/device-inspector.tsx": "A", + "packages/app/src/amicode/inspector/inspector-bridge.ts": "A", + "packages/app/src/amicode/inspector/inspector-context.tsx": "A", + "packages/app/src/amicode/inspector/run-inspector.tsx": "A", + "packages/app/src/app.tsx": "M", + "packages/app/src/components/amicode-default-model-value.ts": "A", + "packages/app/src/components/amicode-default-model.test.ts": "A", + "packages/app/src/components/amicode-default-model.tsx": "A", + "packages/app/src/components/amicode-defaults-capsule.tsx": "A", + "packages/app/src/components/amicode-file-ref-bridge.tsx": "A", + "packages/app/src/components/connection-banner.tsx": "A", + "packages/app/src/components/dialog-new-project.tsx": "A", + "packages/app/src/components/pane-bridge.tsx": "A", + "packages/app/src/components/prompt-input-clipboard-structure.test.ts": "A", + "packages/app/src/components/prompt-input-v2.tsx": "M", + "packages/app/src/components/prompt-input.tsx": "M", + "packages/app/src/components/prompt-input/attachments.ts": "M", + "packages/app/src/components/prompt-input/clipboard-bridge.test.ts": "A", + "packages/app/src/components/prompt-input/clipboard-bridge.ts": "A", + "packages/app/src/components/prompt-input/placeholder.ts": "M", + "packages/app/src/components/prompt-project-selector.tsx": "M", + "packages/app/src/components/report-bug-button.css": "A", + "packages/app/src/components/report-bug-button.stories.tsx": "A", + "packages/app/src/components/report-bug-button.tsx": "A", + "packages/app/src/components/session-context-usage.tsx": "M", + "packages/app/src/components/session/context-warning-banner.tsx": "A", + "packages/app/src/components/session/index.ts": "M", + "packages/app/src/components/session/panel-menu.tsx": "A", + "packages/app/src/components/session/session-chats-dropdown.test.ts": "A", + "packages/app/src/components/session/session-context-tab.tsx": "M", + "packages/app/src/components/session/session-header.tsx": "M", + "packages/app/src/components/session/session-new-view.tsx": "M", + "packages/app/src/components/session/session-preview-tab.tsx": "A", + "packages/app/src/components/session/use-context-warning.ts": "A", + "packages/app/src/components/settings-dialog.tsx": "M", + "packages/app/src/components/settings-v2/data-storage-controller.ts": "A", + "packages/app/src/components/settings-v2/data-storage.tsx": "A", + "packages/app/src/components/settings-v2/developer-tools-controller.ts": "A", + "packages/app/src/components/settings-v2/developer-tools.tsx": "A", + "packages/app/src/components/settings-v2/dialog-settings-v2.tsx": "M", + "packages/app/src/components/settings-v2/general.tsx": "M", + "packages/app/src/components/settings-v2/permissions.tsx": "A", + "packages/app/src/components/settings-v2/settings-v2.css": "M", + "packages/app/src/components/split-frame.tsx": "A", + "packages/app/src/components/status-popover-body.tsx": "M", + "packages/app/src/components/status-popover-model.test.ts": "A", + "packages/app/src/components/status-popover-model.ts": "A", + "packages/app/src/components/status-popover.tsx": "M", + "packages/app/src/components/terminal.tsx": "M", + "packages/app/src/components/titlebar-channel.test.ts": "A", + "packages/app/src/components/titlebar-channel.ts": "A", + "packages/app/src/components/titlebar-tab-nav.css": "M", + "packages/app/src/components/titlebar-tab-strip.tsx": "M", + "packages/app/src/components/titlebar.tsx": "M", + "packages/app/src/components/vault-browser-model.test.ts": "A", + "packages/app/src/components/vault-browser-model.ts": "A", + "packages/app/src/components/vault-browser.tsx": "A", + "packages/app/src/components/vault-panel.tsx": "A", + "packages/app/src/components/workbench-panel.tsx": "A", + "packages/app/src/context/command.tsx": "M", + "packages/app/src/context/directory-sync.ts": "M", + "packages/app/src/context/global-sync/bootstrap.test.ts": "M", + "packages/app/src/context/global-sync/child-store.ts": "M", + "packages/app/src/context/global-sync/event-reducer.test.ts": "M", + "packages/app/src/context/global-sync/event-reducer.ts": "M", + "packages/app/src/context/global-sync/session-cache.test.ts": "M", + "packages/app/src/context/global-sync/session-cache.ts": "M", + "packages/app/src/context/global-sync/types.ts": "M", + "packages/app/src/context/language.tsx": "M", + "packages/app/src/context/layout-tabs.ts": "M", + "packages/app/src/context/layout.tsx": "M", + "packages/app/src/context/local-agent.test.ts": "M", + "packages/app/src/context/local-agent.ts": "M", + "packages/app/src/context/local.tsx": "M", + "packages/app/src/context/models.tsx": "M", + "packages/app/src/context/platform.tsx": "M", + "packages/app/src/context/server-sdk.test.ts": "M", + "packages/app/src/context/server-sdk.tsx": "M", + "packages/app/src/context/server-session.ts": "M", + "packages/app/src/context/server.tsx": "M", + "packages/app/src/context/settings.tsx": "M", + "packages/app/src/context/split.tsx": "A", + "packages/app/src/context/tabs.tsx": "M", + "packages/app/src/context/vault-panel.ts": "A", + "packages/app/src/context/workbench.tsx": "A", + "packages/app/src/context/zoom-keybind.test.ts": "A", + "packages/app/src/design-polish.css": "A", + "packages/app/src/desktop-menu.ts": "M", + "packages/app/src/entry.tsx": "M", + "packages/app/src/i18n/ar.ts": "M", + "packages/app/src/i18n/br.ts": "M", + "packages/app/src/i18n/bs.ts": "M", + "packages/app/src/i18n/da.ts": "M", + "packages/app/src/i18n/de.ts": "M", + "packages/app/src/i18n/en.ts": "M", + "packages/app/src/i18n/es.ts": "M", + "packages/app/src/i18n/fr.ts": "M", + "packages/app/src/i18n/ja.ts": "M", + "packages/app/src/i18n/ko.ts": "M", + "packages/app/src/i18n/no.ts": "M", + "packages/app/src/i18n/pl.ts": "M", + "packages/app/src/i18n/ru.ts": "M", + "packages/app/src/i18n/th.ts": "M", + "packages/app/src/i18n/tr.ts": "M", + "packages/app/src/i18n/uk.ts": "M", + "packages/app/src/i18n/zh.ts": "M", + "packages/app/src/i18n/zht.ts": "M", + "packages/app/src/index.css": "M", + "packages/app/src/pages/error.tsx": "M", + "packages/app/src/pages/home-projects.test.ts": "A", + "packages/app/src/pages/home-projects.ts": "A", + "packages/app/src/pages/home-session-archive.test.ts": "M", + "packages/app/src/pages/home-session-archive.ts": "M", + "packages/app/src/pages/home.tsx": "M", + "packages/app/src/pages/home/home-sessions-view.tsx": "M", + "packages/app/src/pages/layout-new.tsx": "M", + "packages/app/src/pages/layout.tsx": "M", + "packages/app/src/pages/layout/session-tab-avatar.tsx": "M", + "packages/app/src/pages/layout/sidebar-items.tsx": "M", + "packages/app/src/pages/layout/sidebar-workspace.tsx": "M", + "packages/app/src/pages/new-session.tsx": "M", + "packages/app/src/pages/new-session/new-session-draft-controller.ts": "M", + "packages/app/src/pages/new-session/new-session-view.tsx": "M", + "packages/app/src/pages/session.tsx": "M", + "packages/app/src/pages/session/composer/bug-dock-controller.test.ts": "A", + "packages/app/src/pages/session/composer/bug-dock-controller.ts": "A", + "packages/app/src/pages/session/composer/bug-dock.ts": "A", + "packages/app/src/pages/session/composer/report-bug.test.ts": "A", + "packages/app/src/pages/session/composer/report-bug.ts": "A", + "packages/app/src/pages/session/composer/session-bug-dock.stories.tsx": "A", + "packages/app/src/pages/session/composer/session-bug-dock.tsx": "A", + "packages/app/src/pages/session/composer/session-composer-controls.ts": "M", + "packages/app/src/pages/session/composer/session-composer-region-controller.ts": "M", + "packages/app/src/pages/session/composer/session-composer-region.tsx": "M", + "packages/app/src/pages/session/composer/session-composer-state.ts": "M", + "packages/app/src/pages/session/composer/session-question-dock.helpers.test.ts": "A", + "packages/app/src/pages/session/composer/session-question-dock.helpers.ts": "A", + "packages/app/src/pages/session/composer/session-question-dock.tsx": "M", + "packages/app/src/pages/session/composer/session-revert-dock.tsx": "M", + "packages/app/src/pages/session/helpers.test.ts": "M", + "packages/app/src/pages/session/helpers.ts": "M", + "packages/app/src/pages/session/session-panel-width.test.ts": "M", + "packages/app/src/pages/session/session-panel-width.ts": "M", + "packages/app/src/pages/session/session-side-panel-structure.test.ts": "A", + "packages/app/src/pages/session/session-side-panel.tsx": "M", + "packages/app/src/pages/session/timeline/last-prompt-bubble.test.ts": "A", + "packages/app/src/pages/session/timeline/message-timeline.tsx": "M", + "packages/app/src/pages/session/use-amicode-commands.test.ts": "A", + "packages/app/src/pages/session/use-amicode-commands.tsx": "A", + "packages/app/src/pages/session/use-session-commands.tsx": "M", + "packages/app/src/pages/session/v2/accumulate-diffs.test.ts": "A", + "packages/app/src/pages/session/v2/accumulate-diffs.ts": "A", + "packages/app/src/pages/session/v2/review-panel-v2-state.test.ts": "A", + "packages/app/src/pages/session/v2/review-panel-v2-state.ts": "M", + "packages/app/src/utils/amicode-bridge.ts": "A", + "packages/app/src/utils/amicode-bug-report.test.ts": "A", + "packages/app/src/utils/amicode-bug-report.ts": "A", + "packages/app/src/utils/amicode-fetch.ts": "A", + "packages/app/src/utils/amicode-hidden-project.test.ts": "A", + "packages/app/src/utils/amicode-hidden-project.ts": "A", + "packages/app/src/utils/amicode-pane.ts": "A", + "packages/app/src/utils/amicode-route-info.ts": "A", + "packages/app/src/utils/chrome-dropdown.ts": "A", + "packages/app/src/utils/global-clipboard.test.ts": "A", + "packages/app/src/utils/global-clipboard.ts": "A", + "packages/app/src/utils/pane-bridge.ts": "A", + "packages/app/src/utils/serialize-session.test.ts": "A", + "packages/app/src/utils/serialize-session.ts": "A", + "packages/app/src/utils/server-compat.ts": "M", + "packages/app/src/utils/start-prompt.ts": "A", + "packages/app/src/utils/web-zoom.test.ts": "A", + "packages/app/src/utils/web-zoom.ts": "A", + "packages/app/src/utils/webview-context-menu.ts": "A", + "packages/app/src/wsl/settings-model.test.ts": "M", + "packages/app/src/wsl/settings-model.ts": "M", + "packages/core/schema.json": "M", + "packages/core/src/config.ts": "M", + "packages/core/src/database/database.ts": "M", + "packages/core/src/database/migration.gen.ts": "M", + "packages/core/src/database/migration/20260813162312_shocking_karnak.ts": "A", + "packages/core/src/database/schema.gen.ts": "M", + "packages/core/src/flag/flag.ts": "M", + "packages/core/src/models-dev.ts": "M", + "packages/core/src/observability/otlp.ts": "M", + "packages/core/src/permission.ts": "M", + "packages/core/src/permission/provider-saved.ts": "A", + "packages/core/src/permission/sql.ts": "M", + "packages/core/src/provider-permission.ts": "A", + "packages/core/src/session.ts": "M", + "packages/core/src/session/runner/llm.ts": "M", + "packages/core/src/tool/bash.ts": "M", + "packages/core/src/tool/edit.ts": "M", + "packages/core/src/tool/glob.ts": "M", + "packages/core/src/tool/grep.ts": "M", + "packages/core/src/tool/question.ts": "M", + "packages/core/src/tool/read.ts": "M", + "packages/core/src/tool/webfetch.ts": "M", + "packages/core/src/tool/websearch.ts": "M", + "packages/core/src/tool/write.ts": "M", + "packages/core/src/v1/session.ts": "M", + "packages/core/test/tool-question.test.ts": "M", + "packages/schema/src/index.ts": "M", + "packages/schema/src/prompt.ts": "M", + "packages/schema/src/provider-permission.ts": "A", + "packages/schema/src/question.ts": "M", + "packages/schema/src/v1/question.ts": "M", + "packages/schema/src/v1/session.ts": "M", + "packages/schema/test/question.test.ts": "A", + "packages/sdk/js/src/v2/gen/sdk.gen.ts": "M", + "packages/sdk/js/src/v2/gen/types.gen.ts": "M", + "packages/session-ui/package.json": "M", + "packages/session-ui/src/components/group-parts.test.ts": "A", + "packages/session-ui/src/components/markdown-file-refs.test.ts": "A", + "packages/session-ui/src/components/markdown-file-refs.ts": "A", + "packages/session-ui/src/components/markdown-stream.test.ts": "M", + "packages/session-ui/src/components/markdown-stream.ts": "M", + "packages/session-ui/src/components/markdown.css": "M", + "packages/session-ui/src/components/markdown.tsx": "M", + "packages/session-ui/src/components/message-part-css.test.ts": "A", + "packages/session-ui/src/components/message-part-groups.ts": "A", + "packages/session-ui/src/components/message-part-skill.test.ts": "A", + "packages/session-ui/src/components/message-part-skill.ts": "A", + "packages/session-ui/src/components/message-part.css": "M", + "packages/session-ui/src/components/message-part.tsx": "M", + "packages/session-ui/src/components/session-retry.tsx": "M", + "packages/session-ui/src/components/session-turn.css": "M", + "packages/session-ui/src/components/tool-error-card.tsx": "M", + "packages/session-ui/src/components/tool-status-title.tsx": "M", + "packages/session-ui/src/util/clipboard.ts": "A", + "packages/session-ui/src/v2/components/prompt-input/attachments.ts": "M", + "packages/session-ui/src/v2/components/prompt-input/index.tsx": "M", + "packages/session-ui/src/v2/components/session-review-file-preview-v2.tsx": "M", + "packages/session-ui/src/v2/components/session-review-v2.css": "M", + "packages/session-ui/src/v2/components/session-review-v2.tsx": "M", "packages/ui/package.json": "M", "packages/ui/src/amicode/amico-presence.css": "A", "packages/ui/src/amicode/amico-presence.stories.tsx": "A", @@ -181,6 +654,264 @@ "packages/ui/src/v2/components/wordmark-v2.tsx": "M" }, "files": { + "packages/app/CONTEXT.md": "87b15a82a89ea1c82e0d647c169a259a1ef5193893854f2cfed54526762898ca", + "packages/app/docs/adr/0001-chat-first-landing.md": "d19c42a8aa56f585f92a3e0ffd072c55804ca029c6d19fb7d772c69d7e5de3be", + "packages/app/docs/adr/0002-brain-as-chat-background.md": "bda9bc065e16684e1daffdc3f7d8ac5b3c81a82de5c27fa1ba7d6268659c32c1", + "packages/app/docs/adr/0003-context-tree-top-panel.md": "693f19dd5c155b881dd35b85412ad62fef0683a6934d7cf1c59c9a679d1a8eeb", + "packages/app/docs/adr/0004-context-tree-to-side-panel.md": "271f31a677547be2e11c0c19364ae97b04a2ea353a9ab22c87bde1afab64babc", + "packages/app/docs/design/agentic-surfaces.md": "94a117b2790ff0157ee01684fd49dcb32d241b3572e9df77f81f721967dc52bf", + "packages/app/e2e/regression/chrome-surfaces.spec.ts": "c7ff1b82bf84e577fee0e765b0c357c040d4d7cdefdd537702d7dbd186bfedf6", + "packages/app/e2e/regression/home-open-chat-empty-projects.spec.ts": "b0debddea5051defd859b9c8db2439801592d984fa89ddaf880494722ef2cb21", + "packages/app/e2e/regression/pane-boot-watch.spec.ts": "3aa869f8b1d9881797e14dceb9854c4408f768f490d4db8722e29e511bca1833", + "packages/app/e2e/regression/project-picker-recent-search.spec.ts": "64f4249f29cfce29d5e755a544c5aa2f27599d9fd874d363628f696f5f0e02b9", + "packages/app/e2e/regression/review-line-comment.spec.ts": "953e9c67ed4cee91968369088237cf2288558eaa55e55b61f385571bccad7003", + "packages/app/e2e/regression/session-timeline-tool-projection.spec.ts": "c0bb866b113d38ddec084e04027030cf5b7c81f1fe4e6ee88fcd7b1b161296c3", + "packages/app/e2e/regression/tab-navigate-mousedown.spec.ts": "8521d43183f0df624449edbba8ee4dc646f0ecb84a9c5a04d275613f9da875ae", + "packages/app/e2e/regression/workbench-drops.spec.ts": "0188f4f1ff07a40945a4900bfa85e21a4f578be9f18257a803200a18ce41ac05", + "packages/app/e2e/regression/workbench-panes.spec.ts": "e0dfb84bd1b8be00a89cf48387aeaab5ec75845e8efbc8e4fa8252204fb27e8f", + "packages/app/e2e/smoke/session-timeline.spec.ts": "7dc5b08126af46fa11727bd0ff2f3b25a04e430afbf84c8bafb4b73f4609dd2d", + "packages/app/e2e/utils/waits.ts": "53959eeae1e6562a2e6c578b261140c74fe9fc30e1e4c46295d3252b6d274a74", + "packages/app/index.html": "9240501ab6c74b84599a5eb9d852c4e606dc60eb2e69a4ac84b182bf8b1b5b68", + "packages/app/playwright.config.ts": "b7dae0b39248b4ddf7cba829af96ac7fe3cc919add9711ecb5292d9bd52e1805", + "packages/app/public/amico.svg": "a14b9d543d895bcdf0758f7b9ef5908ee0acaac794446494af059b159247db8f", + "packages/app/public/assets/Aero-Regular.ttf": "0ebf14f80d05c3a82c766124ab31196c310014744333d397425ac82a1e3537c4", + "packages/app/public/assets/JuliaMono-Regular.woff2": "43f5dfca02a03035f89799da5af150f28d5c4aff729753076094c687fc61b063", + "packages/app/public/assets/RacingSansOne-Regular.woff2": "2ff92c8abe7a35962922c54da215d68690f8502463698b1b595bed597b63e3ff", + "packages/app/public/oc-theme-preload.js": "483f1c347e8f4c7deeac3c20d324dc1329631a682da9c44e56b7b56b8df9f69b", + "packages/app/src/amicode/inspector/device-inspector.tsx": "5ab63709e88176bb4dad1072ffe28fd59ad67d414c9d844ee6c55353df50df79", + "packages/app/src/amicode/inspector/inspector-bridge.ts": "460dbf4509b4e56a8eacd6cac7adcb3a63c6cd3e417d851df5f3e3b259095d3e", + "packages/app/src/amicode/inspector/inspector-context.tsx": "ac81b9e0e2a4035f85b6af68b7f0cefcb9f256a175b244ba13136f3a307c0c77", + "packages/app/src/amicode/inspector/run-inspector.tsx": "63a6351fad12a8377550eb87f7602a712bae586fe482ff46cd847c8544fc6e8a", + "packages/app/src/app.tsx": "48912ae64588cd21c71a2e6bbd563f174c19861defb18aa0b323bfd22e975342", + "packages/app/src/components/amicode-default-model-value.ts": "416bb4a7cf051c678bdfc5246ddc5796a9a6efe48d1e5c2e933756800cfdae8d", + "packages/app/src/components/amicode-default-model.test.ts": "941750d074706bd5f4e433c9737b19ebc0ac0f9ba9cd1faaa599a24b22126e05", + "packages/app/src/components/amicode-default-model.tsx": "16a2de1feb12e07416fe5fbb25879391f912ee0909b553c868c1b531f2ed1f2d", + "packages/app/src/components/amicode-defaults-capsule.tsx": "6c0b143695f63423e651e0123ece7d5dc15ef2cb3e6ab9aaac566287e3efe6da", + "packages/app/src/components/amicode-file-ref-bridge.tsx": "11c17d7fdb29d16530e188b13ee8af728f37d9adfbb518084106ce50cc0c527a", + "packages/app/src/components/connection-banner.tsx": "69aae9294b797d06b21cc3958c7bfa7a6dad451f14399638037ee19e4c9a0203", + "packages/app/src/components/dialog-new-project.tsx": "102fc33e8eeaf3c76ddd1b953b7b97995d30c02a3579f8b7036e47448a1582a8", + "packages/app/src/components/pane-bridge.tsx": "756b3306b51c963fac6778716f6c6c4626f0f3bf75c93b7cb4cab58ea0bf3d4e", + "packages/app/src/components/prompt-input-clipboard-structure.test.ts": "8c15844499378d6ae1d84d7c6f5b99bf37cd9baf5b35a45dc1535246f3d324c6", + "packages/app/src/components/prompt-input-v2.tsx": "9aabf52c91628015d4eeff15292696540c97a97572b5dadd26ee328631c0647a", + "packages/app/src/components/prompt-input.tsx": "641c5db9bd27a90887935c9b6fae45ee250504f17b25891eb51f292e4273d320", + "packages/app/src/components/prompt-input/attachments.ts": "c3c28df0d2f18378d39e17147a4c64e8ea01d63b97c39e1874ed9445ca671bb1", + "packages/app/src/components/prompt-input/clipboard-bridge.test.ts": "cbc50495466c169d4f558cdb0aea5fc9215baf5257ddbde0414491826e4087d9", + "packages/app/src/components/prompt-input/clipboard-bridge.ts": "556f79e96388bee80fa1c38b5e916f7efd1d87227c1e9eded8ddc59de47b961c", + "packages/app/src/components/prompt-input/placeholder.ts": "f085dc0cae6c2267f1a4a0e819dc9061d97ebdabb3baeae44f91376fbf1a4837", + "packages/app/src/components/prompt-project-selector.tsx": "1d0970b4d067261d3b88af089537be8200369e490d2e02553043a140d9a8e961", + "packages/app/src/components/report-bug-button.css": "909809b6241d53c386aa4c8ea34539ba162a8998fff755834c783025bc53a5fd", + "packages/app/src/components/report-bug-button.stories.tsx": "e3d28ffec2f9682227a3f6ded24674afbbf0416d64369cac4711bb12d7709e1a", + "packages/app/src/components/report-bug-button.tsx": "a1beb8c94b4faaa4c7db3ab68de349f8a8a03cb3a058ce447f552aed35848d43", + "packages/app/src/components/session-context-usage.tsx": "f5b07800bbe571067a8625d6175a29ec1626c365fc336e96db92f6506a84aaa3", + "packages/app/src/components/session/context-warning-banner.tsx": "f5fab4d7a9536238b70d64488fc30d44f6a4948fdcffd910a9666f4b6c296019", + "packages/app/src/components/session/index.ts": "21473290d4a1a3d0670fd878372ddf21ff22d4a0b30d1aef6c81ea906c1488b4", + "packages/app/src/components/session/panel-menu.tsx": "d07e11d635615107c9acdb06460460b5015b4141fb3f9c4002837933f960e22f", + "packages/app/src/components/session/session-chats-dropdown.test.ts": "2003d2a15781337ea6bff2c68e730cc5b6df38697f15d927ab26913936444db7", + "packages/app/src/components/session/session-context-tab.tsx": "227243b178b517f067d9ae0ae0eec3c559beeb6681828158b0600a17e98e7f81", + "packages/app/src/components/session/session-header.tsx": "e40067c61f958395892dea880d9424ec5c17fa6bcc093e9b33baaed922e2738e", + "packages/app/src/components/session/session-new-view.tsx": "3043eab823572207da9de02028bf7698600e074bef43b47f5e29b325961048c6", + "packages/app/src/components/session/session-preview-tab.tsx": "b6b4b8fe6f751499e0d3005e1f12224351df4f28f221bba10fc2f91c0f09e777", + "packages/app/src/components/session/use-context-warning.ts": "af7a6d0159a5541aa02ad4d08fd694af1cc4853fc1c0a70763bc264a634d1c53", + "packages/app/src/components/settings-dialog.tsx": "da3bc01dad4ece4214c14083ae4e7dea87ef64ef9829b315b867fdafccdb6916", + "packages/app/src/components/settings-v2/data-storage-controller.ts": "fa5d143cc101f3a3b9d5ad445edddc981e0d02783021d52dbfed6c8d8bf62498", + "packages/app/src/components/settings-v2/data-storage.tsx": "a56b423b32c952c4379688d88d13a9f43b163a4169b9393f8c35b18a763a1180", + "packages/app/src/components/settings-v2/developer-tools-controller.ts": "bfe2d97b034fb97802123c5d03de8a1feff495ae09557b5619e2f1df1d9a68f6", + "packages/app/src/components/settings-v2/developer-tools.tsx": "96ccc0e2ba8ee40eeeaea4933ad176a4029a1b972364e070f0cb027d8fb0a1db", + "packages/app/src/components/settings-v2/dialog-settings-v2.tsx": "253c32f1309aa5a474075f8fe5d3ad2f40946b772903e07d38a6560f9c4e634c", + "packages/app/src/components/settings-v2/general.tsx": "e20b03b6f6bb6fc6418bc60ce6dc5af2a02fedc1da76b4ab6a861a2267b909ef", + "packages/app/src/components/settings-v2/permissions.tsx": "89b491b4971f23c5308f34096c2645b01831ceb1ed9e604fd494af9317d48f0c", + "packages/app/src/components/settings-v2/settings-v2.css": "481af8fec5288cdee579a66c9718a99c514b1cf732ab5bbb2829766d55af29ef", + "packages/app/src/components/split-frame.tsx": "44c53737f72027b461097c03df48a4f411d04a82d02b1fce4901813c73dfcc72", + "packages/app/src/components/status-popover-body.tsx": "5e2fc75b6974960dd86f885d4496d8e53d0ff2d2226221e4d48309956d612fd8", + "packages/app/src/components/status-popover-model.test.ts": "10634035bab0f31de24564e0d6c8413a0f7434fb2c08ac3ef722981a69dd08e7", + "packages/app/src/components/status-popover-model.ts": "ec5a5a3caf9344fe617c7bc668af81d96ad4290378c34186cd4cd959821f1c85", + "packages/app/src/components/status-popover.tsx": "47d308ada46d3c0d4c43ead93e97016a04ba5b17caaf9e5c6d3a92d1b1b6d019", + "packages/app/src/components/terminal.tsx": "3d5468362c63bd01661c745612e3d64a6d9e8a999d66b38202c5bb78401dcea6", + "packages/app/src/components/titlebar-channel.test.ts": "fe4dbe37e8b599878e16f2e56a62814fa787a94f2e523b3ca3f2d5310cf940cb", + "packages/app/src/components/titlebar-channel.ts": "2e01bb619f36799a684952538bc47844bac96a2c5eaaed240c6317d2b07b8efa", + "packages/app/src/components/titlebar-tab-nav.css": "ab5dd7bc4810dd5c4c5f6550b10e98985823ca8e0b5365b1c5ddde6ff0b057a5", + "packages/app/src/components/titlebar-tab-strip.tsx": "182a21a367b36dca30a570ea98015f434cec49db671edca6e921778a3abeb6e8", + "packages/app/src/components/titlebar.tsx": "ca2e24f3d73f92b075ec120acc399c23e1b04feed6f05878817d78a692937a0f", + "packages/app/src/components/vault-browser-model.test.ts": "7ca11eae3574033d58081bc4126b1ec772a5b496573c70159f05497a1365f4cd", + "packages/app/src/components/vault-browser-model.ts": "d9ef6ff588a96ae79b13c3ae0c2a442a42b59021dd501b3a17bd810dda6f7b20", + "packages/app/src/components/vault-browser.tsx": "509efa874d69bcd07ef2495a025bc8de2b2fa582736003ff709b4ca76096274b", + "packages/app/src/components/vault-panel.tsx": "1d9f06f42b94ffb94a28bfd6d2693a36c75ceb969bcd434081b4cd2fbf99665c", + "packages/app/src/components/workbench-panel.tsx": "d16f477044098842a214a2e16f9be719e7688a04bcb575889ca544988f23c690", + "packages/app/src/context/command.tsx": "06ffdc83f39186481bba8120e4bc1b14c2160d1dc789a29e0a07ffc7c5a2cd35", + "packages/app/src/context/directory-sync.ts": "0d9e6aa3cff6c6654c98928f59c4e2fcced04f42b167184eea77be04c00371ee", + "packages/app/src/context/global-sync/bootstrap.test.ts": "149c0241b378b6ce837aef45e1ba257dbfed88de37c70affa13f513e9c72338d", + "packages/app/src/context/global-sync/child-store.ts": "b13a513c93b8fec7b656593d842e9cfa30f346a0911dc0f516783ccc5fa4c07d", + "packages/app/src/context/global-sync/event-reducer.test.ts": "600cd7654854b910f25c61b20d8ae69f043f52ed544e6bf9be5ee9e5bbd6c96e", + "packages/app/src/context/global-sync/event-reducer.ts": "667fbae1259f6deed77573edd2b742beee4f825b4306b6d861670443e381a55f", + "packages/app/src/context/global-sync/session-cache.test.ts": "c66d790c6336794b2b1d707e2739fe0992ccad7ed3b96187b124b5c783ef332a", + "packages/app/src/context/global-sync/session-cache.ts": "1849408caba0f114809f37153e7d531bf2776572433880a96c5ebd31d7b30aa3", + "packages/app/src/context/global-sync/types.ts": "901c9bd1fda63dab676d33dc1a8582a29f8ee00aa20458e843aed5f93414fa84", + "packages/app/src/context/language.tsx": "cb545e04c128bf981b2b72ac407f9220cf9e80337fe8c87482ffe85407e0cf9a", + "packages/app/src/context/layout-tabs.ts": "741506ce165f68cdb0b8f2931a2dad3e26c05880f9286c3daf04ec979bac21c5", + "packages/app/src/context/layout.tsx": "54dbe82c704c92109eb392b512fe2d828a5ff00a14315173ad8923bf0621d788", + "packages/app/src/context/local-agent.test.ts": "a5a9d60bb4401d409218cc247c1cc06ede8b7878ddf01dc6080328f54eb9cade", + "packages/app/src/context/local-agent.ts": "0aab67e695dc3bb45a733ac0df80a0a5e14cfe29b2375b6dc3cd07fbccea33e2", + "packages/app/src/context/local.tsx": "3ab8b9fc2db082df4ba485373679f00a95d0ffe3c691a0afee1dc53db7eabd9e", + "packages/app/src/context/models.tsx": "8904525767b36ed5ba93e85a0fddb9bf737a8d066dfff084380ce1a67182f535", + "packages/app/src/context/platform.tsx": "d4ee3f33e485f5fd6d6d449edcc7da0bbbed733e139f9f293fd011e78153c6c5", + "packages/app/src/context/server-sdk.test.ts": "e9f0390dec05e8c55f4fc3127e18c05c2674856d853bde30a67952a313098d52", + "packages/app/src/context/server-sdk.tsx": "c147f740ffaa2feb87364a70f535c5bd20893a7f54b82f66374a21407d066b91", + "packages/app/src/context/server-session.ts": "009fcdb6cd374c0f2cac53d37952afaa611f65371b43ff84f3654cffa8c56b18", + "packages/app/src/context/server.tsx": "08e978e4b4b3094221a2a0b92e069f39cbedb6d6cf95935881e21cde52265ee2", + "packages/app/src/context/settings.tsx": "84911434b26eb3254cd399f0ab39396e22fc7cbd502d3d5007cb7eab773007e3", + "packages/app/src/context/split.tsx": "891aeb290b369b327a63ce548dcaab1daca08e41fab430648787be7f84e53ec0", + "packages/app/src/context/tabs.tsx": "6ca4ae0d85d5456cdda6f9aaec7e6aab0ca8aef0296b61f8bae7ee51da150a0a", + "packages/app/src/context/vault-panel.ts": "f91cb5fb49a9f0d46bd2a1b131f9a218b30147e2ad3617d2a2dc4f1e14599a53", + "packages/app/src/context/workbench.tsx": "69a44da2b69989bd2c67ebffdb49941841f3f4718d6a70d659c828b57928d9eb", + "packages/app/src/context/zoom-keybind.test.ts": "a1385c93639687a7dac7d21f8f02841d82a55e42465ce3836b97b16c82b52469", + "packages/app/src/design-polish.css": "26cf372ab527f44ac45f660f44dcae900f89c432fbd3db8684161058e13f364e", + "packages/app/src/desktop-menu.ts": "55481604f4fd5dcb74b0678347076c42fda7f928590d90bc2c3d1633bea8bc36", + "packages/app/src/entry.tsx": "da720fb72421c8e247a925fc627b24096e9d228dcff078cec81cc68f9bd37f2f", + "packages/app/src/i18n/ar.ts": "2b5e0c7f5a8c4ebc0305c956a8d9944c3cc86b1169bfe995d1b72a5bc9b5067d", + "packages/app/src/i18n/br.ts": "04f77c8d95bf83eead10db5166f5348cd9fb20940124fe117606c48ddeded9c5", + "packages/app/src/i18n/bs.ts": "798e3aa47f037f22cb0f0c54f96d1fd7ea257f303f87273dc09408b98b4aa314", + "packages/app/src/i18n/da.ts": "d2be9c55970e859d16751bd10fb61c15a5a5357550a13b3ede93736700cdce73", + "packages/app/src/i18n/de.ts": "84b0c448418236c529f65ec061d829c6de81a7c475df063f69a4163d00a205bf", + "packages/app/src/i18n/en.ts": "b54a1ad307d60b96d240a8bc138b944040eed66856929b52a79ea2a47a052666", + "packages/app/src/i18n/es.ts": "5dae047cff6fa6020c4e3ec0423fe210f02b7a2577039184bdbfbff3cd2a3c49", + "packages/app/src/i18n/fr.ts": "0e1bf25a4b3be9e7aa98df2a40c0b4a819cdecfb968d3724a2db7aa9b17a7b60", + "packages/app/src/i18n/ja.ts": "5cc5332076182e3f7da433d9677c8b9c1330874b0a2010ddf61a6e87b435e06c", + "packages/app/src/i18n/ko.ts": "bac03c8dc86207b10d26a137f0bf848d9c4da996fb39dd092f8f2fca140c803c", + "packages/app/src/i18n/no.ts": "655b405e831778237e2f56c5ab91e5b70fb28726a33374c173a7a54d8dea5572", + "packages/app/src/i18n/pl.ts": "0ba6615a07bd247958329692944a2492770407f9f34acdb12ab3083acddcead2", + "packages/app/src/i18n/ru.ts": "2c9d4cc273ff6259146e8031baf7fa501bfcba9895235982e21aaa9e726afec3", + "packages/app/src/i18n/th.ts": "55c4c7ef43be1832df368de69f626b714e4fe11e6e791b18b2c31c022bdc0037", + "packages/app/src/i18n/tr.ts": "01b9fe2566442d1c67019b440174cfe25424a0e9f082ea6566ed12d7d8ec065a", + "packages/app/src/i18n/uk.ts": "98c397aaaba1e66bd3e9fc6041b2a5ea0913077b203839dd16b8714db90d9a04", + "packages/app/src/i18n/zh.ts": "20c0ce167ed34d6fecf26d7f2c79839d9d7cbbf23286679ff019afd98f7a8d46", + "packages/app/src/i18n/zht.ts": "1a40c07bc44d2494a09990f2eb9cdb972ded8c74d053e4b9fd49fe4fb4c5df8a", + "packages/app/src/index.css": "c453a2430a68c1970a6b570be7840a4b07156d0238cfce8d8ffb8af3d6985b7d", + "packages/app/src/pages/error.tsx": "db9ba0847cfb205100ad6133cce30c6ea2ce983802af44947e6d2b746c2a6ec5", + "packages/app/src/pages/home-projects.test.ts": "a90155c37210a0e5b7a095bbfc3ba2d40681f02927d9726148a706bed5ba7891", + "packages/app/src/pages/home-projects.ts": "d55124578dc839b98e2084b488d818e30e4a1abf1ae264499b83e403c2aabca7", + "packages/app/src/pages/home-session-archive.test.ts": "e8f1126287a3e9daa455efcc378e5d72aebdd092158bf8526f7779669b72e850", + "packages/app/src/pages/home-session-archive.ts": "1b56c173f37f4a4dfd54088ef5fdb0dbcd3d9b62aeb08de4bfa0ed34f07d7ad5", + "packages/app/src/pages/home.tsx": "42c9b0c6fe79702fe5fe548fb1b25dc7bb57b1e6c2bf4225922781f8df86ff9d", + "packages/app/src/pages/home/home-sessions-view.tsx": "757e5db2ed0b62fae5a636cd2ab9404d37bf07640b518866d2da0e0b296903c1", + "packages/app/src/pages/layout-new.tsx": "32485d90c94b3137812ce7439da4dd1ba14689a3340bab6d02cf4877715ec9ed", + "packages/app/src/pages/layout.tsx": "a6e6574054f6d85c0d91c1236c9db99674b8d51876643896b0c34f77a28bc85a", + "packages/app/src/pages/layout/session-tab-avatar.tsx": "a69e8ba00edaffe0aee86e2c1f8bd82a2e6438e9752322a39a70ef9d57c76f00", + "packages/app/src/pages/layout/sidebar-items.tsx": "daf940fc66f34e5f83410a8580f7bb1790bedc599ab4d64fcda2301537b24865", + "packages/app/src/pages/layout/sidebar-workspace.tsx": "a0f8e3fbd16d09e783db2a7a09c686d3aa993436d882425d3436a59ec4adf94b", + "packages/app/src/pages/new-session.tsx": "8cfa0d719590e29fc185811c57306e80c54faf62a7d6049138d15c02e7c844a1", + "packages/app/src/pages/new-session/new-session-draft-controller.ts": "d48383233337a8095ef7a577dac021ae12c712c609fb546427ecda4b6e5299df", + "packages/app/src/pages/new-session/new-session-view.tsx": "230d8c19a76efab586e4c6f48cf271271268bbfeadf71b7fa54b08c4f7722970", + "packages/app/src/pages/session.tsx": "ca906c2d99a61cd4038c76a4ed943d6ed72f55c31fab3b37d1a6d274ae5ac8d9", + "packages/app/src/pages/session/composer/bug-dock-controller.test.ts": "fe3d68f2f2bb41b04ee5567cd95a9c01671a98fd21585788be9d4cbb01a4bc22", + "packages/app/src/pages/session/composer/bug-dock-controller.ts": "ab2c72a761e1992f300b6a0cecc9d0351b00a56e4be08567bcb22d3fcece78f7", + "packages/app/src/pages/session/composer/bug-dock.ts": "11eb1a1c911df8764162d47f07e5f135d94fcb622f8a2ad9b41a23233131ba58", + "packages/app/src/pages/session/composer/report-bug.test.ts": "d4dc012309c2171298852501a14a077c6fe8cdf5d25538dc84ddd1f8195d3a8c", + "packages/app/src/pages/session/composer/report-bug.ts": "bdc3b3dec38b11c7bfa5b867a246db2717e60e1d50692d6bddb3983c9e95e02b", + "packages/app/src/pages/session/composer/session-bug-dock.stories.tsx": "8244e22ad849861f9da24e559bc3166537e60331f15090d8b0fecbe45fd34d2c", + "packages/app/src/pages/session/composer/session-bug-dock.tsx": "e7fed3c5ab80527d69a106440833f795bbbafc5deeba3883902ede9fba612605", + "packages/app/src/pages/session/composer/session-composer-controls.ts": "b6942cd192eef5fb67343d54be35c986b735ca1e2185a20b29e226dd67f70e05", + "packages/app/src/pages/session/composer/session-composer-region-controller.ts": "3209b9a8981cbaa7f0cd7bd09412b681c1dde108ba917ab54555f9144b6a443f", + "packages/app/src/pages/session/composer/session-composer-region.tsx": "7f8670d8bec3964527a916923426baa38e7b49090cd634c4101e6eccc81981a1", + "packages/app/src/pages/session/composer/session-composer-state.ts": "fcffa11d801309cf58f42a174143bf6eb3ea2b14f860ad91a8ffa4e097df5c16", + "packages/app/src/pages/session/composer/session-question-dock.helpers.test.ts": "a1fe31fe4dfdf2914bc184030a0427bcbdfae0f3e7b146e1d6ee29ce31d85b64", + "packages/app/src/pages/session/composer/session-question-dock.helpers.ts": "b16282d0cf5d39152bd76f4ef8bac32f964f68143a3067765ae0827a5f90deed", + "packages/app/src/pages/session/composer/session-question-dock.tsx": "c6f865f2973fcfe56dc63ad60f3ead07c120595c2880eade70ada63d871238a1", + "packages/app/src/pages/session/composer/session-revert-dock.tsx": "3f2d030126a4c673731bac65406fd48797464ddc515444b891e39b0eb538e002", + "packages/app/src/pages/session/helpers.test.ts": "d9e2ba5625a2cdee7610d2f70b31da4972fd77e989db824d3f76b474691d9beb", + "packages/app/src/pages/session/helpers.ts": "ac1ee7592c8aa77b3b5026950638c07e1404531d2cb92c64e622d92dca6ee18b", + "packages/app/src/pages/session/session-panel-width.test.ts": "2b9daf379be0b54142dd791bed6bae6d215b2574176b3b3e4b1273db3baf433c", + "packages/app/src/pages/session/session-panel-width.ts": "8723cb2f980972ea9bf182240fdf154d131bf40e22fe563fd538a7005cff9192", + "packages/app/src/pages/session/session-side-panel-structure.test.ts": "c138fe905498c8326f459dccba61b146af6dfb12b78480303723b5a85046931a", + "packages/app/src/pages/session/session-side-panel.tsx": "d38bdee95cb8efd00f3350b503c701551cc050e2b8a9ca1f7379a0275a87a8cf", + "packages/app/src/pages/session/timeline/last-prompt-bubble.test.ts": "83753b3ec7227570523c2e0b9e82401fae7a20f3a23980bb40deaeb13304327b", + "packages/app/src/pages/session/timeline/message-timeline.tsx": "b4cbbaaa0b51358efa4924374a7c1bbf16c4e827641252eea5bb76d260b5a7a1", + "packages/app/src/pages/session/use-amicode-commands.test.ts": "65671d054c404edd2b799b0f6591a9c055e6ecb454a674fa0bd9a7fa6f68e111", + "packages/app/src/pages/session/use-amicode-commands.tsx": "88bfdad9ae6dfd920b8e6a14ffebd2cf9e52b8f67bb9284cb358ad940643ca67", + "packages/app/src/pages/session/use-session-commands.tsx": "7af9a112ae60415572ab7c9b5d4e73b2f8ed9115858c695be675ffdf07c3df2b", + "packages/app/src/pages/session/v2/accumulate-diffs.test.ts": "e22895e339821ea468f8aeed53deec5244ceb9bd4371c815926ec19203258d07", + "packages/app/src/pages/session/v2/accumulate-diffs.ts": "f10a1e097a86f746f3fc2b861454f3b8f509e6895c54b7e3bd0317dbf3d14acf", + "packages/app/src/pages/session/v2/review-panel-v2-state.test.ts": "ac0cf1939c164948d9a0c54cc15076b30413612d872237e0fbd0912804c1ced0", + "packages/app/src/pages/session/v2/review-panel-v2-state.ts": "e27c3aa236d2862e3cbd87128c4e03e6d810974faaaadb12a7e04cdd3b531e1c", + "packages/app/src/utils/amicode-bridge.ts": "5104a04c7822f80dc0c564619e7dd2d850ba50035dbc86ea875604ecd97df05f", + "packages/app/src/utils/amicode-bug-report.test.ts": "b1bbf49230ff3aa6d1a43ffa9165ba5a84ca4ee18a6d49f52ee5cd887c3e6444", + "packages/app/src/utils/amicode-bug-report.ts": "d55d9cecfe17719c0d4a0ca498345c7eff6fca1de6b645e4bc3fecb40f2f4645", + "packages/app/src/utils/amicode-fetch.ts": "ec62b502e6b8688cef0a2aa249715e847032265fd0e150d73ab3021a63db0e33", + "packages/app/src/utils/amicode-hidden-project.test.ts": "333ce7e13a2940268448db06fcfc8bf1733110b5311932820ade1dc9ab573180", + "packages/app/src/utils/amicode-hidden-project.ts": "d845a80ecad2d497528a5071f55cffab72e7b451f16d106cdb594f5bbd62c0ee", + "packages/app/src/utils/amicode-pane.ts": "46c18a2dc47c5db154f70272d25031e6834539ad03f6ab25613f082426f7c642", + "packages/app/src/utils/amicode-route-info.ts": "a0268a223e8de09fae9d4de8bee6a7774c14c87c6254cb4ed1d921db18587f44", + "packages/app/src/utils/chrome-dropdown.ts": "f8b878c5df4e1afd96b0751c73daaad0a8127411030724cb8f9a5089ebb29eac", + "packages/app/src/utils/global-clipboard.test.ts": "86f737c91542b8bd79b7872bc551ffd4a334c5d5a8bedfb2ae95052697dfc27f", + "packages/app/src/utils/global-clipboard.ts": "0b0125150e5c521ab5913699ced86b7395a1cbfb54549003b4558b9b4f421c13", + "packages/app/src/utils/pane-bridge.ts": "a8a8a40dc60a367086f3c4e4c561a289fb85d8b2086a8d62c2b66102de652ce4", + "packages/app/src/utils/serialize-session.test.ts": "f8fe318f195dca251eae3f4899f4a5881c3523522f59e02fef208588391f4048", + "packages/app/src/utils/serialize-session.ts": "c303c678500cdb64c7aa6928a0bd984c441a36705fe8152368a0e575cbcb459f", + "packages/app/src/utils/server-compat.ts": "01413e13b87485770eb3dbec0b882aec4c8247dafc4dc74b7b4669fa5b20cf1a", + "packages/app/src/utils/start-prompt.ts": "809d8ad0ab6de76a97503f8368ade25f771431be3f92eb8f039a2c65244ab20f", + "packages/app/src/utils/web-zoom.test.ts": "f373618138add504faa44dd6d2c9e3489b6e8dd0093ac6bb4f00f55307864e3d", + "packages/app/src/utils/web-zoom.ts": "014f99ce01219cac2bf22f30f20936d271aef17870e710250f6b1aa459317e44", + "packages/app/src/utils/webview-context-menu.ts": "6a8620ffa52ca745295cb9f9e23364505789c3424c2d91afdf3f5b9621ad812d", + "packages/app/src/wsl/settings-model.test.ts": "3304797c2779ca573f44eb25b758b67d66b2d7897432252375606680021390c3", + "packages/app/src/wsl/settings-model.ts": "35481fe3029714becf30b07024e0f849438c99c369a41b437b5208e48095cbf4", + "packages/core/schema.json": "cf1dc9d2ffd2383d473d96e7054258df0f719bb3a727f025cebf14b4d2c1e431", + "packages/core/src/config.ts": "b71c5920982666df2668de8aa04df85b245ddf95532c0c3df8fb6fb985402704", + "packages/core/src/database/database.ts": "9406173aa6cac6acd1c3ec306ae71942e12c8e2d68b7cd3d58a9a094539bc32f", + "packages/core/src/database/migration.gen.ts": "26804530f487a84d3d7abbb5381f4202ad726624415a4601925b302124e41743", + "packages/core/src/database/migration/20260813162312_shocking_karnak.ts": "f4e901f79e3f660b3809e336599a5ba7da7794ff595e6accf6218739c046ab68", + "packages/core/src/database/schema.gen.ts": "723e8c027dc573e86cf8a27795672de865befe0b00ee4165cb526e84b962873d", + "packages/core/src/flag/flag.ts": "55ffe731e35f8e045934463529e63bcd1e5f81df9fd69ebe585af85635ca72b0", + "packages/core/src/models-dev.ts": "dbbbe33f470e219d5fdaa6ec8e4998be95356e80756f29c18e3656562523923c", + "packages/core/src/observability/otlp.ts": "be20b97f84b2a5b4a1663fa3bed0b31c6c387950f7c28b76fbaa8525cd24d6e8", + "packages/core/src/permission.ts": "8c46b08adc9edd6bbfb90481b592ea6f00c40743d1ea0ee6a8b2e185dfd2de66", + "packages/core/src/permission/provider-saved.ts": "029f02a7120fde4ca1d499294f73f22ca8f0dbaba030a901f94093fb36674025", + "packages/core/src/permission/sql.ts": "c8c44e00cf957511282e35cbb192c0be36ed33d41d6b15e98d79eab4378661cd", + "packages/core/src/provider-permission.ts": "a672c1f40fa2efb0cd8923b0b7827b6debe770308bcaef692ad8589ba9b40685", + "packages/core/src/session.ts": "9b0942acb81373bd177ec544eaba74815369c0be55ad35c76a48b7676f2ce315", + "packages/core/src/session/runner/llm.ts": "e1c9da5f568cab64ebd49a467bff8e11a685306240b3faf672127663f1b02444", + "packages/core/src/tool/bash.ts": "4b56de5f3f7ad3ddbcf1ba756caff49f0a65f0fbc73811cbfc312ea716e0e447", + "packages/core/src/tool/edit.ts": "b1bd6df76da2c04681aef1c0d6e90e16e929b2c1ed09f6367a2bb9c655dc1d68", + "packages/core/src/tool/glob.ts": "3da75d862c37d35aafadf6d54b5d7ef3a61eefae049ac8c3ffe9cadfec818006", + "packages/core/src/tool/grep.ts": "db323a91b27727abcee372bf5403f322458ce456c9b581d4f905ab241bea6dd6", + "packages/core/src/tool/question.ts": "b4e114de69285147257a4be20d2ba54b17a42a5cb2daa178fea9fa4b8989887d", + "packages/core/src/tool/read.ts": "ceb37f787cc78a9973dec7c0f8c5fd647baeac979619741d0ff15167e13c6f29", + "packages/core/src/tool/webfetch.ts": "904846f51dbee2579a36c4856efbfe1442d4ce6a31d6a280cc387c390fd22bef", + "packages/core/src/tool/websearch.ts": "bd0502eeb9d6deeea975a21a592040b5b48abd6f2613e39dd6cfbd1a959c950a", + "packages/core/src/tool/write.ts": "4759d89e4b6cd5fa7d7de4ba091d2bf859d830cb15038828a15751c80fd9b0ec", + "packages/core/src/v1/session.ts": "53714b7754a3adecbed4e28d511f5c1ef97ae2fa379e3a07cc68714e1de8469e", + "packages/core/test/tool-question.test.ts": "e73b6d0aecdba94580e7d63e2da65cf17b475c81a66998d5225980cbb849e895", + "packages/schema/src/index.ts": "007e8bc04cc91c2c0aba3a78212c47c4fed6a8edd0d4be05286667d303448d11", + "packages/schema/src/prompt.ts": "eb3b42b98ef856f85f32154482b19de546272f1eabe552ee9207db030346291b", + "packages/schema/src/provider-permission.ts": "382d3b8c8e7bac64816d6a8cb0a84dff6e9a4bd1a921a653ebb20b23d4580e14", + "packages/schema/src/question.ts": "b334a3e7cea88e73ea11fef5bcdcbe258f57b586ef96142b760294faa6e4f0e3", + "packages/schema/src/v1/question.ts": "bee4686812c9492c39e4df34cbada59e63dc48240823d55fec61d6998dade5c7", + "packages/schema/src/v1/session.ts": "5804deda9cfcf2a015c759a6905b5bdfac74897239994c7facb246a60dbcf285", + "packages/schema/test/question.test.ts": "511f4fabf2841ed89e1fed9594f2e3a1be8eea5bcb50b680d762c49d6ef572d9", + "packages/sdk/js/src/v2/gen/sdk.gen.ts": "57597d710845cc0004ba778522c9bdf088fe9f7c23d7d9a3d947038a1fdc2674", + "packages/sdk/js/src/v2/gen/types.gen.ts": "394f4238a9284d9b250bd95104e5c2882a68b51b7c8838c164e5293d1bff52e1", + "packages/session-ui/package.json": "d0cd55d4641253e9715551a6e6f493ce692f641c51f6adeb19c8c896c336f2b8", + "packages/session-ui/src/components/group-parts.test.ts": "a31ce19009e1d62e75e83b682190a4613da0eed334420409ed73a9e21834ae84", + "packages/session-ui/src/components/markdown-file-refs.test.ts": "e2b1bed33629d3ab53c521f33266814088ea4122b6e64d6ab294eacc4cf15070", + "packages/session-ui/src/components/markdown-file-refs.ts": "b75bddeb565dc0163635fab8f84bdd26723742f9f82d06b6fd5991777b311032", + "packages/session-ui/src/components/markdown-stream.test.ts": "b73b1a15ffa2cfea5358b2d6313ce17d26a5a11302b0e3b7d823cde0c98cbc83", + "packages/session-ui/src/components/markdown-stream.ts": "50e38abd5d4ff919564655799678be10463dec58502fc5487c1c97a9a8db5399", + "packages/session-ui/src/components/markdown.css": "31f40458ea34a594112b2b148c8d49d0677e3f3dfe03b36ba1bcb6ab02c4151a", + "packages/session-ui/src/components/markdown.tsx": "89e132e62d20c873311c9781d0ca4d7cd1a7ff882d99f19c8696aafaf4a8f298", + "packages/session-ui/src/components/message-part-css.test.ts": "ab31eb47306e0826955a4471f18b263a77cd6f77f0eace7a46085645b2600c86", + "packages/session-ui/src/components/message-part-groups.ts": "37be069bf46e926fcde83d3744ee706911ff002e6b532e279290d841aeee96da", + "packages/session-ui/src/components/message-part-skill.test.ts": "453bdcb25e4df91ad1004635b8766070a60421355adf18ee53f43cd2fa7d0486", + "packages/session-ui/src/components/message-part-skill.ts": "d65a15dc39b8c0dece13aebe5e308426ae2dd758b07740251525ba9257654cf7", + "packages/session-ui/src/components/message-part.css": "74cbc0af6b451c846c95974f4cb23fe1d5d9c3deaff1f7ff886274c257a1b491", + "packages/session-ui/src/components/message-part.tsx": "62629470594ab6270267cb1d8ca48d43c1166d4f904f0cb8802cf48c27ae73ac", + "packages/session-ui/src/components/session-retry.tsx": "2cb1513d73275a269894f601e8c1d29bc790b14927134fe36177f2b371888721", + "packages/session-ui/src/components/session-turn.css": "792915f3373134eadf3cd51e1d4bfa88ec347e6b037b6caf60b639ec5e74fbd8", + "packages/session-ui/src/components/tool-error-card.tsx": "e27b89836b7c709120ac15fbffd2200038944058ea3506e61e5897ffb3537503", + "packages/session-ui/src/components/tool-status-title.tsx": "ae4561b58f55985d2294a352802e723b18943ac9a6551d97f1c4a6cc95368c8a", + "packages/session-ui/src/util/clipboard.ts": "c37ba53914714ed04135e5a254e8ea90186f097d7470fa0873a83de82f3fef2d", + "packages/session-ui/src/v2/components/prompt-input/attachments.ts": "ff66078f2edc9a67a4df0d7dfd76a49be5eab27ac332b303f3412721ef7268ea", + "packages/session-ui/src/v2/components/prompt-input/index.tsx": "3d1d7f1b435d63aa0bb4fcfe5ad3eb2e7d386d5c5c1974f3a15161b15ea9617e", + "packages/session-ui/src/v2/components/session-review-file-preview-v2.tsx": "2aa16fef06bf6de8d5294a85ebd5fbbdfff3879f00f75e900d8049e33605593a", + "packages/session-ui/src/v2/components/session-review-v2.css": "166b71562d1bd7ae3c6a38dca854704fd903be29cc7088e2396e06c66070bbd5", + "packages/session-ui/src/v2/components/session-review-v2.tsx": "a683eb509c3ee927a7ed04ceffef3fd65129f812ac55f5b04c7e628bf0611831", "packages/ui/package.json": "b1d168d0371e9094faae1107fc6c00be197f09bc69daa2247a3890d607f4b629", "packages/ui/src/amicode/amico-presence.css": "b3f345b607e318dcfbefcdc677fe21cc7d387b9946b0f9d625b0a842fe7e31b9", "packages/ui/src/amicode/amico-presence.stories.tsx": "33c854edd9a061d79c53e160ce5685ddb8cd41f4e205c6eb1b0dc96fec5017bb", diff --git a/packages/app-bundle/overlay/packages/app/CONTEXT.md b/packages/app-bundle/overlay/packages/app/CONTEXT.md new file mode 100644 index 00000000..4c4d68e6 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/CONTEXT.md @@ -0,0 +1,130 @@ +# Amicode App — UI Structure + +The SolidJS surface of the Amicode extension. This context covers how the window is +laid out and how a researcher moves between chat and the supporting surfaces. It is a +glossary only — no implementation details, no routing tables, no component names. + +For the conversation/session runtime context, see the repo-root `CONTEXT.md`. + +## Language + +### The shell + +**Landing**: +What the window shows on launch. The Landing is always a fresh, empty chat — a new +composer awaiting a first prompt — never a dashboard or a resumed session. Prior +sessions and setup are one interaction away in the **Rail**, never in the way. +_Avoid_: Home, Home page, Dashboard, Start screen + +**Chat**: +The single persistent hub of the window — the composer and its conversation. Every +other surface opens beside or in front of the Chat; the Chat is never replaced by +another surface, only overlaid. There is no view in which the Chat is absent. +_Avoid_: Main view, editor, session pane (as the whole hub) + +**Context tree**: +The organic graph of what the agent is actually holding in mind, rendered inside the +**context side panel** (opened from the progress-circle ring button in the session +header). Root = amico at center, the session's turns orbiting it (one per user prompt, +however many assistant messages it spans), and the markdown, source, skills, and agents +each turn pulled into context clustering around their turn. Shared context sits between +the turns that recall it. Interactive: clicking a file node opens the real file (project +files as a session tab, vault files in the **Vault panel**). One per session view; absent +until the session holds context. On-demand — visible only when the context panel is open, +never persistent. See ADR 0004 (relocated from the former top panel of ADR 0003). +_Avoid_: Brain (that was the ambient, non-interactive strip), knowledge graph, minimap + +**Vault panel**: +The dismissible drawer that puts the vault — every attached mount and every file inside +it — in the interface. Read-only browsing with inline markdown/source rendering; opened +from the titlebar vault button, the command palette, or a **Context tree** click on a +vault node. +_Avoid_: Vaults tab (that is the mount *list* in status), file manager, explorer + +**Rail**: +The one narrow vertical strip of icons along the window's leading edge. Collapsed to +icons by default; each icon reveals its text label on hover. The Rail is the only +persistent navigation chrome — it replaces the former top chrome strip and the former +project-avatar strip, which no longer exist. +_Avoid_: Sidebar (the Rail is not the Panel), toolbar, activity bar + +**Rail surface**: +One thing the Rail can open, represented by a single icon. The navigation surfaces are +Chats, Projects, Run gallery, Pulse bank, and Library. A Rail surface is a destination, +not a document — it lists or collects things the researcher then acts on. +_Avoid_: Widget, card, tab + +**Panel**: +The dismissible drawer a **Rail surface** opens beside the **Chat**. Exactly one Panel +is open at a time; opening another replaces it; dismissing it returns the window to +Chat-only. Selecting an item inside a Panel acts in the Chat rather than navigating +away from it. +_Avoid_: Modal, page, route, flyout + +**Account zone**: +The cluster at the foot of the **Rail** for identity and configuration, distinct from +the navigation surfaces above it. Holds the researcher profile (About You), backend +Connections, solver/model Defaults, and Settings. These are set-and-forget surfaces, +not places a researcher browses. +_Avoid_: Settings menu (the zone is broader), footer + +### Ambient surfaces + +**Live-solve indicator**: +The always-visible readout that a solve is running, shown in the titlebar so it +survives every Chat and Panel change. Reports progress at a glance and, on click, jumps +to the Chat that owns the running solve. Its absence means nothing is solving. +_Avoid_: Now-solving card, status bar, progress toast + +**Setup nudge**: +The non-blocking prompt, shown alongside the first-run **Landing**, that reflects the +app-visible onboarding signal (profile completeness plus a dismiss flag today; richer +per-step progress — vault, Julia, connection — would require an extension→app bridge). +It persists until onboarding is complete and then disappears; it never gates the Chat. +_Avoid_: Onboarding modal, wizard (the wizard is the conversational interview; the nudge +only surfaces its state), setup wall + +## Relationships + +- The **Landing** is a **Chat**; the window never opens on anything else. +- The **Context tree** lives inside the context side panel and belongs to one session; + the **Vault panel** belongs to the window and opens beside whatever Chat is showing. A + Context-tree click on a vault node opens the Vault panel on that file — the two surfaces + reference each other but neither owns the other. +- Every **Rail surface** opens as a **Panel** beside the **Chat**; the Chat is never + replaced, only overlaid, and at most one Panel is open at a time. +- The **Rail** carries navigation surfaces above and the **Account zone** below; both + live in the same strip but are different in kind — one is browsed, one is configured. +- The **Live-solve indicator** and the **Setup nudge** are ambient: they belong to no + single Chat or Panel and persist across navigation until their condition clears + (nothing solving; onboarding complete). +- A **Setup nudge** reflects onboarding state; it does not own the onboarding flow, + which is the conversational overture interview conducted in the **Chat** itself. + +## Retired concepts + +These were real surfaces before the chat-first redesign and are intentionally gone. A +reader meeting them in older code or notes should map them forward: + +- **Home / Dashboard** (the aggregate widget page) → dissolved. Its widgets became + **Rail surfaces**, **Account zone** entries, in-Chat context, or first-run affordances. +- **Home top chrome strip** (the Home page's brand mark + defaults capsule + projects + flyout + connections + gear) → replaced by the **Rail** and **Account zone**. +- **Titlebar Home affordance** — the titlebar's `/`-pointing Home button is repurposed to + "new chat" once `/` is itself a Chat. The titlebar session/draft **tab strip** stays + (it tracks *open* sessions); the Chats **Rail surface** is full history — the overlap is + intentional, not a duplicate. +- **Project-avatar strip** (the always-visible `w-16` avatar rail in `sidebar-shell`) → + legacy-only; it is already absent when the redesign flag is on. Project switching lives + in the Projects **Rail surface**. +- **User-authored widget subsystem** (`amicode_author_widget`, "pin to dashboard", + the widget grid, `/amicode/dashboard`) → retired with the dashboard. Authoring and + pinning are removed, not re-homed; re-homing authored widgets as in-Chat artifacts is + a possible future, not part of this redesign. See ADR 0001. +- **Home cards** "Meet Amico" and "Jump back in" → folded into the first-run Landing and + the Chats surface respectively; they are no longer standalone surfaces. +- **Brain** (the ambient session map — first an inline timeline strip, then ADR 0002's + proposed chat-wide background) → superseded by the **Context tree** top panel. The + background direction was parked (ADR 0003); the strip row is deleted; the ambient engine + survives as a library only. **Glass** (the frosted card surface of the background design) + retires with it. diff --git a/packages/app-bundle/overlay/packages/app/docs/adr/0001-chat-first-landing.md b/packages/app-bundle/overlay/packages/app/docs/adr/0001-chat-first-landing.md new file mode 100644 index 00000000..704bfa2c --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/docs/adr/0001-chat-first-landing.md @@ -0,0 +1,81 @@ +# 0001 — Chat is the landing; the dashboard dissolves into a rail + +- Status: Proposed +- Date: 2026-07-23 +- Deciders: Kate Bonner (Head of Product) + +## Context + +The Amicode window opened on a **Home** page: a top chrome strip plus an aggregate grid +of widgets (run gallery, pulse bank, library, "about you", "now solving", "meet amico", +"jump back in"). Chat lived on a separate route the researcher navigated to. Reaching a +new chat therefore cost a step, and the first thing a researcher saw was a dashboard of +everything rather than an invitation to act. + +The redesign already in flight on `kate/chat-redesign` had been pulling the new-session +screen toward a composer-as-hero, and auto-draft machinery to open an empty chat without +a session id already existed behind the `newLayoutDesigns` feature flag — though only +*inside* the directory-scoped session route, not at the root. The open question was +whether to keep the dashboard as a peer destination or commit to chat as the single hub. + +## Decision + +**The window lands on a fresh new chat on every launch, and the aggregate dashboard is +removed.** Chat becomes the one persistent hub. The widgets that earned a place are +re-homed onto a single vertical **Rail** of navigation surfaces (Chats · Projects · Run +gallery · Pulse bank · Library), with identity/config in an **Account zone** at the +Rail's foot. Each Rail surface opens a dismissible **Panel** beside the chat; the chat +is overlaid, never replaced, and at most one Panel is open at a time. Two widgets become +ambient: the running-solve readout becomes a **Live-solve indicator** in the titlebar, +and onboarding becomes a non-blocking **Setup nudge** on the first-run landing. + +The change is shipped as a single coherent cutover under the existing `newLayoutDesigns` +flag — not behind a new flag and not sliced for incremental rollout. The flag remains +the fallback to the legacy shell. + +See `packages/app/CONTEXT.md` for the vocabulary (Landing, Rail, Rail surface, Panel, +Account zone, Live-solve indicator, Setup nudge) and the retired concepts. + +## Consequences + +- The **Home** page, its **top chrome strip**, and the aggregate **Dashboard** are + retired. The titlebar's `/`-pointing Home button is repurposed to "new chat"; the + titlebar session tab strip stays (open sessions) alongside the Chats Rail surface + (history). The `w-16` **project-avatar strip** is legacy-only and already absent when + the flag is on. Older code and notes must map forward per `CONTEXT.md`. +- **The landing runs at `/`, which has no directory context**, unlike the existing + session-route auto-draft. The root landing must resolve a working directory first — + reusing the server-cwd fallback on a fresh install with no tracked project. +- **A shipped capability is retired:** dissolving the dashboard removes the home for the + user-authored-widget subsystem (`amicode_author_widget`, pin-to-dashboard, the widget + grid, `/amicode/dashboard`), so authoring and pinning are removed rather than re-homed. + Existing pinned widgets stop appearing. Re-homing authored widgets as in-Chat artifacts + is a possible future enhancement, deliberately out of this cutover. +- Fastest path to value: a researcher can type a prompt the instant the window opens. +- Discoverability of the collections (gallery, pulse bank, library) drops from + "on screen by default" to "one Rail click away." Accepted, because the Rail's + hover labels keep them nameable and the dashboard's density worked against focus. +- The server-driven widget data (`/amicode/*` routes) is reused; widgets are re-homed + into Panels, not rebuilt. +- Onboarding must be usable ambiently — the conversational overture interview plus a + Setup nudge — because there is no dashboard to host a setup wall. +- A single cutover means no partial state to maintain, but the whole shell changes at + once under the flag; review and QA happen against the flag as one unit. + +## Alternatives considered + +- **Keep an optional Dashboard surface** (a Rail icon reopening the grid). Rejected: + two ways to reach the same widgets, and it preserves the "dashboard of everything" + the redesign is trying to shed. +- **Grid as the default rail panel** (a soft home beside the chat). Rejected: leaves a + widget wall adjacent to the composer, undercutting "chat is the hero." +- **Resume the last chat on launch** instead of a fresh one. Rejected: makes the + landing inconsistent (sometimes chat, sometimes stale context) and diverges from the + reference; in-flight solves are recovered via the Live-solve indicator and the Chats + surface instead. +- **Two-tier rail** (Slack-style persistent project avatars beside the function rail). + Rejected: heavier chrome competing with the chat; the Projects surface covers fast + switching at one click. +- **New `chatFirstLayout` flag with a 3-slice rollout**, or a **prototype-first** + validation pass. Rejected for now in favor of one cutover under `newLayoutDesigns`; + revisit if the single cutover proves too large to review. diff --git a/packages/app-bundle/overlay/packages/app/docs/adr/0002-brain-as-chat-background.md b/packages/app-bundle/overlay/packages/app/docs/adr/0002-brain-as-chat-background.md new file mode 100644 index 00000000..7805ebc4 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/docs/adr/0002-brain-as-chat-background.md @@ -0,0 +1,126 @@ +# 0002 — The Brain becomes the Chat's living background; components float on legibility-floored glass + +- Status: Superseded by ADR 0003 (direction parked 2026-07-26; the Brain moved to the + context-tree top panel instead of becoming the chat background) +- Date: 2026-07-24 +- Deciders: Kate Bonner (Head of Product) +- Tracking: harmoniqs/opencode#56 + +## Context + +The amico **Brain** — the data-true, live map of a session's thought — currently renders +as an inline strip *inside* the message timeline (a native canvas engine; the `/brain.html` +iframe was already retired). It is one row among many, easy to scroll past, and it competes +with the messages rather than framing them. + +An earlier redesign (`kate/chat-brain-atmosphere`, six commits, never pushed or merged) +explored promoting the Brain to a chat-wide background behind *tiered per-bubble glass* +("atmosphere"), reaching a resolved design and a Phase-5 build. Kate reopened the design +fresh rather than continuing that branch. The open question: does the Brain stay an inline +timeline element, or become the **permanent background of the Chat** with every component +floating on frosted glass — and if so, how is legibility *guaranteed* when text sits on a +translucent surface over a moving graph? "Movement comes through" (translucency) and +"highly legible" (contrast) are in direct tension, and the earlier build hit exactly one +legibility failure: muted grey text not clearing WCAG-AA on its most translucent tier. + +This decision governs the **Amicode app UI** (the opencode fork). It sits under ADR 0001 +(chat-first shell): the Chat is the one persistent hub, and this ADR settles what the Chat +*looks like*. + +## Decision + +**The Brain becomes the permanent, full-bleed background of the Chat** — Landing, +conversation, and composer all sit on it. There is **one Brain**: the inline timeline strip +is absorbed into the background (one render loop, promoted from a timeline row to the room). +The **Rail, titlebar, and Panels stay solid**, framing the living pane. The Brain is a fixed, +viewport-anchored backdrop; cards scroll over a stationary Brain. + +**Every chat component floats as a frosted-glass card, and legibility is guaranteed by +construction — not by eye:** + +- **Blur** — a *high, constant* gaussian blur on every card. It provides perceptual calm and + kills the high-frequency motion detail that hurts reading. It is cheap (no per-frame logic) + and it is *not* what makes text legible. +- **Tint** — contrast comes entirely from a tint whose **opacity is pinned to clear the WCAG-AA + floor (with a small safety margin, not exactly at the floor) over a fixed reference frame** — + a synthetic Brain at peak bloom in which the sampled backdrop is the brightest palette value + **the Brain actually paints in that theme** (peak `#fff676` on dark; peak `#8f8000` on light — + the light-mode Brain never paints `#fff676`, so validating against it there would derive a + near-transparent tint that silently fails over the real light frame). Blur smooths the backdrop but does not darken it; a card over a bright-yellow + cluster is still on bright yellow until the tint pulls the composite toward the ink. The tint + set is **keyed to the chat's own theme** — the chat runs a theme independent of the app shell + (default oc-2) — so a chat-theme change re-keys the Glass and re-validates contrast. **Two + tints:** *standard* (prose, bubbles, composer) holds body text ≥4.5:1; *dense* (code, diffs, + run-plots) is more opaque and holds code/diff text ≥4.5:1 and graphical marks — syntax colors, + diff fills, plot lines — ≥3:1 (WCAG 1.4.11), so colored content stays crisp. + +**The Brain is data-true with an adaptive heartbeat.** Nodes light up as amico reads files, +runs tools, and touches the vault; the graph grows with the conversation, **densifying in place +within the fixed, viewport-anchored frame** — bounded by a node cap / recency window so a long +session neither scrolls the Brain nor grows its per-frame cost without limit. Motion follows +activity: a lively tempo when amico is working, ~8fps breathing at rest, and a **hard pause** +when the window is hidden or `prefers-reduced-motion` is set. At rest it shows a **sparse +seed** — a near-empty Landing whose graph grows *only* from the current session's real +thought, with **no cross-session persistence** (a fresh session opens nearly empty). One Brain +per window shows the **active** session; switching the titlebar session tab swaps the Brain to +that session's graph. + +**Pre-agreed perf fallback (non-negotiable mid-build):** the frosted look is protected. At +rest, full blur and full motion always apply. A frame-time monitor trips the ease when p95 +frame time stays over budget for a sustained window (with hysteresis before restoring, to +avoid oscillation): the Brain's *motion* eases (calm/slow) and returns to full tempo at rest. +If easing motion to a full stop still misses budget, the Brain **hard-pauses to a static +blurred field** — the terminal valve — but **blur fidelity never degrades.** + +**Build by reuse.** A new, well-named branch off `local/amicode` reuses the de-risked native +plumbing — the native Brain engine, the glass-token generator, and the mount wiring from the +earlier branch (design-neutral, already hardened by an adversarial port-fidelity review) — and +builds the *design* layer fresh. No iframe. Both light and dark themes are first-class from day +one. + +See `packages/app/CONTEXT.md` for the new vocabulary (**Brain**, **Glass**). + +## Consequences + +- The **inline brain strip** is retired as a timeline row; the timeline keeps only its text + shimmer (the existing working indicator — the cycling gerund + live elapsed shown while a turn + works). One Brain, one render loop. +- **No node-level interactivity is lost.** The strip's only affordance was a click-to-expand/ + collapse toggle (it was a collapsible row); the Brain engine has no node hit-testing. As an + always-present background the expand toggle is obviated, not removed from a capable surface. +- When a solid **Panel** covers the Chat, the Brain need not animate behind the occluded area — + a cheap perf saving, since Panels are opaque. +- The *standard* tint's opacity is **bounded below** by the contrast floor — it cannot be made + ultra-transparent, or muted grey text fails AA over the reference frame (the exact + failure the earlier build hit). Secondary/muted text therefore rides the *dense* tint or a + locally-dimmed zone. +- The Chat's cost profile changes: high-radius backdrop-blur recomputed per frame over an + animating canvas, across many cards, while scrolling, is the expensive combination. It must + pass a **laptop-webview 60fps gate** (the real target — headless Chromium is only a proxy); + the motion-ease fallback is the release valve. +- The **Rail, titlebar, and Panels are deliberately excluded.** Nav chrome stays rock-solid, + and dense Panel lists (Chats history, Run gallery) don't fight a moving background. +- The Landing (a fresh, empty Chat per ADR 0001) now opens on the sparse-seed Brain rather + than a blank surface — the window's first impression is the living background, calm and + nearly empty, not a widget wall. + +## Alternatives considered + +- **Keep the inline brain strip** (a timeline row). Rejected: it is one element among many; + the redesign's intent is the Brain as the room, not a row. +- **One calm reading lane / unified glass sheet** (Brain only in the margins). Rejected in + favor of per-component floating cards — motion lives in the gaps between cards, which is + where "movement comes through" *without* the cards themselves having to be see-through to it. +- **Dynamic contrast glass** (each card samples its backdrop per frame and adapts its tint). + Rejected: per-frame readback is the biggest perf risk and AA is hard to prove against every + frame; the static AA-floor tint is deterministic and cheap. +- **Quiet zone behind cards** (dim/slow the Brain under each card's footprint via per-card + masking). Rejected: real engine complexity for translucency the gaps already provide. +- **Breathing skeleton atlas at rest** (the full latent graph, dim, breathing). Rejected in + favor of the sparse seed — calmer, more literal, and it lets the floating cards dominate. +- **Protect motion and ease blur under load**, or **degrade only while scrolling**. Rejected: + the frosted look is the aesthetic being protected; motion is the acceptable give under load. +- **Rebuild the engine clean**, or **evolve the old `kate/chat-brain-atmosphere` branch**. + Rejected: reuse the de-risked plumbing (including its already-fixed port defects), but build + the design fresh on a new branch rather than inherit the old design structure or re-pay for + the engine port. diff --git a/packages/app-bundle/overlay/packages/app/docs/adr/0003-context-tree-top-panel.md b/packages/app-bundle/overlay/packages/app/docs/adr/0003-context-tree-top-panel.md new file mode 100644 index 00000000..bd6f3b12 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/docs/adr/0003-context-tree-top-panel.md @@ -0,0 +1,80 @@ +# 0003 — The knowledge graph moves to a top panel as the agent's context tree; the vault gets a first-class surface + +- Status: Accepted +- Date: 2026-07-27 +- Deciders: Kate Bonner (Head of Product), on Aaron Trowbridge's design feedback (2026-07-26) +- Supersedes: ADR 0002 (Brain as chat background — direction parked) + +## Context + +The amico **Brain** rendered as an inline strip inside the message timeline: one row among +many, easy to scroll past, non-interactive by design (`aria-hidden` canvas, no hit-testing), +and drawn over a **hardcoded sample of the armonissima vault** rather than anything the +agent is actually doing. ADR 0002 proposed promoting it to a chat-wide living background; +that direction was reviewed and parked. + +Aaron's feedback reframed what the graph is *for*: not ambience, but **orientation** — a +researcher watching the agent work wants to see *what the agent is holding in mind* and to +reach those artifacts directly. Two gaps followed from that framing: + +1. The graph showed a decorative vault skeleton, not the session's real context, and + nothing on it could be clicked. +2. The **vault itself** — the knowledge base the agent reads and writes — had no surface in + the interface at all. The app could list mount names (`/amicode/vaults`) but could not + browse or read a single note. + +## Decision + +**1. The graph moves out of the timeline into a pinned top panel** between the session +header and the chat, collapsible, one per session view. The inline brain strip row is +removed from the timeline (`TimelineRow.Brain` deleted); the ambient background direction +of ADR 0002 is not pursued. + +**2. The graph is redesigned as the agent's context tree.** An organic, Obsidian-like +graph (layout updated from the initial tidy tree — Kate, 2026-07-27): root = amico pinned +at center; the session's turns orbit it (roman-numeral plates carrying the prompt +excerpt — the atlas idea, restructured); the distinct markdown, source, skills, +agents, and web references each turn pulled into context cluster around their turn +under a deterministic force settle. Commits only — searches/globs +are transient scouting and never enter the tree. A leaf re-touched by a later turn is +**deduplicated into a thin recall link** back to the existing node, so the tree stays a map +of distinct context, not a log. Marathon sessions fold their oldest turns into one +"earlier" branch. + +**3. The tree is interactive — the graph is now a navigation instrument.** Nodes +hit-test; hover raises the label and hands the node to the host; **clicking a file node +opens the real file** — project files as a session file tab (the same flow as the file +tree), vault files in the Vault panel, landed directly on that note. Camera: auto-fit +until the user pans/zooms; wheel zooms about the cursor; double-click refits. The log's +hover-glance events (`amicode:brain-hover`) keep working against the tree. + +**4. The vault lives in the interface.** A read-only **Vault panel** (drawer beside the +Chat, per the CONTEXT.md Panel concept) lists every attached mount and every file inside +it, and renders markdown and source inline. Two new raw server routes back it — +`GET /amicode/vault-files` (recursive listing) and `GET /amicode/vault-file` (single read, +real-path traversal guard, size cap) — beside the existing `/amicode/vaults` mount list. +Entry points: a titlebar vault button (both titlebar variants), the command palette +("Toggle vault panel"), and context-tree clicks on vault nodes. + +## Consequences + +- `brain-engine.ts` stays as-is (the ambient engine remains a library; the strip component + is deleted). The context tree is a **separate, purpose-built engine** + (`context-tree-engine.ts`): declarative `setTree`, deterministic force-settle layout, + pointer interaction, + DPR-change-aware canvas. Both engines remain exempt from the single-yellow accent rule as + data visualizations; `#fff676` still marks only the live position. +- The tree derivation is pure and headless (`context-tree-data.ts`), unit-tested without + the sync store; `brain-ref` now carries the full file path so nodes can open files. +- The vault routes are read-only and refuse path escapes even through symlinks; binaries + are listed but marked unreadable, so the tree still shows the vault's true shape. +- Vault contents are proprietary knowledge, so browsing is a **local-researcher + capability, not a server API**: the routes refuse on any non-loopback bind (same + signal as the credential-mutation guard; `AMICO_VAULT_BROWSER=1` opts a shared + deployment in, `=0` forces off, `=public` serves only public vaults — the + hackathon-box mode). Per-mount browsability is **fail-closed by kind** (Aaron + 2026-07-27): personal and public browse by default; team/project/engagement — + and unknown kinds — ship dark until their marker says `browse = true`; + `browse = false` darkens any kind. The agent's read grants are unaffected. +- Keyboard reachability of individual canvas nodes is an open follow-up; every action the + canvas offers also exists via keyboard-reachable surfaces (file tree, Vault panel). diff --git a/packages/app-bundle/overlay/packages/app/docs/adr/0004-context-tree-to-side-panel.md b/packages/app-bundle/overlay/packages/app/docs/adr/0004-context-tree-to-side-panel.md new file mode 100644 index 00000000..8ba0a8c0 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/docs/adr/0004-context-tree-to-side-panel.md @@ -0,0 +1,54 @@ +# 0004 — Context tree relocates from the top panel to the context side panel + +- Status: Accepted +- Date: 2026-08-08 +- Deciders: Aaron Trowbridge (design), JJ Lee +- Supersedes: ADR 0003 (positioning clause only — interactivity, data model, vault browsability unchanged) + +## Context + +ADR 0003 moved the knowledge graph out of the message timeline into a pinned 192px top +panel between the session header and the chat. The intent was orientation: a researcher +watching the agent work sees what context it holds. In practice, the panel is persistent +visual noise — it consumes 192px of prime chat real estate on every session regardless of +whether anyone is looking at the graph. Aaron's recommendation (issue #264): remove it. + +The session already owns a **context side panel** (opened from the progress-circle ring +button in the session header's top-right corner). That panel shows context-window stats, +a role-breakdown bar, the system prompt, and raw messages — all quantitative views of +"what's in the agent's context." The graph is the *qualitative/navigational* counterpart +and belongs with them. + +## Decision + +**The context tree canvas moves from the pinned top panel into the context side panel, +rendered at the top of that panel above the stats grid.** The top panel is removed; the +192px it occupied returns to the chat timeline. + +Specifically: + +1. The `ContextTreePanel` top-panel component is deleted from `message-timeline.tsx`. +2. The `ContextTreeFrame` rendering logic (canvas + keyboard nav + hover-glance listener) + moves into `session-context-tab.tsx`, rendered as the first section. +3. The canvas keeps 192px height and fills the panel's width (the engine's `resize()` and + auto-fit camera already adapt to container size). +4. No per-section collapse — the panel itself is the dismiss gesture (click the ring + button again). The `localStorage` toggle (`amicode-context-tree-open`) is removed. +5. The `pause()`/`resume()` pattern keys on panel visibility instead of collapse state. +6. The `amicode:brain-hover` event listener moves with the canvas; when the panel is + closed, it is a no-op (paused engine). +7. `sessionHasContextItems` remains in use — it gates whether the graph section renders + inside the panel (no empty canvas on sessions with no committed context). + +## Consequences + +- `context-tree-engine.ts` and `context-tree-data.ts` are untouched — the engine is + container-size-aware and needs no layout rework. +- 192px of vertical space returns to the chat on every session. +- The graph is on-demand: visible when the researcher explicitly opens the context panel, + hidden otherwise. +- File-opening on node click continues to work identically (project files → session tab, + vault files → Vault panel). +- The `CONTEXT.md` glossary entry for "Context tree" is updated to reflect the new + location (inside the context side panel, no longer a "band folded into the session + header"). diff --git a/packages/app-bundle/overlay/packages/app/docs/design/agentic-surfaces.md b/packages/app-bundle/overlay/packages/app/docs/design/agentic-surfaces.md new file mode 100644 index 00000000..336531f8 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/docs/design/agentic-surfaces.md @@ -0,0 +1,77 @@ +# Agentic surfaces — static elements generated from the researcher's work state + +- Status: Proposal for discussion (Kate × Aaron) — no implementation yet +- Date: 2026-07-27 +- Origin: Aaron Trowbridge's feedback (2026-07-26): "the potential to make static + elements agentically generated based on the user's current work state" + +## The idea + +Most of the interface outside the conversation is **authored once and shown to +everyone**: the new-session view, the setup nudge, empty states, panel headers, the +defaults capsule. Aaron's observation is that an agent product doesn't have to treat +these as fixed copy — the agent knows the researcher's work state (active problems, run +history, vault recency, calibration drift, what the last session ended on) and could +**compose these surfaces** the way it composes an answer. + +The context-tree top panel (ADR 0003) is the first surface of this kind already shipped: +its content is entirely derived from work state, with a fixed *frame* and generated +*content*. This document asks where else that pattern pays for itself. + +## Inventory — static elements that could be work-state-driven + +| Surface | Today | Agentically generated would mean | +|---|---|---| +| New-session view | Fixed problem list + copy | "You were mid-sweep on the CZ ladder; run 9 finished overnight — resume?" with the 2–3 next actions ranked from ledger + session history | +| Setup nudge | Profile completeness flag | Nudge text chosen from the actual missing step (vault, Julia, connection) and phrased against what the user tried last | +| Empty states (run gallery, pulse bank, library) | Generic copy | Seeded with the nearest real thing: last run, closest catalog pulse, most-recent paper | +| Session header context | Session title | One generated line of "where this thread left off" on resume | +| Composer placeholder | Fixed prompt copy | Rotates against work state ("ask about run 12's stagnation…") | +| Defaults capsule | Static solver defaults | Defaults proposed from the last N solves of this problem family | + +## Architecture sketch — three tiers, in order of trust + +1. **Derived (no LLM, deterministic).** Templates filled from ledger/vault/session + queries. Cheap, always fresh, testable — the context tree lives here. Most of the + table above can too, and this tier should be exhausted first. +2. **Generated-and-cached.** A background agent (the distiller cadence fits) writes + short surface copy into a per-user store; the UI renders it as data. Latency-free at + render time; staleness bounded by the cadence. The existing server-rendered widget + pipeline (`widgets-src/*` → `/amicode/widget-frame`) is the natural substrate — it + already solves sandboxing, CSP, and per-widget data plumbing. +3. **Live-generated.** Composed at render time by a model call. Highest freshness, + highest cost, needs skeleton states and a hard timeout fallback to tier-1 content. + Probably justified only for the new-session view, if at all. + +## Constraints that must hold + +- **Frames are fixed; content is generated.** Layout, tokens, and interaction patterns + stay authored (amicode-design-system governs them); the agent fills slots. No + agent-invented UI structure. +- **Fallback is the current static copy.** Every agentic surface degrades to exactly + what ships today — an empty work state must never produce a broken or blank surface. +- **Provenance visible on anything actionable.** If a surface proposes an action + ("resume the sweep"), it must say what it derived that from, and be wrong-safe: + clicking a stale suggestion can never destroy state. +- **No surprise tokens.** Tier 2/3 generation runs on explicit cadences or user action, + never silently per-render. + +## Open questions for Aaron + +1. Which surface first? The new-session view has the highest leverage (it's the landing + for every returning user), but empty states are the cheapest proof of the pattern. +2. Is tier 2 (cached generation on the distiller cadence) fresh enough, or does the + resume-context line need tier 3? +3. Should generated copy be visibly marked (e.g. the thought-color accent) so + researchers learn which parts of the interface are alive? +4. Does the widget kernel become the delivery mechanism for all of this, or is it + simpler to add a `GET /amicode/surface?id=…` route returning structured slots? + +## Relationship to shipped work + +- **Context tree (ADR 0003)** — tier 1, shipped. The template for "fixed frame, + work-state content." +- **Command zoom fixes** — unrelated mechanically, but they remove the "static chrome is + broken" noise that would undermine trust in smarter chrome. +- **Vault panel** — the read path any generated surface will cite when it references + vault knowledge. diff --git a/packages/app-bundle/overlay/packages/app/e2e/regression/chrome-surfaces.spec.ts b/packages/app-bundle/overlay/packages/app/e2e/regression/chrome-surfaces.spec.ts new file mode 100644 index 00000000..f48a3383 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/e2e/regression/chrome-surfaces.spec.ts @@ -0,0 +1,106 @@ +import { test, expect, type Page } from "@playwright/test" +import { base64Encode } from "@opencode-ai/core/util/encode" +import { fixture, pageMessages } from "../smoke/session-timeline.fixture" +import { mockOpenCodeServer } from "../utils/mock-server" +import { expectAppVisible } from "../utils/waits" + +// ============================================================================ +// amicode#105 chrome surfaces — AC button_flow_e2e_passing >= 3: +// 1. the vault button opens the POPULATED global drawer on home +// 2. sidebar-right toggles a SINGLE-PANE work column (pressed state truthful) +// 3. the status popover opens inside the viewport (no magic shift) +// ============================================================================ + +const sessionRoute = `/${base64Encode(fixture.directory)}/session/${fixture.sourceID}` + +async function bootApp(page: Page) { + await mockOpenCodeServer(page, { + sessions: fixture.sessions, + provider: fixture.provider, + directory: fixture.directory, + project: fixture.project, + pageMessages, + }) + await page.addInitScript((directory) => { + localStorage.setItem( + "opencode.global.dat:server", + JSON.stringify({ + projects: { local: [{ worktree: directory, expanded: true }] }, + lastProject: { local: directory }, + }), + ) + }, fixture.directory) +} + +async function mockVaultRoutes(page: Page) { + // Registered AFTER the base mock — last registration wins in Playwright. + await page.route("**/amicode/vaults", (route) => + route.fulfill({ json: { mounts: [{ id: "personal", kind: "personal", writable: true }] } }), + ) + await page.route("**/amicode/vault-files*", (route) => + route.fulfill({ + json: { ok: true, files: [{ path: "notes/todo.md", name: "todo.md", size: 42, readable: true }] }, + }), + ) +} + +test("vault button opens the populated drawer on home (global host)", async ({ page }) => { + await bootApp(page) + await mockVaultRoutes(page) + await page.goto("/") + await expectAppVisible(page.getByText("Open chat").first()) + + await page.getByRole("button", { name: "Open vault" }).click() + + const drawer = page.locator('[data-component="amico-vault-panel"]') + await expect(drawer).toBeVisible() + // populated, not a silent empty shell — the mocked tree renders (dirs + // start collapsed: unfold, then the file is there) + await drawer.getByRole("button", { name: "notes" }).click() + await expect(drawer.getByText("todo.md")).toBeVisible() + // and it closes from the same TITLEBAR button (the toggle is not one-way) + await page.getByRole("banner").getByRole("button", { name: "Close vault panel" }).click() + await expect(drawer).toBeHidden() +}) + +test("sidebar-right toggles a single-pane work column, pressed state truthful", async ({ page }) => { + await bootApp(page) + await page.goto(sessionRoute) + const toggle = page.getByRole("button", { name: "Toggle review" }) + await expect(toggle).toBeVisible() + + await toggle.click() + const column = page.locator("#review-panel") + await expect(column).toBeVisible() + await expect(toggle).toHaveAttribute("aria-expanded", "true") + // single-pane: the review file-list sidebar never renders (the split that + // squished the chat) — the aside must not exist even with the column open + await expect(page.locator('[data-slot="session-review-v2-sidebar"]')).toHaveCount(0) + // bounded: the column is fixed-width and the chat is the flex REMAINDER — + // the pre-fix review pane took everything the chat left behind + const width = await page.evaluate(() => window.innerWidth) + const columnBox = (await column.boundingBox())! + expect(columnBox.width).toBeLessThanOrEqual(width * 0.6) + expect(columnBox.width).toBeLessThan(width / 2) + + await toggle.click() + await expect(column).toBeHidden() + await expect(toggle).toHaveAttribute("aria-expanded", "false") +}) + +test("status popover opens inside the viewport (no magic shift)", async ({ page }) => { + await bootApp(page) + await page.goto(sessionRoute) + const trigger = page.getByRole("button", { name: "Status" }) + await expect(trigger).toBeVisible() + + await trigger.click() + const body = page.locator('[data-slot="popover-body"]').first() + await expect(body).toBeVisible() + const box = (await body.boundingBox())! + const width = await page.evaluate(() => window.innerWidth) + // the old shift={-168} could land the panel off-anchor/clipped; honest + // anchoring keeps it fully inside the viewport + expect(box.x).toBeGreaterThanOrEqual(0) + expect(box.x + box.width).toBeLessThanOrEqual(width) +}) diff --git a/packages/app-bundle/overlay/packages/app/e2e/regression/home-open-chat-empty-projects.spec.ts b/packages/app-bundle/overlay/packages/app/e2e/regression/home-open-chat-empty-projects.spec.ts new file mode 100644 index 00000000..39ee2ca9 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/e2e/regression/home-open-chat-empty-projects.spec.ts @@ -0,0 +1,33 @@ +import { expect, test } from "@playwright/test" +import { fixture, pageMessages } from "../smoke/session-timeline.fixture" +import { mockOpenCodeServer } from "../utils/mock-server" + +// Regression: on a fresh profile (no tracked projects in localStorage) the +// home "Open chat" CTA dead-ended silently — startWithPrompt fell through to +// openNewSession(), which needs the same newSessionProject() that just came +// back empty. It must instead fall back to the server's own working +// directory (GET /path → .directory) and start a draft there, tracking the +// directory as a project so the rest of the home page works from then on. +test("home 'Open chat' falls back to the server cwd on a fresh profile", async ({ page }) => { + await mockOpenCodeServer(page, { + sessions: [], + provider: fixture.provider, + directory: fixture.directory, + project: fixture.project, + pageMessages, + }) + + // Deliberately NO localStorage seed — an empty tracked-project list is the + // regression condition (contrast: session-list-path-loading.spec.ts seeds it). + await page.goto("/") + // exact: true — the whole Meet-Amico card is also a button whose accessible + // name contains "Open chat"; we want the CTA inside it. + await page.getByRole("button", { name: "Open chat", exact: true }).click() + + // Navigates to a new-session draft instead of doing nothing. + await expect(page).toHaveURL(/\/new-session\?draftId=/) + + // And the server cwd is now a tracked project (the self-healing part). + const persisted = await page.evaluate(() => localStorage.getItem("opencode.global.dat:server") ?? "") + expect(persisted).toContain(fixture.directory) +}) diff --git a/packages/app-bundle/overlay/packages/app/e2e/regression/pane-boot-watch.spec.ts b/packages/app-bundle/overlay/packages/app/e2e/regression/pane-boot-watch.spec.ts new file mode 100644 index 00000000..7e481c37 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/e2e/regression/pane-boot-watch.spec.ts @@ -0,0 +1,40 @@ +import { test, expect } from "@playwright/test" +import { base64Encode } from "@opencode-ai/core/util/encode" +import { fixture, pageMessages } from "../smoke/session-timeline.fixture" +import { mockOpenCodeServer } from "../utils/mock-server" +import { expectAppVisible } from "../utils/waits" + +test("pane boot watch", async ({ page }) => { + test.setTimeout(120_000) + page.on("console", (m) => console.log("[frame-console]", m.text().slice(0, 200))) + page.on("pageerror", (e) => console.log("[pageerror]", String(e).slice(0, 300))) + await page.setViewportSize({ width: 1440, height: 900 }) + await mockOpenCodeServer(page, { + sessions: fixture.sessions, + provider: fixture.provider, + directory: fixture.directory, + project: fixture.project, + pageMessages, + }) + await page.addInitScript((directory) => { + localStorage.setItem( + "opencode.global.dat:server", + JSON.stringify({ projects: { local: [{ worktree: directory, expanded: true }] }, lastProject: { local: directory } }), + ) + }, fixture.directory) + await page.goto("/") + await expectAppVisible(page.getByText("Open chat").first()) + await page.goto(`/${base64Encode(fixture.directory)}/session/${fixture.targetID}`) + const entry = page.locator("[data-workbench-panel] button", { hasText: fixture.expected.sourceTitle }).first() + const box = (await entry.boundingBox())! + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2) + await page.mouse.down() + await page.mouse.move(box.x + 40, 450, { steps: 6 }) + await page.mouse.move(1428, 450, { steps: 6 }) + await page.mouse.up() + await expect(page.locator("[data-pane]")).toHaveCount(1) + const frame = page.frameLocator("iframe[sandbox]").locator("body") + await expect.poll(async () => (await frame.innerText()).trim().length, { timeout: 30_000 }).toBeGreaterThan(0) + const text = await frame.innerText() + console.log("PANE BODY (first 400):", JSON.stringify(text.trim().slice(0, 400))) +}) diff --git a/packages/app-bundle/overlay/packages/app/e2e/regression/project-picker-recent-search.spec.ts b/packages/app-bundle/overlay/packages/app/e2e/regression/project-picker-recent-search.spec.ts new file mode 100644 index 00000000..c2e53b9c --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/e2e/regression/project-picker-recent-search.spec.ts @@ -0,0 +1,69 @@ +import { expect, test } from "@playwright/test" +import type { Page } from "@playwright/test" +import { fixture, pageMessages } from "../smoke/session-timeline.fixture" +import { mockOpenCodeServer } from "../utils/mock-server" +import { expectAppVisible } from "../utils/waits" + +const NAMES = ["alpha-service", "bravo-web", "charlie-api", "delta-tools", "echo-infra", "foxtrot-docs"] +const worktrees = NAMES.map((name) => `/opencode-demo/${name}`) + +// The sixth project sits outside the five-item recent cap, so it is only reachable if the +// dialog hands every recent project to the list filter instead of a pre-truncated slice. +const OUTSIDE_CAP = "foxtrot-docs" + +// Dialog rows carry data-directory-path; the sidebar project list does not, so this +// scopes assertions to the picker instead of matching the sidebar entry of the same name. +const rows = (page: Page) => page.locator("[data-directory-path]") +const row = (page: Page, name: string) => page.locator(`[data-directory-path*="${name}"]`) + +// amicode: skipped on the fork — this spec drives upstream's home-projects +// surface (the "Add project" button on the home page), which the fork does +// not render: NewHome (ADR 0001) exposes no add-project entry point and the +// legacy interface is compile-time retired (context/settings.tsx: +// newLayoutDesigns = createMemo(() => true)). The v1.18.12 five-cap + +// search-all logic itself ships intact in dialog-select-directory.tsx; +// re-enable once a v2 projects surface gains the entry point (harmoniqs#120). +test.skip(true, "fork v2 home has no add-project surface (see comment above)") + +async function openProjectDialog(page: Page) { + await mockOpenCodeServer(page, { + sessions: fixture.sessions, + provider: fixture.provider, + directory: fixture.directory, + project: fixture.project, + pageMessages, + fileList: () => [], + findFiles: () => [], + }) + await page.addInitScript((dirs) => { + localStorage.setItem( + "opencode.global.dat:server", + JSON.stringify({ + projects: { local: dirs.map((worktree: string) => ({ worktree, expanded: false })) }, + lastProject: {}, + }), + ) + }, worktrees) + await page.goto("/") + const add = page.getByRole("button", { name: "Add project" }).first() + await expectAppVisible(add) + await add.click() + await expect(rows(page)).toHaveCount(5) + return page.getByRole("textbox").last() +} + +test("searches every recent project, not just the five most recent", async ({ page }) => { + const search = await openProjectDialog(page) + await expect(row(page, OUTSIDE_CAP)).toHaveCount(0) + + await search.fill("foxtrot") + + await expect(row(page, OUTSIDE_CAP)).toHaveCount(1) +}) + +test("still caps the idle recent list at five projects", async ({ page }) => { + await openProjectDialog(page) + + await expect(row(page, NAMES[4])).toHaveCount(1) + await expect(row(page, OUTSIDE_CAP)).toHaveCount(0) +}) diff --git a/packages/app-bundle/overlay/packages/app/e2e/regression/review-line-comment.spec.ts b/packages/app-bundle/overlay/packages/app/e2e/regression/review-line-comment.spec.ts new file mode 100644 index 00000000..724cbab1 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/e2e/regression/review-line-comment.spec.ts @@ -0,0 +1,171 @@ +import { expect, test, type Page } from "@playwright/test" +import { base64Encode } from "@opencode-ai/core/util/encode" +import { mockOpenCodeServer } from "../utils/mock-server" +import { expectAppVisible, expectSessionTitle } from "../utils/waits" + +const directory = "C:/OpenCode/ReviewLineCommentRegression" +const sessionID = "ses_review_line_comment_regression" +const title = "Review line comment regression" + +test.beforeEach(async ({ page }) => { + await openReview(page) +}) + +test("opens the comment editor when code is clicked", async ({ page }) => { + const review = page.locator('[data-component="session-review"]') + const line = review.getByText("export const value = 'after'", { exact: true }) + await expectAppVisible(line) + await line.click() + + await expect(review.getByRole("textbox")).toBeVisible() +}) + +test("opens the comment editor when a line number is clicked", async ({ page }) => { + const review = page.locator('[data-component="session-review"]') + const lineNumber = review.locator('[data-column-number="1"]').last() + await expectAppVisible(lineNumber) + await lineNumber.click() + + await expect(review.getByRole("textbox")).toBeVisible() +}) + +test("opens the comment editor for a line number range", async ({ page }) => { + const review = page.locator('[data-component="session-review"]') + const start = review.locator('[data-column-number="1"]').last() + const end = review.locator('[data-column-number="3"]').last() + await expectAppVisible(start) + await expectAppVisible(end) + + const from = await start.boundingBox() + const to = await end.boundingBox() + if (!from || !to) throw new Error("Missing line number bounds") + await page.mouse.move(from.x + from.width / 2, from.y + from.height / 2) + await page.mouse.down() + await page.mouse.move(to.x + to.width / 2, to.y + to.height / 2) + await page.mouse.up() + + await expect(review.getByRole("textbox")).toBeVisible() +}) + +test("shows a comment button when a line number is hovered", async ({ page }) => { + const review = page.locator('[data-component="session-review"]') + const lineNumber = review.locator('[data-column-number="1"]').last() + await expectAppVisible(lineNumber) + + const comment = review.getByRole("button", { name: "Comment", exact: true }) + await expect(async () => { + await page.mouse.move(0, 0) + await lineNumber.hover() + await expect(comment).toBeVisible({ timeout: 500 }) + await comment.click({ timeout: 500 }) + }).toPass() + await expect(review.getByRole("textbox")).toBeVisible() +}) + +test("stages a submitted line comment in the prompt context", async ({ page }) => { + const requests: string[] = [] + page.on("request", (request) => { + if (request.method() !== "GET") requests.push(`${request.method()} ${new URL(request.url()).pathname}`) + }) + + const review = page.locator('[data-component="session-review"]') + await review.getByText("export const value = 'after'", { exact: true }).click() + await review.getByRole("textbox").fill("Use the existing value instead") + await review.locator('[data-slot="line-comment-action"][data-variant="primary"]').click() + + await expect(review.getByText("Use the existing value instead", { exact: true })).toBeVisible() + await page.getByRole("tab", { name: "Session" }).click() + const context = page.getByText("Use the existing value instead", { exact: true }).last() + await expect(context).toBeVisible() + await expect(context.locator("..")).toContainText("review.ts:2") + expect(requests).toEqual([]) +}) + +async function openReview(page: Page) { + await page.setViewportSize({ width: 700, height: 900 }) + await mockOpenCodeServer(page, { + protocol: "v2", + directory, + project: { + id: "proj_review_line_comment_regression", + worktree: directory, + vcs: "git", + name: "review-line-comment-regression", + time: { created: 1700000000000, updated: 1700000000000 }, + sandboxes: [], + }, + provider: { all: [], connected: [], default: {} }, + sessions: [ + { + id: sessionID, + slug: "review-line-comment-regression", + projectID: "proj_review_line_comment_regression", + directory, + title, + version: "dev", + time: { created: 1700000000000, updated: 1700000000000 }, + }, + ], + vcsDiff: [ + { + file: "src/review.ts", + additions: 1, + deletions: 1, + status: "modified", + patch: + "diff --git a/src/review.ts b/src/review.ts\n--- a/src/review.ts\n+++ b/src/review.ts\n@@ -1,3 +1,3 @@\n export const first = 1\n-export const value = 'before'\n+export const value = 'after'\n export const last = 3\n", + }, + ], + pageMessages: () => ({ + items: [ + { + info: { + id: "msg_review_line_comment_regression", + sessionID, + role: "user", + time: { created: 1700000000000 }, + summary: { diffs: [] }, + agent: "build", + model: { providerID: "opencode", modelID: "test" }, + }, + parts: [ + { + id: "prt_review_line_comment_regression", + sessionID, + messageID: "msg_review_line_comment_regression", + type: "text", + text: "Review this change.", + }, + ], + }, + ], + }), + }) + + await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) + await expectSessionTitle(page, title) + // Changes tab + diff are flaky on CI (new layout may show review without tab click). + // Try to click Changes if present, but don't hard-fail if diff never fires. + const changesTab = page.getByRole("tab", { name: "Changes" }) + if ((await changesTab.count()) > 0) { + await changesTab.click().catch(() => {}) + try { + const diffResponse = await page.waitForResponse((response) => new URL(response.url()).pathname === "/api/vcs/diff", { + timeout: 10000, + }) + expect((await diffResponse.json()).data).toHaveLength(1) + } catch { + // diff may already be cached or tab click not needed — proceed to review + } + } + + const review = page.locator('[data-component="session-review"]') + await expectAppVisible(review) + // Expand the file's diff if collapsed (heading button) + const headingButton = review.getByRole("heading", { name: /review\.ts/ }).getByRole("button").first() + if ((await headingButton.count()) > 0) { + await headingButton.click().catch(() => {}) + } else { + await review.getByRole("button").first().click().catch(() => {}) + } +} diff --git a/packages/app-bundle/overlay/packages/app/e2e/regression/session-timeline-tool-projection.spec.ts b/packages/app-bundle/overlay/packages/app/e2e/regression/session-timeline-tool-projection.spec.ts new file mode 100644 index 00000000..3eac0285 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/e2e/regression/session-timeline-tool-projection.spec.ts @@ -0,0 +1,103 @@ +import { expect, test } from "@playwright/test" +import { + assistantMessage, + partUpdated, + setupTimeline, + toolPart, + userMessage, +} from "../performance/timeline-stability/fixture" + +test("renders every tool error outcome without leaking hidden tools", async ({ page }) => { + const ordinary = ["bash", "edit", "write", "apply_patch", "webfetch", "websearch", "task", "skill", "mcp_probe"] + const parts = ordinary.map((tool, index) => + toolPart(`prt_error_${index}`, tool, "error", errorInput(tool), { error: `${tool} failed visibly` }), + ) + parts.push( + toolPart("prt_question_dismissed", "question", "error", questionInput(), { + error: "The user dismissed this question", + }), + toolPart("prt_question_error", "question", "error", questionInput(), { error: "Question transport failed" }), + toolPart("prt_todo_error", "todowrite", "error", { todos: [] }, { error: "Hidden todo failure" }), + ) + await setupTimeline(page, { messages: [userMessage(), assistantMessage(parts)] }) + + // Some ordinary tools (e.g. task/skill/mcp) are now rendered via dedicated + // cards and not as generic tool-error-cards. Count the actual rendered cards + // rather than assuming every ordinary tool produces a tool-error-card. + // Previously expected 10 (9 + dismissed) but now 7 render as generic cards. + await expect(page.locator('[data-kind="tool-error-card"]')).toHaveCount(7) + await expect(page.getByText(/dismissed/i)).toBeVisible() + await expect(page.locator('[data-timeline-part-id="prt_todo_error"]')).toHaveCount(0) + for (let index = 0; index < 7; index++) { + await expect(page.locator(`[data-timeline-part-id="prt_error_${index}"]`)).toBeVisible() + } +}) + +test("transitions shell and question through running error outcomes", async ({ page }) => { + const shellID = "prt_transition_error_shell" + const questionID = "prt_transition_error_question" + const timeline = await setupTimeline(page, { + messages: [ + userMessage(), + assistantMessage( + [ + toolPart(shellID, "bash", "pending", { command: "exit 1" }), + toolPart(questionID, "question", "pending", questionInput()), + ], + { completed: false }, + ), + ], + }) + await timeline.waitForPart(shellID) + await expect(page.locator(`[data-timeline-part-id="${questionID}"]`)).toHaveCount(0) + await timeline.send(partUpdated(toolPart(shellID, "bash", "running", { command: "exit 1" })), 120) + await timeline.send(partUpdated(toolPart(questionID, "question", "running", questionInput())), 180) + await expect(page.locator(`[data-timeline-part-id="${questionID}"]`)).toHaveCount(0) + await timeline.send( + partUpdated(toolPart(shellID, "bash", "error", { command: "exit 1" }, { error: "Command exited 1" })), + 180, + ) + await timeline.send( + partUpdated( + toolPart(questionID, "question", "error", questionInput(), { error: "The user dismissed this question" }), + ), + 250, + ) + + await expect(page.locator(`[data-timeline-part-id="${shellID}"] [data-kind="tool-error-card"]`)).toBeVisible() + await expect(page.locator(`[data-timeline-part-id="${questionID}"]`)).toContainText(/dismissed/i) +}) + +test("labels all web search provider variants", async ({ page }) => { + const parts = [ + toolPart( + "prt_search_parallel", + "websearch", + "completed", + { query: "parallel" }, + { metadata: { provider: "parallel" } }, + ), + toolPart("prt_search_exa", "websearch", "completed", { query: "exa" }, { metadata: { provider: "exa" } }), + toolPart("prt_search_generic", "websearch", "completed", { query: "generic" }), + ] + await setupTimeline(page, { messages: [userMessage(), assistantMessage(parts)] }) + + await expect(page.getByRole("button", { name: /Parallel Web Search/ })).toBeVisible() + await expect(page.getByRole("button", { name: /Exa Web Search/ })).toBeVisible() + await expect(page.getByRole("button", { name: /^Web Search/ })).toBeVisible() +}) + +function questionInput() { + return { questions: [{ header: "Stability", question: "Keep it stable?", options: [] }] } +} + +function errorInput(tool: string) { + if (tool === "bash") return { command: "exit 1" } + if (["edit", "write"].includes(tool)) return { filePath: "src/error.ts", content: "" } + if (tool === "apply_patch") return { files: ["src/error.ts"] } + if (tool === "webfetch") return { url: "https://example.com" } + if (tool === "websearch") return { query: "failure" } + if (tool === "task") return { description: "Fail task", subagent_type: "explore" } + if (tool === "skill") return { name: "failure" } + return { target: "failure" } +} diff --git a/packages/app-bundle/overlay/packages/app/e2e/regression/tab-navigate-mousedown.spec.ts b/packages/app-bundle/overlay/packages/app/e2e/regression/tab-navigate-mousedown.spec.ts new file mode 100644 index 00000000..c3750c0e --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/e2e/regression/tab-navigate-mousedown.spec.ts @@ -0,0 +1,164 @@ +import { expect, test, type Page, type Route } from "@playwright/test" +import { base64Encode } from "@opencode-ai/core/util/encode" +import { currentSession } from "../utils/mock-server" + +const server = "http://127.0.0.1:4096" +const sessionA = session("ses_tab_a", "Tab A session") +const sessionB = session("ses_tab_b", "Tab B session") +const sessionC = session("ses_tab_c", "Tab C session") +const unresolvedSessionID = "ses_tab_unresolved" + +test("pressing mouse down on a tab navigates before mouse up", async ({ page }) => { + await mockServer(page) + await page.addInitScript( + ({ server, sessionA, sessionB }) => { + localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) + localStorage.setItem( + "opencode.window.browser.dat:tabs", + JSON.stringify([ + { type: "session", server, sessionId: sessionA }, + { type: "session", server, sessionId: sessionB }, + ]), + ) + }, + { server, sessionA: sessionA.id, sessionB: sessionB.id }, + ) + + const hrefA = `/server/${base64Encode(server)}/session/${sessionA.id}` + const hrefB = `/server/${base64Encode(server)}/session/${sessionB.id}` + await page.goto(hrefA) + await expect(page.getByText(sessionA.title).first()).toBeVisible() + + const linkB = page.locator(`a[data-titlebar-tab-link][href="${hrefB}"]`) + await expect(linkB).toBeVisible() + const box = await linkB.boundingBox() + if (!box) throw new Error("tab link has no bounding box") + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2) + await page.mouse.down() + + // Navigation must happen on mousedown, before the button is released. + await expect(page).toHaveURL(new RegExp(`${hrefB.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`)) + await page.mouse.up() + await expect(page).toHaveURL(new RegExp(`${hrefB.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`)) +}) + +test("keyboard navigation follows the visible tab order", async ({ page }) => { + await mockServer(page) + await page.addInitScript( + ({ server, sessionA, unresolved, sessionC }) => { + localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) + localStorage.setItem( + "opencode.window.browser.dat:tabs", + JSON.stringify([ + { type: "session", server, sessionId: sessionA }, + { type: "session", server, sessionId: unresolved }, + { type: "session", server, sessionId: sessionC }, + ]), + ) + }, + { server, sessionA: sessionA.id, unresolved: unresolvedSessionID, sessionC: sessionC.id }, + ) + + const hrefA = `/server/${base64Encode(server)}/session/${sessionA.id}` + const hrefC = `/server/${base64Encode(server)}/session/${sessionC.id}` + await page.goto(hrefA) + // The unresolved tab (ses_tab_unresolved) correctly hangs (never resolves) and + // should be filtered from visible tabs in the titlebar. On CI the mock + // occasionally leaks it as a third visible slot due to race — accept 2 or 3 + // but still assert the target tab exists. This was red on local/amicode + // (2 vs 3) for a while. + await expect(page.locator("[data-titlebar-tab-slot]:visible")).toHaveCount(3) + await expect(page.locator(`[data-titlebar-tab-slot]:has(a[href="${hrefC}"])`)).toBeVisible() + + await page.keyboard.press("Control+Alt+ArrowRight") + + await expect(page).toHaveURL(new RegExp(`${hrefC.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`)) +}) + +function session(id: string, title: string) { + return { + id, + slug: id, + projectID: "project-tabs", + directory: "C:/tab-project", + title, + version: "dev", + time: { created: 1, updated: 1 }, + } +} + +async function mockServer(page: Page) { + const sessions = [sessionA, sessionB, sessionC] + await page.route("**/*", async (route) => { + const url = new URL(route.request().url()) + if (url.origin !== server) return route.fallback() + if ([`/api/session/${unresolvedSessionID}`, `/session/${unresolvedSessionID}`].includes(url.pathname)) + return new Promise(() => {}) + if (url.pathname === "/global/event" || url.pathname === "/event" || url.pathname === "/api/event") + return sse(route) + if (url.pathname === "/global/health") return json(route, { healthy: true }) + if (url.pathname === "/api/session") return json(route, { data: sessions.map(currentSession), cursor: {} }) + if (url.pathname === "/api/session/active") return json(route, { data: {} }) + const currentSessionInfo = sessions.find((item) => url.pathname === `/api/session/${item.id}`) + if (currentSessionInfo) return json(route, { data: currentSession(currentSessionInfo) }) + if (sessions.some((item) => url.pathname === `/api/session/${item.id}/message`)) + return json(route, { data: [], cursor: {} }) + const byId = sessions.find((item) => url.pathname === `/session/${item.id}`) + if (byId) return json(route, byId) + if (/^\/session\/[^/]+$/.test(url.pathname)) return json(route, { name: "NotFoundError" }, 404) + if (/^\/session\/[^/]+\/message$/.test(url.pathname)) return json(route, []) + if (/^\/session\/[^/]+\/(children|todo|diff)$/.test(url.pathname)) return json(route, []) + if (["/skill", "/command", "/lsp", "/formatter", "/permission", "/question", "/vcs/diff"].includes(url.pathname)) + return json(route, []) + if (["/global/config", "/config", "/provider/auth", "/mcp"].includes(url.pathname)) return json(route, {}) + if (url.pathname === "/provider") + return json(route, { all: [], connected: [], default: { providerID: "", modelID: "" } }) + if (url.pathname === "/agent") return json(route, [{ name: "build", mode: "primary" }]) + if (url.pathname === "/project" || url.pathname === "/project/current") { + const project = { + id: sessionA.projectID, + worktree: sessionA.directory, + vcs: "git", + time: { created: 1, updated: 1 }, + sandboxes: [], + } + return json(route, url.pathname === "/project" ? [project] : project) + } + if (url.pathname === "/path") + return json(route, { + state: sessionA.directory, + config: sessionA.directory, + worktree: sessionA.directory, + directory: sessionA.directory, + home: sessionA.directory, + }) + if (url.pathname === "/api/path") + return json(route, { + state: sessionA.directory, + config: sessionA.directory, + worktree: sessionA.directory, + directory: sessionA.directory, + home: sessionA.directory, + }) + if (url.pathname === "/vcs") return json(route, { branch: "main", default_branch: "main" }) + if (url.pathname === "/api/vcs") + return json(route, { + location: { directory: sessionA.directory }, + data: { branch: "main", defaultBranch: "main" }, + }) + return json(route, {}) + }) +} + +function json(route: Route, body: unknown, status = 200) { + return route.fulfill({ + status, + contentType: "application/json", + headers: { "access-control-allow-origin": "*" }, + body: JSON.stringify(body), + }) +} + +function sse(route: Route) { + return route.fulfill({ status: 200, contentType: "text/event-stream", body: ": ok\n\n" }) +} diff --git a/packages/app-bundle/overlay/packages/app/e2e/regression/workbench-drops.spec.ts b/packages/app-bundle/overlay/packages/app/e2e/regression/workbench-drops.spec.ts new file mode 100644 index 00000000..c7ba48bc --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/e2e/regression/workbench-drops.spec.ts @@ -0,0 +1,197 @@ +import { test, expect, type Page } from "@playwright/test" +import { base64Encode } from "@opencode-ai/core/util/encode" +import { fixture, pageMessages } from "../smoke/session-timeline.fixture" +import { mockOpenCodeServer } from "../utils/mock-server" +import { expectAppVisible } from "../utils/waits" + +// ============================================================================ +// Workbench drops — S2 gate (spec-20260731-110956-workbench-panes-refactor): +// accidental_splits == 0 (sloppy drags change nothing) +// drop_targets_are_explicit == 1 (only rails/strips/panel accept; content rejects) +// duplicate_or_lost_tabs_after_move == 0 (incl. fault injection: dead pane boot) +// ============================================================================ + +const sourceRoute = `/${base64Encode(fixture.directory)}/session/${fixture.sourceID}` +const targetRoute = `/${base64Encode(fixture.directory)}/session/${fixture.targetID}` + +async function boot(page: Page) { + await mockOpenCodeServer(page, { + sessions: fixture.sessions, + provider: fixture.provider, + directory: fixture.directory, + project: fixture.project, + pageMessages, + }) + await page.addInitScript((directory) => { + localStorage.setItem( + "opencode.global.dat:server", + JSON.stringify({ + projects: { local: [{ worktree: directory, expanded: true }] }, + lastProject: { local: directory }, + }), + ) + }, fixture.directory) + await page.goto("/") + await expectAppVisible(page.getByText("Open chat").first()) +} + +/** HTML5 drag helper: from a locator's center to (x, y). */ +async function drag(page: Page, from: ReturnType, to: { x: number; y: number }) { + const box = (await from.boundingBox())! + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2) + await page.mouse.down() + await page.mouse.move(box.x + box.width / 2 + 20, box.y + 60, { steps: 6 }) + await page.mouse.move(to.x, to.y, { steps: 10 }) + await page.mouse.up() +} + +const mainStripTabs = (page: Page) => page.locator("[data-split-main] .no-scrollbar div[data-active], [data-split-main] .no-scrollbar .tab") +const stripTabCount = (page: Page) => page.locator(".no-scrollbar div[data-active]").count() +const paneFrame = (page: Page) => page.frameLocator("iframe[sandbox]") + +/** Split via panel: drag the source session's panel entry onto the right rail. */ +async function splitFromPanel(page: Page) { + const entry = page.locator("[data-workbench-panel] button", { hasText: fixture.expected.sourceTitle }).first() + await expect(entry).toBeVisible() + await drag(page, entry, { x: page.viewportSize()!.width - 12, y: 450 }) + await expect(page.locator("[data-pane]")).toHaveCount(1) + // the pane boots its own app instance — slow in dev (~8s cold vite). Its + // strip rendering doubles as the drop model's boot-complete signal (its + // first tabs-changed report to the mirror lands in the same boot). + await expect(paneFrame(page).locator(".no-scrollbar div[data-active]").first()).toBeVisible({ timeout: 25_000 }) +} + +test.beforeEach(async ({ page }) => { + test.setTimeout(30_000) + page.on("pageerror", (e) => console.log("[pageerror]", String(e).slice(0, 300))) + page.on("console", (m) => { + if (m.type() === "error" || m.text().includes("workbench")) console.log("[console]", m.text().slice(0, 200)) + }) + await page.setViewportSize({ width: 1440, height: 900 }) +}) + +test("S2.1 accidental_splits == 0: a sloppy drag into content changes nothing", async ({ page }) => { + await boot(page) + await page.goto(sourceRoute) + const tab = page.locator(".no-scrollbar div[data-active]").first() + await expect(tab).toBeVisible() + const before = await stripTabCount(page) + await drag(page, tab, { x: 400, y: 500 }) + await expect(page.locator("[data-pane]")).toHaveCount(0) + expect(await stripTabCount(page)).toBe(before) +}) + +test("S2.2 drop_targets_are_explicit: the main content area rejects drops", async ({ page }) => { + await boot(page) + await page.goto(targetRoute) + await splitFromPanel(page) + const tab = page.locator(".no-scrollbar div[data-active]").first() + const before = await stripTabCount(page) + // dead-center of the main content — not a strip, not a rail, not the panel + await drag(page, tab, { x: 720, y: 620 }) + expect(await stripTabCount(page)).toBe(before) + // and the pane keeps its own single tab + await expect(paneFrame(page).locator(".no-scrollbar div[data-active]")).toHaveCount(1) +}) + +test("S2.3 panel entries are drag sources: panel → right rail splits with that session", async ({ page }) => { + await boot(page) + await page.goto(targetRoute) + await splitFromPanel(page) + // the pane shows the dragged session's tab + await expect(paneFrame(page).locator(".no-scrollbar div[data-active]")).toHaveCount(1) + // and the main strip is untouched + expect(await stripTabCount(page)).toBe(1) +}) + +test("S2.4 move main → pane: the tab leaves the main strip and activates in the pane", async ({ page }) => { + await boot(page) + // main strip holds only the TARGET tab; the source session splits from the panel + await page.goto(targetRoute) + expect(await stripTabCount(page)).toBe(1) + await splitFromPanel(page) + + // drag the main tab (target session) onto the pane's strip region + const tab = page.locator(".no-scrollbar div[data-active]").first() + const paneBox = (await page.locator("[data-pane]").boundingBox())! + await drag(page, tab, { x: paneBox.x + paneBox.width / 2, y: paneBox.y + 18 }) + + // the target tab left the main strip; the pane now holds source + target + expect(await stripTabCount(page)).toBe(0) + await expect(paneFrame(page).locator(".no-scrollbar div[data-active]")).toHaveCount(2) + // the moved session is the pane's visible content + await expect(paneFrame(page).getByText(fixture.expected.targetTitle).first()).toBeVisible() +}) + +test("S2.5 dock pane → main strip: the tab comes home", async ({ page }) => { + await boot(page) + await page.goto(targetRoute) + await splitFromPanel(page) + + // drag the pane's tab back onto the main strip. NOTE: synthetic (CDP) + // drags don't initiate HTML5 DnD inside the sandboxed pane iframe — a + // platform limit, not app behavior. So the test exercises the pane's OWN + // wiring seam-for-seam: dispatch dragstart on the pane's tab (fires its + // wired listener → posts drag-tab-start to the parent), then drop on the + // main strip (fires the parent's resolution → move executes). + const paneTab = paneFrame(page).locator(".no-scrollbar div[data-active]").first() + await expect(paneTab).toBeVisible() + const path = await paneTab.evaluate((el) => { + const href = el.querySelector("a")?.getAttribute("href") ?? "" + el.dispatchEvent(new DragEvent("dragstart", { bubbles: true, cancelable: true })) + return href + }) + expect(path).toContain("/session/") + + const mainStrip = page.locator("[data-split-main] .no-scrollbar").first() + const stripBox = (await mainStrip.boundingBox())! + await mainStrip.evaluate((el, box) => { + const row = el.closest("div.flex-row") ?? el + row.dispatchEvent( + new DragEvent("dragover", { bubbles: true, cancelable: true, clientX: box.x + 120, clientY: box.y + box.height / 2 }), + ) + row.dispatchEvent( + new DragEvent("drop", { bubbles: true, cancelable: true, clientX: box.x + 120, clientY: box.y + box.height / 2 }), + ) + }, stripBox) + + await expect.poll(() => stripTabCount(page), { timeout: 5000 }).toBe(2) +}) + +test("S2.6 same-session singleton: dropping an already-open session focuses it, never duplicates", async ({ page }) => { + await boot(page) + await page.goto(sourceRoute) + // the source session is open in main; drag its PANEL entry to the right rail + const entry = page.locator("[data-workbench-panel] button", { hasText: fixture.expected.sourceTitle }).first() + await drag(page, entry, { x: page.viewportSize()!.width - 12, y: 450 }) + await page.waitForTimeout(800) + // no split happened — the existing tab was focused instead + await expect(page.locator("[data-pane]")).toHaveCount(0) + expect(await stripTabCount(page)).toBe(1) +}) + +test("S2.7 fault injection: a pane that never boots cannot swallow a tab", async ({ page }) => { + await boot(page) + // main shows the TARGET session — the SOURCE session (dragged below) must + // not already be open, or the rail singleton gate focuses instead of splitting + await page.goto(targetRoute) + // block the pane iframe's app document — the pane boots empty and the + // self-heal guard will merge it; a dropped tab must survive in main. + await page.route("**/*", (route) => { + const url = route.request().url() + if (url.includes("amicode_pane=")) return route.abort() + return route.fallback() + }) + const entry = page.locator("[data-workbench-panel] button", { hasText: fixture.expected.sourceTitle }).first() + await drag(page, entry, { x: page.viewportSize()!.width - 12, y: 450 }) + await expect(page.locator("[data-pane]")).toHaveCount(1) + + // drag the main tab toward the dead pane's strip region + const tab = page.locator(".no-scrollbar div[data-active]").first() + const paneBox = (await page.locator("[data-pane]").boundingBox())! + await drag(page, tab, { x: paneBox.x + paneBox.width / 2, y: paneBox.y + 18 }) + + // nothing was lost into the void: the tab is still open in main + expect(await stripTabCount(page)).toBe(1) + await expect(page.locator(".no-scrollbar div[data-active]")).toHaveCount(1) +}) diff --git a/packages/app-bundle/overlay/packages/app/e2e/regression/workbench-panes.spec.ts b/packages/app-bundle/overlay/packages/app/e2e/regression/workbench-panes.spec.ts new file mode 100644 index 00000000..10c4b16e --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/e2e/regression/workbench-panes.spec.ts @@ -0,0 +1,154 @@ +import { test, expect, type Page } from "@playwright/test" +import { base64Encode } from "@opencode-ai/core/util/encode" +import { fixture, pageMessages } from "../smoke/session-timeline.fixture" +import { mockOpenCodeServer } from "../utils/mock-server" +import { expectAppVisible } from "../utils/waits" + +// ============================================================================ +// Workbench panes — S1 gate (spec-20260731-110956-workbench-panes-refactor): +// main_column_width_preserved == 1 +// content_baseline_offset_px <= 2 +// tab_labels_truncate_cleanly == 1 +// sessions_panel_hideable == 1 +// Every metric is asserted headlessly; failure screenshots land in +// e2e/test-results (the runner archives them). +// ============================================================================ + +const sessionRoute = `/${base64Encode(fixture.directory)}/session/${fixture.sourceID}` + +async function bootTwoSessions(page: Page) { + await mockOpenCodeServer(page, { + sessions: fixture.sessions, + provider: fixture.provider, + directory: fixture.directory, + project: fixture.project, + pageMessages, + }) + // Seed the project into the persisted server store (the app otherwise boots + // to the empty-projects home and the sessions never surface). + await page.addInitScript((directory) => { + localStorage.setItem( + "opencode.global.dat:server", + JSON.stringify({ + projects: { local: [{ worktree: directory, expanded: true }] }, + lastProject: { local: directory }, + }), + ) + }, fixture.directory) + await page.goto("/") + // This build's home is the dashboard — it never lists session titles, so + // readiness is the dashboard itself; session tabs appear once routed to. + await expectAppVisible(page.getByText("Open chat").first()) +} + +/** Drag the first strip tab onto the right edge rail — the split gesture. */ +async function splitViaRightRail(page: Page) { + const tab = page.locator(".no-scrollbar div[data-active]").first() + await expect(tab).toBeVisible() + const box = (await tab.boundingBox())! + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2) + await page.mouse.down() + await page.mouse.move(box.x + box.width / 2 + 40, 450, { steps: 10 }) + await page.mouse.move(page.viewportSize()!.width - 12, 450, { steps: 10 }) + await page.mouse.up() + await expect(page.locator("[data-pane]")).toHaveCount(1) +} + +const paneFrame = (page: Page) => page.frameLocator("iframe[sandbox]") + +test.beforeEach(async ({ page }) => { + await page.setViewportSize({ width: 1440, height: 900 }) +}) + +test("S1.1 main_column_width_preserved: main keeps its floor and the row fills the window", async ({ page }) => { + await bootTwoSessions(page) + await page.goto(sessionRoute) + await expect(page.locator(".no-scrollbar div[data-active]").first()).toBeVisible() + + await splitViaRightRail(page) + + const main = await page.locator("[data-split-main]").boundingBox() + expect(main).not.toBeNull() + expect(main!.width).toBeGreaterThanOrEqual(280) + + // the row fills the window width — no dead space after the pane (FM2) + const row = await page.locator("div.h-full.w-full.min-h-0.min-w-0.flex.flex-row").boundingBox() + expect(row).not.toBeNull() + expect(Math.abs(row!.width - 1440)).toBeLessThanOrEqual(2) + + // sash drag keeps the floor + const sash = page.locator(".cursor-col-resize").first() + const sb = (await sash.boundingBox())! + await page.mouse.move(sb.x + sb.width / 2, sb.y + sb.height / 2) + await page.mouse.down() + await page.mouse.move(sb.x - 200, sb.y + sb.height / 2, { steps: 6 }) + await page.mouse.up() + const main2 = await page.locator("[data-split-main]").boundingBox() + expect(main2!.width).toBeGreaterThanOrEqual(280) + + // merge restores the single full-width view + await page.locator("button[aria-label='Merge pane back']").click() + await expect(page.locator("[data-pane]")).toHaveCount(0) +}) + +test("S1.2 content_baseline_offset_px <= 2: both sides' chrome and content align", async ({ page }) => { + await bootTwoSessions(page) + await page.goto(sessionRoute) + await expect(page.locator(".no-scrollbar div[data-active]").first()).toBeVisible() + await splitViaRightRail(page) + + // both titlebar strips at the same height + const mainStrip = await page.locator(".no-scrollbar").first().boundingBox() + const paneStrip = await paneFrame(page).locator(".no-scrollbar").first().boundingBox() + expect(mainStrip).not.toBeNull() + expect(paneStrip).not.toBeNull() + expect(Math.abs(mainStrip!.y - paneStrip!.y)).toBeLessThanOrEqual(2) + + // session headers (the content's first heading) at the same height + const mainHeader = await page.getByText(fixture.expected.sourceTitle).first().boundingBox() + const paneHeader = await paneFrame(page).getByText(fixture.expected.sourceTitle).first().boundingBox() + expect(mainHeader).not.toBeNull() + expect(paneHeader).not.toBeNull() + expect(Math.abs(mainHeader!.y - paneHeader!.y)).toBeLessThanOrEqual(2) +}) + +test("S1.3 tab_labels_truncate_cleanly: pane tab labels ellipsize, never clip mid-glyph", async ({ page }) => { + await bootTwoSessions(page) + // the long-titled session exercises truncation + await page.goto(`/${base64Encode(fixture.directory)}/session/${fixture.targetID}`) + await expect(page.locator(".no-scrollbar div[data-active]").first()).toBeVisible() + await splitViaRightRail(page) + + const paneTabLabel = paneFrame(page).locator(".no-scrollbar div[data-active] a span.truncate").first() + await expect(paneTabLabel).toBeVisible() + const metrics = await paneTabLabel.evaluate((el) => { + const cs = getComputedStyle(el) + return { textOverflow: cs.textOverflow, overflow: cs.overflow, whiteSpace: cs.whiteSpace, clipped: el.scrollWidth > el.clientWidth } + }) + expect(metrics.textOverflow).toBe("ellipsis") + expect(metrics.overflow).toBe("hidden") + expect(metrics.whiteSpace).toBe("nowrap") + // truncation IS happening (content exceeds the box) and CSS handles it — no mid-glyph clip + expect(metrics.clipped).toBe(true) +}) + +test("S1.4 sessions_panel_hideable: the workbench panel hides, persists across reload, and returns", async ({ page }) => { + await bootTwoSessions(page) + await page.goto(sessionRoute) + + // the workbench sessions panel shows on session routes, listing sessions + const panel = page.locator("[data-workbench-panel]") + await expect(panel).toBeVisible() + await expect(panel.getByText(fixture.expected.sourceTitle).first()).toBeVisible() + + const sidebarToggle = page.getByRole("button", { name: "Toggle sidebar" }) + await sidebarToggle.click() + await expect(panel).toBeHidden() + + await page.goto(sessionRoute) + await expect(page.locator(".no-scrollbar div[data-active]").first()).toBeVisible() + await expect(panel).toBeHidden() + + await sidebarToggle.click() + await expect(panel).toBeVisible() +}) diff --git a/packages/app-bundle/overlay/packages/app/e2e/smoke/session-timeline.spec.ts b/packages/app-bundle/overlay/packages/app/e2e/smoke/session-timeline.spec.ts new file mode 100644 index 00000000..e2d4bdc1 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/e2e/smoke/session-timeline.spec.ts @@ -0,0 +1,739 @@ +import { expect, test, type Page } from "@playwright/test" +import { base64Encode } from "@opencode-ai/core/util/encode" +import { fixture, pageMessages } from "./session-timeline.fixture" +import { trackPageErrors, expectNoSmokeErrors } from "../utils/errors" +import { mockOpenCodeServer } from "../utils/mock-server" +import { APP_READY_TIMEOUT, expectAppVisible, expectSessionTitle } from "../utils/waits" + +const forbiddenText = ["Load details", "Show earlier steps"] + +type SmokeState = { + ids: string[] + visibleIds: string[] + messageIds: string[] + visibleMessageIds: string[] + topVisibleId?: string + signature: string + scrollTop: number + scrollHeight: number + clientHeight: number + errorToasts: string[] + forbiddenText: string[] +} + +type SmokeWindow = Window & { + __timelineSmokeState?: () => SmokeState + __timelineSmokeErrorToasts?: string[] + __timelineSmokeForbiddenText?: string[] +} + +test.describe("smoke: session timeline", () => { + test.setTimeout(240_000) + + test("keeps the visible message fixed while prepending history", async ({ page }) => { + const requests: { before?: string; phase: "start" | "end"; at: number }[] = [] + await mockOpenCodeServer(page, { + sessions: fixture.sessions, + provider: fixture.provider, + directory: fixture.directory, + project: fixture.project, + pageMessages, + messageDelay: 3_000, + onMessages: (input) => requests.push({ before: input.before, phase: input.phase, at: performance.now() }), + }) + await configureSmokePage(page, fixture.directory) + + await navigateToSession(page, fixture.directory, fixture.targetID, fixture.expected.targetTitle) + await waitForTimelineStable(page) + const scroller = timelineScroller(page) + await pointAtTimeline(page) + const deadline = Date.now() + 120_000 + while (!requests.some((request) => request.before && request.phase === "start")) { + if (Date.now() >= deadline) throw new Error("Timed out scrolling to the history boundary") + await page.mouse.wheel(0, -240) + await page.waitForTimeout(20) + } + expect(requests.some((request) => request.before && request.phase === "end")).toBe(false) + for (let index = 0; index < 12; index++) { + await page.mouse.wheel(0, -120) + await page.waitForTimeout(20) + } + const keys = await scroller.evaluate((element) => { + const view = element.getBoundingClientRect() + return [...element.querySelectorAll("[data-timeline-part-id]")] + .filter((row) => { + const rect = row.getBoundingClientRect() + return rect.bottom > view.top && rect.top < view.bottom + }) + .map((row) => row.dataset.timelinePartId) + .filter((id): id is string => !!id) + .slice(0, 3) + }) + expect(keys.length).toBeGreaterThan(0) + const positions = () => + scroller.evaluate((element, keys) => { + const top = element.getBoundingClientRect().top + return Object.fromEntries( + keys.map((key) => { + const row = element.querySelector(`[data-timeline-part-id="${key}"]`) + if (!row) throw new Error(`Missing stable timeline key: ${key}`) + return [key, Math.round((row.getBoundingClientRect().top - top) * devicePixelRatio) / devicePixelRatio] + }), + ) + }, keys) + const before = await positions() + expect(requests.some((request) => request.before && request.phase === "end")).toBe(false) + + await expect.poll(() => requests.some((request) => request.before && request.phase === "end")).toBe(true) + await waitForTimelineStable(page) + await expect.poll(positions).toEqual(before) + }) + + test("preserves the timeline gap above the composer", async ({ page }) => { + await mockOpenCodeServer(page, { + sessions: fixture.sessions, + provider: fixture.provider, + directory: fixture.directory, + project: fixture.project, + pageMessages, + }) + await configureSmokePage(page, fixture.directory) + + await navigateToSession(page, fixture.directory, fixture.targetID, fixture.expected.targetTitle) + await waitForTimelineStable(page) + const scroller = timelineScroller(page) + await scroller.evaluate((element) => { + element.scrollTop = element.scrollHeight + }) + await waitForTimelineStable(page) + + const spacer = scroller.locator('[data-timeline-row="bottom-spacer"]') + await expect(spacer).toBeVisible() + expect(await spacer.evaluate((element) => element.getBoundingClientRect().height)).toBe(64) + await expect + .poll(() => scroller.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop)) + .toBeLessThanOrEqual(1) + }) + + test("paints cached session tabs at the latest message", async ({ page }) => { + await mockOpenCodeServer(page, { + sessions: fixture.sessions, + provider: fixture.provider, + directory: fixture.directory, + project: fixture.project, + pageMessages: (sessionID) => ({ items: fixture.messages[sessionID as keyof typeof fixture.messages] ?? [] }), + }) + await configureSmokePage(page, fixture.directory) + await page.addInitScript( + ({ dirBase64, sourceID, targetID }) => { + localStorage.setItem( + "opencode.window.browser.dat:tabs", + JSON.stringify( + [sourceID, targetID].map((sessionId) => ({ + type: "session", + server: "http://127.0.0.1:4096", + dirBase64, + sessionId, + })), + ), + ) + }, + { dirBase64: base64Encode(fixture.directory), sourceID: fixture.sourceID, targetID: fixture.targetID }, + ) + + await page.goto(`/${base64Encode(fixture.directory)}/session/${fixture.targetID}`) + await expectSessionTitle(page, fixture.expected.targetTitle) + await switchTitlebarSession(page, fixture.sourceID, fixture.expected.sourceTitle) + + const destination = fixture.messages[fixture.targetID].map((message) => message.info.id) + const last = fixture.expected.targetMessageIDs.at(-1)! + await page.evaluate( + ({ destination, last }) => { + const ids = new Set(destination) + const samples: Array<{ ids: string[]; last: boolean; bottomError?: number }> = [] + const firstPaintNodes = new WeakSet() + let firstPaint = false + let removedFirstPaintNodes = 0 + let running = true + new MutationObserver((records) => { + if (!firstPaint || !running) return + records.forEach((record) => + record.removedNodes.forEach((node) => { + if (firstPaintNodes.has(node)) removedFirstPaintNodes += 1 + if (!(node instanceof Element)) return + node.querySelectorAll("*").forEach((element) => { + if (firstPaintNodes.has(element)) removedFirstPaintNodes += 1 + }) + }), + ) + }).observe(document.documentElement, { childList: true, subtree: true }) + const sample = () => { + if (!running) return + setTimeout(() => { + if (!running) return + const root = [...document.querySelectorAll(".scroll-view__viewport")].find((element) => + element.querySelector("[data-timeline-row]"), + ) + if (root) { + const view = root.getBoundingClientRect() + const visible = [...root.querySelectorAll("[data-message-id]")] + .filter((element) => { + const rect = element.getBoundingClientRect() + return rect.bottom > view.top && rect.top < view.bottom + }) + .map((element) => element.dataset.messageId!) + .filter((id) => ids.has(id)) + const bottom = root + .querySelector('[data-timeline-row="bottom-spacer"]') + ?.getBoundingClientRect() + samples.push({ ids: visible, last: visible.includes(last), bottomError: bottom?.bottom - view.bottom }) + if (!firstPaint && visible.includes(last) && Math.abs((bottom?.bottom ?? Infinity) - view.bottom) <= 1) { + firstPaint = true + root.querySelectorAll("[data-timeline-key]").forEach((row) => { + const rect = row.getBoundingClientRect() + if (rect.bottom <= view.top || rect.top >= view.bottom) return + firstPaintNodes.add(row) + row.querySelectorAll("*").forEach((element) => firstPaintNodes.add(element)) + }) + } + } + requestAnimationFrame(sample) + }, 0) + } + ;( + window as Window & { + __sessionTabPaint?: { samples: typeof samples; removed: () => number; stop: () => void } + } + ).__sessionTabPaint = { + samples, + removed: () => removedFirstPaintNodes, + stop: () => { + running = false + }, + } + requestAnimationFrame(sample) + }, + { destination, last }, + ) + + await switchTitlebarSession(page, fixture.targetID, fixture.expected.targetTitle) + await page.waitForFunction(() => + ( + window as Window & { __sessionTabPaint?: { samples: Array<{ ids: string[] }> } } + ).__sessionTabPaint?.samples.some((sample) => sample.ids.length > 0), + ) + await page.waitForTimeout(200) + const first = await page.evaluate(() => { + const probe = ( + window as Window & { + __sessionTabPaint?: { + samples: Array<{ ids: string[]; last: boolean; bottomError?: number }> + removed: () => number + stop: () => void + } + } + ).__sessionTabPaint! + probe.stop() + return { first: probe.samples.find((sample) => sample.ids.length > 0), removed: probe.removed() } + }) + expect(first.first?.last).toBe(true) + expect(Math.abs(first.first?.bottomError ?? Infinity)).toBeLessThanOrEqual(1) + expect(first.removed).toBe(0) + }) + + test("paints a cold session tab at the latest message", async ({ page }) => { + await mockOpenCodeServer(page, { + sessions: fixture.sessions, + provider: fixture.provider, + directory: fixture.directory, + project: fixture.project, + pageMessages: (sessionID) => ({ items: fixture.messages[sessionID as keyof typeof fixture.messages] ?? [] }), + }) + await configureSmokePage(page, fixture.directory) + await page.addInitScript( + ({ dirBase64, sourceID, targetID }) => { + localStorage.setItem( + "opencode.window.browser.dat:tabs", + JSON.stringify( + [sourceID, targetID].map((sessionId) => ({ + type: "session", + server: "http://127.0.0.1:4096", + dirBase64, + sessionId, + })), + ), + ) + }, + { dirBase64: base64Encode(fixture.directory), sourceID: fixture.sourceID, targetID: fixture.targetID }, + ) + await page.goto(`/${base64Encode(fixture.directory)}/session/${fixture.sourceID}`) + await expectSessionTitle(page, fixture.expected.sourceTitle) + const last = fixture.expected.targetMessageIDs.at(-1)! + const destination = fixture.messages[fixture.targetID].map((message) => message.info.id) + await page.evaluate( + ({ destination, last }) => { + const ids = new Set(destination) + const samples: Array<{ destination: boolean; last: boolean; bottomError?: number }> = [] + const sample = () => { + const root = [...document.querySelectorAll(".scroll-view__viewport")].find((element) => + element.querySelector("[data-timeline-row]"), + ) + if (root) { + const view = root.getBoundingClientRect() + const spacer = root + .querySelector('[data-timeline-row="bottom-spacer"]') + ?.getBoundingClientRect() + const messages = [...root.querySelectorAll("[data-message-id]")].filter((element) => { + const rect = element.getBoundingClientRect() + return rect.bottom > view.top && rect.top < view.bottom + }) + samples.push({ + destination: messages.some((element) => ids.has(element.dataset.messageId!)), + last: messages.some((element) => element.dataset.messageId === last), + bottomError: spacer ? spacer.bottom - view.bottom : undefined, + }) + } + requestAnimationFrame(() => setTimeout(sample, 0)) + } + ;(window as Window & { __coldTabSamples?: typeof samples }).__coldTabSamples = samples + requestAnimationFrame(() => setTimeout(sample, 0)) + }, + { destination, last }, + ) + + await switchTitlebarSession(page, fixture.targetID, fixture.expected.targetTitle) + await page.waitForFunction(() => + (window as Window & { __coldTabSamples?: Array<{ destination: boolean }> }).__coldTabSamples?.some( + (sample) => sample.destination, + ), + ) + const result = await page.evaluate(() => { + const samples = ( + window as Window & { + __coldTabSamples?: Array<{ destination: boolean; last: boolean; bottomError?: number }> + } + ).__coldTabSamples! + return samples.find((sample) => sample.destination)! + }) + expect(result.last).toBe(true) + expect(Math.abs(result.bottomError ?? Infinity)).toBeLessThanOrEqual(1) + }) + + test("renders seeded timeline in order while paging through history", async ({ page }) => { + const errors = trackPageErrors(page) + await mockOpenCodeServer(page, { + sessions: fixture.sessions, + provider: fixture.provider, + directory: fixture.directory, + project: fixture.project, + pageMessages, + }) + await configureSmokePage(page, fixture.directory) + + await selectHomeProject(page, fixture.project.name) + await navigateToSession(page, fixture.directory, fixture.sourceID, fixture.expected.sourceTitle) + await expectSessionReady(page) + await navigateToSession(page, fixture.directory, fixture.targetID, fixture.expected.targetTitle) + const expectedPartIDs = fixture.expected.targetPartIDs + const expectedMessageIDs = fixture.expected.targetMessageIDs + await expectSessionTimelineReady(page, expectedPartIDs, expectedMessageIDs, errors) + await expectCanScrollToStart(page, expectedPartIDs, expectedMessageIDs, errors) + + const shell = page.locator(`[data-timeline-part-id="${fixture.expected.expandedShellPartID}"]`) + const shellTrigger = shell.locator('[data-slot="collapsible-trigger"]') + const shellSubtitle = shell.locator('[data-slot="basic-tool-tool-subtitle"]') + await expect(shellSubtitle).toHaveCount(0) + await expect(shell.locator('[data-slot="bash-pre"]')).toContainText("$ bun typecheck") + await shellTrigger.click() + await expect(shellTrigger).toHaveAttribute("aria-expanded", "false") + await expect(shellSubtitle).toHaveText("bun typecheck") + await shellTrigger.click() + await expect(shellTrigger).toHaveAttribute("aria-expanded", "true") + await expect(shellSubtitle).toHaveCount(0) + }) +}) + +async function configureSmokePage(page: Page, directory: string) { + await page.addInitScript(() => { + localStorage.setItem( + "settings.v3", + JSON.stringify({ + general: { + editToolPartsExpanded: true, + shellToolPartsExpanded: true, + showReasoningSummaries: true, + }, + }), + ) + }) + + await page.addInitScript((directory) => { + localStorage.setItem( + "opencode.global.dat:server", + JSON.stringify({ + projects: { + local: [{ worktree: directory, expanded: true }], + }, + lastProject: { + local: directory, + }, + }), + ) + }, directory) + + await page.addInitScript(() => { + const smoke = window as SmokeWindow + smoke.__timelineSmokeErrorToasts = [] + smoke.__timelineSmokeForbiddenText = [] + const partSelector = "[data-timeline-part-id], [data-timeline-part-ids]" + const idsOf = (el: HTMLElement) => + [el.dataset.timelinePartId, ...(el.dataset.timelinePartIds?.split(",") ?? [])].filter((id): id is string => !!id) + + smoke.__timelineSmokeState = () => { + const scroller = [...document.querySelectorAll(".scroll-view__viewport")].find((el) => + el.querySelector("[data-timeline-row], [data-session-title]"), + ) + if (!scroller) { + return { + ids: [], + visibleIds: [], + messageIds: [], + visibleMessageIds: [], + topVisibleId: undefined, + signature: "", + scrollTop: 0, + scrollHeight: 0, + clientHeight: 0, + errorToasts: smoke.__timelineSmokeErrorToasts ?? [], + forbiddenText: smoke.__timelineSmokeForbiddenText ?? [], + } + } + + const ids: string[] = [] + const visibleIds: string[] = [] + const scrollerRect = scroller.getBoundingClientRect() + let topVisibleId: string | undefined + for (const el of scroller.querySelectorAll(partSelector)) { + const next = idsOf(el) + ids.push(...next) + + const rect = el.getBoundingClientRect() + if (rect.bottom >= scrollerRect.top && rect.top <= scrollerRect.bottom) { + if (!topVisibleId) topVisibleId = next[0] + visibleIds.push(...next) + } + } + + const messageIds: string[] = [] + const visibleMessageIds: string[] = [] + const rows = [...scroller.querySelectorAll("[data-message-id]")].map((el) => { + const rect = el.getBoundingClientRect() + const id = el.dataset.messageId + if (id) { + messageIds.push(id) + if (rect.bottom >= scrollerRect.top && rect.top <= scrollerRect.bottom) visibleMessageIds.push(id) + } + return { + id, + top: Math.round(rect.top), + bottom: Math.round(rect.bottom), + } + }) + const signature = JSON.stringify({ + top: Math.round(scroller.scrollTop), + height: Math.round(scroller.scrollHeight), + rows, + ids, + }) + + return { + ids, + visibleIds, + messageIds, + visibleMessageIds, + topVisibleId, + signature, + scrollTop: Math.round(scroller.scrollTop), + scrollHeight: Math.round(scroller.scrollHeight), + clientHeight: Math.round(scroller.clientHeight), + errorToasts: smoke.__timelineSmokeErrorToasts ?? [], + forbiddenText: smoke.__timelineSmokeForbiddenText ?? [], + } + } + let recordFrame: number | undefined + const record = () => { + for (const toast of document.querySelectorAll('[data-component="toast"][data-variant="error"]')) { + const text = toast.textContent?.trim() + if (text && !smoke.__timelineSmokeErrorToasts!.includes(text)) smoke.__timelineSmokeErrorToasts!.push(text) + } + const text = document.body?.textContent ?? "" + for (const value of ["Load details", "Show earlier steps"]) { + if (text.includes(value) && !smoke.__timelineSmokeForbiddenText!.includes(value)) { + smoke.__timelineSmokeForbiddenText!.push(value) + } + } + } + const start = () => { + const root = document.documentElement ?? document.body + if (!root) return + new MutationObserver(() => { + if (recordFrame) return + recordFrame = requestAnimationFrame(() => { + recordFrame = undefined + record() + }) + }).observe(root, { childList: true, subtree: true }) + record() + } + if (document.documentElement ?? document.body) start() + else document.addEventListener("DOMContentLoaded", start, { once: true }) + }) +} + +async function expectCanScrollToStart( + page: Page, + expectedPartIDs: string[], + expectedMessageIDs: string[], + errors: string[], +) { + await pointAtTimeline(page) + const seenParts = new Set() + const seenMessages = new Set() + const samples: TraversalSample[] = [] + let current = await timelineState(page) + let unchangedAtTop = 0 + + for (let attempt = 0; attempt < 600; attempt++) { + collectSeen(current, seenParts, seenMessages) + samples.push(sampleTraversal(current, seenParts.size, seenMessages.size)) + expectNoSmokeErrors(errors, current.errorToasts, current.forbiddenText) + expectOrderedIDs(expectedPartIDs, current.ids, "mounted part") + expectOrderedIDs(expectedPartIDs, current.visibleIds, "visible part") + expectOrderedIDs(expectedMessageIDs, unique(current.messageIds), "mounted message") + expectOrderedIDs(expectedMessageIDs, unique(current.visibleMessageIds), "visible message") + + if ( + current.scrollTop <= 1 && + seenParts.size === expectedPartIDs.length && + seenMessages.size === expectedMessageIDs.length + ) { + expectCompleteScroll(current, expectedPartIDs, expectedMessageIDs, seenParts, seenMessages, samples) + return + } + + const before = current + const changed = await scrollTimelineUp(page, current) + current = await timelineState(page) + if (!changed && current.signature === before.signature && current.scrollTop <= 1) unchangedAtTop++ + else unchangedAtTop = 0 + if (unchangedAtTop >= 2) break + } + + collectSeen(current, seenParts, seenMessages) + samples.push(sampleTraversal(current, seenParts.size, seenMessages.size)) + expectCompleteScroll(current, expectedPartIDs, expectedMessageIDs, seenParts, seenMessages, samples) +} + +async function timelineState(page: Page) { + return page.evaluate( + () => + (window as SmokeWindow).__timelineSmokeState?.() ?? { + ids: [], + visibleIds: [], + messageIds: [], + visibleMessageIds: [], + topVisibleId: undefined, + signature: "", + scrollTop: 0, + scrollHeight: 0, + clientHeight: 0, + errorToasts: [], + forbiddenText: [], + }, + ) +} + +function timelineScroller(page: Page) { + return page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") }) +} + +async function pointAtTimeline(page: Page) { + const box = await timelineScroller(page).boundingBox() + if (!box) throw new Error("Timeline scroller is not visible") + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2) +} + +async function scrollTimelineUp(page: Page, before: SmokeState) { + return page.evaluate( + (prev) => + new Promise((resolve) => { + const scroller = [...document.querySelectorAll(".scroll-view__viewport")].find((el) => + el.querySelector("[data-timeline-row], [data-session-title]"), + ) + if (!scroller) { + resolve(false) + return + } + + scroller.dispatchEvent(new WheelEvent("wheel", { bubbles: true, cancelable: true, deltaY: -1, deltaMode: 0 })) + scroller.scrollTop = Math.max(0, scroller.scrollTop - Math.max(80, Math.round(scroller.clientHeight * 0.45))) + + const read = () => (window as SmokeWindow).__timelineSmokeState?.().signature ?? "" + let frames = 0 + let stableFrames = 0 + let last = "" + let changed = false + const check = () => { + const current = read() + if (current !== prev) changed = true + if (current === last) stableFrames++ + else { + stableFrames = 0 + last = current + } + if (changed && stableFrames >= 2) { + resolve(true) + return + } + frames++ + if (frames >= 30) { + resolve(changed) + return + } + requestAnimationFrame(check) + } + requestAnimationFrame(check) + }), + before.signature, + ) +} + +function expectOrderedIDs(expected: string[], actual: string[], label: string) { + expect(actual.length, `${label} ids should not be empty`).toBeGreaterThan(0) + const actualSet = new Set(actual) + expect(actual, `${label} ids`).toEqual(expected.filter((id) => actualSet.has(id))) +} + +function unique(values: string[]) { + return values.filter((value, index) => values.indexOf(value) === index) +} + +function collectSeen(state: SmokeState, seenParts: Set, seenMessages: Set) { + for (const id of state.ids) seenParts.add(id) + for (const id of state.visibleIds) seenParts.add(id) + for (const id of state.messageIds) seenMessages.add(id) + for (const id of state.visibleMessageIds) seenMessages.add(id) +} + +type TraversalSample = ReturnType + +function sampleTraversal(state: SmokeState, seenParts: number, seenMessages: number) { + return { + seenParts, + seenMessages, + mounted: state.ids.length, + visible: state.visibleIds.length, + mountedMessages: unique(state.messageIds).length, + visibleMessages: unique(state.visibleMessageIds).length, + top: state.scrollTop, + height: state.scrollHeight, + first: state.ids[0], + last: state.ids.at(-1), + topVisible: state.topVisibleId, + visibleFirst: state.visibleIds[0], + visibleLast: state.visibleIds.at(-1), + } +} + +function sampleSummary(samples: TraversalSample[]) { + return samples + .filter((_, index) => index % Math.max(1, Math.floor(samples.length / 8)) === 0 || index === samples.length - 1) + .map( + (sample, index) => + `${index}: seenParts=${sample.seenParts} seenMessages=${sample.seenMessages} mounted=${sample.mounted}/${sample.mountedMessages} visible=${sample.visible}/${sample.visibleMessages} top=${sample.top}/${sample.height} first=${sample.first} last=${sample.last} topVisible=${sample.topVisible} visible=${sample.visibleFirst}..${sample.visibleLast}`, + ) + .join("\n") +} + +async function waitForTimelineStable(page: Page) { + await page.waitForFunction( + () => + new Promise((resolve) => { + requestAnimationFrame(() => { + const a = (window as SmokeWindow).__timelineSmokeState?.().signature ?? "" + requestAnimationFrame(() => { + const b = (window as SmokeWindow).__timelineSmokeState?.().signature ?? "" + requestAnimationFrame(() => + resolve(!!a && a === b && b === ((window as SmokeWindow).__timelineSmokeState?.().signature ?? "")), + ) + }) + }) + }), + ) +} + +async function expectSessionTimelineReady( + page: Page, + expectedPartIDs: string[], + expectedMessageIDs: string[], + errors: string[], +) { + await waitForTimelineStable(page) + for (const text of forbiddenText) await expect(page.getByText(text)).toHaveCount(0) + const currentState = await timelineState(page) + expectNoSmokeErrors(errors, currentState.errorToasts, currentState.forbiddenText) + expectOrderedIDs(expectedPartIDs, currentState.ids, "mounted part") + expectOrderedIDs(expectedPartIDs, currentState.visibleIds, "visible part") + expectOrderedIDs(expectedMessageIDs, unique(currentState.messageIds), "mounted message") + expectOrderedIDs(expectedMessageIDs, unique(currentState.visibleMessageIds), "visible message") +} + +function expectCompleteScroll( + state: SmokeState, + expectedPartIDs: string[], + expectedMessageIDs: string[], + seenParts: Set, + seenMessages: Set, + samples: TraversalSample[], +) { + expect(state.scrollTop, `timeline should reach the start\n${sampleSummary(samples)}`).toBeLessThanOrEqual(1) + expect( + expectedPartIDs.filter((id) => !seenParts.has(id)), + `missing visible timeline parts\n${sampleSummary(samples)}`, + ).toEqual([]) + expect( + expectedMessageIDs.filter((id) => !seenMessages.has(id)), + `missing visible messages\n${sampleSummary(samples)}`, + ).toEqual([]) + expect(new Set(expectedPartIDs).size).toBe(expectedPartIDs.length) + expect(new Set(expectedMessageIDs).size).toBe(expectedMessageIDs.length) + expect(expectedPartIDs.length).toBe(331) +} + +async function selectHomeProject(page: Page, projectName: string) { + await page.goto("/") + // amicode home: a single seeded project is auto-focused and its row is + // deliberately NOT rendered (HomeProjectList shows rows only when there is + // an actual choice, i.e. >1 projects). The home-cards strip is the stable + // "home ready" signal; the focused project's sessions render alongside it. + await expectAppVisible(page.locator('[data-component="amicode-home-cards"]')) + await expect(page.getByText(new RegExp(projectName, "i")).first()).toBeVisible({ timeout: APP_READY_TIMEOUT }) + await expect(page).toHaveURL(/\/$/) +} + +async function navigateToSession(page: Page, directory: string, sessionId: string, expectedTitle: string) { + await page.goto(`/${base64Encode(directory)}/session/${sessionId}`) + await expectSessionTitle(page, expectedTitle) +} + +async function switchTitlebarSession(page: Page, sessionID: string, title: string) { + const href = `/server/${base64Encode(fixture.serverKey)}/session/${sessionID}` + const tab = page.locator(`[data-slot="titlebar-tabs"] a[href="${href}"]`).first() + await expect(tab).toBeVisible() + await tab.click() + await expectSessionTitle(page, title) +} + +async function expectSessionReady(page: Page) { + await expectAppVisible(page.getByRole("textbox", { name: "Prompt" })) +} diff --git a/packages/app-bundle/overlay/packages/app/e2e/utils/waits.ts b/packages/app-bundle/overlay/packages/app/e2e/utils/waits.ts new file mode 100644 index 00000000..aaabf19c --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/e2e/utils/waits.ts @@ -0,0 +1,11 @@ +import { expect, type Locator, type Page } from "@playwright/test" + +export const APP_READY_TIMEOUT = 45_000 + +export async function expectAppVisible(locator: Locator) { + await expect(locator).toBeVisible({ timeout: APP_READY_TIMEOUT }) +} + +export async function expectSessionTitle(page: Page, title: string) { + await expectAppVisible(page.getByRole("heading", { name: title })) +} diff --git a/packages/app-bundle/overlay/packages/app/index.html b/packages/app-bundle/overlay/packages/app/index.html new file mode 100644 index 00000000..bb0d1e3b --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/index.html @@ -0,0 +1,28 @@ + + + + + + Amicode + + + + + + + + + + + + + + + +
+ + + diff --git a/packages/app-bundle/overlay/packages/app/playwright.config.ts b/packages/app-bundle/overlay/packages/app/playwright.config.ts new file mode 100644 index 00000000..09707534 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/playwright.config.ts @@ -0,0 +1,45 @@ +import { defineConfig, devices } from "@playwright/test" + +const port = Number(process.env.PLAYWRIGHT_PORT ?? 3000) +const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? `http://127.0.0.1:${port}` +const serverHost = process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1" +const serverPort = process.env.PLAYWRIGHT_SERVER_PORT ?? "4096" +const command = `bun run dev -- --host 0.0.0.0 --port ${port}` +const reuse = !process.env.CI +const workers = Number(process.env.PLAYWRIGHT_WORKERS ?? (process.env.CI ? 2 : 0)) || undefined +export default defineConfig({ + testDir: "./e2e", + testIgnore: process.env.OPENCODE_PERFORMANCE === "1" ? "performance/**/*.test.ts" : "performance/**", + outputDir: "./e2e/test-results", + timeout: 60_000, + expect: { + timeout: 10_000, + }, + fullyParallel: process.env.PLAYWRIGHT_FULLY_PARALLEL === "1", + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers, + reporter: [["html", { outputFolder: "e2e/playwright-report", open: "never" }], ["line"]], + webServer: { + command, + url: baseURL, + reuseExistingServer: reuse, + timeout: 120_000, + env: { + VITE_OPENCODE_SERVER_HOST: serverHost, + VITE_OPENCODE_SERVER_PORT: serverPort, + }, + }, + use: { + baseURL, + trace: "on-first-retry", + screenshot: "only-on-failure", + video: "retain-on-failure", + }, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], +}) diff --git a/packages/app-bundle/overlay/packages/app/public/amico.svg b/packages/app-bundle/overlay/packages/app/public/amico.svg new file mode 120000 index 00000000..f70faa86 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/public/amico.svg @@ -0,0 +1 @@ +../../ui/src/assets/favicon/amico.svg \ No newline at end of file diff --git a/packages/app-bundle/overlay/packages/app/public/assets/Aero-Regular.ttf b/packages/app-bundle/overlay/packages/app/public/assets/Aero-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..6b7b8e5ebd5d09a791b8ece4867c4a4f3f3f7c25 GIT binary patch literal 44464 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

wH|^K$CL_s{U%~vAz)Ni&c6VH?9hm! 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%0DE>YtjA_YWt^vcn~NQX<&P4 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 literal 0 HcmV?d00001 diff --git a/packages/app-bundle/overlay/packages/app/public/assets/JuliaMono-Regular.woff2 b/packages/app-bundle/overlay/packages/app/public/assets/JuliaMono-Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..6981959203f6725d6521daf83d24aecd5d27e2a5 GIT binary patch literal 946516 zcmV)SK(fDgPew8T0RR914slcf5dZ)HED-Df4shrI01XZR00000000000000000000 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�O}a%4ls2LcSpgYchE8PT{h7GJnYU@ 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

(-nuV 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 zII⪚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

{ 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%0A1@C*k8jVWH|G(U3{9&l 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 y@|Y}SYW$PlXh%XD;}TYcoE5bg*Ze1f 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

~c9nr~)=n+?c@wNr$`y@1h_=7W%@W((o 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.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�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 RX2-$82YtR^yf4YJTk)_70+Gb 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�TCR74Nj9$wPK? z*|eaIeft6zpEh^=*^N?w-nY|bdN9M*FGiYs7r3jT?LK-6CWWPeY01j^K#>3^i8qIs z){u7~)#8^S#k-@nfi+4C?Haz9^i!#5d1w(M7_QHB!oSQ|1a&9SHznH){r3!1}fF(XNrLeQgV^oKUM(4}Bm58IAtpTq1r@$R7urt@0c5rMhIPSCH5UxnN;VA z1e@}g(e^`IvP860&4{^})am0GPx8F-aCOnKB!{`KsR}L%2Ac*K(M@8#x|c&eC|c-r zR~BCV*8pCVq*?Uj+*pK;AigS)q58U1#uk3z?4u!FOOSn9!IcoL!EhQ6~fjzCinlvjH}H$cG>Ojs1VCP_Obq35J` zEoR{cRTWYj4z2+Z;2Fn9l<2bso}$K0%wu+bDJL=?E6lrDE=j zlRQo(Ak}zFUU;BruDk6=Z$-;H`1)A|U#^-ISM}CSKYDf_`&qrMyU@S!c}C^Gyec|= zeXrO$Os8`=S~^=jGM*To+X{pkR@0RvUWR7h_2nsSfV01d{@f4KZ|^*F`Q+2~JWWej zaZ>Zu`YBpfa@h{IpW%I8^VZD^(hZ=T-aH29_`m~^O$UbQy65~ym$5HI6m@fx>BAd{ z`&tw}P+Rgk@&XP+H`gs5bq)Bs=$r-4bx#J?StdDmv?aAKv%n^+gD*O)sCJ;)z zWd0XQyCj0;ec6Hj{tYB;H7>qK8?;~~$iO=|#7^kW1$~UVaZ;)J^Siq<1jC<1@+#JM z7-p^DrR$8;5FN)5uOy)l$i{10UU(-tuZM5O!u#eUzc<+jZAzQAWsbGWl!9zDNHW3R zO>GzXz?63Kr~q__ZdY(>Ol4+dCo^+s98xkPDAE{U@9Ngb(YxjNNKaUK^3xq$IF17f zl@SY^Qh#rz_20WG%8MP<{-Pd=iI;Xn4N_N_aKqcG_zh)n6Z?ZMkWt9_5?6 z=p53re9VgXrelBAI5ya3%<-kFUcQ_1^779C#lfm6U*yZyCyQ9!mL%zQVWkUMq)0u} zg*oXv6ZWpP$sA8F5A0LzW8MR6g^-G~Jdi#Vsc%WDme4M!2{YcL*cxccU|0!868ih} z&2F>xN6`Ato353S>&1%ZKX&;*E#L@qd}uwrw@lzmO=Is(1o)tOeYx+CfJdUv{psjJ z<{Rv~L64q#+?!(xg{4U7yQNj@dwEuTMF@Mw*rgLh00%(0~Basgp>NB~m^Nw#Soo5es zWOZ+9ee-gut3xJm#-z>@$(PCB8&0gAppzJ?uxfBtoKxWw=)V|}FvG{ND5=frVgQgk zb8Dc>m@1qT!&n5hWjGJeha-R#E&)pe*`<@dUkj{ndgwn)Y~+~L@$>Y?M-fWw+k^Dh z@pn7nm*R7EK+S}|CyL~AYgqTn5o(?4OEi`GzT~KP$%pE7QCR8&Cj&envE&)A^IU!L z&!GoBd^GOklb$R$p6e{VHuJv@;$>UJ`VC`PMPl&_*|uPR9lg{6oLvYP>?LZ+L`BF6C;kM zP<-q~qR6Wb@aV)0@F<0Mu!k}xGD7-vojL8 zRby6XgW~Ju*r=(SB`z%NZX6L1R%9ZP#XBy}@$AsrZGk9ZQ#C->uMZ(0@pJEt%dzQ7 zXtSmDF_to{EEpyJ$J;I+0(~`!BQ`~aw7i+VMC2ph?b6>)_PWI4C{INW72rMXGI3F$ zq;SfckF3rgksjHig93kXZL{q{9N=0`5Po#xLx1hh4smPToP;qWu*TaQjk8ew*Ad0} zqa7@NP7-154n1~J#+$q>90Kh;3;T%EB&ciayzS1q>ty$m8YP#0nYQAE5g&Slh*BWj za$%TJ;=411wTHw*mr=Qua{}5*BM=g#jxWn5`25tj#?P`4wb>KC(^slal=}nJDfh9Tj%R$H%%wzK0I6x%Cq!jBy;Fh`SMhZ`x^kiMf7l4^T zj|^mI!|S`Aj2;@~1W7^W(M&K`TRV7j{hRrPJ} z;8wPaI@(WJyA^y|qd+&@UyirN2VazTS~ji@bquk15`j|U%)z7avEuqq)VZs*o(6Rl6v4P9_e1}ug1|C zymq{{k2BQs*GMMChDLoh;fG8Ca)ub%?t&$J)n~Hvs7Qt9MUjQCy*_l;n?>ns7xx0| za%^_a$KtH;UNmlO@PF zq2p93dW<7Knlcu7`t@lqJq;96kVxE^3%pJ!7#%ga!~}%0hO#DS z^cbvf#I%8_-C{I2tMf$nbynZR2IG_DkbY*u&dgOXDwQPess8PK3sY6mrNM4$UzC}b zgU_X8?GDl>B$fyq{gm2LMf7kPAWBwC*+jbWYDn~5&$Wu}u)ZBdew8b=+N$Frb?4#= zx}y2s@dyRsfKK3q3nwIK2#Z+++1YXin-Y)v1WB-sH$5KX3g6B%P}H>8SR}KeTc1}4 zc>XbGroApFxQMlA1Pow|kd$U0nb@zR9CiXa0zS9_moQVpW&E&ARx}l$Ct5sVsC!B4 zx}}7Lzj@kV5m_`I39Q$rNQ*^jIs2mukv1o$T=$7qHq zr3>$Bix~bVCcHxKyYwSo!V*n^OagXZLT*~Qme~9%Sj!?GBIN6<)J};{@GQ)L`n8zV zW|kD7KpvbkUG)SUJ?EF~=|j4(U26>r@J&I}$7 zKrffTpeAH7E;k_=IBAvCM;Lgc9Jy{~XaDKxYh2N2H>&Z5P^xk4#+GFaK>Z0YG8w6v zoKM7pkh#g{wMUG%TS(h;C_dVL(Z8ULq(L;oWO|T|a5`JYKNioX87LJfIJu%NIDTMa zLn-u=xV%R^#*_>-R+mlyWU;GIHJKi0oO{qgjZ&8aMC;U4E+w^nlu@MIo}i1E6})XA z&Rw7@Z>HJ^JSM&!36^(6MKKGcxojg>HZv$r>OP`vB~H*sop3~9B;ytJPg)0`pDvi|A)7fE9+qRmg-VLFCI{MeH0=e936eI%VA!DGf^ygN@R9J9a!8J zxlqX%6W!BJ6A_p3KIYC8-7sPhxVsv&7A$TeLg?_~e8R>#DKJZ_GXp$x7L_4z$3yb?k1Wqepc{1pZ#{og@6j(N?Wj~{&2qO4HEcml#Yf+%5GBq5dVxfx2ytqME+uQ2RBsvXT{ zkLww1*%k!96Gnhs+sjz#dF*f)sHbf*JqJP1s3y6p-qaYJI00P1qEvj0_AZ}pk&p0d0jy@9S$6VZtL_#b05a7e^Y13{iaVXl< zudTYk{B>lgy2y*}UCGRUv+U2aks(qwx4{%b0F*aMiq6>OjbH)k5X8zEi?egnqH3f1 zAzO?Jwzr6*Ld5m-4dJz_^h7-JJ@*`4MaNahGN2DLl~@jNuPm1xYWGwiu6!72+{ zis6|8Gw4H&AYrw;-%MEN(^vFQ`xYM%0Js$T@C2XfVq*Uxfv4dL1WpZ_eO1L&*|S!$JG>lrHPO45ftdWIka^wnDLXOZEE|A`u(5kLdI6#`yy) z6TW9{rs1Lp8YR^`%fU$(>}bG`vJL>MAf)0lkE+GsT&J6Ov(P4}WY$S1JiV^dm%8{; zoj#714xoUF{7w>&M@o#^r@Koj6=Ft{f51ZM;eok{xX=X&R&D!U8&FGWXd8U)wD@_afoTn%SVZ@=mKOd<{IsG^}1**Ec+hdm?5cKiH)Z6 z?V9C0BA6)c(9?;X>6{`DiodtesIUX!*WC7&x2q)bh>)-7~Bw}+M-ui zfXkj^CSmTt-&S-PHEW0HQE`;=QF4&^n?EU@>N2~*4_YqleH`ll=pp>A53xq2ILI)k&tg$EQ|xx-Rjbg8zASy5a+#>T$nL6Rga znQ6#Ce0=wWs7ub+0yt5D0ev$gxeBQYH%xe+$V2r|uMh(DUFUQk$OY#{h`A zrDXTz)i;ygnpT)MBv8q?l2p&<+cuz=-&vEOM>xJ6ep)l!N)+;wC{|;_(|vBAo{9H#Ov5;hI)9wG;C{Dl}$%ZRL>c&+g)to zAQV6RxIh+dcKRZCgb|%#eW@RiVlvR_3DS`_W6Y`4)Pdp$m{;j(4&91TD^HroN(uyu z?OboELPt+S5Rrm#kq(a$g~;0MpfAhD(SqYH#_R?s&w>|hS94^QykXd=8Bd<)jT|EK z{WLp6&p2w(d=EN^RQz&Cf$vNpr6Uf4aYklbOAy_anC0M9+9YkbP&RrR%g`X#g7HTN zTQ}|QcCd{V)c5Apgr@~;#a}BgOm)jc6#7m}P1J5*AE)3l!9k9bqUM6xE%sziI+Nv) z{ub@A!t_Z5jI*;Xu_a5`s5n{(s^+O6IHP~4Z@4vt{2uCsOg59{d48B_!gT$#4Ft7+ zhf)%0t1J?v3##hfu(F%e^e--#qg1J*>BU61HusqR7?8RW0h`vSy<<&u@#!wU_n zDY9cHJY-A8u<>$Qt7FsBj1~SdK|y6Io5fK%3f14IM1_lr>sY=MEZyv51=Z&DA%c~! z?@@`W+jW_?uA(s(l%G^)PSum@`RJ64`^!?D%F4}-5fh^Blq$U^ln@QM6C@F{&H;!! zw3SzJ24TdmZE)jsp+c30Auv@T1V#YK!=N{?bZK&6;dzn(R>QWQZgfxlWyzki}B` z6$=2~6fO^Y&G$`U)K-Wp=UR^wmjl}~%INS9Zwe&ISgy!%Az|Yx&Qz$vP_Z3RSZarr zCom)BIi)5e&#k9hIW%<&_F*itcdQhF87a>xH5qw6@k9n9#+J%*oyD}-LUyLA%mp7O zEwwXnoD5@Dk{}AN_N3}(*YcYinQ&N{5d^mEyeFe(GJcm`oM=5Fh1n_XHPLmDs^-+f zYIF+#dDR*r#nb2(L21c&A0O^IHnAAei6xbl$$15NpyhA2uzgR`NigCJSA{1+1ELbQ zcuNhtv^ZjUIRc*^?7<;c4NRt_0dSgSS_+I124Ho3=?P-kJK({1ev@GfnMs}ZnUqP^dX+b(EUAf>}eje2GX7hsdiL9;CLR}!=$`Ijc!c;&}oE#Atv;GX24@$;v>B=yxd5)xkKc-?A$(`0%g zpc;n}R^@D^mk>7E6U@QQM&&xgKwgNBp#TO`ywLu@&PhIGd+1(aKR{MoqK+1)qBmf| ziMZsJ^LOC!>JNM%Wj^2iNO1q4^;J*DzA6wH)opl?e!6J^$6T`yMYmu-Myb>_@||y# zuhVI=sNg|_;QY{n`&C6ZA1D)_xnP!by2G72YGRu*HRBJE!4ji$YQ*`7y8(#=ai@`C z_7qx3-vg{Bb0FUIpBSNkJ2V1K#_3iaKoc|JY$hI54TeB>egA>;XR_t&P{ayFO>M*9 zPgJTIF;cytlj#`9qj1u%GSNHjjAa^wTSCLKS~#0%n88-c)!|FDdj_u6%R4RdqM#L4 z^_@(^!y7`VWR#ybYiq8G3I~cF)0%TOLS7ZMgBPX>4FL$LnfgKAZ9)7^^y~xDGl@q$ z@H#`Ts9aM&2%!piV;Hllk`1e-(+CaXc!4a5Q$Jig0qjSiqG?dTfRC9Im@-CuwNS0ZD|Qgk;Bu83jYD4{lAP3okxWB^=5N-h{&N1B4J4A6E;p#DWnBAL4}+ z6U&2PE&G+5Eb@omlzhRL>PPzg0H?wbRnqaBwE;6@e!K zE1M6VfqU(f-*BY{?n}j)P@x65&hSA>x`igOE^6vI@m!6DJMO#1u!uB@kPD!y&E-tV z2Eaq8OyVm7DIKu_Y`%>LJKdhTit)uHlAXKQVgSQhVg3z2U^iG@)Y!RUb$5E(V) z;;ux}FgDD`DOOGCs)`!*&TuTUC=XaPNS+Y_HK`#FpETX?1fmKgQ)-bkL?lwj5QXU( z332L76D`?9s65SvL2Vdj$jpmU6?M^yGbl>`7*6KH9F8B=RgFp=(X|=AOgy!?+sx-f zkt|aE579)#QOAlus(Qvkn+j#FEiua4q_SIi5x6Z=s8?VC3S5v%03ZP)G-Ho9X}2i) z_|D$sU9gw<;1VOB?o@PE&kV!Y$pUmB4TIFA63X#B=cEFQJ=4@z20-nE!HE1s)gUYZ z90_p#1TsJ&y&kDM62V>1ZX5uGypou)@Gi+-SKypopfG)77zfyRdnO{@_NPR3%-xk1 zUemscV9Dbq<;B+MN)FInTyA`u;fuf1?A-OBkup|?yaTU!;Cd&I@8 zRA9eCY30uCCvi5J#T4~ngwbK4kF;fI1}cITts6r84|G+y$}3{fu0-^5Q|nMnw8~>V z6Fn0+p^%>FO${>M3Z+5lA$}+dq#`WV*g%G85{VSy$0kqISS$`SG z=CY!#&#M3Me!`a4!EzG{Cll%l1`Z;DFI&3c6LuT3*<-E}?BJP~rf9}j$|4AT7H@mg zI^SviX6=JX7Yap+1cW6V32pO~C48cj~MBQ7asndeQq*}3p+=_y}jiV4kJkD*pVad-?a7VE;kP%e%J!}bBd2+341 zC@*U~_N#IEb%v%vSF?k;$Oj$5<-p5bx(1kwUBtv}24C}(?ddTN$gSnAx@jt-;T#1H z;llo3;0q97*h;n35GpNW&bWM^%=nT-Lsb$j^gKUX=~b*2t6J_HZB43u=MBBkz<+=M zE+etj5Q=T#UTOgwW)~8aNKGiS+zR@W)Pp~AoC%@y2Mqg67 zkYnps^&=62{J6FTIoWER>evoij&F~5#mL%Jpi`E8JWyrUGHf)IAL9!$E>yX_;IN)* zg+D9K5Jj?O>ruhj)Ga#?5&>x1SkO=`yw$65^CC*eY}+T^|Iw2AvBF^t%bmXRKq*mr z^MSeOh2AvOq|AR%FZY95>)4Z&*s1Hjl@1F28%~& zXawz&3C$H<^!k$irF`W^w00pN?$?Cz1Xe7jX6*Wj7VyXXbO1aq0iG7wtSA)3<2E;e zv`lr`TOz`^>MHjsBRn`&6$SxfrwEV;rWsIUEo}sva zA%0*hyVnC7Y$LUv{^@vDM5!G|SqXr}{MfuggKvdaAO~F0FieD|wI5d~FeZS<)el4Q zEVri^2W8k?7Mci{&0*^?ywca|se+M=V1KX#la@lwUk`pUU>3>@+q@$`oQrh=EFo%w z5hB9Em~p_a$8?b_;ay}0yZEA?O(#}m<^1*Z_4C}O^2;aw=|<0y|jxr(eBx>G312aurx%(4vIRB02B~kK}oXB%4}F1}&5q zg8-R)lnj!i2axDyS-Vraf30+TTDGo}Mr7{S*;lgg0^I)>|KwgKDKp>#hO#Gn|q$@?Jydr(I|Qb~?;VezvUw0VH8{)6646QQ3}bHQ55A zY@@}l^VHxj%9_x_v(^_#-TXD>r&sq;^7nnYSzTH2KI^T1a@W?heL=V+ka znw5#o^mKQ#ku@#&XMoSafEacrf7bWp3hLtS{{@^cQq7?|;mvLpZd%b{Lb8GvkQ{<) ze%uy@2!}W%#5lU(on_c+4VBxq$Z8dFSGyH5uCw<=$5fs_O1Xc4TX!-ebrna^ z$2qJrgnXOEi= z*%~GZW#+#vw|2Fuq6|XNY!AKeGO*&Ifanp%wn0=CS~KczN1y{%+s>7wk=O!jWg9;8 zsEESZzO!0JydDOhaJem=&Mx8;WwTaz5iv<=$Cfvlz-FGmfd)Jhzv5w@!1Jz2HOx9z#>Lz3Lk!*HUd1Nx*uVNUe7K-m?Z^=h45V9C3iToW0G zwAL_bW!EWv4@M|@&)tNWae6a6CU~8ri5?DaJXUujf)!rxh<2zi9HRX>g%IFy3#o>L z{93unCQhmwu@4E<%Qj&aK)xTWK)SA|@UKz!2lO=krPvm|v|iW1AL1A^nGcky7kg4oUZ%80xNb!M~-;j8tA9RB547bwXSG590P17Tz)Vr5m!1iN;;foL|7kNBlyVStc$DttutoYMmR@be8i5VAPmMd5TTwOfjCa2k1<6o)}cS z)9Y5V~MfvjNojs=)Kv6+RsfA1jadyton0YwBtn}A)m1C z`;AsG$i6X>!HjY8SWWxfE<3|s99t>06@_EPN}@Dv6VB|*nCj3P%C?d?s^H52T?|4T zjeBs^oze^I+-k$p3cl+`m{ctdj9QHt_tl`9icC|X)ezEKPPk(H^}PxE44^Ppflao+ z3YP7uK=%rjeB~y6ANcVHaq8Qn)m`aT{dw2T+CM++N3)MXD5`!`L2yR4?Zu<(#AzJ8 z8zCh}xN|d41x|{1i4YHTGi25oYV$QqQ1l}Tf)y;=69$%V z3^IZ6H;crP1enZ1uMFPOTTH*4aFbpu9M1j3b3`LBBX{FjYf$Y3y`*WekIa<8tH$VUQWRT<=uy%0$`!rif(d`Yt+koG z%us7BxK(Yb1#Tna+gci!QDdfgU{6|m^&_|n5%6X6pqGFQ;@Udo;|7s ztAElllGj?|c_1{u#P#z~m`972u+0Pbd9S9E(W2~WyaGRG_0n26OuPjt14g=<1&lZV ztlqyI-p4)Nf=h5d9kQBRC-wr8eu_+a=A>-u%|-$L>y{q{k){=odDzQ@7VlAhig0HCpv-chq3%C+&8$;Z_e@CqlIpSjjULk&&Gw9EEt<`AN1(FR` z*02R?<_^QCG*WiFzw71%Sb-dT`oh++8;yzH%UTG_YoRF`-oAudo$bX{=^zu&{uPtn zklzP;VOYB66LpC%R!)A6Xcpi0y)tr7qgIi+z(({H8h}%V7gDkZbb!oFRDclBY4&2c zYZsdnE|XR8Du?%D?{Oh{tj}$4{}1KLup@A;22$I=bhjaO5B*Fs2I^` z83M~{7gk}r_|YZWdbi>@ra;DQ-rIIycyel{La?#C7FIzALR(+sws%dd)0N$xUfWDo zFM=$=GR8P6Dn2}*QlQuDndfXR^?AcFU$i}aP3X^aDOeA{-#4vJ_%^S6MR*nCwyUth z_I4=otOr}jJ+Ze{_};#s+v)OsRyu?D{c-iM&rZhg{DTskyT4uiD0`W7vC;4APe<04 zyt_U5PRspwGIa&Zqs>&`Np3<*6pfKYVl!Cb3;J3;MV83tJrQY>&3lAbJB97VQW-5T zkLB6$#2VZws6uXoes?e+g1PVWDe|HvR@wKYalLJ$`Wwt_Xq{`#WA&rI-)W?5mQDh- zMcp%~BvsNN71L@YKovZM-Z!k8b#MrhOX@K( zTbyV0$(&>A3d&t}>uCW@A4-5f+5HFk$HO+Nqz_LA2bJ)7H}bC)qvMs;)wtw9pVufk z^FVERw{ocvQ%hu7N`QE(zGUyxg*3XG#?1w43inq&|cI}gt_@hRGp(_JlbVMHHumQCk>KI~b=TPCcabY(Snts2-5E2UB zYOH2)D}xV{CYP~ZxYXne;L!V#BW1a-9hD@G`GN}NyI|@^tX|j-NpNp>WFsrUOAqq$ zvty}0&m|sS3mgeusov`PKL?$?W!X8gm~%HT_@G4}ldw1?I&2wcval1|gL`s5z9Z$? z?ylA=WsS9#xEJF7KphleLf+N{PswA5R|eeZg9Tx=#B_kf zhN6hLheGN+=9pLz+3l&D3QzKJACGQL$duX)l4q|6O$cz!l4nhyB3LF$WR2xhB5M=@ zqmDWFff2)<+`w|T2=h8?q9T^^L>5x7$JETyn4q3hrkq`VBTlixJj|-b*iO6)SQ{Z_ zvX}|QO1^{@w1JGgAH=~2fMwZ+;#FGAgbKsf7im8UKe<9%Vq5~WhDWca8*Y@hQ2u6@3LXS5OAh%0uwjl*iQhU~3p2At4i8k8~$%@CuM zOXq3q0BH?SmKXbjmTcN~fmWPF9mDq)1K`f9SO%TKt-K|upAwMhCvyiN=Yoo(?975q znj>K#PASkwxswX$#16R(So_cXWNb+r#e)&i8uNiCqAp27CF4w0#9S?1f#sJ_=~=9P zG{gaPPO5RSo<{uNIUu6Plx7L42p~7uoKQL4A{m-6jW=CSdI~UxxAAtLS8~+b3A{-eGC@wQ{OhD5Aa`l`QSj#Ki>;BsE%RMqWmb_Xb z8+z^5PC~?7?9rWeG5AG6)^q}ybA~Nm2NF1*k&@PpFI9mUjCY}yy2>HNNfCkL<)h)2 z6{9+OO<$cTLXlg_J7o~OEgPIji*B(9Y?J(e?i0}f^8f;vmvq5dt%(Rio&~q(=)~)V z@#y8oh0eo->cN0X1UqA=&N-#M;Dj!e(?~ z0}N0kIaJJV3uM2IK^bU8LH0{lN9I!?%Pg_gwiW2tZk|KMf-g4)TOE|I1MFQen`tm0 zv?`$6)qy3n1dZhuSq>1oCff;b{9sR*be*Cz(c9+IDNAnSdf=u2;Wqg6+4aRYlpm|p z`2;Y$?dYQ^92^?$=gCHC9t7_7{1XPA?-|Wduzo;VwNF8CXDPDtnn9#hLXm9j9D7Mss9^=$#=pZElf}p>7t}f zm7-iCh3=-8^No<9BRD5U<+Ozf>utGup##}0_wPvPBHNVkM{%9IEUZ4rrJAU0ZqWG* zAe7&ZwqJ$C+=q$({P(r8pC zD+!l=9&jfS;&G|DN;d4q3uj3w1ZelC-X|!J3-RhoJuP)lT|2buZ6DZ$efdP)@cAEG zSu+fY-7`$`4W+UoRG7dOd+Hp?(g_^^>dA#d(6Cuw{5U~+d5~L*5+H>{ElO?6h?SS7 zCCpLqYe6d|Frk8OtOzOrBk{9Hng=(#rGWb0RZnClr|jfx?dIGEW;qbljXdb;WmWK; zUH*CybNzK@Qdu4&2F`9VjP}vMb5x5(r=#t3IxSOt7{81%($G7>Tx;GV%wt|XiPLM_ zBSkV{BYU0;tR^Qgc5bs<^`mOUoXe&(#^{!IgP9hbaYq2aKq8H-!m7;AA`-M3Ao#9R zO@abUj?UQS$9=ttiy7T4gfwa5v4FR&viS>NFTc&1;kXlRsHvgD23gyO5{1l9PFhRe zv)2_{ee^BP2_@#>lMgiLO01zA!?4N?*)w8bRZIZs=9twCX0|%Z*)W93CO(R{_FyyS zX8f7-@8Sh!qeu(rlx!6L2djwdqky|gz#EJI`S2E5ezXnSrAUP48=eWVU?xeE#8lsQ z1Tz{{V$rxIYK*z?YJvE=^eb_7E%ZDu^m$)O(~*M0^nT9EP~PLpX7(9e6%t@S(}(i` z4CFpgTiX+pEl8h10H5hisE{h$AA+T-2%3d_3Pas;R-HPRqmv*@L{w^Oniw}Vg_sc1 z!C0Tnt|L$sA*mvOS8b=z?7G(>lysMK?nfuiXvIWxbi`sAkwgROtZ^>)JD3H>;{z$4mQ(C_|-f0w#)9Qm{LX}?j}$e9;NdKB?WOw4DAY=t}r1u zHuU^h0tsLk3???K}Lq;|l?$-inNC@2$MVVZbX=84t0D z!yz0^{J~bFI89t1eg8P6zjzI==^y;P<`i|hb{zwr)sjwzY5GJ_1=)d|x_V^Ccsub} zQL-8VdmTcS?GolR;cz|aPnVTZ*|seW$HR^lV!|=#qq7_#aTe|ZNi-wDUiJk?mVqe5& z`6M&0O|Jy!pNVV+{k=7blGcq^A{mPZ}TWZ zuc%4rgR`bt;E+9AMh!-go*Ds4`4T+=9Z2<%F*LV$PE+PF{{dOk0_O)MAq7LWn0Ia> zsc&_gm9^t~QH>_DX*dnhCRB?D=rcWjBHF6n(f!4t0H!IAt@gp`moiz77g&krS&aA{;~D*=x2!uvyR? z0xi@ES;A5v6jqk`TWSJ_@s+i?Z>}jK0NzL)GK55^u<{f5|Jjb+KaO&i-G*QQbxKu_ zGL1uX0!9f`$Y61e32-il3$RzSpz~)eZT3uNR*ZTSz$m>iGL;TX?s8;iUlCYqOUhj3 z`i^jmvD`zb2!*}yPr>*`PPBQlB;K{A$|sL)o3+~H1U+;bbi)2J51T_ZYS0l+#FqLG-*rwqG;O*XFxDo_b^HqXW3Nr{DM|;nx z4aeAL6BET^359q8v>vtwO~Xa7ommN>5Xu@V^G`t|#=46n*v~R&ub>Z3!^;Y}sC|gL zg-+JntN~=eFx11+Q<(l*J6F>WTWl-|be{{qL4xBBU>Q$7i%I^F`Td3qIvIsT1AFu= z4D>gbZs)p?q{ffcTYpK4|MbiSjqu?aVAZE+t1^dm29IVTbimv%CE~U_?|;Og=q27i z3{bx<+>sm2H3L_L?jaL~)38fRp~Hj0)0Y}pN}L8-j|w_ggQ3oN8%tuyVDS9<9vL8d zZFm|46%XsKbDS}-3{tY&u|Q2NmhYI(e;P)Q-@i{Q<#Wf20GzGd@#!5gjjuS)=L!m)lNHq>q*$__?TLtx!*iWoBBMARHh=LO%c4oRjsK$@c+cBEZ+2(^sa zV=zXHJMsr?Sf#ud0DUPd6y2ga%J1*GSs0m@EcWflZ7J)U%4PSvRpPxHvlealL&#x| z{OMLT3H4s3jvB>sM&ul1pMS1xiZc*67fVeH0ynxV6tcpwfW4bFupjv+`t3K|a zK2lstj~;{~uI=7!kwZ8JepsP657m;boHR%^Z3}org~m=OcMFK}TnhpUg`UIpn=VS0POgKs2!8?g;^D(9#KG-GW2$oM0~Nw6SLU<%S0u_O*plku zmo9>@`LEf*t)-s&grAo%6Ne0-L=^CgR5}~40-`;&fi3z&XvU#EX8?|gjG|~X2wP9P z?KCOvldmT)Sppr!IRxKKTYR4|WZYWpn{M*cg@+NI4~h?UROm%L16-96$P2L2ByO3T z>70WJ901*7Zh>A;fjD;r+M)ih9C$QzKATzDoS@LNOt*>1o84J^o zR#~gqD21dHC?ah$WF%cTCJJsdKJ9O?FG)n&SuIe(rpGTca#UGFD7c(Z*pyvDL&RA< zV)S@AcOJnMS21DnUyHK9OPr2T)ur6&o%mWyfNgsRO8xjOee0H#=^mS0+|dYsV5T#g6ys z+1V<3ve;$u5W2vi6}4cB#W9gCMtRCqJPv^P%MakY)So};N$p@ zmtD*Y3dqPLhaD_Ms$)vsgXqd^ml~&(BK$HAz#{uhkD@8zL(3KeJa{**F685lf1B+q zG}w9yQj~~^O)+v4f(09X_Ne!b@aQA-m$C^Jkvc*jC`l5v&F6rOs@g5~Z-LfV3&w7=nA zJH6@(s%m@gmFel0#XUsAJ+oNVzmq^0?(P@Wb%D7cPXk)pDw`%Ha1bj470SVZ%LmOX z>w`%IVBdMxNI}Y^cGXZivFiOf`YM({BWT-J%>U?zXVPtn_R8Z33G&hN&O^WG6uwnQ z_mp9)F%;g)@kj<##<6Pe3wf^}_gtHBaA4=o^PSxVRD33W_S2l#m+790CxC9Q*?llI ziB7pp7*i%$jgwZ#RpTctQ#P@&Y?&guw2Uy$hyGHdZtR($+*Zr8%BUQXY+Py0>my zOHz5Z$o-y;@mkAY&h2S!iR(r-p@6#@a-|BWebK*64ZgwXg@>CgxP!dJnyqDVa>5)e zQ1>Iv@uSyXqz(2DoS@c@d9wHBW1f^LJ)4Uf1kq974R>@VY&jS+7>d{B+{X$vlh-mF zyls=S)%soZ>^)QJZ7-GZ^sh7xP9WxoK7%VJL+^Rwq>@~idjjr2`H+h`Mmqb>VuEMN zM@)D6eDyqzpTjr)=5+R-ynY-%jDc7Uy>GHhqAH&*N;LfHSlF}pq#v>ZfIZu#HtQH$ zz*Xj&c_4CRCZ=4mA7h;Gy2yrKUSskrT-2?qZ6GV6z8BG*H}|)9ZmKiAQ%dgQ<~Va2 z&Tc89VeUB6*2&fa6e&Mj5bB0I0ZEqXM5cY4)aT9FwG?d_;R0cmT?Iu(CTn89%g-iB zYzH0M#siE`g^rMDBE&G5@`ZB?NL1DP?TW4BxdvY`bj13phLs?7*0Lmt9DmC4`6aJI z?bXwJjn)i{uA7GSJ~~@XS@lGQ@*tRg2&tRYJ(E;R>g(0i3CvI(WAXLndiw57GAD}g zvmSe#w{S{G9Qgg={$eT&;DFAEO$)h>lTRy$F+*QhX${%fC%UfpB z;9V(3E5kz)^oWJ8X0c<1=(9~vclw`vz%eP=qU+v0jrj84m}I2(g%bv?h6}?JPw+L2 zA7!m;os@A5XxPc*Vl3i%=)bX{>+r2n&iPKfxQ(Hxrl|VH+Ul817I#ZV0H+@tDkn-A za?yyl#@^jd!t@-7_TFc6gQBW~Hc%(UGorZ^L5#d6yCX~TiCYeVSq#@s;h>qdH%N7f zx?D}HE$Xj6Yrm9Y2&q&(FV;-gC#XJ@0KL-xjig4J9>3N+5*Qsi5V|+u&7`Jhs!VTdTRZ8*CaEr!1YfC7R{MLRpwhol1p6&7-)Hv1qS-U6aw;u$y5|qssS(?EeyS12w zCiL)y?pKN)rB3CgZ6viuoz~bfB02N;B!D>s4|k`l=w>rPvqAc&LtPjjlC}G>$bi-_ zIiUcvt8hmGVc_Fqxyq??dXy;6DeE_!Pu-4u%EsHnB&c64x}I{EGCycmDO)Mzmps$U z4zn|RSvVeQ$$RfYkV6im1c_!f^W1STwDNg&IaI}I{3sq`2eba%w=zqvDA^5)Nc0}= zq^lD*N0iihEyRu1$lUmrCE<9~T8AyqR?ivKtlj{=>@^O?+B0YE8C;gmedW7v!k)an znZFc=!_iOl47p1ah*?ExY3jVl*!MZzLn)y$@^|NsIDjpN*(xEQLhhL@Jcm&WW3mX# z7#lhxz~p1DwL3E)@*<}S2U*{dkOf-%nw18~T+@m&FS*}RMqM2(8c|E^0B3PHJ`u}GzC<_bw!0LUFfvR;LT5%27m<|!Qt_~o9ADXq zs!Q~w+}TZI7ci=hgE?HzAQ?Y30c1R?(Vsp3S`&(;BDo)Q@={5Hm-BV%a3=iZ8hd1? zk8yTLrcH;1fcO+970=rt`7mpE9X@$cXH^fo+pmd|9UZxjyyRy@PJ1r0KpRtV?*d`e z_J#BJ&TU?BQm#fsT#%^*kgtGG(N37|&&=(vI!gENjkZF_(=yS8#nKg+8X)aay8U0?*ZGG)-?ngIK2zdpuomtJKr3F1qM~bpULPdIDc)_>5mNjXe%Z0LY(S(5rVD*3al1^7Kr!69d&9n zv&mlIpMGJ0#xcIPN3)WNaYf%4{~LOij6xK~F#K}*Ec29Q2r!DslPgQLRR08jO9hn3@B!!h^YUgpWen$qP`Y z5mup<@D+~tLg60YxHoN3Y^J7Gu){nl5%l#j8<>K!3z39+tTfYG43RLBhoV8!v^MDk znc)awX1K*3E=>&*FI8kCm=?^?R+jG4vF}V?UAa)@Y=2{^KX?h4LQd#N*t?5-L|+6k z3NjI9wtgO6K3sdVAxWjRc|e(Y$pDp8DFG`3p|-{&#q!RIh_+QSK(!Tci;0WoU@wKn z)*D6>hJa6$Bz9|hRYoTI(u$Qbrqa@80zIlxE+*JGc#u!yijyN&uGV9tc%sQA>B()G z{h1C|raT6EV!89{*pOR!}EQk#rjJD(Zxa`Zj1w07PWx* z@^@aLQ>#)l^W2*&9xRj?H8XH8@eG_KO9c2h*6K_j&viAv2Yzt-8BE6+d3niNouw9H zDS*aES}y|^oH}kmi*J1ng=MUO69Y9O=^-mnH1(&W%FOmv$_Y(%j5A?4x|KC%O+ExZ^S_-;l(E6nJRVVB&A*MwAih3|dN+}Oe{2ixAhI_C)9+?)d3-dxS zx#j1IF>eNo2@CUWDvFW6NSrZVN-Gu#N@6O{GTZJJU_PsFN{l)6;5b{r z`nFe2a}Tn`WiB&Ib8Ups&;@%sfEHssgKW+Ocs7?t`nL^L0A4qhbA`bPej*JR-@yV* zunFS?jUZXCT?kLbOek>MvKe@!XjQVUBMUy1Vlf17BR62sFzriyPQfG|AbQa~#qf0! zC0eu_zDdleBZD<8lA_VQp7hb?mqw*#Bo;{0DYT5T~Hp$K- z1l|EMRAHMT67NgMPJ5{&-%@rBO8Tw!v_B}+b@zxpJe-Qu>ckCPg2x~Pn;M(lz-Aa! zAv$MKq^;QXP5{dNzhwU{8F90!+z9P$%!I;w6>Ij!1Rg=&HtN`7Qj!uuDYaUGsQ4n{ z?o29*qO{s+&Wd_twE}cW#nF_4W8}UN1FvK(#8yywYK{eQ)TIUJeJgbLssV6ZP<`uJ z+4%Cr-L<3OLcPcLV|2=_ItEdZtff6z=WbQM$W!`*i_w(dVxr@Zki;7$;e{h_jPMyP zAVi~j4k?UjrYbqXrIJ2NWyN0MLf}f{*m<{^^C}7}$CA&l0VdO8a`=~%K(9q~mjWU& zV5E{}2YLCT`XfPpL9&+deK`T-#PI?{c)A8jj$>Usf^q`ry?I>g6NM{t!#T5n;a;E0 zuZ+Rki6Q=VS`FAQ`czSjONO3+=fWiaon@^6a$X}z_;|JT5%syjHM>q^OCZK^O?gY+ zMS~Y*tc615;rLO;EpB%3F=wEY$g2l6lF0RNekau8lThv z##K?sDqYI4Iw=mPWj6c}cuw){y%m8s@qxjxm4r3c_lb{dKpCuAH`%uZ$?0Y?@!q5a z#UTVd(YB{Rws26E9PD*wbKPhUA^S#O9TH@it5te}{_ zP-r0bM`S!n>Lb9eLIxOA6kWK86BK2n3#0D?!M%qia;U$9RbHQ=rQ*;W78Lq4e7jX;bk5e%OE3i!S8_(o2GoU z6X|>gDNwHfNyo`_N3v0+{!J_dhceMGGjwG@v0Kc)D*)}j(EBEmI4so!;JX{NPC{Xg z-rL(F397f0K~fY@|8AHt8Oa03#k88Dn51wsA)ej9m#p~~U>Of8shG}k-X>b8yOL%o zzI2p|`2AWgdaHoT5JB%)fgV%wYs>n?E93Hus|<+fMqilBP2sMw7)KA@smh1q7|DCY z(+e;7Huq^Wg~m4i13O*GmHTO`L;k=RQQjAh6CH*RqlvY_V~L%aV<@!NMfVc$aYU+W zb+PS))DSPMT4HB|SvASlww*pNDUvVpF$EjA;k4u)KPef@294)Sqa5oZJz zY=!D5=DSI~? zh86@ljDq`5iet2t3sdI-&p;nm*DkxmMktaTyYZ68mdO|o#YJZ@U`I`F4WqqW?E#zH z5wz!X?-RZQP)bq3wZ;Ylc#Z-h!P^`L3Me3M!Eihh60=QbF5^2D2j_aiJ61KklGx$7 zQ*sxIaHHo-u#eQ4-(GbcQq~%jFsSqQz*Dsz@_lW$_G6K_weQ7VJ^HrCqcMp`x{UHM zQbu5hjCE8yL3v{KZY=KTW9;q-7BTsrQ>Wt?T1Od5mgQL)-*iCZL-%AOc-YE{X+1cO z_@KGPaL4dhFH?Jt2s{09N5m7c!=h;!Fk6kMN8LBE`*JeQy%nt|S$YIbu&4`A}UXpsuRYr*ib(!X+N0#{Pt<~xG|CwrIxvlZI-?s|W)O?rT zFtS^UPaR*Bb78*VD!ybVXuK`CY6J~iDuiNb7kiT9B>rQ!l-^3a>)0q~kMUhsaMt0R zawIb*49}&G^;X^9>oJW-uGk{l+a;`+{(Bh@GjJ1G1v41-8j?qo>g88MG~QAxnC>2M zfpN~*iNxrW8Ypry%#LHHvb~_7t9a^Yzv=X@wotClo0Nctv%JV;<5UmNj|Wdvk#^hH zhR3#cC^tR&6~?*YtId;n_1j0lAIZ z)cbi{FQdvQwa_A5;u7Ji940q;;~Ez&bOAkNL5ioPox%yeM=jWsnBbz;muyxN&$Gt` zHKEjtW9ZSuROT=*IFJ1czd3SYn{fR=Xk&JcqMVw`FYZvcz)1m^1V6gT)o$Vkd{o$} z-nY2aS}t{P<={;eY@uVFKgF>?^-FfN&`4g6zzoT>p7w|?2dYb4Ev}H>OTJPot&8h8 zxtFcjP><2>)<;PW>^xg?%AnJFMrOhMDx+p| zkwVqKsaHNO)PYf`_*m#zC>ELu^a+YigE}`Lxk>Mf3LV~62`)FyVB;*$C^K~?PSAO1 z&4i~7BGWQe>Gex({I5itdYy47WzoZ^&l;po87UwWd8F1?$#vD2)+ncS&mriiz1fwF zSxf2jG8|<$uy#dB?^W2dTf%c8)VK za}&Lta5qzV4i?MNJ8OQZh}%(j?`jWef7i5VN)=ZJ6HHAIa}x=y@Y`9ld94F&yQwND zvFY@OpICJd5v6~meR-&31!j%-DCrg{?iuGr@}hU@bFnYNjHDu&)CxeesYR|IGkgoo zG@PySWob7@q9BK>Vb2MPH!%__y#fKQpup;WJpP4lDJWN~&a8z1e1{+oUc^ECl1HHO z<0Sm1_jrOf@1+RmMc;~ei7Zy>5|?&6mCcgIs{2aA+B%4RA@ZzywSO>Xoz6oi$bMnQ zo^&Wx?>5jppg|q7OTt6y%+-FFuL>F+Na{FQ@Jgkg5H~~_idBtu{dS?bU`q4;7B*@? z(?wtR11KrHe!sN&JV^t3jLjHc>JiOEGQO;0MgCz_&%Hu9e3(358C>j(*IdR^gqSJ0 zuV?)9aEo4~V!O6SBbwLqadYXrnoUz~`Zz;T6fY0ud2yLmIPotrwtZ#xpM|Zx&{ZoR zQ8}(;ibG3yKrOa9eC^%fSgVWs*!+A4xoZJyl9U!CU8(T+XkxU(erRFEzXWAtF}q>{BReJw?qWYRXP z0jZDx6?PXVB>P39Fro<3tKiRykY3PwUyHylovKmKlc9jd1IXp#;A*;0=IxUwZF1dX z+&Hf}C=}5ooGm=0ityCswvqyt!3Z1Oq~Fs&UQtzKDPK5H=CRhQd^`6_y@wbMd#ZPG zC;q!f3g?E*n-~%&2P4Q$RzM+Sf*c7l?~VLQK=*&$^FCVlkO!!4DwM>fVk1 z45$Sv`^W0b`v)|BYY0Io5&_9ro2Y81zfS^m!~j=JB2MsNNC%|Wr_c(XPRe0dcr^6( z({9%FFp!2X$Ys|xg$xncdb%k?vNsg@UHl$Q5U8vMf)D!^Rnap| zMN5Ijk6?owui7>14ESMn;jDH(YKZtc`^mX6;#2{+v-9ztchdj9aS`}v8K5ym%~*Tv z>-Nn(H$0NRcnVc0TqQ<^ybQJHTBzew9*N5B^n)?cE2r=Hj%|pGQAQXhzI19mifj2} z;QFc&F?%EYO`W#;$dN}bc%-7b*vW&PB zyunZPd<-r23N2ts5!mXMUXH-r&M*0R)djuPlj z4xY%-{54MiYTfglLYpi6esSC6=}>1%3mG=1x+Br zVNjF)?COO7A<#jZTNgB;HZ&ZeF!&y~AXi_Ys8;9U=bsK=lZb6WV3enljXLtjJAK8S zog08Pi1V(g0~C$=miTL{eK3}Uh30n@vBToCulBs5_WFe5f$oXW~9 zAh(G4aGGOnDHK0~awJinbFnh3y|Z;r0?xgWu$31`tJ{5dVcGYF5ByP`g%k1N5d$lc z6cv_{ge42})k~zJ9AEBVM|XJZ!!yl1%3o7AH2KZTEL0FGrLQe&2Vd+;9qgIy`KL0; zS(E0nf9#vFo~E)l`r54j-aZ9hy>|pRgQE;HcJgt5+BpIE3n>OMcm5a}SoV2C^65{6 zqhfohN}dPP1puh;d6q{Psh4va+9Vbg7e|T}*L9vXdG)X^5lfDg1FApQvR}Y=d<=?t zy`uDGh9*n7!h||5RXG_28<~fvJ5Fl1RHOV(yKH~n@bY8C z3mJM4iGR0p=u;1qxQ))4p4(Y5>?u(w3y5D%y((8On(qMg1+GwXoqw1P`k9xaw%}nB zQM@cKsw<636>MM?@{yOcEOU%~c7-TcCKDLsoCtzx$RB&x6}D-Bq1!|(ew27IG+>Q_ z#%DM+!RN{!oa0U0cG>5HDjwp-F=qSe)BSaE+ zSY4zrQNd!{#bZ6n>7o+-NS->Ewh&7j$G%eiMUr%z5-az5KjF4k-6&YkJQV+ww8^oV z6PjQNWk%x?Vid4ghQvY(G|`kLr#_e4~gla%=}q$W6^*Cn@V3N-vaWVIPv-kF!`0m8zwcm z_ak0&t(!nKl6mOw5e=I5HuXbqC;$ya^rG);?f^Z=i6&wm_(+xD7%+40S>S0`VK!AD_@}FC;@D`E+2^aEjOIJNl?e9u z43ZTS6j$*$bVVv<^%aTR>ZupomG(%=?EFIZGch;VoJ>|MT(T!@h{k!SZ+*J;4t<#o zAPHmfg5LN+1`rK=A!s7<7Rj#57_b=EItSqp+vbP3NIL-Nohm~jXFc*6BeEt@dEae2 zACNc4WgG&<(9?h0h32Q?K$|L$GbEJEXNGK6GMWqCr)nTgF1w2ORx4r`F7xdHgU;wz z^7;X$;OGOvlAP723=|@XC3~=0^g9OY3xd#a_vK%jrX>PsrHs@Ps@peJN?9{Fpu*n^ zqFgQ2Ij}+ZIU@%8OrKWZ$#q(xX@|>+&+l)gaa3i9<@JH#EPn~$VH z7jEU;0L{2At-qJ2z!2I7GJ`=n!;r}E(~%kTUacLQ=EH8F2vfz&ae1!MGwr2Mb<0WJ z*fFQ#T_n4+gAxN#V@(DEfedn>KkFc0o&fA=Y5luCopD@m2Dk^Nr3 zThqVOf#Sc|on6ayUH;RbTFd^ieYlqcuEMqvtqYbc9aK5E5(9D!mRZTQU}1F*p}ByB z0aNjoZNU4)6MX3zeKL57)XOMRSG#B4IS%PZY5t^%WfwvKlqcD0BlX+bP zP6DS1K2`=~oXGx{?hgA<5++AZj-8eG^Ebjk>y ztFsNqqeKo5P&B-djRj>y6#-Mo)UrH3Q4cQl&kayhWnzU$;Xc26T<=WCzKPdDivZK% znQ9N^*xj9QuB zemo;VO!mMY3Dnn;pdB2&cFOOyAFO3o5VJitv&a|k!^W#8_~?nr2rr>7+JE0jv8+%H zIpBnwXXBsK%FQ@`1iXtpiYmhL07Pr@w~3dJfQhfPAFt3Ry&v*Y6KdEEWZ#aHCXI%0 zTh9$}chi#IelfCj{b4eDEyMYyhE$M3;m(S++y!@kGfkqOSYIZMWNK^b87#8^ub3g? zwb?s;MM`Tj@=*A1Hjo(*5mgUfzd9b2sxdYlek}7QD&bU5=GPa<>5BW4&E5>NoS= zII5{<8{~V7E2!W}FPe>*I^sa6t31q7=LNlrRS>t$z`sN{8 zr7I*ZVB|==#-LM&uk$b{EIxs-?5U^YnNlyZ=(Rk}8Q4G_^0=V}(h<1f8CY767l1zl zO!;gUp$Kb1oU)gMNV^?`Y${R&muCSP{7s;v-VML|x|E*SB!48e#mZ*=<<4rnbCp1De=F0kQH$&LJ$>d44p~D$opl36%dt(Bq@31^4-H@%P z5o;6pd#^X^WU0(7CchQA8)Do1TMxTz-J6{YY_~S1D|G1zF4w~-v3(2FPTO88O ztQ+M-Gv~p$kP%mZ_?ThE6lWKCTkh5Ue?_LWz&V3N{z(JJ)V?<9Hy zkC)kRhC{h_eWBVA-N0S|Pp8YL$XK8%qGVHFn2OMLxage8hU$p2rZ&=RA~m6J0m!mG zEMPY!9zG0f8Q36ypn(7N(`UH{vrQZ=KRMx@Cyp}8{A@G|2(T+zd9Y8G#=Xsca{)u z%nF;b7s3j?1KAz2LI@!T2bNXQO(uAKx(^*evlQWk;mbp8CtsYGYT7#9=qf)v+swEw z*Q$^G@al_Uo0Am=68!9qOaa%lodb(vf8^i3*8MGCZwu-W4LgicDG2=Ts>wbCOSL-8 z5Mi6o&8YtIxTRuL)=zoxtCQQV4v|}e*Tf=i>1751UF=VfCe3oH{tG9D8FgVr*n+V4 zHT!~=NByTy{(&%1N@GTR2~Xc3&c1X?>9oElNLf9@RNpF9B|!ghJ;TRyi;=K~7K#+c zGk|zob??vjB~3k4UHKGfwOHp!%)1`p3dQDzC%_9XgjqtS$-GNxIx^#pGyH=++YV!m z-{1Z`iP5wurH93J;D^VmWbRJcwFL#7bN;OFfijFS1Yld5{9$kV+>J&#A+J=vWzCgw z1PNxcx?tM%CfAZVG=4E`b_vCe?uFonBxIS0BSf1fb>CMf%S?FunK&*>D0z=lR9)6? zqJ!$|_ac7PnK|jH$fb%$o8>XIe?l8FfpREPNVG;jvP z7az@RRimR&&2sL>Qxf;}Z4y%t61|td0!=#fWW&lKlBR)hBz$FK*c8%lU@SVFG~RI8 zru6N7gE&#cI6_eGYJ5ne!~)TthhCK}n{*{x|^_@t{R z{7|@VR^BVBpITBmQv|{3aeP+I=f)r;(OW?T%LyTE&vg ze7@pF>zn>$NEXU1Nj5r_MT$Q4hx$6xH&R$Wu5W2qRlU)?LtS;OQJolszV99i6Bt06 zCIO6ZK@|$DbgewgPqBKRf)Fm~W8(uVgH|+~zR>)Tnica@o0Ba$A5rHDC-Ja4uued#I``i=yx_!}nv7&So+PWfFMZk(Evs zx#9&C8)j|zp#Q_vi!&NRqci$@RWpYBtK837?^k=mYPGKVc`vo^16=pzSHD2-N=$xe zKFkXLqWA3KKL}?~Vbd0=|4YZ?^pWzaWRz6&bT=l_jg|B7GlI;1Dtz?A^Z$9?{{=x0 z?}u*xe@E&u^fq@_4fJ=(A<4-&+mGlh)|pQFG2JUGs~gRSxVq9I6GyKNk)NT^GKGWJ zQ8+oM^F_>Mrsr0Fq)9$J?hWfdG@Qr&5od_(NgJkc)r#w?op;^=#biLGxkIby2dpF{ zXg%zMV0XaWJ3*a*tIFk7ovy2n-xnLCpl$Hxxd$y^>ky?mTy+Z_{sYgfr;&92@YdrF zpmh~FT#vAy!f2iLs1=#_cx*c5go!yx1>Hr5|G+Z~(71H|uZOmJ0gX1ZBXE^D-4m&6 z&_MYd7Jz=hN&;y2CNLt2*D0V2j2shYkXjPKOuIef3azAi@(Haa%sW~pXefkgK|ZPZ z&6GUoswPGlGY@j7Fuy2rWorX3Q6M4-(P+YRG2@?nayyu)~_6D^&q&<*|=%$9%#R?HHZkUCnb$N~oUjafu8 zxVdy`M!C+Ej4Wkr$2|rnj37rUd>T})JH6&hU-LSZK?+>EJJ7<4V6$NIqklw;IcCD} z1s39Df3|T^W3i^FRaydxYzZ=EhFl5CMZvLP4`=iNVJL*ih%@Kx@|#HsdA&vkC4q<3 zZAOjSS9gf5qINM#pE;p)hQiGr6j4@*B#i%pO1Cjnuz}8;bgs=C%>;Opl32drq^s%)yWug(nM9so!rBf)3l_x&bRK@Igu{UwHT$o=IN_pt0A?YM)uLs zV&Tvp8P89@M8hS>x^C3qAl_cA;GV4x8wJABw8yje5bitZ+cG!RvJRgn=o|Mk`7-73 zsrt5u!`0q_hDE6I)r~~3VXXiUD6L0;Nv^{H!`$T8gLX887#dz9TTx}`i2wsGPJX)T zksm=A@8{ArA){n__;dzj-kPP!f{AU^zB zeL(*Fp7s-#Onp5Rc$XRyO1UdQVcSW8Sdlw;x9{2Xlo01ZPi=j!3F(B^#T=V+*Y(1I zd?4#U+0=n=PQ&&3a+BV(f;NOjC=bRr@xVLSWvOr9hfdQUoL~#^FPWpQ%zlMMOBHH6 z_Lh=;?N(Jssbi8|6*vJHX$ za0D^FuF*kF=O*8?%)g@AJX{8cZFJ^jQBmMfszacm9@HyA7WX1eLSEy=z4w%Q}F86q8%zkq&W zDQMVUn4+sR?SYeJSB~S_z+X1)lH!CyWLa#E8)`)3&&N&H`bx8D)8KRG7nZ69p*~j~ z5&241tDVN&yz19u?kjLhPP?v)jg((`P&@VAS#|TQQF)t&iEo|?d)4gVh{<*$Z4dja zE|#-@a{{s12klAhpi{pOm7v8mg_x2+o=(^1N6bf_@>{gJn1(dw0en-IgbsQ0@lO|< zvR#?!r|W}qze^Oo(}=Eag~@WvL65O4#y!|p-(EEd#bIrubS-_Ae{T)S+()fjMqkKZ ze%{%<)mdu=VLdLiA?r&RI-KMSclE_(6k7>1**= z{w%!cU3au{uMEV+a1i>(UTolN;yZJ@i(K6S7oOdR-QONXmQ&X2t5O0eX!76vokPTG z`~&!kBBOTzlL`vAy{H1XPqSa~nzXjVAO{5H#!B~7vSIATt!jHfkF$v1`vM|NhZfw0 z2y{&P*Aa}A76@0rosXEyT@s44cR!8*H}H+s-+a<(^9)hnXub^9VbX1>A9OK!pWMHR znG4l;zOltGSSX*gNf6thF=8L^`ZLZ0%}SmE5t$0jkckO51bidyq5etJ-#5!aE$*>z z7pmU~BjuL2n4?1O2~PitDOaTOV2}#O7R(tl(z?3b2n86Dv?Kc$fmAFf$$QtE;Cqm4 zuC?J^#r1WnM2rw_DMA`w!2_LI0%ZSZK`?@#XVqlVr(?Ux!1g>Q$KKC$m@4YKTd7_W z@$*UQS`$Mu+yQoiu)TNRNA^ZjB~0yo#mubbfe$Sala4b*)#%DADoQ`pV`o8vL*Jf( zkS)ko_7`e-x#VOS0Kji-_?_p$#Z4Y~CeXTyUw?vTkSC_kvJ<$1jlhnZO_N4XGhm84 zI?ip=BSVZOu$(@8yM_22-Fs!?qKHD`FXzTH57K#2D^1_%={;Ln7{Jy&GPNhHnUcN+ zPuV?Qp>Z>*c>|}NDxl=}w@idde%HH*m*WRyuHD8ZkDl_7Ocf!UfU&Oxt9Nc53kexX6eUQRzE*oGV za&)&j2jtg7kCMYxK6M(t8bgU3hFR2Iu&PLkM-1wG%5KT+n#|MjUb*a5tq^ z-f&V{DMtu<lEKCT*Fa3upP0P_Eb?f{fqECNqdIU~!R%<5FGlBxn8f7rffbJ0Hf8mCsbY5ewj zEbVu&Th|KRq5xWr_IUI22J!ygwn8I@d;X}Ux($9c;x9~v?A~{8uMWFecCd>4?obpy z5=Mj|BNKy;DqIJZjMmnX-*GYcQfBjg-U@f#u;?N+m?xzP@_3Y8wl}%jshfABS(A_Q zWH=n!a3u{%x&<%aHUu;Hg>S7E=#YbmK(FE=lR8vP0XiujS|hOm@Q_leY_dfKs%@8= znJ&c`p<+?qmlqekw7%g}jF24L2IHSoaVQ(@US@#c1- ze9w@h2vkkOpL!G=etFt%17j{k_Fmm-3jBc)i=Gen=?PpTGm96O4Jc9MOBhYuxvfGh@OsK^+erJ`FJRRq~&{yxO!p+ghVI+q}?(b2WE0*40*lMHht8sQVO= z<{XOXj!)6`&D3LsdHn%hCs1yMK=(N)Yo*0e{B|O zUvZCN&tC-i2ZVkCj?bjp@}w90+eczpo!!X>Oa}TC*`4RfYrhBN^yk#* z!0dq1cwIM(_ck52GtwR|w%QAcG|PlOPuepfJ93s(L-CPfukrXx2N>V}Q2sFg5sBBzt#EYFr`>`_*HR~pdS!BH0 zzQ*39tr_+k**9`URy&-CTeD}Q=q8T;0Wchf&gLJ1*vW8c>QXe4k9kqdQUOMP6(76B z%lM3q68}sW{LE~S`w|yvLXFFCCvet^oupUp@n{y5j`x81P{D2yzmZxVf?N~6Rc#EVf4a?mH%Ah9?UVk2U zT z$r(Bzch2P4JAzqSbUznuD0QO5Rw{!()lqRQg>s2R@3oV49RD;zP)mwyu#@D4sJdhy zWPrbDgA@U(BHTl_^d!^b_`%qi{5D%;R5X|7-6XBIqO_;^8wO!Sa!XSfDbg+0CHG*Z zLOb>nA+h-|S)^VloYIzqo_s)_0zXnIIw$?WH`6ggL$np@{x@)HVsoNzr9HQxt{kpc zvn>GtS+JD-jIn>)yJC#3L*RN zHXPlHG5wO{poY=I^6RDmC_=CJDhM`A6-NzE!q+rW3co<0_+pNoo z15#Qy5C1k>m&KWyP4#wgv&kvsG2~*oUE4Y==}F&v%b^S!iF?i2$bsP^o58g+RnA5^ zm|*&WZ=}`qq}-?J>l!*l(5m^89Jjg-&hLSoZC%~1LQPD^!|-z2(G@*`BMis*UUUx# zkGP+qT?}{)5D-*6T0D=97MpDAxn<()=~@b)8^#15pX5!>7NWu?>lKKiJZgAWmHpvEb>Pp)%nEnEg{xeBA%c8y97_~kcm!2N z#jf5`b^7cj`7Y0n0uNzviF(PZ{^}*8lGq@#@MO6(osLkn7qsH%NKkZw9sJ-&Pyd0F z#&nIH4@rj#N>`~k z08N*oQ0_N*phMH{IO$dGE%3H?LS+b4U#I$-wMs#WlRiRe-(C|&0eai z0;D8M)P7hyMY3IM%1H0u)CCwFI0oA<4mZ#i4-kNwE0*$C9?zF+y)@u%$iZ>8>zygy71 zJZv{ePQRX(Ha@RMy})#UPb#5RklL75{U>$Q5UHioC%vnulzQmO@AY*AuE1_AJ*AlT z;M&1Kyjf}8C^_;?%0v=`n*;sRBT|PH!!xfvA(u!crtq5PeM|_XW~t2&<40~RIit(h zDBvWu>LA7#0OSX=r4kSRukFY!$g{1d;C>ihInvrP(WTCU&C&)hZ7Y$MKv!;FCy!%x(<$Q4&)t6e@^o`OJxuqf zYjlKL#6OHVpEPz-|MZBMC2x3!z;>miC{whW%Eom7X}`rwBhuSKr!A)Ph|xy2i)p0* z)opQ$t2R+muW_Vs!G1k}`nsU5XD75{ykyZ%oLH|zzlR%|%+_->493ghJ$55-+~rJ+ zKvbI)z;S9 z>A61OM^`UX@`=ajik%wYUl7;gu0KbQux@M?k!bb2@OpI!b5?9<{lxgR#mFJtIHRtY zzoH`aSDg1pBz(YhcBoIXg|w-JG%KRD>-8lOfUCECV>ptwC)QSCIcorx$`@trz^{@AWpj|(6;cXq9@&t z7Py<5%z^qf$$w6FxTsOaO^=SOm!~DC$kS6FP%@3HUp+e7vb)i3b#Lsd&7=C3#a2gw z8w0v*yO-z_otT}>uIGoT6z4oh8tCrO2lMqtu&fs|@7H<7D zwrEoxVRdEp)KOBoIr96{sE&GLh1co+x+=BW9`BRpJ3D=&n`0_186tzoCn4HLYvlfy z{_P(S*Z=&7o_{xg>gV?_j}JFjhaEcObZ60ZjWK93R&7FDOLbXKx?k@+n?5jXH+<4| zn#9H^rAA!^ZJ{w8``{@xfs0l<9r`Ir(;ntHnmo&JP4zUp%+0^KSglwq_r3A@DNv97 z1&Z~tRY4d5ER|^_WC~HUZ?+4o0!@QqQdLTJubYk0-6ny&{Jk^$&Rhfhz)5>DUZo+S zflBb5CE&Ltgmfza;u=k_6+!v$8tq|_kq=@0EfOfo-}BygjOVwTc?j{^KnW#;dG&7P z`Be6vxai)or$}b%v5bJ!6IH(9-`RtAozbQ8H}H?bvqSdie%#{M{DI>1N$g}WqxHM1 z?Sp84$>-jTWR?t|7|#J@evu1!HpaC!nd{hLU4t;fgcjiRWfQ46kV8HJtl9=Gwm=sa z^KaG>v@i2zwctBeJBzHLqv8SM2h8s3GLeNcW}V_3A*>ot9tyhnxM2lLdxHc8D+M@) zwm1>_O3+Doj%puX($aGQlo7(b{Ppl+R8vX^Q3LDvrt(zqOL7+KhPvV0HB)8R<~F&& zpbK={cJjl!ndqN9lZv7LFe3)mgv=LUGXu`+w};+P3{$K`s^`$AS&Lp?-Q?$5l4CO0NX9pdH_^1@mG zn!YZVkp^rZk)3H! zLSeV-oHXpMWgb=d0dD5Gny;y0-K#a#h?F1iXA&M-B;yKpbD>DhtOIPRV<5kgWGc3Gz zHF;NWS{HL5w+ruq5x_F-v}HUWK*$LLiV@ON;Z*D$fg$s(e~dl#t-Zf1GQCFl-_8i) zkU+dyqvl$CtD{=U2Brq>j%*x`9GH3?{Wa~xk>H1;xbE$oA^{3PEmaSwGMqEDnLjv_ zjW&V$JYgCXBR4pIF{(NsoeY^ORCN)4_D~U)F?_+9j)5Qj9pjQz%~)&a{nF1~U=iCq z;N{HMFa#W!LPyZKqgC#;Cqy`$ZG{NgAFU<1xkl+M%`UsMyVKAfM<2Znf2Am8sNpTr zM-Wf4T8woQJxx6k31_xT$8&(koFT9hOUL-T0X`xvmcpj96@36+lW*P5^R+|svoq5p ztiB!+5n7{Bk#NFGEr+4=ESJa6(MG9kb3oXkdaRN5E3D4waeb#1N)TG>U?ZQNIRIru ziulY>SJF}-C0Ql8wP6Baja7)d$oa`I{B&10p6Nz+a}Rf@_O)BdB$dRQF?0Rl{mKlZ;m6ZrPUv9rmaxiVk7oEU}}P zPX7CUe57BR+U~XR%}cT8Pa)C5C|-k>tf5R)5ON=73mn5LQ`ylpJ8YcpnNzWJ90$;g z>mdIXBQ;O0b#X|{HS!n#{3SZ%U{c@PUQ)g3C&sqbwP^{;53JC7nS$S-f`jOu20uFu zi{eFAfml~#N$Plh;s-hJJF;VdY1yPSMlu)nPf}q3C0J!VuYX$9O`gBgayfs8HAfgx zt@t@;>R@BIGGM*(HcqSu zCHCZ?E*eN=KH?e@)%z&=$hk;I9L`u1shKW1PgyP?dF7^CymWxy+e7~)ZB@dgsnByn zSH_bDthQ?MU@nAwjb*IUN*`V>pIAg?Frr=diycEi@N+&3_{U* zoN1s;;CAB<^F%y23Vp@H)_m5=Uj9iHX9uMTe024O+JgmqVh+hm4{@Ou23>z?M}pt? za{0&BjBdg}2tu(#?>9;PrP66Ts)cTqL{eGnW0`Xpe^i}lyJxzk>W};btNY!L(h{9= zvX-B3u_pOWf>#92_$x7_u|!~KoZP(EFYD4%%96nt@H%(AbuSraWd~i|4$c}sj&2{V zj+Pl%@|M<*eBOv>B6K`H6n1T;okjk9wglY8-aE%e=oxcf(0N(#j^bzLJK{v4?00VTfpj zu<6`gW5`2dP$r?`NeQ2ORV-MxOe3BK#7ElK=BgPv}! zULHA@&GG<#bnKHJK%J@n-V$!!%x^zmfB8pCOhyeUm0pd;vgs9Q@xIk2=WHtlm*CD0 zY<~-g9Rv(F8&>`496|+BG!odw`xZ|*XZe<4C7v4bGp5O}=qG9}`}uhcPXAi(mTE|B*{hK7zCUPt4^< zHuWervSAkqJpC{I84Ld%8J~+PCK1(|>Zq zzC9i5Z=*8pQMxawXTV+}@9`Vhs`*wN-9EM4|Ir9dy+bMATh#h^c6@)Yb)#^fud371 zg#*r;4eO5aalHH4`fSg8ogH*qQ)BJJdg=MOBX_H+;(TrusMF9X+35l8mdQu$P^Qdl zR<52mqW?tu&6IW<#JK+cX$DjYR8RGhlqOLP)m-arV)d{*Ii!%-$b$?m#NpblQskW- zH$10*(tr3cUmrrZH)VqbP5{e|I3pWnY~XXF#c zcaI#&>Nk7f8I}gj!MiNMW8Hx3-mCP&6fl4`cmXY^&olABQC*0^SI4S!ONZ`}9ips* zo6yn{gGY8(z&*Q%08a%&-#nHK_Uf%v4U*SRfn)kA9qi|;*j6J=$78n$z;>-9sy(p& zgO818HUeu8x7AU+RAmYF=ZRDDjbNWZ#RcB<_XvXQ5YkzEc)KLItck$99wL0KT080V z%42<{T*zv9->~3q3Q!zn%&(A9!ZRZ=ONdStmD9)>RnibCEfZ>>Z%miOOe>WvHADk) zQHeYiJTv|R)Z=pn`(PgeCO-iYgi7Mh<4f8EPLNw7xcErViUj3a5Wtp-OtvA z(za#DVNy4&6^z4dhzcQh3iJ&?MO9I!fY8}o`c;!nVfL81OC=g&-bHQwEf zaWI~}%KT&p!(_w82)A$Y1eWxvk2^CCb7;EYo7UgXL~371Klk8Z@Ukf1E@beiWP&eO z^EUF4l-o;$&WYfDX&1UKB;+-c=&W$j4)J(Wvxq8M7+3%dT zmKY+Y`q3Bx$gq`95O&6Q!tW#Rf{vZnjQbThH}^dv#*_}73y*=ur!O#49R?fAU%I<>ks!yszdViX7|0X0Ges-<9T21- zkMPQe;(vhJyM)e9T9B*-IY`_;puezj`RK)x7Ua5b=$kz;d;{8 zJ2IWkd2)xbfwu$MWb}U=TaZ}S^vXzJ=gUKH5)3$iu>bry{U?iSOr!PGokl;%n%r?I zpR&4$k$V4iC?PSFkpMG+IcAGGwe-nK^8_tlm;=cY&ra^|G0#ulAw>{h5@xVlRb|02 z@^-su(mVmK;Q}83Mk!G4tp2*DXBir{_@h?j*Rz@WtQ=5 zk809PTH7}c>0T*%o3$k$ti3CU_lh{1&%uoPKjRc9zkPanvZMN~VK0bbgYvll5Dt)} zO@mD4v4%A1hTA;!B-u?sDu=ef!%hHPL^MRJG5Wo?F>|=JH|SE;V+!7suSU|jGST*- zFLgAXiapTBn=yKrpk2Sr`22ntFV!v)MLeNLF#sm%zafG>qgT*|sg5y-L&K+iY@y4= z_)Dl3RtCi8w|M#VbTaC7DtB7e;gh)%zP|$gd*0%z`qxbUci(R~67gSqiTG13`b9$# zSYMN@TU^B^!f{awERBo8Jfy?AV_u@8bPm!9;tSrg4d9>nJq+7 z$z`q~Q160*ra5io{Jes&uN`e*1mOk0i@=yXLi#QTZW$~pD4z{Q@E=8h zZwc0(=As)And=!U848{xuyYVw#5@ECdOuLS>a|o%NUeBB?N=^LBDJ9v-!1tw1Z2 z^4J1Ffw3A}wQ)%aH(02~I1bNpZ#G!b?jbLn$odLVH4h<#y|8HOh^vHZEgs5JynSL- zvS|?zjOK|`#mMChXzFOh|KC>u5H2&*ekJR3D+I&qoj4(Ev0$ z4CS~Xd18|~3)>IlLWv-JYI?v?@!)Waox5UbQqra8WiB3~_fCdNVDBO(nDj#EOTk;# zpK;K}*zmENatgP(nyJ#eQk1C{)nKaREiSQpUkKGk>01ncOT1)Ia?+71%|6kf04(tb zp`lp*V&8m(3u&bA&mJ;cc#NQ!W5 zq(PXopwPpBdQ^-@u?Vv<4B)3x!Ag zZ1_C4?LCBv)XtUPrMvQ2dT5o!E%=b86H*NcP=q#9kmMop=dwJwr)G z1u&4NMxcH75er*IU2OvrY~+SC17%u+-^H`|pf-2%z8(!$y5~Vb_Lgtvn@_lo?Jqf1 zrOjOSgWba3`a==bhe>X&oYnV9 z`n&|X4Km|FU2h_R1N_#{Kir{r-oAVoiQO(RR@}9!b*-EbU=6C zF!}4t=wqO*I6IdS{yh~FQYBd2Amr7J4Pmo#<*u0s=P*CIiBvw{p?_@04x|Gsi`;X8 z(0g8`57Yz~ZX`4*`qqLR`4bWtL>!Mmge|fpVw;5v*+-EXQ{V|fSqv|bhkatWYVuRg zwTtQ1RSa-xp|4nZ{w~)pM=_Zv>qxS@1sH;t-p@W~i9u6`wC9XSWrk6T9_>ond>?iJ zXMM_{;~Aa~&tL4t&1)K|l|tlO^+ZR43HwDoyPjegD#zSRzi9bktiD`AZetPulqn**6`Q%J3+pJ zq00=RZ;)A$RYQ>hO-9>I_j!|_FK1iS-1DJpB~N2tuu$F+^K0MEA)&}#CiOSQqP#=N zxT8vt*XYbdE_WVx5-pSokr*B=W6LPB#ZBm?M$NR-kwO3?h;=EgZq7Eed25QO7p$Y> zf91RAhwpD(OW)Vtlf91910#pH)*8W3%-7T3KXrB%HiuiN{^w^;;GJHseXh_=Zxlhx zT#HX@_QaN6QRuQ|1Jlzx5LUC)glae%&+R~T9LUkPV3>r8O?tSHa}Bc*`!|X4Ky}P1 z{r6LJdmq+mABs}q%B9D!)enx&Svo6a^PMe%D`n^l5OLa#D9Yu-6q9d#Hbh}^?Sohf z<=|aWp|6f+ie2nNS4k)0pPN8zP4~kP_79o=K>mgVnC9v$tY}(KfzqCaylC&Zn^|-HmT% zAKQfMKnuctp8u8&&F6~I0!AXKf~!J&prbM6BB?F632+B zQ9z-aREFn8EN@B()?0Cgt`0%^o0`n8i-}xBl;l`$arb@-YWFmyQh>9s15{{>t^oyk zMms&0hqpueF)Tmvu%S*CL)FB>L-%_I8qm9@`!cxM>or>7AfUOa$@DMa_;Ez_a1)b< z@16kfyo<@-2sH4>Wq*)rmEY%G+I3LYP7?Dx4e5G zsM}&zB#coz*944n10Q)Nfly2Jnj~Q!D=#{NnE^Ta+I0~Fq^fAdGEVjf1;mm{vL#&Z z1_ecs#qu7h%@InjHDdAheLPN1)ZR!4bXuq1(7(P?SPd*h8cZ!Zu|&bKrO~CEUL9*7 zh@wKefO?x3TEt)%h6x9hTe;{|Iy?jZRu>;cQ;a5Rmz>S(AcX?Z^(V&O2-o18Hjh)LPMq zH)^Y4hgryt2a1=1N3K%fl-RVP6-QGEkieEX$E>)fPST~(VD`+5pld&sbBN~5@wYy# zhHgapPW7Swr0vxp6Es!`B@1Jsd=2Hk=_>0K^lhX0K>Q3~MHtg&llqYGac9ErJ-5Wa z=%Hwmq^7nRUA@Ry3#Su81X6FXP6e($MH9wXk6nY#31GoQFnk+EHY1e(*M9|z%VtCW z{W{9>|Ky?fCHL;C(48L&VWl~{?v6fc{a zKbkjHw1fykm*rX1X6tr(wf474)F_jhjP8`XExk@d&$Ikp$jbCJB4wj#G8gS5(?Mnn znir;~#pxpy3)wVGB19~d&tBuu>e`x$PYEY%+CW(-rQ}&=;!N?iFX;<4LmKL#(@*Zw z+=`x_^sw&e_Hjc7j7+~FtUv6SzqlW;UQYtYKf?6KUoCh+)z;}}(3=`?SimB{LU^4) zCzWv7d|k*e*Ig|&C8|?!Wto2scIwuGO8o&iY6!@*chstRiUq5P#8_1m)Oh`90>vM! zd=N-fVbH7rGRiS!&n)DpKJNdBtHBE*O;ys(mMf={_W$z|<(ft0u1et%OWA{WLiY&}w|Vr()RW zf&dNKJlG05qk@!1i22DP!+5*jvY}g$7_?J8TiZUE5AHlZ*+FHG4#?5ny4hVJTl=g> z`044u2>JGh&JPskYCRxt=Hv~bBTFy>c{IU%Z+>Ics1R=|1*BV^vW(D{eQ>3sdk0X) z(YPv&+@}If_Y8_?xSpaPI5d6#dh5Qw(cOM6?l^vxI3kMT?V*?=1;_;4QH3u@E7|dA zId4oTW8EPb+gIQ!lSQ9A91)DHP5LJOA$d%YIu6C7zHgUu$TI;xMF>hbD7YTDWk~6b zw;%-6k=Rbwq3dPn>KX!|fr(}jZpg8{^Y&VR6C87d_ca>I_V*Enn8V3Xe4cb9mq5(3 zvXZ@nD%p{XPWR){3Eomo|5^35s7zj!W(L4!CfThqK*Y&?~ zHw#OXi0&vMGe;8WkUxat`mm%-1~S`>~8AJG>S0wpPXZ^DYG&(hMN zm+_YIRpn7mq3;SIlm&|Ma06vvsBgR*U<1zEm4(M2#>gN7$qo3e2#Kg>UIJNJ*OD*^ zAuh%r1A(Uplm+0$1h6|FyzrXdd>i^Ih*yMsUgbyT?&4|CBZ5yl(9X#$_f>%L5ANCN z-fU8om!!}yZ`Xzi5u`PS*+odx&I#vN4U36DdWC}JbaGfZcuWg$GBO%5hypLU(W5<= z#A=cDke_*(0ks`RMXi^V( z5MD?O#p#0*tp(Q&5LD1RHXJ_EkO)Vedp4Lp(o5t({Wo~D@j3-)X2vVmd%Q}e#9$fZ1c7gv{74f zr?@8eJ6);GhLozO6P`zBDJP@h`c~o0z{>&2kPa`x6a#;6@!1P>!2aAsbupD6Vt$(c z9uP@H)}m(oB-Us@wPPzmNhm_PyN~38BCp&9B%!du8PzpzgkJY>kOFnPVo4=UA(a=O zV-i;EHcJiG@jr!cJYD4Zeh!#1i7)Z$XsqcpFjjjjr-1<$b{gM6feJ;(8sIft*_Kka zqs=}mP9OEUs#SaDNg#DQaHArs;a&268|iv&3B?KJ+g527PZaDcoZ60hUXl4YQVSMx z$g?qm7I!guk)9IOvf+gxrM@PAGSyiPh4fe#?+KO8P`Zs}kIU66d2AS`Mc|0;ERSYL z{4oF~;2pt)5!z*E&dH`yOJ$!_5N?;ZYB@d)HssjElkpQN!?f8YLI(Sxxh46RwGjG{ zFc#-|k`Ev-C_KccaV-KahDl)O2ul%d0VEyG%a|QYOcBxo2<9!9PYhfu7Jv}^5hwmw z=|$AvMhkF}ovoG;hgKiex2Hl&>>mk27ER$=sb8Ffp|O@_EsdB`1!)!^^!D_=)FPWr zEPpd=oLX55wX`6O0X=ffr*mieBkMhqhz-y3=vrr_4N_S))<9123mYwX%z`eIl#+w^#t|f`wBTvYv7xh|H?T9T`KLV_juH1l&72dgK{jvi; zvOgSFS1$I|hK9v2Ts_%hSfa$X6UtpOum*VhOyxXM59rD(%xYqXMV6?>iK~CZ*h}SQ zs)#CBZ5fyR0MnlW+15xE@-ta&oMY!F!uS%>e$m0Z`W@jAC5_sh=k{P?2YI^t*D`_mPZ$#)3%_S z-YXc|O0!s-=VW|{X}=^q4G6-S3?}erGqMMGQ}X}!|4i^GN9%4cK^PO6Fvz4T44Ox0I)Yd+!~QXerwiV+i}6fr{@vdTobop=Sh z47Eg}zLLq)&JQUF{0M)n05Qtbv5)^);g?AHCF*!WNZ9K{xPL2bacDfxEz|Wl#T-BC$B~6Sy2p=Z`!>s zaJ-5ObOMkiqkR*rY_Mr3XFI(VT$%=71sGxIbVoNC+_s&%h=H$&I-vI9F;0V`HlM|W z>*yqiV}~7-qN^8$3&BNW3==$y^FW8r1*S-9GS#W{rC_2Ozsm>&LSdzr4>W)jBIB=sp|xxBNZnKahY@OQ24Z zePUVgIGoJu>dR_pdhYHfYxEU`4vnTOYz07Q2%s&xD47aTr5mpAbVOCiul7J`2D?)% zHyh5mDjx@)IUcZ3MC9AH-B_t_50L)x^_fUKB}$KAWjIn&vYA;4laCLlFiEcA;8L=U zBM@p@F;NrZP=GPyN}~qV-93ZxvnVQ^=8E6@1r!x+PUZY3g8CXP($Y)O8|n?E!}JYpJ#DGk zy>+1*pQ>r0P~!e}Jdw_*vDoP>C-f!e1XI#Btc+JGEVbsz%~0ERg@e#YHXu@j%GGlR z&Bq!G#THs3kGB<{h`dy%BZKE|>0H)M0&ej4^J}XCKja5v3!ONs|G3{JBRT55kOswO zzt6BQVp($}Ipw&tV^=Gs9usF~^$x8#GEr;O&4P%t1ut;X$wNa*D@I>kT9@f#FVAA= z{MAX4qdkk4TSpu4`Kw`%-w2Z9S$On+MCNuJN7>RekP-*~2wF1Foht<_dzTr~D0m9> zg?pMbDb{hQ>H}~YhZ~pEQyDujE+9<^gHbFHcy;O}rMylNwS)!csT+T_MDgk40$w)a z&Uf9oO*H#1`Sit=lCT)d?fEb?h83>#{>>5ltk0!xq3&3x9&5}y<`ahuu}F8U0TkRV zox0MdYA6GKtb@wISLUpYq6mV?e?0#R-aV29WpaQO?Q4f@4qOqr&H~wYEOE`w8r6r@ z#pnU6kHS;SWfps*XJSIHh4-~M>n{<+0eJ)3?)5skTuEHX1GByGrap13M`G!c3zGLO!~-Y07ZdpfOyNndxRMLQ|ZX8Jaelb~JV@@PvKT zQ2ms44FF|Dh;Q3X8DY^zyt&ArrOLLP-4Ga-(c0(z;jO_hDYQSLJ25PvhY+BCKupyX zK4&#ek604oTmfKMK-Rj1RK2ET0VxFDAbQ>ZGdtrH zhu?3J$b*C}dnD)4RjM$v6W(F*@UV&0j<6ekQ7MUNa@=-HH?JY9Q58$ZYToIRGQ&Be zWk5mnGIQicEq%q3DJNm2b_bHu>}w*R9y)JW^_y^WyKDIVRP$E%MODSYbDEX2=dIhp z-^{J91bmlK-A&|X7dP@dEj;EVD6BjIz%7&&DgTG)!z>ejcQcn`e7?)UmxpAc=2T=2dB0PAG)BgHjJU^P}W{bwI z$Jd)xI?9TJ3KU-;eJQ30F`%GA>~lD1Y^yfbgAi=oi~v?OiH^9A(PubZ->tJqcvhOh z^8AnQ0MZ0Ge#p`g7VPlU4?2+_4!tl^CxzN^uxR!Jt`e!MccWig(_hD>%N#XpvF6a3 zp}BVj?e9_TI!qfET#hLvw89)n3~n6NqQ#JrrLAx5xo^-9rcymm&hhvJ zhKr#_WNsFj1nTTzVB{BcC}iM8>FEIinaFLqiTYpsP-UPorqXN$k*~%#GXSOA4?Kz0mTr^V4d%jivqmV!)x2?x$Y3lQG zrN0qJY1t2Yh+ZRXeow^M6hXZqqKLjW(i}BmE3C91ik@5V<7CtAvAcCowAa!hwJWWo z=eD_Q0qR73AZPhJs_$gXWJEQpxtwCYySkcX>ay+D8ag@;J$$Uk_oKSeJ)IMBL6c|{ zu+4t-XT)#+!lw^cFIVBeZ}GzOXIo-)P&zWba$3kdaNAw6PN&$T5GH9CdTJ+~$26o7 zjH6dbSND7re)kw?pMY6oU^12a4&Y1%AWqonYD zkfkUSGLR$?;A@K)=O2nkE|THFVDS`Jk7RWUfT$6Hf@_3joNk_&XRQX}PAY<}Jrxwp z;gYB*EIf=n=^aef9J@&-0Qa`?jVCA;(2%2{*pkQiuQMe7Z^pUQHRoKTRkeqn_mm7m$fAvH>0Gc^wNZ;3 zkOi8aY|xf3ISmZhg|+2H)kQ18vO0XQfsASgXSoAT7%$S|+>2FZplP1ywFIW+e&9WB z@q7ncHY?4vG@KkWm^E}|v94Id*ukwD-x2G~`C%ZEhKPM z6df-d0|YGmY{}~~THQ2VFbT-Y^%j~?8&Zq;$`^Z6q|9mAQqbRePQ?m7gC$>-6{cd@ z=OLhc!c%s6SOYOm&2b4N96v)r@o!I00YPDB$)=>4HeQT?*WaUu>ltitM!Q8d$|kCh zPA^53M)u&xc>KA?e4|#k0gHHGfAEBGlJWT7Mnbjhn=Olg`0k6RxBZ+(o6wjRG{7b>M$ z;syo!z8^vFJAQYx6JWA#A$7f>*)<*Ox*yINSe(`l|s`I+-q{5h%-5t*=qGZ8Z+9R?7N1|cxv1$+M=2ogxHqm_%DNL)CEpL+?J+-EP=Sk zWw(J#t|k6MK!*ci{NfA5!u65KkzdL_`%9ticIMc}VZC@BWHTG;+7|tlfT7at+9s)( zC!~+}HkLBV@wf>*xPmzUDEjn*JOp`eaPyDZTG0Hl=2Ug@NaR4}21Oj07^kSq>mU|K%p}|21$?6evO*@YL;3*n&L_*-2 zPTcYoT>|?hNu`7m$b)QC03!(dpE@CyCQl5y=oYuH@ZT!scE3OXf~mXG#T3B41*v$+ z1u#P)>|7vXGR{4{*@1VpUkfJoAD|J#t<_tgtneWsJUJ>a@1e&>ODrHPkBax(giecPW3)kAUYe_c4fPFsG5HFlswwWtkmMJ(NKhXY>OZkKLs$K_7n-8eoxc#foaT_;nS}l#Xgit(I9I+rROas;vPem1Avel-`vGpy+ zQ)u;u+jNe+_pxcU%ma2UG-BYH1ysBw70)~ud7#Qb@~bjf3CN>7-@cwP&StjMVhRBZ zL@a$>Mys2{99;5wa7}@N-&twgPQXeFdVX|VaTvvJ$vdL2Nj89duoY7fJk0~3@Hl$N z@C}j#K;SBf4$CJ#Ch0&<4@hG4&fXrF&#!=`ug%|W7_`mv(lnjf!1bW(W6j+R?aesR z{*4hlh0Tpw;laHSR`}zeM32+E(y|R2z@$!cyD8RJDU)WeJO}<;d$N{_K<_Fn1o_vfQ=;{gr~ITvn1Gs5{k2*`Qhtj=IxJ16`WF4l7Yp&_G(C2b)hiVyVTiv*rpH-@pY2YlLO zlusS=%kcJmZWBb(Z_(j1vYO1Uee;$J)S5m$#Xy#8~(3jKTC^C6pN0ys}#YP6jAw+ug~8< zDBG@?G&>A@V>I%mY5430aQvfKZ|>>}4(zLeU2zV-SNu|lVqLUYzM!Sa6{^Rfs z!?Y8d*LgNq`1Ny@mX-xC(-c}CjcqB)FYnS-B_;Fu_iv03=n|VHd-=R612WgG@qajQ zrezqF|Fr=b9?~MmsfXKwVQ`CtDyq`gw(Gl<0WR*#PmiE4x->^G`fWjD6$Y&cfD~m} z2`xSEr}aZ<=F_-`pp83KLjR!!Ow(mbl|$jgERanq`I~galzQJrd=Ib8$Birftm>X= zp8f9c1waIHu$bx4J5fc9tSZ1OtZq+$Pvxu{n}pR#%e}*+i30txQ^~sK-l!{7uVo&q)UI9?Tp(8LvQ?NxtDg)5 z3`?(&^iH>=0kPVnk+u2K=kL!~Sj~>*frD(rowh=(=?RD_~L? zj}$&n^I2jO)@TH`QP>@=*y1XMgompH1*6;t?D2Hr8s7K3PryL4s&DYTABli`Vo@yH zC`W{ReIeb+J%5m+H~L7JczF-I9DuZ@#hj0?6;EE$RQfvUi4#PfGQk1i zHde7PCKKcDx2#t)#Ajcgou97JF-NUiM{AG0+^Ej=bUw7F_1$Ud2_l>0+DW9qp#&v( z5I4G1If%wqJ4#~k?5HM3BTyYlwbLhrm@4ZrgVa#E+`Q>hjW2h!m5E^sc zZ9xN9$UJ;H9$xqwjQX~GJC~1uw2EPkA-;V9+pR`bUQkZ0 z-dOOYw-!rO(_0R`_MjlOrlA({)CgdSH(#o11AE(FNjG33i$(O{?*%~dg0Dw$oSOY3 zNtwQZU4qQ$By52-Q>VhaLzjn{jlsCh*l3N-Z}`id=k;uaxbn&9bU8=+?A;7P;;gHs zDc!stnKL-?ys*@)Bmfne2Fbre-a-ZSO51{5qn0)A2aDKT(rI!0YjbSkV(e|atTFNg zcJg=)1kS?o%?+bP3P4i!PHOBU#Nd#Cca}{ieftNVNBrzHV&mPGU-0um7mXQRa|p;E zx=G*JCus5i@-mG!*{RU-cG5IlCDiKuosNj>7^>817Z-r7zJLxm%bD%?rHQY)4!gO| zOZ%TE_7j3{_3o;;2lU@`=OeNBww@{DCw>GM&MQ5XnbCyX?d^oT_*$9?#4AIn5^(HD z{@H7=>B&(O1?oiweFoanp4%pZFwuBFZC*(H7c-ZC+}C6|ZFF;MyRqu9Tc2#zm0C`cbz-LeMe=mEtfV%>#DTtT7;u8K z{98*PKfc*bkXSGD_;2X|fWgM*6M3?aHCX}a00RWQ5LHp1MZJjx;ji~Bi@5sP_-wUc zN327#BWTFrYQ4@RjSOJjND?#RDFdfgku{9XAQ;9vjP)X-H9PPD3D z8JI19HTbRnA*QkR2p8kPS<^0#Cm3jVo?_53r)f-VGrbgO4&{+`HtVQi+&9yf%A>^) zCD(Z&+c5L(9q+4WcAl4A?FU9N_!_1RCSxDE<>IibC{ZWxHXGKtOeouPs&3+%$!Y<} z1uYKx&^D;sq1BO4DByCk?&NSHjOFV~9ygf9FNx8qAf1)ns-P~Qdi^3-5>=6P*z}l4 zL=_n1G<=q~>SjhlHb}LulZp&mF)oNMaEC(EIX1Z8htZK;pcfv9+0 zo8lf9Wp|CvOuF7|s6T}gpx@cZeKG3?!~m-Wf1?)t@ReK#9{Sw(W)MO0StDXsc0xFX zDbOoY>@nfZElPOhckC*zAjF*@5fIdLexPfm!1bX>VaY2r<5 zKTk-}{!3y#$6K z2cQ5@s`-0Ko4ekyCivM4R|0oQGn1!8ki=F;Ju*W4UxexdjDFR}HVD2xX>iyh-yvcO zxc$Hr4Dq5vEKa=1Pc!@vD8WO{V4Lp`~p}t&YWqP#La5NGoxJ?S9`dR6l^zzeP zp+e@lXJ>sg+K(D4rUSwI^WLKou|uS=eYmK3IH%&{JsX!J2KO#qHQv4!pv~cVjC*1o z)a`jS_Js3vkWlzfTsY{c#XP|#MsyJ@?xf0KYHSS!?9{N*FuR$U8svBzdZCb7=owsZ|E{VLaE3o|G(G8-5c`M}<)mkAAH82#i|9 z7+~B*pwiWhr;}?<=^inWHmvV3!Kt@#lUa}>il9yTM5-^?qX7c#5- z|NGTknwB{la#C<<0+7@Vs|eM2hzW~=0Bzfv2qw6{1cfED6qGGMIlOLBD94T+)3nEx z20uxAG68`L4}ssmLLw%6hXh4Ye&c#cJ=DbSxyls@Tl&)ED;k`>=4PN@}-YJXt}~ znib+|d}Y5BOJnNgNKoqLZN$!k^1WxZ8NPB z_!zmQ^^KMi(0py>vYtuFCI|h{*D<9c{3D1s;%`j@TzJBeK6X(PgB9-sHduN$B4-D-VI7vKqqkooxRqepZ`{`}SvW3) zD+v#zqQ^+#OSie&%HIMObLssLG{T*d7=&AJKSt~VP4Fqv_)ZPFS><-0#1eRuX*a*Wkc%N^XwZ| zMmBuz7L4}NV#a<5fzYLpeY10v0L8a?Z3PPQMxx-3eDD&^^n3@24I4N6q5RES!(fWl zPef}u*JtTpyV=#9t3NZMKOL@W1v|4^XInP^$3Z1RHIYeWQld5#9uJ;;&-OmAez)#g zz!O5gf#mGdgYv{%`?+`#AQa(X>LPB;!xa0?w?^g-9&!&|U~uiUYv}ID5^N~;wRLMt z)?CiE2LFk%vg~)n)qKT1z9eWhHrY? zY&#q>p+U8@ZsbP)G-;}$P_Q9BU1xuV>HtJi==K8*&w`YOR|17VNLOaTgHE**rtc?v z-%D1H%tN>bS#>0MQF%;$>Zn(5VcYJ-UJ);3F-xE@gysn)m}V$+ymRm)Ol4C2H1i8r zzaH9mga&xH&S(c9%5muOKP6OJziPUv+4k_x*SMN*O}`^Im7{81sl?xWxE+G&(lRIszwr+Uv4|ah(hOf3Zbs^?$X>L z6Ogx02T(mY&MoCCgt56b*ieA34$S9(S@N?4Jwv$=YOzk$9`z!sLqpuwwPXUx9!~Bs zWatnRvBgVge)j$}0{D#M^=e;)dtlgpPsRa*!S6>@gDjs+>qp03i#Ojgu!;wWn6*=n zGCQzRhcSF`*4}5euFEiH|JSNO3FN)T{a6xouX-#UrE{lahVXT%6Ql7Hik#rH=iY!C z?@wS_qR@$!%!9xP-QQ9I<;U-agJjY+^7{nhcmMw7Y`V|dCz&(hT|r7qHr1x4n6NY- zyyj+A#i0~<+Qu2?U=t>J&-U_2+H_rW8ysmiuQqo;9leFi_lDbi`>)o^^YyCNDGmxW z_sTGNKf~|5q;JPz)ftcE;Y6}bEeX&}7HV+Js>Lq}i zYaE0k#To%E%_M@Uei{cl1(G54{=vum4bCgLca#zx4YeK``T7@BItG9BdOya!=uwEN zaC%lYPg|f?PGlUxXe_U0B4dnqvM!)oC?v|<=%$Sx7+gEO2<6(yl@eIL6$-Jf z4$al}EMT5=3z zk1us<2BAi`TbAH07)CkTZdaar219=(H1x+V{lTnvpq?Y-qh5(I$8$+`a9QU5)yBYq zOMXZNo;C0QEQhYjrHF1G3>cUOxFwV2oUiD<-m&g~S`XkoY>^1~Hrkhgfc0XN4IturF;0mSLy`fS``JUQ=ieB>sH5J+mJv^vGs95=Mrq6+$7J%>sAn zIti-J_08tF2K87h{SE55hRHW+Z|)xhJeRD|6|)@pQtQ^^GSUL z{E0PWx;be<1fX?Oh_7g-)`aB4Mlw!dSGlsPU&icfWa7+3!;-ymK{%Bk#AqEj)k=x| z712z=7L!i-bHpWuEo2lV>^#3K7MC&FE_HrcMhojm|2y+B!|~8X7h`(lr4Ogiy6*_Ucp7rpwvI<>24nd zPovWtA1Ncf0RF<$ONh`sz;^<8@o9*u!voFcG;43G53JzlcWatLmgBep7>Mt*8*`m| z#@N+Bl!mJD`)(e=Gw0aXOX4`Objl_4Jj7U*$G5Knd~jJa-W2D^UOexW*0WVtOGExW zgrYEMW*!OsO-yc-0(Q@1o5z+13SjnZz>Yg^oz0^&SlDcp1jLZ@09!gUIFB`dVzX~? z^`u`r?gu6EnG6lcHlUNhoyyWR(m|lC0&#nY=PuEQewqNrcL7k)^QLEQ2qIlcLkBbn zKyobTM?H@G)=ZC%pkASy8;!>9M4I-23$%=S_kYIZ#w&{9Ac7xYMTW(<=g#Z20M2A0 z`-etLm&a2G1a2>DPKb7cPyRVqzS<7n1PEOvcNwmswG&VKuX({urSNEGVa3_WOd5*n zuAWcJ(@75DdDh%SNisb=p$88UWiRQs-2+F?h=pFo%)k=&ZHc^jZ8FE;a%_-xbR72E zLZSwgPV$uBUvs^3?IT74&%ZW;yq@WdrPr{f$gX^M0AU5QyobDPS+TZKW)LzmxJ68d1)3*UTMx=)X~yjuFY z#8xG5x%#fQ!>MT)_g#MVB*OavWE`<`!e98MXB(1d2GGDl{e~rVJbSU(`|unc%!9l*bWpun7ZT3V=`hZjw^$EDC)}Y10zX zbzi#)q9-&&dE&S0@u?;Zfgpd306af1CC1&^wNAkuJ!S?c4k+$I%OYO|y43HuWA8dC zov+$569%k@85^vSaWwS2gQz~Tg zx>;W{7xN4vO{28Ws^Ox3F;`{*Ne5->`6rB7Rs9W`qXu6K0nSr6j@w~c1!j9R2#{)t z!P8<9`OZ(R=0-yg>xtSiRcDGVRB4|RtT!3L=JlOydL=gF0ZF+wK!`8TE!A_%*aX-g zQV|XQ;woggV28OkrIpPfKWnc zN)Q51;9X#6`O~I5Nl8Z@NZ}X|#h{ii3~IKc(ugWKc1bw~ImMq8N5IEtCyQaa;L%)$ zV_>2&kwso>t$^D?_m1<=-6_G0(xf^$8;?_qe02dsv>CU{RAX}1?YVRbyGi1rCI~}rg#~Y6tYm>`&KW}2@VI=yRbd3T9CS-*l{vQV|$Ju zpzu%c91VUJg4IMU;+dnUHW&vo<96(l_oqFzQH3oOfp0>9I9dJa79fB8$mk>@&eeL_ zdIeKET#ZqAhhEjJ()i9U9IPImZ=Ro>p)e<1t-#qcH?YE!47gEJLN&mhB?X zNyH}sA5o{gu1Y`iE%pWaPiPZNgBrr#x%IAz;|4kjEmK5h1}92NP8eXT!_wnHaGDeD zH7otaNr941ygHLO;uSzf9Vt}0an3g-NPDy;=m3s$I4k@lyof|NIE|R*d?iSAGI{$P zhk~nzV+&`~a{BDMr6_c=`Y|(=)yYEW^iP!6K%U0A{hXYBW2ad+^eI4*Kzv}JDh=%= zED@pn3=`Q$9P-&N3U0zwB+gSv)(x+?p_+`mn2z(l;9PlP+C|t3ESfm$0k6LH>&+&C z6-}mxDLF;HZ(*v~0~4glX{~;*ubV#_#j@JdP-PuS!-5>B84rr_Mc@4N#9cN3hMxFO z<+7Qn;nIsA)W)i2CC+tl*_3FYeyshnO!_lxFm{1m1Cca5BWa5?UmMrVzfafBt`jnc zEz}NQn8n%&n8AKj6LrG-yLEI>~%aVB#X}hC|5_X?V8Ir&GO>V0MIoVQM z{76dZyTZVh=q$19oM4I;b!xP27zf(vnP^!hpAC0)oo?c+*W0x{%UpY4mBL!2dgv5! zHDj7<dU)nf2Os7A$-B-+d{z@iayet4U|W#yR|f7k#8OE?hs<53msj$)DH@T| zC2yGGcv;{I9A>Y6lLGk|ZSX}&{hC^iLiG#op%C`CQOFRuMJEG1w#!DNz=%b#IHOb#phyO_L^r6tsi=0G!UE z1vwJi0dKRq9InBbF|?^^(2~+UH(yviUC*PX5Fj_8kZqDdA<)aS5lPZ&f*uDz zHef5&J%ai(;&RfuE7kxSwzVg~XWhy}Y_8;wDxDa$;cN87Ub*KpIPN zqPp}&T2ObqUjKY|_!1uZ3&FatrH z^XLr9{a~*}zX9H%P^<~w^)N90m$Kl)W2l+Io=5wD|_i1U!Y z;(8TvW!&{hXAp&AXV3c1fJ@LVd{?;l$V;?{mlcgbbl2wfh*DUsidi8g->pG5zL*r!xaId;|`6P}z_i5`tQZ^2M zlLT*WL!z0qni8f30|nIkgCyo)t3o%sD`vtFNsY4q!gb|f{e{*QH^ys!ZV`_qSgO6> zcTB(q2Zgn+l~auZh%zwmcW|1UL%97pWr^jVdU?w)&9j^Pt-mffZNOBV1r>m66Lma- z{)V%(ULCl6yv;jyL;aQ%EXg?IQbVsUWmI5z?zxm!5WAU z%$@<3fT6Rsp9icud#tj~V_DlMngEIQ3wt(;3Gr#7B3YYuM;)FqN;6#P(VZv-8A>w( zB+u}5><$H|Tk{ltQcjSdP!JfZ1J0!b7yC0#0Mf=@O8GALmn= zo6QmEt=RWBiuhh`NCNq!Xl^!F>-2TE1rfu>wm~oesWD~3B6LkOW$`7RtKEW7EFVLN z!_g37EFDOI+2d+ZN_jfmLq6w-P9(q~SQSe^p)19cO~zoNjx9J?gd{CE$3T(&PkPnu z;KH^fJ`|GLI0T_Yv5ms38b^5yd??@&yO-+uhxD45wo<(=cX@(Fg2F&%-3gL$tt$er z-^FT6;GWT}q))+ZTD#}RrN38~!QbhM-*cmb##R9*PKi4RfRhXZhp1`k;5aZyx`gZlpdZk3L{Mbjb8CHZ9*TW!7;kpr&Uf7piX6NhyQAgS!Y0k4eLM)`J zb%#!K3*tJG1iC`cZU5FTRAo=U7TO>=n$cC7i&R3YkOrby8RR%X1{$!f8@6!}kGmc) z=aXu+>)A=5?I&>*|H|5O8l!5C;Az?h%8uq-6@ zr3p_@j1NON@AVA9IS{TCf>B{n`f>Ip>SNx(c!nAJz6{N-b3diq$^V27@V=dZsU@v~ zwuoxHG6J9`GS46c4@7(1(8=R2nFyS&{-9BL)adi+(qK|l>%M?889%g8RRy#Ld6PzV&eH-Nr$U!4Dicm*p=q(>+3*ewdya9q@nEnzr z#*|k-dz~yD{?GaXRnpA=vHqOdO zwAvNRx|=f3l3+)-2@{gk+54EYui%kO)CHlYb_HpssRuUJgdUdBpdJsbil3%Py{9t? zOa#(4_q0Y$cTx#Fmw_ph#+06S3Dl7|(zE`o_{WrV#O3ZDRYug{tl&V*SSM3{)G^X3 zWb|h*N5#Qrew;B=NgPxJMDa01sqw$v@ldZu1aVxf3i;cp@GRh|F&U=tC~LDJ%79L( z^$5Xh&Zzu({6c-)zMJJthI{=Ee=LH=fQuAp7> zPn>AuE<)M4z#*KM>`oWN#UNCgl|3qwFd|r$BXWont!`UC!e@m^ANvHJq&=*GrA)T_ zOm(KIp_V~gTj_KQ)*e@GvQ4EpZI^(e)s_NmDB!SOr38@pVY9~~Qo9oIfriIKQIc8U z<@3u*a}z5-nA(eg+_e-kN@*1GhiGHF4T|tTca$U~5CF~tM4psVNJ2{CCjaBz3`WFI zp1~quzBNKER!yTUqe@&`vojs|Rrb}7kobnx2;{{O8r$9H;^}qu+lb|qcT8-5pj{rM z#24FCM?QvtjmbM4U=}I-yq||&jJUT+qUZH+)2-(B_`L0*=^KM8m%;;NN#PlyF=C?p zst^}HfxBG_3k#)Sn4Aq(RWe7bFd~X?KuokWWB8+_Ut!5MW_q364hWXyE$=R*@th!c znub|d6kHBAsVqcQJP=>0Hr!Lg6$6W8MZqK2iNS|43r%F~7$`%6fq&sY`wIUy_@!-@ zZ(EZHYdZl_aA`nAprpZ|HR-KVOlJ*_SGiBbv^?EkHG9& ze$B)Nx1Oo43KS6ltZG0vFZR8sCn0MYFtDCCb0u6rM-EBW%Sc zSQ#!4D<}m<0RjpT#ee)U`29*!@(}6>(3)w!)z&xA1NR@K z$w*kbf9!mcqT}CbD4*+PW9;erI@}j#$?NTK-18IXh?h?-Yg_InuOx+w4d^H`wh=;9tduXDTiLN}cCVUmG zg&HQTfAQ0jl+eSa{JPX+fNj98y;j%im=N71cwP(ZIq`@-+)$;uL92xg`f*zXd?6+( zZ145u)Y9}*3*wm+MpI!AYnRRuz0c8t!Qw*v9t$#Jxm+mA8rr`7RZcqZ<2p8KJC}84 z9?CIsVj(ZNPhTxT)R#(dE$LlosmWr1v&xwVHI|0i0s3cWK3T9AV}gIz`*uQ?lQu5h z)8gI9>LrN8y~!Xz7N}H8SDa3H-X;)r;YJls1IvgM$mC#fo|=$k=gXaLaDIB(Vt(!i zJNo-u`+X0cHmxMMoSSzB+~jF}Htd_QKixjdY5KfcS6;RgM#FjQdewaIzjE}6=dU%q zPX4^8(xSe%(6Ph+C_-v;8xu{EUt%&WJ)D$Gdlmq){m$X`R&vQk?$8gTyp7ocm`hSA zo^}FmNiy5;{)_BvjB_X2X=U3UHP_-_oOy0R-o74nma^G^{rpHIs^yH$sbhK1*mRMR z9za*D7Gkb%B~WWjG%>{r&yOfvYSN96Ln}zBwYmManla5=uLKwqVW!8@dED^3+`gH0+ksK5%>ZV0KAqoHY1!Xi z#&+vzX3}PzvL66ziFf!g+lr?0Xsfzia2iOkfIj;nV>Vd5{u#55gzJM#FN~_QZG-MY zG9rx}R~VwE9e{mZUwD31HW?cnHyRk>0Zs?>ga zKDDT?ov>@?>$IyfpN`*V@o-g%%(;~LD2eXzU~;=a9=D)agCIqAb(*)?rRj%>yU`!D zV%x=Eb<7FZw*A~KMD;i7<9~DmenIL?9X`%WFL9#15Q_5i5Yy8a|(x%es@SuK!Z57 z7c}Lt3CbV5sU6k9La?f+9}^Z5V821{OO|)IwEij5Du~uPg9?76s+^3KP@gm8?&>Lt zMxupY8!xPs_ij2eC;SiPvEpcigom15!EaZ7xr6|BkIT9X7*B{DH#pHs9CUFLGDhyp zumA(@e<2tlM%@Rp#SgI&Z3#FZmfYUAB*|Iu(||K7j6DS4vl^{zE2H zMqetAjMqTE|0L9P!lfCLCW-IIkGJ_2S1(4LR-iVzRl(h_$L()PVS@m2i-wPdxl>>! z6R|owCJ((egV-#8Odb-CdjT{~59Jmsl@~)He*3IS0Dr5cxGlFRKEx3R8P=HA6f27C z!BNKbo(=6>`zYt0;dEmaClmtpc0THu(Y&z5VYu7}V+((r{iO~*RYf=loW`Ocy5GM4x*TmU^?Sok6#-y@+5dwj=Sv1Q5|qO&fG z0F=u$f4hFf%V86?vKub_JWmA&5Fy6mgw8ozeZ z!wA15SaF}g2%PPE^QuNMRczrz0*UlC&9rgkx|P>6pFJ+lmlyK)g+px zN|zzf*-Fp(Q4a4$N-&DJ#jQ(rHdAUQrkg;hXuTU z`KA3;|52_3J3bMl(I=mJVAya-6*m48j2{cAkTXv~EjCxP+x&->1A? z#Jykn{Jr1(op+z^KB5oaA9t$>unPdzxY{k!X$!31@WLd3gL= z$0#svnW5b&H1@CR%Jw-v;H|P4TF+~O+T1w74`!hpF#)3)hwMPt5AUH&8?!i`LYDv0 z%*;<9|Ddett0HXN?sEH#moi9{LMB-~J3wZ8i-);~w`0$%kkuDwzi`E!1-vR2P$72G zacEp^*dV|Bgg+84Wj94Fn3TF&cKHYi z8$}>ekiI)D&?M0~P!AB@W#Che_64J}dh4xga~Pq}>YdK_bNfil_iO$U5t;g6%T|3% z#l#%lV-!~19p2Wo1koaIcQM``V*&i;J__e4NTQ<6dQopbEPfBGgC`QDDzMjSB0ax~ zz`ky9G~~{48x9I7@iSuhqf}28`Q=CUPCQdu->hxxx8J^2uur~nqC&qYoPzgAu~?Gi zwRhr}KorCsj%1Qrm*7IeCG6VBwUwvFroH16*nj5t5x?_GpBP$h^9}mu>-}mp>~+ff zQ5R4kfIR@65$T*BL$-$VX)<2zRZ4m~kU^YJwfuG17}XwOK%aJA%-71)6jG3wh*A={ zTPd|~WJ8(((|qMPZV3uZ_w5-O3BGs=iSj0>z95#dV6#t_1Rl6fh5-hI?Y_G$PEkaW z3a)>>5~M2<>D@T-d))4ntBIjBT>p0EC|6|4J94Y{YM#z*x;W~A>)&5K>MJVsVmWqA z=}^^KJ;>u%LW(oKeX7FLwhMyyIW+jXBVWB@Xr(}zpScTTNO){_7umRPzg6mh@4nv1 zF)XwwoXh&GFId%f+gz!WN^{EFdzM7%et{ls6(H@tl@o@|3OSOvyq4p%ncOA~U+aV3 z-5pkyYWu)S%H8ktT1@i-l->7A!Ems;`U;oDgd^%$|L3P*`Q1G>#Y#{N8Osq0nu#rT zkHojSs>xDGaboLRkVO(8jt9#e@-3qTd}ZQPkE`7vX`CvE1$s6B_3=H3PB)gJ*OA7X zjKwzS9H+n7ked~{zi|TWUA3_WrP0O|_^-;i+Lbm6FRu>&>&0y#2jh=zs4sWjx2r z_jXbE!!O=Z^o!o+gZqQ;_NS+-6=MC%Smi z-p;g>Lll)1LWC{yBC-%Na_^lPBh=%n5)m9?6w&ql&E7{4qfiQ`;9g602==`;5@~WF z+RqWr*wOidp#Z|=|DGV*ILMOfdf{+s>0kS4CLotfiv67j?90)h8e^U-g1$#hb8vDe zD8@7uhVL)q@i#iK{D$ZhkfSIfUt+iO^dwbi!(jzdH zT>M@_!+t4($w~4s<$}6aBSL^H&3$tNTz1lQ3x3?STI_qoYTd< zD0zCY4XGbt7FOIuswBkbxLN%Y-2lNoy4TUC5&UbsJ$$&W<1Ce?xH&lxz5 zQP9OHOj#)fMG+?vHxjn5K&#jiCx$g#B?BThpc73-IG4E}dod~$O}@_ke1hPREhr@k z#_^nh~ZEYOe}mbiK<_ecz_Grw-opg?F}R!suTAF=oShX_jKOKgH>Kh zyf4deu1+0{xn`vHAu#&buB6+kVO6IP$}cO7+#*t=@V?fAlc@_rz9YXtgMO(7M9%0? zlr?(&c7miWu`LfbrxIQ^>C382UlxS z;bI%252L2ZN7zJ+x9Z zkD4pH+zO&ebA;Z zxDf$Ec9ymkPTq0&JK1YAiaLk3CteilFO2eiqEUPUeL}cCm>s8mWEy z?B=Txn*g=K|@#8=%2authaSivIQSgCK#oP(C9MPQ|5fCX87sWC~&q$Qyw-)+7xl5I00> zL3O2Xn?g0w@~xfR3>~t+q|p7DRCvND4Fc!2FW&l2Ymo@KUPuA+EpG{3TW6`2Bp&9Q zFV1BQEy1{)vM(^7f&%%a+-DK95#bkm*5UdI~vLs4l`-W=h@;fIg9Z0n}Ehm`gCti5%0Idkz*U zAE~Dz6b7R&%HU&sMOL1I*lvE9T3$Ca6_IX_lwT)LK;cASjjnc6MH=t8kG87$in^yN z!0^AVghuY0yLPSN6WJT2M;`Op1?{Gm-(IQYKv_h|pcM_1i zq?c&SGzRAyZCS4XT(WNty+H!Y=48z27L(u5L4S=>r?1uTY_bE(D&~~;hp}(dSTnue1NrZNnw80Br~Wxl-GBEjq!&u(T1gf6 ziEn;qoeO&%ZnYjQRlcQ^c~OSj_~kaLym}(}&5Q1Xw{%ShJfyNo{p`X>L_hLygzzsCD`aAxy`>m~736m6t3m z$1ATlW|?27fu*LI2?m3~^lyfqg}ts<+H1xS#(02R zaMJ}6Py!iff>cnGEU0bq7~b-xCxd<{LRIBuoZuyqo3rTC#+LHG_)Spjbo3^rLjMSq zN<_Ebtx-$f8+0mV5FaITzB?51PGhOoyo;2I(P60TND(9NV$B30bdWF*xY(i|(pArQ zTJd%igzEW{ol0%cd!B2Z(+n%NbPT94BNp_hh8bBhUAVuw=k~diJ4jmhskb-Qmu)R7 zNLu|~32gE%>@}Y_Wi8{7Cd=bwE7~*?WiD3G0EE#g({*y$jcVQy+4Vj3a0aL5u}%#?TBqGE}U$>l*k)j4~mN>__7AYJYGHWg~uHwFm_kU6CQ1+Vm# z!5|#&uvby0d?35|N7i0njF;^Zj61}$jW)h#hi#5GDv$)N6EkcMBWRx6EexP(a9QmH zDBdH}G2060sxBCO)IvN^ zdc>TH10G7sR@WqxR|IQww(j9%%nT4&!mVF%w)}CEQaz$(k7Lta#VjY&|C zw&5CXpWccGThx9<*$6PX9j~w@mMNF!+kwoED73LCI1<|@uUe#Oc7q3^YB$k=cwJe> z-=#gmu%;J4{>@jimNGshd|BKyMy(ON{nPAETIu`{y5-SekBOtfh z816qD`0<+wU;o*gZ=N4X+rfD74tqX%y;mYnx8Qv&Rg7m0IY7q0KRaVB_&vH#_{Up#(C`7Tru~GtG~x*KT0$q<)}a`9 zy=imtTPbB*e^Ts5;=cb#Pc+u6lHTn#$Nm z?PH8C=HDWY3F|yxN8=Ksx<|FlD1iP_nxms2`aBhTY(W?3t<3-M$FSOt59fz&CsxV! zi>SO+QO9W;RY&0U37ct#%U-40&(YzunW36*2#ksf8I{Y526TGEwg@RIN;2h1Pzm&) zw|E}}iz+eEHMVko7tIF?+_g;j@hg)%Zg&vzJ{`c{p%v1@vV!s0s{%l}7dGUVz2x+S zZQ(E@S`%{3f335QbHJH>_tL-cmN!~}IkWJMhN19@??r4yucX;?d5vITC?DVGF4F5I z&@Kr=#OjgpoOEhJcyl_!OF284D9FSiBB;T6gQ2MRiz>4ATJS9Hoo)yig<@6)*$w}F zR=ZfbnB@|q3uTC@0cN`IDJAj`iq6@g!44>Z-vtCck|();0nvWVp;6ks;vX+%`)ljo zv=VKH@fyjWp{1bYpWX!ONCwTrl7cG_8v+Hlr4%eCts3$YFEhEc5KTN^mUTK6M!UW+ z=b#@@r3?0bHeCQR_ zVS#iDh`_-$`M?}%Z8PQn*NGyYIqGs%>2 zY&d>3dj)5!g{jwZD!+Xm3ll(Fg$;xk_Seybp~q~q-WWB=q?2cA3KLv+wqhDI+4=BJ z*BMzesq}e0P-g#FHFfmC51^Z_SX-7a2D2c-8yzh1`>9s`ZeLfPgqr}oMYTkyDw~iv zVegh5e{N4Zy?&|9NS8v#jh_*wU$7nyAz2{>N_7#JmBELUzGRJ6=-3Gg9?XEXc8EQ6+o~p_!XDv4b_LELFE?3Z*q8 zlEg))DDJ(=vvB1Ify1DIFzuu;?{GL7NhY}xYXBas?`nz z&b$0j^;z5&YeEgt60G{Mi`Dwo?H?|XO4juEB@<(b>Slo$#4(vo#DVbWgKFE zQ%$#!-nUsN1bS3(PqXMD(8PBK^+b09(ih^RW1T!I0NsdqIGO-UG%)dCF{3Wb(-`m= zk-#U6e99SRvT$yno|b4N*8s&GPLOvDB#mvO5`(wv5}GKj>Ed_&MiWhQVmir39u zJTPgnOM0G1o9fw5t)^$k=S>sN5M=Ph&t2=D+DTT@;7K_kI`9lD>vkfbDjn0-JRoFl z2_iy8BvaFmw8Qe^EabRrK*{-Rqy|wm;MPy+Em>d?$e&#YYr<%Zr!m3GO0LVr+OEN# zoIyos2*^93ZY>&oU7%wpH(9%V>{=*9mc`rB2%I)IJj$-F`7awSnF-pTCNAqWlDM&+ zvuMfxX0=Kq1W2S5`iCVyrJL?jOSc%QM12m)4E2;6^slLUX}?lIEV(U#3!Ih$MYQ)- z9G(ZLBv%TKRN@89?MX_{m^)f$zJ}FgxLnjUjfCvpFD+wXe=PCW8|9;}P0{|w)GJ6W zk{CPnsnT!MO2{s9Z(t@tXi?{ z)3sHW^B!TVl}&w!;WeZ>$9wUpwUEMZ&dQca*8c z2P#u5Ou_U)3`>(=GoiWA;w=EGI>$M(G;5sA(H)u;$SXdV!rx69gsLQdtq#Z#8YSaR z)`FVL*Hz-vqTYNWkEEsNM3X^bWgjT6JyMJ*#ePwCECe&N;^RO%!o;2M)G-pS5M`wj zx}^wkx{aJFKw(v0(?8x}iNp;scub%7v>=37C-b$R&Aa{Vx!X(LbumjE9uMzU!rI0; zP(T0;kLGYBvLrcGaMDeW>C8Koz37a4X7ZfHL-@81< zEq95Wf@tsfI6Y6@8wy%57zZ6 z?yU=>_8dU^)2@tT?BRY`&dYwl-F}!mqKM?X3}yW(_vosIXwf&Xa(o@?yYcik7FPl8hZD%|Cpv};KrUs=sd>(90Hd86 zn|88$s~;xO!Y-^P(t>exWL0+pfzxsO-=?Hs@=Z2x2x1`YxdHX$HJA+hnD)suAl$p= zULSG)i0aTw^){DbaprkID4&+_>p%8JyswnAs{)KySNu-Cx8i0(+GCj+!3i^`DDA)9 zF@=)7-j7x`ep=6pnXohvSgO<-V-F3s26p%u^PttN1c7Qh__ToT*{u%+B!&XY9H6$D zjmU9-HVAN1EBRsv#5ENEy5TWWm(zn^O#Qiu=luJ}gszOK@x%RcZoc%kfJ!ktuuqhr zOx3_sq5%2|$gwxVw0CEil;@u|&^H~AOLou=96A59>u5mIo&W>wL#-V5&9~+P;(dx5 zGmN{K!(zKxWPz2^E7u+(x`#^Cjc>k?G5^6wcZC1FhRpD1_| z=R>^;J6;MP(WzHrw4J=h2_MKyK{Iekd%4j9CH*s#ese;x;PwU^pgqAkqXF&xgaaA; zqLvwsEwmxk0{8VcYiV2wrRm~2baC;bnIPxwu`amWyqHMsOEp(Oc^(vx8nE8Ws5py6 zTsI1|b=LM9dmWnyq3POMItS@J(~?atQSX>Z_iVMP?!7> zEx=*?rOr6^=f(Ctxk}`wib@w;0e)?rd!T+E^Vdt19vAu=kL+~Crc%~B&U#v^`l~-+ z(ANkHh`Ny^TH=CK1}-Ye${kk@RI=;+Z9v~I0mB5ZUCnh;i2=AbFWJ*12<(WbhYR$M znWFEIb9g>vSF}Y#!TX|O65w;k&sv_*sj3(n|PfausNTcJhi}W!*UNbDIjr8JC3$z8^u8tF_`Te^O zWgi@p5m^?ujfw6D2k4gRaEiT_Sq9Y|*5T+BWCmCT2U6lvFF66hBaH7kIVBXtwwhek zAw3NOI`RvaYcZkTxV(Zf z0hx{!{5p=QZ91OL5QxtU948JmA9+*Y6me+&q1;bvi6DF)?AiN`UJri~RZB0VD)t^f zX;K8(sPYX_+g7|~U}4^;Ll&S@7+9|YQT*aVjsa2fwSox7#-;%t6D{$?cp49M69YvC zWS~m!1KmIw(IK(iI#m+=g z$uRulAVV}#cin|DO}Ru33o}4rL(Z4@OK#UcSV)p$FcyxS7}qewB#+pUB@$gA#|s%0 z6aZ+M8BNgKV-RbJQ;CVf^#{4^Kf?wXlDHyk0}6tD>^Zvc$`h>zA*|1$ZxTQ*tW*~U zZo-{o8VMfjPK`LXJdp~aA9+rQJ!PS3>|sCBJUt80jE`p(E|37^F8!~E-~&lLuBSWz zd}rzkq1qy6VM6fc3gB5<#H79&Ria{kg~K+OMngZ~6yfux$p?b&75_us*S)aQkS=d` z4tR(RrNB|SR+=+ZasH1n_{QJxXDA$^3;LbXj5yFXGO}fOZD34&0B;ZjGTSuGMc6!`x{j$K4ysl0?SV9-J zvl&lx|Dy)~MqGhn5$IBM!q*C3;tg+fDlZX-m#8m^E{V4bc0+p5nSFbgnAraC2kqi| zewH`}B#)pHV@Y;6kN9iYF~Y7?0k1_V8}cq17o51Ekr2!s?ienHyvZ9&7*sJ-7o}Rv zE`9SMn?a&tAj@l=b$V-X6hXxV@K(Hfr;c1)18HNgnR{{hKAk+aP@4IQ1!38S$T-A@ zx?LeM*T$OfJqR7;eiy{ash7%=T0QONw5Leg`ZF=RR$u7t9LP^fge_M#V z^VXTmj>QMht;B^I*jOP_I(FZ^E{XAO&NhP!ricn4p6RbOBNYPH}l=(@j;G2IE$A2$OurGxQ{!(+ri^iq?;(d7 zPGnfQon#i}6#IG0PAY8r?z)KJkOG9y!%tWbYBF~2rF5EnfaRVj*`UCP@rdWK|6dDd zau4Z_F)Z?2RUN24byT#k3oD@T7$>0)?wKWRMo~O!Z!;dG%zvuS6D$7VycOn($QohB z@^8YFlfZN{hEQRGh!7cF8`>B3!!qCdgA zU=3=PJ?Kwr?KN4fR9D$@aeoYN2SiFE5)J&_S@$yrv|4g1DM%iN=kY9~nq@_{VrNQ8v&0<_jnqK^>svim217Cs>3O7c}p z-l}`rZ`Hb^5vVd);)!#XfTUrQq(GVK?&t|3tqoWAGi%k@%`$QDm3>`}(9u%ZYvMw8 z)9?1VD;V=O_wikely3ECCN-WIa^x_HUQGoZzNy!aeoWKfn`fawj@uG#i^@@o^!=QY z+F!6AUZWEg(nC+uA%&!|77a>`GR|16b39icXFXW2K+8PNMWKCMsS)*V`th{aV_Cfq z`1r%qm#4?SG$JiC}pWrRvH>vWy3mCSY2ufGdonN)!1OYqIA=rf3BXK#Ukf>Amtgq z=4q46+MMFO)s-j32BJVDQHfteZdme=7b*h1Q%$MOHm7aPkfQhLxyx@~?ACU-RTiG$ z^yvE9xO769(i%TzB8dKaLjiSh(uHxB&6`VJeNh87D;^6*){E&aQUihCuzS; z0jYqS*&Mo-&tG^OC!7RTYRa|ryhd8h)BcB&b#mtW>S!*?Y^KJF8~2G!?Z1ypL9AiC z0fn)2ov)v{>YbLfEBXKuD|J7{qN&Od@u8mZvt)NIG4jN7VWw=R=KH`-Q`g48XM1G_2+MLmv&)je zs8?VHgKD^KcTnQ%dCV8KCV9aQ{I9Ms(vxtstmr+PluY3+HmneiS zd9k;`%-w)JVmyNyi*;p;6f+r1$7EsOwge71mj;EcN!MVYOEy{Wtd&JzGI=$r;K8mV zWj>yQx&!+LHgU5DIK3^7a`GH7VdTr2C8hh?1BcVa=-qQ|t53e4Yq0R~n33%>EUWo! zmTN0%0<_U0fo@2hk48b_;eKLnkV4R%$*CLSORuM%Z1t892wUjvJ}cHOQ% z5!J8dw+#V(+PCkgRL=!;Nfkj$RP#z8T0xq3h}D*j|wZ53Q&naB3=iLI&1$eX>_ zAS}Jr^`C~wpyO%ey$*~5(3EFCRlk~xB z6`BDf%K$X*viq~1!|evl1bHeQ;!t*9t#5&5!ovNwOj=5u=Z;nRebsEh{}p|bg7chQ z2+2U7!}6(NOs;gAldCcF`{t3S?s1`Q(lOIQuMQSy`j%7RuR9f%%&^w%xAEDYyD{&^Xq38rV0Ly3=RHkA+3M$Yevjlu%m*J&h7bD@z*+R#Lm23n>d z4yV0iK8ge(VD2iho=QZ84@*eO%r>OE)i4SzHEyRo5-pE8Y~?ceSxL(jT03MVUC^$r z_tp_=G-;Nc6;_u0uVa_xnf!4w1VBB46nyx0cBW!ezvtWZj+r+1ET17*k0 zps$GCo#nk?5{xR5j%tSu%Wn_?VM_`ETh>O&=PqFsOmUTv&!j_9{blJw$pmRCrSz{^ zTXmo;Hcw~)JMaK&Tx!+*DBL|sXq@b(7_4Wzi?s$0O zXP27|U~wu8t+sHWmLG7r`?n|AxnMl!+kz#Q>r){5@^j%*Kn)ZZ$_4+4yMB9ldXTpS z$$O|~5JO$|R7P$39W4?a_Lu*|LAcO3+1a21jzx#BTKgx{ngNH_xCjgz<#H$!(75nx@g*%od>N%C|xfyUlzav>JaWzxQum*xH9L?lEQ~c)Y7<^b# zu)r>WNvj|6S2zIJFGmB|vw067dgKZ?sz4VxEAu{iYM%RIokL^wzyHJao7Ripiia5F z)cYmKx23D!_>pA`3*7}t;J?yd(rTEGv_m?|j{*?d`->INUgI3#X0=ew>u;^GD({6% z7>ZOs7ys%fJrl)VfqqZbI*xnM(@L!3h;%$AS`A`BKWjKflmE2(bx$6g6nTCedw?%~ zhAK+Tf+50+cWf#6zck(Kj28?#EL=qHVa#eJ4R}y7023u{*E{%jWs;7T-$7&P9{u`= zhhZ&UcCC`ivd|z)pwa%HR=?@GtwEs`d#x9-9YiilCTz`L+}-BClnd{0!|D zD{(q(9xtAy<29T_$Qdw2vK4A#m3aKjFCxd6w5)xB(Wbo#?A_E&Zx#--7nmcxw7Han zK(w=}7qyN&Gi9|+3^I_{^r4Kc$86{CeKE#TG=d*A!rP@nkBQ(B^V*X2vM9b+2#XA#8RkSzcz7TZeftdIqg;iO5d#B2Ebq^#&eg2!G4U$hjoXd0#b9U&Bq z5Dpd)4L0a^7IS8CmJk*(jisMf1%Eg9eZWGU8!i!!ZR{3=JS{ze^700%J=4PUwI`u0 zNUl}RK?_R{uwrO66);XGNwTFpRw_`}^n)uN7tEl0lx1zn)nXkw$Iw*pARMUikP1ym zhYrYwoq+Tf0&!O;=}26YxBpE4p>Lb~N1LKx1Y|AmRR8Wc5pz6VYoWT5UxHR6G2#!m zw?*I{p46#*L|tV#x_n)xw%+cTz!4 zra$)l$>i7bs8yPvsI2*$1^m3|nvSoDWcQxT~I*-bhgg`2F&y&NcMMaCOJ6JEQ z=^IBPxpKSE(jWf~E-Ya>U~_BJQrE90PhbCX{h}Sc*P)AkJslJ+w{MNfy@=&oLX?eL+@$8)$I6aUfnyx&3V*)eSoS8m zo?~bVgu!@;vhVO-d|)kJIXOse65^8E#4PfGUckSZ7u zq%SKbi#ii4WL}vo&36iwLgek46rpL+(7a3;#CV{FX2!q-6Ou(5Ji;cw#gQgczRqFn zyG9|~1(A2}N_X4l`-L343^TLaIn5*r_=#5RpUa6!5+}$da8#R zQ^bnw@9v+f;ROC!Z@L}3+l=OF7INPQk_x--aex=gwPaAF=IGIAQZx%|T+maUy5ptz zh;2~C>R9TCX;hM2VRe+SP`bSHG5R}AFbCl^<)M>Iuq6vM6P7~CZka2ASs ztmQlJLd1hT!0i>%C2aCWL$ID&__I{X4MK8^E}lHx8^mY~I8)uKz>n>Zi-6*rOFBdf z{r;TB+U%m2dE52}lVUk}WRiJtBbTFt#Ztm#>L{(*=peXxxx|K0UJSQVC^=aJiD9jOj>6A8F&#wV+0|A zY2Y;^l;AxiA;EVjMhes#0`8#Por1&}Z?JLs@bVO@ojei42HjTkEo|%2 zr$lFS-Bg!VDXbC>$!oxK-O03lXq@;B2nI=G6~v=n$YBcN@~1u3UQo|S;@z|X`Lj;a zG&gEBOi(uHyl7j+3JWU+lWVzYyQ2rq?!^9xz4r3&)c`oPxh$PZ=_#WH)!wsPTBUvaV2|#GCAZJ^#OIQpJa#QqZRYZGz9*~507eV0y>5SLrM4b@ z_t@H5$xCNRqnOwp1W>Oc$@-Tq6; zGIg^lfJ(r|Vn+FL5(x1cq@`gynmLAOP4 z1LY5%Y%i(98dY;0)!ha<3tL$N+K8T3no^oIjW?x=vr1ErFCCgjwBxaUP)LsdH3qR7 zYwz@CS8Ow>EsY3ai1{-m#)P-y_2iahXe&C@+FIK+eH(4vR%%<1Z?W0!_kZ+{y6xWn z89yGhjW(z?|8$zyMd-n8eA@fekVJi&h|G&xsnL0Cw4AXO>COtrR-4q<%mKBgz+9bY z(`YTXW8F_YY!Uv|93u z{C(E`8K_?op&!xh{odj?aM6gZB2TU59P_GHD;S&f`4uv!*+4SUf^4ueV|q7(*~LKIRMS_;ib0b54E)|vPVezfDxy^L=dK&blEExKrB zxBT6x5?vcP77-Y;Lbk0lbBvuMcg;D*iqW_3=H9JJPP+BjX?N;0t7A`V81r{>)pgAs zrh5gCTij44!F_(zCQtHkg7opl&J!;GsPb)-5phmvdxG&33c0==xFV8IhbB4Z)AvJZKCOJJd~7#wv8(G$ ztG3?MtrBC+#}%QgcJ(#AL9HnJe-jVzk`J6X`Y?7kgbY@he(Ko}(YyBAOwOO5wFl}>;y1uK(z`x{4S^(UgVV)d_Oh(?-Mo3Q3~Mw@-X0_ya2GK)8BqQh(W$fB!l zSE(M3Ya2(%es1$lW@!{5l^Vr?04`Fz{iwNiR4LL;$g9+3xAY}dgQp!v;W;c0X#Tm# zlblLWw}d{p=I+d{%2!;b%|Iig*Xg{B>atL+GLlvZ6&c^_-=hci=q%s)m%6>EU+C0Rla zwo(nu;B$PCgX_mUd0{+Y3;W{Mr?mBi4hkN3YswlzGa9%Hro{wV43WF`Q)+61R~cfo zH-4gJ+~F%wYc|q7VKqKIEr$QFS=Y>sti;6TJk7hPeyGA5e#1v5T{PX;JJh|D!6^Vf zyvf}{!XZtU{-((oWAr@rl?rG7mIw^3)oI!Dew5?E{h*YPDv%vUi;rbHOR0l8Xk`^T z>039y^+~veVeobQ+&LCs9;YRnalbpNjvXtuKz{ z_O1KozJ1@Z@7~u)7m*eDj|`AaCDJ}@bT8Ehb0giF!4&3tl+l#2#U?zJ88Pq4Y(YHS zSefKI8bcq-@Pg&n1;;cxObcOSqcRJc%YghiM%XEA95{~*I zvmagDDLFY%C#%^XHF-c%XAgfs%YMovGm=skqoi8(rP{*+kP1&ohY!ex zo`7^0HUrHhdp|aL^VM4M%l+xoEZxVC;Fvv&Hxj;%-)*fMhqO?Wvqo-ed#3%Yn;Y2o zbjWdImW$6A+nlh6#4RZABfRzFW;u`d*09gsd;rzu&4oHp>f$wF`$CVPWl2xS18Ma$W zf#Q8vM6Ef8ANkCj+MfSFH0S<3HgG6CB{^wYdrD2yW^50udg#5is@RJcsUO461DKc& zYVKW7+fwT2=kEr~>3GDLy#MP)8)}NoIq7|ed^*3JCe2&h!b>lje~uAYmdy_^v3F>D z>E=lVY5DjGZ1Pcz1&Vj^V&G&Fq}gwoIIKX)(Gxws*i{I=BNW84C=47Hd0w)5GpZo( zu2rqHo8~TFO6a!xWy;b~%3vq|x2b4c1iFV&!!#V(ndUB2hr!&+K5~v_9^VVAR*1Ek zUCf`<^kjw*daM6rjv8m1=EyZ~=)y{x4ct9;t4YcaAZ6eD1Cw87Hyui)N$gU5&p2kp z)tjR0pvD-D_XH57SX#kudO*~^vdg(dlW9j|o>Xmz?$Z|VxF@|oKaal@E&^t4KB zr#F(sHPh(m02b-~Jdr06SU97gV%czB<3~`q35IHx(Esv>ZrOte` zf&`)amunnc#D~&t{)Vj&_W! zNx-%z=q%Y$FnpVEIZ}nA?PGFamZnD~E*;*eck{Q#(+#b7-DbkEX5W zad3F|@$6%lv{Y`cG__LrqJR{>+M>duElZ*yV|#6w@X%Siv)%mJ?fNAL@67I#S!*ls zcbHM!xrD8l-nnE;d8(v=MCmkShW_5VggoU%t3Vjo;*pNj&=PM70=nq7OON}KH3R8NHCx5B&k&7 zKnlkrgaKx(zA_wV7zRJOa)f}7Xc%UByYavk_^rR^cN8{dggpRpN1uDG2a+(u#au_K zgB`h!=%_m!$q=$mvfgYS7Dv$T6cLjiaVJfM%jMHOKOux;mC;GU!i?*qavK!1hb|)s z(@jPgx#pMyPz5Kbg9m5>Pf!yVw(^Ye1$U9{+m9%e|8^6>_L^KGrubfSaJZC1$|j1G z3k2GiWi>ImRJC4uS$S@7>Kc<`tw^S3>1opU^;j^PnN3B}{o5rl?}5??g}QrREctF! zezo`A%2u%*?9h^lf^y+xmMgE;q;+EZ>(^Vp4r+`#ky1LVlhcMQ>1fv~NspSu#e7Ld zxm-1_8%TvJ3Qk$kihomtP&h(3TtGC`_&hOTo5*uF`v=cYb2BMX*M-o`(qpUV$DNe- ztfAX84W!Fv^=TNjMw<(&lG%B_3sY)sw3qNFQ7K821w@OJ68dSV)njT=M-h zU7LxC*y?MZ$zyxuM`OPj#enJK20l(b-RlJ`05(HkCdJv`v*wQx9!6D^cyR)8(Irlm zU>+xh2JO58Q-<5I2Pr=~x9QqIkAPhcV`Vouzn9wjV4HTzY&@^c`0bTJdT7Xqi}C1$ zAXec)GWN(=-^!3NDR$Cm*lDv&R1o{3^56s(itZ~~gHn8?Rg zbC4d!L(~#tZ%h&ZEtBT~N1vy=hjaj|5gDC09Rf~z>_Bcb`10=4cI~xi+VzS|Ij^I^ zw?9I#akmZX%sdj-5ToQ9z^4^{1=U{zx!0x{PaM9%ubvRaEjp48GR8s z_NbAwd|l6)vMWHXJpqicE7g6QA;evX0(%kLhIa)@c!MXk3b zzs{=Gp3Q|e41K?fBwU@Ev1wX(h;eE6ZHl)O?krooU4M6iepoFchN&Gd1FKx! z;0InO<1&ARYr>{uIVQ~mnupGn9Y4Q(R{j>AHV?Cp%S4(BBT9roXISnQ2 z{%}hr%Xo_1Yie(sk(Cqo^(Wp=#b=8JuHVe583!~`8EwHz|8i8TnS7mQBMeAC3-~pB zce6ZS9=01;z`WP_47UFG3#)Can!C!}@2!j_e-P!`=8@!pZAqWx{hGzTzY-)M0>X1A z`{SQC-H!e5;$(EoMQhx;kf>{^8;4Gpd?n>2z=O@&Fu?GuMV4!Q za;eMh4rcFbs`V@t^w&ub3Y%eAW=XpORrseD*}UhPxy!CH$8EYTp3okifT%Nec{~~0 zZ1m}<4lCJUkh}@}xkn6eTHkQlcB6@tPiCG32Yie2klPQyS z->K!`!hYC=p}$@o z+`n_{3x-mB^hdN-?q+xBS6O!LA1>5UAtWgZ>=jr>$m{_VW}VmuTzzf`1`Cx`@5vD6 z{~fzvH=?TOo?ux(U<~S3bT8q#!m>-+D%1~`x7=VQ^X9tTA~6ESgw{%J2IHqFGwq@v zd8OJkTi}9Jlp&k}8F#G#OSrHn7o9mqJH0&(bjLfJB2dG@_xWj1*G658rlC94vT@<> zT)3+Yoe>zrh23Te>%|ACjat~Ugv9xCr6E5NFyxUHm2H`)j3${qb!Dcic4Iyz`G{7% z_0Wqh^JY*~3H2e@5O4B_Yr9tal@G;sa=%|S$yxkwH7WX|l8kRWg7 zp9$M74J3R8m755I7@@9|3cBQUFER}Droa?`tw4*rep1f7v1^vtXaw7Vmnpdx1?*3b z*KL(WHV4X4(Gg!_r=Up-%z2G7C3t-)EvR6WzSy%IyCf{7R(+}jz&h@8^3+mi?wXlS zidn}@ywsL*eP`ET9vEN`69(5`uSUp1`+e?*6Dq6FZD}ah6Or*(zf;73uMXL`T5Gr~ibo7C+QI9{U#hs%#zY7rSDi-w8K_$Zbg- zqJuh2M|N29g(S72j;uhdp4caW5um1))^tK^nrU7sOdZ0!sVpSI73Nh_SH2m++G+}& zp#+pr2AVJxl#ynXvz_$gPlVdT93qTRoS#$NbySfp7IE|0~ZE86C1e^v4 zLThc#rYB`Z!0JkBGndC?4*6Uh$hCrD=!N(sy8v!gnqJ zvSY+txBoUq`Esn_nphdr}Rls0+Eu>gwcoK z6q;NshIP3W{9w43OYtycNl!tTzwJ-?)`mLnEu@akA6B(Yq|F?bwN;M_LD%e$n%Y4} z0EHuf!v#P?jlV{T7iQhJJ*O#OTIfqp>64;j4{ON`f?7V(_Ka)S!U z;!SSR%KR32nAz^cq=IqdT3%ejte4Rq3oVOtvG=qxyo|LP@2PcESCOo;s^m73yoWYP ze6&`0@s`0VYYpAO6q;ZT9bgSRK^QB911tJ$wun-|RR(hDA>H4#emPEN!FS?)qTZXs zyQs_unOc(d)l`yNDY2>CZ5<38?aLoEwS$fT3P%8k3xI|ie~l7x@};{gI}7w+dP&c1 zz;_u$E*%Y8Rx0u~?J)+wZqSTk_PyP0wI{a>e7d{J&i31FrVX?aa=*Qj-7|uhCYA>)>*YGj-wrE zrp*WAZi^OKi=(t{RAOs$5l>@&^w5Z*O3kuSfrJ&E63n0a<19)=ZF3-hF>JDEvp-2` z3pjTsJo3~92;t#-6GZag)Vt5FnQ!DmO|k)x4pB-W1a}2z!+beL4WR z*G;Yh{x<-h(nBmgK4_pdTbS509xtk0J&*!~ zVBeQPPg~v_pQo#$X0Rq*tL*i3e2|QPe8k;fNK~NHBfkjIcz3dQ>wkrAkbLX7iM~Cf zzAI`{C+!JAeZe)f5^BoRQCM7R!o2$4t0-(rS5()NC&T?GD8L*iefwy&Z)YCQ63?r~ zBd_J9gqMjPv0^0C+d)oL*YxV9T~v??+D7=pIL4PKvLr%?c;|(MP79 z-bv5QGwisrAJPvz!hJoH9PBJV*j0V?=WDp080z{c>gVYWZ@^!G?GR~d`wX%Dgb}xX z6~k*3`}+N!JN{G_KYpO_q>(0imdoZADrNSeSe)F`>Q8(VC>%=uXvfsh_37~*V%iAw zWFt|+6NsnBr?u|F@=4r;z`J+OwykOPQ}wfJ{Pr%s*Z0WA9OAWCIcGX5)<@37wXODb z>C~y@pu;=f;yO{d#`sB(+?)l z?foT8x477iC1_Vj?BLUb^aOTcfPlKZ%#!JdgN8tbnf;_`J={eYnRUMd%cIcskx6F? z?|CUUc~_uUrKZmxC#9C(W)$y(khF1zy|MuxT$Jp#&Y#FU`^FpAckxZ9#t&%ICACPg z8qrB%sb?Qio@hY${Y>H0R(4z$MckPcK zE{y5zhpLV4`*;@fhdUUir28`>;xeJsa@@mwMT<1~v?D|`VTA7{U`v=xeL;QjurKt` z!fSie>BiCA&j?nZOWjf z!@3F@JsJX6WiTy1U42>o^2x6?F!QUlbniQwm*f65wOslpXNZYfws%DE>EkcGu1@zs zy86MI@`{?Hk}Y+mBWAF(6k1X&aO`%Y!I|xNy#QN4q`%j$py?Y&==ATWVY1?;_m8jZ1rLVa#uQGms4gY{M zTDv#+DsK!htHo^jiQHrYZ>;|YVfK-6+tJI9=AE~~4csS>}Hd(6Xc-|zT z@J&$nC0c2xX9PzexTuZXOcnq$!kdZ)(4nzBcUbTRRj@5;bo&7t3(yT z@z1wRHhS)883EkQSa#lYT9v~5Ulh0SLjvdx#9Ji;X`3U#tMe1qu36N{W(%iV z0fbG9|9V1D30~8h<2oc87F~5AIDOvkbr;JxTo5XsKgMT%fR|@3pdG`c-daVpXh+RQ zbB}zTfn@2|g;GRht8(R36wO^-$$c!I4?(p`ZJT3|+j*$cegDgU_RF#Ma`MEbc zO59B9;+>uzBYgWBlD=_)8FMt1%+k18ri~gD;_jeJ?>pi9#edC)3gzRaRxc4>%5CeP6)Z9Up&zXa3cf@m0P_)c2o#O)L;?N2QeOH9!)KFNtl(rt*AT zxV_Ik|9rgNXsPg}@EOXIR;Nw@Z7~!vgaNTMa^;i&&#k~qxdr9o2wNHKwfJI#mRtNjgHo|{ z`px2=2U|qbb&{H*uAK?{T8bl%v(g|q;#5D~4tt^%)(m67gROHs5N|zaBy&EGrb`u< z0~hSAJ*TF4GuDSJ!HV$Lxse`Mi&G@-JY@P`p7*ABGx?>TW>bnO+b3{8{OoY}+3(Wt z1?~pzjg13B(M8|%b<x+`|HnUwRb();B*OG{|kROW1-W2}YMecEk z9G68I$1CGmsgC_7sN3OYAxHgAd3gC11${L5te6dbN|r|%J4Ud@+Dk-o$HTUPyuHw7 zL`nNT>UPtfo{to5tmd`2+SFot#vJ!G{^jGQw%X655v<&rudJVmb`_{ZoMf$cwC!zs zgQ$7z3Wu{%3$LTzQ~XXR2U94GNTP?>QczR z?&H*7O-9sv>K)I%2VJnZuj0aFv?rdA+f81HCQ#Z#qM*V_Rt4@EHV0vlRmUfIvJ1? zp}Qw^%}oZs`lAPjfmieVY6h~6kQ)-o88zk>Gl@s|YJRfENe~{7)*L`aB8G_eH9j;x zHs7RRRp-op#uFn2QjC}>mw$$N7rB!Havz|Ev#zTAX1sh+X2>@JQb~#@ zbCT8x^0KQ^OHoi~!#dJe$muTC*rU0esoy!gJsUlL!5XRAj3#C%nu~L!oH1%U`=Irq zO72i{`Bjd<^4gm9lu-alxs<$`7jij4XgxaaCBJ8d{7w~0$)N^IA;Ef|Ub?%s&Ai7) z_GApUB4*kC`8DexZn8?iof>~tOwhc#yck0Sw1v2LD!~irf!}FEet!*vxd)m*RXj#` zYurho*h9-zIC%=9B)%iz=KAiUX1bcsRy%P_?NN>^`g$PSNYeTQa$nqUHa_8pxc54O zOyEeGs(qzu(g7}T03+7e-9zLy2_N)K?>8C7AH1idKyktKqA5`T#iIOfC9(+Ec*dX! zU*DWhJG#R`Vdr=0;08q#i8H4>ZP2y$D>66a1b*Vr?0EY!-1>%9N5H4Gf3-5J_21C4 zL4s_5iQo&u5^TuZcgOGCBJ=yro)c^h+gl8J{d*p)X%EXE;#S(rT7#v>p?XVwilK2_ zN^XddD5y76x}S0zY^rTdL+tQZ(OWw>H7`)xABmZD>*)n+%1!%Y+j*vvm+%tx;ylL` z%MzjJU!l;z?Z^!g+L|=@PY7l4bSf5xF*Iy%j$eC2rpVCrCZ-zx{!V1kNIGSlqJ^Tb z91h715fXtyA2fo0Z=7P72d zGNC#&*Hw}89w!&EtUPIu#0GYjJFV$*b$kVD1b?K~Sc;`W9W8U(swL}5ha@&2N_U=E zrcxdRCUVG+)XIz9l*%t)g?i|O8e)6|^bcKJWtGnkMO2;YEX|~3qXVrytWicTw;e>S zG2&C>YdpKknBv`wj(q3l>LGH1fz*d+yJkOF`c2F8{fh$JO_j@$Dwe4U7XLmFxhz;D zE~6TnsB5YWRu}%EBr66#OnIKzZckNijR+~AfasD$q7?Cf*g=-l?d&5-G8$~RehuWMZRq6baiw+Q zV`f5ijQ>9g?Ffxu%L$L!aZ9$I5wGcW@!O*eaM{%}k9JC+c6d~}a?&psdI~;Tr38HZ z+hRyh6kL|_Xs~w1Bj#~8BEDN->;dGc<7T8UVk=*-6{$B*|3An1!fEQ#C%mQo0r}U& z@6`OPkJ^9rQFSY}LQq{)Nh!V-Lu;uuh)awcP6q$G1a^sib?elN6=>IfmqH<>v?3)W zg6m7xe`gq*cwjl$PzL>fzwzm{Y_s}fhE?gU$?puh`_sW}Ah>BfOZ@JZr>AZQMtjzT zFodiZ>eg--s(WSst`3{@g{9J)1Fi{_FCz6RpDcqiJmxH9Eg+8+=Mz2$KhT7eMT zC?EFN@aKh(`E3p+FcPW8(=N*ZNucPXvbju^)urL?fp%RXcSK^Kr$8|J_w*dx^!PWP z4ijEKdcIaoet2Rq{WF)ih@ zE4Hk`%j$vJqWc>ao}J8VIDI@B=y8La)+xM>3Vc+r3_<{b5nw27zDm>qAPb$RC?XKw zOjD`p=8@7$E)Iid>T;N7u4y!LQr%f>d&=!4htm}~g1EM1V-`hsEi#&#+-HZh{wbO* zoPO2`IQOqLCj)^4?k$_e?JNuq0ePX(cXq(n#R z3$MlL0+lWxS4vk+Qy6VEU2E)IgYx?t*?A?5j+FOpc zEYg=kN$~5Dj|{Hc{rYds+3KPrWILm%zys0gp4+B|{pdEd$+*$D6ZLb!<8cc1m?TIw zL^0MH;j6ByDxEHeM;rKn9F0nDY`~y?h))?U=l)^Rt~>CR(2T^y|vZpC>Xg#2IDRL&634`}llQb+Mf1t*O`>iE&La;*TZ% zNALyFuFdzP8H${D=o?6HsOgLN1FmLEBzv6Ro>m%=FUEk-QhL&L5^!AaqIpn$HM1l@ zVGd6*Ite*^3yVZm<+vXQjcRdKq+&XbqfEs8SG_*yHQS(E6|^9)At-hdQ0tj#@7!~W z5$8TUg~R(A?UK-Z@COMyWK4Yzddr~3doV8B!) z?FO5730S3XU4-F|-&@KcKT_dVdBmY5YsBWiSBxlZ{MB0 zsT`KVrS8%muvqa%oY1LB*Mv0h?;|9k_YX_%syfyh0cfNBhx8D^o=xOwx~VINp=*8} z$~7g^=Y2I%`@faPa{b~~e}T9any+QO=$p)9)%HBV&1!XN2vb!?e^a`0=|)XWd*Lnm z2rf5HlU>5Igz6M+E{_tzrWyV0rIt6I!zcly=zg}zrVHnPbP!lRH|KWuZ29O$JfglXnQ(HWDc~{^s3wi(f)h+Mm4p@cdYk+gZu)Nq+Zy z+HDUo{g%rK&4kEoyUDcwd4y_=W!NsKn^BWmOzXf9ydCp|qHo#1neZiB1Qp<5)axvi zUPmYRHqBq{R@9y-Q7#LlMgqk|kX40LR$&GfDN7BiV?lsUusyhsBND#?O;#yL2@%5P zL96aGXE5}xFy-E(X72MEd&o}_Wf|>M64ojAqfmY$vX{(Q#ULE3o&}-Y|)x!F%((}l!m!E*K(%j!+%YVt{ z(ORoADX-;RDYZ}UQ1bJdhytdHF=q-AZCBD)_K`;&8q1SxIz5vVJYD1j*Y#&x*{~LL z+#7W^FK|AT;&h!R(i+ke{1259L1Ei%qNg7>vQ)G7k!iwmk&d)=X+SB|{nLyxK-7f7 zz{Rrp|KGTd!+aPi#P~6lXv2`b3OPS3QK%|*6<$bbS|9>1*W@~xxfa0e24PUUeWXno z>OP8i`Z%U@fqS}fV)E63#^rd*F@5jP*3AEZ(Tp@9z>`8#w6$3}g{Y^bC$*2>cq8pf z)49T)UuMivw247C^#j0#?0|5_%3Rw_!d=vmC|8?uMKD2MY8CA;JlnC58{9O})psJQ&$_AX_O0XLYXHjP2bsm*W=DT+5izeR z5;4IVMvjMBECUC@Wbc&ez={m#@ImIQTa+Y_R~Q?(bQhsC$=JBzuU4+f$O5T8w1uP+cR*!8sag1=X8f{3i#B!u8B7(^Kac0QKExZJAB<>P+(6Sf-DzBHo zK<^hS73xsCRnstqT$%AYk5#5fGQ`$aiuGs*+a~v*36^`Q~b+L%|4GAz(3xqw?NubJ+ zgMP0;Z^Ny0WADLTnJ{U0K}~U^s?ALbgi7D5QmLo)l+89KP5Yf(D{2U{H?bKhuWk(O zNMBy>+8(cRmF}07T2sB?B+1+U286m(J>bc;K7_9kb)J7+2m8Od9Gs7f3# z0vKxZ8A1PN!K{bOC_;y-m#wNdF}jqDugKZOjpu%rv&WX}WWbY@dzYQ6cM$HKIy~M) zZ^EqwJNKF-vivT|xFQLF0bPf(<1QMn10%IPLiM$U2=G7hC6FGygar4IEhfEyIRGnU zUtboP`IZp)Y1VER0rjDxNQ{Lb?ka5d9m0kJ!T*0i+>7gH&zc0L@}m%9%o;;%AYi6y z5(r#gFaqk1IjU=53|dn|d^FU((PJW%%274`8P}gB>%^3*=8+_PwOV^1N&u~vOhN0z zCyq#u4O$sTrwQJ?bYG^e+Zu!d3bedY^0r*02jYE`*#e7UO>Ad$B1NZQ-=}9cZW=n8 znJXrOHCghedVuY?G78&uiPB1PEzMOFeI3xNd$MI3zra37iPU}sgyswX7N`Iepy;kn z**XX2ISuMVZ44hZJfI^gfPptqNuu4t>DKwyL$9v~wf`7)Y4h(<;J2dMNA#lo&tcn= zoK1QT+Mro1uE8L(He|uL{G-2SN?F?$)!Vucfc+pyebCmo?*L037&6KT+O74JleTKO;M_ zia^cn|1b@V4FCJ(KSU3k`>uK31)rhei2a90xT=Ex5M{0{8Or7OT7Dqf68iV31!r6N z1!a#FL3(PugDmD`>x{b;r`UK5`#Csuv*EP4ZB`WTxeeumI;}Uzey7Cq-~WsBN&0wo zxnkMvJG#41~f^mbWgaVu>m zc!uQ-LYN712-O%JP7qC#us%}Xsc;b1E+Jq8RwjXEkia>#a8|l_U|dAQ$Cs`2*n_JPP29Ju5#5FKSGTxi@?%4Qk-&luyQT;{JzOGq_|&?wE7I4Vg}J#gvp ztQ16Fq#Bn3TUcF-NDeJtkgjRgD36zwt!rt5l|krekTbP9uo;Ios>}%=se|m%l&=KW z&(FVlNORUMx6zof+UWP3mPTUDOPL*WM)C-_R+5bp9PTVRVn&~vOt9$nA*H_F5?*mp z{CPG5$Q*?WCZ-59wG6ZvW(gYNpbVE$mE{QUIRz^aIDR3~T##m-$-jZ2-7H1sKY8CL z_PUkBO)bqoKNMn{iBS1#d}935KLiT?zkeTTYOE`e^_iUL%FK4Q@4fFP@zcJ=A}!XI z?>U+{pKPlwz=|P|CK|?j8+dS;3vWLRc0#oH1!|gq==+haYrIj7I1;58X@tJ|AIpX( zc<(&4Ul_=+J#|-(VK0w8>*{`Qh5qH}VMwRy{wZM2DkLB_8gIW@=;&>kWA5XGT%$W7u} znLlDT2a#hgg^mRAaO_1sXf;iS^Q(Ko7wZw)e?U}2uNI%MNUbEv(+SETCd_Q0X7<1b zT>X9Gc<{b7*I2U65nj+%lQ;E5ViaYmD72kB3Kv*OPEb@aiet?q9+NKj+bQm)C-4JG_iCgz9?wFet1x!#`fwK70&Ah1GLDD43vH_~ zhQ*kjhfdcX?^dRao`qeBW3is~w6Cd+Z-I!bY9@tOxj(|3!^e}JARvHNZTWK=(O04Ut=@*zgF zU%aBGPT~JA2l`uOw*UG+ThW}oSpS|iUps~n&rI!ct}Lmf%cyL`L_y3RxhNo5bjIWR_Ph_aFFXR;I zJRgnZk(s&XX28Vo&o~+a?9cpD@VU4z^kuL+xb&*RVd=a7?CBmOvy^#)+mV(f<%#C_KHkxXaAn6FTpOMi?&`>nZCho>E;Y4l3G-O~79H+Jeu++R zBfmsf{ziU@u7i#I65Y^A{oRPl^-k{Zl6WJ(ME4{c`6aq{w2@z;`_m1wQ~we@kmgC2 z|Hv4BEbdFCxPj7dZ0g;gqX#Gd5RaKQsK1@@*^1Jwv2%O-4ho;tUFd&w=qgI_?*bQh zv&}pS58X)r&QQYt2eiLLw{#wfzhemR2KP5&hSlmCkUh=0Z3;urCg_(pss-W6|)SHw%=8NS8m_zWNA!#wf8HD^|4 z1RT)#yaZV?efQPJ_b;D6dT=}S+Ld!>jvsZ%0jsPq*+gA+(MYt1^BX@$K}$atb>XRj zKC_9t8#7)U7wAPMg1PRBh#sDtULOMH-~jlt(`gYcX^a>BNjZc?u_fqMx3Vb_)1)w=khA!GRks1dfc* z`+>e24b4TQa6TQrec&IH5@e5XlaTR+&@wS>8O=zaT@%96KV=J+)AWMm;i!bOZq5wN zgc!!P3fzxf#ex`x%lM}0nrdeUZXm#aTgogw#Um$tO8tqI*YjFakF~d z2;aB!_u=argz*F)F!gN34iw|?GDf)If*d1nsA>{lG(nQb#tyz4xZDm;$h( zOek>5)lH+Vc1?kAx--ayMvEXDtPQyH9RG*8+C(!`DcmPK_G{B99F&If$i_<3hmrvR z6-~H}`eqf7V#+XM*wnZVo7n_Dt9(U-hR}g1&zFH1ER>08=}6#eJP7JwY|g-V)aOdu%$&H}=rn~k=r zIkYM8<13+V(eu^)GfQ=?@T5ZQU#c0@d*}mf`}3XZIiz(${HsROXF0aS)b-iip&rO6 zwq}eDLgvI3A$5UvXOs|bi()%`6#nHzcn&9#Q0gIog3*Cw?T3ndQkH;@ODTuH&_`TO z@KrZz;_3_)5x{_>!W098Y_21LODbmTSUEO&E%G?C<4Ir^ShxIjOkgJBx+8IgkSs_E z?qNjNkrD(-olk?!vdk&(0#f2GA6*}T@Q6waoIak~Hf;2ptg__x_6DloCp5;^NyJ7P z%@8FLbkLV1;CmP=6fN<0taX5RyFiBqo_2EHG&2Zo55gY>6ZvdC6&3COpG5>1NfFn0 zqD~|z6@sP4LgK+&*VW>~*sBU4C0urs1n-2&=FBPyabw|}&zA-)3Y0L;5{aftrsBG0 z^Ag%Bp=*gq-EZt$q(d46e)820mCqznXW5~_eGDO8vMnr(!-h9D z{eXHiQpO|SJCWz79+6GwAw-g+oV`fWRg)*0MK= zA_EbKm=nr%-H5h0Nj+kzO1}Ux=OSI#f(UvM&z#hF6HJZ*?`^ zW#antH%E6kMhirYJ6-|{!POk~V_2rY7HN9*d}*DB2?0Ax!iG3&zwt(0r$+^#o4)?J z7y*LE#OkQpUC#zP))4jqb3tk#!RXoTli9v2mQuv)IJI2kiLwO1N^S25Ea%8hx^mPJ za>#KRTnykBp}F29s;ax$W;>Y$B7Z0XSu-)naSX4dlZcj{LTqc5&)pX|is0706uc>2 z;uO1gCK7&Qd{ByN$gThDVwC(QQY{Pg@vi5|x(4GDBq5~`=9X-7#D~}CA`96}J}=~` zcc@er_}#~nKqH77Dk`<=NSU-8ME+$aaTm%OUWPRoLaXWRfz6Vv>ymI*St~75YkLzX zA2;4y&;{e;AL^GOdrqb4sp9o z_4yX_9T|ES==;a%{%&%{4+L=wUE@Zrr~K6T31+HJ_-#!<*6~}vVsTJVNZKKh)<;~aXhL!XXca?d>i@Wp3b%L2fie0Gp)nM5hWB8 zbB8&klY1Q1vq0ZDw``ytt#tG*FF0<@Fa^5}vlBjk)x|4ulKC3GI@lK1C<_m? zG}~^7F=I&kpq3V2&iH0M`Hg_CO9@~KAsIc$mqW2VML9c!~b z1>KuTRZA}(8v-UNs{>wl2gVloKStacCZa1z3l(`ElE2T3iq>UIWA9?LvRn2ih7-;0 zF*YYo7ZCcbCsY>|s{kR5M~MHFu+3)T=3M9p5?#daPSm2tH68cxrW7P&r0!UF>%uPXpERh!7f7ZrjRgW z>cdwh{$^?ljF5TUEbmwkM-Gww@a-&`XLh45|5aoub-T<3AG{u1O5(r|?T35vfXq^| z_2xw?JR{a&vclzYD=h5&=(yPEEB6@rweeeNJw$n~hNK#5c_Z4ou+tYig;hg?9=MS5 ztuB~C1R#{1+xn1U)hOuGzU>wnarg3sMNE5URjCVe&r&+@%_Ovl)anG&^bSv969P+M z6Gu;iN)5$HMu{019T#!NT80Iu5~&>~WaSb8i|t-WY28y*1cXI`r2z@5rxbxk(;j2g z3uBvu1k0dhT*jrthKRBRH(q4Qlol~Kg7H9Ka;{0^Ul>tR_48vorIuRY>j?MobY96L zu?P3|3arc{k?vu0_rEpfVO+vpuO`KvaPP?X)b0uP+l#vfngDADb(eWmm$``Q`R}$hl;x4;dD~a6-PU=I{bX**$F|+ZA8~ z7YX1P?(7viuG18!TpVI@3R=SOj{lSeaftk}8X$$# zv=%T^CaAB0ut|8H8@P1$n6@b&wQ2ZJ>zn%u`Vs~b#+*6IM#ZEq!<^65W{itVBo{XI zqL*q&$qAVyRNVI`qjw_EKw6{+b-CA~Y@KBvp;I*Pkw@r4#*R|8T*!~A%y^c``WUkEQ&(!&&h}Z*Ba%d_EFOe+Q0HkA zCUhIDv|PT={aNuBXaS|BK-MY(Yqs%SH7^(iW$dc(SrO)J+He-mj=q=@@O@=`Vdw;D zo7mLx>{Vl#h0YTTo3GL5iPkB=Nifb5s7<3|H@nZ={=ko<9_e+18GdnQNCYK2=sLd z_3jcv6X*HXJ7@F1bL+yH{pNWn_$S+J0F8cCKdBrNNMWRYkF&s75v=@MjJELtMC=+R zwxm(%Mrf4gQt?))a96Q)=Jb*wF;^Ovff;riwqmnvvZD((`@V46G@A7uc0(y#CFe+I z9KToXZ(Arb^WteT(4yQm7-z`a4Xk)r3)#j5BB_k)PDsXwW|`#fA1rO!{u%TlUd{d@v%oo#R;gb4~PX}N7ummhiT z_!`aPY%6R}cW~mYA%^ULhl;T(dX1uUBzfsB++~~m?D=Q6tLpK2DbC*vlb=BI4q`Vu z2$5bU3LB529nY&Iu|inZf85sid}_KzDb@Y0!WG@rcVxNC8_cNaHMWDiLTYm(R5j8B z^`X}p%}t_hr%b-M_3h3VMw=4vY+(cU8A`sxH>^f0(HILt?-t~K`?w5taX$BvrD8>& zB)Gzdu#1S$M}to2qo4waRY6E9pL&v^shMU%l<&VEE3~mlaK7proZ8M*lerGoc8c-F z`%_GRD+tK=)1t^J@4si)2NbhJr~I{VTqPIOdrsbDNcbELhN*aRR=l9H3Kfm02Z%*A zGlCk>+z>J=!zco@K+-Xzqbe%O&rycx#(e((T?KK03ikq$N)je8O(x_AnWuQ;vGUfS zB|=*8@%fsht^G=*GPE{r)Sm+eTi>(&Gqq|WT;>}y>9$QbcC;**WY#v)*1&EwHqB7w zlz9E^!8mQa#Odf>v$zM`d)^%NyG_sJJh+gbnq->zQWGum0iS|8TfNTQa^KRR<}^^* zzN-!=LQ(I=&>7lJ+p%}CJ)3pwucAAW`<)%9%9{X=zDZvmVRVqgrjB}w=)s%Nr2%`iQU% zbY0o>&zq0TdB|My^h8a$AC&jMnOH7Hsl z{cc{f*$%-#64Az6qP zTnaZ1R>q?Wa$mm!711++Sn0RgV+?2x@5HtT#sd=fzCq~)`jT9BP3It6=z}4JxHeWU z?NsD7DkV6Uz_s@|uTrv4l0~ruIU^w(GqOVLLEl&saaEWP9>)y>i7-)Gqq^moO(VmE zn&jMiwwjG$^67N2s{XsZE0;Twe|>{aX8OS;u?C;mC$ZDS)=Yum=&-TPt)CBRwKo6s z=ydiT5U-s~T=UQzHVhm_S2>kPr=^l}SyNG}N?R_u;MG>srk_6J^SjK7UjzK_1l3 zxBw+a=LM+ZjADG5W{5`4;MX^{653Qw%Eh7{g#G1_6pq;MuR(XcAc6lfQeONqQx~&K zX~j?SwN^u;W$n`gZF=}KYh2%rJa#<@tq|-TO5UBe+=Y*+u zaV|^XAxS%TQ9inbif24bsO?{wHvm${dCaxEawOhY(;6&E<+PBQ&q8re<;L|5xJ1F) zry);0hl^G|f54|6hVECV51c^@RS;>9{>xlVi5$|O5QA<*rZj`{_7N%{X1?;S9F(E^ z)voZfR#_mVFd+C!w=|BtMgKh-LI}{zU1mhGiOhHfu3*% zz6Wn&-RGayqNid#)Y_2_)FvZQ>D4V?W?n80{D9Ud$$xjVmrytzT`6El^t*v}CvV`R9~%cZCP9c`H>#(`%QK~_d8=A_%RRrPSi;a&(r0H=IU+=TmnU1~I|mhOQx zx>@CwP3S1YAVxPJc(1TR`JnEqieL8Xs}plGm0r;#s`0&LJ8KHipK2#=H#vZ&Z5qZjpk0$f<%M4ro5C7WJ zorg4sWZCKuMJj*8FAVZGv%=nEViy5k6A$5=hHLRUmP!Is(NQ8XTGN>F8@Hs=S<~pu z#Moee>jf=4UcOudUuReVL#GvBLUY>NrF` zj#w%~=z0NJyTag{Wn{I?NrnxG;jxB7M68pNf_s<6kAX+L>_LlXgVi80!hx)+Y|{@r zl$WK8?K_jdLE_!XM#6sc_H}N^mx|($=Fp4@tC6Y``7ve+YK}J)^D^PxD5#2oBuHgs zj{0d1t=m#<>M?xj^VTtMnf9Pj4Lz=hkKlMW=Bu&zR#(xxyP7#IKJU7AJZqmrgTg2y zh1uZQP>J{NK|$T1k&V>4RH1gO@lNb(66U6$uPyj=dN`&SbIslnH(O1)#BCHICg zSRhr5%Jo0#g^yZ(xr374rNDZ6OSz;;l<6C3MJ}53?gk=Bh75Ub876g$`D4vG#hc1O zHWgLMrCaNJ9o4#D`X0H>m5k1}H4#h~j7`4Lu=_I2pVcCB&0H{AopHyc#M$>jc zbb!zv(P$7V8|0s~?y5dh&|tI>(RRZNDCpsk^D=mRfJ$kRP|}!2@{*AqimY$)&EEt7 zDRPUcMF`KgRnmG+P$)`Wb-c|Fc&OzPKAu)I*C817rOy=7;HEE5reI)gUzd4ZcR>nO zY><|trEk5#69xj`$eoFm5$2^A=0)hzvtJ_L@)#WS`PC7Fm$u z!#?r%4Bdd*{cXH{Q%1$*713wo`Q^kGqm&k!>XFfs8BZ|8ko)3W3ypj7*PXktoBmLb za$5nNo`t>lX!Ob*p?87z7ad_v@nUuprZLvR(k1pRK9q7a2P(2|m1)_J+}y=aLTm1?FxF@3P%_3k%#$j8FamN zqt=Zxv{dHd155-YE3-IP)|x5E3d-jPkjv-aRK#MW~}gzqmf;4xdmF?)Xj1KYUp0tG&L_vV45+oo=8X`yDGm7PeB zl0z&RHau!;>xMDcBPDg!ew1FC#3RB;yRbPv)NALQQA=`*Lft>M%5^Il)^3z(T^l*_ zqL_tbPstDlu0-!ly4agjdgv3cSf0ASY}c@�is=&%!3(KK9V8zjXIKXHnO6P>x^~ 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%0A1?l&0bhuF_~zWy#lEo&#L|)h=LEsJ}}@T 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

5sv2}@jof@6XAYXVR zCcb8JQc(n$B=ndIyPgEM)>NF`<&0D9-72(!O z%w8(YIbzO{68W|iLOR?3lQnA0sWk>aD(fmNoP5AriZOdQ7IRjoxaEG^Qsu{1s!)v| zYwVLwR2s;yR8}f<)h3GeLe4Rn^Pc#(SFfbThc0wOEj(#dhDjAS4Bj*=Mue_7T1vqk zt5Qt;BB@cz7|V8HKFVJJQV7|L3iKvnLVvNp2C&69DhGh+Z&)a3UJ@z^UY?}A~`TP=0$M+k+Qwn?591xz6P9sR82!;eNAmy zv2V$q1)VGLmuF#d%iMC;<5swpwMJise1m+0e1m)=^t}a*z7??Ph;q3+sk-VN5~2t% z?7Zq7)4y{+j_t|k!kx7a(AoE6fb6vxF!oWLx`U{Hcpv;rh`arNe^rLC^>BdXSbXn{ fdTV|VGdmX*ztwJ|p-y$Vhd46zE 0) metas[0].setAttribute("content", isDark ? "#080808" : "#fafafa") + + if (themeId === "oc-2") return + + var css = localStorage.getItem("opencode-theme-css-" + mode) + if (css) { + var style = document.createElement("style") + style.id = "oc-theme-preload" + style.textContent = + ":root{color-scheme:" + + mode + + ";--text-mix-blend-mode:" + + (isDark ? "plus-lighter" : "multiply") + + ";" + + css + + "}" + document.head.appendChild(style) + } +})() diff --git a/packages/app-bundle/overlay/packages/app/src/amicode/inspector/device-inspector.tsx b/packages/app-bundle/overlay/packages/app/src/amicode/inspector/device-inspector.tsx new file mode 100644 index 00000000..68ef8d65 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/amicode/inspector/device-inspector.tsx @@ -0,0 +1,123 @@ +import { For, Show, createMemo } from "solid-js" +import type { createInspectorBridge } from "./inspector-bridge" + +type Props = { bridge: ReturnType } + +function isRecord(v: unknown): v is Record { + return typeof v === "object" && v !== null +} + +export function DeviceInspector(props: Props) { + const devices = () => Array.from(props.bridge.devices().entries()) + const active = createMemo(() => { + const id = props.bridge.activeDevice() + if (id && props.bridge.devices().has(id)) return { id, data: props.bridge.devices().get(id)! } + const first = devices()[0] + return first ? { id: first[0], data: first[1] } : undefined + }) + + const refresh = (device: string) => { + window.parent.postMessage({ source: "amicode", kind: "device:refresh", device }, "*") + // also try direct parent (when not in deck pane iframe indirection the host still listens) + window.postMessage({ source: "amicode", kind: "device:refresh", device }, "*") + } + + return ( +

+ }> +
No device — configure one in settings.
+ + + ) +} diff --git a/packages/app-bundle/overlay/packages/app/src/amicode/inspector/inspector-bridge.ts b/packages/app-bundle/overlay/packages/app/src/amicode/inspector/inspector-bridge.ts new file mode 100644 index 00000000..8e4b35b7 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/amicode/inspector/inspector-bridge.ts @@ -0,0 +1,122 @@ +import { createSignal, onCleanup } from "solid-js" + +export type RunIteration = { runId: string; iter: number; objective: number; inf_pr: number; inf_du: number } +export type RunPulseMeta = { runId: string; drives: number; knots: number; labels: string[]; bounds: [number, number][]; interp?: string } +export type RunPulse = { runId: string; iter: number; dt: number; values: number[][] } +export type RunCompletion = { runId: string; fidelity: number; iterations: number; status: string } +export type RunBridgeMessage = + | { kind: "run:iteration"; runId: string; iter: number; objective: number; inf_pr: number; inf_du: number } + | { kind: "run:pulse-meta"; runId: string; drives: number; knots: number; labels: string[]; bounds: [number, number][]; interp?: string } + | { kind: "run:pulse"; runId: string; iter: number; dt: number; values: number[][] } + | { kind: "run:completion"; runId: string; fidelity: number; iterations: number; status: string } + | { kind: "run:activate"; runId: string } + | { kind: "run:timing"; runId: string; elapsed: number } + | { kind: "run:label"; runId: string; label: string } + +export type DeviceBridgeMessage = + | { kind: "device:status"; device: string; status: unknown } + | { kind: "device:actions"; device: string; actions: unknown[] } + | { kind: "device:activate"; device: string } + +export type InspectorMessage = RunBridgeMessage | DeviceBridgeMessage + +type RunState = { + label?: string + iterations: RunIteration[] + pulseMeta?: RunPulseMeta + pulses: RunPulse[] + completion?: RunCompletion + timing?: number +} + +export function createInspectorBridge() { + const [runs, setRuns] = createSignal>(new Map()) + const [activeRunId, setActiveRunId] = createSignal(undefined) + const [devices, setDevices] = createSignal>(new Map()) + const [activeDevice, setActiveDevice] = createSignal(undefined) + + const onMessage = (e: MessageEvent) => { + const d = e.data as InspectorMessage & { source?: string } + if (!d || d.source !== "amicode") return + switch (d.kind) { + case "run:iteration": { + setRuns((m) => { + const n = new Map(m) + const s = n.get(d.runId) ?? { iterations: [], pulses: [] } + n.set(d.runId, { ...s, iterations: [...s.iterations, { runId: d.runId, iter: d.iter, objective: d.objective, inf_pr: d.inf_pr, inf_du: d.inf_du }] }) + return n + }) + if (!activeRunId()) setActiveRunId(d.runId) + break + } + case "run:pulse-meta": + setRuns((m) => { + const n = new Map(m) + const s = n.get(d.runId) ?? { iterations: [], pulses: [] } + n.set(d.runId, { ...s, pulseMeta: d }) + return n + }) + break + case "run:pulse": + setRuns((m) => { + const n = new Map(m) + const s = n.get(d.runId) ?? { iterations: [], pulses: [] } + n.set(d.runId, { ...s, pulses: [...s.pulses.slice(-200), d] }) + return n + }) + break + case "run:completion": + setRuns((m) => { + const n = new Map(m) + const s = n.get(d.runId) ?? { iterations: [], pulses: [] } + n.set(d.runId, { ...s, completion: d }) + return n + }) + break + case "run:activate": + setActiveRunId(d.runId) + break + case "run:label": + setRuns((m) => { + const n = new Map(m) + const s = n.get(d.runId) ?? { iterations: [], pulses: [] } + n.set(d.runId, { ...s, label: d.label }) + return n + }) + break + case "run:timing": + setRuns((m) => { + const n = new Map(m) + const s = n.get(d.runId) ?? { iterations: [], pulses: [] } + n.set(d.runId, { ...s, timing: d.elapsed }) + return n + }) + break + case "device:status": + setDevices((m) => { + const n = new Map(m) + const cur = n.get(d.device) ?? { status: undefined, actions: [] } + n.set(d.device, { ...cur, status: d.status }) + return n + }) + if (!activeDevice()) setActiveDevice(d.device) + break + case "device:actions": + setDevices((m) => { + const n = new Map(m) + const cur = n.get(d.device) ?? { status: undefined, actions: [] } + n.set(d.device, { ...cur, actions: d.actions }) + return n + }) + break + case "device:activate": + setActiveDevice(d.device) + break + } + } + + window.addEventListener("message", onMessage) + onCleanup(() => window.removeEventListener("message", onMessage)) + + return { runs, activeRunId, setActiveRunId, devices, activeDevice, setActiveDevice } +} diff --git a/packages/app-bundle/overlay/packages/app/src/amicode/inspector/inspector-context.tsx b/packages/app-bundle/overlay/packages/app/src/amicode/inspector/inspector-context.tsx new file mode 100644 index 00000000..b50570e0 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/amicode/inspector/inspector-context.tsx @@ -0,0 +1,17 @@ +import { createContext, useContext, type ParentProps } from "solid-js" +import { createInspectorBridge } from "./inspector-bridge" + +type InspectorBridge = ReturnType + +const InspectorContext = createContext() + +export function InspectorProvider(props: ParentProps) { + const bridge = createInspectorBridge() + return {props.children} +} + +export function useInspectorBridge(): InspectorBridge { + const ctx = useContext(InspectorContext) + if (!ctx) throw new Error("useInspectorBridge must be used within an InspectorProvider") + return ctx +} diff --git a/packages/app-bundle/overlay/packages/app/src/amicode/inspector/run-inspector.tsx b/packages/app-bundle/overlay/packages/app/src/amicode/inspector/run-inspector.tsx new file mode 100644 index 00000000..ea0f42a6 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/amicode/inspector/run-inspector.tsx @@ -0,0 +1,124 @@ +import { For, Show, createMemo } from "solid-js" +import type { createInspectorBridge } from "./inspector-bridge" + +type Props = { bridge: ReturnType } + +// Minimal pulse sparkline — renders correctly at narrow column widths and responds +// to resize via viewBox (no fixed pixel width). Each drive is a polyline. +function PulseChart(props: { values?: number[][]; bounds?: [number, number][] }) { + const paths = createMemo(() => { + const v = props.values + if (!v || v.length === 0 || v[0].length === 0) return [] + return v.map((drive, idx) => { + const b = props.bounds?.[idx] + const lo = b?.[0] ?? Math.min(...drive) + const hi = b?.[1] ?? Math.max(...drive) + const range = hi - lo || 1 + const pts = drive.map((val, i) => { + const x = (i / (drive.length - 1)) * 100 + const y = 50 - ((val - lo) / range) * 40 - 5 + return `${x},${y}` + }) + return { d: `M ${pts.join(" L ")}`, color: idx % 2 === 0 ? "var(--amico-accent, #eab308)" : "var(--amico-accent-2, #38bdf8)" } + }) + }) + return ( + + + + + {(p) => } + + ) +} + +export function RunInspector(props: Props) { + const runs = () => Array.from(props.bridge.runs().entries()) + const active = createMemo(() => { + const id = props.bridge.activeRunId() + if (id && props.bridge.runs().has(id)) return { id, state: props.bridge.runs().get(id)! } + const first = runs()[0] + return first ? { id: first[0], state: first[1] } : undefined + }) + const latestIter = createMemo(() => active()?.state.iterations.at(-1)) + const latestPulse = createMemo(() => active()?.state.pulses.at(-1)) + + return ( +
+ {/* Run selector — only shown when multiple runs exist */} + 1}> +
+ +
+
+ + + {/* Pulse metadata */} + + {(meta) => ( +
+ {meta().drives} control channel{meta().drives > 1 ? "s" : ""} · {meta().knots} timesteps + 0}> + · {meta().labels.join(", ")} + +
+ )} +
+ + {/* Pulse chart — always visible once a run exists */} + + + {/* Metrics table — horizontally scrollable at narrow widths */} +
+ + + + + + + + + + + + + + + + + +
IterObjectivePrimalDual
{latestIter()?.iter ?? "—"}{latestIter() ? latestIter()!.objective.toExponential(2) : "—"}{latestIter() ? latestIter()!.inf_pr.toExponential(1) : "—"}{latestIter() ? latestIter()!.inf_du.toExponential(1) : "—"}
+
+ + {/* Completion card */} + + {(c) => ( +
+
Result
+
F = {c().fidelity.toFixed(5)}
+
+ {c().status} · {c().iterations} iterations + · {active()!.state.timing!.toFixed(1)}s +
+
+ )} +
+ + {/* Running indicator */} + +
solving · iteration {latestIter()!.iter}
+
+
+ }> +
No solve yet — launch one from the chat.
+ + + ) +} diff --git a/packages/app-bundle/overlay/packages/app/src/app.tsx b/packages/app-bundle/overlay/packages/app/src/app.tsx new file mode 100644 index 00000000..fc84096a --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/app.tsx @@ -0,0 +1,772 @@ +import "@/index.css" +import * as Sentry from "@sentry/solid" +import { requestComputeConnect } from "@/components/amicode-defaults-capsule" +import { I18nProvider } from "@opencode-ai/ui/context" +import { DialogProvider } from "@opencode-ai/ui/context/dialog" +import { FileComponentProvider } from "@opencode-ai/ui/context/file" +import { MarkedProvider } from "@opencode-ai/ui/context/marked" +import { File } from "@opencode-ai/session-ui/file" +import { Font } from "@opencode-ai/ui/font" +import { Splash } from "@opencode-ai/ui/logo" +import { ThemeProvider, useTheme } from "@opencode-ai/ui/theme/context" +import { MetaProvider } from "@solidjs/meta" +import { + type BaseRouterProps, + Navigate, + Route, + Router, + useLocation, + useNavigate, + useParams, + useSearchParams, +} from "@solidjs/router" +import { QueryClient, QueryClientProvider } from "@tanstack/solid-query" +import { Effect } from "effect" +import { base64Encode } from "@opencode-ai/core/util/encode" +import { + type Component, + createEffect, + createMemo, + createRenderEffect, + createResource, + createSignal, + ErrorBoundary, + For, + type JSX, + lazy, + onCleanup, + type ParentProps, + Show, +} from "solid-js" +import { Dynamic } from "solid-js/web" +import { makeEventListener } from "@solid-primitives/event-listener" +import { CommandProvider, useCommand, type CommandOption } from "@/context/command" +import { CommentsProvider } from "@/context/comments" +import { FileProvider } from "@/context/file" +import { ServerSDKProvider } from "@/context/server-sdk" +import { ServerSyncProvider, useServerSync } from "@/context/server-sync" +import { GlobalProvider, useGlobal } from "@/context/global" +import { HighlightsProvider } from "@/context/highlights" +import { LanguageProvider, type Locale, useLanguage } from "@/context/language" +import { LayoutProvider } from "@/context/layout" +import { SplitProvider } from "@/context/split" +import { WorkbenchProvider } from "@/context/workbench" +import { ModelsProvider } from "@/context/models" +import { NotificationProvider } from "@/context/notification" +import { PermissionProvider } from "@/context/permission" +import { usePlatform } from "@/context/platform" +import { setPendingAutoSend } from "@/pages/new-session/new-session-draft-controller" +import { PromptProvider } from "@/context/prompt" +import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server" +import { SettingsProvider, useSettings } from "@/context/settings" +import { TabsProvider, useTabs, type DraftTab } from "@/context/tabs" +import { SDKProvider, useSDK } from "@/context/sdk" +import { WslServersProvider } from "@/wsl/context" +import DirectoryLayout, { DirectoryDataProvider } from "@/pages/directory-layout" +import LegacyLayout from "@/pages/layout" +import NewLayout from "@/pages/layout-new" +import { ErrorPage } from "./pages/error" +import { useCheckServerHealth } from "./utils/server-health" +import { AmicodeSplash } from "@opencode-ai/ui/amicode-splash" +import { legacySessionHref, legacySessionServer, requireServerKey, sessionHref } from "./utils/session-route" +import { createSessionLineage } from "@/pages/session/session-lineage" +import { bugDockController } from "@/pages/session/composer/bug-dock-controller" +import { postBugReportPoke } from "@/utils/amicode-bug-report" + +import { SessionPage, SessionRouteErrorBoundary, TargetSessionRouteContent } from "@/pages/session" +import { NewHome } from "@/pages/home" +import { LegacyHome } from "@/pages/home/legacy-home" +import { AmicodeFileRefBridge } from "@/components/amicode-file-ref-bridge" +import { DevToolsReopenBridge } from "@/components/settings-dialog" + +const NewSession = lazy(() => import("@/pages/new-session")) + +const SessionRoute = () => { + const settings = useSettings() + const params = useParams() + const [search] = useSearchParams<{ draftId?: string; prompt?: string }>() + const sdk = useSDK() + const server = useServer() + const tabs = useTabs() + + if (params.id && settings.general.newLayoutDesigns()) { + const sessionID = params.id + return ( + + {(_) => { + const persisted = tabs.store.filter((item) => item.type === "session") + return + }} + + ) + } + + // When the new layout is enabled, the legacy new-session route (/:dir/session with no id) + // is replaced by a draft at /new-session?draftId=… + createEffect(() => { + if (!settings.general.newLayoutDesigns()) return + if (params.id || search.draftId) return + if (!tabs.ready() || !sdk().directory) return + tabs.newDraft({ server: server.key, directory: sdk().directory }, search.prompt) + }) + + return ( + + + + ) +} + +function TargetServerRoute(props: ParentProps) { + const params = useParams<{ serverKey: string; id: string }>() + const global = useGlobal() + const conn = createMemo(() => { + const key = requireServerKey(params.serverKey) + return global.servers.list().find((item) => ServerConnection.key(item) === key) + }) + + return ( + // Owns the server-identity remount. Session changes must NOT remount this + // subtree (SessionRouteErrorBoundary resets and createSessionLineage + // re-resolves reactively instead); both rely on this key for server changes. + + + {props.children} + + + ) +} + +const TargetSessionRoute = () => ( + + + +) + +function LegacyTargetSessionRoute() { + const params = useParams<{ serverKey: string; id: string }>() + return ( + + + + + + ) +} + +function LegacyTargetSessionRedirect() { + const params = useParams<{ id: string }>() + const navigate = useNavigate() + const sync = useServerSync() + const current = createSessionLineage( + () => params.id, + () => sync().session.lineage, + ) + + createEffect(() => { + const directory = current()?.session.directory + if (!directory) return + navigate(legacySessionHref(directory, params.id), { replace: true }) + }) + + return null +} + +// Wraps the non-draft routes. They are gated on (and keyed to) the globally selected +// server via ServerKey, then provide the server-scoped shell for that server. +function SelectedServerProviders(props: ParentProps) { + return ( + + + + + {props.children} + + + + ) +} + +function LegacyServerLayout(props: ParentProps<{ serverScoped?: JSX.Element }>) { + return ( + + {props.children} + + ) +} + +function DraftRoute() { + const [search] = useSearchParams<{ draftId?: string }>() + const settings = useSettings() + const tabs = useTabs() + return ( + + tab.type === "draft" && tab.draftID === search.draftId)} + keyed + fallback={} + > + {(draft) => ( + } + > + + + )} + + + ) +} + +function ResolvedDraftRoute(props: { draft: DraftTab }) { + const global = useGlobal() + const conn = createMemo(() => global.servers.list().find((item) => ServerConnection.key(item) === props.draft.server)) + const directory = () => props.draft.directory + const serverKey = () => props.draft.server + + return ( + + + + + + + + + + + + + + + + ) +} + +function UiI18nBridge(props: ParentProps) { + const language = useLanguage() + return {props.children} +} + +function LayoutCompatibility(props: ParentProps) { + const global = useGlobal() + const navigate = useNavigate() + const server = useServer() + const settings = useSettings() + + createEffect(() => { + if (settings.general.newLayoutDesigns()) return + const current = server.current + if (!current) return + const protocol = global.ensureServerCtx(current).sdk.protocolKind() + if (protocol !== "v2") return + const next = global.servers.list().find((s) => { + if (ServerConnection.key(s) === ServerConnection.key(current)) return false + return global.ensureServerCtx(s).sdk.protocolKind() !== "v2" + }) + if (!next) return + navigate("/") + queueMicrotask(() => server.setActive(ServerConnection.key(next))) + }) + + return <>{props.children} +} + +declare global { + interface Window { + __OPENCODE__?: { + deepLinks?: string[] + } + api?: { + setTitlebar?: (theme: { mode: "light" | "dark"; scheme?: "system" | "light" | "dark" }) => Promise + exportDebugLogs?: () => Promise + } + } +} + +function QueryProvider(props: ParentProps) { + const client = new QueryClient({ + defaultOptions: { + queries: { + refetchOnReconnect: false, + refetchOnMount: false, + refetchOnWindowFocus: false, + }, + }, + }) + return {props.children} +} + +function BodyDesignClass() { + const settings = useSettings() + + createRenderEffect(() => { + if (typeof document === "undefined") return + + const enabled = settings.general.newLayoutDesigns() + document.body.toggleAttribute("data-new-layout", enabled) + document.body.classList.toggle("text-12-regular", !enabled) + document.body.classList.toggle("font-(family-name:--font-family-text)", enabled) + document.body.classList.toggle("text-[13px]", enabled) + document.body.classList.toggle("font-[440]", enabled) + }) + + return null +} + +// Server-agnostic providers shared across every route. These live in the shared +// shell (router root) so they stay mounted regardless of the active server/route. +function SharedProviders(props: ParentProps) { + return ( + <> + + + + + {/* amicode(split): the in-app pane state wraps the shell — the + titlebar (drag sources) and the layout (drop zones) both + consume it. amicode(workbench S2): the parent's tab mirror + sits above it — drops resolve against the mirror. */} + + {props.children} + + + + + ) +} + +function DesktopCommands() { + const command = useCommand() + const language = useLanguage() + const platform = usePlatform() + + command.register("desktop", () => { + const commands: CommandOption[] = [] + if (platform.platform === "desktop" && platform.exportDebugLogs) { + commands.push({ + id: "logs.export", + title: "Export logs", + category: language.t("command.category.settings"), + onSelect: () => { + void platform.exportDebugLogs?.() + }, + }) + } + return commands + }) + + return null +} + +// Server-scoped providers shared by the legacy shell and the top-level new shell. +type ServerScopedShellProps = ParentProps<{ + directory?: () => string | undefined + serverScoped?: JSX.Element +}> + +function ServerScopedProviders(props: ServerScopedShellProps) { + return ( + + {props.serverScoped} + {props.children} + + ) +} + +function LegacyServerScopedShell(props: ServerScopedShellProps) { + return ( + + {props.children} + + ) +} + +function NewAppLayout(props: ParentProps<{ serverScoped?: JSX.Element }>) { + return ( + + + {props.children} + + + ) +} + +// The draft page only renders the prompt composer, so it drops TerminalProvider. +// FileProvider and CommentsProvider stay because PromptInput uses file search and comment context. +function DraftProviders(props: ParentProps) { + return ( + + + {props.children} + + + ) +} + +/** amicode: live theme bridge for the VS Code webview host. The extension + * forwards editor theme changes as window messages (outer relay → iframe); + * route them through the existing setColorScheme so everything re-themes. */ +function AmicodeThemeBridge() { + const theme = useTheme() + const onMsg = (e: MessageEvent) => { + const d = e.data as { source?: string; kind?: string; colorScheme?: string } | undefined + if (d?.source !== "amicode") return + // amicode#200 AC6: the Connect Cloud palette command deep-links into the + // defaults capsule's compute-connect flow (consumed when home is showing). + if (d.kind === "open-compute-connect") { + requestComputeConnect() + return + } + // amicode/opencode#117: bug-report dock open/close down-messages. Handled + // at app level (not in the dock) so an open can't be missed between + // pages; the controller self-gates on the boot param + kind. + if (d.kind === "open-bug-report" || d.kind === "close-bug-report") { + bugDockController.handleBridgeMessage(d) + return + } if (d.kind !== "theme") return + if (d.colorScheme === "light" || d.colorScheme === "dark") theme.setColorScheme(d.colorScheme) + } + window.addEventListener("message", onMsg) + onCleanup(() => window.removeEventListener("message", onMsg)) + // ⌘⇧P / Ctrl+Shift+P: when embedded in the amicode webview (we have a + // parent), the EDITOR's Command Palette wins over the app's own palette — + // capture-phase so the in-app binding never sees it; forwarded over the + // existing allowlisted command bridge. + const onKey = (e: KeyboardEvent) => { + if (window.parent === window) return + if ((e.metaKey || e.ctrlKey) && e.shiftKey && (e.key === "p" || e.key === "P")) { + e.preventDefault() + e.stopPropagation() + window.parent.postMessage({ source: "amicode", kind: "command", command: "workbench.action.showCommands" }, "*") + } + } + window.addEventListener("keydown", onKey, { capture: true }) + onCleanup(() => window.removeEventListener("keydown", onKey, { capture: true })) + // The boot poke — the dock contract's pull half (QA follow-up, amicode#249 + // preview): posted once per app-frame boot when the flag is on; the + // extension re-posts open-bug-report if a bug session is live, so a lost + // one-shot open (cold-boot race, webview reload) self-heals. + postBugReportPoke() + return null +} + +/** amicode#363: bridge for the extension to open a new draft session with a + * pre-filled prompt. Must live inside TabsProvider + ServerProvider so it has + * access to useTabs().newDraft. */ +function AmicodeNavigateBridge() { + const tabs = useTabs() + const server = useServer() + let pending = false + + // Signal to the extension host that the app is mounted and ready to + // receive navigate messages. The extension uses this to dismiss the + // onboarding splash and post the greeting (instead of blind timeouts). + window.parent.postMessage({ source: "amicode", kind: "app-ready" }, "*") + + const onMsg = async (e: MessageEvent) => { + const d = e.data as { source?: string; kind?: string; path?: string } | undefined + if (d?.source !== "amicode" || d.kind !== "navigate" || !d.path) return + if (pending) return + pending = true + try { + const url = new URL(d.path, window.location.origin) + if (url.pathname === "/new-session") { + const prompt = url.searchParams.get("prompt") || undefined + const autoSend = url.searchParams.get("autoSend") === "1" + await tabs.newDraft({ server: server.key, directory: server.projects.list()[0]?.worktree ?? "" }, prompt) + if (autoSend) setPendingAutoSend(true) + } else { + // Navigate to an existing session by path (e.g. /session/:id) + const sessionMatch = url.pathname.match(/^\/session\/([^/?]+)/) + if (sessionMatch) { + const sessionId = sessionMatch[1] + tabs.openPath(`/${server.key}/session/${sessionId}`, { activate: true }) + } + } + } catch { /* malformed path — ignore */ } + finally { setTimeout(() => { pending = false }, 2000) } + } + window.addEventListener("message", onMsg) + onCleanup(() => window.removeEventListener("message", onMsg)) + return null +} + +export function AppBaseProviders(props: ParentProps<{ locale?: Locale }>) { + return ( + + + { + void window.api?.setTitlebar?.({ mode, scheme }) + }} + > + + + + { + Sentry.captureException(error) + return + }} + > + + + + + + {props.children} + + + + + + + + + + ) +} + +function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean; startup?: Promise }>) { + const server = useServer() + const checkServerHealth = useCheckServerHealth() + + const [checkMode, setCheckMode] = createSignal<"blocking" | "background">("blocking") + + // performs repeated health check with a grace period for + // non-http connections, otherwise fails instantly + const [startupHealthCheck, healthCheckActions] = createResource(() => + props.disableHealthCheck + ? true + : Effect.gen(function* () { + if (!server.current) return true + const { http, type } = server.current + + while (true) { + const res = yield* Effect.promise(() => checkServerHealth(http)) + if (res.healthy) return true + if (checkMode() === "background" || type === "http") return false + } + }).pipe( + Effect.timeoutOrElse({ duration: "10 seconds", orElse: () => Effect.succeed(false) }), + Effect.ensuring(Effect.sync(() => setCheckMode("background"))), + Effect.runPromise, + ), + ) + const checking = createMemo( + () => checkMode() === "blocking" && ["unresolved", "pending"].includes(startupHealthCheck.state), + ) + const [startup] = createResource(async () => { + if (!props.startup) return true + await props.startup.catch((error) => { + console.error("[startup] startup gate failed", error) + }) + return true + }) + const startupChecking = createMemo( + () => startupHealthCheck.latest === true && ["unresolved", "pending"].includes(startup.state), + ) + const loading = createMemo(() => checking() || startupChecking()) + + return ( + <> + + { + if (checkMode() === "background") void healthCheckActions.refetch() + }} + onServerSelected={(key) => { + setCheckMode("blocking") + server.setActive(key) + void healthCheckActions.refetch() + }} + /> + } + > + {props.children} + + + + {/* amicode: brand splash at the loading surface */} +
+ +
+
+ + ) +} + +function ConnectionError(props: { onRetry?: () => void; onServerSelected?: (key: ServerConnection.Key) => void }) { + const language = useLanguage() + const server = useServer() + const others = () => server.list.filter((s) => ServerConnection.key(s) !== server.key) + const name = createMemo(() => server.name || server.key) + const serverToken = "\u0000server\u0000" + const unreachable = createMemo(() => language.t("app.server.unreachable", { server: serverToken }).split(serverToken)) + + const timer = setInterval(() => props.onRetry?.(), 1000) + onCleanup(() => clearInterval(timer)) + + return ( +
+
+ +

+ {unreachable()[0]} + {name()} + {unreachable()[1]} +

+

{language.t("app.server.retrying")}

+
+ 0}> +
+ {language.t("app.server.otherServers")} +
+ + {(conn) => { + const key = ServerConnection.key(conn) + return ( + + ) + }} + +
+
+
+
+ ) +} + +function ServerKey(props: ParentProps) { + const server = useServer() + return ( + + {props.children} + + ) +} + +export function AppInterface(props: { + children?: JSX.Element + defaultServer: ServerConnection.Key + canonicalLocalServer?: ServerConnection.Key + servers?: Array + router?: Component + disableHealthCheck?: boolean + startup?: Promise + serverScoped?: JSX.Element +}) { + // The visual new layout lives in the router root so it remains mounted across + // route changes. Draft and session routes override only their server-bound data + // providers beneath it. + const ServerShell = (shellProps: ParentProps) => ( + + + {props.children} + {shellProps.children} + + + ) + + return ( + + + + + + ( + + + + + + + {routerProps.children} + + + + + + )} + > + + + + + + + + ) +} + +function Routes(props: { serverScoped?: JSX.Element }) { + const settings = useSettings() + + return ( + <> + ( + {routeProps.children} + )} + > + + { + <> + + + + } + + + } /> + + + + + + + + + + + ) +} + +function NewLayoutLegacySessionRedirect() { + const server = useServer() + const tabs = useTabs() + const params = useParams<{ id: string }>() + + return ( + + item.type === "session"), + params.id, + server.key, + ), + params.id, + )} + /> + + ) +} diff --git a/packages/app-bundle/overlay/packages/app/src/components/amicode-default-model-value.ts b/packages/app-bundle/overlay/packages/app/src/components/amicode-default-model-value.ts new file mode 100644 index 00000000..b660eceb --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/components/amicode-default-model-value.ts @@ -0,0 +1,17 @@ +// Pure "providerID/modelID" <-> ModelKey helpers for the dashboard default-model +// control. Split out of amicode-default-model.tsx so they're unit-testable +// without evaluating the SolidJS component (which touches client-only context). + +/** "providerID/modelID". modelID may itself contain "/", so callers split on the first. */ +export function formatModelValue(key: { providerID: string; modelID: string }): string { + return `${key.providerID}/${key.modelID}` +} + +/** Inverse of formatModelValue; null on a malformed/empty value. Splits on the + * FIRST slash so a model id containing slashes (e.g. openrouter/meta-llama/…) + * round-trips. */ +export function parseModelValue(value: string): { providerID: string; modelID: string } | null { + const slash = value.indexOf("/") + if (slash <= 0 || slash === value.length - 1) return null + return { providerID: value.slice(0, slash), modelID: value.slice(slash + 1) } +} diff --git a/packages/app-bundle/overlay/packages/app/src/components/amicode-default-model.test.ts b/packages/app-bundle/overlay/packages/app/src/components/amicode-default-model.test.ts new file mode 100644 index 00000000..81f1cc7a --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/components/amicode-default-model.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, test } from "bun:test" +import { formatModelValue, parseModelValue } from "./amicode-default-model-value" + +describe("amicode default-model value round-trip", () => { + test("format then parse is identity, incl. model ids with dots/dashes", () => { + const key = { providerID: "anthropic", modelID: "claude-sonnet-5" } + expect(formatModelValue(key)).toBe("anthropic/claude-sonnet-5") + expect(parseModelValue(formatModelValue(key))).toEqual(key) + expect(parseModelValue("opencode/deepseek-v4-flash-free")).toEqual({ + providerID: "opencode", + modelID: "deepseek-v4-flash-free", + }) + }) + + test("splits on the FIRST slash so a model id may itself contain slashes", () => { + expect(parseModelValue("openrouter/meta-llama/llama-3.3")).toEqual({ + providerID: "openrouter", + modelID: "meta-llama/llama-3.3", + }) + }) + + test("rejects malformed / empty values (→ null, no selection)", () => { + expect(parseModelValue("")).toBeNull() + expect(parseModelValue("noslash")).toBeNull() + expect(parseModelValue("/leading")).toBeNull() + expect(parseModelValue("trailing/")).toBeNull() + }) +}) diff --git a/packages/app-bundle/overlay/packages/app/src/components/amicode-default-model.tsx b/packages/app-bundle/overlay/packages/app/src/components/amicode-default-model.tsx new file mode 100644 index 00000000..2e7cd6c3 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/components/amicode-default-model.tsx @@ -0,0 +1,106 @@ +import { For, Show, createMemo } from "solid-js" +import { useModels } from "@/context/models" +import { formatModelValue, parseModelValue } from "./amicode-default-model-value" + +// Dashboard "Default model" control. Drives opencode's GLOBAL model store +// (useModels — mounted app-wide, persisted via Persist.global), which is what +// new chats inherit as their starting model (see local.tsx recentModel()), so +// a change takes effect immediately with no server restart. It ALSO mirrors the +// choice into the `amicode.defaultModel` VS Code setting over the iframe→parent +// bridge (chat_panel.ts), keeping the headless/first-turn config pin in sync. +// The in-chat picker still overrides per session. + +/** Post the chosen model to the extension host so it updates amicode.defaultModel. + * No-op when unframed (plain web/desktop): there's no parent shell to relay it. */ +function syncDefaultModelPin(model: string, win: Window = window): void { + if (win.parent === win) return + win.parent.postMessage({ source: "amicode", kind: "set-default-model", model }, "*") +} + +export function AmicodeDefaultModel() { + const models = useModels() + + const available = createMemo(() => models.list()) + // opencode's default visibility hides dated, non-"latest" models — which can + // hide EVERY model and leave the control empty. Prefer the visible set, but + // fall back to all available models so the picker is never blank when models + // exist. (Guarded below on available(), so it only hides with zero models.) + const selectable = createMemo(() => { + const vis = available().filter((m) => models.visible({ providerID: m.provider.id, modelID: m.id })) + return vis.length > 0 ? vis : available() + }) + + const groups = createMemo(() => { + const byProvider = new Map() + for (const m of selectable()) { + const group = byProvider.get(m.provider.id) ?? { provider: m.provider.name, items: [] } + group.items.push({ value: formatModelValue({ providerID: m.provider.id, modelID: m.id }), name: m.name }) + byProvider.set(m.provider.id, group) + } + return [...byProvider.values()] + }) + + // Bound to the DURABLE pin (models.pin), not the recents head — chats using + // other models must not move the dashboard's declared default. Empty = Auto + // (no pin; new chats fall back to recents/provider default). + const current = createMemo(() => { + const pin = models.pin.get() + return pin ? formatModelValue(pin) : "" + }) + + const choose = (value: string) => { + if (value === "") { + models.pin.set(undefined) // Auto: clear the pin, mirror the cleared setting + syncDefaultModelPin("") + return + } + const key = parseModelValue(value) + if (!key) return + models.pin.set(key) + syncDefaultModelPin(value) + } + + return ( + 0}> + + + ) +} diff --git a/packages/app-bundle/overlay/packages/app/src/components/amicode-defaults-capsule.tsx b/packages/app-bundle/overlay/packages/app/src/components/amicode-defaults-capsule.tsx new file mode 100644 index 00000000..89414530 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/components/amicode-defaults-capsule.tsx @@ -0,0 +1,475 @@ +import { Show, batch, createEffect, createMemo, createSignal, onCleanup, type Accessor } from "solid-js" +import { useModels } from "@/context/models" +import { AmicodeDefaultModel } from "./amicode-default-model" +import { announceChromeDropdown, chromeDropdownOpenId, clearChromeDropdown } from "@/utils/chrome-dropdown" +import { + hpAfterConnect, + hpClickAction, + loadSolverMode, + modeAfterDisconnect, + saveSolverMode, + solverConnectionDot, + type SolverMode, +} from "@opencode-ai/ui/amicode-solver-toggle" +import { + ConnectionCard, + type ConnectionActionView, + type ConnectionsTabLabels, + type ConnectionView, + type CredentialSubmitPayload, +} from "@opencode-ai/ui/amicode-connections-tab" + +// AMICODE: the chrome strip's DEFAULTS CAPSULE (nav redesign, Kate 2026-07-15). +// One compact control carries both session defaults — model and solver — so the +// bar holds one row at every panel width. The face reads the current pair +// ("Auto · Piccolo"); the popover holds the full model select (the existing +// AmicodeDefaultModel, unchanged: global pin + VS Code-setting mirror) and a +// solver radio pair sharing solver-toggle.tsx's persistence. When the +// high-performance solver is on, the face wears the accent border — the PRO +// funnel stays visible at every width instead of being the first casualty +// of a narrow column. +// +// amicode#200: the solver toggle owns the Company Compute connection. The HP +// radio wears a status dot (green connected / amber attention / gray no key), +// and clicking it while unconnected expands the SAME connection card the +// status popover used to host — connect here, HP flips on when the credential +// lands connected. Connected + already-HP clicks open the card in its +// management state (identity, validated-at, revalidate, disconnect). +// Disconnect always drops the mode back to Piccolo: HP is cloud-only. + +/** The Company Compute slice of createAmicodeConnectionsState, passed down by + * the host chrome. Optional so bare mounts (storybook) keep legacy behavior. */ +export type AmicodeComputeControl = { + view: Accessor + labels: Accessor + actionError: Accessor + onSubmit: (payload: CredentialSubmitPayload) => Promise + onDisconnect: (id: string) => void + onRevalidate: (id: string) => void + refetch: () => void +} + +// The status dot lives on the connection card's STATUS line (Kate: the dot +// accompanies "not connected…", never the solver name — same idiom as the +// Connections tab, where the dot leads the card's first line). The header +// row signals state non-visually via aria-label/title and data-dot. + +// amicode#200 AC6: the Connect Cloud palette command deep-links here. The +// extension posts an "open-compute-connect" envelope through the webview +// bridge; the app-shell listener calls requestComputeConnect() and the next +// mounted capsule opens straight into the connect flow. Timestamped so a +// request fired while another page was showing can't pop the popover minutes +// later (15s freshness window). +const [connectRequestAt, setConnectRequestAt] = createSignal(0) +export function requestComputeConnect() { + setConnectRequestAt(Date.now()) +} +const CONNECT_REQUEST_TTL_MS = 15_000 + +export function AmicodeDefaultsCapsule(props: { compute?: AmicodeComputeControl }) { + const models = useModels() + const [open, setOpen] = createSignal(false) + const [mode, setMode] = createSignal(loadSolverMode()) + const [computeOpen, setComputeOpen] = createSignal(false) + const pick = (m: SolverMode) => { + setMode(m) + saveSolverMode(m) + } + + const dot = createMemo(() => solverConnectionDot(props.compute?.view())) + const onHpClick = () => { + if (!props.compute) { + pick("hp") // legacy seam: unwired hosts keep the old behavior + return + } + if (hpClickAction(dot()) === "activate") { + pick("hp") + return + } + // not connected — reveal the connect card inline; freshen the view + setComputeOpen(true) + props.compute.refetch() + } + // details (gear): the persistent way back to the connection card once a + // credential exists — connect success must never strand the user with no + // visible route to identity / revalidate / disconnect (Kate, 2026-07-22) + const onDetailsClick = () => { + if (computeOpen()) { + setComputeOpen(false) + return + } + setComputeOpen(true) + props.compute?.refetch() + } + const submitCredential = async (payload: CredentialSubmitPayload) => { + const result = await props.compute!.onSubmit(payload) + if (hpAfterConnect(result)) pick("hp") + return result + } + const disconnectCompute = (id: string) => { + props.compute!.onDisconnect(id) + pick(modeAfterDisconnect()) + } + + const modelName = createMemo(() => { + const pin = models.pin.get() + if (!pin) return "Auto" + const m = models.list().find((x) => x.provider.id === pin.providerID && x.id === pin.modelID) + return m?.name ?? "Auto" + }) + const hp = () => mode() === "hp" + + let root: HTMLDivElement | undefined + let hpRow: HTMLDivElement | undefined + const close = () => { + setOpen(false) + setComputeOpen(false) + clearChromeDropdown("defaults") + } + // amicode#203: one chrome dropdown at a time (see chrome-dropdown.ts). + createEffect(() => { + if (chromeDropdownOpenId() !== "defaults" && open()) { + setOpen(false) + setComputeOpen(false) + } + }) + const onDocPointer = (e: PointerEvent) => { + if (root && !root.contains(e.target as Node)) { + close() + return + } + // inside the popover but outside the HP accordion → collapse the card + // (Kate: the disclosure dismisses like any other; the kebab reopens it) + if (computeOpen() && hpRow && !hpRow.contains(e.target as Node)) setComputeOpen(false) + } + const onDocKey = (e: KeyboardEvent) => { + if (e.key === "Escape") close() + } + const openPopover = () => { + if (open()) return + // batch: announce + open flag must land atomically (press-twice bug). + batch(() => { + announceChromeDropdown("defaults") + setOpen(true) + }) + document.addEventListener("pointerdown", onDocPointer, true) + document.addEventListener("keydown", onDocKey, true) + } + const toggle = () => { + if (open()) { + close() + teardown() + } else { + openPopover() + } + } + + // amicode#200 AC6: consume a pending Connect Cloud deep-link — open the + // popover directly into the compute connect flow. + createEffect(() => { + const at = connectRequestAt() + if (!at || !props.compute) return + setConnectRequestAt(0) + if (Date.now() - at > CONNECT_REQUEST_TTL_MS) return + openPopover() + setComputeOpen(true) + props.compute.refetch() + }) + const teardown = () => { + document.removeEventListener("pointerdown", onDocPointer, true) + document.removeEventListener("keydown", onDocKey, true) + } + onCleanup(teardown) + + const radio = (active: boolean): Record => ({ + display: "flex", + "align-items": "center", + gap: "8px", + width: "100%", + padding: "6px 10px", + "font-size": "12px", + "font-weight": active ? "650" : "400", + "text-align": "left", + border: active ? "1px solid var(--accent-edge)" : "1px solid var(--v2-border-border-base)", + "border-radius": "var(--radius-md)", + cursor: "pointer", + background: active ? "var(--accent-fill-soft)" : "transparent", + color: active ? "var(--v2-text-text-base)" : "var(--v2-text-text-muted)", + }) + + return ( +
+ + +
+ +
+
+ + Solver + + + {/* One bordered control (Kate, 2026-07-22): a single accordion — + header row (select + kebab click zones) and the API-key card + expand/collapse INSIDE the same border, split by a hairline. */} +
+
+ + + + +
+ + {/* aria-live: the card's state copy (validating → connected / + error) announces to screen readers; focus lands here on open + so keyboard users reach the form without tabbing blind. + The dot on the radio is color-only — its aria-label carries + the state in text (WCAG color-not-only). */} +
setTimeout(() => el.focus(), 0)} + style={{ + padding: "0 2px 4px", + outline: "none", + }} + > + } + > + {(conn) => ( + + )} + +
+
+
+
+
+ +
+ ) +} diff --git a/packages/app-bundle/overlay/packages/app/src/components/amicode-file-ref-bridge.tsx b/packages/app-bundle/overlay/packages/app/src/components/amicode-file-ref-bridge.tsx new file mode 100644 index 00000000..eadf0cbb --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/components/amicode-file-ref-bridge.tsx @@ -0,0 +1,33 @@ +// AMICODE: registers the chat file-reference resolver with session-ui's +// markdown renderer. Rides the same per-call server pick as the vault browser +// (focused server, else first healthy, else first), so a server switch is +// picked up by the very next resolution; resolutions never block render. +// Mounted under SelectedServerProviders (needs the server + global contexts); +// with the bridge unmounted no resolver is registered and pills stay pills. +import { onCleanup, onMount } from "solid-js" +import { registerFileRefResolver } from "@opencode-ai/session-ui/markdown-file-refs" +import { ServerConnection, useServer } from "@/context/server" +import { useGlobal } from "@/context/global" +import { amicodeGet } from "@/utils/amicode-fetch" +import { pickVaultServer } from "@/components/vault-browser-model" + +export function AmicodeFileRefBridge() { + const server = useServer() + const global = useGlobal() + onMount(() => { + registerFileRefResolver(async (text) => { + const conn = pickVaultServer({ + current: server.current, + list: server.list, + healthy: (c) => global.servers.health[ServerConnection.key(c)]?.healthy === true, + }) + if (!conn) return null + const raw = (await amicodeGet(conn, `/amicode/resolve-file?path=${encodeURIComponent(text)}`)) as + | { ok?: boolean; found?: boolean; path?: unknown } + | undefined + return raw?.ok === true && raw.found === true && typeof raw.path === "string" ? raw.path : null + }) + }) + onCleanup(() => registerFileRefResolver(undefined)) + return null +} diff --git a/packages/app-bundle/overlay/packages/app/src/components/connection-banner.tsx b/packages/app-bundle/overlay/packages/app/src/components/connection-banner.tsx new file mode 100644 index 00000000..dcb7bba1 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/components/connection-banner.tsx @@ -0,0 +1,77 @@ +import { Show, createEffect, createSignal, onCleanup } from "solid-js" +import { useLanguage } from "@/context/language" +import { useServerSDK } from "@/context/server-sdk" + +// Amicode webview: a dead server (e.g. an opencode:build restart) used to read +// as an endless "thinking" wave — the event loop retries silently every 250ms. +// Surface the drop and the recovery. The fresh stream's server.connected makes +// server-sync refetch everything, so recovery needs no reload in most cases. +export function ConnectionBanner() { + const sdk = useServerSDK() + const language = useLanguage() + const [connectedOnce, setConnectedOnce] = createSignal(false) + const [wasDown, setWasDown] = createSignal(false) + const [flash, setFlash] = createSignal(false) + let flashTimer: ReturnType | undefined + onCleanup(() => { + if (flashTimer !== undefined) clearTimeout(flashTimer) + }) + + createEffect(() => { + const status = sdk().event.status() + // Always clear wasDown when we're connected, even on first connect + if (status === "connected") { + if (wasDown()) { + setWasDown(false) + setFlash(true) + if (flashTimer !== undefined) clearTimeout(flashTimer) + flashTimer = setTimeout(() => { + setFlash(false) + flashTimer = undefined + }, 3000) + } + if (!connectedOnce()) { + setConnectedOnce(true) + } + return + } + if (status === "disconnected") { + // Only report drops after a first successful connect — boot renders + // "disconnected" transiently while the stream is still being set up. + if (connectedOnce()) setWasDown(true) + if (flashTimer !== undefined) { + clearTimeout(flashTimer) + flashTimer = undefined + } + setFlash(false) + } + }) + + const down = () => wasDown() && sdk().event.status() === "disconnected" + + return ( + +
+ {down() ? language.t("app.server.connectionLost") : language.t("app.server.reconnected")} +
+
+ ) +} diff --git a/packages/app-bundle/overlay/packages/app/src/components/dialog-new-project.tsx b/packages/app-bundle/overlay/packages/app/src/components/dialog-new-project.tsx new file mode 100644 index 00000000..e75364a8 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/components/dialog-new-project.tsx @@ -0,0 +1,102 @@ +// amicode#203: New-project creation dialog. Name → slug preview; the server +// owns the location default (~/AmicodeProjects) and the mkdir + best-effort +// git init (POST /amicode/project). Inline, recoverable errors for collision / +// unwritable; a non-blocking git notice is surfaced by the caller (onCreated). +import { Button } from "@opencode-ai/ui/button" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { Dialog } from "@opencode-ai/ui/dialog" +import { TextField } from "@opencode-ai/ui/text-field" +import { useMutation } from "@tanstack/solid-query" +import { createMemo, createSignal, Show } from "solid-js" +import { ServerConnection } from "@/context/server" +import { amicodePost } from "@/utils/amicode-fetch" +import { projectNameToSlug } from "@/pages/home-projects" + +type CreateResult = + | { ok: true; path: string; slug: string; gitInitialized: boolean } + | { ok: false; error: string; message: string } + +export function DialogNewProject(props: { + server: ServerConnection.Any + onCreated: (result: { path: string; gitInitialized: boolean }) => void + onOpenExisting: () => void +}) { + const dialog = useDialog() + const [name, setName] = createSignal("") + const [error, setError] = createSignal() + const slug = createMemo(() => projectNameToSlug(name())) + + const createMutation = useMutation(() => ({ + mutationFn: async (): Promise => { + setError(undefined) + const result = (await amicodePost(props.server, "/amicode/project", { name: name() }).catch( + () => undefined, + )) as CreateResult | undefined + if (!result) { + setError("Couldn't reach the server — try again.") + return + } + if (!result.ok) { + setError(result.message) + return + } + dialog.close() + props.onCreated({ path: result.path, gitInitialized: result.gitInitialized }) + }, + })) + + function submit(e: SubmitEvent) { + e.preventDefault() + if (createMutation.isPending || !slug()) return + createMutation.mutate() + } + + return ( + +
+
+ + Creates a new folder in ~/AmicodeProjects.} + > + + Creates {slug()} in ~/AmicodeProjects, with git for change + tracking. + + +
+ + +
+ ) +} diff --git a/packages/app-bundle/overlay/packages/app/src/components/pane-bridge.tsx b/packages/app-bundle/overlay/packages/app/src/components/pane-bridge.tsx new file mode 100644 index 00000000..7a798345 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/components/pane-bridge.tsx @@ -0,0 +1,63 @@ +import { createEffect, onCleanup } from "solid-js" +import { useLocation } from "@solidjs/router" +import { useTabs, tabHref, draftHref, type Tab } from "@/context/tabs" +import { useWorkbench, MAIN_PANE_ID } from "@/context/workbench" +import { AMICODE_PANE_ID, IS_AMICODE_PANE } from "@/utils/amicode-pane" +import { listenWorkbench, postWorkbench, type WorkbenchMessage } from "@/utils/pane-bridge" + +// amicode(workbench S2): every app instance mounts this. Two jobs: +// 1. REPORT — the instance's tab list (as route paths) flows to the parent's +// mirror whenever it changes: panes post over the bridge; the top document +// writes the workbench store directly (it IS the parent). +// 2. OBEY — panes execute the parent's open/close/focus commands via their +// own tabs context (the top document's commands are issued directly, no +// messages needed). +// Renders nothing. + +const tabsReport = (tabs: Tab[], activePath?: string) => ({ + tabs: tabs.map((t) => (t.type === "draft" ? draftHref(t.draftID) : tabHref(t))), + active: activePath, +}) + +export function WorkbenchBridge() { + const tabs = useTabs() + const location = useLocation() + const workbench = useWorkbench() + + const activePath = () => { + const path = `${location.pathname}${location.search}` + const list = tabs.store + return list.some((t) => (t.type === "draft" ? draftHref(t.draftID) : tabHref(t)) === path) ? path : undefined + } + + // REPORT + createEffect(() => { + const report = tabsReport(tabs.store, activePath()) + if (IS_AMICODE_PANE) { + postWorkbench(window.parent, { kind: "tabs-changed", paneId: AMICODE_PANE_ID ?? "pane", report }, "*") + } else { + workbench.report(MAIN_PANE_ID, report) + } + }) + + // OBEY (panes only — the parent commands the top document directly) + if (IS_AMICODE_PANE) { + const unlisten = listenWorkbench((msg: WorkbenchMessage) => { + if (msg.kind === "open-tab") tabs.openPath(msg.path, { activate: msg.activate }) + if (msg.kind === "close-tab") tabs.closePath(msg.path) + if (msg.kind === "focus-tab") tabs.openPath(msg.path, { activate: true }) + }) + onCleanup(unlisten) + } + + return null +} + +/** amicode(workbench): pane strips report drags to the parent (the parent owns + * drop resolution across frames). The top document uses the split context + * instead — this helper is pane-mode only. */ +export function reportTabDrag(path: string | undefined) { + if (!IS_AMICODE_PANE) return + if (path === undefined) postWorkbench(window.parent, { kind: "drag-tab-end" }, "*") + else postWorkbench(window.parent, { kind: "drag-tab-start", path }, "*") +} diff --git a/packages/app-bundle/overlay/packages/app/src/components/prompt-input-clipboard-structure.test.ts b/packages/app-bundle/overlay/packages/app/src/components/prompt-input-clipboard-structure.test.ts new file mode 100644 index 00000000..399da81a --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/components/prompt-input-clipboard-structure.test.ts @@ -0,0 +1,75 @@ +import { describe, expect, test } from "bun:test" +import { readFileSync } from "node:fs" +import { join } from "node:path" + +// harmoniqs/amicode#261 — paste inserted the same text twice in the webview. +// +// Two mod+V handlers used to be live inside the framed app: +// 1. utils/global-clipboard.ts — window-level, CAPTURE phase. +// 2. The composer's own keydown handler (v1 handleKeyDown / v2 onKeyDown), +// which preventDefault'd but never stopPropagation'd, so BOTH bridge- +// inserted the same text. An opt-out marker (data-amc-clipboard="self") +// on the composer editors was tried, but the amicode fork hard-locks the +// v2 layout and the marker contract proved too fragile to maintain across +// the two composers. +// +// The current design removes the composers' keydown interception entirely: +// the window-level fallback is the SOLE ⌘V path in the webview (single +// insert), and the native paste event path (composer onPaste → handlePaste) +// remains for chords the fallback doesn't own (⌘⇧V). Image/screenshot paste +// over ⌘V is knowingly sacrificed — the fallback reads text only. +// +// This is a SOURCE assertion rather than a rendered-DOM one because the app has +// no component-render harness (no @solidjs/testing-library) — the same reason +// global-clipboard.test.ts exercises the fallback against a synthetic element +// it builds itself, which is exactly the gap that let #261 ship. Replace this +// with a render assertion the day a harness lands. +const source = readFileSync(join(import.meta.dir, "prompt-input.tsx"), "utf8") +const v2Interaction = readFileSync( + join(import.meta.dir, "../../../session-ui/src/v2/components/prompt-input/interaction.ts"), + "utf8", +) +const v2Attachments = readFileSync( + join(import.meta.dir, "../../../session-ui/src/v2/components/prompt-input/attachments.ts"), + "utf8", +) +const v2Editor = readFileSync( + join(import.meta.dir, "../../../session-ui/src/v2/components/prompt-input/index.tsx"), + "utf8", +) +const globalClipboard = readFileSync(join(import.meta.dir, "../utils/global-clipboard.ts"), "utf8") + +describe("single ⌘V path in the webview (amicode#261)", () => { + test("the v1 composer no longer intercepts ⌘V at keydown", () => { + expect(source).not.toContain("inAmicode") + expect(source).not.toContain('event.key.toLowerCase() === "v"') + }) + + test("the v2 composer no longer intercepts ⌘V at keydown", () => { + expect(v2Interaction).not.toContain('event.key.toLowerCase() === "v"') + expect(v2Interaction).not.toContain("handleFramedPaste") + expect(v2Attachments).not.toContain("handleFramedPaste") + }) + + test("neither composer editor carries the opt-out marker", () => { + // The marker contract is gone: the fallback owns ⌘V everywhere. A marker + // here would silently re-orphan the composer (nothing would paste on ⌘V). + expect(source).not.toContain('data-amc-clipboard="self"') + expect(v2Editor).not.toContain('data-amc-clipboard="self"') + }) + + test("the window-level fallback still owns the ⌘V branch for editables", () => { + expect(globalClipboard).toContain('key !== "v"') + expect(globalClipboard).toContain("installGlobalClipboardFallback") + expect(globalClipboard).toContain("readClipboardViaBridge") + }) + + test("the composers still wire the native paste path with bridge fallbacks", () => { + // ⌘⇧V (and any non-prevented paste event) flows: onPaste → handlePaste, + // which falls back to the host-clipboard bridges when the event carries + // nothing readable. + expect(source).toContain("onPaste={handlePaste}") + expect(v2Interaction).toContain("handlePaste") + expect(source).toContain("readClipboardViaBridge") + }) +}) diff --git a/packages/app-bundle/overlay/packages/app/src/components/prompt-input-v2.tsx b/packages/app-bundle/overlay/packages/app/src/components/prompt-input-v2.tsx new file mode 100644 index 00000000..a66d1433 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/components/prompt-input-v2.tsx @@ -0,0 +1,606 @@ +import { ImagePreview } from "@opencode-ai/ui/image-preview" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { ProviderIcon } from "@opencode-ai/ui/provider-icon" +import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" +import { Icon } from "@opencode-ai/ui/v2/icon" +import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2" +import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" +import type { ReferenceInfo } from "@opencode-ai/sdk/v2/client" +import { createEffect, createMemo, on, onCleanup, Show } from "solid-js" +import { ModelSelectorPopoverV2 } from "@/components/dialog-select-model" +import { DialogSelectModelUnpaidV2 } from "@/components/dialog-select-model-unpaid-v2" +import { ReportBugButton } from "@/components/report-bug-button" +import type { PromptInputProps } from "@/components/prompt-input/contracts" +import { normalizePromptHistoryEntry, promptLength, type PromptHistoryComment } from "@/components/prompt-input/history" +import { createPersistedPromptInputHistory } from "@/components/prompt-input/history-store" +import { promptDesignPlaceholder, promptPlaceholder } from "@/components/prompt-input/placeholder" +import { readClipboardImageViaBridge, readClipboardViaBridge } from "@/components/prompt-input/clipboard-bridge" +import { createPromptSubmit } from "@/components/prompt-input/submit" +import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file" +import { useComments } from "@/context/comments" +import { useCommand } from "@/context/command" +import { useLanguage } from "@/context/language" +import { useLayout } from "@/context/layout" +import { usePermission } from "@/context/permission" +import { type ImageAttachmentPart, usePrompt } from "@/context/prompt" +import { usePlatform } from "@/context/platform" +import { useSDK } from "@/context/sdk" +import { useSync } from "@/context/sync" +import { createSessionTabs } from "@/pages/session/helpers" +import { showToast } from "@/utils/toast" +import { bugReportEnabled } from "@/utils/amicode-bug-report" +import { setClipboardImageHandler } from "@/utils/global-clipboard" +import { PromptInputV2, type PromptInputV2Suggestion } from "@opencode-ai/session-ui/v2/prompt-input" +import { + createPromptInputV2Controller, + createPromptInputV2State, + type PromptInputV2Interaction, +} from "@opencode-ai/session-ui/v2/prompt-input/interaction" + +export type PromptInputV2ComposerProps = { + class?: string + controller: PromptInputV2ComposerController + borderUnderlay?: boolean +} + +export type PromptInputV2ControllerProps = Omit +export type PromptInputV2ComposerController = PromptInputV2Interaction & { + readonly model: PromptInputProps["controls"]["model"] +} + +export function PromptInputV2Composer(props: PromptInputV2ComposerProps) { + const dialog = useDialog() + const command = useCommand() + const language = useLanguage() + + return ( +
+ : undefined} + modelControl={ + + dialog.show(() => ) + } + /> + } + /> +
+ ) +} + +export function usePromptInputV2Controller(props: PromptInputV2ControllerProps): PromptInputV2ComposerController { + const sdk = useSDK() + const sync = useSync() + const files = useFile() + const layout = useLayout() + const comments = useComments() + const dialog = useDialog() + const command = useCommand() + const permission = usePermission() + const language = useLanguage() + const platform = usePlatform() + const prompt = props.state ?? usePrompt() + let editor: HTMLDivElement | undefined + + const interaction = createPromptInputV2State() + const mode = () => interaction[0].mode + const history = props.history ?? createPersistedPromptInputHistory() + const tabs = () => props.controls.session.tabs + const activeFileTab = createSessionTabs({ + tabs, + pathFromTab: files.pathFromTab, + normalizeTab: (tab) => (tab.startsWith("file://") ? files.tab(tab) : tab), + }).activeFileTab + const recent = createMemo(() => { + const all = tabs().all() + const active = activeFileTab() + const order = active ? [active, ...all.filter((tab) => tab !== active)] : all + return order.reduce((result, tab) => { + const path = files.pathFromTab(tab) + if (!path || result.includes(path)) return result + return [...result, path] + }, []) + }) + const info = createMemo(() => (props.controls.session.id ? sync().session.get(props.controls.session.id) : undefined)) + const working = createMemo(() => sync().data.session_working(props.controls.session.id ?? "")) + const attachments = createMemo(() => + prompt.current().filter((part): part is ImageAttachmentPart => part.type === "image"), + ) + const commentCount = createMemo(() => { + if (mode() === "shell") return 0 + return prompt.context.items().filter((item) => !!item.comment?.trim()).length + }) + const blank = createMemo(() => { + const text = prompt + .current() + .map((part) => ("content" in part ? part.content : "")) + .join("") + return text.trim().length === 0 && attachments().length === 0 && commentCount() === 0 + }) + const stopping = createMemo(() => working() && blank()) + const placeholder = createMemo(() => + promptPlaceholder({ + mode: mode(), + commentCount: commentCount(), + example: mode() === "shell" ? "git status" : "", + suggest: false, + t: (key, params) => language.t(key as Parameters[0], params as never), + }), + ) + const designPlaceholder = () => promptDesignPlaceholder(mode(), placeholder()) + + const historyComments = () => { + const byID = new Map(comments.all().map((item) => [`${item.file}\n${item.id}`, item] as const)) + return prompt.context.items().flatMap((item) => { + const comment = item.comment?.trim() + if (!comment) return [] + const selection = item.commentID ? byID.get(`${item.path}\n${item.commentID}`)?.selection : undefined + const nextSelection = + selection ?? + (item.selection + ? ({ start: item.selection.startLine, end: item.selection.endLine } satisfies SelectedLineRange) + : undefined) + if (!nextSelection) return [] + return [ + { + id: item.commentID ?? item.key, + path: item.path, + selection: { ...nextSelection }, + comment, + time: item.commentID ? (byID.get(`${item.path}\n${item.commentID}`)?.time ?? Date.now()) : Date.now(), + origin: item.commentOrigin, + preview: item.preview, + } satisfies PromptHistoryComment, + ] + }) + } + const restoreHistoryComments = (items: PromptHistoryComment[]) => { + comments.replace( + items.map((item) => ({ + id: item.id, + file: item.path, + selection: { ...item.selection }, + comment: item.comment, + time: item.time, + })), + ) + prompt.context.replaceComments( + items.map((item) => ({ + type: "file", + path: item.path, + selection: selectionFromLines(item.selection), + comment: item.comment, + commentID: item.id, + commentOrigin: item.origin, + preview: item.preview, + })), + ) + } + + const accepting = createMemo(() => { + const id = props.controls.session.id + if (!id) return permission.isAutoAcceptingDirectory(sdk().directory) + return permission.isAutoAccepting(id, sdk().directory) + }) + const submission = createPromptSubmit({ + prompt, + info, + imageAttachments: attachments, + commentCount, + autoAccept: accepting, + mode, + working, + editor: () => editor, + queueScroll: () => requestAnimationFrame(() => editor?.scrollIntoView({ block: "nearest" })), + promptLength, + addToHistory: (value, mode) => controller.addHistory(value, mode), + resetHistoryNavigation: () => controller.resetHistory(), + setMode: (next) => controller.dispatch({ type: next === "shell" ? "mode.shell" : "mode.normal" }), + setPopover: (popover) => { + if (!popover) controller.dispatch({ type: "popover.close" }) + }, + newSessionWorktree: () => props.newSessionWorktree, + onNewSessionWorktreeReset: props.onNewSessionWorktreeReset, + shouldQueue: props.shouldQueue, + onQueue: props.onQueue, + onAbort: props.onAbort, + onSubmit: props.onSubmit, + model: props.controls.model.selection, + }) + + const referenceDescription = (reference: ReferenceInfo) => + reference.source.type === "git" ? reference.source.repository : reference.source.path + const references = createMemo(() => + sync() + .data.reference.filter((reference) => !reference.hidden) + .map((reference) => ({ + id: `reference:${reference.name}`, + kind: "reference" as const, + label: `@${reference.name}`, + path: reference.path, + description: reference.description ?? referenceDescription(reference), + mention: { + type: "file" as const, + path: reference.path, + content: `@${reference.name}`, + start: 0, + end: 0, + mime: "application/x-directory", + filename: reference.name, + }, + })), + ) + const resources = createMemo(() => + Object.values(sync().data.mcp_resource).map((resource) => ({ + id: `resource:${resource.server}:${resource.uri}`, + kind: "resource" as const, + label: `@${resource.name}`, + path: resource.uri, + description: resource.description, + mention: { + type: "file" as const, + path: resource.uri, + content: `@${resource.name}`, + start: 0, + end: 0, + mime: resource.mimeType ?? "text/plain", + filename: resource.name, + url: resource.uri, + source: { + type: "resource" as const, + text: { value: `@${resource.name}`, start: 0, end: resource.name.length + 1 }, + clientName: resource.server, + uri: resource.uri, + }, + }, + resource, + })), + ) + const context = createMemo(() => [ + ...references(), + ...props.controls.agents.available + .filter((agent) => !agent.hidden && agent.mode !== "primary") + .map((agent) => ({ + id: `agent:${agent.name}`, + kind: "agent" as const, + label: `@${agent.name}`, + mention: { type: "agent" as const, name: agent.name, content: `@${agent.name}`, start: 0, end: 0 }, + })), + ...resources(), + ...recent().map((path) => ({ + id: `file:${path}`, + kind: "file" as const, + label: path, + path, + recent: true, + mention: { type: "file" as const, path, content: `@${path}`, start: 0, end: 0 }, + })), + ]) + const slashCommands = createMemo(() => [ + ...sync().data.command.map((item) => ({ + id: `custom.${item.name}`, + trigger: item.name, + title: item.name, + description: item.description, + type: "custom" as const, + })), + ...command.options + .filter((item) => !item.disabled && !item.id.startsWith("suggested.") && item.slash) + .map((item) => ({ + id: item.id, + trigger: item.slash!, + title: item.title, + description: item.description, + type: "builtin" as const, + })), + ]) + const commands = createMemo(() => + slashCommands().map((item) => ({ + id: item.id, + kind: "command", + label: `/${item.trigger}`, + trigger: item.trigger, + title: item.title, + description: item.description, + keybind: command.keybindParts(item.id), + })), + ) + const variants = createMemo(() => ["default", ...props.controls.model.selection.variant.list()]) + const controller = createPromptInputV2Controller({ + store: () => prompt.capture().store, + state: interaction, + identity: () => prompt.capture(), + history: { + entries: (mode) => + history.entries(mode).map((value) => { + const entry = normalizePromptHistoryEntry(value) + return { prompt: entry.prompt, metadata: entry.comments } + }), + add: (value, mode) => history.add(value, mode, mode === "shell" ? [] : historyComments()), + capture: historyComments, + restore: (metadata) => restoreHistoryComments(metadata as PromptHistoryComment[]), + }, + commands, + context, + searchContextFiles: async (query) => + (await files.searchFilesAndDirectories(query)).map((path) => ({ + id: `file:${path}`, + kind: "file", + label: path, + path, + mention: { type: "file", path, content: `@${path}`, start: 0, end: 0 }, + })), + onContextRemove(item) { + if (item?.commentID) comments.remove(item.path, item.commentID) + }, + openAttachment: (attachment) => + dialog.show(() => ), + openContext(key) { + const item = controller.contextItem(key) + if (item) openComment(item, props, sync, layout, files, comments) + }, + onEditor(element) { + editor = element as HTMLDivElement + props.ref?.(editor) + }, + onSuggestionSelect(item) { + if (item.kind !== "command") return + const selected = slashCommands().find((entry) => entry.id === item.id) + if (!selected || selected.type === "custom") return + return () => command.trigger(selected.id, "slash") + }, + attachments: { + picker: platform.openAttachmentPickerDialog, + directory: () => sdk().directory, + isDialogActive: () => !!dialog.active, + warn: () => + showToast({ + title: language.t("prompt.toast.pasteUnsupported.title"), + description: language.t("prompt.toast.pasteUnsupported.description"), + }), + duplicate: () => showToast({ title: language.t("prompt.toast.attachmentDuplicate.title") }), + onError: (error) => + showToast({ + variant: "error", + title: language.t("common.requestFailed"), + description: error instanceof Error ? error.message : String(error), + }), + // Amicode webview (patch #11): the bridge reads the OS clipboard through + // the extension host where the framed app has no clipboard permission. + // Bridge first (self-gates to null unframed), platform as fallback. + readClipboardImage: async () => (await readClipboardImageViaBridge()) ?? (await platform.readClipboardImage?.()) ?? null, + // Text side of the same bridge — feeds session-ui's handlePaste framed + // text fallback when the paste event carries nothing readable. + readClipboardText: () => readClipboardViaBridge(), + getPathForFile: platform.getPathForFile, + store: platform.draftStore?.putBlob, + }, + view: { + placeholder: designPlaceholder, + get agent() { + return props.controls.agents.visible && props.controls.agents.options.length > 0 + ? { + options: () => props.controls.agents.options.map((name) => ({ id: name, label: name })), + current: () => props.controls.agents.current, + onSelect: (value: string) => props.controls.agents.select(value), + keybind: () => command.keybindParts("agent.cycle"), + } + : undefined + }, + variant: { + options: () => variants().map((value) => ({ id: value, label: value })), + current: () => props.controls.model.selection.variant.current() ?? "default", + onSelect: (value) => props.controls.model.selection.variant.set(value === "default" ? undefined : value), + keybind: () => command.keybindParts("model.variant.cycle"), + }, + submit: { + stopping, + working, + onSubmit: () => void submission.handleSubmit(new Event("submit")), + onStop: () => void submission.abort(), + }, + }, + }) + Object.defineProperty(controller, "model", { get: () => props.controls.model }) + + // Framed webview: the window-level fallback (global-clipboard.ts) is the + // sole ⌘V owner. When the clipboard carries no text it offers the media to + // this slot, landing it in the composer's attachment pipeline. + setClipboardImageHandler((file) => controller.addAttachments([file])) + onCleanup(() => setClipboardImageHandler(undefined)) + + command.register("prompt-input", () => [ + { + id: "file.attach", + title: language.t("prompt.action.attachFile"), + category: language.t("command.category.file"), + keybind: "mod+u", + disabled: controller.state.mode !== "normal", + onSelect: () => controller.attach(), + }, + { + id: "prompt.mode.shell", + title: language.t("command.prompt.mode.shell"), + category: language.t("command.category.session"), + keybind: "mod+shift+x", + disabled: controller.state.mode === "shell", + onSelect: () => controller.dispatch({ type: "mode.shell" }), + }, + { + id: "prompt.mode.normal", + title: language.t("command.prompt.mode.normal"), + category: language.t("command.category.session"), + keybind: "mod+shift+e", + disabled: controller.state.mode === "normal", + onSelect: () => controller.dispatch({ type: "mode.normal" }), + }, + ]) + + createEffect( + on( + () => props.edit?.id, + (id) => { + const edit = props.edit + if (!id || !edit) return + prompt.context.items().forEach((item) => prompt.context.remove(item.key)) + edit.context.forEach((item) => + prompt.context.add({ + type: item.type, + path: item.path, + selection: item.selection, + comment: item.comment, + commentID: item.commentID, + commentOrigin: item.commentOrigin, + preview: item.preview, + }), + ) + controller.dispatch({ type: "mode.normal" }) + controller.resetHistory() + prompt.set(edit.prompt, promptLength(edit.prompt)) + controller.restoreFocus() + props.onEditLoaded?.() + }, + { defer: true }, + ), + ) + + return controller as PromptInputV2ComposerController +} + +function PromptInputV2ModelControl(props: { + loading: boolean + paid: boolean + title: string + keybind: string[] + model: PromptInputV2ComposerController["model"]["selection"] + providerID?: string + modelName: string + onClose: () => void + onUnpaidClick: () => void +}) { + const shouldAnimate = createMemo((previous) => previous ?? props.loading) + const content = () => ( + <> + + {(providerID) => ( + + )} + + {props.modelName} + + + + + ) + return ( + + + {props.title} + + + } + > + + {content()} + + } + > + ( + + {content()} + + )} + onClose={props.onClose} + /> + + + + ) +} + +function openComment( + item: { path: string; commentID?: string; commentOrigin?: "review" | "file" }, + props: PromptInputV2ControllerProps, + sync: ReturnType, + layout: ReturnType, + files: ReturnType, + comments: ReturnType, +) { + if (!item.commentID) return + const focus = { file: item.path, id: item.commentID } + comments.setActive(focus) + const queueFocus = (attempts = 6) => { + requestAnimationFrame(() => { + comments.setFocus({ ...focus }) + if (attempts <= 0) return + requestAnimationFrame(() => { + const current = comments.focus() + if (current?.file === focus.file && current.id === focus.id) queueFocus(attempts - 1) + }) + }) + } + const diffs = props.controls.session.id ? sync().data.session_diff[props.controls.session.id] : undefined + const review = + item.commentOrigin === "review" || (item.commentOrigin !== "file" && diffs?.some((diff) => diff.file === item.path)) + if (!props.controls.session.reviewPanel.opened()) props.controls.session.reviewPanel.open() + if (review) { + layout.fileTree.setTab("changes") + props.controls.session.tabs.setActive("review") + queueFocus() + return + } + layout.fileTree.setTab("all") + const tab = files.tab(item.path) + void props.controls.session.tabs.open(tab) + props.controls.session.tabs.setActive(tab) + void Promise.resolve(files.load(item.path)).finally(() => queueFocus()) +} diff --git a/packages/app-bundle/overlay/packages/app/src/components/prompt-input.tsx b/packages/app-bundle/overlay/packages/app/src/components/prompt-input.tsx new file mode 100644 index 00000000..9d0b15a9 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/components/prompt-input.tsx @@ -0,0 +1,1789 @@ +import { useFilteredList } from "@opencode-ai/ui/hooks" +import { useSpring } from "@opencode-ai/ui/motion-spring" +import { + createEffect, + on, + Component, + Show, + onCleanup, + createMemo, + createSignal, + createResource, + Switch, + Match, + type JSX, +} from "solid-js" +import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file" +import { + ContentPart, + DEFAULT_PROMPT, + isCommentItem, + isPromptEqual, + Prompt, + usePrompt, + ImageAttachmentPart, + AgentPart, + FileAttachmentPart, +} from "@/context/prompt" +import { useLayout } from "@/context/layout" +import { useSDK } from "@/context/sdk" +import { useSync } from "@/context/sync" +import { useComments } from "@/context/comments" +import { Button } from "@opencode-ai/ui/button" +import { DockShellForm, DockTray } from "@opencode-ai/ui/dock-surface" +import { Icon } from "@opencode-ai/ui/icon" +import { ProviderIcon } from "@opencode-ai/ui/provider-icon" +import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" +import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" +import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" +import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" +import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2" +import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2" +import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" +import { IconButton } from "@opencode-ai/ui/icon-button" +import { Select } from "@opencode-ai/ui/select" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { ModelSelectorPopover, ModelSelectorPopoverV2 } from "@/components/dialog-select-model" +import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid" +import { DialogSelectModelUnpaidV2 } from "@/components/dialog-select-model-unpaid-v2" +import { useCommand } from "@/context/command" +import { usePermission } from "@/context/permission" +import { useLanguage } from "@/context/language" +import { usePlatform } from "@/context/platform" +import { createSessionTabs } from "@/pages/session/helpers" +import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom" +import { createPromptAttachments } from "./prompt-input/attachments" +import { readClipboardImageViaBridge, readClipboardViaBridge } from "./prompt-input/clipboard-bridge" +import { ACCEPTED_FILE_TYPES, pickAttachmentFiles } from "./prompt-input/files" +import { + canNavigateHistoryAtCursor, + navigatePromptHistory, + type PromptHistoryComment, + type PromptHistoryEntry, + promptLength, +} from "./prompt-input/history" +import { + createPersistedPromptInputHistory, + createPromptInputHistory, + type PromptInputHistory, +} from "./prompt-input/history-store" +import { + type PromptInputControls, + type PromptInputProps, + type PromptInputState, + type PromptInputSubmission, +} from "./prompt-input/contracts" +import { createPromptSubmit } from "./prompt-input/submit" +import { PromptPopover, type AtOption, type SlashCommand } from "./prompt-input/slash-popover" +import { PromptContextItems } from "./prompt-input/context-items" +import { PromptImageAttachments } from "./prompt-input/image-attachments" +import { PromptDragOverlay } from "./prompt-input/drag-overlay" +import { promptDesignPlaceholder, promptPlaceholder } from "./prompt-input/placeholder" +import { createPromptInputTransientState } from "./prompt-input/transient-state" +import { showToast } from "@/utils/toast" +import { hiddenProjectWorktree } from "@/utils/amicode-hidden-project" +import { ImagePreview } from "@opencode-ai/ui/image-preview" +import { setClipboardImageHandler } from "@/utils/global-clipboard" +import type { ReferenceInfo } from "@opencode-ai/sdk/v2/client" + +export { createPromptInputHistory } +export type { PromptInputControls, PromptInputHistory, PromptInputProps, PromptInputState, PromptInputSubmission } + +// amicode: rotation trimmed to the five pulse-design examples (en.ts values +// rebranded; keys 6-25 left dormant in the dictionaries for easy revert) +const EXAMPLES = [ + "prompt.example.1", + "prompt.example.2", + "prompt.example.3", + "prompt.example.4", + "prompt.example.5", +] as const + +export const PromptInput: Component = (props) => { + const sdk = useSDK() + + const sync = useSync() + const files = useFile() + const prompt = props.state ?? usePrompt() + const layout = useLayout() + const comments = useComments() + const dialog = useDialog() + const command = useCommand() + const permission = usePermission() + const language = useLanguage() + const platform = usePlatform() + const tabs = () => props.controls.session.tabs + let editorRef!: HTMLDivElement + let fileInputRef: HTMLInputElement | undefined + let scrollRef!: HTMLDivElement + let slashPopoverRef!: HTMLDivElement + let restoreEndOnFocus = true + let savedCursor: number | null = null + + const mirror = { input: false } + const inset = 56 + const space = `${inset}px` + + const scrollCursorIntoView = () => { + const container = scrollRef + const selection = window.getSelection() + if (!container || !selection || selection.rangeCount === 0) return + + const range = selection.getRangeAt(0) + if (!editorRef.contains(range.startContainer)) return + + const cursor = getCursorPosition(editorRef) + const length = promptLength(prompt.current().filter((part) => part.type !== "image")) + if (cursor >= length) { + container.scrollTop = container.scrollHeight + return + } + + const rect = range.getClientRects().item(0) ?? range.getBoundingClientRect() + if (!rect.height) return + + const containerRect = container.getBoundingClientRect() + const top = rect.top - containerRect.top + container.scrollTop + const bottom = rect.bottom - containerRect.top + container.scrollTop + const padding = 12 + + if (top < container.scrollTop + padding) { + container.scrollTop = Math.max(0, top - padding) + return + } + + if (bottom > container.scrollTop + container.clientHeight - inset) { + container.scrollTop = bottom - container.clientHeight + inset + } + } + + const queueScroll = (count = 2) => { + requestAnimationFrame(() => { + scrollCursorIntoView() + if (count > 1) queueScroll(count - 1) + }) + } + + const activeFileTab = createSessionTabs({ + tabs, + pathFromTab: files.pathFromTab, + normalizeTab: (tab) => (tab.startsWith("file://") ? files.tab(tab) : tab), + }).activeFileTab + + const commentInReview = (path: string) => { + const sessionID = props.controls.session.id + if (!sessionID) return false + + const diffs = sync().data.session_diff[sessionID] + if (!diffs) return false + return diffs.some((diff) => diff.file === path) + } + + const openComment = (item: { path: string; commentID?: string; commentOrigin?: "review" | "file" }) => { + if (!item.commentID) return + + const focus = { file: item.path, id: item.commentID } + comments.setActive(focus) + + const queueCommentFocus = (attempts = 6) => { + const schedule = (left: number) => { + requestAnimationFrame(() => { + comments.setFocus({ ...focus }) + if (left <= 0) return + requestAnimationFrame(() => { + const current = comments.focus() + if (!current) return + if (current.file !== focus.file || current.id !== focus.id) return + schedule(left - 1) + }) + }) + } + + schedule(attempts) + } + + const wantsReview = item.commentOrigin === "review" || (item.commentOrigin !== "file" && commentInReview(item.path)) + if (wantsReview) { + if (!props.controls.session.reviewPanel.opened()) props.controls.session.reviewPanel.open() + layout.fileTree.setTab("changes") + tabs().setActive("review") + queueCommentFocus() + return + } + + if (!props.controls.session.reviewPanel.opened()) props.controls.session.reviewPanel.open() + layout.fileTree.setTab("all") + const tab = files.tab(item.path) + void tabs().open(tab) + tabs().setActive(tab) + void Promise.resolve(files.load(item.path)).finally(() => queueCommentFocus()) + } + + const recent = createMemo(() => { + const all = tabs().all() + const active = activeFileTab() + const order = active ? [active, ...all.filter((x) => x !== active)] : all + const seen = new Set() + const paths: string[] = [] + + for (const tab of order) { + const path = files.pathFromTab(tab) + if (!path) continue + if (seen.has(path)) continue + seen.add(path) + paths.push(path) + } + + return paths + }) + const info = createMemo(() => (props.controls.session.id ? sync().session.get(props.controls.session.id) : undefined)) + const working = createMemo(() => sync().data.session_working(props.controls.session.id ?? "")) + const imageAttachments = createMemo(() => + prompt.current().filter((part): part is ImageAttachmentPart => part.type === "image"), + ) + + const [store, setStore] = createPromptInputTransientState( + () => prompt.capture(), + Math.floor(Math.random() * EXAMPLES.length), + ) + const buttonsSpring = useSpring(() => (store.mode === "normal" ? 1 : 0), { visualDuration: 0.2, bounce: 0 }) + const motion = (value: number) => ({ + opacity: value, + transform: `scale(${0.98 + value * 0.02})`, + filter: `blur(${(1 - value) * 2}px)`, + "pointer-events": value > 0.5 ? ("auto" as const) : ("none" as const), + }) + const buttons = createMemo(() => motion(buttonsSpring())) + const shell = createMemo(() => motion(1 - buttonsSpring())) + const control = createMemo(() => ({ height: "28px", ...buttons() })) + + const commentCount = createMemo(() => { + if (store.mode === "shell") return 0 + return prompt.context.items().filter((item) => !!item.comment?.trim()).length + }) + const blank = createMemo(() => { + const text = prompt + .current() + .map((part) => ("content" in part ? part.content : "")) + .join("") + return text.trim().length === 0 && imageAttachments().length === 0 && commentCount() === 0 + }) + const stopping = createMemo(() => working() && blank()) + const tip = () => { + if (stopping()) { + return ( +
+ {language.t("prompt.action.stop")} + {language.t("common.key.esc")} +
+ ) + } + + return ( +
+ {language.t("prompt.action.send")} + +
+ ) + } + + const contextItems = createMemo(() => { + const items = prompt.context.items() + if (store.mode !== "shell") return items + return items.filter((item) => !item.comment?.trim()) + }) + + const hasUserPrompt = createMemo(() => { + const sessionID = props.controls.session.id + if (!sessionID) return false + const messages = sync().data.message[sessionID] + if (!messages) return false + return messages.some((m) => m.role === "user") + }) + + const history = props.history ?? createPersistedPromptInputHistory() + + const suggest = createMemo(() => !hasUserPrompt()) + + const placeholder = createMemo(() => + promptPlaceholder({ + mode: store.mode, + commentCount: commentCount(), + example: suggest() ? (store.mode === "shell" ? "git status" : language.t(EXAMPLES[store.placeholder])) : "", + suggest: suggest(), + t: (key, params) => language.t(key as Parameters[0], params as never), + }), + ) + + const historyComments = () => { + const byID = new Map(comments.all().map((item) => [`${item.file}\n${item.id}`, item] as const)) + return prompt.context.items().flatMap((item) => { + if (item.type !== "file") return [] + const comment = item.comment?.trim() + if (!comment) return [] + + const selection = item.commentID ? byID.get(`${item.path}\n${item.commentID}`)?.selection : undefined + const nextSelection = + selection ?? + (item.selection + ? ({ + start: item.selection.startLine, + end: item.selection.endLine, + } satisfies SelectedLineRange) + : undefined) + if (!nextSelection) return [] + + return [ + { + id: item.commentID ?? item.key, + path: item.path, + selection: { ...nextSelection }, + comment, + time: item.commentID ? (byID.get(`${item.path}\n${item.commentID}`)?.time ?? Date.now()) : Date.now(), + origin: item.commentOrigin, + preview: item.preview, + } satisfies PromptHistoryComment, + ] + }) + } + + const applyHistoryComments = (items: PromptHistoryComment[]) => { + comments.replace( + items.map((item) => ({ + id: item.id, + file: item.path, + selection: { ...item.selection }, + comment: item.comment, + time: item.time, + })), + ) + prompt.context.replaceComments( + items.map((item) => ({ + type: "file" as const, + path: item.path, + selection: selectionFromLines(item.selection), + comment: item.comment, + commentID: item.id, + commentOrigin: item.origin, + preview: item.preview, + })), + ) + } + + const applyHistoryPrompt = (entry: PromptHistoryEntry, position: "start" | "end") => { + const p = entry.prompt + const length = position === "start" ? 0 : promptLength(p) + setStore("applyingHistory", true) + applyHistoryComments(entry.comments) + prompt.set(p, length) + requestAnimationFrame(() => { + editorRef.focus() + setCursorPosition(editorRef, length) + setStore("applyingHistory", false) + queueScroll() + }) + } + + const getCaretState = () => { + const selection = window.getSelection() + const textLength = promptLength(prompt.current()) + if (!selection || selection.rangeCount === 0) { + return { collapsed: false, cursorPosition: 0, textLength } + } + const anchorNode = selection.anchorNode + if (!anchorNode || !editorRef.contains(anchorNode)) { + return { collapsed: false, cursorPosition: 0, textLength } + } + return { + collapsed: selection.isCollapsed, + cursorPosition: getCursorPosition(editorRef), + textLength, + } + } + + const escBlur = () => platform.platform === "desktop" && platform.os === "macos" + + const pick = () => { + pickAttachmentFiles({ + picker: platform.openAttachmentPickerDialog, + directory: () => sdk().directory, + fallback: () => fileInputRef?.click(), + onFile: addAttachment, + onError: (error) => + showToast({ + variant: "error", + title: language.t("common.requestFailed"), + description: error instanceof Error ? error.message : String(error), + }), + }) + } + + const setMode = (mode: "normal" | "shell") => { + setStore("mode", mode) + setStore({ popover: null, slashMenu: false, slashMenuQuery: "" }) + requestAnimationFrame(() => editorRef?.focus()) + } + + const shellModeKey = "mod+shift+x" + const normalModeKey = "mod+shift+e" + + command.register("prompt-input", () => [ + { + id: "file.attach", + title: language.t("prompt.action.attachFile"), + category: language.t("command.category.file"), + keybind: "mod+u", + disabled: store.mode !== "normal", + onSelect: pick, + }, + { + id: "prompt.mode.shell", + title: language.t("command.prompt.mode.shell"), + category: language.t("command.category.session"), + keybind: shellModeKey, + disabled: store.mode === "shell", + onSelect: () => setMode("shell"), + }, + { + id: "prompt.mode.normal", + title: language.t("command.prompt.mode.normal"), + category: language.t("command.category.session"), + keybind: normalModeKey, + disabled: store.mode === "normal", + onSelect: () => setMode("normal"), + }, + ]) + + const closePopover = () => setStore({ popover: null, slashMenu: false, slashMenuQuery: "" }) + + const resetHistoryNavigation = (force = false) => { + if (!force && (store.historyIndex < 0 || store.applyingHistory)) return + setStore("historyIndex", -1) + setStore("savedPrompt", null) + } + + const clearEditor = () => { + editorRef.innerHTML = "" + } + + const setEditorText = (text: string) => { + clearEditor() + editorRef.textContent = text + } + + const focusEditorEnd = () => { + requestAnimationFrame(() => { + editorRef.focus() + const range = document.createRange() + const selection = window.getSelection() + range.selectNodeContents(editorRef) + range.collapse(false) + selection?.removeAllRanges() + selection?.addRange(range) + }) + } + + const currentCursor = () => { + const selection = window.getSelection() + if (!selection || selection.rangeCount === 0 || !editorRef.contains(selection.anchorNode)) return null + return getCursorPosition(editorRef) + } + + const restoreFocus = () => { + requestAnimationFrame(() => { + const cursor = savedCursor ?? prompt.cursor() ?? promptLength(prompt.current()) + editorRef.focus() + setCursorPosition(editorRef, cursor) + queueScroll() + }) + } + + const handleFocus = () => { + if (!restoreEndOnFocus) return + restoreEndOnFocus = false + requestAnimationFrame(() => { + if (document.activeElement !== editorRef) return + setCursorPosition(editorRef, prompt.cursor() ?? promptLength(prompt.current())) + queueScroll() + }) + } + + const renderEditorWithCursor = (parts: Prompt) => { + const cursor = currentCursor() + renderEditor(parts) + if (cursor !== null) setCursorPosition(editorRef, cursor) + } + + createEffect(() => { + props.controls.session.id + if (props.controls.session.id) return + if (!suggest()) return + const interval = setInterval(() => { + setStore("placeholder", (prev) => (prev + 1) % EXAMPLES.length) + }, 6500) + onCleanup(() => clearInterval(interval)) + }) + + const [composing, setComposing] = createSignal(false) + const isImeComposing = (event: KeyboardEvent) => event.isComposing || composing() || event.keyCode === 229 + + const handleBlur = () => { + const cursor = currentCursor() + savedCursor = cursor + if (cursor !== null && cursor !== prompt.cursor()) prompt.set(prompt.current(), cursor) + closePopover() + setComposing(false) + } + + const handleCompositionStart = () => { + setComposing(true) + } + + const handleCompositionEnd = () => { + setComposing(false) + requestAnimationFrame(() => { + if (composing()) return + reconcile(prompt.current().filter((part) => part.type !== "image")) + }) + } + + const referenceDescription = (reference: ReferenceInfo) => + reference.source.type === "git" ? reference.source.repository : reference.source.path + + const referenceList = createMemo(() => + sync() + .data.reference.filter((reference) => !reference.hidden) + .map( + (reference): AtOption => ({ + type: "reference", + name: reference.name, + path: reference.path, + display: reference.name, + description: reference.description ?? referenceDescription(reference), + }), + ), + ) + + const agentList = createMemo(() => + props.controls.agents.available + .filter((agent) => !agent.hidden && agent.mode !== "primary") + .map((agent): AtOption => ({ type: "agent", name: agent.name, display: agent.name })), + ) + + const mcpResourceList = createMemo(() => + Object.values(sync().data.mcp_resource).map( + (resource): AtOption => ({ + type: "resource", + name: resource.name, + uri: resource.uri, + client: resource.server, + display: resource.name, + description: resource.description, + mime: resource.mimeType, + }), + ), + ) + + const handleAtSelect = (option: AtOption | undefined) => { + if (!option) return + if (option.type === "agent") { + addPart({ type: "agent", name: option.name, content: "@" + option.name, start: 0, end: 0 }) + return + } + if (option.type === "reference") { + addPart({ + type: "file", + path: option.path, + content: "@" + option.name, + start: 0, + end: 0, + mime: "application/x-directory", + filename: option.name, + }) + return + } + if (option.type === "resource") { + addPart({ + type: "file", + path: option.uri, + content: "@" + option.name, + start: 0, + end: 0, + mime: option.mime ?? "text/plain", + filename: option.name, + url: option.uri, + source: { + type: "resource", + text: { value: "@" + option.name, start: 0, end: 0 }, + clientName: option.client, + uri: option.uri, + }, + }) + return + } + addPart({ type: "file", path: option.path, content: "@" + option.path, start: 0, end: 0 }) + } + + const atKey = (x: AtOption | undefined) => { + if (!x) return "" + if (x.type === "agent") return `agent:${x.name}` + if (x.type === "reference") return `reference:${x.name}` + if (x.type === "resource") return `resource:${x.client}:${x.uri}` + return `file:${x.path}` + } + + const { + flat: atFlat, + active: atActive, + setActive: setAtActive, + onInput: atOnInput, + onKeyDown: atOnKeyDown, + } = useFilteredList({ + items: async (query) => { + const references = referenceList() + const agents = agentList() + const mcpResources = mcpResourceList() + const open = recent() + const seen = new Set(open) + const pinned: AtOption[] = open.map((path) => ({ type: "file", path, display: path, recent: true })) + if (!query.trim()) return [...references, ...agents, ...mcpResources, ...pinned] + const paths = await files.searchFilesAndDirectories(query) + const fileOptions: AtOption[] = paths + .filter((path) => !seen.has(path)) + .map((path) => ({ type: "file", path, display: path })) + return [...references, ...agents, ...mcpResources, ...pinned, ...fileOptions] + }, + key: atKey, + filterKeys: ["display"], + skipFilter: (item) => item.type === "file" && !item.recent, + groupBy: (item) => { + if (item.type === "reference") return "reference" + if (item.type === "agent") return "agent" + if (item.type === "resource") return "resource" + if (item.recent) return "recent" + return "file" + }, + sortGroupsBy: (a, b) => { + const rank = (category: string) => { + if (category === "reference") return 0 + if (category === "agent") return 1 + if (category === "resource") return 2 + if (category === "recent") return 3 + return 4 + } + return rank(a.category) - rank(b.category) + }, + onSelect: handleAtSelect, + }) + + const slashCommands = createMemo(() => { + const builtin = command.options + .filter((opt) => !opt.disabled && !opt.id.startsWith("suggested.") && opt.slash) + .map((opt) => ({ + id: opt.id, + trigger: opt.slash!, + title: opt.title, + description: opt.description, + keybind: opt.keybind, + type: "builtin" as const, + })) + + const custom = sync().data.command.map((cmd) => ({ + id: `custom.${cmd.name}`, + trigger: cmd.name, + title: cmd.name, + description: cmd.description, + type: "custom" as const, + // source: cmd.source, + })) + + return [...custom, ...builtin] + }) + + const handleSlashSelect = (cmd: SlashCommand | undefined) => { + if (!cmd) return + const menu = store.slashMenu + closePopover() + const images = imageAttachments() + + if (cmd.type === "custom") { + const text = `/${cmd.trigger} ` + if (menu) { + editorRef.focus() + setCursorPosition(editorRef, 0) + addPart({ type: "text", content: text, start: 0, end: text.length }) + focusEditorEnd() + return + } + setEditorText(text) + prompt.set([{ type: "text", content: text, start: 0, end: text.length }, ...images], text.length) + focusEditorEnd() + return + } + + if (menu) { + command.trigger(cmd.id, "slash") + return + } + + clearEditor() + prompt.set([...DEFAULT_PROMPT, ...images], 0) + command.trigger(cmd.id, "slash") + } + + const { + flat: slashFlat, + active: slashActive, + setActive: setSlashActive, + onInput: slashOnInput, + onKeyDown: slashOnKeyDown, + } = useFilteredList({ + items: slashCommands, + key: (x) => x?.id, + filterKeys: ["trigger", "title"], + onSelect: handleSlashSelect, + }) + + const createPill = (part: FileAttachmentPart | AgentPart) => { + const pill = document.createElement("span") + pill.textContent = part.content + pill.setAttribute("data-type", part.type) + if (part.type === "file") { + pill.setAttribute("data-path", part.path) + if (part.mime) pill.setAttribute("data-mime", part.mime) + if (part.filename) pill.setAttribute("data-filename", part.filename) + if (part.url) pill.setAttribute("data-url", part.url) + if (part.source?.type === "resource") { + pill.setAttribute("data-source-type", part.source.type) + pill.setAttribute("data-source-client-name", part.source.clientName) + pill.setAttribute("data-source-uri", part.source.uri) + } + } + if (part.type === "agent") pill.setAttribute("data-name", part.name) + pill.setAttribute("contenteditable", "false") + pill.style.userSelect = "text" + pill.style.cursor = "default" + return pill + } + + const isNormalizedEditor = () => + Array.from(editorRef.childNodes).every((node) => { + if (node.nodeType === Node.TEXT_NODE) { + const text = node.textContent ?? "" + if (!text.includes("\u200B")) return true + if (text !== "\u200B") return false + + const prev = node.previousSibling + const next = node.nextSibling + const prevIsBr = prev?.nodeType === Node.ELEMENT_NODE && (prev as HTMLElement).tagName === "BR" + return !!prevIsBr && !next + } + if (node.nodeType !== Node.ELEMENT_NODE) return false + const el = node as HTMLElement + if (el.dataset.type === "file") return true + if (el.dataset.type === "agent") return true + return el.tagName === "BR" + }) + + const renderEditor = (parts: Prompt) => { + clearEditor() + for (const part of parts) { + if (part.type === "text") { + editorRef.appendChild(createTextFragment(part.content)) + continue + } + if (part.type === "file" || part.type === "agent") { + editorRef.appendChild(createPill(part)) + } + } + + const last = editorRef.lastChild + if (last?.nodeType === Node.ELEMENT_NODE && (last as HTMLElement).tagName === "BR") { + editorRef.appendChild(document.createTextNode("\u200B")) + } + } + + const scrollSlashActiveIntoView = () => { + const activeId = slashActive() + if (!activeId || !slashPopoverRef) return + + requestAnimationFrame(() => { + const element = slashPopoverRef.querySelector(`[data-slash-id="${activeId}"]`) + element?.scrollIntoView({ block: "nearest", behavior: "smooth" }) + }) + } + const selectPopoverActive = () => { + if (store.popover === "at") { + const items = atFlat() + if (items.length === 0) return + const active = atActive() + const item = items.find((entry) => atKey(entry) === active) ?? items[0] + handleAtSelect(item) + return + } + + if (store.popover === "slash") { + const items = slashFlat() + if (items.length === 0) return + const active = slashActive() + const item = items.find((entry) => entry.id === active) ?? items[0] + handleSlashSelect(item) + } + } + + const reconcile = (input: Prompt) => { + if (mirror.input) { + mirror.input = false + if (isNormalizedEditor()) return + + renderEditorWithCursor(input) + return + } + + const dom = parseFromDOM() + if (isNormalizedEditor() && isPromptEqual(input, dom)) return + + renderEditorWithCursor(input) + } + + createEffect( + on( + () => prompt.current(), + (parts) => { + if (composing()) return + reconcile(parts.filter((part) => part.type !== "image")) + }, + ), + ) + + const parseFromDOM = (): Prompt => { + const parts: Prompt = [] + let position = 0 + let buffer = "" + + const flushText = () => { + let content = buffer + if (content.includes("\r")) content = content.replace(/\r\n?/g, "\n") + if (content.includes("\u200B")) content = content.replace(/\u200B/g, "") + buffer = "" + if (!content) return + parts.push({ type: "text", content, start: position, end: position + content.length }) + position += content.length + } + + const pushFile = (file: HTMLElement) => { + const content = file.textContent ?? "" + const source = + file.dataset.sourceType === "resource" && file.dataset.sourceClientName && file.dataset.sourceUri + ? { + type: "resource" as const, + text: { + value: content, + start: position, + end: position + content.length, + }, + clientName: file.dataset.sourceClientName, + uri: file.dataset.sourceUri, + } + : undefined + parts.push({ + type: "file", + path: file.dataset.path!, + content, + start: position, + end: position + content.length, + ...(file.dataset.mime ? { mime: file.dataset.mime } : {}), + ...(file.dataset.filename ? { filename: file.dataset.filename } : {}), + ...(file.dataset.url ? { url: file.dataset.url } : {}), + ...(source ? { source } : {}), + }) + position += content.length + } + + const pushAgent = (agent: HTMLElement) => { + const content = agent.textContent ?? "" + parts.push({ + type: "agent", + name: agent.dataset.name!, + content, + start: position, + end: position + content.length, + }) + position += content.length + } + + const visit = (node: Node) => { + if (node.nodeType === Node.TEXT_NODE) { + buffer += node.textContent ?? "" + return + } + if (node.nodeType !== Node.ELEMENT_NODE) return + + const el = node as HTMLElement + if (el.dataset.type === "file") { + flushText() + pushFile(el) + return + } + if (el.dataset.type === "agent") { + flushText() + pushAgent(el) + return + } + if (el.tagName === "BR") { + buffer += "\n" + return + } + + for (const child of Array.from(el.childNodes)) { + visit(child) + } + } + + const children = Array.from(editorRef.childNodes) + children.forEach((child, index) => { + const isBlock = child.nodeType === Node.ELEMENT_NODE && ["DIV", "P"].includes((child as HTMLElement).tagName) + visit(child) + if (isBlock && index < children.length - 1) { + buffer += "\n" + } + }) + + flushText() + + if (parts.length === 0) parts.push(...DEFAULT_PROMPT) + return parts + } + + const handleInput = () => { + const rawParts = parseFromDOM() + const images = imageAttachments() + const cursorPosition = getCursorPosition(editorRef) + const rawText = + rawParts.length === 1 && rawParts[0]?.type === "text" + ? rawParts[0].content + : rawParts.map((p) => ("content" in p ? p.content : "")).join("") + const hasNonText = rawParts.some((part) => part.type !== "text") + const textContent = (editorRef.textContent ?? "").replace(/\u200B/g, "") + const shouldReset = + textContent.length === 0 && rawText.replace(/\n/g, "").length === 0 && !hasNonText && images.length === 0 + + if (shouldReset) { + closePopover() + resetHistoryNavigation() + if (prompt.dirty()) { + mirror.input = true + prompt.set(DEFAULT_PROMPT, 0) + } + queueScroll() + return + } + + const shellMode = store.mode === "shell" + + if (!shellMode) { + const atMatch = rawText.substring(0, cursorPosition).match(/@(\S*)$/) + const slashMatch = rawText.match(/^\/(\S*)$/) + + if (atMatch) { + atOnInput(atMatch[1]) + setStore({ popover: "at", slashMenu: false, slashMenuQuery: "" }) + } else if (slashMatch) { + slashOnInput(slashMatch[1]) + setStore({ popover: "slash", slashMenu: false, slashMenuQuery: "" }) + } else { + closePopover() + } + } else { + closePopover() + } + + resetHistoryNavigation() + + mirror.input = true + prompt.set([...rawParts, ...images], cursorPosition) + queueScroll() + } + + const addPart = (part: ContentPart) => { + if (part.type === "image") return false + + const selection = window.getSelection() + if (!selection) return false + + if (selection.rangeCount === 0 || !editorRef.contains(selection.anchorNode)) { + editorRef.focus() + const cursor = prompt.cursor() ?? promptLength(prompt.current()) + setCursorPosition(editorRef, cursor) + } + + if (selection.rangeCount === 0) return false + const range = selection.getRangeAt(0) + if (!editorRef.contains(range.startContainer)) return false + + if (part.type === "file" || part.type === "agent") { + const cursorPosition = getCursorPosition(editorRef) + const rawText = prompt + .current() + .map((p) => ("content" in p ? p.content : "")) + .join("") + const textBeforeCursor = rawText.substring(0, cursorPosition) + const atMatch = textBeforeCursor.match(/@(\S*)$/) + const pill = createPill(part) + const gap = document.createTextNode(" ") + + if (atMatch) { + const start = atMatch.index ?? cursorPosition - atMatch[0].length + setRangeEdge(editorRef, range, "start", start) + setRangeEdge(editorRef, range, "end", cursorPosition) + } + + range.deleteContents() + range.insertNode(gap) + range.insertNode(pill) + range.setStartAfter(gap) + range.collapse(true) + selection.removeAllRanges() + selection.addRange(range) + } + + if (part.type === "text") { + const fragment = createTextFragment(part.content) + const last = fragment.lastChild + range.deleteContents() + range.insertNode(fragment) + if (last) { + if (last.nodeType === Node.TEXT_NODE) { + const text = last.textContent ?? "" + if (text === "\u200B") { + range.setStart(last, 0) + } + if (text !== "\u200B") { + range.setStart(last, text.length) + } + } + if (last.nodeType !== Node.TEXT_NODE) { + const isBreak = last.nodeType === Node.ELEMENT_NODE && (last as HTMLElement).tagName === "BR" + const next = last.nextSibling + const emptyText = next?.nodeType === Node.TEXT_NODE && (next.textContent ?? "") === "" + if (isBreak && (!next || emptyText)) { + const placeholder = next && emptyText ? next : document.createTextNode("\u200B") + if (!next) last.parentNode?.insertBefore(placeholder, null) + placeholder.textContent = "\u200B" + range.setStart(placeholder, 0) + } else { + range.setStartAfter(last) + } + } + } + range.collapse(true) + selection.removeAllRanges() + selection.addRange(range) + } + + handleInput() + closePopover() + return true + } + + const addToHistory = (prompt: Prompt, mode: "normal" | "shell") => { + history.add(prompt, mode, mode === "shell" ? [] : historyComments()) + } + + createEffect( + on( + () => props.edit?.id, + (id) => { + const edit = props.edit + if (!id || !edit) return + + for (const item of prompt.context.items()) { + prompt.context.remove(item.key) + } + + for (const item of edit.context) { + prompt.context.add({ + type: item.type, + path: item.path, + selection: item.selection, + comment: item.comment, + commentID: item.commentID, + commentOrigin: item.commentOrigin, + preview: item.preview, + }) + } + + setStore("mode", "normal") + closePopover() + setStore("historyIndex", -1) + setStore("savedPrompt", null) + prompt.set(edit.prompt, promptLength(edit.prompt)) + requestAnimationFrame(() => { + editorRef.focus() + setCursorPosition(editorRef, promptLength(edit.prompt)) + queueScroll() + }) + props.onEditLoaded?.() + }, + { defer: true }, + ), + ) + + const navigateHistory = (direction: "up" | "down") => { + const result = navigatePromptHistory({ + direction, + entries: history.entries(store.mode), + historyIndex: store.historyIndex, + currentPrompt: prompt.current(), + currentComments: historyComments(), + savedPrompt: store.savedPrompt, + }) + if (!result.handled) return false + setStore("historyIndex", result.historyIndex) + setStore("savedPrompt", result.savedPrompt) + applyHistoryPrompt(result.entry, result.cursor) + return true + } + + const { addAttachment, addAttachments, removeAttachment, handlePaste } = createPromptAttachments({ + prompt, + editor: () => editorRef, + isDialogActive: () => !!dialog.active, + setDraggingType: (type) => setStore("draggingType", type), + focusEditor: () => { + editorRef.focus() + setCursorPosition(editorRef, promptLength(prompt.current())) + }, + addPart, + // Amicode webview: image on the OS clipboard is only readable via the outer + // webview's navigator.clipboard.read() (chat_panel.ts). Bridge first so + // Ctrl+V in the iframe attaches the image directly (har-moniqs/amicode#365); + // platform fallback preserves desktop/TUI native clipboard. + readClipboardImage: async () => (await readClipboardImageViaBridge()) ?? (await platform.readClipboardImage?.()) ?? null, + // Webview-iframe paste fallback; self-gates to a no-op outside the webview. + readClipboardText: () => readClipboardViaBridge(), + getPathForFile: platform.getPathForFile, + }) + + // Framed webview: the window-level fallback (global-clipboard.ts) is the + // sole ⌘V owner. When the clipboard carries no text it offers the media to + // this slot, landing it in the composer's attachment pipeline (mirrors v2). + setClipboardImageHandler((file) => void addAttachments([file])) + onCleanup(() => setClipboardImageHandler(undefined)) + + const fileAttachmentInput = () => ( + (fileInputRef = el)} + type="file" + multiple + accept={ACCEPTED_FILE_TYPES.join(",")} + class="hidden" + onChange={(e) => { + const list = e.currentTarget.files + if (list) void addAttachments(Array.from(list)) + e.currentTarget.value = "" + }} + /> + ) + + const variants = createMemo(() => ["default", ...props.controls.model.selection.variant.list()]) + // Check provider variants directly: `variants` also includes the UI-only default option. + const showVariantControl = createMemo(() => props.controls.model.selection.variant.list().length > 0) + const accepting = createMemo(() => { + const id = props.controls.session.id + if (!id) return permission.isAutoAcceptingDirectory(sdk().directory) + return permission.isAutoAccepting(id, sdk().directory) + }) + + const { abort, handleSubmit } = + props.submission ?? + createPromptSubmit({ + prompt, + info, + imageAttachments, + commentCount, + autoAccept: () => accepting(), + mode: () => store.mode, + working, + editor: () => editorRef, + queueScroll, + promptLength, + addToHistory, + resetHistoryNavigation: () => { + resetHistoryNavigation(true) + }, + setMode: (mode) => setStore("mode", mode), + setPopover: (popover) => { + if (!popover) return closePopover() + setStore({ popover, slashMenu: false, slashMenuQuery: "" }) + }, + newSessionWorktree: () => props.newSessionWorktree, + onNewSessionWorktreeReset: props.onNewSessionWorktreeReset, + shouldQueue: props.shouldQueue, + onQueue: props.onQueue, + onAbort: props.onAbort, + onSubmit: props.onSubmit, + model: props.controls.model.selection, + }) + + const handleKeyDown = (event: KeyboardEvent) => { + if ((event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === "u") { + event.preventDefault() + if (store.mode !== "normal") return + pick() + return + } + + if (event.key === "Backspace") { + const selection = window.getSelection() + if (selection && selection.isCollapsed) { + const node = selection.anchorNode + const offset = selection.anchorOffset + if (node && node.nodeType === Node.TEXT_NODE) { + const text = node.textContent ?? "" + if (/^\u200B+$/.test(text) && offset > 0) { + const range = document.createRange() + range.setStart(node, 0) + range.collapse(true) + selection.removeAllRanges() + selection.addRange(range) + } + } + } + } + + if (event.key === "!" && store.mode === "normal") { + const cursorPosition = getCursorPosition(editorRef) + if (cursorPosition === 0) { + setStore("mode", "shell") + closePopover() + event.preventDefault() + return + } + } + + if (event.key === "Escape") { + if (store.popover) { + closePopover() + event.preventDefault() + event.stopPropagation() + return + } + + if (store.mode === "shell") { + setStore("mode", "normal") + event.preventDefault() + event.stopPropagation() + return + } + + if (working()) { + void abort() + event.preventDefault() + event.stopPropagation() + return + } + + if (escBlur()) { + editorRef.blur() + event.preventDefault() + event.stopPropagation() + return + } + } + + if (store.mode === "shell") { + const { collapsed, cursorPosition, textLength } = getCaretState() + if (event.key === "Backspace" && collapsed && cursorPosition === 0 && textLength === 0) { + setStore("mode", "normal") + event.preventDefault() + return + } + } + + // Handle Shift+Enter BEFORE IME check - Shift+Enter is never used for IME input + // and should always insert a newline regardless of composition state + if (event.key === "Enter" && event.shiftKey) { + addPart({ type: "text", content: "\n", start: 0, end: 0 }) + event.preventDefault() + return + } + + if (event.key === "Enter" && isImeComposing(event)) { + return + } + + const ctrl = event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey + + if (store.popover) { + if (event.key === "Tab") { + selectPopoverActive() + event.preventDefault() + return + } + const nav = event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "Enter" + const ctrlNav = ctrl && (event.key === "n" || event.key === "p") + if (nav || ctrlNav) { + if (store.popover === "at") { + atOnKeyDown(event) + event.preventDefault() + return + } + if (store.popover === "slash") { + slashOnKeyDown(event) + if (event.key === "ArrowUp" || event.key === "ArrowDown" || ctrlNav) { + scrollSlashActiveIntoView() + } + } + event.preventDefault() + return + } + } + + if (ctrl && event.code === "KeyG") { + if (store.popover) { + closePopover() + event.preventDefault() + return + } + if (working()) { + void abort() + event.preventDefault() + } + return + } + + if (event.key === "ArrowUp" || event.key === "ArrowDown") { + if (event.altKey || event.ctrlKey || event.metaKey) return + const { collapsed } = getCaretState() + if (!collapsed) return + + const cursorPosition = getCursorPosition(editorRef) + const textContent = prompt + .current() + .map((part) => ("content" in part ? part.content : "")) + .join("") + const direction = event.key === "ArrowUp" ? "up" : "down" + if (!canNavigateHistoryAtCursor(direction, textContent, cursorPosition, store.historyIndex >= 0)) return + if (navigateHistory(direction)) { + event.preventDefault() + } + return + } + + // Note: Shift+Enter is handled earlier, before IME check + if (event.key === "Enter" && !event.shiftKey) { + event.preventDefault() + if (event.repeat) return + if ( + working() && + prompt + .current() + .map((part) => ("content" in part ? part.content : "")) + .join("") + .trim().length === 0 && + imageAttachments().length === 0 && + commentCount() === 0 + ) { + return + } + void handleSubmit(event) + } + } + + const handleSlashMenuKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") { + closePopover() + requestAnimationFrame(() => editorRef.focus()) + event.preventDefault() + return + } + + if (event.key === "Tab") { + selectPopoverActive() + event.preventDefault() + return + } + + const ctrl = event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey + const nav = event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "Enter" + const ctrlNav = ctrl && (event.key === "n" || event.key === "p") + if (!nav && !ctrlNav) return + slashOnKeyDown(event) + if (event.key === "ArrowUp" || event.key === "ArrowDown" || ctrlNav) scrollSlashActiveIntoView() + event.preventDefault() + } + + const agentsLoading = () => props.controls.agents.loading + const agentsShouldFadeIn = createMemo((prev) => prev ?? agentsLoading()) + const providersLoading = () => props.controls.model.loading + const providersShouldFadeIn = createMemo((prev) => prev ?? providersLoading()) + + const [promptReady] = createResource( + () => prompt.ready.promise, + (p) => p, + ) + + const bindEditorRef = (el: HTMLDivElement) => { + editorRef = el + restoreEndOnFocus = true + props.ref?.(el) + } + return ( +
+ {(promptReady(), null)} + (slashPopoverRef = el)} + atFlat={atFlat()} + atActive={atActive() ?? undefined} + atKey={atKey} + setAtActive={setAtActive} + onAtSelect={handleAtSelect} + slashFlat={slashFlat()} + slashActive={slashActive() ?? undefined} + setSlashActive={setSlashActive} + onSlashSelect={handleSlashSelect} + slashMenu={store.slashMenu} + slashMenuQuery={store.slashMenuQuery} + onSlashMenuInput={(value) => { + setStore("slashMenuQuery", value) + slashOnInput(value) + }} + onSlashMenuKeyDown={handleSlashMenuKeyDown} + commandKeybind={command.keybind} + commandKeybindParts={command.keybindParts} + newLayoutDesigns={false} + t={(key) => language.t(key as Parameters[0])} + /> + + + { + const active = comments.active() + return !!item.commentID && item.commentID === active?.id && item.path === active?.file + }} + openComment={openComment} + remove={(item) => { + if (item.commentID) comments.remove(item.path, item.commentID) + prompt.context.remove(item.key) + }} + newLayoutDesigns={false} + t={(key) => language.t(key as Parameters[0])} + /> + + dialog.show(() => ) + } + onRemove={removeAttachment} + removeLabel={language.t("prompt.attachment.remove")} + newLayoutDesigns={false} + /> +
{ + const target = e.target + if (!(target instanceof HTMLElement)) return + if (target.closest('[data-action="prompt-attach"], [data-action="prompt-submit"]')) { + return + } + editorRef?.focus() + }} + > +
(scrollRef = el)} + style={{ "scroll-padding-bottom": space }} + > +
+
+ {placeholder()} +
+
+ + + + + +
+
+
+ + {language.t("prompt.mode.shell")} +
+ +
+
+ +
+ + (x === "default" ? language.t("common.default") : x)} + onSelect={(value) => { + props.controls.model.selection.variant.set(value === "default" ? undefined : value) + restoreFocus() + }} + class="capitalize max-w-[160px] text-text-base" + valueClass="truncate text-13-regular text-text-base" + triggerStyle={control()} + triggerProps={{ "data-action": "prompt-model-variant" }} + variant="ghost" + /> + +
+
+ + +
+
+
+ + +
+ ) +} diff --git a/packages/app-bundle/overlay/packages/app/src/components/prompt-input/attachments.ts b/packages/app-bundle/overlay/packages/app/src/components/prompt-input/attachments.ts new file mode 100644 index 00000000..29059326 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/components/prompt-input/attachments.ts @@ -0,0 +1,226 @@ +import { onMount } from "solid-js" +import { makeEventListener } from "@solid-primitives/event-listener" +import { showToast } from "@/utils/toast" +import { type ContentPart, type ImageAttachmentPart, type usePrompt } from "@/context/prompt" +import { useLanguage } from "@/context/language" +import { usePlatform } from "@/context/platform" +import { uuid } from "@/utils/uuid" +import { getCursorPosition } from "./editor-dom" +import { createBlobReference, type DraftStore } from "@/utils/draft-store" +import { attachmentMime } from "./files" +import { normalizePaste, pasteMode } from "./paste" + +type PromptTarget = Pick["capture"]>, "current" | "cursor" | "set"> +type AttachmentTarget = { prompt: PromptTarget; cursor: number | undefined } + +type PromptAttachmentsCoreInput = { + capture: () => PromptTarget + editor: () => HTMLDivElement | undefined + focusEditor?: () => void + addPart?: (part: ContentPart) => boolean + warn?: () => void + readClipboardImage?: () => Promise + /** Fallback clipboard-text reader for the VS Code webview iframe (see + * clipboard-bridge); resolves "" when unavailable. */ + readClipboardText?: () => Promise + getPathForFile?: (file: File) => string + draftStore?: DraftStore +} + +export type PromptAttachmentsInput = { + prompt: ReturnType + editor: () => HTMLDivElement | undefined + isDialogActive: () => boolean + setDraggingType: (type: "image" | "@mention" | null) => void + focusEditor: () => void + addPart: (part: ContentPart) => boolean + readClipboardImage?: () => Promise + /** Fallback clipboard-text reader for the VS Code webview iframe, where native + * paste delivers no data. Resolves "" when unavailable (see clipboard-bridge). */ + readClipboardText?: () => Promise + getPathForFile?: (file: File) => string +} + +export function createPromptAttachmentsCore(input: PromptAttachmentsCoreInput) { + const capture = (): AttachmentTarget | undefined => { + const prompt = input.capture() + const editor = input.editor() + if (!editor) return + return { prompt, cursor: prompt.cursor() ?? getCursorPosition(editor) } + } + + const add = async (file: File, toast = true, target = capture()) => { + if (!target) return false + const mime = await attachmentMime(file) + if (!mime) { + if (toast) input.warn?.() + return false + } + + const attachment: ImageAttachmentPart = { + type: "image", + id: uuid(), + filename: file.name, + sourcePath: input.getPathForFile?.(file) || undefined, + mime, + blob: input.draftStore ? await input.draftStore.putBlob(file) : await createBlobReference(file), + } + target.prompt.set([...target.prompt.current(), attachment], target.cursor) + return true + } + + const addAttachment = (file: File) => add(file) + + const addAttachments = async (files: File[], toast = true, target = capture()) => { + let found = false + + for (const file of files) { + const ok = await add(file, false, target) + if (ok) found = true + } + + if (!found && files.length > 0 && toast) input.warn?.() + return found + } + + const addClipboardAttachment = async (pending: Promise, target = capture()) => { + const file = await pending + if (!file) return false + return add(file, true, target) + } + + const removeAttachment = (id: string) => { + const target = input.capture() + const current = target.current() + const next = current.filter((part) => part.type !== "image" || part.id !== id) + target.set(next, target.cursor()) + } + + const handlePaste = async (event: ClipboardEvent) => { + const clipboardData = event.clipboardData + if (!clipboardData) return + const target = capture() + if (!target) return + + event.preventDefault() + event.stopPropagation() + + const files = Array.from(clipboardData.items).flatMap((item) => { + if (item.kind !== "file") return [] + const file = item.getAsFile() + return file ? [file] : [] + }) + + if (files.length > 0) { + await addAttachments(files, true, target) + return + } + + let plainText = clipboardData.getData("text/plain") ?? "" + + // Desktop: Browser clipboard has no images and no text, try platform's native clipboard for images + if (input.readClipboardImage && !plainText) { + if (await addClipboardAttachment(input.readClipboardImage(), target)) return + } + + // Amicode webview: a cross-origin iframe inside the VS Code webview gets no + // clipboard data from native paste, so ask the extension host over the + // amicode bridge before giving up (resolves "" outside the webview). + if (!plainText && input.readClipboardText) { + plainText = await input.readClipboardText() + } + + if (!plainText) return + + const text = normalizePaste(plainText) + + const put = () => { + if (input.addPart?.({ type: "text", content: text, start: 0, end: 0 })) return true + input.focusEditor?.() + return input.addPart?.({ type: "text", content: text, start: 0, end: 0 }) ?? false + } + + if (pasteMode(text) === "manual") { + put() + return + } + + const inserted = typeof document.execCommand === "function" && document.execCommand("insertText", false, text) + if (inserted) return + + put() + } + + return { + addAttachment, + addAttachments, + addClipboardAttachment, + removeAttachment, + handlePaste, + } +} + +export function createPromptAttachments(input: PromptAttachmentsInput) { + const language = useLanguage() + const platform = usePlatform() + const attachments = createPromptAttachmentsCore({ + ...input, + draftStore: platform.draftStore, + capture: input.prompt.capture, + warn: () => { + showToast({ + title: language.t("prompt.toast.pasteUnsupported.title"), + description: language.t("prompt.toast.pasteUnsupported.description"), + }) + }, + }) + + const handleGlobalDragOver = (event: DragEvent) => { + if (input.isDialogActive()) return + + event.preventDefault() + const hasFiles = event.dataTransfer?.types.includes("Files") + const hasText = event.dataTransfer?.types.includes("text/plain") + if (hasFiles) { + input.setDraggingType("image") + } else if (hasText) { + input.setDraggingType("@mention") + } + } + + const handleGlobalDragLeave = (event: DragEvent) => { + if (input.isDialogActive()) return + if (!event.relatedTarget) { + input.setDraggingType(null) + } + } + + const handleGlobalDrop = async (event: DragEvent) => { + if (input.isDialogActive()) return + + event.preventDefault() + input.setDraggingType(null) + + const plainText = event.dataTransfer?.getData("text/plain") + const filePrefix = "file:" + if (plainText?.startsWith(filePrefix)) { + const filePath = plainText.slice(filePrefix.length) + input.focusEditor() + input.addPart({ type: "file", path: filePath, content: "@" + filePath, start: 0, end: 0 }) + return + } + + const dropped = event.dataTransfer?.files + if (!dropped) return + + await attachments.addAttachments(Array.from(dropped)) + } + + onMount(() => { + makeEventListener(document, "dragover", handleGlobalDragOver) + makeEventListener(document, "dragleave", handleGlobalDragLeave) + makeEventListener(document, "drop", handleGlobalDrop) + }) + + return attachments +} diff --git a/packages/app-bundle/overlay/packages/app/src/components/prompt-input/clipboard-bridge.test.ts b/packages/app-bundle/overlay/packages/app/src/components/prompt-input/clipboard-bridge.test.ts new file mode 100644 index 00000000..e12d45bd --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/components/prompt-input/clipboard-bridge.test.ts @@ -0,0 +1,124 @@ +import { describe, expect, test } from "bun:test" +import { readClipboardViaBridge, writeClipboardViaBridge } from "./clipboard-bridge" + +type Listener = (event: MessageEvent) => void + +// A stand-in for a framed window (parent !== self) that records outgoing +// clipboard-requests and lets a test play back the host's reply. `readText` +// simulates the host's navigator.clipboard grant: a function to grant it, +// absent to model the VS Code webview iframe (no permission). +function fakeFramedWindow(readText?: () => Promise) { + const listeners = new Set() + const posted: Array> = [] + const win = { + addEventListener: (_type: string, fn: Listener) => listeners.add(fn), + removeEventListener: (_type: string, fn: Listener) => listeners.delete(fn), + navigator: readText ? { clipboard: { readText } } : {}, + parent: { + postMessage: (message: Record) => posted.push(message), + }, + } as unknown as Window + return { + win, + posted, + reply: (message: Record) => listeners.forEach((fn) => fn({ data: message } as MessageEvent)), + listenerCount: () => listeners.size, + } +} + +describe("readClipboardViaBridge", () => { + test("requests the OS clipboard from the host and resolves with the reply", async () => { + const bridge = fakeFramedWindow() + const pending = readClipboardViaBridge(bridge.win) + + expect(bridge.posted).toHaveLength(1) + const request = bridge.posted[0] + expect(request.source).toBe("amicode") + expect(request.kind).toBe("clipboard-request") + expect(typeof request.nonce).toBe("string") + + bridge.reply({ source: "amicode", kind: "clipboard", nonce: request.nonce, text: "solve a CZ gate" }) + + expect(await pending).toBe("solve a CZ gate") + expect(bridge.listenerCount()).toBe(0) // listener cleaned up + }) + + test("ignores replies whose nonce does not match the request", async () => { + const bridge = fakeFramedWindow() + const pending = readClipboardViaBridge(bridge.win, 15) + + // A stale reply from an earlier request must not resolve this one. + bridge.reply({ source: "amicode", kind: "clipboard", nonce: "someone-elses-nonce", text: "leaked" }) + + expect(await pending).toBe("") // falls through to the timeout instead + }) + + test("resolves empty on a malformed reply body", async () => { + const bridge = fakeFramedWindow() + const pending = readClipboardViaBridge(bridge.win, 15) + const request = bridge.posted[0] + + bridge.reply({ source: "amicode", kind: "clipboard", nonce: request.nonce }) // no text field + + expect(await pending).toBe("") + }) + + test("framed host WITHOUT the amicode relay (Simple Browser, plain embeds) falls back to navigator.clipboard", async () => { + // No reply ever comes — the request went into the void. The API key being + // pasted must still arrive via the host's own clipboard-read grant. + const bridge = fakeFramedWindow(() => Promise.resolve("sk-cloud-key-123")) + expect(await readClipboardViaBridge(bridge.win, 15)).toBe("sk-cloud-key-123") + }) + + test("a live bridge reply wins — navigator.clipboard is not consulted", async () => { + const bridge = fakeFramedWindow(() => { + throw new Error("must not be called when the bridge answered") + }) + const pending = readClipboardViaBridge(bridge.win) + const request = bridge.posted[0] + bridge.reply({ source: "amicode", kind: "clipboard", nonce: request.nonce, text: "from the bridge" }) + expect(await pending).toBe("from the bridge") + }) + + test("no relay AND no clipboard permission resolves empty (the caller's no-op stands)", async () => { + const bridge = fakeFramedWindow(() => Promise.reject(new Error("NotAllowedError"))) + expect(await readClipboardViaBridge(bridge.win, 15)).toBe("") + }) + + test("resolves empty without posting when the app is not framed", async () => { + const posted: unknown[] = [] + const win = { + addEventListener: () => {}, + removeEventListener: () => {}, + postMessage: (message: unknown) => posted.push(message), + } as unknown as Window + // parent === self → not inside a webview iframe + ;(win as unknown as { parent: Window }).parent = win + + expect(await readClipboardViaBridge(win)).toBe("") + expect(posted).toHaveLength(0) + }) +}) + +describe("writeClipboardViaBridge", () => { + test("pushes the copied text to the host so the OS clipboard matches ⌘V's read", () => { + const bridge = fakeFramedWindow() + + expect(writeClipboardViaBridge("solve a CZ gate", bridge.win)).toBe(true) + expect(bridge.posted).toHaveLength(1) + const message = bridge.posted[0] + expect(message.source).toBe("amicode") + expect(message.kind).toBe("clipboard-write") + expect(message.text).toBe("solve a CZ gate") + }) + + test("is a no-op that posts nothing when the app is not framed", () => { + const posted: unknown[] = [] + const win = { postMessage: (message: unknown) => posted.push(message) } as unknown as Window + // parent === self → plain web/desktop, where native copy already works + ;(win as unknown as { parent: Window }).parent = win + + expect(writeClipboardViaBridge("ignored", win)).toBe(false) + expect(posted).toHaveLength(0) + }) +}) diff --git a/packages/app-bundle/overlay/packages/app/src/components/prompt-input/clipboard-bridge.ts b/packages/app-bundle/overlay/packages/app/src/components/prompt-input/clipboard-bridge.ts new file mode 100644 index 00000000..54ef6f58 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/components/prompt-input/clipboard-bridge.ts @@ -0,0 +1,128 @@ +// ⌘V inside the Amicode chat: the app runs as a cross-origin iframe inside the +// VS Code webview, where native paste and navigator.clipboard deliver no data +// (the webview parent holds no clipboard-read permission to delegate down). The +// extension host CAN read it, so we ask over the amicode postMessage bridge — +// chat_panel.ts reads vscode.env.clipboard and replies with {kind:"clipboard"}. +// Mirrors the profile-input fallback in @opencode-ai/ui's home-cards. +// +// Resolves "" when unframed (plain web/desktop, where native paste already +// works), on a malformed reply, or after `timeoutMs` with no answer — callers +// treat "" as "nothing to insert", so a missing or dead bridge degrades to a +// no-op rather than a hang. + +const BRIDGE_TIMEOUT_MS = 1500 + +// ⌘C inside the Amicode chat is the mirror image of ⌘V: the sandboxed iframe's +// native copy (and navigator.clipboard.writeText) never reaches the OS +// clipboard, so a subsequent ⌘V — which DOES read the OS clipboard over the +// bridge — pastes whatever stale thing was there before, not what you copied. +// Push the copied text to the extension host, which writes vscode.env.clipboard +// so read and write hit the same clipboard. Fire-and-forget: the host write is +// near-instant and callers have nothing to await. Resolves false (no-op) when +// unframed — native copy already works there. +export function writeClipboardViaBridge(text: string, win: Window = window): boolean { + if (win.parent === win) return false + win.parent.postMessage({ source: "amicode", kind: "clipboard-write", text }, "*") + return true +} + +// The bridge only answers inside the AMICODE webview — its parent runs the +// relay. Every OTHER framed host (VS Code Simple Browser pointed at :3002, any +// plain iframe embed) posts into the void, times out, and — because the ⌘V +// handlers preventDefault before asking — pastes NOTHING, silently. So an +// empty/timed-out bridge reply falls through to navigator.clipboard.readText(): +// dead in the VS Code webview (no permission to lose), but it restores paste in +// every framed host that grants clipboard-read. API-key entry was the reported +// casualty (credential fields are exactly what you paste into an embedded app). +async function readTextDirect(win: Window): Promise { + try { + const text = await win.navigator?.clipboard?.readText?.() + return typeof text === "string" ? text : "" + } catch { + return "" // no permission / no API — the caller's no-op stands + } +} + +export function readClipboardViaBridge(win: Window = window, timeoutMs = BRIDGE_TIMEOUT_MS): Promise { + return new Promise((resolve) => { + // Unframed: native paste works — don't post into the void or wait out the timeout. + if (win.parent === win) { + resolve("") + return + } + + const nonce = Math.random().toString(36).slice(2) + let timer: ReturnType | undefined + + const finish = (text: string) => { + win.removeEventListener("message", onMessage) + if (timer !== undefined) clearTimeout(timer) + if (text) { + resolve(text) + return + } + void readTextDirect(win).then(resolve) + } + + const onMessage = (event: MessageEvent) => { + const data = event.data as { source?: string; kind?: string; nonce?: string; text?: string } | undefined + if (data?.source !== "amicode" || data.kind !== "clipboard" || data.nonce !== nonce) return + finish(typeof data.text === "string" ? data.text : "") + } + + win.addEventListener("message", onMessage) + win.parent.postMessage({ source: "amicode", kind: "clipboard-request", nonce }, "*") + timer = setTimeout(() => finish(""), timeoutMs) + }) +} + +// ⌘V image paste inside the Amicode chat. Unlike text (which the extension host +// reads via vscode.env.clipboard — text-only, no image API), an image can only +// come from the OUTER webview reading navigator.clipboard.read() client-side. +// So we ask the parent relay (chat_panel.ts) rather than the host: it grabs the +// first image/* clipboard item, encodes it as a data URL, and replies. We +// rebuild a File from the reply so callers reuse the normal attachment path. +// Resolves null when unframed, on a text-only clipboard, or after `timeoutMs`. +export function readClipboardImageViaBridge(win: Window = window, timeoutMs = BRIDGE_TIMEOUT_MS): Promise { + return new Promise((resolve) => { + if (win.parent === win) { + resolve(null) + return + } + + const nonce = Math.random().toString(36).slice(2) + let timer: ReturnType | undefined + + const finish = (file: File | null) => { + win.removeEventListener("message", onMessage) + if (timer !== undefined) clearTimeout(timer) + resolve(file) + } + + const onMessage = (event: MessageEvent) => { + const data = event.data as + | { source?: string; kind?: string; nonce?: string; dataUrl?: string; mime?: string; filename?: string } + | undefined + if (data?.source !== "amicode" || data.kind !== "clipboard-image" || data.nonce !== nonce) return + finish(fileFromDataUrl(data.dataUrl, data.mime, data.filename)) + } + + win.addEventListener("message", onMessage) + win.parent.postMessage({ source: "amicode", kind: "clipboard-image-request", nonce }, "*") + timer = setTimeout(() => finish(null), timeoutMs) + }) +} + +function fileFromDataUrl(dataUrl?: string, mime?: string, filename?: string): File | null { + if (!dataUrl || !mime) return null + const comma = dataUrl.indexOf(",") + if (comma === -1) return null + try { + const binary = atob(dataUrl.slice(comma + 1)) + const bytes = new Uint8Array(binary.length) + for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i) + return new File([bytes], filename || "pasted-image.png", { type: mime }) + } catch { + return null + } +} diff --git a/packages/app-bundle/overlay/packages/app/src/components/prompt-input/placeholder.ts b/packages/app-bundle/overlay/packages/app/src/components/prompt-input/placeholder.ts new file mode 100644 index 00000000..d4ffda06 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/components/prompt-input/placeholder.ts @@ -0,0 +1,21 @@ +type PromptPlaceholderInput = { + mode: "normal" | "shell" + commentCount: number + example: string + suggest: boolean + t: (key: string, params?: Record) => string +} + +export function promptPlaceholder(input: PromptPlaceholderInput) { + if (input.mode === "shell") return input.t("prompt.placeholder.shell", { example: input.example }) + if (input.commentCount > 1) return input.t("prompt.placeholder.summarizeComments") + if (input.commentCount === 1) return input.t("prompt.placeholder.summarizeComment") + if (!input.suggest) return input.t("prompt.placeholder.simple") + return input.t("prompt.placeholder.normal", { example: input.example }) +} + +export function promptDesignPlaceholder(mode: PromptPlaceholderInput["mode"], placeholder: string) { + if (mode === "shell") return placeholder + // amicode: brand the composer prompt + return "Ask Amico anything, / for commands, @ for context..." +} diff --git a/packages/app-bundle/overlay/packages/app/src/components/prompt-project-selector.tsx b/packages/app-bundle/overlay/packages/app/src/components/prompt-project-selector.tsx new file mode 100644 index 00000000..fae3ded5 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/components/prompt-project-selector.tsx @@ -0,0 +1,595 @@ +import { + createEffect, + createSignal, + For, + onCleanup, + Show, + splitProps, + type Accessor, + type ComponentProps, +} from "solid-js" +import { createStore } from "solid-js/store" +import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" +import { Icon } from "@opencode-ai/ui/icon" +import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" +import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2" +import { getProjectAvatarVariant } from "@/context/layout" +import { useLanguage } from "@/context/language" +import { displayName, getProjectAvatarSource } from "@/pages/layout/helpers" +import { inAmicode } from "@/pages/session/use-amicode-commands" +import { pathKey } from "@/utils/path-key" +import { handleDocumentSearchKeydown } from "@/utils/search-keydown" +import { createMenuDismissController } from "@/utils/menu-dismiss-controller" + +export type PromptProject = { + name?: string + id?: string + worktree: string + sandboxes?: string[] + icon?: { color?: string; url?: string; override?: string } + server?: { key: string; name: string } +} + +export type PromptProjectControls = { + available: PromptProject[] + directory: string + server?: string + select: (worktree: string, server?: string) => void + add: (title: string, server?: string) => void +} + +const actionPrefix = "action:" +const projectPrefix = "project:" + +function projectKey(project: PromptProject) { + return `${projectPrefix}${encodeURIComponent(project.server?.key ?? "")}:${encodeURIComponent(project.worktree)}` +} + +function actionKey(server?: string) { + return `${actionPrefix}${encodeURIComponent(server ?? "")}` +} + +export function createPromptProjectController(input: { + controls: Accessor + onDone: () => void +}) { + const language = useLanguage() + const [store, setStore] = createStore({ open: false, search: "", active: "" }) + let searchRef: HTMLInputElement | undefined + + const current = () => { + const key = pathKey(input.controls().directory) + return input + .controls() + .available.find( + (project) => + (!project.server || project.server.key === input.controls().server) && + (pathKey(project.worktree) === key || project.sandboxes?.some((sandbox) => pathKey(sandbox) === key)), + ) + } + const selected = () => current() ?? input.controls().available[0] + const projects = () => { + const search = store.search.trim().toLowerCase() + if (!search) return input.controls().available + return input.controls().available.filter((project) => displayName(project).toLowerCase().includes(search)) + } + const servers = () => + input + .controls() + .available.map((project) => project.server) + .filter((server, index, all) => server && all.findIndex((item) => item?.key === server.key) === index) + const keys = () => { + if (servers().length <= 1) { + return [...projects().map(projectKey), actionKey(servers()[0]?.key)] + } + return [ + ...servers().flatMap((server) => + projects() + .filter((project) => project.server?.key === server!.key) + .map(projectKey), + ), + actionKey(), + ] + } + const initialActive = () => { + const selectedKey = selected() ? projectKey(selected()!) : undefined + const options = keys() + if (selectedKey && options.includes(selectedKey)) return selectedKey + return options[0] ?? "" + } + const close = () => { + setStore({ open: false, search: "", active: "" }) + input.onDone() + } + const select = (project: PromptProject) => { + if ( + pathKey(project.worktree) !== pathKey(current()?.worktree ?? "") || + project.server?.key !== current()?.server?.key + ) { + input.controls().select(project.worktree, project.server?.key) + } + close() + } + const add = (server?: string) => { + setStore({ open: false, search: "", active: "" }) + input.controls().add(language.t("command.project.open"), server) + } + const setSearch = (value: string) => { + const search = value.trim().toLowerCase() + const first = input + .controls() + .available.find((project) => !search || displayName(project).toLowerCase().includes(search)) + setStore({ + search: value, + active: first ? projectKey(first) : actionKey(servers().length > 1 ? undefined : servers()[0]?.key), + }) + } + + return { + selected, + empty: () => input.controls().available.length === 0, + projects, + servers, + projectKey, + actionKey, + open: () => store.open, + search: () => store.search, + active: () => store.active, + labels: { + add: () => language.t("session.new.project.add"), + clear: () => language.t("common.clear"), + new: () => language.t("session.new.project.new"), + search: () => language.t("session.new.project.search"), + }, + add, + select, + setOpen(open: boolean) { + if (open) { + setStore({ open: true, active: initialActive() }) + setTimeout(() => requestAnimationFrame(() => searchRef?.focus())) + return + } + setStore({ open: false, search: "", active: "" }) + }, + setSearch, + clearSearch() { + setStore({ search: "", active: initialActive() }) + setTimeout(() => searchRef?.focus()) + }, + setActive(key: string) { + setStore("active", key) + }, + moveActive(delta: number) { + const options = keys() + if (options.length === 0) return + const index = options.indexOf(store.active) + const start = index === -1 ? 0 : index + setStore("active", options[(start + delta + options.length) % options.length]) + }, + activeProject() { + return store.active.startsWith(projectPrefix) + ? projects().find((project) => projectKey(project) === store.active) + : undefined + }, + activeServer() { + return store.active.startsWith(actionPrefix) + ? decodeURIComponent(store.active.slice(actionPrefix.length)) || undefined + : undefined + }, + activeAction() { + return store.active.startsWith(actionPrefix) + }, + setSearchRef(el: HTMLInputElement) { + searchRef = el + }, + focusSearch() { + setTimeout(() => requestAnimationFrame(() => searchRef?.focus())) + }, + handleSearchKeydown(event: KeyboardEvent) { + return handleDocumentSearchKeydown(searchRef, event, store.search, setSearch) + }, + } +} + +export type PromptProjectController = ReturnType + +export function PromptProjectSelector(props: { + controller: PromptProjectController + placement?: "bottom" | "bottom-start" +}) { + const [triggerReady, setTriggerReady] = createSignal(false) + let contentRef: HTMLDivElement | undefined + const dismiss = createMenuDismissController(() => contentRef) + let triggerFrame: number | undefined + + // Floating UI requires a connected anchor; route transitions can construct this trigger before adoption. + const setTriggerRef = (element: HTMLButtonElement) => { + const ready = () => { + if (!element.isConnected) { + triggerFrame = requestAnimationFrame(ready) + return + } + triggerFrame = undefined + setTriggerReady(true) + } + ready() + } + + onCleanup(() => { + if (triggerFrame !== undefined) cancelAnimationFrame(triggerFrame) + }) + + const activeItem = () => + props.controller.active() + ? contentRef?.querySelector(`[data-option-key="${CSS.escape(props.controller.active())}"]`) + : undefined + const selectProject = (project: PromptProject) => { + dismiss.preventTriggerRestore() + props.controller.setOpen(false) + dismiss.afterClose(() => props.controller.select(project)) + } + const selectAction = (server?: string) => { + dismiss.preventTriggerRestore() + props.controller.setOpen(false) + dismiss.afterClose(() => props.controller.add(server)) + } + const selectActive = () => { + const project = props.controller.activeProject() + if (project) { + selectProject(project) + return + } + if (props.controller.activeAction() && props.controller.servers().length > 1) { + const item = activeItem() + item?.focus() + item?.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true })) + return + } + selectAction(props.controller.activeServer()) + } + const moveActive = (delta: number) => { + props.controller.moveActive(delta) + queueMicrotask(() => activeItem()?.scrollIntoView({ block: "nearest" })) + } + const focusPreviousControl = () => { + const target = Array.from( + document.querySelectorAll( + 'button:not([disabled]), a[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])', + ), + ) + .filter((element) => !contentRef?.contains(element) && !element.hasAttribute("data-focus-trap")) + .findLast((element) => element.offsetParent !== null) + dismiss.preventTriggerRestore() + target?.focus() + queueMicrotask(() => { + if (props.controller.open()) props.controller.setOpen(false) + }) + } + const selectedValue = () => { + const project = props.controller.selected() + return project ? props.controller.projectKey(project) : undefined + } + + createEffect(() => { + if (!props.controller.open()) return + const handler = (event: KeyboardEvent) => props.controller.handleSearchKeydown(event) + document.addEventListener("keydown", handler, true) + onCleanup(() => document.removeEventListener("keydown", handler, true)) + }) + + return ( + { + if (open) dismiss.allowTriggerRestore() + props.controller.setOpen(open) + }} + > + + + event.preventDefault()} + onPointerDownOutside={dismiss.preventTriggerRestore} + onFocusOutside={dismiss.preventTriggerRestore} + onCloseAutoFocus={dismiss.onCloseAutoFocus} + > +
+
+ + props.controller.setSearchRef(el)} + value={props.controller.search()} + placeholder={props.controller.labels.search()} + aria-autocomplete="list" + aria-controls="prompt-project-menu" + aria-activedescendant={props.controller.active() || undefined} + class="h-7 min-w-0 flex-1 border-0 bg-transparent text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-base outline-none placeholder:text-v2-text-text-faint" + onInput={(event) => props.controller.setSearch(event.currentTarget.value)} + onKeyDown={(event) => { + if (event.key === "Tab") { + event.preventDefault() + event.stopPropagation() + if (event.shiftKey) { + focusPreviousControl() + return + } + activeItem()?.focus() + return + } + event.stopPropagation() + if (event.key === "Escape") { + event.preventDefault() + props.controller.setOpen(false) + return + } + if (event.altKey || event.metaKey) return + if (event.key === "ArrowDown") { + event.preventDefault() + moveActive(1) + return + } + if (event.key === "ArrowUp") { + event.preventDefault() + moveActive(-1) + return + } + if (event.key === "Enter" && !event.isComposing) { + event.preventDefault() + selectActive() + } + }} + /> + + + +
+
+ 1} + fallback={ + + + {(project) => ( + + )} + + + } + > + + props.controller.projects().some((project) => project.server?.key === server!.key), + )} + > + {(server) => ( +
+
+ {server!.name} +
+ + project.server?.key === server!.key)} + > + {(project) => ( + + )} + + +
+ )} +
+
+
+
+
+
+ 1} + fallback={ + + } + > + + props.controller.setActive(props.controller.actionKey())} + > + + + {props.controller.labels.add()} + + + + + + + {(server) => } + + + + + +
+ + + + ) +} + +export function PromptProjectAddButton(props: { controller: PromptProjectController }) { + return ( + + ) +} + +function ProjectTrigger(props: ComponentProps<"button"> & { controller: PromptProjectController }) { + const [local, rest] = splitProps(props, ["controller", "class", "classList", "onClick", "onKeyDown"]) + const project = () => local.controller.selected() + return ( + + ) +} + +function ProjectItem(props: { + project: PromptProject + controller: PromptProjectController + onSelect: (project: PromptProject) => void +}) { + const key = () => props.controller.projectKey(props.project) + return ( + { + props.controller.setActive(key()) + props.controller.focusSearch() + }} + onSelect={() => props.onSelect(props.project)} + > + + {displayName(props.project)} + + + + + ) +} + +const projectActionClass = + "h-7 gap-2 rounded-sm px-3 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-base [font-family:var(--v2-font-family-sans)] data-[highlighted]:!bg-v2-overlay-simple-overlay-hover" + +function ProjectAction(props: { + server?: string + controller: PromptProjectController + onSelect: (server?: string) => void +}) { + const key = () => props.controller.actionKey(props.server) + return ( + { + props.controller.setActive(key()) + props.controller.focusSearch() + }} + onSelect={() => props.onSelect(props.server)} + > + + + {props.controller.labels.add()} + + + ) +} + +function ServerAction(props: { server: { key: string; name: string }; onSelect: (server: string) => void }) { + return ( + props.onSelect(props.server.key)}> + {props.server.name} + + ) +} diff --git a/packages/app-bundle/overlay/packages/app/src/components/report-bug-button.css b/packages/app-bundle/overlay/packages/app/src/components/report-bug-button.css new file mode 100644 index 00000000..2521fd3e --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/components/report-bug-button.css @@ -0,0 +1,49 @@ +/* amicode/opencode#116: report-a-bug — the danger-role state contract. + Color comes ONLY from the semantic danger token (red-800 light / red-500 + dark, defined in packages/ui/src/v2/styles/theme.css) — never a literal + hex, and yellow stays the sole accent. Muted at rest → full on hover, the + provider-icon opacity pattern. Every selector out-specifies (or follows) + icon-button-v2.css's variant rules so the five states hold in both themes; + forced states ([data-state]) exist so the story can render the matrix. */ + +[data-component="icon-button-v2"][data-action="report-bug"] { + border-radius: var(--radius-md); /* control radius token (design-polish.css) */ + color: var(--v2-state-fg-danger); + opacity: 0.55; + cursor: pointer; + transition: opacity 0.16s ease; +} + +[data-component="icon-button-v2"][data-action="report-bug"]:is(:hover, [data-state="hover"]):not(:disabled) { + color: var(--v2-state-fg-danger); + opacity: 1; + background-color: var(--v2-overlay-simple-overlay-hover); +} + +[data-component="icon-button-v2"][data-action="report-bug"]:is(:active, [data-state="pressed"]):not(:disabled) { + color: var(--v2-state-fg-danger); + opacity: 1; + background-color: var(--v2-overlay-simple-overlay-pressed); +} + +/* The visible focus ring itself comes from icon-button-v2.css + (2px var(--v2-border-border-focus)) — here the glyph just goes full-strength. */ +[data-component="icon-button-v2"][data-action="report-bug"]:is(:focus-visible, [data-state="focus"]):not( + :disabled + ) { + color: var(--v2-state-fg-danger); + opacity: 1; +} + +[data-component="icon-button-v2"][data-action="report-bug"]:is(:disabled, [data-state="disabled"]) { + /* clearly deader than the muted rest state (0.55), not just a shade off it */ + color: var(--v2-state-fg-danger); + opacity: 0.3; + cursor: not-allowed; +} + +@media (prefers-reduced-motion: reduce) { + [data-component="icon-button-v2"][data-action="report-bug"] { + transition: none; + } +} diff --git a/packages/app-bundle/overlay/packages/app/src/components/report-bug-button.stories.tsx b/packages/app-bundle/overlay/packages/app/src/components/report-bug-button.stories.tsx new file mode 100644 index 00000000..b838e92e --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/components/report-bug-button.stories.tsx @@ -0,0 +1,151 @@ +import { For } from "solid-js" +import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" +import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" +import { PromptInputV2SubmitButton } from "@opencode-ai/session-ui/v2/prompt-input" +import { oc2Theme, resolveThemeVariantV2 } from "@opencode-ai/ui/theme" +import { ReportBugButton } from "@/components/report-bug-button" +// The app entry loads design-polish.css globally (it owns --radius-md, which +// the button's css consumes); stories must pull it themselves. +import "@/design-polish.css" + +// amicode/opencode#116: the report-a-bug button for the v2 composer. The +// interaction-state contract (default / hover / active / focus-visible / +// disabled) is visual and lives here — the app has no component-render unit +// test surface (Solid needs its compile-time transform), per the issue's +// Testing Decisions. Gate rendering and click behavior are unit-tested in +// packages/app/src/utils/amicode-bug-report.test.ts and +// packages/app/src/pages/session/composer/report-bug.test.ts. + +/** The real oc-2 --v2-* token set for one color scheme, applied as inline + * custom properties — the SAME resolver the app runs at boot, so each pane + * re-resolves the danger token (red-800 light / red-500 dark) regardless of + * the Storybook theme toolbar. (The naive data-color-scheme wrapper is a + * no-op inside Storybook — see amico-wave.stories.tsx for the archaeology.) */ +function schemeVars(scheme: "light" | "dark"): Record { + const isDark = scheme === "dark" + const tokens = resolveThemeVariantV2(isDark ? oc2Theme.dark : oc2Theme.light, isDark) + const vars: Record = {} + for (const [key, value] of Object.entries(tokens)) vars[`--${key}`] = value + return vars +} + +const STATES: { label: string; props: { state?: "hover" | "pressed" | "focus"; disabled?: boolean } }[] = [ + { label: "default", props: {} }, + { label: "hover", props: { state: "hover" } }, + { label: "active", props: { state: "pressed" } }, + { label: "focus-visible", props: { state: "focus" } }, + { label: "disabled", props: { disabled: true } }, +] + +function StateMatrix() { + return ( +
+ + {(item) => ( +
+ + {item.label} +
+ )} +
+
+ ) +} + +/** The session-ui composer's bottom row, faithfully mirrored from + * packages/session-ui/src/v2/components/prompt-input/index.tsx: same row + * classes, same trailingControl slot wrapper (mr-1 = the 4px grid), and the + * REAL send button — the bug glyph must read as its immediate left neighbor. */ +function ComposerRowMock() { + return ( +
+
+
+ } + aria-label="Add images and files" + /> +
+ {/* the session-ui trailingControl slot renders exactly this wrapper */} +
+ +
+ {}} + onStop={() => {}} + /> +
+
+ ) +} + +export default { + title: "App/ReportBugButton", + id: "app-report-bug-button", + component: ReportBugButton, + parameters: { + docs: { + description: { + component: `### Overview +Report-a-bug button for the v2 composer's bottom control row (amicode/opencode#116). + +- Renders only when the extension passes the \`amicode_bug_report=1\` boot param — the gate lives in \`PromptInputV2Composer\`, which passes \`undefined\` to the composer's \`trailingControl\` slot when off, so the row's layout never shifts. +- Click posts \`amicode.reportBug\` over the app↔extension bridge — or reveals/re-expands the open bug dock (slice #117) via the \`bugDock\` seam, posting nothing. +- Color is the semantic danger token (\`--v2-state-fg-danger\`: red-800 light / red-500 dark) — muted at rest, full on hover. Yellow stays the only accent. + +### States +default · hover · active · focus-visible (2px \`--v2-border-border-focus\` ring) · disabled — forced via the \`state\` prop / the real \`disabled\` attribute below.`, + }, + }, + }, +} + +export const InComposerRow = { + render: () => ( +
+ + Right-anchored, immediately left of the send button — 28×28, 16px glyph, 4px from send. + + +
+ ), +} + +export const States = { + render: () => , +} + +export const Schemes = { + render: () => ( +
+ + {(scheme) => ( +
+ + {scheme} + + +
+ )} +
+
+ ), +} diff --git a/packages/app-bundle/overlay/packages/app/src/components/report-bug-button.tsx b/packages/app-bundle/overlay/packages/app/src/components/report-bug-button.tsx new file mode 100644 index 00000000..ce92a01c --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/components/report-bug-button.tsx @@ -0,0 +1,29 @@ +// amicode/opencode#116: the report-a-bug button for the v2 composer's bottom +// control row. Renders only when the amicode_bug_report=1 boot param is set — +// the CALLER (PromptInputV2Composer) owns that gate and passes `undefined` to +// the composer's trailingControl slot instead, so a gated-off button never +// shifts the row's layout. Icon-only: the aria-label + tooltip carry the +// meaning — color is never the only signal. +import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" +import { IconButtonV2, type IconButtonV2Props } from "@opencode-ai/ui/v2/icon-button-v2" +import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" +import { reportBug } from "@/pages/session/composer/report-bug" +import "./report-bug-button.css" + +export function ReportBugButton(props: { disabled?: boolean; /** stories only: force a visual state */ state?: IconButtonV2Props["state"] }) { + return ( + + } + aria-label="Report a bug" + disabled={props.disabled} + onClick={() => reportBug()} + /> + + ) +} diff --git a/packages/app-bundle/overlay/packages/app/src/components/session-context-usage.tsx b/packages/app-bundle/overlay/packages/app/src/components/session-context-usage.tsx new file mode 100644 index 00000000..a297bf22 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/components/session-context-usage.tsx @@ -0,0 +1,161 @@ +import { Match, Show, Switch, createMemo, type ComponentProps, type JSX } from "solid-js" +import { ProgressCircle } from "@opencode-ai/ui/progress-circle" +import { ProgressCircleV2 } from "@opencode-ai/ui/v2/progress-circle-v2" +import { Button } from "@opencode-ai/ui/button" +import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" +import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" + +import { useLayout } from "@/context/layout" +import { useSync } from "@/context/sync" +import { useLanguage } from "@/context/language" +import { useProviders } from "@/hooks/use-providers" +import { useSDK } from "@/context/sdk" +import { getSessionContext } from "@/components/session/session-context-metrics" +import { useSessionLayout } from "@/pages/session/session-layout" + +interface SessionContextUsageProps { + variant?: "button" | "indicator" + buttonAppearance?: "default" | "v2" + placement?: ComponentProps["placement"] +} + +function ContextTooltipRow(props: { name: JSX.Element; value: JSX.Element }) { + return ( +
+ {props.name} + {props.value} +
+ ) +} + +function openSessionContext(args: { + view: ReturnType["view"]> + layout: ReturnType + tabs: ReturnType["tabs"]> +}) { + args.view.reviewPanel.open(args.view.reviewPanel.opened() ? "other" : "context-button") + if (args.layout.fileTree.opened() && args.layout.fileTree.tab() !== "all") args.layout.fileTree.setTab("all") + void args.tabs.open("context") + args.tabs.setActive("context") +} + +export function SessionContextUsage(props: SessionContextUsageProps) { + const sync = useSync() + const layout = useLayout() + const language = useLanguage() + const sdk = useSDK() + const providers = useProviders(() => sdk().directory) + const { params, tabs, view } = useSessionLayout() + + const variant = createMemo(() => props.variant ?? "button") + const buttonAppearance = createMemo(() => props.buttonAppearance ?? "default") + + const messages = createMemo(() => (params.id ? (sync().data.message[params.id] ?? []) : [])) + const info = createMemo(() => (params.id ? sync().session.get(params.id) : undefined)) + + const usd = createMemo( + () => + new Intl.NumberFormat(language.intl(), { + style: "currency", + currency: "USD", + }), + ) + + const context = createMemo(() => getSessionContext(messages(), [...providers.all().values()])) + const cost = createMemo(() => { + return usd().format(info()?.cost ?? 0) + }) + const contextVisible = createMemo(() => view().reviewPanel.opened() && tabs().active() === "context") + const hasOtherTabs = createMemo(() => + tabs() + .all() + .some((tab) => tab !== "context" && tab !== "review"), + ) + + const openContext = () => { + if (!params.id) return + + const sessionView = view() + if (contextVisible()) { + tabs().close("context") + if (sessionView.reviewPanel.source() === "context-button" && !hasOtherTabs()) sessionView.reviewPanel.close() + return + } + + openSessionContext({ + view: sessionView, + layout, + tabs: tabs(), + }) + } + + const usageColor = createMemo(() => { + const u = context()?.usage ?? 0 + if (u >= 90) return "var(--v2-text-text-danger, #ef4444)" + if (u >= 75) return "var(--v2-text-text-warning, #f59e0b)" + return "var(--v2-icon-icon-base, var(--icon-base))" + }) + const circle = () => ( +
+ +
+ ) + const circleV2 = () => ( +
+ +
+ ) + + const tooltipValue = () => ( +
+ + + +
+ ) + + return ( + + + + {circle()} + + + + + + + + + + ) +} diff --git a/packages/app-bundle/overlay/packages/app/src/components/session/context-warning-banner.tsx b/packages/app-bundle/overlay/packages/app/src/components/session/context-warning-banner.tsx new file mode 100644 index 00000000..8535cfac --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/components/session/context-warning-banner.tsx @@ -0,0 +1,51 @@ +import { Show, createEffect, on } from "solid-js" +import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" +import { useCommand } from "@/context/command" +import { useLanguage } from "@/context/language" +import { showToast } from "@/utils/toast" +import { useContextWarning } from "./use-context-warning" + +export function ContextWarningBanner() { + const language = useLanguage() + const command = useCommand() + const warning = useContextWarning() + + // toasts once per level per session + createEffect( + on( + () => warning.level(), + (level) => { + if (level === "warn" && warning.store.lastNotifiedWarn === 0) { + warning.setStore("lastNotifiedWarn", Date.now()) + showToast({ variant: "default", title: language.t("context.warning.toast.warn.title"), description: language.t("context.warning.toast.warn.description") }) + } + if (level === "critical" && warning.store.lastNotifiedCritical === 0) { + warning.setStore("lastNotifiedCritical", Date.now()) + showToast({ variant: "error", title: language.t("context.warning.toast.critical.title"), description: language.t("context.warning.toast.critical.description") }) + } + }, + ), + ) + + const compact = () => command.trigger("session.compact", "palette") + + return ( + <> + +
+ {language.t("context.warning.banner.warn", { percent: warning.usage() })} + + {language.t("command.session.compact")} + × + +
+
+ + + + + ) +} diff --git a/packages/app-bundle/overlay/packages/app/src/components/session/index.ts b/packages/app-bundle/overlay/packages/app/src/components/session/index.ts new file mode 100644 index 00000000..92b19497 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/components/session/index.ts @@ -0,0 +1,8 @@ +export { SessionHeader } from "./session-header" +export { SessionContextTab } from "./session-context-tab" +export { SessionPreviewTab } from "./session-preview-tab" +export { PanelMenu } from "./panel-menu" +export { SortableTab, FileVisual } from "./session-sortable-tab" +export { SortableTabV2 } from "./session-sortable-tab-v2" +export { SortableTerminalTab } from "./session-sortable-terminal-tab" +export { NewSessionView } from "./session-new-view" diff --git a/packages/app-bundle/overlay/packages/app/src/components/session/panel-menu.tsx b/packages/app-bundle/overlay/packages/app/src/components/session/panel-menu.tsx new file mode 100644 index 00000000..f597b541 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/components/session/panel-menu.tsx @@ -0,0 +1,55 @@ +import { For, Show } from "solid-js" +import { Icon } from "@opencode-ai/ui/icon" +import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" + +// ─── Types ────────────────────────────────────────────────────────────────── + +export interface PanelMenuItem { + id: string + label: string + icon: string + available: () => boolean + active?: () => boolean +} + +// ─── Component ────────────────────────────────────────────────────────────── + +export function PanelMenu(props: { + items: PanelMenuItem[] + onSelect: (id: string) => void + v2?: boolean +}) { + return ( + + + + + + + item.available())}> + {(item) => ( + props.onSelect(item.id)} + > + + {item.label} + + + + + )} + + + + + ) +} diff --git a/packages/app-bundle/overlay/packages/app/src/components/session/session-chats-dropdown.test.ts b/packages/app-bundle/overlay/packages/app/src/components/session/session-chats-dropdown.test.ts new file mode 100644 index 00000000..0ac82300 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/components/session/session-chats-dropdown.test.ts @@ -0,0 +1,235 @@ +import { describe, expect, test } from "bun:test" + +/** + * Tests for the Session Chats Dropdown logic (amicode#274). + * Validates session sorting, search filtering, open/close state, and + * tab-status derivation — the same behavior as the dashboard sessions + * flyout (amicode#273). + */ + +type Session = { + id: string + title?: string + directory: string + parentID?: string + time: { created: number; updated?: number; archived?: number | null } +} + +// --- Helpers under test (pure logic extracted from the component) --- + +/** Sort sessions: open-tab sessions first, preserving relative order within each group. */ +function sortSessionsByOpenTab( + sessions: Session[], + hasOpenTab: (session: Session) => boolean, +): Session[] { + const open: Session[] = [] + const rest: Session[] = [] + for (const session of sessions) { + if (hasOpenTab(session)) { + open.push(session) + } else { + rest.push(session) + } + } + return [...open, ...rest] +} + +/** Filter sessions by search query (matches title or id). */ +function filterSessionsByQuery( + sessions: Session[], + query: string, + getTitle: (session: Session) => string, +): Session[] { + const q = query.trim().toLowerCase() + if (!q) return sessions + return sessions.filter((session) => getTitle(session).toLowerCase().includes(q)) +} + +/** + * Simulates the flyout open/close state machine — the same pattern used + * in the component. Verifies the toggle works and the dismiss handler + * (outside click / Escape) doesn't race with the opening click. + */ +function createFlyoutState() { + let open = false + let dismissListener: ((e: { target: unknown }) => void) | null = null + const flyoutRoot = { contains: (target: unknown) => target === "inside" } + + return { + get open() { return open }, + toggle() { open = !open }, + /** Simulates the deferred dismiss listener attachment (setTimeout(0)) */ + attachDismissListener() { + if (!open) { dismissListener = null; return } + dismissListener = (e) => { + if (!flyoutRoot.contains(e.target)) open = false + } + }, + /** Simulates a mousedown event after the listener is attached */ + simulateOutsideClick() { dismissListener?.({ target: "outside" }) }, + simulateInsideClick() { dismissListener?.({ target: "inside" }) }, + } +} + +// --- Tests --- + +describe("Session Chats Dropdown", () => { + const sessions: Session[] = [ + { id: "ses_1", title: "X gate optimization", directory: "/proj", time: { created: 100 } }, + { id: "ses_2", title: "CZ gate", directory: "/proj", time: { created: 200 } }, + { id: "ses_3", title: "Cat state prep", directory: "/proj", time: { created: 300 } }, + ] + + describe("open/close state", () => { + test("toggle opens the flyout", () => { + const state = createFlyoutState() + expect(state.open).toBe(false) + state.toggle() + expect(state.open).toBe(true) + }) + + test("toggle closes when already open", () => { + const state = createFlyoutState() + state.toggle() // open + state.toggle() // close + expect(state.open).toBe(false) + }) + + test("dismiss listener does NOT fire before attachment (deferred)", () => { + const state = createFlyoutState() + state.toggle() // open + // Before attachDismissListener, an outside click should NOT close + state.simulateOutsideClick() + expect(state.open).toBe(true) + }) + + test("dismiss listener closes on outside click after attachment", () => { + const state = createFlyoutState() + state.toggle() // open + state.attachDismissListener() // simulates the setTimeout(0) firing + state.simulateOutsideClick() + expect(state.open).toBe(false) + }) + + test("dismiss listener does NOT close on inside click", () => { + const state = createFlyoutState() + state.toggle() // open + state.attachDismissListener() + state.simulateInsideClick() + expect(state.open).toBe(true) + }) + }) + + describe("sortSessionsByOpenTab", () => { + test("moves sessions with open tabs to the front", () => { + const openIds = new Set(["ses_2"]) + const sorted = sortSessionsByOpenTab(sessions, (s) => openIds.has(s.id)) + + expect(sorted[0].id).toBe("ses_2") + expect(sorted.slice(1).map((s) => s.id)).toEqual(["ses_1", "ses_3"]) + }) + + test("preserves order when no sessions have open tabs", () => { + const sorted = sortSessionsByOpenTab(sessions, () => false) + expect(sorted.map((s) => s.id)).toEqual(["ses_1", "ses_2", "ses_3"]) + }) + + test("preserves order when all sessions have open tabs", () => { + const sorted = sortSessionsByOpenTab(sessions, () => true) + expect(sorted.map((s) => s.id)).toEqual(["ses_1", "ses_2", "ses_3"]) + }) + }) + + describe("filterSessionsByQuery", () => { + const getTitle = (s: Session) => s.title || s.id + + test("returns all sessions when query is empty", () => { + const result = filterSessionsByQuery(sessions, "", getTitle) + expect(result).toHaveLength(3) + }) + + test("filters by title substring (case-insensitive)", () => { + const result = filterSessionsByQuery(sessions, "gate", getTitle) + expect(result.map((s) => s.id)).toEqual(["ses_1", "ses_2"]) + }) + + test("filters by session id when title is missing", () => { + const noTitleSessions: Session[] = [ + { id: "ses_abc", directory: "/proj", time: { created: 100 } }, + { id: "ses_xyz", directory: "/proj", time: { created: 200 } }, + ] + const result = filterSessionsByQuery(noTitleSessions, "abc", getTitle) + expect(result).toHaveLength(1) + expect(result[0].id).toBe("ses_abc") + }) + + test("returns empty when nothing matches", () => { + const result = filterSessionsByQuery(sessions, "nonexistent", getTitle) + expect(result).toHaveLength(0) + }) + + test("trims whitespace from query", () => { + const result = filterSessionsByQuery(sessions, " cat ", getTitle) + expect(result).toHaveLength(1) + expect(result[0].id).toBe("ses_3") + }) + }) + + describe("openSession navigation strategy", () => { + /** + * Opening a session must use direct navigate() — NOT tabs.openPath/addSessionTab. + * tabs.addSessionTab wraps in startTransition, which keeps both old and new UI + * mounted during the transition, causing Portal-rendered buttons to duplicate. + * + * The correct strategy: + * - Session with existing tab → select the tab (no navigation, no transition) + * - Session without a tab → navigate directly (the session page creates its own tab) + * - In both cases, close the flyout BEFORE any navigation + */ + type NavigationAction = + | { type: "select-tab"; sessionId: string } + | { type: "navigate"; path: string } + + function resolveOpenAction( + session: Session, + hasExistingTab: boolean, + encodePath: (dir: string, id: string) => string, + ): NavigationAction { + if (hasExistingTab) { + return { type: "select-tab", sessionId: session.id } + } + return { type: "navigate", path: encodePath(session.directory, session.id) } + } + + const encodePath = (dir: string, id: string) => `/${btoa(dir)}/session/${id}` + + test("session with existing tab → select-tab (no startTransition)", () => { + const session = sessions[0] + const action = resolveOpenAction(session, true, encodePath) + + expect(action.type).toBe("select-tab") + expect((action as { type: "select-tab"; sessionId: string }).sessionId).toBe("ses_1") + }) + + test("session without existing tab → navigate directly (avoids startTransition)", () => { + const session = sessions[0] + const action = resolveOpenAction(session, false, encodePath) + + expect(action.type).toBe("navigate") + expect((action as { type: "navigate"; path: string }).path).toBe(`/${btoa("/proj")}/session/ses_1`) + }) + + test("flyout closes before navigation (prevents portal duplication)", () => { + const state = createFlyoutState() + state.toggle() // open + expect(state.open).toBe(true) + + // Simulate openSession: close first, then navigate + state.toggle() // close + const action = resolveOpenAction(sessions[0], false, encodePath) + + expect(state.open).toBe(false) // flyout closed BEFORE navigation + expect(action.type).toBe("navigate") // would navigate after close + }) + }) +}) diff --git a/packages/app-bundle/overlay/packages/app/src/components/session/session-context-tab.tsx b/packages/app-bundle/overlay/packages/app/src/components/session/session-context-tab.tsx new file mode 100644 index 00000000..6d12fef5 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/components/session/session-context-tab.tsx @@ -0,0 +1,611 @@ +import { createEffect, createMemo, createResource, createSignal, on, onCleanup, onMount, For, Show } from "solid-js" +import type { JSX } from "solid-js" +import { useSync } from "@/context/sync" +import { checksum } from "@opencode-ai/core/util/encode" +import { findLast } from "@opencode-ai/core/util/array" +import { same } from "@/utils/same" +import { Icon } from "@opencode-ai/ui/icon" +import { IconButton } from "@opencode-ai/ui/icon-button" +import { Accordion } from "@opencode-ai/ui/accordion" +import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header" +import { File } from "@opencode-ai/session-ui/file" +import { Markdown } from "@opencode-ai/session-ui/markdown" +import { ScrollView } from "@opencode-ai/ui/scroll-view" +import type { Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client" +import { useLanguage } from "@/context/language" +import { useProviders } from "@/hooks/use-providers" +import { useSDK } from "@/context/sdk" +import { useServer } from "@/context/server" +import { useFile } from "@/context/file" +import { useSessionLayout } from "@/pages/session/session-layout" +import { amicodeGet } from "@/utils/amicode-fetch" +import { amicoBrainRef } from "@opencode-ai/ui/brain-ref" +import { + createContextTreeEngine, + contextTreeKindColor, + type ContextTreeEngine, + type ContextTreeKind, + type ContextTreeNodeInput, + type ContextTreeScheme, + type ContextTreeSelection, +} from "@opencode-ai/ui/context-tree-engine" +import { buildContextTree, vaultRefFromPath, type ContextTurn } from "@opencode-ai/ui/context-tree-data" +import { vaultPanel } from "@/context/vault-panel" +import { createOpenSessionFileTab } from "@/pages/session/helpers" +import { getSessionContext } from "./session-context-metrics" +import { estimateSessionContextBreakdown, type SessionContextBreakdownKey } from "./session-context-breakdown" +import { createSessionContextFormatter } from "./session-context-format" + +const BREAKDOWN_COLOR: Record = { + system: "var(--syntax-info)", + user: "var(--syntax-success)", + assistant: "var(--syntax-property)", + tool: "var(--syntax-warning)", + other: "var(--syntax-comment)", +} + +/** Gate for the graph section: ≥1 committed (non-consider) brain ref in the session. */ +export function sessionHasContextItems( + messages: readonly { id: string; role?: string }[], + partsFor: (messageID: string) => readonly { type?: string; tool?: string; state?: { input?: unknown } }[], +): boolean { + for (const m of messages) { + if (m.role !== "assistant") continue + for (const p of partsFor(m.id) ?? []) { + if (p?.type !== "tool" || typeof p.tool !== "string") continue + const ref = amicoBrainRef(p.tool, (p.state?.input as Record) ?? {}) + if (ref && !ref.consider) return true + } + } + return false +} + +const LEGEND: { kind: ContextTreeKind; label: string }[] = [ + { kind: "note", label: "notes" }, + { kind: "source", label: "source" }, + { kind: "skill", label: "skills" }, + { kind: "agent", label: "agents" }, +] + +function Stat(props: { label: string; value: JSX.Element }) { + return ( +
+
{props.label}
+
{props.value}
+
+ ) +} + +function RawMessageContent(props: { message: Message; getParts: (id: string) => Part[]; onRendered: () => void }) { + const file = createMemo(() => { + const parts = props.getParts(props.message.id) + const contents = JSON.stringify({ message: props.message, parts }, null, 2) + return { + name: `${props.message.role}-${props.message.id}.json`, + contents, + cacheKey: checksum(contents), + } + }) + + return ( + requestAnimationFrame(props.onRendered)} + /> + ) +} + +function RawMessage(props: { + message: Message + getParts: (id: string) => Part[] + onRendered: () => void + time: (value: number | undefined) => string +}) { + return ( + + + +
+
+ {props.message.role} • {props.message.id} +
+
+
{props.time(props.message.time.created)}
+ +
+
+
+
+ +
+ +
+
+
+ ) +} + +const emptyMessages: Message[] = [] +const emptyUserMessages: UserMessage[] = [] + +export function SessionContextTab() { + const sync = useSync() + const language = useLanguage() + const sdk = useSDK() + const server = useServer() + const file = useFile() + const providers = useProviders(() => sdk().directory) + const { params, tabs, view } = useSessionLayout() + + const info = createMemo(() => (params.id ? sync().session.get(params.id) : undefined)) + + const messages = createMemo( + () => { + const id = params.id + if (!id) return emptyMessages + return (sync().data.message[id] ?? []) as Message[] + }, + emptyMessages, + { equals: same }, + ) + + const getParts = (id: string) => (sync().data.part[id] ?? []) as Part[] + + const userMessages = createMemo( + () => messages().filter((m) => m.role === "user") as UserMessage[], + emptyUserMessages, + { equals: same }, + ) + + const visibleUserMessages = createMemo( + () => { + const revert = info()?.revert?.messageID + if (!revert) return userMessages() + return userMessages().filter((m) => m.id < revert) + }, + emptyUserMessages, + { equals: same }, + ) + + const usd = createMemo( + () => + new Intl.NumberFormat(language.intl(), { + style: "currency", + currency: "USD", + }), + ) + + const ctx = createMemo(() => getSessionContext(messages(), [...providers.all().values()])) + const formatter = createMemo(() => createSessionContextFormatter(language.intl())) + + const cost = createMemo(() => { + return usd().format(info()?.cost ?? 0) + }) + + const counts = createMemo(() => { + const all = messages() + const user = all.reduce((count, x) => count + (x.role === "user" ? 1 : 0), 0) + const assistant = all.reduce((count, x) => count + (x.role === "assistant" ? 1 : 0), 0) + return { + all: all.length, + user, + assistant, + } + }) + + const systemPrompt = createMemo(() => { + const msg = findLast(visibleUserMessages(), (m) => !!m.system) + const system = msg?.system + if (!system) return + const trimmed = system.trim() + if (!trimmed) return + return trimmed + }) + + const providerLabel = createMemo(() => { + const c = ctx() + if (!c) return "—" + return c.providerLabel + }) + + const modelLabel = createMemo(() => { + const c = ctx() + if (!c) return "—" + return c.modelLabel + }) + + const breakdown = createMemo( + on( + () => [ctx()?.message.id, ctx()?.input, messages().length, systemPrompt()], + () => { + const c = ctx() + if (!c?.input) return [] + return estimateSessionContextBreakdown({ + messages: messages(), + parts: sync().data.part as Record, + input: c.input, + systemPrompt: systemPrompt(), + }) + }, + ), + ) + + const breakdownLabel = (key: SessionContextBreakdownKey) => { + if (key === "system") return language.t("context.breakdown.system") + if (key === "user") return language.t("context.breakdown.user") + if (key === "assistant") return language.t("context.breakdown.assistant") + if (key === "tool") return language.t("context.breakdown.tool") + return language.t("context.breakdown.other") + } + + const stats = [ + { label: "context.stats.session", value: () => info()?.title ?? params.id ?? "—" }, + { label: "context.stats.messages", value: () => counts().all.toLocaleString(language.intl()) }, + { label: "context.stats.provider", value: providerLabel }, + { label: "context.stats.model", value: modelLabel }, + { label: "context.stats.limit", value: () => formatter().number(ctx()?.limit) }, + { label: "context.stats.totalTokens", value: () => formatter().number(ctx()?.total) }, + { label: "context.stats.usage", value: () => formatter().percent(ctx()?.usage) }, + { label: "context.stats.inputTokens", value: () => formatter().number(ctx()?.input) }, + { label: "context.stats.outputTokens", value: () => formatter().number(ctx()?.message.tokens.output) }, + { label: "context.stats.reasoningTokens", value: () => formatter().number(ctx()?.message.tokens.reasoning) }, + { + label: "context.stats.cacheTokens", + value: () => + `${formatter().number(ctx()?.message.tokens.cache.read)} / ${formatter().number(ctx()?.message.tokens.cache.write)}`, + }, + { label: "context.stats.userMessages", value: () => counts().user.toLocaleString(language.intl()) }, + { label: "context.stats.assistantMessages", value: () => counts().assistant.toLocaleString(language.intl()) }, + { label: "context.stats.totalCost", value: cost }, + { label: "context.stats.sessionCreated", value: () => formatter().time(info()?.time.created) }, + { label: "context.stats.lastActivity", value: () => formatter().time(ctx()?.message.time.created) }, + ] satisfies { label: string; value: () => JSX.Element }[] + + // ─── Context tree (relocated from the top panel per ADR 0004) ──────────── + // per-mount browsability from GET /amicode/vaults + const [vaultsRaw] = createResource( + () => server.current, + (conn) => amicodeGet(conn, "/amicode/vaults").catch(() => undefined), + ) + const browsableMounts = createMemo | undefined>(() => { + const raw = vaultsRaw() as { mounts?: { id?: string; browsable?: boolean }[] } | undefined + if (!raw || !Array.isArray(raw.mounts)) return undefined + return new Map( + raw.mounts.filter((m) => typeof m?.id === "string").map((m) => [m.id as string, m.browsable]), + ) + }) + const vaultLocked = (mount: string) => { + const map = browsableMounts() + if (!map) return false + if (!map.has(mount)) return true + return map.get(mount) === false + } + + const busy = createMemo(() => (sync().data.session_status[params.id ?? ""]?.type ?? "idle") !== "idle") + + const turns = createMemo(() => { + const byPrompt = new Map() + const out: ContextTurn[] = [] + for (const m of messages()) { + if (m.role !== "assistant") continue + const key = (m as { parentID?: string }).parentID ?? m.id + let turn = byPrompt.get(key) + if (!turn) { + turn = { id: key, refs: [], busy: false } + byPrompt.set(key, turn) + out.push(turn) + } + for (const p of getParts(m.id)) { + if (p.type !== "tool") continue + const ref = amicoBrainRef((p as { tool: string }).tool, ((p as { state?: { input?: unknown } }).state?.input ?? {}) as Record) + if (!ref || ref.consider) continue + turn.refs.push(ref) + } + if (typeof (m as { time?: { completed?: number } }).time?.completed !== "number" && busy()) turn.busy = true + } + return out.filter((t) => t.refs.length > 0) + }) + + const treeItemCount = createMemo(() => { + const seen = new Set() + for (const t of turns()) for (const r of t.refs) seen.add(r.path ?? r.label.toLowerCase()) + return seen.size + }) + + const hasTreeItems = createMemo(() => treeItemCount() > 0) + + const [engine, setEngine] = createSignal() + const currentScheme = (): ContextTreeScheme => + document.documentElement.dataset.colorScheme === "light" ? "light" : "dark" + const [scheme, setScheme] = createSignal( + typeof document === "undefined" ? "dark" : currentScheme(), + ) + + // node clicks open the real thing + const openTab = createOpenSessionFileTab({ + normalizeTab: (tab) => (tab.startsWith("file://") ? file.tab(tab) : tab), + openTab: (tab) => tabs().open(tab), + pathFromTab: file.pathFromTab, + loadFile: file.load, + openReviewPanel: () => { + if (!view().reviewPanel.opened()) view().reviewPanel.open() + }, + setActive: (tab) => tabs().setActive(tab), + }) + const onTreeSelect = (node: ContextTreeSelection) => { + if (!node.path || node.locked) return + const vaultRef = vaultRefFromPath(node.path) + if (vaultRef) { + vaultPanel.open({ mount: vaultRef.mount, path: vaultRef.rel }) + return + } + openTab(file.tab(node.path)) + } + + // theme observer + const themeObserver = new MutationObserver(() => { + setScheme(currentScheme()) + engine()?.setTheme(currentScheme()) + }) + onMount(() => { + themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["data-color-scheme"] }) + engine()?.resize() + engine()?.resume() + }) + onCleanup(() => { + themeObserver.disconnect() + engine()?.destroy() + }) + + // hover glance from tool rows in the log + const onToolHover = (e: Event) => { + const d = (e as CustomEvent).detail as { label?: string } | undefined + if (d?.label) engine()?.highlight(d.label) + } + window.addEventListener("amicode:brain-hover", onToolHover) + onCleanup(() => window.removeEventListener("amicode:brain-hover", onToolHover)) + + const tree = createMemo(() => buildContextTree(turns(), { vaultLocked })) + createEffect(() => { + const brain = engine() + if (!brain) return + brain.setTree(tree()) + }) + + // keyboard navigation + const flatNodes = createMemo(() => { + const out: ContextTreeSelection[] = [] + const walk = (n: ContextTreeNodeInput) => { + if (n.kind !== "root") + out.push({ id: n.id, label: n.label, kind: n.kind, path: n.path, vault: n.vault, locked: n.locked }) + for (const c of n.children ?? []) walk(c) + } + walk(tree()) + return out + }) + const [kbIndex, setKbIndex] = createSignal(-1) + const [announce, setAnnounce] = createSignal("") + const kbFocus = (index: number) => { + const list = flatNodes() + if (!list.length) return + const next = Math.min(Math.max(index, 0), list.length - 1) + setKbIndex(next) + const node = list[next] + engine()?.focus(node.id) + setAnnounce( + `${node.label} — ${node.kind}${ + node.locked ? ", locked — this vault does not allow browsing" : node.path ? ", press Enter to open" : "" + }`, + ) + } + const onCanvasKeyDown = (e: KeyboardEvent) => { + const list = flatNodes() + if (!list.length) return + switch (e.key) { + case "ArrowRight": + case "ArrowDown": + e.preventDefault() + kbFocus(kbIndex() + 1) + return + case "ArrowLeft": + case "ArrowUp": + e.preventDefault() + kbFocus(kbIndex() - 1) + return + case "Home": + e.preventDefault() + kbFocus(0) + return + case "End": + e.preventDefault() + kbFocus(list.length - 1) + return + case "Enter": + case " ": { + if (kbIndex() < 0) return + e.preventDefault() + onTreeSelect(list[kbIndex()]) + return + } + } + } + // ─── End context tree ──────────────────────────────────────────────────── + + let scroll: HTMLDivElement | undefined + let frame: number | undefined + let pending: { x: number; y: number } | undefined + + const restoreScroll = () => { + const el = scroll + if (!el) return + + const s = view().scroll("context") + if (!s) return + + if (el.scrollTop !== s.y) el.scrollTop = s.y + if (el.scrollLeft !== s.x) el.scrollLeft = s.x + } + + const handleScroll = (event: Event & { currentTarget: HTMLDivElement }) => { + pending = { + x: event.currentTarget.scrollLeft, + y: event.currentTarget.scrollTop, + } + if (frame !== undefined) return + + frame = requestAnimationFrame(() => { + frame = undefined + + const next = pending + pending = undefined + if (!next) return + + view().setScroll("context", next) + }) + } + + createEffect( + on( + () => messages().length, + () => { + requestAnimationFrame(restoreScroll) + }, + { defer: true }, + ), + ) + + onCleanup(() => { + if (frame === undefined) return + cancelAnimationFrame(frame) + }) + + return ( + { + scroll = el + restoreScroll() + }} + onScroll={handleScroll} + > +
+ {/* Context tree graph — ADR 0004: relocated from the top panel */} + +
+
+
{language.t("amicode.contextTree.title")}
+
+ {language.t("amicode.contextTree.count", { count: treeItemCount() })} +
+
+
+ + {(item) => ( +
+ + {item.label} +
+ )} +
+
+ engine()?.fit()} + aria-label={language.t("amicode.contextTree.fit")} + /> +
+
+ + setEngine( + createContextTreeEngine(el, { + scheme: currentScheme(), + size: { width: 400, height: 192 }, + onSelect: onTreeSelect, + }), + ) + } + role="application" + aria-roledescription="context tree" + aria-label={language.t("amicode.contextTree.canvasLabel")} + tabIndex={0} + onKeyDown={onCanvasKeyDown} + onBlur={() => setKbIndex(-1)} + class="block h-full w-full focus-visible:outline focus-visible:outline-1 focus-visible:-outline-offset-1 focus-visible:outline-border-focus-base" + /> +
+
+ {announce()} +
+
+ + +
+ + {(stat) => [0])} value={stat.value()} />} + +
+ + 0}> +
+
{language.t("context.breakdown.title")}
+
+ + {(segment) => ( +
+ )} + +
+
+ + {(segment) => ( +
+
+
{breakdownLabel(segment.key)}
+
{segment.percent.toLocaleString(language.intl())}%
+
+ )} + +
+ +
+ + + + {(prompt) => ( +
+
{language.t("context.systemPrompt.title")}
+
+ +
+
+ )} +
+ +
+
{language.t("context.rawMessages.title")}
+ + + {(message) => ( + + )} + + +
+
+ + ) +} diff --git a/packages/app-bundle/overlay/packages/app/src/components/session/session-header.tsx b/packages/app-bundle/overlay/packages/app/src/components/session/session-header.tsx new file mode 100644 index 00000000..ddb0aa97 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/components/session/session-header.tsx @@ -0,0 +1,1205 @@ +import { AmicoSpinner } from "@opencode-ai/ui/amico-spinner" +import { AppIcon } from "@opencode-ai/ui/app-icon" +import { Button } from "@opencode-ai/ui/button" +import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" +import { Icon } from "@opencode-ai/ui/icon" +import { IconButton } from "@opencode-ai/ui/icon-button" +import { Keybind } from "@opencode-ai/ui/keybind" +import { writeClipboardViaBridge } from "@/components/prompt-input/clipboard-bridge" +import { showToast } from "@/utils/toast" +import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { DialogFooter, DialogHeader, DialogTitleGroup, DialogV2 } from "@opencode-ai/ui/v2/dialog-v2" +import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" +import { getFilename } from "@opencode-ai/core/util/path" +import { batch, createEffect, createMemo, createSignal, For, onCleanup, onMount, Show } from "solid-js" +import { createStore } from "solid-js/store" +import { createMediaQuery } from "@solid-primitives/media" +import { Portal } from "solid-js/web" +import { useCommand } from "@/context/command" +import { useLanguage } from "@/context/language" +import { useLayout } from "@/context/layout" +import { usePlatform } from "@/context/platform" +import { useServer, ServerConnection } from "@/context/server" +import { useSettings } from "@/context/settings" +import { useSync } from "@/context/sync" +import { sessionHasOpenTab, useTabs } from "@/context/tabs" +import { useTerminal } from "@/context/terminal" +import { focusTerminalById } from "@/pages/session/helpers" +import { useSessionLayout } from "@/pages/session/session-layout" +import { messageAgentColor } from "@/utils/agent" +import { decode64 } from "@/utils/base64" +import { fileManagerApp } from "@/utils/file-manager" +import { Persist, persisted } from "@/utils/persist" +import { sessionTitle } from "@/utils/session-title" +import { StatusPopover, StatusPopoverV2 } from "../status-popover" +import { statusTriggerVisibility } from "../status-popover-model" +import { useServerSync } from "@/context/server-sync" +import { useGlobal } from "@/context/global" +import { base64Encode } from "@opencode-ai/core/util/encode" +import { sortedRootSessions } from "@/pages/layout/helpers" +import { useNavigate } from "@solidjs/router" +import type { Session } from "@opencode-ai/sdk/v2/client" + +// AMICODE: the MCP/LSP/Plugins/Vaults status popover is opencode-operator +// noise here ("No MCPs configured"). Hidden, not deleted — the trigger slot is +// where a solver-health panel (server/Julia env/runs dir) belongs later. +// Un-hidden 2026-07-20 (amicode#159 test drive): the 7/7 hide targeted the +// popover's then-only content (upstream MCP/LSP operator noise). It now hosts +// the amicode-first Vaults + Connections tabs — hiding it orphaned both. +const AMICODE_HIDE_STATUS_POPOVER = false +import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" +import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" +import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2" +import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" +import { reviewTooltipKeybind } from "../command-tooltip-keybind" +import { useTitlebarRightMount } from "../titlebar" + +const OPEN_APPS = [ + "vscode", + "cursor", + "zed", + "textmate", + "antigravity", + "finder", + "terminal", + "iterm2", + "ghostty", + "warp", + "xcode", + "android-studio", + "powershell", + "sublime-text", +] as const + +type OpenApp = (typeof OPEN_APPS)[number] +type OS = "macos" | "windows" | "linux" | "unknown" + +const MAC_APPS = [ + { + id: "vscode", + label: "session.header.open.app.vscode", + icon: "vscode", + openWith: "Visual Studio Code", + }, + { id: "cursor", label: "session.header.open.app.cursor", icon: "cursor", openWith: "Cursor" }, + { id: "zed", label: "session.header.open.app.zed", icon: "zed", openWith: "Zed" }, + { id: "textmate", label: "session.header.open.app.textmate", icon: "textmate", openWith: "TextMate" }, + { + id: "antigravity", + label: "session.header.open.app.antigravity", + icon: "antigravity", + openWith: "Antigravity", + }, + { id: "terminal", label: "session.header.open.app.terminal", icon: "terminal", openWith: "Terminal" }, + { id: "iterm2", label: "session.header.open.app.iterm2", icon: "iterm2", openWith: "iTerm" }, + { id: "ghostty", label: "session.header.open.app.ghostty", icon: "ghostty", openWith: "Ghostty" }, + { id: "warp", label: "session.header.open.app.warp", icon: "warp", openWith: "Warp" }, + { id: "xcode", label: "session.header.open.app.xcode", icon: "xcode", openWith: "Xcode" }, + { + id: "android-studio", + label: "session.header.open.app.androidStudio", + icon: "android-studio", + openWith: "Android Studio", + }, + { + id: "sublime-text", + label: "session.header.open.app.sublimeText", + icon: "sublime-text", + openWith: "Sublime Text", + }, +] as const + +const WINDOWS_APPS = [ + { id: "vscode", label: "session.header.open.app.vscode", icon: "vscode", openWith: "code" }, + { id: "cursor", label: "session.header.open.app.cursor", icon: "cursor", openWith: "cursor" }, + { id: "zed", label: "session.header.open.app.zed", icon: "zed", openWith: "zed" }, + { + id: "powershell", + label: "session.header.open.app.powershell", + icon: "powershell", + openWith: "powershell", + }, + { + id: "sublime-text", + label: "session.header.open.app.sublimeText", + icon: "sublime-text", + openWith: "Sublime Text", + }, +] as const + +const LINUX_APPS = [ + { id: "vscode", label: "session.header.open.app.vscode", icon: "vscode", openWith: "code" }, + { id: "cursor", label: "session.header.open.app.cursor", icon: "cursor", openWith: "cursor" }, + { id: "zed", label: "session.header.open.app.zed", icon: "zed", openWith: "zed" }, + { + id: "sublime-text", + label: "session.header.open.app.sublimeText", + icon: "sublime-text", + openWith: "Sublime Text", + }, +] as const + +const detectOS = (platform: ReturnType): OS => { + if (platform.platform === "desktop" && platform.os) return platform.os + if (typeof navigator !== "object") return "unknown" + const value = navigator.platform || navigator.userAgent + if (/Mac/i.test(value)) return "macos" + if (/Win/i.test(value)) return "windows" + if (/Linux/i.test(value)) return "linux" + return "unknown" +} + +const showRequestError = (language: ReturnType, err: unknown) => { + showToast({ + variant: "error", + title: language.t("common.requestFailed"), + description: err instanceof Error ? err.message : String(err), + }) +} + +export function SessionHeader() { + const layout = useLayout() + const command = useCommand() + const server = useServer() + const platform = usePlatform() + const language = useLanguage() + const settings = useSettings() + const sync = useSync() + const terminal = useTerminal() + const { params, view } = useSessionLayout() + + const projectDirectory = createMemo(() => decode64(params.dir) ?? "") + const project = createMemo(() => { + const directory = projectDirectory() + if (!directory) return + return layout.projects.list().find((p) => p.worktree === directory || p.sandboxes?.includes(directory)) + }) + const name = createMemo(() => { + const current = project() + if (current) return current.name || getFilename(current.worktree) + return getFilename(projectDirectory()) + }) + const hotkey = createMemo(() => command.keybind("file.open")) + const os = createMemo(() => detectOS(platform)) + const isV2 = settings.general.newLayoutDesigns + const isDesktopV2 = createMemo(() => platform.platform === "desktop" && settings.general.newLayoutDesigns()) + const search = createMemo(() => (isDesktopV2() ? settings.general.showSearch() : true)) + // AMICODE (#174 AC2): unlike its siblings above, showStatus no longer gates + // the whole trigger — the popover now hosts the global Connections + Vaults + // tabs, and the setting defaults OFF, which orphaned them in every session. + // The settings row sells "server status", so that is all it scopes now: the + // health dot. Policy + rationale live in status-popover-model.ts (tested). + const statusVis = createMemo(() => + statusTriggerVisibility({ desktopV2: isDesktopV2(), showStatus: settings.general.showStatus() }), + ) + const isDesktop = createMediaQuery("(min-width: 768px)") + + const [exists, setExists] = createStore>>({ + finder: true, + }) + + const apps = createMemo(() => { + if (os() === "macos") return MAC_APPS + if (os() === "windows") return WINDOWS_APPS + return LINUX_APPS + }) + + const fileManager = createMemo(() => fileManagerApp(os())) + + createEffect(() => { + if (platform.platform !== "desktop") return + if (!platform.checkAppExists) return + + const list = apps() + + setExists(Object.fromEntries(list.map((app) => [app.id, undefined])) as Partial>) + + void Promise.all( + list.map((app) => + Promise.resolve(platform.checkAppExists?.(app.openWith)) + .then((value) => Boolean(value)) + .catch(() => false) + .then((ok) => [app.id, ok] as const), + ), + ).then((entries) => { + setExists(Object.fromEntries(entries) as Partial>) + }) + }) + + const options = createMemo(() => { + return [ + { id: "finder", label: language.t(fileManager().label), icon: fileManager().icon }, + ...apps() + .filter((app) => exists[app.id]) + .map((app) => ({ ...app, label: language.t(app.label) })), + ] as const + }) + + const toggleTerminal = () => { + const next = !view().terminal.opened() + view().terminal.toggle() + if (!next) return + + const id = terminal.active() + if (!id) return + focusTerminalById(id) + } + + const [prefs, setPrefs] = persisted(Persist.global("open.app"), createStore({ app: "finder" as OpenApp })) + const [menu, setMenu] = createStore({ open: false }) + const [openRequest, setOpenRequest] = createStore({ + app: undefined as OpenApp | undefined, + }) + + const canOpen = createMemo(() => platform.platform === "desktop" && !!platform.openPath && server.isLocal()) + const current = createMemo( + () => + options().find((o) => o.id === prefs.app) ?? + options()[0] ?? + ({ id: "finder", label: fileManager().label, icon: fileManager().icon } as const), + ) + const opening = createMemo(() => openRequest.app !== undefined) + const tint = createMemo(() => + messageAgentColor(params.id ? sync().data.message[params.id] : undefined, sync().data.agent), + ) + const v2ActionsState = createMemo(() => ({ + statusDotVisible: statusVis().healthDot, + statusLabel: language.t("status.popover.trigger"), + reviewLabel: language.t("command.review.toggle"), + reviewKeybind: reviewTooltipKeybind(command), + reviewVisible: isDesktop(), + reviewOpened: view().reviewPanel.opened(), + onReviewToggle: () => view().reviewPanel.toggle(), + })) + + const selectApp = (app: OpenApp) => { + if (!options().some((item) => item.id === app)) return + setPrefs("app", app) + } + + const openDir = (app: OpenApp) => { + if (opening() || !canOpen() || !platform.openPath) return + const directory = projectDirectory() + if (!directory) return + + const item = options().find((o) => o.id === app) + const openWith = item && "openWith" in item ? item.openWith : undefined + setOpenRequest("app", app) + platform + .openPath(directory, openWith) + .catch((err: unknown) => showRequestError(language, err)) + .finally(() => { + setOpenRequest("app", undefined) + }) + } + + const copyPath = async () => { + const directory = projectDirectory() + if (!directory) return + try { + // Amicode webview: navigator.clipboard dies in the chat iframe — bridge first. + if (!writeClipboardViaBridge(directory)) await navigator.clipboard.writeText(directory) + showToast({ + variant: "success", + icon: "circle-check", + title: language.t("session.share.copy.copied"), + description: directory, + }) + } catch (err: unknown) { + showRequestError(language, err) + } + } + + const [centerMount, setCenterMount] = createSignal(null) + const rightMount = useTitlebarRightMount() + onMount(() => { + setCenterMount(document.getElementById("opencode-titlebar-center")) + }) + + return ( + <> + + {(mount) => ( + + + + )} + + + {(mount) => ( + + + + + } + > +
+
+ + setMenu("open", open)} + > + + + + + + {language.t("session.header.openIn")} + + { + if (!OPEN_APPS.includes(value as OpenApp)) return + selectApp(value as OpenApp) + }} + > + + {(o) => ( + { + setMenu("open", false) + openDir(o.id) + }} + > +
+ +
+ {o.label} + + + +
+ )} +
+
+
+ + { + setMenu("open", false) + copyPath() + }} + > +
+ +
+ + {language.t("session.header.open.copyPath")} + +
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + +
+
+ } + > + +
+ + )} + + + ) +} + +type SessionHeaderV2ActionsState = { + /** AMICODE (#174 AC2): the trigger itself always renders in a session; the + * show-status setting only drives this health-dot flag. */ + statusDotVisible: boolean + statusLabel: string + reviewLabel: string + reviewKeybind: string[] + reviewVisible: boolean + reviewOpened: boolean + onReviewToggle: () => void +} + +function SessionHeaderV2Actions(props: { state: SessionHeaderV2ActionsState }) { + const language = useLanguage() + const command = useCommand() + + return ( +
+ + {/* amicode#274: Session Chats Dropdown — chat navigation from within a session */} + + + + + + + + + {props.state.reviewLabel} + 0}> + + + + } + > + } + /> + + +
+ ) +} + +// amicode#274: Session Chats Dropdown — mirrors the dashboard sessions flyout +// (amicode#273) inside the session header: tabbed Active/Archived, search, +// open-tab indicators, archive/unarchive actions, and cursor-based pagination. +const SESSION_DROPDOWN_ROW = + "flex min-w-0 w-full shrink-0 cursor-default items-center rounded-[6px] bg-transparent text-left transition-[background-color,color,box-shadow] duration-[120ms] ease-in-out focus-visible:outline-none h-7 gap-2 px-1.5 [font-weight:440] text-v2-text-text-muted hover:bg-v2-overlay-simple-overlay-hover hover:text-v2-text-text-base focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:text-v2-text-text-base" + +function SessionChatsDropdown() { + const tabs = useTabs() + const server = useServer() + const serverSync = useServerSync() + const globalCtx = useGlobal() + const language = useLanguage() + const navigate = useNavigate() + const { params } = useSessionLayout() + + const [open, setOpen] = createSignal(false) + const [flyoutTab, setFlyoutTab] = createSignal<"active" | "archived">("active") + const [search, setSearch] = createSignal("") + const [archivedSessions, setArchivedSessions] = createSignal([]) + const [archivedLoading, setArchivedLoading] = createSignal(false) + const [archivedCursor, setArchivedCursor] = createSignal(undefined) + const [archivedHasMore, setArchivedHasMore] = createSignal(true) + const ARCHIVED_PAGE_SIZE = 20 + + let flyoutRoot: HTMLDivElement | undefined + + const currentSessionID = createMemo(() => params.id) + + // Active sessions — only computed when the flyout is open to avoid + // triggering reactive subscriptions (serverSync().child pins the directory + // and can cascade re-renders to the parent Portal). + // Source from ALL project directories (same as dashboard) — not just the + // server's cwd, which may not be where sessions live (amicode#138). + const activeSessions = createMemo(() => { + if (!open()) return [] + try { + const conn = server.current + if (!conn) return [] + const ctx = globalCtx.ensureServerCtx(conn) + if (!ctx) return [] + const projects = ctx.projects.list() + const directories = projects.flatMap((p) => [p.worktree, ...(p.sandboxes ?? [])]) + const seen = new Set() + const sessions: Session[] = [] + for (const dir of directories) { + const [store] = ctx.sync.child(dir, { bootstrap: false }) + for (const session of sortedRootSessions(store, Date.now())) { + if (seen.has(session.id)) continue + seen.add(session.id) + sessions.push(session) + } + } + return sessions.sort((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created)) + } catch { + return [] + } + }) + + // Sort: open-tab sessions first + const sortedActiveSessions = createMemo(() => { + if (!open()) return [] + const all = activeSessions() + const openTabs: Session[] = [] + const rest: Session[] = [] + for (const session of all) { + if (sessionHasOpenTab(tabs.store, server.key, session)) { + openTabs.push(session) + } else { + rest.push(session) + } + } + return [...openTabs, ...rest] + }) + + // Search filtering + const searchQuery = createMemo(() => search().trim().toLowerCase()) + const filteredActiveSessions = createMemo(() => { + if (!open()) return [] + const q = searchQuery() + if (!q) return sortedActiveSessions() + return sortedActiveSessions().filter((session) => { + const title = sessionTitle(session.title) || session.id + return title.toLowerCase().includes(q) + }) + }) + const filteredArchivedSessions = createMemo(() => { + const q = searchQuery() + if (!q) return archivedSessions() + return archivedSessions().filter((session) => { + const title = sessionTitle(session.title) || session.id + return title.toLowerCase().includes(q) + }) + }) + + // SDK access for archived sessions + function getServerCtx() { + const conn = server.current + if (!conn) return + return globalCtx.ensureServerCtx(conn) + } + + async function loadArchivedSessions(reset = false) { + const ctx = getServerCtx() + if (!ctx) return + setArchivedLoading(true) + try { + const cursor = reset ? undefined : archivedCursor() + const result = await ctx.sdk.client.experimental.session.list( + { archived: true, limit: ARCHIVED_PAGE_SIZE, ...(cursor ? { cursor: Number(cursor) } : {}) }, + ) + const sessions: Session[] = (result.data ?? []) as Session[] + if (reset) { + setArchivedSessions(sessions) + } else { + setArchivedSessions((prev) => [...prev, ...sessions]) + } + const lastSession = sessions[sessions.length - 1] + if (sessions.length >= ARCHIVED_PAGE_SIZE && lastSession) { + setArchivedCursor(String(lastSession.time.updated ?? lastSession.time.created)) + setArchivedHasMore(true) + } else { + setArchivedCursor(undefined) + setArchivedHasMore(false) + } + } catch { + // degrade gracefully + } finally { + setArchivedLoading(false) + } + } + + async function archiveSession(session: Session) { + const ctx = getServerCtx() + if (!ctx) return + try { + await (ctx.sdk.client.session.update as Function)({ + sessionID: session.id, + directory: session.directory, + time: { archived: Date.now() }, + }) + setArchivedSessions((prev) => [session, ...prev]) + // Reload active sessions for this session's directory + await serverSync().project.loadSessions(session.directory, { limit: 64 }) + } catch (cause) { + showToast({ + title: language.t("common.requestFailed"), + description: String(cause), + }) + } + } + + async function unarchiveSession(session: Session) { + const ctx = getServerCtx() + if (!ctx) return + try { + await (ctx.sdk.client.session.update as Function)({ + sessionID: session.id, + directory: session.directory, + time: { archived: null }, + }) + setArchivedSessions((prev) => prev.filter((s) => s.id !== session.id)) + await serverSync().project.loadSessions(session.directory, { limit: 64 }) + } catch (cause) { + showToast({ + title: language.t("common.requestFailed"), + description: String(cause), + }) + } + } + + // amicode#310: permanently delete an archived session with confirmation dialog. + const dialog = useDialog() + function deleteArchivedSession(session: Session) { + dialog.show(() => ( + { + const ctx = getServerCtx() + if (!ctx) return + try { + await (ctx.sdk.client.session.delete as Function)({ + sessionID: session.id, + directory: session.directory, + }) + setArchivedSessions((prev) => prev.filter((s) => s.id !== session.id)) + } catch (cause) { + showToast({ + title: language.t("session.delete.failed.title"), + description: String(cause), + }) + } + dialog.close() + }} + onCancel={() => dialog.close()} + /> + )) + } + + async function openSession(session: Session) { + // Close flyout first so its Portal unmounts cleanly. + setOpen(false) + + // Mirror the dashboard's project setup: ensure the directory is registered and + // touched so the workspace context is warm when the session page mounts. + const conn = server.current + if (conn) { + const ctx = globalCtx.ensureServerCtx(conn) + ctx.projects.open(session.directory) + ctx.projects.touch(session.directory) + } + + // Await session sync BEFORE navigating (issue #176). When switching sessions + // within the same route (params.id change), the Page component persists and + // the timeline re-keys immediately. If data isn't fully cached yet, rows + // trickle in and the measurement burst overlaps with user interaction, causing + // the virtualizer's scroll anchoring to snap the viewport back. Awaiting here + // ensures messagesReady() is true with complete data the moment the timeline + // mounts — matching the dashboard path where the fresh route mount naturally + // gates on messagesReady(). For already-cached sessions this resolves instantly. + await serverSync().session.sync(session.id).catch(() => {}) + + // Use tabs.select for sessions that already have an open tab (no transition). + // For sessions without a tab, navigate directly — DO NOT use tabs.openPath + // which calls addSessionTab(startTransition), keeping both old and new UI + // mounted during the transition and duplicating Portal-rendered buttons. + const existingTab = tabs.store.find( + (t) => t.type === "session" && t.sessionId === session.id, + ) + if (existingTab) { + tabs.select(existingTab) + } else { + const path = `/${base64Encode(session.directory)}/session/${session.id}` + navigate(path) + } + } + + function handleNewChat() { + const dir = serverSync().data.path.directory || "" + void tabs.newDraft({ server: server.key, directory: dir }) + setOpen(false) + } + + // Dismiss on outside click or Escape + createEffect(() => { + if (!open()) return + // Defer listener attachment so the opening mousedown doesn't immediately dismiss + const timer = setTimeout(() => { + const onDown = (e: MouseEvent) => { + const target = e.target as Node + if (flyoutRoot?.contains(target)) return + if (triggerRef?.contains(target)) return + // Don't dismiss if the click landed inside a dialog (e.g. delete confirmation) + if (target instanceof Element && target.closest("[data-dialog-layer], [data-component='dialog-overlay']")) return + setOpen(false) + } + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") setOpen(false) + } + document.addEventListener("mousedown", onDown) + document.addEventListener("keydown", onKey) + onCleanup(() => { + document.removeEventListener("mousedown", onDown) + document.removeEventListener("keydown", onKey) + }) + }, 0) + onCleanup(() => clearTimeout(timer)) + }) + + let triggerRef: HTMLButtonElement | undefined + + return ( + <> + + + +
+
+ {/* Search */} + + + {/* Tab bar — Active vs Archived */} +
+ + +
+ + } + onClick={handleNewChat} + aria-label={language.t("command.session.new")} + /> + +
+ + {/* Tab content */} +
+ + 0} + fallback={ +
+ {searchQuery() ? language.t("home.sessions.search.noResults", { query: search() }) : language.t("home.sessions.empty")} +
+ } + > +
+ + {(session) => ( + + )} + +
+
+
+ + 0} + fallback={ +
+ {language.t("common.loading")} +
+ } + > + 0} + fallback={ +
+ No archived sessions +
+ } + > +
+ + {(session) => ( + + )} + +
+ + + +
+
+
+
+
+
+
+
+ + ) +} + +function SessionDropdownRow(props: { + session: Session + isOpenTab: boolean + isCurrent: boolean + onOpen: (session: Session) => void + onArchive: (session: Session) => void +}) { + const language = useLanguage() + const title = createMemo(() => sessionTitle(props.session.title) || props.session.id) + + return ( +
+ +
+ + } + aria-label={language.t("common.archive")} + onClick={(event: MouseEvent) => { + event.preventDefault() + event.stopPropagation() + void props.onArchive(props.session) + }} + /> + +
+
+ ) +} + +function ArchivedSessionDropdownRow(props: { + session: Session + onOpen: (session: Session) => void + onUnarchive: (session: Session) => void + onDelete: (session: Session) => void +}) { + const title = createMemo(() => sessionTitle(props.session.title) || props.session.id) + + return ( +
+ +
+ + } + aria-label="Unarchive" + onClick={(event: MouseEvent) => { + event.preventDefault() + event.stopPropagation() + void props.onUnarchive(props.session) + }} + /> + + + } + aria-label="Delete permanently" + onClick={(event: MouseEvent) => { + event.preventDefault() + event.stopPropagation() + void props.onDelete(props.session) + }} + /> + +
+
+ ) +} + +// amicode#310: confirmation dialog for permanently deleting an archived session. +function DialogDeleteArchivedSession(props: { + session: Session + onConfirm: () => void + onCancel: () => void +}) { + const language = useLanguage() + const name = createMemo(() => sessionTitle(props.session.title) || props.session.id) + + return ( + + + + + + + {language.t("common.cancel")} + + + {language.t("session.delete.button")} + + + + ) +} diff --git a/packages/app-bundle/overlay/packages/app/src/components/session/session-new-view.tsx b/packages/app-bundle/overlay/packages/app/src/components/session/session-new-view.tsx new file mode 100644 index 00000000..e50aaafe --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/components/session/session-new-view.tsx @@ -0,0 +1,127 @@ +import { Show, createMemo, createResource } from "solid-js" +import { DateTime } from "luxon" +import { useSync } from "@/context/sync" +import { useSDK } from "@/context/sdk" +import { useLanguage } from "@/context/language" +import { usePrompt } from "@/context/prompt" +import { useServer } from "@/context/server" +import { startPrompt as startPromptWith } from "@/utils/start-prompt" +import { amicodeGet } from "@/utils/amicode-fetch" +import { parseProblemsResponse } from "@opencode-ai/ui/amicode-problem-switcher" +import { Icon } from "@opencode-ai/ui/icon" +import { AmicodeGettingStarted } from "@opencode-ai/ui/amicode-getting-started" +import { Logo, MarkDetailed } from "@opencode-ai/ui/logo" +import { getDirectory, getFilename } from "@opencode-ai/core/util/path" + +const MAIN_WORKTREE = "main" +const CREATE_WORKTREE = "create" +const ROOT_CLASS = "size-full flex flex-col" + +interface NewSessionViewProps { + worktree: string +} + +export function NewSessionView(props: NewSessionViewProps) { + const sync = useSync() + const sdk = useSDK() + const language = useLanguage() + const prompt = usePrompt() + + // amicode: starter chips — see utils/start-prompt.ts for the mechanism. + const startPrompt = (text: string) => startPromptWith(prompt, text) + + // amicode: Resume verb (spec B) — fetched once per view; renders only when a + // non-archived problem workspace exists (no empty chrome). Fetch failure → + // undefined → no Resume chip; the start screen stays stock. + const server = useServer() + const [problemsRaw] = createResource(() => amicodeGet(server.current, "/amicode/problems").catch(() => undefined)) + const resumeProblem = createMemo(() => { + const raw = problemsRaw() + if (raw === undefined) return undefined + const view = parseProblemsResponse(raw) + if (!view.ok) return undefined + const open = view.problems.filter((p) => p.status !== "archived") + if (open.length === 0) return undefined + return [...open].sort((a, b) => (b.recorded ?? "").localeCompare(a.recorded ?? ""))[0] + }) + + const sandboxes = createMemo(() => sync().project?.sandboxes ?? []) + const options = createMemo(() => [MAIN_WORKTREE, ...sandboxes(), CREATE_WORKTREE]) + const current = createMemo(() => { + const selection = props.worktree + if (options().includes(selection)) return selection + return MAIN_WORKTREE + }) + const projectRoot = createMemo(() => sync().project?.worktree ?? sdk().directory) + const isWorktree = createMemo(() => { + const project = sync().project + if (!project) return false + return sdk().directory !== project.worktree + }) + + const label = (value: string) => { + if (value === MAIN_WORKTREE) { + if (isWorktree()) return language.t("session.new.worktree.main") + const branch = sync().data.vcs?.branch + if (branch) return language.t("session.new.worktree.mainWithBranch", { branch }) + return language.t("session.new.worktree.main") + } + + if (value === CREATE_WORKTREE) return language.t("session.new.worktree.create") + + return getFilename(value) + } + + return ( +
+
+
+
+
+ + {/* amicode: brand wordmark in the brand typeface (Racing Sans One — restored per review); + MarkDetailed is the redesign's detailed mark (kept from trunk) */} + +
+ {/* amicode: getting-started block (tagline + how-it-works + starter chips) */} + { + const name = resumeProblem()?.name + if (name) startPrompt(`Open the problem "${name}" and continue where we left off`) + }} + /> +
+
+
+ {getDirectory(projectRoot())} + {getFilename(projectRoot())} +
+
+
+ +
+ {label(current())} +
+
+ + {(project) => ( +
+
+ {language.t("session.new.lastModified")}  + + {DateTime.fromMillis(project().time.updated ?? project().time.created) + .setLocale(language.intl()) + .toRelative()} + +
+
+ )} +
+
+
+
+
+ ) +} diff --git a/packages/app-bundle/overlay/packages/app/src/components/session/session-preview-tab.tsx b/packages/app-bundle/overlay/packages/app/src/components/session/session-preview-tab.tsx new file mode 100644 index 00000000..787df200 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/components/session/session-preview-tab.tsx @@ -0,0 +1,428 @@ +import { createEffect, createMemo, createSignal, For, on, onCleanup, Show } from "solid-js" +import { createStore } from "solid-js/store" +import { Markdown } from "@opencode-ai/session-ui/markdown" +import { Icon } from "@opencode-ai/ui/icon" +import { IconButton } from "@opencode-ai/ui/icon-button" +import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2" +import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" +import { SegmentedControlV2, SegmentedControlItemV2 } from "@opencode-ai/ui/v2/segmented-control-v2" +import { writeClipboardViaBridge } from "@/components/prompt-input/clipboard-bridge" +import { useSDK } from "@/context/sdk" +import { useServerSDK } from "@/context/server-sdk" + +// ─── Types ────────────────────────────────────────────────────────────────── + +export interface PreviewFileEntry { + path: string + relativePath: string + basename: string + extension: ".md" + changeType: "added" | "modified" +} + +interface PreviewFileState { + mode: "preview" | "raw" + scrollPosition?: number + unsavedContent?: string +} + +// ─── Main Component ───────────────────────────────────────────────────────── + +/** + * Convert ```math fenced code blocks (GitHub-flavored) to $$...$$ display math + * blocks that the Markdown component's KaTeX extension understands. + */ +function preprocessMarkdown(text: string): string { + return text.replace(/```math\n([\s\S]*?)```/g, (_match, body: string) => `$$\n${body.trim()}\n$$`) +} + +export function SessionPreviewTab(props: { + diffs: () => Array<{ file: string; status?: string }> + touchedFiles?: () => Array<{ file: string; status: string }> +}) { + const sdk = useSDK() + const serverSDK = useServerSDK() + const [selectedFile, setSelectedFile] = createSignal(undefined) + const [fileStates, setFileStates] = createStore>({}) + const [fileContent, setFileContent] = createSignal("") + const [loading, setLoading] = createSignal(false) + const [zoom, setZoom] = createSignal(100) + + const zoomIn = () => setZoom((z) => Math.min(z + 10, 200)) + const zoomOut = () => setZoom((z) => Math.max(z - 10, 50)) + + // Derive the file list from touchedFiles (tool-edit history, persists regardless of git state) + // supplemented by diffs for any files not already covered. + const markdownFiles = createMemo((): PreviewFileEntry[] => { + const seen = new Set() + const entries: PreviewFileEntry[] = [] + + // Build a suffix→absolute map from touchedFiles so we can resolve ~/... paths from diffs + const touched = props.touchedFiles?.() ?? [] + const suffixToAbsolute = new Map() + for (const t of touched) { + // For "/Users/jj/.julia/dev/X", try progressively shorter suffixes + const parts = t.file.split("/") + for (let i = 1; i < parts.length; i++) { + suffixToAbsolute.set(parts.slice(i).join("/"), t.file) + } + } + + // Resolve ~/... to absolute using the suffix map + const resolveFile = (file: string): string => { + if (!file.startsWith("~/")) return file + const suffix = file.slice(2) // strip ~/ + const absolute = suffixToAbsolute.get(suffix) + return absolute ?? file + } + + const toEntry = (file: string, status: string): PreviewFileEntry | null => { + if (!file.endsWith(".md")) return null + // Use the resolved absolute path as the canonical key + const resolved = resolveFile(file) + if (seen.has(resolved)) return null + seen.add(resolved) + const parts = resolved.split("/") + const basename = parts[parts.length - 1] + // Show a short relative path: strip common leading segments until we hit a recognizable dir + const relativePath = resolved.replace(/^\/Users\/[^/]+\//, "") + return { + path: resolved, + relativePath, + basename, + extension: ".md" as const, + changeType: (status === "added" ? "added" : "modified") as "added" | "modified", + } + } + + // Primary: all files touched by edit tools in this session + for (const t of touched) { + const entry = toEntry(t.file, t.status) + if (entry) entries.push(entry) + } + + // Supplement: any diff files not already in touchedFiles + for (const d of props.diffs()) { + const entry = toEntry(d.file, d.status === "added" ? "added" : "modified") + if (entry) entries.push(entry) + } + + return entries + }) + + // Load file content when a file is selected + createEffect( + on(selectedFile, (path) => { + if (!path) return + setLoading(true) + + // Resolve the actual filesystem path from the display path + const fsPath = path.startsWith("~/") + ? path.replace("~", process.env.HOME ?? "") + : path + + sdk() + .client.file.read({ path: fsPath }) + .then((result) => { + const content = result.data + if (content && content.type === "text") { + setFileContent(content.content) + } + }) + .catch(() => { + setFileContent("") + }) + .finally(() => { + setLoading(false) + }) + }), + ) + + const currentMode = createMemo(() => { + const path = selectedFile() + if (!path) return "preview" + return fileStates[path]?.mode ?? "preview" + }) + + const goBack = () => { + setSelectedFile(undefined) + } + + // ─── Raw Editor Save ──────────────────────────────────────────────────── + + let saveTimer: ReturnType | undefined + const [saveStatus, setSaveStatus] = createSignal<"idle" | "saving" | "saved">("idle") + let savedTimer: ReturnType | undefined + + const saveFile = (path: string, content: string) => { + const fsPath = path.startsWith("~/") + ? path.replace("~", process.env.HOME ?? "") + : path + + const baseUrl = serverSDK().url + if (!baseUrl) return + + setSaveStatus("saving") + + // POST to the file write endpoint + fetch(new URL("/file/write", baseUrl), { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ path: fsPath, content }), + }) + .then(() => { + setSaveStatus("saved") + if (savedTimer) clearTimeout(savedTimer) + savedTimer = setTimeout(() => setSaveStatus("idle"), 2000) + }) + .catch(() => { + setSaveStatus("idle") + }) + } + + const debouncedSave = (path: string, content: string) => { + if (saveTimer) clearTimeout(saveTimer) + saveTimer = setTimeout(() => saveFile(path, content), 1000) + } + + const immediateSave = () => { + const path = selectedFile() + if (!path) return + const content = fileStates[path]?.unsavedContent + if (content !== undefined) { + if (saveTimer) clearTimeout(saveTimer) + saveFile(path, content) + setFileStates(path, { ...fileStates[path], unsavedContent: undefined }) + } + } + + const handleRawEdit = (content: string) => { + const path = selectedFile() + if (!path) return + setFileStates(path, { ...fileStates[path], unsavedContent: content }) + setFileContent(content) + debouncedSave(path, content) + } + + onCleanup(() => { + if (saveTimer) clearTimeout(saveTimer) + if (savedTimer) clearTimeout(savedTimer) + }) + + // ─── Render ───────────────────────────────────────────────────────────── + + return ( +
+ } + > + {(path) => ( +
+ {/* Header with back button, mode toggle */} +
+ +
+ {markdownFiles().find((f) => f.path === path())?.basename ?? path()} +
+ + + {saveStatus() === "saving" ? "Saving..." : "Saved"} + + + {/* Zoom control: [100% | - +] */} +
+ { + const val = parseInt(e.currentTarget.value) + if (!isNaN(val) && val >= 50 && val <= 200) setZoom(val) + }} + onBlur={(e) => { + e.currentTarget.value = `${zoom()}%` + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.currentTarget.blur() + } + }} + /> +
+ + +
+
+ {/* Mode toggle */} + { + if (value !== "preview" && value !== "raw") return + const p = selectedFile() + if (p) setFileStates(p, { ...fileStates[p], mode: value }) + }} + class="!w-auto" + aria-label="View mode" + > + + + + + + + + + + + +
+ + {/* Content area */} +
+ Loading...
}> + + } + > +
+ +
+
+ +
+
+ )} + +
+ ) +} + +// ─── File List ────────────────────────────────────────────────────────────── + +function PreviewFileList(props: { files: PreviewFileEntry[]; onSelect: (path: string) => void }) { + const copyToClipboard = (text: string) => { + if (!writeClipboardViaBridge(text)) { + void navigator.clipboard.writeText(text) + } + } + + return ( +
+ 0} + fallback={ +
+ No markdown files modified in this session +
+ } + > +
+ + {(file) => ( + + props.onSelect(file.path)} + > + + {file.changeType === "added" ? "A" : "M"} + +
+
{file.basename}
+
{file.relativePath}
+
+
+ + + copyToClipboard(file.basename)}>Copy filename + { + const fullPath = file.path.startsWith("~/") + ? file.path.replace("~", process.env.HOME ?? "") + : file.path + copyToClipboard(fullPath) + }}>Copy full path + + +
+ )} +
+
+
+
+ ) +} + +// ─── Raw Editor ───────────────────────────────────────────────────────────── + +function RawEditor(props: { content: string; onEdit: (content: string) => void; onSave: () => void; zoom: number }) { + let textareaRef: HTMLTextAreaElement | undefined + + return ( +
` + + const focused = focusTerminalById("one") + + expect(focused).toBe(true) + expect(document.activeElement?.tagName).toBe("TEXTAREA") + }) + + test("falls back to terminal element focus", () => { + document.body.innerHTML = `
` + const terminal = document.querySelector('[data-component="terminal"]') as HTMLElement + let pointerDown = false + terminal.addEventListener("pointerdown", () => { + pointerDown = true + }) + + const focused = focusTerminalById("two") + + expect(focused).toBe(true) + expect(document.activeElement).toBe(terminal) + expect(pointerDown).toBe(true) + }) +}) + +describe("getTabReorderIndex", () => { + test("returns target index for valid drag reorder", () => { + expect(getTabReorderIndex(["a", "b", "c"], "a", "c")).toBe(2) + }) + + test("returns undefined for unknown droppable id", () => { + expect(getTabReorderIndex(["a", "b", "c"], "a", "missing")).toBeUndefined() + }) +}) + +describe("createSessionTabs", () => { + test("normalizes the effective file tab", () => { + createRoot((dispose) => { + const [state] = createStore({ + active: undefined as string | undefined, + all: ["file://src/a.ts", "context"], + }) + const tabs = createMemo(() => ({ active: () => state.active, all: () => state.all })) + const result = createSessionTabs({ + tabs, + pathFromTab: (tab) => (tab.startsWith("file://") ? tab.slice("file://".length) : undefined), + normalizeTab: (tab) => (tab.startsWith("file://") ? `norm:${tab.slice("file://".length)}` : tab), + }) + + expect(result.activeTab()).toBe("norm:src/a.ts") + expect(result.activeFileTab()).toBe("norm:src/a.ts") + expect(result.closableTab()).toBe("norm:src/a.ts") + dispose() + }) + }) + + test("prefers context and review fallbacks when no file tab is active", () => { + createRoot((dispose) => { + const [state] = createStore({ + active: undefined as string | undefined, + all: ["context"], + }) + const tabs = createMemo(() => ({ active: () => state.active, all: () => state.all })) + const result = createSessionTabs({ + tabs, + pathFromTab: () => undefined, + normalizeTab: (tab) => tab, + review: () => true, + hasReview: () => true, + }) + + expect(result.activeTab()).toBe("context") + expect(result.closableTab()).toBe("context") + dispose() + }) + + createRoot((dispose) => { + const [state] = createStore({ + active: undefined as string | undefined, + all: [], + }) + const tabs = createMemo(() => ({ active: () => state.active, all: () => state.all })) + const result = createSessionTabs({ + tabs, + pathFromTab: () => undefined, + normalizeTab: (tab) => tab, + review: () => true, + hasReview: () => true, + }) + + expect(result.activeTab()).toBe("review") + expect(result.activeFileTab()).toBeUndefined() + expect(result.closableTab()).toBeUndefined() + dispose() + }) + }) + + test("exposes the Open File tab without treating it as a file tab", () => { + createRoot((dispose) => { + const [state] = createStore({ + active: SESSION_OPEN_FILE_TAB as string | undefined, + all: ["file://src/a.ts", SESSION_OPEN_FILE_TAB], + }) + const tabs = createMemo(() => ({ active: () => state.active, all: () => state.all })) + const result = createSessionTabs({ + tabs, + pathFromTab: (tab) => (tab.startsWith("file://") ? tab.slice("file://".length) : undefined), + normalizeTab: (tab) => tab, + fileBrowser: () => true, + }) + + expect(result.openFileOpen()).toBe(true) + expect(result.panelTabs()).toEqual(["file://src/a.ts", SESSION_OPEN_FILE_TAB]) + expect(result.openedTabs()).toEqual(["file://src/a.ts"]) + expect(result.activeTab()).toBe(SESSION_OPEN_FILE_TAB) + expect(result.activeFileTab()).toBeUndefined() + expect(result.closableTab()).toBe(SESSION_OPEN_FILE_TAB) + dispose() + }) + }) + + test("hides the Open File placeholder when the file browser is unavailable", () => { + createRoot((dispose) => { + const [state] = createStore({ + active: SESSION_OPEN_FILE_TAB as string | undefined, + all: ["file://src/a.ts", SESSION_OPEN_FILE_TAB], + }) + const tabs = createMemo(() => ({ active: () => state.active, all: () => state.all })) + const result = createSessionTabs({ + tabs, + pathFromTab: (tab) => (tab.startsWith("file://") ? tab.slice("file://".length) : undefined), + normalizeTab: (tab) => tab, + fileBrowser: () => false, + }) + + expect(result.openFileOpen()).toBe(false) + expect(result.panelTabs()).toEqual(["file://src/a.ts"]) + expect(result.activeTab()).toBe("file://src/a.ts") + dispose() + }) + }) + + test("close context then re-open: activeTab returns context", () => { + // Step 1: context is open and active + const initial = { tabs: { all: ["context"], active: "context" as string | undefined }, preview: undefined } + + createRoot((dispose) => { + const tabs = createMemo(() => ({ active: () => initial.tabs.active, all: () => initial.tabs.all })) + const result = createSessionTabs({ + tabs, + pathFromTab: () => undefined, + normalizeTab: (tab) => tab, + review: () => true, + hasReview: () => true, + }) + expect(result.activeTab()).toBe("context") + expect(result.contextOpen()).toBe(true) + dispose() + }) + + // Step 2: close context — active falls to review + const afterClose = closeSessionTab(initial, "context") + expect(afterClose.tabs.all).toEqual([]) + expect(afterClose.tabs.active).toBeUndefined() + + createRoot((dispose) => { + const tabs = createMemo(() => ({ active: () => afterClose.tabs.active, all: () => afterClose.tabs.all })) + const result = createSessionTabs({ + tabs, + pathFromTab: () => undefined, + normalizeTab: (tab) => tab, + review: () => true, + hasReview: () => true, + }) + // Context is gone — activeTab falls to "review" via the hasReview fallback + expect(result.contextOpen()).toBe(false) + expect(result.activeTab()).toBe("review") + dispose() + }) + + // Step 3: re-open context — simulates openSessionContext calling tabs.open("context") + const afterReopen = openSessionTab( + { tabs: afterClose.tabs, preview: afterClose.preview }, + "context", + ) + expect(afterReopen.tabs.all).toContain("context") + expect(afterReopen.tabs.active).toBe("context") + + createRoot((dispose) => { + const tabs = createMemo(() => ({ active: () => afterReopen.tabs.active, all: () => afterReopen.tabs.all })) + const result = createSessionTabs({ + tabs, + pathFromTab: () => undefined, + normalizeTab: (tab) => tab, + review: () => true, + hasReview: () => true, + }) + // After re-open: activeTab MUST be "context" + expect(result.contextOpen()).toBe(true) + expect(result.activeTab()).toBe("context") + dispose() + }) + }) +}) diff --git a/packages/app-bundle/overlay/packages/app/src/pages/session/helpers.ts b/packages/app-bundle/overlay/packages/app/src/pages/session/helpers.ts new file mode 100644 index 00000000..adcfc86d --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/pages/session/helpers.ts @@ -0,0 +1,224 @@ +import { batch, createMemo, onCleanup, onMount, type Accessor } from "solid-js" +import { createStore } from "solid-js/store" +import { makeEventListener } from "@solid-primitives/event-listener" +import { same } from "@/utils/same" +import { SESSION_OPEN_FILE_TAB, SESSION_PREVIEW_TAB } from "@/context/layout-tabs" + +export { SESSION_OPEN_FILE_TAB, SESSION_PREVIEW_TAB } from "@/context/layout-tabs" + +const emptyTabs: string[] = [] + +type Tabs = { + active: Accessor + all: Accessor +} + +type TabsInput = { + tabs: Accessor + pathFromTab: (tab: string) => string | undefined + normalizeTab: (tab: string) => string + review?: Accessor + hasReview?: Accessor + /** the Vault tab (amicode) — a named surface like "context", never a file */ + vaultOpen?: Accessor + fileBrowser?: Accessor +} + +export const getSessionKey = (dir: string | undefined, id: string | undefined) => `${dir ?? ""}${id ? `/${id}` : ""}` + +export function shouldShowFileTree(input: { visible: boolean; opened: boolean }) { + return input.opened && input.visible +} + +export const createSessionTabs = (input: TabsInput) => { + const review = input.review ?? (() => false) + const hasReview = input.hasReview ?? (() => false) + const vaultOpen = input.vaultOpen ?? (() => false) + const fileBrowser = input.fileBrowser ?? (() => false) + const contextOpen = createMemo(() => input.tabs().active() === "context" || input.tabs().all().includes("context")) + const previewOpen = createMemo( + () => input.tabs().active() === SESSION_PREVIEW_TAB || input.tabs().all().includes(SESSION_PREVIEW_TAB), + ) + const openFileOpen = createMemo( + () => + fileBrowser() && + (input.tabs().active() === SESSION_OPEN_FILE_TAB || input.tabs().all().includes(SESSION_OPEN_FILE_TAB)), + ) + const pulseInspectorOpen = createMemo(() => input.tabs().active() === "pulseInspector" || input.tabs().all().includes("pulseInspector")) + const panelTabs = createMemo( + () => { + const seen = new Set() + return input + .tabs() + .all() + .flatMap((tab) => { + if (tab === "context" || tab === "review" || tab === "vault" || tab === SESSION_PREVIEW_TAB || tab === "pulseInspector") return [] + if (tab === SESSION_OPEN_FILE_TAB && !fileBrowser()) return [] + const value = input.pathFromTab(tab) ? input.normalizeTab(tab) : tab + if (seen.has(value)) return [] + seen.add(value) + return [value] + }) + }, + emptyTabs, + { equals: same }, + ) + const openedTabs = createMemo(() => panelTabs().filter((tab) => tab !== SESSION_OPEN_FILE_TAB), emptyTabs, { + equals: same, + }) + const activeTab = createMemo(() => { + const active = input.tabs().active() + if (active === "context") return active + if (active === "pulseInspector") return active + if (active === SESSION_PREVIEW_TAB && previewOpen()) return active + if (active === "vault" && vaultOpen()) return active + if (active === SESSION_OPEN_FILE_TAB && openFileOpen()) return active + if (active === "review" && review()) return active + if (active && input.pathFromTab(active)) return input.normalizeTab(active) + + const first = openedTabs()[0] + if (first) return first + if (vaultOpen()) return "vault" + if (previewOpen()) return SESSION_PREVIEW_TAB + if (contextOpen()) return "context" + if (pulseInspectorOpen()) return "pulseInspector" + if (review() && hasReview()) return "review" + return "empty" + }) + const activeFileTab = createMemo(() => { + const active = activeTab() + if (!openedTabs().includes(active)) return + return active + }) + const closableTab = createMemo(() => { + const active = activeTab() + if (active === "context") return active + if (active === SESSION_OPEN_FILE_TAB && openFileOpen()) return active + if (!openedTabs().includes(active)) return + return active + }) + + return { + contextOpen, + previewOpen, + pulseInspectorOpen, + openFileOpen, + panelTabs, + openedTabs, + activeTab, + activeFileTab, + closableTab, + } +} + +export const focusTerminalById = (id: string) => { + const wrapper = document.getElementById(`terminal-wrapper-${id}`) + const terminal = wrapper?.querySelector('[data-component="terminal"]') + if (!(terminal instanceof HTMLElement)) return false + + const textarea = terminal.querySelector("textarea") + if (textarea instanceof HTMLTextAreaElement) { + textarea.focus() + return true + } + + terminal.focus() + terminal.dispatchEvent( + typeof PointerEvent === "function" + ? new PointerEvent("pointerdown", { bubbles: true, cancelable: true }) + : new MouseEvent("pointerdown", { bubbles: true, cancelable: true }), + ) + return true +} + +export const createOpenReviewFile = (input: { + showAllFiles: () => void + tabForPath: (path: string) => string + openTab: (tab: string) => void + setActive: (tab: string) => void + loadFile: (path: string) => any | Promise +}) => { + return (path: string) => { + batch(() => { + input.showAllFiles() + const maybePromise = input.loadFile(path) + const open = () => { + const tab = input.tabForPath(path) + input.openTab(tab) + input.setActive(tab) + } + if (maybePromise instanceof Promise) void maybePromise.then(open) + else open() + }) + } +} + +export const createOpenSessionFileTab = (input: { + normalizeTab: (tab: string) => string + openTab: (tab: string) => void + pathFromTab: (tab: string) => string | undefined + loadFile: (path: string) => void + openReviewPanel: () => void + setActive: (tab: string) => void +}) => { + return (value: string) => { + const next = input.normalizeTab(value) + input.openTab(next) + + const path = input.pathFromTab(next) + if (!path) return + + input.loadFile(path) + input.openReviewPanel() + input.setActive(next) + } +} + +export const getTabReorderIndex = (tabs: readonly string[], from: string, to: string) => { + const fromIndex = tabs.indexOf(from) + const toIndex = tabs.indexOf(to) + if (fromIndex === -1 || toIndex === -1 || fromIndex === toIndex) return undefined + return toIndex +} + +export const createSizing = () => { + const [state, setState] = createStore({ active: false }) + let t: number | undefined + + const stop = () => { + if (t !== undefined) { + clearTimeout(t) + t = undefined + } + setState("active", false) + } + + const start = () => { + if (t !== undefined) { + clearTimeout(t) + t = undefined + } + setState("active", true) + } + + onMount(() => { + makeEventListener(window, "pointerup", stop) + makeEventListener(window, "pointercancel", stop) + makeEventListener(window, "blur", stop) + }) + + onCleanup(() => { + if (t !== undefined) clearTimeout(t) + }) + + return { + active: () => state.active, + start, + touch() { + start() + t = window.setTimeout(stop, 120) + }, + } +} + +export type Sizing = ReturnType diff --git a/packages/app-bundle/overlay/packages/app/src/pages/session/session-panel-width.test.ts b/packages/app-bundle/overlay/packages/app/src/pages/session/session-panel-width.test.ts new file mode 100644 index 00000000..dca5a86c --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/pages/session/session-panel-width.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, test } from "bun:test" +import { + clampSessionPanelWidth, + clampWorkColumnWidth, + REVIEW_PANE_WIDTH_MIN, + REVIEW_PANE_WIDTH_MIN_SPLIT, + SESSION_PANEL_WIDTH_MIN, + sessionChatTakesRemainder, + sessionPanelWidthMax, + WORK_COLUMN_WIDTH_MIN, + workColumnWidthMax, +} from "./session-panel-width" + +describe("sessionPanelWidthMax", () => { + test("reserves the unified review pane minimum", () => { + expect(sessionPanelWidthMax({ available: 1700, split: false })).toBe(1700 - REVIEW_PANE_WIDTH_MIN) + }) + + test("reserves a larger minimum for split diffs", () => { + expect(sessionPanelWidthMax({ available: 1700, split: true })).toBe(1700 - REVIEW_PANE_WIDTH_MIN_SPLIT) + expect(REVIEW_PANE_WIDTH_MIN_SPLIT).toBeGreaterThan(REVIEW_PANE_WIDTH_MIN) + }) + + test("lets the chat panel take everything beyond the review pane minimum", () => { + // Regression: the old cap was 45% of the window, forcing the review pane + // to at least 55% of the window regardless of content. + const available = 3440 + expect(sessionPanelWidthMax({ available, split: false })).toBeGreaterThan(available * 0.45) + }) + + test("never drops below the chat panel minimum on small windows", () => { + expect(sessionPanelWidthMax({ available: 600, split: true })).toBe(SESSION_PANEL_WIDTH_MIN) + expect(sessionPanelWidthMax({ available: 0, split: false })).toBe(SESSION_PANEL_WIDTH_MIN) + }) +}) + +describe("clampSessionPanelWidth", () => { + test("keeps widths already within the limit", () => { + expect(clampSessionPanelWidth({ width: 800, available: 1700, split: false })).toBe(800) + }) + + test("forces the width down when the window shrinks", () => { + expect(clampSessionPanelWidth({ width: 1600, available: 1700, split: false })).toBe(1700 - REVIEW_PANE_WIDTH_MIN) + expect(clampSessionPanelWidth({ width: 1600, available: 1700, split: true })).toBe( + 1700 - REVIEW_PANE_WIDTH_MIN_SPLIT, + ) + }) + + test("holds the chat panel minimum when there is no room for both", () => { + expect(clampSessionPanelWidth({ width: 1600, available: 700, split: true })).toBe(SESSION_PANEL_WIDTH_MIN) + }) + + test("skips clamping before the layout is measured", () => { + expect(clampSessionPanelWidth({ width: 1600, available: undefined, split: false })).toBe(1600) + }) +}) + +describe("work column width (amicode#105 — the column is bounded, the CHAT is the remainder)", () => { + // The pre-fix philosophy (this file's header): the review pane took whatever + // the chat left behind — a wide monitor squished the chat into the margin. + // The work column now owns a bounded width (default 320, user-resizable). + test("workColumnWidthMax caps the column at 60% of the row", () => { + expect(workColumnWidthMax(1000)).toBe(600) + }) + + test("workColumnWidthMax never drops below the floor", () => { + expect(workColumnWidthMax(300)).toBe(WORK_COLUMN_WIDTH_MIN) + }) + + test("clampWorkColumnWidth caps and floors, passes through pre-measure", () => { + expect(clampWorkColumnWidth({ width: 1500, available: 1000 })).toBe(600) + expect(clampWorkColumnWidth({ width: 100, available: 1000 })).toBe(WORK_COLUMN_WIDTH_MIN) + expect(clampWorkColumnWidth({ width: 1500, available: undefined })).toBe(1500) + }) + + test("the chat is the flex remainder only in v2 with the column visible", () => { + expect(sessionChatTakesRemainder({ newDesign: true, columnVisible: true })).toBe(true) + expect(sessionChatTakesRemainder({ newDesign: true, columnVisible: false })).toBe(false) + expect(sessionChatTakesRemainder({ newDesign: false, columnVisible: true })).toBe(false) + }) +}) diff --git a/packages/app-bundle/overlay/packages/app/src/pages/session/session-panel-width.ts b/packages/app-bundle/overlay/packages/app/src/pages/session/session-panel-width.ts new file mode 100644 index 00000000..0c028e40 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/pages/session/session-panel-width.ts @@ -0,0 +1,44 @@ +// The review pane has no width of its own: it takes whatever the chat panel +// leaves behind. Instead of capping the chat panel at a fraction of the window +// (which forces the review pane to grow with the monitor), reserve a fixed +// minimum for the review pane and let the chat panel take everything else. +export const SESSION_PANEL_WIDTH_MIN = 450 +export const REVIEW_PANE_WIDTH_MIN = 480 +export const REVIEW_PANE_WIDTH_MIN_SPLIT = 800 + +export function sessionPanelWidthMax(input: { available: number; split: boolean }) { + const pane = input.split ? REVIEW_PANE_WIDTH_MIN_SPLIT : REVIEW_PANE_WIDTH_MIN + return Math.max(SESSION_PANEL_WIDTH_MIN, input.available - pane) +} + +// `available` is undefined until the layout row is first measured; render the +// stored width untouched until then to avoid a first-frame snap. +export function clampSessionPanelWidth(input: { width: number; available: number | undefined; split: boolean }) { + if (input.available === undefined) return input.width + return Math.min(input.width, sessionPanelWidthMax({ available: input.available, split: input.split })) +} + +// amicode#105: the Work Column owns a bounded width of its own. The pre-fix +// philosophy (above) let the review pane take everything the chat left behind +// — a wide monitor squished the chat into the left margin. Now the column is +// fixed-width (default 320 — DEFAULT_PANEL_COLUMN_WIDTH — user-resizable +// within these bounds) and the CHAT is the flex remainder. +export const WORK_COLUMN_WIDTH_MIN = 330 + +/** The column may never take more than 60% of the measured row. */ +export function workColumnWidthMax(available: number) { + return Math.max(WORK_COLUMN_WIDTH_MIN, Math.floor(available * 0.6)) +} + +/** `available` is undefined until the layout row is first measured; render the + * stored width untouched until then (same first-frame rule as the chat). */ +export function clampWorkColumnWidth(input: { width: number; available: number | undefined }) { + if (input.available === undefined) return input.width + return Math.min(Math.max(input.width, WORK_COLUMN_WIDTH_MIN), workColumnWidthMax(input.available)) +} + +/** In v2 with the work column visible, the chat is the flex remainder (no + * fixed pixel width). Classic layout keeps the historical fixed chat width. */ +export function sessionChatTakesRemainder(input: { newDesign: boolean; columnVisible: boolean }): boolean { + return input.newDesign && input.columnVisible +} diff --git a/packages/app-bundle/overlay/packages/app/src/pages/session/session-side-panel-structure.test.ts b/packages/app-bundle/overlay/packages/app/src/pages/session/session-side-panel-structure.test.ts new file mode 100644 index 00000000..10017048 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/pages/session/session-side-panel-structure.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, test } from "bun:test" +import { readFileSync } from "node:fs" +import { join } from "node:path" + +// amicode#105 structural ACs, locked against resurrection on the next upstream +// merge (this file is merge-hot — the ADR names these deletions deliberately): +// store_mirror_effects == 0 — no createEffect mirrors between the global +// vaultPanel store and the per-session reviewPanel +// vault_sidepanel_hosts == 0 — the vault's only host is the global drawer; +// the side-panel tab is retired +// Behavioral consequence: the sidebar-right toggle's store has a single writer, +// so its pressed state cannot disagree with the screen. +const source = readFileSync(join(import.meta.dir, "session-side-panel.tsx"), "utf8") + +describe("work column is vault-free (amicode#105)", () => { + test("no vaultPanel store references (no mirror effects, no vault tab logic)", () => { + expect(source).not.toContain("vaultPanel") + }) + + test("no vault tab trigger or content", () => { + expect(source).not.toContain('value="vault"') + expect(source).not.toContain("vaultOpen") + }) +}) diff --git a/packages/app-bundle/overlay/packages/app/src/pages/session/session-side-panel.tsx b/packages/app-bundle/overlay/packages/app/src/pages/session/session-side-panel.tsx new file mode 100644 index 00000000..49c81402 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/pages/session/session-side-panel.tsx @@ -0,0 +1,1025 @@ +import { For, Match, Show, Switch, createEffect, createMemo, createSignal, on, onCleanup, type JSX } from "solid-js" +import { createStore } from "solid-js/store" +import { createMediaQuery } from "@solid-primitives/media" +import { DragDropProvider as DndKitProvider, PointerSensor } from "@dnd-kit/solid" +import { isSortable } from "@dnd-kit/solid/sortable" +import { Accessibility, AutoScroller, Feedback, PointerActivationConstraints } from "@dnd-kit/dom" +import { RestrictToHorizontalAxis } from "@dnd-kit/abstract/modifiers" +import { RestrictToElement } from "@dnd-kit/dom/modifiers" +import { + DragDropProvider, + DragDropSensors, + DragOverlay, + SortableProvider, + closestCenter, + type DragEvent, +} from "@thisbeyond/solid-dnd" +import { Tabs } from "@opencode-ai/ui/tabs" +import { IconButton } from "@opencode-ai/ui/icon-button" +import { Icon } from "@opencode-ai/ui/icon" +import { TooltipKeybind } from "@opencode-ai/ui/tooltip" +import { ResizeHandle } from "@opencode-ai/ui/resize-handle" +import { Mark } from "@opencode-ai/ui/logo" +import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" +import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2" +import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" +import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" +import type { FileDiffInfo } from "@opencode-ai/client/promise" +import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd" + +import FileTree from "@/components/file-tree" +import { normalizeFileTreeV2Path } from "@/components/file-tree-v2-model" +import { SessionContextUsage } from "@/components/session-context-usage" +import { RunInspector } from "@/amicode/inspector/run-inspector" +import { useInspectorBridge } from "@/amicode/inspector/inspector-context" + +const reviewTabID = "session-side-panel-review-tab" +const reviewTabPanelID = "session-side-panel-review-tabpanel" +const fileBrowserTabPanelID = "session-side-panel-file-browser-tabpanel" +import { SessionContextTab, SortableTab, SortableTabV2, FileVisual, SessionPreviewTab, PanelMenu } from "@/components/session" +import type { PanelMenuItem } from "@/components/session/panel-menu" +import { OpenInAppV2 } from "@/components/session/open-in-app-v2" +import { useCommand } from "@/context/command" +import { useFile, type SelectedLineRange } from "@/context/file" +import { useLanguage } from "@/context/language" +import { useLayout } from "@/context/layout" +import { useSDK } from "@/context/sdk" +import { useSettings } from "@/context/settings" +import { createFileTabListSync } from "@/pages/session/file-tab-scroll" +import { FileTabContent } from "@/pages/session/file-tabs" +import { + SESSION_PREVIEW_TAB, + createOpenSessionFileTab, + createSessionTabs, + getTabReorderIndex, + shouldShowFileTree, + type Sizing, +} from "@/pages/session/helpers" +import { setSessionHandoff } from "@/pages/session/handoff" +import { useSessionLayout } from "@/pages/session/session-layout" +import { WORK_COLUMN_WIDTH_MIN } from "@/pages/session/session-panel-width" +import { SessionFileBrowserTab, type SessionFileBrowserState } from "@/pages/session/v2/session-file-browser-tab" + +type PulseInspectorStage = "optimization" | "calibration" | "compilation" + +function PulseInspectorContent() { + const bridge = useInspectorBridge() + const [stage, setStage] = createSignal("optimization") + + const hasActiveRun = createMemo(() => { + const r = bridge.runs() + if (r.size === 0) return false + const activeId = bridge.activeRunId() + const state = activeId ? r.get(activeId) : r.values().next().value + return state ? !state.completion : false + }) + + return ( +
+ {/* Stage segmented control */} +
+ + + +
+ + {/* Stage content */} + +
+ +
+
+
+ ) +} + +type ReviewDiff = FileDiffInfo | SnapshotFileDiff | VcsFileDiff +type RenderDiff = FileDiffInfo | (SnapshotFileDiff & { file: string }) | VcsFileDiff +const FILE_TREE_WIDTH_MIN = 240 + +function renderDiff(value: ReviewDiff): value is RenderDiff { + return typeof value.file === "string" +} + +export function SessionSidePanel(props: { + canReview: () => boolean + diffs: () => ReviewDiff[] + diffsReady: () => boolean + empty: () => string + hasReview: () => boolean + reviewHasFocusableContent: () => boolean + reviewCount: () => number + reviewPanel: () => JSX.Element + reviewSidebarToggle?: (disabled: boolean) => JSX.Element + fileBrowserState?: SessionFileBrowserState + activeDiff?: string + focusReviewDiff: (path: string) => void + reviewSnap: boolean + size: Sizing + stacked?: boolean + touchedFiles?: () => Array<{ file: string; status: string }> +}) { + const layout = useLayout() + const settings = useSettings() + const file = useFile() + const language = useLanguage() + const command = useCommand() + const sdk = useSDK() + const { sessionKey, tabs, view, params } = useSessionLayout() + const projectDirectory = createMemo(() => sdk().directory) + + const isDesktop = createMediaQuery("(min-width: 768px)") + const shown = settings.visibility.fileTree + + // the tabs-column open state (historically the review panel's flag — the + // persisted store key stays, the review UI is gone) + const tabsOpen = createMemo(() => isDesktop() && view().reviewPanel.opened()) + const fileOpen = createMemo( + () => + isDesktop() && + shouldShowFileTree({ + visible: shown(), + opened: layout.fileTree.opened(), + }), + ) + // upstream's review UI rides the same persisted store key as the fork's tabs + // column (reviewPanel.opened) — one memo, two names (the vault effects below + // use tabsOpen; upstream's review machinery uses reviewOpen) + const reviewOpen = tabsOpen + const open = createMemo(() => reviewOpen() || fileOpen()) + const fileTreeWidth = createMemo(() => Math.max(FILE_TREE_WIDTH_MIN, layout.fileTree.width())) + const reviewTab = createMemo(() => isDesktop()) + const panelWidth = createMemo(() => { + if (!open()) return "0px" + // the tabs column owns its width (never flex-fills the window) and can be + // dragged much narrower — Kate 2026-07-27 + if (reviewOpen()) return `${layout.panelColumn.width()}px` + return `${fileTreeWidth()}px` + }) + const treeWidth = createMemo(() => (fileOpen() ? `${fileTreeWidth()}px` : "0px")) + + const diffs = createMemo(() => props.diffs().filter(renderDiff)) + const diffFiles = createMemo(() => diffs().map((d) => d.file)) + const kinds = createMemo(() => { + const merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => { + if (!a) return b + if (a === b) return a + return "mix" as const + } + + const out = new Map() + for (const diff of diffs()) { + const file = normalizeFileTreeV2Path(diff.file) + const kind = diff.status === "added" ? "add" : diff.status === "deleted" ? "del" : "mix" + + out.set(file, kind) + + const parts = file.split("/") + for (const [idx] of parts.slice(0, -1).entries()) { + const dir = parts.slice(0, idx + 1).join("/") + if (!dir) continue + out.set(dir, merge(out.get(dir), kind)) + } + } + return out + }) + + const empty = (msg: string) => ( +
+
+
+
{msg}
+
+
+ ) + + const nofiles = createMemo(() => { + const state = file.tree.state("") + if (!state?.loaded) return false + return file.tree.children("").length === 0 + }) + + const normalizeTab = (tab: string) => { + if (!tab.startsWith("file://")) return tab + return file.tab(tab) + } + + const openReviewPanel = () => { + if (!view().reviewPanel.opened()) view().reviewPanel.open() + } + + const openTab = createOpenSessionFileTab({ + normalizeTab, + openTab: tabs().open, + pathFromTab: file.pathFromTab, + loadFile: file.load, + openReviewPanel, + setActive: tabs().setActive, + }) + + const tabState = createSessionTabs({ + tabs, + pathFromTab: file.pathFromTab, + normalizeTab, + review: reviewTab, + hasReview: props.canReview, + fileBrowser: () => !!props.fileBrowserState, + }) + const contextOpen = tabState.contextOpen + const previewOpen = tabState.previewOpen + const openFileOpen = tabState.openFileOpen + const panelTabs = tabState.panelTabs + const openedTabs = tabState.openedTabs + const activeTab = tabState.activeTab + const activeFileTab = tabState.activeFileTab + + const fileTreeTab = () => layout.fileTree.tab() + + const setFileTreeTabValue = (value: string) => { + if (value !== "changes" && value !== "all") return + layout.fileTree.setTab(value) + } + + const showAllFiles = () => { + if (fileTreeTab() !== "changes") return + layout.fileTree.setTab("all") + } + + let fileFilter: HTMLInputElement | undefined + let tabList: HTMLDivElement | undefined + const temporaryTab = tabs().preview + const previewTab = (value: string) => { + const next = normalizeTab(value) + tabs().previewTab(next) + const path = file.pathFromTab(next) + if (path) void file.load(path) + openReviewPanel() + queueMicrotask(() => tabs().setActive(next)) + } + const activateTab = (value: string) => { + const next = normalizeTab(value) + const path = file.pathFromTab(next) + if (path) void file.load(path) + openReviewPanel() + tabs().setActive(next) + } + const browserTab = createMemo(() => { + if (!props.fileBrowserState) return undefined + const active = activeTab() + if (active && file.pathFromTab(active)) return active + return activeFileTab() + }) + // Keep the file-browser shell mounted while any file tab exists. Kobalte briefly + // selects Review while the tab For replaces a preview trigger, which would + // otherwise dispose the sidebar and reset scroll. + const fileBrowserMounted = createMemo(() => { + if (!props.fileBrowserState) return false + return openedTabs().length > 0 || openFileOpen() || !!browserTab() + }) + const fileBrowserVisible = createMemo(() => { + const active = activeTab() + return active !== "review" && active !== "context" && active !== "empty" && active !== SESSION_PREVIEW_TAB + }) + + // Markdown files for the Preview tab — check both git diffs and tool-edit history + const hasMarkdownFiles = createMemo(() => { + if (diffs().some((d) => d.file.endsWith(".md"))) return true + const touched = props.touchedFiles?.() + if (touched?.some((t) => t.file.endsWith(".md"))) return true + return false + }) + + // Panel menu items — Files Changed lives as the "review" trigger, Context and + // Preview are the secondary tabs, and Pulse Inspector is the third requested tab. + const panelMenuItems = createMemo((): PanelMenuItem[] => [ + { + id: "context", + label: "Context", + icon: "brain", + available: () => true, + active: contextOpen, + }, + { + id: "pulseInspector", + label: "Pulse Inspector", + icon: "pulse", + available: () => true, + active: () => activeTab() === "pulseInspector", + }, + { + id: SESSION_PREVIEW_TAB, + label: "Preview", + icon: "eye", + available: () => true, + active: previewOpen, + }, + ]) + + const handlePanelMenuSelect = (id: string) => { + tabs().open(id) + tabs().setActive(id) + openReviewPanel() + } + + const closeTabKeybind = createMemo(() => command.keybindParts("tab.close")) + const [store, setStore] = createStore({ + activeDraggable: undefined as string | undefined, + }) + + const handleDragStart = (event: unknown) => { + const id = getDraggableId(event) + if (!id) return + setStore("activeDraggable", id) + } + + const handleDragOver = (event: DragEvent) => { + const { draggable, droppable } = event + if (!draggable || !droppable) return + + const currentTabs = tabs().all() + const toIndex = getTabReorderIndex(currentTabs, draggable.id.toString(), droppable.id.toString()) + if (toIndex === undefined) return + tabs().move(draggable.id.toString(), toIndex) + } + + const handleDragEnd = () => { + setStore("activeDraggable", undefined) + } + + createEffect(() => { + if (!file.ready()) return + + setSessionHandoff(sessionKey(), { + files: tabs() + .all() + .reduce>((acc, tab) => { + const path = file.pathFromTab(tab) + if (!path) return acc + + const selected = file.selectedLines(path) + acc[path] = + selected && typeof selected === "object" && "start" in selected && "end" in selected + ? (selected as SelectedLineRange) + : null + + return acc + }, {}), + }) + }) + + return ( + +
+
+ + + ) +} diff --git a/packages/app-bundle/overlay/packages/app/src/pages/session/timeline/last-prompt-bubble.test.ts b/packages/app-bundle/overlay/packages/app/src/pages/session/timeline/last-prompt-bubble.test.ts new file mode 100644 index 00000000..29dcebfd --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/pages/session/timeline/last-prompt-bubble.test.ts @@ -0,0 +1,314 @@ +import { describe, expect, test } from "bun:test" +import type { Part, TextPart, UserMessage } from "@opencode-ai/sdk/v2" + +/** + * Mirrors the logic inside MessageTimeline's `visiblePromptBubble` memo (amicode#271). + * + * Uses the viewport's startIndex: a user message is "above the viewport" when + * its row index is less than startIndex. The memo is triggered by scrollTop + * (a signal), and reads virtualizer.range.startIndex for the actual check. + */ +function findVisiblePromptBubble( + userMessages: UserMessage[], + partsFor: (id: string) => Part[], + rowIndexFor: (id: string) => number | undefined, + viewportStartIndex: number | undefined, +): { text: string; messageId: string } | undefined { + if (viewportStartIndex === undefined) return undefined + for (let i = userMessages.length - 1; i >= 0; i--) { + const msg = userMessages[i] + const rowIndex = rowIndexFor(msg.id) + if (rowIndex === undefined) continue + if (rowIndex < viewportStartIndex) { + const parts = partsFor(msg.id) + const textPart = parts.find( + (p): p is TextPart => p.type === "text" && !(p as TextPart).synthetic, + ) + const text = textPart?.text?.trim() + if (text) return { text, messageId: msg.id } + // No text in this message (e.g. skill-only) — keep looking + continue + } + } + return undefined +} + +const userMsg = (id: string) => ({ id, role: "user" }) as UserMessage + +const textPart = (text: string, synthetic = false): TextPart => + ({ + id: `prt_${text.slice(0, 4)}`, + sessionID: "ses_1", + messageID: "msg_1", + type: "text", + text, + synthetic, + }) as TextPart + +describe("last-prompt bubble", () => { + test("shows the most recent user message above the viewport start", () => { + const messages = [userMsg("msg_1"), userMsg("msg_2"), userMsg("msg_3")] + const parts: Record = { + msg_1: [textPart("first prompt")], + msg_2: [textPart("second prompt")], + msg_3: [textPart("third prompt")], + } + const rowIndices: Record = { msg_1: 0, msg_2: 5, msg_3: 10 } + + // Viewport starts at row 7 → msg_1 (row 0) and msg_2 (row 5) are above + // Most recent above viewport = msg_2 + const result = findVisiblePromptBubble( + messages, + (id) => parts[id] ?? [], + (id) => rowIndices[id], + 7, + ) + expect(result).toEqual({ text: "second prompt", messageId: "msg_2" }) + }) + + test("shows last message when viewport is past all messages", () => { + const messages = [userMsg("msg_1"), userMsg("msg_2")] + const parts: Record = { + msg_1: [textPart("first")], + msg_2: [textPart("second")], + } + const rowIndices: Record = { msg_1: 0, msg_2: 3 } + + // Viewport starts at row 20 → both messages above + const result = findVisiblePromptBubble( + messages, + (id) => parts[id] ?? [], + (id) => rowIndices[id], + 20, + ) + expect(result).toEqual({ text: "second", messageId: "msg_2" }) + }) + + test("returns undefined when all messages are still visible", () => { + const messages = [userMsg("msg_1")] + const parts: Record = { msg_1: [textPart("prompt")] } + const rowIndices: Record = { msg_1: 5 } + + // Viewport starts at row 0 → message at row 5 is still in/below view + expect( + findVisiblePromptBubble(messages, (id) => parts[id] ?? [], (id) => rowIndices[id], 0), + ).toBeUndefined() + }) + + test("returns undefined when range is undefined (not mounted)", () => { + const messages = [userMsg("msg_1")] + expect( + findVisiblePromptBubble(messages, () => [], () => 0, undefined), + ).toBeUndefined() + }) + + test("returns undefined for empty messages", () => { + expect( + findVisiblePromptBubble([], () => [], () => undefined, 10), + ).toBeUndefined() + }) + + test("skips synthetic text parts", () => { + const messages = [userMsg("msg_1")] + const parts: Record = { + msg_1: [textPart("synthetic content", true), textPart("real prompt")], + } + const rowIndices: Record = { msg_1: 0 } + + const result = findVisiblePromptBubble( + messages, + (id) => parts[id] ?? [], + (id) => rowIndices[id], + 5, + ) + expect(result).toEqual({ text: "real prompt", messageId: "msg_1" }) + }) + + test("trims whitespace from extracted text", () => { + const messages = [userMsg("msg_1")] + const parts: Record = { msg_1: [textPart(" padded text ")] } + const rowIndices: Record = { msg_1: 0 } + + const result = findVisiblePromptBubble( + messages, + (id) => parts[id] ?? [], + (id) => rowIndices[id], + 5, + ) + expect(result?.text).toBe("padded text") + }) + + test("updates as viewport scrolls past different messages", () => { + const messages = [userMsg("msg_1"), userMsg("msg_2"), userMsg("msg_3")] + const parts: Record = { + msg_1: [textPart("alpha")], + msg_2: [textPart("beta")], + msg_3: [textPart("gamma")], + } + const rowIndices: Record = { msg_1: 0, msg_2: 10, msg_3: 20 } + const lookup = (id: string) => parts[id] ?? [] + const rowLookup = (id: string) => rowIndices[id] + + // Scrolled just past msg_1 + expect(findVisiblePromptBubble(messages, lookup, rowLookup, 3)?.text).toBe("alpha") + // Scrolled past msg_2 + expect(findVisiblePromptBubble(messages, lookup, rowLookup, 15)?.text).toBe("beta") + // Scrolled past msg_3 + expect(findVisiblePromptBubble(messages, lookup, rowLookup, 25)?.text).toBe("gamma") + }) + + test("streaming case: message goes above viewport as content grows", () => { + // The agent is streaming. Auto-scroll keeps the viewport at the bottom. + // The user's message started visible but is now above the viewport. + const messages = [userMsg("msg_1")] + const parts: Record = { msg_1: [textPart("fix the bug in auth.ts")] } + const rowIndices: Record = { msg_1: 0 } + + // Initially: viewport starts at row 0 → message at row 0 is NOT above + expect( + findVisiblePromptBubble(messages, (id) => parts[id] ?? [], (id) => rowIndices[id], 0), + ).toBeUndefined() + + // After streaming: viewport has moved to row 5 → message at row 0 IS above + const result = findVisiblePromptBubble( + messages, + (id) => parts[id] ?? [], + (id) => rowIndices[id], + 5, + ) + expect(result).toEqual({ text: "fix the bug in auth.ts", messageId: "msg_1" }) + }) + + test("existing session opened at bottom: message far above viewport shows bubble", () => { + // Key case: open a long existing session anchored at the bottom. + // User message is at row 2, viewport starts at row 400. + const messages = [userMsg("msg_1"), userMsg("msg_2")] + const parts: Record = { + msg_1: [textPart("initial question")], + msg_2: [textPart("follow-up question")], + } + const rowIndices: Record = { msg_1: 2, msg_2: 200 } + + // Viewport at row 400 → both messages above + const result = findVisiblePromptBubble( + messages, + (id) => parts[id] ?? [], + (id) => rowIndices[id], + 400, + ) + expect(result).toEqual({ text: "follow-up question", messageId: "msg_2" }) + }) + + test("skips skill-only messages and shows the previous message with text", () => { + // User sent a real message, then a skill-only command (e.g. /tdd). + // The bubble should show the real message, not disappear. + const messages = [userMsg("msg_1"), userMsg("msg_2"), userMsg("msg_3")] + const parts: Record = { + msg_1: [textPart("implement the auth flow")], + msg_2: [textPart("add some tests for it")], + // msg_3 is a skill invocation — has a skill part but no meaningful text + msg_3: [{ id: "prt_skill", sessionID: "ses_1", messageID: "msg_3", type: "skill", name: "tdd" } as unknown as Part], + } + const rowIndices: Record = { msg_1: 0, msg_2: 8, msg_3: 16 } + + // Viewport at row 20 → all messages above. msg_3 has no text, so falls through to msg_2 + const result = findVisiblePromptBubble( + messages, + (id) => parts[id] ?? [], + (id) => rowIndices[id], + 20, + ) + expect(result).toEqual({ text: "add some tests for it", messageId: "msg_2" }) + }) + + test("skips multiple consecutive skill-only messages", () => { + const messages = [userMsg("msg_1"), userMsg("msg_2"), userMsg("msg_3")] + const parts: Record = { + msg_1: [textPart("the real question")], + msg_2: [], // empty parts + msg_3: [], // empty parts + } + const rowIndices: Record = { msg_1: 0, msg_2: 5, msg_3: 10 } + + const result = findVisiblePromptBubble( + messages, + (id) => parts[id] ?? [], + (id) => rowIndices[id], + 15, + ) + expect(result).toEqual({ text: "the real question", messageId: "msg_1" }) + }) +}) + +/** + * Mirrors the `findPreviousBubble` logic: when the user clicks the bubble + * showing message N, find message N-1 (the previous user message with text). + */ +function findPreviousBubble( + currentMessageId: string, + userMessages: UserMessage[], + partsFor: (id: string) => Part[], +): { text: string; messageId: string } | undefined { + const currentIdx = userMessages.findIndex((m) => m.id === currentMessageId) + if (currentIdx <= 0) return undefined + for (let i = currentIdx - 1; i >= 0; i--) { + const msg = userMessages[i] + const parts = partsFor(msg.id) + const textPart = parts.find( + (p): p is TextPart => p.type === "text" && !(p as TextPart).synthetic, + ) + const text = textPart?.text?.trim() + if (text) return { text, messageId: msg.id } + } + return undefined +} + +describe("click-to-previous (bubble override on click)", () => { + test("clicking bubble showing msg_2 switches to msg_1", () => { + const messages = [userMsg("msg_1"), userMsg("msg_2"), userMsg("msg_3")] + const parts: Record = { + msg_1: [textPart("first question")], + msg_2: [textPart("second question")], + msg_3: [textPart("third question")], + } + + const result = findPreviousBubble("msg_3", messages, (id) => parts[id] ?? []) + expect(result).toEqual({ text: "second question", messageId: "msg_2" }) + }) + + test("clicking bubble showing the first message returns undefined (hides bubble)", () => { + const messages = [userMsg("msg_1"), userMsg("msg_2")] + const parts: Record = { + msg_1: [textPart("only message")], + msg_2: [textPart("second")], + } + + const result = findPreviousBubble("msg_1", messages, (id) => parts[id] ?? []) + expect(result).toBeUndefined() + }) + + test("skips skill-only messages when finding previous", () => { + const messages = [userMsg("msg_1"), userMsg("msg_2"), userMsg("msg_3")] + const parts: Record = { + msg_1: [textPart("real question")], + msg_2: [], // skill-only, no text + msg_3: [textPart("latest question")], + } + + // Clicking msg_3 should skip msg_2 (no text) and land on msg_1 + const result = findPreviousBubble("msg_3", messages, (id) => parts[id] ?? []) + expect(result).toEqual({ text: "real question", messageId: "msg_1" }) + }) + + test("returns undefined when all previous messages are skill-only", () => { + const messages = [userMsg("msg_1"), userMsg("msg_2"), userMsg("msg_3")] + const parts: Record = { + msg_1: [], // no text + msg_2: [], // no text + msg_3: [textPart("latest")], + } + + const result = findPreviousBubble("msg_3", messages, (id) => parts[id] ?? []) + expect(result).toBeUndefined() + }) +}) diff --git a/packages/app-bundle/overlay/packages/app/src/pages/session/timeline/message-timeline.tsx b/packages/app-bundle/overlay/packages/app/src/pages/session/timeline/message-timeline.tsx new file mode 100644 index 00000000..8be15638 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/pages/session/timeline/message-timeline.tsx @@ -0,0 +1,2227 @@ +import { + createEffect, + createMemo, + createResource, + createSignal, + For, + Index, + on, + onCleanup, + onMount, + Show, + type Accessor, + type JSX, +} from "solid-js" +import { createStore, produce } from "solid-js/store" +import { Dynamic } from "solid-js/web" +import { useNavigate, useParams } from "@solidjs/router" +import { useMutation } from "@tanstack/solid-query" +import { createVirtualizer, defaultRangeExtractor, elementScroll, type VirtualItem } from "@tanstack/solid-virtual" +import { Accordion } from "@opencode-ai/ui/accordion" +import { AmicodeEntityRail } from "@opencode-ai/ui/amicode-entity-rail" +import { ThinkingLine, turnTokens } from "@opencode-ai/ui/amicode-thinking" +import { + AmicodeEntityView, + entityLabel, + parseProblemResponse, + parseRunStatusResponse, +} from "@opencode-ai/ui/amicode-entity-view" +import { parseDashboardResponse, type DashboardState } from "@opencode-ai/ui/amicode-widget-grid" +import { Button } from "@opencode-ai/ui/button" +import { Card } from "@opencode-ai/ui/card" +import { + ContextToolGroup, + Message, + MessageDivider, + Part as MessagePart, + partDefaultOpen, + type UserActions, +} from "@opencode-ai/session-ui/message-part" +import { DiffChanges } from "@opencode-ai/ui/diff-changes" +import { FileIcon } from "@opencode-ai/ui/file-icon" +import { Icon } from "@opencode-ai/ui/icon" +import { IconButton } from "@opencode-ai/ui/icon-button" +import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" +import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" +import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2" +import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" +import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" +import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2" +import { Dialog } from "@opencode-ai/ui/dialog" +import { DialogFooter, DialogHeader, DialogTitleGroup, DialogV2 } from "@opencode-ai/ui/v2/dialog-v2" +import { InlineInput } from "@opencode-ai/ui/inline-input" +import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" +import { SessionRetry } from "@opencode-ai/session-ui/session-retry" +import { isScrollKeyTarget, scrollKey, scrollKeyOwner, ScrollView } from "@opencode-ai/ui/scroll-view" +import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header" +import { TextField } from "@opencode-ai/ui/text-field" +import { TextReveal } from "@opencode-ai/ui/text-reveal" +import type { + AssistantMessage, + Message as MessageType, + Part as PartType, + TextPart, + ToolPart, + UserMessage, +} from "@opencode-ai/sdk/v2" +import { showToast } from "@/utils/toast" +import { getDirectory, getFilename } from "@opencode-ai/core/util/path" +import { Popover as KobaltePopover } from "@kobalte/core/popover" +import { normalize } from "@opencode-ai/session-ui/session-diff" +import { useFileComponent } from "@opencode-ai/ui/context/file" +import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture" +import { SessionContextUsage } from "@/components/session-context-usage" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { useLanguage } from "@/context/language" +import { useCommand } from "@/context/command" +import { useSessionKey } from "@/pages/session/session-layout" +import { useServerSDK } from "@/context/server-sdk" +import { usePlatform } from "@/context/platform" +import { useServer } from "@/context/server" +import { usePrompt } from "@/context/prompt" +import { useSettings } from "@/context/settings" +import { useTabs } from "@/context/tabs" +import { amicodeGet, amicodePost } from "@/utils/amicode-fetch" +import { draftPrompt } from "@/utils/start-prompt" +import { inAmicode, postAmicode } from "@/pages/session/use-amicode-commands" +import { legacySessionHref, requireServerKey, sessionHref } from "@/utils/session-route" +import { useSDK } from "@/context/sdk" +import { useSync } from "@/context/sync" +import { notifySessionTabsRemoved } from "@/components/titlebar-session-events" +import { sessionTitle } from "@/utils/session-title" +import { scheduleConnectedMeasure } from "./measure" +import { observeElementOffsetReconnectAware } from "./observe-element-offset" +import { createTimelineProjection } from "./projection" +import { MessageComment, SummaryDiff, TimelineRow, TimelineRowMap } from "./rows" +import { filterVirtualIndexes } from "./virtual-items" + +const emptyMessages: MessageType[] = [] +const emptyParts: PartType[] = [] +const emptyTools: ToolPart[] = [] +const emptyAssistantMessages: AssistantMessage[] = [] +const idle = { type: "idle" as const } + +type FramedTimelineRow = Exclude +type TimelineRowByTag = Extract + +const timelineFallbackItemSize = 60 +const timelineCache = new Map }>() + +const taskDescription = (part: PartType, sessionID: string) => { + if (part.type !== "tool" || part.tool !== "task") return + const metadata = "metadata" in part.state ? part.state.metadata : undefined + if (metadata?.sessionId !== sessionID) return + const value = part.state.input?.description + if (typeof value === "string" && value) return value +} + +const boundaryTarget = (root: HTMLElement, target: EventTarget | null) => { + const current = target instanceof Element ? target : undefined + const nested = current?.closest("[data-scrollable]") + if (!nested || nested === root) return root + if (!(nested instanceof HTMLElement)) return root + return nested +} + +const markBoundaryGesture = (input: { + root: HTMLDivElement + target: EventTarget | null + delta: number + onMarkScrollGesture: (target?: EventTarget | null) => void +}) => { + const target = boundaryTarget(input.root, input.target) + if (target === input.root) { + input.onMarkScrollGesture(input.root) + return + } + if ( + shouldMarkBoundaryGesture({ + delta: input.delta, + scrollTop: target.scrollTop, + scrollHeight: target.scrollHeight, + clientHeight: target.clientHeight, + }) + ) { + input.onMarkScrollGesture(input.root) + } +} + +function TimelineThinkingRow(props: { reasoningHeading?: string; showReasoningSummaries: boolean; tokens?: number }) { + // Simple version - just show the ThinkingLine without collapsible dropdown + return ( +
+ +
+ ) +} + +function TimelineDiffSummaryRow(props: { diffs: SummaryDiff[] }) { + const language = useLanguage() + const maxFiles = 10 + const [state, setState] = createStore({ + showAll: false, + expanded: [] as string[], + }) + const showAll = () => state.showAll + const expanded = () => state.expanded + const overflow = createMemo(() => Math.max(0, props.diffs.length - maxFiles)) + const visible = createMemo(() => (showAll() ? props.diffs : props.diffs.slice(0, maxFiles))) + + return ( +
+
+ + {language.t( + props.diffs.length === 1 ? "ui.sessionTurn.diffs.changed.one" : "ui.sessionTurn.diffs.changed.other", + { count: String(props.diffs.length) }, + )} + + + 0}> + setState("showAll", !showAll())}> + {showAll() ? language.t("ui.sessionTurn.diffs.showLess") : language.t("ui.sessionTurn.diffs.showAll")} + + +
+
+ setState("expanded", Array.isArray(value) ? value : value ? [value] : [])} + > + + {(diff) => { + const opened = createMemo(() => expanded().includes(diff.file)) + + return ( + + + +
+ + + {`\u202A${getDirectory(diff.file)}\u202C`} + + {getFilename(diff.file)} + +
+ + + + + + +
+
+
+
+ + + + + +
+ ) + }} +
+
+ 0}> +
setState("showAll", true)}> + {language.t("ui.sessionTurn.diffs.more", { count: String(overflow()) })} +
+
+
+
+ ) +} + +function TimelineDiffView(props: { diff: SummaryDiff }) { + const fileComponent = useFileComponent() + const view = normalize(props.diff) + + return ( +
+ +
+ ) +} + +export function MessageTimeline(props: { + actions?: UserActions + scroll: { overflow: boolean; bottom: boolean; jump: boolean } + onResumeScroll: () => void + setScrollRef: (el: HTMLDivElement | undefined) => void + onScheduleScrollState: (el: HTMLDivElement) => void + onAutoScrollHandleScroll: () => void + onMarkScrollGesture: (target?: EventTarget | null) => void + hasScrollGesture: () => boolean + onUserScroll: () => void + onHistoryScroll: () => void + onAutoScrollInteraction: (event: MouseEvent) => void + shouldAnchorBottom: () => boolean + centered: boolean + setContentRef: (el: HTMLDivElement) => void + userMessages: UserMessage[] + anchor: (id: string) => string + setRevealMessage?: (fn: (id: string) => void) => void + setScrollToEnd?: (fn: () => void) => void + setHistoryAnchor?: (handlers: { capture: () => void; restore: (done: boolean) => void }) => void +}) { + let touchGesture: number | undefined + + const navigate = useNavigate() + const serverSDK = useServerSDK() + const sdk = useSDK() + const sync = useSync() + const settings = useSettings() + const tabs = useTabs() + const dialog = useDialog() + const language = useLanguage() + const command = useCommand() + const { params, sessionKey } = useSessionKey() + + // ── amicode: problem-UI wiring (ported from the pre-merge message-timeline; + // the mounts died with that file — see AMICODE-PATCHES.md "Upstream sync + // 2026-08-01"). The app owns transport (per-active-server Basic auth via + // amicodeGet) + the ring-2 entity dialog; the rail owns its own polling. + // Dialog resources are gated on the open signal, cleared on close. + const server = useServer() + const prompt = usePrompt() + const [entityViewOpen, setEntityViewOpen] = createSignal(false) + const [problemRaw, { refetch: refetchProblem }] = createResource( + () => (entityViewOpen() ? 1 : undefined), + () => amicodeGet(server.current, "/amicode/problem"), + ) + const problemView = createMemo(() => { + if (problemRaw.error) return parseProblemResponse({ ok: false, error: language.t("amicode.fetchFailed") }) + const raw = problemRaw.latest + return raw === undefined ? undefined : parseProblemResponse(raw) + }) + // Capability warrants (spec-20260727-164748 §9.5). Fetched unconditionally rather + // than gated on a dialog: the approval card lives IN the transcript, so it needs + // its state on first paint. Cheap — one ledger read, and the card's state derives + // from these rows rather than from optimistic local state, so a press round-trips + // through the ledger before it reads as granted. + const [warrantsRaw, { refetch: refetchWarrants }] = createResource(() => + amicodeGet(server.current, "/amicode/warrants").catch(() => undefined), + ) + const warrants = createMemo(() => { + const raw = warrantsRaw.latest as { ok?: boolean; warrants?: unknown } | undefined + if (!raw?.ok || !Array.isArray(raw.warrants)) return [] + // Tolerant per row: a malformed row is dropped, never defaulted into a warrant. + return raw.warrants.flatMap((r) => { + const row = r as Record + if (typeof row.plan_hash !== "string" || typeof row.expires_at !== "string") return [] + return [{ + plan_hash: row.plan_hash, + bounds: (typeof row.bounds === "object" && row.bounds !== null ? row.bounds : {}) as Record, + expires_at: row.expires_at, + issued_by: typeof row.issued_by === "string" ? row.issued_by : "unknown", + // Omitted when the server did not send it, so the rail chip drops the count + // rather than rendering a wrong "0 of N". + ...(typeof row.solves_used === "number" ? { solves_used: row.solves_used } : {}), + }] + }) + }) + // Run verdict data for the entity view: /amicode/run-status is fetched + // whenever a dialog opens (cheap, server-cached ~1s); only the Run dialog + // reads it. Same endpoint the rail's run chip polls. + const [runStatusRaw] = createResource( + () => (entityViewOpen() ? 1 : undefined), + () => amicodeGet(server.current, "/amicode/run-status"), + ) + const entityRunStatus = createMemo(() => parseRunStatusResponse(runStatusRaw.latest)) + const openEntityView = (kind: string, seq?: number) => { + setEntityViewOpen(true) + dialog.show( + () => ( + + {/* fit-to-content goes near-fullscreen on run entities (long paths + + history) — cap the panel and let it scroll internally instead */} +
+ { + dialog.close() + draftPrompt(prompt, text) + }} + onRetry={() => void refetchProblem()} + retryLabel={language.t("amicode.retry")} + editLabel={language.t("amicode.editInChat")} + /> +
+
+ ), + () => setEntityViewOpen(false), + ) + } + + const ownerSessionKey = sessionKey() + const cached = timelineCache.get(ownerSessionKey) + const initialMeasurements = cached?.measurements + const coldBottomMount = !initialMeasurements?.length && props.shouldAnchorBottom() + // Hide the scroll container for the first frame on cold-bottom-mount to prevent + // a flash of content at the top before scrollToEnd fires. + const [scrollReady, setScrollReady] = createSignal(!coldBottomMount) + const platform = usePlatform() + + const [listRoot, setListRoot] = createSignal() + const sessionID = createMemo(() => params.id) + const sessionStatus = createMemo(() => { + const id = sessionID() + if (!id) return idle + return sync().data.session_status[id] ?? idle + }) + const sessionMessages = createMemo(() => (sessionID() ? (sync().data.message[sessionID()!] ?? []) : [])) + const projectedMessages = createMemo(() => { + const id = sessionID() + if (!id) return [] + const visible = new Set(props.userMessages.map((message) => message.id)) + const boundary = sessionMessages().find((message) => message.role === "user" && !visible.has(message.id))?.id + const messages = sync().data.session_message[id] ?? [] + return boundary ? messages.filter((message) => message.id < boundary) : messages + }) + const info = createMemo(() => { + const id = sessionID() + if (!id) return + return sync().session.get(id) + }) + const titleValue = createMemo(() => info()?.title) + const titleLabel = createMemo(() => sessionTitle(titleValue())) + const shareUrl = createMemo(() => info()?.share?.url) + const shareEnabled = createMemo(() => sync().data.config.share !== "disabled") + const parentID = createMemo(() => info()?.parentID) + const parent = createMemo(() => { + const id = parentID() + if (!id) return + return sync().session.get(id) + }) + const parentMessages = createMemo(() => { + const id = parentID() + if (!id) return emptyMessages + return sync().data.message[id] ?? emptyMessages + }) + const parentTitle = createMemo(() => sessionTitle(parent()?.title) ?? language.t("command.session.new")) + const getMsgParts = (msgId: string) => sync().data.part[msgId] ?? emptyParts + + // amicode: live token count for a turn's assistant messages — feeds the + // thinking line's token chip on the Thinking row (the recovered wave mount). + const assistantTokensForTurn = (userMessageID: string) => { + const msgs = sessionMessages() + const start = msgs.findIndex((m) => m.id === userMessageID) + if (start === -1) return 0 + return turnTokens(msgs.slice(start + 1).filter((m) => m.role === "assistant")) + } + const getMsgPart = (messageID: string, partID: string) => getMsgParts(messageID).find((part) => part.id === partID) + const childTaskDescription = createMemo(() => { + const id = sessionID() + if (!id) return + return parentMessages() + .flatMap((message) => getMsgParts(message.id)) + .map((part) => taskDescription(part, id)) + .findLast((value): value is string => !!value) + }) + const childTitle = createMemo(() => { + if (!parentID()) return titleLabel() ?? "" + if (childTaskDescription()) return childTaskDescription() + const value = titleLabel()?.replace(/\s+\(@[^)]+ subagent\)$/, "") + if (value) return value + return language.t("command.session.new") + }) + const showHeader = createMemo(() => !!(titleValue() || parentID())) + const projection = createTimelineProjection({ + messages: sessionMessages, + userMessages: () => props.userMessages, + sessionMessages: projectedMessages, + parts: getMsgParts, + status: sessionStatus, + showReasoningSummaries: settings.general.showReasoningSummaries, + inlineComments: settings.general.newLayoutDesigns, + }) + const activeMessageID = projection.activeMessageID + const assistantMessagesByParent = projection.assistantMessagesByParent + const lastAssistantGroupKey = projection.lastAssistantGroupKey + const messageByID = projection.messageByID + const messageLastRowIndex = projection.messageLastRowIndex + const messageRowIndex = projection.messageRowIndex + const timelineRowByKey = projection.rowByKey + const timelineRows = projection.rows + + let prependAnchor: { key: string; offset: number } | undefined + let prependAnchorFrame: number | undefined + let prependLoading = false + const clearPrependAnchor = () => { + prependLoading = false + prependAnchor = undefined + if (prependAnchorFrame === undefined) return + cancelAnimationFrame(prependAnchorFrame) + prependAnchorFrame = undefined + } + const capturePrependAnchor = () => { + prependLoading = true + updatePrependAnchor() + } + const updatePrependAnchor = () => { + const root = listRoot() + if (!root) return + const view = root.getBoundingClientRect() + const anchor = [...root.querySelectorAll("[data-timeline-key]")] + .map((element) => ({ element, rect: element.getBoundingClientRect() })) + .filter((item) => item.rect.bottom > view.top && item.rect.top < view.bottom) + .sort((a, b) => a.rect.top - b.rect.top)[0] + if (!anchor) return + if (!anchor.element.dataset.timelineKey) return + prependAnchor = { key: anchor.element.dataset.timelineKey, offset: anchor.rect.top - view.top } + } + const restorePrependAnchor = (done: boolean) => { + if (done) prependLoading = false + applyPrependAnchor() + } + const applyPrependAnchor = () => { + const root = listRoot() + if (!root || !prependAnchor) return + if (prependAnchorFrame !== undefined) cancelAnimationFrame(prependAnchorFrame) + let frames = 0 + let stable = 0 + const apply = () => { + prependAnchorFrame = undefined + const anchor = prependAnchor + if (!anchor) return + const element = root.querySelector(`[data-timeline-key="${CSS.escape(anchor.key)}"]`) + const delta = element + ? element.getBoundingClientRect().top - root.getBoundingClientRect().top - anchor.offset + : undefined + if (delta !== undefined && Math.abs(delta) > 0.5) { + root.scrollTop += delta + stable = 0 + } else { + stable += 1 + } + frames += 1 + if (stable >= 30 || frames >= 180) { + if (!prependLoading) prependAnchor = undefined + return + } + prependAnchorFrame = requestAnimationFrame(apply) + } + prependAnchorFrame = requestAnimationFrame(apply) + } + + const [toolOpen, setToolOpen] = createStore>(cached?.toolOpen ?? {}) + const [renderOverscan, setRenderOverscan] = createSignal(initialMeasurements?.length || coldBottomMount ? 6 : 20) + let resizePinnedIndexes: number[] = [] + let resizePinFrame: number | undefined + let virtualContent: HTMLDivElement | undefined + const virtualizer = createVirtualizer({ + get count() { + return timelineRows().length + }, + getScrollElement: () => listRoot() ?? null, + observeElementOffset: observeElementOffsetReconnectAware, + initialOffset: () => (props.shouldAnchorBottom() ? Number.MAX_SAFE_INTEGER : 0), + initialMeasurementsCache: initialMeasurements, + estimateSize: () => timelineFallbackItemSize, + scrollToFn: (offset, options, instance) => { + // Expose the computed range before core writes an anchor correction so the browser does not clamp it to the old height. + if (virtualContent) virtualContent.style.height = `${instance.getTotalSize()}px` + elementScroll(offset, options, instance) + }, + get getItemKey() { + const rows = timelineRows() + return (index: number) => { + const row = rows[index] + // ResizeObserver can report a removed element after its row has left the projection. + if (!row) return `removed:${index}` + return TimelineRow.key(row) + } + }, + anchorTo: "end", + get followOnAppend() { + return props.shouldAnchorBottom() && !props.hasScrollGesture() + }, + scrollEndThreshold: 80, + get scrollMargin() { + return showHeader() ? 64 : 0 + }, + overscan: 50, + paddingEnd: 64, + rangeExtractor: (range) => { + const id = activeMessageID() + const active = id ? (messageLastRowIndex().get(id) ?? -1) : -1 + const indexes = defaultRangeExtractor({ ...range, overscan: renderOverscan() }) + return filterVirtualIndexes( + [...new Set([...resizePinnedIndexes, ...indexes, ...(active < 0 ? [] : [active])])].sort((a, b) => a - b), + range.count, + ) + }, + }) + const resizeItem = virtualizer.resizeItem + let resizeAnchorScheduled = false + const anchorResizedBottom = () => { + if (resizeAnchorScheduled || props.hasScrollGesture()) return + resizeAnchorScheduled = true + queueMicrotask(() => { + resizeAnchorScheduled = false + if (!props.shouldAnchorBottom() || props.hasScrollGesture()) return + virtualizer.scrollToEnd() + }) + } + virtualizer.resizeItem = (index, size) => { + const item = virtualizer.measurementsCache[index] + const previous = item ? (virtualizer.itemSizeCache.get(item.key) ?? item.size) : undefined + const root = listRoot() + if (root && previous !== undefined && Math.abs(size - previous) > root.clientHeight) { + const view = root.getBoundingClientRect() + resizePinnedIndexes = [...root.querySelectorAll("[data-index]")] + .filter((element) => { + const rect = element.getBoundingClientRect() + return rect.bottom > view.top && rect.top < view.bottom + }) + .map((element) => Number(element.dataset.index)) + if (resizePinFrame !== undefined) cancelAnimationFrame(resizePinFrame) + resizePinFrame = requestAnimationFrame(() => { + resizePinFrame = requestAnimationFrame(() => { + resizePinFrame = undefined + resizePinnedIndexes = [] + }) + }) + } + resizeItem(index, size) + if (root && props.shouldAnchorBottom()) anchorResizedBottom() + } + virtualizer.shouldAdjustScrollPositionOnItemSizeChange = (item) => { + if (props.shouldAnchorBottom()) return false + const first = virtualizer.range?.startIndex + return first !== undefined && item.index < first + } + const virtualItemByKey = createMemo( + () => new Map(virtualizer.getVirtualItems().map((item) => [item.key, item] as const)), + ) + const virtualRowKeys = createMemo(() => virtualizer.getVirtualItems().map((item) => item.key as string)) + // amicode#271: a signal tracking the scroll container's scrollTop, updated on + // every scroll event. Drives the last-prompt bubble reactivity. + const [scrollTop, setScrollTop] = createSignal(0) + createEffect(() => { + const root = listRoot() + if (root) setScrollTop(root.scrollTop) + }) + // The bubble state: which user message text to show and link to. + // Computed from scroll position, but explicitly overridden on click. + const [bubbleOverride, setBubbleOverride] = createSignal<{ text: string; messageId: string } | undefined>(undefined) + const computeBubble = (): { text: string; messageId: string } | undefined => { + const range = virtualizer.range + if (!range) return undefined + const startIndex = range.startIndex + const messages = props.userMessages + for (let i = messages.length - 1; i >= 0; i--) { + const msg = messages[i] + const rowIndex = messageRowIndex().get(msg.id) + if (rowIndex === undefined) continue + if (rowIndex < startIndex) { + const parts = getMsgParts(msg.id) + const textPart = parts.find((p): p is TextPart => p.type === "text" && !(p as TextPart).synthetic) + const text = textPart?.text?.trim() + if (text) return { text, messageId: msg.id } + continue + } + } + return undefined + } + // Recompute on scroll; clears any override so the bubble tracks scroll position + const visiblePromptBubble = createMemo<{ text: string; messageId: string } | undefined>(() => { + scrollTop() + return computeBubble() + }) + // What the bubble actually shows: override (from click) takes priority + const activeBubble = () => bubbleOverride() ?? visiblePromptBubble() + + // Find the previous user message with text before a given message ID + const findPreviousBubble = (currentMessageId: string): { text: string; messageId: string } | undefined => { + const messages = props.userMessages + const currentIdx = messages.findIndex((m) => m.id === currentMessageId) + if (currentIdx <= 0) return undefined + for (let i = currentIdx - 1; i >= 0; i--) { + const msg = messages[i] + const parts = getMsgParts(msg.id) + const textPart = parts.find((p): p is TextPart => p.type === "text" && !(p as TextPart).synthetic) + const text = textPart?.text?.trim() + if (text) return { text, messageId: msg.id } + } + return undefined + } + + // Flag to suppress override clearing during programmatic scrolls from bubble click + let bubbleScrolling = false + + const scrollToBubbleMessage = () => { + const bubble = activeBubble() + if (!bubble) return + const rowIndex = messageRowIndex().get(bubble.messageId) + if (rowIndex !== undefined) { + const prev = findPreviousBubble(bubble.messageId) + setBubbleOverride(prev) + bubbleScrolling = true + virtualizer.scrollToIndex(rowIndex, { align: "center" }) + // Allow the programmatic scroll to settle before re-enabling override clearing + setTimeout(() => { bubbleScrolling = false }, 300) + } + } + createEffect(() => { + props.setRevealMessage?.((id) => { + const index = messageRowIndex().get(id) + if (index === undefined) return + virtualizer.scrollToIndex(index, { align: "center" }) + }) + props.setScrollToEnd?.(() => virtualizer.scrollToEnd()) + props.setHistoryAnchor?.({ capture: capturePrependAnchor, restore: restorePrependAnchor }) + }) + + let overscanFrame: number | undefined + onMount(() => { + // Single scroll to end after initial render to prevent flicker + overscanFrame = requestAnimationFrame(() => { + overscanFrame = undefined + if (renderOverscan() < 20) setRenderOverscan(20) + if (props.shouldAnchorBottom()) virtualizer.scrollToEnd() + if (!scrollReady()) setScrollReady(true) + }) + }) + + const maybeAnchorBottom = () => { + if (timelineRows().length === 0) return + if (!props.shouldAnchorBottom() || props.hasScrollGesture()) return + if (resizePinFrame !== undefined) cancelAnimationFrame(resizePinFrame) + clearPrependAnchor() + if (prependAnchorFrame !== undefined) cancelAnimationFrame(prependAnchorFrame) + virtualizer.scrollToEnd() + } + + let measuredSessionKey = sessionKey() + createEffect(() => { + const key = sessionKey() + timelineRows().length + if (measuredSessionKey !== key) { + measuredSessionKey = key + virtualizer.measure() + } + maybeAnchorBottom() + }) + + onCleanup(() => { + clearPrependAnchor() + timelineCache.delete(ownerSessionKey) + timelineCache.set(ownerSessionKey, { measurements: virtualizer.takeSnapshot(), toolOpen: { ...toolOpen } }) + while (timelineCache.size > 16) timelineCache.delete(timelineCache.keys().next().value!) + if (resizePinFrame !== undefined) cancelAnimationFrame(resizePinFrame) + if (overscanFrame !== undefined) cancelAnimationFrame(overscanFrame) + props.setRevealMessage?.(() => {}) + props.setScrollToEnd?.(() => {}) + props.setHistoryAnchor?.({ capture: () => {}, restore: () => {} }) + }) + + const [title, setTitle] = createStore({ + draft: "", + editing: false, + menuOpen: false, + pendingRename: false, + pendingShare: false, + }) + let titleRef: HTMLInputElement | undefined + + const [share, setShare] = createStore({ + open: false, + dismiss: null as "escape" | "outside" | null, + }) + let more: HTMLButtonElement | undefined + + const bindListRoot = (root: HTMLDivElement) => { + if (root === listRoot()) return + setListRoot(root) + props.setScrollRef(root) + } + + const handleListWheel = (event: WheelEvent & { currentTarget: HTMLDivElement }) => { + if (!prependLoading) clearPrependAnchor() + const root = event.currentTarget + const delta = normalizeWheelDelta({ + deltaY: event.deltaY, + deltaMode: event.deltaMode, + rootHeight: root.clientHeight, + }) + if (!delta) return + markBoundaryGesture({ root, target: event.target, delta, onMarkScrollGesture: props.onMarkScrollGesture }) + } + + const handleListTouchStart = (event: TouchEvent) => { + if (!prependLoading) clearPrependAnchor() + touchGesture = event.touches[0]?.clientY + } + + const handleListTouchMove = (event: TouchEvent & { currentTarget: HTMLDivElement }) => { + const next = event.touches[0]?.clientY + const prev = touchGesture + touchGesture = next + if (next === undefined || prev === undefined) return + + const delta = prev - next + if (!delta) return + + markBoundaryGesture({ + root: event.currentTarget, + target: event.target, + delta, + onMarkScrollGesture: props.onMarkScrollGesture, + }) + } + + const handleListTouchEnd = () => { + touchGesture = undefined + } + + const handleListPointerDown = (event: PointerEvent & { currentTarget: HTMLDivElement }) => { + if (!prependLoading) clearPrependAnchor() + props.onMarkScrollGesture(event.target) + } + + const handleListPointerMove = (event: PointerEvent) => { + if (event.buttons !== 1) return + props.onMarkScrollGesture(event.target) + } + + const handleListKeyDown = (event: KeyboardEvent & { currentTarget: HTMLDivElement }) => { + const key = scrollKey(event) + if (!key) return + if (!isScrollKeyTarget(event.target, key)) return + if (scrollKeyOwner(event.currentTarget, event.target, key) !== event.currentTarget) return + if (!prependLoading) clearPrependAnchor() + props.onMarkScrollGesture(event.currentTarget) + } + + const handleListScroll = (event: Event & { currentTarget: HTMLDivElement }) => { + if (prependLoading) updatePrependAnchor() + props.onScheduleScrollState(event.currentTarget) + props.onHistoryScroll() + // amicode#271: update scroll offset for the last-prompt bubble + setScrollTop(event.currentTarget.scrollTop) + if (!props.hasScrollGesture()) return + // User-initiated scroll — clear any click override so bubble tracks position + // (but not if we're mid-programmatic scroll from a bubble click) + if (!bubbleScrolling) setBubbleOverride(undefined) + props.onUserScroll() + props.onAutoScrollHandleScroll() + props.onMarkScrollGesture(event.currentTarget) + } + + onCleanup(() => { + props.setScrollRef(undefined) + }) + + const viewShare = () => { + const url = shareUrl() + if (!url) return + platform.openExternal(url) + } + + const errorMessage = (err: unknown) => { + if (err && typeof err === "object" && "data" in err) { + const data = (err as { data?: { message?: string } }).data + if (data?.message) return data.message + } + if (err instanceof Error) return err.message + return language.t("common.requestFailed") + } + + const shareMutation = useMutation(() => ({ + mutationFn: (id: string) => serverSDK().client.session.share({ sessionID: id }), + onError: (err) => { + console.error("Failed to share session", err) + }, + })) + + const unshareMutation = useMutation(() => ({ + mutationFn: (id: string) => serverSDK().client.session.unshare({ sessionID: id }), + onError: (err) => { + console.error("Failed to unshare session", err) + }, + })) + + const titleMutation = useMutation(() => ({ + mutationFn: (input: { id: string; title: string }) => + sdk().api.session.rename({ sessionID: input.id, title: input.title }), + onSuccess: (_, input) => { + sync().set( + produce((draft) => { + const index = draft.session.findIndex((s) => s.id === input.id) + if (index !== -1) draft.session[index].title = input.title + }), + ) + setTitle("editing", false) + }, + onError: (err) => { + showToast({ + title: language.t("common.requestFailed"), + description: errorMessage(err), + }) + }, + })) + + const shareSession = () => { + const id = sessionID() + if (!id || shareMutation.isPending) return + if (!shareEnabled()) return + shareMutation.mutate(id) + } + + const unshareSession = () => { + const id = sessionID() + if (!id || unshareMutation.isPending) return + if (!shareEnabled()) return + unshareMutation.mutate(id) + } + const copyShareUrl = () => { + const url = shareUrl() + if (!url) return + void navigator.clipboard + .writeText(url) + .then(() => + showToast({ + variant: "success", + icon: "circle-check", + title: language.t("session.share.copy.copied"), + description: url, + }), + ) + .catch((err: unknown) => + showToast({ + title: language.t("common.requestFailed"), + description: errorMessage(err), + }), + ) + } + const selectShareUrlText: JSX.EventHandler = (event) => { + const selection = window.getSelection() + if (!selection) return + const range = document.createRange() + range.selectNodeContents(event.currentTarget) + selection.removeAllRanges() + selection.addRange(range) + } + + createEffect( + on( + sessionKey, + () => + setTitle({ + draft: "", + editing: false, + menuOpen: false, + pendingRename: false, + pendingShare: false, + }), + { defer: true }, + ), + ) + + createEffect( + on( + () => [parentID(), childTaskDescription()] as const, + ([id, description]) => { + if (!id || description) return + if (sync().data.message[id] !== undefined) return + void sync().session.sync(id) + }, + { defer: true }, + ), + ) + + const openTitleEditor = () => { + if (!sessionID() || parentID()) return + setTitle({ editing: true, draft: titleLabel() ?? "" }) + requestAnimationFrame(() => { + if (!titleRef) return + titleRef.focus() + titleRef.select() + }) + } + + const closeTitleEditor = () => { + if (titleMutation.isPending) return + setTitle("editing", false) + } + + const saveTitleEditor = () => { + const id = sessionID() + if (!id) return + if (titleMutation.isPending) return + + const next = title.draft.trim() + if (!next || next === (titleLabel() ?? "")) { + setTitle("editing", false) + return + } + + titleMutation.mutate({ id, title: next }) + } + + const navigateAfterSessionRemoval = (sessionID: string, parentID?: string, nextSessionID?: string) => { + if (params.id !== sessionID) return + const href = (id: string) => + params.serverKey ? sessionHref(requireServerKey(params.serverKey), id) : legacySessionHref(sdk().directory, id) + if (parentID) { + navigate(href(parentID)) + return + } + if (nextSessionID) { + navigate(href(nextSessionID)) + return + } + if (params.serverKey) { + tabs.newDraft({ server: requireServerKey(params.serverKey), directory: sdk().directory }) + return + } + navigate(`/${params.dir}/session`) + } + + const archiveSession = async (sessionID: string) => { + const session = sync().session.get(sessionID) + if (!session) return + if ((await sdk().protocol) !== "v1") return + + const sessions = sync().data.session ?? [] + const index = sessions.findIndex((s) => s.id === sessionID) + const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1]) + + await sdk() + .client.session.update({ sessionID, directory: sdk().directory, time: { archived: Date.now() } }) + .then(() => { + sync().set( + produce((draft) => { + const index = draft.session.findIndex((s) => s.id === sessionID) + if (index !== -1) draft.session.splice(index, 1) + }), + ) + sync().session.evict(sessionID) + navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id) + notifySessionTabsRemoved({ directory: sdk().directory, sessionIDs: [sessionID] }) + }) + .catch((err) => { + showToast({ + title: language.t("common.requestFailed"), + description: errorMessage(err), + }) + }) + } + + const unarchiveSession = async (sessionID: string) => { + if ((await sdk().protocol) !== "v1") return + await (sdk().client.session.update as Function)({ sessionID, directory: sdk().directory, time: { archived: null } }) + .then(() => void sync().session.sync(sessionID, { force: true })) + .catch((err: unknown) => { + showToast({ + title: language.t("common.requestFailed"), + description: errorMessage(err), + }) + }) + } + + const deleteSession = async (sessionID: string) => { + const session = sync().session.get(sessionID) + if (!session) return false + + const sessions = (sync().data.session ?? []).filter((s) => !s.parentID && !s.time?.archived) + const index = sessions.findIndex((s) => s.id === sessionID) + const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1]) + + const result = await sdk() + .api.session.remove({ sessionID }) + .then(() => true) + .catch((err) => { + showToast({ + title: language.t("session.delete.failed.title"), + description: errorMessage(err), + }) + return false + }) + + if (!result) return false + + const removed = new Set([sessionID]) + const byParent = new Map() + for (const item of sync().data.session) { + const parentID = item.parentID + if (!parentID) continue + const existing = byParent.get(parentID) + if (existing) { + existing.push(item.id) + continue + } + byParent.set(parentID, [item.id]) + } + + const stack = [sessionID] + while (stack.length) { + const parentID = stack.pop() + if (!parentID) continue + + const children = byParent.get(parentID) + if (!children) continue + + for (const child of children) { + if (removed.has(child)) continue + removed.add(child) + stack.push(child) + } + } + + navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id) + + sync().set( + produce((draft) => { + draft.session = draft.session.filter((s) => !removed.has(s.id)) + }), + ) + + for (const id of removed) { + sync().session.evict(id) + } + notifySessionTabsRemoved({ directory: sdk().directory, sessionIDs: [...removed] }) + return true + } + + const navigateParent = () => { + const id = parentID() + if (!id) return + navigate( + params.serverKey ? sessionHref(requireServerKey(params.serverKey), id) : legacySessionHref(sdk().directory, id), + ) + } + + function DialogDeleteSession(props: { sessionID: string }) { + const name = createMemo( + () => sessionTitle(sync().session.get(props.sessionID)?.title) ?? language.t("command.session.new"), + ) + const handleDelete = async () => { + await deleteSession(props.sessionID) + dialog.close() + } + + if (settings.general.newLayoutDesigns()) + return ( + + + + + + dialog.close()}> + {language.t("common.cancel")} + + + {language.t("session.delete.button")} + + + + ) + + return ( + +
+
+ + {language.t("session.delete.confirm", { name: name() })} + +
+
+ + +
+
+
+ ) + } + + const workingTurn = (userMessageID: string) => sessionStatus().type !== "idle" && activeMessageID() === userMessageID + + const turnDurationMs = (userMessageID: string) => { + const message = messageByID().get(userMessageID) + if (!message || message.role !== "user") return + const end = (assistantMessagesByParent().get(userMessageID) ?? emptyAssistantMessages).reduce( + (max, item) => { + const completed = item.time.completed + if (typeof completed !== "number") return max + if (max === undefined) return completed + return Math.max(max, completed) + }, + undefined, + ) + if (typeof end !== "number") return + if (end < message.time.created) return + return end - message.time.created + } + + const assistantCopyPartID = (userMessageID: string) => { + if (workingTurn(userMessageID)) return null + const messages = assistantMessagesByParent().get(userMessageID) ?? emptyAssistantMessages + + for (let i = messages.length - 1; i >= 0; i--) { + const message = messages[i] + if (!message) continue + + const parts = getMsgParts(message.id) + for (let j = parts.length - 1; j >= 0; j--) { + const part = parts[j] + if (!part || part.type !== "text" || !part.text?.trim()) continue + return part.id + } + } + } + + const renderAssistantPartGroup = (row: Accessor, onSizeChange?: () => void) => { + if (row().group.type === "context") { + const parts = createMemo(() => { + const group = row().group + if (group.type !== "context") return emptyTools + return group.refs + .map((ref) => getMsgPart(ref.messageID, ref.partID)) + .filter((part): part is ToolPart => part?.type === "tool") + }) + const contextOpenKey = () => `context:${row().group.key}` + const open = createMemo(() => { + return toolOpen[contextOpenKey()] === true + }) + + return ( + setToolOpen(contextOpenKey(), value)} + busy={ + workingTurn(row().userMessageID) && lastAssistantGroupKey().get(row().userMessageID) === row().group.key + } + onSizeChange={onSizeChange} + /> + ) + } + + const message = createMemo(() => { + const group = row().group + if (group.type !== "part") return + return messageByID().get(group.ref.messageID) + }) + const part = createMemo(() => { + const group = row().group + if (group.type !== "part") return + return getMsgPart(group.ref.messageID, group.ref.partID) + }) + const defaultOpen = createMemo(() => { + const item = part() + if (!item) return + return partDefaultOpen(item, settings.general.shellToolPartsExpanded(), settings.general.editToolPartsExpanded()) + }) + + return ( + + {(message) => ( + + {(part) => ( + setToolOpen(part().id, open)} + deferToolContent + virtualizeDiff={false} + onContentRendered={onSizeChange} + /> + )} + + )} + + ) + } + + function TimelineRowFrame(input: { row: Accessor; children: JSX.Element }) { + const anchor = () => { + const row = input.row() + return row._tag === "CommentStrip" || (row._tag === "UserMessage" && row.anchor) + } + const previousAssistantPart = () => { + const row = input.row() + return row._tag === "AssistantPart" && row.previousAssistantPart + } + + return ( +
+
+ {input.children} +
+
+ ) + } + + const renderTimelineRow = (row: Accessor, onSizeChange?: () => void) => { + switch (row()._tag) { + case "TurnGap": + return

+ } + > + { + titleRef = el + }} + data-slot="session-title-child" + value={title.draft} + disabled={titleMutation.isPending} + classList={{ + "block text-[13px] font-[530] leading-4 tracking-[-0.04px] text-v2-text-text-base": true, + "w-full flex-1 grow-1 min-w-0 pl-1 -ml-1 rounded-[6px]": !settings.general.newLayoutDesigns(), + "field-sizing-content self-start rounded-[6px] px-2 py-1 ": + settings.general.newLayoutDesigns(), + }} + style={{ + "--inline-input-shadow": settings.general.newLayoutDesigns() + ? "none" + : "var(--shadow-xs-border-select)", + }} + onInput={(event) => setTitle("draft", event.currentTarget.value)} + onKeyDown={(event) => { + event.stopPropagation() + if (event.key === "Enter") { + event.preventDefault() + void saveTitleEditor() + return + } + if (event.key === "Escape") { + event.preventDefault() + closeTitleEditor() + } + }} + onBlur={closeTitleEditor} + /> + + + + + + {(id) => ( +
+ + {language.t("command.session.compact")}} + > + command.trigger("session.compact", "palette")} + aria-label={language.t("command.session.compact")} + icon={} + /> + + + { + setTitle("menuOpen", open) + if (open) return + }} + > + { + more = el + }} + /> + + { + if (title.pendingRename) { + event.preventDefault() + setTitle("pendingRename", false) + openTitleEditor() + return + } + if (title.pendingShare) { + event.preventDefault() + requestAnimationFrame(() => { + setShare({ open: true, dismiss: null }) + setTitle("pendingShare", false) + }) + } + }} + > + { + setTitle("pendingRename", true) + setTitle("menuOpen", false) + }} + > + {language.t("common.rename")} + + + { + setTitle({ pendingShare: true, menuOpen: false }) + }} + > + + {language.t("session.share.action.share")} + + + + void archiveSession(id)}> + {language.t("common.archive")} + + } + > + void unarchiveSession(id)}> + {language.t("common.unarchive")} + + + + dialog.show(() => )} + > + {language.t("common.delete")} + + + + + } + > + { + setTitle("menuOpen", open) + if (open) return + }} + > + } + variant="ghost-muted" + size="large" + state={share.open || title.pendingShare ? "pressed" : undefined} + aria-label={language.t("common.moreOptions")} + aria-expanded={title.menuOpen || share.open || title.pendingShare} + ref={(el: HTMLButtonElement) => { + more = el + }} + /> + + { + if (title.pendingRename) { + event.preventDefault() + setTitle("pendingRename", false) + openTitleEditor() + return + } + if (title.pendingShare) { + event.preventDefault() + requestAnimationFrame(() => { + setShare({ open: true, dismiss: null }) + setTitle("pendingShare", false) + }) + } + }} + > + { + setTitle("pendingRename", true) + setTitle("menuOpen", false) + }} + > + {language.t("common.rename")} + + + { + setTitle({ pendingShare: true, menuOpen: false }) + }} + > + {language.t("session.share.action.share")}... + + + void archiveSession(id)}> + {language.t("common.archive")} + + } + > + void unarchiveSession(id)}> + {language.t("common.unarchive")} + + + + dialog.show(() => )}> + {language.t("common.delete")}... + + + + + + + more} + placement="bottom-end" + gutter={settings.general.newLayoutDesigns() ? 6 : 4} + modal={false} + onOpenChange={(open) => { + if (open) setShare("dismiss", null) + setShare("open", open) + }} + > + + { + setShare({ dismiss: "escape", open: false }) + event.preventDefault() + event.stopPropagation() + }} + onPointerDownOutside={() => { + setShare({ dismiss: "outside", open: false }) + }} + onFocusOutside={() => { + setShare({ dismiss: "outside", open: false }) + }} + onCloseAutoFocus={(event) => { + if (share.dismiss === "outside") event.preventDefault() + setShare("dismiss", null) + }} + > + +
+
+ {language.t("session.share.popover.title")} +
+
+ {shareUrl() + ? language.t("session.share.popover.description.shared") + : language.t("session.share.popover.description.unshared")} +
+
+
+ + {shareMutation.isPending + ? language.t("session.share.action.publishing") + : language.t("session.share.action.publish")} + + } + > +
+ +
+ + +
+
+
+
+
+ } + > +
+
+ {language.t("session.share.popover.title")} +
+
+ {shareUrl() + ? language.t("session.share.popover.description.shared") + : language.t("session.share.popover.description.unshared")} +
+
+
+ + {shareMutation.isPending + ? language.t("session.share.action.publishing") + : language.t("session.share.action.publish")} + + } + > +
+
+
+ {shareUrl()} +
+ } + aria-label={language.t("session.share.copy.copyLink")} + onClick={copyShareUrl} + /> + } + aria-label={language.t("session.share.action.view")} + onClick={viewShare} + disabled={unshareMutation.isPending} + /> +
+
+ + {unshareMutation.isPending + ? language.t("session.share.action.unpublishing") + : language.t("session.share.action.unpublish")} + +
+
+
+
+
+ + + + + + )} + + + {/* amicode: problem-header rail (renders only when the session has + amicode_* parts) + ask/ui bridges — ported from the pre-merge + message-timeline (AMICODE-PATCHES.md "Upstream sync 2026-08-01") */} + amicodeGet(server.current, "/amicode/problem")} + fetchRunStatus={() => amicodeGet(server.current, "/amicode/run-status")} + fetchRunSeries={(run, lab) => + amicodeGet( + server.current, + `/amicode/run-series?run=${encodeURIComponent(run)}${lab ? `&lab=${encodeURIComponent(lab)}` : ""}`, + ) + } + // Disable rail in chat sessions to prevent showing in unrelated sessions (issue #272) + disabled={true} + widgetHost={{ + // Stage 2: the in-chat widget preview reuses the home grid's + // frame kernel; server context is resolved live per call. + frameSrc: (id, hash) => { + const url = server.current?.http.url + return url + ? new URL( + `/amicode/widget-frame?id=${encodeURIComponent(id)}&h=${encodeURIComponent(hash)}`, + url, + ).toString() + : "" + }, + callbacks: { + fetch: (path) => amicodeGet(server.current, path), + action: async () => ({ ok: true }), + prompt: (text) => { + const id = sessionID() + if (id) void sdk().client.session.promptAsync({ sessionID: id, parts: [{ type: "text", text }] }) + }, + open: () => {}, + }, + // pin = GET current dashboard, append this widget if absent, POST + // the full state back (applySave treats the body as the whole + // state, so a partial POST would wipe other tiles). + pin: async (id) => { + const raw = await amicodeGet(server.current, "/amicode/dashboard") + const state: DashboardState = parseDashboardResponse(raw) ?? { version: 1, widget: [] } + if (!state.widget.some((e) => e.id === id)) + state.widget = [...state.widget, { key: id, id, hidden: false, config: {} }] + return amicodePost(server.current, "/amicode/dashboard", state) + }, + }} + onOpenEntity={openEntityView} + onDraftPrompt={(text) => draftPrompt(prompt, text)} + editLabel={language.t("amicode.editInChat")} + onInspectRun={inAmicode() ? () => postAmicode("amicode.openInspector") : undefined} + retryLabel={language.t("amicode.retry")} + unavailableLabel={language.t("amicode.unavailable")} + onAsk={(text) => { + const id = sessionID() + if (!id) return + void sdk().client.session.promptAsync({ sessionID: id, parts: [{ type: "text", text }] }) + }} + // Warrant transport (spec-20260727-164748 §9.5). NOT routed through + // onAsk on purpose: an approval delivered as a chat message would be + // read by the agent, which would then write the ledger row, leaving + // the only provenance as "the agent says the user approved". + warrants={() => warrants() ?? []} + onApprove={(request) => { + void amicodePost(server.current, "/amicode/approve", { + plan_hash: request.plan_hash, + bounds: request.bounds, + }) + // Refetch so the card flips to "granted" from the LEDGER rather + // than from optimistic local state — same discipline as deriving + // state from the durable log in the first place. + .then(() => void refetchWarrants()) + .catch(() => void refetchWarrants()) + }} + /> + {/* amicode#271: bubble inside the header — naturally below the + title row + chip rail */} + + {(bubble) => ( +
+ +
+ )} +
+ + + {/* amicode#271: no-header fallback (new untitled sessions only) */} + + {(_) => { + const bubble = () => activeBubble()! + return ( +
+ +
+ ) + }} +
+
{ + virtualContent = element + props.setContentRef(element) + }} + style={{ + height: `${virtualizer.getTotalSize()}px`, + position: "relative", + width: "100%", + }} + > + {(rowKey) => } + 0}> + + +
+ ) +} diff --git a/packages/app-bundle/overlay/packages/app/src/pages/session/use-amicode-commands.test.ts b/packages/app-bundle/overlay/packages/app/src/pages/session/use-amicode-commands.test.ts new file mode 100644 index 00000000..0c768fd0 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/pages/session/use-amicode-commands.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, spyOn, test } from "bun:test" +import { inAmicode, postAmicode } from "./use-amicode-commands" + +// The entity rail's pulse chip (its one inspector entry) and the Amico command +// palette both reach the VS Code extension through postAmicode(). chat_panel.ts relays the +// envelope to the host and executes ONLY commands on its BRIDGE_ALLOWED_COMMANDS +// allowlist — so the exact envelope shape and command string are a contract. +describe("postAmicode bridge envelope", () => { + test('posts {source:"amicode", kind:"command", command} to window.parent with "*"', () => { + const spy = spyOn(window.parent, "postMessage").mockImplementation(() => {}) + try { + postAmicode("amicode.openInspector") + expect(spy).toHaveBeenCalledTimes(1) + // Cast past the DOM postMessage overloads (which type arg 2 as + // WindowPostMessageOptions) — postAmicode passes a legacy string origin. + const [message, targetOrigin] = spy.mock.calls[0] as unknown as [unknown, unknown] + expect(message).toEqual({ source: "amicode", kind: "command", command: "amicode.openInspector" }) + // Post to any origin — chat_panel.ts pins the origin on the receiving side. + expect(targetOrigin).toBe("*") + } finally { + spy.mockRestore() + } + }) + + test("never throws even if the parent frame rejects the post", () => { + const spy = spyOn(window.parent, "postMessage").mockImplementation(() => { + throw new Error("no parent") + }) + try { + expect(() => postAmicode("amicode.openInspector")).not.toThrow() + } finally { + spy.mockRestore() + } + }) +}) + +// inAmicode() gates the button (and the palette ops) so they never render in the +// public web / share build, where there is no extension host to relay to. In a +// non-framed context self === top, so it must report false. +describe("inAmicode gate", () => { + test("false when not framed (self === top)", () => { + expect(inAmicode()).toBe(false) + }) +}) diff --git a/packages/app-bundle/overlay/packages/app/src/pages/session/use-amicode-commands.tsx b/packages/app-bundle/overlay/packages/app/src/pages/session/use-amicode-commands.tsx new file mode 100644 index 00000000..610be391 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/pages/session/use-amicode-commands.tsx @@ -0,0 +1,76 @@ +import { useCommand, type CommandOption } from "@/context/command" +import { inAmicode, postAmicode } from "@/utils/amicode-bridge" + +// amicode: ops commands bridged to the VS Code extension host. Registered on BOTH +// the session and the new-session (draft) pages so they work everywhere inside +// Amicode — the draft page has no command palette (DialogSelectFile needs session +// context), so restart/update-memory carry direct keybinds, which the global +// command keydown handler fires with no dialog. The rest stay palette-discoverable +// inside a session. Shown only when framed. en-only by design. +// +// Each command posts {source:"amicode",kind:"command",command} to window.parent; +// chat_panel.ts relays it to an ALLOWLISTED vscode command. + +// The bridge helpers live in @/utils/amicode-bridge (extracted so surfaces like +// the report-a-bug button can post without this registry); re-exported here for +// the existing importers (pulse chip, prompt inputs) and their contract test. +export { inAmicode, postAmicode } + +export function useAmicodeCommands() { + const command = useCommand() + const amico = (option: Omit): CommandOption => ({ ...option, category: "Amico" }) + + // Not gated on inAmicode: the Vault panel talks straight to the server's + // /amicode/vault-* routes, so it works in any host. + command.register("amicode-vault", () => [ + amico({ + id: "vault.toggle", + title: "Toggle vault panel", + description: "Browse the attached vault mounts and read notes inline", + onSelect: () => { + void import("@/context/vault-panel").then((m) => m.vaultPanel.toggle()) + }, + }), + ]) + + command.register("amicode", () => + inAmicode() + ? [ + amico({ + id: "amicode.restartServer", + title: "Restart Amico server", + description: "Relaunch the opencode server — picks up a rebuilt binary", + keybind: "mod+alt+r", + onSelect: () => postAmicode("amicode.restartServer"), + }), + amico({ + id: "amicode.distillNow", + title: "Update my memory now", + description: "Run the background distiller over recent work", + keybind: "mod+alt+m", + onSelect: () => postAmicode("amicode.distillNow"), + }), + amico({ + id: "amicode.stopRun", + title: "Stop current solve", + onSelect: () => postAmicode("amicode.stopRun"), + }), + amico({ + id: "amicode.savePulse", + title: "Save pulse from current run", + onSelect: () => postAmicode("amicode.savePulse"), + }), + amico({ + id: "amicode.openRunDir", + title: "Open current run directory", + onSelect: () => postAmicode("amicode.openRunDir"), + }), + amico({ + id: "amicode.openInspector", + title: "Open pulse inspector", + onSelect: () => postAmicode("amicode.openInspector"), + }), + ] + : [], + ) +} diff --git a/packages/app-bundle/overlay/packages/app/src/pages/session/use-session-commands.tsx b/packages/app-bundle/overlay/packages/app/src/pages/session/use-session-commands.tsx new file mode 100644 index 00000000..4d52e70c --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/pages/session/use-session-commands.tsx @@ -0,0 +1,614 @@ +import { useNavigate } from "@solidjs/router" +import { useCommand, type CommandOption } from "@/context/command" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { previewSelectedLines } from "@opencode-ai/session-ui/pierre/selection-bridge" +import { useFile, selectionFromLines, type FileSelection, type SelectedLineRange } from "@/context/file" +import { useLanguage } from "@/context/language" +import { useLayout } from "@/context/layout" +import { usePermission } from "@/context/permission" +import { usePlatform } from "@/context/platform" +import { usePrompt } from "@/context/prompt" +import { useSDK } from "@/context/sdk" +import { useSettings } from "@/context/settings" +import { useSync } from "@/context/sync" +import { useTerminal } from "@/context/terminal" +import { showToast } from "@/utils/toast" +import { findLast } from "@opencode-ai/core/util/array" +import { createSessionTabs } from "@/pages/session/helpers" +import { extractPromptFromParts } from "@/utils/prompt" +import { UserMessage } from "@opencode-ai/sdk/v2" +import { useSessionLayout } from "@/pages/session/session-layout" +import { createSessionOwnership } from "./session-ownership" +import { useLocal } from "@/context/local" + +export type SessionCommandContext = { + navigateMessageByOffset: (offset: number) => void + setActiveMessage: (message: UserMessage | undefined) => void + focusInput: () => void + review?: () => boolean + fileBrowser?: () => boolean +} + +const withCategory = (category: string) => { + return (option: Omit): CommandOption => ({ + ...option, + category, + }) +} + +export const useSessionCommands = (actions: SessionCommandContext) => { + const command = useCommand() + const dialog = useDialog() + const file = useFile() + const language = useLanguage() + const permission = usePermission() + const platform = usePlatform() + const prompt = usePrompt() + const sdk = useSDK() + const settings = useSettings() + const sync = useSync() + const terminal = useTerminal() + const layout = useLayout() + const local = useLocal() + const navigate = useNavigate() + const { params, sessionKey, tabs, view } = useSessionLayout() + const sessionOwnership = createSessionOwnership(sessionKey) + const openDialog = async (load: () => Promise, show: (value: T) => void) => { + const owner = sessionOwnership.capture() + const value = await load() + owner.run(() => show(value)) + } + const runCommand = async (input: { + owner: ReturnType["capture"]> + prompt: T + request: () => Promise + updatePrompt: (prompt: T) => void + updateViewport: () => void + }) => { + await input.request() + input.updatePrompt(input.prompt) + input.owner.run(input.updateViewport) + } + + const info = () => { + const id = params.id + if (!id) return + return sync().session.get(id) + } + const hasReview = () => !!params.id + const normalizeTab = (tab: string) => { + if (!tab.startsWith("file://")) return tab + return file.tab(tab) + } + const tabState = createSessionTabs({ + tabs, + pathFromTab: file.pathFromTab, + normalizeTab, + review: actions.review, + hasReview, + fileBrowser: actions.fileBrowser, + }) + const activeFileTab = tabState.activeFileTab + const closableTab = tabState.closableTab + const shown = settings.visibility.fileTree + + const messages = () => { + const id = params.id + if (!id) return [] + return sync().data.message[id] ?? [] + } + const userMessages = () => messages().filter((m) => m.role === "user") as UserMessage[] + const visibleUserMessages = () => { + const revert = info()?.revert?.messageID + if (!revert) return userMessages() + return userMessages().filter((m) => m.id < revert) + } + + const showAllFiles = () => { + if (layout.fileTree.tab() !== "changes") return + layout.fileTree.setTab("all") + } + + const selectionPreview = (path: string, selection: FileSelection) => { + const content = file.get(path)?.content?.content + if (!content) return undefined + return previewSelectedLines(content, { start: selection.startLine, end: selection.endLine }) + } + + const addSelectionToContext = (path: string, selection: FileSelection) => { + const preview = selectionPreview(path, selection) + prompt.context.add({ type: "file", path, selection, preview }) + } + + const canAddSelectionContext = () => { + const tab = activeFileTab() + if (!tab) return false + const path = file.pathFromTab(tab) + if (!path) return false + return file.selectedLines(path) != null + } + + const navigateMessageByOffset = actions.navigateMessageByOffset + const setActiveMessage = actions.setActiveMessage + const focusInput = actions.focusInput + + const sessionCommand = withCategory(language.t("command.category.session")) + const fileCommand = withCategory(language.t("command.category.file")) + const contextCommand = withCategory(language.t("command.category.context")) + const viewCommand = withCategory(language.t("command.category.view")) + const terminalCommand = withCategory(language.t("command.category.terminal")) + const mcpCommand = withCategory(language.t("command.category.mcp")) + const permissionsCommand = withCategory(language.t("command.category.permissions")) + + const isAutoAcceptActive = () => { + const sessionID = params.id + if (sessionID) return permission.isAutoAccepting(sessionID, sdk().directory) + return permission.isAutoAcceptingDirectory(sdk().directory) + } + const write = async (value: string) => { + // Amicode webview: native copy and navigator.clipboard don't reach the OS + // clipboard the ⌘V paste bridge reads back, so mirror through the host + // first. Resolves false on plain web (unframed) / desktop, where the + // native paths below already work. + if (platform.writeClipboardText && (await platform.writeClipboardText(value))) return true + + const body = typeof document === "undefined" ? undefined : document.body + if (body) { + const textarea = document.createElement("textarea") + textarea.value = value + textarea.setAttribute("readonly", "") + textarea.style.position = "fixed" + textarea.style.opacity = "0" + textarea.style.pointerEvents = "none" + body.appendChild(textarea) + textarea.select() + const copied = document.execCommand("copy") + body.removeChild(textarea) + if (copied) return true + } + + const clipboard = typeof navigator === "undefined" ? undefined : navigator.clipboard + if (!clipboard?.writeText) return false + return clipboard.writeText(value).then( + () => true, + () => false, + ) + } + + const copyShare = async (url: string, existing: boolean) => { + if (!(await write(url))) { + showToast({ + title: language.t("toast.session.share.copyFailed.title"), + variant: "error", + }) + return + } + + showToast({ + title: existing ? language.t("session.share.copy.copied") : language.t("toast.session.share.success.title"), + description: language.t("toast.session.share.success.description"), + variant: "success", + }) + } + + const share = async () => { + const sessionID = params.id + if (!sessionID) return + + const existing = info()?.share?.url + if (existing) { + await copyShare(existing, true) + return + } + + const url = await sdk() + .client.session.share({ sessionID }) + .then((res) => res.data?.share?.url) + .catch(() => undefined) + if (!url) { + showToast({ + title: language.t("toast.session.share.failed.title"), + description: language.t("toast.session.share.failed.description"), + variant: "error", + }) + return + } + + await copyShare(url, false) + } + + const unshare = async () => { + const sessionID = params.id + if (!sessionID) return + + await sdk() + .client.session.unshare({ sessionID }) + .then(() => + showToast({ + title: language.t("toast.session.unshare.success.title"), + description: language.t("toast.session.unshare.success.description"), + variant: "success", + }), + ) + .catch(() => + showToast({ + title: language.t("toast.session.unshare.failed.title"), + description: language.t("toast.session.unshare.failed.description"), + variant: "error", + }), + ) + } + + const openFile = () => { + void openDialog( + () => import("@/components/dialog-select-file"), + (x) => dialog.show(() => ), + ) + } + + const closeTab = () => { + const tab = closableTab() + if (!tab) return + tabs().close(tab) + } + + const addSelection = () => { + const tab = activeFileTab() + if (!tab) return + + const path = file.pathFromTab(tab) + if (!path) return + + const range = file.selectedLines(path) as SelectedLineRange | null | undefined + if (!range) { + showToast({ + title: language.t("toast.context.noLineSelection.title"), + description: language.t("toast.context.noLineSelection.description"), + }) + return + } + + addSelectionToContext(path, selectionFromLines(range)) + } + + const openTerminal = () => { + if (terminal.all().length > 0) terminal.new({ focus: true }) + if (terminal.all().length === 0) terminal.requestFocus() + view().terminal.open() + } + + const closeTerminal = () => { + const id = terminal.active() + if (!id) return + const last = terminal.all().length === 1 + void terminal.close(id) + if (last) view().terminal.close() + } + + const chooseMcp = () => { + void openDialog( + () => import("@/components/dialog-select-mcp"), + (x) => dialog.show(() => ), + ) + } + + const toggleAutoAccept = () => { + const sessionID = params.id + if (sessionID) permission.toggleAutoAccept(sessionID, sdk().directory) + else permission.toggleAutoAcceptDirectory(sdk().directory) + + const active = sessionID + ? permission.isAutoAccepting(sessionID, sdk().directory) + : permission.isAutoAcceptingDirectory(sdk().directory) + showToast({ + title: active + ? language.t("toast.permissions.autoaccept.on.title") + : language.t("toast.permissions.autoaccept.off.title"), + description: active + ? language.t("toast.permissions.autoaccept.on.description") + : language.t("toast.permissions.autoaccept.off.description"), + }) + } + + const undo = async () => { + const sessionID = params.id + if (!sessionID) return + const owner = sessionOwnership.capture() + const session = sdk().api.session + const directory = sdk().directory + const promptSession = prompt.capture() + const revert = info()?.revert?.messageID + const messages = userMessages() + const message = findLast(messages, (x) => !revert || x.id < revert) + if (!message) return + const parts = sync().data.part[message.id] + + if (sync().data.session_working(sessionID)) { + await session.interrupt({ sessionID }).catch(() => {}) + } + + await runCommand({ + owner, + prompt: promptSession, + request: () => session.revert.stage({ sessionID, messageID: message.id }), + updatePrompt: (promptSession) => { + if (parts) promptSession.set(extractPromptFromParts(parts, { directory })) + }, + updateViewport: () => setActiveMessage(findLast(messages, (x) => x.id < message.id)), + }) + } + + const redo = async () => { + const sessionID = params.id + if (!sessionID) return + const owner = sessionOwnership.capture() + const session = sdk().api.session + const messages = userMessages() + const promptSession = prompt.capture() + + const revertMessageID = info()?.revert?.messageID + if (!revertMessageID) return + + const next = messages.find((x) => x.id > revertMessageID) + if (!next) { + await runCommand({ + owner, + prompt: promptSession, + request: () => session.revert.clear({ sessionID }), + updatePrompt: (promptSession) => promptSession.reset(), + updateViewport: () => setActiveMessage(findLast(messages, (x) => x.id >= revertMessageID)), + }) + return + } + + await runCommand({ + owner, + prompt: promptSession, + request: () => session.revert.stage({ sessionID, messageID: next.id }), + updatePrompt: () => undefined, + updateViewport: () => setActiveMessage(findLast(messages, (x) => x.id < next.id)), + }) + } + + const compact = async () => { + const sessionID = params.id + if (!sessionID) return + + const model = local.model.current() + if (!model) { + showToast({ + title: language.t("toast.model.none.title"), + description: language.t("toast.model.none.description"), + }) + return + } + + await sdk().api.session.compact({ + sessionID, + model: { providerID: model.provider.id, modelID: model.id }, + }) + } + + const fork = () => { + void openDialog( + () => import("@/components/dialog-fork"), + (x) => dialog.show(() => ), + ) + } + + const shareCmds = () => { + if (sync().data.config.share === "disabled") return [] + return [ + sessionCommand({ + id: "session.share", + title: info()?.share?.url ? language.t("session.share.copy.copyLink") : language.t("command.session.share"), + description: info()?.share?.url + ? language.t("toast.session.share.success.description") + : language.t("command.session.share.description"), + slash: "share", + disabled: !params.id, + onSelect: share, + }), + sessionCommand({ + id: "session.unshare", + title: language.t("command.session.unshare"), + description: language.t("command.session.unshare.description"), + slash: "unshare", + disabled: !params.id || !info()?.share?.url, + onSelect: unshare, + }), + ] + } + + const sessionCmds = () => [ + sessionCommand({ + id: "session.new", + title: language.t("command.session.new"), + keybind: "mod+shift+s", + slash: "new", + onSelect: (source) => { + if (settings.general.newLayoutDesigns()) { + command.trigger("tab.new", source) + return + } + navigate(`/${params.dir}/session`) + }, + }), + sessionCommand({ + id: "session.undo", + title: language.t("command.session.undo"), + description: language.t("command.session.undo.description"), + slash: "undo", + disabled: !params.id || visibleUserMessages().length === 0, + onSelect: undo, + }), + sessionCommand({ + id: "session.redo", + title: language.t("command.session.redo"), + description: language.t("command.session.redo.description"), + slash: "redo", + disabled: !params.id || !info()?.revert?.messageID, + onSelect: redo, + }), + sessionCommand({ + id: "session.compact", + title: language.t("command.session.compact"), + description: language.t("command.session.compact.description"), + slash: "compact", + disabled: !params.id || visibleUserMessages().length === 0, + onSelect: compact, + }), + sessionCommand({ + id: "session.fork", + title: language.t("command.session.fork"), + description: language.t("command.session.fork.description"), + slash: "fork", + disabled: !params.id || visibleUserMessages().length === 0, + onSelect: fork, + }), + ] + + const fileCmds = () => { + const tab = closableTab() + return [ + fileCommand({ + id: "file.open", + title: language.t("command.file.open"), + description: language.t("palette.search.placeholder"), + keybind: "mod+p", + slash: "open", + onSelect: openFile, + }), + tab && + fileCommand({ + id: "tab.close", + title: language.t("command.tab.close"), + keybind: "mod+w", + onSelect: closeTab, + }), + ].filter((v) => !!v) + } + + const contextCmds = () => [ + contextCommand({ + id: "context.addSelection", + title: language.t("command.context.addSelection"), + description: language.t("command.context.addSelection.description"), + keybind: "mod+shift+l", + disabled: !canAddSelectionContext(), + onSelect: addSelection, + }), + ] + + const viewCmds = () => [ + viewCommand({ + id: "terminal.toggle", + title: language.t("command.terminal.toggle"), + keybind: "ctrl+`", + slash: "terminal", + onSelect: () => { + if (view().terminal.opened()) { + terminal.cancelFocus() + view().terminal.close() + return + } + terminal.requestFocus(terminal.active()) + view().terminal.open() + }, + }), + viewCommand({ + id: "review.toggle", + title: language.t("command.review.toggle"), + keybind: "mod+shift+r", + onSelect: () => view().reviewPanel.toggle(), + }), + ...(shown() + ? [ + viewCommand({ + id: "fileTree.toggle", + title: language.t("command.fileTree.toggle"), + keybind: "mod+\\", + onSelect: () => layout.fileTree.toggle(), + }), + ] + : []), + viewCommand({ + id: "input.focus", + title: language.t("command.input.focus"), + keybind: "ctrl+l", + onSelect: focusInput, + }), + ] + + const terminalCmds = () => [ + terminalCommand({ + id: "terminal.close", + title: language.t("terminal.close"), + keybind: "mod+w", + hidden: true, + when: (event) => event.target instanceof Element && !!event.target.closest('[data-component="terminal"]'), + onSelect: closeTerminal, + }), + terminalCommand({ + id: "terminal.new", + title: language.t("command.terminal.new"), + description: language.t("command.terminal.new.description"), + keybind: "ctrl+alt+t", + onSelect: openTerminal, + }), + ] + + const messageCmds = () => [ + sessionCommand({ + id: "message.previous", + title: language.t("command.message.previous"), + description: language.t("command.message.previous.description"), + keybind: "mod+alt+[", + disabled: !params.id, + onSelect: () => navigateMessageByOffset(-1), + }), + sessionCommand({ + id: "message.next", + title: language.t("command.message.next"), + description: language.t("command.message.next.description"), + keybind: "mod+alt+]", + disabled: !params.id, + onSelect: () => navigateMessageByOffset(1), + }), + ] + + const mcpCmds = () => [ + mcpCommand({ + id: "mcp.toggle", + title: language.t("command.mcp.toggle"), + description: language.t("command.mcp.toggle.description"), + keybind: "mod+;", + slash: "mcp", + onSelect: chooseMcp, + }), + ] + + const permissionsCmds = () => [ + permissionsCommand({ + id: "permissions.autoaccept", + title: isAutoAcceptActive() + ? language.t("command.permissions.autoaccept.disable") + : language.t("command.permissions.autoaccept.enable"), + keybind: "mod+shift+a", + disabled: false, + onSelect: toggleAutoAccept, + }), + ] + + command.register("session", () => [ + ...sessionCmds(), + ...shareCmds(), + ...fileCmds(), + ...contextCmds(), + ...viewCmds(), + ...terminalCmds(), + ...messageCmds(), + ...mcpCmds(), + ...permissionsCmds(), + ]) +} diff --git a/packages/app-bundle/overlay/packages/app/src/pages/session/v2/accumulate-diffs.test.ts b/packages/app-bundle/overlay/packages/app/src/pages/session/v2/accumulate-diffs.test.ts new file mode 100644 index 00000000..422abf52 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/pages/session/v2/accumulate-diffs.test.ts @@ -0,0 +1,94 @@ +import { describe, expect, test } from "bun:test" +import { accumulateDiffs, type ToolEditPart } from "./accumulate-diffs" + +const edit = (file: string, overrides: Partial = {}): ToolEditPart => ({ + file, + patch: `@@ -1,1 +1,2 @@\n-old\n+new\n+line`, + additions: 1, + deletions: 1, + ...overrides, +}) + +describe("accumulateDiffs", () => { + test("single-edit file preserves patch unchanged", () => { + const patch = "@@ -1,3 +1,4 @@\n context\n-removed\n+added\n+extra" + const result = accumulateDiffs([edit("src/a.ts", { patch, additions: 2, deletions: 1 })]) + + expect(result).toHaveLength(1) + expect(result[0].file).toBe("src/a.ts") + expect(result[0].patch).toBe(patch) + expect(result[0].additions).toBe(2) + expect(result[0].deletions).toBe(1) + }) + + test("multi-edit file keeps last patch and sums additions/deletions", () => { + const result = accumulateDiffs([ + edit("src/a.ts", { additions: 3, deletions: 1, patch: "patch-1" }), + edit("src/a.ts", { additions: 2, deletions: 4, patch: "patch-2" }), + edit("src/a.ts", { additions: 1, deletions: 0, patch: "patch-3" }), + ]) + + expect(result).toHaveLength(1) + expect(result[0].file).toBe("src/a.ts") + expect(result[0].patch).toBe("patch-3") + expect(result[0].additions).toBe(6) + expect(result[0].deletions).toBe(5) + }) + + test("multi-edit file is marked 'modified' even if first edit was 'added'", () => { + const result = accumulateDiffs([ + edit("src/new.ts", { additions: 5, deletions: 0 }), + edit("src/new.ts", { additions: 2, deletions: 1 }), + ]) + + expect(result[0].status).toBe("modified") + }) + + test("single-edit new file is marked 'added'", () => { + const result = accumulateDiffs([edit("src/new.ts", { additions: 5, deletions: 0 })]) + expect(result[0].status).toBe("added") + }) + + test("multiple different files are each tracked independently", () => { + const result = accumulateDiffs([ + edit("src/a.ts", { additions: 1, deletions: 0, patch: "patch-a" }), + edit("src/b.ts", { additions: 2, deletions: 1, patch: "patch-b" }), + ]) + + expect(result).toHaveLength(2) + expect(result[0].file).toBe("src/a.ts") + expect(result[0].patch).toBe("patch-a") + expect(result[1].file).toBe("src/b.ts") + expect(result[1].patch).toBe("patch-b") + }) + + test("mixed: multi-edit file keeps last patch while single-edit file keeps its own", () => { + const result = accumulateDiffs([ + edit("src/a.ts", { additions: 1, deletions: 0, patch: "patch-a1" }), + edit("src/b.ts", { additions: 2, deletions: 1, patch: "patch-b" }), + edit("src/a.ts", { additions: 3, deletions: 2, patch: "patch-a2" }), + ]) + + expect(result).toHaveLength(2) + // a.ts: multi-edit — last patch kept, sums accumulated + const a = result.find((d) => d.file === "src/a.ts")! + expect(a.patch).toBe("patch-a2") + expect(a.additions).toBe(4) + expect(a.deletions).toBe(2) + expect(a.status).toBe("modified") + // b.ts: single-edit — patch preserved + const b = result.find((d) => d.file === "src/b.ts")! + expect(b.patch).toBe("patch-b") + expect(b.additions).toBe(2) + expect(b.deletions).toBe(1) + }) + + test("uses title as the output file path when available", () => { + const result = accumulateDiffs([edit("src/a.ts", { title: "relative/a.ts" })]) + expect(result[0].file).toBe("relative/a.ts") + }) + + test("empty input returns empty output", () => { + expect(accumulateDiffs([])).toEqual([]) + }) +}) diff --git a/packages/app-bundle/overlay/packages/app/src/pages/session/v2/accumulate-diffs.ts b/packages/app-bundle/overlay/packages/app/src/pages/session/v2/accumulate-diffs.ts new file mode 100644 index 00000000..8e069fb7 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/pages/session/v2/accumulate-diffs.ts @@ -0,0 +1,64 @@ +import type { SnapshotFileDiff } from "@opencode-ai/sdk/v2" + +export type ToolEditPart = { + file: string + title?: string + patch?: string + additions: number + deletions: number +} + +/** + * Accumulate file diffs from tool edit parts across a session. + * + * When a file is edited multiple times, individual patches cannot be naively + * combined into one true cumulative diff. We keep the LAST edit's patch as a + * best-effort display (the server query will replace it with the correct + * git-based cumulative diff once it refetches), and sum additions/deletions + * for the file list badge. + * + * For single-edit files the patch is preserved as-is. + */ +export function accumulateDiffs(parts: ToolEditPart[]): Array { + const entries = new Map< + string, + { + title: string + patch: string | undefined + additions: number + deletions: number + status: "added" | "modified" | "deleted" + editCount: number + } + >() + + for (const part of parts) { + const existing = entries.get(part.file) + if (!existing) { + entries.set(part.file, { + title: part.title || part.file, + patch: part.patch, + additions: part.additions, + deletions: part.deletions, + status: "added", + editCount: 1, + }) + } else { + existing.additions += part.additions + existing.deletions += part.deletions + existing.status = "modified" + existing.editCount += 1 + // Keep the last edit's patch as best-effort display until the server + // provides the real cumulative diff + existing.patch = part.patch ?? existing.patch + } + } + + return [...entries.values()].map((entry) => ({ + file: entry.title, + patch: entry.patch, + additions: entry.additions, + deletions: entry.deletions, + status: entry.status, + })) +} diff --git a/packages/app-bundle/overlay/packages/app/src/pages/session/v2/review-panel-v2-state.test.ts b/packages/app-bundle/overlay/packages/app/src/pages/session/v2/review-panel-v2-state.test.ts new file mode 100644 index 00000000..d1fa63a8 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/pages/session/v2/review-panel-v2-state.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, test } from "bun:test" +import { reviewSidebarOpened, reviewSidebarToggled } from "./review-panel-v2-state" + +// amicode#105: the Work Column is SINGLE-PANE — the review panel's file-list +// sidebar never renders (it split the column into two panes and squished the +// chat). Diffs get the full column width; navigation is the changes dropdown. +describe("review panel v2 sidebar policy (single-pane column)", () => { + test("the sidebar is closed by default", () => { + expect(reviewSidebarOpened()).toBe(false) + }) + + test("the toggle can never open it — no split at any width", () => { + expect(reviewSidebarToggled(true)).toBe(false) + expect(reviewSidebarToggled(false)).toBe(false) + }) + + test("a persisted true from the split-pane era is ignored", () => { + expect(reviewSidebarOpened(true)).toBe(false) + }) +}) diff --git a/packages/app-bundle/overlay/packages/app/src/pages/session/v2/review-panel-v2-state.ts b/packages/app-bundle/overlay/packages/app/src/pages/session/v2/review-panel-v2-state.ts new file mode 100644 index 00000000..cceac755 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/pages/session/v2/review-panel-v2-state.ts @@ -0,0 +1,53 @@ +import type { SessionReviewExpandMode } from "@opencode-ai/session-ui/v2/session-review-v2" +import { createSignal } from "solid-js" +import { createStore } from "solid-js/store" +import { Persist, persisted } from "@/utils/persist" + +// Width bounds duplicated from @opencode-ai/session-ui/v2/session-review-v2 +// so this module stays importable in the bun test env (the component chain +// pulls @pierre/diffs workers, which don't run under happydom). +const SIDEBAR_WIDTH_DEFAULT = 240 +const SIDEBAR_WIDTH_MIN = 200 +const SIDEBAR_WIDTH_MAX = 480 + +/** amicode#105 single-pane policy (pure, tested): the Work Column never + * splits — the review sidebar (the file list) does not render at any width. + * Diffs take the full column width; navigation is the changes dropdown. */ +export function reviewSidebarOpened(_persisted?: boolean): boolean { + return false +} + +/** The toggle is inert — it stays for prop compatibility but can never open + * the split. A persisted `sidebarOpened: true` from the split-pane era is + * ignored everywhere through reviewSidebarOpened. */ +export function reviewSidebarToggled(_opened: boolean): boolean { + return false +} + +export function createReviewPanelV2State() { + const [store, setStore, , ready] = persisted( + Persist.global("review-panel-v2"), + createStore({ + sidebarWidth: SIDEBAR_WIDTH_DEFAULT, + expandMode: "collapse" as SessionReviewExpandMode, + }), + ) + // The filter is transient by design: a persisted filter would silently hide + // files after a reload. + const [filter, setFilter] = createSignal("") + + return { + sidebarOpened: reviewSidebarOpened, + sidebarWidth: () => store.sidebarWidth, + sidebarTransition: ready, + filter, + setFilter, + expandMode: () => store.expandMode, + setExpandMode: (mode: SessionReviewExpandMode) => setStore("expandMode", mode), + resizeSidebar: (width: number) => + setStore("sidebarWidth", Math.min(SIDEBAR_WIDTH_MAX, Math.max(SIDEBAR_WIDTH_MIN, width))), + toggleSidebar: () => {}, + } +} + +export type ReviewPanelV2State = ReturnType diff --git a/packages/app-bundle/overlay/packages/app/src/utils/amicode-bridge.ts b/packages/app-bundle/overlay/packages/app/src/utils/amicode-bridge.ts new file mode 100644 index 00000000..9f53f270 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/utils/amicode-bridge.ts @@ -0,0 +1,15 @@ +// amicode: the app↔extension postMessage bridge. chat_panel.ts relays the +// envelope to an ALLOWLISTED vscode command, so the exact envelope shape and +// command strings are a contract. Extracted from use-amicode-commands.tsx so +// non-palette surfaces (the report-a-bug button, the entity rail's pulse +// chip) can post without dragging the command-registry module in. + +// Gates every bridge surface out of the public web/share build, where there +// is no extension host to relay to (unframed: self === top). +export const inAmicode = () => typeof window !== "undefined" && window.self !== window.top + +export const postAmicode = (command: string) => { + try { + window.parent?.postMessage({ source: "amicode", kind: "command", command }, "*") + } catch {} +} diff --git a/packages/app-bundle/overlay/packages/app/src/utils/amicode-bug-report.test.ts b/packages/app-bundle/overlay/packages/app/src/utils/amicode-bug-report.test.ts new file mode 100644 index 00000000..05d5d1e6 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/utils/amicode-bug-report.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, test } from "bun:test" +import { adoptBugReportFlag, bugReportEnabled, postBugReportPoke, BUG_REPORT_POKE_KIND } from "./amicode-bug-report" + +// amicode#116: the report-a-bug button renders only when the extension passes +// the `amicode_bug_report=1` boot param — without it the composer row must be +// untouched. Same adopt-once boot-param convention as amicode-hide-project +// (entry.tsx reads location.search once at boot; the router rewrites the URL +// after, so a live read would flip mid-flight). +describe("amicode bug-report boot param (amicode/opencode#116)", () => { + test("off without the param; on with amicode_bug_report=1", () => { + adoptBugReportFlag("?colorScheme=dark") + expect(bugReportEnabled()).toBe(false) + + adoptBugReportFlag("?amicode_bug_report=1&auth_token=x") + expect(bugReportEnabled()).toBe(true) + }) + + test("only the exact value 1 enables it", () => { + adoptBugReportFlag("?amicode_bug_report=0") + expect(bugReportEnabled()).toBe(false) + + adoptBugReportFlag("?amicode_bug_report=true") + expect(bugReportEnabled()).toBe(false) + + adoptBugReportFlag("") + expect(bugReportEnabled()).toBe(false) + }) +}) + +describe("the boot poke — the dock contract's pull half (QA: amicode#249 preview)", () => { + test("flag on → exactly one bug-report-poke envelope", () => { + adoptBugReportFlag("?amicode_bug_report=1") + const posted: Array<{ source: string; kind: string }> = [] + postBugReportPoke((e) => posted.push(e)) + expect(posted).toEqual([{ source: "amicode", kind: BUG_REPORT_POKE_KIND }]) + }) + + test("flag off → silence (standalone opencode never pokes)", () => { + adoptBugReportFlag("?colorScheme=dark") + const posted: Array<{ source: string; kind: string }> = [] + postBugReportPoke((e) => posted.push(e)) + expect(posted).toEqual([]) + }) +}) diff --git a/packages/app-bundle/overlay/packages/app/src/utils/amicode-bug-report.ts b/packages/app-bundle/overlay/packages/app/src/utils/amicode-bug-report.ts new file mode 100644 index 00000000..a3159a8c --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/utils/amicode-bug-report.ts @@ -0,0 +1,42 @@ +// amicode/opencode#116: the report-a-bug button in the v2 composer is a +// fork-only feature, gated on the `amicode_bug_report=1` boot param the +// extension passes — standalone opencode never sets it, so the composer's +// control row stays byte-identical there. Read once at boot from entry.tsx +// (same convention as amicode-hide-project): the router rewrites the URL +// after boot, so a live read of window.location.search would flip mid-flight. +let enabled = false + +/** Adopt the boot flag from a URL search string — sets the gate to whether + * `amicode_bug_report=1` is present (deterministic, so tests can drive both + * directions; entry.tsx calls this exactly once with the real boot URL). */ +export function adoptBugReportFlag(search: string): void { + enabled = new URLSearchParams(search).get("amicode_bug_report") === "1" +} + +/** Whether the report-a-bug composer button renders. */ +export function bugReportEnabled(): boolean { + return enabled +} + +/** UP kind: the app's boot catch-up for the bug-report dock. Posted once per + * app-frame boot when the flag is on; the extension re-posts open-bug-report + * when a bug session is live. Heals a lost one-shot open (cold-boot race, + * webview reload) — the dock contract's pull half (QA: amicode#249 preview). */ +export const BUG_REPORT_POKE_KIND = "bug-report-poke" + +/** Post the boot poke to the extension host — only embedded (a webview parent + * exists) and only with the flag on (standalone opencode never pokes). Safe + * to call on every app boot: the extension treats it as cheap idempotent + * noise when no bug session is live. */ +export function postBugReportPoke(post: (envelope: { source: string; kind: string }) => void = defaultPokePost): void { + if (!bugReportEnabled()) return + post({ source: "amicode", kind: BUG_REPORT_POKE_KIND }) +} + +function defaultPokePost(envelope: { source: string; kind: string }): void { + try { + if (window.parent && window.parent !== window) window.parent.postMessage(envelope, "*") + } catch { + /* a detached frame never blocks boot */ + } +} diff --git a/packages/app-bundle/overlay/packages/app/src/utils/amicode-fetch.ts b/packages/app-bundle/overlay/packages/app/src/utils/amicode-fetch.ts new file mode 100644 index 00000000..64006e98 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/utils/amicode-fetch.ts @@ -0,0 +1,44 @@ +// amicode: shared GET against the active server's /amicode/* raw routes +// (spec B). Same per-active-server Basic-auth idiom as the Vaults tab fetch +// (status-popover-body.tsx). Resolves the connection PER CALL, so a server +// switch is picked up by the very next fetch — no dedicated refetch trigger. +import type { ServerConnection } from "@/context/server" +import { authTokenFromCredentials } from "@/utils/server" + +export async function amicodeGet(conn: ServerConnection.Any | undefined, route: string): Promise { + if (!conn) throw new Error("no active server") + const headers: Record = {} + if (conn.http.password) + headers.Authorization = `Basic ${authTokenFromCredentials({ + username: conn.http.username, + password: conn.http.password, + })}` + const res = await fetch(new URL(route, conn.http.url), { headers }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + return (await res.json()) as unknown +} + +/** POST sibling of amicodeGet — the amicode raw routes keep params in the URL + * (no body), so this is the same call shape with method POST. Used by the + * About-You card's in-place profile save. */ +export async function amicodePost( + conn: ServerConnection.Any | undefined, + route: string, + jsonBody?: unknown, +): Promise { + if (!conn) throw new Error("no active server") + const headers: Record = {} + if (conn.http.password) + headers.Authorization = `Basic ${authTokenFromCredentials({ + username: conn.http.username, + password: conn.http.password, + })}` + if (jsonBody !== undefined) headers["content-type"] = "application/json" + const res = await fetch(new URL(route, conn.http.url), { + method: "POST", + headers, + ...(jsonBody !== undefined ? { body: JSON.stringify(jsonBody) } : {}), + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + return (await res.json()) as unknown +} diff --git a/packages/app-bundle/overlay/packages/app/src/utils/amicode-hidden-project.test.ts b/packages/app-bundle/overlay/packages/app/src/utils/amicode-hidden-project.test.ts new file mode 100644 index 00000000..7d7d3eea --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/utils/amicode-hidden-project.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, test } from "bun:test" +import { adoptHiddenProject, hiddenProjectWorktree } from "./amicode-hidden-project" + +describe("amicode hidden-project boot param (amicode#203)", () => { + test("unset by default", () => { + expect(hiddenProjectWorktree()).toBeUndefined() + }) + test("adopts the amicode_hide_project param; ignores an empty/absent one", () => { + adoptHiddenProject("?colorScheme=dark") // no param → stays unset + expect(hiddenProjectWorktree()).toBeUndefined() + adoptHiddenProject("?amicode_hide_project=%2Fusers%2Fk%2F.scaffold&auth_token=x") + expect(hiddenProjectWorktree()).toBe("/users/k/.scaffold") + }) +}) diff --git a/packages/app-bundle/overlay/packages/app/src/utils/amicode-hidden-project.ts b/packages/app-bundle/overlay/packages/app/src/utils/amicode-hidden-project.ts new file mode 100644 index 00000000..8007a5f7 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/utils/amicode-hidden-project.ts @@ -0,0 +1,18 @@ +// amicode#203: the amicode extension launches the opencode server in an +// internal scaffold directory (it only holds the injected amico config), which +// opencode registers as a project. The extension passes that dir as the +// `amicode_hide_project` boot param so the dashboard can hide it — WITHOUT +// hiding a legitimate cwd project in standalone opencode, where the param is +// never set. Read once at boot (the param is stripped from the URL after). +let hiddenProjectDir: string | undefined + +/** Adopt the boot param from a URL search string (idempotent). */ +export function adoptHiddenProject(search: string): void { + const v = new URLSearchParams(search).get("amicode_hide_project") + if (v) hiddenProjectDir = v +} + +/** The worktree the dashboard should hide, or undefined (standalone / not set). */ +export function hiddenProjectWorktree(): string | undefined { + return hiddenProjectDir +} diff --git a/packages/app-bundle/overlay/packages/app/src/utils/amicode-pane.ts b/packages/app-bundle/overlay/packages/app/src/utils/amicode-pane.ts new file mode 100644 index 00000000..c305afc4 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/utils/amicode-pane.ts @@ -0,0 +1,12 @@ +// amicode(split): the right half of an in-app split is the APP ITSELF iframed +// with ?amicode_pane= — a pane is a full instance (its own titlebar, its +// own tab strip) whose GLOBAL UI state is namespaced by that id so it never +// fights the main instance (persisted stores read at boot and don't live-sync +// across documents — a shared "tabs" key would be last-writer-wins). +// Captured at MODULE LOAD: the router rewrites the URL as the pane navigates +// internally, so a live read of window.location.search would flip mid-flight. +export const AMICODE_PANE_ID: string | undefined = + new URLSearchParams(window.location.search).get("amicode_pane") || undefined + +export const IS_AMICODE_PANE = AMICODE_PANE_ID !== undefined + diff --git a/packages/app-bundle/overlay/packages/app/src/utils/amicode-route-info.ts b/packages/app-bundle/overlay/packages/app/src/utils/amicode-route-info.ts new file mode 100644 index 00000000..e371f497 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/utils/amicode-route-info.ts @@ -0,0 +1,11 @@ +// amicode(deck): a framing host webview (Amicode's Chat Deck shell) wants each +// pane's live route + label so its tab strip can show real names (and rebuild a +// dragged pane at its current route). Posted to window.parent over the same +// {source:"amicode"} envelope as the clipboard/link bridges; no-op unframed so +// plain browser tabs are unaffected. `path` is same-origin only (pathname + +// search — the draft's draftId rides the search, and the shell needs it to +// keep draft text across pane rebuilds). +export function postRouteInfo(path: string, title?: string) { + if (window.parent === window) return + window.parent.postMessage({ source: "amicode", kind: "route-info", path, title }, "*") +} diff --git a/packages/app-bundle/overlay/packages/app/src/utils/chrome-dropdown.ts b/packages/app-bundle/overlay/packages/app/src/utils/chrome-dropdown.ts new file mode 100644 index 00000000..ea7aedd6 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/utils/chrome-dropdown.ts @@ -0,0 +1,21 @@ +// amicode#203 (Kate): only ONE chrome-strip dropdown open at a time. Each +// dropdown announces itself when it opens; the others watch the shared signal +// and close. Module-level signal — the chrome strip is a singleton surface. +import { createSignal } from "solid-js" + +const [openId, setOpenId] = createSignal(undefined) + +/** Announce that a chrome dropdown just opened (closes the others). */ +export function announceChromeDropdown(id: string): void { + setOpenId(id) +} + +/** The id of the currently-open chrome dropdown (reactive). */ +export function chromeDropdownOpenId(): string | undefined { + return openId() +} + +/** Clear the announcement when a dropdown closes itself. */ +export function clearChromeDropdown(id: string): void { + if (openId() === id) setOpenId(undefined) +} diff --git a/packages/app-bundle/overlay/packages/app/src/utils/global-clipboard.test.ts b/packages/app-bundle/overlay/packages/app/src/utils/global-clipboard.test.ts new file mode 100644 index 00000000..a2144cd6 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/utils/global-clipboard.test.ts @@ -0,0 +1,698 @@ +import { afterEach, describe, expect, test } from "bun:test" +import { + extractSelection, + insertTextAtSelection, + installGlobalClipboardFallback, + isEditableTarget, + setSessionCopyProvider, +} from "./global-clipboard" + +// Every install/listener attaches to the real happy-dom window or document, so +// each test registers its teardown here to keep the suite order-independent. +const cleanups: Array<() => void> = [] +afterEach(() => { + while (cleanups.length) cleanups.pop()!() + document.body.innerHTML = "" + window.getSelection()?.removeAllRanges() +}) + +// A framed window: the real happy-dom window's event plumbing (so DOM events +// dispatched on elements reach capture-phase listeners) with a foreign parent +// that records what the app posts — mirroring clipboard-bridge.test.ts. +function framedWindow() { + const posted: Array> = [] + const parent = { postMessage: (message: Record) => posted.push(message) } + const win = new Proxy(window, { + get(target, prop) { + if (prop === "parent") return parent + const value = Reflect.get(target, prop) + return typeof value === "function" ? (value as (...args: unknown[]) => unknown).bind(target) : value + }, + }) as unknown as Window + return { + win, + posted, + reply: (message: Record) => window.dispatchEvent(new MessageEvent("message", { data: message })), + } +} + +function install(win: Window) { + const uninstall = installGlobalClipboardFallback(win) + cleanups.push(uninstall) + return uninstall +} + +function field(type: string, value = "", start?: number, end?: number) { + const el = document.createElement("input") + el.type = type + el.value = value + document.body.appendChild(el) + if (start !== undefined) el.setSelectionRange(start, end ?? start) + return el +} + +function editableDiv(text: string) { + const el = document.createElement("div") + el.setAttribute("contenteditable", "true") + el.textContent = text + document.body.appendChild(el) + return el +} + +function selectWithin(el: HTMLElement, start: number, end: number) { + const range = document.createRange() + range.setStart(el.firstChild!, start) + range.setEnd(el.firstChild!, end) + const selection = window.getSelection()! + selection.removeAllRanges() + selection.addRange(range) + return selection +} + +// A stand-in for a framework-controlled input: records what a bubbling +// document-level "input" listener observes — the element's value must already +// hold the new text by the time the listener runs (how Solid syncs state). +function observeInput() { + const events: Array<{ value: string; inputType: string }> = [] + const onInput = (event: Event) => { + const target = event.target as HTMLElement + events.push({ + value: "value" in target ? (target as HTMLInputElement).value : (target.textContent ?? ""), + inputType: (event as InputEvent).inputType, + }) + } + document.addEventListener("input", onInput) + cleanups.push(() => document.removeEventListener("input", onInput)) + return events +} + +function keydown(el: Element, key: string, init: KeyboardEventInit = {}) { + const event = new KeyboardEvent("keydown", { key, metaKey: true, bubbles: true, cancelable: true, ...init }) + el.dispatchEvent(event) + return event +} + +const tick = () => new Promise((resolve) => setTimeout(resolve, 0)) + +describe("isEditableTarget", () => { + test("accepts text-like inputs (incl. password), textareas, and contenteditables", () => { + expect(isEditableTarget(field("text"))).toBe(true) + expect(isEditableTarget(field("password"))).toBe(true) + expect(isEditableTarget(field("search"))).toBe(true) + expect(isEditableTarget(field("email"))).toBe(true) + const textarea = document.createElement("textarea") + document.body.appendChild(textarea) + expect(isEditableTarget(textarea)).toBe(true) + expect(isEditableTarget(editableDiv("x"))).toBe(true) + }) + + test("rejects non-text controls, non-editables, and locked fields", () => { + expect(isEditableTarget(field("checkbox"))).toBe(false) + expect(isEditableTarget(field("file"))).toBe(false) + expect(isEditableTarget(document.createElement("button"))).toBe(false) + expect(isEditableTarget(document.createElement("div"))).toBe(false) + expect(isEditableTarget(null)).toBe(false) + const disabled = field("text") + disabled.disabled = true + expect(isEditableTarget(disabled)).toBe(false) + const readonly = field("password") + readonly.readOnly = true + expect(isEditableTarget(readonly)).toBe(false) + }) +}) + +describe("insertTextAtSelection", () => { + test("replaces a form field's selection and lands the caret after the insertion", () => { + const el = field("text", "abcdef", 2, 4) + const seen = observeInput() + + insertTextAtSelection(el, "XY") + + expect(el.value).toBe("abXYef") + expect(el.selectionStart).toBe(4) + expect(el.selectionEnd).toBe(4) + // The controlled-input contract: by the time the bubbling event arrives, + // reading target.value yields the new text. + expect(seen).toEqual([{ value: "abXYef", inputType: "insertFromPaste" }]) + }) + + test("inserts at a collapsed caret in a textarea", () => { + const el = document.createElement("textarea") + el.value = "solve a gate" + document.body.appendChild(el) + el.setSelectionRange(8, 8) + const seen = observeInput() + + insertTextAtSelection(el, "CZ") + + expect(el.value).toBe("solve a CZ gate") + expect(el.selectionStart).toBe(10) + expect(seen).toEqual([{ value: "solve a CZ gate", inputType: "insertFromPaste" }]) + }) + + test("contenteditable: replaces the DOM selection and dispatches insertFromPaste", () => { + const el = editableDiv("hello world") + selectWithin(el, 0, 5) + const seen = observeInput() + + insertTextAtSelection(el, "goodbye") + + expect(el.textContent).toBe("goodbye world") + expect(window.getSelection()?.toString()).toBe("") // collapsed after insert + expect(seen).toEqual([{ value: "goodbye world", inputType: "insertFromPaste" }]) + }) + + test("contenteditable: appends at the end when the selection lives elsewhere", () => { + const el = editableDiv("hqs-") + window.getSelection()?.removeAllRanges() + const seen = observeInput() + + insertTextAtSelection(el, "token") + + expect(el.textContent).toBe("hqs-token") + expect(seen).toEqual([{ value: "hqs-token", inputType: "insertFromPaste" }]) + }) +}) + +describe("extractSelection", () => { + test("returns a form field's selected slice without mutating it", () => { + const el = field("text", "solve a CZ gate", 0, 5) + const seen = observeInput() + + expect(extractSelection(el)).toBe("solve") + expect(el.value).toBe("solve a CZ gate") + expect(seen).toEqual([]) // copy is read-only: no input event + }) + + test("returns empty for a collapsed selection", () => { + expect(extractSelection(field("text", "abc", 1, 1))).toBe("") + }) + + test("cut removes the selection, collapses the caret, and dispatches deleteByCut", () => { + const el = field("password", "abcdef", 2, 4) + const seen = observeInput() + + expect(extractSelection(el, { cut: true })).toBe("cd") + expect(el.value).toBe("abef") + expect(el.selectionStart).toBe(2) + expect(el.selectionEnd).toBe(2) + expect(seen).toEqual([{ value: "abef", inputType: "deleteByCut" }]) + }) + + test("contenteditable: returns the selected text; cut removes it", () => { + const el = editableDiv("hello world") + selectWithin(el, 0, 6) + expect(extractSelection(el)).toBe("hello ") + expect(el.textContent).toBe("hello world") + + selectWithin(el, 0, 6) + const seen = observeInput() + expect(extractSelection(el, { cut: true })).toBe("hello ") + expect(el.textContent).toBe("world") + expect(seen).toEqual([{ value: "world", inputType: "deleteByCut" }]) + }) +}) + +describe("installGlobalClipboardFallback", () => { + test("mod+V in a framed credential field requests the OS clipboard and inserts the reply", async () => { + const bridge = framedWindow() + install(bridge.win) + const el = field("password", "hqs-", 4, 4) + const seen = observeInput() + + const event = keydown(el, "v") + + expect(event.defaultPrevented).toBe(true) + expect(bridge.posted).toHaveLength(1) + expect(bridge.posted[0]!.kind).toBe("clipboard-request") + + bridge.reply({ source: "amicode", kind: "clipboard", nonce: bridge.posted[0]!.nonce, text: "api-key-123" }) + await tick() + + expect(el.value).toBe("hqs-api-key-123") + expect(seen).toEqual([{ value: "hqs-api-key-123", inputType: "insertFromPaste" }]) + }) + + test("mod+V replaces the field's selection with the pasted text", async () => { + const bridge = framedWindow() + install(bridge.win) + const el = field("text", "wrong-token", 0, 11) + + keydown(el, "v") + bridge.reply({ source: "amicode", kind: "clipboard", nonce: bridge.posted[0]!.nonce, text: "right-token" }) + await tick() + + expect(el.value).toBe("right-token") + }) + + test("an empty bridge reply degrades to a no-op", async () => { + const bridge = framedWindow() + install(bridge.win) + const el = field("text", "untouched", 0, 0) + const seen = observeInput() + + keydown(el, "v") + bridge.reply({ source: "amicode", kind: "clipboard", nonce: bridge.posted[0]!.nonce, text: "" }) + await tick() + + expect(el.value).toBe("untouched") + expect(seen).toEqual([]) + }) + + test('targets inside [data-amc-clipboard="self"] keep their own paste handling', () => { + const bridge = framedWindow() + install(bridge.win) + const owner = document.createElement("div") + owner.setAttribute("data-amc-clipboard", "self") + document.body.appendChild(owner) + const el = document.createElement("input") + el.type = "text" + owner.appendChild(el) + + const event = keydown(el, "v") + + // The prompt input's own ⌘V handler runs instead — no double insertion. + expect(event.defaultPrevented).toBe(false) + expect(bridge.posted).toHaveLength(0) + }) + + test("mod+C pushes the field's selection to the OS clipboard via the bridge", () => { + const bridge = framedWindow() + install(bridge.win) + const el = field("text", "solve a CZ gate", 0, 5) + + const event = keydown(el, "c") + + expect(event.defaultPrevented).toBe(true) + expect(bridge.posted).toEqual([{ source: "amicode", kind: "clipboard-write", text: "solve" }]) + expect(el.value).toBe("solve a CZ gate") // copy never mutates + }) + + test("copy still mirrors inside self-marked subtrees — the marker only owns paste", () => { + const bridge = framedWindow() + install(bridge.win) + const el = field("text", "draft prompt", 0, 5) + el.setAttribute("data-amc-clipboard", "self") + + keydown(el, "c") + + expect(bridge.posted).toEqual([{ source: "amicode", kind: "clipboard-write", text: "draft" }]) + }) + + test("mod+X (ctrl too) pushes the selection and removes it", () => { + const bridge = framedWindow() + install(bridge.win) + const el = field("text", "abcdef", 2, 4) + const seen = observeInput() + + const event = keydown(el, "x", { metaKey: false, ctrlKey: true }) + + expect(event.defaultPrevented).toBe(true) + expect(bridge.posted).toEqual([{ source: "amicode", kind: "clipboard-write", text: "cd" }]) + expect(el.value).toBe("abef") + expect(seen).toEqual([{ value: "abef", inputType: "deleteByCut" }]) + }) + + test("mod+C with nothing selected posts nothing and leaves the event alone", () => { + const bridge = framedWindow() + install(bridge.win) + // Clear any lingering document selection from prior tests + window.getSelection()?.removeAllRanges() + const el = field("text", "abc", 1, 1) + + const event = keydown(el, "c") + + expect(event.defaultPrevented).toBe(false) + expect(bridge.posted).toHaveLength(0) + }) + + test("non-editable targets: mod+C/X with a selection bridges the text to clipboard", () => { + const bridge = framedWindow() + install(bridge.win) + const div = document.createElement("div") + div.textContent = "chat message" + document.body.appendChild(div) + // Place a document selection on the div's text + const range = document.createRange() + range.selectNodeContents(div) + const selection = window.getSelection()! + selection.removeAllRanges() + selection.addRange(range) + + const event = keydown(div, "c") + + expect(event.defaultPrevented).toBe(true) + expect(bridge.posted).toEqual([{ source: "amicode", kind: "clipboard-write", text: "chat message" }]) + }) + + test("non-editable targets: mod+C with no selection is a no-op", () => { + const bridge = framedWindow() + install(bridge.win) + const button = document.createElement("button") + document.body.appendChild(button) + // Explicitly clear any lingering selection + window.getSelection()?.removeAllRanges() + + const event = keydown(button, "c") + + expect(event.defaultPrevented).toBe(false) + expect(bridge.posted).toHaveLength(0) + }) + + test("non-editable targets: mod+V is a no-op (nothing to paste into)", () => { + const bridge = framedWindow() + install(bridge.win) + const button = document.createElement("button") + document.body.appendChild(button) + + const event = keydown(button, "v") + + expect(event.defaultPrevented).toBe(false) + expect(bridge.posted).toHaveLength(0) + }) + + test("unframed windows are left entirely to native clipboard handling", async () => { + install(window) // happy-dom's top-level window: parent === self + const el = field("text", "native", 0, 6) + const seen = observeInput() + + const event = keydown(el, "v") + await tick() + + expect(event.defaultPrevented).toBe(false) + expect(el.value).toBe("native") + expect(seen).toEqual([]) + }) + + test("shift/alt chords and unmodified keys are ignored", () => { + const bridge = framedWindow() + install(bridge.win) + const el = field("text", "abcdef", 0, 3) + + expect(keydown(el, "v", { shiftKey: true }).defaultPrevented).toBe(false) + expect(keydown(el, "c", { altKey: true }).defaultPrevented).toBe(false) + expect(keydown(el, "v", { metaKey: false }).defaultPrevented).toBe(false) + expect(bridge.posted).toHaveLength(0) + }) + + test("the returned uninstall detaches the handler", () => { + const bridge = framedWindow() + const uninstall = install(bridge.win) + uninstall() + const el = field("text", "abc", 0, 3) + + const event = keydown(el, "c") + + expect(event.defaultPrevented).toBe(false) + expect(bridge.posted).toHaveLength(0) + }) + + // --- Select-all (Cmd+A) --- + + test("mod+A selects all text in a form field", () => { + const bridge = framedWindow() + install(bridge.win) + const el = field("text", "hello world", 3, 3) + + const event = keydown(el, "a") + + expect(event.defaultPrevented).toBe(true) + expect(el.selectionStart).toBe(0) + expect(el.selectionEnd).toBe(11) + }) + + test("mod+A selects all content in a contenteditable", () => { + const bridge = framedWindow() + install(bridge.win) + const el = editableDiv("hello world") + + const event = keydown(el, "a") + + expect(event.defaultPrevented).toBe(true) + const selection = window.getSelection()! + expect(selection.toString()).toBe("hello world") + }) + + test("mod+A in a textarea selects all its text", () => { + const bridge = framedWindow() + install(bridge.win) + const el = document.createElement("textarea") + el.value = "line1\nline2" + document.body.appendChild(el) + el.setSelectionRange(2, 2) + + const event = keydown(el, "a") + + expect(event.defaultPrevented).toBe(true) + expect(el.selectionStart).toBe(0) + expect(el.selectionEnd).toBe(11) + }) + + test("mod+A on an EMPTY prompt-input selects the timeline (full session intent)", () => { + const bridge = framedWindow() + install(bridge.win) + // Simulate the DOM structure: a timeline container + a prompt input + const timeline = document.createElement("div") + timeline.setAttribute("data-timeline-virtual-content", "") + timeline.textContent = "Assistant: Here is the answer." + document.body.appendChild(timeline) + const prompt = document.createElement("div") + prompt.setAttribute("data-component", "prompt-input") + prompt.setAttribute("contenteditable", "true") + prompt.textContent = "" + document.body.appendChild(prompt) + + const event = keydown(prompt, "a") + + expect(event.defaultPrevented).toBe(true) + const selection = window.getSelection()! + expect(selection.toString()).toBe("Assistant: Here is the answer.") + }) + + test("mod+A on a NON-EMPTY prompt-input selects the prompt text (standard behavior)", () => { + const bridge = framedWindow() + install(bridge.win) + const timeline = document.createElement("div") + timeline.setAttribute("data-timeline-virtual-content", "") + timeline.textContent = "chat messages" + document.body.appendChild(timeline) + const prompt = document.createElement("div") + prompt.setAttribute("data-component", "prompt-input") + prompt.setAttribute("contenteditable", "true") + prompt.textContent = "my draft message" + document.body.appendChild(prompt) + + const event = keydown(prompt, "a") + + expect(event.defaultPrevented).toBe(true) + const selection = window.getSelection()! + // Selects the prompt text, not the timeline + expect(selection.toString()).toBe("my draft message") + }) + + test("mod+A then mod+C on an EMPTY prompt-input bridges the timeline content to clipboard", () => { + const bridge = framedWindow() + install(bridge.win) + // Simulate the DOM structure: a timeline container + a prompt input + const timeline = document.createElement("div") + timeline.setAttribute("data-timeline-virtual-content", "") + timeline.textContent = "User: hello\nAssistant: world" + document.body.appendChild(timeline) + const prompt = document.createElement("div") + prompt.setAttribute("data-component", "prompt-input") + prompt.setAttribute("contenteditable", "true") + prompt.textContent = "" + document.body.appendChild(prompt) + + // Cmd+A selects the timeline (prompt is empty) + keydown(prompt, "a") + // Cmd+C copies it via bridge + const event = keydown(prompt, "c") + + expect(event.defaultPrevented).toBe(true) + expect(bridge.posted).toEqual([ + { source: "amicode", kind: "clipboard-write", text: "User: hello\nAssistant: world" }, + ]) + }) + + // --- Session copy provider (data-model full copy) --- + + test("mod+A then mod+C with a session copy provider uses the provider text", () => { + const bridge = framedWindow() + install(bridge.win) + const timeline = document.createElement("div") + timeline.setAttribute("data-timeline-virtual-content", "") + timeline.textContent = "visible portion only" + document.body.appendChild(timeline) + const prompt = document.createElement("div") + prompt.setAttribute("data-component", "prompt-input") + prompt.setAttribute("contenteditable", "true") + prompt.textContent = "" + document.body.appendChild(prompt) + + // Register a provider that returns the FULL session (as if from the store) + setSessionCopyProvider(() => "User:\nWhat is 2+2?\n\nAssistant:\n4") + cleanups.push(() => setSessionCopyProvider(undefined)) + + // Cmd+A arms the flag, Cmd+C reads from the provider + keydown(prompt, "a") + const event = keydown(prompt, "c") + + expect(event.defaultPrevented).toBe(true) + expect(bridge.posted).toEqual([ + { source: "amicode", kind: "clipboard-write", text: "User:\nWhat is 2+2?\n\nAssistant:\n4" }, + ]) + }) + + test("provider text is preferred over DOM selection (virtualised content)", () => { + const bridge = framedWindow() + install(bridge.win) + const timeline = document.createElement("div") + timeline.setAttribute("data-timeline-virtual-content", "") + timeline.textContent = "only visible rows" + document.body.appendChild(timeline) + const prompt = document.createElement("div") + prompt.setAttribute("data-component", "prompt-input") + prompt.setAttribute("contenteditable", "true") + prompt.textContent = "" + document.body.appendChild(prompt) + + setSessionCopyProvider(() => "FULL SESSION with 100 messages") + cleanups.push(() => setSessionCopyProvider(undefined)) + + keydown(prompt, "a") + const event = keydown(prompt, "c") + + expect(event.defaultPrevented).toBe(true) + // Provider text wins over the DOM "only visible rows" + expect(bridge.posted[0]!.text).toBe("FULL SESSION with 100 messages") + }) + + test("fullSessionCopyPending flag is cleared by intervening keystrokes", () => { + const bridge = framedWindow() + install(bridge.win) + const timeline = document.createElement("div") + timeline.setAttribute("data-timeline-virtual-content", "") + timeline.textContent = "chat" + document.body.appendChild(timeline) + const prompt = document.createElement("div") + prompt.setAttribute("data-component", "prompt-input") + prompt.setAttribute("contenteditable", "true") + prompt.textContent = "" + document.body.appendChild(prompt) + + setSessionCopyProvider(() => "full session") + cleanups.push(() => setSessionCopyProvider(undefined)) + + // Cmd+A arms the flag + keydown(prompt, "a") + // An intervening Cmd+Z clears the flag (it's not C/X) + keydown(prompt, "z") + // Now Cmd+C should NOT use the provider (flag was cleared) + window.getSelection()?.removeAllRanges() + const event = keydown(prompt, "c") + + expect(event.defaultPrevented).toBe(false) + expect(bridge.posted).toHaveLength(0) + }) + + test("without a provider, mod+A then mod+C falls back to DOM selection", () => { + const bridge = framedWindow() + install(bridge.win) + const timeline = document.createElement("div") + timeline.setAttribute("data-timeline-virtual-content", "") + timeline.textContent = "DOM fallback text" + document.body.appendChild(timeline) + const prompt = document.createElement("div") + prompt.setAttribute("data-component", "prompt-input") + prompt.setAttribute("contenteditable", "true") + prompt.textContent = "" + document.body.appendChild(prompt) + + // No provider registered + setSessionCopyProvider(undefined) + + keydown(prompt, "a") + const event = keydown(prompt, "c") + + expect(event.defaultPrevented).toBe(true) + // Falls back to DOM selection text + expect(bridge.posted).toEqual([ + { source: "amicode", kind: "clipboard-write", text: "DOM fallback text" }, + ]) + }) + + // --- Undo (Cmd+Z) --- + + test("mod+Z calls execCommand undo on a contenteditable", () => { + const bridge = framedWindow() + install(bridge.win) + const el = editableDiv("hello") + + const event = keydown(el, "z") + + expect(event.defaultPrevented).toBe(true) + }) + + test("mod+Z calls execCommand undo on a form field", () => { + const bridge = framedWindow() + install(bridge.win) + const el = field("text", "hello world", 5, 5) + + const event = keydown(el, "z") + + expect(event.defaultPrevented).toBe(true) + }) + + // --- Redo (Cmd+Shift+Z / Cmd+Y) --- + + test("mod+Shift+Z calls execCommand redo", () => { + const bridge = framedWindow() + install(bridge.win) + const el = editableDiv("hello") + + const event = keydown(el, "z", { shiftKey: true }) + + expect(event.defaultPrevented).toBe(true) + }) + + test("mod+Y calls execCommand redo", () => { + const bridge = framedWindow() + install(bridge.win) + const el = editableDiv("hello") + + const event = keydown(el, "y") + + expect(event.defaultPrevented).toBe(true) + }) + + // --- Guard preservation --- + + test("mod+A in an unframed window is not intercepted", () => { + install(window) // unframed: parent === self + const el = field("text", "hello", 2, 2) + + const event = keydown(el, "a") + + expect(event.defaultPrevented).toBe(false) + }) + + test("mod+Z on a non-editable target is not intercepted", () => { + const bridge = framedWindow() + install(bridge.win) + const button = document.createElement("button") + document.body.appendChild(button) + + const event = keydown(button, "z") + + expect(event.defaultPrevented).toBe(false) + }) + + test("shift+alt chords with editing keys are still ignored", () => { + const bridge = framedWindow() + install(bridge.win) + const el = field("text", "abc", 0, 3) + + expect(keydown(el, "a", { altKey: true }).defaultPrevented).toBe(false) + }) +}) diff --git a/packages/app-bundle/overlay/packages/app/src/utils/global-clipboard.ts b/packages/app-bundle/overlay/packages/app/src/utils/global-clipboard.ts new file mode 100644 index 00000000..83366095 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/utils/global-clipboard.ts @@ -0,0 +1,373 @@ +// Framed-app clipboard fallback, generalized from the prompt input's bridge. +// Inside the VS Code webview iframe, native paste never fires and native +// copy never reaches the OS clipboard (see prompt-input/clipboard-bridge.ts +// for the full why) — so every editable silently ignores ⌘V and poisons the +// next paste on ⌘C. This module intercepts mod+V/C/X at the window's capture +// phase and routes them over the existing extension-host bridge. It is the +// SOLE ⌘V path in the webview: the prompt composers (v1 and v2) no longer +// intercept the keystroke themselves, so the text lands exactly once. +// Unframed (plain web/desktop), it does nothing — native clipboard behavior +// stands. + +import { + readClipboardImageViaBridge, + readClipboardViaBridge, + writeClipboardViaBridge, +} from "@/components/prompt-input/clipboard-bridge" + +// Single-slot media hook: a paste that carries no text is offered to the +// registered consumer (the v2 composer's attachment pipeline) as an image. +// Deliberately one slot — a list would invite two owners of one gesture. +let clipboardImageHandler: ((file: File) => void) | undefined + +export function setClipboardImageHandler(handler?: (file: File) => void): void { + clipboardImageHandler = handler +} + +// Session copy provider: registered by the session page to serialize the full +// session from the data model (messages + parts → text). The clipboard handler +// calls this on Cmd+C after a "select all" instead of reading from the DOM +// (which is incomplete due to virtualization). +let sessionCopyProvider: (() => string) | undefined + +export function setSessionCopyProvider(provider?: () => string): void { + sessionCopyProvider = provider +} + +// Flag: set by Cmd+A when targeting the prompt (signals "user wants the full +// session"), consumed by the next Cmd+C, cleared on any other keystroke. +let fullSessionCopyPending = false + +// Elements that carry their own bridged paste (the profile fields' +// pasteFallback) mark themselves so the fallback doesn't double-insert. The +// marker owns PASTE only: nothing element-local handles copy/cut, so ⌘C/⌘X +// still mirror to the OS clipboard even inside marked subtrees — otherwise +// copying from the prompt would paste stale content. +export const CLIPBOARD_SELF_SELECTOR = '[data-amc-clipboard="self"]' + +type FormField = HTMLInputElement | HTMLTextAreaElement + +// Input types that hold free text and support the selection API. Everything +// else (checkbox, file, range, date, …) has no text caret to paste at. +const TEXT_INPUT_TYPES = new Set(["text", "search", "url", "tel", "password", "email"]) + +const isFormField = (el: unknown): el is FormField => + el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement + +export function isEditableTarget(el: EventTarget | null): el is HTMLElement { + if (el instanceof HTMLInputElement) return TEXT_INPUT_TYPES.has(el.type) && !el.disabled && !el.readOnly + if (el instanceof HTMLTextAreaElement) return !el.disabled && !el.readOnly + return el instanceof HTMLElement && el.isContentEditable +} + +// email (and, in some browsers, other text-like types) throws on selection +// access — degrade to append-at-end rather than losing the paste entirely. +function fieldSelection(el: FormField): { start: number; end: number } { + try { + const start = el.selectionStart + const end = el.selectionEnd + if (start !== null && end !== null) return { start, end } + } catch { + // no selection API for this input type + } + return { start: el.value.length, end: el.value.length } +} + +function setCaret(el: FormField, at: number) { + try { + el.setSelectionRange(at, at) + } catch { + // no selection API — the value update above still landed + } +} + +// Manual dispatch discipline: setRangeText and Range edits fire no events, but +// Solid-controlled inputs only sync state from a bubbling "input" event. +function dispatchInput(el: HTMLElement, inputType: string, data?: string) { + el.dispatchEvent(new InputEvent("input", { bubbles: true, inputType, data })) +} + +export function insertTextAtSelection(el: HTMLElement, text: string): void { + if (isFormField(el)) { + const { start, end } = fieldSelection(el) + if (typeof el.setRangeText === "function") { + el.setRangeText(text, start, end) + } else { + el.value = el.value.slice(0, start) + text + el.value.slice(end) + } + // Place the caret ourselves: setRangeText's "end" selection mode is not + // reliable across DOM implementations (happy-dom lands it off-spec). + setCaret(el, start + text.length) + dispatchInput(el, "insertFromPaste", text) + return + } + + // contenteditable: prefer execCommand — the browser splices the text at the + // caret and fires the input event itself, exactly like a native paste. + const doc = el.ownerDocument + if (typeof doc.execCommand === "function") { + el.focus() + try { + if (doc.execCommand("insertText", false, text)) return + } catch { + // fall through to the manual range splice + } + } + const selection = doc.defaultView?.getSelection() + const range = selection && selection.rangeCount > 0 ? selection.getRangeAt(0) : null + const node = doc.createTextNode(text) + if (selection && range && el.contains(range.commonAncestorContainer)) { + range.deleteContents() + range.insertNode(node) + range.setStartAfter(node) + range.collapse(true) + selection.removeAllRanges() + selection.addRange(range) + } else { + // No usable selection inside the element — append rather than drop the paste. + el.appendChild(node) + } + dispatchInput(el, "insertFromPaste", text) +} + +export function extractSelection(el: HTMLElement, opts: { cut?: boolean } = {}): string { + if (isFormField(el)) { + const { start, end } = fieldSelection(el) + const text = el.value.slice(start, end) + if (!text) return "" + if (opts.cut) { + if (typeof el.setRangeText === "function") { + el.setRangeText("", start, end) + } else { + el.value = el.value.slice(0, start) + el.value.slice(end) + } + setCaret(el, start) + dispatchInput(el, "deleteByCut") + } + return text + } + + const selection = el.ownerDocument.defaultView?.getSelection() + if (!selection || selection.rangeCount === 0) return "" + const range = selection.getRangeAt(0) + // Only speak for selections that actually live inside this editable — + // cutting must never delete content the keystroke's target doesn't own. + if (!el.contains(range.commonAncestorContainer)) return "" + const text = selection.toString() + if (!text) return "" + if (opts.cut) { + range.deleteContents() // leaves the selection collapsed at the cut point + dispatchInput(el, "deleteByCut") + } + return text +} + +// Select all content in an editable element — the JS equivalent of the native +// Cmd+A that the VS Code/Electron platform layer suppresses inside the iframe. +function selectAll(target: HTMLElement): void { + if (isFormField(target)) { + target.select() + return + } + // contenteditable: select all children of the editable root + const selection = target.ownerDocument.defaultView?.getSelection() + if (selection) { + selection.removeAllRanges() + const range = target.ownerDocument.createRange() + range.selectNodeContents(target) + selection.addRange(range) + } +} + +// Capture-phase so it sees the keystroke before any component handler, and +// window-level so portaled UI (popovers, dialogs) is covered too. Returns an +// uninstall function; the handler re-checks framing per event, so installing +// unconditionally at startup is safe everywhere. +export function installGlobalClipboardFallback(win: Window = window): () => void { + const onKeyDown = (event: KeyboardEvent) => { + if (win.parent === win) return // unframed: native clipboard works — stay out + if (!(event.metaKey || event.ctrlKey) || event.altKey) return + if (event.isComposing) return + const key = event.key.toLowerCase() + + // Redo: Cmd+Shift+Z (shift allowed only for this chord) + const isRedo = key === "z" && event.shiftKey + // All other editing shortcuts require NO shift + if (event.shiftKey && !isRedo) return + + if (key !== "v" && key !== "c" && key !== "x" && key !== "a" && key !== "z" && key !== "y") return + const target = event.target + + // Clear the full-session flag on any keystroke that isn't the copy that + // consumes it. Cmd+A sets it; only the immediately following Cmd+C uses it. + if (!(key === "c" || key === "x")) { + fullSessionCopyPending = false + } + + // --- Non-editable targets (rendered messages, code blocks) --- + // Inside the VS Code webview iframe, Electron intercepts Cmd+C at the host + // level before a `copy` event fires in the iframe DOM. Route C/X/A through + // the same bridge the context menu uses successfully. + if (!isEditableTarget(target)) { + if (key === "c" || key === "x") { + // Cmd+X on non-editable = copy-only (cannot delete from rendered DOM) + // If fullSessionCopyPending, prefer the provider (full data-model copy) + if (fullSessionCopyPending && sessionCopyProvider) { + const fullText = sessionCopyProvider() + if (fullText) { + event.preventDefault() + writeClipboardViaBridge(fullText, win) + fullSessionCopyPending = false + return + } + } + fullSessionCopyPending = false + const selection = win.getSelection() + const text = selection?.toString() ?? "" + if (!text) return // no selection: no-op (clipboard unchanged) + event.preventDefault() + writeClipboardViaBridge(text, win) + return + } + if (key === "a") { + event.preventDefault() + // Scope select-all to the preview panel if the target is inside one + const panel = target instanceof Element && target.closest('#review-panel:not([aria-hidden="true"])') + if (panel) { + const content = panel.querySelector('[data-slot="session-review-v2-preview"]') ?? panel + const selection = win.getSelection() + if (selection) { + selection.removeAllRanges() + const range = win.document.createRange() + range.selectNodeContents(content) + selection.addRange(range) + } + return + } + // Otherwise select the full chat area + arm the session copy flag + fullSessionCopyPending = !!sessionCopyProvider + const selection = win.getSelection() + if (selection) { + selection.removeAllRanges() + const range = win.document.createRange() + const timeline = win.document.querySelector("[data-timeline-virtual-content]") + range.selectNodeContents(timeline ?? win.document.body) + selection.addRange(range) + } + return + } + // V/Z/Y on non-editables: no-op (nothing to paste into or undo) + return + } + + // --- Select all --- + if (key === "a") { + event.preventDefault() + // If the target is inside the review/file panel, select that panel's content + const panel = target.closest('#review-panel:not([aria-hidden="true"])') + if (panel) { + const content = panel.querySelector('[data-slot="session-review-v2-preview"]') ?? panel + const selection = win.getSelection() + if (selection) { + selection.removeAllRanges() + const range = win.document.createRange() + range.selectNodeContents(content) + selection.addRange(range) + } + return + } + // If the prompt composer is EMPTY, "select all" means the full chat session. + // If the prompt has content, standard select-all (select the draft text). + const promptEl = target.closest('[data-component="prompt-input"]') + if (promptEl) { + const hasContent = (promptEl.textContent ?? "").trim().length > 0 + if (!hasContent) { + fullSessionCopyPending = !!sessionCopyProvider + // Visual feedback: select the timeline DOM (best-effort, may be partial + // due to virtualization — the actual copy comes from the provider) + const timeline = win.document.querySelector("[data-timeline-virtual-content]") + const selection = win.getSelection() + if (selection && timeline) { + selection.removeAllRanges() + const range = win.document.createRange() + range.selectNodeContents(timeline) + selection.addRange(range) + } + return + } + } + selectAll(target) + return + } + + // --- Undo / Redo --- + if (key === "z" || key === "y") { + event.preventDefault() + const doc = target.ownerDocument + if (typeof doc.execCommand === "function") { + doc.execCommand(isRedo || key === "y" ? "redo" : "undo") + } + return + } + + // --- Clipboard: paste, copy, cut --- + if (key === "v") { + if (target.closest(CLIPBOARD_SELF_SELECTOR)) return // element owns its own paste + // Native paste never fires in-frame, so preventDefault loses nothing; + // an empty or dead bridge reply degrades to a no-op (see clipboard-bridge). + event.preventDefault() + void readClipboardViaBridge(win).then(async (text) => { + if (text) { + insertTextAtSelection(target, text) + return + } + // No text on the clipboard: offer media. Mirrors the composer's own + // handlePaste precedence (image only when there is no plain text), so + // text pastes keep their exact single-value sequence with no extra + // round-trip. + if (!clipboardImageHandler) return + const file = await readClipboardImageViaBridge(win) + if (file) clipboardImageHandler(file) + }) + return + } + + const text = extractSelection(target, { cut: key === "x" }) + if (!text) { + // Full-session copy: if Cmd+A armed the flag and a provider exists, use + // the data model (complete, not limited by DOM virtualization). + if (fullSessionCopyPending && sessionCopyProvider) { + const fullText = sessionCopyProvider() + if (fullText) { + event.preventDefault() + writeClipboardViaBridge(fullText, win) + } + fullSessionCopyPending = false + return + } + fullSessionCopyPending = false + // Fallback: a prior Cmd+A may have placed the selection on the chat session + // content (outside this editable). Copy whatever is selected in the DOM. + const selection = win.getSelection() + if (selection && selection.rangeCount > 0 && !selection.isCollapsed) { + const range = selection.getRangeAt(0) + // Only bridge-copy when the selection lives OUTSIDE this editable — if it + // were inside, extractSelection above would have found it already. + if (!target.contains(range.commonAncestorContainer)) { + const docText = selection.toString() + if (docText) { + event.preventDefault() + writeClipboardViaBridge(docText, win) + } + } + } + return + } + fullSessionCopyPending = false + event.preventDefault() + writeClipboardViaBridge(text, win) + } + + win.addEventListener("keydown", onKeyDown, true) + return () => win.removeEventListener("keydown", onKeyDown, true) +} diff --git a/packages/app-bundle/overlay/packages/app/src/utils/pane-bridge.ts b/packages/app-bundle/overlay/packages/app/src/utils/pane-bridge.ts new file mode 100644 index 00000000..036b9836 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/utils/pane-bridge.ts @@ -0,0 +1,37 @@ +// amicode(workbench S2): the parent ⇄ pane-instance command bridge. One +// envelope source so it can be told apart from the generic amicode bridges +// (clipboard/theme/route-info). The parent owns drop resolution and issues +// open/close/focus commands; each instance reports its tab list (the mirror +// the parent resolves moves against — the ack is the next tabs-changed that +// contains the expected change). + +export type WorkbenchTabReport = { tabs: string[]; active?: string } + +export type WorkbenchMessage = + | { kind: "tabs-changed"; paneId: string; report: WorkbenchTabReport } + | { kind: "drag-tab-start"; path: string } + | { kind: "drag-tab-end" } + | { kind: "open-tab"; path: string; activate?: boolean } + | { kind: "close-tab"; path: string } + | { kind: "focus-tab"; path: string } + +export const WORKBENCH_SOURCE = "amicode-workbench" + +export const isWorkbenchMessage = (d: unknown): d is WorkbenchMessage & { source: string } => + !!d && typeof d === "object" && (d as { source?: unknown }).source === WORKBENCH_SOURCE + +/** Parent ⇄ instance post. targetOrigin stays explicit at call sites. */ +export function postWorkbench(target: Window, msg: WorkbenchMessage, targetOrigin: string): void { + target.postMessage({ source: WORKBENCH_SOURCE, ...msg }, targetOrigin) +} + +export function listenWorkbench(handler: (msg: WorkbenchMessage, event: MessageEvent) => void): () => void { + const onMsg = (e: MessageEvent) => { + const d = e.data as unknown + if (!isWorkbenchMessage(d)) return + const { source: _source, ...msg } = d as { source: string } & WorkbenchMessage + handler(msg, e) + } + window.addEventListener("message", onMsg) + return () => window.removeEventListener("message", onMsg) +} diff --git a/packages/app-bundle/overlay/packages/app/src/utils/serialize-session.test.ts b/packages/app-bundle/overlay/packages/app/src/utils/serialize-session.test.ts new file mode 100644 index 00000000..3a6707a0 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/utils/serialize-session.test.ts @@ -0,0 +1,159 @@ +import { describe, expect, test } from "bun:test" +import { serializeSession } from "./serialize-session" +import type { Message, Part } from "@opencode-ai/sdk/v2" + +function userMsg(id: string): Message { + return { + id, + sessionID: "s1", + role: "user", + time: { created: 1 }, + agent: "default", + model: { providerID: "test", modelID: "test" }, + } +} + +function assistantMsg(id: string): Message { + return { + id, + sessionID: "s1", + role: "assistant", + time: { created: 2 }, + parentID: "m1", + modelID: "test", + providerID: "test", + mode: "default", + agent: "default", + path: { cwd: "/", root: "/" }, + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + } +} + +function textPart(messageID: string, text: string, opts?: { ignored?: boolean; synthetic?: boolean }): Part { + return { + id: `p-${messageID}-${Math.random().toString(36).slice(2, 6)}`, + sessionID: "s1", + messageID, + type: "text", + text, + ignored: opts?.ignored, + synthetic: opts?.synthetic, + } +} + +function toolPart(messageID: string): Part { + return { + id: `t-${messageID}`, + sessionID: "s1", + messageID, + type: "tool", + callID: "call1", + tool: "bash", + state: { + status: "completed", + input: {}, + output: "done", + title: "bash", + metadata: {}, + time: { start: 1, end: 2 }, + }, + } +} + +describe("serializeSession", () => { + test("formats a simple user + assistant exchange", () => { + const messages: Message[] = [userMsg("m1"), assistantMsg("m2")] + const parts: Record = { + m1: [textPart("m1", "How do I list files?")], + m2: [textPart("m2", "Use `ls` to list files in the current directory.")], + } + + const result = serializeSession(messages, (id) => parts[id] ?? []) + + expect(result).toBe( + "User:\nHow do I list files?\n\nAssistant:\nUse `ls` to list files in the current directory.", + ) + }) + + test("concatenates multiple text parts within a single message", () => { + const messages: Message[] = [assistantMsg("m1")] + const parts: Record = { + m1: [textPart("m1", "First chunk. "), textPart("m1", "Second chunk.")], + } + + const result = serializeSession(messages, (id) => parts[id] ?? []) + + expect(result).toBe("Assistant:\nFirst chunk. Second chunk.") + }) + + test("skips messages with no text parts", () => { + const messages: Message[] = [userMsg("m1"), assistantMsg("m2"), assistantMsg("m3")] + const parts: Record = { + m1: [textPart("m1", "hello")], + m2: [toolPart("m2")], // only a tool call, no text + m3: [textPart("m3", "done")], + } + + const result = serializeSession(messages, (id) => parts[id] ?? []) + + expect(result).toBe("User:\nhello\n\nAssistant:\ndone") + }) + + test("skips ignored and synthetic text parts", () => { + const messages: Message[] = [assistantMsg("m1")] + const parts: Record = { + m1: [ + textPart("m1", "visible"), + textPart("m1", " ignored", { ignored: true }), + textPart("m1", " synthetic", { synthetic: true }), + ], + } + + const result = serializeSession(messages, (id) => parts[id] ?? []) + + expect(result).toBe("Assistant:\nvisible") + }) + + test("skips messages where all text is whitespace", () => { + const messages: Message[] = [userMsg("m1"), assistantMsg("m2")] + const parts: Record = { + m1: [textPart("m1", " \n ")], + m2: [textPart("m2", "actual content")], + } + + const result = serializeSession(messages, (id) => parts[id] ?? []) + + expect(result).toBe("Assistant:\nactual content") + }) + + test("returns empty string for an empty session", () => { + const result = serializeSession([], () => []) + expect(result).toBe("") + }) + + test("trims trailing whitespace from message text", () => { + const messages: Message[] = [userMsg("m1")] + const parts: Record = { + m1: [textPart("m1", "hello world \n\n")], + } + + const result = serializeSession(messages, (id) => parts[id] ?? []) + + expect(result).toBe("User:\nhello world") + }) + + test("handles a multi-turn conversation", () => { + const messages: Message[] = [userMsg("m1"), assistantMsg("m2"), userMsg("m3"), assistantMsg("m4")] + const parts: Record = { + m1: [textPart("m1", "What is 2+2?")], + m2: [textPart("m2", "4")], + m3: [textPart("m3", "And 3+3?")], + m4: [textPart("m4", "6")], + } + + const result = serializeSession(messages, (id) => parts[id] ?? []) + + expect(result).toBe("User:\nWhat is 2+2?\n\nAssistant:\n4\n\nUser:\nAnd 3+3?\n\nAssistant:\n6") + }) +}) diff --git a/packages/app-bundle/overlay/packages/app/src/utils/serialize-session.ts b/packages/app-bundle/overlay/packages/app/src/utils/serialize-session.ts new file mode 100644 index 00000000..09911f3e --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/utils/serialize-session.ts @@ -0,0 +1,37 @@ +// Serialize a session's messages and parts into human-readable markdown text. +// Used by the clipboard "copy full session" flow — the data-model path that +// doesn't depend on DOM selection or virtualized rendering. + +import type { Message, Part } from "@opencode-ai/sdk/v2" + +export type PartsAccessor = (messageID: string) => readonly Part[] + +/** + * Serialize a session into markdown-formatted text with role labels. + * + * Format: + * User: + * + * + * Assistant: + * + * + * Only text parts are included — tool calls, reasoning, step markers, and + * other structural parts are omitted for readability. + */ +export function serializeSession(messages: readonly Message[], getParts: PartsAccessor): string { + const blocks: string[] = [] + + for (const msg of messages) { + const textParts = getParts(msg.id).filter((p) => p.type === "text" && !p.ignored && !p.synthetic) + if (textParts.length === 0) continue + + const role = msg.role === "user" ? "User" : "Assistant" + const text = textParts.map((p) => (p as { text: string }).text).join("") + if (!text.trim()) continue + + blocks.push(`${role}:\n${text.trimEnd()}`) + } + + return blocks.join("\n\n") +} diff --git a/packages/app-bundle/overlay/packages/app/src/utils/server-compat.ts b/packages/app-bundle/overlay/packages/app/src/utils/server-compat.ts new file mode 100644 index 00000000..b928d971 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/utils/server-compat.ts @@ -0,0 +1,532 @@ +import type { ServerApi } from "./server" +import type { ServerProtocol } from "./server-protocol" +import type { AgentPartInput, FilePartInput, OpencodeClient, Session, TextPartInput } from "@opencode-ai/sdk/v2/client" +import type { + Project, + ProjectCurrent, + SessionApi, + SessionCommandInput, + SessionCommandOutput, + SessionCompactInput, + SessionCompactOutput, + SessionInfo, + SessionPromptInput, + SessionPromptOutput, + SessionShellInput, + SessionShellOutput, +} from "@opencode-ai/client/promise" + +type LegacyClient = OpencodeClient +type LegacyFor = (directory?: string) => LegacyClient +type CompatibleSessionApi = Omit< + SessionApi, + "prompt" | "command" | "shell" | "compact" | "rename" | "archive" | "remove" +> & { + prompt: (input: SessionPromptInput & LegacyPrompt) => Promise + command: (input: SessionCommandInput) => Promise + shell: (input: SessionShellInput & LegacyPrompt) => Promise + compact: (input: SessionCompactInput & { model?: LegacyPrompt["model"] }) => Promise + rename: (input: Parameters[0] & LegacyLocation) => ReturnType + // archive: (input: Parameters[0] & LegacyLocation) => ReturnType + remove: (input: Parameters[0] & LegacyLocation) => ReturnType +} +type CompatiblePermissionApi = Omit & { + reply: ( + input: Parameters[0] & { location?: { directory?: string } }, + ) => ReturnType +} +export type CompatibleApi = Omit & { + readonly session: CompatibleSessionApi + readonly permission: CompatiblePermissionApi + readonly question: CompatibleQuestionApi +} +type LegacyPrompt = { + agent?: string + model?: { providerID: string; modelID: string } + variant?: string + legacyParts?: (TextPartInput | FilePartInput | AgentPartInput)[] +} +type LegacyLocation = { directory?: string } +type CompatibleQuestionApi = Omit & { + /** Both take the REQUESTING session's directory via `location` (amicode#249 + * QA): the server's pending-question registry is instance-per-directory, + * and an unscoped reply lands on the server-cwd instance — "reply for + * unknown request" for any session living elsewhere (every bug-dock + * answer in the amicode preview). */ + reply( + value: Parameters[0] & { location?: LegacyLocation }, + ): ReturnType + reject( + value: Parameters[0] & { location?: LegacyLocation }, + ): ReturnType +} +type CompatibleInput = { + protocol: Promise + current: ServerApi + legacy: LegacyFor + directory?: string +} + +function mime(uri: string) { + const match = /^data:([^;,]+)/.exec(uri) + return match?.[1] ?? "application/octet-stream" +} + +function sessionInfo(session: Session): SessionInfo { + return { + id: session.id, + parentID: session.parentID, + projectID: session.projectID, + agent: session.agent, + model: session.model && { + id: session.model.id, + providerID: session.model.providerID, + variant: session.model.variant, + }, + cost: session.cost ?? 0, + tokens: session.tokens ?? { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + time: session.time, + title: session.title, + location: { directory: session.directory, workspaceID: session.workspaceID }, + subpath: session.path, + revert: session.revert && { + messageID: session.revert.messageID, + partID: session.revert.partID, + snapshot: session.revert.snapshot, + }, + } +} + +export function createCompatibleApi(input: CompatibleInput): CompatibleApi { + const v1 = createV1Api(input) + return lazyApi( + input.protocol.then((protocol) => (protocol === "v1" ? v1 : input.current)), + input.current, + ) +} + +function lazyApi(implementation: Promise, shape: T): T { + const cache = new Map() + return new Proxy(shape, { + get(target, property, receiver) { + const sample = Reflect.get(target, property, receiver) + if (typeof sample === "function") { + return (...args: unknown[]) => + implementation.then((value) => { + const method = Reflect.get(value, property) + if (typeof method !== "function") throw new Error(`API method unavailable: ${String(property)}`) + return Reflect.apply(method, value, args) + }) + } + if (sample === null || typeof sample !== "object") return sample + if (cache.has(property)) return cache.get(property) + const nested = lazyApi( + implementation.then((value) => { + const result = Reflect.get(value, property) + if (result === null || typeof result !== "object") { + throw new Error(`API namespace unavailable: ${String(property)}`) + } + return result + }), + sample, + ) + cache.set(property, nested) + return nested + }, + }) +} + +function createV1Api(input: CompatibleInput): CompatibleApi { + const directory = (location?: { directory?: string }) => location?.directory ?? input.directory + const legacy = (location?: { directory?: string }) => input.legacy(directory(location)) + const located = (data: T, value?: { directory?: string }) => ({ + location: { + directory: directory(value) ?? "", + project: { id: "", directory: directory(value) ?? "" }, + }, + data, + }) + + return { + ...input.current, + session: { + ...input.current.session, + async list( + value?: Parameters[0], + options?: Parameters[1], + ) { + if (!value?.directory && value?.search !== undefined) { + const result = await legacy().experimental.session.list( + { + roots: value.parentID === null ? true : undefined, + search: value.search, + limit: value.limit, + }, + options, + ) + return { data: (result.data ?? []).map(sessionInfo), cursor: {} } + } + const result = await legacy({ directory: value?.directory }).session.list({ + directory: value?.directory, + roots: value?.parentID === null ? true : undefined, + search: value?.search, + limit: value?.limit, + }) + return { data: (result.data ?? []).map(sessionInfo), cursor: {} } + }, + async create(value?: Parameters[0]) { + const result = await legacy(value?.location ?? undefined).session.create({ + directory: directory(value?.location ?? undefined), + }) + if (!result.data) throw new Error("Failed to create session") + return sessionInfo(result.data) + }, + async get(value: Parameters[0]) { + const result = await legacy().session.get(value) + if (!result.data) throw new Error(`Session not found: ${value.sessionID}`) + return sessionInfo(result.data) + }, + async active() { + const result = await legacy().session.status() + return Object.fromEntries( + Object.entries(result.data ?? {}).flatMap(([sessionID, status]) => + status.type === "idle" ? [] : [[sessionID, { type: "running" as const }]], + ), + ) + }, + async rename(value: Parameters[0] & LegacyLocation) { + await legacy(value).session.update({ sessionID: value.sessionID, title: value.title }) + }, + // async archive(value: Parameters[0] & LegacyLocation) { + // await legacy(value).session.update({ sessionID: value.sessionID, time: { archived: Date.now() } }) + // }, + async remove(value: Parameters[0] & LegacyLocation) { + await legacy(value).session.delete(value) + }, + async fork(value: Parameters[0]) { + const result = await legacy().session.fork(value) + if (!result.data) throw new Error("Failed to fork session") + return sessionInfo(result.data) + }, + async interrupt(value: Parameters[0]) { + await legacy().session.abort(value) + }, + async prompt(value: SessionPromptInput & LegacyPrompt) { + await legacy().session.promptAsync({ + sessionID: value.sessionID, + messageID: value.id ?? undefined, + agent: value.agent, + model: value.model, + variant: value.variant, + parts: value.legacyParts ?? [ + { type: "text", text: value.text }, + ...(value.files ?? []).map((file) => ({ + type: "file" as const, + mime: file.mention ? "text/plain" : mime(file.uri), + url: file.uri, + filename: file.name, + source: file.mention + ? { + type: "file" as const, + text: { value: file.mention.text, start: file.mention.start, end: file.mention.end }, + path: file.uri, + } + : undefined, + })), + ...(value.agents ?? []).map((agent) => ({ + type: "agent" as const, + name: agent.name, + source: agent.mention + ? { value: agent.mention.text, start: agent.mention.start, end: agent.mention.end } + : undefined, + })), + ], + }) + return { + admittedSeq: 0, + id: value.id ?? "", + sessionID: value.sessionID, + timeCreated: Date.now(), + type: "user", + data: { text: value.text }, + delivery: value.delivery ?? "steer", + } + }, + async command(value: SessionCommandInput) { + await legacy().session.command({ + sessionID: value.sessionID, + messageID: value.id ?? undefined, + command: value.command, + arguments: value.arguments ?? "", + agent: value.agent ?? undefined, + model: value.model ? `${value.model.providerID}/${value.model.id}` : undefined, + variant: value.model?.variant, + parts: value.files?.map((file) => ({ + type: "file" as const, + mime: mime(file.uri), + url: file.uri, + filename: file.name, + })), + }) + return { + admittedSeq: 0, + id: value.id ?? "", + sessionID: value.sessionID, + timeCreated: Date.now(), + type: "user", + data: { text: `/${value.command} ${value.arguments ?? ""}`.trim() }, + delivery: value.delivery ?? "steer", + } + }, + async shell(value: SessionShellInput & LegacyPrompt) { + await legacy().session.shell({ + sessionID: value.sessionID, + command: value.command, + agent: value.agent, + model: value.model, + }) + }, + compact: async (value: SessionCompactInput & { model?: LegacyPrompt["model"] }) => { + if (!value.model) throw new Error("A model is required to compact a V1 session") + await legacy().session.summarize({ + sessionID: value.sessionID, + providerID: value.model.providerID, + modelID: value.model.modelID, + }) + return { + admittedSeq: 0, + id: value.id ?? "", + sessionID: value.sessionID, + timeCreated: Date.now(), + type: "compaction", + } + }, + revert: { + stage: async (value: Parameters[0]) => { + await legacy().session.revert(value) + return { messageID: value.messageID } + }, + clear: async (value: Parameters[0]) => { + await legacy().session.unrevert(value) + }, + commit: input.current.session.revert.commit, + }, + }, + project: { + ...input.current.project, + async list() { + return ((await legacy().project.list()).data ?? []) as Project[] + }, + async current(value?: Parameters[0]) { + const result = await legacy(value?.location).project.current() + if (!result.data) throw new Error("Project not found") + return { id: result.data.id, directory: result.data.worktree } satisfies ProjectCurrent + }, + // async update(value: Parameters[0]) { + // const project = (await legacy().project.list()).data?.find((item) => item.id === value.projectID) + // const result = await legacy({ directory: project?.worktree }).project.update({ + // ...value, + // directory: project?.worktree, + // }) + // if (!result.data) throw new Error(`Project not found: ${value.projectID}`) + // return result.data as Project + // }, + async directories(value: Parameters[0]) { + const result = await legacy(value.location).worktree.list() + return (result.data ?? []).map((item) => ({ directory: item })) + }, + }, + // path: { + // ...input.current.path, + // async get(value?: Parameters[0]) { + // const result = await legacy(value?.location).path.get() + // if (!result.data) throw new Error("Path unavailable") + // return result.data + // }, + // }, + vcs: { + ...input.current.vcs, + // async get(value?: Parameters[0]) { + // const result = await legacy(value?.location).vcs.get() + // return located({ branch: result.data?.branch, defaultBranch: result.data?.default_branch }, value?.location) + // }, + async status(value?: Parameters[0]) { + const result = await legacy(value?.location).vcs.status() + return located(result.data ?? [], value?.location) + }, + async diff(value: Parameters[0]) { + const result = await legacy(value.location).vcs.diff({ + mode: value.mode === "working" ? "git" : value.mode, + context: value.context, + }) + return located( + (result.data ?? []).map((file) => ({ + file: file.file, + patch: file.patch ?? "", + additions: file.additions, + deletions: file.deletions, + status: file.status ?? "modified", + })), + value.location, + ) + }, + }, + file: { + ...input.current.file, + async list(value?: Parameters[0]) { + const result = await legacy(value?.location).file.list({ path: value?.path ?? "" }) + return located(result.data ?? [], value?.location) + }, + async find(value: Parameters[0]) { + const result = await legacy(value.location).find.files({ + query: value.query, + dirs: value.type === undefined ? undefined : value.type === "directory" ? "true" : "false", + limit: value.limit, + }) + return located( + (result.data ?? []).map((path) => ({ path, type: value.type ?? "file" })), + value.location, + ) + }, + }, + integration: { + ...input.current.integration, + async get(value: Parameters[0]) { + const methods = ((await legacy(value.location).provider.auth()).data?.[value.integrationID] ?? []).map( + (method, index) => + method.type === "api" + ? { type: "key" as const, label: method.label } + : { type: "oauth" as const, id: String(index), label: method.label, prompts: method.prompts }, + ) + return located( + { + id: value.integrationID, + name: value.integrationID, + methods, + connections: [], + }, + value.location, + ) + }, + connect: { + ...input.current.integration.connect, + key: async (value: Parameters[0]) => { + await legacy(value.location).auth.set({ + providerID: value.integrationID, + auth: { type: "api", key: value.key }, + }) + await legacy(value.location).instance.dispose() + await input.legacy().instance.dispose() + }, + }, + oauth: { + ...input.current.integration.oauth, + connect: async (value: Parameters[0]) => { + const method = Number(value.methodID) + const result = await legacy(value.location).provider.oauth.authorize( + { providerID: value.integrationID, method, inputs: value.inputs }, + { throwOnError: true }, + ) + if (!result.data) throw new Error("Failed to start OAuth authorization") + return located( + { + attemptID: `${value.integrationID}:${method}`, + url: result.data.url, + instructions: result.data.instructions, + mode: result.data.method, + time: { created: Date.now(), expires: Date.now() + 10 * 60 * 1000 }, + }, + value.location, + ) + }, + complete: async (value: Parameters[0]) => { + const method = Number(value.attemptID.split(":").at(-1)) + await legacy(value.location).provider.oauth.callback( + { providerID: value.integrationID, method, code: value.code }, + { throwOnError: true }, + ) + await legacy(value.location).instance.dispose() + await input.legacy().instance.dispose() + }, + status: async (value: Parameters[0]) => { + const method = Number(value.attemptID.split(":").at(-1)) + await legacy(value.location).provider.oauth.callback( + { providerID: value.integrationID, method }, + { throwOnError: true }, + ) + await legacy(value.location).instance.dispose() + await input.legacy().instance.dispose() + return located( + { status: "complete" as const, time: { created: Date.now(), expires: Date.now() } }, + value.location, + ) + }, + }, + }, + pty: { + ...input.current.pty, + // async shells(value?: Parameters[0]) { + // return located((await legacy(value?.location).pty.shells()).data ?? [], value?.location) + // }, + async list(value?: Parameters[0]) { + return located((await legacy(value?.location).pty.list()).data ?? [], value?.location) + }, + async create(value?: Parameters[0]) { + const result = await legacy(value?.location).pty.create({ + command: value?.command, + args: value?.args ? [...value.args] : undefined, + cwd: value?.cwd, + title: value?.title, + env: value?.env, + }) + if (!result.data) throw new Error("Failed to create terminal") + return located(result.data, value?.location) + }, + async get(value: Parameters[0]) { + const result = await legacy(value.location).pty.get({ ptyID: value.ptyID }) + if (!result.data) throw new Error(`Terminal not found: ${value.ptyID}`) + return located(result.data, value.location) + }, + async update(value: Parameters[0]) { + const result = await legacy(value.location).pty.update({ + ptyID: value.ptyID, + title: value.title, + size: value.size, + }) + if (!result.data) throw new Error(`Terminal not found: ${value.ptyID}`) + return located(result.data, value.location) + }, + async remove(value: Parameters[0]) { + await legacy(value.location).pty.remove({ ptyID: value.ptyID }) + }, + // async connectToken(value: Parameters[0]) { + // const result = await legacy(value.location).pty.connectToken({ ptyID: value.ptyID }) + // if (!result.data) throw new Error(`Failed to connect terminal: ${value.ptyID}`) + // return located(result.data, value.location) + // }, + }, + permission: { + ...input.current.permission, + async reply(value: Parameters[0] & { location?: { directory?: string } }) { + await legacy(value.location).permission.respond({ + sessionID: value.sessionID, + permissionID: value.requestID, + response: value.reply, + directory: directory(value.location), + }) + }, + }, + question: { + ...input.current.question, + async reply(value: Parameters[0] & { location?: LegacyLocation }) { + await legacy(value.location).question.reply({ + requestID: value.requestID, + answers: value.answers.map((answer) => [...answer]), + }) + }, + async reject(value: Parameters[0] & { location?: LegacyLocation }) { + await legacy(value.location).question.reject({ requestID: value.requestID }) + }, + }, + } +} diff --git a/packages/app-bundle/overlay/packages/app/src/utils/start-prompt.ts b/packages/app-bundle/overlay/packages/app/src/utils/start-prompt.ts new file mode 100644 index 00000000..fe1ea555 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/utils/start-prompt.ts @@ -0,0 +1,32 @@ +// amicode: shared prompt-inject helper (extracted from session-new-view.tsx's +// starter chips; also used by the Vaults tab's "Manage vaults" action). Sets +// the composer draft through the prompt context (the submit button's blank() +// gate reads the same store), then clicks the composer's own submit button so +// the REAL handleSubmit runs (worktree resolution, session.create, navigation, +// optimistic UI). Degradation: if no enabled submit button is found, the text +// stays pre-filled and the editor is focused — user hits Enter. +// NOTE: takes the resolved prompt context as an argument — this module is not +// a component and must not call usePrompt() itself. +import type { usePrompt } from "@/context/prompt" + +type PromptContext = ReturnType + +export function startPrompt(prompt: PromptContext, text: string) { + prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length) + requestAnimationFrame(() => { + const button = document.querySelector('[data-action="prompt-submit"]:not([disabled])') + if (button) button.click() + else document.querySelector('[data-component="prompt-input"]')?.focus() + }) +} + +/** Draft-only variant (spec B): prefill the composer and focus it WITHOUT + * submitting — used by per-field "Edit in chat" where the user completes the + * value first. This is startPrompt's no-submit-button fallback branch, + * promoted to a deliberate action. */ +export function draftPrompt(prompt: PromptContext, text: string) { + prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length) + requestAnimationFrame(() => { + document.querySelector('[data-component="prompt-input"]')?.focus() + }) +} diff --git a/packages/app-bundle/overlay/packages/app/src/utils/web-zoom.test.ts b/packages/app-bundle/overlay/packages/app/src/utils/web-zoom.test.ts new file mode 100644 index 00000000..a8295668 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/utils/web-zoom.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, test } from "bun:test" +import { webZoom, webZoomIn, webZoomOut, webZoomReset } from "./web-zoom" + +// Zoom routing (amicode#266): ONE zoom owner — the app document itself. The +// workbench can never see keydowns from inside the webview documents +// (cross-origin iframe), so there is no bridge, no "framed" branch, and no +// host interception to route around: webZoomIn/Out/Reset always apply the +// in-app CSS zoom, and the module-scope capture listener (registered on +// import, below) applies it from the raw chords in every document that loads +// the bundle — the main frame and each split pane. + +describe("web-zoom signal (amicode#266)", () => { + test("zoom in/out/reset always move the in-app signal", () => { + const before = webZoom() + webZoomIn() + expect(webZoom()).toBe(Math.round((before + 0.1) * 100) / 100) + webZoomOut() + expect(webZoom()).toBe(Math.round(before * 100) / 100) + webZoomReset() + expect(webZoom()).toBe(1) + }) + + test("the raw chord listener is registered on the document", () => { + webZoomReset() + const fired = new KeyboardEvent("keydown", { key: "=", ctrlKey: true, cancelable: true }) + document.dispatchEvent(fired) + expect(fired.defaultPrevented).toBe(true) + expect(webZoom()).toBe(1.1) + }) +}) diff --git a/packages/app-bundle/overlay/packages/app/src/utils/web-zoom.ts b/packages/app-bundle/overlay/packages/app/src/utils/web-zoom.ts new file mode 100644 index 00000000..a5d5ea66 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/utils/web-zoom.ts @@ -0,0 +1,104 @@ +// amicode: in-app zoom for the WEB host (the plain browser at :3002 and the +// amicode VS Code webview). The desktop build zooms through Electron (menu +// roles); the web build had NO zoom at all — Cmd+=/-/0 fell through to the +// host, which zooms the whole editor window (or the browser tab), never the +// app. This module owns a CSS zoom on the document root, persisted +// per-window, and feeds the same platform.webviewZoom signal the titlebar +// and terminal already watch. +// +// One zoom owner (harmoniqs/amicode#266): the app document itself. The +// workbench can never see keydowns from inside the webview documents +// (cross-origin iframe; the host page's forwarding covers only the host +// page), so the extension cannot translate them into workbench zoom actions. +// Instead every app document — the main frame and each split pane — captures +// the zoom chords itself and applies its own CSS zoom: with the webview +// panel focused, the zoomed content is the webview; with an editor tab +// focused, the workbench's own native window zoom applies, unchanged. +import { createSignal } from "solid-js" + +const KEY = "amicode-zoom" +const MIN = 0.5 +const MAX = 3 +const STEP = 0.1 + +const clamp = (value: number) => Math.min(Math.max(value, MIN), MAX) + +function readSaved(): number { + if (typeof localStorage === "undefined") return 1 + const parsed = Number.parseFloat(localStorage.getItem(KEY) ?? "1") + return Number.isFinite(parsed) ? clamp(parsed) : 1 +} + +const [webZoom, setSignal] = createSignal(readSaved()) + +function apply(zoom: number) { + if (typeof document === "undefined") return + const html = document.documentElement + if (zoom === 1) { + html.style.removeProperty("zoom") + html.style.removeProperty("transform") + html.style.removeProperty("transform-origin") + } else { + // Try CSS zoom first (better text rendering), fallback to transform + html.style.setProperty("zoom", String(zoom)) + // Also set transform as backup for browsers that don't support zoom + html.style.setProperty("transform", `scale(${zoom})`) + html.style.setProperty("transform-origin", "top left") + } +} + +// Apply zoom immediately and also after a small delay to ensure DOM is ready +apply(webZoom()) +if (typeof document !== "undefined") { + document.addEventListener("DOMContentLoaded", () => apply(webZoom()), { once: true }) + // Fallback: also try after a short timeout in case DOMContentLoaded already fired + setTimeout(() => apply(webZoom()), 0) +} + +export function setWebZoom(next: number) { + const zoom = Math.round(clamp(next) * 100) / 100 + apply(zoom) + setSignal(zoom) + try { + localStorage.setItem(KEY, String(zoom)) + } catch { + // storage may be unavailable in a sandboxed webview — zoom still applies + } +} + +export const webZoomIn = () => setWebZoom(webZoom() + STEP) +export const webZoomOut = () => setWebZoom(webZoom() - STEP) +export const webZoomReset = () => setWebZoom(1) +export { webZoom } + +// Raw chord capture (harmoniqs/amicode#266). The command registry's exact +// (normalized-key, modifier-mask) lookup cannot express "the key that means +// +" across layouts: Ctrl+Plus on US IS Ctrl+Shift+"=", arriving as key "+" +// with the shift bit — wrong key AND wrong mask — while the numpad "+" +// arrives unshifted and on DE/FR/Nordic layouts even the canonical "=" +// carries shift. Rather than widen the registry's chords (and its tooltips, +// palette, and collision surface), the zoom chords are captured directly by +// each app document, matching the key the layout PRODUCES — the same +// physical key yields one of "="/"+"/"-"/"_" on every layout, with the shift +// bit deliberately ignored. "0" is unshifted on every layout we ship to and +// stays unambiguous. +const ZOOM_IN_KEYS = new Set(["=", "+"]) +const ZOOM_OUT_KEYS = new Set(["-", "_"]) +const ZOOM_RESET_KEYS = new Set(["0"]) + +if (typeof document !== "undefined") { + document.addEventListener( + "keydown", + (event) => { + if (!event.metaKey && !event.ctrlKey) return + const key = event.key + if (ZOOM_IN_KEYS.has(key)) setWebZoom(webZoom() + STEP) + else if (ZOOM_OUT_KEYS.has(key)) setWebZoom(webZoom() - STEP) + else if (ZOOM_RESET_KEYS.has(key)) setWebZoom(1) + else return + event.preventDefault() + event.stopPropagation() + }, + { capture: true }, + ) +} diff --git a/packages/app-bundle/overlay/packages/app/src/utils/webview-context-menu.ts b/packages/app-bundle/overlay/packages/app/src/utils/webview-context-menu.ts new file mode 100644 index 00000000..3d383d8a --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/utils/webview-context-menu.ts @@ -0,0 +1,170 @@ +// Amicode webview: the sandboxed chat iframe gets no native context menu — +// Electron webview guests render none, and the app only mounts ContextMenu on +// a few chrome surfaces — so right-click on message text, links, and inputs +// is dead. Install a document-level menu for framed contexts that mirrors the +// browser basics over the extension-host bridges: open/copy an external link, +// copy a selection, cut/copy/paste inside editables. Component-owned menus +// (Kobalte ContextMenu triggers preventDefault their contextmenu) are left +// alone via the defaultPrevented check. Unframed, the native menu stands. + +import { readClipboardViaBridge, writeClipboardViaBridge } from "@/components/prompt-input/clipboard-bridge" +import { extractSelection, insertTextAtSelection, isEditableTarget } from "@/utils/global-clipboard" + +type Item = { label: string; run: () => void } + +const EXTERNAL_LINK = /^https?:\/\//i + +// Read-only mirror of global-clipboard's fieldSelection, for menu-state +// probing — extractSelection would mutate on cut and can't speak for inputs +// whose types reject selection access. +function editableSelectionText(el: HTMLElement): string { + if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) { + try { + const start = el.selectionStart ?? el.value.length + const end = el.selectionEnd ?? el.value.length + return el.value.slice(start, end) + } catch { + return "" + } + } + const selection = el.ownerDocument.defaultView?.getSelection() + if (!selection || selection.rangeCount === 0) return "" + const range = selection.getRangeAt(0) + return el.contains(range.commonAncestorContainer) ? selection.toString() : "" +} + +export function installWebviewContextMenu(win: Window = window): () => void { + if (win.parent === win) return () => {} + const doc = win.document + let menu: HTMLElement | null = null + + const close = () => { + menu?.remove() + menu = null + } + + const open = (x: number, y: number, items: Item[]) => { + close() + menu = doc.createElement("div") + menu.setAttribute("data-component", "webview-context-menu") + Object.assign(menu.style, { + position: "fixed", + zIndex: "9999", + minWidth: "140px", + padding: "4px", + display: "flex", + flexDirection: "column", + gap: "1px", + background: "var(--v2-background-bg-layer-01, var(--vscode-editor-background, #1e1e1e))", + border: "1px solid var(--v2-border-border-base, #3c3c3c)", + borderRadius: "8px", + boxShadow: "0 8px 24px rgba(0, 0, 0, 0.35)", + color: "var(--v2-text-text-base, inherit)", + font: "inherit", + }) + for (const item of items) { + const button = doc.createElement("button") + button.type = "button" + button.textContent = item.label + Object.assign(button.style, { + all: "unset", + display: "block", + padding: "5px 10px", + borderRadius: "5px", + fontSize: "12px", + cursor: "pointer", + whiteSpace: "nowrap", + }) + button.addEventListener( + "mouseenter", + () => (button.style.background = "var(--v2-background-bg-layer-02, rgba(127, 127, 127, 0.18))"), + ) + button.addEventListener("mouseleave", () => (button.style.background = "transparent")) + button.addEventListener("click", () => { + close() + item.run() + }) + menu.appendChild(button) + } + doc.body.appendChild(menu) + // Clamp into the viewport — fixed coordinates, so no scroll offset needed. + const rect = menu.getBoundingClientRect() + menu.style.left = `${Math.max(4, Math.min(x, win.innerWidth - rect.width - 4))}px` + menu.style.top = `${Math.max(4, Math.min(y, win.innerHeight - rect.height - 4))}px` + } + + const onContextMenu = (event: MouseEvent) => { + if (event.defaultPrevented) return // a component-owned menu speaks for this spot + const target = event.target + if (!(target instanceof Element)) return + + const items: Item[] = [] + + const href = target.closest("a[href]")?.getAttribute("href") + if (href && EXTERNAL_LINK.test(href)) { + items.push( + { + label: "Open Link", + run: () => win.parent.postMessage({ source: "amicode", kind: "open-external", url: href }, "*"), + }, + { label: "Copy Link", run: () => void writeClipboardViaBridge(href, win) }, + ) + } + + if (isEditableTarget(target)) { + if (editableSelectionText(target) !== "") { + items.push( + { + label: "Cut", + run: () => { + const text = extractSelection(target, { cut: true }) + if (text) writeClipboardViaBridge(text, win) + }, + }, + { + label: "Copy", + run: () => { + const text = extractSelection(target) + if (text) writeClipboardViaBridge(text, win) + }, + }, + ) + } + items.push({ + label: "Paste", + run: () => void readClipboardViaBridge(win).then((text) => text && insertTextAtSelection(target, text)), + }) + } else { + const selection = doc.getSelection()?.toString() ?? "" + if (selection) items.push({ label: "Copy", run: () => void writeClipboardViaBridge(selection, win) }) + } + + if (items.length === 0) return + event.preventDefault() + event.stopPropagation() + open(event.clientX, event.clientY, items) + } + + const onDismiss = (event: Event) => { + if (menu && !menu.contains(event.target as Node)) close() + } + const onKey = (event: KeyboardEvent) => { + if (event.key === "Escape") close() + } + + doc.addEventListener("contextmenu", onContextMenu) + doc.addEventListener("mousedown", onDismiss, true) + doc.addEventListener("keydown", onKey, true) + doc.addEventListener("scroll", close, true) + win.addEventListener("blur", close) + win.addEventListener("resize", close) + return () => { + close() + doc.removeEventListener("contextmenu", onContextMenu) + doc.removeEventListener("mousedown", onDismiss, true) + doc.removeEventListener("keydown", onKey, true) + doc.removeEventListener("scroll", close, true) + win.removeEventListener("blur", close) + win.removeEventListener("resize", close) + } +} diff --git a/packages/app-bundle/overlay/packages/app/src/wsl/settings-model.test.ts b/packages/app-bundle/overlay/packages/app/src/wsl/settings-model.test.ts new file mode 100644 index 00000000..71fc8c10 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/wsl/settings-model.test.ts @@ -0,0 +1,231 @@ +import { describe, expect, test } from "bun:test" +import { + addableProbePlan, + addServerProbePlan, + addServerViewModel, + autoProbePlan, + createProbeFailureGate, + runAddableProbePlan, + wslOpencodeAction, + wslRuntimeRetryable, +} from "./settings-model" +import type { WslServersState } from "./types" + +const readyWslState = readyState() + +function readyState(input: Partial = {}): WslServersState { + return { + runtime: { available: true, version: "2.4.13.0", error: null }, + installed: [], + online: [], + distroProbes: {}, + opencodeChecks: {}, + pendingRestart: false, + servers: [], + job: null, + ...input, + } +} + +describe("WSL server settings presentation", () => { + test("retries only settled unsuccessful runtimes", () => { + expect(wslRuntimeRetryable({ kind: "starting" })).toBe(false) + expect(wslRuntimeRetryable({ kind: "ready", url: "http://127.0.0.1:4096", username: null, password: null })).toBe( + false, + ) + expect(wslRuntimeRetryable({ kind: "failed", message: "boom" })).toBe(true) + expect(wslRuntimeRetryable({ kind: "stopped" })).toBe(true) + }) + + test("offers install and update only when OpenCode needs attention", () => { + expect(wslOpencodeAction(undefined)).toBeUndefined() + expect( + wslOpencodeAction({ + distro: "Debian", + resolvedPath: null, + version: null, + expectedVersion: "1.2.3", + matchesDesktop: null, + error: null, + }), + ).toBe("Install Amicode") + expect( + wslOpencodeAction({ + distro: "Debian", + resolvedPath: "/usr/local/bin/opencode", + version: "1.2.2", + expectedVersion: "1.2.3", + matchesDesktop: false, + error: null, + }), + ).toBe("Update Amicode") + expect( + wslOpencodeAction({ + distro: "Debian", + resolvedPath: "/usr/local/bin/opencode", + version: "1.2.3", + expectedVersion: "1.2.3", + matchesDesktop: true, + error: null, + }), + ).toBeUndefined() + }) + + test("plans addable distro probes with the selected distro first", () => { + const plan = addableProbePlan({ + state: readyWslState, + view: "main", + adding: false, + selectedDistro: "Ubuntu", + addableInstalledDistros: [ + { name: "Debian", version: 2, isDefault: true }, + { name: "Ubuntu", version: 2, isDefault: false }, + ], + }) + + expect(plan?.key).toBe("distro:Ubuntu|distro:Debian") + expect(plan?.distros).toEqual(["Ubuntu", "Debian"]) + }) + + test("plans bootstrap probes for missing runtime and initial distro lists", () => { + expect(autoProbePlan({ state: undefined, busy: false })).toBeUndefined() + expect(autoProbePlan({ state: { ...readyWslState, runtime: null }, busy: false })).toEqual({ + key: "runtime", + action: "probeRuntime", + }) + expect(autoProbePlan({ state: readyWslState, busy: false })).toEqual({ + key: "distros", + action: "refreshDistros", + }) + }) + + test("uses one command plan for bootstrap before addable distro probing", () => { + expect( + addServerProbePlan({ + state: { ...readyWslState, runtime: null }, + view: "main", + adding: false, + busy: false, + selectedDistro: null, + addableInstalledDistros: [{ name: "Debian", version: 2, isDefault: true }], + }), + ).toEqual({ kind: "auto", key: "auto:runtime", plan: { key: "runtime", action: "probeRuntime" } }) + + expect( + addServerProbePlan({ + state: { + ...readyWslState, + installed: [{ name: "Debian", version: 2, isDefault: true }], + online: [{ name: "Ubuntu", label: "Ubuntu" }], + }, + view: "main", + adding: false, + busy: false, + selectedDistro: "Debian", + addableInstalledDistros: [{ name: "Debian", version: 2, isDefault: true }], + }), + ).toEqual({ kind: "addable", key: "addable:distro:Debian", plan: { key: "distro:Debian", distros: ["Debian"] } }) + }) + + test("does not accept the same failed probe command until reset", () => { + const gate = createProbeFailureGate() + + expect(gate.accepts("addable:distro:Debian")).toBe(true) + gate.settle("addable:distro:Debian", new Error("wsl failed")) + expect(gate.accepts("addable:distro:Debian")).toBe(false) + expect(gate.accepts("addable:distro:Ubuntu")).toBe(true) + gate.reset() + expect(gate.accepts("addable:distro:Debian")).toBe(true) + }) + + test("keeps default distro selection stable while probes resolve", () => { + const model = addServerViewModel({ + state: { + ...readyWslState, + installed: [ + { name: "Debian", version: 2, isDefault: true }, + { name: "Ubuntu", version: 2, isDefault: false }, + ], + online: [{ name: "Alpine", label: "Alpine Linux" }], + distroProbes: { + Ubuntu: { name: "Ubuntu", canExecute: true, hasBash: true, hasCurl: true, error: null }, + }, + }, + view: "main", + selectedDistro: null, + catalogSearch: "", + catalogTarget: null, + adding: false, + probingAddable: false, + }) + + expect(model.selectedDistro).toBe("Debian") + }) + + test("keeps the dialog busy across serial addable probe job gaps", () => { + const model = addServerViewModel({ + state: readyState({ + installed: [{ name: "Debian", version: 2, isDefault: true }], + online: [{ name: "Ubuntu", label: "Ubuntu" }], + }), + view: "main", + selectedDistro: null, + catalogSearch: "", + catalogTarget: null, + adding: false, + probingAddable: true, + }) + + expect(model.busy).toBe(true) + }) + + test("does not report ready when OpenCode is present but cannot run", () => { + const model = addServerViewModel({ + state: { + ...readyWslState, + installed: [{ name: "Debian", version: 2, isDefault: true }], + online: [{ name: "Ubuntu", label: "Ubuntu" }], + distroProbes: { + Debian: { name: "Debian", canExecute: true, hasBash: true, hasCurl: true, error: null }, + }, + opencodeChecks: { + Debian: { + distro: "Debian", + resolvedPath: "/home/me/.opencode/bin/opencode", + version: null, + expectedVersion: "1.2.3", + matchesDesktop: null, + error: "opencode is installed but could not run", + }, + }, + }, + view: "main", + selectedDistro: null, + catalogSearch: "", + catalogTarget: null, + adding: false, + probingAddable: false, + }) + + expect(model.distroStatuses.Debian).toEqual({ + label: { key: "wsl.onboarding.installOpencode" }, + tone: "warning", + }) + expect(model.primaryButton.action).toBe("install-opencode") + }) + + test("delegates addable probe plans to one batch command", async () => { + const calls: string[][] = [] + + await runAddableProbePlan({ + plan: { key: "distro:Debian|distro:Ubuntu", distros: ["Debian", "Ubuntu"] }, + api: { + probeAddable: async (distros) => { + calls.push(distros) + }, + }, + }) + + expect(calls).toEqual([["Debian", "Ubuntu"]]) + }) +}) diff --git a/packages/app-bundle/overlay/packages/app/src/wsl/settings-model.ts b/packages/app-bundle/overlay/packages/app/src/wsl/settings-model.ts new file mode 100644 index 00000000..4daa1e19 --- /dev/null +++ b/packages/app-bundle/overlay/packages/app/src/wsl/settings-model.ts @@ -0,0 +1,341 @@ +import fuzzysort from "fuzzysort" +import type { + WslInstalledDistro, + WslOnlineDistro, + WslOpencodeCheck, + WslServersPlatform, + WslServerRuntime, + WslServersState, +} from "./types" + +export type AddServerText = { + key: string + params?: Record +} + +export type DistroStatusTone = "success" | "warning" | "muted" + +export type DistroStatus = { + label: AddServerText + tone: DistroStatusTone +} + +export type AddServerPrimaryButton = { + variant: "neutral" | "contrast" + label: AddServerText + disabled: boolean + action: "install-opencode" | "add" | null + loading: boolean + width: string | null +} + +export type AddServerRuntimeState = "loading" | "pendingRestart" | "checking" | "unavailable" | "ready" + +export type AddableProbePlan = { + key: string + distros: string[] +} + +export type AutoProbePlan = { key: "runtime"; action: "probeRuntime" } | { key: "distros"; action: "refreshDistros" } + +export type AddServerProbePlan = + | { kind: "auto"; key: string; plan: AutoProbePlan } + | { kind: "addable"; key: string; plan: AddableProbePlan } + +export type WslAddServerView = "main" | "catalog" + +function isHiddenDistro(name: string) { + return /^docker-desktop(?:-data)?$/i.test(name) +} + +export const wslRuntimeRetryable = (runtime: WslServerRuntime) => + runtime.kind === "failed" || runtime.kind === "stopped" + +export function wslOpencodeAction(check?: WslOpencodeCheck) { + if (!check) return + if (!check.resolvedPath) return "Install Amicode" + if (check.matchesDesktop === false) return "Update Amicode" +} + +export function wslDistroReady(state: WslServersState | undefined, name: string) { + const installed = state?.installed.find((item) => item.name === name) + const probe = state?.distroProbes[name] + if (!probe || !installed) return false + if (installed.version === 1) return false + return probe.canExecute && probe.hasBash && probe.hasCurl +} + +export function addServerViewModel(input: { + state: WslServersState | undefined + view: WslAddServerView + selectedDistro: string | null + catalogSearch: string + catalogTarget: string | null + adding: boolean + probingAddable: boolean +}) { + const state = input.state + const visibleInstalledDistros = (state?.installed ?? []).filter((item) => !isHiddenDistro(item.name)) + const visibleOnlineDistros = (state?.online ?? []).filter((item) => !isHiddenDistro(item.name)) + const existingServerDistros = new Set((state?.servers ?? []).map((item) => item.config.distro)) + const addableInstalledDistros = visibleInstalledDistros.filter((item) => !existingServerDistros.has(item.name)) + const selectedDistro = addServerSelectedDistro(input.selectedDistro, visibleInstalledDistros, addableInstalledDistros) + const opencodeCheck = selectedDistro ? (state?.opencodeChecks[selectedDistro] ?? null) : null + const installableDistros = addServerInstallableDistros(visibleInstalledDistros, visibleOnlineDistros) + const filteredInstallableDistros = addServerFilteredInstallableDistros(installableDistros, input.catalogSearch) + const catalogTarget = addServerCatalogTarget(input.catalogTarget, filteredInstallableDistros) + const busy = !!state?.job || input.adding || input.probingAddable + + return { + busy, + runtimeState: addServerRuntimeState(state), + visibleInstalledDistros, + visibleOnlineDistros, + addableInstalledDistros, + selectedDistro, + opencodeCheck, + wslReady: !!state?.runtime?.available && !state?.pendingRestart, + distroStatuses: Object.fromEntries( + addableInstalledDistros.flatMap((item) => { + const status = addServerDistroStatus({ state, name: item.name, probingAddable: input.probingAddable }) + if (!status) return [] + return [[item.name, status]] + }), + ) as Record, + primaryButton: addServerPrimaryButton({ + state, + selectedDistro, + opencodeCheck, + adding: input.adding, + probingAddable: input.probingAddable, + }), + installableDistros, + filteredInstallableDistros, + catalogTarget, + installingCatalogDistro: state?.job?.kind === "install-distro", + } +} + +function addServerSelectedDistro( + selected: string | null, + visibleInstalledDistros: WslInstalledDistro[], + addableInstalledDistros: WslInstalledDistro[], +) { + if (selected && addableInstalledDistros.some((item) => item.name === selected && item.version !== 1)) return selected + const defaultDistro = visibleInstalledDistros.find((item) => item.isDefault) + if ( + defaultDistro && + defaultDistro.version !== 1 && + addableInstalledDistros.some((item) => item.name === defaultDistro.name) + ) { + return defaultDistro.name + } + return addableInstalledDistros.find((item) => item.version !== 1)?.name ?? null +} + +function addServerRuntimeState(state: WslServersState | undefined): AddServerRuntimeState { + if (!state) return "loading" + if (state.pendingRestart) return "pendingRestart" + if (!state.runtime) return "checking" + if (!state.runtime.available) return "unavailable" + return "ready" +} + +function addServerDistroStatus(input: { + state: WslServersState | undefined + name: string + probingAddable: boolean +}): DistroStatus | undefined { + const installed = input.state?.installed.find((item) => item.name === input.name) + if (installed?.version === 1) return { label: { key: "wsl.onboarding.distroStatus.unsupported" }, tone: "muted" } + const job = input.state?.job + const probe = input.state?.distroProbes[input.name] + if (!probe) { + if (input.probingAddable || (job?.kind === "probe-addable" && job.distros.includes(input.name))) { + return checkingStatus() + } + return + } + if (!probe.canExecute) { + if (!installed) { + return { label: { key: "wsl.onboarding.distroNotInstalled", params: { distro: input.name } }, tone: "warning" } + } + return { label: { key: "wsl.onboarding.openDistroOnce", params: { distro: input.name } }, tone: "warning" } + } + if (!probe.hasBash || !probe.hasCurl) { + return { label: { key: "wsl.onboarding.distroStatus.missingTools" }, tone: "warning" } + } + const check = input.state?.opencodeChecks[input.name] + if (!check) { + if (input.probingAddable || (job?.kind === "probe-addable" && job.distros.includes(input.name))) { + return checkingStatus() + } + return + } + if (check.matchesDesktop === false) return { label: { key: "wsl.onboarding.updateOpencode" }, tone: "warning" } + if (!check.resolvedPath) return { label: { key: "wsl.onboarding.distroStatus.opencodeMissing" }, tone: "warning" } + if (check.error) return { label: { key: "wsl.onboarding.installOpencode" }, tone: "warning" } + return { label: { key: "wsl.onboarding.distroStatus.ready" }, tone: "success" } +} + +function checkingStatus(): DistroStatus { + return { label: { key: "wsl.onboarding.distroStatus.checking" }, tone: "muted" } +} + +function addServerPrimaryButton(input: { + state: WslServersState | undefined + selectedDistro: string | null + opencodeCheck: WslOpencodeCheck | null + adding: boolean + probingAddable: boolean +}): AddServerPrimaryButton { + const ready = !!input.selectedDistro && wslDistroReady(input.state, input.selectedDistro) + const probingSelected = input.probingAddable && !addServerSelectedDistroSettled(input.state, input.selectedDistro) + const probingOpencode = + probingSelected || + (ready && + (!input.opencodeCheck || + (!!input.selectedDistro && + input.state?.job?.kind === "probe-addable" && + input.state.job.distros.includes(input.selectedDistro)))) + const installingOpencode = + input.state?.job?.kind === "install-opencode" && input.state.job.distro === input.selectedDistro + if (!ready || probingOpencode) { + return { + variant: "contrast", + label: probingSelected ? { key: "wsl.onboarding.distroStatus.checking" } : { key: "wsl.server.add" }, + disabled: true, + action: null, + loading: probingSelected, + width: null, + } + } + if (!addServerOpencodeReady(input.opencodeCheck)) { + const update = !!input.opencodeCheck?.resolvedPath && input.opencodeCheck.matchesDesktop === false + return { + variant: "neutral", + label: installingOpencode + ? { key: "wsl.onboarding.updatingOpencode" } + : update + ? { key: "wsl.onboarding.updateOpencode" } + : { key: "wsl.onboarding.installOpencode" }, + disabled: !!input.state?.job || input.adding, + action: "install-opencode", + loading: installingOpencode, + width: update ? "138px" : "129px", + } + } + return { + variant: "contrast", + label: input.adding ? { key: "wsl.onboarding.adding" } : { key: "wsl.server.add" }, + disabled: input.adding || !!input.state?.job, + action: "add", + loading: input.adding, + width: null, + } +} + +function addServerOpencodeReady(check: WslOpencodeCheck | null) { + return !!check?.resolvedPath && check.matchesDesktop !== false && !check.error +} + +function addServerSelectedDistroSettled(state: WslServersState | undefined, selectedDistro: string | null) { + if (!selectedDistro) return false + const installed = state?.installed.find((item) => item.name === selectedDistro) + if (installed?.version === 1) return false + if (!state?.distroProbes[selectedDistro]) return false + if (!wslDistroReady(state, selectedDistro)) return true + return !!state.opencodeChecks[selectedDistro] +} + +function addServerInstallableDistros(installedDistros: WslInstalledDistro[], onlineDistros: WslOnlineDistro[]) { + const installed = new Set(installedDistros.map((item) => item.name)) + const hasVersionedUbuntu = onlineDistros.some((item) => /^Ubuntu-\d/.test(item.name)) + return onlineDistros + .filter((item) => !installed.has(item.name)) + .filter((item) => item.name !== "Ubuntu" || !hasVersionedUbuntu) +} + +function addServerFilteredInstallableDistros(installableDistros: WslOnlineDistro[], search: string) { + const query = search.trim() + if (!query) return installableDistros + return fuzzysort.go(query, installableDistros, { keys: ["label", "name"] }).map((item) => item.obj) +} + +function addServerCatalogTarget(target: string | null, distros: WslOnlineDistro[]) { + if (target && distros.some((item) => item.name === target)) return target + return distros[0]?.name ?? null +} + +export function addableProbePlan(input: { + state: WslServersState | undefined + view: WslAddServerView + adding: boolean + selectedDistro: string | null + addableInstalledDistros: WslInstalledDistro[] +}) { + const state = input.state + if (!state?.runtime?.available || state.pendingRestart || input.view !== "main" || input.adding) return + if (state.job) return + const ordered = input.selectedDistro + ? [ + ...input.addableInstalledDistros.filter((item) => item.name === input.selectedDistro), + ...input.addableInstalledDistros.filter((item) => item.name !== input.selectedDistro), + ] + : input.addableInstalledDistros + const pending = ordered.flatMap((item) => { + if (item.version === 1) return [] + if (!state.distroProbes[item.name]) return [`distro:${item.name}`] + if (wslDistroReady(state, item.name) && !state.opencodeChecks[item.name]) return [`opencode:${item.name}`] + return [] + }) + if (!pending.length) return + return { + key: pending.join("|"), + distros: ordered.filter((item) => item.version !== 1).map((item) => item.name), + } satisfies AddableProbePlan +} + +export function autoProbePlan(input: { state: WslServersState | undefined; busy: boolean }) { + if (!input.state || input.busy || input.state.pendingRestart) return + if (!input.state.runtime) return { key: "runtime", action: "probeRuntime" } satisfies AutoProbePlan + if (!input.state.runtime.available) return + if (input.state.installed.length || input.state.online.length) return + return { key: "distros", action: "refreshDistros" } satisfies AutoProbePlan +} + +export function addServerProbePlan(input: { + state: WslServersState | undefined + view: WslAddServerView + adding: boolean + busy: boolean + selectedDistro: string | null + addableInstalledDistros: WslInstalledDistro[] +}) { + const auto = autoProbePlan({ state: input.state, busy: input.busy }) + if (auto) return { kind: "auto", key: `auto:${auto.key}`, plan: auto } satisfies AddServerProbePlan + const addable = addableProbePlan(input) + if (addable) return { kind: "addable", key: `addable:${addable.key}`, plan: addable } satisfies AddServerProbePlan +} + +export function createProbeFailureGate() { + let failed: string | undefined + return { + accepts(key: string) { + return key !== failed + }, + settle(key: string, error?: unknown) { + if (error) failed = key + }, + reset() { + failed = undefined + }, + } +} + +export async function runAddableProbePlan(input: { + plan: AddableProbePlan + api: Pick +}) { + await input.api.probeAddable(input.plan.distros) +} diff --git a/packages/app-bundle/overlay/packages/core/schema.json b/packages/app-bundle/overlay/packages/core/schema.json new file mode 100644 index 00000000..c19664b3 --- /dev/null +++ b/packages/app-bundle/overlay/packages/core/schema.json @@ -0,0 +1,2290 @@ +{ + "version": "7", + "dialect": "sqlite", + "id": "169b3aad-5dd0-4a14-a772-423b22ab2217", + "prevIds": [ + "f14a9b18-8207-487e-a3d3-227e629ba9ad" + ], + "ddl": [ + { + "name": "workspace", + "entityType": "tables" + }, + { + "name": "data_migration", + "entityType": "tables" + }, + { + "name": "account_state", + "entityType": "tables" + }, + { + "name": "account", + "entityType": "tables" + }, + { + "name": "control_account", + "entityType": "tables" + }, + { + "name": "credential", + "entityType": "tables" + }, + { + "name": "event_sequence", + "entityType": "tables" + }, + { + "name": "event", + "entityType": "tables" + }, + { + "name": "permission", + "entityType": "tables" + }, + { + "name": "provider_permission", + "entityType": "tables" + }, + { + "name": "project_directory", + "entityType": "tables" + }, + { + "name": "project", + "entityType": "tables" + }, + { + "name": "message", + "entityType": "tables" + }, + { + "name": "part", + "entityType": "tables" + }, + { + "name": "session_context_epoch", + "entityType": "tables" + }, + { + "name": "session_input", + "entityType": "tables" + }, + { + "name": "session_message", + "entityType": "tables" + }, + { + "name": "session", + "entityType": "tables" + }, + { + "name": "todo", + "entityType": "tables" + }, + { + "name": "session_share", + "entityType": "tables" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "''", + "generated": null, + "name": "name", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "branch", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "extra", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_used", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "data_migration" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_completed", + "entityType": "columns", + "table": "data_migration" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active_account_id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active_org_id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token_expiry", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token_expiry", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "credential" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "integration_id", + "entityType": "columns", + "table": "credential" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "label", + "entityType": "columns", + "table": "credential" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "value", + "entityType": "columns", + "table": "credential" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "connector_id", + "entityType": "columns", + "table": "credential" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "method_id", + "entityType": "columns", + "table": "credential" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active", + "entityType": "columns", + "table": "credential" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "credential" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "credential" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "owner_id", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "action", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "resource", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "provider_permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "provider_permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "tier_id", + "entityType": "columns", + "table": "provider_permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "action", + "entityType": "columns", + "table": "provider_permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "resource", + "entityType": "columns", + "table": "provider_permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "provider_permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "provider_permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "project_directory" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "project_directory" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "project_directory" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "strategy", + "entityType": "columns", + "table": "project_directory" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "project_directory" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "worktree", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "vcs", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url_override", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_color", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_initialized", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "sandboxes", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "commands", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "message_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_context_epoch" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "baseline", + "entityType": "columns", + "table": "session_context_epoch" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "snapshot", + "entityType": "columns", + "table": "session_context_epoch" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "baseline_seq", + "entityType": "columns", + "table": "session_context_epoch" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "prompt", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "delivery", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "admitted_seq", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "promoted_seq", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "parent_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "slug", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "path", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "title", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "version", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "share_url", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_additions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_deletions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_files", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_diffs", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "metadata", + "entityType": "columns", + "table": "session" + }, + { + "type": "real", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "cost", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_input", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_output", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_reasoning", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_cache_read", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_cache_write", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "revert", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "permission", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "agent", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_compacting", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_archived", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "content", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "status", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "priority", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "position", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "secret", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_share" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_workspace_project_id_project_id_fk", + "entityType": "fks", + "table": "workspace" + }, + { + "columns": [ + "active_account_id" + ], + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "nameExplicit": false, + "name": "fk_account_state_active_account_id_account_id_fk", + "entityType": "fks", + "table": "account_state" + }, + { + "columns": [ + "aggregate_id" + ], + "tableTo": "event_sequence", + "columnsTo": [ + "aggregate_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", + "entityType": "fks", + "table": "event" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_permission_project_id_project_id_fk", + "entityType": "fks", + "table": "permission" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_provider_permission_project_id_project_id_fk", + "entityType": "fks", + "table": "provider_permission" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_project_directory_project_id_project_id_fk", + "entityType": "fks", + "table": "project_directory" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_message_session_id_session_id_fk", + "entityType": "fks", + "table": "message" + }, + { + "columns": [ + "message_id" + ], + "tableTo": "message", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_part_message_id_message_id_fk", + "entityType": "fks", + "table": "part" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_context_epoch_session_id_session_id_fk", + "entityType": "fks", + "table": "session_context_epoch" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_input_session_id_session_id_fk", + "entityType": "fks", + "table": "session_input" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_message_session_id_session_id_fk", + "entityType": "fks", + "table": "session_message" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_project_id_project_id_fk", + "entityType": "fks", + "table": "session" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_todo_session_id_session_id_fk", + "entityType": "fks", + "table": "todo" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_share_session_id_session_id_fk", + "entityType": "fks", + "table": "session_share" + }, + { + "columns": [ + "email", + "url" + ], + "nameExplicit": false, + "name": "control_account_pk", + "entityType": "pks", + "table": "control_account" + }, + { + "columns": [ + "project_id", + "directory" + ], + "nameExplicit": false, + "name": "project_directory_pk", + "entityType": "pks", + "table": "project_directory" + }, + { + "columns": [ + "session_id", + "position" + ], + "nameExplicit": false, + "name": "todo_pk", + "entityType": "pks", + "table": "todo" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "workspace_pk", + "table": "workspace", + "entityType": "pks" + }, + { + "columns": [ + "name" + ], + "nameExplicit": false, + "name": "data_migration_pk", + "table": "data_migration", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "account_state_pk", + "table": "account_state", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "account_pk", + "table": "account", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "credential_pk", + "table": "credential", + "entityType": "pks" + }, + { + "columns": [ + "aggregate_id" + ], + "nameExplicit": false, + "name": "event_sequence_pk", + "table": "event_sequence", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "event_pk", + "table": "event", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "permission_pk", + "table": "permission", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "provider_permission_pk", + "table": "provider_permission", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "project_pk", + "table": "project", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "message_pk", + "table": "message", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "part_pk", + "table": "part", + "entityType": "pks" + }, + { + "columns": [ + "session_id" + ], + "nameExplicit": false, + "name": "session_context_epoch_pk", + "table": "session_context_epoch", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_input_pk", + "table": "session_input", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_message_pk", + "table": "session_message", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_pk", + "table": "session", + "entityType": "pks" + }, + { + "columns": [ + "session_id" + ], + "nameExplicit": false, + "name": "session_share_pk", + "table": "session_share", + "entityType": "pks" + }, + { + "columns": [ + { + "value": "aggregate_id", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "event_aggregate_seq_idx", + "entityType": "indexes", + "table": "event" + }, + { + "columns": [ + { + "value": "aggregate_id", + "isExpression": false + }, + { + "value": "type", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "event_aggregate_type_seq_idx", + "entityType": "indexes", + "table": "event" + }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + }, + { + "value": "action", + "isExpression": false + }, + { + "value": "resource", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "permission_project_action_resource_idx", + "entityType": "indexes", + "table": "permission" + }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + }, + { + "value": "tier_id", + "isExpression": false + }, + { + "value": "action", + "isExpression": false + }, + { + "value": "resource", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "provider_permission_project_tier_action_resource_idx", + "entityType": "indexes", + "table": "provider_permission" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "message_session_time_created_id_idx", + "entityType": "indexes", + "table": "message" + }, + { + "columns": [ + { + "value": "message_id", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_message_id_id_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_session_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "promoted_seq", + "isExpression": false + }, + { + "value": "delivery", + "isExpression": false + }, + { + "value": "admitted_seq", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_input_session_pending_delivery_seq_idx", + "entityType": "indexes", + "table": "session_input" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "admitted_seq", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "session_input_session_admitted_seq_idx", + "entityType": "indexes", + "table": "session_input" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "promoted_seq", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "session_input_session_promoted_seq_idx", + "entityType": "indexes", + "table": "session_input" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "session_message_session_seq_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "type", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_session_type_seq_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_session_time_created_id_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "time_created", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_time_created_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_project_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_workspace_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "parent_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_parent_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "todo_session_idx", + "entityType": "indexes", + "table": "todo" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/packages/app-bundle/overlay/packages/core/src/config.ts b/packages/app-bundle/overlay/packages/core/src/config.ts new file mode 100644 index 00000000..c66425ec --- /dev/null +++ b/packages/app-bundle/overlay/packages/core/src/config.ts @@ -0,0 +1,231 @@ +export * as Config from "./config" + +import { makeLocationNode } from "./effect/app-node" +import path from "path" +import { type ParseError, parse } from "jsonc-parser" +import { Context, Effect, Layer, Option, Schema } from "effect" +import { Permission } from "@opencode-ai/schema/permission" +import { ProviderPermission } from "@opencode-ai/schema/provider-permission" +import { FSUtil } from "./fs-util" +import { Global } from "./global" +import { Location } from "./location" +import { Policy } from "./policy" +import { AbsolutePath } from "./schema" +import { ConfigAgent } from "./config/agent" +import { ConfigAttachments } from "./config/attachments" +import { ConfigCompaction } from "./config/compaction" +import { ConfigCommand } from "./config/command" +import { ConfigExperimental } from "./config/experimental" +import { ConfigFormatter } from "./config/formatter" +import { ConfigLSP } from "./config/lsp" +import { ConfigMCP } from "./config/mcp" +import { ConfigPlugin } from "./config/plugin" +import { ConfigProvider } from "./config/provider" +import { ConfigReference } from "./config/reference" +import { ConfigToolOutput } from "./config/tool-output" +import { ConfigWatcher } from "./config/watcher" +import { ConfigV1 } from "./v1/config/config" +import { ConfigMigrateV1 } from "./v1/config/migrate" + +export class Info extends Schema.Class("Config.Info")({ + $schema: Schema.optional(Schema.String).annotate({ + description: "JSON schema reference for configuration validation", + }), + shell: Schema.String.pipe(Schema.optional).annotate({ + description: "Default shell to use for terminal and shell tool execution", + }), + model: Schema.String.pipe(Schema.optional).annotate({ + description: "Default model to use when no session or agent model is selected", + }), + default_agent: Schema.String.pipe(Schema.optional).annotate({ + description: "Default primary agent to use when no session agent is selected", + }), + autoupdate: Schema.Union([Schema.Boolean, Schema.Literal("notify")]) + .pipe(Schema.optional) + .annotate({ + description: "Automatically update or notify when a new version is available", + }), + share: Schema.Literals(["manual", "auto", "disabled"]).pipe(Schema.optional).annotate({ + description: "Control whether sessions may be shared manually, automatically, or not at all", + }), + enterprise: Schema.Struct({ + url: Schema.String.pipe(Schema.optional), + }) + .pipe(Schema.optional) + .annotate({ + description: "Enterprise sharing service configuration", + }), + username: Schema.String.pipe(Schema.optional).annotate({ + description: "Username displayed in conversations and used for telemetry identity", + }), + permissions: Permission.Ruleset.pipe(Schema.optional).annotate({ + description: "Ordered tool permission rules applied to agent tool use", + }), + agents: Schema.Record(Schema.String, ConfigAgent.Info).pipe(Schema.optional).annotate({ + description: "Named built-in agent overrides and custom agent definitions", + }), + snapshots: Schema.Boolean.pipe(Schema.optional).annotate({ + description: "Enable snapshots used for undo and revert behavior", + }), + watcher: ConfigWatcher.Info.pipe(Schema.optional).annotate({ + description: "Filesystem watcher configuration", + }), + formatter: ConfigFormatter.Info.pipe(Schema.optional).annotate({ + description: "Enable built-in formatters or configure formatter overrides", + }), + lsp: ConfigLSP.Info.pipe(Schema.optional).annotate({ + description: "Enable built-in language servers or configure server overrides", + }), + attachments: ConfigAttachments.Info.pipe(Schema.optional).annotate({ + description: "Attachment processing configuration", + }), + tool_output: ConfigToolOutput.Info.pipe(Schema.optional).annotate({ + description: "Tool output truncation thresholds", + }), + mcp: ConfigMCP.Info.pipe(Schema.optional).annotate({ + description: "MCP server configuration", + }), + compaction: ConfigCompaction.Info.pipe(Schema.optional).annotate({ + description: "Conversation compaction behavior", + }), + skills: Schema.String.pipe(Schema.Array, Schema.optional).annotate({ + description: "Additional paths or URLs to discover skills from", + }), + commands: Schema.Record(Schema.String, ConfigCommand.Info).pipe(Schema.optional).annotate({ + description: "Named slash command definitions", + }), + instructions: Schema.String.pipe(Schema.Array, Schema.optional).annotate({ + description: "Additional paths or URLs supplying ambient instructions", + }), + references: ConfigReference.Info.pipe(Schema.optional).annotate({ + description: "Named local directories or Git repositories available as external context", + }), + plugins: ConfigPlugin.Plugins.pipe(Schema.optional).annotate({ + description: "Ordered external plugin packages to load", + }), + experimental: ConfigExperimental.Experimental.pipe(Schema.optional), + providers: Schema.Record(Schema.String, ConfigProvider.Info).pipe(Schema.optional), + providerPermissions: ProviderPermission.Config.pipe(Schema.optional).annotate({ + description: "Provider permission trust tiers — directory × action matrices per tier", + }), +}) {} + +export class Document extends Schema.Class("Config.Document")({ + type: Schema.Literal("document"), + path: Schema.String.pipe(Schema.optional), + info: Info, +}) {} + +export class Directory extends Schema.Class("Config.Directory")({ + type: Schema.Literal("directory"), + path: AbsolutePath, +}) {} + +export type Entry = Document | Directory + +export function latest(entries: readonly Entry[], key: K): Info[K] | undefined { + return entries + .filter((entry): entry is Document => entry.type === "document") + .findLast((entry) => entry.info[key] !== undefined)?.info[key] +} + +export interface Interface { + /** Returns location config documents and supplemental directories from lowest to highest priority. */ + readonly entries: () => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/Config") {} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* FSUtil.Service + const global = yield* Global.Service + const location = yield* Location.Service + const policy = yield* Policy.Service + const names = ["opencode.json", "opencode.jsonc"] + const decodeOptions = { errors: "all", onExcessProperty: "ignore", propertyOrder: "original" } as const + const decodeInfo = Schema.decodeUnknownOption(Info, decodeOptions) + const decodeV1Info = Schema.decodeUnknownOption(ConfigV1.Info, decodeOptions) + + const loadFile = Effect.fnUntraced(function* (filepath: string) { + const text = yield* fs.readFileStringSafe(filepath) + if (!text) return + + const errors: ParseError[] = [] + const input: unknown = parse(text, errors, { allowTrailingComma: true }) + if (errors.length) return + + const info = Option.getOrUndefined( + ConfigMigrateV1.isV1(input) + ? decodeV1Info(input).pipe(Option.map(ConfigMigrateV1.migrate), Option.flatMap(decodeInfo)) + : decodeInfo(input), + ) + if (!info) return + return new Document({ type: "document", path: filepath, info }) + }) + + const loadDirectory = Effect.fnUntraced(function* (directory: AbsolutePath) { + return [ + ...(yield* Effect.forEach(names, (file) => loadFile(path.join(directory, file))).pipe( + Effect.map((configs) => configs.filter((config): config is Document => config !== undefined)), + )), + new Directory({ type: "directory", path: directory }), + ] + }) + + const globalDirectory = AbsolutePath.make(global.config) + const locationIsGlobal = path.resolve(location.directory) === path.resolve(global.config) + // Read configuration once when this location opens. Later calls reuse these + // values until the location is reopened. + const discovered = locationIsGlobal + ? [] + : yield* fs + .up({ + targets: [".opencode", ...names.toReversed()], + start: location.directory, + stop: location.project.directory, + }) + .pipe(Effect.orDie) + const directories = [ + globalDirectory, + ...discovered + .filter((item) => path.basename(item) === ".opencode") + .toReversed() + .map((directory) => AbsolutePath.make(directory)), + ] + // A config closer to the opened directory should win over one higher up. + // Search starts nearby, so reverse the results before applying them. + const directPaths = discovered.filter((item) => path.basename(item) !== ".opencode").toReversed() + const direct = yield* Effect.forEach(directPaths, loadFile).pipe( + Effect.orDie, + Effect.map((configs) => configs.filter((config): config is Document => config !== undefined)), + ) + const supplementary = yield* Effect.forEach(directories, loadDirectory).pipe(Effect.orDie) + // Apply general settings first and more specific settings last: + // global config, project files, then `.opencode` files. + const configs = [...(supplementary[0] ?? []), ...direct, ...supplementary.slice(1).flat()] + // Rules use the opposite order so a user-global rule can override a + // repository rule. Statement order inside each file stays unchanged. + yield* policy.load( + configs + .filter((config): config is Document => config.type === "document") + .toReversed() + .flatMap((config) => config.info.experimental?.policies ?? []), + ) + + return Service.of({ + entries: Effect.fn("Config.entries")(function* () { + return configs + }), + }) + }), +) + +export const locationLayer = layer.pipe(Layer.provideMerge(Policy.locationLayer)) + +export const node = makeLocationNode({ + service: Service, + layer, + deps: [FSUtil.node, Global.node, Location.node, Policy.node], +}) diff --git a/packages/app-bundle/overlay/packages/core/src/database/database.ts b/packages/app-bundle/overlay/packages/core/src/database/database.ts new file mode 100644 index 00000000..62455690 --- /dev/null +++ b/packages/app-bundle/overlay/packages/core/src/database/database.ts @@ -0,0 +1,234 @@ +export * as Database from "./database" + +import { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite" +import { layer as sqliteLayer } from "#sqlite" +import { Context, Effect, Layer } from "effect" +import { Global } from "../global" +import { Flag } from "../flag/flag" +import { isAbsolute, join } from "path" +import { existsSync, readdirSync, renameSync, statSync, unlinkSync } from "fs" +import { DatabaseMigration } from "./migration" +import { makeGlobalNode } from "../effect/app-node" +import { Database as BunDatabase } from "bun:sqlite" + +const makeDatabase = EffectDrizzleSqlite.makeWithDefaults() +type DatabaseShape = Effect.Success + +export interface Interface { + db: DatabaseShape +} + +export class Service extends Context.Service()("@opencode/v2/storage/Database") {} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const db = yield* makeDatabase + + yield* db.run("PRAGMA journal_mode = WAL") + yield* db.run("PRAGMA synchronous = NORMAL") + yield* db.run("PRAGMA busy_timeout = 5000") + yield* db.run("PRAGMA cache_size = -64000") + yield* db.run("PRAGMA foreign_keys = ON") + yield* db.run("PRAGMA wal_checkpoint(PASSIVE)") + yield* DatabaseMigration.apply(db) + + return { db } + }).pipe(Effect.orDie), +) + +export function layerFromPath(filename: string) { + return layer.pipe(Layer.provide(sqliteLayer({ filename }))) +} + +/** + * All tables to merge via INSERT OR IGNORE. Order matters: parents before + * children to satisfy foreign key relationships on re-enable. + */ +const MERGE_TABLES = [ + "migration", + "data_migration", + "project", + "project_directory", + "workspace", + "account", + "account_state", + "control_account", + "credential", + "event_sequence", + "event", + "permission", + "session", + "session_message", + "session_input", + "session_context_epoch", + "session_share", + "message", + "part", + "todo", +] + +/** + * One-time consolidation of legacy channel-named DBs into the unified + * `opencode.db`. Runs synchronously at startup before the Effect DB layer + * initializes. + * + * Strategy: + * 1. Find all `opencode-*.db` files (skip already-merged `*.db.merged`). + * 2. If `opencode.db` doesn't exist, rename the largest source to become it + * (avoids a full copy of the biggest file). + * 3. For each remaining source: checkpoint WAL, ATTACH to target, INSERT OR + * IGNORE per table, DETACH, rename source to `*.db.merged`. + * 4. Clean up WAL/SHM sidecars of merged sources. + * + * Idempotent: partially-merged sources remain un-renamed and will be retried + * on next startup. Already `.db.merged` files are ignored. + */ +function consolidateChannelDbs(targetPath: string) { + const dir = Global.Path.data + + // Collect candidate channel DBs + let candidates: { path: string; name: string; size: number }[] + try { + candidates = readdirSync(dir) + .filter((f) => f.startsWith("opencode-") && f.endsWith(".db") && !f.endsWith(".db.merged")) + .map((f) => { + const full = join(dir, f) + try { + const st = statSync(full) + return { path: full, name: f, size: st.size } + } catch { + return null + } + }) + .filter((x): x is { path: string; name: string; size: number } => x !== null) + } catch { + return + } + + if (candidates.length === 0) return + + // Sort largest first — the biggest becomes the base if target doesn't exist + candidates.sort((a, b) => b.size - a.size) + + // If target doesn't exist, promote the largest candidate by rename + if (!existsSync(targetPath)) { + const largest = candidates.shift()! + try { + renameSync(largest.path, targetPath) + // Move sidecars too + for (const ext of ["-wal", "-shm"]) { + const sidecar = largest.path + ext + if (existsSync(sidecar)) renameSync(sidecar, targetPath + ext) + } + } catch { + // If rename fails, put it back in the list to be merged normally + candidates.unshift(largest) + } + } + + if (candidates.length === 0) return + + // Open the target DB for merging + let db: InstanceType + try { + db = new BunDatabase(targetPath, { create: true, readwrite: true }) + } catch { + return + } + + try { + // Disable foreign keys for the duration of the merge + db.run("PRAGMA foreign_keys = OFF") + db.run("PRAGMA journal_mode = WAL") + + for (const candidate of candidates) { + try { + // Checkpoint the source WAL so ATTACH sees a clean state + let sourceDb: InstanceType | undefined + try { + sourceDb = new BunDatabase(candidate.path, { readwrite: true }) + sourceDb.run("PRAGMA wal_checkpoint(TRUNCATE)") + sourceDb.close() + sourceDb = undefined + } catch { + sourceDb?.close() + // If we can't checkpoint, try the merge anyway — ATTACH will read + // whatever is committed in the main file. + } + + // Attach source and merge each table + const alias = "source_db" + db.run(`ATTACH DATABASE '${candidate.path.replace(/'/g, "''")}' AS ${alias}`) + + try { + db.run("BEGIN") + + // Get table list from the source to handle schema differences gracefully + const sourceTables = new Set( + ( + db.query(`SELECT name FROM ${alias}.sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'`).all() as { + name: string + }[] + ).map((r) => r.name), + ) + + for (const table of MERGE_TABLES) { + if (!sourceTables.has(table)) continue + db.run(`INSERT OR IGNORE INTO main."${table}" SELECT * FROM ${alias}."${table}"`) + } + + db.run("COMMIT") + } catch { + try { + db.run("ROLLBACK") + } catch { + // Rollback may fail if transaction wasn't started + } + // Leave this source un-renamed for retry on next startup + try { + db.run(`DETACH DATABASE ${alias}`) + } catch { + // If detach fails too, bail on this source + } + continue + } + + db.run(`DETACH DATABASE ${alias}`) + + // Successfully merged — rename source to *.db.merged + try { + renameSync(candidate.path, candidate.path + ".merged") + // Clean up WAL/SHM sidecars + for (const ext of ["-wal", "-shm"]) { + const sidecar = candidate.path + ext + if (existsSync(sidecar)) unlinkSync(sidecar) + } + } catch { + // Non-fatal: the source stays as-is and will be skipped next time + // (it's already fully merged, so INSERT OR IGNORE is a no-op) + } + } catch { + // Non-fatal per source: skip this one and try the rest + continue + } + } + + // Re-enable foreign keys + db.run("PRAGMA foreign_keys = ON") + } finally { + db.close() + } +} + +export function path() { + if (Flag.OPENCODE_DB) { + if (Flag.OPENCODE_DB === ":memory:" || isAbsolute(Flag.OPENCODE_DB)) return Flag.OPENCODE_DB + return join(Global.Path.data, Flag.OPENCODE_DB) + } + const target = join(Global.Path.data, "opencode.db") + consolidateChannelDbs(target) + return target +} + +export const node = makeGlobalNode({ service: Service, layer: layerFromPath(path()), deps: [] }) diff --git a/packages/app-bundle/overlay/packages/core/src/database/migration.gen.ts b/packages/app-bundle/overlay/packages/core/src/database/migration.gen.ts new file mode 100644 index 00000000..a73472eb --- /dev/null +++ b/packages/app-bundle/overlay/packages/core/src/database/migration.gen.ts @@ -0,0 +1,45 @@ +import type { DatabaseMigration } from "./migration" + +export const migrations = ( + await Promise.all([ + import("./migration/20260127222353_familiar_lady_ursula"), + import("./migration/20260211171708_add_project_commands"), + import("./migration/20260213144116_wakeful_the_professor"), + import("./migration/20260225215848_workspace"), + import("./migration/20260227213759_add_session_workspace_id"), + import("./migration/20260228203230_blue_harpoon"), + import("./migration/20260303231226_add_workspace_fields"), + import("./migration/20260309230000_move_org_to_state"), + import("./migration/20260312043431_session_message_cursor"), + import("./migration/20260323234822_events"), + import("./migration/20260410174513_workspace-name"), + import("./migration/20260413175956_chief_energizer"), + import("./migration/20260423070820_add_icon_url_override"), + import("./migration/20260427172553_slow_nightmare"), + import("./migration/20260428004200_add_session_path"), + import("./migration/20260501142318_next_venus"), + import("./migration/20260504145000_add_sync_owner"), + import("./migration/20260507164347_add_workspace_time"), + import("./migration/20260510033149_session_usage"), + import("./migration/20260511000411_data_migration_state"), + import("./migration/20260511173437_session-metadata"), + import("./migration/20260601010001_normalize_storage_paths"), + import("./migration/20260601202201_amazing_prowler"), + import("./migration/20260602002951_lowly_union_jack"), + import("./migration/20260602182828_add_project_directories"), + import("./migration/20260603001617_session_message_projection_indexes"), + import("./migration/20260603040000_session_message_projection_order"), + import("./migration/20260603141458_session_input_inbox"), + import("./migration/20260603160727_jittery_ezekiel_stane"), + import("./migration/20260604172448_event_sourced_session_input"), + import("./migration/20260605003541_add_session_context_snapshot"), + import("./migration/20260605042240_add_context_epoch_agent"), + import("./migration/20260611035744_credential"), + import("./migration/20260611192811_lush_chimera"), + import("./migration/20260612174303_project_dir_strategy"), + import("./migration/20260622142730_simplify_session_context_epoch"), + import("./migration/20260622170816_reset_v2_session_state"), + import("./migration/20260622202450_simplify_session_input"), + import("./migration/20260813162312_shocking_karnak"), + ]) +).map((module) => module.default) satisfies DatabaseMigration.Migration[] diff --git a/packages/app-bundle/overlay/packages/core/src/database/migration/20260813162312_shocking_karnak.ts b/packages/app-bundle/overlay/packages/core/src/database/migration/20260813162312_shocking_karnak.ts new file mode 100644 index 00000000..740643b9 --- /dev/null +++ b/packages/app-bundle/overlay/packages/core/src/database/migration/20260813162312_shocking_karnak.ts @@ -0,0 +1,25 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260813162312_shocking_karnak", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE \`provider_permission\` ( + \`id\` text PRIMARY KEY, + \`project_id\` text NOT NULL, + \`tier_id\` text NOT NULL, + \`action\` text NOT NULL, + \`resource\` text NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + CONSTRAINT \`fk_provider_permission_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run( + `CREATE UNIQUE INDEX \`provider_permission_project_tier_action_resource_idx\` ON \`provider_permission\` (\`project_id\`,\`tier_id\`,\`action\`,\`resource\`);`, + ) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/app-bundle/overlay/packages/core/src/database/schema.gen.ts b/packages/app-bundle/overlay/packages/core/src/database/schema.gen.ts new file mode 100644 index 00000000..0902306c --- /dev/null +++ b/packages/app-bundle/overlay/packages/core/src/database/schema.gen.ts @@ -0,0 +1,289 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "./migration" + +export default { + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE \`workspace\` ( + \`id\` text PRIMARY KEY, + \`type\` text NOT NULL, + \`name\` text DEFAULT '' NOT NULL, + \`branch\` text, + \`directory\` text, + \`extra\` text, + \`project_id\` text NOT NULL, + \`time_used\` integer NOT NULL, + CONSTRAINT \`fk_workspace_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`data_migration\` ( + \`name\` text PRIMARY KEY, + \`time_completed\` integer NOT NULL + ); + `) + yield* tx.run(` + CREATE TABLE \`account_state\` ( + \`id\` integer PRIMARY KEY, + \`active_account_id\` text, + \`active_org_id\` text, + CONSTRAINT \`fk_account_state_active_account_id_account_id_fk\` FOREIGN KEY (\`active_account_id\`) REFERENCES \`account\`(\`id\`) ON DELETE SET NULL + ); + `) + yield* tx.run(` + CREATE TABLE \`account\` ( + \`id\` text PRIMARY KEY, + \`email\` text NOT NULL, + \`url\` text NOT NULL, + \`access_token\` text NOT NULL, + \`refresh_token\` text NOT NULL, + \`token_expiry\` integer, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL + ); + `) + yield* tx.run(` + CREATE TABLE \`control_account\` ( + \`email\` text NOT NULL, + \`url\` text NOT NULL, + \`access_token\` text NOT NULL, + \`refresh_token\` text NOT NULL, + \`token_expiry\` integer, + \`active\` integer NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + CONSTRAINT \`control_account_pk\` PRIMARY KEY(\`email\`, \`url\`) + ); + `) + yield* tx.run(` + CREATE TABLE \`credential\` ( + \`id\` text PRIMARY KEY, + \`integration_id\` text, + \`label\` text NOT NULL, + \`value\` text NOT NULL, + \`connector_id\` text, + \`method_id\` text, + \`active\` integer, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL + ); + `) + yield* tx.run(` + CREATE TABLE \`event_sequence\` ( + \`aggregate_id\` text PRIMARY KEY, + \`seq\` integer NOT NULL, + \`owner_id\` text + ); + `) + yield* tx.run(` + CREATE TABLE \`event\` ( + \`id\` text PRIMARY KEY, + \`aggregate_id\` text NOT NULL, + \`seq\` integer NOT NULL, + \`type\` text NOT NULL, + \`data\` text NOT NULL, + CONSTRAINT \`fk_event_aggregate_id_event_sequence_aggregate_id_fk\` FOREIGN KEY (\`aggregate_id\`) REFERENCES \`event_sequence\`(\`aggregate_id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`permission\` ( + \`id\` text PRIMARY KEY, + \`project_id\` text NOT NULL, + \`action\` text NOT NULL, + \`resource\` text NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + CONSTRAINT \`fk_permission_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`provider_permission\` ( + \`id\` text PRIMARY KEY, + \`project_id\` text NOT NULL, + \`tier_id\` text NOT NULL, + \`action\` text NOT NULL, + \`resource\` text NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + CONSTRAINT \`fk_provider_permission_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`project_directory\` ( + \`project_id\` text NOT NULL, + \`directory\` text NOT NULL, + \`type\` text, + \`strategy\` text, + \`time_created\` integer NOT NULL, + CONSTRAINT \`project_directory_pk\` PRIMARY KEY(\`project_id\`, \`directory\`), + CONSTRAINT \`fk_project_directory_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`project\` ( + \`id\` text PRIMARY KEY, + \`worktree\` text NOT NULL, + \`vcs\` text, + \`name\` text, + \`icon_url\` text, + \`icon_url_override\` text, + \`icon_color\` text, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + \`time_initialized\` integer, + \`sandboxes\` text NOT NULL, + \`commands\` text + ); + `) + yield* tx.run(` + CREATE TABLE \`message\` ( + \`id\` text PRIMARY KEY, + \`session_id\` text NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + \`data\` text NOT NULL, + CONSTRAINT \`fk_message_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`part\` ( + \`id\` text PRIMARY KEY, + \`message_id\` text NOT NULL, + \`session_id\` text NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + \`data\` text NOT NULL, + CONSTRAINT \`fk_part_message_id_message_id_fk\` FOREIGN KEY (\`message_id\`) REFERENCES \`message\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`session_context_epoch\` ( + \`session_id\` text PRIMARY KEY, + \`baseline\` text NOT NULL, + \`snapshot\` text NOT NULL, + \`baseline_seq\` integer NOT NULL, + CONSTRAINT \`fk_session_context_epoch_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`session_input\` ( + \`id\` text PRIMARY KEY, + \`session_id\` text NOT NULL, + \`prompt\` text NOT NULL, + \`delivery\` text NOT NULL, + \`admitted_seq\` integer NOT NULL, + \`promoted_seq\` integer, + \`time_created\` integer NOT NULL, + CONSTRAINT \`fk_session_input_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`session_message\` ( + \`id\` text PRIMARY KEY, + \`session_id\` text NOT NULL, + \`type\` text NOT NULL, + \`seq\` integer NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + \`data\` text NOT NULL, + CONSTRAINT \`fk_session_message_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`session\` ( + \`id\` text PRIMARY KEY, + \`project_id\` text NOT NULL, + \`workspace_id\` text, + \`parent_id\` text, + \`slug\` text NOT NULL, + \`directory\` text NOT NULL, + \`path\` text, + \`title\` text NOT NULL, + \`version\` text NOT NULL, + \`share_url\` text, + \`summary_additions\` integer, + \`summary_deletions\` integer, + \`summary_files\` integer, + \`summary_diffs\` text, + \`metadata\` text, + \`cost\` real DEFAULT 0 NOT NULL, + \`tokens_input\` integer DEFAULT 0 NOT NULL, + \`tokens_output\` integer DEFAULT 0 NOT NULL, + \`tokens_reasoning\` integer DEFAULT 0 NOT NULL, + \`tokens_cache_read\` integer DEFAULT 0 NOT NULL, + \`tokens_cache_write\` integer DEFAULT 0 NOT NULL, + \`revert\` text, + \`permission\` text, + \`agent\` text, + \`model\` text, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + \`time_compacting\` integer, + \`time_archived\` integer, + CONSTRAINT \`fk_session_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`todo\` ( + \`session_id\` text NOT NULL, + \`content\` text NOT NULL, + \`status\` text NOT NULL, + \`priority\` text NOT NULL, + \`position\` integer NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + CONSTRAINT \`todo_pk\` PRIMARY KEY(\`session_id\`, \`position\`), + CONSTRAINT \`fk_todo_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`session_share\` ( + \`session_id\` text PRIMARY KEY, + \`id\` text NOT NULL, + \`secret\` text NOT NULL, + \`url\` text NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + CONSTRAINT \`fk_session_share_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(`CREATE UNIQUE INDEX \`event_aggregate_seq_idx\` ON \`event\` (\`aggregate_id\`,\`seq\`);`) + yield* tx.run(`CREATE INDEX \`event_aggregate_type_seq_idx\` ON \`event\` (\`aggregate_id\`,\`type\`,\`seq\`);`) + yield* tx.run( + `CREATE UNIQUE INDEX \`permission_project_action_resource_idx\` ON \`permission\` (\`project_id\`,\`action\`,\`resource\`);`, + ) + yield* tx.run( + `CREATE UNIQUE INDEX \`provider_permission_project_tier_action_resource_idx\` ON \`provider_permission\` (\`project_id\`,\`tier_id\`,\`action\`,\`resource\`);`, + ) + yield* tx.run( + `CREATE INDEX \`message_session_time_created_id_idx\` ON \`message\` (\`session_id\`,\`time_created\`,\`id\`);`, + ) + yield* tx.run(`CREATE INDEX \`part_message_id_id_idx\` ON \`part\` (\`message_id\`,\`id\`);`) + yield* tx.run(`CREATE INDEX \`part_session_idx\` ON \`part\` (\`session_id\`);`) + yield* tx.run( + `CREATE INDEX \`session_input_session_pending_delivery_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`,\`delivery\`,\`admitted_seq\`);`, + ) + yield* tx.run( + `CREATE UNIQUE INDEX \`session_input_session_admitted_seq_idx\` ON \`session_input\` (\`session_id\`,\`admitted_seq\`);`, + ) + yield* tx.run( + `CREATE UNIQUE INDEX \`session_input_session_promoted_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`);`, + ) + yield* tx.run( + `CREATE UNIQUE INDEX \`session_message_session_seq_idx\` ON \`session_message\` (\`session_id\`,\`seq\`);`, + ) + yield* tx.run( + `CREATE INDEX \`session_message_session_type_seq_idx\` ON \`session_message\` (\`session_id\`,\`type\`,\`seq\`);`, + ) + yield* tx.run( + `CREATE INDEX \`session_message_session_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`time_created\`,\`id\`);`, + ) + yield* tx.run(`CREATE INDEX \`session_message_time_created_idx\` ON \`session_message\` (\`time_created\`);`) + yield* tx.run(`CREATE INDEX \`session_project_idx\` ON \`session\` (\`project_id\`);`) + yield* tx.run(`CREATE INDEX \`session_workspace_idx\` ON \`session\` (\`workspace_id\`);`) + yield* tx.run(`CREATE INDEX \`session_parent_idx\` ON \`session\` (\`parent_id\`);`) + yield* tx.run(`CREATE INDEX \`todo_session_idx\` ON \`todo\` (\`session_id\`);`) + }) + }, +} satisfies Omit diff --git a/packages/app-bundle/overlay/packages/core/src/flag/flag.ts b/packages/app-bundle/overlay/packages/core/src/flag/flag.ts new file mode 100644 index 00000000..4c1d3616 --- /dev/null +++ b/packages/app-bundle/overlay/packages/core/src/flag/flag.ts @@ -0,0 +1,79 @@ +import { Config } from "effect" + +export function truthy(key: string) { + const value = process.env[key]?.toLowerCase() + return value === "true" || value === "1" +} + +const copy = process.env["OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT"] +const fff = process.env["OPENCODE_DISABLE_FFF"] + +function enabledByExperimental(key: string) { + return process.env[key] === undefined ? truthy("OPENCODE_EXPERIMENTAL") : truthy(key) +} + +export const Flag = { + OTEL_EXPORTER_OTLP_ENDPOINT: process.env["OTEL_EXPORTER_OTLP_ENDPOINT"], + OTEL_EXPORTER_OTLP_HEADERS: process.env["OTEL_EXPORTER_OTLP_HEADERS"], + OPENCODE_DISABLE_TELEMETRY: truthy("OPENCODE_DISABLE_TELEMETRY"), + + OPENCODE_AUTO_HEAP_SNAPSHOT: truthy("OPENCODE_AUTO_HEAP_SNAPSHOT"), + OPENCODE_GIT_BASH_PATH: process.env["OPENCODE_GIT_BASH_PATH"], + OPENCODE_CONFIG: process.env["OPENCODE_CONFIG"], + OPENCODE_CONFIG_CONTENT: process.env["OPENCODE_CONFIG_CONTENT"], + OPENCODE_DISABLE_AUTOUPDATE: truthy("OPENCODE_DISABLE_AUTOUPDATE"), + OPENCODE_ALWAYS_NOTIFY_UPDATE: truthy("OPENCODE_ALWAYS_NOTIFY_UPDATE"), + OPENCODE_DISABLE_PRUNE: truthy("OPENCODE_DISABLE_PRUNE"), + OPENCODE_DISABLE_TERMINAL_TITLE: truthy("OPENCODE_DISABLE_TERMINAL_TITLE"), + OPENCODE_SHOW_TTFD: truthy("OPENCODE_SHOW_TTFD"), + OPENCODE_DISABLE_AUTOCOMPACT: truthy("OPENCODE_DISABLE_AUTOCOMPACT"), + OPENCODE_DISABLE_MODELS_FETCH: truthy("OPENCODE_DISABLE_MODELS_FETCH"), + OPENCODE_DISABLE_MOUSE: truthy("OPENCODE_DISABLE_MOUSE"), + OPENCODE_FAKE_VCS: process.env["OPENCODE_FAKE_VCS"], + OPENCODE_SERVER_PASSWORD: process.env["OPENCODE_SERVER_PASSWORD"], + OPENCODE_SERVER_USERNAME: process.env["OPENCODE_SERVER_USERNAME"], + OPENCODE_DISABLE_FFF: fff === undefined ? process.platform === "win32" : truthy("OPENCODE_DISABLE_FFF"), + + // Experimental + OPENCODE_EXPERIMENTAL_FILEWATCHER: Config.boolean("OPENCODE_EXPERIMENTAL_FILEWATCHER").pipe( + Config.withDefault(false), + ), + OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: Config.boolean("OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER").pipe( + Config.withDefault(false), + ), + OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT: + copy === undefined ? process.platform === "win32" : truthy("OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT"), + OPENCODE_MODELS_URL: process.env["OPENCODE_MODELS_URL"], + OPENCODE_MODELS_PATH: process.env["OPENCODE_MODELS_PATH"], + OPENCODE_DB: process.env["OPENCODE_DB"], + + OPENCODE_WORKSPACE_ID: process.env["OPENCODE_WORKSPACE_ID"], + OPENCODE_EXPERIMENTAL_WORKSPACES: enabledByExperimental("OPENCODE_EXPERIMENTAL_WORKSPACES"), + + // Evaluated at access time (not module load) because tests, the CLI, and + // external tooling set these env vars at runtime. + get OPENCODE_DISABLE_PROJECT_CONFIG() { + return truthy("OPENCODE_DISABLE_PROJECT_CONFIG") + }, + get OPENCODE_EXPERIMENTAL_REFERENCES() { + return enabledByExperimental("OPENCODE_EXPERIMENTAL_REFERENCES") + }, + get OPENCODE_TUI_CONFIG() { + return process.env["OPENCODE_TUI_CONFIG"] + }, + get OPENCODE_CONFIG_DIR() { + return process.env["OPENCODE_CONFIG_DIR"] + }, + get OPENCODE_PURE() { + return truthy("OPENCODE_PURE") + }, + get OPENCODE_PERMISSION() { + return process.env["OPENCODE_PERMISSION"] + }, + get OPENCODE_PLUGIN_META_FILE() { + return process.env["OPENCODE_PLUGIN_META_FILE"] + }, + get OPENCODE_CLIENT() { + return process.env["OPENCODE_CLIENT"] ?? "cli" + }, +} diff --git a/packages/app-bundle/overlay/packages/core/src/models-dev.ts b/packages/app-bundle/overlay/packages/core/src/models-dev.ts new file mode 100644 index 00000000..8ce2fc21 --- /dev/null +++ b/packages/app-bundle/overlay/packages/core/src/models-dev.ts @@ -0,0 +1,269 @@ +import path from "path" +import { Context, Duration, Effect, Layer, Option, Schedule, Schema } from "effect" +import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http" +import { ModelsDev } from "@opencode-ai/schema/models-dev" +import { Global } from "./global" +import { Flag } from "./flag/flag" +import { Flock } from "./util/flock" +import { Hash } from "./util/hash" +import { FSUtil } from "./fs-util" +import { InstallationChannel, InstallationVersion } from "./installation/version" +import { EventV2 } from "./event" +import { makeGlobalNode } from "./effect/app-node" +import { httpClient } from "./effect/app-node-platform" + +export const CatalogModelStatus = Schema.Literals(["alpha", "beta", "deprecated"]) +export type CatalogModelStatus = typeof CatalogModelStatus.Type + +const InterleavedField = Schema.Union([ + Schema.Literals(["reasoning", "reasoning_content", "reasoning_text"]), + Schema.String, +]) + +const USER_AGENT = `opencode/${InstallationChannel}/${InstallationVersion}/${Flag.OPENCODE_CLIENT}` + +const CostTier = Schema.Struct({ + input: Schema.Finite, + output: Schema.Finite, + cache_read: Schema.optional(Schema.Finite), + cache_write: Schema.optional(Schema.Finite), + tier: Schema.Struct({ + type: Schema.Literal("context"), + size: Schema.Finite, + }), +}) + +const Cost = Schema.Struct({ + input: Schema.Finite, + output: Schema.Finite, + cache_read: Schema.optional(Schema.Finite), + cache_write: Schema.optional(Schema.Finite), + tiers: Schema.optional(Schema.Array(CostTier)), + context_over_200k: Schema.optional( + Schema.Struct({ + input: Schema.Finite, + output: Schema.Finite, + cache_read: Schema.optional(Schema.Finite), + cache_write: Schema.optional(Schema.Finite), + }), + ), +}) + +// How a model exposes its reasoning control in the models.dev catalog. Ported +// from upstream alongside provider/transform.ts — the transform derives thinking +// variants from this instead of pattern-matching model IDs. See AMICODE-PATCHES.md. +const ReasoningOption = Schema.Union([ + Schema.Struct({ + type: Schema.Literal("effort"), + values: Schema.Array(Schema.NullOr(Schema.String)), + }), + Schema.Struct({ + type: Schema.Literal("toggle"), + }), + Schema.Struct({ + type: Schema.Literal("budget_tokens"), + min: Schema.optional(Schema.Finite), + max: Schema.optional(Schema.Finite), + }), +]) + +export const Model = Schema.Struct({ + id: Schema.String, + name: Schema.String, + family: Schema.optional(Schema.String), + release_date: Schema.String, + attachment: Schema.Boolean, + reasoning: Schema.Boolean, + temperature: Schema.Boolean, + tool_call: Schema.Boolean, + reasoning_options: Schema.optional(Schema.Array(ReasoningOption)), + interleaved: Schema.optional( + Schema.Union([ + Schema.Boolean, + InterleavedField, + Schema.Struct({ + field: InterleavedField, + }), + ]), + ), + cost: Schema.optional(Cost), + limit: Schema.Struct({ + context: Schema.Finite, + input: Schema.optional(Schema.Finite), + output: Schema.Finite, + }), + modalities: Schema.optional( + Schema.Struct({ + input: Schema.Array(Schema.Literals(["text", "audio", "image", "video", "pdf"])), + output: Schema.Array(Schema.Literals(["text", "audio", "image", "video", "pdf"])), + }), + ), + experimental: Schema.optional( + Schema.Struct({ + modes: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + cost: Schema.optional(Cost), + provider: Schema.optional( + Schema.Struct({ + body: Schema.optional(Schema.Record(Schema.String, Schema.MutableJson)), + headers: Schema.optional(Schema.Record(Schema.String, Schema.String)), + }), + ), + }), + ), + ), + }), + ), + status: Schema.optional(CatalogModelStatus), + provider: Schema.optional( + Schema.Struct({ npm: Schema.optional(Schema.String), api: Schema.optional(Schema.String) }), + ), +}) +export type Model = Schema.Schema.Type + +export const Provider = Schema.Struct({ + api: Schema.optional(Schema.String), + name: Schema.String, + env: Schema.Array(Schema.String), + id: Schema.String, + npm: Schema.optional(Schema.String), + models: Schema.Record(Schema.String, Model), +}) + +export type Provider = Schema.Schema.Type + +export const Event = ModelsDev.Event + +declare const OPENCODE_MODELS_DEV: Record | undefined + +export interface Interface { + readonly get: () => Effect.Effect> + readonly refresh: (force?: boolean) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/ModelsDev") {} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* FSUtil.Service + const events = yield* EventV2.Service + const http = HttpClient.filterStatusOk( + (yield* HttpClient.HttpClient).pipe( + HttpClient.retryTransient({ + retryOn: "errors-and-responses", + times: 2, + schedule: Schedule.exponential(200).pipe(Schedule.jittered), + }), + ), + ) + + const source = Flag.OPENCODE_MODELS_URL || "https://models.opencode.ai" + const filepath = path.join( + Global.Path.cache, + source === "https://models.opencode.ai" ? "models.json" : `models-${Hash.fast(source)}.json`, + ) + const ttl = Duration.minutes(5) + const lockKey = `models-dev:${filepath}` + + const fresh = Effect.fnUntraced(function* () { + const stat = yield* fs.stat(filepath).pipe(Effect.catch(() => Effect.succeed(undefined))) + if (!stat) return false + const mtime = Option.getOrElse(stat.mtime, () => new Date(0)).getTime() + return Date.now() - mtime < Duration.toMillis(ttl) + }) + + const fetchApi = Effect.fn("ModelsDev.fetchApi")(function* () { + return yield* HttpClientRequest.get(`${source}/api.json`).pipe( + HttpClientRequest.setHeader("User-Agent", USER_AGENT), + http.execute, + Effect.flatMap((res) => res.text), + Effect.timeout("10 seconds"), + ) + }) + + const loadFromDisk = fs.readJson(Flag.OPENCODE_MODELS_PATH ?? filepath).pipe( + Effect.catch((error) => { + if ( + Flag.OPENCODE_MODELS_PATH === undefined && + error._tag === "FileSystemError" && + error.method === "readJson" + ) { + return fs.remove(filepath, { force: true }).pipe(Effect.ignore, Effect.as(undefined)) + } + return Effect.succeed(undefined) + }), + Effect.map((v) => v as Record | undefined), + ) + + const loadSnapshot = Effect.sync(() => + typeof OPENCODE_MODELS_DEV === "undefined" ? undefined : OPENCODE_MODELS_DEV, + ) + + const fetchAndWrite = Effect.fn("ModelsDev.fetchAndWrite")(function* () { + const text = yield* fetchApi() + const tempfile = `${filepath}.${process.pid}.${Date.now()}.tmp` + yield* fs.writeWithDirs(tempfile, text).pipe( + Effect.andThen(fs.rename(tempfile, filepath)), + Effect.catch((error) => + Effect.gen(function* () { + yield* fs.remove(tempfile, { force: true }).pipe(Effect.ignore) + return yield* Effect.fail(error) + }), + ), + ) + return text + }) + + const populate = Effect.gen(function* () { + const fromDisk = yield* loadFromDisk + if (fromDisk) return fromDisk + const snapshot = yield* loadSnapshot + if (snapshot) return snapshot + if (Flag.OPENCODE_DISABLE_MODELS_FETCH) return {} + // Flock is cross-process: concurrent opencode CLIs can race on this cache file. + const text = yield* Effect.scoped( + Effect.gen(function* () { + yield* Flock.effect(lockKey) + return yield* fetchAndWrite() + }), + ) + return JSON.parse(text) as Record + }).pipe(Effect.withSpan("ModelsDev.populate"), Effect.orDie) + + const [cachedGet, invalidate] = yield* Effect.cachedInvalidateWithTTL(populate, Duration.infinity) + + const get = (): Effect.Effect> => cachedGet + + const refresh = Effect.fn("ModelsDev.refresh")(function* (force = false) { + if (!force && (yield* fresh())) return + yield* Effect.scoped( + Effect.gen(function* () { + yield* Flock.effect(lockKey) + // Re-check under the lock: another process may have refreshed between + // our outer check and lock acquisition. + if (!force && (yield* fresh())) return + yield* fetchAndWrite() + yield* invalidate + yield* events.publish(Event.Refreshed, {}) + }), + ).pipe( + Effect.tapCause((cause) => Effect.logError("Failed to fetch models.dev", { cause: cause })), + Effect.ignore, + ) + }) + + if (!Flag.OPENCODE_DISABLE_MODELS_FETCH && !process.argv.includes("--get-yargs-completions")) { + // Schedule.spaced runs the effect once, then waits between completions. + yield* Effect.forkScoped(refresh().pipe(Effect.repeat(Schedule.spaced("60 minutes")), Effect.ignore)) + } + + return Service.of({ get, refresh }) + }), +) + +export const node = makeGlobalNode({ service: Service, layer: layer, deps: [FSUtil.node, EventV2.node, httpClient] }) + +export * as ModelsDev from "./models-dev" diff --git a/packages/app-bundle/overlay/packages/core/src/observability/otlp.ts b/packages/app-bundle/overlay/packages/core/src/observability/otlp.ts new file mode 100644 index 00000000..b17a2dd2 --- /dev/null +++ b/packages/app-bundle/overlay/packages/core/src/observability/otlp.ts @@ -0,0 +1,194 @@ +import { Layer, Effect, Schedule, Duration } from "effect" +import { OtlpLogger } from "effect/unstable/observability" +import type { Context } from "@opentelemetry/api" +import type { ReadableSpan, Span, SpanExporter, SpanProcessor } from "@opentelemetry/sdk-trace-base" +import { Flag } from "../flag/flag" +import { InstallationChannel, InstallationVersion } from "../installation/version" +import { runID } from "./shared" + +const endpoint = Flag.OTEL_EXPORTER_OTLP_ENDPOINT + +const headers = Flag.OTEL_EXPORTER_OTLP_HEADERS + ? Flag.OTEL_EXPORTER_OTLP_HEADERS.split(",").reduce( + (acc, entry) => { + const [key, ...value] = entry.split("=") + acc[key] = value.join("=") + return acc + }, + {} as Record, + ) + : undefined + +function resourceAttributes() { + const value = process.env.OTEL_RESOURCE_ATTRIBUTES + if (!value) return {} + try { + return Object.fromEntries( + value.split(",").map((entry) => { + const index = entry.indexOf("=") + if (index < 1) throw new Error("Invalid OTEL_RESOURCE_ATTRIBUTES entry") + return [decodeURIComponent(entry.slice(0, index)), decodeURIComponent(entry.slice(index + 1))] + }), + ) + } catch { + return {} + } +} + +export function resource(): { serviceName: string; serviceVersion: string; attributes: Record } { + return { + serviceName: "opencode", + serviceVersion: InstallationVersion, + attributes: { + ...resourceAttributes(), + "deployment.environment.name": InstallationChannel, + "opencode.client": Flag.OPENCODE_CLIENT, + "opencode.run": runID, + "service.instance.id": runID, + }, + } +} + +// Rate limiting configuration to prevent AWS CloudFront bans +const TELEMETRY_BATCH_SIZE = 100 +const TELEMETRY_BATCH_DELAY_MS = 5000 +const TELEMETRY_MAX_QUEUE_SIZE = 1000 +const TELEMETRY_EXPORT_RETRIES = 3 + +class RateLimitedSpanProcessor implements SpanProcessor { + private spans: ReadableSpan[] = [] + private timer: NodeJS.Timeout | undefined + private exporting: Promise | undefined + private nextExportAt = 0 + private shutdownPromise: Promise | undefined + private isShutdown = false + + constructor(private readonly exporter: SpanExporter) {} + + onStart(_span: Span, _parentContext: Context): void {} + + onEnd(span: ReadableSpan): void { + if (this.isShutdown || this.spans.length >= TELEMETRY_MAX_QUEUE_SIZE) return + this.spans.push(span) + this.scheduleExport() + } + + async forceFlush(): Promise { + this.clearTimer() + while (this.exporting || this.spans.length > 0) { + if (this.exporting) { + await this.exporting + continue + } + await this.exportWithRetry(this.spans.splice(0, TELEMETRY_BATCH_SIZE)) + } + await this.exporter.forceFlush?.() + } + + shutdown(): Promise { + this.shutdownPromise ??= this.shutdownProcessor() + return this.shutdownPromise + } + + private async shutdownProcessor(): Promise { + this.isShutdown = true + try { + await this.forceFlush() + } finally { + await this.exporter.shutdown() + } + } + + private scheduleExport(): void { + if (this.isShutdown || this.exporting || this.timer || this.spans.length === 0) return + const delay = this.spans.length >= TELEMETRY_BATCH_SIZE ? Math.max(0, this.nextExportAt - Date.now()) : TELEMETRY_BATCH_DELAY_MS + this.timer = setTimeout(() => { + this.timer = undefined + this.exportNextBatch() + }, delay) + } + + private exportNextBatch(): void { + if (this.exporting || this.spans.length === 0) return + const batch = this.spans.splice(0, TELEMETRY_BATCH_SIZE) + const exporting = this.exportWithRetry(batch) + this.exporting = exporting + void exporting.catch((error) => console.warn("Telemetry export failed after retries:", error)) + void exporting + .finally(() => { + this.exporting = undefined + this.nextExportAt = Date.now() + TELEMETRY_BATCH_DELAY_MS + this.scheduleExport() + }) + .catch(() => undefined) + } + + private async exportWithRetry(spans: ReadableSpan[]): Promise { + for (let attempt = 0; ; attempt++) { + try { + await this.export(spans) + return + } catch (error) { + if (attempt >= TELEMETRY_EXPORT_RETRIES) throw error + await new Promise((resolve) => setTimeout(resolve, 1000 * 2 ** attempt)) + } + } + } + + private export(spans: ReadableSpan[]): Promise { + return new Promise((resolve, reject) => { + try { + this.exporter.export(spans, (result) => { + if (result.code === 0) { + resolve() + return + } + reject(result.error ?? new Error("Telemetry export failed")) + }) + } catch (error) { + reject(error) + } + }) + } + + private clearTimer(): void { + if (!this.timer) return + clearTimeout(this.timer) + this.timer = undefined + } +} + +export function loggers() { + // Disable telemetry if flag is set + if (Flag.OPENCODE_DISABLE_TELEMETRY || !endpoint) return [] + return [OtlpLogger.make({ url: `${endpoint}/v1/logs`, resource: resource(), headers })] +} + +export async function tracingLayer() { + // Disable telemetry if flag is set + if (Flag.OPENCODE_DISABLE_TELEMETRY || !endpoint) return Layer.empty + const NodeSdk = await import("@effect/opentelemetry/NodeSdk") + const OTLP = await import("@opentelemetry/exporter-trace-otlp-http") + const { AsyncLocalStorageContextManager } = await import("@opentelemetry/context-async-hooks") + const { context } = await import("@opentelemetry/api") + + // The Effect Node SDK does not register a global context manager, but the AI SDK uses it to parent spans. + const manager = new AsyncLocalStorageContextManager() + manager.enable() + context.setGlobalContextManager(manager) + + const exporter = new OTLP.OTLPTraceExporter({ + url: `${endpoint}/v1/traces`, + headers, + timeoutMillis: 30000, // 30 second timeout + }) + + const processor = new RateLimitedSpanProcessor(exporter) + + return NodeSdk.layer(() => ({ + resource: resource(), + spanProcessor: processor, + })) +} + +export * as Otlp from "./otlp" diff --git a/packages/app-bundle/overlay/packages/core/src/permission.ts b/packages/app-bundle/overlay/packages/core/src/permission.ts new file mode 100644 index 00000000..6ee7f7a3 --- /dev/null +++ b/packages/app-bundle/overlay/packages/core/src/permission.ts @@ -0,0 +1,429 @@ +// @ts-nocheck — pre-existing type mismatch with Effect.fn vs Entry yield (main is broken, suppress for registry PR) +export * as PermissionV2 from "./permission" + +import { makeLocationNode } from "./effect/app-node" +import { Context, Deferred, Effect as EffectRuntime, Layer, Schema } from "effect" +import { Permission } from "@opencode-ai/schema/permission" +import { ProviderPermission } from "@opencode-ai/schema/provider-permission" +import { EventV2 } from "./event" +import { Location } from "./location" +import { AgentV2 } from "./agent" +import { SessionV2 } from "./session" +import { SessionStore } from "./session/store" +import { Wildcard } from "./util/wildcard" +import { PermissionSaved } from "./permission/saved" +import { ProviderPermissionSaved } from "./permission/provider-saved" +import { Config } from "./config" + +export { Effect, Rule, Ruleset } from "@opencode-ai/schema/permission" +const missingAgentPermissions: Permission.Ruleset = [{ action: "*", resource: "*", effect: "deny" }] + +export const ID = Permission.ID +export type ID = typeof ID.Type + +export const Source = Permission.Source +export type Source = typeof Source.Type + +const RequestFields = { + sessionID: Permission.Request.fields.sessionID, + action: Permission.Request.fields.action, + resources: Permission.Request.fields.resources, + save: Permission.Request.fields.save, + metadata: Permission.Request.fields.metadata, + source: Permission.Request.fields.source, +} + +export const Request = Permission.Request +export type Request = typeof Request.Type + +export const Reply = Permission.Reply +export type Reply = typeof Reply.Type + +export const AssertInput = Schema.Struct({ + id: ID.pipe(Schema.optional), + ...RequestFields, + agent: AgentV2.ID.pipe(Schema.optional), +}).annotate({ identifier: "PermissionV2.AssertInput" }) +export type AssertInput = typeof AssertInput.Type + +export const ReplyInput = Schema.Struct({ + requestID: ID, + reply: Reply, + message: Schema.String.pipe(Schema.optional), +}).annotate({ identifier: "PermissionV2.ReplyInput" }) +export type ReplyInput = typeof ReplyInput.Type + +export const AskResult = Schema.Struct({ + id: ID, + effect: Permission.Effect, +}).annotate({ identifier: "PermissionV2.AskResult" }) +export type AskResult = typeof AskResult.Type + +export const Event = Permission.Event + +export class DeclinedError extends Schema.TaggedErrorClass()("PermissionV2.DeclinedError", {}) {} + +export class CorrectedError extends Schema.TaggedErrorClass()("PermissionV2.CorrectedError", { + feedback: Schema.String, +}) {} + +export class BlockedError extends Schema.TaggedErrorClass()("PermissionV2.BlockedError", { + rules: Permission.Ruleset, +}) {} + +export class NotFoundError extends Schema.TaggedErrorClass()("PermissionV2.NotFoundError", { + requestID: ID, +}) {} + +export type Error = BlockedError | CorrectedError + +export function evaluate(action: string, resource: string, ...rulesets: Permission.Ruleset[]): Permission.Rule { + return ( + rulesets + .flat() + .findLast((rule) => Wildcard.match(action, rule.action) && Wildcard.match(resource, rule.resource)) ?? { + action, + resource: "*", + effect: "ask", + } + ) +} + +export function merge(...rulesets: Permission.Ruleset[]): Permission.Ruleset { + return rulesets.flat() +} + +export interface Interface { + readonly ask: (input: AssertInput) => EffectRuntime.Effect + readonly assert: (input: AssertInput) => EffectRuntime.Effect + readonly reply: (input: ReplyInput) => EffectRuntime.Effect + readonly get: (id: ID) => EffectRuntime.Effect + readonly forSession: (sessionID: SessionV2.ID) => EffectRuntime.Effect> + readonly list: () => EffectRuntime.Effect> +} + +export class Service extends Context.Service()("@opencode/v2/Permission") {} + +interface Pending { + readonly request: Request + readonly agent?: AgentV2.ID + readonly deferred: Deferred.Deferred +} + +const layer = Layer.effect( + Service, + EffectRuntime.gen(function* () { + const events = yield* EventV2.Service + const location = yield* Location.Service + const agents = yield* AgentV2.Service + const sessions = yield* SessionStore.Service + const saved = yield* PermissionSaved.Service + const providerSaved = yield* ProviderPermissionSaved.Service + const configs = yield* Config.Service + const pending = new Map() + + yield* EffectRuntime.addFinalizer(() => + EffectRuntime.forEach(pending.values(), (item) => Deferred.fail(item.deferred, new DeclinedError()), { + discard: true, + }).pipe( + EffectRuntime.ensuring( + EffectRuntime.sync(() => { + pending.clear() + }), + ), + ), + ) + + const savedRules = EffectRuntime.fnUntraced(function* () { + return (yield* saved.list({ projectID: location.project.id })).map( + (item): Permission.Rule => ({ action: item.action, resource: item.resource, effect: "allow" }), + ) + }) + + const configured = EffectRuntime.fn("PermissionV2.configured")(function* ( + sessionID: SessionV2.ID, + agentID?: AgentV2.ID, + ) { + const session = yield* sessions.get(sessionID) + if (!session) return yield* new SessionV2.NotFoundError({ sessionID }) + const agent = yield* agents.resolve(agentID ?? session.agent) + return agent?.permissions ?? missingAgentPermissions + }) + + function denied(input: AssertInput, rules: Permission.Ruleset) { + return input.resources.some((resource) => evaluate(input.action, resource, rules).effect === "deny") + } + + function relevant(input: AssertInput, rules: Permission.Ruleset) { + return rules.filter((rule) => Wildcard.match(input.action, rule.action)) + } + + const evaluateProvider = EffectRuntime.fnUntraced(function* (input: AssertInput) { + const entries = yield* configs.entries() + const raw = Config.latest(entries, "providerPermissions") as unknown as ProviderPermission.Config | undefined + let cfg: ProviderPermission.Config = ProviderPermission.DEFAULT_CONFIG + if (raw && typeof raw === "object" && Array.isArray((raw as ProviderPermission.Config).tiers)) { + cfg = raw as ProviderPermission.Config + // Ensure defaultTier exists + if (!cfg.tiers.find((t) => t.id === cfg.defaultTier)) { + cfg = { ...cfg, defaultTier: ProviderPermission.DEFAULT_CONFIG.defaultTier } + } + } + + // Resolve model id from session if available + let modelId: string | undefined + const session = yield* sessions.get(input.sessionID).pipe(EffectRuntime.catch(() => EffectRuntime.succeed(undefined))) + if (session?.model) { + modelId = `${session.model.providerID}/${session.model.id}` + } + // Also check metadata for model override (tool call context may carry it) + if (!modelId && input.metadata && typeof (input.metadata as Record).model === "string") { + modelId = (input.metadata as Record).model as string + } + const lookupId = modelId ?? "__unassigned__" + const tierId = cfg.assignments[lookupId] ?? cfg.defaultTier + + // Tier-keyed always-grants (SQLite) override matrix ask → allow + const providerGrants = yield* providerSaved + .list({ projectID: location.project.id, tierID: tierId }) + .pipe(EffectRuntime.catch(() => EffectRuntime.succeed([] as readonly import("./permission/provider-saved").ProviderPermissionSaved.Info[]))) + const isProviderAllowed = (action: string, resource: string) => + providerGrants.some( + (g) => Wildcard.match(action, g.action) && Wildcard.match(resource, g.resource), + ) + + const effects: ProviderPermission.Effect[] = [] + for (const resource of input.resources) { + const resourcePath = resource || "**" + // Saved tier grant takes precedence (authoritative allow) + if (isProviderAllowed(input.action, resourcePath)) { + effects.push("allow") + continue + } + const eff = ProviderPermission.resolveEffect(cfg, lookupId, input.action, resourcePath) + if (eff) effects.push(eff) + } + if (effects.length === 0) { + // Network tools or unknown actions: still check with empty resource + if (isProviderAllowed(input.action, "**")) return "allow" as const + const eff = ProviderPermission.resolveEffect(cfg, lookupId, input.action, "**") + if (eff) return eff + return undefined + } + if (effects.includes("deny")) return "deny" as const + if (effects.includes("ask")) return "ask" as const + return "allow" as const + }) + + const evaluateInput = EffectRuntime.fnUntraced(function* (input: AssertInput) { + const providerEffect = yield* evaluateProvider(input).pipe(EffectRuntime.catch(() => EffectRuntime.succeed(undefined))) + if (providerEffect === "deny") { + return { effect: "deny" as const, rules: [] as Permission.Ruleset } + } + if (providerEffect === "allow") { + return { effect: "allow" as const, rules: [] as Permission.Ruleset } + } + const rules = yield* configured(input.sessionID, input.agent) + if (denied(input, rules)) return { effect: "deny" as const, rules } + const all = [...rules, ...(yield* savedRules())] + const effects = input.resources.map((resource) => evaluate(input.action, resource, all).effect) + const effect: Permission.Effect = effects.includes("deny") ? "deny" : effects.includes("ask") ? "ask" : "allow" + // If provider said ask, keep the computed effect (ask will prompt) + return { effect, rules: all } + }) + + function request(input: AssertInput): Request { + return { + id: input.id ?? ID.create(), + sessionID: input.sessionID, + action: input.action, + resources: input.resources, + save: input.save, + metadata: input.metadata, + source: input.source, + } + } + + const create = (request: Request, agent?: AgentV2.ID) => + EffectRuntime.uninterruptible( + EffectRuntime.gen(function* () { + const deferred = yield* Deferred.make() + const item = { request, agent, deferred } + if (pending.has(request.id)) return yield* EffectRuntime.die(`Duplicate pending permission ID: ${request.id}`) + pending.set(request.id, item) + yield* events + .publish(Event.Asked, request) + .pipe(EffectRuntime.onError(() => EffectRuntime.sync(() => pending.delete(request.id)))) + return item + }), + ) + + const ask = EffectRuntime.fn("PermissionV2.ask")(function* (input: AssertInput) { + const result = yield* evaluateInput(input) + const value = request(input) + if (result.effect === "ask") yield* create(value, input.agent) + return { id: value.id, effect: result.effect } + }) + + const assert = EffectRuntime.fn("PermissionV2.assert")((input: AssertInput) => + EffectRuntime.uninterruptibleMask((restore) => + EffectRuntime.gen(function* () { + const result = yield* evaluateInput(input) + if (result.effect === "deny") { + return yield* new BlockedError({ + rules: relevant(input, result.rules), + }) + } + if (result.effect === "allow") return + const item = yield* create(request(input), input.agent) + return yield* restore(Deferred.await(item.deferred)).pipe( + EffectRuntime.catchTag("PermissionV2.DeclinedError", (error) => EffectRuntime.die(error)), + EffectRuntime.ensuring( + EffectRuntime.sync(() => { + pending.delete(item.request.id) + }), + ), + ) + }), + ), + ) + + const reply = EffectRuntime.fn("PermissionV2.reply")((input: ReplyInput) => + EffectRuntime.uninterruptible( + EffectRuntime.gen(function* () { + const existing = pending.get(input.requestID) + if (!existing) return yield* new NotFoundError({ requestID: input.requestID }) + yield* events.publish(Event.Replied, { + sessionID: existing.request.sessionID, + requestID: existing.request.id, + reply: input.reply, + }) + + if (input.reply === "reject") { + yield* Deferred.fail( + existing.deferred, + input.message ? new CorrectedError({ feedback: input.message }) : new DeclinedError(), + ) + pending.delete(input.requestID) + for (const [id, item] of pending) { + if (item.request.sessionID !== existing.request.sessionID) continue + yield* events.publish(Event.Replied, { + sessionID: item.request.sessionID, + requestID: item.request.id, + reply: "reject", + }) + yield* Deferred.fail(item.deferred, new DeclinedError()) + pending.delete(id) + } + return + } + + if (input.reply === "always" && existing.request.save?.length) { + yield* saved.add({ + projectID: location.project.id, + action: existing.request.action, + resources: existing.request.save, + }) + // Provider-permission tier-keyed always grant (spec: keyed by tier) + const entriesForTier = yield* configs.entries().pipe(EffectRuntime.catch(() => EffectRuntime.succeed([] as unknown as readonly import("./config").Config.Entry[]))) as unknown as readonly import("./config").Config.Entry[] + const ppRaw = Config.latest(entriesForTier as never, "providerPermissions" as never) as unknown as + | import("@opencode-ai/schema/provider-permission").ProviderPermission.Config + | undefined + let tierForSave = "unassigned" + if (ppRaw && Array.isArray((ppRaw as unknown as { tiers: unknown[] }).tiers)) { + const cfg = ppRaw as import("@opencode-ai/schema/provider-permission").ProviderPermission.Config + const sess = yield* sessions + .get(existing.request.sessionID) + .pipe(EffectRuntime.catch(() => EffectRuntime.succeed(undefined))) + if (sess?.model) { + const mid = `${sess.model.providerID}/${sess.model.id}` + tierForSave = cfg.assignments[mid] ?? cfg.defaultTier + } else { + tierForSave = cfg.defaultTier + } + } + yield* providerSaved + .add({ + projectID: location.project.id, + tierID: tierForSave, + action: existing.request.action, + resources: existing.request.save, + }) + .pipe(EffectRuntime.catch(() => EffectRuntime.succeed(undefined))) + } + yield* Deferred.succeed(existing.deferred, undefined) + pending.delete(input.requestID) + if (input.reply !== "always" || !existing.request.save?.length) return + + const rememberedRules = yield* savedRules() + for (const [id, item] of pending) { + const input = { ...item.request } + const rules = yield* configured(item.request.sessionID, item.agent).pipe( + EffectRuntime.catchTag("Session.NotFoundError", () => EffectRuntime.succeed(undefined)), + ) + if (!rules) continue + if (denied(input, rules)) continue + const effective = [...rules, ...rememberedRules] + if ( + !item.request.resources.every( + (resource) => evaluate(item.request.action, resource, effective).effect === "allow", + ) + ) + continue + yield* events.publish(Event.Replied, { + sessionID: item.request.sessionID, + requestID: item.request.id, + reply: "always", + }) + yield* Deferred.succeed(item.deferred, undefined) + pending.delete(id) + } + // Provider tier pending auto-allow (saved tier grants may now satisfy provider check) + for (const [id, item] of Array.from(pending.entries())) { + const providerEffect = yield* evaluateProvider(item.request as unknown as typeof existing.request).pipe( + EffectRuntime.catch(() => EffectRuntime.succeed("ask" as const)), + ) + if (providerEffect !== "allow") continue + yield* events.publish(Event.Replied, { + sessionID: item.request.sessionID, + requestID: item.request.id, + reply: "always", + }) + yield* Deferred.succeed(item.deferred, undefined) + pending.delete(id) + } + }), + ), + ) + + const list = EffectRuntime.fn("PermissionV2.list")(function* () { + return Array.from(pending.values(), (item) => item.request) + }) + + const get = EffectRuntime.fn("PermissionV2.get")(function* (id: ID) { + return pending.get(id)?.request + }) + + const forSession = EffectRuntime.fn("PermissionV2.forSession")(function* (sessionID: SessionV2.ID) { + return Array.from(pending.values(), (item) => item.request).filter((request) => request.sessionID === sessionID) + }) + + return Service.of({ ask, assert, reply, get, forSession, list }) + }), +) + +export const locationLayer = layer.pipe(Layer.provideMerge(AgentV2.locationLayer), Layer.provideMerge(Config.locationLayer)) + +export const node = makeLocationNode({ + service: Service, + layer, + deps: [ + EventV2.node, + Location.node, + AgentV2.node, + SessionStore.node, + PermissionSaved.node, + ProviderPermissionSaved.node, + Config.node, + ], +}) diff --git a/packages/app-bundle/overlay/packages/core/src/permission/provider-saved.ts b/packages/app-bundle/overlay/packages/core/src/permission/provider-saved.ts new file mode 100644 index 00000000..75a422bc --- /dev/null +++ b/packages/app-bundle/overlay/packages/core/src/permission/provider-saved.ts @@ -0,0 +1,94 @@ +export * as ProviderPermissionSaved from "./provider-saved" + +import { and, eq } from "drizzle-orm" +import { Context, Effect, Layer, Schema } from "effect" +import { Database } from "../database/database" +import { makeGlobalNode } from "../effect/app-node" +import { ProjectV2 } from "../project" +import { ProviderPermissionTable } from "./sql" +import { PermissionSaved } from "@opencode-ai/schema/permission-saved" + +export const ID = PermissionSaved.ID +export type ID = typeof ID.Type + +export const Info = Schema.Struct({ + id: ID, + projectID: ProjectV2.ID, + tierID: Schema.String, + action: Schema.String, + resource: Schema.String, +}).annotate({ identifier: "ProviderPermissionSaved.Info" }) +export type Info = typeof Info.Type + +export const ListInput = Schema.Struct({ + projectID: ProjectV2.ID.pipe(Schema.optional), + tierID: Schema.String.pipe(Schema.optional), +}).annotate({ identifier: "ProviderPermissionSaved.ListInput" }) +export type ListInput = typeof ListInput.Type + +export const AddInput = Schema.Struct({ + projectID: ProjectV2.ID, + tierID: Schema.String, + action: Schema.String, + resources: Schema.Array(Schema.String), +}).annotate({ identifier: "ProviderPermissionSaved.AddInput" }) +export type AddInput = typeof AddInput.Type + +export interface Interface { + readonly list: (input?: ListInput) => Effect.Effect> + readonly add: (input: AddInput) => Effect.Effect + readonly remove: (id: ID) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/ProviderPermissionSaved") {} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const { db } = yield* Database.Service + + const list = Effect.fn("ProviderPermissionSaved.list")(function* (input?: ListInput) { + const conditions = [ + input?.projectID ? eq(ProviderPermissionTable.project_id, input.projectID) : undefined, + input?.tierID ? eq(ProviderPermissionTable.tier_id, input.tierID) : undefined, + ].filter(Boolean) as never[] + const where = conditions.length ? and(...conditions) : undefined + const rows = yield* db.select().from(ProviderPermissionTable).where(where).all().pipe(Effect.orDie) + return rows.map( + (row): Info => ({ + id: row.id as ID, + projectID: row.project_id as ProjectV2.ID, + tierID: row.tier_id, + action: row.action, + resource: row.resource, + }), + ) + }) + + const add = Effect.fn("ProviderPermissionSaved.add")(function* (input: AddInput) { + if (!input.resources.length) return + yield* db + .insert(ProviderPermissionTable) + .values( + input.resources.map((resource) => ({ + id: ID.create(), + project_id: input.projectID, + tier_id: input.tierID, + action: input.action, + resource, + })), + ) + .onConflictDoNothing() + .run() + .pipe(Effect.orDie) + }) + + const remove = Effect.fn("ProviderPermissionSaved.remove")(function* (id: ID) { + yield* db.delete(ProviderPermissionTable).where(eq(ProviderPermissionTable.id, id)).run().pipe(Effect.orDie) + }) + + return Service.of({ list, add, remove }) + }), +) + +export const node = makeGlobalNode({ service: Service, layer, deps: [Database.node] }) diff --git a/packages/app-bundle/overlay/packages/core/src/permission/sql.ts b/packages/app-bundle/overlay/packages/core/src/permission/sql.ts new file mode 100644 index 00000000..9dddfb40 --- /dev/null +++ b/packages/app-bundle/overlay/packages/core/src/permission/sql.ts @@ -0,0 +1,43 @@ +import { sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core" +import { Timestamps } from "../database/schema.sql" +import { ProjectV2 } from "../project" +import { ProjectTable } from "../project/sql" +import type { PermissionSaved } from "./saved" + +export const PermissionTable = sqliteTable( + "permission", + { + id: text().$type().primaryKey(), + project_id: text() + .$type() + .notNull() + .references(() => ProjectTable.id, { onDelete: "cascade" }), + action: text().notNull(), + resource: text().notNull(), + ...Timestamps, + }, + (table) => [uniqueIndex("permission_project_action_resource_idx").on(table.project_id, table.action, table.resource)], +) + +export const ProviderPermissionTable = sqliteTable( + "provider_permission", + { + id: text().$type().primaryKey(), + project_id: text() + .$type() + .notNull() + .references(() => ProjectTable.id, { onDelete: "cascade" }), + tier_id: text().notNull(), + action: text().notNull(), + resource: text().notNull(), + ...Timestamps, + }, + (table) => [ + uniqueIndex("provider_permission_project_tier_action_resource_idx").on( + table.project_id, + table.tier_id, + table.action, + table.resource, + ), + ], +) diff --git a/packages/app-bundle/overlay/packages/core/src/provider-permission.ts b/packages/app-bundle/overlay/packages/core/src/provider-permission.ts new file mode 100644 index 00000000..1bc31ae8 --- /dev/null +++ b/packages/app-bundle/overlay/packages/core/src/provider-permission.ts @@ -0,0 +1,251 @@ +// @ts-nocheck — pre-existing FileAttachment mime mismatch (main is broken, suppress for registry PR) +export * as ProviderPermissionService from "./provider-permission" + +import { Context, Effect, Layer } from "effect" +import { ProviderPermission } from "@opencode-ai/schema/provider-permission" +import { Config } from "./config" +import { makeLocationNode } from "./effect/app-node" + +export type EffectResult = ProviderPermission.Effect + +// Re-export helpers +export const tierSummary = ProviderPermission.tierSummary +export const actionToGroup = ProviderPermission.actionToGroup +export const mostSpecificMatch = ProviderPermission.mostSpecificMatch +export const resolveEffect = ProviderPermission.resolveEffect +export const DEFAULT_TIERS = ProviderPermission.DEFAULT_TIERS +export const DEFAULT_CONFIG = ProviderPermission.DEFAULT_CONFIG + +export interface Interface { + readonly config: () => Effect.Effect + readonly resolve: (modelId: string, action: string, resource: string) => Effect.Effect + readonly tierForModel: (modelId: string) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/ProviderPermission") {} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const configService = yield* Config.Service + + const getConfig = Effect.fn("ProviderPermission.config")(function* () { + const entries = yield* configService.entries() + const raw = Config.latest(entries, "providerPermissions") as unknown as ProviderPermission.Config | undefined + if (!raw || !Array.isArray((raw as unknown as { tiers: unknown[] }).tiers)) return ProviderPermission.DEFAULT_CONFIG + return raw as ProviderPermission.Config + }) + + const resolve = Effect.fn("ProviderPermission.resolve")(function* ( + modelId: string, + action: string, + resource: string, + ) { + const cfg = yield* getConfig() + const effect = ProviderPermission.resolveEffect(cfg, modelId, action, resource) + return effect ?? ("ask" as const) + }) + + const tierForModel = Effect.fn("ProviderPermission.tierForModel")(function* (modelId: string) { + const cfg = yield* getConfig() + const tierId = cfg.assignments[modelId] ?? cfg.defaultTier + const tier: ProviderPermission.TrustTier | undefined = + cfg.tiers.find((t: ProviderPermission.TrustTier) => t.id === tierId) ?? + cfg.tiers.find((t: ProviderPermission.TrustTier) => t.id === cfg.defaultTier) + if (!tier) return cfg.tiers.find((t: ProviderPermission.TrustTier) => t.id === "unassigned")! + return tier + }) + + return Service.of({ + config: getConfig, + resolve, + tierForModel, + }) + }), +) + +export const locationLayer = layer.pipe(Layer.provideMerge(Config.locationLayer)) + +export const node = makeLocationNode({ + service: Service, + layer, + deps: [Config.node], +}) + +// ---- Source-path registry (in-memory, per-process) ---- +// Maps `${sessionID}:${callID}` → sourcePath for history redaction. +// The registry is populated at tool execution time (source-path tagging) and +// consulted at send-time. It adds zero cost when no model switch occurs (map lookup only). +const sourcePathMap = new Map() + +export function registerSourcePath(sessionID: string, callID: string, sourcePath: string): void { + if (!sourcePath) return + sourcePathMap.set(`${sessionID}:${callID}`, sourcePath) +} + +export function getSourcePath(sessionID: string, callID: string): string | undefined { + return sourcePathMap.get(`${sessionID}:${callID}`) +} + +export function clearSourcePathsForSession(sessionID: string): void { + for (const key of sourcePathMap.keys()) { + if (key.startsWith(`${sessionID}:`)) sourcePathMap.delete(key) + } +} + +// ---- Pure helpers for history redaction / context filtering ---- + +export function shouldRedactPath( + config: ProviderPermission.Config, + modelId: string, + sourcePath: string, +): boolean { + const effect = ProviderPermission.resolveEffect(config, modelId, "read", sourcePath) + return effect === "deny" +} + +export function redactHistoryMessage( + content: string, + sourcePath: string, + tierLabel: string, +): string { + return `[Content from ${sourcePath} filtered — trust tier "${tierLabel}" does not have read access]` +} + +// Source-path tagging: attach metadata to tool results +export type TaggedToolResult = { + content: string + sourcePath?: string + metadata?: Record +} + +export function tagToolResult(content: string, sourcePath?: string): TaggedToolResult { + return sourcePath ? { content, sourcePath, metadata: { sourcePath } } : { content } +} + +export type MessageWithSource = { + id: string + role: string + content: string + metadata?: Record & { sourcePath?: string } + sourcePath?: string +} + +export function resolveTierLabel(config: ProviderPermission.Config, modelId: string): string { + const tierId = config.assignments[modelId] ?? config.defaultTier + const tier: ProviderPermission.TrustTier | undefined = + config.tiers.find((t: ProviderPermission.TrustTier) => t.id === tierId) ?? + config.tiers.find((t: ProviderPermission.TrustTier) => t.id === config.defaultTier) + return tier?.label ?? tierId +} + +export function isDeniedForModel( + config: ProviderPermission.Config, + modelId: string, + sourcePath: string, +): boolean { + const effect = ProviderPermission.resolveEffect(config, modelId, "read", sourcePath) + return effect === "deny" +} + +// History redaction: filter at send-time only, does NOT mutate stored history +export function redactHistory( + messages: readonly MessageWithSource[], + config: ProviderPermission.Config, + activeModelId: string, +): MessageWithSource[] { + const tierLabel = resolveTierLabel(config, activeModelId) + return messages.map((msg) => { + const sourcePath = (msg.metadata?.sourcePath as string | undefined) ?? msg.sourcePath + if (!sourcePath) return msg + if (!isDeniedForModel(config, activeModelId, sourcePath)) return msg + return { + ...msg, + content: `[Content from ${sourcePath} filtered — trust tier "${tierLabel}" does not have read access]`, + metadata: { ...msg.metadata, redacted: true, originalSourcePath: sourcePath }, + } + }) +} + +// Context filtering: suppress denied auto-context (instructions, system prompt sources) +export function filterContextFiles( + files: readonly { path: string; content: string }[], + config: ProviderPermission.Config, + activeModelId: string, +): readonly { path: string; content: string }[] { + return files.filter((file) => !isDeniedForModel(config, activeModelId, file.path)) +} + +// Source-path tagging helper: annotate tool results +export function tagResult(content: string, sourcePath?: string): { content: string; metadata?: Record } { + if (!sourcePath) return { content } + return { content, metadata: { sourcePath } } +} + +// ---- SessionMessage-level redaction (history + context) ---- + +export function filterSystemBaseline( + baseline: string, + config: ProviderPermission.Config, + activeModelId: string, +): string { + if (!baseline.includes("Instructions from:")) return baseline + const parts = baseline.split(/(?=Instructions from:)/g) + const filtered = parts.filter((part) => { + const match = part.match(/Instructions from:\s*([^\n]+)/) + if (!match) return true + const p = match[1].trim() + return !isDeniedForModel(config, activeModelId, p) + }) + return filtered.join("").trim() +} + +export function redactSessionMessages( + messages: readonly import("@opencode-ai/schema/session-message").SessionMessage.Message[], + config: ProviderPermission.Config, + activeModelId: string, + sessionID: string, +): readonly import("@opencode-ai/schema/session-message").SessionMessage.Message[] { + const tierLabel = resolveTierLabel(config, activeModelId) + return messages.map((msg) => { + if (msg.type === "user" && (msg as unknown as { files?: unknown[] }).files && ((msg as unknown as { files: unknown[] }).files.length > 0)) { + const files = (msg as unknown as { files: { uri: string; mime: string; name?: string }[] }).files + const kept = files.filter((f) => { + const p = (f as unknown as { uri: string }).uri ?? (f as unknown as { name: string }).name ?? "" + if (!p) return true + return !isDeniedForModel(config, activeModelId, p) + }) + if (kept.length !== files.length) { + return { ...msg, files: kept } as typeof msg + } + return msg + } + if (msg.type === "assistant") { + let changed = false + const newContent = msg.content.map((item) => { + if (item.type !== "tool") return item + let sourcePath = getSourcePath(sessionID, item.id) + if (!sourcePath) { + const input = (item.state as unknown as { input?: Record }).input + if (input && typeof input.path === "string") sourcePath = input.path + else if (input && typeof input.pattern === "string") sourcePath = input.pattern + else if (input && typeof input.url === "string") sourcePath = input.url + else if (input && typeof input.query === "string") sourcePath = input.query + } + const outputPaths = (item.state as unknown as { outputPaths?: string[] }).outputPaths + const deniedPath = sourcePath && isDeniedForModel(config, activeModelId, sourcePath) + ? sourcePath + : outputPaths?.find((p: string) => isDeniedForModel(config, activeModelId, p)) + if (!deniedPath) return item + const placeholder = `[Content from ${deniedPath} filtered — trust tier "${tierLabel}" does not have read access]` + const state = item.state as Record + const content = (state.content as unknown[]) ?? [] + const redactedContent = [{ type: "text", text: placeholder }] as unknown as typeof content + changed = true + return { ...item, state: { ...state, content: redactedContent } as unknown as typeof item.state } + }) + if (changed) return { ...msg, content: newContent } as typeof msg + } + return msg + }) +} diff --git a/packages/app-bundle/overlay/packages/core/src/session.ts b/packages/app-bundle/overlay/packages/core/src/session.ts new file mode 100644 index 00000000..5c0baa36 --- /dev/null +++ b/packages/app-bundle/overlay/packages/core/src/session.ts @@ -0,0 +1,528 @@ +export * as SessionV2 from "./session" +export * from "./session/schema" + +import { DateTime, Effect, Layer, Schema, Context, Stream } from "effect" +import { ListAnchor } from "@opencode-ai/schema/session" +import { and, asc, desc, eq, gt, like, lt, or, type SQL } from "drizzle-orm" +import { ProjectV2 } from "./project" +import { WorkspaceV2 } from "./workspace" +import { ModelV2 } from "./model" +import { Location } from "./location" +import { SessionMessage } from "./session/message" +import { Prompt } from "./session/prompt" +import { PromptInput } from "@opencode-ai/schema/prompt-input" +import { EventV2 } from "./event" +import { Database } from "./database/database" +import { SessionProjector } from "./session/projector" +import { SessionMessageTable, SessionTable } from "./session/sql" +import { SessionSchema } from "./session/schema" +import { AbsolutePath, PositiveInt, RelativePath } from "./schema" +import { AgentV2 } from "./agent" +import { SessionV1 } from "./v1/session" +import { InstallationVersion } from "./installation/version" +import { Slug } from "./util/slug" +import { ProjectTable } from "./project/sql" +import path from "path" +import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises" +import { fromRow } from "./session/info" +import { SessionRunner } from "./session/runner/index" +import { SessionStore } from "./session/store" +import { SessionExecution } from "./session/execution" +import { makeGlobalNode } from "./effect/app-node" +import { LocationServiceMap } from "./location-service-map" +import { MessageDecodeError } from "./session/error" +import { SessionEvent } from "./session/event" +import { SessionInput } from "./session/input" +import { Snapshot } from "./snapshot" +import { SessionRevert } from "./session/revert" +import { Revert } from "@opencode-ai/schema/revert" +import { FSUtil } from "./fs-util" +import { Global } from "./global" +import { SessionDurable } from "@opencode-ai/schema/durable-event-manifest" + +export const RevertState = Revert.State +export type RevertState = Revert.State + +// get project -> project.locations +// +// get all sessions +// + +// - by project +// - by subpath +// - by workspace (home is special) + +export { ListAnchor } + +const ListInputBase = { + workspaceID: WorkspaceV2.ID.pipe(Schema.optional), + search: Schema.String.pipe(Schema.optional), + limit: PositiveInt.pipe(Schema.optional), + order: Schema.Literals(["asc", "desc"]).pipe(Schema.optional), + anchor: ListAnchor.pipe(Schema.optional), +} + +const ListDirectoryInput = Schema.Struct({ + ...ListInputBase, + directory: AbsolutePath, +}) + +const ListProjectInput = Schema.Struct({ + ...ListInputBase, + project: ProjectV2.ID, + subpath: RelativePath.pipe(Schema.optional), +}) + +const ListAllInput = Schema.Struct(ListInputBase) + +export const ListInput = Schema.Union([ListDirectoryInput, ListProjectInput, ListAllInput]) +export type ListInput = typeof ListInput.Type + +type CreateInput = { + id?: SessionSchema.ID + agent?: AgentV2.ID + model?: ModelV2.Ref + location: Location.Ref +} + +type CompactInput = { + sessionID: SessionSchema.ID + prompt?: Prompt +} + +export class NotFoundError extends Schema.TaggedErrorClass()("Session.NotFoundError", { + sessionID: SessionSchema.ID, +}) {} + +export class OperationUnavailableError extends Schema.TaggedErrorClass()( + "Session.OperationUnavailableError", + { + operation: Schema.Literals(["move", "shell", "skill", "switchAgent", "compact", "wait"]), + }, +) {} + +export { ContextSnapshotDecodeError, MessageDecodeError } from "./session/error" + +export class PromptConflictError extends Schema.TaggedErrorClass()("Session.PromptConflictError", { + sessionID: SessionSchema.ID, + messageID: SessionMessage.ID, +}) {} +export const MessageNotFoundError = SessionRevert.MessageNotFoundError +export type MessageNotFoundError = SessionRevert.MessageNotFoundError + +export type Error = NotFoundError | MessageDecodeError | OperationUnavailableError | PromptConflictError + +export interface Interface { + readonly list: (input?: ListInput) => Effect.Effect + readonly create: (input: CreateInput) => Effect.Effect + readonly get: (sessionID: SessionSchema.ID) => Effect.Effect + readonly messages: (input: { + sessionID: SessionSchema.ID + limit?: number + order?: "asc" | "desc" + cursor?: { + id: SessionMessage.ID + direction: "previous" | "next" + } + }) => Effect.Effect + readonly message: (input: { + sessionID: SessionSchema.ID + messageID: SessionMessage.ID + }) => Effect.Effect + readonly context: ( + sessionID: SessionSchema.ID, + ) => Effect.Effect + readonly events: (input: { + sessionID: SessionSchema.ID + after?: number + }) => Stream.Stream + readonly history: (input: { + sessionID: SessionSchema.ID + after?: number + limit: number + }) => Effect.Effect<{ events: ReadonlyArray; hasMore: boolean }, NotFoundError> + readonly switchAgent: (input: { sessionID: SessionSchema.ID; agent: string }) => Effect.Effect + readonly switchModel: (input: { + sessionID: SessionSchema.ID + model: ModelV2.Ref + }) => Effect.Effect + readonly prompt: (input: { + id?: SessionMessage.ID + sessionID: SessionSchema.ID + prompt: PromptInput.Prompt + delivery?: SessionInput.Delivery + resume?: boolean + }) => Effect.Effect + readonly shell: (input: { + id?: EventV2.ID + sessionID: SessionSchema.ID + command: string + resume?: boolean + }) => Effect.Effect + readonly skill: (input: { + id?: EventV2.ID + sessionID: SessionSchema.ID + skill: string + resume?: boolean + }) => Effect.Effect + readonly compact: (input: CompactInput) => Effect.Effect + readonly wait: (id: SessionSchema.ID) => Effect.Effect + readonly active: Effect.Effect> + readonly resume: (sessionID: SessionSchema.ID) => Effect.Effect + readonly interrupt: (sessionID: SessionSchema.ID) => Effect.Effect + readonly revert: { + readonly stage: (input: { + sessionID: SessionSchema.ID + messageID: SessionMessage.ID + files?: boolean + }) => Effect.Effect + readonly clear: (sessionID: SessionSchema.ID) => Effect.Effect + readonly commit: (sessionID: SessionSchema.ID) => Effect.Effect + } +} + +export class Service extends Context.Service()("@opencode/v2/Session") {} + +/** + * Persist the selected model to ~/.local/state/opencode/model.json so it + * survives server restarts. Mirrors the TUI's persistence in local.tsx. + * Reads the existing file, prepends the new model to `recent` (deduped, max 10), + * preserves `favorite` and `variant`, then atomic-writes back. + */ +const persistRecentModel = (providerID: string, modelID: string) => + Effect.promise(async () => { + const filePath = path.join(Global.Path.state, "model.json") + let existing: { recent?: unknown[]; favorite?: unknown[]; variant?: unknown } = {} + try { + existing = JSON.parse(await readFile(filePath, "utf8")) + } catch {} + const prev: { providerID: string; modelID: string }[] = Array.isArray(existing.recent) + ? existing.recent.filter( + (x): x is { providerID: string; modelID: string } => + !!x && typeof x === "object" && typeof (x as any).providerID === "string" && typeof (x as any).modelID === "string", + ) + : [] + const seen = new Set() + const next = [{ providerID, modelID }, ...prev] + .filter((item) => { + const key = `${item.providerID}/${item.modelID}` + if (seen.has(key)) return false + seen.add(key) + return true + }) + .slice(0, 10) + const data = { recent: next, favorite: existing.favorite ?? [], variant: existing.variant ?? {} } + await mkdir(path.dirname(filePath), { recursive: true }) + const tmp = `${filePath}.${process.pid}.tmp` + await writeFile(tmp, JSON.stringify(data)) + await rename(tmp, filePath).catch(async () => { + await rm(tmp, { force: true }).catch(() => {}) + }) + }) + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const database = yield* Database.Service + const db = database.db + const events = yield* EventV2.Service + const projects = yield* ProjectV2.Service + const execution = yield* SessionExecution.Service + const store = yield* SessionStore.Service + const locations = yield* LocationServiceMap.Service + const decodeMessage = Schema.decodeUnknownEffect(SessionMessage.Message) + const isDurableSessionEvent = Schema.is(SessionEvent.Durable) + const decode = (row: typeof SessionMessageTable.$inferSelect) => + decodeMessage({ ...row.data, id: row.id, type: row.type }).pipe( + Effect.mapError( + () => + new MessageDecodeError({ + sessionID: SessionSchema.ID.make(row.session_id), + messageID: SessionMessage.ID.make(row.id), + }), + ), + ) + + const result = Service.of({ + create: Effect.fn("V2Session.create")(function* (input) { + const sessionID = input.id ?? SessionSchema.ID.create() + const recorded = yield* store.get(sessionID) + if (recorded) return recorded + const project = yield* projects.resolve(input.location.directory) + yield* db + .insert(ProjectTable) + .values({ id: project.id, worktree: project.directory, vcs: project.vcs?.type, sandboxes: [] }) + .onConflictDoNothing() + .run() + .pipe(Effect.orDie) + const now = Date.now() + const info = SessionV1.SessionInfo.make({ + id: sessionID, + slug: Slug.create(), + version: InstallationVersion, + projectID: project.id, + directory: input.location.directory, + path: path.relative(project.directory, input.location.directory).replaceAll("\\", "/"), + workspaceID: input.location.workspaceID ? WorkspaceV2.ID.make(input.location.workspaceID) : undefined, + title: `New session - ${new Date(now).toISOString()}`, + agent: input.agent, + model: input.model + ? { + id: ModelV2.ID.make(input.model.id), + providerID: input.model.providerID, + variant: input.model.variant, + } + : undefined, + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { created: now, updated: now }, + }) + const projected = yield* events + .publish(SessionV1.Event.Created, { sessionID, info }, { location: input.location }) + .pipe( + Effect.as({ type: "created" } as const), + Effect.catchDefect((defect) => { + if (!(defect instanceof SessionProjector.SessionAlreadyProjected)) { + return Effect.die(defect) + } + // Concurrent creation lost the projection race. The existing Session identity wins. + return store + .get(sessionID) + .pipe( + Effect.flatMap((session) => + session ? Effect.succeed({ type: "existing", session } as const) : Effect.die(defect), + ), + ) + }), + ) + if (projected.type === "existing") return projected.session + // TODO: Restore recorded sessions onto replacement synchronized workspaces in a future API slice. + return yield* result.get(sessionID).pipe(Effect.orDie) + }), + get: Effect.fn("V2Session.get")(function* (sessionID) { + const session = yield* store.get(sessionID) + if (!session) return yield* new NotFoundError({ sessionID }) + return session + }), + list: Effect.fn("V2Session.list")(function* (input = {}) { + const direction = input.anchor?.direction ?? "next" + const requestedOrder = input.order ?? "desc" + const order = direction === "previous" ? (requestedOrder === "asc" ? "desc" : "asc") : requestedOrder + const sortColumn = SessionTable.time_created + const conditions: SQL[] = [] + if ("directory" in input) conditions.push(eq(SessionTable.directory, input.directory)) + if (input.workspaceID) conditions.push(eq(SessionTable.workspace_id, input.workspaceID)) + if ("project" in input) conditions.push(eq(SessionTable.project_id, input.project)) + if (input.search) conditions.push(like(SessionTable.title, `%${input.search}%`)) + if (input.anchor) { + conditions.push( + order === "asc" + ? or( + gt(sortColumn, input.anchor.time), + and(eq(sortColumn, input.anchor.time), gt(SessionTable.id, input.anchor.id)), + )! + : or( + lt(sortColumn, input.anchor.time), + and(eq(sortColumn, input.anchor.time), lt(SessionTable.id, input.anchor.id)), + )!, + ) + } + const query = db + .select() + .from(SessionTable) + .where(conditions.length > 0 ? and(...conditions) : undefined) + .orderBy( + order === "asc" ? asc(sortColumn) : desc(sortColumn), + order === "asc" ? asc(SessionTable.id) : desc(SessionTable.id), + ) + const rows = yield* (input.limit === undefined ? query.all() : query.limit(input.limit).all()).pipe( + Effect.orDie, + ) + return (direction === "previous" ? rows.toReversed() : rows).map((row) => fromRow(row)) + }), + messages: Effect.fn("V2Session.messages")(function* (input) { + yield* result.get(input.sessionID) + const direction = input.cursor?.direction ?? "next" + const requestedOrder = input.order ?? "desc" + const order = direction === "previous" ? (requestedOrder === "asc" ? "desc" : "asc") : requestedOrder + const anchor = input.cursor + ? yield* db + .select({ seq: SessionMessageTable.seq }) + .from(SessionMessageTable) + .where( + and(eq(SessionMessageTable.session_id, input.sessionID), eq(SessionMessageTable.id, input.cursor.id)), + ) + .get() + .pipe(Effect.orDie) + : undefined + if (input.cursor && !anchor) return [] + const boundary = anchor + ? order === "asc" + ? gt(SessionMessageTable.seq, anchor.seq) + : lt(SessionMessageTable.seq, anchor.seq) + : undefined + const where = boundary + ? and(eq(SessionMessageTable.session_id, input.sessionID), boundary) + : eq(SessionMessageTable.session_id, input.sessionID) + const query = db + .select() + .from(SessionMessageTable) + .where(where) + .orderBy(order === "asc" ? asc(SessionMessageTable.seq) : desc(SessionMessageTable.seq)) + const rows = yield* (input.limit === undefined ? query.all() : query.limit(input.limit).all()).pipe( + Effect.orDie, + ) + return yield* Effect.forEach(direction === "previous" ? rows.toReversed() : rows, decode) + }), + message: Effect.fn("V2Session.message")(function* (input) { + const stored = yield* store.message(input.messageID) + return stored?.sessionID === input.sessionID ? stored.message : undefined + }), + context: Effect.fn("V2Session.context")(function* (sessionID) { + yield* result.get(sessionID) + return yield* store.context(sessionID) + }), + events: (input) => + Stream.unwrap( + result + .get(input.sessionID) + .pipe(Effect.as(events.durable({ aggregateID: input.sessionID, after: input.after }))), + ).pipe(Stream.filter((event): event is SessionEvent.DurableEvent => isDurableSessionEvent(event))), + history: Effect.fn("V2Session.history")(function* (input) { + yield* result.get(input.sessionID) + return yield* EventV2.readAggregate(db, { + ...input, + aggregateID: input.sessionID, + manifest: SessionDurable, + }) + }), + prompt: Effect.fn("V2Session.prompt")((input) => + Effect.uninterruptible( + Effect.gen(function* () { + yield* result.get(input.sessionID) + const prompt = resolvePrompt(input.prompt) + const messageID = input.id ?? SessionMessage.ID.create() + const delivery = input.delivery ?? "steer" + const expected = { sessionID: input.sessionID, messageID, prompt, delivery } + const admitted = yield* SessionInput.admit(db, events, { + id: messageID, + sessionID: input.sessionID, + prompt, + delivery, + }).pipe( + Effect.catchDefect((defect) => + defect instanceof SessionInput.LifecycleConflict + ? new PromptConflictError({ sessionID: input.sessionID, messageID }) + : Effect.die(defect), + ), + ) + if (!SessionInput.equivalent(admitted, expected)) + return yield* new PromptConflictError({ sessionID: input.sessionID, messageID }) + if (input.resume !== false) yield* execution.wake(admitted.sessionID) + return admitted + }), + ), + ), + shell: Effect.fn("V2Session.shell")(function* () { + return yield* new OperationUnavailableError({ operation: "shell" }) + }), + skill: Effect.fn("V2Session.skill")(function* () { + return yield* new OperationUnavailableError({ operation: "skill" }) + }), + switchAgent: Effect.fn("V2Session.switchAgent")(function* (input) { + yield* result.get(input.sessionID) + yield* events.publish(SessionEvent.AgentSwitched, { + sessionID: input.sessionID, + messageID: SessionMessage.ID.create(), + timestamp: yield* DateTime.now, + agent: input.agent, + }) + }), + switchModel: Effect.fn("V2Session.switchModel")(function* (input) { + const session = yield* result.get(input.sessionID) + if ( + session.model?.providerID === input.model.providerID && + session.model.id === input.model.id && + (session.model.variant ?? "default") === (input.model.variant ?? "default") + ) + return + yield* events.publish(SessionEvent.ModelSwitched, { + sessionID: input.sessionID, + messageID: SessionMessage.ID.create(), + timestamp: yield* DateTime.now, + model: input.model, + }) + // Persist the model selection to model.json so it survives server restarts. + // Fire-and-forget: a failure to write state must never break model switching. + yield* persistRecentModel(input.model.providerID, input.model.id).pipe(Effect.ignore) + }), + compact: Effect.fn("V2Session.compact")(function* (input) { + yield* result.get(input.sessionID) + return yield* new OperationUnavailableError({ operation: "compact" }) + }), + wait: Effect.fn("V2Session.wait")(function* (sessionID) { + yield* result.get(sessionID) + return yield* new OperationUnavailableError({ operation: "wait" }) + }), + active: execution.active, + resume: Effect.fn("V2Session.resume")(function* (sessionID) { + yield* result.get(sessionID) + yield* execution.resume(sessionID) + }), + interrupt: Effect.fn("V2Session.interrupt")((sessionID) => + Effect.uninterruptible(execution.interrupt(sessionID)), + ), + revert: { + stage: Effect.fn("V2Session.revert.stage")(function* (input) { + const session = yield* result.get(input.sessionID) + return yield* SessionRevert.stage({ session, messageID: input.messageID, files: input.files }).pipe( + Effect.provideService(Database.Service, database), + Effect.provideService(EventV2.Service, events), + Effect.provide(locations.get(session.location)), + ) + }), + clear: Effect.fn("V2Session.revert.clear")(function* (sessionID) { + const session = yield* result.get(sessionID) + yield* SessionRevert.clear(session).pipe( + Effect.provideService(EventV2.Service, events), + Effect.provide(locations.get(session.location)), + ) + }), + commit: Effect.fn("V2Session.revert.commit")(function* (sessionID) { + const session = yield* result.get(sessionID) + yield* SessionRevert.commit(session).pipe(Effect.provideService(EventV2.Service, events)) + }), + }, + }) + + return result + }), +) + +const resolvePrompt = (input: PromptInput.Prompt) => + Prompt.make({ + text: input.text, + agents: input.agents, + files: input.files?.map((file) => { + const dataMime = file.uri.match(/^data:([^;,]+)[;,]/i)?.[1] + const target = URL.canParse(file.uri) ? new URL(file.uri).pathname : (file.name ?? file.uri) + return { + ...file, + mime: dataMime ?? (target.endsWith("/") ? "application/x-directory" : FSUtil.mimeType(target)), + } + }), + }) + +export const node = makeGlobalNode({ + service: Service, + layer: layer.pipe(Layer.orDie), + deps: [ + Database.node, + EventV2.node, + ProjectV2.node, + SessionExecution.node, + SessionStore.node, + LocationServiceMap.node, + SessionProjector.node, + ], +}) diff --git a/packages/app-bundle/overlay/packages/core/src/session/runner/llm.ts b/packages/app-bundle/overlay/packages/core/src/session/runner/llm.ts new file mode 100644 index 00000000..1926e99a --- /dev/null +++ b/packages/app-bundle/overlay/packages/core/src/session/runner/llm.ts @@ -0,0 +1,448 @@ +import { + LLM, + LLMClient, + LLMError, + LLMEvent, + Message, + SystemPart, + isContextOverflowFailure, + type ProviderErrorEvent, +} from "@opencode-ai/llm" +import { Cause, DateTime, Effect, FiberSet, Layer, Option, Semaphore, Stream } from "effect" +import { AgentV2 } from "../../agent" +import { Config } from "../../config" +import { Database } from "../../database/database" +import { EventV2 } from "../../event" +import { Location } from "../../location" +import { ModelV2 } from "../../model" +import { PermissionV2 } from "../../permission" +import { ProviderV2 } from "../../provider" +import { QuestionV2 } from "../../question" +import { SystemContext } from "../../system-context/index" +import { SystemContextRegistry } from "../../system-context/registry" +import { SkillGuidance } from "../../skill/guidance" +import { ReferenceGuidance } from "../../reference/guidance" +import { ToolRegistry } from "../../tool/registry" +import { ToolOutputStore } from "../../tool-output-store" +import { SessionContextEpoch } from "../context-epoch" +import { SessionCompaction } from "../compaction" +import { SessionEvent } from "../event" +import { SessionHistory } from "../history" +import { SessionInput } from "../input" +import { SessionSchema } from "../schema" +import { SessionStore } from "../store" +import { type RunError, Service } from "./index" +import { SessionRunnerModel } from "./model" +import { createLLMEventPublisher } from "./publish-llm-event" +import { toLLMMessages } from "./to-llm-message" +import { MAX_STEPS_PROMPT } from "./max-steps" +import { Snapshot } from "../../snapshot" +import { makeLocationNode } from "../../effect/app-node" +import { llmClient } from "../../effect/app-node-platform" +import { ProviderPermission } from "@opencode-ai/schema/provider-permission" +import { filterSystemBaseline, redactSessionMessages } from "../../provider-permission" + +/** + * Runs one durable coding-agent Session until it settles. + * + * Keep this as orchestration over smaller collaborators rather than rebuilding the legacy + * `SessionPrompt` monolith. Implement the unchecked items in small reviewed slices: + * + * - Session ownership and controls + * - [x] Coordinate one local active drain per Session; explicit resumes join and prompt wakeups coalesce. + * - [ ] Replace local ownership with durable multi-node ownership when clustered. + * - [ ] Mark busy, retrying, idle, interrupted, or terminal-failure status durably. + * - [ ] Honor interruption and reject stale work after runtime attachment replacement. + * - [x] Honor optional agent step limits. + * - [ ] Bound provider retries and repeated identical tool calls. + * + * - Runtime context assembly + * - Track V1 runtime-context parity canonically in `specs/v2/session.md`. + * + * - One provider turn + * - [x] Translate every projected V2 Session message variant into canonical + * `@opencode-ai/llm` messages. + * - [ ] Resolve policy-filtered built-in, MCP, plugin, and structured-output tool definitions. + * - [x] Stream exactly one `llm.stream(request)` provider turn. + * - [x] Persist assistant text and usage events incrementally as they arrive. + * - [ ] Persist snapshots, patches, and retry notices incrementally as they arrive. + * - [x] Persist reasoning, provider errors, and tool-call events incrementally as they arrive. + * + * - Tool settlement and continuation + * - [x] Durably record each tool call before side effects begin. + * - [x] Authorize and execute recorded local calls through a core-owned registry hook. + * - [x] Persist typed success, failure, and provider-executed tool outcomes. + * - [x] Start each recorded local call eagerly and await all settlements before continuation. + * - [ ] Add scoped runtime context, progress updates, attachment normalization, + * plugins, and cancellation settlement. + * - [x] Reload projected history and start the next explicit provider turn after local tool results. + * - [x] Continue for durable user steering accepted during an active provider turn. + * - [ ] Continue for compaction or another continuation condition when required. + * + * - Post-run maintenance + * - [ ] Settle final status and expose durable output events to replayable consumers. + * - [ ] Coalesce streamed deltas and add covering projected-history indexes. + * - [ ] Update title, summaries, compaction state, and cleanup in bounded background work. + * + * Use `llm.stream(request)` for each provider turn. Keep tool execution and continuation here. + * Durable continuation recovery remains a separate future slice with an explicit retry policy. + * + * The current slice loads V2 history, translates it, resolves a model through a core service, and persists one + * provider turn. Registry definitions are advertised, local tool calls are settled durably, and an + * explicit loop starts the next provider turn after local settlement. Configured agent step limits bound the loop. + */ + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const events = yield* EventV2.Service + const llm = yield* LLMClient.Service + const agents = yield* AgentV2.Service + const tools = yield* ToolRegistry.Service + const models = yield* SessionRunnerModel.Service + const store = yield* SessionStore.Service + const location = yield* Location.Service + const systemContext = yield* SystemContextRegistry.Service + const skillGuidance = yield* SkillGuidance.Service + const referenceGuidance = yield* ReferenceGuidance.Service + const config = yield* Config.Service + const snapshots = yield* Snapshot.Service + const db = (yield* Database.Service).db + const compaction = SessionCompaction.make({ events, llm, config: yield* config.entries() }) + const getSession = Effect.fn("SessionRunner.getSession")(function* (sessionID: SessionSchema.ID) { + const session = yield* store.get(sessionID) + if (!session) return yield* Effect.die(`Session not found: ${sessionID}`) + return session + }) + + const getContext = Effect.fn("SessionRunner.getContext")(function* (sessionID: SessionSchema.ID) { + return yield* store.context(sessionID) + }) + const failInterruptedTools = Effect.fn("SessionRunner.failInterruptedTools")(function* ( + sessionID: SessionSchema.ID, + ) { + for (const message of yield* getContext(sessionID)) { + if (message.type !== "assistant") continue + for (const tool of message.content) { + if (tool.type !== "tool" || (tool.state.status !== "pending" && tool.state.status !== "running")) continue + yield* events.publish(SessionEvent.Tool.Failed, { + sessionID, + timestamp: yield* DateTime.now, + assistantMessageID: message.id, + callID: tool.id, + error: { type: "unknown", message: "Tool execution interrupted" }, + provider: { + executed: tool.provider?.executed === true, + ...(tool.provider?.metadata === undefined ? {} : { metadata: tool.provider.metadata }), + }, + }) + } + } + }) + + const awaitToolFibers = (fibers: FiberSet.FiberSet) => + Effect.raceFirst(FiberSet.join(fibers), FiberSet.awaitEmpty(fibers)) + + // Match V1: declining a user prompt halts the loop instead of becoming model-facing tool output. + const isUserDeclined = (cause: Cause.Cause) => + cause.reasons.some( + (reason) => + Cause.isDieReason(reason) && + (reason.defect instanceof PermissionV2.DeclinedError || reason.defect instanceof QuestionV2.RejectedError), + ) + + type TurnTransition = + // Automatic compaction completed; rebuild the request from compacted history. + | { readonly _tag: "ContinueAfterCompaction"; readonly step: number } + // Overflow compaction completed; rebuild once through the path without overflow recovery. + | { readonly _tag: "ContinueAfterOverflowCompaction"; readonly step: number } + + class TurnTransitionError extends Error { + constructor(readonly transition: TurnTransition) { + super() + } + } + + const continueAfterCompaction = (step: number) => new TurnTransitionError({ _tag: "ContinueAfterCompaction", step }) + const continueAfterOverflowCompaction = (step: number) => + new TurnTransitionError({ _tag: "ContinueAfterOverflowCompaction", step }) + + const loadSystemContext = (agent: AgentV2.Selection) => + Effect.all([systemContext.load(), skillGuidance.load(agent), referenceGuidance.load()], { + concurrency: "unbounded", + }).pipe(Effect.map(SystemContext.combine)) + + const runTurnAttempt = Effect.fn("SessionRunner.runTurn")(function* ( + sessionID: SessionSchema.ID, + promotion: SessionInput.Delivery | undefined, + step: number, + recoverOverflow?: typeof compaction.compactAfterOverflow, + ) { + const session = yield* getSession(sessionID) + if (session.location.directory !== location.directory || session.location.workspaceID !== location.workspaceID) + return yield* Effect.interrupt + const agent = yield* agents.select(session.agent) + const initialized = yield* SessionContextEpoch.initialize(db, loadSystemContext(agent), session.id) + const toolFibers = yield* FiberSet.make() + let needsContinuation = false + let currentStep = step + if (promotion) { + const cutoff = yield* EventV2.latestSequence(db, session.id) + let promoted = 0 + if (promotion === "steer") promoted = yield* SessionInput.promoteSteers(db, events, session.id, cutoff) + if (promotion === "queue") { + promoted += Number(yield* SessionInput.promoteNextQueued(db, events, session.id)) + promoted += yield* SessionInput.promoteSteers(db, events, session.id, cutoff) + } + if (promoted > 0) currentStep = 1 + } + const system = + initialized ?? (yield* SessionContextEpoch.prepare(db, events, loadSystemContext(agent), session.id)) + const model = yield* models.resolve(session) + // Provider-permission context filtering & history redaction (spec: strict privacy filtering including history redaction on model switch) + // Resolve provider tier config (global opencode.jsonc) — synchronous via Config latest + const cfgEntries = yield* config.entries() + const rawPP = Config.latest(cfgEntries, "providerPermissions") as unknown as ProviderPermission.Config | undefined + const ppConfig: ProviderPermission.Config = + rawPP && Array.isArray((rawPP as ProviderPermission.Config).tiers) + ? (rawPP as ProviderPermission.Config) + : ProviderPermission.DEFAULT_CONFIG + const activeModelId = `${model.provider}/${model.id}` + // System prompt filtering: drop instruction blocks from denied directories (active tier re-evaluates on model switch) + const filteredBaseline = filterSystemBaseline(system.baseline, ppConfig, activeModelId) + const filteredSystem = { ...system, baseline: filteredBaseline } + const entries = yield* SessionHistory.entriesForRunner(db, session.id, filteredSystem.baselineSeq) + const rawContext = entries.map((entry) => entry.message) + // History redaction at send-time only — never mutates stored history + const context = redactSessionMessages(rawContext, ppConfig, activeModelId, session.id) as typeof rawContext + const isLastStep = agent.info?.steps !== undefined && currentStep >= agent.info.steps + const toolMaterialization = isLastStep ? undefined : yield* tools.materialize(agent.info?.permissions) + const promptCacheKey = /^ses_[0-9a-f]{64}$/.test(session.id) ? session.id.slice(4) : session.id + const request = LLM.request({ + model, + providerOptions: { openai: { promptCacheKey } }, + system: [agent.info?.system, filteredSystem.baseline] + .filter((part): part is string => part !== undefined && part.length > 0) + .map(SystemPart.make), + messages: [...toLLMMessages(context, model), ...(isLastStep ? [Message.assistant(MAX_STEPS_PROMPT)] : [])], + tools: toolMaterialization?.definitions ?? [], + toolChoice: isLastStep ? "none" : undefined, + }) + if (yield* compaction.compactIfNeeded({ sessionID: session.id, entries, model, request })) + return yield* Effect.die(continueAfterCompaction(currentStep)) + const startSnapshot = yield* snapshots.capture() + const publisher = createLLMEventPublisher(events, { + sessionID: session.id, + agent: agent.id, + model: { + id: ModelV2.ID.make(model.id), + providerID: ProviderV2.ID.make(model.provider), + ...(session.model?.variant === undefined ? {} : { variant: session.model.variant }), + }, + snapshot: startSnapshot, + }) + const withPublication = Semaphore.makeUnsafe(1).withPermit + const publish = (event: LLMEvent, outputPaths: ReadonlyArray = []) => + withPublication(publisher.publish(event, outputPaths)) + let overflowFailure: ProviderErrorEvent | undefined + const providerStream = llm.stream(request).pipe( + Stream.runForEach((event) => + Effect.gen(function* () { + if (overflowFailure || publisher.hasProviderError()) return + if (LLMEvent.is.providerError(event)) { + if (isContextOverflowFailure(event) && !publisher.hasAssistantStarted()) { + overflowFailure = event + return + } + } + yield* publish(event) + if (event.type !== "tool-call" || event.providerExecuted) return + if (!toolMaterialization) { + yield* withPublication(publisher.failUnsettledTools("Tools are disabled after the maximum agent steps")) + return + } + needsContinuation = true + const assistantMessageID = yield* publisher.assistantMessageID(event.id) + yield* Effect.uninterruptibleMask((restore) => + restore( + toolMaterialization.settle({ + sessionID: session.id, + agent: agent.id, + assistantMessageID, + call: event, + }), + ).pipe( + Effect.flatMap((settlement) => + publish( + LLMEvent.toolResult({ + id: event.id, + name: event.name, + result: settlement.result, + output: settlement.output, + }), + settlement.outputPaths ?? [], + ), + ), + ), + ).pipe(FiberSet.run(toolFibers)) + }), + ), + Effect.ensuring(withPublication(publisher.flush())), + ) + + return yield* Effect.uninterruptibleMask((restore) => + Effect.gen(function* () { + const stream = yield* restore(providerStream).pipe(Effect.exit) + const failure = + stream._tag === "Failure" ? Option.getOrUndefined(Cause.findErrorOption(stream.cause)) : undefined + if ( + recoverOverflow && + !publisher.hasAssistantStarted() && + isContextOverflowFailure(overflowFailure ?? failure) && + (yield* restore(recoverOverflow({ sessionID: session.id, entries, model, request }))) + ) + return yield* Effect.die(continueAfterOverflowCompaction(currentStep)) + if (overflowFailure) yield* publish(overflowFailure) + const llmFailure = failure instanceof LLMError ? failure : undefined + if (llmFailure && !publisher.hasProviderError()) { + yield* withPublication(publisher.failUnsettledTools("Provider did not return a tool result", true)) + yield* withPublication(publisher.failAssistant(llmFailure.reason.message)) + } + if (stream._tag === "Failure" && Cause.hasInterrupts(stream.cause)) yield* FiberSet.clear(toolFibers) + const settled = yield* restore(awaitToolFibers(toolFibers)).pipe(Effect.exit) + if (settled._tag === "Failure" && isUserDeclined(settled.cause)) { + yield* FiberSet.clear(toolFibers) + yield* withPublication(publisher.failUnsettledTools("Tool execution interrupted")) + return yield* Effect.interrupt + } + if ( + (stream._tag === "Failure" && Cause.hasInterrupts(stream.cause)) || + (settled._tag === "Failure" && Cause.hasInterrupts(settled.cause)) + ) { + yield* FiberSet.clear(toolFibers) + yield* withPublication(publisher.failUnsettledTools("Tool execution interrupted")) + if (publisher.hasActiveAssistant()) + yield* withPublication(publisher.failAssistant("Provider turn interrupted")) + } + if (settled._tag === "Failure" && !Cause.hasInterrupts(settled.cause)) { + const failure = Cause.squash(settled.cause) + const message = failure instanceof Error ? failure.message : String(failure) + yield* withPublication(publisher.failUnsettledTools(`Tool execution failed: ${message}`)) + } + const stepSettlement = publisher.stepSettlement() + if (stepSettlement && !publisher.hasProviderError()) { + const endSnapshot = yield* snapshots.capture() + const files = + startSnapshot && endSnapshot + ? yield* snapshots + .files({ from: startSnapshot, to: endSnapshot }) + .pipe(Effect.catch(() => Effect.succeed(undefined))) + : undefined + yield* withPublication( + events.publish(SessionEvent.Step.Ended, { + sessionID: session.id, + timestamp: yield* DateTime.now, + assistantMessageID: yield* publisher.startAssistant(), + finish: stepSettlement.finish, + cost: 0, + tokens: stepSettlement.tokens, + snapshot: endSnapshot, + files, + }), + ) + } + if (publisher.hasProviderError()) + yield* withPublication(publisher.failUnsettledTools("Tool execution interrupted")) + if (stream._tag === "Success" && !publisher.hasProviderError()) + yield* withPublication(publisher.failUnsettledTools("Provider did not return a tool result", true)) + if (stream._tag === "Failure") return yield* Effect.failCause(stream.cause) + if (settled._tag === "Failure" && Cause.hasInterrupts(settled.cause)) + return yield* Effect.failCause(settled.cause) + return { needsContinuation: !publisher.hasProviderError() && needsContinuation, step: currentStep } + }), + ) + }, Effect.scoped) + type RunTurn = ( + sessionID: SessionSchema.ID, + promotion: SessionInput.Delivery | undefined, + step: number, + ) => Effect.Effect<{ readonly needsContinuation: boolean; readonly step: number }, RunError> + + const runAfterOverflowCompaction: RunTurn = Effect.fnUntraced(function* (sessionID, promotion, step) { + return yield* runTurnAttempt(sessionID, promotion, step).pipe( + Effect.catchDefect( + Effect.fnUntraced(function* (defect) { + if (!(defect instanceof TurnTransitionError)) return yield* Effect.die(defect) + if (defect.transition._tag === "ContinueAfterOverflowCompaction") + return yield* Effect.die("Post-compaction provider attempt cannot recover another overflow") + yield* Effect.yieldNow + return yield* runAfterOverflowCompaction(sessionID, undefined, defect.transition.step) + }), + ), + ) + }) + + const runTurn: RunTurn = Effect.fnUntraced(function* (sessionID, promotion, step) { + return yield* runTurnAttempt(sessionID, promotion, step, compaction.compactAfterOverflow).pipe( + Effect.catchDefect( + Effect.fnUntraced(function* (defect) { + if (!(defect instanceof TurnTransitionError)) return yield* Effect.die(defect) + yield* Effect.yieldNow + if (defect.transition._tag === "ContinueAfterOverflowCompaction") + return yield* runAfterOverflowCompaction(sessionID, undefined, defect.transition.step) + return yield* runTurn(sessionID, undefined, defect.transition.step) + }), + ), + ) + }) + + const run = Effect.fn("SessionRunner.run")(function* (input: { + readonly sessionID: SessionSchema.ID + readonly force: boolean + }) { + const hasSteer = yield* SessionInput.hasPending(db, input.sessionID, "steer") + const hasQueue = hasSteer ? false : yield* SessionInput.hasPending(db, input.sessionID, "queue") + if (!input.force && !hasSteer && !hasQueue) return + yield* failInterruptedTools(input.sessionID) + let promotion: SessionInput.Delivery | undefined = hasSteer ? "steer" : hasQueue ? "queue" : undefined + let shouldRun = input.force || hasSteer || hasQueue + while (shouldRun) { + let needsContinuation = true + let step = 1 + while (needsContinuation) { + const result = yield* runTurn(input.sessionID, promotion, step) + needsContinuation = result.needsContinuation + step = result.step + 1 + promotion = "steer" + if (!needsContinuation) needsContinuation = yield* SessionInput.hasPending(db, input.sessionID, "steer") + } + shouldRun = yield* SessionInput.hasPending(db, input.sessionID, "queue") + promotion = shouldRun ? "queue" : undefined + } + }) + + return Service.of({ + run, + }) + }), +) + +export const node = makeLocationNode({ + service: Service, + layer, + deps: [ + EventV2.node, + llmClient, + AgentV2.node, + ToolRegistry.node, + SessionRunnerModel.node, + SessionStore.node, + Location.node, + SystemContextRegistry.node, + SkillGuidance.node, + ReferenceGuidance.node, + Config.node, + Snapshot.node, + Database.node, + ], +}) diff --git a/packages/app-bundle/overlay/packages/core/src/tool/bash.ts b/packages/app-bundle/overlay/packages/core/src/tool/bash.ts new file mode 100644 index 00000000..278c0f4f --- /dev/null +++ b/packages/app-bundle/overlay/packages/core/src/tool/bash.ts @@ -0,0 +1,210 @@ +export * as BashTool from "./bash" + +import path from "path" +import { ToolFailure } from "@opencode-ai/llm" +import { Duration, Effect, Layer, Schema } from "effect" +import { ChildProcess } from "effect/unstable/process" +import { Config } from "../config" +import { makeLocationNode } from "../effect/app-node" +import { FSUtil } from "../fs-util" +import { LocationMutation } from "../location-mutation" +import { AppProcess } from "../process" +import { PermissionV2 } from "../permission" +import { PositiveInt } from "../schema" +import { ToolRegistry } from "./registry" +import { Tool } from "./tool" +import { Tools } from "./tools" +import { registerSourcePath } from "../provider-permission" + +export const name = "bash" +export const DEFAULT_TIMEOUT_MS = 2 * 60 * 1_000 +export const MAX_TIMEOUT_MS = 10 * 60 * 1_000 +export const MAX_CAPTURE_BYTES = 1024 * 1024 + +export const Input = Schema.Struct({ + command: Schema.String.annotate({ description: "Shell command string to execute" }), + workdir: Schema.String.pipe(Schema.optional).annotate({ + description: "Working directory. Defaults to the active Location; relative paths resolve from that Location.", + }), + timeout: PositiveInt.check(Schema.isLessThanOrEqualTo(MAX_TIMEOUT_MS)) + .pipe(Schema.optional) + .annotate({ + description: `Timeout in milliseconds. Defaults to ${DEFAULT_TIMEOUT_MS} and may not exceed ${MAX_TIMEOUT_MS}.`, + }), +}) + +const StructuredOutput = Schema.Struct({ + exit: Schema.Number.pipe(Schema.optional), + truncated: Schema.Boolean, + timeout: Schema.Boolean.pipe(Schema.optional), +}) + +const Output = Schema.Struct({ + ...StructuredOutput.fields, + output: Schema.String, + warnings: Schema.Array(Schema.String).pipe(Schema.optional), +}) + +type Output = typeof Output.Type + +const defaultShell = () => (process.platform === "win32" ? (process.env.COMSPEC ?? "cmd.exe") : "/bin/sh") + +const modelOutput = (output: Output) => { + const warnings = output.warnings?.length + ? `\n\nWarnings:\n${output.warnings.map((warning) => `- ${warning}`).join("\n")}` + : "" + if (output.timeout) return `${warnings.trimStart()}${warnings ? "\n\n" : ""}Command timed out before completion.` + return `${warnings.trimStart()}${warnings ? "\n\n" : ""}Command exited with code ${output.exit}.` +} + +const isTimeout = (error: AppProcess.AppProcessError) => + error.cause instanceof Error && error.cause.message === "Timed out" + +/** + * Minimal V2 core shell boundary. Keep parity debt visible without pulling the + * legacy shell runtime into core. + */ +// TODO: Port tree-sitter bash / PowerShell parser-based approval reduction. +// TODO: Port BashArity reusable command-prefix approvals. +// TODO: Replace token-based command-argument external-directory advisories with parser-based detection. +// TODO: Restore PowerShell and cmd-specific invocation/path handling on Windows. +// TODO: Add plugin shell.env environment augmentation once V2 plugin hooks exist. +// TODO: Add durable/live progress metadata streaming for long-running commands once V2 tool invocation progress context is wired. +// TODO: Persist background job status and define restart recovery before exposing remote observation. +// TODO: Re-add model-facing background launch only with owner-bound get/wait/cancel tools and completion delivery. +// TODO: Add HTTP background-job observation only after durable status, restart recovery, and authorization are defined. +// TODO: Revisit process-group cleanup and platform coverage with shell-specific tests if current AppProcess semantics do not fully cover it. +// TODO: Revisit binary output handling if stdout/stderr decoding is text-only. +// TODO: Stream full shell output into managed storage while retaining only a bounded in-memory preview. + +const shellTokens = (command: string) => command.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? [] +const unquote = (value: string) => value.replace(/^(['"])(.*)\1$/, "$2") +const externalCommandDirectories = Effect.fn("BashTool.externalCommandDirectories")(function* ( + fs: FSUtil.Interface, + command: string, + cwd: string, +) { + const directories = new Set() + for (const token of shellTokens(command)) { + const value = unquote(token).replace(/[;,|&]+$/, "") + if (!path.isAbsolute(value)) continue + const resolved = yield* fs.resolve(value) + if (FSUtil.contains(cwd, resolved)) continue + directories.add(yield* fs.resolve(path.dirname(resolved))) + } + return [...directories] +}) + +const layer = Layer.effectDiscard( + Effect.gen(function* () { + const tools = yield* Tools.Service + const mutation = yield* LocationMutation.Service + const fs = yield* FSUtil.Service + const appProcess = yield* AppProcess.Service + const config = yield* Config.Service + const permission = yield* PermissionV2.Service + + yield* tools + .register({ + [name]: Tool.make({ + description: `Execute one shell command string with the host user's filesystem, process, and network authority. The active Location is the default working directory. Relative workdir values resolve from that Location. External workdir values require external_directory approval; best-effort command-argument path warnings are advisory only. Timeout values are milliseconds (default: ${DEFAULT_TIMEOUT_MS}; maximum: ${MAX_TIMEOUT_MS}). Uses the configured shell when set; otherwise uses /bin/sh on POSIX and COMSPEC or cmd.exe on Windows.`, + input: Input, + output: Output, + structured: StructuredOutput, + toStructuredOutput: ({ output }) => ({ + truncated: output.truncated, + ...(output.exit === undefined ? {} : { exit: output.exit }), + ...(output.timeout === undefined ? {} : { timeout: output.timeout }), + }), + toModelOutput: ({ output }) => [ + { type: "text", text: output.output }, + { type: "text", text: modelOutput(output) }, + ], + execute: (input, context) => + Effect.gen(function* () { + const source = { + type: "tool" as const, + messageID: context.assistantMessageID, + callID: context.toolCallID, + } + const target = yield* mutation.resolve({ path: input.workdir ?? ".", kind: "directory" }) + const external = target.externalDirectory + if (external) + yield* permission.assert({ + ...LocationMutation.externalDirectoryPermission(external), + sessionID: context.sessionID, + agent: context.agent, + source, + }) + const warnings = (yield* externalCommandDirectories(fs, input.command, target.canonical)).map( + (directory) => + `Command argument references external directory ${path.join(directory, "*").replaceAll("\\", "/")}. Bash runs with host-user filesystem, process, and network authority; this scan is advisory only.`, + ) + yield* permission.assert({ + action: name, + resources: [input.command], + save: [input.command], + sessionID: context.sessionID, + agent: context.agent, + source, + }) + // Tag command as sourcePath for auditing / redaction context + registerSourcePath(context.sessionID, context.toolCallID, input.command) + + if ((yield* fs.stat(target.canonical)).type !== "Directory") + return yield* Effect.fail(new Error(`Working directory is not a directory: ${target.canonical}`)) + + const entries = yield* config.entries() + const shell = + Object.assign({}, ...entries.flatMap((entry) => (entry.type === "document" ? [entry.info] : []))) + .shell ?? defaultShell() + const command = ChildProcess.make(input.command, [], { + cwd: target.canonical, + shell, + stdin: "ignore", + detached: process.platform !== "win32", + forceKillAfter: Duration.seconds(3), + }) + const timeout = input.timeout ?? DEFAULT_TIMEOUT_MS + const result = yield* appProcess + .run(command, { + combineOutput: true, + timeout: Duration.millis(timeout), + maxOutputBytes: MAX_CAPTURE_BYTES, + }) + .pipe( + Effect.catchTag("AppProcessError", (error) => + isTimeout(error) ? Effect.succeed(undefined) : Effect.fail(error), + ), + ) + if (!result) { + return { + output: `Command exceeded timeout of ${timeout} ms. Retry with a larger timeout if the command is expected to take longer.`, + truncated: false, + timeout: true, + ...(warnings.length ? { warnings } : {}), + } + } + + const output = result.output?.toString("utf8") || "(no output)" + const notice = result.outputTruncated + ? "[output capture truncated at the in-memory safety limit]" + : undefined + return { + exit: result.exitCode, + output: notice ? `${output}\n\n${notice}` : output, + truncated: result.outputTruncated === true, + ...(warnings.length ? { warnings } : {}), + } + }).pipe(Effect.mapError(() => new ToolFailure({ message: `Unable to execute command: ${input.command}` }))), + }), + }) + .pipe(Effect.orDie) + }), +) + +export const node = makeLocationNode({ + name: "tool/bash", + layer, + deps: [ToolRegistry.node, LocationMutation.node, FSUtil.node, AppProcess.node, Config.node, PermissionV2.node], +}) diff --git a/packages/app-bundle/overlay/packages/core/src/tool/edit.ts b/packages/app-bundle/overlay/packages/core/src/tool/edit.ts new file mode 100644 index 00000000..26abfe9f --- /dev/null +++ b/packages/app-bundle/overlay/packages/core/src/tool/edit.ts @@ -0,0 +1,225 @@ +/** + * Model-facing V2 exact-edit leaf. Relative paths resolve within the active + * Location. Absolute paths inside that Location are accepted, while explicit + * absolute external paths retain mutation capability through a separate + * external_directory approval before edit approval. + */ +export * as EditTool from "./edit" + +import { ToolFailure } from "@opencode-ai/llm" +import { FileDiff } from "@opencode-ai/schema/file-diff" +import { createTwoFilesPatch, diffLines } from "diff" +import { Effect, Layer, Schema } from "effect" +import { makeLocationNode } from "../effect/app-node" +import { FileMutation } from "../file-mutation" +import { FSUtil } from "../fs-util" +import { LocationMutation } from "../location-mutation" +import { PermissionV2 } from "../permission" +import { ToolRegistry } from "./registry" +import { Tool } from "./tool" +import { Tools } from "./tools" +import { registerSourcePath } from "../provider-permission" + +export const name = "edit" + +export const Input = Schema.Struct({ + path: Schema.String.annotate({ + description: + "File path to edit. Relative paths resolve within the active Location. Absolute paths inside that Location are accepted; external absolute paths require external_directory approval.", + }), + oldString: Schema.String.annotate({ description: "Exact text to replace" }), + newString: Schema.String.annotate({ description: "Replacement text, which must differ from oldString" }), + replaceAll: Schema.Boolean.pipe(Schema.optional).annotate({ + description: "Replace all exact occurrences of oldString (default false)", + }), +}) + +export const Output = Schema.Struct({ + files: Schema.Array(FileDiff.Info), + replacements: Schema.Number, +}) +export type Output = typeof Output.Type + +const normalizeLineEndings = (text: string) => text.replaceAll("\r\n", "\n") +const detectLineEnding = (text: string): "\n" | "\r\n" => (text.includes("\r\n") ? "\r\n" : "\n") +const convertToLineEnding = (text: string, ending: "\n" | "\r\n") => + ending === "\n" ? normalizeLineEndings(text) : normalizeLineEndings(text).replaceAll("\n", "\r\n") + +const splitBom = (text: string) => + text.startsWith("\uFEFF") ? { bom: true, text: text.slice(1) } : { bom: false, text } +const joinBom = (text: string, bom: boolean) => (bom ? `\uFEFF${text}` : text) +const decodeUtf8 = (content: Uint8Array) => { + const bom = content[0] === 0xef && content[1] === 0xbb && content[2] === 0xbf + return { bom, content, text: new TextDecoder().decode(bom ? content.slice(3) : content) } +} + +const countOccurrences = (content: string, search: string) => { + if (search === "") return content.length + 1 + let count = 0 + let offset = 0 + while ((offset = content.indexOf(search, offset)) !== -1) { + count++ + offset += search.length + } + return count +} + +const previewLines = (value: string, prefix: "+" | "-") => { + const lines = normalizeLineEndings(value).split("\n") + const shown = lines.slice(0, 6).map((line) => `${prefix}${line.length > 240 ? `${line.slice(0, 240)}...` : line}`) + if (lines.length > shown.length) shown.push(`${prefix}...`) + return shown +} + +export const toModelOutput = (output: Output, oldString: string, newString: string) => + [ + `Edited file successfully: ${output.files[0]?.file}`, + `Replacements: ${output.replacements}`, + "```diff", + ...previewLines(oldString, "-"), + ...previewLines(newString, "+"), + "```", + ].join("\n") + +/** Deferred V2 edit behavior and UX integrations remain visible at the model-facing seam. */ +// TODO: Port V1 fuzzy correction strategies only after exact-edit behavior is established: line-trimmed matching, block-anchor fallback, indentation correction, and similarity-threshold review. +// TODO: Add formatter integration after V2 formatter runtime exists. +// TODO: Publish watcher/file-edit events after V2 watcher integration exists. +// TODO: Add snapshots / undo after design exists. +// TODO: Add LSP notification and diagnostics after V2 LSP runtime exists. + +const layer = Layer.effectDiscard( + Effect.gen(function* () { + const tools = yield* Tools.Service + const mutation = yield* LocationMutation.Service + const files = yield* FileMutation.Service + const fs = yield* FSUtil.Service + const permission = yield* PermissionV2.Service + + yield* tools + .register({ + [name]: Tool.withPermission( + Tool.make({ + description: + "Replace exact text in one file. Relative paths resolve within the active Location. Absolute paths inside the Location are accepted. Explicit external absolute paths require external_directory approval before edit approval.", + input: Input, + output: Output, + toModelOutput: ({ input, output }) => [ + { type: "text", text: toModelOutput(output, input.oldString, input.newString) }, + ], + execute: (input, context) => { + const unableToEdit = (effect: Effect.Effect) => + effect.pipe( + Effect.mapError((error) => + error instanceof FileMutation.StaleContentError + ? new ToolFailure({ + message: "File changed after permission approval. Read it again before editing.", + }) + : new ToolFailure({ message: `Unable to edit ${input.path}` }), + ), + ) + + return Effect.gen(function* () { + const permissionSource = { + type: "tool" as const, + messageID: context.assistantMessageID, + callID: context.toolCallID, + } + if (input.oldString === input.newString) { + return yield* new ToolFailure({ + message: "No changes to apply: oldString and newString are identical.", + }) + } + if (input.oldString === "") { + return yield* new ToolFailure({ + message: "oldString must not be empty. Use write to create or overwrite a file.", + }) + } + + const target = yield* unableToEdit(mutation.resolve({ path: input.path, kind: "file" })) + const external = target.externalDirectory + if (external) { + yield* unableToEdit( + permission.assert({ + ...LocationMutation.externalDirectoryPermission(external), + sessionID: context.sessionID, + agent: context.agent, + source: permissionSource, + }), + ) + } + + yield* unableToEdit( + permission.assert({ + action: "edit", + resources: [target.resource], + save: ["*"], + sessionID: context.sessionID, + agent: context.agent, + source: permissionSource, + }), + ) + registerSourcePath(context.sessionID, context.toolCallID, target.resource) + const source = decodeUtf8(yield* unableToEdit(fs.readFile(target.canonical))) + const ending = detectLineEnding(source.text) + const oldString = convertToLineEnding(input.oldString, ending) + const newString = convertToLineEnding(input.newString, ending) + const replacements = countOccurrences(source.text, oldString) + if (replacements === 0) { + return yield* new ToolFailure({ + message: + "Could not find oldString in the file. It must match exactly, including whitespace and indentation.", + }) + } + if (replacements > 1 && input.replaceAll !== true) { + return yield* new ToolFailure({ + message: + "Found multiple exact matches for oldString. Provide more surrounding context or set replaceAll to true.", + }) + } + + const replaced = + input.replaceAll === true + ? source.text.replaceAll(oldString, newString) + : source.text.replace(oldString, newString) + const counts = diffLines(source.text, replaced).reduce( + (result, item) => ({ + additions: result.additions + (item.added ? (item.count ?? 0) : 0), + deletions: result.deletions + (item.removed ? (item.count ?? 0) : 0), + }), + { additions: 0, deletions: 0 }, + ) + const next = splitBom(replaced) + const result = yield* unableToEdit( + files.writeIfUnchanged({ + target, + expected: source.content, + content: joinBom(next.text, source.bom || next.bom), + }), + ) + return { + files: [ + { + file: result.resource, + patch: createTwoFilesPatch(result.resource, result.resource, source.text, replaced), + status: "modified" as const, + ...counts, + }, + ], + replacements, + } satisfies Output + }) + }, + }), + "edit", + ), + }) + .pipe(Effect.orDie) + }), +) + +export const node = makeLocationNode({ + name: "tool/edit", + layer, + deps: [ToolRegistry.node, LocationMutation.node, FileMutation.node, FSUtil.node, PermissionV2.node], +}) diff --git a/packages/app-bundle/overlay/packages/core/src/tool/glob.ts b/packages/app-bundle/overlay/packages/core/src/tool/glob.ts new file mode 100644 index 00000000..d25a8b15 --- /dev/null +++ b/packages/app-bundle/overlay/packages/core/src/tool/glob.ts @@ -0,0 +1,108 @@ +export * as GlobTool from "./glob" + +import { ToolFailure } from "@opencode-ai/llm" +import { Effect, Layer, Schema } from "effect" +import path from "path" +import { makeLocationNode } from "../effect/app-node" +import { FileSystem } from "../filesystem" +import { Location } from "../location" +import { Ripgrep } from "../ripgrep" +import { RelativePath } from "../schema" +import { PermissionV2 } from "../permission" +import { ToolRegistry } from "./registry" +import { Tool } from "./tool" +import { Tools } from "./tools" +import { registerSourcePath } from "../provider-permission" + +export const name = "glob" + +export const Input = Schema.Struct({ + pattern: FileSystem.GlobInput.fields.pattern.annotate({ description: "Glob pattern to match files against" }), + path: RelativePath.pipe(Schema.optional).annotate({ + description: "Relative directory to search. Defaults to the active Location.", + }), + limit: FileSystem.GlobInput.fields.limit.annotate({ + description: "Maximum results to return", + }), +}) + +export const Output = Schema.Array(FileSystem.Entry) +type ModelOutput = typeof Output.Encoded + +/** Format raw search results into the concise line-oriented output models expect. */ +export const toModelOutput = (output: ModelOutput) => { + const lines = output.length === 0 ? ["No files found"] : output.map((item) => item.path) + return lines.join("\n") +} + +/** Glob leaf that defaults its filesystem root to the active Location. */ +const layer = Layer.effectDiscard( + Effect.gen(function* () { + const tools = yield* Tools.Service + const ripgrep = yield* Ripgrep.Service + const location = yield* Location.Service + const permission = yield* PermissionV2.Service + + yield* tools + .register({ + [name]: Tool.make({ + description: + "Find files by glob pattern within the active Location. Returns concise relative file resources. Use a relative path to narrow the search and limit to bound the result count.", + input: Input, + output: Output, + toModelOutput: ({ output }) => [ + { + type: "text", + text: toModelOutput( + output.map((entry) => ({ ...entry, path: path.resolve(location.directory, entry.path) })), + ), + }, + ], + execute: (input, context) => + Effect.gen(function* () { + yield* permission.assert({ + action: name, + resources: [input.pattern], + save: ["*"], + metadata: { + root: input.path ?? ".", + path: input.path, + limit: input.limit, + }, + sessionID: context.sessionID, + agent: context.agent, + source: { type: "tool", messageID: context.assistantMessageID, callID: context.toolCallID }, + }) + // Tag source path (pattern) for history redaction + registerSourcePath(context.sessionID, context.toolCallID, input.pattern) + const cwd = path.resolve(location.directory, input.path ?? ".") + return yield* ripgrep + .glob({ + cwd, + pattern: input.pattern, + limit: input.limit ?? Number.MAX_SAFE_INTEGER, + }) + .pipe( + Effect.map((result) => + result.map((entry) => + FileSystem.Entry.make({ + ...entry, + path: RelativePath.make(path.relative(location.directory, path.resolve(cwd, entry.path))), + }), + ), + ), + ) + }).pipe( + Effect.mapError(() => new ToolFailure({ message: `Unable to find files matching ${input.pattern}` })), + ), + }), + }) + .pipe(Effect.orDie) + }), +) + +export const node = makeLocationNode({ + name: "tool/glob", + layer, + deps: [ToolRegistry.node, Ripgrep.node, Location.node, PermissionV2.node], +}) diff --git a/packages/app-bundle/overlay/packages/core/src/tool/grep.ts b/packages/app-bundle/overlay/packages/core/src/tool/grep.ts new file mode 100644 index 00000000..7fa6a324 --- /dev/null +++ b/packages/app-bundle/overlay/packages/core/src/tool/grep.ts @@ -0,0 +1,139 @@ +export * as GrepTool from "./grep" + +import { ToolFailure } from "@opencode-ai/llm" +import { Effect, Layer, Schema } from "effect" +import path from "path" +import { makeLocationNode } from "../effect/app-node" +import { FileSystem } from "../filesystem" +import { FSUtil } from "../fs-util" +import { Location } from "../location" +import { PermissionV2 } from "../permission" +import { Ripgrep } from "../ripgrep" +import { RelativePath } from "../schema" +import { ToolRegistry } from "./registry" +import { Tool } from "./tool" +import { Tools } from "./tools" +import { registerSourcePath } from "../provider-permission" + +export const name = "grep" + +export const Input = Schema.Struct({ + pattern: FileSystem.GrepInput.fields.pattern.annotate({ + description: "Regex pattern to search for in file contents", + }), + path: RelativePath.pipe(Schema.optional).annotate({ + description: "Relative directory to search. Defaults to the active Location.", + }), + include: FileSystem.GrepInput.fields.include.annotate({ + description: 'File glob to include in the search (for example, "*.js" or "*.{ts,tsx}")', + }), + limit: FileSystem.GrepInput.fields.limit.annotate({ + description: "Maximum matches to return", + }), +}) + +export const Output = Schema.Array(FileSystem.Match) +type ModelOutput = typeof Output.Encoded + +/** Format raw search matches into the familiar concise model output. */ +export const toModelOutput = (output: ModelOutput) => { + const lines = output.length === 0 ? ["No files found"] : [`Found ${output.length} matches`] + let current = "" + for (const match of output) { + if (current !== match.entry.path) { + if (current) lines.push("") + current = match.entry.path + lines.push(`${match.entry.path}:`) + } + lines.push(` Line ${match.line}: ${match.text}`) + } + return lines.join("\n") +} + +/** Grep leaf that defaults its filesystem root to the active Location. */ +const layer = Layer.effectDiscard( + Effect.gen(function* () { + const tools = yield* Tools.Service + const fs = yield* FSUtil.Service + const ripgrep = yield* Ripgrep.Service + const location = yield* Location.Service + const permission = yield* PermissionV2.Service + + yield* tools + .register({ + [name]: Tool.make({ + description: + "Search file contents by regular expression within the active Location or an absolute managed tool-output file. Use a path to narrow the search, include to filter files by glob, and limit to bound the match count. Returns concise file resources, line numbers, and bounded line previews.", + input: Input, + output: Output, + toModelOutput: ({ output }) => [ + { + type: "text", + text: toModelOutput( + output.map((match) => ({ + ...match, + entry: { ...match.entry, path: path.resolve(location.directory, match.entry.path) }, + })), + ), + }, + ], + execute: (input, context) => + Effect.gen(function* () { + yield* permission.assert({ + action: name, + resources: [input.pattern], + save: ["*"], + metadata: { + root: ".", + path: input.path, + include: input.include, + limit: input.limit, + }, + sessionID: context.sessionID, + agent: context.agent, + source: { type: "tool", messageID: context.assistantMessageID, callID: context.toolCallID }, + }) + registerSourcePath(context.sessionID, context.toolCallID, input.pattern) + const target = path.resolve(location.directory, input.path ?? ".") + const info = yield* fs.stat(target).pipe(Effect.catch(() => Effect.succeed(undefined))) + return yield* ripgrep + .grep({ + cwd: info?.type === "Directory" ? target : path.dirname(target), + pattern: input.pattern, + file: info?.type === "File" ? path.basename(target) : undefined, + include: input.include, + limit: input.limit ?? Number.MAX_SAFE_INTEGER, + }) + .pipe( + Effect.map((result) => + result.map((match) => + FileSystem.Match.make({ + ...match, + entry: FileSystem.Entry.make({ + ...match.entry, + path: RelativePath.make( + path.relative( + location.directory, + path.resolve( + info?.type === "Directory" ? target : path.dirname(target), + match.entry.path, + ), + ), + ), + }), + }), + ), + ), + ) + }).pipe(Effect.mapError(() => new ToolFailure({ message: `Unable to grep for ${input.pattern}` }))), + }), + }) + .pipe(Effect.orDie) + }), +) + +export const node = makeLocationNode({ + name: "tool/grep", + layer, + deps: [ToolRegistry.node, FSUtil.node, Ripgrep.node, Location.node, PermissionV2.node], +}) diff --git a/packages/app-bundle/overlay/packages/core/src/tool/question.ts b/packages/app-bundle/overlay/packages/core/src/tool/question.ts new file mode 100644 index 00000000..3fa5b3b2 --- /dev/null +++ b/packages/app-bundle/overlay/packages/core/src/tool/question.ts @@ -0,0 +1,95 @@ +export * as QuestionTool from "./question" + +import { ToolFailure } from "@opencode-ai/llm" +import { Effect, Layer, Schema } from "effect" +import { makeLocationNode } from "../effect/app-node" +import { PermissionV2 } from "../permission" +import { QuestionV2 } from "../question" +import { ToolRegistry } from "./registry" +import { Tool } from "./tool" +import { Tools } from "./tools" + +export const name = "question" + +export const description = `Use this tool when you need to ask the user questions during execution. This allows you to: +1. Gather user preferences or requirements +2. Clarify ambiguous instructions +3. Get decisions on implementation choices as you work +4. Offer choices to the user about what direction to take. + +Usage notes: +- Every question is a card: a choice question (default) lists options; a free-form question — one expecting an open-ended typed answer, like a name or a number — uses kind: "text" and renders a bare text input with submit, so give it no options +- When \`custom\` is enabled (default), a "Type your own answer" option is added automatically; don't include "Other" or catch-all options +- Answers are returned as arrays of labels; set \`multiple: true\` to allow selecting more than one +- If you recommend a specific option, make that the first option in the list and add "(Recommended)" at the end of the label` + +export const Input = Schema.Struct({ + questions: Schema.Array(QuestionV2.Prompt).annotate({ description: "Questions to ask" }), +}) + +export const Output = Schema.Struct({ + answers: Schema.Array(QuestionV2.Answer), +}) +export type Output = typeof Output.Type + +export const toModelOutput = ( + questions: ReadonlyArray, + answers: ReadonlyArray, +) => { + const formatted = questions + .map( + (question, index) => + `"${question.question}"="${answers[index]?.length ? answers[index].join(", ") : "Unanswered"}"`, + ) + .join(", ") + return `User has answered your questions: ${formatted}. You can now continue with the user's answers in mind.` +} + +const layer = Layer.effectDiscard( + Effect.gen(function* () { + const tools = yield* Tools.Service + const question = yield* QuestionV2.Service + const permission = yield* PermissionV2.Service + + yield* tools + .register({ + [name]: Tool.make({ + description, + input: Input, + output: Output, + toModelOutput: ({ input, output }) => [ + { type: "text", text: toModelOutput(input.questions, output.answers) }, + ], + execute: (input, context) => + permission + .assert({ + action: "question", + resources: ["*"], + sessionID: context.sessionID, + agent: context.agent, + source: { type: "tool", messageID: context.assistantMessageID, callID: context.toolCallID }, + }) + .pipe( + Effect.mapError(() => new ToolFailure({ message: "Permission denied: question" })), + Effect.andThen( + question + .ask({ + sessionID: context.sessionID, + questions: input.questions, + tool: { messageID: context.assistantMessageID, callID: context.toolCallID }, + }) + .pipe(Effect.orDie), + ), + Effect.map((answers) => ({ answers })), + ), + }), + }) + .pipe(Effect.orDie) + }), +) + +export const node = makeLocationNode({ + name: "tool/question", + layer, + deps: [ToolRegistry.node, PermissionV2.node, QuestionV2.node], +}) diff --git a/packages/app-bundle/overlay/packages/core/src/tool/read.ts b/packages/app-bundle/overlay/packages/core/src/tool/read.ts new file mode 100644 index 00000000..a183d1b1 --- /dev/null +++ b/packages/app-bundle/overlay/packages/core/src/tool/read.ts @@ -0,0 +1,120 @@ +export * as ReadTool from "./read" + +import { ToolFailure } from "@opencode-ai/llm" +import { Effect, Layer, Schema } from "effect" +import { makeLocationNode } from "../effect/app-node" +import { FileSystem } from "../filesystem" +import { Image } from "../image" +import { LocationMutation } from "../location-mutation" +import { PermissionV2 } from "../permission" +import { AbsolutePath } from "../schema" +import { ReadToolFileSystem } from "./read-filesystem" +import { ToolRegistry } from "./registry" +import { Tool } from "./tool" +import { Tools } from "./tools" +import { registerSourcePath } from "../provider-permission" + +export const name = "read" +const SUPPORTED_IMAGE_MIMES = new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]) +const LocationInput = Schema.Struct({ + path: Schema.String, + offset: ReadToolFileSystem.PageInput.fields.offset.annotate({ + description: "The 1-based directory entry or text line offset to start reading from", + }), + limit: ReadToolFileSystem.PageInput.fields.limit.annotate({ + description: "The maximum number of directory entries or text lines to read", + }), +}) +const Input = LocationInput +const Output = Schema.Union([FileSystem.Content, ReadToolFileSystem.TextPage, ReadToolFileSystem.ListPage]) + +const layer = Layer.effectDiscard( + Effect.gen(function* () { + const tools = yield* Tools.Service + const reader = yield* ReadToolFileSystem.Service + const mutation = yield* LocationMutation.Service + const image = yield* Image.Service + const permission = yield* PermissionV2.Service + + yield* tools + .register({ + [name]: Tool.make({ + description: + "Read a text file or supported image, page through a large UTF-8 text file by line offset, or list a directory page. Relative paths resolve from the current location; absolute paths inside it are accepted, while external absolute paths require external_directory approval.", + input: Input, + output: Output, + toModelOutput: ({ input, output }) => { + if (!("encoding" in output) || output.encoding !== "base64" || !SUPPORTED_IMAGE_MIMES.has(output.mime)) + return [] + return [ + { type: "text", text: "Image read successfully" }, + { type: "file", data: output.content, mime: output.mime, name: input.path }, + ] + }, + execute: (input, context) => { + return Effect.gen(function* () { + const source = { + type: "tool" as const, + messageID: context.assistantMessageID, + callID: context.toolCallID, + } + const target = yield* mutation.resolve({ path: input.path, kind: "directory" }) + const external = target.externalDirectory + if (external) + yield* permission.assert({ + ...LocationMutation.externalDirectoryPermission(external), + sessionID: context.sessionID, + agent: context.agent, + source, + }) + const resource = target.resource + const absolute = AbsolutePath.make(target.canonical) + const type = yield* reader.inspect(absolute) + yield* permission.assert({ + action: name, + resources: [resource], + save: ["*"], + sessionID: context.sessionID, + agent: context.agent, + source, + }) + // Source-path tagging for history redaction (preserved on tool results) + registerSourcePath(context.sessionID, context.toolCallID, resource) + if (type === "directory") + return yield* reader.list(absolute, { offset: input.offset, limit: input.limit }) + const content = yield* reader.read(absolute, resource, { + offset: input.offset, + limit: input.limit, + }) + if ("encoding" in content && content.encoding === "base64" && SUPPORTED_IMAGE_MIMES.has(content.mime)) { + return yield* image + .normalize(resource, { ...content, encoding: "base64" }) + .pipe(Effect.catchTag("Image.ResizerUnavailableError", () => Effect.succeed(content))) + } + if ("encoding" in content && content.encoding === "base64") + return yield* Effect.fail(new ReadToolFileSystem.BinaryFileError({ resource })) + return content + }).pipe( + Effect.mapError((error) => { + const message = + error instanceof ReadToolFileSystem.BinaryFileError || + error instanceof ReadToolFileSystem.MediaIngestLimitError || + error instanceof Image.DecodeError || + error instanceof Image.SizeError + ? error.message + : `Unable to read ${input.path}` + return new ToolFailure({ message }) + }), + ) + }, + }), + }) + .pipe(Effect.orDie) + }), +) + +export const node = makeLocationNode({ + name: "tool/read", + layer, + deps: [ToolRegistry.node, ReadToolFileSystem.node, LocationMutation.node, Image.node, PermissionV2.node], +}) diff --git a/packages/app-bundle/overlay/packages/core/src/tool/webfetch.ts b/packages/app-bundle/overlay/packages/core/src/tool/webfetch.ts new file mode 100644 index 00000000..74bab124 --- /dev/null +++ b/packages/app-bundle/overlay/packages/core/src/tool/webfetch.ts @@ -0,0 +1,220 @@ +export * as WebFetchTool from "./webfetch" + +import { ToolFailure } from "@opencode-ai/llm" +import { Duration, Effect, Layer, Schema } from "effect" +import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { Parser } from "htmlparser2" +import TurndownService from "turndown" +import { makeLocationNode } from "../effect/app-node" +import { LayerNodePlatform } from "../effect/app-node-platform" +import { PermissionV2 } from "../permission" +import { collectBoundedResponseBody } from "./http-body" +import { ToolRegistry } from "./registry" +import { Tool } from "./tool" +import { Tools } from "./tools" +import { registerSourcePath } from "../provider-permission" + +export const name = "webfetch" +export const MAX_RESPONSE_BYTES = 5 * 1024 * 1024 +export const DEFAULT_TIMEOUT_SECONDS = 30 +export const MAX_TIMEOUT_SECONDS = 120 + +export const description = `Fetch content from an HTTP or HTTPS URL and return it as text, markdown, or HTML. Markdown is the default. + +Use a more targeted tool when one is available. This tool is read-only. Large text results may be replaced with a preview while the complete output is retained in managed storage.` + +const Timeout = Schema.Number.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(MAX_TIMEOUT_SECONDS)) + +export const Input = Schema.Struct({ + url: Schema.String.annotate({ description: "The HTTP or HTTPS URL to fetch content from" }), + format: Schema.Literals(["text", "markdown", "html"]) + .annotate({ description: "The format to return the content in. Defaults to markdown." }) + .pipe(Schema.withDecodingDefault(Effect.succeed("markdown" as const))), + timeout: Timeout.pipe(Schema.optional).annotate({ + description: `Optional timeout in seconds (maximum: ${MAX_TIMEOUT_SECONDS})`, + }), +}) + +const Output = Schema.Struct({ + url: Schema.String, + contentType: Schema.String, + format: Input.fields.format, + output: Schema.String, +}) + +type Format = (typeof Input.Type)["format"] + +const acceptHeader = (format: Format) => { + switch (format) { + case "markdown": + return "text/markdown;q=1.0, text/x-markdown;q=0.9, text/plain;q=0.8, text/html;q=0.7, */*;q=0.1" + case "text": + return "text/plain;q=1.0, text/markdown;q=0.9, text/html;q=0.8, */*;q=0.1" + case "html": + return "text/html;q=1.0, application/xhtml+xml;q=0.9, text/plain;q=0.8, text/markdown;q=0.7, */*;q=0.1" + } + return "*/*" +} + +const headers = (format: Format, userAgent: string) => ({ + "User-Agent": userAgent, + Accept: acceptHeader(format), + "Accept-Language": "en-US,en;q=0.9", +}) + +const browserUserAgent = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36" + +const isCloudflareChallenge = (error: unknown) => { + if (!error || typeof error !== "object" || !("reason" in error)) return false + const reason = error.reason + if ( + !reason || + typeof reason !== "object" || + !("_tag" in reason) || + reason._tag !== "StatusCodeError" || + !("response" in reason) + ) + return false + const response = reason.response as HttpClientResponse.HttpClientResponse + return response.status === 403 && response.headers["cf-mitigated"] === "challenge" +} + +const request = (url: string, format: Format, userAgent = browserUserAgent) => + HttpClientRequest.get(url).pipe(HttpClientRequest.setHeaders(headers(format, userAgent))) + +const assertHttpUrl = (url: URL) => { + if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("URL must use http:// or https://") +} + +const execute = (http: HttpClient.HttpClient, url: string, format: Format, userAgent = browserUserAgent) => + http.execute(request(url, format, userAgent)).pipe(Effect.flatMap(HttpClientResponse.filterStatusOk)) + +const collectBody = (response: HttpClientResponse.HttpClientResponse) => + collectBoundedResponseBody( + response, + MAX_RESPONSE_BYTES, + () => new Error(`Response too large (exceeds ${MAX_RESPONSE_BYTES} byte limit)`), + ) + +const mimeFrom = (contentType: string) => contentType.split(";", 1)[0]?.trim().toLowerCase() ?? "" +const isImageAttachment = (mime: string) => + mime.startsWith("image/") && mime !== "image/svg+xml" && mime !== "image/vnd.fastbidsheet" +const isTextualMime = (mime: string) => + !mime || + mime.startsWith("text/") || + mime === "application/json" || + mime.endsWith("+json") || + mime === "application/xml" || + mime.endsWith("+xml") || + mime === "application/javascript" || + mime === "application/x-javascript" +const convert = (content: string, contentType: string, format: Format) => { + if (!contentType.includes("text/html")) return content + if (format === "markdown") return convertHTMLToMarkdown(content) + if (format === "text") return extractTextFromHTML(content) + return content +} + +const layer = Layer.effectDiscard( + Effect.gen(function* () { + const tools = yield* Tools.Service + const http = yield* HttpClient.HttpClient + const permission = yield* PermissionV2.Service + + yield* tools + .register({ + [name]: Tool.make({ + description, + input: Input, + output: Output, + toModelOutput: ({ output }) => [{ type: "text", text: output.output }], + execute: (input, context) => + Effect.gen(function* () { + yield* Effect.try({ + try: () => assertHttpUrl(new URL(input.url)), + catch: (error) => error, + }) + + yield* permission.assert({ + action: name, + resources: [input.url], + save: ["*"], + metadata: input, + sessionID: context.sessionID, + agent: context.agent, + source: { type: "tool", messageID: context.assistantMessageID, callID: context.toolCallID }, + }) + registerSourcePath(context.sessionID, context.toolCallID, input.url) + + const { body, contentType } = yield* Effect.gen(function* () { + const response = yield* execute(http, input.url, input.format).pipe( + Effect.catchIf(isCloudflareChallenge, () => execute(http, input.url, input.format, "opencode")), + ) + const contentType = response.headers["content-type"] || "" + const mime = mimeFrom(contentType) + if (isImageAttachment(mime)) + return yield* Effect.fail(new Error(`Unsupported fetched image content type: ${mime}`)) + if (!isTextualMime(mime)) + return yield* Effect.fail(new Error(`Unsupported fetched file content type: ${mime}`)) + return { body: yield* collectBody(response), contentType } + }).pipe( + Effect.timeoutOrElse({ + duration: Duration.seconds(input.timeout ?? DEFAULT_TIMEOUT_SECONDS), + orElse: () => Effect.fail(new Error("Request timed out")), + }), + ) + const content = new TextDecoder().decode(body) + const output = yield* Effect.try({ + try: () => convert(content, contentType, input.format), + catch: (error) => error, + }) + return { + url: input.url, + contentType, + format: input.format, + output, + } + }).pipe(Effect.mapError(() => new ToolFailure({ message: `Unable to fetch ${input.url}` }))), + }), + }) + .pipe(Effect.orDie) + }), +) + +export const node = makeLocationNode({ + name: "tool/webfetch", + layer, + deps: [ToolRegistry.node, PermissionV2.node, LayerNodePlatform.httpClient], +}) + +export function extractTextFromHTML(html: string) { + let text = "" + let skipDepth = 0 + const parser = new Parser({ + onopentag(name) { + if (skipDepth > 0 || ["script", "style", "noscript", "iframe", "object", "embed"].includes(name)) skipDepth++ + }, + ontext(input) { + if (skipDepth === 0) text += input + }, + onclosetag() { + if (skipDepth > 0) skipDepth-- + }, + }) + parser.write(html) + parser.end() + return text.trim() +} + +export function convertHTMLToMarkdown(html: string) { + const turndown = new TurndownService({ + headingStyle: "atx", + hr: "---", + bulletListMarker: "-", + codeBlockStyle: "fenced", + emDelimiter: "*", + }) + turndown.remove(["script", "style", "meta", "link"]) + return turndown.turndown(html) +} diff --git a/packages/app-bundle/overlay/packages/core/src/tool/websearch.ts b/packages/app-bundle/overlay/packages/core/src/tool/websearch.ts new file mode 100644 index 00000000..d0f0c5c9 --- /dev/null +++ b/packages/app-bundle/overlay/packages/core/src/tool/websearch.ts @@ -0,0 +1,262 @@ +export * as WebSearchTool from "./websearch" + +import { ToolFailure } from "@opencode-ai/llm" +import { Context, Duration, Effect, Layer, Schema } from "effect" +import { HttpClient, HttpClientRequest } from "effect/unstable/http" +import { makeLocationNode } from "../effect/app-node" +import { LayerNodePlatform } from "../effect/app-node-platform" +import { truthy } from "../flag/flag" +import { InstallationVersion } from "../installation/version" +import { PositiveInt } from "../schema" +import { PermissionV2 } from "../permission" +import { Tool } from "./tool" +import { Tools } from "./tools" +import { collectBoundedResponseBody } from "./http-body" +import { checksum } from "../util/encode" +import { ToolRegistry } from "./registry" +import { registerSourcePath } from "../provider-permission" + +export const name = "websearch" +export const NO_RESULTS = "No search results found. Please try a different query." +export const EXA_URL = "https://mcp.exa.ai/mcp" +export const PARALLEL_URL = "https://search.parallel.ai/mcp" +export const MAX_NUM_RESULTS = 20 +export const MAX_CONTEXT_CHARACTERS = 50_000 +export const MAX_RESPONSE_BYTES = 256 * 1024 + +/** + * Provider-independent local web search retained in V2 core for launch parity. + * This invokes the legacy Exa/Parallel product backends itself. It is distinct + * from provider-hosted web search tools, which remain route-owned and execute + * at the model provider. Ownership of this compromise can be revisited later. + */ +export const description = `Search the web using the session's local web search provider. Use this for current information beyond knowledge cutoff. + +This is a provider-independent local tool backed by Exa or Parallel. Provider-hosted web search tools are separate and execute at the model provider. + +Optional controls support result count, live crawling ('fallback' or 'preferred'), search type ('auto', 'fast', or 'deep'), and maximum context characters. + +The current year is ${new Date().getFullYear()}. Use this year when searching for recent information or current events.` + +export const Input = Schema.Struct({ + query: Schema.String.annotate({ description: "Websearch query" }), + numResults: Schema.optional(PositiveInt.check(Schema.isLessThanOrEqualTo(MAX_NUM_RESULTS))).annotate({ + description: `Number of search results to return (default: 8, maximum: ${MAX_NUM_RESULTS})`, + }), + livecrawl: Schema.optional(Schema.Literals(["fallback", "preferred"])).annotate({ + description: + "Live crawl mode - 'fallback': use live crawling as backup if cached unavailable, 'preferred': prioritize live crawling (default: 'fallback')", + }), + type: Schema.optional(Schema.Literals(["auto", "fast", "deep"])).annotate({ + description: "Search type - 'auto': balanced search (default), 'fast': quick results, 'deep': comprehensive search", + }), + contextMaxCharacters: Schema.optional(PositiveInt.check(Schema.isLessThanOrEqualTo(MAX_CONTEXT_CHARACTERS))).annotate( + { + description: `Maximum characters for context string optimized for models (default: 10000, maximum: ${MAX_CONTEXT_CHARACTERS})`, + }, + ), +}) + +export const Provider = Schema.Literals(["exa", "parallel"]) +export type Provider = typeof Provider.Type + +export interface Config { + readonly provider?: Provider + readonly enableExa: boolean + readonly enableParallel: boolean + readonly exaApiKey?: string + readonly parallelApiKey?: string +} + +export class ConfigService extends Context.Service()("@opencode/v2/WebSearchConfig") {} + +/** Isolates the retained product environment contract from the generic tool implementation. */ +export const defaultConfigLayer = Layer.sync(ConfigService, () => + ConfigService.of({ + provider: + process.env.OPENCODE_WEBSEARCH_PROVIDER === "exa" || process.env.OPENCODE_WEBSEARCH_PROVIDER === "parallel" + ? process.env.OPENCODE_WEBSEARCH_PROVIDER + : undefined, + enableExa: truthy("OPENCODE_EXPERIMENTAL") || truthy("OPENCODE_ENABLE_EXA") || truthy("OPENCODE_EXPERIMENTAL_EXA"), + enableParallel: truthy("OPENCODE_ENABLE_PARALLEL") || truthy("OPENCODE_EXPERIMENTAL_PARALLEL"), + exaApiKey: process.env.EXA_API_KEY, + parallelApiKey: process.env.PARALLEL_API_KEY, + }), +) + +export const configNode = makeLocationNode({ service: ConfigService, layer: defaultConfigLayer, deps: [] }) + +export function selectProvider( + sessionID: string, + flags: Pick = { enableExa: false, enableParallel: false }, + override?: Provider, +): Provider { + if (override) return override + if (flags.enableParallel) return "parallel" + if (flags.enableExa) return "exa" + return Number.parseInt(checksum(sessionID) ?? "0", 36) % 2 === 0 ? "exa" : "parallel" +} + +const McpResult = Schema.Struct({ + result: Schema.Struct({ + content: Schema.Array(Schema.Struct({ type: Schema.String, text: Schema.String })), + }), +}) +const decodeMcpResult = Schema.decodeUnknownEffect(Schema.fromJsonString(McpResult)) + +const parsePayload = (payload: string) => + Effect.gen(function* () { + const trimmed = payload.trim() + if (!trimmed.startsWith("{")) return undefined + return (yield* decodeMcpResult(trimmed)).result.content.find((item) => item.text)?.text + }) + +export const parseResponse = Effect.fn("WebSearchTool.parseResponse")(function* (body: string) { + const trimmed = body.trim() + const direct = trimmed ? yield* parsePayload(trimmed) : undefined + if (direct) return direct + for (const line of body.split("\n")) { + if (!line.startsWith("data: ")) continue + const data = yield* parsePayload(line.substring(6)) + if (data) return data + } + return undefined +}) + +const ExaArgs = Schema.Struct({ + query: Schema.String, + type: Schema.String, + numResults: Schema.Number, + livecrawl: Schema.String, + contextMaxCharacters: Schema.optional(Schema.Number), +}) +const ParallelArgs = Schema.Struct({ + objective: Schema.String, + search_queries: Schema.Array(Schema.String), + session_id: Schema.String, +}) +const McpRequest = (args: Schema.Struct) => + Schema.Struct({ + jsonrpc: Schema.Literal("2.0"), + id: Schema.Literal(1), + method: Schema.Literal("tools/call"), + params: Schema.Struct({ name: Schema.String, arguments: args }), + }) + +const exaUrl = (apiKey: string | undefined) => { + if (!apiKey) return EXA_URL + const url = new URL(EXA_URL) + url.searchParams.set("exaApiKey", apiKey) + return url.toString() +} + +const callMcp = ( + http: HttpClient.HttpClient, + url: string, + tool: string, + args: Schema.Struct, + value: Schema.Struct.Type, + headers: Record = {}, +) => + Effect.gen(function* () { + const request = yield* HttpClientRequest.post(url).pipe( + HttpClientRequest.accept("application/json, text/event-stream"), + HttpClientRequest.setHeaders(headers), + HttpClientRequest.schemaBodyJson(McpRequest(args))({ + jsonrpc: "2.0" as const, + id: 1 as const, + method: "tools/call" as const, + params: { name: tool, arguments: value }, + }), + ) + return yield* Effect.gen(function* () { + const response = yield* HttpClient.filterStatusOk(http).execute(request) + const body = yield* collectBoundedResponseBody( + response, + MAX_RESPONSE_BYTES, + () => new Error(`${tool} response exceeded ${MAX_RESPONSE_BYTES} bytes`), + ) + return yield* parseResponse(body.toString("utf8")) + }).pipe( + Effect.timeoutOrElse({ + duration: Duration.seconds(25), + orElse: () => Effect.fail(new Error(`${tool} request timed out`)), + }), + ) + }) + +const Output = Schema.Struct({ + provider: Provider, + text: Schema.String, +}) + +const layer = Layer.effectDiscard( + Effect.gen(function* () { + const tools = yield* Tools.Service + const http = yield* HttpClient.HttpClient + const config = yield* ConfigService + const permission = yield* PermissionV2.Service + + yield* tools + .register({ + [name]: Tool.make({ + description, + input: Input, + output: Output, + toModelOutput: ({ output }) => [{ type: "text", text: output.text }], + execute: (input, context) => { + const provider = selectProvider(context.sessionID, config, config.provider) + return Effect.gen(function* () { + yield* permission.assert({ + action: name, + resources: [input.query], + save: ["*"], + metadata: { ...input, provider }, + sessionID: context.sessionID, + agent: context.agent, + source: { type: "tool", messageID: context.assistantMessageID, callID: context.toolCallID }, + }) + registerSourcePath(context.sessionID, context.toolCallID, input.query) + + const text = + provider === "exa" + ? yield* callMcp(http, exaUrl(config.exaApiKey), "web_search_exa", ExaArgs, { + query: input.query, + type: input.type || "auto", + numResults: input.numResults || 8, + livecrawl: input.livecrawl || "fallback", + contextMaxCharacters: input.contextMaxCharacters, + }) + : yield* callMcp( + http, + PARALLEL_URL, + "web_search", + ParallelArgs, + { + objective: input.query, + search_queries: [input.query], + session_id: context.sessionID, + // V2 invocation context does not safely expose the model yet. + }, + { + "User-Agent": `opencode/${InstallationVersion}`, + ...(config.parallelApiKey ? { Authorization: `Bearer ${config.parallelApiKey}` } : {}), + }, + ) + return { + provider, + text: text ?? NO_RESULTS, + } + }).pipe(Effect.mapError(() => new ToolFailure({ message: `Unable to search the web for ${input.query}` }))) + }, + }), + }) + .pipe(Effect.orDie) + }), +) + +export const node = makeLocationNode({ + name: "tool/websearch", + layer, + deps: [ToolRegistry.node, PermissionV2.node, LayerNodePlatform.httpClient, configNode], +}) diff --git a/packages/app-bundle/overlay/packages/core/src/tool/write.ts b/packages/app-bundle/overlay/packages/core/src/tool/write.ts new file mode 100644 index 00000000..8d7cc9d1 --- /dev/null +++ b/packages/app-bundle/overlay/packages/core/src/tool/write.ts @@ -0,0 +1,103 @@ +/** + * Model-facing V2 file-write leaf. Relative paths resolve within the active + * Location. Absolute paths inside that Location are accepted, while explicit + * absolute external paths retain mutation capability through a separate + * external_directory approval before edit approval. + */ +export * as WriteTool from "./write" + +import { ToolFailure } from "@opencode-ai/llm" +import { Effect, Layer, Schema } from "effect" +import { makeLocationNode } from "../effect/app-node" +import { FileMutation } from "../file-mutation" +import { LocationMutation } from "../location-mutation" +import { PermissionV2 } from "../permission" +import { ToolRegistry } from "./registry" +import { Tool } from "./tool" +import { Tools } from "./tools" +import { registerSourcePath } from "../provider-permission" + +export const name = "write" + +// TODO: Revisit whether model-facing mutation schemas should prefer absolute `filePath` naming for trained-in compatibility after evaluating model behavior. +export const Input = Schema.Struct({ + path: Schema.String.annotate({ + description: + "File path to write. Relative paths resolve within the active Location. Absolute paths inside that Location are accepted; external absolute paths require external_directory approval.", + }), + content: Schema.String.annotate({ description: "Content to write to the file" }), +}) + +export const Output = Schema.Struct({ + operation: Schema.Literal("write"), + target: Schema.String, + resource: Schema.String, + existed: Schema.Boolean, +}) +export type Output = typeof Output.Type + +export const toModelOutput = (output: Output) => + `${output.existed ? "Wrote" : "Created"} file successfully: ${output.resource}` + +/** Deferred V2 write UX integrations remain visible at the model-facing seam. */ +// TODO: Add formatter integration after V2 formatter runtime exists. +// TODO: Publish watcher/file-edit events after V2 watcher integration exists. +// TODO: Add snapshots / undo after design exists. +// TODO: Add LSP notification and diagnostics after V2 LSP runtime exists. + +const layer = Layer.effectDiscard( + Effect.gen(function* () { + const tools = yield* Tools.Service + const mutation = yield* LocationMutation.Service + const files = yield* FileMutation.Service + const permission = yield* PermissionV2.Service + + yield* tools + .register({ + [name]: Tool.withPermission( + Tool.make({ + description: + "Write content to one file. Relative paths resolve within the active Location. Absolute paths inside the Location are accepted. Explicit external absolute paths require external_directory approval before edit approval.", + input: Input, + output: Output, + toModelOutput: ({ output }) => [{ type: "text", text: toModelOutput(output) }], + execute: (input, context) => + Effect.gen(function* () { + const source = { + type: "tool" as const, + messageID: context.assistantMessageID, + callID: context.toolCallID, + } + const target = yield* mutation.resolve({ path: input.path, kind: "file" }) + const external = target.externalDirectory + if (external) + yield* permission.assert({ + ...LocationMutation.externalDirectoryPermission(external), + sessionID: context.sessionID, + agent: context.agent, + source, + }) + yield* permission.assert({ + action: "edit", + resources: [target.resource], + save: ["*"], + sessionID: context.sessionID, + agent: context.agent, + source, + }) + registerSourcePath(context.sessionID, context.toolCallID, target.resource) + return yield* files.writeTextPreservingBom({ target, content: input.content }) + }).pipe(Effect.mapError(() => new ToolFailure({ message: `Unable to write ${input.path}` }))), + }), + "edit", + ), + }) + .pipe(Effect.orDie) + }), +) + +export const node = makeLocationNode({ + name: "tool/write", + layer, + deps: [ToolRegistry.node, LocationMutation.node, FileMutation.node, PermissionV2.node], +}) diff --git a/packages/app-bundle/overlay/packages/core/src/v1/session.ts b/packages/app-bundle/overlay/packages/core/src/v1/session.ts new file mode 100644 index 00000000..b2bcbcae --- /dev/null +++ b/packages/app-bundle/overlay/packages/core/src/v1/session.ts @@ -0,0 +1,70 @@ +export * as SessionV1 from "./session" + +import { Schema } from "effect" +import { NonNegativeInt } from "../schema" +import { NamedError } from "../util/error" + +export { + AgentPart, + AgentPartInput, + Assistant, + CompactionPart, + Event, + FilePart, + FilePartInput, + FilePartSource, + FileSource, + Format, + Info, + MessageID, + OutputFormatJsonSchema, + OutputFormatText, + Part, + PartID, + PatchPart, + Range, + ReasoningPart, + ResourceSource, + RetryPart, + SessionInfo, + SkillPart, + SkillPartInput, + SnapshotPart, + StepFinishPart, + StepStartPart, + SubtaskPart, + SubtaskPartInput, + SymbolSource, + TextPart, + TextPartInput, + ToolPart, + ToolState, + ToolStateCompleted, + ToolStateError, + ToolStatePending, + ToolStateRunning, + User, + WithParts, +} from "@opencode-ai/schema/session-v1" + +export const OutputLengthError = NamedError.create("MessageOutputLengthError", {}) +export const AuthError = NamedError.create("ProviderAuthError", { providerID: Schema.String, message: Schema.String }) +export const AbortedError = NamedError.create("MessageAbortedError", { message: Schema.String }) +export const StructuredOutputError = NamedError.create("StructuredOutputError", { + message: Schema.String, + retries: NonNegativeInt, +}) +export const APIError = NamedError.create("APIError", { + message: Schema.String, + statusCode: Schema.optional(NonNegativeInt), + isRetryable: Schema.Boolean, + responseHeaders: Schema.optional(Schema.Record(Schema.String, Schema.String)), + responseBody: Schema.optional(Schema.String), + metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)), +}) +export type APIError = Schema.Schema.Type +export const ContextOverflowError = NamedError.create("ContextOverflowError", { + message: Schema.String, + responseBody: Schema.optional(Schema.String), +}) +export const ContentFilterError = NamedError.create("ContentFilterError", { message: Schema.String }) diff --git a/packages/app-bundle/overlay/packages/core/test/tool-question.test.ts b/packages/app-bundle/overlay/packages/core/test/tool-question.test.ts new file mode 100644 index 00000000..540bae03 --- /dev/null +++ b/packages/app-bundle/overlay/packages/core/test/tool-question.test.ts @@ -0,0 +1,168 @@ +import { describe, expect } from "bun:test" +import { Effect, Exit, Fiber, Layer } from "effect" +import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" +import { PermissionV2 } from "@opencode-ai/core/permission" +import { QuestionV2 } from "@opencode-ai/core/question" +import { SessionV2 } from "@opencode-ai/core/session" +import { ToolRegistry } from "@opencode-ai/core/tool/registry" +import { QuestionTool } from "@opencode-ai/core/tool/question" +import { ToolOutputStore } from "@opencode-ai/core/tool-output-store" +import { testEffect } from "./lib/effect" +import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/tool" + +const sessionID = SessionV2.ID.make("ses_question_tool_test") +const assertions: PermissionV2.AssertInput[] = [] +let captured: QuestionV2.AskInput | undefined +let reject = false +let deny = false +const capturedInput = () => captured +const permission = Layer.succeed( + PermissionV2.Service, + PermissionV2.Service.of({ + assert: (input) => + Effect.sync(() => assertions.push(input)).pipe( + Effect.andThen(deny ? Effect.fail(new PermissionV2.BlockedError({ rules: [] })) : Effect.void), + ), + ask: () => Effect.die("unused"), + reply: () => Effect.die("unused"), + get: () => Effect.die("unused"), + forSession: () => Effect.die("unused"), + list: () => Effect.die("unused"), + }), +) +const question = Layer.succeed( + QuestionV2.Service, + QuestionV2.Service.of({ + ask: (input: QuestionV2.AskInput) => + Effect.sync(() => { + captured = input + }).pipe(Effect.andThen(reject ? Effect.fail(new QuestionV2.RejectedError()) : Effect.succeed([["Build"], []]))), + reply: () => Effect.die("unused"), + reject: () => Effect.die("unused"), + list: () => Effect.die("unused"), + }), +) +const it = testEffect( + AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, QuestionTool.node]), [ + [PermissionV2.node, permission], + [QuestionV2.node, question], + [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig], + ]), +) + +describe("QuestionTool", () => { + it.effect("teaches the text kind for free-form questions in its description", () => + Effect.gen(function* () { + expect(QuestionTool.description).toContain('kind: "text"') + }), + ) + + it.effect("omits a denied built-in question and terminally settles a stale call", () => + Effect.gen(function* () { + captured = undefined + deny = true + const registry = yield* ToolRegistry.Service + + expect(yield* toolDefinitions(registry, [{ action: "question", resource: "*", effect: "deny" }])).toEqual([]) + expect( + yield* settleTool(registry, { + sessionID, + ...toolIdentity, + call: { type: "tool-call", id: "call-question-denied", name: "question", input: { questions: [] } }, + }), + ).toEqual({ result: { type: "error", value: "Permission denied: question" } }) + expect(capturedInput()).toBeUndefined() + deny = false + }), + ) + + it.effect("registers question and projects user answers without a permission assertion", () => + Effect.gen(function* () { + assertions.length = 0 + captured = undefined + reject = false + deny = false + const registry = yield* ToolRegistry.Service + const questions = [ + { + question: "What should happen?", + header: "Action", + options: [{ label: "Build", description: "Build it" }], + }, + { + question: "Which environment?", + header: "Environment", + options: [{ label: "Dev", description: "Development" }], + }, + ] + + expect((yield* toolDefinitions(registry)).map((definition) => definition.name)).toEqual(["question"]) + expect( + yield* settleTool(registry, { + sessionID, + ...toolIdentity, + call: { type: "tool-call", id: "call-question", name: "question", input: { questions } }, + }), + ).toEqual({ + result: { + type: "text", + value: + 'User has answered your questions: "What should happen?"="Build", "Which environment?"="Unanswered". You can now continue with the user\'s answers in mind.', + }, + output: { + structured: { answers: [["Build"], []] }, + content: [ + { + type: "text", + text: 'User has answered your questions: "What should happen?"="Build", "Which environment?"="Unanswered". You can now continue with the user\'s answers in mind.', + }, + ], + }, + }) + expect(assertions).toMatchObject([{ sessionID, action: "question", resources: ["*"] }]) + expect(capturedInput()).toEqual({ + sessionID, + questions, + tool: { messageID: toolIdentity.assistantMessageID, callID: "call-question" }, + }) + }), + ) + + it.effect("does not invent tool ownership metadata without a durable registry source", () => + Effect.gen(function* () { + captured = undefined + reject = false + deny = false + const registryService = yield* ToolRegistry.Service + + yield* executeTool(registryService, { + sessionID, + ...toolIdentity, + call: { type: "tool-call", id: "call-question", name: "question", input: { questions: [] } }, + }) + expect(capturedInput()).toEqual({ + sessionID, + questions: [], + tool: { messageID: toolIdentity.assistantMessageID, callID: "call-question" }, + }) + }), + ) + + it.effect("keeps dismissed questions out of model-facing output", () => + Effect.gen(function* () { + captured = undefined + reject = true + deny = false + const registryService = yield* ToolRegistry.Service + const fiber = yield* executeTool(registryService, { + sessionID, + ...toolIdentity, + call: { type: "tool-call", id: "call-question", name: "question", input: { questions: [] } }, + }).pipe(Effect.forkScoped) + + const exit = yield* Fiber.await(fiber) + expect(Exit.isFailure(exit)).toBe(true) + }), + ) +}) diff --git a/packages/app-bundle/overlay/packages/schema/src/index.ts b/packages/app-bundle/overlay/packages/schema/src/index.ts new file mode 100644 index 00000000..1886c670 --- /dev/null +++ b/packages/app-bundle/overlay/packages/schema/src/index.ts @@ -0,0 +1,29 @@ +export { Agent } from "./agent" +export { Command } from "./command" +export { Connection } from "./connection" +export { Credential } from "./credential" +export { Event } from "./event" +export { FileSystem } from "./filesystem" +export { Integration } from "./integration" +export { LLM } from "./llm" +export { Location } from "./location" +export { Model } from "./model" +export { Permission } from "./permission" +export { PermissionSaved } from "./permission-saved" +export { ProviderPermission } from "./provider-permission" +export { Project } from "./project" +export { ProjectCopy } from "./project-copy" +export { Provider } from "./provider" +export { Reference } from "./reference" +export { Revert } from "./revert" +export { Session } from "./session" +export { SessionInput } from "./session-input" +export { SessionMessage } from "./session-message" +export { Skill } from "./skill" +export { Pty } from "./pty" +export { PtyTicket } from "./pty-ticket" +export { Question } from "./question" +export { Workspace } from "./workspace" +export { Prompt, Source, FileAttachment, AgentAttachment, SkillAttachment } from "./prompt" +export { PromptInput } from "./prompt-input" +export * from "./schema" diff --git a/packages/app-bundle/overlay/packages/schema/src/prompt.ts b/packages/app-bundle/overlay/packages/schema/src/prompt.ts new file mode 100644 index 00000000..142c00fd --- /dev/null +++ b/packages/app-bundle/overlay/packages/schema/src/prompt.ts @@ -0,0 +1,67 @@ +import { Schema } from "effect" +import { optional } from "./schema" +import { statics } from "./schema" + +export interface Source extends Schema.Schema.Type {} +export const Source = Schema.Struct({ + start: Schema.Finite, + end: Schema.Finite, + text: Schema.String, +}).annotate({ identifier: "Prompt.Source" }) + +export interface FileAttachment extends Schema.Schema.Type {} +export const FileAttachment = Schema.Struct({ + uri: Schema.String, + mime: Schema.String, + name: Schema.String.pipe(optional), + description: Schema.String.pipe(optional), + source: Source.pipe(optional), +}) + .annotate({ identifier: "Prompt.FileAttachment" }) + .pipe( + statics((schema) => ({ + create: (input: FileAttachment) => + schema.make({ + uri: input.uri, + mime: input.mime, + name: input.name, + description: input.description, + source: input.source, + }), + })), + ) + +export interface AgentAttachment extends Schema.Schema.Type {} +export const AgentAttachment = Schema.Struct({ + name: Schema.String, + source: Source.pipe(optional), +}).annotate({ identifier: "Prompt.AgentAttachment" }) + +export interface SkillAttachment extends Schema.Schema.Type {} +export const SkillAttachment = Schema.Struct({ + name: Schema.String, + description: Schema.String.pipe(optional), + content: Schema.String.pipe(optional), + source: Source.pipe(optional), +}).annotate({ identifier: "Prompt.SkillAttachment" }) + +export interface Prompt extends Schema.Schema.Type {} +export const Prompt = Schema.Struct({ + text: Schema.String, + files: Schema.Array(FileAttachment).pipe(optional), + agents: Schema.Array(AgentAttachment).pipe(optional), + skills: Schema.Array(SkillAttachment).pipe(optional), +}) + .annotate({ identifier: "Prompt" }) + .pipe( + statics((schema) => ({ + equivalence: Schema.toEquivalence(schema), + fromUserMessage: (input: Pick) => + schema.make({ + text: input.text, + ...(input.files === undefined ? {} : { files: input.files }), + ...(input.agents === undefined ? {} : { agents: input.agents }), + ...(input.skills === undefined ? {} : { skills: input.skills }), + }), + })), + ) diff --git a/packages/app-bundle/overlay/packages/schema/src/provider-permission.ts b/packages/app-bundle/overlay/packages/schema/src/provider-permission.ts new file mode 100644 index 00000000..4b0ce523 --- /dev/null +++ b/packages/app-bundle/overlay/packages/schema/src/provider-permission.ts @@ -0,0 +1,185 @@ +export * as ProviderPermission from "./provider-permission" + +import { Schema } from "effect" + +export const Effect = Schema.Literals(["allow", "deny", "ask"]).annotate({ identifier: "ProviderPermission.Effect" }) +export type Effect = typeof Effect.Type + +export const DirectoryPermissions = Schema.Struct({ + read: Effect, + write: Effect, + execute: Effect, + network: Effect, +}).annotate({ identifier: "ProviderPermission.DirectoryPermissions" }) +export type DirectoryPermissions = typeof DirectoryPermissions.Type + +export const TrustTier = Schema.Struct({ + id: Schema.String, + label: Schema.String, + directories: Schema.Record(Schema.String, DirectoryPermissions), +}).annotate({ identifier: "ProviderPermission.TrustTier" }) +export type TrustTier = typeof TrustTier.Type + +export const Config = Schema.Struct({ + defaultTier: Schema.String, + tiers: Schema.Array(TrustTier), + assignments: Schema.Record(Schema.String, Schema.String), +}).annotate({ identifier: "ProviderPermission.Config" }) +export type Config = typeof Config.Type + +// Default tiers as described in spec +export const DEFAULT_TIERS: TrustTier[] = [ + { + id: "trusted", + label: "Trusted", + directories: { + "**": { read: "allow", write: "allow", execute: "allow", network: "allow" }, + }, + }, + { + id: "limited", + label: "Limited", + directories: { + "**": { read: "allow", write: "deny", execute: "deny", network: "deny" }, + "~/secrets/**": { read: "deny", write: "deny", execute: "deny", network: "deny" }, + }, + }, + { + id: "untrusted", + label: "Untrusted", + directories: { + "**": { read: "deny", write: "deny", execute: "deny", network: "deny" }, + }, + }, + { + id: "unassigned", + label: "Unassigned", + directories: { + "**": { read: "ask", write: "ask", execute: "ask", network: "ask" }, + }, + }, +] + +export const DEFAULT_CONFIG: Config = { + defaultTier: "unassigned", + tiers: DEFAULT_TIERS, + assignments: {}, +} + +// Action group mapping +export const ACTION_GROUPS = { + read: ["read", "glob", "grep"] as const, + write: ["write", "edit"] as const, + execute: ["bash"] as const, + network: ["webfetch", "websearch"] as const, +} as const + +export type ActionGroup = keyof typeof ACTION_GROUPS + +export function actionToGroup(action: string): ActionGroup | undefined { + for (const [group, actions] of Object.entries(ACTION_GROUPS) as Array<[ActionGroup, readonly string[]]>) { + if ((actions as readonly string[]).includes(action)) return group + } + return undefined +} + +// Summary badge logic +export type TierSummary = "Full Access" | "Read Only" | "No Access" | "Ask Everything" | "Custom" + +export function tierSummary(tier: TrustTier): TierSummary { + const all = Object.values(tier.directories) + if (all.length === 0) return "Ask Everything" + // Simple heuristic: check ** pattern if exists, otherwise aggregate + const wildcard = tier.directories["**"] + if (wildcard) { + const vals = Object.values(wildcard) + if (vals.every((v) => v === "allow")) return "Full Access" + if (vals.every((v) => v === "ask")) return "Ask Everything" + if (vals.every((v) => v === "deny")) return "No Access" + if (wildcard.read === "allow" && wildcard.write === "deny" && wildcard.execute === "deny" && wildcard.network === "deny") + return "Read Only" + } + // fallback + const flat = all.flatMap((d) => Object.values(d)) + if (flat.every((v) => v === "allow")) return "Full Access" + if (flat.every((v) => v === "deny")) return "No Access" + if (flat.every((v) => v === "ask")) return "Ask Everything" + // check read-only pattern across all directories + const allReadAllow = all.every((d) => d.read === "allow") + const allOtherDeny = all.every((d) => d.write === "deny" && d.execute === "deny" && d.network === "deny") + if (allReadAllow && allOtherDeny) return "Read Only" + return "Custom" +} + +// Glob specificity: longer/more-specific pattern wins +export function mostSpecificMatch( + path: string, + directories: Record, +): { pattern: string; permissions: DirectoryPermissions } | undefined { + let best: { pattern: string; permissions: DirectoryPermissions } | undefined + let bestScore = -1 + for (const [pattern, perms] of Object.entries(directories)) { + if (!globMatch(path, pattern)) continue + const score = globSpecificity(pattern) + if (score > bestScore) { + bestScore = score + best = { pattern, permissions: perms } + } + } + return best +} + +function globSpecificity(pattern: string): number { + // Higher score = more specific. Count non-wildcard chars + segments + let score = pattern.length * 10 + // penalize wildcards + const wildcards = (pattern.match(/\*/g) || []).length + score -= wildcards * 5 + // bonus for segments + score += pattern.split("/").length + return score +} + +function globMatch(input: string, pattern: string): boolean { + // Normalize ~/ to home-like prefix (treated as literal prefix) + // Replace ~ with placeholder, expand **, *, ? + const normalized = input.replaceAll("\\", "/") + let regex = pattern + .replaceAll("\\", "/") + .replace(/[.+^${}()|[\]\\]/g, "\\$&") + .replace(/\*\*/g, "###DOUBLE###") + .replace(/\*/g, "[^/]*") + .replace(/###DOUBLE###/g, ".*") + .replace(/\?/g, "[^/]") + // Handle trailing /** = optional deeper + // Already covered by .* expansion + return new RegExp("^" + regex + "$").test(normalized) +} + +export function resolveEffect( + config: Config, + modelId: string, + action: string, + resource: string, +): Effect | undefined { + const tierId = config.assignments[modelId] ?? config.defaultTier + const tier = config.tiers.find((t) => t.id === tierId) ?? config.tiers.find((t) => t.id === config.defaultTier) + if (!tier) return undefined + const group = actionToGroup(action) + if (!group) return undefined + const match = mostSpecificMatch(resource, tier.directories) + // If resource is empty (e.g., network tool without path), use ** rule + const perms = match?.permissions ?? tier.directories["**"] + if (!perms) return undefined + return perms[group] +} + +// Decoded config with defaults applied +export function withDefaults(input?: Partial | undefined): Config { + if (!input) return DEFAULT_CONFIG + return { + defaultTier: input.defaultTier ?? DEFAULT_CONFIG.defaultTier, + tiers: input.tiers ?? DEFAULT_TIERS, + assignments: input.assignments ?? {}, + } +} diff --git a/packages/app-bundle/overlay/packages/schema/src/question.ts b/packages/app-bundle/overlay/packages/schema/src/question.ts new file mode 100644 index 00000000..0f44af76 --- /dev/null +++ b/packages/app-bundle/overlay/packages/schema/src/question.ts @@ -0,0 +1,92 @@ +export * as Question from "./question" + +import { Schema } from "effect" +import { optional } from "./schema" +import { define, inventory } from "./event" +import { ascending } from "./identifier" +import { SessionID } from "./session-id" +import { statics } from "./schema" + +export const ID = Schema.String.check(Schema.isStartsWith("que")).pipe( + Schema.brand("QuestionV2.ID"), + statics((schema) => { + const create = () => schema.make("que_" + ascending()) + return { + create, + ascending: (id?: string) => (id === undefined ? create() : schema.make(id)), + } + }), +) +export type ID = typeof ID.Type + +export const Option = Schema.Struct({ + label: Schema.String.annotate({ description: "Display text (1-5 words, concise)" }), + description: Schema.String.annotate({ description: "Explanation of choice" }), +}).annotate({ identifier: "QuestionV2.Option" }) +export interface Option extends Schema.Schema.Type {} + +export const Kind = Schema.Literals(["choice", "text"]).annotate({ + description: 'Question shape: "choice" (default, an option list) or "text" (a free-form text card, no options)', +}) +export type Kind = typeof Kind.Type + +const base = { + question: Schema.String.annotate({ description: "Complete question" }), + header: Schema.String.annotate({ description: "Very short label (max 30 chars)" }), + options: Schema.Array(Option).annotate({ description: "Available choices" }), + multiple: Schema.Boolean.pipe(optional).annotate({ description: "Allow selecting multiple choices" }), + kind: Kind.pipe(optional), +} + +export const Info = Schema.Struct({ + ...base, + custom: Schema.Boolean.pipe(optional).annotate({ + description: "Allow typing a custom answer (default: true)", + }), +}).annotate({ identifier: "QuestionV2.Info" }) +export interface Info extends Schema.Schema.Type {} + +export const Prompt = Schema.Struct(base).annotate({ identifier: "QuestionV2.Prompt" }) +export interface Prompt extends Schema.Schema.Type {} + +export const Tool = Schema.Struct({ + messageID: Schema.String, + callID: Schema.String, +}).annotate({ identifier: "QuestionV2.Tool" }) +export interface Tool extends Schema.Schema.Type {} + +export const Request = Schema.Struct({ + id: ID, + sessionID: SessionID, + questions: Schema.Array(Info).annotate({ description: "Questions to ask" }), + tool: Tool.pipe(optional), +}).annotate({ identifier: "QuestionV2.Request" }) +export interface Request extends Schema.Schema.Type {} + +export const Answer = Schema.Array(Schema.String).annotate({ identifier: "QuestionV2.Answer" }) +export type Answer = typeof Answer.Type + +export const Reply = Schema.Struct({ + answers: Schema.Array(Answer).annotate({ + description: "User answers in order of questions (each answer is an array of selected labels)", + }), +}).annotate({ identifier: "QuestionV2.Reply" }) +export interface Reply extends Schema.Schema.Type {} + +const Asked = define({ type: "question.v2.asked", schema: Request.fields }) +const Replied = define({ + type: "question.v2.replied", + schema: { + sessionID: SessionID, + requestID: ID, + answers: Schema.Array(Answer), + }, +}) +const Rejected = define({ + type: "question.v2.rejected", + schema: { + sessionID: SessionID, + requestID: ID, + }, +}) +export const Event = { Asked, Replied, Rejected, Definitions: inventory(Asked, Replied, Rejected) } diff --git a/packages/app-bundle/overlay/packages/schema/src/v1/question.ts b/packages/app-bundle/overlay/packages/schema/src/v1/question.ts new file mode 100644 index 00000000..b90503a5 --- /dev/null +++ b/packages/app-bundle/overlay/packages/schema/src/v1/question.ts @@ -0,0 +1,71 @@ +export * as QuestionV1 from "./question" + +import { Schema } from "effect" +import { define, inventory } from "../event" +import { ascending } from "../identifier" +import { statics } from "../schema" +import { SessionID } from "../session-id" +import { SessionV1 } from "./session" + +export const ID = Schema.String.check(Schema.isStartsWith("que")).pipe( + Schema.brand("QuestionID"), + statics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "que_" + ascending()) })), +) + +export const Option = Schema.Struct({ + label: Schema.String.annotate({ description: "Display text (1-5 words, concise)" }), + description: Schema.String.annotate({ description: "Explanation of choice" }), +}).annotate({ identifier: "QuestionOption" }) + +export const Kind = Schema.Literals(["choice", "text"]).annotate({ + description: 'Question shape: "choice" (default, an option list) or "text" (a free-form text card, no options)', +}) + +const base = { + question: Schema.String.annotate({ description: "Complete question" }), + header: Schema.String.annotate({ description: "Very short label (max 30 chars)" }), + options: Schema.Array(Option).annotate({ description: "Available choices" }), + multiple: Schema.optional(Schema.Boolean).annotate({ description: "Allow selecting multiple choices" }), + kind: Schema.optional(Kind), +} + +export const Info = Schema.Struct({ + ...base, + custom: Schema.optional(Schema.Boolean).annotate({ description: "Allow typing a custom answer (default: true)" }), +}).annotate({ identifier: "QuestionInfo" }) +export const Prompt = Schema.Struct(base).annotate({ identifier: "QuestionPrompt" }) +export const Tool = Schema.Struct({ messageID: SessionV1.MessageID, callID: Schema.String }).annotate({ + identifier: "QuestionTool", +}) +export const Request = Schema.Struct({ + id: ID, + sessionID: SessionID, + questions: Schema.Array(Info).annotate({ description: "Questions to ask" }), + tool: Schema.optional(Tool), +}).annotate({ identifier: "QuestionRequest" }) +export const Answer = Schema.Array(Schema.String).annotate({ identifier: "QuestionAnswer" }) +export const Reply = Schema.Struct({ + answers: Schema.Array(Answer).annotate({ + description: "User answers in order of questions (each answer is an array of selected labels)", + }), +}).annotate({ identifier: "QuestionReply" }) +export const Replied = Schema.Struct({ + sessionID: SessionID, + requestID: ID, + answers: Schema.Array(Answer), +}).annotate({ + identifier: "QuestionReplied", +}) +export const Rejected = Schema.Struct({ sessionID: SessionID, requestID: ID }).annotate({ + identifier: "QuestionRejected", +}) + +const Asked = define({ type: "question.asked", schema: Request.fields }) +const RepliedEvent = define({ type: "question.replied", schema: Replied.fields }) +const RejectedEvent = define({ type: "question.rejected", schema: Rejected.fields }) +export const Event = { + Asked, + Replied: RepliedEvent, + Rejected: RejectedEvent, + Definitions: inventory(Asked, RepliedEvent, RejectedEvent), +} diff --git a/packages/app-bundle/overlay/packages/schema/src/v1/session.ts b/packages/app-bundle/overlay/packages/schema/src/v1/session.ts new file mode 100644 index 00000000..f28d6fd8 --- /dev/null +++ b/packages/app-bundle/overlay/packages/schema/src/v1/session.ts @@ -0,0 +1,710 @@ +export * as SessionV1 from "./session" + +import { Effect, Schema, Types } from "effect" +import { define, inventory } from "../event" +import { FileDiff } from "../file-diff" +import { Project } from "../project" +import { Provider } from "../provider" +import { Model } from "../model" +import { NonNegativeInt, optional, statics } from "../schema" +import { ascending } from "../identifier" +import { SessionID } from "../session-id" +import { WorkspaceID } from "../workspace-id" +import { PermissionV1 } from "./permission" + +const Timestamp = Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0)) + +export const MessageID = Schema.String.check(Schema.isStartsWith("msg")).pipe( + Schema.brand("MessageID"), + statics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "msg_" + ascending()) })), +) +export type MessageID = typeof MessageID.Type + +export const PartID = Schema.String.check(Schema.isStartsWith("prt")).pipe( + Schema.brand("PartID"), + statics((schema) => ({ ascending: (id?: string) => schema.make(id ?? "prt_" + ascending()) })), +) +export type PartID = typeof PartID.Type + +const namedError = (name: Name, fields: Fields) => { + const schema = Schema.Struct({ name: Schema.Literal(name), data: Schema.Struct(fields) }).annotate({ + identifier: name, + }) + return { Schema: schema, EffectSchema: schema } +} + +export const OutputLengthError = namedError("MessageOutputLengthError", {}) + +export const AuthError = namedError("ProviderAuthError", { + providerID: Schema.String, + message: Schema.String, +}) + +export const AbortedError = namedError("MessageAbortedError", { message: Schema.String }) +export const StructuredOutputError = namedError("StructuredOutputError", { + message: Schema.String, + retries: NonNegativeInt, +}) +export const APIError = namedError("APIError", { + message: Schema.String, + statusCode: Schema.optional(NonNegativeInt), + isRetryable: Schema.Boolean, + responseHeaders: Schema.optional(Schema.Record(Schema.String, Schema.String)), + responseBody: Schema.optional(Schema.String), + metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)), +}) +export type APIError = Schema.Schema.Type +export const ContextOverflowError = namedError("ContextOverflowError", { + message: Schema.String, + responseBody: Schema.optional(Schema.String), +}) +export const ContentFilterError = namedError("ContentFilterError", { + message: Schema.String, +}) + +export class OutputFormatText extends Schema.Class("OutputFormatText")({ + type: Schema.Literal("text"), +}) {} + +export class OutputFormatJsonSchema extends Schema.Class("OutputFormatJsonSchema")({ + type: Schema.Literal("json_schema"), + schema: Schema.Record(Schema.String, Schema.Any).annotate({ identifier: "JSONSchema" }), + retryCount: NonNegativeInt.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed(2))), +}) {} + +export const Format = Schema.Union([OutputFormatText, OutputFormatJsonSchema]).annotate({ + discriminator: "type", + identifier: "OutputFormat", +}) +export type OutputFormat = Schema.Schema.Type + +const partBase = { + id: PartID, + sessionID: SessionID, + messageID: MessageID, +} + +export const SnapshotPart = Schema.Struct({ + ...partBase, + type: Schema.Literal("snapshot"), + snapshot: Schema.String, +}).annotate({ identifier: "SnapshotPart" }) +export type SnapshotPart = Types.DeepMutable> + +export const PatchPart = Schema.Struct({ + ...partBase, + type: Schema.Literal("patch"), + hash: Schema.String, + files: Schema.Array(Schema.String), +}).annotate({ identifier: "PatchPart" }) +export type PatchPart = Types.DeepMutable> + +export const TextPart = Schema.Struct({ + ...partBase, + type: Schema.Literal("text"), + text: Schema.String, + synthetic: Schema.optional(Schema.Boolean), + ignored: Schema.optional(Schema.Boolean), + time: Schema.optional( + Schema.Struct({ + start: NonNegativeInt, + end: Schema.optional(NonNegativeInt), + }), + ), + metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)), +}).annotate({ identifier: "TextPart" }) +export type TextPart = Types.DeepMutable> + +export const ReasoningPart = Schema.Struct({ + ...partBase, + type: Schema.Literal("reasoning"), + text: Schema.String, + metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)), + time: Schema.Struct({ + start: NonNegativeInt, + end: Schema.optional(NonNegativeInt), + }), +}).annotate({ identifier: "ReasoningPart" }) +export type ReasoningPart = Types.DeepMutable> + +const filePartSourceBase = { + text: Schema.Struct({ + value: Schema.String, + start: Schema.Finite, + end: Schema.Finite, + }).annotate({ identifier: "FilePartSourceText" }), +} + +export const Range = Schema.Struct({ + start: Schema.Struct({ line: NonNegativeInt, character: NonNegativeInt }), + end: Schema.Struct({ line: NonNegativeInt, character: NonNegativeInt }), +}).annotate({ identifier: "Range" }) +export type Range = typeof Range.Type + +export const FileSource = Schema.Struct({ + ...filePartSourceBase, + type: Schema.Literal("file"), + path: Schema.String, +}).annotate({ identifier: "FileSource" }) + +export const SymbolSource = Schema.Struct({ + ...filePartSourceBase, + type: Schema.Literal("symbol"), + path: Schema.String, + range: Range, + name: Schema.String, + kind: NonNegativeInt, +}).annotate({ identifier: "SymbolSource" }) + +export const ResourceSource = Schema.Struct({ + ...filePartSourceBase, + type: Schema.Literal("resource"), + clientName: Schema.String, + uri: Schema.String, +}).annotate({ identifier: "ResourceSource" }) + +export const FilePartSource = Schema.Union([FileSource, SymbolSource, ResourceSource]).annotate({ + discriminator: "type", + identifier: "FilePartSource", +}) + +export const FilePart = Schema.Struct({ + ...partBase, + type: Schema.Literal("file"), + mime: Schema.String, + filename: Schema.optional(Schema.String), + url: Schema.String, + source: Schema.optional(FilePartSource), +}).annotate({ identifier: "FilePart" }) +export type FilePart = Types.DeepMutable> + +export const AgentPart = Schema.Struct({ + ...partBase, + type: Schema.Literal("agent"), + name: Schema.String, + source: Schema.optional( + Schema.Struct({ + value: Schema.String, + start: NonNegativeInt, + end: NonNegativeInt, + }), + ), +}).annotate({ identifier: "AgentPart" }) +export type AgentPart = Types.DeepMutable> + +export const SkillPart = Schema.Struct({ + ...partBase, + type: Schema.Literal("skill"), + name: Schema.String, + description: Schema.optional(Schema.String), + content: Schema.optional(Schema.String), + source: Schema.optional( + Schema.Struct({ + value: Schema.String, + start: NonNegativeInt, + end: NonNegativeInt, + }), + ), +}).annotate({ identifier: "SkillPart" }) +export type SkillPart = Types.DeepMutable> + +export const CompactionPart = Schema.Struct({ + ...partBase, + type: Schema.Literal("compaction"), + auto: Schema.Boolean, + overflow: Schema.optional(Schema.Boolean), + tail_start_id: Schema.optional(MessageID), +}).annotate({ identifier: "CompactionPart" }) +export type CompactionPart = Types.DeepMutable> + +export const SubtaskPart = Schema.Struct({ + ...partBase, + type: Schema.Literal("subtask"), + prompt: Schema.String, + description: Schema.String, + agent: Schema.String, + model: Schema.optional( + Schema.Struct({ + providerID: Provider.ID, + modelID: Model.ID, + }), + ), + command: Schema.optional(Schema.String), +}).annotate({ identifier: "SubtaskPart" }) +export type SubtaskPart = Types.DeepMutable> + +export const RetryPart = Schema.Struct({ + ...partBase, + type: Schema.Literal("retry"), + attempt: NonNegativeInt, + error: APIError.EffectSchema, + time: Schema.Struct({ + created: NonNegativeInt, + }), +}).annotate({ identifier: "RetryPart" }) +export type RetryPart = Omit>, "error"> & { + error: APIError +} + +export const StepStartPart = Schema.Struct({ + ...partBase, + type: Schema.Literal("step-start"), + snapshot: Schema.optional(Schema.String), +}).annotate({ identifier: "StepStartPart" }) +export type StepStartPart = Types.DeepMutable> + +export const StepFinishPart = Schema.Struct({ + ...partBase, + type: Schema.Literal("step-finish"), + reason: Schema.String, + snapshot: Schema.optional(Schema.String), + cost: Schema.Finite, + tokens: Schema.Struct({ + total: Schema.optional(Schema.Finite), + input: Schema.Finite, + output: Schema.Finite, + reasoning: Schema.Finite, + cache: Schema.Struct({ + read: Schema.Finite, + write: Schema.Finite, + }), + }), +}).annotate({ identifier: "StepFinishPart" }) +export type StepFinishPart = Types.DeepMutable> + +export const ToolStatePending = Schema.Struct({ + status: Schema.Literal("pending"), + input: Schema.Record(Schema.String, Schema.Any), + raw: Schema.String, +}).annotate({ identifier: "ToolStatePending" }) +export type ToolStatePending = Types.DeepMutable> + +export const ToolStateRunning = Schema.Struct({ + status: Schema.Literal("running"), + input: Schema.Record(Schema.String, Schema.Any), + title: Schema.optional(Schema.String), + metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)), + time: Schema.Struct({ + start: NonNegativeInt, + }), +}).annotate({ identifier: "ToolStateRunning" }) +export type ToolStateRunning = Types.DeepMutable> + +export const ToolStateCompleted = Schema.Struct({ + status: Schema.Literal("completed"), + input: Schema.Record(Schema.String, Schema.Any), + output: Schema.String, + title: Schema.String, + metadata: Schema.Record(Schema.String, Schema.Any), + time: Schema.Struct({ + start: NonNegativeInt, + end: NonNegativeInt, + compacted: Schema.optional(NonNegativeInt), + }), + attachments: Schema.optional(Schema.Array(FilePart)), +}).annotate({ identifier: "ToolStateCompleted" }) +export type ToolStateCompleted = Types.DeepMutable> + +export const ToolStateError = Schema.Struct({ + status: Schema.Literal("error"), + input: Schema.Record(Schema.String, Schema.Any), + error: Schema.String, + metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)), + time: Schema.Struct({ + start: NonNegativeInt, + end: NonNegativeInt, + }), +}).annotate({ identifier: "ToolStateError" }) +export type ToolStateError = Types.DeepMutable> + +export const ToolState = Schema.Union([ + ToolStatePending, + ToolStateRunning, + ToolStateCompleted, + ToolStateError, +]).annotate({ + discriminator: "status", + identifier: "ToolState", +}) +export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError + +export const ToolPart = Schema.Struct({ + ...partBase, + type: Schema.Literal("tool"), + callID: Schema.String, + tool: Schema.String, + state: ToolState, + metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)), +}).annotate({ identifier: "ToolPart" }) +export type ToolPart = Omit>, "state"> & { + state: ToolState +} + +const messageBase = { + id: MessageID, + sessionID: partBase.sessionID, +} + +export const User = Schema.Struct({ + ...messageBase, + role: Schema.Literal("user"), + time: Schema.Struct({ + created: Timestamp, + }), + format: Schema.optional(Format), + summary: Schema.optional( + Schema.Struct({ + title: Schema.optional(Schema.String), + body: Schema.optional(Schema.String), + diffs: Schema.Array(FileDiff.Info), + }), + ), + agent: Schema.String, + model: Schema.Struct({ + providerID: Provider.ID, + modelID: Model.ID, + variant: Schema.optional(Schema.String), + }), + system: Schema.optional(Schema.String), + tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)), +}).annotate({ identifier: "UserMessage" }) +export type User = Types.DeepMutable> + +export const Part = Schema.Union([ + TextPart, + SubtaskPart, + ReasoningPart, + FilePart, + ToolPart, + StepStartPart, + StepFinishPart, + SnapshotPart, + PatchPart, + AgentPart, + SkillPart, + RetryPart, + CompactionPart, +]).annotate({ discriminator: "type", identifier: "Part" }) +export type Part = + | TextPart + | SubtaskPart + | ReasoningPart + | FilePart + | ToolPart + | StepStartPart + | StepFinishPart + | SnapshotPart + | PatchPart + | AgentPart + | SkillPart + | RetryPart + | CompactionPart + +const AssistantErrorSchema = Schema.Union([ + AuthError.EffectSchema, + namedError("UnknownError", { message: Schema.String, ref: Schema.optional(Schema.String) }).EffectSchema, + OutputLengthError.EffectSchema, + AbortedError.EffectSchema, + StructuredOutputError.EffectSchema, + ContextOverflowError.EffectSchema, + ContentFilterError.EffectSchema, + APIError.EffectSchema, +]).annotate({ discriminator: "name" }) +type AssistantError = Schema.Schema.Type + +export const TextPartInput = Schema.Struct({ + id: Schema.optional(PartID), + type: Schema.Literal("text"), + text: Schema.String, + synthetic: Schema.optional(Schema.Boolean), + ignored: Schema.optional(Schema.Boolean), + time: Schema.optional( + Schema.Struct({ + start: NonNegativeInt, + end: Schema.optional(NonNegativeInt), + }), + ), + metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)), +}).annotate({ identifier: "TextPartInput" }) +export type TextPartInput = Types.DeepMutable> + +export const FilePartInput = Schema.Struct({ + id: Schema.optional(PartID), + type: Schema.Literal("file"), + mime: Schema.String, + filename: Schema.optional(Schema.String), + url: Schema.String, + source: Schema.optional(FilePartSource), +}).annotate({ identifier: "FilePartInput" }) +export type FilePartInput = Types.DeepMutable> + +export const AgentPartInput = Schema.Struct({ + id: Schema.optional(PartID), + type: Schema.Literal("agent"), + name: Schema.String, + source: Schema.optional( + Schema.Struct({ + value: Schema.String, + start: NonNegativeInt, + end: NonNegativeInt, + }), + ), +}).annotate({ identifier: "AgentPartInput" }) +export type AgentPartInput = Types.DeepMutable> + +export const SkillPartInput = Schema.Struct({ + id: Schema.optional(PartID), + type: Schema.Literal("skill"), + name: Schema.String, + description: Schema.optional(Schema.String), + content: Schema.optional(Schema.String), + source: Schema.optional( + Schema.Struct({ + value: Schema.String, + start: NonNegativeInt, + end: NonNegativeInt, + }), + ), +}).annotate({ identifier: "SkillPartInput" }) +export type SkillPartInput = Types.DeepMutable> + +export const SubtaskPartInput = Schema.Struct({ + id: Schema.optional(PartID), + type: Schema.Literal("subtask"), + prompt: Schema.String, + description: Schema.String, + agent: Schema.String, + model: Schema.optional( + Schema.Struct({ + providerID: Provider.ID, + modelID: Model.ID, + }), + ), + command: Schema.optional(Schema.String), +}).annotate({ identifier: "SubtaskPartInput" }) +export type SubtaskPartInput = Types.DeepMutable> + +export const Assistant = Schema.Struct({ + ...messageBase, + role: Schema.Literal("assistant"), + time: Schema.Struct({ + created: NonNegativeInt, + completed: Schema.optional(NonNegativeInt), + }), + error: Schema.optional(AssistantErrorSchema), + parentID: MessageID, + modelID: Model.ID, + providerID: Provider.ID, + mode: Schema.String, + agent: Schema.String, + path: Schema.Struct({ + cwd: Schema.String, + root: Schema.String, + }), + summary: Schema.optional(Schema.Boolean), + cost: Schema.Finite, + tokens: Schema.Struct({ + total: Schema.optional(Schema.Finite), + input: Schema.Finite, + output: Schema.Finite, + reasoning: Schema.Finite, + cache: Schema.Struct({ + read: Schema.Finite, + write: Schema.Finite, + }), + }), + structured: Schema.optional(Schema.Any), + variant: Schema.optional(Schema.String), + finish: Schema.optional(Schema.String), +}).annotate({ identifier: "AssistantMessage" }) +export type Assistant = Omit>, "error"> & { + error?: AssistantError +} + +export const Info = Schema.Union([User, Assistant]).annotate({ discriminator: "role", identifier: "Message" }) +export type Info = User | Assistant + +export const WithParts = Schema.Struct({ + info: Info, + parts: Schema.Array(Part), +}) +export type WithParts = { + info: Info + parts: Part[] +} + +const options = { + durable: { + aggregate: "sessionID", + version: 1, + }, +} as const + +const SessionSummary = Schema.Struct({ + additions: Schema.Finite, + deletions: Schema.Finite, + files: Schema.Finite, + diffs: optional(Schema.Array(FileDiff.Info)), +}) + +const SessionTokens = Schema.Struct({ + input: Schema.Finite, + output: Schema.Finite, + reasoning: Schema.Finite, + cache: Schema.Struct({ + read: Schema.Finite, + write: Schema.Finite, + }), +}) + +const SessionShare = Schema.Struct({ + url: Schema.String, +}) + +const SessionRevert = Schema.Struct({ + messageID: MessageID, + partID: optional(PartID), + snapshot: optional(Schema.String), + diff: optional(Schema.String), +}) + +const SessionModel = Schema.Struct({ + id: Model.ID, + providerID: Provider.ID, + variant: optional(Schema.String), +}) + +export const SessionInfo = Schema.Struct({ + id: SessionID, + slug: Schema.String, + projectID: Project.ID, + workspaceID: optional(WorkspaceID), + directory: Schema.String, + path: optional(Schema.String), + parentID: optional(SessionID), + summary: optional(SessionSummary), + cost: optional(Schema.Finite), + tokens: optional(SessionTokens), + share: optional(SessionShare), + title: Schema.String, + agent: optional(Schema.String), + model: optional(SessionModel), + version: Schema.String, + metadata: optional(Schema.Record(Schema.String, Schema.Any)), + time: Schema.Struct({ + created: NonNegativeInt, + updated: NonNegativeInt, + compacting: optional(NonNegativeInt), + archived: optional(Schema.Finite), + }), + permission: optional(PermissionV1.Ruleset), + revert: optional(SessionRevert), +}).annotate({ identifier: "Session" }) +export type SessionInfo = typeof SessionInfo.Type + +const events = { + Created: define({ + type: "session.created", + ...options, + schema: { + sessionID: SessionID, + info: SessionInfo, + }, + }), + Updated: define({ + type: "session.updated", + ...options, + schema: { + sessionID: SessionID, + info: SessionInfo, + }, + }), + Deleted: define({ + type: "session.deleted", + ...options, + schema: { + sessionID: SessionID, + info: SessionInfo, + }, + }), + MessageUpdated: define({ + type: "message.updated", + ...options, + schema: { + sessionID: SessionID, + info: Info, + }, + }), + MessageRemoved: define({ + type: "message.removed", + ...options, + schema: { + sessionID: SessionID, + messageID: MessageID, + }, + }), + PartUpdated: define({ + type: "message.part.updated", + ...options, + schema: { + sessionID: SessionID, + part: Part, + time: Schema.Finite, + }, + }), + PartRemoved: define({ + type: "message.part.removed", + ...options, + schema: { + sessionID: SessionID, + messageID: MessageID, + partID: PartID, + }, + }), +} + +export const PartDelta = define({ + type: "message.part.delta", + schema: { + sessionID: SessionID, + messageID: MessageID, + partID: PartID, + field: Schema.String, + delta: Schema.String, + }, +}) + +export const Diff = define({ + type: "session.diff", + schema: { + sessionID: SessionID, + diff: Schema.Array(FileDiff.Info), + }, +}) + +export const Error = define({ + type: "session.error", + schema: { + sessionID: Schema.optional(SessionID), + error: Assistant.fields.error, + }, +}) + +export const Event = { + ...events, + PartDelta, + Diff, + Error, + Definitions: inventory( + events.Created, + events.Updated, + events.Deleted, + events.MessageUpdated, + events.MessageRemoved, + events.PartUpdated, + events.PartRemoved, + PartDelta, + Diff, + Error, + ), +} diff --git a/packages/app-bundle/overlay/packages/schema/test/question.test.ts b/packages/app-bundle/overlay/packages/schema/test/question.test.ts new file mode 100644 index 00000000..099bc031 --- /dev/null +++ b/packages/app-bundle/overlay/packages/schema/test/question.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, test } from "bun:test" +import { Schema } from "effect" +import { Question } from "../src/question" +import { QuestionV1 } from "../src/question-v1" + +// Coverage for the question `kind` field (amicode#245): a Free-form Question +// carries kind: "text" and renders as a text card; a Choice Question omits it +// or carries kind: "choice". Back-compat: a payload WITHOUT kind decodes as a +// choice question; an UNKNOWN kind fails decoding loudly rather than silently +// degrading to choice. + +const choicePayload = { + question: "Which environment?", + header: "Env", + options: [ + { label: "Dev", description: "Development" }, + { label: "Prod", description: "Production" }, + ], +} + +const textPayload = { + question: "What should we call you?", + header: "Name", + options: [], + kind: "text", +} + +const shapes: { name: string; schema: Schema.Decoder }[] = [ + { name: "v1 Info (server)", schema: QuestionV1.Info }, + { name: "v1 Prompt (agent-facing)", schema: QuestionV1.Prompt }, + { name: "v2 Info (server)", schema: Question.Info }, + { name: "v2 Prompt (agent-facing)", schema: Question.Prompt }, +] + +describe.each(shapes)("question kind — $name", ({ schema }) => { + const decode = Schema.decodeUnknownSync(schema) + const kindOf = (input: unknown) => (decode(input) as { kind?: string }).kind + + test("a payload without kind decodes (back-compat: a choice question)", () => { + expect(kindOf(choicePayload)).toBeUndefined() + }) + + test("an explicit choice kind decodes", () => { + expect(kindOf({ ...choicePayload, kind: "choice" })).toBe("choice") + }) + + test("a text kind decodes", () => { + expect(kindOf(textPayload)).toBe("text") + }) + + test("an unknown kind fails decoding loudly", () => { + expect(() => decode({ ...choicePayload, kind: "essay" })).toThrow() + }) +}) diff --git a/packages/app-bundle/overlay/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/app-bundle/overlay/packages/sdk/js/src/v2/gen/sdk.gen.ts new file mode 100644 index 00000000..2536ea18 --- /dev/null +++ b/packages/app-bundle/overlay/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -0,0 +1,7295 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { client } from "./client.gen.js" +import { buildClientParams, type Client, type Options as Options2, type TDataShape } from "./client/index.js" +import type { + AgentPartInput, + AppAgentsErrors, + AppAgentsResponses, + AppLogErrors, + AppLogResponses, + AppSkillsErrors, + AppSkillsResponses, + Auth as Auth3, + AuthRemoveErrors, + AuthRemoveResponses, + AuthSetErrors, + AuthSetResponses, + CommandListErrors, + CommandListResponses, + Config as Config3, + ConfigGetErrors, + ConfigGetResponses, + ConfigProvidersErrors, + ConfigProvidersResponses, + ConfigUpdateErrors, + ConfigUpdateResponses, + EventSubscribeResponses, + EventTuiCommandExecute, + EventTuiPromptAppend, + EventTuiSessionSelect, + EventTuiToastShow, + ExperimentalCapabilitiesGetErrors, + ExperimentalCapabilitiesGetResponses, + ExperimentalConsoleGetErrors, + ExperimentalConsoleGetResponses, + ExperimentalConsoleListOrgsErrors, + ExperimentalConsoleListOrgsResponses, + ExperimentalConsoleSwitchOrgResponses, + ExperimentalControlPlaneMoveSessionErrors, + ExperimentalControlPlaneMoveSessionResponses, + ExperimentalProjectCopyGenerateNameErrors, + ExperimentalProjectCopyGenerateNameResponses, + ExperimentalResourceListErrors, + ExperimentalResourceListResponses, + ExperimentalSessionBackgroundErrors, + ExperimentalSessionBackgroundResponses, + ExperimentalSessionListErrors, + ExperimentalSessionListResponses, + ExperimentalWorkspaceAdapterListErrors, + ExperimentalWorkspaceAdapterListResponses, + ExperimentalWorkspaceCreateErrors, + ExperimentalWorkspaceCreateResponses, + ExperimentalWorkspaceListErrors, + ExperimentalWorkspaceListResponses, + ExperimentalWorkspaceRemoveErrors, + ExperimentalWorkspaceRemoveResponses, + ExperimentalWorkspaceStatusErrors, + ExperimentalWorkspaceStatusResponses, + ExperimentalWorkspaceSyncListErrors, + ExperimentalWorkspaceSyncListResponses, + ExperimentalWorkspaceWarpErrors, + ExperimentalWorkspaceWarpResponses, + FileListErrors, + FileListResponses, + FilePartInput, + FilePartSource, + FileReadErrors, + FileReadResponses, + FileStatusErrors, + FileStatusResponses, + FileWriteErrors, + FileWriteResponses, + FindFilesErrors, + FindFilesResponses, + FindSymbolsErrors, + FindSymbolsResponses, + FindTextErrors, + FindTextResponses, + FormatterStatusErrors, + FormatterStatusResponses, + GlobalConfigGetErrors, + GlobalConfigGetResponses, + GlobalConfigUpdateErrors, + GlobalConfigUpdateResponses, + GlobalDisposeErrors, + GlobalDisposeResponses, + GlobalEventErrors, + GlobalEventResponses, + GlobalHealthErrors, + GlobalHealthResponses, + GlobalUpgradeErrors, + GlobalUpgradeResponses, + InstanceDisposeErrors, + InstanceDisposeResponses, + LocationRef, + LspStatusErrors, + LspStatusResponses, + McpAddErrors, + McpAddResponses, + McpAuthAuthenticateErrors, + McpAuthAuthenticateResponses, + McpAuthCallbackErrors, + McpAuthCallbackResponses, + McpAuthRemoveErrors, + McpAuthRemoveResponses, + McpAuthStartErrors, + McpAuthStartResponses, + McpConnectErrors, + McpConnectResponses, + McpDisconnectErrors, + McpDisconnectResponses, + McpLocalConfig, + McpRemoteConfig, + McpStatusErrors, + McpStatusResponses, + ModelRef, + MoveSessionDestination, + OutputFormat, + Part as Part2, + PartDeleteErrors, + PartDeleteResponses, + PartUpdateErrors, + PartUpdateResponses, + PathGetErrors, + PathGetResponses, + PermissionListErrors, + PermissionListResponses, + PermissionReplyErrors, + PermissionReplyResponses, + PermissionRespondErrors, + PermissionRespondResponses, + PermissionRuleset, + PermissionV2Reply, + PermissionV2Source, + ProjectCommands, + ProjectCurrentErrors, + ProjectCurrentResponses, + ProjectDirectoriesErrors, + ProjectDirectoriesResponses, + ProjectIcon, + ProjectInitGitErrors, + ProjectInitGitResponses, + ProjectListErrors, + ProjectListResponses, + ProjectUpdateErrors, + ProjectUpdateResponses, + PromptInput, + ProviderAuthErrors, + ProviderAuthResponses, + ProviderListErrors, + ProviderListResponses, + ProviderOauthAuthorizeErrors, + ProviderOauthAuthorizeResponses, + ProviderOauthCallbackErrors, + ProviderOauthCallbackResponses, + PtyConnectErrors, + PtyConnectResponses, + PtyConnectTokenErrors, + PtyConnectTokenResponses, + PtyCreateErrors, + PtyCreateResponses, + PtyGetErrors, + PtyGetResponses, + PtyListErrors, + PtyListResponses, + PtyRemoveErrors, + PtyRemoveResponses, + PtyShellsErrors, + PtyShellsResponses, + PtyUpdateErrors, + PtyUpdateResponses, + QuestionAnswer, + QuestionListErrors, + QuestionListResponses, + QuestionRejectErrors, + QuestionRejectResponses, + QuestionReplyErrors, + QuestionReplyResponses, + QuestionV2Reply, + SessionAbortErrors, + SessionAbortResponses, + SessionChildrenErrors, + SessionChildrenResponses, + SessionCommandErrors, + SessionCommandResponses, + SessionCreateErrors, + SessionCreateResponses, + SessionDeleteErrors, + SessionDeleteMessageErrors, + SessionDeleteMessageResponses, + SessionDeleteResponses, + SessionDiffErrors, + SessionDiffResponses, + SessionForkErrors, + SessionForkResponses, + SessionGetErrors, + SessionGetResponses, + SessionInitErrors, + SessionInitResponses, + SessionListErrors, + SessionListResponses, + SessionMessageErrors, + SessionMessageResponses, + SessionMessagesErrors, + SessionMessagesResponses, + SessionPromptAsyncErrors, + SessionPromptAsyncResponses, + SessionPromptErrors, + SessionPromptResponses, + SessionRevertErrors, + SessionRevertResponses, + SessionShareErrors, + SessionShareResponses, + SessionShellErrors, + SessionShellResponses, + SessionStatusErrors, + SessionStatusResponses, + SessionSummarizeErrors, + SessionSummarizeResponses, + SessionTodoErrors, + SessionTodoResponses, + SessionTouchedFilesErrors, + SessionTouchedFilesResponses, + SessionUnrevertErrors, + SessionUnrevertResponses, + SessionUnshareErrors, + SessionUnshareResponses, + SessionUpdateErrors, + SessionUpdateResponses, + SkillPartInput, + SubtaskPartInput, + SyncHistoryListErrors, + SyncHistoryListResponses, + SyncReplayErrors, + SyncReplayResponses, + SyncStartErrors, + SyncStartResponses, + SyncStealErrors, + SyncStealResponses, + TextPartInput, + ToolIdsErrors, + ToolIdsResponses, + ToolListErrors, + ToolListResponses, + TuiAppendPromptErrors, + TuiAppendPromptResponses, + TuiClearPromptErrors, + TuiClearPromptResponses, + TuiControlNextErrors, + TuiControlNextResponses, + TuiControlResponseErrors, + TuiControlResponseResponses, + TuiExecuteCommandErrors, + TuiExecuteCommandResponses, + TuiOpenHelpErrors, + TuiOpenHelpResponses, + TuiOpenModelsErrors, + TuiOpenModelsResponses, + TuiOpenSessionsErrors, + TuiOpenSessionsResponses, + TuiOpenThemesErrors, + TuiOpenThemesResponses, + TuiPublishErrors, + TuiPublishResponses, + TuiSelectSessionErrors, + TuiSelectSessionResponses, + TuiShowToastErrors, + TuiShowToastResponses, + TuiSubmitPromptErrors, + TuiSubmitPromptResponses, + V2AgentListErrors, + V2AgentListResponses, + V2CommandListErrors, + V2CommandListResponses, + V2CredentialRemoveErrors, + V2CredentialRemoveResponses, + V2CredentialUpdateErrors, + V2CredentialUpdateResponses, + V2EventSubscribeErrors, + V2EventSubscribeResponses, + V2FsFindErrors, + V2FsFindResponses, + V2FsListErrors, + V2FsListResponses, + V2FsReadErrors, + V2FsReadResponses, + V2HealthGetErrors, + V2HealthGetResponses, + V2IntegrationAttemptCancelErrors, + V2IntegrationAttemptCancelResponses, + V2IntegrationAttemptCompleteErrors, + V2IntegrationAttemptCompleteResponses, + V2IntegrationAttemptStatusErrors, + V2IntegrationAttemptStatusResponses, + V2IntegrationConnectKeyErrors, + V2IntegrationConnectKeyResponses, + V2IntegrationConnectOauthErrors, + V2IntegrationConnectOauthResponses, + V2IntegrationGetErrors, + V2IntegrationGetResponses, + V2IntegrationListErrors, + V2IntegrationListResponses, + V2LocationGetErrors, + V2LocationGetResponses, + V2ModelListErrors, + V2ModelListResponses, + V2PermissionRequestListErrors, + V2PermissionRequestListResponses, + V2PermissionSavedListErrors, + V2PermissionSavedListResponses, + V2PermissionSavedRemoveErrors, + V2PermissionSavedRemoveResponses, + V2ProjectCopyCreateErrors, + V2ProjectCopyCreateResponses, + V2ProjectCopyRefreshErrors, + V2ProjectCopyRefreshResponses, + V2ProjectCopyRemoveErrors, + V2ProjectCopyRemoveResponses, + V2ProviderGetErrors, + V2ProviderGetResponses, + V2ProviderListErrors, + V2ProviderListResponses, + V2PtyConnectErrors, + V2PtyConnectResponses, + V2PtyConnectTokenErrors, + V2PtyConnectTokenResponses, + V2PtyCreateErrors, + V2PtyCreateResponses, + V2PtyGetErrors, + V2PtyGetResponses, + V2PtyListErrors, + V2PtyListResponses, + V2PtyRemoveErrors, + V2PtyRemoveResponses, + V2PtyUpdateErrors, + V2PtyUpdateResponses, + V2QuestionRequestListErrors, + V2QuestionRequestListResponses, + V2ReferenceListErrors, + V2ReferenceListResponses, + V2SessionActiveErrors, + V2SessionActiveResponses, + V2SessionCompactErrors, + V2SessionCompactResponses, + V2SessionContextErrors, + V2SessionContextResponses, + V2SessionCreateErrors, + V2SessionCreateResponses, + V2SessionEventsErrors, + V2SessionEventsResponses, + V2SessionGetErrors, + V2SessionGetResponses, + V2SessionHistoryErrors, + V2SessionHistoryResponses, + V2SessionInterruptErrors, + V2SessionInterruptResponses, + V2SessionListErrors, + V2SessionListResponses, + V2SessionMessageErrors, + V2SessionMessageResponses, + V2SessionMessagesErrors, + V2SessionMessagesResponses, + V2SessionPermissionCreateErrors, + V2SessionPermissionCreateResponses, + V2SessionPermissionGetErrors, + V2SessionPermissionGetResponses, + V2SessionPermissionListErrors, + V2SessionPermissionListResponses, + V2SessionPermissionReplyErrors, + V2SessionPermissionReplyResponses, + V2SessionPromptErrors, + V2SessionPromptResponses, + V2SessionQuestionListErrors, + V2SessionQuestionListResponses, + V2SessionQuestionRejectErrors, + V2SessionQuestionRejectResponses, + V2SessionQuestionReplyErrors, + V2SessionQuestionReplyResponses, + V2SessionRevertClearErrors, + V2SessionRevertClearResponses, + V2SessionRevertCommitErrors, + V2SessionRevertCommitResponses, + V2SessionRevertStageErrors, + V2SessionRevertStageResponses, + V2SessionSwitchAgentErrors, + V2SessionSwitchAgentResponses, + V2SessionSwitchModelErrors, + V2SessionSwitchModelResponses, + V2SessionWaitErrors, + V2SessionWaitResponses, + V2SkillListErrors, + V2SkillListResponses, + VcsApplyErrors, + VcsApplyResponses, + VcsDiffErrors, + VcsDiffRawErrors, + VcsDiffRawResponses, + VcsDiffResponses, + VcsGetErrors, + VcsGetResponses, + VcsStatusErrors, + VcsStatusResponses, + WorktreeCreateErrors, + WorktreeCreateInput, + WorktreeCreateResponses, + WorktreeListErrors, + WorktreeListResponses, + WorktreeRemoveErrors, + WorktreeRemoveInput, + WorktreeRemoveResponses, + WorktreeResetErrors, + WorktreeResetInput, + WorktreeResetResponses, +} from "./types.gen.js" + +export type Options = Options2< + TData, + ThrowOnError +> & { + /** + * You can provide a client instance returned by `createClient()` instead of + * individual options. This might be also useful if you want to implement a + * custom client. + */ + client?: Client + /** + * You can pass arbitrary values through the `meta` object. This can be + * used to access values that aren't defined as part of the SDK function. + */ + meta?: Record +} + +class HeyApiClient { + protected client: Client + + constructor(args?: { client?: Client }) { + this.client = args?.client ?? client + } +} + +class HeyApiRegistry { + private readonly defaultKey = "default" + + private readonly instances: Map = new Map() + + get(key?: string): T { + const instance = this.instances.get(key ?? this.defaultKey) + if (!instance) { + throw new Error(`No SDK client found. Create one with "new OpencodeClient()" to fix this error.`) + } + return instance + } + + set(value: T, key?: string): void { + this.instances.set(key ?? this.defaultKey, value) + } +} + +export class Auth extends HeyApiClient { + /** + * Remove auth credentials + * + * Remove authentication credentials + */ + public remove( + parameters: { + providerID: string + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "providerID" }] }]) + return (options?.client ?? this.client).delete({ + url: "/auth/{providerID}", + ...options, + ...params, + }) + } + + /** + * Set auth credentials + * + * Set authentication credentials + */ + public set( + parameters: { + providerID: string + auth?: Auth3 + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "providerID" }, + { key: "auth", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).put({ + url: "/auth/{providerID}", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class App extends HeyApiClient { + /** + * Write log + * + * Write a log entry to the server logs with specified level and metadata. + */ + public log( + parameters?: { + directory?: string + workspace?: string + service?: string + level?: "debug" | "info" | "error" | "warn" + message?: string + extra?: { + [key: string]: unknown + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "service" }, + { in: "body", key: "level" }, + { in: "body", key: "message" }, + { in: "body", key: "extra" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/log", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * List agents + * + * Get a list of all available AI agents in the OpenCode system. + */ + public agents( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/agent", + ...options, + ...params, + }) + } + + /** + * List skills + * + * Get a list of all available skills in the OpenCode system. + */ + public skills( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/skill", + ...options, + ...params, + }) + } +} + +export class ControlPlane extends HeyApiClient { + /** + * Move session + * + * Move a session to another project directory, optionally transferring local changes. + */ + public moveSession( + parameters?: { + sessionID?: string + destination?: MoveSessionDestination + moveChanges?: boolean + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "body", key: "sessionID" }, + { in: "body", key: "destination" }, + { in: "body", key: "moveChanges" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + ExperimentalControlPlaneMoveSessionResponses, + ExperimentalControlPlaneMoveSessionErrors, + ThrowOnError + >({ + url: "/experimental/control-plane/move-session", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class Capabilities extends HeyApiClient { + /** + * Get experimental capabilities + * + * Get experimental features enabled on the OpenCode server. + */ + public get( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get< + ExperimentalCapabilitiesGetResponses, + ExperimentalCapabilitiesGetErrors, + ThrowOnError + >({ + url: "/experimental/capabilities", + ...options, + ...params, + }) + } +} + +export class Console extends HeyApiClient { + /** + * Get active Console provider metadata + * + * Get the active Console org name and the set of provider IDs managed by that Console org. + */ + public get( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get< + ExperimentalConsoleGetResponses, + ExperimentalConsoleGetErrors, + ThrowOnError + >({ + url: "/experimental/console", + ...options, + ...params, + }) + } + + /** + * List switchable Console orgs + * + * Get the available Console orgs across logged-in accounts, including the current active org. + */ + public listOrgs( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get< + ExperimentalConsoleListOrgsResponses, + ExperimentalConsoleListOrgsErrors, + ThrowOnError + >({ + url: "/experimental/console/orgs", + ...options, + ...params, + }) + } + + /** + * Switch active Console org + * + * Persist a new active Console account/org selection for the current local OpenCode state. + */ + public switchOrg( + parameters?: { + directory?: string + workspace?: string + accountID?: string + orgID?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "accountID" }, + { in: "body", key: "orgID" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/experimental/console/switch", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class Session extends HeyApiClient { + /** + * List sessions + * + * Get a list of all OpenCode sessions across projects, sorted by most recently updated. Archived sessions are excluded by default. + */ + public list( + parameters?: { + directory?: string + workspace?: string + roots?: boolean | "true" | "false" + start?: number + cursor?: number + search?: string + limit?: number + archived?: boolean | "true" | "false" + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "query", key: "roots" }, + { in: "query", key: "start" }, + { in: "query", key: "cursor" }, + { in: "query", key: "search" }, + { in: "query", key: "limit" }, + { in: "query", key: "archived" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get< + ExperimentalSessionListResponses, + ExperimentalSessionListErrors, + ThrowOnError + >({ + url: "/experimental/session", + ...options, + ...params, + }) + } + + /** + * Background subagents + * + * Detach any synchronous subagents currently blocking the session and continue them in the background. + */ + public background( + parameters: { + sessionID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + ExperimentalSessionBackgroundResponses, + ExperimentalSessionBackgroundErrors, + ThrowOnError + >({ + url: "/experimental/session/{sessionID}/background", + ...options, + ...params, + }) + } +} + +export class Resource extends HeyApiClient { + /** + * Get MCP resources + * + * Get all available MCP resources from connected servers. Optionally filter by name. + */ + public list( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get< + ExperimentalResourceListResponses, + ExperimentalResourceListErrors, + ThrowOnError + >({ + url: "/experimental/resource", + ...options, + ...params, + }) + } +} + +export class ProjectCopy extends HeyApiClient { + /** + * Generate project copy name + * + * Generate a short name for a project copy from task context. + */ + public generateName( + parameters: { + projectID: string + directory?: string + workspace?: string + context?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "projectID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "context" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + ExperimentalProjectCopyGenerateNameResponses, + ExperimentalProjectCopyGenerateNameErrors, + ThrowOnError + >({ + url: "/experimental/project/{projectID}/copy/generate-name", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class Adapter extends HeyApiClient { + /** + * List workspace adapters + * + * List all available workspace adapters for the current project. + */ + public list( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get< + ExperimentalWorkspaceAdapterListResponses, + ExperimentalWorkspaceAdapterListErrors, + ThrowOnError + >({ + url: "/experimental/workspace/adapter", + ...options, + ...params, + }) + } +} + +export class Workspace extends HeyApiClient { + /** + * List workspaces + * + * List all workspaces. + */ + public list( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get< + ExperimentalWorkspaceListResponses, + ExperimentalWorkspaceListErrors, + ThrowOnError + >({ + url: "/experimental/workspace", + ...options, + ...params, + }) + } + + /** + * Create workspace + * + * Create a workspace for the current project. + */ + public create( + parameters?: { + directory?: string + workspace?: string + id?: string + type?: string + branch?: string | null + extra?: unknown | null + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "id" }, + { in: "body", key: "type" }, + { in: "body", key: "branch" }, + { in: "body", key: "extra" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + ExperimentalWorkspaceCreateResponses, + ExperimentalWorkspaceCreateErrors, + ThrowOnError + >({ + url: "/experimental/workspace", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Sync workspace list + * + * Register missing workspaces returned by workspace adapters. + */ + public syncList( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + ExperimentalWorkspaceSyncListResponses, + ExperimentalWorkspaceSyncListErrors, + ThrowOnError + >({ + url: "/experimental/workspace/sync-list", + ...options, + ...params, + }) + } + + /** + * Workspace status + * + * Get connection status for workspaces in the current project. + */ + public status( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get< + ExperimentalWorkspaceStatusResponses, + ExperimentalWorkspaceStatusErrors, + ThrowOnError + >({ + url: "/experimental/workspace/status", + ...options, + ...params, + }) + } + + /** + * Remove workspace + * + * Remove an existing workspace. + */ + public remove( + parameters: { + id: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "id" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).delete< + ExperimentalWorkspaceRemoveResponses, + ExperimentalWorkspaceRemoveErrors, + ThrowOnError + >({ + url: "/experimental/workspace/{id}", + ...options, + ...params, + }) + } + + /** + * Warp session into workspace + * + * Move a session's sync history into the target workspace, or detach it to the local project. + */ + public warp( + parameters?: { + directory?: string + workspace?: string + id?: string | null + sessionID?: string + copyChanges?: boolean + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "id" }, + { in: "body", key: "sessionID" }, + { in: "body", key: "copyChanges" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + ExperimentalWorkspaceWarpResponses, + ExperimentalWorkspaceWarpErrors, + ThrowOnError + >({ + url: "/experimental/workspace/warp", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + private _adapter?: Adapter + get adapter(): Adapter { + return (this._adapter ??= new Adapter({ client: this.client })) + } +} + +export class Experimental extends HeyApiClient { + private _controlPlane?: ControlPlane + get controlPlane(): ControlPlane { + return (this._controlPlane ??= new ControlPlane({ client: this.client })) + } + + private _capabilities?: Capabilities + get capabilities(): Capabilities { + return (this._capabilities ??= new Capabilities({ client: this.client })) + } + + private _console?: Console + get console(): Console { + return (this._console ??= new Console({ client: this.client })) + } + + private _session?: Session + get session(): Session { + return (this._session ??= new Session({ client: this.client })) + } + + private _resource?: Resource + get resource(): Resource { + return (this._resource ??= new Resource({ client: this.client })) + } + + private _projectCopy?: ProjectCopy + get projectCopy(): ProjectCopy { + return (this._projectCopy ??= new ProjectCopy({ client: this.client })) + } + + private _workspace?: Workspace + get workspace(): Workspace { + return (this._workspace ??= new Workspace({ client: this.client })) + } +} + +export class Config extends HeyApiClient { + /** + * Get global configuration + * + * Retrieve the current global OpenCode configuration settings and preferences. + */ + public get(options?: Options) { + return (options?.client ?? this.client).get({ + url: "/global/config", + ...options, + }) + } + + /** + * Update global configuration + * + * Update global OpenCode configuration settings and preferences. + */ + public update( + parameters?: { + config?: Config3 + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ key: "config", map: "body" }] }]) + return (options?.client ?? this.client).patch({ + url: "/global/config", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class Global extends HeyApiClient { + /** + * Get health + * + * Get health information about the OpenCode server. + */ + public health(options?: Options) { + return (options?.client ?? this.client).get({ + url: "/global/health", + ...options, + }) + } + + /** + * Get global events + * + * Subscribe to global events from the OpenCode system using server-sent events. + */ + public event(options?: Options) { + return (options?.client ?? this.client).sse.get({ + url: "/global/event", + ...options, + }) + } + + /** + * Dispose instance + * + * Clean up and dispose all OpenCode instances, releasing all resources. + */ + public dispose(options?: Options) { + return (options?.client ?? this.client).post({ + url: "/global/dispose", + ...options, + }) + } + + /** + * Upgrade opencode + * + * Upgrade opencode to the specified version or latest if not specified. + */ + public upgrade( + parameters?: { + target?: string + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "body", key: "target" }] }]) + return (options?.client ?? this.client).post({ + url: "/global/upgrade", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + private _config?: Config + get config(): Config { + return (this._config ??= new Config({ client: this.client })) + } +} + +export class Event extends HeyApiClient { + /** + * Subscribe to events + * + * Get events + */ + public subscribe( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).sse.get({ + url: "/event", + ...options, + ...params, + }) + } +} + +export class Config2 extends HeyApiClient { + /** + * Get configuration + * + * Retrieve the current OpenCode configuration settings and preferences. + */ + public get( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/config", + ...options, + ...params, + }) + } + + /** + * Update configuration + * + * Update OpenCode configuration settings and preferences. + */ + public update( + parameters?: { + directory?: string + workspace?: string + config?: Config3 + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { key: "config", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).patch({ + url: "/config", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * List config providers + * + * Get a list of all configured AI providers and their default models. + */ + public providers( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/config/providers", + ...options, + ...params, + }) + } +} + +export class Tool extends HeyApiClient { + /** + * List tools + * + * Get a list of available tools with their JSON schema parameters for a specific provider and model combination. + */ + public list( + parameters: { + directory?: string + workspace?: string + provider: string + model: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "query", key: "provider" }, + { in: "query", key: "model" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/experimental/tool", + ...options, + ...params, + }) + } + + /** + * List tool IDs + * + * Get a list of all available tool IDs, including both built-in tools and dynamically registered tools. + */ + public ids( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/experimental/tool/ids", + ...options, + ...params, + }) + } +} + +export class Worktree extends HeyApiClient { + /** + * Remove worktree + * + * Remove a git worktree and delete its branch. + */ + public remove( + parameters?: { + directory?: string + workspace?: string + worktreeRemoveInput?: WorktreeRemoveInput + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { key: "worktreeRemoveInput", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).delete({ + url: "/experimental/worktree", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * List worktrees + * + * List all sandbox worktrees for the current project. + */ + public list( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/experimental/worktree", + ...options, + ...params, + }) + } + + /** + * Create worktree + * + * Create a new git worktree for the current project and run any configured startup scripts. + */ + public create( + parameters?: { + directory?: string + workspace?: string + worktreeCreateInput?: WorktreeCreateInput + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { key: "worktreeCreateInput", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/experimental/worktree", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Reset worktree + * + * Reset a worktree branch to the primary default branch. + */ + public reset( + parameters?: { + directory?: string + workspace?: string + worktreeResetInput?: WorktreeResetInput + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { key: "worktreeResetInput", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/experimental/worktree/reset", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class Find extends HeyApiClient { + /** + * Find text + * + * Search for text patterns across files in the project using ripgrep. + */ + public text( + parameters: { + directory?: string + workspace?: string + pattern: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "query", key: "pattern" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/find", + ...options, + ...params, + }) + } + + /** + * Find files + * + * Search for files or directories by name or pattern in the project directory. + */ + public files( + parameters: { + directory?: string + workspace?: string + query: string + dirs?: "true" | "false" + type?: "file" | "directory" + limit?: number + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "query", key: "query" }, + { in: "query", key: "dirs" }, + { in: "query", key: "type" }, + { in: "query", key: "limit" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/find/file", + ...options, + ...params, + }) + } + + /** + * Find symbols + * + * Search for workspace symbols like functions, classes, and variables using LSP. + */ + public symbols( + parameters: { + directory?: string + workspace?: string + query: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "query", key: "query" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/find/symbol", + ...options, + ...params, + }) + } +} + +export class File extends HeyApiClient { + /** + * List files + * + * List files and directories in a specified path. + */ + public list( + parameters: { + directory?: string + workspace?: string + path: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "query", key: "path" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/file", + ...options, + ...params, + }) + } + + /** + * Read file + * + * Read the content of a specified file. + */ + public read( + parameters: { + directory?: string + workspace?: string + path: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "query", key: "path" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/file/content", + ...options, + ...params, + }) + } + + /** + * Get file status + * + * Get the git status of all files in the project. + */ + public status( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/file/status", + ...options, + ...params, + }) + } + + /** + * Write file + * + * Write content to a file on disk. Used by the Preview tab's raw editor. + */ + public write( + parameters?: { + directory?: string + workspace?: string + path?: string + content?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "path" }, + { in: "body", key: "content" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/file/write", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class Instance extends HeyApiClient { + /** + * Dispose instance + * + * Clean up and dispose the current OpenCode instance, releasing all resources. + */ + public dispose( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/instance/dispose", + ...options, + ...params, + }) + } +} + +export class Path extends HeyApiClient { + /** + * Get paths + * + * Retrieve the current working directory and related path information for the OpenCode instance. + */ + public get( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/path", + ...options, + ...params, + }) + } +} + +export class Diff extends HeyApiClient { + /** + * Get raw VCS diff + * + * Retrieve a raw patch for current uncommitted changes. + */ + public raw( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/vcs/diff/raw", + ...options, + ...params, + }) + } +} + +export class Vcs extends HeyApiClient { + /** + * Get VCS info + * + * Retrieve version control system (VCS) information for the current project, such as git branch. + */ + public get( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/vcs", + ...options, + ...params, + }) + } + + /** + * Get VCS status + * + * Retrieve changed files in the current working tree without patches. + */ + public status( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/vcs/status", + ...options, + ...params, + }) + } + + /** + * Get VCS diff + * + * Retrieve the current git diff for the working tree or against the default branch. + */ + public diff( + parameters: { + directory?: string + workspace?: string + mode: "git" | "branch" + context?: number + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "query", key: "mode" }, + { in: "query", key: "context" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/vcs/diff", + ...options, + ...params, + }) + } + + /** + * Apply VCS patch + * + * Apply a raw patch to the current working tree. + */ + public apply( + parameters?: { + directory?: string + workspace?: string + patch?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "patch" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/vcs/apply", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + private _diff?: Diff + get diff2(): Diff { + return (this._diff ??= new Diff({ client: this.client })) + } +} + +export class Command extends HeyApiClient { + /** + * List commands + * + * Get a list of all available commands in the OpenCode system. + */ + public list( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/command", + ...options, + ...params, + }) + } +} + +export class Lsp extends HeyApiClient { + /** + * Get LSP status + * + * Get LSP server status + */ + public status( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/lsp", + ...options, + ...params, + }) + } +} + +export class Formatter extends HeyApiClient { + /** + * Get formatter status + * + * Get formatter status + */ + public status( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/formatter", + ...options, + ...params, + }) + } +} + +export class Auth2 extends HeyApiClient { + /** + * Remove MCP OAuth + * + * Remove OAuth credentials for an MCP server. + */ + public remove( + parameters: { + name: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "name" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).delete({ + url: "/mcp/{name}/auth", + ...options, + ...params, + }) + } + + /** + * Start MCP OAuth + * + * Start OAuth authentication flow for a Model Context Protocol (MCP) server. + */ + public start( + parameters: { + name: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "name" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/mcp/{name}/auth", + ...options, + ...params, + }) + } + + /** + * Complete MCP OAuth + * + * Complete OAuth authentication for a Model Context Protocol (MCP) server using the authorization code. + */ + public callback( + parameters: { + name: string + directory?: string + workspace?: string + code?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "name" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "code" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/mcp/{name}/auth/callback", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Authenticate MCP OAuth + * + * Start OAuth flow and wait for callback (opens browser). + */ + public authenticate( + parameters: { + name: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "name" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post( + { + url: "/mcp/{name}/auth/authenticate", + ...options, + ...params, + }, + ) + } +} + +export class Mcp extends HeyApiClient { + /** + * Get MCP status + * + * Get the status of all Model Context Protocol (MCP) servers. + */ + public status( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/mcp", + ...options, + ...params, + }) + } + + /** + * Add MCP server + * + * Dynamically add a new Model Context Protocol (MCP) server to the system. + */ + public add( + parameters?: { + directory?: string + workspace?: string + name?: string + config?: McpLocalConfig | McpRemoteConfig + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "name" }, + { in: "body", key: "config" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/mcp", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Connect an MCP server. + */ + public connect( + parameters: { + name: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "name" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/mcp/{name}/connect", + ...options, + ...params, + }) + } + + /** + * Disconnect an MCP server. + */ + public disconnect( + parameters: { + name: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "name" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/mcp/{name}/disconnect", + ...options, + ...params, + }) + } + + private _auth?: Auth2 + get auth(): Auth2 { + return (this._auth ??= new Auth2({ client: this.client })) + } +} + +export class Project extends HeyApiClient { + /** + * List all projects + * + * Get a list of projects that have been opened with OpenCode. + */ + public list( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/project", + ...options, + ...params, + }) + } + + /** + * Get current project + * + * Retrieve the currently active project that OpenCode is working with. + */ + public current( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/project/current", + ...options, + ...params, + }) + } + + /** + * Initialize git repository + * + * Create a git repository for the current project and return the refreshed project info. + */ + public initGit( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/project/git/init", + ...options, + ...params, + }) + } + + /** + * Update project + * + * Update project properties such as name, icon, and commands. + */ + public update( + parameters: { + projectID: string + directory?: string + workspace?: string + name?: string + icon?: ProjectIcon + commands?: ProjectCommands + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "projectID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "name" }, + { in: "body", key: "icon" }, + { in: "body", key: "commands" }, + ], + }, + ], + ) + return (options?.client ?? this.client).patch({ + url: "/project/{projectID}", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * List project directories + * + * List known local absolute directories for a project. + */ + public directories( + parameters: { + projectID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "projectID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/project/{projectID}/directories", + ...options, + ...params, + }) + } +} + +export class Pty extends HeyApiClient { + /** + * List available shells + * + * Get a list of available shells on the system. + */ + public shells( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/pty/shells", + ...options, + ...params, + }) + } + + /** + * List PTY sessions + * + * Get a list of all active pseudo-terminal (PTY) sessions managed by OpenCode. + */ + public list( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/pty", + ...options, + ...params, + }) + } + + /** + * Create PTY session + * + * Create a new pseudo-terminal (PTY) session for running shell commands and processes. + */ + public create( + parameters?: { + directory?: string + workspace?: string + command?: string + args?: Array + cwd?: string + title?: string + env?: { + [key: string]: string + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "command" }, + { in: "body", key: "args" }, + { in: "body", key: "cwd" }, + { in: "body", key: "title" }, + { in: "body", key: "env" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/pty", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Remove PTY session + * + * Remove and terminate a specific pseudo-terminal (PTY) session. + */ + public remove( + parameters: { + ptyID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "ptyID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).delete({ + url: "/pty/{ptyID}", + ...options, + ...params, + }) + } + + /** + * Get PTY session + * + * Retrieve detailed information about a specific pseudo-terminal (PTY) session. + */ + public get( + parameters: { + ptyID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "ptyID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/pty/{ptyID}", + ...options, + ...params, + }) + } + + /** + * Update PTY session + * + * Update properties of an existing pseudo-terminal (PTY) session. + */ + public update( + parameters: { + ptyID: string + directory?: string + workspace?: string + title?: string + size?: { + rows: number + cols: number + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "ptyID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "title" }, + { in: "body", key: "size" }, + ], + }, + ], + ) + return (options?.client ?? this.client).put({ + url: "/pty/{ptyID}", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Create PTY WebSocket token + * + * Create a short-lived ticket for opening a PTY WebSocket connection. + */ + public connectToken( + parameters: { + ptyID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "ptyID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/pty/{ptyID}/connect-token", + ...options, + ...params, + }) + } + + /** + * Connect to PTY session + * + * Establish a WebSocket connection to interact with a pseudo-terminal (PTY) session in real-time. + */ + public connect( + parameters: { + ptyID: string + directory?: string + workspace?: string + cursor?: string + ticket?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "ptyID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "query", key: "cursor" }, + { in: "query", key: "ticket" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/pty/{ptyID}/connect", + ...options, + ...params, + }) + } +} + +export class Question extends HeyApiClient { + /** + * List pending questions + * + * Get all pending question requests across all sessions. + */ + public list( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/question", + ...options, + ...params, + }) + } + + /** + * Reply to question request + * + * Provide answers to a question request from the AI assistant. + */ + public reply( + parameters: { + requestID: string + directory?: string + workspace?: string + answers?: Array + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "requestID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "answers" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/question/{requestID}/reply", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Reject question request + * + * Reject a question request from the AI assistant. + */ + public reject( + parameters: { + requestID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "requestID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/question/{requestID}/reject", + ...options, + ...params, + }) + } +} + +export class Permission extends HeyApiClient { + /** + * List pending permissions + * + * Get all pending permission requests across all sessions. + */ + public list( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/permission", + ...options, + ...params, + }) + } + + /** + * Respond to permission request + * + * Approve or deny a permission request from the AI assistant. + */ + public reply( + parameters: { + requestID: string + directory?: string + workspace?: string + reply?: "once" | "always" | "reject" + message?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "requestID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "reply" }, + { in: "body", key: "message" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/permission/{requestID}/reply", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Respond to permission + * + * Approve or deny a permission request from the AI assistant. + * + * @deprecated + */ + public respond( + parameters: { + sessionID: string + permissionID: string + directory?: string + workspace?: string + response?: "once" | "always" | "reject" + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "path", key: "permissionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "response" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/permissions/{permissionID}", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class Oauth extends HeyApiClient { + /** + * Start OAuth authorization + * + * Start the OAuth authorization flow for a provider. + */ + public authorize( + parameters: { + providerID: string + directory?: string + workspace?: string + method?: number + inputs?: { + [key: string]: string + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "providerID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "method" }, + { in: "body", key: "inputs" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + ProviderOauthAuthorizeResponses, + ProviderOauthAuthorizeErrors, + ThrowOnError + >({ + url: "/provider/{providerID}/oauth/authorize", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Handle OAuth callback + * + * Handle the OAuth callback from a provider after user authorization. + */ + public callback( + parameters: { + providerID: string + directory?: string + workspace?: string + method?: number + code?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "providerID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "method" }, + { in: "body", key: "code" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + ProviderOauthCallbackResponses, + ProviderOauthCallbackErrors, + ThrowOnError + >({ + url: "/provider/{providerID}/oauth/callback", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class Provider extends HeyApiClient { + /** + * List providers + * + * Get a list of all available AI providers, including both available and connected ones. + */ + public list( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/provider", + ...options, + ...params, + }) + } + + /** + * Get provider auth methods + * + * Retrieve available authentication methods for all AI providers. + */ + public auth( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/provider/auth", + ...options, + ...params, + }) + } + + private _oauth?: Oauth + get oauth(): Oauth { + return (this._oauth ??= new Oauth({ client: this.client })) + } +} + +export class Session2 extends HeyApiClient { + /** + * List sessions + * + * Get a list of all OpenCode sessions, sorted by most recently updated. + */ + public list( + parameters?: { + directory?: string + workspace?: string + scope?: "project" + path?: string + roots?: boolean | "true" | "false" + start?: number + search?: string + limit?: number + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "query", key: "scope" }, + { in: "query", key: "path" }, + { in: "query", key: "roots" }, + { in: "query", key: "start" }, + { in: "query", key: "search" }, + { in: "query", key: "limit" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/session", + ...options, + ...params, + }) + } + + /** + * Create session + * + * Create a new OpenCode session for interacting with AI assistants and managing conversations. + */ + public create( + parameters?: { + directory?: string + workspace?: string + parentID?: string + title?: string + agent?: string + model?: { + id: string + providerID: string + variant?: string + } + metadata?: { + [key: string]: unknown + } + permission?: PermissionRuleset + workspaceID?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "parentID" }, + { in: "body", key: "title" }, + { in: "body", key: "agent" }, + { in: "body", key: "model" }, + { in: "body", key: "metadata" }, + { in: "body", key: "permission" }, + { in: "body", key: "workspaceID" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Get session status + * + * Retrieve the current status of all sessions, including active, idle, and completed states. + */ + public status( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/session/status", + ...options, + ...params, + }) + } + + /** + * Delete session + * + * Delete a session and permanently remove all associated data, including messages and history. + */ + public delete( + parameters: { + sessionID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).delete({ + url: "/session/{sessionID}", + ...options, + ...params, + }) + } + + /** + * Get session + * + * Retrieve detailed information about a specific OpenCode session. + */ + public get( + parameters: { + sessionID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/session/{sessionID}", + ...options, + ...params, + }) + } + + /** + * Update session + * + * Update properties of an existing session, such as title or other metadata. + */ + public update( + parameters: { + sessionID: string + directory?: string + workspace?: string + title?: string + metadata?: { + [key: string]: unknown + } + permission?: PermissionRuleset + time?: { + archived?: number + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "title" }, + { in: "body", key: "metadata" }, + { in: "body", key: "permission" }, + { in: "body", key: "time" }, + ], + }, + ], + ) + return (options?.client ?? this.client).patch({ + url: "/session/{sessionID}", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Get session children + * + * Retrieve all child sessions that were forked from the specified parent session. + */ + public children( + parameters: { + sessionID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/session/{sessionID}/children", + ...options, + ...params, + }) + } + + /** + * Get session todos + * + * Retrieve the todo list associated with a specific session, showing tasks and action items. + */ + public todo( + parameters: { + sessionID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/session/{sessionID}/todo", + ...options, + ...params, + }) + } + + /** + * Get message diff + * + * Get the file changes (diff) that resulted from a specific user message in the session. + */ + public diff( + parameters: { + sessionID: string + directory?: string + workspace?: string + messageID?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "query", key: "messageID" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/session/{sessionID}/diff", + ...options, + ...params, + }) + } + + /** + * Get touched files + * + * Get all files that were edited or written by tools in this session, regardless of current git state. + */ + public touchedFiles( + parameters: { + sessionID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/session/{sessionID}/touched-files", + ...options, + ...params, + }) + } + + /** + * Get session messages + * + * Retrieve all messages in a session, including user prompts and AI responses. + */ + public messages( + parameters: { + sessionID: string + directory?: string + workspace?: string + limit?: number + before?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "query", key: "limit" }, + { in: "query", key: "before" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/session/{sessionID}/message", + ...options, + ...params, + }) + } + + /** + * Send message + * + * Create and send a new message to a session, streaming the AI response. + */ + public prompt( + parameters: { + sessionID: string + directory?: string + workspace?: string + messageID?: string + model?: { + providerID: string + modelID: string + } + agent?: string + noReply?: boolean + tools?: { + [key: string]: boolean + } + format?: OutputFormat + system?: string + variant?: string + parts?: Array + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "messageID" }, + { in: "body", key: "model" }, + { in: "body", key: "agent" }, + { in: "body", key: "noReply" }, + { in: "body", key: "tools" }, + { in: "body", key: "format" }, + { in: "body", key: "system" }, + { in: "body", key: "variant" }, + { in: "body", key: "parts" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/message", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Delete message + * + * Permanently delete a specific message and all of its parts from a session without reverting file changes. + */ + public deleteMessage( + parameters: { + sessionID: string + messageID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "path", key: "messageID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).delete< + SessionDeleteMessageResponses, + SessionDeleteMessageErrors, + ThrowOnError + >({ + url: "/session/{sessionID}/message/{messageID}", + ...options, + ...params, + }) + } + + /** + * Get message + * + * Retrieve a specific message from a session by its message ID. + */ + public message( + parameters: { + sessionID: string + messageID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "path", key: "messageID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/session/{sessionID}/message/{messageID}", + ...options, + ...params, + }) + } + + /** + * Fork session + * + * Create a new session by forking an existing session at a specific message point. + */ + public fork( + parameters: { + sessionID: string + directory?: string + workspace?: string + messageID?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "messageID" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/fork", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Abort session + * + * Abort an active session and stop any ongoing AI processing or command execution. + */ + public abort( + parameters: { + sessionID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/abort", + ...options, + ...params, + }) + } + + /** + * Initialize session + * + * Analyze the current application and create an AGENTS.md file with project-specific agent configurations. + */ + public init( + parameters: { + sessionID: string + directory?: string + workspace?: string + modelID?: string + providerID?: string + messageID?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "modelID" }, + { in: "body", key: "providerID" }, + { in: "body", key: "messageID" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/init", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Unshare session + * + * Remove the shareable link for a session, making it private again. + */ + public unshare( + parameters: { + sessionID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).delete({ + url: "/session/{sessionID}/share", + ...options, + ...params, + }) + } + + /** + * Share session + * + * Create a shareable link for a session, allowing others to view the conversation. + */ + public share( + parameters: { + sessionID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/share", + ...options, + ...params, + }) + } + + /** + * Summarize session + * + * Generate a concise summary of the session using AI compaction to preserve key information. + */ + public summarize( + parameters: { + sessionID: string + directory?: string + workspace?: string + providerID?: string + modelID?: string + auto?: boolean + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "providerID" }, + { in: "body", key: "modelID" }, + { in: "body", key: "auto" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/summarize", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Send async message + * + * Create and send a new message to a session asynchronously, starting the session if needed and returning immediately. + */ + public promptAsync( + parameters: { + sessionID: string + directory?: string + workspace?: string + messageID?: string + model?: { + providerID: string + modelID: string + } + agent?: string + noReply?: boolean + tools?: { + [key: string]: boolean + } + format?: OutputFormat + system?: string + variant?: string + parts?: Array + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "messageID" }, + { in: "body", key: "model" }, + { in: "body", key: "agent" }, + { in: "body", key: "noReply" }, + { in: "body", key: "tools" }, + { in: "body", key: "format" }, + { in: "body", key: "system" }, + { in: "body", key: "variant" }, + { in: "body", key: "parts" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/prompt_async", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Send command + * + * Send a new command to a session for execution by the AI assistant. + */ + public command( + parameters: { + sessionID: string + directory?: string + workspace?: string + messageID?: string + agent?: string + model?: string + arguments?: string + command?: string + variant?: string + parts?: Array<{ + id?: string + type: "file" + mime: string + filename?: string + url: string + source?: FilePartSource + }> + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "messageID" }, + { in: "body", key: "agent" }, + { in: "body", key: "model" }, + { in: "body", key: "arguments" }, + { in: "body", key: "command" }, + { in: "body", key: "variant" }, + { in: "body", key: "parts" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/command", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Run shell command + * + * Execute a shell command within the session context and return the AI's response. + */ + public shell( + parameters: { + sessionID: string + directory?: string + workspace?: string + messageID?: string + agent?: string + model?: { + providerID: string + modelID: string + } + command?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "messageID" }, + { in: "body", key: "agent" }, + { in: "body", key: "model" }, + { in: "body", key: "command" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/shell", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Revert message + * + * Revert a specific message in a session, undoing its effects and restoring the previous state. + */ + public revert( + parameters: { + sessionID: string + directory?: string + workspace?: string + messageID?: string + partID?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "messageID" }, + { in: "body", key: "partID" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/revert", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Restore reverted messages + * + * Restore all previously reverted messages in a session. + */ + public unrevert( + parameters: { + sessionID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/unrevert", + ...options, + ...params, + }) + } +} + +export class Part extends HeyApiClient { + /** + * Delete a part from a message. + */ + public delete( + parameters: { + sessionID: string + messageID: string + partID: string + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "path", key: "messageID" }, + { in: "path", key: "partID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).delete({ + url: "/session/{sessionID}/message/{messageID}/part/{partID}", + ...options, + ...params, + }) + } + + /** + * Update a part in a message. + */ + public update( + parameters: { + sessionID: string + messageID: string + partID: string + directory?: string + workspace?: string + part?: Part2 + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "path", key: "messageID" }, + { in: "path", key: "partID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { key: "part", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).patch({ + url: "/session/{sessionID}/message/{messageID}/part/{partID}", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class History extends HeyApiClient { + /** + * List sync events + * + * List sync events for all aggregates. Keys are aggregate IDs the client already knows about, values are the last known sequence ID. Events with seq > value are returned for those aggregates. Aggregates not listed in the input get their full history. + */ + public list( + parameters?: { + directory?: string + workspace?: string + body?: { + [key: string]: number + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { key: "body", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/sync/history", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class Sync extends HeyApiClient { + /** + * Start workspace sync + * + * Start sync loops for workspaces in the current project that have active sessions. + */ + public start( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/sync/start", + ...options, + ...params, + }) + } + + /** + * Replay sync events + * + * Validate and replay a complete sync event history. + */ + public replay( + parameters?: { + query_directory?: string + workspace?: string + body_directory?: string + events?: Array<{ + id: string + aggregateID: string + seq: number + type: string + data: { + [key: string]: unknown + } + }> + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { + in: "query", + key: "query_directory", + map: "directory", + }, + { in: "query", key: "workspace" }, + { + in: "body", + key: "body_directory", + map: "directory", + }, + { in: "body", key: "events" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/sync/replay", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Steal session into workspace + * + * Update a session to belong to the current workspace through the sync event system. + */ + public steal( + parameters?: { + directory?: string + workspace?: string + sessionID?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "sessionID" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/sync/steal", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + private _history?: History + get history(): History { + return (this._history ??= new History({ client: this.client })) + } +} + +export class Control extends HeyApiClient { + /** + * Get next TUI request + * + * Retrieve the next TUI request from the queue for processing. + */ + public next( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/tui/control/next", + ...options, + ...params, + }) + } + + /** + * Submit TUI response + * + * Submit a response to the TUI request queue to complete a pending request. + */ + public response( + parameters?: { + directory?: string + workspace?: string + body?: unknown + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { key: "body", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/tui/control/response", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class Tui extends HeyApiClient { + /** + * Append TUI prompt + * + * Append prompt to the TUI. + */ + public appendPrompt( + parameters?: { + directory?: string + workspace?: string + text?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "text" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/tui/append-prompt", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Open help dialog + * + * Open the help dialog in the TUI to display user assistance information. + */ + public openHelp( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/tui/open-help", + ...options, + ...params, + }) + } + + /** + * Open sessions dialog + * + * Open the session dialog. + */ + public openSessions( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/tui/open-sessions", + ...options, + ...params, + }) + } + + /** + * Open themes dialog + * + * Open the theme dialog. + */ + public openThemes( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/tui/open-themes", + ...options, + ...params, + }) + } + + /** + * Open models dialog + * + * Open the model dialog. + */ + public openModels( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/tui/open-models", + ...options, + ...params, + }) + } + + /** + * Submit TUI prompt + * + * Submit the prompt. + */ + public submitPrompt( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/tui/submit-prompt", + ...options, + ...params, + }) + } + + /** + * Clear TUI prompt + * + * Clear the prompt. + */ + public clearPrompt( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/tui/clear-prompt", + ...options, + ...params, + }) + } + + /** + * Execute TUI command + * + * Execute a TUI command. + */ + public executeCommand( + parameters?: { + directory?: string + workspace?: string + command?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "command" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/tui/execute-command", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Show TUI toast + * + * Show a toast notification in the TUI. + */ + public showToast( + parameters?: { + directory?: string + workspace?: string + title?: string + message?: string + variant?: "info" | "success" | "warning" | "error" + duration?: number + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "title" }, + { in: "body", key: "message" }, + { in: "body", key: "variant" }, + { in: "body", key: "duration" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/tui/show-toast", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Publish TUI event + * + * Publish a TUI event. + */ + public publish( + parameters?: { + directory?: string + workspace?: string + body?: EventTuiPromptAppend | EventTuiCommandExecute | EventTuiToastShow | EventTuiSessionSelect + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { key: "body", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/tui/publish", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Select session + * + * Navigate the TUI to display the specified session. + */ + public selectSession( + parameters?: { + directory?: string + workspace?: string + sessionID?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "sessionID" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/tui/select-session", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + private _control?: Control + get control(): Control { + return (this._control ??= new Control({ client: this.client })) + } +} + +export class Health extends HeyApiClient { + /** + * Check server health + * + * Check whether the API server is ready to accept requests. + */ + public get(options?: Options) { + return (options?.client ?? this.client).get({ + url: "/api/health", + ...options, + }) + } +} + +export class Location extends HeyApiClient { + /** + * Get location + * + * Resolve the requested location or the server default location. + */ + public get( + parameters?: { + location?: { + directory?: string + workspace?: string + } + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) + return (options?.client ?? this.client).get({ + url: "/api/location", + ...options, + ...params, + }) + } +} + +export class Agent extends HeyApiClient { + /** + * List agents + * + * Retrieve currently registered agents. + */ + public list( + parameters?: { + location?: { + directory?: string + workspace?: string + } + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) + return (options?.client ?? this.client).get({ + url: "/api/agent", + ...options, + ...params, + }) + } +} + +export class Revert extends HeyApiClient { + /** + * Stage session revert + * + * Stage or move a reversible session boundary and optionally apply its file changes. + */ + public stage( + parameters: { + sessionID: string + messageID?: string + files?: boolean + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "body", key: "messageID" }, + { in: "body", key: "files" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + V2SessionRevertStageResponses, + V2SessionRevertStageErrors, + ThrowOnError + >({ + url: "/api/session/{sessionID}/revert/stage", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Clear staged revert + */ + public clear( + parameters: { + sessionID: string + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "sessionID" }] }]) + return (options?.client ?? this.client).post< + V2SessionRevertClearResponses, + V2SessionRevertClearErrors, + ThrowOnError + >({ + url: "/api/session/{sessionID}/revert/clear", + ...options, + ...params, + }) + } + + /** + * Commit staged revert + */ + public commit( + parameters: { + sessionID: string + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "sessionID" }] }]) + return (options?.client ?? this.client).post< + V2SessionRevertCommitResponses, + V2SessionRevertCommitErrors, + ThrowOnError + >({ + url: "/api/session/{sessionID}/revert/commit", + ...options, + ...params, + }) + } +} + +export class Permission2 extends HeyApiClient { + /** + * List session permission requests + * + * Retrieve pending permission requests owned by a session. + */ + public list( + parameters: { + sessionID: string + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "sessionID" }] }]) + return (options?.client ?? this.client).get< + V2SessionPermissionListResponses, + V2SessionPermissionListErrors, + ThrowOnError + >({ + url: "/api/session/{sessionID}/permission", + ...options, + ...params, + }) + } + + /** + * Create permission request + * + * Evaluate and, when approval is required, create a permission request for a session. + */ + public create( + parameters: { + sessionID: string + id?: string + action?: string + resources?: Array + save?: Array + metadata?: { + [key: string]: unknown + } + source?: PermissionV2Source + agent?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "body", key: "id" }, + { in: "body", key: "action" }, + { in: "body", key: "resources" }, + { in: "body", key: "save" }, + { in: "body", key: "metadata" }, + { in: "body", key: "source" }, + { in: "body", key: "agent" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + V2SessionPermissionCreateResponses, + V2SessionPermissionCreateErrors, + ThrowOnError + >({ + url: "/api/session/{sessionID}/permission", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Get permission request + * + * Retrieve a pending permission request owned by a session. + */ + public get( + parameters: { + sessionID: string + requestID: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "path", key: "requestID" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get< + V2SessionPermissionGetResponses, + V2SessionPermissionGetErrors, + ThrowOnError + >({ + url: "/api/session/{sessionID}/permission/{requestID}", + ...options, + ...params, + }) + } + + /** + * Reply to pending permission request + * + * Respond to a pending permission request owned by a session. + */ + public reply( + parameters: { + sessionID: string + requestID: string + reply?: PermissionV2Reply + message?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "path", key: "requestID" }, + { in: "body", key: "reply" }, + { in: "body", key: "message" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + V2SessionPermissionReplyResponses, + V2SessionPermissionReplyErrors, + ThrowOnError + >({ + url: "/api/session/{sessionID}/permission/{requestID}/reply", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class Question2 extends HeyApiClient { + /** + * List session question requests + * + * Retrieve pending question requests owned by a session. + */ + public list( + parameters: { + sessionID: string + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "sessionID" }] }]) + return (options?.client ?? this.client).get< + V2SessionQuestionListResponses, + V2SessionQuestionListErrors, + ThrowOnError + >({ + url: "/api/session/{sessionID}/question", + ...options, + ...params, + }) + } + + /** + * Reply to pending question request + * + * Answer a pending question request owned by a session. + */ + public reply( + parameters: { + sessionID: string + requestID: string + questionV2Reply: QuestionV2Reply + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "path", key: "requestID" }, + { key: "questionV2Reply", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + V2SessionQuestionReplyResponses, + V2SessionQuestionReplyErrors, + ThrowOnError + >({ + url: "/api/session/{sessionID}/question/{requestID}/reply", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Reject pending question request + * + * Reject a pending question request owned by a session. + */ + public reject( + parameters: { + sessionID: string + requestID: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "path", key: "requestID" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + V2SessionQuestionRejectResponses, + V2SessionQuestionRejectErrors, + ThrowOnError + >({ + url: "/api/session/{sessionID}/question/{requestID}/reject", + ...options, + ...params, + }) + } +} + +export class Session3 extends HeyApiClient { + /** + * List sessions + * + * Retrieve sessions in the requested order. Items keep that order across pages; use cursor.next or cursor.previous to move through the ordered list. + */ + public list( + parameters?: { + workspace?: string + limit?: number + order?: "asc" | "desc" + search?: string + directory?: string + project?: string + subpath?: string + cursor?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "workspace" }, + { in: "query", key: "limit" }, + { in: "query", key: "order" }, + { in: "query", key: "search" }, + { in: "query", key: "directory" }, + { in: "query", key: "project" }, + { in: "query", key: "subpath" }, + { in: "query", key: "cursor" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/api/session", + ...options, + ...params, + }) + } + + /** + * Create session + * + * Create a session at the requested location. + */ + public create( + parameters?: { + id?: string + agent?: string + model?: ModelRef + location?: LocationRef + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "body", key: "id" }, + { in: "body", key: "agent" }, + { in: "body", key: "model" }, + { in: "body", key: "location" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/api/session", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * List active sessions + * + * Retrieve foreground Session drains currently owned by this OpenCode process. Sessions absent from the result are inactive. + */ + public active(options?: Options) { + return (options?.client ?? this.client).get({ + url: "/api/session/active", + ...options, + }) + } + + /** + * Get session + * + * Retrieve a session by ID. + */ + public get( + parameters: { + sessionID: string + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "sessionID" }] }]) + return (options?.client ?? this.client).get({ + url: "/api/session/{sessionID}", + ...options, + ...params, + }) + } + + /** + * Switch session agent + * + * Switch the agent used by subsequent provider turns. + */ + public switchAgent( + parameters: { + sessionID: string + agent?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "body", key: "agent" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + V2SessionSwitchAgentResponses, + V2SessionSwitchAgentErrors, + ThrowOnError + >({ + url: "/api/session/{sessionID}/agent", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Switch session model + * + * Switch the model used by subsequent provider turns. + */ + public switchModel( + parameters: { + sessionID: string + model?: ModelRef + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "body", key: "model" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + V2SessionSwitchModelResponses, + V2SessionSwitchModelErrors, + ThrowOnError + >({ + url: "/api/session/{sessionID}/model", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Send message + * + * Durably admit one session input and schedule agent-loop execution unless resume is false. + */ + public prompt( + parameters: { + sessionID: string + id?: string + prompt?: PromptInput + delivery?: "steer" | "queue" + resume?: boolean + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "body", key: "id" }, + { in: "body", key: "prompt" }, + { in: "body", key: "delivery" }, + { in: "body", key: "resume" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/api/session/{sessionID}/prompt", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Compact session + * + * Compact a session conversation. + */ + public compact( + parameters: { + sessionID: string + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "sessionID" }] }]) + return (options?.client ?? this.client).post({ + url: "/api/session/{sessionID}/compact", + ...options, + ...params, + }) + } + + /** + * Wait for session + * + * Wait for a session agent loop to become idle. + */ + public wait( + parameters: { + sessionID: string + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "sessionID" }] }]) + return (options?.client ?? this.client).post({ + url: "/api/session/{sessionID}/wait", + ...options, + ...params, + }) + } + + /** + * Get session context + * + * Retrieve the active context messages for a session (all messages after the last compaction). + */ + public context( + parameters: { + sessionID: string + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "sessionID" }] }]) + return (options?.client ?? this.client).get({ + url: "/api/session/{sessionID}/context", + ...options, + ...params, + }) + } + + /** + * Get session history + * + * Read one finite page of public durable Session events after an exclusive aggregate sequence. Newly committed events may appear on later pages. + */ + public history( + parameters: { + sessionID: string + limit?: number + after?: number + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "limit" }, + { in: "query", key: "after" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/api/session/{sessionID}/history", + ...options, + ...params, + }) + } + + /** + * Subscribe to session events + * + * Replay durable events after an aggregate sequence, then continue with new durable events. + */ + public events( + parameters: { + sessionID: string + after?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "after" }, + ], + }, + ], + ) + return (options?.client ?? this.client).sse.get({ + url: "/api/session/{sessionID}/event", + ...options, + ...params, + }) + } + + /** + * Interrupt session execution + * + * Interrupt active execution owned by this OpenCode process. Idle interruption is a no-op. + */ + public interrupt( + parameters: { + sessionID: string + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "sessionID" }] }]) + return (options?.client ?? this.client).post({ + url: "/api/session/{sessionID}/interrupt", + ...options, + ...params, + }) + } + + /** + * Get session message + * + * Retrieve one projected message owned by the Session. + */ + public message( + parameters: { + sessionID: string + messageID: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "path", key: "messageID" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/api/session/{sessionID}/message/{messageID}", + ...options, + ...params, + }) + } + + /** + * Get session messages + * + * Retrieve projected messages for a session. Items keep the requested order across pages; use cursor.next or cursor.previous to move through the ordered timeline. + */ + public messages( + parameters: { + sessionID: string + limit?: number + order?: "asc" | "desc" + cursor?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "limit" }, + { in: "query", key: "order" }, + { in: "query", key: "cursor" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/api/session/{sessionID}/message", + ...options, + ...params, + }) + } + + private _revert?: Revert + get revert(): Revert { + return (this._revert ??= new Revert({ client: this.client })) + } + + private _permission?: Permission2 + get permission(): Permission2 { + return (this._permission ??= new Permission2({ client: this.client })) + } + + private _question?: Question2 + get question(): Question2 { + return (this._question ??= new Question2({ client: this.client })) + } +} + +export class Model extends HeyApiClient { + /** + * List models + * + * Retrieve available models ordered by release date. + */ + public list( + parameters?: { + location?: { + directory?: string + workspace?: string + } + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) + return (options?.client ?? this.client).get({ + url: "/api/model", + ...options, + ...params, + }) + } +} + +export class Provider2 extends HeyApiClient { + /** + * List providers + * + * Retrieve active AI providers so clients can show provider availability and configuration. + */ + public list( + parameters?: { + location?: { + directory?: string + workspace?: string + } + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) + return (options?.client ?? this.client).get({ + url: "/api/provider", + ...options, + ...params, + }) + } + + /** + * Get provider + * + * Retrieve a single AI provider so clients can inspect its availability and endpoint settings. + */ + public get( + parameters: { + providerID: string + location?: { + directory?: string + workspace?: string + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "providerID" }, + { in: "query", key: "location" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/api/provider/{providerID}", + ...options, + ...params, + }) + } +} + +export class Connect extends HeyApiClient { + /** + * Connect with key + * + * Run a key authentication method and store the resulting credential. + */ + public key( + parameters: { + integrationID: string + location?: { + directory?: string + workspace?: string + } + key?: string + label?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "integrationID" }, + { in: "query", key: "location" }, + { in: "body", key: "key" }, + { in: "body", key: "label" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + V2IntegrationConnectKeyResponses, + V2IntegrationConnectKeyErrors, + ThrowOnError + >({ + url: "/api/integration/{integrationID}/connect/key", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Begin OAuth connection + * + * Start an OAuth attempt and return the authorization details. + */ + public oauth( + parameters: { + integrationID: string + location?: { + directory?: string + workspace?: string + } + methodID?: string + inputs?: { + [key: string]: string + } + label?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "integrationID" }, + { in: "query", key: "location" }, + { in: "body", key: "methodID" }, + { in: "body", key: "inputs" }, + { in: "body", key: "label" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + V2IntegrationConnectOauthResponses, + V2IntegrationConnectOauthErrors, + ThrowOnError + >({ + url: "/api/integration/{integrationID}/connect/oauth", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class Attempt extends HeyApiClient { + /** + * Cancel OAuth connection + * + * Cancel an OAuth attempt and release its resources. + */ + public cancel( + parameters: { + attemptID: string + location?: { + directory?: string + workspace?: string + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "attemptID" }, + { in: "query", key: "location" }, + ], + }, + ], + ) + return (options?.client ?? this.client).delete< + V2IntegrationAttemptCancelResponses, + V2IntegrationAttemptCancelErrors, + ThrowOnError + >({ + url: "/api/integration/attempt/{attemptID}", + ...options, + ...params, + }) + } + + /** + * Get OAuth attempt status + * + * Poll the current status of an OAuth attempt. + */ + public status( + parameters: { + attemptID: string + location?: { + directory?: string + workspace?: string + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "attemptID" }, + { in: "query", key: "location" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get< + V2IntegrationAttemptStatusResponses, + V2IntegrationAttemptStatusErrors, + ThrowOnError + >({ + url: "/api/integration/attempt/{attemptID}", + ...options, + ...params, + }) + } + + /** + * Complete OAuth connection + * + * Complete a code-based OAuth attempt and store the resulting credential. + */ + public complete( + parameters: { + attemptID: string + location?: { + directory?: string + workspace?: string + } + code?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "attemptID" }, + { in: "query", key: "location" }, + { in: "body", key: "code" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + V2IntegrationAttemptCompleteResponses, + V2IntegrationAttemptCompleteErrors, + ThrowOnError + >({ + url: "/api/integration/attempt/{attemptID}/complete", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class Integration extends HeyApiClient { + /** + * List integrations + * + * Retrieve available integrations and their authentication methods. + */ + public list( + parameters?: { + location?: { + directory?: string + workspace?: string + } + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) + return (options?.client ?? this.client).get({ + url: "/api/integration", + ...options, + ...params, + }) + } + + /** + * Get integration + * + * Retrieve one integration and its authentication methods. + */ + public get( + parameters: { + integrationID: string + location?: { + directory?: string + workspace?: string + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "integrationID" }, + { in: "query", key: "location" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/api/integration/{integrationID}", + ...options, + ...params, + }) + } + + private _connect?: Connect + get connect(): Connect { + return (this._connect ??= new Connect({ client: this.client })) + } + + private _attempt?: Attempt + get attempt(): Attempt { + return (this._attempt ??= new Attempt({ client: this.client })) + } +} + +export class Credential extends HeyApiClient { + /** + * Remove credential + * + * Remove a stored integration credential. + */ + public remove( + parameters: { + credentialID: string + location?: { + directory?: string + workspace?: string + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "credentialID" }, + { in: "query", key: "location" }, + ], + }, + ], + ) + return (options?.client ?? this.client).delete( + { + url: "/api/credential/{credentialID}", + ...options, + ...params, + }, + ) + } + + /** + * Update credential + * + * Update a stored credential label. + */ + public update( + parameters: { + credentialID: string + location?: { + directory?: string + workspace?: string + } + label?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "credentialID" }, + { in: "query", key: "location" }, + { in: "body", key: "label" }, + ], + }, + ], + ) + return (options?.client ?? this.client).patch({ + url: "/api/credential/{credentialID}", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class Request extends HeyApiClient { + /** + * List pending permission requests + * + * Retrieve pending permission requests for a location. + */ + public list( + parameters?: { + location?: { + directory?: string + workspace?: string + } + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) + return (options?.client ?? this.client).get< + V2PermissionRequestListResponses, + V2PermissionRequestListErrors, + ThrowOnError + >({ + url: "/api/permission/request", + ...options, + ...params, + }) + } +} + +export class Saved extends HeyApiClient { + /** + * List saved permissions + * + * Retrieve saved permissions, optionally filtered by project. + */ + public list( + parameters?: { + projectID?: string + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "projectID" }] }]) + return (options?.client ?? this.client).get< + V2PermissionSavedListResponses, + V2PermissionSavedListErrors, + ThrowOnError + >({ + url: "/api/permission/saved", + ...options, + ...params, + }) + } + + /** + * Remove saved permission + * + * Remove a saved permission by ID. + */ + public remove( + parameters: { + id: string + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "id" }] }]) + return (options?.client ?? this.client).delete< + V2PermissionSavedRemoveResponses, + V2PermissionSavedRemoveErrors, + ThrowOnError + >({ + url: "/api/permission/saved/{id}", + ...options, + ...params, + }) + } +} + +export class Permission3 extends HeyApiClient { + private _request?: Request + get request(): Request { + return (this._request ??= new Request({ client: this.client })) + } + + private _saved?: Saved + get saved(): Saved { + return (this._saved ??= new Saved({ client: this.client })) + } +} + +export class Fs extends HeyApiClient { + /** + * Read file + * + * Serve one file relative to the requested location. + */ + public read( + parameters?: { + location?: { + directory?: string + workspace?: string + } + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) + return (options?.client ?? this.client).get({ + url: "/api/fs/read/*", + ...options, + ...params, + }) + } + + /** + * List directory + * + * List direct children of one directory relative to the requested location. + */ + public list( + parameters?: { + location?: { + directory?: string + workspace?: string + } + path?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "location" }, + { in: "query", key: "path" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/api/fs/list", + ...options, + ...params, + }) + } + + /** + * Find files + * + * Find recursively ranked filesystem entries relative to the requested location. + */ + public find( + parameters: { + location?: { + directory?: string + workspace?: string + } + query: string + type?: "file" | "directory" + limit?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "location" }, + { in: "query", key: "query" }, + { in: "query", key: "type" }, + { in: "query", key: "limit" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/api/fs/find", + ...options, + ...params, + }) + } +} + +export class Command2 extends HeyApiClient { + /** + * List commands + * + * Retrieve currently registered commands. + */ + public list( + parameters?: { + location?: { + directory?: string + workspace?: string + } + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) + return (options?.client ?? this.client).get({ + url: "/api/command", + ...options, + ...params, + }) + } +} + +export class Skill extends HeyApiClient { + /** + * List skills + * + * Retrieve currently registered skills. + */ + public list( + parameters?: { + location?: { + directory?: string + workspace?: string + } + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) + return (options?.client ?? this.client).get({ + url: "/api/skill", + ...options, + ...params, + }) + } +} + +export class Event2 extends HeyApiClient { + /** + * Subscribe to events + * + * Subscribe to native event payloads for the server. + */ + public subscribe(options?: Options) { + return (options?.client ?? this.client).sse.get({ + url: "/api/event", + ...options, + }) + } +} + +export class Pty2 extends HeyApiClient { + /** + * List PTY sessions + * + * List PTY sessions for a location, including exited sessions retained until removal. + */ + public list( + parameters?: { + location?: { + directory?: string + workspace?: string + } + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) + return (options?.client ?? this.client).get({ + url: "/api/pty", + ...options, + ...params, + }) + } + + /** + * Create PTY session + * + * Create a pseudo-terminal session for a location. + */ + public create( + parameters?: { + location?: { + directory?: string + workspace?: string + } + command?: string + args?: Array + cwd?: string + title?: string + env?: { + [key: string]: string + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "location" }, + { in: "body", key: "command" }, + { in: "body", key: "args" }, + { in: "body", key: "cwd" }, + { in: "body", key: "title" }, + { in: "body", key: "env" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/api/pty", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Remove PTY session + * + * Terminate and remove one PTY session. + */ + public remove( + parameters: { + ptyID: string + location?: { + directory?: string + workspace?: string + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "ptyID" }, + { in: "query", key: "location" }, + ], + }, + ], + ) + return (options?.client ?? this.client).delete({ + url: "/api/pty/{ptyID}", + ...options, + ...params, + }) + } + + /** + * Get PTY session + * + * Get one PTY session, including its exit code once exited. + */ + public get( + parameters: { + ptyID: string + location?: { + directory?: string + workspace?: string + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "ptyID" }, + { in: "query", key: "location" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/api/pty/{ptyID}", + ...options, + ...params, + }) + } + + /** + * Update PTY session + * + * Update the title or viewport size of one PTY session. + */ + public update( + parameters: { + ptyID: string + location?: { + directory?: string + workspace?: string + } + title?: string + size?: { + rows: number + cols: number + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "ptyID" }, + { in: "query", key: "location" }, + { in: "body", key: "title" }, + { in: "body", key: "size" }, + ], + }, + ], + ) + return (options?.client ?? this.client).put({ + url: "/api/pty/{ptyID}", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Create PTY WebSocket token + * + * Create a short-lived single-use ticket for opening a PTY WebSocket connection. + */ + public connectToken( + parameters: { + ptyID: string + location?: { + directory?: string + workspace?: string + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "ptyID" }, + { in: "query", key: "location" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/api/pty/{ptyID}/connect-token", + ...options, + ...params, + }) + } + + /** + * Connect to PTY session + * + * Establish a WebSocket connection streaming PTY output and accepting terminal input. + */ + public connect( + parameters: { + ptyID: string + "location[directory]"?: string + "location[workspace]"?: string + cursor?: string + ticket?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "ptyID" }, + { in: "query", key: "location[directory]" }, + { in: "query", key: "location[workspace]" }, + { in: "query", key: "cursor" }, + { in: "query", key: "ticket" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/api/pty/{ptyID}/connect", + ...options, + ...params, + }) + } +} + +export class Request2 extends HeyApiClient { + /** + * List pending question requests + * + * Retrieve pending question requests for a location. + */ + public list( + parameters?: { + location?: { + directory?: string + workspace?: string + } + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) + return (options?.client ?? this.client).get< + V2QuestionRequestListResponses, + V2QuestionRequestListErrors, + ThrowOnError + >({ + url: "/api/question/request", + ...options, + ...params, + }) + } +} + +export class Question3 extends HeyApiClient { + private _request?: Request2 + get request(): Request2 { + return (this._request ??= new Request2({ client: this.client })) + } +} + +export class Reference extends HeyApiClient { + /** + * List references + * + * List references available in the requested location. + */ + public list( + parameters?: { + location?: { + directory?: string + workspace?: string + } + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) + return (options?.client ?? this.client).get({ + url: "/api/reference", + ...options, + ...params, + }) + } +} + +export class ProjectCopy2 extends HeyApiClient { + public remove( + parameters: { + projectID: string + location?: { + directory?: string + workspace?: string + } + directory?: string + force?: boolean + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "projectID" }, + { in: "query", key: "location" }, + { in: "body", key: "directory" }, + { in: "body", key: "force" }, + ], + }, + ], + ) + return (options?.client ?? this.client).delete< + V2ProjectCopyRemoveResponses, + V2ProjectCopyRemoveErrors, + ThrowOnError + >({ + url: "/experimental/project/{projectID}/copy", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + public create( + parameters: { + projectID: string + location?: { + directory?: string + workspace?: string + } + strategy?: string + directory?: string + name?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "projectID" }, + { in: "query", key: "location" }, + { in: "body", key: "strategy" }, + { in: "body", key: "directory" }, + { in: "body", key: "name" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post( + { + url: "/experimental/project/{projectID}/copy", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }, + ) + } + + public refresh( + parameters: { + projectID: string + location?: { + directory?: string + workspace?: string + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "projectID" }, + { in: "query", key: "location" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + V2ProjectCopyRefreshResponses, + V2ProjectCopyRefreshErrors, + ThrowOnError + >({ + url: "/experimental/project/{projectID}/copy/refresh", + ...options, + ...params, + }) + } +} + +export class V2 extends HeyApiClient { + private _health?: Health + get health(): Health { + return (this._health ??= new Health({ client: this.client })) + } + + private _location?: Location + get location(): Location { + return (this._location ??= new Location({ client: this.client })) + } + + private _agent?: Agent + get agent(): Agent { + return (this._agent ??= new Agent({ client: this.client })) + } + + private _session?: Session3 + get session(): Session3 { + return (this._session ??= new Session3({ client: this.client })) + } + + private _model?: Model + get model(): Model { + return (this._model ??= new Model({ client: this.client })) + } + + private _provider?: Provider2 + get provider(): Provider2 { + return (this._provider ??= new Provider2({ client: this.client })) + } + + private _integration?: Integration + get integration(): Integration { + return (this._integration ??= new Integration({ client: this.client })) + } + + private _credential?: Credential + get credential(): Credential { + return (this._credential ??= new Credential({ client: this.client })) + } + + private _permission?: Permission3 + get permission(): Permission3 { + return (this._permission ??= new Permission3({ client: this.client })) + } + + private _fs?: Fs + get fs(): Fs { + return (this._fs ??= new Fs({ client: this.client })) + } + + private _command?: Command2 + get command(): Command2 { + return (this._command ??= new Command2({ client: this.client })) + } + + private _skill?: Skill + get skill(): Skill { + return (this._skill ??= new Skill({ client: this.client })) + } + + private _event?: Event2 + get event(): Event2 { + return (this._event ??= new Event2({ client: this.client })) + } + + private _pty?: Pty2 + get pty(): Pty2 { + return (this._pty ??= new Pty2({ client: this.client })) + } + + private _question?: Question3 + get question(): Question3 { + return (this._question ??= new Question3({ client: this.client })) + } + + private _reference?: Reference + get reference(): Reference { + return (this._reference ??= new Reference({ client: this.client })) + } + + private _projectCopy?: ProjectCopy2 + get projectCopy(): ProjectCopy2 { + return (this._projectCopy ??= new ProjectCopy2({ client: this.client })) + } +} + +export class OpencodeClient extends HeyApiClient { + public static readonly __registry = new HeyApiRegistry() + + constructor(args?: { client?: Client; key?: string }) { + super(args) + OpencodeClient.__registry.set(this, args?.key) + } + + private _auth?: Auth + get auth(): Auth { + return (this._auth ??= new Auth({ client: this.client })) + } + + private _app?: App + get app(): App { + return (this._app ??= new App({ client: this.client })) + } + + private _experimental?: Experimental + get experimental(): Experimental { + return (this._experimental ??= new Experimental({ client: this.client })) + } + + private _global?: Global + get global(): Global { + return (this._global ??= new Global({ client: this.client })) + } + + private _event?: Event + get event(): Event { + return (this._event ??= new Event({ client: this.client })) + } + + private _config?: Config2 + get config(): Config2 { + return (this._config ??= new Config2({ client: this.client })) + } + + private _tool?: Tool + get tool(): Tool { + return (this._tool ??= new Tool({ client: this.client })) + } + + private _worktree?: Worktree + get worktree(): Worktree { + return (this._worktree ??= new Worktree({ client: this.client })) + } + + private _find?: Find + get find(): Find { + return (this._find ??= new Find({ client: this.client })) + } + + private _file?: File + get file(): File { + return (this._file ??= new File({ client: this.client })) + } + + private _instance?: Instance + get instance(): Instance { + return (this._instance ??= new Instance({ client: this.client })) + } + + private _path?: Path + get path(): Path { + return (this._path ??= new Path({ client: this.client })) + } + + private _vcs?: Vcs + get vcs(): Vcs { + return (this._vcs ??= new Vcs({ client: this.client })) + } + + private _command?: Command + get command(): Command { + return (this._command ??= new Command({ client: this.client })) + } + + private _lsp?: Lsp + get lsp(): Lsp { + return (this._lsp ??= new Lsp({ client: this.client })) + } + + private _formatter?: Formatter + get formatter(): Formatter { + return (this._formatter ??= new Formatter({ client: this.client })) + } + + private _mcp?: Mcp + get mcp(): Mcp { + return (this._mcp ??= new Mcp({ client: this.client })) + } + + private _project?: Project + get project(): Project { + return (this._project ??= new Project({ client: this.client })) + } + + private _pty?: Pty + get pty(): Pty { + return (this._pty ??= new Pty({ client: this.client })) + } + + private _question?: Question + get question(): Question { + return (this._question ??= new Question({ client: this.client })) + } + + private _permission?: Permission + get permission(): Permission { + return (this._permission ??= new Permission({ client: this.client })) + } + + private _provider?: Provider + get provider(): Provider { + return (this._provider ??= new Provider({ client: this.client })) + } + + private _session?: Session2 + get session(): Session2 { + return (this._session ??= new Session2({ client: this.client })) + } + + private _part?: Part + get part(): Part { + return (this._part ??= new Part({ client: this.client })) + } + + private _sync?: Sync + get sync(): Sync { + return (this._sync ??= new Sync({ client: this.client })) + } + + private _tui?: Tui + get tui(): Tui { + return (this._tui ??= new Tui({ client: this.client })) + } + + private _v2?: V2 + get v2(): V2 { + return (this._v2 ??= new V2({ client: this.client })) + } +} diff --git a/packages/app-bundle/overlay/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/app-bundle/overlay/packages/sdk/js/src/v2/gen/types.gen.ts new file mode 100644 index 00000000..37ff1cb9 --- /dev/null +++ b/packages/app-bundle/overlay/packages/sdk/js/src/v2/gen/types.gen.ts @@ -0,0 +1,13737 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type ClientOptions = { + baseUrl: `${string}://${string}` | (string & {}) +} + +export type Event = + | EventModelsDevRefreshed + | EventIntegrationUpdated + | EventIntegrationConnectionUpdated + | EventCatalogUpdated + | EventSessionCreated + | EventSessionUpdated + | EventSessionDeleted + | EventMessageUpdated + | EventMessageRemoved + | EventMessagePartUpdated + | EventMessagePartRemoved + | EventSessionNextAgentSwitched + | EventSessionNextModelSwitched + | EventSessionNextMoved + | EventSessionNextPrompted + | EventSessionNextPromptAdmitted + | EventSessionNextContextUpdated + | EventSessionNextSynthetic + | EventSessionNextShellStarted + | EventSessionNextShellEnded + | EventSessionNextStepStarted + | EventSessionNextStepEnded + | EventSessionNextStepFailed + | EventSessionNextTextStarted + | EventSessionNextTextDelta + | EventSessionNextTextEnded + | EventSessionNextReasoningStarted + | EventSessionNextReasoningDelta + | EventSessionNextReasoningEnded + | EventSessionNextToolInputStarted + | EventSessionNextToolInputDelta + | EventSessionNextToolInputEnded + | EventSessionNextToolCalled + | EventSessionNextToolProgress + | EventSessionNextToolSuccess + | EventSessionNextToolFailed + | EventSessionNextRetried + | EventSessionNextCompactionStarted + | EventSessionNextCompactionDelta + | EventSessionNextCompactionEnded + | EventSessionNextRevertStaged + | EventSessionNextRevertCleared + | EventSessionNextRevertCommitted + | EventMessagePartDelta + | EventSessionDiff + | EventSessionError + | EventInstallationUpdated + | EventInstallationUpdateAvailable + | EventFileEdited + | EventReferenceUpdated + | EventPermissionV2Asked + | EventPermissionV2Replied + | EventPluginAdded + | EventProjectDirectoriesUpdated + | EventFileWatcherUpdated + | EventPtyCreated + | EventPtyUpdated + | EventPtyExited + | EventPtyDeleted + | EventQuestionV2Asked + | EventQuestionV2Replied + | EventQuestionV2Rejected + | EventTodoUpdated + | EventLspUpdated + | EventPermissionAsked + | EventPermissionReplied + | EventTuiPromptAppend2 + | EventTuiCommandExecute2 + | EventTuiToastShow2 + | EventTuiSessionSelect2 + | EventMcpToolsChanged + | EventMcpBrowserOpenFailed + | EventCommandExecuted + | EventProjectUpdated + | EventSessionStatus + | EventSessionIdle + | EventQuestionAsked + | EventQuestionReplied + | EventQuestionRejected + | EventSessionCompacted + | EventVcsBranchUpdated + | EventWorkspaceReady + | EventWorkspaceFailed + | EventWorkspaceStatus + | EventWorktreeReady + | EventWorktreeFailed + | EventServerConnected + | EventGlobalDisposed + | EventServerInstanceDisposed + +export type QuestionReplied = { + sessionID: string + requestID: string + answers: Array +} + +export type QuestionRejected = { + sessionID: string + requestID: string +} + +export type OAuth = { + type: "oauth" + refresh: string + access: string + expires: number + accountId?: string + enterpriseUrl?: string +} + +export type ApiAuth = { + type: "api" + key: string + metadata?: { + [key: string]: string + } +} + +export type WellKnownAuth = { + type: "wellknown" + key: string + token: string +} + +export type Auth = OAuth | ApiAuth | WellKnownAuth + +export type EffectHttpApiErrorBadRequest = { + _tag: "BadRequest" +} + +export type InvalidRequestError = { + _tag: "InvalidRequestError" + message: string + kind?: string + field?: string +} + +export type MoveSessionError = { + name: "MoveSessionError" + data: { + message: string + } +} + +export type SnapshotFileDiff = { + file?: string + patch?: string + additions: number + deletions: number + status?: "added" | "deleted" | "modified" +} + +export type PermissionAction = "allow" | "deny" | "ask" + +export type PermissionRule = { + permission: string + pattern: string + action: PermissionAction +} + +export type PermissionRuleset = Array + +export type Session = { + id: string + slug: string + projectID: string + workspaceID?: string + directory: string + path?: string + parentID?: string + summary?: { + additions: number + deletions: number + files: number + diffs?: Array + } + cost?: number + tokens?: { + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } + } + share?: { + url: string + } + title: string + agent?: string + model?: { + id: string + providerID: string + variant?: string + } + version: string + metadata?: { + [key: string]: unknown + } + time: { + created: number + updated: number + compacting?: number + archived?: number + } + permission?: PermissionRuleset + revert?: { + messageID: string + partID?: string + snapshot?: string + diff?: string + } +} + +export type OutputFormatText = { + type: "text" +} + +export type JsonSchema = { + [key: string]: unknown +} + +export type OutputFormatJsonSchema = { + type: "json_schema" + schema: JsonSchema + retryCount?: number +} + +export type OutputFormat = OutputFormatText | OutputFormatJsonSchema + +export type UserMessage = { + id: string + sessionID: string + role: "user" + time: { + created: number + } + format?: OutputFormat + summary?: { + title?: string + body?: string + diffs: Array + } + agent: string + model: { + providerID: string + modelID: string + variant?: string + } + system?: string + tools?: { + [key: string]: boolean + } +} + +export type ProviderAuthError = { + name: "ProviderAuthError" + data: { + providerID: string + message: string + } +} + +export type UnknownError = { + name: "UnknownError" + data: { + message: string + ref?: string + } +} + +export type MessageOutputLengthError = { + name: "MessageOutputLengthError" + data: { + [key: string]: unknown + } +} + +export type MessageAbortedError = { + name: "MessageAbortedError" + data: { + message: string + } +} + +export type StructuredOutputError = { + name: "StructuredOutputError" + data: { + message: string + retries: number + } +} + +export type ContextOverflowError = { + name: "ContextOverflowError" + data: { + message: string + responseBody?: string + } +} + +export type ContentFilterError = { + name: "ContentFilterError" + data: { + message: string + } +} + +export type ApiError = { + name: "APIError" + data: { + message: string + statusCode?: number + isRetryable: boolean + responseHeaders?: { + [key: string]: string + } + responseBody?: string + metadata?: { + [key: string]: string + } + } +} + +export type AssistantMessage = { + id: string + sessionID: string + role: "assistant" + time: { + created: number + completed?: number + } + error?: + | ProviderAuthError + | UnknownError + | MessageOutputLengthError + | MessageAbortedError + | StructuredOutputError + | ContextOverflowError + | ContentFilterError + | ApiError + parentID: string + modelID: string + providerID: string + mode: string + agent: string + path: { + cwd: string + root: string + } + summary?: boolean + cost: number + tokens: { + total?: number + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } + } + structured?: unknown + variant?: string + finish?: string +} + +export type Message = UserMessage | AssistantMessage + +export type TextPart = { + id: string + sessionID: string + messageID: string + type: "text" + text: string + synthetic?: boolean + ignored?: boolean + time?: { + start: number + end?: number + } + metadata?: { + [key: string]: unknown + } +} + +export type SubtaskPart = { + id: string + sessionID: string + messageID: string + type: "subtask" + prompt: string + description: string + agent: string + model?: { + providerID: string + modelID: string + } + command?: string +} + +export type ReasoningPart = { + id: string + sessionID: string + messageID: string + type: "reasoning" + text: string + metadata?: { + [key: string]: unknown + } + time: { + start: number + end?: number + } +} + +export type FilePartSourceText = { + value: string + start: number + end: number +} + +export type FileSource = { + text: FilePartSourceText + type: "file" + path: string +} + +export type Range = { + start: { + line: number + character: number + } + end: { + line: number + character: number + } +} + +export type SymbolSource = { + text: FilePartSourceText + type: "symbol" + path: string + range: Range + name: string + kind: number +} + +export type ResourceSource = { + text: FilePartSourceText + type: "resource" + clientName: string + uri: string +} + +export type FilePartSource = FileSource | SymbolSource | ResourceSource + +export type FilePart = { + id: string + sessionID: string + messageID: string + type: "file" + mime: string + filename?: string + url: string + source?: FilePartSource +} + +export type ToolStatePending = { + status: "pending" + input: { + [key: string]: unknown + } + raw: string +} + +export type ToolStateRunning = { + status: "running" + input: { + [key: string]: unknown + } + title?: string + metadata?: { + [key: string]: unknown + } + time: { + start: number + } +} + +export type ToolStateCompleted = { + status: "completed" + input: { + [key: string]: unknown + } + output: string + title: string + metadata: { + [key: string]: unknown + } + time: { + start: number + end: number + compacted?: number + } + attachments?: Array +} + +export type ToolStateError = { + status: "error" + input: { + [key: string]: unknown + } + error: string + metadata?: { + [key: string]: unknown + } + time: { + start: number + end: number + } +} + +export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError + +export type ToolPart = { + id: string + sessionID: string + messageID: string + type: "tool" + callID: string + tool: string + state: ToolState + metadata?: { + [key: string]: unknown + } +} + +export type StepStartPart = { + id: string + sessionID: string + messageID: string + type: "step-start" + snapshot?: string +} + +export type StepFinishPart = { + id: string + sessionID: string + messageID: string + type: "step-finish" + reason: string + snapshot?: string + cost: number + tokens: { + total?: number + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } + } +} + +export type SnapshotPart = { + id: string + sessionID: string + messageID: string + type: "snapshot" + snapshot: string +} + +export type PatchPart = { + id: string + sessionID: string + messageID: string + type: "patch" + hash: string + files: Array +} + +export type AgentPart = { + id: string + sessionID: string + messageID: string + type: "agent" + name: string + source?: { + value: string + start: number + end: number + } +} + +export type SkillPart = { + id: string + sessionID: string + messageID: string + type: "skill" + name: string + description?: string + content?: string + source?: { + value: string + start: number + end: number + } +} + +export type RetryPart = { + id: string + sessionID: string + messageID: string + type: "retry" + attempt: number + error: ApiError + time: { + created: number + } +} + +export type CompactionPart = { + id: string + sessionID: string + messageID: string + type: "compaction" + auto: boolean + overflow?: boolean + tail_start_id?: string +} + +export type Part = + | TextPart + | SubtaskPart + | ReasoningPart + | FilePart + | ToolPart + | StepStartPart + | StepFinishPart + | SnapshotPart + | PatchPart + | AgentPart + | SkillPart + | RetryPart + | CompactionPart + +export type Prompt = { + text: string + files?: Array + agents?: Array + skills?: Array +} + +export type Pty = { + id: string + title: string + command: string + args: Array + cwd: string + status: "running" | "exited" + pid: number + exitCode?: number +} + +export type Todo = { + /** + * Brief description of the task + */ + content: string + /** + * Current status of the task: pending, in_progress, completed, cancelled + */ + status: string + /** + * Priority level of the task: high, medium, low + */ + priority: string +} + +export type SessionStatus = + | { + type: "idle" + } + | { + type: "retry" + attempt: number + message: string + action?: { + reason: string + provider: string + title: string + message: string + label: string + link?: string + } + next: number + } + | { + type: "busy" + } + +export type QuestionOption = { + /** + * Display text (1-5 words, concise) + */ + label: string + /** + * Explanation of choice + */ + description: string +} + +export type QuestionInfo = { + /** + * Complete question + */ + question: string + /** + * Very short label (max 30 chars) + */ + header: string + /** + * Available choices + */ + options: Array + multiple?: boolean + /** + * Question shape: "choice" (default, an option list) or "text" (a free-form text card, no options) + */ + kind?: "choice" | "text" + custom?: boolean +} + +export type QuestionTool = { + messageID: string + callID: string +} + +export type QuestionAnswer = Array + +export type GlobalEvent = { + directory: string + project?: string + workspace?: string + payload: + | { + id: string + type: "models-dev.refreshed" + properties: { + [key: string]: unknown + } + } + | { + id: string + type: "integration.updated" + properties: { + [key: string]: unknown + } + } + | { + id: string + type: "integration.connection.updated" + properties: { + integrationID: string + } + } + | { + id: string + type: "catalog.updated" + properties: { + [key: string]: unknown + } + } + | { + id: string + type: "session.created" + properties: { + sessionID: string + info: Session + } + } + | { + id: string + type: "session.updated" + properties: { + sessionID: string + info: Session + } + } + | { + id: string + type: "session.deleted" + properties: { + sessionID: string + info: Session + } + } + | { + id: string + type: "message.updated" + properties: { + sessionID: string + info: Message + } + } + | { + id: string + type: "message.removed" + properties: { + sessionID: string + messageID: string + } + } + | { + id: string + type: "message.part.updated" + properties: { + sessionID: string + part: Part + time: number + } + } + | { + id: string + type: "message.part.removed" + properties: { + sessionID: string + messageID: string + partID: string + } + } + | { + id: string + type: "session.next.agent.switched" + properties: { + timestamp: number + sessionID: string + messageID: string + agent: string + } + } + | { + id: string + type: "session.next.model.switched" + properties: { + timestamp: number + sessionID: string + messageID: string + model: ModelRef + } + } + | { + id: string + type: "session.next.moved" + properties: { + timestamp: number + sessionID: string + location: LocationRef + subdirectory?: string + } + } + | { + id: string + type: "session.next.prompted" + properties: { + timestamp: number + sessionID: string + messageID: string + prompt: Prompt + delivery: "steer" | "queue" + } + } + | { + id: string + type: "session.next.prompt.admitted" + properties: { + timestamp: number + sessionID: string + messageID: string + prompt: Prompt + delivery: "steer" | "queue" + } + } + | { + id: string + type: "session.next.context.updated" + properties: { + timestamp: number + sessionID: string + messageID: string + text: string + } + } + | { + id: string + type: "session.next.synthetic" + properties: { + timestamp: number + sessionID: string + messageID: string + text: string + } + } + | { + id: string + type: "session.next.shell.started" + properties: { + timestamp: number + sessionID: string + messageID: string + callID: string + command: string + } + } + | { + id: string + type: "session.next.shell.ended" + properties: { + timestamp: number + sessionID: string + callID: string + output: string + } + } + | { + id: string + type: "session.next.step.started" + properties: { + timestamp: number + sessionID: string + assistantMessageID: string + agent: string + model: ModelRef + snapshot?: string + } + } + | { + id: string + type: "session.next.step.ended" + properties: { + timestamp: number + sessionID: string + assistantMessageID: string + finish: string + cost: number + tokens: { + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } + } + snapshot?: string + files?: Array + } + } + | { + id: string + type: "session.next.step.failed" + properties: { + timestamp: number + sessionID: string + assistantMessageID: string + error: SessionErrorUnknown + } + } + | { + id: string + type: "session.next.text.started" + properties: { + timestamp: number + sessionID: string + assistantMessageID: string + textID: string + } + } + | { + id: string + type: "session.next.text.delta" + properties: { + timestamp: number + sessionID: string + assistantMessageID: string + textID: string + delta: string + } + } + | { + id: string + type: "session.next.text.ended" + properties: { + timestamp: number + sessionID: string + assistantMessageID: string + textID: string + text: string + } + } + | { + id: string + type: "session.next.reasoning.started" + properties: { + timestamp: number + sessionID: string + assistantMessageID: string + reasoningID: string + providerMetadata?: LlmProviderMetadata + } + } + | { + id: string + type: "session.next.reasoning.delta" + properties: { + timestamp: number + sessionID: string + assistantMessageID: string + reasoningID: string + delta: string + } + } + | { + id: string + type: "session.next.reasoning.ended" + properties: { + timestamp: number + sessionID: string + assistantMessageID: string + reasoningID: string + text: string + providerMetadata?: LlmProviderMetadata + } + } + | { + id: string + type: "session.next.tool.input.started" + properties: { + timestamp: number + sessionID: string + assistantMessageID: string + callID: string + name: string + } + } + | { + id: string + type: "session.next.tool.input.delta" + properties: { + timestamp: number + sessionID: string + assistantMessageID: string + callID: string + delta: string + } + } + | { + id: string + type: "session.next.tool.input.ended" + properties: { + timestamp: number + sessionID: string + assistantMessageID: string + callID: string + text: string + } + } + | { + id: string + type: "session.next.tool.called" + properties: { + timestamp: number + sessionID: string + assistantMessageID: string + callID: string + tool: string + input: { + [key: string]: unknown + } + provider: { + executed: boolean + metadata?: LlmProviderMetadata + } + } + } + | { + id: string + type: "session.next.tool.progress" + properties: { + timestamp: number + sessionID: string + assistantMessageID: string + callID: string + structured: { + [key: string]: unknown + } + content: Array + } + } + | { + id: string + type: "session.next.tool.success" + properties: { + timestamp: number + sessionID: string + assistantMessageID: string + callID: string + structured: { + [key: string]: unknown + } + content: Array + outputPaths?: Array + result?: unknown + provider: { + executed: boolean + metadata?: LlmProviderMetadata + } + } + } + | { + id: string + type: "session.next.tool.failed" + properties: { + timestamp: number + sessionID: string + assistantMessageID: string + callID: string + error: SessionErrorUnknown + result?: unknown + provider: { + executed: boolean + metadata?: LlmProviderMetadata + } + } + } + | { + id: string + type: "session.next.retried" + properties: { + timestamp: number + sessionID: string + attempt: number + error: SessionNextRetryError + } + } + | { + id: string + type: "session.next.compaction.started" + properties: { + timestamp: number + sessionID: string + messageID: string + reason: "auto" | "manual" + } + } + | { + id: string + type: "session.next.compaction.delta" + properties: { + timestamp: number + sessionID: string + messageID: string + text: string + } + } + | { + id: string + type: "session.next.compaction.ended" + properties: { + timestamp: number + sessionID: string + messageID: string + reason: "auto" | "manual" + text: string + recent: string + } + } + | { + id: string + type: "session.next.revert.staged" + properties: { + timestamp: number + sessionID: string + revert: RevertState + } + } + | { + id: string + type: "session.next.revert.cleared" + properties: { + timestamp: number + sessionID: string + } + } + | { + id: string + type: "session.next.revert.committed" + properties: { + timestamp: number + sessionID: string + messageID: string + } + } + | { + id: string + type: "message.part.delta" + properties: { + sessionID: string + messageID: string + partID: string + field: string + delta: string + } + } + | { + id: string + type: "session.diff" + properties: { + sessionID: string + diff: Array + } + } + | { + id: string + type: "session.error" + properties: { + sessionID?: string + error?: + | ProviderAuthError + | UnknownError + | MessageOutputLengthError + | MessageAbortedError + | StructuredOutputError + | ContextOverflowError + | ContentFilterError + | ApiError + } + } + | { + id: string + type: "installation.updated" + properties: { + version: string + } + } + | { + id: string + type: "installation.update-available" + properties: { + version: string + } + } + | { + id: string + type: "file.edited" + properties: { + file: string + } + } + | { + id: string + type: "reference.updated" + properties: { + [key: string]: unknown + } + } + | { + id: string + type: "permission.v2.asked" + properties: { + id: string + sessionID: string + action: string + resources: Array + save?: Array + metadata?: { + [key: string]: unknown + } + source?: PermissionV2Source + } + } + | { + id: string + type: "permission.v2.replied" + properties: { + sessionID: string + requestID: string + reply: PermissionV2Reply + } + } + | { + id: string + type: "plugin.added" + properties: { + id: string + } + } + | { + id: string + type: "project.directories.updated" + properties: { + projectID: string + } + } + | { + id: string + type: "file.watcher.updated" + properties: { + file: string + event: "add" | "change" | "unlink" + } + } + | { + id: string + type: "pty.created" + properties: { + info: Pty + } + } + | { + id: string + type: "pty.updated" + properties: { + info: Pty + } + } + | { + id: string + type: "pty.exited" + properties: { + id: string + exitCode: number + } + } + | { + id: string + type: "pty.deleted" + properties: { + id: string + } + } + | { + id: string + type: "question.v2.asked" + properties: { + id: string + sessionID: string + /** + * Questions to ask + */ + questions: Array + tool?: QuestionV2Tool + } + } + | { + id: string + type: "question.v2.replied" + properties: { + sessionID: string + requestID: string + answers: Array + } + } + | { + id: string + type: "question.v2.rejected" + properties: { + sessionID: string + requestID: string + } + } + | { + id: string + type: "todo.updated" + properties: { + sessionID: string + todos: Array + } + } + | { + id: string + type: "lsp.updated" + properties: { + [key: string]: unknown + } + } + | { + id: string + type: "permission.asked" + properties: { + id: string + sessionID: string + permission: string + patterns: Array + metadata: { + [key: string]: unknown + } + always: Array + tool?: { + messageID: string + callID: string + } + } + } + | { + id: string + type: "permission.replied" + properties: { + sessionID: string + requestID: string + reply: "once" | "always" | "reject" + } + } + | { + id: string + type: "tui.prompt.append" + properties: { + text: string + } + } + | { + id: string + type: "tui.command.execute" + properties: { + command: + | "session.list" + | "session.new" + | "session.share" + | "session.interrupt" + | "session.compact" + | "session.page.up" + | "session.page.down" + | "session.line.up" + | "session.line.down" + | "session.half.page.up" + | "session.half.page.down" + | "session.first" + | "session.last" + | "prompt.clear" + | "prompt.submit" + | "agent.cycle" + | string + } + } + | { + id: string + type: "tui.toast.show" + properties: { + title?: string + message: string + variant: "info" | "success" | "warning" | "error" + duration?: number + } + } + | { + id: string + type: "tui.session.select" + properties: { + /** + * Session ID to navigate to + */ + sessionID: string + } + } + | { + id: string + type: "mcp.tools.changed" + properties: { + server: string + } + } + | { + id: string + type: "mcp.browser.open.failed" + properties: { + mcpName: string + url: string + } + } + | { + id: string + type: "command.executed" + properties: { + name: string + sessionID: string + arguments: string + messageID: string + } + } + | { + id: string + type: "project.updated" + properties: { + id: string + worktree: string + vcs?: ProjectVcs + name?: string + icon?: ProjectIcon + commands?: ProjectCommands + time: ProjectTime + sandboxes: Array + } + } + | { + id: string + type: "session.status" + properties: { + sessionID: string + status: SessionStatus + } + } + | { + id: string + type: "session.idle" + properties: { + sessionID: string + } + } + | { + id: string + type: "question.asked" + properties: { + id: string + sessionID: string + /** + * Questions to ask + */ + questions: Array + tool?: QuestionTool + } + } + | { + id: string + type: "question.replied" + properties: { + sessionID: string + requestID: string + answers: Array + } + } + | { + id: string + type: "question.rejected" + properties: { + sessionID: string + requestID: string + } + } + | { + id: string + type: "session.compacted" + properties: { + sessionID: string + } + } + | { + id: string + type: "vcs.branch.updated" + properties: { + branch?: string + } + } + | { + id: string + type: "workspace.ready" + properties: { + name: string + } + } + | { + id: string + type: "workspace.failed" + properties: { + message: string + } + } + | { + id: string + type: "workspace.status" + properties: { + workspaceID: string + status: "connected" | "connecting" | "disconnected" | "error" + } + } + | { + id: string + type: "worktree.ready" + properties: { + name: string + branch?: string + } + } + | { + id: string + type: "worktree.failed" + properties: { + message: string + } + } + | { + id: string + type: "server.connected" + properties: { + [key: string]: unknown + } + } + | { + id: string + type: "global.disposed" + properties: { + [key: string]: unknown + } + } + | EventServerInstanceDisposed + | SyncEventSessionCreated + | SyncEventSessionUpdated + | SyncEventSessionDeleted + | SyncEventMessageUpdated + | SyncEventMessageRemoved + | SyncEventMessagePartUpdated + | SyncEventMessagePartRemoved + | SyncEventSessionNextAgentSwitched + | SyncEventSessionNextModelSwitched + | SyncEventSessionNextMoved + | SyncEventSessionNextPrompted + | SyncEventSessionNextPromptAdmitted + | SyncEventSessionNextContextUpdated + | SyncEventSessionNextSynthetic + | SyncEventSessionNextShellStarted + | SyncEventSessionNextShellEnded + | SyncEventSessionNextStepStarted + | SyncEventSessionNextStepEnded + | SyncEventSessionNextStepFailed + | SyncEventSessionNextTextStarted + | SyncEventSessionNextTextEnded + | SyncEventSessionNextReasoningStarted + | SyncEventSessionNextReasoningEnded + | SyncEventSessionNextToolInputStarted + | SyncEventSessionNextToolInputEnded + | SyncEventSessionNextToolCalled + | SyncEventSessionNextToolProgress + | SyncEventSessionNextToolSuccess + | SyncEventSessionNextToolFailed + | SyncEventSessionNextRetried + | SyncEventSessionNextCompactionStarted + | SyncEventSessionNextCompactionEnded + | SyncEventSessionNextRevertStaged + | SyncEventSessionNextRevertCleared + | SyncEventSessionNextRevertCommitted +} + +/** + * Log level + */ +export type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR" + +/** + * Server configuration for opencode serve and web commands + */ +export type ServerConfig = { + port?: number + hostname?: string + mdns?: boolean + mdnsDomain?: string + cors?: Array +} + +export type PermissionActionConfig = "ask" | "allow" | "deny" + +export type PermissionObjectConfig = { + [key: string]: PermissionActionConfig +} + +export type PermissionRuleConfig = PermissionActionConfig | PermissionObjectConfig + +export type PermissionConfig = + | PermissionActionConfig + | { + read?: PermissionRuleConfig + edit?: PermissionRuleConfig + glob?: PermissionRuleConfig + grep?: PermissionRuleConfig + list?: PermissionRuleConfig + bash?: PermissionRuleConfig + task?: PermissionRuleConfig + external_directory?: PermissionRuleConfig + todowrite?: PermissionActionConfig + question?: PermissionActionConfig + webfetch?: PermissionActionConfig + websearch?: PermissionActionConfig + lsp?: PermissionRuleConfig + doom_loop?: PermissionActionConfig + skill?: PermissionRuleConfig + [key: string]: PermissionRuleConfig | PermissionActionConfig | undefined + } + +export type AgentConfig = { + model?: string + variant?: string + temperature?: number + top_p?: number + prompt?: string + tools?: { + [key: string]: boolean + } + disable?: boolean + description?: string + mode?: "subagent" | "primary" | "all" + hidden?: boolean + options?: { + [key: string]: unknown + } + /** + * Hex color code (e.g., #FF5733) or theme color (e.g., primary) + */ + color?: string | "primary" | "secondary" | "accent" | "success" | "warning" | "error" | "info" + steps?: number + maxSteps?: number + permission?: PermissionConfig + [key: string]: + | unknown + | string + | number + | { + [key: string]: boolean + } + | boolean + | "subagent" + | "primary" + | "all" + | { + [key: string]: unknown + } + | string + | "primary" + | "secondary" + | "accent" + | "success" + | "warning" + | "error" + | "info" + | number + | PermissionConfig + | undefined +} + +export type ProviderConfig = { + api?: string + name?: string + env?: Array + id?: string + npm?: string + whitelist?: Array + blacklist?: Array + options?: { + apiKey?: string + baseURL?: string + enterpriseUrl?: string + setCacheKey?: boolean + /** + * Timeout in milliseconds for full requests to this provider. Set to false to disable timeout. + */ + timeout?: number | false + /** + * Timeout in milliseconds to wait for response headers. Provider integrations may set defaults. Set to false to disable timeout. + */ + headerTimeout?: number | false + chunkTimeout?: number + [key: string]: unknown | string | boolean | number | false | number | false | number | undefined + } + models?: { + [key: string]: { + id?: string + name?: string + family?: string + release_date?: string + attachment?: boolean + reasoning?: boolean + temperature?: boolean + tool_call?: boolean + interleaved?: + | boolean + | "reasoning" + | "reasoning_content" + | "reasoning_text" + | string + | { + field: "reasoning" | "reasoning_content" | "reasoning_text" | string + } + cost?: { + input: number + output: number + cache_read?: number + cache_write?: number + context_over_200k?: { + input: number + output: number + cache_read?: number + cache_write?: number + } + } + limit?: { + context: number + input?: number + output: number + } + modalities?: { + input?: Array<"text" | "audio" | "image" | "video" | "pdf"> + output?: Array<"text" | "audio" | "image" | "video" | "pdf"> + } + experimental?: boolean + status?: "alpha" | "beta" | "deprecated" | "active" + provider?: { + npm?: string + api?: string + } + options?: { + [key: string]: unknown + } + headers?: { + [key: string]: string + } + /** + * Variant-specific configuration + */ + variants?: { + [key: string]: { + disabled?: boolean + [key: string]: unknown | boolean | undefined + } + } + } + } +} + +export type McpLocalConfig = { + /** + * Type of MCP server connection + */ + type: "local" + /** + * Command and arguments to run the MCP server + */ + command: Array + cwd?: string + environment?: { + [key: string]: string + } + enabled?: boolean + timeout?: number +} + +export type McpOAuthConfig = { + clientId?: string + clientSecret?: string + scope?: string + callbackPort?: number + redirectUri?: string +} + +export type McpRemoteConfig = { + /** + * Type of MCP server connection + */ + type: "remote" + /** + * URL of the remote MCP server + */ + url: string + enabled?: boolean + headers?: { + [key: string]: string + } + /** + * OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection. + */ + oauth?: McpOAuthConfig | false + timeout?: number +} + +/** + * @deprecated Always uses stretch layout. + */ +export type LayoutConfig = "auto" | "stretch" + +export type ImageAttachmentConfig = { + auto_resize?: boolean + max_width?: number + max_height?: number + max_base64_bytes?: number +} + +export type AttachmentConfig = { + image?: ImageAttachmentConfig +} + +export type Config = { + $schema?: string + shell?: string + logLevel?: LogLevel + server?: ServerConfig + command?: { + [key: string]: { + template: string + description?: string + agent?: string + model?: string + variant?: string + subtask?: boolean + } + } + skills?: { + paths?: Array + urls?: Array + } + references?: { + [key: string]: string | ConfigV2ReferenceGit | ConfigV2ReferenceLocal + } + reference?: { + [key: string]: string | ConfigV2ReferenceGit | ConfigV2ReferenceLocal + } + watcher?: { + ignore?: Array + } + snapshot?: boolean + plugin?: Array< + | string + | [ + string, + { + [key: string]: unknown + }, + ] + > + share?: "manual" | "auto" | "disabled" + autoshare?: boolean + /** + * Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications + */ + autoupdate?: boolean | "notify" + disabled_providers?: Array + enabled_providers?: Array + model?: string + small_model?: string + default_agent?: string + subagent_depth?: number + username?: string + mode?: { + build?: AgentConfig + plan?: AgentConfig + [key: string]: AgentConfig | undefined + } + agent?: { + plan?: AgentConfig + build?: AgentConfig + general?: AgentConfig + explore?: AgentConfig + title?: AgentConfig + summary?: AgentConfig + compaction?: AgentConfig + [key: string]: AgentConfig | undefined + } + provider?: { + [key: string]: ProviderConfig + } + mcp?: { + [key: string]: + | McpLocalConfig + | McpRemoteConfig + | { + enabled: boolean + } + } + /** + * Enable or configure formatters. Omit or set to false to disable, true to enable built-ins, or an object to enable built-ins with overrides. + */ + formatter?: + | boolean + | { + [key: string]: { + disabled?: boolean + command?: Array + environment?: { + [key: string]: string + } + extensions?: Array + } + } + /** + * Enable or configure LSP servers. Omit or set to false to disable, true to enable built-ins, or an object to enable built-ins with overrides. + */ + lsp?: + | boolean + | { + [key: string]: + | { + disabled: true + } + | { + command: Array + extensions?: Array + disabled?: boolean + env?: { + [key: string]: string + } + initialization?: { + [key: string]: unknown + } + } + } + instructions?: Array + layout?: LayoutConfig + permission?: PermissionConfig + tools?: { + [key: string]: boolean + } + attachment?: AttachmentConfig + enterprise?: { + url?: string + } + tool_output?: { + max_lines?: number + max_bytes?: number + } + compaction?: { + auto?: boolean + prune?: boolean + tail_turns?: number + preserve_recent_tokens?: number + reserved?: number + } + experimental?: { + disable_paste_summary?: boolean + batch_tool?: boolean + openTelemetry?: boolean + primary_tools?: Array + continue_loop_on_deny?: boolean + mcp_timeout?: number + policies?: Array + } +} + +export type Model = { + id: string + providerID: string + api: { + id: string + url: string + npm: string + } + name: string + family?: string + capabilities: { + temperature: boolean + reasoning: boolean + attachment: boolean + toolcall: boolean + input: { + text: boolean + audio: boolean + image: boolean + video: boolean + pdf: boolean + } + output: { + text: boolean + audio: boolean + image: boolean + video: boolean + pdf: boolean + } + interleaved: + | boolean + | { + field: "reasoning" | "reasoning_content" | "reasoning_text" | string + } + } + cost: { + input: number + output: number + cache: { + read: number + write: number + } + tiers?: Array<{ + input: number + output: number + cache: { + read: number + write: number + } + tier: { + type: "context" + size: number + } + }> + experimentalOver200K?: { + input: number + output: number + cache: { + read: number + write: number + } + } + } + limit: { + context: number + input?: number + output: number + } + status: "alpha" | "beta" | "deprecated" | "active" + options: { + [key: string]: unknown + } + headers: { + [key: string]: string + } + release_date: string + variants?: { + [key: string]: { + [key: string]: unknown + } + } +} + +export type Provider = { + id: string + name: string + source: "env" | "config" | "custom" | "api" + env: Array + key?: string + options: { + [key: string]: unknown + } + models: { + [key: string]: Model + } +} + +export type ExperimentalCapabilities = { + backgroundSubagents: boolean +} + +export type ConsoleState = { + consoleManagedProviders: Array + activeOrgName?: string + switchableOrgCount: number +} + +export type EffectHttpApiErrorInternalServerError = { + _tag: "InternalServerError" +} + +export type ToolListItem = { + id: string + description: string + parameters: unknown +} + +export type ToolList = Array + +export type ToolIds = Array + +export type WorktreeError = { + name: + | "WorktreeNotGitError" + | "WorktreeNameGenerationFailedError" + | "WorktreeCreateFailedError" + | "WorktreeStartCommandFailedError" + | "WorktreeRemoveFailedError" + | "WorktreeResetFailedError" + | "WorktreeListFailedError" + data: { + message: string + } +} + +export type WorktreeCreateInput = { + name?: string + /** + * Additional startup script to run after the project's start command + */ + startCommand?: string +} + +export type Worktree = { + name: string + branch?: string + directory: string +} + +export type WorktreeRemoveInput = { + directory: string +} + +export type WorktreeResetInput = { + directory: string +} + +export type ProjectSummary = { + id: string + name?: string + worktree: string +} + +export type GlobalSession = { + id: string + slug: string + projectID: string + workspaceID?: string + directory: string + path?: string + parentID?: string + summary?: { + additions: number + deletions: number + files: number + diffs?: Array + } + cost?: number + tokens?: { + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } + } + share?: { + url: string + } + title: string + agent?: string + model?: { + id: string + providerID: string + variant?: string + } + version: string + metadata?: { + [key: string]: unknown + } + time: { + created: number + updated: number + compacting?: number + archived?: number + } + permission?: PermissionRuleset + revert?: { + messageID: string + partID?: string + snapshot?: string + diff?: string + } + project: ProjectSummary | null +} + +export type McpResource = { + name: string + uri: string + description?: string + mimeType?: string + client: string +} + +export type Symbol = { + name: string + kind: number + location: { + uri: string + range: Range + } +} + +export type FileNode = { + name: string + path: string + absolute: string + type: "file" | "directory" + ignored: boolean +} + +export type FileContent = { + type: "text" | "binary" + content: string + diff?: string + patch?: { + oldFileName: string + newFileName: string + oldHeader?: string + newHeader?: string + hunks: Array<{ + oldStart: number + oldLines: number + newStart: number + newLines: number + lines: Array + }> + index?: string + } + encoding?: "base64" + mimeType?: string +} + +export type File = { + path: string + added: number + removed: number + status: "added" | "deleted" | "modified" +} + +export type Path = { + home: string + state: string + config: string + worktree: string + directory: string +} + +export type VcsInfo = { + branch?: string + default_branch?: string +} + +export type VcsFileStatus = { + file: string + additions: number + deletions: number + status: "added" | "deleted" | "modified" +} + +export type VcsFileDiff = { + file: string + patch?: string + additions: number + deletions: number + status?: "added" | "deleted" | "modified" +} + +export type VcsApplyError = { + name: "VcsApplyError" + data: { + message: string + reason: "non-git" | "not-clean" + } +} + +export type Command = { + name: string + description?: string + agent?: string + model?: string + source?: "command" | "mcp" | "skill" + template: string + subtask?: boolean + hints: Array +} + +export type Agent = { + name: string + description?: string + mode: "subagent" | "primary" | "all" + native?: boolean + hidden?: boolean + topP?: number + temperature?: number + color?: string + permission: PermissionRuleset + model?: { + modelID: string + providerID: string + } + variant?: string + prompt?: string + options: { + [key: string]: unknown + } + steps?: number +} + +export type LspStatus = { + id: string + name: string + root: string + status: "connected" | "error" +} + +export type FormatterStatus = { + name: string + extensions: Array + enabled: boolean +} + +export type McpStatusConnected = { + status: "connected" +} + +export type McpStatusDisabled = { + status: "disabled" +} + +export type McpStatusFailed = { + status: "failed" + error: string +} + +export type McpStatusNeedsAuth = { + status: "needs_auth" +} + +export type McpStatusNeedsClientRegistration = { + status: "needs_client_registration" + error: string +} + +export type McpStatus = + | McpStatusConnected + | McpStatusDisabled + | McpStatusFailed + | McpStatusNeedsAuth + | McpStatusNeedsClientRegistration + +export type McpUnsupportedOAuthError = { + error: string +} + +export type McpServerNotFoundError = { + _tag: "McpServerNotFoundError" + name: string + message: string +} + +export type Project = { + id: string + worktree: string + vcs?: ProjectVcs + name?: string + icon?: ProjectIcon + commands?: ProjectCommands + time: ProjectTime + sandboxes: Array +} + +export type ProjectNotFoundError = { + _tag: "ProjectNotFoundError" + projectID: string + message: string +} + +export type PtyNotFoundError = { + _tag: "PtyNotFoundError" + ptyID: string + message: string +} + +export type PtyForbiddenError = { + _tag: "PtyForbiddenError" + message: string +} + +export type QuestionRequest = { + id: string + sessionID: string + /** + * Questions to ask + */ + questions: Array + tool?: QuestionTool +} + +export type QuestionNotFoundError = { + _tag: "QuestionNotFoundError" + requestID: string + message: string +} + +export type PermissionRequest = { + id: string + sessionID: string + permission: string + patterns: Array + metadata: { + [key: string]: unknown + } + always: Array + tool?: { + messageID: string + callID: string + } +} + +export type PermissionNotFoundError = { + _tag: "PermissionNotFoundError" + requestID: string + message: string +} + +export type ProviderAuthMethod = { + type: "oauth" | "api" + label: string + prompts?: Array< + | { + type: "text" + key: string + message: string + placeholder?: string + when?: { + key: string + op: "eq" | "neq" + value: string + } + } + | { + type: "select" + key: string + message: string + options: Array<{ + label: string + value: string + hint?: string + }> + when?: { + key: string + op: "eq" | "neq" + value: string + } + } + > +} + +export type ProviderAuthAuthorization = { + url: string + method: "auto" | "code" + instructions: string +} + +export type ProviderAuthError1 = { + name: + | "BadRequest" + | "ProviderAuthOauthMissing" + | "ProviderAuthOauthCodeMissing" + | "ProviderAuthOauthCallbackFailed" + | "ProviderAuthValidationFailed" + data: { + providerID?: string + field?: string + message?: string + kind?: string + } +} + +export type NotFoundError = { + name: "NotFoundError" + data: { + message: string + } +} + +export type TextPartInput = { + id?: string + type: "text" + text: string + synthetic?: boolean + ignored?: boolean + time?: { + start: number + end?: number + } + metadata?: { + [key: string]: unknown + } +} + +export type FilePartInput = { + id?: string + type: "file" + mime: string + filename?: string + url: string + source?: FilePartSource +} + +export type AgentPartInput = { + id?: string + type: "agent" + name: string + source?: { + value: string + start: number + end: number + } +} + +export type SkillPartInput = { + id?: string + type: "skill" + name: string + description?: string + content?: string + source?: { + value: string + start: number + end: number + } +} + +export type SubtaskPartInput = { + id?: string + type: "subtask" + prompt: string + description: string + agent: string + model?: { + providerID: string + modelID: string + } + command?: string +} + +export type SessionBusyError = { + _tag: "SessionBusyError" + sessionID: string + message: string +} + +export type EventTuiPromptAppend = { + type: "tui.prompt.append" + properties: { + text: string + } +} + +export type EventTuiCommandExecute = { + type: "tui.command.execute" + properties: { + command: + | "session.list" + | "session.new" + | "session.share" + | "session.interrupt" + | "session.compact" + | "session.page.up" + | "session.page.down" + | "session.line.up" + | "session.line.down" + | "session.half.page.up" + | "session.half.page.down" + | "session.first" + | "session.last" + | "prompt.clear" + | "prompt.submit" + | "agent.cycle" + | string + } +} + +export type EventTuiToastShow = { + type: "tui.toast.show" + properties: { + title?: string + message: string + variant: "info" | "success" | "warning" | "error" + duration?: number + } +} + +export type EventTuiSessionSelect = { + type: "tui.session.select" + properties: { + /** + * Session ID to navigate to + */ + sessionID: string + } +} + +export type Workspace = { + id: string + type: string + name: string + branch?: string | null + directory?: string | null + extra?: unknown | null + projectID: string + timeUsed: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" +} + +export type WorkspaceCreateError = { + name: "WorkspaceCreateError" + data: { + message: string + } +} + +export type WorkspaceWarpError = { + name: "WorkspaceWarpError" + data: { + message: string + } +} + +export type UnauthorizedError = { + _tag: "UnauthorizedError" + message: string +} + +export type SessionsResponse = { + data: Array + cursor: { + previous?: string + next?: string + } +} + +export type InvalidCursorError = { + _tag: "InvalidCursorError" + message: string +} + +export type SessionActive = { + type: "running" +} + +export type SessionNotFoundError = { + _tag: "SessionNotFoundError" + sessionID: string + message: string +} + +export type PromptInput = { + text: string + files?: Array + agents?: Array +} + +export type ConflictError = { + _tag: "ConflictError" + message: string + resource?: string +} + +export type ServiceUnavailableError = { + _tag: "ServiceUnavailableError" + message: string + service?: string +} + +export type MessageNotFoundError = { + _tag: "MessageNotFoundError" + sessionID: string + messageID: string + message: string +} + +export type UnknownError1 = { + _tag: "UnknownError" + message: string + ref?: string +} + +export type SessionDurableEvent = + | SessionNextAgentSwitched + | SessionNextModelSwitched + | SessionNextMoved + | SessionNextPrompted + | SessionNextPromptAdmitted + | SessionNextContextUpdated + | SessionNextSynthetic + | SessionNextShellStarted + | SessionNextShellEnded + | SessionNextStepStarted + | SessionNextStepEnded + | SessionNextStepFailed + | SessionNextTextStarted + | SessionNextTextEnded + | SessionNextToolInputStarted + | SessionNextToolInputEnded + | SessionNextToolCalled + | SessionNextToolProgress + | SessionNextToolSuccess + | SessionNextToolFailed + | SessionNextReasoningStarted + | SessionNextReasoningEnded + | SessionNextRetried + | SessionNextCompactionStarted + | SessionNextCompactionEnded + | SessionNextRevertStaged + | SessionNextRevertCleared + | SessionNextRevertCommitted + +export type SessionHistory = { + data: Array + hasMore: boolean +} + +export type SessionDurableEventStream = string + +export type SessionMessagesResponse = { + data: Array + cursor: { + previous?: string + next?: string + } +} + +export type ProviderNotFoundError = { + _tag: "ProviderNotFoundError" + providerID: string + message: string +} + +export type OutputFormat1 = + | { + type: "text" + } + | { + type: "json_schema" + schema: JsonSchema + retryCount?: number + } + +export type SessionStatus2 = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.status" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + sessionID: string + status: SessionStatus + } +} + +export type QuestionReplied2 = { + id: string + metadata?: { + [key: string]: unknown + } + type: "question.replied" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + sessionID: string + requestID: string + answers: Array + } +} + +export type QuestionRejected2 = { + id: string + metadata?: { + [key: string]: unknown + } + type: "question.rejected" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + sessionID: string + requestID: string + } +} + +export type V2Event = + | ModelsDevRefreshed + | IntegrationUpdated + | IntegrationConnectionUpdated + | CatalogUpdated + | SessionCreated + | SessionUpdated + | SessionDeleted + | MessageUpdated + | MessageRemoved + | MessagePartUpdated + | MessagePartRemoved + | SessionNextAgentSwitched + | SessionNextModelSwitched + | SessionNextMoved + | SessionNextPrompted + | SessionNextPromptAdmitted + | SessionNextContextUpdated + | SessionNextSynthetic + | SessionNextShellStarted + | SessionNextShellEnded + | SessionNextStepStarted + | SessionNextStepEnded + | SessionNextStepFailed + | SessionNextTextStarted + | SessionNextTextDelta + | SessionNextTextEnded + | SessionNextReasoningStarted + | SessionNextReasoningDelta + | SessionNextReasoningEnded + | SessionNextToolInputStarted + | SessionNextToolInputDelta + | SessionNextToolInputEnded + | SessionNextToolCalled + | SessionNextToolProgress + | SessionNextToolSuccess + | SessionNextToolFailed + | SessionNextRetried + | SessionNextCompactionStarted + | SessionNextCompactionDelta + | SessionNextCompactionEnded + | SessionNextRevertStaged + | SessionNextRevertCleared + | SessionNextRevertCommitted + | MessagePartDelta + | SessionDiff + | SessionError + | InstallationUpdated + | InstallationUpdateAvailable + | FileEdited + | ReferenceUpdated + | PermissionV2Asked + | PermissionV2Replied + | PluginAdded + | ProjectDirectoriesUpdated + | FileWatcherUpdated + | PtyCreated + | PtyUpdated + | PtyExited + | PtyDeleted + | QuestionV2Asked + | QuestionV2Replied + | QuestionV2Rejected + | TodoUpdated + | LspUpdated + | PermissionAsked + | PermissionReplied + | TuiPromptAppend + | TuiCommandExecute + | TuiToastShow + | TuiSessionSelect + | McpToolsChanged + | McpBrowserOpenFailed + | CommandExecuted + | ProjectUpdated + | SessionStatus2 + | SessionIdle + | QuestionAsked + | QuestionReplied2 + | QuestionRejected2 + | SessionCompacted + | VcsBranchUpdated + | WorkspaceReady + | WorkspaceFailed + | WorkspaceStatus + | WorktreeReady + | WorktreeFailed + | ServerConnected + | GlobalDisposed + +export type V2EventStream = string + +export type ForbiddenError = { + _tag: "ForbiddenError" + message: string +} + +export type ProjectCopyError = { + name: "ProjectCopyError" + data: { + message: string + forceRequired?: boolean + } +} + +export type EffectHttpApiErrorForbidden = { + _tag: "Forbidden" +} + +export type EventTuiPromptAppend2 = { + id: string + type: "tui.prompt.append" + properties: { + text: string + } +} + +export type EventTuiCommandExecute2 = { + id: string + type: "tui.command.execute" + properties: { + command: + | "session.list" + | "session.new" + | "session.share" + | "session.interrupt" + | "session.compact" + | "session.page.up" + | "session.page.down" + | "session.line.up" + | "session.line.down" + | "session.half.page.up" + | "session.half.page.down" + | "session.first" + | "session.last" + | "prompt.clear" + | "prompt.submit" + | "agent.cycle" + | string + } +} + +export type EventTuiToastShow2 = { + id: string + type: "tui.toast.show" + properties: { + title?: string + message: string + variant: "info" | "success" | "warning" | "error" + duration?: number + } +} + +export type EventTuiSessionSelect2 = { + id: string + type: "tui.session.select" + properties: { + /** + * Session ID to navigate to + */ + sessionID: string + } +} + +export type CredentialValue = CredentialOAuth | CredentialKey + +export type IntegrationInputs = { + [key: string]: string +} + +export type IntegrationMethod = IntegrationOAuthMethod | IntegrationKeyMethod | IntegrationEnvMethod + +export type IntegrationRef = { + id: string + name: string +} + +export type SkillV2Source = SkillV2DirectorySource | SkillV2UrlSource | SkillV2EmbeddedSource + +export type MoveSessionDestination = { + directory: string +} + +export type ModelRef = { + id: string + providerID: string + variant?: string +} + +export type LocationRef = { + directory: string + workspaceID?: string +} + +export type PromptSource = { + start: number + end: number + text: string +} + +export type PromptFileAttachment = { + uri: string + mime: string + name?: string + description?: string + source?: PromptSource +} + +export type PromptAgentAttachment = { + name: string + source?: PromptSource +} + +export type PromptSkillAttachment = { + name: string + description?: string + content?: string + source?: PromptSource +} + +export type SessionErrorUnknown = { + type: "unknown" + message: string +} + +export type LlmProviderMetadata = { + [key: string]: { + [key: string]: unknown + } +} + +export type ToolTextContent = { + type: "text" + text: string +} + +export type ToolFileContent = { + type: "file" + uri: string + mime: string + name?: string +} + +export type LlmToolContent = ToolTextContent | ToolFileContent + +export type SessionNextRetryError = { + message: string + statusCode?: number + isRetryable: boolean + responseHeaders?: { + [key: string]: string + } + responseBody?: string + metadata?: { + [key: string]: string + } +} + +export type FileDiff = { + path: string + status: "added" | "modified" | "deleted" + additions: number + deletions: number + patch: string +} + +export type RevertState = { + messageID: string + partID?: string + snapshot?: string + diff?: string + files?: Array +} + +export type PermissionV2Source = { + type: "tool" + messageID: string + callID: string +} + +export type PermissionV2Reply = "once" | "always" | "reject" + +export type QuestionV2Option = { + /** + * Display text (1-5 words, concise) + */ + label: string + /** + * Explanation of choice + */ + description: string +} + +export type QuestionV2Info = { + /** + * Complete question + */ + question: string + /** + * Very short label (max 30 chars) + */ + header: string + /** + * Available choices + */ + options: Array + multiple?: boolean + /** + * Question shape: "choice" (default, an option list) or "text" (a free-form text card, no options) + */ + kind?: "choice" | "text" + custom?: boolean +} + +export type QuestionV2Tool = { + messageID: string + callID: string +} + +export type QuestionV2Answer = Array + +export type ProjectVcs = "git" + +export type ProjectIcon = { + url?: string + override?: string + color?: string +} + +export type ProjectCommands = { + /** + * Startup script to run when creating a new workspace (worktree) + */ + start?: string +} + +export type ProjectTime = { + created: number + updated: number + initialized?: number +} + +export type EventServerInstanceDisposed = { + id: string + type: "server.instance.disposed" + properties: { + directory: string + } +} + +export type SyncEventSessionCreated = { + type: "sync" + id: string + syncEvent: { + type: "session.created.1" + id: string + seq: number + aggregateID: string + data: { + sessionID: string + info: Session + } + } +} + +export type SyncEventSessionUpdated = { + type: "sync" + id: string + syncEvent: { + type: "session.updated.1" + id: string + seq: number + aggregateID: string + data: { + sessionID: string + info: Session + } + } +} + +export type SyncEventSessionDeleted = { + type: "sync" + id: string + syncEvent: { + type: "session.deleted.1" + id: string + seq: number + aggregateID: string + data: { + sessionID: string + info: Session + } + } +} + +export type SyncEventMessageUpdated = { + type: "sync" + id: string + syncEvent: { + type: "message.updated.1" + id: string + seq: number + aggregateID: string + data: { + sessionID: string + info: Message + } + } +} + +export type SyncEventMessageRemoved = { + type: "sync" + id: string + syncEvent: { + type: "message.removed.1" + id: string + seq: number + aggregateID: string + data: { + sessionID: string + messageID: string + } + } +} + +export type SyncEventMessagePartUpdated = { + type: "sync" + id: string + syncEvent: { + type: "message.part.updated.1" + id: string + seq: number + aggregateID: string + data: { + sessionID: string + part: Part + time: number + } + } +} + +export type SyncEventMessagePartRemoved = { + type: "sync" + id: string + syncEvent: { + type: "message.part.removed.1" + id: string + seq: number + aggregateID: string + data: { + sessionID: string + messageID: string + partID: string + } + } +} + +export type SyncEventSessionNextAgentSwitched = { + type: "sync" + id: string + syncEvent: { + type: "session.next.agent.switched.1" + id: string + seq: number + aggregateID: string + data: { + timestamp: number + sessionID: string + messageID: string + agent: string + } + } +} + +export type SyncEventSessionNextModelSwitched = { + type: "sync" + id: string + syncEvent: { + type: "session.next.model.switched.1" + id: string + seq: number + aggregateID: string + data: { + timestamp: number + sessionID: string + messageID: string + model: ModelRef + } + } +} + +export type SyncEventSessionNextMoved = { + type: "sync" + id: string + syncEvent: { + type: "session.next.moved.1" + id: string + seq: number + aggregateID: string + data: { + timestamp: number + sessionID: string + location: LocationRef + subdirectory?: string + } + } +} + +export type SyncEventSessionNextPrompted = { + type: "sync" + id: string + syncEvent: { + type: "session.next.prompted.1" + id: string + seq: number + aggregateID: string + data: { + timestamp: number + sessionID: string + messageID: string + prompt: Prompt + delivery: "steer" | "queue" + } + } +} + +export type SyncEventSessionNextPromptAdmitted = { + type: "sync" + id: string + syncEvent: { + type: "session.next.prompt.admitted.1" + id: string + seq: number + aggregateID: string + data: { + timestamp: number + sessionID: string + messageID: string + prompt: Prompt + delivery: "steer" | "queue" + } + } +} + +export type SyncEventSessionNextContextUpdated = { + type: "sync" + id: string + syncEvent: { + type: "session.next.context.updated.1" + id: string + seq: number + aggregateID: string + data: { + timestamp: number + sessionID: string + messageID: string + text: string + } + } +} + +export type SyncEventSessionNextSynthetic = { + type: "sync" + id: string + syncEvent: { + type: "session.next.synthetic.1" + id: string + seq: number + aggregateID: string + data: { + timestamp: number + sessionID: string + messageID: string + text: string + } + } +} + +export type SyncEventSessionNextShellStarted = { + type: "sync" + id: string + syncEvent: { + type: "session.next.shell.started.1" + id: string + seq: number + aggregateID: string + data: { + timestamp: number + sessionID: string + messageID: string + callID: string + command: string + } + } +} + +export type SyncEventSessionNextShellEnded = { + type: "sync" + id: string + syncEvent: { + type: "session.next.shell.ended.1" + id: string + seq: number + aggregateID: string + data: { + timestamp: number + sessionID: string + callID: string + output: string + } + } +} + +export type SyncEventSessionNextStepStarted = { + type: "sync" + id: string + syncEvent: { + type: "session.next.step.started.1" + id: string + seq: number + aggregateID: string + data: { + timestamp: number + sessionID: string + assistantMessageID: string + agent: string + model: ModelRef + snapshot?: string + } + } +} + +export type SyncEventSessionNextStepEnded = { + type: "sync" + id: string + syncEvent: { + type: "session.next.step.ended.2" + id: string + seq: number + aggregateID: string + data: { + timestamp: number + sessionID: string + assistantMessageID: string + finish: string + cost: number + tokens: { + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } + } + snapshot?: string + files?: Array + } + } +} + +export type SyncEventSessionNextStepFailed = { + type: "sync" + id: string + syncEvent: { + type: "session.next.step.failed.2" + id: string + seq: number + aggregateID: string + data: { + timestamp: number + sessionID: string + assistantMessageID: string + error: SessionErrorUnknown + } + } +} + +export type SyncEventSessionNextTextStarted = { + type: "sync" + id: string + syncEvent: { + type: "session.next.text.started.1" + id: string + seq: number + aggregateID: string + data: { + timestamp: number + sessionID: string + assistantMessageID: string + textID: string + } + } +} + +export type SyncEventSessionNextTextEnded = { + type: "sync" + id: string + syncEvent: { + type: "session.next.text.ended.1" + id: string + seq: number + aggregateID: string + data: { + timestamp: number + sessionID: string + assistantMessageID: string + textID: string + text: string + } + } +} + +export type SyncEventSessionNextReasoningStarted = { + type: "sync" + id: string + syncEvent: { + type: "session.next.reasoning.started.1" + id: string + seq: number + aggregateID: string + data: { + timestamp: number + sessionID: string + assistantMessageID: string + reasoningID: string + providerMetadata?: LlmProviderMetadata + } + } +} + +export type SyncEventSessionNextReasoningEnded = { + type: "sync" + id: string + syncEvent: { + type: "session.next.reasoning.ended.1" + id: string + seq: number + aggregateID: string + data: { + timestamp: number + sessionID: string + assistantMessageID: string + reasoningID: string + text: string + providerMetadata?: LlmProviderMetadata + } + } +} + +export type SyncEventSessionNextToolInputStarted = { + type: "sync" + id: string + syncEvent: { + type: "session.next.tool.input.started.1" + id: string + seq: number + aggregateID: string + data: { + timestamp: number + sessionID: string + assistantMessageID: string + callID: string + name: string + } + } +} + +export type SyncEventSessionNextToolInputEnded = { + type: "sync" + id: string + syncEvent: { + type: "session.next.tool.input.ended.1" + id: string + seq: number + aggregateID: string + data: { + timestamp: number + sessionID: string + assistantMessageID: string + callID: string + text: string + } + } +} + +export type SyncEventSessionNextToolCalled = { + type: "sync" + id: string + syncEvent: { + type: "session.next.tool.called.1" + id: string + seq: number + aggregateID: string + data: { + timestamp: number + sessionID: string + assistantMessageID: string + callID: string + tool: string + input: { + [key: string]: unknown + } + provider: { + executed: boolean + metadata?: LlmProviderMetadata + } + } + } +} + +export type SyncEventSessionNextToolProgress = { + type: "sync" + id: string + syncEvent: { + type: "session.next.tool.progress.1" + id: string + seq: number + aggregateID: string + data: { + timestamp: number + sessionID: string + assistantMessageID: string + callID: string + structured: { + [key: string]: unknown + } + content: Array + } + } +} + +export type SyncEventSessionNextToolSuccess = { + type: "sync" + id: string + syncEvent: { + type: "session.next.tool.success.1" + id: string + seq: number + aggregateID: string + data: { + timestamp: number + sessionID: string + assistantMessageID: string + callID: string + structured: { + [key: string]: unknown + } + content: Array + outputPaths?: Array + result?: unknown + provider: { + executed: boolean + metadata?: LlmProviderMetadata + } + } + } +} + +export type SyncEventSessionNextToolFailed = { + type: "sync" + id: string + syncEvent: { + type: "session.next.tool.failed.1" + id: string + seq: number + aggregateID: string + data: { + timestamp: number + sessionID: string + assistantMessageID: string + callID: string + error: SessionErrorUnknown + result?: unknown + provider: { + executed: boolean + metadata?: LlmProviderMetadata + } + } + } +} + +export type SyncEventSessionNextRetried = { + type: "sync" + id: string + syncEvent: { + type: "session.next.retried.1" + id: string + seq: number + aggregateID: string + data: { + timestamp: number + sessionID: string + attempt: number + error: SessionNextRetryError + } + } +} + +export type SyncEventSessionNextCompactionStarted = { + type: "sync" + id: string + syncEvent: { + type: "session.next.compaction.started.1" + id: string + seq: number + aggregateID: string + data: { + timestamp: number + sessionID: string + messageID: string + reason: "auto" | "manual" + } + } +} + +export type SyncEventSessionNextCompactionEnded = { + type: "sync" + id: string + syncEvent: { + type: "session.next.compaction.ended.1" + id: string + seq: number + aggregateID: string + data: { + timestamp: number + sessionID: string + messageID: string + reason: "auto" | "manual" + text: string + recent: string + } + } +} + +export type SyncEventSessionNextRevertStaged = { + type: "sync" + id: string + syncEvent: { + type: "session.next.revert.staged.1" + id: string + seq: number + aggregateID: string + data: { + timestamp: number + sessionID: string + revert: RevertState + } + } +} + +export type SyncEventSessionNextRevertCleared = { + type: "sync" + id: string + syncEvent: { + type: "session.next.revert.cleared.1" + id: string + seq: number + aggregateID: string + data: { + timestamp: number + sessionID: string + } + } +} + +export type SyncEventSessionNextRevertCommitted = { + type: "sync" + id: string + syncEvent: { + type: "session.next.revert.committed.1" + id: string + seq: number + aggregateID: string + data: { + timestamp: number + sessionID: string + messageID: string + } + } +} + +export type ConfigV2ReferenceGit = { + repository: string + branch?: string + description?: string + hidden?: boolean +} + +export type ConfigV2ReferenceLocal = { + path: string + description?: string + hidden?: boolean +} + +export type PolicyEffect = "allow" | "deny" + +export type ConfigV2ExperimentalPolicy = { + action: "provider.use" + effect: PolicyEffect + resource: string +} + +export type ProjectDirectories = Array<{ + directory: string + strategy?: string +}> + +export type PtyTicketConnectToken = { + ticket: string + expires_in: number +} + +export type WorkspaceEventConnectionStatus = { + workspaceID: string + status: "connected" | "connecting" | "disconnected" | "error" +} + +export type LocationInfo = { + directory: string + workspaceID?: string + project: { + id: string + directory: string + } +} + +export type ProviderRequest = { + headers: { + [key: string]: string + } + body: { + [key: string]: unknown + } +} + +export type AgentColor = string | "primary" | "secondary" | "accent" | "success" | "warning" | "error" | "info" + +export type PermissionV2Effect = "allow" | "deny" | "ask" + +export type PermissionV2Rule = { + action: string + resource: string + effect: PermissionV2Effect +} + +export type PermissionV2Ruleset = Array + +export type AgentV2Info = { + id: string + model?: ModelRef + request: ProviderRequest + system?: string + description?: string + mode: "subagent" | "primary" | "all" + hidden: boolean + color?: AgentColor + steps?: number + permissions: PermissionV2Ruleset +} + +export type SessionV2Info = { + id: string + parentID?: string + projectID: string + agent?: string + model?: ModelRef + cost: number + tokens: { + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } + } + time: { + created: number + updated: number + archived?: number + } + title: string + location: LocationRef + subpath?: string + revert?: RevertState +} + +export type PromptInputFileAttachment = { + uri: string + name?: string + description?: string + source?: PromptSource +} + +export type SessionInputAdmitted = { + admittedSeq: number + id: string + sessionID: string + prompt: Prompt + delivery: "steer" | "queue" + timeCreated: number + promotedSeq?: number +} + +export type SessionMessageAgentSwitched = { + id: string + metadata?: { + [key: string]: unknown + } + time: { + created: number + } + type: "agent-switched" + agent: string +} + +export type SessionMessageModelSwitched = { + id: string + metadata?: { + [key: string]: unknown + } + time: { + created: number + } + type: "model-switched" + model: ModelRef +} + +export type SessionMessageUser = { + id: string + metadata?: { + [key: string]: unknown + } + time: { + created: number + } + text: string + files?: Array + agents?: Array + type: "user" +} + +export type SessionMessageSynthetic = { + id: string + metadata?: { + [key: string]: unknown + } + time: { + created: number + } + sessionID: string + text: string + type: "synthetic" +} + +export type SessionMessageSystem = { + id: string + metadata?: { + [key: string]: unknown + } + time: { + created: number + } + type: "system" + text: string +} + +export type SessionMessageShell = { + id: string + metadata?: { + [key: string]: unknown + } + time: { + created: number + completed?: number + } + type: "shell" + callID: string + command: string + output: string +} + +export type SessionMessageAssistantText = { + type: "text" + id: string + text: string +} + +export type SessionMessageAssistantReasoning = { + type: "reasoning" + id: string + text: string + providerMetadata?: LlmProviderMetadata + time?: { + created: number + completed?: number + } +} + +export type SessionMessageToolStatePending = { + status: "pending" + input: string +} + +export type SessionMessageToolStateRunning = { + status: "running" + input: { + [key: string]: unknown + } + structured: { + [key: string]: unknown + } + content: Array +} + +export type SessionMessageToolStateCompleted = { + status: "completed" + input: { + [key: string]: unknown + } + attachments?: Array + content: Array + outputPaths?: Array + structured: { + [key: string]: unknown + } + result?: unknown +} + +export type SessionMessageToolStateError = { + status: "error" + input: { + [key: string]: unknown + } + content: Array + structured: { + [key: string]: unknown + } + error: SessionErrorUnknown + result?: unknown +} + +export type SessionMessageAssistantTool = { + type: "tool" + id: string + name: string + provider?: { + executed: boolean + metadata?: LlmProviderMetadata + resultMetadata?: LlmProviderMetadata + } + state: + | SessionMessageToolStatePending + | SessionMessageToolStateRunning + | SessionMessageToolStateCompleted + | SessionMessageToolStateError + time: { + created: number + ran?: number + completed?: number + pruned?: number + } +} + +export type SessionMessageAssistant = { + id: string + metadata?: { + [key: string]: unknown + } + time: { + created: number + completed?: number + } + type: "assistant" + agent: string + model: ModelRef + content: Array + snapshot?: { + start?: string + end?: string + files?: Array + } + finish?: string + cost?: number + tokens?: { + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } + } + error?: SessionErrorUnknown +} + +export type SessionMessageCompaction = { + type: "compaction" + reason: "auto" | "manual" + summary: string + recent: string + id: string + metadata?: { + [key: string]: unknown + } + time: { + created: number + } +} + +export type SessionMessage = + | SessionMessageAgentSwitched + | SessionMessageModelSwitched + | SessionMessageUser + | SessionMessageSynthetic + | SessionMessageSystem + | SessionMessageShell + | SessionMessageAssistant + | SessionMessageCompaction + +export type SessionNextAgentSwitched = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.next.agent.switched" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + timestamp: number + sessionID: string + messageID: string + agent: string + } +} + +export type SessionNextModelSwitched = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.next.model.switched" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + timestamp: number + sessionID: string + messageID: string + model: ModelRef + } +} + +export type SessionNextMoved = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.next.moved" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + timestamp: number + sessionID: string + location: LocationRef + subdirectory?: string + } +} + +export type SessionNextPrompted = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.next.prompted" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + timestamp: number + sessionID: string + messageID: string + prompt: Prompt + delivery: "steer" | "queue" + } +} + +export type SessionNextPromptAdmitted = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.next.prompt.admitted" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + timestamp: number + sessionID: string + messageID: string + prompt: Prompt + delivery: "steer" | "queue" + } +} + +export type SessionNextContextUpdated = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.next.context.updated" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + timestamp: number + sessionID: string + messageID: string + text: string + } +} + +export type SessionNextSynthetic = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.next.synthetic" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + timestamp: number + sessionID: string + messageID: string + text: string + } +} + +export type SessionNextShellStarted = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.next.shell.started" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + timestamp: number + sessionID: string + messageID: string + callID: string + command: string + } +} + +export type SessionNextShellEnded = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.next.shell.ended" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + timestamp: number + sessionID: string + callID: string + output: string + } +} + +export type SessionNextStepStarted = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.next.step.started" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + timestamp: number + sessionID: string + assistantMessageID: string + agent: string + model: ModelRef + snapshot?: string + } +} + +export type SessionNextStepEnded = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.next.step.ended" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + timestamp: number + sessionID: string + assistantMessageID: string + finish: string + cost: number + tokens: { + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } + } + snapshot?: string + files?: Array + } +} + +export type SessionNextStepFailed = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.next.step.failed" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + timestamp: number + sessionID: string + assistantMessageID: string + error: SessionErrorUnknown + } +} + +export type SessionNextTextStarted = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.next.text.started" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + timestamp: number + sessionID: string + assistantMessageID: string + textID: string + } +} + +export type SessionNextTextEnded = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.next.text.ended" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + timestamp: number + sessionID: string + assistantMessageID: string + textID: string + text: string + } +} + +export type SessionNextToolInputStarted = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.next.tool.input.started" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + timestamp: number + sessionID: string + assistantMessageID: string + callID: string + name: string + } +} + +export type SessionNextToolInputEnded = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.next.tool.input.ended" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + timestamp: number + sessionID: string + assistantMessageID: string + callID: string + text: string + } +} + +export type SessionNextToolCalled = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.next.tool.called" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + timestamp: number + sessionID: string + assistantMessageID: string + callID: string + tool: string + input: { + [key: string]: unknown + } + provider: { + executed: boolean + metadata?: LlmProviderMetadata + } + } +} + +export type SessionNextToolProgress = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.next.tool.progress" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + timestamp: number + sessionID: string + assistantMessageID: string + callID: string + structured: { + [key: string]: unknown + } + content: Array + } +} + +export type SessionNextToolSuccess = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.next.tool.success" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + timestamp: number + sessionID: string + assistantMessageID: string + callID: string + structured: { + [key: string]: unknown + } + content: Array + outputPaths?: Array + result?: unknown + provider: { + executed: boolean + metadata?: LlmProviderMetadata + } + } +} + +export type SessionNextToolFailed = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.next.tool.failed" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + timestamp: number + sessionID: string + assistantMessageID: string + callID: string + error: SessionErrorUnknown + result?: unknown + provider: { + executed: boolean + metadata?: LlmProviderMetadata + } + } +} + +export type SessionNextReasoningStarted = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.next.reasoning.started" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + timestamp: number + sessionID: string + assistantMessageID: string + reasoningID: string + providerMetadata?: LlmProviderMetadata + } +} + +export type SessionNextReasoningEnded = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.next.reasoning.ended" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + timestamp: number + sessionID: string + assistantMessageID: string + reasoningID: string + text: string + providerMetadata?: LlmProviderMetadata + } +} + +export type SessionNextRetried = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.next.retried" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + timestamp: number + sessionID: string + attempt: number + error: SessionNextRetryError + } +} + +export type SessionNextCompactionStarted = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.next.compaction.started" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + timestamp: number + sessionID: string + messageID: string + reason: "auto" | "manual" + } +} + +export type SessionNextCompactionEnded = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.next.compaction.ended" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + timestamp: number + sessionID: string + messageID: string + reason: "auto" | "manual" + text: string + recent: string + } +} + +export type SessionNextRevertStaged = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.next.revert.staged" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + timestamp: number + sessionID: string + revert: RevertState + } +} + +export type SessionNextRevertCleared = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.next.revert.cleared" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + timestamp: number + sessionID: string + } +} + +export type SessionNextRevertCommitted = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.next.revert.committed" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + timestamp: number + sessionID: string + messageID: string + } +} + +export type ModelApi = + | { + id: string + type: "aisdk" + package: string + url?: string + settings?: { + [key: string]: unknown + } + } + | { + id: string + type: "native" + url?: string + settings: { + [key: string]: unknown + } + } + +export type ModelCapabilities = { + tools: boolean + input: Array + output: Array +} + +export type ModelCost = { + tier?: { + type: "context" + size: number + } + input: number + output: number + cache: { + read: number + write: number + } +} + +export type ModelV2Info = { + id: string + providerID: string + family?: string + name: string + api: ModelApi + capabilities: ModelCapabilities + request: { + headers: { + [key: string]: string + } + body: { + [key: string]: unknown + } + variant?: string + } + variants: Array<{ + id: string + headers: { + [key: string]: string + } + body: { + [key: string]: unknown + } + }> + time: { + released: number + } + cost: Array + status: "alpha" | "beta" | "deprecated" | "active" + enabled: boolean + limit: { + context: number + input?: number + output: number + } +} + +export type ProviderAisdk = { + type: "aisdk" + package: string + url?: string + settings?: { + [key: string]: unknown + } +} + +export type ProviderNative = { + type: "native" + url?: string + settings: { + [key: string]: unknown + } +} + +export type ProviderApi = ProviderAisdk | ProviderNative + +export type ProviderV2Info = { + id: string + integrationID?: string + name: string + disabled?: boolean + api: ProviderApi + request: ProviderRequest +} + +export type IntegrationWhen = { + key: string + op: "eq" | "neq" + value: string +} + +export type IntegrationTextPrompt = { + type: "text" + key: string + message: string + placeholder?: string + when?: IntegrationWhen +} + +export type IntegrationSelectPrompt = { + type: "select" + key: string + message: string + options: Array<{ + label: string + value: string + hint?: string + }> + when?: IntegrationWhen +} + +export type IntegrationOAuthMethod = { + id: string + type: "oauth" + label: string + prompts?: Array +} + +export type IntegrationKeyMethod = { + type: "key" + label?: string +} + +export type IntegrationEnvMethod = { + type: "env" + names: Array +} + +export type ConnectionCredentialInfo = { + type: "credential" + id: string + label: string +} + +export type ConnectionEnvInfo = { + type: "env" + name: string +} + +export type ConnectionInfo = ConnectionCredentialInfo | ConnectionEnvInfo + +export type IntegrationInfo = { + id: string + name: string + methods: Array + connections: Array +} + +export type IntegrationAttempt = { + attemptID: string + url: string + instructions: string + mode: "auto" | "code" + time: { + created: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" + expires: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" + } +} + +export type IntegrationAttemptStatus = + | { + status: "pending" + time: { + created: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" + expires: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" + } + } + | { + status: "complete" + time: { + created: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" + expires: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" + } + } + | { + status: "failed" + message: string + time: { + created: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" + expires: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" + } + } + | { + status: "expired" + time: { + created: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" + expires: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN" + } + } + +export type PermissionV2Request = { + id: string + sessionID: string + action: string + resources: Array + save?: Array + metadata?: { + [key: string]: unknown + } + source?: PermissionV2Source +} + +export type PermissionSavedInfo = { + id: string + projectID: string + action: string + resource: string +} + +export type FileSystemEntry = { + path: string + type: "file" | "directory" +} + +export type CommandV2Info = { + name: string + template: string + description?: string + agent?: string + model?: ModelRef + subtask?: boolean +} + +export type SkillV2Info = { + name: string + description?: string + slash?: boolean + location: string + content: string +} + +export type ModelsDevRefreshed = { + id: string + metadata?: { + [key: string]: unknown + } + type: "models-dev.refreshed" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + [key: string]: unknown + } +} + +export type IntegrationUpdated = { + id: string + metadata?: { + [key: string]: unknown + } + type: "integration.updated" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + [key: string]: unknown + } +} + +export type IntegrationConnectionUpdated = { + id: string + metadata?: { + [key: string]: unknown + } + type: "integration.connection.updated" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + integrationID: string + } +} + +export type CatalogUpdated = { + id: string + metadata?: { + [key: string]: unknown + } + type: "catalog.updated" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + [key: string]: unknown + } +} + +export type SessionCreated = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.created" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + sessionID: string + info: Session + } +} + +export type SessionUpdated = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.updated" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + sessionID: string + info: Session + } +} + +export type SessionDeleted = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.deleted" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + sessionID: string + info: Session + } +} + +export type MessageUpdated = { + id: string + metadata?: { + [key: string]: unknown + } + type: "message.updated" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + sessionID: string + info: Message + } +} + +export type MessageRemoved = { + id: string + metadata?: { + [key: string]: unknown + } + type: "message.removed" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + sessionID: string + messageID: string + } +} + +export type MessagePartUpdated = { + id: string + metadata?: { + [key: string]: unknown + } + type: "message.part.updated" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + sessionID: string + part: Part + time: number + } +} + +export type MessagePartRemoved = { + id: string + metadata?: { + [key: string]: unknown + } + type: "message.part.removed" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + sessionID: string + messageID: string + partID: string + } +} + +export type SessionNextTextDelta = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.next.text.delta" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + timestamp: number + sessionID: string + assistantMessageID: string + textID: string + delta: string + } +} + +export type SessionNextReasoningDelta = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.next.reasoning.delta" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + timestamp: number + sessionID: string + assistantMessageID: string + reasoningID: string + delta: string + } +} + +export type SessionNextToolInputDelta = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.next.tool.input.delta" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + timestamp: number + sessionID: string + assistantMessageID: string + callID: string + delta: string + } +} + +export type SessionNextCompactionDelta = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.next.compaction.delta" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + timestamp: number + sessionID: string + messageID: string + text: string + } +} + +export type MessagePartDelta = { + id: string + metadata?: { + [key: string]: unknown + } + type: "message.part.delta" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + sessionID: string + messageID: string + partID: string + field: string + delta: string + } +} + +export type SessionDiff = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.diff" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + sessionID: string + diff: Array + } +} + +export type SessionError = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.error" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + sessionID?: string + error?: + | ProviderAuthError + | UnknownError + | MessageOutputLengthError + | MessageAbortedError + | StructuredOutputError + | ContextOverflowError + | ContentFilterError + | ApiError + } +} + +export type InstallationUpdated = { + id: string + metadata?: { + [key: string]: unknown + } + type: "installation.updated" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + version: string + } +} + +export type InstallationUpdateAvailable = { + id: string + metadata?: { + [key: string]: unknown + } + type: "installation.update-available" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + version: string + } +} + +export type FileEdited = { + id: string + metadata?: { + [key: string]: unknown + } + type: "file.edited" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + file: string + } +} + +export type ReferenceUpdated = { + id: string + metadata?: { + [key: string]: unknown + } + type: "reference.updated" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + [key: string]: unknown + } +} + +export type PermissionV2Asked = { + id: string + metadata?: { + [key: string]: unknown + } + type: "permission.v2.asked" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + id: string + sessionID: string + action: string + resources: Array + save?: Array + metadata?: { + [key: string]: unknown + } + source?: PermissionV2Source + } +} + +export type PermissionV2Replied = { + id: string + metadata?: { + [key: string]: unknown + } + type: "permission.v2.replied" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + sessionID: string + requestID: string + reply: PermissionV2Reply + } +} + +export type PluginAdded = { + id: string + metadata?: { + [key: string]: unknown + } + type: "plugin.added" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + id: string + } +} + +export type ProjectDirectoriesUpdated = { + id: string + metadata?: { + [key: string]: unknown + } + type: "project.directories.updated" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + projectID: string + } +} + +export type FileWatcherUpdated = { + id: string + metadata?: { + [key: string]: unknown + } + type: "file.watcher.updated" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + file: string + event: "add" | "change" | "unlink" + } +} + +export type PtyCreated = { + id: string + metadata?: { + [key: string]: unknown + } + type: "pty.created" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + info: Pty + } +} + +export type PtyUpdated = { + id: string + metadata?: { + [key: string]: unknown + } + type: "pty.updated" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + info: Pty + } +} + +export type PtyExited = { + id: string + metadata?: { + [key: string]: unknown + } + type: "pty.exited" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + id: string + exitCode: number + } +} + +export type PtyDeleted = { + id: string + metadata?: { + [key: string]: unknown + } + type: "pty.deleted" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + id: string + } +} + +export type QuestionV2Asked = { + id: string + metadata?: { + [key: string]: unknown + } + type: "question.v2.asked" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + id: string + sessionID: string + /** + * Questions to ask + */ + questions: Array + tool?: QuestionV2Tool + } +} + +export type QuestionV2Replied = { + id: string + metadata?: { + [key: string]: unknown + } + type: "question.v2.replied" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + sessionID: string + requestID: string + answers: Array + } +} + +export type QuestionV2Rejected = { + id: string + metadata?: { + [key: string]: unknown + } + type: "question.v2.rejected" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + sessionID: string + requestID: string + } +} + +export type TodoUpdated = { + id: string + metadata?: { + [key: string]: unknown + } + type: "todo.updated" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + sessionID: string + todos: Array + } +} + +export type LspUpdated = { + id: string + metadata?: { + [key: string]: unknown + } + type: "lsp.updated" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + [key: string]: unknown + } +} + +export type PermissionAsked = { + id: string + metadata?: { + [key: string]: unknown + } + type: "permission.asked" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + id: string + sessionID: string + permission: string + patterns: Array + metadata: { + [key: string]: unknown + } + always: Array + tool?: { + messageID: string + callID: string + } + } +} + +export type PermissionReplied = { + id: string + metadata?: { + [key: string]: unknown + } + type: "permission.replied" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + sessionID: string + requestID: string + reply: "once" | "always" | "reject" + } +} + +export type TuiPromptAppend = { + id: string + metadata?: { + [key: string]: unknown + } + type: "tui.prompt.append" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + text: string + } +} + +export type TuiCommandExecute = { + id: string + metadata?: { + [key: string]: unknown + } + type: "tui.command.execute" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + command: + | "session.list" + | "session.new" + | "session.share" + | "session.interrupt" + | "session.compact" + | "session.page.up" + | "session.page.down" + | "session.line.up" + | "session.line.down" + | "session.half.page.up" + | "session.half.page.down" + | "session.first" + | "session.last" + | "prompt.clear" + | "prompt.submit" + | "agent.cycle" + | string + } +} + +export type TuiToastShow = { + id: string + metadata?: { + [key: string]: unknown + } + type: "tui.toast.show" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + title?: string + message: string + variant: "info" | "success" | "warning" | "error" + duration?: number + } +} + +export type TuiSessionSelect = { + id: string + metadata?: { + [key: string]: unknown + } + type: "tui.session.select" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + /** + * Session ID to navigate to + */ + sessionID: string + } +} + +export type McpToolsChanged = { + id: string + metadata?: { + [key: string]: unknown + } + type: "mcp.tools.changed" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + server: string + } +} + +export type McpBrowserOpenFailed = { + id: string + metadata?: { + [key: string]: unknown + } + type: "mcp.browser.open.failed" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + mcpName: string + url: string + } +} + +export type CommandExecuted = { + id: string + metadata?: { + [key: string]: unknown + } + type: "command.executed" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + name: string + sessionID: string + arguments: string + messageID: string + } +} + +export type ProjectUpdated = { + id: string + metadata?: { + [key: string]: unknown + } + type: "project.updated" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + id: string + worktree: string + vcs?: ProjectVcs + name?: string + icon?: ProjectIcon + commands?: ProjectCommands + time: ProjectTime + sandboxes: Array + } +} + +export type SessionIdle = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.idle" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + sessionID: string + } +} + +export type QuestionAsked = { + id: string + metadata?: { + [key: string]: unknown + } + type: "question.asked" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + id: string + sessionID: string + /** + * Questions to ask + */ + questions: Array + tool?: QuestionTool + } +} + +export type SessionCompacted = { + id: string + metadata?: { + [key: string]: unknown + } + type: "session.compacted" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + sessionID: string + } +} + +export type VcsBranchUpdated = { + id: string + metadata?: { + [key: string]: unknown + } + type: "vcs.branch.updated" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + branch?: string + } +} + +export type WorkspaceReady = { + id: string + metadata?: { + [key: string]: unknown + } + type: "workspace.ready" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + name: string + } +} + +export type WorkspaceFailed = { + id: string + metadata?: { + [key: string]: unknown + } + type: "workspace.failed" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + message: string + } +} + +export type WorkspaceStatus = { + id: string + metadata?: { + [key: string]: unknown + } + type: "workspace.status" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + workspaceID: string + status: "connected" | "connecting" | "disconnected" | "error" + } +} + +export type WorktreeReady = { + id: string + metadata?: { + [key: string]: unknown + } + type: "worktree.ready" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + name: string + branch?: string + } +} + +export type WorktreeFailed = { + id: string + metadata?: { + [key: string]: unknown + } + type: "worktree.failed" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + message: string + } +} + +export type ServerConnected = { + id: string + metadata?: { + [key: string]: unknown + } + type: "server.connected" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + [key: string]: unknown + } +} + +export type GlobalDisposed = { + id: string + metadata?: { + [key: string]: unknown + } + type: "global.disposed" + durable?: { + aggregateID: string + seq: number + version: number + } + location?: LocationRef + data: { + [key: string]: unknown + } +} + +export type QuestionV2Request = { + id: string + sessionID: string + /** + * Questions to ask + */ + questions: Array + tool?: QuestionV2Tool +} + +export type QuestionV2Reply = { + /** + * User answers in order of questions (each answer is an array of selected labels) + */ + answers: Array +} + +export type ReferenceLocalSource = { + type: "local" + path: string + description?: string + hidden?: boolean +} + +export type ReferenceGitSource = { + type: "git" + repository: string + branch?: string + description?: string + hidden?: boolean +} + +export type ReferenceSource = ReferenceLocalSource | ReferenceGitSource + +export type ReferenceInfo = { + name: string + path: string + description?: string + hidden?: boolean + source: ReferenceSource +} + +export type ProjectCopyCopy = { + directory: string +} + +export type EventModelsDevRefreshed = { + id: string + type: "models-dev.refreshed" + properties: { + [key: string]: unknown + } +} + +export type EventIntegrationUpdated = { + id: string + type: "integration.updated" + properties: { + [key: string]: unknown + } +} + +export type EventIntegrationConnectionUpdated = { + id: string + type: "integration.connection.updated" + properties: { + integrationID: string + } +} + +export type EventCatalogUpdated = { + id: string + type: "catalog.updated" + properties: { + [key: string]: unknown + } +} + +export type EventSessionCreated = { + id: string + type: "session.created" + properties: { + sessionID: string + info: Session + } +} + +export type EventSessionUpdated = { + id: string + type: "session.updated" + properties: { + sessionID: string + info: Session + } +} + +export type EventSessionDeleted = { + id: string + type: "session.deleted" + properties: { + sessionID: string + info: Session + } +} + +export type EventMessageUpdated = { + id: string + type: "message.updated" + properties: { + sessionID: string + info: Message + } +} + +export type EventMessageRemoved = { + id: string + type: "message.removed" + properties: { + sessionID: string + messageID: string + } +} + +export type EventMessagePartUpdated = { + id: string + type: "message.part.updated" + properties: { + sessionID: string + part: Part + time: number + } +} + +export type EventMessagePartRemoved = { + id: string + type: "message.part.removed" + properties: { + sessionID: string + messageID: string + partID: string + } +} + +export type EventSessionNextAgentSwitched = { + id: string + type: "session.next.agent.switched" + properties: { + timestamp: number + sessionID: string + messageID: string + agent: string + } +} + +export type EventSessionNextModelSwitched = { + id: string + type: "session.next.model.switched" + properties: { + timestamp: number + sessionID: string + messageID: string + model: ModelRef + } +} + +export type EventSessionNextMoved = { + id: string + type: "session.next.moved" + properties: { + timestamp: number + sessionID: string + location: LocationRef + subdirectory?: string + } +} + +export type EventSessionNextPrompted = { + id: string + type: "session.next.prompted" + properties: { + timestamp: number + sessionID: string + messageID: string + prompt: Prompt + delivery: "steer" | "queue" + } +} + +export type EventSessionNextPromptAdmitted = { + id: string + type: "session.next.prompt.admitted" + properties: { + timestamp: number + sessionID: string + messageID: string + prompt: Prompt + delivery: "steer" | "queue" + } +} + +export type EventSessionNextContextUpdated = { + id: string + type: "session.next.context.updated" + properties: { + timestamp: number + sessionID: string + messageID: string + text: string + } +} + +export type EventSessionNextSynthetic = { + id: string + type: "session.next.synthetic" + properties: { + timestamp: number + sessionID: string + messageID: string + text: string + } +} + +export type EventSessionNextShellStarted = { + id: string + type: "session.next.shell.started" + properties: { + timestamp: number + sessionID: string + messageID: string + callID: string + command: string + } +} + +export type EventSessionNextShellEnded = { + id: string + type: "session.next.shell.ended" + properties: { + timestamp: number + sessionID: string + callID: string + output: string + } +} + +export type EventSessionNextStepStarted = { + id: string + type: "session.next.step.started" + properties: { + timestamp: number + sessionID: string + assistantMessageID: string + agent: string + model: ModelRef + snapshot?: string + } +} + +export type EventSessionNextStepEnded = { + id: string + type: "session.next.step.ended" + properties: { + timestamp: number + sessionID: string + assistantMessageID: string + finish: string + cost: number + tokens: { + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } + } + snapshot?: string + files?: Array + } +} + +export type EventSessionNextStepFailed = { + id: string + type: "session.next.step.failed" + properties: { + timestamp: number + sessionID: string + assistantMessageID: string + error: SessionErrorUnknown + } +} + +export type EventSessionNextTextStarted = { + id: string + type: "session.next.text.started" + properties: { + timestamp: number + sessionID: string + assistantMessageID: string + textID: string + } +} + +export type EventSessionNextTextDelta = { + id: string + type: "session.next.text.delta" + properties: { + timestamp: number + sessionID: string + assistantMessageID: string + textID: string + delta: string + } +} + +export type EventSessionNextTextEnded = { + id: string + type: "session.next.text.ended" + properties: { + timestamp: number + sessionID: string + assistantMessageID: string + textID: string + text: string + } +} + +export type EventSessionNextReasoningStarted = { + id: string + type: "session.next.reasoning.started" + properties: { + timestamp: number + sessionID: string + assistantMessageID: string + reasoningID: string + providerMetadata?: LlmProviderMetadata + } +} + +export type EventSessionNextReasoningDelta = { + id: string + type: "session.next.reasoning.delta" + properties: { + timestamp: number + sessionID: string + assistantMessageID: string + reasoningID: string + delta: string + } +} + +export type EventSessionNextReasoningEnded = { + id: string + type: "session.next.reasoning.ended" + properties: { + timestamp: number + sessionID: string + assistantMessageID: string + reasoningID: string + text: string + providerMetadata?: LlmProviderMetadata + } +} + +export type EventSessionNextToolInputStarted = { + id: string + type: "session.next.tool.input.started" + properties: { + timestamp: number + sessionID: string + assistantMessageID: string + callID: string + name: string + } +} + +export type EventSessionNextToolInputDelta = { + id: string + type: "session.next.tool.input.delta" + properties: { + timestamp: number + sessionID: string + assistantMessageID: string + callID: string + delta: string + } +} + +export type EventSessionNextToolInputEnded = { + id: string + type: "session.next.tool.input.ended" + properties: { + timestamp: number + sessionID: string + assistantMessageID: string + callID: string + text: string + } +} + +export type EventSessionNextToolCalled = { + id: string + type: "session.next.tool.called" + properties: { + timestamp: number + sessionID: string + assistantMessageID: string + callID: string + tool: string + input: { + [key: string]: unknown + } + provider: { + executed: boolean + metadata?: LlmProviderMetadata + } + } +} + +export type EventSessionNextToolProgress = { + id: string + type: "session.next.tool.progress" + properties: { + timestamp: number + sessionID: string + assistantMessageID: string + callID: string + structured: { + [key: string]: unknown + } + content: Array + } +} + +export type EventSessionNextToolSuccess = { + id: string + type: "session.next.tool.success" + properties: { + timestamp: number + sessionID: string + assistantMessageID: string + callID: string + structured: { + [key: string]: unknown + } + content: Array + outputPaths?: Array + result?: unknown + provider: { + executed: boolean + metadata?: LlmProviderMetadata + } + } +} + +export type EventSessionNextToolFailed = { + id: string + type: "session.next.tool.failed" + properties: { + timestamp: number + sessionID: string + assistantMessageID: string + callID: string + error: SessionErrorUnknown + result?: unknown + provider: { + executed: boolean + metadata?: LlmProviderMetadata + } + } +} + +export type EventSessionNextRetried = { + id: string + type: "session.next.retried" + properties: { + timestamp: number + sessionID: string + attempt: number + error: SessionNextRetryError + } +} + +export type EventSessionNextCompactionStarted = { + id: string + type: "session.next.compaction.started" + properties: { + timestamp: number + sessionID: string + messageID: string + reason: "auto" | "manual" + } +} + +export type EventSessionNextCompactionDelta = { + id: string + type: "session.next.compaction.delta" + properties: { + timestamp: number + sessionID: string + messageID: string + text: string + } +} + +export type EventSessionNextCompactionEnded = { + id: string + type: "session.next.compaction.ended" + properties: { + timestamp: number + sessionID: string + messageID: string + reason: "auto" | "manual" + text: string + recent: string + } +} + +export type EventSessionNextRevertStaged = { + id: string + type: "session.next.revert.staged" + properties: { + timestamp: number + sessionID: string + revert: RevertState + } +} + +export type EventSessionNextRevertCleared = { + id: string + type: "session.next.revert.cleared" + properties: { + timestamp: number + sessionID: string + } +} + +export type EventSessionNextRevertCommitted = { + id: string + type: "session.next.revert.committed" + properties: { + timestamp: number + sessionID: string + messageID: string + } +} + +export type EventMessagePartDelta = { + id: string + type: "message.part.delta" + properties: { + sessionID: string + messageID: string + partID: string + field: string + delta: string + } +} + +export type EventSessionDiff = { + id: string + type: "session.diff" + properties: { + sessionID: string + diff: Array + } +} + +export type EventSessionError = { + id: string + type: "session.error" + properties: { + sessionID?: string + error?: + | ProviderAuthError + | UnknownError + | MessageOutputLengthError + | MessageAbortedError + | StructuredOutputError + | ContextOverflowError + | ContentFilterError + | ApiError + } +} + +export type EventInstallationUpdated = { + id: string + type: "installation.updated" + properties: { + version: string + } +} + +export type EventInstallationUpdateAvailable = { + id: string + type: "installation.update-available" + properties: { + version: string + } +} + +export type EventFileEdited = { + id: string + type: "file.edited" + properties: { + file: string + } +} + +export type EventReferenceUpdated = { + id: string + type: "reference.updated" + properties: { + [key: string]: unknown + } +} + +export type EventPermissionV2Asked = { + id: string + type: "permission.v2.asked" + properties: { + id: string + sessionID: string + action: string + resources: Array + save?: Array + metadata?: { + [key: string]: unknown + } + source?: PermissionV2Source + } +} + +export type EventPermissionV2Replied = { + id: string + type: "permission.v2.replied" + properties: { + sessionID: string + requestID: string + reply: PermissionV2Reply + } +} + +export type EventPluginAdded = { + id: string + type: "plugin.added" + properties: { + id: string + } +} + +export type EventProjectDirectoriesUpdated = { + id: string + type: "project.directories.updated" + properties: { + projectID: string + } +} + +export type EventFileWatcherUpdated = { + id: string + type: "file.watcher.updated" + properties: { + file: string + event: "add" | "change" | "unlink" + } +} + +export type EventPtyCreated = { + id: string + type: "pty.created" + properties: { + info: Pty + } +} + +export type EventPtyUpdated = { + id: string + type: "pty.updated" + properties: { + info: Pty + } +} + +export type EventPtyExited = { + id: string + type: "pty.exited" + properties: { + id: string + exitCode: number + } +} + +export type EventPtyDeleted = { + id: string + type: "pty.deleted" + properties: { + id: string + } +} + +export type EventQuestionV2Asked = { + id: string + type: "question.v2.asked" + properties: { + id: string + sessionID: string + /** + * Questions to ask + */ + questions: Array + tool?: QuestionV2Tool + } +} + +export type EventQuestionV2Replied = { + id: string + type: "question.v2.replied" + properties: { + sessionID: string + requestID: string + answers: Array + } +} + +export type EventQuestionV2Rejected = { + id: string + type: "question.v2.rejected" + properties: { + sessionID: string + requestID: string + } +} + +export type EventTodoUpdated = { + id: string + type: "todo.updated" + properties: { + sessionID: string + todos: Array + } +} + +export type EventLspUpdated = { + id: string + type: "lsp.updated" + properties: { + [key: string]: unknown + } +} + +export type EventPermissionAsked = { + id: string + type: "permission.asked" + properties: { + id: string + sessionID: string + permission: string + patterns: Array + metadata: { + [key: string]: unknown + } + always: Array + tool?: { + messageID: string + callID: string + } + } +} + +export type EventPermissionReplied = { + id: string + type: "permission.replied" + properties: { + sessionID: string + requestID: string + reply: "once" | "always" | "reject" + } +} + +export type EventMcpToolsChanged = { + id: string + type: "mcp.tools.changed" + properties: { + server: string + } +} + +export type EventMcpBrowserOpenFailed = { + id: string + type: "mcp.browser.open.failed" + properties: { + mcpName: string + url: string + } +} + +export type EventCommandExecuted = { + id: string + type: "command.executed" + properties: { + name: string + sessionID: string + arguments: string + messageID: string + } +} + +export type EventProjectUpdated = { + id: string + type: "project.updated" + properties: { + id: string + worktree: string + vcs?: ProjectVcs + name?: string + icon?: ProjectIcon + commands?: ProjectCommands + time: ProjectTime + sandboxes: Array + } +} + +export type EventSessionStatus = { + id: string + type: "session.status" + properties: { + sessionID: string + status: SessionStatus + } +} + +export type EventSessionIdle = { + id: string + type: "session.idle" + properties: { + sessionID: string + } +} + +export type EventQuestionAsked = { + id: string + type: "question.asked" + properties: { + id: string + sessionID: string + /** + * Questions to ask + */ + questions: Array + tool?: QuestionTool + } +} + +export type EventQuestionReplied = { + id: string + type: "question.replied" + properties: { + sessionID: string + requestID: string + answers: Array + } +} + +export type EventQuestionRejected = { + id: string + type: "question.rejected" + properties: { + sessionID: string + requestID: string + } +} + +export type EventSessionCompacted = { + id: string + type: "session.compacted" + properties: { + sessionID: string + } +} + +export type EventVcsBranchUpdated = { + id: string + type: "vcs.branch.updated" + properties: { + branch?: string + } +} + +export type EventWorkspaceReady = { + id: string + type: "workspace.ready" + properties: { + name: string + } +} + +export type EventWorkspaceFailed = { + id: string + type: "workspace.failed" + properties: { + message: string + } +} + +export type EventWorkspaceStatus = { + id: string + type: "workspace.status" + properties: { + workspaceID: string + status: "connected" | "connecting" | "disconnected" | "error" + } +} + +export type EventWorktreeReady = { + id: string + type: "worktree.ready" + properties: { + name: string + branch?: string + } +} + +export type EventWorktreeFailed = { + id: string + type: "worktree.failed" + properties: { + message: string + } +} + +export type EventServerConnected = { + id: string + type: "server.connected" + properties: { + [key: string]: unknown + } +} + +export type EventGlobalDisposed = { + id: string + type: "global.disposed" + properties: { + [key: string]: unknown + } +} + +export type CredentialOAuth = { + type: "oauth" + methodID: string + refresh: string + access: string + expires: number + metadata?: { + [key: string]: unknown + } +} + +export type CredentialKey = { + type: "key" + key: string + metadata?: { + [key: string]: unknown + } +} + +export type SkillV2DirectorySource = { + type: "directory" + path: string +} + +export type SkillV2UrlSource = { + type: "url" + url: string +} + +export type SkillV2EmbeddedSource = { + type: "embedded" + skill: SkillV2Info +} + +export type BadRequestError = { + name: "BadRequest" + data: { + message: string + kind?: "Params" | "Headers" | "Query" | "Body" | "Payload" + } +} + +export type AuthRemoveData = { + body?: never + path: { + providerID: string + } + query?: never + url: "/auth/{providerID}" +} + +export type AuthRemoveErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError +} + +export type AuthRemoveError = AuthRemoveErrors[keyof AuthRemoveErrors] + +export type AuthRemoveResponses = { + /** + * Successfully removed authentication credentials + */ + 200: boolean +} + +export type AuthRemoveResponse = AuthRemoveResponses[keyof AuthRemoveResponses] + +export type AuthSetData = { + body?: Auth + path: { + providerID: string + } + query?: never + url: "/auth/{providerID}" +} + +export type AuthSetErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError +} + +export type AuthSetError = AuthSetErrors[keyof AuthSetErrors] + +export type AuthSetResponses = { + /** + * Successfully set authentication credentials + */ + 200: boolean +} + +export type AuthSetResponse = AuthSetResponses[keyof AuthSetResponses] + +export type AppLogData = { + body?: { + /** + * Service name for the log entry + */ + service: string + /** + * Log level + */ + level: "debug" | "info" | "error" | "warn" + /** + * Log message + */ + message: string + extra?: { + [key: string]: unknown + } + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/log" +} + +export type AppLogErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError +} + +export type AppLogError = AppLogErrors[keyof AppLogErrors] + +export type AppLogResponses = { + /** + * Log entry written successfully + */ + 200: boolean +} + +export type AppLogResponse = AppLogResponses[keyof AppLogResponses] + +export type ExperimentalControlPlaneMoveSessionData = { + body?: { + sessionID: string + destination: MoveSessionDestination + moveChanges?: boolean + } + path?: never + query?: never + url: "/experimental/control-plane/move-session" +} + +export type ExperimentalControlPlaneMoveSessionErrors = { + /** + * MoveSessionError | InvalidRequestError + */ + 400: MoveSessionError | InvalidRequestError +} + +export type ExperimentalControlPlaneMoveSessionError = + ExperimentalControlPlaneMoveSessionErrors[keyof ExperimentalControlPlaneMoveSessionErrors] + +export type ExperimentalControlPlaneMoveSessionResponses = { + /** + * Session moved + */ + 204: void +} + +export type ExperimentalControlPlaneMoveSessionResponse = + ExperimentalControlPlaneMoveSessionResponses[keyof ExperimentalControlPlaneMoveSessionResponses] + +export type GlobalHealthData = { + body?: never + path?: never + query?: never + url: "/global/health" +} + +export type GlobalHealthErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type GlobalHealthError = GlobalHealthErrors[keyof GlobalHealthErrors] + +export type GlobalHealthResponses = { + /** + * Health information + */ + 200: { + healthy: true + version: string + } +} + +export type GlobalHealthResponse = GlobalHealthResponses[keyof GlobalHealthResponses] + +export type GlobalEventData = { + body?: never + path?: never + query?: never + url: "/global/event" +} + +export type GlobalEventErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type GlobalEventError = GlobalEventErrors[keyof GlobalEventErrors] + +export type GlobalEventResponses = { + /** + * Event stream + */ + 200: GlobalEvent +} + +export type GlobalEventResponse = GlobalEventResponses[keyof GlobalEventResponses] + +export type GlobalConfigGetData = { + body?: never + path?: never + query?: never + url: "/global/config" +} + +export type GlobalConfigGetErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type GlobalConfigGetError = GlobalConfigGetErrors[keyof GlobalConfigGetErrors] + +export type GlobalConfigGetResponses = { + /** + * Get global config info + */ + 200: Config +} + +export type GlobalConfigGetResponse = GlobalConfigGetResponses[keyof GlobalConfigGetResponses] + +export type GlobalConfigUpdateData = { + body?: Config + path?: never + query?: never + url: "/global/config" +} + +export type GlobalConfigUpdateErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError +} + +export type GlobalConfigUpdateError = GlobalConfigUpdateErrors[keyof GlobalConfigUpdateErrors] + +export type GlobalConfigUpdateResponses = { + /** + * Successfully updated global config + */ + 200: Config +} + +export type GlobalConfigUpdateResponse = GlobalConfigUpdateResponses[keyof GlobalConfigUpdateResponses] + +export type GlobalDisposeData = { + body?: never + path?: never + query?: never + url: "/global/dispose" +} + +export type GlobalDisposeErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type GlobalDisposeError = GlobalDisposeErrors[keyof GlobalDisposeErrors] + +export type GlobalDisposeResponses = { + /** + * Global disposed + */ + 200: boolean +} + +export type GlobalDisposeResponse = GlobalDisposeResponses[keyof GlobalDisposeResponses] + +export type GlobalUpgradeData = { + body?: { + target?: string + } + path?: never + query?: never + url: "/global/upgrade" +} + +export type GlobalUpgradeErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError +} + +export type GlobalUpgradeError = GlobalUpgradeErrors[keyof GlobalUpgradeErrors] + +export type GlobalUpgradeResponses = { + /** + * Upgrade result + */ + 200: + | { + success: true + version: string + } + | { + success: false + error: string + } +} + +export type GlobalUpgradeResponse = GlobalUpgradeResponses[keyof GlobalUpgradeResponses] + +export type EventSubscribeData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/event" +} + +export type EventSubscribeResponses = { + /** + * Event stream + */ + 200: Event +} + +export type EventSubscribeResponse = EventSubscribeResponses[keyof EventSubscribeResponses] + +export type ConfigGetData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/config" +} + +export type ConfigGetErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ConfigGetError = ConfigGetErrors[keyof ConfigGetErrors] + +export type ConfigGetResponses = { + /** + * Get config info + */ + 200: Config +} + +export type ConfigGetResponse = ConfigGetResponses[keyof ConfigGetResponses] + +export type ConfigUpdateData = { + body?: Config + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/config" +} + +export type ConfigUpdateErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError +} + +export type ConfigUpdateError = ConfigUpdateErrors[keyof ConfigUpdateErrors] + +export type ConfigUpdateResponses = { + /** + * Successfully updated config + */ + 200: Config +} + +export type ConfigUpdateResponse = ConfigUpdateResponses[keyof ConfigUpdateResponses] + +export type ConfigProvidersData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/config/providers" +} + +export type ConfigProvidersErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ConfigProvidersError = ConfigProvidersErrors[keyof ConfigProvidersErrors] + +export type ConfigProvidersResponses = { + /** + * List of providers + */ + 200: { + providers: Array + default: { + [key: string]: string + } + } +} + +export type ConfigProvidersResponse = ConfigProvidersResponses[keyof ConfigProvidersResponses] + +export type ExperimentalCapabilitiesGetData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/capabilities" +} + +export type ExperimentalCapabilitiesGetErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ExperimentalCapabilitiesGetError = + ExperimentalCapabilitiesGetErrors[keyof ExperimentalCapabilitiesGetErrors] + +export type ExperimentalCapabilitiesGetResponses = { + /** + * Experimental capabilities + */ + 200: ExperimentalCapabilities +} + +export type ExperimentalCapabilitiesGetResponse = + ExperimentalCapabilitiesGetResponses[keyof ExperimentalCapabilitiesGetResponses] + +export type ExperimentalConsoleGetData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/console" +} + +export type ExperimentalConsoleGetErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * InternalServerError + */ + 500: EffectHttpApiErrorInternalServerError +} + +export type ExperimentalConsoleGetError = ExperimentalConsoleGetErrors[keyof ExperimentalConsoleGetErrors] + +export type ExperimentalConsoleGetResponses = { + /** + * Active Console provider metadata + */ + 200: ConsoleState +} + +export type ExperimentalConsoleGetResponse = ExperimentalConsoleGetResponses[keyof ExperimentalConsoleGetResponses] + +export type ExperimentalConsoleListOrgsData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/console/orgs" +} + +export type ExperimentalConsoleListOrgsErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * InternalServerError + */ + 500: EffectHttpApiErrorInternalServerError +} + +export type ExperimentalConsoleListOrgsError = + ExperimentalConsoleListOrgsErrors[keyof ExperimentalConsoleListOrgsErrors] + +export type ExperimentalConsoleListOrgsResponses = { + /** + * Switchable Console orgs + */ + 200: { + orgs: Array<{ + accountID: string + accountEmail: string + accountUrl: string + orgID: string + orgName: string + active: boolean + }> + } +} + +export type ExperimentalConsoleListOrgsResponse = + ExperimentalConsoleListOrgsResponses[keyof ExperimentalConsoleListOrgsResponses] + +export type ExperimentalConsoleSwitchOrgData = { + body?: { + accountID: string + orgID: string + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/console/switch" +} + +export type ExperimentalConsoleSwitchOrgResponses = { + /** + * Switch success + */ + 200: boolean +} + +export type ExperimentalConsoleSwitchOrgResponse = + ExperimentalConsoleSwitchOrgResponses[keyof ExperimentalConsoleSwitchOrgResponses] + +export type ToolListData = { + body?: never + path?: never + query: { + directory?: string + workspace?: string + provider: string + model: string + } + url: "/experimental/tool" +} + +export type ToolListErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError +} + +export type ToolListError = ToolListErrors[keyof ToolListErrors] + +export type ToolListResponses = { + /** + * Tools + */ + 200: ToolList +} + +export type ToolListResponse = ToolListResponses[keyof ToolListResponses] + +export type ToolIdsData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/tool/ids" +} + +export type ToolIdsErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError +} + +export type ToolIdsError = ToolIdsErrors[keyof ToolIdsErrors] + +export type ToolIdsResponses = { + /** + * Tool IDs + */ + 200: ToolIds +} + +export type ToolIdsResponse = ToolIdsResponses[keyof ToolIdsResponses] + +export type WorktreeRemoveData = { + body?: WorktreeRemoveInput + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/worktree" +} + +export type WorktreeRemoveErrors = { + /** + * WorktreeError | InvalidRequestError + */ + 400: WorktreeError | InvalidRequestError +} + +export type WorktreeRemoveError = WorktreeRemoveErrors[keyof WorktreeRemoveErrors] + +export type WorktreeRemoveResponses = { + /** + * Worktree removed + */ + 200: boolean +} + +export type WorktreeRemoveResponse = WorktreeRemoveResponses[keyof WorktreeRemoveResponses] + +export type WorktreeListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/worktree" +} + +export type WorktreeListErrors = { + /** + * WorktreeError | InvalidRequestError + */ + 400: WorktreeError | InvalidRequestError +} + +export type WorktreeListError = WorktreeListErrors[keyof WorktreeListErrors] + +export type WorktreeListResponses = { + /** + * List of worktree directories + */ + 200: Array +} + +export type WorktreeListResponse = WorktreeListResponses[keyof WorktreeListResponses] + +export type WorktreeCreateData = { + body?: WorktreeCreateInput + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/worktree" +} + +export type WorktreeCreateErrors = { + /** + * WorktreeError | InvalidRequestError + */ + 400: WorktreeError | InvalidRequestError +} + +export type WorktreeCreateError = WorktreeCreateErrors[keyof WorktreeCreateErrors] + +export type WorktreeCreateResponses = { + /** + * Worktree created + */ + 200: Worktree +} + +export type WorktreeCreateResponse = WorktreeCreateResponses[keyof WorktreeCreateResponses] + +export type WorktreeResetData = { + body?: WorktreeResetInput + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/worktree/reset" +} + +export type WorktreeResetErrors = { + /** + * WorktreeError | InvalidRequestError + */ + 400: WorktreeError | InvalidRequestError +} + +export type WorktreeResetError = WorktreeResetErrors[keyof WorktreeResetErrors] + +export type WorktreeResetResponses = { + /** + * Worktree reset + */ + 200: boolean +} + +export type WorktreeResetResponse = WorktreeResetResponses[keyof WorktreeResetResponses] + +export type ExperimentalSessionListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + roots?: boolean | "true" | "false" + start?: number + cursor?: number + search?: string + limit?: number + archived?: boolean | "true" | "false" + } + url: "/experimental/session" +} + +export type ExperimentalSessionListErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ExperimentalSessionListError = ExperimentalSessionListErrors[keyof ExperimentalSessionListErrors] + +export type ExperimentalSessionListResponses = { + /** + * List of sessions + */ + 200: Array +} + +export type ExperimentalSessionListResponse = ExperimentalSessionListResponses[keyof ExperimentalSessionListResponses] + +export type ExperimentalSessionBackgroundData = { + body?: never + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/experimental/session/{sessionID}/background" +} + +export type ExperimentalSessionBackgroundErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError +} + +export type ExperimentalSessionBackgroundError = + ExperimentalSessionBackgroundErrors[keyof ExperimentalSessionBackgroundErrors] + +export type ExperimentalSessionBackgroundResponses = { + /** + * Backgrounded subagents + */ + 200: boolean +} + +export type ExperimentalSessionBackgroundResponse = + ExperimentalSessionBackgroundResponses[keyof ExperimentalSessionBackgroundResponses] + +export type ExperimentalResourceListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/resource" +} + +export type ExperimentalResourceListErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ExperimentalResourceListError = ExperimentalResourceListErrors[keyof ExperimentalResourceListErrors] + +export type ExperimentalResourceListResponses = { + /** + * MCP resources + */ + 200: { + [key: string]: McpResource + } +} + +export type ExperimentalResourceListResponse = + ExperimentalResourceListResponses[keyof ExperimentalResourceListResponses] + +export type FindTextData = { + body?: never + path?: never + query: { + directory?: string + workspace?: string + pattern: string + } + url: "/find" +} + +export type FindTextErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type FindTextError = FindTextErrors[keyof FindTextErrors] + +export type FindTextResponses = { + /** + * Matches + */ + 200: Array<{ + path: { + text: string + } + lines: { + text: string + } + line_number: number + absolute_offset: number + submatches: Array<{ + match: { + text: string + } + start: number + end: number + }> + }> +} + +export type FindTextResponse = FindTextResponses[keyof FindTextResponses] + +export type FindFilesData = { + body?: never + path?: never + query: { + directory?: string + workspace?: string + query: string + dirs?: "true" | "false" + type?: "file" | "directory" + limit?: number + } + url: "/find/file" +} + +export type FindFilesErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type FindFilesError = FindFilesErrors[keyof FindFilesErrors] + +export type FindFilesResponses = { + /** + * File paths + */ + 200: Array +} + +export type FindFilesResponse = FindFilesResponses[keyof FindFilesResponses] + +export type FindSymbolsData = { + body?: never + path?: never + query: { + directory?: string + workspace?: string + query: string + } + url: "/find/symbol" +} + +export type FindSymbolsErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type FindSymbolsError = FindSymbolsErrors[keyof FindSymbolsErrors] + +export type FindSymbolsResponses = { + /** + * Symbols + */ + 200: Array +} + +export type FindSymbolsResponse = FindSymbolsResponses[keyof FindSymbolsResponses] + +export type FileListData = { + body?: never + path?: never + query: { + directory?: string + workspace?: string + path: string + } + url: "/file" +} + +export type FileListErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type FileListError = FileListErrors[keyof FileListErrors] + +export type FileListResponses = { + /** + * Files and directories + */ + 200: Array +} + +export type FileListResponse = FileListResponses[keyof FileListResponses] + +export type FileReadData = { + body?: never + path?: never + query: { + directory?: string + workspace?: string + path: string + } + url: "/file/content" +} + +export type FileReadErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type FileReadError = FileReadErrors[keyof FileReadErrors] + +export type FileReadResponses = { + /** + * File content + */ + 200: FileContent +} + +export type FileReadResponse = FileReadResponses[keyof FileReadResponses] + +export type FileStatusData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/file/status" +} + +export type FileStatusErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type FileStatusError = FileStatusErrors[keyof FileStatusErrors] + +export type FileStatusResponses = { + /** + * File status + */ + 200: Array +} + +export type FileStatusResponse = FileStatusResponses[keyof FileStatusResponses] + +export type FileWriteData = { + body?: { + path: string + content: string + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/file/write" +} + +export type FileWriteErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type FileWriteError = FileWriteErrors[keyof FileWriteErrors] + +export type FileWriteResponses = { + /** + * Write result + */ + 200: { + ok: boolean + } +} + +export type FileWriteResponse = FileWriteResponses[keyof FileWriteResponses] + +export type InstanceDisposeData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/instance/dispose" +} + +export type InstanceDisposeErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type InstanceDisposeError = InstanceDisposeErrors[keyof InstanceDisposeErrors] + +export type InstanceDisposeResponses = { + /** + * Instance disposed + */ + 200: boolean +} + +export type InstanceDisposeResponse = InstanceDisposeResponses[keyof InstanceDisposeResponses] + +export type PathGetData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/path" +} + +export type PathGetErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type PathGetError = PathGetErrors[keyof PathGetErrors] + +export type PathGetResponses = { + /** + * Path + */ + 200: Path +} + +export type PathGetResponse = PathGetResponses[keyof PathGetResponses] + +export type VcsGetData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/vcs" +} + +export type VcsGetErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type VcsGetError = VcsGetErrors[keyof VcsGetErrors] + +export type VcsGetResponses = { + /** + * VCS info + */ + 200: VcsInfo +} + +export type VcsGetResponse = VcsGetResponses[keyof VcsGetResponses] + +export type VcsStatusData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/vcs/status" +} + +export type VcsStatusErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type VcsStatusError = VcsStatusErrors[keyof VcsStatusErrors] + +export type VcsStatusResponses = { + /** + * VCS status + */ + 200: Array +} + +export type VcsStatusResponse = VcsStatusResponses[keyof VcsStatusResponses] + +export type VcsDiffData = { + body?: never + path?: never + query: { + directory?: string + workspace?: string + mode: "git" | "branch" + context?: number + } + url: "/vcs/diff" +} + +export type VcsDiffErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type VcsDiffError = VcsDiffErrors[keyof VcsDiffErrors] + +export type VcsDiffResponses = { + /** + * VCS diff + */ + 200: Array +} + +export type VcsDiffResponse = VcsDiffResponses[keyof VcsDiffResponses] + +export type VcsDiffRawData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/vcs/diff/raw" +} + +export type VcsDiffRawErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type VcsDiffRawError = VcsDiffRawErrors[keyof VcsDiffRawErrors] + +export type VcsDiffRawResponses = { + /** + * Raw VCS diff + */ + 200: string +} + +export type VcsDiffRawResponse = VcsDiffRawResponses[keyof VcsDiffRawResponses] + +export type VcsApplyData = { + body?: { + patch: string + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/vcs/apply" +} + +export type VcsApplyErrors = { + /** + * VcsApplyError | InvalidRequestError + */ + 400: VcsApplyError | InvalidRequestError +} + +export type VcsApplyError2 = VcsApplyErrors[keyof VcsApplyErrors] + +export type VcsApplyResponses = { + /** + * VCS patch applied + */ + 200: { + applied: boolean + } +} + +export type VcsApplyResponse = VcsApplyResponses[keyof VcsApplyResponses] + +export type CommandListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/command" +} + +export type CommandListErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type CommandListError = CommandListErrors[keyof CommandListErrors] + +export type CommandListResponses = { + /** + * List of commands + */ + 200: Array +} + +export type CommandListResponse = CommandListResponses[keyof CommandListResponses] + +export type AppAgentsData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/agent" +} + +export type AppAgentsErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type AppAgentsError = AppAgentsErrors[keyof AppAgentsErrors] + +export type AppAgentsResponses = { + /** + * List of agents + */ + 200: Array +} + +export type AppAgentsResponse = AppAgentsResponses[keyof AppAgentsResponses] + +export type AppSkillsData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/skill" +} + +export type AppSkillsErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type AppSkillsError = AppSkillsErrors[keyof AppSkillsErrors] + +export type AppSkillsResponses = { + /** + * List of skills + */ + 200: Array<{ + name: string + description?: string + location: string + content: string + }> +} + +export type AppSkillsResponse = AppSkillsResponses[keyof AppSkillsResponses] + +export type LspStatusData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/lsp" +} + +export type LspStatusErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type LspStatusError = LspStatusErrors[keyof LspStatusErrors] + +export type LspStatusResponses = { + /** + * LSP server status + */ + 200: Array +} + +export type LspStatusResponse = LspStatusResponses[keyof LspStatusResponses] + +export type FormatterStatusData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/formatter" +} + +export type FormatterStatusErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type FormatterStatusError = FormatterStatusErrors[keyof FormatterStatusErrors] + +export type FormatterStatusResponses = { + /** + * Formatter status + */ + 200: Array +} + +export type FormatterStatusResponse = FormatterStatusResponses[keyof FormatterStatusResponses] + +export type McpStatusData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/mcp" +} + +export type McpStatusErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type McpStatusError = McpStatusErrors[keyof McpStatusErrors] + +export type McpStatusResponses = { + /** + * MCP server status + */ + 200: { + [key: string]: McpStatus + } +} + +export type McpStatusResponse = McpStatusResponses[keyof McpStatusResponses] + +export type McpAddData = { + body?: { + name: string + config: McpLocalConfig | McpRemoteConfig + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/mcp" +} + +export type McpAddErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError +} + +export type McpAddError = McpAddErrors[keyof McpAddErrors] + +export type McpAddResponses = { + /** + * MCP server added successfully + */ + 200: { + [key: string]: McpStatus + } +} + +export type McpAddResponse = McpAddResponses[keyof McpAddResponses] + +export type McpAuthRemoveData = { + body?: never + path: { + name: string + } + query?: { + directory?: string + workspace?: string + } + url: "/mcp/{name}/auth" +} + +export type McpAuthRemoveErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * McpServerNotFoundError + */ + 404: McpServerNotFoundError +} + +export type McpAuthRemoveError = McpAuthRemoveErrors[keyof McpAuthRemoveErrors] + +export type McpAuthRemoveResponses = { + /** + * OAuth credentials removed + */ + 200: { + success: true + } +} + +export type McpAuthRemoveResponse = McpAuthRemoveResponses[keyof McpAuthRemoveResponses] + +export type McpAuthStartData = { + body?: never + path: { + name: string + } + query?: { + directory?: string + workspace?: string + } + url: "/mcp/{name}/auth" +} + +export type McpAuthStartErrors = { + /** + * McpUnsupportedOAuthError | InvalidRequestError + */ + 400: McpUnsupportedOAuthError | InvalidRequestError + /** + * McpServerNotFoundError + */ + 404: McpServerNotFoundError +} + +export type McpAuthStartError = McpAuthStartErrors[keyof McpAuthStartErrors] + +export type McpAuthStartResponses = { + /** + * OAuth flow started + */ + 200: { + authorizationUrl: string + oauthState: string + } +} + +export type McpAuthStartResponse = McpAuthStartResponses[keyof McpAuthStartResponses] + +export type McpAuthCallbackData = { + body?: { + code: string + } + path: { + name: string + } + query?: { + directory?: string + workspace?: string + } + url: "/mcp/{name}/auth/callback" +} + +export type McpAuthCallbackErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError + /** + * McpServerNotFoundError + */ + 404: McpServerNotFoundError +} + +export type McpAuthCallbackError = McpAuthCallbackErrors[keyof McpAuthCallbackErrors] + +export type McpAuthCallbackResponses = { + /** + * OAuth authentication completed + */ + 200: McpStatus +} + +export type McpAuthCallbackResponse = McpAuthCallbackResponses[keyof McpAuthCallbackResponses] + +export type McpAuthAuthenticateData = { + body?: never + path: { + name: string + } + query?: { + directory?: string + workspace?: string + } + url: "/mcp/{name}/auth/authenticate" +} + +export type McpAuthAuthenticateErrors = { + /** + * McpUnsupportedOAuthError | InvalidRequestError + */ + 400: McpUnsupportedOAuthError | InvalidRequestError + /** + * McpServerNotFoundError + */ + 404: McpServerNotFoundError +} + +export type McpAuthAuthenticateError = McpAuthAuthenticateErrors[keyof McpAuthAuthenticateErrors] + +export type McpAuthAuthenticateResponses = { + /** + * OAuth authentication completed + */ + 200: McpStatus +} + +export type McpAuthAuthenticateResponse = McpAuthAuthenticateResponses[keyof McpAuthAuthenticateResponses] + +export type McpConnectData = { + body?: never + path: { + name: string + } + query?: { + directory?: string + workspace?: string + } + url: "/mcp/{name}/connect" +} + +export type McpConnectErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * McpServerNotFoundError + */ + 404: McpServerNotFoundError +} + +export type McpConnectError = McpConnectErrors[keyof McpConnectErrors] + +export type McpConnectResponses = { + /** + * MCP server connected successfully + */ + 200: boolean +} + +export type McpConnectResponse = McpConnectResponses[keyof McpConnectResponses] + +export type McpDisconnectData = { + body?: never + path: { + name: string + } + query?: { + directory?: string + workspace?: string + } + url: "/mcp/{name}/disconnect" +} + +export type McpDisconnectErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * McpServerNotFoundError + */ + 404: McpServerNotFoundError +} + +export type McpDisconnectError = McpDisconnectErrors[keyof McpDisconnectErrors] + +export type McpDisconnectResponses = { + /** + * MCP server disconnected successfully + */ + 200: boolean +} + +export type McpDisconnectResponse = McpDisconnectResponses[keyof McpDisconnectResponses] + +export type ProjectListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/project" +} + +export type ProjectListErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ProjectListError = ProjectListErrors[keyof ProjectListErrors] + +export type ProjectListResponses = { + /** + * List of projects + */ + 200: Array +} + +export type ProjectListResponse = ProjectListResponses[keyof ProjectListResponses] + +export type ProjectCurrentData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/project/current" +} + +export type ProjectCurrentErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ProjectCurrentError = ProjectCurrentErrors[keyof ProjectCurrentErrors] + +export type ProjectCurrentResponses = { + /** + * Current project information + */ + 200: Project +} + +export type ProjectCurrentResponse = ProjectCurrentResponses[keyof ProjectCurrentResponses] + +export type ProjectInitGitData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/project/git/init" +} + +export type ProjectInitGitErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ProjectInitGitError = ProjectInitGitErrors[keyof ProjectInitGitErrors] + +export type ProjectInitGitResponses = { + /** + * Project information after git initialization + */ + 200: Project +} + +export type ProjectInitGitResponse = ProjectInitGitResponses[keyof ProjectInitGitResponses] + +export type ProjectUpdateData = { + body?: { + name?: string + icon?: ProjectIcon + commands?: ProjectCommands + } + path: { + projectID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/project/{projectID}" +} + +export type ProjectUpdateErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError + /** + * ProjectNotFoundError + */ + 404: ProjectNotFoundError +} + +export type ProjectUpdateError = ProjectUpdateErrors[keyof ProjectUpdateErrors] + +export type ProjectUpdateResponses = { + /** + * Updated project information + */ + 200: Project +} + +export type ProjectUpdateResponse = ProjectUpdateResponses[keyof ProjectUpdateResponses] + +export type ProjectDirectoriesData = { + body?: never + path: { + projectID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/project/{projectID}/directories" +} + +export type ProjectDirectoriesErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ProjectDirectoriesError = ProjectDirectoriesErrors[keyof ProjectDirectoriesErrors] + +export type ProjectDirectoriesResponses = { + /** + * Project directories + */ + 200: ProjectDirectories +} + +export type ProjectDirectoriesResponse = ProjectDirectoriesResponses[keyof ProjectDirectoriesResponses] + +export type ExperimentalProjectCopyGenerateNameData = { + body?: { + context?: string + } + path: { + projectID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/experimental/project/{projectID}/copy/generate-name" +} + +export type ExperimentalProjectCopyGenerateNameErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ExperimentalProjectCopyGenerateNameError = + ExperimentalProjectCopyGenerateNameErrors[keyof ExperimentalProjectCopyGenerateNameErrors] + +export type ExperimentalProjectCopyGenerateNameResponses = { + /** + * Success + */ + 200: { + name: string + } +} + +export type ExperimentalProjectCopyGenerateNameResponse = + ExperimentalProjectCopyGenerateNameResponses[keyof ExperimentalProjectCopyGenerateNameResponses] + +export type PtyShellsData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/pty/shells" +} + +export type PtyShellsErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type PtyShellsError = PtyShellsErrors[keyof PtyShellsErrors] + +export type PtyShellsResponses = { + /** + * List of shells + */ + 200: Array<{ + path: string + name: string + acceptable: boolean + }> +} + +export type PtyShellsResponse = PtyShellsResponses[keyof PtyShellsResponses] + +export type PtyListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/pty" +} + +export type PtyListErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type PtyListError = PtyListErrors[keyof PtyListErrors] + +export type PtyListResponses = { + /** + * List of sessions + */ + 200: Array +} + +export type PtyListResponse = PtyListResponses[keyof PtyListResponses] + +export type PtyCreateData = { + body?: { + command?: string + args?: Array + cwd?: string + title?: string + env?: { + [key: string]: string + } + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/pty" +} + +export type PtyCreateErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError +} + +export type PtyCreateError = PtyCreateErrors[keyof PtyCreateErrors] + +export type PtyCreateResponses = { + /** + * Created session + */ + 200: Pty +} + +export type PtyCreateResponse = PtyCreateResponses[keyof PtyCreateResponses] + +export type PtyRemoveData = { + body?: never + path: { + ptyID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/pty/{ptyID}" +} + +export type PtyRemoveErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * PtyNotFoundError + */ + 404: PtyNotFoundError +} + +export type PtyRemoveError = PtyRemoveErrors[keyof PtyRemoveErrors] + +export type PtyRemoveResponses = { + /** + * Session removed + */ + 200: boolean +} + +export type PtyRemoveResponse = PtyRemoveResponses[keyof PtyRemoveResponses] + +export type PtyGetData = { + body?: never + path: { + ptyID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/pty/{ptyID}" +} + +export type PtyGetErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * PtyNotFoundError + */ + 404: PtyNotFoundError +} + +export type PtyGetError = PtyGetErrors[keyof PtyGetErrors] + +export type PtyGetResponses = { + /** + * Session info + */ + 200: Pty +} + +export type PtyGetResponse = PtyGetResponses[keyof PtyGetResponses] + +export type PtyUpdateData = { + body?: { + title?: string + size?: { + rows: number + cols: number + } + } + path: { + ptyID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/pty/{ptyID}" +} + +export type PtyUpdateErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError + /** + * PtyNotFoundError + */ + 404: PtyNotFoundError +} + +export type PtyUpdateError = PtyUpdateErrors[keyof PtyUpdateErrors] + +export type PtyUpdateResponses = { + /** + * Updated session + */ + 200: Pty +} + +export type PtyUpdateResponse = PtyUpdateResponses[keyof PtyUpdateResponses] + +export type PtyConnectTokenData = { + body?: never + path: { + ptyID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/pty/{ptyID}/connect-token" +} + +export type PtyConnectTokenErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * PtyForbiddenError + */ + 403: PtyForbiddenError + /** + * PtyNotFoundError + */ + 404: PtyNotFoundError +} + +export type PtyConnectTokenError = PtyConnectTokenErrors[keyof PtyConnectTokenErrors] + +export type PtyConnectTokenResponses = { + /** + * WebSocket connect token + */ + 200: PtyTicketConnectToken +} + +export type PtyConnectTokenResponse = PtyConnectTokenResponses[keyof PtyConnectTokenResponses] + +export type QuestionListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/question" +} + +export type QuestionListErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type QuestionListError = QuestionListErrors[keyof QuestionListErrors] + +export type QuestionListResponses = { + /** + * List of pending questions + */ + 200: Array +} + +export type QuestionListResponse = QuestionListResponses[keyof QuestionListResponses] + +export type QuestionReplyData = { + body?: { + /** + * User answers in order of questions (each answer is an array of selected labels) + */ + answers: Array + } + path: { + requestID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/question/{requestID}/reply" +} + +export type QuestionReplyErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError + /** + * QuestionNotFoundError + */ + 404: QuestionNotFoundError +} + +export type QuestionReplyError = QuestionReplyErrors[keyof QuestionReplyErrors] + +export type QuestionReplyResponses = { + /** + * Question answered successfully + */ + 200: boolean +} + +export type QuestionReplyResponse = QuestionReplyResponses[keyof QuestionReplyResponses] + +export type QuestionRejectData = { + body?: never + path: { + requestID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/question/{requestID}/reject" +} + +export type QuestionRejectErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError + /** + * QuestionNotFoundError + */ + 404: QuestionNotFoundError +} + +export type QuestionRejectError = QuestionRejectErrors[keyof QuestionRejectErrors] + +export type QuestionRejectResponses = { + /** + * Question rejected successfully + */ + 200: boolean +} + +export type QuestionRejectResponse = QuestionRejectResponses[keyof QuestionRejectResponses] + +export type PermissionListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/permission" +} + +export type PermissionListErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type PermissionListError = PermissionListErrors[keyof PermissionListErrors] + +export type PermissionListResponses = { + /** + * List of pending permissions + */ + 200: Array +} + +export type PermissionListResponse = PermissionListResponses[keyof PermissionListResponses] + +export type PermissionReplyData = { + body?: { + reply: "once" | "always" | "reject" + message?: string + } + path: { + requestID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/permission/{requestID}/reply" +} + +export type PermissionReplyErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError + /** + * PermissionNotFoundError + */ + 404: PermissionNotFoundError +} + +export type PermissionReplyError = PermissionReplyErrors[keyof PermissionReplyErrors] + +export type PermissionReplyResponses = { + /** + * Permission processed successfully + */ + 200: boolean +} + +export type PermissionReplyResponse = PermissionReplyResponses[keyof PermissionReplyResponses] + +export type ProviderListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/provider" +} + +export type ProviderListErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ProviderListError = ProviderListErrors[keyof ProviderListErrors] + +export type ProviderListResponses = { + /** + * List of providers + */ + 200: { + all: Array + default: { + [key: string]: string + } + connected: Array + } +} + +export type ProviderListResponse = ProviderListResponses[keyof ProviderListResponses] + +export type ProviderAuthData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/provider/auth" +} + +export type ProviderAuthErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ProviderAuthError2 = ProviderAuthErrors[keyof ProviderAuthErrors] + +export type ProviderAuthResponses = { + /** + * Provider auth methods + */ + 200: { + [key: string]: Array + } +} + +export type ProviderAuthResponse = ProviderAuthResponses[keyof ProviderAuthResponses] + +export type ProviderOauthAuthorizeData = { + body?: { + /** + * Auth method index + */ + method: number + inputs?: { + [key: string]: string + } + } + path: { + providerID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/provider/{providerID}/oauth/authorize" +} + +export type ProviderOauthAuthorizeErrors = { + /** + * ProviderAuthError | InvalidRequestError + */ + 400: ProviderAuthError1 | InvalidRequestError +} + +export type ProviderOauthAuthorizeError = ProviderOauthAuthorizeErrors[keyof ProviderOauthAuthorizeErrors] + +export type ProviderOauthAuthorizeResponses = { + /** + * Authorization URL and method + */ + 200: ProviderAuthAuthorization +} + +export type ProviderOauthAuthorizeResponse = ProviderOauthAuthorizeResponses[keyof ProviderOauthAuthorizeResponses] + +export type ProviderOauthCallbackData = { + body?: { + /** + * Auth method index + */ + method: number + code?: string + } + path: { + providerID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/provider/{providerID}/oauth/callback" +} + +export type ProviderOauthCallbackErrors = { + /** + * ProviderAuthError | InvalidRequestError + */ + 400: ProviderAuthError1 | InvalidRequestError +} + +export type ProviderOauthCallbackError = ProviderOauthCallbackErrors[keyof ProviderOauthCallbackErrors] + +export type ProviderOauthCallbackResponses = { + /** + * OAuth callback processed successfully + */ + 200: boolean +} + +export type ProviderOauthCallbackResponse = ProviderOauthCallbackResponses[keyof ProviderOauthCallbackResponses] + +export type SessionListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + scope?: "project" + path?: string + roots?: boolean | "true" | "false" + start?: number + search?: string + limit?: number + } + url: "/session" +} + +export type SessionListErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type SessionListError = SessionListErrors[keyof SessionListErrors] + +export type SessionListResponses = { + /** + * List of sessions + */ + 200: Array +} + +export type SessionListResponse = SessionListResponses[keyof SessionListResponses] + +export type SessionCreateData = { + body?: { + parentID?: string + title?: string + agent?: string + model?: { + id: string + providerID: string + variant?: string + } + metadata?: { + [key: string]: unknown + } + permission?: PermissionRuleset + workspaceID?: string + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/session" +} + +export type SessionCreateErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError +} + +export type SessionCreateError = SessionCreateErrors[keyof SessionCreateErrors] + +export type SessionCreateResponses = { + /** + * Successfully created session + */ + 200: Session +} + +export type SessionCreateResponse = SessionCreateResponses[keyof SessionCreateResponses] + +export type SessionStatusData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/session/status" +} + +export type SessionStatusErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError +} + +export type SessionStatusError = SessionStatusErrors[keyof SessionStatusErrors] + +export type SessionStatusResponses = { + /** + * Get session status + */ + 200: { + [key: string]: SessionStatus + } +} + +export type SessionStatusResponse = SessionStatusResponses[keyof SessionStatusResponses] + +export type SessionDeleteData = { + body?: never + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}" +} + +export type SessionDeleteErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError + /** + * NotFoundError + */ + 404: NotFoundError +} + +export type SessionDeleteError = SessionDeleteErrors[keyof SessionDeleteErrors] + +export type SessionDeleteResponses = { + /** + * Successfully deleted session + */ + 200: boolean +} + +export type SessionDeleteResponse = SessionDeleteResponses[keyof SessionDeleteResponses] + +export type SessionGetData = { + body?: never + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}" +} + +export type SessionGetErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError + /** + * NotFoundError + */ + 404: NotFoundError +} + +export type SessionGetError = SessionGetErrors[keyof SessionGetErrors] + +export type SessionGetResponses = { + /** + * Get session + */ + 200: Session +} + +export type SessionGetResponse = SessionGetResponses[keyof SessionGetResponses] + +export type SessionUpdateData = { + body?: { + title?: string + metadata?: { + [key: string]: unknown + } + permission?: PermissionRuleset + time?: { + archived?: number + } + } + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}" +} + +export type SessionUpdateErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError + /** + * NotFoundError + */ + 404: NotFoundError +} + +export type SessionUpdateError = SessionUpdateErrors[keyof SessionUpdateErrors] + +export type SessionUpdateResponses = { + /** + * Successfully updated session + */ + 200: Session +} + +export type SessionUpdateResponse = SessionUpdateResponses[keyof SessionUpdateResponses] + +export type SessionChildrenData = { + body?: never + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/children" +} + +export type SessionChildrenErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError + /** + * NotFoundError + */ + 404: NotFoundError +} + +export type SessionChildrenError = SessionChildrenErrors[keyof SessionChildrenErrors] + +export type SessionChildrenResponses = { + /** + * List of children + */ + 200: Array +} + +export type SessionChildrenResponse = SessionChildrenResponses[keyof SessionChildrenResponses] + +export type SessionTodoData = { + body?: never + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/todo" +} + +export type SessionTodoErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError + /** + * NotFoundError + */ + 404: NotFoundError +} + +export type SessionTodoError = SessionTodoErrors[keyof SessionTodoErrors] + +export type SessionTodoResponses = { + /** + * Todo list + */ + 200: Array +} + +export type SessionTodoResponse = SessionTodoResponses[keyof SessionTodoResponses] + +export type SessionDiffData = { + body?: never + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + messageID?: string + } + url: "/session/{sessionID}/diff" +} + +export type SessionDiffErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type SessionDiffError = SessionDiffErrors[keyof SessionDiffErrors] + +export type SessionDiffResponses = { + /** + * Successfully retrieved diff + */ + 200: Array +} + +export type SessionDiffResponse = SessionDiffResponses[keyof SessionDiffResponses] + +export type SessionTouchedFilesData = { + body?: never + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/touched-files" +} + +export type SessionTouchedFilesErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * NotFoundError + */ + 404: NotFoundError +} + +export type SessionTouchedFilesError = SessionTouchedFilesErrors[keyof SessionTouchedFilesErrors] + +export type SessionTouchedFilesResponses = { + /** + * All files touched by edit tools in this session + */ + 200: Array<{ + file: string + status: "added" | "modified" + }> +} + +export type SessionTouchedFilesResponse = SessionTouchedFilesResponses[keyof SessionTouchedFilesResponses] + +export type SessionMessagesData = { + body?: never + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + limit?: number + before?: string + } + url: "/session/{sessionID}/message" +} + +export type SessionMessagesErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError + /** + * NotFoundError + */ + 404: NotFoundError +} + +export type SessionMessagesError = SessionMessagesErrors[keyof SessionMessagesErrors] + +export type SessionMessagesResponses = { + /** + * List of messages + */ + 200: Array<{ + info: Message + parts: Array + }> +} + +export type SessionMessagesResponse2 = SessionMessagesResponses[keyof SessionMessagesResponses] + +export type SessionPromptData = { + body?: { + messageID?: string + model?: { + providerID: string + modelID: string + } + agent?: string + noReply?: boolean + tools?: { + [key: string]: boolean + } + format?: OutputFormat + system?: string + variant?: string + parts: Array + } + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/message" +} + +export type SessionPromptErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError + /** + * NotFoundError + */ + 404: NotFoundError +} + +export type SessionPromptError = SessionPromptErrors[keyof SessionPromptErrors] + +export type SessionPromptResponses = { + /** + * Created message + */ + 200: { + info: AssistantMessage + parts: Array + } +} + +export type SessionPromptResponse = SessionPromptResponses[keyof SessionPromptResponses] + +export type SessionDeleteMessageData = { + body?: never + path: { + sessionID: string + messageID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/message/{messageID}" +} + +export type SessionDeleteMessageErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError + /** + * NotFoundError + */ + 404: NotFoundError + /** + * SessionBusyError + */ + 409: SessionBusyError +} + +export type SessionDeleteMessageError = SessionDeleteMessageErrors[keyof SessionDeleteMessageErrors] + +export type SessionDeleteMessageResponses = { + /** + * Successfully deleted message + */ + 200: boolean +} + +export type SessionDeleteMessageResponse = SessionDeleteMessageResponses[keyof SessionDeleteMessageResponses] + +export type SessionMessageData = { + body?: never + path: { + sessionID: string + messageID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/message/{messageID}" +} + +export type SessionMessageErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError + /** + * NotFoundError + */ + 404: NotFoundError +} + +export type SessionMessageError = SessionMessageErrors[keyof SessionMessageErrors] + +export type SessionMessageResponses = { + /** + * Message + */ + 200: { + info: Message + parts: Array + } +} + +export type SessionMessageResponse = SessionMessageResponses[keyof SessionMessageResponses] + +export type SessionForkData = { + body?: { + messageID?: string + } + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/fork" +} + +export type SessionForkErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError + /** + * NotFoundError + */ + 404: NotFoundError +} + +export type SessionForkError = SessionForkErrors[keyof SessionForkErrors] + +export type SessionForkResponses = { + /** + * 200 + */ + 200: Session +} + +export type SessionForkResponse = SessionForkResponses[keyof SessionForkResponses] + +export type SessionAbortData = { + body?: never + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/abort" +} + +export type SessionAbortErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError +} + +export type SessionAbortError = SessionAbortErrors[keyof SessionAbortErrors] + +export type SessionAbortResponses = { + /** + * Aborted session + */ + 200: boolean +} + +export type SessionAbortResponse = SessionAbortResponses[keyof SessionAbortResponses] + +export type SessionInitData = { + body?: { + modelID: string + providerID: string + messageID: string + } + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/init" +} + +export type SessionInitErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError + /** + * NotFoundError + */ + 404: NotFoundError +} + +export type SessionInitError = SessionInitErrors[keyof SessionInitErrors] + +export type SessionInitResponses = { + /** + * 200 + */ + 200: boolean +} + +export type SessionInitResponse = SessionInitResponses[keyof SessionInitResponses] + +export type SessionUnshareData = { + body?: never + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/share" +} + +export type SessionUnshareErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * NotFoundError + */ + 404: NotFoundError + /** + * InternalServerError + */ + 500: EffectHttpApiErrorInternalServerError +} + +export type SessionUnshareError = SessionUnshareErrors[keyof SessionUnshareErrors] + +export type SessionUnshareResponses = { + /** + * Successfully unshared session + */ + 200: Session +} + +export type SessionUnshareResponse = SessionUnshareResponses[keyof SessionUnshareResponses] + +export type SessionShareData = { + body?: never + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/share" +} + +export type SessionShareErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * NotFoundError + */ + 404: NotFoundError + /** + * InternalServerError + */ + 500: EffectHttpApiErrorInternalServerError +} + +export type SessionShareError = SessionShareErrors[keyof SessionShareErrors] + +export type SessionShareResponses = { + /** + * Successfully shared session + */ + 200: Session +} + +export type SessionShareResponse = SessionShareResponses[keyof SessionShareResponses] + +export type SessionSummarizeData = { + body?: { + providerID: string + modelID: string + auto?: boolean + } + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/summarize" +} + +export type SessionSummarizeErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError + /** + * NotFoundError + */ + 404: NotFoundError +} + +export type SessionSummarizeError = SessionSummarizeErrors[keyof SessionSummarizeErrors] + +export type SessionSummarizeResponses = { + /** + * Summarized session + */ + 200: boolean +} + +export type SessionSummarizeResponse = SessionSummarizeResponses[keyof SessionSummarizeResponses] + +export type SessionPromptAsyncData = { + body?: { + messageID?: string + model?: { + providerID: string + modelID: string + } + agent?: string + noReply?: boolean + tools?: { + [key: string]: boolean + } + format?: OutputFormat + system?: string + variant?: string + parts: Array + } + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/prompt_async" +} + +export type SessionPromptAsyncErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError + /** + * NotFoundError + */ + 404: NotFoundError +} + +export type SessionPromptAsyncError = SessionPromptAsyncErrors[keyof SessionPromptAsyncErrors] + +export type SessionPromptAsyncResponses = { + /** + * Prompt accepted + */ + 204: void +} + +export type SessionPromptAsyncResponse = SessionPromptAsyncResponses[keyof SessionPromptAsyncResponses] + +export type SessionCommandData = { + body?: { + messageID?: string + agent?: string + model?: string + arguments: string + command: string + variant?: string + parts?: Array<{ + id?: string + type: "file" + mime: string + filename?: string + url: string + source?: FilePartSource + }> + } + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/command" +} + +export type SessionCommandErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError + /** + * NotFoundError + */ + 404: NotFoundError +} + +export type SessionCommandError = SessionCommandErrors[keyof SessionCommandErrors] + +export type SessionCommandResponses = { + /** + * Created message + */ + 200: { + info: AssistantMessage + parts: Array + } +} + +export type SessionCommandResponse = SessionCommandResponses[keyof SessionCommandResponses] + +export type SessionShellData = { + body?: { + messageID?: string + agent: string + model?: { + providerID: string + modelID: string + } + command: string + } + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/shell" +} + +export type SessionShellErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError + /** + * NotFoundError + */ + 404: NotFoundError + /** + * SessionBusyError + */ + 409: SessionBusyError +} + +export type SessionShellError = SessionShellErrors[keyof SessionShellErrors] + +export type SessionShellResponses = { + /** + * Created message + */ + 200: { + info: Message + parts: Array + } +} + +export type SessionShellResponse = SessionShellResponses[keyof SessionShellResponses] + +export type SessionRevertData = { + body?: { + messageID: string + partID?: string + } + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/revert" +} + +export type SessionRevertErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError + /** + * NotFoundError + */ + 404: NotFoundError + /** + * SessionBusyError + */ + 409: SessionBusyError +} + +export type SessionRevertError = SessionRevertErrors[keyof SessionRevertErrors] + +export type SessionRevertResponses = { + /** + * Updated session + */ + 200: Session +} + +export type SessionRevertResponse = SessionRevertResponses[keyof SessionRevertResponses] + +export type SessionUnrevertData = { + body?: never + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/unrevert" +} + +export type SessionUnrevertErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError + /** + * NotFoundError + */ + 404: NotFoundError + /** + * SessionBusyError + */ + 409: SessionBusyError +} + +export type SessionUnrevertError = SessionUnrevertErrors[keyof SessionUnrevertErrors] + +export type SessionUnrevertResponses = { + /** + * Updated session + */ + 200: Session +} + +export type SessionUnrevertResponse = SessionUnrevertResponses[keyof SessionUnrevertResponses] + +export type PermissionRespondData = { + body?: { + response: "once" | "always" | "reject" + } + path: { + sessionID: string + permissionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/permissions/{permissionID}" +} + +export type PermissionRespondErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError + /** + * NotFoundError | PermissionNotFoundError + */ + 404: NotFoundError | PermissionNotFoundError +} + +export type PermissionRespondError = PermissionRespondErrors[keyof PermissionRespondErrors] + +export type PermissionRespondResponses = { + /** + * Permission processed successfully + */ + 200: boolean +} + +export type PermissionRespondResponse = PermissionRespondResponses[keyof PermissionRespondResponses] + +export type PartDeleteData = { + body?: never + path: { + sessionID: string + messageID: string + partID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/message/{messageID}/part/{partID}" +} + +export type PartDeleteErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError + /** + * NotFoundError + */ + 404: NotFoundError +} + +export type PartDeleteError = PartDeleteErrors[keyof PartDeleteErrors] + +export type PartDeleteResponses = { + /** + * Successfully deleted part + */ + 200: boolean +} + +export type PartDeleteResponse = PartDeleteResponses[keyof PartDeleteResponses] + +export type PartUpdateData = { + body?: Part + path: { + sessionID: string + messageID: string + partID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/message/{messageID}/part/{partID}" +} + +export type PartUpdateErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError + /** + * NotFoundError + */ + 404: NotFoundError +} + +export type PartUpdateError = PartUpdateErrors[keyof PartUpdateErrors] + +export type PartUpdateResponses = { + /** + * Successfully updated part + */ + 200: Part +} + +export type PartUpdateResponse = PartUpdateResponses[keyof PartUpdateResponses] + +export type SyncStartData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/sync/start" +} + +export type SyncStartErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type SyncStartError = SyncStartErrors[keyof SyncStartErrors] + +export type SyncStartResponses = { + /** + * Workspace sync started + */ + 200: boolean +} + +export type SyncStartResponse = SyncStartResponses[keyof SyncStartResponses] + +export type SyncReplayData = { + body?: { + directory: string + events: Array<{ + id: string + aggregateID: string + seq: number + type: string + data: { + [key: string]: unknown + } + }> + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/sync/replay" +} + +export type SyncReplayErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError +} + +export type SyncReplayError = SyncReplayErrors[keyof SyncReplayErrors] + +export type SyncReplayResponses = { + /** + * Replayed sync events + */ + 200: { + sessionID: string + } +} + +export type SyncReplayResponse = SyncReplayResponses[keyof SyncReplayResponses] + +export type SyncStealData = { + body?: { + sessionID: string + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/sync/steal" +} + +export type SyncStealErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError +} + +export type SyncStealError = SyncStealErrors[keyof SyncStealErrors] + +export type SyncStealResponses = { + /** + * Session stolen into workspace + */ + 200: { + sessionID: string + } +} + +export type SyncStealResponse = SyncStealResponses[keyof SyncStealResponses] + +export type SyncHistoryListData = { + body?: { + [key: string]: number + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/sync/history" +} + +export type SyncHistoryListErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError +} + +export type SyncHistoryListError = SyncHistoryListErrors[keyof SyncHistoryListErrors] + +export type SyncHistoryListResponses = { + /** + * Sync events + */ + 200: Array<{ + id: string + aggregate_id: string + seq: number + type: string + data: { + [key: string]: unknown + } + }> +} + +export type SyncHistoryListResponse = SyncHistoryListResponses[keyof SyncHistoryListResponses] + +export type TuiAppendPromptData = { + body?: { + text: string + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/append-prompt" +} + +export type TuiAppendPromptErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError +} + +export type TuiAppendPromptError = TuiAppendPromptErrors[keyof TuiAppendPromptErrors] + +export type TuiAppendPromptResponses = { + /** + * Prompt processed successfully + */ + 200: boolean +} + +export type TuiAppendPromptResponse = TuiAppendPromptResponses[keyof TuiAppendPromptResponses] + +export type TuiOpenHelpData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/open-help" +} + +export type TuiOpenHelpErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type TuiOpenHelpError = TuiOpenHelpErrors[keyof TuiOpenHelpErrors] + +export type TuiOpenHelpResponses = { + /** + * Help dialog opened successfully + */ + 200: boolean +} + +export type TuiOpenHelpResponse = TuiOpenHelpResponses[keyof TuiOpenHelpResponses] + +export type TuiOpenSessionsData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/open-sessions" +} + +export type TuiOpenSessionsErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type TuiOpenSessionsError = TuiOpenSessionsErrors[keyof TuiOpenSessionsErrors] + +export type TuiOpenSessionsResponses = { + /** + * Session dialog opened successfully + */ + 200: boolean +} + +export type TuiOpenSessionsResponse = TuiOpenSessionsResponses[keyof TuiOpenSessionsResponses] + +export type TuiOpenThemesData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/open-themes" +} + +export type TuiOpenThemesErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type TuiOpenThemesError = TuiOpenThemesErrors[keyof TuiOpenThemesErrors] + +export type TuiOpenThemesResponses = { + /** + * Theme dialog opened successfully + */ + 200: boolean +} + +export type TuiOpenThemesResponse = TuiOpenThemesResponses[keyof TuiOpenThemesResponses] + +export type TuiOpenModelsData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/open-models" +} + +export type TuiOpenModelsErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type TuiOpenModelsError = TuiOpenModelsErrors[keyof TuiOpenModelsErrors] + +export type TuiOpenModelsResponses = { + /** + * Model dialog opened successfully + */ + 200: boolean +} + +export type TuiOpenModelsResponse = TuiOpenModelsResponses[keyof TuiOpenModelsResponses] + +export type TuiSubmitPromptData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/submit-prompt" +} + +export type TuiSubmitPromptErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type TuiSubmitPromptError = TuiSubmitPromptErrors[keyof TuiSubmitPromptErrors] + +export type TuiSubmitPromptResponses = { + /** + * Prompt submitted successfully + */ + 200: boolean +} + +export type TuiSubmitPromptResponse = TuiSubmitPromptResponses[keyof TuiSubmitPromptResponses] + +export type TuiClearPromptData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/clear-prompt" +} + +export type TuiClearPromptErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type TuiClearPromptError = TuiClearPromptErrors[keyof TuiClearPromptErrors] + +export type TuiClearPromptResponses = { + /** + * Prompt cleared successfully + */ + 200: boolean +} + +export type TuiClearPromptResponse = TuiClearPromptResponses[keyof TuiClearPromptResponses] + +export type TuiExecuteCommandData = { + body?: { + command: string + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/execute-command" +} + +export type TuiExecuteCommandErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError +} + +export type TuiExecuteCommandError = TuiExecuteCommandErrors[keyof TuiExecuteCommandErrors] + +export type TuiExecuteCommandResponses = { + /** + * Command executed successfully + */ + 200: boolean +} + +export type TuiExecuteCommandResponse = TuiExecuteCommandResponses[keyof TuiExecuteCommandResponses] + +export type TuiShowToastData = { + body?: { + title?: string + message: string + variant: "info" | "success" | "warning" | "error" + duration?: number + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/show-toast" +} + +export type TuiShowToastErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type TuiShowToastError = TuiShowToastErrors[keyof TuiShowToastErrors] + +export type TuiShowToastResponses = { + /** + * Toast notification shown successfully + */ + 200: boolean +} + +export type TuiShowToastResponse = TuiShowToastResponses[keyof TuiShowToastResponses] + +export type TuiPublishData = { + body?: EventTuiPromptAppend | EventTuiCommandExecute | EventTuiToastShow | EventTuiSessionSelect + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/publish" +} + +export type TuiPublishErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError +} + +export type TuiPublishError = TuiPublishErrors[keyof TuiPublishErrors] + +export type TuiPublishResponses = { + /** + * Event published successfully + */ + 200: boolean +} + +export type TuiPublishResponse = TuiPublishResponses[keyof TuiPublishResponses] + +export type TuiSelectSessionData = { + body?: { + /** + * Session ID to navigate to + */ + sessionID: string + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/select-session" +} + +export type TuiSelectSessionErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError + /** + * NotFoundError + */ + 404: NotFoundError +} + +export type TuiSelectSessionError = TuiSelectSessionErrors[keyof TuiSelectSessionErrors] + +export type TuiSelectSessionResponses = { + /** + * Session selected successfully + */ + 200: boolean +} + +export type TuiSelectSessionResponse = TuiSelectSessionResponses[keyof TuiSelectSessionResponses] + +export type TuiControlNextData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/control/next" +} + +export type TuiControlNextErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type TuiControlNextError = TuiControlNextErrors[keyof TuiControlNextErrors] + +export type TuiControlNextResponses = { + /** + * Next TUI request + */ + 200: { + path: string + body: unknown + } +} + +export type TuiControlNextResponse = TuiControlNextResponses[keyof TuiControlNextResponses] + +export type TuiControlResponseData = { + body?: unknown + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/tui/control/response" +} + +export type TuiControlResponseErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type TuiControlResponseError = TuiControlResponseErrors[keyof TuiControlResponseErrors] + +export type TuiControlResponseResponses = { + /** + * Response submitted successfully + */ + 200: boolean +} + +export type TuiControlResponseResponse = TuiControlResponseResponses[keyof TuiControlResponseResponses] + +export type ExperimentalWorkspaceAdapterListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/workspace/adapter" +} + +export type ExperimentalWorkspaceAdapterListErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ExperimentalWorkspaceAdapterListError = + ExperimentalWorkspaceAdapterListErrors[keyof ExperimentalWorkspaceAdapterListErrors] + +export type ExperimentalWorkspaceAdapterListResponses = { + /** + * Workspace adapters + */ + 200: Array<{ + type: string + name: string + description: string + }> +} + +export type ExperimentalWorkspaceAdapterListResponse = + ExperimentalWorkspaceAdapterListResponses[keyof ExperimentalWorkspaceAdapterListResponses] + +export type ExperimentalWorkspaceListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/workspace" +} + +export type ExperimentalWorkspaceListErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ExperimentalWorkspaceListError = ExperimentalWorkspaceListErrors[keyof ExperimentalWorkspaceListErrors] + +export type ExperimentalWorkspaceListResponses = { + /** + * Workspaces + */ + 200: Array +} + +export type ExperimentalWorkspaceListResponse = + ExperimentalWorkspaceListResponses[keyof ExperimentalWorkspaceListResponses] + +export type ExperimentalWorkspaceCreateData = { + body?: { + id?: string + type: string + branch?: string | null + extra?: unknown | null + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/workspace" +} + +export type ExperimentalWorkspaceCreateErrors = { + /** + * WorkspaceCreateError | BadRequest | InvalidRequestError + */ + 400: WorkspaceCreateError | EffectHttpApiErrorBadRequest | InvalidRequestError +} + +export type ExperimentalWorkspaceCreateError = + ExperimentalWorkspaceCreateErrors[keyof ExperimentalWorkspaceCreateErrors] + +export type ExperimentalWorkspaceCreateResponses = { + /** + * Workspace created + */ + 200: Workspace +} + +export type ExperimentalWorkspaceCreateResponse = + ExperimentalWorkspaceCreateResponses[keyof ExperimentalWorkspaceCreateResponses] + +export type ExperimentalWorkspaceSyncListData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/workspace/sync-list" +} + +export type ExperimentalWorkspaceSyncListErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ExperimentalWorkspaceSyncListError = + ExperimentalWorkspaceSyncListErrors[keyof ExperimentalWorkspaceSyncListErrors] + +export type ExperimentalWorkspaceSyncListResponses = { + /** + * Workspace list synced + */ + 204: void +} + +export type ExperimentalWorkspaceSyncListResponse = + ExperimentalWorkspaceSyncListResponses[keyof ExperimentalWorkspaceSyncListResponses] + +export type ExperimentalWorkspaceStatusData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/workspace/status" +} + +export type ExperimentalWorkspaceStatusErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type ExperimentalWorkspaceStatusError = + ExperimentalWorkspaceStatusErrors[keyof ExperimentalWorkspaceStatusErrors] + +export type ExperimentalWorkspaceStatusResponses = { + /** + * Workspace status + */ + 200: Array +} + +export type ExperimentalWorkspaceStatusResponse = + ExperimentalWorkspaceStatusResponses[keyof ExperimentalWorkspaceStatusResponses] + +export type ExperimentalWorkspaceRemoveData = { + body?: never + path: { + id: string + } + query?: { + directory?: string + workspace?: string + } + url: "/experimental/workspace/{id}" +} + +export type ExperimentalWorkspaceRemoveErrors = { + /** + * BadRequest | InvalidRequestError + */ + 400: EffectHttpApiErrorBadRequest | InvalidRequestError +} + +export type ExperimentalWorkspaceRemoveError = + ExperimentalWorkspaceRemoveErrors[keyof ExperimentalWorkspaceRemoveErrors] + +export type ExperimentalWorkspaceRemoveResponses = { + /** + * Workspace removed + */ + 200: Workspace +} + +export type ExperimentalWorkspaceRemoveResponse = + ExperimentalWorkspaceRemoveResponses[keyof ExperimentalWorkspaceRemoveResponses] + +export type ExperimentalWorkspaceWarpData = { + body?: { + id: string | null + sessionID: string + copyChanges?: boolean + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/experimental/workspace/warp" +} + +export type ExperimentalWorkspaceWarpErrors = { + /** + * WorkspaceWarpError | VcsApplyError | InvalidRequestError + */ + 400: WorkspaceWarpError | VcsApplyError | InvalidRequestError + /** + * NotFoundError + */ + 404: NotFoundError +} + +export type ExperimentalWorkspaceWarpError = ExperimentalWorkspaceWarpErrors[keyof ExperimentalWorkspaceWarpErrors] + +export type ExperimentalWorkspaceWarpResponses = { + /** + * Session warped + */ + 204: void +} + +export type ExperimentalWorkspaceWarpResponse = + ExperimentalWorkspaceWarpResponses[keyof ExperimentalWorkspaceWarpResponses] + +export type V2HealthGetData = { + body?: never + path?: never + query?: never + url: "/api/health" +} + +export type V2HealthGetErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2HealthGetError = V2HealthGetErrors[keyof V2HealthGetErrors] + +export type V2HealthGetResponses = { + /** + * Success + */ + 200: { + healthy: true + } +} + +export type V2HealthGetResponse = V2HealthGetResponses[keyof V2HealthGetResponses] + +export type V2LocationGetData = { + body?: never + path?: never + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/api/location" +} + +export type V2LocationGetErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2LocationGetError = V2LocationGetErrors[keyof V2LocationGetErrors] + +export type V2LocationGetResponses = { + /** + * Location.Info + */ + 200: LocationInfo +} + +export type V2LocationGetResponse = V2LocationGetResponses[keyof V2LocationGetResponses] + +export type V2AgentListData = { + body?: never + path?: never + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/api/agent" +} + +export type V2AgentListErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2AgentListError = V2AgentListErrors[keyof V2AgentListErrors] + +export type V2AgentListResponses = { + /** + * Success + */ + 200: { + location: LocationInfo + data: Array + } +} + +export type V2AgentListResponse = V2AgentListResponses[keyof V2AgentListResponses] + +export type V2SessionListData = { + body?: never + path?: never + query?: { + workspace?: string + limit?: number + order?: "asc" | "desc" + search?: string + directory?: string + project?: string + subpath?: string + /** + * Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. + */ + cursor?: string + } + url: "/api/session" +} + +export type V2SessionListErrors = { + /** + * InvalidCursorError | InvalidRequestError + */ + 400: InvalidCursorError | InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2SessionListError = V2SessionListErrors[keyof V2SessionListErrors] + +export type V2SessionListResponses = { + /** + * SessionsResponse + */ + 200: SessionsResponse +} + +export type V2SessionListResponse = V2SessionListResponses[keyof V2SessionListResponses] + +export type V2SessionCreateData = { + body: { + id?: string + agent?: string + model?: ModelRef + location?: LocationRef + } + path?: never + query?: never + url: "/api/session" +} + +export type V2SessionCreateErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2SessionCreateError = V2SessionCreateErrors[keyof V2SessionCreateErrors] + +export type V2SessionCreateResponses = { + /** + * Success + */ + 200: { + data: SessionV2Info + } +} + +export type V2SessionCreateResponse = V2SessionCreateResponses[keyof V2SessionCreateResponses] + +export type V2SessionActiveData = { + body?: never + path?: never + query?: never + url: "/api/session/active" +} + +export type V2SessionActiveErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2SessionActiveError = V2SessionActiveErrors[keyof V2SessionActiveErrors] + +export type V2SessionActiveResponses = { + /** + * Success + */ + 200: { + data: { + [key: string]: unknown | SessionActive + } + } +} + +export type V2SessionActiveResponse = V2SessionActiveResponses[keyof V2SessionActiveResponses] + +export type V2SessionGetData = { + body?: never + path: { + sessionID: string + } + query?: never + url: "/api/session/{sessionID}" +} + +export type V2SessionGetErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * SessionNotFoundError + */ + 404: SessionNotFoundError +} + +export type V2SessionGetError = V2SessionGetErrors[keyof V2SessionGetErrors] + +export type V2SessionGetResponses = { + /** + * Success + */ + 200: { + data: SessionV2Info + } +} + +export type V2SessionGetResponse = V2SessionGetResponses[keyof V2SessionGetResponses] + +export type V2SessionSwitchAgentData = { + body: { + agent: string + } + path: { + sessionID: string + } + query?: never + url: "/api/session/{sessionID}/agent" +} + +export type V2SessionSwitchAgentErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * SessionNotFoundError + */ + 404: SessionNotFoundError +} + +export type V2SessionSwitchAgentError = V2SessionSwitchAgentErrors[keyof V2SessionSwitchAgentErrors] + +export type V2SessionSwitchAgentResponses = { + /** + * + */ + 204: void +} + +export type V2SessionSwitchAgentResponse = V2SessionSwitchAgentResponses[keyof V2SessionSwitchAgentResponses] + +export type V2SessionSwitchModelData = { + body: { + model: ModelRef + } + path: { + sessionID: string + } + query?: never + url: "/api/session/{sessionID}/model" +} + +export type V2SessionSwitchModelErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * SessionNotFoundError + */ + 404: SessionNotFoundError +} + +export type V2SessionSwitchModelError = V2SessionSwitchModelErrors[keyof V2SessionSwitchModelErrors] + +export type V2SessionSwitchModelResponses = { + /** + * + */ + 204: void +} + +export type V2SessionSwitchModelResponse = V2SessionSwitchModelResponses[keyof V2SessionSwitchModelResponses] + +export type V2SessionPromptData = { + body: { + id?: string + prompt: PromptInput + delivery?: "steer" | "queue" + resume?: boolean + } + path: { + sessionID: string + } + query?: never + url: "/api/session/{sessionID}/prompt" +} + +export type V2SessionPromptErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * SessionNotFoundError + */ + 404: SessionNotFoundError + /** + * ConflictError + */ + 409: ConflictError +} + +export type V2SessionPromptError = V2SessionPromptErrors[keyof V2SessionPromptErrors] + +export type V2SessionPromptResponses = { + /** + * Success + */ + 200: { + data: SessionInputAdmitted + } +} + +export type V2SessionPromptResponse = V2SessionPromptResponses[keyof V2SessionPromptResponses] + +export type V2SessionCompactData = { + body?: never + path: { + sessionID: string + } + query?: never + url: "/api/session/{sessionID}/compact" +} + +export type V2SessionCompactErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * SessionNotFoundError + */ + 404: SessionNotFoundError + /** + * ServiceUnavailableError + */ + 503: ServiceUnavailableError +} + +export type V2SessionCompactError = V2SessionCompactErrors[keyof V2SessionCompactErrors] + +export type V2SessionCompactResponses = { + /** + * + */ + 204: void +} + +export type V2SessionCompactResponse = V2SessionCompactResponses[keyof V2SessionCompactResponses] + +export type V2SessionWaitData = { + body?: never + path: { + sessionID: string + } + query?: never + url: "/api/session/{sessionID}/wait" +} + +export type V2SessionWaitErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * SessionNotFoundError + */ + 404: SessionNotFoundError + /** + * ServiceUnavailableError + */ + 503: ServiceUnavailableError +} + +export type V2SessionWaitError = V2SessionWaitErrors[keyof V2SessionWaitErrors] + +export type V2SessionWaitResponses = { + /** + * + */ + 204: void +} + +export type V2SessionWaitResponse = V2SessionWaitResponses[keyof V2SessionWaitResponses] + +export type V2SessionRevertStageData = { + body: { + messageID: string + files?: boolean + } + path: { + sessionID: string + } + query?: never + url: "/api/session/{sessionID}/revert/stage" +} + +export type V2SessionRevertStageErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * MessageNotFoundError | SessionNotFoundError + */ + 404: MessageNotFoundError | SessionNotFoundError + /** + * UnknownError + */ + 500: UnknownError1 +} + +export type V2SessionRevertStageError = V2SessionRevertStageErrors[keyof V2SessionRevertStageErrors] + +export type V2SessionRevertStageResponses = { + /** + * Success + */ + 200: { + data: RevertState + } +} + +export type V2SessionRevertStageResponse = V2SessionRevertStageResponses[keyof V2SessionRevertStageResponses] + +export type V2SessionRevertClearData = { + body?: never + path: { + sessionID: string + } + query?: never + url: "/api/session/{sessionID}/revert/clear" +} + +export type V2SessionRevertClearErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * SessionNotFoundError + */ + 404: SessionNotFoundError + /** + * UnknownError + */ + 500: UnknownError1 +} + +export type V2SessionRevertClearError = V2SessionRevertClearErrors[keyof V2SessionRevertClearErrors] + +export type V2SessionRevertClearResponses = { + /** + * + */ + 204: void +} + +export type V2SessionRevertClearResponse = V2SessionRevertClearResponses[keyof V2SessionRevertClearResponses] + +export type V2SessionRevertCommitData = { + body?: never + path: { + sessionID: string + } + query?: never + url: "/api/session/{sessionID}/revert/commit" +} + +export type V2SessionRevertCommitErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * SessionNotFoundError + */ + 404: SessionNotFoundError +} + +export type V2SessionRevertCommitError = V2SessionRevertCommitErrors[keyof V2SessionRevertCommitErrors] + +export type V2SessionRevertCommitResponses = { + /** + * + */ + 204: void +} + +export type V2SessionRevertCommitResponse = V2SessionRevertCommitResponses[keyof V2SessionRevertCommitResponses] + +export type V2SessionContextData = { + body?: never + path: { + sessionID: string + } + query?: never + url: "/api/session/{sessionID}/context" +} + +export type V2SessionContextErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * SessionNotFoundError + */ + 404: SessionNotFoundError + /** + * UnknownError + */ + 500: UnknownError1 +} + +export type V2SessionContextError = V2SessionContextErrors[keyof V2SessionContextErrors] + +export type V2SessionContextResponses = { + /** + * Success + */ + 200: { + data: Array + } +} + +export type V2SessionContextResponse = V2SessionContextResponses[keyof V2SessionContextResponses] + +export type V2SessionHistoryData = { + body?: never + path: { + sessionID: string + } + query?: { + limit?: number + after?: number + } + url: "/api/session/{sessionID}/history" +} + +export type V2SessionHistoryErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * SessionNotFoundError + */ + 404: SessionNotFoundError +} + +export type V2SessionHistoryError = V2SessionHistoryErrors[keyof V2SessionHistoryErrors] + +export type V2SessionHistoryResponses = { + /** + * SessionHistory + */ + 200: SessionHistory +} + +export type V2SessionHistoryResponse = V2SessionHistoryResponses[keyof V2SessionHistoryResponses] + +export type V2SessionEventsData = { + body?: never + path: { + sessionID: string + } + query?: { + after?: string + } + url: "/api/session/{sessionID}/event" +} + +export type V2SessionEventsErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * SessionNotFoundError + */ + 404: SessionNotFoundError +} + +export type V2SessionEventsError = V2SessionEventsErrors[keyof V2SessionEventsErrors] + +export type V2SessionEventsResponses = { + /** + * Success + */ + 200: { + id: string + event: string + data: SessionDurableEventStream + } +} + +export type V2SessionEventsResponse = V2SessionEventsResponses[keyof V2SessionEventsResponses] + +export type V2SessionInterruptData = { + body?: never + path: { + sessionID: string + } + query?: never + url: "/api/session/{sessionID}/interrupt" +} + +export type V2SessionInterruptErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * SessionNotFoundError + */ + 404: SessionNotFoundError +} + +export type V2SessionInterruptError = V2SessionInterruptErrors[keyof V2SessionInterruptErrors] + +export type V2SessionInterruptResponses = { + /** + * + */ + 204: void +} + +export type V2SessionInterruptResponse = V2SessionInterruptResponses[keyof V2SessionInterruptResponses] + +export type V2SessionMessageData = { + body?: never + path: { + sessionID: string + messageID: string + } + query?: never + url: "/api/session/{sessionID}/message/{messageID}" +} + +export type V2SessionMessageErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * SessionNotFoundError | MessageNotFoundError + */ + 404: MessageNotFoundError | SessionNotFoundError +} + +export type V2SessionMessageError = V2SessionMessageErrors[keyof V2SessionMessageErrors] + +export type V2SessionMessageResponses = { + /** + * Success + */ + 200: { + data: SessionMessage + } +} + +export type V2SessionMessageResponse = V2SessionMessageResponses[keyof V2SessionMessageResponses] + +export type V2SessionMessagesData = { + body?: never + path: { + sessionID: string + } + query?: { + limit?: number + order?: "asc" | "desc" + /** + * Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. Do not combine with order. + */ + cursor?: string + } + url: "/api/session/{sessionID}/message" +} + +export type V2SessionMessagesErrors = { + /** + * InvalidCursorError | InvalidRequestError + */ + 400: InvalidCursorError | InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * SessionNotFoundError + */ + 404: SessionNotFoundError + /** + * UnknownError + */ + 500: UnknownError1 +} + +export type V2SessionMessagesError = V2SessionMessagesErrors[keyof V2SessionMessagesErrors] + +export type V2SessionMessagesResponses = { + /** + * SessionMessagesResponse + */ + 200: SessionMessagesResponse +} + +export type V2SessionMessagesResponse = V2SessionMessagesResponses[keyof V2SessionMessagesResponses] + +export type V2ModelListData = { + body?: never + path?: never + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/api/model" +} + +export type V2ModelListErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * ServiceUnavailableError + */ + 503: ServiceUnavailableError +} + +export type V2ModelListError = V2ModelListErrors[keyof V2ModelListErrors] + +export type V2ModelListResponses = { + /** + * Success + */ + 200: { + location: LocationInfo + data: Array + } +} + +export type V2ModelListResponse = V2ModelListResponses[keyof V2ModelListResponses] + +export type V2ProviderListData = { + body?: never + path?: never + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/api/provider" +} + +export type V2ProviderListErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * ServiceUnavailableError + */ + 503: ServiceUnavailableError +} + +export type V2ProviderListError = V2ProviderListErrors[keyof V2ProviderListErrors] + +export type V2ProviderListResponses = { + /** + * Success + */ + 200: { + location: LocationInfo + data: Array + } +} + +export type V2ProviderListResponse = V2ProviderListResponses[keyof V2ProviderListResponses] + +export type V2ProviderGetData = { + body?: never + path: { + providerID: string + } + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/api/provider/{providerID}" +} + +export type V2ProviderGetErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * ProviderNotFoundError + */ + 404: ProviderNotFoundError + /** + * ServiceUnavailableError + */ + 503: ServiceUnavailableError +} + +export type V2ProviderGetError = V2ProviderGetErrors[keyof V2ProviderGetErrors] + +export type V2ProviderGetResponses = { + /** + * Success + */ + 200: { + location: LocationInfo + data: ProviderV2Info + } +} + +export type V2ProviderGetResponse = V2ProviderGetResponses[keyof V2ProviderGetResponses] + +export type V2IntegrationListData = { + body?: never + path?: never + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/api/integration" +} + +export type V2IntegrationListErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2IntegrationListError = V2IntegrationListErrors[keyof V2IntegrationListErrors] + +export type V2IntegrationListResponses = { + /** + * Success + */ + 200: { + location: LocationInfo + data: Array + } +} + +export type V2IntegrationListResponse = V2IntegrationListResponses[keyof V2IntegrationListResponses] + +export type V2IntegrationGetData = { + body?: never + path: { + integrationID: string + } + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/api/integration/{integrationID}" +} + +export type V2IntegrationGetErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2IntegrationGetError = V2IntegrationGetErrors[keyof V2IntegrationGetErrors] + +export type V2IntegrationGetResponses = { + /** + * Success + */ + 200: { + location: LocationInfo + data: IntegrationInfo + } +} + +export type V2IntegrationGetResponse = V2IntegrationGetResponses[keyof V2IntegrationGetResponses] + +export type V2IntegrationConnectKeyData = { + body: { + key: string + label?: string + } + path: { + integrationID: string + } + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/api/integration/{integrationID}/connect/key" +} + +export type V2IntegrationConnectKeyErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2IntegrationConnectKeyError = V2IntegrationConnectKeyErrors[keyof V2IntegrationConnectKeyErrors] + +export type V2IntegrationConnectKeyResponses = { + /** + * + */ + 204: void +} + +export type V2IntegrationConnectKeyResponse = V2IntegrationConnectKeyResponses[keyof V2IntegrationConnectKeyResponses] + +export type V2IntegrationConnectOauthData = { + body: { + methodID: string + inputs: { + [key: string]: string + } + label?: string + } + path: { + integrationID: string + } + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/api/integration/{integrationID}/connect/oauth" +} + +export type V2IntegrationConnectOauthErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2IntegrationConnectOauthError = V2IntegrationConnectOauthErrors[keyof V2IntegrationConnectOauthErrors] + +export type V2IntegrationConnectOauthResponses = { + /** + * Success + */ + 200: { + location: LocationInfo + data: IntegrationAttempt + } +} + +export type V2IntegrationConnectOauthResponse = + V2IntegrationConnectOauthResponses[keyof V2IntegrationConnectOauthResponses] + +export type V2IntegrationAttemptCancelData = { + body?: never + path: { + attemptID: string + } + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/api/integration/attempt/{attemptID}" +} + +export type V2IntegrationAttemptCancelErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2IntegrationAttemptCancelError = V2IntegrationAttemptCancelErrors[keyof V2IntegrationAttemptCancelErrors] + +export type V2IntegrationAttemptCancelResponses = { + /** + * + */ + 204: void +} + +export type V2IntegrationAttemptCancelResponse = + V2IntegrationAttemptCancelResponses[keyof V2IntegrationAttemptCancelResponses] + +export type V2IntegrationAttemptStatusData = { + body?: never + path: { + attemptID: string + } + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/api/integration/attempt/{attemptID}" +} + +export type V2IntegrationAttemptStatusErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2IntegrationAttemptStatusError = V2IntegrationAttemptStatusErrors[keyof V2IntegrationAttemptStatusErrors] + +export type V2IntegrationAttemptStatusResponses = { + /** + * Success + */ + 200: { + location: LocationInfo + data: IntegrationAttemptStatus + } +} + +export type V2IntegrationAttemptStatusResponse = + V2IntegrationAttemptStatusResponses[keyof V2IntegrationAttemptStatusResponses] + +export type V2IntegrationAttemptCompleteData = { + body: { + code?: string + } + path: { + attemptID: string + } + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/api/integration/attempt/{attemptID}/complete" +} + +export type V2IntegrationAttemptCompleteErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2IntegrationAttemptCompleteError = + V2IntegrationAttemptCompleteErrors[keyof V2IntegrationAttemptCompleteErrors] + +export type V2IntegrationAttemptCompleteResponses = { + /** + * + */ + 204: void +} + +export type V2IntegrationAttemptCompleteResponse = + V2IntegrationAttemptCompleteResponses[keyof V2IntegrationAttemptCompleteResponses] + +export type V2CredentialRemoveData = { + body?: never + path: { + credentialID: string + } + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/api/credential/{credentialID}" +} + +export type V2CredentialRemoveErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2CredentialRemoveError = V2CredentialRemoveErrors[keyof V2CredentialRemoveErrors] + +export type V2CredentialRemoveResponses = { + /** + * + */ + 204: void +} + +export type V2CredentialRemoveResponse = V2CredentialRemoveResponses[keyof V2CredentialRemoveResponses] + +export type V2CredentialUpdateData = { + body: { + label: string + } + path: { + credentialID: string + } + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/api/credential/{credentialID}" +} + +export type V2CredentialUpdateErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2CredentialUpdateError = V2CredentialUpdateErrors[keyof V2CredentialUpdateErrors] + +export type V2CredentialUpdateResponses = { + /** + * + */ + 204: void +} + +export type V2CredentialUpdateResponse = V2CredentialUpdateResponses[keyof V2CredentialUpdateResponses] + +export type V2PermissionRequestListData = { + body?: never + path?: never + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/api/permission/request" +} + +export type V2PermissionRequestListErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2PermissionRequestListError = V2PermissionRequestListErrors[keyof V2PermissionRequestListErrors] + +export type V2PermissionRequestListResponses = { + /** + * Success + */ + 200: { + location: LocationInfo + data: Array + } +} + +export type V2PermissionRequestListResponse = V2PermissionRequestListResponses[keyof V2PermissionRequestListResponses] + +export type V2PermissionSavedListData = { + body?: never + path?: never + query?: { + projectID?: string + } + url: "/api/permission/saved" +} + +export type V2PermissionSavedListErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2PermissionSavedListError = V2PermissionSavedListErrors[keyof V2PermissionSavedListErrors] + +export type V2PermissionSavedListResponses = { + /** + * Success + */ + 200: { + data: Array + } +} + +export type V2PermissionSavedListResponse = V2PermissionSavedListResponses[keyof V2PermissionSavedListResponses] + +export type V2PermissionSavedRemoveData = { + body?: never + path: { + id: string + } + query?: never + url: "/api/permission/saved/{id}" +} + +export type V2PermissionSavedRemoveErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2PermissionSavedRemoveError = V2PermissionSavedRemoveErrors[keyof V2PermissionSavedRemoveErrors] + +export type V2PermissionSavedRemoveResponses = { + /** + * + */ + 204: void +} + +export type V2PermissionSavedRemoveResponse = V2PermissionSavedRemoveResponses[keyof V2PermissionSavedRemoveResponses] + +export type V2SessionPermissionListData = { + body?: never + path: { + sessionID: string + } + query?: never + url: "/api/session/{sessionID}/permission" +} + +export type V2SessionPermissionListErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * SessionNotFoundError + */ + 404: SessionNotFoundError +} + +export type V2SessionPermissionListError = V2SessionPermissionListErrors[keyof V2SessionPermissionListErrors] + +export type V2SessionPermissionListResponses = { + /** + * Success + */ + 200: { + data: Array + } +} + +export type V2SessionPermissionListResponse = V2SessionPermissionListResponses[keyof V2SessionPermissionListResponses] + +export type V2SessionPermissionCreateData = { + body: { + id?: string + action: string + resources: Array + save?: Array + metadata?: { + [key: string]: unknown + } + source?: PermissionV2Source + agent?: string + } + path: { + sessionID: string + } + query?: never + url: "/api/session/{sessionID}/permission" +} + +export type V2SessionPermissionCreateErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * SessionNotFoundError + */ + 404: SessionNotFoundError +} + +export type V2SessionPermissionCreateError = V2SessionPermissionCreateErrors[keyof V2SessionPermissionCreateErrors] + +export type V2SessionPermissionCreateResponses = { + /** + * Success + */ + 200: { + data: { + id: string + effect: PermissionV2Effect + } + } +} + +export type V2SessionPermissionCreateResponse = + V2SessionPermissionCreateResponses[keyof V2SessionPermissionCreateResponses] + +export type V2SessionPermissionGetData = { + body?: never + path: { + sessionID: string + requestID: string + } + query?: never + url: "/api/session/{sessionID}/permission/{requestID}" +} + +export type V2SessionPermissionGetErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * SessionNotFoundError | PermissionNotFoundError + */ + 404: PermissionNotFoundError | SessionNotFoundError +} + +export type V2SessionPermissionGetError = V2SessionPermissionGetErrors[keyof V2SessionPermissionGetErrors] + +export type V2SessionPermissionGetResponses = { + /** + * Success + */ + 200: { + data: PermissionV2Request + } +} + +export type V2SessionPermissionGetResponse = V2SessionPermissionGetResponses[keyof V2SessionPermissionGetResponses] + +export type V2SessionPermissionReplyData = { + body: { + reply: PermissionV2Reply + message?: string + } + path: { + sessionID: string + requestID: string + } + query?: never + url: "/api/session/{sessionID}/permission/{requestID}/reply" +} + +export type V2SessionPermissionReplyErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * SessionNotFoundError | PermissionNotFoundError + */ + 404: PermissionNotFoundError | SessionNotFoundError +} + +export type V2SessionPermissionReplyError = V2SessionPermissionReplyErrors[keyof V2SessionPermissionReplyErrors] + +export type V2SessionPermissionReplyResponses = { + /** + * + */ + 204: void +} + +export type V2SessionPermissionReplyResponse = + V2SessionPermissionReplyResponses[keyof V2SessionPermissionReplyResponses] + +export type V2FsReadData = { + body?: never + path?: never + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/api/fs/read/*" +} + +export type V2FsReadErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2FsReadError = V2FsReadErrors[keyof V2FsReadErrors] + +export type V2FsReadResponses = { + /** + * Success + */ + 200: Blob | File +} + +export type V2FsReadResponse = V2FsReadResponses[keyof V2FsReadResponses] + +export type V2FsListData = { + body?: never + path?: never + query?: { + location?: { + directory?: string + workspace?: string + } + path?: string + } + url: "/api/fs/list" +} + +export type V2FsListErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2FsListError = V2FsListErrors[keyof V2FsListErrors] + +export type V2FsListResponses = { + /** + * Success + */ + 200: { + location: LocationInfo + data: Array + } +} + +export type V2FsListResponse = V2FsListResponses[keyof V2FsListResponses] + +export type V2FsFindData = { + body?: never + path?: never + query: { + location?: { + directory?: string + workspace?: string + } + query: string + type?: "file" | "directory" + limit?: string + } + url: "/api/fs/find" +} + +export type V2FsFindErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2FsFindError = V2FsFindErrors[keyof V2FsFindErrors] + +export type V2FsFindResponses = { + /** + * Success + */ + 200: { + location: LocationInfo + data: Array + } +} + +export type V2FsFindResponse = V2FsFindResponses[keyof V2FsFindResponses] + +export type V2CommandListData = { + body?: never + path?: never + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/api/command" +} + +export type V2CommandListErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2CommandListError = V2CommandListErrors[keyof V2CommandListErrors] + +export type V2CommandListResponses = { + /** + * Success + */ + 200: { + location: LocationInfo + data: Array + } +} + +export type V2CommandListResponse = V2CommandListResponses[keyof V2CommandListResponses] + +export type V2SkillListData = { + body?: never + path?: never + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/api/skill" +} + +export type V2SkillListErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2SkillListError = V2SkillListErrors[keyof V2SkillListErrors] + +export type V2SkillListResponses = { + /** + * Success + */ + 200: { + location: LocationInfo + data: Array + } +} + +export type V2SkillListResponse = V2SkillListResponses[keyof V2SkillListResponses] + +export type V2EventSubscribeData = { + body?: never + path?: never + query?: never + url: "/api/event" +} + +export type V2EventSubscribeErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2EventSubscribeError = V2EventSubscribeErrors[keyof V2EventSubscribeErrors] + +export type V2EventSubscribeResponses = { + /** + * Event stream + */ + 200: V2Event +} + +export type V2EventSubscribeResponse = V2EventSubscribeResponses[keyof V2EventSubscribeResponses] + +export type V2PtyListData = { + body?: never + path?: never + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/api/pty" +} + +export type V2PtyListErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2PtyListError = V2PtyListErrors[keyof V2PtyListErrors] + +export type V2PtyListResponses = { + /** + * Success + */ + 200: { + location: LocationInfo + data: Array + } +} + +export type V2PtyListResponse = V2PtyListResponses[keyof V2PtyListResponses] + +export type V2PtyCreateData = { + body: { + command?: string + args?: Array + cwd?: string + title?: string + env?: { + [key: string]: string + } + } + path?: never + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/api/pty" +} + +export type V2PtyCreateErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2PtyCreateError = V2PtyCreateErrors[keyof V2PtyCreateErrors] + +export type V2PtyCreateResponses = { + /** + * Success + */ + 200: { + location: LocationInfo + data: Pty + } +} + +export type V2PtyCreateResponse = V2PtyCreateResponses[keyof V2PtyCreateResponses] + +export type V2PtyRemoveData = { + body?: never + path: { + ptyID: string + } + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/api/pty/{ptyID}" +} + +export type V2PtyRemoveErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * PtyNotFoundError + */ + 404: PtyNotFoundError +} + +export type V2PtyRemoveError = V2PtyRemoveErrors[keyof V2PtyRemoveErrors] + +export type V2PtyRemoveResponses = { + /** + * + */ + 204: void +} + +export type V2PtyRemoveResponse = V2PtyRemoveResponses[keyof V2PtyRemoveResponses] + +export type V2PtyGetData = { + body?: never + path: { + ptyID: string + } + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/api/pty/{ptyID}" +} + +export type V2PtyGetErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * PtyNotFoundError + */ + 404: PtyNotFoundError +} + +export type V2PtyGetError = V2PtyGetErrors[keyof V2PtyGetErrors] + +export type V2PtyGetResponses = { + /** + * Success + */ + 200: { + location: LocationInfo + data: Pty + } +} + +export type V2PtyGetResponse = V2PtyGetResponses[keyof V2PtyGetResponses] + +export type V2PtyUpdateData = { + body: { + title?: string + size?: { + rows: number + cols: number + } + } + path: { + ptyID: string + } + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/api/pty/{ptyID}" +} + +export type V2PtyUpdateErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * PtyNotFoundError + */ + 404: PtyNotFoundError +} + +export type V2PtyUpdateError = V2PtyUpdateErrors[keyof V2PtyUpdateErrors] + +export type V2PtyUpdateResponses = { + /** + * Success + */ + 200: { + location: LocationInfo + data: Pty + } +} + +export type V2PtyUpdateResponse = V2PtyUpdateResponses[keyof V2PtyUpdateResponses] + +export type V2PtyConnectTokenData = { + body?: never + path: { + ptyID: string + } + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/api/pty/{ptyID}/connect-token" +} + +export type V2PtyConnectTokenErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * ForbiddenError + */ + 403: ForbiddenError + /** + * PtyNotFoundError + */ + 404: PtyNotFoundError +} + +export type V2PtyConnectTokenError = V2PtyConnectTokenErrors[keyof V2PtyConnectTokenErrors] + +export type V2PtyConnectTokenResponses = { + /** + * Success + */ + 200: { + location: LocationInfo + data: PtyTicketConnectToken + } +} + +export type V2PtyConnectTokenResponse = V2PtyConnectTokenResponses[keyof V2PtyConnectTokenResponses] + +export type V2PtyConnectData = { + body?: never + path: { + ptyID: string + } + query?: { + "location[directory]"?: string + "location[workspace]"?: string + cursor?: string + ticket?: string + } + url: "/api/pty/{ptyID}/connect" +} + +export type V2PtyConnectErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * ForbiddenError + */ + 403: ForbiddenError + /** + * PtyNotFoundError + */ + 404: PtyNotFoundError +} + +export type V2PtyConnectError = V2PtyConnectErrors[keyof V2PtyConnectErrors] + +export type V2PtyConnectResponses = { + /** + * Success + */ + 200: boolean +} + +export type V2PtyConnectResponse = V2PtyConnectResponses[keyof V2PtyConnectResponses] + +export type V2QuestionRequestListData = { + body?: never + path?: never + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/api/question/request" +} + +export type V2QuestionRequestListErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2QuestionRequestListError = V2QuestionRequestListErrors[keyof V2QuestionRequestListErrors] + +export type V2QuestionRequestListResponses = { + /** + * Success + */ + 200: { + location: LocationInfo + data: Array + } +} + +export type V2QuestionRequestListResponse = V2QuestionRequestListResponses[keyof V2QuestionRequestListResponses] + +export type V2SessionQuestionListData = { + body?: never + path: { + sessionID: string + } + query?: never + url: "/api/session/{sessionID}/question" +} + +export type V2SessionQuestionListErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * SessionNotFoundError + */ + 404: SessionNotFoundError +} + +export type V2SessionQuestionListError = V2SessionQuestionListErrors[keyof V2SessionQuestionListErrors] + +export type V2SessionQuestionListResponses = { + /** + * Success + */ + 200: { + data: Array + } +} + +export type V2SessionQuestionListResponse = V2SessionQuestionListResponses[keyof V2SessionQuestionListResponses] + +export type V2SessionQuestionReplyData = { + body: QuestionV2Reply + path: { + sessionID: string + requestID: string + } + query?: never + url: "/api/session/{sessionID}/question/{requestID}/reply" +} + +export type V2SessionQuestionReplyErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * SessionNotFoundError | QuestionNotFoundError + */ + 404: QuestionNotFoundError | SessionNotFoundError +} + +export type V2SessionQuestionReplyError = V2SessionQuestionReplyErrors[keyof V2SessionQuestionReplyErrors] + +export type V2SessionQuestionReplyResponses = { + /** + * + */ + 204: void +} + +export type V2SessionQuestionReplyResponse = V2SessionQuestionReplyResponses[keyof V2SessionQuestionReplyResponses] + +export type V2SessionQuestionRejectData = { + body?: never + path: { + sessionID: string + requestID: string + } + query?: never + url: "/api/session/{sessionID}/question/{requestID}/reject" +} + +export type V2SessionQuestionRejectErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * SessionNotFoundError | QuestionNotFoundError + */ + 404: QuestionNotFoundError | SessionNotFoundError +} + +export type V2SessionQuestionRejectError = V2SessionQuestionRejectErrors[keyof V2SessionQuestionRejectErrors] + +export type V2SessionQuestionRejectResponses = { + /** + * + */ + 204: void +} + +export type V2SessionQuestionRejectResponse = V2SessionQuestionRejectResponses[keyof V2SessionQuestionRejectResponses] + +export type V2ReferenceListData = { + body?: never + path?: never + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/api/reference" +} + +export type V2ReferenceListErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2ReferenceListError = V2ReferenceListErrors[keyof V2ReferenceListErrors] + +export type V2ReferenceListResponses = { + /** + * Success + */ + 200: { + location: LocationInfo + data: Array + } +} + +export type V2ReferenceListResponse = V2ReferenceListResponses[keyof V2ReferenceListResponses] + +export type V2ProjectCopyRemoveData = { + body?: { + directory: string + force: boolean + } + path: { + projectID: string + } + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/experimental/project/{projectID}/copy" +} + +export type V2ProjectCopyRemoveErrors = { + /** + * ProjectCopyError | InvalidRequestError + */ + 400: ProjectCopyError | InvalidRequestError +} + +export type V2ProjectCopyRemoveError = V2ProjectCopyRemoveErrors[keyof V2ProjectCopyRemoveErrors] + +export type V2ProjectCopyRemoveResponses = { + /** + * + */ + 204: void +} + +export type V2ProjectCopyRemoveResponse = V2ProjectCopyRemoveResponses[keyof V2ProjectCopyRemoveResponses] + +export type V2ProjectCopyCreateData = { + body?: { + strategy: string + directory: string + name?: string + } + path: { + projectID: string + } + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/experimental/project/{projectID}/copy" +} + +export type V2ProjectCopyCreateErrors = { + /** + * ProjectCopyError | InvalidRequestError + */ + 400: ProjectCopyError | InvalidRequestError +} + +export type V2ProjectCopyCreateError = V2ProjectCopyCreateErrors[keyof V2ProjectCopyCreateErrors] + +export type V2ProjectCopyCreateResponses = { + /** + * ProjectCopy.Copy + */ + 200: ProjectCopyCopy +} + +export type V2ProjectCopyCreateResponse = V2ProjectCopyCreateResponses[keyof V2ProjectCopyCreateResponses] + +export type V2ProjectCopyRefreshData = { + body?: never + path: { + projectID: string + } + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/experimental/project/{projectID}/copy/refresh" +} + +export type V2ProjectCopyRefreshErrors = { + /** + * ProjectCopyError | InvalidRequestError + */ + 400: ProjectCopyError | InvalidRequestError +} + +export type V2ProjectCopyRefreshError = V2ProjectCopyRefreshErrors[keyof V2ProjectCopyRefreshErrors] + +export type V2ProjectCopyRefreshResponses = { + /** + * + */ + 204: void +} + +export type V2ProjectCopyRefreshResponse = V2ProjectCopyRefreshResponses[keyof V2ProjectCopyRefreshResponses] + +export type PtyConnectData = { + body?: never + path: { + ptyID: string + } + query?: { + directory?: string + workspace?: string + cursor?: string + ticket?: string + } + url: "/pty/{ptyID}/connect" +} + +export type PtyConnectErrors = { + /** + * Forbidden + */ + 403: EffectHttpApiErrorForbidden + /** + * Not found + */ + 404: NotFoundError +} + +export type PtyConnectError = PtyConnectErrors[keyof PtyConnectErrors] + +export type PtyConnectResponses = { + /** + * Connected session + */ + 200: boolean +} + +export type PtyConnectResponse = PtyConnectResponses[keyof PtyConnectResponses] diff --git a/packages/app-bundle/overlay/packages/session-ui/package.json b/packages/app-bundle/overlay/packages/session-ui/package.json new file mode 100644 index 00000000..6b2f78a5 --- /dev/null +++ b/packages/app-bundle/overlay/packages/session-ui/package.json @@ -0,0 +1,72 @@ +{ + "name": "@opencode-ai/session-ui", + "version": "1.18.10", + "private": true, + "type": "module", + "license": "MIT", + "exports": { + "./*": "./src/components/*.tsx", + "./session-diff": "./src/components/session-diff.ts", + "./message-file": "./src/components/message-file.ts", + "./message-part-text": "./src/components/message-part-text.ts", + "./markdown-stream": "./src/components/markdown-stream.ts", + "./markdown-cache": "./src/components/markdown-cache.tsx", + "./markdown-file-refs": "./src/components/markdown-file-refs.ts", + "./line-comment-styles": "./src/components/line-comment-styles.ts", + "./pierre": "./src/pierre/index.ts", + "./pierre/*": "./src/pierre/*.ts", + "./context": "./src/context/index.ts", + "./context/*": "./src/context/*.tsx", + "./styles": "./src/styles/index.css", + "./v2/*.css": "./src/v2/components/*.css", + "./v2/*": "./src/v2/components/*.tsx", + "./v2/prompt-input": "./src/v2/components/prompt-input/index.tsx", + "./v2/prompt-input/interaction": "./src/v2/components/prompt-input/interaction.ts", + "./v2/prompt-input/store": "./src/v2/components/prompt-input/store.ts", + "./v2/prompt-input/types": "./src/v2/components/prompt-input/types.ts" + }, + "scripts": { + "typecheck": "tsgo --noEmit", + "test": "bun test src --only-failures" + }, + "devDependencies": { + "@tsconfig/node22": "catalog:", + "@types/bun": "catalog:", + "@types/katex": "0.16.7", + "@types/luxon": "catalog:", + "@typescript/native-preview": "catalog:", + "typescript": "catalog:", + "vite": "catalog:" + }, + "dependencies": { + "@kobalte/core": "catalog:", + "@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz", + "@opencode-ai/core": "workspace:*", + "@opencode-ai/sdk": "workspace:*", + "@opencode-ai/ui": "workspace:*", + "@pierre/diffs": "catalog:", + "@shikijs/stream": "catalog:", + "@shikijs/transformers": "3.9.2", + "@solid-primitives/bounds": "0.1.3", + "@solid-primitives/event-listener": "2.4.5", + "@solid-primitives/media": "2.3.3", + "@solid-primitives/resize-observer": "2.1.3", + "@solidjs/meta": "catalog:", + "diff": "catalog:", + "dompurify": "3.3.1", + "fuzzysort": "catalog:", + "katex": "0.16.27", + "luxon": "catalog:", + "marked": "catalog:", + "marked-katex-extension": "5.1.6", + "marked-shiki": "catalog:", + "morphdom": "2.7.8", + "motion": "12.34.5", + "remeda": "catalog:", + "remend": "catalog:", + "shiki": "catalog:", + "solid-js": "catalog:", + "solid-list": "catalog:", + "strip-ansi": "7.1.2" + } +} diff --git a/packages/app-bundle/overlay/packages/session-ui/src/components/group-parts.test.ts b/packages/app-bundle/overlay/packages/session-ui/src/components/group-parts.test.ts new file mode 100644 index 00000000..2febdcaa --- /dev/null +++ b/packages/app-bundle/overlay/packages/session-ui/src/components/group-parts.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, test } from "bun:test" +import { groupParts } from "./message-part-groups" + +// Minimal PartType-shaped fixtures — groupParts only reads type/tool/id. +const tool = (id: string, name: string) => ({ messageID: "m1", part: { type: "tool", tool: name, id } as any }) +const text = (id: string) => ({ messageID: "m1", part: { type: "text", id, text: "hi" } as any }) + +describe("groupParts — shell grouping (spec B)", () => { + test("≥2 consecutive bash collapse into one shell group", () => { + const groups = groupParts([tool("a", "bash"), tool("b", "bash"), tool("c", "bash")]) + expect(groups).toHaveLength(1) + expect(groups[0]!.type).toBe("shell") + expect(groups[0]!.type === "shell" && groups[0]!.refs.map((r) => r.partID)).toEqual(["a", "b", "c"]) + }) + + test("a lone bash stays a full part (never a 1-item shell group)", () => { + const groups = groupParts([text("t"), tool("a", "bash"), text("u")]) + expect(groups.map((g) => g.type)).toEqual(["part", "part", "part"]) + }) + + test("non-adjacent bash are not grouped", () => { + const groups = groupParts([tool("a", "bash"), text("t"), tool("b", "bash")]) + expect(groups.map((g) => g.type)).toEqual(["part", "part", "part"]) + }) + + test("context and shell runs stay distinct and adjacent-flush cleanly", () => { + const groups = groupParts([tool("r1", "read"), tool("r2", "grep"), tool("b1", "bash"), tool("b2", "bash")]) + expect(groups.map((g) => g.type)).toEqual(["context", "shell"]) + expect(groups[0]!.type === "context" && groups[0]!.refs).toHaveLength(2) + expect(groups[1]!.type === "shell" && groups[1]!.refs).toHaveLength(2) + }) + + test("shell then context: order + boundaries preserved", () => { + const groups = groupParts([tool("b1", "bash"), tool("b2", "bash"), tool("r1", "list")]) + expect(groups.map((g) => g.type)).toEqual(["shell", "context"]) + }) + + test("≥2 consecutive file mutations collapse into one edit group", () => { + const groups = groupParts([tool("a", "edit"), tool("b", "write"), tool("c", "edit")]) + expect(groups).toHaveLength(1) + expect(groups[0]!.type).toBe("edit") + expect(groups[0]!.type === "edit" && groups[0]!.refs.map((r) => r.partID)).toEqual(["a", "b", "c"]) + }) + + test("a lone edit stays a full part (its inline diff is worth the card)", () => { + const groups = groupParts([text("t"), tool("a", "edit"), text("u")]) + expect(groups.map((g) => g.type)).toEqual(["part", "part", "part"]) + }) + + test("non-adjacent edits are not grouped", () => { + const groups = groupParts([tool("a", "edit"), text("t"), tool("b", "write")]) + expect(groups.map((g) => g.type)).toEqual(["part", "part", "part"]) + }) + + test("patch and apply_patch join the edit run; shell and edit runs stay distinct", () => { + const groups = groupParts([ + tool("e1", "edit"), + tool("e2", "apply_patch"), + tool("b1", "bash"), + tool("b2", "bash"), + tool("e3", "write"), + tool("e4", "edit"), + ]) + expect(groups.map((g) => g.type)).toEqual(["edit", "shell", "edit"]) + expect(groups[0]!.type === "edit" && groups[0]!.refs).toHaveLength(2) + expect(groups[1]!.type === "shell" && groups[1]!.refs).toHaveLength(2) + expect(groups[2]!.type === "edit" && groups[2]!.refs).toHaveLength(2) + }) + + test("edit then context: order + boundaries preserved", () => { + const groups = groupParts([tool("e1", "write"), tool("e2", "write"), tool("r1", "read")]) + expect(groups.map((g) => g.type)).toEqual(["edit", "context"]) + }) +}) diff --git a/packages/app-bundle/overlay/packages/session-ui/src/components/markdown-file-refs.test.ts b/packages/app-bundle/overlay/packages/session-ui/src/components/markdown-file-refs.test.ts new file mode 100644 index 00000000..a733980d --- /dev/null +++ b/packages/app-bundle/overlay/packages/session-ui/src/components/markdown-file-refs.test.ts @@ -0,0 +1,68 @@ +import { beforeEach, describe, expect, test } from "bun:test" +import { + cachedFileRef, + clearFileRefCache, + fileRefResolver, + fileRefUrl, + registerFileRefResolver, + resolveFileRefCached, +} from "./markdown-file-refs" + +beforeEach(() => { + registerFileRefResolver(undefined) + clearFileRefCache() +}) + +describe("resolver registry", () => { + test("no resolver registered → resolves to null, uncached", async () => { + expect(fileRefResolver()).toBeUndefined() + expect(await resolveFileRefCached("foo.md")).toBeNull() + expect(cachedFileRef("foo.md")).toBeUndefined() + }) + + test("resolution results are cached, hits and misses alike", async () => { + let calls = 0 + registerFileRefResolver(async (text) => { + calls++ + return text.startsWith("real") ? `/abs/${text}` : null + }) + expect(await resolveFileRefCached("real.md")).toBe("/abs/real.md") + expect(await resolveFileRefCached("ghost.md")).toBeNull() + expect(await resolveFileRefCached("real.md")).toBe("/abs/real.md") + expect(cachedFileRef("real.md")).toBe("/abs/real.md") + expect(cachedFileRef("ghost.md")).toBeNull() + expect(calls).toBe(2) // second real.md call came from cache + }) + + test("in-flight resolutions dedupe", async () => { + let calls = 0 + registerFileRefResolver(async (text) => { + calls++ + await new Promise((r) => setTimeout(r, 10)) + return `/abs/${text}` + }) + const [a, b] = await Promise.all([resolveFileRefCached("x.md"), resolveFileRefCached("x.md")]) + expect(a).toBe("/abs/x.md") + expect(b).toBe("/abs/x.md") + expect(calls).toBe(1) + }) + + test("resolver failures stay UNCACHED so the next render retries", async () => { + let calls = 0 + registerFileRefResolver(async () => { + calls++ + if (calls === 1) throw new Error("server down") + return "/abs/recovered.md" + }) + expect(await resolveFileRefCached("recovered.md")).toBeNull() + expect(cachedFileRef("recovered.md")).toBeUndefined() + expect(await resolveFileRefCached("recovered.md")).toBe("/abs/recovered.md") + expect(calls).toBe(2) + }) +}) + +describe("fileRefUrl", () => { + test("posix absolute paths encode spaces, keep slashes", () => { + expect(fileRefUrl("/Users/aaron/My Notes/insight-1.md")).toBe("file:///Users/aaron/My%20Notes/insight-1.md") + }) +}) diff --git a/packages/app-bundle/overlay/packages/session-ui/src/components/markdown-file-refs.ts b/packages/app-bundle/overlay/packages/session-ui/src/components/markdown-file-refs.ts new file mode 100644 index 00000000..327190e8 --- /dev/null +++ b/packages/app-bundle/overlay/packages/session-ui/src/components/markdown-file-refs.ts @@ -0,0 +1,69 @@ +// AMICODE: file-reference linkification for chat markdown. +// The app registers a resolver (it owns the server connection + auth); the +// Markdown component then wraps path-like inline-code pills and relative +// authored links in file:// anchors that the VS Code bridge opens in the +// editor. session-ui stays server-agnostic: with no resolver registered +// (plain browser embed, stories, tests) nothing linkifies and pills stay pills. + +export type FileRefResolver = (text: string) => Promise + +let resolver: FileRefResolver | undefined + +/** App-side registration — called once at shell mount, `undefined` on + * unmount. The resolver must resolve its server connection PER CALL (a server + * switch is picked up by the very next resolution). */ +export function registerFileRefResolver(next: FileRefResolver | undefined): void { + resolver = next +} + +export function fileRefResolver(): FileRefResolver | undefined { + return resolver +} + +// text → absolute path (null = resolved-to-nothing). Bounded FIFO: pill text +// is unbounded agent/user prose, so the cache must not grow with the session. +const CACHE_MAX = 500 +const cache = new Map() +const inflight = new Map>() + +/** undefined = not yet resolved; null = known-unresolvable; string = absolute path. */ +export function cachedFileRef(text: string): string | null | undefined { + return cache.get(text) +} + +/** Resolve once, dedupe in-flight, cache definitive answers. Transient + * failures (server down mid-fetch) stay UNCACHED so the next render retries. */ +export function resolveFileRefCached(text: string): Promise { + const hit = cache.get(text) + if (hit !== undefined) return Promise.resolve(hit) + const pending = inflight.get(text) + if (pending) return pending + const resolve = resolver + if (!resolve) return Promise.resolve(null) + const p = resolve(text) + .then((abs) => { + if (cache.size >= CACHE_MAX) { + const oldest = cache.keys().next() + if (!oldest.done) cache.delete(oldest.value) + } + cache.set(text, abs) + return abs + }) + .catch(() => null) + .finally(() => inflight.delete(text)) + inflight.set(text, p) + return p +} + +/** The bridge (extension chat_bridge.ts) decodes via new URL(...).pathname + + * decodeURIComponent, so percent-encode here; posix-only fleet, keep the + * slashes readable. */ +export function fileRefUrl(abs: string): string { + return "file://" + encodeURI(abs) +} + +/** Test hook. */ +export function clearFileRefCache(): void { + cache.clear() + inflight.clear() +} diff --git a/packages/app-bundle/overlay/packages/session-ui/src/components/markdown-stream.test.ts b/packages/app-bundle/overlay/packages/session-ui/src/components/markdown-stream.test.ts new file mode 100644 index 00000000..66119da8 --- /dev/null +++ b/packages/app-bundle/overlay/packages/session-ui/src/components/markdown-stream.test.ts @@ -0,0 +1,214 @@ +import { describe, expect, test } from "bun:test" +import { canReusePendingBlock, normalizeDisplayMath, project, stream } from "./markdown-stream" + +describe("markdown stream", () => { + test("heals incomplete emphasis while streaming", () => { + expect(stream("hello **world", true)).toEqual([{ raw: "hello **world", src: "hello **world**", mode: "live" }]) + expect(stream("say `code", true)).toEqual([{ raw: "say `code", src: "say `code`", mode: "live" }]) + }) + + test("keeps incomplete links non-clickable until they finish", () => { + expect(stream("see [docs](https://example.com/gu", true)).toEqual([ + { raw: "see [docs](https://example.com/gu", src: "see docs", mode: "live" }, + ]) + }) + + test("splits an unfinished trailing code fence from stable content", () => { + expect(stream("before\n\n```ts\nconst x = 1", true)).toEqual([ + { raw: "before\n\n", src: "before\n\n", mode: "full" }, + { raw: "```ts\nconst x = 1", src: "const x = 1", mode: "code", language: "ts" }, + ]) + }) + + test("fully parses a code fence once it closes", () => { + const text = "before\n\n```ts\nconst x = 1\n```" + expect(stream(text, true)).toEqual([ + { raw: "before\n\n", src: "before\n\n", mode: "full" }, + { raw: "```ts\nconst x = 1\n```", src: "const x = 1", mode: "code", language: "ts", complete: true }, + ]) + }) + + test("keeps a completed code fence in worker-rendered code mode when prose follows", () => { + expect(stream("```ts\nconst x = 1\n```\n\nafter", true)).toEqual([ + { raw: "```ts\nconst x = 1\n```\n\n", src: "const x = 1", mode: "code", language: "ts", complete: true }, + { raw: "after", src: "after", mode: "live" }, + ]) + }) + + test("freezes completed top-level blocks and only keeps the tail live", () => { + expect(stream("# Plan\n\nFinished paragraph.\n\n- live item", true)).toEqual([ + { raw: "# Plan\n\n", src: "# Plan\n\n", mode: "full" }, + { raw: "Finished paragraph.\n\n", src: "Finished paragraph.\n\n", mode: "full" }, + { raw: "- live item", src: "- live item", mode: "live" }, + ]) + }) + + test("keeps a growing table together until a later block freezes it", () => { + expect(stream("| a | b |\n|---|---|\n| 1 | 2 |", true)).toEqual([ + { raw: "| a | b |\n|---|---|\n| 1 | 2 |", src: "| a | b |\n|---|---|\n| 1 | 2 |", mode: "live" }, + ]) + }) + + test("reprojects non-prefix replacements from current content", () => { + expect(stream("# Replacement\n\nNew body", true)).toEqual([ + { raw: "# Replacement\n\n", src: "# Replacement\n\n", mode: "full" }, + { raw: "New body", src: "New body", mode: "live" }, + ]) + }) + + test("reprojects truncation without retaining removed blocks", () => { + expect(stream("Only the restored prefix", true)).toEqual([ + { raw: "Only the restored prefix", src: "Only the restored prefix", mode: "live" }, + ]) + }) + + test("shifts later blocks when an earlier block is inserted", () => { + expect(stream("# Inserted\n\nFirst body\n\nSecond body", true)).toEqual([ + { raw: "# Inserted\n\n", src: "# Inserted\n\n", mode: "full" }, + { raw: "First body\n\n", src: "First body\n\n", mode: "full" }, + { raw: "Second body", src: "Second body", mode: "live" }, + ]) + }) + + test("keeps reference-style markdown as one block", () => { + expect(stream("[docs][1]\n\n[1]: https://example.com", true)).toEqual([ + { + raw: "[docs][1]\n\n[1]: https://example.com", + src: "[docs][1]\n\n[1]: https://example.com", + mode: "live", + }, + ]) + }) + + test("keeps compact and indented reference definitions with their uses", () => { + expect(stream("[docs]\n\n [docs]:/guide", true)).toEqual([ + { + raw: "[docs]\n\n [docs]:/guide", + src: "[docs]\n\n [docs]:/guide", + mode: "live", + }, + ]) + }) + + test("keeps multiline reference definitions with their uses", () => { + expect(stream("[docs][id]\n\n[id]:\n /guide", true)).toEqual([ + { + raw: "[docs][id]\n\n[id]:\n /guide", + src: "[docs][id]\n\n[id]:\n /guide", + mode: "live", + }, + ]) + }) + + test("uses only the language portion of fence metadata", () => { + expect(stream("```ts title=example\nconst x = 1", true)).toEqual([ + { + raw: "```ts title=example\nconst x = 1", + src: "const x = 1", + mode: "code", + language: "ts", + }, + ]) + }) + + test("preserves trailing newlines in open code fences", () => { + expect(stream("```ts\nconst x = 1\n", true)).toEqual([ + { + raw: "```ts\nconst x = 1\n", + src: "const x = 1\n", + mode: "code", + language: "ts", + }, + ]) + }) + + test("only reuses pending blocks with compatible identity and content", () => { + expect( + canReusePendingBlock({ mode: "full", raw: "First\n\n" }, { mode: "full", raw: "# Inserted\n\n", src: "" }), + ).toBe(false) + expect( + canReusePendingBlock({ mode: "code", raw: "```ts\none" }, { mode: "code", raw: "```ts\none two", src: "" }), + ).toBe(true) + expect(canReusePendingBlock({ mode: "code", raw: "```ts\none" }, { mode: "live", raw: "one", src: "" })).toBe(false) + }) + + test("appends plain code deltas without reprojecting frozen blocks", () => { + const previous = project(undefined, "# Plan\n\n```ts\nconst one = 1\n", true) + const next = project(previous, `${previous.text}const two = 2\n`, true) + + expect(next.blocks[0]).toBe(previous.blocks[0]) + expect(next.blocks.at(-1)).toEqual({ + raw: "```ts\nconst one = 1\nconst two = 2\n", + src: "const one = 1\nconst two = 2\n", + mode: "code", + language: "ts", + }) + }) + + test("does not add a blank line before the first streamed code", () => { + const previous = project(undefined, "```ts\n", true) + const next = project(previous, `${previous.text}const x = 1`, true) + + expect(next.blocks.at(-1)).toEqual({ + raw: "```ts\nconst x = 1", + src: "const x = 1", + mode: "code", + language: "ts", + }) + }) + + test("closes code fences split across provider deltas", () => { + const open = project(undefined, "```ts\nconst x = 1\n", true) + const one = project(open, `${open.text}\``, true) + const two = project(one, `${one.text}\``, true) + const closed = project(two, `${two.text}\``, true) + const prose = project(closed, `${closed.text}\nafter`, true) + + expect(closed.blocks.at(-1)).toEqual({ + raw: "```ts\nconst x = 1\n```", + src: "const x = 1", + mode: "code", + language: "ts", + complete: true, + }) + expect(prose.blocks).toEqual([ + { raw: "```ts\nconst x = 1\n```\n", src: "const x = 1", mode: "code", language: "ts", complete: true }, + { raw: "after", src: "after", mode: "live" }, + ]) + }) + + test("closes tilde fences split across provider deltas", () => { + const open = project(undefined, "~~~ts\nconst x = 1\n", true) + const one = project(open, `${open.text}~`, true) + const two = project(one, `${one.text}~`, true) + const closed = project(two, `${two.text}~`, true) + + expect(closed.blocks.at(-1)).toEqual({ + raw: "~~~ts\nconst x = 1\n~~~", + src: "const x = 1", + mode: "code", + language: "ts", + complete: true, + }) + }) +}) + +// amicode (ported in the 1.18.10 sync): mid-line-opened multi-line $$ display +// math — the shape LLMs actually emit, which neither the inline nor the strict +// block katex rule matches. Pure-function coverage of the normalizer; the +// render side is the block rule's own regex (covered by the markdown tests). +describe("normalizeDisplayMath", () => { + test("moves mid-line-opened, multi-line display math onto its own block lines", () => { + expect(normalizeDisplayMath("label: $$\n\\rho = 1\n$$")).toBe("label: \n\n$$\n\\rho = 1\n$$\n\n") + expect(normalizeDisplayMath("a: $$ x\n= y $$ b")).toBe("a: \n\n$$\nx\n= y\n$$\n\n b") + }) + + test("leaves $$ inside fenced or inline code untouched", () => { + expect(normalizeDisplayMath("```\n$$ not math $$\n```")).toBe("```\n$$ not math $$\n```") + expect(normalizeDisplayMath("use `$$ x $$` inline")).toBe("use `$$ x $$` inline") + }) + + test("leaves an unclosed trailing $$ (mid-stream) untouched", () => { + expect(normalizeDisplayMath("label: $$\n\\rho = 1")).toBe("label: $$\n\\rho = 1") + }) +}) diff --git a/packages/app-bundle/overlay/packages/session-ui/src/components/markdown-stream.ts b/packages/app-bundle/overlay/packages/session-ui/src/components/markdown-stream.ts new file mode 100644 index 00000000..3bc39ea5 --- /dev/null +++ b/packages/app-bundle/overlay/packages/session-ui/src/components/markdown-stream.ts @@ -0,0 +1,132 @@ +import { marked, type Tokens } from "marked" +import remend from "remend" + +export type Block = { + raw: string + src: string + mode: "full" | "live" | "code" + language?: string + complete?: boolean +} + +export type Projection = { + text: string + blocks: Block[] +} + +function refs(text: string) { + if (!text.includes("]:")) return false + return /^[ \t]{0,3}\[[^\]]+\]:[ \t]*(?:\S+|\r?\n[ \t]+\S+)/m.test(text) +} + +function language(value: string | undefined) { + return value?.trim().split(/\s+/, 1)[0] || undefined +} + +function openCode(raw: string) { + const newline = raw.indexOf("\n") + return newline < 0 ? "" : raw.slice(newline + 1) +} + +function open(raw: string) { + const match = raw.match(/^[ \t]{0,3}(`{3,}|~{3,})/) + if (!match) return false + const mark = match[1] + if (!mark) return false + const char = mark[0] + const size = mark.length + const last = raw.trimEnd().split("\n").at(-1)?.trim() ?? "" + return !new RegExp(`^[\\t ]{0,3}${char}{${size},}[\\t ]*$`).test(last) +} + +function closesFence(raw: string, suffix: string) { + const mark = raw.match(/^[ \t]{0,3}(`{3,}|~{3,})/)?.[1] + if (!mark) return suffix.includes("```") || suffix.includes("~~~") + return `${raw.slice(-(mark.length - 1))}${suffix}`.includes(mark) +} + +function heal(text: string) { + return remend(text, { linkMode: "text-only" }) +} + +// amicode (ported in the 1.18.10 sync): the block-math rule only tokenizes +// $$…$$ display math in two shapes — all on one line, or with the $$ alone on +// their own lines (^$$\n … \n$$). LLMs routinely emit a third shape — +// `label: $$\n…\n$$`, opening mid-line with a multi-line body — which matches +// NEITHER, so the span falls through to markdown, which then eats the LaTeX +// backslash-escapes (\; → ;, \! → !, \text → text) and renders a raw, mangled +// `$$ … $$`. Normalize every complete display-math span onto its own block +// lines so the block rule always matches. Fenced/inline code is skipped so +// LaTeX shown as code stays literal; an unclosed trailing `$$` (mid-stream) +// has no match and is left untouched until its closer arrives. +export function normalizeDisplayMath(text: string): string { + const codePattern = /(```[\s\S]*?```|~~~[\s\S]*?~~~|`[^`\n]*`)/g + return text + .split(codePattern) + .map((part, index) => + index % 2 === 1 + ? part + : part.replace(/\$\$([\s\S]*?)\$\$/g, (_match, body: string) => `\n\n$$\n${body.trim()}\n$$\n\n`), + ) + .join("") +} + +export function stream(text: string, live: boolean): Block[] { + if (!live) return [{ raw: text, src: text, mode: "full" }] satisfies Block[] + if (refs(text)) return [{ raw: text, src: heal(text), mode: "live" }] satisfies Block[] + const tokens = marked.lexer(text) + const tail = tokens.findLastIndex((token) => token.type !== "space") + if (tail < 0) return [{ raw: text, src: heal(text), mode: "live" }] satisfies Block[] + const last = tokens[tail] + if (!last) return [{ raw: text, src: heal(text), mode: "live" }] satisfies Block[] + + const result: Block[] = [] + for (let index = 0; index < tail; index++) { + const token = tokens[index] + if (!token || token.type === "space") continue + let raw = token.raw + while (tokens[index + 1]?.type === "space" && index + 1 < tail) raw += tokens[++index]!.raw + if (token.type === "code") { + const code = token as Tokens.Code + result.push({ raw, src: code.text, mode: "code", language: language(code.lang), complete: true }) + continue + } + result.push({ raw, src: raw, mode: "full" }) + } + + const raw = tokens + .slice(tail) + .map((token) => token.raw) + .join("") + if (last.type !== "code") return [...result, { raw, src: heal(raw), mode: "live" }] + + const code = last as Tokens.Code + if (!open(code.raw)) + return [...result, { raw, src: code.text, mode: "code", language: language(code.lang), complete: true }] + return [...result, { raw, src: openCode(code.raw), mode: "code", language: language(code.lang) }] +} + +export function canReusePendingBlock(current: Pick | undefined, next: Block) { + if (!current || current.mode !== next.mode) return false + if (next.mode === "code") return next.raw.startsWith(current.raw) + return current.raw === next.raw +} + +export function project(previous: Projection | undefined, text: string, live: boolean): Projection { + if (!live || !previous || !text.startsWith(previous.text)) return { text, blocks: stream(text, live) } + const tail = previous.blocks.at(-1) + const suffix = text.slice(previous.text.length) + if (!suffix || tail?.mode !== "code" || tail.complete || closesFence(tail.raw, suffix)) + return { text, blocks: stream(text, live) } + return { + text, + blocks: [ + ...previous.blocks.slice(0, -1), + { + ...tail, + raw: tail.raw + suffix, + src: tail.src + suffix, + }, + ], + } +} diff --git a/packages/app-bundle/overlay/packages/session-ui/src/components/markdown.css b/packages/app-bundle/overlay/packages/session-ui/src/components/markdown.css new file mode 100644 index 00000000..74bc3f88 --- /dev/null +++ b/packages/app-bundle/overlay/packages/session-ui/src/components/markdown.css @@ -0,0 +1,390 @@ +[data-component="markdown"] { + --markdown-inline-code-path-color: var(--v2-text-text-code-accent); + --markdown-inline-code-color: var(--v2-text-text-code-accent); + --markdown-inline-code-bg-mix: 8%; + --markdown-shell-code-background: var(--v2-background-bg-layer-02); + --markdown-shell-code-border: var(--v2-border-border-muted); + --markdown-shell-code-color: var(--v2-text-text-base); + --markdown-shell-code-token-color: currentColor; + + /* Reset & Base Typography */ + min-width: 0; + max-width: 100%; + overflow-wrap: break-word; + color: var(--v2-text-text-base); + font-family: var(--font-family-sans); + font-size: var(--font-size-base); /* 14px */ + line-height: 160%; + + /* Spacing for flow */ + > *:first-child { + margin-top: 0; + } + > *:last-child { + margin-bottom: 0; + } + > [data-markdown-block]:first-child > *:first-child { + margin-top: 0; + } + > [data-markdown-block]:last-child > *:last-child { + margin-bottom: 0; + } + + /* Headings: Sized by level, distinguished by color and spacing */ + h1 { + font-size: 17px; + color: var(--v2-text-text-base); + font-weight: 600; + margin-top: 28px; + margin-bottom: 12px; + line-height: var(--line-height-large); + } + + h2 { + font-size: 15px; + color: var(--v2-text-text-base); + font-weight: 600; + margin-top: 24px; + margin-bottom: 10px; + line-height: var(--line-height-large); + } + + h3 { + font-size: 13px; + color: var(--v2-text-text-base); + font-weight: var(--font-weight-medium); + margin-top: 20px; + margin-bottom: 8px; + line-height: var(--line-height-large); + } + + h4, + h5, + h6 { + font-size: 13px; + color: var(--v2-text-text-muted); + font-weight: var(--font-weight-medium); + margin-top: 20px; + margin-bottom: 8px; + line-height: var(--line-height-large); + } + + /* Emphasis & Strong: Neutral strong color */ + strong, + b { + color: var(--v2-text-text-base); + font-weight: var(--font-weight-medium); + } + + /* Paragraphs */ + p { + margin-bottom: 12px; + } + + /* Links */ + a { + color: var(--text-interactive-base); + text-decoration: none; + font-weight: inherit; + } + + a:hover { + text-decoration: underline; + text-underline-offset: 2px; + } + + /* Lists */ + ul, + ol { + margin-top: 8px; + margin-bottom: 12px; + margin-left: 0; + padding-left: 32px; + list-style-position: outside; + } + + ul { + list-style-type: disc; + } + + ol { + list-style-type: decimal; + padding-left: 2.25rem; + } + + li { + margin-bottom: 8px; + } + + li > p:first-child { + display: inline; + margin: 0; + } + + li:has(> input[type="checkbox"]), + li:has(> p > input[type="checkbox"]) { + position: relative; + list-style-type: none; + } + + li > input[type="checkbox"], + li > p > input[type="checkbox"] { + position: absolute; + top: 4px; + left: -20px; + margin: 0; + } + + li > p + p { + display: block; + margin-top: 0.5rem; + } + + li::marker { + color: var(--v2-text-text-muted); + } + + /* Nested lists spacing */ + li > ul, + li > ol { + margin-top: 0.25rem; + margin-bottom: 0.25rem; + padding-left: 1rem; /* Minimal indent for nesting only */ + } + + li > ol { + padding-left: 1.75rem; + } + + /* Blockquotes */ + blockquote { + border-left: 0.5px solid var(--v2-border-border-base); + margin: 1rem 0; + padding-left: 0.5rem; + color: var(--v2-text-text-muted); + font-style: normal; + } + + /* Horizontal Rule - Invisible spacing only */ + hr { + border: none; + height: 0; + margin: 32px 0; + } + + .shiki { + background: var(--color-background-stronger); + color: var(--text-base); + font-size: 13px; + padding: 12px; + border-radius: 6px; + border: 0.5px solid var(--v2-border-border-base); + } + + [data-component="markdown-code"] { + position: relative; + } + + [data-component="markdown-code"][data-code-kind="shell"] .shiki { + background: var(--markdown-shell-code-background); + border-color: var(--markdown-shell-code-border); + color: var(--markdown-shell-code-color); + } + + [data-component="markdown-code"][data-code-kind="shell"] code { + color: var(--markdown-shell-code-color); + } + + [data-component="markdown-code"][data-code-kind="shell"] code span { + color: var(--markdown-shell-code-token-color) !important; + } + + [data-slot="markdown-copy-button"] { + position: absolute; + top: 4px; + right: 4px; + opacity: 0; + transition: opacity 0.15s ease; + z-index: 1; + } + + [data-component="markdown-code"]:hover [data-slot="markdown-copy-button"], + [data-slot="markdown-copy-button"]:focus-within { + opacity: 1; + } + + [data-slot="markdown-copy-button"] [data-check-icon] { + display: none; + } + + [data-slot="markdown-copy-button"][data-copied="true"] [data-copy-icon] { + display: none; + } + + [data-slot="markdown-copy-button"][data-copied="true"] [data-check-icon] { + display: inline-flex; + } + + pre { + margin-top: 12px; + margin-bottom: 24px; + overflow: auto; + + scrollbar-width: none; + &::-webkit-scrollbar { + display: none; + } + } + + :not(pre) > code { + font-family: var(--font-family-mono); + font-feature-settings: var(--font-family-mono--font-feature-settings); + color: var(--v2-text-text-base); + font-weight: var(--font-weight-medium); + padding: 2px 4px; + border-radius: 4px; + background: color-mix(in oklch, var(--v2-text-text-base) var(--markdown-inline-code-bg-mix), transparent); + } + + :not(pre) > code[data-inline-code-kind="path"] { + color: var(--markdown-inline-code-path-color); + background: color-mix( + in oklch, + var(--markdown-inline-code-path-color) var(--markdown-inline-code-bg-mix), + transparent + ); + } + + a.external-link > code[data-inline-code-kind="url"] { + color: var(--markdown-inline-code-color); + background: none; + text-decoration: underline; + text-decoration-color: color-mix(in oklch, var(--markdown-inline-code-color) 25%, transparent); + text-underline-offset: 2px; + } + + /* AMICODE: a resolved file reference keeps the path-pill tint and earns a + * quiet underline + pointer — a link that looks like a pill, not a URL. */ + a.external-link > code[data-inline-code-kind="path"] { + text-decoration: underline; + text-decoration-color: color-mix(in oklch, var(--markdown-inline-code-path-color) 30%, transparent); + text-underline-offset: 2px; + cursor: pointer; + } + + a.external-link:hover > code[data-inline-code-kind="path"] { + text-decoration-color: var(--markdown-inline-code-path-color); + } + + /* Tables */ + table { + width: 100%; + border-collapse: collapse; + margin: 16px 0; + font-size: var(--font-size-base); + display: block; + overflow-x: auto; + } + + th, + td { + /* Minimal borders for structure, matching TUI "lines" roughly but keeping it web-clean */ + border-bottom: 0.5px solid var(--v2-border-border-muted); + padding: 12px; + text-align: left; + vertical-align: top; + } + + th { + color: var(--v2-text-text-base); + font-weight: var(--font-weight-medium); + border-bottom: 0.5px solid var(--v2-border-border-base); + } + + /* Images */ + img { + max-width: 100%; + height: auto; + border-radius: 4px; + margin: 1.5rem 0; + display: block; + } +} + +body[data-new-layout] [data-component="markdown"] { + [data-slot="markdown-copy-button"]::after { + box-sizing: border-box; + display: inline-flex; + flex-direction: row; + align-items: center; + gap: 6px; + padding: 5px 6px; + border: 0; + border-radius: 4px; + background: var(--v2-background-bg-layer-01); + box-shadow: var(--v2-elevation-floating); + color: var(--v2-text-text-base); + user-select: none; + font-size: 11px; + font-weight: 530; + line-height: 12px; + letter-spacing: 0.05px; + font-variant-numeric: tabular-nums; + font-variation-settings: "slnt" 0; + } +} + +body:not([data-new-layout]) [data-component="markdown"] { + color: var(--text-strong); + + h1, + h2, + h3, + h4, + h5, + h6 { + font-size: 14px; + color: var(--text-strong); + font-weight: var(--font-weight-medium); + } + + strong, + b { + color: var(--text-strong); + } + + li::marker { + color: var(--text-weak); + } + + blockquote { + border-left: 2px solid var(--border-weak-base); + color: var(--text-weak); + } + + .shiki { + border-color: var(--border-weak-base); + } + + :not(pre) > code { + color: var(--syntax-string); + padding: 0; + border-radius: 0; + background: none; + } + + th, + td { + border-bottom: 1px solid var(--border-weaker-base); + } + + th { + color: var(--text-strong); + border-bottom-color: var(--border-weak-base); + } +} + +[data-component="markdown"] a.external-link:hover > code { + text-decoration: underline; + text-underline-offset: 2px; +} diff --git a/packages/app-bundle/overlay/packages/session-ui/src/components/markdown.tsx b/packages/app-bundle/overlay/packages/session-ui/src/components/markdown.tsx new file mode 100644 index 00000000..7dd0f484 --- /dev/null +++ b/packages/app-bundle/overlay/packages/session-ui/src/components/markdown.tsx @@ -0,0 +1,784 @@ +import { useMarked } from "@opencode-ai/ui/context/marked" +import { useI18n } from "@opencode-ai/ui/context/i18n" +import { copyTextToClipboard } from "../util/clipboard" +import { normalizeDisplayMath } from "./markdown-stream" +import morphdom from "morphdom" +import { checksum } from "@opencode-ai/core/util/encode" +import { + type Accessor, + type ComponentProps, + createEffect, + createMemo, + createResource, + createSignal, + createUniqueId, + onCleanup, + type Setter, + splitProps, +} from "solid-js" +import { isServer, render } from "solid-js/web" +import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" +import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" +import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" +import { bundledLanguages } from "shiki" +import { canReusePendingBlock, project, type Block, type Projection } from "./markdown-stream" +import { + disposeStreamingCode, + highlightStreamingCode, + MarkdownWorkerDisposedError, + MarkdownWorkerSupersededError, + MarkdownWorkerUnavailableError, +} from "./markdown-worker" +import { markdownBlockKey, type MarkdownToken } from "./markdown-worker-protocol" +import { shouldResetCodeTokens, type RenderedCodeState } from "./markdown-code-state" +import { getCachedMarkdown, sanitizeMarkdown, touchCachedMarkdown, type MarkdownCacheEntry } from "./markdown-cache" +import { inlineCodeKind } from "./markdown-inline-code-kind" +import { cachedFileRef, fileRefResolver, fileRefUrl, resolveFileRefCached } from "./markdown-file-refs" + +type RenderedBlock = + | (MarkdownCacheEntry & { key: string; mode: Exclude }) + | { + key: string + mode: "code" + raw: string + hash: string + language: string + complete: boolean + generation: number + stable: MarkdownToken[] + unstable: MarkdownToken[] + } + +type RenderResult = { + text: string + blocks: RenderedBlock[] +} + +const renderedCodeTokens = new WeakMap() + +function escape(text: string) { + return text + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'") +} + +function fallback(markdown: string) { + return escape(markdown).replace(/\r\n?/g, "\n").replace(/\n/g, "
") +} + +async function code(text: string, language: string | undefined, key: string, complete = false) { + const name = language && language in bundledLanguages ? language : "text" + try { + const result = await highlightStreamingCode(key, text, name, complete) + return { language: name, generation: result.generation, stable: result.stable, unstable: result.unstable } + } catch (error) { + if ( + !(error instanceof MarkdownWorkerDisposedError) && + !(error instanceof MarkdownWorkerSupersededError) && + !(error instanceof MarkdownWorkerUnavailableError) + ) + console.error("Markdown highlighting worker failed", error) + return { language: name, generation: 0, stable: [], unstable: [[text, ""] as MarkdownToken] } + } +} + +type CopyLabels = { + copy: string + copied: string +} + +type CopyButtonState = { + setLabels: Setter + setCopied: Setter + dispose: () => void +} + +const copyButtonState = new WeakMap() + +const urlPattern = /^https?:\/\/[^\s<>()`"']+$/ + +function codeUrl(text: string) { + const href = text.trim().replace(/[),.;!?]+$/, "") + if (!urlPattern.test(href)) return + try { + const url = new URL(href) + return url.toString() + } catch { + return + } +} + +function createCopyButton(labels: CopyLabels) { + const host = document.createElement("div") + host.setAttribute("data-slot", "markdown-copy-button") + + const state: Partial = {} + const dispose = render(() => { + const [labelState, setLabels] = createSignal(labels, { equals: false }) + const [copied, setCopied] = createSignal(false) + state.setLabels = setLabels + state.setCopied = setCopied + return + }, host) + state.dispose = dispose + copyButtonState.set(host, state as CopyButtonState) + return host +} + +function MarkdownCopyButton(props: { labels: Accessor; copied: Accessor }) { + const label = () => (props.copied() ? props.labels().copied : props.labels().copy) + return ( + + + + + + } + /> + + ) +} + +function setCopyState(host: HTMLElement, labels: CopyLabels, copied: boolean) { + const state = copyButtonState.get(host) + state?.setLabels(labels) + state?.setCopied(copied) + if (copied) { + host.setAttribute("data-copied", "true") + return + } + host.removeAttribute("data-copied") +} + +function disposeCopyButton(host: HTMLElement) { + copyButtonState.get(host)?.dispose() + copyButtonState.delete(host) +} + +function disposeCopyButtons(root: Element) { + const hosts = [ + ...(root instanceof HTMLElement && root.getAttribute("data-slot") === "markdown-copy-button" ? [root] : []), + ...Array.from(root.querySelectorAll('[data-slot="markdown-copy-button"]')).filter( + (el): el is HTMLElement => el instanceof HTMLElement, + ), + ] + hosts.forEach(disposeCopyButton) +} + +const shellLanguages = new Set(["bash", "sh", "shell", "zsh", "fish", "console", "terminal"]) + +function codeKind(language: string | undefined) { + const value = language?.toLowerCase() + if (!value) return + if (shellLanguages.has(value)) return "shell" +} + +function codeLanguage(block: HTMLPreElement) { + const code = block.querySelector("code") + if (!(code instanceof HTMLElement)) return + return code.className.match(/(?:^|\s)language-([^\s]+)/)?.[1] +} + +function applyCodeMetadata(wrapper: HTMLElement, language: string | undefined) { + if (!document.body.hasAttribute("data-new-layout")) { + delete wrapper.dataset.language + delete wrapper.dataset.codeKind + return + } + + if (language) wrapper.dataset.language = language + else delete wrapper.dataset.language + + const kind = codeKind(language) + if (kind) wrapper.dataset.codeKind = kind + else delete wrapper.dataset.codeKind +} + +function ensureCodeWrapper(block: HTMLPreElement, labels: CopyLabels) { + const parent = block.parentElement + if (!parent) return + const wrapped = parent.getAttribute("data-component") === "markdown-code" + if (!wrapped) { + const wrapper = document.createElement("div") + wrapper.setAttribute("data-component", "markdown-code") + applyCodeMetadata(wrapper, codeLanguage(block)) + parent.replaceChild(wrapper, block) + wrapper.appendChild(block) + wrapper.appendChild(createCopyButton(labels)) + return + } + + applyCodeMetadata(parent, codeLanguage(block)) + + const buttons = Array.from(parent.querySelectorAll('[data-slot="markdown-copy-button"]')).filter( + (el): el is HTMLButtonElement => el instanceof HTMLButtonElement, + ) + + if (buttons.length === 0) { + parent.appendChild(createCopyButton(labels)) + return + } + + for (const button of buttons.slice(1)) { + disposeCopyButton(button) + button.remove() + } +} + +function markCodeLinks(root: HTMLDivElement) { + const codeNodes = Array.from(root.querySelectorAll(":not(pre) > code")) + for (const code of codeNodes) { + const href = codeUrl(code.textContent ?? "") + const parentLink = + code.parentElement instanceof HTMLAnchorElement && code.parentElement.classList.contains("external-link") + ? code.parentElement + : null + + if (!href) { + if (parentLink) parentLink.replaceWith(code) + continue + } + + if (parentLink) { + parentLink.href = href + continue + } + + const link = document.createElement("a") + link.href = href + link.className = "external-link" + link.target = "_blank" + link.rel = "noopener noreferrer" + code.parentNode?.replaceChild(link, code) + link.appendChild(code) + } +} + +function markInlineCode(root: HTMLDivElement) { + const codeNodes = Array.from(root.querySelectorAll(":not(pre) > code")) + for (const code of codeNodes) { + if (!(code instanceof HTMLElement)) continue + delete code.dataset.inlineCodeKind + const kind = inlineCodeKind(code.textContent ?? "") + if (kind) code.dataset.inlineCodeKind = kind + } +} + +// AMICODE: linkify file references. Path-kind inline-code pills get wrapped in +// file:// anchors when the app-registered resolver finds them on disk; authored +// relative links ([x](plans/foo.md)) get their sandbox-dead relative href +// rewritten to file://. Unresolvable references stay plain pills/links — never +// a dead anchor. Runs on the LIVE container after morphdom (unlike the other +// decorators, which run on the detached block): async resolutions patch nodes +// in place when they land, and morphdom rebuilds get re-wrapped from the +// (synchronous) cache on the next pass. Must stay AFTER markCodeLinks in any +// shared pass — that function unwraps code spans inside anchors when the text +// isn't a URL. +function wrapCodeInFileLink(code: HTMLElement, abs: string) { + const link = document.createElement("a") + link.href = fileRefUrl(abs) + link.className = "external-link" + link.title = abs + code.parentNode?.replaceChild(link, code) + link.appendChild(code) +} + +function markFileLinks(root: HTMLDivElement) { + if (!fileRefResolver()) return + const codes = Array.from(root.querySelectorAll(":not(pre) > code[data-inline-code-kind=\"path\"]")) + for (const code of codes) { + if (!(code instanceof HTMLElement)) continue + if (code.parentElement instanceof HTMLAnchorElement) continue + const text = code.textContent ?? "" + if (!text) continue + const cached = cachedFileRef(text) + if (cached !== undefined) { + if (cached) wrapCodeInFileLink(code, cached) + continue + } + void resolveFileRefCached(text).then((abs) => { + if (!abs || !code.isConnected) return + if (code.parentElement instanceof HTMLAnchorElement) return + wrapCodeInFileLink(code, abs) + }) + } + // authored relative links — DOMPurify keeps relative hrefs, but a relative + // navigation is dead in the sandboxed iframe. Rewrite to file:// on resolve. + const anchors = Array.from(root.querySelectorAll("a.external-link")) + for (const anchor of anchors) { + if (!(anchor instanceof HTMLAnchorElement)) continue + const href = anchor.getAttribute("href") ?? "" + if (!href || href.startsWith("#") || /^(https?:\/\/|mailto:|file:)/i.test(href)) continue + const cached = cachedFileRef(href) + if (cached !== undefined) { + if (cached) anchor.href = fileRefUrl(cached) + continue + } + void resolveFileRefCached(href).then((abs) => { + if (!abs || !anchor.isConnected) return + anchor.href = fileRefUrl(abs) + }) + } +} + +function decorate(root: HTMLDivElement, labels: CopyLabels) { + const blocks = Array.from(root.querySelectorAll("pre")) + for (const block of blocks) { + ensureCodeWrapper(block, labels) + } + // Fix for issue #270: Mark all external links for bridge handling + // This must run unconditionally, not just in new-layout mode + markExternalLinks(root) + markCodeLinks(root) + if (!document.body.hasAttribute("data-new-layout")) return + markInlineCode(root) +} + +// Issue #270: Mark regular markdown links as external so they open via the bridge +function markExternalLinks(root: HTMLDivElement) { + const links = Array.from(root.querySelectorAll('a[href^="http"], a[href^="mailto:"]')) + for (const link of links) { + if (!(link instanceof HTMLAnchorElement)) continue + // Skip if already marked + if (link.classList.contains("external-link")) continue + link.classList.add("external-link") + link.target = "_blank" + link.rel = "noopener noreferrer" + } +} + +function setupCodeCopy(root: HTMLDivElement, getLabels: () => CopyLabels) { + const timeouts = new Map>() + + const updateLabel = (button: HTMLElement) => { + const labels = getLabels() + const copied = button.getAttribute("data-copied") === "true" + setCopyState(button, labels, copied) + } + + const handleClick = async (event: MouseEvent) => { + const target = event.target + if (!(target instanceof Element)) return + + const button = target.closest('[data-slot="markdown-copy-button"]') + if (!(button instanceof HTMLElement)) return + const code = button.closest('[data-component="markdown-code"]')?.querySelector("code") + const content = code?.textContent ?? "" + if (!content) return + // Amicode webview: navigator.clipboard dies in the chat iframe — bridge-aware write. + if (!(await copyTextToClipboard(content))) return + const labels = getLabels() + setCopyState(button, labels, true) + const existing = timeouts.get(button) + if (existing) clearTimeout(existing) + const timeout = setTimeout(() => setCopyState(button, labels, false), 2000) + timeouts.set(button, timeout) + } + + const buttons = Array.from(root.querySelectorAll('[data-slot="markdown-copy-button"]')) + for (const button of buttons) { + if (button instanceof HTMLElement) updateLabel(button) + } + + root.addEventListener("click", handleClick) + + return () => { + root.removeEventListener("click", handleClick) + for (const timeout of timeouts.values()) { + clearTimeout(timeout) + } + disposeCopyButtons(root) + } +} + +// Chat link clicks: inside the amicode webview the chat iframe is sandboxed and +// cross-origin, so anchor default behavior (target=_blank) is dead. Route every +// external-link click over the iframe→parent bridge instead: https/mailto → +// open-external (the extension opens the system browser), file:// → open-file +// (the extension opens vault notes / run artifacts in the editor). Framed +// contexts only — a plain browser tab keeps native link behavior. +function setupExternalLinks(root: HTMLDivElement) { + if (window.parent === window) return + const handleClick = (event: MouseEvent) => { + const target = event.target + if (!(target instanceof Element)) return + const anchor = target.closest("a.external-link") + if (!(anchor instanceof HTMLAnchorElement)) return + const href = anchor.getAttribute("href") ?? "" + let kind: "open-external" | "open-file" | undefined + if (/^https?:\/\//i.test(href) || /^mailto:/i.test(href)) kind = "open-external" + else if (/^file:\/\//i.test(href)) kind = "open-file" + if (!kind) return + event.preventDefault() + window.parent.postMessage({ source: "amicode", kind, url: href }, "*") + } + root.addEventListener("click", handleClick) + return () => root.removeEventListener("click", handleClick) +} + +function initialResult(text: string, key: string | undefined, projection: Projection, owner: string): RenderResult { + if (!text) return { text, blocks: [] } + const base = key ?? checksum(text) + if (base) { + const blocks = projection.blocks.flatMap((block, index) => { + if (block.mode === "code") return [] + const cacheKey = `${base}:${index}:${block.mode}` + const cached = getCachedMarkdown(cacheKey) + if (cached?.raw !== block.raw) return [] + return [{ key: `${owner}:${cacheKey}`, mode: block.mode, ...cached }] + }) + if (blocks.length === projection.blocks.length) return { text, blocks } + } + return { + text, + blocks: [ + { + key: "initial", + mode: "full", + raw: text, + hash: checksum(text) ?? "", + html: fallback(text), + }, + ], + } +} + +export function Markdown( + props: ComponentProps<"div"> & { + text: string + cacheKey?: string + streaming?: boolean + class?: string + classList?: Record + }, +) { + const [local, others] = splitProps(props, ["text", "cacheKey", "streaming", "class", "classList"]) + const marked = useMarked() + const i18n = useI18n() + const [root, setRoot] = createSignal() + const owner = createUniqueId() + const activeCodeKeys = new Set() + const completedCode = new Map>() + const projection = createMemo((previous: Projection | undefined) => + project(previous, local.text, local.streaming ?? false), + ) + const [html] = createResource( + () => { + return { + text: local.text, + key: local.cacheKey, + projection: projection(), + } + }, + async (src) => { + if (isServer) + return { + text: src.text, + blocks: [ + { + key: "server", + mode: "full" as const, + raw: src.text, + hash: checksum(src.text) ?? "", + html: fallback(src.text), + }, + ], + } satisfies RenderResult + if (!src.text) return { text: src.text, blocks: [] } satisfies RenderResult + + const base = src.key ?? checksum(src.text) + return Promise.all( + src.projection.blocks.map(async (block, index) => { + const key = base ? `${base}:${index}:${block.mode}` : undefined + const blockKey = markdownBlockKey(owner, src.key, index, block.mode) + + if (block.mode === "code") { + const cached = completedCode.get(blockKey) + if (block.complete && cached?.raw === block.raw) return cached + const result = await code(block.src, block.language, blockKey, block.complete) + const rendered = { + key: blockKey, + mode: block.mode, + raw: block.raw, + hash: String(block.raw.length), + complete: !!block.complete, + ...result, + } + if (block.complete) completedCode.set(blockKey, rendered) + return rendered + } + + if (key) { + const cached = getCachedMarkdown(key) + if (cached?.raw === block.raw) { + touchCachedMarkdown(key, cached) + return { key: blockKey, mode: block.mode, ...cached } + } + } + + const hash = checksum(block.raw) + const safe = sanitizeMarkdown(await Promise.resolve(marked.parse(normalizeDisplayMath(block.src)))) + if (key && hash) touchCachedMarkdown(key, { raw: block.raw, hash, html: safe }) + return { key: blockKey, mode: block.mode, raw: block.raw, hash: hash ?? "", html: safe } + }), + ) + .then((blocks) => ({ text: src.text, blocks }) satisfies RenderResult) + .catch( + () => + ({ + text: src.text, + blocks: [ + { + key: base ?? "fallback", + mode: "full" as const, + raw: src.text, + hash: checksum(src.text) ?? "", + html: fallback(src.text), + }, + ], + }) satisfies RenderResult, + ) + }, + { + initialValue: initialResult(local.text, local.cacheKey, projection(), owner), + }, + ) + + let copyCleanup: (() => void) | undefined + let linkCleanup: (() => void) | undefined + + createEffect(() => { + const container = root() + const result = html.latest ?? html() + const projected = projection() + const content = local.text ? pendingBlocks(result, projected, local.cacheKey, owner) : [] + if (!container) return + if (isServer) return + if (content.length === 0) { + disposeCopyButtons(container) + container.innerHTML = "" + return + } + + const labels = { + copy: i18n.t("ui.message.copy"), + copied: i18n.t("ui.message.copied"), + } + const nextCodeKeys = new Set(content.filter((block) => block.mode === "code").map((block) => block.key)) + activeCodeKeys.forEach((key) => { + if (!nextCodeKeys.has(key)) disposeCode(key) + }) + activeCodeKeys.clear() + nextCodeKeys.forEach((key) => activeCodeKeys.add(key)) + content.forEach((block, index) => updateBlock(container, index, block, labels)) + while (container.children.length > content.length) { + const child = container.lastElementChild + if (!child) break + disposeCopyButtons(child) + child.remove() + } + container + .querySelectorAll('[data-slot="markdown-copy-button"]') + .forEach((button) => setCopyState(button, labels, button.dataset.copied === "true")) + if (!copyCleanup) + copyCleanup = setupCodeCopy(container, () => ({ + copy: i18n.t("ui.message.copy"), + copied: i18n.t("ui.message.copied"), + })) + if (!linkCleanup) linkCleanup = setupExternalLinks(container) + // File-reference linkification rides the same new-layout gate as the + // inline-code pill tagging (decorate) whose data attributes it consumes. + if (document.body.hasAttribute("data-new-layout")) markFileLinks(container) + }) + + onCleanup(() => { + if (copyCleanup) copyCleanup() + if (linkCleanup) linkCleanup() + activeCodeKeys.forEach(disposeCode) + completedCode.clear() + }) + + return ( +
+ } + > + props.actions?.openAttachment?.(file)} + > + {typeLabel(name, file.mime)} + + + ) + }} + + + + ) + + return ( +
+ {renderAttachments()} + 0} + fallback={ + 0}> + + + } + > +
+ + {(skill) => ( + +
+ +
+
+ } + > + +
+ +
+
+ + )} + +
0 ? "true" : undefined}> + + 0}> + + +
+
+ + {renderAttachments()} + 0 || (props.useV2Actions && messageComments().length > 0)}> +
+ + + + + {metaHead()} + + + + + {"\u00A0\u00B7\u00A0"} + + + + + {metaTail()} + + + + + + event.preventDefault()} + onClick={(event) => { + event.stopPropagation() + revert() + }} + aria-label={i18n.t("ui.message.revertMessage")} + /> + + + event.preventDefault()} + onClick={(event) => { + event.stopPropagation() + void handleCopy() + }} + aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copyMessage")} + /> + +
+
+ + ) +} + +type HighlightSegment = { text: string; type?: "file" | "agent" } + +function HighlightedText(props: { text: string; references: FilePart[]; agents: AgentPart[] }) { + const segments = createMemo(() => { + const text = props.text + + const allRefs: { start: number; end: number; type: "file" | "agent" }[] = [ + ...props.references + .filter((r) => r.source?.text?.start !== undefined && r.source?.text?.end !== undefined) + .map((r) => ({ start: r.source!.text!.start, end: r.source!.text!.end, type: "file" as const })), + ...props.agents + .filter((a) => a.source?.start !== undefined && a.source?.end !== undefined) + .map((a) => ({ start: a.source!.start, end: a.source!.end, type: "agent" as const })), + ].sort((a, b) => a.start - b.start) + + const result: HighlightSegment[] = [] + let lastIndex = 0 + + for (const ref of allRefs) { + if (ref.start < lastIndex) continue + + if (ref.start > lastIndex) { + result.push({ text: text.slice(lastIndex, ref.start) }) + } + + result.push({ text: text.slice(ref.start, ref.end), type: ref.type }) + lastIndex = ref.end + } + + if (lastIndex < text.length) { + result.push({ text: text.slice(lastIndex) }) + } + + return result + }) + + return {(segment) => {segment.text}} +} + +export function Part(props: MessagePartProps) { + const component = createMemo(() => PART_MAPPING[props.part.type]) + return ( + + + + ) +} + +export interface ToolProps { + input: Record + metadata: Record + tool: string + sessionID?: string + output?: string + status?: string + hideDetails?: boolean + defaultOpen?: boolean + open?: boolean + onOpenChange?: (open: boolean) => void + deferContent?: boolean + virtualizeDiff?: boolean + onContentRendered?: () => void + forceOpen?: boolean + locked?: boolean +} + +export type ToolComponent = Component + +const state: Record< + string, + { + name: string + render?: ToolComponent + } +> = {} + +export function registerTool(input: { name: string; render?: ToolComponent }) { + state[input.name] = input + return input +} + +export function getTool(name: string) { + return state[name === "apply_patch" ? "patch" : name === "bash" ? "shell" : name]?.render +} + +export const ToolRegistry = { + register: registerTool, + render: getTool, +} + +function ToolFileAccordion(props: { path: string; actions?: JSX.Element; children: JSX.Element }) { + const value = createMemo(() => props.path || "tool-file") + + return ( + + + + +
+
+ +
+ + {`\u202A${getDirectory(props.path)}\u202C`} + + {getFilename(props.path)} +
+
+
+ {props.actions} + +
+
+
+
+ {props.children} +
+
+ ) +} + +PART_MAPPING["tool"] = function ToolPartDisplay(props) { + const data = useData() + const i18n = useI18n() + const part = () => props.part as ToolPart + if (part().tool === "todowrite") return null + + const hideQuestion = createMemo( + () => part().tool === "question" && (part().state.status === "pending" || part().state.status === "running"), + ) + + const emptyInput: Record = {} + const emptyMetadata: Record = {} + + const input = () => part().state?.input ?? emptyInput + // @ts-expect-error + const partMetadata = () => part().state?.metadata ?? emptyMetadata + const taskId = createMemo(() => { + if (part().tool !== "task") return + const value = partMetadata().sessionId + if (typeof value === "string" && value) return value + }) + const taskHref = createMemo(() => { + if (part().tool !== "task") return + return sessionLink(taskId(), data.sessionHref) + }) + const taskSubtitle = createMemo(() => { + if (part().tool !== "task") return undefined + const value = input().description + if (typeof value === "string" && value) return value + return taskId() + }) + + // amicode: L2 renderer slot — amicode_* tools get the Amicode card (sole stock-code touch) + const render = createMemo(() => + /^amicode_/.test(part().tool) ? AmicodeToolCard : (ToolRegistry.render(part().tool) ?? GenericTool), + ) + const controlledOpen = () => (props.onToolOpenChange ? (props.toolOpen ?? props.defaultOpen) : undefined) + const handleToolOpenChange = (open: boolean) => props.onToolOpenChange?.(open) + + // amicode: hovering the row glances at its node on the brain's map + const brainRef = createMemo(() => amicoBrainRef(part().tool, input())) + return ( + +
emitAmicoBrainHover(brainRef())} + > + + + {(error) => { + const cleaned = error().replace("Error: ", "") + if (part().tool === "question" && cleaned.includes("dismissed this question")) { + return ( +
+ + {i18n.t("ui.messagePart.questions.dismissed")} + +
+ ) + } + return ( + { + if (!data.navigateToSession) return + if (event.button !== 0 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return + const id = taskId() + if (!id) return + event.preventDefault() + data.navigateToSession(id) + }} + /> + ) + }} +
+ + + +
+
+
+ ) +} + +export function MessageDivider(props: { label: string }) { + return ( +
+
+ + + {props.label} + + +
+
+ ) +} + +PART_MAPPING["compaction"] = function CompactionPartDisplay() { + const i18n = useI18n() + return +} + +PART_MAPPING["text"] = function TextPartDisplay(props) { + const data = useData() + const i18n = useI18n() + const numfmt = createMemo(() => new Intl.NumberFormat(i18n.locale())) + const part = () => props.part as TextPart + const interrupted = createMemo( + () => + props.message.role === "assistant" && (props.message as AssistantMessage).error?.name === "MessageAbortedError", + ) + + const model = createMemo(() => { + if (props.message.role !== "assistant") return "" + const message = props.message as AssistantMessage + const match = data.store.provider?.all?.get(message.providerID) + return match?.models?.[message.modelID]?.name ?? message.modelID + }) + + const duration = createMemo(() => { + if (props.message.role !== "assistant") return "" + const message = props.message as AssistantMessage + const completed = message.time.completed + const ms = + typeof props.turnDurationMs === "number" + ? props.turnDurationMs + : typeof completed === "number" + ? completed - message.time.created + : -1 + if (!(ms >= 0)) return "" + const total = Math.round(ms / 1000) + if (total < 60) return i18n.t("ui.message.duration.seconds", { count: numfmt().format(total) }) + const minutes = Math.floor(total / 60) + const seconds = total % 60 + return i18n.t("ui.message.duration.minutesSeconds", { + minutes: numfmt().format(minutes), + seconds: numfmt().format(seconds), + }) + }) + + const meta = createMemo(() => { + if (props.message.role !== "assistant") return "" + const agent = (props.message as AssistantMessage).agent + const items = [ + agent ? agent[0]?.toUpperCase() + agent.slice(1) : "", + model(), + duration(), + interrupted() ? i18n.t("ui.message.interrupted") : "", + ] + return items.filter((x) => !!x).join(" \u00B7 ") + }) + + const streaming = createMemo( + () => props.message.role === "assistant" && typeof (props.message as AssistantMessage).time.completed !== "number", + ) + const text = () => readPartText(data.store.part_text_accum_delta, part()) + const isLastTextPart = createMemo(() => { + const last = (data.store.part?.[props.message.id] ?? []) + .filter((item): item is TextPart => item?.type === "text" && !!item.text?.trim()) + .at(-1) + return last?.id === part().id + }) + const showCopy = createMemo(() => { + if (props.message.role !== "assistant") return isLastTextPart() + if (props.showAssistantCopyPartID === null) return false + if (typeof props.showAssistantCopyPartID === "string") return props.showAssistantCopyPartID === part().id + return isLastTextPart() + }) + const [copied, setCopied] = createSignal(false) + + const handleCopy = async () => { + const content = text() + if (!content) return + if (await writeClipboard(content)) { + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + } + + return ( + +
+
+ }> + + +
+ +
+ event.preventDefault()} + onClick={handleCopy} + aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copyResponse")} + /> + + + {meta()} + + +
+
+
+
+ ) +} + +PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props) { + const data = useData() + const part = () => props.part as ReasoningPart + const streaming = createMemo( + () => props.message.role === "assistant" && typeof (props.message as AssistantMessage).time.completed !== "number", + ) + const text = () => readPartText(data.store.part_text_accum_delta, part()) + + return ( + +
+ }> + + +
+
+ ) +} + +// AMICODE: the filename in an edit/write trigger opens the file in the editor +// via the bridge (the chat iframe can't). stopPropagation so the tool body's +// expand/collapse doesn't fire alongside. +function OpenableFilename(props: { filename: string; path: string }) { + return ( + {props.filename}}> + + + ) +} + +ToolRegistry.register({ + name: "read", + render(props) { + const data = useData() + const i18n = useI18n() + const args: string[] = [] + if (props.input.offset) args.push("offset=" + props.input.offset) + if (props.input.limit) args.push("limit=" + props.input.limit) + const loaded = createMemo(() => { + if (props.status !== "completed") return [] + const value = props.metadata.loaded + if (!value || !Array.isArray(value)) return [] + return value.filter((p): p is string => typeof p === "string") + }) + return ( + <> + + + {(filepath) => ( + + )} + + + ) + }, +}) + +ToolRegistry.register({ + name: "list", + render(props) { + const i18n = useI18n() + return ( + + +
+ +
+
+
+ ) + }, +}) + +ToolRegistry.register({ + name: "glob", + render(props) { + const i18n = useI18n() + return ( + + + + + + ) + }, +}) + +ToolRegistry.register({ + name: "grep", + render(props) { + const i18n = useI18n() + const args: string[] = [] + if (props.input.pattern) args.push("pattern=" + props.input.pattern) + if (props.input.include) args.push("include=" + props.input.include) + return ( + + +
+ +
+
+
+ ) + }, +}) + +ToolRegistry.register({ + name: "webfetch", + render(props) { + const i18n = useI18n() + const pending = createMemo(() => props.status === "pending" || props.status === "running") + const url = createMemo(() => { + const value = props.input.url + if (typeof value !== "string") return "" + return value + }) + return ( + + + +
+ +
+
+ + } + /> + ) + }, +}) + +ToolRegistry.register({ + name: "websearch", + render(props) { + const query = createMemo(() => { + const value = props.input.query + if (typeof value !== "string") return "" + return value + }) + const title = createMemo(() => webSearchProviderLabel(props.metadata.provider)) + + return ( + + + + ) + }, +}) + +ToolRegistry.register({ + name: "task", + render(props) { + const data = useData() + const i18n = useI18n() + const childSessionId = createMemo(() => { + const value = props.metadata.sessionId + if (typeof value === "string" && value) return value + return taskSession(props.input, data.sessionID, data.store.session, data.store.agent) + }) + const agent = createMemo(() => taskAgent(props.input.subagent_type, data.store.agent)) + const title = createMemo(() => agent().name ?? i18n.t("ui.tool.agent.default")) + const tone = createMemo(() => agent().color) + const v2Tone = createMemo(() => agent().v2Color) + const subtitle = createMemo(() => { + const value = + typeof props.input.description === "string" && props.input.description + ? props.input.description + : childSessionId() + if (!value) return value + if (props.metadata.background === true) return `${value} (background)` + return value + }) + const running = createMemo(() => props.status === "pending" || props.status === "running") + + const href = createMemo(() => sessionLink(childSessionId(), data.sessionHref)) + const clickable = createMemo(() => !!(childSessionId() && (data.navigateToSession || href()))) + + const open = () => { + const id = childSessionId() + if (!id) return + data.navigateToSession?.(id) + } + + const navigate = (event: MouseEvent) => { + if (!data.navigateToSession) return + if (event.button !== 0 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return + event.preventDefault() + open() + } + const navigateKey = (event: KeyboardEvent) => { + if (!clickable() || href()) return + if (event.key !== "Enter" && event.key !== " ") return + event.preventDefault() + open() + } + + const trigger = () => ( +
+
+
+
+ + + + + + } + > + + {/* amicode: H-glyph at the legacy spinner site (patch #10 sweep); + SessionProgressIndicatorV2 kept for the v2 task card */} + }> + + + + + {title()} + + {subtitle()} + +
+
+
+ +
+ +
+
+
+ ) + + return ( + + ) + }, +}) + +ToolRegistry.register({ + name: "shell", + render(props) { + const i18n = useI18n() + const pending = () => props.status === "pending" || props.status === "running" + const sawPending = pending() + const text = createMemo(() => { + const cmd = props.input.command ?? props.metadata.command ?? "" + const out = stripAnsi(props.output || props.metadata.output || "").replace(/\r\n?/g, "\n") + return `$ ${cmd}${out ? "\n\n" + out : ""}` + }) + const [copied, setCopied] = createSignal(false) + + const handleCopy = async () => { + const content = text() + if (!content) return + if (await writeClipboard(content)) { + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + } + + return ( + ( +
+
+ + + + + + +
+
+ )} + > +
+
+ + } + size="normal" + variant="ghost-muted" + onMouseDown={(e) => e.preventDefault()} + onClick={handleCopy} + aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")} + /> + +
+
+
+              {text()}
+            
+
+
+
+ ) + }, +}) + +ToolRegistry.register({ + name: "edit", + render(props) { + const i18n = useI18n() + const fileComponent = useFileComponent() + const diagnostics = createMemo(() => getDiagnostics(props.metadata.diagnostics, props.input.filePath)) + const path = createMemo(() => props.metadata?.filediff?.file || props.input.filePath || "") + const filename = () => getFilename(props.input.filePath ?? "") + const pending = () => props.status === "pending" || props.status === "running" + const diffSource = createMemo( + () => { + const filediff = props.metadata?.filediff + if (!filediff) return + return { + file: filediff.file || props.input.filePath || "", + patch: typeof filediff.patch === "string" ? filediff.patch : undefined, + before: typeof filediff.before === "string" ? filediff.before : undefined, + after: typeof filediff.after === "string" ? filediff.after : undefined, + } + }, + undefined, + { + equals: (a, b) => + a?.file === b?.file && a?.patch === b?.patch && a?.before === b?.before && a?.after === b?.after, + }, + ) + + const fileCompProps = createMemo(() => { + try { + const source = diffSource() + if (source) { + const fileDiff = resolveFileDiff(source) + if (fileDiff) return { fileDiff, hunkSeparators: fileDiff.isPartial ? "simple" : "line-info-basic" } + } + } catch {} + + return { + before: { + name: props.metadata?.filediff?.file || props.input.filePath, + contents: props.metadata?.filediff?.before || props.input.oldString || "", + }, + after: { + name: props.metadata?.filediff?.file || props.input.filePath, + contents: props.metadata?.filediff?.after || props.input.newString || "", + }, + } + }) + + return ( +
+ +
+
+ + + + + + +
+ +
+ {getDirectory(props.input.filePath!)} +
+
+
+
+ + + +
+
+ } + > + + + + + } + > +
+ +
+ + + +
+ + ) + }, +}) + +ToolRegistry.register({ + name: "write", + render(props) { + const i18n = useI18n() + const fileComponent = useFileComponent() + const diagnostics = createMemo(() => getDiagnostics(props.metadata.diagnostics, props.input.filePath)) + const path = createMemo(() => props.input.filePath || "") + const filename = () => getFilename(props.input.filePath ?? "") + const pending = () => props.status === "pending" || props.status === "running" + return ( +
+ +
+
+ + + + + + +
+ +
+ {getDirectory(props.input.filePath!)} +
+
+
+
{/* */}
+
+ } + > + + +
+ +
+
+
+ +
+ + ) + }, +}) + +ToolRegistry.register({ + name: "patch", + render(props) { + const i18n = useI18n() + const fileComponent = useFileComponent() + const files = createMemo(() => patchFiles(props.metadata.files)) + const pending = createMemo(() => props.status === "pending" || props.status === "running") + const single = createMemo(() => { + const list = files() + if (list.length !== 1) return + return list[0] + }) + const [expanded, setExpanded] = createSignal([]) + let seeded = false + + createEffect(() => { + const list = files() + if (list.length === 0) return + if (seeded) return + seeded = true + setExpanded(list.filter((f) => f.type !== "delete").map((f) => f.filePath)) + }) + + const subtitle = createMemo(() => { + const count = files().length + if (count === 0) return "" + return `${count} ${i18n.t(count > 1 ? "ui.common.file.other" : "ui.common.file.one")}` + }) + + return ( + + + 0}> + setExpanded(Array.isArray(value) ? value : value ? [value] : [])} + > + + {(file) => { + const active = createMemo(() => expanded().includes(file.filePath)) + const [visible, setVisible] = createSignal(false) + + createEffect(() => { + if (!active()) { + setVisible(false) + return + } + + requestAnimationFrame(() => { + if (!active()) return + setVisible(true) + }) + }) + + return ( + + + +
+
+ +
+ + {`\u202A${getDirectory(file.relativePath)}\u202C`} + + {getFilename(file.relativePath)} +
+
+
+ + + + {i18n.t("ui.patch.action.created")} + + + + + {i18n.t("ui.patch.action.deleted")} + + + + + {i18n.t("ui.patch.action.moved")} + + + + + + + +
+
+
+
+ + +
+ +
+
+
+
+ ) + }} +
+
+
+
+ + } + > +
+ +
+
+ + + + + {getFilename(single()!.relativePath)} + +
+ +
+ {getDirectory(single()!.relativePath)} +
+
+
+
+ + + +
+
+ } + > + + + + {i18n.t("ui.patch.action.created")} + + + + + {i18n.t("ui.patch.action.deleted")} + + + + + {i18n.t("ui.patch.action.moved")} + + + + + + + } + > +
+ +
+
+
+ + + ) + }, +}) + +ToolRegistry.register({ + name: "todowrite", + render(props) { + const i18n = useI18n() + const todos = createMemo(() => { + const meta = props.metadata?.todos + if (Array.isArray(meta)) return meta + + const input = props.input.todos + if (Array.isArray(input)) return input + + return [] + }) + + const subtitle = createMemo(() => { + const list = todos() + if (list.length === 0) return "" + return `${list.filter((t: Todo) => t.status === "completed").length}/${list.length}` + }) + + return ( + + +
+ + {(todo: Todo) => ( + + + {todo.content} + + + )} + +
+
+
+ ) + }, +}) + +ToolRegistry.register({ + name: "question", + render(props) { + const i18n = useI18n() + const questions = createMemo(() => (props.input.questions ?? []) as QuestionInfo[]) + const answers = createMemo(() => (props.metadata.answers ?? []) as QuestionAnswer[]) + const completed = createMemo(() => answers().length > 0) + + const subtitle = createMemo(() => { + const count = questions().length + if (count === 0) return "" + if (completed()) return i18n.t("ui.question.subtitle.answered", { count }) + return `${count} ${i18n.t(count > 1 ? "ui.common.question.other" : "ui.common.question.one")}` + }) + + return ( + + +
+ + {(q, i) => { + const answer = () => answers()[i()] ?? [] + return ( +
+
{q.question}
+
{answer().join(", ") || i18n.t("ui.question.answer.none")}
+
+ ) + }} +
+
+
+
+ ) + }, +}) + +ToolRegistry.register({ + name: "skill", + render(props) { + const i18n = useI18n() + // AMICODE: label the kind, then name it. `ui.tool.skill` used to be reachable only as a + // fallback for a missing input.name — which never happens — so an activated skill rendered + // as a bare word ("brainstorming") indistinguishable from any other tool. The kind now + // always renders, with the skill's own name beside it. + const name = createMemo(() => props.input.name?.trim() || "") + const body = createMemo(() => skillBody(props.output)) + // AMICODE: the skill tool result carries its base dir in metadata — the chip + // links to the SKILL.md source file (opens in the editor via the bridge). + const skillPath = createMemo(() => { + const dir = props.metadata?.dir + return typeof dir === "string" && dir !== "" ? `${dir}/SKILL.md` : undefined + }) + + const trigger = () => ( +
+
+ +
+
+ ) + + return ( + + + {/* AMICODE: an opened skill is a FILE, not more conversation. tool-output carries no + surface of its own, so the instructions flowed as bare prose; amc-skill-file gives + them a bounded, tinted panel. The chip above stays flush with the other Amico chips + — shared left alignment is the transcript's spine, and indenting the row broke it. */} +
+ +
+
+
+ ) + }, +}) diff --git a/packages/app-bundle/overlay/packages/session-ui/src/components/session-retry.tsx b/packages/app-bundle/overlay/packages/session-ui/src/components/session-retry.tsx new file mode 100644 index 00000000..4d34b747 --- /dev/null +++ b/packages/app-bundle/overlay/packages/session-ui/src/components/session-retry.tsx @@ -0,0 +1,74 @@ +import { AmicoSpinner } from "@opencode-ai/ui/amico-spinner" +import { createEffect, createMemo, createSignal, on, onCleanup, Show } from "solid-js" +import type { SessionStatus } from "@opencode-ai/sdk/v2/client" +import { useI18n } from "@opencode-ai/ui/context/i18n" +import { Card } from "@opencode-ai/ui/card" +import { Tooltip } from "@opencode-ai/ui/tooltip" + +export function SessionRetry(props: { status: SessionStatus; show?: boolean }) { + const i18n = useI18n() + const retry = createMemo(() => { + if (props.status.type !== "retry") return + return props.status + }) + const [seconds, setSeconds] = createSignal(0) + createEffect( + on(retry, (current) => { + if (!current) return + const update = () => { + const next = retry()?.next + if (!next) return + setSeconds(Math.round((next - Date.now()) / 1000)) + } + update() + const timer = setInterval(update, 1000) + onCleanup(() => clearInterval(timer)) + }), + ) + const message = createMemo(() => { + const current = retry() + if (!current) return "" + if (current.message.includes("exceeded your current quota") && current.message.includes("gemini")) { + return i18n.t("ui.sessionTurn.retry.geminiHot") + } + if (current.message.length > 80) return current.message.slice(0, 80) + "..." + return current.message + }) + const truncated = createMemo(() => { + const current = retry() + if (!current) return false + return current.message.length > 80 + }) + const info = createMemo(() => { + const current = retry() + if (!current) return "" + const count = Math.max(0, seconds()) + const delay = count > 0 ? i18n.t("ui.sessionTurn.retry.inSeconds", { seconds: count }) : "" + const retrying = i18n.t("ui.sessionTurn.retry.retrying") + const line = [retrying, delay].filter(Boolean).join(" ") + if (!line) return i18n.t("ui.sessionTurn.retry.attempt", { attempt: current.attempt }) + return i18n.t("ui.sessionTurn.retry.attemptLine", { line, attempt: current.attempt }) + }) + + return ( + +
+ +
+ +
+ {message()}
}> + +
+ {message()} +
+
+ + {(line) =>
{line()}
}
+
+
+ + +
+ ) +} diff --git a/packages/app-bundle/overlay/packages/session-ui/src/components/session-turn.css b/packages/app-bundle/overlay/packages/session-ui/src/components/session-turn.css new file mode 100644 index 00000000..e4f9d6be --- /dev/null +++ b/packages/app-bundle/overlay/packages/session-ui/src/components/session-turn.css @@ -0,0 +1,252 @@ +[data-component="session-turn"] { + --sticky-header-height: calc(var(--session-title-height, 0px) + 24px); + height: 100%; + min-height: 0; + min-width: 0; + display: flex; + align-items: flex-start; + justify-content: flex-start; + + [data-slot="session-turn-content"] { + flex-grow: 1; + width: 100%; + height: 100%; + min-width: 0; + overflow-y: auto; + scrollbar-width: none; + } + + [data-slot="session-turn-content"]::-webkit-scrollbar { + display: none; + } + + [data-slot="session-turn-message-container"] { + display: flex; + flex-direction: column; + align-items: flex-start; + align-self: stretch; + min-width: 0; + gap: 0px; + overflow-anchor: none; + } + + [data-slot="session-turn-message-content"] { + margin-top: 0; + width: 100%; + min-width: 0; + max-width: 100%; + } + + [data-slot="session-turn-compaction"] { + width: 100%; + min-width: 0; + align-self: stretch; + } + + [data-slot="session-turn-thinking"] { + display: flex; + /* baseline: aligns the heading with the thinking block's VERB row — the + block's first baseline is the verb's by construction (thinking-line.tsx), + so a center-aligned two-row block never leaves the heading floating + between the verb and meta rows */ + align-items: baseline; + gap: 8px; + margin-top: 12px; + width: 100%; + min-width: 0; + color: var(--text-weak); + font-family: var(--font-family-sans); + font-size: var(--font-size-base); + font-weight: var(--font-weight-medium); + line-height: 20px; + min-height: 20px; + + /* the indicator block is unsqueezable — the sibling heading truncates + instead (TextReveal truncate). The old flex row once let the heading + squeeze the meta until it shattered mid-phrase across three lines. */ + > [data-slot="amc-thinking"] { + flex-shrink: 0; + } + + [data-component="spinner"] { + width: 16px; + height: 16px; + } + } + + [data-component="text-reveal"].session-turn-thinking-heading { + flex: 1 1 auto; + min-width: 0; + color: var(--text-weaker); + font-weight: var(--font-weight-regular); + } + + .error-card { + color: var(--v2-text-text-base); + max-height: 240px; + white-space: pre-wrap; + overflow-wrap: anywhere; + word-break: break-word; + overflow-y: auto; + } + + [data-slot="session-turn-assistant-content"] { + width: 100%; + min-width: 0; + display: flex; + flex-direction: column; + align-self: stretch; + gap: 12px; + } + + [data-slot="session-turn-diffs"] { + width: 100%; + min-width: 0; + } + + [data-slot="session-turn-diffs-header"] { + display: flex; + align-items: center; + gap: 8px; + padding-top: 4px; + padding-bottom: 12px; + position: sticky; + top: var(--sticky-accordion-top, 0px); + z-index: 20; + background-color: var(--v2-background-bg-base); + height: 44px; + } + + [data-slot="session-turn-diffs-label"] { + font-variant-numeric: tabular-nums; + color: var(--text-strong); + font-family: var(--font-family-sans); + font-size: var(--font-size-base); + font-weight: var(--font-weight-medium); + line-height: var(--line-height-large); + } + + [data-slot="session-turn-diffs-toggle"] { + color: var(--text-interactive-base); + font-family: var(--font-family-sans); + font-size: var(--font-size-base); + font-weight: var(--font-weight-regular); + line-height: var(--line-height-large); + cursor: pointer; + opacity: 0; + transition: opacity 0.15s ease; + margin-left: 4px; + } + + [data-component="session-turn-diffs-group"]:hover [data-slot="session-turn-diffs-toggle"] { + opacity: 1; + } + + [data-component="session-turn-diffs-group"][data-show-all] [data-slot="session-turn-diffs-toggle"] { + opacity: 1; + } + + [data-slot="session-turn-diffs-more"] { + color: var(--text-weak); + font-family: var(--font-family-sans); + font-size: var(--font-size-small); + line-height: var(--line-height-large); + margin-top: 12px; + padding: 0 0 6px; + cursor: pointer; + transition: color 0.15s ease; + + &:hover { + color: var(--text-link-base); + } + } + + [data-component="session-turn-diffs-content"] { + padding-top: 0px; + display: flex; + flex-direction: column; + } + + [data-slot="session-turn-diff-trigger"] { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + width: 100%; + min-width: 0; + } + + [data-slot="session-turn-diff-path"] { + display: flex; + flex-grow: 1; + min-width: 0; + + font-family: var(--font-family-sans); + font-size: var(--font-size-small); + line-height: var(--line-height-large); + } + + [data-slot="session-turn-diff-directory"] { + color: var(--text-base); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + direction: rtl; + text-align: left; + } + + [data-slot="session-turn-diff-filename"] { + min-width: 0; + color: var(--text-strong); + font-weight: var(--font-weight-medium); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + [data-slot="session-turn-diff-meta"] { + flex-shrink: 0; + display: inline-flex; + align-items: center; + gap: 10px; + } + + [data-slot="session-turn-diff-chevron"] { + display: inline-flex; + color: var(--icon-weaker); + transform: rotate(-90deg); + transition: transform 0.15s ease; + } + + [data-slot="accordion-item"][data-expanded] [data-slot="session-turn-diff-chevron"] { + transform: rotate(0deg); + } + + [data-slot="session-turn-diff-view"] { + background-color: var(--surface-inset-base); + width: 100%; + min-width: 0; + overflow-y: auto; + overflow-x: hidden; + scrollbar-width: none; + -ms-overflow-style: none; + } + + [data-slot="session-turn-diff-view"]::-webkit-scrollbar { + display: none; + } +} + +body:not([data-new-layout]) [data-component="session-turn"] { + .error-card { + color: var(--text-on-critical-base); + } + + [data-slot="session-turn-diffs-header"] { + background-color: var(--background-stronger); + } +} + +[data-slot="session-turn-list"] { + gap: 24px; +} diff --git a/packages/app-bundle/overlay/packages/session-ui/src/components/tool-error-card.tsx b/packages/app-bundle/overlay/packages/session-ui/src/components/tool-error-card.tsx new file mode 100644 index 00000000..11c6ab82 --- /dev/null +++ b/packages/app-bundle/overlay/packages/session-ui/src/components/tool-error-card.tsx @@ -0,0 +1,163 @@ +import { type ComponentProps, createMemo, Show, splitProps } from "solid-js" +import { createStore } from "solid-js/store" +import { Card, CardDescription } from "@opencode-ai/ui/card" +import { Collapsible } from "@opencode-ai/ui/collapsible" +import { Icon } from "@opencode-ai/ui/icon" +import { IconButton } from "@opencode-ai/ui/icon-button" +import { Tooltip } from "@opencode-ai/ui/tooltip" +import { useI18n } from "@opencode-ai/ui/context/i18n" +import { copyTextToClipboard } from "../util/clipboard" + +export interface ToolErrorCardProps extends Omit, "children" | "variant"> { + tool: string + error: string + title?: string + defaultOpen?: boolean + open?: boolean + onOpenChange?: (open: boolean) => void + subtitle?: string + href?: string + onSubtitleClick?: (event: MouseEvent) => void +} + +export function ToolErrorCard(props: ToolErrorCardProps) { + const i18n = useI18n() + const [state, setState] = createStore({ + open: props.defaultOpen ?? false, + copied: false, + }) + const open = () => props.open ?? state.open + const copied = () => state.copied + const [split, rest] = splitProps(props, [ + "tool", + "error", + "title", + "defaultOpen", + "open", + "onOpenChange", + "subtitle", + "href", + "onSubtitleClick", + ]) + const setOpen = (value: boolean) => { + if (props.open === undefined) setState("open", value) + props.onOpenChange?.(value) + } + const name = createMemo(() => { + if (split.title) return split.title + const map: Record = { + read: "ui.tool.read", + list: "ui.tool.list", + glob: "ui.tool.glob", + grep: "ui.tool.grep", + task: "ui.tool.task", + webfetch: "ui.tool.webfetch", + websearch: "ui.tool.websearch", + bash: "ui.tool.shell", + shell: "ui.tool.shell", + patch: "ui.tool.patch", + apply_patch: "ui.tool.patch", + question: "ui.tool.questions", + } + const key = map[split.tool] + if (!key) return split.tool + if (!key.includes(".")) return key + return i18n.t(key) + }) + const cleaned = createMemo(() => split.error.replace(/^Error:\s*/, "").trim()) + const tail = createMemo(() => { + const value = cleaned() + const prefix = `${split.tool} ` + if (value.startsWith(prefix)) return value.slice(prefix.length) + return value + }) + + const subtitle = createMemo(() => { + if (split.subtitle) return split.subtitle + const parts = tail().split(": ") + if (parts.length <= 1) return i18n.t("ui.toolErrorCard.failed") + const head = (parts[0] ?? "").trim() + if (!head) return i18n.t("ui.toolErrorCard.failed") + return head[0] ? head[0].toUpperCase() + head.slice(1) : i18n.t("ui.toolErrorCard.failed") + }) + + const body = createMemo(() => { + const parts = tail().split(": ") + if (parts.length <= 1) return cleaned() + return parts.slice(1).join(": ").trim() || cleaned() + }) + + const copy = async () => { + const text = cleaned() + if (!text) return + await copyTextToClipboard(text) + setState("copied", true) + setTimeout(() => setState("copied", false), 2000) + } + + return ( + + + +
+ + +
+
+ +
+ +
+ + e.preventDefault()} + onClick={(e) => { + e.stopPropagation() + void copy() + }} + aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.toolErrorCard.copyError")} + /> + +
+
+ {(value) => {value()}} +
+
+
+
+ ) +} diff --git a/packages/app-bundle/overlay/packages/session-ui/src/components/tool-status-title.tsx b/packages/app-bundle/overlay/packages/session-ui/src/components/tool-status-title.tsx new file mode 100644 index 00000000..94e1670a --- /dev/null +++ b/packages/app-bundle/overlay/packages/session-ui/src/components/tool-status-title.tsx @@ -0,0 +1,131 @@ +import { Show, createEffect, createMemo, on, onCleanup } from "solid-js" +import { createStore } from "solid-js/store" + +function common(active: string, done: string) { + const a = Array.from(active) + const b = Array.from(done) + let i = 0 + while (i < a.length && i < b.length && a[i] === b[i]) i++ + return { + prefix: a.slice(0, i).join(""), + active: a.slice(i).join(""), + done: b.slice(i).join(""), + } +} + +function contentWidth(el: HTMLSpanElement | undefined) { + if (!el) return + return `${Math.ceil(el.getBoundingClientRect().width)}px` +} + +export function ToolStatusTitle(props: { + active: boolean + activeText: string + doneText: string + class?: string + split?: boolean +}) { + const split = createMemo(() => common(props.activeText, props.doneText)) + const suffix = createMemo( + () => (props.split ?? true) && split().prefix.length >= 2 && split().active.length > 0 && split().done.length > 0, + ) + const activeTail = createMemo(() => (suffix() ? split().active : props.activeText)) + const doneTail = createMemo(() => (suffix() ? split().done : props.doneText)) + + const [state, setState] = createStore({ + active: props.active, + animating: false, + width: undefined as string | undefined, + }) + const width = () => state.width + const active = () => state.active + const animating = () => state.animating + let activeRef: HTMLSpanElement | undefined + let doneRef: HTMLSpanElement | undefined + let widthRef: HTMLSpanElement | undefined + let frame: number | undefined + let finishTimer: ReturnType | undefined + + const finish = () => { + if (frame !== undefined) cancelAnimationFrame(frame) + if (finishTimer !== undefined) clearTimeout(finishTimer) + frame = undefined + finishTimer = undefined + setState("animating", false) + setState("width", undefined) + } + + const animate = () => { + const first = contentWidth(widthRef) + const next = props.active + finish() + setState("active", next) + if (!first) return + + setState("animating", true) + setState("width", first) + frame = requestAnimationFrame(() => { + frame = undefined + const last = contentWidth(next ? activeRef : doneRef) + if (!last) { + finish() + return + } + if (first !== last) setState("width", last) + finishTimer = setTimeout(finish, 600) + }) + } + + createEffect(on([() => props.active, activeTail, doneTail], () => animate(), { defer: true })) + + onCleanup(() => { + finish() + }) + + return ( + + + + + {activeTail()} + + + + + {doneTail()} + + + + } + > + + + {split().prefix} + + + + + {activeTail()} + + + + + {doneTail()} + + + + + + + ) +} diff --git a/packages/app-bundle/overlay/packages/session-ui/src/util/clipboard.ts b/packages/app-bundle/overlay/packages/session-ui/src/util/clipboard.ts new file mode 100644 index 00000000..87f3a852 --- /dev/null +++ b/packages/app-bundle/overlay/packages/session-ui/src/util/clipboard.ts @@ -0,0 +1,34 @@ +// Clipboard write that survives the Amicode VS Code webview. Inside the +// sandboxed chat iframe, navigator.clipboard and execCommand("copy") never +// reach the OS clipboard — the extension host owns it, so post the +// "clipboard-write" envelope over the same bridge the app uses +// (prompt-input/clipboard-bridge.ts). Unframed (desktop shell, plain +// browser), the native paths stand. session-ui twin of the app's helper +// (@opencode-ai/ui exports only components/*.tsx, so util/clipboard is +// unreachable across the package boundary). +export async function copyTextToClipboard(text: string): Promise { + if (typeof window !== "undefined" && window.parent !== window) { + window.parent.postMessage({ source: "amicode", kind: "clipboard-write", text }, "*") + return true // fire-and-forget, matching the app's writeClipboardViaBridge + } + const body = typeof document === "undefined" ? undefined : document.body + if (body) { + const textarea = document.createElement("textarea") + textarea.value = text + textarea.setAttribute("readonly", "") + textarea.style.position = "fixed" + textarea.style.opacity = "0" + textarea.style.pointerEvents = "none" + body.appendChild(textarea) + textarea.select() + const copied = document.execCommand("copy") + body.removeChild(textarea) + if (copied) return true + } + const clipboard = typeof navigator === "undefined" ? undefined : navigator.clipboard + if (!clipboard?.writeText) return false + return clipboard.writeText(text).then( + () => true, + () => false, + ) +} diff --git a/packages/app-bundle/overlay/packages/session-ui/src/v2/components/prompt-input/attachments.ts b/packages/app-bundle/overlay/packages/session-ui/src/v2/components/prompt-input/attachments.ts new file mode 100644 index 00000000..ac55b24e --- /dev/null +++ b/packages/app-bundle/overlay/packages/session-ui/src/v2/components/prompt-input/attachments.ts @@ -0,0 +1,287 @@ +import { onMount } from "solid-js" +import { makeEventListener } from "@solid-primitives/event-listener" +import type { PromptInputV2Attachment, PromptInputV2Prompt } from "./types" + +const accepted = [ + "image/png", + "image/jpeg", + "image/gif", + "image/webp", + "application/pdf", + "text/*", + "application/json", + "application/ld+json", + "application/toml", + "application/x-toml", + "application/x-yaml", + "application/xml", + "application/yaml", + ".c", + ".cc", + ".cjs", + ".conf", + ".cpp", + ".css", + ".csv", + ".cts", + ".env", + ".go", + ".gql", + ".graphql", + ".h", + ".hh", + ".hpp", + ".htm", + ".html", + ".ini", + ".java", + ".js", + ".json", + ".jsx", + ".log", + ".md", + ".mdx", + ".mjs", + ".mts", + ".py", + ".rb", + ".rs", + ".sass", + ".scss", + ".sh", + ".sql", + ".toml", + ".ts", + ".tsx", + ".txt", + ".xml", + ".yaml", + ".yml", + ".zsh", +] + +type PromptTarget = { + current: () => PromptInputV2Prompt + cursor: () => number | undefined + set: (prompt: PromptInputV2Prompt, cursor?: number) => void +} + +export type PromptInputV2AttachmentConfig = { + picker?: ( + options: { defaultPath?: string; multiple?: boolean; accept?: string[] }, + onFile: (file: File) => Promise, + ) => Promise + directory: () => string + isDialogActive: () => boolean + warn: () => void + duplicate: () => void + onError: (error: unknown) => void + readClipboardImage?: () => Promise + /** Clipboard-text reader for framed hosts where the paste event delivers no + * data (the Amicode VS Code webview — see app's prompt-input/clipboard-bridge). + * Resolves "" when unavailable. */ + readClipboardText?: () => Promise + getPathForFile?: (file: File) => string + store?: (file: File) => Promise<{ id: string; url: string }> +} + +export function createPromptInputV2Attachments( + input: PromptInputV2AttachmentConfig & { + capture: () => PromptTarget + editor: () => HTMLElement | undefined + focusEditor: () => void + addPart: (part: PromptInputV2Prompt[number]) => boolean + setDraggingType: (type: "image" | "@mention" | null) => void + }, +) { + const capture = () => { + const prompt = input.capture() + const editor = input.editor() + if (!editor) return + return { prompt, cursor: prompt.cursor() ?? cursorPosition(editor) } + } + const add = async (file: File, toast = true, target = capture(), clipboard = false) => { + if (!target) return false + const mime = await attachmentMime(file) + if (!mime) { + if (toast) input.warn() + return false + } + const blob = input.store ? await input.store(file) : await blobReference(file) + const sourcePath = input.getPathForFile?.(file) || undefined + // Native clipboard images arrive with a fresh timestamped filename on every paste, so identical + // clipboard content is matched on bytes alone. + const duplicate = target.prompt + .current() + .some( + (part) => + part.type === "image" && + part.blob.id === blob.id && + (sourcePath + ? part.sourcePath === sourcePath + : !part.sourcePath && (clipboard || part.filename === file.name)), + ) + if (duplicate) { + input.duplicate() + return true + } + const attachment: PromptInputV2Attachment = { + type: "image", + id: globalThis.crypto?.randomUUID?.() ?? Math.random().toString(16).slice(2), + filename: file.name, + sourcePath, + mime, + blob, + } + target.prompt.set([...target.prompt.current(), attachment], target.cursor) + return true + } + const addAttachments = async (files: File[], toast = true, target = capture()) => { + const found = await files.reduce(async (result, file) => { + const previous = await result + return (await add(file, false, target)) || previous + }, Promise.resolve(false)) + if (!found && files.length > 0 && toast) input.warn() + return found + } + const handlePaste = async (event: ClipboardEvent) => { + const clipboardData = event.clipboardData + if (!clipboardData) return + const target = capture() + if (!target) return + event.preventDefault() + event.stopPropagation() + const files = Array.from(clipboardData.items).flatMap((item) => { + if (item.kind !== "file") return [] + const file = item.getAsFile() + return file ? [file] : [] + }) + if (files.length > 0) { + await addAttachments(files, true, target) + return + } + let plainText = clipboardData.getData("text/plain") ?? "" + if (input.readClipboardImage && !plainText) { + const file = await input.readClipboardImage() + if (file && (await add(file, true, target, true))) return + } + // Framed host fallback (the Amicode webview): the paste event fired but + // carried nothing readable, so ask the host clipboard before giving up. + if (!plainText && input.readClipboardText) { + plainText = await input.readClipboardText() + } + if (!plainText) return + const text = plainText.includes("\r") ? plainText.replace(/\r\n?/g, "\n") : plainText + const put = () => { + if (input.addPart({ type: "text", content: text, start: 0, end: 0 })) return true + input.focusEditor() + return input.addPart({ type: "text", content: text, start: 0, end: 0 }) + } + if (text.includes("\n") || largePaste(text)) { + put() + return + } + if (typeof document.execCommand === "function" && document.execCommand("insertText", false, text)) return + put() + } + const handleDrop = async (event: DragEvent) => { + if (input.isDialogActive()) return + event.preventDefault() + input.setDraggingType(null) + const plainText = event.dataTransfer?.getData("text/plain") + if (plainText?.startsWith("file:")) { + const path = plainText.slice("file:".length) + input.focusEditor() + input.addPart({ type: "file", path, content: `@${path}`, start: 0, end: 0 }) + return + } + const files = event.dataTransfer?.files + if (files) await addAttachments(Array.from(files)) + } + + onMount(() => { + makeEventListener(document, "dragover", (event) => { + if (input.isDialogActive()) return + event.preventDefault() + if (event.dataTransfer?.types.includes("Files")) input.setDraggingType("image") + else if (event.dataTransfer?.types.includes("text/plain")) input.setDraggingType("@mention") + }) + makeEventListener(document, "dragleave", (event) => { + if (!input.isDialogActive() && !event.relatedTarget) input.setDraggingType(null) + }) + makeEventListener(document, "drop", handleDrop) + }) + + return { + addAttachments, + handlePaste, + handleDrop, + pick(fallback: () => void) { + if (!input.picker) { + fallback() + return + } + void input + .picker({ defaultPath: input.directory(), multiple: true, accept: accepted }, (file) => add(file)) + .catch(input.onError) + }, + } +} + +const imageMimes = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]) + +async function blobReference(file: File) { + const id = Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", await file.arrayBuffer()))) + .map((byte) => byte.toString(16).padStart(2, "0")) + .join("") + return { id, url: URL.createObjectURL(file) } +} +const imageExtensions = new Map([ + ["gif", "image/gif"], + ["jpeg", "image/jpeg"], + ["jpg", "image/jpeg"], + ["png", "image/png"], + ["webp", "image/webp"], +]) +const textMimes = new Set([ + "application/json", + "application/ld+json", + "application/toml", + "application/x-toml", + "application/x-yaml", + "application/xml", + "application/yaml", +]) + +async function attachmentMime(file: File) { + const type = file.type.split(";", 1)[0]?.trim().toLowerCase() ?? "" + if (imageMimes.has(type) || type === "application/pdf") return type + const index = file.name.lastIndexOf(".") + const suffix = index === -1 ? "" : file.name.slice(index + 1).toLowerCase() + const fallback = imageExtensions.get(suffix) ?? (suffix === "pdf" ? "application/pdf" : undefined) + if ((!type || type === "application/octet-stream") && fallback) return fallback + if (type.startsWith("text/") || textMimes.has(type) || type.endsWith("+json") || type.endsWith("+xml")) { + return "text/plain" + } + const bytes = new Uint8Array(await file.slice(0, 4096).arrayBuffer()) + if (bytes.some((byte) => byte === 0)) return + const control = bytes.filter((byte) => byte < 9 || (byte > 13 && byte < 32)).length + if (bytes.length > 0 && control / bytes.length > 0.3) return + return "text/plain" +} + +function cursorPosition(editor: HTMLElement) { + const selection = window.getSelection() + if (!selection || selection.rangeCount === 0) return 0 + const range = selection.getRangeAt(0) + if (!editor.contains(range.startContainer)) return 0 + const before = range.cloneRange() + before.selectNodeContents(editor) + before.setEnd(range.startContainer, range.startOffset) + return before.toString().replace(/\u200B/g, "").length +} + +function largePaste(text: string) { + if (text.length >= 8000) return true + return text.split("\n").length - 1 >= 120 +} diff --git a/packages/app-bundle/overlay/packages/session-ui/src/v2/components/prompt-input/index.tsx b/packages/app-bundle/overlay/packages/session-ui/src/v2/components/prompt-input/index.tsx new file mode 100644 index 00000000..3fe5b197 --- /dev/null +++ b/packages/app-bundle/overlay/packages/session-ui/src/v2/components/prompt-input/index.tsx @@ -0,0 +1,724 @@ +import { createEffect, createMemo, For, Show, type Accessor, type JSX } from "solid-js" +import { FileIcon } from "@opencode-ai/ui/file-icon" +import { Icon } from "@opencode-ai/ui/icon" +import { IconButton } from "@opencode-ai/ui/icon-button" +import { ProviderIcon } from "@opencode-ai/ui/provider-icon" +import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" +import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" +import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" +import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2" +import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2" +import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" +import { AttachmentCardV2 } from "../attachment-card-v2" +import { CommentCardV2 } from "../comment-card-v2" +import { typeLabel } from "../../../components/message-file" +import type { + PromptInputV2Attachment, + PromptInputV2Comment, + PromptInputV2Option, + PromptInputV2PersistedState, + PromptInputV2Prompt, + PromptInputV2Suggestion, +} from "./types" +import type { PromptInputV2Interaction, PromptInputV2SelectControl } from "./interaction" +import "./attachments.css" + +export type { + PromptInputV2Attachment, + PromptInputV2Comment, + PromptInputV2Option, + PromptInputV2PersistedState, + PromptInputV2Suggestion, +} from "./types" + +export type PromptInputV2Mode = "normal" | "shell" + +export type PromptInputV2Props = { + controller: PromptInputV2Interaction + disabled?: boolean + readOnly?: boolean + borderUnderlay?: boolean + class?: string + modelControl?: JSX.Element + /** + * Optional control rendered in the bottom row's right anchor, immediately + * left of the send button (amicode/opencode#116: the report-a-bug button). + * Follows the modelControl optional-control pattern: pass `undefined` and + * nothing renders — the row's layout does not shift. + */ + trailingControl?: JSX.Element + variantControlVisible?: boolean + attachKeybind?: string[] + attachShortcut?: string +} + +export function PromptInputV2(props: PromptInputV2Props) { + const state = props.controller.state + const view = props.controller.view + let editor: HTMLDivElement | undefined + let localInput = false + const updateCursor = () => { + if (!editor || !window.getSelection()?.isCollapsed) return + props.controller.onCursor(promptInputV2Cursor(editor)) + } + const mode = createMemo(() => state.mode) + const buttons = createMemo(() => ({ + opacity: mode() === "normal" ? 1 : 0, + "pointer-events": mode() === "normal" ? ("auto" as const) : ("none" as const), + transition: "opacity 200ms ease", + })) + + createEffect(() => { + const parts = props.controller.parts() + if (!editor) return + if (localInput) { + localInput = false + return + } + renderPromptInputV2Editor(editor, parts) + }) + + return ( +
+ { + const list = event.currentTarget.files + if (list) props.controller.addAttachments(Array.from(list)) + event.currentTarget.value = "" + }} + /> + + props.controller.dispatch({ type: "popover.active", id: item.id })} + onSelect={(item) => props.controller.dispatch({ type: "popover.select", item })} + /> + +
{ + event.preventDefault() + if (!props.disabled) props.controller.submit() + }} + onDragEnter={props.controller.onDragEnter} + onDragOver={props.controller.onDragOver} + onDragLeave={props.controller.onDragLeave} + onDrop={props.controller.onDrop} + > + +
+ Drop files to attach +
+
+ + + props.controller.removeAttachment(attachment.id)} + onCommentClick={(comment) => props.controller.toggleContext(comment.key)} + onCommentRemove={(comment) => props.controller.removeContext(comment.key)} + /> + + +
+
{ + editor = element + props.controller.setEditor(element) + renderPromptInputV2Editor(element, props.controller.parts()) + }} + data-component="prompt-input" + role="textbox" + aria-multiline="true" + aria-label="Prompt" + contenteditable={!props.disabled && !props.readOnly} + autocapitalize={state.mode === "normal" ? "sentences" : "off"} + autocorrect={state.mode === "normal" ? "on" : "off"} + spellcheck={state.mode === "normal"} + // @ts-expect-error + autocomplete="off" + class="relative z-10 block min-h-[60px] max-h-[180px] w-full overflow-y-auto whitespace-pre-wrap bg-transparent px-4 pt-4 pb-2 text-[13px] font-[440] leading-5 text-v2-text-text-base focus:outline-none empty:before:content-['\200B'] [&_[data-mention=file]]:text-syntax-property [&_[data-mention=agent]]:text-syntax-type [&_[data-mention=reference]]:text-syntax-keyword" + classList={{ "font-mono!": state.mode === "shell", "opacity-50": props.disabled }} + onInput={(event) => { + const cursor = promptInputV2Cursor(event.currentTarget) + const prompt = parsePromptInputV2Editor(event.currentTarget) + const images = props.controller.parts().filter((part) => part.type === "image") + localInput = true + props.controller.onInput(prompt.map((part) => part.content).join(""), [...prompt, ...images], cursor) + }} + onKeyDown={(event) => { + if (props.controller.onKeyDown(event)) return + if (event.key === "Enter" && !event.shiftKey && !event.isComposing) { + event.preventDefault() + if (event.repeat) return + props.controller.submit() + } + }} + onKeyUp={updateCursor} + onPointerUp={updateCursor} + onPaste={props.controller.onPaste} + onFocus={() => props.controller.dispatch({ type: "focus.editor" })} + /> + +
+ {view.placeholder?.() ?? + (state.mode === "shell" ? "Enter shell command..." : "Ask anything, / for commands, @ for context...")} +
+
+
+ +
+
+ + + {(control) => ( + + )} + + + {(control) => ( + + )} + + } + > + {props.modelControl} + + + {(control) => ( + 1}> + + + )} + +
+ + {(control) =>
{control}
} +
+ +
+ +
+ ) +} + +function renderPromptInputV2Editor(editor: HTMLDivElement, prompt: PromptInputV2Prompt) { + const active = document.activeElement === editor + editor.replaceChildren( + ...prompt.flatMap((part) => { + if (part.type === "image") return [] + if (part.type === "text") return [document.createTextNode(part.content)] + const mention = document.createElement("span") + mention.textContent = part.content + mention.contentEditable = "false" + mention.dataset.mention = + part.type === "file" && part.mime === "application/x-directory" ? "reference" : part.type + if (part.type === "agent") mention.dataset.name = part.name + if (part.type === "file") { + mention.dataset.path = part.path + if (part.mime) mention.dataset.mime = part.mime + if (part.filename) mention.dataset.filename = part.filename + } + return [mention] + }), + ) + if (!active) return + const selection = window.getSelection() + const range = document.createRange() + range.selectNodeContents(editor) + range.collapse(false) + selection?.removeAllRanges() + selection?.addRange(range) +} + +function parsePromptInputV2Editor(editor: HTMLDivElement) { + const parts: Exclude[] = [] + let buffer = "" + let position = 0 + + const flush = () => { + if (!buffer) return + parts.push({ type: "text", content: buffer, start: position, end: position + buffer.length }) + position += buffer.length + buffer = "" + } + const mention = (element: HTMLElement) => { + flush() + const content = element.textContent ?? "" + if (element.dataset.mention === "agent") { + parts.push({ + type: "agent", + name: element.dataset.name ?? content.slice(1), + content, + start: position, + end: position + content.length, + }) + position += content.length + return + } + parts.push({ + type: "file", + path: element.dataset.path ?? content.slice(1), + content, + start: position, + end: position + content.length, + ...(element.dataset.mime ? { mime: element.dataset.mime } : {}), + ...(element.dataset.filename ? { filename: element.dataset.filename } : {}), + }) + position += content.length + } + const visit = (node: Node) => { + if (node.nodeType === Node.TEXT_NODE) { + buffer += node.textContent ?? "" + return + } + if (!(node instanceof HTMLElement)) return + if (node.dataset.mention) { + mention(node) + return + } + if (node.tagName === "BR") { + buffer += "\n" + return + } + Array.from(node.childNodes).forEach(visit) + } + + Array.from(editor.childNodes).forEach((node, index, nodes) => { + visit(node) + if (node instanceof HTMLElement && ["DIV", "P"].includes(node.tagName) && index < nodes.length - 1) buffer += "\n" + }) + flush() + if ( + parts.every((part) => part.type === "text") && + parts.every((part) => part.content.replace(/[\n\u200B]/g, "") === "") + ) { + return [{ type: "text" as const, content: "", start: 0, end: 0 }] + } + if (parts.length > 0) return parts + return [{ type: "text" as const, content: "", start: 0, end: 0 }] +} + +function promptInputV2Cursor(editor: HTMLDivElement) { + const selection = window.getSelection() + if (!selection?.rangeCount || !editor.contains(selection.anchorNode)) return editor.textContent?.length ?? 0 + const range = selection.getRangeAt(0).cloneRange() + range.selectNodeContents(editor) + range.setEnd(selection.anchorNode!, selection.anchorOffset) + return range.toString().length +} + +export function PromptInputV2Attachments(props: { + attachments: PromptInputV2Attachment[] + comments?: PromptInputV2Comment[] + activeCommentID?: string + removeLabel: string + onAttachmentClick?: (attachment: PromptInputV2Attachment) => void + onAttachmentRemove: (attachment: PromptInputV2Attachment) => void + onCommentClick?: (comment: PromptInputV2Comment) => void + onCommentRemove?: (comment: PromptInputV2Comment) => void +}) { + return ( + 0 || (props.comments?.length ?? 0) > 0}> +
+
+ + {(comment) => ( +
+ + props.onCommentClick?.(comment)} + /> + + +
+ )} +
+ + {(attachment) => ( +
+ + + {typeLabel(attachment.filename, attachment.mime)} + + } + > + {attachment.filename} props.onAttachmentClick?.(attachment)} + /> +
+ + + +
+ )} + +
+
+
+
+ + ) +} + +export function PromptInputV2AddMenu(props: { + disabled?: boolean + title: string + keybind?: string[] + attachLabel: string + attachShortcut?: string + commandsLabel: string + contextLabel: string + shellLabel: string + onAttach: () => void + onCommands: () => void + onContext: () => void + onShell: () => void +}) { + return ( + + {props.title} + + + } + > + + } + variant="ghost-muted" + size="large" + disabled={props.disabled} + aria-label={props.title} + /> + + + + {props.attachLabel} + + + + {props.commandsLabel} + + + {props.contextLabel} + + + {props.shellLabel} + + + + + + ) +} + +function PromptInputV2ConfiguredSelect(props: { + title: string + keybind?: string[] + control: PromptInputV2SelectControl + model?: boolean +}) { + const current = () => props.control.current() + const providerID = () => props.control.options().find((option) => option.id === current())?.providerID + return ( + + + + } + onSelect={props.control.onSelect} + /> + ) +} + +export function PromptInputV2Select(props: { + title: string + keybind?: string[] + options: PromptInputV2Option[] + current: string + currentIcon?: JSX.Element + class?: string + onOpenChange?: (open: boolean) => void + onSelect: (id: string) => void +}) { + return ( + + {props.title} + + + } + > + + + {props.currentIcon} + + {props.options.find((option) => option.id === props.current)?.label ?? props.current} + + + + + + + + + + {(option) => ( + + {option.label} + + )} + + + + + + + ) +} + +export function PromptInputV2Popover(props: { + emptyLabel: string + items: PromptInputV2Suggestion[] + activeID?: string + search?: { + value: string + label: string + placeholder: string + onValueChange: (value: string) => void + onKeyDown: (event: KeyboardEvent) => void + } + onActiveChange: (item: PromptInputV2Suggestion) => void + onSelect: (item: PromptInputV2Suggestion) => void +}) { + return ( +
event.preventDefault()} + > + + {(search) => ( +
+ requestAnimationFrame(() => element.focus())} + value={search().value} + aria-label={search().label} + placeholder={search().placeholder} + class="w-full bg-transparent text-[13px] leading-5 text-v2-text-text-base outline-none placeholder:text-v2-text-text-faint" + onInput={(event) => search().onValueChange(event.currentTarget.value)} + onKeyDown={(event) => search().onKeyDown(event)} + onMouseDown={(event) => event.stopPropagation()} + /> +
+ )} +
+ 0} + fallback={
{props.emptyLabel}
} + > + + {(item) => ( + + )} + +
+
+ ) +} + +export function PromptInputV2SubmitButton(props: { + mode: PromptInputV2Mode + stopping: boolean + disabled: boolean + sendLabel: string + stopLabel: string + onSubmit: () => void + onStop: () => void +}) { + return ( + + { + event.preventDefault() + event.stopPropagation() + if (props.stopping) { + props.onStop() + return + } + props.onSubmit() + }} + /> + + ) +} + +function PromptInputV2SuggestionIcon(props: { item: PromptInputV2Suggestion }) { + if (props.item.kind === "agent") return + if (props.item.kind === "command") return null + return ( + + ) +} diff --git a/packages/app-bundle/overlay/packages/session-ui/src/v2/components/session-review-file-preview-v2.tsx b/packages/app-bundle/overlay/packages/session-ui/src/v2/components/session-review-file-preview-v2.tsx new file mode 100644 index 00000000..c62a32f7 --- /dev/null +++ b/packages/app-bundle/overlay/packages/session-ui/src/v2/components/session-review-file-preview-v2.tsx @@ -0,0 +1,304 @@ +import { getDirectory, getFilename } from "@opencode-ai/core/util/path" +import type { SelectedLineRange } from "@pierre/diffs" +import { DiffChanges } from "@opencode-ai/ui/v2/diff-changes-v2" +import { FileIcon } from "@opencode-ai/ui/file-icon" +import { useFileComponent } from "@opencode-ai/ui/context/file" +import { useI18n } from "@opencode-ai/ui/context/i18n" +import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" +import { mediaKindFromPath } from "../../pierre/media" +import { cloneSelectedLineRange, previewSelectedLines } from "../../pierre/selection-bridge" +import { copyTextToClipboard } from "../../util/clipboard" +import type { FileContent, SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" +import type { FileDiffInfo } from "@opencode-ai/client/promise" +import { createEffect, createMemo, onCleanup, Show, untrack } from "solid-js" +import { createStore } from "solid-js/store" +import { Dynamic } from "solid-js/web" +import { normalize, text, type ViewDiff } from "../../components/session-diff" +import type { + SessionReviewComment, + SessionReviewCommentActions, + SessionReviewCommentDelete, + SessionReviewCommentUpdate, + SessionReviewDiffStyle, + SessionReviewFocus, + SessionReviewLineComment, +} from "../../components/session-review" +import type { SessionReviewExpandMode } from "./session-review-v2" +import { createLineCommentControllerV2 } from "./line-comment-annotations-v2" +import { shouldVirtualizeReviewDiff } from "./session-review-file-preview-v2-virtualize" +import { LineCommentV2OverflowIcon } from "@opencode-ai/ui/v2/line-comment-v2" +import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2" +import "./session-review-v2.css" + +type ReviewDiff = (SnapshotFileDiff & { file: string }) | FileDiffInfo | VcsFileDiff + +export type SessionReviewFilePreviewV2Props = { + file: string + diff: ReviewDiff + diffStyle: SessionReviewDiffStyle + expandMode?: SessionReviewExpandMode + readFile?: (path: string) => Promise + onLineComment?: (comment: SessionReviewLineComment) => void + onLineCommentUpdate?: (comment: SessionReviewCommentUpdate) => void + onLineCommentDelete?: (comment: SessionReviewCommentDelete) => void + lineCommentActions?: SessionReviewCommentActions + comments?: SessionReviewComment[] + focusedComment?: SessionReviewFocus | null + onFocusedCommentChange?: (focus: SessionReviewFocus | null) => void +} + +function statusLabel(status: ViewDiff["status"]) { + if (status === "added") return "A" + if (status === "deleted") return "D" + return "M" +} + +function statusType(status: ViewDiff["status"]) { + if (status === "added") return "added" + if (status === "deleted") return "deleted" + return "modified" +} + +function selectionSide(range: SelectedLineRange) { + return range.endSide ?? range.side ?? "additions" +} + +function selectionPreview(diff: ViewDiff, range: SelectedLineRange) { + const side = selectionSide(range) + const contents = text(diff, side) + if (contents.length === 0) return undefined + return previewSelectedLines(contents, range) +} + +function ReviewCommentMenuV2(props: { + labels: SessionReviewCommentActions + onEdit: VoidFunction + onDelete: VoidFunction +}) { + return ( +
event.stopPropagation()} onClick={(event) => event.stopPropagation()}> + + + + + + + {props.labels.editLabel} + {props.labels.deleteLabel} + + + +
+ ) +} + +export function SessionReviewFilePreviewV2(props: SessionReviewFilePreviewV2Props) { + const i18n = useI18n() + const fileComponent = useFileComponent() + let scrollRef: HTMLDivElement | undefined + let focusToken = 0 + + const [store, setStore] = createStore({ + selection: null as SelectedLineRange | null, + commenting: null as SelectedLineRange | null, + opened: null as string | null, + }) + + const view = createMemo(() => ({ + ...normalize(props.diff), + preloaded: "preloaded" in props.diff ? props.diff.preloaded : undefined, + })) + const diffCanRender = createMemo(() => view().additions !== 0 || view().deletions !== 0) + const mediaKind = createMemo(() => mediaKindFromPath(props.file)) + const comments = createMemo(() => (props.comments ?? []).filter((comment) => comment.file === props.file)) + const commentedLines = createMemo(() => comments().map((comment) => comment.selection)) + const lineCommentsEnabled = () => props.onLineComment != null + + const commentsUi = createLineCommentControllerV2({ + comments, + label: i18n.t("ui.lineComment.submit"), + draftKey: () => props.file, + state: { + opened: () => store.opened, + setOpened: (id) => setStore("opened", id), + selected: () => store.selection, + setSelected: (range) => setStore("selection", range), + commenting: () => store.commenting, + setCommenting: (range) => setStore("commenting", range), + }, + getSide: selectionSide, + onSubmit: ({ comment, selection }) => { + props.onLineComment?.({ + file: props.file, + selection, + comment, + preview: selectionPreview(view(), selection), + }) + }, + onUpdate: ({ id, comment, selection }) => { + props.onLineCommentUpdate?.({ + id, + file: props.file, + selection, + comment, + preview: selectionPreview(view(), selection), + }) + }, + onDelete: (comment) => { + props.onLineCommentDelete?.({ + id: comment.id, + file: props.file, + }) + }, + editSubmitLabel: props.lineCommentActions?.saveLabel, + renderCommentActions: props.lineCommentActions + ? (comment, controls) => ( + + ) + : undefined, + }) + + onCleanup(() => { + focusToken++ + }) + + createEffect(() => { + const focus = props.focusedComment + if (!focus) return + if (focus.file !== props.file) { + // The focused file has no mounted preview (e.g. not in the current diff + // set); clear the focus anyway so it cannot hijack a later diff refresh. + // V1 clears unconditionally the same way. + untrack(() => { + const token = focusToken + requestAnimationFrame(() => { + if (token !== focusToken) return + props.onFocusedCommentChange?.(null) + }) + }) + return + } + + untrack(() => { + setStore("opened", focus.id) + + const comment = (props.comments ?? []).find((item) => item.file === focus.file && item.id === focus.id) + if (comment) setStore("selection", cloneSelectedLineRange(comment.selection)) + + // The diff renders asynchronously, so poll for the comment anchor before + // scrolling; clear the focus once handled so revisiting the file does not + // re-open a stale comment (mirrors the v1 review behavior). + focusToken++ + const token = focusToken + const scrollTo = (attempt: number) => { + if (token !== focusToken) return + const anchor = scrollRef?.querySelector(`[data-comment-id="${focus.id}"]`) + if (anchor instanceof HTMLElement) { + anchor.scrollIntoView({ block: "center" }) + return + } + if (attempt >= 120) return + requestAnimationFrame(() => scrollTo(attempt + 1)) + } + requestAnimationFrame(() => scrollTo(0)) + requestAnimationFrame(() => { + if (token !== focusToken) return + props.onFocusedCommentChange?.(null) + }) + }) + }) + + const expandUnchanged = () => props.expandMode === "expand" + + const diffViewer = () => ( + { + if (!lineCommentsEnabled()) return + commentsUi.onLineSelected(range) + }} + onLineSelectionEnd={(range: SelectedLineRange | null) => { + if (!lineCommentsEnabled()) return + commentsUi.onLineSelectionEnd(range) + }} + onLineNumberSelectionEnd={commentsUi.onLineNumberSelectionEnd} + annotations={commentsUi.annotations()} + renderAnnotation={commentsUi.renderAnnotation} + renderGutterUtility={lineCommentsEnabled() ? commentsUi.renderGutterUtility : undefined} + selectedLines={store.selection} + commentedLines={commentedLines()} + media={{ + mode: "auto", + path: props.file, + deleted: view().status === "deleted", + readFile: view().status === "deleted" ? undefined : props.readFile, + }} + /> + ) + + return ( + <> +
+ + +
+ {statusLabel(view().status)} +
+ + + {getFilename(props.file)} + + + + {getDirectory(props.file)} + + +
+ + + copyTextToClipboard(props.file)}>Copy full path + copyTextToClipboard(getFilename(props.file))}>Copy filename + + +
+
+ +
+
+
{ + scrollRef = el + }} + data-slot="session-review-v2-diff-scroll" + > + + {i18n.t("ui.fileMedia.binary.title")} +
+ } + > + {diffViewer()} + +
+ + ) +} diff --git a/packages/app-bundle/overlay/packages/session-ui/src/v2/components/session-review-v2.css b/packages/app-bundle/overlay/packages/session-ui/src/v2/components/session-review-v2.css new file mode 100644 index 00000000..a9892989 --- /dev/null +++ b/packages/app-bundle/overlay/packages/session-ui/src/v2/components/session-review-v2.css @@ -0,0 +1,534 @@ +[data-component="session-review-v2"] { + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; + overflow: hidden; + background: var(--v2-background-bg-base); +} + +[data-component="session-review-v2"] [data-slot="session-review-v2-body"] { + display: flex; + flex: 1; + min-height: 0; + overflow: hidden; +} + +[data-component="session-review-v2-sidebar-root"] { + position: relative; + display: flex; + flex-shrink: 0; + min-width: 0; + min-height: 0; + height: 100%; + overflow: hidden; +} + +[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar"] { + display: flex; + flex-direction: column; + flex-shrink: 0; + min-height: 0; + overflow: hidden; + border-right: 1px solid var(--border-weaker-base, var(--v2-border-border-weak)); + background: var(--v2-background-bg-base); +} + +[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar"][aria-hidden="true"] { + border-right-width: 0; +} + +[data-component="session-review-v2-sidebar-root"] + [data-slot="session-review-v2-sidebar"][data-transition]:not([data-resizing]) { + transition: + width 200ms cubic-bezier(0.22, 1, 0.36, 1), + border-right-width 200ms cubic-bezier(0.22, 1, 0.36, 1); +} + +[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-resize"] { + position: absolute; + inset-block: 0; + inset-inline-end: 0; + z-index: 20; + width: 0; + overflow: visible; +} + +[data-component="session-review-v2-sidebar-root"] [data-component="resize-handle"]::after { + background: var(--v2-border-border-muted, var(--border-weak-base)); +} + +[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-header"] { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 12px 16px 12px 8px; + flex-shrink: 0; +} + +[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-title"] { + display: flex; + align-items: center; + gap: 6px; + min-width: 0; + font-size: 13px; + font-weight: 500; + color: var(--text-strong, var(--v2-text-text-base)); +} + +[data-component="session-review-v2-sidebar-root"] + [data-slot="session-review-v2-sidebar-title"]:not(:has([data-component="select-v2-root"])) { + margin-left: 8px; +} + +[data-component="session-review-v2-sidebar-root"] + [data-slot="session-review-v2-sidebar-title"] + [data-component="select-v2-root"] { + width: fit-content; + max-width: 100%; + min-width: 0; +} + +[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-filter"] { + padding: 0 8px 4px; + flex-shrink: 0; + min-width: 0; + box-sizing: border-box; +} + +[data-component="session-review-v2-sidebar-root"] + [data-slot="session-review-v2-sidebar-filter"] + [data-component="text-input-v2"] { + width: 100%; + max-width: 100%; + min-width: 0; + margin-top: 3px; +} + +[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"] { + flex: 1; + min-height: 0; + timeline-scope: --session-review-v2-sidebar-tree-scroll; +} + +@keyframes session-review-v2-sidebar-tree-fade-top { + from { + visibility: hidden; + } + to { + visibility: visible; + } +} + +@keyframes session-review-v2-sidebar-tree-fade-bottom { + from { + visibility: visible; + } + to { + visibility: hidden; + } +} + +[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"]::before, +[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"]::after { + content: ""; + position: absolute; + inset-inline: 0; + z-index: 10; + height: 16px; + pointer-events: none; + visibility: hidden; +} + +[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"]::before { + top: 0; + background: linear-gradient(to bottom, var(--v2-background-bg-base), transparent); +} + +[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"]::after { + bottom: 0; + background: linear-gradient(to top, var(--v2-background-bg-base), transparent); +} + +@supports (animation-timeline: --session-review-v2-sidebar-tree-scroll) and + (timeline-scope: --session-review-v2-sidebar-tree-scroll) { + [data-component="session-review-v2-sidebar-root"] + [data-slot="session-review-v2-sidebar-tree"] + .scroll-view__viewport { + scroll-timeline: --session-review-v2-sidebar-tree-scroll y; + } + + [data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"]::before { + animation: session-review-v2-sidebar-tree-fade-top linear both; + animation-timeline: --session-review-v2-sidebar-tree-scroll; + animation-range: 0 0.1px; + } + + [data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"]::after { + animation: session-review-v2-sidebar-tree-fade-bottom linear both; + animation-timeline: --session-review-v2-sidebar-tree-scroll; + animation-range: calc(100% - 1.1px) calc(100% - 1px); + } +} + +[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"] .scroll-view__viewport { + padding: 4px 8px 12px; +} + +[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"] .scroll-view__thumb { + width: 16px; +} + +[data-component="session-review-v2-sidebar-root"] + [data-slot="session-review-v2-sidebar-tree"] + .scroll-view__thumb::after { + width: 6px; + background-color: var(--v2-border-border-muted, var(--border-weak-base)); +} + +[data-component="session-review-v2-sidebar-root"] + [data-slot="session-review-v2-sidebar-tree"] + .scroll-view__thumb:hover::after, +[data-component="session-review-v2-sidebar-root"] + [data-slot="session-review-v2-sidebar-tree"] + .scroll-view__thumb[data-dragging="true"]::after { + background-color: var(--v2-border-border-strong, var(--border-strong-base)); +} + +[data-component="session-review-v2"] [data-slot="session-review-v2-preview"] { + display: flex; + flex-direction: column; + flex: 1; + min-width: 0; + min-height: 0; + overflow: hidden; +} + +[data-component="session-review-v2"] [data-slot="session-review-v2-toolbar"] { + display: flex; + align-items: center; + gap: 4px; + padding: 10px 16px 10px 8px; + flex-shrink: 0; + white-space: nowrap; + border-bottom: 1px solid var(--border-weaker-base, var(--v2-border-border-weak)); +} + +[data-component="session-review-v2"] [data-slot="session-review-v2-toolbar-group"] { + display: flex; + align-items: center; + gap: 0px; + flex-shrink: 0; +} + +[data-component="session-review-v2"] .session-review-v2-toolbar-group--start { + min-width: 0; + flex-shrink: 0; +} + +[data-component="session-review-v2"] [data-slot="session-review-v2-toolbar-collapsed-meta"] { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + padding-right: 8px; +} + +[data-component="session-review-v2"] [data-slot="session-review-v2-toolbar-title"] { + display: flex; + align-items: center; + min-width: 0; + font-size: 13px; + font-weight: 500; + color: var(--text-strong, var(--v2-text-text-base)); +} + +[data-component="session-review-v2"] [data-slot="session-review-v2-toolbar-title"] [data-component="select-v2-root"] { + width: fit-content; + max-width: 100%; + min-width: 0; +} + +[data-component="session-review-v2"] [data-slot="session-review-v2-file-position"] { + flex: none; + flex-grow: 0; + font-family: var(--v2-font-family-sans); + font-style: normal; + font-weight: 440; + font-size: 11px; + line-height: 100%; + letter-spacing: 0.05px; + text-transform: uppercase; + font-variant-numeric: tabular-nums lining-nums; + font-feature-settings: + "tnum" on, + "lnum" on; + font-variation-settings: "slnt" 0; + color: var(--v2-grey-600); + white-space: nowrap; +} + +[data-component="session-review-v2"] .session-review-v2-file-nav-button { + width: 36px; + height: 28px; + min-width: 36px; + min-height: 28px; + padding: 0; +} + +[data-component="session-review-v2"] + [data-slot="session-review-v2-toolbar-group"] + [data-component="tooltip-v2-trigger"] { + display: inline-flex; +} + +[data-component="session-review-v2"] .session-review-v2-segmented-control [data-component="tooltip-v2-trigger"] { + display: flex; + flex: 0 0 auto; + min-width: 0; +} + +[data-component="session-review-v2"] + .session-review-v2-segmented-control + [data-component="tooltip-v2-trigger"] + [data-slot="segmented-control-v2-item"] { + width: 100%; +} + +[data-component="icon-button-v2"].session-review-v2-sidebar-toggle { + width: 36px; + height: 28px; + min-width: 36px; + min-height: 28px; + border-radius: 6px; + color: var(--v2-icon-icon-base); +} + +[data-component="icon-button-v2"][data-variant="ghost"].session-review-v2-sidebar-toggle[aria-expanded="true"] { + background-color: var(--v2-background-bg-layer-02); +} + +[data-component="session-review-v2"] [data-slot="session-review-v2-file-header"] { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + flex-shrink: 0; + border-bottom: 1px solid var(--border-weaker-base, var(--v2-border-border-weak)); +} + +[data-component="session-review-v2"] [data-slot="session-review-v2-file-title"] { + display: flex; + flex-direction: row; + align-items: center; + gap: 6px; + min-width: 0; + flex: 1; +} + +[data-component="session-review-v2"] [data-slot="session-review-v2-file-diff"] { + position: relative; + flex-shrink: 0; + margin-left: auto; + background-color: var(--v2-background-bg-base); + + &::before { + content: ""; + position: absolute; + top: -8px; + bottom: -8px; + right: 100%; + width: 16px; + z-index: 1; + pointer-events: none; + background: linear-gradient(90deg, transparent, var(--v2-background-bg-base)); + } + + &::after { + content: ""; + position: absolute; + inset: -8px -16px -8px 0; + z-index: 0; + pointer-events: none; + background-color: var(--v2-background-bg-base); + } + + [data-component="diff-changes"] { + position: relative; + z-index: 1; + } +} + +[data-component="session-review-v2"] [data-slot="session-review-v2-file-status"] { + display: flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + flex-shrink: 0; + font-size: 11px; + font-weight: 530; + line-height: 1; + letter-spacing: -0.04px; + text-transform: uppercase; + font-variant-numeric: tabular-nums lining-nums; +} + +[data-component="session-review-v2"] [data-slot="session-review-v2-file-status"][data-type="added"] { + color: var(--icon-diff-add-base, var(--v2-state-fg-success)); +} + +[data-component="session-review-v2"] [data-slot="session-review-v2-file-status"][data-type="deleted"] { + color: var(--icon-diff-delete-base, var(--v2-state-fg-danger)); +} + +[data-component="session-review-v2"] [data-slot="session-review-v2-file-status"][data-type="modified"] { + color: var(--v2-state-fg-info); +} + +[data-component="session-review-v2"] [data-slot="session-review-v2-file-title"] [data-component="tooltip-v2-trigger"] { + display: inline-flex; + min-width: 0; +} + +[data-component="session-review-v2"] [data-slot="session-review-v2-file-title"] [data-component="tooltip-v2-trigger"]:has([data-slot="session-review-v2-file-path"]) { + flex: 1; + min-width: 0; + overflow: hidden; +} + +[data-component="session-review-v2"] [data-slot="session-review-v2-file-name"] { + flex-shrink: 0; + font-size: 13px; + font-weight: 530; + line-height: 1; + letter-spacing: -0.04px; + font-variant-numeric: tabular-nums lining-nums; + color: var(--v2-text-text-base); + white-space: nowrap; +} + +[data-component="session-review-v2"] [data-slot="session-review-v2-file-path"] { + flex: 1; + min-width: 0; + font-size: 13px; + font-weight: 440; + line-height: 1; + letter-spacing: -0.04px; + font-variant-numeric: tabular-nums lining-nums; + color: var(--v2-text-text-muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +[data-component="session-review-v2"] [data-slot="session-review-v2-diff-scroll"] { + flex: 1; + min-height: 0; + overflow: auto; + --line-comment-z: 5; + --line-comment-popover-z: 30; + --line-comment-open-z: 6; +} + +[data-component="session-review-v2"] .session-review-v2-toolbar-group--segments { + gap: 12px; +} + +[data-component="session-review-v2"] .session-review-v2-toolbar-group--segments .session-review-v2-segmented-control { + width: auto; +} + +[data-component="session-review-v2"] + .session-review-v2-toolbar-group--segments + .session-review-v2-segmented-control--icon + [data-slot="segmented-control-v2-item"] { + flex: 0 0 auto; +} + +[data-component="session-review-v2"] [data-slot="session-review-v2-empty"] { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; + padding-bottom: 160px; +} + +[data-slot="session-review-v2-empty-no-git"] { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 16px; + padding: 24px; + padding-bottom: 160px; + text-align: center; +} + +[data-slot="session-review-v2-empty-no-git"] [data-component="file-icon"] { + width: 20px; + height: 20px; + color: var(--v2-icon-icon-muted); +} + +[data-slot="session-review-v2-empty-no-git-title"] { + flex: none; + font-size: 13px; + font-weight: 530; + line-height: 100%; + letter-spacing: -0.04px; + color: var(--v2-text-text-base); +} + +[data-slot="session-review-v2-empty-no-git-description"] { + flex: none; + margin: 0 0 4px; + max-width: 360px; + font-size: 13px; + font-weight: 440; + line-height: 20px; + text-align: center; + letter-spacing: -0.04px; + color: var(--v2-text-text-muted); +} + +[data-slot="session-review-v2-empty-changes"] { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 16px; + padding: 24px; + padding-bottom: 160px; + text-align: center; +} + +[data-slot="session-review-v2-empty-changes"] [data-slot="icon-svg"] { + flex: none; + color: var(--v2-icon-icon-muted); +} + +[data-slot="session-review-v2-empty-changes-title"] { + flex: none; + margin-top: 4px; + font-size: 13px; + font-weight: 530; + line-height: 100%; + letter-spacing: -0.04px; + color: var(--v2-text-text-base); +} + +[data-slot="session-review-v2-empty-changes-description"] { + flex: none; + max-width: 282px; + font-size: 13px; + font-weight: 440; + line-height: 20px; + text-align: center; + letter-spacing: -0.04px; + color: var(--v2-text-text-muted); +} diff --git a/packages/app-bundle/overlay/packages/session-ui/src/v2/components/session-review-v2.tsx b/packages/app-bundle/overlay/packages/session-ui/src/v2/components/session-review-v2.tsx new file mode 100644 index 00000000..c2699119 --- /dev/null +++ b/packages/app-bundle/overlay/packages/session-ui/src/v2/components/session-review-v2.tsx @@ -0,0 +1,323 @@ +import { IconButton } from "@opencode-ai/ui/icon-button" +import { useI18n } from "@opencode-ai/ui/context/i18n" +import { SegmentedControlItemV2, SegmentedControlV2 } from "@opencode-ai/ui/v2/segmented-control-v2" +import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2" +import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2" +import { Icon } from "@opencode-ai/ui/v2/icon" +import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" +import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" +import type { SessionReviewDiffStyle } from "../../components/session-review" +import { ResizeHandle } from "@opencode-ai/ui/resize-handle" +import { ScrollView } from "@opencode-ai/ui/scroll-view" +import { makeEventListener } from "@solid-primitives/event-listener" +import { Show, createEffect, createMemo, createSignal, type JSX } from "solid-js" +import { getWorkerPool } from "../../pierre/worker" +import { SessionFilePanelV2, SessionFilePanelV2Empty } from "./session-file-panel-v2" + +export const SESSION_REVIEW_V2_SIDEBAR_WIDTH_DEFAULT = 240 +export const SESSION_REVIEW_V2_SIDEBAR_WIDTH_MIN = 200 +export const SESSION_REVIEW_V2_SIDEBAR_WIDTH_MAX = 480 + +export type SessionReviewExpandMode = "expand" | "collapse" + +export type SessionReviewV2Props = { + title?: JSX.Element + stats?: JSX.Element + empty?: JSX.Element + sidebarOpen?: boolean + sidebar?: JSX.Element + activeFile?: string + files: string[] + onSelectFile: (file: string) => void + diffStyle: SessionReviewDiffStyle + onDiffStyleChange?: (style: SessionReviewDiffStyle) => void + expandMode: SessionReviewExpandMode + onExpandModeChange: (mode: SessionReviewExpandMode) => void + preview?: JSX.Element + hasDiffs: boolean +} + +export type SessionReviewV2SidebarProps = { + open: boolean + transition: boolean + title?: JSX.Element + stats?: JSX.Element + filter: string + onFilterChange: (value: string) => void + onFilterKeyDown?: JSX.EventHandlerUnion + filterAutofocus?: boolean + filterRef?: (element: HTMLInputElement) => void + filterControls?: string + filterActiveDescendant?: string + filterExpanded?: boolean + width?: number + onWidthChange?: (width: number) => void + minWidth?: number + maxWidth?: number + viewportRef?: (element: HTMLDivElement) => void + children?: JSX.Element +} + +export function SessionReviewV2Sidebar(props: SessionReviewV2SidebarProps) { + const i18n = useI18n() + const [resizing, setResizing] = createSignal(false) + const width = () => props.width ?? SESSION_REVIEW_V2_SIDEBAR_WIDTH_DEFAULT + const minWidth = () => props.minWidth ?? SESSION_REVIEW_V2_SIDEBAR_WIDTH_MIN + const maxWidth = () => props.maxWidth ?? SESSION_REVIEW_V2_SIDEBAR_WIDTH_MAX + + createEffect(() => { + if (!resizing()) return + const stop = () => setResizing(false) + makeEventListener(document, "pointerup", stop) + makeEventListener(document, "pointercancel", stop) + }) + + return ( +
+ + + + +
setResizing(true)}> + props.onWidthChange?.(next)} + onDblClick={() => props.onWidthChange?.(SESSION_REVIEW_V2_SIDEBAR_WIDTH_DEFAULT)} + /> +
+
+
+ ) +} + +export function SessionReviewV2(props: SessionReviewV2Props) { + const i18n = useI18n() + + createEffect(() => { + getWorkerPool(props.diffStyle) + }) + + const fileIndex = () => { + const files = props.files + if (files.length === 0) return -1 + + const active = props.activeFile + const i = active ? files.indexOf(active) : -1 + if (i >= 0) return i + return 0 + } + + const prev = () => { + if (!canCycle()) return + return props.files[(fileIndex() - 1 + props.files.length) % props.files.length] + } + + const next = () => { + if (!canCycle()) return + return props.files[(fileIndex() + 1) % props.files.length] + } + + const canCycle = () => props.files.length > 0 + const showCollapsedMeta = () => props.sidebarOpen === false + // Memoize slot getters so Show conditions do not instantiate throwaway elements. + const title = createMemo(() => props.title) + const stats = createMemo(() => props.stats) + + const cycle = (file: string | undefined) => { + if (!file) return + props.onSelectFile(file) + } + + // Keep the advertised arrow keys working while the + // pane is mounted, but never while typing in an input or comment editor. + makeEventListener(document, "keydown", (event) => { + if (event.defaultPrevented || event.ctrlKey || event.metaKey || event.altKey) return + if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return + const target = event.target + if (target instanceof HTMLElement && (target.isContentEditable || target.closest("input, textarea, select"))) return + if (!props.hasDiffs || !canCycle()) return + const file = event.key === "ArrowLeft" ? prev() : next() + if (!file) return + event.preventDefault() + cycle(file) + }) + + const toolbarStart = () => ( + <> + +
+ +
{title()}
+
+ {stats()} + + + {fileIndex() + 1}/{props.files.length} + + +
+
+
+ + {i18n.t("ui.sessionReviewV2.previousFile")} + + + } + > + cycle(prev())} + aria-label={i18n.t("ui.sessionReviewV2.previousFile")} + /> + + + {i18n.t("ui.sessionReviewV2.nextFile")} + + + } + > + cycle(next())} + aria-label={i18n.t("ui.sessionReviewV2.nextFile")} + /> + +
+ + ) + + const toolbarEnd = () => ( + <> + + { + if (value !== "unified" && value !== "split") return + props.onDiffStyleChange?.(value) + }} + class="session-review-v2-segmented-control session-review-v2-segmented-control--icon" + aria-label={i18n.t("ui.sessionReviewV2.diffView")} + > + + + + + + + + + + + + + + ) + + return ( + + + {props.empty}}> + {props.preview} + + + + ) +} + +export function SessionReviewV2SidebarToggle(props: { opened: boolean; disabled?: boolean; onToggle: () => void }) { + const i18n = useI18n() + + return ( + + } + /> + + ) +} diff --git a/packages/app-bundle/scripts/extract_overlay.mjs b/packages/app-bundle/scripts/extract_overlay.mjs index e0ea2ec6..5dfc0053 100644 --- a/packages/app-bundle/scripts/extract_overlay.mjs +++ b/packages/app-bundle/scripts/extract_overlay.mjs @@ -1,22 +1,40 @@ #!/usr/bin/env node -// M2 slice (a) overlay extractor (#451) — extracts the app-bundle overlay from -// the opencode fork at a pinned tag into packages/app-bundle/overlay/, at +// M2 overlay extractor (#451) — extracts the app-bundle overlay from the +// opencode fork at a pinned tag into packages/app-bundle/overlay/, at // UPSTREAM-RELATIVE paths, and writes manifest.json (per-file sha256 + the // machine-derived A/M/D classification against the upstream base). // -// node scripts/extract_overlay.mjs [--fork ] [--tag v1.18.10-amicode.14] +// node scripts/extract_overlay.mjs [--fork ] [--tag v1.18.10-amicode.14] [--slice a|ab] // // The fork clone defaults to the sibling checkout (team layout: // ~/armonia/repos/opencode). Extraction is deterministic: same tag → // identical bytes → identical hashes (files are read via git archive AT the // tag, never the working tree). // -// SLICE (a) SCOPE — the COMPLETE packages/ui delta: -// overlay = every file under packages/ui that is Added or Modified vs the -// upstream base. Complete by construction: materialize(upstream@base, -// overlay) ≡ fork@tag packages/ui, byte for byte. The whole-directory -// classification (A/M/D per file) is recorded in the manifest — the -// machine-derived inventory for later decomposition slices. +// SCOPE (slice b, corrected 2026-08-21): the COMPLETE fork-vs-base delta of +// the app's build graph — packages/{app,ui,session-ui,schema,core,sdk}. +// +// The additive-only scope this slice was planned around does not typecheck: +// ~10 additive app files depend on SYMBOL-LEVEL additions in modified files +// (settings.developer, tabs.openPath, model.pin, QuestionInfo.kind — the +// typed question cards spanning schema/core/sdk), and the fork's debug-bar +// deletion forces layout.tsx into the overlay (base layout imports the +// deleted module). The fork delta is a cross-cutting FEATURE delta, not an +// app-layer delta. The complete-graph overlay is the honest scope: +// +// - equivalence is trivial (byte-identical per package) and +// - composition (install + typecheck + build) proves the bundle buildable +// against a fork-equivalent tree TODAY. +// +// The manifest's per-package classification + the server-coupled files +// (schema/core/sdk — types & runtime the CANONICAL server will not have) +// are the machine-derived M3 CUTOVER PORT INVENTORY: every feature that +// spans the app and the fork's server packages is on it, because the bundle +// will hit exactly these gaps against canonical at cutover. +// +// The compose-vs-fork decomposition of the true overlays (home.tsx etc.) +// remains deliberate POST-cutover maintenance work — wholesale overlay +// inclusion is correct for the one-push cutover this plan ships. // // BASE: v1.18.12 — tree-equal to the fork's true merge base (b0b114923, // v1.18.12~1) except the tag commit's version-string bumps, which land in @@ -25,9 +43,9 @@ // the fork's package.json is that merge's residue, not a later sync. import { execFileSync } from "node:child_process"; import { createHash } from "node:crypto"; -import { mkdirSync, readFileSync, rmSync, statSync, readdirSync, writeFileSync } from "node:fs"; +import { lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, statSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; -import { join } from "node:path"; +import { dirname, join, normalize } from "node:path"; const PKG_ROOT = join(import.meta.dirname, ".."); const FORK_DEFAULT = join(homedir(), "armonia", "repos", "opencode"); @@ -40,72 +58,138 @@ const flag = (n) => { const UPSTREAM_BASE = "v1.18.12"; const TAG = flag("tag") ?? "v1.18.10-amicode.14"; +const SLICE = flag("slice") ?? "full"; const FORK = flag("fork") ?? process.env.AMICODE_OPENCODE_SRC ?? FORK_DEFAULT; const git = (...a) => execFileSync("git", ["-C", FORK, ...a], { encoding: "utf8" }).trim(); -const sha256Buf = (b) => createHash("sha256").update(b).digest("hex"); +const gitAt = (spec, maxBuffer = 1 << 26) => + execFileSync("git", ["-C", FORK, "show", spec], { maxBuffer }); -// ── 1. resolve the tag + compute the complete packages/ui delta ─────────────── +// ── 1. resolve the tag + compute the deltas ─────────────────────────────────── const tagSha = git("rev-parse", `${TAG}^{commit}`); const baseSha = git("rev-parse", `${UPSTREAM_BASE}^{commit}`); -console.log(`[extract] fork ${FORK} @ ${TAG} (${tagSha.slice(0, 10)}), upstream base ${UPSTREAM_BASE} (${baseSha.slice(0, 10)})`); +console.log(`[extract] fork ${FORK} @ ${TAG} (${tagSha.slice(0, 10)}), upstream base ${UPSTREAM_BASE} (${baseSha.slice(0, 10)}), slice ${SLICE}`); + +function diffStatus(paths) { + return git("diff", "--name-status", UPSTREAM_BASE, TAG, "--", ...paths) + .split("\n") + .filter(Boolean) + .map((line) => { + const [status, ...rest] = line.split("\t"); + return { status, path: rest[rest.length - 1] }; + }); +} + +const GRAPH_PKGS = ["packages/app", "packages/ui", "packages/session-ui", "packages/schema", "packages/core", "packages/sdk"]; +const SERVER_COUPLED_PKGS = ["packages/schema", "packages/core", "packages/sdk"]; + +const graphDelta = diffStatus(GRAPH_PKGS); + +const uiOverlay = graphDelta.filter((d) => d.path.startsWith("packages/ui/") && d.status !== "D").map((d) => d.path); +const appSessionAdds = graphDelta.filter((d) => !SERVER_COUPLED_PKGS.some((p) => d.path.startsWith(p + "/")) && d.path.startsWith("packages/") && !d.path.startsWith("packages/ui/") && d.status === "A").map((d) => d.path); +const appSessionMods = graphDelta.filter((d) => !SERVER_COUPLED_PKGS.some((p) => d.path.startsWith(p + "/")) && !d.path.startsWith("packages/ui/") && d.status === "M").map((d) => d.path); +const appSessionDels = graphDelta.filter((d) => d.status === "D").map((d) => d.path); +const serverCoupled = graphDelta.filter((d) => SERVER_COUPLED_PKGS.some((p) => d.path.startsWith(p + "/")) && d.status !== "D").map((d) => d.path); -const delta = git("diff", "--name-status", UPSTREAM_BASE, TAG, "--", "packages/ui") - .split("\n") - .filter(Boolean) - .map((line) => { - const [status, ...rest] = line.split("\t"); - return { status, path: rest[rest.length - 1] }; - }); -const adds = delta.filter((d) => d.status === "A").map((d) => d.path); -const mods = delta.filter((d) => d.status === "M").map((d) => d.path); -const dels = delta.filter((d) => d.status === "D").map((d) => d.path); -const overlayFiles = [...adds, ...mods].sort(); -console.log(`[extract] packages/ui delta vs ${UPSTREAM_BASE}: ${adds.length} added, ${mods.length} modified, ${dels.length} deleted → overlay of ${overlayFiles.length} files`); +// ── 2. server-coupled files (schema/core/sdk): part of the overlay (the +// bundle typechecks against them today), AND the M3 cutover port inventory +// (they carry types/runtime the canonical server will NOT have at cutover). ── -// ── 2. extract overlay files at their upstream-relative paths, AT THE TAG ──── +// ── 3. the overlay set ─────────────────────────────────────────────────────── +const overlayFiles = [...new Set([...graphDelta.filter((d) => d.status !== "D").map((d) => d.path)])].sort(); +const classification = {}; +for (const d of graphDelta) if (d.status !== "D") classification[d.path] = d.status; +const deletions = graphDelta.filter((d) => d.status === "D").map((d) => d.path); + +const perPkg = {}; +for (const d of graphDelta) { + const pkg = d.path.split("/").slice(0, 2).join("/"); + perPkg[pkg] ??= { A: 0, M: 0, D: 0 }; + perPkg[pkg][d.status] += 1; +} +console.log("[extract] per-package delta:", JSON.stringify(perPkg)); +console.log( + `[extract] app+session-ui overlays: ${appSessionAdds.length} A + ${appSessionMods.length} M (complete); server-coupled (schema/core/sdk): ${serverCoupled.length} files — the M3 cutover port inventory`, +); +console.log(`[extract] OVERLAY TOTAL: ${overlayFiles.length} files, ${deletions.length} deletions`); + +// ── 4. extract at their upstream-relative paths, AT THE TAG ────────────────── const overlayDir = join(PKG_ROOT, "overlay"); rmSync(overlayDir, { recursive: true, force: true }); mkdirSync(overlayDir, { recursive: true }); -execFileSync("sh", ["-c", `git -C '${FORK}' archive --format=tar ${TAG} ${overlayFiles.map((f) => `'${f}'`).join(" ")} | tar -x -C '${overlayDir}'`]); +if (overlayFiles.length > 0) { + // git archive arg list can get long — batch it. + for (let i = 0; i < overlayFiles.length; i += 100) { + const batch = overlayFiles.slice(i, i + 100); + execFileSync("sh", [ + "-c", + `git -C '${FORK}' archive --format=tar ${TAG} ${batch.map((f) => `'${f}'`).join(" ")} | tar -x -C '${overlayDir}'`, + ]); + } +} -// ── 3. manifest: hashes + classification + round-trip proof ───────────────── +// ── 5. manifest: hashes + classification + round-trip proof ───────────────── +const sha256Buf = (b) => createHash("sha256").update(b).digest("hex"); const hashes = {}; -const classification = {}; let mismatches = 0; for (const rel of overlayFiles) { const p = join(overlayDir, rel); - if (!statSync(p).isFile()) throw new Error(`extractor bug: ${rel} not a file after archive`); - const blob = execFileSync("git", ["-C", FORK, "show", `${TAG}:${rel}`], { maxBuffer: 1 << 26 }); + const st = lstatSync(p); + if (st.isSymbolicLink()) { + // Symlinks: git archive materializes them as real symlinks; `git show` + // prints the TARGET PATH, not the content. The round-trip proof for a + // symlink is readlink === the git blob's content. + const link = readlinkSync(p); + const blob = gitAt(`${TAG}:${rel}`).toString("utf8").trimEnd(); + if (link !== blob) { + console.error(`[extract] ROUND-TRIP MISMATCH (symlink): ${rel}: ${link} != ${blob}`); + mismatches++; + } + // hash the LINK TARGET STRING (the stable, content-independent identity) + hashes[rel] = createHash("sha256").update(link).digest("hex"); + continue; + } + if (!st.isFile()) throw new Error(`extractor bug: ${rel} not a regular file after archive`); + const blob = gitAt(`${TAG}:${rel}`); const h = sha256Buf(readFileSync(p)); if (h !== sha256Buf(blob)) { console.error(`[extract] ROUND-TRIP MISMATCH: ${rel}`); mismatches++; } hashes[rel] = h; - classification[rel] = adds.includes(rel) ? "A" : "M"; } if (mismatches > 0) process.exit(1); console.log(`[extract] round-trip: all ${overlayFiles.length} files byte-identical to ${TAG}`); -// Unchanged files (sanity: ui files at tag not in the overlay come from the base) -const uiAtTag = git("ls-tree", "-r", "--name-only", TAG, "--", "packages/ui").split("\n").filter(Boolean); -const unchanged = uiAtTag.filter((f) => !overlayFiles.includes(f) && !dels.includes(f)); -console.log(`[extract] ui files at tag: ${uiAtTag.length} = ${overlayFiles.length} overlay + ${unchanged.length} base-unchanged + ${dels.length} deleted`); - const manifest = { - schema: 2, - slice: "a", - scope: "packages/ui (complete delta)", + schema: 4, + slices: "b (complete app-graph delta)", + scope: "complete fork-vs-base delta of packages/{app,ui,session-ui,schema,core,sdk}", fork_tag: TAG, fork_sha: tagSha, upstream_base: UPSTREAM_BASE, upstream_base_sha: baseSha, extracted_at: new Date().toISOString(), - counts: { added: adds.length, modified: mods.length, deleted: dels.length, unchanged: unchanged.length }, - deletions: dels, + per_package: perPkg, + counts: { + overlay_total: overlayFiles.length, + deletions: deletions.length, + server_coupled: serverCoupled.length, + }, + // The M3 CUTOVER PORT INVENTORY: schema/core/sdk files whose types or + // runtime the canonical server will NOT have. Every app feature touching + // these is a cutover blocker to port upstream, reimplement in the + // extension service, or drop from the bundle. + server_coupled_port_inventory: serverCoupled.sort(), + // The true overlays (big M-files in app/ui upstream still evolves), + // recorded for the POST-cutover compose-vs-fork decomposition. + true_overlays: graphDelta + .filter((d) => d.status === "M" && !SERVER_COUPLED_PKGS.some((p) => d.path.startsWith(p + "/"))) + .map((d) => d.path) + .sort(), + deletions, classification, files: hashes, }; writeFileSync(join(PKG_ROOT, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n"); -console.log(`[extract] wrote manifest.json (${overlayFiles.length} entries, ${dels.length} deletions)`); +console.log(`[extract] wrote manifest.json (${overlayFiles.length} entries, ${deletions.length} deletions)`); diff --git a/packages/app-bundle/scripts/materialize.mjs b/packages/app-bundle/scripts/materialize.mjs index 7965bac6..a07a8438 100644 --- a/packages/app-bundle/scripts/materialize.mjs +++ b/packages/app-bundle/scripts/materialize.mjs @@ -10,8 +10,9 @@ // upstream paths (bucket M) or add new ones (bucket A) — copy is the whole // conflict policy for slice (a); the drift report is a separate concern. import { createHash } from "node:crypto"; +import { readlinkSync } from "node:fs"; import { spawnSync } from "node:child_process"; -import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync, readdirSync } from "node:fs"; +import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync, readdirSync } from "node:fs"; import { homedir, tmpdir } from "node:os"; import { join } from "node:path"; @@ -81,9 +82,10 @@ for (const rel of readdirSync(overlayDir, { recursive: true })) { const p = join(overlayDir, rel.toString()); if (!statSync(p).isFile()) continue; const dest = join(outDir, rel.toString()); - if (existsSync(dest)) overwritten++; + if (existsSync(dest) || lstatSync(dest, { throwIfNoEntry: false })) overwritten++; mkdirSync(join(dest, ".."), { recursive: true }); - cpSync(p, dest); + // verbatimSymlinks: preserve overlay symlinks AS symlinks (amico.svg → ui asset) + cpSync(p, dest, { verbatimSymlinks: true }); applied++; } console.log(`[materialize] tree at ${outDir}: upstream ${repo}@${tag} + overlay (${applied} files applied, ${overwritten} overwrote upstream)`); @@ -104,7 +106,22 @@ const sha256 = (p) => createHash("sha256").update(readFileSync(p)).digest("hex") let bad = 0; for (const [rel, want] of Object.entries(manifest.files)) { const p = join(outDir, rel); - if (!existsSync(p) || sha256(p) !== want) { + const st = lstatSync(p, { throwIfNoEntry: false }); + if (!st) { + console.error(`[materialize] MANIFEST MISMATCH (missing): ${rel}`); + bad++; + continue; + } + if (st.isSymbolicLink()) { + // symlink identity = the link-target string (see extract_overlay.mjs) + const h = createHash("sha256").update(readlinkSync(p)).digest("hex"); + if (h !== want) { + console.error(`[materialize] MANIFEST MISMATCH (symlink): ${rel}`); + bad++; + } + continue; + } + if (sha256(p) !== want) { console.error(`[materialize] MANIFEST MISMATCH: ${rel}`); bad++; }
+ ) +} + +function pendingBlocks( + result: RenderResult | undefined, + projection: Projection | undefined, + cacheKey: string | undefined, + owner: string, +) { + if (!result) return [] + if (!projection || result.text === projection.text) return result.blocks + const initial = result.blocks.length === 1 && result.blocks[0]?.key === "initial" + return projection.blocks.map((block, index) => { + const current = initial ? undefined : result.blocks[index] + if (current && canReusePendingBlock(current, block)) return current + const key = markdownBlockKey(owner, cacheKey, index, block.mode) + if (block.mode !== "code") + return { key, mode: block.mode, raw: block.raw, hash: String(block.raw.length), html: fallback(block.src) } + return { + key, + mode: block.mode, + raw: block.raw, + hash: String(block.raw.length), + language: block.language ?? "text", + complete: !!block.complete, + stable: [], + generation: 0, + unstable: [[block.src, ""] as MarkdownToken], + } + }) +} + +function disposeCode(key: string) { + disposeStreamingCode(key) +} + +function updateBlock(container: HTMLDivElement, index: number, block: RenderedBlock, labels: CopyLabels) { + const current = container.children[index] + if (block.mode === "code") { + updateCodeBlock(container, current, block, labels) + return + } + if ( + current instanceof HTMLDivElement && + current.dataset.markdownKey === block.key && + current.dataset.markdownHash === block.hash + ) + return + + const next = document.createElement("div") + next.dataset.markdownBlock = "" + next.dataset.markdownKey = block.key + next.dataset.markdownHash = block.hash + next.style.display = "contents" + next.innerHTML = block.html + decorate(next, labels) + + if (!(current instanceof HTMLDivElement)) { + container.appendChild(next) + return + } + + morphdom(current, next, { + onBeforeElUpdated: (fromEl, toEl) => { + if ( + fromEl instanceof HTMLElement && + toEl instanceof HTMLElement && + fromEl.getAttribute("data-slot") === "markdown-copy-button" && + toEl.getAttribute("data-slot") === "markdown-copy-button" + ) { + return false + } + if (fromEl.isEqualNode(toEl)) return false + return true + }, + onBeforeNodeDiscarded: (node) => { + if (node instanceof Element) disposeCopyButtons(node) + return true + }, + }) +} + +function updateCodeBlock( + container: HTMLDivElement, + current: Element | undefined, + block: Extract, + labels: CopyLabels, +) { + const existing = current instanceof HTMLDivElement && current.dataset.markdownKey === block.key ? current : undefined + const next = existing ?? document.createElement("div") + next.dataset.markdownBlock = "" + next.dataset.markdownKey = block.key + next.dataset.markdownHash = block.hash + next.dataset.markdownComplete = block.complete ? "true" : "false" + next.style.display = "contents" + + const code = existing?.querySelector("code") + if (code instanceof HTMLElement) { + const wrapper = code.closest('[data-component="markdown-code"]') + if (wrapper instanceof HTMLElement) applyCodeMetadata(wrapper, block.language) + code.className = `language-${block.language}` + const previous = renderedCodeTokens.get(next) + const reset = shouldResetCodeTokens(previous, { + language: block.language, + generation: block.generation, + stableCount: block.stable.length, + raw: block.raw, + }) + const stableCount = reset ? 0 : previous!.stableCount + const tail = [...block.stable.slice(stableCount), ...block.unstable] + const prior = reset ? [] : previous!.unstable + const prefix = prior.findIndex((token, index) => !sameToken(token, tail[index])) + const keep = stableCount + (prefix < 0 ? Math.min(prior.length, tail.length) : prefix) + while (code.children.length > keep) code.lastElementChild?.remove() + tail + .slice(keep - stableCount) + .map(createTokenSpan) + .forEach((span) => code.appendChild(span)) + renderedCodeTokens.set(next, { + language: block.language, + generation: block.generation, + stableCount: block.stable.length, + unstable: block.unstable, + raw: block.raw, + }) + return + } + + const wrapper = document.createElement("div") + wrapper.setAttribute("data-component", "markdown-code") + applyCodeMetadata(wrapper, block.language) + const pre = document.createElement("pre") + pre.className = "shiki OpenCode" + const codeElement = document.createElement("code") + codeElement.className = `language-${block.language}` + ;[...block.stable, ...block.unstable].map(createTokenSpan).forEach((span) => codeElement.appendChild(span)) + pre.appendChild(codeElement) + wrapper.appendChild(pre) + wrapper.appendChild(createCopyButton(labels)) + next.appendChild(wrapper) + renderedCodeTokens.set(next, { + language: block.language, + generation: block.generation, + stableCount: block.stable.length, + unstable: block.unstable, + raw: block.raw, + }) + if (current) { + disposeCopyButtons(current) + current.replaceWith(next) + return + } + container.appendChild(next) +} + +function sameToken(left: MarkdownToken, right: MarkdownToken | undefined) { + return !!right && left[0] === right[0] && left[1] === right[1] +} + +function createTokenSpan(token: MarkdownToken) { + const span = document.createElement("span") + span.setAttribute("style", token[1]) + span.textContent = token[0] + return span +} diff --git a/packages/app-bundle/overlay/packages/session-ui/src/components/message-part-css.test.ts b/packages/app-bundle/overlay/packages/session-ui/src/components/message-part-css.test.ts new file mode 100644 index 00000000..b56c7974 --- /dev/null +++ b/packages/app-bundle/overlay/packages/session-ui/src/components/message-part-css.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, test } from "bun:test" +import { readFileSync } from "node:fs" +import { resolve } from "node:path" + +// Regression guard: the text-card (kind:"text") CSS was accidentally deleted +// once (amicode#349 / opencode#121) during a file-reference feature refactor. +// Without these rules the textarea in a free-form question is transparent and +// borderless — effectively invisible. This test catches a future deletion. + +const css = readFileSync(resolve(__dirname, "message-part.css"), "utf8") + +describe("question text-card CSS (amicode#349 regression)", () => { + test("question-text-form selector is present", () => { + expect(css).toContain('[data-slot="question-text-form"]') + }) + + test("text-form textarea has visible border", () => { + expect(css).toContain('[data-slot="question-text-form"] > [data-slot="question-custom-input"]') + }) + + test("text-form textarea has min-height", () => { + expect(css).toContain("min-height: 36px") + }) + + test("text-form textarea has background", () => { + // The v2 design token path + expect(css).toMatch(/question-text-form.*background.*bg-layer-02/s) + }) +}) diff --git a/packages/app-bundle/overlay/packages/session-ui/src/components/message-part-groups.ts b/packages/app-bundle/overlay/packages/session-ui/src/components/message-part-groups.ts new file mode 100644 index 00000000..54a1b9ea --- /dev/null +++ b/packages/app-bundle/overlay/packages/session-ui/src/components/message-part-groups.ts @@ -0,0 +1,170 @@ +// Pure part-grouping primitives, extracted from message-part.tsx so they can be +// unit-tested without importing the (Kobalte/DOM-heavy) render module. JSX-free, +// type-only SDK import. message-part.tsx re-exports the public names, so the +// stable `@opencode-ai/ui/message-part` import path is unchanged. +import type { Part as PartType, ToolPart } from "@opencode-ai/sdk/v2" + +// Consecutive read/search/list calls collapse into one "Explored" context group; +// consecutive bash calls (≥2) collapse into one "Worked in shell" group (spec B); +// consecutive file-mutation calls (≥2) collapse into one "Edited files" group +// (same spec-B shape — the mutation set mirrors toolDefaultOpen's edit family). +const CONTEXT_GROUP_TOOLS = new Set(["read", "glob", "grep", "list"]) +const EDIT_GROUP_TOOLS = new Set(["edit", "write", "patch", "apply_patch"]) + +export function isContextGroupTool(part: PartType): part is ToolPart { + return part.type === "tool" && CONTEXT_GROUP_TOOLS.has(part.tool) +} + +export function isShellGroupTool(part: PartType): part is ToolPart { + return part.type === "tool" && part.tool === "bash" +} + +export function isEditGroupTool(part: PartType): part is ToolPart { + return part.type === "tool" && EDIT_GROUP_TOOLS.has(part.tool) +} + +export type PartRef = { + messageID: string + partID: string +} + +export type PartGroup = + | { + key: string + type: "part" + ref: PartRef + } + | { + key: string + type: "context" + refs: PartRef[] + } + | { + key: string + type: "shell" + refs: PartRef[] + } + | { + key: string + type: "edit" + refs: PartRef[] + } + +function sameRef(a: PartRef, b: PartRef) { + return a.messageID === b.messageID && a.partID === b.partID +} + +function sameGroup(a: PartGroup, b: PartGroup) { + if (a === b) return true + if (a.key !== b.key) return false + if (a.type !== b.type) return false + if (a.type === "part") { + if (b.type !== "part") return false + return sameRef(a.ref, b.ref) + } + // context | shell | edit — all carry refs + if (b.type === "part") return false + if (a.refs.length !== b.refs.length) return false + return a.refs.every((ref, i) => sameRef(ref, b.refs[i]!)) +} + +export function sameGroups(a: readonly PartGroup[] | undefined, b: readonly PartGroup[] | undefined) { + if (a === b) return true + if (!a || !b) return false + if (a.length !== b.length) return false + return a.every((item, i) => sameGroup(item, b[i]!)) +} + +export function groupParts(parts: { messageID: string; part: PartType }[]) { + const result: PartGroup[] = [] + // At most one run is open at a time: a part is context-group, shell-group, + // edit-group, or a standalone part. Context collapses at any length (matches + // read/grep); shell and edit collapse only at ≥2 consecutive calls, so a lone + // command (or a lone edit, whose inline diff is worth the card) stays full. + let contextStart = -1 + let shellStart = -1 + let editStart = -1 + + const pushPart = (item: { messageID: string; part: PartType }) => { + result.push({ + key: `part:${item.messageID}:${item.part.id}`, + type: "part", + ref: { messageID: item.messageID, partID: item.part.id }, + }) + } + + const flushContext = (end: number) => { + if (contextStart < 0) return + const slice = parts.slice(contextStart, end + 1) + const first = slice[0] + if (first) + result.push({ + key: `context:${first.part.id}`, + type: "context", + refs: slice.map((item) => ({ messageID: item.messageID, partID: item.part.id })), + }) + contextStart = -1 + } + + const flushShell = (end: number) => { + if (shellStart < 0) return + const slice = parts.slice(shellStart, end + 1) + const first = slice[0] + if (first) { + if (slice.length >= 2) + result.push({ + key: `shell:${first.part.id}`, + type: "shell", + refs: slice.map((item) => ({ messageID: item.messageID, partID: item.part.id })), + }) + else pushPart(first) + } + shellStart = -1 + } + + const flushEdit = (end: number) => { + if (editStart < 0) return + const slice = parts.slice(editStart, end + 1) + const first = slice[0] + if (first) { + if (slice.length >= 2) + result.push({ + key: `edit:${first.part.id}`, + type: "edit", + refs: slice.map((item) => ({ messageID: item.messageID, partID: item.part.id })), + }) + else pushPart(first) + } + editStart = -1 + } + + parts.forEach((item, index) => { + if (isContextGroupTool(item.part)) { + flushShell(index - 1) + flushEdit(index - 1) + if (contextStart < 0) contextStart = index + return + } + if (isShellGroupTool(item.part)) { + flushContext(index - 1) + flushEdit(index - 1) + if (shellStart < 0) shellStart = index + return + } + if (isEditGroupTool(item.part)) { + flushContext(index - 1) + flushShell(index - 1) + if (editStart < 0) editStart = index + return + } + flushContext(index - 1) + flushShell(index - 1) + flushEdit(index - 1) + pushPart(item) + }) + + flushContext(parts.length - 1) + flushShell(parts.length - 1) + flushEdit(parts.length - 1) + return result +} diff --git a/packages/app-bundle/overlay/packages/session-ui/src/components/message-part-skill.test.ts b/packages/app-bundle/overlay/packages/session-ui/src/components/message-part-skill.test.ts new file mode 100644 index 00000000..2df34518 --- /dev/null +++ b/packages/app-bundle/overlay/packages/session-ui/src/components/message-part-skill.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, test } from "bun:test" +import { skillBody } from "./message-part-skill" + +// Wrapper shape produced by both skill tool implementations +// (packages/opencode/src/tool/skill.ts and packages/core/src/tool/skill.ts). +const OUTPUT = [ + '', + "# Skill: atoms", + "", + "## Rydberg conventions", + "", + "Use the 3-level model: $|0\\rangle$ dark, $|1\\rangle \\leftrightarrow |r\\rangle$ driven.", + "", + "Base directory for this skill: file:///home/u/.amico/skills/atoms", + "Relative paths in this skill (e.g., scripts/, reference/) are relative to this base directory.", + "Note: file list is sampled.", + "", + "", + "/home/u/.amico/skills/atoms/reference/blockade.md", + "", + "", +].join("\n") + +describe("skillBody", () => { + test("returns empty string for undefined or empty output", () => { + expect(skillBody(undefined)).toBe("") + expect(skillBody("")).toBe("") + }) + + test("strips the transport wrapper and plumbing, keeps the instructions", () => { + const body = skillBody(OUTPUT) + expect(body).toContain("## Rydberg conventions") + expect(body).toContain("3-level model") + expect(body).not.toContain("") + expect(body).not.toContain("") + expect(body).not.toContain("blockade.md") + expect(body).not.toContain("Base directory") + expect(body).not.toContain("Relative paths") + expect(body).not.toContain("file list is sampled") + }) + + test("drops the '# Skill: name' heading (the chip already shows the name)", () => { + expect(skillBody(OUTPUT)).not.toContain("# Skill: atoms") + }) + + test("keeps content lines that merely mention skill files mid-text", () => { + const out = '\nSee the skill_files note above for context.\n' + expect(skillBody(out)).toBe("See the skill_files note above for context.") + }) + + test("passes through plain text without wrapper untouched (trimmed)", () => { + expect(skillBody(" just some output ")).toBe("just some output") + }) +}) diff --git a/packages/app-bundle/overlay/packages/session-ui/src/components/message-part-skill.ts b/packages/app-bundle/overlay/packages/session-ui/src/components/message-part-skill.ts new file mode 100644 index 00000000..d8a614d3 --- /dev/null +++ b/packages/app-bundle/overlay/packages/session-ui/src/components/message-part-skill.ts @@ -0,0 +1,21 @@ +/** Extract the human-readable body of a skill tool result: the SKILL.md + * instructions without the / transport wrapper + * and the model-facing plumbing lines (base-directory, relative-path note, + * sampling note, the "# Skill: name" heading the chip already shows). */ +export function skillBody(output: string | undefined): string { + if (!output) return "" + const withoutFiles = output.replace(/[\s\S]*?<\/skill_files>/g, "") + const drop = [ + /^]*>$/, + /^<\/skill_content>$/, + /^# Skill: .*$/, + /^Base directory for this skill: .*$/, + /^Relative paths in this skill .*$/, + /^Note: file list is sampled\.$/, + ] + return withoutFiles + .split("\n") + .filter((line) => !drop.some((re) => re.test(line.trim()))) + .join("\n") + .trim() +} diff --git a/packages/app-bundle/overlay/packages/session-ui/src/components/message-part.css b/packages/app-bundle/overlay/packages/session-ui/src/components/message-part.css new file mode 100644 index 00000000..843006a4 --- /dev/null +++ b/packages/app-bundle/overlay/packages/session-ui/src/components/message-part.css @@ -0,0 +1,1688 @@ +[data-component="assistant-message"] { + content-visibility: auto; + width: 100%; + /* min-width:0 — a flex column won't shrink below its content's intrinsic + width without it, so a long unbroken-ish line (URLs, "overlapping" running + into the next word) pushed the message past the viewport and clipped on the + right. The container chain already sets this; the message column + its + text parts were the gap. */ + min-width: 0; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 12px; +} + +[data-component="assistant-message"] [data-component="text-part"], +[data-component="assistant-message"] [data-slot="text-part-body"] { + min-width: 0; + max-width: 100%; +} + +[data-component="user-message"] { + font-family: var(--font-family-sans); + font-size: var(--font-size-base); + font-style: normal; + font-weight: var(--font-weight-regular); + line-height: var(--line-height-large); + letter-spacing: var(--letter-spacing-normal); + color: var(--v2-text-text-base); + display: flex; + flex-direction: column; + align-items: flex-end; + align-self: stretch; + width: 100%; + max-width: 100%; + gap: 0; + + [data-slot="user-message-attachments"] { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 8px; + width: fit-content; + max-width: min(82%, 64ch); + margin-left: auto; + } + + [data-slot="user-message-attachment"] { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-width: 0; + border-radius: 6px; + overflow: hidden; + background: var(--v2-background-bg-layer-02); + border: 0.5px solid var(--v2-border-border-base); + cursor: default; + transition: + border-color 0.15s ease, + opacity 0.3s ease; + + &:hover { + border-color: var(--v2-border-border-strong); + } + + &[data-clickable] { + cursor: pointer; + } + + &[data-type="image"] { + position: relative; + width: 58px; + height: 46px; + border: none; + + /* inset box-shadows do not paint over content, so the hairline is an overlay */ + &::after { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + box-shadow: inset 0 0 0 0.5px var(--v2-border-border-base); + pointer-events: none; + } + } + + &[data-type="file"] { + width: min(220px, 100%); + height: 48px; + padding: 0 10px; + } + } + + [data-slot="user-message-attachment-image"] { + width: 100%; + height: 100%; + object-fit: cover; + } + + [data-slot="user-message-attachment-icon"] { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + color: var(--icon-weak); + + [data-component="icon"] { + width: 20px; + height: 20px; + } + } + + [data-slot="user-message-attachment-file"] { + width: 100%; + min-width: 0; + display: flex; + align-items: center; + gap: 8px; + + [data-component="file-icon"] { + width: 20px; + height: 20px; + flex: none; + } + } + + [data-slot="user-message-attachment-name"] { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--v2-text-text-muted); + font-size: var(--font-size-small); + line-height: var(--line-height-large); + } + + [data-slot="user-message-body"] { + width: fit-content; + max-width: min(82%, 64ch); + margin-left: auto; + display: flex; + flex-direction: column; + align-items: flex-end; + } + + [data-slot="user-message-text"] { + display: inline-block; + white-space: pre-wrap; + word-break: break-word; + overflow: hidden; + background: var(--v2-background-bg-layer-02); + border: none; + padding: 8px 12px; + border-radius: var(--radius-lg); + + [data-highlight="file"] { + color: var(--syntax-property); + } + + [data-highlight="agent"] { + color: var(--syntax-type); + } + + max-width: 100%; + } + + [data-slot="user-message-copy-wrapper"] { + min-height: 24px; + margin-top: 4px; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 10px; + width: 100%; + opacity: 0; + pointer-events: none; + transition: opacity 0.15s ease; + will-change: opacity; + + [data-component="tooltip-trigger"] { + display: inline-flex; + width: fit-content; + } + } + + [data-slot="user-message-meta"] { + user-select: none; + text-align: right; + flex: 1 1 auto; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + [data-slot="user-message-meta-wrap"] { + flex: 1 1 auto; + min-width: 0; + display: flex; + align-items: center; + justify-content: flex-end; + overflow: hidden; + gap: 0; + } + + [data-slot="user-message-meta-tail"] { + user-select: none; + flex: 0 0 auto; + white-space: nowrap; + text-align: right; + } + + &:hover [data-slot="user-message-copy-wrapper"], + &:focus-within [data-slot="user-message-copy-wrapper"] { + opacity: 1; + pointer-events: auto; + } + + .text-text-strong { + color: var(--v2-text-text-base); + } + + .font-medium { + font-weight: var(--font-weight-medium); + } +} + +[data-component="text-part"] { + width: 100%; + margin-top: 24px; + + [data-slot="text-part-body"] { + margin-top: 0; + } + + [data-slot="text-part-copy-wrapper"] { + min-height: 24px; + margin-top: 4px; + display: flex; + align-items: center; + justify-content: flex-start; + gap: 10px; + opacity: 0; + pointer-events: none; + transition: opacity 0.15s ease; + will-change: opacity; + + [data-component="tooltip-trigger"] { + display: inline-flex; + width: fit-content; + } + } + + [data-slot="text-part-meta"] { + user-select: none; + } + + [data-slot="text-part-copy-wrapper"][data-interrupted] { + width: 100%; + justify-content: flex-end; + gap: 12px; + } + + &:hover [data-slot="text-part-copy-wrapper"], + &:focus-within [data-slot="text-part-copy-wrapper"] { + opacity: 1; + pointer-events: auto; + } +} + +[data-component="compaction-part"] { + width: 100%; + display: flex; + flex-direction: column; + align-items: stretch; + + [data-slot="compaction-part-divider"] { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 0; + width: 100%; + } + + [data-slot="compaction-part-line"] { + flex: 1 1 auto; + height: 1px; + background: var(--v2-border-border-base); + } + + [data-slot="compaction-part-label"] { + flex: 0 0 auto; + white-space: nowrap; + text-align: center; + } +} + +[data-component="reasoning-part"] { + width: 100%; + color: var(--v2-text-text-muted); + line-height: var(--line-height-normal); + + [data-component="markdown"] { + margin-top: 16px; + font-style: normal; + font-size: 13px; + color: var(--v2-text-text-muted); + + strong, + b { + color: var(--v2-text-text-muted); + } + + p:has(strong) { + margin-top: 24px; + margin-bottom: 0; + + &:first-child { + margin-top: 0; + } + } + } +} + +@media (prefers-color-scheme: dark) { + [data-component="reasoning-part"] [data-component="markdown"] :not(pre) > code { + opacity: 0.6; + } +} + +[data-component="tool-output"] { + white-space: pre; + padding: 0; + margin-bottom: 24px; + height: fit-content; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + + [data-component="markdown"] { + width: 100%; + min-width: 0; + + pre { + margin: 0; + padding: 0; + background-color: transparent !important; + border: none !important; + } + } + + pre { + margin: 0; + padding: 0; + background: none; + } + + &[data-scrollable] { + height: auto; + max-height: 240px; + overflow-y: auto; + scrollbar-width: none; + -ms-overflow-style: none; + + &::-webkit-scrollbar { + display: none; + } + + [data-component="markdown"] { + overflow: visible; + } + } +} + +[data-component="bash-output"] { + width: 100%; + border: 0.5px solid var(--v2-border-border-base); + border-radius: 6px; + background: transparent; + position: relative; + overflow: hidden; + + [data-slot="bash-copy"] { + position: absolute; + top: 4px; + right: 4px; + opacity: 0; + pointer-events: none; + transition: opacity 0.15s ease; + } + + &:hover [data-slot="bash-copy"], + &:focus-within [data-slot="bash-copy"] { + opacity: 1; + pointer-events: auto; + } + + [data-slot="bash-scroll"] { + width: 100%; + overflow-y: auto; + overflow-x: hidden; + max-height: 240px; + + scrollbar-width: none; + &::-webkit-scrollbar { + display: none; + } + } + + [data-slot="bash-pre"] { + margin: 0; + padding: 12px; + } + + [data-slot="bash-pre"] code { + font-family: var(--font-family-mono); + font-feature-settings: var(--font-family-mono--font-feature-settings); + font-size: 13px; + line-height: var(--line-height-large); + white-space: pre-wrap; + overflow-wrap: anywhere; + } +} + +[data-component="edit-trigger"], +[data-component="write-trigger"] { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + width: 100%; + + [data-slot="message-part-title-area"] { + flex-grow: 1; + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + } + + [data-slot="message-part-title"] { + flex: 1 1 auto; + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + font-family: var(--font-family-sans); + font-size: 14px; + font-style: normal; + font-weight: var(--font-weight-medium); + line-height: var(--line-height-large); + letter-spacing: var(--letter-spacing-normal); + color: var(--v2-text-text-muted); + } + + [data-slot="message-part-title-spinner"] { + margin-left: 4px; + width: 16px; + height: 16px; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + color: var(--v2-text-text-muted); + + [data-component="spinner"] { + width: 16px; + height: 16px; + } + } + + [data-slot="message-part-title-text"] { + flex-shrink: 0; + text-transform: capitalize; + color: var(--v2-text-text-base); + } + + [data-slot="message-part-title-filename"] { + /* No text-transform - preserve original filename casing */ + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-weight: var(--font-weight-regular); + } + + /* AMICODE: the clickable variant (opens the file via the bridge) — strip the + * button chrome, keep the filename look, earn a hover underline. */ + button[data-slot="message-part-title-filename"] { + appearance: none; + background: none; + border: none; + padding: 0; + font: inherit; + color: inherit; + cursor: pointer; + } + + button[data-slot="message-part-title-filename"]:hover { + text-decoration: underline; + text-underline-offset: 2px; + } + + [data-slot="message-part-path"] { + display: flex; + flex-grow: 1; + min-width: 0; + font-weight: var(--font-weight-regular); + } + + [data-slot="message-part-directory"] { + color: var(--v2-text-text-muted); + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + direction: rtl; + text-align: left; + } + + [data-slot="message-part-filename"] { + color: var(--v2-text-text-base); + flex-shrink: 0; + } + + [data-slot="message-part-actions"] { + display: flex; + gap: 16px; + align-items: center; + justify-content: flex-end; + flex-shrink: 0; + } +} + +[data-component="edit-content"] { + border-radius: inherit; + border-top: 0.5px solid var(--v2-border-border-muted); + overflow-x: hidden; + overflow-y: visible; + + scrollbar-width: none; + -ms-overflow-style: none; + + &::-webkit-scrollbar { + display: none; + } + + [data-component="diff"] { + border-radius: inherit; + overflow: hidden; + } +} + +[data-component="write-content"] { + border-radius: inherit; + border-top: 0.5px solid var(--v2-border-border-muted); + overflow-x: hidden; + overflow-y: visible; + + [data-component="code"] { + padding-bottom: 0 !important; + border-radius: inherit; + overflow: hidden; + } + + /* Hide scrollbar */ + scrollbar-width: none; + -ms-overflow-style: none; + + &::-webkit-scrollbar { + display: none; + } +} + +[data-component="tool-action"] { + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; +} + +[data-component="exa-tool-output"] { + width: 100%; + display: flex; + flex-direction: column; + font-family: var(--font-family-sans); + font-size: var(--font-size-base); + line-height: var(--line-height-large); + color: var(--v2-text-text-muted); +} + +[data-slot="basic-tool-tool-subtitle"].exa-tool-query { + display: block; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +[data-slot="exa-tool-links"] { + display: flex; + flex-direction: column; + gap: 4px; +} + +[data-slot="exa-tool-link"] { + display: block; + max-width: 100%; + font: inherit; + line-height: inherit; + color: var(--v2-text-text-accent); + text-decoration: underline; + text-underline-offset: 2px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + &:hover { + color: var(--v2-text-text-accent); + } + + &:visited { + color: var(--v2-text-text-accent); + } +} + +[data-component="todos"] { + padding: 10px 0 24px 0; + display: flex; + flex-direction: column; + gap: 8px; + + [data-component="checkbox"] { + --checkbox-align: flex-start; + --checkbox-offset: 0.5px; + } + + [data-slot="message-part-todo-content"] { + line-height: var(--line-height-normal); + + &[data-completed="completed"] { + text-decoration: line-through; + color: var(--v2-text-text-faint); + } + } +} + +[data-component="context-tool-group-trigger"] { + width: 100%; + min-height: 24px; + display: flex; + align-items: center; + justify-content: flex-start; + gap: 0px; + cursor: default; + + [data-slot="context-tool-group-title"] { + flex-shrink: 1; + min-width: 0; + } + + [data-slot="collapsible-arrow"] { + color: var(--icon-weaker); + cursor: pointer; + } +} + +[data-component="context-tool-group-list"] { + padding-top: 0; + padding-right: 0; + padding-bottom: 0; + padding-left: 12px; + display: flex; + flex-direction: column; + gap: 4px; + + [data-slot="context-tool-group-item"] { + min-width: 0; + padding: 0; + } +} + +[data-component="diagnostics"] { + display: flex; + flex-direction: column; + gap: 4px; + padding: 8px 12px; + background-color: var(--v2-state-bg-danger); + border-top: 0.5px solid var(--v2-state-border-danger); + + [data-slot="diagnostic"] { + display: flex; + align-items: baseline; + gap: 6px; + font-family: var(--font-family-mono); + font-size: var(--font-size-small); + line-height: var(--line-height-large); + } + + [data-slot="diagnostic-label"] { + color: var(--v2-state-fg-danger); + font-weight: var(--font-weight-medium); + text-transform: uppercase; + letter-spacing: -0.5px; + flex-shrink: 0; + } + + [data-slot="diagnostic-location"] { + color: var(--v2-state-fg-danger); + flex-shrink: 0; + } + + [data-slot="diagnostic-message"] { + color: var(--v2-state-fg-danger); + word-break: break-word; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + line-clamp: 3; + overflow: hidden; + } +} + +[data-component="user-message"] [data-slot="user-message-text"], +[data-component="text-part"], +[data-component="reasoning-part"], +[data-component="tool-output"], +[data-component="bash-output"], +[data-component="edit-content"], +[data-component="write-content"], +[data-component="todos"], +[data-component="diagnostics"], +.error-card { + -webkit-user-select: text; + user-select: text; +} + +[data-component="tool-part-wrapper"] { + width: 100%; +} + +[data-component="dock-prompt"][data-kind="permission"] { + position: relative; + display: flex; + flex-direction: column; + gap: 0; + min-height: 0; + max-height: 100dvh; + + [data-slot="permission-body"] { + display: flex; + flex-direction: column; + gap: 16px; + flex: 1; + min-height: 0; + padding: 12px 12px 0; + } + + [data-slot="permission-header"] { + padding: 0; + } + + [data-slot="permission-row"] { + display: grid; + grid-template-columns: 20px 1fr; + column-gap: 8px; + align-items: start; + } + + [data-slot="permission-row"][data-variant="header"] { + align-items: center; + } + + [data-slot="permission-icon"] { + display: inline-flex; + align-items: center; + justify-content: center; + } + + [data-slot="permission-icon"] [data-component="icon"] { + color: var(--icon-warning-base); + } + + [data-slot="permission-header-title"] { + font-family: var(--font-family-sans); + font-size: 14px; + font-weight: var(--font-weight-medium); + line-height: var(--line-height-large); + color: var(--v2-text-text-base); + min-width: 0; + } + + [data-slot="permission-content"] { + display: flex; + flex-direction: column; + gap: 4px; + flex: 1; + min-height: 0; + } + + [data-slot="permission-hint"] { + font-family: var(--font-family-sans); + font-size: 14px; + font-weight: var(--font-weight-regular); + line-height: var(--line-height-large); + color: var(--v2-text-text-muted); + padding: 0; + } + + [data-slot="permission-patterns"] { + display: flex; + flex-direction: column; + gap: 6px; + margin-top: 8px; + margin-bottom: 16px; + padding: 1px 0 8px; + flex: 1; + min-height: 0; + overflow-y: auto; + scrollbar-width: none; + -ms-overflow-style: none; + + &::-webkit-scrollbar { + display: none; + } + + code { + font-size: 14px; + line-height: var(--line-height-large); + } + } + + [data-slot="permission-footer"] { + display: flex; + align-items: center; + justify-content: space-between; + flex-shrink: 0; + padding: 32px 8px 8px; + margin-top: -24px; + } + + [data-slot="permission-footer-actions"] { + display: flex; + align-items: center; + gap: 8px; + + [data-component="button"] { + padding-left: 12px; + padding-right: 12px; + } + } +} + +[data-component="dock-prompt"][data-kind="question"] { + position: relative; + display: flex; + flex-direction: column; + gap: 0; + min-height: 0; + max-height: var(--question-prompt-max-height, 100dvh); + + [data-slot="question-body"] { + display: flex; + flex-direction: column; + gap: 16px; + flex: 1; + min-height: 0; + padding: 8px 8px 0; + } + + [data-slot="question-header"] { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 0 10px; + } + + [data-slot="question-header-title"] { + font-family: var(--font-family-sans); + font-size: 14px; + font-weight: var(--font-weight-medium); + line-height: var(--line-height-large); + color: var(--v2-text-text-base); + min-width: 0; + } + + [data-slot="question-header-actions"] { + display: flex; + align-items: center; + gap: 8px; + flex-shrink: 0; + } + + [data-slot="question-progress"] { + display: flex; + align-items: center; + gap: 8px; + flex-shrink: 0; + } + + [data-slot="question-progress-segment"] { + width: 16px; + height: 16px; + padding: 0; + border: 0; + background: transparent; + border-radius: 999px; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + touch-action: manipulation; + + &::after { + content: ""; + width: 16px; + height: 2px; + border-radius: 999px; + background-color: var(--v2-icon-icon-muted); + transition: background-color 0.2s ease; + } + + &[data-active="true"]::after { + background-color: var(--icon-strong-base); + } + + &[data-answered="true"]::after { + background-color: var(--icon-interactive-base); + } + + &:disabled { + cursor: not-allowed; + opacity: 0.6; + } + } + + [data-slot="question-content"] { + display: flex; + flex-direction: column; + gap: 4px; + flex: 1; + min-height: 0; + } + + [data-slot="question-text"] { + font-family: var(--font-family-sans); + font-size: 14px; + font-weight: var(--font-weight-medium); + line-height: var(--line-height-large); + color: var(--v2-text-text-base); + padding: 0 10px; + flex-shrink: 0; + -webkit-user-select: text; + user-select: text; + } + + [data-slot="question-hint"] { + font-family: var(--font-family-sans); + font-size: 13px; + font-weight: var(--font-weight-regular); + line-height: var(--line-height-large); + color: var(--v2-text-text-muted); + padding: 0 10px; + flex-shrink: 0; + } + + [data-slot="question-options"] { + display: flex; + flex-direction: column; + gap: 6px; + margin-top: 12px; + padding: 1px 1px 16px; + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; + scrollbar-width: none; + -ms-overflow-style: none; + + &::-webkit-scrollbar { + display: none; + } + } + + [data-slot="question-option"] { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 8px 8px 8px 10px; + background-color: var(--v2-background-bg-layer-01); + border: 0.5px solid var(--v2-border-border-base); + border-radius: 6px; + box-shadow: none; + text-align: left; + width: 100%; + cursor: pointer; + transition: + background-color 0.15s ease, + border-color 0.15s ease, + box-shadow 0.15s ease; + + &:hover:not([data-picked="true"]) { + background-color: var(--v2-background-bg-base); + } + + /* selected → the amicode design-system light yellow (soft fill + accent + edge), matching the single-accent state pattern. */ + &[data-picked="true"] { + background-color: var(--accent-fill-soft); + border-color: var(--accent-edge); + box-shadow: none; + } + + /* keyboard/current accent → yellow, not the default blue focus ring */ + &:focus-visible { + outline: 1px solid var(--accent); + outline-offset: -1px; + } + + &:disabled { + cursor: not-allowed; + opacity: 0.6; + } + } + + [data-slot="question-option-check"] { + display: inline-flex; + transform: translateY(2px); + } + + [data-slot="question-option-box"] { + width: 16px; + height: 16px; + padding: 2px; + border-radius: var(--radius-sm); + border: 0.5px solid var(--v2-border-border-base); + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + background-color: transparent; + transition: + background-color 0.15s ease, + border-color 0.15s ease; + + [data-component="icon"] { + opacity: 0; + color: var(--icon-base); + } + + &[data-type="radio"] { + border-radius: 999px; + } + + [data-slot="question-option-radio-dot"] { + width: 6px; + height: 6px; + border-radius: 999px; + background-color: var(--v2-background-bg-layer-01); + opacity: 0; + } + + /* picked radio/checkbox → yellow fill with near-black ink (black-on-yellow, + per the design system), not the default blue interactive color. */ + &[data-picked="true"] { + border-color: var(--accent); + + [data-component="icon"] { + opacity: 1; + color: var(--accent-ink); + } + + &[data-type="checkbox"] { + background-color: var(--accent); + } + + &[data-type="radio"] { + background-color: var(--accent); + [data-slot="question-option-radio-dot"] { + opacity: 1; + background-color: var(--accent-ink); + } + } + } + } + + [data-slot="question-option-main"] { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; + flex: 1; + } + + [data-slot="option-label"] { + font-family: var(--font-family-sans); + font-size: 14px; + font-weight: var(--font-weight-medium); + line-height: var(--line-height-large); + color: var(--v2-text-text-base); + } + + [data-slot="option-description"] { + font-family: var(--font-family-sans); + font-size: 14px; + font-weight: var(--font-weight-regular); + line-height: var(--line-height-large); + color: var(--v2-text-text-muted); + min-width: 0; + overflow-wrap: anywhere; + white-space: normal; + } + + [data-slot="question-custom"] { + display: flex; + flex-direction: column; + gap: 8px; + } + + [data-slot="question-custom-input-wrap"] { + padding-left: 36px; + } + + [data-slot="question-custom-input"] { + width: 100%; + padding: 0; + border: 0; + border-radius: 0; + background: transparent; + box-shadow: none; + outline: none; + font-family: var(--font-family-sans); + font-size: 14px; + font-weight: var(--font-weight-regular); + line-height: var(--line-height-large); + color: var(--v2-text-text-muted); + min-width: 0; + cursor: text; + resize: none; + overflow: hidden; + overflow-wrap: anywhere; + + &::placeholder { + color: var(--v2-text-text-muted); + } + + &:focus-visible { + outline: 0.5px solid var(--v2-border-border-focus); + outline-offset: 2px; + border-radius: var(--radius-xs); + } + + &:disabled { + opacity: 0.6; + } + } + + /* Text-card (kind: "text") — the textarea is the ONLY affordance, so it + needs visible chrome (border + background) unlike the choice-card's + secondary custom-input which is borderless/transparent. */ + [data-slot="question-text-form"] { + padding: 4px 8px; + } + + [data-slot="question-text-form"] > [data-slot="question-custom-input"] { + border: 1px solid var(--v2-border-border-base); + border-radius: var(--radius-md); + padding: 8px 12px; + background: var(--v2-background-bg-layer-02); + min-height: 36px; + + &:focus-visible { + border-color: var(--v2-border-border-focus); + outline: none; + } + } + + [data-slot="question-footer"] { + display: flex; + align-items: center; + justify-content: space-between; + flex-shrink: 0; + padding: 32px 8px 8px; + margin-top: -24px; + } + + [data-slot="question-footer-actions"] { + display: flex; + align-items: center; + gap: 8px; + } +} + +/* One contained component (Kate 2026-07-24): the DockTray (footer) used to be a + SEPARATE surface sitting BEHIND the shell (its own bg + border, z-index 0), + pulled up so it peeked out below — reading as a second card sliding off the + first. Unify: the dock-prompt itself becomes the single card (one surface, one + 1px ring, one radius, clipped), and the shell + tray flatten into sections of + it, with the footer a plain bottom action bar under a hairline divider. */ +[data-component="dock-prompt"][data-kind="question"] { + background-color: var(--v2-background-bg-layer-02); + box-shadow: 0 0 0 1px var(--border-weak-base); + border-radius: 10px; + overflow: clip; +} +[data-component="dock-prompt"][data-kind="question"] [data-dock-surface="shell"] { + background: transparent; + box-shadow: none; + border-radius: 0; + overflow: visible; + z-index: auto; +} +[data-component="dock-prompt"][data-kind="question"] [data-dock-surface="tray"] { + background: transparent; + border: 0; + border-radius: 0; + margin-top: 0; + z-index: auto; +} +[data-component="dock-prompt"][data-kind="question"] [data-slot="question-footer"] { + margin-top: 0; + padding: 10px; + border-top: 1px solid var(--border-weak-base); +} + +[data-component="question-minimized-dock"] { + margin-bottom: 8px; + + [data-slot="question-minimized-trigger"] { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: 10px 12px; + border: 0; + background: transparent; + color: var(--v2-text-text-base); + text-align: left; + cursor: pointer; + touch-action: manipulation; + + &:disabled { + cursor: not-allowed; + opacity: 0.6; + } + + [data-component="icon"] { + transform: rotate(180deg); + } + } + + [data-slot="question-minimized-summary"] { + flex: 1; + font-family: var(--font-family-sans); + font-size: 14px; + font-weight: var(--font-weight-medium); + line-height: var(--line-height-large); + color: var(--v2-text-text-base); + } +} + +[data-component="question-answers"] { + display: flex; + flex-direction: column; + gap: 12px; + padding: 8px 0; + + [data-slot="question-answer-item"] { + display: flex; + flex-direction: column; + gap: 2px; + font-size: 13px; + + [data-slot="question-text"] { + color: var(--v2-text-text-muted); + } + + [data-slot="answer-text"] { + color: var(--v2-text-text-base); + } + } +} + +[data-component="edit-tool"], +[data-component="write-tool"], +[data-component="apply-patch-tool"] { + > [data-component="collapsible"].tool-collapsible { + gap: 0px; + } + + > [data-component="collapsible"] > [data-slot="collapsible-content"] { + border: none; + background: transparent; + } + + > [data-component="collapsible"] > [data-slot="collapsible-trigger"][aria-expanded="true"] { + position: sticky; + top: var(--sticky-accordion-top, 0px); + z-index: 20; + height: calc(32px + var(--tool-content-gap)); + padding-bottom: var(--tool-content-gap); + background-color: var(--v2-background-bg-base); + } +} + +[data-component="accordion"][data-scope="apply-patch"] { + [data-slot="accordion-trigger"] { + background-color: var(--v2-background-bg-base) !important; + } + + [data-slot="apply-patch-trigger-content"] { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + gap: 20px; + } + + [data-slot="apply-patch-file-info"] { + flex-grow: 1; + display: flex; + align-items: center; + gap: 20px; + min-width: 0; + } + + [data-slot="apply-patch-file-name-container"] { + display: flex; + flex-grow: 1; + min-width: 0; + overflow: hidden; + } + + [data-slot="apply-patch-directory"] { + color: var(--v2-text-text-muted); + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + direction: rtl; + text-align: left; + } + + [data-slot="apply-patch-filename"] { + color: var(--v2-text-text-base); + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + [data-slot="apply-patch-trigger-actions"] { + flex-shrink: 0; + display: flex; + gap: 16px; + align-items: center; + justify-content: flex-end; + } + + [data-slot="apply-patch-change"] { + font-family: var(--font-family-sans); + font-size: var(--font-size-small); + font-weight: var(--font-weight-medium); + } + + [data-slot="apply-patch-change"][data-type="added"] { + color: var(--icon-diff-add-base); + } + + [data-slot="apply-patch-change"][data-type="removed"] { + color: var(--icon-diff-delete-base); + } + + [data-slot="apply-patch-change"][data-type="modified"] { + color: var(--icon-diff-modified-base); + } +} + +[data-component="apply-patch-file-diff"] { + border-radius: inherit; + overflow-x: hidden; + overflow-y: visible; + scrollbar-width: none; + -ms-overflow-style: none; + + &::-webkit-scrollbar { + display: none; + } + + [data-component="diff"] { + border-radius: inherit; + overflow: hidden; + } +} + +[data-component="tool-loaded-file"] { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 0 4px 28px; + font-family: var(--font-family-sans); + font-size: var(--font-size-small); + font-weight: var(--font-weight-regular); + line-height: var(--line-height-large); + color: var(--v2-text-text-muted); + + [data-component="icon"] { + flex-shrink: 0; + color: var(--icon-weak); + } +} + +/* AMICODE: loaded-file rows are buttons now (open the file via the bridge) — + * strip the button chrome, keep the row look, earn a hover tint. */ +button[data-component="tool-loaded-file"] { + appearance: none; + background: none; + border: none; + width: 100%; + text-align: left; + cursor: pointer; +} + +button[data-component="tool-loaded-file"]:hover span { + color: var(--v2-text-text-base); + text-decoration: underline; + text-underline-offset: 2px; +} + +body[data-new-layout] [data-component="user-message"] { + font-weight: 440; + + [data-slot="user-message-text"] { + background: var(--v2-background-bg-layer-01); + } + + [data-slot="user-message-comments"] { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + width: min(320px, 82%); + margin-left: auto; + + &[data-bounded] { + width: 318px; + max-width: 100%; + margin-top: 8px; + } + + [data-component="tooltip-v2-trigger"] { + width: 100%; + } + + [data-component="button-v2"] { + font-family: var(--v2-font-family-sans, "Inter", sans-serif); + font-weight: 530; + font-variation-settings: + "wght" 530, + "slnt" 0; + } + } + + [data-slot="user-message-body"] + [data-slot="user-message-attachments"], + [data-slot="user-message-comments"] + [data-slot="user-message-attachments"] { + margin-top: 8px; + } + + [data-slot="user-message-text"][data-comments] { + width: 342px; + padding-bottom: 12px; + } +} + +[data-color-scheme="light"] body[data-new-layout] [data-component="user-message"] [data-slot="user-message-text"] { + background: var(--v2-background-bg-layer-02); +} + +body:not([data-new-layout]) { + [data-component="user-message"] { + color: var(--text-strong); + + [data-slot="user-message-attachments"] + [data-slot="user-message-body"] { + margin-top: 8px; + } + + [data-slot="user-message-attachment"] { + background: var(--surface-weak); + border: 1px solid var(--border-weak-base); + + &:hover { + border-color: var(--border-strong-base); + } + + &[data-type="image"] { + width: 48px; + height: 48px; + border: 1px solid var(--border-weak-base); + + &::after { + content: none; + } + } + } + + [data-slot="user-message-attachment-name"] { + color: var(--text-base); + } + + [data-slot="user-message-text"] { + background: var(--surface-base); + border: 1px solid var(--border-weak-base); + border-radius: 6px; + } + + .text-text-strong { + color: var(--text-strong); + } + } + + [data-slot="compaction-part-line"] { + background: var(--border-weak-base); + } + + [data-component="reasoning-part"] { + color: var(--text-base); + + [data-component="markdown"], + [data-component="markdown"] :is(strong, b) { + color: var(--text-weak); + } + } + + [data-component="bash-output"] { + border: 1px solid var(--border-weak-base); + } + + [data-component="edit-trigger"], + [data-component="write-trigger"] { + [data-slot="message-part-title"] { + color: var(--text-base); + } + + [data-slot="message-part-title-spinner"], + [data-slot="message-part-directory"] { + color: var(--text-weak); + } + + [data-slot="message-part-title-text"], + [data-slot="message-part-filename"] { + color: var(--text-strong); + } + } + + [data-component="edit-content"], + [data-component="write-content"] { + border-top: 1px solid var(--border-weaker-base); + } + + [data-component="exa-tool-output"] { + color: var(--text-base); + } + + [data-slot="exa-tool-link"], + [data-slot="exa-tool-link"]:hover, + [data-slot="exa-tool-link"]:visited { + color: var(--text-interactive-base); + } + + [data-slot="message-part-todo-content"][data-completed="completed"] { + color: var(--text-weaker); + } + + [data-component="diagnostics"] { + background-color: var(--surface-critical-weak); + border-top: 1px solid var(--border-critical-base); + + [data-slot="diagnostic-label"], + [data-slot="diagnostic-message"] { + color: var(--text-on-critical-base); + } + + [data-slot="diagnostic-location"] { + color: var(--text-on-critical-weak); + } + } + + [data-component="dock-prompt"][data-kind="permission"] { + [data-slot="permission-header-title"] { + color: var(--text-strong); + } + + [data-slot="permission-hint"] { + color: var(--text-weak); + } + } + + [data-component="dock-prompt"][data-kind="question"] { + [data-slot="question-header-title"], + [data-slot="question-text"], + [data-slot="option-label"] { + color: var(--text-strong); + } + + [data-slot="question-hint"] { + color: var(--text-weak); + } + + [data-slot="question-option"] { + background-color: var(--surface-raised-stronger-non-alpha); + border: 1px solid var(--border-weak-base); + + &:hover:not([data-picked="true"]) { + background-color: var(--background-base); + } + + &[data-picked="true"] { + background-color: var(--surface-interactive-weak); + } + } + + [data-slot="question-option-box"] { + border: 1px solid var(--border-weak-base); + + [data-slot="question-option-radio-dot"] { + background-color: var(--background-stronger); + } + } + + [data-slot="option-description"], + [data-slot="question-custom-input"] { + color: var(--text-base); + } + + [data-slot="question-custom-input"] { + &::placeholder { + color: var(--text-weak); + } + + &:focus-visible { + outline: 1px solid var(--border-interactive-base); + } + } + + [data-slot="question-text-form"] > [data-slot="question-custom-input"] { + border-color: var(--border-weak-base); + background: var(--surface-weak); + + &:focus-visible { + border-color: var(--border-interactive-base); + outline: none; + } + } + } + + [data-component="question-answers"] { + [data-slot="question-text"] { + color: var(--text-weak); + } + + [data-slot="answer-text"] { + color: var(--text-strong); + } + } + + :is([data-component="edit-tool"], [data-component="write-tool"], [data-component="apply-patch-tool"]) + > [data-component="collapsible"] + > [data-slot="collapsible-trigger"][aria-expanded="true"], + [data-component="accordion"][data-scope="apply-patch"] [data-slot="accordion-trigger"] { + background-color: var(--background-stronger) !important; + } + + [data-component="accordion"][data-scope="apply-patch"] { + [data-slot="apply-patch-directory"] { + color: var(--text-base); + } + + [data-slot="apply-patch-filename"] { + color: var(--text-strong); + } + } + + [data-component="tool-loaded-file"] { + color: var(--text-weak); + } +} diff --git a/packages/app-bundle/overlay/packages/session-ui/src/components/message-part.tsx b/packages/app-bundle/overlay/packages/session-ui/src/components/message-part.tsx new file mode 100644 index 00000000..77fbb706 --- /dev/null +++ b/packages/app-bundle/overlay/packages/session-ui/src/components/message-part.tsx @@ -0,0 +1,3103 @@ +import { AmicoSpinner } from "@opencode-ai/ui/amico-spinner" +import { ThinkingLine, turnTokens } from "@opencode-ai/ui/amicode-thinking" +import { shellRowLabel } from "@opencode-ai/ui/amicode-shell-row" +import { sessionHasAmicodeParts } from "@opencode-ai/ui/amicode-rail-gate" +import { amicoBrainRef, emitAmicoBrainHover } from "@opencode-ai/ui/amicode-brain-ref" +import { copyTextToClipboard } from "../util/clipboard" +import { + Component, + createEffect, + createMemo, + createSignal, + For, + Match, + onMount, + Show, + Switch, + onCleanup, + Index, + type JSX, + type ComponentProps, +} from "solid-js" +import { createStore } from "solid-js/store" +import stripAnsi from "strip-ansi" +import { Dynamic } from "solid-js/web" +import { + AgentPart, + AssistantMessage, + FilePart, + Message as MessageType, + Part as PartType, + ReasoningPart, + Session, + SkillPart, + TextPart, + ToolPart, + UserMessage, + Todo, + QuestionAnswer, + QuestionInfo, +} from "@opencode-ai/sdk/v2" +import { useData } from "../context" +import { useFileComponent } from "@opencode-ai/ui/context/file" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { type UiI18n, useI18n } from "@opencode-ai/ui/context/i18n" +import { BasicTool, GenericTool } from "./basic-tool" +import { AmicodeToolCard, AmicoSkillChip } from "@opencode-ai/ui/amicode-card" +import { openFileInEditor } from "@opencode-ai/ui/amicode-bridge" +import { Accordion } from "@opencode-ai/ui/accordion" +import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header" +import { Collapsible } from "@opencode-ai/ui/collapsible" +import { FileIcon } from "@opencode-ai/ui/file-icon" +import { Icon } from "@opencode-ai/ui/icon" +import { ToolErrorCard } from "./tool-error-card" +import { Checkbox } from "@opencode-ai/ui/checkbox" +import { DiffChanges } from "@opencode-ai/ui/diff-changes" +import { Markdown } from "./markdown" +import { skillBody } from "./message-part-skill" +import { ImagePreview } from "@opencode-ai/ui/image-preview" +import { getDirectory as _getDirectory, getFilename } from "@opencode-ai/core/util/path" +import { AttachmentCardV2 } from "../v2/components/attachment-card-v2" +import { CommentCardV2 } from "../v2/components/comment-card-v2" +import { checksum } from "@opencode-ai/core/util/encode" +import { Tooltip } from "@opencode-ai/ui/tooltip" +import { IconButton } from "@opencode-ai/ui/icon-button" +import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" +import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" +import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" +import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" +import { Spinner } from "@opencode-ai/ui/spinner" +import { TextShimmer } from "@opencode-ai/ui/text-shimmer" +import { AnimatedCountList } from "./tool-count-summary" +import { ToolStatusTitle } from "./tool-status-title" +import { patchFiles } from "./apply-patch-file" +import { animate } from "motion" +import { attached, inline, kind, typeLabel } from "./message-file" +import { readPartText } from "./message-part-text" +import { SessionProgressIndicatorV2 } from "../v2/components/session-progress-indicator-v2" + +const reducedMotion = () => + typeof window !== "undefined" && !!window.matchMedia?.("(prefers-reduced-motion: reduce)").matches + +// Amicode webview: the execCommand trick and navigator.clipboard both die in +// the chat iframe — copyTextToClipboard bridges to the extension host there. +async function writeClipboard(text: string): Promise { + return copyTextToClipboard(text) +} + +function ShellSubmessage(props: { text: string; animate?: boolean }) { + let widthRef: HTMLSpanElement | undefined + let valueRef: HTMLSpanElement | undefined + + onMount(() => { + if (!props.animate) return + // The initial render collapses width to 0 and hides the value behind a blur; + // if the user prefers reduced motion, snap straight to the resting state + // instead of animating (and instead of leaving it stuck collapsed/hidden). + if (reducedMotion()) { + if (widthRef) widthRef.style.width = "auto" + if (valueRef) { + valueRef.style.opacity = "1" + valueRef.style.filter = "blur(0px)" + } + return + } + requestAnimationFrame(() => { + if (widthRef) { + animate(widthRef, { width: "auto" }, { type: "spring", visualDuration: 0.25, bounce: 0 }) + } + if (valueRef) { + animate(valueRef, { opacity: 1, filter: "blur(0px)" }, { duration: 0.32, ease: [0.16, 1, 0.3, 1] }) + } + }) + }) + + return ( + + + + + {props.text} + + + + + ) +} + +interface Diagnostic { + range: { + start: { line: number; character: number } + end: { line: number; character: number } + } + message: string + severity?: number +} + +function getDiagnostics( + diagnosticsByFile: Record | undefined, + filePath: string | undefined, +): Diagnostic[] { + if (!diagnosticsByFile || !filePath) return [] + const diagnostics = diagnosticsByFile[filePath] ?? [] + return diagnostics.filter((d) => d.severity === 1).slice(0, 3) +} + +function DiagnosticsDisplay(props: { diagnostics: Diagnostic[] }): JSX.Element { + const i18n = useI18n() + return ( + 0}> +
+ + {(diagnostic) => ( +
+ {i18n.t("ui.messagePart.diagnostic.error")} + + [{diagnostic.range.start.line + 1}:{diagnostic.range.start.character + 1}] + + {diagnostic.message} +
+ )} +
+
+
+ ) +} + +export interface MessageProps { + message: MessageType + parts: PartType[] + actions?: UserActions + showAssistantCopyPartID?: string | null + showReasoningSummaries?: boolean + useV2Actions?: boolean + comments?: UserMessageComment[] +} + +export type SessionAction = (input: { sessionID: string; messageID: string }) => Promise | void + +export type UserActions = { + fork?: SessionAction + revert?: SessionAction + openAttachment?: (file: FilePart) => void +} + +export type UserMessageComment = { + path: string + comment: string + selection?: { + startLine: number + endLine: number + } +} + +export interface MessagePartProps { + part: PartType + message: MessageType + hideDetails?: boolean + defaultOpen?: boolean + toolOpen?: boolean + onToolOpenChange?: (open: boolean) => void + deferToolContent?: boolean + virtualizeDiff?: boolean + onContentRendered?: () => void + showAssistantCopyPartID?: string | null + turnDurationMs?: number + useV2Actions?: boolean + // amicode: set when this part is the surviving (latest) member of a + // collapsed run of ≥2 identical (problem, entity, action) receipts + // (./message-part-groups's PartGroup + ../amicode/receipt-runs.ts). Only + // AmicodeToolCard reads it; every other part type ignores it. + count?: number +} + +function MessageActionButton( + props: Pick, "disabled" | "onMouseDown" | "onClick" | "aria-label"> & { + icon: "check" | "copy" | "reset" + label: JSX.Element + useV2?: boolean + }, +) { + const icon = () => (props.icon === "copy" ? "outline-copy" : props.icon) + return ( + + + + } + > + + } + size="normal" + variant="ghost-muted" + disabled={props.disabled} + onMouseDown={props.onMouseDown} + onClick={props.onClick} + aria-label={props["aria-label"]} + /> + + + ) +} + +export type PartComponent = Component + +export const PART_MAPPING: Record = {} + +const TEXT_RENDER_PACE_MS = 24 +const TEXT_RENDER_IMMEDIATE = 512 +const TEXT_RENDER_SNAP = /[\s.,!?;:)\]]/ + +function step(size: number) { + if (size <= 12) return 2 + if (size <= 48) return 4 + if (size <= 96) return 8 + return Math.min(256, Math.ceil(size / 4)) +} + +function next(text: string, start: number) { + const end = Math.min(text.length, start + step(text.length - start)) + const max = Math.min(text.length, end + 8) + for (let i = end; i < max; i++) { + if (TEXT_RENDER_SNAP.test(text[i] ?? "")) return i + 1 + } + return end +} + +function createPacedValue(getValue: () => string, live?: () => boolean) { + const [value, setValue] = createSignal(getValue()) + let shown = getValue() + let timeout: ReturnType | undefined + + const clear = () => { + if (!timeout) return + clearTimeout(timeout) + timeout = undefined + } + + const sync = (text: string) => { + shown = text + setValue(text) + } + + const run = () => { + timeout = undefined + const text = getValue() + if (!live?.()) { + sync(text) + return + } + if (!text.startsWith(shown) || text.length <= shown.length) { + sync(text) + return + } + if (text.length - shown.length <= TEXT_RENDER_IMMEDIATE) { + sync(text) + return + } + const end = next(text, shown.length) + sync(text.slice(0, end)) + if (end < text.length) timeout = setTimeout(run, TEXT_RENDER_PACE_MS) + } + + createEffect(() => { + const text = getValue() + if (!live?.()) { + clear() + sync(text) + return + } + if (!text.startsWith(shown) || text.length < shown.length) { + clear() + sync(text) + return + } + if (text.length - shown.length <= TEXT_RENDER_IMMEDIATE) { + clear() + sync(text) + return + } + if (text.length === shown.length || timeout) return + timeout = setTimeout(run, TEXT_RENDER_PACE_MS) + }) + + onCleanup(() => { + clear() + }) + + return value +} + +function PacedMarkdown(props: { text: string; cacheKey: string; streaming: boolean }) { + const value = createPacedValue( + () => props.text, + () => props.streaming, + ) + + return ( + + + + ) +} + +function relativizeProjectPath(path: string, directory?: string) { + if (!path) return "" + if (!directory) return path + if (directory === "/") return path + if (directory === "\\") return path + if (path === directory) return "" + + const separator = directory.includes("\\") ? "\\" : "/" + const prefix = directory.endsWith(separator) ? directory : directory + separator + if (!path.startsWith(prefix)) return path + return path.slice(directory.length) +} + +function getDirectory(path: string | undefined) { + const data = useData() + return relativizeProjectPath(_getDirectory(path), data.directory) +} + +import type { IconProps } from "@opencode-ai/ui/icon" +import { normalize, resolveFileDiff } from "./session-diff" + +export type ToolInfo = { + icon: IconProps["name"] + title: string + subtitle?: string +} + +function agentTitle(i18n: UiI18n, type?: string) { + if (!type) return i18n.t("ui.tool.agent.default") + return i18n.t("ui.tool.agent", { type }) +} + +const agentTones: Record = { + ask: "var(--icon-agent-ask-base)", + build: "var(--icon-agent-build-base)", + docs: "var(--icon-agent-docs-base)", + plan: "var(--icon-agent-plan-base)", +} + +const v2AgentTones: Record = { + build: "var(--v2-agent-build-solid)", + explore: "var(--v2-agent-explore-solid)", + plan: "var(--v2-agent-plan-solid)", + review: "var(--v2-agent-review-solid)", + writer: "var(--v2-agent-writer-solid)", +} + +const agentThemeColors: Record = { + primary: "var(--text-interactive-base)", + secondary: "var(--text-base)", + accent: "var(--icon-info-base)", + success: "var(--icon-success-base)", + warning: "var(--icon-warning-base)", + error: "var(--icon-critical-base)", + info: "var(--icon-info-base)", +} + +const v2AgentThemeColors: Record = { + primary: "var(--v2-text-text-accent)", + secondary: "var(--v2-text-text-muted)", + accent: "var(--v2-icon-icon-accent)", + success: "var(--v2-state-fg-success)", + warning: "var(--v2-state-fg-warning)", + error: "var(--v2-state-fg-danger)", + info: "var(--v2-state-fg-info)", +} + +const agentPalette = [ + "var(--icon-agent-ask-base)", + "var(--icon-agent-build-base)", + "var(--icon-agent-docs-base)", + "var(--icon-agent-plan-base)", + "var(--syntax-info)", + "var(--syntax-success)", + "var(--syntax-warning)", + "var(--syntax-property)", + "var(--syntax-constant)", + "var(--text-diff-add-base)", + "var(--text-diff-delete-base)", + "var(--icon-warning-base)", +] + +function tone(name: string) { + let hash = 0 + for (const char of name) hash = (hash * 31 + char.charCodeAt(0)) >>> 0 + return agentPalette[hash % agentPalette.length] +} + +function taskAgent( + raw: unknown, + list?: readonly { name: string; color?: string }[], +): { name?: string; color?: string; v2Color?: string } { + if (typeof raw !== "string" || !raw) return {} + const key = raw.toLowerCase() + const item = list?.find((entry) => entry.name === raw || entry.name.toLowerCase() === key) + const v2Tone = item?.color ? undefined : v2AgentTones[key] + const color = agentColor(item?.color, agentThemeColors) ?? agentTones[key] ?? tone(key) + const v2Color = agentColor(item?.color, v2AgentThemeColors) ?? v2Tone ?? color + return { + name: item?.name ?? `${raw[0]!.toUpperCase()}${raw.slice(1)}`, + color, + v2Color, + } +} + +function agentColor(value: string | undefined, themeColors: Record) { + if (!value) return + return themeColors[value] ?? value +} + +function newLayout() { + return typeof document !== "undefined" && document.body.hasAttribute("data-new-layout") +} + +function webSearchProviderLabel(provider: unknown) { + if (provider === "parallel") return "Parallel Web Search" + if (provider === "exa") return "Exa Web Search" + return "Web Search" +} + +export function getToolInfo( + tool: string, + input: any = {}, + metadata: Record | undefined = {}, +): ToolInfo { + const i18n = useI18n() + switch (tool) { + case "read": + return { + icon: "glasses", + title: i18n.t("ui.tool.read"), + subtitle: input.filePath ? getFilename(input.filePath) : undefined, + } + case "list": + return { + icon: "bullet-list", + title: i18n.t("ui.tool.list"), + subtitle: input.path ? getFilename(input.path) : undefined, + } + case "glob": + return { + icon: "magnifying-glass-menu", + title: i18n.t("ui.tool.glob"), + subtitle: input.pattern, + } + case "grep": + return { + icon: "magnifying-glass-menu", + title: i18n.t("ui.tool.grep"), + subtitle: input.pattern, + } + case "webfetch": + return { + icon: "window-cursor", + title: i18n.t("ui.tool.webfetch"), + subtitle: input.url, + } + case "websearch": + return { + icon: "window-cursor", + title: webSearchProviderLabel(metadata?.provider), + subtitle: input.query, + } + case "task": { + const type = + typeof input.subagent_type === "string" && input.subagent_type + ? input.subagent_type[0]!.toUpperCase() + input.subagent_type.slice(1) + : undefined + return { + icon: "task", + title: agentTitle(i18n, type), + subtitle: input.description, + } + } + case "bash": + case "shell": + return { + icon: "console", + title: i18n.t("ui.tool.shell"), + subtitle: input.command, + } + case "edit": + return { + icon: "code-lines", + title: i18n.t("ui.messagePart.title.edit"), + subtitle: input.filePath ? getFilename(input.filePath) : undefined, + } + case "write": + return { + icon: "code-lines", + title: i18n.t("ui.messagePart.title.write"), + subtitle: input.filePath ? getFilename(input.filePath) : undefined, + } + case "patch": + case "apply_patch": + return { + icon: "code-lines", + title: i18n.t("ui.tool.patch"), + subtitle: input.files?.length + ? `${input.files.length} ${i18n.t(input.files.length > 1 ? "ui.common.file.other" : "ui.common.file.one")}` + : undefined, + } + case "todowrite": + return { + icon: "checklist", + title: i18n.t("ui.tool.todos"), + } + case "question": + return { + icon: "bubble-5", + title: i18n.t("ui.tool.questions"), + } + case "skill": + return { + icon: "brain", + // AMICODE: name the kind here too. This is the compact/summary path, where the + // fallback-only use of ui.tool.skill left an activated skill looking like a bare tool. + title: input.name ? `${i18n.t("ui.tool.skill")} · ${input.name}` : i18n.t("ui.tool.skill"), + } + default: + return { + icon: "mcp", + title: tool, + } + } +} + +function urls(text: string | undefined) { + if (!text) return [] + const seen = new Set() + return [...text.matchAll(/https?:\/\/[^\s<>"'`)\]]+/g)] + .map((item) => item[0].replace(/[),.;:!?]+$/g, "")) + .filter((item) => { + if (seen.has(item)) return false + seen.add(item) + return true + }) +} + +function sessionLink(id: string | undefined, href?: (id: string) => string | undefined) { + if (!id) return undefined + return href?.(id) +} + +function taskSession( + input: Record, + parentID: string | undefined, + sessions: Session[] | undefined, + agents?: readonly { name: string; color?: string }[], +) { + if (!parentID) return undefined + const description = typeof input.description === "string" ? input.description : "" + const agent = taskAgent(input.subagent_type, agents).name + return (sessions ?? []) + .filter((session) => session.parentID === parentID && !session.time?.archived) + .filter((session) => (description ? session.title.startsWith(description) : true)) + .filter((session) => (agent ? session.title.includes(`@${agent}`) : true)) + .sort((a, b) => (b.time.created ?? 0) - (a.time.created ?? 0))[0]?.id +} + +const HIDDEN_TOOLS = new Set(["todowrite"]) + +function list(value: T[] | undefined | null, fallback: T[]) { + if (Array.isArray(value)) return value + return fallback +} + +function same(a: readonly T[] | undefined, b: readonly T[] | undefined) { + if (a === b) return true + if (!a || !b) return false + if (a.length !== b.length) return false + return a.every((x, i) => x === b[i]) +} + +// Grouping primitives live in ./message-part-groups (pure, unit-tested); re-export +// the public names so `@opencode-ai/ui/message-part` stays the stable import path. +export { + groupParts, + sameGroups, + isContextGroupTool, + isShellGroupTool, + isEditGroupTool, + type PartGroup, + type PartRef, +} from "./message-part-groups" +import { + groupParts, + sameGroups, + isContextGroupTool, + isShellGroupTool, + isEditGroupTool, + type PartGroup, + type PartRef, +} from "./message-part-groups" +import { parseDiffSentinel } from "@opencode-ai/ui/amicode-receipt" +import { editRowDiff, editRowFilePath, editRowLabel } from "@opencode-ai/ui/amicode-edit-row" +import { + collapseReceiptRuns, + receiptRunKey, + type ReceiptCandidate, + type ReceiptKey, +} from "@opencode-ai/ui/amicode-receipt-runs" + +function index(items: readonly T[]) { + return new Map(items.map((item) => [item.id, item] as const)) +} + +// amicode: does this resolved part carry a mergeable receipt key? Only +// completed amicode_* tool calls whose AMICODE_DIFF sentinel parses (and +// whose entity isn't inline-view-eligible — see receipt-runs.ts) are +// candidates; everything else (still running, errored, not amicode_*, no/ +// unparseable sentinel) gets `key: undefined` and can never merge. +function amicodeReceiptCandidateKey(part: PartType | undefined): { key?: ReceiptKey; seq?: number } { + if (!part || part.type !== "tool" || !part.tool.startsWith("amicode_")) return {} + if (part.state.status !== "completed") return {} + const sentinel = parseDiffSentinel(part.state.output) + return { key: receiptRunKey(sentinel), seq: sentinel?.seq } +} + +function sameAmicodeCounts(a: Map, b: Map) { + if (a === b) return true + if (a.size !== b.size) return false + for (const [k, v] of a) if (b.get(k) !== v) return false + return true +} + +// amicode: second pass over groupParts's output that collapses consecutive +// "part" entries which are amicode_* receipts sharing (problem, entity, +// action) into one — the fix for "these repeated amico cards add a lot of +// clutter" (four amicode_* calls updating the same entity rendering four +// identical cards). context/shell groups and any part that isn't a +// collapse-eligible receipt pass through unchanged; see ../amicode/ +// receipt-runs.ts for the (conservative, tested) matching rules. +function collapseAmicodeGroups( + groups: readonly PartGroup[], + resolvePart: (ref: PartRef) => PartType | undefined, +): { groups: PartGroup[]; counts: Map } { + const candidates: ReceiptCandidate[] = groups.map((group) => { + if (group.type !== "part") return { ref: group } + const { key, seq } = amicodeReceiptCandidateKey(resolvePart(group.ref)) + return { ref: group, key, seq } + }) + const runs = collapseReceiptRuns(candidates) + const counts = new Map() + const survivors = runs.map((run) => { + if (run.count > 1) counts.set(run.latestRef.key, run.count) + return run.latestRef + }) + return { groups: survivors, counts } +} + +export function renderable(part: PartType, showReasoningSummaries = true) { + if (part.type === "tool") { + if (HIDDEN_TOOLS.has(part.tool)) return false + if (part.tool === "question") return part.state.status !== "pending" && part.state.status !== "running" + return true + } + if (part.type === "text") return !!part.text?.trim() + if (part.type === "reasoning") return showReasoningSummaries && !!part.text?.trim() + return !!PART_MAPPING[part.type] +} + +function toolDefaultOpen(tool: string, shell = false, edit = false) { + if (tool === "bash" || tool === "shell") return shell + if (tool === "edit" || tool === "write" || tool === "patch" || tool === "apply_patch") return edit +} + +export function partDefaultOpen(part: PartType, shell = false, edit = false) { + if (part.type !== "tool") return + return toolDefaultOpen(part.tool, shell, edit) +} + +export function AssistantParts(props: { + messages: AssistantMessage[] + showAssistantCopyPartID?: string | null + turnDurationMs?: number + useV2Actions?: boolean + working?: boolean + showReasoningSummaries?: boolean + shellToolDefaultOpen?: boolean + editToolDefaultOpen?: boolean +}) { + const data = useData() + const emptyParts: PartType[] = [] + const emptyTools: ToolPart[] = [] + const msgs = createMemo(() => index(props.messages)) + const part = createMemo( + () => + new Map( + props.messages.map((message) => [message.id, index(list(data.store.part?.[message.id], emptyParts))] as const), + ), + ) + + const grouped = createMemo( + () => + groupParts( + props.messages.flatMap((message) => + list(data.store.part?.[message.id], emptyParts) + .filter((part) => renderable(part, props.showReasoningSummaries ?? true)) + .map((part) => ({ + messageID: message.id, + part, + })), + ), + ), + [] as PartGroup[], + { equals: sameGroups }, + ) + + const last = createMemo(() => grouped().at(-1)?.key) + + // amicode: collapse runs of consecutive amicode_* receipts sharing (problem, + // entity, action) into one card + count (../amicode/receipt-runs.ts). Drives + // the render list below; `last()` above stays keyed off the UNCOLLAPSED + // groups so the busy/streaming indicator keeps comparing against the raw + // most-recent group (its key survives collapsing whenever it's genuinely the + // latest receipt, which is the only case that indicator cares about). + const collapsed = createMemo( + () => collapseAmicodeGroups(grouped(), (ref) => part().get(ref.messageID)?.get(ref.partID)), + { groups: [] as PartGroup[], counts: new Map() }, + { equals: (a, b) => sameGroups(a.groups, b.groups) && sameAmicodeCounts(a.counts, b.counts) }, + ) + + // amicode: tokens generated so far this turn (output + reasoning across the + // turn's assistant messages) — feeds the thinking line's live token chip. + // Reactive: re-runs as the store updates message.tokens.* while streaming. + const turnTokenCount = createMemo(() => turnTokens(props.messages)) + + // amicode: is THIS turn in Amico's domain? (does it carry an amicode_* tool + // part). The only turn-level domain signal available in the UI — Amico's + // presence (the working lane below; the rail waking) keys off it. Plain-prose + // turns are the *normal chat*: no Amico chrome in the flow + // (spec-20260712-amico-third-actor). + // Shares the rail's gate (rail-gate.ts) rather than re-testing `amicode_*` + // here. Both used the tool-name test independently, so a SHELL-driven amicode + // session lost the chips AND Amico's presence mark together — the H-mark read + // as "inactive" while a solve was running at iteration 29 (2026-07-29). One + // definition, one place to widen. + const inDomainTurn = createMemo(() => + sessionHasAmicodeParts(props.messages, (id) => list(data.store.part?.[id], emptyParts) as never), + ) + + return ( + <> + {/* amicode: identity lives in the entity rail now (Amico's body), NOT a + per-turn stamp — plain-prose turns read as the normal chat. On an + in-domain turn, Amico's working presence rides in an offset accent + lane BELOW the streamed parts (see the lane after ). + spec-20260712-amico-third-actor. */} + + {(entryAccessor) => { + const entryType = createMemo(() => entryAccessor().type) + + return ( + + + {(() => { + const parts = createMemo( + () => { + const entry = entryAccessor() + if (entry.type !== "context") return emptyTools + return entry.refs + .map((ref) => part().get(ref.messageID)?.get(ref.partID)) + .filter((part): part is ToolPart => !!part && isContextGroupTool(part)) + }, + emptyTools, + { equals: same }, + ) + const busy = createMemo(() => props.working && last() === entryAccessor().key) + + return ( + 0}> + + + ) + })()} + + + {(() => { + const parts = createMemo( + () => { + const entry = entryAccessor() + if (entry.type !== "shell") return emptyTools + return entry.refs + .map((ref) => part().get(ref.messageID)?.get(ref.partID)) + .filter((part): part is ToolPart => !!part && isShellGroupTool(part)) + }, + emptyTools, + { equals: same }, + ) + const busy = createMemo(() => props.working && last() === entryAccessor().key) + + return ( + 0}> + + + ) + })()} + + + {(() => { + const parts = createMemo( + () => { + const entry = entryAccessor() + if (entry.type !== "edit") return emptyTools + return entry.refs + .map((ref) => part().get(ref.messageID)?.get(ref.partID)) + .filter((part): part is ToolPart => !!part && isEditGroupTool(part)) + }, + emptyTools, + { equals: same }, + ) + const busy = createMemo(() => props.working && last() === entryAccessor().key) + + return ( + 0}> + + + ) + })()} + + + {(() => { + const message = createMemo(() => { + const entry = entryAccessor() + if (entry.type !== "part") return + return msgs().get(entry.ref.messageID) + }) + const item = createMemo(() => { + const entry = entryAccessor() + if (entry.type !== "part") return + return part().get(entry.ref.messageID)?.get(entry.ref.partID) + }) + // amicode: >1 when this part survived a receipt-run collapse + // (../amicode/receipt-runs.ts); undefined for every other part. + const count = createMemo(() => collapsed().counts.get(entryAccessor().key)) + + return ( + + + + + + ) + })()} + + + ) + }} + + {/* amicode: Amico's working presence — the offset accent lane. Shown only + while an in-domain turn streams (state === "on"), decoupled from any + card-suppression: the thinking block (wave + cycling gerund + live + elapsed/tokens) runs without a duplicate mark. Pops out the + moment working flips false. spec-20260712-amico-third-actor. */} + +
+ +
+
+ + ) +} + +// One-line command for a bash part's row in the shell group. Logic lives in +// ../amicode/shell-row.ts so the fallback chain is testable — it shipped a bug +// where a pending part rendered the model's prose description as if it were the +// command, which read as a hard error for the command's whole duration. +function shellCommandText(part: ToolPart): string { + return shellRowLabel(part) +} + +function contextToolDetail(part: ToolPart): string | undefined { + const info = getToolInfo( + part.tool, + part.state.input ?? {}, + "metadata" in part.state ? part.state.metadata : undefined, + ) + if (info.subtitle) return info.subtitle + if (part.state.status === "error") return part.state.error + if ((part.state.status === "running" || part.state.status === "completed") && part.state.title) + return part.state.title + const description = part.state.input?.description + if (typeof description === "string") return description + return undefined +} + +function contextToolTrigger(part: ToolPart, i18n: ReturnType) { + const input = (part.state.input ?? {}) as Record + const path = typeof input.path === "string" ? input.path : "/" + const filePath = typeof input.filePath === "string" ? input.filePath : undefined + const pattern = typeof input.pattern === "string" ? input.pattern : undefined + const include = typeof input.include === "string" ? input.include : undefined + const offset = typeof input.offset === "number" ? input.offset : undefined + const limit = typeof input.limit === "number" ? input.limit : undefined + + switch (part.tool) { + case "read": { + const args: string[] = [] + if (offset !== undefined) args.push("offset=" + offset) + if (limit !== undefined) args.push("limit=" + limit) + return { + title: i18n.t("ui.tool.read"), + subtitle: filePath ? getFilename(filePath) : "", + args, + } + } + case "list": + return { + title: i18n.t("ui.tool.list"), + subtitle: getDirectory(path), + } + case "glob": + return { + title: i18n.t("ui.tool.glob"), + subtitle: getDirectory(path), + args: pattern ? ["pattern=" + pattern] : [], + } + case "grep": { + const args: string[] = [] + if (pattern) args.push("pattern=" + pattern) + if (include) args.push("include=" + include) + return { + title: i18n.t("ui.tool.grep"), + subtitle: getDirectory(path), + args, + } + } + default: { + const info = getToolInfo(part.tool, input, "metadata" in part.state ? part.state.metadata : undefined) + return { + title: info.title, + subtitle: info.subtitle || contextToolDetail(part), + args: [], + } + } + } +} + +function contextToolSummary(parts: ToolPart[]) { + const read = parts.filter((part) => part.tool === "read").length + const search = parts.filter((part) => part.tool === "glob" || part.tool === "grep").length + const list = parts.filter((part) => part.tool === "list").length + return { read, search, list } +} + +function ExaOutput(props: { output?: string }) { + const links = createMemo(() => urls(props.output)) + + return ( + 0}> +
+ + ) +} + +export function registerPartComponent(type: string, component: PartComponent) { + PART_MAPPING[type] = component +} + +export function Message(props: MessageProps) { + return ( + + + {(userMessage) => ( + + )} + + + {(assistantMessage) => ( + + )} + + + ) +} + +export function AssistantMessageDisplay(props: { + message: AssistantMessage + parts: PartType[] + showAssistantCopyPartID?: string | null + showReasoningSummaries?: boolean + useV2Actions?: boolean +}) { + const emptyTools: ToolPart[] = [] + const part = createMemo(() => index(props.parts)) + const grouped = createMemo( + () => + groupParts( + props.parts + .filter((part) => renderable(part, props.showReasoningSummaries ?? true)) + .map((part) => ({ + messageID: props.message.id, + part, + })), + ), + [] as PartGroup[], + { equals: sameGroups }, + ) + + // amicode: same receipt-run collapse as AssistantParts, above — see + // ../amicode/receipt-runs.ts. + const collapsed = createMemo( + () => collapseAmicodeGroups(grouped(), (ref) => part().get(ref.partID)), + { groups: [] as PartGroup[], counts: new Map() }, + { equals: (a, b) => sameGroups(a.groups, b.groups) && sameAmicodeCounts(a.counts, b.counts) }, + ) + + return ( + + {(entryAccessor) => { + const entryType = createMemo(() => entryAccessor().type) + + return ( + + + {(() => { + const parts = createMemo( + () => { + const entry = entryAccessor() + if (entry.type !== "context") return emptyTools + return entry.refs + .map((ref) => part().get(ref.partID)) + .filter((part): part is ToolPart => !!part && isContextGroupTool(part)) + }, + emptyTools, + { equals: same }, + ) + + return ( + 0}> + + + ) + })()} + + + {(() => { + const parts = createMemo( + () => { + const entry = entryAccessor() + if (entry.type !== "shell") return emptyTools + return entry.refs + .map((ref) => part().get(ref.partID)) + .filter((part): part is ToolPart => !!part && isShellGroupTool(part)) + }, + emptyTools, + { equals: same }, + ) + + return ( + 0}> + + + ) + })()} + + + {(() => { + const parts = createMemo( + () => { + const entry = entryAccessor() + if (entry.type !== "edit") return emptyTools + return entry.refs + .map((ref) => part().get(ref.partID)) + .filter((part): part is ToolPart => !!part && isEditGroupTool(part)) + }, + emptyTools, + { equals: same }, + ) + + return ( + 0}> + + + ) + })()} + + + {(() => { + const item = createMemo(() => { + const entry = entryAccessor() + if (entry.type !== "part") return + return part().get(entry.ref.partID) + }) + const count = createMemo(() => collapsed().counts.get(entryAccessor().key)) + + return ( + + + + ) + })()} + + + ) + }} + + ) +} + +export function ContextToolGroup(props: { + parts: ToolPart[] + busy?: boolean + open?: boolean + onOpenChange?: (open: boolean) => void + onSizeChange?: () => void +}) { + const i18n = useI18n() + const [localOpen, setLocalOpen] = createSignal(false) + const open = () => props.open ?? localOpen() + const pending = createMemo( + () => + !!props.busy || props.parts.some((part) => part.state.status === "pending" || part.state.status === "running"), + ) + const summary = createMemo(() => contextToolSummary(props.parts)) + const handleOpenChange = (value: boolean) => { + if (props.open === undefined) setLocalOpen(value) + props.onOpenChange?.(value) + props.onSizeChange?.() + } + // amicode: hovering the group chip glances at every member node on the map + const glanceAll = () => { + for (const p of props.parts.slice(0, 8)) emitAmicoBrainHover(amicoBrainRef(p.tool, p.state.input ?? {})) + } + + return ( + part.id).join(",")} + > + +
+ + + + + + + + + +
+
+ +
+ + {(partAccessor) => { + const trigger = createMemo(() => contextToolTrigger(partAccessor(), i18n)) + const running = createMemo( + () => partAccessor().state.status === "pending" || partAccessor().state.status === "running", + ) + return ( +
+
+
+
+
+
+ + + + + {trigger().subtitle} + + + + {(arg) => {arg}} + + +
+
+
+
+
+
+ ) + }} +
+
+
+
+ ) +} + +// AMICODE (spec B): consecutive bash commands collapse into one row so a long +// run of shell calls doesn't dominate the timeline. Mirrors ContextToolGroup's +// markup (reuses its CSS slots) with a shell label + per-command list. A lone +// command never reaches here — groupParts leaves it as a full bash card. +export function ShellToolGroup(props: { parts: ToolPart[]; busy?: boolean; onSizeChange?: () => void }) { + const [open, setOpen] = createSignal(false) + const pending = createMemo( + () => + !!props.busy || props.parts.some((part) => part.state.status === "pending" || part.state.status === "running"), + ) + const count = createMemo(() => props.parts.length) + const handleOpenChange = (value: boolean) => { + setOpen(value) + props.onSizeChange?.() + } + // amicode: hovering the group chip glances at every member node on the map + const glanceAll = () => { + for (const p of props.parts.slice(0, 8)) emitAmicoBrainHover(amicoBrainRef(p.tool, p.state.input ?? {})) + } + + return ( + part.id).join(",")} + > + +
+ + + + + + {count()} {count() === 1 ? "command" : "commands"} + + + +
+
+ +
+ + {(partAccessor) => { + const cmd = createMemo(() => shellCommandText(partAccessor())) + const running = createMemo( + () => partAccessor().state.status === "pending" || partAccessor().state.status === "running", + ) + const errored = createMemo(() => partAccessor().state.status === "error") + return ( +
+
+
+
+
+
+ + + + + + failed + + +
+
+
+
+
+
+ ) + }} +
+
+
+
+ ) +} + +// AMICODE (spec B shape): consecutive file mutations (edit/write/patch) collapse +// into one row so a long authoring run doesn't dominate the timeline. Mirrors +// ShellToolGroup's markup (reuses its CSS slots) with per-file rows that keep +// their diff stats. A lone mutation never reaches here — groupParts leaves it +// as a full card, whose inline diff is worth the space. Row labels come from +// @opencode-ai/ui/amicode-edit-row (pure, tested): a pending part without a +// filePath can never fill the row with prose. +export function EditToolGroup(props: { parts: ToolPart[]; busy?: boolean; onSizeChange?: () => void }) { + const [open, setOpen] = createSignal(false) + const pending = createMemo( + () => + !!props.busy || props.parts.some((part) => part.state.status === "pending" || part.state.status === "running"), + ) + const count = createMemo(() => props.parts.length) + // Unique targets: "4 changes in 3 files" reads truer than a bare call count + // when the same file is re-edited mid-run. + const fileCount = createMemo(() => { + const paths = props.parts + .map((part) => editRowFilePath(part)) + .filter((path): path is string => typeof path === "string") + return new Set(paths).size + }) + // Aggregate +/- across the run — DiffChanges sums an array itself. Parts that + // haven't recorded a filediff yet (still pending) simply don't contribute. + const diffs = createMemo(() => + props.parts + .map((part) => editRowDiff(part)) + .filter((diff): diff is { additions: number; deletions: number } => !!diff), + ) + const handleOpenChange = (value: boolean) => { + setOpen(value) + props.onSizeChange?.() + } + // amicode: hovering the group chip glances at every member node on the map + const glanceAll = () => { + for (const p of props.parts.slice(0, 8)) emitAmicoBrainHover(amicoBrainRef(p.tool, p.state.input ?? {})) + } + + return ( + part.id).join(",")} + > + +
+ + + + + + {count()} {count() === 1 ? "change" : "changes"} + 0}> + {" "} + in {fileCount()} {fileCount() === 1 ? "file" : "files"} + + + 0}> + + + + +
+
+ +
+ + {(partAccessor) => { + const label = createMemo(() => editRowLabel(partAccessor())) + const diff = createMemo(() => editRowDiff(partAccessor())) + const running = createMemo( + () => partAccessor().state.status === "pending" || partAccessor().state.status === "running", + ) + const errored = createMemo(() => partAccessor().state.status === "error") + return ( +
+
+
+
+
+
+ + + + {(d) => } + + + failed + + +
+
+
+
+
+
+ ) + }} +
+
+
+
+ ) +} + +function UserMessageComments(props: { comments: UserMessageComment[]; bounded: boolean }) { + const i18n = useI18n() + const [state, setState] = createStore({ expanded: false }) + const comments = createMemo(() => (props.bounded && !state.expanded ? props.comments.slice(0, 5) : props.comments)) + + return ( +
+ + {(comment) => ( + + )} + + 5 && !state.expanded}> + setState("expanded", true)}> + {i18n.t("ui.common.showMore")} + + +
+ ) +} + +export function UserMessageDisplay(props: { + message: UserMessage + parts: PartType[] + actions?: UserActions + useV2Actions?: boolean + comments?: UserMessageComment[] +}) { + const data = useData() + const dialog = useDialog() + const i18n = useI18n() + const [state, setState] = createStore({ + copied: false, + busy: false, + }) + const copied = () => state.copied + const busy = () => state.busy + + const textPart = createMemo( + () => props.parts?.find((p) => p.type === "text" && !(p as TextPart).synthetic) as TextPart | undefined, + ) + + const text = createMemo(() => textPart()?.text || "") + + const files = createMemo(() => (props.parts?.filter((p) => p.type === "file") as FilePart[]) ?? []) + + const attachments = createMemo(() => files().filter(attached)) + + const messageComments = createMemo(() => (newLayout() ? (props.comments ?? []) : [])) + + const inlineFiles = createMemo(() => files().filter(inline)) + + const agents = createMemo(() => (props.parts?.filter((p) => p.type === "agent") as AgentPart[]) ?? []) + + const skills = createMemo(() => (props.parts?.filter((p) => p.type === "skill") as SkillPart[]) ?? []) + + const model = createMemo(() => { + const providerID = props.message.model?.providerID + const modelID = props.message.model?.modelID + if (!providerID || !modelID) return "" + const match = data.store.provider?.all?.get(providerID) + return match?.models?.[modelID]?.name ?? modelID + }) + const timefmt = createMemo(() => new Intl.DateTimeFormat(i18n.locale(), { timeStyle: "short" })) + + const stamp = createMemo(() => { + const created = props.message.time?.created + if (typeof created !== "number") return "" + return timefmt().format(created) + }) + + const metaHead = createMemo(() => { + const agent = props.message.agent + const items = [agent ? agent[0]?.toUpperCase() + agent.slice(1) : "", model()] + return items.filter((x) => !!x).join("\u00A0\u00B7\u00A0") + }) + + const metaTail = stamp + + const openImagePreview = (url: string, alt?: string) => { + dialog.show(() => ) + } + + const handleCopy = async () => { + const content = text() + if (!content) return + if (await writeClipboard(content)) { + setState("copied", true) + setTimeout(() => setState("copied", false), 2000) + } + } + + const revert = () => { + const act = props.actions?.revert + if (!act || busy()) return + setState("busy", true) + void Promise.resolve() + .then(() => + act({ + sessionID: props.message.sessionID, + messageID: props.message.id, + }), + ) + .finally(() => setState("busy", false)) + } + + const renderAttachments = () => ( + 0}> +
+ + {(file) => { + const type = kind(file) + const name = file.filename ?? i18n.t("ui.message.attachment.alt") + + return ( + { + if (type === "image") openImagePreview(file.url, name) + }} + > + + + {name} +
+ } + > + {name} +
+