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,27 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { extractPreviewTargets, previewTargetFromMarkdownHref, stripPreviewTargets } from './preview-targets'
|
|
4
|
+
|
|
5
|
+
describe('preview target detection', () => {
|
|
6
|
+
it('does not infer preview targets from raw paths or URLs', () => {
|
|
7
|
+
expect(extractPreviewTargets('Preview: http://localhost:5173/')).toEqual([])
|
|
8
|
+
expect(extractPreviewTargets('Open index.html\n/tmp/demo.html\nhttp://localhost:5173/')).toEqual([])
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('decodes preview markdown hrefs', () => {
|
|
12
|
+
expect(previewTargetFromMarkdownHref('#preview/%2Ftmp%2Fdemo.html')).toBe('/tmp/demo.html')
|
|
13
|
+
expect(previewTargetFromMarkdownHref('#preview:%2Ftmp%2Fdemo.html')).toBe('/tmp/demo.html')
|
|
14
|
+
expect(previewTargetFromMarkdownHref('#media:%2Ftmp%2Fdemo.mp4')).toBeNull()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('extracts preview targets from already-rendered preview markers', () => {
|
|
18
|
+
expect(extractPreviewTargets('[Preview: demo.html](#preview:%2Ftmp%2Fdemo.html)')).toEqual(['/tmp/demo.html'])
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('strips preview targets from visible assistant text', () => {
|
|
22
|
+
expect(stripPreviewTargets('ready\n/tmp/mycelium-bunnies.html\nopen it')).toBe(
|
|
23
|
+
'ready\n/tmp/mycelium-bunnies.html\nopen it'
|
|
24
|
+
)
|
|
25
|
+
expect(stripPreviewTargets('[Preview: demo.html](#preview:%2Ftmp%2Fdemo.html)\nopen it')).toBe('open it')
|
|
26
|
+
})
|
|
27
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const PREVIEW_MARKDOWN_RE = /\[Preview:[^\]]+\]\((?<href>#preview[:/][^)]+)\)/gi
|
|
2
|
+
|
|
3
|
+
export function stripPreviewTargets(text: string): string {
|
|
4
|
+
return text
|
|
5
|
+
.replace(PREVIEW_MARKDOWN_RE, '')
|
|
6
|
+
.replace(/[ \t]+\n/g, '\n')
|
|
7
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
8
|
+
.trim()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function extractPreviewTargets(text: string): string[] {
|
|
12
|
+
const targets: string[] = []
|
|
13
|
+
const seen = new Set<string>()
|
|
14
|
+
|
|
15
|
+
for (const match of text.matchAll(PREVIEW_MARKDOWN_RE)) {
|
|
16
|
+
const target = previewTargetFromMarkdownHref(match.groups?.href)
|
|
17
|
+
|
|
18
|
+
if (target && !seen.has(target)) {
|
|
19
|
+
seen.add(target)
|
|
20
|
+
targets.push(target)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return targets
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function previewMarkdownHref(target: string): string {
|
|
28
|
+
return `#preview/${encodeURIComponent(target)}`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function previewTargetFromMarkdownHref(href?: string): string | null {
|
|
32
|
+
if (!href?.startsWith('#preview:') && !href?.startsWith('#preview/')) {
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
return decodeURIComponent(href.slice('#preview'.length + 1))
|
|
38
|
+
} catch {
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function previewName(target: string): string {
|
|
44
|
+
try {
|
|
45
|
+
const url = new URL(target)
|
|
46
|
+
|
|
47
|
+
if (url.protocol === 'file:') {
|
|
48
|
+
return decodeURIComponent(url.pathname).split(/[\\/]/).filter(Boolean).pop() || target
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const file = url.pathname.split('/').filter(Boolean).pop()
|
|
52
|
+
|
|
53
|
+
return file || url.host
|
|
54
|
+
} catch {
|
|
55
|
+
return target.split(/[\\/]/).filter(Boolean).pop() || target
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function previewDisplayLabel(target: string): string {
|
|
60
|
+
const escaped = previewName(target).replace(/[[\]\\]/g, '\\$&')
|
|
61
|
+
|
|
62
|
+
return `Preview: ${escaped}`
|
|
63
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Deterministic per-profile color so a profile is glanceable across the app
|
|
2
|
+
// (the sidebar profile rail). The default/root profile has no color — named
|
|
3
|
+
// profiles get a stable hue derived from the name, so the same profile always
|
|
4
|
+
// reads the same color without persisting anything.
|
|
5
|
+
|
|
6
|
+
const PROFILE_TAG_SATURATION = 68
|
|
7
|
+
const PROFILE_TAG_LIGHTNESS = 58
|
|
8
|
+
|
|
9
|
+
function hashString(value: string): number {
|
|
10
|
+
let hash = 0
|
|
11
|
+
|
|
12
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
13
|
+
hash = (hash * 31 + value.charCodeAt(index)) >>> 0
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return hash
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Returns an hsl() string for a named profile, or null for default/empty
|
|
20
|
+
// (rendered neutral / untagged).
|
|
21
|
+
export function profileColor(name: null | string | undefined): null | string {
|
|
22
|
+
const key = (name ?? '').trim()
|
|
23
|
+
|
|
24
|
+
if (!key || key === 'default') {
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const hue = hashString(key) % 360
|
|
29
|
+
|
|
30
|
+
return `hsl(${hue} ${PROFILE_TAG_SATURATION}% ${PROFILE_TAG_LIGHTNESS}%)`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// A profile's effective color: a user-picked override wins, else the
|
|
34
|
+
// deterministic hue. Default/empty stays neutral (null) regardless.
|
|
35
|
+
export function resolveProfileColor(
|
|
36
|
+
name: null | string | undefined,
|
|
37
|
+
overrides: Record<string, string>
|
|
38
|
+
): null | string {
|
|
39
|
+
const key = (name ?? '').trim()
|
|
40
|
+
|
|
41
|
+
if (!key || key === 'default') {
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return overrides[key] ?? profileColor(key)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Curated swatches for the rail color picker — evenly spaced hues at the same
|
|
49
|
+
// saturation/lightness as the deterministic palette, so picks stay cohesive.
|
|
50
|
+
export const PROFILE_SWATCHES: readonly string[] = Array.from(
|
|
51
|
+
{ length: 12 },
|
|
52
|
+
(_, index) => `hsl(${index * 30} ${PROFILE_TAG_SATURATION}% ${PROFILE_TAG_LIGHTNESS}%)`
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
// Translucent fill derived from a profile color, for tag backgrounds.
|
|
56
|
+
export function profileColorSoft(color: string, percent = 16): string {
|
|
57
|
+
return `color-mix(in srgb, ${color} ${percent}%, transparent)`
|
|
58
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { isProviderSetupErrorMessage } from './provider-setup-errors'
|
|
4
|
+
|
|
5
|
+
describe('isProviderSetupErrorMessage', () => {
|
|
6
|
+
it('matches generic missing-provider copy', () => {
|
|
7
|
+
expect(isProviderSetupErrorMessage('No inference provider configured. Run `NASTECH model` to choose one.')).toBe(
|
|
8
|
+
true
|
|
9
|
+
)
|
|
10
|
+
expect(isProviderSetupErrorMessage('No inference provider is configured.')).toBe(true)
|
|
11
|
+
expect(isProviderSetupErrorMessage('No NasTech provider is configured.')).toBe(true)
|
|
12
|
+
expect(isProviderSetupErrorMessage('set an API key (OPENROUTER_API_KEY) in ~/.NASTECH/.env')).toBe(true)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('does not match non-provider runtime failures', () => {
|
|
16
|
+
expect(
|
|
17
|
+
isProviderSetupErrorMessage('Selected runtime is not available. setup.status reports configured credentials.')
|
|
18
|
+
).toBe(false)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('returns false for empty input', () => {
|
|
22
|
+
expect(isProviderSetupErrorMessage('')).toBe(false)
|
|
23
|
+
expect(isProviderSetupErrorMessage(null)).toBe(false)
|
|
24
|
+
expect(isProviderSetupErrorMessage(undefined)).toBe(false)
|
|
25
|
+
})
|
|
26
|
+
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const PROVIDER_SETUP_ERROR_RE =
|
|
2
|
+
/No (?:inference|NasTech) provider(?: is)? configured|no_provider_configured|OPENROUTER_API_KEY|OPENAI_API_KEY|ANTHROPIC_API_KEY|set an API key/i
|
|
3
|
+
|
|
4
|
+
export function isProviderSetupErrorMessage(message: null | string | undefined): boolean {
|
|
5
|
+
const text = message?.trim()
|
|
6
|
+
|
|
7
|
+
if (!text) {
|
|
8
|
+
return false
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return PROVIDER_SETUP_ERROR_RE.test(text)
|
|
12
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { QueryClient } from '@tanstack/react-query'
|
|
2
|
+
|
|
3
|
+
// Shared React Query client. Lives in its own module (not main.tsx) so non-React
|
|
4
|
+
// code — e.g. the profile store on a gateway swap — can invalidate cached,
|
|
5
|
+
// profile-scoped settings without importing the app entry point.
|
|
6
|
+
export const queryClient = new QueryClient({
|
|
7
|
+
defaultOptions: {
|
|
8
|
+
queries: {
|
|
9
|
+
refetchOnWindowFocus: false,
|
|
10
|
+
staleTime: 60_000
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
})
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { parseMarkdownIntoBlocks } from '@assistant-ui/react-streamdown'
|
|
2
|
+
import remend from 'remend'
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { findRemendWindowStart, tailBoundedRemend } from './remend-tail'
|
|
6
|
+
|
|
7
|
+
const CORPUS = `# Heading one
|
|
8
|
+
|
|
9
|
+
Intro paragraph with **bold**, *italic*, \`inline code\`, and a [link](https://example.com).
|
|
10
|
+
|
|
11
|
+
## Code
|
|
12
|
+
|
|
13
|
+
\`\`\`python
|
|
14
|
+
def main():
|
|
15
|
+
cost = "$5"
|
|
16
|
+
print(f"total: $\{cost}")
|
|
17
|
+
\`\`\`
|
|
18
|
+
|
|
19
|
+
Some text after the fence with $x^2 + y^2$ inline math.
|
|
20
|
+
|
|
21
|
+
$$
|
|
22
|
+
\\int_0^1 f(x) dx
|
|
23
|
+
$$
|
|
24
|
+
|
|
25
|
+
- list item one with **bold**
|
|
26
|
+
- list item two
|
|
27
|
+
|
|
28
|
+
| col a | col b |
|
|
29
|
+
| ----- | ----- |
|
|
30
|
+
| 1 | 2 |
|
|
31
|
+
|
|
32
|
+
~~~js
|
|
33
|
+
const s = \`template \${value}\`
|
|
34
|
+
~~~
|
|
35
|
+
|
|
36
|
+
Final paragraph with ~~strike~~ and unfinished [link text](https://exa
|
|
37
|
+
`
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Render-equivalence oracle: full-text remend and tail-bounded remend may
|
|
41
|
+
* differ in raw string output ONLY in ways that cannot affect rendering —
|
|
42
|
+
* i.e. after block splitting, every block must be identical. (Streamdown
|
|
43
|
+
* renders blocks independently, so block-level equality IS render equality.)
|
|
44
|
+
*/
|
|
45
|
+
function blocksOf(text: string): string[] {
|
|
46
|
+
return parseMarkdownIntoBlocks(text)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('tailBoundedRemend', () => {
|
|
50
|
+
it('matches full remend block output at every streaming prefix', () => {
|
|
51
|
+
for (let end = 1; end <= CORPUS.length; end++) {
|
|
52
|
+
const prefix = CORPUS.slice(0, end)
|
|
53
|
+
const full = blocksOf(remend(prefix))
|
|
54
|
+
const tail = blocksOf(tailBoundedRemend(prefix))
|
|
55
|
+
|
|
56
|
+
expect(tail, `prefix length ${end}: ${JSON.stringify(prefix.slice(-60))}`).toEqual(full)
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('repairs an unclosed fence opened early in a long message', () => {
|
|
61
|
+
const text = `intro\n\n\`\`\`python\n${'x = 1\n'.repeat(500)}print("$dollar")`
|
|
62
|
+
const repaired = tailBoundedRemend(text)
|
|
63
|
+
|
|
64
|
+
expect(blocksOf(repaired)).toEqual(blocksOf(remend(text)))
|
|
65
|
+
// the window must reach back to the fence opener
|
|
66
|
+
expect(findRemendWindowStart(text)).toBe(text.indexOf('```python'))
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('bounds the window to the tail paragraph when no fence is open', () => {
|
|
70
|
+
const text = `para one\n\npara two\n\npara three with **bold`
|
|
71
|
+
const start = findRemendWindowStart(text)
|
|
72
|
+
|
|
73
|
+
expect(start).toBe(text.indexOf('para three'))
|
|
74
|
+
expect(tailBoundedRemend(text)).toBe(remend(text))
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('widens the window across an open $$ math block', () => {
|
|
78
|
+
const text = `before\n\n$$\n\\frac{a}{b}`
|
|
79
|
+
const start = findRemendWindowStart(text)
|
|
80
|
+
|
|
81
|
+
expect(start).toBeLessThanOrEqual(text.indexOf('$$'))
|
|
82
|
+
expect(blocksOf(tailBoundedRemend(text))).toEqual(blocksOf(remend(text)))
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('handles closed constructs without modification', () => {
|
|
86
|
+
const text = `done **bold** and \`code\`\n\n\`\`\`js\nconst a = 1\n\`\`\`\n\nlast line.`
|
|
87
|
+
|
|
88
|
+
expect(tailBoundedRemend(text)).toBe(text)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('intentionally diverges from full remend on cross-block dangling openers', () => {
|
|
92
|
+
// Full remend scans the whole document and appends `**` for an opener
|
|
93
|
+
// left dangling in an EARLIER block, dumping stray asterisks into the
|
|
94
|
+
// unrelated tail block ("|**"). Because Streamdown splits into blocks
|
|
95
|
+
// after the repair, that opener never renders as bold either way — the
|
|
96
|
+
// tail-bounded result is the cleaner of the two. This test documents
|
|
97
|
+
// the divergence so a future remend upgrade that changes the behavior
|
|
98
|
+
// gets noticed.
|
|
99
|
+
const text = `- item with **dangling\n- item two\n\n|`
|
|
100
|
+
|
|
101
|
+
expect(remend(text).endsWith('|**')).toBe(true)
|
|
102
|
+
expect(tailBoundedRemend(text).endsWith('|')).toBe(true)
|
|
103
|
+
expect(tailBoundedRemend(text).endsWith('|**')).toBe(false)
|
|
104
|
+
})
|
|
105
|
+
})
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import remend from 'remend'
|
|
2
|
+
|
|
3
|
+
// Tail-bounded incomplete-markdown repair.
|
|
4
|
+
//
|
|
5
|
+
// Streamdown's built-in `parseIncompleteMarkdown` runs `remend` over the whole
|
|
6
|
+
// accumulated message on every streaming flush (~18% of script time on 50KB+
|
|
7
|
+
// messages). But repairs only ever matter in the trailing block: inline
|
|
8
|
+
// constructs can't cross a blank line, and Streamdown splits into blocks AFTER
|
|
9
|
+
// the repair, so a dangling opener in an earlier block can't reach the tail.
|
|
10
|
+
// We run `remend` on just that block instead.
|
|
11
|
+
|
|
12
|
+
const BACKTICK = 96 // `
|
|
13
|
+
const TILDE = 126 // ~
|
|
14
|
+
const SPACE = 32
|
|
15
|
+
const TAB = 9
|
|
16
|
+
const BACKSLASH = 92
|
|
17
|
+
|
|
18
|
+
const isSpace = (c: number) => c === SPACE || c === TAB
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Index of the last top-level block start — the char after the most recent
|
|
22
|
+
* blank line that sits outside any open code fence or `$$` math block. An
|
|
23
|
+
* unclosed fence/math always begins after that blank, so it stays wholly
|
|
24
|
+
* inside the window without separate tracking. One cheap char pass, no regex.
|
|
25
|
+
*/
|
|
26
|
+
export function findRemendWindowStart(text: string): number {
|
|
27
|
+
const n = text.length
|
|
28
|
+
let inFence = false
|
|
29
|
+
let fenceChar = 0
|
|
30
|
+
let fenceRun = 0
|
|
31
|
+
let inMath = false
|
|
32
|
+
let boundary = 0
|
|
33
|
+
let pending = -1 // a blank line, committed to `boundary` once content follows
|
|
34
|
+
|
|
35
|
+
for (let lineStart = 0; lineStart <= n; ) {
|
|
36
|
+
let lineEnd = text.indexOf('\n', lineStart)
|
|
37
|
+
|
|
38
|
+
if (lineEnd === -1) {
|
|
39
|
+
lineEnd = n
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let i = lineStart
|
|
43
|
+
|
|
44
|
+
while (i < lineEnd && isSpace(text.charCodeAt(i))) {
|
|
45
|
+
i += 1
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const first = i < lineEnd ? text.charCodeAt(i) : -1
|
|
49
|
+
let marker = false
|
|
50
|
+
|
|
51
|
+
// Fence open/close (``` or ~~~, ≤3 spaces indent).
|
|
52
|
+
if ((first === BACKTICK || first === TILDE) && i - lineStart <= 3) {
|
|
53
|
+
let run = i
|
|
54
|
+
|
|
55
|
+
while (run < lineEnd && text.charCodeAt(run) === first) {
|
|
56
|
+
run += 1
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (run - i >= 3) {
|
|
60
|
+
marker = true
|
|
61
|
+
|
|
62
|
+
if (!inFence) {
|
|
63
|
+
inFence = true
|
|
64
|
+
fenceChar = first
|
|
65
|
+
fenceRun = run - i
|
|
66
|
+
} else if (first === fenceChar && run - i >= fenceRun && onlyWhitespace(text, run, lineEnd)) {
|
|
67
|
+
inFence = false
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Toggle `$$` math state on plain lines ($$ inside a fence is literal).
|
|
73
|
+
if (!inFence && !marker) {
|
|
74
|
+
for (let s = text.indexOf('$$', lineStart); s !== -1 && s < lineEnd - 1; s = text.indexOf('$$', s + 2)) {
|
|
75
|
+
if (s === 0 || text.charCodeAt(s - 1) !== BACKSLASH) {
|
|
76
|
+
inMath = !inMath
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (first === -1 && !inFence && !inMath) {
|
|
82
|
+
pending = lineEnd + 1
|
|
83
|
+
} else if (pending !== -1) {
|
|
84
|
+
boundary = pending
|
|
85
|
+
pending = -1
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
lineStart = lineEnd + 1
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return boundary
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function onlyWhitespace(text: string, from: number, to: number): boolean {
|
|
95
|
+
for (let i = from; i < to; i += 1) {
|
|
96
|
+
if (!isSpace(text.charCodeAt(i))) {
|
|
97
|
+
return false
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return true
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function tailBoundedRemend(text: string): string {
|
|
105
|
+
const start = findRemendWindowStart(text)
|
|
106
|
+
|
|
107
|
+
return start <= 0 ? remend(text) : text.slice(0, start) + remend(text.slice(start))
|
|
108
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { interpretRuntimeReadiness } from './runtime-readiness'
|
|
4
|
+
|
|
5
|
+
describe('interpretRuntimeReadiness', () => {
|
|
6
|
+
it('prefers runtime_check when both signals exist', () => {
|
|
7
|
+
const result = interpretRuntimeReadiness({
|
|
8
|
+
setup: { provider_configured: false },
|
|
9
|
+
setupError: null,
|
|
10
|
+
runtime: { ok: true },
|
|
11
|
+
runtimeError: null
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
expect(result).toEqual({
|
|
15
|
+
checksDisagree: true,
|
|
16
|
+
ready: true,
|
|
17
|
+
reason: null,
|
|
18
|
+
source: 'runtime_check'
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('surfaces runtime mismatch details when runtime_check fails', () => {
|
|
23
|
+
const result = interpretRuntimeReadiness({
|
|
24
|
+
setup: { provider_configured: true },
|
|
25
|
+
setupError: null,
|
|
26
|
+
runtime: { error: 'No provider can serve the selected model.', ok: false },
|
|
27
|
+
runtimeError: null
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
expect(result.ready).toBe(false)
|
|
31
|
+
expect(result.source).toBe('runtime_check')
|
|
32
|
+
expect(result.checksDisagree).toBe(true)
|
|
33
|
+
expect(result.reason).toContain('No provider can serve the selected model.')
|
|
34
|
+
expect(result.reason).toContain('setup.status reports configured credentials')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('falls back to setup.status when runtime_check has no boolean result', () => {
|
|
38
|
+
const result = interpretRuntimeReadiness({
|
|
39
|
+
setup: { provider_configured: true },
|
|
40
|
+
setupError: null,
|
|
41
|
+
runtime: null,
|
|
42
|
+
runtimeError: 'runtime check RPC unavailable'
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
expect(result).toEqual({
|
|
46
|
+
checksDisagree: false,
|
|
47
|
+
ready: true,
|
|
48
|
+
reason: null,
|
|
49
|
+
source: 'setup_status'
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('uses explicit fallback when both checks are missing', () => {
|
|
54
|
+
const result = interpretRuntimeReadiness({
|
|
55
|
+
setup: null,
|
|
56
|
+
setupError: 'setup.status timeout',
|
|
57
|
+
runtime: null,
|
|
58
|
+
runtimeError: 'setup.runtime_check timeout'
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
expect(result.ready).toBe(false)
|
|
62
|
+
expect(result.source).toBe('fallback')
|
|
63
|
+
expect(result.reason).toBe('setup.runtime_check timeout')
|
|
64
|
+
})
|
|
65
|
+
})
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
export interface SetupStatusSnapshot {
|
|
2
|
+
provider_configured?: boolean
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export interface RuntimeCheckSnapshot {
|
|
6
|
+
error?: string
|
|
7
|
+
ok?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface RuntimeReadinessSignals {
|
|
11
|
+
setup: null | SetupStatusSnapshot
|
|
12
|
+
setupError: null | string
|
|
13
|
+
runtime: null | RuntimeCheckSnapshot
|
|
14
|
+
runtimeError: null | string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface RuntimeReadinessOptions {
|
|
18
|
+
defaultReason?: string
|
|
19
|
+
unknownReady?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface RuntimeReadinessResult {
|
|
23
|
+
checksDisagree: boolean
|
|
24
|
+
ready: boolean
|
|
25
|
+
reason: null | string
|
|
26
|
+
source: 'fallback' | 'runtime_check' | 'setup_status'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type RuntimeReadinessRequester = <T = unknown>(method: string, params?: Record<string, unknown>) => Promise<T>
|
|
30
|
+
|
|
31
|
+
const DEFAULT_NOT_READY_REASON = 'Add a provider credential before sending your first message.'
|
|
32
|
+
|
|
33
|
+
function toErrorMessage(error: unknown): null | string {
|
|
34
|
+
if (error instanceof Error) {
|
|
35
|
+
return error.message
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (typeof error === 'string') {
|
|
39
|
+
return error
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (error === null || error === undefined) {
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return String(error)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeMessage(value: null | string | undefined): null | string {
|
|
50
|
+
const next = value?.trim()
|
|
51
|
+
|
|
52
|
+
return next ? next : null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function requestWithFallback<T>(
|
|
56
|
+
requestGateway: RuntimeReadinessRequester,
|
|
57
|
+
method: string
|
|
58
|
+
): Promise<{ error: null | string; value: null | T }> {
|
|
59
|
+
try {
|
|
60
|
+
return { error: null, value: await requestGateway<T>(method) }
|
|
61
|
+
} catch (error) {
|
|
62
|
+
return { error: toErrorMessage(error), value: null }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function fetchRuntimeReadinessSignals(
|
|
67
|
+
requestGateway: RuntimeReadinessRequester
|
|
68
|
+
): Promise<RuntimeReadinessSignals> {
|
|
69
|
+
const [setup, runtime] = await Promise.all([
|
|
70
|
+
requestWithFallback<SetupStatusSnapshot>(requestGateway, 'setup.status'),
|
|
71
|
+
requestWithFallback<RuntimeCheckSnapshot>(requestGateway, 'setup.runtime_check')
|
|
72
|
+
])
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
setup: setup.value,
|
|
76
|
+
setupError: setup.error,
|
|
77
|
+
runtime: runtime.value,
|
|
78
|
+
runtimeError: runtime.error
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function interpretRuntimeReadiness(
|
|
83
|
+
signals: RuntimeReadinessSignals,
|
|
84
|
+
options: RuntimeReadinessOptions = {}
|
|
85
|
+
): RuntimeReadinessResult {
|
|
86
|
+
const defaultReason = options.defaultReason ?? DEFAULT_NOT_READY_REASON
|
|
87
|
+
const unknownReady = options.unknownReady ?? false
|
|
88
|
+
|
|
89
|
+
const setupConfigured =
|
|
90
|
+
typeof signals.setup?.provider_configured === 'boolean' ? Boolean(signals.setup.provider_configured) : undefined
|
|
91
|
+
|
|
92
|
+
const runtimeOk = typeof signals.runtime?.ok === 'boolean' ? Boolean(signals.runtime.ok) : undefined
|
|
93
|
+
const runtimeFailure = normalizeMessage(signals.runtime?.error) ?? normalizeMessage(signals.runtimeError)
|
|
94
|
+
const setupFailure = normalizeMessage(signals.setupError)
|
|
95
|
+
|
|
96
|
+
const checksDisagree =
|
|
97
|
+
typeof setupConfigured === 'boolean' && typeof runtimeOk === 'boolean' && setupConfigured !== runtimeOk
|
|
98
|
+
|
|
99
|
+
if (typeof runtimeOk === 'boolean') {
|
|
100
|
+
if (runtimeOk) {
|
|
101
|
+
return {
|
|
102
|
+
checksDisagree,
|
|
103
|
+
ready: true,
|
|
104
|
+
reason: null,
|
|
105
|
+
source: 'runtime_check'
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let reason = runtimeFailure ?? defaultReason
|
|
110
|
+
|
|
111
|
+
if (checksDisagree && setupConfigured) {
|
|
112
|
+
reason = `${reason} setup.status reports configured credentials, but runtime resolution still failed.`
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
checksDisagree,
|
|
117
|
+
ready: false,
|
|
118
|
+
reason,
|
|
119
|
+
source: 'runtime_check'
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (typeof setupConfigured === 'boolean') {
|
|
124
|
+
return {
|
|
125
|
+
checksDisagree: false,
|
|
126
|
+
ready: setupConfigured,
|
|
127
|
+
reason: setupConfigured ? null : (runtimeFailure ?? setupFailure ?? defaultReason),
|
|
128
|
+
source: 'setup_status'
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
checksDisagree: false,
|
|
134
|
+
ready: unknownReady,
|
|
135
|
+
reason: unknownReady ? null : (runtimeFailure ?? setupFailure ?? defaultReason),
|
|
136
|
+
source: 'fallback'
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function evaluateRuntimeReadiness(
|
|
141
|
+
requestGateway: RuntimeReadinessRequester,
|
|
142
|
+
options: RuntimeReadinessOptions = {}
|
|
143
|
+
): Promise<RuntimeReadinessResult> {
|
|
144
|
+
const signals = await fetchRuntimeReadinessSignals(requestGateway)
|
|
145
|
+
|
|
146
|
+
return interpretRuntimeReadiness(signals, options)
|
|
147
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { SessionInfo } from '@/nastech'
|
|
2
|
+
import { getSessionMessages } from '@/nastech'
|
|
3
|
+
import { translateNow } from '@/i18n'
|
|
4
|
+
import { notify, notifyError } from '@/store/notifications'
|
|
5
|
+
|
|
6
|
+
interface ExportSessionParams {
|
|
7
|
+
sessionId: string
|
|
8
|
+
title?: string | null
|
|
9
|
+
session?: SessionInfo
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function sanitizeFilenamePart(value: string) {
|
|
13
|
+
return value
|
|
14
|
+
.trim()
|
|
15
|
+
.toLowerCase()
|
|
16
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
17
|
+
.replace(/^-+|-+$/g, '')
|
|
18
|
+
.slice(0, 48)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function sessionExportFilename(sessionId: string, title?: string | null) {
|
|
22
|
+
const titlePart = title ? sanitizeFilenamePart(title) : ''
|
|
23
|
+
const idPart = sanitizeFilenamePart(sessionId).slice(0, 8) || 'session'
|
|
24
|
+
|
|
25
|
+
return `${titlePart || 'session'}-${idPart}.json`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function exportSession(sessionId: string, params: Omit<ExportSessionParams, 'sessionId'> = {}) {
|
|
29
|
+
if (!sessionId) {
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const { messages } = await getSessionMessages(sessionId)
|
|
35
|
+
|
|
36
|
+
const payload = {
|
|
37
|
+
exported_at: new Date().toISOString(),
|
|
38
|
+
session_id: sessionId,
|
|
39
|
+
title: params.title ?? null,
|
|
40
|
+
session: params.session ?? null,
|
|
41
|
+
message_count: messages.length,
|
|
42
|
+
messages
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' })
|
|
46
|
+
const downloadUrl = URL.createObjectURL(blob)
|
|
47
|
+
const anchor = document.createElement('a')
|
|
48
|
+
anchor.href = downloadUrl
|
|
49
|
+
anchor.download = sessionExportFilename(sessionId, params.title)
|
|
50
|
+
anchor.click()
|
|
51
|
+
URL.revokeObjectURL(downloadUrl)
|
|
52
|
+
|
|
53
|
+
notify({ kind: 'success', message: translateNow('desktop.sessionExported'), durationMs: 2_000 })
|
|
54
|
+
} catch (err) {
|
|
55
|
+
notifyError(err, translateNow('desktop.sessionExportFailed'))
|
|
56
|
+
}
|
|
57
|
+
}
|