@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
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
EDITOR_MIN_FONT_SIZE,
|
|
22
22
|
} from '@/shared/lib/code/editor/typography';
|
|
23
23
|
import { useEditorFontStack } from '@/client/hooks/editor/font';
|
|
24
|
+
import { useEditorTheme } from '@/client/hooks/editor/theme';
|
|
24
25
|
import { MOBILE_SCROLL_POSITIONS_KEY } from '@/shared/lib/workspace/scroll-positions';
|
|
25
26
|
import { ViewerSurface, hasViewerSurface } from '@/client/components/workspace/editor/viewers/dispatch';
|
|
26
27
|
import { BINARY_VIEWERS, RENDERED_VIEWERS, VIEWER_LABELS, viewerKindFor } from '@/shared/lib/fs/document-kind';
|
|
@@ -49,6 +50,7 @@ export function MobileFullEditor({ file, missing = false, onClose, onFileSaved,
|
|
|
49
50
|
const [fontSize, setFontSize] = useState(EDITOR_DEFAULT_FONT_SIZE);
|
|
50
51
|
const [wordWrap, setWordWrap] = useState(true);
|
|
51
52
|
const editorFontFamily = useEditorFontStack();
|
|
53
|
+
const editorTheme = useEditorTheme();
|
|
52
54
|
|
|
53
55
|
const isMd = viewerKind === 'markdown';
|
|
54
56
|
// A byte viewer (image, PDF, DOCX, workbook) owns the whole surface and has
|
|
@@ -76,6 +78,12 @@ export function MobileFullEditor({ file, missing = false, onClose, onFileSaved,
|
|
|
76
78
|
missing,
|
|
77
79
|
});
|
|
78
80
|
|
|
81
|
+
/**
|
|
82
|
+
* The code surface's scroller, which belongs to CodeMirror and appears only
|
|
83
|
+
* once its view mounts.
|
|
84
|
+
*/
|
|
85
|
+
const [codeScroller, setCodeScroller] = useState<HTMLElement | null>(null);
|
|
86
|
+
|
|
79
87
|
// Closing and reopening the same file is a remount, so the offset is restored
|
|
80
88
|
// from the session's own state — the phone shows one file at a time, and its
|
|
81
89
|
// key is that file's path (a rendered view keeps its own offset, as on the
|
|
@@ -88,6 +96,9 @@ export function MobileFullEditor({ file, missing = false, onClose, onFileSaved,
|
|
|
88
96
|
// and the desktop drops entries when a tab closes — against ids that mean
|
|
89
97
|
// nothing here.
|
|
90
98
|
slot: MOBILE_SCROLL_POSITIONS_KEY,
|
|
99
|
+
// The code surface's scroller belongs to CodeMirror and appears only once
|
|
100
|
+
// its view mounts; the markdown preview owns its own container.
|
|
101
|
+
element: isMd && isPreview ? undefined : codeScroller,
|
|
91
102
|
});
|
|
92
103
|
|
|
93
104
|
// A search row on the phone opens this full-screen editor, so the hand-off
|
|
@@ -135,9 +146,15 @@ export function MobileFullEditor({ file, missing = false, onClose, onFileSaved,
|
|
|
135
146
|
// The sheet's own list, from the LIVE buffer — the same derivation the
|
|
136
147
|
// desktop section uses, so the two cannot disagree about what a state means.
|
|
137
148
|
// A byte viewer has no text buffer to scan, which is its own sentence.
|
|
138
|
-
const {
|
|
149
|
+
const {
|
|
150
|
+
symbols: outlineSymbols,
|
|
151
|
+
total: outlineTotal,
|
|
152
|
+
truncated: outlineTruncated,
|
|
153
|
+
notice: outlineNotice,
|
|
154
|
+
} = useOutlineView({
|
|
139
155
|
content,
|
|
140
156
|
language: lang,
|
|
157
|
+
path: file.path ?? null,
|
|
141
158
|
state: byteViewer ? 'not-a-document' : editor.loadError ? 'error' : editor.loaded ? 'ready' : 'loading',
|
|
142
159
|
note: byteViewer
|
|
143
160
|
? `${VIEWER_LABELS[viewerKind]} files have no text outline.`
|
|
@@ -148,6 +165,7 @@ export function MobileFullEditor({ file, missing = false, onClose, onFileSaved,
|
|
|
148
165
|
<div
|
|
149
166
|
className="fixed inset-x-0 top-0 h-dvh z-[60] bg-paper flex flex-col font-mono text-xs select-none"
|
|
150
167
|
{...drop.dropProps}
|
|
168
|
+
{...drop.captureDropProps}
|
|
151
169
|
>
|
|
152
170
|
<EditorDropOverlay visible={drop.isDragging} />
|
|
153
171
|
|
|
@@ -208,7 +226,9 @@ export function MobileFullEditor({ file, missing = false, onClose, onFileSaved,
|
|
|
208
226
|
content={editor.content}
|
|
209
227
|
revision={editor.revision}
|
|
210
228
|
/>
|
|
211
|
-
) : (
|
|
229
|
+
) : editor.isLoading || editor.unsupported || editor.loadError || (isMd && isPreview) ? (
|
|
230
|
+
// The states that draw their own scrolling content — and the markdown
|
|
231
|
+
// preview, which is a document, not an editor — share one scroller.
|
|
212
232
|
<div
|
|
213
233
|
ref={scroll.ref}
|
|
214
234
|
onScroll={scroll.onScroll}
|
|
@@ -233,7 +253,7 @@ export function MobileFullEditor({ file, missing = false, onClose, onFileSaved,
|
|
|
233
253
|
<span className="text-xs text-error font-sans">Could not read {file.name}</span>
|
|
234
254
|
<span className="text-[11px] text-ink/50 font-mono break-all">{editor.loadError}</span>
|
|
235
255
|
</div>
|
|
236
|
-
) :
|
|
256
|
+
) : (
|
|
237
257
|
/* Markdown Preview Mode — styled by `.prose-content` (the chat
|
|
238
258
|
timeline's system), see the desktop editor for why there is no
|
|
239
259
|
`prose` wrapper. */
|
|
@@ -244,42 +264,41 @@ export function MobileFullEditor({ file, missing = false, onClose, onFileSaved,
|
|
|
244
264
|
scope={{ path: file.path ?? file.name, root: file.root, repo: file.repo }}
|
|
245
265
|
/>
|
|
246
266
|
</div>
|
|
247
|
-
) : (
|
|
248
|
-
/* Code Editor with Line Numbers - Desktop styled: gutter bg-canvas border-r border-ink/10 */
|
|
249
|
-
<CodeSurface
|
|
250
|
-
ref={surfaceRef}
|
|
251
|
-
value={content}
|
|
252
|
-
onValueChange={editor.onChange}
|
|
253
|
-
language={lang}
|
|
254
|
-
wordWrap={wordWrap}
|
|
255
|
-
marks={revealMarks}
|
|
256
|
-
currentMark={currentMark}
|
|
257
|
-
rootClassName="flex min-h-full bg-paper"
|
|
258
|
-
// The gutter auto-sizes to its widest number rather than taking a
|
|
259
|
-
// fixed width: the numbers scale with the zoom control, and a
|
|
260
|
-
// pinned `w-10` clipped a 3-digit line number at 18px.
|
|
261
|
-
gutterClassName="flex flex-col text-right pr-2 select-none text-ink/30 bg-canvas border-r border-ink/10 font-mono flex-shrink-0"
|
|
262
|
-
gutterLineClassName="min-w-[1.5rem]"
|
|
263
|
-
gutterStyle={{
|
|
264
|
-
fontSize,
|
|
265
|
-
paddingTop: 12,
|
|
266
|
-
paddingBottom: 12,
|
|
267
|
-
lineHeight: EDITOR_LINE_HEIGHT,
|
|
268
|
-
fontFamily: editorFontFamily,
|
|
269
|
-
}}
|
|
270
|
-
editorWrapperClassName="flex-1 p-3 overflow-x-auto min-w-0 bg-paper text-ink code-surface"
|
|
271
|
-
editorClassName="focus:outline-none"
|
|
272
|
-
editorStyle={{
|
|
273
|
-
fontFamily: editorFontFamily,
|
|
274
|
-
fontSize,
|
|
275
|
-
lineHeight: EDITOR_LINE_HEIGHT,
|
|
276
|
-
backgroundColor: 'transparent',
|
|
277
|
-
minHeight: '100%',
|
|
278
|
-
color: 'var(--theme-ink)',
|
|
279
|
-
}}
|
|
280
|
-
/>
|
|
281
267
|
)}
|
|
282
268
|
</div>
|
|
269
|
+
) : (
|
|
270
|
+
/* The code surface owns its own scroller (CodeMirror's), so it is NOT
|
|
271
|
+
wrapped in one: an outer `overflow-auto` resolves the editor's
|
|
272
|
+
`height: 100%` against an auto-height box, so the editor grew to the
|
|
273
|
+
document's full height and the panel had nothing to scroll. */
|
|
274
|
+
<CodeSurface
|
|
275
|
+
ref={surfaceRef}
|
|
276
|
+
value={content}
|
|
277
|
+
onValueChange={editor.onChange}
|
|
278
|
+
language={lang}
|
|
279
|
+
wordWrap={wordWrap}
|
|
280
|
+
documentKey={fileKey}
|
|
281
|
+
marks={revealMarks}
|
|
282
|
+
currentMark={currentMark}
|
|
283
|
+
onScroller={setCodeScroller}
|
|
284
|
+
scrollTestId="editor-scroller"
|
|
285
|
+
// No `bg-paper`: an explicit editor theme paints its own surface, and
|
|
286
|
+
// the panel's colour under it would be a border of the wrong shade.
|
|
287
|
+
// `select-text` is the desktop pane's rule, and it matters more here:
|
|
288
|
+
// the sheet's own root is `select-none` (it is a header-and-status
|
|
289
|
+
// layout), and the editor is now a `contenteditable` whose selection
|
|
290
|
+
// and caret are the BROWSER's business — an inherited `user-select:
|
|
291
|
+
// none` is the one thing that can take that away on a phone.
|
|
292
|
+
className="flex-1 min-h-0 text-ink select-text"
|
|
293
|
+
themeVars={editorTheme.vars}
|
|
294
|
+
themes={editorTheme.themes}
|
|
295
|
+
padding={12}
|
|
296
|
+
style={{
|
|
297
|
+
fontFamily: editorFontFamily,
|
|
298
|
+
fontSize: `${fontSize}px`,
|
|
299
|
+
lineHeight: EDITOR_LINE_HEIGHT,
|
|
300
|
+
}}
|
|
301
|
+
/>
|
|
283
302
|
)}
|
|
284
303
|
|
|
285
304
|
{/* The outline is a floating control rather than a panel section: on a
|
|
@@ -289,7 +308,13 @@ export function MobileFullEditor({ file, missing = false, onClose, onFileSaved,
|
|
|
289
308
|
target over the content, list in a sheet. Hidden for a byte viewer,
|
|
290
309
|
which has no text to outline. */}
|
|
291
310
|
{!byteViewer && (
|
|
292
|
-
<MobileOutlineButton
|
|
311
|
+
<MobileOutlineButton
|
|
312
|
+
path={file.path ?? null}
|
|
313
|
+
symbols={outlineSymbols}
|
|
314
|
+
total={outlineTotal}
|
|
315
|
+
truncated={outlineTruncated}
|
|
316
|
+
emptyNotice={outlineNotice}
|
|
317
|
+
/>
|
|
293
318
|
)}
|
|
294
319
|
|
|
295
320
|
<EditorStatusBar
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The phone's outline sheet.
|
|
8
|
+
*
|
|
9
|
+
* Same two facts the desktop section shows, on a surface that cannot share its
|
|
10
|
+
* markup (the drawer has no room for a section pinned to a floor, so this is a
|
|
11
|
+
* floating button and a modal). Both are pinned here because each fails
|
|
12
|
+
* silently:
|
|
13
|
+
*
|
|
14
|
+
* - **the button names the TOTAL**, so a truncated list cannot read as "this
|
|
15
|
+
* file has 500 symbols";
|
|
16
|
+
* - **a truncated list says what it is not showing**, in the sheet, where the
|
|
17
|
+
* reader is actually looking.
|
|
18
|
+
*
|
|
19
|
+
* Rendered with `h()` against happy-dom. `Modal` is the shared shell every
|
|
20
|
+
* dialog in the app uses, so opening one here is the same code path a phone
|
|
21
|
+
* takes.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'bun:test';
|
|
25
|
+
import { Window } from 'happy-dom';
|
|
26
|
+
import { h, render } from 'preact';
|
|
27
|
+
import { act } from 'preact/test-utils';
|
|
28
|
+
import { MobileOutlineButton } from '@/client/components/mobile/mobile-right-sidebar/OutlineButton';
|
|
29
|
+
import { MAX_OUTLINE_ROWS, outlineTruncatedNotice } from '@/shared/lib/code/outline/rows';
|
|
30
|
+
import type { OutlineSymbol } from '@/shared/lib/code/outline/types';
|
|
31
|
+
import { installDomGlobals, restoreDomGlobals } from '@/test-support/pristine-globals';
|
|
32
|
+
|
|
33
|
+
let container: HTMLElement;
|
|
34
|
+
|
|
35
|
+
beforeAll(() => {
|
|
36
|
+
installDomGlobals(new Window({ url: 'http://localhost' }));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
if (container) {
|
|
41
|
+
render(null, container);
|
|
42
|
+
container.remove();
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
afterAll(() => {
|
|
47
|
+
restoreDomGlobals();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
function symbols(count: number): OutlineSymbol[] {
|
|
51
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
52
|
+
name: `fn${i}`,
|
|
53
|
+
kind: 'function' as const,
|
|
54
|
+
line: i + 1,
|
|
55
|
+
column: 0,
|
|
56
|
+
depth: 0,
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function mount(props: {
|
|
61
|
+
symbols: OutlineSymbol[];
|
|
62
|
+
total?: number;
|
|
63
|
+
truncated?: boolean;
|
|
64
|
+
path?: string | null;
|
|
65
|
+
emptyNotice?: string;
|
|
66
|
+
}): Promise<void> {
|
|
67
|
+
container = document.createElement('div');
|
|
68
|
+
document.body.appendChild(container);
|
|
69
|
+
await act(async () => {
|
|
70
|
+
render(
|
|
71
|
+
h(MobileOutlineButton, {
|
|
72
|
+
path: props.path === undefined ? 'src/a.ts' : props.path,
|
|
73
|
+
symbols: props.symbols,
|
|
74
|
+
total: props.total ?? props.symbols.length,
|
|
75
|
+
truncated: props.truncated ?? false,
|
|
76
|
+
emptyNotice: props.emptyNotice ?? 'No symbols found.',
|
|
77
|
+
}),
|
|
78
|
+
container,
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function trigger(): HTMLButtonElement {
|
|
84
|
+
const el = container.querySelector('button[aria-label^="Outline"]');
|
|
85
|
+
if (!el) throw new Error('no outline trigger rendered');
|
|
86
|
+
return el as HTMLButtonElement;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function open(): Promise<void> {
|
|
90
|
+
await act(async () => {
|
|
91
|
+
trigger().click();
|
|
92
|
+
});
|
|
93
|
+
for (let i = 0; i < 4; i += 1) await act(async () => {});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
describe('MobileOutlineButton', () => {
|
|
97
|
+
test('no path means no control — an attachment chip has nothing to outline', async () => {
|
|
98
|
+
await mount({ symbols: symbols(3), path: null });
|
|
99
|
+
|
|
100
|
+
expect(container.querySelector('button[aria-label^="Outline"]')).toBeNull();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('the trigger names the TOTAL, not the window', async () => {
|
|
104
|
+
await mount({ symbols: symbols(MAX_OUTLINE_ROWS), total: 63_618, truncated: true });
|
|
105
|
+
|
|
106
|
+
// The accessible name is what a screen reader announces and what a spec
|
|
107
|
+
// addresses; a window count here would describe the panel, not the file.
|
|
108
|
+
expect(trigger().getAttribute('aria-label')).toBe('Outline (63618 symbols)');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('a truncated sheet renders the window and says what it is not showing', async () => {
|
|
112
|
+
await mount({ symbols: symbols(MAX_OUTLINE_ROWS), total: 700, truncated: true });
|
|
113
|
+
await open();
|
|
114
|
+
|
|
115
|
+
const rows = container.querySelectorAll('button[aria-label*=", line "]');
|
|
116
|
+
expect(rows).toHaveLength(MAX_OUTLINE_ROWS);
|
|
117
|
+
expect(container.textContent).toContain(outlineTruncatedNotice(700));
|
|
118
|
+
// The header count reads window/total, so it cannot be mistaken for the
|
|
119
|
+
// number of symbols the file has.
|
|
120
|
+
expect(container.textContent).toContain(`${MAX_OUTLINE_ROWS}/700`);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('a complete list shows no truncation sentence', async () => {
|
|
124
|
+
await mount({ symbols: symbols(4) });
|
|
125
|
+
await open();
|
|
126
|
+
|
|
127
|
+
expect(container.querySelectorAll('button[aria-label*=", line "]')).toHaveLength(4);
|
|
128
|
+
expect(container.textContent).not.toContain('Showing the first');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('an empty list prints the reason it is empty, not a blank sheet', async () => {
|
|
132
|
+
await mount({ symbols: [], emptyNotice: 'This tab has no outline.' });
|
|
133
|
+
await open();
|
|
134
|
+
|
|
135
|
+
expect(container.textContent).toContain('This tab has no outline.');
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -27,15 +27,21 @@ import { Modal } from '@/client/components/common/Modal';
|
|
|
27
27
|
import { OutlineKindIcon, outlineKindLabel } from '@/client/components/common/outline-kind-icon';
|
|
28
28
|
import { setLineReveal } from '@/client/lib/line-reveal';
|
|
29
29
|
import type { OutlineSymbol } from '@/shared/lib/code/outline/types';
|
|
30
|
+
import { outlineTruncatedNotice } from '@/shared/lib/code/outline/rows';
|
|
30
31
|
|
|
31
32
|
interface MobileOutlineButtonProps {
|
|
32
33
|
path: string | null;
|
|
34
|
+
/** The rows to render — already bounded by the hook. */
|
|
33
35
|
symbols: OutlineSymbol[];
|
|
36
|
+
/** Every symbol found, so the button can say when it is showing a window. */
|
|
37
|
+
total: number;
|
|
38
|
+
/** True when `symbols` is only the first `MAX_OUTLINE_ROWS`. */
|
|
39
|
+
truncated: boolean;
|
|
34
40
|
/** Why there is nothing to list, when `symbols` is empty. */
|
|
35
41
|
emptyNotice: string;
|
|
36
42
|
}
|
|
37
43
|
|
|
38
|
-
export function MobileOutlineButton({ path, symbols, emptyNotice }: MobileOutlineButtonProps) {
|
|
44
|
+
export function MobileOutlineButton({ path, symbols, total, truncated, emptyNotice }: MobileOutlineButtonProps) {
|
|
39
45
|
const [open, setOpen] = useState(false);
|
|
40
46
|
if (!path) return null;
|
|
41
47
|
|
|
@@ -52,7 +58,7 @@ export function MobileOutlineButton({ path, symbols, emptyNotice }: MobileOutlin
|
|
|
52
58
|
<button
|
|
53
59
|
type="button"
|
|
54
60
|
onClick={() => setOpen(true)}
|
|
55
|
-
aria-label={`Outline (${
|
|
61
|
+
aria-label={`Outline (${total} symbols)`}
|
|
56
62
|
title="Outline"
|
|
57
63
|
className="flex h-9 w-9 items-center justify-center rounded-full border border-ink/20 bg-paper text-ink/60 shadow-sm transition-all hover:bg-ink/5 hover:text-ink active:scale-95"
|
|
58
64
|
>
|
|
@@ -69,7 +75,9 @@ export function MobileOutlineButton({ path, symbols, emptyNotice }: MobileOutlin
|
|
|
69
75
|
<div className="flex min-w-0 items-center gap-2">
|
|
70
76
|
<ListTree size={15} className="flex-shrink-0 text-ink/50" />
|
|
71
77
|
<span className="text-sm font-medium">Outline</span>
|
|
72
|
-
<span className="font-mono text-[11px] text-ink/40">
|
|
78
|
+
<span className="font-mono text-[11px] text-ink/40">
|
|
79
|
+
{truncated ? `${symbols.length}/${total}` : total}
|
|
80
|
+
</span>
|
|
73
81
|
</div>
|
|
74
82
|
}
|
|
75
83
|
>
|
|
@@ -92,6 +100,9 @@ export function MobileOutlineButton({ path, symbols, emptyNotice }: MobileOutlin
|
|
|
92
100
|
</button>
|
|
93
101
|
))
|
|
94
102
|
)}
|
|
103
|
+
{truncated && (
|
|
104
|
+
<div className="px-4 py-2 text-[11px] italic text-ink/40">{outlineTruncatedNotice(total)}</div>
|
|
105
|
+
)}
|
|
95
106
|
</div>
|
|
96
107
|
</Modal>
|
|
97
108
|
)}
|
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Info, Monitor, Settings } from 'lucide-preact';
|
|
2
2
|
import packageJson from '@/../package.json';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
shellDownloadShortLabel,
|
|
6
|
-
type ShellDownload,
|
|
7
|
-
} from '@/client/hooks/ui/updates/shell-download';
|
|
3
|
+
import type { ShellDownload } from '@/client/hooks/ui/updates/shell-download';
|
|
4
|
+
import { ShellUpdateButton } from '@/client/components/common/shell-update-button';
|
|
8
5
|
|
|
9
6
|
interface MobileSessionFooterProps {
|
|
10
7
|
onSettings: () => void;
|
|
@@ -49,27 +46,18 @@ export function MobileSessionFooter({
|
|
|
49
46
|
<span>About</span>
|
|
50
47
|
</button>
|
|
51
48
|
{shellDownload && (
|
|
52
|
-
// Directly after About, matching the desktop footer.
|
|
53
|
-
// button: the download is the browser's (or the WebView's) job, and
|
|
54
|
-
// `target="_blank"` hands it to the system browser so the console is
|
|
55
|
-
// not replaced by a file download.
|
|
49
|
+
// Directly after About, matching the desktop footer.
|
|
56
50
|
//
|
|
57
51
|
// The visible word is the OFFER, not the file type: `APK` told a
|
|
58
52
|
// reader what the bytes are but not what the control does, and on a
|
|
59
53
|
// browser it is the only place the native shell is ever mentioned. An
|
|
60
54
|
// iOS update is an IPA, so the word names the platform instead of a
|
|
61
55
|
// file extension.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
aria-label={shellDownloadLabel(shellDownload)}
|
|
68
|
-
className="flex items-center space-x-1 hover:text-ink transition-colors"
|
|
69
|
-
>
|
|
70
|
-
<Download size={14} />
|
|
71
|
-
<span>{shellDownloadShortLabel(shellDownload)}</span>
|
|
72
|
-
</a>
|
|
56
|
+
//
|
|
57
|
+
// The shared control decides link-versus-button: a desktop shell that
|
|
58
|
+
// knows its architecture installs the artifact itself, and a link
|
|
59
|
+
// there would leave the file in Downloads.
|
|
60
|
+
<ShellUpdateButton download={shellDownload} variant="label" />
|
|
73
61
|
)}
|
|
74
62
|
{onDesktopToggle && (
|
|
75
63
|
<button
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { X } from 'lucide-preact';
|
|
2
2
|
import { BrandWordmark } from '@/client/components/common/brand-wordmark';
|
|
3
3
|
import { titlebarPaddingLeft, titlebarPaddingRight } from '@/shared/lib/ui/titlebar';
|
|
4
|
+
import { dragRegionProps, titlebarInsetLeft } from '@/client/lib/desktop/titlebar';
|
|
4
5
|
|
|
5
6
|
export function MobileSessionHeader({ onClose, viewLabel = 'Sessions' }: { onClose: () => void; viewLabel?: string }) {
|
|
6
7
|
return (
|
|
7
8
|
<div
|
|
8
9
|
className="border-b border-ink/10 flex items-center justify-between px-3 flex-shrink-0 bg-canvas titlebar-drag-region select-none"
|
|
10
|
+
{...dragRegionProps()}
|
|
9
11
|
style={{
|
|
10
12
|
height: 'calc(3.5rem + env(safe-area-inset-top, 0px))',
|
|
11
13
|
// `px-3`, the toolbar's own inset: the wordmark above and the New
|
|
12
14
|
// Session button below share one left edge, which the header's old
|
|
13
|
-
// 14px did not
|
|
14
|
-
|
|
15
|
+
// 14px did not — and the SHELL's own inset, because a desktop shell
|
|
16
|
+
// window narrowed past the layout breakpoint shows this drawer, whose
|
|
17
|
+
// wordmark would otherwise sit under macOS's traffic lights.
|
|
18
|
+
paddingLeft: titlebarInsetLeft(titlebarPaddingLeft('0.75rem', true)),
|
|
15
19
|
// The drawer is the full window width, so on a desktop showing the
|
|
16
20
|
// mobile layout its close button sits under the window controls too.
|
|
17
21
|
paddingRight: titlebarPaddingRight('0.75rem', true),
|
|
@@ -4,6 +4,7 @@ import { useSearchParams } from '@/client/lib/router/search-params';
|
|
|
4
4
|
import { MobileSessionRow } from '@/client/components/mobile/mobile-session-sidebar/SessionRow';
|
|
5
5
|
import { SubagentList } from '@/client/components/common/subagent-list';
|
|
6
6
|
import { WorkspaceOptionsMenu } from '@/client/components/common/workspace-options-menu';
|
|
7
|
+
import { useWorktreeDialog } from '@/client/components/mobile/mobile-session-sidebar/WorktreeDialog';
|
|
7
8
|
import { useShowMore } from '@/client/hooks/ui/show-more';
|
|
8
9
|
import { useExpandedSessions } from '@/client/hooks/workspace/expanded-sessions';
|
|
9
10
|
import { useWorkspaceFolderActions } from '@/client/hooks/workspace/workspace-folder-actions';
|
|
@@ -25,6 +26,8 @@ interface MobileSessionCategoryProps {
|
|
|
25
26
|
activeSessionId: number | string | null;
|
|
26
27
|
onSelectSession: (id: number | string) => void;
|
|
27
28
|
onNewSessionForFolder: (folderId: number) => void;
|
|
29
|
+
/** Start a chat in a worktree made from this folder. */
|
|
30
|
+
onNewWorktreeSession?: (folderId: number, worktreePath: string) => void;
|
|
28
31
|
isExpanded: boolean;
|
|
29
32
|
onToggleExpand: () => void;
|
|
30
33
|
showArchived?: boolean;
|
|
@@ -43,6 +46,7 @@ export function MobileSessionCategory({
|
|
|
43
46
|
activeSessionId,
|
|
44
47
|
onSelectSession,
|
|
45
48
|
onNewSessionForFolder,
|
|
49
|
+
onNewWorktreeSession,
|
|
46
50
|
isExpanded,
|
|
47
51
|
onToggleExpand,
|
|
48
52
|
showArchived = false,
|
|
@@ -53,6 +57,11 @@ export function MobileSessionCategory({
|
|
|
53
57
|
onCancelWait,
|
|
54
58
|
}: MobileSessionCategoryProps) {
|
|
55
59
|
const { visibleCount, showMore, showLess, canShowLess } = useShowMore();
|
|
60
|
+
// The phone's folder row opens the same dialog the desktop one does. Its state
|
|
61
|
+
// lives in a hook so this row stays under the size ceiling.
|
|
62
|
+
const worktreeDialog = useWorktreeDialog(folder, (worktreePath) =>
|
|
63
|
+
onNewWorktreeSession?.(folder.id, worktreePath),
|
|
64
|
+
);
|
|
56
65
|
const { refresh } = useSidebarData();
|
|
57
66
|
const { expandedSessionIds, toggleSession } = useExpandedSessions();
|
|
58
67
|
// The viewed transcript, so its session row dims while a roster entry is
|
|
@@ -223,6 +232,11 @@ export function MobileSessionCategory({
|
|
|
223
232
|
variant="mobile"
|
|
224
233
|
isPinned={folder.isPinned}
|
|
225
234
|
onOpenFolder={onOpenFolder}
|
|
235
|
+
onNewWorktree={
|
|
236
|
+
onNewWorktreeSession && folder.project_path
|
|
237
|
+
? () => { setShowMenu(false); worktreeDialog.open(); }
|
|
238
|
+
: undefined
|
|
239
|
+
}
|
|
226
240
|
onSelectSessions={
|
|
227
241
|
onStartSessionSelection
|
|
228
242
|
? () => { setShowMenu(false); onStartSessionSelection(folder.id); }
|
|
@@ -326,6 +340,8 @@ export function MobileSessionCategory({
|
|
|
326
340
|
onClose={sessionDelete.cancelDelete}
|
|
327
341
|
onConfirm={() => void sessionDelete.confirmDelete()}
|
|
328
342
|
/>
|
|
343
|
+
|
|
344
|
+
{worktreeDialog.dialog}
|
|
329
345
|
</div>
|
|
330
346
|
);
|
|
331
347
|
}
|
|
@@ -10,6 +10,8 @@ interface MobileSessionListProps {
|
|
|
10
10
|
sessionStatus: Record<string, 'stream' | 'finish' | 'abort'>;
|
|
11
11
|
onSelectSession: (id: number | string) => void;
|
|
12
12
|
onNewSessionForFolder: (folderId: number) => void;
|
|
13
|
+
/** Start a chat in a worktree made from this folder. */
|
|
14
|
+
onNewWorktreeSession?: (folderId: number, worktreePath: string) => void;
|
|
13
15
|
onToggleFolder: (folderId: number) => void;
|
|
14
16
|
/** Surfaces a refused folder action (the reveal endpoint's own reason). */
|
|
15
17
|
onToast?: (message: string, type?: 'success' | 'error') => void;
|
|
@@ -28,6 +30,7 @@ export function MobileSessionList({
|
|
|
28
30
|
sessionStatus,
|
|
29
31
|
onSelectSession,
|
|
30
32
|
onNewSessionForFolder,
|
|
33
|
+
onNewWorktreeSession,
|
|
31
34
|
onToggleFolder,
|
|
32
35
|
onToast,
|
|
33
36
|
batch,
|
|
@@ -55,6 +58,7 @@ export function MobileSessionList({
|
|
|
55
58
|
activeSessionId={activeSessionId}
|
|
56
59
|
onSelectSession={onSelectSession}
|
|
57
60
|
onNewSessionForFolder={onNewSessionForFolder}
|
|
61
|
+
onNewWorktreeSession={onNewWorktreeSession}
|
|
58
62
|
isExpanded={expandedFolders[folder.id] ?? true}
|
|
59
63
|
onToggleExpand={() => onToggleFolder(folder.id)}
|
|
60
64
|
showArchived={showArchived}
|
|
@@ -4,6 +4,7 @@ import { useInlineRename } from '@/client/hooks/ui/inline-rename';
|
|
|
4
4
|
import { useSessionRenaming } from '@/client/hooks/workspace/renaming-sessions';
|
|
5
5
|
import { SessionActionsMenu, useRowMenu } from '@/client/components/common/session-actions-menu';
|
|
6
6
|
import { SelectionCheckbox } from '@/client/components/common/batch-selection/SelectionCheckbox';
|
|
7
|
+
import { WorktreeBadge } from '@/client/components/layout/session-sidebar/WorktreeBadge';
|
|
7
8
|
|
|
8
9
|
export interface MobileSessionRowProps {
|
|
9
10
|
session: SessionItemData;
|
|
@@ -175,6 +176,10 @@ export function MobileSessionRow({
|
|
|
175
176
|
waiting for {waitingFor?.title ?? 'another session'}
|
|
176
177
|
</span>
|
|
177
178
|
)}
|
|
179
|
+
{/* The same chip the desktop row draws, from the same field on the
|
|
180
|
+
row payload — one layout cannot be the only one that says where
|
|
181
|
+
the chat writes. */}
|
|
182
|
+
{session.worktree ? <WorktreeBadge worktree={session.worktree} /> : null}
|
|
178
183
|
</span>
|
|
179
184
|
</div>
|
|
180
185
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The New Worktree dialog as a phone folder row opens it.
|
|
8
|
+
*
|
|
9
|
+
* Split out because the row was at the file-size ceiling and because the
|
|
10
|
+
* dialog's state (open, and which folder) belongs to the dialog rather than to a
|
|
11
|
+
* row that also draws sessions, a `⋯` menu and a delete confirmation. The row
|
|
12
|
+
* calls the hook, hands `open` to its menu row and renders `dialog` at its tail —
|
|
13
|
+
* the menu row has to stay INSIDE the dropdown, which is why this returns the
|
|
14
|
+
* modal rather than the trigger.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useState } from 'preact/hooks';
|
|
18
|
+
import { NewWorktreeModal } from '@/client/components/workspace/worktree-modal/index';
|
|
19
|
+
import type { WorkspaceFolderData } from '@/shared/types';
|
|
20
|
+
|
|
21
|
+
export interface WorktreeDialogHandle {
|
|
22
|
+
open: () => void;
|
|
23
|
+
/** Rendered at the row's tail; null until the dialog has been opened once. */
|
|
24
|
+
dialog: preact.JSX.Element | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function useWorktreeDialog(
|
|
28
|
+
folder: WorkspaceFolderData,
|
|
29
|
+
onCreated: (worktreePath: string) => void,
|
|
30
|
+
): WorktreeDialogHandle {
|
|
31
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
open: () => setIsOpen(true),
|
|
35
|
+
dialog: isOpen ? (
|
|
36
|
+
<NewWorktreeModal
|
|
37
|
+
isOpen
|
|
38
|
+
onClose={() => setIsOpen(false)}
|
|
39
|
+
root={folder.project_path ?? null}
|
|
40
|
+
folderId={folder.id}
|
|
41
|
+
folderName={folder.name}
|
|
42
|
+
onCreated={(created) => {
|
|
43
|
+
setIsOpen(false);
|
|
44
|
+
onCreated(created.path);
|
|
45
|
+
}}
|
|
46
|
+
/>
|
|
47
|
+
) : null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Starting a chat inside a worktree, from either layout's sidebar.
|
|
8
|
+
*
|
|
9
|
+
* Desktop and phone do the same three things in the same order, and the order is
|
|
10
|
+
* what makes it work: prewarm the child in the WORKTREE cwd (not the project's),
|
|
11
|
+
* mint the pending session, write its target BEFORE the URL names it, then
|
|
12
|
+
* navigate. Writing the target afterwards would leave the composer drawing the
|
|
13
|
+
* folder while the send path already had its ref armed for something else — the
|
|
14
|
+
* one outcome the pick exists to prevent.
|
|
15
|
+
*
|
|
16
|
+
* Extracted so both sidebars call one function rather than repeating a sequence
|
|
17
|
+
* whose middle step is invisible when it is wrong.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { startPendingSession } from '@/client/hooks/chat/omp/pending-sessions';
|
|
21
|
+
import { triggerSessionPrewarm } from '@/shared/lib/omp/session/prewarm';
|
|
22
|
+
import { setSessionKey } from '@/shared/lib/workspace/session-state/store';
|
|
23
|
+
import { SPAWN_TARGET_KEY } from '@/client/hooks/chat/timeline/spawn-target';
|
|
24
|
+
|
|
25
|
+
export interface WorktreeSessionDeps {
|
|
26
|
+
setSearchParams: (fn: (prev: URLSearchParams) => URLSearchParams, options?: { replace?: boolean }) => void;
|
|
27
|
+
/** Called after the navigation — the phone closes its drawer here. */
|
|
28
|
+
afterNavigate?: () => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Open a pending chat in `worktreePath`, scoped to `folderId`.
|
|
33
|
+
* Returns the pending session's id, so a caller can report or follow it.
|
|
34
|
+
*/
|
|
35
|
+
export function startWorktreeSession(
|
|
36
|
+
deps: WorktreeSessionDeps,
|
|
37
|
+
folderId: number,
|
|
38
|
+
worktreePath: string,
|
|
39
|
+
): string {
|
|
40
|
+
triggerSessionPrewarm(worktreePath);
|
|
41
|
+
const pendingId = startPendingSession(folderId);
|
|
42
|
+
setSessionKey(pendingId, SPAWN_TARGET_KEY, { folderId, worktreePath });
|
|
43
|
+
deps.setSearchParams((prev) => {
|
|
44
|
+
const next = new URLSearchParams(prev);
|
|
45
|
+
next.set('sessionId', pendingId);
|
|
46
|
+
next.set('folderId', folderId.toString());
|
|
47
|
+
return next;
|
|
48
|
+
}, { replace: true });
|
|
49
|
+
deps.afterNavigate?.();
|
|
50
|
+
return pendingId;
|
|
51
|
+
}
|
|
@@ -9,7 +9,9 @@ import { useRestartKabmo } from '@/client/hooks/settings/restart-kabmo';
|
|
|
9
9
|
import { applyDocumentTheme } from '@/client/hooks/ui/theme';
|
|
10
10
|
import { DEFAULT_THEME_ID } from '@/shared/lib/theme/catalog';
|
|
11
11
|
import { EDITOR_DEFAULT_FONT_FAMILY } from '@/shared/lib/code/editor/typography';
|
|
12
|
+
import { EDITOR_DEFAULT_THEME_ID } from '@/shared/lib/code/editor/themes';
|
|
12
13
|
import { EDITOR_FONT_CHANGED_EVENT } from '@/client/hooks/editor/font';
|
|
14
|
+
import { EDITOR_THEME_CHANGED_EVENT } from '@/client/hooks/editor/theme';
|
|
13
15
|
import { useKabmoEvent } from '@/client/hooks/ui/window-event';
|
|
14
16
|
import { SETTINGS_CATEGORIES, SettingsSidebar } from '@/client/components/settings/Sidebar';
|
|
15
17
|
import { AppearanceSettings } from '@/client/components/settings/categories/AppearanceSettings';
|
|
@@ -41,6 +43,7 @@ export interface SettingsModalProps {
|
|
|
41
43
|
const DEFAULT_SETTINGS: SettingsState = {
|
|
42
44
|
theme: DEFAULT_THEME_ID,
|
|
43
45
|
editorFont: EDITOR_DEFAULT_FONT_FAMILY,
|
|
46
|
+
editorTheme: EDITOR_DEFAULT_THEME_ID,
|
|
44
47
|
autoSessionTitle: true,
|
|
45
48
|
soundAlerts: true,
|
|
46
49
|
chatCompletionSound: true,
|
|
@@ -120,6 +123,11 @@ export function SettingsModal({
|
|
|
120
123
|
if (typeof window !== 'undefined' && next.editorFont !== prev.editorFont) {
|
|
121
124
|
window.dispatchEvent(new CustomEvent(EDITOR_FONT_CHANGED_EVENT, { detail: next.editorFont }));
|
|
122
125
|
}
|
|
126
|
+
// Same rule for the editor's colour theme: it is visual, and every open
|
|
127
|
+
// editor subscribes rather than reading the boot-time `appSettings`.
|
|
128
|
+
if (typeof window !== 'undefined' && next.editorTheme !== prev.editorTheme) {
|
|
129
|
+
window.dispatchEvent(new CustomEvent(EDITOR_THEME_CHANGED_EVENT, { detail: next.editorTheme }));
|
|
130
|
+
}
|
|
123
131
|
// Only the keys this update actually touched are persisted. The modal
|
|
124
132
|
// holds a full `SettingsState` snapshot taken when it opened, so writing
|
|
125
133
|
// the whole object back would also republish every value it read at that
|