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,386 @@
|
|
|
1
|
+
import { isLikelyProseFence, sanitizeLanguageTag } from '@/lib/markdown-code'
|
|
2
|
+
import { stripPreviewTargets } from '@/lib/preview-targets'
|
|
3
|
+
|
|
4
|
+
const REASONING_BLOCK_RE = /<(think|thinking|reasoning|scratchpad|analysis)>[\s\S]*?<\/\1>\s*/gi
|
|
5
|
+
const PREVIEW_MARKER_RE = /\[Preview:[^\]]+\]\(#preview[:/][^)]+\)/gi
|
|
6
|
+
|
|
7
|
+
const FENCE_LINE_RE = /^([ \t]*)(`{3,}|~{3,})([^\n]*)$/
|
|
8
|
+
const EMPTY_FENCE_BLOCK_RE = /(^|\n)[ \t]*(?:`{3,}|~{3,})[^\n]*\n[ \t]*(?:`{3,}|~{3,})[ \t]*(?=\n|$)/g
|
|
9
|
+
const CODE_FENCE_SPLIT_RE = /((?:```|~~~)[\s\S]*?(?:```|~~~))/g
|
|
10
|
+
const INLINE_CODE_SPLIT_RE = /(`[^`\n]+`)/g
|
|
11
|
+
// Bare-URL autolink matcher. The character classes EXCLUDE `*` so a URL that
|
|
12
|
+
// abuts markdown emphasis with no separating space (e.g. `**label: https://x**`,
|
|
13
|
+
// a very common LLM pattern) doesn't swallow the trailing `**` into the href.
|
|
14
|
+
// `*` is never meaningful in a real URL path, and GFM's own autolink extension
|
|
15
|
+
// likewise strips trailing emphasis/punctuation — so dropping it here is safe
|
|
16
|
+
// and keeps the emphasis run intact. Other trailing punctuation is still peeled
|
|
17
|
+
// off by the final `[^\s<>"'`*.,;:!?]` class.
|
|
18
|
+
const RAW_URL_RE = /https?:\/\/[^\s<>"'`*]+[^\s<>"'`*.,;:!?]/g
|
|
19
|
+
const LOCAL_PREVIEW_URL_RE = /(^|\s)https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(?::\d+)?\/?[^\s<>"'`]*/gi
|
|
20
|
+
const LOCAL_PREVIEW_ONLY_RE = /^https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(?::\d+)?\/?$/i
|
|
21
|
+
const URL_ONLY_LINE_RE = /^\s*https?:\/\/\S+\s*$/i
|
|
22
|
+
const CITATION_MARKER_RE = /(?<=[\p{L}\p{N})\].,!?:;"'”’])\[(?:\d+(?:\s*,\s*\d+)*)\](?!\()/gu
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Returns true when `body` contains a line that's exactly `marker` (modulo
|
|
26
|
+
* leading/trailing horizontal whitespace) — i.e. an unambiguous close fence
|
|
27
|
+
* for an opening fence with the same marker.
|
|
28
|
+
*
|
|
29
|
+
* Implemented with string comparisons (not RegExp) so that input-derived
|
|
30
|
+
* `marker` values can never bleed into a regex pattern. This matters for
|
|
31
|
+
* CodeQL's `js/incomplete-hostname-regexp` dataflow, which would otherwise
|
|
32
|
+
* trace test-fixture URLs from the input through `marker` into the regex
|
|
33
|
+
* source, even though `marker` is captured by `(`{3,}|~{3,})` and can only
|
|
34
|
+
* ever be backticks or tildes.
|
|
35
|
+
*/
|
|
36
|
+
function hasCloseFenceLine(body: string, marker: string): boolean {
|
|
37
|
+
const lines = body.split('\n')
|
|
38
|
+
|
|
39
|
+
// Original regex required `\n` immediately before the close fence, so the
|
|
40
|
+
// first line of `body` (which has no preceding newline within `body`)
|
|
41
|
+
// cannot itself be the close fence.
|
|
42
|
+
for (let i = 1; i < lines.length; i += 1) {
|
|
43
|
+
const line = lines[i]
|
|
44
|
+
let lo = 0
|
|
45
|
+
let hi = line.length
|
|
46
|
+
|
|
47
|
+
while (lo < hi && (line[lo] === ' ' || line[lo] === '\t')) {
|
|
48
|
+
lo += 1
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
while (hi > lo && (line[hi - 1] === ' ' || line[hi - 1] === '\t')) {
|
|
52
|
+
hi -= 1
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (line.slice(lo, hi) === marker) {
|
|
56
|
+
return true
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return false
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function scrubBacktickNoise(text: string): string {
|
|
64
|
+
const balancedFenceRe = /(^|\n)([ \t]*)(`{3,}|~{3,})([^\n]*)\n([\s\S]*?)\n[ \t]*\3[ \t]*(?=\n|$)/g
|
|
65
|
+
const protectedRanges: { end: number; start: number }[] = []
|
|
66
|
+
let match: RegExpExecArray | null
|
|
67
|
+
|
|
68
|
+
while ((match = balancedFenceRe.exec(text)) !== null) {
|
|
69
|
+
const start = match.index + match[1].length
|
|
70
|
+
|
|
71
|
+
protectedRanges.push({ end: balancedFenceRe.lastIndex, start })
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const danglingCodeFenceRe = /(^|\n)[ \t]*(`{3,}|~{3,})([a-z0-9][a-z0-9+#-]{0,15})[ \t]*\n([\s\S]*)$/gi
|
|
75
|
+
|
|
76
|
+
while ((match = danglingCodeFenceRe.exec(text)) !== null) {
|
|
77
|
+
const start = match.index + match[1].length
|
|
78
|
+
const marker = match[2] || '```'
|
|
79
|
+
const info = match[3] || ''
|
|
80
|
+
const body = match[4] || ''
|
|
81
|
+
|
|
82
|
+
if (!hasCloseFenceLine(body, marker) && sanitizeLanguageTag(info) && !isLikelyProseFence(info, body)) {
|
|
83
|
+
protectedRanges.push({ end: text.length, start })
|
|
84
|
+
|
|
85
|
+
break
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
protectedRanges.sort((a, b) => a.start - b.start)
|
|
90
|
+
|
|
91
|
+
const fenceNoiseRe = /`{3,}/g
|
|
92
|
+
let out = ''
|
|
93
|
+
let cursor = 0
|
|
94
|
+
|
|
95
|
+
for (const range of protectedRanges) {
|
|
96
|
+
out += text.slice(cursor, range.start).replace(fenceNoiseRe, '')
|
|
97
|
+
out += text.slice(range.start, range.end)
|
|
98
|
+
cursor = range.end
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
out += text.slice(cursor).replace(fenceNoiseRe, '')
|
|
102
|
+
|
|
103
|
+
for (let pass = 0; pass < 2; pass += 1) {
|
|
104
|
+
// Match EXACTLY 2 backticks (not part of a longer run) on each side.
|
|
105
|
+
// Without the lookbehind/lookahead, two adjacent triple-backtick
|
|
106
|
+
// fences with only whitespace between them get spliced together —
|
|
107
|
+
// e.g. ```bash\n...\n```\n\n```latex matches the regex's
|
|
108
|
+
// last-2-of-bash-close + \n\n + first-2-of-latex-open and the
|
|
109
|
+
// surrounding fence markers collapse into a single longer block,
|
|
110
|
+
// which the markdown parser then treats as ONE giant code block.
|
|
111
|
+
out = out.replace(/(?<!`)``(?!`)\s*(?<!`)``(?!`)/g, '')
|
|
112
|
+
out = out.replace(/(^|[^`])``(?=\s|[.,;:!?)\]'"\u2014\u2013-]|$)/g, '$1')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return out
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function stripEmptyFenceBlocks(text: string): string {
|
|
119
|
+
return text.replace(EMPTY_FENCE_BLOCK_RE, '$1')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isUrlOnlyBlock(lines: string[]): boolean {
|
|
123
|
+
const nonEmpty = lines.filter(line => line.trim())
|
|
124
|
+
|
|
125
|
+
return nonEmpty.length > 0 && nonEmpty.every(line => URL_ONLY_LINE_RE.test(line))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function autoLinkRawUrls(text: string): string {
|
|
129
|
+
return text.replace(RAW_URL_RE, (url: string, index: number) => {
|
|
130
|
+
const previous = text[index - 1] || ''
|
|
131
|
+
const beforePrevious = text[index - 2] || ''
|
|
132
|
+
|
|
133
|
+
if (previous === '<' || (beforePrevious === ']' && previous === '(')) {
|
|
134
|
+
return url
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return `<${url}>`
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function normalizeVisibleProse(text: string): string {
|
|
142
|
+
return text
|
|
143
|
+
.split(INLINE_CODE_SPLIT_RE)
|
|
144
|
+
.map(part =>
|
|
145
|
+
part.startsWith('`')
|
|
146
|
+
? part
|
|
147
|
+
: autoLinkRawUrls(
|
|
148
|
+
part.replace(/`{3,}/g, '').replace(LOCAL_PREVIEW_URL_RE, '$1').replace(CITATION_MARKER_RE, '')
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
.join('')
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function pushProseFence(out: string[], indent: string, info: string, lines: string[]) {
|
|
155
|
+
if (info) {
|
|
156
|
+
out.push(`${indent}${info}`.trimEnd())
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
out.push(...lines)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function findClosingFence(lines: string[], start: number, marker: string): number {
|
|
163
|
+
for (let cursor = start + 1; cursor < lines.length; cursor += 1) {
|
|
164
|
+
const closeMatch = (lines[cursor] || '').match(FENCE_LINE_RE)
|
|
165
|
+
|
|
166
|
+
if (!closeMatch) {
|
|
167
|
+
continue
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const closeMarker = closeMatch[2] || ''
|
|
171
|
+
const closeInfo = (closeMatch[3] || '').trim()
|
|
172
|
+
|
|
173
|
+
if (!closeInfo && closeMarker[0] === marker[0] && closeMarker.length >= marker.length) {
|
|
174
|
+
return cursor
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return -1
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Languages that should be routed to the math (KaTeX) renderer instead of
|
|
182
|
+
// being shown as a syntax-highlighted code block.
|
|
183
|
+
//
|
|
184
|
+
// We deliberately recognize ONLY `math` here, not `latex` or `tex`.
|
|
185
|
+
// Reasoning: GitHub-style markdown uses ` ```math ` to mean "render as
|
|
186
|
+
// math" and ` ```latex `/` ```tex ` to mean "show LaTeX/TeX source code"
|
|
187
|
+
// (syntax highlighted). Conflating the two breaks code blocks where a
|
|
188
|
+
// user is *discussing* LaTeX rather than embedding it (e.g.,
|
|
189
|
+
// ```latex\n\begin{equation}\n E = mc^2\n\end{equation}``` shown as a
|
|
190
|
+
// teaching example). Anyone who wants math rendered should use ```math.
|
|
191
|
+
const MATH_FENCE_LANGUAGES = new Set(['math'])
|
|
192
|
+
|
|
193
|
+
function isMathFence(language: string): boolean {
|
|
194
|
+
return MATH_FENCE_LANGUAGES.has(language.toLowerCase())
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function normalizeFenceBlocks(text: string): string {
|
|
198
|
+
const sourceLines = text.split('\n')
|
|
199
|
+
const out: string[] = []
|
|
200
|
+
let index = 0
|
|
201
|
+
|
|
202
|
+
while (index < sourceLines.length) {
|
|
203
|
+
const line = sourceLines[index] || ''
|
|
204
|
+
const match = line.match(FENCE_LINE_RE)
|
|
205
|
+
|
|
206
|
+
if (!match) {
|
|
207
|
+
out.push(line)
|
|
208
|
+
index += 1
|
|
209
|
+
|
|
210
|
+
continue
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const indent = match[1] || ''
|
|
214
|
+
const marker = match[2] || '```'
|
|
215
|
+
const infoRaw = (match[3] || '').trim()
|
|
216
|
+
const languageToken = infoRaw.split(/\s+/, 1)[0] || ''
|
|
217
|
+
const language = sanitizeLanguageTag(languageToken)
|
|
218
|
+
const openerValid = !infoRaw || Boolean(language)
|
|
219
|
+
|
|
220
|
+
if (!openerValid) {
|
|
221
|
+
out.push(`${indent}${infoRaw}`.trimEnd())
|
|
222
|
+
index += 1
|
|
223
|
+
|
|
224
|
+
continue
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const closeIndex = findClosingFence(sourceLines, index, marker)
|
|
228
|
+
const bodyLines = sourceLines.slice(index + 1, closeIndex === -1 ? sourceLines.length : closeIndex)
|
|
229
|
+
const body = bodyLines.join('\n')
|
|
230
|
+
|
|
231
|
+
if (closeIndex !== -1 && !body.trim()) {
|
|
232
|
+
index = closeIndex + 1
|
|
233
|
+
|
|
234
|
+
continue
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (closeIndex !== -1 && LOCAL_PREVIEW_ONLY_RE.test(body.trim())) {
|
|
238
|
+
index = closeIndex + 1
|
|
239
|
+
|
|
240
|
+
continue
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (closeIndex !== -1 && isUrlOnlyBlock(bodyLines)) {
|
|
244
|
+
out.push(...bodyLines)
|
|
245
|
+
index = closeIndex + 1
|
|
246
|
+
|
|
247
|
+
continue
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (closeIndex === -1) {
|
|
251
|
+
if (!body.trim()) {
|
|
252
|
+
index += 1
|
|
253
|
+
|
|
254
|
+
continue
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (isLikelyProseFence(infoRaw, body)) {
|
|
258
|
+
pushProseFence(out, indent, infoRaw, bodyLines)
|
|
259
|
+
} else if (isMathFence(language)) {
|
|
260
|
+
// Streaming math fence — rewrite the language tag to "math".
|
|
261
|
+
// remark-math + rehype-katex pick up ```math fenced blocks via
|
|
262
|
+
// the language-math class on the resulting <code> element. We
|
|
263
|
+
// keep the fence intact (instead of converting to $$..$$) so
|
|
264
|
+
// any literal `$$` characters in the body don't collide with
|
|
265
|
+
// an outer math wrapper. No close emitted yet — streaming.
|
|
266
|
+
out.push(`${indent}${marker}math`)
|
|
267
|
+
out.push(...bodyLines)
|
|
268
|
+
} else {
|
|
269
|
+
out.push(`${indent}${marker}${language}`)
|
|
270
|
+
out.push(...bodyLines)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
break
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (isLikelyProseFence(infoRaw, body)) {
|
|
277
|
+
pushProseFence(out, indent, infoRaw, bodyLines)
|
|
278
|
+
index = closeIndex + 1
|
|
279
|
+
|
|
280
|
+
continue
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (isMathFence(language)) {
|
|
284
|
+
// Closed math fence — rewrite the language tag to "math" so
|
|
285
|
+
// rehype-katex's language-math class detection picks it up.
|
|
286
|
+
// Body stays untouched (no $$..$$ rewrite) so authors can write
|
|
287
|
+
// arbitrary LaTeX including `$$display$$` markers without them
|
|
288
|
+
// colliding with our wrapper. Without this rewrite the block
|
|
289
|
+
// would render as a syntax-highlighted "latex" code listing.
|
|
290
|
+
out.push(`${indent}${marker}math`)
|
|
291
|
+
out.push(...bodyLines)
|
|
292
|
+
out.push(`${indent}${marker}`)
|
|
293
|
+
index = closeIndex + 1
|
|
294
|
+
|
|
295
|
+
continue
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
out.push(`${indent}${marker}${language}`)
|
|
299
|
+
out.push(...bodyLines)
|
|
300
|
+
out.push(`${indent}${marker}`)
|
|
301
|
+
index = closeIndex + 1
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return out.join('\n')
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Convert LaTeX bracket delimiters to remark-math's dollar-sign syntax.
|
|
308
|
+
// Models often emit `\(...\)` for inline math and `\[...\]` for display
|
|
309
|
+
// math (the standard LaTeX convention) instead of `$...$` / `$$...$$`.
|
|
310
|
+
// remark-math only natively recognizes the dollar form, so we rewrite at
|
|
311
|
+
// preprocess time. Done with simple non-greedy matches keyed on the
|
|
312
|
+
// escaped-bracket sequences — these are rare enough in non-math content
|
|
313
|
+
// (you'd have to write a literal `\(` followed eventually by a literal
|
|
314
|
+
// `\)` with NO interleaving newline-paragraph-break) that false positives
|
|
315
|
+
// are extremely unlikely.
|
|
316
|
+
const LATEX_INLINE_RE = /\\\(([^\n]+?)\\\)/g
|
|
317
|
+
const LATEX_DISPLAY_RE = /\\\[([\s\S]+?)\\\]/g
|
|
318
|
+
|
|
319
|
+
function rewriteLatexBracketDelimiters(text: string): string {
|
|
320
|
+
return text
|
|
321
|
+
.replace(LATEX_INLINE_RE, (_, body: string) => `$${body}$`)
|
|
322
|
+
.replace(LATEX_DISPLAY_RE, (_, body: string) => `$$${body}$$`)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Escape `$<digit>` patterns so they don't get eaten as math delimiters.
|
|
326
|
+
// Models commonly write currency amounts ($5, $19.99, $1,299) in prose.
|
|
327
|
+
// With `singleDollarTextMath: true`, remark-math is greedy and matches
|
|
328
|
+
// EVERY pair of `$`s — including the open of `$5` to the next `$10`,
|
|
329
|
+
// rendering "5 in my pocket and you have " as italicized math text.
|
|
330
|
+
// The de-facto convention across math-supporting LLM UIs is to treat
|
|
331
|
+
// `$` followed by a digit as currency rather than math, since math
|
|
332
|
+
// expressions almost always start with a letter or `\command`. Trade-
|
|
333
|
+
// off: a math expression like `$5x = 10$` would have its leading 5
|
|
334
|
+
// escaped — annoying but rare. The escape `\$` survives to render as
|
|
335
|
+
// a literal `$` in the final output.
|
|
336
|
+
const CURRENCY_DOLLAR_RE = /(^|[^\\])\$(?=\d)/g
|
|
337
|
+
|
|
338
|
+
function escapeCurrencyDollars(text: string): string {
|
|
339
|
+
return text.replace(CURRENCY_DOLLAR_RE, '$1\\$')
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function preprocessMarkdown(text: string): string {
|
|
343
|
+
const cleaned = text.replace(REASONING_BLOCK_RE, '').replace(PREVIEW_MARKER_RE, '')
|
|
344
|
+
const scrubbed = scrubBacktickNoise(cleaned)
|
|
345
|
+
const normalizedFences = normalizeFenceBlocks(scrubbed)
|
|
346
|
+
const strippedEmptyFences = stripEmptyFenceBlocks(normalizedFences)
|
|
347
|
+
|
|
348
|
+
return strippedEmptyFences
|
|
349
|
+
.split(CODE_FENCE_SPLIT_RE)
|
|
350
|
+
.map(part => {
|
|
351
|
+
// Fence blocks pass through untouched.
|
|
352
|
+
if (/^(?:```|~~~)/.test(part)) {
|
|
353
|
+
return part
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Whitespace-only segments (e.g. the `\n\n` between two adjacent
|
|
357
|
+
// fences) must NOT go through stripPreviewTargets — its internal
|
|
358
|
+
// .trim() would collapse them to '' and glue the surrounding
|
|
359
|
+
// fences together, producing things like ``````math which the
|
|
360
|
+
// markdown parser then reads as a single 6-backtick block.
|
|
361
|
+
if (!part.trim()) {
|
|
362
|
+
return part
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Preserve leading/trailing whitespace around the prose body so
|
|
366
|
+
// that fence-prose-fence sequences keep their blank-line gaps.
|
|
367
|
+
// stripPreviewTargets internally calls .trim() on its result for
|
|
368
|
+
// the benefit of its other (single-segment) callers; here we're
|
|
369
|
+
// operating on a SEGMENT of a larger document where outer
|
|
370
|
+
// whitespace is structural and must survive.
|
|
371
|
+
const leading = part.match(/^\s*/)?.[0] ?? ''
|
|
372
|
+
const trailing = part.match(/\s*$/)?.[0] ?? ''
|
|
373
|
+
|
|
374
|
+
// rewriteLatexBracketDelimiters runs only on prose segments so
|
|
375
|
+
// we don't accidentally touch `\(` inside a code block.
|
|
376
|
+
// escapeCurrencyDollars likewise only runs on prose, so legit
|
|
377
|
+
// `$5` literals inside fenced code stay intact.
|
|
378
|
+
const transformed = normalizeVisibleProse(
|
|
379
|
+
stripPreviewTargets(rewriteLatexBracketDelimiters(escapeCurrencyDollars(part)))
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
return leading + transformed + trailing
|
|
383
|
+
})
|
|
384
|
+
.join('')
|
|
385
|
+
.replace(/[ \t]+\n/g, '\n')
|
|
386
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { $connection } from '@/store/session'
|
|
4
|
+
|
|
5
|
+
import { filePathFromMediaPath, gatewayMediaDataUrl, isRemoteGateway } from './media'
|
|
6
|
+
|
|
7
|
+
describe('isRemoteGateway', () => {
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
$connection.set(null)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('is false with no connection', () => {
|
|
13
|
+
$connection.set(null)
|
|
14
|
+
expect(isRemoteGateway()).toBe(false)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('is false in local mode', () => {
|
|
18
|
+
$connection.set({ mode: 'local' } as never)
|
|
19
|
+
expect(isRemoteGateway()).toBe(false)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('is true in remote mode', () => {
|
|
23
|
+
$connection.set({ mode: 'remote' } as never)
|
|
24
|
+
expect(isRemoteGateway()).toBe(true)
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe('filePathFromMediaPath', () => {
|
|
29
|
+
it('passes through a plain path', () => {
|
|
30
|
+
expect(filePathFromMediaPath('/home/u/.nastech/images/a.png')).toBe('/home/u/.nastech/images/a.png')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('decodes a file:// URL with encoded characters', () => {
|
|
34
|
+
expect(filePathFromMediaPath('file:///tmp/a%20b.png')).toBe('/tmp/a b.png')
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('gatewayMediaDataUrl', () => {
|
|
39
|
+
const api = vi.fn(async () => ({ data_url: 'data:image/png;base64,ZHVtbXk=' }))
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
api.mockClear()
|
|
43
|
+
vi.stubGlobal('window', { NASTECHDesktop: { api } })
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
vi.unstubAllGlobals()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('requests the encoded gateway path and returns the data URL', async () => {
|
|
51
|
+
const url = await gatewayMediaDataUrl('/home/u/.nastech/images/a b.png')
|
|
52
|
+
|
|
53
|
+
expect(url).toBe('data:image/png;base64,ZHVtbXk=')
|
|
54
|
+
expect(api).toHaveBeenCalledWith({
|
|
55
|
+
path: '/api/media?path=%2Fhome%2Fu%2F.nastech%2Fimages%2Fa%20b.png'
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
})
|
package/src/lib/media.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { $connection } from '@/store/session'
|
|
2
|
+
|
|
3
|
+
export type MediaKind = 'audio' | 'image' | 'video' | 'file'
|
|
4
|
+
|
|
5
|
+
interface MediaInfo {
|
|
6
|
+
kind: MediaKind
|
|
7
|
+
mime: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const MEDIA_BY_EXT: Record<string, MediaInfo> = {
|
|
11
|
+
avi: { kind: 'video', mime: 'video/x-msvideo' },
|
|
12
|
+
bmp: { kind: 'image', mime: 'image/bmp' },
|
|
13
|
+
flac: { kind: 'audio', mime: 'audio/flac' },
|
|
14
|
+
gif: { kind: 'image', mime: 'image/gif' },
|
|
15
|
+
jpeg: { kind: 'image', mime: 'image/jpeg' },
|
|
16
|
+
jpg: { kind: 'image', mime: 'image/jpeg' },
|
|
17
|
+
m4a: { kind: 'audio', mime: 'audio/mp4' },
|
|
18
|
+
mkv: { kind: 'video', mime: 'video/x-matroska' },
|
|
19
|
+
mov: { kind: 'video', mime: 'video/quicktime' },
|
|
20
|
+
mp3: { kind: 'audio', mime: 'audio/mpeg' },
|
|
21
|
+
mp4: { kind: 'video', mime: 'video/mp4' },
|
|
22
|
+
ogg: { kind: 'audio', mime: 'audio/ogg' },
|
|
23
|
+
opus: { kind: 'audio', mime: 'audio/ogg; codecs=opus' },
|
|
24
|
+
png: { kind: 'image', mime: 'image/png' },
|
|
25
|
+
svg: { kind: 'image', mime: 'image/svg+xml' },
|
|
26
|
+
wav: { kind: 'audio', mime: 'audio/wav' },
|
|
27
|
+
webm: { kind: 'video', mime: 'video/webm' },
|
|
28
|
+
webp: { kind: 'image', mime: 'image/webp' }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function mediaInfo(path: string): MediaInfo | undefined {
|
|
32
|
+
const ext = path.split(/[?#]/, 1)[0]?.split('.').pop()?.toLowerCase()
|
|
33
|
+
|
|
34
|
+
return ext ? MEDIA_BY_EXT[ext] : undefined
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function mediaKind(path: string): MediaKind {
|
|
38
|
+
return mediaInfo(path)?.kind ?? 'file'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function mediaMime(path: string): string {
|
|
42
|
+
return mediaInfo(path)?.mime ?? 'application/octet-stream'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function mediaName(path: string): string {
|
|
46
|
+
try {
|
|
47
|
+
const url = new URL(path)
|
|
48
|
+
|
|
49
|
+
return url.pathname.split('/').filter(Boolean).pop() || path
|
|
50
|
+
} catch {
|
|
51
|
+
return path.split(/[\\/]/).filter(Boolean).pop() || path
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function mediaMarkdownHref(path: string): string {
|
|
56
|
+
return `#media:${encodeURIComponent(path)}`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function mediaExternalUrl(path: string): string {
|
|
60
|
+
return /^(?:https?|file):/i.test(path) ? path : `file://${path}`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Custom Electron scheme (registered in electron/main.cjs) that streams a local
|
|
64
|
+
// file with Range support. Used for audio/video so playback bypasses the data
|
|
65
|
+
// URL size cap and supports seeking. `path` may be a plain path or `file://…`.
|
|
66
|
+
export function mediaStreamUrl(path: string): string {
|
|
67
|
+
return `NASTECH-media://stream/${encodeURIComponent(filePathFromMediaPath(path))}`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function mediaPathFromMarkdownHref(href?: string): string | null {
|
|
71
|
+
if (!href?.startsWith('#media:')) {
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
return decodeURIComponent(href.slice('#media:'.length))
|
|
77
|
+
} catch {
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function filePathFromMediaPath(path: string): string {
|
|
83
|
+
if (!path.startsWith('file:')) {
|
|
84
|
+
return path
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
return decodeURIComponent(new URL(path).pathname)
|
|
89
|
+
} catch {
|
|
90
|
+
return path.replace(/^file:\/\//, '')
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function mediaDisplayLabel(path: string): string {
|
|
95
|
+
const escaped = mediaName(path).replace(/[[\]\\]/g, '\\$&')
|
|
96
|
+
const kind = mediaKind(path)
|
|
97
|
+
|
|
98
|
+
return `${kind[0].toUpperCase()}${kind.slice(1)}: ${escaped}`
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function isRemoteGateway(): boolean {
|
|
102
|
+
return $connection.get()?.mode === 'remote'
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function gatewayMediaDataUrl(path: string): Promise<string> {
|
|
106
|
+
const result = await window.NASTECHDesktop.api<{ data_url: string }>({
|
|
107
|
+
path: `/api/media?path=${encodeURIComponent(path)}`
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
return result.data_url
|
|
111
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { displayModelName, formatModelStatusLabel, reasoningEffortLabel } from './model-status-label'
|
|
4
|
+
|
|
5
|
+
describe('model-status-label', () => {
|
|
6
|
+
it('formats display names consistently', () => {
|
|
7
|
+
expect(displayModelName('anthropic/claude-opus-4.8-fast')).toBe('Opus 4.8')
|
|
8
|
+
expect(displayModelName('openai/gpt-5.5')).toBe('GPT-5.5')
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('maps reasoning effort to compact labels', () => {
|
|
12
|
+
expect(reasoningEffortLabel('high')).toBe('High')
|
|
13
|
+
expect(reasoningEffortLabel('xhigh')).toBe('Max')
|
|
14
|
+
expect(reasoningEffortLabel('')).toBe('')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('appends fast + effort session state to the status label', () => {
|
|
18
|
+
expect(formatModelStatusLabel('openai/gpt-5.5', { fastMode: true, reasoningEffort: 'high' })).toBe(
|
|
19
|
+
'GPT-5.5 · Fast High'
|
|
20
|
+
)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('always surfaces the effort (default medium) so the level is visible', () => {
|
|
24
|
+
expect(formatModelStatusLabel('openai/gpt-5.5', { reasoningEffort: 'medium' })).toBe('GPT-5.5 · Med')
|
|
25
|
+
expect(formatModelStatusLabel('openai/gpt-5.5')).toBe('GPT-5.5 · Med')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('returns just the placeholder name when there is no model', () => {
|
|
29
|
+
expect(formatModelStatusLabel('')).toBe('No model')
|
|
30
|
+
})
|
|
31
|
+
})
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const REASONING_LABELS: Record<string, string> = {
|
|
2
|
+
none: 'Off',
|
|
3
|
+
minimal: 'Min',
|
|
4
|
+
low: 'Low',
|
|
5
|
+
medium: 'Med',
|
|
6
|
+
high: 'High',
|
|
7
|
+
xhigh: 'Max'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function reasoningEffortLabel(effort: string): string {
|
|
11
|
+
const key = effort.trim().toLowerCase()
|
|
12
|
+
|
|
13
|
+
if (!key) {
|
|
14
|
+
return ''
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return REASONING_LABELS[key] ?? effort
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Strip provider prefix and normalize for display. */
|
|
21
|
+
export function modelBaseId(model: string): string {
|
|
22
|
+
const trimmed = model.trim()
|
|
23
|
+
const slash = trimmed.lastIndexOf('/')
|
|
24
|
+
|
|
25
|
+
return slash >= 0 ? trimmed.slice(slash + 1) : trimmed
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Trailing model-id variants that should render as a grayed tag beside the
|
|
29
|
+
// name (e.g. "Opus 4.8" + "Fast") rather than collapsing two distinct ids to
|
|
30
|
+
// the same display name.
|
|
31
|
+
const VARIANT_TAGS: ReadonlyArray<readonly [RegExp, string]> = [
|
|
32
|
+
[/-fast$/i, 'Fast'],
|
|
33
|
+
[/-thinking$/i, 'Thinking'],
|
|
34
|
+
[/-preview$/i, 'Preview'],
|
|
35
|
+
[/-latest$/i, 'Latest']
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
const titleCase = (text: string): string => text.replace(/\b\w/g, char => char.toUpperCase()).trim()
|
|
39
|
+
|
|
40
|
+
function prettifyBase(base: string): string {
|
|
41
|
+
if (/^claude-/i.test(base)) {
|
|
42
|
+
return titleCase(base.replace(/^claude-/i, '').replace(/-/g, ' '))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (/^gpt-/i.test(base)) {
|
|
46
|
+
return base.replace(/^gpt-/i, 'GPT-')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (/^gemini-/i.test(base)) {
|
|
50
|
+
return base.replace(/^gemini-/i, 'Gemini ').replace(/-/g, ' ')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return titleCase(base.replace(/-/g, ' '))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Split a model id into a clean display name plus an optional grayed variant
|
|
57
|
+
* tag, so distinct ids (e.g. `…-4.8` vs `…-4.8-fast`) don't collapse. */
|
|
58
|
+
export function modelDisplayParts(model: string): { name: string; tag: string } {
|
|
59
|
+
let base = modelBaseId(model)
|
|
60
|
+
let tag = ''
|
|
61
|
+
|
|
62
|
+
for (const [pattern, label] of VARIANT_TAGS) {
|
|
63
|
+
if (pattern.test(base)) {
|
|
64
|
+
tag = label
|
|
65
|
+
base = base.replace(pattern, '')
|
|
66
|
+
|
|
67
|
+
break
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { name: prettifyBase(base) || model.trim() || 'No model', tag }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Friendly one-line model name for menus and the status bar. */
|
|
75
|
+
export function displayModelName(model: string): string {
|
|
76
|
+
return modelDisplayParts(model).name
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Status bar trigger label — model name plus the live session state (effort/fast). */
|
|
80
|
+
export function formatModelStatusLabel(
|
|
81
|
+
model: string,
|
|
82
|
+
options?: { fastMode?: boolean; reasoningEffort?: string }
|
|
83
|
+
): string {
|
|
84
|
+
const name = displayModelName(model)
|
|
85
|
+
|
|
86
|
+
if (!model.trim()) {
|
|
87
|
+
return name
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const parts: string[] = []
|
|
91
|
+
|
|
92
|
+
// Fast is shown when the speed=fast param is on (options.fastMode) OR the
|
|
93
|
+
// active model is a `…-fast` variant (fast via a separate model id).
|
|
94
|
+
if (options?.fastMode || /-fast$/i.test(modelBaseId(model))) {
|
|
95
|
+
parts.push('Fast')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Always surface the effort (empty = NasTech default of medium) so the
|
|
99
|
+
// current reasoning level is visible at a glance, not just when non-default.
|
|
100
|
+
parts.push(reasoningEffortLabel(options?.reasoningEffort ?? '') || 'Med')
|
|
101
|
+
|
|
102
|
+
return `${name} · ${parts.join(' ')}`
|
|
103
|
+
}
|