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,111 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ import {
4
+ $subagentsBySession,
5
+ activeSubagentCount,
6
+ buildSubagentTree,
7
+ clearSessionSubagents,
8
+ pruneDelegateFallbackSubagents,
9
+ upsertSubagent
10
+ } from './subagents'
11
+
12
+ const listFor = (sid: string) => $subagentsBySession.get()[sid] ?? []
13
+
14
+ describe('subagent store', () => {
15
+ beforeEach(() => $subagentsBySession.set({}))
16
+
17
+ it('upserts subagent progress and keeps terminal status stable', () => {
18
+ upsertSubagent('s1', { goal: 'scan files', status: 'running', subagent_id: 'a1', task_index: 0 })
19
+ upsertSubagent('s1', { goal: 'scan files', status: 'completed', subagent_id: 'a1', summary: 'done', task_index: 0 })
20
+ upsertSubagent('s1', { goal: 'scan files', status: 'running', subagent_id: 'a1', task_index: 0, text: 'late' })
21
+
22
+ const item = listFor('s1')[0]
23
+ expect(item?.status).toBe('completed')
24
+ expect(item?.summary).toBe('done')
25
+ })
26
+
27
+ it('builds parent/child trees', () => {
28
+ upsertSubagent('s1', { goal: 'parent', status: 'running', subagent_id: 'p', task_index: 0 })
29
+ upsertSubagent('s1', { goal: 'child', parent_id: 'p', status: 'queued', subagent_id: 'c', task_index: 1 })
30
+
31
+ const tree = buildSubagentTree(listFor('s1'))
32
+ expect(tree).toHaveLength(1)
33
+ expect(tree[0]?.children[0]?.goal).toBe('child')
34
+ expect(activeSubagentCount(listFor('s1'))).toBe(2)
35
+ })
36
+
37
+ it('keeps root nodes in spawn order, not task index order', () => {
38
+ const nowSpy = vi.spyOn(Date, 'now')
39
+ nowSpy.mockReturnValueOnce(1_000)
40
+ upsertSubagent('s1', { goal: 'first spawn', status: 'running', subagent_id: 'a', task_index: 2 })
41
+ nowSpy.mockReturnValueOnce(2_000)
42
+ upsertSubagent('s1', { goal: 'second spawn', status: 'running', subagent_id: 'b', task_index: 0 })
43
+ nowSpy.mockRestore()
44
+
45
+ expect(buildSubagentTree(listFor('s1')).map(n => n.id)).toEqual(['a', 'b'])
46
+ })
47
+
48
+ it('captures live thinking/progress/tool stream lines', () => {
49
+ upsertSubagent(
50
+ 's1',
51
+ { goal: 'scan files', status: 'queued', subagent_id: 'a1', task_index: 0 },
52
+ true,
53
+ 'subagent.spawn_requested'
54
+ )
55
+ upsertSubagent(
56
+ 's1',
57
+ {
58
+ status: 'running',
59
+ subagent_id: 'a1',
60
+ task_index: 0,
61
+ tool_name: 'search_files',
62
+ tool_preview: 'pattern=NASTECH'
63
+ },
64
+ false,
65
+ 'subagent.tool'
66
+ )
67
+ upsertSubagent(
68
+ 's1',
69
+ { status: 'running', subagent_id: 'a1', task_index: 0, text: 'plan the search order' },
70
+ false,
71
+ 'subagent.thinking'
72
+ )
73
+ upsertSubagent(
74
+ 's1',
75
+ { status: 'running', subagent_id: 'a1', task_index: 0, text: 'found candidate matches' },
76
+ false,
77
+ 'subagent.progress'
78
+ )
79
+ upsertSubagent(
80
+ 's1',
81
+ { status: 'completed', subagent_id: 'a1', summary: 'search complete', task_index: 0 },
82
+ false,
83
+ 'subagent.complete'
84
+ )
85
+
86
+ const item = listFor('s1')[0]
87
+ expect(item?.stream.map(e => e.kind)).toEqual(['tool', 'thinking', 'progress', 'summary'])
88
+ expect(item?.stream.find(e => e.kind === 'tool')?.text).toContain('Search Files')
89
+ expect(item?.stream.find(e => e.kind === 'thinking')?.text).toBe('plan the search order')
90
+ expect(item?.stream.find(e => e.kind === 'summary')?.text).toBe('search complete')
91
+ })
92
+
93
+ it('prunes delegate fallback rows once native events arrive', () => {
94
+ upsertSubagent('s1', { goal: 'fallback', status: 'running', subagent_id: 'delegate-tool:abc:0', task_index: 0 })
95
+ upsertSubagent('s1', { goal: 'native', status: 'running', subagent_id: 'sa-0-xyz', task_index: 0 })
96
+
97
+ pruneDelegateFallbackSubagents('s1')
98
+
99
+ expect(listFor('s1').map(item => item.id)).toEqual(['sa-0-xyz'])
100
+ })
101
+
102
+ it('clears one session without touching another', () => {
103
+ upsertSubagent('s1', { goal: 'one', status: 'running', subagent_id: 'a1', task_index: 0 })
104
+ upsertSubagent('s2', { goal: 'two', status: 'running', subagent_id: 'a2', task_index: 0 })
105
+
106
+ clearSessionSubagents('s1')
107
+
108
+ expect($subagentsBySession.get().s1).toBeUndefined()
109
+ expect($subagentsBySession.get().s2).toHaveLength(1)
110
+ })
111
+ })
@@ -0,0 +1,260 @@
1
+ import { atom } from 'nanostores'
2
+
3
+ export type SubagentStatus = 'completed' | 'failed' | 'interrupted' | 'queued' | 'running'
4
+ export type SubagentStreamKind = 'progress' | 'summary' | 'thinking' | 'tool'
5
+
6
+ export interface SubagentStreamEntry {
7
+ at: number
8
+ isError?: boolean
9
+ kind: SubagentStreamKind
10
+ text: string
11
+ }
12
+
13
+ export interface SubagentProgress {
14
+ id: string
15
+ parentId: null | string
16
+ goal: string
17
+ model?: string
18
+ status: SubagentStatus
19
+ taskCount: number
20
+ taskIndex: number
21
+ startedAt: number
22
+ updatedAt: number
23
+ durationSeconds?: number
24
+ costUsd?: number
25
+ inputTokens?: number
26
+ outputTokens?: number
27
+ toolCount?: number
28
+ filesRead: string[]
29
+ filesWritten: string[]
30
+ stream: SubagentStreamEntry[]
31
+ summary?: string
32
+ /** Active tool while running — cleared on terminal status. */
33
+ currentTool?: string
34
+ }
35
+
36
+ export interface SubagentNode extends SubagentProgress {
37
+ children: SubagentNode[]
38
+ }
39
+
40
+ export type SubagentPayload = Record<string, unknown>
41
+
42
+ const TERMINAL: ReadonlySet<SubagentStatus> = new Set(['completed', 'failed', 'interrupted'])
43
+ const MAX_STREAM = 24
44
+ const PREVIEW_MAX = 220
45
+ const TOOL_PREVIEW_MAX = 96
46
+
47
+ export const $subagentsBySession = atom<Record<string, SubagentProgress[]>>({})
48
+
49
+ const isStr = (v: unknown): v is string => typeof v === 'string'
50
+ const str = (v: unknown) => (isStr(v) ? v : '')
51
+ const num = (v: unknown) => (typeof v === 'number' && Number.isFinite(v) ? v : undefined)
52
+ const strList = (v: unknown) => (Array.isArray(v) ? v.filter(isStr) : [])
53
+
54
+ const asStatus = (v: unknown): SubagentStatus =>
55
+ v === 'completed' || v === 'failed' || v === 'interrupted' || v === 'queued' ? v : 'running'
56
+
57
+ const compact = (text: string, max = PREVIEW_MAX) => {
58
+ const line = text.replace(/\s+/g, ' ').trim()
59
+
60
+ if (!line) {
61
+ return ''
62
+ }
63
+
64
+ return line.length > max ? `${line.slice(0, max - 1)}…` : line
65
+ }
66
+
67
+ const toolLabel = (name: string) =>
68
+ name
69
+ .split('_')
70
+ .filter(Boolean)
71
+ .map(p => p[0]!.toUpperCase() + p.slice(1))
72
+ .join(' ') || name
73
+
74
+ const formatTool = (name: string, preview = '') => {
75
+ const snippet = compact(preview, TOOL_PREVIEW_MAX)
76
+
77
+ return snippet ? `${toolLabel(name)}("${snippet}")` : toolLabel(name)
78
+ }
79
+
80
+ interface TailEntry {
81
+ isError?: boolean
82
+ preview?: string
83
+ tool?: string
84
+ }
85
+
86
+ const asTail = (v: unknown): TailEntry[] =>
87
+ Array.isArray(v)
88
+ ? v
89
+ .filter((item): item is Record<string, unknown> => !!item && typeof item === 'object')
90
+ .map(item => ({
91
+ isError: item.is_error === true,
92
+ preview: str(item.preview) || undefined,
93
+ tool: str(item.tool) || undefined
94
+ }))
95
+ : []
96
+
97
+ const idOf = (p: SubagentPayload) =>
98
+ str(p.subagent_id) || `${str(p.parent_id) || 'root'}:${num(p.task_index) ?? 0}:${str(p.goal)}`
99
+
100
+ const appendStream = (stream: SubagentStreamEntry[], entry: SubagentStreamEntry) => {
101
+ const last = stream.at(-1)
102
+
103
+ if (last?.kind === entry.kind && last.text === entry.text && last.isError === entry.isError) {
104
+ return stream
105
+ }
106
+
107
+ return [...stream, entry].slice(-MAX_STREAM)
108
+ }
109
+
110
+ function streamFromPayload(
111
+ payload: SubagentPayload,
112
+ status: SubagentStatus,
113
+ eventType: string,
114
+ at: number
115
+ ): SubagentStreamEntry[] {
116
+ const out: SubagentStreamEntry[] = []
117
+ const tool = str(payload.tool_name)
118
+ const preview = str(payload.tool_preview) || str(payload.text)
119
+ const text = compact(str(payload.text) || preview)
120
+
121
+ for (const tail of asTail(payload.output_tail)) {
122
+ const line = tail.tool ? formatTool(tail.tool, tail.preview ?? '') : compact(tail.preview ?? '')
123
+
124
+ if (line) {
125
+ out.push({ at, isError: tail.isError, kind: tail.tool ? 'tool' : 'progress', text: line })
126
+ }
127
+ }
128
+
129
+ if (tool) {
130
+ out.push({ at, isError: !!payload.error, kind: 'tool', text: formatTool(tool, preview) })
131
+ }
132
+
133
+ if (eventType === 'subagent.progress' && text) {
134
+ out.push({ at, isError: !!payload.error, kind: 'progress', text })
135
+ }
136
+
137
+ if (eventType === 'subagent.thinking' && text) {
138
+ out.push({ at, kind: 'thinking', text })
139
+ }
140
+
141
+ const summary = compact(str(payload.summary) || str(payload.text))
142
+
143
+ if (TERMINAL.has(status) && summary) {
144
+ out.push({ at, isError: status === 'failed', kind: 'summary', text: summary })
145
+ }
146
+
147
+ return out
148
+ }
149
+
150
+ function toProgress(payload: SubagentPayload, prev: SubagentProgress | undefined, eventType = ''): SubagentProgress {
151
+ const at = Date.now()
152
+ const status = asStatus(payload.status)
153
+ const tool = str(payload.tool_name)
154
+ const stream = streamFromPayload(payload, status, eventType, at).reduce(appendStream, prev?.stream ?? [])
155
+ const filesRead = strList(payload.files_read)
156
+ const filesWritten = strList(payload.files_written)
157
+
158
+ return {
159
+ id: prev?.id ?? idOf(payload),
160
+ parentId: str(payload.parent_id) || prev?.parentId || null,
161
+ goal: str(payload.goal) || prev?.goal || 'Subagent',
162
+ model: str(payload.model) || prev?.model,
163
+ status,
164
+ taskCount: num(payload.task_count) ?? prev?.taskCount ?? 1,
165
+ taskIndex: num(payload.task_index) ?? prev?.taskIndex ?? 0,
166
+ startedAt: prev?.startedAt ?? at,
167
+ updatedAt: at,
168
+ durationSeconds: num(payload.duration_seconds) ?? prev?.durationSeconds,
169
+ costUsd: num(payload.cost_usd) ?? prev?.costUsd,
170
+ inputTokens: num(payload.input_tokens) ?? prev?.inputTokens,
171
+ outputTokens: num(payload.output_tokens) ?? prev?.outputTokens,
172
+ toolCount: num(payload.tool_count) ?? prev?.toolCount,
173
+ filesRead: filesRead.length ? filesRead : (prev?.filesRead ?? []),
174
+ filesWritten: filesWritten.length ? filesWritten : (prev?.filesWritten ?? []),
175
+ stream,
176
+ summary: str(payload.summary) || prev?.summary,
177
+ currentTool: TERMINAL.has(status) ? undefined : tool || prev?.currentTool
178
+ }
179
+ }
180
+
181
+ export function clearSessionSubagents(sid: string) {
182
+ const map = $subagentsBySession.get()
183
+
184
+ if (!(sid in map)) {
185
+ return
186
+ }
187
+
188
+ const { [sid]: _drop, ...rest } = map
189
+ $subagentsBySession.set(rest)
190
+ }
191
+
192
+ export function pruneDelegateFallbackSubagents(sid: string) {
193
+ const map = $subagentsBySession.get()
194
+ const list = map[sid]
195
+
196
+ if (!list?.length) {
197
+ return
198
+ }
199
+
200
+ const next = list.filter(item => !item.id.startsWith('delegate-tool:'))
201
+
202
+ if (next.length === list.length) {
203
+ return
204
+ }
205
+
206
+ $subagentsBySession.set({ ...map, [sid]: next })
207
+ }
208
+
209
+ export function upsertSubagent(sid: string, payload: SubagentPayload, createIfMissing = true, eventType?: string) {
210
+ const map = $subagentsBySession.get()
211
+ const list = map[sid] ?? []
212
+ const id = idOf(payload)
213
+ const idx = list.findIndex(item => item.id === id)
214
+
215
+ if (idx < 0 && !createIfMissing) {
216
+ return
217
+ }
218
+
219
+ const prev = idx >= 0 ? list[idx] : undefined
220
+
221
+ if (prev && TERMINAL.has(prev.status)) {
222
+ return
223
+ }
224
+
225
+ const next = toProgress(payload, prev, eventType)
226
+ const nextList = idx >= 0 ? list.map(item => (item.id === id ? next : item)) : [...list, next]
227
+
228
+ $subagentsBySession.set({ ...map, [sid]: nextList })
229
+ }
230
+
231
+ export function buildSubagentTree(items: readonly SubagentProgress[]): SubagentNode[] {
232
+ const nodes = new Map<string, SubagentNode>()
233
+
234
+ for (const item of items) {
235
+ nodes.set(item.id, { ...item, children: [] })
236
+ }
237
+
238
+ const roots: SubagentNode[] = []
239
+
240
+ for (const node of nodes.values()) {
241
+ const parent = node.parentId ? nodes.get(node.parentId) : null
242
+
243
+ if (parent) {
244
+ parent.children.push(node)
245
+ } else {
246
+ roots.push(node)
247
+ }
248
+ }
249
+
250
+ const sort = (a: SubagentNode, b: SubagentNode) =>
251
+ a.startedAt - b.startedAt || a.taskIndex - b.taskIndex || a.goal.localeCompare(b.goal)
252
+
253
+ const walk = (node: SubagentNode) => node.children.sort(sort).forEach(walk)
254
+ roots.sort(sort).forEach(walk)
255
+
256
+ return roots
257
+ }
258
+
259
+ export const activeSubagentCount = (items: readonly SubagentProgress[]) =>
260
+ items.filter(item => item.status === 'queued' || item.status === 'running').length
@@ -0,0 +1,46 @@
1
+ import { atom, type WritableAtom } from 'nanostores'
2
+
3
+ export const $threadScrolledUp = atom(false)
4
+ export const $threadJumpButtonVisible = atom(false)
5
+
6
+ const setter = (target: WritableAtom<boolean>) => (value: boolean) => {
7
+ if (target.get() !== value) {
8
+ target.set(value)
9
+ }
10
+ }
11
+
12
+ const setScrolledUp = setter($threadScrolledUp)
13
+ const setJumpButtonVisible = setter($threadJumpButtonVisible)
14
+
15
+ export const setThreadAtBottom = (isAtBottom: boolean) => {
16
+ setScrolledUp(!isAtBottom)
17
+ setJumpButtonVisible(!isAtBottom)
18
+ }
19
+
20
+ export const resetThreadScroll = () => setThreadAtBottom(true)
21
+
22
+ const handlers = new Set<() => void>()
23
+
24
+ export const onScrollToBottomRequest = (handler: () => void) => {
25
+ handlers.add(handler)
26
+ return () => void handlers.delete(handler)
27
+ }
28
+
29
+ export const requestScrollToBottom = () => handlers.forEach(handler => handler())
30
+
31
+ const editOpenHandlers = new Set<() => void>()
32
+ const editCloseHandlers = new Set<() => void>()
33
+
34
+ export const onThreadEditOpen = (handler: () => void) => {
35
+ editOpenHandlers.add(handler)
36
+ return () => void editOpenHandlers.delete(handler)
37
+ }
38
+
39
+ export const notifyThreadEditOpen = () => editOpenHandlers.forEach(handler => handler())
40
+
41
+ export const onThreadEditClose = (handler: () => void) => {
42
+ editCloseHandlers.add(handler)
43
+ return () => void editCloseHandlers.delete(handler)
44
+ }
45
+
46
+ export const notifyThreadEditClose = () => editCloseHandlers.forEach(handler => handler())
@@ -0,0 +1,47 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ import type { TodoItem } from '@/lib/todos'
4
+
5
+ import { $todosBySession, clearSessionTodos, setSessionTodos } from './todos'
6
+
7
+ const todo = (id: string, status: TodoItem['status']): TodoItem => ({ content: `task ${id}`, id, status })
8
+
9
+ describe('setSessionTodos finished-list auto-clear', () => {
10
+ beforeEach(() => {
11
+ vi.useFakeTimers()
12
+ })
13
+
14
+ afterEach(() => {
15
+ clearSessionTodos('s1')
16
+ vi.useRealTimers()
17
+ })
18
+
19
+ it('keeps an in-flight list indefinitely', () => {
20
+ setSessionTodos('s1', [todo('a', 'completed'), todo('b', 'in_progress')])
21
+
22
+ vi.advanceTimersByTime(60_000)
23
+
24
+ expect($todosBySession.get().s1).toHaveLength(2)
25
+ })
26
+
27
+ it('drops the list shortly after every item completes', () => {
28
+ setSessionTodos('s1', [todo('a', 'completed'), todo('b', 'cancelled')])
29
+
30
+ expect($todosBySession.get().s1).toHaveLength(2)
31
+
32
+ vi.advanceTimersByTime(5_000)
33
+
34
+ expect($todosBySession.get().s1).toBeUndefined()
35
+ })
36
+
37
+ it('cancels the pending clear when a new active list arrives', () => {
38
+ setSessionTodos('s1', [todo('a', 'completed')])
39
+ vi.advanceTimersByTime(2_000)
40
+
41
+ // The next turn starts a fresh plan before the linger expires.
42
+ setSessionTodos('s1', [todo('a', 'completed'), todo('b', 'pending')])
43
+ vi.advanceTimersByTime(60_000)
44
+
45
+ expect($todosBySession.get().s1).toHaveLength(2)
46
+ })
47
+ })
@@ -0,0 +1,64 @@
1
+ import { atom } from 'nanostores'
2
+
3
+ import type { TodoItem } from '@/lib/todos'
4
+
5
+ /**
6
+ * Live todo list per runtime session, rendered by the composer status stack
7
+ * (the inline transcript panel is gone). Fed from two places:
8
+ *
9
+ * - live `todo` tool events (use-message-stream)
10
+ * - stored-session hydration (desktop-controller) — but only when the list is
11
+ * still in flight, so reopening an old chat doesn't pin its finished plan
12
+ * above the composer forever.
13
+ */
14
+ export const $todosBySession = atom<Record<string, TodoItem[]>>({})
15
+
16
+ export const todoListActive = (todos: readonly TodoItem[]) =>
17
+ todos.some(t => t.status === 'pending' || t.status === 'in_progress')
18
+
19
+ // Once a list finishes (every item completed/cancelled), the final state
20
+ // lingers just long enough to see the last checkmark land, then the group
21
+ // drops out of the stack on its own.
22
+ const FINISHED_LINGER_MS = 4_000
23
+ const clearTimers = new Map<string, ReturnType<typeof setTimeout>>()
24
+
25
+ function cancelScheduledClear(sid: string) {
26
+ const timer = clearTimers.get(sid)
27
+
28
+ if (timer !== undefined) {
29
+ clearTimeout(timer)
30
+ clearTimers.delete(sid)
31
+ }
32
+ }
33
+
34
+ export function setSessionTodos(sid: string, todos: TodoItem[]) {
35
+ if (!sid) {
36
+ return
37
+ }
38
+
39
+ cancelScheduledClear(sid)
40
+ $todosBySession.set({ ...$todosBySession.get(), [sid]: todos })
41
+
42
+ if (!todoListActive(todos)) {
43
+ clearTimers.set(
44
+ sid,
45
+ setTimeout(() => {
46
+ clearTimers.delete(sid)
47
+ clearSessionTodos(sid)
48
+ }, FINISHED_LINGER_MS)
49
+ )
50
+ }
51
+ }
52
+
53
+ export function clearSessionTodos(sid: string) {
54
+ cancelScheduledClear(sid)
55
+
56
+ const map = $todosBySession.get()
57
+
58
+ if (!(sid in map)) {
59
+ return
60
+ }
61
+
62
+ const { [sid]: _drop, ...rest } = map
63
+ $todosBySession.set(rest)
64
+ }
@@ -0,0 +1,23 @@
1
+ import { atom } from 'nanostores'
2
+
3
+ const $toolDiffs = atom<Record<string, string>>({})
4
+
5
+ export function recordToolDiff(toolCallId: string, diff: string) {
6
+ if (!toolCallId || !diff) {
7
+ return
8
+ }
9
+
10
+ const current = $toolDiffs.get()
11
+
12
+ if (current[toolCallId] === diff) {
13
+ return
14
+ }
15
+
16
+ $toolDiffs.set({ ...current, [toolCallId]: diff })
17
+ }
18
+
19
+ export function getToolDiff(toolCallId: string): string {
20
+ return toolCallId ? $toolDiffs.get()[toolCallId] || '' : ''
21
+ }
22
+
23
+ export const $toolInlineDiffs = $toolDiffs
@@ -0,0 +1,45 @@
1
+ import { atom, computed, type ReadableAtom } from 'nanostores'
2
+
3
+ type DismissedToolRows = Record<string, true>
4
+
5
+ // Tool rows the user has locally hidden via a row's dismiss control. This is a
6
+ // *view-only* hide: the underlying tool call still lives in the stored chat
7
+ // history, but once a turn has settled the user can clear a completed/failed
8
+ // row out of the way so it stops sitting at the tail of the conversation.
9
+ //
10
+ // Kept in module memory (not localStorage, unlike $toolDisclosureStates) on
11
+ // purpose: the thread is virtualized, so a dismissed row's component unmounts
12
+ // and remounts as it scrolls — component-local state would forget the dismissal
13
+ // and the row would pop back. Storing it here survives those remounts for the
14
+ // life of the app session, while a reload restores every row in place rather
15
+ // than permanently rewriting history from a stray click.
16
+ export const $dismissedToolRows = atom<DismissedToolRows>({})
17
+
18
+ const dismissedCache = new Map<string, ReadableAtom<boolean>>()
19
+
20
+ export function $toolRowDismissed(id: string): ReadableAtom<boolean> {
21
+ let cached = dismissedCache.get(id)
22
+
23
+ if (!cached) {
24
+ cached = computed($dismissedToolRows, rows => Boolean(rows[id]))
25
+ dismissedCache.set(id, cached)
26
+ }
27
+
28
+ return cached
29
+ }
30
+
31
+ export function dismissToolRow(id: string) {
32
+ if (!id || $dismissedToolRows.get()[id]) {
33
+ return
34
+ }
35
+
36
+ $dismissedToolRows.set({ ...$dismissedToolRows.get(), [id]: true })
37
+ }
38
+
39
+ export function clearDismissedToolRows() {
40
+ if (Object.keys($dismissedToolRows.get()).length === 0) {
41
+ return
42
+ }
43
+
44
+ $dismissedToolRows.set({})
45
+ }
@@ -0,0 +1,91 @@
1
+ import { atom, computed, type ReadableAtom } from 'nanostores'
2
+
3
+ import { persistBoolean, storedBoolean } from '@/lib/storage'
4
+
5
+ export type ToolViewMode = 'product' | 'technical'
6
+
7
+ type ToolDisclosureStates = Record<string, boolean>
8
+
9
+ const TOOL_VIEW_TECHNICAL_STORAGE_KEY = 'NASTECH.desktop.toolView.technical'
10
+ const TOOL_DISCLOSURE_STORAGE_KEY = 'NASTECH.desktop.toolDisclosure.v1'
11
+ const MAX_DISCLOSURE_STATES = 240
12
+
13
+ export const $toolViewMode = atom<ToolViewMode>(
14
+ storedBoolean(TOOL_VIEW_TECHNICAL_STORAGE_KEY, false) ? 'technical' : 'product'
15
+ )
16
+ export const $toolDisclosureStates = atom<ToolDisclosureStates>(loadToolDisclosureStates())
17
+ const disclosureOpenCache = new Map<string, ReadableAtom<boolean | undefined>>()
18
+
19
+ $toolViewMode.subscribe(mode => persistBoolean(TOOL_VIEW_TECHNICAL_STORAGE_KEY, mode === 'technical'))
20
+ $toolDisclosureStates.subscribe(persistToolDisclosureStates)
21
+
22
+ export function setToolViewMode(mode: ToolViewMode) {
23
+ $toolViewMode.set(mode)
24
+ }
25
+
26
+ export function $toolDisclosureOpen(id: string): ReadableAtom<boolean | undefined> {
27
+ let cached = disclosureOpenCache.get(id)
28
+
29
+ if (!cached) {
30
+ cached = computed($toolDisclosureStates, states => states[id])
31
+ disclosureOpenCache.set(id, cached)
32
+ }
33
+
34
+ return cached
35
+ }
36
+
37
+ function loadToolDisclosureStates(): ToolDisclosureStates {
38
+ if (typeof window === 'undefined') {
39
+ return {}
40
+ }
41
+
42
+ try {
43
+ const raw = window.localStorage.getItem(TOOL_DISCLOSURE_STORAGE_KEY)
44
+
45
+ if (!raw) {
46
+ return {}
47
+ }
48
+
49
+ const parsed = JSON.parse(raw) as unknown
50
+
51
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
52
+ return {}
53
+ }
54
+
55
+ return Object.fromEntries(
56
+ Object.entries(parsed as Record<string, unknown>)
57
+ .filter((entry): entry is [string, boolean] => typeof entry[0] === 'string' && typeof entry[1] === 'boolean')
58
+ .slice(-MAX_DISCLOSURE_STATES)
59
+ )
60
+ } catch {
61
+ return {}
62
+ }
63
+ }
64
+
65
+ function persistToolDisclosureStates(states: ToolDisclosureStates) {
66
+ if (typeof window === 'undefined') {
67
+ return
68
+ }
69
+
70
+ try {
71
+ const entries = Object.entries(states).slice(-MAX_DISCLOSURE_STATES)
72
+
73
+ window.localStorage.setItem(TOOL_DISCLOSURE_STORAGE_KEY, JSON.stringify(Object.fromEntries(entries)))
74
+ } catch {
75
+ // Tool disclosure is a local UI preference; ignore storage failures.
76
+ }
77
+ }
78
+
79
+ export function setToolDisclosureOpen(id: string, open: boolean) {
80
+ if (!id) {
81
+ return
82
+ }
83
+
84
+ const current = $toolDisclosureStates.get()
85
+
86
+ if (current[id] === open) {
87
+ return
88
+ }
89
+
90
+ $toolDisclosureStates.set({ ...current, [id]: open })
91
+ }