@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
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
*
|
|
10
10
|
* A row's menu (`FileContextMenu`) offers View, Diff, Rename, Delete — all
|
|
11
11
|
* things a FILE has. The root has none of them: it is the listing itself, and
|
|
12
|
-
* the only things you can ask of it are a new file at the top of the
|
|
13
|
-
* paste into it. Reusing the row menu here would have offered six
|
|
14
|
-
* name the wrong target.
|
|
12
|
+
* the only things you can ask of it are a new file or folder at the top of the
|
|
13
|
+
* tree and a paste into it. Reusing the row menu here would have offered six
|
|
14
|
+
* actions that name the wrong target.
|
|
15
15
|
*
|
|
16
16
|
* Paste is the one row-menu action the root DOES own: pasting into the
|
|
17
17
|
* background means pasting into the root folder, which is a real destination —
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import { createPortal } from 'preact/compat';
|
|
23
23
|
import type { RefObject } from 'preact/compat';
|
|
24
|
-
import { ClipboardPaste, FilePlus } from 'lucide-preact';
|
|
24
|
+
import { ClipboardPaste, FilePlus, FolderPlus } from 'lucide-preact';
|
|
25
25
|
|
|
26
26
|
interface RootContextMenuProps {
|
|
27
27
|
x: number;
|
|
@@ -36,7 +36,8 @@ interface RootContextMenuProps {
|
|
|
36
36
|
|
|
37
37
|
export function RootContextMenu({ x, y, pasteLabel, busy, onAction, menuRef }: RootContextMenuProps) {
|
|
38
38
|
const menuWidth = 190;
|
|
39
|
-
|
|
39
|
+
// Two create rows, plus paste when the clipboard can land here.
|
|
40
|
+
const menuHeight = pasteLabel ? 104 : 76;
|
|
40
41
|
|
|
41
42
|
const posX = typeof window !== 'undefined' ? Math.max(10, Math.min(x, window.innerWidth - menuWidth - 12)) : x;
|
|
42
43
|
const posY = typeof window !== 'undefined' ? Math.max(10, Math.min(y, window.innerHeight - menuHeight - 12)) : y;
|
|
@@ -61,6 +62,14 @@ export function RootContextMenu({ x, y, pasteLabel, busy, onAction, menuRef }: R
|
|
|
61
62
|
<FilePlus size={13} className="text-ink/60" />
|
|
62
63
|
<span>New File</span>
|
|
63
64
|
</button>
|
|
65
|
+
<button
|
|
66
|
+
type="button"
|
|
67
|
+
onClick={() => onAction('new_folder')}
|
|
68
|
+
className="w-full text-left px-3 py-1.5 hover:bg-ink/5 flex items-center space-x-2 transition-colors"
|
|
69
|
+
>
|
|
70
|
+
<FolderPlus size={13} className="text-ink/60" />
|
|
71
|
+
<span>New Folder</span>
|
|
72
|
+
</button>
|
|
64
73
|
{pasteLabel && (
|
|
65
74
|
<button
|
|
66
75
|
type="button"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The Files panel's header: the repo picker, Refresh and the filter field.
|
|
8
|
+
*
|
|
9
|
+
* Split out of the panel because it is a self-contained strip of markup with no
|
|
10
|
+
* state of its own — the panel's `index.tsx` sits at the repository's line
|
|
11
|
+
* ceiling, and the tree below it is what the file is for.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { RefreshCw, Search } from 'lucide-preact';
|
|
15
|
+
import { GitRepoPicker } from '@/client/components/workspace/repo-picker';
|
|
16
|
+
|
|
17
|
+
export interface FileExplorerToolbarProps {
|
|
18
|
+
rootPath?: string;
|
|
19
|
+
activeRepo: string;
|
|
20
|
+
repos: string[];
|
|
21
|
+
reposScanning: boolean;
|
|
22
|
+
onRefreshRepos: () => void;
|
|
23
|
+
onSelectRepo: (repo: string) => void;
|
|
24
|
+
searchQuery: string;
|
|
25
|
+
onSearchChange: (value: string) => void;
|
|
26
|
+
onRefresh: () => void;
|
|
27
|
+
/** True while a read is in flight or a topic push just landed. */
|
|
28
|
+
refreshing: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function FileExplorerToolbar({
|
|
32
|
+
rootPath,
|
|
33
|
+
activeRepo,
|
|
34
|
+
repos,
|
|
35
|
+
reposScanning,
|
|
36
|
+
onRefreshRepos,
|
|
37
|
+
onSelectRepo,
|
|
38
|
+
searchQuery,
|
|
39
|
+
onSearchChange,
|
|
40
|
+
onRefresh,
|
|
41
|
+
refreshing,
|
|
42
|
+
}: FileExplorerToolbarProps) {
|
|
43
|
+
return (
|
|
44
|
+
<div className="p-3 border-b border-ink/10 flex flex-col space-y-2">
|
|
45
|
+
<div className="flex items-center justify-between">
|
|
46
|
+
<GitRepoPicker
|
|
47
|
+
rootPath={rootPath}
|
|
48
|
+
activeRepo={activeRepo}
|
|
49
|
+
onSelectRepo={onSelectRepo}
|
|
50
|
+
repos={repos}
|
|
51
|
+
scanning={reposScanning}
|
|
52
|
+
onRefreshRepos={onRefreshRepos}
|
|
53
|
+
/>
|
|
54
|
+
<button
|
|
55
|
+
onClick={onRefresh}
|
|
56
|
+
className="p-1.5 text-ink/40 hover:text-ink hover:bg-ink/5 rounded transition-colors"
|
|
57
|
+
title="Refresh"
|
|
58
|
+
>
|
|
59
|
+
<RefreshCw size={14} className={refreshing ? 'animate-spin' : ''} />
|
|
60
|
+
</button>
|
|
61
|
+
</div>
|
|
62
|
+
<div className="relative">
|
|
63
|
+
<Search size={14} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-ink/40" />
|
|
64
|
+
<input
|
|
65
|
+
type="text"
|
|
66
|
+
placeholder="Search files..."
|
|
67
|
+
value={searchQuery}
|
|
68
|
+
onChange={(e) => onSearchChange(e.currentTarget.value)}
|
|
69
|
+
className="w-full bg-canvas border border-ink/20 rounded pl-8 pr-3 py-1.5 text-xs focus:outline-none focus:border-ink transition-colors text-ink placeholder-ink/40"
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -7,6 +7,7 @@ import { getGitStatusInfo, type FolderGitStatusInfo } from '@/shared/lib/fs/git-
|
|
|
7
7
|
import { useFileActions } from '@/client/hooks/workspace/file-tree-actions';
|
|
8
8
|
import { useFileRowDrag } from '@/client/hooks/workspace/file-row-drag';
|
|
9
9
|
import { useOnClickOutside } from '@/client/hooks/ui/on-click-outside';
|
|
10
|
+
import { useLongPress } from '@/client/hooks/ui/long-press';
|
|
10
11
|
import type { FileClipboard, ClipboardTarget } from '@/client/hooks/workspace/file-clipboard';
|
|
11
12
|
import type { GitChange } from '@/shared/types/git';
|
|
12
13
|
|
|
@@ -18,7 +19,12 @@ interface FileTreeItemProps {
|
|
|
18
19
|
repo?: string;
|
|
19
20
|
onLoadChildren?: (path: string) => Promise<void>;
|
|
20
21
|
onOpenFile?: (file: any) => void;
|
|
22
|
+
/** Re-read the tree — a DELETE has no local answer. */
|
|
21
23
|
onActionComplete: () => void;
|
|
24
|
+
/** Place a row this row's menu just created, without a reload. */
|
|
25
|
+
onCreated?: (entry: { path: string; kind: 'file' | 'folder' }) => void;
|
|
26
|
+
/** Move a row this row's menu just renamed, without a reload. */
|
|
27
|
+
onRenamed?: (fromPath: string, toPath: string) => void;
|
|
22
28
|
expandedPaths?: Set<string>;
|
|
23
29
|
onToggleFolder?: (path: string, open: boolean) => void;
|
|
24
30
|
gitFileMap?: Map<string, GitChange>;
|
|
@@ -31,6 +37,13 @@ interface FileTreeItemProps {
|
|
|
31
37
|
onDragStart: (target: ClipboardTarget) => void;
|
|
32
38
|
onDragEnd: () => void;
|
|
33
39
|
onDropOn: (target: ClipboardTarget) => void;
|
|
40
|
+
/**
|
|
41
|
+
* True on a device typed into with a finger. A row is a drag SOURCE, and on
|
|
42
|
+
* touch a long press starts a native drag of the whole panel — the reported
|
|
43
|
+
* "it selects all of the UI" — while no `contextmenu` reaches this row. The
|
|
44
|
+
* native drag is dropped there and the press opens the menu instead.
|
|
45
|
+
*/
|
|
46
|
+
touch?: boolean;
|
|
34
47
|
}
|
|
35
48
|
|
|
36
49
|
/**
|
|
@@ -63,6 +76,8 @@ export function FileTreeItem({
|
|
|
63
76
|
onLoadChildren,
|
|
64
77
|
onOpenFile,
|
|
65
78
|
onActionComplete,
|
|
79
|
+
onCreated,
|
|
80
|
+
onRenamed,
|
|
66
81
|
expandedPaths,
|
|
67
82
|
onToggleFolder,
|
|
68
83
|
gitFileMap,
|
|
@@ -72,6 +87,7 @@ export function FileTreeItem({
|
|
|
72
87
|
onDragStart,
|
|
73
88
|
onDragEnd,
|
|
74
89
|
onDropOn,
|
|
90
|
+
touch = false,
|
|
75
91
|
}: FileTreeItemProps) {
|
|
76
92
|
const isFolder = file.type === 'folder';
|
|
77
93
|
const [isOpen, setIsOpen] = useState(false);
|
|
@@ -115,13 +131,23 @@ export function FileTreeItem({
|
|
|
115
131
|
diff: { staged: gitChange?.staged || (gitChange?.status ? gitChange.status[0] !== ' ' && gitChange.status[0] !== '?' : false), status: gitStatusInfo?.charStatus },
|
|
116
132
|
onOpenFile,
|
|
117
133
|
onActionComplete,
|
|
134
|
+
onCreated,
|
|
135
|
+
onRenamed,
|
|
118
136
|
clipboard,
|
|
119
137
|
isFolder,
|
|
120
138
|
});
|
|
121
139
|
useOnClickOutside(actions.contextMenuRef, actions.closeContextMenu);
|
|
122
140
|
|
|
141
|
+
// A press-and-hold opens the same menu a right-click does; the click the same
|
|
142
|
+
// gesture produces is swallowed, or lifting the finger would toggle the row.
|
|
143
|
+
const longPress = useLongPress({
|
|
144
|
+
enabled: touch,
|
|
145
|
+
onLongPress: (x, y) => actions.openContextMenu(x, y),
|
|
146
|
+
});
|
|
147
|
+
|
|
123
148
|
const handleToggle = (e: globalThis.MouseEvent) => {
|
|
124
149
|
e.stopPropagation();
|
|
150
|
+
if (longPress.consume()) return;
|
|
125
151
|
if (isFolder) {
|
|
126
152
|
const next = !actualIsOpen;
|
|
127
153
|
setIsOpen(next);
|
|
@@ -175,16 +201,20 @@ export function FileTreeItem({
|
|
|
175
201
|
return (
|
|
176
202
|
<div>
|
|
177
203
|
<div
|
|
178
|
-
className={`flex items-center space-x-1.5 py-1 px-2 hover:bg-ink/5 cursor-pointer rounded group ${
|
|
204
|
+
className={`flex items-center space-x-1.5 py-1 px-2 hover:bg-ink/5 cursor-pointer rounded group select-none ${
|
|
179
205
|
drag.isDragging ? 'opacity-40' : ''
|
|
180
206
|
} ${drag.acceptsDrop ? 'bg-info/10 ring-1 ring-info/40 ring-inset' : ''}`}
|
|
181
|
-
draggable
|
|
207
|
+
draggable={!touch}
|
|
182
208
|
onClick={handleToggle}
|
|
183
209
|
onContextMenu={handleContextMenu}
|
|
184
210
|
onDragStart={drag.onDragStart}
|
|
185
211
|
onDragEnd={drag.onDragEnd}
|
|
186
212
|
onDragOver={drag.onDragOver}
|
|
187
213
|
onDrop={drag.onDrop}
|
|
214
|
+
onTouchStart={longPress.onTouchStart}
|
|
215
|
+
onTouchMove={longPress.onTouchMove}
|
|
216
|
+
onTouchEnd={longPress.onTouchEnd}
|
|
217
|
+
onTouchCancel={longPress.onTouchCancel}
|
|
188
218
|
>
|
|
189
219
|
{isFolder ? (
|
|
190
220
|
actualIsOpen ? <ChevronDown size={12} className="flex-shrink-0 text-ink/40" /> : <ChevronRight size={12} className="flex-shrink-0 text-ink/40" />
|
|
@@ -228,7 +258,7 @@ export function FileTreeItem({
|
|
|
228
258
|
<div className="ml-3 border-l border-ink/10 pl-1">
|
|
229
259
|
{children.map((child: any) => (
|
|
230
260
|
<FileTreeItem
|
|
231
|
-
key={child.
|
|
261
|
+
key={child.path}
|
|
232
262
|
file={child}
|
|
233
263
|
rootPath={rootPath}
|
|
234
264
|
basePath={basePath}
|
|
@@ -236,6 +266,8 @@ export function FileTreeItem({
|
|
|
236
266
|
onLoadChildren={onLoadChildren}
|
|
237
267
|
onOpenFile={onOpenFile}
|
|
238
268
|
onActionComplete={onActionComplete}
|
|
269
|
+
onCreated={onCreated}
|
|
270
|
+
onRenamed={onRenamed}
|
|
239
271
|
expandedPaths={expandedPaths}
|
|
240
272
|
onToggleFolder={onToggleFolder}
|
|
241
273
|
gitFileMap={gitFileMap}
|
|
@@ -245,6 +277,7 @@ export function FileTreeItem({
|
|
|
245
277
|
onDragStart={onDragStart}
|
|
246
278
|
onDragEnd={onDragEnd}
|
|
247
279
|
onDropOn={onDropOn}
|
|
280
|
+
touch={touch}
|
|
248
281
|
/>
|
|
249
282
|
))}
|
|
250
283
|
</div>
|
|
@@ -265,6 +298,7 @@ export function FileTreeItem({
|
|
|
265
298
|
{mounted && actions.showCreateModal && (
|
|
266
299
|
<FileCreateModal
|
|
267
300
|
folderPath={file.path}
|
|
301
|
+
kind={actions.createKind}
|
|
268
302
|
value={actions.createName}
|
|
269
303
|
error={actions.createError}
|
|
270
304
|
isLoading={actions.busy}
|
|
@@ -289,6 +323,7 @@ export function FileTreeItem({
|
|
|
289
323
|
isFolder={isFolder}
|
|
290
324
|
renameValue={actions.renameValue}
|
|
291
325
|
originalPath={file.path}
|
|
326
|
+
error={actions.renameError}
|
|
292
327
|
isLoading={actions.busy}
|
|
293
328
|
onChange={actions.setRenameValue}
|
|
294
329
|
onCancel={actions.closeRename}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The Files panel's filter: keep a node when its own name matches, or when a
|
|
8
|
+
* descendant does.
|
|
9
|
+
*
|
|
10
|
+
* A matching FOLDER keeps its whole subtree (its name is the answer, and hiding
|
|
11
|
+
* its contents would make the match useless), while a folder that only matches
|
|
12
|
+
* through its children keeps just the children that do. Either way the row is
|
|
13
|
+
* forced open, because a match inside a collapsed folder is a match the user
|
|
14
|
+
* cannot see.
|
|
15
|
+
*
|
|
16
|
+
* Extracted from the panel so the render file stays under the line ceiling.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { FsNode } from '@/shared/types';
|
|
20
|
+
|
|
21
|
+
export function filterFileTree(tree: FsNode[], query: string): FsNode[] {
|
|
22
|
+
const lowerQuery = query.trim().toLowerCase();
|
|
23
|
+
if (!lowerQuery) return tree;
|
|
24
|
+
|
|
25
|
+
const filterNode = (node: FsNode): FsNode | null => {
|
|
26
|
+
const isMatch = node.name.toLowerCase().includes(lowerQuery);
|
|
27
|
+
|
|
28
|
+
if (node.type === 'folder') {
|
|
29
|
+
const filteredChildren = (node.children || [])
|
|
30
|
+
.map(filterNode)
|
|
31
|
+
.filter((child): child is FsNode => child !== null);
|
|
32
|
+
|
|
33
|
+
if (isMatch || filteredChildren.length > 0) {
|
|
34
|
+
return {
|
|
35
|
+
...node,
|
|
36
|
+
children: isMatch && filteredChildren.length === 0 ? node.children : filteredChildren,
|
|
37
|
+
forceExpanded: true,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return isMatch ? node : null;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return tree.map(filterNode).filter((node): node is FsNode => node !== null);
|
|
47
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
2
|
-
import { RefreshCw, Search } from 'lucide-preact';
|
|
3
2
|
import type { FsNode } from '@/shared/types';
|
|
4
|
-
import {
|
|
3
|
+
import { FileExplorerToolbar } from '@/client/components/workspace/file-explorer/Toolbar';
|
|
5
4
|
import { FileTreeItem } from '@/client/components/workspace/file-explorer/TreeItem';
|
|
6
5
|
import { RootContextMenu } from '@/client/components/workspace/file-explorer/RootMenu';
|
|
7
6
|
import { FileCreateModal } from '@/client/components/workspace/file-explorer/Modals';
|
|
@@ -11,9 +10,12 @@ import { useGitStatus } from '@/client/hooks/workspace/git-status';
|
|
|
11
10
|
import { useFileListing } from '@/client/hooks/workspace/file-listing';
|
|
12
11
|
import { useFileCreate } from '@/client/hooks/workspace/file-create';
|
|
13
12
|
import { useFileClipboard, type ClipboardTarget } from '@/client/hooks/workspace/file-clipboard';
|
|
13
|
+
import { rekeyExpansion } from '@/shared/lib/fs/file-tree';
|
|
14
|
+
import { filterFileTree } from '@/client/components/workspace/file-explorer/filter';
|
|
14
15
|
import { useRepoList, useRepoScope } from '@/client/hooks/workspace/repo-scope';
|
|
15
16
|
import { useRealtimeTopic } from '@/client/hooks/ui/realtime';
|
|
16
17
|
import { useToasts } from '@/client/hooks/ui/toasts';
|
|
18
|
+
import { useTouchDevice } from '@/client/hooks/ui/touch-device';
|
|
17
19
|
import { ToastStack } from '@/client/components/common/ToastStack';
|
|
18
20
|
import { fsTopic } from '@/shared/lib/realtime/protocol';
|
|
19
21
|
|
|
@@ -98,6 +100,53 @@ export function FileExplorer({ className = '', enabled = true, rootPath, onOpenF
|
|
|
98
100
|
setActiveRepo(repo);
|
|
99
101
|
};
|
|
100
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Open every folder that leads to `path`.
|
|
105
|
+
*
|
|
106
|
+
* A nested create (`lib/parse.ts`) and a rename into a new folder both make
|
|
107
|
+
* directories the user has never opened, and a row placed inside a collapsed
|
|
108
|
+
* folder is a row nobody can see — the write would look like it failed. EVERY
|
|
109
|
+
* ancestor is opened, not only the direct parent: expanding just the last one
|
|
110
|
+
* still hides the entry behind a collapsed `lib`.
|
|
111
|
+
*/
|
|
112
|
+
const revealPath = (path: string) => {
|
|
113
|
+
const segments = path.split('/').slice(0, -1);
|
|
114
|
+
if (segments.length === 0) return;
|
|
115
|
+
const chain = segments.map((_, index) => segments.slice(0, index + 1).join('/'));
|
|
116
|
+
setExpandedPaths(prev => {
|
|
117
|
+
if (chain.every((folder) => prev.has(folder))) return prev;
|
|
118
|
+
const next = new Set(prev);
|
|
119
|
+
for (const folder of chain) next.add(folder);
|
|
120
|
+
setStoredExpandedPaths(Array.from(next));
|
|
121
|
+
return next;
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/** Place a row a create just produced, without re-reading the tree. */
|
|
126
|
+
const insertCreated = (entry: { path: string; kind: 'file' | 'folder' }) => {
|
|
127
|
+
listing.insertEntry(entry);
|
|
128
|
+
revealPath(entry.path);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Move a row a rename just relocated, without re-reading.
|
|
133
|
+
*
|
|
134
|
+
* A folder's descendants and every open branch beneath it carry its path as a
|
|
135
|
+
* prefix, so the expansion set is rekeyed in the same step — an open `src`
|
|
136
|
+
* renamed to `lib` must stay open as `lib`, not collapse.
|
|
137
|
+
*/
|
|
138
|
+
const moveRenamed = (fromPath: string, toPath: string) => {
|
|
139
|
+
listing.moveEntry(fromPath, toPath);
|
|
140
|
+
setExpandedPaths(prev => {
|
|
141
|
+
const next = rekeyExpansion(prev, fromPath, toPath);
|
|
142
|
+
setStoredExpandedPaths(Array.from(next));
|
|
143
|
+
return next;
|
|
144
|
+
});
|
|
145
|
+
// A rename INTO a new folder (`a.ts` → `nested/a.ts`) has the same problem a
|
|
146
|
+
// nested create does: the destination folder is one nobody has opened.
|
|
147
|
+
revealPath(toPath);
|
|
148
|
+
};
|
|
149
|
+
|
|
101
150
|
/**
|
|
102
151
|
* Re-read everything this panel shows. Defined ABOVE the `!enabled` return so
|
|
103
152
|
* the create flow below can use it: a hook cannot be called after a branch.
|
|
@@ -111,6 +160,9 @@ export function FileExplorer({ className = '', enabled = true, rootPath, onOpenF
|
|
|
111
160
|
// One clipboard for the whole tree, owned here and passed down: a
|
|
112
161
|
// subscription per row would be hundreds of listeners for a value that
|
|
113
162
|
// changes on a right-click.
|
|
163
|
+
// A row is a drag source; on a finger the native drag is what turned a long
|
|
164
|
+
// press into a text selection of the whole panel (see `useLongPress`).
|
|
165
|
+
const touch = useTouchDevice();
|
|
114
166
|
const { toasts, pushToast, dismissToast } = useToasts();
|
|
115
167
|
const clipboard = useFileClipboard({
|
|
116
168
|
rootPath,
|
|
@@ -155,7 +207,7 @@ export function FileExplorer({ className = '', enabled = true, rootPath, onOpenF
|
|
|
155
207
|
folderPath: '.',
|
|
156
208
|
rootPath,
|
|
157
209
|
repo: activeRepo,
|
|
158
|
-
onCreated:
|
|
210
|
+
onCreated: insertCreated,
|
|
159
211
|
onOpenCreated: (file) => onOpenFile?.({ ...file, root: rootPath, repo: activeRepo }),
|
|
160
212
|
});
|
|
161
213
|
/** Portals need a real document; the first client render is the gate. */
|
|
@@ -172,34 +224,7 @@ export function FileExplorer({ className = '', enabled = true, rootPath, onOpenF
|
|
|
172
224
|
|
|
173
225
|
const tree = listing.files;
|
|
174
226
|
|
|
175
|
-
const
|
|
176
|
-
if (!searchQuery.trim()) return tree;
|
|
177
|
-
|
|
178
|
-
const lowerQuery = searchQuery.toLowerCase();
|
|
179
|
-
|
|
180
|
-
const filterNode = (node: any): any => {
|
|
181
|
-
const isMatch = node.name.toLowerCase().includes(lowerQuery);
|
|
182
|
-
|
|
183
|
-
if (node.type === 'folder') {
|
|
184
|
-
const filteredChildren = (node.children || []).map(filterNode).filter(Boolean);
|
|
185
|
-
|
|
186
|
-
if (isMatch || filteredChildren.length > 0) {
|
|
187
|
-
return {
|
|
188
|
-
...node,
|
|
189
|
-
children: isMatch && filteredChildren.length === 0 ? node.children : filteredChildren,
|
|
190
|
-
forceExpanded: true
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
return null;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return isMatch ? node : null;
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
return tree.map(filterNode).filter(Boolean);
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
const files = getFilteredFiles();
|
|
227
|
+
const files = filterFileTree(tree, searchQuery);
|
|
203
228
|
// The button spins for this panel's own read OR a topic push: the server
|
|
204
229
|
// republishes `fs:` on every tool call, so the tree moved with nothing on
|
|
205
230
|
// screen saying so otherwise.
|
|
@@ -207,40 +232,23 @@ export function FileExplorer({ className = '', enabled = true, rootPath, onOpenF
|
|
|
207
232
|
|
|
208
233
|
return (
|
|
209
234
|
<div className={`flex flex-col h-full bg-paper ${className}`}>
|
|
210
|
-
<
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
className="p-1.5 text-ink/40 hover:text-ink hover:bg-ink/5 rounded transition-colors"
|
|
223
|
-
title="Refresh"
|
|
224
|
-
>
|
|
225
|
-
<RefreshCw size={14} className={isRefreshing ? 'animate-spin' : ''} />
|
|
226
|
-
</button>
|
|
227
|
-
</div>
|
|
228
|
-
<div className="relative">
|
|
229
|
-
<Search size={14} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-ink/40" />
|
|
230
|
-
<input
|
|
231
|
-
type="text"
|
|
232
|
-
placeholder="Search files..."
|
|
233
|
-
value={searchQuery}
|
|
234
|
-
onChange={(e) => setSearchQuery(e.currentTarget.value)}
|
|
235
|
-
className="w-full bg-canvas border border-ink/20 rounded pl-8 pr-3 py-1.5 text-xs focus:outline-none focus:border-ink transition-colors text-ink placeholder-ink/40"
|
|
236
|
-
/>
|
|
237
|
-
</div>
|
|
238
|
-
</div>
|
|
235
|
+
<FileExplorerToolbar
|
|
236
|
+
rootPath={rootPath}
|
|
237
|
+
activeRepo={activeRepo}
|
|
238
|
+
repos={repos}
|
|
239
|
+
reposScanning={reposScanning}
|
|
240
|
+
onRefreshRepos={rescanRepos}
|
|
241
|
+
onSelectRepo={handleSelectRepo}
|
|
242
|
+
searchQuery={searchQuery}
|
|
243
|
+
onSearchChange={setSearchQuery}
|
|
244
|
+
onRefresh={refresh}
|
|
245
|
+
refreshing={isRefreshing}
|
|
246
|
+
/>
|
|
239
247
|
|
|
240
248
|
<div
|
|
241
249
|
className={`flex-1 scrollbar-overlay-container p-2 font-mono text-[11px] text-ink/80 ${
|
|
242
|
-
|
|
243
|
-
}`}
|
|
250
|
+
touch ? 'file-tree-touch' : ''
|
|
251
|
+
} ${rootAcceptsDrop ? 'bg-info/[0.06]' : ''}`}
|
|
244
252
|
onContextMenu={(e) => {
|
|
245
253
|
// The panel's own background. A row stops propagation before this
|
|
246
254
|
// runs, so this handler only ever sees a right-click that named NO
|
|
@@ -273,7 +281,7 @@ export function FileExplorer({ className = '', enabled = true, rootPath, onOpenF
|
|
|
273
281
|
) : (
|
|
274
282
|
files.map(file => (
|
|
275
283
|
<FileTreeItem
|
|
276
|
-
key={file.
|
|
284
|
+
key={file.path}
|
|
277
285
|
file={file}
|
|
278
286
|
rootPath={rootPath}
|
|
279
287
|
basePath={listing.root}
|
|
@@ -281,6 +289,8 @@ export function FileExplorer({ className = '', enabled = true, rootPath, onOpenF
|
|
|
281
289
|
onLoadChildren={loadChildren}
|
|
282
290
|
onOpenFile={onOpenFile}
|
|
283
291
|
onActionComplete={refresh}
|
|
292
|
+
onCreated={insertCreated}
|
|
293
|
+
onRenamed={moveRenamed}
|
|
284
294
|
expandedPaths={expandedPaths}
|
|
285
295
|
onToggleFolder={handleToggleFolder}
|
|
286
296
|
gitFileMap={gitFileMap}
|
|
@@ -293,6 +303,7 @@ export function FileExplorer({ className = '', enabled = true, rootPath, onOpenF
|
|
|
293
303
|
if (!dragging) return;
|
|
294
304
|
clipboard.move(dragging, target);
|
|
295
305
|
}}
|
|
306
|
+
touch={touch}
|
|
296
307
|
/>
|
|
297
308
|
))
|
|
298
309
|
)}
|
|
@@ -312,7 +323,8 @@ export function FileExplorer({ className = '', enabled = true, rootPath, onOpenF
|
|
|
312
323
|
busy={clipboard.busy}
|
|
313
324
|
onAction={(action) => {
|
|
314
325
|
setRootMenu(null);
|
|
315
|
-
if (action === 'new_file') rootCreate.openDialog();
|
|
326
|
+
if (action === 'new_file') rootCreate.openDialog('file');
|
|
327
|
+
if (action === 'new_folder') rootCreate.openDialog('folder');
|
|
316
328
|
if (action === 'paste') clipboard.paste(rootTarget);
|
|
317
329
|
}}
|
|
318
330
|
/>
|
|
@@ -321,6 +333,7 @@ export function FileExplorer({ className = '', enabled = true, rootPath, onOpenF
|
|
|
321
333
|
{mounted && rootCreate.open && (
|
|
322
334
|
<FileCreateModal
|
|
323
335
|
folderPath="."
|
|
336
|
+
kind={rootCreate.kind}
|
|
324
337
|
value={rootCreate.name}
|
|
325
338
|
error={rootCreate.error}
|
|
326
339
|
isLoading={rootCreate.busy}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The Files panel's local tree edits: a create and a rename land WITHOUT a
|
|
8
|
+
* reload.
|
|
9
|
+
*
|
|
10
|
+
* Both used to answer a mutation by re-reading the root plus every open folder,
|
|
11
|
+
* which is one request per open folder to learn something the response already
|
|
12
|
+
* names — and it also collapsed nothing but cost a visible spinner. The two
|
|
13
|
+
* specs here assert the two halves that matter: the row appears (or moves) in
|
|
14
|
+
* the rendered tree, and NO `/api/fs/dir` read is issued for the root to make
|
|
15
|
+
* that happen.
|
|
16
|
+
*
|
|
17
|
+
* A nested create is the third case, because it is the one a naive insert gets
|
|
18
|
+
* wrong: the folders leading to the entry do not exist in the tree yet, and a
|
|
19
|
+
* row placed under a folder nobody has opened is a row the user cannot see —
|
|
20
|
+
* so the create also opens the chain it created.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'bun:test';
|
|
24
|
+
import { createFileExplorerHarness } from '@/test-support/file-explorer.test-util';
|
|
25
|
+
|
|
26
|
+
const panel = createFileExplorerHarness();
|
|
27
|
+
|
|
28
|
+
beforeAll(() => {
|
|
29
|
+
panel.install();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
panel.unmount();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterAll(() => {
|
|
37
|
+
panel.restore();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/** The root's own reads, which a reload would add one of. */
|
|
41
|
+
function rootReads(): number {
|
|
42
|
+
return panel.dirReads.filter((path) => path === '.').length;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe('Files panel local edits', () => {
|
|
46
|
+
test('a created file appears in the tree without re-reading the root', async () => {
|
|
47
|
+
panel.disk = { '.': { 'existing.ts': 'file' } };
|
|
48
|
+
panel.dirReads = [];
|
|
49
|
+
await panel.mount();
|
|
50
|
+
const before = rootReads();
|
|
51
|
+
|
|
52
|
+
await panel.openRootNewFile();
|
|
53
|
+
await panel.submitNewFile('fresh.ts');
|
|
54
|
+
|
|
55
|
+
expect(panel.buttonLabels()).not.toContain('Create');
|
|
56
|
+
expect(panel.visibleNames()).toContain('fresh.ts');
|
|
57
|
+
// The create answered with the path, so the tree placed it: a reload here
|
|
58
|
+
// would be a root read plus one per open folder.
|
|
59
|
+
expect(rootReads()).toBe(before);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('a created FOLDER appears and is not opened in an editor', async () => {
|
|
63
|
+
panel.disk = { '.': {} };
|
|
64
|
+
panel.dirReads = [];
|
|
65
|
+
await panel.mount();
|
|
66
|
+
const before = rootReads();
|
|
67
|
+
|
|
68
|
+
await panel.openRootNewFolder();
|
|
69
|
+
await panel.submitNewFile('packages');
|
|
70
|
+
|
|
71
|
+
expect(panel.createRequests.at(-1)).toEqual({
|
|
72
|
+
actionType: 'create',
|
|
73
|
+
path: '.',
|
|
74
|
+
name: 'packages',
|
|
75
|
+
kind: 'folder',
|
|
76
|
+
root: '/ws',
|
|
77
|
+
});
|
|
78
|
+
expect(panel.visibleNames()).toContain('packages');
|
|
79
|
+
// A folder has nothing to open — opening one would be an empty editor tab.
|
|
80
|
+
expect(panel.openedFiles).toEqual([]);
|
|
81
|
+
expect(rootReads()).toBe(before);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('a nested create opens the chain it created, so the row is visible', async () => {
|
|
85
|
+
panel.disk = { '.': {} };
|
|
86
|
+
panel.dirReads = [];
|
|
87
|
+
await panel.mount();
|
|
88
|
+
|
|
89
|
+
await panel.openRootNewFile();
|
|
90
|
+
await panel.submitNewFile('lib/deep/parse.ts');
|
|
91
|
+
|
|
92
|
+
// Every folder on the way is expanded and reads its own directory: the
|
|
93
|
+
// entry is not somewhere the user has to go hunting for.
|
|
94
|
+
expect(panel.visibleNames()).toContain('parse.ts');
|
|
95
|
+
expect(panel.visibleNames()).toContain('deep');
|
|
96
|
+
expect(panel.visibleNames()).toContain('lib');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('a rename moves the row in place without re-reading the root', async () => {
|
|
100
|
+
panel.disk = { '.': { 'old.ts': 'file' } };
|
|
101
|
+
panel.dirReads = [];
|
|
102
|
+
await panel.mount();
|
|
103
|
+
const before = rootReads();
|
|
104
|
+
|
|
105
|
+
await panel.rightClickRow('old.ts');
|
|
106
|
+
await panel.clickMenuRow('Rename');
|
|
107
|
+
await panel.submitRename('new.ts');
|
|
108
|
+
|
|
109
|
+
expect(panel.renameRequests.at(-1)).toEqual({
|
|
110
|
+
actionType: 'rename',
|
|
111
|
+
path: 'old.ts',
|
|
112
|
+
newPath: 'new.ts',
|
|
113
|
+
root: '/ws',
|
|
114
|
+
});
|
|
115
|
+
expect(panel.visibleNames()).toContain('new.ts');
|
|
116
|
+
expect(panel.visibleNames()).not.toContain('old.ts');
|
|
117
|
+
expect(rootReads()).toBe(before);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('a refused rename keeps the dialog open with the server’s reason', async () => {
|
|
121
|
+
panel.disk = { '.': { 'old.ts': 'file', 'taken.ts': 'file' } };
|
|
122
|
+
panel.dirReads = [];
|
|
123
|
+
await panel.mount();
|
|
124
|
+
|
|
125
|
+
await panel.rightClickRow('old.ts');
|
|
126
|
+
await panel.clickMenuRow('Rename');
|
|
127
|
+
await panel.submitRename('taken.ts');
|
|
128
|
+
|
|
129
|
+
// The field is still there: "already exists" is a message about what to
|
|
130
|
+
// type next, not a notification about something that happened.
|
|
131
|
+
expect(panel.renameInput()).not.toBeNull();
|
|
132
|
+
expect(panel.documentText()).toContain('taken.ts already exists');
|
|
133
|
+
expect(panel.visibleNames()).toContain('old.ts');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -6,7 +6,7 @@ import { SearchResults } from '@/client/components/workspace/search-panel/Result
|
|
|
6
6
|
import { setSearchReveal } from '@/client/lib/search-reveal';
|
|
7
7
|
import { groupSearchResults, MAX_SEARCH_RESULTS } from '@/shared/lib/fs/search-list';
|
|
8
8
|
import type { SearchResultItem } from '@/shared/types/fs';
|
|
9
|
-
import type { FindOptions } from '@/shared/lib/code/editor/find';
|
|
9
|
+
import type { FindOptions } from '@/shared/lib/code/editor/find-scan';
|
|
10
10
|
|
|
11
11
|
interface SearchPanelBodyProps {
|
|
12
12
|
className: string;
|