@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
|
@@ -3,6 +3,7 @@ import type { SettingsState } from '@/shared/types';
|
|
|
3
3
|
import { resolveThemeId } from '@/shared/lib/theme/catalog';
|
|
4
4
|
import { ThemeGrid } from '@/client/components/settings/categories/appearance-settings/ThemeGrid';
|
|
5
5
|
import { EditorFontSection } from '@/client/components/settings/categories/appearance-settings/EditorFontSection';
|
|
6
|
+
import { EditorThemeSection } from '@/client/components/settings/categories/appearance-settings/EditorThemeSection';
|
|
6
7
|
|
|
7
8
|
interface AppearanceSettingsProps {
|
|
8
9
|
settings: SettingsState;
|
|
@@ -25,6 +26,8 @@ export function AppearanceSettings({ settings, onUpdate }: AppearanceSettingsPro
|
|
|
25
26
|
</section>
|
|
26
27
|
|
|
27
28
|
<EditorFontSection settings={settings} onUpdate={onUpdate} />
|
|
29
|
+
|
|
30
|
+
<EditorThemeSection settings={settings} onUpdate={onUpdate} />
|
|
28
31
|
</div>
|
|
29
32
|
);
|
|
30
33
|
}
|
|
@@ -6,9 +6,12 @@ import { LoadingState } from '@/client/components/settings/LoadingState';
|
|
|
6
6
|
import { useSettingsMasterDetail } from '@/client/hooks/settings/master-detail';
|
|
7
7
|
import { useSidebarData } from '@/client/hooks/chat/omp/session-list';
|
|
8
8
|
import { SettingsMasterDetail } from '@/client/components/settings/master-detail';
|
|
9
|
+
import { WorktreeSettings } from '@/client/components/settings/categories/project-settings/WorktreeSettings';
|
|
10
|
+
import { useToasts } from '@/client/hooks/ui/toasts';
|
|
9
11
|
|
|
10
12
|
export function ProjectSettings() {
|
|
11
13
|
const masterDetail = useSettingsMasterDetail();
|
|
14
|
+
const { pushToast } = useToasts();
|
|
12
15
|
const [projects, setProjects] = useState<ProjectConfigItem[]>([]);
|
|
13
16
|
const [availableModels, setAvailableModels] = useState<string[]>([]);
|
|
14
17
|
const [accentColorOptions, setAccentColorOptions] = useState<AccentColorOption[]>([]);
|
|
@@ -179,14 +182,25 @@ export function ProjectSettings() {
|
|
|
179
182
|
}
|
|
180
183
|
detail={
|
|
181
184
|
selectedProject ? (
|
|
182
|
-
<
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
185
|
+
<div className="flex flex-col">
|
|
186
|
+
<ProjectDetailsForm
|
|
187
|
+
project={selectedProject}
|
|
188
|
+
canDelete={projects.length > 1}
|
|
189
|
+
onUpdateField={handleUpdateField}
|
|
190
|
+
onDeleteProject={handleDeleteProject}
|
|
191
|
+
availableModels={availableModels}
|
|
192
|
+
accentColorOptions={accentColorOptions}
|
|
193
|
+
/>
|
|
194
|
+
{/* Worktrees belong to the PROJECT, not to a session: a create here
|
|
195
|
+
has no chat, and the next chat picks it from the composer. */}
|
|
196
|
+
<div className="border-t border-ink/10 px-4 py-4">
|
|
197
|
+
<WorktreeSettings
|
|
198
|
+
root={selectedProject.path}
|
|
199
|
+
folderId={selectedProject.folderId ?? null}
|
|
200
|
+
onReport={(message, tone) => pushToast(message, tone ?? 'success', { duration: 6000 })}
|
|
201
|
+
/>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
190
204
|
) : null
|
|
191
205
|
}
|
|
192
206
|
/>
|
package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.test.tsx
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The editor theme picker.
|
|
8
|
+
*
|
|
9
|
+
* Three properties are invisible to a markup check and each is a bug if dropped:
|
|
10
|
+
*
|
|
11
|
+
* - **`Auto` is a card and is selected by default**, so an install that never
|
|
12
|
+
* touches this section renders exactly what it did before the feature.
|
|
13
|
+
* - **The swatch is drawn from the row's own colours**, not from the app
|
|
14
|
+
* palette: a card that previewed every theme in the app's ink would make 65
|
|
15
|
+
* themes look identical, which is the whole point of the grid.
|
|
16
|
+
* - **A click writes the theme id through `onUpdate`**, which is what the modal
|
|
17
|
+
* turns into the event an already-open editor subscribes to.
|
|
18
|
+
*
|
|
19
|
+
* Rendered with `h()` against happy-dom; the modal's write path is pinned in
|
|
20
|
+
* `settings/Modal`'s own suite.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
|
|
24
|
+
import { Window } from 'happy-dom';
|
|
25
|
+
import { h, render } from 'preact';
|
|
26
|
+
import { act } from 'preact/test-utils';
|
|
27
|
+
|
|
28
|
+
import { EditorThemeSection } from '@/client/components/settings/categories/appearance-settings/EditorThemeSection';
|
|
29
|
+
import { EDITOR_AUTO_THEME_ID, EDITOR_THEMES, resolveEditorTheme } from '@/shared/lib/code/editor/themes';
|
|
30
|
+
import { EDITOR_DEFAULT_FONT_FAMILY } from '@/shared/lib/code/editor/typography';
|
|
31
|
+
import type { SettingsState } from '@/shared/types';
|
|
32
|
+
|
|
33
|
+
const DOM_GLOBALS = [
|
|
34
|
+
'window', 'document', 'navigator', 'HTMLElement', 'Element', 'Node', 'Event', 'CustomEvent',
|
|
35
|
+
'getComputedStyle',
|
|
36
|
+
] as const;
|
|
37
|
+
|
|
38
|
+
const native: Partial<Record<(typeof DOM_GLOBALS)[number], unknown>> = {};
|
|
39
|
+
|
|
40
|
+
beforeAll(() => {
|
|
41
|
+
const win = new Window({ url: 'http://localhost' });
|
|
42
|
+
const target = globalThis as unknown as Record<string, unknown>;
|
|
43
|
+
for (const key of DOM_GLOBALS) {
|
|
44
|
+
if (!(key in native)) native[key] = target[key];
|
|
45
|
+
const value = (win as unknown as Record<string, unknown>)[key];
|
|
46
|
+
if (value !== undefined) target[key] = value;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterAll(() => {
|
|
51
|
+
const target = globalThis as unknown as Record<string, unknown>;
|
|
52
|
+
for (const key of DOM_GLOBALS) {
|
|
53
|
+
if (native[key] === undefined) delete target[key];
|
|
54
|
+
else target[key] = native[key];
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
let container: HTMLElement | null = null;
|
|
59
|
+
type Update = Partial<SettingsState> | ((prev: SettingsState) => SettingsState);
|
|
60
|
+
let lastUpdate: Update | null = null;
|
|
61
|
+
|
|
62
|
+
/** Reset through a function: assigning `null` inline would narrow the variable
|
|
63
|
+
* at every later read, and the assertions below exist to see the VALUE. */
|
|
64
|
+
function resetUpdate(): void {
|
|
65
|
+
lastUpdate = null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** What the last click asked to persist, as a value the assertions can read. */
|
|
69
|
+
function lastPatch(): unknown {
|
|
70
|
+
return lastUpdate;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function settings(editorTheme: string): SettingsState {
|
|
74
|
+
return { editorTheme, editorFont: EDITOR_DEFAULT_FONT_FAMILY } as SettingsState;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function mount(editorTheme: string): Promise<HTMLElement> {
|
|
78
|
+
if (container) render(null, container);
|
|
79
|
+
container = document.body.appendChild(document.createElement('div'));
|
|
80
|
+
await act(async () => {
|
|
81
|
+
render(
|
|
82
|
+
h(EditorThemeSection, {
|
|
83
|
+
settings: settings(editorTheme),
|
|
84
|
+
onUpdate: (update: Update) => {
|
|
85
|
+
lastUpdate = update;
|
|
86
|
+
},
|
|
87
|
+
}),
|
|
88
|
+
container!,
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
return container;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The card whose label starts with `label` (the label is the first text). */
|
|
95
|
+
function card(root: HTMLElement, label: string): HTMLButtonElement | undefined {
|
|
96
|
+
return Array.from(root.querySelectorAll('button')).find((button) =>
|
|
97
|
+
(button.textContent ?? '').trim().startsWith(label),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
describe('editor theme picker', () => {
|
|
102
|
+
test('Auto is a card, and it is the selected one by default', async () => {
|
|
103
|
+
const root = await mount(EDITOR_AUTO_THEME_ID);
|
|
104
|
+
const auto = card(root, 'Auto');
|
|
105
|
+
expect(auto).toBeDefined();
|
|
106
|
+
expect(auto!.getAttribute('aria-pressed')).toBe('true');
|
|
107
|
+
// Every shipped theme is offered beside it, so the grid is not a two-item
|
|
108
|
+
// list with the catalogue hidden behind a filter.
|
|
109
|
+
expect(root.querySelectorAll('button[aria-pressed]').length).toBe(EDITOR_THEMES.length + 1);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('a stored theme is the selected card, and Auto is not', async () => {
|
|
113
|
+
const root = await mount('dracula');
|
|
114
|
+
expect(card(root, 'Dracula Theme')!.getAttribute('aria-pressed')).toBe('true');
|
|
115
|
+
expect(card(root, 'Auto')!.getAttribute('aria-pressed')).toBe('false');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('the swatch is drawn in the theme’s OWN colours', async () => {
|
|
119
|
+
const root = await mount('dracula');
|
|
120
|
+
const theme = resolveEditorTheme('dracula');
|
|
121
|
+
expect(theme).not.toBeNull();
|
|
122
|
+
const swatch = Array.from(card(root, 'Dracula Theme')!.querySelectorAll('span')).find(
|
|
123
|
+
(node) => (node as HTMLElement).style.backgroundColor !== '',
|
|
124
|
+
) as HTMLElement | undefined;
|
|
125
|
+
expect(swatch).toBeDefined();
|
|
126
|
+
// The card paints the theme's own surface, not the app's — which is what
|
|
127
|
+
// makes 65 themes distinguishable in one grid.
|
|
128
|
+
expect(swatch!.style.backgroundColor.toLowerCase()).toBe(theme!.bg.toLowerCase());
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('a click writes the theme id', async () => {
|
|
132
|
+
resetUpdate();
|
|
133
|
+
const root = await mount(EDITOR_AUTO_THEME_ID);
|
|
134
|
+
await act(async () => {
|
|
135
|
+
card(root, 'Nord')!.click();
|
|
136
|
+
});
|
|
137
|
+
expect(lastPatch()).toEqual({ editorTheme: 'nord' });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('the Auto card writes the sentinel, not an empty string', async () => {
|
|
141
|
+
resetUpdate();
|
|
142
|
+
const root = await mount('nord');
|
|
143
|
+
await act(async () => {
|
|
144
|
+
card(root, 'Auto')!.click();
|
|
145
|
+
});
|
|
146
|
+
// An empty value would resolve to `auto` too, but it would also be a value
|
|
147
|
+
// no reader can distinguish from a missing setting.
|
|
148
|
+
expect(lastPatch()).toEqual({ editorTheme: EDITOR_AUTO_THEME_ID });
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('the light/dark filters narrow the grid without dropping Auto', async () => {
|
|
152
|
+
const root = await mount(EDITOR_AUTO_THEME_ID);
|
|
153
|
+
const darkFilter = Array.from(root.querySelectorAll('button')).find(
|
|
154
|
+
(button) => (button.textContent ?? '').trim() === 'Dark',
|
|
155
|
+
);
|
|
156
|
+
await act(async () => {
|
|
157
|
+
darkFilter!.click();
|
|
158
|
+
});
|
|
159
|
+
const labels = Array.from(root.querySelectorAll('button[aria-pressed]')).map((button) =>
|
|
160
|
+
(button.textContent ?? '').trim().split(/\d/)[0],
|
|
161
|
+
);
|
|
162
|
+
expect(labels.length).toBe(EDITOR_THEMES.filter((theme) => theme.variant === 'dark').length + 1);
|
|
163
|
+
expect(labels.some((label) => label.startsWith('GitHub Light'))).toBe(false);
|
|
164
|
+
// Auto is a choice, not a light/dark theme, so a variant filter keeps it.
|
|
165
|
+
expect(labels.some((label) => label.startsWith('Auto'))).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The code editor's colour theme, as a filterable grid of cards.
|
|
8
|
+
*
|
|
9
|
+
* A second axis beside the app palette (see `themes.ts`): the console keeps its
|
|
10
|
+
* own theme and the editor wears whichever TextMate theme the reader works in.
|
|
11
|
+
* `auto` is the first card and the default, so an install that never opens this
|
|
12
|
+
* picker behaves exactly as before.
|
|
13
|
+
*
|
|
14
|
+
* Three properties are load-bearing and each is a bug if dropped:
|
|
15
|
+
*
|
|
16
|
+
* - **The swatch is drawn from the generated colour rows, not from the theme
|
|
17
|
+
* module.** 65 TextMate definitions is 1.3 MB; a grid that fetched them to
|
|
18
|
+
* draw itself would download the whole catalogue to show a colour. The rows
|
|
19
|
+
* carry the surface, the ink and three syntax accents — the colours that
|
|
20
|
+
* actually tell two themes apart at card size.
|
|
21
|
+
* - **The preview is set in the theme's OWN colours**, so an unselected card
|
|
22
|
+
* still shows what selecting it does.
|
|
23
|
+
* - **Selecting is a write, and the write is announced** (`Modal` dispatches
|
|
24
|
+
* `omp:editor-theme-changed`), which is what reaches an editor that is
|
|
25
|
+
* already open — `appSettings` is a boot-time prop and would only apply on a
|
|
26
|
+
* reload.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { useMemo, useState } from 'preact/hooks';
|
|
30
|
+
import { Check, Moon, Palette, Sun } from 'lucide-preact';
|
|
31
|
+
import type { SettingsState } from '@/shared/types';
|
|
32
|
+
import {
|
|
33
|
+
EDITOR_AUTO_THEME_ID,
|
|
34
|
+
EDITOR_THEMES,
|
|
35
|
+
type EditorTheme,
|
|
36
|
+
} from '@/shared/lib/code/editor/themes';
|
|
37
|
+
import { EDITOR_FONT_CHOICES } from '@/shared/lib/code/editor/typography';
|
|
38
|
+
|
|
39
|
+
interface EditorThemeSectionProps {
|
|
40
|
+
settings: SettingsState;
|
|
41
|
+
onUpdate: (updater: Partial<SettingsState> | ((prev: SettingsState) => SettingsState)) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type VariantFilter = 'all' | 'light' | 'dark';
|
|
45
|
+
|
|
46
|
+
const FILTERS: ReadonlyArray<{ id: VariantFilter; label: string }> = [
|
|
47
|
+
{ id: 'all', label: 'All' },
|
|
48
|
+
{ id: 'light', label: 'Light' },
|
|
49
|
+
{ id: 'dark', label: 'Dark' },
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
/** The face the cards' sample line is set in: the editor's own choice. */
|
|
53
|
+
function sampleFace(font: string): string {
|
|
54
|
+
const choice = EDITOR_FONT_CHOICES.find((entry) => entry.id === font);
|
|
55
|
+
return `"${choice?.id ?? EDITOR_FONT_CHOICES[0].id}", monospace`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function ThemeSwatch({ theme, face }: { theme: EditorTheme; face: string }) {
|
|
59
|
+
return (
|
|
60
|
+
<span
|
|
61
|
+
className="block overflow-hidden rounded border border-ink/10"
|
|
62
|
+
style={{ backgroundColor: theme.bg, color: theme.fg }}
|
|
63
|
+
>
|
|
64
|
+
<span className="block px-1.5 py-1 font-mono text-[10px] leading-[1.4]" style={{ fontFamily: face }}>
|
|
65
|
+
<span style={{ color: theme.keyword }}>const</span> <span>answer</span>{' '}
|
|
66
|
+
<span style={{ color: theme.keyword }}>=</span> <span style={{ color: theme.string }}>42</span>
|
|
67
|
+
<span>{';'}</span>
|
|
68
|
+
<span className="block truncate" style={{ color: theme.comment }}>
|
|
69
|
+
{'// '}
|
|
70
|
+
{theme.label}
|
|
71
|
+
</span>
|
|
72
|
+
</span>
|
|
73
|
+
</span>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function EditorThemeSection({ settings, onUpdate }: EditorThemeSectionProps) {
|
|
78
|
+
const active = settings.editorTheme || EDITOR_AUTO_THEME_ID;
|
|
79
|
+
const [filter, setFilter] = useState<VariantFilter>('all');
|
|
80
|
+
const face = sampleFace(settings.editorFont);
|
|
81
|
+
|
|
82
|
+
const themes = useMemo(
|
|
83
|
+
() => (filter === 'all' ? EDITOR_THEMES : EDITOR_THEMES.filter((theme) => theme.variant === filter)),
|
|
84
|
+
[filter],
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<section className="space-y-3">
|
|
89
|
+
<div>
|
|
90
|
+
<h3 className="font-semibold text-sm uppercase tracking-wider text-ink/80 flex items-center gap-2">
|
|
91
|
+
<Palette size={16} className="text-ink/60" />
|
|
92
|
+
Editor Theme
|
|
93
|
+
</h3>
|
|
94
|
+
<p className="text-[11px] text-ink/60 mt-1">
|
|
95
|
+
Colours for the code editor only — the console around it keeps the theme above. <strong>Auto</strong>{' '}
|
|
96
|
+
follows the app palette; any other choice paints the editor's own surface, gutter, selection and
|
|
97
|
+
syntax, and applies to every open editor without a reload.
|
|
98
|
+
</p>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<div className="flex items-center gap-1.5">
|
|
102
|
+
{FILTERS.map((option) => (
|
|
103
|
+
<button
|
|
104
|
+
key={option.id}
|
|
105
|
+
type="button"
|
|
106
|
+
onClick={() => setFilter(option.id)}
|
|
107
|
+
className={`rounded-full border px-2.5 py-1 text-[11px] font-medium transition-colors cursor-pointer ${
|
|
108
|
+
filter === option.id
|
|
109
|
+
? 'border-ink bg-ink text-paper'
|
|
110
|
+
: 'border-ink/20 bg-paper text-ink/70 hover:border-ink/40 hover:text-ink'
|
|
111
|
+
}`}
|
|
112
|
+
>
|
|
113
|
+
{option.id === 'light' && <Sun size={11} className="mr-1 inline align-[-1px]" />}
|
|
114
|
+
{option.id === 'dark' && <Moon size={11} className="mr-1 inline align-[-1px]" />}
|
|
115
|
+
{option.label}
|
|
116
|
+
</button>
|
|
117
|
+
))}
|
|
118
|
+
<span className="ml-auto font-mono text-[11px] text-ink/50">
|
|
119
|
+
{filter === 'all' ? `${themes.length} themes` : `${themes.length} shown`}
|
|
120
|
+
</span>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
|
124
|
+
{/* `Auto` is a card rather than a checkbox: it is one of the choices,
|
|
125
|
+
and a checkbox beside a grid makes the default look like an
|
|
126
|
+
override of the thing it is a peer of. It is drawn under EVERY
|
|
127
|
+
filter — it is neither light nor dark, and a user who filtered to
|
|
128
|
+
Dark must still be able to get back to the app palette without
|
|
129
|
+
first clearing the filter. */}
|
|
130
|
+
{(
|
|
131
|
+
<button
|
|
132
|
+
type="button"
|
|
133
|
+
onClick={() => onUpdate({ editorTheme: EDITOR_AUTO_THEME_ID })}
|
|
134
|
+
aria-pressed={active === EDITOR_AUTO_THEME_ID}
|
|
135
|
+
className={`flex w-full flex-col gap-2 rounded-lg border p-2 text-left transition-colors cursor-pointer ${
|
|
136
|
+
active === EDITOR_AUTO_THEME_ID
|
|
137
|
+
? 'border-ink bg-ink/5 ring-1 ring-ink/20'
|
|
138
|
+
: 'border-ink/20 bg-paper hover:border-ink/40 hover:bg-ink/5'
|
|
139
|
+
}`}
|
|
140
|
+
>
|
|
141
|
+
<span className="flex items-center gap-2">
|
|
142
|
+
<span className="truncate font-semibold text-ink">Auto</span>
|
|
143
|
+
<span className="shrink-0 rounded-full border border-ink/15 px-1.5 py-px text-[9px] font-semibold uppercase tracking-wider text-ink/50">
|
|
144
|
+
app palette
|
|
145
|
+
</span>
|
|
146
|
+
{active === EDITOR_AUTO_THEME_ID && <Check size={14} className="ml-auto shrink-0 text-ink" />}
|
|
147
|
+
</span>
|
|
148
|
+
<span className="flex h-8 items-center justify-center rounded border border-dashed border-ink/20 text-[10px] text-ink/50">
|
|
149
|
+
Follows the theme above
|
|
150
|
+
</span>
|
|
151
|
+
</button>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
{themes.map((theme) => {
|
|
155
|
+
const selected = active === theme.id;
|
|
156
|
+
return (
|
|
157
|
+
<button
|
|
158
|
+
key={theme.id}
|
|
159
|
+
type="button"
|
|
160
|
+
onClick={() => onUpdate({ editorTheme: theme.id })}
|
|
161
|
+
aria-pressed={selected}
|
|
162
|
+
className={`flex w-full flex-col gap-2 rounded-lg border p-2 text-left transition-colors cursor-pointer ${
|
|
163
|
+
selected
|
|
164
|
+
? 'border-ink bg-ink/5 ring-1 ring-ink/20'
|
|
165
|
+
: 'border-ink/20 bg-paper hover:border-ink/40 hover:bg-ink/5'
|
|
166
|
+
}`}
|
|
167
|
+
>
|
|
168
|
+
<span className="flex items-center gap-2">
|
|
169
|
+
<span className="truncate font-semibold text-ink" title={theme.label}>
|
|
170
|
+
{theme.label}
|
|
171
|
+
</span>
|
|
172
|
+
<span className="shrink-0 rounded-full border border-ink/15 px-1.5 py-px text-[9px] font-semibold uppercase tracking-wider text-ink/50">
|
|
173
|
+
{theme.variant}
|
|
174
|
+
</span>
|
|
175
|
+
{selected && <Check size={14} className="ml-auto shrink-0 text-ink" />}
|
|
176
|
+
</span>
|
|
177
|
+
<ThemeSwatch theme={theme} face={face} />
|
|
178
|
+
</button>
|
|
179
|
+
);
|
|
180
|
+
})}
|
|
181
|
+
</div>
|
|
182
|
+
</section>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Settings → Projects → Worktrees: the list, and the two settings that decide
|
|
8
|
+
* where a new one lands.
|
|
9
|
+
*
|
|
10
|
+
* A row carries what the reader actually asks of a list of worktrees: which
|
|
11
|
+
* branch is held there (and therefore cannot be checked out anywhere else), how
|
|
12
|
+
* many chats start in it, and whether it is dirty. `missing` is its own line
|
|
13
|
+
* because a worktree whose folder was deleted outside Kabmo still owns its
|
|
14
|
+
* sessions, and hiding it would hide them.
|
|
15
|
+
*
|
|
16
|
+
* The two fields below the list are the settings a create reads on the server
|
|
17
|
+
* (`worktreeDirectory`, `worktreeCarryIgnored`), so what is typed here is what
|
|
18
|
+
* the next create uses — including one started by a scheduled task.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { useState } from 'preact/hooks';
|
|
22
|
+
import { readKabmoSetting, writeSetting } from '@/shared/lib/settings/client';
|
|
23
|
+
import { useWorktrees } from '@/client/hooks/workspace/worktree/index';
|
|
24
|
+
import { removeWorktree } from '@/client/hooks/workspace/worktree/index';
|
|
25
|
+
import { WORKTREE_CARRY_SETTING, WORKTREE_DIRECTORY_SETTING } from '@/shared/lib/fs/worktree/settings-keys';
|
|
26
|
+
import { branchDeletable, removalRefusal, worktreeRowDetail } from '@/client/components/settings/categories/project-settings/worktree-row';
|
|
27
|
+
|
|
28
|
+
export interface WorktreeSettingsProps {
|
|
29
|
+
/** The selected project's `project_path`; the section is inert without one. */
|
|
30
|
+
root: string | null;
|
|
31
|
+
folderId: number | null;
|
|
32
|
+
/** Report a refusal — the section never claims a removal it did not get. */
|
|
33
|
+
onReport: (message: string, tone?: 'success' | 'error') => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function WorktreeSettings({ root, folderId, onReport }: WorktreeSettingsProps) {
|
|
37
|
+
const { worktrees, base, loading, error, isRepo, refresh } = useWorktrees({ root, folderId }, Boolean(root));
|
|
38
|
+
const [directory, setDirectory] = useState(() => {
|
|
39
|
+
const stored = readKabmoSetting<string>(WORKTREE_DIRECTORY_SETTING);
|
|
40
|
+
return typeof stored === 'string' ? stored : '';
|
|
41
|
+
});
|
|
42
|
+
const [carry, setCarry] = useState(() => readKabmoSetting<boolean>(WORKTREE_CARRY_SETTING) !== false);
|
|
43
|
+
// `detached` rows name a commit instead of a branch, and the payload carries it.
|
|
44
|
+
|
|
45
|
+
const [removing, setRemoving] = useState<string | null>(null);
|
|
46
|
+
|
|
47
|
+
if (!root) {
|
|
48
|
+
return <p className="text-xs text-ink/50">Bind this workspace to a repository to manage its worktrees.</p>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const removable = worktrees.filter((worktree) => !worktree.isPrimary);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="flex flex-col gap-4">
|
|
55
|
+
<section className="flex flex-col gap-1">
|
|
56
|
+
<h3 className="text-xs font-semibold text-ink">Worktrees</h3>
|
|
57
|
+
{loading && <p className="text-[11px] text-ink/50">Reading worktrees…</p>}
|
|
58
|
+
{error && (
|
|
59
|
+
// A failed read is REPORTED, never rendered as "none": the two look
|
|
60
|
+
// identical on screen and only one of them is actionable.
|
|
61
|
+
<p className="text-[11px] text-error" title={error}>
|
|
62
|
+
Worktrees could not be read: {error}
|
|
63
|
+
</p>
|
|
64
|
+
)}
|
|
65
|
+
{!loading && !error && isRepo === false && (
|
|
66
|
+
// The list is empty for a reason the row has to NAME: a folder that
|
|
67
|
+
// is not a repository can never have a worktree, and "No worktrees
|
|
68
|
+
// yet" would read as one that could be created.
|
|
69
|
+
<p className="text-[11px] text-ink/50">Worktrees need a Git repository.</p>
|
|
70
|
+
)}
|
|
71
|
+
{!loading && !error && isRepo !== false && removable.length === 0 && (
|
|
72
|
+
<p className="text-[11px] text-ink/50">No worktrees yet.</p>
|
|
73
|
+
)}
|
|
74
|
+
{removable.map((worktree) => (
|
|
75
|
+
<div key={worktree.path} className="group flex items-center gap-2 rounded-md px-1 py-1.5 hover:bg-ink/5">
|
|
76
|
+
<div className="min-w-0 flex-1">
|
|
77
|
+
<p className="truncate text-xs text-ink">{worktree.branch || 'detached'}</p>
|
|
78
|
+
<p className="truncate text-[10.5px] text-ink/50" title={worktree.path}>
|
|
79
|
+
{worktreeRowDetail(worktree)}
|
|
80
|
+
</p>
|
|
81
|
+
</div>
|
|
82
|
+
<button
|
|
83
|
+
type="button"
|
|
84
|
+
disabled={removing === worktree.path || removalRefusal(worktree) !== null}
|
|
85
|
+
aria-label={`Remove worktree ${worktree.branch || worktree.path}`}
|
|
86
|
+
title={removalRefusal(worktree) ?? 'Remove worktree'}
|
|
87
|
+
onClick={() => {
|
|
88
|
+
setRemoving(worktree.path);
|
|
89
|
+
void removeWorktree({ root, folderId }, worktree.path, { deleteBranch: branchDeletable(worktree) })
|
|
90
|
+
.then((result) => {
|
|
91
|
+
onReport(
|
|
92
|
+
result.branchDeleted
|
|
93
|
+
? `Removed ${worktree.branch} and its branch`
|
|
94
|
+
: `Removed ${worktree.branch}${result.branchKept ? ' (branch kept)' : ''}`,
|
|
95
|
+
'success',
|
|
96
|
+
);
|
|
97
|
+
return refresh();
|
|
98
|
+
})
|
|
99
|
+
.catch((cause: unknown) => onReport(cause instanceof Error ? cause.message : String(cause), 'error'))
|
|
100
|
+
.finally(() => setRemoving(null));
|
|
101
|
+
}}
|
|
102
|
+
className="shrink-0 rounded px-2 py-1 text-[11px] text-ink/60 hover:bg-error/10 hover:text-error disabled:opacity-40"
|
|
103
|
+
>
|
|
104
|
+
Remove
|
|
105
|
+
</button>
|
|
106
|
+
</div>
|
|
107
|
+
))}
|
|
108
|
+
</section>
|
|
109
|
+
|
|
110
|
+
<label className="flex flex-col gap-1">
|
|
111
|
+
<span className="text-xs font-semibold text-ink">Worktree location</span>
|
|
112
|
+
<input
|
|
113
|
+
value={directory}
|
|
114
|
+
onChange={(event) => setDirectory(event.currentTarget.value)}
|
|
115
|
+
onBlur={() => writeSetting(WORKTREE_DIRECTORY_SETTING, directory.trim())}
|
|
116
|
+
placeholder={base}
|
|
117
|
+
className="h-8 rounded-md border border-ink/15 bg-paper px-2 text-xs text-ink outline-none focus:border-ink/40"
|
|
118
|
+
/>
|
|
119
|
+
<span className="text-[10.5px] text-ink/50">
|
|
120
|
+
Where new worktrees are created. Relative paths are resolved against the project; ~ is your home directory.
|
|
121
|
+
Empty uses {base}.
|
|
122
|
+
</span>
|
|
123
|
+
</label>
|
|
124
|
+
|
|
125
|
+
<label className="flex items-start gap-2">
|
|
126
|
+
<input
|
|
127
|
+
type="checkbox"
|
|
128
|
+
checked={carry}
|
|
129
|
+
onChange={(event) => {
|
|
130
|
+
const next = event.currentTarget.checked;
|
|
131
|
+
setCarry(next);
|
|
132
|
+
writeSetting(WORKTREE_CARRY_SETTING, next);
|
|
133
|
+
}}
|
|
134
|
+
className="mt-0.5"
|
|
135
|
+
/>
|
|
136
|
+
<span className="flex flex-col gap-0.5">
|
|
137
|
+
<span className="text-xs font-semibold text-ink">Copy ignored files into new worktrees</span>
|
|
138
|
+
<span className="text-[10.5px] text-ink/50">
|
|
139
|
+
A new worktree gets the project's ignored build folders (node_modules, .venv, target) copied in, so it works
|
|
140
|
+
without reinstalling.
|
|
141
|
+
</span>
|
|
142
|
+
</span>
|
|
143
|
+
</label>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What a worktree row says, and when its Remove control is refused.
|
|
8
|
+
*
|
|
9
|
+
* These are the sentences a reader decides on, so they are functions rather than
|
|
10
|
+
* inline template strings: a markup test would prove the component renders
|
|
11
|
+
* *something*, while the questions here are "which branch is held" (and therefore
|
|
12
|
+
* cannot be checked out anywhere else), "how many chats start here", and "would
|
|
13
|
+
* this removal lose work".
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, expect, test } from 'bun:test';
|
|
17
|
+
import {
|
|
18
|
+
branchDeletable,
|
|
19
|
+
removalRefusal,
|
|
20
|
+
worktreeRowDetail,
|
|
21
|
+
} from '@/client/components/settings/categories/project-settings/worktree-row';
|
|
22
|
+
import type { WorktreeInfo } from '@/shared/lib/workspace/worktree/types';
|
|
23
|
+
|
|
24
|
+
const base: WorktreeInfo = {
|
|
25
|
+
path: '/data/worktrees/abc/cosmic-dolphin',
|
|
26
|
+
branch: 'wt/cosmic-dolphin',
|
|
27
|
+
head: 'a1b2c3d4e5f6',
|
|
28
|
+
detached: false,
|
|
29
|
+
missing: false,
|
|
30
|
+
isPrimary: false,
|
|
31
|
+
dirty: 0,
|
|
32
|
+
aheadOfStart: 0,
|
|
33
|
+
startRef: 'HEAD',
|
|
34
|
+
sessions: 2,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
describe('worktreeRowDetail', () => {
|
|
38
|
+
test('names the branch, the chat count and the tree state', () => {
|
|
39
|
+
expect(worktreeRowDetail(base)).toBe('wt/cosmic-dolphin checked out here · 2 chats · clean');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('a single chat is not "1 chats"', () => {
|
|
43
|
+
expect(worktreeRowDetail({ ...base, sessions: 1 })).toContain('1 chat ·');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('changed files are counted', () => {
|
|
47
|
+
expect(worktreeRowDetail({ ...base, dirty: 3 })).toContain('3 changed');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('a detached row names a commit instead of a branch', () => {
|
|
51
|
+
expect(worktreeRowDetail({ ...base, branch: '', detached: true })).toContain('detached @ a1b2c3d');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('a missing worktree says its chats still open — that is why the row stays', () => {
|
|
55
|
+
expect(worktreeRowDetail({ ...base, missing: true })).toContain('its chats still open');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('removal rules', () => {
|
|
60
|
+
test('a clean worktree with no commits of its own may lose its branch', () => {
|
|
61
|
+
expect(branchDeletable(base)).toBe(true);
|
|
62
|
+
expect(removalRefusal(base)).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('a branch with commits of its own is kept, and the checkbox is disabled', () => {
|
|
66
|
+
expect(branchDeletable({ ...base, aheadOfStart: 2 })).toBe(false);
|
|
67
|
+
// The worktree itself is still removable: only the branch is protected.
|
|
68
|
+
expect(removalRefusal({ ...base, aheadOfStart: 2 })).toBeNull();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('a dirty worktree is refused with its count', () => {
|
|
72
|
+
expect(removalRefusal({ ...base, dirty: 1 })).toBe('1 uncommitted file here would be lost.');
|
|
73
|
+
expect(removalRefusal({ ...base, dirty: 4 })).toBe('4 uncommitted files here would be lost.');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('the main checkout is never removable', () => {
|
|
77
|
+
expect(removalRefusal({ ...base, isPrimary: true })).toContain('main checkout');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('a detached row has no branch to delete', () => {
|
|
81
|
+
expect(branchDeletable({ ...base, branch: '', detached: true })).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
});
|