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,365 @@
|
|
|
1
|
+
import { atom, computed } from 'nanostores'
|
|
2
|
+
|
|
3
|
+
import { getProfiles, setApiRequestProfile } from '@/nastech'
|
|
4
|
+
import { queryClient } from '@/lib/query-client'
|
|
5
|
+
import {
|
|
6
|
+
arraysEqual,
|
|
7
|
+
persistBoolean,
|
|
8
|
+
persistStringArray,
|
|
9
|
+
persistStringRecord,
|
|
10
|
+
storedBoolean,
|
|
11
|
+
storedStringArray,
|
|
12
|
+
storedStringRecord
|
|
13
|
+
} from '@/lib/storage'
|
|
14
|
+
import { $gateway, ensureGatewayForProfile } from '@/store/gateway'
|
|
15
|
+
import type { ProfileInfo } from '@/types/nastech'
|
|
16
|
+
|
|
17
|
+
// Canonical key for a profile: trimmed, empty → "default". Used everywhere we
|
|
18
|
+
// compare a session's owning profile against the live gateway's profile.
|
|
19
|
+
export function normalizeProfileKey(name: string | null | undefined): string {
|
|
20
|
+
const value = (name ?? '').trim()
|
|
21
|
+
|
|
22
|
+
return value || 'default'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// The profile the running local backend is actually scoped to (mirrors
|
|
26
|
+
// /api/profiles/active `current`). "default" is the root ~/.NASTECH. This is the
|
|
27
|
+
// display source of truth for the statusbar pill; the desktop's *stored*
|
|
28
|
+
// preference (which may be unset) lives in the Electron main process.
|
|
29
|
+
export const $activeProfile = atom<string>('default')
|
|
30
|
+
|
|
31
|
+
// Cached profile list for the picker. Refreshed lazily; the dropdown also
|
|
32
|
+
// re-fetches on open so a profile created elsewhere shows up.
|
|
33
|
+
export const $profiles = atom<ProfileInfo[]>([])
|
|
34
|
+
|
|
35
|
+
export function setActiveProfile(name: string): void {
|
|
36
|
+
$activeProfile.set(name || 'default')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Rail order ─────────────────────────────────────────────────────────────
|
|
40
|
+
// User-defined order for the named (non-default) profile squares in the rail.
|
|
41
|
+
// Names absent from the list fall back to alphabetical, appended at the tail —
|
|
42
|
+
// so a freshly created profile lands at the end until the user drags it.
|
|
43
|
+
const PROFILE_ORDER_STORAGE_KEY = 'NASTECH.desktop.profileOrder'
|
|
44
|
+
|
|
45
|
+
export const $profileOrder = atom<string[]>(storedStringArray(PROFILE_ORDER_STORAGE_KEY))
|
|
46
|
+
|
|
47
|
+
$profileOrder.subscribe(value => persistStringArray(PROFILE_ORDER_STORAGE_KEY, [...value]))
|
|
48
|
+
|
|
49
|
+
export function setProfileOrder(names: string[]): void {
|
|
50
|
+
if (!arraysEqual($profileOrder.get(), names)) {
|
|
51
|
+
$profileOrder.set(names)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Sort items by the stored order; unordered names alphabetise at the tail.
|
|
56
|
+
export function sortByProfileOrder<T extends { name: string }>(items: T[], order: string[]): T[] {
|
|
57
|
+
const rank = new Map(order.map((name, index) => [name, index]))
|
|
58
|
+
|
|
59
|
+
return [...items].sort((a, b) => {
|
|
60
|
+
const ra = rank.get(a.name)
|
|
61
|
+
const rb = rank.get(b.name)
|
|
62
|
+
|
|
63
|
+
if (ra != null && rb != null) {
|
|
64
|
+
return ra - rb
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return ra != null ? -1 : rb != null ? 1 : a.name.localeCompare(b.name)
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── Rail colors ────────────────────────────────────────────────────────────
|
|
72
|
+
// Optional per-profile color override (long-press a rail square to pick). Absent
|
|
73
|
+
// names fall back to the deterministic hue from profileColor(); a local-only
|
|
74
|
+
// cosmetic preference, so single-profile users never touch it.
|
|
75
|
+
const PROFILE_COLORS_STORAGE_KEY = 'NASTECH.desktop.profileColors'
|
|
76
|
+
|
|
77
|
+
export const $profileColors = atom<Record<string, string>>(storedStringRecord(PROFILE_COLORS_STORAGE_KEY))
|
|
78
|
+
|
|
79
|
+
$profileColors.subscribe(value => persistStringRecord(PROFILE_COLORS_STORAGE_KEY, value))
|
|
80
|
+
|
|
81
|
+
// Set (or, with null, clear) a profile's color override.
|
|
82
|
+
export function setProfileColor(name: string, color: null | string): void {
|
|
83
|
+
const key = normalizeProfileKey(name)
|
|
84
|
+
const next = { ...$profileColors.get() }
|
|
85
|
+
|
|
86
|
+
if (color) {
|
|
87
|
+
next[key] = color
|
|
88
|
+
} else {
|
|
89
|
+
delete next[key]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
$profileColors.set(next)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface ActiveProfileResponse {
|
|
96
|
+
active: string
|
|
97
|
+
current: string
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Pull the running backend's current profile + the available profile list.
|
|
101
|
+
// Best-effort: failures (backend not up yet) leave the prior values intact.
|
|
102
|
+
export async function refreshActiveProfile(): Promise<void> {
|
|
103
|
+
try {
|
|
104
|
+
const res = await window.NASTECHDesktop.api<ActiveProfileResponse>({ path: '/api/profiles/active' })
|
|
105
|
+
|
|
106
|
+
setActiveProfile(res.current || 'default')
|
|
107
|
+
} catch {
|
|
108
|
+
// Backend may not be ready; keep the last known value.
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const { profiles } = await getProfiles()
|
|
113
|
+
$profiles.set(profiles)
|
|
114
|
+
} catch {
|
|
115
|
+
// Leave the cached list in place.
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Persist the choice and relaunch the backend under the new NASTECH_HOME. The
|
|
120
|
+
// main process reloads the window, so this normally never returns to the caller
|
|
121
|
+
// (the renderer is torn down). We optimistically reflect the selection first so
|
|
122
|
+
// the pill updates instantly if the reload is delayed.
|
|
123
|
+
export async function switchProfile(name: string): Promise<void> {
|
|
124
|
+
if (!name || name === $activeProfile.get()) {
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
setActiveProfile(name)
|
|
129
|
+
await window.NASTECHDesktop.profile.set(name)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Swap-minimal gateway routing ──────────────────────────────────────────
|
|
133
|
+
// One live gateway at a time. When the user opens/sends a session whose profile
|
|
134
|
+
// differs from the gateway's current profile, we lazily reconnect the single
|
|
135
|
+
// gateway to that profile's backend (spawned on demand by the Electron pool).
|
|
136
|
+
// A single-profile user never triggers a swap, so their path is unchanged.
|
|
137
|
+
|
|
138
|
+
// The profile the live gateway WebSocket is currently connected to. Initialized
|
|
139
|
+
// to the primary (window) backend's profile on boot.
|
|
140
|
+
export const $activeGatewayProfile = atom<string>('default')
|
|
141
|
+
|
|
142
|
+
// Profile for the NEXT new chat (chosen via the new-chat picker). null = primary
|
|
143
|
+
// / default, so single-profile users are unaffected.
|
|
144
|
+
export const $newChatProfile = atom<string | null>(null)
|
|
145
|
+
|
|
146
|
+
// Bumped whenever the profile context actually changes (switch or create). The
|
|
147
|
+
// chat controller subscribes and drops to a fresh new-session draft, so the
|
|
148
|
+
// session you were in doesn't stay sticky across a profile switch.
|
|
149
|
+
export const $freshSessionRequest = atom(0)
|
|
150
|
+
|
|
151
|
+
function requestFreshSession(): void {
|
|
152
|
+
$freshSessionRequest.set($freshSessionRequest.get() + 1)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Route profile-scoped REST settings (config/env/skills/tools/model/…) to the
|
|
156
|
+
// profile the live gateway is currently on, and drop cached settings from the
|
|
157
|
+
// previous profile so pages refetch against the right backend. Fires once
|
|
158
|
+
// immediately (no real change → no invalidation), so single-profile users just
|
|
159
|
+
// get "default" (→ the primary backend) with no extra fetches.
|
|
160
|
+
let _lastRoutedProfile: string | null = null
|
|
161
|
+
|
|
162
|
+
$activeGatewayProfile.subscribe(value => {
|
|
163
|
+
const key = normalizeProfileKey(value)
|
|
164
|
+
setApiRequestProfile(key)
|
|
165
|
+
|
|
166
|
+
if (_lastRoutedProfile !== null && _lastRoutedProfile !== key) {
|
|
167
|
+
// Profile-scoped settings + the unified session list are now stale.
|
|
168
|
+
void queryClient.invalidateQueries()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
_lastRoutedProfile = key
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
// Target profile while a gateway swap is mid-flight (spawning/reconnecting that
|
|
175
|
+
// profile's backend), else null. Drives the chat's "waking up <profile>" loader
|
|
176
|
+
// so a lazy spawn doesn't read as a hang. Single-profile users never swap.
|
|
177
|
+
export const $gatewaySwapTarget = atom<string | null>(null)
|
|
178
|
+
|
|
179
|
+
let gatewaySwitch: Promise<void> | null = null
|
|
180
|
+
|
|
181
|
+
// Make `profile`'s backend the active gateway, lazily opening its socket if it
|
|
182
|
+
// isn't live yet. Unlike the old single-socket swap, background profiles keep
|
|
183
|
+
// their sockets — so their sessions keep streaming concurrently. A null/empty
|
|
184
|
+
// target means "no explicit profile" → keep the current gateway (a plain new
|
|
185
|
+
// chat stays put; single-profile users never leave the primary).
|
|
186
|
+
export async function ensureGatewayProfile(profile: string | null | undefined): Promise<void> {
|
|
187
|
+
if (profile == null || !String(profile).trim()) {
|
|
188
|
+
// "No explicit profile" = use the current gateway. But if an explicit swap
|
|
189
|
+
// (e.g. the user just picked a profile in the switcher) is still in flight,
|
|
190
|
+
// let it settle first so a new chat doesn't race session.create against a
|
|
191
|
+
// half-open socket and land on the wrong backend.
|
|
192
|
+
if (gatewaySwitch) {
|
|
193
|
+
await gatewaySwitch.catch(() => undefined)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const target = normalizeProfileKey(profile)
|
|
200
|
+
|
|
201
|
+
if (normalizeProfileKey($activeGatewayProfile.get()) === target && $gateway.get()) {
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Serialize concurrent activations so two rapid session switches don't race
|
|
206
|
+
// the active pointer.
|
|
207
|
+
if (gatewaySwitch) {
|
|
208
|
+
await gatewaySwitch.catch(() => undefined)
|
|
209
|
+
|
|
210
|
+
if (normalizeProfileKey($activeGatewayProfile.get()) === target && $gateway.get()) {
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
$gatewaySwapTarget.set(target)
|
|
216
|
+
gatewaySwitch = (async () => {
|
|
217
|
+
// ensureGatewayForProfile opens (or reuses) the target's socket and points
|
|
218
|
+
// the active gateway at it — without closing the profile you came from.
|
|
219
|
+
await ensureGatewayForProfile(target)
|
|
220
|
+
$activeGatewayProfile.set(target)
|
|
221
|
+
})()
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
await gatewaySwitch
|
|
225
|
+
} finally {
|
|
226
|
+
gatewaySwitch = null
|
|
227
|
+
$gatewaySwapTarget.set(null)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ── Sidebar profile scope (the "workspace switcher" model) ─────────────────
|
|
232
|
+
// Mirrors how Slack/VS Code/Linear do multi-context: you're "in" one profile at
|
|
233
|
+
// a time and the sidebar shows only that profile's sessions (clean rows, no
|
|
234
|
+
// per-row tags). The lone exception is an explicit "All profiles" mode that
|
|
235
|
+
// fans every profile's sessions into one grouped, browsable list.
|
|
236
|
+
|
|
237
|
+
export const ALL_PROFILES = '__all__'
|
|
238
|
+
|
|
239
|
+
const SHOW_ALL_PROFILES_STORAGE_KEY = 'NASTECH.desktop.showAllProfiles'
|
|
240
|
+
|
|
241
|
+
// Opt-in unified view. When false, scope follows the live gateway profile, so
|
|
242
|
+
// single-profile users (who never see the switcher) are completely unaffected.
|
|
243
|
+
export const $showAllProfiles = atom<boolean>(storedBoolean(SHOW_ALL_PROFILES_STORAGE_KEY, false))
|
|
244
|
+
|
|
245
|
+
$showAllProfiles.subscribe(value => persistBoolean(SHOW_ALL_PROFILES_STORAGE_KEY, value))
|
|
246
|
+
|
|
247
|
+
// The profile context the sidebar is currently showing: a concrete profile key,
|
|
248
|
+
// or ALL_PROFILES for the unified grouped view. Concrete scope is tied to the
|
|
249
|
+
// gateway so opening/selecting a profile (which swaps the gateway) moves the
|
|
250
|
+
// whole sidebar with it — a real context switch, not a separate filter to keep
|
|
251
|
+
// in sync.
|
|
252
|
+
export const $profileScope = computed([$showAllProfiles, $activeGatewayProfile], (showAll, gateway) =>
|
|
253
|
+
showAll ? ALL_PROFILES : normalizeProfileKey(gateway)
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
// Switch the active context to `name`: leave "All profiles" mode, point new
|
|
257
|
+
// chats at it, and swap the single live gateway onto its backend (which moves
|
|
258
|
+
// $activeGatewayProfile → name, so $profileScope follows).
|
|
259
|
+
export function selectProfile(name: string): void {
|
|
260
|
+
const target = normalizeProfileKey(name)
|
|
261
|
+
// Switching profiles (or coming back from the all-profiles browse view) starts
|
|
262
|
+
// fresh; re-tapping the profile you're already in leaves your session be.
|
|
263
|
+
const switching = $showAllProfiles.get() || target !== normalizeProfileKey($activeGatewayProfile.get())
|
|
264
|
+
$showAllProfiles.set(false)
|
|
265
|
+
$newChatProfile.set(target)
|
|
266
|
+
|
|
267
|
+
if (switching) {
|
|
268
|
+
requestFreshSession()
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
void ensureGatewayProfile(target)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Start a fresh session in `name` WITHOUT collapsing the "All profiles" browse
|
|
275
|
+
// view. Unlike selectProfile, it leaves $showAllProfiles untouched, so the
|
|
276
|
+
// unified sidebar stays put — used by the per-profile "+" in the all-profiles
|
|
277
|
+
// session list, where switching scope would throw away the browse state the user
|
|
278
|
+
// is in. Points new chats at the profile and opens its backend so the next
|
|
279
|
+
// message lands in the right place.
|
|
280
|
+
export function newSessionInProfile(name: string): void {
|
|
281
|
+
const target = normalizeProfileKey(name)
|
|
282
|
+
$newChatProfile.set(target)
|
|
283
|
+
requestFreshSession()
|
|
284
|
+
void ensureGatewayProfile(target)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function setShowAllProfiles(value: boolean): void {
|
|
288
|
+
$showAllProfiles.set(value)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function toggleShowAllProfiles(): void {
|
|
292
|
+
$showAllProfiles.set(!$showAllProfiles.get())
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ── Hotkey-driven profile switching ────────────────────────────────────────
|
|
296
|
+
// Positional + relative navigation for the rail, used by the keybind runtime.
|
|
297
|
+
// The ordered list is [default, ...named-in-rail-order]; switching is a no-op
|
|
298
|
+
// when the slot is empty so unused ⌘N keys stay harmless.
|
|
299
|
+
|
|
300
|
+
function orderedProfileKeys(): string[] {
|
|
301
|
+
const profiles = $profiles.get()
|
|
302
|
+
|
|
303
|
+
const named = sortByProfileOrder(
|
|
304
|
+
profiles.filter(profile => !profile.is_default),
|
|
305
|
+
$profileOrder.get()
|
|
306
|
+
).map(profile => normalizeProfileKey(profile.name))
|
|
307
|
+
|
|
308
|
+
const hasDefault = profiles.some(profile => profile.is_default)
|
|
309
|
+
|
|
310
|
+
return hasDefault ? ['default', ...named] : named
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Switch to the default (root ~/.NASTECH) profile — bound to ⌘1.
|
|
314
|
+
export function switchToDefaultProfile(): void {
|
|
315
|
+
const def = $profiles.get().find(profile => profile.is_default)
|
|
316
|
+
|
|
317
|
+
selectProfile(def ? def.name : 'default')
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Switch to the Nth named (non-default) profile in rail order (1-based).
|
|
321
|
+
export function switchProfileToSlot(slot: number): void {
|
|
322
|
+
const named = sortByProfileOrder(
|
|
323
|
+
$profiles.get().filter(profile => !profile.is_default),
|
|
324
|
+
$profileOrder.get()
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
const target = named[slot - 1]
|
|
328
|
+
|
|
329
|
+
if (target) {
|
|
330
|
+
selectProfile(target.name)
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Step to the next/previous profile in the rail, wrapping around.
|
|
335
|
+
export function cycleProfile(direction: 1 | -1): void {
|
|
336
|
+
const keys = orderedProfileKeys()
|
|
337
|
+
|
|
338
|
+
if (keys.length < 2) {
|
|
339
|
+
return
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const current = $showAllProfiles.get() ? -1 : keys.indexOf(normalizeProfileKey($activeGatewayProfile.get()))
|
|
343
|
+
const start = current < 0 ? (direction === 1 ? -1 : 0) : current
|
|
344
|
+
const next = (start + direction + keys.length) % keys.length
|
|
345
|
+
|
|
346
|
+
selectProfile(keys[next])
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Bumped to ask the rail to open its "create profile" dialog (the dialog state
|
|
350
|
+
// is local to the rail component; this lets a global hotkey trigger it).
|
|
351
|
+
export const $profileCreateRequest = atom(0)
|
|
352
|
+
|
|
353
|
+
export function requestProfileCreate(): void {
|
|
354
|
+
$profileCreateRequest.set($profileCreateRequest.get() + 1)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Keepalive ping for the active pool backend so the main-process idle reaper
|
|
358
|
+
// (which can't see the direct renderer↔backend WS) spares it. No-op for the
|
|
359
|
+
// primary/default backend, which is never pooled.
|
|
360
|
+
export function touchActiveGatewayBackend(): void {
|
|
361
|
+
// Always ping: the main process no-ops for non-pool (primary) backends, so we
|
|
362
|
+
// don't need to know which profile is primary from here.
|
|
363
|
+
const target = normalizeProfileKey($activeGatewayProfile.get())
|
|
364
|
+
void window.NASTECHDesktop?.touchBackend?.(target).catch(() => undefined)
|
|
365
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
$approvalRequest,
|
|
5
|
+
$secretRequest,
|
|
6
|
+
$sudoRequest,
|
|
7
|
+
clearAllPrompts,
|
|
8
|
+
clearApprovalRequest,
|
|
9
|
+
clearSecretRequest,
|
|
10
|
+
clearSudoRequest,
|
|
11
|
+
setApprovalRequest,
|
|
12
|
+
setSecretRequest,
|
|
13
|
+
setSudoRequest
|
|
14
|
+
} from './prompts'
|
|
15
|
+
import { $activeSessionId } from './session'
|
|
16
|
+
|
|
17
|
+
// Prompts are parked per-session; the exported $*Request views are scoped to the
|
|
18
|
+
// active session, so each test focuses the session it's asserting on.
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
$activeSessionId.set('s1')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
clearAllPrompts()
|
|
25
|
+
$activeSessionId.set(null)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe('approval prompt store', () => {
|
|
29
|
+
it('holds the active session-keyed approval request', () => {
|
|
30
|
+
setApprovalRequest({ command: 'rm -rf /tmp/x', description: 'recursive delete', sessionId: 's1' })
|
|
31
|
+
|
|
32
|
+
expect($approvalRequest.get()).toEqual({
|
|
33
|
+
command: 'rm -rf /tmp/x',
|
|
34
|
+
description: 'recursive delete',
|
|
35
|
+
sessionId: 's1'
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('parks a background session prompt out of the active view', () => {
|
|
40
|
+
setApprovalRequest({ command: 'x', description: 'd', sessionId: 's2' })
|
|
41
|
+
|
|
42
|
+
// Not visible while s1 is focused …
|
|
43
|
+
expect($approvalRequest.get()).toBeNull()
|
|
44
|
+
|
|
45
|
+
// … but surfaces once the user switches to the session that raised it.
|
|
46
|
+
$activeSessionId.set('s2')
|
|
47
|
+
expect($approvalRequest.get()?.sessionId).toBe('s2')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('clears the active session prompt', () => {
|
|
51
|
+
setApprovalRequest({ command: 'x', description: 'd', sessionId: 's1' })
|
|
52
|
+
clearApprovalRequest('s1')
|
|
53
|
+
|
|
54
|
+
expect($approvalRequest.get()).toBeNull()
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('sudo prompt store', () => {
|
|
59
|
+
it('clears only when the request id matches the in-flight prompt', () => {
|
|
60
|
+
setSudoRequest({ requestId: 'abc', sessionId: 's1' })
|
|
61
|
+
|
|
62
|
+
// A stale clear for a different request must NOT drop the live prompt —
|
|
63
|
+
// otherwise a late response to a prior sudo ask would dismiss the current
|
|
64
|
+
// one and leave the agent blocked.
|
|
65
|
+
clearSudoRequest('s1', 'stale')
|
|
66
|
+
expect($sudoRequest.get()).toEqual({ requestId: 'abc', sessionId: 's1' })
|
|
67
|
+
|
|
68
|
+
clearSudoRequest('s1', 'abc')
|
|
69
|
+
expect($sudoRequest.get()).toBeNull()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('clears unconditionally when no request id is given', () => {
|
|
73
|
+
setSudoRequest({ requestId: 'abc', sessionId: 's1' })
|
|
74
|
+
clearSudoRequest('s1')
|
|
75
|
+
|
|
76
|
+
expect($sudoRequest.get()).toBeNull()
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('secret prompt store', () => {
|
|
81
|
+
it('carries env var and prompt, and clears on id match', () => {
|
|
82
|
+
setSecretRequest({ requestId: 'r1', envVar: 'OPENAI_API_KEY', prompt: 'Paste your key', sessionId: 's1' })
|
|
83
|
+
|
|
84
|
+
expect($secretRequest.get()).toEqual({
|
|
85
|
+
requestId: 'r1',
|
|
86
|
+
envVar: 'OPENAI_API_KEY',
|
|
87
|
+
prompt: 'Paste your key',
|
|
88
|
+
sessionId: 's1'
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
clearSecretRequest('s1', 'mismatch')
|
|
92
|
+
expect($secretRequest.get()).not.toBeNull()
|
|
93
|
+
|
|
94
|
+
clearSecretRequest('s1', 'r1')
|
|
95
|
+
expect($secretRequest.get()).toBeNull()
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe('clearAllPrompts', () => {
|
|
100
|
+
it('drops every kind for one session at once (turn end / interrupt)', () => {
|
|
101
|
+
setApprovalRequest({ command: 'x', description: 'd', sessionId: 's1' })
|
|
102
|
+
setSudoRequest({ requestId: 'abc', sessionId: 's1' })
|
|
103
|
+
setSecretRequest({ requestId: 'r1', envVar: 'E', prompt: 'p', sessionId: 's1' })
|
|
104
|
+
|
|
105
|
+
clearAllPrompts('s1')
|
|
106
|
+
|
|
107
|
+
expect($approvalRequest.get()).toBeNull()
|
|
108
|
+
expect($sudoRequest.get()).toBeNull()
|
|
109
|
+
expect($secretRequest.get()).toBeNull()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('leaves other sessions parked prompts intact', () => {
|
|
113
|
+
setApprovalRequest({ command: 'x', description: 'd', sessionId: 's1' })
|
|
114
|
+
setApprovalRequest({ command: 'y', description: 'e', sessionId: 's2' })
|
|
115
|
+
|
|
116
|
+
clearAllPrompts('s1')
|
|
117
|
+
|
|
118
|
+
$activeSessionId.set('s2')
|
|
119
|
+
expect($approvalRequest.get()?.command).toBe('y')
|
|
120
|
+
})
|
|
121
|
+
})
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { atom, computed, type ReadableAtom } from 'nanostores'
|
|
2
|
+
|
|
3
|
+
import { $activeSessionId } from './session'
|
|
4
|
+
|
|
5
|
+
// Blocking interactive prompts the gateway raises mid-turn. Each maps to a
|
|
6
|
+
// `*.request` event the Python side emits while it blocks the agent thread
|
|
7
|
+
// waiting for a `*.respond` RPC. Without a renderer for these, the agent
|
|
8
|
+
// silently stalls until its timeout (default 5 min) and the tool is BLOCKED.
|
|
9
|
+
//
|
|
10
|
+
// Like clarify, every prompt is parked under the runtime session id that raised
|
|
11
|
+
// it (not one shared slot), so a *background* session running concurrently can
|
|
12
|
+
// raise an approval/sudo/secret prompt and have it wait — surfaced via the
|
|
13
|
+
// sidebar "needs input" badge — until the user switches to that chat. The
|
|
14
|
+
// exported $*Request view is scoped to the active session, so a background
|
|
15
|
+
// prompt never hijacks the foreground.
|
|
16
|
+
|
|
17
|
+
const keyFor = (sessionId: string | null | undefined): string => sessionId ?? ''
|
|
18
|
+
|
|
19
|
+
interface KeyedPrompt {
|
|
20
|
+
sessionId: string | null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface PromptStore<T extends KeyedPrompt> {
|
|
24
|
+
$active: ReadableAtom<null | T>
|
|
25
|
+
clear: (sessionId?: string | null, requestId?: string) => void
|
|
26
|
+
reset: () => void
|
|
27
|
+
set: (request: T) => void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// One per-session prompt kind: a map keyed by session, plus an active-session
|
|
31
|
+
// view for the overlays. `clear` drops one session's entry (a request-id
|
|
32
|
+
// mismatch is a no-op so a stale resolve can't wipe a newer prompt); with no
|
|
33
|
+
// session hint it drops every entry, optionally filtered by request id.
|
|
34
|
+
function keyedPromptStore<T extends KeyedPrompt>(): PromptStore<T> {
|
|
35
|
+
const $all = atom<Record<string, T>>({})
|
|
36
|
+
const idOf = (value: T): string | undefined => (value as { requestId?: string }).requestId
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
$active: computed([$all, $activeSessionId], (all, activeId) => all[keyFor(activeId)] ?? null),
|
|
40
|
+
reset: () => $all.set({}),
|
|
41
|
+
set: request => $all.set({ ...$all.get(), [keyFor(request.sessionId)]: request }),
|
|
42
|
+
clear(sessionId, requestId) {
|
|
43
|
+
const all = $all.get()
|
|
44
|
+
|
|
45
|
+
if (sessionId !== undefined) {
|
|
46
|
+
const key = keyFor(sessionId)
|
|
47
|
+
const current = all[key]
|
|
48
|
+
|
|
49
|
+
if (current && !(requestId && idOf(current) !== requestId)) {
|
|
50
|
+
const next = { ...all }
|
|
51
|
+
delete next[key]
|
|
52
|
+
$all.set(next)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const next = Object.fromEntries(Object.entries(all).filter(([, v]) => requestId && idOf(v) !== requestId))
|
|
59
|
+
|
|
60
|
+
if (Object.keys(next).length !== Object.keys(all).length) {
|
|
61
|
+
$all.set(next as Record<string, T>)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Approval is session-keyed on the backend (one in-flight approval per session,
|
|
68
|
+
// resolved via approval.respond {choice, session_id}). It carries no request_id,
|
|
69
|
+
// unlike sudo/secret which are _block()-style request/response.
|
|
70
|
+
export interface ApprovalRequest extends KeyedPrompt {
|
|
71
|
+
command: string
|
|
72
|
+
description: string
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface SudoRequest extends KeyedPrompt {
|
|
76
|
+
requestId: string
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface SecretRequest extends KeyedPrompt {
|
|
80
|
+
envVar: string
|
|
81
|
+
prompt: string
|
|
82
|
+
requestId: string
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const approval = keyedPromptStore<ApprovalRequest>()
|
|
86
|
+
const sudo = keyedPromptStore<SudoRequest>()
|
|
87
|
+
const secret = keyedPromptStore<SecretRequest>()
|
|
88
|
+
|
|
89
|
+
export const $approvalRequest = approval.$active
|
|
90
|
+
export const setApprovalRequest = approval.set
|
|
91
|
+
export const clearApprovalRequest = approval.clear
|
|
92
|
+
|
|
93
|
+
export const $sudoRequest = sudo.$active
|
|
94
|
+
export const setSudoRequest = sudo.set
|
|
95
|
+
export const clearSudoRequest = sudo.clear
|
|
96
|
+
|
|
97
|
+
export const $secretRequest = secret.$active
|
|
98
|
+
export const setSecretRequest = secret.set
|
|
99
|
+
export const clearSecretRequest = secret.clear
|
|
100
|
+
|
|
101
|
+
// Drop in-flight prompts for `sessionId` (a turn ended) across all three kinds —
|
|
102
|
+
// or every parked prompt when no session is given (global reset / tests).
|
|
103
|
+
export function clearAllPrompts(sessionId?: string | null): void {
|
|
104
|
+
if (sessionId === undefined) {
|
|
105
|
+
approval.reset()
|
|
106
|
+
sudo.reset()
|
|
107
|
+
secret.reset()
|
|
108
|
+
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
approval.clear(sessionId)
|
|
113
|
+
sudo.clear(sessionId)
|
|
114
|
+
secret.clear(sessionId)
|
|
115
|
+
}
|