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,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for Electron net.request calls that ride the OAuth session partition.
|
|
3
|
+
*
|
|
4
|
+
* Electron's ClientRequest forbids app-set restricted headers such as
|
|
5
|
+
* Content-Length. Let Chromium frame the body itself; only set the JSON content
|
|
6
|
+
* type here.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
function serializeJsonBody(body) {
|
|
10
|
+
return body === undefined ? undefined : Buffer.from(JSON.stringify(body))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function setJsonRequestHeaders(request) {
|
|
14
|
+
request.setHeader('Content-Type', 'application/json')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = {
|
|
18
|
+
serializeJsonBody,
|
|
19
|
+
setJsonRequestHeaders
|
|
20
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for OAuth-session Electron net.request helpers.
|
|
3
|
+
*
|
|
4
|
+
* Run with: node --test electron/oauth-net-request.test.cjs
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const test = require('node:test')
|
|
8
|
+
const assert = require('node:assert/strict')
|
|
9
|
+
|
|
10
|
+
const { serializeJsonBody, setJsonRequestHeaders } = require('./oauth-net-request.cjs')
|
|
11
|
+
|
|
12
|
+
test('serializeJsonBody returns undefined for absent bodies', () => {
|
|
13
|
+
assert.equal(serializeJsonBody(undefined), undefined)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('serializeJsonBody JSON-encodes request bodies', () => {
|
|
17
|
+
const body = serializeJsonBody({ archived: true })
|
|
18
|
+
assert.ok(Buffer.isBuffer(body))
|
|
19
|
+
assert.equal(body.toString('utf8'), '{"archived":true}')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('setJsonRequestHeaders does not set Electron-restricted Content-Length', () => {
|
|
23
|
+
const headers = []
|
|
24
|
+
const request = {
|
|
25
|
+
setHeader(name, value) {
|
|
26
|
+
headers.push([name, value])
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
setJsonRequestHeaders(request)
|
|
31
|
+
|
|
32
|
+
assert.deepEqual(headers, [['Content-Type', 'application/json']])
|
|
33
|
+
assert.equal(headers.some(([name]) => name.toLowerCase() === 'content-length'), false)
|
|
34
|
+
})
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
const { contextBridge, ipcRenderer, webUtils } = require('electron')
|
|
2
|
+
|
|
3
|
+
contextBridge.exposeInMainWorld('nastechDesktop', {
|
|
4
|
+
getConnection: profile => ipcRenderer.invoke('nastech:connection', profile),
|
|
5
|
+
touchBackend: profile => ipcRenderer.invoke('nastech:backend:touch', profile),
|
|
6
|
+
getGatewayWsUrl: profile => ipcRenderer.invoke('nastech:gateway:ws-url', profile),
|
|
7
|
+
getBootProgress: () => ipcRenderer.invoke('nastech:boot-progress:get'),
|
|
8
|
+
getConnectionConfig: profile => ipcRenderer.invoke('nastech:connection-config:get', profile),
|
|
9
|
+
saveConnectionConfig: payload => ipcRenderer.invoke('nastech:connection-config:save', payload),
|
|
10
|
+
applyConnectionConfig: payload => ipcRenderer.invoke('nastech:connection-config:apply', payload),
|
|
11
|
+
testConnectionConfig: payload => ipcRenderer.invoke('nastech:connection-config:test', payload),
|
|
12
|
+
probeConnectionConfig: remoteUrl => ipcRenderer.invoke('nastech:connection-config:probe', remoteUrl),
|
|
13
|
+
oauthLoginConnectionConfig: remoteUrl => ipcRenderer.invoke('nastech:connection-config:oauth-login', remoteUrl),
|
|
14
|
+
oauthLogoutConnectionConfig: remoteUrl => ipcRenderer.invoke('nastech:connection-config:oauth-logout', remoteUrl),
|
|
15
|
+
profile: {
|
|
16
|
+
get: () => ipcRenderer.invoke('nastech:profile:get'),
|
|
17
|
+
set: name => ipcRenderer.invoke('nastech:profile:set', name)
|
|
18
|
+
},
|
|
19
|
+
api: request => ipcRenderer.invoke('nastech:api', request),
|
|
20
|
+
notify: payload => ipcRenderer.invoke('nastech:notify', payload),
|
|
21
|
+
requestMicrophoneAccess: () => ipcRenderer.invoke('nastech:requestMicrophoneAccess'),
|
|
22
|
+
readFileDataUrl: filePath => ipcRenderer.invoke('nastech:readFileDataUrl', filePath),
|
|
23
|
+
readFileText: filePath => ipcRenderer.invoke('nastech:readFileText', filePath),
|
|
24
|
+
selectPaths: options => ipcRenderer.invoke('nastech:selectPaths', options),
|
|
25
|
+
writeClipboard: text => ipcRenderer.invoke('nastech:writeClipboard', text),
|
|
26
|
+
saveImageFromUrl: url => ipcRenderer.invoke('nastech:saveImageFromUrl', url),
|
|
27
|
+
saveImageBuffer: (data, ext) => ipcRenderer.invoke('nastech:saveImageBuffer', { data, ext }),
|
|
28
|
+
saveClipboardImage: () => ipcRenderer.invoke('nastech:saveClipboardImage'),
|
|
29
|
+
getPathForFile: file => {
|
|
30
|
+
try {
|
|
31
|
+
return webUtils.getPathForFile(file) || ''
|
|
32
|
+
} catch {
|
|
33
|
+
return ''
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
normalizePreviewTarget: (target, baseDir) => ipcRenderer.invoke('nastech:normalizePreviewTarget', target, baseDir),
|
|
37
|
+
watchPreviewFile: url => ipcRenderer.invoke('nastech:watchPreviewFile', url),
|
|
38
|
+
stopPreviewFileWatch: id => ipcRenderer.invoke('nastech:stopPreviewFileWatch', id),
|
|
39
|
+
setTitleBarTheme: payload => ipcRenderer.send('nastech:titlebar-theme', payload),
|
|
40
|
+
setPreviewShortcutActive: active => ipcRenderer.send('nastech:previewShortcutActive', Boolean(active)),
|
|
41
|
+
openExternal: url => ipcRenderer.invoke('nastech:openExternal', url),
|
|
42
|
+
fetchLinkTitle: url => ipcRenderer.invoke('nastech:fetchLinkTitle', url),
|
|
43
|
+
settings: {
|
|
44
|
+
getDefaultProjectDir: () => ipcRenderer.invoke('nastech:setting:defaultProjectDir:get'),
|
|
45
|
+
setDefaultProjectDir: dir => ipcRenderer.invoke('nastech:setting:defaultProjectDir:set', dir),
|
|
46
|
+
pickDefaultProjectDir: () => ipcRenderer.invoke('nastech:setting:defaultProjectDir:pick')
|
|
47
|
+
},
|
|
48
|
+
revealLogs: () => ipcRenderer.invoke('nastech:logs:reveal'),
|
|
49
|
+
getRecentLogs: () => ipcRenderer.invoke('nastech:logs:recent'),
|
|
50
|
+
readDir: dirPath => ipcRenderer.invoke('nastech:fs:readDir', dirPath),
|
|
51
|
+
gitRoot: startPath => ipcRenderer.invoke('nastech:fs:gitRoot', startPath),
|
|
52
|
+
terminal: {
|
|
53
|
+
dispose: id => ipcRenderer.invoke('nastech:terminal:dispose', id),
|
|
54
|
+
resize: (id, size) => ipcRenderer.invoke('nastech:terminal:resize', id, size),
|
|
55
|
+
start: options => ipcRenderer.invoke('nastech:terminal:start', options),
|
|
56
|
+
write: (id, data) => ipcRenderer.invoke('nastech:terminal:write', id, data),
|
|
57
|
+
onData: (id, callback) => {
|
|
58
|
+
const channel = `nastech:terminal:${id}:data`
|
|
59
|
+
const listener = (_event, payload) => callback(payload)
|
|
60
|
+
ipcRenderer.on(channel, listener)
|
|
61
|
+
return () => ipcRenderer.removeListener(channel, listener)
|
|
62
|
+
},
|
|
63
|
+
onExit: (id, callback) => {
|
|
64
|
+
const channel = `nastech:terminal:${id}:exit`
|
|
65
|
+
const listener = (_event, payload) => callback(payload)
|
|
66
|
+
ipcRenderer.on(channel, listener)
|
|
67
|
+
return () => ipcRenderer.removeListener(channel, listener)
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
onClosePreviewRequested: callback => {
|
|
71
|
+
const listener = () => callback()
|
|
72
|
+
ipcRenderer.on('nastech:close-preview-requested', listener)
|
|
73
|
+
return () => ipcRenderer.removeListener('nastech:close-preview-requested', listener)
|
|
74
|
+
},
|
|
75
|
+
onOpenUpdatesRequested: callback => {
|
|
76
|
+
const listener = () => callback()
|
|
77
|
+
ipcRenderer.on('nastech:open-updates', listener)
|
|
78
|
+
return () => ipcRenderer.removeListener('nastech:open-updates', listener)
|
|
79
|
+
},
|
|
80
|
+
onWindowStateChanged: callback => {
|
|
81
|
+
const listener = (_event, payload) => callback(payload)
|
|
82
|
+
ipcRenderer.on('nastech:window-state-changed', listener)
|
|
83
|
+
return () => ipcRenderer.removeListener('nastech:window-state-changed', listener)
|
|
84
|
+
},
|
|
85
|
+
onPreviewFileChanged: callback => {
|
|
86
|
+
const listener = (_event, payload) => callback(payload)
|
|
87
|
+
ipcRenderer.on('nastech:preview-file-changed', listener)
|
|
88
|
+
return () => ipcRenderer.removeListener('nastech:preview-file-changed', listener)
|
|
89
|
+
},
|
|
90
|
+
onBackendExit: callback => {
|
|
91
|
+
const listener = (_event, payload) => callback(payload)
|
|
92
|
+
ipcRenderer.on('nastech:backend-exit', listener)
|
|
93
|
+
return () => ipcRenderer.removeListener('nastech:backend-exit', listener)
|
|
94
|
+
},
|
|
95
|
+
onPowerResume: callback => {
|
|
96
|
+
const listener = () => callback()
|
|
97
|
+
ipcRenderer.on('nastech:power-resume', listener)
|
|
98
|
+
return () => ipcRenderer.removeListener('nastech:power-resume', listener)
|
|
99
|
+
},
|
|
100
|
+
onBootProgress: callback => {
|
|
101
|
+
const listener = (_event, payload) => callback(payload)
|
|
102
|
+
ipcRenderer.on('nastech:boot-progress', listener)
|
|
103
|
+
return () => ipcRenderer.removeListener('nastech:boot-progress', listener)
|
|
104
|
+
},
|
|
105
|
+
// First-launch bootstrap progress -- emitted by the install.ps1 stage
|
|
106
|
+
// runner in main.cjs (apps/desktop/electron/bootstrap-runner.cjs).
|
|
107
|
+
// Renderer's install overlay subscribes to live events and queries the
|
|
108
|
+
// current snapshot via getBootstrapState() to recover after a devtools
|
|
109
|
+
// reload mid-bootstrap.
|
|
110
|
+
getBootstrapState: () => ipcRenderer.invoke('nastech:bootstrap:get'),
|
|
111
|
+
resetBootstrap: () => ipcRenderer.invoke('nastech:bootstrap:reset'),
|
|
112
|
+
repairBootstrap: () => ipcRenderer.invoke('nastech:bootstrap:repair'),
|
|
113
|
+
cancelBootstrap: () => ipcRenderer.invoke('nastech:bootstrap:cancel'),
|
|
114
|
+
onBootstrapEvent: callback => {
|
|
115
|
+
const listener = (_event, payload) => callback(payload)
|
|
116
|
+
ipcRenderer.on('nastech:bootstrap:event', listener)
|
|
117
|
+
return () => ipcRenderer.removeListener('nastech:bootstrap:event', listener)
|
|
118
|
+
},
|
|
119
|
+
getVersion: () => ipcRenderer.invoke('nastech:version'),
|
|
120
|
+
uninstall: {
|
|
121
|
+
summary: () => ipcRenderer.invoke('nastech:uninstall:summary'),
|
|
122
|
+
run: mode => ipcRenderer.invoke('nastech:uninstall:run', { mode })
|
|
123
|
+
},
|
|
124
|
+
updates: {
|
|
125
|
+
check: () => ipcRenderer.invoke('nastech:updates:check'),
|
|
126
|
+
apply: opts => ipcRenderer.invoke('nastech:updates:apply', opts),
|
|
127
|
+
getBranch: () => ipcRenderer.invoke('nastech:updates:branch:get'),
|
|
128
|
+
setBranch: name => ipcRenderer.invoke('nastech:updates:branch:set', name),
|
|
129
|
+
onProgress: callback => {
|
|
130
|
+
const listener = (_event, payload) => callback(payload)
|
|
131
|
+
ipcRenderer.on('nastech:updates:progress', listener)
|
|
132
|
+
return () => ipcRenderer.removeListener('nastech:updates:progress', listener)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
})
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Secondary "session windows" — one extra OS window per chat so a user can
|
|
2
|
+
// work with multiple chats side by side. The pure, Electron-free pieces live
|
|
3
|
+
// here so they can be unit-tested with node --test (mirroring how the rest of
|
|
4
|
+
// electron/*.cjs splits testable logic out of the main.cjs monolith).
|
|
5
|
+
|
|
6
|
+
const { pathToFileURL } = require('node:url')
|
|
7
|
+
|
|
8
|
+
// Secondary windows open at the minimum usable size — a compact side panel for
|
|
9
|
+
// subagent watch / cmd-click session pop-out, not a second full desktop.
|
|
10
|
+
const SESSION_WINDOW_MIN_WIDTH = 420
|
|
11
|
+
const SESSION_WINDOW_MIN_HEIGHT = 620
|
|
12
|
+
|
|
13
|
+
// Build the renderer URL for a secondary window. The renderer uses a
|
|
14
|
+
// HashRouter, so the session route lives after the '#'. The `?win=secondary`
|
|
15
|
+
// flag MUST sit in the query string BEFORE the '#': anything after the '#' is
|
|
16
|
+
// treated as the route by HashRouter and would break routeSessionId(). The
|
|
17
|
+
// renderer reads the flag from window.location.search to suppress the install /
|
|
18
|
+
// onboarding overlays and the global session sidebar. `watch=1` marks a
|
|
19
|
+
// spectator window (e.g. a running subagent's session): the renderer resumes
|
|
20
|
+
// it lazily so the gateway never builds an agent just to stream into it.
|
|
21
|
+
function buildSessionWindowUrl(sessionId, { devServer, rendererIndexPath, watch } = {}) {
|
|
22
|
+
const query = `?win=secondary${watch ? '&watch=1' : ''}`
|
|
23
|
+
const route = `#/${encodeURIComponent(sessionId)}`
|
|
24
|
+
|
|
25
|
+
if (devServer) {
|
|
26
|
+
const base = devServer.endsWith('/') ? devServer.slice(0, -1) : devServer
|
|
27
|
+
|
|
28
|
+
return `${base}/${query}${route}`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return `${pathToFileURL(rendererIndexPath).toString()}${query}${route}`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// A small registry keyed by sessionId that guarantees one window per chat:
|
|
35
|
+
// opening a session that already has a live window focuses it instead of
|
|
36
|
+
// spawning a duplicate, and a window removes itself from the registry when it
|
|
37
|
+
// closes. The actual BrowserWindow construction is injected (the `factory`) so
|
|
38
|
+
// this module stays free of Electron and is unit-testable.
|
|
39
|
+
function createSessionWindowRegistry() {
|
|
40
|
+
const windows = new Map()
|
|
41
|
+
|
|
42
|
+
function openOrFocus(sessionId, factory) {
|
|
43
|
+
const key = typeof sessionId === 'string' ? sessionId.trim() : ''
|
|
44
|
+
|
|
45
|
+
if (!key) {
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const existing = windows.get(key)
|
|
50
|
+
|
|
51
|
+
if (existing && !existing.isDestroyed()) {
|
|
52
|
+
// Focus-or-create: never duplicate a window for the same chat.
|
|
53
|
+
if (typeof existing.isMinimized === 'function' && existing.isMinimized()) {
|
|
54
|
+
existing.restore?.()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (typeof existing.isVisible === 'function' && !existing.isVisible()) {
|
|
58
|
+
existing.show?.()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
existing.focus?.()
|
|
62
|
+
|
|
63
|
+
return existing
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const win = factory(key)
|
|
67
|
+
|
|
68
|
+
if (!win) {
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
windows.set(key, win)
|
|
73
|
+
|
|
74
|
+
// Self-cleanup on close so the registry never holds a destroyed window.
|
|
75
|
+
win.on?.('closed', () => {
|
|
76
|
+
if (windows.get(key) === win) {
|
|
77
|
+
windows.delete(key)
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
return win
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
openOrFocus,
|
|
86
|
+
get: key => windows.get(key),
|
|
87
|
+
has: key => windows.has(key),
|
|
88
|
+
get size() {
|
|
89
|
+
return windows.size
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = {
|
|
95
|
+
buildSessionWindowUrl,
|
|
96
|
+
createSessionWindowRegistry,
|
|
97
|
+
SESSION_WINDOW_MIN_HEIGHT,
|
|
98
|
+
SESSION_WINDOW_MIN_WIDTH
|
|
99
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
const assert = require('node:assert/strict')
|
|
2
|
+
const test = require('node:test')
|
|
3
|
+
|
|
4
|
+
const { buildSessionWindowUrl, createSessionWindowRegistry } = require('./session-windows.cjs')
|
|
5
|
+
|
|
6
|
+
// A minimal fake BrowserWindow: tracks listeners + destroyed state and lets a
|
|
7
|
+
// test fire the 'closed' event, mirroring the slice of the Electron API the
|
|
8
|
+
// registry actually touches.
|
|
9
|
+
function makeFakeWindow() {
|
|
10
|
+
const listeners = {}
|
|
11
|
+
const calls = { focus: 0, show: 0, restore: 0 }
|
|
12
|
+
let destroyed = false
|
|
13
|
+
let minimized = false
|
|
14
|
+
let visible = true
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
on(event, handler) {
|
|
18
|
+
listeners[event] = handler
|
|
19
|
+
|
|
20
|
+
return this
|
|
21
|
+
},
|
|
22
|
+
emit(event) {
|
|
23
|
+
listeners[event]?.()
|
|
24
|
+
},
|
|
25
|
+
isDestroyed: () => destroyed,
|
|
26
|
+
destroy() {
|
|
27
|
+
destroyed = true
|
|
28
|
+
},
|
|
29
|
+
isMinimized: () => minimized,
|
|
30
|
+
setMinimized(value) {
|
|
31
|
+
minimized = value
|
|
32
|
+
},
|
|
33
|
+
isVisible: () => visible,
|
|
34
|
+
setVisible(value) {
|
|
35
|
+
visible = value
|
|
36
|
+
},
|
|
37
|
+
restore() {
|
|
38
|
+
calls.restore += 1
|
|
39
|
+
minimized = false
|
|
40
|
+
},
|
|
41
|
+
show() {
|
|
42
|
+
calls.show += 1
|
|
43
|
+
visible = true
|
|
44
|
+
},
|
|
45
|
+
focus() {
|
|
46
|
+
calls.focus += 1
|
|
47
|
+
},
|
|
48
|
+
calls
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
test('buildSessionWindowUrl puts the secondary flag before the hash route (dev server)', () => {
|
|
53
|
+
const url = buildSessionWindowUrl('abc123', { devServer: 'http://localhost:5173' })
|
|
54
|
+
|
|
55
|
+
assert.equal(url, 'http://localhost:5173/?win=secondary#/abc123')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('buildSessionWindowUrl avoids a double slash when the dev server has a trailing slash', () => {
|
|
59
|
+
const url = buildSessionWindowUrl('abc123', { devServer: 'http://localhost:5173/' })
|
|
60
|
+
|
|
61
|
+
assert.equal(url, 'http://localhost:5173/?win=secondary#/abc123')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('buildSessionWindowUrl encodes the session id in the hash route', () => {
|
|
65
|
+
const url = buildSessionWindowUrl('a b/c', { devServer: 'http://localhost:5173' })
|
|
66
|
+
|
|
67
|
+
// The query flag must precede the '#' or HashRouter would swallow it as the
|
|
68
|
+
// route; the id is URL-encoded so slashes/spaces survive routeSessionId().
|
|
69
|
+
assert.equal(url, 'http://localhost:5173/?win=secondary#/a%20b%2Fc')
|
|
70
|
+
assert.ok(url.indexOf('?win=secondary') < url.indexOf('#'))
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('buildSessionWindowUrl builds a packaged file URL with the flag before the hash', () => {
|
|
74
|
+
const url = buildSessionWindowUrl('abc', { rendererIndexPath: '/opt/app/index.html' })
|
|
75
|
+
|
|
76
|
+
assert.match(url, /^file:\/\/.*index\.html\?win=secondary#\/abc$/)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('buildSessionWindowUrl adds the watch flag for spectator windows, before the hash', () => {
|
|
80
|
+
const url = buildSessionWindowUrl('abc', { devServer: 'http://localhost:5173', watch: true })
|
|
81
|
+
|
|
82
|
+
assert.equal(url, 'http://localhost:5173/?win=secondary&watch=1#/abc')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('buildSessionWindowUrl routes new-session windows to the draft (#/)', () => {
|
|
86
|
+
const url = buildSessionWindowUrl(null, { devServer: 'http://localhost:5173', newSession: true })
|
|
87
|
+
|
|
88
|
+
assert.equal(url, 'http://localhost:5173/?win=secondary&new=1#/')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('registry opens one window per session and focuses on re-open', () => {
|
|
92
|
+
const registry = createSessionWindowRegistry()
|
|
93
|
+
let built = 0
|
|
94
|
+
const win = makeFakeWindow()
|
|
95
|
+
const factory = () => {
|
|
96
|
+
built += 1
|
|
97
|
+
|
|
98
|
+
return win
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const first = registry.openOrFocus('s1', factory)
|
|
102
|
+
const second = registry.openOrFocus('s1', factory)
|
|
103
|
+
|
|
104
|
+
assert.equal(built, 1, 'factory runs once for the same session')
|
|
105
|
+
assert.equal(first, second)
|
|
106
|
+
assert.equal(registry.size, 1)
|
|
107
|
+
assert.equal(win.calls.focus, 1, 'second open focuses the existing window')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('registry restores + shows a minimized/hidden window on re-open', () => {
|
|
111
|
+
const registry = createSessionWindowRegistry()
|
|
112
|
+
const win = makeFakeWindow()
|
|
113
|
+
registry.openOrFocus('s1', () => win)
|
|
114
|
+
|
|
115
|
+
win.setMinimized(true)
|
|
116
|
+
win.setVisible(false)
|
|
117
|
+
registry.openOrFocus('s1', () => win)
|
|
118
|
+
|
|
119
|
+
assert.equal(win.calls.restore, 1)
|
|
120
|
+
assert.equal(win.calls.show, 1)
|
|
121
|
+
assert.equal(win.calls.focus, 1)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('registry drops the entry when the window closes', () => {
|
|
125
|
+
const registry = createSessionWindowRegistry()
|
|
126
|
+
const win = makeFakeWindow()
|
|
127
|
+
registry.openOrFocus('s1', () => win)
|
|
128
|
+
assert.equal(registry.size, 1)
|
|
129
|
+
|
|
130
|
+
win.emit('closed')
|
|
131
|
+
|
|
132
|
+
assert.equal(registry.size, 0)
|
|
133
|
+
assert.equal(registry.has('s1'), false)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('registry rebuilds a fresh window after the previous one was destroyed', () => {
|
|
137
|
+
const registry = createSessionWindowRegistry()
|
|
138
|
+
const first = makeFakeWindow()
|
|
139
|
+
registry.openOrFocus('s1', () => first)
|
|
140
|
+
first.destroy()
|
|
141
|
+
|
|
142
|
+
let built = 0
|
|
143
|
+
const second = makeFakeWindow()
|
|
144
|
+
const result = registry.openOrFocus('s1', () => {
|
|
145
|
+
built += 1
|
|
146
|
+
|
|
147
|
+
return second
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
assert.equal(built, 1, 'a destroyed window is replaced, not focused')
|
|
151
|
+
assert.equal(result, second)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('registry ignores empty / non-string session ids', () => {
|
|
155
|
+
const registry = createSessionWindowRegistry()
|
|
156
|
+
let built = 0
|
|
157
|
+
const factory = () => {
|
|
158
|
+
built += 1
|
|
159
|
+
|
|
160
|
+
return makeFakeWindow()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
assert.equal(registry.openOrFocus('', factory), null)
|
|
164
|
+
assert.equal(registry.openOrFocus(' ', factory), null)
|
|
165
|
+
assert.equal(registry.openOrFocus(null, factory), null)
|
|
166
|
+
assert.equal(registry.openOrFocus(42, factory), null)
|
|
167
|
+
assert.equal(built, 0)
|
|
168
|
+
assert.equal(registry.size, 0)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test('registry trims the session id before keying', () => {
|
|
172
|
+
const registry = createSessionWindowRegistry()
|
|
173
|
+
const win = makeFakeWindow()
|
|
174
|
+
registry.openOrFocus(' s1 ', () => win)
|
|
175
|
+
|
|
176
|
+
assert.equal(registry.has('s1'), true)
|
|
177
|
+
})
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for choosing a remote URL during passive update checks.
|
|
3
|
+
*
|
|
4
|
+
* A public install can end up with `origin=git@github.com:nastechai/nastech-agent.git`.
|
|
5
|
+
* If the user's GitHub SSH key is FIDO2/passkey-backed, a background `git fetch
|
|
6
|
+
* origin` triggers an unexplained hardware-touch prompt. For passive checks
|
|
7
|
+
* against the official repo we substitute the public HTTPS `ls-remote` path,
|
|
8
|
+
* which needs no auth and cannot prompt. Active update/apply flows are left
|
|
9
|
+
* unchanged.
|
|
10
|
+
*
|
|
11
|
+
* Extracted from main.cjs so the security-critical remote detection is unit
|
|
12
|
+
* testable without booting Electron (main.cjs requires('electron') at load).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const OFFICIAL_REPO_HTTPS_URL = 'https://github.com/nastechai/nastech-agent.git'
|
|
16
|
+
const OFFICIAL_REPO_CANONICAL = 'github.com/nastechai/nastech-agent'
|
|
17
|
+
|
|
18
|
+
// Normalize common GitHub remote URL forms to `host/owner/repo` (lowercased,
|
|
19
|
+
// no trailing slash, no .git suffix) so SSH and HTTPS forms of the same repo
|
|
20
|
+
// compare equal.
|
|
21
|
+
function canonicalGitHubRemote(url) {
|
|
22
|
+
if (!url) return ''
|
|
23
|
+
let value = String(url).trim()
|
|
24
|
+
if (value.startsWith('git@github.com:')) {
|
|
25
|
+
value = `github.com/${value.slice('git@github.com:'.length)}`
|
|
26
|
+
} else if (value.startsWith('ssh://git@github.com/')) {
|
|
27
|
+
value = `github.com/${value.slice('ssh://git@github.com/'.length)}`
|
|
28
|
+
} else {
|
|
29
|
+
try {
|
|
30
|
+
const parsed = new URL(value)
|
|
31
|
+
if (parsed.hostname && parsed.pathname) value = `${parsed.hostname}${parsed.pathname}`
|
|
32
|
+
} catch {
|
|
33
|
+
// Leave non-URL forms unchanged.
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
value = value.trim().replace(/\/+$/, '')
|
|
37
|
+
if (value.endsWith('.git')) value = value.slice(0, -4)
|
|
38
|
+
return value.toLowerCase()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isSshRemote(url) {
|
|
42
|
+
const value = String(url || '').trim().toLowerCase()
|
|
43
|
+
return value.startsWith('git@') || value.startsWith('ssh://')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isOfficialSshRemote(url) {
|
|
47
|
+
return isSshRemote(url) && canonicalGitHubRemote(url) === OFFICIAL_REPO_CANONICAL
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = {
|
|
51
|
+
OFFICIAL_REPO_HTTPS_URL,
|
|
52
|
+
OFFICIAL_REPO_CANONICAL,
|
|
53
|
+
canonicalGitHubRemote,
|
|
54
|
+
isSshRemote,
|
|
55
|
+
isOfficialSshRemote
|
|
56
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for electron/update-remote.cjs — the remote-detection helpers that
|
|
3
|
+
* keep passive update checks off the SSH origin for official installs.
|
|
4
|
+
*
|
|
5
|
+
* Run with: node --test electron/update-remote.test.cjs
|
|
6
|
+
* (Wired into npm test:desktop:platforms in package.json.)
|
|
7
|
+
*
|
|
8
|
+
* Why this matters: a public install can carry
|
|
9
|
+
* origin=git@github.com:nastech-ai/NasTech-Agent.git. A background
|
|
10
|
+
* `git fetch origin` then authenticates over SSH and, with a FIDO2/passkey
|
|
11
|
+
* key, triggers an unexplained hardware-touch prompt. isOfficialSshRemote
|
|
12
|
+
* must reliably recognize the official SSH remote (in every URL form,
|
|
13
|
+
* case-insensitively) so the caller can swap in the anonymous HTTPS path —
|
|
14
|
+
* while NOT misclassifying forks, other hosts, or the HTTPS remote (which
|
|
15
|
+
* never prompts and should keep the normal fetch path).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const test = require('node:test')
|
|
19
|
+
const assert = require('node:assert/strict')
|
|
20
|
+
|
|
21
|
+
const {
|
|
22
|
+
OFFICIAL_REPO_HTTPS_URL,
|
|
23
|
+
OFFICIAL_REPO_CANONICAL,
|
|
24
|
+
canonicalGitHubRemote,
|
|
25
|
+
isSshRemote,
|
|
26
|
+
isOfficialSshRemote
|
|
27
|
+
} = require('./update-remote.cjs')
|
|
28
|
+
|
|
29
|
+
test('canonicalGitHubRemote normalizes SSH and HTTPS forms to the same value', () => {
|
|
30
|
+
assert.equal(canonicalGitHubRemote('git@github.com:nastech-ai/NasTech-Agent.git'), OFFICIAL_REPO_CANONICAL)
|
|
31
|
+
assert.equal(canonicalGitHubRemote('git@github.com:nastech-ai/NasTech-Agent'), OFFICIAL_REPO_CANONICAL)
|
|
32
|
+
assert.equal(canonicalGitHubRemote('ssh://git@github.com/nastech-ai/NasTech-Agent.git'), OFFICIAL_REPO_CANONICAL)
|
|
33
|
+
assert.equal(canonicalGitHubRemote('https://github.com/nastech-ai/NasTech-Agent.git'), OFFICIAL_REPO_CANONICAL)
|
|
34
|
+
// Case-insensitive: an uppercased owner still canonicalizes to the same repo.
|
|
35
|
+
assert.equal(canonicalGitHubRemote('git@github.com:NASTECHAI/nastech-agent.git'), OFFICIAL_REPO_CANONICAL)
|
|
36
|
+
// Trailing slashes are stripped.
|
|
37
|
+
assert.equal(canonicalGitHubRemote('https://github.com/nastech-ai/NasTech-Agent/'), OFFICIAL_REPO_CANONICAL)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('canonicalGitHubRemote is empty for falsy input', () => {
|
|
41
|
+
assert.equal(canonicalGitHubRemote(''), '')
|
|
42
|
+
assert.equal(canonicalGitHubRemote(null), '')
|
|
43
|
+
assert.equal(canonicalGitHubRemote(undefined), '')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('isSshRemote detects scp-like and ssh:// forms only', () => {
|
|
47
|
+
assert.equal(isSshRemote('git@github.com:nastech-ai/NasTech-Agent.git'), true)
|
|
48
|
+
assert.equal(isSshRemote('ssh://git@github.com/nastech-ai/NasTech-Agent.git'), true)
|
|
49
|
+
assert.equal(isSshRemote('https://github.com/nastech-ai/NasTech-Agent.git'), false)
|
|
50
|
+
assert.equal(isSshRemote(''), false)
|
|
51
|
+
assert.equal(isSshRemote(null), false)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('isOfficialSshRemote is true only for the official repo over SSH', () => {
|
|
55
|
+
assert.equal(isOfficialSshRemote('git@github.com:nastech-ai/NasTech-Agent.git'), true)
|
|
56
|
+
assert.equal(isOfficialSshRemote('git@github.com:nastech-ai/NasTech-Agent'), true)
|
|
57
|
+
assert.equal(isOfficialSshRemote('ssh://git@github.com/nastech-ai/NasTech-Agent.git'), true)
|
|
58
|
+
// Case-insensitive owner/repo match.
|
|
59
|
+
assert.equal(isOfficialSshRemote('git@github.com:NASTECHAI/nastech-agent.git'), true)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('isOfficialSshRemote does NOT match forks, other hosts, or HTTPS', () => {
|
|
63
|
+
// A fork over SSH belongs to the user — fetching it is their own remote,
|
|
64
|
+
// not the official upstream, so the SSH-avoidance swap must not apply.
|
|
65
|
+
assert.equal(isOfficialSshRemote('git@github.com:someuser/nastech-agent.git'), false)
|
|
66
|
+
// Same repo name on a different host is not the official repo.
|
|
67
|
+
assert.equal(isOfficialSshRemote('git@gitlab.com:nastech-ai/NasTech-Agent.git'), false)
|
|
68
|
+
// HTTPS to the official repo never prompts for SSH/FIDO2, so it keeps the
|
|
69
|
+
// normal fetch path — must not be flagged as an official SSH remote.
|
|
70
|
+
assert.equal(isOfficialSshRemote('https://github.com/nastech-ai/NasTech-Agent.git'), false)
|
|
71
|
+
assert.equal(isOfficialSshRemote(''), false)
|
|
72
|
+
assert.equal(isOfficialSshRemote(null), false)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('OFFICIAL_REPO_HTTPS_URL canonicalizes to OFFICIAL_REPO_CANONICAL', () => {
|
|
76
|
+
// Invariant: the URL we substitute in must be the same repo we detect.
|
|
77
|
+
assert.equal(canonicalGitHubRemote(OFFICIAL_REPO_HTTPS_URL), OFFICIAL_REPO_CANONICAL)
|
|
78
|
+
})
|