@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
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { describe, expect, test } from 'bun:test';
|
|
7
|
-
import { isVirtualPath, stripReadSelector } from '@/shared/lib/fs/paths';
|
|
7
|
+
import { isVirtualPath, lastPathSegment, stripReadSelector } from '@/shared/lib/fs/paths';
|
|
8
8
|
|
|
9
9
|
describe('stripReadSelector', () => {
|
|
10
10
|
test('peels every selector shape omp accepts', () => {
|
|
@@ -88,3 +88,26 @@ describe('isVirtualPath', () => {
|
|
|
88
88
|
expect(isVirtualPath('')).toBe(false);
|
|
89
89
|
});
|
|
90
90
|
});
|
|
91
|
+
|
|
92
|
+
describe('lastPathSegment', () => {
|
|
93
|
+
test('names the last segment on either separator', () => {
|
|
94
|
+
// The Windows case is the one that shipped broken: the composer's target
|
|
95
|
+
// trigger read `kabmo · C:\Users\…\cosmic-dolphin`, because `split('/')`
|
|
96
|
+
// has nothing to cut in a `\`-separated absolute path.
|
|
97
|
+
expect(lastPathSegment('/data/worktrees/abc/cosmic-dolphin')).toBe('cosmic-dolphin');
|
|
98
|
+
expect(lastPathSegment('C:\\Users\\u\\AppData\\Local\\worktrees\\abc\\cosmic-dolphin')).toBe('cosmic-dolphin');
|
|
99
|
+
expect(lastPathSegment('wt/nested/branch')).toBe('branch');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('a trailing separator names the same thing', () => {
|
|
103
|
+
expect(lastPathSegment('/data/worktrees/abc/cosmic-dolphin/')).toBe('cosmic-dolphin');
|
|
104
|
+
expect(lastPathSegment('C:\\worktrees\\abc\\cosmic-dolphin\\')).toBe('cosmic-dolphin');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('nothing to name is empty, not a stray separator', () => {
|
|
108
|
+
expect(lastPathSegment(undefined)).toBe('');
|
|
109
|
+
expect(lastPathSegment(null)).toBe('');
|
|
110
|
+
expect(lastPathSegment('')).toBe('');
|
|
111
|
+
expect(lastPathSegment('/')).toBe('');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -107,3 +107,20 @@ export function toAbsolutePath(baseDir: string | null | undefined, relPath: stri
|
|
|
107
107
|
// Join with the base's own separator: `C:\proj` + `src` → `C:\proj\src`.
|
|
108
108
|
return `${base}${base.includes('\\') && !base.includes('/') ? '\\' : '/'}${rel}`;
|
|
109
109
|
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The last segment of a path on ANY platform.
|
|
113
|
+
*
|
|
114
|
+
* `split('/').pop()` is the shape this replaces, and it is wrong for every
|
|
115
|
+
* ABSOLUTE path on Windows: a worktree the server created under
|
|
116
|
+
* `C:\…\worktrees\<id>\cosmic-dolphin` came back whole, so the composer's target
|
|
117
|
+
* trigger read `kabmo · C:\Users\…\cosmic-dolphin` instead of
|
|
118
|
+
* `kabmo · cosmic-dolphin` (measured on the Windows E2E leg, which is the only
|
|
119
|
+
* place it shows — every POSIX path splits the same either way). A trailing
|
|
120
|
+
* separator is dropped first so `…/cosmic-dolphin/` names the same worktree.
|
|
121
|
+
*/
|
|
122
|
+
export function lastPathSegment(target: string | null | undefined): string {
|
|
123
|
+
if (!target) return '';
|
|
124
|
+
const parts = target.replace(/[/\\]+$/, '').split(/[/\\]/);
|
|
125
|
+
return parts[parts.length - 1] ?? '';
|
|
126
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The two worktree setting keys, in a module with NO server imports.
|
|
8
|
+
*
|
|
9
|
+
* The server reads them from `kabmo_settings` (`lib/fs/worktree/settings.server.ts`)
|
|
10
|
+
* and the settings panel writes them through `writeSetting`; both sides need the
|
|
11
|
+
* same strings, and the client bundle must not pull in `@/db.server` to get them.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Parent directory new worktrees are created in; empty means the default. */
|
|
15
|
+
export const WORKTREE_DIRECTORY_SETTING = 'worktreeDirectory';
|
|
16
|
+
|
|
17
|
+
/** Whether a create copies the project's ignored build folders. Default: on. */
|
|
18
|
+
export const WORKTREE_CARRY_SETTING = 'worktreeCarryIgnored';
|
|
@@ -32,7 +32,14 @@ export function spawnCwdForNewSession(
|
|
|
32
32
|
folders: PrewarmFolderShape[],
|
|
33
33
|
activeSessionId: string | number | null,
|
|
34
34
|
folderId?: number | string,
|
|
35
|
+
/**
|
|
36
|
+
* A worktree the draft chose. It wins over the folder's project path, because
|
|
37
|
+
* that is the whole point of picking one: the chat is born in the worktree and
|
|
38
|
+
* nothing is ever relocated afterwards.
|
|
39
|
+
*/
|
|
40
|
+
worktreePath?: string | null,
|
|
35
41
|
): string | null {
|
|
42
|
+
if (worktreePath) return worktreePath;
|
|
36
43
|
const target = folderId !== undefined
|
|
37
44
|
? folders.find((f) => String(f.id) === String(folderId))
|
|
38
45
|
: folders.find((f) => f.sessions?.some((s) => String(s.id) === String(activeSessionId)))
|
|
@@ -45,6 +45,22 @@ export const TOPIC_SIDEBAR = 'sidebar';
|
|
|
45
45
|
* the structure needs a JSONL scan to produce. */
|
|
46
46
|
export const TOPIC_SIDEBAR_STATUS = 'sidebar:status';
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* The last-activity stamp of each session with a run IN FLIGHT, keyed by id.
|
|
50
|
+
*
|
|
51
|
+
* Split from `sidebar` for the same reason `sidebar:status` is, but the cost
|
|
52
|
+
* difference is far larger: a transcript grows on every tool call, so this
|
|
53
|
+
* changes per tool call while the structure is a 216 KB payload over 731
|
|
54
|
+
* sessions (measured on the author's install) that must be re-scanned to
|
|
55
|
+
* produce. Only a RUNNING session's stamp can move between turn boundaries, so
|
|
56
|
+
* only those are carried — the payload is one ISO string per live run
|
|
57
|
+
* (measured: 87 bytes for one), against 216 KB for the whole list.
|
|
58
|
+
*
|
|
59
|
+
* The structure remains the authority for every other row; this only ever
|
|
60
|
+
* ADVANCES a stamp the structure already carries (see `applySidebarActivity`).
|
|
61
|
+
*/
|
|
62
|
+
export const TOPIC_SIDEBAR_ACTIVITY = 'sidebar:activity';
|
|
63
|
+
|
|
48
64
|
/** One session's live conversation: agent frames, queue and modes. */
|
|
49
65
|
export function sessionTopic(sessionId: string): string {
|
|
50
66
|
return `session:${sessionId}`;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The `⋯` rule, asserted as arithmetic.
|
|
8
|
+
*
|
|
9
|
+
* Each case is a failure the toolbars had: a row that wrapped into two rows
|
|
10
|
+
* instead of hiding a control, a control hidden while the width was still
|
|
11
|
+
* unknown (a menu flashing on every mount), and a pinned control buried in the
|
|
12
|
+
* menu because the row happened to be narrow.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, expect, test } from 'bun:test';
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
TOOLBAR_ITEM_GAP_PX,
|
|
19
|
+
TOOLBAR_OVERFLOW_TRIGGER_PX,
|
|
20
|
+
toolbarHasOverflow,
|
|
21
|
+
toolbarVisibleClusters,
|
|
22
|
+
} from '@/shared/lib/ui/toolbar-overflow';
|
|
23
|
+
|
|
24
|
+
/** Save, find, wrap, zoom, copy, modes, full screen — the editor's real row. */
|
|
25
|
+
const widths = [26, 26, 26, 52, 52, 52, 26];
|
|
26
|
+
const pinned = [true, false, false, false, false, true, true];
|
|
27
|
+
|
|
28
|
+
/** Row width needed to draw `visible` plus the trigger when anything is hidden. */
|
|
29
|
+
function rowWidth(visible: boolean[]): number {
|
|
30
|
+
const shown = widths.filter((_, index) => visible[index]);
|
|
31
|
+
const overflow = toolbarHasOverflow(visible);
|
|
32
|
+
return (
|
|
33
|
+
shown.reduce((sum, width) => sum + width, 0) +
|
|
34
|
+
TOOLBAR_ITEM_GAP_PX * Math.max(0, shown.length - 1) +
|
|
35
|
+
(overflow ? TOOLBAR_OVERFLOW_TRIGGER_PX + TOOLBAR_ITEM_GAP_PX : 0)
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('toolbarVisibleClusters', () => {
|
|
40
|
+
test('an unmeasured row shows every cluster', () => {
|
|
41
|
+
// Not "hide everything": the row is measured a layout effect later, and a
|
|
42
|
+
// menu that flashes on every mount is worse than one frame of a full row.
|
|
43
|
+
expect(toolbarVisibleClusters({ widths, pinned, available: 0 }).every(Boolean)).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('a row wide enough for everything shows everything, with no trigger', () => {
|
|
47
|
+
const total = widths.reduce((sum, width) => sum + width, 0) + TOOLBAR_ITEM_GAP_PX * (widths.length - 1);
|
|
48
|
+
const visible = toolbarVisibleClusters({ widths, pinned, available: total });
|
|
49
|
+
expect(visible.every(Boolean)).toBe(true);
|
|
50
|
+
expect(toolbarHasOverflow(visible)).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('a narrow row hides what does not fit and reserves the trigger', () => {
|
|
54
|
+
// 200px of row: the three pinned clusters (26 + 52 + 26) plus their gaps
|
|
55
|
+
// leave room for one unpinned control and the `⋯` trigger, and no more.
|
|
56
|
+
const visible = toolbarVisibleClusters({ widths, pinned, available: 200 });
|
|
57
|
+
expect(toolbarHasOverflow(visible)).toBe(true);
|
|
58
|
+
// The promise the arithmetic has to keep: what stays plus the trigger fits.
|
|
59
|
+
expect(rowWidth(visible)).toBeLessThanOrEqual(200);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('a pinned cluster stays on the row at any width', () => {
|
|
63
|
+
// A 120px row fits one 26px control plus the trigger, and the pinned width
|
|
64
|
+
// modes (52px) would not fit at all on their own — they stay regardless,
|
|
65
|
+
// because a menu-only mode button is a mode the reader never finds.
|
|
66
|
+
const visible = toolbarVisibleClusters({ widths, pinned, available: 120 });
|
|
67
|
+
expect(visible[0]).toBe(true);
|
|
68
|
+
expect(visible[5]).toBe(true);
|
|
69
|
+
expect(visible[6]).toBe(true);
|
|
70
|
+
// The unpinned controls are what gives way.
|
|
71
|
+
expect(visible[1]).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('order is preserved: what stays is a subsequence, not a re-sort', () => {
|
|
75
|
+
const visible = toolbarVisibleClusters({ widths, pinned, available: 200 });
|
|
76
|
+
const shownIndexes = visible.flatMap((isShown, index) => (isShown ? [index] : []));
|
|
77
|
+
expect([...shownIndexes].sort((a, b) => a - b)).toEqual(shownIndexes);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('no clusters is no row', () => {
|
|
81
|
+
expect(toolbarVisibleClusters({ widths: [], pinned: [], available: 500 })).toEqual([]);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Which toolbar clusters fit on ONE row, and which go into the `⋯` menu.
|
|
8
|
+
*
|
|
9
|
+
* The editor and diff toolbars carry ten-plus controls and the editor panel is
|
|
10
|
+
* 320px wide on a 1440px screen. WRAPPING was the previous answer and it is the
|
|
11
|
+
* wrong one: a second row moves the document down by the toolbar's own height
|
|
12
|
+
* and reads as a second toolbar. A horizontal scroll is worse still — a clipped
|
|
13
|
+
* control is invisible and reads as "there is nothing more here".
|
|
14
|
+
*
|
|
15
|
+
* So: the clusters that do not fit move into a `⋯` menu, and a cluster can be
|
|
16
|
+
* PINNED to stay on the row at every width. Pinning is what keeps this from
|
|
17
|
+
* being a priority guess — the toolbar's visual order is not its priority order
|
|
18
|
+
* (Save is drawn first because that is where a reader looks for it, while the
|
|
19
|
+
* width modes sit last and are the reason the panel exists).
|
|
20
|
+
*
|
|
21
|
+
* Pure, and measured widths in: the caller lays the clusters out once and hands
|
|
22
|
+
* their rendered widths here. A table of guessed pixel widths drifts the moment
|
|
23
|
+
* a label, an icon or a font changes.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** Width the `⋯` trigger needs, including the gap before it. */
|
|
27
|
+
export const TOOLBAR_OVERFLOW_TRIGGER_PX = 30;
|
|
28
|
+
|
|
29
|
+
/** Gap between two clusters (`gap-x-1`). */
|
|
30
|
+
export const TOOLBAR_ITEM_GAP_PX = 4;
|
|
31
|
+
|
|
32
|
+
export interface ToolbarOverflowArgs {
|
|
33
|
+
/** Rendered width of each cluster, in row order. */
|
|
34
|
+
widths: number[];
|
|
35
|
+
/** Which clusters never leave the row. Same length as `widths`. */
|
|
36
|
+
pinned: boolean[];
|
|
37
|
+
/** Width the row actually has, or 0 before it is measured. */
|
|
38
|
+
available: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Which clusters to render on the row, in row order.
|
|
43
|
+
*
|
|
44
|
+
* An unmeasured row (available 0) keeps everything: hiding controls before the
|
|
45
|
+
* width is known flashes a menu on every mount, and the row is measured a
|
|
46
|
+
* layout effect later.
|
|
47
|
+
*/
|
|
48
|
+
export function toolbarVisibleClusters({ widths, pinned, available }: ToolbarOverflowArgs): boolean[] {
|
|
49
|
+
if (!(available > 0) || widths.length === 0) return widths.map(() => true);
|
|
50
|
+
|
|
51
|
+
const total = widths.reduce((sum, width) => sum + width, 0) + TOOLBAR_ITEM_GAP_PX * (widths.length - 1);
|
|
52
|
+
if (total <= available) return widths.map(() => true);
|
|
53
|
+
|
|
54
|
+
// Pinned clusters are placed FIRST and in full, because whether the row is
|
|
55
|
+
// wide enough for them is not a question — they stay and the row clips if it
|
|
56
|
+
// must. The unpinned ones then take whatever is left, in row order, once the
|
|
57
|
+
// `⋯` trigger's own width is set aside. Filling in one pass over row order
|
|
58
|
+
// instead would let an early unpinned control consume the space a pinned one
|
|
59
|
+
// later needs.
|
|
60
|
+
const shown = widths.map(() => false);
|
|
61
|
+
let used = 0;
|
|
62
|
+
let rendered = 0;
|
|
63
|
+
widths.forEach((width, index) => {
|
|
64
|
+
if (!pinned[index]) return;
|
|
65
|
+
used += width + (rendered > 0 ? TOOLBAR_ITEM_GAP_PX : 0);
|
|
66
|
+
rendered += 1;
|
|
67
|
+
shown[index] = true;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
let budget = available - used - TOOLBAR_OVERFLOW_TRIGGER_PX - TOOLBAR_ITEM_GAP_PX;
|
|
71
|
+
widths.forEach((width, index) => {
|
|
72
|
+
if (pinned[index]) return;
|
|
73
|
+
const cost = width + (rendered > 0 ? TOOLBAR_ITEM_GAP_PX : 0);
|
|
74
|
+
if (cost > budget) return;
|
|
75
|
+
budget -= cost;
|
|
76
|
+
rendered += 1;
|
|
77
|
+
shown[index] = true;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return shown;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** True when at least one cluster is going into the menu. */
|
|
84
|
+
export function toolbarHasOverflow(visible: boolean[]): boolean {
|
|
85
|
+
return visible.some((shown) => !shown);
|
|
86
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The architecture a DESKTOP shell runs as, and the artifact it installs itself
|
|
8
|
+
* from.
|
|
9
|
+
*
|
|
10
|
+
* ## Why this exists at all
|
|
11
|
+
*
|
|
12
|
+
* Every desktop artifact is per-architecture (`Kabmo-0.2.0-mac-arm64.zip`,
|
|
13
|
+
* `Kabmo-0.2.0-win-x64-setup.exe`), and the console used to answer a desktop
|
|
14
|
+
* probe with the RELEASE PAGE precisely because it could not know which
|
|
15
|
+
* architecture the window asking was running on. That is correct for a link and
|
|
16
|
+
* useless for a self-update: handing an arm64 reader an x64 installer produces
|
|
17
|
+
* an app that does not launch, and on Windows an installer that refuses with
|
|
18
|
+
* "this installation package could not be opened".
|
|
19
|
+
*
|
|
20
|
+
* So a desktop shell reports its own architecture over the bridge, and the
|
|
21
|
+
* server names the ONE artifact. This module is the vocabulary both ends share,
|
|
22
|
+
* which is what stops the shell, the route and the asset table from spelling
|
|
23
|
+
* `x86_64`, `x64` and `amd64` for the same machine.
|
|
24
|
+
*
|
|
25
|
+
* ## Two spellings, one canonical token
|
|
26
|
+
*
|
|
27
|
+
* A shell reports the CANONICAL token (`arm64` / `x64`) because that is what the
|
|
28
|
+
* client and the route pass around. Each platform's release assets spell its
|
|
29
|
+
* architecture differently — Linux says `x86_64`, macOS and Windows say `x64` —
|
|
30
|
+
* and that mapping lives in ONE table (`SHELL_ASSETS`), on the server, next to
|
|
31
|
+
* the names it has to match. A shell that mapped its own spelling to the asset's
|
|
32
|
+
* would be a second copy of the same fact.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/** A desktop architecture, in the vocabulary the console and the shells share. */
|
|
36
|
+
export type ShellArch = 'arm64' | 'x64';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Read a canonical arch out of a string, or null.
|
|
40
|
+
*
|
|
41
|
+
* Lenient about the spellings a shell might report (a shell that read
|
|
42
|
+
* `std::env::consts::ARCH` says `x86_64`; one that read
|
|
43
|
+
* `RuntimeInformation.ProcessArchitecture` says `X64`), because a refused arch
|
|
44
|
+
* costs the reader the self-update and buys nothing: the value only chooses
|
|
45
|
+
* between two artifacts of the same product, and the artifact is then verified
|
|
46
|
+
* by the shell before it is installed.
|
|
47
|
+
*/
|
|
48
|
+
export function parseShellArch(value: string | null | undefined): ShellArch | null {
|
|
49
|
+
if (!value) return null;
|
|
50
|
+
const folded = value.trim().toLowerCase();
|
|
51
|
+
if (folded === 'arm64' || folded === 'aarch64') return 'arm64';
|
|
52
|
+
if (folded === 'x64' || folded === 'x86_64' || folded === 'amd64') return 'x64';
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
@@ -19,14 +19,34 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
/** The native shells that report themselves on the UA. */
|
|
22
|
-
export type
|
|
22
|
+
export type MobileShellKind = 'android' | 'ios';
|
|
23
23
|
|
|
24
|
-
/**
|
|
25
|
-
|
|
24
|
+
/**
|
|
25
|
+
* The desktop shells. They carry NO User-Agent token — a desktop app is not
|
|
26
|
+
* downloaded from a page, so the server has nothing to parse per request. They
|
|
27
|
+
* report their version and their ARCHITECTURE over the shell bridge instead
|
|
28
|
+
* (`__kabmoDesktop`), which is why they are a separate family: both facts exist,
|
|
29
|
+
* the UA channel does not.
|
|
30
|
+
*/
|
|
31
|
+
export type DesktopShellKind = 'macos' | 'windows' | 'linux';
|
|
32
|
+
|
|
33
|
+
/** Every shell Kabmo publishes an artifact for. */
|
|
34
|
+
export type ShellKind = MobileShellKind | DesktopShellKind;
|
|
35
|
+
|
|
36
|
+
/** The token's name, before the slash, per MOBILE shell. */
|
|
37
|
+
export const SHELL_UA_PREFIXES: Record<MobileShellKind, string> = {
|
|
26
38
|
android: 'KabmoAndroid/',
|
|
27
39
|
ios: 'KabmoIOS/',
|
|
28
40
|
};
|
|
29
41
|
|
|
42
|
+
/** The desktop family, in the order their release tracks were added. */
|
|
43
|
+
export const DESKTOP_SHELL_KINDS: readonly DesktopShellKind[] = ['macos', 'windows', 'linux'];
|
|
44
|
+
|
|
45
|
+
/** Whether a shell reports itself over the bridge rather than the User-Agent. */
|
|
46
|
+
export function isDesktopShellKind(kind: string): kind is DesktopShellKind {
|
|
47
|
+
return (DESKTOP_SHELL_KINDS as readonly string[]).includes(kind);
|
|
48
|
+
}
|
|
49
|
+
|
|
30
50
|
/**
|
|
31
51
|
* Whether this request came from an Android device at all — the app or a
|
|
32
52
|
* browser on it.
|
|
@@ -60,7 +80,7 @@ export function isAndroidDevice(userAgent: string | null | undefined): boolean {
|
|
|
60
80
|
*/
|
|
61
81
|
export function shellVersion(
|
|
62
82
|
userAgent: string | null | undefined,
|
|
63
|
-
kind:
|
|
83
|
+
kind: MobileShellKind,
|
|
64
84
|
): string | null {
|
|
65
85
|
if (!userAgent) return null;
|
|
66
86
|
|
|
@@ -23,6 +23,29 @@ export function activeProjectForSession(
|
|
|
23
23
|
return { folder: null, projectPath: null };
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* The root the right-panel tools should operate on for this session: its own
|
|
28
|
+
* worktree when the session's cwd is a linked checkout, else the folder's
|
|
29
|
+
* registered project path.
|
|
30
|
+
*
|
|
31
|
+
* A worktree session runs OUTSIDE its folder's project directory, so pointing
|
|
32
|
+
* the panels at `project_path` showed the checkout the agent had left — measured:
|
|
33
|
+
* every panel kept reading the main tree while the run wrote to the worktree.
|
|
34
|
+
* The server's `resolveRoot` accepts a worktree of a registered project for the
|
|
35
|
+
* same reason, so the two agree about what is readable.
|
|
36
|
+
*/
|
|
37
|
+
export function effectiveRootFor(
|
|
38
|
+
folders: WorkspaceFolderData[],
|
|
39
|
+
sessionId: string | number | null | undefined,
|
|
40
|
+
): string | null {
|
|
41
|
+
const { folder } = activeProjectForSession(folders, sessionId);
|
|
42
|
+
if (!folder) return null;
|
|
43
|
+
const row = (folder.sessions || []).find((session) => String(session.id) === String(sessionId));
|
|
44
|
+
const worktree = row?.worktree;
|
|
45
|
+
if (worktree && worktree.status === 'ready') return worktree.path;
|
|
46
|
+
return folder.project_path ?? null;
|
|
47
|
+
}
|
|
48
|
+
|
|
26
49
|
/**
|
|
27
50
|
* Resolve the workspace root the composer should scope `@` file mentions to:
|
|
28
51
|
* the active session's bound project first, then the explicitly selected
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The three width modes' table, asserted as data.
|
|
8
|
+
*
|
|
9
|
+
* Each case is a rule the layout depends on and that no component test would
|
|
10
|
+
* catch: `compact` must NOT hide the sidebar (that is `full`'s job, and the
|
|
11
|
+
* two buttons would be the same button if it did), the right panel is hidden
|
|
12
|
+
* by neither, and clicking an active mode returns to `normal` rather than
|
|
13
|
+
* doing nothing.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, expect, test } from 'bun:test';
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
DEFAULT_EDITOR_VIEW_MODE,
|
|
20
|
+
isEditorViewMode,
|
|
21
|
+
editorViewHides,
|
|
22
|
+
toggledEditorViewMode,
|
|
23
|
+
} from '@/shared/lib/workspace/editor-view-mode';
|
|
24
|
+
|
|
25
|
+
describe('editorViewHides', () => {
|
|
26
|
+
test('compact hides the chat only', () => {
|
|
27
|
+
expect(editorViewHides('compact', 'chat')).toBe(true);
|
|
28
|
+
expect(editorViewHides('compact', 'sidebar')).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('full hides the chat AND the sidebar', () => {
|
|
32
|
+
expect(editorViewHides('full', 'chat')).toBe(true);
|
|
33
|
+
expect(editorViewHides('full', 'sidebar')).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('normal hides nothing', () => {
|
|
37
|
+
expect(editorViewHides('normal', 'chat')).toBe(false);
|
|
38
|
+
expect(editorViewHides('normal', 'sidebar')).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('toggledEditorViewMode', () => {
|
|
43
|
+
test('a mode button enters its mode', () => {
|
|
44
|
+
expect(toggledEditorViewMode('normal', 'compact')).toBe('compact');
|
|
45
|
+
expect(toggledEditorViewMode('normal', 'full')).toBe('full');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('the OTHER mode switches rather than toggling off', () => {
|
|
49
|
+
expect(toggledEditorViewMode('compact', 'full')).toBe('full');
|
|
50
|
+
expect(toggledEditorViewMode('full', 'compact')).toBe('compact');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('clicking the active mode returns to normal', () => {
|
|
54
|
+
expect(toggledEditorViewMode('compact', 'compact')).toBe('normal');
|
|
55
|
+
expect(toggledEditorViewMode('full', 'full')).toBe('normal');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('isEditorViewMode', () => {
|
|
60
|
+
test('accepts the three modes and nothing else', () => {
|
|
61
|
+
expect(['normal', 'compact', 'full'].every(isEditorViewMode)).toBe(true);
|
|
62
|
+
expect(isEditorViewMode('fullscreen')).toBe(false);
|
|
63
|
+
expect(isEditorViewMode(undefined)).toBe(false);
|
|
64
|
+
expect(isEditorViewMode(null)).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('the fallback is the ordinary layout', () => {
|
|
68
|
+
expect(DEFAULT_EDITOR_VIEW_MODE).toBe('normal');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* How wide the editor column is allowed to be, as ONE of three modes.
|
|
8
|
+
*
|
|
9
|
+
* `compact` and `full` both hide the chat timeline and let the editor take the
|
|
10
|
+
* width the chat had; `full` also hides the session sidebar. Neither touches
|
|
11
|
+
* the right panel: the point of the two modes is a wider DOCUMENT, and the
|
|
12
|
+
* right column is where the outline, search hits and the diff list live while
|
|
13
|
+
* it is being read. The third width — the existing `fixed inset-0` full
|
|
14
|
+
* SCREEN — is a different thing entirely (it covers the right panel too) and
|
|
15
|
+
* stays where it is, on its own button.
|
|
16
|
+
*
|
|
17
|
+
* The rules are pure so the layout, both toolbars and the tests read the same
|
|
18
|
+
* table: which columns a mode hides is data, never a condition at a call site.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export type EditorViewMode = 'normal' | 'compact' | 'full';
|
|
22
|
+
|
|
23
|
+
/** The two modes a toolbar button can name. */
|
|
24
|
+
export type EditorViewModeTarget = 'compact' | 'full';
|
|
25
|
+
|
|
26
|
+
/** Session-state slot. Per session, like every other layout choice. */
|
|
27
|
+
export const EDITOR_VIEW_MODE_KEY = 'layout.editorViewMode';
|
|
28
|
+
|
|
29
|
+
export const DEFAULT_EDITOR_VIEW_MODE: EditorViewMode = 'normal';
|
|
30
|
+
|
|
31
|
+
/** A stored value written by another build is not a mode this one can draw. */
|
|
32
|
+
export function isEditorViewMode(value: unknown): value is EditorViewMode {
|
|
33
|
+
return value === 'normal' || value === 'compact' || value === 'full';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* What a mode HIDES. `full` hides everything `compact` does plus the sidebar,
|
|
38
|
+
* so it is strictly wider — the two are ordered, not alternatives.
|
|
39
|
+
*/
|
|
40
|
+
export function editorViewHides(mode: EditorViewMode, column: 'chat' | 'sidebar'): boolean {
|
|
41
|
+
if (column === 'chat') return mode === 'compact' || mode === 'full';
|
|
42
|
+
return mode === 'full';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Clicking the mode a button already names returns to `normal`. */
|
|
46
|
+
export function toggledEditorViewMode(current: EditorViewMode, next: EditorViewModeTarget): EditorViewMode {
|
|
47
|
+
return current === next ? 'normal' : next;
|
|
48
|
+
}
|
|
@@ -124,6 +124,27 @@ describe('resolvePanelWidth', () => {
|
|
|
124
124
|
// panel keeps its minimum and the row clips, rather than collapsing.
|
|
125
125
|
expect(resolvePanelWidth({ ...base, available: 400, siblingWidth: 320, handleCount: 2 })).toBe(320);
|
|
126
126
|
});
|
|
127
|
+
|
|
128
|
+
test('a collapsed chat removes the floor AND the pair caps', () => {
|
|
129
|
+
const available = 2000;
|
|
130
|
+
// The ordinary layout would cap this at 0.6 × 2000 = 1200 (own cap) and at
|
|
131
|
+
// 0.7 × 2000 − 320 = 1080 (pair cap, a sibling reserving its floor), and the
|
|
132
|
+
// chat's 400 floor would bind at 1280. With no chat on screen there is no
|
|
133
|
+
// conversation to leave room for, so only the group's own width limits it.
|
|
134
|
+
const wanted = { fraction: 0.9 };
|
|
135
|
+
expect(resolvePanelWidth({ ...base, available, siblingWidth: 320, handleCount: 1, stored: wanted })).toBe(
|
|
136
|
+
Math.round(PAIR_MAX_FRACTION * available),
|
|
137
|
+
);
|
|
138
|
+
expect(resolvePanelWidth({ ...base, available, siblingWidth: 320, handleCount: 1, stored: wanted, chatHidden: true })).toBe(
|
|
139
|
+
available - 320 - 1 * 3,
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('the floor still wins with the chat collapsed', () => {
|
|
144
|
+
// A group narrower than the sibling and this panel's floor: the panel keeps
|
|
145
|
+
// its minimum rather than collapsing into nothing.
|
|
146
|
+
expect(resolvePanelWidth({ ...base, available: 400, siblingWidth: 320, handleCount: 1, chatHidden: true })).toBe(320);
|
|
147
|
+
});
|
|
127
148
|
});
|
|
128
149
|
|
|
129
150
|
describe('the editor/right-panel pair', () => {
|
|
@@ -273,4 +294,13 @@ describe('pairMaxSize (the drag cap)', () => {
|
|
|
273
294
|
test('no measured area means no cap', () => {
|
|
274
295
|
expect(pairMaxSize({ available: null, siblingMin: editorMin, siblingOpen: true })).toBeUndefined();
|
|
275
296
|
});
|
|
297
|
+
|
|
298
|
+
test('a collapsed chat lets a drag reach the whole row', () => {
|
|
299
|
+
// Two panels and nothing else: the editor may be dragged up to the group's
|
|
300
|
+
// width minus the right panel's floor. The 0.6 own-cap would have frozen the
|
|
301
|
+
// separator at 2400 of 4000 with 1600px of empty row behind it.
|
|
302
|
+
expect(pairMaxSize({ available, siblingMin: rightMin, siblingOpen: true, chatHidden: true })).toBe(available - rightMin);
|
|
303
|
+
// The sibling closed means nothing to reserve at all.
|
|
304
|
+
expect(pairMaxSize({ available, siblingMin: 0, siblingOpen: false, chatHidden: true })).toBe(available);
|
|
305
|
+
});
|
|
276
306
|
});
|
|
@@ -149,6 +149,13 @@ export interface ResolveWidthArgs {
|
|
|
149
149
|
siblingWidth: number;
|
|
150
150
|
/** Separators in the group, which also consume width. */
|
|
151
151
|
handleCount: number;
|
|
152
|
+
/**
|
|
153
|
+
* The CHAT column is collapsed (`compact`/`full` editor views), so it holds no
|
|
154
|
+
* floor and the pair caps do not apply — both exist to leave the conversation
|
|
155
|
+
* a share of the group, and neither has anything to reserve once it is off
|
|
156
|
+
* screen. Keeping them would cap a two-panel layout at 70% of its width.
|
|
157
|
+
*/
|
|
158
|
+
chatHidden?: boolean;
|
|
152
159
|
}
|
|
153
160
|
|
|
154
161
|
/**
|
|
@@ -172,7 +179,7 @@ export interface ResolveWidthArgs {
|
|
|
172
179
|
* default pixel width is used unchanged.
|
|
173
180
|
*/
|
|
174
181
|
export function resolvePanelWidth(args: ResolveWidthArgs): number {
|
|
175
|
-
const { stored, defaultFraction, defaultPx, available, min, pairReservePx = 0, siblingWidth, handleCount } = args;
|
|
182
|
+
const { stored, defaultFraction, defaultPx, available, min, pairReservePx = 0, siblingWidth, handleCount, chatHidden = false } = args;
|
|
176
183
|
|
|
177
184
|
if (available === null || available <= 0) return Math.max(min, Math.round(stored?.px ?? defaultPx));
|
|
178
185
|
|
|
@@ -182,14 +189,20 @@ export function resolvePanelWidth(args: ResolveWidthArgs): number {
|
|
|
182
189
|
// `total × available − the sibling's reserved px` — and ROUNDED, because
|
|
183
190
|
// `0.7 × 4000` is `2799.9999…` in binary floating point and flooring it lands
|
|
184
191
|
// a pixel short of the intended share.
|
|
185
|
-
|
|
186
|
-
|
|
192
|
+
//
|
|
193
|
+
// With the chat COLLAPSED neither cap means anything: the pair's 70% share
|
|
194
|
+
// and the chat floor are both about leaving the conversation room, and there
|
|
195
|
+
// is no conversation on screen. Only the group's own width remains a limit,
|
|
196
|
+
// which is what lets the editor take the width the chat had.
|
|
197
|
+
const ownCap = chatHidden ? Number.MAX_SAFE_INTEGER : Math.round(PAIR_MAX_FRACTION * available);
|
|
198
|
+
const pairCap = chatHidden ? Number.MAX_SAFE_INTEGER : Math.round(PAIR_MAX_TOTAL_FRACTION * available) - pairReservePx;
|
|
199
|
+
const chatFloor = chatHidden ? 0 : MIN_CHAT_PANEL_WIDTH;
|
|
187
200
|
const ceiling = Math.max(
|
|
188
201
|
min,
|
|
189
202
|
Math.min(
|
|
190
203
|
ownCap,
|
|
191
204
|
pairCap,
|
|
192
|
-
Math.floor(available -
|
|
205
|
+
Math.floor(available - chatFloor - siblingWidth - handleCount * HANDLE_WIDTH),
|
|
193
206
|
),
|
|
194
207
|
);
|
|
195
208
|
|
|
@@ -263,9 +276,16 @@ export function pairMaxSize(args: {
|
|
|
263
276
|
siblingMin: number;
|
|
264
277
|
/** The sibling is open and therefore reserves its floor from the pair total. */
|
|
265
278
|
siblingOpen: boolean;
|
|
279
|
+
/** The chat column is collapsed, so the pair's shared share is the whole group. */
|
|
280
|
+
chatHidden?: boolean;
|
|
266
281
|
}): number | undefined {
|
|
267
|
-
const { available, siblingMin, siblingOpen } = args;
|
|
282
|
+
const { available, siblingMin, siblingOpen, chatHidden = false } = args;
|
|
268
283
|
if (available === null || available <= 0) return undefined;
|
|
284
|
+
// No chat on screen means nothing to reserve a share FOR: the pair is the
|
|
285
|
+
// whole row, so each side may be dragged up to the group's width minus the
|
|
286
|
+
// sibling's floor. The 60% own-cap would freeze a separator halfway across a
|
|
287
|
+
// layout whose only two panels are the ones being dragged.
|
|
288
|
+
if (chatHidden) return Math.max(0, Math.floor(available - (siblingOpen ? siblingMin : 0)));
|
|
269
289
|
const ownCap = Math.round(PAIR_MAX_FRACTION * available);
|
|
270
290
|
if (!siblingOpen) return ownCap;
|
|
271
291
|
const pairCap = Math.round(PAIR_MAX_TOTAL_FRACTION * available) - siblingMin;
|