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,185 @@
|
|
|
1
|
+
import type { ComponentProps, ReactNode } from 'react'
|
|
2
|
+
import { useNavigate } from 'react-router-dom'
|
|
3
|
+
|
|
4
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
|
|
5
|
+
import { cn } from '@/lib/utils'
|
|
6
|
+
|
|
7
|
+
// Shared chrome styling for interactive statusbar items (button / link / menu
|
|
8
|
+
// trigger). The 'text' variant intentionally omits hover/transition/disabled.
|
|
9
|
+
const STATUSBAR_ACTION_CLASS =
|
|
10
|
+
'inline-flex h-full items-center gap-1 rounded-none px-1.5 text-[0.6875rem] text-(--ui-text-tertiary) transition-colors hover:bg-(--chrome-action-hover) hover:text-foreground disabled:cursor-default disabled:opacity-45'
|
|
11
|
+
|
|
12
|
+
export interface StatusbarMenuItem {
|
|
13
|
+
id: string
|
|
14
|
+
icon?: ReactNode
|
|
15
|
+
label: string
|
|
16
|
+
className?: string
|
|
17
|
+
disabled?: boolean
|
|
18
|
+
hidden?: boolean
|
|
19
|
+
href?: string
|
|
20
|
+
onSelect?: () => void
|
|
21
|
+
title?: string
|
|
22
|
+
to?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface StatusbarItem {
|
|
26
|
+
id: string
|
|
27
|
+
label?: ReactNode
|
|
28
|
+
detail?: ReactNode
|
|
29
|
+
icon?: ReactNode
|
|
30
|
+
className?: string
|
|
31
|
+
disabled?: boolean
|
|
32
|
+
hidden?: boolean
|
|
33
|
+
href?: string
|
|
34
|
+
menuAlign?: 'center' | 'end' | 'start'
|
|
35
|
+
menuClassName?: string
|
|
36
|
+
menuContent?: ReactNode
|
|
37
|
+
menuItems?: readonly StatusbarMenuItem[]
|
|
38
|
+
onSelect?: () => void
|
|
39
|
+
title?: string
|
|
40
|
+
to?: string
|
|
41
|
+
variant?: 'action' | 'link' | 'menu' | 'text'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type StatusbarItemSide = 'left' | 'right'
|
|
45
|
+
export type SetStatusbarItemGroup = (id: string, items: readonly StatusbarItem[], side?: StatusbarItemSide) => void
|
|
46
|
+
|
|
47
|
+
interface StatusbarControlsProps extends ComponentProps<'footer'> {
|
|
48
|
+
leftItems?: readonly StatusbarItem[]
|
|
49
|
+
items?: readonly StatusbarItem[]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function StatusbarControls({ className, leftItems = [], items = [], ...props }: StatusbarControlsProps) {
|
|
53
|
+
const navigate = useNavigate()
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<footer
|
|
57
|
+
className={cn(
|
|
58
|
+
'flex h-5 shrink-0 items-stretch justify-between gap-2 border-t border-(--ui-stroke-tertiary) bg-(--ui-sidebar-surface-background) px-1 py-0 text-(--ui-text-tertiary) [-webkit-app-region:no-drag]',
|
|
59
|
+
className
|
|
60
|
+
)}
|
|
61
|
+
{...props}
|
|
62
|
+
>
|
|
63
|
+
{/* `overflow-x-clip` (not `overflow-x-auto`) so a wide status item — for
|
|
64
|
+
example "Connecting…" on a fresh/untitled session — can't paint a
|
|
65
|
+
horizontal scrollbar across the bottom of the window. Items already
|
|
66
|
+
`truncate` their labels, so clipping is the right behavior. */}
|
|
67
|
+
<div className="flex min-w-0 items-stretch gap-0.5 overflow-x-clip">
|
|
68
|
+
{leftItems
|
|
69
|
+
.filter(item => !item.hidden)
|
|
70
|
+
.map(item => (
|
|
71
|
+
<StatusbarItemView item={item} key={`left:${item.id}`} navigate={navigate} />
|
|
72
|
+
))}
|
|
73
|
+
</div>
|
|
74
|
+
<div className="flex min-w-0 items-stretch gap-0.5 overflow-x-clip">
|
|
75
|
+
{items
|
|
76
|
+
.filter(item => !item.hidden)
|
|
77
|
+
.map(item => (
|
|
78
|
+
<StatusbarItemView item={item} key={`right:${item.id}`} navigate={navigate} />
|
|
79
|
+
))}
|
|
80
|
+
</div>
|
|
81
|
+
</footer>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function StatusbarItemView({ item, navigate }: { item: StatusbarItem; navigate: ReturnType<typeof useNavigate> }) {
|
|
86
|
+
const content = (
|
|
87
|
+
<>
|
|
88
|
+
{item.icon}
|
|
89
|
+
{item.label && <span className="truncate">{item.label}</span>}
|
|
90
|
+
{item.detail && <span className="truncate text-muted-foreground/80">{item.detail}</span>}
|
|
91
|
+
</>
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if (item.variant === 'menu' && (item.menuContent || (item.menuItems && item.menuItems.length > 0))) {
|
|
95
|
+
return (
|
|
96
|
+
<DropdownMenu>
|
|
97
|
+
<DropdownMenuTrigger asChild>
|
|
98
|
+
<button className={cn(STATUSBAR_ACTION_CLASS, item.className)} disabled={item.disabled} type="button">
|
|
99
|
+
{content}
|
|
100
|
+
</button>
|
|
101
|
+
</DropdownMenuTrigger>
|
|
102
|
+
<DropdownMenuContent
|
|
103
|
+
align={item.menuAlign ?? 'start'}
|
|
104
|
+
className={cn('w-56', item.menuContent && 'p-0', item.menuClassName)}
|
|
105
|
+
side="top"
|
|
106
|
+
sideOffset={8}
|
|
107
|
+
>
|
|
108
|
+
{item.menuContent
|
|
109
|
+
? item.menuContent
|
|
110
|
+
: (item.menuItems ?? [])
|
|
111
|
+
.filter(menuItem => !menuItem.hidden)
|
|
112
|
+
.map(menuItem => (
|
|
113
|
+
<DropdownMenuItem
|
|
114
|
+
className={cn('gap-2 text-foreground focus:bg-accent [&_svg]:size-4', menuItem.className)}
|
|
115
|
+
disabled={menuItem.disabled}
|
|
116
|
+
key={menuItem.id}
|
|
117
|
+
onSelect={() => {
|
|
118
|
+
if (menuItem.to) {
|
|
119
|
+
navigate(menuItem.to)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
menuItem.onSelect?.()
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
{menuItem.href ? (
|
|
126
|
+
<a
|
|
127
|
+
className="inline-flex w-full items-center gap-2"
|
|
128
|
+
href={menuItem.href}
|
|
129
|
+
rel="noreferrer"
|
|
130
|
+
target="_blank"
|
|
131
|
+
>
|
|
132
|
+
{menuItem.icon}
|
|
133
|
+
<span className="truncate">{menuItem.label}</span>
|
|
134
|
+
</a>
|
|
135
|
+
) : (
|
|
136
|
+
<>
|
|
137
|
+
{menuItem.icon}
|
|
138
|
+
<span className="truncate">{menuItem.label}</span>
|
|
139
|
+
</>
|
|
140
|
+
)}
|
|
141
|
+
</DropdownMenuItem>
|
|
142
|
+
))}
|
|
143
|
+
</DropdownMenuContent>
|
|
144
|
+
</DropdownMenu>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (item.variant === 'text' && !item.onSelect && !item.to && !item.href) {
|
|
149
|
+
return (
|
|
150
|
+
<div
|
|
151
|
+
className={cn(
|
|
152
|
+
'inline-flex h-full items-center gap-1 px-1.5 text-[0.6875rem] text-(--ui-text-tertiary)',
|
|
153
|
+
item.className
|
|
154
|
+
)}
|
|
155
|
+
>
|
|
156
|
+
{content}
|
|
157
|
+
</div>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (item.href || item.variant === 'link') {
|
|
162
|
+
return (
|
|
163
|
+
<a className={cn(STATUSBAR_ACTION_CLASS, item.className)} href={item.href} rel="noreferrer" target="_blank">
|
|
164
|
+
{content}
|
|
165
|
+
</a>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<button
|
|
171
|
+
className={cn(STATUSBAR_ACTION_CLASS, item.className)}
|
|
172
|
+
disabled={item.disabled}
|
|
173
|
+
onClick={() => {
|
|
174
|
+
if (item.to) {
|
|
175
|
+
navigate(item.to)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
item.onSelect?.()
|
|
179
|
+
}}
|
|
180
|
+
type="button"
|
|
181
|
+
>
|
|
182
|
+
{content}
|
|
183
|
+
</button>
|
|
184
|
+
)
|
|
185
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { useStore } from '@nanostores/react'
|
|
2
|
+
import type { ComponentProps, ReactNode } from 'react'
|
|
3
|
+
import { useLocation, useNavigate } from 'react-router-dom'
|
|
4
|
+
|
|
5
|
+
import { Button } from '@/components/ui/button'
|
|
6
|
+
import { Codicon } from '@/components/ui/codicon'
|
|
7
|
+
import { useI18n } from '@/i18n'
|
|
8
|
+
import { triggerHaptic } from '@/lib/haptics'
|
|
9
|
+
import { cn } from '@/lib/utils'
|
|
10
|
+
import { $hapticsMuted, toggleHapticsMuted } from '@/store/haptics'
|
|
11
|
+
import { toggleKeybindPanel } from '@/store/keybinds'
|
|
12
|
+
import {
|
|
13
|
+
$fileBrowserOpen,
|
|
14
|
+
$panesFlipped,
|
|
15
|
+
$sidebarOpen,
|
|
16
|
+
toggleFileBrowserOpen,
|
|
17
|
+
togglePanesFlipped,
|
|
18
|
+
toggleSidebarOpen
|
|
19
|
+
} from '@/store/layout'
|
|
20
|
+
|
|
21
|
+
import { appViewForPath, isOverlayView } from '../routes'
|
|
22
|
+
|
|
23
|
+
import { titlebarButtonClass } from './titlebar'
|
|
24
|
+
|
|
25
|
+
export interface TitlebarTool {
|
|
26
|
+
id: string
|
|
27
|
+
label: string
|
|
28
|
+
active?: boolean
|
|
29
|
+
className?: string
|
|
30
|
+
disabled?: boolean
|
|
31
|
+
hidden?: boolean
|
|
32
|
+
href?: string
|
|
33
|
+
icon: ReactNode
|
|
34
|
+
onSelect?: () => void
|
|
35
|
+
title?: string
|
|
36
|
+
to?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type TitlebarToolSide = 'left' | 'right'
|
|
40
|
+
export type SetTitlebarToolGroup = (id: string, tools: readonly TitlebarTool[], side?: TitlebarToolSide) => void
|
|
41
|
+
|
|
42
|
+
interface TitlebarControlsProps extends ComponentProps<'div'> {
|
|
43
|
+
leftTools?: readonly TitlebarTool[]
|
|
44
|
+
tools?: readonly TitlebarTool[]
|
|
45
|
+
onOpenSettings: () => void
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function TitlebarControls({ leftTools = [], tools = [], onOpenSettings }: TitlebarControlsProps) {
|
|
49
|
+
const { t } = useI18n()
|
|
50
|
+
const navigate = useNavigate()
|
|
51
|
+
const location = useLocation()
|
|
52
|
+
const hapticsMuted = useStore($hapticsMuted)
|
|
53
|
+
const fileBrowserOpen = useStore($fileBrowserOpen)
|
|
54
|
+
const sidebarOpen = useStore($sidebarOpen)
|
|
55
|
+
const panesFlipped = useStore($panesFlipped)
|
|
56
|
+
|
|
57
|
+
const toggleHaptics = () => {
|
|
58
|
+
if (!hapticsMuted) {
|
|
59
|
+
triggerHaptic('tap')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
toggleHapticsMuted()
|
|
63
|
+
|
|
64
|
+
if (hapticsMuted) {
|
|
65
|
+
window.requestAnimationFrame(() => triggerHaptic('success'))
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Each titlebar button controls the pane physically on its side, so a flip
|
|
70
|
+
// swaps which pane each one toggles. Default: sessions left, file browser
|
|
71
|
+
// right. Flipped: file browser left, sessions right. Sidebar toggles never
|
|
72
|
+
// carry an active highlight — they're plain show/hide affordances.
|
|
73
|
+
const fileBrowserEdge = { open: fileBrowserOpen, toggle: toggleFileBrowserOpen }
|
|
74
|
+
const sessionsEdge = { open: sidebarOpen, toggle: toggleSidebarOpen }
|
|
75
|
+
const leftEdge = panesFlipped ? fileBrowserEdge : sessionsEdge
|
|
76
|
+
const rightEdge = panesFlipped ? sessionsEdge : fileBrowserEdge
|
|
77
|
+
|
|
78
|
+
const leftToolbarTools: TitlebarTool[] = [
|
|
79
|
+
{
|
|
80
|
+
icon: <Codicon name="layout-sidebar-left" />,
|
|
81
|
+
id: 'sidebar',
|
|
82
|
+
label: leftEdge.open ? t.titlebar.hideSidebar : t.titlebar.showSidebar,
|
|
83
|
+
onSelect: () => {
|
|
84
|
+
triggerHaptic('tap')
|
|
85
|
+
leftEdge.toggle()
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
icon: <Codicon name="arrow-swap" />,
|
|
90
|
+
id: 'flip-panes',
|
|
91
|
+
label: t.titlebar.swapSidebarSides,
|
|
92
|
+
onSelect: () => {
|
|
93
|
+
triggerHaptic('tap')
|
|
94
|
+
togglePanesFlipped()
|
|
95
|
+
},
|
|
96
|
+
title: t.titlebar.swapSidebarSidesTitle
|
|
97
|
+
},
|
|
98
|
+
...leftTools
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
const rightSidebarTool: TitlebarTool = {
|
|
102
|
+
icon: <Codicon name="layout-sidebar-right" />,
|
|
103
|
+
id: 'right-sidebar',
|
|
104
|
+
label: rightEdge.open ? t.titlebar.hideRightSidebar : t.titlebar.showRightSidebar,
|
|
105
|
+
onSelect: () => {
|
|
106
|
+
triggerHaptic('tap')
|
|
107
|
+
rightEdge.toggle()
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Static system tools — always pinned to the screen's right edge.
|
|
112
|
+
const systemTools: TitlebarTool[] = [
|
|
113
|
+
{
|
|
114
|
+
active: hapticsMuted,
|
|
115
|
+
icon: <Codicon name={hapticsMuted ? 'mute' : 'unmute'} />,
|
|
116
|
+
id: 'haptics',
|
|
117
|
+
label: hapticsMuted ? t.titlebar.unmuteHaptics : t.titlebar.muteHaptics,
|
|
118
|
+
onSelect: toggleHaptics
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
icon: <Codicon name="keyboard" />,
|
|
122
|
+
id: 'keybinds',
|
|
123
|
+
label: t.titlebar.openKeybinds,
|
|
124
|
+
onSelect: () => {
|
|
125
|
+
triggerHaptic('open')
|
|
126
|
+
toggleKeybindPanel()
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
icon: <Codicon name="settings-gear" />,
|
|
131
|
+
id: 'settings',
|
|
132
|
+
label: t.titlebar.openSettings,
|
|
133
|
+
onSelect: () => {
|
|
134
|
+
triggerHaptic('open')
|
|
135
|
+
onOpenSettings()
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
// While a full-screen overlay (settings, command center, …) is open it should
|
|
141
|
+
// visually own the window. These control clusters are `fixed` at a higher
|
|
142
|
+
// z-index than the overlay card, so they'd otherwise bleed over it — hide them
|
|
143
|
+
// and let the overlay's own chrome (close button, drag region) take over.
|
|
144
|
+
if (isOverlayView(appViewForPath(location.pathname))) {
|
|
145
|
+
return null
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const visibleSystemTools = systemTools.filter(tool => !tool.hidden)
|
|
149
|
+
const settingsTool = visibleSystemTools.find(tool => tool.id === 'settings')
|
|
150
|
+
const visibleSystemToolsBeforeSettings = visibleSystemTools.filter(tool => tool.id !== 'settings')
|
|
151
|
+
const visiblePaneTools = tools.filter(tool => !tool.hidden)
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<>
|
|
155
|
+
<div
|
|
156
|
+
aria-label={t.shell.windowControls}
|
|
157
|
+
className="fixed left-(--titlebar-controls-left) top-(--titlebar-controls-top) z-70 flex translate-y-0.5 flex-row items-center gap-x-1 pointer-events-auto select-none [-webkit-app-region:no-drag]"
|
|
158
|
+
>
|
|
159
|
+
{leftToolbarTools
|
|
160
|
+
.filter(tool => !tool.hidden)
|
|
161
|
+
.map(tool => (
|
|
162
|
+
<TitlebarToolButton key={tool.id} navigate={navigate} tool={tool} />
|
|
163
|
+
))}
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
{/*
|
|
167
|
+
Pane-scoped tools (preview's monitor / devtools / refresh / X) render
|
|
168
|
+
as their own fixed cluster. AppShell sets --shell-preview-toolbar-gap
|
|
169
|
+
to either the static cluster's width (file-browser closed → cluster
|
|
170
|
+
sits flush against system tools) or the file-browser pane's width
|
|
171
|
+
(file-browser open → cluster sits flush against the file-browser pane,
|
|
172
|
+
i.e. at the preview pane's right edge). No margin hacks needed.
|
|
173
|
+
*/}
|
|
174
|
+
{visiblePaneTools.length > 0 && (
|
|
175
|
+
<div
|
|
176
|
+
aria-label={t.shell.paneControls}
|
|
177
|
+
className="fixed top-(--titlebar-controls-top) right-[calc(var(--titlebar-tools-right)+var(--shell-preview-toolbar-gap,0))] z-70 flex flex-row items-center gap-x-1 pointer-events-auto select-none [-webkit-app-region:no-drag]"
|
|
178
|
+
>
|
|
179
|
+
{visiblePaneTools.map(tool => (
|
|
180
|
+
<TitlebarToolButton key={tool.id} navigate={navigate} tool={tool} />
|
|
181
|
+
))}
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
|
|
185
|
+
<div
|
|
186
|
+
aria-label={t.shell.appControls}
|
|
187
|
+
className="fixed right-(--titlebar-tools-right) top-(--titlebar-controls-top) z-70 flex flex-row items-center justify-end gap-x-1 pointer-events-auto select-none [-webkit-app-region:no-drag]"
|
|
188
|
+
>
|
|
189
|
+
{visibleSystemToolsBeforeSettings.map(tool => (
|
|
190
|
+
<TitlebarToolButton key={tool.id} navigate={navigate} tool={tool} />
|
|
191
|
+
))}
|
|
192
|
+
{settingsTool && <TitlebarToolButton navigate={navigate} tool={settingsTool} />}
|
|
193
|
+
<TitlebarToolButton navigate={navigate} tool={rightSidebarTool} />
|
|
194
|
+
</div>
|
|
195
|
+
</>
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function TitlebarToolButton({ navigate, tool }: { navigate: ReturnType<typeof useNavigate>; tool: TitlebarTool }) {
|
|
200
|
+
// Titlebar actions never show an active background — state reads from the
|
|
201
|
+
// icon itself (e.g. the mute/unmute glyph). aria-pressed still carries it
|
|
202
|
+
// for a11y.
|
|
203
|
+
const className = cn(titlebarButtonClass, 'bg-transparent select-none', tool.className)
|
|
204
|
+
|
|
205
|
+
if (tool.href) {
|
|
206
|
+
return (
|
|
207
|
+
<Button asChild className={className} size="icon-titlebar" variant="ghost">
|
|
208
|
+
<a
|
|
209
|
+
aria-label={tool.label}
|
|
210
|
+
href={tool.href}
|
|
211
|
+
onPointerDown={event => event.stopPropagation()}
|
|
212
|
+
rel="noreferrer"
|
|
213
|
+
target="_blank"
|
|
214
|
+
title={tool.title ?? tool.label}
|
|
215
|
+
>
|
|
216
|
+
{tool.icon}
|
|
217
|
+
</a>
|
|
218
|
+
</Button>
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
<Button
|
|
224
|
+
aria-label={tool.label}
|
|
225
|
+
aria-pressed={tool.active ?? undefined}
|
|
226
|
+
className={className}
|
|
227
|
+
disabled={tool.disabled}
|
|
228
|
+
onClick={() => {
|
|
229
|
+
if (tool.to) {
|
|
230
|
+
navigate(tool.to)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
tool.onSelect?.()
|
|
234
|
+
}}
|
|
235
|
+
onPointerDown={event => event.stopPropagation()}
|
|
236
|
+
size="icon-titlebar"
|
|
237
|
+
title={tool.title ?? tool.label}
|
|
238
|
+
type="button"
|
|
239
|
+
variant="ghost"
|
|
240
|
+
>
|
|
241
|
+
{tool.icon}
|
|
242
|
+
</Button>
|
|
243
|
+
)
|
|
244
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
TITLEBAR_CONTROL_OFFSET_X,
|
|
5
|
+
TITLEBAR_EDGE_INSET,
|
|
6
|
+
TITLEBAR_FALLBACK_WINDOW_BUTTON_X,
|
|
7
|
+
titlebarControlsPosition
|
|
8
|
+
} from './titlebar'
|
|
9
|
+
|
|
10
|
+
describe('titlebarControlsPosition', () => {
|
|
11
|
+
it('offsets controls from visible traffic lights', () => {
|
|
12
|
+
expect(titlebarControlsPosition({ x: 24, y: 10 }).left).toBe(24 + TITLEBAR_CONTROL_OFFSET_X)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('pins to the edge when macOS fullscreen hides traffic lights', () => {
|
|
16
|
+
expect(titlebarControlsPosition({ x: 24, y: 10 }, true).left).toBe(TITLEBAR_EDGE_INSET)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('pins to the edge on Windows/Linux where native controls render on the right', () => {
|
|
20
|
+
expect(titlebarControlsPosition(null).left).toBe(TITLEBAR_EDGE_INSET)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('uses the macOS fallback while the initial window state is unknown', () => {
|
|
24
|
+
expect(titlebarControlsPosition(undefined).left).toBe(TITLEBAR_FALLBACK_WINDOW_BUTTON_X + TITLEBAR_CONTROL_OFFSET_X)
|
|
25
|
+
})
|
|
26
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { NasTechConnection } from '@/global'
|
|
2
|
+
|
|
3
|
+
export const TITLEBAR_HEIGHT = 34
|
|
4
|
+
export const MACOS_TRAFFIC_LIGHTS_HEIGHT = 14
|
|
5
|
+
export const TITLEBAR_ICON_SIZE = 12
|
|
6
|
+
export const TITLEBAR_CONTROL_OFFSET_X = 74
|
|
7
|
+
export const TITLEBAR_CONTROL_HEIGHT = 22
|
|
8
|
+
export const TITLEBAR_CONTROLS_TOP = (TITLEBAR_HEIGHT - TITLEBAR_CONTROL_HEIGHT) / 2
|
|
9
|
+
export const TITLEBAR_FALLBACK_WINDOW_BUTTON_X = 24
|
|
10
|
+
// Edge inset used when no left-side native controls take up that space —
|
|
11
|
+
// Windows/Linux (native overlay is on the right) and macOS fullscreen
|
|
12
|
+
// (traffic lights are hidden). Matches the right-cluster's 0.75rem padding.
|
|
13
|
+
export const TITLEBAR_EDGE_INSET = 14
|
|
14
|
+
|
|
15
|
+
// Titlebar palette only. All sizing/radius/cursor/centering come from the
|
|
16
|
+
// shared <Button size="icon-titlebar"> (used polymorphically via asChild) —
|
|
17
|
+
// Button is the single source of button styling.
|
|
18
|
+
export const titlebarButtonClass =
|
|
19
|
+
'text-muted-foreground/85 hover:bg-(--ui-control-hover-background) hover:text-foreground'
|
|
20
|
+
|
|
21
|
+
export const titlebarHeaderBaseClass =
|
|
22
|
+
'pointer-events-none relative z-3 flex h-(--titlebar-height) shrink-0 items-center justify-start gap-3 border-b border-(--ui-stroke-tertiary) bg-(--ui-chat-surface-background) px-[max(0.75rem,var(--titlebar-content-inset,0rem))]'
|
|
23
|
+
|
|
24
|
+
export const titlebarHeaderShadowClass =
|
|
25
|
+
"after:pointer-events-none after:absolute after:left-0 after:right-0 after:top-full after:h-4 after:bg-linear-to-b after:from-(--ui-chat-surface-background) after:to-transparent after:content-['']"
|
|
26
|
+
|
|
27
|
+
export function titlebarControlsPosition(
|
|
28
|
+
windowButtonPosition: NasTechConnection['windowButtonPosition'] | undefined,
|
|
29
|
+
isFullscreen = false
|
|
30
|
+
) {
|
|
31
|
+
const top = Math.max(0, TITLEBAR_CONTROLS_TOP)
|
|
32
|
+
|
|
33
|
+
// No left-side native controls to dodge:
|
|
34
|
+
// - Windows/Linux: native min/max/close render on the right via titleBarOverlay.
|
|
35
|
+
// - macOS fullscreen: traffic lights are hidden.
|
|
36
|
+
// In both cases, pin the cluster to the edge with a small inset.
|
|
37
|
+
if (windowButtonPosition === null || isFullscreen) {
|
|
38
|
+
return { left: TITLEBAR_EDGE_INSET, top }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
left: (windowButtonPosition?.x ?? TITLEBAR_FALLBACK_WINDOW_BUTTON_X) + TITLEBAR_CONTROL_OFFSET_X,
|
|
43
|
+
top
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
type Side = 'left' | 'right'
|
|
4
|
+
type Groups<T> = Record<Side, Record<string, readonly T[]>>
|
|
5
|
+
|
|
6
|
+
export type GroupSetter<T> = (id: string, items: readonly T[], side?: Side) => void
|
|
7
|
+
|
|
8
|
+
interface GroupRegistry<T> {
|
|
9
|
+
flat: { left: T[]; right: T[] }
|
|
10
|
+
set: GroupSetter<T>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function useGroupRegistry<T>(): GroupRegistry<T> {
|
|
14
|
+
const [groups, setGroups] = useState<Groups<T>>({ left: {}, right: {} })
|
|
15
|
+
|
|
16
|
+
const set = useCallback<GroupSetter<T>>((id, items, side = 'right') => {
|
|
17
|
+
setGroups(current => {
|
|
18
|
+
const next = { ...current, [side]: { ...current[side] } }
|
|
19
|
+
|
|
20
|
+
if (items.length === 0) {
|
|
21
|
+
delete next[side][id]
|
|
22
|
+
} else {
|
|
23
|
+
next[side][id] = items
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return next
|
|
27
|
+
})
|
|
28
|
+
}, [])
|
|
29
|
+
|
|
30
|
+
const flat = useMemo(
|
|
31
|
+
() => ({
|
|
32
|
+
left: Object.values(groups.left).flat(),
|
|
33
|
+
right: Object.values(groups.right).flat()
|
|
34
|
+
}),
|
|
35
|
+
[groups]
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return { flat, set }
|
|
39
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
2
|
+
import { MemoryRouter } from 'react-router-dom'
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
4
|
+
|
|
5
|
+
const getSkills = vi.fn()
|
|
6
|
+
const getToolsets = vi.fn()
|
|
7
|
+
const toggleSkill = vi.fn()
|
|
8
|
+
const toggleToolset = vi.fn()
|
|
9
|
+
const getToolsetConfig = vi.fn()
|
|
10
|
+
const selectToolsetProvider = vi.fn()
|
|
11
|
+
|
|
12
|
+
vi.mock('@/nastech', () => ({
|
|
13
|
+
getSkills: () => getSkills(),
|
|
14
|
+
getToolsets: () => getToolsets(),
|
|
15
|
+
toggleSkill: (name: string, enabled: boolean) => toggleSkill(name, enabled),
|
|
16
|
+
toggleToolset: (name: string, enabled: boolean) => toggleToolset(name, enabled),
|
|
17
|
+
getToolsetConfig: (name: string) => getToolsetConfig(name),
|
|
18
|
+
selectToolsetProvider: (toolset: string, provider: string) => selectToolsetProvider(toolset, provider),
|
|
19
|
+
deleteEnvVar: vi.fn(),
|
|
20
|
+
revealEnvVar: vi.fn(),
|
|
21
|
+
setEnvVar: vi.fn()
|
|
22
|
+
}))
|
|
23
|
+
|
|
24
|
+
// Notifications hit nanostores/timers we don't care about here.
|
|
25
|
+
vi.mock('@/store/notifications', () => ({
|
|
26
|
+
notify: vi.fn(),
|
|
27
|
+
notifyError: vi.fn()
|
|
28
|
+
}))
|
|
29
|
+
|
|
30
|
+
function toolset(overrides: Record<string, unknown> = {}) {
|
|
31
|
+
return {
|
|
32
|
+
name: 'web',
|
|
33
|
+
label: 'Web Search',
|
|
34
|
+
description: 'web_search, web_extract',
|
|
35
|
+
enabled: true,
|
|
36
|
+
available: true,
|
|
37
|
+
configured: true,
|
|
38
|
+
tools: ['web_search', 'web_extract'],
|
|
39
|
+
...overrides
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function renderSkills() {
|
|
44
|
+
return import('./index').then(({ SkillsView }) =>
|
|
45
|
+
render(
|
|
46
|
+
<MemoryRouter initialEntries={['/skills?tab=toolsets']}>
|
|
47
|
+
<SkillsView />
|
|
48
|
+
</MemoryRouter>
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
getSkills.mockResolvedValue([])
|
|
55
|
+
getToolsets.mockResolvedValue([toolset()])
|
|
56
|
+
toggleToolset.mockResolvedValue({ ok: true, name: 'web', enabled: false })
|
|
57
|
+
getToolsetConfig.mockResolvedValue({ has_category: false, active_provider: null, providers: [] })
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
cleanup()
|
|
62
|
+
vi.clearAllMocks()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('SkillsView toolset management', () => {
|
|
66
|
+
it('renders a switch for each toolset and toggles it off', async () => {
|
|
67
|
+
await renderSkills()
|
|
68
|
+
|
|
69
|
+
const sw = await screen.findByRole('switch', { name: 'Toggle Web Search toolset' })
|
|
70
|
+
expect(sw.getAttribute('aria-checked')).toBe('true')
|
|
71
|
+
|
|
72
|
+
fireEvent.click(sw)
|
|
73
|
+
|
|
74
|
+
await waitFor(() => expect(toggleToolset).toHaveBeenCalledWith('web', false))
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('renders toolset titles without leading emoji', async () => {
|
|
78
|
+
getToolsets.mockResolvedValue([
|
|
79
|
+
toolset({ name: 'cronjob', label: '⏰ Cron Jobs', description: 'cron tools' })
|
|
80
|
+
])
|
|
81
|
+
|
|
82
|
+
await renderSkills()
|
|
83
|
+
|
|
84
|
+
expect(await screen.findByText('Cron Jobs')).toBeTruthy()
|
|
85
|
+
expect(screen.queryByText(/⏰/)).toBeNull()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('keeps the configured pill alongside the switch', async () => {
|
|
89
|
+
await renderSkills()
|
|
90
|
+
|
|
91
|
+
await screen.findByRole('switch', { name: 'Toggle Web Search toolset' })
|
|
92
|
+
expect(screen.getByText('Configured')).toBeTruthy()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('expands the provider config panel when the configured pill is clicked', async () => {
|
|
96
|
+
await renderSkills()
|
|
97
|
+
|
|
98
|
+
const configureBtn = await screen.findByRole('button', { name: 'Configure Web Search' })
|
|
99
|
+
fireEvent.click(configureBtn)
|
|
100
|
+
|
|
101
|
+
await waitFor(() => expect(getToolsetConfig).toHaveBeenCalledWith('web'))
|
|
102
|
+
})
|
|
103
|
+
})
|