@kabmo-labs/kabmo 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/client/index.html +1 -1
- package/dist/client/index.js +403 -384
- package/dist/client/static/js/{index-0wvbca0b.js → index-bpptfkz2.js} +905 -875
- package/dist/client/static/js/index-k315p1e8.css +1 -0
- package/package.json +10 -1
- package/src/client/components/common/code-surface/documents.test.ts +120 -0
- package/src/client/components/common/code-surface/documents.ts +201 -0
- package/src/client/components/common/code-surface/extensions.test.ts +110 -0
- package/src/client/components/common/code-surface/extensions.ts +162 -0
- package/src/client/components/common/code-surface/handle.ts +102 -0
- package/src/client/components/common/code-surface/index.tsx +253 -314
- package/src/client/components/common/code-surface/keymap.test.ts +133 -0
- package/src/client/components/common/code-surface/keymap.ts +267 -0
- package/src/client/components/common/code-surface/marks.ts +74 -0
- package/src/client/components/common/code-surface/shiki.ts +234 -0
- package/src/client/components/common/code-surface/sync.test.ts +185 -0
- package/src/client/components/common/code-surface/sync.ts +58 -0
- package/src/client/components/common/code-surface/theme.test.ts +202 -0
- package/src/client/components/common/code-surface/theme.ts +111 -0
- package/src/client/components/common/editor-view-buttons/index.test.tsx +148 -0
- package/src/client/components/common/editor-view-buttons/index.tsx +109 -0
- package/src/client/components/common/header-controls/index.tsx +24 -1
- package/src/client/components/common/header-controls/shell-chrome.test.tsx +272 -0
- package/src/client/components/common/shell-update-button/Modal.tsx +163 -0
- package/src/client/components/common/shell-update-button/index.test.tsx +251 -0
- package/src/client/components/common/shell-update-button/index.tsx +125 -0
- package/src/client/components/common/toolbar-row/index.test.tsx +166 -0
- package/src/client/components/common/toolbar-row/index.tsx +189 -0
- package/src/client/components/common/workspace-options-menu/index.tsx +19 -1
- package/src/client/components/layout/desktop-layout/TopNavbar.tsx +5 -6
- package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +41 -78
- package/src/client/components/layout/desktop-layout/index.tsx +55 -69
- package/src/client/components/layout/desktop-layout/resizer/index.tsx +6 -3
- package/src/client/components/layout/session-sidebar/CategoryHeader.tsx +24 -0
- package/src/client/components/layout/session-sidebar/CategoryHeader.worktree.test.tsx +126 -0
- package/src/client/components/layout/session-sidebar/CategoryItem.tsx +4 -0
- package/src/client/components/layout/session-sidebar/Footer.tsx +8 -15
- package/src/client/components/layout/session-sidebar/SessionItem.tsx +12 -0
- package/src/client/components/layout/session-sidebar/SessionList.tsx +4 -0
- package/src/client/components/layout/session-sidebar/SessionsView.tsx +24 -0
- package/src/client/components/layout/session-sidebar/WorktreeBadge.test.ts +48 -0
- package/src/client/components/layout/session-sidebar/WorktreeBadge.tsx +55 -0
- package/src/client/components/mobile/LayoutWrapper.tsx +23 -16
- package/src/client/components/mobile/RightSidebar.tsx +21 -10
- package/src/client/components/mobile/SessionSidebar.tsx +8 -0
- package/src/client/components/mobile/header-titlebar.test.tsx +172 -0
- package/src/client/components/mobile/mobile-main-view/Header.tsx +10 -1
- package/src/client/components/mobile/mobile-right-sidebar/FullDiff.tsx +3 -0
- package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +63 -38
- package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.test.tsx +137 -0
- package/src/client/components/mobile/mobile-right-sidebar/OutlineButton.tsx +14 -3
- package/src/client/components/mobile/mobile-session-sidebar/Footer.tsx +9 -21
- package/src/client/components/mobile/mobile-session-sidebar/Header.tsx +6 -2
- package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +16 -0
- package/src/client/components/mobile/mobile-session-sidebar/List.tsx +4 -0
- package/src/client/components/mobile/mobile-session-sidebar/SessionRow.tsx +5 -0
- package/src/client/components/mobile/mobile-session-sidebar/WorktreeDialog.tsx +49 -0
- package/src/client/components/mobile/worktree-session.ts +51 -0
- package/src/client/components/settings/Modal.tsx +8 -0
- package/src/client/components/settings/categories/AppearanceSettings.tsx +3 -0
- package/src/client/components/settings/categories/ProjectSettings.tsx +22 -8
- package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.test.tsx +167 -0
- package/src/client/components/settings/categories/appearance-settings/EditorThemeSection.tsx +184 -0
- package/src/client/components/settings/categories/project-settings/WorktreeSettings.tsx +146 -0
- package/src/client/components/settings/categories/project-settings/worktree-row.test.ts +83 -0
- package/src/client/components/settings/categories/project-settings/worktree-row.ts +47 -0
- package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +36 -55
- package/src/client/components/workspace/chat-timeline/PendingChatView.tsx +8 -0
- package/src/client/components/workspace/chat-timeline/SystemNotice.tsx +9 -4
- package/src/client/components/workspace/chat-timeline/TaskResultContent.tsx +9 -7
- package/src/client/components/workspace/chat-timeline/index.tsx +5 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.test.tsx +132 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorkspacePicker.tsx +120 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.test.tsx +319 -0
- package/src/client/components/workspace/chat-timeline/target-picker/WorktreePicker.tsx +285 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/ask-panel/index.test.ts +2 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/hashline-patch/index.tsx +34 -29
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/AstEdit.tsx +11 -9
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Bash.tsx +10 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Edit.tsx +25 -8
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.test.tsx +146 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Eval.tsx +32 -17
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.test.ts +114 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/FileMatches.tsx +111 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Read.tsx +7 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/SearchTool.tsx +9 -59
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ArtifactBody.tsx +2 -7
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.test.ts +27 -28
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/DiffView.tsx +39 -26
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/ExcerptCode.tsx +8 -8
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/HighlightedCode.tsx +35 -0
- package/src/client/components/workspace/chat-timeline/tool-renderers/shared/JsonCodeBlock.tsx +4 -4
- package/src/client/components/workspace/diff-panel/MergeDiffView.tsx +179 -0
- package/src/client/components/workspace/diff-panel/ThemedMergeDiffView.tsx +24 -0
- package/src/client/components/workspace/diff-panel/Toolbar.tsx +224 -186
- package/src/client/components/workspace/diff-panel/ToolbarSegments.tsx +98 -0
- package/src/client/components/workspace/diff-panel/index.tsx +76 -145
- package/src/client/components/workspace/diff-panel/merge.ts +315 -0
- package/src/client/components/workspace/editor/Body.tsx +4 -7
- package/src/client/components/workspace/editor/CommandPalette.tsx +2 -2
- package/src/client/components/workspace/editor/FileDocument.tsx +58 -63
- package/src/client/components/workspace/editor/FilePane.tsx +4 -7
- package/src/client/components/workspace/editor/FindWidget.tsx +7 -2
- package/src/client/components/workspace/editor/Toolbar.tsx +220 -123
- package/src/client/components/workspace/editor/index.tsx +7 -9
- package/src/client/components/workspace/file-explorer/ContextMenu.tsx +2 -1
- package/src/client/components/workspace/file-explorer/Modals.tsx +40 -12
- package/src/client/components/workspace/file-explorer/OutlineSection.test.tsx +18 -0
- package/src/client/components/workspace/file-explorer/OutlineSection.tsx +10 -3
- package/src/client/components/workspace/file-explorer/RootMenu.tsx +14 -5
- package/src/client/components/workspace/file-explorer/Toolbar.tsx +74 -0
- package/src/client/components/workspace/file-explorer/TreeItem.tsx +38 -3
- package/src/client/components/workspace/file-explorer/filter.ts +47 -0
- package/src/client/components/workspace/file-explorer/index.tsx +77 -64
- package/src/client/components/workspace/file-explorer/local-edit.test.ts +135 -0
- package/src/client/components/workspace/search-panel/Body.tsx +1 -1
- package/src/client/components/workspace/worktree-modal/index.test.tsx +118 -0
- package/src/client/components/workspace/worktree-modal/index.tsx +261 -0
- package/src/client/hooks/chat/omp/reconnect.test.ts +338 -0
- package/src/client/hooks/chat/omp/reconnect.ts +187 -0
- package/src/client/hooks/chat/omp/run-liveness.test.ts +71 -0
- package/src/client/hooks/chat/omp/run-liveness.ts +65 -0
- package/src/client/hooks/chat/omp/session-list.activity.test.ts +114 -0
- package/src/client/hooks/chat/omp/session-list.test-util.ts +164 -0
- package/src/client/hooks/chat/omp/session-list.test.ts +30 -150
- package/src/client/hooks/chat/omp/session-list.tsx +69 -9
- package/src/client/hooks/chat/timeline/generating.ts +19 -2
- package/src/client/hooks/chat/timeline/index.ts +27 -28
- package/src/client/hooks/chat/timeline/lost-run.test.ts +329 -0
- package/src/client/hooks/chat/timeline/lost-run.ts +177 -0
- package/src/client/hooks/chat/timeline/send-execute.test.ts +2 -0
- package/src/client/hooks/chat/timeline/send-steer.test.ts +2 -0
- package/src/client/hooks/chat/timeline/send.test.ts +2 -0
- package/src/client/hooks/chat/timeline/send.ts +11 -1
- package/src/client/hooks/chat/timeline/session-load.test-util.ts +205 -0
- package/src/client/hooks/chat/timeline/session-load.test.ts +61 -162
- package/src/client/hooks/chat/timeline/session-load.ts +69 -37
- package/src/client/hooks/chat/timeline/session-switch.test.ts +65 -0
- package/src/client/hooks/chat/timeline/session-switch.ts +63 -0
- package/src/client/hooks/chat/timeline/spawn-target.ts +52 -0
- package/src/client/hooks/chat/timeline/tool-open.test.ts +74 -0
- package/src/client/hooks/chat/timeline/tool-open.ts +52 -8
- package/src/client/hooks/code/deferred-highlight.test.ts +182 -0
- package/src/client/hooks/code/deferred-highlight.ts +129 -0
- package/src/client/hooks/editor/find-state.ts +1 -1
- package/src/client/hooks/editor/theme.test.ts +206 -0
- package/src/client/hooks/editor/theme.ts +82 -0
- package/src/client/hooks/editor/use-editor-drop.test.ts +101 -0
- package/src/client/hooks/editor/use-editor-drop.ts +67 -14
- package/src/client/hooks/editor/use-editor-find.ts +7 -7
- package/src/client/hooks/editor/use-outline-view.test.ts +182 -0
- package/src/client/hooks/editor/use-outline-view.ts +94 -7
- package/src/client/hooks/editor/use-scroll-restore.test.ts +55 -2
- package/src/client/hooks/editor/use-scroll-restore.ts +124 -4
- package/src/client/hooks/editor/use-search-reveal.ts +1 -1
- package/src/client/hooks/ui/long-press.test.ts +174 -0
- package/src/client/hooks/ui/long-press.ts +112 -0
- package/src/client/hooks/ui/settings-modal.ts +56 -0
- package/src/client/hooks/ui/updates/shell-download.test.ts +75 -0
- package/src/client/hooks/ui/updates/shell-download.ts +153 -28
- package/src/client/hooks/ui/updates/shell-offer.test.ts +213 -0
- package/src/client/hooks/ui/updates/shell-update.test.ts +140 -0
- package/src/client/hooks/ui/updates/shell-update.ts +119 -0
- package/src/client/hooks/workspace/diff-file.ts +29 -10
- package/src/client/hooks/workspace/diff-git-actions.ts +147 -0
- package/src/client/hooks/workspace/editor-view.ts +61 -0
- package/src/client/hooks/workspace/file-create.ts +41 -11
- package/src/client/hooks/workspace/file-listing.ts +56 -1
- package/src/client/hooks/workspace/file-tabs.test.ts +62 -5
- package/src/client/hooks/workspace/file-tabs.ts +24 -11
- package/src/client/hooks/workspace/file-tree-actions.ts +37 -5
- package/src/client/hooks/workspace/navbar-session-actions.ts +72 -0
- package/src/client/hooks/workspace/worktree/index.ts +165 -0
- package/src/client/lib/desktop/bridge.ts +45 -0
- package/src/client/lib/search-reveal.ts +1 -1
- package/src/client/main.tsx +5 -1
- package/src/client/tailwind.css +56 -8
- package/src/server/index.ts +7 -2
- package/src/server/lib/auth/guard.test.ts +28 -0
- package/src/server/lib/auth/guard.ts +10 -0
- package/src/server/lib/bundler/titlebar-css.test.ts +42 -1
- package/src/server/lib/db/schema.ts +4 -0
- package/src/server/lib/db/worktree-registry.server.ts +91 -0
- package/src/server/lib/fs/canonical.ts +52 -0
- package/src/server/lib/fs/relative-path.ts +48 -0
- package/src/server/lib/fs/root-worktree.test.ts +116 -0
- package/src/server/lib/fs/root.ts +39 -10
- package/src/server/lib/fs/worktree/carry.ts +134 -0
- package/src/server/lib/fs/worktree/create.test.ts +249 -0
- package/src/server/lib/fs/worktree/create.ts +262 -0
- package/src/server/lib/fs/worktree/errors.ts +43 -0
- package/src/server/lib/fs/worktree/list.test.ts +207 -0
- package/src/server/lib/fs/worktree/list.ts +142 -0
- package/src/server/lib/fs/worktree/location.test.ts +206 -0
- package/src/server/lib/fs/worktree/location.ts +114 -0
- package/src/server/lib/fs/worktree/project-id.ts +53 -0
- package/src/server/lib/fs/worktree/remove.test.ts +242 -0
- package/src/server/lib/fs/worktree/remove.ts +152 -0
- package/src/server/lib/fs/worktree/settings.server.ts +56 -0
- package/src/server/lib/fs/worktree/topology.ts +142 -0
- package/src/server/lib/lifecycle/claim-port.test.ts +156 -0
- package/src/server/lib/lifecycle/port-guard.test.ts +20 -164
- package/src/server/lib/lifecycle/port-guard.ts +39 -7
- package/src/server/lib/lifecycle/probe-health.test.ts +166 -0
- package/src/server/lib/lifecycle/probe.test.ts +77 -164
- package/src/server/lib/lifecycle/probe.ts +121 -13
- package/src/server/lib/omp/rpc/frame-fold-signals.test.ts +112 -0
- package/src/server/lib/omp/rpc/frame-fold.ts +10 -5
- package/src/server/lib/omp/session/sidebar-activity.server.test.ts +162 -0
- package/src/server/lib/omp/session/sidebar-activity.server.ts +70 -0
- package/src/server/lib/omp/session/sidebar-data.canonical.test.ts +155 -0
- package/src/server/lib/omp/session/sidebar-data.server.ts +28 -1
- package/src/server/lib/omp/session/telemetry/compute.ts +13 -1
- package/src/server/lib/omp/session/telemetry.test.ts +33 -0
- package/src/server/lib/panels/local-edits.server.ts +33 -0
- package/src/server/lib/panels/sync.fixture.test-util.ts +92 -0
- package/src/server/lib/panels/sync.local-edits.test.ts +150 -0
- package/src/server/lib/panels/sync.server.test.ts +125 -147
- package/src/server/lib/panels/sync.server.ts +153 -48
- package/src/server/lib/realtime/signals.server.ts +10 -0
- package/src/server/lib/realtime/topics/catalog.server.ts +4 -1
- package/src/server/lib/realtime/topics.server.ts +16 -0
- package/src/server/lib/updates/shell-assets.test.ts +114 -0
- package/src/server/lib/updates/shell-assets.ts +200 -0
- package/src/server/lib/updates/shell-cache.ts +105 -0
- package/src/server/lib/updates/shell-pages.test.ts +187 -0
- package/src/server/lib/updates/shell-pages.ts +157 -0
- package/src/server/lib/updates/shell-release-desktop.test.ts +166 -0
- package/src/server/lib/updates/shell-release.test.ts +59 -71
- package/src/server/lib/updates/shell-release.ts +178 -198
- package/src/server/routes/fs/action-create.test.ts +18 -13
- package/src/server/routes/fs/action-nested.test.ts +184 -0
- package/src/server/routes/fs/action.ts +47 -16
- package/src/server/routes/fs/git-merge.test.ts +11 -1
- package/src/server/routes/fs/read.test.ts +24 -7
- package/src/server/routes/fs/scope.test.ts +15 -10
- package/src/server/routes/fs/scope.ts +3 -14
- package/src/server/routes/index.ts +2 -0
- package/src/server/routes/updates/android.ts +6 -1
- package/src/server/routes/updates/desktop.ts +59 -0
- package/src/server/routes/updates/index.ts +2 -0
- package/src/server/routes/updates/ios.ts +5 -1
- package/src/server/routes/worktrees/create.ts +138 -0
- package/src/server/routes/worktrees/index.ts +25 -0
- package/src/server/routes/worktrees/list.ts +134 -0
- package/src/server/routes/worktrees/remove.ts +102 -0
- package/src/server/routes/worktrees/routes.test.ts +326 -0
- package/src/server/routes/worktrees/shared.ts +90 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.test.ts +36 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.ts +18 -0
- package/src/shared/lib/chat/timeline/run-signals.test.ts +2 -0
- package/src/shared/lib/code/editor/comment-syntax.test.ts +37 -0
- package/src/shared/lib/code/editor/comment-syntax.ts +55 -0
- package/src/shared/lib/code/editor/find-replace.test.ts +134 -0
- package/src/shared/lib/code/editor/{find.ts → find-replace.ts} +23 -107
- package/src/shared/lib/code/editor/{find.test.ts → find-scan.test.ts} +19 -73
- package/src/shared/lib/code/editor/find-scan.ts +177 -0
- package/src/shared/lib/code/editor/keymap.ts +3 -3
- package/src/shared/lib/code/editor/lines.test.ts +7 -55
- package/src/shared/lib/code/editor/lines.ts +8 -48
- package/src/shared/lib/code/editor/marks.test.ts +4 -5
- package/src/shared/lib/code/editor/marks.ts +13 -25
- package/src/shared/lib/code/editor/theme-loader.ts +97 -0
- package/src/shared/lib/code/editor/themes.generated.ts +156 -0
- package/src/shared/lib/code/editor/themes.test.ts +165 -0
- package/src/shared/lib/code/editor/themes.ts +165 -0
- package/src/shared/lib/code/highlight-memo.test.ts +103 -0
- package/src/shared/lib/code/highlight-memo.ts +149 -0
- package/src/shared/lib/code/highlighter-warmup.ts +112 -0
- package/src/shared/lib/code/highlighter.ts +3 -1
- package/src/shared/lib/code/idle.ts +37 -0
- package/src/shared/lib/code/outline/rows.test.ts +96 -0
- package/src/shared/lib/code/outline/rows.ts +94 -0
- package/src/shared/lib/code/syntax-highlight.test.ts +1 -79
- package/src/shared/lib/code/syntax-highlight.ts +2 -3
- package/src/shared/lib/fs/file-kind-classify.test.ts +25 -11
- package/src/shared/lib/fs/file-kind.ts +47 -13
- package/src/shared/lib/fs/file-tree.test.ts +183 -0
- package/src/shared/lib/fs/file-tree.ts +159 -0
- package/src/shared/lib/fs/file-url.test.ts +52 -0
- package/src/shared/lib/fs/file-url.ts +37 -0
- package/src/shared/lib/fs/patch-sides.test.ts +114 -0
- package/src/shared/lib/fs/patch-sides.ts +139 -0
- package/src/shared/lib/fs/paths.test.ts +24 -1
- package/src/shared/lib/fs/paths.ts +17 -0
- package/src/shared/lib/fs/worktree/settings-keys.ts +18 -0
- package/src/shared/lib/omp/session/prewarm.ts +7 -0
- package/src/shared/lib/realtime/protocol.ts +16 -0
- package/src/shared/lib/ui/toolbar-overflow.test.ts +83 -0
- package/src/shared/lib/ui/toolbar-overflow.ts +86 -0
- package/src/shared/lib/updates/shell-arch.ts +54 -0
- package/src/shared/lib/updates/shell-ua.ts +24 -4
- package/src/shared/lib/workspace/active-project.ts +23 -0
- package/src/shared/lib/workspace/editor-view-mode.test.ts +70 -0
- package/src/shared/lib/workspace/editor-view-mode.ts +48 -0
- package/src/shared/lib/workspace/panel-widths.test.ts +30 -0
- package/src/shared/lib/workspace/panel-widths.ts +25 -5
- package/src/shared/lib/workspace/session-state/cache.ts +159 -0
- package/src/shared/lib/workspace/session-state/harness.test-util.ts +55 -0
- package/src/shared/lib/workspace/session-state/merge.ts +100 -0
- package/src/shared/lib/workspace/session-state/store-load.test.ts +206 -0
- package/src/shared/lib/workspace/session-state/store.test.ts +17 -139
- package/src/shared/lib/workspace/session-state/store.ts +92 -130
- package/src/shared/lib/workspace/workspace-modules.test.ts +49 -1
- package/src/shared/lib/workspace/workspace-panel-widths.ts +124 -0
- package/src/shared/lib/workspace/worktree/branch-name.test.ts +95 -0
- package/src/shared/lib/workspace/worktree/branch-name.ts +115 -0
- package/src/shared/lib/workspace/worktree/types.ts +104 -0
- package/src/shared/types/fs.ts +10 -0
- package/src/shared/types/realtime.ts +29 -0
- package/src/shared/types/settings/state.ts +10 -0
- package/src/shared/types/workspace.ts +14 -0
- package/src/test-support/file-explorer-disk.test-util.ts +76 -0
- package/src/test-support/file-explorer.test-util.ts +71 -40
- package/src/test-support/loopback.ts +183 -0
- package/dist/client/static/js/index-nzyjf92m.css +0 -1
- package/src/client/components/common/code-editor/handler.test.ts +0 -274
- package/src/client/components/common/code-editor/handler.ts +0 -175
- package/src/client/components/common/code-editor/index.tsx +0 -314
- package/src/client/components/common/code-surface/measure.ts +0 -148
- package/src/client/components/workspace/diff-panel/SplitView.tsx +0 -209
- package/src/client/components/workspace/diff-panel/UnifiedView.tsx +0 -150
- package/src/client/hooks/editor/scroll-container.test.ts +0 -102
- package/src/client/hooks/editor/scroll-container.ts +0 -26
- package/src/client/hooks/editor/use-code-window.ts +0 -100
- package/src/client/hooks/editor/use-editor-history.test.ts +0 -216
- package/src/client/hooks/editor/use-editor-history.ts +0 -134
- package/src/client/hooks/editor/use-occurrences.test.ts +0 -236
- package/src/client/hooks/editor/use-occurrences.ts +0 -98
- package/src/shared/lib/code/editor/commands.test.ts +0 -155
- package/src/shared/lib/code/editor/commands.ts +0 -260
- package/src/shared/lib/code/editor/edits.test.ts +0 -100
- package/src/shared/lib/code/editor/edits.ts +0 -122
- package/src/shared/lib/code/editor/history.test.ts +0 -118
- package/src/shared/lib/code/editor/history.ts +0 -101
- package/src/shared/lib/code/editor/layers.test.ts +0 -77
- package/src/shared/lib/code/editor/layers.ts +0 -67
- package/src/shared/lib/code/editor/line-edits.test.ts +0 -131
- package/src/shared/lib/code/editor/line-edits.ts +0 -159
- package/src/shared/lib/code/editor/occurrences.test.ts +0 -117
- package/src/shared/lib/code/editor/occurrences.ts +0 -111
- package/src/shared/lib/code/editor/reset-css.test.ts +0 -35
- package/src/shared/lib/code/editor/reset-css.ts +0 -32
- package/src/shared/lib/code/editor/text-commands.test.ts +0 -178
- package/src/shared/lib/code/editor/text-commands.ts +0 -154
- package/src/shared/lib/code/lazy-window.test.ts +0 -161
- package/src/shared/lib/code/lazy-window.ts +0 -156
- package/src/shared/lib/code/windowed-highlight.ts +0 -114
- package/src/shared/lib/fs/diff-parser.test.ts +0 -116
- package/src/shared/lib/fs/diff-parser.ts +0 -213
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Installing a desktop shell's own update, from the console.
|
|
8
|
+
*
|
|
9
|
+
* ## Why the shell does the work
|
|
10
|
+
*
|
|
11
|
+
* A page cannot replace the executable it runs inside, and a browser download
|
|
12
|
+
* leaves a file in a folder the user has to find and open. So the button asks
|
|
13
|
+
* the SHELL (`update_apply` over the bridge), and the shell downloads, verifies,
|
|
14
|
+
* replaces and relaunches. What this hook owns is only the part a page can own:
|
|
15
|
+
* the run's state, and reporting the outcome where the user clicked.
|
|
16
|
+
*
|
|
17
|
+
* ## Why it is a hook rather than a function at the call site
|
|
18
|
+
*
|
|
19
|
+
* Both sidebars draw this control and each one unmounts at a different moment (a
|
|
20
|
+
* phone closes the drawer, a desktop swaps sessions). The state has to live with
|
|
21
|
+
* the control that shows it, and the two must report the SAME vocabulary — so the
|
|
22
|
+
* phases and their sentences are here, not in either footer.
|
|
23
|
+
*
|
|
24
|
+
* ## A failure is always a sentence
|
|
25
|
+
*
|
|
26
|
+
* The shell sends its own reason ("the downloaded file is not a signed Kabmo
|
|
27
|
+
* build", "the installer refused with code 5"), which is the only thing that can
|
|
28
|
+
* tell a user what to do next. Nothing here replaces it with a generic failure,
|
|
29
|
+
* and nothing reports success before the shell said so.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { useCallback, useRef, useState } from 'preact/hooks';
|
|
33
|
+
import {
|
|
34
|
+
applyShellUpdate,
|
|
35
|
+
bridgeError,
|
|
36
|
+
isDesktopShell,
|
|
37
|
+
type ShellUpdateOutcome,
|
|
38
|
+
} from '@/client/lib/desktop/bridge';
|
|
39
|
+
import type { ShellDownload } from '@/client/hooks/ui/updates/shell-download';
|
|
40
|
+
|
|
41
|
+
/** What the control is doing right now. */
|
|
42
|
+
export type ShellUpdatePhase = 'idle' | 'working' | 'done' | 'error';
|
|
43
|
+
|
|
44
|
+
export interface ShellUpdateState {
|
|
45
|
+
phase: ShellUpdatePhase;
|
|
46
|
+
/** The shell's own sentence, for `done` and `error`. */
|
|
47
|
+
message: string | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface UseShellUpdateResult extends ShellUpdateState {
|
|
51
|
+
/** Run the update this offer names. */
|
|
52
|
+
apply: (download: ShellDownload) => Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Forget the last run, back to `idle`.
|
|
55
|
+
*
|
|
56
|
+
* The dialog calls this when it OPENS: a run that failed last time must not greet
|
|
57
|
+
* the user with its old refusal the next time they press the control.
|
|
58
|
+
*/
|
|
59
|
+
reset: () => void;
|
|
60
|
+
/** True while a run is in flight, so the control can disable itself. */
|
|
61
|
+
working: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Whether this offer is something the shell can install by itself.
|
|
66
|
+
*
|
|
67
|
+
* Two facts, and both are required. The artifact must be a FILE (`direct`), and
|
|
68
|
+
* the page must be inside the desktop shell that can install it — an Android
|
|
69
|
+
* shell installs through `PackageInstaller` and iOS cannot install at all, so
|
|
70
|
+
* neither belongs to this path. A browser on a desktop gets neither, because
|
|
71
|
+
* there is no shell to ask.
|
|
72
|
+
*/
|
|
73
|
+
export function canSelfUpdate(download: ShellDownload | null | undefined): boolean {
|
|
74
|
+
if (!download) return false;
|
|
75
|
+
if (!download.direct) return false;
|
|
76
|
+
if (download.platform === 'android' || download.platform === 'ios') return false;
|
|
77
|
+
return isDesktopShell();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function useShellUpdate(): UseShellUpdateResult {
|
|
81
|
+
const [state, setState] = useState<ShellUpdateState>({ phase: 'idle', message: null });
|
|
82
|
+
// A second click while the shell is replacing itself would be a second
|
|
83
|
+
// installer beside the first, so the run is claimed by a ref rather than by
|
|
84
|
+
// the render state — a fast double click lands before the state is read.
|
|
85
|
+
const running = useRef(false);
|
|
86
|
+
|
|
87
|
+
const apply = useCallback(async (download: ShellDownload) => {
|
|
88
|
+
if (running.current) return;
|
|
89
|
+
running.current = true;
|
|
90
|
+
setState({ phase: 'working', message: null });
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const outcome: ShellUpdateOutcome = await applyShellUpdate(download.url, download.latest);
|
|
94
|
+
setState({
|
|
95
|
+
phase: outcome.success ? 'done' : 'error',
|
|
96
|
+
// The shell's sentence is the report. `?? null` rather than a fallback
|
|
97
|
+
// string: a shell that answered nothing has said nothing, and inventing
|
|
98
|
+
// "Update failed" over it would hide that.
|
|
99
|
+
message: outcome.message ?? null,
|
|
100
|
+
});
|
|
101
|
+
} catch (cause) {
|
|
102
|
+
// A refused command (an older shell with no `update_apply`) lands here,
|
|
103
|
+
// and its own message says so.
|
|
104
|
+
setState({ phase: 'error', message: bridgeError(cause) });
|
|
105
|
+
} finally {
|
|
106
|
+
running.current = false;
|
|
107
|
+
}
|
|
108
|
+
}, []);
|
|
109
|
+
|
|
110
|
+
const reset = useCallback(() => {
|
|
111
|
+
// A run in flight is NOT resettable: the shell is already replacing itself, and
|
|
112
|
+
// reporting `idle` over it would re-arm a control whose second press would start
|
|
113
|
+
// a second installer.
|
|
114
|
+
if (running.current) return;
|
|
115
|
+
setState({ phase: 'idle', message: null });
|
|
116
|
+
}, []);
|
|
117
|
+
|
|
118
|
+
return { ...state, apply, reset, working: state.phase === 'working' };
|
|
119
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
|
2
|
-
import type { DiffContentMode } from '@/client/components/workspace/diff-panel/Toolbar';
|
|
3
2
|
|
|
4
3
|
interface UseFileDiffOptions {
|
|
5
4
|
filePath: string;
|
|
@@ -7,11 +6,14 @@ interface UseFileDiffOptions {
|
|
|
7
6
|
isStaged: boolean;
|
|
8
7
|
repo: string;
|
|
9
8
|
root?: string;
|
|
10
|
-
contentMode: DiffContentMode;
|
|
11
9
|
}
|
|
12
10
|
|
|
13
11
|
export interface FileDiffState {
|
|
14
12
|
rawDiff: string;
|
|
13
|
+
/** The document before the change (HEAD, or the index for an unstaged read). */
|
|
14
|
+
oldContent: string;
|
|
15
|
+
/** The document after it (the working file). */
|
|
16
|
+
newContent: string;
|
|
15
17
|
currentStatus: string;
|
|
16
18
|
currentStaged: boolean;
|
|
17
19
|
isLoading: boolean;
|
|
@@ -31,8 +33,10 @@ export interface FileDiffState {
|
|
|
31
33
|
* earlier read overwriting a newer one, and a dependency on the staged/status
|
|
32
34
|
* copies re-keying the callback and firing a fetch per render.
|
|
33
35
|
*/
|
|
34
|
-
export function useFileDiff({ filePath, status, isStaged, repo, root
|
|
36
|
+
export function useFileDiff({ filePath, status, isStaged, repo, root }: UseFileDiffOptions): FileDiffState {
|
|
35
37
|
const [rawDiff, setRawDiff] = useState<string>('');
|
|
38
|
+
const [oldContent, setOldContent] = useState<string>('');
|
|
39
|
+
const [newContent, setNewContent] = useState<string>('');
|
|
36
40
|
const [currentStatus, setCurrentStatus] = useState<string>(status);
|
|
37
41
|
const [currentStaged, setCurrentStaged] = useState<boolean>(isStaged);
|
|
38
42
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
|
@@ -62,11 +66,9 @@ export function useFileDiff({ filePath, status, isStaged, repo, root, contentMod
|
|
|
62
66
|
staged: staged ? '1' : '0',
|
|
63
67
|
repo: repo || '.',
|
|
64
68
|
});
|
|
65
|
-
// `fullContext
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
// path updates while a fetch of its own is already in flight).
|
|
69
|
-
if (contentMode === 'full') params.set('fullContext', '1');
|
|
69
|
+
// No `fullContext`: the panel renders both SIDES of the file now, and
|
|
70
|
+
// how much of them is shown is a client-side collapse. Asking the server
|
|
71
|
+
// for the whole file as a patch would be a second answer to that question.
|
|
70
72
|
if (statusForFetch) params.set('status', statusForFetch);
|
|
71
73
|
if (root) params.set('root', root);
|
|
72
74
|
|
|
@@ -74,6 +76,8 @@ export function useFileDiff({ filePath, status, isStaged, repo, root, contentMod
|
|
|
74
76
|
const data = (await res.json().catch(() => null)) as {
|
|
75
77
|
success?: boolean;
|
|
76
78
|
diff?: string;
|
|
79
|
+
oldContent?: string;
|
|
80
|
+
newContent?: string;
|
|
77
81
|
status?: string;
|
|
78
82
|
staged?: boolean;
|
|
79
83
|
error?: string;
|
|
@@ -83,6 +87,11 @@ export function useFileDiff({ filePath, status, isStaged, repo, root, contentMod
|
|
|
83
87
|
if (seq !== fetchSeqRef.current) return;
|
|
84
88
|
if (data?.success) {
|
|
85
89
|
setRawDiff(data.diff || '');
|
|
90
|
+
// Both sides travel on every read: the merge view compares documents,
|
|
91
|
+
// and a whitespace fold has to be applied to the ORIGINAL, not to the
|
|
92
|
+
// patch (see `foldWhitespaceOnly`).
|
|
93
|
+
setOldContent(data.oldContent ?? '');
|
|
94
|
+
setNewContent(data.newContent ?? '');
|
|
86
95
|
if (data.status) {
|
|
87
96
|
statusRef.current = data.status;
|
|
88
97
|
setCurrentStatus(data.status);
|
|
@@ -100,7 +109,7 @@ export function useFileDiff({ filePath, status, isStaged, repo, root, contentMod
|
|
|
100
109
|
} finally {
|
|
101
110
|
if (seq === fetchSeqRef.current) setIsLoading(false);
|
|
102
111
|
}
|
|
103
|
-
}, [filePath, repo, root
|
|
112
|
+
}, [filePath, repo, root]);
|
|
104
113
|
|
|
105
114
|
useEffect(() => {
|
|
106
115
|
setCurrentStatus(status);
|
|
@@ -124,5 +133,15 @@ export function useFileDiff({ filePath, status, isStaged, repo, root, contentMod
|
|
|
124
133
|
}
|
|
125
134
|
}, []);
|
|
126
135
|
|
|
127
|
-
return {
|
|
136
|
+
return {
|
|
137
|
+
rawDiff,
|
|
138
|
+
oldContent,
|
|
139
|
+
newContent,
|
|
140
|
+
currentStatus,
|
|
141
|
+
currentStaged,
|
|
142
|
+
isLoading,
|
|
143
|
+
errorMessage,
|
|
144
|
+
fetchDiff,
|
|
145
|
+
adoptState,
|
|
146
|
+
};
|
|
128
147
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The diff panel's git writes, and the conflict-marker read behind its banner.
|
|
8
|
+
*
|
|
9
|
+
* Split out of the panel because these are the only parts of it that talk to
|
|
10
|
+
* the server: the panel used to fire and forget (`fetcher.submit` + an
|
|
11
|
+
* optimistic flip + a 300ms timer), which made a rejected action
|
|
12
|
+
* indistinguishable from a successful one — the badge flipped and the diff
|
|
13
|
+
* silently stayed put. Every write here AWAITS its answer and reports the
|
|
14
|
+
* server's own reason.
|
|
15
|
+
*
|
|
16
|
+
* `revert` is the server's name for "restore this file from the index", and it
|
|
17
|
+
* is the one that handles the untracked case (there the file is removed).
|
|
18
|
+
* Sending `discard`, which no handler matched, returned `{success: true}` for
|
|
19
|
+
* doing nothing at all.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { useCallback, useEffect, useState } from 'preact/hooks';
|
|
23
|
+
import type { ConflictMarkers } from '@/shared/lib/fs/git-unmerged';
|
|
24
|
+
|
|
25
|
+
export interface DiffGitActions {
|
|
26
|
+
/** A write is in flight; the toolbar locks its actions. */
|
|
27
|
+
isBusy: boolean;
|
|
28
|
+
/** The server's refusal, or a transport failure, as a sentence. */
|
|
29
|
+
error: string | null;
|
|
30
|
+
/** The conflict sides' labels from the file's own markers (`{}` when none). */
|
|
31
|
+
markers: ConflictMarkers;
|
|
32
|
+
dismissError: () => void;
|
|
33
|
+
/** Stage or unstage this path, then re-read the diff at the new side. */
|
|
34
|
+
stageUnstage: () => Promise<void>;
|
|
35
|
+
/** Take one side of a conflict (or only stage a resolved file with `''`). */
|
|
36
|
+
resolve: (side: 'ours' | 'theirs' | '') => Promise<void>;
|
|
37
|
+
/** Restore this file from the index — the untracked case removes it. */
|
|
38
|
+
revert: () => Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface DiffGitActionsArgs {
|
|
42
|
+
filePath: string;
|
|
43
|
+
repo: string;
|
|
44
|
+
root?: string;
|
|
45
|
+
/** The status the panel currently believes (`UU`, `M`, …). */
|
|
46
|
+
status: string;
|
|
47
|
+
staged: boolean;
|
|
48
|
+
/** The panel is showing a conflicted file, so its marker labels are read. */
|
|
49
|
+
conflicted: boolean;
|
|
50
|
+
/** Adopt a state change the server confirmed. */
|
|
51
|
+
adoptState: (patch: { staged: boolean }) => void;
|
|
52
|
+
/** Re-read the diff, optionally at the other side of the index. */
|
|
53
|
+
fetchDiff: (options?: { staged: boolean }) => Promise<void>;
|
|
54
|
+
/** Something on disk changed, so the workspace panels should refresh. */
|
|
55
|
+
onFileSaved?: () => void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function useDiffGitActions(args: DiffGitActionsArgs): DiffGitActions {
|
|
59
|
+
const { filePath, repo, root, staged, conflicted, adoptState, fetchDiff, onFileSaved } = args;
|
|
60
|
+
const [isBusy, setIsBusy] = useState(false);
|
|
61
|
+
const [error, setError] = useState<string | null>(null);
|
|
62
|
+
const [markers, setMarkers] = useState<ConflictMarkers>({});
|
|
63
|
+
|
|
64
|
+
const run = useCallback(
|
|
65
|
+
async (fields: Record<string, string>): Promise<boolean> => {
|
|
66
|
+
const fd = new FormData();
|
|
67
|
+
for (const [key, value] of Object.entries(fields)) fd.set(key, value);
|
|
68
|
+
fd.set('repo', repo || '.');
|
|
69
|
+
if (root) fd.set('root', root);
|
|
70
|
+
try {
|
|
71
|
+
const res = await fetch('/api/fs/git', { method: 'POST', body: fd });
|
|
72
|
+
const data = (await res.json().catch(() => null)) as { success?: boolean; error?: string } | null;
|
|
73
|
+
if (!res.ok || !data?.success) {
|
|
74
|
+
setError(data?.error || `Request failed (HTTP ${res.status})`);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
setError(null);
|
|
78
|
+
return true;
|
|
79
|
+
} catch (err: unknown) {
|
|
80
|
+
setError(err instanceof Error ? err.message : 'Request failed');
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
[repo, root],
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
/** One write, its busy flag, and the refresh every action ends with. */
|
|
88
|
+
const write = useCallback(
|
|
89
|
+
async (fields: Record<string, string>, after: () => Promise<void>): Promise<void> => {
|
|
90
|
+
setIsBusy(true);
|
|
91
|
+
try {
|
|
92
|
+
if (!(await run(fields))) return;
|
|
93
|
+
await after();
|
|
94
|
+
} finally {
|
|
95
|
+
setIsBusy(false);
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
[run],
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// The marker labels are fetched when the file turns out to be conflicted, and
|
|
102
|
+
// re-fetched when the path changes. Keyed on the path and the status so a
|
|
103
|
+
// resolve (which changes the status away from `UU`) clears the banner.
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (!conflicted) {
|
|
106
|
+
setMarkers({});
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
let cancelled = false;
|
|
110
|
+
const params = new URLSearchParams({ conflictMarkers: '1', file: filePath, repo: repo || '.' });
|
|
111
|
+
if (root) params.set('root', root);
|
|
112
|
+
void fetch(`/api/fs/git?${params.toString()}`)
|
|
113
|
+
.then((res) => res.json() as Promise<{ markers?: ConflictMarkers }>)
|
|
114
|
+
.then((body) => {
|
|
115
|
+
if (!cancelled) setMarkers(body.markers ?? {});
|
|
116
|
+
})
|
|
117
|
+
.catch(() => {
|
|
118
|
+
if (!cancelled) setMarkers({});
|
|
119
|
+
});
|
|
120
|
+
return () => {
|
|
121
|
+
cancelled = true;
|
|
122
|
+
};
|
|
123
|
+
}, [conflicted, filePath, repo, root]);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
isBusy,
|
|
127
|
+
error,
|
|
128
|
+
markers,
|
|
129
|
+
dismissError: () => setError(null),
|
|
130
|
+
stageUnstage: () =>
|
|
131
|
+
write({ actionType: staged ? 'unstage' : 'stage', file: filePath }, async () => {
|
|
132
|
+
adoptState({ staged: !staged });
|
|
133
|
+
onFileSaved?.();
|
|
134
|
+
await fetchDiff({ staged: !staged });
|
|
135
|
+
}),
|
|
136
|
+
resolve: (side) =>
|
|
137
|
+
write({ actionType: 'resolve_conflict', file: filePath, ...(side ? { side } : {}) }, async () => {
|
|
138
|
+
onFileSaved?.();
|
|
139
|
+
await fetchDiff();
|
|
140
|
+
}),
|
|
141
|
+
revert: () =>
|
|
142
|
+
write({ actionType: 'revert', file: filePath }, async () => {
|
|
143
|
+
onFileSaved?.();
|
|
144
|
+
await fetchDiff();
|
|
145
|
+
}),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The desktop editor column's WIDTH mode, read and written through the session
|
|
8
|
+
* slot every surface follows.
|
|
9
|
+
*
|
|
10
|
+
* Two surfaces read it — the layout (which collapses the columns) and both
|
|
11
|
+
* toolbars (which draw the mode buttons) — so it goes through
|
|
12
|
+
* `useSharedSessionState`: `useSessionState` hands each caller a private copy,
|
|
13
|
+
* and a copy would leave the diff toolbar showing `normal` after the editor
|
|
14
|
+
* toolbar entered compact view. A private copy is right for state one component
|
|
15
|
+
* owns; this one is a decision two toolbars and the layout share.
|
|
16
|
+
*
|
|
17
|
+
* The stored mode only means anything while the editor column is OPEN. With it
|
|
18
|
+
* closed the chat is the only thing on screen, and honouring the mode would
|
|
19
|
+
* leave a blank workspace; the effective mode is therefore DERIVED rather than
|
|
20
|
+
* written back, so closing and reopening the column returns the user to the
|
|
21
|
+
* width they chose.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { useSharedSessionState } from '@/client/hooks/workspace/session-state';
|
|
25
|
+
import {
|
|
26
|
+
DEFAULT_EDITOR_VIEW_MODE,
|
|
27
|
+
EDITOR_VIEW_MODE_KEY,
|
|
28
|
+
editorViewHides,
|
|
29
|
+
isEditorViewMode,
|
|
30
|
+
type EditorViewMode,
|
|
31
|
+
} from '@/shared/lib/workspace/editor-view-mode';
|
|
32
|
+
|
|
33
|
+
export interface EditorViewLayout {
|
|
34
|
+
/** The mode in force, after the editor-open guard. */
|
|
35
|
+
mode: EditorViewMode;
|
|
36
|
+
/** The chat column is collapsed and the editor takes its width. */
|
|
37
|
+
chatHidden: boolean;
|
|
38
|
+
/** The session sidebar is collapsed as well (full view only). */
|
|
39
|
+
sidebarHidden: boolean;
|
|
40
|
+
/** Enter a mode, or return to `normal` — see `toggledEditorViewMode`. */
|
|
41
|
+
setMode: (next: EditorViewMode) => void;
|
|
42
|
+
/** Leave `full` for `compact`: the chat stays hidden, the sidebar returns. */
|
|
43
|
+
revealSidebar: () => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function useEditorViewLayout(editorOpen: boolean): EditorViewLayout {
|
|
47
|
+
const [stored, setMode] = useSharedSessionState<EditorViewMode>(EDITOR_VIEW_MODE_KEY, DEFAULT_EDITOR_VIEW_MODE);
|
|
48
|
+
const mode: EditorViewMode = editorOpen && isEditorViewMode(stored) ? stored : DEFAULT_EDITOR_VIEW_MODE;
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
mode,
|
|
52
|
+
chatHidden: editorViewHides(mode, 'chat'),
|
|
53
|
+
sidebarHidden: editorViewHides(mode, 'sidebar'),
|
|
54
|
+
setMode,
|
|
55
|
+
// Only `full` hides the sidebar, so only `full` has anything to step back
|
|
56
|
+
// from. A caller that asks for the sidebar (the navbar's own toggle) must be
|
|
57
|
+
// able to call this unconditionally: turning `normal` into `compact` would
|
|
58
|
+
// hide the chat for a user who only wanted their sidebar back.
|
|
59
|
+
revealSidebar: () => setMode((previous) => (previous === 'full' ? 'compact' : previous)),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Creating one
|
|
7
|
+
* Creating one entry in a named folder of the Files panel, and opening it when
|
|
8
|
+
* it is a file.
|
|
8
9
|
*
|
|
9
10
|
* Extracted from `useFileActions` because the Files panel needs the SAME flow
|
|
10
11
|
* from two places that share no target: a row's context menu (create beside the
|
|
@@ -12,6 +13,12 @@
|
|
|
12
13
|
* root). A second copy would drift on the two things that matter — a refusal
|
|
13
14
|
* keeps the dialog open with the server's reason beside the field, and a success
|
|
14
15
|
* OPENS the file, because the user asked for something to work on.
|
|
16
|
+
*
|
|
17
|
+
* The name may carry SEGMENTS (`lib/parse.ts`), so one dialog creates a nested
|
|
18
|
+
* path and every directory that leads to it. The grammar that keeps the result
|
|
19
|
+
* inside the named folder is the server's (`normalizeRelativePath`); this side
|
|
20
|
+
* only reports its refusal, because a second copy of that rule is how the field
|
|
21
|
+
* accepts something the route then rejects.
|
|
15
22
|
*/
|
|
16
23
|
|
|
17
24
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
@@ -21,30 +28,46 @@ import { useFetcher } from '@/client/lib/router/fetcher';
|
|
|
21
28
|
/** The shape `/api/fs/action` answers a create with. */
|
|
22
29
|
interface CreateResponse {
|
|
23
30
|
success?: boolean;
|
|
24
|
-
/**
|
|
31
|
+
/** `file` or `folder` — what the request asked for. */
|
|
32
|
+
kind?: 'file' | 'folder';
|
|
33
|
+
/** Relative path of the entry that was created. */
|
|
25
34
|
path?: string;
|
|
26
35
|
error?: string;
|
|
27
36
|
}
|
|
28
37
|
|
|
38
|
+
/** What a create produced, for the caller to place in its tree. */
|
|
39
|
+
export interface CreatedEntry {
|
|
40
|
+
path: string;
|
|
41
|
+
name: string;
|
|
42
|
+
kind: 'file' | 'folder';
|
|
43
|
+
}
|
|
44
|
+
|
|
29
45
|
export interface FileCreateDeps {
|
|
30
|
-
/** Folder the
|
|
46
|
+
/** Folder the entry goes in, relative to the listed root (`.` for the root). */
|
|
31
47
|
folderPath: string;
|
|
32
48
|
rootPath?: string;
|
|
33
49
|
repo?: string;
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Fired after the entry exists. The tree inserts it directly rather than
|
|
52
|
+
* re-reading: a create answers with the whole result, and a reload would cost
|
|
53
|
+
* one request per open folder to learn what this call already knows.
|
|
54
|
+
*/
|
|
55
|
+
onCreated: (entry: CreatedEntry) => void;
|
|
56
|
+
/** Fired with a created FILE's relative path, so the panel can open its tab. */
|
|
37
57
|
onOpenCreated?: (file: { path: string; name: string }) => void;
|
|
38
58
|
}
|
|
39
59
|
|
|
40
60
|
export interface FileCreate {
|
|
41
61
|
open: boolean;
|
|
62
|
+
/** Whether the open dialog makes a folder; a row's menu offers both. */
|
|
63
|
+
kind: 'file' | 'folder';
|
|
42
64
|
name: string;
|
|
43
65
|
setName: (value: string) => void;
|
|
44
66
|
/** The server's refusal, rendered inside the dialog. */
|
|
45
67
|
error: string | null;
|
|
46
68
|
busy: boolean;
|
|
47
|
-
|
|
69
|
+
/** Open the dialog; `kind` defaults to a file. */
|
|
70
|
+
openDialog: (kind?: 'file' | 'folder') => void;
|
|
48
71
|
closeDialog: () => void;
|
|
49
72
|
submit: (event: FormEvent) => void;
|
|
50
73
|
}
|
|
@@ -52,6 +75,7 @@ export interface FileCreate {
|
|
|
52
75
|
export function useFileCreate({ folderPath, rootPath, repo, onCreated, onOpenCreated }: FileCreateDeps): FileCreate {
|
|
53
76
|
const fetcher = useFetcher<CreateResponse>();
|
|
54
77
|
const [open, setOpen] = useState(false);
|
|
78
|
+
const [kind, setKind] = useState<'file' | 'folder'>('file');
|
|
55
79
|
const [name, setName] = useState('');
|
|
56
80
|
const [error, setError] = useState<string | null>(null);
|
|
57
81
|
|
|
@@ -71,24 +95,29 @@ export function useFileCreate({ folderPath, rootPath, repo, onCreated, onOpenCre
|
|
|
71
95
|
// The fetcher parses the body whatever the status, so a refusal arrives as
|
|
72
96
|
// `data.error` rather than as a throw — which is what keeps the dialog open.
|
|
73
97
|
if (data.success && data.path) {
|
|
98
|
+
const createdKind = data.kind === 'folder' ? 'folder' : 'file';
|
|
74
99
|
setOpen(false);
|
|
75
100
|
setName('');
|
|
76
101
|
setError(null);
|
|
77
|
-
|
|
78
|
-
|
|
102
|
+
const name = data.path.split('/').pop() || data.path;
|
|
103
|
+
onCreated({ path: data.path, name, kind: createdKind });
|
|
104
|
+
// A folder has nothing to open: the user is about to put something in it.
|
|
105
|
+
if (createdKind === 'file') onOpenCreated?.({ path: data.path, name });
|
|
79
106
|
} else {
|
|
80
|
-
setError(data.error ||
|
|
107
|
+
setError(data.error || `Could not create the ${kind}`);
|
|
81
108
|
}
|
|
82
109
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
83
110
|
}, [fetcher.state, fetcher.data]);
|
|
84
111
|
|
|
85
112
|
return {
|
|
86
113
|
open,
|
|
114
|
+
kind,
|
|
87
115
|
name,
|
|
88
116
|
setName,
|
|
89
117
|
error,
|
|
90
118
|
busy: fetcher.state !== 'idle',
|
|
91
|
-
openDialog: () => {
|
|
119
|
+
openDialog: (nextKind: 'file' | 'folder' = 'file') => {
|
|
120
|
+
setKind(nextKind);
|
|
92
121
|
setName('');
|
|
93
122
|
setError(null);
|
|
94
123
|
setOpen(true);
|
|
@@ -103,6 +132,7 @@ export function useFileCreate({ folderPath, rootPath, repo, onCreated, onOpenCre
|
|
|
103
132
|
formData.append('actionType', 'create');
|
|
104
133
|
formData.append('path', folderPath);
|
|
105
134
|
formData.append('name', trimmed);
|
|
135
|
+
if (kind === 'folder') formData.append('kind', 'folder');
|
|
106
136
|
if (repo && repo !== '.') formData.append('repo', repo);
|
|
107
137
|
if (rootPath) formData.append('root', rootPath);
|
|
108
138
|
void fetcher.submit(formData, { method: 'post', action: '/api/fs/action' });
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
import { useRef, useState } from 'preact/hooks';
|
|
27
27
|
import type { FsNode } from '@/shared/types';
|
|
28
|
-
import { rehydrateTree, setChildrenAt } from '@/shared/lib/fs/file-tree';
|
|
28
|
+
import { placeEntry, rehydrateTree, setChildrenAt, takeEntry, rekeyCache } from '@/shared/lib/fs/file-tree';
|
|
29
29
|
import { isRecord } from '@/shared/lib/util/guards';
|
|
30
30
|
|
|
31
31
|
/**
|
|
@@ -86,6 +86,20 @@ export interface FileListing {
|
|
|
86
86
|
loadChildren: (path: string) => Promise<void>;
|
|
87
87
|
/** Adopt a realtime root snapshot, then re-read the open folders. */
|
|
88
88
|
adoptRoot: (payload: unknown) => void;
|
|
89
|
+
/**
|
|
90
|
+
* Insert a row the panel just created, without a reload.
|
|
91
|
+
*
|
|
92
|
+
* A create answers with the entry's path, so the tree already knows the whole
|
|
93
|
+
* result; re-reading the root plus every open folder to learn it back would
|
|
94
|
+
* be one request per open folder for an answer the caller holds.
|
|
95
|
+
*/
|
|
96
|
+
insertEntry: (entry: { path: string; kind: 'file' | 'folder' }) => void;
|
|
97
|
+
/**
|
|
98
|
+
* Move a row the panel just renamed, without a reload. Returns the folders
|
|
99
|
+
* whose path moved, so the caller can rekey its expansion set — a renamed
|
|
100
|
+
* folder's open branches are still open, under their new names.
|
|
101
|
+
*/
|
|
102
|
+
moveEntry: (fromPath: string, toPath: string) => void;
|
|
89
103
|
/** Forget the root listing and every cached child (a repo switch). */
|
|
90
104
|
reset: () => void;
|
|
91
105
|
}
|
|
@@ -117,6 +131,8 @@ export function useFileListing({
|
|
|
117
131
|
const scopeRef = useRef(scope);
|
|
118
132
|
scopeRef.current = scope;
|
|
119
133
|
const listedScopeRef = useRef('');
|
|
134
|
+
/** The absolute base dir the current listing was read for — what `commit` stamps. */
|
|
135
|
+
const listedRootRef = useRef('');
|
|
120
136
|
const expandedPathsRef = useRef(expandedPaths);
|
|
121
137
|
expandedPathsRef.current = expandedPaths;
|
|
122
138
|
|
|
@@ -132,6 +148,7 @@ export function useFileListing({
|
|
|
132
148
|
/** Render the tree from the current root listing plus the children cache. */
|
|
133
149
|
const commit = (requested: string, root: string) => {
|
|
134
150
|
listedScopeRef.current = requested;
|
|
151
|
+
listedRootRef.current = root;
|
|
135
152
|
setState({
|
|
136
153
|
scope: requested,
|
|
137
154
|
files: rehydrateTree(rootFilesRef.current, childrenCacheRef.current),
|
|
@@ -223,9 +240,45 @@ export function useFileListing({
|
|
|
223
240
|
childrenCacheRef.current = {};
|
|
224
241
|
rootFilesRef.current = [];
|
|
225
242
|
listedScopeRef.current = '';
|
|
243
|
+
listedRootRef.current = '';
|
|
226
244
|
setState({ scope: '', files: [], root: '' });
|
|
227
245
|
};
|
|
228
246
|
|
|
247
|
+
/**
|
|
248
|
+
* The tree edits below patch the same two sources a read fills — the root
|
|
249
|
+
* list and the children cache — and then commit, so `rehydrateTree` remains
|
|
250
|
+
* the only thing that builds what is rendered. Editing the rendered tree
|
|
251
|
+
* directly would be undone by the next commit, which rebuilds from these.
|
|
252
|
+
*/
|
|
253
|
+
const insertEntry = (entry: { path: string; kind: 'file' | 'folder' }): void => {
|
|
254
|
+
const requested = scopeRef.current;
|
|
255
|
+
if (listedScopeRef.current !== requested) return;
|
|
256
|
+
const node: FsNode = {
|
|
257
|
+
name: entry.path.slice(entry.path.lastIndexOf('/') + 1),
|
|
258
|
+
path: entry.path,
|
|
259
|
+
type: entry.kind,
|
|
260
|
+
ignored: false,
|
|
261
|
+
};
|
|
262
|
+
const placed = placeEntry(rootFilesRef.current, childrenCacheRef.current, node);
|
|
263
|
+
if (placed.nodes === rootFilesRef.current) return;
|
|
264
|
+
rootFilesRef.current = placed.nodes;
|
|
265
|
+
childrenCacheRef.current = placed.cache;
|
|
266
|
+
commit(requested, listedRootRef.current);
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const moveEntry = (fromPath: string, toPath: string): void => {
|
|
270
|
+
const requested = scopeRef.current;
|
|
271
|
+
if (listedScopeRef.current !== requested) return;
|
|
272
|
+
const taken = takeEntry(rootFilesRef.current, childrenCacheRef.current, fromPath);
|
|
273
|
+
if (!taken.entry) return;
|
|
274
|
+
childrenCacheRef.current = rekeyCache(taken.cache, fromPath, toPath);
|
|
275
|
+
const moved: FsNode = { ...taken.entry, path: toPath, name: toPath.slice(toPath.lastIndexOf('/') + 1) };
|
|
276
|
+
const placed = placeEntry(taken.nodes, childrenCacheRef.current, moved);
|
|
277
|
+
rootFilesRef.current = placed.nodes;
|
|
278
|
+
childrenCacheRef.current = placed.cache;
|
|
279
|
+
commit(requested, listedRootRef.current);
|
|
280
|
+
};
|
|
281
|
+
|
|
229
282
|
// A listing read for another scope is not this one's to render: the panel
|
|
230
283
|
// shows an empty tree until the current scope's read lands.
|
|
231
284
|
const current = state.scope === scope;
|
|
@@ -237,6 +290,8 @@ export function useFileListing({
|
|
|
237
290
|
reload,
|
|
238
291
|
loadChildren,
|
|
239
292
|
adoptRoot,
|
|
293
|
+
insertEntry,
|
|
294
|
+
moveEntry,
|
|
240
295
|
reset,
|
|
241
296
|
};
|
|
242
297
|
}
|