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,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-installed desktop themes (currently: converted VS Code themes).
|
|
3
|
+
*
|
|
4
|
+
* This is the extensibility seam. The theme context reads the *merged* registry
|
|
5
|
+
* (built-ins + user themes) for `availableThemes` and for every skin lookup, so
|
|
6
|
+
* an installed theme shows up everywhere a built-in does — the Cmd-K palette,
|
|
7
|
+
* the Appearance settings grid, and `/skin` — with no per-surface wiring.
|
|
8
|
+
*
|
|
9
|
+
* Stored as a localStorage record so the boot-time paint (which runs before
|
|
10
|
+
* React mounts) can resolve a user theme synchronously, same as built-ins.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { atom } from 'nanostores'
|
|
14
|
+
|
|
15
|
+
import { BUILTIN_THEMES } from './presets'
|
|
16
|
+
import type { DesktopTheme, DesktopThemeColors } from './types'
|
|
17
|
+
|
|
18
|
+
const USER_THEMES_KEY = 'nastech-desktop-user-themes-v1'
|
|
19
|
+
|
|
20
|
+
// The minimal set of color keys a stored theme must carry to be usable. We keep
|
|
21
|
+
// this loose — `applyTheme` tolerates missing optionals via fallbacks — but a
|
|
22
|
+
// theme with no background/foreground/primary is junk and gets dropped.
|
|
23
|
+
const REQUIRED_COLOR_KEYS: ReadonlyArray<keyof DesktopThemeColors> = ['background', 'foreground', 'primary']
|
|
24
|
+
|
|
25
|
+
function isValidTheme(value: unknown): value is DesktopTheme {
|
|
26
|
+
if (!value || typeof value !== 'object') {
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const theme = value as Partial<DesktopTheme>
|
|
31
|
+
|
|
32
|
+
if (typeof theme.name !== 'string' || typeof theme.label !== 'string' || !theme.colors) {
|
|
33
|
+
return false
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const colors = theme.colors as unknown as Record<string, unknown>
|
|
37
|
+
|
|
38
|
+
return REQUIRED_COLOR_KEYS.every(key => typeof colors[key] === 'string')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readStored(): Record<string, DesktopTheme> {
|
|
42
|
+
try {
|
|
43
|
+
const raw = window.localStorage.getItem(USER_THEMES_KEY)
|
|
44
|
+
|
|
45
|
+
if (!raw) {
|
|
46
|
+
return {}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const parsed: unknown = JSON.parse(raw)
|
|
50
|
+
|
|
51
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
52
|
+
return {}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const out: Record<string, DesktopTheme> = {}
|
|
56
|
+
|
|
57
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
58
|
+
// Never let a stored theme shadow a built-in name.
|
|
59
|
+
if (!BUILTIN_THEMES[key] && isValidTheme(value)) {
|
|
60
|
+
out[key] = value
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return out
|
|
65
|
+
} catch {
|
|
66
|
+
return {}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function persist(record: Record<string, DesktopTheme>) {
|
|
71
|
+
try {
|
|
72
|
+
window.localStorage.setItem(USER_THEMES_KEY, JSON.stringify(record))
|
|
73
|
+
} catch {
|
|
74
|
+
// Best-effort: a restricted storage context shouldn't break theming.
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Reactive map of installed user themes, keyed by slug. */
|
|
79
|
+
export const $userThemes = atom<Record<string, DesktopTheme>>(typeof window === 'undefined' ? {} : readStored())
|
|
80
|
+
|
|
81
|
+
/** Install (or replace) a user theme. Returns the stored theme. */
|
|
82
|
+
export function installUserTheme(theme: DesktopTheme): DesktopTheme {
|
|
83
|
+
if (BUILTIN_THEMES[theme.name]) {
|
|
84
|
+
throw new Error(`"${theme.name}" collides with a built-in theme.`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!isValidTheme(theme)) {
|
|
88
|
+
throw new Error('Theme is missing required colors.')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const next = { ...$userThemes.get(), [theme.name]: theme }
|
|
92
|
+
$userThemes.set(next)
|
|
93
|
+
persist(next)
|
|
94
|
+
|
|
95
|
+
return theme
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Remove a user theme by slug. No-op for unknown / built-in names. */
|
|
99
|
+
export function removeUserTheme(name: string): void {
|
|
100
|
+
const current = $userThemes.get()
|
|
101
|
+
|
|
102
|
+
if (!current[name]) {
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const next = { ...current }
|
|
107
|
+
delete next[name]
|
|
108
|
+
$userThemes.set(next)
|
|
109
|
+
persist(next)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export const isUserTheme = (name: string): boolean => Boolean($userThemes.get()[name])
|
|
113
|
+
|
|
114
|
+
/** Resolve a theme by name across the merged registry (built-in + user). */
|
|
115
|
+
export function resolveTheme(name: string): DesktopTheme | undefined {
|
|
116
|
+
return BUILTIN_THEMES[name] ?? $userThemes.get()[name]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Built-ins first (stable order), then user themes by install order. */
|
|
120
|
+
export function listAllThemes(): DesktopTheme[] {
|
|
121
|
+
return [...Object.values(BUILTIN_THEMES), ...Object.values($userThemes.get())]
|
|
122
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { contrastRatio } from './color'
|
|
4
|
+
import { convertVscodeColorTheme, parseVscodeTheme, vscodeThemeSlug } from './vscode'
|
|
5
|
+
|
|
6
|
+
describe('vscodeThemeSlug', () => {
|
|
7
|
+
it('namespaces, lowercases, and dashes', () => {
|
|
8
|
+
expect(vscodeThemeSlug('Dracula Soft')).toBe('vsc-dracula-soft')
|
|
9
|
+
expect(vscodeThemeSlug(' One Dark Pro!! ')).toBe('vsc-one-dark-pro')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('falls back when the name has no usable characters', () => {
|
|
13
|
+
expect(vscodeThemeSlug('—')).toBe('vsc-theme')
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
describe('parseVscodeTheme (JSONC tolerance)', () => {
|
|
18
|
+
it('strips comments and trailing commas', () => {
|
|
19
|
+
const text = `{
|
|
20
|
+
// a line comment
|
|
21
|
+
"name": "Demo",
|
|
22
|
+
/* block comment */
|
|
23
|
+
"type": "dark",
|
|
24
|
+
"colors": {
|
|
25
|
+
"editor.background": "#1e1e2e", // inline
|
|
26
|
+
},
|
|
27
|
+
}`
|
|
28
|
+
|
|
29
|
+
const parsed = parseVscodeTheme(text)
|
|
30
|
+
expect(parsed.name).toBe('Demo')
|
|
31
|
+
expect(parsed.colors?.['editor.background']).toBe('#1e1e2e')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('throws on a non-object', () => {
|
|
35
|
+
expect(() => parseVscodeTheme('42')).toThrow()
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe('convertVscodeColorTheme', () => {
|
|
40
|
+
const dracula = {
|
|
41
|
+
name: 'Dracula',
|
|
42
|
+
type: 'dark',
|
|
43
|
+
colors: {
|
|
44
|
+
'editor.background': '#282a36',
|
|
45
|
+
'editor.foreground': '#f8f8f2',
|
|
46
|
+
focusBorder: '#6272a4',
|
|
47
|
+
'editorWidget.background': '#21222c',
|
|
48
|
+
'sideBar.background': '#21222c',
|
|
49
|
+
errorForeground: '#ff5555',
|
|
50
|
+
// 8-digit hex (alpha) — must flatten over the background.
|
|
51
|
+
'panel.border': '#bd93f900'
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
it('maps the load-bearing tokens onto the palette', () => {
|
|
56
|
+
const { theme } = convertVscodeColorTheme(dracula, { source: 'dracula-theme.theme-dracula' })
|
|
57
|
+
|
|
58
|
+
expect(theme.name).toBe('vsc-dracula')
|
|
59
|
+
expect(theme.label).toBe('Dracula')
|
|
60
|
+
expect(theme.description).toContain('dracula-theme.theme-dracula')
|
|
61
|
+
expect(theme.colors.background).toBe('#282a36')
|
|
62
|
+
expect(theme.colors.foreground).toBe('#f8f8f2')
|
|
63
|
+
// One accent drives primary + ring + midground together...
|
|
64
|
+
expect(theme.colors.ring).toBe(theme.colors.primary)
|
|
65
|
+
expect(theme.colors.midground).toBe(theme.colors.primary)
|
|
66
|
+
// ...and it's nudged until it reads on the sidebar it labels (the dim
|
|
67
|
+
// focusBorder #6272a4 sits below AA, so it's lifted).
|
|
68
|
+
expect(contrastRatio(theme.colors.primary, theme.colors.sidebarBackground!)).toBeGreaterThanOrEqual(4.5)
|
|
69
|
+
expect(theme.colors.popover).toBe('#21222c')
|
|
70
|
+
expect(theme.colors.sidebarBackground).toBe('#21222c')
|
|
71
|
+
expect(theme.colors.destructive).toBe('#ff5555')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('flattens alpha hex over the background (no #rrggbbaa leaks)', () => {
|
|
75
|
+
const { theme } = convertVscodeColorTheme(dracula)
|
|
76
|
+
expect(theme.colors.border).toMatch(/^#[0-9a-f]{6}$/)
|
|
77
|
+
// 00 alpha over the bg means the border collapses to the background.
|
|
78
|
+
expect(theme.colors.border).toBe('#282a36')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('renders identically in both modes (single palette in both slots)', () => {
|
|
82
|
+
const { theme } = convertVscodeColorTheme(dracula)
|
|
83
|
+
expect(theme.darkColors).toBe(theme.colors)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('records derived fallbacks for omitted tokens', () => {
|
|
87
|
+
const { derived } = convertVscodeColorTheme({
|
|
88
|
+
name: 'Sparse',
|
|
89
|
+
type: 'dark',
|
|
90
|
+
colors: { 'editor.background': '#101010', 'editor.foreground': '#fafafa' }
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// No accent/elevated/sidebar/error tokens → all derived. The accent records
|
|
94
|
+
// its first candidate (button.background) when none of the family is present.
|
|
95
|
+
expect(derived).toContain('button.background')
|
|
96
|
+
expect(derived).toContain('editorWidget.background')
|
|
97
|
+
expect(derived).toContain('editorError.foreground')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('buckets light vs dark from background luminance when type is absent', () => {
|
|
101
|
+
const light = convertVscodeColorTheme({
|
|
102
|
+
name: 'Bright',
|
|
103
|
+
colors: { 'editor.background': '#ffffff', 'editor.foreground': '#1a1a1a' }
|
|
104
|
+
}).theme
|
|
105
|
+
|
|
106
|
+
// A light background should keep a near-white background, not synth dark.
|
|
107
|
+
expect(light.colors.background).toBe('#ffffff')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('throws when there is no colors map', () => {
|
|
111
|
+
expect(() => convertVscodeColorTheme({ name: 'Empty' })).toThrow(/colors/)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const fullAnsi = {
|
|
115
|
+
'terminal.ansiBlack': '#073642',
|
|
116
|
+
'terminal.ansiRed': '#dc322f',
|
|
117
|
+
'terminal.ansiGreen': '#859900',
|
|
118
|
+
'terminal.ansiYellow': '#b58900',
|
|
119
|
+
'terminal.ansiBlue': '#268bd2',
|
|
120
|
+
'terminal.ansiMagenta': '#d33682',
|
|
121
|
+
'terminal.ansiCyan': '#2aa198',
|
|
122
|
+
'terminal.ansiWhite': '#eee8d5',
|
|
123
|
+
'terminal.ansiBrightBlack': '#002b36',
|
|
124
|
+
'terminal.ansiBrightRed': '#cb4b16',
|
|
125
|
+
'terminal.ansiBrightGreen': '#586e75',
|
|
126
|
+
'terminal.ansiBrightYellow': '#657b83',
|
|
127
|
+
'terminal.ansiBrightBlue': '#839496',
|
|
128
|
+
'terminal.ansiBrightMagenta': '#6c71c4',
|
|
129
|
+
'terminal.ansiBrightCyan': '#93a1a1',
|
|
130
|
+
'terminal.ansiBrightWhite': '#fdf6e3'
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
it('lifts the ANSI palette when the full base-8 set is present', () => {
|
|
134
|
+
const { theme } = convertVscodeColorTheme({
|
|
135
|
+
name: 'Solarized Dark',
|
|
136
|
+
type: 'dark',
|
|
137
|
+
colors: {
|
|
138
|
+
'editor.background': '#002b36',
|
|
139
|
+
'editor.foreground': '#93a1a1',
|
|
140
|
+
'terminal.foreground': '#839496',
|
|
141
|
+
'terminalCursor.foreground': '#93a1a1',
|
|
142
|
+
// Alpha selection must survive un-flattened — xterm blends it.
|
|
143
|
+
'terminal.selectionBackground': '#073642aa',
|
|
144
|
+
...fullAnsi
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
expect(theme.terminal?.red).toBe('#dc322f')
|
|
149
|
+
expect(theme.terminal?.brightWhite).toBe('#fdf6e3')
|
|
150
|
+
expect(theme.terminal?.foreground).toBe('#839496')
|
|
151
|
+
expect(theme.terminal?.cursor).toBe('#93a1a1')
|
|
152
|
+
expect(theme.terminal?.selectionBackground).toBe('#073642aa')
|
|
153
|
+
// No background slot — the pane keeps the live surface (transparency).
|
|
154
|
+
expect('background' in (theme.terminal ?? {})).toBe(false)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('keeps the default palette (no terminal slot) when the ANSI set is partial', () => {
|
|
158
|
+
const { theme } = convertVscodeColorTheme({
|
|
159
|
+
name: 'Half',
|
|
160
|
+
type: 'dark',
|
|
161
|
+
colors: {
|
|
162
|
+
'editor.background': '#101010',
|
|
163
|
+
'editor.foreground': '#fafafa',
|
|
164
|
+
'terminal.ansiRed': '#ff0000',
|
|
165
|
+
'terminal.ansiGreen': '#00ff00'
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
expect(theme.terminal).toBeUndefined()
|
|
170
|
+
})
|
|
171
|
+
})
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VS Code color-theme → DesktopTheme converter.
|
|
3
|
+
*
|
|
4
|
+
* VS Code themes carry ~hundreds of `workbench.colorCustomization` keys, but the
|
|
5
|
+
* desktop theme model only needs a `DesktopThemeColors` struct — `applyTheme`
|
|
6
|
+
* derives every glass/shadcn token from a small seed chain via `color-mix()`.
|
|
7
|
+
* In practice ~6 workbench keys carry the whole look (background, foreground,
|
|
8
|
+
* accent, elevated surface, sidebar, error); everything else we derive by mixing
|
|
9
|
+
* those toward the background/foreground. That's the "naive token converter".
|
|
10
|
+
*
|
|
11
|
+
* A VS Code theme is single-mode (light OR dark). Rather than synthesise the
|
|
12
|
+
* opposite mode, we set both `colors` and `darkColors` to the converted palette
|
|
13
|
+
* so the imported theme renders faithfully no matter where the light/dark toggle
|
|
14
|
+
* sits — `renderedModeFor` still picks the `.dark` class from the real
|
|
15
|
+
* background luminance, so surface-bound UI matches what's on screen.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { ensureContrast, luminance, mix, normalizeHex, readableOn } from './color'
|
|
19
|
+
import type { DesktopTerminalPalette, DesktopTheme, DesktopThemeColors } from './types'
|
|
20
|
+
|
|
21
|
+
// Section headers / sidebar labels render in --theme-primary directly on the
|
|
22
|
+
// sidebar surface as small (~10px) uppercase text, so the accent has to clear
|
|
23
|
+
// WCAG AA for normal text (4.5:1) or it's unreadable — the "invisible purple
|
|
24
|
+
// label" case. Imported accents below this get nudged lighter/darker.
|
|
25
|
+
const ACCENT_MIN_CONTRAST = 4.5
|
|
26
|
+
|
|
27
|
+
/** The shape of a VS Code `*-color-theme.json` (only the fields we read). */
|
|
28
|
+
export interface VscodeColorTheme {
|
|
29
|
+
name?: string
|
|
30
|
+
type?: string
|
|
31
|
+
/** Relative path to a base theme this one extends. We don't follow it. */
|
|
32
|
+
include?: string
|
|
33
|
+
colors?: Record<string, unknown>
|
|
34
|
+
tokenColors?: unknown
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ConvertOptions {
|
|
38
|
+
/** Stable id (slug). Defaults to a slug of `raw.name`. */
|
|
39
|
+
slug?: string
|
|
40
|
+
/** Display label. Defaults to `raw.name`. */
|
|
41
|
+
label?: string
|
|
42
|
+
/** Shown under the label in the picker (e.g. the marketplace extension id). */
|
|
43
|
+
source?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ConvertResult {
|
|
47
|
+
theme: DesktopTheme
|
|
48
|
+
/** The source theme's own light/dark (from `type`, else background luminance). */
|
|
49
|
+
mode: 'light' | 'dark'
|
|
50
|
+
/** Workbench keys we wanted but the theme omitted (we derived fallbacks). */
|
|
51
|
+
derived: string[]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Tolerant slug: lowercase, alnum + dashes, deduped, `vsc-` namespaced. */
|
|
55
|
+
export function vscodeThemeSlug(name: string): string {
|
|
56
|
+
const base = name
|
|
57
|
+
.trim()
|
|
58
|
+
.toLowerCase()
|
|
59
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
60
|
+
.replace(/^-+|-+$/g, '')
|
|
61
|
+
.slice(0, 48)
|
|
62
|
+
|
|
63
|
+
return `vsc-${base || 'theme'}`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Parse a VS Code theme file. These ship as JSONC (line/block comments and
|
|
68
|
+
* trailing commas), so a plain `JSON.parse` rejects most real-world files.
|
|
69
|
+
* Strips comments + trailing commas, then parses. Throws on hard syntax errors.
|
|
70
|
+
*/
|
|
71
|
+
export function parseVscodeTheme(text: string): VscodeColorTheme {
|
|
72
|
+
const stripped = text
|
|
73
|
+
// Block comments.
|
|
74
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
75
|
+
// Line comments (not inside strings — naive but fine for theme files).
|
|
76
|
+
.replace(/(^|[^:"'\\])\/\/[^\n\r]*/g, '$1')
|
|
77
|
+
// Trailing commas before } or ].
|
|
78
|
+
.replace(/,(\s*[}\]])/g, '$1')
|
|
79
|
+
|
|
80
|
+
const parsed: unknown = JSON.parse(stripped)
|
|
81
|
+
|
|
82
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
83
|
+
throw new Error('Theme file is not a JSON object.')
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return parsed as VscodeColorTheme
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const isDarkType = (raw: VscodeColorTheme, background: string): boolean => {
|
|
90
|
+
const type = (raw.type ?? '').toLowerCase()
|
|
91
|
+
|
|
92
|
+
if (type.includes('light')) {
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (type === 'dark' || type === 'hc' || type === 'hc-black' || type.includes('dark')) {
|
|
97
|
+
return true
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// No usable `type` — bucket by background luminance.
|
|
101
|
+
return luminance(background) < 0.4
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// xterm ITheme ANSI slots ← VS Code `terminal.ansi*` tokens. Background is
|
|
105
|
+
// deliberately excluded — the pane keeps the live skin surface (transparency).
|
|
106
|
+
const ANSI_TOKENS: ReadonlyArray<readonly [keyof DesktopTerminalPalette, string]> = [
|
|
107
|
+
['black', 'terminal.ansiBlack'],
|
|
108
|
+
['red', 'terminal.ansiRed'],
|
|
109
|
+
['green', 'terminal.ansiGreen'],
|
|
110
|
+
['yellow', 'terminal.ansiYellow'],
|
|
111
|
+
['blue', 'terminal.ansiBlue'],
|
|
112
|
+
['magenta', 'terminal.ansiMagenta'],
|
|
113
|
+
['cyan', 'terminal.ansiCyan'],
|
|
114
|
+
['white', 'terminal.ansiWhite'],
|
|
115
|
+
['brightBlack', 'terminal.ansiBrightBlack'],
|
|
116
|
+
['brightRed', 'terminal.ansiBrightRed'],
|
|
117
|
+
['brightGreen', 'terminal.ansiBrightGreen'],
|
|
118
|
+
['brightYellow', 'terminal.ansiBrightYellow'],
|
|
119
|
+
['brightBlue', 'terminal.ansiBrightBlue'],
|
|
120
|
+
['brightMagenta', 'terminal.ansiBrightMagenta'],
|
|
121
|
+
['brightCyan', 'terminal.ansiBrightCyan'],
|
|
122
|
+
['brightWhite', 'terminal.ansiBrightWhite']
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
const BASE_ANSI: ReadonlyArray<keyof DesktopTerminalPalette> = [
|
|
126
|
+
'black',
|
|
127
|
+
'red',
|
|
128
|
+
'green',
|
|
129
|
+
'yellow',
|
|
130
|
+
'blue',
|
|
131
|
+
'magenta',
|
|
132
|
+
'cyan',
|
|
133
|
+
'white'
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
const HEX_RE = /^#[0-9a-f]{3,8}$/i
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Lift a theme's integrated-terminal ANSI palette, if it ships one.
|
|
140
|
+
*
|
|
141
|
+
* All-or-nothing on the base-8 colors: a half-filled palette mixed with our
|
|
142
|
+
* defaults reads worse than just keeping the defaults, so we adopt the theme's
|
|
143
|
+
* palette only when the full base set is present. ANSI slots flatten alpha over
|
|
144
|
+
* the editor background; selection keeps its alpha so xterm can blend it.
|
|
145
|
+
*/
|
|
146
|
+
function extractTerminalPalette(colors: Record<string, unknown>, background: string): DesktopTerminalPalette | undefined {
|
|
147
|
+
const hex = (key: string): string | undefined =>
|
|
148
|
+
normalizeHex(typeof colors[key] === 'string' ? (colors[key] as string) : null, background) ?? undefined
|
|
149
|
+
|
|
150
|
+
const palette: DesktopTerminalPalette = {}
|
|
151
|
+
|
|
152
|
+
for (const [slot, token] of ANSI_TOKENS) {
|
|
153
|
+
const value = hex(token)
|
|
154
|
+
|
|
155
|
+
if (value) {
|
|
156
|
+
palette[slot] = value
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!BASE_ANSI.every(slot => palette[slot])) {
|
|
161
|
+
return undefined
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const foreground = hex('terminal.foreground')
|
|
165
|
+
const cursor = hex('terminalCursor.foreground') ?? hex('terminalCursor.background')
|
|
166
|
+
const selection = typeof colors['terminal.selectionBackground'] === 'string' ? colors['terminal.selectionBackground'].trim() : ''
|
|
167
|
+
|
|
168
|
+
if (foreground) {
|
|
169
|
+
palette.foreground = foreground
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (cursor) {
|
|
173
|
+
palette.cursor = cursor
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (HEX_RE.test(selection)) {
|
|
177
|
+
palette.selectionBackground = selection
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return palette
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** First normalizable hex among `keys`, composited over `backdrop`. */
|
|
184
|
+
const pick = (
|
|
185
|
+
colors: Record<string, unknown>,
|
|
186
|
+
keys: string[],
|
|
187
|
+
backdrop: string
|
|
188
|
+
): { key: string; value: string } | null => {
|
|
189
|
+
for (const key of keys) {
|
|
190
|
+
const value = normalizeHex(typeof colors[key] === 'string' ? (colors[key] as string) : null, backdrop)
|
|
191
|
+
|
|
192
|
+
if (value) {
|
|
193
|
+
return { key, value }
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return null
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function convertVscodeColorTheme(raw: VscodeColorTheme, opts: ConvertOptions = {}): ConvertResult {
|
|
201
|
+
const colors = raw.colors && typeof raw.colors === 'object' ? (raw.colors as Record<string, unknown>) : null
|
|
202
|
+
|
|
203
|
+
if (!colors) {
|
|
204
|
+
throw new Error('Theme has no "colors" map — not a VS Code color theme.')
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const derived: string[] = []
|
|
208
|
+
|
|
209
|
+
// Background first: it's the backdrop every other token flattens alpha over.
|
|
210
|
+
const backgroundHit = pick(colors, ['editor.background', 'editorPane.background', 'editorGroup.background'], '#000000')
|
|
211
|
+
const dark = isDarkType(raw, backgroundHit?.value ?? '#1e1e1e')
|
|
212
|
+
const background = backgroundHit?.value ?? (dark ? '#1e1e1e' : '#ffffff')
|
|
213
|
+
|
|
214
|
+
if (!backgroundHit) {
|
|
215
|
+
derived.push('editor.background')
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// `take` records a derived fallback when the theme omits the key.
|
|
219
|
+
const take = (keys: string[], fallback: string): string => {
|
|
220
|
+
const hit = pick(colors, keys, background)
|
|
221
|
+
|
|
222
|
+
if (hit) {
|
|
223
|
+
return hit.value
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
derived.push(keys[0])
|
|
227
|
+
|
|
228
|
+
return fallback
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const foreground = take(['editor.foreground', 'foreground'], dark ? '#d4d4d4' : '#1f1f1f')
|
|
232
|
+
|
|
233
|
+
// Brand accent — the single most load-bearing token. Drives primary buttons,
|
|
234
|
+
// focus rings, the streaming cursor, active-session pills, and sidebar labels.
|
|
235
|
+
// Prefer the saturated "brand" tokens (button / link / badge) over focusBorder,
|
|
236
|
+
// which many themes set to a muted gray — picking it first made imported
|
|
237
|
+
// accents look like the desktop defaults. We enforce contrast below regardless.
|
|
238
|
+
const accentSource = take(
|
|
239
|
+
[
|
|
240
|
+
'button.background',
|
|
241
|
+
'textLink.activeForeground',
|
|
242
|
+
'textLink.foreground',
|
|
243
|
+
'activityBarBadge.background',
|
|
244
|
+
'badge.background',
|
|
245
|
+
'progressBar.background',
|
|
246
|
+
'pickerGroup.foreground',
|
|
247
|
+
'list.highlightForeground',
|
|
248
|
+
'editorLink.activeForeground',
|
|
249
|
+
'focusBorder',
|
|
250
|
+
'tab.activeBorder',
|
|
251
|
+
'statusBarItem.remoteBackground'
|
|
252
|
+
],
|
|
253
|
+
mix(foreground, background, 0.55)
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
const elevated = take(
|
|
257
|
+
['editorWidget.background', 'dropdown.background', 'menu.background', 'quickInput.background', 'editorSuggestWidget.background'],
|
|
258
|
+
mix(background, foreground, dark ? 0.08 : 0.05)
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
const card = take(
|
|
262
|
+
['sideBarSectionHeader.background', 'tab.inactiveBackground', 'editorGroupHeader.tabsBackground'],
|
|
263
|
+
mix(background, foreground, dark ? 0.04 : 0.025)
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
const sidebar = take(['sideBar.background', 'activityBar.background'], mix(background, foreground, dark ? 0.02 : 0.012))
|
|
267
|
+
|
|
268
|
+
// The accent labels the sidebar (--theme-primary), so guarantee it reads
|
|
269
|
+
// there — otherwise low-contrast brand colors leave invisible section headers.
|
|
270
|
+
const accent = ensureContrast(accentSource, sidebar, ACCENT_MIN_CONTRAST)
|
|
271
|
+
|
|
272
|
+
const border = take(
|
|
273
|
+
['panel.border', 'editorGroup.border', 'sideBar.border', 'contrastBorder', 'widget.border', 'input.border'],
|
|
274
|
+
mix(background, foreground, dark ? 0.16 : 0.14)
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
const input = take(['input.background', 'dropdown.background', 'quickInput.background'], mix(background, foreground, dark ? 0.1 : 0.06))
|
|
278
|
+
|
|
279
|
+
const mutedForeground = take(
|
|
280
|
+
['descriptionForeground', 'editorLineNumber.foreground', 'tab.inactiveForeground', 'disabledForeground'],
|
|
281
|
+
mix(foreground, background, 0.45)
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
const destructive = take(
|
|
285
|
+
['editorError.foreground', 'errorForeground', 'editorOverviewRuler.errorForeground', 'notificationsErrorIcon.foreground'],
|
|
286
|
+
'#e25563'
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
const muted = mix(background, foreground, dark ? 0.06 : 0.04)
|
|
290
|
+
const accentSoft = mix(accent, background, dark ? 0.82 : 0.88)
|
|
291
|
+
const secondary = mix(accent, background, dark ? 0.72 : 0.86)
|
|
292
|
+
|
|
293
|
+
const palette: DesktopThemeColors = {
|
|
294
|
+
background,
|
|
295
|
+
foreground,
|
|
296
|
+
card,
|
|
297
|
+
cardForeground: foreground,
|
|
298
|
+
muted,
|
|
299
|
+
mutedForeground,
|
|
300
|
+
popover: elevated,
|
|
301
|
+
popoverForeground: foreground,
|
|
302
|
+
primary: accent,
|
|
303
|
+
primaryForeground: readableOn(accent),
|
|
304
|
+
secondary,
|
|
305
|
+
secondaryForeground: foreground,
|
|
306
|
+
accent: accentSoft,
|
|
307
|
+
accentForeground: foreground,
|
|
308
|
+
border,
|
|
309
|
+
input,
|
|
310
|
+
ring: accent,
|
|
311
|
+
midground: accent,
|
|
312
|
+
midgroundForeground: readableOn(accent),
|
|
313
|
+
composerRing: accent,
|
|
314
|
+
destructive,
|
|
315
|
+
destructiveForeground: readableOn(destructive),
|
|
316
|
+
sidebarBackground: sidebar,
|
|
317
|
+
sidebarBorder: border,
|
|
318
|
+
userBubble: mix(card, accent, dark ? 0.18 : 0.12),
|
|
319
|
+
userBubbleBorder: border
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const label = (opts.label ?? raw.name ?? 'VS Code Theme').trim()
|
|
323
|
+
const slug = opts.slug ?? vscodeThemeSlug(label)
|
|
324
|
+
const terminal = extractTerminalPalette(colors, background)
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
derived,
|
|
328
|
+
mode: dark ? 'dark' : 'light',
|
|
329
|
+
theme: {
|
|
330
|
+
name: slug,
|
|
331
|
+
label,
|
|
332
|
+
description: opts.source ? `VS Code · ${opts.source}` : 'Imported from VS Code',
|
|
333
|
+
// Single palette in both slots. A lone VS Code theme is one-mode; callers
|
|
334
|
+
// that have both a light and dark variant (a Marketplace extension family)
|
|
335
|
+
// recombine them into proper colors/darkColors via buildThemeFromMarketplace.
|
|
336
|
+
colors: palette,
|
|
337
|
+
darkColors: palette,
|
|
338
|
+
// Only set when the theme ships a full ANSI palette — the terminal keeps
|
|
339
|
+
// its built-in VS Code defaults otherwise.
|
|
340
|
+
...(terminal ? { terminal } : {})
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|