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.
Files changed (546) hide show
  1. package/.prettierrc +11 -0
  2. package/DESIGN.md +167 -0
  3. package/README.md +141 -0
  4. package/assets/icon.icns +0 -0
  5. package/assets/icon.ico +0 -0
  6. package/assets/icon.png +0 -0
  7. package/components.json +21 -0
  8. package/electron/backend-env.cjs +112 -0
  9. package/electron/backend-env.test.cjs +111 -0
  10. package/electron/backend-probes.cjs +106 -0
  11. package/electron/backend-probes.test.cjs +82 -0
  12. package/electron/backend-ready.cjs +66 -0
  13. package/electron/bootstrap-platform.cjs +91 -0
  14. package/electron/bootstrap-platform.test.cjs +111 -0
  15. package/electron/bootstrap-runner.cjs +720 -0
  16. package/electron/bootstrap-runner.test.cjs +138 -0
  17. package/electron/connection-config.cjs +254 -0
  18. package/electron/connection-config.test.cjs +329 -0
  19. package/electron/dashboard-token.cjs +99 -0
  20. package/electron/dashboard-token.test.cjs +142 -0
  21. package/electron/desktop-uninstall.cjs +232 -0
  22. package/electron/desktop-uninstall.test.cjs +246 -0
  23. package/electron/entitlements.mac.inherit.plist +14 -0
  24. package/electron/entitlements.mac.plist +14 -0
  25. package/electron/fs-read-dir.cjs +109 -0
  26. package/electron/fs-read-dir.test.cjs +364 -0
  27. package/electron/gateway-ws-probe.cjs +188 -0
  28. package/electron/gateway-ws-probe.test.cjs +122 -0
  29. package/electron/git-root.cjs +54 -0
  30. package/electron/git-root.test.cjs +40 -0
  31. package/electron/git-worktrees.cjs +174 -0
  32. package/electron/hardening.cjs +184 -0
  33. package/electron/hardening.test.cjs +116 -0
  34. package/electron/main.cjs +5762 -0
  35. package/electron/oauth-net-request.cjs +20 -0
  36. package/electron/oauth-net-request.test.cjs +34 -0
  37. package/electron/preload.cjs +135 -0
  38. package/electron/session-windows.cjs +99 -0
  39. package/electron/session-windows.test.cjs +177 -0
  40. package/electron/update-remote.cjs +56 -0
  41. package/electron/update-remote.test.cjs +78 -0
  42. package/electron/vscode-marketplace.cjs +331 -0
  43. package/electron/vscode-marketplace.test.cjs +113 -0
  44. package/electron/windows-child-process.test.cjs +57 -0
  45. package/electron/windows-user-env.cjs +76 -0
  46. package/electron/windows-user-env.test.cjs +90 -0
  47. package/electron/workspace-cwd.cjs +38 -0
  48. package/electron/workspace-cwd.test.cjs +45 -0
  49. package/eslint.config.mjs +122 -0
  50. package/index.html +17 -0
  51. package/package.json +254 -0
  52. package/pr-assets/session-source-folders.png +0 -0
  53. package/preview-demo.html +65 -0
  54. package/public/apple-touch-icon.png +0 -0
  55. package/public/ds-assets/filler-bg0.jpg +0 -0
  56. package/public/nastech-frames/nastech-frame-0.png +0 -0
  57. package/public/nastech-frames/nastech-frame-1.png +0 -0
  58. package/public/nastech-frames/nastech-frame-2.png +0 -0
  59. package/public/nastech-frames/nastech-frame-3.png +0 -0
  60. package/public/nastech-frames/nastech-frame-4.png +0 -0
  61. package/public/nastech-frames/nastech-frame-5.png +0 -0
  62. package/public/nastech-frames/nastech-frame-6.png +0 -0
  63. package/public/nastech-frames/nastech-frame-7.png +0 -0
  64. package/public/nastech-girl.jpg +0 -0
  65. package/public/nastech-sprite.png +0 -0
  66. package/public/nastech.png +0 -0
  67. package/scripts/after-pack.cjs +41 -0
  68. package/scripts/assert-dist-built.cjs +70 -0
  69. package/scripts/assert-dist-built.test.cjs +84 -0
  70. package/scripts/assert-root-install.cjs +13 -0
  71. package/scripts/before-build.cjs +11 -0
  72. package/scripts/before-pack.cjs +78 -0
  73. package/scripts/before-pack.test.cjs +53 -0
  74. package/scripts/click-session.mjs +51 -0
  75. package/scripts/dev-no-hmr.mjs +22 -0
  76. package/scripts/diag-jump.mjs +115 -0
  77. package/scripts/diag-scroll-reset.mjs +229 -0
  78. package/scripts/eval.mjs +21 -0
  79. package/scripts/leak-typing.mjs +222 -0
  80. package/scripts/measure-jump.mjs +108 -0
  81. package/scripts/measure-latency.mjs +184 -0
  82. package/scripts/measure-real-stream.mjs +252 -0
  83. package/scripts/measure-submit.mjs +179 -0
  84. package/scripts/measure-synthetic-stream.mjs +322 -0
  85. package/scripts/notarize-artifact.cjs +77 -0
  86. package/scripts/notarize.cjs +100 -0
  87. package/scripts/patch-electron-builder-mac-binary.cjs +59 -0
  88. package/scripts/probe-renderer.mjs +38 -0
  89. package/scripts/probe-thread.mjs +40 -0
  90. package/scripts/profile-long-stream.mjs +191 -0
  91. package/scripts/profile-real-stream.mjs +137 -0
  92. package/scripts/profile-synth-stream.mjs +103 -0
  93. package/scripts/profile-typing-lag.md +381 -0
  94. package/scripts/profile-typing.mjs +260 -0
  95. package/scripts/reload-renderer.mjs +25 -0
  96. package/scripts/reload.mjs +36 -0
  97. package/scripts/set-exe-identity.cjs +94 -0
  98. package/scripts/stage-native-deps.cjs +159 -0
  99. package/scripts/test-desktop.mjs +425 -0
  100. package/scripts/write-build-stamp.cjs +126 -0
  101. package/src/app/agents/index.tsx +398 -0
  102. package/src/app/artifacts/index.test.ts +62 -0
  103. package/src/app/artifacts/index.tsx +906 -0
  104. package/src/app/chat/chat-drop-overlay.tsx +48 -0
  105. package/src/app/chat/chat-swap-overlay.tsx +47 -0
  106. package/src/app/chat/composer/attachments.tsx +114 -0
  107. package/src/app/chat/composer/completion-drawer.tsx +63 -0
  108. package/src/app/chat/composer/context-menu.tsx +172 -0
  109. package/src/app/chat/composer/controls.tsx +289 -0
  110. package/src/app/chat/composer/drop-affordance.ts +2 -0
  111. package/src/app/chat/composer/enter-submit-dom-race.test.tsx +218 -0
  112. package/src/app/chat/composer/focus.ts +134 -0
  113. package/src/app/chat/composer/help-hint.tsx +59 -0
  114. package/src/app/chat/composer/hooks/use-at-completions.ts +141 -0
  115. package/src/app/chat/composer/hooks/use-live-completion-adapter.ts +119 -0
  116. package/src/app/chat/composer/hooks/use-mic-recorder.ts +291 -0
  117. package/src/app/chat/composer/hooks/use-slash-completions.ts +114 -0
  118. package/src/app/chat/composer/hooks/use-voice-conversation.ts +390 -0
  119. package/src/app/chat/composer/hooks/use-voice-recorder.ts +116 -0
  120. package/src/app/chat/composer/ime-composition-dom-repro.test.tsx +108 -0
  121. package/src/app/chat/composer/index.tsx +1611 -0
  122. package/src/app/chat/composer/inline-refs.ts +138 -0
  123. package/src/app/chat/composer/model-pill.tsx +86 -0
  124. package/src/app/chat/composer/queue-panel.tsx +130 -0
  125. package/src/app/chat/composer/rich-editor.test.ts +18 -0
  126. package/src/app/chat/composer/rich-editor.ts +165 -0
  127. package/src/app/chat/composer/skin-slash-popover.tsx +61 -0
  128. package/src/app/chat/composer/slash-nav-dom-repro.test.tsx +186 -0
  129. package/src/app/chat/composer/status-stack/index.tsx +202 -0
  130. package/src/app/chat/composer/status-stack/status-row.tsx +155 -0
  131. package/src/app/chat/composer/text-utils.test.ts +77 -0
  132. package/src/app/chat/composer/text-utils.ts +107 -0
  133. package/src/app/chat/composer/trigger-popover.test.tsx +42 -0
  134. package/src/app/chat/composer/trigger-popover.tsx +116 -0
  135. package/src/app/chat/composer/types.ts +64 -0
  136. package/src/app/chat/composer/url-dialog.tsx +82 -0
  137. package/src/app/chat/composer/voice-activity.tsx +252 -0
  138. package/src/app/chat/hooks/use-composer-actions.test.ts +57 -0
  139. package/src/app/chat/hooks/use-composer-actions.ts +525 -0
  140. package/src/app/chat/hooks/use-file-drop-zone.ts +118 -0
  141. package/src/app/chat/index.tsx +390 -0
  142. package/src/app/chat/perf-probe.tsx +269 -0
  143. package/src/app/chat/right-rail/index.ts +1 -0
  144. package/src/app/chat/right-rail/preview-console-state.ts +82 -0
  145. package/src/app/chat/right-rail/preview-console.tsx +290 -0
  146. package/src/app/chat/right-rail/preview-file.tsx +559 -0
  147. package/src/app/chat/right-rail/preview-pane.test.tsx +43 -0
  148. package/src/app/chat/right-rail/preview-pane.tsx +657 -0
  149. package/src/app/chat/right-rail/preview.tsx +171 -0
  150. package/src/app/chat/scroll-to-bottom-button.test.tsx +67 -0
  151. package/src/app/chat/scroll-to-bottom-button.tsx +74 -0
  152. package/src/app/chat/sidebar/cron-jobs-section.tsx +325 -0
  153. package/src/app/chat/sidebar/index.tsx +1219 -0
  154. package/src/app/chat/sidebar/load-more-row.tsx +30 -0
  155. package/src/app/chat/sidebar/order.test.ts +21 -0
  156. package/src/app/chat/sidebar/order.ts +17 -0
  157. package/src/app/chat/sidebar/profile-switcher.tsx +516 -0
  158. package/src/app/chat/sidebar/session-actions-menu.tsx +264 -0
  159. package/src/app/chat/sidebar/session-row.tsx +257 -0
  160. package/src/app/chat/sidebar/virtual-session-list.tsx +154 -0
  161. package/src/app/chat/sidebar/workspace-groups.test.ts +149 -0
  162. package/src/app/chat/sidebar/workspace-groups.ts +326 -0
  163. package/src/app/chat/thread-loading.test.ts +34 -0
  164. package/src/app/chat/thread-loading.ts +26 -0
  165. package/src/app/command-center/index.tsx +654 -0
  166. package/src/app/command-palette/index.tsx +513 -0
  167. package/src/app/command-palette/marketplace-theme-page.tsx +157 -0
  168. package/src/app/cron/index.tsx +942 -0
  169. package/src/app/cron/job-state.ts +29 -0
  170. package/src/app/desktop-controller.tsx +938 -0
  171. package/src/app/floating-hud.ts +22 -0
  172. package/src/app/gateway/hooks/use-gateway-boot.test.tsx +265 -0
  173. package/src/app/gateway/hooks/use-gateway-boot.ts +387 -0
  174. package/src/app/gateway/hooks/use-gateway-request.ts +138 -0
  175. package/src/app/hooks/use-keybinds.ts +186 -0
  176. package/src/app/hooks/use-refresh-hotkey.ts +45 -0
  177. package/src/app/hooks/use-route-enum-param.ts +38 -0
  178. package/src/app/index.tsx +1 -0
  179. package/src/app/layout-constants.ts +13 -0
  180. package/src/app/messaging/index.tsx +648 -0
  181. package/src/app/messaging/platform-icon.tsx +93 -0
  182. package/src/app/model-picker-overlay.tsx +42 -0
  183. package/src/app/model-visibility-overlay.tsx +31 -0
  184. package/src/app/overlays/overlay-chrome.tsx +66 -0
  185. package/src/app/overlays/overlay-search-input.tsx +33 -0
  186. package/src/app/overlays/overlay-split-layout.tsx +130 -0
  187. package/src/app/overlays/overlay-view.tsx +91 -0
  188. package/src/app/page-search-shell.tsx +75 -0
  189. package/src/app/profiles/create-profile-dialog.tsx +154 -0
  190. package/src/app/profiles/delete-profile-dialog.tsx +65 -0
  191. package/src/app/profiles/index.tsx +671 -0
  192. package/src/app/profiles/rename-profile-dialog.tsx +125 -0
  193. package/src/app/right-sidebar/files/dnd-manager.ts +27 -0
  194. package/src/app/right-sidebar/files/ipc.test.ts +100 -0
  195. package/src/app/right-sidebar/files/ipc.ts +161 -0
  196. package/src/app/right-sidebar/files/remote-picker.tsx +177 -0
  197. package/src/app/right-sidebar/files/tree.tsx +224 -0
  198. package/src/app/right-sidebar/files/use-project-tree.test.ts +190 -0
  199. package/src/app/right-sidebar/files/use-project-tree.ts +268 -0
  200. package/src/app/right-sidebar/index.test.tsx +75 -0
  201. package/src/app/right-sidebar/index.tsx +395 -0
  202. package/src/app/right-sidebar/store.ts +15 -0
  203. package/src/app/right-sidebar/terminal/buffer.ts +65 -0
  204. package/src/app/right-sidebar/terminal/index.tsx +98 -0
  205. package/src/app/right-sidebar/terminal/persistent.tsx +122 -0
  206. package/src/app/right-sidebar/terminal/selection.ts +75 -0
  207. package/src/app/right-sidebar/terminal/use-terminal-session.ts +504 -0
  208. package/src/app/routes.ts +88 -0
  209. package/src/app/session/hooks/use-context-suggestions.ts +58 -0
  210. package/src/app/session/hooks/use-cwd-actions.ts +109 -0
  211. package/src/app/session/hooks/use-message-stream.ts +957 -0
  212. package/src/app/session/hooks/use-model-controls.test.tsx +198 -0
  213. package/src/app/session/hooks/use-model-controls.ts +106 -0
  214. package/src/app/session/hooks/use-nastech-config.ts +74 -0
  215. package/src/app/session/hooks/use-preview-routing.test.tsx +168 -0
  216. package/src/app/session/hooks/use-preview-routing.ts +223 -0
  217. package/src/app/session/hooks/use-prompt-actions.test.tsx +316 -0
  218. package/src/app/session/hooks/use-prompt-actions.ts +1030 -0
  219. package/src/app/session/hooks/use-route-resume.test.tsx +136 -0
  220. package/src/app/session/hooks/use-route-resume.ts +115 -0
  221. package/src/app/session/hooks/use-session-actions.test.tsx +119 -0
  222. package/src/app/session/hooks/use-session-actions.ts +885 -0
  223. package/src/app/session/hooks/use-session-state-cache.test.tsx +118 -0
  224. package/src/app/session/hooks/use-session-state-cache.ts +191 -0
  225. package/src/app/session-picker-overlay.tsx +32 -0
  226. package/src/app/session-switcher.tsx +107 -0
  227. package/src/app/settings/about-settings.tsx +173 -0
  228. package/src/app/settings/appearance-settings.tsx +162 -0
  229. package/src/app/settings/config-settings.tsx +384 -0
  230. package/src/app/settings/constants.ts +545 -0
  231. package/src/app/settings/credential-key-ui.tsx +373 -0
  232. package/src/app/settings/env-credentials.tsx +198 -0
  233. package/src/app/settings/env-var-actions-menu.tsx +136 -0
  234. package/src/app/settings/field-copy.ts +56 -0
  235. package/src/app/settings/gateway-settings.tsx +620 -0
  236. package/src/app/settings/helpers.test.ts +138 -0
  237. package/src/app/settings/helpers.ts +151 -0
  238. package/src/app/settings/index.tsx +237 -0
  239. package/src/app/settings/keys-settings.tsx +96 -0
  240. package/src/app/settings/mcp-settings.tsx +271 -0
  241. package/src/app/settings/model-settings.test.tsx +157 -0
  242. package/src/app/settings/model-settings.tsx +559 -0
  243. package/src/app/settings/notifications-settings.tsx +150 -0
  244. package/src/app/settings/primitives.tsx +115 -0
  245. package/src/app/settings/providers-settings.test.tsx +100 -0
  246. package/src/app/settings/providers-settings.tsx +258 -0
  247. package/src/app/settings/sessions-settings.tsx +276 -0
  248. package/src/app/settings/toolset-config-panel.test.tsx +289 -0
  249. package/src/app/settings/toolset-config-panel.tsx +449 -0
  250. package/src/app/settings/types.ts +42 -0
  251. package/src/app/settings/uninstall-section.tsx +185 -0
  252. package/src/app/settings/use-deep-link-highlight.ts +60 -0
  253. package/src/app/shell/app-shell.tsx +167 -0
  254. package/src/app/shell/gateway-menu-panel.tsx +150 -0
  255. package/src/app/shell/hooks/use-overlay-routing.ts +71 -0
  256. package/src/app/shell/hooks/use-status-snapshot.ts +57 -0
  257. package/src/app/shell/hooks/use-statusbar-items.tsx +403 -0
  258. package/src/app/shell/keybind-panel.tsx +220 -0
  259. package/src/app/shell/model-edit-submenu.test.tsx +84 -0
  260. package/src/app/shell/model-edit-submenu.tsx +245 -0
  261. package/src/app/shell/model-menu-panel.tsx +295 -0
  262. package/src/app/shell/sidebar-label.tsx +22 -0
  263. package/src/app/shell/statusbar-controls.tsx +185 -0
  264. package/src/app/shell/titlebar-controls.tsx +244 -0
  265. package/src/app/shell/titlebar.test.ts +26 -0
  266. package/src/app/shell/titlebar.ts +45 -0
  267. package/src/app/shell/use-group-registry.ts +39 -0
  268. package/src/app/skills/index.test.tsx +103 -0
  269. package/src/app/skills/index.tsx +371 -0
  270. package/src/app/types.ts +99 -0
  271. package/src/app/updates-overlay.tsx +369 -0
  272. package/src/components/Backdrop.tsx +114 -0
  273. package/src/components/assistant-ui/ansi-text.tsx +34 -0
  274. package/src/components/assistant-ui/clarify-tool.tsx +281 -0
  275. package/src/components/assistant-ui/directive-text.test.ts +39 -0
  276. package/src/components/assistant-ui/directive-text.tsx +389 -0
  277. package/src/components/assistant-ui/markdown-text.test.ts +204 -0
  278. package/src/components/assistant-ui/markdown-text.tsx +497 -0
  279. package/src/components/assistant-ui/message-render-boundary.test.tsx +80 -0
  280. package/src/components/assistant-ui/message-render-boundary.tsx +48 -0
  281. package/src/components/assistant-ui/streaming.test.tsx +739 -0
  282. package/src/components/assistant-ui/thread-list.tsx +307 -0
  283. package/src/components/assistant-ui/thread-virtualizer.tsx +512 -0
  284. package/src/components/assistant-ui/thread.tsx +1474 -0
  285. package/src/components/assistant-ui/todo-tool.tsx +109 -0
  286. package/src/components/assistant-ui/tool-approval-group.test.tsx +158 -0
  287. package/src/components/assistant-ui/tool-approval.test.tsx +81 -0
  288. package/src/components/assistant-ui/tool-approval.tsx +209 -0
  289. package/src/components/assistant-ui/tool-fallback-model.test.ts +66 -0
  290. package/src/components/assistant-ui/tool-fallback-model.ts +1368 -0
  291. package/src/components/assistant-ui/tool-fallback.tsx +466 -0
  292. package/src/components/assistant-ui/tooltip-icon-button.tsx +33 -0
  293. package/src/components/assistant-ui/user-message-edit.test.tsx +141 -0
  294. package/src/components/assistant-ui/user-message-text.tsx +150 -0
  295. package/src/components/boot-failure-overlay.tsx +246 -0
  296. package/src/components/boot-failure-reauth.test.ts +100 -0
  297. package/src/components/boot-failure-reauth.ts +81 -0
  298. package/src/components/brand-mark.tsx +19 -0
  299. package/src/components/chat/activity-timer-text.tsx +24 -0
  300. package/src/components/chat/activity-timer.test.tsx +43 -0
  301. package/src/components/chat/activity-timer.ts +64 -0
  302. package/src/components/chat/code-card.tsx +78 -0
  303. package/src/components/chat/compact-markdown.tsx +113 -0
  304. package/src/components/chat/composer-dock.ts +31 -0
  305. package/src/components/chat/diff-lines.tsx +54 -0
  306. package/src/components/chat/disclosure-row.tsx +63 -0
  307. package/src/components/chat/generated-image-context.tsx +19 -0
  308. package/src/components/chat/generated-image-result.tsx +174 -0
  309. package/src/components/chat/image-generation-placeholder.tsx +279 -0
  310. package/src/components/chat/intro-copy.jsonl +75 -0
  311. package/src/components/chat/intro.tsx +182 -0
  312. package/src/components/chat/preview-attachment.tsx +125 -0
  313. package/src/components/chat/shiki-highlighter.tsx +107 -0
  314. package/src/components/chat/status-row.tsx +70 -0
  315. package/src/components/chat/status-section.tsx +42 -0
  316. package/src/components/chat/terminal-output.tsx +50 -0
  317. package/src/components/chat/zoomable-image.tsx +177 -0
  318. package/src/components/desktop-install-overlay.tsx +595 -0
  319. package/src/components/desktop-onboarding-overlay.test.tsx +100 -0
  320. package/src/components/desktop-onboarding-overlay.tsx +1286 -0
  321. package/src/components/error-boundary.tsx +77 -0
  322. package/src/components/gateway-connecting-overlay.test.tsx +143 -0
  323. package/src/components/gateway-connecting-overlay.tsx +183 -0
  324. package/src/components/haptics-provider.tsx +19 -0
  325. package/src/components/language-switcher.test.tsx +53 -0
  326. package/src/components/language-switcher.tsx +175 -0
  327. package/src/components/model-picker.tsx +340 -0
  328. package/src/components/model-visibility-dialog.tsx +155 -0
  329. package/src/components/notifications.tsx +196 -0
  330. package/src/components/page-loader.tsx +34 -0
  331. package/src/components/pane-shell/context.ts +14 -0
  332. package/src/components/pane-shell/index.ts +4 -0
  333. package/src/components/pane-shell/pane-shell.test.tsx +333 -0
  334. package/src/components/pane-shell/pane-shell.tsx +330 -0
  335. package/src/components/prompt-overlays.tsx +234 -0
  336. package/src/components/session-picker.tsx +108 -0
  337. package/src/components/status-dot.tsx +26 -0
  338. package/src/components/ui/action-status.tsx +25 -0
  339. package/src/components/ui/alert.tsx +53 -0
  340. package/src/components/ui/badge.tsx +35 -0
  341. package/src/components/ui/braille-spinner.tsx +61 -0
  342. package/src/components/ui/button.tsx +81 -0
  343. package/src/components/ui/checkbox.tsx +27 -0
  344. package/src/components/ui/codicon.tsx +20 -0
  345. package/src/components/ui/command.tsx +111 -0
  346. package/src/components/ui/confirm-dialog.tsx +109 -0
  347. package/src/components/ui/context-menu.tsx +141 -0
  348. package/src/components/ui/control.ts +25 -0
  349. package/src/components/ui/copy-button.test.tsx +36 -0
  350. package/src/components/ui/copy-button.tsx +229 -0
  351. package/src/components/ui/dialog.tsx +152 -0
  352. package/src/components/ui/disclosure-caret.tsx +20 -0
  353. package/src/components/ui/dropdown-menu.tsx +291 -0
  354. package/src/components/ui/error-state.tsx +50 -0
  355. package/src/components/ui/fade-text.tsx +110 -0
  356. package/src/components/ui/glyph-spinner.tsx +63 -0
  357. package/src/components/ui/input.tsx +22 -0
  358. package/src/components/ui/kbd.tsx +37 -0
  359. package/src/components/ui/loader.tsx +558 -0
  360. package/src/components/ui/log-view.tsx +17 -0
  361. package/src/components/ui/pagination.tsx +114 -0
  362. package/src/components/ui/popover.tsx +44 -0
  363. package/src/components/ui/scroll-area.tsx +43 -0
  364. package/src/components/ui/search-field.tsx +80 -0
  365. package/src/components/ui/segmented-control.tsx +51 -0
  366. package/src/components/ui/select.tsx +92 -0
  367. package/src/components/ui/separator.tsx +26 -0
  368. package/src/components/ui/sheet.tsx +116 -0
  369. package/src/components/ui/sidebar.tsx +674 -0
  370. package/src/components/ui/skeleton.tsx +7 -0
  371. package/src/components/ui/switch.tsx +49 -0
  372. package/src/components/ui/tabs.tsx +36 -0
  373. package/src/components/ui/text-tab.tsx +43 -0
  374. package/src/components/ui/textarea.tsx +11 -0
  375. package/src/components/ui/tool-icon.tsx +65 -0
  376. package/src/components/ui/tooltip.tsx +69 -0
  377. package/src/fonts/JetBrainsMono-Bold.woff2 +0 -0
  378. package/src/fonts/JetBrainsMono-Italic.woff2 +0 -0
  379. package/src/fonts/JetBrainsMono-Regular.woff2 +0 -0
  380. package/src/global.d.ts +457 -0
  381. package/src/hooks/use-image-download.ts +85 -0
  382. package/src/hooks/use-media-query.ts +24 -0
  383. package/src/hooks/use-mobile.ts +3 -0
  384. package/src/hooks/use-resize-observer.ts +38 -0
  385. package/src/hooks/use-worktree-info.ts +68 -0
  386. package/src/i18n/catalog.ts +12 -0
  387. package/src/i18n/context.test.tsx +232 -0
  388. package/src/i18n/context.tsx +183 -0
  389. package/src/i18n/define-locale.ts +41 -0
  390. package/src/i18n/en.ts +1779 -0
  391. package/src/i18n/index.ts +20 -0
  392. package/src/i18n/ja.ts +1890 -0
  393. package/src/i18n/languages.test.ts +43 -0
  394. package/src/i18n/languages.ts +86 -0
  395. package/src/i18n/runtime.test.ts +75 -0
  396. package/src/i18n/runtime.ts +53 -0
  397. package/src/i18n/types.ts +1452 -0
  398. package/src/i18n/zh-hant.ts +1849 -0
  399. package/src/i18n/zh.ts +1923 -0
  400. package/src/lib/ansi.test.ts +123 -0
  401. package/src/lib/ansi.ts +175 -0
  402. package/src/lib/chat-messages.test.ts +708 -0
  403. package/src/lib/chat-messages.ts +885 -0
  404. package/src/lib/chat-runtime.test.ts +18 -0
  405. package/src/lib/chat-runtime.ts +335 -0
  406. package/src/lib/clipboard.ts +28 -0
  407. package/src/lib/commit-changelog.test.ts +114 -0
  408. package/src/lib/commit-changelog.ts +177 -0
  409. package/src/lib/completion-sound.ts +519 -0
  410. package/src/lib/desktop-fs.test.ts +116 -0
  411. package/src/lib/desktop-fs.ts +113 -0
  412. package/src/lib/desktop-slash-commands.test.ts +126 -0
  413. package/src/lib/desktop-slash-commands.ts +286 -0
  414. package/src/lib/embedded-images.test.ts +35 -0
  415. package/src/lib/embedded-images.ts +60 -0
  416. package/src/lib/external-link.test.tsx +168 -0
  417. package/src/lib/external-link.tsx +303 -0
  418. package/src/lib/gateway-events.test.ts +27 -0
  419. package/src/lib/gateway-events.ts +49 -0
  420. package/src/lib/gateway-ws-url.test.ts +78 -0
  421. package/src/lib/gateway-ws-url.ts +91 -0
  422. package/src/lib/generated-images.test.ts +97 -0
  423. package/src/lib/generated-images.ts +116 -0
  424. package/src/lib/haptics.ts +129 -0
  425. package/src/lib/icons.ts +203 -0
  426. package/src/lib/incremental-external-store-runtime.ts +188 -0
  427. package/src/lib/katex-memo.ts +260 -0
  428. package/src/lib/keybinds/actions.ts +125 -0
  429. package/src/lib/keybinds/combo.test.ts +86 -0
  430. package/src/lib/keybinds/combo.ts +169 -0
  431. package/src/lib/local-preview.ts +126 -0
  432. package/src/lib/markdown-code.test.ts +23 -0
  433. package/src/lib/markdown-code.ts +195 -0
  434. package/src/lib/markdown-preprocess.ts +386 -0
  435. package/src/lib/media.remote.test.ts +58 -0
  436. package/src/lib/media.ts +111 -0
  437. package/src/lib/model-status-label.test.ts +31 -0
  438. package/src/lib/model-status-label.ts +103 -0
  439. package/src/lib/mutable-ref.ts +6 -0
  440. package/src/lib/preview-targets.test.ts +27 -0
  441. package/src/lib/preview-targets.ts +63 -0
  442. package/src/lib/profile-color.ts +58 -0
  443. package/src/lib/provider-setup-errors.test.ts +26 -0
  444. package/src/lib/provider-setup-errors.ts +12 -0
  445. package/src/lib/query-client.ts +13 -0
  446. package/src/lib/remend-tail.test.ts +105 -0
  447. package/src/lib/remend-tail.ts +108 -0
  448. package/src/lib/runtime-readiness.test.ts +65 -0
  449. package/src/lib/runtime-readiness.ts +147 -0
  450. package/src/lib/session-export.ts +57 -0
  451. package/src/lib/session-search.test.ts +58 -0
  452. package/src/lib/session-search.ts +19 -0
  453. package/src/lib/session-source.ts +62 -0
  454. package/src/lib/speech-text.ts +35 -0
  455. package/src/lib/statusbar.ts +91 -0
  456. package/src/lib/storage.test.ts +25 -0
  457. package/src/lib/storage.ts +107 -0
  458. package/src/lib/todos.test.ts +35 -0
  459. package/src/lib/todos.ts +51 -0
  460. package/src/lib/tool-result-summary.test.ts +106 -0
  461. package/src/lib/tool-result-summary.ts +467 -0
  462. package/src/lib/update-copy.test.ts +38 -0
  463. package/src/lib/update-copy.ts +44 -0
  464. package/src/lib/use-enter-animation.ts +100 -0
  465. package/src/lib/utils.ts +6 -0
  466. package/src/lib/voice-playback.ts +128 -0
  467. package/src/lib/yolo-session.ts +26 -0
  468. package/src/main.tsx +43 -0
  469. package/src/nastech.test.ts +49 -0
  470. package/src/nastech.ts +718 -0
  471. package/src/store/activity.ts +100 -0
  472. package/src/store/boot.ts +91 -0
  473. package/src/store/clarify.test.ts +81 -0
  474. package/src/store/clarify.ts +69 -0
  475. package/src/store/command-palette.ts +20 -0
  476. package/src/store/compaction.test.ts +53 -0
  477. package/src/store/compaction.ts +38 -0
  478. package/src/store/completion-sound.ts +32 -0
  479. package/src/store/composer-input-history.test.ts +147 -0
  480. package/src/store/composer-input-history.ts +158 -0
  481. package/src/store/composer-queue.test.ts +148 -0
  482. package/src/store/composer-queue.ts +239 -0
  483. package/src/store/composer-status.test.ts +99 -0
  484. package/src/store/composer-status.ts +277 -0
  485. package/src/store/composer.test.ts +106 -0
  486. package/src/store/composer.ts +184 -0
  487. package/src/store/cron.ts +19 -0
  488. package/src/store/gateway.ts +290 -0
  489. package/src/store/haptics.ts +17 -0
  490. package/src/store/keybinds.ts +139 -0
  491. package/src/store/layout.ts +176 -0
  492. package/src/store/model-presets.test.ts +51 -0
  493. package/src/store/model-presets.ts +86 -0
  494. package/src/store/model-visibility.test.ts +37 -0
  495. package/src/store/model-visibility.ts +108 -0
  496. package/src/store/native-notifications.test.ts +192 -0
  497. package/src/store/native-notifications.ts +203 -0
  498. package/src/store/notifications.ts +165 -0
  499. package/src/store/onboarding.test.ts +372 -0
  500. package/src/store/onboarding.ts +866 -0
  501. package/src/store/panes.test.ts +146 -0
  502. package/src/store/panes.ts +145 -0
  503. package/src/store/preview.test.ts +135 -0
  504. package/src/store/preview.ts +466 -0
  505. package/src/store/profile.test.ts +89 -0
  506. package/src/store/profile.ts +365 -0
  507. package/src/store/prompts.test.ts +121 -0
  508. package/src/store/prompts.ts +115 -0
  509. package/src/store/session-switcher.test.ts +115 -0
  510. package/src/store/session-switcher.ts +128 -0
  511. package/src/store/session-sync.ts +25 -0
  512. package/src/store/session.test.ts +131 -0
  513. package/src/store/session.ts +255 -0
  514. package/src/store/subagents.test.ts +111 -0
  515. package/src/store/subagents.ts +260 -0
  516. package/src/store/thread-scroll.ts +46 -0
  517. package/src/store/todos.test.ts +47 -0
  518. package/src/store/todos.ts +64 -0
  519. package/src/store/tool-diffs.ts +23 -0
  520. package/src/store/tool-dismiss.ts +45 -0
  521. package/src/store/tool-view.ts +91 -0
  522. package/src/store/translucency.ts +38 -0
  523. package/src/store/updates.test.ts +77 -0
  524. package/src/store/updates.ts +315 -0
  525. package/src/store/voice-playback.ts +24 -0
  526. package/src/store/windows.test.ts +143 -0
  527. package/src/store/windows.ts +77 -0
  528. package/src/styles.css +1235 -0
  529. package/src/themes/color.ts +142 -0
  530. package/src/themes/context.tsx +339 -0
  531. package/src/themes/index.ts +3 -0
  532. package/src/themes/install.test.ts +119 -0
  533. package/src/themes/install.ts +95 -0
  534. package/src/themes/presets.test.ts +33 -0
  535. package/src/themes/presets.ts +293 -0
  536. package/src/themes/profile-theme.test.ts +41 -0
  537. package/src/themes/types.ts +66 -0
  538. package/src/themes/use-skin-command.ts +60 -0
  539. package/src/themes/user-themes.test.ts +63 -0
  540. package/src/themes/user-themes.ts +122 -0
  541. package/src/themes/vscode.test.ts +171 -0
  542. package/src/themes/vscode.ts +343 -0
  543. package/src/types/nastech.ts +646 -0
  544. package/src/vite-env.d.ts +1 -0
  545. package/tsconfig.json +25 -0
  546. 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
+ }