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,113 @@
1
+ import type {
2
+ NasTechConnection,
3
+ NasTechReadDirResult,
4
+ NasTechReadFileTextResult,
5
+ NasTechSelectPathsOptions,
6
+ NasTechWorktreeInfo
7
+ } from '@/global'
8
+ import { $connection } from '@/store/session'
9
+
10
+ export interface DesktopFsRemotePicker {
11
+ selectPaths: (options?: NasTechSelectPathsOptions) => Promise<string[]>
12
+ }
13
+
14
+ let remotePicker: DesktopFsRemotePicker | null = null
15
+
16
+ export function setDesktopFsRemotePicker(next: DesktopFsRemotePicker | null) {
17
+ remotePicker = next
18
+ }
19
+
20
+ function connectionCacheKey(connection: NasTechConnection | null) {
21
+ if (!connection) {
22
+ return 'local:'
23
+ }
24
+ return `${connection.mode || 'local'}:${connection.profile || ''}:${connection.baseUrl || ''}`
25
+ }
26
+
27
+ export function desktopFsCacheKey() {
28
+ return connectionCacheKey($connection.get())
29
+ }
30
+
31
+ export function isDesktopFsRemoteMode() {
32
+ return $connection.get()?.mode === 'remote'
33
+ }
34
+
35
+ function fsPath(endpoint: string, filePath: string) {
36
+ return `/api/fs/${endpoint}?path=${encodeURIComponent(filePath)}`
37
+ }
38
+
39
+ function bridge() {
40
+ const desktop = window.NASTECHDesktop
41
+ if (!desktop) {
42
+ throw new Error('NasTech Desktop bridge is unavailable')
43
+ }
44
+ return desktop
45
+ }
46
+
47
+ export async function readDesktopDir(path: string): Promise<NasTechReadDirResult> {
48
+ const desktop = bridge()
49
+ if (!isDesktopFsRemoteMode()) {
50
+ return desktop.readDir(path)
51
+ }
52
+ return desktop.api<NasTechReadDirResult>({ path: fsPath('list', path) })
53
+ }
54
+
55
+ export async function readDesktopFileText(path: string): Promise<NasTechReadFileTextResult> {
56
+ const desktop = bridge()
57
+ if (!isDesktopFsRemoteMode()) {
58
+ return desktop.readFileText(path)
59
+ }
60
+ return desktop.api<NasTechReadFileTextResult>({ path: fsPath('read-text', path) })
61
+ }
62
+
63
+ export async function readDesktopFileDataUrl(path: string): Promise<string> {
64
+ const desktop = bridge()
65
+ if (!isDesktopFsRemoteMode()) {
66
+ return desktop.readFileDataUrl(path)
67
+ }
68
+
69
+ const result = await desktop.api<string | { dataUrl?: string }>({ path: fsPath('read-data-url', path) })
70
+ return typeof result === 'string' ? result : result.dataUrl || ''
71
+ }
72
+
73
+ export async function desktopGitRoot(path: string): Promise<string | null> {
74
+ const desktop = bridge()
75
+ if (!isDesktopFsRemoteMode()) {
76
+ return desktop.gitRoot ? desktop.gitRoot(path) : null
77
+ }
78
+
79
+ const result = await desktop.api<{ root: string | null }>({ path: fsPath('git-root', path) })
80
+ return result.root
81
+ }
82
+
83
+ // Worktree detection runs against the LOCAL filesystem (the electron main
84
+ // process). For a remote backend the session cwds live on another machine, so
85
+ // we can't resolve them here — callers fall back to the path-name heuristic.
86
+ export async function desktopWorktrees(cwds: string[]): Promise<Record<string, NasTechWorktreeInfo | null>> {
87
+ if (isDesktopFsRemoteMode()) {
88
+ return {}
89
+ }
90
+
91
+ const desktop = bridge()
92
+
93
+ return desktop.worktrees ? desktop.worktrees(cwds) : {}
94
+ }
95
+
96
+ export async function desktopDefaultCwd(): Promise<{ branch: string; cwd: string } | null> {
97
+ if (!isDesktopFsRemoteMode()) {
98
+ return null
99
+ }
100
+
101
+ return bridge().api<{ branch: string; cwd: string }>({ path: '/api/fs/default-cwd' })
102
+ }
103
+
104
+ export async function selectDesktopPaths(options?: NasTechSelectPathsOptions): Promise<string[]> {
105
+ const desktop = bridge()
106
+ if (!isDesktopFsRemoteMode()) {
107
+ return desktop.selectPaths(options)
108
+ }
109
+ if (!options?.directories || options.multiple !== false) {
110
+ return []
111
+ }
112
+ return remotePicker ? remotePicker.selectPaths(options) : []
113
+ }
@@ -0,0 +1,126 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import {
4
+ desktopSkinSlashCompletions,
5
+ desktopSlashDescription,
6
+ desktopSlashUnavailableMessage,
7
+ filterDesktopCommandsCatalog,
8
+ isDesktopSlashCommand,
9
+ isDesktopSlashSuggestion,
10
+ isModelPickerCommand
11
+ } from './desktop-slash-commands'
12
+
13
+ describe('desktop slash command curation', () => {
14
+ it('keeps core desktop chat commands in suggestions', () => {
15
+ expect(isDesktopSlashSuggestion('/new')).toBe(true)
16
+ expect(isDesktopSlashSuggestion('/branch')).toBe(true)
17
+ expect(isDesktopSlashSuggestion('/skin')).toBe(true)
18
+ expect(isDesktopSlashSuggestion('/usage')).toBe(true)
19
+ expect(isDesktopSlashSuggestion('/version')).toBe(true)
20
+ expect(isDesktopSlashSuggestion('/yolo')).toBe(true)
21
+ expect(isDesktopSlashCommand('/yolo')).toBe(true)
22
+ })
23
+
24
+ it('surfaces skill and quick commands (extensions) in suggestions and lets them run', () => {
25
+ expect(isDesktopSlashSuggestion('/my-skill')).toBe(true)
26
+ expect(isDesktopSlashSuggestion('/gif-search')).toBe(true)
27
+ expect(isDesktopSlashCommand('/my-skill')).toBe(true)
28
+ })
29
+
30
+ it('hides terminal, messaging, and dedicated-UI commands from suggestions', () => {
31
+ expect(isDesktopSlashSuggestion('/clear')).toBe(false)
32
+ expect(isDesktopSlashSuggestion('/compact')).toBe(false)
33
+ expect(isDesktopSlashSuggestion('/redraw')).toBe(false)
34
+ expect(isDesktopSlashSuggestion('/approve')).toBe(false)
35
+ expect(isDesktopSlashSuggestion('/model')).toBe(false)
36
+ expect(isDesktopSlashSuggestion('/skills')).toBe(false)
37
+ expect(isDesktopSlashSuggestion('/voice')).toBe(false)
38
+ expect(isDesktopSlashSuggestion('/curator')).toBe(false)
39
+ })
40
+
41
+ it('allows aliases to execute without cluttering the popover', () => {
42
+ expect(isDesktopSlashSuggestion('/reset')).toBe(false)
43
+ expect(isDesktopSlashCommand('/reset')).toBe(true)
44
+ })
45
+
46
+ it('filters built-in catalog noise but keeps skill / quick-command extensions', () => {
47
+ const filtered = filterDesktopCommandsCatalog({
48
+ categories: [
49
+ {
50
+ name: 'Session',
51
+ pairs: [
52
+ ['/new', 'Start a new session'],
53
+ ['/clear', 'Clear terminal screen']
54
+ ]
55
+ },
56
+ {
57
+ name: 'User commands',
58
+ pairs: [['/ship-it', 'Run release checklist']]
59
+ }
60
+ ],
61
+ pairs: [
62
+ ['/new', 'Start a new session'],
63
+ ['/model', 'Switch model'],
64
+ ['/ship-it', 'Run release checklist']
65
+ ],
66
+ skill_count: 2
67
+ })
68
+
69
+ expect(filtered.categories).toEqual([
70
+ { name: 'Session', pairs: [['/new', 'Start a new desktop chat']] },
71
+ { name: 'User commands', pairs: [['/ship-it', 'Run release checklist']] }
72
+ ])
73
+ expect(filtered.pairs).toEqual([
74
+ ['/new', 'Start a new desktop chat'],
75
+ ['/ship-it', 'Run release checklist']
76
+ ])
77
+ expect(filtered.skill_count).toBe(2)
78
+ })
79
+
80
+ it('uses desktop-specific labels for commands with different UI behavior', () => {
81
+ expect(desktopSlashDescription('/branch', 'Branch the current session')).toBe(
82
+ 'Branch the latest message into a new chat'
83
+ )
84
+ expect(desktopSlashDescription('/skin', 'Show or change the display skin/theme')).toBe(
85
+ 'Switch desktop theme or cycle to the next one'
86
+ )
87
+ })
88
+
89
+ it('builds /skin completions from desktop themes', () => {
90
+ const completions = desktopSkinSlashCompletions(
91
+ [
92
+ { name: 'mono', label: 'Mono', description: 'Clean grayscale' },
93
+ { name: 'midnight', label: 'Midnight', description: 'Deep blue' },
94
+ { name: 'slate', label: 'Slate', description: 'Cool slate blue' }
95
+ ],
96
+ 'mono',
97
+ 'm'
98
+ )
99
+
100
+ expect(completions).toEqual([
101
+ {
102
+ text: '/skin mono',
103
+ display: '/skin mono',
104
+ meta: 'Mono (current) - Clean grayscale'
105
+ },
106
+ {
107
+ text: '/skin midnight',
108
+ display: '/skin midnight',
109
+ meta: 'Midnight - Deep blue'
110
+ }
111
+ ])
112
+ })
113
+
114
+ it('explains known commands that desktop owns elsewhere', () => {
115
+ expect(desktopSlashUnavailableMessage('/model sonnet')).toContain('model picker')
116
+ expect(desktopSlashUnavailableMessage('/skills')).toContain('desktop sidebar')
117
+ expect(desktopSlashUnavailableMessage('/clear')).toContain('terminal interface')
118
+ })
119
+
120
+ it('flags /model as a picker-owned command so the desktop opens the overlay', () => {
121
+ expect(isModelPickerCommand('/model')).toBe(true)
122
+ expect(isModelPickerCommand('/model sonnet')).toBe(true)
123
+ expect(isModelPickerCommand('/new')).toBe(false)
124
+ expect(isModelPickerCommand('/skills')).toBe(false)
125
+ })
126
+ })
@@ -0,0 +1,286 @@
1
+ export interface CommandsCatalogSection {
2
+ name: string
3
+ pairs: [string, string][]
4
+ }
5
+
6
+ export interface CommandsCatalogLike {
7
+ categories?: CommandsCatalogSection[]
8
+ pairs?: [string, string][]
9
+ skill_count?: number
10
+ warning?: string
11
+ }
12
+
13
+ export interface DesktopSlashCompletion {
14
+ display: string
15
+ meta: string
16
+ text: string
17
+ }
18
+
19
+ export interface DesktopThemeCommandOption {
20
+ description: string
21
+ label: string
22
+ name: string
23
+ }
24
+
25
+ const DESKTOP_COMMAND_META = [
26
+ ['/agents', 'Show active desktop sessions and running tasks'],
27
+ ['/background', 'Run a prompt in the background'],
28
+ ['/branch', 'Branch the latest message into a new chat'],
29
+ ['/compress', 'Compress this conversation context'],
30
+ ['/debug', 'Create a debug report'],
31
+ ['/goal', 'Manage the standing goal for this session'],
32
+ ['/help', 'Show desktop slash commands'],
33
+ ['/new', 'Start a new desktop chat'],
34
+ ['/profile', 'Switch the active NasTech profile'],
35
+ ['/queue', 'Queue a prompt for the next turn'],
36
+ ['/resume', 'Resume a saved session'],
37
+ ['/retry', 'Retry the last user message'],
38
+ ['/rollback', 'List or restore filesystem checkpoints'],
39
+ ['/skin', 'Switch desktop theme or cycle to the next one'],
40
+ ['/status', 'Show current session status'],
41
+ ['/steer', 'Steer the current run after the next tool call'],
42
+ ['/stop', 'Stop running background processes'],
43
+ ['/title', 'Rename the current session'],
44
+ ['/undo', 'Remove the last user/assistant exchange'],
45
+ ['/usage', 'Show token usage for this session'],
46
+ ['/version', 'Show NasTech Agent version'],
47
+ ['/yolo', 'Toggle YOLO — auto-approve dangerous commands']
48
+ ] as const
49
+
50
+ const DESKTOP_COMMANDS: ReadonlySet<string> = new Set(DESKTOP_COMMAND_META.map(([command]) => command))
51
+
52
+ const DESKTOP_ALIASES = new Map([
53
+ ['/bg', '/background'],
54
+ ['/btw', '/background'],
55
+ ['/fork', '/branch'],
56
+ ['/q', '/queue'],
57
+ ['/reload_mcp', '/reload-mcp'],
58
+ ['/reload_skills', '/reload-skills'],
59
+ ['/reset', '/new'],
60
+ ['/tasks', '/agents']
61
+ ])
62
+
63
+ const DESKTOP_COMMAND_DESCRIPTIONS: ReadonlyMap<string, string> = new Map(DESKTOP_COMMAND_META)
64
+
65
+ const PICKER_OWNED_COMMANDS = new Set(['/model'])
66
+
67
+ const TERMINAL_ONLY_COMMANDS = new Set([
68
+ '/browser',
69
+ '/busy',
70
+ '/clear',
71
+ '/commands',
72
+ '/compact',
73
+ '/config',
74
+ '/copy',
75
+ '/cron',
76
+ '/details',
77
+ '/exit',
78
+ '/footer',
79
+ '/gateway',
80
+ '/gquota',
81
+ '/history',
82
+ '/image',
83
+ '/indicator',
84
+ '/logs',
85
+ '/mouse',
86
+ '/paste',
87
+ '/platforms',
88
+ '/plugins',
89
+ '/quit',
90
+ '/redraw',
91
+ '/reload',
92
+ '/restart',
93
+ '/save',
94
+ '/sb',
95
+ '/set-home',
96
+ '/sethome',
97
+ '/snap',
98
+ '/snapshot',
99
+ '/statusbar',
100
+ '/toolsets',
101
+ '/tools',
102
+ '/update',
103
+ '/verbose'
104
+ ])
105
+
106
+ const MESSAGING_ONLY_COMMANDS = new Set(['/approve', '/deny'])
107
+
108
+ const SETTINGS_OWNED_COMMANDS = new Set(['/skills'])
109
+
110
+ const ADVANCED_COMMANDS = new Set([
111
+ '/curator',
112
+ '/fast',
113
+ '/insights',
114
+ '/kanban',
115
+ '/personality',
116
+ '/reasoning',
117
+ '/reload-mcp',
118
+ '/reload-skills',
119
+ '/voice'
120
+ ])
121
+
122
+ const BLOCKED_COMMANDS = new Set([
123
+ ...PICKER_OWNED_COMMANDS,
124
+ ...TERMINAL_ONLY_COMMANDS,
125
+ ...MESSAGING_ONLY_COMMANDS,
126
+ ...SETTINGS_OWNED_COMMANDS,
127
+ ...ADVANCED_COMMANDS
128
+ ])
129
+
130
+ function normalizeCommand(command: string): string {
131
+ const trimmed = command.trim()
132
+ const base = (trimmed.startsWith('/') ? trimmed : `/${trimmed}`).split(/\s+/, 1)[0]?.toLowerCase() || ''
133
+
134
+ return base
135
+ }
136
+
137
+ export function canonicalDesktopSlashCommand(command: string): string {
138
+ const normalized = normalizeCommand(command)
139
+
140
+ return DESKTOP_ALIASES.get(normalized) || normalized
141
+ }
142
+
143
+ export function isDesktopSlashCommand(command: string): boolean {
144
+ const normalized = normalizeCommand(command)
145
+ const canonical = canonicalDesktopSlashCommand(normalized)
146
+
147
+ if (BLOCKED_COMMANDS.has(normalized) || BLOCKED_COMMANDS.has(canonical)) {
148
+ return false
149
+ }
150
+
151
+ return DESKTOP_COMMANDS.has(canonical) || !isKnownNasTechSlashCommand(normalized)
152
+ }
153
+
154
+ /**
155
+ * An "extension" command is anything the backend surfaces that is NOT one of
156
+ * NasTech' built-in slash commands — i.e. skill commands (`/gif-search`,
157
+ * `/codex`, …) and user-defined quick commands. These are user-activated, so
158
+ * they should appear in the desktop slash palette even though they aren't in
159
+ * the curated `DESKTOP_COMMANDS` allow-list. This mirrors the predicate in
160
+ * `isDesktopSlashCommand` that already lets them EXECUTE when typed.
161
+ */
162
+ export function isDesktopSlashExtensionCommand(command: string): boolean {
163
+ const normalized = normalizeCommand(command)
164
+
165
+ if (!normalized || normalized === '/') {
166
+ return false
167
+ }
168
+
169
+ return !isKnownNasTechSlashCommand(normalized)
170
+ }
171
+
172
+ export function isDesktopSlashSuggestion(command: string): boolean {
173
+ const normalized = normalizeCommand(command)
174
+ const canonical = canonicalDesktopSlashCommand(normalized)
175
+
176
+ // Surface skill / quick commands (extensions the backend provides) alongside
177
+ // the curated built-ins. Built-in aliases stay hidden so the popover isn't
178
+ // cluttered with duplicates.
179
+ if (isDesktopSlashExtensionCommand(normalized)) {
180
+ return true
181
+ }
182
+
183
+ return DESKTOP_COMMANDS.has(canonical) && !DESKTOP_ALIASES.has(normalized)
184
+ }
185
+
186
+ /**
187
+ * True for commands the desktop fulfils by opening the model picker overlay
188
+ * (e.g. `/model`) rather than executing a slash command. The caller opens the
189
+ * picker UI instead of printing the "uses the desktop model picker" notice.
190
+ */
191
+ export function isModelPickerCommand(command: string): boolean {
192
+ const normalized = normalizeCommand(command)
193
+ const canonical = canonicalDesktopSlashCommand(normalized)
194
+
195
+ return PICKER_OWNED_COMMANDS.has(canonical)
196
+ }
197
+
198
+ export function desktopSlashUnavailableMessage(command: string): string | null {
199
+ const normalized = normalizeCommand(command)
200
+ const canonical = canonicalDesktopSlashCommand(normalized)
201
+
202
+ if (PICKER_OWNED_COMMANDS.has(canonical)) {
203
+ return `/${canonical.slice(1)} uses the desktop model picker instead of a slash command.`
204
+ }
205
+
206
+ if (SETTINGS_OWNED_COMMANDS.has(canonical)) {
207
+ return `/${canonical.slice(1)} is managed from the desktop sidebar.`
208
+ }
209
+
210
+ if (MESSAGING_ONLY_COMMANDS.has(canonical)) {
211
+ return `/${canonical.slice(1)} is only used from messaging platforms.`
212
+ }
213
+
214
+ if (ADVANCED_COMMANDS.has(canonical)) {
215
+ return `/${canonical.slice(1)} is not shown in the desktop slash palette. Use the relevant desktop control or terminal interface instead.`
216
+ }
217
+
218
+ if (TERMINAL_ONLY_COMMANDS.has(normalized) || TERMINAL_ONLY_COMMANDS.has(canonical)) {
219
+ return `/${canonical.slice(1)} is only available in the terminal interface.`
220
+ }
221
+
222
+ return null
223
+ }
224
+
225
+ export function desktopSlashDescription(command: string, fallback = ''): string {
226
+ const canonical = canonicalDesktopSlashCommand(command)
227
+
228
+ return DESKTOP_COMMAND_DESCRIPTIONS.get(canonical) || fallback
229
+ }
230
+
231
+ export function desktopSkinSlashCompletions(
232
+ themes: DesktopThemeCommandOption[],
233
+ activeThemeName: string,
234
+ argPrefix: string
235
+ ): DesktopSlashCompletion[] {
236
+ const prefix = argPrefix.trim().toLowerCase()
237
+
238
+ const commands: DesktopSlashCompletion[] = [
239
+ {
240
+ text: '/skin list',
241
+ display: '/skin list',
242
+ meta: 'Show available desktop themes'
243
+ },
244
+ {
245
+ text: '/skin next',
246
+ display: '/skin next',
247
+ meta: 'Cycle to the next desktop theme'
248
+ },
249
+ ...themes.map(theme => ({
250
+ text: `/skin ${theme.name}`,
251
+ display: `/skin ${theme.name}`,
252
+ meta: `${theme.label}${theme.name === activeThemeName ? ' (current)' : ''} - ${theme.description}`
253
+ }))
254
+ ]
255
+
256
+ if (!prefix) {
257
+ return commands
258
+ }
259
+
260
+ return commands.filter(item => item.text.slice('/skin '.length).toLowerCase().startsWith(prefix))
261
+ }
262
+
263
+ export function filterDesktopCommandsCatalog(catalog: CommandsCatalogLike): CommandsCatalogLike {
264
+ const categories = catalog.categories
265
+ ?.map(section => ({
266
+ ...section,
267
+ pairs: section.pairs
268
+ .filter(([command]) => isDesktopSlashSuggestion(command))
269
+ .map(([command, description]) => [command, desktopSlashDescription(command, description)] as [string, string])
270
+ }))
271
+ .filter(section => section.pairs.length > 0)
272
+
273
+ const pairs = catalog.pairs
274
+ ?.filter(([command]) => isDesktopSlashSuggestion(command))
275
+ .map(([command, description]) => [command, desktopSlashDescription(command, description)] as [string, string])
276
+
277
+ return {
278
+ ...catalog,
279
+ ...(categories ? { categories } : {}),
280
+ ...(pairs ? { pairs } : {})
281
+ }
282
+ }
283
+
284
+ function isKnownNasTechSlashCommand(command: string): boolean {
285
+ return DESKTOP_COMMANDS.has(command) || DESKTOP_ALIASES.has(command) || BLOCKED_COMMANDS.has(command)
286
+ }
@@ -0,0 +1,35 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { extractEmbeddedImages } from './embedded-images'
4
+
5
+ const SAMPLE_PNG_DATA_URL = 'data:image/png;base64,' + 'A'.repeat(120)
6
+
7
+ describe('extractEmbeddedImages', () => {
8
+ it('returns text untouched when no data URL is present', () => {
9
+ expect(extractEmbeddedImages('describe this')).toEqual({ cleanedText: 'describe this', images: [] })
10
+ })
11
+
12
+ it('lifts a bare data:image URL out of prose', () => {
13
+ const result = extractEmbeddedImages(`describe this ${SAMPLE_PNG_DATA_URL}`)
14
+
15
+ expect(result.cleanedText).toBe('describe this')
16
+ expect(result.images).toEqual([SAMPLE_PNG_DATA_URL])
17
+ })
18
+
19
+ it('lifts a JSON-wrapped image_url envelope out of prose', () => {
20
+ const result = extractEmbeddedImages(
21
+ `describe this{"type":"image_url","image_url":{"url":"${SAMPLE_PNG_DATA_URL}"}}`
22
+ )
23
+
24
+ expect(result.cleanedText).toBe('describe this')
25
+ expect(result.images).toEqual([SAMPLE_PNG_DATA_URL])
26
+ })
27
+
28
+ it('extracts multiple embedded images', () => {
29
+ const second = 'data:image/jpeg;base64,' + 'B'.repeat(96)
30
+ const result = extractEmbeddedImages(`first ${SAMPLE_PNG_DATA_URL} mid ${second} tail`)
31
+
32
+ expect(result.cleanedText).toBe('first mid tail')
33
+ expect(result.images).toEqual([SAMPLE_PNG_DATA_URL, second])
34
+ })
35
+ })
@@ -0,0 +1,60 @@
1
+ const EMBEDDED_IMAGE_RE =
2
+ /(\{\s*"type"\s*:\s*"image_url"\s*,\s*"image_url"\s*:\s*\{\s*"url"\s*:\s*")?(data:image\/[\w.+-]+;base64,[A-Za-z0-9+/=]{64,})("\s*\}\s*\})?/g
3
+
4
+ const DATA_URL_RE = /^data:([\w./+-]+);base64,(.*)$/i
5
+
6
+ export const DATA_IMAGE_URL_RE = /^data:image\/[\w.+-]+;base64,/i
7
+
8
+ export interface EmbeddedImageExtraction {
9
+ cleanedText: string
10
+ images: string[]
11
+ }
12
+
13
+ export function dataUrlToBlob(dataUrl: string): Blob | null {
14
+ const match = DATA_URL_RE.exec(dataUrl.trim())
15
+
16
+ if (!match) {
17
+ return null
18
+ }
19
+
20
+ try {
21
+ const bytes = atob(match[2])
22
+ const buffer = new Uint8Array(bytes.length)
23
+
24
+ for (let i = 0; i < bytes.length; i += 1) {
25
+ buffer[i] = bytes.charCodeAt(i)
26
+ }
27
+
28
+ return new Blob([buffer], { type: match[1] })
29
+ } catch {
30
+ return null
31
+ }
32
+ }
33
+
34
+ export function extractEmbeddedImages(text: string): EmbeddedImageExtraction {
35
+ if (!text || !text.includes('data:image/')) {
36
+ return { cleanedText: text, images: [] }
37
+ }
38
+
39
+ const images: string[] = []
40
+
41
+ const cleanedText = text
42
+ .replace(EMBEDDED_IMAGE_RE, (_match, _open, dataUrl: string) => {
43
+ images.push(dataUrl)
44
+
45
+ return ''
46
+ })
47
+ .replace(/[ \t]+\n/g, '\n')
48
+ .replace(/\n{3,}/g, '\n\n')
49
+ .trim()
50
+
51
+ return { cleanedText, images }
52
+ }
53
+
54
+ export function embeddedImageUrls(text: string): string[] {
55
+ return extractEmbeddedImages(text).images
56
+ }
57
+
58
+ export function textWithoutEmbeddedImages(text: string): string {
59
+ return extractEmbeddedImages(text).cleanedText
60
+ }