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,142 @@
1
+ /**
2
+ * Small color helpers shared by the theme context (synthesised light variants)
3
+ * and the VS Code theme converter (token → seed mapping).
4
+ *
5
+ * Everything works in 6-digit `#rrggbb`. `normalizeHex` is the front door for
6
+ * untrusted input (VS Code themes use `#rgb`, `#rgba`, `#rrggbbaa`, and named
7
+ * tokens), flattening alpha over a backdrop so downstream math stays simple.
8
+ */
9
+
10
+ export function hexToRgb(hex: string): [number, number, number] | null {
11
+ const clean = hex.trim().replace(/^#/, '')
12
+
13
+ if (!/^[0-9a-f]{6}$/i.test(clean)) {
14
+ return null
15
+ }
16
+
17
+ return [0, 2, 4].map(i => parseInt(clean.slice(i, i + 2), 16)) as [number, number, number]
18
+ }
19
+
20
+ export const rgbToHex = ([r, g, b]: [number, number, number]): string =>
21
+ `#${[r, g, b].map(n => Math.round(Math.min(255, Math.max(0, n))).toString(16).padStart(2, '0')).join('')}`
22
+
23
+ export function mix(a: string, b: string, amount: number): string {
24
+ const ar = hexToRgb(a)
25
+ const br = hexToRgb(b)
26
+
27
+ return ar && br
28
+ ? rgbToHex([ar[0] + (br[0] - ar[0]) * amount, ar[1] + (br[1] - ar[1]) * amount, ar[2] + (br[2] - ar[2]) * amount])
29
+ : a
30
+ }
31
+
32
+ const linearize = (channel: number): number =>
33
+ channel <= 0.03928 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4
34
+
35
+ /** WCAG relative luminance (gamma-corrected), 0..1. */
36
+ export function relativeLuminance(hex: string): number {
37
+ const rgb = hexToRgb(hex)
38
+
39
+ if (!rgb) {
40
+ return 0
41
+ }
42
+
43
+ const [r, g, b] = rgb.map(v => linearize(v / 255))
44
+
45
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b
46
+ }
47
+
48
+ /** WCAG contrast ratio (1..21) between two hex colors. */
49
+ export function contrastRatio(a: string, b: string): number {
50
+ const la = relativeLuminance(a)
51
+ const lb = relativeLuminance(b)
52
+
53
+ return la >= lb ? (la + 0.05) / (lb + 0.05) : (lb + 0.05) / (la + 0.05)
54
+ }
55
+
56
+ /** Returns a readable foreground (#161616 or #ffffff) for a background hex. */
57
+ export function readableOn(hex: string): string {
58
+ return relativeLuminance(hex) > 0.58 ? '#161616' : '#ffffff'
59
+ }
60
+
61
+ /**
62
+ * Guarantee `color` reads against `bg`: if it's below `min` contrast, mix it
63
+ * toward white (on a dark bg) or black (on a light bg) in steps until it clears,
64
+ * keeping the hue as much as possible. Used so imported accents never collapse
65
+ * into a near-background sidebar (the "invisible label" case).
66
+ */
67
+ export function ensureContrast(color: string, bg: string, min: number): string {
68
+ if (contrastRatio(color, bg) >= min) {
69
+ return color
70
+ }
71
+
72
+ const towards = relativeLuminance(bg) < 0.5 ? '#ffffff' : '#000000'
73
+ let best = color
74
+
75
+ for (let amount = 0.2; amount <= 1.0001; amount += 0.2) {
76
+ best = mix(color, towards, Math.min(amount, 1))
77
+
78
+ if (contrastRatio(best, bg) >= min) {
79
+ return best
80
+ }
81
+ }
82
+
83
+ return best
84
+ }
85
+
86
+ /** Perceptual-ish luminance in 0..1 (naive, for light/dark bucketing). */
87
+ export function luminance(hex: string): number {
88
+ const rgb = hexToRgb(hex)
89
+
90
+ if (!rgb) {
91
+ return 0
92
+ }
93
+
94
+ const [r, g, b] = rgb.map(v => v / 255)
95
+
96
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b
97
+ }
98
+
99
+ /**
100
+ * Coerce any CSS hex color VS Code themes throw at us into a flat 6-digit
101
+ * `#rrggbb`, compositing alpha over `backdrop`. Accepts `#rgb`, `#rgba`,
102
+ * `#rrggbb`, `#rrggbbaa` (with or without the leading `#`). Returns null for
103
+ * non-hex values (named colors, `rgb()`, etc.) so callers can fall back.
104
+ */
105
+ export function normalizeHex(input: string | undefined | null, backdrop = '#000000'): string | null {
106
+ if (typeof input !== 'string') {
107
+ return null
108
+ }
109
+
110
+ let clean = input.trim().replace(/^#/, '')
111
+
112
+ // Expand shorthand (#rgb / #rgba) to full width.
113
+ if (clean.length === 3 || clean.length === 4) {
114
+ clean = clean
115
+ .split('')
116
+ .map(ch => ch + ch)
117
+ .join('')
118
+ }
119
+
120
+ if (!/^[0-9a-f]{6}([0-9a-f]{2})?$/i.test(clean)) {
121
+ return null
122
+ }
123
+
124
+ const rgb = hexToRgb(`#${clean.slice(0, 6)}`)
125
+
126
+ if (!rgb) {
127
+ return null
128
+ }
129
+
130
+ if (clean.length === 6) {
131
+ return rgbToHex(rgb)
132
+ }
133
+
134
+ const alpha = parseInt(clean.slice(6, 8), 16) / 255
135
+ const base = hexToRgb(backdrop) ?? [0, 0, 0]
136
+
137
+ return rgbToHex([
138
+ base[0] + (rgb[0] - base[0]) * alpha,
139
+ base[1] + (rgb[1] - base[1]) * alpha,
140
+ base[2] + (rgb[2] - base[2]) * alpha
141
+ ])
142
+ }
@@ -0,0 +1,339 @@
1
+ /**
2
+ * Desktop theme context.
3
+ *
4
+ * Applies the active theme as CSS custom properties on :root so every
5
+ * Tailwind utility that references a color or font-family token picks up
6
+ * the change automatically.
7
+ *
8
+ * Mode (light/dark/system) controls brightness; skin controls accent.
9
+ * The two are persisted independently. Shift+X toggles light/dark.
10
+ */
11
+
12
+ import { createContext, type ReactNode, useCallback, useContext, useEffect, useMemo, useState } from 'react'
13
+
14
+ import { matchesQuery, useMediaQuery } from '@/hooks/use-media-query'
15
+ import { persistString, persistStringRecord, storedString, storedStringRecord } from '@/lib/storage'
16
+
17
+ import { BUILTIN_THEME_LIST, BUILTIN_THEMES, DEFAULT_SKIN_NAME, DEFAULT_TYPOGRAPHY, nousTheme } from './presets'
18
+ import type { DesktopTheme, DesktopThemeColors } from './types'
19
+
20
+ const SKIN_KEY = 'NASTECH-desktop-theme-v2'
21
+ const MODE_KEY = 'NASTECH-desktop-mode-v1'
22
+ // Per-profile skin + light/dark mode assignments: { [profileKey]: value }.
23
+ // A profile inherits the global default until it's given its own assignment.
24
+ const PROFILE_SKINS_KEY = 'NASTECH-desktop-profile-themes-v1'
25
+ const PROFILE_MODES_KEY = 'NASTECH-desktop-profile-modes-v1'
26
+ const RETIRED_SKINS = new Set(['nastech-light', 'default', 'gold'])
27
+
28
+ export type ThemeMode = 'light' | 'dark' | 'system'
29
+
30
+ const INJECTED_FONT_URLS = new Set<string>()
31
+
32
+ const resolveMode = (mode: ThemeMode, systemDark = matchesQuery('(prefers-color-scheme: dark)')): 'light' | 'dark' =>
33
+ mode === 'system' ? (systemDark ? 'dark' : 'light') : mode
34
+
35
+ const normalizeSkin = (name: string | null): string =>
36
+ name && BUILTIN_THEMES[name] && !RETIRED_SKINS.has(name) ? name : DEFAULT_SKIN_NAME
37
+
38
+ const normalizeMode = (value: string | null): ThemeMode =>
39
+ value === 'light' || value === 'dark' || value === 'system' ? value : 'light'
40
+
41
+ // ─── Per-profile appearance persistence ─────────────────────────────────────
42
+ // Skin and mode are each stored per profile. "default" is the legacy global
43
+ // slot — it reads/writes the global key directly. Named profiles get their own
44
+ // entry and fall back to the global until assigned, so unassigned profiles and
45
+ // pre-per-profile installs are unaffected.
46
+ const profilePref = <T extends string>(record: string, legacy: string, normalize: (v: string | null) => T) => ({
47
+ resolve: (profile: string): T => normalize(storedStringRecord(record)[profile] ?? storedString(legacy)),
48
+ assign: (profile: string, value: T): void => {
49
+ if (profile === 'default') {
50
+ persistString(legacy, value)
51
+ } else {
52
+ persistStringRecord(record, { ...storedStringRecord(record), [profile]: value })
53
+ }
54
+ }
55
+ })
56
+
57
+ export const skinPref = profilePref(PROFILE_SKINS_KEY, SKIN_KEY, normalizeSkin)
58
+ export const modePref = profilePref(PROFILE_MODES_KEY, MODE_KEY, normalizeMode)
59
+
60
+ // ─── Color math (for synthesised light variants of dark-only skins) ────────
61
+
62
+ function hexToRgb(hex: string): [number, number, number] | null {
63
+ const clean = hex.trim().replace(/^#/, '')
64
+
65
+ if (!/^[0-9a-f]{6}$/i.test(clean)) {
66
+ return null
67
+ }
68
+
69
+ return [0, 2, 4].map(i => parseInt(clean.slice(i, i + 2), 16)) as [number, number, number]
70
+ }
71
+
72
+ const rgbToHex = ([r, g, b]: [number, number, number]) =>
73
+ `#${[r, g, b].map(n => Math.round(n).toString(16).padStart(2, '0')).join('')}`
74
+
75
+ function mix(a: string, b: string, amount: number): string {
76
+ const ar = hexToRgb(a)
77
+ const br = hexToRgb(b)
78
+
79
+ return ar && br
80
+ ? rgbToHex([ar[0] + (br[0] - ar[0]) * amount, ar[1] + (br[1] - ar[1]) * amount, ar[2] + (br[2] - ar[2]) * amount])
81
+ : a
82
+ }
83
+
84
+ function readableOn(hex: string): string {
85
+ const rgb = hexToRgb(hex)
86
+
87
+ if (!rgb) {
88
+ return '#ffffff'
89
+ }
90
+
91
+ const [r, g, b] = rgb.map(v => {
92
+ const c = v / 255
93
+
94
+ return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4
95
+ })
96
+
97
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b > 0.58 ? '#161616' : '#ffffff'
98
+ }
99
+
100
+ function synthLightColors(seed: DesktopTheme): DesktopThemeColors {
101
+ const accent = seed.colors.ring || seed.colors.primary
102
+ const soft = mix('#ffffff', accent, 0.1)
103
+ const softer = mix('#ffffff', accent, 0.06)
104
+ const border = mix('#ececef', accent, 0.14)
105
+ const midground = seed.colors.midground ?? accent
106
+
107
+ return {
108
+ background: '#ffffff',
109
+ foreground: '#161616',
110
+ card: '#ffffff',
111
+ cardForeground: '#161616',
112
+ muted: softer,
113
+ mutedForeground: mix('#6b6b70', accent, 0.16),
114
+ popover: '#ffffff',
115
+ popoverForeground: '#161616',
116
+ primary: accent,
117
+ primaryForeground: readableOn(accent),
118
+ secondary: soft,
119
+ secondaryForeground: mix('#2a2a2a', accent, 0.34),
120
+ accent: soft,
121
+ accentForeground: mix('#2a2a2a', accent, 0.34),
122
+ border,
123
+ input: mix('#e2e2e6', accent, 0.18),
124
+ ring: accent,
125
+ midground,
126
+ midgroundForeground: readableOn(midground),
127
+ destructive: '#b94a3a',
128
+ destructiveForeground: '#ffffff',
129
+ sidebarBackground: mix('#fafafa', accent, 0.05),
130
+ sidebarBorder: border,
131
+ userBubble: soft,
132
+ userBubbleBorder: border
133
+ }
134
+ }
135
+
136
+ /** Returns the seed palette for a given skin + mode (no overrides applied). */
137
+ export function getBaseColors(skinName: string, mode: 'light' | 'dark'): DesktopThemeColors {
138
+ const seed = BUILTIN_THEMES[skinName] ?? nousTheme
139
+
140
+ if (mode === 'dark') {
141
+ return seed.darkColors ?? seed.colors
142
+ }
143
+
144
+ return seed.darkColors ? seed.colors : synthLightColors(seed)
145
+ }
146
+
147
+ function deriveTheme(skinName: string, mode: 'light' | 'dark'): DesktopTheme {
148
+ const seed = BUILTIN_THEMES[skinName] ?? nousTheme
149
+
150
+ return {
151
+ ...seed,
152
+ name: `${skinName}-${mode}`,
153
+ label: `${seed.label} ${mode === 'light' ? 'Light' : 'Dark'}`,
154
+ description: `${seed.label} ${mode} palette`,
155
+ colors: getBaseColors(skinName, mode)
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Some palettes intentionally keep a bright background even when
161
+ * `mode === 'dark'`, so we shouldn't apply the `.dark` class. Decide from
162
+ * the actual background luminance.
163
+ */
164
+ function renderedModeFor(colors: DesktopThemeColors, mode: 'light' | 'dark'): 'light' | 'dark' {
165
+ const rgb = hexToRgb(colors.background)
166
+
167
+ if (!rgb) {
168
+ return mode
169
+ }
170
+
171
+ const [r, g, b] = rgb.map(v => v / 255)
172
+
173
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b > 0.5 ? 'light' : 'dark'
174
+ }
175
+
176
+ // ─── CSS application ────────────────────────────────────────────────────────
177
+
178
+ // Per-mode mix knobs. Light/dark fallbacks live in styles.css `:root` /
179
+ // `:root.dark`; setting them inline keeps active-skin overrides surviving
180
+ // the boot-time paint.
181
+ const mixesFor = (isDark: boolean): Record<string, string> => ({
182
+ '--theme-mix-chrome': isDark ? '74%' : '92%',
183
+ '--theme-mix-sidebar': '100%',
184
+ '--theme-mix-card': isDark ? '38%' : '22%',
185
+ '--theme-mix-elevated': isDark ? '46%' : '28%',
186
+ '--theme-mix-bubble': isDark ? '46%' : '0%'
187
+ })
188
+
189
+ function applyTheme(theme: DesktopTheme, mode: 'light' | 'dark') {
190
+ if (typeof document === 'undefined') {
191
+ return
192
+ }
193
+
194
+ const root = document.documentElement
195
+ const c = theme.colors
196
+ const typo = { ...DEFAULT_TYPOGRAPHY, ...nousTheme.typography, ...theme.typography }
197
+ const rendered = renderedModeFor(c, mode)
198
+ const isDark = rendered === 'dark'
199
+ const midground = c.midground ?? c.ring
200
+ const skinName = theme.name.endsWith(`-${mode}`) ? theme.name.slice(0, -mode.length - 1) : theme.name
201
+
202
+ root.style.setProperty('color-scheme', rendered)
203
+ root.dataset.NASTECHTheme = skinName
204
+ root.dataset.NASTECHMode = rendered
205
+ root.classList.toggle('dark', isDark)
206
+
207
+ // Brand seeds feed every glass + shadcn token via `color-mix()` in styles.css.
208
+ const seeds: Record<string, string> = {
209
+ '--theme-foreground': c.foreground,
210
+ '--theme-primary': c.primary,
211
+ '--theme-secondary': c.secondary,
212
+ '--theme-accent-soft': c.accent,
213
+ '--theme-midground': midground,
214
+ '--theme-warm': c.primary,
215
+ '--theme-background-seed': c.background,
216
+ '--theme-sidebar-seed': c.sidebarBackground ?? c.background,
217
+ '--theme-card-seed': c.card,
218
+ '--theme-elevated-seed': c.popover,
219
+ '--theme-bubble-seed': c.userBubble ?? c.popover
220
+ }
221
+
222
+ // shadcn/Tailwind tokens that aren't derived from the seed chain.
223
+ const palette: Record<string, string> = {
224
+ '--dt-primary-foreground': c.primaryForeground,
225
+ '--dt-secondary-foreground': c.secondaryForeground,
226
+ '--dt-accent-foreground': c.accentForeground,
227
+ '--dt-border': c.border,
228
+ '--dt-input': c.input,
229
+ '--dt-ring': c.ring,
230
+ '--dt-muted': c.muted,
231
+ '--dt-midground-foreground': c.midgroundForeground ?? readableOn(midground),
232
+ '--dt-composer-ring': c.composerRing ?? midground,
233
+ '--dt-destructive': c.destructive,
234
+ '--dt-destructive-foreground': c.destructiveForeground,
235
+ '--dt-sidebar-border': c.sidebarBorder ?? c.border,
236
+ '--dt-user-bubble-border': c.userBubbleBorder ?? c.border,
237
+ '--dt-font-sans': typo.fontSans,
238
+ '--dt-font-mono': typo.fontMono,
239
+ '--noise-opacity-mul': isDark ? 'calc(0.04 / 0.21)' : 'calc(0.34 / 0.21)'
240
+ }
241
+
242
+ for (const [k, v] of Object.entries({ ...seeds, ...mixesFor(isDark), ...palette })) {
243
+ root.style.setProperty(k, v)
244
+ }
245
+
246
+ window.NASTECHDesktop?.setTitleBarTheme?.({
247
+ background: c.background,
248
+ foreground: c.foreground
249
+ })
250
+
251
+ if (typo.fontUrl && !INJECTED_FONT_URLS.has(typo.fontUrl)) {
252
+ const link = document.createElement('link')
253
+ link.rel = 'stylesheet'
254
+ link.href = typo.fontUrl
255
+ link.dataset.NASTECHThemeFont = 'true'
256
+ document.head.appendChild(link)
257
+ INJECTED_FONT_URLS.add(typo.fontUrl)
258
+ }
259
+ }
260
+
261
+ // Boot-time paint to avoid a flash before <ThemeProvider> mounts.
262
+ if (typeof window !== 'undefined') {
263
+ const skin = normalizeSkin(window.localStorage.getItem(SKIN_KEY))
264
+ const mode = (window.localStorage.getItem(MODE_KEY) as ThemeMode) ?? 'light'
265
+ const resolved = resolveMode(mode)
266
+ applyTheme(deriveTheme(skin, resolved), resolved)
267
+ }
268
+
269
+ // ─── Context ────────────────────────────────────────────────────────────────
270
+
271
+ interface ThemeContextValue {
272
+ theme: DesktopTheme
273
+ themeName: string
274
+ mode: ThemeMode
275
+ resolvedMode: 'light' | 'dark'
276
+ availableThemes: Array<{ name: string; label: string; description: string }>
277
+ setTheme: (name: string) => void
278
+ setMode: (mode: ThemeMode) => void
279
+ }
280
+
281
+ const SKIN_LIST = BUILTIN_THEME_LIST.map(({ name, label, description }) => ({ name, label, description }))
282
+
283
+ const ThemeContext = createContext<ThemeContextValue>({
284
+ theme: nousTheme,
285
+ themeName: DEFAULT_SKIN_NAME,
286
+ mode: 'light',
287
+ resolvedMode: 'light',
288
+ availableThemes: SKIN_LIST,
289
+ setTheme: () => {},
290
+ setMode: () => {}
291
+ })
292
+
293
+ export function ThemeProvider({ children }: { children: ReactNode }) {
294
+ const [themeName, setThemeNameState] = useState(() =>
295
+ typeof window === 'undefined' ? DEFAULT_SKIN_NAME : normalizeSkin(window.localStorage.getItem(SKIN_KEY))
296
+ )
297
+
298
+ const [mode, setModeState] = useState<ThemeMode>(() =>
299
+ typeof window === 'undefined' ? 'light' : ((window.localStorage.getItem(MODE_KEY) as ThemeMode) ?? 'light')
300
+ )
301
+
302
+ const systemDark = useMediaQuery('(prefers-color-scheme: dark)')
303
+ const resolvedMode = resolveMode(mode, systemDark)
304
+ const activeTheme = useMemo(() => deriveTheme(themeName, resolvedMode), [themeName, resolvedMode])
305
+
306
+ useEffect(() => applyTheme(activeTheme, resolvedMode), [activeTheme, resolvedMode])
307
+
308
+ const setTheme = useCallback((name: string) => {
309
+ const next = normalizeSkin(name)
310
+ setThemeNameState(next)
311
+ window.localStorage.setItem(SKIN_KEY, next)
312
+ }, [])
313
+
314
+ const setMode = useCallback((next: ThemeMode) => {
315
+ setModeState(next)
316
+ window.localStorage.setItem(MODE_KEY, next)
317
+ }, [])
318
+
319
+ // The light/dark toggle (Shift+X by default) is owned by the keybind runtime
320
+ // (`appearance.toggleMode`) so it shows up in the hotkey map and is rebindable.
321
+
322
+ const value = useMemo<ThemeContextValue>(
323
+ () => ({ theme: activeTheme, themeName, mode, resolvedMode, availableThemes: SKIN_LIST, setTheme, setMode }),
324
+ [activeTheme, themeName, mode, resolvedMode, setTheme, setMode]
325
+ )
326
+
327
+ return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
328
+ }
329
+
330
+ export const useTheme = (): ThemeContextValue => useContext(ThemeContext)
331
+
332
+ /** Sync the desktop skin with the active NasTech backend theme on connect. */
333
+ export function useSyncThemeFromBackend(backendThemeName: string | undefined, setTheme: (name: string) => void) {
334
+ useEffect(() => {
335
+ if (backendThemeName && BUILTIN_THEMES[backendThemeName]) {
336
+ setTheme(backendThemeName)
337
+ }
338
+ }, [backendThemeName, setTheme])
339
+ }
@@ -0,0 +1,3 @@
1
+ export { ThemeProvider, useSyncThemeFromBackend, useTheme } from './context'
2
+ export { BUILTIN_THEME_LIST, BUILTIN_THEMES, DEFAULT_SKIN_NAME } from './presets'
3
+ export type { DesktopTheme, DesktopThemeColors, DesktopThemeTypography } from './types'
@@ -0,0 +1,119 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import type { DesktopMarketplaceThemeResult } from '@/global'
4
+
5
+ import { luminance } from './color'
6
+ import { buildThemeFromMarketplace } from './install'
7
+
8
+ const themeJson = (type: 'light' | 'dark', background: string, foreground: string) =>
9
+ JSON.stringify({ type, colors: { 'editor.background': background, 'editor.foreground': foreground } })
10
+
11
+ // A full base-8 ANSI set keyed off `red` so each variant is distinguishable.
12
+ const ansiColors = (red: string) => ({
13
+ 'terminal.ansiBlack': '#000000',
14
+ 'terminal.ansiRed': red,
15
+ 'terminal.ansiGreen': '#00aa00',
16
+ 'terminal.ansiYellow': '#aaaa00',
17
+ 'terminal.ansiBlue': '#0000aa',
18
+ 'terminal.ansiMagenta': '#aa00aa',
19
+ 'terminal.ansiCyan': '#00aaaa',
20
+ 'terminal.ansiWhite': '#aaaaaa'
21
+ })
22
+
23
+ const themeJsonWithAnsi = (type: 'light' | 'dark', background: string, foreground: string, red: string) =>
24
+ JSON.stringify({ type, colors: { 'editor.background': background, 'editor.foreground': foreground, ...ansiColors(red) } })
25
+
26
+ describe('buildThemeFromMarketplace', () => {
27
+ it('folds a light + dark variant into one family with both slots', () => {
28
+ const result: DesktopMarketplaceThemeResult = {
29
+ extensionId: 'ryanolsonx.solarized',
30
+ displayName: 'Solarized',
31
+ themes: [
32
+ { label: 'Solarized Light', uiTheme: 'vs', contents: themeJson('light', '#fdf6e3', '#586e75') },
33
+ { label: 'Solarized Dark', uiTheme: 'vs-dark', contents: themeJson('dark', '#002b36', '#93a1a1') }
34
+ ]
35
+ }
36
+
37
+ const theme = buildThemeFromMarketplace(result)
38
+
39
+ expect(theme.label).toBe('Solarized')
40
+ expect(theme.name).toBe('vsc-solarized')
41
+ // colors = the light variant, darkColors = the dark variant → the toggle works.
42
+ expect(theme.colors.background).toBe('#fdf6e3')
43
+ expect(theme.darkColors?.background).toBe('#002b36')
44
+ expect(luminance(theme.colors.background)).toBeGreaterThan(0.5)
45
+ expect(luminance(theme.darkColors!.background)).toBeLessThan(0.5)
46
+ })
47
+
48
+ it('orders variants by contribution regardless of light/dark sequence', () => {
49
+ const result: DesktopMarketplaceThemeResult = {
50
+ extensionId: 'github.github-vscode-theme',
51
+ displayName: 'GitHub Theme',
52
+ themes: [
53
+ { label: 'GitHub Dark Default', uiTheme: 'vs-dark', contents: themeJson('dark', '#0d1117', '#e6edf3') },
54
+ { label: 'GitHub Light Default', uiTheme: 'vs', contents: themeJson('light', '#ffffff', '#1f2328') }
55
+ ]
56
+ }
57
+
58
+ const theme = buildThemeFromMarketplace(result)
59
+ expect(theme.colors.background).toBe('#ffffff')
60
+ expect(theme.darkColors?.background).toBe('#0d1117')
61
+ })
62
+
63
+ it('fills both slots with the sole palette for a single-variant extension', () => {
64
+ const result: DesktopMarketplaceThemeResult = {
65
+ extensionId: 'dracula-theme.theme-dracula',
66
+ displayName: 'Dracula',
67
+ themes: [{ label: 'Dracula', uiTheme: 'vs-dark', contents: themeJson('dark', '#282a36', '#f8f8f2') }]
68
+ }
69
+
70
+ const theme = buildThemeFromMarketplace(result)
71
+ expect(theme.colors.background).toBe('#282a36')
72
+ expect(theme.darkColors).toBe(theme.colors)
73
+ })
74
+
75
+ it('keys each variant terminal palette to its mode (terminal / darkTerminal)', () => {
76
+ const result: DesktopMarketplaceThemeResult = {
77
+ extensionId: 'ryanolsonx.solarized',
78
+ displayName: 'Solarized',
79
+ themes: [
80
+ { label: 'Solarized Light', uiTheme: 'vs', contents: themeJsonWithAnsi('light', '#fdf6e3', '#586e75', '#dc322f') },
81
+ { label: 'Solarized Dark', uiTheme: 'vs-dark', contents: themeJsonWithAnsi('dark', '#002b36', '#93a1a1', '#ff5f56') }
82
+ ]
83
+ }
84
+
85
+ const theme = buildThemeFromMarketplace(result)
86
+ expect(theme.terminal?.red).toBe('#dc322f')
87
+ expect(theme.darkTerminal?.red).toBe('#ff5f56')
88
+ })
89
+
90
+ it('reuses the sole variant terminal palette for both modes', () => {
91
+ const result: DesktopMarketplaceThemeResult = {
92
+ extensionId: 'dracula-theme.theme-dracula',
93
+ displayName: 'Dracula',
94
+ themes: [{ label: 'Dracula', uiTheme: 'vs-dark', contents: themeJsonWithAnsi('dark', '#282a36', '#f8f8f2', '#ff5555') }]
95
+ }
96
+
97
+ const theme = buildThemeFromMarketplace(result)
98
+ expect(theme.terminal?.red).toBe('#ff5555')
99
+ expect(theme.darkTerminal?.red).toBe('#ff5555')
100
+ })
101
+
102
+ it('leaves terminal slots unset when no variant ships an ANSI palette', () => {
103
+ const result: DesktopMarketplaceThemeResult = {
104
+ extensionId: 'x.plain',
105
+ displayName: 'Plain',
106
+ themes: [{ label: 'Plain', uiTheme: 'vs-dark', contents: themeJson('dark', '#101010', '#fafafa') }]
107
+ }
108
+
109
+ const theme = buildThemeFromMarketplace(result)
110
+ expect(theme.terminal).toBeUndefined()
111
+ expect(theme.darkTerminal).toBeUndefined()
112
+ })
113
+
114
+ it('throws when the extension contributes no themes', () => {
115
+ expect(() =>
116
+ buildThemeFromMarketplace({ extensionId: 'x.y', displayName: 'X', themes: [] })
117
+ ).toThrow(/does not contribute/i)
118
+ })
119
+ })