nastechai-desktop 18.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierrc +11 -0
- package/DESIGN.md +167 -0
- package/README.md +141 -0
- package/assets/icon.icns +0 -0
- package/assets/icon.ico +0 -0
- package/assets/icon.png +0 -0
- package/components.json +21 -0
- package/electron/backend-env.cjs +112 -0
- package/electron/backend-env.test.cjs +111 -0
- package/electron/backend-probes.cjs +106 -0
- package/electron/backend-probes.test.cjs +82 -0
- package/electron/backend-ready.cjs +66 -0
- package/electron/bootstrap-platform.cjs +91 -0
- package/electron/bootstrap-platform.test.cjs +111 -0
- package/electron/bootstrap-runner.cjs +720 -0
- package/electron/bootstrap-runner.test.cjs +138 -0
- package/electron/connection-config.cjs +254 -0
- package/electron/connection-config.test.cjs +329 -0
- package/electron/dashboard-token.cjs +99 -0
- package/electron/dashboard-token.test.cjs +142 -0
- package/electron/desktop-uninstall.cjs +232 -0
- package/electron/desktop-uninstall.test.cjs +246 -0
- package/electron/entitlements.mac.inherit.plist +14 -0
- package/electron/entitlements.mac.plist +14 -0
- package/electron/fs-read-dir.cjs +109 -0
- package/electron/fs-read-dir.test.cjs +364 -0
- package/electron/gateway-ws-probe.cjs +188 -0
- package/electron/gateway-ws-probe.test.cjs +122 -0
- package/electron/git-root.cjs +54 -0
- package/electron/git-root.test.cjs +40 -0
- package/electron/git-worktrees.cjs +174 -0
- package/electron/hardening.cjs +184 -0
- package/electron/hardening.test.cjs +116 -0
- package/electron/main.cjs +5762 -0
- package/electron/oauth-net-request.cjs +20 -0
- package/electron/oauth-net-request.test.cjs +34 -0
- package/electron/preload.cjs +135 -0
- package/electron/session-windows.cjs +99 -0
- package/electron/session-windows.test.cjs +177 -0
- package/electron/update-remote.cjs +56 -0
- package/electron/update-remote.test.cjs +78 -0
- package/electron/vscode-marketplace.cjs +331 -0
- package/electron/vscode-marketplace.test.cjs +113 -0
- package/electron/windows-child-process.test.cjs +57 -0
- package/electron/windows-user-env.cjs +76 -0
- package/electron/windows-user-env.test.cjs +90 -0
- package/electron/workspace-cwd.cjs +38 -0
- package/electron/workspace-cwd.test.cjs +45 -0
- package/eslint.config.mjs +122 -0
- package/index.html +17 -0
- package/package.json +254 -0
- package/pr-assets/session-source-folders.png +0 -0
- package/preview-demo.html +65 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/ds-assets/filler-bg0.jpg +0 -0
- package/public/nastech-frames/nastech-frame-0.png +0 -0
- package/public/nastech-frames/nastech-frame-1.png +0 -0
- package/public/nastech-frames/nastech-frame-2.png +0 -0
- package/public/nastech-frames/nastech-frame-3.png +0 -0
- package/public/nastech-frames/nastech-frame-4.png +0 -0
- package/public/nastech-frames/nastech-frame-5.png +0 -0
- package/public/nastech-frames/nastech-frame-6.png +0 -0
- package/public/nastech-frames/nastech-frame-7.png +0 -0
- package/public/nastech-girl.jpg +0 -0
- package/public/nastech-sprite.png +0 -0
- package/public/nastech.png +0 -0
- package/scripts/after-pack.cjs +41 -0
- package/scripts/assert-dist-built.cjs +70 -0
- package/scripts/assert-dist-built.test.cjs +84 -0
- package/scripts/assert-root-install.cjs +13 -0
- package/scripts/before-build.cjs +11 -0
- package/scripts/before-pack.cjs +78 -0
- package/scripts/before-pack.test.cjs +53 -0
- package/scripts/click-session.mjs +51 -0
- package/scripts/dev-no-hmr.mjs +22 -0
- package/scripts/diag-jump.mjs +115 -0
- package/scripts/diag-scroll-reset.mjs +229 -0
- package/scripts/eval.mjs +21 -0
- package/scripts/leak-typing.mjs +222 -0
- package/scripts/measure-jump.mjs +108 -0
- package/scripts/measure-latency.mjs +184 -0
- package/scripts/measure-real-stream.mjs +252 -0
- package/scripts/measure-submit.mjs +179 -0
- package/scripts/measure-synthetic-stream.mjs +322 -0
- package/scripts/notarize-artifact.cjs +77 -0
- package/scripts/notarize.cjs +100 -0
- package/scripts/patch-electron-builder-mac-binary.cjs +59 -0
- package/scripts/probe-renderer.mjs +38 -0
- package/scripts/probe-thread.mjs +40 -0
- package/scripts/profile-long-stream.mjs +191 -0
- package/scripts/profile-real-stream.mjs +137 -0
- package/scripts/profile-synth-stream.mjs +103 -0
- package/scripts/profile-typing-lag.md +381 -0
- package/scripts/profile-typing.mjs +260 -0
- package/scripts/reload-renderer.mjs +25 -0
- package/scripts/reload.mjs +36 -0
- package/scripts/set-exe-identity.cjs +94 -0
- package/scripts/stage-native-deps.cjs +159 -0
- package/scripts/test-desktop.mjs +425 -0
- package/scripts/write-build-stamp.cjs +126 -0
- package/src/app/agents/index.tsx +398 -0
- package/src/app/artifacts/index.test.ts +62 -0
- package/src/app/artifacts/index.tsx +906 -0
- package/src/app/chat/chat-drop-overlay.tsx +48 -0
- package/src/app/chat/chat-swap-overlay.tsx +47 -0
- package/src/app/chat/composer/attachments.tsx +114 -0
- package/src/app/chat/composer/completion-drawer.tsx +63 -0
- package/src/app/chat/composer/context-menu.tsx +172 -0
- package/src/app/chat/composer/controls.tsx +289 -0
- package/src/app/chat/composer/drop-affordance.ts +2 -0
- package/src/app/chat/composer/enter-submit-dom-race.test.tsx +218 -0
- package/src/app/chat/composer/focus.ts +134 -0
- package/src/app/chat/composer/help-hint.tsx +59 -0
- package/src/app/chat/composer/hooks/use-at-completions.ts +141 -0
- package/src/app/chat/composer/hooks/use-live-completion-adapter.ts +119 -0
- package/src/app/chat/composer/hooks/use-mic-recorder.ts +291 -0
- package/src/app/chat/composer/hooks/use-slash-completions.ts +114 -0
- package/src/app/chat/composer/hooks/use-voice-conversation.ts +390 -0
- package/src/app/chat/composer/hooks/use-voice-recorder.ts +116 -0
- package/src/app/chat/composer/ime-composition-dom-repro.test.tsx +108 -0
- package/src/app/chat/composer/index.tsx +1611 -0
- package/src/app/chat/composer/inline-refs.ts +138 -0
- package/src/app/chat/composer/model-pill.tsx +86 -0
- package/src/app/chat/composer/queue-panel.tsx +130 -0
- package/src/app/chat/composer/rich-editor.test.ts +18 -0
- package/src/app/chat/composer/rich-editor.ts +165 -0
- package/src/app/chat/composer/skin-slash-popover.tsx +61 -0
- package/src/app/chat/composer/slash-nav-dom-repro.test.tsx +186 -0
- package/src/app/chat/composer/status-stack/index.tsx +202 -0
- package/src/app/chat/composer/status-stack/status-row.tsx +155 -0
- package/src/app/chat/composer/text-utils.test.ts +77 -0
- package/src/app/chat/composer/text-utils.ts +107 -0
- package/src/app/chat/composer/trigger-popover.test.tsx +42 -0
- package/src/app/chat/composer/trigger-popover.tsx +116 -0
- package/src/app/chat/composer/types.ts +64 -0
- package/src/app/chat/composer/url-dialog.tsx +82 -0
- package/src/app/chat/composer/voice-activity.tsx +252 -0
- package/src/app/chat/hooks/use-composer-actions.test.ts +57 -0
- package/src/app/chat/hooks/use-composer-actions.ts +525 -0
- package/src/app/chat/hooks/use-file-drop-zone.ts +118 -0
- package/src/app/chat/index.tsx +390 -0
- package/src/app/chat/perf-probe.tsx +269 -0
- package/src/app/chat/right-rail/index.ts +1 -0
- package/src/app/chat/right-rail/preview-console-state.ts +82 -0
- package/src/app/chat/right-rail/preview-console.tsx +290 -0
- package/src/app/chat/right-rail/preview-file.tsx +559 -0
- package/src/app/chat/right-rail/preview-pane.test.tsx +43 -0
- package/src/app/chat/right-rail/preview-pane.tsx +657 -0
- package/src/app/chat/right-rail/preview.tsx +171 -0
- package/src/app/chat/scroll-to-bottom-button.test.tsx +67 -0
- package/src/app/chat/scroll-to-bottom-button.tsx +74 -0
- package/src/app/chat/sidebar/cron-jobs-section.tsx +325 -0
- package/src/app/chat/sidebar/index.tsx +1219 -0
- package/src/app/chat/sidebar/load-more-row.tsx +30 -0
- package/src/app/chat/sidebar/order.test.ts +21 -0
- package/src/app/chat/sidebar/order.ts +17 -0
- package/src/app/chat/sidebar/profile-switcher.tsx +516 -0
- package/src/app/chat/sidebar/session-actions-menu.tsx +264 -0
- package/src/app/chat/sidebar/session-row.tsx +257 -0
- package/src/app/chat/sidebar/virtual-session-list.tsx +154 -0
- package/src/app/chat/sidebar/workspace-groups.test.ts +149 -0
- package/src/app/chat/sidebar/workspace-groups.ts +326 -0
- package/src/app/chat/thread-loading.test.ts +34 -0
- package/src/app/chat/thread-loading.ts +26 -0
- package/src/app/command-center/index.tsx +654 -0
- package/src/app/command-palette/index.tsx +513 -0
- package/src/app/command-palette/marketplace-theme-page.tsx +157 -0
- package/src/app/cron/index.tsx +942 -0
- package/src/app/cron/job-state.ts +29 -0
- package/src/app/desktop-controller.tsx +938 -0
- package/src/app/floating-hud.ts +22 -0
- package/src/app/gateway/hooks/use-gateway-boot.test.tsx +265 -0
- package/src/app/gateway/hooks/use-gateway-boot.ts +387 -0
- package/src/app/gateway/hooks/use-gateway-request.ts +138 -0
- package/src/app/hooks/use-keybinds.ts +186 -0
- package/src/app/hooks/use-refresh-hotkey.ts +45 -0
- package/src/app/hooks/use-route-enum-param.ts +38 -0
- package/src/app/index.tsx +1 -0
- package/src/app/layout-constants.ts +13 -0
- package/src/app/messaging/index.tsx +648 -0
- package/src/app/messaging/platform-icon.tsx +93 -0
- package/src/app/model-picker-overlay.tsx +42 -0
- package/src/app/model-visibility-overlay.tsx +31 -0
- package/src/app/overlays/overlay-chrome.tsx +66 -0
- package/src/app/overlays/overlay-search-input.tsx +33 -0
- package/src/app/overlays/overlay-split-layout.tsx +130 -0
- package/src/app/overlays/overlay-view.tsx +91 -0
- package/src/app/page-search-shell.tsx +75 -0
- package/src/app/profiles/create-profile-dialog.tsx +154 -0
- package/src/app/profiles/delete-profile-dialog.tsx +65 -0
- package/src/app/profiles/index.tsx +671 -0
- package/src/app/profiles/rename-profile-dialog.tsx +125 -0
- package/src/app/right-sidebar/files/dnd-manager.ts +27 -0
- package/src/app/right-sidebar/files/ipc.test.ts +100 -0
- package/src/app/right-sidebar/files/ipc.ts +161 -0
- package/src/app/right-sidebar/files/remote-picker.tsx +177 -0
- package/src/app/right-sidebar/files/tree.tsx +224 -0
- package/src/app/right-sidebar/files/use-project-tree.test.ts +190 -0
- package/src/app/right-sidebar/files/use-project-tree.ts +268 -0
- package/src/app/right-sidebar/index.test.tsx +75 -0
- package/src/app/right-sidebar/index.tsx +395 -0
- package/src/app/right-sidebar/store.ts +15 -0
- package/src/app/right-sidebar/terminal/buffer.ts +65 -0
- package/src/app/right-sidebar/terminal/index.tsx +98 -0
- package/src/app/right-sidebar/terminal/persistent.tsx +122 -0
- package/src/app/right-sidebar/terminal/selection.ts +75 -0
- package/src/app/right-sidebar/terminal/use-terminal-session.ts +504 -0
- package/src/app/routes.ts +88 -0
- package/src/app/session/hooks/use-context-suggestions.ts +58 -0
- package/src/app/session/hooks/use-cwd-actions.ts +109 -0
- package/src/app/session/hooks/use-message-stream.ts +957 -0
- package/src/app/session/hooks/use-model-controls.test.tsx +198 -0
- package/src/app/session/hooks/use-model-controls.ts +106 -0
- package/src/app/session/hooks/use-nastech-config.ts +74 -0
- package/src/app/session/hooks/use-preview-routing.test.tsx +168 -0
- package/src/app/session/hooks/use-preview-routing.ts +223 -0
- package/src/app/session/hooks/use-prompt-actions.test.tsx +316 -0
- package/src/app/session/hooks/use-prompt-actions.ts +1030 -0
- package/src/app/session/hooks/use-route-resume.test.tsx +136 -0
- package/src/app/session/hooks/use-route-resume.ts +115 -0
- package/src/app/session/hooks/use-session-actions.test.tsx +119 -0
- package/src/app/session/hooks/use-session-actions.ts +885 -0
- package/src/app/session/hooks/use-session-state-cache.test.tsx +118 -0
- package/src/app/session/hooks/use-session-state-cache.ts +191 -0
- package/src/app/session-picker-overlay.tsx +32 -0
- package/src/app/session-switcher.tsx +107 -0
- package/src/app/settings/about-settings.tsx +173 -0
- package/src/app/settings/appearance-settings.tsx +162 -0
- package/src/app/settings/config-settings.tsx +384 -0
- package/src/app/settings/constants.ts +545 -0
- package/src/app/settings/credential-key-ui.tsx +373 -0
- package/src/app/settings/env-credentials.tsx +198 -0
- package/src/app/settings/env-var-actions-menu.tsx +136 -0
- package/src/app/settings/field-copy.ts +56 -0
- package/src/app/settings/gateway-settings.tsx +620 -0
- package/src/app/settings/helpers.test.ts +138 -0
- package/src/app/settings/helpers.ts +151 -0
- package/src/app/settings/index.tsx +237 -0
- package/src/app/settings/keys-settings.tsx +96 -0
- package/src/app/settings/mcp-settings.tsx +271 -0
- package/src/app/settings/model-settings.test.tsx +157 -0
- package/src/app/settings/model-settings.tsx +559 -0
- package/src/app/settings/notifications-settings.tsx +150 -0
- package/src/app/settings/primitives.tsx +115 -0
- package/src/app/settings/providers-settings.test.tsx +100 -0
- package/src/app/settings/providers-settings.tsx +258 -0
- package/src/app/settings/sessions-settings.tsx +276 -0
- package/src/app/settings/toolset-config-panel.test.tsx +289 -0
- package/src/app/settings/toolset-config-panel.tsx +449 -0
- package/src/app/settings/types.ts +42 -0
- package/src/app/settings/uninstall-section.tsx +185 -0
- package/src/app/settings/use-deep-link-highlight.ts +60 -0
- package/src/app/shell/app-shell.tsx +167 -0
- package/src/app/shell/gateway-menu-panel.tsx +150 -0
- package/src/app/shell/hooks/use-overlay-routing.ts +71 -0
- package/src/app/shell/hooks/use-status-snapshot.ts +57 -0
- package/src/app/shell/hooks/use-statusbar-items.tsx +403 -0
- package/src/app/shell/keybind-panel.tsx +220 -0
- package/src/app/shell/model-edit-submenu.test.tsx +84 -0
- package/src/app/shell/model-edit-submenu.tsx +245 -0
- package/src/app/shell/model-menu-panel.tsx +295 -0
- package/src/app/shell/sidebar-label.tsx +22 -0
- package/src/app/shell/statusbar-controls.tsx +185 -0
- package/src/app/shell/titlebar-controls.tsx +244 -0
- package/src/app/shell/titlebar.test.ts +26 -0
- package/src/app/shell/titlebar.ts +45 -0
- package/src/app/shell/use-group-registry.ts +39 -0
- package/src/app/skills/index.test.tsx +103 -0
- package/src/app/skills/index.tsx +371 -0
- package/src/app/types.ts +99 -0
- package/src/app/updates-overlay.tsx +369 -0
- package/src/components/Backdrop.tsx +114 -0
- package/src/components/assistant-ui/ansi-text.tsx +34 -0
- package/src/components/assistant-ui/clarify-tool.tsx +281 -0
- package/src/components/assistant-ui/directive-text.test.ts +39 -0
- package/src/components/assistant-ui/directive-text.tsx +389 -0
- package/src/components/assistant-ui/markdown-text.test.ts +204 -0
- package/src/components/assistant-ui/markdown-text.tsx +497 -0
- package/src/components/assistant-ui/message-render-boundary.test.tsx +80 -0
- package/src/components/assistant-ui/message-render-boundary.tsx +48 -0
- package/src/components/assistant-ui/streaming.test.tsx +739 -0
- package/src/components/assistant-ui/thread-list.tsx +307 -0
- package/src/components/assistant-ui/thread-virtualizer.tsx +512 -0
- package/src/components/assistant-ui/thread.tsx +1474 -0
- package/src/components/assistant-ui/todo-tool.tsx +109 -0
- package/src/components/assistant-ui/tool-approval-group.test.tsx +158 -0
- package/src/components/assistant-ui/tool-approval.test.tsx +81 -0
- package/src/components/assistant-ui/tool-approval.tsx +209 -0
- package/src/components/assistant-ui/tool-fallback-model.test.ts +66 -0
- package/src/components/assistant-ui/tool-fallback-model.ts +1368 -0
- package/src/components/assistant-ui/tool-fallback.tsx +466 -0
- package/src/components/assistant-ui/tooltip-icon-button.tsx +33 -0
- package/src/components/assistant-ui/user-message-edit.test.tsx +141 -0
- package/src/components/assistant-ui/user-message-text.tsx +150 -0
- package/src/components/boot-failure-overlay.tsx +246 -0
- package/src/components/boot-failure-reauth.test.ts +100 -0
- package/src/components/boot-failure-reauth.ts +81 -0
- package/src/components/brand-mark.tsx +19 -0
- package/src/components/chat/activity-timer-text.tsx +24 -0
- package/src/components/chat/activity-timer.test.tsx +43 -0
- package/src/components/chat/activity-timer.ts +64 -0
- package/src/components/chat/code-card.tsx +78 -0
- package/src/components/chat/compact-markdown.tsx +113 -0
- package/src/components/chat/composer-dock.ts +31 -0
- package/src/components/chat/diff-lines.tsx +54 -0
- package/src/components/chat/disclosure-row.tsx +63 -0
- package/src/components/chat/generated-image-context.tsx +19 -0
- package/src/components/chat/generated-image-result.tsx +174 -0
- package/src/components/chat/image-generation-placeholder.tsx +279 -0
- package/src/components/chat/intro-copy.jsonl +75 -0
- package/src/components/chat/intro.tsx +182 -0
- package/src/components/chat/preview-attachment.tsx +125 -0
- package/src/components/chat/shiki-highlighter.tsx +107 -0
- package/src/components/chat/status-row.tsx +70 -0
- package/src/components/chat/status-section.tsx +42 -0
- package/src/components/chat/terminal-output.tsx +50 -0
- package/src/components/chat/zoomable-image.tsx +177 -0
- package/src/components/desktop-install-overlay.tsx +595 -0
- package/src/components/desktop-onboarding-overlay.test.tsx +100 -0
- package/src/components/desktop-onboarding-overlay.tsx +1286 -0
- package/src/components/error-boundary.tsx +77 -0
- package/src/components/gateway-connecting-overlay.test.tsx +143 -0
- package/src/components/gateway-connecting-overlay.tsx +183 -0
- package/src/components/haptics-provider.tsx +19 -0
- package/src/components/language-switcher.test.tsx +53 -0
- package/src/components/language-switcher.tsx +175 -0
- package/src/components/model-picker.tsx +340 -0
- package/src/components/model-visibility-dialog.tsx +155 -0
- package/src/components/notifications.tsx +196 -0
- package/src/components/page-loader.tsx +34 -0
- package/src/components/pane-shell/context.ts +14 -0
- package/src/components/pane-shell/index.ts +4 -0
- package/src/components/pane-shell/pane-shell.test.tsx +333 -0
- package/src/components/pane-shell/pane-shell.tsx +330 -0
- package/src/components/prompt-overlays.tsx +234 -0
- package/src/components/session-picker.tsx +108 -0
- package/src/components/status-dot.tsx +26 -0
- package/src/components/ui/action-status.tsx +25 -0
- package/src/components/ui/alert.tsx +53 -0
- package/src/components/ui/badge.tsx +35 -0
- package/src/components/ui/braille-spinner.tsx +61 -0
- package/src/components/ui/button.tsx +81 -0
- package/src/components/ui/checkbox.tsx +27 -0
- package/src/components/ui/codicon.tsx +20 -0
- package/src/components/ui/command.tsx +111 -0
- package/src/components/ui/confirm-dialog.tsx +109 -0
- package/src/components/ui/context-menu.tsx +141 -0
- package/src/components/ui/control.ts +25 -0
- package/src/components/ui/copy-button.test.tsx +36 -0
- package/src/components/ui/copy-button.tsx +229 -0
- package/src/components/ui/dialog.tsx +152 -0
- package/src/components/ui/disclosure-caret.tsx +20 -0
- package/src/components/ui/dropdown-menu.tsx +291 -0
- package/src/components/ui/error-state.tsx +50 -0
- package/src/components/ui/fade-text.tsx +110 -0
- package/src/components/ui/glyph-spinner.tsx +63 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/kbd.tsx +37 -0
- package/src/components/ui/loader.tsx +558 -0
- package/src/components/ui/log-view.tsx +17 -0
- package/src/components/ui/pagination.tsx +114 -0
- package/src/components/ui/popover.tsx +44 -0
- package/src/components/ui/scroll-area.tsx +43 -0
- package/src/components/ui/search-field.tsx +80 -0
- package/src/components/ui/segmented-control.tsx +51 -0
- package/src/components/ui/select.tsx +92 -0
- package/src/components/ui/separator.tsx +26 -0
- package/src/components/ui/sheet.tsx +116 -0
- package/src/components/ui/sidebar.tsx +674 -0
- package/src/components/ui/skeleton.tsx +7 -0
- package/src/components/ui/switch.tsx +49 -0
- package/src/components/ui/tabs.tsx +36 -0
- package/src/components/ui/text-tab.tsx +43 -0
- package/src/components/ui/textarea.tsx +11 -0
- package/src/components/ui/tool-icon.tsx +65 -0
- package/src/components/ui/tooltip.tsx +69 -0
- package/src/fonts/JetBrainsMono-Bold.woff2 +0 -0
- package/src/fonts/JetBrainsMono-Italic.woff2 +0 -0
- package/src/fonts/JetBrainsMono-Regular.woff2 +0 -0
- package/src/global.d.ts +457 -0
- package/src/hooks/use-image-download.ts +85 -0
- package/src/hooks/use-media-query.ts +24 -0
- package/src/hooks/use-mobile.ts +3 -0
- package/src/hooks/use-resize-observer.ts +38 -0
- package/src/hooks/use-worktree-info.ts +68 -0
- package/src/i18n/catalog.ts +12 -0
- package/src/i18n/context.test.tsx +232 -0
- package/src/i18n/context.tsx +183 -0
- package/src/i18n/define-locale.ts +41 -0
- package/src/i18n/en.ts +1779 -0
- package/src/i18n/index.ts +20 -0
- package/src/i18n/ja.ts +1890 -0
- package/src/i18n/languages.test.ts +43 -0
- package/src/i18n/languages.ts +86 -0
- package/src/i18n/runtime.test.ts +75 -0
- package/src/i18n/runtime.ts +53 -0
- package/src/i18n/types.ts +1452 -0
- package/src/i18n/zh-hant.ts +1849 -0
- package/src/i18n/zh.ts +1923 -0
- package/src/lib/ansi.test.ts +123 -0
- package/src/lib/ansi.ts +175 -0
- package/src/lib/chat-messages.test.ts +708 -0
- package/src/lib/chat-messages.ts +885 -0
- package/src/lib/chat-runtime.test.ts +18 -0
- package/src/lib/chat-runtime.ts +335 -0
- package/src/lib/clipboard.ts +28 -0
- package/src/lib/commit-changelog.test.ts +114 -0
- package/src/lib/commit-changelog.ts +177 -0
- package/src/lib/completion-sound.ts +519 -0
- package/src/lib/desktop-fs.test.ts +116 -0
- package/src/lib/desktop-fs.ts +113 -0
- package/src/lib/desktop-slash-commands.test.ts +126 -0
- package/src/lib/desktop-slash-commands.ts +286 -0
- package/src/lib/embedded-images.test.ts +35 -0
- package/src/lib/embedded-images.ts +60 -0
- package/src/lib/external-link.test.tsx +168 -0
- package/src/lib/external-link.tsx +303 -0
- package/src/lib/gateway-events.test.ts +27 -0
- package/src/lib/gateway-events.ts +49 -0
- package/src/lib/gateway-ws-url.test.ts +78 -0
- package/src/lib/gateway-ws-url.ts +91 -0
- package/src/lib/generated-images.test.ts +97 -0
- package/src/lib/generated-images.ts +116 -0
- package/src/lib/haptics.ts +129 -0
- package/src/lib/icons.ts +203 -0
- package/src/lib/incremental-external-store-runtime.ts +188 -0
- package/src/lib/katex-memo.ts +260 -0
- package/src/lib/keybinds/actions.ts +125 -0
- package/src/lib/keybinds/combo.test.ts +86 -0
- package/src/lib/keybinds/combo.ts +169 -0
- package/src/lib/local-preview.ts +126 -0
- package/src/lib/markdown-code.test.ts +23 -0
- package/src/lib/markdown-code.ts +195 -0
- package/src/lib/markdown-preprocess.ts +386 -0
- package/src/lib/media.remote.test.ts +58 -0
- package/src/lib/media.ts +111 -0
- package/src/lib/model-status-label.test.ts +31 -0
- package/src/lib/model-status-label.ts +103 -0
- package/src/lib/mutable-ref.ts +6 -0
- package/src/lib/preview-targets.test.ts +27 -0
- package/src/lib/preview-targets.ts +63 -0
- package/src/lib/profile-color.ts +58 -0
- package/src/lib/provider-setup-errors.test.ts +26 -0
- package/src/lib/provider-setup-errors.ts +12 -0
- package/src/lib/query-client.ts +13 -0
- package/src/lib/remend-tail.test.ts +105 -0
- package/src/lib/remend-tail.ts +108 -0
- package/src/lib/runtime-readiness.test.ts +65 -0
- package/src/lib/runtime-readiness.ts +147 -0
- package/src/lib/session-export.ts +57 -0
- package/src/lib/session-search.test.ts +58 -0
- package/src/lib/session-search.ts +19 -0
- package/src/lib/session-source.ts +62 -0
- package/src/lib/speech-text.ts +35 -0
- package/src/lib/statusbar.ts +91 -0
- package/src/lib/storage.test.ts +25 -0
- package/src/lib/storage.ts +107 -0
- package/src/lib/todos.test.ts +35 -0
- package/src/lib/todos.ts +51 -0
- package/src/lib/tool-result-summary.test.ts +106 -0
- package/src/lib/tool-result-summary.ts +467 -0
- package/src/lib/update-copy.test.ts +38 -0
- package/src/lib/update-copy.ts +44 -0
- package/src/lib/use-enter-animation.ts +100 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/voice-playback.ts +128 -0
- package/src/lib/yolo-session.ts +26 -0
- package/src/main.tsx +43 -0
- package/src/nastech.test.ts +49 -0
- package/src/nastech.ts +718 -0
- package/src/store/activity.ts +100 -0
- package/src/store/boot.ts +91 -0
- package/src/store/clarify.test.ts +81 -0
- package/src/store/clarify.ts +69 -0
- package/src/store/command-palette.ts +20 -0
- package/src/store/compaction.test.ts +53 -0
- package/src/store/compaction.ts +38 -0
- package/src/store/completion-sound.ts +32 -0
- package/src/store/composer-input-history.test.ts +147 -0
- package/src/store/composer-input-history.ts +158 -0
- package/src/store/composer-queue.test.ts +148 -0
- package/src/store/composer-queue.ts +239 -0
- package/src/store/composer-status.test.ts +99 -0
- package/src/store/composer-status.ts +277 -0
- package/src/store/composer.test.ts +106 -0
- package/src/store/composer.ts +184 -0
- package/src/store/cron.ts +19 -0
- package/src/store/gateway.ts +290 -0
- package/src/store/haptics.ts +17 -0
- package/src/store/keybinds.ts +139 -0
- package/src/store/layout.ts +176 -0
- package/src/store/model-presets.test.ts +51 -0
- package/src/store/model-presets.ts +86 -0
- package/src/store/model-visibility.test.ts +37 -0
- package/src/store/model-visibility.ts +108 -0
- package/src/store/native-notifications.test.ts +192 -0
- package/src/store/native-notifications.ts +203 -0
- package/src/store/notifications.ts +165 -0
- package/src/store/onboarding.test.ts +372 -0
- package/src/store/onboarding.ts +866 -0
- package/src/store/panes.test.ts +146 -0
- package/src/store/panes.ts +145 -0
- package/src/store/preview.test.ts +135 -0
- package/src/store/preview.ts +466 -0
- package/src/store/profile.test.ts +89 -0
- package/src/store/profile.ts +365 -0
- package/src/store/prompts.test.ts +121 -0
- package/src/store/prompts.ts +115 -0
- package/src/store/session-switcher.test.ts +115 -0
- package/src/store/session-switcher.ts +128 -0
- package/src/store/session-sync.ts +25 -0
- package/src/store/session.test.ts +131 -0
- package/src/store/session.ts +255 -0
- package/src/store/subagents.test.ts +111 -0
- package/src/store/subagents.ts +260 -0
- package/src/store/thread-scroll.ts +46 -0
- package/src/store/todos.test.ts +47 -0
- package/src/store/todos.ts +64 -0
- package/src/store/tool-diffs.ts +23 -0
- package/src/store/tool-dismiss.ts +45 -0
- package/src/store/tool-view.ts +91 -0
- package/src/store/translucency.ts +38 -0
- package/src/store/updates.test.ts +77 -0
- package/src/store/updates.ts +315 -0
- package/src/store/voice-playback.ts +24 -0
- package/src/store/windows.test.ts +143 -0
- package/src/store/windows.ts +77 -0
- package/src/styles.css +1235 -0
- package/src/themes/color.ts +142 -0
- package/src/themes/context.tsx +339 -0
- package/src/themes/index.ts +3 -0
- package/src/themes/install.test.ts +119 -0
- package/src/themes/install.ts +95 -0
- package/src/themes/presets.test.ts +33 -0
- package/src/themes/presets.ts +293 -0
- package/src/themes/profile-theme.test.ts +41 -0
- package/src/themes/types.ts +66 -0
- package/src/themes/use-skin-command.ts +60 -0
- package/src/themes/user-themes.test.ts +63 -0
- package/src/themes/user-themes.ts +122 -0
- package/src/themes/vscode.test.ts +171 -0
- package/src/themes/vscode.ts +343 -0
- package/src/types/nastech.ts +646 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +25 -0
- package/vite.config.ts +56 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict')
|
|
4
|
+
const fs = require('node:fs')
|
|
5
|
+
const os = require('node:os')
|
|
6
|
+
const path = require('node:path')
|
|
7
|
+
const test = require('node:test')
|
|
8
|
+
const { pathToFileURL } = require('node:url')
|
|
9
|
+
|
|
10
|
+
const { readDirForIpc } = require('./fs-read-dir.cjs')
|
|
11
|
+
|
|
12
|
+
function mkTmpDir() {
|
|
13
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'nastech-fs-read-dir-'))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function fakeDirent(name, flags = {}) {
|
|
17
|
+
return {
|
|
18
|
+
name,
|
|
19
|
+
isDirectory: () => Boolean(flags.directory),
|
|
20
|
+
isFile: () => Boolean(flags.file),
|
|
21
|
+
isSymbolicLink: () => Boolean(flags.symlink)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test('readDirForIpc hides noisy directories and files from the project tree', async () => {
|
|
26
|
+
const root = mkTmpDir()
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
fs.mkdirSync(path.join(root, 'node_modules'))
|
|
30
|
+
fs.mkdirSync(path.join(root, 'src'))
|
|
31
|
+
fs.writeFileSync(path.join(root, 'target'), 'hidden file')
|
|
32
|
+
fs.writeFileSync(path.join(root, 'README.md'), 'visible file')
|
|
33
|
+
|
|
34
|
+
const result = await readDirForIpc(root)
|
|
35
|
+
|
|
36
|
+
assert.equal(result.error, undefined)
|
|
37
|
+
assert.deepEqual(
|
|
38
|
+
result.entries.map(entry => entry.name),
|
|
39
|
+
['src', 'README.md']
|
|
40
|
+
)
|
|
41
|
+
} finally {
|
|
42
|
+
fs.rmSync(root, { recursive: true, force: true })
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('readDirForIpc filters a hidden basename whether it is a file or directory', async () => {
|
|
47
|
+
const dirRoot = mkTmpDir()
|
|
48
|
+
const fileRoot = mkTmpDir()
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
fs.mkdirSync(path.join(dirRoot, 'node_modules'))
|
|
52
|
+
fs.writeFileSync(path.join(dirRoot, 'visible.txt'), 'visible')
|
|
53
|
+
fs.writeFileSync(path.join(fileRoot, 'node_modules'), 'hidden file')
|
|
54
|
+
fs.writeFileSync(path.join(fileRoot, 'visible.txt'), 'visible')
|
|
55
|
+
|
|
56
|
+
assert.deepEqual(
|
|
57
|
+
(await readDirForIpc(dirRoot)).entries.map(entry => entry.name),
|
|
58
|
+
['visible.txt']
|
|
59
|
+
)
|
|
60
|
+
assert.deepEqual(
|
|
61
|
+
(await readDirForIpc(fileRoot)).entries.map(entry => entry.name),
|
|
62
|
+
['visible.txt']
|
|
63
|
+
)
|
|
64
|
+
} finally {
|
|
65
|
+
fs.rmSync(dirRoot, { recursive: true, force: true })
|
|
66
|
+
fs.rmSync(fileRoot, { recursive: true, force: true })
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('readDirForIpc returns directories before files and sorts by name within groups', async () => {
|
|
71
|
+
const root = mkTmpDir()
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
fs.writeFileSync(path.join(root, 'z.txt'), 'z')
|
|
75
|
+
fs.mkdirSync(path.join(root, 'src'))
|
|
76
|
+
fs.writeFileSync(path.join(root, 'a.txt'), 'a')
|
|
77
|
+
fs.mkdirSync(path.join(root, 'lib'))
|
|
78
|
+
|
|
79
|
+
const result = await readDirForIpc(root)
|
|
80
|
+
|
|
81
|
+
assert.equal(result.error, undefined)
|
|
82
|
+
assert.deepEqual(
|
|
83
|
+
result.entries.map(entry => entry.name),
|
|
84
|
+
['lib', 'src', 'a.txt', 'z.txt']
|
|
85
|
+
)
|
|
86
|
+
} finally {
|
|
87
|
+
fs.rmSync(root, { recursive: true, force: true })
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('readDirForIpc accepts file URLs for directories', async () => {
|
|
92
|
+
const root = mkTmpDir()
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
fs.mkdirSync(path.join(root, 'src'))
|
|
96
|
+
fs.writeFileSync(path.join(root, 'README.md'), 'visible file')
|
|
97
|
+
|
|
98
|
+
const result = await readDirForIpc(pathToFileURL(root).toString())
|
|
99
|
+
|
|
100
|
+
assert.equal(result.error, undefined)
|
|
101
|
+
assert.deepEqual(
|
|
102
|
+
result.entries.map(entry => entry.name),
|
|
103
|
+
['src', 'README.md']
|
|
104
|
+
)
|
|
105
|
+
} finally {
|
|
106
|
+
fs.rmSync(root, { recursive: true, force: true })
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('readDirForIpc returns invalid-path for blank or non-string input', async () => {
|
|
111
|
+
let readdirCalls = 0
|
|
112
|
+
const fsImpl = {
|
|
113
|
+
promises: {
|
|
114
|
+
readdir: async () => {
|
|
115
|
+
readdirCalls += 1
|
|
116
|
+
return []
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
assert.deepEqual(await readDirForIpc('', { fs: fsImpl }), { entries: [], error: 'invalid-path' })
|
|
122
|
+
assert.deepEqual(await readDirForIpc(' ', { fs: fsImpl }), { entries: [], error: 'invalid-path' })
|
|
123
|
+
assert.deepEqual(await readDirForIpc(null, { fs: fsImpl }), { entries: [], error: 'invalid-path' })
|
|
124
|
+
assert.equal(readdirCalls, 0)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('readDirForIpc rejects Windows device paths before readdir', async () => {
|
|
128
|
+
let readdirCalls = 0
|
|
129
|
+
const fsImpl = {
|
|
130
|
+
promises: {
|
|
131
|
+
readdir: async () => {
|
|
132
|
+
readdirCalls += 1
|
|
133
|
+
return []
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
assert.deepEqual(await readDirForIpc('\\\\?\\C:\\secret', { fs: fsImpl }), {
|
|
139
|
+
entries: [],
|
|
140
|
+
error: 'device-path'
|
|
141
|
+
})
|
|
142
|
+
assert.equal(readdirCalls, 0)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('readDirForIpc returns filesystem error codes instead of throwing', async () => {
|
|
146
|
+
const root = mkTmpDir()
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const result = await readDirForIpc(path.join(root, 'missing'))
|
|
150
|
+
|
|
151
|
+
assert.deepEqual(result, { entries: [], error: 'ENOENT' })
|
|
152
|
+
} finally {
|
|
153
|
+
fs.rmSync(root, { recursive: true, force: true })
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
test('readDirForIpc marks a symlink to a directory as a directory', async t => {
|
|
158
|
+
const root = mkTmpDir()
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
fs.mkdirSync(path.join(root, 'actual-dir'))
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
fs.symlinkSync(path.join(root, 'actual-dir'), path.join(root, 'linked-dir'), 'dir')
|
|
165
|
+
} catch (error) {
|
|
166
|
+
if (error?.code === 'EPERM' || error?.code === 'EACCES') {
|
|
167
|
+
t.skip(`symlink creation is not permitted on this platform (${error.code})`)
|
|
168
|
+
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
throw error
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const result = await readDirForIpc(root)
|
|
176
|
+
const linked = result.entries.find(entry => entry.name === 'linked-dir')
|
|
177
|
+
|
|
178
|
+
assert.equal(result.error, undefined)
|
|
179
|
+
assert.equal(linked?.isDirectory, true)
|
|
180
|
+
} finally {
|
|
181
|
+
fs.rmSync(root, { recursive: true, force: true })
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test('readDirForIpc marks a Windows junction to a directory as a directory', async t => {
|
|
186
|
+
if (process.platform !== 'win32') {
|
|
187
|
+
t.skip('junctions are a Windows-specific symlink type')
|
|
188
|
+
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const root = mkTmpDir()
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
fs.mkdirSync(path.join(root, 'actual-dir'))
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
fs.symlinkSync(path.join(root, 'actual-dir'), path.join(root, 'junction-dir'), 'junction')
|
|
199
|
+
} catch (error) {
|
|
200
|
+
if (error?.code === 'EPERM' || error?.code === 'EACCES') {
|
|
201
|
+
t.skip(`junction creation is not permitted on this platform (${error.code})`)
|
|
202
|
+
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
throw error
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const result = await readDirForIpc(root)
|
|
210
|
+
const junction = result.entries.find(entry => entry.name === 'junction-dir')
|
|
211
|
+
|
|
212
|
+
assert.equal(result.error, undefined)
|
|
213
|
+
assert.equal(junction?.isDirectory, true)
|
|
214
|
+
} finally {
|
|
215
|
+
fs.rmSync(root, { recursive: true, force: true })
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
test('readDirForIpc allows expanding symlink or junction directories outside the project root', async t => {
|
|
220
|
+
const root = mkTmpDir()
|
|
221
|
+
const outside = mkTmpDir()
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
fs.writeFileSync(path.join(outside, 'outside.txt'), 'ok')
|
|
225
|
+
|
|
226
|
+
const linkPath = path.join(root, 'outside-link')
|
|
227
|
+
try {
|
|
228
|
+
fs.symlinkSync(outside, linkPath, process.platform === 'win32' ? 'junction' : 'dir')
|
|
229
|
+
} catch (error) {
|
|
230
|
+
if (error?.code === 'EPERM' || error?.code === 'EACCES') {
|
|
231
|
+
t.skip(`directory symlink creation is not permitted on this platform (${error.code})`)
|
|
232
|
+
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
throw error
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const result = await readDirForIpc(linkPath)
|
|
240
|
+
|
|
241
|
+
assert.equal(result.error, undefined)
|
|
242
|
+
assert.deepEqual(result.entries, [
|
|
243
|
+
{ name: 'outside.txt', path: path.join(linkPath, 'outside.txt'), isDirectory: false }
|
|
244
|
+
])
|
|
245
|
+
} finally {
|
|
246
|
+
fs.rmSync(root, { recursive: true, force: true })
|
|
247
|
+
fs.rmSync(outside, { recursive: true, force: true })
|
|
248
|
+
}
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
test('readDirForIpc stats symbolic links and unknown entries without dropping the whole listing', async () => {
|
|
252
|
+
const input = path.join('virtual-root')
|
|
253
|
+
const resolved = path.resolve(input)
|
|
254
|
+
const statCalls = []
|
|
255
|
+
const fsImpl = {
|
|
256
|
+
promises: {
|
|
257
|
+
readdir: async () => [
|
|
258
|
+
fakeDirent('unknown-entry'),
|
|
259
|
+
fakeDirent('linked-dir', { symlink: true }),
|
|
260
|
+
fakeDirent('broken-link', { symlink: true }),
|
|
261
|
+
fakeDirent('plain.txt', { file: true })
|
|
262
|
+
],
|
|
263
|
+
stat: async fullPath => {
|
|
264
|
+
if (fullPath === resolved) {
|
|
265
|
+
return { isDirectory: () => true }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
statCalls.push(fullPath)
|
|
269
|
+
if (fullPath.endsWith(`${path.sep}linked-dir`)) {
|
|
270
|
+
return { isDirectory: () => true }
|
|
271
|
+
}
|
|
272
|
+
throw Object.assign(new Error('gone'), { code: 'ENOENT' })
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const result = await readDirForIpc(input, { fs: fsImpl })
|
|
278
|
+
|
|
279
|
+
assert.equal(result.error, undefined)
|
|
280
|
+
assert.deepEqual(
|
|
281
|
+
statCalls.sort(),
|
|
282
|
+
[path.join(resolved, 'broken-link'), path.join(resolved, 'linked-dir'), path.join(resolved, 'unknown-entry')].sort()
|
|
283
|
+
)
|
|
284
|
+
assert.deepEqual(result.entries, [
|
|
285
|
+
{ name: 'linked-dir', path: path.join(resolved, 'linked-dir'), isDirectory: true },
|
|
286
|
+
{ name: 'broken-link', path: path.join(resolved, 'broken-link'), isDirectory: false },
|
|
287
|
+
{ name: 'plain.txt', path: path.join(resolved, 'plain.txt'), isDirectory: false },
|
|
288
|
+
{ name: 'unknown-entry', path: path.join(resolved, 'unknown-entry'), isDirectory: false }
|
|
289
|
+
])
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
test('readDirForIpc bounds concurrent stats while preserving complete sorted output', async () => {
|
|
293
|
+
const input = path.join('virtual-root')
|
|
294
|
+
const resolved = path.resolve(input)
|
|
295
|
+
const names = Array.from({ length: 105 }, (_, index) => `entry-${String(104 - index).padStart(3, '0')}`)
|
|
296
|
+
const failedName = 'entry-100'
|
|
297
|
+
const directoryNames = new Set(names.filter((_, index) => index % 10 === 4))
|
|
298
|
+
const successfulDirectoryNames = new Set([...directoryNames].filter(name => name !== failedName))
|
|
299
|
+
const statCalls = []
|
|
300
|
+
let active = 0
|
|
301
|
+
let peak = 0
|
|
302
|
+
let releaseStats
|
|
303
|
+
let markFirstStatStarted
|
|
304
|
+
const statsReleased = new Promise(resolve => {
|
|
305
|
+
releaseStats = resolve
|
|
306
|
+
})
|
|
307
|
+
const firstStatStarted = new Promise(resolve => {
|
|
308
|
+
markFirstStatStarted = resolve
|
|
309
|
+
})
|
|
310
|
+
const fsImpl = {
|
|
311
|
+
promises: {
|
|
312
|
+
readdir: async () => [
|
|
313
|
+
fakeDirent('node_modules', { symlink: true }),
|
|
314
|
+
...names.map((name, index) => fakeDirent(name, { symlink: index % 2 === 0 }))
|
|
315
|
+
],
|
|
316
|
+
stat: async fullPath => {
|
|
317
|
+
if (fullPath === resolved) {
|
|
318
|
+
return { isDirectory: () => true }
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
statCalls.push(fullPath)
|
|
322
|
+
active += 1
|
|
323
|
+
peak = Math.max(peak, active)
|
|
324
|
+
markFirstStatStarted()
|
|
325
|
+
await statsReleased
|
|
326
|
+
active -= 1
|
|
327
|
+
|
|
328
|
+
const name = path.basename(fullPath)
|
|
329
|
+
if (name === failedName) {
|
|
330
|
+
throw Object.assign(new Error('gone'), { code: 'ENOENT' })
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return { isDirectory: () => successfulDirectoryNames.has(name) }
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const resultPromise = readDirForIpc(input, { fs: fsImpl })
|
|
339
|
+
await firstStatStarted
|
|
340
|
+
await new Promise(resolve => setImmediate(resolve))
|
|
341
|
+
releaseStats()
|
|
342
|
+
const result = await resultPromise
|
|
343
|
+
|
|
344
|
+
const expectedNames = [
|
|
345
|
+
...names.filter(name => successfulDirectoryNames.has(name)).sort(),
|
|
346
|
+
...names.filter(name => !successfulDirectoryNames.has(name)).sort()
|
|
347
|
+
]
|
|
348
|
+
|
|
349
|
+
assert.equal(result.error, undefined)
|
|
350
|
+
assert.equal(result.entries.length, names.length)
|
|
351
|
+
assert.equal(statCalls.length, names.length)
|
|
352
|
+
assert.equal(statCalls.some(fullPath => fullPath.endsWith(`${path.sep}node_modules`)), false)
|
|
353
|
+
assert.ok(peak > 1, `expected concurrent stats, observed peak ${peak}`)
|
|
354
|
+
assert.ok(peak <= 16, `expected at most 16 concurrent stats, observed peak ${peak}`)
|
|
355
|
+
assert.deepEqual(
|
|
356
|
+
result.entries.map(entry => entry.name),
|
|
357
|
+
expectedNames
|
|
358
|
+
)
|
|
359
|
+
assert.equal(result.entries.find(entry => entry.name === failedName)?.isDirectory, false)
|
|
360
|
+
assert.equal(
|
|
361
|
+
result.entries.filter(entry => entry.isDirectory).length,
|
|
362
|
+
successfulDirectoryNames.size
|
|
363
|
+
)
|
|
364
|
+
})
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live WebSocket validation for the remote-gateway "Test remote" button.
|
|
3
|
+
*
|
|
4
|
+
* Background: the desktop boot does two independent things to a remote gateway:
|
|
5
|
+
*
|
|
6
|
+
* 1. The MAIN process hits ``GET /api/status`` over HTTP (token in a header)
|
|
7
|
+
* to confirm the backend is up. This is what "Test remote" historically
|
|
8
|
+
* checked, and what the boot logs print as "Remote NasTech backend is
|
|
9
|
+
* ready".
|
|
10
|
+
* 2. The RENDERER then opens a live WebSocket to ``/api/ws`` (credential in a
|
|
11
|
+
* query param) via ``gateway.connect()``. The chat surface only works once
|
|
12
|
+
* THIS succeeds.
|
|
13
|
+
*
|
|
14
|
+
* Those two paths use different processes, transports, and credentials, and the
|
|
15
|
+
* server applies extra guards to the WS upgrade that the HTTP status route never
|
|
16
|
+
* sees (Host/Origin checks, ws-ticket/token auth, peer-IP checks). So a gateway
|
|
17
|
+
* can pass the HTTP status check yet reject the WebSocket — which surfaces to
|
|
18
|
+
* the user as a green "Test remote" followed by an opaque "Could not connect to
|
|
19
|
+
* NasTech gateway" on the boot overlay.
|
|
20
|
+
*
|
|
21
|
+
* This module performs the second half of the check: it actually opens the WS
|
|
22
|
+
* URL and confirms the upgrade is accepted (and isn't immediately torn down by
|
|
23
|
+
* a post-upgrade auth rejection). The ``WebSocketImpl`` is injectable so the
|
|
24
|
+
* unit tests can drive the handshake without a real socket; in production the
|
|
25
|
+
* caller passes the Node/Electron global ``WebSocket``.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 10_000
|
|
29
|
+
// After the upgrade is accepted, a gateway that rejects the credential
|
|
30
|
+
// post-handshake closes the socket almost immediately. Wait a short grace
|
|
31
|
+
// window: a frame (gateway.ready) or a still-open socket means success; an
|
|
32
|
+
// early close means the upgrade was accepted but the session was refused.
|
|
33
|
+
const DEFAULT_READY_GRACE_MS = 750
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Attempt a live WebSocket connection and classify the outcome.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} wsUrl - Fully-formed ws(s):// URL including the credential.
|
|
39
|
+
* @param {object} [options]
|
|
40
|
+
* @param {new (url: string) => any} [options.WebSocketImpl] - WebSocket ctor.
|
|
41
|
+
* @param {number} [options.connectTimeoutMs]
|
|
42
|
+
* @param {number} [options.readyGraceMs]
|
|
43
|
+
* @returns {Promise<{ ok: boolean, reason?: string }>}
|
|
44
|
+
*/
|
|
45
|
+
function probeGatewayWebSocket(wsUrl, options = {}) {
|
|
46
|
+
const WebSocketImpl = options.WebSocketImpl
|
|
47
|
+
const connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS
|
|
48
|
+
const readyGraceMs = options.readyGraceMs ?? DEFAULT_READY_GRACE_MS
|
|
49
|
+
|
|
50
|
+
if (typeof WebSocketImpl !== 'function') {
|
|
51
|
+
return Promise.resolve({
|
|
52
|
+
ok: false,
|
|
53
|
+
reason: 'WebSocket is not available in this runtime.'
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return new Promise(resolve => {
|
|
58
|
+
let settled = false
|
|
59
|
+
let opened = false
|
|
60
|
+
let connectTimer = null
|
|
61
|
+
let graceTimer = null
|
|
62
|
+
let socket
|
|
63
|
+
|
|
64
|
+
const clearTimers = () => {
|
|
65
|
+
if (connectTimer !== null) {
|
|
66
|
+
clearTimeout(connectTimer)
|
|
67
|
+
connectTimer = null
|
|
68
|
+
}
|
|
69
|
+
if (graceTimer !== null) {
|
|
70
|
+
clearTimeout(graceTimer)
|
|
71
|
+
graceTimer = null
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const finish = result => {
|
|
76
|
+
if (settled) return
|
|
77
|
+
settled = true
|
|
78
|
+
clearTimers()
|
|
79
|
+
try {
|
|
80
|
+
socket?.close?.()
|
|
81
|
+
} catch {
|
|
82
|
+
// ignore — best effort teardown
|
|
83
|
+
}
|
|
84
|
+
resolve(result)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
socket = new WebSocketImpl(wsUrl)
|
|
89
|
+
} catch (error) {
|
|
90
|
+
finish({
|
|
91
|
+
ok: false,
|
|
92
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
93
|
+
})
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const onOpen = () => {
|
|
98
|
+
if (settled) return
|
|
99
|
+
opened = true
|
|
100
|
+
// Upgrade accepted. Give the server a brief window to reject the
|
|
101
|
+
// credential post-handshake (early close) before declaring success.
|
|
102
|
+
graceTimer = setTimeout(() => {
|
|
103
|
+
finish({ ok: true })
|
|
104
|
+
}, readyGraceMs)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const onMessage = () => {
|
|
108
|
+
// Any frame means the gateway accepted us and is talking — unambiguous
|
|
109
|
+
// success, no need to wait out the grace window.
|
|
110
|
+
finish({ ok: true })
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const onError = event => {
|
|
114
|
+
finish({
|
|
115
|
+
ok: false,
|
|
116
|
+
reason: extractErrorReason(event) || 'WebSocket connection failed.'
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const onClose = event => {
|
|
121
|
+
if (settled) return
|
|
122
|
+
if (opened) {
|
|
123
|
+
// Opened, then closed inside the grace window: the upgrade was accepted
|
|
124
|
+
// but the session was refused (e.g. ws-ticket/token rejected, or a
|
|
125
|
+
// server-side Host/Origin guard tripped after accept).
|
|
126
|
+
finish({
|
|
127
|
+
ok: false,
|
|
128
|
+
reason: closeReason(event, 'The gateway accepted the connection then closed it (credential rejected?).')
|
|
129
|
+
})
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
finish({
|
|
133
|
+
ok: false,
|
|
134
|
+
reason: closeReason(event, 'The gateway closed the WebSocket before it opened.')
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
addListener(socket, 'open', onOpen)
|
|
139
|
+
addListener(socket, 'message', onMessage)
|
|
140
|
+
addListener(socket, 'error', onError)
|
|
141
|
+
addListener(socket, 'close', onClose)
|
|
142
|
+
|
|
143
|
+
if (connectTimeoutMs > 0) {
|
|
144
|
+
connectTimer = setTimeout(() => {
|
|
145
|
+
finish({
|
|
146
|
+
ok: false,
|
|
147
|
+
reason: `Timed out after ${connectTimeoutMs}ms waiting for the WebSocket to open.`
|
|
148
|
+
})
|
|
149
|
+
}, connectTimeoutMs)
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function addListener(socket, type, handler) {
|
|
155
|
+
if (typeof socket.addEventListener === 'function') {
|
|
156
|
+
socket.addEventListener(type, handler)
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
// Node's global WebSocket implements addEventListener; this fallback keeps the
|
|
160
|
+
// helper usable with the `ws` package's EventEmitter shape too.
|
|
161
|
+
if (typeof socket.on === 'function') {
|
|
162
|
+
socket.on(type, handler)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function extractErrorReason(event) {
|
|
167
|
+
if (!event) return ''
|
|
168
|
+
if (event instanceof Error) return event.message
|
|
169
|
+
const err = event.error || event.message
|
|
170
|
+
if (err instanceof Error) return err.message
|
|
171
|
+
if (typeof err === 'string') return err
|
|
172
|
+
return ''
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function closeReason(event, fallback) {
|
|
176
|
+
const code = event && typeof event.code === 'number' ? event.code : null
|
|
177
|
+
const reason = event && typeof event.reason === 'string' ? event.reason.trim() : ''
|
|
178
|
+
if (code && reason) return `${fallback} (code ${code}: ${reason})`
|
|
179
|
+
if (code) return `${fallback} (code ${code})`
|
|
180
|
+
if (reason) return `${fallback} (${reason})`
|
|
181
|
+
return fallback
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = {
|
|
185
|
+
DEFAULT_CONNECT_TIMEOUT_MS,
|
|
186
|
+
DEFAULT_READY_GRACE_MS,
|
|
187
|
+
probeGatewayWebSocket
|
|
188
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for electron/gateway-ws-probe.cjs.
|
|
3
|
+
*
|
|
4
|
+
* Run with: node --test electron/gateway-ws-probe.test.cjs
|
|
5
|
+
* (Wired into npm test:desktop:platforms in package.json.)
|
|
6
|
+
*
|
|
7
|
+
* The probe drives a real WebSocket handshake for the "Test remote" button.
|
|
8
|
+
* Here we inject a fake socket so we can deterministically replay each handshake
|
|
9
|
+
* outcome (open, frame, error, early close, never-opens) without a network.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const test = require('node:test')
|
|
13
|
+
const assert = require('node:assert/strict')
|
|
14
|
+
|
|
15
|
+
const { probeGatewayWebSocket } = require('./gateway-ws-probe.cjs')
|
|
16
|
+
|
|
17
|
+
// Minimal WebSocket double: records listeners synchronously (the probe attaches
|
|
18
|
+
// them in its executor) and exposes emit() so the test can replay events.
|
|
19
|
+
function makeFakeWs() {
|
|
20
|
+
const instances = []
|
|
21
|
+
class FakeWs {
|
|
22
|
+
constructor(url) {
|
|
23
|
+
this.url = url
|
|
24
|
+
this.listeners = {}
|
|
25
|
+
this.closed = false
|
|
26
|
+
instances.push(this)
|
|
27
|
+
}
|
|
28
|
+
addEventListener(type, fn) {
|
|
29
|
+
;(this.listeners[type] ||= []).push(fn)
|
|
30
|
+
}
|
|
31
|
+
close() {
|
|
32
|
+
this.closed = true
|
|
33
|
+
}
|
|
34
|
+
emit(type, event) {
|
|
35
|
+
for (const fn of this.listeners[type] || []) fn(event)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { FakeWs, instances }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const FAST = { connectTimeoutMs: 1_000, readyGraceMs: 10 }
|
|
42
|
+
|
|
43
|
+
test('probe resolves ok when the socket opens and stays open', async () => {
|
|
44
|
+
const { FakeWs, instances } = makeFakeWs()
|
|
45
|
+
const promise = probeGatewayWebSocket('ws://host/api/ws?token=t', { WebSocketImpl: FakeWs, ...FAST })
|
|
46
|
+
instances[0].emit('open')
|
|
47
|
+
const result = await promise
|
|
48
|
+
assert.deepEqual(result, { ok: true })
|
|
49
|
+
assert.equal(instances[0].closed, true)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('probe resolves ok immediately when a frame arrives', async () => {
|
|
53
|
+
const { FakeWs, instances } = makeFakeWs()
|
|
54
|
+
const promise = probeGatewayWebSocket('ws://host/api/ws?token=t', {
|
|
55
|
+
WebSocketImpl: FakeWs,
|
|
56
|
+
connectTimeoutMs: 1_000,
|
|
57
|
+
readyGraceMs: 10_000 // long grace: success must come from the frame, not the timer
|
|
58
|
+
})
|
|
59
|
+
instances[0].emit('open')
|
|
60
|
+
instances[0].emit('message', { data: '{"jsonrpc":"2.0"}' })
|
|
61
|
+
const result = await promise
|
|
62
|
+
assert.deepEqual(result, { ok: true })
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('probe fails when the socket errors before opening', async () => {
|
|
66
|
+
const { FakeWs, instances } = makeFakeWs()
|
|
67
|
+
const promise = probeGatewayWebSocket('ws://host/api/ws?token=t', { WebSocketImpl: FakeWs, ...FAST })
|
|
68
|
+
instances[0].emit('error', { message: 'ECONNREFUSED' })
|
|
69
|
+
const result = await promise
|
|
70
|
+
assert.equal(result.ok, false)
|
|
71
|
+
assert.match(result.reason, /ECONNREFUSED/)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('probe fails when the gateway closes before opening', async () => {
|
|
75
|
+
const { FakeWs, instances } = makeFakeWs()
|
|
76
|
+
const promise = probeGatewayWebSocket('ws://host/api/ws?token=t', { WebSocketImpl: FakeWs, ...FAST })
|
|
77
|
+
instances[0].emit('close', { code: 1006 })
|
|
78
|
+
const result = await promise
|
|
79
|
+
assert.equal(result.ok, false)
|
|
80
|
+
assert.match(result.reason, /before it opened/)
|
|
81
|
+
assert.match(result.reason, /1006/)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('probe fails when the gateway accepts then immediately closes (auth rejected)', async () => {
|
|
85
|
+
const { FakeWs, instances } = makeFakeWs()
|
|
86
|
+
const promise = probeGatewayWebSocket('ws://host/api/ws?token=t', { WebSocketImpl: FakeWs, ...FAST })
|
|
87
|
+
instances[0].emit('open')
|
|
88
|
+
instances[0].emit('close', { code: 4403, reason: 'forbidden' })
|
|
89
|
+
const result = await promise
|
|
90
|
+
assert.equal(result.ok, false)
|
|
91
|
+
assert.match(result.reason, /credential rejected/)
|
|
92
|
+
assert.match(result.reason, /4403/)
|
|
93
|
+
assert.match(result.reason, /forbidden/)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('probe times out when the socket never opens', async () => {
|
|
97
|
+
const { FakeWs } = makeFakeWs()
|
|
98
|
+
const result = await probeGatewayWebSocket('ws://host/api/ws?token=t', {
|
|
99
|
+
WebSocketImpl: FakeWs,
|
|
100
|
+
connectTimeoutMs: 20,
|
|
101
|
+
readyGraceMs: 10
|
|
102
|
+
})
|
|
103
|
+
assert.equal(result.ok, false)
|
|
104
|
+
assert.match(result.reason, /Timed out/)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('probe fails gracefully when the constructor throws', async () => {
|
|
108
|
+
class ThrowingWs {
|
|
109
|
+
constructor() {
|
|
110
|
+
throw new Error('bad url')
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const result = await probeGatewayWebSocket('ws://host/api/ws', { WebSocketImpl: ThrowingWs, ...FAST })
|
|
114
|
+
assert.equal(result.ok, false)
|
|
115
|
+
assert.match(result.reason, /bad url/)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('probe reports unavailable when no WebSocket implementation is provided', async () => {
|
|
119
|
+
const result = await probeGatewayWebSocket('ws://host/api/ws', { WebSocketImpl: undefined })
|
|
120
|
+
assert.equal(result.ok, false)
|
|
121
|
+
assert.match(result.reason, /not available/)
|
|
122
|
+
})
|