@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,109 @@
|
|
|
1
|
+
export type ConfigInvalidError = {
|
|
2
|
+
name: "ConfigInvalidError"
|
|
3
|
+
data: {
|
|
4
|
+
path?: string
|
|
5
|
+
message?: string
|
|
6
|
+
issues?: Array<{ message: string; path: string[] }>
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type ProviderModelNotFoundError = {
|
|
11
|
+
name: "ProviderModelNotFoundError"
|
|
12
|
+
data: {
|
|
13
|
+
providerID: string
|
|
14
|
+
modelID: string
|
|
15
|
+
suggestions?: string[]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type Translator = (key: string, vars?: Record<string, string | number>) => string
|
|
20
|
+
|
|
21
|
+
function tr(translator: Translator | undefined, key: string, text: string, vars?: Record<string, string | number>) {
|
|
22
|
+
if (!translator) return text
|
|
23
|
+
const out = translator(key, vars)
|
|
24
|
+
if (!out || out === key) return text
|
|
25
|
+
return out
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function formatServerError(error: unknown, translate?: Translator, fallback?: string) {
|
|
29
|
+
const unwrapped = unwrapNamedError(error)
|
|
30
|
+
if (isConfigInvalidErrorLike(unwrapped)) return parseReadableConfigInvalidError(unwrapped, translate)
|
|
31
|
+
if (isProviderModelNotFoundErrorLike(unwrapped)) return parseReadableProviderModelNotFoundError(unwrapped, translate)
|
|
32
|
+
if (error instanceof Error && error.message) return error.message
|
|
33
|
+
if (typeof error === "string" && error) return error
|
|
34
|
+
if (fallback) return fallback
|
|
35
|
+
return tr(translate, "error.chain.unknown", "Unknown error")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function unwrapNamedError(error: unknown): unknown {
|
|
39
|
+
if (error instanceof Error && error.cause && typeof error.cause === "object" && "body" in error.cause) {
|
|
40
|
+
return (error.cause as Record<string, unknown>).body
|
|
41
|
+
}
|
|
42
|
+
return error
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Client-synthesized session not-found errors share one constructor and
|
|
46
|
+
// predicate so the message contract cannot drift between the sync store
|
|
47
|
+
// (server-session.ts), the route lineage (session-lineage.ts), and the
|
|
48
|
+
// not-found fallback matching (session.tsx).
|
|
49
|
+
const sessionNotFoundMessage = (sessionID: string) => `Session not found: ${sessionID}`
|
|
50
|
+
|
|
51
|
+
export function sessionNotFoundError(sessionID: string) {
|
|
52
|
+
return new Error(sessionNotFoundMessage(sessionID))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function isLocalSessionNotFoundError(error: unknown, sessionID: string) {
|
|
56
|
+
return error instanceof Error && error.message === sessionNotFoundMessage(sessionID)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function isSessionNotFoundError(error: unknown, sessionID: string) {
|
|
60
|
+
const unwrapped = unwrapNamedError(error)
|
|
61
|
+
if (typeof unwrapped !== "object" || unwrapped === null) return false
|
|
62
|
+
const value = unwrapped as Record<string, unknown>
|
|
63
|
+
return value._tag === "SessionNotFoundError" && value.sessionID === sessionID
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isConfigInvalidErrorLike(error: unknown): error is ConfigInvalidError {
|
|
67
|
+
if (typeof error !== "object" || error === null) return false
|
|
68
|
+
const o = error as Record<string, unknown>
|
|
69
|
+
return o.name === "ConfigInvalidError" && typeof o.data === "object" && o.data !== null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isProviderModelNotFoundErrorLike(error: unknown): error is ProviderModelNotFoundError {
|
|
73
|
+
if (typeof error !== "object" || error === null) return false
|
|
74
|
+
const o = error as Record<string, unknown>
|
|
75
|
+
return o.name === "ProviderModelNotFoundError" && typeof o.data === "object" && o.data !== null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function parseReadableConfigInvalidError(errorInput: ConfigInvalidError, translator?: Translator) {
|
|
79
|
+
const file = errorInput.data.path && errorInput.data.path !== "config" ? errorInput.data.path : "config"
|
|
80
|
+
const detail = errorInput.data.message?.trim() ?? ""
|
|
81
|
+
const issues = (errorInput.data.issues ?? [])
|
|
82
|
+
.map((issue) => {
|
|
83
|
+
const msg = issue.message.trim()
|
|
84
|
+
if (!issue.path.length) return msg
|
|
85
|
+
return `${issue.path.join(".")}: ${msg}`
|
|
86
|
+
})
|
|
87
|
+
.filter(Boolean)
|
|
88
|
+
const msg = issues.length ? issues.join("\n") : detail
|
|
89
|
+
if (!msg) return tr(translator, "error.chain.configInvalid", `Config file at ${file} is invalid`, { path: file })
|
|
90
|
+
return tr(translator, "error.chain.configInvalidWithMessage", `Config file at ${file} is invalid: ${msg}`, {
|
|
91
|
+
path: file,
|
|
92
|
+
message: msg,
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function parseReadableProviderModelNotFoundError(errorInput: ProviderModelNotFoundError, translator?: Translator) {
|
|
97
|
+
const p = errorInput.data.providerID.trim()
|
|
98
|
+
const m = errorInput.data.modelID.trim()
|
|
99
|
+
const list = (errorInput.data.suggestions ?? []).map((v) => v.trim()).filter(Boolean)
|
|
100
|
+
const body = tr(translator, "error.chain.modelNotFound", `Model not found: ${p}/${m}`, { provider: p, model: m })
|
|
101
|
+
const tail = tr(translator, "error.chain.checkConfig", "Check your config (neurocode.json) provider/model names")
|
|
102
|
+
if (list.length) {
|
|
103
|
+
const suggestions = list.slice(0, 5).join(", ")
|
|
104
|
+
return [body, tr(translator, "error.chain.didYouMean", `Did you mean: ${suggestions}`, { suggestions }), tail].join(
|
|
105
|
+
"\n",
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
return [body, tail].join("\n")
|
|
109
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import type { ServerConnection } from "@/context/server"
|
|
3
|
+
import { checkServerHealth } from "./server-health"
|
|
4
|
+
|
|
5
|
+
const server: ServerConnection.HttpBase = {
|
|
6
|
+
url: "http://localhost:4096",
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function abortFromInput(input: RequestInfo | URL, init?: RequestInit) {
|
|
10
|
+
if (init?.signal) return init.signal
|
|
11
|
+
if (input instanceof Request) return input.signal
|
|
12
|
+
return undefined
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("checkServerHealth", () => {
|
|
16
|
+
test("returns healthy response with version", async () => {
|
|
17
|
+
let request: URL | undefined
|
|
18
|
+
const fetch = (async (input: RequestInfo | URL) => {
|
|
19
|
+
request = input instanceof URL ? input : new URL(input instanceof Request ? input.url : input)
|
|
20
|
+
return new Response(JSON.stringify({ healthy: true, version: "1.2.3" }), {
|
|
21
|
+
status: 200,
|
|
22
|
+
headers: { "content-type": "application/json" },
|
|
23
|
+
})
|
|
24
|
+
}) as unknown as typeof globalThis.fetch
|
|
25
|
+
|
|
26
|
+
const result = await checkServerHealth(server, fetch)
|
|
27
|
+
|
|
28
|
+
expect(result).toEqual({ healthy: true, version: "1.2.3" })
|
|
29
|
+
expect(request?.pathname).toBe("/api/health")
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test("falls back to the V1 health endpoint", async () => {
|
|
33
|
+
const paths: string[] = []
|
|
34
|
+
const fetch = (async (input: RequestInfo | URL) => {
|
|
35
|
+
const url = input instanceof URL ? input : new URL(input instanceof Request ? input.url : input)
|
|
36
|
+
paths.push(url.pathname)
|
|
37
|
+
if (url.pathname === "/api/health") return new Response(undefined, { status: 404 })
|
|
38
|
+
return Response.json({ healthy: true, version: "1.18.4" })
|
|
39
|
+
}) as unknown as typeof globalThis.fetch
|
|
40
|
+
|
|
41
|
+
expect(await checkServerHealth(server, fetch)).toEqual({ healthy: true, version: "1.18.4" })
|
|
42
|
+
expect(paths).toEqual(["/api/health", "/global/health"])
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test("falls back when the current health response is malformed", async () => {
|
|
46
|
+
const paths: string[] = []
|
|
47
|
+
const fetch = (async (input: RequestInfo | URL) => {
|
|
48
|
+
const url = input instanceof URL ? input : new URL(input instanceof Request ? input.url : input)
|
|
49
|
+
paths.push(url.pathname)
|
|
50
|
+
if (url.pathname === "/api/health") return Response.json({})
|
|
51
|
+
return Response.json({ healthy: true, version: "1.18.4" })
|
|
52
|
+
}) as unknown as typeof globalThis.fetch
|
|
53
|
+
|
|
54
|
+
expect(await checkServerHealth(server, fetch)).toEqual({ healthy: true, version: "1.18.4" })
|
|
55
|
+
expect(paths).toEqual(["/api/health", "/global/health"])
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test("allows slow servers thirty seconds by default", async () => {
|
|
59
|
+
const timeout = Object.getOwnPropertyDescriptor(AbortSignal, "timeout")
|
|
60
|
+
let timeoutMs = 0
|
|
61
|
+
Object.defineProperty(AbortSignal, "timeout", {
|
|
62
|
+
configurable: true,
|
|
63
|
+
value: (ms: number) => {
|
|
64
|
+
timeoutMs = ms
|
|
65
|
+
return new AbortController().signal
|
|
66
|
+
},
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const fetch = (async () =>
|
|
70
|
+
new Response(JSON.stringify({ healthy: true, version: "1.2.3" }), {
|
|
71
|
+
status: 200,
|
|
72
|
+
headers: { "content-type": "application/json" },
|
|
73
|
+
})) as unknown as typeof globalThis.fetch
|
|
74
|
+
|
|
75
|
+
await checkServerHealth(server, fetch).finally(() => {
|
|
76
|
+
if (timeout) Object.defineProperty(AbortSignal, "timeout", timeout)
|
|
77
|
+
if (!timeout) Reflect.deleteProperty(AbortSignal, "timeout")
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
expect(timeoutMs).toBe(30_000)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test("returns unhealthy when request fails", async () => {
|
|
84
|
+
const fetch = (async () => {
|
|
85
|
+
throw new Error("network")
|
|
86
|
+
}) as unknown as typeof globalThis.fetch
|
|
87
|
+
|
|
88
|
+
const result = await checkServerHealth(server, fetch)
|
|
89
|
+
|
|
90
|
+
expect(result).toEqual({ healthy: false })
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test("uses timeout fallback when AbortSignal.timeout is unavailable", async () => {
|
|
94
|
+
const timeout = Object.getOwnPropertyDescriptor(AbortSignal, "timeout")
|
|
95
|
+
Object.defineProperty(AbortSignal, "timeout", {
|
|
96
|
+
configurable: true,
|
|
97
|
+
value: undefined,
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
let aborted = false
|
|
101
|
+
const fetch = ((input: RequestInfo | URL, init?: RequestInit) =>
|
|
102
|
+
new Promise<Response>((_resolve, reject) => {
|
|
103
|
+
const signal = abortFromInput(input, init)
|
|
104
|
+
signal?.addEventListener(
|
|
105
|
+
"abort",
|
|
106
|
+
() => {
|
|
107
|
+
aborted = true
|
|
108
|
+
reject(new DOMException("Aborted", "AbortError"))
|
|
109
|
+
},
|
|
110
|
+
{ once: true },
|
|
111
|
+
)
|
|
112
|
+
})) as unknown as typeof globalThis.fetch
|
|
113
|
+
|
|
114
|
+
const result = await checkServerHealth(server, fetch, {
|
|
115
|
+
timeoutMs: 10,
|
|
116
|
+
}).finally(() => {
|
|
117
|
+
if (timeout) Object.defineProperty(AbortSignal, "timeout", timeout)
|
|
118
|
+
if (!timeout) Reflect.deleteProperty(AbortSignal, "timeout")
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
expect(aborted).toBe(true)
|
|
122
|
+
expect(result).toEqual({ healthy: false })
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test("uses provided abort signal", async () => {
|
|
126
|
+
let signal: AbortSignal | undefined
|
|
127
|
+
const fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
128
|
+
signal = abortFromInput(input, init)
|
|
129
|
+
return new Response(JSON.stringify({ healthy: true, version: "1.2.3" }), {
|
|
130
|
+
status: 200,
|
|
131
|
+
headers: { "content-type": "application/json" },
|
|
132
|
+
})
|
|
133
|
+
}) as unknown as typeof globalThis.fetch
|
|
134
|
+
|
|
135
|
+
const abort = new AbortController()
|
|
136
|
+
await checkServerHealth(server, fetch, {
|
|
137
|
+
signal: abort.signal,
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
expect(signal).toBe(abort.signal)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test("retries transient failures and eventually succeeds", async () => {
|
|
144
|
+
let count = 0
|
|
145
|
+
const fetch = (async () => {
|
|
146
|
+
count += 1
|
|
147
|
+
if (count < 3) throw new TypeError("network")
|
|
148
|
+
return new Response(JSON.stringify({ healthy: true, version: "1.2.3" }), {
|
|
149
|
+
status: 200,
|
|
150
|
+
headers: { "content-type": "application/json" },
|
|
151
|
+
})
|
|
152
|
+
}) as unknown as typeof globalThis.fetch
|
|
153
|
+
|
|
154
|
+
const result = await checkServerHealth(server, fetch, {
|
|
155
|
+
retryCount: 2,
|
|
156
|
+
retryDelayMs: 1,
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
expect(count).toBe(3)
|
|
160
|
+
expect(result).toEqual({ healthy: true, version: "1.2.3" })
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test("returns unhealthy when retries are exhausted", async () => {
|
|
164
|
+
let count = 0
|
|
165
|
+
const fetch = (async () => {
|
|
166
|
+
count += 1
|
|
167
|
+
throw new TypeError("network")
|
|
168
|
+
}) as unknown as typeof globalThis.fetch
|
|
169
|
+
|
|
170
|
+
const result = await checkServerHealth(server, fetch, {
|
|
171
|
+
retryCount: 2,
|
|
172
|
+
retryDelayMs: 1,
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
expect(count).toBe(6)
|
|
176
|
+
expect(result).toEqual({ healthy: false })
|
|
177
|
+
})
|
|
178
|
+
})
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { usePlatform } from "@/context/platform"
|
|
2
|
+
import { ServerConnection } from "@/context/server"
|
|
3
|
+
import { authTokenFromCredentials, createSdkForServer } from "./server"
|
|
4
|
+
import { ClientError, NeuroCode } from "@neurocode-ai/client"
|
|
5
|
+
import { Accessor, createEffect, onCleanup } from "solid-js"
|
|
6
|
+
import { createStore, reconcile } from "solid-js/store"
|
|
7
|
+
|
|
8
|
+
export type ServerHealth = { healthy: boolean; version?: string }
|
|
9
|
+
|
|
10
|
+
interface CheckServerHealthOptions {
|
|
11
|
+
timeoutMs?: number
|
|
12
|
+
signal?: AbortSignal
|
|
13
|
+
retryCount?: number
|
|
14
|
+
retryDelayMs?: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const defaultTimeoutMs = 30_000
|
|
18
|
+
const defaultRetryCount = 2
|
|
19
|
+
const defaultRetryDelayMs = 100
|
|
20
|
+
const cacheMs = 750
|
|
21
|
+
const healthCache = new Map<
|
|
22
|
+
string,
|
|
23
|
+
{ at: number; done: boolean; fetch: typeof globalThis.fetch; promise: Promise<ServerHealth> }
|
|
24
|
+
>()
|
|
25
|
+
|
|
26
|
+
function cacheKey(server: ServerConnection.HttpBase) {
|
|
27
|
+
return `${server.url}\n${server.username ?? ""}\n${server.password ?? ""}`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function timeoutSignal(timeoutMs: number) {
|
|
31
|
+
const timeout = (AbortSignal as unknown as { timeout?: (ms: number) => AbortSignal }).timeout
|
|
32
|
+
if (timeout) {
|
|
33
|
+
try {
|
|
34
|
+
return {
|
|
35
|
+
signal: timeout.call(AbortSignal, timeoutMs),
|
|
36
|
+
clear: undefined as (() => void) | undefined,
|
|
37
|
+
}
|
|
38
|
+
} catch {}
|
|
39
|
+
}
|
|
40
|
+
const controller = new AbortController()
|
|
41
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
42
|
+
return { signal: controller.signal, clear: () => clearTimeout(timer) }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function wait(ms: number, signal?: AbortSignal) {
|
|
46
|
+
return new Promise<void>((resolve, reject) => {
|
|
47
|
+
if (signal?.aborted) {
|
|
48
|
+
reject(new DOMException("Aborted", "AbortError"))
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
const timer = setTimeout(() => {
|
|
52
|
+
signal?.removeEventListener("abort", onAbort)
|
|
53
|
+
resolve()
|
|
54
|
+
}, ms)
|
|
55
|
+
const onAbort = () => {
|
|
56
|
+
clearTimeout(timer)
|
|
57
|
+
reject(new DOMException("Aborted", "AbortError"))
|
|
58
|
+
}
|
|
59
|
+
signal?.addEventListener("abort", onAbort, { once: true })
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function retryable(error: unknown, signal?: AbortSignal) {
|
|
64
|
+
if (signal?.aborted) return false
|
|
65
|
+
if (error instanceof ClientError) return error.reason === "Transport"
|
|
66
|
+
if (!(error instanceof Error)) return false
|
|
67
|
+
if (error.name === "AbortError" || error.name === "TimeoutError") return false
|
|
68
|
+
if (error instanceof TypeError) return true
|
|
69
|
+
return /network|fetch|econnreset|econnrefused|enotfound|timedout/i.test(error.message)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function checkServerHealth(
|
|
73
|
+
server: ServerConnection.HttpBase,
|
|
74
|
+
fetch: typeof globalThis.fetch,
|
|
75
|
+
opts?: CheckServerHealthOptions,
|
|
76
|
+
): Promise<ServerHealth> {
|
|
77
|
+
const timeout = opts?.signal ? undefined : timeoutSignal(opts?.timeoutMs ?? defaultTimeoutMs)
|
|
78
|
+
const signal = opts?.signal ?? timeout?.signal
|
|
79
|
+
const retryCount = opts?.retryCount ?? defaultRetryCount
|
|
80
|
+
const retryDelayMs = opts?.retryDelayMs ?? defaultRetryDelayMs
|
|
81
|
+
const next = (count: number, error: unknown) => {
|
|
82
|
+
if (count >= retryCount || !retryable(error, signal)) return Promise.resolve({ healthy: false } as const)
|
|
83
|
+
return wait(retryDelayMs * (count + 1), signal)
|
|
84
|
+
.then(() => attempt(count + 1))
|
|
85
|
+
.catch(() => ({ healthy: false }))
|
|
86
|
+
}
|
|
87
|
+
const attempt = async (count: number): Promise<ServerHealth> => {
|
|
88
|
+
const current = await NeuroCode.make({
|
|
89
|
+
baseUrl: server.url,
|
|
90
|
+
fetch,
|
|
91
|
+
headers: server.password
|
|
92
|
+
? {
|
|
93
|
+
Authorization: `Basic ${authTokenFromCredentials({ username: server.username, password: server.password })}`,
|
|
94
|
+
}
|
|
95
|
+
: undefined,
|
|
96
|
+
})
|
|
97
|
+
.health.get({ signal })
|
|
98
|
+
.then((x) =>
|
|
99
|
+
typeof x.healthy === "boolean"
|
|
100
|
+
? { data: { healthy: x.healthy, version: x.version } }
|
|
101
|
+
: { error: new Error("Invalid health response") },
|
|
102
|
+
)
|
|
103
|
+
.catch((error) => ({ error }))
|
|
104
|
+
if ("data" in current && current.data) return current.data
|
|
105
|
+
if (signal?.aborted) return { healthy: false }
|
|
106
|
+
|
|
107
|
+
return createSdkForServer({ server, fetch, signal })
|
|
108
|
+
.global.health()
|
|
109
|
+
.then((x) => (x.error ? next(count, x.error) : { healthy: x.data?.healthy === true, version: x.data?.version }))
|
|
110
|
+
.catch((error) => next(count, error))
|
|
111
|
+
}
|
|
112
|
+
return attempt(0).finally(() => timeout?.clear?.())
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const pollMs = 10_000
|
|
116
|
+
|
|
117
|
+
export function useCheckServerHealth() {
|
|
118
|
+
const platform = usePlatform()
|
|
119
|
+
const fetcher = platform.fetch ?? globalThis.fetch
|
|
120
|
+
|
|
121
|
+
return (http: ServerConnection.HttpBase) => {
|
|
122
|
+
const key = cacheKey(http)
|
|
123
|
+
const hit = healthCache.get(key)
|
|
124
|
+
const now = Date.now()
|
|
125
|
+
if (hit && hit.fetch === fetcher && (!hit.done || now - hit.at < cacheMs)) return hit.promise
|
|
126
|
+
const promise = checkServerHealth(http, fetcher).finally(() => {
|
|
127
|
+
const next = healthCache.get(key)
|
|
128
|
+
if (!next || next.promise !== promise) return
|
|
129
|
+
next.done = true
|
|
130
|
+
next.at = Date.now()
|
|
131
|
+
})
|
|
132
|
+
healthCache.set(key, { at: now, done: false, fetch: fetcher, promise })
|
|
133
|
+
return promise
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const useServerHealth = (servers: Accessor<ServerConnection.Any[]>, enabled: Accessor<boolean>) => {
|
|
138
|
+
const checkServerHealth = useCheckServerHealth()
|
|
139
|
+
const [status, setStatus] = createStore({} as Record<ServerConnection.Key, ServerHealth | undefined>)
|
|
140
|
+
|
|
141
|
+
createEffect(() => {
|
|
142
|
+
if (!enabled()) {
|
|
143
|
+
setStatus(reconcile({}))
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
const list = servers()
|
|
147
|
+
let dead = false
|
|
148
|
+
|
|
149
|
+
const refresh = async () => {
|
|
150
|
+
const results: Record<string, ServerHealth> = {}
|
|
151
|
+
await Promise.all(
|
|
152
|
+
list.map(async (conn) => {
|
|
153
|
+
const key = ServerConnection.key(conn)
|
|
154
|
+
const result = await checkServerHealth(conn.http)
|
|
155
|
+
results[key] = result
|
|
156
|
+
if (!dead) setStatus(key, result)
|
|
157
|
+
}),
|
|
158
|
+
)
|
|
159
|
+
if (dead) return
|
|
160
|
+
setStatus(reconcile(results))
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
void refresh()
|
|
164
|
+
const id = setInterval(() => void refresh(), pollMs)
|
|
165
|
+
onCleanup(() => {
|
|
166
|
+
dead = true
|
|
167
|
+
clearInterval(id)
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
return status
|
|
172
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { detectServerProtocol } from "./server-protocol"
|
|
3
|
+
|
|
4
|
+
const server = { url: "http://localhost:4096" }
|
|
5
|
+
const json = (value: unknown, status = 200) =>
|
|
6
|
+
new Response(JSON.stringify(value), { status, headers: { "content-type": "application/json" } })
|
|
7
|
+
const mockFetch = (run: (input: string | URL | Request) => Promise<Response>) =>
|
|
8
|
+
Object.assign(run, { preconnect: globalThis.fetch.preconnect })
|
|
9
|
+
|
|
10
|
+
describe("detectServerProtocol", () => {
|
|
11
|
+
test("prefers the legacy health endpoint when both API generations exist", async () => {
|
|
12
|
+
const fetcher = mockFetch((input) => {
|
|
13
|
+
const path = new URL(input instanceof Request ? input.url : input).pathname
|
|
14
|
+
if (path === "/global/health") return Promise.resolve(json({ healthy: true, version: "1.18.4" }))
|
|
15
|
+
return Promise.resolve(json({ healthy: true, version: "2.0.0", pid: 123 }))
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
expect(await detectServerProtocol(server, fetcher)).toBe("v1")
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test("recognizes V2 health by its process identifier", async () => {
|
|
22
|
+
const fetcher = mockFetch((input) => {
|
|
23
|
+
const path = new URL(input instanceof Request ? input.url : input).pathname
|
|
24
|
+
if (path === "/global/health") return Promise.resolve(json({}, 404))
|
|
25
|
+
return Promise.resolve(json({ healthy: true, version: "2.0.0", pid: 123 }))
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
expect(await detectServerProtocol(server, fetcher)).toBe("v2")
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test("recognizes the transitional V1 API health response", async () => {
|
|
32
|
+
const fetcher = mockFetch((input) => {
|
|
33
|
+
const path = new URL(input instanceof Request ? input.url : input).pathname
|
|
34
|
+
if (path === "/global/health") return Promise.resolve(json({}, 404))
|
|
35
|
+
return Promise.resolve(json({ healthy: true }))
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
expect(await detectServerProtocol(server, fetcher)).toBe("v1")
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ServerConnection } from "@/context/server"
|
|
2
|
+
import { authTokenFromCredentials } from "./server"
|
|
3
|
+
|
|
4
|
+
export type ServerProtocol = "v1" | "v2"
|
|
5
|
+
|
|
6
|
+
function headers(server: ServerConnection.HttpBase) {
|
|
7
|
+
if (!server.password) return
|
|
8
|
+
return {
|
|
9
|
+
Authorization: `Basic ${authTokenFromCredentials({ username: server.username, password: server.password })}`,
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function probe(server: ServerConnection.HttpBase, fetch: typeof globalThis.fetch, path: string) {
|
|
14
|
+
const response = await fetch(new URL(path, server.url), {
|
|
15
|
+
headers: headers(server),
|
|
16
|
+
signal: AbortSignal.timeout(5_000),
|
|
17
|
+
})
|
|
18
|
+
if (!response.ok || !response.headers.get("content-type")?.includes("application/json")) return
|
|
19
|
+
const value: unknown = await response.json()
|
|
20
|
+
if (!value || typeof value !== "object") return
|
|
21
|
+
return value
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function detectServerProtocol(
|
|
25
|
+
server: ServerConnection.HttpBase,
|
|
26
|
+
fetch: typeof globalThis.fetch,
|
|
27
|
+
): Promise<ServerProtocol> {
|
|
28
|
+
const legacy = await probe(server, fetch, "/global/health").catch(() => undefined)
|
|
29
|
+
if (legacy && "healthy" in legacy && legacy.healthy === true) return "v1"
|
|
30
|
+
|
|
31
|
+
const current = await probe(server, fetch, "/api/health").catch(() => undefined)
|
|
32
|
+
if (current && "pid" in current && typeof current.pid === "number") return "v2"
|
|
33
|
+
if (current && "healthy" in current && current.healthy === true) return "v1"
|
|
34
|
+
return "v2"
|
|
35
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { ScopedKey, ServerScope, SessionRouteKey, SessionStateKey, migrateLegacySessionStateKeys } from "./server-scope"
|
|
3
|
+
|
|
4
|
+
describe("ServerScope", () => {
|
|
5
|
+
test("uses a stable local scope for the canonical sidecar", () => {
|
|
6
|
+
expect(String(ServerScope.fromServerKey("sidecar" as Parameters<typeof ServerScope.fromServerKey>[0]))).toBe(
|
|
7
|
+
"local",
|
|
8
|
+
)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
test("keeps configured loopback servers distinct from the canonical sidecar", () => {
|
|
12
|
+
expect(
|
|
13
|
+
String(ServerScope.fromServerKey("http://localhost:4096" as Parameters<typeof ServerScope.fromServerKey>[0])),
|
|
14
|
+
).toBe("http://localhost:4096")
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test("uses a stable local scope for an explicit canonical web server", () => {
|
|
18
|
+
const key = "http://localhost:4096" as Parameters<typeof ServerScope.fromServerKey>[0]
|
|
19
|
+
expect(String(ServerScope.fromServerKey(key, key))).toBe("local")
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe("SessionStateKey", () => {
|
|
24
|
+
test("combines local and remote scope with route identity", () => {
|
|
25
|
+
const route = SessionRouteKey.fromRoute("cmVwbw", "session-1")
|
|
26
|
+
expect(String(SessionStateKey.from(ServerScope.local, route))).toBe("local\0cmVwbw/session-1")
|
|
27
|
+
expect(String(SessionStateKey.from("https://windows.example" as ServerScope, route))).toBe(
|
|
28
|
+
"https://windows.example\0cmVwbw/session-1",
|
|
29
|
+
)
|
|
30
|
+
expect(SessionStateKey.from("https://debian.example" as ServerScope, route)).not.toBe(
|
|
31
|
+
SessionStateKey.from("https://windows.example" as ServerScope, route),
|
|
32
|
+
)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test("extracts route keys from scoped and legacy state keys", () => {
|
|
36
|
+
expect(String(SessionStateKey.route("cmVwbw/session-1"))).toBe("cmVwbw/session-1")
|
|
37
|
+
expect(String(SessionStateKey.route("local\0cmVwbw/session-1"))).toBe("cmVwbw/session-1")
|
|
38
|
+
expect(String(SessionStateKey.route("https://debian.example\0cmVwbw/session-1"))).toBe("cmVwbw/session-1")
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
describe("migrateLegacySessionStateKeys", () => {
|
|
43
|
+
test("copies legacy route keys into local scope without overwriting scoped state", () => {
|
|
44
|
+
expect(
|
|
45
|
+
migrateLegacySessionStateKeys({
|
|
46
|
+
"cmVwbw/session-1": { active: "legacy" },
|
|
47
|
+
"local\0cmVwbw/session-1": { active: "scoped" },
|
|
48
|
+
"https://debian.example\0cmVwbw/session-1": { active: "remote" },
|
|
49
|
+
}),
|
|
50
|
+
).toEqual({
|
|
51
|
+
"local\0cmVwbw/session-1": { active: "scoped" },
|
|
52
|
+
"https://debian.example\0cmVwbw/session-1": { active: "remote" },
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test("rejects invalid identity fragments", () => {
|
|
57
|
+
expect(() => ScopedKey.from(ServerScope.local, "bad\0directory")).toThrow(
|
|
58
|
+
"Scoped key part cannot contain null bytes",
|
|
59
|
+
)
|
|
60
|
+
})
|
|
61
|
+
})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { ServerConnection } from "@/context/server"
|
|
2
|
+
|
|
3
|
+
export type ServerScope = string & { readonly __brand: "ServerScope" }
|
|
4
|
+
export type SessionRouteKey = string & { readonly __brand: "SessionRouteKey" }
|
|
5
|
+
export type SessionStateKey = string & { readonly __brand: "SessionStateKey" }
|
|
6
|
+
export type ScopedKey = string & { readonly __brand: "ScopedKey" }
|
|
7
|
+
|
|
8
|
+
const separator = "\u0000"
|
|
9
|
+
|
|
10
|
+
function fragment(label: string, value: string) {
|
|
11
|
+
if (value.includes(separator)) throw new Error(`${label} cannot contain null bytes`)
|
|
12
|
+
return value
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function compose(scope: ServerScope, parts: string[]) {
|
|
16
|
+
return [fragment("Server scope", scope), ...parts.map((part) => fragment("Scoped key part", part))].join(separator)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const ServerScope = {
|
|
20
|
+
local: "local" as ServerScope,
|
|
21
|
+
fromServerKey(key: ServerConnection.Key, canonicalLocalServer?: ServerConnection.Key) {
|
|
22
|
+
return fragment(
|
|
23
|
+
"Server scope",
|
|
24
|
+
key === "sidecar" || key === canonicalLocalServer ? ServerScope.local : key,
|
|
25
|
+
) as ServerScope
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const SessionRouteKey = {
|
|
30
|
+
fromRoute(dir: string | undefined, sessionID?: string) {
|
|
31
|
+
return fragment("Session route", `${dir ?? ""}${sessionID ? "/" + sessionID : ""}`) as SessionRouteKey
|
|
32
|
+
},
|
|
33
|
+
fromLegacy(key: string) {
|
|
34
|
+
return fragment("Legacy session route", key) as SessionRouteKey
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const SessionStateKey = {
|
|
39
|
+
from(scope: ServerScope, route: SessionRouteKey) {
|
|
40
|
+
return compose(scope, [route]) as SessionStateKey
|
|
41
|
+
},
|
|
42
|
+
route(key: string) {
|
|
43
|
+
const split = key.lastIndexOf(separator)
|
|
44
|
+
return SessionRouteKey.fromLegacy(split === -1 ? key : key.slice(split + 1))
|
|
45
|
+
},
|
|
46
|
+
scope(key: string) {
|
|
47
|
+
const split = key.indexOf(separator)
|
|
48
|
+
if (split === -1) return ServerScope.local
|
|
49
|
+
return fragment("Stored server scope", key.slice(0, split)) as ServerScope
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const ScopedKey = {
|
|
54
|
+
from(scope: ServerScope, ...parts: string[]) {
|
|
55
|
+
return compose(scope, parts) as ScopedKey
|
|
56
|
+
},
|
|
57
|
+
prefix(scope: ServerScope, ...parts: string[]) {
|
|
58
|
+
return `${ScopedKey.from(scope, ...parts)}${separator}`
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function migrateLegacySessionStateKeys(value: unknown) {
|
|
63
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return value
|
|
64
|
+
const entries = Object.entries(value)
|
|
65
|
+
if (entries.every(([key]) => key.includes(separator))) return value
|
|
66
|
+
const scoped = Object.fromEntries(entries.filter(([key]) => key.includes(separator)))
|
|
67
|
+
for (const [key, item] of entries) {
|
|
68
|
+
if (key.includes(separator)) continue
|
|
69
|
+
const next = SessionStateKey.from(ServerScope.local, SessionRouteKey.fromLegacy(key))
|
|
70
|
+
if (!(next in scoped)) scoped[next] = item
|
|
71
|
+
}
|
|
72
|
+
return scoped
|
|
73
|
+
}
|