@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
|
@@ -13,18 +13,13 @@ import { ResizeHandle } from '@/client/components/layout/desktop-layout/ResizeHa
|
|
|
13
13
|
import { useAvailableWidth } from '@/client/hooks/workspace/available-width';
|
|
14
14
|
import { useSessionStateContext } from '@/client/hooks/workspace/session-state/context';
|
|
15
15
|
import {
|
|
16
|
-
DEFAULT_EDITOR_FRACTIONS,
|
|
17
|
-
DEFAULT_EDITOR_WIDTHS,
|
|
18
16
|
HANDLE_WIDTH,
|
|
19
17
|
MIN_EDITOR_PANEL_WIDTH,
|
|
20
18
|
fillerMinWidth,
|
|
21
|
-
pairMaxSize,
|
|
22
|
-
panelFraction,
|
|
23
|
-
resolvePanelWidth,
|
|
24
19
|
type EditorWidthMode,
|
|
25
20
|
type PanelWidths,
|
|
26
21
|
} from '@/shared/lib/workspace/panel-widths';
|
|
27
|
-
import {
|
|
22
|
+
import { resolveWorkspacePanelWidths } from '@/shared/lib/workspace/workspace-panel-widths';
|
|
28
23
|
|
|
29
24
|
const Editor = lazy(() => import('@/client/components/workspace/editor/index').then((m) => ({ default: m.Editor })));
|
|
30
25
|
|
|
@@ -43,6 +38,13 @@ function PanelSuspense({ children }: { children: ReactNode }) {
|
|
|
43
38
|
interface WorkspacePanelsProps {
|
|
44
39
|
appSettings?: Record<string, any>;
|
|
45
40
|
showEditor: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* The chat column is collapsed: the editor's compact/full view took its
|
|
43
|
+
* width. The chat STAYS MOUNTED (it owns the live stream and the composer)
|
|
44
|
+
* but is sized to nothing and marked inert, so a run in progress is not
|
|
45
|
+
* thrown away by widening a document.
|
|
46
|
+
*/
|
|
47
|
+
chatHidden?: boolean;
|
|
46
48
|
editorPanelRef: RefObject<PanelImperativeHandle | null>;
|
|
47
49
|
showRightPanel: boolean;
|
|
48
50
|
/** A built-in view id or a plugin panel key (`plugin:<id>/<panel>`). */
|
|
@@ -78,6 +80,7 @@ export function WorkspacePanels(props: WorkspacePanelsProps) {
|
|
|
78
80
|
const {
|
|
79
81
|
appSettings,
|
|
80
82
|
showEditor,
|
|
83
|
+
chatHidden = false,
|
|
81
84
|
editorPanelRef,
|
|
82
85
|
showRightPanel,
|
|
83
86
|
activeRightPanel,
|
|
@@ -138,75 +141,27 @@ export function WorkspacePanels(props: WorkspacePanelsProps) {
|
|
|
138
141
|
// one, which is why a Kabmo without one looks exactly as it did.
|
|
139
142
|
const editorPlugins = usePanelSlotPlugins();
|
|
140
143
|
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
// The editor and the right panel are a COUPLED pair (see `panel-widths.ts`):
|
|
152
|
-
// they share `PAIR_MAX_TOTAL_FRACTION` and each is capped by `PAIR_MAX_FRACTION`.
|
|
153
|
-
// The right panel is resolved first, so it reserves the editor's WANTED share
|
|
154
|
-
// (the editor has no result yet); the editor then reserves the right panel's
|
|
155
|
-
// ACTUAL width. That asymmetry is deliberate: an editor asking for more than
|
|
156
|
-
// its default makes the right panel give way first, and the pair still sums
|
|
157
|
-
// to the total.
|
|
158
|
-
const editorFraction = panelFraction(panelWidths[editorWidthMode], DEFAULT_EDITOR_FRACTIONS[editorWidthMode], available);
|
|
159
|
-
const editorReservePx = available != null && available > 0 ? Math.round(editorFraction * available) : 0;
|
|
160
|
-
|
|
161
|
-
// Resolve the RIGHT panel first, then the editor against the width it
|
|
162
|
-
// actually got. The px order is the other half of the contract: each panel's
|
|
163
|
-
// group budget is `available − chat floor − the sibling's reserved width −
|
|
164
|
-
// handles`, so reserving only a sibling's FLOOR (the previous shape) let the
|
|
165
|
-
// two declared widths sum past the group, and flexbox then shrank BOTH —
|
|
166
|
-
// measured at 1440 with both open: the editor asked for 507 and rendered 477,
|
|
167
|
-
// the right panel asked for 245 and rendered 230. The right panel is resolved
|
|
168
|
-
// first because it is the smaller, view-specific slot: it keeps its own width
|
|
169
|
-
// and the editor absorbs the remainder.
|
|
170
|
-
const rightWidth = resolvePanelWidth({
|
|
171
|
-
stored: panelWidths.right?.[activeRightPanel],
|
|
172
|
-
defaultFraction: activePanel?.defaultFraction ?? DEFAULT_RIGHT_PANEL_FRACTION,
|
|
173
|
-
defaultPx: activePanel?.defaultWidth ?? MIN_EDITOR_PANEL_WIDTH,
|
|
174
|
-
available,
|
|
175
|
-
min: rightPanelMin,
|
|
176
|
-
// The pair cap: the right panel may not exceed `total − the editor's wanted share`.
|
|
177
|
-
pairReservePx: showEditor ? editorReservePx : 0,
|
|
178
|
-
// The editor renders before the right panel and has not been resolved yet,
|
|
179
|
-
// so only its floor can be reserved here.
|
|
180
|
-
siblingWidth: showEditor ? MIN_EDITOR_PANEL_WIDTH : 0,
|
|
181
|
-
handleCount,
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
const editorWidth = resolvePanelWidth({
|
|
185
|
-
stored: panelWidths[editorWidthMode],
|
|
186
|
-
defaultFraction: DEFAULT_EDITOR_FRACTIONS[editorWidthMode],
|
|
187
|
-
defaultPx: DEFAULT_EDITOR_WIDTHS[editorWidthMode],
|
|
188
|
-
available,
|
|
189
|
-
min: MIN_EDITOR_PANEL_WIDTH,
|
|
190
|
-
// The pair cap: the editor may not exceed `total − the right panel's width`.
|
|
191
|
-
pairReservePx: showRightPanel ? rightWidth : 0,
|
|
192
|
-
// The right panel is resolved above; reserve the width it actually took so
|
|
193
|
-
// the editor's group budget cannot push the pair past the group.
|
|
194
|
-
siblingWidth: showRightPanel ? rightWidth : 0,
|
|
144
|
+
// The active view's own floor and defaults are read here and handed to the
|
|
145
|
+
// resolver: a view that is switched off (or a plugin that has not loaded) has
|
|
146
|
+
// no entry, and the right panel falls back to the editor's own floor — it
|
|
147
|
+
// renders the notice, not a view.
|
|
148
|
+
const {
|
|
149
|
+
editor: editorWidth,
|
|
150
|
+
right: rightWidth,
|
|
151
|
+
editorMax: editorMaxSize,
|
|
152
|
+
rightMax: rightMaxSize,
|
|
153
|
+
rightMin: rightPanelMin,
|
|
195
154
|
handleCount,
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
* `undefined` before the group is measured (no cap yet).
|
|
207
|
-
*/
|
|
208
|
-
const editorMaxSize = pairMaxSize({ available, siblingMin: rightPanelMin, siblingOpen: showRightPanel });
|
|
209
|
-
const rightMaxSize = pairMaxSize({ available, siblingMin: MIN_EDITOR_PANEL_WIDTH, siblingOpen: showEditor });
|
|
155
|
+
} = resolveWorkspacePanelWidths({
|
|
156
|
+
available,
|
|
157
|
+
panelWidths,
|
|
158
|
+
editorWidthMode,
|
|
159
|
+
activeRightPanel,
|
|
160
|
+
activePanel,
|
|
161
|
+
showEditor,
|
|
162
|
+
showRightPanel,
|
|
163
|
+
chatHidden,
|
|
164
|
+
});
|
|
210
165
|
|
|
211
166
|
/**
|
|
212
167
|
* Widths of the fixed panels as they are right now, keyed by slot: the
|
|
@@ -222,7 +177,11 @@ export function WorkspacePanels(props: WorkspacePanelsProps) {
|
|
|
222
177
|
const patch: PanelWidths = {};
|
|
223
178
|
const editor = editorPanelRef.current?.getSize()?.inPixels;
|
|
224
179
|
const right = rightPanelRef.current?.getSize()?.inPixels;
|
|
225
|
-
|
|
180
|
+
// With the chat hidden the editor's width is whatever is left over, not a
|
|
181
|
+
// choice — persisting it would overwrite the width the user set for the
|
|
182
|
+
// ordinary layout with the one this mode produced. A drag made here still
|
|
183
|
+
// moves the RIGHT panel, which is the width that was actually chosen.
|
|
184
|
+
if (!chatHidden && editor != null && editor > 0) {
|
|
226
185
|
patch[editorWidthMode] = { px: Math.round(editor), ...(available ? { fraction: editor / available } : {}) };
|
|
227
186
|
}
|
|
228
187
|
if (right != null && right > 0) {
|
|
@@ -250,6 +209,10 @@ export function WorkspacePanels(props: WorkspacePanelsProps) {
|
|
|
250
209
|
<Panel
|
|
251
210
|
id="center-panel"
|
|
252
211
|
filler
|
|
212
|
+
// Collapsed — not unmounted — in compact/full view: the chat holds the
|
|
213
|
+
// live run, the composer draft and the scroll position, and widening a
|
|
214
|
+
// document must not discard any of them.
|
|
215
|
+
collapsed={chatHidden}
|
|
253
216
|
minSize={fillerMinWidth({
|
|
254
217
|
available,
|
|
255
218
|
reservedPx:
|
|
@@ -258,15 +221,15 @@ export function WorkspacePanels(props: WorkspacePanelsProps) {
|
|
|
258
221
|
handleCount * HANDLE_WIDTH,
|
|
259
222
|
})}
|
|
260
223
|
>
|
|
261
|
-
<div className="relative w-full h-full" {...drop.dropProps}>
|
|
224
|
+
<div className="relative w-full h-full" {...drop.dropProps} {...drop.captureDropProps} inert={chatHidden}>
|
|
262
225
|
<EditorDropOverlay visible={drop.isDragging} />
|
|
263
226
|
<ChatTimeline className="w-full h-full" appSettings={appSettings} onSessionTitle={onSessionTitle} />
|
|
264
227
|
</div>
|
|
265
228
|
</Panel>
|
|
266
229
|
|
|
267
|
-
{showEditor && <ResizeHandle />}
|
|
230
|
+
{showEditor && !chatHidden && <ResizeHandle />}
|
|
268
231
|
<Panel panelRef={editorPanelRef} id="editor-panel" defaultSize={editorWidth} minSize={MIN_EDITOR_PANEL_WIDTH} maxSize={editorMaxSize} collapsed={!showEditor}>
|
|
269
|
-
<div className="flex w-full h-full" {...drop.dropProps}>
|
|
232
|
+
<div className="flex w-full h-full" {...drop.dropProps} {...drop.captureDropProps}>
|
|
270
233
|
<EditorDropOverlay visible={drop.isDragging} />
|
|
271
234
|
<EditorPluginRail
|
|
272
235
|
activeKey={activeEditorPanelKey}
|
|
@@ -3,8 +3,7 @@ import { useSearchParams } from '@/client/lib/router/search-params';
|
|
|
3
3
|
import { Group, Panel, type PanelImperativeHandle } from '@/client/components/layout/desktop-layout/resizer';
|
|
4
4
|
import { LeftColumn } from '@/client/components/layout/desktop-layout/LeftColumn';
|
|
5
5
|
import { SettingsModal } from '@/client/components/settings/LazyModal';
|
|
6
|
-
import
|
|
7
|
-
import { activeProjectForSession } from '@/shared/lib/workspace/active-project';
|
|
6
|
+
import { activeProjectForSession, effectiveRootFor } from '@/shared/lib/workspace/active-project';
|
|
8
7
|
import { useFileTabs } from '@/client/hooks/workspace/file-tabs';
|
|
9
8
|
import { useSessionState } from '@/client/hooks/workspace/session-state';
|
|
10
9
|
import { TopNavbar } from '@/client/components/layout/desktop-layout/TopNavbar';
|
|
@@ -18,17 +17,17 @@ import { MIN_LEFT_DOCK_PLUGIN_WIDTH } from '@/shared/lib/workspace/left-dock-wid
|
|
|
18
17
|
import { SESSIONS_VIEW_ID } from '@/shared/lib/workspace/left-dock';
|
|
19
18
|
import { useOpenPanelRequest } from '@/client/hooks/workspace/open-panel';
|
|
20
19
|
import { useLeftDock, useLeftDockWidth } from '@/client/hooks/workspace/left-dock';
|
|
20
|
+
import { useEditorViewLayout } from '@/client/hooks/workspace/editor-view';
|
|
21
21
|
import { useLeftDockView } from '@/client/components/workspace/plugin-panel/catalog';
|
|
22
22
|
import type { EditorWidthMode } from '@/shared/lib/workspace/panel-widths';
|
|
23
|
-
import {
|
|
23
|
+
import { useSettingsModal } from '@/client/hooks/ui/settings-modal';
|
|
24
24
|
import { useWorkspaceKeybindings } from '@/client/hooks/workspace/keybindings';
|
|
25
25
|
import { PluginBootstrap } from '@/client/lib/plugins/bootstrap';
|
|
26
26
|
import { useToasts } from '@/client/hooks/ui/toasts';
|
|
27
27
|
import type { EditorDropResult } from '@/client/hooks/editor/use-editor-drop';
|
|
28
28
|
import { ToastStack } from '@/client/components/common/ToastStack';
|
|
29
29
|
import { SessionDeleteModal } from '@/client/components/common/session-delete-modal';
|
|
30
|
-
import {
|
|
31
|
-
import { useSessionDelete } from '@/client/hooks/workspace/session-delete';
|
|
30
|
+
import { useNavbarSessionActions } from '@/client/hooks/workspace/navbar-session-actions';
|
|
32
31
|
|
|
33
32
|
interface DesktopLayoutProps {
|
|
34
33
|
sessionId: string | null;
|
|
@@ -71,7 +70,13 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
|
|
|
71
70
|
if (folderIdParam) return folders.find(f => String(f.id) === String(folderIdParam)) ?? null;
|
|
72
71
|
return null;
|
|
73
72
|
}, [folders, sessionId, folderIdParam]);
|
|
74
|
-
|
|
73
|
+
// A worktree session runs OUTSIDE its folder's project path, so the panels
|
|
74
|
+
// follow the session's own root — the checkout it left would otherwise be what
|
|
75
|
+
// Files, Search, Git and the terminal read.
|
|
76
|
+
const activeProjectPath = useMemo(
|
|
77
|
+
() => effectiveRootFor(folders, sessionId || '') ?? activeProject?.project_path ?? null,
|
|
78
|
+
[folders, sessionId, activeProject],
|
|
79
|
+
);
|
|
75
80
|
const hasActiveContext = !!activeProject;
|
|
76
81
|
|
|
77
82
|
const handleOpenTab = useCallback(() => {
|
|
@@ -106,32 +111,30 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
|
|
|
106
111
|
[openedFiles, activeFileId],
|
|
107
112
|
);
|
|
108
113
|
|
|
114
|
+
/**
|
|
115
|
+
* The editor's WIDTH mode (`compact` / `full`), entered from either toolbar.
|
|
116
|
+
*
|
|
117
|
+
* It collapses columns around the editor and is resolved here, not in the
|
|
118
|
+
* panels, because the sidebar is a sibling of the whole workspace group: a
|
|
119
|
+
* full view that left it standing would not be the width it promises.
|
|
120
|
+
*
|
|
121
|
+
* The mode is derived from `showEditor`, so closing the column returns the
|
|
122
|
+
* chat and the sidebar without writing anything — reopening it comes back to
|
|
123
|
+
* the width the user chose.
|
|
124
|
+
*/
|
|
125
|
+
const editorView = useEditorViewLayout(showEditor);
|
|
126
|
+
const sidebarOpen = showLeftPanel && !editorView.sidebarHidden;
|
|
127
|
+
|
|
109
128
|
// Width restoration is declarative: each panel's `defaultSize` comes from the
|
|
110
129
|
// remembered width for its current slot and `Panel` adopts a change, so a
|
|
111
130
|
// view switch needs no imperative push and no cached layout can go stale.
|
|
112
131
|
|
|
113
|
-
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
114
|
-
const [settingsCategory, setSettingsCategory] = useState<SettingsCategoryId>('appearance');
|
|
115
|
-
const [autoOpenAddProvider, setAutoOpenAddProvider] = useState(false);
|
|
116
132
|
const [refreshKey, setRefreshKey] = useState(0);
|
|
117
133
|
const handleRefreshWorkspace = () => setRefreshKey(k => k + 1);
|
|
118
134
|
const [sessionTitle, setSessionTitle] = useState<string | null>(null);
|
|
119
135
|
const streamStatus = useAgentStreamStatus();
|
|
136
|
+
const settings = useSettingsModal();
|
|
120
137
|
|
|
121
|
-
// The navbar's title menu acts on the session on screen. Its row may live in
|
|
122
|
-
// a folder that is not expanded in the sidebar, so the session is found by id
|
|
123
|
-
// across every folder rather than by walking the visible list. No row means
|
|
124
|
-
// nothing to act on — a session the sidebar does not know (a chat running
|
|
125
|
-
// outside every registered workspace) gets no menu rather than dead items.
|
|
126
|
-
const activeSession = useMemo(() => {
|
|
127
|
-
if (!sessionId) return null;
|
|
128
|
-
for (const folder of folders) {
|
|
129
|
-
const found = (folder.sessions || []).find((s) => String(s.id) === String(sessionId));
|
|
130
|
-
if (found) return found;
|
|
131
|
-
}
|
|
132
|
-
return null;
|
|
133
|
-
}, [folders, sessionId]);
|
|
134
|
-
const sidebar = useSidebarData();
|
|
135
138
|
const { toasts, pushToast, dismissToast } = useToasts();
|
|
136
139
|
|
|
137
140
|
/**
|
|
@@ -143,20 +146,19 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
|
|
|
143
146
|
(result: EditorDropResult) => pushToast(result.message, result.ok ? 'success' : 'error'),
|
|
144
147
|
[pushToast],
|
|
145
148
|
);
|
|
146
|
-
const {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
: undefined;
|
|
149
|
+
const { menu: sessionActions, sessionDelete } = useNavbarSessionActions(folders, sessionId, pushToast);
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Ask for the sidebar (or put it away).
|
|
153
|
+
*
|
|
154
|
+
* Full view hides the sidebar, so asking for it must leave that mode —
|
|
155
|
+
* otherwise the toggle flips a stored flag the layout is ignoring and the
|
|
156
|
+
* click does nothing. `compact` is the mode that keeps the wider editor.
|
|
157
|
+
*/
|
|
158
|
+
const handleToggleLeftPanel = (show: boolean) => {
|
|
159
|
+
if (show) editorView.revealSidebar();
|
|
160
|
+
setShowLeftPanel(show);
|
|
161
|
+
};
|
|
160
162
|
|
|
161
163
|
// The workspace shortcuts, from the shared keymap so the toggles' tooltips
|
|
162
164
|
// name the bound chord. `⌘S` is absent: the editor owns it, and binding it
|
|
@@ -164,26 +166,13 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
|
|
|
164
166
|
useWorkspaceKeybindings({
|
|
165
167
|
showEditor,
|
|
166
168
|
editorHasContent,
|
|
167
|
-
|
|
169
|
+
// Through the handler, not the raw flag: in full view the sidebar is hidden
|
|
170
|
+
// by the editor's WIDTH mode, so flipping `showLeftPanel` alone would change
|
|
171
|
+
// a value the layout is ignoring and the chord would do nothing.
|
|
172
|
+
toggleSidebar: () => handleToggleLeftPanel(!sidebarOpen),
|
|
168
173
|
toggleEditor: () => setUserToggledEditor((previous) => !(previous ?? showEditor)),
|
|
169
174
|
toggleRightPanel: () => setShowRightPanel((previous) => !previous),
|
|
170
|
-
openSettings: () =>
|
|
171
|
-
setSettingsCategory('appearance');
|
|
172
|
-
setAutoOpenAddProvider(false);
|
|
173
|
-
setSettingsOpen((previous) => !previous);
|
|
174
|
-
},
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
// Global event listener for settings triggers
|
|
178
|
-
useKabmoEvent('omp:open-settings', (e) => {
|
|
179
|
-
const customEvent = e as CustomEvent<{ category?: SettingsCategoryId; autoOpenAdd?: boolean }>;
|
|
180
|
-
if (customEvent.detail?.category) {
|
|
181
|
-
setSettingsCategory(customEvent.detail.category);
|
|
182
|
-
} else {
|
|
183
|
-
setSettingsCategory('appearance');
|
|
184
|
-
}
|
|
185
|
-
setAutoOpenAddProvider(!!customEvent.detail?.autoOpenAdd);
|
|
186
|
-
setSettingsOpen(true);
|
|
175
|
+
openSettings: () => (settings.isOpen ? settings.close() : settings.open()),
|
|
187
176
|
});
|
|
188
177
|
|
|
189
178
|
// Clicking the ACTIVE view on an open panel collapses that panel; any other
|
|
@@ -194,7 +183,6 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
|
|
|
194
183
|
if (!collapsing) setActiveRightPanel(panel);
|
|
195
184
|
};
|
|
196
185
|
|
|
197
|
-
const handleToggleLeftPanel = (show: boolean) => setShowLeftPanel(show);
|
|
198
186
|
const handleToggleRightPanel = () => setShowRightPanel(!showRightPanel);
|
|
199
187
|
const handleToggleEditor = () => setUserToggledEditor(!showEditor);
|
|
200
188
|
|
|
@@ -237,7 +225,7 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
|
|
|
237
225
|
>
|
|
238
226
|
<LeftColumn
|
|
239
227
|
panelRef={leftPanelRef}
|
|
240
|
-
open={
|
|
228
|
+
open={sidebarOpen}
|
|
241
229
|
railEnabled={dock.enabled}
|
|
242
230
|
view={leftView}
|
|
243
231
|
onSelectView={(panel) => {
|
|
@@ -245,7 +233,7 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
|
|
|
245
233
|
// toggle would hide the rail the user is aiming at, and the dock
|
|
246
234
|
// has its own collapse control in the header.
|
|
247
235
|
setActiveLeftView(panel);
|
|
248
|
-
if (!
|
|
236
|
+
if (!sidebarOpen) handleToggleLeftPanel(true);
|
|
249
237
|
}}
|
|
250
238
|
onCollapse={() => handleToggleLeftPanel(false)}
|
|
251
239
|
width={sidebarWidth}
|
|
@@ -266,20 +254,21 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
|
|
|
266
254
|
<TopNavbar
|
|
267
255
|
sessionTitle={sessionTitle}
|
|
268
256
|
sessionId={sessionId}
|
|
269
|
-
showLeftPanel={
|
|
257
|
+
showLeftPanel={sidebarOpen}
|
|
270
258
|
showEditor={showEditor}
|
|
271
259
|
showRightPanel={showRightPanel}
|
|
272
260
|
streamStatus={streamStatus}
|
|
273
261
|
onSwitchToMobile={onSwitchToMobile}
|
|
274
262
|
onToggleEditor={handleToggleEditor}
|
|
275
263
|
onToggleRightPanel={handleToggleRightPanel}
|
|
276
|
-
onToggleLeftPanel={() => handleToggleLeftPanel(!
|
|
264
|
+
onToggleLeftPanel={() => handleToggleLeftPanel(!sidebarOpen)}
|
|
277
265
|
sessionActions={sessionActions}
|
|
278
266
|
/>
|
|
279
267
|
|
|
280
268
|
<WorkspacePanels
|
|
281
269
|
appSettings={appSettings}
|
|
282
270
|
showEditor={showEditor}
|
|
271
|
+
chatHidden={editorView.chatHidden}
|
|
283
272
|
editorPanelRef={editorPanelRef}
|
|
284
273
|
showRightPanel={showRightPanel}
|
|
285
274
|
activeRightPanel={activeRightPanel}
|
|
@@ -315,15 +304,12 @@ export function DesktopLayout({ sessionId, onSwitchToMobile, appSettings = {} }:
|
|
|
315
304
|
</div>
|
|
316
305
|
|
|
317
306
|
{/* Global Desktop Settings Modal */}
|
|
318
|
-
<SettingsModal
|
|
319
|
-
isOpen={
|
|
320
|
-
onClose={
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
324
|
-
initialCategory={settingsCategory}
|
|
325
|
-
autoOpenAddProvider={autoOpenAddProvider}
|
|
326
|
-
appSettings={appSettings}
|
|
307
|
+
<SettingsModal
|
|
308
|
+
isOpen={settings.isOpen}
|
|
309
|
+
onClose={settings.close}
|
|
310
|
+
initialCategory={settings.category}
|
|
311
|
+
autoOpenAddProvider={settings.autoOpenAddProvider}
|
|
312
|
+
appSettings={appSettings}
|
|
327
313
|
/>
|
|
328
314
|
|
|
329
315
|
{/* The navbar title menu's delete path and the rename-with-AI refusal
|
|
@@ -212,11 +212,14 @@ export function Panel(props: {
|
|
|
212
212
|
|
|
213
213
|
const baseStyle: CSSProperties = filler
|
|
214
214
|
? {
|
|
215
|
-
|
|
215
|
+
// A collapsed filler keeps its DOM (and therefore its state: the chat
|
|
216
|
+
// owns the live stream and the composer) but takes no width, so the
|
|
217
|
+
// sibling panels' own widths are the whole row.
|
|
218
|
+
flex: collapsed ? '0 0 0%' : '1 1 0%',
|
|
216
219
|
// The filler keeps its floor on the group's axis (and none across it,
|
|
217
220
|
// where it is sized by the container): see the group model note above.
|
|
218
|
-
minWidth: orientation === 'horizontal' ? minPx : 0,
|
|
219
|
-
minHeight: orientation === 'horizontal' ? 0 : minPx,
|
|
221
|
+
minWidth: orientation === 'horizontal' ? (collapsed ? 0 : minPx) : 0,
|
|
222
|
+
minHeight: orientation === 'horizontal' ? 0 : collapsed ? 0 : minPx,
|
|
220
223
|
overflow: 'hidden',
|
|
221
224
|
position: 'relative',
|
|
222
225
|
}
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
* props; this file only decides how the two states look.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { useState } from 'preact/hooks';
|
|
17
18
|
import { ChevronDown, ChevronRight, MoreHorizontal, Pin, Plus } from 'lucide-preact';
|
|
19
|
+
import { NewWorktreeModal } from '@/client/components/workspace/worktree-modal/index';
|
|
18
20
|
import { SelectionCheckbox } from '@/client/components/common/batch-selection/SelectionCheckbox';
|
|
19
21
|
import { WorkspaceOptionsMenu } from '@/client/components/common/workspace-options-menu';
|
|
20
22
|
import { getProjectIcon } from '@/shared/lib/workspace/project-icon';
|
|
@@ -39,6 +41,8 @@ export interface CategoryHeaderProps {
|
|
|
39
41
|
showMenu: boolean;
|
|
40
42
|
onToggleMenu: () => void;
|
|
41
43
|
onNewSession: () => void;
|
|
44
|
+
/** Start a chat in a worktree made from this folder. */
|
|
45
|
+
onNewWorktreeSession?: (folderId: number, worktreePath: string) => void;
|
|
42
46
|
onOpenFolder?: () => void;
|
|
43
47
|
onSelectSessions?: () => void;
|
|
44
48
|
confirmDelete: boolean;
|
|
@@ -58,6 +62,7 @@ export function CategoryHeader({
|
|
|
58
62
|
showMenu,
|
|
59
63
|
onToggleMenu,
|
|
60
64
|
onNewSession,
|
|
65
|
+
onNewWorktreeSession,
|
|
61
66
|
onOpenFolder,
|
|
62
67
|
onSelectSessions,
|
|
63
68
|
confirmDelete,
|
|
@@ -67,6 +72,10 @@ export function CategoryHeader({
|
|
|
67
72
|
onCancelDelete,
|
|
68
73
|
}: CategoryHeaderProps) {
|
|
69
74
|
const ProjectIcon = getProjectIcon(folder.icon);
|
|
75
|
+
// The dialog lives here rather than in the page: the row that opens it is this
|
|
76
|
+
// one, and the folder it belongs to is what the dialog needs.
|
|
77
|
+
const [worktreeDialogOpen, setWorktreeDialogOpen] = useState(false);
|
|
78
|
+
const canCreateWorktree = Boolean(onNewWorktreeSession && folder.project_path);
|
|
70
79
|
|
|
71
80
|
return (
|
|
72
81
|
<div className="group flex items-center justify-between h-7 text-xs font-semibold text-ink px-2 hover:bg-ink/5 rounded-md transition-colors select-none">
|
|
@@ -159,6 +168,7 @@ export function CategoryHeader({
|
|
|
159
168
|
isPinned={folder.isPinned}
|
|
160
169
|
onOpenFolder={onOpenFolder}
|
|
161
170
|
onSelectSessions={onSelectSessions}
|
|
171
|
+
onNewWorktree={canCreateWorktree ? () => { onToggleMenu(); setWorktreeDialogOpen(true); } : undefined}
|
|
162
172
|
confirmDelete={confirmDelete}
|
|
163
173
|
onPin={onPin}
|
|
164
174
|
onDelete={onDelete}
|
|
@@ -170,6 +180,20 @@ export function CategoryHeader({
|
|
|
170
180
|
</div>
|
|
171
181
|
</div>
|
|
172
182
|
)}
|
|
183
|
+
|
|
184
|
+
{canCreateWorktree && (
|
|
185
|
+
<NewWorktreeModal
|
|
186
|
+
isOpen={worktreeDialogOpen}
|
|
187
|
+
onClose={() => setWorktreeDialogOpen(false)}
|
|
188
|
+
root={folder.project_path ?? null}
|
|
189
|
+
folderId={folder.id}
|
|
190
|
+
folderName={folder.name}
|
|
191
|
+
onCreated={(created) => {
|
|
192
|
+
setWorktreeDialogOpen(false);
|
|
193
|
+
onNewWorktreeSession?.(folder.id, created.path);
|
|
194
|
+
}}
|
|
195
|
+
/>
|
|
196
|
+
)}
|
|
173
197
|
</div>
|
|
174
198
|
);
|
|
175
199
|
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The folder header's `New worktree…` entry.
|
|
8
|
+
*
|
|
9
|
+
* This row is the ONLY entry point to the dialog on the desktop, and it is
|
|
10
|
+
* reachable through four components (SessionsView → SessionList → Category →
|
|
11
|
+
* CategoryHeader). The prop is optional at every hop, so a hop that forgets to
|
|
12
|
+
* forward it fails silently: the typechecker is satisfied, the menu renders, and
|
|
13
|
+
* the row is simply absent. Measured — that is exactly how the desktop half
|
|
14
|
+
* shipped unreachable while the phone's worked.
|
|
15
|
+
*
|
|
16
|
+
* The header is mounted directly here, which is the half that can be asserted
|
|
17
|
+
* cheaply; the assembled path is what `tests/e2e/specs/worktree.e2e.ts` covers.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'bun:test';
|
|
21
|
+
import { Window } from 'happy-dom';
|
|
22
|
+
import { h, render } from 'preact';
|
|
23
|
+
import { act } from 'preact/test-utils';
|
|
24
|
+
import { CategoryHeader } from '@/client/components/layout/session-sidebar/CategoryHeader';
|
|
25
|
+
import { installDomGlobals, restoreDomGlobals } from '@/test-support/pristine-globals';
|
|
26
|
+
import type { WorkspaceFolderData } from '@/shared/types';
|
|
27
|
+
|
|
28
|
+
const folder: WorkspaceFolderData = {
|
|
29
|
+
id: 7,
|
|
30
|
+
name: 'kabmo',
|
|
31
|
+
project_path: '/Users/dev/kabmo',
|
|
32
|
+
isExpanded: true,
|
|
33
|
+
hasMore: false,
|
|
34
|
+
totalSessions: 0,
|
|
35
|
+
sessions: [],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
let host: HTMLElement | null = null;
|
|
39
|
+
|
|
40
|
+
beforeAll(() => {
|
|
41
|
+
installDomGlobals(new Window({ url: 'http://localhost' }));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
if (host) {
|
|
46
|
+
render(null, host);
|
|
47
|
+
host.remove();
|
|
48
|
+
host = null;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterAll(() => {
|
|
53
|
+
restoreDomGlobals();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/** Mount the header with its `⋯` menu already open. */
|
|
57
|
+
function mountHeader(overrides: Partial<Parameters<typeof CategoryHeader>[0]> = {}): HTMLElement {
|
|
58
|
+
host = document.createElement('div');
|
|
59
|
+
document.body.appendChild(host);
|
|
60
|
+
render(
|
|
61
|
+
h(CategoryHeader, {
|
|
62
|
+
folder,
|
|
63
|
+
isOpen: true,
|
|
64
|
+
onToggleFolder: () => {},
|
|
65
|
+
checkbox: null,
|
|
66
|
+
batchActive: false,
|
|
67
|
+
menuRef: { current: null },
|
|
68
|
+
showMenu: true,
|
|
69
|
+
onToggleMenu: () => {},
|
|
70
|
+
onNewSession: () => {},
|
|
71
|
+
confirmDelete: false,
|
|
72
|
+
onPin: () => {},
|
|
73
|
+
onDelete: () => {},
|
|
74
|
+
onRequestDelete: () => {},
|
|
75
|
+
onCancelDelete: () => {},
|
|
76
|
+
...overrides,
|
|
77
|
+
}),
|
|
78
|
+
host,
|
|
79
|
+
);
|
|
80
|
+
return host;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The menu's own buttons, by their words. */
|
|
84
|
+
function menuRows(container: HTMLElement): string[] {
|
|
85
|
+
return [...container.querySelectorAll('button')].map((button) => (button.textContent ?? '').trim()).filter(Boolean);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
describe('CategoryHeader worktree entry', () => {
|
|
89
|
+
test('the menu offers New worktree… when a handler is supplied', () => {
|
|
90
|
+
const container = mountHeader({ onNewWorktreeSession: () => {} });
|
|
91
|
+
expect(menuRows(container)).toContain('New worktree…');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('no row without a handler — a folder that cannot create one does not offer it', () => {
|
|
95
|
+
const container = mountHeader();
|
|
96
|
+
expect(menuRows(container).some((row) => row.startsWith('New worktree'))).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('a folder with no project path offers nothing: a worktree needs a repository', () => {
|
|
100
|
+
const container = mountHeader({
|
|
101
|
+
folder: { ...folder, project_path: null },
|
|
102
|
+
onNewWorktreeSession: () => {},
|
|
103
|
+
});
|
|
104
|
+
expect(menuRows(container).some((row) => row.startsWith('New worktree'))).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('clicking it opens the dialog, closed on its own name field', async () => {
|
|
108
|
+
const container = mountHeader({ onNewWorktreeSession: () => {} });
|
|
109
|
+
const row = [...container.querySelectorAll('button')].find(
|
|
110
|
+
(button) => (button.textContent ?? '').trim() === 'New worktree…',
|
|
111
|
+
);
|
|
112
|
+
if (!row) throw new Error('no New worktree row rendered');
|
|
113
|
+
|
|
114
|
+
await act(async () => {
|
|
115
|
+
row.click();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// The dialog names the project it will branch from, and starts on a
|
|
119
|
+
// generated branch with its folder following it.
|
|
120
|
+
const text = (document.body.textContent ?? '').replace(/\s+/g, ' ');
|
|
121
|
+
expect(text).toContain('Start from');
|
|
122
|
+
expect(text).toContain('kabmo');
|
|
123
|
+
const branch = (document.body.querySelector('input') as HTMLInputElement | null)?.value ?? '';
|
|
124
|
+
expect(branch).toMatch(/^wt\/[a-z]+-[a-z]+$/);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -24,6 +24,7 @@ export function Category({
|
|
|
24
24
|
activeSessionId,
|
|
25
25
|
onSelectSession,
|
|
26
26
|
onNewSessionForFolder,
|
|
27
|
+
onNewWorktreeSession,
|
|
27
28
|
forceExpanded = false,
|
|
28
29
|
showArchived = false,
|
|
29
30
|
sessionStatus = {},
|
|
@@ -36,6 +37,7 @@ export function Category({
|
|
|
36
37
|
activeSessionId: number | string | null;
|
|
37
38
|
onSelectSession: (id: number | string) => void;
|
|
38
39
|
onNewSessionForFolder: (id: number) => void;
|
|
40
|
+
onNewWorktreeSession?: (folderId: number, worktreePath: string) => void;
|
|
39
41
|
forceExpanded?: boolean;
|
|
40
42
|
showArchived?: boolean;
|
|
41
43
|
sessionStatus?: Record<string, 'stream' | 'finish' | 'abort'>;
|
|
@@ -172,6 +174,7 @@ export function Category({
|
|
|
172
174
|
showMenu={showMenu}
|
|
173
175
|
onToggleMenu={() => setShowMenu(!showMenu)}
|
|
174
176
|
onNewSession={() => onNewSessionForFolder(folder.id)}
|
|
177
|
+
onNewWorktreeSession={onNewWorktreeSession}
|
|
175
178
|
onOpenFolder={onOpenFolder}
|
|
176
179
|
onSelectSessions={
|
|
177
180
|
onStartSessionSelection
|
|
@@ -211,6 +214,7 @@ export function Category({
|
|
|
211
214
|
isArchived={session.is_archived === 1}
|
|
212
215
|
status={sessionStatus[sessionKey]}
|
|
213
216
|
awaitingInput={Boolean(session.awaitingInput)}
|
|
217
|
+
worktree={session.worktree}
|
|
214
218
|
timeAgo={relativeTimeAgo(session.updated_at ?? session.created_at)}
|
|
215
219
|
onClick={() => onSelectSession(session.id)}
|
|
216
220
|
onArchive={() => handleArchive(session)}
|