@neurocode-ai/app 1.18.8
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/AGENTS.md +35 -0
- package/README.md +50 -0
- package/V1_API_MIGRATION.md +220 -0
- package/bunfig.toml +3 -0
- package/create-effect-simplification-spec.md +515 -0
- package/e2e/performance/AGENTS.md +13 -0
- package/e2e/performance/README.md +79 -0
- package/e2e/performance/benchmark.ts +144 -0
- package/e2e/performance/chrome-trace.ts +95 -0
- package/e2e/performance/playwright.config.ts +20 -0
- package/e2e/performance/playwright.uncapped.config.ts +13 -0
- package/e2e/performance/timeline/first-navigation-benchmark.spec.ts +87 -0
- package/e2e/performance/timeline/first-navigation-metrics.ts +32 -0
- package/e2e/performance/timeline/first-navigation-probe.ts +86 -0
- package/e2e/performance/timeline/home-tab-navigation-benchmark.spec.ts +114 -0
- package/e2e/performance/timeline/navigation-milestones.ts +128 -0
- package/e2e/performance/timeline/review-pane-scaling-benchmark.spec.ts +312 -0
- package/e2e/performance/timeline/session-parent-hydration-benchmark.spec.ts +172 -0
- package/e2e/performance/timeline/session-tab-flash.spec.ts +67 -0
- package/e2e/performance/timeline/session-tab-repaint-probe.ts +251 -0
- package/e2e/performance/timeline/session-tab-switch-benchmark.spec.ts +144 -0
- package/e2e/performance/timeline/session-tab-switch-metrics.ts +59 -0
- package/e2e/performance/timeline/session-tab-switch-probe.ts +226 -0
- package/e2e/performance/timeline/session-timeline-benchmark.fixture.ts +504 -0
- package/e2e/performance/timeline/session-timeline-benchmark.spec.ts +306 -0
- package/e2e/performance/timeline/session-timeline-profile.ts +40 -0
- package/e2e/performance/timeline/session-timeline-stream-probe.ts +547 -0
- package/e2e/performance/timeline/session-timeline-stress.fixture.ts +369 -0
- package/e2e/performance/timeline/timeline-test-helpers.ts +134 -0
- package/e2e/performance/timeline-stability/README.md +61 -0
- package/e2e/performance/timeline-stability/adverse.spec.ts +250 -0
- package/e2e/performance/timeline-stability/context-matrix.spec.ts +192 -0
- package/e2e/performance/timeline-stability/environment-matrix.spec.ts +113 -0
- package/e2e/performance/timeline-stability/file-matrix.spec.ts +121 -0
- package/e2e/performance/timeline-stability/file-mutation.spec.ts +113 -0
- package/e2e/performance/timeline-stability/fixture.test.ts +68 -0
- package/e2e/performance/timeline-stability/fixture.ts +566 -0
- package/e2e/performance/timeline-stability/interaction.spec.ts +242 -0
- package/e2e/performance/timeline-stability/lifecycle.spec.ts +157 -0
- package/e2e/performance/timeline-stability/oracle-browser.spec.ts +75 -0
- package/e2e/performance/timeline-stability/playwright.config.ts +17 -0
- package/e2e/performance/timeline-stability/scroll-interaction.spec.ts +353 -0
- package/e2e/performance/timeline-stability/shell-matrix.spec.ts +255 -0
- package/e2e/performance/timeline-stability/tool-mutation.spec.ts +106 -0
- package/e2e/performance/timeline-stability/tools.spec.ts +198 -0
- package/e2e/performance/timeline-stability/transition-matrix.spec.ts +272 -0
- package/e2e/performance/unit/chrome-trace-write.test.ts +15 -0
- package/e2e/performance/unit/first-navigation-metrics.test.ts +32 -0
- package/e2e/performance/unit/mock-server.test.ts +46 -0
- package/e2e/performance/unit/navigation-milestones.test.ts +34 -0
- package/e2e/performance/unit/session-tab-repaint-probe.test.ts +42 -0
- package/e2e/performance/unit/session-tab-switch-metrics.test.ts +86 -0
- package/e2e/performance/unit/session-tab-switch-probe.test.ts +55 -0
- package/e2e/performance/unit/session-timeline-stream-probe.test.ts +14 -0
- package/e2e/performance/unit/session-timeline-visual-tracking.test.ts +16 -0
- package/e2e/performance/unit/timeline-test-helpers.test.ts +10 -0
- package/e2e/performance/unit/visual-stability.test.ts +392 -0
- package/e2e/regression/cross-server-tab-close.spec.ts +153 -0
- package/e2e/regression/file-browser-sidebar-tab-switch.spec.ts +150 -0
- package/e2e/regression/legacy-new-session.spec.ts +41 -0
- package/e2e/regression/new-session-panel-corner.spec.ts +83 -0
- package/e2e/regression/prompt-input-v2-command-draft.spec.ts +50 -0
- package/e2e/regression/prompt-thinking-level.spec.ts +84 -0
- package/e2e/regression/remote-session-settings.spec.ts +302 -0
- package/e2e/regression/remote-tab-busy.spec.ts +131 -0
- package/e2e/regression/review-image-flash.spec.ts +202 -0
- package/e2e/regression/review-line-comment.spec.ts +158 -0
- package/e2e/regression/review-open-file.spec.ts +163 -0
- package/e2e/regression/review-state-persistence.spec.ts +155 -0
- package/e2e/regression/review-tab-switch.spec.ts +147 -0
- package/e2e/regression/review-terminal-stacked.spec.ts +308 -0
- package/e2e/regression/session-list-path-loading.spec.ts +41 -0
- package/e2e/regression/session-request-docks.spec.ts +221 -0
- package/e2e/regression/session-timeline-accessibility.spec.ts +22 -0
- package/e2e/regression/session-timeline-collapse-state.spec.ts +439 -0
- package/e2e/regression/session-timeline-context-resize.spec.ts +374 -0
- package/e2e/regression/session-timeline-context-state.spec.ts +31 -0
- package/e2e/regression/session-timeline-file-projection.spec.ts +52 -0
- package/e2e/regression/session-timeline-file-state.spec.ts +94 -0
- package/e2e/regression/session-timeline-history-root.spec.ts +242 -0
- package/e2e/regression/session-timeline-lifecycle-state.spec.ts +111 -0
- package/e2e/regression/session-timeline-locale-projection.spec.ts +25 -0
- package/e2e/regression/session-timeline-projection.spec.ts +287 -0
- package/e2e/regression/session-timeline-reasoning-projection.spec.ts +93 -0
- package/e2e/regression/session-timeline-reducer-projection.spec.ts +43 -0
- package/e2e/regression/session-timeline-shell-outline.spec.ts +228 -0
- package/e2e/regression/session-timeline-tool-projection.spec.ts +99 -0
- package/e2e/regression/session-timeline-tool-state.spec.ts +77 -0
- package/e2e/regression/session-timeline-transport.spec.ts +116 -0
- package/e2e/regression/session-todo-dock-navigation.spec.ts +190 -0
- package/e2e/regression/subagent-child-navigation.spec.ts +203 -0
- package/e2e/regression/tab-navigate-mousedown.spec.ts +127 -0
- package/e2e/regression/terminal-composer-focus.spec.ts +227 -0
- package/e2e/regression/terminal-hidden.spec.ts +117 -0
- package/e2e/regression/terminal-tab-switch.spec.ts +165 -0
- package/e2e/reproduction/timeline-suspense/index.html +34 -0
- package/e2e/reproduction/timeline-suspense/main.tsx +315 -0
- package/e2e/reproduction/timeline-suspense/playwright.config.ts +34 -0
- package/e2e/reproduction/timeline-suspense/timeline-suspense.repro.ts +179 -0
- package/e2e/reproduction/timeline-suspense/vite.config.ts +7 -0
- package/e2e/smoke/session-timeline.fixture.ts +315 -0
- package/e2e/smoke/session-timeline.spec.ts +740 -0
- package/e2e/tsconfig.json +20 -0
- package/e2e/utils/errors.ts +18 -0
- package/e2e/utils/mock-server.ts +443 -0
- package/e2e/utils/sse-transport.ts +312 -0
- package/e2e/utils/visual-stability/analyzer.ts +209 -0
- package/e2e/utils/visual-stability/capture.ts +51 -0
- package/e2e/utils/visual-stability/index.ts +8 -0
- package/e2e/utils/visual-stability/invariant.ts +112 -0
- package/e2e/utils/visual-stability/model.ts +47 -0
- package/e2e/utils/visual-stability/probe.ts +226 -0
- package/e2e/utils/visual-stability/regions.ts +18 -0
- package/e2e/utils/visual-stability/reporter.ts +63 -0
- package/e2e/utils/visual-stability/scenario.ts +20 -0
- package/e2e/utils/visual-stability.ts +54 -0
- package/e2e/utils/waits.ts +11 -0
- package/happydom.ts +75 -0
- package/index.html +28 -0
- package/package.json +96 -0
- package/playwright.config.ts +45 -0
- package/public/_headers +17 -0
- package/public/apple-touch-icon-v3.png +1 -0
- package/public/apple-touch-icon.png +1 -0
- package/public/assets/Inter.ttf +0 -0
- package/public/assets/JetBrainsMonoNerdFontMono-Regular.woff2 +0 -0
- package/public/favicon-96x96-v3.png +1 -0
- package/public/favicon-96x96.png +1 -0
- package/public/favicon-v3.ico +1 -0
- package/public/favicon-v3.svg +1 -0
- package/public/favicon.ico +1 -0
- package/public/favicon.svg +1 -0
- package/public/oc-theme-preload.js +40 -0
- package/public/site.webmanifest +1 -0
- package/public/social-share-zen.png +1 -0
- package/public/social-share.png +1 -0
- package/public/web-app-manifest-192x192.png +1 -0
- package/public/web-app-manifest-512x512.png +1 -0
- package/src/addons/serialize.test.ts +327 -0
- package/src/addons/serialize.ts +642 -0
- package/src/app.tsx +659 -0
- package/src/assets/help/home.png +0 -0
- package/src/assets/help/introducing-tabs.mp4 +0 -0
- package/src/assets/help/placeholder.png +0 -0
- package/src/assets/help/tabs.png +0 -0
- package/src/components/command-palette.ts +281 -0
- package/src/components/command-tooltip-keybind.test.ts +22 -0
- package/src/components/command-tooltip-keybind.ts +11 -0
- package/src/components/debug-bar.tsx +550 -0
- package/src/components/dialog-command-palette-v2.css +220 -0
- package/src/components/dialog-command-palette-v2.tsx +342 -0
- package/src/components/dialog-connect-provider.stories.tsx +73 -0
- package/src/components/dialog-connect-provider.tsx +1178 -0
- package/src/components/dialog-custom-provider-form.ts +158 -0
- package/src/components/dialog-custom-provider.test.ts +80 -0
- package/src/components/dialog-custom-provider.tsx +330 -0
- package/src/components/dialog-edit-project-v2.tsx +156 -0
- package/src/components/dialog-edit-project.tsx +170 -0
- package/src/components/dialog-fork.tsx +104 -0
- package/src/components/dialog-manage-models.tsx +265 -0
- package/src/components/dialog-release-notes.tsx +144 -0
- package/src/components/dialog-select-directory-v2.css +107 -0
- package/src/components/dialog-select-directory-v2.tsx +378 -0
- package/src/components/dialog-select-directory.tsx +191 -0
- package/src/components/dialog-select-file.tsx +189 -0
- package/src/components/dialog-select-mcp.tsx +92 -0
- package/src/components/dialog-select-model-search.test.ts +19 -0
- package/src/components/dialog-select-model-search.ts +18 -0
- package/src/components/dialog-select-model-unpaid-v2.stories.tsx +55 -0
- package/src/components/dialog-select-model-unpaid-v2.tsx +175 -0
- package/src/components/dialog-select-model-unpaid.tsx +147 -0
- package/src/components/dialog-select-model.tsx +549 -0
- package/src/components/dialog-select-server.tsx +722 -0
- package/src/components/dialog-settings.tsx +94 -0
- package/src/components/dialog-usage-exceeded.tsx +44 -0
- package/src/components/directory-picker-domain.test.ts +248 -0
- package/src/components/directory-picker-domain.ts +407 -0
- package/src/components/directory-picker-policy.ts +7 -0
- package/src/components/directory-picker.test.ts +21 -0
- package/src/components/directory-picker.tsx +45 -0
- package/src/components/edit-project.ts +122 -0
- package/src/components/file-tree-v2-model.test.ts +74 -0
- package/src/components/file-tree-v2-model.ts +107 -0
- package/src/components/file-tree-v2.tsx +299 -0
- package/src/components/file-tree.test.ts +80 -0
- package/src/components/file-tree.tsx +509 -0
- package/src/components/help-button.tsx +148 -0
- package/src/components/link.tsx +26 -0
- package/src/components/model-tooltip.tsx +124 -0
- package/src/components/pierre-tree.test.ts +23 -0
- package/src/components/prompt-input/attachments.test.ts +108 -0
- package/src/components/prompt-input/attachments.ts +228 -0
- package/src/components/prompt-input/build-request-parts.test.ts +396 -0
- package/src/components/prompt-input/build-request-parts.ts +213 -0
- package/src/components/prompt-input/context-items.tsx +98 -0
- package/src/components/prompt-input/contracts.ts +57 -0
- package/src/components/prompt-input/drag-overlay.tsx +25 -0
- package/src/components/prompt-input/editor-dom.test.ts +99 -0
- package/src/components/prompt-input/editor-dom.ts +148 -0
- package/src/components/prompt-input/files.ts +98 -0
- package/src/components/prompt-input/history-store.ts +47 -0
- package/src/components/prompt-input/history.test.ts +153 -0
- package/src/components/prompt-input/history.ts +256 -0
- package/src/components/prompt-input/image-attachments.css +43 -0
- package/src/components/prompt-input/image-attachments.tsx +168 -0
- package/src/components/prompt-input/paste.ts +24 -0
- package/src/components/prompt-input/placeholder.test.ts +48 -0
- package/src/components/prompt-input/placeholder.ts +20 -0
- package/src/components/prompt-input/slash-popover.tsx +376 -0
- package/src/components/prompt-input/submission-state.ts +33 -0
- package/src/components/prompt-input/submit.test.ts +598 -0
- package/src/components/prompt-input/submit.ts +634 -0
- package/src/components/prompt-input/transient-state.ts +46 -0
- package/src/components/prompt-input-v2.tsx +592 -0
- package/src/components/prompt-input.stories.tsx +223 -0
- package/src/components/prompt-input.tsx +1790 -0
- package/src/components/prompt-project-selector.tsx +593 -0
- package/src/components/prompt-workspace-selector.tsx +128 -0
- package/src/components/server/server-row-menu.tsx +96 -0
- package/src/components/server/server-row.tsx +127 -0
- package/src/components/session/index.ts +7 -0
- package/src/components/session/open-in-app-v2.tsx +98 -0
- package/src/components/session/open-in-app.tsx +229 -0
- package/src/components/session/session-context-breakdown.test.ts +61 -0
- package/src/components/session/session-context-breakdown.ts +132 -0
- package/src/components/session/session-context-format.ts +20 -0
- package/src/components/session/session-context-metrics.test.ts +99 -0
- package/src/components/session/session-context-metrics.ts +65 -0
- package/src/components/session/session-context-tab.tsx +343 -0
- package/src/components/session/session-header.tsx +568 -0
- package/src/components/session/session-new-design-view.tsx +16 -0
- package/src/components/session/session-new-view.tsx +91 -0
- package/src/components/session/session-sortable-tab-v2.tsx +74 -0
- package/src/components/session/session-sortable-tab.tsx +78 -0
- package/src/components/session/session-sortable-terminal-tab-v2.tsx +284 -0
- package/src/components/session/session-sortable-terminal-tab.tsx +193 -0
- package/src/components/session-context-usage.tsx +169 -0
- package/src/components/settings-dialog.tsx +43 -0
- package/src/components/settings-general.tsx +794 -0
- package/src/components/settings-keybinds.tsx +553 -0
- package/src/components/settings-list.tsx +5 -0
- package/src/components/settings-models.tsx +149 -0
- package/src/components/settings-providers.tsx +264 -0
- package/src/components/settings-server-picker.tsx +106 -0
- package/src/components/settings-servers.tsx +33 -0
- package/src/components/settings-v2/dialog-server-v2.tsx +134 -0
- package/src/components/settings-v2/dialog-settings-v2.tsx +98 -0
- package/src/components/settings-v2/general.tsx +722 -0
- package/src/components/settings-v2/index.tsx +1 -0
- package/src/components/settings-v2/interface-transition.stories.tsx +76 -0
- package/src/components/settings-v2/interface-transition.tsx +54 -0
- package/src/components/settings-v2/models.tsx +138 -0
- package/src/components/settings-v2/parts/list.tsx +6 -0
- package/src/components/settings-v2/parts/row.tsx +20 -0
- package/src/components/settings-v2/providers.tsx +264 -0
- package/src/components/settings-v2/servers.tsx +139 -0
- package/src/components/settings-v2/settings-v2.css +675 -0
- package/src/components/status-popover-body.tsx +514 -0
- package/src/components/status-popover-indicator.test.ts +36 -0
- package/src/components/status-popover-indicator.ts +20 -0
- package/src/components/status-popover.tsx +190 -0
- package/src/components/terminal.tsx +759 -0
- package/src/components/titlebar-history.test.ts +63 -0
- package/src/components/titlebar-history.ts +57 -0
- package/src/components/titlebar-session-events.test.ts +32 -0
- package/src/components/titlebar-session-events.ts +35 -0
- package/src/components/titlebar-tab-gesture.test.ts +33 -0
- package/src/components/titlebar-tab-gesture.ts +17 -0
- package/src/components/titlebar-tab-nav.css +100 -0
- package/src/components/titlebar-tab-nav.tsx +429 -0
- package/src/components/titlebar-tab-popover.css +127 -0
- package/src/components/titlebar-tab-popover.tsx +92 -0
- package/src/components/titlebar-tab-strip.tsx +333 -0
- package/src/components/titlebar.css +77 -0
- package/src/components/titlebar.tsx +776 -0
- package/src/components/ui/drawer.tsx +119 -0
- package/src/components/updater-action.test.ts +26 -0
- package/src/components/updater-action.ts +51 -0
- package/src/components/virtual-scroll-element.test.ts +18 -0
- package/src/components/virtual-scroll-element.ts +4 -0
- package/src/components/windows-app-menu.tsx +131 -0
- package/src/constants/file-picker.ts +89 -0
- package/src/context/closed-tabs.ts +40 -0
- package/src/context/command-keybind.test.ts +69 -0
- package/src/context/command.test.ts +55 -0
- package/src/context/command.tsx +467 -0
- package/src/context/comments.test.ts +188 -0
- package/src/context/comments.tsx +261 -0
- package/src/context/directory-sync.ts +155 -0
- package/src/context/file/content-cache.ts +88 -0
- package/src/context/file/path.test.ts +360 -0
- package/src/context/file/path.ts +151 -0
- package/src/context/file/tree-store.ts +174 -0
- package/src/context/file/types.ts +41 -0
- package/src/context/file/view-cache.ts +147 -0
- package/src/context/file/watcher.test.ts +149 -0
- package/src/context/file/watcher.ts +53 -0
- package/src/context/file-content-eviction-accounting.test.ts +65 -0
- package/src/context/file.tsx +303 -0
- package/src/context/global-sync/bootstrap.test.ts +231 -0
- package/src/context/global-sync/bootstrap.ts +536 -0
- package/src/context/global-sync/child-store.test.ts +276 -0
- package/src/context/global-sync/child-store.ts +396 -0
- package/src/context/global-sync/event-reducer.test.ts +616 -0
- package/src/context/global-sync/event-reducer.ts +478 -0
- package/src/context/global-sync/eviction.ts +28 -0
- package/src/context/global-sync/home-session-index.test.ts +154 -0
- package/src/context/global-sync/home-session-index.ts +174 -0
- package/src/context/global-sync/mcp.test.ts +54 -0
- package/src/context/global-sync/mcp.ts +19 -0
- package/src/context/global-sync/queue.test.ts +46 -0
- package/src/context/global-sync/queue.ts +87 -0
- package/src/context/global-sync/session-cache.test.ts +104 -0
- package/src/context/global-sync/session-cache.ts +62 -0
- package/src/context/global-sync/session-load.ts +33 -0
- package/src/context/global-sync/session-trim.test.ts +59 -0
- package/src/context/global-sync/session-trim.ts +57 -0
- package/src/context/global-sync/types.ts +135 -0
- package/src/context/global-sync/utils.test.ts +133 -0
- package/src/context/global-sync/utils.ts +171 -0
- package/src/context/global.tsx +161 -0
- package/src/context/highlights.tsx +233 -0
- package/src/context/language.tsx +243 -0
- package/src/context/layout-helpers.ts +38 -0
- package/src/context/layout-scroll.test.ts +64 -0
- package/src/context/layout-scroll.ts +126 -0
- package/src/context/layout-tabs.test.ts +82 -0
- package/src/context/layout-tabs.ts +103 -0
- package/src/context/layout.test.ts +69 -0
- package/src/context/layout.tsx +1075 -0
- package/src/context/local-agent.test.ts +29 -0
- package/src/context/local-agent.ts +7 -0
- package/src/context/local.tsx +417 -0
- package/src/context/mcp.ts +19 -0
- package/src/context/model-variant.test.ts +86 -0
- package/src/context/model-variant.ts +52 -0
- package/src/context/models.tsx +173 -0
- package/src/context/notification.tsx +478 -0
- package/src/context/permission-auto-respond.test.ts +125 -0
- package/src/context/permission-auto-respond.ts +60 -0
- package/src/context/permission.tsx +484 -0
- package/src/context/platform.tsx +142 -0
- package/src/context/prompt-state.test.ts +29 -0
- package/src/context/prompt-state.ts +266 -0
- package/src/context/prompt.tsx +170 -0
- package/src/context/sdk.tsx +17 -0
- package/src/context/server-sdk.test.ts +196 -0
- package/src/context/server-sdk.tsx +444 -0
- package/src/context/server-session-v2-reducer.test.ts +156 -0
- package/src/context/server-session-v2-reducer.ts +502 -0
- package/src/context/server-session.test.ts +1638 -0
- package/src/context/server-session.ts +1425 -0
- package/src/context/server-sync.test.ts +231 -0
- package/src/context/server-sync.tsx +753 -0
- package/src/context/server.test.ts +245 -0
- package/src/context/server.tsx +360 -0
- package/src/context/settings.test.ts +79 -0
- package/src/context/settings.tsx +531 -0
- package/src/context/sync-optimistic.test.ts +123 -0
- package/src/context/sync.tsx +119 -0
- package/src/context/tab-memory.ts +36 -0
- package/src/context/tabs.test.ts +107 -0
- package/src/context/tabs.tsx +387 -0
- package/src/context/terminal-title.ts +24 -0
- package/src/context/terminal.test.ts +91 -0
- package/src/context/terminal.tsx +546 -0
- package/src/custom-elements.d.ts +1 -0
- package/src/desktop-menu.test.ts +13 -0
- package/src/desktop-menu.ts +223 -0
- package/src/entry.tsx +182 -0
- package/src/env.d.ts +31 -0
- package/src/hooks/provider-catalog.test.ts +59 -0
- package/src/hooks/provider-catalog.ts +27 -0
- package/src/hooks/use-providers.ts +71 -0
- package/src/i18n/ar.ts +1008 -0
- package/src/i18n/br.ts +1024 -0
- package/src/i18n/bs.ts +1100 -0
- package/src/i18n/da.ts +1092 -0
- package/src/i18n/de.ts +1037 -0
- package/src/i18n/en.ts +1102 -0
- package/src/i18n/es.ts +1108 -0
- package/src/i18n/fr.ts +1037 -0
- package/src/i18n/ja.ts +1016 -0
- package/src/i18n/ko.ts +1015 -0
- package/src/i18n/no.ts +1111 -0
- package/src/i18n/parity.test.ts +118 -0
- package/src/i18n/pl.ts +1024 -0
- package/src/i18n/ru.ts +1103 -0
- package/src/i18n/th.ts +1087 -0
- package/src/i18n/tr.ts +1106 -0
- package/src/i18n/uk.ts +1105 -0
- package/src/i18n/zh.ts +1078 -0
- package/src/i18n/zht.ts +1074 -0
- package/src/index.css +330 -0
- package/src/index.ts +30 -0
- package/src/pages/directory-layout.tsx +122 -0
- package/src/pages/error-description.test.ts +17 -0
- package/src/pages/error-description.ts +11 -0
- package/src/pages/error.tsx +372 -0
- package/src/pages/home/home-controller.ts +108 -0
- package/src/pages/home/home-projects-controller.tsx +128 -0
- package/src/pages/home/home-projects-view.tsx +615 -0
- package/src/pages/home/home-projects.tsx +40 -0
- package/src/pages/home/home-scroll-controller.ts +145 -0
- package/src/pages/home/home-session-search-controller.ts +114 -0
- package/src/pages/home/home-sessions-controller.tsx +314 -0
- package/src/pages/home/home-sessions-view.tsx +550 -0
- package/src/pages/home/home-sessions.tsx +48 -0
- package/src/pages/home/legacy-home.tsx +142 -0
- package/src/pages/home-session-archive.test.ts +51 -0
- package/src/pages/home-session-archive.ts +27 -0
- package/src/pages/home-session-open.test.ts +31 -0
- package/src/pages/home-session-open.ts +14 -0
- package/src/pages/home.tsx +50 -0
- package/src/pages/layout/deep-links.ts +50 -0
- package/src/pages/layout/helpers.test.ts +321 -0
- package/src/pages/layout/helpers.ts +150 -0
- package/src/pages/layout/inline-editor.tsx +126 -0
- package/src/pages/layout/project-avatar-state.ts +49 -0
- package/src/pages/layout/session-tab-avatar.tsx +60 -0
- package/src/pages/layout/sidebar-items.tsx +335 -0
- package/src/pages/layout/sidebar-project.tsx +377 -0
- package/src/pages/layout/sidebar-shell.tsx +125 -0
- package/src/pages/layout/sidebar-workspace.tsx +488 -0
- package/src/pages/layout-new.tsx +53 -0
- package/src/pages/layout.tsx +2441 -0
- package/src/pages/new-session.tsx +282 -0
- package/src/pages/session/composer/index.ts +4 -0
- package/src/pages/session/composer/prompt-model-selection.ts +133 -0
- package/src/pages/session/composer/session-composer-controls.ts +129 -0
- package/src/pages/session/composer/session-composer-region-controller.ts +145 -0
- package/src/pages/session/composer/session-composer-region.tsx +168 -0
- package/src/pages/session/composer/session-composer-state.test.ts +138 -0
- package/src/pages/session/composer/session-composer-state.ts +204 -0
- package/src/pages/session/composer/session-followup-dock.tsx +109 -0
- package/src/pages/session/composer/session-permission-dock.tsx +74 -0
- package/src/pages/session/composer/session-question-dock.tsx +640 -0
- package/src/pages/session/composer/session-request-tree.ts +52 -0
- package/src/pages/session/composer/session-revert-dock.stories.tsx +124 -0
- package/src/pages/session/composer/session-revert-dock.tsx +188 -0
- package/src/pages/session/composer/session-todo-dock.tsx +277 -0
- package/src/pages/session/composer/todo-panel-motion.stories.tsx +622 -0
- package/src/pages/session/file-tab-scroll.test.ts +40 -0
- package/src/pages/session/file-tab-scroll.ts +67 -0
- package/src/pages/session/file-tabs.tsx +800 -0
- package/src/pages/session/handoff.ts +36 -0
- package/src/pages/session/helpers.test.ts +214 -0
- package/src/pages/session/helpers.ts +209 -0
- package/src/pages/session/message-gesture.test.ts +62 -0
- package/src/pages/session/message-gesture.ts +21 -0
- package/src/pages/session/message-id-from-hash.ts +6 -0
- package/src/pages/session/new-session-layout.ts +2 -0
- package/src/pages/session/review-tab.tsx +172 -0
- package/src/pages/session/session-layout.ts +30 -0
- package/src/pages/session/session-lineage.ts +69 -0
- package/src/pages/session/session-model-helpers.test.ts +151 -0
- package/src/pages/session/session-model-helpers.ts +63 -0
- package/src/pages/session/session-ownership.ts +37 -0
- package/src/pages/session/session-panel-layout.test.ts +19 -0
- package/src/pages/session/session-panel-layout.ts +6 -0
- package/src/pages/session/session-panel-width.test.ts +52 -0
- package/src/pages/session/session-panel-width.ts +19 -0
- package/src/pages/session/session-side-panel.tsx +865 -0
- package/src/pages/session/terminal-label.ts +16 -0
- package/src/pages/session/terminal-panel-v2.tsx +352 -0
- package/src/pages/session/terminal-panel.test.ts +25 -0
- package/src/pages/session/terminal-panel.tsx +343 -0
- package/src/pages/session/timeline/measure.test.ts +30 -0
- package/src/pages/session/timeline/measure.ts +5 -0
- package/src/pages/session/timeline/message-timeline.tsx +1864 -0
- package/src/pages/session/timeline/model.test.ts +95 -0
- package/src/pages/session/timeline/model.ts +128 -0
- package/src/pages/session/timeline/observe-element-offset.test.ts +199 -0
- package/src/pages/session/timeline/observe-element-offset.ts +73 -0
- package/src/pages/session/timeline/projection.test.ts +96 -0
- package/src/pages/session/timeline/projection.ts +81 -0
- package/src/pages/session/timeline/row-reconciliation.ts +56 -0
- package/src/pages/session/timeline/rows-current.test.ts +170 -0
- package/src/pages/session/timeline/rows.ts +344 -0
- package/src/pages/session/timeline/summary-diffs.test.ts +42 -0
- package/src/pages/session/timeline/summary-diffs.ts +20 -0
- package/src/pages/session/timeline/timeline-row.ts +80 -0
- package/src/pages/session/timeline/virtual-items.ts +3 -0
- package/src/pages/session/usage-exceeded-dialogs.tsx +104 -0
- package/src/pages/session/use-composer-commands.tsx +83 -0
- package/src/pages/session/use-session-commands.tsx +606 -0
- package/src/pages/session/use-session-hash-scroll.test.ts +16 -0
- package/src/pages/session/use-session-hash-scroll.ts +202 -0
- package/src/pages/session/v2/review-diff-kinds.test.ts +70 -0
- package/src/pages/session/v2/review-diff-kinds.ts +62 -0
- package/src/pages/session/v2/review-panel-v2-state.ts +41 -0
- package/src/pages/session/v2/review-panel-v2.tsx +255 -0
- package/src/pages/session/v2/session-file-browser-tab.tsx +181 -0
- package/src/pages/session/v2/session-file-list-v2.tsx +166 -0
- package/src/pages/session.tsx +2385 -0
- package/src/sst-env.d.ts +12 -0
- package/src/theme-preload.test.ts +46 -0
- package/src/updater.ts +17 -0
- package/src/utils/agent.ts +44 -0
- package/src/utils/aim.ts +138 -0
- package/src/utils/base64.ts +10 -0
- package/src/utils/comment-note.ts +88 -0
- package/src/utils/diffs.test.ts +75 -0
- package/src/utils/diffs.ts +50 -0
- package/src/utils/file-manager.ts +24 -0
- package/src/utils/id.ts +93 -0
- package/src/utils/notification-click.test.ts +27 -0
- package/src/utils/notification-click.ts +13 -0
- package/src/utils/path-key.ts +24 -0
- package/src/utils/persist.test.ts +211 -0
- package/src/utils/persist.ts +661 -0
- package/src/utils/prompt.test.ts +44 -0
- package/src/utils/prompt.ts +203 -0
- package/src/utils/refcount.test.ts +49 -0
- package/src/utils/refcount.ts +32 -0
- package/src/utils/runtime-adapters.test.ts +64 -0
- package/src/utils/runtime-adapters.ts +39 -0
- package/src/utils/same.ts +6 -0
- package/src/utils/scoped-cache.test.ts +69 -0
- package/src/utils/scoped-cache.ts +104 -0
- package/src/utils/search-keydown.ts +116 -0
- package/src/utils/server-compat.test.ts +193 -0
- package/src/utils/server-compat.ts +512 -0
- package/src/utils/server-errors.test.ts +175 -0
- package/src/utils/server-errors.ts +109 -0
- package/src/utils/server-health.test.ts +178 -0
- package/src/utils/server-health.ts +172 -0
- package/src/utils/server-protocol.test.ts +40 -0
- package/src/utils/server-protocol.ts +35 -0
- package/src/utils/server-scope.test.ts +61 -0
- package/src/utils/server-scope.ts +73 -0
- package/src/utils/server.test.ts +23 -0
- package/src/utils/server.ts +62 -0
- package/src/utils/session-message.test.ts +214 -0
- package/src/utils/session-message.ts +355 -0
- package/src/utils/session-route.test.ts +71 -0
- package/src/utils/session-route.ts +39 -0
- package/src/utils/session-title.ts +7 -0
- package/src/utils/session.test.ts +94 -0
- package/src/utils/session.ts +37 -0
- package/src/utils/solid-dnd.tsx +49 -0
- package/src/utils/sound.ts +102 -0
- package/src/utils/terminal-websocket-url.test.ts +80 -0
- package/src/utils/terminal-websocket-url.ts +35 -0
- package/src/utils/terminal-writer.test.ts +64 -0
- package/src/utils/terminal-writer.ts +65 -0
- package/src/utils/time.ts +22 -0
- package/src/utils/toast.tsx +34 -0
- package/src/utils/uuid.test.ts +78 -0
- package/src/utils/uuid.ts +12 -0
- package/src/utils/worktree.test.ts +58 -0
- package/src/utils/worktree.ts +76 -0
- package/src/wsl/add-server-probes.ts +57 -0
- package/src/wsl/context.tsx +36 -0
- package/src/wsl/dialog-add-server.tsx +463 -0
- package/src/wsl/dialog-add-wsl-server.css +392 -0
- package/src/wsl/settings-model.test.ts +231 -0
- package/src/wsl/settings-model.ts +341 -0
- package/src/wsl/settings.tsx +173 -0
- package/src/wsl/types.ts +85 -0
- package/sst-env.d.ts +10 -0
- package/test-browser/command-palette.test.ts +78 -0
- package/test-browser/motion-spring.test.ts +20 -0
- package/test-browser/prompt-attachments.test.ts +90 -0
- package/test-browser/prompt-persistence.test.ts +71 -0
- package/test-browser/prompt-scope.test.ts +14 -0
- package/test-browser/prompt-submission-state.test.ts +73 -0
- package/test-browser/prompt-transient-state.test.ts +38 -0
- package/test-browser/review-panel-v2-state.test.ts +65 -0
- package/test-browser/session-lineage.test.ts +231 -0
- package/test-browser/session-ownership.test.ts +45 -0
- package/test-browser/solid-virtual.test.ts +112 -0
- package/test-browser/toast-owner.test.ts +26 -0
- package/tsconfig.json +26 -0
- package/vendor/opencode-ai-client-1.17.13.tgz +0 -0
- package/vite.config.ts +33 -0
- package/vite.js +48 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { createPathHelpers, stripQueryAndHash, unquoteGitPath, encodeFilePath } from "./path"
|
|
3
|
+
|
|
4
|
+
describe("file path helpers", () => {
|
|
5
|
+
test("normalizes file inputs against workspace root", () => {
|
|
6
|
+
const path = createPathHelpers(() => "/repo")
|
|
7
|
+
expect(path.normalize("file:///repo/src/app.ts?x=1#h")).toBe("src/app.ts")
|
|
8
|
+
expect(path.normalize("/repo/src/app.ts")).toBe("src/app.ts")
|
|
9
|
+
expect(path.normalize("./src/app.ts")).toBe("src/app.ts")
|
|
10
|
+
expect(path.normalizeDir("src/components///")).toBe("src/components")
|
|
11
|
+
expect(path.tab("src/app.ts")).toBe("file://src/app.ts")
|
|
12
|
+
expect(path.pathFromTab("file://src/app.ts")).toBe("src/app.ts")
|
|
13
|
+
expect(path.pathFromTab("other://src/app.ts")).toBeUndefined()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test("normalizes Windows absolute paths with mixed separators", () => {
|
|
17
|
+
const path = createPathHelpers(() => "C:\\repo")
|
|
18
|
+
expect(path.normalize("C:\\repo\\src\\app.ts")).toBe("src\\app.ts")
|
|
19
|
+
expect(path.normalize("C:/repo/src/app.ts")).toBe("src/app.ts")
|
|
20
|
+
expect(path.normalize("file://C:/repo/src/app.ts")).toBe("src/app.ts")
|
|
21
|
+
expect(path.normalize("c:\\repo\\src\\app.ts")).toBe("src\\app.ts")
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test("keeps query/hash stripping behavior stable", () => {
|
|
25
|
+
expect(stripQueryAndHash("a/b.ts#L12?x=1")).toBe("a/b.ts")
|
|
26
|
+
expect(stripQueryAndHash("a/b.ts?x=1#L12")).toBe("a/b.ts")
|
|
27
|
+
expect(stripQueryAndHash("a/b.ts")).toBe("a/b.ts")
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test("unquotes git escaped octal path strings", () => {
|
|
31
|
+
expect(unquoteGitPath('"a/\\303\\251.txt"')).toBe("a/\u00e9.txt")
|
|
32
|
+
expect(unquoteGitPath('"plain\\nname"')).toBe("plain\nname")
|
|
33
|
+
expect(unquoteGitPath("a/b/c.ts")).toBe("a/b/c.ts")
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe("encodeFilePath", () => {
|
|
38
|
+
describe("Linux/Unix paths", () => {
|
|
39
|
+
test("should handle Linux absolute path", () => {
|
|
40
|
+
const linuxPath = "/home/user/project/README.md"
|
|
41
|
+
const result = encodeFilePath(linuxPath)
|
|
42
|
+
const fileUrl = `file://${result}`
|
|
43
|
+
|
|
44
|
+
// Should create a valid URL
|
|
45
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
46
|
+
expect(result).toBe("/home/user/project/README.md")
|
|
47
|
+
|
|
48
|
+
const url = new URL(fileUrl)
|
|
49
|
+
expect(url.protocol).toBe("file:")
|
|
50
|
+
expect(url.pathname).toBe("/home/user/project/README.md")
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test("should handle Linux path with special characters", () => {
|
|
54
|
+
const linuxPath = "/home/user/file#name with spaces.txt"
|
|
55
|
+
const result = encodeFilePath(linuxPath)
|
|
56
|
+
const fileUrl = `file://${result}`
|
|
57
|
+
|
|
58
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
59
|
+
expect(result).toBe("/home/user/file%23name%20with%20spaces.txt")
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test("should handle Linux relative path", () => {
|
|
63
|
+
const relativePath = "src/components/App.tsx"
|
|
64
|
+
const result = encodeFilePath(relativePath)
|
|
65
|
+
|
|
66
|
+
expect(result).toBe("src/components/App.tsx")
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test("should handle Linux root directory", () => {
|
|
70
|
+
const result = encodeFilePath("/")
|
|
71
|
+
expect(result).toBe("/")
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test("should handle Linux path with all special chars", () => {
|
|
75
|
+
const path = "/path/to/file#with?special%chars&more.txt"
|
|
76
|
+
const result = encodeFilePath(path)
|
|
77
|
+
const fileUrl = `file://${result}`
|
|
78
|
+
|
|
79
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
80
|
+
expect(result).toContain("%23") // #
|
|
81
|
+
expect(result).toContain("%3F") // ?
|
|
82
|
+
expect(result).toContain("%25") // %
|
|
83
|
+
expect(result).toContain("%26") // &
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe("macOS paths", () => {
|
|
88
|
+
test("should handle macOS absolute path", () => {
|
|
89
|
+
const macPath = "/Users/kelvin/Projects/opencode/README.md"
|
|
90
|
+
const result = encodeFilePath(macPath)
|
|
91
|
+
const fileUrl = `file://${result}`
|
|
92
|
+
|
|
93
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
94
|
+
expect(result).toBe("/Users/kelvin/Projects/opencode/README.md")
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test("should handle macOS path with spaces", () => {
|
|
98
|
+
const macPath = "/Users/kelvin/My Documents/file.txt"
|
|
99
|
+
const result = encodeFilePath(macPath)
|
|
100
|
+
const fileUrl = `file://${result}`
|
|
101
|
+
|
|
102
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
103
|
+
expect(result).toContain("My%20Documents")
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe("Windows paths", () => {
|
|
108
|
+
test("should handle Windows absolute path with backslashes", () => {
|
|
109
|
+
const windowsPath = "D:\\dev\\projects\\neurocode\\README.bs.md"
|
|
110
|
+
const result = encodeFilePath(windowsPath)
|
|
111
|
+
const fileUrl = `file://${result}`
|
|
112
|
+
|
|
113
|
+
// Should create a valid, parseable URL
|
|
114
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
115
|
+
|
|
116
|
+
const url = new URL(fileUrl)
|
|
117
|
+
expect(url.protocol).toBe("file:")
|
|
118
|
+
expect(url.pathname).toContain("README.bs.md")
|
|
119
|
+
expect(result).toBe("/D:/dev/projects/opencode/README.bs.md")
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test("should handle mixed separator path (Windows + Unix)", () => {
|
|
123
|
+
// This is what happens in build-request-parts.ts when concatenating paths
|
|
124
|
+
const mixedPath = "D:\\dev\\projects\\opencode/README.bs.md"
|
|
125
|
+
const result = encodeFilePath(mixedPath)
|
|
126
|
+
const fileUrl = `file://${result}`
|
|
127
|
+
|
|
128
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
129
|
+
expect(result).toBe("/D:/dev/projects/opencode/README.bs.md")
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test("should handle Windows path with spaces", () => {
|
|
133
|
+
const windowsPath = "C:\\Program Files\\MyApp\\file with spaces.txt"
|
|
134
|
+
const result = encodeFilePath(windowsPath)
|
|
135
|
+
const fileUrl = `file://${result}`
|
|
136
|
+
|
|
137
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
138
|
+
expect(result).toContain("Program%20Files")
|
|
139
|
+
expect(result).toContain("file%20with%20spaces.txt")
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test("should handle Windows path with special chars in filename", () => {
|
|
143
|
+
const windowsPath = "D:\\projects\\file#name with ?marks.txt"
|
|
144
|
+
const result = encodeFilePath(windowsPath)
|
|
145
|
+
const fileUrl = `file://${result}`
|
|
146
|
+
|
|
147
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
148
|
+
expect(result).toContain("file%23name%20with%20%3Fmarks.txt")
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test("should handle Windows root directory", () => {
|
|
152
|
+
const windowsPath = "C:\\"
|
|
153
|
+
const result = encodeFilePath(windowsPath)
|
|
154
|
+
const fileUrl = `file://${result}`
|
|
155
|
+
|
|
156
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
157
|
+
expect(result).toBe("/C:/")
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test("should handle Windows relative path with backslashes", () => {
|
|
161
|
+
const windowsPath = "src\\components\\App.tsx"
|
|
162
|
+
const result = encodeFilePath(windowsPath)
|
|
163
|
+
|
|
164
|
+
// Relative paths shouldn't get the leading slash
|
|
165
|
+
expect(result).toBe("src/components/App.tsx")
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test("should NOT create invalid URL like the bug report", () => {
|
|
169
|
+
// This is the exact scenario from bug report by @alexyaroshuk
|
|
170
|
+
const windowsPath = "D:\\dev\\projects\\neurocode\\README.bs.md"
|
|
171
|
+
const result = encodeFilePath(windowsPath)
|
|
172
|
+
const fileUrl = `file://${result}`
|
|
173
|
+
|
|
174
|
+
// The bug was creating: file://D%3A%5Cdev%5Cprojects%5Copencode/README.bs.md
|
|
175
|
+
expect(result).not.toContain("%5C") // Should not have encoded backslashes
|
|
176
|
+
expect(result).not.toBe("D%3A%5Cdev%5Cprojects%5Copencode/README.bs.md")
|
|
177
|
+
|
|
178
|
+
// Should be valid
|
|
179
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
test("should handle lowercase drive letters", () => {
|
|
183
|
+
const windowsPath = "c:\\users\\test\\file.txt"
|
|
184
|
+
const result = encodeFilePath(windowsPath)
|
|
185
|
+
const fileUrl = `file://${result}`
|
|
186
|
+
|
|
187
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
188
|
+
expect(result).toBe("/c:/users/test/file.txt")
|
|
189
|
+
})
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
describe("Cross-platform compatibility", () => {
|
|
193
|
+
test("should preserve Unix paths unchanged (except encoding)", () => {
|
|
194
|
+
const unixPath = "/usr/local/bin/app"
|
|
195
|
+
const result = encodeFilePath(unixPath)
|
|
196
|
+
expect(result).toBe("/usr/local/bin/app")
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
test("should normalize Windows paths for cross-platform use", () => {
|
|
200
|
+
const windowsPath = "C:\\Users\\test\\file.txt"
|
|
201
|
+
const result = encodeFilePath(windowsPath)
|
|
202
|
+
// Should convert to forward slashes and add leading /
|
|
203
|
+
expect(result).not.toContain("\\")
|
|
204
|
+
expect(result).toMatch(/^\/[A-Za-z]:\//)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
test("should handle relative paths the same on all platforms", () => {
|
|
208
|
+
const unixRelative = "src/app.ts"
|
|
209
|
+
const windowsRelative = "src\\app.ts"
|
|
210
|
+
|
|
211
|
+
const unixResult = encodeFilePath(unixRelative)
|
|
212
|
+
const windowsResult = encodeFilePath(windowsRelative)
|
|
213
|
+
|
|
214
|
+
// Both should normalize to forward slashes
|
|
215
|
+
expect(unixResult).toBe("src/app.ts")
|
|
216
|
+
expect(windowsResult).toBe("src/app.ts")
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
describe("Edge cases", () => {
|
|
221
|
+
test("should handle empty path", () => {
|
|
222
|
+
const result = encodeFilePath("")
|
|
223
|
+
expect(result).toBe("")
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
test("should handle path with multiple consecutive slashes", () => {
|
|
227
|
+
const result = encodeFilePath("//path//to///file.txt")
|
|
228
|
+
// Multiple slashes should be preserved (backend handles normalization)
|
|
229
|
+
expect(result).toBe("//path//to///file.txt")
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
test("should encode Unicode characters", () => {
|
|
233
|
+
const unicodePath = "/home/user/文档/README.md"
|
|
234
|
+
const result = encodeFilePath(unicodePath)
|
|
235
|
+
const fileUrl = `file://${result}`
|
|
236
|
+
|
|
237
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
238
|
+
// Unicode should be encoded
|
|
239
|
+
expect(result).toContain("%E6%96%87%E6%A1%A3")
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
test("should handle already normalized Windows path", () => {
|
|
243
|
+
// Path that's already been normalized (has / before drive letter)
|
|
244
|
+
const alreadyNormalized = "/D:/path/file.txt"
|
|
245
|
+
const result = encodeFilePath(alreadyNormalized)
|
|
246
|
+
|
|
247
|
+
// Should not add another leading slash
|
|
248
|
+
expect(result).toBe("/D:/path/file.txt")
|
|
249
|
+
expect(result).not.toContain("//D")
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
test("should handle just drive letter", () => {
|
|
253
|
+
const justDrive = "D:"
|
|
254
|
+
const result = encodeFilePath(justDrive)
|
|
255
|
+
const fileUrl = `file://${result}`
|
|
256
|
+
|
|
257
|
+
expect(result).toBe("/D:")
|
|
258
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
test("should handle Windows path with trailing backslash", () => {
|
|
262
|
+
const trailingBackslash = "C:\\Users\\test\\"
|
|
263
|
+
const result = encodeFilePath(trailingBackslash)
|
|
264
|
+
const fileUrl = `file://${result}`
|
|
265
|
+
|
|
266
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
267
|
+
expect(result).toBe("/C:/Users/test/")
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
test("should handle very long paths", () => {
|
|
271
|
+
const longPath = "C:\\Users\\test\\" + "verylongdirectoryname\\".repeat(20) + "file.txt"
|
|
272
|
+
const result = encodeFilePath(longPath)
|
|
273
|
+
const fileUrl = `file://${result}`
|
|
274
|
+
|
|
275
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
276
|
+
expect(result).not.toContain("\\")
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
test("should handle paths with dots", () => {
|
|
280
|
+
const pathWithDots = "C:\\Users\\..\\test\\.\\file.txt"
|
|
281
|
+
const result = encodeFilePath(pathWithDots)
|
|
282
|
+
const fileUrl = `file://${result}`
|
|
283
|
+
|
|
284
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
285
|
+
// Dots should be preserved (backend normalizes)
|
|
286
|
+
expect(result).toContain("..")
|
|
287
|
+
expect(result).toContain("/./")
|
|
288
|
+
})
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
describe("Regression tests for PR #12424", () => {
|
|
292
|
+
test("should handle file with # in name", () => {
|
|
293
|
+
const path = "/path/to/file#name.txt"
|
|
294
|
+
const result = encodeFilePath(path)
|
|
295
|
+
const fileUrl = `file://${result}`
|
|
296
|
+
|
|
297
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
298
|
+
expect(result).toBe("/path/to/file%23name.txt")
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
test("should handle file with ? in name", () => {
|
|
302
|
+
const path = "/path/to/file?name.txt"
|
|
303
|
+
const result = encodeFilePath(path)
|
|
304
|
+
const fileUrl = `file://${result}`
|
|
305
|
+
|
|
306
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
307
|
+
expect(result).toBe("/path/to/file%3Fname.txt")
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
test("should handle file with % in name", () => {
|
|
311
|
+
const path = "/path/to/file%name.txt"
|
|
312
|
+
const result = encodeFilePath(path)
|
|
313
|
+
const fileUrl = `file://${result}`
|
|
314
|
+
|
|
315
|
+
expect(() => new URL(fileUrl)).not.toThrow()
|
|
316
|
+
expect(result).toBe("/path/to/file%25name.txt")
|
|
317
|
+
})
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
describe("Integration with file:// URL construction", () => {
|
|
321
|
+
test("should work with query parameters (Linux)", () => {
|
|
322
|
+
const path = "/home/user/file.txt"
|
|
323
|
+
const encoded = encodeFilePath(path)
|
|
324
|
+
const fileUrl = `file://${encoded}?start=10&end=20`
|
|
325
|
+
|
|
326
|
+
const url = new URL(fileUrl)
|
|
327
|
+
expect(url.searchParams.get("start")).toBe("10")
|
|
328
|
+
expect(url.searchParams.get("end")).toBe("20")
|
|
329
|
+
expect(url.pathname).toBe("/home/user/file.txt")
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
test("should work with query parameters (Windows)", () => {
|
|
333
|
+
const path = "C:\\Users\\test\\file.txt"
|
|
334
|
+
const encoded = encodeFilePath(path)
|
|
335
|
+
const fileUrl = `file://${encoded}?start=10&end=20`
|
|
336
|
+
|
|
337
|
+
const url = new URL(fileUrl)
|
|
338
|
+
expect(url.searchParams.get("start")).toBe("10")
|
|
339
|
+
expect(url.searchParams.get("end")).toBe("20")
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
test("should parse correctly in URL constructor (Linux)", () => {
|
|
343
|
+
const path = "/var/log/app.log"
|
|
344
|
+
const fileUrl = `file://${encodeFilePath(path)}`
|
|
345
|
+
const url = new URL(fileUrl)
|
|
346
|
+
|
|
347
|
+
expect(url.protocol).toBe("file:")
|
|
348
|
+
expect(url.pathname).toBe("/var/log/app.log")
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
test("should parse correctly in URL constructor (Windows)", () => {
|
|
352
|
+
const path = "D:\\logs\\app.log"
|
|
353
|
+
const fileUrl = `file://${encodeFilePath(path)}`
|
|
354
|
+
const url = new URL(fileUrl)
|
|
355
|
+
|
|
356
|
+
expect(url.protocol).toBe("file:")
|
|
357
|
+
expect(url.pathname).toContain("app.log")
|
|
358
|
+
})
|
|
359
|
+
})
|
|
360
|
+
})
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
export function stripFileProtocol(input: string) {
|
|
2
|
+
if (!input.startsWith("file://")) return input
|
|
3
|
+
return input.slice("file://".length)
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function stripQueryAndHash(input: string) {
|
|
7
|
+
const hashIndex = input.indexOf("#")
|
|
8
|
+
const queryIndex = input.indexOf("?")
|
|
9
|
+
|
|
10
|
+
if (hashIndex !== -1 && queryIndex !== -1) {
|
|
11
|
+
return input.slice(0, Math.min(hashIndex, queryIndex))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (hashIndex !== -1) return input.slice(0, hashIndex)
|
|
15
|
+
if (queryIndex !== -1) return input.slice(0, queryIndex)
|
|
16
|
+
return input
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function unquoteGitPath(input: string) {
|
|
20
|
+
if (!input.startsWith('"')) return input
|
|
21
|
+
if (!input.endsWith('"')) return input
|
|
22
|
+
const body = input.slice(1, -1)
|
|
23
|
+
const bytes: number[] = []
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < body.length; i++) {
|
|
26
|
+
const char = body[i]!
|
|
27
|
+
if (char !== "\\") {
|
|
28
|
+
bytes.push(char.charCodeAt(0))
|
|
29
|
+
continue
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const next = body[i + 1]
|
|
33
|
+
if (!next) {
|
|
34
|
+
bytes.push("\\".charCodeAt(0))
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (next >= "0" && next <= "7") {
|
|
39
|
+
const chunk = body.slice(i + 1, i + 4)
|
|
40
|
+
const match = chunk.match(/^[0-7]{1,3}/)
|
|
41
|
+
if (!match) {
|
|
42
|
+
bytes.push(next.charCodeAt(0))
|
|
43
|
+
i++
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
bytes.push(parseInt(match[0], 8))
|
|
47
|
+
i += match[0].length
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const escaped =
|
|
52
|
+
next === "n"
|
|
53
|
+
? "\n"
|
|
54
|
+
: next === "r"
|
|
55
|
+
? "\r"
|
|
56
|
+
: next === "t"
|
|
57
|
+
? "\t"
|
|
58
|
+
: next === "b"
|
|
59
|
+
? "\b"
|
|
60
|
+
: next === "f"
|
|
61
|
+
? "\f"
|
|
62
|
+
: next === "v"
|
|
63
|
+
? "\v"
|
|
64
|
+
: next === "\\" || next === '"'
|
|
65
|
+
? next
|
|
66
|
+
: undefined
|
|
67
|
+
|
|
68
|
+
bytes.push((escaped ?? next).charCodeAt(0))
|
|
69
|
+
i++
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return new TextDecoder().decode(new Uint8Array(bytes))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function decodeFilePath(input: string) {
|
|
76
|
+
try {
|
|
77
|
+
return decodeURIComponent(input)
|
|
78
|
+
} catch {
|
|
79
|
+
return input
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function encodeFilePath(filepath: string): string {
|
|
84
|
+
// Normalize Windows paths: convert backslashes to forward slashes
|
|
85
|
+
let normalized = filepath.replace(/\\/g, "/")
|
|
86
|
+
|
|
87
|
+
// Handle Windows absolute paths (D:/path -> /D:/path for proper file:// URLs)
|
|
88
|
+
if (/^[A-Za-z]:/.test(normalized)) {
|
|
89
|
+
normalized = "/" + normalized
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Encode each path segment (preserving forward slashes as path separators)
|
|
93
|
+
// Keep the colon in Windows drive letters (`/C:/...`) so downstream file URL parsers
|
|
94
|
+
// can reliably detect drives.
|
|
95
|
+
return normalized
|
|
96
|
+
.split("/")
|
|
97
|
+
.map((segment, index) => {
|
|
98
|
+
if (index === 1 && /^[A-Za-z]:$/.test(segment)) return segment
|
|
99
|
+
return encodeURIComponent(segment)
|
|
100
|
+
})
|
|
101
|
+
.join("/")
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function createPathHelpers(scope: () => string) {
|
|
105
|
+
const normalize = (input: string) => {
|
|
106
|
+
const root = scope()
|
|
107
|
+
|
|
108
|
+
let path = unquoteGitPath(decodeFilePath(stripQueryAndHash(stripFileProtocol(input))))
|
|
109
|
+
|
|
110
|
+
// Separator-agnostic prefix stripping for Cygwin/native Windows compatibility
|
|
111
|
+
// Only case-insensitive on Windows (drive letter or UNC paths)
|
|
112
|
+
const windows = /^[A-Za-z]:/.test(root) || root.startsWith("\\\\")
|
|
113
|
+
const canonRoot = windows ? root.replace(/\\/g, "/").toLowerCase() : root.replace(/\\/g, "/")
|
|
114
|
+
const canonPath = windows ? path.replace(/\\/g, "/").toLowerCase() : path.replace(/\\/g, "/")
|
|
115
|
+
if (
|
|
116
|
+
canonPath.startsWith(canonRoot) &&
|
|
117
|
+
(canonRoot.endsWith("/") || canonPath === canonRoot || canonPath[canonRoot.length] === "/")
|
|
118
|
+
) {
|
|
119
|
+
// Slice from original path to preserve native separators
|
|
120
|
+
path = path.slice(root.length)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (path.startsWith("./") || path.startsWith(".\\")) {
|
|
124
|
+
path = path.slice(2)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (path.startsWith("/") || path.startsWith("\\")) {
|
|
128
|
+
path = path.slice(1)
|
|
129
|
+
}
|
|
130
|
+
return path
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const tab = (input: string) => {
|
|
134
|
+
const path = normalize(input)
|
|
135
|
+
return `file://${encodeFilePath(path)}`
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const pathFromTab = (tabValue: string) => {
|
|
139
|
+
if (!tabValue.startsWith("file://")) return
|
|
140
|
+
return normalize(tabValue)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const normalizeDir = (input: string) => normalize(input).replace(/\/+$/, "")
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
normalize,
|
|
147
|
+
tab,
|
|
148
|
+
pathFromTab,
|
|
149
|
+
normalizeDir,
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { createStore, produce, reconcile } from "solid-js/store"
|
|
2
|
+
import type { FileNode } from "@neurocode-ai/sdk/v2"
|
|
3
|
+
|
|
4
|
+
type DirectoryState = {
|
|
5
|
+
expanded: boolean
|
|
6
|
+
loaded?: boolean
|
|
7
|
+
loading?: boolean
|
|
8
|
+
error?: string
|
|
9
|
+
children?: string[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type TreeStoreOptions = {
|
|
13
|
+
scope: () => string
|
|
14
|
+
normalizeDir: (input: string) => string
|
|
15
|
+
list: (input: string) => Promise<FileNode[]>
|
|
16
|
+
onError: (message: string) => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function createFileTreeStore(options: TreeStoreOptions) {
|
|
20
|
+
const [tree, setTree] = createStore<{
|
|
21
|
+
node: Record<string, FileNode>
|
|
22
|
+
dir: Record<string, DirectoryState>
|
|
23
|
+
}>({
|
|
24
|
+
node: {},
|
|
25
|
+
dir: { "": { expanded: true } },
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const inflight = new Map<string, Promise<void>>()
|
|
29
|
+
|
|
30
|
+
const reset = () => {
|
|
31
|
+
inflight.clear()
|
|
32
|
+
setTree("node", reconcile({}))
|
|
33
|
+
setTree("dir", reconcile({}))
|
|
34
|
+
setTree("dir", "", { expanded: true })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const ensureDir = (path: string) => {
|
|
38
|
+
if (tree.dir[path]) return
|
|
39
|
+
setTree("dir", path, { expanded: false })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const listDir = (input: string, opts?: { force?: boolean }) => {
|
|
43
|
+
const dir = options.normalizeDir(input)
|
|
44
|
+
ensureDir(dir)
|
|
45
|
+
|
|
46
|
+
const current = tree.dir[dir]
|
|
47
|
+
if (!opts?.force && current?.loaded) return Promise.resolve()
|
|
48
|
+
|
|
49
|
+
const pending = inflight.get(dir)
|
|
50
|
+
if (pending) return pending
|
|
51
|
+
|
|
52
|
+
setTree(
|
|
53
|
+
"dir",
|
|
54
|
+
dir,
|
|
55
|
+
produce((draft) => {
|
|
56
|
+
draft.loading = true
|
|
57
|
+
draft.error = undefined
|
|
58
|
+
}),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
const directory = options.scope()
|
|
62
|
+
|
|
63
|
+
const promise = options
|
|
64
|
+
.list(dir)
|
|
65
|
+
.then((nodes) => {
|
|
66
|
+
if (options.scope() !== directory) return
|
|
67
|
+
const prevChildren = tree.dir[dir]?.children ?? []
|
|
68
|
+
const nextChildren = nodes.map((node) => node.path)
|
|
69
|
+
const nextSet = new Set(nextChildren)
|
|
70
|
+
|
|
71
|
+
setTree(
|
|
72
|
+
"node",
|
|
73
|
+
produce((draft) => {
|
|
74
|
+
const removedDirs: string[] = []
|
|
75
|
+
|
|
76
|
+
for (const child of prevChildren) {
|
|
77
|
+
if (nextSet.has(child)) continue
|
|
78
|
+
const existing = draft[child]
|
|
79
|
+
if (existing?.type === "directory") removedDirs.push(child)
|
|
80
|
+
delete draft[child]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (removedDirs.length > 0) {
|
|
84
|
+
const keys = Object.keys(draft)
|
|
85
|
+
for (const key of keys) {
|
|
86
|
+
for (const removed of removedDirs) {
|
|
87
|
+
if (!key.startsWith(removed + "/")) continue
|
|
88
|
+
delete draft[key]
|
|
89
|
+
break
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for (const node of nodes) {
|
|
95
|
+
draft[node.path] = node
|
|
96
|
+
}
|
|
97
|
+
}),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
setTree(
|
|
101
|
+
"dir",
|
|
102
|
+
dir,
|
|
103
|
+
produce((draft) => {
|
|
104
|
+
draft.loaded = true
|
|
105
|
+
draft.loading = false
|
|
106
|
+
draft.children = nextChildren
|
|
107
|
+
}),
|
|
108
|
+
)
|
|
109
|
+
})
|
|
110
|
+
.catch((e) => {
|
|
111
|
+
if (options.scope() !== directory) return
|
|
112
|
+
setTree(
|
|
113
|
+
"dir",
|
|
114
|
+
dir,
|
|
115
|
+
produce((draft) => {
|
|
116
|
+
draft.loading = false
|
|
117
|
+
draft.error = e.message
|
|
118
|
+
}),
|
|
119
|
+
)
|
|
120
|
+
options.onError(e.message)
|
|
121
|
+
})
|
|
122
|
+
.finally(() => {
|
|
123
|
+
inflight.delete(dir)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
inflight.set(dir, promise)
|
|
127
|
+
return promise
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// `list: false` marks a directory expanded without fetching its children, for
|
|
131
|
+
// trees whose nodes are synthesized from a filter; listing directories that
|
|
132
|
+
// only exist on a diff's base branch fails and surfaces error toasts.
|
|
133
|
+
const expandDir = (input: string, behavior?: { list?: boolean }) => {
|
|
134
|
+
const dir = options.normalizeDir(input)
|
|
135
|
+
ensureDir(dir)
|
|
136
|
+
setTree("dir", dir, "expanded", true)
|
|
137
|
+
if (behavior?.list === false) return
|
|
138
|
+
void listDir(dir)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const collapseDir = (input: string) => {
|
|
142
|
+
const dir = options.normalizeDir(input)
|
|
143
|
+
ensureDir(dir)
|
|
144
|
+
setTree("dir", dir, "expanded", false)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const dirState = (input: string) => {
|
|
148
|
+
const dir = options.normalizeDir(input)
|
|
149
|
+
return tree.dir[dir]
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const children = (input: string) => {
|
|
153
|
+
const dir = options.normalizeDir(input)
|
|
154
|
+
const ids = tree.dir[dir]?.children
|
|
155
|
+
if (!ids) return []
|
|
156
|
+
const out: FileNode[] = []
|
|
157
|
+
for (const id of ids) {
|
|
158
|
+
const node = tree.node[id]
|
|
159
|
+
if (node) out.push(node)
|
|
160
|
+
}
|
|
161
|
+
return out
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
listDir,
|
|
166
|
+
expandDir,
|
|
167
|
+
collapseDir,
|
|
168
|
+
dirState,
|
|
169
|
+
children,
|
|
170
|
+
node: (path: string) => tree.node[path],
|
|
171
|
+
isLoaded: (path: string) => Boolean(tree.dir[path]?.loaded),
|
|
172
|
+
reset,
|
|
173
|
+
}
|
|
174
|
+
}
|