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,497 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { TextMessagePartProvider, useMessagePartText } from '@assistant-ui/react'
|
|
4
|
+
import {
|
|
5
|
+
type StreamdownTextComponents,
|
|
6
|
+
StreamdownTextPrimitive,
|
|
7
|
+
type SyntaxHighlighterProps
|
|
8
|
+
} from '@assistant-ui/react-streamdown'
|
|
9
|
+
import { code } from '@streamdown/code'
|
|
10
|
+
import { type ComponentProps, memo, type ReactNode, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react'
|
|
11
|
+
|
|
12
|
+
import { PreviewAttachment } from '@/components/chat/preview-attachment'
|
|
13
|
+
import { SyntaxHighlighter } from '@/components/chat/shiki-highlighter'
|
|
14
|
+
import { ZoomableImage } from '@/components/chat/zoomable-image'
|
|
15
|
+
import { normalizeExternalUrl, openExternalLink, PrettyLink } from '@/lib/external-link'
|
|
16
|
+
import { createMemoizedMathPlugin } from '@/lib/katex-memo'
|
|
17
|
+
import { preprocessMarkdown } from '@/lib/markdown-preprocess'
|
|
18
|
+
import {
|
|
19
|
+
filePathFromMediaPath,
|
|
20
|
+
mediaExternalUrl,
|
|
21
|
+
mediaKind,
|
|
22
|
+
mediaName,
|
|
23
|
+
mediaPathFromMarkdownHref,
|
|
24
|
+
mediaStreamUrl
|
|
25
|
+
} from '@/lib/media'
|
|
26
|
+
import { previewTargetFromMarkdownHref } from '@/lib/preview-targets'
|
|
27
|
+
import { cn } from '@/lib/utils'
|
|
28
|
+
|
|
29
|
+
// Math rendering plugin (KaTeX). Configured once at module scope — the
|
|
30
|
+
// plugin is stateless beyond its internal cache so re-creating per-render
|
|
31
|
+
// would needlessly thrash. We use a memoizing wrapper around rehype-katex
|
|
32
|
+
// (see lib/katex-memo.ts) so that during streaming we re-katex only the
|
|
33
|
+
// equations whose source actually changed since the last token. With the
|
|
34
|
+
// stock @streamdown/math plugin every equation re-renders on every token,
|
|
35
|
+
// which throttles UI updates badly for math-heavy responses; the memoized
|
|
36
|
+
// plugin keeps the steady-state work proportional to "new equations
|
|
37
|
+
// arriving" rather than "equations × tokens-per-second".
|
|
38
|
+
//
|
|
39
|
+
// `singleDollarTextMath: true` enables `$x^2$` for inline math (de-facto
|
|
40
|
+
// LLM convention). The default false-setting only accepts `$$...$$`.
|
|
41
|
+
const mathPlugin = createMemoizedMathPlugin({ singleDollarTextMath: true })
|
|
42
|
+
|
|
43
|
+
async function mediaSrc(path: string): Promise<string> {
|
|
44
|
+
if (/^(?:https?|data):/i.test(path)) {
|
|
45
|
+
return path
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Stream audio/video through the custom protocol: data URLs are capped and
|
|
49
|
+
// load the whole file into memory, which broke playback for larger videos.
|
|
50
|
+
if (window.NASTECHDesktop && ['audio', 'video'].includes(mediaKind(path))) {
|
|
51
|
+
return mediaStreamUrl(path)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!window.NASTECHDesktop?.readFileDataUrl) {
|
|
55
|
+
return mediaExternalUrl(path)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return window.NASTECHDesktop.readFileDataUrl(filePathFromMediaPath(path))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function OpenMediaButton({ kind, path }: { kind: 'audio' | 'video'; path: string }) {
|
|
62
|
+
return (
|
|
63
|
+
<button
|
|
64
|
+
className="mt-2 bg-transparent text-xs font-medium text-muted-foreground underline underline-offset-4 decoration-current/20 hover:text-foreground"
|
|
65
|
+
onClick={() => void window.NASTECHDesktop?.openExternal(mediaExternalUrl(path))}
|
|
66
|
+
type="button"
|
|
67
|
+
>
|
|
68
|
+
Open {kind} file
|
|
69
|
+
</button>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function MediaAttachment({ path }: { path: string }) {
|
|
74
|
+
const [src, setSrc] = useState('')
|
|
75
|
+
const [failed, setFailed] = useState(false)
|
|
76
|
+
const kind = mediaKind(path)
|
|
77
|
+
const name = mediaName(path)
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
let cancelled = false
|
|
81
|
+
let objectUrl = ''
|
|
82
|
+
|
|
83
|
+
setFailed(false)
|
|
84
|
+
setSrc('')
|
|
85
|
+
void mediaSrc(path)
|
|
86
|
+
.then(value => {
|
|
87
|
+
if (value.startsWith('blob:')) {
|
|
88
|
+
objectUrl = value
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!cancelled) {
|
|
92
|
+
setSrc(value)
|
|
93
|
+
} else if (objectUrl) {
|
|
94
|
+
URL.revokeObjectURL(objectUrl)
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
.catch(() => {
|
|
98
|
+
if (!cancelled) {
|
|
99
|
+
setFailed(true)
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
return () => {
|
|
104
|
+
cancelled = true
|
|
105
|
+
|
|
106
|
+
if (objectUrl) {
|
|
107
|
+
URL.revokeObjectURL(objectUrl)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}, [path])
|
|
111
|
+
|
|
112
|
+
if (kind === 'image' && src) {
|
|
113
|
+
return (
|
|
114
|
+
<span className="block">
|
|
115
|
+
<MarkdownImage alt={name} src={src} />
|
|
116
|
+
</span>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (kind === 'audio' && src) {
|
|
121
|
+
return (
|
|
122
|
+
<span className="my-3 block max-w-md rounded-xl border border-border bg-muted/35 p-3">
|
|
123
|
+
<span className="mb-2 block truncate text-xs font-medium text-muted-foreground">{name}</span>
|
|
124
|
+
<audio className="block w-full" controls onError={() => setFailed(true)} preload="metadata" src={src} />
|
|
125
|
+
{failed && <OpenMediaButton kind="audio" path={path} />}
|
|
126
|
+
</span>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (kind === 'video' && src) {
|
|
131
|
+
return (
|
|
132
|
+
<span className="my-3 block max-w-2xl rounded-xl border border-border bg-muted/35 p-3">
|
|
133
|
+
<span className="mb-2 block truncate text-xs font-medium text-muted-foreground">{name}</span>
|
|
134
|
+
<video
|
|
135
|
+
className="block max-h-112 w-full rounded-lg bg-black"
|
|
136
|
+
controls
|
|
137
|
+
onError={() => setFailed(true)}
|
|
138
|
+
src={src}
|
|
139
|
+
/>
|
|
140
|
+
{failed && <OpenMediaButton kind="video" path={path} />}
|
|
141
|
+
</span>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<a
|
|
147
|
+
className="font-semibold text-foreground underline underline-offset-4 decoration-current/20 wrap-anywhere"
|
|
148
|
+
href="#"
|
|
149
|
+
onClick={event => {
|
|
150
|
+
event.preventDefault()
|
|
151
|
+
openExternalLink(mediaExternalUrl(path))
|
|
152
|
+
}}
|
|
153
|
+
>
|
|
154
|
+
{failed ? `Open ${name}` : `Loading ${name}...`}
|
|
155
|
+
</a>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function childrenToText(children: unknown): string {
|
|
160
|
+
if (typeof children === 'string' || typeof children === 'number') {
|
|
161
|
+
return String(children).trim()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (Array.isArray(children) && children.every(c => typeof c === 'string' || typeof c === 'number')) {
|
|
165
|
+
return children.join('').trim()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return ''
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function MarkdownLink({ children, className, href, ...props }: ComponentProps<'a'>) {
|
|
172
|
+
const mediaPath = mediaPathFromMarkdownHref(href)
|
|
173
|
+
|
|
174
|
+
if (mediaPath) {
|
|
175
|
+
return <MediaAttachment path={mediaPath} />
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const previewTarget = previewTargetFromMarkdownHref(href)
|
|
179
|
+
|
|
180
|
+
if (previewTarget) {
|
|
181
|
+
return <PreviewAttachment source="explicit-link" target={previewTarget} />
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const target = href ? normalizeExternalUrl(href) : href
|
|
185
|
+
|
|
186
|
+
if (!target || !/^https?:\/\//i.test(target)) {
|
|
187
|
+
return (
|
|
188
|
+
<a
|
|
189
|
+
className={cn(
|
|
190
|
+
'font-semibold text-foreground underline underline-offset-4 decoration-current/20 wrap-anywhere',
|
|
191
|
+
className
|
|
192
|
+
)}
|
|
193
|
+
href={href}
|
|
194
|
+
rel="noopener noreferrer"
|
|
195
|
+
target="_blank"
|
|
196
|
+
{...props}
|
|
197
|
+
>
|
|
198
|
+
{children}
|
|
199
|
+
</a>
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const text = childrenToText(children)
|
|
204
|
+
const fallbackLabel = text && normalizeExternalUrl(text) !== target ? text : undefined
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<PrettyLink className={cn('wrap-anywhere', className)} fallbackLabel={fallbackLabel} href={target} {...props} />
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function MarkdownImage({ className, src, alt, ...props }: ComponentProps<'img'>) {
|
|
212
|
+
return (
|
|
213
|
+
<ZoomableImage
|
|
214
|
+
alt={alt}
|
|
215
|
+
className={cn(
|
|
216
|
+
'm-0 block h-auto w-auto max-h-(--image-preview-height) max-w-[min(100%,var(--image-preview-max-width))] rounded-lg object-contain shadow-[0_0.0625rem_0.125rem_color-mix(in_srgb,#000_4%,transparent),0_0.625rem_1.5rem_color-mix(in_srgb,#000_5%,transparent)]',
|
|
217
|
+
className
|
|
218
|
+
)}
|
|
219
|
+
containerClassName="my-2 block w-fit max-w-full"
|
|
220
|
+
slot="aui_markdown-image"
|
|
221
|
+
src={src}
|
|
222
|
+
{...props}
|
|
223
|
+
/>
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Steady character-reveal for streaming text: decouples visible cadence from
|
|
228
|
+
// bursty arrival so text flows instead of popping (cf. assistant-ui's useSmooth,
|
|
229
|
+
// reimplemented for a tunable rate). Proportional drain — each frame reveals a
|
|
230
|
+
// slice of the backlog so the reveal converges within ~REVEAL_DRAIN_MS whatever
|
|
231
|
+
// the size; the per-frame cap stops a huge dump rendering as one slab. The loop
|
|
232
|
+
// is gated on backlog, not isRunning, so a stream that completes mid-reveal
|
|
233
|
+
// keeps draining its tail instead of snapping.
|
|
234
|
+
const REVEAL_DRAIN_MS = 500
|
|
235
|
+
const REVEAL_MAX_CHARS_PER_FRAME = 30
|
|
236
|
+
|
|
237
|
+
function useSmoothReveal(text: string, isRunning: boolean): string {
|
|
238
|
+
const [displayed, setDisplayed] = useState(isRunning ? '' : text)
|
|
239
|
+
const targetRef = useRef(text)
|
|
240
|
+
const shownRef = useRef(displayed)
|
|
241
|
+
const frameRef = useRef<number | null>(null)
|
|
242
|
+
const lastTickRef = useRef(0)
|
|
243
|
+
|
|
244
|
+
shownRef.current = displayed
|
|
245
|
+
targetRef.current = text
|
|
246
|
+
|
|
247
|
+
useEffect(() => {
|
|
248
|
+
if (typeof window === 'undefined') {
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Non-extending change (regenerate / branch / history swap): restart from
|
|
253
|
+
// empty while streaming, else snap to the replacement.
|
|
254
|
+
if (!text.startsWith(shownRef.current)) {
|
|
255
|
+
shownRef.current = isRunning ? '' : text
|
|
256
|
+
setDisplayed(shownRef.current)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (shownRef.current.length >= text.length || frameRef.current !== null) {
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
lastTickRef.current = performance.now()
|
|
264
|
+
|
|
265
|
+
const tick = () => {
|
|
266
|
+
const now = performance.now()
|
|
267
|
+
const dt = now - lastTickRef.current
|
|
268
|
+
lastTickRef.current = now
|
|
269
|
+
|
|
270
|
+
const remaining = targetRef.current.length - shownRef.current.length
|
|
271
|
+
const add = Math.min(remaining, REVEAL_MAX_CHARS_PER_FRAME, Math.max(1, Math.ceil((remaining * dt) / REVEAL_DRAIN_MS)))
|
|
272
|
+
shownRef.current = targetRef.current.slice(0, shownRef.current.length + add)
|
|
273
|
+
setDisplayed(shownRef.current)
|
|
274
|
+
|
|
275
|
+
frameRef.current = shownRef.current.length < targetRef.current.length ? requestAnimationFrame(tick) : null
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
frameRef.current = requestAnimationFrame(tick)
|
|
279
|
+
}, [text, isRunning])
|
|
280
|
+
|
|
281
|
+
useEffect(
|
|
282
|
+
() => () => {
|
|
283
|
+
if (frameRef.current !== null && typeof window !== 'undefined') {
|
|
284
|
+
cancelAnimationFrame(frameRef.current)
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
[]
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
return displayed
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Re-publish the part context with a smooth character-reveal, above
|
|
294
|
+
// DeferStreamingText so the reveal feeds the deferred markdown pipeline. Status
|
|
295
|
+
// stays running while revealing so the caret persists past the underlying part
|
|
296
|
+
// settling.
|
|
297
|
+
function SmoothStreamingText({ children }: { children: ReactNode }) {
|
|
298
|
+
const { text, status } = useMessagePartText()
|
|
299
|
+
const isRunning = status.type === 'running'
|
|
300
|
+
const revealed = useSmoothReveal(text, isRunning)
|
|
301
|
+
|
|
302
|
+
return (
|
|
303
|
+
<TextMessagePartProvider isRunning={isRunning || revealed !== text} text={revealed}>
|
|
304
|
+
{children}
|
|
305
|
+
</TextMessagePartProvider>
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Re-publish the active message-part context with React's `useDeferredValue`
|
|
311
|
+
* applied to the streaming text and status. The outer wrapper still re-renders
|
|
312
|
+
* on every token, but the work it does is trivial (one hook, one provider).
|
|
313
|
+
*
|
|
314
|
+
* The expensive subtree (Streamdown → micromark → mdast → hast → React) lives
|
|
315
|
+
* inside `<TextMessagePartProvider>` and reads the deferred text via the
|
|
316
|
+
* normal `useMessagePartText` hook. React's concurrent scheduler then has
|
|
317
|
+
* permission to:
|
|
318
|
+
* - skip intermediate token states when the next token arrives mid-render
|
|
319
|
+
* (it abandons the in-flight deferred render and starts over)
|
|
320
|
+
* - deprioritize the markdown render when the main thread is busy with an
|
|
321
|
+
* urgent task (typing, scrolling, layout work elsewhere)
|
|
322
|
+
*
|
|
323
|
+
* Net effect: per-token CPU is unchanged but the *blocking* part of that work
|
|
324
|
+
* goes away — typing-while-streaming stays a single-frame paint, scroll
|
|
325
|
+
* stutter disappears, and the longtask histogram tightens because long
|
|
326
|
+
* commits can be interrupted and discarded.
|
|
327
|
+
*
|
|
328
|
+
* Industry standard (Streamdown's own block-array setState already uses
|
|
329
|
+
* `useTransition`); this just lifts the deferral up to the consumer text
|
|
330
|
+
* boundary so it covers the whole pipeline, not just the inner setState.
|
|
331
|
+
*/
|
|
332
|
+
function DeferStreamingText({ children }: { children: ReactNode }) {
|
|
333
|
+
const { text, status } = useMessagePartText()
|
|
334
|
+
const deferredText = useDeferredValue(text)
|
|
335
|
+
const isRunning = status.type === 'running'
|
|
336
|
+
|
|
337
|
+
return (
|
|
338
|
+
<TextMessagePartProvider isRunning={isRunning} text={deferredText}>
|
|
339
|
+
{children}
|
|
340
|
+
</TextMessagePartProvider>
|
|
341
|
+
)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
interface MarkdownTextSurfaceProps {
|
|
345
|
+
containerClassName?: string
|
|
346
|
+
containerProps?: ComponentProps<'div'>
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Headings shrink to chat scale rather than the prose default (h1≈xl). Kept
|
|
350
|
+
// table-driven so adding/tweaking levels is one row.
|
|
351
|
+
const HEADING_SIZES: Record<'h1' | 'h2' | 'h3' | 'h4', string> = {
|
|
352
|
+
h1: 'text-[1rem] tracking-tight',
|
|
353
|
+
h2: 'text-[0.9375rem] tracking-tight',
|
|
354
|
+
h3: 'text-[0.875rem]',
|
|
355
|
+
h4: 'text-[0.8125rem]'
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const MARKDOWN_CONTAINER_CLASS_NAME = cn(
|
|
359
|
+
'aui-md prose w-full max-w-none overflow-hidden text-[length:var(--conversation-text-font-size)] leading-(--dt-line-height) text-foreground',
|
|
360
|
+
'prose-p:leading-(--dt-line-height) prose-li:leading-(--dt-line-height)',
|
|
361
|
+
'prose-headings:text-foreground prose-strong:text-foreground',
|
|
362
|
+
'prose-a:break-words prose-p:[overflow-wrap:anywhere]',
|
|
363
|
+
'prose-li:marker:text-muted-foreground/70',
|
|
364
|
+
'prose-code:rounded-[0.25rem] prose-code:px-[0.1875rem] prose-code:py-px prose-code:font-mono prose-code:text-[0.9em] prose-code:font-normal prose-code:before:content-none prose-code:after:content-none',
|
|
365
|
+
'[&>*:first-child]:mt-0 [&>*:last-child]:mb-0 [&>*+*]:mt-(--paragraph-gap)'
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
function MarkdownTextSurface({ containerClassName, containerProps }: MarkdownTextSurfaceProps) {
|
|
369
|
+
const { status } = useMessagePartText()
|
|
370
|
+
const isStreaming = status.type === 'running'
|
|
371
|
+
|
|
372
|
+
// Keep code parsing enabled while streaming so incomplete fenced blocks still
|
|
373
|
+
// render as code cards. The expensive Shiki pass is deferred by
|
|
374
|
+
// `SyntaxHighlighter` below when `isStreaming` is true.
|
|
375
|
+
const plugins = useMemo(() => ({ math: mathPlugin, code }), [])
|
|
376
|
+
|
|
377
|
+
const components = useMemo(
|
|
378
|
+
() =>
|
|
379
|
+
({
|
|
380
|
+
h1: ({ className, ...props }: ComponentProps<'h1'>) => (
|
|
381
|
+
<h1 className={cn('my-1 font-semibold', HEADING_SIZES.h1, className)} {...props} />
|
|
382
|
+
),
|
|
383
|
+
h2: ({ className, ...props }: ComponentProps<'h2'>) => (
|
|
384
|
+
<h2 className={cn('my-1 font-semibold', HEADING_SIZES.h2, className)} {...props} />
|
|
385
|
+
),
|
|
386
|
+
h3: ({ className, ...props }: ComponentProps<'h3'>) => (
|
|
387
|
+
<h3 className={cn('my-1 font-semibold', HEADING_SIZES.h3, className)} {...props} />
|
|
388
|
+
),
|
|
389
|
+
h4: ({ className, ...props }: ComponentProps<'h4'>) => (
|
|
390
|
+
<h4 className={cn('my-1 font-semibold', HEADING_SIZES.h4, className)} {...props} />
|
|
391
|
+
),
|
|
392
|
+
p: ({ className, ...props }: ComponentProps<'p'>) => (
|
|
393
|
+
// Vertical rhythm is owned by styles.css (`--paragraph-gap`), which
|
|
394
|
+
// must out-specify Tailwind Typography's `prose` margins — so no
|
|
395
|
+
// `my-*` here on purpose.
|
|
396
|
+
<p className={cn('wrap-anywhere leading-(--dt-line-height)', className)} {...props} />
|
|
397
|
+
),
|
|
398
|
+
a: MarkdownLink,
|
|
399
|
+
// `---` as quiet spacing, not a heavy full-width rule.
|
|
400
|
+
hr: (_props: ComponentProps<'hr'>) => <div aria-hidden className="my-3" />,
|
|
401
|
+
blockquote: ({ className, ...props }: ComponentProps<'blockquote'>) => (
|
|
402
|
+
<blockquote
|
|
403
|
+
className={cn('border-l-2 border-border pl-3 text-muted-foreground italic', className)}
|
|
404
|
+
{...props}
|
|
405
|
+
/>
|
|
406
|
+
),
|
|
407
|
+
ul: ({ className, ...props }: ComponentProps<'ul'>) => (
|
|
408
|
+
<ul className={cn('my-1 gap-0', className)} {...props} />
|
|
409
|
+
),
|
|
410
|
+
ol: ({ className, ...props }: ComponentProps<'ol'>) => (
|
|
411
|
+
<ol className={cn('my-1 gap-0', className)} {...props} />
|
|
412
|
+
),
|
|
413
|
+
li: ({ className, ...props }: ComponentProps<'li'>) => (
|
|
414
|
+
<li className={cn('leading-(--dt-line-height)', className)} {...props} />
|
|
415
|
+
),
|
|
416
|
+
table: ({ className, ...props }: ComponentProps<'table'>) => (
|
|
417
|
+
<div className="aui-md-table my-2 max-w-full overflow-x-auto rounded-[0.375rem] border border-border">
|
|
418
|
+
<table
|
|
419
|
+
className={cn(
|
|
420
|
+
'm-0 w-full border-collapse text-[0.8125rem] [&_tr]:border-b [&_tr]:border-border last:[&_tr]:border-0',
|
|
421
|
+
className
|
|
422
|
+
)}
|
|
423
|
+
{...props}
|
|
424
|
+
/>
|
|
425
|
+
</div>
|
|
426
|
+
),
|
|
427
|
+
thead: ({ className, ...props }: ComponentProps<'thead'>) => (
|
|
428
|
+
<thead className={cn('m-0 bg-muted/35 text-muted-foreground', className)} {...props} />
|
|
429
|
+
),
|
|
430
|
+
th: ({ className, ...props }: ComponentProps<'th'>) => (
|
|
431
|
+
<th
|
|
432
|
+
className={cn(
|
|
433
|
+
'px-2.5 py-1.5 text-left align-middle text-[0.75rem] font-medium text-muted-foreground',
|
|
434
|
+
className
|
|
435
|
+
)}
|
|
436
|
+
{...props}
|
|
437
|
+
/>
|
|
438
|
+
),
|
|
439
|
+
td: ({ className, ...props }: ComponentProps<'td'>) => (
|
|
440
|
+
<td className={cn('px-2.5 py-1.5 align-top text-[0.8125rem] leading-snug', className)} {...props} />
|
|
441
|
+
),
|
|
442
|
+
img: MarkdownImage,
|
|
443
|
+
SyntaxHighlighter: (props: SyntaxHighlighterProps) => <SyntaxHighlighter {...props} defer={isStreaming} />
|
|
444
|
+
}) as StreamdownTextComponents,
|
|
445
|
+
[isStreaming]
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
return (
|
|
449
|
+
<StreamdownTextPrimitive
|
|
450
|
+
components={components}
|
|
451
|
+
containerClassName={cn(MARKDOWN_CONTAINER_CLASS_NAME, containerClassName)}
|
|
452
|
+
containerProps={containerProps}
|
|
453
|
+
lineNumbers={false}
|
|
454
|
+
mode="streaming"
|
|
455
|
+
// Always auto-close incomplete fences — even during streaming.
|
|
456
|
+
// Without this, an unclosed ```python ... ``` whose body contains
|
|
457
|
+
// `$` (very common: shell snippets, JS template strings, dollar
|
|
458
|
+
// amounts) leaks those dollars out to the math parser and they
|
|
459
|
+
// get rendered as broken inline math until the closing fence
|
|
460
|
+
// arrives. Shiki is independently deferred via `defer={isStreaming}`
|
|
461
|
+
// on the SyntaxHighlighter component, so we don't pay code-block
|
|
462
|
+
// tokenization on every token even with this set.
|
|
463
|
+
parseIncompleteMarkdown
|
|
464
|
+
plugins={plugins}
|
|
465
|
+
preprocess={preprocessMarkdown}
|
|
466
|
+
/>
|
|
467
|
+
)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
interface MarkdownTextContentProps extends MarkdownTextSurfaceProps {
|
|
471
|
+
isRunning: boolean
|
|
472
|
+
text: string
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export function MarkdownTextContent({ isRunning, text, ...surfaceProps }: MarkdownTextContentProps) {
|
|
476
|
+
return (
|
|
477
|
+
<TextMessagePartProvider isRunning={isRunning} text={text}>
|
|
478
|
+
<SmoothStreamingText>
|
|
479
|
+
<DeferStreamingText>
|
|
480
|
+
<MarkdownTextSurface {...surfaceProps} />
|
|
481
|
+
</DeferStreamingText>
|
|
482
|
+
</SmoothStreamingText>
|
|
483
|
+
</TextMessagePartProvider>
|
|
484
|
+
)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const MarkdownTextImpl = () => {
|
|
488
|
+
return (
|
|
489
|
+
<SmoothStreamingText>
|
|
490
|
+
<DeferStreamingText>
|
|
491
|
+
<MarkdownTextSurface />
|
|
492
|
+
</DeferStreamingText>
|
|
493
|
+
</SmoothStreamingText>
|
|
494
|
+
)
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export const MarkdownText = memo(MarkdownTextImpl)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { cleanup, render, screen } from '@testing-library/react'
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { MessageRenderBoundary } from './message-render-boundary'
|
|
5
|
+
|
|
6
|
+
afterEach(cleanup)
|
|
7
|
+
|
|
8
|
+
function Boom({ error }: { error: Error | null }): null {
|
|
9
|
+
if (error) {
|
|
10
|
+
throw error
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const lookupError = new Error('tapClientLookup: Index 2 out of bounds (length: 2)')
|
|
17
|
+
|
|
18
|
+
describe('MessageRenderBoundary', () => {
|
|
19
|
+
it('renders children when nothing throws', () => {
|
|
20
|
+
render(
|
|
21
|
+
<MessageRenderBoundary resetKey="a">
|
|
22
|
+
<div>content</div>
|
|
23
|
+
</MessageRenderBoundary>
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
expect(screen.getByText('content')).toBeTruthy()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('swallows the transient tapClientLookup out-of-bounds store race', () => {
|
|
30
|
+
const spy = vi.spyOn(console, 'error').mockImplementation(() => undefined)
|
|
31
|
+
|
|
32
|
+
const { container } = render(
|
|
33
|
+
<MessageRenderBoundary resetKey="a">
|
|
34
|
+
<Boom error={lookupError} />
|
|
35
|
+
</MessageRenderBoundary>
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
expect(container.innerHTML).toBe('')
|
|
39
|
+
spy.mockRestore()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('recovers on the next consistent snapshot when resetKey changes', () => {
|
|
43
|
+
const spy = vi.spyOn(console, 'error').mockImplementation(() => undefined)
|
|
44
|
+
|
|
45
|
+
const { rerender } = render(
|
|
46
|
+
<MessageRenderBoundary resetKey="a">
|
|
47
|
+
<Boom error={lookupError} />
|
|
48
|
+
</MessageRenderBoundary>
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
rerender(
|
|
52
|
+
<MessageRenderBoundary resetKey="b">
|
|
53
|
+
<Boom error={null} />
|
|
54
|
+
</MessageRenderBoundary>
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
rerender(
|
|
58
|
+
<MessageRenderBoundary resetKey="b">
|
|
59
|
+
<div>recovered</div>
|
|
60
|
+
</MessageRenderBoundary>
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
expect(screen.getByText('recovered')).toBeTruthy()
|
|
64
|
+
spy.mockRestore()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('re-throws unrelated errors so real bugs still surface', () => {
|
|
68
|
+
const spy = vi.spyOn(console, 'error').mockImplementation(() => undefined)
|
|
69
|
+
|
|
70
|
+
expect(() =>
|
|
71
|
+
render(
|
|
72
|
+
<MessageRenderBoundary resetKey="a">
|
|
73
|
+
<Boom error={new Error('genuine render bug')} />
|
|
74
|
+
</MessageRenderBoundary>
|
|
75
|
+
)
|
|
76
|
+
).toThrow('genuine render bug')
|
|
77
|
+
|
|
78
|
+
spy.mockRestore()
|
|
79
|
+
})
|
|
80
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Component, type ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
// `@assistant-ui/store`'s index-keyed child-scope lookup (`tapClientLookup`)
|
|
4
|
+
// throws — rather than returning undefined — when a subscriber reads an index
|
|
5
|
+
// that the message/parts list no longer has. This races during high-frequency
|
|
6
|
+
// store replacement (session switch mid-stream, gateway reconnect replay): a
|
|
7
|
+
// subscriber from the previous, longer list is still in React's notification
|
|
8
|
+
// queue and reads one slot past the new, shorter array before it can unmount.
|
|
9
|
+
// The throw is transient and self-heals on the next consistent snapshot, but
|
|
10
|
+
// without a local boundary it unwinds to the root and blanks the whole app.
|
|
11
|
+
// Upstream-tracked: assistant-ui/assistant-ui#4051, #3652.
|
|
12
|
+
const isTransientLookupError = (error: unknown): boolean =>
|
|
13
|
+
error instanceof Error && /tapClient(Lookup|Resource).*out of bounds/.test(error.message)
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
// Changes whenever the message list mutates; remounting clears the caught
|
|
17
|
+
// error so the next consistent render recovers silently.
|
|
18
|
+
resetKey: string
|
|
19
|
+
children: ReactNode
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class MessageRenderBoundary extends Component<Props, { error: Error | null }> {
|
|
23
|
+
state: { error: Error | null } = { error: null }
|
|
24
|
+
|
|
25
|
+
static getDerivedStateFromError(error: Error) {
|
|
26
|
+
return { error }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
componentDidUpdate(prev: Props) {
|
|
30
|
+
if (this.state.error && prev.resetKey !== this.props.resetKey) {
|
|
31
|
+
this.setState({ error: null })
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
render() {
|
|
36
|
+
if (this.state.error) {
|
|
37
|
+
// Only swallow the transient store race; re-throw anything else so real
|
|
38
|
+
// bugs still reach the root error boundary.
|
|
39
|
+
if (!isTransientLookupError(this.state.error)) {
|
|
40
|
+
throw this.state.error
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return this.props.children
|
|
47
|
+
}
|
|
48
|
+
}
|