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,77 @@
|
|
|
1
|
+
import { Component, type ErrorInfo, type ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
import { Button } from '@/components/ui/button'
|
|
4
|
+
import { ErrorState } from '@/components/ui/error-state'
|
|
5
|
+
import { useI18n } from '@/i18n'
|
|
6
|
+
|
|
7
|
+
export interface ErrorBoundaryFallbackProps {
|
|
8
|
+
error: Error
|
|
9
|
+
reset: () => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ErrorBoundaryProps {
|
|
13
|
+
children: ReactNode
|
|
14
|
+
fallback?: (props: ErrorBoundaryFallbackProps) => ReactNode
|
|
15
|
+
label?: string
|
|
16
|
+
onError?: (error: Error, info: ErrorInfo) => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ErrorBoundaryState {
|
|
20
|
+
error: Error | null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
24
|
+
state: ErrorBoundaryState = { error: null }
|
|
25
|
+
|
|
26
|
+
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
|
27
|
+
return { error }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
componentDidCatch(error: Error, info: ErrorInfo) {
|
|
31
|
+
const tag = this.props.label ? `[error-boundary:${this.props.label}]` : '[error-boundary]'
|
|
32
|
+
console.error(tag, error, info.componentStack)
|
|
33
|
+
this.props.onError?.(error, info)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
reset = () => {
|
|
37
|
+
this.setState({ error: null })
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
render() {
|
|
41
|
+
const { error } = this.state
|
|
42
|
+
|
|
43
|
+
if (!error) {
|
|
44
|
+
return this.props.children
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (this.props.fallback) {
|
|
48
|
+
return this.props.fallback({ error, reset: this.reset })
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return <RootErrorFallback error={error} reset={this.reset} />
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function RootErrorFallback({ error, reset }: ErrorBoundaryFallbackProps) {
|
|
56
|
+
const { t } = useI18n()
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="fixed inset-0 z-[1500] grid place-items-center bg-(--ui-chat-surface-background) p-6">
|
|
60
|
+
<ErrorState
|
|
61
|
+
className="w-full max-w-[28rem]"
|
|
62
|
+
description={error.message || t.errors.boundaryDesc}
|
|
63
|
+
title={t.errors.boundaryTitle}
|
|
64
|
+
>
|
|
65
|
+
<Button className="font-semibold" onClick={reset} size="lg">
|
|
66
|
+
{t.common.retry}
|
|
67
|
+
</Button>
|
|
68
|
+
<Button onClick={() => window.location.reload()} variant="text">
|
|
69
|
+
{t.errors.reloadWindow}
|
|
70
|
+
</Button>
|
|
71
|
+
<Button onClick={() => void window.NASTECHDesktop?.revealLogs()?.catch(() => undefined)} variant="text">
|
|
72
|
+
{t.errors.openLogs}
|
|
73
|
+
</Button>
|
|
74
|
+
</ErrorState>
|
|
75
|
+
</div>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { cleanup, render, screen } from '@testing-library/react'
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { $desktopBoot } from '@/store/boot'
|
|
5
|
+
import { $desktopOnboarding } from '@/store/onboarding'
|
|
6
|
+
import { $gatewayState, setGatewayState } from '@/store/session'
|
|
7
|
+
|
|
8
|
+
import { BootFailureOverlay } from './boot-failure-overlay'
|
|
9
|
+
import { GatewayConnectingOverlay } from './gateway-connecting-overlay'
|
|
10
|
+
|
|
11
|
+
// Repro for the "remote gateway → stuck on CONNECTING, no way to settings"
|
|
12
|
+
// report. The connecting overlay (z-1200, full-screen, pointer-events on) is
|
|
13
|
+
// shown whenever `gatewayState !== 'open' && !boot.error`. The ONLY escape
|
|
14
|
+
// hatch — BootFailureOverlay, which has "Use local gateway" / "Sign in" /
|
|
15
|
+
// "Retry" — only renders when `boot.error` is set.
|
|
16
|
+
//
|
|
17
|
+
// useGatewayBoot only calls failDesktopBoot() (which sets boot.error) when the
|
|
18
|
+
// INITIAL boot() throws. After the first successful connect (bootCompleted),
|
|
19
|
+
// any later socket drop goes through scheduleReconnect(), which loops FOREVER
|
|
20
|
+
// against the dead remote and never sets boot.error. So gatewayState sits at
|
|
21
|
+
// 'closed'/'error' with boot.error null → CONNECTING forever, recovery overlay
|
|
22
|
+
// never appears, settings unreachable.
|
|
23
|
+
|
|
24
|
+
function resetStores() {
|
|
25
|
+
setGatewayState('idle')
|
|
26
|
+
$desktopBoot.set({
|
|
27
|
+
error: null,
|
|
28
|
+
fakeMode: false,
|
|
29
|
+
message: 'ready',
|
|
30
|
+
phase: 'renderer.ready',
|
|
31
|
+
progress: 100,
|
|
32
|
+
running: false,
|
|
33
|
+
timestamp: Date.now(),
|
|
34
|
+
visible: false
|
|
35
|
+
})
|
|
36
|
+
$desktopOnboarding.set({
|
|
37
|
+
configured: true,
|
|
38
|
+
flow: { status: 'idle' },
|
|
39
|
+
mode: 'oauth',
|
|
40
|
+
providers: null,
|
|
41
|
+
reason: null,
|
|
42
|
+
requested: false,
|
|
43
|
+
firstRunSkipped: false,
|
|
44
|
+
manual: false
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
beforeEach(resetStores)
|
|
49
|
+
afterEach(cleanup)
|
|
50
|
+
|
|
51
|
+
// The connecting overlay renders "CONN" + a scrambled tail inside one
|
|
52
|
+
// uppercase span; match that node specifically so the recovery overlay's
|
|
53
|
+
// "Lost connection…" copy doesn't read as a false positive.
|
|
54
|
+
const isConnectingShown = () =>
|
|
55
|
+
screen.queryAllByText((_, el) => /^CONN[/\\|\-_=+<>~:*A-Z]*$/.test(el?.textContent?.trim() ?? '')).length > 0
|
|
56
|
+
const isRecoveryShown = () =>
|
|
57
|
+
Boolean(screen.queryByText(/use local gateway/i) || screen.queryByText(/retry/i) || screen.queryByText(/sign in/i))
|
|
58
|
+
|
|
59
|
+
describe('connecting overlay vs recovery surface', () => {
|
|
60
|
+
it('hard initial-boot failure surfaces the recovery overlay (the working path)', () => {
|
|
61
|
+
// failDesktopBoot() ran: error set, gateway never opened.
|
|
62
|
+
$desktopBoot.set({ ...$desktopBoot.get(), error: 'NasTech backend did not become ready', running: false, visible: true })
|
|
63
|
+
setGatewayState('error')
|
|
64
|
+
|
|
65
|
+
render(
|
|
66
|
+
<>
|
|
67
|
+
<GatewayConnectingOverlay />
|
|
68
|
+
<BootFailureOverlay />
|
|
69
|
+
</>
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
expect(isRecoveryShown()).toBe(true)
|
|
73
|
+
// Connecting overlay bows out when boot.error is set.
|
|
74
|
+
expect(isConnectingShown()).toBe(false)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('REPRO: remote socket drops AFTER a successful boot → stuck on CONNECTING, no recovery, no settings', () => {
|
|
78
|
+
// 1. Initial boot succeeded: gateway opened, boot completed (no error).
|
|
79
|
+
setGatewayState('open')
|
|
80
|
+
const { rerender } = render(
|
|
81
|
+
<>
|
|
82
|
+
<GatewayConnectingOverlay />
|
|
83
|
+
<BootFailureOverlay />
|
|
84
|
+
</>
|
|
85
|
+
)
|
|
86
|
+
expect(isConnectingShown()).toBe(false)
|
|
87
|
+
|
|
88
|
+
// 2. The remote VPS socket drops (sleep/wake, remote restart, network).
|
|
89
|
+
// bootCompleted is true, so useGatewayBoot routes this through
|
|
90
|
+
// scheduleReconnect() — boot.error stays NULL.
|
|
91
|
+
setGatewayState('closed')
|
|
92
|
+
rerender(
|
|
93
|
+
<>
|
|
94
|
+
<GatewayConnectingOverlay />
|
|
95
|
+
<BootFailureOverlay />
|
|
96
|
+
</>
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
// The connecting overlay reappears and latches...
|
|
100
|
+
expect(isConnectingShown()).toBe(true)
|
|
101
|
+
// ...with NO recovery surface, because boot.error was never set.
|
|
102
|
+
expect(isRecoveryShown()).toBe(false)
|
|
103
|
+
|
|
104
|
+
// 3. Reconnect loops forever against the dead remote: gatewayState bounces
|
|
105
|
+
// closed → error → closed, boot.error never gets set. The user is
|
|
106
|
+
// pinned on CONNECTING with no path to Settings indefinitely.
|
|
107
|
+
setGatewayState('error')
|
|
108
|
+
rerender(
|
|
109
|
+
<>
|
|
110
|
+
<GatewayConnectingOverlay />
|
|
111
|
+
<BootFailureOverlay />
|
|
112
|
+
</>
|
|
113
|
+
)
|
|
114
|
+
expect($desktopBoot.get().error).toBeNull()
|
|
115
|
+
expect(isConnectingShown()).toBe(true)
|
|
116
|
+
expect(isRecoveryShown()).toBe(false)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('FIX: once the prolonged reconnect raises a recoverable boot error, the recovery overlay takes over', () => {
|
|
120
|
+
// Mirrors what useGatewayBoot.scheduleReconnect() now does after ~45s of
|
|
121
|
+
// failed post-boot reconnects: it calls failDesktopBoot(), flipping the UI
|
|
122
|
+
// from the dead-end CONNECTING overlay to the recovery surface.
|
|
123
|
+
setGatewayState('error')
|
|
124
|
+
$desktopBoot.set({
|
|
125
|
+
...$desktopBoot.get(),
|
|
126
|
+
error: 'Lost connection to the NasTech gateway and could not reconnect.',
|
|
127
|
+
running: false,
|
|
128
|
+
visible: true
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
render(
|
|
132
|
+
<>
|
|
133
|
+
<GatewayConnectingOverlay />
|
|
134
|
+
<BootFailureOverlay />
|
|
135
|
+
</>
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
// Escape hatch is now reachable; the connecting overlay bows out.
|
|
139
|
+
expect(isRecoveryShown()).toBe(true)
|
|
140
|
+
expect(screen.getByText(/use local gateway/i)).toBeTruthy()
|
|
141
|
+
expect(isConnectingShown()).toBe(false)
|
|
142
|
+
})
|
|
143
|
+
})
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { useStore } from '@nanostores/react'
|
|
2
|
+
import { useEffect, useRef, useState } from 'react'
|
|
3
|
+
|
|
4
|
+
import { cn } from '@/lib/utils'
|
|
5
|
+
import { $desktopBoot } from '@/store/boot'
|
|
6
|
+
import { $gatewayState } from '@/store/session'
|
|
7
|
+
|
|
8
|
+
// Static, always-legible prefix; only TAIL ever scrambles. Splitting them at
|
|
9
|
+
// the render level means no timer logic (even a stale HMR one) can ever
|
|
10
|
+
// scramble "CONN".
|
|
11
|
+
const PREFIX = 'CONN'
|
|
12
|
+
const TAIL = 'ECTING'
|
|
13
|
+
// Even-weight mono ascii so cycling glyphs don't jump width (matches the
|
|
14
|
+
// nousnet-web download-button decode effect).
|
|
15
|
+
const SCRAMBLE_CHARS = '/\\|-_=+<>~:*'
|
|
16
|
+
const TICK_MS = 45
|
|
17
|
+
|
|
18
|
+
// Exit choreography (ms): text fades down + out, hold, then the overlay fades.
|
|
19
|
+
const TEXT_OUT_MS = 360
|
|
20
|
+
const POST_TEXT_HOLD_MS = 300
|
|
21
|
+
const OVERLAY_OUT_MS = 520
|
|
22
|
+
// Preview-only: how long to "connect" for, and the pause before replaying.
|
|
23
|
+
const PREVIEW_CONNECT_MS = 2600
|
|
24
|
+
const PREVIEW_REPLAY_MS = 1100
|
|
25
|
+
|
|
26
|
+
type Phase = 'live' | 'text-out' | 'overlay-out' | 'gone'
|
|
27
|
+
|
|
28
|
+
// Dev affordance: a warm Cmd+R reconnects almost instantly, so the overlay
|
|
29
|
+
// only flashes. Load with `?connecting=1` to force a looping preview.
|
|
30
|
+
function forcedPreview(): boolean {
|
|
31
|
+
if (!import.meta.env.DEV || typeof window === 'undefined') {
|
|
32
|
+
return false
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
return new URLSearchParams(window.location.search).get('connecting') === '1'
|
|
37
|
+
} catch {
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function scrambledTail(resolvedCount: number): string {
|
|
43
|
+
return Array.from(TAIL, (ch, i) =>
|
|
44
|
+
i < resolvedCount ? ch : SCRAMBLE_CHARS[(Math.random() * SCRAMBLE_CHARS.length) | 0]
|
|
45
|
+
).join('')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function GatewayConnectingOverlay() {
|
|
49
|
+
const gatewayState = useStore($gatewayState)
|
|
50
|
+
const boot = useStore($desktopBoot)
|
|
51
|
+
const [previewing] = useState(forcedPreview)
|
|
52
|
+
const [tail, setTail] = useState(TAIL)
|
|
53
|
+
const [phase, setPhase] = useState<Phase>('live')
|
|
54
|
+
|
|
55
|
+
const connecting = gatewayState !== 'open' && !boot.error
|
|
56
|
+
// Latches once we've actually shown the overlay, so the brief frame where
|
|
57
|
+
// gatewayState flips to "open" (connecting -> false) before the exit phase
|
|
58
|
+
// kicks in doesn't unmount us and cause a flash.
|
|
59
|
+
const shownRef = useRef(false)
|
|
60
|
+
|
|
61
|
+
if (previewing || connecting) {
|
|
62
|
+
shownRef.current = true
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Decode loop — only while live (freeze the resolved word during the exit).
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (phase !== 'live' || (!previewing && !connecting)) {
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let resolved = 0
|
|
72
|
+
let hold = 0
|
|
73
|
+
|
|
74
|
+
const id = window.setInterval(() => {
|
|
75
|
+
if (resolved >= TAIL.length) {
|
|
76
|
+
hold += 1
|
|
77
|
+
|
|
78
|
+
if (hold > 16) {
|
|
79
|
+
resolved = 0
|
|
80
|
+
hold = 0
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
setTail(TAIL)
|
|
84
|
+
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
resolved += 0.5
|
|
89
|
+
setTail(scrambledTail(Math.floor(resolved)))
|
|
90
|
+
}, TICK_MS)
|
|
91
|
+
|
|
92
|
+
return () => window.clearInterval(id)
|
|
93
|
+
}, [phase, previewing, connecting])
|
|
94
|
+
|
|
95
|
+
// Kick off the exit when connected: real connect, or a faked timer in preview.
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (phase !== 'live') {
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (previewing) {
|
|
102
|
+
const id = window.setTimeout(() => {
|
|
103
|
+
setTail(TAIL)
|
|
104
|
+
setPhase('text-out')
|
|
105
|
+
}, PREVIEW_CONNECT_MS)
|
|
106
|
+
|
|
107
|
+
return () => window.clearTimeout(id)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (gatewayState === 'open' && shownRef.current) {
|
|
111
|
+
setTail(TAIL)
|
|
112
|
+
setPhase('text-out')
|
|
113
|
+
}
|
|
114
|
+
}, [phase, previewing, gatewayState])
|
|
115
|
+
|
|
116
|
+
// Advance the exit choreography: text-out -> overlay-out -> gone.
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
if (phase === 'text-out') {
|
|
119
|
+
const id = window.setTimeout(() => setPhase('overlay-out'), TEXT_OUT_MS + POST_TEXT_HOLD_MS)
|
|
120
|
+
|
|
121
|
+
return () => window.clearTimeout(id)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (phase === 'overlay-out') {
|
|
125
|
+
const id = window.setTimeout(() => setPhase('gone'), OVERLAY_OUT_MS)
|
|
126
|
+
|
|
127
|
+
return () => window.clearTimeout(id)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Preview replays so we can keep watching the transition.
|
|
131
|
+
if (phase === 'gone' && previewing) {
|
|
132
|
+
const id = window.setTimeout(() => {
|
|
133
|
+
setTail(TAIL)
|
|
134
|
+
setPhase('live')
|
|
135
|
+
}, PREVIEW_REPLAY_MS)
|
|
136
|
+
|
|
137
|
+
return () => window.clearTimeout(id)
|
|
138
|
+
}
|
|
139
|
+
}, [phase, previewing])
|
|
140
|
+
|
|
141
|
+
// Boot failed — BootFailureOverlay owns the screen; don't linger behind it.
|
|
142
|
+
if (boot.error && !previewing) {
|
|
143
|
+
return null
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Real connect: once the fade finishes, get out of the way for good.
|
|
147
|
+
if (phase === 'gone' && !previewing) {
|
|
148
|
+
return null
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Never showed (e.g. gateway already up on a warm reload) — stay out.
|
|
152
|
+
if (!previewing && !connecting && !shownRef.current) {
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const leaving = phase !== 'live'
|
|
157
|
+
const overlayHidden = phase === 'overlay-out' || phase === 'gone'
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<div
|
|
161
|
+
className={cn(
|
|
162
|
+
'fixed inset-0 z-[1200] grid place-items-center bg-(--ui-chat-surface-background) transition-opacity duration-500 ease-out',
|
|
163
|
+
overlayHidden ? 'pointer-events-none opacity-0' : 'opacity-100'
|
|
164
|
+
)}
|
|
165
|
+
>
|
|
166
|
+
<style>{'@keyframes gco-cursor { 0%, 49% { opacity: 1 } 50%, 100% { opacity: 0 } }'}</style>
|
|
167
|
+
<span
|
|
168
|
+
className={cn(
|
|
169
|
+
'inline-flex items-center pl-[0.4em] font-mono text-[0.64rem] font-semibold uppercase tracking-[0.4em] tabular-nums text-(--theme-primary) transition duration-300 ease-out',
|
|
170
|
+
leaving ? 'translate-y-2 opacity-0 saturate-0' : 'translate-y-0 opacity-100 saturate-100'
|
|
171
|
+
)}
|
|
172
|
+
>
|
|
173
|
+
{PREFIX}
|
|
174
|
+
{tail}
|
|
175
|
+
<span
|
|
176
|
+
aria-hidden="true"
|
|
177
|
+
className="dither ml-0.5 inline-block size-2 shrink-0 -translate-y-px rounded-[1px]"
|
|
178
|
+
style={{ animation: 'gco-cursor 1s step-end infinite' }}
|
|
179
|
+
/>
|
|
180
|
+
</span>
|
|
181
|
+
</div>
|
|
182
|
+
)
|
|
183
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useStore } from '@nanostores/react'
|
|
2
|
+
import { type ReactNode, useEffect } from 'react'
|
|
3
|
+
import { useWebHaptics } from 'web-haptics/react'
|
|
4
|
+
|
|
5
|
+
import { registerHapticTrigger } from '@/lib/haptics'
|
|
6
|
+
import { $hapticsMuted } from '@/store/haptics'
|
|
7
|
+
|
|
8
|
+
export function HapticsProvider({ children }: { children: ReactNode }) {
|
|
9
|
+
const muted = useStore($hapticsMuted)
|
|
10
|
+
const { trigger } = useWebHaptics({ debug: true, showSwitch: false })
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
registerHapticTrigger(muted ? null : trigger)
|
|
14
|
+
|
|
15
|
+
return () => registerHapticTrigger(null)
|
|
16
|
+
}, [muted, trigger])
|
|
17
|
+
|
|
18
|
+
return <>{children}</>
|
|
19
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import type { NasTechConfigRecord } from '@/nastech'
|
|
5
|
+
import { type I18nConfigClient, I18nProvider } from '@/i18n'
|
|
6
|
+
|
|
7
|
+
import { LanguageSwitcher } from './language-switcher'
|
|
8
|
+
|
|
9
|
+
// cmdk (the searchable list) wires a ResizeObserver and scrolls the active
|
|
10
|
+
// item into view — neither exists in jsdom. Stub them, matching the polyfill
|
|
11
|
+
// idiom in tool-approval-group.test.tsx.
|
|
12
|
+
class TestResizeObserver {
|
|
13
|
+
observe() {}
|
|
14
|
+
unobserve() {}
|
|
15
|
+
disconnect() {}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
vi.stubGlobal('ResizeObserver', TestResizeObserver)
|
|
19
|
+
|
|
20
|
+
Element.prototype.scrollIntoView = function scrollIntoView() {}
|
|
21
|
+
|
|
22
|
+
describe('LanguageSwitcher', () => {
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
cleanup()
|
|
25
|
+
vi.restoreAllMocks()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('persists language changes through display.language config', async () => {
|
|
29
|
+
const saveConfig = vi.fn().mockResolvedValue({ ok: true })
|
|
30
|
+
const latestConfig: NasTechConfigRecord = { display: { language: 'en', skin: 'slate' } }
|
|
31
|
+
|
|
32
|
+
const configClient: I18nConfigClient = {
|
|
33
|
+
getConfig: vi.fn().mockResolvedValue(latestConfig),
|
|
34
|
+
saveConfig
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
render(
|
|
38
|
+
<I18nProvider configClient={configClient}>
|
|
39
|
+
<LanguageSwitcher />
|
|
40
|
+
</I18nProvider>
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
await waitFor(() => {
|
|
44
|
+
expect(screen.getByRole('button', { name: 'Switch language' }).hasAttribute('disabled')).toBe(false)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
fireEvent.click(screen.getByRole('button', { name: 'Switch language' }))
|
|
48
|
+
fireEvent.click(screen.getByRole('option', { name: /日本語/i }))
|
|
49
|
+
|
|
50
|
+
await waitFor(() => expect(saveConfig).toHaveBeenCalledTimes(1))
|
|
51
|
+
expect(saveConfig).toHaveBeenCalledWith({ display: { language: 'ja', skin: 'slate' } })
|
|
52
|
+
})
|
|
53
|
+
})
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import { Button } from '@/components/ui/button'
|
|
4
|
+
import { Command, CommandInput, CommandItem, CommandList } from '@/components/ui/command'
|
|
5
|
+
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
|
6
|
+
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'
|
|
7
|
+
import { useIsMobile } from '@/hooks/use-mobile'
|
|
8
|
+
import { type Locale, LOCALE_META, useI18n } from '@/i18n'
|
|
9
|
+
import { triggerHaptic } from '@/lib/haptics'
|
|
10
|
+
import { Check, ChevronDown, Globe } from '@/lib/icons'
|
|
11
|
+
import { cn } from '@/lib/utils'
|
|
12
|
+
import { notifyError } from '@/store/notifications'
|
|
13
|
+
|
|
14
|
+
export interface LanguageSwitcherProps {
|
|
15
|
+
className?: string
|
|
16
|
+
collapsed?: boolean
|
|
17
|
+
dropUp?: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface LanguageCommandProps {
|
|
21
|
+
allLocales: Array<[Locale, (typeof LOCALE_META)[Locale]]>
|
|
22
|
+
autoFocus?: boolean
|
|
23
|
+
disabled?: boolean
|
|
24
|
+
locale: Locale
|
|
25
|
+
noResults: string
|
|
26
|
+
onSelect: (code: Locale) => void
|
|
27
|
+
searchPlaceholder: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function LanguageSwitcher({ className, collapsed = false, dropUp = false }: LanguageSwitcherProps) {
|
|
31
|
+
const { isSavingLocale, locale, setLocale, t } = useI18n()
|
|
32
|
+
const [open, setOpen] = useState(false)
|
|
33
|
+
const isMobile = useIsMobile()
|
|
34
|
+
const useMobileSheet = Boolean(dropUp && isMobile)
|
|
35
|
+
const current = LOCALE_META[locale]
|
|
36
|
+
const allLocales = Object.entries(LOCALE_META) as Array<[Locale, typeof current]>
|
|
37
|
+
const title = t.language.switchTo
|
|
38
|
+
|
|
39
|
+
const selectLocale = async (code: Locale) => {
|
|
40
|
+
if (code === locale || isSavingLocale) {
|
|
41
|
+
setOpen(false)
|
|
42
|
+
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
triggerHaptic('selection')
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
await setLocale(code)
|
|
50
|
+
setOpen(false)
|
|
51
|
+
triggerHaptic('success')
|
|
52
|
+
} catch (error) {
|
|
53
|
+
notifyError(error, t.language.saveError)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const trigger = (
|
|
58
|
+
<Button
|
|
59
|
+
aria-expanded={open}
|
|
60
|
+
aria-label={title}
|
|
61
|
+
className={cn(
|
|
62
|
+
'min-w-32 justify-between gap-2 border-(--ui-stroke-tertiary) bg-(--ui-bg-quinary) px-2.5 text-left text-muted-foreground hover:text-foreground',
|
|
63
|
+
collapsed && 'min-w-0 px-2',
|
|
64
|
+
className
|
|
65
|
+
)}
|
|
66
|
+
disabled={isSavingLocale}
|
|
67
|
+
size="sm"
|
|
68
|
+
title={title}
|
|
69
|
+
type="button"
|
|
70
|
+
variant="outline"
|
|
71
|
+
>
|
|
72
|
+
<span className="inline-flex min-w-0 items-center gap-2">
|
|
73
|
+
<Globe className="size-3.5 shrink-0" />
|
|
74
|
+
{!collapsed && <span className="truncate">{current.name}</span>}
|
|
75
|
+
</span>
|
|
76
|
+
{!collapsed && <ChevronDown className="size-3 shrink-0 opacity-70" />}
|
|
77
|
+
</Button>
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if (useMobileSheet) {
|
|
81
|
+
return (
|
|
82
|
+
<Sheet onOpenChange={setOpen} open={open}>
|
|
83
|
+
<SheetTrigger asChild>{trigger}</SheetTrigger>
|
|
84
|
+
<SheetContent className="max-h-[min(28rem,80vh)] rounded-t-xl" side="bottom">
|
|
85
|
+
<SheetHeader>
|
|
86
|
+
<SheetTitle>{title}</SheetTitle>
|
|
87
|
+
<SheetDescription>{t.language.description}</SheetDescription>
|
|
88
|
+
</SheetHeader>
|
|
89
|
+
<LanguageCommand
|
|
90
|
+
allLocales={allLocales}
|
|
91
|
+
disabled={isSavingLocale}
|
|
92
|
+
locale={locale}
|
|
93
|
+
noResults={t.language.noResults}
|
|
94
|
+
onSelect={code => void selectLocale(code)}
|
|
95
|
+
searchPlaceholder={t.language.searchPlaceholder}
|
|
96
|
+
/>
|
|
97
|
+
</SheetContent>
|
|
98
|
+
</Sheet>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<Popover onOpenChange={setOpen} open={open}>
|
|
104
|
+
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
|
|
105
|
+
<PopoverContent align="end" className="w-56 p-0" side={dropUp ? 'top' : 'bottom'}>
|
|
106
|
+
<LanguageCommand
|
|
107
|
+
allLocales={allLocales}
|
|
108
|
+
autoFocus
|
|
109
|
+
disabled={isSavingLocale}
|
|
110
|
+
locale={locale}
|
|
111
|
+
noResults={t.language.noResults}
|
|
112
|
+
onSelect={code => void selectLocale(code)}
|
|
113
|
+
searchPlaceholder={t.language.searchPlaceholder}
|
|
114
|
+
/>
|
|
115
|
+
</PopoverContent>
|
|
116
|
+
</Popover>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function LanguageCommand({
|
|
121
|
+
allLocales,
|
|
122
|
+
autoFocus,
|
|
123
|
+
disabled,
|
|
124
|
+
locale,
|
|
125
|
+
noResults,
|
|
126
|
+
onSelect,
|
|
127
|
+
searchPlaceholder
|
|
128
|
+
}: LanguageCommandProps) {
|
|
129
|
+
const [search, setSearch] = useState('')
|
|
130
|
+
|
|
131
|
+
// Own the search term and filter manually. cmdk's built-in shouldFilter
|
|
132
|
+
// reorders items by its fuzzy-match score (≈alphabetical with an empty
|
|
133
|
+
// query), which destroys the curated en→zh→zh-hant→ja order. We disable it
|
|
134
|
+
// and do a plain substring filter that preserves array order — matching
|
|
135
|
+
// model-picker.tsx. Match against the endonym, the (hidden) English name,
|
|
136
|
+
// and the locale code so "日本"/"japanese"/"ja" all find Japanese.
|
|
137
|
+
const q = search.trim().toLowerCase()
|
|
138
|
+
|
|
139
|
+
const filtered = allLocales.filter(
|
|
140
|
+
([code, meta]) =>
|
|
141
|
+
!q ||
|
|
142
|
+
meta.name.toLowerCase().includes(q) ||
|
|
143
|
+
meta.englishName.toLowerCase().includes(q) ||
|
|
144
|
+
code.toLowerCase().includes(q)
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<Command className="bg-transparent" shouldFilter={false}>
|
|
149
|
+
<CommandInput autoFocus={autoFocus} onValueChange={setSearch} placeholder={searchPlaceholder} value={search} />
|
|
150
|
+
<CommandList className="max-h-80 p-1">
|
|
151
|
+
{filtered.length === 0 ? (
|
|
152
|
+
<div className="py-6 text-center text-sm text-muted-foreground">{noResults}</div>
|
|
153
|
+
) : (
|
|
154
|
+
filtered.map(([code, meta]) => {
|
|
155
|
+
const selected = code === locale
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<CommandItem
|
|
159
|
+
className={cn(selected ? 'font-medium text-foreground' : 'text-muted-foreground')}
|
|
160
|
+
disabled={disabled}
|
|
161
|
+
key={code}
|
|
162
|
+
onSelect={() => onSelect(code)}
|
|
163
|
+
value={code}
|
|
164
|
+
>
|
|
165
|
+
<Check className={cn('size-3.5 shrink-0 text-primary', !selected && 'invisible')} />
|
|
166
|
+
<span className="min-w-0 flex-1 truncate">{meta.name}</span>
|
|
167
|
+
<span className="font-mono text-[0.65rem] uppercase text-(--ui-text-tertiary)">{code}</span>
|
|
168
|
+
</CommandItem>
|
|
169
|
+
)
|
|
170
|
+
})
|
|
171
|
+
)}
|
|
172
|
+
</CommandList>
|
|
173
|
+
</Command>
|
|
174
|
+
)
|
|
175
|
+
}
|