@kabmo-labs/kabmo 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/client/index.html +1 -1
- package/dist/client/index.js +403 -384
- package/dist/client/static/js/{index-0wvbca0b.js → index-bpptfkz2.js} +905 -875
- package/dist/client/static/js/index-k315p1e8.css +1 -0
- package/package.json +10 -1
- package/src/client/components/common/code-surface/documents.test.ts +120 -0
- package/src/client/components/common/code-surface/documents.ts +201 -0
- package/src/client/components/common/code-surface/extensions.test.ts +110 -0
- package/src/client/components/common/code-surface/extensions.ts +162 -0
- package/src/client/components/common/code-surface/handle.ts +102 -0
- package/src/client/components/common/code-surface/index.tsx +253 -314
- package/src/client/components/common/code-surface/keymap.test.ts +133 -0
- package/src/client/components/common/code-surface/keymap.ts +267 -0
- package/src/client/components/common/code-surface/marks.ts +74 -0
- package/src/client/components/common/code-surface/shiki.ts +234 -0
- package/src/client/components/common/code-surface/sync.test.ts +185 -0
- package/src/client/components/common/code-surface/sync.ts +58 -0
- package/src/client/components/common/code-surface/theme.test.ts +202 -0
- package/src/client/components/common/code-surface/theme.ts +111 -0
- package/src/client/components/common/editor-view-buttons/index.test.tsx +148 -0
- package/src/client/components/common/editor-view-buttons/index.tsx +109 -0
- package/src/client/components/common/header-controls/index.tsx +24 -1
- package/src/client/components/common/header-controls/shell-chrome.test.tsx +272 -0
- package/src/client/components/common/shell-update-button/Modal.tsx +163 -0
- package/src/client/components/common/shell-update-button/index.test.tsx +251 -0
- package/src/client/components/common/shell-update-button/index.tsx +125 -0
- package/src/client/components/common/toolbar-row/index.test.tsx +166 -0
- package/src/client/components/common/toolbar-row/index.tsx +189 -0
- package/src/client/components/common/workspace-options-menu/index.tsx +19 -1
- package/src/client/components/layout/desktop-layout/TopNavbar.tsx +5 -6
- package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +41 -78
- package/src/client/components/layout/desktop-layout/index.tsx +55 -69
- package/src/client/components/layout/desktop-layout/resizer/index.tsx +6 -3
- package/src/client/components/layout/session-sidebar/CategoryHeader.tsx +24 -0
- package/src/client/components/layout/session-sidebar/CategoryHeader.worktree.test.tsx +126 -0
- package/src/client/components/layout/session-sidebar/CategoryItem.tsx +4 -0
- package/src/client/components/layout/session-sidebar/Footer.tsx +8 -15
- package/src/client/components/layout/session-sidebar/SessionItem.tsx +12 -0
- package/src/client/components/layout/session-sidebar/SessionList.tsx +4 -0
- package/src/client/components/layout/session-sidebar/SessionsView.tsx +24 -0
- package/src/client/components/layout/session-sidebar/WorktreeBadge.test.ts +48 -0
- package/src/client/components/layout/session-sidebar/WorktreeBadge.tsx +55 -0
- package/src/client/components/mobile/LayoutWrapper.tsx +23 -16
- package/src/client/components/mobile/RightSidebar.tsx +21 -10
- package/src/client/components/mobile/SessionSidebar.tsx +8 -0
- package/src/client/components/mobile/header-titlebar.test.tsx +172 -0
- package/src/client/components/mobile/mobile-main-view/Header.tsx +10 -1
- package/src/client/components/mobile/mobile-right-sidebar/FullDiff.tsx +3 -0
- package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +63 -38
- package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.test.tsx +137 -0
- package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.tsx +14 -3
- package/src/client/components/mobile/mobile-session-sidebar/Footer.tsx +9 -21
- package/src/client/components/mobile/mobile-session-sidebar/Header.tsx +6 -2
- package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +16 -0
- package/src/client/components/mobile/mobile-session-sidebar/List.tsx +4 -0
- package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +5 -0
- package/src/client/components/mobile/mobile-session-sidebar/WorktreeDialog.tsx +49 -0
- package/src/client/components/mobile/worktree-session.ts +51 -0
- package/src/client/components/settings/Modal.tsx +8 -0
- package/src/client/components/settings/categories/AppearanceSettings.tsx +3 -0
- package/src/client/components/settings/categories/ProjectSettings.tsx +22 -8
- package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.test.tsx +167 -0
- package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.tsx +184 -0
- package/src/client/components/settings/categories/project-settings/WorktreeSettings.tsx +146 -0
- package/src/client/components/settings/categories/project-settings/worktree-row.test.ts +83 -0
- package/src/client/components/settings/categories/project-settings/worktree-row.ts +47 -0
- package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +36 -55
- package/src/client/components/workspace/chat-timeline/PendingChatView.tsx +8 -0
- package/src/client/components/workspace/chat-timeline/SystemNotice.tsx +9 -4
- package/src/client/components/workspace/chat-timeline/TaskResultContent.tsx +9 -7
- package/src/client/components/workspace/chat-timeline/index.tsx +5 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.test.tsx +132 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.tsx +120 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.test.tsx +319 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.tsx +285 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.test.ts +2 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/hashline-patch/index.tsx +34 -29
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/AstEdit.tsx +11 -9
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Bash.tsx +10 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Edit.tsx +25 -8
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.test.tsx +146 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.tsx +32 -17
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.test.ts +114 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.tsx +111 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Read.tsx +7 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/SearchTool.tsx +9 -59
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ArtifactBody.tsx +2 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.test.ts +27 -28
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.tsx +39 -26
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ExcerptCode.tsx +8 -8
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/HighlightedCode.tsx +35 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/JsonCodeBlock.tsx +4 -4
- package/src/client/components/workspace/diff-panel/MergeDiffView.tsx +179 -0
- package/src/client/components/workspace/diff-panel/ThemedMergeDiffView.tsx +24 -0
- package/src/client/components/workspace/diff-panel/Toolbar.tsx +224 -186
- package/src/client/components/workspace/diff-panel/ToolbarSegments.tsx +98 -0
- package/src/client/components/workspace/diff-panel/index.tsx +76 -145
- package/src/client/components/workspace/diff-panel/merge.ts +315 -0
- package/src/client/components/workspace/editor/Body.tsx +4 -7
- package/src/client/components/workspace/editor/CommandPalette.tsx +2 -2
- package/src/client/components/workspace/editor/FileDocument.tsx +58 -63
- package/src/client/components/workspace/editor/FilePane.tsx +4 -7
- package/src/client/components/workspace/editor/FindWidget.tsx +7 -2
- package/src/client/components/workspace/editor/Toolbar.tsx +220 -123
- package/src/client/components/workspace/editor/index.tsx +7 -9
- package/src/client/components/workspace/file-explorer/ContextMenu.tsx +2 -1
- package/src/client/components/workspace/file-explorer/Modals.tsx +40 -12
- package/src/client/components/workspace/file-explorer/OutlineSection.test.tsx +18 -0
- package/src/client/components/workspace/file-explorer/OutlineSection.tsx +10 -3
- package/src/client/components/workspace/file-explorer/RootMenu.tsx +14 -5
- package/src/client/components/workspace/file-explorer/Toolbar.tsx +74 -0
- package/src/client/components/workspace/file-explorer/TreeItem.tsx +38 -3
- package/src/client/components/workspace/file-explorer/filter.ts +47 -0
- package/src/client/components/workspace/file-explorer/index.tsx +77 -64
- package/src/client/components/workspace/file-explorer/local-edit.test.ts +135 -0
- package/src/client/components/workspace/search-panel/Body.tsx +1 -1
- package/src/client/components/workspace/worktree-modal/index.test.tsx +118 -0
- package/src/client/components/workspace/worktree-modal/index.tsx +261 -0
- package/src/client/hooks/chat/omp/reconnect.test.ts +338 -0
- package/src/client/hooks/chat/omp/reconnect.ts +187 -0
- package/src/client/hooks/chat/omp/run-liveness.test.ts +71 -0
- package/src/client/hooks/chat/omp/run-liveness.ts +65 -0
- package/src/client/hooks/chat/omp/session-list.activity.test.ts +114 -0
- package/src/client/hooks/chat/omp/session-list.test-util.ts +164 -0
- package/src/client/hooks/chat/omp/session-list.test.ts +30 -150
- package/src/client/hooks/chat/omp/session-list.tsx +69 -9
- package/src/client/hooks/chat/timeline/generating.ts +19 -2
- package/src/client/hooks/chat/timeline/index.ts +27 -28
- package/src/client/hooks/chat/timeline/lost-run.test.ts +329 -0
- package/src/client/hooks/chat/timeline/lost-run.ts +177 -0
- package/src/client/hooks/chat/timeline/send-execute.test.ts +2 -0
- package/src/client/hooks/chat/timeline/send-steer.test.ts +2 -0
- package/src/client/hooks/chat/timeline/send.test.ts +2 -0
- package/src/client/hooks/chat/timeline/send.ts +11 -1
- package/src/client/hooks/chat/timeline/session-load.test-util.ts +205 -0
- package/src/client/hooks/chat/timeline/session-load.test.ts +61 -162
- package/src/client/hooks/chat/timeline/session-load.ts +69 -37
- package/src/client/hooks/chat/timeline/session-switch.test.ts +65 -0
- package/src/client/hooks/chat/timeline/session-switch.ts +63 -0
- package/src/client/hooks/chat/timeline/spawn-target.ts +52 -0
- package/src/client/hooks/chat/timeline/tool-open.test.ts +74 -0
- package/src/client/hooks/chat/timeline/tool-open.ts +52 -8
- package/src/client/hooks/code/deferred-highlight.test.ts +182 -0
- package/src/client/hooks/code/deferred-highlight.ts +129 -0
- package/src/client/hooks/editor/find-state.ts +1 -1
- package/src/client/hooks/editor/theme.test.ts +206 -0
- package/src/client/hooks/editor/theme.ts +82 -0
- package/src/client/hooks/editor/use-editor-drop.test.ts +101 -0
- package/src/client/hooks/editor/use-editor-drop.ts +67 -14
- package/src/client/hooks/editor/use-editor-find.ts +7 -7
- package/src/client/hooks/editor/use-outline-view.test.ts +182 -0
- package/src/client/hooks/editor/use-outline-view.ts +94 -7
- package/src/client/hooks/editor/use-scroll-restore.test.ts +55 -2
- package/src/client/hooks/editor/use-scroll-restore.ts +124 -4
- package/src/client/hooks/editor/use-search-reveal.ts +1 -1
- package/src/client/hooks/ui/long-press.test.ts +174 -0
- package/src/client/hooks/ui/long-press.ts +112 -0
- package/src/client/hooks/ui/settings-modal.ts +56 -0
- package/src/client/hooks/ui/updates/shell-download.test.ts +75 -0
- package/src/client/hooks/ui/updates/shell-download.ts +153 -28
- package/src/client/hooks/ui/updates/shell-offer.test.ts +213 -0
- package/src/client/hooks/ui/updates/shell-update.test.ts +140 -0
- package/src/client/hooks/ui/updates/shell-update.ts +119 -0
- package/src/client/hooks/workspace/diff-file.ts +29 -10
- package/src/client/hooks/workspace/diff-git-actions.ts +147 -0
- package/src/client/hooks/workspace/editor-view.ts +61 -0
- package/src/client/hooks/workspace/file-create.ts +41 -11
- package/src/client/hooks/workspace/file-listing.ts +56 -1
- package/src/client/hooks/workspace/file-tabs.test.ts +62 -5
- package/src/client/hooks/workspace/file-tabs.ts +24 -11
- package/src/client/hooks/workspace/file-tree-actions.ts +37 -5
- package/src/client/hooks/workspace/navbar-session-actions.ts +72 -0
- package/src/client/hooks/workspace/worktree/index.ts +165 -0
- package/src/client/lib/desktop/bridge.ts +45 -0
- package/src/client/lib/search-reveal.ts +1 -1
- package/src/client/main.tsx +5 -1
- package/src/client/tailwind.css +56 -8
- package/src/server/index.ts +7 -2
- package/src/server/lib/auth/guard.test.ts +28 -0
- package/src/server/lib/auth/guard.ts +10 -0
- package/src/server/lib/bundler/titlebar-css.test.ts +42 -1
- package/src/server/lib/db/schema.ts +4 -0
- package/src/server/lib/db/worktree-registry.server.ts +91 -0
- package/src/server/lib/fs/canonical.ts +52 -0
- package/src/server/lib/fs/relative-path.ts +48 -0
- package/src/server/lib/fs/root-worktree.test.ts +116 -0
- package/src/server/lib/fs/root.ts +39 -10
- package/src/server/lib/fs/worktree/carry.ts +134 -0
- package/src/server/lib/fs/worktree/create.test.ts +249 -0
- package/src/server/lib/fs/worktree/create.ts +262 -0
- package/src/server/lib/fs/worktree/errors.ts +43 -0
- package/src/server/lib/fs/worktree/list.test.ts +207 -0
- package/src/server/lib/fs/worktree/list.ts +142 -0
- package/src/server/lib/fs/worktree/location.test.ts +206 -0
- package/src/server/lib/fs/worktree/location.ts +114 -0
- package/src/server/lib/fs/worktree/project-id.ts +53 -0
- package/src/server/lib/fs/worktree/remove.test.ts +242 -0
- package/src/server/lib/fs/worktree/remove.ts +152 -0
- package/src/server/lib/fs/worktree/settings.server.ts +56 -0
- package/src/server/lib/fs/worktree/topology.ts +142 -0
- package/src/server/lib/lifecycle/claim-port.test.ts +156 -0
- package/src/server/lib/lifecycle/port-guard.test.ts +20 -164
- package/src/server/lib/lifecycle/port-guard.ts +39 -7
- package/src/server/lib/lifecycle/probe-health.test.ts +166 -0
- package/src/server/lib/lifecycle/probe.test.ts +77 -164
- package/src/server/lib/lifecycle/probe.ts +121 -13
- package/src/server/lib/omp/rpc/frame-fold-signals.test.ts +112 -0
- package/src/server/lib/omp/rpc/frame-fold.ts +10 -5
- package/src/server/lib/omp/session/sidebar-activity.server.test.ts +162 -0
- package/src/server/lib/omp/session/sidebar-activity.server.ts +70 -0
- package/src/server/lib/omp/session/sidebar-data.canonical.test.ts +155 -0
- package/src/server/lib/omp/session/sidebar-data.server.ts +28 -1
- package/src/server/lib/omp/session/telemetry/compute.ts +13 -1
- package/src/server/lib/omp/session/telemetry.test.ts +33 -0
- package/src/server/lib/panels/local-edits.server.ts +33 -0
- package/src/server/lib/panels/sync.fixture.test-util.ts +92 -0
- package/src/server/lib/panels/sync.local-edits.test.ts +150 -0
- package/src/server/lib/panels/sync.server.test.ts +125 -147
- package/src/server/lib/panels/sync.server.ts +153 -48
- package/src/server/lib/realtime/signals.server.ts +10 -0
- package/src/server/lib/realtime/topics/catalog.server.ts +4 -1
- package/src/server/lib/realtime/topics.server.ts +16 -0
- package/src/server/lib/updates/shell-assets.test.ts +114 -0
- package/src/server/lib/updates/shell-assets.ts +200 -0
- package/src/server/lib/updates/shell-cache.ts +105 -0
- package/src/server/lib/updates/shell-pages.test.ts +187 -0
- package/src/server/lib/updates/shell-pages.ts +157 -0
- package/src/server/lib/updates/shell-release-desktop.test.ts +166 -0
- package/src/server/lib/updates/shell-release.test.ts +59 -71
- package/src/server/lib/updates/shell-release.ts +178 -198
- package/src/server/routes/fs/action-create.test.ts +18 -13
- package/src/server/routes/fs/action-nested.test.ts +184 -0
- package/src/server/routes/fs/action.ts +47 -16
- package/src/server/routes/fs/git-merge.test.ts +11 -1
- package/src/server/routes/fs/read.test.ts +24 -7
- package/src/server/routes/fs/scope.test.ts +15 -10
- package/src/server/routes/fs/scope.ts +3 -14
- package/src/server/routes/index.ts +2 -0
- package/src/server/routes/updates/android.ts +6 -1
- package/src/server/routes/updates/desktop.ts +59 -0
- package/src/server/routes/updates/index.ts +2 -0
- package/src/server/routes/updates/ios.ts +5 -1
- package/src/server/routes/worktrees/create.ts +138 -0
- package/src/server/routes/worktrees/index.ts +25 -0
- package/src/server/routes/worktrees/list.ts +134 -0
- package/src/server/routes/worktrees/remove.ts +102 -0
- package/src/server/routes/worktrees/routes.test.ts +326 -0
- package/src/server/routes/worktrees/shared.ts +90 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.test.ts +36 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.ts +18 -0
- package/src/shared/lib/chat/timeline/run-signals.test.ts +2 -0
- package/src/shared/lib/code/editor/comment-syntax.test.ts +37 -0
- package/src/shared/lib/code/editor/comment-syntax.ts +55 -0
- package/src/shared/lib/code/editor/find-replace.test.ts +134 -0
- package/src/shared/lib/code/editor/{find.ts → find-replace.ts} +23 -107
- package/src/shared/lib/code/editor/{find.test.ts → find-scan.test.ts} +19 -73
- package/src/shared/lib/code/editor/find-scan.ts +177 -0
- package/src/shared/lib/code/editor/keymap.ts +3 -3
- package/src/shared/lib/code/editor/lines.test.ts +7 -55
- package/src/shared/lib/code/editor/lines.ts +8 -48
- package/src/shared/lib/code/editor/marks.test.ts +4 -5
- package/src/shared/lib/code/editor/marks.ts +13 -25
- package/src/shared/lib/code/editor/theme-loader.ts +97 -0
- package/src/shared/lib/code/editor/themes.generated.ts +156 -0
- package/src/shared/lib/code/editor/themes.test.ts +165 -0
- package/src/shared/lib/code/editor/themes.ts +165 -0
- package/src/shared/lib/code/highlight-memo.test.ts +103 -0
- package/src/shared/lib/code/highlight-memo.ts +149 -0
- package/src/shared/lib/code/highlighter-warmup.ts +112 -0
- package/src/shared/lib/code/highlighter.ts +3 -1
- package/src/shared/lib/code/idle.ts +37 -0
- package/src/shared/lib/code/outline/rows.test.ts +96 -0
- package/src/shared/lib/code/outline/rows.ts +94 -0
- package/src/shared/lib/code/syntax-highlight.test.ts +1 -79
- package/src/shared/lib/code/syntax-highlight.ts +2 -3
- package/src/shared/lib/fs/file-kind-classify.test.ts +25 -11
- package/src/shared/lib/fs/file-kind.ts +47 -13
- package/src/shared/lib/fs/file-tree.test.ts +183 -0
- package/src/shared/lib/fs/file-tree.ts +159 -0
- package/src/shared/lib/fs/file-url.test.ts +52 -0
- package/src/shared/lib/fs/file-url.ts +37 -0
- package/src/shared/lib/fs/patch-sides.test.ts +114 -0
- package/src/shared/lib/fs/patch-sides.ts +139 -0
- package/src/shared/lib/fs/paths.test.ts +24 -1
- package/src/shared/lib/fs/paths.ts +17 -0
- package/src/shared/lib/fs/worktree/settings-keys.ts +18 -0
- package/src/shared/lib/omp/session/prewarm.ts +7 -0
- package/src/shared/lib/realtime/protocol.ts +16 -0
- package/src/shared/lib/ui/toolbar-overflow.test.ts +83 -0
- package/src/shared/lib/ui/toolbar-overflow.ts +86 -0
- package/src/shared/lib/updates/shell-arch.ts +54 -0
- package/src/shared/lib/updates/shell-ua.ts +24 -4
- package/src/shared/lib/workspace/active-project.ts +23 -0
- package/src/shared/lib/workspace/editor-view-mode.test.ts +70 -0
- package/src/shared/lib/workspace/editor-view-mode.ts +48 -0
- package/src/shared/lib/workspace/panel-widths.test.ts +30 -0
- package/src/shared/lib/workspace/panel-widths.ts +25 -5
- package/src/shared/lib/workspace/session-state/cache.ts +159 -0
- package/src/shared/lib/workspace/session-state/harness.test-util.ts +55 -0
- package/src/shared/lib/workspace/session-state/merge.ts +100 -0
- package/src/shared/lib/workspace/session-state/store-load.test.ts +206 -0
- package/src/shared/lib/workspace/session-state/store.test.ts +17 -139
- package/src/shared/lib/workspace/session-state/store.ts +92 -130
- package/src/shared/lib/workspace/workspace-modules.test.ts +49 -1
- package/src/shared/lib/workspace/workspace-panel-widths.ts +124 -0
- package/src/shared/lib/workspace/worktree/branch-name.test.ts +95 -0
- package/src/shared/lib/workspace/worktree/branch-name.ts +115 -0
- package/src/shared/lib/workspace/worktree/types.ts +104 -0
- package/src/shared/types/fs.ts +10 -0
- package/src/shared/types/realtime.ts +29 -0
- package/src/shared/types/settings/state.ts +10 -0
- package/src/shared/types/workspace.ts +14 -0
- package/src/test-support/file-explorer-disk.test-util.ts +76 -0
- package/src/test-support/file-explorer.test-util.ts +71 -40
- package/src/test-support/loopback.ts +183 -0
- package/dist/client/static/js/index-nzyjf92m.css +0 -1
- package/src/client/components/common/code-editor/handler.test.ts +0 -274
- package/src/client/components/common/code-editor/handler.ts +0 -175
- package/src/client/components/common/code-editor/index.tsx +0 -314
- package/src/client/components/common/code-surface/measure.ts +0 -148
- package/src/client/components/workspace/diff-panel/SplitView.tsx +0 -209
- package/src/client/components/workspace/diff-panel/UnifiedView.tsx +0 -150
- package/src/client/hooks/editor/scroll-container.test.ts +0 -102
- package/src/client/hooks/editor/scroll-container.ts +0 -26
- package/src/client/hooks/editor/use-code-window.ts +0 -100
- package/src/client/hooks/editor/use-editor-history.test.ts +0 -216
- package/src/client/hooks/editor/use-editor-history.ts +0 -134
- package/src/client/hooks/editor/use-occurrences.test.ts +0 -236
- package/src/client/hooks/editor/use-occurrences.ts +0 -98
- package/src/shared/lib/code/editor/commands.test.ts +0 -155
- package/src/shared/lib/code/editor/commands.ts +0 -260
- package/src/shared/lib/code/editor/edits.test.ts +0 -100
- package/src/shared/lib/code/editor/edits.ts +0 -122
- package/src/shared/lib/code/editor/history.test.ts +0 -118
- package/src/shared/lib/code/editor/history.ts +0 -101
- package/src/shared/lib/code/editor/layers.test.ts +0 -77
- package/src/shared/lib/code/editor/layers.ts +0 -67
- package/src/shared/lib/code/editor/line-edits.test.ts +0 -131
- package/src/shared/lib/code/editor/line-edits.ts +0 -159
- package/src/shared/lib/code/editor/occurrences.test.ts +0 -117
- package/src/shared/lib/code/editor/occurrences.ts +0 -111
- package/src/shared/lib/code/editor/reset-css.test.ts +0 -35
- package/src/shared/lib/code/editor/reset-css.ts +0 -32
- package/src/shared/lib/code/editor/text-commands.test.ts +0 -178
- package/src/shared/lib/code/editor/text-commands.ts +0 -154
- package/src/shared/lib/code/lazy-window.test.ts +0 -161
- package/src/shared/lib/code/lazy-window.ts +0 -156
- package/src/shared/lib/code/windowed-highlight.ts +0 -114
- package/src/shared/lib/fs/diff-parser.test.ts +0 -116
- package/src/shared/lib/fs/diff-parser.ts +0 -213
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* One worktree row's second line, and the words the delete control uses.
|
|
8
|
+
*
|
|
9
|
+
* Split from the component because these are the sentences a reader decides on:
|
|
10
|
+
* "which branch is held here" (and therefore cannot be checked out anywhere
|
|
11
|
+
* else), "how many chats start here", and "is it safe to delete". A markup test
|
|
12
|
+
* would prove the component renders *something*; these functions are what it has
|
|
13
|
+
* to say.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { WorktreeInfo } from '@/shared/lib/workspace/worktree/types';
|
|
17
|
+
|
|
18
|
+
/** What a row says under its branch. */
|
|
19
|
+
export function worktreeRowDetail(worktree: WorktreeInfo): string {
|
|
20
|
+
if (worktree.missing) return 'folder is missing — its chats still open';
|
|
21
|
+
const branch = worktree.detached
|
|
22
|
+
? `detached @ ${worktree.head.slice(0, 7) || 'unknown'}`
|
|
23
|
+
: `${worktree.branch} checked out here`;
|
|
24
|
+
const sessions = worktree.sessions === 1 ? '1 chat' : `${worktree.sessions} chats`;
|
|
25
|
+
const changes = worktree.dirty === 0 ? 'clean' : `${worktree.dirty} changed`;
|
|
26
|
+
return `${branch} · ${sessions} · ${changes}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Whether the branch may be deleted along with the worktree.
|
|
31
|
+
*
|
|
32
|
+
* `git branch -d` refuses a branch with commits of its own, so the checkbox is
|
|
33
|
+
* offered only when there is nothing to lose — a control that can only be
|
|
34
|
+
* refused is worse than one that is disabled with the reason beside it.
|
|
35
|
+
*/
|
|
36
|
+
export function branchDeletable(worktree: WorktreeInfo): boolean {
|
|
37
|
+
return !worktree.detached && Boolean(worktree.branch) && worktree.aheadOfStart === 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Why the remove control is refused, or null when it may run. */
|
|
41
|
+
export function removalRefusal(worktree: WorktreeInfo): string | null {
|
|
42
|
+
if (worktree.isPrimary) return 'The main checkout cannot be removed.';
|
|
43
|
+
if (worktree.dirty > 0) {
|
|
44
|
+
return `${worktree.dirty} uncommitted file${worktree.dirty === 1 ? '' : 's'} here would be lost.`;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import type { SetStateAction } from 'preact/compat';
|
|
2
2
|
import { Fragment } from 'preact';
|
|
3
|
-
import { useEffect, useMemo, useRef
|
|
4
|
-
import {
|
|
5
|
-
import type { Attachment, ChatMessageData } from '@/shared/types';
|
|
6
|
-
import { useOnClickOutside } from '@/client/hooks/ui/on-click-outside';
|
|
3
|
+
import { useEffect, useMemo, useRef } from 'preact/hooks';
|
|
4
|
+
import type { Attachment, ChatMessageData, WorkspaceFolderData } from '@/shared/types';
|
|
7
5
|
import { ChatInput } from '@/client/components/workspace/chat-timeline/chat-input/index';
|
|
8
6
|
import { ChatMessageItem } from '@/client/components/workspace/chat-timeline/MessageItem';
|
|
9
7
|
import { RunFooter } from '@/client/components/workspace/chat-timeline/RunFooter';
|
|
@@ -16,6 +14,9 @@ import {
|
|
|
16
14
|
type WaitingCandidate,
|
|
17
15
|
} from '@/client/components/workspace/chat-timeline/chat-input/WaitingDependencyPicker';
|
|
18
16
|
import { Hourglass } from 'lucide-preact';
|
|
17
|
+
import { WorkspacePicker } from '@/client/components/workspace/chat-timeline/target-picker/WorkspacePicker';
|
|
18
|
+
import { WorktreePicker } from '@/client/components/workspace/chat-timeline/target-picker/WorktreePicker';
|
|
19
|
+
import type { SpawnTarget } from '@/client/hooks/chat/timeline/spawn-target';
|
|
19
20
|
|
|
20
21
|
interface EmptyWorkspacePromptProps {
|
|
21
22
|
className?: string;
|
|
@@ -65,6 +66,15 @@ interface EmptyWorkspacePromptProps {
|
|
|
65
66
|
streamStatusMap?: Record<string, string | undefined>;
|
|
66
67
|
/** A name for a streaming session the sidebar has not scanned yet. */
|
|
67
68
|
titleForSession?: (id: string) => string | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* Where this chat will run. The picker draws it and the send path reads it
|
|
71
|
+
* through its own ref, so a worktree choice survives the remount a stretch or
|
|
72
|
+
* a reload performs.
|
|
73
|
+
*/
|
|
74
|
+
spawnTarget?: SpawnTarget | null;
|
|
75
|
+
onSpawnTargetChange?: (next: SpawnTarget | null) => void;
|
|
76
|
+
/** Reports a worktree the composer created or failed to create. */
|
|
77
|
+
onWorktreeReport?: (message: string) => void;
|
|
68
78
|
/** The "wait for session" pick, persisted per chat. */
|
|
69
79
|
waitingFor?: WaitingCandidate | null;
|
|
70
80
|
onWaitingForChange?: (candidate: WaitingCandidate | null) => void;
|
|
@@ -108,20 +118,16 @@ export function EmptyWorkspacePrompt({
|
|
|
108
118
|
onNewChat,
|
|
109
119
|
streamStatusMap = {},
|
|
110
120
|
titleForSession,
|
|
121
|
+
spawnTarget = null,
|
|
122
|
+
onSpawnTargetChange,
|
|
123
|
+
onWorktreeReport,
|
|
111
124
|
waitingFor = null,
|
|
112
125
|
onWaitingForChange,
|
|
113
126
|
heldWait = null,
|
|
114
127
|
onCancelWait,
|
|
115
128
|
}: EmptyWorkspacePromptProps) {
|
|
116
|
-
const [showWorkspace, setShowWorkspace] = useState(false);
|
|
117
|
-
const workspaceRef = useRef<HTMLDivElement>(null);
|
|
118
|
-
useOnClickOutside(workspaceRef, () => setShowWorkspace(false));
|
|
119
129
|
const timelineRef = useRef<HTMLDivElement>(null);
|
|
120
130
|
|
|
121
|
-
const selectedFolder = useMemo(() => {
|
|
122
|
-
return folders?.find(f => f.id === selectedFolderId);
|
|
123
|
-
}, [folders, selectedFolderId]);
|
|
124
|
-
|
|
125
131
|
// Sessions the wait may name: the ones with a run in flight, from the same
|
|
126
132
|
// server-tracked map the sidebar's spinner is drawn from.
|
|
127
133
|
const candidates = useMemo(
|
|
@@ -159,51 +165,26 @@ export function EmptyWorkspacePrompt({
|
|
|
159
165
|
a target workspace, so the composer is the only focus. */}
|
|
160
166
|
{messages.length === 0 && (
|
|
161
167
|
<div className="flex items-center gap-1 flex-wrap">
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
{folders.length === 0 ? (
|
|
182
|
-
<div className="px-3 py-2 text-ink/40 italic">No workspaces available</div>
|
|
183
|
-
) : (
|
|
184
|
-
folders.map((f: any) => (
|
|
185
|
-
<button
|
|
186
|
-
key={f.id}
|
|
187
|
-
type="button"
|
|
188
|
-
onClick={() => { setSelectedFolderId(f.id); setShowWorkspace(false); }}
|
|
189
|
-
className={`w-full text-left px-3 py-1.5 flex items-center justify-between transition-colors cursor-pointer ${
|
|
190
|
-
selectedFolderId === f.id
|
|
191
|
-
? 'bg-ink/10 text-ink font-semibold'
|
|
192
|
-
: 'text-ink/80 hover:bg-ink/5 hover:text-ink'
|
|
193
|
-
}`}
|
|
194
|
-
>
|
|
195
|
-
<div className="flex items-center space-x-2 truncate pr-2">
|
|
196
|
-
<Folder size={12} className={selectedFolderId === f.id ? "text-ink flex-shrink-0" : "text-ink/50 flex-shrink-0"} />
|
|
197
|
-
<span className="truncate font-medium">{f.name}</span>
|
|
198
|
-
</div>
|
|
199
|
-
{selectedFolderId === f.id && <Check size={12} className="text-ink flex-shrink-0" />}
|
|
200
|
-
</button>
|
|
201
|
-
))
|
|
202
|
-
)}
|
|
203
|
-
</div>
|
|
204
|
-
</div>
|
|
168
|
+
{/* Two controls, because they answer two questions: the workspace
|
|
169
|
+
(which project) and the worktree (which checkout of it). The
|
|
170
|
+
worktree control draws itself only for a git repository, and it
|
|
171
|
+
reads its list on mount because its own trigger names the branch
|
|
172
|
+
the project root holds. */}
|
|
173
|
+
<WorkspacePicker
|
|
174
|
+
folders={folders as WorkspaceFolderData[]}
|
|
175
|
+
selectedFolderId={selectedFolderId}
|
|
176
|
+
setSelectedFolderId={setSelectedFolderId}
|
|
177
|
+
onClearWorktree={() => onSpawnTargetChange?.(null)}
|
|
178
|
+
/>
|
|
179
|
+
{onSpawnTargetChange && (
|
|
180
|
+
<WorktreePicker
|
|
181
|
+
folders={folders as WorkspaceFolderData[]}
|
|
182
|
+
selectedFolderId={selectedFolderId}
|
|
183
|
+
target={spawnTarget}
|
|
184
|
+
setTarget={onSpawnTargetChange}
|
|
185
|
+
onReport={onWorktreeReport}
|
|
186
|
+
/>
|
|
205
187
|
)}
|
|
206
|
-
</div>
|
|
207
188
|
{/* The wait picker sits BESIDE the workspace picker, because both
|
|
208
189
|
answer the same question about the chat that has not started:
|
|
209
190
|
where it runs, and what it waits for. It only appears while there
|
|
@@ -20,6 +20,7 @@ import type { ComposerModes } from '@/client/components/workspace/chat-timeline/
|
|
|
20
20
|
import type { WaitingCandidate } from '@/client/components/workspace/chat-timeline/chat-input/WaitingDependencyPicker';
|
|
21
21
|
import type { DeferredModelStore } from '@/client/hooks/chat/timeline/deferred-model';
|
|
22
22
|
import { EmptyWorkspacePrompt } from '@/client/components/workspace/chat-timeline/EmptyWorkspacePrompt';
|
|
23
|
+
import type { SpawnTarget } from '@/client/hooks/chat/timeline/spawn-target';
|
|
23
24
|
|
|
24
25
|
export interface PendingChatViewProps {
|
|
25
26
|
className: string;
|
|
@@ -59,6 +60,10 @@ export interface PendingChatViewProps {
|
|
|
59
60
|
onWaitingForChange: (candidate: WaitingCandidate | null) => void;
|
|
60
61
|
heldWait: { title: string | null } | null;
|
|
61
62
|
onCancelWait: () => void;
|
|
63
|
+
/** Where the chat will run: the folder, or one of its worktrees. */
|
|
64
|
+
spawnTarget?: SpawnTarget | null;
|
|
65
|
+
onSpawnTargetChange?: (next: SpawnTarget | null) => void;
|
|
66
|
+
onWorktreeReport?: (message: string) => void;
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
export function PendingChatView(props: PendingChatViewProps) {
|
|
@@ -101,6 +106,9 @@ export function PendingChatView(props: PendingChatViewProps) {
|
|
|
101
106
|
onWaitingForChange={props.onWaitingForChange}
|
|
102
107
|
heldWait={props.heldWait}
|
|
103
108
|
onCancelWait={props.onCancelWait}
|
|
109
|
+
spawnTarget={props.spawnTarget}
|
|
110
|
+
onSpawnTargetChange={props.onSpawnTargetChange}
|
|
111
|
+
onWorktreeReport={props.onWorktreeReport}
|
|
104
112
|
/>
|
|
105
113
|
);
|
|
106
114
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { useMemo, useRef, useState } from 'preact/hooks';
|
|
2
2
|
import { AlertCircle, Bell, Bot, CheckCircle2, ChevronDown, Clock, Info, Layers, Target } from 'lucide-preact';
|
|
3
3
|
import { isCodeLike } from '@/shared/lib/code/language';
|
|
4
|
-
import {
|
|
5
|
-
import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
|
|
4
|
+
import { useDeferredHighlight } from '@/client/hooks/code/deferred-highlight';
|
|
6
5
|
import { useIsTruncated } from '@/client/hooks/ui/text-overflow';
|
|
7
6
|
import { parseTaskNotice } from '@/shared/lib/chat/task-result-parser';
|
|
8
7
|
import { isReminderTag, stripNoticeTags, unwrapXmlEnvelope } from '@/shared/lib/chat/xml-envelope';
|
|
@@ -53,7 +52,6 @@ interface SystemNoticeProps {
|
|
|
53
52
|
* injection renders open and has no collapse control (see `forcedOpen`). */
|
|
54
53
|
export function SystemNotice({ notice, source, isMobile = false }: SystemNoticeProps) {
|
|
55
54
|
const [userOpen, setUserOpen] = useState(false);
|
|
56
|
-
useSyntaxReady();
|
|
57
55
|
|
|
58
56
|
// 1. Check if notice contains a structured <task-result> block
|
|
59
57
|
const taskNotice = useMemo(() => parseTaskNotice(notice), [notice]);
|
|
@@ -115,6 +113,13 @@ export function SystemNotice({ notice, source, isMobile = false }: SystemNoticeP
|
|
|
115
113
|
};
|
|
116
114
|
}, [notice]);
|
|
117
115
|
|
|
116
|
+
// Deferred and gated: the code branch only renders while the card is open, so
|
|
117
|
+
// a collapsed notice computes nothing and an opened one paints its text first.
|
|
118
|
+
const codeHtml = useDeferredHighlight(
|
|
119
|
+
genericInfo.isCode ? genericInfo.rest || genericInfo.clean : '',
|
|
120
|
+
'javascript',
|
|
121
|
+
);
|
|
122
|
+
|
|
118
123
|
const rawTitle = goalNotice
|
|
119
124
|
? goalNoticeTitle(goalNotice)
|
|
120
125
|
: taskNotice?.intro || reminderInfo?.title || genericInfo.firstLine || notice;
|
|
@@ -284,7 +289,7 @@ export function SystemNotice({ notice, source, isMobile = false }: SystemNoticeP
|
|
|
284
289
|
<pre
|
|
285
290
|
className="max-h-56 overflow-auto font-mono text-[11px] leading-relaxed whitespace-pre-wrap break-words text-ink/75 scrollbar-overlay-container"
|
|
286
291
|
dangerouslySetInnerHTML={{
|
|
287
|
-
__html:
|
|
292
|
+
__html: codeHtml,
|
|
288
293
|
}}
|
|
289
294
|
/>
|
|
290
295
|
) : (
|
|
@@ -4,15 +4,13 @@ import type { ParsedTaskNotice } from '@/shared/lib/chat/task-result-parser';
|
|
|
4
4
|
import { MarkdownRenderer } from '@/client/components/common/MarkdownRenderer';
|
|
5
5
|
import { CopyButton } from '@/client/components/common/CopyButton';
|
|
6
6
|
import { isCodeLike } from '@/shared/lib/code/language';
|
|
7
|
-
import {
|
|
8
|
-
import { useSyntaxReady } from '@/client/hooks/ui/syntax-ready';
|
|
7
|
+
import { useDeferredHighlight } from '@/client/hooks/code/deferred-highlight';
|
|
9
8
|
|
|
10
9
|
interface TaskResultContentProps {
|
|
11
10
|
task: ParsedTaskNotice;
|
|
12
11
|
}
|
|
13
12
|
|
|
14
13
|
export function TaskResultContent({ task }: TaskResultContentProps) {
|
|
15
|
-
useSyntaxReady();
|
|
16
14
|
const structured = task.structuredOutput;
|
|
17
15
|
const isRawCode = !task.formattedJson && isCodeLike(task.rawOutput);
|
|
18
16
|
|
|
@@ -20,6 +18,13 @@ export function TaskResultContent({ task }: TaskResultContentProps) {
|
|
|
20
18
|
structured || !isRawCode ? 'formatted' : 'raw'
|
|
21
19
|
);
|
|
22
20
|
|
|
21
|
+
// Gated on the tab that draws it: the raw view is one of two, and tokenizing
|
|
22
|
+
// it while the formatted view is on screen is work nobody sees.
|
|
23
|
+
const rawHtml = useDeferredHighlight(
|
|
24
|
+
activeTab === 'raw' ? task.formattedJson || task.rawOutput : '',
|
|
25
|
+
task.formattedJson ? 'json' : isRawCode ? 'javascript' : 'markdown',
|
|
26
|
+
);
|
|
27
|
+
|
|
23
28
|
return (
|
|
24
29
|
<div className="space-y-3">
|
|
25
30
|
{/* Top action toolbar / tab toggles */}
|
|
@@ -151,10 +156,7 @@ export function TaskResultContent({ task }: TaskResultContentProps) {
|
|
|
151
156
|
<pre
|
|
152
157
|
className="max-h-64 overflow-auto rounded-lg border border-ink/8 bg-paper p-3 font-mono text-[11px] leading-relaxed whitespace-pre-wrap break-words text-ink/80 scrollbar-overlay-container"
|
|
153
158
|
dangerouslySetInnerHTML={{
|
|
154
|
-
__html:
|
|
155
|
-
task.formattedJson || task.rawOutput,
|
|
156
|
-
task.formattedJson ? 'json' : isRawCode ? 'javascript' : 'markdown'
|
|
157
|
-
),
|
|
159
|
+
__html: rawHtml,
|
|
158
160
|
}}
|
|
159
161
|
/>
|
|
160
162
|
)}
|
|
@@ -88,6 +88,8 @@ export function ChatTimeline({ className = '', appSettings = {}, onSessionTitle,
|
|
|
88
88
|
extensionDialogs,
|
|
89
89
|
resolveExtensionDialog,
|
|
90
90
|
respondToExtensionUi,
|
|
91
|
+
spawnTarget,
|
|
92
|
+
setSpawnTarget,
|
|
91
93
|
} = useChatTimeline({ folders, streamStatusMap, appSettings, reportActionError: pushToast });
|
|
92
94
|
|
|
93
95
|
// Ask dialogs render on their own tool card; anything else omp is blocked on
|
|
@@ -215,6 +217,9 @@ export function ChatTimeline({ className = '', appSettings = {}, onSessionTitle,
|
|
|
215
217
|
onWaitingForChange={waitingDependency.set}
|
|
216
218
|
heldWait={heldWait ? { title: heldWait.dependsOnTitle } : null}
|
|
217
219
|
onCancelWait={cancelHeldWait}
|
|
220
|
+
spawnTarget={spawnTarget}
|
|
221
|
+
onSpawnTargetChange={setSpawnTarget}
|
|
222
|
+
onWorktreeReport={(message) => pushToast(message, 'success', { duration: 6000 })}
|
|
218
223
|
/>
|
|
219
224
|
);
|
|
220
225
|
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The composer's WORKSPACE picker, rendered.
|
|
8
|
+
*
|
|
9
|
+
* The cases a reader would otherwise find only in a browser:
|
|
10
|
+
*
|
|
11
|
+
* - the trigger names the selected folder, and says what it is waiting for
|
|
12
|
+
* when nothing is selected;
|
|
13
|
+
* - the menu lists folders and NOTHING about worktrees — the split is the
|
|
14
|
+
* point of this control, and a worktree row here is the regression;
|
|
15
|
+
* - choosing another folder clears the worktree pick, because a checkout of
|
|
16
|
+
* the previous project names a path the new one cannot resolve.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'bun:test';
|
|
20
|
+
import { Window } from 'happy-dom';
|
|
21
|
+
import { h, render } from 'preact';
|
|
22
|
+
import { act } from 'preact/test-utils';
|
|
23
|
+
import { installDomGlobals, restoreDomGlobals } from '@/test-support/pristine-globals';
|
|
24
|
+
import { WorkspacePicker, workspaceLabel } from '@/client/components/workspace/chat-timeline/target-picker/WorkspacePicker';
|
|
25
|
+
import type { WorkspaceFolderData } from '@/shared/types';
|
|
26
|
+
|
|
27
|
+
const folder = (id: number, name: string): WorkspaceFolderData => ({
|
|
28
|
+
id,
|
|
29
|
+
name,
|
|
30
|
+
project_path: `/repo/${name}`,
|
|
31
|
+
isExpanded: true,
|
|
32
|
+
hasMore: false,
|
|
33
|
+
totalSessions: 0,
|
|
34
|
+
sessions: [],
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const mounted: HTMLElement[] = [];
|
|
38
|
+
|
|
39
|
+
beforeAll(() => {
|
|
40
|
+
installDomGlobals(new Window({ url: 'http://localhost' }));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
for (const host of mounted.splice(0)) {
|
|
45
|
+
render(null, host);
|
|
46
|
+
host.remove();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterAll(() => {
|
|
51
|
+
restoreDomGlobals();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
async function mount(node: h.JSX.Element): Promise<HTMLElement> {
|
|
55
|
+
const host = document.createElement('div');
|
|
56
|
+
document.body.appendChild(host);
|
|
57
|
+
await act(async () => {
|
|
58
|
+
render(node, host);
|
|
59
|
+
});
|
|
60
|
+
mounted.push(host);
|
|
61
|
+
return host;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function textOf(host: HTMLElement): string {
|
|
65
|
+
return (host.textContent ?? '').replace(/\s+/g, ' ').trim();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe('workspaceLabel', () => {
|
|
69
|
+
test('the selected folder', () => {
|
|
70
|
+
expect(workspaceLabel([folder(1, 'kabmo')], 1)).toBe('kabmo');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('what it is waiting for, with nothing selected', () => {
|
|
74
|
+
expect(workspaceLabel([], null)).toBe('Select Workspace Context');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('WorkspacePicker', () => {
|
|
79
|
+
test('the menu lists the workspaces and nothing about worktrees', async () => {
|
|
80
|
+
const host = await mount(
|
|
81
|
+
h(WorkspacePicker, {
|
|
82
|
+
folders: [folder(1, 'kabmo'), folder(2, 'notes')],
|
|
83
|
+
selectedFolderId: 1,
|
|
84
|
+
setSelectedFolderId: () => {},
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
await act(async () => {
|
|
89
|
+
(host.querySelector('button') as HTMLButtonElement).click();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const text = textOf(host);
|
|
93
|
+
expect(text).toContain('Target Workspace');
|
|
94
|
+
expect(text).toContain('kabmo');
|
|
95
|
+
expect(text).toContain('notes');
|
|
96
|
+
// The split: a worktree row here would be the merged menu coming back.
|
|
97
|
+
expect(text).not.toContain('Quick worktree');
|
|
98
|
+
expect(text).not.toContain('New worktree');
|
|
99
|
+
expect(text).not.toContain('Worktrees');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('choosing a folder clears the worktree pick', async () => {
|
|
103
|
+
// A record rather than two locals: a value assigned inside a callback is
|
|
104
|
+
// still narrowed to its initializer by control-flow analysis, so `picked`
|
|
105
|
+
// would be `null` at the assertion.
|
|
106
|
+
const seen: { picked: number | null; cleared: number } = { picked: null, cleared: 0 };
|
|
107
|
+
const host = await mount(
|
|
108
|
+
h(WorkspacePicker, {
|
|
109
|
+
folders: [folder(1, 'kabmo'), folder(2, 'notes')],
|
|
110
|
+
selectedFolderId: 1,
|
|
111
|
+
setSelectedFolderId: (id) => {
|
|
112
|
+
seen.picked = id;
|
|
113
|
+
},
|
|
114
|
+
onClearWorktree: () => {
|
|
115
|
+
seen.cleared += 1;
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
await act(async () => {
|
|
121
|
+
(host.querySelector('button') as HTMLButtonElement).click();
|
|
122
|
+
});
|
|
123
|
+
const rows = [...host.querySelectorAll('[role="menuitemradio"]')] as HTMLButtonElement[];
|
|
124
|
+
expect(rows.length).toBe(2);
|
|
125
|
+
await act(async () => {
|
|
126
|
+
rows[1]?.click();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
expect(seen.picked).toBe(2);
|
|
130
|
+
expect(seen.cleared).toBe(1);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* WHERE the chat runs: the workspace folder.
|
|
8
|
+
*
|
|
9
|
+
* One control of the composer's draft-target pair, and the pair exists because
|
|
10
|
+
* the two questions are different: the folder is the project, and the worktree
|
|
11
|
+
* is the checkout inside it (`WorktreePicker`). They were merged into one menu
|
|
12
|
+
* once, and the result read as a workspace picker with worktree rows buried in
|
|
13
|
+
* it — a user looking for another project scrolled past branch names, and a
|
|
14
|
+
* user looking for a worktree could not tell the control was the place to look.
|
|
15
|
+
* OpenChamber draws them as two controls for the same reason.
|
|
16
|
+
*
|
|
17
|
+
* Picking a folder CLEARS the worktree pick: a checkout of the previous project
|
|
18
|
+
* names a path that does not exist under the new one.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { useRef, useState } from 'preact/hooks';
|
|
22
|
+
import { Check, ChevronDown, Folder } from 'lucide-preact';
|
|
23
|
+
import { useOnClickOutside } from '@/client/hooks/ui/on-click-outside';
|
|
24
|
+
import type { WorkspaceFolderData } from '@/shared/types';
|
|
25
|
+
|
|
26
|
+
export interface WorkspacePickerProps {
|
|
27
|
+
folders: WorkspaceFolderData[];
|
|
28
|
+
selectedFolderId: number | null;
|
|
29
|
+
setSelectedFolderId: (id: number | null) => void;
|
|
30
|
+
/** Called with `null` when the user moves to another project. */
|
|
31
|
+
onClearWorktree?: () => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The trigger's words, exported so a test asserts them without the markup. */
|
|
35
|
+
export function workspaceLabel(folders: WorkspaceFolderData[], selectedFolderId: number | null): string {
|
|
36
|
+
const folder = folders.find((candidate) => String(candidate.id) === String(selectedFolderId));
|
|
37
|
+
return folder?.name ?? 'Select Workspace Context';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function WorkspacePicker({
|
|
41
|
+
folders,
|
|
42
|
+
selectedFolderId,
|
|
43
|
+
setSelectedFolderId,
|
|
44
|
+
onClearWorktree,
|
|
45
|
+
}: WorkspacePickerProps) {
|
|
46
|
+
const [open, setOpen] = useState(false);
|
|
47
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
48
|
+
useOnClickOutside(containerRef, () => setOpen(false));
|
|
49
|
+
|
|
50
|
+
const pick = (folder: WorkspaceFolderData) => {
|
|
51
|
+
setSelectedFolderId(folder.id);
|
|
52
|
+
// A worktree belongs to the project it was made from; keeping the pick
|
|
53
|
+
// across a project change would spawn the chat in a path this project
|
|
54
|
+
// cannot resolve.
|
|
55
|
+
onClearWorktree?.();
|
|
56
|
+
setOpen(false);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const label = workspaceLabel(folders, selectedFolderId);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className="relative shrink-0" ref={containerRef}>
|
|
63
|
+
<button
|
|
64
|
+
type="button"
|
|
65
|
+
// Two triggers sit side by side, so each needs a name that says which
|
|
66
|
+
// question it answers: "kabmo" and "main" alone are both ambiguous.
|
|
67
|
+
aria-label={`Workspace: ${label}`}
|
|
68
|
+
aria-haspopup="menu"
|
|
69
|
+
aria-expanded={open}
|
|
70
|
+
onClick={() => setOpen((value) => !value)}
|
|
71
|
+
className="group flex items-center space-x-1.5 px-1.5 py-1 rounded-md text-xs font-medium text-ink/75 hover:text-ink hover:bg-ink/5 focus-visible:bg-ink/5 transition-all outline-none cursor-pointer select-none"
|
|
72
|
+
>
|
|
73
|
+
<Folder size={13} className="text-ink/50 group-hover:text-ink/80 transition-colors flex-shrink-0" />
|
|
74
|
+
<span className="max-w-[260px] truncate tracking-tight">{label}</span>
|
|
75
|
+
<ChevronDown
|
|
76
|
+
size={13}
|
|
77
|
+
className={`text-ink/40 group-hover:text-ink/70 flex-shrink-0 transition-transform duration-150 ${open ? 'rotate-180 text-ink/70' : ''}`}
|
|
78
|
+
/>
|
|
79
|
+
</button>
|
|
80
|
+
|
|
81
|
+
{open && (
|
|
82
|
+
<div
|
|
83
|
+
role="menu"
|
|
84
|
+
className="absolute bottom-full left-0 mb-1.5 w-64 bg-paper border border-ink/10 rounded-lg shadow-lg z-50 flex flex-col overflow-hidden text-xs py-1"
|
|
85
|
+
>
|
|
86
|
+
<div className="px-3 py-1.5 text-[10px] uppercase tracking-wider text-ink/40 font-semibold border-b border-ink/5">
|
|
87
|
+
Target Workspace
|
|
88
|
+
</div>
|
|
89
|
+
<div className="max-h-56 scrollbar-overlay-container py-1">
|
|
90
|
+
{folders.length === 0 ? (
|
|
91
|
+
<div className="px-3 py-2 text-ink/40 italic">No workspaces available</div>
|
|
92
|
+
) : (
|
|
93
|
+
folders.map((folder) => {
|
|
94
|
+
const active = String(selectedFolderId) === String(folder.id);
|
|
95
|
+
return (
|
|
96
|
+
<button
|
|
97
|
+
key={folder.id}
|
|
98
|
+
type="button"
|
|
99
|
+
role="menuitemradio"
|
|
100
|
+
aria-checked={active}
|
|
101
|
+
onClick={() => pick(folder)}
|
|
102
|
+
className={`w-full text-left px-3 py-1.5 flex items-center justify-between transition-colors cursor-pointer ${
|
|
103
|
+
active ? 'bg-ink/10 text-ink font-semibold' : 'text-ink/80 hover:bg-ink/5 hover:text-ink'
|
|
104
|
+
}`}
|
|
105
|
+
>
|
|
106
|
+
<span className="flex items-center space-x-2 truncate pr-2">
|
|
107
|
+
<Folder size={12} className={active ? 'text-ink flex-shrink-0' : 'text-ink/50 flex-shrink-0'} />
|
|
108
|
+
<span className="truncate font-medium">{folder.name}</span>
|
|
109
|
+
</span>
|
|
110
|
+
{active && <Check size={12} className="text-ink flex-shrink-0" />}
|
|
111
|
+
</button>
|
|
112
|
+
);
|
|
113
|
+
})
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|