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,150 @@
|
|
|
1
|
+
import type { FC } from 'react'
|
|
2
|
+
import { Fragment, useMemo } from 'react'
|
|
3
|
+
|
|
4
|
+
import { DirectiveContent } from '@/components/assistant-ui/directive-text'
|
|
5
|
+
import { cn } from '@/lib/utils'
|
|
6
|
+
|
|
7
|
+
// User messages should render the bare-minimum of markdown: backtick `code`
|
|
8
|
+
// spans and ``` fenced blocks. We deliberately don't pull in the full
|
|
9
|
+
// assistant Markdown pipeline (Streamdown + KaTeX + syntax highlighter)
|
|
10
|
+
// because user input rarely contains structured docs and the heavy pipeline
|
|
11
|
+
// adds a lot of runtime cost per bubble.
|
|
12
|
+
//
|
|
13
|
+
// Directive chips (`@file:`, `@image:`, ...) still resolve via DirectiveContent
|
|
14
|
+
// inside the plain-text segments.
|
|
15
|
+
|
|
16
|
+
interface FenceSegment {
|
|
17
|
+
kind: 'fence'
|
|
18
|
+
code: string
|
|
19
|
+
lang: string | null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface InlineSegment {
|
|
23
|
+
kind: 'inline'
|
|
24
|
+
text: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface InlineCodeSegment {
|
|
28
|
+
kind: 'inline-code'
|
|
29
|
+
code: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface InlineTextSegment {
|
|
33
|
+
kind: 'inline-text'
|
|
34
|
+
text: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type TopSegment = FenceSegment | InlineSegment
|
|
38
|
+
type InlineNode = InlineCodeSegment | InlineTextSegment
|
|
39
|
+
|
|
40
|
+
const FENCE_RE = /```([^\n`]*)\n([\s\S]*?)```/g
|
|
41
|
+
|
|
42
|
+
// Greedy backtick run length so ``code with `backticks` inside`` works.
|
|
43
|
+
const INLINE_CODE_RE = /(`+)([^`\n][\s\S]*?)\1/g
|
|
44
|
+
|
|
45
|
+
function splitFences(text: string): TopSegment[] {
|
|
46
|
+
const segments: TopSegment[] = []
|
|
47
|
+
let cursor = 0
|
|
48
|
+
|
|
49
|
+
for (const match of text.matchAll(FENCE_RE)) {
|
|
50
|
+
const start = match.index ?? 0
|
|
51
|
+
|
|
52
|
+
if (start > cursor) {
|
|
53
|
+
segments.push({ kind: 'inline', text: text.slice(cursor, start) })
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
segments.push({
|
|
57
|
+
kind: 'fence',
|
|
58
|
+
lang: (match[1] || '').trim() || null,
|
|
59
|
+
code: match[2] ?? ''
|
|
60
|
+
})
|
|
61
|
+
cursor = start + match[0].length
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (cursor < text.length) {
|
|
65
|
+
segments.push({ kind: 'inline', text: text.slice(cursor) })
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return segments
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function splitInlineCode(text: string): InlineNode[] {
|
|
72
|
+
const nodes: InlineNode[] = []
|
|
73
|
+
let cursor = 0
|
|
74
|
+
|
|
75
|
+
for (const match of text.matchAll(INLINE_CODE_RE)) {
|
|
76
|
+
const start = match.index ?? 0
|
|
77
|
+
|
|
78
|
+
if (start > cursor) {
|
|
79
|
+
nodes.push({ kind: 'inline-text', text: text.slice(cursor, start) })
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
nodes.push({ kind: 'inline-code', code: match[2] })
|
|
83
|
+
cursor = start + match[0].length
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (cursor < text.length) {
|
|
87
|
+
nodes.push({ kind: 'inline-text', text: text.slice(cursor) })
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return nodes
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface UserMessageTextProps {
|
|
94
|
+
text: string
|
|
95
|
+
className?: string
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const UserMessageText: FC<UserMessageTextProps> = ({ className, text }) => {
|
|
99
|
+
const top = useMemo(() => splitFences(text), [text])
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<span className={cn('block', className)} data-slot="aui_user-message-text">
|
|
103
|
+
{top.map((segment, segmentIndex) => {
|
|
104
|
+
if (segment.kind === 'fence') {
|
|
105
|
+
return (
|
|
106
|
+
<pre
|
|
107
|
+
className="my-1.5 max-w-full overflow-x-auto rounded-md border border-border/45 bg-[color-mix(in_srgb,currentColor_5%,transparent)] px-2.5 py-2 font-mono text-[0.86em] leading-snug"
|
|
108
|
+
data-slot="aui_user-fence"
|
|
109
|
+
key={`fence-${segmentIndex}`}
|
|
110
|
+
>
|
|
111
|
+
<code className="block whitespace-pre">{segment.code}</code>
|
|
112
|
+
</pre>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<Fragment key={`inline-${segmentIndex}`}>
|
|
118
|
+
<InlineSegmentView text={segment.text} />
|
|
119
|
+
</Fragment>
|
|
120
|
+
)
|
|
121
|
+
})}
|
|
122
|
+
</span>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const InlineSegmentView: FC<{ text: string }> = ({ text }) => {
|
|
127
|
+
const nodes = useMemo(() => splitInlineCode(text), [text])
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<span className="wrap-anywhere block whitespace-pre-line">
|
|
131
|
+
{nodes.map((node, nodeIndex) =>
|
|
132
|
+
node.kind === 'inline-code' ? (
|
|
133
|
+
<code
|
|
134
|
+
className="mx-px rounded bg-[color-mix(in_srgb,currentColor_8%,transparent)] px-1 py-px font-mono text-[0.92em]"
|
|
135
|
+
data-slot="aui_user-inline-code"
|
|
136
|
+
key={`code-${nodeIndex}`}
|
|
137
|
+
>
|
|
138
|
+
{node.code}
|
|
139
|
+
</code>
|
|
140
|
+
) : (
|
|
141
|
+
// Pass plain-text bits through DirectiveContent so @file:/@url: chips
|
|
142
|
+
// still render. DirectiveContent already preserves whitespace.
|
|
143
|
+
<Fragment key={`text-${nodeIndex}`}>
|
|
144
|
+
<DirectiveContent text={node.text} />
|
|
145
|
+
</Fragment>
|
|
146
|
+
)
|
|
147
|
+
)}
|
|
148
|
+
</span>
|
|
149
|
+
)
|
|
150
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { useStore } from '@nanostores/react'
|
|
2
|
+
import { useEffect, useState } from 'react'
|
|
3
|
+
|
|
4
|
+
import { Button } from '@/components/ui/button'
|
|
5
|
+
import { ErrorIcon } from '@/components/ui/error-state'
|
|
6
|
+
import { LogView } from '@/components/ui/log-view'
|
|
7
|
+
import type { DesktopConnectionConfig } from '@/global'
|
|
8
|
+
import { useI18n } from '@/i18n'
|
|
9
|
+
import { FileText, Loader2, LogIn, RefreshCw, Wrench } from '@/lib/icons'
|
|
10
|
+
import { $desktopBoot } from '@/store/boot'
|
|
11
|
+
import { notify, notifyError } from '@/store/notifications'
|
|
12
|
+
import { $desktopOnboarding } from '@/store/onboarding'
|
|
13
|
+
|
|
14
|
+
import type { RemoteReauth } from './boot-failure-reauth'
|
|
15
|
+
import { deriveProviderShape, isRemoteReauthFailure, signInLabel } from './boot-failure-reauth'
|
|
16
|
+
|
|
17
|
+
type BusyAction = 'local' | 'repair' | 'retry' | 'signin' | null
|
|
18
|
+
|
|
19
|
+
// A remote gateway whose access cookie has lapsed (e.g. the dashboard
|
|
20
|
+
// restarted on the remote box) boots into this overlay with a reauth-shaped
|
|
21
|
+
// error. The local-recovery buttons (Retry resets the local bootstrap latch;
|
|
22
|
+
// Repair re-runs the installer) are no-ops for that case — the only fix is to
|
|
23
|
+
// re-establish the remote session. The detection + copy helpers live in
|
|
24
|
+
// ./boot-failure-reauth so they're unit-testable without a React render.
|
|
25
|
+
|
|
26
|
+
// Recovery surface for a hard boot failure (gateway never came up, backend
|
|
27
|
+
// exited during startup, bootstrap latched, …). Without this the app shell
|
|
28
|
+
// renders dead — "gateway offline", no composer, only a toast — with no way
|
|
29
|
+
// to retry, repair the install, switch the gateway, or find the logs.
|
|
30
|
+
export function BootFailureOverlay() {
|
|
31
|
+
const boot = useStore($desktopBoot)
|
|
32
|
+
const onboarding = useStore($desktopOnboarding)
|
|
33
|
+
const { t } = useI18n()
|
|
34
|
+
const [busy, setBusy] = useState<BusyAction>(null)
|
|
35
|
+
const [logs, setLogs] = useState<string[]>([])
|
|
36
|
+
const [showLogs, setShowLogs] = useState(false)
|
|
37
|
+
const [remoteReauth, setRemoteReauth] = useState<RemoteReauth | null>(null)
|
|
38
|
+
|
|
39
|
+
const visible = Boolean(boot.error) && !boot.running
|
|
40
|
+
// While first-run onboarding owns the picker/flow we let it surface its own
|
|
41
|
+
// progress; the recovery overlay is for hard failures, which it covers via a
|
|
42
|
+
// higher z-index regardless of onboarding state.
|
|
43
|
+
const suppressed = onboarding.flow.status !== 'idle' && onboarding.flow.status !== 'error'
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!visible) {
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
void window.NASTECHDesktop
|
|
51
|
+
?.getRecentLogs()
|
|
52
|
+
.then(res => setLogs(res.lines ?? []))
|
|
53
|
+
.catch(() => undefined)
|
|
54
|
+
}, [visible])
|
|
55
|
+
|
|
56
|
+
// Resolve whether this boot failure is a remote-gateway reauth so we can
|
|
57
|
+
// offer the actionable "Sign in" path instead of the local-only recovery
|
|
58
|
+
// buttons. Runs whenever the overlay becomes visible.
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (!visible) {
|
|
61
|
+
setRemoteReauth(null)
|
|
62
|
+
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let cancelled = false
|
|
67
|
+
|
|
68
|
+
void (async () => {
|
|
69
|
+
const desktop = window.NASTECHDesktop
|
|
70
|
+
|
|
71
|
+
if (!desktop?.getConnectionConfig) {
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let config: DesktopConnectionConfig
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
config = await desktop.getConnectionConfig()
|
|
79
|
+
} catch {
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (cancelled || !isRemoteReauthFailure(config)) {
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Best-effort probe for the provider shape so the button copy matches
|
|
88
|
+
// what the user will see in the login window (password form vs OAuth
|
|
89
|
+
// redirect). Probe failure just keeps the generic copy.
|
|
90
|
+
let shape = deriveProviderShape(null)
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const probe = await desktop.probeConnectionConfig(config.remoteUrl)
|
|
94
|
+
shape = deriveProviderShape(probe?.providers)
|
|
95
|
+
} catch {
|
|
96
|
+
// Generic copy is fine.
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!cancelled) {
|
|
100
|
+
setRemoteReauth({ url: config.remoteUrl, ...shape })
|
|
101
|
+
}
|
|
102
|
+
})()
|
|
103
|
+
|
|
104
|
+
return () => {
|
|
105
|
+
cancelled = true
|
|
106
|
+
}
|
|
107
|
+
}, [visible])
|
|
108
|
+
|
|
109
|
+
if (!visible || suppressed) {
|
|
110
|
+
return null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const retry = async () => {
|
|
114
|
+
setBusy('retry')
|
|
115
|
+
await window.NASTECHDesktop?.resetBootstrap().catch(() => undefined)
|
|
116
|
+
window.location.reload()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const repair = async () => {
|
|
120
|
+
setBusy('repair')
|
|
121
|
+
await window.NASTECHDesktop?.repairBootstrap().catch(() => undefined)
|
|
122
|
+
window.location.reload()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const switchToLocalGateway = async () => {
|
|
126
|
+
setBusy('local')
|
|
127
|
+
// applyConnectionConfig reloads the window from the main process.
|
|
128
|
+
await window.NASTECHDesktop?.applyConnectionConfig({ mode: 'local' }).catch(() => undefined)
|
|
129
|
+
setBusy(null)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Open the gateway's login window (renders the username/password form for a
|
|
133
|
+
// basic gateway, or the OAuth redirect otherwise — the desktop drives both
|
|
134
|
+
// through the same window). On a successful sign-in the session cookie is
|
|
135
|
+
// re-established in the persistent partition; reload so boot re-runs and the
|
|
136
|
+
// reconnect now mints a ticket against a live session.
|
|
137
|
+
const signInRemote = async () => {
|
|
138
|
+
if (!remoteReauth) {
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
setBusy('signin')
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const result = await window.NASTECHDesktop?.oauthLoginConnectionConfig(remoteReauth.url)
|
|
146
|
+
|
|
147
|
+
if (result?.connected) {
|
|
148
|
+
notify({ kind: 'success', title: t.boot.failure.signedInTitle, message: t.boot.failure.signedInMessage })
|
|
149
|
+
window.location.reload()
|
|
150
|
+
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
notify({
|
|
155
|
+
kind: 'warning',
|
|
156
|
+
title: t.boot.failure.signInIncompleteTitle,
|
|
157
|
+
message: t.boot.failure.signInIncompleteMessage
|
|
158
|
+
})
|
|
159
|
+
} catch (err) {
|
|
160
|
+
notifyError(err, t.boot.failure.signInFailed)
|
|
161
|
+
} finally {
|
|
162
|
+
setBusy(null)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const openLogs = () => void window.NASTECHDesktop?.revealLogs().catch(() => undefined)
|
|
167
|
+
const copy = t.boot.failure
|
|
168
|
+
|
|
169
|
+
const label = signInLabel(remoteReauth, {
|
|
170
|
+
identityProvider: copy.identityProvider,
|
|
171
|
+
remoteGateway: copy.signInToRemoteGateway,
|
|
172
|
+
withProvider: copy.signInWithProvider
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<div className="fixed inset-0 z-[1400] flex items-center justify-center bg-(--ui-chat-surface-background) p-6">
|
|
177
|
+
<div className="w-full max-w-[40rem] overflow-hidden rounded-xl border border-(--stroke-nastech) bg-(--ui-chat-bubble-background) shadow-nastech">
|
|
178
|
+
<div className="flex items-start gap-3 px-5 py-4">
|
|
179
|
+
<ErrorIcon className="mt-0.5" size="1.25rem" />
|
|
180
|
+
<div>
|
|
181
|
+
<h2 className="text-[0.9375rem] font-semibold tracking-tight">
|
|
182
|
+
{remoteReauth ? copy.remoteTitle : copy.title}
|
|
183
|
+
</h2>
|
|
184
|
+
<p className="mt-1 text-[0.8125rem] leading-5 text-(--ui-text-tertiary)">
|
|
185
|
+
{remoteReauth ? copy.remoteDescription : copy.description}
|
|
186
|
+
</p>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<div className="grid gap-4 p-5">
|
|
191
|
+
<div className="rounded-2xl border border-destructive/30 bg-destructive/10 px-4 py-3 text-xs text-destructive">
|
|
192
|
+
{boot.error}
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<div className="grid gap-2">
|
|
196
|
+
<div className="flex flex-wrap gap-2">
|
|
197
|
+
{remoteReauth ? (
|
|
198
|
+
<Button disabled={Boolean(busy)} onClick={() => void signInRemote()}>
|
|
199
|
+
{busy === 'signin' ? <Loader2 className="animate-spin" /> : <LogIn />}
|
|
200
|
+
{label}
|
|
201
|
+
</Button>
|
|
202
|
+
) : (
|
|
203
|
+
<Button disabled={Boolean(busy)} onClick={() => void retry()}>
|
|
204
|
+
{busy === 'retry' ? <Loader2 className="animate-spin" /> : <RefreshCw />}
|
|
205
|
+
{copy.retry}
|
|
206
|
+
</Button>
|
|
207
|
+
)}
|
|
208
|
+
{!remoteReauth ? (
|
|
209
|
+
<Button disabled={Boolean(busy)} onClick={() => void repair()} variant="secondary">
|
|
210
|
+
{busy === 'repair' ? <Loader2 className="animate-spin" /> : <Wrench />}
|
|
211
|
+
{copy.repairInstall}
|
|
212
|
+
</Button>
|
|
213
|
+
) : null}
|
|
214
|
+
<Button disabled={Boolean(busy)} onClick={() => void switchToLocalGateway()} variant="secondary">
|
|
215
|
+
{busy === 'local' ? <Loader2 className="animate-spin" /> : null}
|
|
216
|
+
{copy.useLocalGateway}
|
|
217
|
+
</Button>
|
|
218
|
+
<Button onClick={openLogs} variant="ghost">
|
|
219
|
+
<FileText />
|
|
220
|
+
{copy.openLogs}
|
|
221
|
+
</Button>
|
|
222
|
+
</div>
|
|
223
|
+
<p className="text-xs text-muted-foreground">
|
|
224
|
+
{remoteReauth ? copy.remoteSignInHint : copy.repairHint}
|
|
225
|
+
</p>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
{logs.length > 0 ? (
|
|
229
|
+
<div className="grid gap-2">
|
|
230
|
+
<Button
|
|
231
|
+
className="-ml-2 self-start font-medium"
|
|
232
|
+
onClick={() => setShowLogs(v => !v)}
|
|
233
|
+
size="xs"
|
|
234
|
+
type="button"
|
|
235
|
+
variant="text"
|
|
236
|
+
>
|
|
237
|
+
{showLogs ? copy.hideRecentLogs : copy.showRecentLogs}
|
|
238
|
+
</Button>
|
|
239
|
+
{showLogs ? <LogView className="max-h-48">{logs.slice(-40).join('')}</LogView> : null}
|
|
240
|
+
</div>
|
|
241
|
+
) : null}
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
)
|
|
246
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import type { DesktopConnectionConfig } from '@/global'
|
|
4
|
+
|
|
5
|
+
import { deriveProviderShape, isRemoteReauthFailure, signInLabel } from './boot-failure-reauth'
|
|
6
|
+
|
|
7
|
+
function config(overrides: Partial<DesktopConnectionConfig> = {}): DesktopConnectionConfig {
|
|
8
|
+
return {
|
|
9
|
+
envOverride: false,
|
|
10
|
+
mode: 'remote',
|
|
11
|
+
profile: null,
|
|
12
|
+
remoteAuthMode: 'oauth',
|
|
13
|
+
remoteOauthConnected: false,
|
|
14
|
+
remoteTokenPreview: null,
|
|
15
|
+
remoteTokenSet: false,
|
|
16
|
+
remoteUrl: 'https://box:9119',
|
|
17
|
+
...overrides
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('isRemoteReauthFailure', () => {
|
|
22
|
+
it('true for a remote, gated, disconnected gateway with a URL', () => {
|
|
23
|
+
expect(isRemoteReauthFailure(config())).toBe(true)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('false when the oauth session is still connected', () => {
|
|
27
|
+
expect(isRemoteReauthFailure(config({ remoteOauthConnected: true }))).toBe(false)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('false for a local gateway', () => {
|
|
31
|
+
expect(isRemoteReauthFailure(config({ mode: 'local' }))).toBe(false)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('false for a token (non-gated) remote gateway', () => {
|
|
35
|
+
expect(isRemoteReauthFailure(config({ remoteAuthMode: 'token' }))).toBe(false)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('false when there is no remote URL to sign in against', () => {
|
|
39
|
+
expect(isRemoteReauthFailure(config({ remoteUrl: '' }))).toBe(false)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('false for null/undefined config', () => {
|
|
43
|
+
expect(isRemoteReauthFailure(null)).toBe(false)
|
|
44
|
+
expect(isRemoteReauthFailure(undefined)).toBe(false)
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('deriveProviderShape', () => {
|
|
49
|
+
it('generic copy when there are no providers', () => {
|
|
50
|
+
expect(deriveProviderShape([])).toEqual({ isPassword: false, providerLabel: 'your identity provider' })
|
|
51
|
+
expect(deriveProviderShape(null)).toEqual({ isPassword: false, providerLabel: 'your identity provider' })
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('password shape when the sole provider supports password', () => {
|
|
55
|
+
expect(
|
|
56
|
+
deriveProviderShape([{ name: 'basic', displayName: 'Username & Password', supportsPassword: true }])
|
|
57
|
+
).toEqual({ isPassword: true, providerLabel: 'Username & Password' })
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('OAuth shape when the provider is a redirect IDP', () => {
|
|
61
|
+
expect(deriveProviderShape([{ name: 'nastech', displayName: 'NasTech', supportsPassword: false }])).toEqual({
|
|
62
|
+
isPassword: false,
|
|
63
|
+
providerLabel: 'NasTech'
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('mixed deployment keeps generic OAuth copy (not every provider is password)', () => {
|
|
68
|
+
const shape = deriveProviderShape([
|
|
69
|
+
{ name: 'basic', displayName: 'Username & Password', supportsPassword: true },
|
|
70
|
+
{ name: 'nastech', displayName: 'NasTech', supportsPassword: false }
|
|
71
|
+
])
|
|
72
|
+
|
|
73
|
+
expect(shape.isPassword).toBe(false)
|
|
74
|
+
expect(shape.providerLabel).toBe('Username & Password / NasTech')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('falls back to name when displayName is empty', () => {
|
|
78
|
+
expect(deriveProviderShape([{ name: 'basic', displayName: '', supportsPassword: true }]).providerLabel).toBe(
|
|
79
|
+
'basic'
|
|
80
|
+
)
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
describe('signInLabel', () => {
|
|
85
|
+
it('password gateway gets the plain "Sign in to remote gateway" copy', () => {
|
|
86
|
+
expect(signInLabel({ url: 'x', isPassword: true, providerLabel: 'Username & Password' })).toBe(
|
|
87
|
+
'Sign in to remote gateway'
|
|
88
|
+
)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('OAuth gateway names the provider', () => {
|
|
92
|
+
expect(signInLabel({ url: 'x', isPassword: false, providerLabel: 'NasTech' })).toBe(
|
|
93
|
+
'Sign in with NasTech'
|
|
94
|
+
)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('null reauth falls back to the generic provider phrase', () => {
|
|
98
|
+
expect(signInLabel(null)).toBe('Sign in with your identity provider')
|
|
99
|
+
})
|
|
100
|
+
})
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { DesktopAuthProvider, DesktopConnectionConfig } from '@/global'
|
|
2
|
+
|
|
3
|
+
// Pure helpers for the boot-failure overlay's remote-reauth branch. Kept out
|
|
4
|
+
// of the .tsx so they can be unit-tested without a React/jsdom render (the
|
|
5
|
+
// jsx-dev-runtime resolution in this repo's vitest setup is flaky for
|
|
6
|
+
// component renders, but these are plain functions).
|
|
7
|
+
|
|
8
|
+
export interface RemoteReauth {
|
|
9
|
+
url: string
|
|
10
|
+
// True when every advertised provider is username/password — drives the
|
|
11
|
+
// button copy ("Sign in to remote gateway" vs "Sign in with <provider>"),
|
|
12
|
+
// mirroring the gateway-settings page. Probe is best-effort.
|
|
13
|
+
isPassword: boolean
|
|
14
|
+
providerLabel: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface SignInCopy {
|
|
18
|
+
identityProvider: string
|
|
19
|
+
remoteGateway: string
|
|
20
|
+
withProvider: (provider: string) => string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const DEFAULT_SIGN_IN_COPY: SignInCopy = {
|
|
24
|
+
identityProvider: 'your identity provider',
|
|
25
|
+
remoteGateway: 'Sign in to remote gateway',
|
|
26
|
+
withProvider: provider => `Sign in with ${provider}`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// A remote, gated (oauth-bucket), not-currently-connected gateway is a
|
|
30
|
+
// remote-reauth boot failure: the access cookie lapsed (e.g. the remote
|
|
31
|
+
// dashboard restarted) and the local-recovery buttons (Retry/Repair) can't
|
|
32
|
+
// fix it — only re-establishing the remote session can. A connected oauth
|
|
33
|
+
// session, or a token/local gateway, boots for some other reason the
|
|
34
|
+
// local-recovery buttons address, so those return false here.
|
|
35
|
+
export function isRemoteReauthFailure(config: DesktopConnectionConfig | null | undefined): boolean {
|
|
36
|
+
if (!config) {
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
config.mode === 'remote' &&
|
|
42
|
+
config.remoteAuthMode === 'oauth' &&
|
|
43
|
+
!config.remoteOauthConnected &&
|
|
44
|
+
Boolean(config.remoteUrl)
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Derive the password flag + display label from the probed providers. A
|
|
49
|
+
// gateway is treated as password-style only when EVERY advertised provider
|
|
50
|
+
// supports password (a mixed deployment keeps the generic OAuth copy), so the
|
|
51
|
+
// button copy matches the login window the user is about to see.
|
|
52
|
+
export function deriveProviderShape(providers: DesktopAuthProvider[] | null | undefined): {
|
|
53
|
+
isPassword: boolean
|
|
54
|
+
providerLabel: string
|
|
55
|
+
} {
|
|
56
|
+
const list = providers ?? []
|
|
57
|
+
|
|
58
|
+
if (list.length === 0) {
|
|
59
|
+
return { isPassword: false, providerLabel: 'your identity provider' }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const isPassword = list.every(p => Boolean(p.supportsPassword))
|
|
63
|
+
|
|
64
|
+
const providerLabel =
|
|
65
|
+
list.length === 1
|
|
66
|
+
? list[0].displayName || list[0].name
|
|
67
|
+
: list.map(p => p.displayName || p.name).join(' / ')
|
|
68
|
+
|
|
69
|
+
return { isPassword, providerLabel }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Button copy for the remote sign-in action.
|
|
73
|
+
export function signInLabel(reauth: RemoteReauth | null, copy: SignInCopy = DEFAULT_SIGN_IN_COPY): string {
|
|
74
|
+
if (reauth?.isPassword) {
|
|
75
|
+
return copy.remoteGateway
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const provider = reauth?.providerLabel === DEFAULT_SIGN_IN_COPY.identityProvider ? copy.identityProvider : reauth?.providerLabel
|
|
79
|
+
|
|
80
|
+
return copy.withProvider(provider ?? copy.identityProvider)
|
|
81
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils'
|
|
2
|
+
|
|
3
|
+
const assetPath = (path: string) => `${import.meta.env.BASE_URL}${path.replace(/^\/+/, '')}`
|
|
4
|
+
|
|
5
|
+
// Brand badge: NasTech logo mark on a white tile, identical in light/dark.
|
|
6
|
+
// Fills the tile (softly rounded); size via className (default size-14).
|
|
7
|
+
export function BrandMark({ className, ...props }: React.ComponentProps<'span'>) {
|
|
8
|
+
return (
|
|
9
|
+
<span
|
|
10
|
+
className={cn(
|
|
11
|
+
'inline-flex size-14 shrink-0 items-center justify-center overflow-hidden rounded-md bg-white',
|
|
12
|
+
className
|
|
13
|
+
)}
|
|
14
|
+
{...props}
|
|
15
|
+
>
|
|
16
|
+
<img alt="" className="size-full object-contain" src={assetPath('nastech-logo.png')} />
|
|
17
|
+
</span>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils'
|
|
2
|
+
|
|
3
|
+
import { formatElapsed } from './activity-timer'
|
|
4
|
+
|
|
5
|
+
interface ActivityTimerTextProps {
|
|
6
|
+
seconds: number
|
|
7
|
+
className?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function ActivityTimerText({ seconds, className }: ActivityTimerTextProps) {
|
|
11
|
+
return (
|
|
12
|
+
<span
|
|
13
|
+
className={cn(
|
|
14
|
+
// Tinted with --dt-midground (very low alpha) so the timer reads
|
|
15
|
+
// as part of the same "live signal" cluster as the dither block /
|
|
16
|
+
// arc-border / working-session dot, instead of being neutral chrome.
|
|
17
|
+
'shrink-0 font-mono text-[0.56rem] leading-none tracking-[0.02em] text-midground/55 tabular-nums',
|
|
18
|
+
className
|
|
19
|
+
)}
|
|
20
|
+
>
|
|
21
|
+
{formatElapsed(seconds)}
|
|
22
|
+
</span>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { act, render, screen } from '@testing-library/react'
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { __resetElapsedTimerRegistryForTests, useElapsedSeconds } from './activity-timer'
|
|
5
|
+
|
|
6
|
+
function Probe({ active, timerKey }: { active: boolean; timerKey?: string }) {
|
|
7
|
+
const elapsed = useElapsedSeconds(active, timerKey)
|
|
8
|
+
|
|
9
|
+
return <span data-testid="elapsed">{elapsed}</span>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe('useElapsedSeconds', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
vi.useFakeTimers()
|
|
15
|
+
vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z'))
|
|
16
|
+
__resetElapsedTimerRegistryForTests()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
vi.useRealTimers()
|
|
21
|
+
__resetElapsedTimerRegistryForTests()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('keeps elapsed time stable across remounts for the same key', () => {
|
|
25
|
+
const first = render(<Probe active timerKey="tool:abc" />)
|
|
26
|
+
|
|
27
|
+
act(() => {
|
|
28
|
+
vi.advanceTimersByTime(5_000)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
expect(screen.getByTestId('elapsed').textContent).toBe('5')
|
|
32
|
+
|
|
33
|
+
first.unmount()
|
|
34
|
+
|
|
35
|
+
act(() => {
|
|
36
|
+
vi.advanceTimersByTime(3_000)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
render(<Probe active timerKey="tool:abc" />)
|
|
40
|
+
|
|
41
|
+
expect(screen.getByTestId('elapsed').textContent).toBe('8')
|
|
42
|
+
})
|
|
43
|
+
})
|