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,22 @@
|
|
|
1
|
+
// Shared chrome for the top-center floating HUDs (command palette + session
|
|
2
|
+
// switcher). They pin just under the title bar, centered, and lean on a crisp
|
|
3
|
+
// border + shadow to separate from the app — no dimming/blurring backdrop.
|
|
4
|
+
// Each caller layers on its own z-index, width, and overflow.
|
|
5
|
+
export const HUD_POSITION = 'fixed left-1/2 top-3 -translate-x-1/2'
|
|
6
|
+
|
|
7
|
+
// Matches the app's borderless-overlay surface (dialog, keybind panel, …):
|
|
8
|
+
// hairline `--stroke-nastech` paired with the soft `--shadow-nastech` float.
|
|
9
|
+
export const HUD_SURFACE = 'rounded-xl border border-(--stroke-nastech) bg-(--ui-chat-bubble-background) shadow-nastech'
|
|
10
|
+
|
|
11
|
+
// One row/text size for both HUDs (compact — two notches under `text-sm`).
|
|
12
|
+
export const HUD_TEXT = 'text-xs'
|
|
13
|
+
|
|
14
|
+
// Shared item layout + padding for both HUDs. Tight vertical rhythm so rows
|
|
15
|
+
// don't feel chunky; overrides the shadcn `CommandItem` default (`px-2 py-1.5`).
|
|
16
|
+
export const HUD_ITEM = 'gap-2 px-2 py-1'
|
|
17
|
+
|
|
18
|
+
// Section headings styled like the sidebar panel labels: brand-tinted, uppercase,
|
|
19
|
+
// tightly tracked — plain text, no sticky chrome bar. Targets the cmdk group
|
|
20
|
+
// heading via the universal-descendant variant.
|
|
21
|
+
export const HUD_HEADING =
|
|
22
|
+
'**:[[cmdk-group-heading]]:static **:[[cmdk-group-heading]]:bg-transparent **:[[cmdk-group-heading]]:px-2.5 **:[[cmdk-group-heading]]:pb-1 **:[[cmdk-group-heading]]:pt-2.5 **:[[cmdk-group-heading]]:text-[0.64rem] **:[[cmdk-group-heading]]:font-semibold **:[[cmdk-group-heading]]:uppercase **:[[cmdk-group-heading]]:tracking-[0.16em] **:[[cmdk-group-heading]]:text-(--theme-primary)'
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { act, cleanup, render } from '@testing-library/react'
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { $desktopBoot } from '@/store/boot'
|
|
5
|
+
import { $gatewayState } from '@/store/session'
|
|
6
|
+
|
|
7
|
+
import { useGatewayBoot } from './use-gateway-boot'
|
|
8
|
+
|
|
9
|
+
// End-to-end-ish repro of the "remote VPS → stuck on CONNECTING, no Settings"
|
|
10
|
+
// bug that drives the REAL useGatewayBoot hook + REAL NasTechGateway through a
|
|
11
|
+
// fake WebSocket we fully control. No Docker / no real port: from the desktop's
|
|
12
|
+
// point of view a "remote VPS" is just a WebSocket that opens once and later
|
|
13
|
+
// refuses to reopen, so that is exactly (and only) what we fake.
|
|
14
|
+
//
|
|
15
|
+
// The previous test (gateway-connecting-overlay.test.tsx) hand-set the stores
|
|
16
|
+
// and asserted the overlays; this one proves the HOOK actually PRODUCES that
|
|
17
|
+
// stuck store combo — closing the "inferred by reading code" gap on the
|
|
18
|
+
// post-boot reconnect loop.
|
|
19
|
+
|
|
20
|
+
type Listener = (ev: unknown) => void
|
|
21
|
+
|
|
22
|
+
// Minimal WebSocket stand-in implementing only what json-rpc-gateway.connect()
|
|
23
|
+
// touches: readyState, add/removeEventListener('open'|'error'|'close'), close().
|
|
24
|
+
class FakeWebSocket {
|
|
25
|
+
static OPEN = 1
|
|
26
|
+
static CLOSED = 3
|
|
27
|
+
// Flipped by the test: 'open' = next socket connects; 'fail' = next socket
|
|
28
|
+
// errors (a dead remote). Mirrors a VPS going away after the first connect.
|
|
29
|
+
static mode: 'open' | 'fail' = 'open'
|
|
30
|
+
static instances: FakeWebSocket[] = []
|
|
31
|
+
|
|
32
|
+
readyState = 0
|
|
33
|
+
private listeners: Record<string, Set<Listener>> = {}
|
|
34
|
+
|
|
35
|
+
constructor(public url: string) {
|
|
36
|
+
FakeWebSocket.instances.push(this)
|
|
37
|
+
const willOpen = FakeWebSocket.mode === 'open'
|
|
38
|
+
// Resolve on the next microtask/macrotask so connect()'s promise wiring is
|
|
39
|
+
// in place before open/error fires (matches real async socket handshake).
|
|
40
|
+
setTimeout(() => {
|
|
41
|
+
if (willOpen) {
|
|
42
|
+
this.readyState = FakeWebSocket.OPEN
|
|
43
|
+
this.emit('open', {})
|
|
44
|
+
} else {
|
|
45
|
+
this.readyState = FakeWebSocket.CLOSED
|
|
46
|
+
this.emit('error', {})
|
|
47
|
+
}
|
|
48
|
+
}, 0)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
addEventListener(type: string, fn: Listener) {
|
|
52
|
+
;(this.listeners[type] ??= new Set()).add(fn)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
removeEventListener(type: string, fn: Listener) {
|
|
56
|
+
this.listeners[type]?.delete(fn)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
close() {
|
|
60
|
+
this.readyState = FakeWebSocket.CLOSED
|
|
61
|
+
this.emit('close', {})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Force-drop an open socket, as a sleeping laptop / restarted remote would.
|
|
65
|
+
drop() {
|
|
66
|
+
this.readyState = FakeWebSocket.CLOSED
|
|
67
|
+
this.emit('close', {})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private emit(type: string, ev: unknown) {
|
|
71
|
+
for (const fn of this.listeners[type] ?? []) fn(ev)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function fakeDesktop() {
|
|
76
|
+
const conn = {
|
|
77
|
+
authMode: 'token' as const,
|
|
78
|
+
baseUrl: 'https://vps.example.com',
|
|
79
|
+
profile: 'default',
|
|
80
|
+
token: 't',
|
|
81
|
+
wsUrl: 'wss://vps.example.com/api/ws?token=t'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
getConnection: vi.fn(async () => conn),
|
|
86
|
+
getGatewayWsUrl: vi.fn(async () => conn.wsUrl),
|
|
87
|
+
getBootProgress: vi.fn(async () => ({
|
|
88
|
+
error: null,
|
|
89
|
+
fakeMode: false,
|
|
90
|
+
message: '',
|
|
91
|
+
phase: 'init',
|
|
92
|
+
progress: 0,
|
|
93
|
+
running: true,
|
|
94
|
+
timestamp: Date.now()
|
|
95
|
+
})),
|
|
96
|
+
onBootProgress: vi.fn(() => () => undefined),
|
|
97
|
+
onBackendExit: vi.fn(() => () => undefined),
|
|
98
|
+
onPowerResume: vi.fn(() => () => undefined),
|
|
99
|
+
onWindowStateChanged: vi.fn(() => () => undefined),
|
|
100
|
+
touchBackend: vi.fn(async () => undefined),
|
|
101
|
+
profile: { get: vi.fn(async () => ({ profile: 'default' })) }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function Harness() {
|
|
106
|
+
useGatewayBoot({
|
|
107
|
+
handleGatewayEvent: () => undefined,
|
|
108
|
+
onConnectionReady: () => undefined,
|
|
109
|
+
onGatewayReady: () => undefined,
|
|
110
|
+
refreshNasTechConfig: async () => undefined,
|
|
111
|
+
refreshSessions: async () => undefined
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
return null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const originalWebSocket = globalThis.WebSocket
|
|
118
|
+
|
|
119
|
+
beforeEach(() => {
|
|
120
|
+
vi.useFakeTimers()
|
|
121
|
+
FakeWebSocket.mode = 'open'
|
|
122
|
+
FakeWebSocket.instances = []
|
|
123
|
+
;(globalThis as { WebSocket: unknown }).WebSocket = FakeWebSocket
|
|
124
|
+
;(window as { NASTECHDesktop?: unknown }).NASTECHDesktop = fakeDesktop()
|
|
125
|
+
$gatewayState.set('idle')
|
|
126
|
+
$desktopBoot.set({
|
|
127
|
+
error: null,
|
|
128
|
+
fakeMode: false,
|
|
129
|
+
message: '',
|
|
130
|
+
phase: 'init',
|
|
131
|
+
progress: 0,
|
|
132
|
+
running: true,
|
|
133
|
+
timestamp: Date.now(),
|
|
134
|
+
visible: true
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
afterEach(() => {
|
|
139
|
+
cleanup()
|
|
140
|
+
vi.useRealTimers()
|
|
141
|
+
;(globalThis as { WebSocket: unknown }).WebSocket = originalWebSocket
|
|
142
|
+
delete (window as { NASTECHDesktop?: unknown }).NASTECHDesktop
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
// Let pending microtasks (awaits) AND the queued 0ms socket open/error fire.
|
|
146
|
+
async function flushAsync() {
|
|
147
|
+
await act(async () => {
|
|
148
|
+
await vi.advanceTimersByTimeAsync(0)
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Drive the exponential backoff forward by its full cap so the next scheduled
|
|
153
|
+
// reconnect attempt actually runs (1s,2s,4s,8s,15s,15s…). Returns after the
|
|
154
|
+
// attempt's async work settles.
|
|
155
|
+
async function advanceBackoff() {
|
|
156
|
+
await act(async () => {
|
|
157
|
+
await vi.advanceTimersByTimeAsync(15_000)
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
describe('useGatewayBoot remote reconnect loop (real hook, fake socket)', () => {
|
|
162
|
+
it('INITIAL boot against a dead VPS: getConnection hangs (waitForNasTech) → app sits in the connecting combo, then fails', async () => {
|
|
163
|
+
// The report's actual path: a fresh launch pointed at an unreachable VPS.
|
|
164
|
+
// startNasTech()'s remote branch awaits waitForNasTech() for 45s before it
|
|
165
|
+
// throws, so the renderer's `await desktop.getConnection()` stays pending
|
|
166
|
+
// that whole window. During it: gatewayState is still 'idle' (connect was
|
|
167
|
+
// never reached) and boot.error is null → connecting=true → the fullscreen
|
|
168
|
+
// CONNECTING overlay, latched, blocking Settings.
|
|
169
|
+
let rejectConn: (e: Error) => void = () => undefined
|
|
170
|
+
const desktop = fakeDesktop()
|
|
171
|
+
desktop.getConnection = vi.fn(
|
|
172
|
+
() =>
|
|
173
|
+
new Promise((_resolve, reject) => {
|
|
174
|
+
rejectConn = reject
|
|
175
|
+
})
|
|
176
|
+
)
|
|
177
|
+
;(window as { NASTECHDesktop?: unknown }).NASTECHDesktop = desktop
|
|
178
|
+
|
|
179
|
+
render(<Harness />)
|
|
180
|
+
await flushAsync()
|
|
181
|
+
|
|
182
|
+
// getConnection is still pending — the dead-VPS wait. No socket was ever
|
|
183
|
+
// created, gatewayState never left idle, boot.error is null.
|
|
184
|
+
expect(FakeWebSocket.instances).toHaveLength(0)
|
|
185
|
+
expect($gatewayState.get()).not.toBe('open')
|
|
186
|
+
expect($desktopBoot.get().error).toBeNull()
|
|
187
|
+
// ^ connecting === true here → fullscreen CONNECTING, no Settings.
|
|
188
|
+
|
|
189
|
+
// After ~45s waitForNasTech gives up and getConnection rejects → boot()
|
|
190
|
+
// catch → failDesktopBoot → the BootFailureOverlay recovery surface.
|
|
191
|
+
await act(async () => {
|
|
192
|
+
rejectConn(new Error('NasTech backend did not become ready: timeout'))
|
|
193
|
+
await vi.advanceTimersByTimeAsync(0)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
expect($desktopBoot.get().error).toBeTruthy()
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('a remote that drops post-boot keeps looping with NO boot.error (the dead-end CONNECTING combo)', async () => {
|
|
200
|
+
render(<Harness />)
|
|
201
|
+
await flushAsync()
|
|
202
|
+
|
|
203
|
+
// Initial boot connected.
|
|
204
|
+
expect($gatewayState.get()).toBe('open')
|
|
205
|
+
expect($desktopBoot.get().error).toBeNull()
|
|
206
|
+
expect(FakeWebSocket.instances).toHaveLength(1)
|
|
207
|
+
|
|
208
|
+
// The remote VPS goes away: drop the live socket, and make every reopen
|
|
209
|
+
// fail from here on.
|
|
210
|
+
FakeWebSocket.mode = 'fail'
|
|
211
|
+
act(() => FakeWebSocket.instances[0].drop())
|
|
212
|
+
await flushAsync()
|
|
213
|
+
|
|
214
|
+
// Burn a couple backoff cycles BEFORE the escalation threshold (<6 attempts,
|
|
215
|
+
// ~the first ~15s). This is the window where stock and fixed behave the
|
|
216
|
+
// same: socket down, hook retrying, gatewayState non-open, boot.error still
|
|
217
|
+
// null → CONNECTING covers the screen with no recovery surface. (Past ~45s
|
|
218
|
+
// the fix raises boot.error; that's asserted in the next test.)
|
|
219
|
+
await advanceBackoff()
|
|
220
|
+
|
|
221
|
+
expect($gatewayState.get()).not.toBe('open')
|
|
222
|
+
expect($desktopBoot.get().error).toBeNull()
|
|
223
|
+
// It is actively retrying, not idle — more sockets were minted.
|
|
224
|
+
expect(FakeWebSocket.instances.length).toBeGreaterThan(1)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('FIX: after the prolonged drop the hook raises a recoverable boot error (the escape hatch)', async () => {
|
|
228
|
+
render(<Harness />)
|
|
229
|
+
await flushAsync()
|
|
230
|
+
expect($desktopBoot.get().error).toBeNull()
|
|
231
|
+
|
|
232
|
+
FakeWebSocket.mode = 'fail'
|
|
233
|
+
act(() => FakeWebSocket.instances[0].drop())
|
|
234
|
+
await flushAsync()
|
|
235
|
+
|
|
236
|
+
// Walk the backoff past the >=6 attempt threshold (~45s of failures).
|
|
237
|
+
for (let i = 0; i < 8; i += 1) {
|
|
238
|
+
await advanceBackoff()
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// The hook surfaced the recoverable error → BootFailureOverlay (Use local
|
|
242
|
+
// gateway / Sign in / Retry) becomes reachable instead of CONNECTING.
|
|
243
|
+
expect($desktopBoot.get().error).toBeTruthy()
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('FIX: a successful reconnect clears the recoverable error', async () => {
|
|
247
|
+
render(<Harness />)
|
|
248
|
+
await flushAsync()
|
|
249
|
+
|
|
250
|
+
FakeWebSocket.mode = 'fail'
|
|
251
|
+
act(() => FakeWebSocket.instances[0].drop())
|
|
252
|
+
await flushAsync()
|
|
253
|
+
for (let i = 0; i < 8; i += 1) {
|
|
254
|
+
await advanceBackoff()
|
|
255
|
+
}
|
|
256
|
+
expect($desktopBoot.get().error).toBeTruthy()
|
|
257
|
+
|
|
258
|
+
// The remote comes back: next reconnect attempt opens.
|
|
259
|
+
FakeWebSocket.mode = 'open'
|
|
260
|
+
await advanceBackoff()
|
|
261
|
+
|
|
262
|
+
expect($gatewayState.get()).toBe('open')
|
|
263
|
+
expect($desktopBoot.get().error).toBeNull()
|
|
264
|
+
})
|
|
265
|
+
})
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
import type { NasTechConnection } from '@/global'
|
|
4
|
+
import { NasTechGateway } from '@/nastech'
|
|
5
|
+
import { translateNow } from '@/i18n'
|
|
6
|
+
import { isGatewayReauthRequired, resolveGatewayWsUrl } from '@/lib/gateway-ws-url'
|
|
7
|
+
import {
|
|
8
|
+
$desktopBoot,
|
|
9
|
+
applyDesktopBootProgress,
|
|
10
|
+
completeDesktopBoot,
|
|
11
|
+
failDesktopBoot,
|
|
12
|
+
setDesktopBootStep
|
|
13
|
+
} from '@/store/boot'
|
|
14
|
+
import {
|
|
15
|
+
$gateway,
|
|
16
|
+
closeSecondaryGateways,
|
|
17
|
+
configureGatewayRegistry,
|
|
18
|
+
ensureGatewayForProfile,
|
|
19
|
+
pruneSecondaryGateways,
|
|
20
|
+
reconnectSecondaryGateways,
|
|
21
|
+
reportPrimaryGatewayState,
|
|
22
|
+
setPrimaryGateway,
|
|
23
|
+
touchSecondaryGateways
|
|
24
|
+
} from '@/store/gateway'
|
|
25
|
+
import { notify, notifyError } from '@/store/notifications'
|
|
26
|
+
import { $activeGatewayProfile, normalizeProfileKey, touchActiveGatewayBackend } from '@/store/profile'
|
|
27
|
+
import {
|
|
28
|
+
$attentionSessionIds,
|
|
29
|
+
$connection,
|
|
30
|
+
$sessions,
|
|
31
|
+
$workingSessionIds,
|
|
32
|
+
setConnection,
|
|
33
|
+
setSessionsLoading
|
|
34
|
+
} from '@/store/session'
|
|
35
|
+
import type { RpcEvent } from '@/types/nastech'
|
|
36
|
+
|
|
37
|
+
interface GatewayBootOptions {
|
|
38
|
+
handleGatewayEvent: (event: RpcEvent) => void
|
|
39
|
+
onConnectionReady: (
|
|
40
|
+
connection: Awaited<ReturnType<NonNullable<typeof window.NASTECHDesktop>['getConnection']>> | null
|
|
41
|
+
) => void
|
|
42
|
+
onGatewayReady: (gateway: NasTechGateway | null) => void
|
|
43
|
+
refreshNasTechConfig: () => Promise<void>
|
|
44
|
+
refreshSessions: () => Promise<void>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function useGatewayBoot({
|
|
48
|
+
handleGatewayEvent,
|
|
49
|
+
onConnectionReady,
|
|
50
|
+
onGatewayReady,
|
|
51
|
+
refreshNasTechConfig,
|
|
52
|
+
refreshSessions
|
|
53
|
+
}: GatewayBootOptions) {
|
|
54
|
+
const callbacksRef = useRef({
|
|
55
|
+
handleGatewayEvent,
|
|
56
|
+
onConnectionReady,
|
|
57
|
+
onGatewayReady,
|
|
58
|
+
refreshNasTechConfig,
|
|
59
|
+
refreshSessions
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
callbacksRef.current = {
|
|
63
|
+
handleGatewayEvent,
|
|
64
|
+
onConnectionReady,
|
|
65
|
+
onGatewayReady,
|
|
66
|
+
refreshNasTechConfig,
|
|
67
|
+
refreshSessions
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
let cancelled = false
|
|
72
|
+
const desktop = window.NASTECHDesktop
|
|
73
|
+
|
|
74
|
+
const publish = (next: NasTechConnection | null) => {
|
|
75
|
+
callbacksRef.current.onConnectionReady(next)
|
|
76
|
+
setConnection(next)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!desktop) {
|
|
80
|
+
failDesktopBoot('Desktop IPC bridge is unavailable.')
|
|
81
|
+
setSessionsLoading(false)
|
|
82
|
+
|
|
83
|
+
return () => void (cancelled = true)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// --- Reconnect-after-sleep machinery -------------------------------------
|
|
87
|
+
// macOS sleep silently drops the renderer's WebSocket. The backend Python
|
|
88
|
+
// process keeps running, but nothing re-opened the socket on wake, so the
|
|
89
|
+
// composer stayed disabled forever on "Starting NasTech...". Once the
|
|
90
|
+
// initial boot succeeds we treat any non-open state as recoverable and
|
|
91
|
+
// reconnect with backoff, and we nudge a reconnect on the OS/browser
|
|
92
|
+
// signals that fire around wake (power resume, network online, the window
|
|
93
|
+
// becoming visible).
|
|
94
|
+
let bootCompleted = false
|
|
95
|
+
let reconnecting = false
|
|
96
|
+
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
|
97
|
+
let reconnectAttempt = 0
|
|
98
|
+
// Surface "sign in again" once per disconnect episode, not on every backoff
|
|
99
|
+
// tick — a stale OAuth ticket fails every attempt and would otherwise stack
|
|
100
|
+
// identical error toasts (and their haptics). Reset on the next clean open.
|
|
101
|
+
let reauthNotified = false
|
|
102
|
+
|
|
103
|
+
// Wrap the live getter in a call so TS control-flow analysis doesn't narrow
|
|
104
|
+
// `connectionState` to a constant across the early-return guards (the state
|
|
105
|
+
// genuinely changes between reads).
|
|
106
|
+
const gatewayOpen = () => gateway.connectionState === 'open'
|
|
107
|
+
|
|
108
|
+
const clearReconnectTimer = () => {
|
|
109
|
+
if (reconnectTimer !== null) {
|
|
110
|
+
clearTimeout(reconnectTimer)
|
|
111
|
+
reconnectTimer = null
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const attemptReconnect = async () => {
|
|
116
|
+
if (cancelled || reconnecting || gatewayOpen()) {
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
reconnecting = true
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const conn = await desktop.getConnection($activeGatewayProfile.get())
|
|
124
|
+
|
|
125
|
+
if (cancelled) {
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
publish(conn)
|
|
130
|
+
// Re-mint the WS URL before reconnecting. OAuth tickets are single-use
|
|
131
|
+
// with a short TTL, so the ticket baked into the cached conn.wsUrl is
|
|
132
|
+
// dead on every reconnect after the initial boot — reusing it surfaces
|
|
133
|
+
// as an opaque "Could not connect to NasTech gateway". resolveGatewayWsUrl
|
|
134
|
+
// mints a fresh ticket (or throws a reauth error in OAuth mode rather
|
|
135
|
+
// than connecting with a stale one). For local/token gateways the URL
|
|
136
|
+
// carries a long-lived token and the re-mint is a cheap no-op.
|
|
137
|
+
const wsUrl = await resolveGatewayWsUrl(desktop, conn)
|
|
138
|
+
await gateway.connect(wsUrl)
|
|
139
|
+
|
|
140
|
+
if (cancelled) {
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
reconnectAttempt = 0
|
|
145
|
+
// Resync state that may have moved on the backend while we were asleep.
|
|
146
|
+
await callbacksRef.current.refreshNasTechConfig().catch(() => undefined)
|
|
147
|
+
await callbacksRef.current.refreshSessions().catch(() => undefined)
|
|
148
|
+
} catch (err) {
|
|
149
|
+
// OAuth session expired mid-reconnect: surface the actionable "sign in
|
|
150
|
+
// again" message once instead of silently looping the backoff against a
|
|
151
|
+
// ticket that can never succeed. Transport failures fall through to the
|
|
152
|
+
// backoff in the finally block below.
|
|
153
|
+
if (!cancelled && isGatewayReauthRequired(err) && !reauthNotified) {
|
|
154
|
+
reauthNotified = true
|
|
155
|
+
notifyError(err, translateNow('boot.errors.gatewaySignInRequired'))
|
|
156
|
+
}
|
|
157
|
+
} finally {
|
|
158
|
+
reconnecting = false
|
|
159
|
+
|
|
160
|
+
if (!cancelled && !gatewayOpen()) {
|
|
161
|
+
scheduleReconnect()
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function scheduleReconnect() {
|
|
167
|
+
if (cancelled || reconnecting || reconnectTimer !== null || gatewayOpen()) {
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 1s, 2s, 4s … capped at 15s.
|
|
172
|
+
const delay = Math.min(15_000, 1_000 * 2 ** Math.min(reconnectAttempt, 4))
|
|
173
|
+
reconnectAttempt += 1
|
|
174
|
+
reconnectTimer = setTimeout(() => {
|
|
175
|
+
reconnectTimer = null
|
|
176
|
+
void attemptReconnect()
|
|
177
|
+
}, delay)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const reconnectNow = () => {
|
|
181
|
+
if (cancelled || !bootCompleted) {
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
clearReconnectTimer()
|
|
186
|
+
reconnectAttempt = 0
|
|
187
|
+
reconnectSecondaryGateways()
|
|
188
|
+
|
|
189
|
+
if (!gatewayOpen()) {
|
|
190
|
+
void attemptReconnect()
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const offBootProgress = desktop.onBootProgress(payload => applyDesktopBootProgress(payload))
|
|
195
|
+
void desktop
|
|
196
|
+
.getBootProgress()
|
|
197
|
+
.then(snapshot => applyDesktopBootProgress(snapshot))
|
|
198
|
+
.catch(() => undefined)
|
|
199
|
+
|
|
200
|
+
setDesktopBootStep({
|
|
201
|
+
phase: 'renderer.boot',
|
|
202
|
+
message: translateNow('boot.steps.startingDesktopConnection'),
|
|
203
|
+
progress: 6
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
const gateway = new NasTechGateway()
|
|
207
|
+
callbacksRef.current.onGatewayReady(gateway)
|
|
208
|
+
setPrimaryGateway(gateway, normalizeProfileKey($activeGatewayProfile.get()))
|
|
209
|
+
// Secondary (background-profile) sockets funnel into the same handler.
|
|
210
|
+
configureGatewayRegistry({ onEvent: event => callbacksRef.current.handleGatewayEvent(event) })
|
|
211
|
+
|
|
212
|
+
const offState = gateway.onState(st => {
|
|
213
|
+
// Mirror to the composer only while the primary is the active profile —
|
|
214
|
+
// a background secondary reconnect mustn't flip the foreground state.
|
|
215
|
+
reportPrimaryGatewayState(st)
|
|
216
|
+
|
|
217
|
+
if (st === 'open') {
|
|
218
|
+
reconnectAttempt = 0
|
|
219
|
+
reauthNotified = false
|
|
220
|
+
clearReconnectTimer()
|
|
221
|
+
} else if (bootCompleted && (st === 'closed' || st === 'error')) {
|
|
222
|
+
// The socket dropped after a healthy boot (typically sleep/wake). Try
|
|
223
|
+
// to bring it back instead of leaving the composer stuck disabled.
|
|
224
|
+
scheduleReconnect()
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
const offEvent = gateway.onEvent(event => callbacksRef.current.handleGatewayEvent(event))
|
|
229
|
+
|
|
230
|
+
// Wake signals: power resume (macOS/Windows), network coming back, and the
|
|
231
|
+
// window regaining focus/visibility. Each nudges an immediate reconnect.
|
|
232
|
+
const offPowerResume = desktop.onPowerResume?.(() => reconnectNow())
|
|
233
|
+
|
|
234
|
+
const onOnline = () => reconnectNow()
|
|
235
|
+
|
|
236
|
+
const onVisible = () => {
|
|
237
|
+
if (document.visibilityState === 'visible') {
|
|
238
|
+
reconnectNow()
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
window.addEventListener('online', onOnline)
|
|
243
|
+
document.addEventListener('visibilitychange', onVisible)
|
|
244
|
+
|
|
245
|
+
// Keep live pool backends alive while this window is open (the main process
|
|
246
|
+
// can't observe the direct renderer↔backend WS). No-op for the primary.
|
|
247
|
+
const keepaliveTimer = setInterval(() => {
|
|
248
|
+
touchActiveGatewayBackend()
|
|
249
|
+
touchSecondaryGateways()
|
|
250
|
+
}, 60_000)
|
|
251
|
+
|
|
252
|
+
// Bound concurrency cost to live work: keep a background socket only while
|
|
253
|
+
// its profile has a running (working) or blocked (needs-input) session.
|
|
254
|
+
// Once that profile goes idle its socket is dropped and its backend is free
|
|
255
|
+
// to idle-reap. The active profile is always spared.
|
|
256
|
+
const recomputeKeptGateways = () => {
|
|
257
|
+
const live = new Set([...$workingSessionIds.get(), ...$attentionSessionIds.get()])
|
|
258
|
+
const keep = new Set<string>()
|
|
259
|
+
|
|
260
|
+
for (const session of $sessions.get()) {
|
|
261
|
+
if (live.has(session.id)) {
|
|
262
|
+
keep.add(normalizeProfileKey(session.profile))
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
pruneSecondaryGateways(keep)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const offWorking = $workingSessionIds.subscribe(() => recomputeKeptGateways())
|
|
270
|
+
const offAttention = $attentionSessionIds.subscribe(() => recomputeKeptGateways())
|
|
271
|
+
const offActiveProfile = $activeGatewayProfile.subscribe(() => recomputeKeptGateways())
|
|
272
|
+
|
|
273
|
+
const offWindowState = desktop.onWindowStateChanged?.(payload => {
|
|
274
|
+
const current = $connection.get()
|
|
275
|
+
|
|
276
|
+
if (current) {
|
|
277
|
+
publish({ ...current, ...payload })
|
|
278
|
+
}
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
const offExit = desktop.onBackendExit(() => {
|
|
282
|
+
if ($desktopBoot.get().running || $desktopBoot.get().visible) {
|
|
283
|
+
failDesktopBoot(translateNow('boot.errors.backgroundExitedDuringStartup'))
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
notify({
|
|
287
|
+
kind: 'error',
|
|
288
|
+
title: translateNow('boot.errors.backendStopped'),
|
|
289
|
+
message: translateNow('boot.errors.backgroundExited'),
|
|
290
|
+
durationMs: 0
|
|
291
|
+
})
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
async function boot() {
|
|
295
|
+
try {
|
|
296
|
+
const conn = await desktop.getConnection()
|
|
297
|
+
|
|
298
|
+
if (cancelled) {
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
setDesktopBootStep({
|
|
303
|
+
phase: 'renderer.gateway.connect',
|
|
304
|
+
message: translateNow('boot.steps.connectingGateway'),
|
|
305
|
+
progress: 95
|
|
306
|
+
})
|
|
307
|
+
publish(conn)
|
|
308
|
+
// Mint a fresh WS URL right before connecting. For OAuth gateways the
|
|
309
|
+
// ticket is single-use with a short TTL, so the ticket baked into
|
|
310
|
+
// conn.wsUrl is stale; resolveGatewayWsUrl() re-mints it and, on
|
|
311
|
+
// failure, throws a reauth error rather than connecting with a dead
|
|
312
|
+
// ticket (which would surface as an opaque "connection closed").
|
|
313
|
+
const wsUrl = await resolveGatewayWsUrl(desktop, conn)
|
|
314
|
+
await gateway.connect(wsUrl)
|
|
315
|
+
|
|
316
|
+
if (cancelled) {
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Record which profile the primary (window) backend booted as, so
|
|
321
|
+
// same-profile resumes are no-op swaps and any reconnect targets the
|
|
322
|
+
// right backend. Best-effort: a missing preference means "default".
|
|
323
|
+
try {
|
|
324
|
+
const pref = await desktop.profile?.get?.()
|
|
325
|
+
const profileKey = (pref?.profile ?? '').trim() || 'default'
|
|
326
|
+
$activeGatewayProfile.set(profileKey)
|
|
327
|
+
setPrimaryGateway(gateway, profileKey)
|
|
328
|
+
void ensureGatewayForProfile(profileKey)
|
|
329
|
+
} catch {
|
|
330
|
+
$activeGatewayProfile.set('default')
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
setDesktopBootStep({
|
|
334
|
+
phase: 'renderer.config',
|
|
335
|
+
message: translateNow('boot.steps.loadingSettings'),
|
|
336
|
+
progress: 97
|
|
337
|
+
})
|
|
338
|
+
await callbacksRef.current.refreshNasTechConfig()
|
|
339
|
+
|
|
340
|
+
if (cancelled) {
|
|
341
|
+
return
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
setDesktopBootStep({
|
|
345
|
+
phase: 'renderer.sessions',
|
|
346
|
+
message: translateNow('boot.steps.loadingSessions'),
|
|
347
|
+
progress: 99
|
|
348
|
+
})
|
|
349
|
+
await callbacksRef.current.refreshSessions()
|
|
350
|
+
completeDesktopBoot()
|
|
351
|
+
bootCompleted = true
|
|
352
|
+
} catch (err) {
|
|
353
|
+
if (!cancelled) {
|
|
354
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
355
|
+
failDesktopBoot(message)
|
|
356
|
+
notifyError(err, translateNow('boot.errors.desktopBootFailed'))
|
|
357
|
+
setSessionsLoading(false)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
void boot()
|
|
363
|
+
|
|
364
|
+
return () => {
|
|
365
|
+
cancelled = true
|
|
366
|
+
clearReconnectTimer()
|
|
367
|
+
clearInterval(keepaliveTimer)
|
|
368
|
+
offWorking()
|
|
369
|
+
offAttention()
|
|
370
|
+
offActiveProfile()
|
|
371
|
+
window.removeEventListener('online', onOnline)
|
|
372
|
+
document.removeEventListener('visibilitychange', onVisible)
|
|
373
|
+
offPowerResume?.()
|
|
374
|
+
offState()
|
|
375
|
+
offEvent()
|
|
376
|
+
offExit()
|
|
377
|
+
offWindowState?.()
|
|
378
|
+
offBootProgress()
|
|
379
|
+
closeSecondaryGateways()
|
|
380
|
+
gateway.close()
|
|
381
|
+
publish(null)
|
|
382
|
+
callbacksRef.current.onGatewayReady(null)
|
|
383
|
+
setPrimaryGateway(null)
|
|
384
|
+
$gateway.set(null)
|
|
385
|
+
}
|
|
386
|
+
}, [])
|
|
387
|
+
}
|