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,957 @@
|
|
|
1
|
+
import type { QueryClient } from '@tanstack/react-query'
|
|
2
|
+
import { type MutableRefObject, useCallback, useEffect, useRef } from 'react'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
appendAssistantTextPart,
|
|
6
|
+
appendReasoningPart,
|
|
7
|
+
assistantTextPart,
|
|
8
|
+
type ChatMessage,
|
|
9
|
+
type ChatMessagePart,
|
|
10
|
+
chatMessageText,
|
|
11
|
+
type GatewayEventPayload,
|
|
12
|
+
reasoningPart,
|
|
13
|
+
renderMediaTags,
|
|
14
|
+
upsertToolPart
|
|
15
|
+
} from '@/lib/chat-messages'
|
|
16
|
+
import { coerceGatewayText, coerceThinkingText, normalizePersonalityValue } from '@/lib/chat-runtime'
|
|
17
|
+
import { triggerHaptic } from '@/lib/haptics'
|
|
18
|
+
import { isProviderSetupErrorMessage } from '@/lib/provider-setup-errors'
|
|
19
|
+
import { setClarifyRequest } from '@/store/clarify'
|
|
20
|
+
import { notify } from '@/store/notifications'
|
|
21
|
+
import { requestDesktopOnboarding } from '@/store/onboarding'
|
|
22
|
+
import { clearAllPrompts, setApprovalRequest, setSecretRequest, setSudoRequest } from '@/store/prompts'
|
|
23
|
+
import {
|
|
24
|
+
setCurrentBranch,
|
|
25
|
+
setCurrentCwd,
|
|
26
|
+
setCurrentFastMode,
|
|
27
|
+
setCurrentModel,
|
|
28
|
+
setCurrentPersonality,
|
|
29
|
+
setCurrentProvider,
|
|
30
|
+
setCurrentReasoningEffort,
|
|
31
|
+
setCurrentServiceTier,
|
|
32
|
+
setCurrentUsage,
|
|
33
|
+
setTurnStartedAt,
|
|
34
|
+
setYoloActive
|
|
35
|
+
} from '@/store/session'
|
|
36
|
+
import { clearSessionSubagents, pruneDelegateFallbackSubagents, upsertSubagent } from '@/store/subagents'
|
|
37
|
+
import { recordToolDiff } from '@/store/tool-diffs'
|
|
38
|
+
import type { RpcEvent } from '@/types/nastech'
|
|
39
|
+
|
|
40
|
+
import type { ClientSessionState } from '../../types'
|
|
41
|
+
|
|
42
|
+
interface MessageStreamOptions {
|
|
43
|
+
activeSessionIdRef: MutableRefObject<string | null>
|
|
44
|
+
hydrateFromStoredSession: (
|
|
45
|
+
attempts?: number,
|
|
46
|
+
storedSessionId?: string | null,
|
|
47
|
+
runtimeSessionId?: string | null
|
|
48
|
+
) => Promise<void>
|
|
49
|
+
queryClient: QueryClient
|
|
50
|
+
refreshNasTechConfig: () => Promise<void>
|
|
51
|
+
refreshSessions: () => Promise<void>
|
|
52
|
+
updateSessionState: (
|
|
53
|
+
sessionId: string,
|
|
54
|
+
updater: (state: ClientSessionState) => ClientSessionState,
|
|
55
|
+
storedSessionId?: string | null
|
|
56
|
+
) => ClientSessionState
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface QueuedStreamDeltas {
|
|
60
|
+
assistant: string
|
|
61
|
+
reasoning: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Minimum gap between two assistant-text flushes during a stream. Was 16ms
|
|
65
|
+
// (rAF only), which at typical LLM token rates of ~30-80 tok/sec meant every
|
|
66
|
+
// token got its own React commit + Streamdown markdown re-parse, scaling
|
|
67
|
+
// linearly with the growing last-block length. Bumping to 33ms lets ~2 tokens
|
|
68
|
+
// batch into one commit at 60 tok/sec without introducing visible lag on the
|
|
69
|
+
// streaming text (still 30 fps of visible text growth). Big perceived
|
|
70
|
+
// smoothness win on long messages with big trailing paragraphs; see
|
|
71
|
+
// `scripts/profile-typing-lag.md` for the measurement work behind this.
|
|
72
|
+
const STREAM_DELTA_FLUSH_MS = 33
|
|
73
|
+
|
|
74
|
+
// Gateway/provider failures sometimes arrive as message.complete text instead
|
|
75
|
+
// of an explicit error event. Treat matches as inline assistant errors so they
|
|
76
|
+
// persist like real error events and don't get erased by hydrate fallback.
|
|
77
|
+
const COMPLETION_ERROR_PATTERNS = [
|
|
78
|
+
/^API call failed after \d+ retries:/i,
|
|
79
|
+
/^HTTP\s+\d{3}\b/i,
|
|
80
|
+
/^(Provider|Gateway)\s+error:/i
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
function completionErrorText(finalText: string): string | null {
|
|
84
|
+
const text = finalText.trim()
|
|
85
|
+
|
|
86
|
+
return text && COMPLETION_ERROR_PATTERNS.some(re => re.test(text)) ? text : null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const SUBAGENT_EVENT_TYPES = new Set([
|
|
90
|
+
'subagent.spawn_requested',
|
|
91
|
+
'subagent.start',
|
|
92
|
+
'subagent.thinking',
|
|
93
|
+
'subagent.tool',
|
|
94
|
+
'subagent.progress',
|
|
95
|
+
'subagent.complete'
|
|
96
|
+
])
|
|
97
|
+
|
|
98
|
+
// Anonymous progress events that carry todos but no name still belong to the
|
|
99
|
+
// todo stream; named todo events are obviously routed there too.
|
|
100
|
+
function toTodoPayload(payload: GatewayEventPayload | undefined): GatewayEventPayload | undefined {
|
|
101
|
+
if (!payload) {
|
|
102
|
+
return undefined
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const isTodo = payload.name === 'todo' || (!payload.name && Object.hasOwn(payload, 'todos'))
|
|
106
|
+
|
|
107
|
+
return isTodo ? { ...payload, name: 'todo', tool_id: payload.tool_id || 'todo-live' } : undefined
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
111
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : {}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function parseMaybeRecord(value: unknown): Record<string, unknown> {
|
|
115
|
+
if (typeof value === 'string') {
|
|
116
|
+
try {
|
|
117
|
+
return asRecord(JSON.parse(value))
|
|
118
|
+
} catch {
|
|
119
|
+
return {}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return asRecord(value)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const firstString = (...candidates: unknown[]): string => {
|
|
127
|
+
for (const v of candidates) {
|
|
128
|
+
if (typeof v === 'string' && v) {
|
|
129
|
+
return v
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return ''
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function delegateTaskPayloads(
|
|
137
|
+
payload: GatewayEventPayload | undefined,
|
|
138
|
+
phase: 'running' | 'complete',
|
|
139
|
+
sourceEventType?: string
|
|
140
|
+
): Record<string, unknown>[] {
|
|
141
|
+
if (payload?.name !== 'delegate_task') {
|
|
142
|
+
return []
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const args = parseMaybeRecord(payload.args ?? payload.input)
|
|
146
|
+
const result = parseMaybeRecord(payload.result)
|
|
147
|
+
const rawTasks = Array.isArray(args.tasks) ? args.tasks : []
|
|
148
|
+
const tasks = rawTasks.length ? rawTasks.map(parseMaybeRecord) : [args]
|
|
149
|
+
const status = phase === 'complete' ? (payload.error ? 'failed' : 'completed') : 'running'
|
|
150
|
+
const toolId = payload.tool_id || payload.tool_call_id || payload.id || 'delegate_task'
|
|
151
|
+
const progressText = firstString(payload.preview, payload.message, payload.context)
|
|
152
|
+
|
|
153
|
+
const eventType =
|
|
154
|
+
phase === 'complete'
|
|
155
|
+
? 'subagent.complete'
|
|
156
|
+
: sourceEventType === 'tool.start'
|
|
157
|
+
? 'subagent.start'
|
|
158
|
+
: 'subagent.progress'
|
|
159
|
+
|
|
160
|
+
return tasks.map((task, index) => {
|
|
161
|
+
const goal = firstString(task.goal, args.goal, payload.context) || 'Delegated task'
|
|
162
|
+
const summary = firstString(result.summary, payload.summary, payload.message)
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
depth: 0,
|
|
166
|
+
duration_seconds: payload.duration_s,
|
|
167
|
+
goal,
|
|
168
|
+
status,
|
|
169
|
+
subagent_id: `delegate-tool:${toolId}:${index}`,
|
|
170
|
+
summary: summary || undefined,
|
|
171
|
+
task_count: tasks.length,
|
|
172
|
+
task_index: index,
|
|
173
|
+
text: eventType === 'subagent.progress' ? progressText || goal : undefined,
|
|
174
|
+
tool_name: eventType === 'subagent.start' ? 'delegate_task' : undefined,
|
|
175
|
+
tool_preview: eventType === 'subagent.start' ? progressText : undefined,
|
|
176
|
+
toolsets: Array.isArray(task.toolsets) ? task.toolsets : Array.isArray(args.toolsets) ? args.toolsets : [],
|
|
177
|
+
event_type: eventType,
|
|
178
|
+
output_tail:
|
|
179
|
+
phase === 'complete' && summary
|
|
180
|
+
? [{ is_error: Boolean(payload.error), preview: summary, tool: 'delegate_task' }]
|
|
181
|
+
: undefined
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function useMessageStream({
|
|
187
|
+
activeSessionIdRef,
|
|
188
|
+
hydrateFromStoredSession,
|
|
189
|
+
queryClient,
|
|
190
|
+
refreshNasTechConfig,
|
|
191
|
+
refreshSessions,
|
|
192
|
+
updateSessionState
|
|
193
|
+
}: MessageStreamOptions) {
|
|
194
|
+
// Patch the in-flight assistant message (or seed it). Centralises the
|
|
195
|
+
// streamId/groupId bookkeeping every event callback would otherwise repeat.
|
|
196
|
+
const mutateStream = useCallback(
|
|
197
|
+
(
|
|
198
|
+
sessionId: string,
|
|
199
|
+
transform: (parts: ChatMessagePart[], message: ChatMessage) => ChatMessagePart[],
|
|
200
|
+
seed: () => ChatMessagePart[],
|
|
201
|
+
opts: {
|
|
202
|
+
pending?: (message: ChatMessage) => boolean
|
|
203
|
+
} = {}
|
|
204
|
+
) => {
|
|
205
|
+
const apply = () => {
|
|
206
|
+
updateSessionState(sessionId, state => {
|
|
207
|
+
// After a stop, drop any late deltas / tool events for the
|
|
208
|
+
// cancelled turn so they don't keep growing the (now finalized)
|
|
209
|
+
// assistant bubble or, worse, seed a brand-new bubble that
|
|
210
|
+
// appears to belong to the next user message.
|
|
211
|
+
if (state.interrupted) {
|
|
212
|
+
return state
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const streamId = state.streamId ?? `assistant-stream-${Date.now()}`
|
|
216
|
+
const groupId = state.pendingBranchGroup ?? undefined
|
|
217
|
+
const prev = state.messages
|
|
218
|
+
let nextMessages: ChatMessage[]
|
|
219
|
+
|
|
220
|
+
if (!prev.some(m => m.id === streamId)) {
|
|
221
|
+
nextMessages = [
|
|
222
|
+
...prev,
|
|
223
|
+
{
|
|
224
|
+
id: streamId,
|
|
225
|
+
role: 'assistant',
|
|
226
|
+
parts: seed(),
|
|
227
|
+
pending: true,
|
|
228
|
+
branchGroupId: groupId
|
|
229
|
+
}
|
|
230
|
+
]
|
|
231
|
+
} else {
|
|
232
|
+
nextMessages = prev.map(m =>
|
|
233
|
+
m.id === streamId
|
|
234
|
+
? {
|
|
235
|
+
...m,
|
|
236
|
+
parts: transform(m.parts, m),
|
|
237
|
+
pending: opts.pending ? opts.pending(m) : true
|
|
238
|
+
}
|
|
239
|
+
: m
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
...state,
|
|
245
|
+
messages: nextMessages,
|
|
246
|
+
streamId,
|
|
247
|
+
sawAssistantPayload: true,
|
|
248
|
+
awaitingResponse: false
|
|
249
|
+
}
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
apply()
|
|
254
|
+
},
|
|
255
|
+
[updateSessionState]
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
const queuedDeltasRef = useRef<Map<string, QueuedStreamDeltas>>(new Map())
|
|
259
|
+
const flushHandleRef = useRef<number | null>(null)
|
|
260
|
+
const lastFlushAtRef = useRef<number>(0)
|
|
261
|
+
const nativeSubagentSessionsRef = useRef<Set<string>>(new Set())
|
|
262
|
+
|
|
263
|
+
const flushQueuedDeltas = useCallback(
|
|
264
|
+
(sessionId?: string) => {
|
|
265
|
+
const queue = queuedDeltasRef.current
|
|
266
|
+
const ids = sessionId ? [sessionId] : [...queue.keys()]
|
|
267
|
+
|
|
268
|
+
for (const id of ids) {
|
|
269
|
+
const queued = queue.get(id)
|
|
270
|
+
|
|
271
|
+
if (!queued) {
|
|
272
|
+
continue
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
queue.delete(id)
|
|
276
|
+
|
|
277
|
+
if (queued.assistant) {
|
|
278
|
+
mutateStream(
|
|
279
|
+
id,
|
|
280
|
+
parts => appendAssistantTextPart(parts, queued.assistant),
|
|
281
|
+
() => [assistantTextPart(queued.assistant)]
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (queued.reasoning) {
|
|
286
|
+
mutateStream(
|
|
287
|
+
id,
|
|
288
|
+
parts => appendReasoningPart(parts, queued.reasoning),
|
|
289
|
+
() => [reasoningPart(queued.reasoning)]
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
[mutateStream]
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
const scheduleDeltaFlush = useCallback(() => {
|
|
298
|
+
if (flushHandleRef.current !== null) {
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (typeof window === 'undefined') {
|
|
303
|
+
flushQueuedDeltas()
|
|
304
|
+
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Enforce a floor on the gap between two flushes. Without it, an LLM
|
|
309
|
+
// emitting tokens slower than the rAF cadence (~30-80 tok/sec is typical)
|
|
310
|
+
// forces one React commit + Streamdown re-parse per token, and the
|
|
311
|
+
// last-block markdown re-parse cost is roughly linear in current block
|
|
312
|
+
// length. With this floor, slower streams still coalesce ~2 tokens per
|
|
313
|
+
// commit and the synthetic harness shows longtask counts drop from ~5/5s
|
|
314
|
+
// to ~1/5s on big sessions (see scripts/profile-typing-lag.md).
|
|
315
|
+
const sinceLast = performance.now() - lastFlushAtRef.current
|
|
316
|
+
|
|
317
|
+
const runFlush = () => {
|
|
318
|
+
flushHandleRef.current = null
|
|
319
|
+
lastFlushAtRef.current = performance.now()
|
|
320
|
+
flushQueuedDeltas()
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (sinceLast >= STREAM_DELTA_FLUSH_MS && typeof window.requestAnimationFrame === 'function') {
|
|
324
|
+
flushHandleRef.current = window.requestAnimationFrame(runFlush)
|
|
325
|
+
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
flushHandleRef.current = window.setTimeout(runFlush, Math.max(0, STREAM_DELTA_FLUSH_MS - sinceLast))
|
|
330
|
+
}, [flushQueuedDeltas])
|
|
331
|
+
|
|
332
|
+
const queueDelta = useCallback(
|
|
333
|
+
(sessionId: string, key: keyof QueuedStreamDeltas, delta: string) => {
|
|
334
|
+
if (!delta) {
|
|
335
|
+
return
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const queued = queuedDeltasRef.current.get(sessionId) ?? { assistant: '', reasoning: '' }
|
|
339
|
+
queued[key] += delta
|
|
340
|
+
queuedDeltasRef.current.set(sessionId, queued)
|
|
341
|
+
scheduleDeltaFlush()
|
|
342
|
+
},
|
|
343
|
+
[scheduleDeltaFlush]
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
useEffect(
|
|
347
|
+
() => () => {
|
|
348
|
+
if (flushHandleRef.current !== null && typeof window !== 'undefined') {
|
|
349
|
+
if (typeof window.cancelAnimationFrame === 'function') {
|
|
350
|
+
window.cancelAnimationFrame(flushHandleRef.current)
|
|
351
|
+
} else {
|
|
352
|
+
window.clearTimeout(flushHandleRef.current)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
flushHandleRef.current = null
|
|
357
|
+
flushQueuedDeltas()
|
|
358
|
+
},
|
|
359
|
+
[flushQueuedDeltas]
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
const appendAssistantDelta = useCallback(
|
|
363
|
+
(sessionId: string, delta: string) => {
|
|
364
|
+
if (!delta) {
|
|
365
|
+
return
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
queueDelta(sessionId, 'assistant', delta)
|
|
369
|
+
},
|
|
370
|
+
[queueDelta]
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
const appendReasoningDelta = useCallback(
|
|
374
|
+
(sessionId: string, delta: string, replace = false) => {
|
|
375
|
+
if (!delta) {
|
|
376
|
+
return
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (!replace) {
|
|
380
|
+
queueDelta(sessionId, 'reasoning', delta)
|
|
381
|
+
|
|
382
|
+
return
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
flushQueuedDeltas(sessionId)
|
|
386
|
+
|
|
387
|
+
mutateStream(
|
|
388
|
+
sessionId,
|
|
389
|
+
(parts, message) => {
|
|
390
|
+
if (replace && chatMessageText(message).trim()) {
|
|
391
|
+
return parts
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (replace) {
|
|
395
|
+
return [...parts.filter(part => part.type !== 'reasoning'), reasoningPart(delta)]
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return appendReasoningPart(parts, delta)
|
|
399
|
+
},
|
|
400
|
+
() => [reasoningPart(delta)]
|
|
401
|
+
)
|
|
402
|
+
},
|
|
403
|
+
[flushQueuedDeltas, mutateStream, queueDelta]
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
const upsertToolCall = useCallback(
|
|
407
|
+
(
|
|
408
|
+
sessionId: string,
|
|
409
|
+
payload: GatewayEventPayload | undefined,
|
|
410
|
+
phase: 'running' | 'complete',
|
|
411
|
+
sourceEventType?: string
|
|
412
|
+
) => {
|
|
413
|
+
// Text deltas flush on a timer but tool events apply now; flush first so
|
|
414
|
+
// a tool part can't jump ahead of the text that preceded it.
|
|
415
|
+
flushQueuedDeltas(sessionId)
|
|
416
|
+
|
|
417
|
+
if (!nativeSubagentSessionsRef.current.has(sessionId)) {
|
|
418
|
+
for (const subagentPayload of delegateTaskPayloads(payload, phase, sourceEventType)) {
|
|
419
|
+
upsertSubagent(
|
|
420
|
+
sessionId,
|
|
421
|
+
subagentPayload,
|
|
422
|
+
true,
|
|
423
|
+
phase === 'complete' ? 'delegate.complete' : 'delegate.running'
|
|
424
|
+
)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
mutateStream(
|
|
429
|
+
sessionId,
|
|
430
|
+
parts => upsertToolPart(parts, payload, phase),
|
|
431
|
+
() => upsertToolPart([], payload, phase),
|
|
432
|
+
{ pending: m => phase !== 'complete' || (m.pending ?? false) }
|
|
433
|
+
)
|
|
434
|
+
},
|
|
435
|
+
[flushQueuedDeltas, mutateStream]
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
const completeAssistantMessage = useCallback(
|
|
439
|
+
(sessionId: string, text: string) => {
|
|
440
|
+
let shouldHydrate = false
|
|
441
|
+
|
|
442
|
+
const completedState = updateSessionState(sessionId, state => {
|
|
443
|
+
// Late completion from an already-cancelled turn: cancelRun has
|
|
444
|
+
// already finalized the bubble (kept the partial text, dropped it if
|
|
445
|
+
// empty). Re-running the dedupe below would replace the partial with
|
|
446
|
+
// the just-cancelled full text, so we settle and bail instead.
|
|
447
|
+
if (state.interrupted) {
|
|
448
|
+
return {
|
|
449
|
+
...state,
|
|
450
|
+
awaitingResponse: false,
|
|
451
|
+
busy: false,
|
|
452
|
+
needsInput: false,
|
|
453
|
+
pendingBranchGroup: null,
|
|
454
|
+
streamId: null,
|
|
455
|
+
turnStartedAt: null
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const streamId = state.streamId
|
|
460
|
+
const finalText = renderMediaTags(text).trim()
|
|
461
|
+
const completionError = completionErrorText(finalText)
|
|
462
|
+
const normalize = (value: string) => value.replace(/\s+/g, ' ').trim()
|
|
463
|
+
const dedupeReference = normalize(finalText)
|
|
464
|
+
|
|
465
|
+
const replaceTextPart = (parts: ChatMessagePart[]) => {
|
|
466
|
+
const kept = parts.filter(part => {
|
|
467
|
+
if (part.type === 'text') {
|
|
468
|
+
return false
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (part.type !== 'reasoning' || !dedupeReference) {
|
|
472
|
+
return true
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const r = normalize(part.text)
|
|
476
|
+
|
|
477
|
+
return !(r && (dedupeReference.startsWith(r) || r.startsWith(dedupeReference)))
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
return finalText ? [...kept, assistantTextPart(finalText)] : kept
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const completeMessage = (message: ChatMessage): ChatMessage =>
|
|
484
|
+
completionError
|
|
485
|
+
? {
|
|
486
|
+
...message,
|
|
487
|
+
error: completionError,
|
|
488
|
+
parts: message.parts.filter(part => part.type !== 'text'),
|
|
489
|
+
pending: false
|
|
490
|
+
}
|
|
491
|
+
: {
|
|
492
|
+
...message,
|
|
493
|
+
parts: replaceTextPart(message.parts),
|
|
494
|
+
pending: false
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const newAssistantFromCompletion = (): ChatMessage => ({
|
|
498
|
+
id: `assistant-${Date.now()}`,
|
|
499
|
+
role: 'assistant',
|
|
500
|
+
parts: completionError ? [] : [assistantTextPart(finalText)],
|
|
501
|
+
branchGroupId: state.pendingBranchGroup ?? undefined,
|
|
502
|
+
...(completionError && { error: completionError })
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
const prev = state.messages
|
|
506
|
+
let nextMessages = prev
|
|
507
|
+
|
|
508
|
+
if (streamId && prev.some(m => m.id === streamId)) {
|
|
509
|
+
nextMessages = prev.map(m => (m.id === streamId ? completeMessage(m) : m))
|
|
510
|
+
} else {
|
|
511
|
+
const fallbackIndex = [...prev]
|
|
512
|
+
.reverse()
|
|
513
|
+
.findIndex(message => message.role === 'assistant' && !message.hidden)
|
|
514
|
+
|
|
515
|
+
if (fallbackIndex >= 0) {
|
|
516
|
+
const index = prev.length - 1 - fallbackIndex
|
|
517
|
+
const existing = prev[index]
|
|
518
|
+
const existingText = chatMessageText(existing).trim()
|
|
519
|
+
|
|
520
|
+
if (existing.pending || (finalText && existingText === finalText)) {
|
|
521
|
+
nextMessages = prev.map((message, messageIndex) =>
|
|
522
|
+
messageIndex === index ? completeMessage(message) : message
|
|
523
|
+
)
|
|
524
|
+
} else if (finalText) {
|
|
525
|
+
nextMessages = [...prev, newAssistantFromCompletion()]
|
|
526
|
+
}
|
|
527
|
+
} else if (finalText) {
|
|
528
|
+
nextMessages = [...prev, newAssistantFromCompletion()]
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const hasInlineError = nextMessages.some(m => m.role === 'assistant' && m.error && !m.hidden)
|
|
533
|
+
const lastVisible = [...nextMessages].reverse().find(m => !m.hidden)
|
|
534
|
+
const unresolvedUserTail = lastVisible?.role === 'user'
|
|
535
|
+
shouldHydrate =
|
|
536
|
+
!completionError && !hasInlineError && !unresolvedUserTail && (!state.sawAssistantPayload || !finalText)
|
|
537
|
+
|
|
538
|
+
return {
|
|
539
|
+
...state,
|
|
540
|
+
messages: nextMessages,
|
|
541
|
+
streamId: null,
|
|
542
|
+
pendingBranchGroup: null,
|
|
543
|
+
awaitingResponse: false,
|
|
544
|
+
busy: false,
|
|
545
|
+
needsInput: false,
|
|
546
|
+
turnStartedAt: null
|
|
547
|
+
}
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
void refreshSessions().catch(() => undefined)
|
|
551
|
+
|
|
552
|
+
if (shouldHydrate) {
|
|
553
|
+
void hydrateFromStoredSession(3, completedState.storedSessionId, sessionId)
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (document.hidden && sessionId === activeSessionIdRef.current) {
|
|
557
|
+
void window.NASTECHDesktop?.notify({
|
|
558
|
+
title: 'NasTech finished',
|
|
559
|
+
body: text.slice(0, 140) || 'The response is ready.'
|
|
560
|
+
})
|
|
561
|
+
}
|
|
562
|
+
},
|
|
563
|
+
[activeSessionIdRef, hydrateFromStoredSession, refreshSessions, updateSessionState]
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
const failAssistantMessage = useCallback(
|
|
567
|
+
(sessionId: string, errorMessage: string) => {
|
|
568
|
+
updateSessionState(sessionId, state => {
|
|
569
|
+
const streamId = state.streamId ?? `assistant-error-${Date.now()}`
|
|
570
|
+
const groupId = state.pendingBranchGroup ?? undefined
|
|
571
|
+
const prev = state.messages
|
|
572
|
+
const error = errorMessage.trim() || 'NasTech reported an error'
|
|
573
|
+
|
|
574
|
+
const nextMessages = prev.some(m => m.id === streamId)
|
|
575
|
+
? prev.map(message =>
|
|
576
|
+
message.id === streamId
|
|
577
|
+
? {
|
|
578
|
+
...message,
|
|
579
|
+
error,
|
|
580
|
+
pending: false
|
|
581
|
+
}
|
|
582
|
+
: message
|
|
583
|
+
)
|
|
584
|
+
: [
|
|
585
|
+
...prev,
|
|
586
|
+
{
|
|
587
|
+
id: streamId,
|
|
588
|
+
role: 'assistant' as const,
|
|
589
|
+
parts: [],
|
|
590
|
+
error,
|
|
591
|
+
pending: false,
|
|
592
|
+
branchGroupId: groupId
|
|
593
|
+
}
|
|
594
|
+
]
|
|
595
|
+
|
|
596
|
+
return {
|
|
597
|
+
...state,
|
|
598
|
+
messages: nextMessages,
|
|
599
|
+
streamId: null,
|
|
600
|
+
pendingBranchGroup: null,
|
|
601
|
+
sawAssistantPayload: true,
|
|
602
|
+
awaitingResponse: false,
|
|
603
|
+
busy: false,
|
|
604
|
+
needsInput: false,
|
|
605
|
+
turnStartedAt: null
|
|
606
|
+
}
|
|
607
|
+
})
|
|
608
|
+
},
|
|
609
|
+
[updateSessionState]
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
const handleGatewayEvent = useCallback(
|
|
613
|
+
(event: RpcEvent) => {
|
|
614
|
+
const payload = event.payload as GatewayEventPayload | undefined
|
|
615
|
+
const explicitSid = event.session_id || ''
|
|
616
|
+
const sessionId = explicitSid || activeSessionIdRef.current
|
|
617
|
+
const isActiveEvent = !!sessionId && sessionId === activeSessionIdRef.current
|
|
618
|
+
|
|
619
|
+
if (event.type === 'gateway.ready') {
|
|
620
|
+
return
|
|
621
|
+
} else if (event.type === 'session.info') {
|
|
622
|
+
// Apply session-scoped fields when the event targets the active
|
|
623
|
+
// session, OR when it's a global broadcast and we have no session.
|
|
624
|
+
const apply = explicitSid ? isActiveEvent : !activeSessionIdRef.current
|
|
625
|
+
const modelChanged = typeof payload?.model === 'string'
|
|
626
|
+
const providerChanged = typeof payload?.provider === 'string'
|
|
627
|
+
const runningChanged = typeof payload?.running === 'boolean'
|
|
628
|
+
|
|
629
|
+
if (apply) {
|
|
630
|
+
const runtimeInfo: { branch?: string; cwd?: string } = {}
|
|
631
|
+
|
|
632
|
+
if (modelChanged) {
|
|
633
|
+
setCurrentModel(payload!.model || '')
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (providerChanged) {
|
|
637
|
+
setCurrentProvider(payload!.provider || '')
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (typeof payload?.cwd === 'string') {
|
|
641
|
+
setCurrentCwd(payload.cwd)
|
|
642
|
+
runtimeInfo.cwd = payload.cwd
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (typeof payload?.branch === 'string') {
|
|
646
|
+
setCurrentBranch(payload.branch)
|
|
647
|
+
runtimeInfo.branch = payload.branch
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (sessionId && (runtimeInfo.cwd !== undefined || runtimeInfo.branch !== undefined)) {
|
|
651
|
+
updateSessionState(sessionId, state => ({
|
|
652
|
+
...state,
|
|
653
|
+
branch: runtimeInfo.branch ?? state.branch,
|
|
654
|
+
cwd: runtimeInfo.cwd ?? state.cwd
|
|
655
|
+
}))
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (typeof payload?.personality === 'string') {
|
|
659
|
+
setCurrentPersonality(normalizePersonalityValue(payload.personality))
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (typeof payload?.reasoning_effort === 'string') {
|
|
663
|
+
setCurrentReasoningEffort(payload.reasoning_effort)
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (typeof payload?.service_tier === 'string') {
|
|
667
|
+
setCurrentServiceTier(payload.service_tier)
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (typeof payload?.fast === 'boolean') {
|
|
671
|
+
setCurrentFastMode(payload.fast)
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (typeof payload?.yolo === 'boolean') {
|
|
675
|
+
setYoloActive(payload.yolo)
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (runningChanged && sessionId) {
|
|
679
|
+
updateSessionState(sessionId, state => {
|
|
680
|
+
const busy = Boolean(payload!.running)
|
|
681
|
+
|
|
682
|
+
if (state.busy === busy && (busy || !state.awaitingResponse)) {
|
|
683
|
+
return state
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (busy) {
|
|
687
|
+
return {
|
|
688
|
+
...state,
|
|
689
|
+
busy,
|
|
690
|
+
turnStartedAt: state.turnStartedAt ?? Date.now()
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (state.awaitingResponse && !state.sawAssistantPayload) {
|
|
695
|
+
return state
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
return {
|
|
699
|
+
...state,
|
|
700
|
+
awaitingResponse: false,
|
|
701
|
+
busy,
|
|
702
|
+
pendingBranchGroup: null,
|
|
703
|
+
streamId: null,
|
|
704
|
+
turnStartedAt: null
|
|
705
|
+
}
|
|
706
|
+
})
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (payload?.usage && (!explicitSid || isActiveEvent)) {
|
|
711
|
+
setCurrentUsage(current => ({ ...current, ...payload.usage }))
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
if (typeof payload?.credential_warning === 'string' && payload.credential_warning) {
|
|
715
|
+
requestDesktopOnboarding(payload.credential_warning)
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
void refreshNasTechConfig()
|
|
719
|
+
|
|
720
|
+
if (modelChanged || providerChanged) {
|
|
721
|
+
void queryClient.invalidateQueries({
|
|
722
|
+
queryKey: explicitSid && sessionId ? ['model-options', sessionId] : ['model-options']
|
|
723
|
+
})
|
|
724
|
+
}
|
|
725
|
+
} else if (event.type === 'message.start') {
|
|
726
|
+
if (!sessionId) {
|
|
727
|
+
return
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
flushQueuedDeltas(sessionId)
|
|
731
|
+
clearSessionSubagents(sessionId)
|
|
732
|
+
nativeSubagentSessionsRef.current.delete(sessionId)
|
|
733
|
+
|
|
734
|
+
if (isActiveEvent) {
|
|
735
|
+
triggerHaptic('streamStart')
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
updateSessionState(sessionId, state => ({
|
|
739
|
+
...state,
|
|
740
|
+
busy: true,
|
|
741
|
+
awaitingResponse: true,
|
|
742
|
+
sawAssistantPayload: false,
|
|
743
|
+
interrupted: false,
|
|
744
|
+
turnStartedAt: Date.now()
|
|
745
|
+
}))
|
|
746
|
+
|
|
747
|
+
if (isActiveEvent) {
|
|
748
|
+
setTurnStartedAt(Date.now())
|
|
749
|
+
}
|
|
750
|
+
} else if (event.type === 'message.delta') {
|
|
751
|
+
if (sessionId) {
|
|
752
|
+
appendAssistantDelta(sessionId, coerceGatewayText(payload?.text))
|
|
753
|
+
}
|
|
754
|
+
} else if (event.type === 'thinking.delta') {
|
|
755
|
+
// thinking.delta carries the kawaii spinner status (face + verb from
|
|
756
|
+
// KawaiiSpinner), not real reasoning. The bottom-of-thread loading
|
|
757
|
+
// indicator already covers that UX, so we ignore these events to
|
|
758
|
+
// avoid a duplicative "Thinking" disclosure showing spinner text.
|
|
759
|
+
} else if (event.type === 'reasoning.delta') {
|
|
760
|
+
if (sessionId) {
|
|
761
|
+
appendReasoningDelta(sessionId, coerceThinkingText(payload?.text))
|
|
762
|
+
}
|
|
763
|
+
} else if (event.type === 'reasoning.available') {
|
|
764
|
+
if (sessionId) {
|
|
765
|
+
appendReasoningDelta(sessionId, coerceThinkingText(payload?.text), true)
|
|
766
|
+
}
|
|
767
|
+
} else if (event.type === 'message.complete') {
|
|
768
|
+
if (!sessionId) {
|
|
769
|
+
return
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Turn ended — drop any blocking prompt still open for THIS session
|
|
773
|
+
// (e.g. interrupted, or the approval already resolved). Scoped to the
|
|
774
|
+
// session so a background turn finishing can't wipe the active chat's
|
|
775
|
+
// prompt, and vice versa.
|
|
776
|
+
clearAllPrompts(sessionId)
|
|
777
|
+
|
|
778
|
+
flushQueuedDeltas(sessionId)
|
|
779
|
+
|
|
780
|
+
if (isActiveEvent) {
|
|
781
|
+
triggerHaptic('streamDone')
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const finalText = coerceGatewayText(payload?.text) || coerceGatewayText(payload?.rendered)
|
|
785
|
+
completeAssistantMessage(sessionId, finalText)
|
|
786
|
+
|
|
787
|
+
if (isActiveEvent) {
|
|
788
|
+
setTurnStartedAt(null)
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (payload?.usage) {
|
|
792
|
+
setCurrentUsage(current => ({ ...current, ...payload.usage }))
|
|
793
|
+
}
|
|
794
|
+
} else if (event.type === 'tool.start' || event.type === 'tool.progress' || event.type === 'tool.generating') {
|
|
795
|
+
if (!sessionId) {
|
|
796
|
+
return
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
flushQueuedDeltas(sessionId)
|
|
800
|
+
upsertToolCall(sessionId, toTodoPayload(payload) ?? payload, 'running', event.type)
|
|
801
|
+
} else if (event.type === 'tool.complete') {
|
|
802
|
+
if (sessionId) {
|
|
803
|
+
flushQueuedDeltas(sessionId)
|
|
804
|
+
upsertToolCall(sessionId, toTodoPayload(payload) ?? payload, 'complete', event.type)
|
|
805
|
+
// A pending clarify blocks the turn, so the first tool.complete after
|
|
806
|
+
// one is the clarify resolving — drop the "needs input" flag here so
|
|
807
|
+
// the sidebar indicator clears as soon as it's answered, not only at
|
|
808
|
+
// message.complete.
|
|
809
|
+
updateSessionState(sessionId, state => (state.needsInput ? { ...state, needsInput: false } : state))
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
if (typeof payload?.inline_diff === 'string' && payload.inline_diff.trim()) {
|
|
813
|
+
recordToolDiff(payload.tool_id || payload.name || '', payload.inline_diff)
|
|
814
|
+
}
|
|
815
|
+
} else if (SUBAGENT_EVENT_TYPES.has(event.type)) {
|
|
816
|
+
if (sessionId && payload) {
|
|
817
|
+
if (!nativeSubagentSessionsRef.current.has(sessionId)) {
|
|
818
|
+
pruneDelegateFallbackSubagents(sessionId)
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
nativeSubagentSessionsRef.current.add(sessionId)
|
|
822
|
+
upsertSubagent(
|
|
823
|
+
sessionId,
|
|
824
|
+
payload as Record<string, unknown>,
|
|
825
|
+
event.type === 'subagent.spawn_requested' || event.type === 'subagent.start',
|
|
826
|
+
event.type
|
|
827
|
+
)
|
|
828
|
+
}
|
|
829
|
+
} else if (event.type === 'clarify.request') {
|
|
830
|
+
// Surface the clarify tool's overlay. The Python side is blocked on
|
|
831
|
+
// `clarify.respond`, so without this handler the agent would hang
|
|
832
|
+
// forever (see tools/clarify_tool.py + tui_gateway/server.py:_block).
|
|
833
|
+
//
|
|
834
|
+
// Store the request for whichever session raised it — even a background
|
|
835
|
+
// one. clarify.request is a one-shot event; if we dropped it for an
|
|
836
|
+
// unfocused session, that session would block on `clarify.respond`
|
|
837
|
+
// indefinitely and re-focusing it could never recover (the event is
|
|
838
|
+
// gone). Parking it per-session lets the user answer once they switch
|
|
839
|
+
// over; the inline ClarifyTool reads the active session's entry.
|
|
840
|
+
const requestId = typeof payload?.request_id === 'string' ? payload.request_id : ''
|
|
841
|
+
const question = typeof payload?.question === 'string' ? payload.question : ''
|
|
842
|
+
|
|
843
|
+
if (requestId && question) {
|
|
844
|
+
setClarifyRequest({
|
|
845
|
+
requestId,
|
|
846
|
+
question,
|
|
847
|
+
choices: Array.isArray(payload?.choices) ? payload!.choices!.filter(c => typeof c === 'string') : null,
|
|
848
|
+
sessionId: sessionId ?? null
|
|
849
|
+
})
|
|
850
|
+
|
|
851
|
+
// The transcript only renders the active session, so a background
|
|
852
|
+
// clarify is otherwise invisible (the row just keeps spinning like
|
|
853
|
+
// it's working). Flag the session so the sidebar shows a persistent
|
|
854
|
+
// "needs input" indicator on its row — works for the active session
|
|
855
|
+
// too, and survives alt-tab / window blur (unlike a toast).
|
|
856
|
+
if (sessionId) {
|
|
857
|
+
updateSessionState(sessionId, state => ({ ...state, needsInput: true }))
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
} else if (event.type === 'approval.request') {
|
|
861
|
+
// Dangerous-command / execute_code approval. The Python side is blocked
|
|
862
|
+
// in _await_gateway_decision() until approval.respond lands; without
|
|
863
|
+
// this the agent stalls until its 5-min timeout and the tool is BLOCKED.
|
|
864
|
+
// Park it per-session (like clarify) so a *background* profile's turn can
|
|
865
|
+
// raise it and wait — the sidebar flags "needs input" and the inline bar
|
|
866
|
+
// surfaces once the user focuses that chat.
|
|
867
|
+
setApprovalRequest({
|
|
868
|
+
command: typeof payload?.command === 'string' ? payload.command : '',
|
|
869
|
+
description: typeof payload?.description === 'string' ? payload.description : 'dangerous command',
|
|
870
|
+
sessionId: sessionId ?? null
|
|
871
|
+
})
|
|
872
|
+
|
|
873
|
+
if (sessionId) {
|
|
874
|
+
updateSessionState(sessionId, state => ({ ...state, needsInput: true }))
|
|
875
|
+
}
|
|
876
|
+
} else if (event.type === 'sudo.request') {
|
|
877
|
+
// Sudo password capture (tools/terminal_tool.py). Blocked on
|
|
878
|
+
// sudo.respond {request_id, password}.
|
|
879
|
+
const requestId = typeof payload?.request_id === 'string' ? payload.request_id : ''
|
|
880
|
+
|
|
881
|
+
if (requestId) {
|
|
882
|
+
setSudoRequest({ requestId, sessionId: sessionId ?? null })
|
|
883
|
+
|
|
884
|
+
if (sessionId) {
|
|
885
|
+
updateSessionState(sessionId, state => ({ ...state, needsInput: true }))
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
} else if (event.type === 'secret.request') {
|
|
889
|
+
// Skill credential capture (tools/skills_tool.py). Blocked on
|
|
890
|
+
// secret.respond {request_id, value}.
|
|
891
|
+
const requestId = typeof payload?.request_id === 'string' ? payload.request_id : ''
|
|
892
|
+
|
|
893
|
+
if (requestId) {
|
|
894
|
+
setSecretRequest({
|
|
895
|
+
requestId,
|
|
896
|
+
envVar: typeof payload?.env_var === 'string' ? payload.env_var : '',
|
|
897
|
+
prompt: typeof payload?.prompt === 'string' ? payload.prompt : '',
|
|
898
|
+
sessionId: sessionId ?? null
|
|
899
|
+
})
|
|
900
|
+
|
|
901
|
+
if (sessionId) {
|
|
902
|
+
updateSessionState(sessionId, state => ({ ...state, needsInput: true }))
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
} else if (event.type === 'error') {
|
|
906
|
+
const errorMessage = payload?.message || 'NasTech reported an error'
|
|
907
|
+
const looksLikeProviderSetup = isProviderSetupErrorMessage(errorMessage)
|
|
908
|
+
|
|
909
|
+
// A turn that errors out has also ended — drop any open blocking prompt
|
|
910
|
+
// for this session so an approval/sudo/secret overlay can't linger past
|
|
911
|
+
// the failed turn (same intent as the message.complete clear).
|
|
912
|
+
if (sessionId) {
|
|
913
|
+
clearAllPrompts(sessionId)
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
if (looksLikeProviderSetup) {
|
|
917
|
+
requestDesktopOnboarding(errorMessage)
|
|
918
|
+
} else if (isActiveEvent) {
|
|
919
|
+
notify({
|
|
920
|
+
kind: 'error',
|
|
921
|
+
title: 'NasTech error',
|
|
922
|
+
message: errorMessage
|
|
923
|
+
})
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
if (sessionId) {
|
|
927
|
+
flushQueuedDeltas(sessionId)
|
|
928
|
+
failAssistantMessage(sessionId, errorMessage)
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
if (isActiveEvent) {
|
|
932
|
+
setTurnStartedAt(null)
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
},
|
|
936
|
+
[
|
|
937
|
+
appendAssistantDelta,
|
|
938
|
+
appendReasoningDelta,
|
|
939
|
+
activeSessionIdRef,
|
|
940
|
+
completeAssistantMessage,
|
|
941
|
+
failAssistantMessage,
|
|
942
|
+
flushQueuedDeltas,
|
|
943
|
+
queryClient,
|
|
944
|
+
refreshNasTechConfig,
|
|
945
|
+
updateSessionState,
|
|
946
|
+
upsertToolCall
|
|
947
|
+
]
|
|
948
|
+
)
|
|
949
|
+
|
|
950
|
+
return {
|
|
951
|
+
appendAssistantDelta,
|
|
952
|
+
appendReasoningDelta,
|
|
953
|
+
completeAssistantMessage,
|
|
954
|
+
handleGatewayEvent,
|
|
955
|
+
upsertToolCall
|
|
956
|
+
}
|
|
957
|
+
}
|