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,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Window translucency (see-through window).
|
|
3
|
+
*
|
|
4
|
+
* One lever, 0–100. 0 = off (fully opaque, the default). Higher = more of the
|
|
5
|
+
* desktop shows through the whole window — the main process maps it to the
|
|
6
|
+
* native window opacity (`setOpacity`), the same effect as the Windows
|
|
7
|
+
* shift-scroll trick. macOS + Windows only; Linux has no runtime window
|
|
8
|
+
* opacity, so it's a no-op there.
|
|
9
|
+
*
|
|
10
|
+
* The renderer owns the value and mirrors it to the main process over IPC.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { atom } from 'nanostores'
|
|
14
|
+
|
|
15
|
+
import { persistString, storedString } from '@/lib/storage'
|
|
16
|
+
|
|
17
|
+
const KEY = 'nastech.desktop.translucency.v1'
|
|
18
|
+
|
|
19
|
+
const clamp = (n: number): number => Math.min(100, Math.max(0, Math.round(n)))
|
|
20
|
+
|
|
21
|
+
const read = (): number => {
|
|
22
|
+
const n = Number(storedString(KEY))
|
|
23
|
+
|
|
24
|
+
return Number.isFinite(n) ? clamp(n) : 0
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const $translucency = atom<number>(typeof window === 'undefined' ? 0 : read())
|
|
28
|
+
|
|
29
|
+
export function setTranslucency(intensity: number): void {
|
|
30
|
+
$translucency.set(clamp(intensity))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (typeof window !== 'undefined') {
|
|
34
|
+
$translucency.subscribe(intensity => {
|
|
35
|
+
persistString(KEY, String(intensity))
|
|
36
|
+
window.NASTECHDesktop?.setTranslucency?.({ intensity })
|
|
37
|
+
})
|
|
38
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import type { DesktopUpdateStatus } from '@/global'
|
|
4
|
+
|
|
5
|
+
const storage = new Map<string, string>()
|
|
6
|
+
|
|
7
|
+
vi.mock('@/lib/storage', () => ({
|
|
8
|
+
persistString: (key: string, value: null | string) => {
|
|
9
|
+
if (value === null) {
|
|
10
|
+
storage.delete(key)
|
|
11
|
+
} else {
|
|
12
|
+
storage.set(key, value)
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
storedString: (key: string) => storage.get(key) ?? null
|
|
16
|
+
}))
|
|
17
|
+
|
|
18
|
+
const notifySpy = vi.fn()
|
|
19
|
+
const dismissSpy = vi.fn()
|
|
20
|
+
|
|
21
|
+
vi.mock('@/store/notifications', () => ({
|
|
22
|
+
notify: (...args: unknown[]) => notifySpy(...args),
|
|
23
|
+
dismissNotification: (...args: unknown[]) => dismissSpy(...args)
|
|
24
|
+
}))
|
|
25
|
+
|
|
26
|
+
const { maybeNotifyUpdateAvailable } = await import('./updates')
|
|
27
|
+
|
|
28
|
+
const status = (over: Partial<DesktopUpdateStatus> = {}): DesktopUpdateStatus => ({
|
|
29
|
+
supported: true,
|
|
30
|
+
behind: 3,
|
|
31
|
+
targetSha: 'sha-a',
|
|
32
|
+
fetchedAt: 0,
|
|
33
|
+
...over
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const lastToast = () => notifySpy.mock.calls.at(-1)?.[0] as { onDismiss: () => void }
|
|
37
|
+
|
|
38
|
+
describe('maybeNotifyUpdateAvailable', () => {
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
storage.clear()
|
|
41
|
+
notifySpy.mockClear()
|
|
42
|
+
vi.useRealTimers()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('shows when an update is available and not snoozed', () => {
|
|
46
|
+
maybeNotifyUpdateAvailable(status())
|
|
47
|
+
expect(notifySpy).toHaveBeenCalledTimes(1)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('stays quiet for new commits once the toast was closed', () => {
|
|
51
|
+
maybeNotifyUpdateAvailable(status())
|
|
52
|
+
lastToast().onDismiss() // user closes it → cooldown starts
|
|
53
|
+
notifySpy.mockClear()
|
|
54
|
+
|
|
55
|
+
// A different commit lands while still within the cooldown window.
|
|
56
|
+
maybeNotifyUpdateAvailable(status({ targetSha: 'sha-b', behind: 9 }))
|
|
57
|
+
expect(notifySpy).not.toHaveBeenCalled()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('re-shows once the cooldown elapses', () => {
|
|
61
|
+
vi.useFakeTimers()
|
|
62
|
+
vi.setSystemTime(0)
|
|
63
|
+
|
|
64
|
+
maybeNotifyUpdateAvailable(status())
|
|
65
|
+
lastToast().onDismiss()
|
|
66
|
+
notifySpy.mockClear()
|
|
67
|
+
|
|
68
|
+
vi.setSystemTime(25 * 60 * 60 * 1000) // > 24h cooldown
|
|
69
|
+
maybeNotifyUpdateAvailable(status({ targetSha: 'sha-b' }))
|
|
70
|
+
expect(notifySpy).toHaveBeenCalledTimes(1)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('does nothing when already up to date', () => {
|
|
74
|
+
maybeNotifyUpdateAvailable(status({ behind: 0 }))
|
|
75
|
+
expect(notifySpy).not.toHaveBeenCalled()
|
|
76
|
+
})
|
|
77
|
+
})
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Desktop self-update store. Tracks distance from the configured branch,
|
|
3
|
+
* surfaces it as an ambient pill, and orchestrates the apply flow.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { atom } from 'nanostores'
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
DesktopUpdateApplyOptions,
|
|
10
|
+
DesktopUpdateApplyResult,
|
|
11
|
+
DesktopUpdateProgress,
|
|
12
|
+
DesktopUpdateStage,
|
|
13
|
+
DesktopUpdateStatus,
|
|
14
|
+
DesktopVersionInfo
|
|
15
|
+
} from '@/global'
|
|
16
|
+
import { translateNow } from '@/i18n'
|
|
17
|
+
import { persistString, storedString } from '@/lib/storage'
|
|
18
|
+
import { dismissNotification, notify } from '@/store/notifications'
|
|
19
|
+
|
|
20
|
+
export interface UpdateApplyState {
|
|
21
|
+
applying: boolean
|
|
22
|
+
stage: DesktopUpdateStage
|
|
23
|
+
message: string
|
|
24
|
+
percent: number | null
|
|
25
|
+
error: string | null
|
|
26
|
+
/** When the stage is 'manual': the exact command the user should run
|
|
27
|
+
* (CLI install with no staged updater). */
|
|
28
|
+
command: string | null
|
|
29
|
+
log: readonly { stage: DesktopUpdateStage; message: string; at: number }[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const IDLE: UpdateApplyState = {
|
|
33
|
+
applying: false,
|
|
34
|
+
stage: 'idle',
|
|
35
|
+
message: '',
|
|
36
|
+
percent: null,
|
|
37
|
+
error: null,
|
|
38
|
+
command: null,
|
|
39
|
+
log: []
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const $desktopVersion = atom<DesktopVersionInfo | null>(null)
|
|
43
|
+
export const $updateApply = atom<UpdateApplyState>(IDLE)
|
|
44
|
+
export const $updateChecking = atom<boolean>(false)
|
|
45
|
+
export const $updateOverlayOpen = atom<boolean>(false)
|
|
46
|
+
export const $updateStatus = atom<DesktopUpdateStatus | null>(null)
|
|
47
|
+
|
|
48
|
+
export const setUpdateOverlayOpen = (open: boolean) => $updateOverlayOpen.set(open)
|
|
49
|
+
export const resetUpdateApplyState = () => $updateApply.set(IDLE)
|
|
50
|
+
|
|
51
|
+
const UPDATE_TOAST_ID = 'desktop-update-available'
|
|
52
|
+
// Time-based snooze instead of per-sha dismissal: this repo lands ~100 commits
|
|
53
|
+
// a day, so a "don't show this exact sha again" guard re-popped the toast on
|
|
54
|
+
// every new commit. We instead suppress the toast for a cooldown window that
|
|
55
|
+
// (re)starts whenever the user closes it.
|
|
56
|
+
const UPDATE_TOAST_SNOOZE_KEY = 'NASTECH:update-toast-snooze-until'
|
|
57
|
+
const UPDATE_TOAST_COOLDOWN_MS = 24 * 60 * 60 * 1000
|
|
58
|
+
|
|
59
|
+
function snoozeUpdateToast(): void {
|
|
60
|
+
persistString(UPDATE_TOAST_SNOOZE_KEY, String(Date.now() + UPDATE_TOAST_COOLDOWN_MS))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isUpdateToastSnoozed(): boolean {
|
|
64
|
+
const until = Number(storedString(UPDATE_TOAST_SNOOZE_KEY) || 0)
|
|
65
|
+
|
|
66
|
+
return Number.isFinite(until) && Date.now() < until
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Must match tui_gateway's DESKTOP_BACKEND_CONTRACT that this build was written
|
|
70
|
+
// against. The backend reports its own value in session runtime info; a lower
|
|
71
|
+
// value (or none — a pre-GUI checkout) means GUI<->backend skew.
|
|
72
|
+
const REQUIRED_BACKEND_CONTRACT = 1
|
|
73
|
+
const SKEW_TOAST_ID = 'backend-contract-skew'
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Guard against a desktop GUI talking to a backend that predates its contract
|
|
77
|
+
* (e.g. a bb/gui-built app pointed at a `main` checkout). Rather than failing
|
|
78
|
+
* cryptically downstream, surface a persistent warning with a one-click align
|
|
79
|
+
* that runs the normal update flow (which self-heals to the right branch).
|
|
80
|
+
*/
|
|
81
|
+
export function reportBackendContract(contract: number | undefined): void {
|
|
82
|
+
if ((contract ?? 0) >= REQUIRED_BACKEND_CONTRACT) {
|
|
83
|
+
dismissNotification(SKEW_TOAST_ID)
|
|
84
|
+
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
notify({
|
|
89
|
+
action: { label: translateNow('notifications.updateNasTech'), onClick: () => void applyUpdates() },
|
|
90
|
+
durationMs: 0,
|
|
91
|
+
id: SKEW_TOAST_ID,
|
|
92
|
+
kind: 'warning',
|
|
93
|
+
message: translateNow('notifications.backendOutOfDateMessage'),
|
|
94
|
+
title: translateNow('notifications.backendOutOfDateTitle')
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Fire a toast when an update is available, at most once per cooldown window.
|
|
100
|
+
* Closing the toast — dismissing it or opening the updates window from it —
|
|
101
|
+
* (re)starts the cooldown, so a busy upstream branch doesn't re-spam the user
|
|
102
|
+
* on every new commit. The snooze is persisted, so it survives relaunches too.
|
|
103
|
+
*/
|
|
104
|
+
export function maybeNotifyUpdateAvailable(status: DesktopUpdateStatus | null) {
|
|
105
|
+
if (!status || status.supported === false || status.error || !status.targetSha) {
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if ((status.behind ?? 0) <= 0) {
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (isUpdateToastSnoozed()) {
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if ($updateApply.get().applying) {
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const behind = status.behind ?? 0
|
|
122
|
+
|
|
123
|
+
notify({
|
|
124
|
+
action: {
|
|
125
|
+
label: translateNow('notifications.seeWhatsNew'),
|
|
126
|
+
onClick: () => {
|
|
127
|
+
snoozeUpdateToast()
|
|
128
|
+
openUpdatesWindow()
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
durationMs: 0,
|
|
132
|
+
id: UPDATE_TOAST_ID,
|
|
133
|
+
kind: 'info',
|
|
134
|
+
message: translateNow('notifications.updateReadyMessage', behind),
|
|
135
|
+
onDismiss: () => snoozeUpdateToast(),
|
|
136
|
+
title: translateNow('notifications.updateReadyTitle')
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Opens the updates dialog and kicks off a fresh check so the user always
|
|
142
|
+
* sees current state, even if a stale status is cached from earlier.
|
|
143
|
+
*/
|
|
144
|
+
export function openUpdatesWindow(): void {
|
|
145
|
+
$updateOverlayOpen.set(true)
|
|
146
|
+
void checkUpdates()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Re-read the running app's version from the Electron main process and
|
|
150
|
+
* publish it on `$desktopVersion`. Called when the About panel mounts, the
|
|
151
|
+
* update flow finishes, and the window regains focus, so the About text
|
|
152
|
+
* stays in sync with the just-installed binary instead of frozen at the
|
|
153
|
+
* value captured at first-load. */
|
|
154
|
+
export async function refreshDesktopVersion(): Promise<DesktopVersionInfo | null> {
|
|
155
|
+
if (typeof window === 'undefined') {
|
|
156
|
+
return null
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Best-effort UI sync: callers (checkUpdates, startUpdatePoller, window
|
|
160
|
+
// focus handler) all kick this off with `void refreshDesktopVersion()`,
|
|
161
|
+
// so any rejection from the IPC bridge (e.g. main process shutting down
|
|
162
|
+
// mid-reload, or the bridge not yet ready on first paint) would surface
|
|
163
|
+
// as an unhandled promise rejection in the renderer. Swallow it.
|
|
164
|
+
try {
|
|
165
|
+
const next = await window.NASTECHDesktop?.getVersion?.()
|
|
166
|
+
|
|
167
|
+
if (next) {
|
|
168
|
+
$desktopVersion.set(next)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return next ?? null
|
|
172
|
+
} catch {
|
|
173
|
+
return null
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function checkUpdates(): Promise<DesktopUpdateStatus | null> {
|
|
178
|
+
const bridge = window.NASTECHDesktop?.updates
|
|
179
|
+
|
|
180
|
+
if (!bridge || $updateChecking.get()) {
|
|
181
|
+
return $updateStatus.get()
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
$updateChecking.set(true)
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const status = await bridge.check()
|
|
188
|
+
$updateStatus.set(status)
|
|
189
|
+
maybeNotifyUpdateAvailable(status)
|
|
190
|
+
// The update check pulls the latest nastech_cli + bundled package metadata
|
|
191
|
+
// into place. Re-read the running version so About reflects the now-fresh
|
|
192
|
+
// checkout rather than the one captured at process start.
|
|
193
|
+
void refreshDesktopVersion()
|
|
194
|
+
|
|
195
|
+
return status
|
|
196
|
+
} catch (error) {
|
|
197
|
+
const previous = $updateStatus.get()
|
|
198
|
+
|
|
199
|
+
const fallback: DesktopUpdateStatus = {
|
|
200
|
+
supported: previous?.supported ?? true,
|
|
201
|
+
branch: previous?.branch,
|
|
202
|
+
error: 'check-failed',
|
|
203
|
+
message: error instanceof Error ? error.message : String(error),
|
|
204
|
+
fetchedAt: Date.now()
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
$updateStatus.set(fallback)
|
|
208
|
+
|
|
209
|
+
return fallback
|
|
210
|
+
} finally {
|
|
211
|
+
$updateChecking.set(false)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function applyUpdates(opts: DesktopUpdateApplyOptions = {}): Promise<DesktopUpdateApplyResult> {
|
|
216
|
+
const bridge = window.NASTECHDesktop?.updates
|
|
217
|
+
|
|
218
|
+
if (!bridge) {
|
|
219
|
+
return { ok: false, error: 'unavailable', message: 'Desktop bridge unavailable.' }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
dismissNotification(UPDATE_TOAST_ID)
|
|
223
|
+
$updateApply.set({ ...IDLE, applying: true, stage: 'prepare', message: 'Starting update…' })
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
const result = await bridge.apply(opts)
|
|
227
|
+
|
|
228
|
+
// CLI install with no staged updater: not an error — the user just runs
|
|
229
|
+
// `NASTECH update` themselves. Land on a dedicated manual state so the
|
|
230
|
+
// overlay shows the command + copy button instead of a dead retry loop.
|
|
231
|
+
if (result?.manual) {
|
|
232
|
+
$updateApply.set({
|
|
233
|
+
...IDLE,
|
|
234
|
+
applying: false,
|
|
235
|
+
stage: 'manual',
|
|
236
|
+
message: result.command ?? 'NASTECH update',
|
|
237
|
+
command: result.command ?? 'NASTECH update'
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return result
|
|
242
|
+
} catch (error) {
|
|
243
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
244
|
+
$updateApply.set({ ...$updateApply.get(), applying: false, stage: 'error', error: 'apply-failed', message })
|
|
245
|
+
|
|
246
|
+
return { ok: false, error: 'apply-failed', message }
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function ingestProgress(payload: DesktopUpdateProgress): void {
|
|
251
|
+
const current = $updateApply.get()
|
|
252
|
+
const log = [...current.log, { stage: payload.stage, message: payload.message, at: payload.at }].slice(-50)
|
|
253
|
+
const terminal = payload.stage === 'error' || payload.stage === 'restart' || payload.stage === 'manual'
|
|
254
|
+
|
|
255
|
+
$updateApply.set({
|
|
256
|
+
applying: !terminal,
|
|
257
|
+
stage: payload.stage,
|
|
258
|
+
message: payload.message,
|
|
259
|
+
percent: payload.percent,
|
|
260
|
+
error: payload.error,
|
|
261
|
+
// 'manual' carries the command to run in its message field.
|
|
262
|
+
command: payload.stage === 'manual' ? payload.message : current.command,
|
|
263
|
+
log
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
let pollerStarted = false
|
|
268
|
+
let backgroundTimer: ReturnType<typeof setInterval> | null = null
|
|
269
|
+
let lastFocusAt = 0
|
|
270
|
+
|
|
271
|
+
/** Wire up background polling + progress streaming. Idempotent. */
|
|
272
|
+
export function startUpdatePoller(): void {
|
|
273
|
+
if (pollerStarted || typeof window === 'undefined') {
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const bridge = window.NASTECHDesktop?.updates
|
|
278
|
+
|
|
279
|
+
if (!bridge) {
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
pollerStarted = true
|
|
284
|
+
void checkUpdates()
|
|
285
|
+
void refreshDesktopVersion()
|
|
286
|
+
bridge.onProgress(ingestProgress)
|
|
287
|
+
|
|
288
|
+
window.addEventListener('focus', onFocus)
|
|
289
|
+
backgroundTimer = setInterval(() => void checkUpdates(), 30 * 60 * 1000)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function stopUpdatePoller(): void {
|
|
293
|
+
if (backgroundTimer !== null) {
|
|
294
|
+
clearInterval(backgroundTimer)
|
|
295
|
+
backgroundTimer = null
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
window.removeEventListener('focus', onFocus)
|
|
299
|
+
pollerStarted = false
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function onFocus() {
|
|
303
|
+
const now = Date.now()
|
|
304
|
+
|
|
305
|
+
if (now - lastFocusAt < 5 * 60 * 1000) {
|
|
306
|
+
return
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
lastFocusAt = now
|
|
310
|
+
void checkUpdates()
|
|
311
|
+
// Cheap and safe to re-read on every (throttled) focus: the user may have
|
|
312
|
+
// updated NasTech from another window/CLI between focuses, and About should
|
|
313
|
+
// catch up without forcing a restart.
|
|
314
|
+
void refreshDesktopVersion()
|
|
315
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { atom } from 'nanostores'
|
|
2
|
+
|
|
3
|
+
export type VoicePlaybackSource = 'read-aloud' | 'voice-conversation'
|
|
4
|
+
export type VoicePlaybackStatus = 'idle' | 'preparing' | 'speaking'
|
|
5
|
+
|
|
6
|
+
export interface VoicePlaybackState {
|
|
7
|
+
audioElement: HTMLAudioElement | null
|
|
8
|
+
messageId: string | null
|
|
9
|
+
sequence: number
|
|
10
|
+
source: VoicePlaybackSource | null
|
|
11
|
+
status: VoicePlaybackStatus
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const $voicePlayback = atom<VoicePlaybackState>({
|
|
15
|
+
audioElement: null,
|
|
16
|
+
messageId: null,
|
|
17
|
+
sequence: 0,
|
|
18
|
+
source: null,
|
|
19
|
+
status: 'idle'
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
export function setVoicePlaybackState(next: VoicePlaybackState) {
|
|
23
|
+
$voicePlayback.set(next)
|
|
24
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { canOpenSessionWindow, openNewSessionInNewWindow, openSessionInNewWindow } from './windows'
|
|
4
|
+
|
|
5
|
+
const desktopWindow = window as unknown as { nastechDesktop?: Window['nastechDesktop'] }
|
|
6
|
+
const initialNasTechDesktop = desktopWindow.nastechDesktop
|
|
7
|
+
|
|
8
|
+
const notifyError = vi.fn()
|
|
9
|
+
|
|
10
|
+
vi.mock('./notifications', () => ({
|
|
11
|
+
notifyError: (...args: unknown[]) => notifyError(...args)
|
|
12
|
+
}))
|
|
13
|
+
|
|
14
|
+
function installBridge(
|
|
15
|
+
openSessionWindow?: Window['nastechDesktop']['openSessionWindow'],
|
|
16
|
+
openNewSessionWindow?: Window['nastechDesktop']['openNewSessionWindow']
|
|
17
|
+
) {
|
|
18
|
+
desktopWindow.nastechDesktop = {
|
|
19
|
+
...(openSessionWindow ? { openSessionWindow } : {}),
|
|
20
|
+
...(openNewSessionWindow ? { openNewSessionWindow } : {})
|
|
21
|
+
} as unknown as Window['nastechDesktop']
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
notifyError.mockClear()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
if (initialNasTechDesktop) {
|
|
30
|
+
desktopWindow.nastechDesktop = initialNasTechDesktop
|
|
31
|
+
} else {
|
|
32
|
+
delete desktopWindow.nastechDesktop
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('canOpenSessionWindow', () => {
|
|
37
|
+
it('is false when the desktop bridge is absent', () => {
|
|
38
|
+
delete desktopWindow.nastechDesktop
|
|
39
|
+
expect(canOpenSessionWindow()).toBe(false)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('is false when the bridge lacks openSessionWindow', () => {
|
|
43
|
+
installBridge(undefined)
|
|
44
|
+
expect(canOpenSessionWindow()).toBe(false)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('is true when the bridge exposes openSessionWindow', () => {
|
|
48
|
+
installBridge(vi.fn().mockResolvedValue({ ok: true }))
|
|
49
|
+
expect(canOpenSessionWindow()).toBe(true)
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('openSessionInNewWindow', () => {
|
|
54
|
+
it('no-ops without a session id', async () => {
|
|
55
|
+
const open = vi.fn().mockResolvedValue({ ok: true })
|
|
56
|
+
installBridge(open)
|
|
57
|
+
|
|
58
|
+
await openSessionInNewWindow('')
|
|
59
|
+
|
|
60
|
+
expect(open).not.toHaveBeenCalled()
|
|
61
|
+
expect(notifyError).not.toHaveBeenCalled()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('no-ops gracefully when the bridge is absent (web fallback)', async () => {
|
|
65
|
+
delete desktopWindow.nastechDesktop
|
|
66
|
+
|
|
67
|
+
await openSessionInNewWindow('s1')
|
|
68
|
+
|
|
69
|
+
expect(notifyError).not.toHaveBeenCalled()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('invokes the bridge with the session id', async () => {
|
|
73
|
+
const open = vi.fn().mockResolvedValue({ ok: true })
|
|
74
|
+
installBridge(open)
|
|
75
|
+
|
|
76
|
+
await openSessionInNewWindow('s1')
|
|
77
|
+
|
|
78
|
+
expect(open).toHaveBeenCalledWith('s1', undefined)
|
|
79
|
+
expect(notifyError).not.toHaveBeenCalled()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('forwards the watch flag for spectator (subagent) windows', async () => {
|
|
83
|
+
const open = vi.fn().mockResolvedValue({ ok: true })
|
|
84
|
+
installBridge(open)
|
|
85
|
+
|
|
86
|
+
await openSessionInNewWindow('s1', { watch: true })
|
|
87
|
+
|
|
88
|
+
expect(open).toHaveBeenCalledWith('s1', { watch: true })
|
|
89
|
+
expect(notifyError).not.toHaveBeenCalled()
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('notifies on an ok:false result', async () => {
|
|
93
|
+
installBridge(vi.fn().mockResolvedValue({ ok: false, error: 'invalid-session-id' }))
|
|
94
|
+
|
|
95
|
+
await openSessionInNewWindow('s1')
|
|
96
|
+
|
|
97
|
+
expect(notifyError).toHaveBeenCalledTimes(1)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('notifies when the bridge throws', async () => {
|
|
101
|
+
installBridge(vi.fn().mockRejectedValue(new Error('boom')))
|
|
102
|
+
|
|
103
|
+
await openSessionInNewWindow('s1')
|
|
104
|
+
|
|
105
|
+
expect(notifyError).toHaveBeenCalledTimes(1)
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
describe('openNewSessionInNewWindow', () => {
|
|
110
|
+
it('no-ops gracefully when the bridge is absent (web fallback)', async () => {
|
|
111
|
+
delete desktopWindow.nastechDesktop
|
|
112
|
+
|
|
113
|
+
await openNewSessionInNewWindow()
|
|
114
|
+
|
|
115
|
+
expect(notifyError).not.toHaveBeenCalled()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('no-ops when openNewSessionWindow is missing', async () => {
|
|
119
|
+
installBridge(vi.fn().mockResolvedValue({ ok: true }))
|
|
120
|
+
|
|
121
|
+
await openNewSessionInNewWindow()
|
|
122
|
+
|
|
123
|
+
expect(notifyError).not.toHaveBeenCalled()
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('invokes the bridge', async () => {
|
|
127
|
+
const openNew = vi.fn().mockResolvedValue({ ok: true })
|
|
128
|
+
installBridge(vi.fn().mockResolvedValue({ ok: true }), openNew)
|
|
129
|
+
|
|
130
|
+
await openNewSessionInNewWindow()
|
|
131
|
+
|
|
132
|
+
expect(openNew).toHaveBeenCalledTimes(1)
|
|
133
|
+
expect(notifyError).not.toHaveBeenCalled()
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('notifies on an ok:false result', async () => {
|
|
137
|
+
installBridge(vi.fn().mockResolvedValue({ ok: true }), vi.fn().mockResolvedValue({ ok: false, error: 'nope' }))
|
|
138
|
+
|
|
139
|
+
await openNewSessionInNewWindow()
|
|
140
|
+
|
|
141
|
+
expect(notifyError).toHaveBeenCalledTimes(1)
|
|
142
|
+
})
|
|
143
|
+
})
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { notifyError } from './notifications'
|
|
2
|
+
|
|
3
|
+
// Window flag set by the Electron main process when it opens a standalone
|
|
4
|
+
// session window (see electron/main.cjs buildSessionWindowUrl). It rides in the
|
|
5
|
+
// query string BEFORE the HashRouter '#', so we read it from location.search,
|
|
6
|
+
// never from the router. A "secondary" window renders a single chat without the
|
|
7
|
+
// global session sidebar or the install / onboarding overlays.
|
|
8
|
+
const SECONDARY_WINDOW_FLAG = 'secondary'
|
|
9
|
+
|
|
10
|
+
let secondaryWindowCache: boolean | null = null
|
|
11
|
+
|
|
12
|
+
export function isSecondaryWindow(): boolean {
|
|
13
|
+
if (secondaryWindowCache !== null) {
|
|
14
|
+
return secondaryWindowCache
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let result = false
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
result = new URLSearchParams(window.location.search).get('win') === SECONDARY_WINDOW_FLAG
|
|
21
|
+
} catch {
|
|
22
|
+
result = false
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
secondaryWindowCache = result
|
|
26
|
+
|
|
27
|
+
return result
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let watchWindowCache: boolean | null = null
|
|
31
|
+
|
|
32
|
+
// A "watch" window spectates a session that is being driven elsewhere (a
|
|
33
|
+
// running subagent). It resumes lazily — the gateway registers history + a
|
|
34
|
+
// transport for the live mirror without building an agent, so opening it is
|
|
35
|
+
// cheap even while the backend is busy running the delegation.
|
|
36
|
+
export function isWatchWindow(): boolean {
|
|
37
|
+
if (watchWindowCache !== null) {
|
|
38
|
+
return watchWindowCache
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let result = false
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
result = new URLSearchParams(window.location.search).get('watch') === '1'
|
|
45
|
+
} catch {
|
|
46
|
+
result = false
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
watchWindowCache = result
|
|
50
|
+
|
|
51
|
+
return result
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// True when running inside the Electron desktop shell (the preload bridge is
|
|
55
|
+
// present). The "open in new window" affordance is desktop-only.
|
|
56
|
+
export function canOpenSessionWindow(): boolean {
|
|
57
|
+
return typeof window !== 'undefined' && typeof window.NASTECHDesktop?.openSessionWindow === 'function'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Open (or focus) a standalone OS window for a single chat session. No-ops
|
|
61
|
+
// gracefully outside Electron so callers can wire it unconditionally.
|
|
62
|
+
// `watch: true` opens a spectator window (lazy resume, live-mirror stream).
|
|
63
|
+
export async function openSessionInNewWindow(sessionId: string, opts?: { watch?: boolean }): Promise<void> {
|
|
64
|
+
if (!sessionId || !canOpenSessionWindow()) {
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const result = await window.NASTECHDesktop.openSessionWindow(sessionId, opts)
|
|
70
|
+
|
|
71
|
+
if (!result?.ok) {
|
|
72
|
+
notifyError(new Error(result?.error || 'unknown error'), 'Could not open chat in a new window')
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
notifyError(err, 'Could not open chat in a new window')
|
|
76
|
+
}
|
|
77
|
+
}
|