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,168 @@
|
|
|
1
|
+
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
__resetLinkTitleCache,
|
|
6
|
+
ExternalLink,
|
|
7
|
+
fetchLinkTitle,
|
|
8
|
+
hostPathLabel,
|
|
9
|
+
isTitleFetchable,
|
|
10
|
+
LinkifiedText,
|
|
11
|
+
PrettyLink,
|
|
12
|
+
urlSlugTitleLabel
|
|
13
|
+
} from './external-link'
|
|
14
|
+
|
|
15
|
+
const desktopWindow = window as unknown as { NASTECHDesktop?: Window['NASTECHDesktop'] }
|
|
16
|
+
const initialNasTechDesktop = desktopWindow.NASTECHDesktop
|
|
17
|
+
|
|
18
|
+
function installDesktopBridge(partial: Partial<Window['NASTECHDesktop']> = {}) {
|
|
19
|
+
desktopWindow.NASTECHDesktop = {
|
|
20
|
+
fetchLinkTitle: vi.fn().mockResolvedValue(''),
|
|
21
|
+
openExternal: vi.fn().mockResolvedValue(undefined),
|
|
22
|
+
...partial
|
|
23
|
+
} as unknown as Window['NASTECHDesktop']
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
__resetLinkTitleCache()
|
|
28
|
+
vi.restoreAllMocks()
|
|
29
|
+
cleanup()
|
|
30
|
+
|
|
31
|
+
if (initialNasTechDesktop) {
|
|
32
|
+
desktopWindow.NASTECHDesktop = initialNasTechDesktop
|
|
33
|
+
} else {
|
|
34
|
+
delete desktopWindow.NASTECHDesktop
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('external link helpers', () => {
|
|
39
|
+
it('formats URL fallbacks as host + path', () => {
|
|
40
|
+
expect(
|
|
41
|
+
hostPathLabel(
|
|
42
|
+
'https://www.getyourguide.com/culebra-island-l145468/from-fajardo-full-day-cordillera-islands-catamaran-tour-t19894/'
|
|
43
|
+
)
|
|
44
|
+
).toBe('getyourguide.com/culebra-island-l145468/from-fajardo-full-day-cordillera-islands-catamaran-tour-t19894')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('derives readable title fallbacks from URL slugs', () => {
|
|
48
|
+
expect(
|
|
49
|
+
urlSlugTitleLabel(
|
|
50
|
+
'https://www.getyourguide.com/fajardo-l882/from-fajardo-icacos-island-full-day-catamaran-trip-t19891/'
|
|
51
|
+
)
|
|
52
|
+
).toBe('From Fajardo Icacos Island Full Day Catamaran Trip')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('filters out local/non-http targets for title fetches', () => {
|
|
56
|
+
expect(isTitleFetchable('https://www.expedia.com/things-to-do/foo')).toBe(true)
|
|
57
|
+
expect(isTitleFetchable('http://localhost:5174')).toBe(false)
|
|
58
|
+
expect(isTitleFetchable('file:///tmp/demo.html')).toBe(false)
|
|
59
|
+
expect(isTitleFetchable('mailto:hello@example.com')).toBe(false)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('deduplicates in-flight title fetches and caches results', async () => {
|
|
63
|
+
const bridge = vi.fn().mockResolvedValue('El Yunque Tour Water Slide, Rope Swing & Pickup')
|
|
64
|
+
installDesktopBridge({ fetchLinkTitle: bridge as unknown as Window['NASTECHDesktop']['fetchLinkTitle'] })
|
|
65
|
+
|
|
66
|
+
const url =
|
|
67
|
+
'https://www.expedia.com/things-to-do/puerto-rico-el-yunque-rainforest-adventure-with-transport.a46272756.activity-details'
|
|
68
|
+
|
|
69
|
+
const [first, second] = await Promise.all([fetchLinkTitle(url), fetchLinkTitle(url)])
|
|
70
|
+
|
|
71
|
+
expect(first).toBe('El Yunque Tour Water Slide, Rope Swing & Pickup')
|
|
72
|
+
expect(second).toBe('El Yunque Tour Water Slide, Rope Swing & Pickup')
|
|
73
|
+
expect(bridge).toHaveBeenCalledTimes(1)
|
|
74
|
+
|
|
75
|
+
const third = await fetchLinkTitle(url)
|
|
76
|
+
|
|
77
|
+
expect(third).toBe('El Yunque Tour Water Slide, Rope Swing & Pickup')
|
|
78
|
+
expect(bridge).toHaveBeenCalledTimes(1)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('shares cache across protocol/www URL variants', async () => {
|
|
82
|
+
const bridge = vi.fn().mockResolvedValue('Shared Canonical Title')
|
|
83
|
+
installDesktopBridge({ fetchLinkTitle: bridge as unknown as Window['NASTECHDesktop']['fetchLinkTitle'] })
|
|
84
|
+
|
|
85
|
+
const first = 'https://www.getyourguide.com/san-juan-puerto-rico-l355/sunset-tours-tc306/'
|
|
86
|
+
const second = 'http://getyourguide.com/san-juan-puerto-rico-l355/sunset-tours-tc306/'
|
|
87
|
+
|
|
88
|
+
const [a, b] = await Promise.all([fetchLinkTitle(first), fetchLinkTitle(second)])
|
|
89
|
+
|
|
90
|
+
expect(a).toBe('Shared Canonical Title')
|
|
91
|
+
expect(b).toBe('Shared Canonical Title')
|
|
92
|
+
expect(bridge).toHaveBeenCalledTimes(1)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('opens links via the desktop bridge', () => {
|
|
96
|
+
const openExternal = vi.fn().mockResolvedValue(undefined)
|
|
97
|
+
installDesktopBridge({ openExternal: openExternal as unknown as Window['NASTECHDesktop']['openExternal'] })
|
|
98
|
+
|
|
99
|
+
render(<ExternalLink href="https://example.com/path/to/resource">Example link</ExternalLink>)
|
|
100
|
+
|
|
101
|
+
fireEvent.click(screen.getByRole('link', { name: 'Example link' }))
|
|
102
|
+
expect(openExternal).toHaveBeenCalledWith('https://example.com/path/to/resource')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('shows a trailing external-link icon', () => {
|
|
106
|
+
installDesktopBridge()
|
|
107
|
+
|
|
108
|
+
render(<ExternalLink href="https://example.com/path/to/resource">Example link</ExternalLink>)
|
|
109
|
+
|
|
110
|
+
const link = screen.getByRole('link', { name: 'Example link' })
|
|
111
|
+
expect(link.querySelector('svg')).toBeTruthy()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('renders pretty links with fetched titles and no host suffix', async () => {
|
|
115
|
+
const bridge = vi.fn().mockResolvedValue('From Fajardo: Full-Day Culebra Islands Catamaran Tour')
|
|
116
|
+
installDesktopBridge({ fetchLinkTitle: bridge as unknown as Window['NASTECHDesktop']['fetchLinkTitle'] })
|
|
117
|
+
|
|
118
|
+
const url =
|
|
119
|
+
'https://www.getyourguide.com/culebra-island-l145468/from-fajardo-full-day-cordillera-islands-catamaran-tour-t19894/'
|
|
120
|
+
|
|
121
|
+
render(<LinkifiedText text={`Read ${url}`} />)
|
|
122
|
+
|
|
123
|
+
const link = screen.getByTitle(url)
|
|
124
|
+
expect(link.textContent).toContain('From Fajardo Full Day Cordillera Islands Catamaran Tour')
|
|
125
|
+
|
|
126
|
+
await waitFor(() => {
|
|
127
|
+
expect(link.textContent).toContain('From Fajardo: Full-Day Culebra Islands Catamaran Tour')
|
|
128
|
+
})
|
|
129
|
+
expect(link.textContent).not.toContain('getyourguide.com')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('shows host/path fallback when title is unavailable', () => {
|
|
133
|
+
installDesktopBridge()
|
|
134
|
+
const url = 'https://www.expedia.com/things-to-do/puerto-rico-el-yunque'
|
|
135
|
+
|
|
136
|
+
render(<PrettyLink href={url} />)
|
|
137
|
+
|
|
138
|
+
const link = screen.getByTitle(url)
|
|
139
|
+
|
|
140
|
+
expect(link.textContent).toBe('Puerto Rico El Yunque')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('ignores error-like fetched titles and falls back to slug label', async () => {
|
|
144
|
+
const bridge = vi.fn().mockResolvedValue('GetYourGuide – Error')
|
|
145
|
+
installDesktopBridge({ fetchLinkTitle: bridge as unknown as Window['NASTECHDesktop']['fetchLinkTitle'] })
|
|
146
|
+
|
|
147
|
+
const url =
|
|
148
|
+
'https://www.getyourguide.com/culebra-island-l145468/from-fajardo-full-day-cordillera-islands-catamaran-tour-t19894/'
|
|
149
|
+
|
|
150
|
+
render(<PrettyLink href={url} />)
|
|
151
|
+
|
|
152
|
+
const link = screen.getByTitle(url)
|
|
153
|
+
await waitFor(() => {
|
|
154
|
+
expect(link.textContent).toBe('From Fajardo Full Day Cordillera Islands Catamaran Tour')
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('normalizes scheme-less links before opening', () => {
|
|
159
|
+
installDesktopBridge()
|
|
160
|
+
|
|
161
|
+
render(<LinkifiedText text="Source expedia.com/things-to-do/puerto-rico-el-yunque-rainforest-adventure" />)
|
|
162
|
+
|
|
163
|
+
const link = screen.getByRole('link')
|
|
164
|
+
expect(link.getAttribute('href')).toBe(
|
|
165
|
+
'https://expedia.com/things-to-do/puerto-rico-el-yunque-rainforest-adventure'
|
|
166
|
+
)
|
|
167
|
+
})
|
|
168
|
+
})
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import type { ComponentProps, ReactNode } from 'react'
|
|
2
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
3
|
+
|
|
4
|
+
import { ArrowUpRight } from '@/lib/icons'
|
|
5
|
+
|
|
6
|
+
import { cn } from './utils'
|
|
7
|
+
|
|
8
|
+
const titleCache = new Map<string, string>()
|
|
9
|
+
const titleInflight = new Map<string, Promise<string>>()
|
|
10
|
+
const titleSubs = new Map<string, Set<(value: string) => void>>()
|
|
11
|
+
|
|
12
|
+
const URL_RE =
|
|
13
|
+
/(?:https?:\/\/|www\.)[^\s<>"'`]+[^\s<>"'`.,;:!?)]|[a-z0-9](?:[a-z0-9-]*\.)+[a-z]{2,}(?:\/[^\s<>"'`.,;:!?)]*)?/gi
|
|
14
|
+
|
|
15
|
+
const DOMAIN_RE = /^(?:www\.)?[a-z0-9](?:[a-z0-9-]*\.)+[a-z]{2,}(?::\d+)?(?:[/?#][^\s]*)?$/i
|
|
16
|
+
const SKIP_PROTO_RE = /^(?:file|data|mailto|javascript|blob|chrome|about|NASTECH):/i
|
|
17
|
+
const LOCAL_HOST_RE = /^(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(?::\d+)?$/i
|
|
18
|
+
|
|
19
|
+
const ERROR_TITLE_RE =
|
|
20
|
+
/\b(?:access denied|attention required|captcha|error|forbidden|just a moment|request blocked|too many requests)\b/i
|
|
21
|
+
|
|
22
|
+
export function normalizeExternalUrl(value: string): string {
|
|
23
|
+
const trimmed = value.trim()
|
|
24
|
+
|
|
25
|
+
if (!trimmed || /^https?:\/\//i.test(trimmed)) {
|
|
26
|
+
return trimmed
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return DOMAIN_RE.test(trimmed) ? `https://${trimmed}` : trimmed
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseUrl(value: string): null | URL {
|
|
33
|
+
try {
|
|
34
|
+
return new URL(normalizeExternalUrl(value))
|
|
35
|
+
} catch {
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function titleCacheKey(value: string): string {
|
|
41
|
+
const url = parseUrl(value)
|
|
42
|
+
|
|
43
|
+
if (!url) {
|
|
44
|
+
return normalizeExternalUrl(value)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const host = url.hostname.replace(/^www\./i, '').toLowerCase()
|
|
48
|
+
const pathname = url.pathname === '/' ? '/' : url.pathname.replace(/\/+$/, '') || '/'
|
|
49
|
+
|
|
50
|
+
return `${host}${pathname}${url.search || ''}`
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function shortHostLabel(value: string): string {
|
|
54
|
+
return parseUrl(value)?.hostname.replace(/^www\./, '') ?? value
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function hostPathLabel(value: string): string {
|
|
58
|
+
const url = parseUrl(value)
|
|
59
|
+
|
|
60
|
+
if (!url) {
|
|
61
|
+
return value
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const host = url.hostname.replace(/^www\./, '')
|
|
65
|
+
const path = url.pathname && url.pathname !== '/' ? url.pathname.replace(/\/$/, '') : ''
|
|
66
|
+
|
|
67
|
+
return `${host}${path}`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function cleanSlug(segment: string): string {
|
|
71
|
+
try {
|
|
72
|
+
return decodeURIComponent(segment)
|
|
73
|
+
.replace(/\.a\d+\..*$/i, '')
|
|
74
|
+
.replace(/\.(?:html?|php|aspx?)$/i, '')
|
|
75
|
+
.replace(/(?:[-_.](?:[a-z]{1,3}\d{2,}|i\d{2,}))+$/i, '')
|
|
76
|
+
.replace(/[_-]+/g, ' ')
|
|
77
|
+
.replace(/\s+/g, ' ')
|
|
78
|
+
.trim()
|
|
79
|
+
} catch {
|
|
80
|
+
return ''
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function urlSlugTitleLabel(value: string): string {
|
|
85
|
+
const url = parseUrl(value)
|
|
86
|
+
|
|
87
|
+
for (const segment of url?.pathname.split('/').filter(Boolean).reverse() ?? []) {
|
|
88
|
+
const cleaned = cleanSlug(segment)
|
|
89
|
+
|
|
90
|
+
if (!cleaned || !/[a-z]/i.test(cleaned)) {
|
|
91
|
+
continue
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (/^(?:[a-z]{1,3}\d+|\d+)$/i.test(cleaned.replace(/\s+/g, ''))) {
|
|
95
|
+
continue
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const titled = cleaned.replace(/\b[a-z]/g, c => c.toUpperCase())
|
|
99
|
+
|
|
100
|
+
if (titled.length >= 4) {
|
|
101
|
+
return titled
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return hostPathLabel(value)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function isTitleFetchable(value: string): boolean {
|
|
109
|
+
if (!value || SKIP_PROTO_RE.test(value)) {
|
|
110
|
+
return false
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const url = parseUrl(value)
|
|
114
|
+
|
|
115
|
+
return Boolean(url && /^https?:$/.test(url.protocol) && !LOCAL_HOST_RE.test(url.host))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function fetchLinkTitle(url: string): Promise<string> {
|
|
119
|
+
const normalizedUrl = normalizeExternalUrl(url)
|
|
120
|
+
const key = titleCacheKey(normalizedUrl)
|
|
121
|
+
|
|
122
|
+
if (!isTitleFetchable(normalizedUrl)) {
|
|
123
|
+
return Promise.resolve('')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (titleCache.has(key)) {
|
|
127
|
+
return Promise.resolve(titleCache.get(key) ?? '')
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const pending = titleInflight.get(key)
|
|
131
|
+
|
|
132
|
+
if (pending) {
|
|
133
|
+
return pending
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const bridge = typeof window === 'undefined' ? undefined : window.NASTECHDesktop?.fetchLinkTitle
|
|
137
|
+
|
|
138
|
+
if (!bridge) {
|
|
139
|
+
titleCache.set(key, '')
|
|
140
|
+
|
|
141
|
+
return Promise.resolve('')
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const promise = bridge(normalizedUrl)
|
|
145
|
+
.then(value => (value || '').replace(/\s+/g, ' ').trim())
|
|
146
|
+
.then(clean => (clean && !ERROR_TITLE_RE.test(clean) ? clean : ''))
|
|
147
|
+
.catch(() => '')
|
|
148
|
+
.then(safe => {
|
|
149
|
+
titleCache.set(key, safe)
|
|
150
|
+
titleInflight.delete(key)
|
|
151
|
+
titleSubs.get(key)?.forEach(sub => sub(safe))
|
|
152
|
+
|
|
153
|
+
return safe
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
titleInflight.set(key, promise)
|
|
157
|
+
|
|
158
|
+
return promise
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function useLinkTitle(url?: null | string): string {
|
|
162
|
+
const normalizedUrl = useMemo(() => (url ? normalizeExternalUrl(url) : ''), [url])
|
|
163
|
+
const key = useMemo(() => (normalizedUrl ? titleCacheKey(normalizedUrl) : ''), [normalizedUrl])
|
|
164
|
+
const [title, setTitle] = useState(() => (key ? (titleCache.get(key) ?? '') : ''))
|
|
165
|
+
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
setTitle(key ? (titleCache.get(key) ?? '') : '')
|
|
168
|
+
|
|
169
|
+
if (!key || !isTitleFetchable(normalizedUrl)) {
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const subs = titleSubs.get(key) ?? new Set<(value: string) => void>()
|
|
174
|
+
|
|
175
|
+
subs.add(setTitle)
|
|
176
|
+
titleSubs.set(key, subs)
|
|
177
|
+
void fetchLinkTitle(normalizedUrl)
|
|
178
|
+
|
|
179
|
+
return () => {
|
|
180
|
+
subs.delete(setTitle)
|
|
181
|
+
|
|
182
|
+
if (!subs.size) {
|
|
183
|
+
titleSubs.delete(key)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}, [key, normalizedUrl])
|
|
187
|
+
|
|
188
|
+
return title
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function openExternalLink(href: string): void {
|
|
192
|
+
if (href) {
|
|
193
|
+
void window.NASTECHDesktop?.openExternal?.(href)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
interface ExternalLinkProps extends Omit<ComponentProps<'a'>, 'href' | 'target'> {
|
|
198
|
+
href: string
|
|
199
|
+
children?: ReactNode
|
|
200
|
+
showExternalIcon?: boolean
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function ExternalLinkIcon({ className }: { className?: string }) {
|
|
204
|
+
return <ArrowUpRight aria-hidden className={cn('ml-1 inline size-[0.78em] align-[-0.08em] opacity-70', className)} />
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function ExternalLink({
|
|
208
|
+
children,
|
|
209
|
+
className,
|
|
210
|
+
href,
|
|
211
|
+
onClick,
|
|
212
|
+
showExternalIcon = true,
|
|
213
|
+
...rest
|
|
214
|
+
}: ExternalLinkProps) {
|
|
215
|
+
const target = normalizeExternalUrl(href)
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<a
|
|
219
|
+
className={cn('font-semibold text-foreground underline underline-offset-4 decoration-current/20', className)}
|
|
220
|
+
href={target}
|
|
221
|
+
onClick={event => {
|
|
222
|
+
event.stopPropagation()
|
|
223
|
+
onClick?.(event)
|
|
224
|
+
|
|
225
|
+
if (event.defaultPrevented) {
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
event.preventDefault()
|
|
230
|
+
openExternalLink(target)
|
|
231
|
+
}}
|
|
232
|
+
rel="noopener noreferrer"
|
|
233
|
+
target="_blank"
|
|
234
|
+
{...rest}
|
|
235
|
+
>
|
|
236
|
+
{children ?? urlSlugTitleLabel(target)}
|
|
237
|
+
{showExternalIcon && <ExternalLinkIcon />}
|
|
238
|
+
</a>
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
interface PrettyLinkProps extends Omit<ComponentProps<'a'>, 'href' | 'target'> {
|
|
243
|
+
href: string
|
|
244
|
+
label?: string
|
|
245
|
+
fallbackLabel?: string
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function PrettyLink({ className, fallbackLabel, href, label, ...rest }: PrettyLinkProps) {
|
|
249
|
+
const target = useMemo(() => normalizeExternalUrl(href), [href])
|
|
250
|
+
const fetched = useLinkTitle(label ? null : target)
|
|
251
|
+
const display = fetched || label?.trim() || fallbackLabel?.trim() || urlSlugTitleLabel(target)
|
|
252
|
+
|
|
253
|
+
return (
|
|
254
|
+
<ExternalLink className={cn('wrap-break-word', className)} href={target} title={target} {...rest}>
|
|
255
|
+
<span className="font-medium">{display}</span>
|
|
256
|
+
</ExternalLink>
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
interface LinkifiedTextProps {
|
|
261
|
+
className?: string
|
|
262
|
+
text: string
|
|
263
|
+
pretty?: boolean
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function LinkifiedText({ className, pretty = true, text }: LinkifiedTextProps) {
|
|
267
|
+
const nodes: ReactNode[] = []
|
|
268
|
+
let cursor = 0
|
|
269
|
+
|
|
270
|
+
for (const match of text.matchAll(URL_RE)) {
|
|
271
|
+
const raw = match[0]
|
|
272
|
+
const url = normalizeExternalUrl(raw)
|
|
273
|
+
const index = match.index ?? 0
|
|
274
|
+
|
|
275
|
+
if (index > cursor) {
|
|
276
|
+
nodes.push(text.slice(cursor, index))
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
nodes.push(
|
|
280
|
+
pretty ? (
|
|
281
|
+
<PrettyLink href={url} key={`${url}-${index}`} />
|
|
282
|
+
) : (
|
|
283
|
+
<ExternalLink href={url} key={`${url}-${index}`}>
|
|
284
|
+
{raw}
|
|
285
|
+
</ExternalLink>
|
|
286
|
+
)
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
cursor = index + raw.length
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (cursor < text.length) {
|
|
293
|
+
nodes.push(text.slice(cursor))
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return <span className={className}>{nodes.length ? nodes : text}</span>
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function __resetLinkTitleCache(): void {
|
|
300
|
+
titleCache.clear()
|
|
301
|
+
titleInflight.clear()
|
|
302
|
+
titleSubs.clear()
|
|
303
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { gatewayEventRequiresSessionId } from './gateway-events'
|
|
4
|
+
|
|
5
|
+
describe('gateway event routing', () => {
|
|
6
|
+
it('drops only unscoped subagent events (genuinely background work)', () => {
|
|
7
|
+
expect(gatewayEventRequiresSessionId('subagent.progress')).toBe(true)
|
|
8
|
+
expect(gatewayEventRequiresSessionId('subagent.start')).toBe(true)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('attributes unscoped foreground turn events to the active chat', () => {
|
|
12
|
+
// These must NOT be dropped when unscoped — they are the focused turn's own
|
|
13
|
+
// output, and dropping them loses the live response until a refetch (#42178).
|
|
14
|
+
expect(gatewayEventRequiresSessionId('message.delta')).toBe(false)
|
|
15
|
+
expect(gatewayEventRequiresSessionId('message.complete')).toBe(false)
|
|
16
|
+
expect(gatewayEventRequiresSessionId('reasoning.delta')).toBe(false)
|
|
17
|
+
expect(gatewayEventRequiresSessionId('tool.start')).toBe(false)
|
|
18
|
+
expect(gatewayEventRequiresSessionId('approval.request')).toBe(false)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('allows global events to remain unscoped', () => {
|
|
22
|
+
expect(gatewayEventRequiresSessionId('gateway.ready')).toBe(false)
|
|
23
|
+
expect(gatewayEventRequiresSessionId('preview.restart.progress')).toBe(false)
|
|
24
|
+
expect(gatewayEventRequiresSessionId('session.info')).toBe(false)
|
|
25
|
+
expect(gatewayEventRequiresSessionId(undefined)).toBe(false)
|
|
26
|
+
})
|
|
27
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { StatusbarMenuItem } from '@/app/shell/statusbar-controls'
|
|
2
|
+
|
|
3
|
+
const LOG_TAIL = 5
|
|
4
|
+
|
|
5
|
+
interface RpcEventLike {
|
|
6
|
+
payload?: unknown
|
|
7
|
+
type?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function asRecord(payload: unknown): Record<string, unknown> {
|
|
11
|
+
return payload && typeof payload === 'object' ? (payload as Record<string, unknown>) : {}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const SCOPED_EVENT_PREFIXES = ['subagent.']
|
|
15
|
+
|
|
16
|
+
export function gatewayEventRequiresSessionId(type: string | undefined): boolean {
|
|
17
|
+
if (type == null) return false
|
|
18
|
+
return SCOPED_EVENT_PREFIXES.some(prefix => type.startsWith(prefix))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function gatewayEventCompletedFileDiff(event: RpcEventLike): boolean {
|
|
22
|
+
if (event.type !== 'tool.complete') {
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const diff = asRecord(event.payload).inline_diff
|
|
27
|
+
|
|
28
|
+
return typeof diff === 'string' && diff.trim().length > 0
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function buildGatewayLogItems(lines: readonly string[]): readonly StatusbarMenuItem[] {
|
|
32
|
+
if (lines.length === 0) {
|
|
33
|
+
return [
|
|
34
|
+
{
|
|
35
|
+
className: 'text-muted-foreground',
|
|
36
|
+
disabled: true,
|
|
37
|
+
id: 'gateway-log-empty',
|
|
38
|
+
label: 'No recent gateway log lines'
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return lines.slice(-LOG_TAIL).map((line, index) => ({
|
|
44
|
+
className: 'font-mono text-[0.68rem] text-muted-foreground',
|
|
45
|
+
disabled: true,
|
|
46
|
+
id: `gateway-log:${index}`,
|
|
47
|
+
label: line.trim().slice(0, 120) || '(blank log line)'
|
|
48
|
+
}))
|
|
49
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { GatewayReauthRequiredError, isGatewayReauthRequired, resolveGatewayWsUrl } from './gateway-ws-url'
|
|
4
|
+
|
|
5
|
+
const oauthConn = { authMode: 'oauth' as const, wsUrl: 'ws://host/api/ws?ticket=stale' }
|
|
6
|
+
const tokenConn = { authMode: 'token' as const, wsUrl: 'ws://host/api/ws?token=abc' }
|
|
7
|
+
|
|
8
|
+
describe('resolveGatewayWsUrl', () => {
|
|
9
|
+
describe('oauth mode', () => {
|
|
10
|
+
it('uses the freshly minted URL', async () => {
|
|
11
|
+
const getGatewayWsUrl = vi.fn().mockResolvedValue('ws://host/api/ws?ticket=fresh')
|
|
12
|
+
await expect(resolveGatewayWsUrl({ getGatewayWsUrl }, oauthConn)).resolves.toBe('ws://host/api/ws?ticket=fresh')
|
|
13
|
+
expect(getGatewayWsUrl).toHaveBeenCalledOnce()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('throws a reauth error instead of falling back to the stale cached ticket', async () => {
|
|
17
|
+
const getGatewayWsUrl = vi.fn().mockRejectedValue(new Error('401 cookie expired'))
|
|
18
|
+
await expect(resolveGatewayWsUrl({ getGatewayWsUrl }, oauthConn)).rejects.toBeInstanceOf(
|
|
19
|
+
GatewayReauthRequiredError
|
|
20
|
+
)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('preserves the underlying mint failure as the cause', async () => {
|
|
24
|
+
const cause = new Error('401 cookie expired')
|
|
25
|
+
const getGatewayWsUrl = vi.fn().mockRejectedValue(cause)
|
|
26
|
+
const error = await resolveGatewayWsUrl({ getGatewayWsUrl }, oauthConn).catch(e => e)
|
|
27
|
+
expect(error).toBeInstanceOf(GatewayReauthRequiredError)
|
|
28
|
+
expect((error as GatewayReauthRequiredError).cause).toBe(cause)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('throws a reauth error when the preload cannot mint (no method)', async () => {
|
|
32
|
+
await expect(resolveGatewayWsUrl({}, oauthConn)).rejects.toBeInstanceOf(GatewayReauthRequiredError)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('never returns the stale cached ticket on failure', async () => {
|
|
36
|
+
const getGatewayWsUrl = vi.fn().mockRejectedValue(new Error('boom'))
|
|
37
|
+
const result = await resolveGatewayWsUrl({ getGatewayWsUrl }, oauthConn).catch(() => 'threw')
|
|
38
|
+
expect(result).toBe('threw')
|
|
39
|
+
expect(result).not.toBe(oauthConn.wsUrl)
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe('token / local mode', () => {
|
|
44
|
+
it('uses the minted URL when available', async () => {
|
|
45
|
+
const getGatewayWsUrl = vi.fn().mockResolvedValue('ws://host/api/ws?token=fresh')
|
|
46
|
+
await expect(resolveGatewayWsUrl({ getGatewayWsUrl }, tokenConn)).resolves.toBe('ws://host/api/ws?token=fresh')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('falls back to the cached URL when minting fails (token is long-lived)', async () => {
|
|
50
|
+
const getGatewayWsUrl = vi.fn().mockRejectedValue(new Error('transient'))
|
|
51
|
+
await expect(resolveGatewayWsUrl({ getGatewayWsUrl }, tokenConn)).resolves.toBe(tokenConn.wsUrl)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('falls back to the cached URL when the preload method is absent', async () => {
|
|
55
|
+
await expect(resolveGatewayWsUrl({}, tokenConn)).resolves.toBe(tokenConn.wsUrl)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('treats a missing authMode as non-oauth (falls back safely)', async () => {
|
|
59
|
+
await expect(resolveGatewayWsUrl({}, { wsUrl: tokenConn.wsUrl })).resolves.toBe(tokenConn.wsUrl)
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('isGatewayReauthRequired', () => {
|
|
65
|
+
it('detects the dedicated error class', () => {
|
|
66
|
+
expect(isGatewayReauthRequired(new GatewayReauthRequiredError('x'))).toBe(true)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('detects plain objects tagged with needsOauthLogin (from the main process)', () => {
|
|
70
|
+
expect(isGatewayReauthRequired({ needsOauthLogin: true })).toBe(true)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('rejects generic errors', () => {
|
|
74
|
+
expect(isGatewayReauthRequired(new Error('connection closed'))).toBe(false)
|
|
75
|
+
expect(isGatewayReauthRequired(null)).toBe(false)
|
|
76
|
+
expect(isGatewayReauthRequired('string')).toBe(false)
|
|
77
|
+
})
|
|
78
|
+
})
|