@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,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Nested paths at the route: a create and a rename that name SEGMENTS.
|
|
8
|
+
*
|
|
9
|
+
* The Files panel's dialogs used to accept one segment, so `lib/parse.ts` was
|
|
10
|
+
* unreachable from the UI — the user had to create `lib`, expand it, then the
|
|
11
|
+
* file. Both actions now take a relative path, and the rules that keep that
|
|
12
|
+
* inside the folder the user named are asserted here because they are the whole
|
|
13
|
+
* reason a nested path is safe:
|
|
14
|
+
*
|
|
15
|
+
* - a `..` SEGMENT is refused even when it would land back inside the root
|
|
16
|
+
* (`a/../b.ts` resolves there), so the refusal is about the path the user
|
|
17
|
+
* typed rather than about where it happened to land;
|
|
18
|
+
* - an ABSOLUTE path is refused, because `path.join(dir, abs)` discards the
|
|
19
|
+
* folder the dialog showed;
|
|
20
|
+
* - a rename resolves against the LISTED root, not the workspace root — a
|
|
21
|
+
* rename inside a nested repo used to resolve to a path outside the repo;
|
|
22
|
+
* - a rename never overwrites an existing destination, and never moves a folder
|
|
23
|
+
* into its own subtree.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { afterEach, describe, expect, test } from 'bun:test';
|
|
27
|
+
import fs from 'node:fs';
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
import { action } from '@/server/routes/fs/action';
|
|
30
|
+
|
|
31
|
+
/** Probes live in the repo root — the one directory the root allow-list always accepts. */
|
|
32
|
+
const DIR = path.join(process.cwd(), 'tmp-nested-probe');
|
|
33
|
+
|
|
34
|
+
function reset(files: Record<string, string> = {}): void {
|
|
35
|
+
fs.rmSync(DIR, { recursive: true, force: true });
|
|
36
|
+
fs.mkdirSync(DIR, { recursive: true });
|
|
37
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
38
|
+
const target = path.join(DIR, rel);
|
|
39
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
40
|
+
fs.writeFileSync(target, content);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
fs.rmSync(DIR, { recursive: true, force: true });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
async function post(fields: Record<string, string>) {
|
|
49
|
+
const form = new FormData();
|
|
50
|
+
form.append('root', process.cwd());
|
|
51
|
+
for (const [key, value] of Object.entries(fields)) form.append(key, value);
|
|
52
|
+
const res = await action({ request: new Request('http://localhost/api/fs/action', { method: 'POST', body: form }) } as never);
|
|
53
|
+
return { status: res.status, body: (await res.json()) as { success?: boolean; kind?: string; path?: string; previousPath?: string; error?: string } };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe('fs create with a nested path', () => {
|
|
57
|
+
test('creates every directory that leads to the file', async () => {
|
|
58
|
+
reset();
|
|
59
|
+
|
|
60
|
+
const { status, body } = await post({ actionType: 'create', path: 'tmp-nested-probe', name: 'lib/deep/parse.ts' });
|
|
61
|
+
|
|
62
|
+
expect(status).toBe(200);
|
|
63
|
+
expect(body).toEqual({ success: true, kind: 'file', path: 'tmp-nested-probe/lib/deep/parse.ts' });
|
|
64
|
+
expect(fs.readFileSync(path.join(DIR, 'lib/deep/parse.ts'), 'utf8')).toBe('');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('creates a FOLDER when asked, and reports the kind back', async () => {
|
|
68
|
+
reset();
|
|
69
|
+
|
|
70
|
+
const { status, body } = await post({ actionType: 'create', path: 'tmp-nested-probe', name: 'a/b', kind: 'folder' });
|
|
71
|
+
|
|
72
|
+
expect(status).toBe(200);
|
|
73
|
+
expect(body).toEqual({ success: true, kind: 'folder', path: 'tmp-nested-probe/a/b' });
|
|
74
|
+
expect(fs.statSync(path.join(DIR, 'a/b')).isDirectory()).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('normalizes a backslash and repeated separators into one path', async () => {
|
|
78
|
+
reset();
|
|
79
|
+
|
|
80
|
+
const { body } = await post({ actionType: 'create', path: 'tmp-nested-probe', name: 'x\\\\y//z.ts' });
|
|
81
|
+
|
|
82
|
+
expect(body.path).toBe('tmp-nested-probe/x/y/z.ts');
|
|
83
|
+
expect(fs.existsSync(path.join(DIR, 'x/y/z.ts'))).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('refuses a .. segment even when it would land inside the root', async () => {
|
|
87
|
+
reset({ 'keep/.gitkeep': '' });
|
|
88
|
+
|
|
89
|
+
// `keep/../escape.ts` resolves inside the probe directory, so a containment
|
|
90
|
+
// check alone would accept it — the refusal is about the path as typed.
|
|
91
|
+
const { status, body } = await post({ actionType: 'create', path: 'tmp-nested-probe', name: 'keep/../escape.ts' });
|
|
92
|
+
|
|
93
|
+
expect(status).toBe(400);
|
|
94
|
+
expect(body.error).toContain('..');
|
|
95
|
+
expect(fs.existsSync(path.join(DIR, 'escape.ts'))).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('refuses an absolute path', async () => {
|
|
99
|
+
reset();
|
|
100
|
+
|
|
101
|
+
for (const name of ['/tmp/absolute.ts', 'C:/absolute.ts', 'C:relative.ts']) {
|
|
102
|
+
expect((await post({ actionType: 'create', path: 'tmp-nested-probe', name })).status).toBe(400);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('refuses a nested path whose final entry already exists', async () => {
|
|
107
|
+
reset({ 'lib/parse.ts': 'keep me' });
|
|
108
|
+
|
|
109
|
+
const { status, body } = await post({ actionType: 'create', path: 'tmp-nested-probe', name: 'lib/parse.ts' });
|
|
110
|
+
|
|
111
|
+
expect(status).toBe(409);
|
|
112
|
+
expect(body.error).toContain('already exists');
|
|
113
|
+
expect(fs.readFileSync(path.join(DIR, 'lib/parse.ts'), 'utf8')).toBe('keep me');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('fs rename with a nested path', () => {
|
|
118
|
+
test('renames into a new nested folder, creating its parents', async () => {
|
|
119
|
+
reset({ 'a.ts': 'A' });
|
|
120
|
+
|
|
121
|
+
const { status, body } = await post({ actionType: 'rename', path: 'tmp-nested-probe/a.ts', newPath: 'tmp-nested-probe/lib/deep/a.ts' });
|
|
122
|
+
|
|
123
|
+
expect(status).toBe(200);
|
|
124
|
+
expect(body).toEqual({ success: true, path: 'tmp-nested-probe/lib/deep/a.ts', previousPath: 'tmp-nested-probe/a.ts' });
|
|
125
|
+
expect(fs.existsSync(path.join(DIR, 'a.ts'))).toBe(false);
|
|
126
|
+
expect(fs.readFileSync(path.join(DIR, 'lib/deep/a.ts'), 'utf8')).toBe('A');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('a plain rename inside one folder keeps its siblings', async () => {
|
|
130
|
+
reset({ 'src/a.ts': 'A', 'src/b.ts': 'B' });
|
|
131
|
+
|
|
132
|
+
const { body } = await post({ actionType: 'rename', path: 'tmp-nested-probe/src/a.ts', newPath: 'tmp-nested-probe/src/c.ts' });
|
|
133
|
+
|
|
134
|
+
expect(body.path).toBe('tmp-nested-probe/src/c.ts');
|
|
135
|
+
expect(fs.existsSync(path.join(DIR, 'src/b.ts'))).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('refuses an existing destination instead of clobbering it', async () => {
|
|
139
|
+
reset({ 'a.ts': 'A', 'b.ts': 'B' });
|
|
140
|
+
|
|
141
|
+
const { status, body } = await post({ actionType: 'rename', path: 'tmp-nested-probe/a.ts', newPath: 'tmp-nested-probe/b.ts' });
|
|
142
|
+
|
|
143
|
+
expect(status).toBe(409);
|
|
144
|
+
expect(body.error).toContain('already exists');
|
|
145
|
+
expect(fs.readFileSync(path.join(DIR, 'b.ts'), 'utf8')).toBe('B');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('refuses moving a folder into its own subtree', async () => {
|
|
149
|
+
reset({ 'src/a.ts': 'A' });
|
|
150
|
+
|
|
151
|
+
const { status, body } = await post({ actionType: 'rename', path: 'tmp-nested-probe/src', newPath: 'tmp-nested-probe/src/deep' });
|
|
152
|
+
|
|
153
|
+
expect(status).toBe(400);
|
|
154
|
+
expect(body.error).toContain('inside itself');
|
|
155
|
+
expect(fs.existsSync(path.join(DIR, 'src/a.ts'))).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('refuses a .. segment and an absolute destination', async () => {
|
|
159
|
+
reset({ 'a.ts': 'A' });
|
|
160
|
+
|
|
161
|
+
expect((await post({ actionType: 'rename', path: 'tmp-nested-probe/a.ts', newPath: 'tmp-nested-probe/../out.ts' })).status).toBe(400);
|
|
162
|
+
expect((await post({ actionType: 'rename', path: 'tmp-nested-probe/a.ts', newPath: '/tmp/out.ts' })).status).toBe(400);
|
|
163
|
+
expect(fs.existsSync(path.join(DIR, 'a.ts'))).toBe(true);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('renaming to the same path is a no-op that still reports both ends', async () => {
|
|
167
|
+
reset({ 'a.ts': 'A' });
|
|
168
|
+
|
|
169
|
+
const { status, body } = await post({ actionType: 'rename', path: 'tmp-nested-probe/a.ts', newPath: 'tmp-nested-probe/a.ts' });
|
|
170
|
+
|
|
171
|
+
expect(status).toBe(200);
|
|
172
|
+
expect(body.path).toBe('tmp-nested-probe/a.ts');
|
|
173
|
+
expect(fs.readFileSync(path.join(DIR, 'a.ts'), 'utf8')).toBe('A');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('a missing source is refused rather than creating an empty entry', async () => {
|
|
177
|
+
reset();
|
|
178
|
+
|
|
179
|
+
const { status } = await post({ actionType: 'rename', path: 'tmp-nested-probe/gone.ts', newPath: 'tmp-nested-probe/here.ts' });
|
|
180
|
+
|
|
181
|
+
expect(status).toBe(404);
|
|
182
|
+
expect(fs.existsSync(path.join(DIR, 'here.ts'))).toBe(false);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
@@ -3,10 +3,11 @@ import { errorResponse } from '@/server/lib/route-adapter';
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { isMockMode } from '@/server/mock.server';
|
|
6
|
-
import { getDefaultFsRoot, resolveRoot, resolveWithinRoot } from '@/server/lib/fs/root';
|
|
6
|
+
import { getDefaultFsRoot, isWithinRoot, resolveRoot, resolveWithinRoot } from '@/server/lib/fs/root';
|
|
7
7
|
import { runShell } from '@/server/lib/fs/shell';
|
|
8
8
|
import { revealInFileManager } from '@/server/lib/fs/reveal';
|
|
9
|
-
import { duplicateEntry, nextDuplicateName, pasteEntry } from '@/server/lib/fs/transfer';
|
|
9
|
+
import { duplicateEntry, nextDuplicateName, pasteEntry, toRelPath } from '@/server/lib/fs/transfer';
|
|
10
|
+
import { normalizeRelativePath } from '@/server/lib/fs/relative-path';
|
|
10
11
|
import { transferRefusal } from '@/shared/lib/fs/clipboard';
|
|
11
12
|
import { toDiskText } from '@/shared/lib/code/line-endings';
|
|
12
13
|
|
|
@@ -104,13 +105,15 @@ export async function action({ request }: ActionFunctionArgs) {
|
|
|
104
105
|
await Bun.write(fullPath, content);
|
|
105
106
|
return json({ success: true });
|
|
106
107
|
} else if (actionType === 'create') {
|
|
107
|
-
// The Files panel's "New file": an empty
|
|
108
|
-
// right-clicked.
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
108
|
+
// The Files panel's "New file"/"New folder": an empty entry beside the
|
|
109
|
+
// folder that was right-clicked. The NAME may carry segments
|
|
110
|
+
// (`lib/parse.ts`), so one dialog creates a nested path and every
|
|
111
|
+
// intermediate directory with it — the grammar that keeps it inside the
|
|
112
|
+
// named folder lives in `normalizeRelativePath`, not in a slash test here.
|
|
113
|
+
const kind = formData.get('kind') === 'folder' ? 'folder' : 'file';
|
|
114
|
+
const name = normalizeRelativePath((formData.get('name') as string) ?? '');
|
|
115
|
+
if (!name) {
|
|
116
|
+
return json({ error: 'Enter a name without .. or a leading slash' }, { status: 400 });
|
|
114
117
|
}
|
|
115
118
|
const target = resolveWithinRoot(scopedRoot, path.join(filePath || '.', name));
|
|
116
119
|
if (!target) {
|
|
@@ -122,21 +125,49 @@ export async function action({ request }: ActionFunctionArgs) {
|
|
|
122
125
|
if ((await Bun.file(target).stat().catch(() => null)) !== null) {
|
|
123
126
|
return json({ error: `${name} already exists` }, { status: 409 });
|
|
124
127
|
}
|
|
125
|
-
await fs.promises.mkdir(
|
|
126
|
-
|
|
128
|
+
if (kind === 'folder') await fs.promises.mkdir(target, { recursive: true });
|
|
129
|
+
else {
|
|
130
|
+
await fs.promises.mkdir(path.dirname(target), { recursive: true });
|
|
131
|
+
await Bun.write(target, '');
|
|
132
|
+
}
|
|
127
133
|
// Relative to the LISTED root, which is the identity the tree renders by.
|
|
128
|
-
return json({ success: true, path:
|
|
134
|
+
return json({ success: true, kind, path: toRelPath(scopedRoot, target) });
|
|
129
135
|
} else if (actionType === 'delete') {
|
|
130
136
|
await fs.promises.rm(fullPath, { recursive: true, force: true });
|
|
131
137
|
return json({ success: true });
|
|
132
138
|
} else if (actionType === 'rename') {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
139
|
+
// `newPath` is relative to the LISTED root, exactly like `path` is. It
|
|
140
|
+
// used to be resolved against `rootDir`, so renaming inside a nested repo
|
|
141
|
+
// resolved to a path under the workspace root instead of the repo — a
|
|
142
|
+
// move out of the tree the row belongs to.
|
|
143
|
+
const newPath = normalizeRelativePath((formData.get('newPath') as string) ?? '');
|
|
144
|
+
if (!newPath) {
|
|
145
|
+
return json({ error: 'Enter a name without .. or a leading slash' }, { status: 400 });
|
|
146
|
+
}
|
|
147
|
+
const fullNewPath = resolveWithinRoot(scopedRoot, newPath);
|
|
148
|
+
if (!fullNewPath) {
|
|
136
149
|
return json({ error: 'Invalid new path' }, { status: 403 });
|
|
137
150
|
}
|
|
151
|
+
if (fullNewPath === fullPath) return json({ success: true, path: newPath, previousPath: filePath });
|
|
152
|
+
const source = await Bun.file(fullPath).stat().catch(() => null);
|
|
153
|
+
if (!source) return json({ error: 'That entry no longer exists' }, { status: 404 });
|
|
154
|
+
// A folder cannot be moved into itself: `rename(a, a/b)` fails with
|
|
155
|
+
// EINVAL on every platform, but with an errno the panel cannot explain —
|
|
156
|
+
// and the destination directory may not exist yet, so the check has to be
|
|
157
|
+
// on the PATHS rather than on the filesystem.
|
|
158
|
+
if (source.isDirectory() && isWithinRoot(fullPath, fullNewPath)) {
|
|
159
|
+
return json({ error: 'A folder cannot be moved inside itself' }, { status: 400 });
|
|
160
|
+
}
|
|
161
|
+
// `rename` clobbers silently, so an existing destination is refused: the
|
|
162
|
+
// entry the user meant to keep would otherwise disappear without a word.
|
|
163
|
+
if ((await Bun.file(fullNewPath).stat().catch(() => null)) !== null) {
|
|
164
|
+
return json({ error: `${newPath} already exists` }, { status: 409 });
|
|
165
|
+
}
|
|
166
|
+
// The destination's parents are created, which is what makes a rename
|
|
167
|
+
// INTO a new folder work — the same nested grammar the create accepts.
|
|
168
|
+
await fs.promises.mkdir(path.dirname(fullNewPath), { recursive: true });
|
|
138
169
|
await fs.promises.rename(fullPath, fullNewPath);
|
|
139
|
-
return json({ success: true });
|
|
170
|
+
return json({ success: true, path: newPath, previousPath: filePath });
|
|
140
171
|
} else if (actionType === 'open_explorer') {
|
|
141
172
|
const isDir = (await Bun.file(fullPath).stat().catch(() => null))?.isDirectory() ?? false;
|
|
142
173
|
// A file reveals its containing directory — there is no portable "select
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* answers "no <op> in progress" and only `reset --merge HEAD` clears it.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import { afterAll, describe, expect, test } from 'bun:test';
|
|
22
|
+
import { afterAll, describe, expect, setDefaultTimeout, test } from 'bun:test';
|
|
23
23
|
import fs from 'node:fs';
|
|
24
24
|
import path from 'node:path';
|
|
25
25
|
import { action } from '@/server/routes/fs/git';
|
|
@@ -36,6 +36,16 @@ import { removeDir } from '@/test-support/remove-dir';
|
|
|
36
36
|
* action ran against THE Kabmo REPOSITORY.
|
|
37
37
|
*/
|
|
38
38
|
const FIXTURE_ROOT = path.join(process.cwd(), 'tmp-test-git-merge');
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Every case here builds a real repository and drives several `git` processes
|
|
42
|
+
* through it, so Bun's 5s per-test default is a budget for the RUNNER rather
|
|
43
|
+
* than for the code. Measured on a loaded CI runner: the cherry-pick case alone
|
|
44
|
+
* spent 5.0s and timed out, while the same commit passed the `Checks` job and
|
|
45
|
+
* failed the `Release` one minutes later — the same suite, two machines, one
|
|
46
|
+
* answer each. `git-actions-sync.test.ts` sets the same 30s for the same reason.
|
|
47
|
+
*/
|
|
48
|
+
setDefaultTimeout(30_000);
|
|
39
49
|
const repos: string[] = [];
|
|
40
50
|
|
|
41
51
|
function git(cwd: string, ...args: string[]): string {
|
|
@@ -21,6 +21,7 @@ import fs from 'fs';
|
|
|
21
21
|
import path from 'path';
|
|
22
22
|
import { readFile } from '@/server/routes/fs/read';
|
|
23
23
|
import { EDITOR_MAX_FILE_BYTES, PDF_MAX_FILE_BYTES } from '@/shared/lib/fs/file-kind';
|
|
24
|
+
import { formatBytes } from '@/shared/lib/format/number';
|
|
24
25
|
|
|
25
26
|
const DIR = path.join(process.cwd(), 'tmp-read-gate');
|
|
26
27
|
|
|
@@ -88,16 +89,23 @@ describe('fs read gate', () => {
|
|
|
88
89
|
|
|
89
90
|
expect(status).toBe(415);
|
|
90
91
|
expect(body.kind).toBe('too-large');
|
|
91
|
-
|
|
92
|
+
// Derived from the constant, not spelled out: the sentence and the gate must
|
|
93
|
+
// name the same number, and a literal here would pass while they drifted.
|
|
94
|
+
expect(body.error).toContain(formatBytes(EDITOR_MAX_FILE_BYTES));
|
|
92
95
|
});
|
|
93
96
|
|
|
94
97
|
// A PDF is streamed to Chrome's own viewer through `/api/fs/raw`, so the text
|
|
95
|
-
// ceiling
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
// "
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
// ceiling has no bearing on it — the route must point at the raw route rather
|
|
99
|
+
// than refuse it. Refusing it was the bug: the size test ran before the
|
|
100
|
+
// document branch, so a 6 MB PDF was answered "larger than the 5.00 MB the
|
|
101
|
+
// editor opens". 6 MB is the size that was over the old text ceiling and is
|
|
102
|
+
// still a perfectly ordinary PDF.
|
|
103
|
+
//
|
|
104
|
+
// It is written at 6 MB rather than at `EDITOR_MAX_FILE_BYTES + 1` because the
|
|
105
|
+
// two ceilings are independent: the text one is now ABOVE the PDF one, so
|
|
106
|
+
// "past text, under PDF" is no longer a size that exists.
|
|
107
|
+
test('a PDF past the old text ceiling is still served by the raw route', async () => {
|
|
108
|
+
write('report.pdf', new Uint8Array(6 * 1024 * 1024).fill(0x25));
|
|
101
109
|
const { status, body } = await read('report.pdf');
|
|
102
110
|
|
|
103
111
|
expect(status).toBe(400);
|
|
@@ -105,6 +113,15 @@ describe('fs read gate', () => {
|
|
|
105
113
|
expect(body.kind).toBeUndefined();
|
|
106
114
|
});
|
|
107
115
|
|
|
116
|
+
test('a PDF past its own ceiling is refused before the document branch', async () => {
|
|
117
|
+
write('enormous.pdf', new Uint8Array(PDF_MAX_FILE_BYTES + 1).fill(0x25));
|
|
118
|
+
const { status, body } = await read('enormous.pdf');
|
|
119
|
+
|
|
120
|
+
expect(status).toBe(415);
|
|
121
|
+
expect(body.kind).toBe('too-large');
|
|
122
|
+
expect(body.error).toContain(formatBytes(PDF_MAX_FILE_BYTES));
|
|
123
|
+
});
|
|
124
|
+
|
|
108
125
|
test('a PDF past the PDF ceiling is refused for size, naming 30 MB', async () => {
|
|
109
126
|
write('huge.pdf', new Uint8Array(PDF_MAX_FILE_BYTES + 1).fill(0x25));
|
|
110
127
|
const { status, body } = await read('huge.pdf');
|
|
@@ -20,7 +20,8 @@ import fs from 'fs';
|
|
|
20
20
|
import path from 'path';
|
|
21
21
|
import { testPosix } from '@/test-support/platform';
|
|
22
22
|
import { resolveFileScope } from '@/server/routes/fs/scope';
|
|
23
|
-
import { DOCUMENT_MAX_FILE_BYTES, PDF_MAX_FILE_BYTES } from '@/shared/lib/fs/file-kind';
|
|
23
|
+
import { DOCUMENT_MAX_FILE_BYTES, EDITOR_MAX_FILE_BYTES, PDF_MAX_FILE_BYTES } from '@/shared/lib/fs/file-kind';
|
|
24
|
+
import { formatBytes } from '@/shared/lib/format/number';
|
|
24
25
|
|
|
25
26
|
const APP = process.cwd();
|
|
26
27
|
const DIR = path.join(APP, 'tmp-scope-probe');
|
|
@@ -158,14 +159,18 @@ describe('fs scope route', () => {
|
|
|
158
159
|
expect(body.error).toContain('20.00 MB');
|
|
159
160
|
});
|
|
160
161
|
|
|
161
|
-
// Plain text keeps the text ceiling
|
|
162
|
-
// life of the tab
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
expect(status).toBe(200);
|
|
168
|
-
expect(body.kind).toBe('
|
|
169
|
-
|
|
162
|
+
// Plain text keeps the text ceiling, which is its own number: the buffer is
|
|
163
|
+
// held for the life of the tab and re-posted on every save, where a document
|
|
164
|
+
// is converted once. 6 MB used to be over it; it is a normal generated file
|
|
165
|
+
// and must open.
|
|
166
|
+
test('a text file over the text ceiling is refused, and one under it is not', async () => {
|
|
167
|
+
const ok = await scope(write('big.log', new Uint8Array(6 * 1024 * 1024).fill(0x61)));
|
|
168
|
+
expect(ok.status).toBe(200);
|
|
169
|
+
expect(ok.body.kind).toBe('text');
|
|
170
|
+
|
|
171
|
+
const over = await scope(write('huge.log', new Uint8Array(EDITOR_MAX_FILE_BYTES + 1).fill(0x61)));
|
|
172
|
+
expect(over.status).toBe(200);
|
|
173
|
+
expect(over.body.kind).toBe('too-large');
|
|
174
|
+
expect(over.body.error).toContain(formatBytes(EDITOR_MAX_FILE_BYTES));
|
|
170
175
|
});
|
|
171
176
|
});
|
|
@@ -26,25 +26,14 @@
|
|
|
26
26
|
import { json, type LoaderFunctionArgs } from '@/server/lib/remix-compat';
|
|
27
27
|
import { resolveScopeForAbsolutePath } from '@/server/lib/fs/root';
|
|
28
28
|
import { BINARY_SNIFF_BYTES, classifyEditorFile, unsupportedEditorMessage } from '@/shared/lib/fs/file-kind';
|
|
29
|
+
import { pathFromFileUrl } from '@/shared/lib/fs/file-url';
|
|
29
30
|
import fs from 'fs';
|
|
30
31
|
import path from 'path';
|
|
31
32
|
|
|
32
|
-
/**
|
|
33
|
-
* A `file://` URL to the path it names.
|
|
34
|
-
*
|
|
35
|
-
* The pathname is percent-encoded (a space is `%20`, and a Finder drop of
|
|
36
|
-
* `My Notes.md` is exactly that), so decoding is not optional. `decodeURIComponent`
|
|
37
|
-
* can throw on a malformed escape, which is answered as "not a path" rather
|
|
38
|
-
* than surfacing as a 500.
|
|
39
|
-
*/
|
|
33
|
+
/** A `file://` URL to the path it names; see `pathFromFileUrl` for the rules. */
|
|
40
34
|
function pathFromReference(reference: string): string | null {
|
|
41
35
|
if (!/^file:\/\//i.test(reference)) return reference;
|
|
42
|
-
|
|
43
|
-
const url = new URL(reference);
|
|
44
|
-
return decodeURIComponent(url.pathname);
|
|
45
|
-
} catch {
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
36
|
+
return pathFromFileUrl(reference);
|
|
48
37
|
}
|
|
49
38
|
|
|
50
39
|
export async function resolveFileScope({ request }: LoaderFunctionArgs) {
|
|
@@ -12,6 +12,7 @@ import { waitingBindings } from '@/server/routes/waiting/bindings';
|
|
|
12
12
|
import { settingsBindings } from '@/server/routes/settings';
|
|
13
13
|
import { fsBindings } from '@/server/routes/fs';
|
|
14
14
|
import { foldersBindings } from '@/server/routes/folders';
|
|
15
|
+
import { worktreesBindings } from '@/server/routes/worktrees';
|
|
15
16
|
import { filesBindings } from '@/server/routes/files';
|
|
16
17
|
import { telemetryBindings } from '@/server/routes/telemetry';
|
|
17
18
|
import { terminalBindings } from '@/server/routes/terminal';
|
|
@@ -42,6 +43,7 @@ const allBindings: HandlerBinding[] = [
|
|
|
42
43
|
...settingsBindings,
|
|
43
44
|
...fsBindings,
|
|
44
45
|
...foldersBindings,
|
|
46
|
+
...worktreesBindings,
|
|
45
47
|
...filesBindings,
|
|
46
48
|
...telemetryBindings,
|
|
47
49
|
...terminalBindings,
|
|
@@ -12,11 +12,16 @@ import { fetchShellRelease, type ShellRelease } from '@/server/lib/updates/shell
|
|
|
12
12
|
*
|
|
13
13
|
* A failed probe is a valid answer (`url: null`), never a 500: the sidebar's job
|
|
14
14
|
* is to omit a button it cannot back with a link.
|
|
15
|
+
*
|
|
16
|
+
* PUBLIC, unlike its siblings: the Android shell asks this from native code
|
|
17
|
+
* (`AppUpdateCheck`), which has no session cookie, and its "Check for update"
|
|
18
|
+
* button is for a shell too stale to load the console. See `PUBLIC_EXACT` in
|
|
19
|
+
* `lib/auth/guard.ts`.
|
|
15
20
|
*/
|
|
16
21
|
export async function loader() {
|
|
17
22
|
const release: ShellRelease | null = await fetchShellRelease('android');
|
|
18
23
|
return json(
|
|
19
|
-
release ?? { url: null, latest: null, releaseUrl: null },
|
|
24
|
+
release ?? { url: null, latest: null, releaseUrl: null, direct: false },
|
|
20
25
|
{ headers: NO_STORE_HEADERS },
|
|
21
26
|
);
|
|
22
27
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { json, NO_STORE_HEADERS } from '@/server/lib/remix-compat';
|
|
2
|
+
import { fetchShellRelease, type ShellRelease } from '@/server/lib/updates/shell-release';
|
|
3
|
+
import { isDesktopShellKind, type DesktopShellKind } from '@/shared/lib/updates/shell-ua';
|
|
4
|
+
import { parseShellArch, type ShellArch } from '@/shared/lib/updates/shell-arch';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* GET /api/updates/desktop?kind=macos|windows|linux[&arch=arm64|x64]
|
|
8
|
+
*
|
|
9
|
+
* ONE route with an explicit `kind`, unlike the mobile pair's two routes, and
|
|
10
|
+
* the difference is deliberate. `?kind` here is REQUIRED and refused when it is
|
|
11
|
+
* missing or names no desktop shell, so no absence can silently mean a platform:
|
|
12
|
+
* the mobile pair's split exists precisely because a defaulted parameter would
|
|
13
|
+
* have made "the newest shell" mean Android without anyone saying so.
|
|
14
|
+
*
|
|
15
|
+
* ## `arch` is what makes the offer installable
|
|
16
|
+
*
|
|
17
|
+
* Only an INSTALLED desktop shell asks for this — the page reports its own
|
|
18
|
+
* version over the shell bridge (`__kabmoDesktop.shellVersion`), because a
|
|
19
|
+
* desktop app carries no User-Agent token. Without `arch` the answer is the
|
|
20
|
+
* RELEASE PAGE: every desktop artifact is per-architecture, and a console served
|
|
21
|
+
* to another machine cannot guess which one this window runs on.
|
|
22
|
+
*
|
|
23
|
+
* WITH `arch` the answer is that machine's own installer, and `direct: true`
|
|
24
|
+
* says so — which is what lets the sidebar offer an in-app update instead of a
|
|
25
|
+
* link. The shell still verifies the bytes before installing them; this route
|
|
26
|
+
* only decides WHICH file, never whether it is trusted.
|
|
27
|
+
*
|
|
28
|
+
* An `arch` that names nothing this product publishes is a REFUSAL rather than a
|
|
29
|
+
* silent fallback: a caller that asked for a build we do not make should be told,
|
|
30
|
+
* not handed the other architecture's installer.
|
|
31
|
+
*
|
|
32
|
+
* A failed probe is a valid answer (`url: null`), never a 500: the sidebar's job
|
|
33
|
+
* is to omit a button it cannot back with a link.
|
|
34
|
+
*/
|
|
35
|
+
export async function loader({ request }: { request: Request }) {
|
|
36
|
+
const params = new URL(request.url).searchParams;
|
|
37
|
+
const kind = params.get('kind');
|
|
38
|
+
if (!isDesktopShellKind(kind ?? '')) {
|
|
39
|
+
return json(
|
|
40
|
+
{ error: 'kind must be one of macos, windows, linux' },
|
|
41
|
+
{ status: 400, headers: NO_STORE_HEADERS },
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const requested = params.get('arch');
|
|
46
|
+
const arch: ShellArch | null = requested ? parseShellArch(requested) : null;
|
|
47
|
+
if (requested && !arch) {
|
|
48
|
+
return json(
|
|
49
|
+
{ error: 'arch must be one of arm64, x64' },
|
|
50
|
+
{ status: 400, headers: NO_STORE_HEADERS },
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const release: ShellRelease | null = await fetchShellRelease(kind as DesktopShellKind, fetch, arch);
|
|
55
|
+
return json(
|
|
56
|
+
release ?? { url: null, latest: null, releaseUrl: null, direct: false },
|
|
57
|
+
{ headers: NO_STORE_HEADERS },
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -3,6 +3,7 @@ import * as updatesAndroid from '@/server/routes/updates/android';
|
|
|
3
3
|
import * as updatesApply from '@/server/routes/updates/apply';
|
|
4
4
|
import * as updatesChangelog from '@/server/routes/updates/changelog';
|
|
5
5
|
import * as updatesCheck from '@/server/routes/updates/check';
|
|
6
|
+
import * as updatesDesktop from '@/server/routes/updates/desktop';
|
|
6
7
|
import * as updatesIos from '@/server/routes/updates/ios';
|
|
7
8
|
|
|
8
9
|
export const updatesBindings: HandlerBinding[] = [
|
|
@@ -10,5 +11,6 @@ export const updatesBindings: HandlerBinding[] = [
|
|
|
10
11
|
...bindingsFor(updatesApply, '/api/updates/apply'),
|
|
11
12
|
...bindingsFor(updatesChangelog, '/api/updates/changelog'),
|
|
12
13
|
...bindingsFor(updatesCheck, '/api/updates/check'),
|
|
14
|
+
...bindingsFor(updatesDesktop, '/api/updates/desktop'),
|
|
13
15
|
...bindingsFor(updatesIos, '/api/updates/ios'),
|
|
14
16
|
];
|
|
@@ -16,11 +16,15 @@ import { fetchShellRelease, type ShellRelease } from '@/server/lib/updates/shell
|
|
|
16
16
|
*
|
|
17
17
|
* A failed probe is a valid answer (`url: null`), never a 500: the sidebar's job
|
|
18
18
|
* is to omit a button it cannot back with a link.
|
|
19
|
+
*
|
|
20
|
+
* PUBLIC, for the same reason as `/api/updates/android`: the installed shell
|
|
21
|
+
* asks it from native code with no session cookie, and the check has to work
|
|
22
|
+
* before the console can load.
|
|
19
23
|
*/
|
|
20
24
|
export async function loader() {
|
|
21
25
|
const release: ShellRelease | null = await fetchShellRelease('ios');
|
|
22
26
|
return json(
|
|
23
|
-
release ?? { url: null, latest: null, releaseUrl: null },
|
|
27
|
+
release ?? { url: null, latest: null, releaseUrl: null, direct: false },
|
|
24
28
|
{ headers: NO_STORE_HEADERS },
|
|
25
29
|
);
|
|
26
30
|
}
|