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,1030 @@
|
|
|
1
|
+
import type { AppendMessage, ThreadMessage } from '@assistant-ui/react'
|
|
2
|
+
import { type MutableRefObject, useCallback } from 'react'
|
|
3
|
+
|
|
4
|
+
import { getProfiles, transcribeAudio } from '@/nastech'
|
|
5
|
+
import { translateNow, type Translations, useI18n } from '@/i18n'
|
|
6
|
+
import { branchGroupForUser, type ChatMessage, chatMessageText, textPart } from '@/lib/chat-messages'
|
|
7
|
+
import {
|
|
8
|
+
attachmentDisplayText,
|
|
9
|
+
parseCommandDispatch,
|
|
10
|
+
parseSlashCommand,
|
|
11
|
+
pathLabel,
|
|
12
|
+
SLASH_COMMAND_RE
|
|
13
|
+
} from '@/lib/chat-runtime'
|
|
14
|
+
import {
|
|
15
|
+
type CommandsCatalogLike,
|
|
16
|
+
desktopSlashUnavailableMessage,
|
|
17
|
+
filterDesktopCommandsCatalog,
|
|
18
|
+
isDesktopSlashCommand,
|
|
19
|
+
isModelPickerCommand
|
|
20
|
+
} from '@/lib/desktop-slash-commands'
|
|
21
|
+
import { triggerHaptic } from '@/lib/haptics'
|
|
22
|
+
import { setMutableRef } from '@/lib/mutable-ref'
|
|
23
|
+
import { isProviderSetupErrorMessage } from '@/lib/provider-setup-errors'
|
|
24
|
+
import { setSessionYolo } from '@/lib/yolo-session'
|
|
25
|
+
import {
|
|
26
|
+
$composerAttachments,
|
|
27
|
+
addComposerAttachment,
|
|
28
|
+
clearComposerAttachments,
|
|
29
|
+
type ComposerAttachment,
|
|
30
|
+
terminalContextBlocksFromDraft
|
|
31
|
+
} from '@/store/composer'
|
|
32
|
+
import { clearNotifications, notify, notifyError } from '@/store/notifications'
|
|
33
|
+
import { requestDesktopOnboarding } from '@/store/onboarding'
|
|
34
|
+
import { $activeGatewayProfile, $newChatProfile, ensureGatewayProfile, normalizeProfileKey } from '@/store/profile'
|
|
35
|
+
import {
|
|
36
|
+
$busy,
|
|
37
|
+
$messages,
|
|
38
|
+
$yoloActive,
|
|
39
|
+
setAwaitingResponse,
|
|
40
|
+
setBusy,
|
|
41
|
+
setMessages,
|
|
42
|
+
setModelPickerOpen,
|
|
43
|
+
setSessions,
|
|
44
|
+
setYoloActive
|
|
45
|
+
} from '@/store/session'
|
|
46
|
+
|
|
47
|
+
import type {
|
|
48
|
+
ClientSessionState,
|
|
49
|
+
ImageAttachResponse,
|
|
50
|
+
SessionSteerResponse,
|
|
51
|
+
SessionTitleResponse,
|
|
52
|
+
SlashExecResponse
|
|
53
|
+
} from '../../types'
|
|
54
|
+
|
|
55
|
+
function blobToDataUrl(blob: Blob): Promise<string> {
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
const reader = new FileReader()
|
|
58
|
+
|
|
59
|
+
reader.addEventListener('load', () => {
|
|
60
|
+
if (typeof reader.result === 'string') {
|
|
61
|
+
resolve(reader.result)
|
|
62
|
+
} else {
|
|
63
|
+
reject(new Error(translateNow('desktop.audioReadFailed')))
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
reader.addEventListener('error', () => reject(reader.error || new Error(translateNow('desktop.audioReadFailed'))))
|
|
67
|
+
reader.readAsDataURL(blob)
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isProviderSetupError(error: unknown) {
|
|
72
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
73
|
+
|
|
74
|
+
return isProviderSetupErrorMessage(message)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function inlineErrorMessage(error: unknown, fallback: string): string {
|
|
78
|
+
const raw = error instanceof Error ? error.message : typeof error === 'string' ? error : fallback
|
|
79
|
+
|
|
80
|
+
return (raw.match(/Error invoking remote method '[^']+': Error: (.+)$/)?.[1] ?? raw).replace(/^Error:\s*/, '').trim()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface PromptActionsOptions {
|
|
84
|
+
activeSessionId: string | null
|
|
85
|
+
activeSessionIdRef: MutableRefObject<string | null>
|
|
86
|
+
busyRef: MutableRefObject<boolean>
|
|
87
|
+
branchCurrentSession: () => Promise<boolean>
|
|
88
|
+
createBackendSessionForSend: (preview?: string | null) => Promise<string | null>
|
|
89
|
+
handleSkinCommand: (arg: string) => string
|
|
90
|
+
refreshSessions: () => Promise<void>
|
|
91
|
+
requestGateway: <T>(method: string, params?: Record<string, unknown>) => Promise<T>
|
|
92
|
+
selectedStoredSessionIdRef: MutableRefObject<string | null>
|
|
93
|
+
startFreshSessionDraft: () => void
|
|
94
|
+
sttEnabled: boolean
|
|
95
|
+
updateSessionState: (
|
|
96
|
+
sessionId: string,
|
|
97
|
+
updater: (state: ClientSessionState) => ClientSessionState,
|
|
98
|
+
storedSessionId?: string | null
|
|
99
|
+
) => ClientSessionState
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface SubmitTextOptions {
|
|
103
|
+
attachments?: ComposerAttachment[]
|
|
104
|
+
fromQueue?: boolean
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function renderCommandsCatalog(catalog: CommandsCatalogLike, copy: Translations['desktop']): string {
|
|
108
|
+
const desktopCatalog = filterDesktopCommandsCatalog(catalog)
|
|
109
|
+
|
|
110
|
+
const sections = desktopCatalog.categories?.length
|
|
111
|
+
? desktopCatalog.categories
|
|
112
|
+
: [{ name: copy.desktopCommands, pairs: desktopCatalog.pairs ?? [] }]
|
|
113
|
+
|
|
114
|
+
const body = sections
|
|
115
|
+
.filter(section => section.pairs.length > 0)
|
|
116
|
+
.map(section => {
|
|
117
|
+
const rows = section.pairs.map(([cmd, desc]) => `${cmd.padEnd(18)} ${desc}`)
|
|
118
|
+
|
|
119
|
+
return [`${section.name}:`, ...rows].join('\n')
|
|
120
|
+
})
|
|
121
|
+
.join('\n\n')
|
|
122
|
+
|
|
123
|
+
const tail = [
|
|
124
|
+
desktopCatalog.skill_count ? copy.skillCommandsAvailable(desktopCatalog.skill_count) : '',
|
|
125
|
+
desktopCatalog.warning ? copy.warningLine(desktopCatalog.warning) : ''
|
|
126
|
+
]
|
|
127
|
+
.filter(Boolean)
|
|
128
|
+
.join('\n')
|
|
129
|
+
|
|
130
|
+
return [body || 'No desktop commands available.', tail].filter(Boolean).join('\n\n')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function slashStatusText(command: string, output: string): string {
|
|
134
|
+
return [`slash:${command}`, output.trim()].filter(Boolean).join('\n')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function appendText(message: AppendMessage): string {
|
|
138
|
+
return message.content
|
|
139
|
+
.map(part => ('text' in part ? part.text : ''))
|
|
140
|
+
.join('')
|
|
141
|
+
.trim()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function visibleUserOrdinal(messages: readonly ChatMessage[], end: number): number {
|
|
145
|
+
return messages.slice(0, end).filter(m => m.role === 'user' && !m.hidden).length
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function usePromptActions({
|
|
149
|
+
activeSessionId,
|
|
150
|
+
activeSessionIdRef,
|
|
151
|
+
busyRef,
|
|
152
|
+
branchCurrentSession,
|
|
153
|
+
createBackendSessionForSend,
|
|
154
|
+
handleSkinCommand,
|
|
155
|
+
refreshSessions,
|
|
156
|
+
requestGateway,
|
|
157
|
+
selectedStoredSessionIdRef,
|
|
158
|
+
startFreshSessionDraft,
|
|
159
|
+
sttEnabled,
|
|
160
|
+
updateSessionState
|
|
161
|
+
}: PromptActionsOptions) {
|
|
162
|
+
const { t } = useI18n()
|
|
163
|
+
const copy = t.desktop
|
|
164
|
+
|
|
165
|
+
const appendSessionTextMessage = useCallback(
|
|
166
|
+
(sessionId: string, role: ChatMessage['role'], text: string) => {
|
|
167
|
+
const body = text.trim()
|
|
168
|
+
|
|
169
|
+
if (!body) {
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
updateSessionState(
|
|
174
|
+
sessionId,
|
|
175
|
+
state => ({
|
|
176
|
+
...state,
|
|
177
|
+
messages: [
|
|
178
|
+
...state.messages,
|
|
179
|
+
{
|
|
180
|
+
id: `${role}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
181
|
+
role,
|
|
182
|
+
parts: [textPart(body)]
|
|
183
|
+
}
|
|
184
|
+
]
|
|
185
|
+
}),
|
|
186
|
+
selectedStoredSessionIdRef.current
|
|
187
|
+
)
|
|
188
|
+
},
|
|
189
|
+
[selectedStoredSessionIdRef, updateSessionState]
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
const syncImageAttachmentsForSubmit = useCallback(
|
|
193
|
+
async (
|
|
194
|
+
sessionId: string,
|
|
195
|
+
attachments: ComposerAttachment[],
|
|
196
|
+
options: { updateComposerAttachments?: boolean } = {}
|
|
197
|
+
) => {
|
|
198
|
+
const updateComposerAttachments = options.updateComposerAttachments ?? true
|
|
199
|
+
const images = attachments.filter(attachment => attachment.kind === 'image' && attachment.path)
|
|
200
|
+
|
|
201
|
+
for (const attachment of images) {
|
|
202
|
+
if (attachment.attachedSessionId === sessionId) {
|
|
203
|
+
continue
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const result = await requestGateway<ImageAttachResponse>('image.attach', {
|
|
207
|
+
session_id: sessionId,
|
|
208
|
+
path: attachment.path
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
if (!result.attached) {
|
|
212
|
+
const label = attachment.label || (attachment.path ? pathLabel(attachment.path) : 'image')
|
|
213
|
+
throw new Error(result.message || `Could not attach ${label}`)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const attachedPath = result.path || attachment.path
|
|
217
|
+
|
|
218
|
+
if (updateComposerAttachments) {
|
|
219
|
+
addComposerAttachment({
|
|
220
|
+
...attachment,
|
|
221
|
+
id: attachment.id,
|
|
222
|
+
label: attachedPath ? pathLabel(attachedPath) : attachment.label,
|
|
223
|
+
path: attachedPath,
|
|
224
|
+
attachedSessionId: sessionId
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
[requestGateway]
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
const submitPromptText = useCallback(
|
|
233
|
+
async (rawText: string, options?: SubmitTextOptions) => {
|
|
234
|
+
const visibleText = rawText.trim()
|
|
235
|
+
const usingComposerAttachments = !options?.attachments
|
|
236
|
+
const attachments = options?.attachments ?? $composerAttachments.get()
|
|
237
|
+
|
|
238
|
+
const contextRefs = attachments
|
|
239
|
+
.map(a => a.refText)
|
|
240
|
+
.filter(Boolean)
|
|
241
|
+
.join('\n')
|
|
242
|
+
|
|
243
|
+
const terminalContextBlocks = terminalContextBlocksFromDraft(rawText).join('\n\n')
|
|
244
|
+
const hasImage = attachments.some(a => a.kind === 'image')
|
|
245
|
+
const attachmentRefs = attachments.map(attachmentDisplayText).filter((r): r is string => Boolean(r))
|
|
246
|
+
|
|
247
|
+
const text =
|
|
248
|
+
[contextRefs, terminalContextBlocks, visibleText].filter(Boolean).join('\n\n') ||
|
|
249
|
+
(hasImage ? 'What do you see in this image?' : '')
|
|
250
|
+
|
|
251
|
+
// Queue drains fire on the busy→false settle edge, where busyRef (synced
|
|
252
|
+
// from $busy by a separate effect) may still read true — honoring it would
|
|
253
|
+
// bounce the drained send. The drain lock serializes them; the user path
|
|
254
|
+
// keeps the guard so a stray Enter mid-turn can't double-submit.
|
|
255
|
+
if (!text || (!options?.fromQueue && busyRef.current)) {
|
|
256
|
+
return false
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const optimisticId = `user-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
260
|
+
|
|
261
|
+
const userMessage: ChatMessage = {
|
|
262
|
+
id: optimisticId,
|
|
263
|
+
role: 'user',
|
|
264
|
+
parts: [textPart(visibleText || (attachmentRefs.length ? '' : attachments.map(a => a.label).join(', ')))],
|
|
265
|
+
attachmentRefs
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const releaseBusy = () => {
|
|
269
|
+
setMutableRef(busyRef, false)
|
|
270
|
+
setBusy(false)
|
|
271
|
+
setAwaitingResponse(false)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Idempotent optimistic insert — re-running with the resolved sessionId
|
|
275
|
+
// after createBackendSessionForSend just overwrites with the same id.
|
|
276
|
+
const seedOptimistic = (sid: string) =>
|
|
277
|
+
updateSessionState(
|
|
278
|
+
sid,
|
|
279
|
+
state => ({
|
|
280
|
+
...state,
|
|
281
|
+
messages: state.messages.some(m => m.id === optimisticId)
|
|
282
|
+
? state.messages
|
|
283
|
+
: [...state.messages, userMessage],
|
|
284
|
+
busy: true,
|
|
285
|
+
awaitingResponse: true,
|
|
286
|
+
pendingBranchGroup: null,
|
|
287
|
+
sawAssistantPayload: false,
|
|
288
|
+
// Fresh submit = new turn — clear any leftover interrupt flag, else
|
|
289
|
+
// mutateStream/completeAssistantMessage drop every delta of this turn
|
|
290
|
+
// (what made drained-after-interrupt sends go silent).
|
|
291
|
+
interrupted: false
|
|
292
|
+
}),
|
|
293
|
+
selectedStoredSessionIdRef.current
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
const dropOptimistic = (sid: null | string) => {
|
|
297
|
+
if (!sid) {
|
|
298
|
+
setMessages(current => current.filter(m => m.id !== optimisticId))
|
|
299
|
+
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
updateSessionState(
|
|
304
|
+
sid,
|
|
305
|
+
state => ({
|
|
306
|
+
...state,
|
|
307
|
+
messages: state.messages.filter(m => m.id !== optimisticId),
|
|
308
|
+
busy: false,
|
|
309
|
+
awaitingResponse: false,
|
|
310
|
+
pendingBranchGroup: null
|
|
311
|
+
}),
|
|
312
|
+
selectedStoredSessionIdRef.current
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
setMutableRef(busyRef, true)
|
|
317
|
+
setBusy(true)
|
|
318
|
+
setAwaitingResponse(true)
|
|
319
|
+
clearNotifications()
|
|
320
|
+
|
|
321
|
+
let sessionId: null | string = activeSessionId
|
|
322
|
+
|
|
323
|
+
if (sessionId) {
|
|
324
|
+
seedOptimistic(sessionId)
|
|
325
|
+
} else {
|
|
326
|
+
setMessages(current => [...current, userMessage])
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (!sessionId) {
|
|
330
|
+
try {
|
|
331
|
+
sessionId = await createBackendSessionForSend(visibleText)
|
|
332
|
+
} catch (err) {
|
|
333
|
+
dropOptimistic(null)
|
|
334
|
+
releaseBusy()
|
|
335
|
+
notifyError(err, copy.sessionUnavailable)
|
|
336
|
+
|
|
337
|
+
return false
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (!sessionId) {
|
|
341
|
+
dropOptimistic(null)
|
|
342
|
+
releaseBusy()
|
|
343
|
+
notify({ kind: 'error', title: copy.sessionUnavailable, message: copy.createSessionFailed })
|
|
344
|
+
|
|
345
|
+
return false
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
seedOptimistic(sessionId)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
await syncImageAttachmentsForSubmit(sessionId, attachments, {
|
|
353
|
+
updateComposerAttachments: usingComposerAttachments
|
|
354
|
+
})
|
|
355
|
+
await requestGateway('prompt.submit', { session_id: sessionId, text })
|
|
356
|
+
|
|
357
|
+
if (usingComposerAttachments) {
|
|
358
|
+
clearComposerAttachments()
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return true
|
|
362
|
+
} catch (err) {
|
|
363
|
+
const message = inlineErrorMessage(err, copy.promptFailed)
|
|
364
|
+
|
|
365
|
+
releaseBusy()
|
|
366
|
+
updateSessionState(sessionId, state => ({
|
|
367
|
+
...state,
|
|
368
|
+
messages: [
|
|
369
|
+
...state.messages,
|
|
370
|
+
{
|
|
371
|
+
id: `assistant-error-${Date.now()}`,
|
|
372
|
+
role: 'assistant',
|
|
373
|
+
parts: [],
|
|
374
|
+
error: message || copy.promptFailed,
|
|
375
|
+
branchGroupId: state.pendingBranchGroup ?? undefined
|
|
376
|
+
}
|
|
377
|
+
],
|
|
378
|
+
busy: false,
|
|
379
|
+
awaitingResponse: false,
|
|
380
|
+
pendingBranchGroup: null,
|
|
381
|
+
sawAssistantPayload: true
|
|
382
|
+
}))
|
|
383
|
+
|
|
384
|
+
if (isProviderSetupError(err)) {
|
|
385
|
+
requestDesktopOnboarding(copy.providerCredentialRequired)
|
|
386
|
+
|
|
387
|
+
return false
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
notifyError(err, copy.promptFailed)
|
|
391
|
+
|
|
392
|
+
return false
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
[
|
|
396
|
+
activeSessionId,
|
|
397
|
+
busyRef,
|
|
398
|
+
copy,
|
|
399
|
+
createBackendSessionForSend,
|
|
400
|
+
requestGateway,
|
|
401
|
+
selectedStoredSessionIdRef,
|
|
402
|
+
syncImageAttachmentsForSubmit,
|
|
403
|
+
updateSessionState
|
|
404
|
+
]
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
const executeSlashCommand = useCallback(
|
|
408
|
+
async (rawCommand: string, options?: { sessionId?: string; recordInput?: boolean }) => {
|
|
409
|
+
const runSlash = async (commandText: string, sessionHint?: string, recordInput = true): Promise<void> => {
|
|
410
|
+
const command = commandText.trim()
|
|
411
|
+
const { name, arg } = parseSlashCommand(command)
|
|
412
|
+
const normalizedName = name.toLowerCase()
|
|
413
|
+
|
|
414
|
+
if (!name) {
|
|
415
|
+
const sessionId = sessionHint || activeSessionIdRef.current || (await createBackendSessionForSend())
|
|
416
|
+
|
|
417
|
+
if (sessionId) {
|
|
418
|
+
appendSessionTextMessage(sessionId, 'system', copy.emptySlashCommand)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (normalizedName === 'new' || normalizedName === 'reset') {
|
|
425
|
+
startFreshSessionDraft()
|
|
426
|
+
|
|
427
|
+
return
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (normalizedName === 'branch' || normalizedName === 'fork') {
|
|
431
|
+
await branchCurrentSession()
|
|
432
|
+
|
|
433
|
+
return
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// /yolo maps to the status-bar YOLO control — a per-session approval
|
|
437
|
+
// bypass, same scope as the TUI's Shift+Tab. With no session yet we arm
|
|
438
|
+
// it locally; the session-create path applies it on the first message.
|
|
439
|
+
if (normalizedName === 'yolo') {
|
|
440
|
+
const sid = sessionHint || activeSessionIdRef.current
|
|
441
|
+
const next = !$yoloActive.get()
|
|
442
|
+
|
|
443
|
+
if (!sid) {
|
|
444
|
+
setYoloActive(next)
|
|
445
|
+
notify({ kind: 'success', message: next ? copy.yoloArmed : copy.yoloOff })
|
|
446
|
+
|
|
447
|
+
return
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
try {
|
|
451
|
+
const active = await setSessionYolo(requestGateway, sid, next)
|
|
452
|
+
appendSessionTextMessage(sid, 'system', copy.yoloSystem(active))
|
|
453
|
+
} catch {
|
|
454
|
+
notify({ kind: 'error', title: copy.yoloTitle, message: copy.yoloToggleFailed })
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// /model opens the desktop model picker overlay — the same full
|
|
461
|
+
// provider+model picker reachable from the status-bar model button —
|
|
462
|
+
// instead of the headless prompt_toolkit modal the slash worker can't
|
|
463
|
+
// render. With explicit args (`/model <name> [--provider ...]`) run the
|
|
464
|
+
// switch directly through slash.exec so power users can still type it.
|
|
465
|
+
if (isModelPickerCommand(`/${normalizedName}`)) {
|
|
466
|
+
if (!arg.trim()) {
|
|
467
|
+
setModelPickerOpen(true)
|
|
468
|
+
|
|
469
|
+
return
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const sid = sessionHint || activeSessionIdRef.current || (await createBackendSessionForSend())
|
|
473
|
+
|
|
474
|
+
if (!sid) {
|
|
475
|
+
notify({ kind: 'error', title: 'Session unavailable', message: 'Could not create a new session' })
|
|
476
|
+
|
|
477
|
+
return
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
try {
|
|
481
|
+
const result = await requestGateway<SlashExecResponse>('slash.exec', {
|
|
482
|
+
session_id: sid,
|
|
483
|
+
command: command.replace(/^\/+/, '')
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
const body = result?.output || `/${name}: model switched`
|
|
487
|
+
appendSessionTextMessage(
|
|
488
|
+
sid,
|
|
489
|
+
'system',
|
|
490
|
+
recordInput ? slashStatusText(command, body) : body
|
|
491
|
+
)
|
|
492
|
+
} catch (err) {
|
|
493
|
+
appendSessionTextMessage(
|
|
494
|
+
sid,
|
|
495
|
+
'system',
|
|
496
|
+
`error: ${err instanceof Error ? err.message : String(err)}`
|
|
497
|
+
)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (normalizedName === 'skin' && !sessionHint && !activeSessionIdRef.current) {
|
|
504
|
+
notify({ kind: 'success', message: handleSkinCommand(arg) })
|
|
505
|
+
|
|
506
|
+
return
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// /profile selects which profile new chats open in — no app relaunch.
|
|
510
|
+
// A profile is per-session now, so an existing thread can't change its
|
|
511
|
+
// profile mid-stream; `/profile <name>` instead points the next new chat
|
|
512
|
+
// (and the current empty draft) at that profile's backend.
|
|
513
|
+
if (normalizedName === 'profile') {
|
|
514
|
+
const target = arg.trim()
|
|
515
|
+
const current = normalizeProfileKey($activeGatewayProfile.get())
|
|
516
|
+
|
|
517
|
+
if (!target) {
|
|
518
|
+
notify({
|
|
519
|
+
kind: 'success',
|
|
520
|
+
message: copy.profileStatus(current)
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
return
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
const { profiles } = await getProfiles()
|
|
528
|
+
const match = profiles.find(profile => profile.name === target)
|
|
529
|
+
|
|
530
|
+
if (!match) {
|
|
531
|
+
notify({
|
|
532
|
+
kind: 'error',
|
|
533
|
+
title: copy.unknownProfile,
|
|
534
|
+
message: copy.noProfileNamed(target, profiles.map(profile => profile.name).join(', '))
|
|
535
|
+
})
|
|
536
|
+
|
|
537
|
+
return
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const key = normalizeProfileKey(match.name)
|
|
541
|
+
|
|
542
|
+
$newChatProfile.set(key)
|
|
543
|
+
// Swap the live gateway now so an empty draft sends into this
|
|
544
|
+
// profile immediately; an existing thread keeps its own profile.
|
|
545
|
+
await ensureGatewayProfile(key)
|
|
546
|
+
notify({ kind: 'success', message: copy.newChatsProfile(match.name) })
|
|
547
|
+
} catch (err) {
|
|
548
|
+
notifyError(err, copy.setProfileFailed)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const sessionId = sessionHint || activeSessionIdRef.current || (await createBackendSessionForSend())
|
|
555
|
+
|
|
556
|
+
if (!sessionId) {
|
|
557
|
+
notify({
|
|
558
|
+
kind: 'error',
|
|
559
|
+
title: copy.sessionUnavailable,
|
|
560
|
+
message: copy.createSessionFailed
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
return
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const renderSlashOutput = (text: string) =>
|
|
567
|
+
appendSessionTextMessage(sessionId, 'system', recordInput ? slashStatusText(command, text) : text)
|
|
568
|
+
|
|
569
|
+
// /title <name> renames the session. Route through the gateway's
|
|
570
|
+
// `session.title` RPC — the same path the TUI uses — NOT the REST
|
|
571
|
+
// renameSession endpoint and NOT the slash worker.
|
|
572
|
+
//
|
|
573
|
+
// Why not the slash worker: it's a separate NasTechCLI subprocess whose
|
|
574
|
+
// SQLite write to the shared state.db can silently fail (notably on
|
|
575
|
+
// Windows), and it never refreshes the sidebar.
|
|
576
|
+
//
|
|
577
|
+
// Why not REST renameSession: `sessionId` here is the *runtime* session
|
|
578
|
+
// id returned by session.create — it is NOT the stored DB `sessions.id`,
|
|
579
|
+
// and session.create deliberately does not persist a DB row until the
|
|
580
|
+
// first turn. The REST PATCH endpoint resolves against the sessions
|
|
581
|
+
// table, so a runtime id (or a brand-new, not-yet-persisted session)
|
|
582
|
+
// 404s with "Session not found" on every platform. See #38508 / #38576.
|
|
583
|
+
//
|
|
584
|
+
// session.title maps the runtime id to the in-memory session, writes
|
|
585
|
+
// through the gateway's own DB connection, and QUEUES the title
|
|
586
|
+
// (`pending: true`) when the row isn't persisted yet — so it works for a
|
|
587
|
+
// fresh chat too. refreshSessions() then pulls the authoritative title
|
|
588
|
+
// back into the sidebar. A bare `/title` (no arg) still falls through to
|
|
589
|
+
// the worker to display the current title.
|
|
590
|
+
if (normalizedName === 'title' && arg) {
|
|
591
|
+
try {
|
|
592
|
+
const result = await requestGateway<SessionTitleResponse>('session.title', {
|
|
593
|
+
session_id: sessionId,
|
|
594
|
+
title: arg
|
|
595
|
+
})
|
|
596
|
+
|
|
597
|
+
const finalTitle = (result?.title || arg).trim()
|
|
598
|
+
const queued = result?.pending === true
|
|
599
|
+
|
|
600
|
+
setSessions(prev => prev.map(s => (s.id === sessionId ? { ...s, title: finalTitle || null } : s)))
|
|
601
|
+
await refreshSessions().catch(() => undefined)
|
|
602
|
+
renderSlashOutput(
|
|
603
|
+
finalTitle
|
|
604
|
+
? `Session title set: ${finalTitle}${queued ? ' (queued while session initializes)' : ''}`
|
|
605
|
+
: 'Session title cleared.'
|
|
606
|
+
)
|
|
607
|
+
} catch (err) {
|
|
608
|
+
renderSlashOutput(`error: ${err instanceof Error ? err.message : String(err)}`)
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (normalizedName === 'skin') {
|
|
615
|
+
renderSlashOutput(handleSkinCommand(arg))
|
|
616
|
+
|
|
617
|
+
return
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (name === 'help' || name === 'commands') {
|
|
621
|
+
try {
|
|
622
|
+
const catalog = await requestGateway<CommandsCatalogLike>('commands.catalog', { session_id: sessionId })
|
|
623
|
+
|
|
624
|
+
renderSlashOutput(renderCommandsCatalog(catalog, copy))
|
|
625
|
+
} catch (err) {
|
|
626
|
+
renderSlashOutput(`error: ${err instanceof Error ? err.message : String(err)}`)
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (!isDesktopSlashCommand(name)) {
|
|
633
|
+
renderSlashOutput(desktopSlashUnavailableMessage(name) || `/${name} is not available in the desktop app.`)
|
|
634
|
+
|
|
635
|
+
return
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
try {
|
|
639
|
+
const result = await requestGateway<SlashExecResponse>('slash.exec', {
|
|
640
|
+
session_id: sessionId,
|
|
641
|
+
command: command.replace(/^\/+/, '')
|
|
642
|
+
})
|
|
643
|
+
|
|
644
|
+
const body = result?.output || `/${name}: no output`
|
|
645
|
+
renderSlashOutput(result?.warning ? `warning: ${result.warning}\n${body}` : body)
|
|
646
|
+
|
|
647
|
+
return
|
|
648
|
+
} catch {
|
|
649
|
+
// Fall back to command.dispatch for skill/send/alias directives.
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
try {
|
|
653
|
+
const dispatch = parseCommandDispatch(
|
|
654
|
+
await requestGateway<unknown>('command.dispatch', {
|
|
655
|
+
session_id: sessionId,
|
|
656
|
+
name,
|
|
657
|
+
arg
|
|
658
|
+
})
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
if (!dispatch) {
|
|
662
|
+
renderSlashOutput('error: invalid response: command.dispatch')
|
|
663
|
+
|
|
664
|
+
return
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (dispatch.type === 'exec' || dispatch.type === 'plugin') {
|
|
668
|
+
renderSlashOutput(dispatch.output ?? '(no output)')
|
|
669
|
+
|
|
670
|
+
return
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (dispatch.type === 'alias') {
|
|
674
|
+
await runSlash(`/${dispatch.target}${arg ? ` ${arg}` : ''}`, sessionId, false)
|
|
675
|
+
|
|
676
|
+
return
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const message = ('message' in dispatch ? dispatch.message : '')?.trim() ?? ''
|
|
680
|
+
|
|
681
|
+
if (!message) {
|
|
682
|
+
renderSlashOutput(
|
|
683
|
+
`/${name}: ${dispatch.type === 'skill' ? 'skill payload missing message' : 'empty message'}`
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
return
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (dispatch.type === 'skill') {
|
|
690
|
+
renderSlashOutput(`⚡ loading skill: ${dispatch.name}`)
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (busyRef.current) {
|
|
694
|
+
renderSlashOutput('session busy — /interrupt the current turn before sending this command')
|
|
695
|
+
|
|
696
|
+
return
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
await submitPromptText(message)
|
|
700
|
+
} catch (err) {
|
|
701
|
+
renderSlashOutput(`error: ${err instanceof Error ? err.message : String(err)}`)
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
await runSlash(rawCommand, options?.sessionId, options?.recordInput ?? true)
|
|
706
|
+
},
|
|
707
|
+
[
|
|
708
|
+
activeSessionIdRef,
|
|
709
|
+
appendSessionTextMessage,
|
|
710
|
+
branchCurrentSession,
|
|
711
|
+
busyRef,
|
|
712
|
+
copy,
|
|
713
|
+
createBackendSessionForSend,
|
|
714
|
+
handleSkinCommand,
|
|
715
|
+
refreshSessions,
|
|
716
|
+
requestGateway,
|
|
717
|
+
startFreshSessionDraft,
|
|
718
|
+
submitPromptText
|
|
719
|
+
]
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
const submitText = useCallback(
|
|
723
|
+
async (rawText: string, options?: SubmitTextOptions) => {
|
|
724
|
+
const visibleText = rawText.trim()
|
|
725
|
+
const attachments = options?.attachments ?? $composerAttachments.get()
|
|
726
|
+
|
|
727
|
+
if (!attachments.length && SLASH_COMMAND_RE.test(visibleText)) {
|
|
728
|
+
triggerHaptic('selection')
|
|
729
|
+
await executeSlashCommand(visibleText)
|
|
730
|
+
|
|
731
|
+
return true
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
return await submitPromptText(rawText, options)
|
|
735
|
+
},
|
|
736
|
+
[executeSlashCommand, submitPromptText]
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
const transcribeVoiceAudio = useCallback(
|
|
740
|
+
async (audio: Blob) => {
|
|
741
|
+
if (!sttEnabled) {
|
|
742
|
+
throw new Error(copy.sttDisabled)
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const dataUrl = await blobToDataUrl(audio)
|
|
746
|
+
const result = await transcribeAudio(dataUrl, audio.type)
|
|
747
|
+
|
|
748
|
+
return result.transcript
|
|
749
|
+
},
|
|
750
|
+
[copy.sttDisabled, sttEnabled]
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
const cancelRun = useCallback(async () => {
|
|
754
|
+
const sessionId = activeSessionId || activeSessionIdRef.current
|
|
755
|
+
|
|
756
|
+
setAwaitingResponse(false)
|
|
757
|
+
|
|
758
|
+
// Interrupting keeps whatever was already generated and just
|
|
759
|
+
// stops — no "[interrupted]" marker. A pending/streaming message with no
|
|
760
|
+
// body text is dropped entirely so we never leave an empty bubble behind.
|
|
761
|
+
const finalizeMessages = (messages: ChatMessage[], streamId?: string | null) =>
|
|
762
|
+
messages
|
|
763
|
+
.filter(
|
|
764
|
+
message =>
|
|
765
|
+
!((message.pending || message.id === streamId) && !chatMessageText(message).trim())
|
|
766
|
+
)
|
|
767
|
+
.map(message =>
|
|
768
|
+
message.pending || message.id === streamId ? { ...message, pending: false } : message
|
|
769
|
+
)
|
|
770
|
+
|
|
771
|
+
if (!sessionId) {
|
|
772
|
+
setMutableRef(busyRef, false)
|
|
773
|
+
setBusy(false)
|
|
774
|
+
setMessages(finalizeMessages($messages.get()))
|
|
775
|
+
|
|
776
|
+
return
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
updateSessionState(sessionId, state => {
|
|
780
|
+
const streamId = state.streamId
|
|
781
|
+
|
|
782
|
+
const messages = finalizeMessages(state.messages, streamId)
|
|
783
|
+
|
|
784
|
+
return {
|
|
785
|
+
...state,
|
|
786
|
+
messages,
|
|
787
|
+
busy: true,
|
|
788
|
+
awaitingResponse: false,
|
|
789
|
+
streamId: null,
|
|
790
|
+
pendingBranchGroup: null,
|
|
791
|
+
interrupted: true
|
|
792
|
+
}
|
|
793
|
+
})
|
|
794
|
+
|
|
795
|
+
try {
|
|
796
|
+
await requestGateway('session.interrupt', { session_id: sessionId })
|
|
797
|
+
} catch (err) {
|
|
798
|
+
setMutableRef(busyRef, false)
|
|
799
|
+
setBusy(false)
|
|
800
|
+
notifyError(err, copy.stopFailed)
|
|
801
|
+
}
|
|
802
|
+
}, [activeSessionId, activeSessionIdRef, busyRef, copy.stopFailed, requestGateway, updateSessionState])
|
|
803
|
+
|
|
804
|
+
// Steer = nudge the live turn without interrupting: the gateway appends the
|
|
805
|
+
// text to the next tool result so the model reads it on its next iteration
|
|
806
|
+
// (desktop parity with `/steer`). Returns false on reject (no live tool
|
|
807
|
+
// window) so the caller can fall back to queueing the words for the next turn.
|
|
808
|
+
const steerPrompt = useCallback(
|
|
809
|
+
async (rawText: string): Promise<boolean> => {
|
|
810
|
+
const text = rawText.trim()
|
|
811
|
+
const sessionId = activeSessionId || activeSessionIdRef.current
|
|
812
|
+
|
|
813
|
+
if (!text || !sessionId) {
|
|
814
|
+
return false
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
try {
|
|
818
|
+
const result = await requestGateway<SessionSteerResponse>('session.steer', { session_id: sessionId, text })
|
|
819
|
+
|
|
820
|
+
if (result?.status === 'queued') {
|
|
821
|
+
triggerHaptic('submit')
|
|
822
|
+
// Inline note (not a toast) so the nudge lives in the transcript next
|
|
823
|
+
// to the turn it steered. The `steer:` prefix is rendered as a codicon
|
|
824
|
+
// row by SystemMessage (see STEER_NOTE_RE), same style as slash output.
|
|
825
|
+
appendSessionTextMessage(sessionId, 'system', `steer:${text}`)
|
|
826
|
+
|
|
827
|
+
return true
|
|
828
|
+
}
|
|
829
|
+
} catch {
|
|
830
|
+
// Swallow — caller queues the text so nothing is lost.
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
return false
|
|
834
|
+
},
|
|
835
|
+
[activeSessionId, activeSessionIdRef, appendSessionTextMessage, requestGateway]
|
|
836
|
+
)
|
|
837
|
+
|
|
838
|
+
const reloadFromMessage = useCallback(
|
|
839
|
+
async (parentId: string | null) => {
|
|
840
|
+
if (!activeSessionId || $busy.get()) {
|
|
841
|
+
return
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const messages = $messages.get()
|
|
845
|
+
const parentIndex = parentId ? messages.findIndex(message => message.id === parentId) : messages.length - 1
|
|
846
|
+
|
|
847
|
+
const userIndex =
|
|
848
|
+
parentIndex >= 0
|
|
849
|
+
? [...messages.slice(0, parentIndex + 1)].reverse().findIndex(message => message.role === 'user')
|
|
850
|
+
: -1
|
|
851
|
+
|
|
852
|
+
if (userIndex < 0) {
|
|
853
|
+
return
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const absoluteUserIndex = parentIndex - userIndex
|
|
857
|
+
const userMessage = messages[absoluteUserIndex]
|
|
858
|
+
const userText = userMessage ? chatMessageText(userMessage).trim() : ''
|
|
859
|
+
|
|
860
|
+
if (!userText) {
|
|
861
|
+
return
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const targetAssistant =
|
|
865
|
+
parentId && messages[parentIndex]?.role === 'assistant'
|
|
866
|
+
? messages[parentIndex]
|
|
867
|
+
: messages.slice(absoluteUserIndex + 1).find(message => message.role === 'assistant')
|
|
868
|
+
|
|
869
|
+
const branchGroupId = targetAssistant?.branchGroupId ?? branchGroupForUser(userMessage)
|
|
870
|
+
const truncateBeforeUserOrdinal = visibleUserOrdinal(messages, absoluteUserIndex)
|
|
871
|
+
|
|
872
|
+
clearNotifications()
|
|
873
|
+
updateSessionState(activeSessionId, state => {
|
|
874
|
+
const nextUserIndex = state.messages.findIndex(
|
|
875
|
+
(message, index) => index > absoluteUserIndex && message.role === 'user'
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
const end = nextUserIndex < 0 ? state.messages.length : nextUserIndex
|
|
879
|
+
|
|
880
|
+
return {
|
|
881
|
+
...state,
|
|
882
|
+
busy: true,
|
|
883
|
+
awaitingResponse: true,
|
|
884
|
+
pendingBranchGroup: branchGroupId,
|
|
885
|
+
sawAssistantPayload: false,
|
|
886
|
+
interrupted: false,
|
|
887
|
+
messages: [
|
|
888
|
+
...state.messages.slice(0, absoluteUserIndex + 1),
|
|
889
|
+
...state.messages
|
|
890
|
+
.slice(absoluteUserIndex + 1, end)
|
|
891
|
+
.map(message => (message.role === 'assistant' ? { ...message, branchGroupId, hidden: true } : message))
|
|
892
|
+
]
|
|
893
|
+
}
|
|
894
|
+
})
|
|
895
|
+
|
|
896
|
+
try {
|
|
897
|
+
await requestGateway('prompt.submit', {
|
|
898
|
+
session_id: activeSessionId,
|
|
899
|
+
text: userText,
|
|
900
|
+
truncate_before_user_ordinal: truncateBeforeUserOrdinal
|
|
901
|
+
})
|
|
902
|
+
} catch (err) {
|
|
903
|
+
updateSessionState(activeSessionId, state => ({
|
|
904
|
+
...state,
|
|
905
|
+
busy: false,
|
|
906
|
+
awaitingResponse: false
|
|
907
|
+
}))
|
|
908
|
+
notifyError(err, copy.regenerateFailed)
|
|
909
|
+
}
|
|
910
|
+
},
|
|
911
|
+
[activeSessionId, copy.regenerateFailed, requestGateway, updateSessionState]
|
|
912
|
+
)
|
|
913
|
+
|
|
914
|
+
const editMessage = useCallback(
|
|
915
|
+
async (edited: AppendMessage) => {
|
|
916
|
+
const sessionId = activeSessionId || activeSessionIdRef.current
|
|
917
|
+
const sourceId = edited.sourceId || edited.parentId
|
|
918
|
+
const text = appendText(edited)
|
|
919
|
+
|
|
920
|
+
if (!sessionId || !sourceId || !text || edited.role !== 'user' || $busy.get()) {
|
|
921
|
+
return
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
const messages = $messages.get()
|
|
925
|
+
const sourceIndex = messages.findIndex(m => m.id === sourceId)
|
|
926
|
+
const source = messages[sourceIndex]
|
|
927
|
+
|
|
928
|
+
if (!source || source.role !== 'user' || chatMessageText(source).trim() === text) {
|
|
929
|
+
return
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Failed turn: optimistic user msg never reached the gateway, so truncating
|
|
933
|
+
// by ordinal would 422. Submit as a plain resend instead.
|
|
934
|
+
const nextMessage = messages[sourceIndex + 1]
|
|
935
|
+
const isFailedTurn = nextMessage?.role === 'assistant' && Boolean(nextMessage.error)
|
|
936
|
+
const editedMessage: ChatMessage = { ...source, parts: [textPart(text)] }
|
|
937
|
+
|
|
938
|
+
clearNotifications()
|
|
939
|
+
setMutableRef(busyRef, true)
|
|
940
|
+
setBusy(true)
|
|
941
|
+
setAwaitingResponse(true)
|
|
942
|
+
updateSessionState(sessionId, state => ({
|
|
943
|
+
...state,
|
|
944
|
+
busy: true,
|
|
945
|
+
awaitingResponse: true,
|
|
946
|
+
pendingBranchGroup: null,
|
|
947
|
+
sawAssistantPayload: false,
|
|
948
|
+
interrupted: false,
|
|
949
|
+
messages: [...state.messages.slice(0, sourceIndex), editedMessage]
|
|
950
|
+
}))
|
|
951
|
+
|
|
952
|
+
const submit = (truncateOrdinal?: number) =>
|
|
953
|
+
requestGateway('prompt.submit', {
|
|
954
|
+
session_id: sessionId,
|
|
955
|
+
text,
|
|
956
|
+
...(truncateOrdinal !== undefined && { truncate_before_user_ordinal: truncateOrdinal })
|
|
957
|
+
})
|
|
958
|
+
|
|
959
|
+
const isStaleTargetError = (err: unknown) =>
|
|
960
|
+
/no longer in session history|not in session history/i.test(err instanceof Error ? err.message : String(err))
|
|
961
|
+
|
|
962
|
+
try {
|
|
963
|
+
await submit(isFailedTurn ? undefined : visibleUserOrdinal(messages, sourceIndex))
|
|
964
|
+
} catch (err) {
|
|
965
|
+
let surfaced = err
|
|
966
|
+
|
|
967
|
+
if (!isFailedTurn && isStaleTargetError(err)) {
|
|
968
|
+
try {
|
|
969
|
+
await submit()
|
|
970
|
+
|
|
971
|
+
return
|
|
972
|
+
} catch (retryErr) {
|
|
973
|
+
surfaced = retryErr
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
setMutableRef(busyRef, false)
|
|
978
|
+
setBusy(false)
|
|
979
|
+
setAwaitingResponse(false)
|
|
980
|
+
updateSessionState(sessionId, state => ({ ...state, busy: false, awaitingResponse: false }))
|
|
981
|
+
notifyError(surfaced, copy.editFailed)
|
|
982
|
+
}
|
|
983
|
+
},
|
|
984
|
+
[activeSessionId, activeSessionIdRef, busyRef, copy.editFailed, requestGateway, updateSessionState]
|
|
985
|
+
)
|
|
986
|
+
|
|
987
|
+
const handleThreadMessagesChange = useCallback(
|
|
988
|
+
(nextMessages: readonly ThreadMessage[]) => {
|
|
989
|
+
const visibleIds = new Set(nextMessages.map(m => m.id))
|
|
990
|
+
const sessionId = activeSessionIdRef.current
|
|
991
|
+
|
|
992
|
+
if (!sessionId) {
|
|
993
|
+
return
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
updateSessionState(sessionId, state => {
|
|
997
|
+
let changed = false
|
|
998
|
+
|
|
999
|
+
const messages = state.messages.map(message => {
|
|
1000
|
+
if (message.role !== 'assistant' || !message.branchGroupId) {
|
|
1001
|
+
return message
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const hidden = !visibleIds.has(message.id)
|
|
1005
|
+
|
|
1006
|
+
if (message.hidden === hidden) {
|
|
1007
|
+
return message
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
changed = true
|
|
1011
|
+
|
|
1012
|
+
return { ...message, hidden }
|
|
1013
|
+
})
|
|
1014
|
+
|
|
1015
|
+
return changed ? { ...state, messages } : state
|
|
1016
|
+
})
|
|
1017
|
+
},
|
|
1018
|
+
[activeSessionIdRef, updateSessionState]
|
|
1019
|
+
)
|
|
1020
|
+
|
|
1021
|
+
return {
|
|
1022
|
+
cancelRun,
|
|
1023
|
+
editMessage,
|
|
1024
|
+
handleThreadMessagesChange,
|
|
1025
|
+
reloadFromMessage,
|
|
1026
|
+
steerPrompt,
|
|
1027
|
+
submitText,
|
|
1028
|
+
transcribeVoiceAudio
|
|
1029
|
+
}
|
|
1030
|
+
}
|