@kabmo-labs/kabmo 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/client/index.html +1 -1
- package/dist/client/index.js +403 -384
- package/dist/client/static/js/{index-0wvbca0b.js → index-bpptfkz2.js} +905 -875
- package/dist/client/static/js/index-k315p1e8.css +1 -0
- package/package.json +10 -1
- package/src/client/components/common/code-surface/documents.test.ts +120 -0
- package/src/client/components/common/code-surface/documents.ts +201 -0
- package/src/client/components/common/code-surface/extensions.test.ts +110 -0
- package/src/client/components/common/code-surface/extensions.ts +162 -0
- package/src/client/components/common/code-surface/handle.ts +102 -0
- package/src/client/components/common/code-surface/index.tsx +253 -314
- package/src/client/components/common/code-surface/keymap.test.ts +133 -0
- package/src/client/components/common/code-surface/keymap.ts +267 -0
- package/src/client/components/common/code-surface/marks.ts +74 -0
- package/src/client/components/common/code-surface/shiki.ts +234 -0
- package/src/client/components/common/code-surface/sync.test.ts +185 -0
- package/src/client/components/common/code-surface/sync.ts +58 -0
- package/src/client/components/common/code-surface/theme.test.ts +202 -0
- package/src/client/components/common/code-surface/theme.ts +111 -0
- package/src/client/components/common/editor-view-buttons/index.test.tsx +148 -0
- package/src/client/components/common/editor-view-buttons/index.tsx +109 -0
- package/src/client/components/common/header-controls/index.tsx +24 -1
- package/src/client/components/common/header-controls/shell-chrome.test.tsx +272 -0
- package/src/client/components/common/shell-update-button/Modal.tsx +163 -0
- package/src/client/components/common/shell-update-button/index.test.tsx +251 -0
- package/src/client/components/common/shell-update-button/index.tsx +125 -0
- package/src/client/components/common/toolbar-row/index.test.tsx +166 -0
- package/src/client/components/common/toolbar-row/index.tsx +189 -0
- package/src/client/components/common/workspace-options-menu/index.tsx +19 -1
- package/src/client/components/layout/desktop-layout/TopNavbar.tsx +5 -6
- package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +41 -78
- package/src/client/components/layout/desktop-layout/index.tsx +55 -69
- package/src/client/components/layout/desktop-layout/resizer/index.tsx +6 -3
- package/src/client/components/layout/session-sidebar/CategoryHeader.tsx +24 -0
- package/src/client/components/layout/session-sidebar/CategoryHeader.worktree.test.tsx +126 -0
- package/src/client/components/layout/session-sidebar/CategoryItem.tsx +4 -0
- package/src/client/components/layout/session-sidebar/Footer.tsx +8 -15
- package/src/client/components/layout/session-sidebar/SessionItem.tsx +12 -0
- package/src/client/components/layout/session-sidebar/SessionList.tsx +4 -0
- package/src/client/components/layout/session-sidebar/SessionsView.tsx +24 -0
- package/src/client/components/layout/session-sidebar/WorktreeBadge.test.ts +48 -0
- package/src/client/components/layout/session-sidebar/WorktreeBadge.tsx +55 -0
- package/src/client/components/mobile/LayoutWrapper.tsx +23 -16
- package/src/client/components/mobile/RightSidebar.tsx +21 -10
- package/src/client/components/mobile/SessionSidebar.tsx +8 -0
- package/src/client/components/mobile/header-titlebar.test.tsx +172 -0
- package/src/client/components/mobile/mobile-main-view/Header.tsx +10 -1
- package/src/client/components/mobile/mobile-right-sidebar/FullDiff.tsx +3 -0
- package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +63 -38
- package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.test.tsx +137 -0
- package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.tsx +14 -3
- package/src/client/components/mobile/mobile-session-sidebar/Footer.tsx +9 -21
- package/src/client/components/mobile/mobile-session-sidebar/Header.tsx +6 -2
- package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +16 -0
- package/src/client/components/mobile/mobile-session-sidebar/List.tsx +4 -0
- package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +5 -0
- package/src/client/components/mobile/mobile-session-sidebar/WorktreeDialog.tsx +49 -0
- package/src/client/components/mobile/worktree-session.ts +51 -0
- package/src/client/components/settings/Modal.tsx +8 -0
- package/src/client/components/settings/categories/AppearanceSettings.tsx +3 -0
- package/src/client/components/settings/categories/ProjectSettings.tsx +22 -8
- package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.test.tsx +167 -0
- package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.tsx +184 -0
- package/src/client/components/settings/categories/project-settings/WorktreeSettings.tsx +146 -0
- package/src/client/components/settings/categories/project-settings/worktree-row.test.ts +83 -0
- package/src/client/components/settings/categories/project-settings/worktree-row.ts +47 -0
- package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +36 -55
- package/src/client/components/workspace/chat-timeline/PendingChatView.tsx +8 -0
- package/src/client/components/workspace/chat-timeline/SystemNotice.tsx +9 -4
- package/src/client/components/workspace/chat-timeline/TaskResultContent.tsx +9 -7
- package/src/client/components/workspace/chat-timeline/index.tsx +5 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.test.tsx +132 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.tsx +120 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.test.tsx +319 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.tsx +285 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.test.ts +2 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/hashline-patch/index.tsx +34 -29
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/AstEdit.tsx +11 -9
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Bash.tsx +10 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Edit.tsx +25 -8
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.test.tsx +146 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.tsx +32 -17
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.test.ts +114 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.tsx +111 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Read.tsx +7 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/SearchTool.tsx +9 -59
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ArtifactBody.tsx +2 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.test.ts +27 -28
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.tsx +39 -26
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ExcerptCode.tsx +8 -8
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/HighlightedCode.tsx +35 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/JsonCodeBlock.tsx +4 -4
- package/src/client/components/workspace/diff-panel/MergeDiffView.tsx +179 -0
- package/src/client/components/workspace/diff-panel/ThemedMergeDiffView.tsx +24 -0
- package/src/client/components/workspace/diff-panel/Toolbar.tsx +224 -186
- package/src/client/components/workspace/diff-panel/ToolbarSegments.tsx +98 -0
- package/src/client/components/workspace/diff-panel/index.tsx +76 -145
- package/src/client/components/workspace/diff-panel/merge.ts +315 -0
- package/src/client/components/workspace/editor/Body.tsx +4 -7
- package/src/client/components/workspace/editor/CommandPalette.tsx +2 -2
- package/src/client/components/workspace/editor/FileDocument.tsx +58 -63
- package/src/client/components/workspace/editor/FilePane.tsx +4 -7
- package/src/client/components/workspace/editor/FindWidget.tsx +7 -2
- package/src/client/components/workspace/editor/Toolbar.tsx +220 -123
- package/src/client/components/workspace/editor/index.tsx +7 -9
- package/src/client/components/workspace/file-explorer/ContextMenu.tsx +2 -1
- package/src/client/components/workspace/file-explorer/Modals.tsx +40 -12
- package/src/client/components/workspace/file-explorer/OutlineSection.test.tsx +18 -0
- package/src/client/components/workspace/file-explorer/OutlineSection.tsx +10 -3
- package/src/client/components/workspace/file-explorer/RootMenu.tsx +14 -5
- package/src/client/components/workspace/file-explorer/Toolbar.tsx +74 -0
- package/src/client/components/workspace/file-explorer/TreeItem.tsx +38 -3
- package/src/client/components/workspace/file-explorer/filter.ts +47 -0
- package/src/client/components/workspace/file-explorer/index.tsx +77 -64
- package/src/client/components/workspace/file-explorer/local-edit.test.ts +135 -0
- package/src/client/components/workspace/search-panel/Body.tsx +1 -1
- package/src/client/components/workspace/worktree-modal/index.test.tsx +118 -0
- package/src/client/components/workspace/worktree-modal/index.tsx +261 -0
- package/src/client/hooks/chat/omp/reconnect.test.ts +338 -0
- package/src/client/hooks/chat/omp/reconnect.ts +187 -0
- package/src/client/hooks/chat/omp/run-liveness.test.ts +71 -0
- package/src/client/hooks/chat/omp/run-liveness.ts +65 -0
- package/src/client/hooks/chat/omp/session-list.activity.test.ts +114 -0
- package/src/client/hooks/chat/omp/session-list.test-util.ts +164 -0
- package/src/client/hooks/chat/omp/session-list.test.ts +30 -150
- package/src/client/hooks/chat/omp/session-list.tsx +69 -9
- package/src/client/hooks/chat/timeline/generating.ts +19 -2
- package/src/client/hooks/chat/timeline/index.ts +27 -28
- package/src/client/hooks/chat/timeline/lost-run.test.ts +329 -0
- package/src/client/hooks/chat/timeline/lost-run.ts +177 -0
- package/src/client/hooks/chat/timeline/send-execute.test.ts +2 -0
- package/src/client/hooks/chat/timeline/send-steer.test.ts +2 -0
- package/src/client/hooks/chat/timeline/send.test.ts +2 -0
- package/src/client/hooks/chat/timeline/send.ts +11 -1
- package/src/client/hooks/chat/timeline/session-load.test-util.ts +205 -0
- package/src/client/hooks/chat/timeline/session-load.test.ts +61 -162
- package/src/client/hooks/chat/timeline/session-load.ts +69 -37
- package/src/client/hooks/chat/timeline/session-switch.test.ts +65 -0
- package/src/client/hooks/chat/timeline/session-switch.ts +63 -0
- package/src/client/hooks/chat/timeline/spawn-target.ts +52 -0
- package/src/client/hooks/chat/timeline/tool-open.test.ts +74 -0
- package/src/client/hooks/chat/timeline/tool-open.ts +52 -8
- package/src/client/hooks/code/deferred-highlight.test.ts +182 -0
- package/src/client/hooks/code/deferred-highlight.ts +129 -0
- package/src/client/hooks/editor/find-state.ts +1 -1
- package/src/client/hooks/editor/theme.test.ts +206 -0
- package/src/client/hooks/editor/theme.ts +82 -0
- package/src/client/hooks/editor/use-editor-drop.test.ts +101 -0
- package/src/client/hooks/editor/use-editor-drop.ts +67 -14
- package/src/client/hooks/editor/use-editor-find.ts +7 -7
- package/src/client/hooks/editor/use-outline-view.test.ts +182 -0
- package/src/client/hooks/editor/use-outline-view.ts +94 -7
- package/src/client/hooks/editor/use-scroll-restore.test.ts +55 -2
- package/src/client/hooks/editor/use-scroll-restore.ts +124 -4
- package/src/client/hooks/editor/use-search-reveal.ts +1 -1
- package/src/client/hooks/ui/long-press.test.ts +174 -0
- package/src/client/hooks/ui/long-press.ts +112 -0
- package/src/client/hooks/ui/settings-modal.ts +56 -0
- package/src/client/hooks/ui/updates/shell-download.test.ts +75 -0
- package/src/client/hooks/ui/updates/shell-download.ts +153 -28
- package/src/client/hooks/ui/updates/shell-offer.test.ts +213 -0
- package/src/client/hooks/ui/updates/shell-update.test.ts +140 -0
- package/src/client/hooks/ui/updates/shell-update.ts +119 -0
- package/src/client/hooks/workspace/diff-file.ts +29 -10
- package/src/client/hooks/workspace/diff-git-actions.ts +147 -0
- package/src/client/hooks/workspace/editor-view.ts +61 -0
- package/src/client/hooks/workspace/file-create.ts +41 -11
- package/src/client/hooks/workspace/file-listing.ts +56 -1
- package/src/client/hooks/workspace/file-tabs.test.ts +62 -5
- package/src/client/hooks/workspace/file-tabs.ts +24 -11
- package/src/client/hooks/workspace/file-tree-actions.ts +37 -5
- package/src/client/hooks/workspace/navbar-session-actions.ts +72 -0
- package/src/client/hooks/workspace/worktree/index.ts +165 -0
- package/src/client/lib/desktop/bridge.ts +45 -0
- package/src/client/lib/search-reveal.ts +1 -1
- package/src/client/main.tsx +5 -1
- package/src/client/tailwind.css +56 -8
- package/src/server/index.ts +7 -2
- package/src/server/lib/auth/guard.test.ts +28 -0
- package/src/server/lib/auth/guard.ts +10 -0
- package/src/server/lib/bundler/titlebar-css.test.ts +42 -1
- package/src/server/lib/db/schema.ts +4 -0
- package/src/server/lib/db/worktree-registry.server.ts +91 -0
- package/src/server/lib/fs/canonical.ts +52 -0
- package/src/server/lib/fs/relative-path.ts +48 -0
- package/src/server/lib/fs/root-worktree.test.ts +116 -0
- package/src/server/lib/fs/root.ts +39 -10
- package/src/server/lib/fs/worktree/carry.ts +134 -0
- package/src/server/lib/fs/worktree/create.test.ts +249 -0
- package/src/server/lib/fs/worktree/create.ts +262 -0
- package/src/server/lib/fs/worktree/errors.ts +43 -0
- package/src/server/lib/fs/worktree/list.test.ts +207 -0
- package/src/server/lib/fs/worktree/list.ts +142 -0
- package/src/server/lib/fs/worktree/location.test.ts +206 -0
- package/src/server/lib/fs/worktree/location.ts +114 -0
- package/src/server/lib/fs/worktree/project-id.ts +53 -0
- package/src/server/lib/fs/worktree/remove.test.ts +242 -0
- package/src/server/lib/fs/worktree/remove.ts +152 -0
- package/src/server/lib/fs/worktree/settings.server.ts +56 -0
- package/src/server/lib/fs/worktree/topology.ts +142 -0
- package/src/server/lib/lifecycle/claim-port.test.ts +156 -0
- package/src/server/lib/lifecycle/port-guard.test.ts +20 -164
- package/src/server/lib/lifecycle/port-guard.ts +39 -7
- package/src/server/lib/lifecycle/probe-health.test.ts +166 -0
- package/src/server/lib/lifecycle/probe.test.ts +77 -164
- package/src/server/lib/lifecycle/probe.ts +121 -13
- package/src/server/lib/omp/rpc/frame-fold-signals.test.ts +112 -0
- package/src/server/lib/omp/rpc/frame-fold.ts +10 -5
- package/src/server/lib/omp/session/sidebar-activity.server.test.ts +162 -0
- package/src/server/lib/omp/session/sidebar-activity.server.ts +70 -0
- package/src/server/lib/omp/session/sidebar-data.canonical.test.ts +155 -0
- package/src/server/lib/omp/session/sidebar-data.server.ts +28 -1
- package/src/server/lib/omp/session/telemetry/compute.ts +13 -1
- package/src/server/lib/omp/session/telemetry.test.ts +33 -0
- package/src/server/lib/panels/local-edits.server.ts +33 -0
- package/src/server/lib/panels/sync.fixture.test-util.ts +92 -0
- package/src/server/lib/panels/sync.local-edits.test.ts +150 -0
- package/src/server/lib/panels/sync.server.test.ts +125 -147
- package/src/server/lib/panels/sync.server.ts +153 -48
- package/src/server/lib/realtime/signals.server.ts +10 -0
- package/src/server/lib/realtime/topics/catalog.server.ts +4 -1
- package/src/server/lib/realtime/topics.server.ts +16 -0
- package/src/server/lib/updates/shell-assets.test.ts +114 -0
- package/src/server/lib/updates/shell-assets.ts +200 -0
- package/src/server/lib/updates/shell-cache.ts +105 -0
- package/src/server/lib/updates/shell-pages.test.ts +187 -0
- package/src/server/lib/updates/shell-pages.ts +157 -0
- package/src/server/lib/updates/shell-release-desktop.test.ts +166 -0
- package/src/server/lib/updates/shell-release.test.ts +59 -71
- package/src/server/lib/updates/shell-release.ts +178 -198
- package/src/server/routes/fs/action-create.test.ts +18 -13
- package/src/server/routes/fs/action-nested.test.ts +184 -0
- package/src/server/routes/fs/action.ts +47 -16
- package/src/server/routes/fs/git-merge.test.ts +11 -1
- package/src/server/routes/fs/read.test.ts +24 -7
- package/src/server/routes/fs/scope.test.ts +15 -10
- package/src/server/routes/fs/scope.ts +3 -14
- package/src/server/routes/index.ts +2 -0
- package/src/server/routes/updates/android.ts +6 -1
- package/src/server/routes/updates/desktop.ts +59 -0
- package/src/server/routes/updates/index.ts +2 -0
- package/src/server/routes/updates/ios.ts +5 -1
- package/src/server/routes/worktrees/create.ts +138 -0
- package/src/server/routes/worktrees/index.ts +25 -0
- package/src/server/routes/worktrees/list.ts +134 -0
- package/src/server/routes/worktrees/remove.ts +102 -0
- package/src/server/routes/worktrees/routes.test.ts +326 -0
- package/src/server/routes/worktrees/shared.ts +90 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.test.ts +36 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.ts +18 -0
- package/src/shared/lib/chat/timeline/run-signals.test.ts +2 -0
- package/src/shared/lib/code/editor/comment-syntax.test.ts +37 -0
- package/src/shared/lib/code/editor/comment-syntax.ts +55 -0
- package/src/shared/lib/code/editor/find-replace.test.ts +134 -0
- package/src/shared/lib/code/editor/{find.ts → find-replace.ts} +23 -107
- package/src/shared/lib/code/editor/{find.test.ts → find-scan.test.ts} +19 -73
- package/src/shared/lib/code/editor/find-scan.ts +177 -0
- package/src/shared/lib/code/editor/keymap.ts +3 -3
- package/src/shared/lib/code/editor/lines.test.ts +7 -55
- package/src/shared/lib/code/editor/lines.ts +8 -48
- package/src/shared/lib/code/editor/marks.test.ts +4 -5
- package/src/shared/lib/code/editor/marks.ts +13 -25
- package/src/shared/lib/code/editor/theme-loader.ts +97 -0
- package/src/shared/lib/code/editor/themes.generated.ts +156 -0
- package/src/shared/lib/code/editor/themes.test.ts +165 -0
- package/src/shared/lib/code/editor/themes.ts +165 -0
- package/src/shared/lib/code/highlight-memo.test.ts +103 -0
- package/src/shared/lib/code/highlight-memo.ts +149 -0
- package/src/shared/lib/code/highlighter-warmup.ts +112 -0
- package/src/shared/lib/code/highlighter.ts +3 -1
- package/src/shared/lib/code/idle.ts +37 -0
- package/src/shared/lib/code/outline/rows.test.ts +96 -0
- package/src/shared/lib/code/outline/rows.ts +94 -0
- package/src/shared/lib/code/syntax-highlight.test.ts +1 -79
- package/src/shared/lib/code/syntax-highlight.ts +2 -3
- package/src/shared/lib/fs/file-kind-classify.test.ts +25 -11
- package/src/shared/lib/fs/file-kind.ts +47 -13
- package/src/shared/lib/fs/file-tree.test.ts +183 -0
- package/src/shared/lib/fs/file-tree.ts +159 -0
- package/src/shared/lib/fs/file-url.test.ts +52 -0
- package/src/shared/lib/fs/file-url.ts +37 -0
- package/src/shared/lib/fs/patch-sides.test.ts +114 -0
- package/src/shared/lib/fs/patch-sides.ts +139 -0
- package/src/shared/lib/fs/paths.test.ts +24 -1
- package/src/shared/lib/fs/paths.ts +17 -0
- package/src/shared/lib/fs/worktree/settings-keys.ts +18 -0
- package/src/shared/lib/omp/session/prewarm.ts +7 -0
- package/src/shared/lib/realtime/protocol.ts +16 -0
- package/src/shared/lib/ui/toolbar-overflow.test.ts +83 -0
- package/src/shared/lib/ui/toolbar-overflow.ts +86 -0
- package/src/shared/lib/updates/shell-arch.ts +54 -0
- package/src/shared/lib/updates/shell-ua.ts +24 -4
- package/src/shared/lib/workspace/active-project.ts +23 -0
- package/src/shared/lib/workspace/editor-view-mode.test.ts +70 -0
- package/src/shared/lib/workspace/editor-view-mode.ts +48 -0
- package/src/shared/lib/workspace/panel-widths.test.ts +30 -0
- package/src/shared/lib/workspace/panel-widths.ts +25 -5
- package/src/shared/lib/workspace/session-state/cache.ts +159 -0
- package/src/shared/lib/workspace/session-state/harness.test-util.ts +55 -0
- package/src/shared/lib/workspace/session-state/merge.ts +100 -0
- package/src/shared/lib/workspace/session-state/store-load.test.ts +206 -0
- package/src/shared/lib/workspace/session-state/store.test.ts +17 -139
- package/src/shared/lib/workspace/session-state/store.ts +92 -130
- package/src/shared/lib/workspace/workspace-modules.test.ts +49 -1
- package/src/shared/lib/workspace/workspace-panel-widths.ts +124 -0
- package/src/shared/lib/workspace/worktree/branch-name.test.ts +95 -0
- package/src/shared/lib/workspace/worktree/branch-name.ts +115 -0
- package/src/shared/lib/workspace/worktree/types.ts +104 -0
- package/src/shared/types/fs.ts +10 -0
- package/src/shared/types/realtime.ts +29 -0
- package/src/shared/types/settings/state.ts +10 -0
- package/src/shared/types/workspace.ts +14 -0
- package/src/test-support/file-explorer-disk.test-util.ts +76 -0
- package/src/test-support/file-explorer.test-util.ts +71 -40
- package/src/test-support/loopback.ts +183 -0
- package/dist/client/static/js/index-nzyjf92m.css +0 -1
- package/src/client/components/common/code-editor/handler.test.ts +0 -274
- package/src/client/components/common/code-editor/handler.ts +0 -175
- package/src/client/components/common/code-editor/index.tsx +0 -314
- package/src/client/components/common/code-surface/measure.ts +0 -148
- package/src/client/components/workspace/diff-panel/SplitView.tsx +0 -209
- package/src/client/components/workspace/diff-panel/UnifiedView.tsx +0 -150
- package/src/client/hooks/editor/scroll-container.test.ts +0 -102
- package/src/client/hooks/editor/scroll-container.ts +0 -26
- package/src/client/hooks/editor/use-code-window.ts +0 -100
- package/src/client/hooks/editor/use-editor-history.test.ts +0 -216
- package/src/client/hooks/editor/use-editor-history.ts +0 -134
- package/src/client/hooks/editor/use-occurrences.test.ts +0 -236
- package/src/client/hooks/editor/use-occurrences.ts +0 -98
- package/src/shared/lib/code/editor/commands.test.ts +0 -155
- package/src/shared/lib/code/editor/commands.ts +0 -260
- package/src/shared/lib/code/editor/edits.test.ts +0 -100
- package/src/shared/lib/code/editor/edits.ts +0 -122
- package/src/shared/lib/code/editor/history.test.ts +0 -118
- package/src/shared/lib/code/editor/history.ts +0 -101
- package/src/shared/lib/code/editor/layers.test.ts +0 -77
- package/src/shared/lib/code/editor/layers.ts +0 -67
- package/src/shared/lib/code/editor/line-edits.test.ts +0 -131
- package/src/shared/lib/code/editor/line-edits.ts +0 -159
- package/src/shared/lib/code/editor/occurrences.test.ts +0 -117
- package/src/shared/lib/code/editor/occurrences.ts +0 -111
- package/src/shared/lib/code/editor/reset-css.test.ts +0 -35
- package/src/shared/lib/code/editor/reset-css.ts +0 -32
- package/src/shared/lib/code/editor/text-commands.test.ts +0 -178
- package/src/shared/lib/code/editor/text-commands.ts +0 -154
- package/src/shared/lib/code/lazy-window.test.ts +0 -161
- package/src/shared/lib/code/lazy-window.ts +0 -156
- package/src/shared/lib/code/windowed-highlight.ts +0 -114
- package/src/shared/lib/fs/diff-parser.test.ts +0 -116
- package/src/shared/lib/fs/diff-parser.ts +0 -213
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The native shell's update/install control, shared by both sidebar layouts.
|
|
8
|
+
*
|
|
9
|
+
* ## Two controls, because there are two different jobs
|
|
10
|
+
*
|
|
11
|
+
* - **A link** when the console cannot name a file: a desktop shell that did not
|
|
12
|
+
* report its architecture, or any offer whose artifact is not installable from
|
|
13
|
+
* inside. Following it is the browser's (or the WebView's) job.
|
|
14
|
+
* - **A button** when the shell CAN install it: the click opens a CONFIRMATION, and
|
|
15
|
+
* the confirmation asks the shell to download, verify and replace itself. That is
|
|
16
|
+
* the desktop self-update, and it is the whole reason this component exists.
|
|
17
|
+
*
|
|
18
|
+
* The decision is `canSelfUpdate`, never a check on the platform at the call site:
|
|
19
|
+
* Android installs through `PackageInstaller` (its own path, unchanged), iOS cannot
|
|
20
|
+
* install at all, and a browser has no shell to ask.
|
|
21
|
+
*
|
|
22
|
+
* ## The control is ONLY a control
|
|
23
|
+
*
|
|
24
|
+
* Every word a run has to say — the confirmation, the progress, the shell's own
|
|
25
|
+
* refusal — is in `Modal.tsx`. The footer is a 24px row of glyphs, and a status
|
|
26
|
+
* sentence pushed into it moved the neighbouring controls and read as a status
|
|
27
|
+
* line for the SIDEBAR rather than for the update.
|
|
28
|
+
*
|
|
29
|
+
* ## The state lives here, not in either footer
|
|
30
|
+
*
|
|
31
|
+
* Both layouts draw this control and each unmounts at a different moment (a phone
|
|
32
|
+
* closes its drawer, a desktop swaps sessions). One hook, one vocabulary of
|
|
33
|
+
* phases, so the two cannot report the same run differently.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { useState } from 'preact/hooks';
|
|
37
|
+
import { createPortal } from 'preact/compat';
|
|
38
|
+
import { Download, Loader2 } from 'lucide-preact';
|
|
39
|
+
import {
|
|
40
|
+
shellDownloadLabel,
|
|
41
|
+
shellDownloadShortLabel,
|
|
42
|
+
type ShellDownload,
|
|
43
|
+
} from '@/client/hooks/ui/updates/shell-download';
|
|
44
|
+
import { canSelfUpdate, useShellUpdate } from '@/client/hooks/ui/updates/shell-update';
|
|
45
|
+
import { ShellUpdateModal } from '@/client/components/common/shell-update-button/Modal';
|
|
46
|
+
|
|
47
|
+
interface ShellUpdateButtonProps {
|
|
48
|
+
download: ShellDownload;
|
|
49
|
+
/** `icon` for the desktop footer's glyph row, `label` for the phone's. */
|
|
50
|
+
variant: 'icon' | 'label';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const ICON_CLASS = 'p-1 rounded hover:text-ink hover:bg-ink/5 cursor-pointer transition-colors';
|
|
54
|
+
|
|
55
|
+
export function ShellUpdateButton({ download, variant }: ShellUpdateButtonProps) {
|
|
56
|
+
const { phase, message, apply, reset, working } = useShellUpdate();
|
|
57
|
+
// The dialog is open from the CLICK until it is dismissed, and the run's phases
|
|
58
|
+
// are drawn inside it. A closed dialog whose run is still `working` is not a
|
|
59
|
+
// state this component can be in: `confirm` is what opens it.
|
|
60
|
+
const [open, setOpen] = useState(false);
|
|
61
|
+
const label = shellDownloadLabel(download);
|
|
62
|
+
|
|
63
|
+
if (!canSelfUpdate(download)) {
|
|
64
|
+
// A link, and `target="_blank"` hands it to the system browser so the console
|
|
65
|
+
// is not replaced by a file download.
|
|
66
|
+
return (
|
|
67
|
+
<a
|
|
68
|
+
href={download.url}
|
|
69
|
+
target="_blank"
|
|
70
|
+
rel="noopener noreferrer"
|
|
71
|
+
title={label}
|
|
72
|
+
aria-label={label}
|
|
73
|
+
className={ICON_CLASS}
|
|
74
|
+
>
|
|
75
|
+
{variant === 'icon' ? <Download size={16} /> : <span>{shellDownloadShortLabel(download)}</span>}
|
|
76
|
+
</a>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<>
|
|
82
|
+
<button
|
|
83
|
+
type="button"
|
|
84
|
+
// A dismissed run is not a run: reopening starts from the confirmation, not
|
|
85
|
+
// from the previous attempt's refusal.
|
|
86
|
+
onClick={() => {
|
|
87
|
+
reset();
|
|
88
|
+
setOpen(true);
|
|
89
|
+
}}
|
|
90
|
+
disabled={working}
|
|
91
|
+
title={label}
|
|
92
|
+
aria-label={label}
|
|
93
|
+
className={`${ICON_CLASS} disabled:cursor-not-allowed disabled:opacity-60 inline-flex items-center gap-1`}
|
|
94
|
+
>
|
|
95
|
+
{working ? (
|
|
96
|
+
<Loader2 size={variant === 'icon' ? 16 : 13} className="animate-spin motion-reduce:animate-none" />
|
|
97
|
+
) : (
|
|
98
|
+
<Download size={variant === 'icon' ? 16 : 13} />
|
|
99
|
+
)}
|
|
100
|
+
{variant === 'label' && <span>{shellDownloadShortLabel(download)}</span>}
|
|
101
|
+
</button>
|
|
102
|
+
|
|
103
|
+
{open &&
|
|
104
|
+
// A PORTAL, not a child of this row. The footer is a 24px flex row of
|
|
105
|
+
// glyphs, so a dialog rendered inside it is nested in a control strip: its
|
|
106
|
+
// text joins the row's own text content (measured in the browser — the row
|
|
107
|
+
// read as the dialog's body), and any `overflow`/`transform` added to that
|
|
108
|
+
// row later would clip a `fixed` overlay. The portal is what makes "the
|
|
109
|
+
// footer says nothing about a run" true rather than merely visually true.
|
|
110
|
+
createPortal(
|
|
111
|
+
<ShellUpdateModal
|
|
112
|
+
download={download}
|
|
113
|
+
phase={phase === 'idle' ? 'confirm' : phase}
|
|
114
|
+
message={message}
|
|
115
|
+
onConfirm={() => void apply(download)}
|
|
116
|
+
// Closing mid-run hides the dialog but does not cancel the shell: it is
|
|
117
|
+
// already replacing itself, and a Cancel that could not stop it would be
|
|
118
|
+
// a button that lies. The control stays disabled until the run answers.
|
|
119
|
+
onClose={() => setOpen(false)}
|
|
120
|
+
/>,
|
|
121
|
+
document.body,
|
|
122
|
+
)}
|
|
123
|
+
</>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The toolbar stays ONE row, and what does not fit is one click away.
|
|
8
|
+
*
|
|
9
|
+
* The failure this pins is a row that becomes two: the editor panel is 320px
|
|
10
|
+
* wide on a 1440px screen, and the width-mode buttons pushed ten controls past
|
|
11
|
+
* it. Wrapping moved the document down by the toolbar's own height and read as
|
|
12
|
+
* a second toolbar; a horizontal scroll clipped the tail invisibly. Neither is
|
|
13
|
+
* visible to a state-only assertion, so the cases below are about what is
|
|
14
|
+
* RENDERED and what the `⋯` menu offers.
|
|
15
|
+
*
|
|
16
|
+
* happy-dom lays nothing out, so `getBoundingClientRect` and `clientWidth` are
|
|
17
|
+
* stubbed at the PROTOTYPE level — the rule reads exactly those two values, and
|
|
18
|
+
* stubbing them before the first render is what lets the component's own layout
|
|
19
|
+
* effect measure a real row instead of a row of zeros.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'bun:test';
|
|
23
|
+
import { Window } from 'happy-dom';
|
|
24
|
+
import { h, render } from 'preact';
|
|
25
|
+
import { act } from 'preact/test-utils';
|
|
26
|
+
import { Save, Search, WrapText } from 'lucide-preact';
|
|
27
|
+
import { ToolbarRow, type ToolbarCluster } from '@/client/components/common/toolbar-row';
|
|
28
|
+
import { installDomGlobals, restoreDomGlobals } from '@/test-support/pristine-globals';
|
|
29
|
+
|
|
30
|
+
let container: HTMLElement;
|
|
31
|
+
/** What the stubbed layout reports for the row, and for each cluster in order. */
|
|
32
|
+
let rowWidth = 0;
|
|
33
|
+
let clusterWidths: number[] = [];
|
|
34
|
+
|
|
35
|
+
/** Captured inside `beforeAll`: the DOM globals do not exist at module load. */
|
|
36
|
+
let originalRect: typeof Element.prototype.getBoundingClientRect;
|
|
37
|
+
let originalClientWidth: PropertyDescriptor | undefined;
|
|
38
|
+
|
|
39
|
+
/** The row is the flex container the component renders; clusters carry a marker. */
|
|
40
|
+
function isRow(el: HTMLElement): boolean {
|
|
41
|
+
return el.className.includes('flex-nowrap');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function rectOf(width: number): DOMRect {
|
|
45
|
+
return { width, height: 0, top: 0, left: 0, right: width, bottom: 0, x: 0, y: 0, toJSON: () => ({}) } as DOMRect;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
beforeAll(() => {
|
|
49
|
+
installDomGlobals(new Window({ url: 'http://localhost' }));
|
|
50
|
+
originalRect = Element.prototype.getBoundingClientRect;
|
|
51
|
+
originalClientWidth = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'clientWidth');
|
|
52
|
+
Element.prototype.getBoundingClientRect = function (this: Element) {
|
|
53
|
+
const el = this as HTMLElement;
|
|
54
|
+
if (el.hasAttribute?.('data-toolbar-cluster')) {
|
|
55
|
+
const siblings = [...(el.parentElement?.querySelectorAll('[data-toolbar-cluster]') ?? [])];
|
|
56
|
+
return rectOf(clusterWidths[siblings.indexOf(el)] ?? 0);
|
|
57
|
+
}
|
|
58
|
+
if (isRow(el)) return rectOf(rowWidth);
|
|
59
|
+
return originalRect.call(this);
|
|
60
|
+
};
|
|
61
|
+
Object.defineProperty(HTMLElement.prototype, 'clientWidth', {
|
|
62
|
+
configurable: true,
|
|
63
|
+
get(this: HTMLElement) {
|
|
64
|
+
if (isRow(this)) return rowWidth;
|
|
65
|
+
return originalClientWidth?.get?.call(this) ?? 0;
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
afterEach(() => {
|
|
71
|
+
if (container) {
|
|
72
|
+
render(null, container);
|
|
73
|
+
container.remove();
|
|
74
|
+
}
|
|
75
|
+
rowWidth = 0;
|
|
76
|
+
clusterWidths = [];
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
afterAll(() => {
|
|
80
|
+
Element.prototype.getBoundingClientRect = originalRect;
|
|
81
|
+
if (originalClientWidth) Object.defineProperty(HTMLElement.prototype, 'clientWidth', originalClientWidth);
|
|
82
|
+
restoreDomGlobals();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
/** Three clusters, the first pinned — the editor toolbar in miniature. */
|
|
86
|
+
function clusters(onSelect: string[] = []): ToolbarCluster[] {
|
|
87
|
+
const make = (id: string, label: string, icon: typeof Save, pinned?: boolean): ToolbarCluster => ({
|
|
88
|
+
id,
|
|
89
|
+
pinned,
|
|
90
|
+
actions: [{ label, icon, onSelect: () => onSelect.push(label) }],
|
|
91
|
+
children: h('button', { type: 'button', 'aria-label': label }, label),
|
|
92
|
+
});
|
|
93
|
+
return [
|
|
94
|
+
make('save', 'Save file', Save, true),
|
|
95
|
+
make('find', 'Find and replace', Search),
|
|
96
|
+
make('wrap', 'Word wrap on', WrapText),
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function mount(width: number, widths: number[], onSelect: string[] = []): Promise<void> {
|
|
101
|
+
rowWidth = width;
|
|
102
|
+
clusterWidths = widths;
|
|
103
|
+
container = document.createElement('div');
|
|
104
|
+
document.body.appendChild(container);
|
|
105
|
+
await act(async () => {
|
|
106
|
+
render(h(ToolbarRow, { clusters: clusters(onSelect) }), container);
|
|
107
|
+
});
|
|
108
|
+
// The layout effect commits its measurement in a second pass, the way the
|
|
109
|
+
// browser gives one before paint.
|
|
110
|
+
await act(async () => {});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function rowButtons(): HTMLButtonElement[] {
|
|
114
|
+
const host = container.firstElementChild as HTMLElement;
|
|
115
|
+
return [...host.querySelectorAll<HTMLButtonElement>(':scope > div > button')];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function openMenu(): Promise<void> {
|
|
119
|
+
const trigger = rowButtons().find((button) => button.getAttribute('aria-label') === 'More actions');
|
|
120
|
+
if (!trigger) throw new Error('no overflow trigger rendered');
|
|
121
|
+
await act(async () => {
|
|
122
|
+
trigger.dispatchEvent(new window.MouseEvent('click', { bubbles: true }) as never);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
describe('ToolbarRow overflow', () => {
|
|
127
|
+
test('a wide row draws every cluster and no trigger', async () => {
|
|
128
|
+
await mount(400, [60, 60, 60]);
|
|
129
|
+
expect(rowButtons().map((button) => button.getAttribute('aria-label'))).toEqual([
|
|
130
|
+
'Save file',
|
|
131
|
+
'Find and replace',
|
|
132
|
+
'Word wrap on',
|
|
133
|
+
]);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('a narrow row keeps the pinned cluster and moves the rest into `⋯`', async () => {
|
|
137
|
+
await mount(120, [60, 60, 60]);
|
|
138
|
+
const labels = rowButtons().map((button) => button.getAttribute('aria-label'));
|
|
139
|
+
expect(labels).toContain('Save file');
|
|
140
|
+
expect(labels).toContain('More actions');
|
|
141
|
+
expect(labels).not.toContain('Word wrap on');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('the menu offers the hidden controls, named in words', async () => {
|
|
145
|
+
await mount(120, [60, 60, 60]);
|
|
146
|
+
await openMenu();
|
|
147
|
+
const items = [...container.querySelectorAll('[role="menuitem"]')].map((item) => item.textContent?.trim());
|
|
148
|
+
expect(items).toContain('Word wrap on');
|
|
149
|
+
// The pinned control is on the row, so it is NOT in the menu.
|
|
150
|
+
expect(items).not.toContain('Save file');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('a menu row runs the same action the button would', async () => {
|
|
154
|
+
const ran: string[] = [];
|
|
155
|
+
await mount(120, [60, 60, 60], ran);
|
|
156
|
+
await openMenu();
|
|
157
|
+
const item = [...container.querySelectorAll<HTMLButtonElement>('[role="menuitem"]')].find(
|
|
158
|
+
(row) => row.textContent?.trim() === 'Word wrap on',
|
|
159
|
+
);
|
|
160
|
+
if (!item) throw new Error('the wrap row is missing from the menu');
|
|
161
|
+
await act(async () => {
|
|
162
|
+
item.dispatchEvent(new window.MouseEvent('click', { bubbles: true }) as never);
|
|
163
|
+
});
|
|
164
|
+
expect(ran).toEqual(['Word wrap on']);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* One toolbar row that never becomes two.
|
|
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 — a clipped
|
|
13
|
+
* control is invisible. So the clusters that do not fit move into a `⋯` menu and
|
|
14
|
+
* every control stays one click away.
|
|
15
|
+
*
|
|
16
|
+
* Widths are MEASURED, never tabulated: the row lays every cluster out once and
|
|
17
|
+
* reads each one's rendered width, then hides what does not fit. A table of
|
|
18
|
+
* guessed pixel widths drifts the moment a label, an icon or a font changes, and
|
|
19
|
+
* a wrong guess here is a control that is either clipped or needlessly buried.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
|
|
23
|
+
import type { ComponentChildren } from 'preact';
|
|
24
|
+
import { Ellipsis, type LucideIcon } from 'lucide-preact';
|
|
25
|
+
import { useOnClickOutside } from '@/client/hooks/ui/on-click-outside';
|
|
26
|
+
import { toolbarHasOverflow, toolbarVisibleClusters } from '@/shared/lib/ui/toolbar-overflow';
|
|
27
|
+
|
|
28
|
+
/** One row the `⋯` menu offers when its cluster is not on the row. */
|
|
29
|
+
export interface ToolbarOverflowAction {
|
|
30
|
+
label: string;
|
|
31
|
+
icon: LucideIcon;
|
|
32
|
+
onSelect: () => void;
|
|
33
|
+
/** Rendered as a pressed row (word wrap on, a mode already active). */
|
|
34
|
+
active?: boolean;
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A logical group of controls that hides as a unit.
|
|
40
|
+
*
|
|
41
|
+
* A group rather than a single button because the clusters are already grouped
|
|
42
|
+
* in the toolbars (zoom is two buttons, copy/download is a pair, the width modes
|
|
43
|
+
* are a pair) and splitting them would let half of a pair stay on the row.
|
|
44
|
+
*/
|
|
45
|
+
export interface ToolbarCluster {
|
|
46
|
+
id: string;
|
|
47
|
+
/**
|
|
48
|
+
* Never leave the row, whatever its width.
|
|
49
|
+
*
|
|
50
|
+
* The visual order is not the priority order: Save is drawn first because that
|
|
51
|
+
* is where a reader looks for it, while the width modes sit last and are the
|
|
52
|
+
* reason the panel exists. Pinning says which ones a menu row must not be the
|
|
53
|
+
* only way to reach.
|
|
54
|
+
*/
|
|
55
|
+
pinned?: boolean;
|
|
56
|
+
/** What the `⋯` menu offers when this cluster is hidden. */
|
|
57
|
+
actions: ToolbarOverflowAction[];
|
|
58
|
+
children: ComponentChildren;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface ToolbarRowProps {
|
|
62
|
+
clusters: ToolbarCluster[];
|
|
63
|
+
className?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function ToolbarRow({ clusters, className = '' }: ToolbarRowProps) {
|
|
67
|
+
const rowRef = useRef<HTMLDivElement>(null);
|
|
68
|
+
/** Measured widths, tagged with the cluster set they describe. */
|
|
69
|
+
const [metrics, setMetrics] = useState<{ signature: string; widths: number[] } | null>(null);
|
|
70
|
+
const [available, setAvailable] = useState(0);
|
|
71
|
+
|
|
72
|
+
// The set changes when a conditional cluster appears or disappears (a
|
|
73
|
+
// previewable file opened, a byte viewer took over), and stale widths would
|
|
74
|
+
// then hide the wrong cluster. Rendering every cluster until they are measured
|
|
75
|
+
// is what gives the ruler something to measure — and the layout effect runs
|
|
76
|
+
// BEFORE paint, so the unresolved frame is never seen.
|
|
77
|
+
const signature = clusters.map((cluster) => `${cluster.id}:${cluster.pinned ? 1 : 0}`).join('|');
|
|
78
|
+
const ready = metrics?.signature === signature;
|
|
79
|
+
const widths = ready ? metrics.widths : [];
|
|
80
|
+
const pinned = clusters.map((cluster) => cluster.pinned === true);
|
|
81
|
+
const visible = widths.length > 0
|
|
82
|
+
? toolbarVisibleClusters({ widths, pinned, available })
|
|
83
|
+
: clusters.map(() => true);
|
|
84
|
+
|
|
85
|
+
useLayoutEffect(() => {
|
|
86
|
+
const row = rowRef.current;
|
|
87
|
+
if (!row) return;
|
|
88
|
+
if (!ready) {
|
|
89
|
+
const children = [...row.querySelectorAll<HTMLElement>('[data-toolbar-cluster]')];
|
|
90
|
+
if (children.length === clusters.length) {
|
|
91
|
+
setMetrics({ signature, widths: children.map((child) => child.getBoundingClientRect().width) });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
setAvailable(row.clientWidth);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// The row's width changes on a window resize, a panel drag and a layout mode
|
|
98
|
+
// switch — none of which are `window` events.
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
const row = rowRef.current;
|
|
101
|
+
if (!row || typeof ResizeObserver === 'undefined') return;
|
|
102
|
+
const observer = new ResizeObserver(() => setAvailable(row.clientWidth));
|
|
103
|
+
observer.observe(row);
|
|
104
|
+
return () => observer.disconnect();
|
|
105
|
+
}, []);
|
|
106
|
+
|
|
107
|
+
const hiddenActions = clusters.filter((_, index) => !visible[index]).flatMap((cluster) => cluster.actions);
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div ref={rowRef} className={`flex flex-nowrap items-center justify-end gap-x-1 overflow-hidden ${className}`}>
|
|
111
|
+
{clusters.map((cluster, index) =>
|
|
112
|
+
visible[index] ? (
|
|
113
|
+
<div key={cluster.id} data-toolbar-cluster="" className="flex items-center flex-shrink-0">
|
|
114
|
+
{cluster.children}
|
|
115
|
+
</div>
|
|
116
|
+
) : null,
|
|
117
|
+
)}
|
|
118
|
+
{toolbarHasOverflow(visible) && <OverflowMenu actions={hiddenActions} />}
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The `⋯` trigger and its menu.
|
|
125
|
+
*
|
|
126
|
+
* A menu rather than a horizontal scroll: a clipped action is invisible, and the
|
|
127
|
+
* diff toolbar already learned that a scroll strip with no visible scrollbar
|
|
128
|
+
* reads as "there is nothing more here" (measured: 361px of content in a 296px
|
|
129
|
+
* box, with `Stage` entirely outside it).
|
|
130
|
+
*/
|
|
131
|
+
function OverflowMenu({ actions }: { actions: ToolbarOverflowAction[] }) {
|
|
132
|
+
const [open, setOpen] = useState(false);
|
|
133
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
134
|
+
useOnClickOutside(rootRef, () => setOpen(false));
|
|
135
|
+
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
if (!open) return;
|
|
138
|
+
const onKeyDown = (event: globalThis.KeyboardEvent) => {
|
|
139
|
+
if (event.key === 'Escape') setOpen(false);
|
|
140
|
+
};
|
|
141
|
+
document.addEventListener('keydown', onKeyDown);
|
|
142
|
+
return () => document.removeEventListener('keydown', onKeyDown);
|
|
143
|
+
}, [open]);
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<div className="relative flex-shrink-0" ref={rootRef}>
|
|
147
|
+
<button
|
|
148
|
+
type="button"
|
|
149
|
+
title="More actions"
|
|
150
|
+
aria-label="More actions"
|
|
151
|
+
aria-haspopup="menu"
|
|
152
|
+
aria-expanded={open}
|
|
153
|
+
onClick={() => setOpen(!open)}
|
|
154
|
+
className={`flex items-center justify-center p-1.5 rounded transition-colors cursor-pointer ${
|
|
155
|
+
open ? 'bg-ink/10 text-ink' : 'text-ink/50 hover:text-ink hover:bg-ink/5'
|
|
156
|
+
}`}
|
|
157
|
+
>
|
|
158
|
+
<Ellipsis size={14} />
|
|
159
|
+
</button>
|
|
160
|
+
|
|
161
|
+
{open && (
|
|
162
|
+
<div
|
|
163
|
+
role="menu"
|
|
164
|
+
className="absolute right-0 top-full mt-1 w-52 bg-paper border border-ink/15 rounded-lg shadow-lg z-50 py-1 text-xs"
|
|
165
|
+
>
|
|
166
|
+
{actions.map((action) => (
|
|
167
|
+
<button
|
|
168
|
+
key={action.label}
|
|
169
|
+
type="button"
|
|
170
|
+
role="menuitem"
|
|
171
|
+
disabled={action.disabled}
|
|
172
|
+
aria-pressed={action.active}
|
|
173
|
+
onClick={() => {
|
|
174
|
+
setOpen(false);
|
|
175
|
+
action.onSelect();
|
|
176
|
+
}}
|
|
177
|
+
className={`w-full text-left flex items-center gap-2 px-3 py-1.5 transition-colors disabled:cursor-default disabled:opacity-40 ${
|
|
178
|
+
action.active ? 'bg-ink/5 text-ink font-medium' : 'text-ink/75 hover:bg-ink/5 hover:text-ink'
|
|
179
|
+
}`}
|
|
180
|
+
>
|
|
181
|
+
<action.icon size={13} className="flex-shrink-0" />
|
|
182
|
+
<span className="truncate">{action.label}</span>
|
|
183
|
+
</button>
|
|
184
|
+
))}
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
187
|
+
</div>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* source of truth.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { FolderOpen, ListChecks, Pin, PinOff, Trash2 } from 'lucide-preact';
|
|
16
|
+
import { FolderOpen, GitBranch, ListChecks, Pin, PinOff, Trash2 } from 'lucide-preact';
|
|
17
17
|
|
|
18
18
|
interface WorkspaceOptionsMenuProps {
|
|
19
19
|
variant: 'desktop' | 'mobile';
|
|
@@ -23,6 +23,12 @@ interface WorkspaceOptionsMenuProps {
|
|
|
23
23
|
onOpenFolder?: () => void;
|
|
24
24
|
/** Enter the session batch-selection mode scoped to this workspace. */
|
|
25
25
|
onSelectSessions?: () => void;
|
|
26
|
+
/**
|
|
27
|
+
* Open the New worktree dialog for this workspace. Offered only for a folder
|
|
28
|
+
* bound to a repository: a worktree needs one, and a row that opens a dialog
|
|
29
|
+
* which can only refuse is a row that teaches the user nothing.
|
|
30
|
+
*/
|
|
31
|
+
onNewWorktree?: () => void;
|
|
26
32
|
confirmDelete: boolean;
|
|
27
33
|
onPin: () => void;
|
|
28
34
|
onDelete: () => void;
|
|
@@ -35,6 +41,7 @@ export function WorkspaceOptionsMenu({
|
|
|
35
41
|
isPinned = false,
|
|
36
42
|
onOpenFolder,
|
|
37
43
|
onSelectSessions,
|
|
44
|
+
onNewWorktree,
|
|
38
45
|
confirmDelete,
|
|
39
46
|
onPin,
|
|
40
47
|
onDelete,
|
|
@@ -71,6 +78,12 @@ export function WorkspaceOptionsMenu({
|
|
|
71
78
|
<span>Select sessions</span>
|
|
72
79
|
</button>
|
|
73
80
|
)}
|
|
81
|
+
{onNewWorktree && (
|
|
82
|
+
<button type="button" onClick={onNewWorktree} className="w-full text-left px-3 py-2 hover:bg-ink/5 text-ink/80 flex items-center space-x-2">
|
|
83
|
+
<GitBranch size={13} />
|
|
84
|
+
<span>New worktree…</span>
|
|
85
|
+
</button>
|
|
86
|
+
)}
|
|
74
87
|
<button type="button" onClick={onRequestDelete} className="w-full text-left px-3 py-2 hover:bg-error/10 text-error flex items-center space-x-2">
|
|
75
88
|
<Trash2 size={13} />
|
|
76
89
|
<span>Delete Workspace</span>
|
|
@@ -105,6 +118,11 @@ export function WorkspaceOptionsMenu({
|
|
|
105
118
|
<ListChecks size={12} /><span>Select sessions</span>
|
|
106
119
|
</button>
|
|
107
120
|
)}
|
|
121
|
+
{onNewWorktree && (
|
|
122
|
+
<button type="button" onClick={onNewWorktree} className="w-full text-left px-3 py-1.5 hover:bg-ink/5 cursor-pointer flex items-center space-x-2 text-ink/80">
|
|
123
|
+
<GitBranch size={12} /><span>New worktree…</span>
|
|
124
|
+
</button>
|
|
125
|
+
)}
|
|
108
126
|
<button type="button" onClick={onRequestDelete} className="w-full text-left px-3 py-1.5 hover:bg-ink/5 cursor-pointer flex items-center space-x-2 text-error">
|
|
109
127
|
<Trash2 size={12} /><span>Delete Workspace</span>
|
|
110
128
|
</button>
|
|
@@ -12,8 +12,7 @@ import { titlebarPaddingLeft, titlebarPaddingRight } from '@/shared/lib/ui/title
|
|
|
12
12
|
import { ServerSwitcher } from '@/client/components/common/server-switcher';
|
|
13
13
|
import { useDesktopServers } from '@/client/hooks/desktop/servers';
|
|
14
14
|
import { isDesktopShell } from '@/client/lib/desktop/bridge';
|
|
15
|
-
import { dragRegionProps,
|
|
16
|
-
import { WindowControls } from '@/client/components/common/window-controls';
|
|
15
|
+
import { dragRegionProps, titlebarInsetLeft } from '@/client/lib/desktop/titlebar';
|
|
17
16
|
import { useOnClickOutside } from '@/client/hooks/ui/on-click-outside';
|
|
18
17
|
|
|
19
18
|
interface TopNavbarProps {
|
|
@@ -203,10 +202,10 @@ export function TopNavbar({
|
|
|
203
202
|
onToggleEditor={onToggleEditor}
|
|
204
203
|
onSwitchToMobile={onSwitchToMobile}
|
|
205
204
|
/>
|
|
206
|
-
{/* The Windows
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
205
|
+
{/* The Windows/Linux caption buttons come from `HeaderControls` itself:
|
|
206
|
+
a desktop shell window can be narrower than the mobile breakpoint,
|
|
207
|
+
so the phone header has to draw them too — and a control that only
|
|
208
|
+
one layout renders is a control the other layout's window loses. */}
|
|
210
209
|
</div>
|
|
211
210
|
|
|
212
211
|
{menu.anchor && sessionActions && (
|