@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,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The composer's WORKTREE picker, rendered.
|
|
8
|
+
*
|
|
9
|
+
* These are the cases a reader would otherwise meet in a browser and nowhere
|
|
10
|
+
* else:
|
|
11
|
+
*
|
|
12
|
+
* - a workspace that is not a git repository gets NO control (a permanently
|
|
13
|
+
* disabled one in every notes folder is noise, and a click that can only
|
|
14
|
+
* refuse is worse);
|
|
15
|
+
* - the trigger names the BRANCH, which is why the list is read on mount and
|
|
16
|
+
* not only when the menu opens;
|
|
17
|
+
* - the menu's groups are the three the plan names, with creating ABOVE the
|
|
18
|
+
* list so a long list cannot hide it;
|
|
19
|
+
* - the project root row clears the worktree pick.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'bun:test';
|
|
23
|
+
import { Window } from 'happy-dom';
|
|
24
|
+
import { h, render } from 'preact';
|
|
25
|
+
import { act } from 'preact/test-utils';
|
|
26
|
+
import { installDomGlobals, restoreDomGlobals } from '@/test-support/pristine-globals';
|
|
27
|
+
import {
|
|
28
|
+
WorktreePicker,
|
|
29
|
+
selectedWorktree,
|
|
30
|
+
worktreeLabel,
|
|
31
|
+
} from '@/client/components/workspace/chat-timeline/target-picker/WorktreePicker';
|
|
32
|
+
import type { WorktreeInfo } from '@/shared/lib/workspace/worktree/types';
|
|
33
|
+
import type { WorkspaceFolderData } from '@/shared/types';
|
|
34
|
+
|
|
35
|
+
const folder: WorkspaceFolderData = {
|
|
36
|
+
id: 1,
|
|
37
|
+
name: 'kabmo',
|
|
38
|
+
project_path: '/repo',
|
|
39
|
+
isExpanded: true,
|
|
40
|
+
hasMore: false,
|
|
41
|
+
totalSessions: 0,
|
|
42
|
+
sessions: [],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const row = (overrides: Partial<WorktreeInfo>): WorktreeInfo => ({
|
|
46
|
+
path: '/data/worktrees/x/main',
|
|
47
|
+
branch: 'main',
|
|
48
|
+
head: 'abc1234',
|
|
49
|
+
detached: false,
|
|
50
|
+
missing: false,
|
|
51
|
+
isPrimary: true,
|
|
52
|
+
dirty: 0,
|
|
53
|
+
aheadOfStart: 0,
|
|
54
|
+
startRef: '',
|
|
55
|
+
sessions: 0,
|
|
56
|
+
...overrides,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const primary = row({});
|
|
60
|
+
const made = row({
|
|
61
|
+
path: '/data/worktrees/x/cosmic-dolphin',
|
|
62
|
+
branch: 'wt/cosmic-dolphin',
|
|
63
|
+
isPrimary: false,
|
|
64
|
+
dirty: 2,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const nativeFetch = Bun.fetch;
|
|
68
|
+
const mounted: HTMLElement[] = [];
|
|
69
|
+
|
|
70
|
+
beforeAll(() => {
|
|
71
|
+
installDomGlobals(new Window({ url: 'http://localhost' }));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
afterEach(() => {
|
|
75
|
+
for (const host of mounted.splice(0)) {
|
|
76
|
+
render(null, host);
|
|
77
|
+
host.remove();
|
|
78
|
+
}
|
|
79
|
+
(globalThis as unknown as Record<string, unknown>).fetch = nativeFetch;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
afterAll(() => {
|
|
83
|
+
restoreDomGlobals();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
async function mount(node: h.JSX.Element): Promise<HTMLElement> {
|
|
87
|
+
const host = document.createElement('div');
|
|
88
|
+
document.body.appendChild(host);
|
|
89
|
+
await act(async () => {
|
|
90
|
+
render(node, host);
|
|
91
|
+
});
|
|
92
|
+
mounted.push(host);
|
|
93
|
+
return host;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Answer the list read without a server. `isRepo` is the whole point here. */
|
|
97
|
+
function stubList(worktrees: WorktreeInfo[], isRepo = true): string[] {
|
|
98
|
+
const calls: string[] = [];
|
|
99
|
+
(globalThis as unknown as Record<string, unknown>).fetch = async (input: RequestInfo | URL) => {
|
|
100
|
+
calls.push(String(input));
|
|
101
|
+
const payload = { worktrees, base: '/data/worktrees/x', isRepo };
|
|
102
|
+
return { ok: true, json: async () => payload } as unknown as Response;
|
|
103
|
+
};
|
|
104
|
+
return calls;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* A stub whose LIST grows once a create has answered — the state a real control
|
|
109
|
+
* is in the moment after it makes a worktree.
|
|
110
|
+
*/
|
|
111
|
+
function stubCreate(listBefore: WorktreeInfo[], created: WorktreeInfo): string[] {
|
|
112
|
+
const calls: string[] = [];
|
|
113
|
+
let created_ = false;
|
|
114
|
+
(globalThis as unknown as Record<string, unknown>).fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
115
|
+
calls.push(`${init?.method ?? 'GET'} ${String(input)}`);
|
|
116
|
+
if (init?.method === 'POST') {
|
|
117
|
+
created_ = true;
|
|
118
|
+
return { ok: true, json: async () => ({ worktree: created }) } as unknown as Response;
|
|
119
|
+
}
|
|
120
|
+
const worktrees = created_ ? [...listBefore, created] : listBefore;
|
|
121
|
+
return {
|
|
122
|
+
ok: true,
|
|
123
|
+
json: async () => ({ worktrees, base: '/data/worktrees/x', isRepo: true }),
|
|
124
|
+
} as unknown as Response;
|
|
125
|
+
};
|
|
126
|
+
return calls;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Drain the fetch promise chain; each empty `act` is one round. */
|
|
130
|
+
async function settle(): Promise<void> {
|
|
131
|
+
for (let round = 0; round < 10; round += 1) await act(async () => {});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function textOf(host: HTMLElement): string {
|
|
135
|
+
return (host.textContent ?? '').replace(/\s+/g, ' ').trim();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
describe('worktreeLabel', () => {
|
|
139
|
+
test('the branch the project root holds, not the word "root"', () => {
|
|
140
|
+
expect(worktreeLabel([primary], null)).toBe('main');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('the picked worktree’s branch', () => {
|
|
144
|
+
expect(worktreeLabel([primary, made], { folderId: 1, worktreePath: made.path })).toBe('wt/cosmic-dolphin');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('a pick the list has not caught up with still names something', () => {
|
|
148
|
+
// The dialog selects the new worktree the moment it is created, and the
|
|
149
|
+
// list that answers the next render may predate it.
|
|
150
|
+
expect(worktreeLabel([primary], { folderId: 1, worktreePath: '/data/worktrees/x/just-made' })).toBe('just-made');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('nothing read yet falls back to the words a reader can act on', () => {
|
|
154
|
+
expect(worktreeLabel([], null)).toBe('Project root');
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('selectedWorktree', () => {
|
|
159
|
+
test('the row the pick names, and null for the root', () => {
|
|
160
|
+
expect(selectedWorktree([primary, made], { folderId: 1, worktreePath: made.path })?.branch).toBe('wt/cosmic-dolphin');
|
|
161
|
+
expect(selectedWorktree([primary, made], null)).toBeNull();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('WorktreePicker', () => {
|
|
166
|
+
test('a workspace that is not a git repository draws no control', async () => {
|
|
167
|
+
const calls = stubList([], false);
|
|
168
|
+
const host = await mount(
|
|
169
|
+
h(WorktreePicker, {
|
|
170
|
+
folders: [folder],
|
|
171
|
+
selectedFolderId: 1,
|
|
172
|
+
target: null,
|
|
173
|
+
setTarget: () => {},
|
|
174
|
+
}),
|
|
175
|
+
);
|
|
176
|
+
await settle();
|
|
177
|
+
|
|
178
|
+
// The read happened — the answer is what suppresses the control, not a
|
|
179
|
+
// guess about the folder's shape.
|
|
180
|
+
expect(calls.length).toBeGreaterThan(0);
|
|
181
|
+
expect(host.querySelector('button')).toBeNull();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('the trigger names the root branch, read before the menu is opened', async () => {
|
|
185
|
+
stubList([primary, made]);
|
|
186
|
+
const host = await mount(
|
|
187
|
+
h(WorktreePicker, {
|
|
188
|
+
folders: [folder],
|
|
189
|
+
selectedFolderId: 1,
|
|
190
|
+
target: null,
|
|
191
|
+
setTarget: () => {},
|
|
192
|
+
}),
|
|
193
|
+
);
|
|
194
|
+
await settle();
|
|
195
|
+
|
|
196
|
+
const trigger = host.querySelector('button') as HTMLButtonElement | null;
|
|
197
|
+
expect(trigger?.textContent).toContain('main');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('the menu offers the project root, both create paths, and the list', async () => {
|
|
201
|
+
stubList([primary, made]);
|
|
202
|
+
const host = await mount(
|
|
203
|
+
h(WorktreePicker, {
|
|
204
|
+
folders: [folder],
|
|
205
|
+
selectedFolderId: 1,
|
|
206
|
+
target: null,
|
|
207
|
+
setTarget: () => {},
|
|
208
|
+
}),
|
|
209
|
+
);
|
|
210
|
+
await settle();
|
|
211
|
+
|
|
212
|
+
await act(async () => {
|
|
213
|
+
(host.querySelector('button') as HTMLButtonElement).click();
|
|
214
|
+
});
|
|
215
|
+
await settle();
|
|
216
|
+
|
|
217
|
+
const text = textOf(host);
|
|
218
|
+
expect(text).toContain('Project root');
|
|
219
|
+
expect(text).toContain('Quick worktree');
|
|
220
|
+
expect(text).toContain('New worktree…');
|
|
221
|
+
expect(text).toContain('Worktrees');
|
|
222
|
+
expect(text).toContain('wt/cosmic-dolphin');
|
|
223
|
+
// Creating sits above the LIST: a project with many worktrees must not push
|
|
224
|
+
// the action that makes one out of sight.
|
|
225
|
+
expect(text.indexOf('Quick worktree')).toBeLessThan(text.indexOf('wt/cosmic-dolphin'));
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('the project root row clears the worktree pick', async () => {
|
|
229
|
+
stubList([primary, made]);
|
|
230
|
+
const picks: Array<{ folderId: number | null; worktreePath: string | null } | null> = [];
|
|
231
|
+
const host = await mount(
|
|
232
|
+
h(WorktreePicker, {
|
|
233
|
+
folders: [folder],
|
|
234
|
+
selectedFolderId: 1,
|
|
235
|
+
target: { folderId: 1, worktreePath: made.path },
|
|
236
|
+
setTarget: (next) => picks.push(next),
|
|
237
|
+
}),
|
|
238
|
+
);
|
|
239
|
+
await settle();
|
|
240
|
+
|
|
241
|
+
await act(async () => {
|
|
242
|
+
(host.querySelector('button') as HTMLButtonElement).click();
|
|
243
|
+
});
|
|
244
|
+
await settle();
|
|
245
|
+
|
|
246
|
+
const rows = [...host.querySelectorAll('[role="menuitemradio"]')] as HTMLButtonElement[];
|
|
247
|
+
// Root first, then the worktree.
|
|
248
|
+
expect(rows.length).toBe(2);
|
|
249
|
+
await act(async () => {
|
|
250
|
+
rows[0]?.click();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
expect(picks).toEqual([{ folderId: 1, worktreePath: null }]);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('a picked worktree marks its own row and names itself on the trigger', async () => {
|
|
257
|
+
stubList([primary, made]);
|
|
258
|
+
const host = await mount(
|
|
259
|
+
h(WorktreePicker, {
|
|
260
|
+
folders: [folder],
|
|
261
|
+
selectedFolderId: 1,
|
|
262
|
+
target: { folderId: 1, worktreePath: made.path },
|
|
263
|
+
setTarget: () => {},
|
|
264
|
+
}),
|
|
265
|
+
);
|
|
266
|
+
await settle();
|
|
267
|
+
|
|
268
|
+
expect((host.querySelector('button') as HTMLButtonElement).textContent).toContain('wt/cosmic-dolphin');
|
|
269
|
+
// The dirty count is on the trigger too: a chat about to run in a checkout
|
|
270
|
+
// with two changed files should say so before the first prompt.
|
|
271
|
+
expect(host.querySelector('[aria-label="2 uncommitted files in this checkout"]')).not.toBeNull();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test('a create re-reads the list, so the trigger names the branch not the folder', async () => {
|
|
275
|
+
// The pick names a path the list has never seen — exactly the state after a
|
|
276
|
+
// create. `worktreeLabel` falls back to the folder's last segment there, so
|
|
277
|
+
// the trigger read `groovy-yak` where the branch is `wt/groovy-yak`, and it
|
|
278
|
+
// kept reading that until the menu was opened again (measured in the
|
|
279
|
+
// browser). The list is what the label is derived from, so the create has to
|
|
280
|
+
// re-read it.
|
|
281
|
+
const fresh = row({
|
|
282
|
+
path: '/data/worktrees/x/groovy-yak',
|
|
283
|
+
branch: 'wt/groovy-yak',
|
|
284
|
+
isPrimary: false,
|
|
285
|
+
});
|
|
286
|
+
const calls = stubCreate([primary], fresh);
|
|
287
|
+
const picks: Array<{ folderId: number | null; worktreePath: string | null } | null> = [];
|
|
288
|
+
const host = await mount(
|
|
289
|
+
h(WorktreePicker, {
|
|
290
|
+
folders: [folder],
|
|
291
|
+
selectedFolderId: 1,
|
|
292
|
+
target: null,
|
|
293
|
+
setTarget: (next) => picks.push(next),
|
|
294
|
+
}),
|
|
295
|
+
);
|
|
296
|
+
await settle();
|
|
297
|
+
|
|
298
|
+
await act(async () => {
|
|
299
|
+
(host.querySelector('button') as HTMLButtonElement).click();
|
|
300
|
+
});
|
|
301
|
+
await settle();
|
|
302
|
+
|
|
303
|
+
const quick = [...host.querySelectorAll('[role="menuitem"]')].find((node) =>
|
|
304
|
+
(node.textContent ?? '').includes('Quick worktree'),
|
|
305
|
+
) as HTMLButtonElement | undefined;
|
|
306
|
+
expect(quick).toBeDefined();
|
|
307
|
+
await act(async () => {
|
|
308
|
+
quick?.click();
|
|
309
|
+
});
|
|
310
|
+
await settle();
|
|
311
|
+
|
|
312
|
+
expect(picks).toEqual([{ folderId: 1, worktreePath: fresh.path }]);
|
|
313
|
+
// The read AFTER the POST is the one that matters; without it the label is
|
|
314
|
+
// `groovy-yak`.
|
|
315
|
+
const postIndex = calls.findIndex((call) => call.startsWith('POST'));
|
|
316
|
+
expect(postIndex).toBeGreaterThanOrEqual(0);
|
|
317
|
+
expect(calls.slice(postIndex + 1).some((call) => call.startsWith('GET'))).toBe(true);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* WHICH CHECKOUT of the workspace the chat runs in: the project root, or one of
|
|
8
|
+
* its git worktrees.
|
|
9
|
+
*
|
|
10
|
+
* Its own control, not a group inside the workspace picker. The two answer
|
|
11
|
+
* different questions — which project, and which checkout of it — and the merged
|
|
12
|
+
* menu made both harder: a user looking for another project scrolled through
|
|
13
|
+
* branch names, and the worktree rows had no trigger of their own to be found
|
|
14
|
+
* from. OpenChamber's composer draws the same pair, and its worktree `Select`
|
|
15
|
+
* carries the branch label on the trigger, which is what this one does too.
|
|
16
|
+
*
|
|
17
|
+
* The control is ABSENT for a workspace that is not a git repository: there is
|
|
18
|
+
* nothing to pick and nothing to create, so a permanently disabled control in
|
|
19
|
+
* every notes folder would be noise. `isRepo` comes from the list payload, so
|
|
20
|
+
* the decision is the server's, not a guess from the folder's shape.
|
|
21
|
+
*
|
|
22
|
+
* The pick is `{folderId, worktreePath}`; the folder is what the workspace
|
|
23
|
+
* picker chose, and this control only ever moves the second half.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
|
27
|
+
import { AlertTriangle, Check, ChevronDown, GitBranch } from 'lucide-preact';
|
|
28
|
+
import { useOnClickOutside } from '@/client/hooks/ui/on-click-outside';
|
|
29
|
+
import { createWorktree, useWorktrees } from '@/client/hooks/workspace/worktree/index';
|
|
30
|
+
import { NewWorktreeModal } from '@/client/components/workspace/worktree-modal/index';
|
|
31
|
+
import type { SpawnTarget } from '@/client/hooks/chat/timeline/spawn-target';
|
|
32
|
+
import type { WorktreeInfo } from '@/shared/lib/workspace/worktree/types';
|
|
33
|
+
import { lastPathSegment } from '@/shared/lib/fs/paths';
|
|
34
|
+
import type { WorkspaceFolderData } from '@/shared/types';
|
|
35
|
+
|
|
36
|
+
export interface WorktreePickerProps {
|
|
37
|
+
folders: WorkspaceFolderData[];
|
|
38
|
+
selectedFolderId: number | null;
|
|
39
|
+
target: SpawnTarget | null;
|
|
40
|
+
setTarget: (next: SpawnTarget | null) => void;
|
|
41
|
+
/** Reports a completed create, or the refusal that stopped one. */
|
|
42
|
+
onReport?: (message: string) => void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The worktree row the pick names, or null for the project root. */
|
|
46
|
+
export function selectedWorktree(worktrees: WorktreeInfo[], target: SpawnTarget | null): WorktreeInfo | null {
|
|
47
|
+
if (!target?.worktreePath) return null;
|
|
48
|
+
return worktrees.find((worktree) => worktree.path === target.worktreePath) ?? null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The trigger's words: the branch of the checkout the chat will run in.
|
|
53
|
+
*
|
|
54
|
+
* The project root is named by the branch it holds rather than by the word
|
|
55
|
+
* "root", which is what OpenChamber shows and what a reader needs: the root of a
|
|
56
|
+
* repository sitting on `main` and one sitting on `wt/scratch` are different
|
|
57
|
+
* places to start a chat. `Project root` remains the fallback for a payload that
|
|
58
|
+
* has not arrived, or a repository git cannot read a branch from.
|
|
59
|
+
*/
|
|
60
|
+
export function worktreeLabel(worktrees: WorktreeInfo[], target: SpawnTarget | null): string {
|
|
61
|
+
const picked = selectedWorktree(worktrees, target);
|
|
62
|
+
if (target?.worktreePath) {
|
|
63
|
+
if (!picked) return lastPathSegment(target.worktreePath) || 'Worktree';
|
|
64
|
+
return picked.branch || (picked.detached ? 'detached' : lastPathSegment(picked.path) || 'Worktree');
|
|
65
|
+
}
|
|
66
|
+
const primary = worktrees.find((worktree) => worktree.isPrimary);
|
|
67
|
+
return primary?.branch || 'Project root';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function WorktreePicker({ folders, selectedFolderId, target, setTarget, onReport }: WorktreePickerProps) {
|
|
71
|
+
const [open, setOpen] = useState(false);
|
|
72
|
+
const [creating, setCreating] = useState(false);
|
|
73
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
74
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
75
|
+
useOnClickOutside(containerRef, () => setOpen(false));
|
|
76
|
+
|
|
77
|
+
const folder = folders.find((candidate) => String(candidate.id) === String(selectedFolderId)) ?? null;
|
|
78
|
+
const scopeRoot = folder?.project_path ?? null;
|
|
79
|
+
const folderId = folder ? Number(folder.id) : null;
|
|
80
|
+
|
|
81
|
+
// Read on MOUNT, not only on open: the trigger's own label is the branch the
|
|
82
|
+
// project root holds, and whether this control exists at all is the payload's
|
|
83
|
+
// `isRepo`. Measured cost of the read on this repository: ~150 ms, one spawn
|
|
84
|
+
// per worktree for its dirty count.
|
|
85
|
+
const { worktrees, isRepo, error, refresh } = useWorktrees(
|
|
86
|
+
{ root: scopeRoot, folderId },
|
|
87
|
+
Boolean(scopeRoot),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Re-read when the menu opens, so a worktree made in another tab or by the
|
|
91
|
+
// dialog is in the list the user is looking at.
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (open) void refresh();
|
|
94
|
+
}, [open, refresh]);
|
|
95
|
+
|
|
96
|
+
// A pick is cleared by the WORKSPACE picker's own choice, never by an effect
|
|
97
|
+
// comparing `target.folderId` with the selected folder: on a reload the target
|
|
98
|
+
// is restored from the session's UI state a beat before the folder resolves,
|
|
99
|
+
// and a mismatch check in that window wiped the very pick it was protecting.
|
|
100
|
+
|
|
101
|
+
const list = useMemo(() => worktrees.filter((worktree) => !worktree.isPrimary), [worktrees]);
|
|
102
|
+
const picked = selectedWorktree(worktrees, target);
|
|
103
|
+
const primary = worktrees.find((worktree) => worktree.isPrimary) ?? null;
|
|
104
|
+
const dirty = (picked ?? primary)?.dirty ?? 0;
|
|
105
|
+
|
|
106
|
+
const pickRoot = () => {
|
|
107
|
+
if (folderId !== null) setTarget({ folderId, worktreePath: null });
|
|
108
|
+
setOpen(false);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const pickWorktree = (worktreePath: string) => {
|
|
112
|
+
if (folderId !== null) setTarget({ folderId, worktreePath });
|
|
113
|
+
setOpen(false);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Quick worktree: a generated branch and folder, created and selected without
|
|
118
|
+
* leaving the draft. Reported rather than silent — the create takes a moment
|
|
119
|
+
* and a pick that changed with no word reads as a mis-click.
|
|
120
|
+
*/
|
|
121
|
+
const quickWorktree = async () => {
|
|
122
|
+
if (!scopeRoot || folderId === null) return;
|
|
123
|
+
setCreating(true);
|
|
124
|
+
try {
|
|
125
|
+
const created = await createWorktree({ root: scopeRoot, folderId }, { mode: 'new' });
|
|
126
|
+
pickWorktree(created.path);
|
|
127
|
+
// The list this control reads is the one that was fetched when the menu
|
|
128
|
+
// opened — before the worktree existed. Without this read the pick names a
|
|
129
|
+
// path no row holds, `worktreeLabel` falls back to the folder's last
|
|
130
|
+
// segment, and the trigger says `groovy-yak` where the checkout's branch
|
|
131
|
+
// is `wt/groovy-yak` (measured in the browser, and for the whole time the
|
|
132
|
+
// menu stays closed, which is when the label matters).
|
|
133
|
+
await refresh();
|
|
134
|
+
onReport?.(`Worktree ${created.branch || lastPathSegment(created.path)} created`);
|
|
135
|
+
} catch (failure) {
|
|
136
|
+
onReport?.(failure instanceof Error ? failure.message : String(failure));
|
|
137
|
+
} finally {
|
|
138
|
+
setCreating(false);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Not a repository (or not read yet): no control at all. Rendering one that
|
|
143
|
+
// can only refuse would promise a worktree a plain folder cannot hold.
|
|
144
|
+
if (!scopeRoot || isRepo !== true) return null;
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<div className="relative shrink-0" ref={containerRef}>
|
|
148
|
+
<button
|
|
149
|
+
type="button"
|
|
150
|
+
aria-label={`Worktree: ${worktreeLabel(worktrees, target)}`}
|
|
151
|
+
aria-haspopup="menu"
|
|
152
|
+
aria-expanded={open}
|
|
153
|
+
onClick={() => setOpen((value) => !value)}
|
|
154
|
+
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"
|
|
155
|
+
>
|
|
156
|
+
{dirty > 0 ? (
|
|
157
|
+
<AlertTriangle
|
|
158
|
+
size={13}
|
|
159
|
+
className="text-warning flex-shrink-0"
|
|
160
|
+
aria-label={`${dirty} uncommitted file${dirty === 1 ? '' : 's'} in this checkout`}
|
|
161
|
+
/>
|
|
162
|
+
) : (
|
|
163
|
+
<GitBranch size={13} className="text-ink/50 group-hover:text-ink/80 transition-colors flex-shrink-0" />
|
|
164
|
+
)}
|
|
165
|
+
<span className="max-w-[240px] truncate tracking-tight">{worktreeLabel(worktrees, target)}</span>
|
|
166
|
+
<ChevronDown
|
|
167
|
+
size={13}
|
|
168
|
+
className={`text-ink/40 group-hover:text-ink/70 flex-shrink-0 transition-transform duration-150 ${open ? 'rotate-180 text-ink/70' : ''}`}
|
|
169
|
+
/>
|
|
170
|
+
</button>
|
|
171
|
+
|
|
172
|
+
{open && (
|
|
173
|
+
<div
|
|
174
|
+
role="menu"
|
|
175
|
+
className="absolute bottom-full left-0 mb-1.5 w-72 bg-paper border border-ink/10 rounded-lg shadow-lg z-50 flex flex-col overflow-hidden text-xs py-1"
|
|
176
|
+
>
|
|
177
|
+
<div className="px-3 py-1.5 text-[10px] uppercase tracking-wider text-ink/40 font-semibold border-b border-ink/5">
|
|
178
|
+
Project root
|
|
179
|
+
</div>
|
|
180
|
+
<button
|
|
181
|
+
type="button"
|
|
182
|
+
role="menuitemradio"
|
|
183
|
+
aria-checked={!target?.worktreePath}
|
|
184
|
+
onClick={pickRoot}
|
|
185
|
+
title={primary?.path ?? scopeRoot}
|
|
186
|
+
className={`w-full text-left px-3 py-1.5 flex items-center justify-between transition-colors cursor-pointer ${
|
|
187
|
+
!target?.worktreePath ? 'bg-ink/10 text-ink font-semibold' : 'text-ink/80 hover:bg-ink/5 hover:text-ink'
|
|
188
|
+
}`}
|
|
189
|
+
>
|
|
190
|
+
<span className="flex items-center gap-2 truncate pr-2">
|
|
191
|
+
<GitBranch size={12} className="text-ink/50 flex-shrink-0" />
|
|
192
|
+
<span className="truncate font-medium">{primary?.branch || folder?.name || 'Project root'}</span>
|
|
193
|
+
{primary && primary.dirty > 0 && (
|
|
194
|
+
<span className="text-ink/40 shrink-0" title={`${primary.dirty} uncommitted files`}>
|
|
195
|
+
●{primary.dirty}
|
|
196
|
+
</span>
|
|
197
|
+
)}
|
|
198
|
+
</span>
|
|
199
|
+
{!target?.worktreePath && <Check size={12} className="text-ink flex-shrink-0" />}
|
|
200
|
+
</button>
|
|
201
|
+
|
|
202
|
+
{/* Making one comes before the list, so a long list never hides it. */}
|
|
203
|
+
<div className="my-1 border-t border-ink/5" />
|
|
204
|
+
<button
|
|
205
|
+
type="button"
|
|
206
|
+
role="menuitem"
|
|
207
|
+
disabled={creating}
|
|
208
|
+
onClick={() => void quickWorktree()}
|
|
209
|
+
className="w-full text-left px-3 py-1.5 flex items-center gap-2 text-ink/80 hover:bg-ink/5 hover:text-ink disabled:opacity-50"
|
|
210
|
+
>
|
|
211
|
+
<span aria-hidden="true">⚡</span>
|
|
212
|
+
{creating ? 'Creating…' : 'Quick worktree'}
|
|
213
|
+
</button>
|
|
214
|
+
<button
|
|
215
|
+
type="button"
|
|
216
|
+
role="menuitem"
|
|
217
|
+
onClick={() => {
|
|
218
|
+
setOpen(false);
|
|
219
|
+
setDialogOpen(true);
|
|
220
|
+
}}
|
|
221
|
+
className="w-full text-left px-3 py-1.5 flex items-center gap-2 text-ink/80 hover:bg-ink/5 hover:text-ink"
|
|
222
|
+
>
|
|
223
|
+
<GitBranch size={12} className="text-ink/50" />
|
|
224
|
+
New worktree…
|
|
225
|
+
</button>
|
|
226
|
+
|
|
227
|
+
<div className="my-1 border-t border-ink/5" />
|
|
228
|
+
<div className="px-3 py-1.5 text-[10px] uppercase tracking-wider text-ink/40 font-semibold">Worktrees</div>
|
|
229
|
+
<div className="max-h-56 scrollbar-overlay-container py-1">
|
|
230
|
+
{list.length === 0 ? (
|
|
231
|
+
<div className="px-3 py-2 text-ink/40 italic">
|
|
232
|
+
{error ? 'Worktrees could not be read' : 'No worktrees yet'}
|
|
233
|
+
</div>
|
|
234
|
+
) : (
|
|
235
|
+
list.map((worktree) => {
|
|
236
|
+
const active = target?.worktreePath === worktree.path;
|
|
237
|
+
return (
|
|
238
|
+
<button
|
|
239
|
+
key={worktree.path}
|
|
240
|
+
type="button"
|
|
241
|
+
role="menuitemradio"
|
|
242
|
+
aria-checked={active}
|
|
243
|
+
onClick={() => pickWorktree(worktree.path)}
|
|
244
|
+
title={worktree.path}
|
|
245
|
+
className={`w-full text-left px-3 py-1.5 flex items-center justify-between transition-colors cursor-pointer ${
|
|
246
|
+
active ? 'bg-ink/10 text-ink font-semibold' : 'text-ink/80 hover:bg-ink/5 hover:text-ink'
|
|
247
|
+
}`}
|
|
248
|
+
>
|
|
249
|
+
<span className="flex items-center gap-2 truncate pr-2">
|
|
250
|
+
<GitBranch size={12} className="text-ink/50 flex-shrink-0" />
|
|
251
|
+
<span className="truncate font-medium">{worktree.branch || 'detached'}</span>
|
|
252
|
+
{worktree.missing && <span className="text-warning shrink-0">missing</span>}
|
|
253
|
+
{worktree.dirty > 0 && (
|
|
254
|
+
<span className="text-ink/40 shrink-0" title={`${worktree.dirty} uncommitted files`}>
|
|
255
|
+
●{worktree.dirty}
|
|
256
|
+
</span>
|
|
257
|
+
)}
|
|
258
|
+
</span>
|
|
259
|
+
{active && <Check size={12} className="text-ink flex-shrink-0" />}
|
|
260
|
+
</button>
|
|
261
|
+
);
|
|
262
|
+
})
|
|
263
|
+
)}
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
)}
|
|
267
|
+
|
|
268
|
+
<NewWorktreeModal
|
|
269
|
+
isOpen={dialogOpen}
|
|
270
|
+
onClose={() => setDialogOpen(false)}
|
|
271
|
+
root={scopeRoot}
|
|
272
|
+
folderId={folderId}
|
|
273
|
+
folderName={folder?.name}
|
|
274
|
+
onCreated={(created) => {
|
|
275
|
+
pickWorktree(created.path);
|
|
276
|
+
// Same reason as the quick path: the pick names a worktree the list
|
|
277
|
+
// this control holds has never seen, so the trigger would fall back to
|
|
278
|
+
// the folder name until the menu was opened again.
|
|
279
|
+
void refresh();
|
|
280
|
+
onReport?.(`Worktree ${created.branch} created`);
|
|
281
|
+
}}
|
|
282
|
+
/>
|
|
283
|
+
</div>
|
|
284
|
+
);
|
|
285
|
+
}
|
package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.test.ts
CHANGED
|
@@ -40,6 +40,8 @@ function mountFromFrame(frame: IncomingExtensionUiRequest): ExtensionUiDialogReq
|
|
|
40
40
|
persistMessages: () => {}, abortControllerRef: { current: null }, appSettings: {},
|
|
41
41
|
enqueueExtensionDialog: (request) => queue.push(request),
|
|
42
42
|
withdrawExtensionDialog: (targetId) => { const i = queue.findIndex((r) => r.id === targetId); if (i >= 0) queue.splice(i, 1); },
|
|
43
|
+
reloadMessages: () => {},
|
|
44
|
+
needsReloadRef: { current: false },
|
|
43
45
|
} satisfies OmpAgentCallbacksDeps;
|
|
44
46
|
createOmpAgentCallbacks(deps).onExtensionUiRequest?.(frame);
|
|
45
47
|
|