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,290 @@
|
|
|
1
|
+
import type { ConnectionState, GatewayEvent } from '@NASTECH/shared'
|
|
2
|
+
import { atom } from 'nanostores'
|
|
3
|
+
|
|
4
|
+
import { NasTechGateway } from '@/nastech'
|
|
5
|
+
import { resolveGatewayWsUrl } from '@/lib/gateway-ws-url'
|
|
6
|
+
import { setGatewayState } from '@/store/session'
|
|
7
|
+
|
|
8
|
+
// ── Multi-profile gateway routing ──────────────────────────────────────────
|
|
9
|
+
// Concurrent sessions across profiles need concurrent sockets: the renderer's
|
|
10
|
+
// event handler is already session-keyed, so the only thing stopping two
|
|
11
|
+
// profiles streaming at once was the single swapping socket. We keep that one
|
|
12
|
+
// socket as the PRIMARY (window) backend — owned by use-gateway-boot, with all
|
|
13
|
+
// its boot-progress / sleep-wake machinery — and add one persistent SECONDARY
|
|
14
|
+
// socket per *other* profile that has live work. Every socket feeds the same
|
|
15
|
+
// handleGatewayEvent, so background sessions keep painting. Single-profile users
|
|
16
|
+
// only ever have the primary, so their path is byte-for-byte unchanged.
|
|
17
|
+
|
|
18
|
+
const normKey = (profile: string | null | undefined): string => (profile ?? '').trim() || 'default'
|
|
19
|
+
|
|
20
|
+
// Read connection state through a call so TS control-flow analysis doesn't
|
|
21
|
+
// narrow the getter to a constant across guards (it genuinely changes).
|
|
22
|
+
const isOpen = (gateway: NasTechGateway | null): boolean => gateway?.connectionState === 'open'
|
|
23
|
+
|
|
24
|
+
// The active gateway instance, exposed for inline message-stream components
|
|
25
|
+
// (e.g. inline ClarifyTool, model overlays) that call gateway methods without
|
|
26
|
+
// the instance threaded down through props.
|
|
27
|
+
export const $gateway = atom<NasTechGateway | null>(null)
|
|
28
|
+
|
|
29
|
+
interface RegistryConfig {
|
|
30
|
+
onEvent: (event: GatewayEvent) => void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let config: RegistryConfig | null = null
|
|
34
|
+
|
|
35
|
+
export function configureGatewayRegistry(cfg: RegistryConfig): void {
|
|
36
|
+
config = cfg
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Primary (window) backend ───────────────────────────────────────────────
|
|
40
|
+
let primaryGateway: NasTechGateway | null = null
|
|
41
|
+
let primaryProfile = 'default'
|
|
42
|
+
|
|
43
|
+
export function setPrimaryGateway(gateway: NasTechGateway | null, profile = 'default'): void {
|
|
44
|
+
primaryGateway = gateway
|
|
45
|
+
primaryProfile = normKey(profile)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Secondary (pool) backends ──────────────────────────────────────────────
|
|
49
|
+
interface Secondary {
|
|
50
|
+
profile: string
|
|
51
|
+
gateway: NasTechGateway
|
|
52
|
+
offEvent: () => void
|
|
53
|
+
offState: () => void
|
|
54
|
+
reconnectTimer: ReturnType<typeof setTimeout> | null
|
|
55
|
+
reconnectAttempt: number
|
|
56
|
+
reconnecting: boolean
|
|
57
|
+
// While true the entry auto-reconnects on drop; pruning flips it off so a
|
|
58
|
+
// deliberate close doesn't trigger the backoff loop.
|
|
59
|
+
wantOpen: boolean
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const secondaries = new Map<string, Secondary>()
|
|
63
|
+
|
|
64
|
+
let activeKey = 'default'
|
|
65
|
+
|
|
66
|
+
export function isActivePrimary(): boolean {
|
|
67
|
+
return activeKey === primaryProfile
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function activeGateway(): NasTechGateway | null {
|
|
71
|
+
if (activeKey === primaryProfile) {
|
|
72
|
+
return primaryGateway
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return secondaries.get(activeKey)?.gateway ?? primaryGateway
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Mirror a backend's connection state into the global composer state, but only
|
|
79
|
+
// when that backend is the one the user is currently looking at. Lets the
|
|
80
|
+
// composer reflect the active profile's socket without a background reconnect
|
|
81
|
+
// flipping the foreground enabled/disabled state.
|
|
82
|
+
function reportGatewayState(profile: string, state: ConnectionState): void {
|
|
83
|
+
if (normKey(profile) === activeKey) {
|
|
84
|
+
setGatewayState(state)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function reportPrimaryGatewayState(state: ConnectionState): void {
|
|
89
|
+
reportGatewayState(primaryProfile, state)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function setActive(profile: string): void {
|
|
93
|
+
activeKey = normKey(profile)
|
|
94
|
+
const gateway = activeGateway()
|
|
95
|
+
$gateway.set(gateway)
|
|
96
|
+
setGatewayState(gateway?.connectionState ?? 'closed')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function clearTimer(entry: Secondary): void {
|
|
100
|
+
if (entry.reconnectTimer !== null) {
|
|
101
|
+
clearTimeout(entry.reconnectTimer)
|
|
102
|
+
entry.reconnectTimer = null
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function openSecondary(entry: Secondary): Promise<void> {
|
|
107
|
+
const desktop = window.NASTECHDesktop
|
|
108
|
+
|
|
109
|
+
if (!desktop) {
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const conn = await desktop.getConnection(entry.profile)
|
|
114
|
+
const wsUrl = await resolveGatewayWsUrl(desktop, conn)
|
|
115
|
+
await entry.gateway.connect(wsUrl)
|
|
116
|
+
void desktop.touchBackend?.(entry.profile).catch(() => undefined)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function scheduleReconnect(entry: Secondary): void {
|
|
120
|
+
if (entry.reconnecting || entry.reconnectTimer !== null || !entry.wantOpen) {
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 1s, 2s, 4s … capped at 15s — same backoff shape as the primary.
|
|
125
|
+
const delay = Math.min(15_000, 1_000 * 2 ** Math.min(entry.reconnectAttempt, 4))
|
|
126
|
+
entry.reconnectAttempt += 1
|
|
127
|
+
entry.reconnectTimer = setTimeout(() => {
|
|
128
|
+
entry.reconnectTimer = null
|
|
129
|
+
void reconnectSecondary(entry)
|
|
130
|
+
}, delay)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function reconnectSecondary(entry: Secondary): Promise<void> {
|
|
134
|
+
if (entry.reconnecting || !entry.wantOpen || isOpen(entry.gateway)) {
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
entry.reconnecting = true
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
await openSecondary(entry)
|
|
142
|
+
entry.reconnectAttempt = 0
|
|
143
|
+
} catch {
|
|
144
|
+
// Transport failure → fall through to the backoff below.
|
|
145
|
+
} finally {
|
|
146
|
+
entry.reconnecting = false
|
|
147
|
+
|
|
148
|
+
if (entry.wantOpen && !isOpen(entry.gateway)) {
|
|
149
|
+
scheduleReconnect(entry)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function createSecondary(profile: string): Secondary {
|
|
155
|
+
const gateway = new NasTechGateway()
|
|
156
|
+
|
|
157
|
+
const entry: Secondary = {
|
|
158
|
+
profile,
|
|
159
|
+
gateway,
|
|
160
|
+
offEvent: () => {},
|
|
161
|
+
offState: () => {},
|
|
162
|
+
reconnectTimer: null,
|
|
163
|
+
reconnectAttempt: 0,
|
|
164
|
+
reconnecting: false,
|
|
165
|
+
wantOpen: true
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
entry.offEvent = gateway.onEvent(event => config?.onEvent(event))
|
|
169
|
+
entry.offState = gateway.onState(state => {
|
|
170
|
+
reportGatewayState(profile, state)
|
|
171
|
+
|
|
172
|
+
if (state === 'open') {
|
|
173
|
+
entry.reconnectAttempt = 0
|
|
174
|
+
clearTimer(entry)
|
|
175
|
+
} else if ((state === 'closed' || state === 'error') && entry.wantOpen) {
|
|
176
|
+
scheduleReconnect(entry)
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
secondaries.set(profile, entry)
|
|
181
|
+
|
|
182
|
+
return entry
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Make `profile` the active gateway, lazily opening its socket if needed. The
|
|
186
|
+
// primary is a no-op fast path. Background sockets are never closed here.
|
|
187
|
+
export async function ensureGatewayForProfile(profile: string): Promise<void> {
|
|
188
|
+
const key = normKey(profile)
|
|
189
|
+
|
|
190
|
+
if (key === primaryProfile) {
|
|
191
|
+
setActive(key)
|
|
192
|
+
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let entry = secondaries.get(key)
|
|
197
|
+
|
|
198
|
+
if (!entry) {
|
|
199
|
+
entry = createSecondary(key)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
entry.wantOpen = true
|
|
203
|
+
|
|
204
|
+
if (!isOpen(entry.gateway)) {
|
|
205
|
+
clearTimer(entry)
|
|
206
|
+
entry.reconnectAttempt = 0
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
await openSecondary(entry)
|
|
210
|
+
} catch {
|
|
211
|
+
scheduleReconnect(entry)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
setActive(key)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Reconnect the active gateway after a transient request failure. Primary
|
|
219
|
+
// reconnects are owned by use-gateway-boot, so we only drive secondaries here.
|
|
220
|
+
export async function ensureActiveGatewayOpen(): Promise<NasTechGateway | null> {
|
|
221
|
+
if (activeKey === primaryProfile) {
|
|
222
|
+
return primaryGateway
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const entry = secondaries.get(activeKey)
|
|
226
|
+
|
|
227
|
+
if (!entry) {
|
|
228
|
+
return null
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (!isOpen(entry.gateway)) {
|
|
232
|
+
await reconnectSecondary(entry)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return isOpen(entry.gateway) ? entry.gateway : null
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Wake signal (sleep/network/visibility): nudge every live secondary back open.
|
|
239
|
+
export function reconnectSecondaryGateways(): void {
|
|
240
|
+
for (const entry of secondaries.values()) {
|
|
241
|
+
if (!entry.wantOpen || isOpen(entry.gateway)) {
|
|
242
|
+
continue
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
entry.reconnectAttempt = 0
|
|
246
|
+
clearTimer(entry)
|
|
247
|
+
void reconnectSecondary(entry)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Keep the idle reaper from killing a backend we still need: ping every live
|
|
252
|
+
// secondary. The active one is pinged separately (touchActiveGatewayBackend).
|
|
253
|
+
export function touchSecondaryGateways(): void {
|
|
254
|
+
const desktop = window.NASTECHDesktop
|
|
255
|
+
|
|
256
|
+
for (const entry of secondaries.values()) {
|
|
257
|
+
if (entry.wantOpen) {
|
|
258
|
+
void desktop?.touchBackend?.(entry.profile).catch(() => undefined)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Close + evict secondaries whose profile is neither active nor in `keep`
|
|
264
|
+
// (profiles with a running / needs-input session). Bounds cost to live work.
|
|
265
|
+
export function pruneSecondaryGateways(keep: Set<string>): void {
|
|
266
|
+
for (const [key, entry] of [...secondaries]) {
|
|
267
|
+
if (key === activeKey || keep.has(key)) {
|
|
268
|
+
continue
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
entry.wantOpen = false
|
|
272
|
+
clearTimer(entry)
|
|
273
|
+
entry.offEvent()
|
|
274
|
+
entry.offState()
|
|
275
|
+
entry.gateway.close()
|
|
276
|
+
secondaries.delete(key)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function closeSecondaryGateways(): void {
|
|
281
|
+
for (const entry of secondaries.values()) {
|
|
282
|
+
entry.wantOpen = false
|
|
283
|
+
clearTimer(entry)
|
|
284
|
+
entry.offEvent()
|
|
285
|
+
entry.offState()
|
|
286
|
+
entry.gateway.close()
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
secondaries.clear()
|
|
290
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { atom } from 'nanostores'
|
|
2
|
+
|
|
3
|
+
import { persistBoolean, storedBoolean } from '@/lib/storage'
|
|
4
|
+
|
|
5
|
+
const HAPTICS_MUTED_STORAGE_KEY = 'NASTECH.desktop.hapticsMuted'
|
|
6
|
+
|
|
7
|
+
export const $hapticsMuted = atom(storedBoolean(HAPTICS_MUTED_STORAGE_KEY, false))
|
|
8
|
+
|
|
9
|
+
$hapticsMuted.subscribe(muted => persistBoolean(HAPTICS_MUTED_STORAGE_KEY, muted))
|
|
10
|
+
|
|
11
|
+
export function setHapticsMuted(muted: boolean) {
|
|
12
|
+
$hapticsMuted.set(muted)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function toggleHapticsMuted() {
|
|
16
|
+
$hapticsMuted.set(!$hapticsMuted.get())
|
|
17
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { atom, computed } from 'nanostores'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
defaultBindings,
|
|
5
|
+
KEYBIND_ACTION_IDS,
|
|
6
|
+
keybindAction,
|
|
7
|
+
type KeybindBindings
|
|
8
|
+
} from '@/lib/keybinds/actions'
|
|
9
|
+
import { arraysEqual, persistString, storedString } from '@/lib/storage'
|
|
10
|
+
|
|
11
|
+
const STORAGE_KEY = 'NASTECH.desktop.keybinds'
|
|
12
|
+
|
|
13
|
+
// Defaults overlaid with the user's stored overrides. Unknown / stale action ids
|
|
14
|
+
// are dropped; actions added in a later release pick up their shipped default.
|
|
15
|
+
function loadBindings(): KeybindBindings {
|
|
16
|
+
const base = defaultBindings()
|
|
17
|
+
const raw = storedString(STORAGE_KEY)
|
|
18
|
+
|
|
19
|
+
if (!raw) {
|
|
20
|
+
return base
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse(raw) as Record<string, unknown>
|
|
25
|
+
|
|
26
|
+
for (const id of KEYBIND_ACTION_IDS) {
|
|
27
|
+
const value = parsed[id]
|
|
28
|
+
|
|
29
|
+
if (Array.isArray(value)) {
|
|
30
|
+
base[id] = value.filter((combo): combo is string => typeof combo === 'string')
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
// Corrupt storage falls back to defaults.
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return base
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Persist only the actions whose combos differ from their shipped default, so
|
|
41
|
+
// changing a default never gets shadowed by a stored snapshot.
|
|
42
|
+
function persistBindings(bindings: KeybindBindings): void {
|
|
43
|
+
const defaults = defaultBindings()
|
|
44
|
+
const diff: KeybindBindings = {}
|
|
45
|
+
|
|
46
|
+
for (const id of KEYBIND_ACTION_IDS) {
|
|
47
|
+
const current = bindings[id] ?? []
|
|
48
|
+
|
|
49
|
+
if (!arraysEqual(current, defaults[id] ?? [])) {
|
|
50
|
+
diff[id] = current
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
persistString(STORAGE_KEY, JSON.stringify(diff))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const $bindings = atom<KeybindBindings>(loadBindings())
|
|
58
|
+
|
|
59
|
+
$bindings.subscribe(persistBindings)
|
|
60
|
+
|
|
61
|
+
// Reverse lookup combo → actionId for dispatch. First action wins on conflict;
|
|
62
|
+
// the panel/edit overlay surface conflicts so users can resolve them.
|
|
63
|
+
export const $comboIndex = computed($bindings, bindings => {
|
|
64
|
+
const index = new Map<string, string>()
|
|
65
|
+
|
|
66
|
+
for (const id of KEYBIND_ACTION_IDS) {
|
|
67
|
+
for (const combo of bindings[id] ?? []) {
|
|
68
|
+
if (!index.has(combo)) {
|
|
69
|
+
index.set(combo, id)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return index
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
export function setBinding(actionId: string, combos: string[]): void {
|
|
78
|
+
if (!keybindAction(actionId)) {
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
$bindings.set({ ...$bindings.get(), [actionId]: [...combos] })
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function resetBinding(actionId: string): void {
|
|
86
|
+
const action = keybindAction(actionId)
|
|
87
|
+
|
|
88
|
+
if (!action) {
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
$bindings.set({ ...$bindings.get(), [actionId]: [...action.defaults] })
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function resetAllBindings(): void {
|
|
96
|
+
$bindings.set(defaultBindings())
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Other actions that already use `combo` (excluding `actionId` itself).
|
|
100
|
+
export function conflictsFor(actionId: string, combo: string): string[] {
|
|
101
|
+
const bindings = $bindings.get()
|
|
102
|
+
|
|
103
|
+
return KEYBIND_ACTION_IDS.filter(id => id !== actionId && (bindings[id] ?? []).includes(combo))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Capture ─────────────────────────────────────────────────────────────────
|
|
107
|
+
// `$capture` is the action currently listening for its next keypress (a panel
|
|
108
|
+
// row armed for rebinding). Session-only — never persisted.
|
|
109
|
+
|
|
110
|
+
export const $capture = atom<string | null>(null)
|
|
111
|
+
|
|
112
|
+
export function beginCapture(actionId: string): void {
|
|
113
|
+
$capture.set(actionId)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function endCapture(): void {
|
|
117
|
+
$capture.set(null)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── Panel ───────────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
export const $keybindPanelOpen = atom(false)
|
|
123
|
+
|
|
124
|
+
export function openKeybindPanel(): void {
|
|
125
|
+
$keybindPanelOpen.set(true)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function closeKeybindPanel(): void {
|
|
129
|
+
$keybindPanelOpen.set(false)
|
|
130
|
+
$capture.set(null)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function toggleKeybindPanel(): void {
|
|
134
|
+
if ($keybindPanelOpen.get()) {
|
|
135
|
+
closeKeybindPanel()
|
|
136
|
+
} else {
|
|
137
|
+
openKeybindPanel()
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { atom, computed, type ReadableAtom } from 'nanostores'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
arraysEqual,
|
|
5
|
+
insertUniqueId,
|
|
6
|
+
persistBoolean,
|
|
7
|
+
persistStringArray,
|
|
8
|
+
storedBoolean,
|
|
9
|
+
storedStringArray
|
|
10
|
+
} from '@/lib/storage'
|
|
11
|
+
|
|
12
|
+
import { $paneStates, ensurePaneRegistered, setPaneOpen, setPaneWidthOverride, togglePane } from './panes'
|
|
13
|
+
|
|
14
|
+
export const SIDEBAR_DEFAULT_WIDTH = 237
|
|
15
|
+
export const SIDEBAR_MAX_WIDTH = 360
|
|
16
|
+
// Open at the same width as the sessions sidebar so the two rails match.
|
|
17
|
+
export const FILE_BROWSER_DEFAULT_WIDTH = `${SIDEBAR_DEFAULT_WIDTH}px`
|
|
18
|
+
export const FILE_BROWSER_MIN_WIDTH = '14rem'
|
|
19
|
+
export const FILE_BROWSER_MAX_WIDTH = '20rem'
|
|
20
|
+
|
|
21
|
+
export const SIDEBAR_SESSIONS_PAGE_SIZE = 50
|
|
22
|
+
|
|
23
|
+
const SIDEBAR_PINNED_STORAGE_KEY = 'NASTECH.desktop.pinnedSessions'
|
|
24
|
+
const SIDEBAR_AGENTS_GROUPED_STORAGE_KEY = 'NASTECH.desktop.agentsGroupedByWorkspace'
|
|
25
|
+
const SIDEBAR_CRON_OPEN_STORAGE_KEY = 'NASTECH.desktop.sidebarCronOpen'
|
|
26
|
+
const PANES_FLIPPED_STORAGE_KEY = 'NASTECH.desktop.panesFlipped'
|
|
27
|
+
|
|
28
|
+
export const CHAT_SIDEBAR_PANE_ID = 'chat-sidebar'
|
|
29
|
+
export const FILE_BROWSER_PANE_ID = 'file-browser'
|
|
30
|
+
export const RIGHT_RAIL_PREVIEW_TAB_ID = 'preview'
|
|
31
|
+
|
|
32
|
+
export type RightRailTabId = typeof RIGHT_RAIL_PREVIEW_TAB_ID | `file:${string}`
|
|
33
|
+
|
|
34
|
+
ensurePaneRegistered(CHAT_SIDEBAR_PANE_ID, { open: true })
|
|
35
|
+
ensurePaneRegistered(FILE_BROWSER_PANE_ID, { open: false })
|
|
36
|
+
|
|
37
|
+
export const $sidebarOpen: ReadableAtom<boolean> = computed(
|
|
38
|
+
$paneStates,
|
|
39
|
+
states => states[CHAT_SIDEBAR_PANE_ID]?.open ?? true
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
export const $fileBrowserOpen: ReadableAtom<boolean> = computed(
|
|
43
|
+
$paneStates,
|
|
44
|
+
states => states[FILE_BROWSER_PANE_ID]?.open ?? false
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
export const $rightRailActiveTabId = atom<RightRailTabId>(RIGHT_RAIL_PREVIEW_TAB_ID)
|
|
48
|
+
|
|
49
|
+
export const $sidebarWidth: ReadableAtom<number> = computed($paneStates, states => {
|
|
50
|
+
const override = states[CHAT_SIDEBAR_PANE_ID]?.widthOverride
|
|
51
|
+
|
|
52
|
+
return typeof override === 'number' ? override : SIDEBAR_DEFAULT_WIDTH
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
export const $pinnedSessionIds = atom(storedStringArray(SIDEBAR_PINNED_STORAGE_KEY))
|
|
56
|
+
export const $sidebarPinsOpen = atom(true)
|
|
57
|
+
export const $sidebarRecentsOpen = atom(true)
|
|
58
|
+
// Cron-job sessions live in their own section below recents, collapsed by
|
|
59
|
+
// default (it only renders at all when cron sessions exist) so the
|
|
60
|
+
// scheduler's `[IMPORTANT: …]` first-message previews don't spam recents.
|
|
61
|
+
export const $sidebarCronOpen = atom(storedBoolean(SIDEBAR_CRON_OPEN_STORAGE_KEY, false))
|
|
62
|
+
export const $sidebarAgentsGrouped = atom(storedBoolean(SIDEBAR_AGENTS_GROUPED_STORAGE_KEY, false))
|
|
63
|
+
// When true, the sessions sidebar moves to the right and the file browser +
|
|
64
|
+
// preview rail move to the left — a mirror of the default layout.
|
|
65
|
+
export const $panesFlipped = atom(storedBoolean(PANES_FLIPPED_STORAGE_KEY, false))
|
|
66
|
+
export const $isSidebarResizing = atom(false)
|
|
67
|
+
export const $sessionsLimit = atom(SIDEBAR_SESSIONS_PAGE_SIZE)
|
|
68
|
+
|
|
69
|
+
$pinnedSessionIds.subscribe(ids => persistStringArray(SIDEBAR_PINNED_STORAGE_KEY, [...ids]))
|
|
70
|
+
$sidebarCronOpen.subscribe(open => persistBoolean(SIDEBAR_CRON_OPEN_STORAGE_KEY, open))
|
|
71
|
+
$sidebarAgentsGrouped.subscribe(grouped => persistBoolean(SIDEBAR_AGENTS_GROUPED_STORAGE_KEY, grouped))
|
|
72
|
+
$panesFlipped.subscribe(flipped => persistBoolean(PANES_FLIPPED_STORAGE_KEY, flipped))
|
|
73
|
+
|
|
74
|
+
export function setSidebarWidth(width: number) {
|
|
75
|
+
const bounded = Math.min(SIDEBAR_MAX_WIDTH, Math.max(SIDEBAR_DEFAULT_WIDTH, width))
|
|
76
|
+
setPaneWidthOverride(CHAT_SIDEBAR_PANE_ID, bounded)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function setSidebarOpen(open: boolean) {
|
|
80
|
+
setPaneOpen(CHAT_SIDEBAR_PANE_ID, open)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function toggleSidebarOpen() {
|
|
84
|
+
togglePane(CHAT_SIDEBAR_PANE_ID)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function toggleFileBrowserOpen() {
|
|
88
|
+
togglePane(FILE_BROWSER_PANE_ID)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function setFileBrowserOpen(open: boolean) {
|
|
92
|
+
setPaneOpen(FILE_BROWSER_PANE_ID, open)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Hotkey → focus the sessions search field. Opens the sidebar first, then lets
|
|
96
|
+
// the field (which only mounts when the sidebar is open) subscribe + focus.
|
|
97
|
+
export const SESSION_SEARCH_FOCUS_EVENT = 'NASTECH:focus-session-search'
|
|
98
|
+
|
|
99
|
+
export function requestSessionSearchFocus() {
|
|
100
|
+
setSidebarOpen(true)
|
|
101
|
+
|
|
102
|
+
if (typeof window !== 'undefined') {
|
|
103
|
+
window.setTimeout(() => window.dispatchEvent(new CustomEvent(SESSION_SEARCH_FOCUS_EVENT)), 0)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function togglePanesFlipped() {
|
|
108
|
+
$panesFlipped.set(!$panesFlipped.get())
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function selectRightRailTab(id: RightRailTabId) {
|
|
112
|
+
$rightRailActiveTabId.set(id)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function setSidebarPinsOpen(open: boolean) {
|
|
116
|
+
$sidebarPinsOpen.set(open)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function setSidebarRecentsOpen(open: boolean) {
|
|
120
|
+
$sidebarRecentsOpen.set(open)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function setSidebarCronOpen(open: boolean) {
|
|
124
|
+
$sidebarCronOpen.set(open)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function setSidebarAgentsGrouped(grouped: boolean) {
|
|
128
|
+
$sidebarAgentsGrouped.set(grouped)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function setSidebarResizing(resizing: boolean) {
|
|
132
|
+
$isSidebarResizing.set(resizing)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function pinSession(sessionId: string, index?: number) {
|
|
136
|
+
const prev = $pinnedSessionIds.get()
|
|
137
|
+
const next = insertUniqueId(prev, sessionId, index ?? prev.filter(id => id !== sessionId).length)
|
|
138
|
+
|
|
139
|
+
if (!arraysEqual(prev, next)) {
|
|
140
|
+
$pinnedSessionIds.set(next)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function unpinSession(sessionId: string) {
|
|
145
|
+
const prev = $pinnedSessionIds.get()
|
|
146
|
+
const next = prev.filter(id => id !== sessionId)
|
|
147
|
+
|
|
148
|
+
if (!arraysEqual(prev, next)) {
|
|
149
|
+
$pinnedSessionIds.set(next)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function reorderPinnedSession(sessionId: string, targetIndex: number) {
|
|
154
|
+
const prev = $pinnedSessionIds.get()
|
|
155
|
+
|
|
156
|
+
if (!prev.includes(sessionId)) {
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const next = insertUniqueId(prev, sessionId, targetIndex)
|
|
161
|
+
|
|
162
|
+
if (!arraysEqual(prev, next)) {
|
|
163
|
+
$pinnedSessionIds.set(next)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function bumpSessionsLimit(step: number = SIDEBAR_SESSIONS_PAGE_SIZE) {
|
|
168
|
+
const safeStep = Math.max(1, Math.floor(step))
|
|
169
|
+
$sessionsLimit.set($sessionsLimit.get() + safeStep)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function resetSessionsLimit() {
|
|
173
|
+
if ($sessionsLimit.get() !== SIDEBAR_SESSIONS_PAGE_SIZE) {
|
|
174
|
+
$sessionsLimit.set(SIDEBAR_SESSIONS_PAGE_SIZE)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { $modelPresets, applyModelPreset, getModelPreset, modelPresetKey, setModelPreset } from './model-presets'
|
|
4
|
+
|
|
5
|
+
describe('model presets', () => {
|
|
6
|
+
beforeEach(() => $modelPresets.set({}))
|
|
7
|
+
|
|
8
|
+
it('round-trips a preset and merges patches without dropping prior fields', () => {
|
|
9
|
+
setModelPreset('anthropic', 'claude-opus-4-8', { effort: 'high' })
|
|
10
|
+
setModelPreset('anthropic', 'claude-opus-4-8', { fast: true })
|
|
11
|
+
|
|
12
|
+
expect(getModelPreset('anthropic', 'claude-opus-4-8')).toEqual({ effort: 'high', fast: true })
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('returns an empty preset for unknown models', () => {
|
|
16
|
+
expect(getModelPreset('x', 'y')).toEqual({})
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('keys by provider::model', () => {
|
|
20
|
+
expect(modelPresetKey('openai', 'gpt-5.5')).toBe('openai::gpt-5.5')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('pushes only the provided dimensions to the gateway', async () => {
|
|
24
|
+
const calls: { method: string; params?: Record<string, unknown> }[] = []
|
|
25
|
+
|
|
26
|
+
const request = async <T>(method: string, params?: Record<string, unknown>) => {
|
|
27
|
+
calls.push({ method, params })
|
|
28
|
+
|
|
29
|
+
return {} as T
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
await applyModelPreset({ effort: 'high' }, { failMessage: 'x', request, sessionId: 's1' })
|
|
33
|
+
await applyModelPreset({}, { failMessage: 'x', request, sessionId: 's1' })
|
|
34
|
+
|
|
35
|
+
expect(calls).toEqual([{ method: 'config.set', params: { key: 'reasoning', session_id: 's1', value: 'high' } }])
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('no-ops without a session so selecting a model cannot mutate global config', async () => {
|
|
39
|
+
const calls: { method: string; params?: Record<string, unknown> }[] = []
|
|
40
|
+
|
|
41
|
+
const request = async <T>(method: string, params?: Record<string, unknown>) => {
|
|
42
|
+
calls.push({ method, params })
|
|
43
|
+
|
|
44
|
+
return {} as T
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
await applyModelPreset({ effort: 'high', fast: true }, { failMessage: 'x', request, sessionId: null })
|
|
48
|
+
|
|
49
|
+
expect(calls).toEqual([])
|
|
50
|
+
})
|
|
51
|
+
})
|