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,188 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AssistantRuntimeImpl,
|
|
3
|
+
BaseAssistantRuntimeCore,
|
|
4
|
+
ExternalStoreThreadListRuntimeCore,
|
|
5
|
+
ExternalStoreThreadRuntimeCore,
|
|
6
|
+
hasUpcomingMessage
|
|
7
|
+
} from '@assistant-ui/core/internal'
|
|
8
|
+
import {
|
|
9
|
+
type AssistantRuntime,
|
|
10
|
+
type ExternalStoreAdapter,
|
|
11
|
+
type ThreadMessage,
|
|
12
|
+
useRuntimeAdapters
|
|
13
|
+
} from '@assistant-ui/react'
|
|
14
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
15
|
+
|
|
16
|
+
const EMPTY_ARRAY = Object.freeze([])
|
|
17
|
+
|
|
18
|
+
const shallowEqual = (a: object, b: object): boolean => {
|
|
19
|
+
const aKeys = Object.keys(a)
|
|
20
|
+
|
|
21
|
+
if (aKeys.length !== Object.keys(b).length) {
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
for (const key of aKeys) {
|
|
26
|
+
if (a[key as keyof typeof a] !== b[key as keyof typeof b]) {
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return true
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const getThreadListAdapter = (store: ExternalStoreAdapter) => store.adapters?.threadList ?? {}
|
|
35
|
+
|
|
36
|
+
function syncRepositoryIncrementally(
|
|
37
|
+
runtime: ExternalStoreThreadRuntimeCore,
|
|
38
|
+
messageRepository: NonNullable<ExternalStoreAdapter['messageRepository']>
|
|
39
|
+
): readonly ThreadMessage[] {
|
|
40
|
+
const repository = (runtime as unknown as { repository: ExternalStoreThreadRuntimeCore['repository'] }).repository
|
|
41
|
+
const incomingIds = new Set(messageRepository.messages.map(({ message }) => message.id))
|
|
42
|
+
|
|
43
|
+
for (const { message, parentId } of messageRepository.messages) {
|
|
44
|
+
repository.addOrUpdateMessage(parentId, message)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const { message } of repository.export().messages) {
|
|
48
|
+
if (!incomingIds.has(message.id)) {
|
|
49
|
+
repository.deleteMessage(message.id)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const headId = messageRepository.headId ?? messageRepository.messages.at(-1)?.message.id ?? null
|
|
54
|
+
|
|
55
|
+
repository.resetHead(headId)
|
|
56
|
+
|
|
57
|
+
return repository.getMessages()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
class IncrementalExternalStoreThreadRuntimeCore extends ExternalStoreThreadRuntimeCore {
|
|
61
|
+
override __internal_setAdapter(store: ExternalStoreAdapter): void {
|
|
62
|
+
if (!store.messageRepository) {
|
|
63
|
+
super.__internal_setAdapter(store)
|
|
64
|
+
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const self = this as unknown as {
|
|
69
|
+
_assistantOptimisticId: null | string
|
|
70
|
+
_capabilities: object
|
|
71
|
+
_messages: readonly ThreadMessage[]
|
|
72
|
+
_notifyEventSubscribers: (event: string, payload: object) => void
|
|
73
|
+
_notifySubscribers: () => void
|
|
74
|
+
_store?: ExternalStoreAdapter
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (self._store === store) {
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const isRunning = store.isRunning ?? false
|
|
82
|
+
this.isDisabled = store.isDisabled ?? false
|
|
83
|
+
|
|
84
|
+
const oldStore = self._store
|
|
85
|
+
self._store = store
|
|
86
|
+
|
|
87
|
+
if (this.extras !== store.extras) {
|
|
88
|
+
this.extras = store.extras
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const newSuggestions = store.suggestions ?? EMPTY_ARRAY
|
|
92
|
+
|
|
93
|
+
if (!shallowEqual(this.suggestions, newSuggestions)) {
|
|
94
|
+
this.suggestions = newSuggestions
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const newCapabilities = {
|
|
98
|
+
switchToBranch: store.setMessages !== undefined,
|
|
99
|
+
switchBranchDuringRun: false,
|
|
100
|
+
edit: store.onEdit !== undefined,
|
|
101
|
+
reload: store.onReload !== undefined,
|
|
102
|
+
cancel: store.onCancel !== undefined,
|
|
103
|
+
speech: store.adapters?.speech !== undefined,
|
|
104
|
+
dictation: store.adapters?.dictation !== undefined,
|
|
105
|
+
voice: store.adapters?.voice !== undefined,
|
|
106
|
+
unstable_copy: store.unstable_capabilities?.copy !== false,
|
|
107
|
+
attachments: !!store.adapters?.attachments,
|
|
108
|
+
feedback: !!store.adapters?.feedback,
|
|
109
|
+
queue: false
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!shallowEqual(self._capabilities, newCapabilities)) {
|
|
113
|
+
self._capabilities = newCapabilities
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (oldStore && oldStore.isRunning === store.isRunning && oldStore.messageRepository === store.messageRepository) {
|
|
117
|
+
self._notifySubscribers()
|
|
118
|
+
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (self._assistantOptimisticId) {
|
|
123
|
+
this.repository.deleteMessage(self._assistantOptimisticId)
|
|
124
|
+
self._assistantOptimisticId = null
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const messages = syncRepositoryIncrementally(this, store.messageRepository)
|
|
128
|
+
|
|
129
|
+
if (messages.length > 0) {
|
|
130
|
+
this.ensureInitialized()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if ((oldStore?.isRunning ?? false) !== (store.isRunning ?? false)) {
|
|
134
|
+
self._notifyEventSubscribers(store.isRunning ? 'runStart' : 'runEnd', {})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (hasUpcomingMessage(isRunning, messages)) {
|
|
138
|
+
self._assistantOptimisticId = this.repository.appendOptimisticMessage(messages.at(-1)?.id ?? null, {
|
|
139
|
+
role: 'assistant',
|
|
140
|
+
content: []
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this.repository.resetHead(self._assistantOptimisticId ?? messages.at(-1)?.id ?? null)
|
|
145
|
+
self._messages = this.repository.getMessages()
|
|
146
|
+
self._notifySubscribers()
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
class IncrementalExternalStoreRuntimeCore extends BaseAssistantRuntimeCore {
|
|
151
|
+
threads: ExternalStoreThreadListRuntimeCore
|
|
152
|
+
|
|
153
|
+
constructor(adapter: ExternalStoreAdapter) {
|
|
154
|
+
super()
|
|
155
|
+
|
|
156
|
+
this.threads = new ExternalStoreThreadListRuntimeCore(
|
|
157
|
+
getThreadListAdapter(adapter),
|
|
158
|
+
() => new IncrementalExternalStoreThreadRuntimeCore(this._contextProvider, adapter)
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
setAdapter(adapter: ExternalStoreAdapter): void {
|
|
163
|
+
this.threads.__internal_setAdapter(getThreadListAdapter(adapter))
|
|
164
|
+
this.threads.getMainThreadRuntimeCore().__internal_setAdapter(adapter)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function useIncrementalExternalStoreRuntime<T extends ThreadMessage>(
|
|
169
|
+
store: ExternalStoreAdapter<T>
|
|
170
|
+
): AssistantRuntime {
|
|
171
|
+
const [runtime] = useState(() => new IncrementalExternalStoreRuntimeCore(store as ExternalStoreAdapter))
|
|
172
|
+
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
runtime.setAdapter(store as ExternalStoreAdapter)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
const { modelContext } = useRuntimeAdapters() ?? {}
|
|
178
|
+
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
if (!modelContext) {
|
|
181
|
+
return undefined
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return runtime.registerModelContextProvider(modelContext)
|
|
185
|
+
}, [modelContext, runtime])
|
|
186
|
+
|
|
187
|
+
return useMemo(() => new AssistantRuntimeImpl(runtime), [runtime])
|
|
188
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memoizing wrapper around `rehype-katex`.
|
|
3
|
+
*
|
|
4
|
+
* Why: the default `@streamdown/math` plugin runs `rehype-katex` on every
|
|
5
|
+
* markdown commit. During streaming, that means each new token re-runs
|
|
6
|
+
* KaTeX on EVERY math node in the message — including equations that
|
|
7
|
+
* haven't changed since the last token. For math-heavy responses (a
|
|
8
|
+
* model deriving an equation step-by-step) this becomes a major source
|
|
9
|
+
* of jank: 20 unchanged equations each pay ~5–20ms of katex.renderToString
|
|
10
|
+
* work per token, adding up to hundreds of ms of CPU bound work that
|
|
11
|
+
* delays the next streaming update.
|
|
12
|
+
*
|
|
13
|
+
* What this plugin does: walk the hast tree looking for the math nodes
|
|
14
|
+
* that `remark-math` emits (`<code class="math-inline">…</code>` for
|
|
15
|
+
* inline and `<pre><code class="math-display">…</code></pre>` for
|
|
16
|
+
* display), key them by `(displayMode, value)`, and serve them from an
|
|
17
|
+
* in-memory LRU cache when we've rendered the same equation before.
|
|
18
|
+
* Cache misses still go through `katex.renderToString`; cache hits
|
|
19
|
+
* return the previously generated hast subtree.
|
|
20
|
+
*
|
|
21
|
+
* Result: each unique equation only pays the katex cost once. Adding
|
|
22
|
+
* one new equation to a paragraph re-renders just that one equation
|
|
23
|
+
* instead of all of them. The cache is process-global so it survives
|
|
24
|
+
* moves between messages (e.g., re-rendering a session).
|
|
25
|
+
*
|
|
26
|
+
* Compatibility: the produced hast structure matches what `rehype-katex`
|
|
27
|
+
* itself produces — we use the same `hast-util-from-html-isomorphic`
|
|
28
|
+
* fragment parsing and the same parent-splice semantics, including the
|
|
29
|
+
* `<pre>`-walk-up for display mode. Drop-in replacement for the math
|
|
30
|
+
* slot in streamdown's PluginConfig.
|
|
31
|
+
*
|
|
32
|
+
* Wire it in via `createMemoizedMathPlugin`:
|
|
33
|
+
*
|
|
34
|
+
* import { createMemoizedMathPlugin } from '@/lib/katex-memo'
|
|
35
|
+
* const math = createMemoizedMathPlugin({ singleDollarTextMath: true })
|
|
36
|
+
* <Streamdown plugins={{ math }} ... />
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import type { Element, ElementContent, Parent, Root } from 'hast'
|
|
40
|
+
import { fromHtmlIsomorphic } from 'hast-util-from-html-isomorphic'
|
|
41
|
+
import { toText } from 'hast-util-to-text'
|
|
42
|
+
import katex from 'katex'
|
|
43
|
+
import remarkMath from 'remark-math'
|
|
44
|
+
import type { Pluggable } from 'unified'
|
|
45
|
+
import { SKIP, visitParents } from 'unist-util-visit-parents'
|
|
46
|
+
import type { VFile } from 'vfile'
|
|
47
|
+
|
|
48
|
+
interface KatexMemoOptions {
|
|
49
|
+
/**
|
|
50
|
+
* Color used for KaTeX errors when we fall back to the lenient parser.
|
|
51
|
+
* Mirrors `@streamdown/math`'s default so the visual output is identical.
|
|
52
|
+
*/
|
|
53
|
+
errorColor?: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface MathPluginConfig {
|
|
57
|
+
/**
|
|
58
|
+
* Match `singleDollarTextMath` from `@streamdown/math`. When true the
|
|
59
|
+
* remark-math parser treats `$x$` as inline math; when false it requires
|
|
60
|
+
* `$$x$$`. Models almost always emit the single-dollar form, so we
|
|
61
|
+
* default it to true at the createMemoizedMathPlugin call site.
|
|
62
|
+
*/
|
|
63
|
+
singleDollarTextMath?: boolean
|
|
64
|
+
errorColor?: string
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Cached rendered hast — children to splice into the math node's parent. */
|
|
68
|
+
type CachedRender = ElementContent[]
|
|
69
|
+
|
|
70
|
+
const CACHE_LIMIT = 512
|
|
71
|
+
|
|
72
|
+
class LruCache<K, V> {
|
|
73
|
+
private readonly map = new Map<K, V>()
|
|
74
|
+
|
|
75
|
+
get(key: K): undefined | V {
|
|
76
|
+
const value = this.map.get(key)
|
|
77
|
+
|
|
78
|
+
if (value === undefined) {
|
|
79
|
+
return undefined
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Refresh recency by re-inserting at the tail. Map iteration order is
|
|
83
|
+
// insertion order, so the oldest entry is at the head.
|
|
84
|
+
this.map.delete(key)
|
|
85
|
+
this.map.set(key, value)
|
|
86
|
+
|
|
87
|
+
return value
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
set(key: K, value: V): void {
|
|
91
|
+
if (this.map.has(key)) {
|
|
92
|
+
this.map.delete(key)
|
|
93
|
+
} else if (this.map.size >= CACHE_LIMIT) {
|
|
94
|
+
const oldest = this.map.keys().next().value
|
|
95
|
+
|
|
96
|
+
if (oldest !== undefined) {
|
|
97
|
+
this.map.delete(oldest)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this.map.set(key, value)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const cache = new LruCache<string, CachedRender>()
|
|
106
|
+
|
|
107
|
+
function cacheKey(displayMode: boolean, value: string): string {
|
|
108
|
+
// `\u0001` is a control character that (a) won't appear in normal
|
|
109
|
+
// markdown and (b) is a single byte so the join is cheap.
|
|
110
|
+
return `${displayMode ? 'd' : 'i'}\u0001${value}`
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Render one math expression with the same two-pass strategy `rehype-katex`
|
|
115
|
+
* uses internally: try strict first (so genuine TeX errors get reported in
|
|
116
|
+
* the VFile message stream), and on failure fall back to lenient mode so
|
|
117
|
+
* the document still renders without a thrown exception. The lenient
|
|
118
|
+
* fallback paints the equation in `errorColor` instead of erroring out.
|
|
119
|
+
*/
|
|
120
|
+
function renderMath(
|
|
121
|
+
value: string,
|
|
122
|
+
displayMode: boolean,
|
|
123
|
+
errorColor: string,
|
|
124
|
+
file: VFile,
|
|
125
|
+
element: Element
|
|
126
|
+
): ElementContent[] {
|
|
127
|
+
let html: string
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
html = katex.renderToString(value, { displayMode, throwOnError: true })
|
|
131
|
+
} catch (error) {
|
|
132
|
+
const cause = error as Error
|
|
133
|
+
|
|
134
|
+
file.message('Could not render math with KaTeX', {
|
|
135
|
+
cause,
|
|
136
|
+
place: element.position,
|
|
137
|
+
ruleId: cause.name?.toLowerCase() ?? 'katex',
|
|
138
|
+
source: 'rehype-katex-memo'
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
html = katex.renderToString(value, {
|
|
143
|
+
displayMode,
|
|
144
|
+
errorColor,
|
|
145
|
+
strict: 'ignore',
|
|
146
|
+
throwOnError: false
|
|
147
|
+
})
|
|
148
|
+
} catch {
|
|
149
|
+
// Last-resort fallback — render the source text inside a styled span
|
|
150
|
+
// so the user at least sees what was supposed to be there. Mirrors
|
|
151
|
+
// rehype-katex's own escape hatch.
|
|
152
|
+
return [
|
|
153
|
+
{
|
|
154
|
+
type: 'element',
|
|
155
|
+
tagName: 'span',
|
|
156
|
+
properties: {
|
|
157
|
+
className: ['katex-error'],
|
|
158
|
+
style: `color:${errorColor}`,
|
|
159
|
+
title: String(error)
|
|
160
|
+
},
|
|
161
|
+
children: [{ type: 'text', value }]
|
|
162
|
+
}
|
|
163
|
+
]
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const fragment = fromHtmlIsomorphic(html, { fragment: true })
|
|
168
|
+
|
|
169
|
+
return fragment.children as ElementContent[]
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The actual rehype plugin. Wraps `rehype-katex`'s logic with our LRU
|
|
174
|
+
* cache. Mirrors the upstream visitor exactly except for the cache lookup
|
|
175
|
+
* and an LRU.set on miss.
|
|
176
|
+
*/
|
|
177
|
+
function createMemoizedRehypeKatex(options: KatexMemoOptions = {}): Pluggable {
|
|
178
|
+
const errorColor = options.errorColor ?? 'var(--color-muted-foreground)'
|
|
179
|
+
|
|
180
|
+
return () =>
|
|
181
|
+
function transform(tree: Root, file: VFile): undefined {
|
|
182
|
+
visitParents(tree, 'element', (element, parents) => {
|
|
183
|
+
const classes = Array.isArray(element.properties?.className) ? (element.properties.className as string[]) : []
|
|
184
|
+
|
|
185
|
+
// Match the same class set rehype-katex looks for. `language-math`
|
|
186
|
+
// is the markdown ` ```math ` form, `math-inline` is what
|
|
187
|
+
// remark-math emits for `$x$`, `math-display` for `$$x$$`.
|
|
188
|
+
const languageMath = classes.includes('language-math')
|
|
189
|
+
const mathDisplay = classes.includes('math-display')
|
|
190
|
+
const mathInline = classes.includes('math-inline')
|
|
191
|
+
|
|
192
|
+
if (!(languageMath || mathDisplay || mathInline)) {
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let displayMode = mathDisplay
|
|
197
|
+
let scope: Element = element
|
|
198
|
+
let parent: Parent | undefined = parents[parents.length - 1]
|
|
199
|
+
|
|
200
|
+
// For ` ```math ` the scope walks up to the wrapping <pre> and
|
|
201
|
+
// we treat it as display math. Same logic rehype-katex uses.
|
|
202
|
+
if (languageMath && parent && parent.type === 'element' && (parent as Element).tagName === 'pre') {
|
|
203
|
+
scope = parent as Element
|
|
204
|
+
parent = parents[parents.length - 2]
|
|
205
|
+
displayMode = true
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// No parent means the math node is at the root — there's nothing
|
|
209
|
+
// to splice into, so bail. This shouldn't happen for properly
|
|
210
|
+
// nested markdown but is the same defensive guard rehype-katex has.
|
|
211
|
+
if (!parent) {
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const value = toText(scope, { whitespace: 'pre' })
|
|
216
|
+
const key = cacheKey(displayMode, value)
|
|
217
|
+
let cached = cache.get(key)
|
|
218
|
+
|
|
219
|
+
if (!cached) {
|
|
220
|
+
cached = renderMath(value, displayMode, errorColor, file, scope)
|
|
221
|
+
cache.set(key, cached)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Splice CLONES of the cached children into the parent. Reusing
|
|
225
|
+
// the same node instances across renders would let downstream
|
|
226
|
+
// rehype plugins or toJsxRuntime mutate the cached subtree —
|
|
227
|
+
// breaking the next cache hit. structuredClone is ~100µs per
|
|
228
|
+
// equation, well below the ~5–20ms katex.renderToString cost
|
|
229
|
+
// we're avoiding.
|
|
230
|
+
const clonedChildren = cached.map(child => structuredClone(child))
|
|
231
|
+
const index = parent.children.indexOf(scope as ElementContent)
|
|
232
|
+
|
|
233
|
+
if (index === -1) {
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
parent.children.splice(index, 1, ...clonedChildren)
|
|
238
|
+
|
|
239
|
+
return SKIP
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Build a streamdown MathPlugin object that uses the memoized rehype-katex
|
|
246
|
+
* wrapper. Drop-in for `@streamdown/math`'s `createMathPlugin`.
|
|
247
|
+
*/
|
|
248
|
+
export function createMemoizedMathPlugin(config: MathPluginConfig = {}) {
|
|
249
|
+
const remarkPlugin: Pluggable = [remarkMath, { singleDollarTextMath: config.singleDollarTextMath ?? false }]
|
|
250
|
+
|
|
251
|
+
const rehypePlugin = createMemoizedRehypeKatex({ errorColor: config.errorColor })
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
name: 'katex' as const,
|
|
255
|
+
type: 'math' as const,
|
|
256
|
+
remarkPlugin,
|
|
257
|
+
rehypePlugin,
|
|
258
|
+
getStyles: () => 'katex/dist/katex.min.css'
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// The single source of truth for rebindable desktop hotkeys.
|
|
2
|
+
//
|
|
3
|
+
// Each entry is pure metadata: an id, a category, and the default combo(s).
|
|
4
|
+
// Handlers are wired separately in `use-keybinds.ts` (they need React context
|
|
5
|
+
// like navigate / theme); labels come from i18n (`t.keybinds.actions[id]`). To
|
|
6
|
+
// add a hotkey, add a row here and a handler there — nothing else.
|
|
7
|
+
|
|
8
|
+
export type KeybindCategory = 'composer' | 'profiles' | 'session' | 'navigation' | 'view'
|
|
9
|
+
|
|
10
|
+
// The self-referential opener — bound + dispatched like any action, but shown in
|
|
11
|
+
// the panel subtitle (not as its own row).
|
|
12
|
+
export const KEYBIND_PANEL_ACTION = 'keybinds.openPanel'
|
|
13
|
+
|
|
14
|
+
// `composer` is read-only; the rest are rebindable. `view` is the catch-all for
|
|
15
|
+
// layout, appearance, and the panel-opener.
|
|
16
|
+
export const KEYBIND_CATEGORIES: readonly KeybindCategory[] = [
|
|
17
|
+
'composer',
|
|
18
|
+
'profiles',
|
|
19
|
+
'session',
|
|
20
|
+
'navigation',
|
|
21
|
+
'view'
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
export interface KeybindActionMeta {
|
|
25
|
+
id: string
|
|
26
|
+
category: KeybindCategory
|
|
27
|
+
/** Default combos. Empty = shipped unbound (user can assign one). */
|
|
28
|
+
defaults: readonly string[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Positional switch slots for *named* profiles: ⌘1…⌘9 for profiles 1-9, then
|
|
32
|
+
// ⌘⌥1…⌘⌥9 for 10-18. The default profile gets the two-key mnemonic ⌘D (see
|
|
33
|
+
// `profile.default`) — ⌘` is macOS-reserved (window cycling) and ⌘0 is reset-zoom.
|
|
34
|
+
export const PROFILE_SLOT_COUNT = 18
|
|
35
|
+
|
|
36
|
+
function comboForSlot(slot: number): string {
|
|
37
|
+
return slot <= 9 ? `mod+${slot}` : `mod+alt+${slot - 9}`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const PROFILE_SWITCH_ACTIONS: KeybindActionMeta[] = Array.from({ length: PROFILE_SLOT_COUNT }, (_, i) => ({
|
|
41
|
+
id: `profile.switch.${i + 1}`,
|
|
42
|
+
category: 'profiles' as const,
|
|
43
|
+
defaults: [comboForSlot(i + 1)]
|
|
44
|
+
}))
|
|
45
|
+
|
|
46
|
+
export const KEYBIND_ACTIONS: readonly KeybindActionMeta[] = [
|
|
47
|
+
// ── Composer ─────────────────────────────────────────────────────────────
|
|
48
|
+
{ id: 'composer.focus', category: 'composer', defaults: [] },
|
|
49
|
+
{ id: 'composer.modelPicker', category: 'composer', defaults: [] },
|
|
50
|
+
|
|
51
|
+
// ── Profiles ─────────────────────────────────────────────────────────────
|
|
52
|
+
{ id: 'profile.default', category: 'profiles', defaults: ['mod+d'] },
|
|
53
|
+
...PROFILE_SWITCH_ACTIONS,
|
|
54
|
+
{ id: 'profile.next', category: 'profiles', defaults: ['mod+shift+]'] },
|
|
55
|
+
{ id: 'profile.prev', category: 'profiles', defaults: ['mod+shift+['] },
|
|
56
|
+
{ id: 'profile.toggleAll', category: 'profiles', defaults: ['mod+shift+0'] },
|
|
57
|
+
{ id: 'profile.create', category: 'profiles', defaults: [] },
|
|
58
|
+
|
|
59
|
+
// ── Session ──────────────────────────────────────────────────────────────
|
|
60
|
+
{ id: 'session.new', category: 'session', defaults: ['mod+n', 'shift+n'] },
|
|
61
|
+
{ id: 'session.next', category: 'session', defaults: [] },
|
|
62
|
+
{ id: 'session.prev', category: 'session', defaults: [] },
|
|
63
|
+
{ id: 'session.focusSearch', category: 'session', defaults: ['mod+shift+f'] },
|
|
64
|
+
{ id: 'session.togglePin', category: 'session', defaults: [] },
|
|
65
|
+
|
|
66
|
+
// ── Navigation ───────────────────────────────────────────────────────────
|
|
67
|
+
{ id: 'nav.commandPalette', category: 'navigation', defaults: ['mod+k', 'mod+p'] },
|
|
68
|
+
{ id: 'nav.commandCenter', category: 'navigation', defaults: ['mod+.'] },
|
|
69
|
+
{ id: 'nav.settings', category: 'navigation', defaults: ['mod+,'] },
|
|
70
|
+
{ id: 'nav.profiles', category: 'navigation', defaults: [] },
|
|
71
|
+
{ id: 'nav.skills', category: 'navigation', defaults: [] },
|
|
72
|
+
{ id: 'nav.messaging', category: 'navigation', defaults: [] },
|
|
73
|
+
{ id: 'nav.artifacts', category: 'navigation', defaults: [] },
|
|
74
|
+
{ id: 'nav.cron', category: 'navigation', defaults: [] },
|
|
75
|
+
{ id: 'nav.agents', category: 'navigation', defaults: [] },
|
|
76
|
+
|
|
77
|
+
// ── View (layout + appearance + the shortcuts panel itself) ───────────────
|
|
78
|
+
{ id: 'view.toggleSidebar', category: 'view', defaults: ['mod+b'] },
|
|
79
|
+
{ id: 'view.toggleRightSidebar', category: 'view', defaults: ['mod+j'] },
|
|
80
|
+
{ id: 'view.showFiles', category: 'view', defaults: [] },
|
|
81
|
+
{ id: 'view.showTerminal', category: 'view', defaults: [] },
|
|
82
|
+
// ⌘\ — the backslash reads like a mirror line flipping the layout.
|
|
83
|
+
{ id: 'view.flipPanes', category: 'view', defaults: ['mod+\\'] },
|
|
84
|
+
{ id: 'appearance.toggleMode', category: 'view', defaults: ['shift+x'] },
|
|
85
|
+
{ id: 'keybinds.openPanel', category: 'view', defaults: ['mod+/'] }
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
export const KEYBIND_ACTION_IDS: readonly string[] = KEYBIND_ACTIONS.map(action => action.id)
|
|
89
|
+
|
|
90
|
+
const ACTION_BY_ID = new Map(KEYBIND_ACTIONS.map(action => [action.id, action]))
|
|
91
|
+
|
|
92
|
+
export function keybindAction(id: string): KeybindActionMeta | undefined {
|
|
93
|
+
return ACTION_BY_ID.get(id)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type KeybindBindings = Record<string, string[]>
|
|
97
|
+
|
|
98
|
+
export function defaultBindings(): KeybindBindings {
|
|
99
|
+
return Object.fromEntries(KEYBIND_ACTIONS.map(action => [action.id, [...action.defaults]]))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Fixed, non-rebindable shortcuts surfaced read-only in the panel so the map is
|
|
103
|
+
// complete. `keys` are canonical tokens run through `formatCombo` for display
|
|
104
|
+
// (single symbols like "@" / "/" pass through unchanged). Categories listed here
|
|
105
|
+
// render after the rebindable ones.
|
|
106
|
+
export interface KeybindReadonly {
|
|
107
|
+
id: string
|
|
108
|
+
category: KeybindCategory
|
|
109
|
+
keys: readonly string[]
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export const KEYBIND_READONLY: readonly KeybindReadonly[] = [
|
|
113
|
+
{ id: 'composer.send', category: 'composer', keys: ['enter'] },
|
|
114
|
+
{ id: 'composer.newline', category: 'composer', keys: ['shift+enter'] },
|
|
115
|
+
{ id: 'composer.steer', category: 'composer', keys: ['mod+enter'] },
|
|
116
|
+
{ id: 'composer.sendQueued', category: 'composer', keys: ['mod+shift+k'] },
|
|
117
|
+
{ id: 'composer.mention', category: 'composer', keys: ['@'] },
|
|
118
|
+
{ id: 'composer.slash', category: 'composer', keys: ['/'] },
|
|
119
|
+
{ id: 'composer.help', category: 'composer', keys: ['?'] },
|
|
120
|
+
{ id: 'composer.history', category: 'composer', keys: ['up', 'down'] },
|
|
121
|
+
{ id: 'composer.cancel', category: 'composer', keys: ['escape'] },
|
|
122
|
+
// Fixed, context-local shortcuts surfaced for discoverability.
|
|
123
|
+
{ id: 'view.terminalSelection', category: 'view', keys: ['mod+l'] },
|
|
124
|
+
{ id: 'view.closePreviewTab', category: 'view', keys: ['mod+w'] }
|
|
125
|
+
]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
// `IS_MAC` is resolved once at module load from `navigator`, so each platform
|
|
4
|
+
// case overrides the platform and re-imports the module fresh.
|
|
5
|
+
async function loadCombo(platform: string) {
|
|
6
|
+
Object.defineProperty(window.navigator, 'platform', { value: platform, configurable: true })
|
|
7
|
+
vi.resetModules()
|
|
8
|
+
|
|
9
|
+
return import('./combo')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function keydown(init: KeyboardEventInit): KeyboardEvent {
|
|
13
|
+
return new KeyboardEvent('keydown', init)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
vi.resetModules()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe('comboFromEvent — ctrl as a distinct modifier on macOS', () => {
|
|
21
|
+
it('reports Control+Tab as "ctrl+tab" on macOS (not Cmd)', async () => {
|
|
22
|
+
const { comboFromEvent } = await loadCombo('MacIntel')
|
|
23
|
+
|
|
24
|
+
expect(comboFromEvent(keydown({ code: 'Tab', ctrlKey: true }))).toBe('ctrl+tab')
|
|
25
|
+
expect(comboFromEvent(keydown({ code: 'Tab', ctrlKey: true, shiftKey: true }))).toBe('ctrl+shift+tab')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('keeps Cmd as "mod" and distinct from Control on macOS', async () => {
|
|
29
|
+
const { comboFromEvent } = await loadCombo('MacIntel')
|
|
30
|
+
|
|
31
|
+
expect(comboFromEvent(keydown({ code: 'KeyK', metaKey: true }))).toBe('mod+k')
|
|
32
|
+
expect(comboFromEvent(keydown({ code: 'KeyK', ctrlKey: true }))).toBe('ctrl+k')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('treats Control as the "mod" accelerator off macOS', async () => {
|
|
36
|
+
const { comboFromEvent } = await loadCombo('Win32')
|
|
37
|
+
|
|
38
|
+
expect(comboFromEvent(keydown({ code: 'Tab', ctrlKey: true }))).toBe('mod+tab')
|
|
39
|
+
expect(comboFromEvent(keydown({ code: 'Tab', ctrlKey: true, shiftKey: true }))).toBe('mod+shift+tab')
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe('canonicalizeCombo', () => {
|
|
44
|
+
it('leaves "ctrl+…" untouched on macOS', async () => {
|
|
45
|
+
const { canonicalizeCombo } = await loadCombo('MacIntel')
|
|
46
|
+
|
|
47
|
+
expect(canonicalizeCombo('ctrl+tab')).toBe('ctrl+tab')
|
|
48
|
+
expect(canonicalizeCombo('ctrl+shift+tab')).toBe('ctrl+shift+tab')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('folds "ctrl+…" to "mod+…" off macOS so a real Control press resolves', async () => {
|
|
52
|
+
const { canonicalizeCombo } = await loadCombo('Win32')
|
|
53
|
+
|
|
54
|
+
expect(canonicalizeCombo('ctrl+tab')).toBe('mod+tab')
|
|
55
|
+
expect(canonicalizeCombo('ctrl+shift+tab')).toBe('mod+shift+tab')
|
|
56
|
+
// Non-ctrl combos are unchanged.
|
|
57
|
+
expect(canonicalizeCombo('mod+k')).toBe('mod+k')
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe('formatCombo — honest Control labels', () => {
|
|
62
|
+
it('renders the Control glyph on macOS', async () => {
|
|
63
|
+
const { formatCombo } = await loadCombo('MacIntel')
|
|
64
|
+
|
|
65
|
+
expect(formatCombo('ctrl+tab')).toBe('⌃⇥')
|
|
66
|
+
expect(formatCombo('ctrl+shift+tab')).toBe('⌃⇧⇥')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('renders "Ctrl+…" off macOS (base key keeps its glyph)', async () => {
|
|
70
|
+
const { formatCombo } = await loadCombo('Win32')
|
|
71
|
+
|
|
72
|
+
expect(formatCombo('ctrl+tab')).toBe('Ctrl+⇥')
|
|
73
|
+
expect(formatCombo('ctrl+shift+tab')).toBe('Ctrl+Shift+⇥')
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('comboAllowedInInput', () => {
|
|
78
|
+
it('lets ctrl combos fire while typing (e.g. ⌃Tab from the composer)', async () => {
|
|
79
|
+
const { comboAllowedInInput } = await loadCombo('MacIntel')
|
|
80
|
+
|
|
81
|
+
expect(comboAllowedInInput('ctrl+tab')).toBe(true)
|
|
82
|
+
expect(comboAllowedInInput('ctrl+shift+tab')).toBe(true)
|
|
83
|
+
expect(comboAllowedInInput('mod+k')).toBe(true)
|
|
84
|
+
expect(comboAllowedInInput('shift+x')).toBe(false)
|
|
85
|
+
})
|
|
86
|
+
})
|