@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
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
|
|
27
27
|
import { Window } from 'happy-dom';
|
|
28
28
|
import type { h as PreactH, render as PreactRender } from 'preact';
|
|
29
|
+
import type { ComponentChildren } from 'preact';
|
|
30
|
+
import type { ComponentType } from 'preact/compat';
|
|
29
31
|
import type { act as PreactAct } from 'preact/test-utils';
|
|
30
32
|
import type { useFileTabs as UseFileTabsHook } from '@/client/hooks/workspace/file-tabs';
|
|
31
33
|
import type { OpenFileInput } from '@/client/hooks/workspace/file-tabs';
|
|
@@ -44,7 +46,11 @@ const SESSION = 'new-file-tabs';
|
|
|
44
46
|
const nativeFetch = Bun.fetch;
|
|
45
47
|
|
|
46
48
|
let container: HTMLElement;
|
|
49
|
+
/** The provider component, kept so a case can re-render with a different root. */
|
|
50
|
+
let SessionStateProvider: ComponentType<{ sessionId: string; children: ComponentChildren }>;
|
|
47
51
|
let handleOpenFile: ((file: OpenFileInput) => void) | null = null;
|
|
52
|
+
/** The mounted probe, so a re-render can hand it another root. */
|
|
53
|
+
let Probe: ComponentType<{ rootPath: string | null }>;
|
|
48
54
|
let openedFiles: OpenedFile[] = [];
|
|
49
55
|
let activeFileId: number | string | null = null;
|
|
50
56
|
|
|
@@ -56,23 +62,35 @@ async function settle() {
|
|
|
56
62
|
}
|
|
57
63
|
}
|
|
58
64
|
|
|
65
|
+
/** Re-render the mounted tree with a different workspace root. */
|
|
66
|
+
async function setRoot(rootPath: string | null): Promise<void> {
|
|
67
|
+
await act(async () => {
|
|
68
|
+
render(
|
|
69
|
+
h(SessionStateProvider, { sessionId: SESSION, children: h(Probe, { rootPath }) }),
|
|
70
|
+
container,
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
await settle();
|
|
74
|
+
}
|
|
75
|
+
|
|
59
76
|
/** Mount the hook inside the provider, with `stored` as the session's blob. */
|
|
60
|
-
async function mount(stored: Record<string, unknown> = {}) {
|
|
77
|
+
async function mount(stored: Record<string, unknown> = {}, root: string | null = ROOT) {
|
|
61
78
|
globalThis.fetch = (async () => ({
|
|
62
79
|
ok: true,
|
|
63
80
|
json: async () => ({ sessionId: SESSION, state: stored }),
|
|
64
81
|
text: async () => JSON.stringify({ sessionId: SESSION, state: stored }),
|
|
65
82
|
})) as unknown as typeof fetch;
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
83
|
+
const provider = await import('@/client/components/common/session-state-provider/index');
|
|
84
|
+
SessionStateProvider = provider.SessionStateProvider as typeof SessionStateProvider;
|
|
85
|
+
Probe = ({ rootPath }: { rootPath: string | null }) => {
|
|
86
|
+
const tabs = useFileTabs(rootPath, SESSION);
|
|
69
87
|
handleOpenFile = tabs.handleOpenFile;
|
|
70
88
|
openedFiles = tabs.openedFiles;
|
|
71
89
|
activeFileId = tabs.activeFileId;
|
|
72
90
|
return null;
|
|
73
91
|
};
|
|
74
92
|
await act(async () => {
|
|
75
|
-
render(h(SessionStateProvider, { sessionId: SESSION, children: h(Probe, {}) }), container);
|
|
93
|
+
render(h(SessionStateProvider, { sessionId: SESSION, children: h(Probe, { rootPath: root }) }), container);
|
|
76
94
|
});
|
|
77
95
|
await settle();
|
|
78
96
|
}
|
|
@@ -147,6 +165,45 @@ describe('useFileTabs open resolution', () => {
|
|
|
147
165
|
expect(activeFileId).toBe(fileTabId('src/a.ts'));
|
|
148
166
|
});
|
|
149
167
|
|
|
168
|
+
/**
|
|
169
|
+
* A root that is not known yet must not close the tabs.
|
|
170
|
+
*
|
|
171
|
+
* `activeProjectPath` is derived from the sidebar's folder list, which is empty
|
|
172
|
+
* for a beat on every load and again whenever the structure topic
|
|
173
|
+
* re-subscribes. Treating that `null` as a root CHANGE wiped the file the user
|
|
174
|
+
* had just clicked, and the editor drew "Select a file to open" over it — the
|
|
175
|
+
* rare e2e failure this pins.
|
|
176
|
+
*/
|
|
177
|
+
test('a transiently unknown root does not wipe the open tabs', async () => {
|
|
178
|
+
await mount();
|
|
179
|
+
await act(async () => {
|
|
180
|
+
handleOpenFile?.(explorerPayload('src/a.ts'));
|
|
181
|
+
});
|
|
182
|
+
await settle();
|
|
183
|
+
expect(openedFiles).toHaveLength(1);
|
|
184
|
+
|
|
185
|
+
// The folder list drops out and comes back — the same workspace.
|
|
186
|
+
await setRoot(null);
|
|
187
|
+
await setRoot(ROOT);
|
|
188
|
+
|
|
189
|
+
expect(openedFiles).toHaveLength(1);
|
|
190
|
+
expect(activeFileId).toBe('src/a.ts');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('a DIFFERENT root closes the previous workspace’s tabs', async () => {
|
|
194
|
+
await mount();
|
|
195
|
+
await act(async () => {
|
|
196
|
+
handleOpenFile?.(explorerPayload('src/a.ts'));
|
|
197
|
+
});
|
|
198
|
+
await settle();
|
|
199
|
+
expect(openedFiles).toHaveLength(1);
|
|
200
|
+
|
|
201
|
+
await setRoot('/other');
|
|
202
|
+
|
|
203
|
+
expect(openedFiles).toHaveLength(0);
|
|
204
|
+
expect(activeFileId).toBeNull();
|
|
205
|
+
});
|
|
206
|
+
|
|
150
207
|
test('reopening under the computed id activates the tab rather than adding one', async () => {
|
|
151
208
|
await mount();
|
|
152
209
|
|
|
@@ -52,24 +52,37 @@ export function useFileTabs(
|
|
|
52
52
|
[sessionId],
|
|
53
53
|
);
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
/** The last root a wipe was performed FOR; `null` until one is known. */
|
|
56
|
+
const wipedRootRef = useRef<string | null>(null);
|
|
56
57
|
const { ready: layoutReady } = useSessionStateContext();
|
|
57
|
-
const wipedRootRef = useRef<string | null | undefined>(undefined);
|
|
58
58
|
|
|
59
59
|
useEffect(() => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
// Only a CHANGE from one known root to another closes the previous
|
|
61
|
+
// workspace's tabs.
|
|
62
|
+
//
|
|
63
|
+
// Two rules, and each was a bug first. A root that is not known yet is not a
|
|
64
|
+
// root that changed: the path is derived from the sidebar's folder list,
|
|
65
|
+
// which is empty for a beat on every load (and again whenever the structure
|
|
66
|
+
// topic re-subscribes), so treating `null` as a root wiped the tabs a user
|
|
67
|
+
// had just opened. And the FIRST known root is not a change either — it is
|
|
68
|
+
// the workspace the page was already showing, so wiping on it discarded the
|
|
69
|
+
// tabs the session blob had just restored (measured: the e2e specs had to
|
|
70
|
+
// gate every tree click on the `/state` response to survive it, and a click
|
|
71
|
+
// that lost that race left the editor reading "Select a file to open" over
|
|
72
|
+
// the file the user had opened).
|
|
73
|
+
if (!activeProjectPath || !layoutReady) return;
|
|
74
|
+
const previous = wipedRootRef.current;
|
|
63
75
|
wipedRootRef.current = activeProjectPath;
|
|
76
|
+
if (previous === null || previous === activeProjectPath) return;
|
|
64
77
|
// The tabs are dropped, but their scroll offsets deliberately are NOT: this
|
|
65
|
-
// wipe is not a user closing a tab.
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
78
|
+
// wipe is not a user closing a tab. Dropping here erased the offset the
|
|
79
|
+
// session blob had just restored — measured: the offset persisted as `900`,
|
|
80
|
+
// and came back as `{}` after a reload. The entry is keyed by the file's
|
|
81
|
+
// path hash, so it is reused when that file is opened again, which is the
|
|
82
|
+
// point.
|
|
70
83
|
setOpenedFiles([]);
|
|
71
84
|
setActiveFileId(null);
|
|
72
|
-
}, [activeProjectPath,
|
|
85
|
+
}, [activeProjectPath, layoutReady, setOpenedFiles, setActiveFileId]);
|
|
73
86
|
|
|
74
87
|
const handleOpenFile = useCallback((file: OpenFileInput) => {
|
|
75
88
|
const fileEntry: OpenedFile = {
|
|
@@ -22,7 +22,7 @@ import type { FormEvent, RefObject } from 'preact/compat';
|
|
|
22
22
|
import { useFetcher } from '@/client/lib/router/fetcher';
|
|
23
23
|
import { toAbsolutePath } from '@/shared/lib/fs/paths';
|
|
24
24
|
import { copyToClipboard } from '@/client/hooks/ui/clipboard';
|
|
25
|
-
import { useFileCreate } from '@/client/hooks/workspace/file-create';
|
|
25
|
+
import { useFileCreate, type CreatedEntry } from '@/client/hooks/workspace/file-create';
|
|
26
26
|
import type { FileClipboard } from '@/client/hooks/workspace/file-clipboard';
|
|
27
27
|
import type { FsActionResponse } from '@/shared/types';
|
|
28
28
|
|
|
@@ -40,7 +40,17 @@ export interface FileActionDeps {
|
|
|
40
40
|
/** Which diff to open, when the row is a changed file. */
|
|
41
41
|
diff?: { staged?: boolean; status?: string };
|
|
42
42
|
onOpenFile?: (file: { path: string; name: string; root?: string; repo?: string }) => void;
|
|
43
|
+
/** Re-read the tree — a DELETE has no local answer, so it re-reads. */
|
|
43
44
|
onActionComplete: () => void;
|
|
45
|
+
/**
|
|
46
|
+
* A row the panel just created, placed locally.
|
|
47
|
+
*
|
|
48
|
+
* A create answers with the whole result, so the tree inserts it rather than
|
|
49
|
+
* re-reading the root plus every open folder to learn it back.
|
|
50
|
+
*/
|
|
51
|
+
onCreated?: (entry: CreatedEntry) => void;
|
|
52
|
+
/** A row the panel just renamed, moved locally — same reason as `onCreated`. */
|
|
53
|
+
onRenamed?: (fromPath: string, toPath: string) => void;
|
|
44
54
|
/** The panel's clipboard: Cut/Copy fill it, Paste reads it. */
|
|
45
55
|
clipboard: FileClipboard;
|
|
46
56
|
/** Whether this row is a folder — what Cut/Copy/Paste need to name their target. */
|
|
@@ -56,12 +66,16 @@ export interface FileActions {
|
|
|
56
66
|
showDeleteModal: boolean;
|
|
57
67
|
showHistoryModal: boolean;
|
|
58
68
|
showCreateModal: boolean;
|
|
69
|
+
/** Whether the open create dialog makes a folder. */
|
|
70
|
+
createKind: 'file' | 'folder';
|
|
59
71
|
/** The server's refusal for the create request, rendered inside its dialog. */
|
|
60
72
|
createError: string | null;
|
|
61
73
|
createName: string;
|
|
62
74
|
setCreateName: (value: string) => void;
|
|
63
75
|
renameValue: string;
|
|
64
76
|
setRenameValue: (value: string) => void;
|
|
77
|
+
/** The server's refusal for the rename request, rendered inside its dialog. */
|
|
78
|
+
renameError: string | null;
|
|
65
79
|
/** True while a request is in flight; every dialog's buttons read it. */
|
|
66
80
|
busy: boolean;
|
|
67
81
|
/** Label of the row's Paste action, or null when a paste cannot land here. */
|
|
@@ -79,7 +93,7 @@ export interface FileActions {
|
|
|
79
93
|
closeHistory: () => void;
|
|
80
94
|
}
|
|
81
95
|
|
|
82
|
-
export function useFileActions({ file, diff, onOpenFile, onActionComplete, clipboard, isFolder }: FileActionDeps): FileActions {
|
|
96
|
+
export function useFileActions({ file, diff, onOpenFile, onActionComplete, onCreated, onRenamed, clipboard, isFolder }: FileActionDeps): FileActions {
|
|
83
97
|
const actionFetcher = useFetcher<FsActionResponse>();
|
|
84
98
|
const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null);
|
|
85
99
|
const contextMenuRef = useRef<HTMLDivElement>(null);
|
|
@@ -87,6 +101,7 @@ export function useFileActions({ file, diff, onOpenFile, onActionComplete, clipb
|
|
|
87
101
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
|
88
102
|
const [showHistoryModal, setShowHistoryModal] = useState(false);
|
|
89
103
|
const [renameValue, setRenameValue] = useState(file.path);
|
|
104
|
+
const [renameError, setRenameError] = useState<string | null>(null);
|
|
90
105
|
|
|
91
106
|
const repo = file.repo;
|
|
92
107
|
const rootPath = file.rootPath;
|
|
@@ -98,7 +113,10 @@ export function useFileActions({ file, diff, onOpenFile, onActionComplete, clipb
|
|
|
98
113
|
folderPath: file.path,
|
|
99
114
|
rootPath,
|
|
100
115
|
repo,
|
|
101
|
-
|
|
116
|
+
// The tree places the entry itself and does NOT reload: the response names
|
|
117
|
+
// the whole result, and re-reading the root plus every open folder to learn
|
|
118
|
+
// it back is the reload this path exists to avoid.
|
|
119
|
+
onCreated: (entry) => (onCreated ? onCreated(entry) : onActionComplete()),
|
|
102
120
|
onOpenCreated: (created) => onOpenFile?.({ ...created, root: rootPath, repo: repo || '.' }),
|
|
103
121
|
});
|
|
104
122
|
|
|
@@ -124,7 +142,15 @@ export function useFileActions({ file, diff, onOpenFile, onActionComplete, clipb
|
|
|
124
142
|
if (data.success && !data.type) {
|
|
125
143
|
setShowRenameModal(false);
|
|
126
144
|
setShowDeleteModal(false);
|
|
127
|
-
|
|
145
|
+
setRenameError(null);
|
|
146
|
+
// A rename response names both ends, so the tree moves the row locally
|
|
147
|
+
// instead of re-reading every open folder to discover the move.
|
|
148
|
+
if (data.previousPath && data.path && onRenamed) onRenamed(data.previousPath, data.path);
|
|
149
|
+
else onActionComplete();
|
|
150
|
+
} else if (!data.success && data.error && showRenameModal) {
|
|
151
|
+
// A refusal keeps the dialog open with the server's own words beside the
|
|
152
|
+
// field: "already exists" is a message about what to type next.
|
|
153
|
+
setRenameError(data.error);
|
|
128
154
|
}
|
|
129
155
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
130
156
|
}, [actionFetcher.state, actionFetcher.data]);
|
|
@@ -155,6 +181,7 @@ export function useFileActions({ file, diff, onOpenFile, onActionComplete, clipb
|
|
|
155
181
|
|
|
156
182
|
const handleAction = (actionType: string) => {
|
|
157
183
|
setContextMenu(null);
|
|
184
|
+
setRenameError(null);
|
|
158
185
|
if (actionType === 'view') {
|
|
159
186
|
openFile();
|
|
160
187
|
} else if (actionType === 'diff') {
|
|
@@ -186,7 +213,9 @@ export function useFileActions({ file, diff, onOpenFile, onActionComplete, clipb
|
|
|
186
213
|
setRenameValue(file.path);
|
|
187
214
|
setShowRenameModal(true);
|
|
188
215
|
} else if (actionType === 'new_file') {
|
|
189
|
-
create.openDialog();
|
|
216
|
+
create.openDialog('file');
|
|
217
|
+
} else if (actionType === 'new_folder') {
|
|
218
|
+
create.openDialog('folder');
|
|
190
219
|
} else if (actionType === 'delete') {
|
|
191
220
|
setShowDeleteModal(true);
|
|
192
221
|
} else if (actionType === 'cut') {
|
|
@@ -202,6 +231,7 @@ export function useFileActions({ file, diff, onOpenFile, onActionComplete, clipb
|
|
|
202
231
|
|
|
203
232
|
const submitRename = (e: FormEvent) => {
|
|
204
233
|
e.preventDefault();
|
|
234
|
+
setRenameError(null);
|
|
205
235
|
const fd = new FormData();
|
|
206
236
|
fd.append('actionType', 'rename');
|
|
207
237
|
fd.append('path', file.path);
|
|
@@ -225,11 +255,13 @@ export function useFileActions({ file, diff, onOpenFile, onActionComplete, clipb
|
|
|
225
255
|
showDeleteModal,
|
|
226
256
|
showHistoryModal,
|
|
227
257
|
showCreateModal: create.open,
|
|
258
|
+
createKind: create.kind,
|
|
228
259
|
createError: create.error,
|
|
229
260
|
createName: create.name,
|
|
230
261
|
setCreateName: create.setName,
|
|
231
262
|
renameValue,
|
|
232
263
|
setRenameValue,
|
|
264
|
+
renameError,
|
|
233
265
|
busy: actionFetcher.state !== 'idle' || create.busy || clipboard.busy,
|
|
234
266
|
pasteLabel,
|
|
235
267
|
fetcherState: actionFetcher.state,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The navbar's session menu, resolved from the session ON SCREEN.
|
|
8
|
+
*
|
|
9
|
+
* A hook rather than three statements in the layout because the lookup has a
|
|
10
|
+
* rule of its own: the session's row may live in a folder that is not expanded
|
|
11
|
+
* in the sidebar (or not visible at all), so it is found by id across every
|
|
12
|
+
* folder instead of by walking the visible list. No row means no menu — a
|
|
13
|
+
* session the sidebar does not know (a chat running outside every registered
|
|
14
|
+
* workspace) gets nothing rather than dead items.
|
|
15
|
+
*
|
|
16
|
+
* The pending `new-…` refusal lives here too: such a chat has no transcript
|
|
17
|
+
* anywhere yet, so the server refuses rename and delete, and the menu hides
|
|
18
|
+
* them the way the sidebar does.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { useMemo } from 'preact/hooks';
|
|
22
|
+
import { useSidebarData } from '@/client/hooks/chat/omp/session-list';
|
|
23
|
+
import { useSessionActions } from '@/client/hooks/workspace/session-actions';
|
|
24
|
+
import { useSessionDelete, type SessionDeleteActions } from '@/client/hooks/workspace/session-delete';
|
|
25
|
+
import type { WorkspaceFolderData } from '@/shared/types';
|
|
26
|
+
|
|
27
|
+
/** What `TopNavbar` needs to draw its title menu. */
|
|
28
|
+
export interface NavbarSessionMenu {
|
|
29
|
+
isArchived: boolean;
|
|
30
|
+
onRename?: (name: string) => void;
|
|
31
|
+
onRenameWithAi?: () => void;
|
|
32
|
+
onArchive: () => void;
|
|
33
|
+
onDelete?: () => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface NavbarSessionActions {
|
|
37
|
+
menu: NavbarSessionMenu | undefined;
|
|
38
|
+
/** The delete confirmation's own state machine, which the layout renders. */
|
|
39
|
+
sessionDelete: SessionDeleteActions;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function useNavbarSessionActions(
|
|
43
|
+
folders: WorkspaceFolderData[],
|
|
44
|
+
sessionId: string | null,
|
|
45
|
+
report: (message: string, tone?: 'success' | 'error') => void,
|
|
46
|
+
): NavbarSessionActions {
|
|
47
|
+
const sidebar = useSidebarData();
|
|
48
|
+
const actions = useSessionActions(sidebar.refresh, report);
|
|
49
|
+
const sessionDelete = useSessionDelete();
|
|
50
|
+
|
|
51
|
+
const activeSession = useMemo(() => {
|
|
52
|
+
if (!sessionId) return null;
|
|
53
|
+
for (const folder of folders) {
|
|
54
|
+
const found = (folder.sessions || []).find((s) => String(s.id) === String(sessionId));
|
|
55
|
+
if (found) return found;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}, [folders, sessionId]);
|
|
59
|
+
|
|
60
|
+
const pending = Boolean(sessionId && sessionId.startsWith('new-'));
|
|
61
|
+
const menu: NavbarSessionMenu | undefined = activeSession
|
|
62
|
+
? {
|
|
63
|
+
isArchived: activeSession.is_archived === 1,
|
|
64
|
+
onRename: pending ? undefined : (name: string) => void actions.handleRename(activeSession, name),
|
|
65
|
+
onRenameWithAi: pending ? undefined : () => void actions.handleRenameWithAi(activeSession),
|
|
66
|
+
onArchive: () => actions.handleArchive(activeSession),
|
|
67
|
+
onDelete: pending ? undefined : () => sessionDelete.requestDelete(activeSession),
|
|
68
|
+
}
|
|
69
|
+
: undefined;
|
|
70
|
+
|
|
71
|
+
return { menu, sessionDelete };
|
|
72
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Client state for the worktree surfaces: the list, creation and removal.
|
|
8
|
+
*
|
|
9
|
+
* One module for all three because they share the same two facts — the project
|
|
10
|
+
* the request is scoped to and the failure shape the server answers with — and
|
|
11
|
+
* splitting them would put the fetch envelope in three places.
|
|
12
|
+
*
|
|
13
|
+
* Nothing here invents a path: the server resolves the destination from the
|
|
14
|
+
* dialog's NAME and reports back what it used, so the client cannot name a
|
|
15
|
+
* directory outside the project.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { useCallback, useEffect, useState } from 'preact/hooks';
|
|
19
|
+
import type { WorktreeFailure, WorktreeInfo, WorktreeListPayload, WorktreeValidation } from '@/shared/lib/workspace/worktree/types';
|
|
20
|
+
|
|
21
|
+
export interface WorktreeRequestScope {
|
|
22
|
+
/** The project's `project_path`, sent as `?root=`. */
|
|
23
|
+
root: string | null;
|
|
24
|
+
/** Sent as `?folderId=` so an unbound-but-valid folder still resolves. */
|
|
25
|
+
folderId?: number | string | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function queryFor(scope: WorktreeRequestScope): string {
|
|
29
|
+
const params = new URLSearchParams();
|
|
30
|
+
if (scope.root) params.set('root', scope.root);
|
|
31
|
+
if (scope.folderId !== null && scope.folderId !== undefined) params.set('folderId', String(scope.folderId));
|
|
32
|
+
const query = params.toString();
|
|
33
|
+
return query ? `?${query}` : '';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A refusal the UI reports verbatim; `field` marks the input that caused it. */
|
|
37
|
+
export class WorktreeRequestError extends Error {
|
|
38
|
+
readonly failure: WorktreeFailure;
|
|
39
|
+
|
|
40
|
+
constructor(failure: WorktreeFailure) {
|
|
41
|
+
super(failure.message);
|
|
42
|
+
this.name = 'WorktreeRequestError';
|
|
43
|
+
this.failure = failure;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function readFailure(response: Response): Promise<never> {
|
|
48
|
+
const body = (await response.json().catch(() => null)) as (WorktreeFailure & { error?: string }) | null;
|
|
49
|
+
throw new WorktreeRequestError({
|
|
50
|
+
code: body?.code ?? 'create_failed',
|
|
51
|
+
message: body?.message ?? body?.error ?? `HTTP ${response.status}`,
|
|
52
|
+
...(body?.field ? { field: body.field } : {}),
|
|
53
|
+
...(body?.count !== undefined ? { count: body.count } : {}),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The project's worktrees, with the facts each row shows. */
|
|
58
|
+
export function useWorktrees(scope: WorktreeRequestScope, enabled: boolean) {
|
|
59
|
+
const [payload, setPayload] = useState<WorktreeListPayload | null>(null);
|
|
60
|
+
const [loading, setLoading] = useState(false);
|
|
61
|
+
const [failure, setFailure] = useState<WorktreeFailure | null>(null);
|
|
62
|
+
|
|
63
|
+
const refresh = useCallback(async () => {
|
|
64
|
+
if (!enabled) return;
|
|
65
|
+
setLoading(true);
|
|
66
|
+
try {
|
|
67
|
+
const response = await fetch(`/api/worktrees${queryFor(scope)}`);
|
|
68
|
+
if (!response.ok) await readFailure(response);
|
|
69
|
+
setPayload((await response.json()) as WorktreeListPayload);
|
|
70
|
+
setFailure(null);
|
|
71
|
+
} catch (cause) {
|
|
72
|
+
// Kept apart from the payload: an empty list and a failed read are
|
|
73
|
+
// different answers, and only one of them means nothing is there. The
|
|
74
|
+
// CODE travels too, because "this folder is not a repository" and "git
|
|
75
|
+
// could not answer" are different states to draw — and the first one is
|
|
76
|
+
// how a caller knows to offer no worktree control at all.
|
|
77
|
+
setFailure(
|
|
78
|
+
cause instanceof WorktreeRequestError
|
|
79
|
+
? cause.failure
|
|
80
|
+
: { code: 'create_failed', message: cause instanceof Error ? cause.message : String(cause) },
|
|
81
|
+
);
|
|
82
|
+
} finally {
|
|
83
|
+
setLoading(false);
|
|
84
|
+
}
|
|
85
|
+
}, [enabled, scope.root, scope.folderId]);
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
void refresh();
|
|
89
|
+
}, [refresh]);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
payload,
|
|
93
|
+
worktrees: payload?.worktrees ?? ([] as WorktreeInfo[]),
|
|
94
|
+
/**
|
|
95
|
+
* Whether the project is a git repository at all. `null` while it is still
|
|
96
|
+
* unknown: a caller that draws a worktree control must wait for the answer
|
|
97
|
+
* rather than offer one a plain folder can never satisfy.
|
|
98
|
+
*/
|
|
99
|
+
isRepo: payload ? payload.isRepo : null,
|
|
100
|
+
/** The resolved parent directory a create would use. */
|
|
101
|
+
base: payload?.base ?? '',
|
|
102
|
+
loading,
|
|
103
|
+
failure,
|
|
104
|
+
/** The failure's message, for the surfaces that only print one. */
|
|
105
|
+
error: failure?.message ?? null,
|
|
106
|
+
refresh,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface CreateWorktreeRequest {
|
|
111
|
+
mode: 'new' | 'existing';
|
|
112
|
+
branch?: string;
|
|
113
|
+
worktreeName?: string;
|
|
114
|
+
startRef?: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The plan a create would use, without creating anything. */
|
|
118
|
+
export async function validateWorktree(
|
|
119
|
+
scope: WorktreeRequestScope,
|
|
120
|
+
request: CreateWorktreeRequest,
|
|
121
|
+
): Promise<WorktreeValidation> {
|
|
122
|
+
const response = await fetch(`/api/worktrees/validate${queryFor(scope)}`, {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
headers: { 'content-type': 'application/json' },
|
|
125
|
+
body: JSON.stringify(request),
|
|
126
|
+
});
|
|
127
|
+
const body = (await response.json().catch(() => null)) as WorktreeValidation | null;
|
|
128
|
+
if (!body) return { ok: false, failures: [{ code: 'create_failed', message: `HTTP ${response.status}` }] };
|
|
129
|
+
return body;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function createWorktree(
|
|
133
|
+
scope: WorktreeRequestScope,
|
|
134
|
+
request: CreateWorktreeRequest,
|
|
135
|
+
): Promise<WorktreeInfo> {
|
|
136
|
+
const response = await fetch(`/api/worktrees${queryFor(scope)}`, {
|
|
137
|
+
method: 'POST',
|
|
138
|
+
headers: { 'content-type': 'application/json' },
|
|
139
|
+
body: JSON.stringify(request),
|
|
140
|
+
});
|
|
141
|
+
if (!response.ok) await readFailure(response);
|
|
142
|
+
const body = (await response.json()) as { worktree: WorktreeInfo };
|
|
143
|
+
return body.worktree;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface RemoveWorktreeResult {
|
|
147
|
+
removed: true;
|
|
148
|
+
archived: string[];
|
|
149
|
+
branchDeleted: boolean;
|
|
150
|
+
branchKept: boolean;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function removeWorktree(
|
|
154
|
+
scope: WorktreeRequestScope,
|
|
155
|
+
worktreePath: string,
|
|
156
|
+
options: { deleteBranch?: boolean } = {},
|
|
157
|
+
): Promise<RemoveWorktreeResult> {
|
|
158
|
+
const response = await fetch(`/api/worktrees${queryFor(scope)}`, {
|
|
159
|
+
method: 'DELETE',
|
|
160
|
+
headers: { 'content-type': 'application/json' },
|
|
161
|
+
body: JSON.stringify({ path: worktreePath, deleteBranch: options.deleteBranch === true }),
|
|
162
|
+
});
|
|
163
|
+
if (!response.ok) await readFailure(response);
|
|
164
|
+
return (await response.json()) as RemoveWorktreeResult;
|
|
165
|
+
}
|
|
@@ -30,6 +30,14 @@
|
|
|
30
30
|
export interface DesktopInfo {
|
|
31
31
|
platform: string;
|
|
32
32
|
shellVersion: string;
|
|
33
|
+
/**
|
|
34
|
+
* The machine's architecture, in the canonical vocabulary (`arm64` / `x64`).
|
|
35
|
+
*
|
|
36
|
+
* Every desktop artifact is per-architecture, so without this the console can
|
|
37
|
+
* only offer the release page. Absent on a shell too old to report it, which
|
|
38
|
+
* degrades the offer to that page rather than breaking it.
|
|
39
|
+
*/
|
|
40
|
+
arch?: string;
|
|
33
41
|
/** True when the shell hides the native title bar and the page draws it. */
|
|
34
42
|
overlayTitlebar: boolean;
|
|
35
43
|
/**
|
|
@@ -156,3 +164,40 @@ export function windowToggleMaximize(): Promise<void> {
|
|
|
156
164
|
export function windowClose(): Promise<void> {
|
|
157
165
|
return call<void>('window_close');
|
|
158
166
|
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* What the shell did with an update it was handed.
|
|
170
|
+
*
|
|
171
|
+
* The shell owns the whole run — download, verify, replace, relaunch — because
|
|
172
|
+
* only it can do any of those things: a page cannot replace the executable it is
|
|
173
|
+
* running inside, and a page's "download" leaves a file in a folder the user has
|
|
174
|
+
* to find. This is the report it sends back.
|
|
175
|
+
*
|
|
176
|
+
* `relaunching` is the honest terminal state on success: the shell is about to
|
|
177
|
+
* quit, so a caller that keeps a spinner going is describing a page that is
|
|
178
|
+
* already gone.
|
|
179
|
+
*/
|
|
180
|
+
export interface ShellUpdateOutcome {
|
|
181
|
+
success: boolean;
|
|
182
|
+
/** The version the shell installed, when it could read one. */
|
|
183
|
+
version?: string | null;
|
|
184
|
+
/** A sentence for the user — the shell's own, never a generic failure. */
|
|
185
|
+
message: string;
|
|
186
|
+
relaunching?: boolean;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Ask the shell to install the build it just offered.
|
|
191
|
+
*
|
|
192
|
+
* `url` and `version` are the console's answer, passed through rather than
|
|
193
|
+
* re-derived: the shell has no release list of its own, and asking it to fetch
|
|
194
|
+
* one would be a second reader of the same GitHub endpoint.
|
|
195
|
+
*
|
|
196
|
+
* The shell VERIFIES the bytes before installing them (the platform's own check —
|
|
197
|
+
* a code signature, an archive shape, a version floor). A console that is
|
|
198
|
+
* compromised can therefore make the shell download, but not make it run
|
|
199
|
+
* something the platform will not vouch for.
|
|
200
|
+
*/
|
|
201
|
+
export function applyShellUpdate(url: string, version: string): Promise<ShellUpdateOutcome> {
|
|
202
|
+
return call<ShellUpdateOutcome>('update_apply', { url, version });
|
|
203
|
+
}
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* editor can decide by equality whether the request is for the file it holds.
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
import type { FindOptions } from '@/shared/lib/code/editor/find';
|
|
26
|
+
import type { FindOptions } from '@/shared/lib/code/editor/find-scan';
|
|
27
27
|
|
|
28
28
|
export interface SearchRevealRequest {
|
|
29
29
|
/** Workspace-relative path of the file the match lives in. */
|
package/src/client/main.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { hydrate } from 'preact';
|
|
2
2
|
import { App } from '@/client/App';
|
|
3
3
|
import { bootSyntax } from '@/shared/lib/code/highlighter';
|
|
4
|
+
import { warmSyntaxTokenizers } from '@/shared/lib/code/highlighter-warmup';
|
|
4
5
|
import { primeKabmoSettings } from '@/shared/lib/settings/client';
|
|
5
6
|
import { initDocumentTheme } from '@/client/hooks/ui/theme';
|
|
6
7
|
import { primeWindowChrome } from '@/client/hooks/ui/window-chrome';
|
|
@@ -70,7 +71,10 @@ installFileLaunchHandler((result) => {
|
|
|
70
71
|
});
|
|
71
72
|
|
|
72
73
|
// Fire-and-forget: warms the Shiki highlighter in parallel with hydration.
|
|
73
|
-
|
|
74
|
+
// Once the eager grammars land, each is tokenized once on a tiny sample during
|
|
75
|
+
// idle time — loading a grammar is not compiling it, and the compile used to be
|
|
76
|
+
// paid inside the first tool card the reader expanded.
|
|
77
|
+
void bootSyntax().then(() => warmSyntaxTokenizers());
|
|
74
78
|
|
|
75
79
|
if (root) {
|
|
76
80
|
hydrate(
|