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,18 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { coerceThinkingText } from './chat-runtime'
|
|
4
|
+
|
|
5
|
+
describe('coerceThinkingText', () => {
|
|
6
|
+
it('strips streaming status prefixes from thinking deltas', () => {
|
|
7
|
+
expect(coerceThinkingText("◉_◉ processing... checking the user's request")).toBe("checking the user's request")
|
|
8
|
+
expect(coerceThinkingText('(¬‿¬) analyzing... reading the file')).toBe('reading the file')
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('drops empty thinking rewrite placeholder text', () => {
|
|
12
|
+
expect(
|
|
13
|
+
coerceThinkingText(
|
|
14
|
+
"◉_◉ processing... I don't see any current rewritten thinking or next thinking to process. Could you provide the thinking content you'd like me to rewrite?"
|
|
15
|
+
)
|
|
16
|
+
).toBe('')
|
|
17
|
+
})
|
|
18
|
+
})
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import type { ThreadMessage } from '@assistant-ui/react'
|
|
2
|
+
|
|
3
|
+
import type { QuickModelOption } from '@/app/chat/composer/types'
|
|
4
|
+
import type { ClientSessionState, CommandDispatchResponse } from '@/app/types'
|
|
5
|
+
import { formatRefValue } from '@/components/assistant-ui/directive-text'
|
|
6
|
+
import { type ChatMessage, type ChatMessagePart, chatMessageText, textPart } from '@/lib/chat-messages'
|
|
7
|
+
import type { ComposerAttachment } from '@/store/composer'
|
|
8
|
+
import type { ModelOptionsResponse, SessionInfo } from '@/types/nastech'
|
|
9
|
+
|
|
10
|
+
export const SLASH_COMMAND_RE = /^\/[^\s/]*(?:\s|$)/
|
|
11
|
+
export const BUILTIN_PERSONALITIES = [
|
|
12
|
+
'helpful',
|
|
13
|
+
'concise',
|
|
14
|
+
'technical',
|
|
15
|
+
'creative',
|
|
16
|
+
'teacher',
|
|
17
|
+
'kawaii',
|
|
18
|
+
'catgirl',
|
|
19
|
+
'pirate',
|
|
20
|
+
'shakespeare',
|
|
21
|
+
'surfer',
|
|
22
|
+
'noir',
|
|
23
|
+
'uwu',
|
|
24
|
+
'philosopher',
|
|
25
|
+
'hype'
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
const THINKING_STATUS_PREFIX_RE =
|
|
29
|
+
/^\s*(?:(?:[^\s.]{1,16})\s+)?(?:processing|thinking|reasoning|analyzing|pondering|contemplating|musing|cogitating|ruminating|deliberating|mulling|reflecting|computing|synthesizing|formulating|brainstorming)\.\.\.\s*/i
|
|
30
|
+
|
|
31
|
+
const EMPTY_THINKING_PLACEHOLDER_RE =
|
|
32
|
+
/\b(?:current rewritten thinking|next thinking to process|provide the thinking content|don't see any .*thinking)\b/i
|
|
33
|
+
|
|
34
|
+
export function createClientSessionState(
|
|
35
|
+
storedSessionId: string | null = null,
|
|
36
|
+
messages: ChatMessage[] = []
|
|
37
|
+
): ClientSessionState {
|
|
38
|
+
return {
|
|
39
|
+
storedSessionId,
|
|
40
|
+
messages,
|
|
41
|
+
branch: '',
|
|
42
|
+
cwd: '',
|
|
43
|
+
busy: false,
|
|
44
|
+
awaitingResponse: false,
|
|
45
|
+
streamId: null,
|
|
46
|
+
sawAssistantPayload: false,
|
|
47
|
+
pendingBranchGroup: null,
|
|
48
|
+
interrupted: false,
|
|
49
|
+
needsInput: false,
|
|
50
|
+
turnStartedAt: null
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function sessionTitle(session: SessionInfo): string {
|
|
55
|
+
return session.title?.trim() || session.preview?.trim() || 'Untitled session'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function coerceGatewayText(value: unknown): string {
|
|
59
|
+
if (typeof value === 'string') {
|
|
60
|
+
return value
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (value === null || value === undefined) {
|
|
64
|
+
return ''
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (Array.isArray(value)) {
|
|
68
|
+
return value
|
|
69
|
+
.map(item => {
|
|
70
|
+
if (typeof item === 'string') {
|
|
71
|
+
return item
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (item && typeof item === 'object') {
|
|
75
|
+
const row = item as Record<string, unknown>
|
|
76
|
+
|
|
77
|
+
if (typeof row.text === 'string') {
|
|
78
|
+
return row.text
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (typeof row.output_text === 'string') {
|
|
82
|
+
return row.output_text
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return ''
|
|
87
|
+
})
|
|
88
|
+
.join('')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (typeof value === 'object') {
|
|
92
|
+
const row = value as Record<string, unknown>
|
|
93
|
+
|
|
94
|
+
if (typeof row.text === 'string') {
|
|
95
|
+
return row.text
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (typeof row.output_text === 'string') {
|
|
99
|
+
return row.output_text
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
return JSON.stringify(value)
|
|
104
|
+
} catch {
|
|
105
|
+
return ''
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return String(value)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Normalize a reasoning/thinking text payload from the gateway.
|
|
114
|
+
*
|
|
115
|
+
* Only the leading status prefix (e.g. "NasTech is thinking...") and the
|
|
116
|
+
* obvious placeholder echoes are stripped. We deliberately do NOT trim
|
|
117
|
+
* the delta — reasoning streams as small chunks (often individual tokens
|
|
118
|
+
* with leading or trailing spaces), and trimming each chunk before
|
|
119
|
+
* concatenation collapses adjacent words together. Whitespace between
|
|
120
|
+
* tokens belongs to the data, not chrome.
|
|
121
|
+
*/
|
|
122
|
+
export function coerceThinkingText(value: unknown): string {
|
|
123
|
+
const raw = coerceGatewayText(value).replace(THINKING_STATUS_PREFIX_RE, '')
|
|
124
|
+
|
|
125
|
+
return EMPTY_THINKING_PLACEHOLDER_RE.test(raw) ? '' : raw
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function isImageGenerationTool(name?: string): boolean {
|
|
129
|
+
return name === 'image_generate'
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function contextPath(path: string, cwd: string): string {
|
|
133
|
+
if (!cwd) {
|
|
134
|
+
return path
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const normalizedCwd = cwd.endsWith('/') ? cwd : `${cwd}/`
|
|
138
|
+
|
|
139
|
+
return path.startsWith(normalizedCwd) ? path.slice(normalizedCwd.length) : path
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function attachmentId(kind: ComposerAttachment['kind'], value: string): string {
|
|
143
|
+
return `${kind}:${value}`
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function pathLabel(path: string): string {
|
|
147
|
+
return path.split(/[\\/]/).filter(Boolean).pop() || path
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function attachmentDisplayText(attachment: ComposerAttachment): string | null {
|
|
151
|
+
if (attachment.kind === 'terminal' && attachment.detail) {
|
|
152
|
+
return `\`\`\`terminal\n${attachment.detail.trim()}\n\`\`\``
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (attachment.refText) {
|
|
156
|
+
return attachment.refText
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (attachment.kind === 'image') {
|
|
160
|
+
const id = attachment.detail || attachment.path || attachment.label
|
|
161
|
+
|
|
162
|
+
return id ? `@image:${formatRefValue(id)}` : null
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return null
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function personalityNamesFromConfig(config: unknown): string[] {
|
|
169
|
+
const root = config && typeof config === 'object' ? (config as Record<string, unknown>) : {}
|
|
170
|
+
const agent = root.agent && typeof root.agent === 'object' ? (root.agent as Record<string, unknown>) : {}
|
|
171
|
+
const personalities = agent.personalities
|
|
172
|
+
|
|
173
|
+
return personalities && typeof personalities === 'object' && !Array.isArray(personalities)
|
|
174
|
+
? Object.keys(personalities as Record<string, unknown>)
|
|
175
|
+
: []
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function normalizePersonalityValue(value: string): string {
|
|
179
|
+
const trimmed = value.trim().toLowerCase()
|
|
180
|
+
|
|
181
|
+
return !trimmed || trimmed === 'default' || trimmed === 'none' ? '' : trimmed
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function parseSlashCommand(command: string) {
|
|
185
|
+
const match = command.replace(/^\/+/, '').match(/^(\S+)\s*(.*)$/)
|
|
186
|
+
|
|
187
|
+
return match ? { name: match[1], arg: match[2].trim() } : { name: '', arg: '' }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function parseCommandDispatch(raw: unknown): CommandDispatchResponse | null {
|
|
191
|
+
if (!raw || typeof raw !== 'object') {
|
|
192
|
+
return null
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const row = raw as Record<string, unknown>
|
|
196
|
+
const str = (value: unknown) => (typeof value === 'string' ? value : undefined)
|
|
197
|
+
|
|
198
|
+
switch (row.type) {
|
|
199
|
+
case 'exec':
|
|
200
|
+
|
|
201
|
+
case 'plugin':
|
|
202
|
+
return { type: row.type, output: str(row.output) }
|
|
203
|
+
|
|
204
|
+
case 'alias':
|
|
205
|
+
return typeof row.target === 'string' ? { type: 'alias', target: row.target } : null
|
|
206
|
+
|
|
207
|
+
case 'skill':
|
|
208
|
+
return typeof row.name === 'string' ? { type: 'skill', name: row.name, message: str(row.message) } : null
|
|
209
|
+
|
|
210
|
+
case 'send':
|
|
211
|
+
return typeof row.message === 'string' ? { type: 'send', message: row.message } : null
|
|
212
|
+
|
|
213
|
+
default:
|
|
214
|
+
return null
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function quickModelOptions(
|
|
219
|
+
data: ModelOptionsResponse | undefined,
|
|
220
|
+
currentProvider: string,
|
|
221
|
+
currentModel: string
|
|
222
|
+
): QuickModelOption[] {
|
|
223
|
+
const seen = new Set<string>()
|
|
224
|
+
const options: QuickModelOption[] = []
|
|
225
|
+
|
|
226
|
+
const providers = [...(data?.providers ?? [])].sort((a, b) => {
|
|
227
|
+
if (a.slug === currentProvider) {
|
|
228
|
+
return -1
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (b.slug === currentProvider) {
|
|
232
|
+
return 1
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (a.is_current) {
|
|
236
|
+
return -1
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (b.is_current) {
|
|
240
|
+
return 1
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return 0
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
const add = (provider: string, providerName: string, model: string) => {
|
|
247
|
+
const key = `${provider}:${model}`
|
|
248
|
+
|
|
249
|
+
if (!model || seen.has(key)) {
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
seen.add(key)
|
|
254
|
+
options.push({ provider, providerName, model })
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (currentProvider && currentModel) {
|
|
258
|
+
add(currentProvider, currentProvider, currentModel)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
for (const provider of providers) {
|
|
262
|
+
const models = [...(provider.models ?? [])].sort((a, b) => {
|
|
263
|
+
if (provider.slug === currentProvider && a === currentModel) {
|
|
264
|
+
return -1
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (provider.slug === currentProvider && b === currentModel) {
|
|
268
|
+
return 1
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return 0
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
for (const model of models) {
|
|
275
|
+
add(provider.slug, provider.name, model)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (options.length >= 8) {
|
|
279
|
+
break
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return options.slice(0, 8)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function toRuntimeMessage(message: ChatMessage): ThreadMessage {
|
|
287
|
+
const role =
|
|
288
|
+
message.role === 'user' || message.role === 'assistant' || message.role === 'system' ? message.role : 'assistant'
|
|
289
|
+
|
|
290
|
+
const createdAt = message.timestamp
|
|
291
|
+
? new Date(message.timestamp * 1000)
|
|
292
|
+
: new Date(Number(message.id.match(/\d+/)?.[0]) || Date.now())
|
|
293
|
+
|
|
294
|
+
if (role === 'user') {
|
|
295
|
+
return {
|
|
296
|
+
id: message.id,
|
|
297
|
+
role,
|
|
298
|
+
content: message.parts.filter((part): part is Extract<ChatMessagePart, { type: 'text' }> => part.type === 'text'),
|
|
299
|
+
attachments: [],
|
|
300
|
+
createdAt,
|
|
301
|
+
metadata: { custom: { attachmentRefs: message.attachmentRefs ?? [] } }
|
|
302
|
+
} as ThreadMessage
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (role === 'system') {
|
|
306
|
+
const text = chatMessageText(message)
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
id: message.id,
|
|
310
|
+
role,
|
|
311
|
+
content: [textPart(text)],
|
|
312
|
+
createdAt,
|
|
313
|
+
metadata: { custom: {} }
|
|
314
|
+
} as ThreadMessage
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
id: message.id,
|
|
319
|
+
role,
|
|
320
|
+
content: message.parts as Extract<ThreadMessage, { role: 'assistant' }>['content'],
|
|
321
|
+
createdAt,
|
|
322
|
+
status: message.error
|
|
323
|
+
? { type: 'incomplete', reason: 'error', error: message.error }
|
|
324
|
+
: message.pending
|
|
325
|
+
? { type: 'running' }
|
|
326
|
+
: { type: 'complete', reason: 'stop' },
|
|
327
|
+
metadata: {
|
|
328
|
+
unstable_state: null,
|
|
329
|
+
unstable_annotations: [],
|
|
330
|
+
unstable_data: [],
|
|
331
|
+
steps: [],
|
|
332
|
+
custom: {}
|
|
333
|
+
}
|
|
334
|
+
} as ThreadMessage
|
|
335
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Routes `navigator.clipboard.writeText` through Electron IPC, since the
|
|
2
|
+
// renderer's clipboard API throws "Write permission denied" whenever the
|
|
3
|
+
// document loses focus (e.g. clicking a portaled Radix dropdown). The IPC
|
|
4
|
+
// path runs in the main process and is unconditional.
|
|
5
|
+
|
|
6
|
+
export function installClipboardShim() {
|
|
7
|
+
const ipc = window.NASTECHDesktop?.writeClipboard
|
|
8
|
+
|
|
9
|
+
if (!ipc || !navigator.clipboard) {
|
|
10
|
+
return
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const native = navigator.clipboard.writeText?.bind(navigator.clipboard)
|
|
14
|
+
|
|
15
|
+
const writeText = async (text: string) => {
|
|
16
|
+
try {
|
|
17
|
+
await ipc(text)
|
|
18
|
+
} catch {
|
|
19
|
+
await native?.(text)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
Object.defineProperty(navigator.clipboard, 'writeText', { configurable: true, value: writeText, writable: true })
|
|
25
|
+
} catch {
|
|
26
|
+
// Browser refused override; primitives keep using the native API.
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { buildCommitChangelog, parseCommitHeader } from './commit-changelog'
|
|
4
|
+
|
|
5
|
+
describe('parseCommitHeader', () => {
|
|
6
|
+
it('extracts type, scope, and subject from a conventional header', () => {
|
|
7
|
+
expect(parseCommitHeader('feat(desktop): NSIS prereq detection page')).toEqual({
|
|
8
|
+
breaking: false,
|
|
9
|
+
scope: 'desktop',
|
|
10
|
+
subject: 'NSIS prereq detection page',
|
|
11
|
+
type: 'feat'
|
|
12
|
+
})
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('flags breaking changes via the `!` marker', () => {
|
|
16
|
+
expect(parseCommitHeader('feat(api)!: change endpoint shape')).toMatchObject({
|
|
17
|
+
breaking: true,
|
|
18
|
+
type: 'feat'
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('treats non-conventional commits as untyped with the full header as subject', () => {
|
|
23
|
+
expect(parseCommitHeader('Update README')).toEqual({
|
|
24
|
+
breaking: false,
|
|
25
|
+
scope: null,
|
|
26
|
+
subject: 'Update README',
|
|
27
|
+
type: null
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('ignores body lines and trims whitespace', () => {
|
|
32
|
+
expect(parseCommitHeader(' fix: handle null input \n\nMore detail')).toMatchObject({
|
|
33
|
+
subject: 'handle null input',
|
|
34
|
+
type: 'fix'
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('returns empty subject for blank input', () => {
|
|
39
|
+
expect(parseCommitHeader('')).toEqual({ breaking: false, scope: null, subject: '', type: null })
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe('buildCommitChangelog', () => {
|
|
44
|
+
it('groups commits into user-friendly buckets and capitalizes subjects', () => {
|
|
45
|
+
const groups = buildCommitChangelog([
|
|
46
|
+
{ summary: 'feat(desktop): add NSIS prereq detection page' },
|
|
47
|
+
{ summary: 'fix(sidebar): jitter when dragging' },
|
|
48
|
+
{ summary: 'perf: shave 200ms off cold start' },
|
|
49
|
+
{ summary: 'refactor: extract sidebar row component' }
|
|
50
|
+
])
|
|
51
|
+
|
|
52
|
+
expect(groups.map(g => g.id)).toEqual(['new', 'fixed', 'faster'])
|
|
53
|
+
expect(groups[0]).toMatchObject({ label: "What's new" })
|
|
54
|
+
expect(groups[0].items[0]).toBe('Add NSIS prereq detection page')
|
|
55
|
+
expect(groups[1].items[0]).toBe('Jitter when dragging')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('hides chore/ci/docs/test commits', () => {
|
|
59
|
+
const groups = buildCommitChangelog([
|
|
60
|
+
{ summary: 'chore: bump deps' },
|
|
61
|
+
{ summary: 'ci: tweak workflow' },
|
|
62
|
+
{ summary: 'docs: spelling fix' },
|
|
63
|
+
{ summary: 'feat: real new feature' }
|
|
64
|
+
])
|
|
65
|
+
|
|
66
|
+
expect(groups).toHaveLength(1)
|
|
67
|
+
expect(groups[0].items).toEqual(['Real new feature'])
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('routes unparseable commits to the "Other improvements" bucket', () => {
|
|
71
|
+
const groups = buildCommitChangelog([{ summary: 'Update sidebar styling' }])
|
|
72
|
+
|
|
73
|
+
expect(groups[0].id).toBe('other')
|
|
74
|
+
expect(groups[0].items).toEqual(['Update sidebar styling'])
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('falls back to a neutral placeholder when every commit is filtered or empty', () => {
|
|
78
|
+
const groups = buildCommitChangelog([{ summary: 'chore: bump' }, { summary: 'ci: stuff' }])
|
|
79
|
+
|
|
80
|
+
expect(groups).toEqual([{ id: 'other', items: ['Improvements and fixes'], label: 'In this update' }])
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('dedupes identical subjects and caps the items per group', () => {
|
|
84
|
+
const groups = buildCommitChangelog(
|
|
85
|
+
[
|
|
86
|
+
{ summary: 'fix: thing A' },
|
|
87
|
+
{ summary: 'fix: thing A' },
|
|
88
|
+
{ summary: 'fix: thing B' },
|
|
89
|
+
{ summary: 'fix: thing C' },
|
|
90
|
+
{ summary: 'fix: thing D' },
|
|
91
|
+
{ summary: 'fix: thing E' }
|
|
92
|
+
],
|
|
93
|
+
{ maxPerGroup: 3, maxTotal: 10 }
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
expect(groups[0].items).toEqual(['Thing A', 'Thing B', 'Thing C'])
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('caps total entries across buckets', () => {
|
|
100
|
+
const groups = buildCommitChangelog(
|
|
101
|
+
[
|
|
102
|
+
{ summary: 'feat: a' },
|
|
103
|
+
{ summary: 'feat: b' },
|
|
104
|
+
{ summary: 'fix: c' },
|
|
105
|
+
{ summary: 'fix: d' },
|
|
106
|
+
{ summary: 'perf: e' }
|
|
107
|
+
],
|
|
108
|
+
{ maxTotal: 3 }
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
const totalItems = groups.reduce((sum, g) => sum + g.items.length, 0)
|
|
112
|
+
expect(totalItems).toBe(3)
|
|
113
|
+
})
|
|
114
|
+
})
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny user-facing changelog builder. Takes a list of raw commit summaries,
|
|
3
|
+
* parses the Conventional Commits 1.0 header (`type(scope)!: subject`),
|
|
4
|
+
* filters internal noise (chore/ci/docs/...), and groups the rest into
|
|
5
|
+
* friendly buckets for end users (What's new, Fixed, Faster, Improved).
|
|
6
|
+
*
|
|
7
|
+
* Inlined (rather than depending on `conventional-commits-parser`) because
|
|
8
|
+
* that package's index re-exports a Node `stream` helper which won't load
|
|
9
|
+
* in the sandboxed Electron renderer, and its actual parse logic for the
|
|
10
|
+
* header is a small regex.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type CommitGroupId = 'new' | 'fixed' | 'faster' | 'improved' | 'other'
|
|
14
|
+
|
|
15
|
+
export interface CommitGroup {
|
|
16
|
+
id: CommitGroupId
|
|
17
|
+
label: string
|
|
18
|
+
items: string[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ParsedCommit {
|
|
22
|
+
type: null | string
|
|
23
|
+
scope: null | string
|
|
24
|
+
breaking: boolean
|
|
25
|
+
subject: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CommitChangelogInput {
|
|
29
|
+
summary?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface BuildOptions {
|
|
33
|
+
maxGroups?: number
|
|
34
|
+
maxPerGroup?: number
|
|
35
|
+
maxTotal?: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const GROUP_META: Record<CommitGroupId, { label: string; order: number }> = {
|
|
39
|
+
new: { label: "What's new", order: 0 },
|
|
40
|
+
fixed: { label: 'Fixed', order: 1 },
|
|
41
|
+
faster: { label: 'Faster', order: 2 },
|
|
42
|
+
improved: { label: 'Improved', order: 3 },
|
|
43
|
+
other: { label: 'Other improvements', order: 4 }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const TYPE_TO_GROUP: Record<string, CommitGroupId> = {
|
|
47
|
+
feat: 'new',
|
|
48
|
+
feature: 'new',
|
|
49
|
+
fix: 'fixed',
|
|
50
|
+
bugfix: 'fixed',
|
|
51
|
+
hotfix: 'fixed',
|
|
52
|
+
revert: 'fixed',
|
|
53
|
+
perf: 'faster',
|
|
54
|
+
performance: 'faster',
|
|
55
|
+
refactor: 'improved',
|
|
56
|
+
a11y: 'improved',
|
|
57
|
+
ui: 'improved',
|
|
58
|
+
ux: 'improved'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const HIDDEN_TYPES = new Set([
|
|
62
|
+
'build',
|
|
63
|
+
'chore',
|
|
64
|
+
'ci',
|
|
65
|
+
'dep',
|
|
66
|
+
'deps',
|
|
67
|
+
'doc',
|
|
68
|
+
'docs',
|
|
69
|
+
'lint',
|
|
70
|
+
'release',
|
|
71
|
+
'style',
|
|
72
|
+
'test',
|
|
73
|
+
'tests',
|
|
74
|
+
'wip'
|
|
75
|
+
])
|
|
76
|
+
|
|
77
|
+
const FALLBACK_GROUP: CommitGroup = { id: 'other', items: ['Improvements and fixes'], label: 'In this update' }
|
|
78
|
+
|
|
79
|
+
const CONVENTIONAL_HEADER = /^(?<type>[a-zA-Z][a-zA-Z0-9_-]*)(?:\((?<scope>[^)]+)\))?(?<bang>!)?:\s+(?<subject>.+)$/
|
|
80
|
+
|
|
81
|
+
/** Parse a single commit header line per Conventional Commits 1.0. */
|
|
82
|
+
export function parseCommitHeader(raw: string): ParsedCommit {
|
|
83
|
+
const header = (raw ?? '').split(/\r?\n/, 1)[0].trim()
|
|
84
|
+
|
|
85
|
+
if (!header) {
|
|
86
|
+
return { breaking: false, scope: null, subject: '', type: null }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const match = CONVENTIONAL_HEADER.exec(header)
|
|
90
|
+
|
|
91
|
+
if (!match?.groups) {
|
|
92
|
+
return { breaking: false, scope: null, subject: header, type: null }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
breaking: Boolean(match.groups.bang),
|
|
97
|
+
scope: match.groups.scope ?? null,
|
|
98
|
+
subject: match.groups.subject.trim(),
|
|
99
|
+
type: match.groups.type.toLowerCase()
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function tidySubject(subject: string): string {
|
|
104
|
+
const cleaned = subject
|
|
105
|
+
.replace(/\s+/g, ' ')
|
|
106
|
+
.replace(/[.;,\s]+$/, '')
|
|
107
|
+
.trim()
|
|
108
|
+
|
|
109
|
+
if (!cleaned) {
|
|
110
|
+
return cleaned
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return cleaned.charAt(0).toUpperCase() + cleaned.slice(1)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Build a small grouped changelog from a list of raw commits.
|
|
118
|
+
* Always returns at least one group; falls back to a neutral placeholder
|
|
119
|
+
* when every commit was filtered or unparseable.
|
|
120
|
+
*/
|
|
121
|
+
export function buildCommitChangelog(
|
|
122
|
+
commits: readonly CommitChangelogInput[] | undefined,
|
|
123
|
+
options: BuildOptions = {}
|
|
124
|
+
): CommitGroup[] {
|
|
125
|
+
const { maxGroups = 3, maxPerGroup = 4, maxTotal = 6 } = options
|
|
126
|
+
const groups = new Map<CommitGroupId, string[]>()
|
|
127
|
+
const seen = new Set<string>()
|
|
128
|
+
let total = 0
|
|
129
|
+
|
|
130
|
+
for (const commit of commits ?? []) {
|
|
131
|
+
if (total >= maxTotal) {
|
|
132
|
+
break
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const parsed = parseCommitHeader(commit.summary ?? '')
|
|
136
|
+
|
|
137
|
+
if (parsed.type && HIDDEN_TYPES.has(parsed.type)) {
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const groupId: CommitGroupId = parsed.type ? (TYPE_TO_GROUP[parsed.type] ?? 'other') : 'other'
|
|
142
|
+
const subject = tidySubject(parsed.subject)
|
|
143
|
+
|
|
144
|
+
if (!subject) {
|
|
145
|
+
continue
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const dedupeKey = subject.toLowerCase()
|
|
149
|
+
|
|
150
|
+
if (seen.has(dedupeKey)) {
|
|
151
|
+
continue
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const bucket = groups.get(groupId) ?? []
|
|
155
|
+
|
|
156
|
+
if (bucket.length >= maxPerGroup) {
|
|
157
|
+
continue
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
bucket.push(subject)
|
|
161
|
+
groups.set(groupId, bucket)
|
|
162
|
+
seen.add(dedupeKey)
|
|
163
|
+
total += 1
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const result = Array.from(groups.entries())
|
|
167
|
+
.map(([id, items]) => ({ id, items, label: GROUP_META[id].label, order: GROUP_META[id].order }))
|
|
168
|
+
.sort((a, b) => a.order - b.order)
|
|
169
|
+
.slice(0, maxGroups)
|
|
170
|
+
.map(({ id, items, label }): CommitGroup => ({ id, items, label }))
|
|
171
|
+
|
|
172
|
+
if (result.length === 0) {
|
|
173
|
+
return [FALLBACK_GROUP]
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return result
|
|
177
|
+
}
|