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,109 @@
|
|
|
1
|
+
import { type FC } from 'react'
|
|
2
|
+
|
|
3
|
+
import { Checkbox } from '@/components/ui/checkbox'
|
|
4
|
+
import { Loader2Icon } from '@/lib/icons'
|
|
5
|
+
import { parseTodos, type TodoItem, type TodoStatus } from '@/lib/todos'
|
|
6
|
+
import { cn } from '@/lib/utils'
|
|
7
|
+
|
|
8
|
+
export function todosFromMessageContent(content: unknown): TodoItem[] {
|
|
9
|
+
if (!Array.isArray(content)) {
|
|
10
|
+
return []
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let latest: null | TodoItem[] = null
|
|
14
|
+
|
|
15
|
+
for (const part of content) {
|
|
16
|
+
if (!part || typeof part !== 'object') {
|
|
17
|
+
continue
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const row = part as Record<string, unknown>
|
|
21
|
+
|
|
22
|
+
if (row.type !== 'tool-call' || row.toolName !== 'todo') {
|
|
23
|
+
continue
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const parsed = parseTodos(row.result) ?? parseTodos(row.args)
|
|
27
|
+
|
|
28
|
+
if (parsed !== null) {
|
|
29
|
+
latest = parsed
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return latest ?? []
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const headerLabel = (todos: readonly TodoItem[]): string =>
|
|
37
|
+
todos.find(t => t.status === 'in_progress')?.content ??
|
|
38
|
+
todos.find(t => t.status === 'pending')?.content ??
|
|
39
|
+
todos.at(-1)?.content ??
|
|
40
|
+
'Tasks'
|
|
41
|
+
|
|
42
|
+
const Checkmark: FC<{ status: TodoStatus; label: string }> = ({ status, label }) => {
|
|
43
|
+
if (status === 'in_progress') {
|
|
44
|
+
return (
|
|
45
|
+
<span
|
|
46
|
+
aria-label={`In progress: ${label}`}
|
|
47
|
+
className="grid size-[1.1rem] shrink-0 place-items-center rounded-full border border-ring/65 bg-[color-mix(in_srgb,var(--dt-ring)_14%,transparent)]"
|
|
48
|
+
>
|
|
49
|
+
<Loader2Icon className="size-3 animate-spin text-ring" />
|
|
50
|
+
</span>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const checked = status === 'completed'
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<Checkbox
|
|
58
|
+
aria-label={label}
|
|
59
|
+
checked={checked}
|
|
60
|
+
className={cn(
|
|
61
|
+
'size-[1.1rem] shrink-0 rounded-full border-border/80 pointer-events-none disabled:cursor-default disabled:opacity-100',
|
|
62
|
+
checked &&
|
|
63
|
+
'data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground [&_[data-slot=checkbox-indicator]_svg]:size-3',
|
|
64
|
+
status === 'cancelled' && 'border-muted-foreground/40'
|
|
65
|
+
)}
|
|
66
|
+
disabled
|
|
67
|
+
/>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const HoistedTodoPanel: FC<{ todos: TodoItem[] }> = ({ todos }) => {
|
|
72
|
+
if (!todos.length) {
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const label = headerLabel(todos)
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<section
|
|
80
|
+
className="mt-1 mb-3 inline-block w-fit max-w-full overflow-hidden rounded-2xl border border-border/70 bg-card align-top shadow-[0_1px_2px_0_hsl(var(--foreground)/0.04),0_1px_4px_-1px_hsl(var(--foreground)/0.06)]"
|
|
81
|
+
data-slot="aui_todo-hoisted"
|
|
82
|
+
>
|
|
83
|
+
<header className="px-3 pt-3 pb-2">
|
|
84
|
+
<span
|
|
85
|
+
className="block max-w-full truncate text-[0.85rem] font-semibold leading-tight tracking-tight text-foreground"
|
|
86
|
+
title={label}
|
|
87
|
+
>
|
|
88
|
+
{label}
|
|
89
|
+
</span>
|
|
90
|
+
</header>
|
|
91
|
+
<ul className="grid min-w-0 gap-0.5 px-3 pb-3">
|
|
92
|
+
{todos.map(todo => (
|
|
93
|
+
<li
|
|
94
|
+
// Active row at full presence; everything else fades. Opacity on
|
|
95
|
+
// the row so the checkbox glyph dims with the text.
|
|
96
|
+
className={cn(
|
|
97
|
+
'flex min-w-0 items-center gap-3 py-1.5 transition-opacity',
|
|
98
|
+
todo.status === 'in_progress' ? 'opacity-100' : 'opacity-45'
|
|
99
|
+
)}
|
|
100
|
+
key={todo.id}
|
|
101
|
+
>
|
|
102
|
+
<Checkmark label={todo.content} status={todo.status} />
|
|
103
|
+
<span className="min-w-0 wrap-anywhere text-[0.8rem] leading-[1.2rem] text-foreground">{todo.content}</span>
|
|
104
|
+
</li>
|
|
105
|
+
))}
|
|
106
|
+
</ul>
|
|
107
|
+
</section>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { AssistantRuntimeProvider, type ThreadMessage, useExternalStoreRuntime } from '@assistant-ui/react'
|
|
2
|
+
import { cleanup, render, waitFor } from '@testing-library/react'
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { clearAllPrompts, setApprovalRequest } from '@/store/prompts'
|
|
6
|
+
import { $activeSessionId } from '@/store/session'
|
|
7
|
+
import { $toolDisclosureStates } from '@/store/tool-view'
|
|
8
|
+
|
|
9
|
+
import { Thread } from './thread'
|
|
10
|
+
|
|
11
|
+
// Regression coverage for the "approval must never be buried" bug. Tools now
|
|
12
|
+
// render as a flat list (no collapsible "N steps" group), so a pending tool's
|
|
13
|
+
// inline ApprovalBar is always in the visual flow — never inside a `hidden`
|
|
14
|
+
// body. These assert the bar shows only when an approval is live and is never
|
|
15
|
+
// trapped under a `hidden` ancestor.
|
|
16
|
+
|
|
17
|
+
const createdAt = new Date('2026-06-03T00:00:00.000Z')
|
|
18
|
+
|
|
19
|
+
const resizeObservers = new Set<TestResizeObserver>()
|
|
20
|
+
|
|
21
|
+
class TestResizeObserver {
|
|
22
|
+
private target: Element | null = null
|
|
23
|
+
|
|
24
|
+
constructor(private readonly callback: ResizeObserverCallback) {
|
|
25
|
+
resizeObservers.add(this)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
observe(target: Element) {
|
|
29
|
+
this.target = target
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
unobserve() {}
|
|
33
|
+
|
|
34
|
+
disconnect() {
|
|
35
|
+
resizeObservers.delete(this)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
vi.stubGlobal('ResizeObserver', TestResizeObserver)
|
|
40
|
+
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) =>
|
|
41
|
+
window.setTimeout(() => callback(performance.now()), 0)
|
|
42
|
+
)
|
|
43
|
+
vi.stubGlobal('cancelAnimationFrame', (id: number) => window.clearTimeout(id))
|
|
44
|
+
|
|
45
|
+
Element.prototype.scrollTo = function scrollTo() {}
|
|
46
|
+
|
|
47
|
+
Element.prototype.animate = function animate() {
|
|
48
|
+
return {
|
|
49
|
+
cancel: () => {},
|
|
50
|
+
finished: Promise.resolve()
|
|
51
|
+
} as unknown as Animation
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function stubOffsetDimension(
|
|
55
|
+
prop: 'offsetHeight' | 'offsetWidth',
|
|
56
|
+
clientProp: 'clientHeight' | 'clientWidth',
|
|
57
|
+
fallback: number
|
|
58
|
+
) {
|
|
59
|
+
const previous = Object.getOwnPropertyDescriptor(HTMLElement.prototype, prop)
|
|
60
|
+
|
|
61
|
+
Object.defineProperty(HTMLElement.prototype, prop, {
|
|
62
|
+
configurable: true,
|
|
63
|
+
get() {
|
|
64
|
+
return previous?.get?.call(this) || (this as HTMLElement)[clientProp] || fallback
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
stubOffsetDimension('offsetWidth', 'clientWidth', 800)
|
|
70
|
+
stubOffsetDimension('offsetHeight', 'clientHeight', 600)
|
|
71
|
+
|
|
72
|
+
// A running assistant message with two tools: a completed read_file plus a
|
|
73
|
+
// pending terminal (no result), rendered as a flat two-row list.
|
|
74
|
+
function groupedPendingMessage(): ThreadMessage {
|
|
75
|
+
return {
|
|
76
|
+
id: 'assistant-group-1',
|
|
77
|
+
role: 'assistant',
|
|
78
|
+
content: [
|
|
79
|
+
{
|
|
80
|
+
type: 'tool-call',
|
|
81
|
+
toolCallId: 'read-1',
|
|
82
|
+
toolName: 'read_file',
|
|
83
|
+
args: { path: '/etc/hosts' },
|
|
84
|
+
argsText: JSON.stringify({ path: '/etc/hosts' }),
|
|
85
|
+
result: { content: '127.0.0.1 localhost' }
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
type: 'tool-call',
|
|
89
|
+
toolCallId: 'term-1',
|
|
90
|
+
toolName: 'terminal',
|
|
91
|
+
args: { command: 'rm -rf /tmp/x' },
|
|
92
|
+
argsText: JSON.stringify({ command: 'rm -rf /tmp/x' })
|
|
93
|
+
}
|
|
94
|
+
],
|
|
95
|
+
status: { type: 'running' },
|
|
96
|
+
createdAt,
|
|
97
|
+
metadata: {
|
|
98
|
+
unstable_state: null,
|
|
99
|
+
unstable_annotations: [],
|
|
100
|
+
unstable_data: [],
|
|
101
|
+
steps: [],
|
|
102
|
+
custom: {}
|
|
103
|
+
}
|
|
104
|
+
} as ThreadMessage
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function GroupHarness({ message }: { message: ThreadMessage }) {
|
|
108
|
+
const runtime = useExternalStoreRuntime<ThreadMessage>({
|
|
109
|
+
messages: [message],
|
|
110
|
+
isRunning: message.status?.type === 'running',
|
|
111
|
+
onNew: async () => {}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
116
|
+
<Thread />
|
|
117
|
+
</AssistantRuntimeProvider>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
beforeEach(() => {
|
|
122
|
+
clearAllPrompts()
|
|
123
|
+
$activeSessionId.set('sess-1')
|
|
124
|
+
$toolDisclosureStates.set({})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
afterEach(() => {
|
|
128
|
+
cleanup()
|
|
129
|
+
clearAllPrompts()
|
|
130
|
+
$activeSessionId.set(null)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
describe('flat tool list approval surfacing', () => {
|
|
134
|
+
it('renders no inline approval bar when there is no live approval', async () => {
|
|
135
|
+
const { container } = render(<GroupHarness message={groupedPendingMessage()} />)
|
|
136
|
+
|
|
137
|
+
// The pending terminal row mounts immediately, but its inline ApprovalBar
|
|
138
|
+
// returns null while $approvalRequest is empty.
|
|
139
|
+
await waitFor(() => {
|
|
140
|
+
expect(container.querySelectorAll('[data-slot="tool-block"]').length).toBeGreaterThan(0)
|
|
141
|
+
})
|
|
142
|
+
expect(container.querySelector('[data-slot="tool-approval-inline"]')).toBeNull()
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('surfaces the approval inline and never under a hidden ancestor', async () => {
|
|
146
|
+
setApprovalRequest({ command: 'rm -rf /tmp/x', description: 'dangerous command', sessionId: 'sess-1' })
|
|
147
|
+
|
|
148
|
+
const { container } = render(<GroupHarness message={groupedPendingMessage()} />)
|
|
149
|
+
|
|
150
|
+
await waitFor(() => {
|
|
151
|
+
const bar = container.querySelector('[data-slot="tool-approval-inline"]')
|
|
152
|
+
expect(bar).not.toBeNull()
|
|
153
|
+
// Flat rows live directly in the flow — nothing should ever wrap the bar
|
|
154
|
+
// in a `hidden` subtree.
|
|
155
|
+
expect(bar?.closest('[hidden]')).toBeNull()
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
})
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import type { NasTechGateway } from '@/nastech'
|
|
5
|
+
import { $gateway } from '@/store/gateway'
|
|
6
|
+
import { $approvalRequest, clearAllPrompts, setApprovalRequest } from '@/store/prompts'
|
|
7
|
+
import { $activeSessionId } from '@/store/session'
|
|
8
|
+
|
|
9
|
+
import { PendingToolApproval } from './tool-approval'
|
|
10
|
+
import type { ToolPart } from './tool-fallback-model'
|
|
11
|
+
|
|
12
|
+
function part(toolName: string): ToolPart {
|
|
13
|
+
return { toolName, type: `tool-${toolName}` } as unknown as ToolPart
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function setRequest(command = 'rm -rf /tmp/x') {
|
|
17
|
+
$activeSessionId.set('sess-1')
|
|
18
|
+
setApprovalRequest({ command, description: 'dangerous command', sessionId: 'sess-1' })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function mockGateway() {
|
|
22
|
+
const request = vi.fn().mockResolvedValue({ resolved: true })
|
|
23
|
+
$gateway.set({ request } as unknown as NasTechGateway)
|
|
24
|
+
|
|
25
|
+
return request
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
cleanup()
|
|
30
|
+
clearAllPrompts()
|
|
31
|
+
$activeSessionId.set(null)
|
|
32
|
+
$gateway.set(null)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
describe('PendingToolApproval', () => {
|
|
36
|
+
it('renders nothing when there is no pending approval', () => {
|
|
37
|
+
const { container } = render(<PendingToolApproval part={part('terminal')} />)
|
|
38
|
+
|
|
39
|
+
expect(container.innerHTML).toBe('')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('renders nothing for tools that never raise approval', () => {
|
|
43
|
+
setRequest()
|
|
44
|
+
const { container } = render(<PendingToolApproval part={part('read_file')} />)
|
|
45
|
+
|
|
46
|
+
expect(container.innerHTML).toBe('')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('renders the inline run/reject controls on the pending terminal row', () => {
|
|
50
|
+
setRequest('chmod -R 777 /tmp/x')
|
|
51
|
+
render(<PendingToolApproval part={part('terminal')} />)
|
|
52
|
+
|
|
53
|
+
expect(screen.getByRole('button', { name: /Run/ })).toBeTruthy()
|
|
54
|
+
expect(screen.getByRole('button', { name: /Reject/ })).toBeTruthy()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('sends approval.respond {choice: "once"} and clears the request on Run', async () => {
|
|
58
|
+
const request = mockGateway()
|
|
59
|
+
setRequest()
|
|
60
|
+
render(<PendingToolApproval part={part('terminal')} />)
|
|
61
|
+
|
|
62
|
+
fireEvent.click(screen.getByRole('button', { name: /Run/ }))
|
|
63
|
+
|
|
64
|
+
await waitFor(() => {
|
|
65
|
+
expect(request).toHaveBeenCalledWith('approval.respond', { choice: 'once', session_id: 'sess-1' })
|
|
66
|
+
})
|
|
67
|
+
expect($approvalRequest.get()).toBeNull()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('sends choice "deny" on Reject', async () => {
|
|
71
|
+
const request = mockGateway()
|
|
72
|
+
setRequest()
|
|
73
|
+
render(<PendingToolApproval part={part('terminal')} />)
|
|
74
|
+
|
|
75
|
+
fireEvent.click(screen.getByRole('button', { name: /Reject/ }))
|
|
76
|
+
|
|
77
|
+
await waitFor(() => {
|
|
78
|
+
expect(request).toHaveBeenCalledWith('approval.respond', { choice: 'deny', session_id: 'sess-1' })
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
})
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useStore } from '@nanostores/react'
|
|
4
|
+
import { type FC, useCallback, useEffect, useState } from 'react'
|
|
5
|
+
|
|
6
|
+
import { Button } from '@/components/ui/button'
|
|
7
|
+
import {
|
|
8
|
+
Dialog,
|
|
9
|
+
DialogContent,
|
|
10
|
+
DialogDescription,
|
|
11
|
+
DialogFooter,
|
|
12
|
+
DialogHeader,
|
|
13
|
+
DialogTitle
|
|
14
|
+
} from '@/components/ui/dialog'
|
|
15
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
|
|
16
|
+
import { useI18n } from '@/i18n'
|
|
17
|
+
import { triggerHaptic } from '@/lib/haptics'
|
|
18
|
+
import { ChevronDown, Loader2 } from '@/lib/icons'
|
|
19
|
+
import { $gateway } from '@/store/gateway'
|
|
20
|
+
import { notifyError } from '@/store/notifications'
|
|
21
|
+
import { $approvalRequest, type ApprovalRequest, clearApprovalRequest } from '@/store/prompts'
|
|
22
|
+
|
|
23
|
+
import type { ToolPart } from './tool-fallback-model'
|
|
24
|
+
|
|
25
|
+
// Inline approval control. Rendered as a compact button strip
|
|
26
|
+
// under the pending tool row that raised the approval (the row already shows
|
|
27
|
+
// the command, so the strip deliberately doesn't repeat it) instead of as a
|
|
28
|
+
// modal overlay.
|
|
29
|
+
//
|
|
30
|
+
// Binding is POSITIONAL, not command-matched: the desktop `tool.start` payload
|
|
31
|
+
// carries no structured args (only tool_id/name/context — see
|
|
32
|
+
// tui_gateway/server.py::_on_tool_start), so we cannot join the approval to the
|
|
33
|
+
// row by command string. But `approval.request` only ever fires from the
|
|
34
|
+
// `terminal` / `execute_code` guards and the agent thread blocks on exactly one
|
|
35
|
+
// approval at a time, so the single pending row of those tools IS the row that
|
|
36
|
+
// raised it. The command/description text comes from `$approvalRequest` (the
|
|
37
|
+
// event payload), which is the only place that data reliably exists.
|
|
38
|
+
export const APPROVAL_TOOLS = new Set(['terminal', 'execute_code'])
|
|
39
|
+
|
|
40
|
+
// Canonical gateway choices (ui-tui/src/components/prompts.tsx).
|
|
41
|
+
type ApprovalChoice = 'once' | 'session' | 'always' | 'deny'
|
|
42
|
+
|
|
43
|
+
export const PendingToolApproval: FC<{ part: ToolPart }> = ({ part }) => {
|
|
44
|
+
const request = useStore($approvalRequest)
|
|
45
|
+
|
|
46
|
+
if (!request || !APPROVAL_TOOLS.has(part.toolName)) {
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return <ApprovalBar request={request} />
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const isMac = typeof navigator !== 'undefined' && /Mac|iP(hone|ad|od)/.test(navigator.platform)
|
|
54
|
+
|
|
55
|
+
const ApprovalBar: FC<{ request: ApprovalRequest }> = ({ request }) => {
|
|
56
|
+
const { t } = useI18n()
|
|
57
|
+
const copy = t.assistant.approval
|
|
58
|
+
const gateway = useStore($gateway)
|
|
59
|
+
const [submitting, setSubmitting] = useState<ApprovalChoice | null>(null)
|
|
60
|
+
// "Always allow" persists the pattern to ~/.NASTECH/config.yaml permanently, so
|
|
61
|
+
// it goes through a confirm step rather than firing straight from the menu.
|
|
62
|
+
const [confirmAlways, setConfirmAlways] = useState(false)
|
|
63
|
+
const busy = submitting !== null
|
|
64
|
+
|
|
65
|
+
const respond = useCallback(
|
|
66
|
+
async (choice: ApprovalChoice) => {
|
|
67
|
+
// Another bar (or the keyboard path) may have already resolved this
|
|
68
|
+
// approval; the atom is the single source of truth, so bail if it's gone.
|
|
69
|
+
if (busy || !$approvalRequest.get()) {
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!gateway) {
|
|
74
|
+
notifyError(new Error(copy.gatewayDisconnected), copy.sendFailed)
|
|
75
|
+
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
setSubmitting(choice)
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
await gateway.request<{ resolved?: boolean }>('approval.respond', {
|
|
83
|
+
choice,
|
|
84
|
+
session_id: request.sessionId ?? undefined
|
|
85
|
+
})
|
|
86
|
+
triggerHaptic(choice === 'deny' ? 'cancel' : 'submit')
|
|
87
|
+
clearApprovalRequest(request.sessionId)
|
|
88
|
+
} catch (error) {
|
|
89
|
+
notifyError(error, copy.sendFailed)
|
|
90
|
+
setSubmitting(null)
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
[busy, gateway, request.sessionId]
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
// ⌘/Ctrl+Enter → Run, Esc → Reject.
|
|
97
|
+
// While the confirm dialog is open it owns the keyboard (Esc closes it), so
|
|
98
|
+
// the strip-level shortcuts stand down to avoid denying the whole approval.
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (confirmAlways) {
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
105
|
+
if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {
|
|
106
|
+
event.preventDefault()
|
|
107
|
+
void respond('once')
|
|
108
|
+
} else if (event.key === 'Escape') {
|
|
109
|
+
event.preventDefault()
|
|
110
|
+
void respond('deny')
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
window.addEventListener('keydown', onKeyDown, true)
|
|
115
|
+
|
|
116
|
+
return () => window.removeEventListener('keydown', onKeyDown, true)
|
|
117
|
+
}, [confirmAlways, respond])
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div className="mt-1 flex items-center gap-2.5 ps-5" data-slot="tool-approval-inline">
|
|
121
|
+
<div className="inline-flex h-6 items-stretch overflow-hidden rounded-md border border-primary/25 bg-primary/10 text-primary">
|
|
122
|
+
<Button
|
|
123
|
+
className="h-full gap-1 rounded-none px-2 text-xs font-medium text-primary hover:bg-primary/15 hover:text-primary"
|
|
124
|
+
disabled={busy}
|
|
125
|
+
onClick={() => void respond('once')}
|
|
126
|
+
size="xs"
|
|
127
|
+
variant="ghost"
|
|
128
|
+
>
|
|
129
|
+
{submitting === 'once' ? <Loader2 className="size-3 animate-spin" /> : copy.run}
|
|
130
|
+
{submitting !== 'once' && <span className="text-[0.625rem] text-primary/60">{isMac ? '⌘⏎' : 'Ctrl⏎'}</span>}
|
|
131
|
+
</Button>
|
|
132
|
+
<span aria-hidden className="w-px self-stretch bg-primary/20" />
|
|
133
|
+
<DropdownMenu>
|
|
134
|
+
<DropdownMenuTrigger asChild>
|
|
135
|
+
<Button
|
|
136
|
+
aria-label={copy.moreOptions}
|
|
137
|
+
className="h-full w-5 rounded-none px-0 text-primary hover:bg-primary/15 hover:text-primary"
|
|
138
|
+
disabled={busy}
|
|
139
|
+
size="xs"
|
|
140
|
+
variant="ghost"
|
|
141
|
+
>
|
|
142
|
+
<ChevronDown className="size-3" />
|
|
143
|
+
</Button>
|
|
144
|
+
</DropdownMenuTrigger>
|
|
145
|
+
<DropdownMenuContent align="start" className="min-w-44">
|
|
146
|
+
<DropdownMenuItem onSelect={() => void respond('session')}>{copy.allowSession}</DropdownMenuItem>
|
|
147
|
+
<DropdownMenuItem
|
|
148
|
+
onSelect={() => {
|
|
149
|
+
// Defer one tick so the menu fully unmounts before the dialog
|
|
150
|
+
// mounts — otherwise Radix's focus-return races the dialog and
|
|
151
|
+
// dismisses it via onInteractOutside.
|
|
152
|
+
setTimeout(() => setConfirmAlways(true), 0)
|
|
153
|
+
}}
|
|
154
|
+
>
|
|
155
|
+
{copy.alwaysAllowMenu}
|
|
156
|
+
</DropdownMenuItem>
|
|
157
|
+
<DropdownMenuItem onSelect={() => void respond('deny')} variant="destructive">
|
|
158
|
+
{copy.reject}
|
|
159
|
+
</DropdownMenuItem>
|
|
160
|
+
</DropdownMenuContent>
|
|
161
|
+
</DropdownMenu>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<Button
|
|
165
|
+
className="h-6 gap-1.5 rounded-md px-1.5 text-xs font-normal text-(--ui-text-tertiary) hover:text-foreground"
|
|
166
|
+
disabled={busy}
|
|
167
|
+
onClick={() => void respond('deny')}
|
|
168
|
+
size="xs"
|
|
169
|
+
variant="ghost"
|
|
170
|
+
>
|
|
171
|
+
{submitting === 'deny' ? <Loader2 className="size-3 animate-spin" /> : copy.reject}
|
|
172
|
+
{submitting !== 'deny' && <span className="text-[0.625rem] opacity-55">Esc</span>}
|
|
173
|
+
</Button>
|
|
174
|
+
|
|
175
|
+
<Dialog onOpenChange={setConfirmAlways} open={confirmAlways}>
|
|
176
|
+
<DialogContent className="max-w-md">
|
|
177
|
+
<DialogHeader>
|
|
178
|
+
<DialogTitle>{copy.alwaysTitle}</DialogTitle>
|
|
179
|
+
<DialogDescription>
|
|
180
|
+
{copy.alwaysDescription(request.description)}
|
|
181
|
+
</DialogDescription>
|
|
182
|
+
</DialogHeader>
|
|
183
|
+
|
|
184
|
+
{request.command.trim() && (
|
|
185
|
+
<pre className="max-h-32 overflow-auto whitespace-pre-wrap break-words rounded-md border border-(--ui-stroke-tertiary) bg-(--ui-chat-surface-background) px-2.5 py-1.5 font-mono text-xs leading-snug text-foreground">
|
|
186
|
+
{request.command.trim()}
|
|
187
|
+
</pre>
|
|
188
|
+
)}
|
|
189
|
+
|
|
190
|
+
<DialogFooter>
|
|
191
|
+
<Button onClick={() => setConfirmAlways(false)} size="sm" variant="ghost">
|
|
192
|
+
{t.common.cancel}
|
|
193
|
+
</Button>
|
|
194
|
+
<Button
|
|
195
|
+
onClick={() => {
|
|
196
|
+
setConfirmAlways(false)
|
|
197
|
+
void respond('always')
|
|
198
|
+
}}
|
|
199
|
+
size="sm"
|
|
200
|
+
variant="destructive"
|
|
201
|
+
>
|
|
202
|
+
{copy.alwaysAllow}
|
|
203
|
+
</Button>
|
|
204
|
+
</DialogFooter>
|
|
205
|
+
</DialogContent>
|
|
206
|
+
</Dialog>
|
|
207
|
+
</div>
|
|
208
|
+
)
|
|
209
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { buildToolView, type ToolPart } from './tool-fallback-model'
|
|
4
|
+
|
|
5
|
+
const part = (overrides: Partial<ToolPart>): ToolPart => ({
|
|
6
|
+
args: {},
|
|
7
|
+
isError: false,
|
|
8
|
+
result: {},
|
|
9
|
+
toolCallId: 'call_1',
|
|
10
|
+
toolName: 'vision_analyze',
|
|
11
|
+
type: 'tool-call',
|
|
12
|
+
...overrides
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
describe('buildToolView image handling', () => {
|
|
16
|
+
// vision_analyze reports the input image as a local path; an <img> pointed at
|
|
17
|
+
// a bare path resolves against the renderer origin and 404s, so we render the
|
|
18
|
+
// tool codicon instead of a broken image.
|
|
19
|
+
it('drops bare filesystem paths', () => {
|
|
20
|
+
expect(buildToolView(part({ args: { path: '/Users/me/shot.png' } }), '').imageUrl).toBe('')
|
|
21
|
+
expect(buildToolView(part({ result: { image_path: '/tmp/out.jpg' } }), '').imageUrl).toBe('')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('keeps fetchable data URLs', () => {
|
|
25
|
+
const dataUrl = 'data:image/png;base64,AAAA'
|
|
26
|
+
|
|
27
|
+
expect(buildToolView(part({ result: { image_url: dataUrl } }), '').imageUrl).toBe(dataUrl)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('keeps remote http(s) image URLs', () => {
|
|
31
|
+
const url = 'https://example.com/pic.webp'
|
|
32
|
+
|
|
33
|
+
expect(buildToolView(part({ result: { url } }), '').imageUrl).toBe(url)
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('buildToolView terminal exit-code status', () => {
|
|
38
|
+
const terminal = (result: Record<string, unknown>) =>
|
|
39
|
+
buildToolView(part({ result, toolName: 'terminal' }), '')
|
|
40
|
+
|
|
41
|
+
// A non-zero exit code with real output is not a failure (grep no-match,
|
|
42
|
+
// diff differences, piped commands surfacing the last stage's code, etc.) —
|
|
43
|
+
// it should render as success so the card isn't painted red.
|
|
44
|
+
it('treats non-zero exit with output as success', () => {
|
|
45
|
+
expect(terminal({ exit_code: 7, output: 'node ... 5174 (LISTEN)' }).status).toBe('success')
|
|
46
|
+
expect(terminal({ exit_code: 1, stdout: 'partial results' }).status).toBe('success')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// No output + non-zero exit is a genuine failure worth flagging.
|
|
50
|
+
it('treats non-zero exit with no output as error', () => {
|
|
51
|
+
expect(terminal({ exit_code: 127, output: '' }).status).toBe('error')
|
|
52
|
+
expect(terminal({ exit_code: 1 }).status).toBe('error')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('treats zero exit as success', () => {
|
|
56
|
+
expect(terminal({ exit_code: 0, output: 'done' }).status).toBe('success')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// Explicit error signals still win regardless of output presence.
|
|
60
|
+
it('keeps explicit error signals red even with output', () => {
|
|
61
|
+
expect(terminal({ error: 'boom', exit_code: 0, output: 'partial' }).status).toBe('error')
|
|
62
|
+
expect(buildToolView(part({ isError: true, result: { output: 'x' }, toolName: 'terminal' }), '').status).toBe(
|
|
63
|
+
'error'
|
|
64
|
+
)
|
|
65
|
+
})
|
|
66
|
+
})
|