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,149 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import type { NasTechWorktreeInfo } from '@/global'
4
+ import type { SessionInfo } from '@/types/nastech'
5
+
6
+ import { uniqueCwds, workspaceGroupsFor, workspaceTreeFor, type WorktreeResolver } from './workspace-groups'
7
+
8
+ let nextId = 0
9
+
10
+ function makeSession(cwd: null | string, overrides: Partial<SessionInfo> = {}): SessionInfo {
11
+ return {
12
+ archived: false,
13
+ cwd,
14
+ ended_at: null,
15
+ id: `s${nextId++}`,
16
+ input_tokens: 0,
17
+ is_active: false,
18
+ last_active: 1_000,
19
+ message_count: 1,
20
+ model: 'claude',
21
+ output_tokens: 0,
22
+ preview: null,
23
+ source: 'cli',
24
+ started_at: 1_000,
25
+ title: null,
26
+ tool_call_count: 0,
27
+ ...overrides
28
+ }
29
+ }
30
+
31
+ const labels = (sessions: SessionInfo[]) => workspaceGroupsFor(sessions, 'No workspace').map(g => g.label)
32
+
33
+ describe('workspaceGroupsFor', () => {
34
+ it('groups by full cwd, not by basename — same-named folders are separate groups', () => {
35
+ const groups = workspaceGroupsFor(
36
+ [makeSession('/a/nastech-agent/apps/desktop'), makeSession('/a/nastech-agent-wt-rtl/apps/desktop')],
37
+ 'No workspace'
38
+ )
39
+
40
+ expect(groups).toHaveLength(2)
41
+ })
42
+
43
+ it('disambiguates colliding basenames by walking up the path', () => {
44
+ expect(
45
+ labels([makeSession('/a/nastech-agent/apps/desktop'), makeSession('/a/nastech-agent-wt-rtl/apps/desktop')])
46
+ ).toEqual(['nastech-agent/apps/desktop', 'nastech-agent-wt-rtl/apps/desktop'])
47
+ })
48
+
49
+ it('leaves a unique basename as its short label', () => {
50
+ expect(labels([makeSession('/a/nastech-agent/apps/desktop'), makeSession('/b/heval-py')])).toEqual([
51
+ 'desktop',
52
+ 'heval-py'
53
+ ])
54
+ })
55
+
56
+ it('grows the prefix past one segment when the parent also collides', () => {
57
+ expect(labels([makeSession('/x/proj/apps/desktop'), makeSession('/y/proj/apps/desktop')])).toEqual([
58
+ 'x/proj/apps/desktop',
59
+ 'y/proj/apps/desktop'
60
+ ])
61
+ })
62
+
63
+ it('keeps the synthetic no-workspace group untouched even if a real group shares its label', () => {
64
+ const groups = workspaceGroupsFor([makeSession(null), makeSession('/a/No workspace')], 'No workspace')
65
+ const noWorkspace = groups.find(g => g.path === null)
66
+
67
+ expect(noWorkspace?.label).toBe('No workspace')
68
+ })
69
+ })
70
+
71
+ const info = (over: Partial<NasTechWorktreeInfo> & Pick<NasTechWorktreeInfo, 'repoRoot' | 'worktreeRoot'>): NasTechWorktreeInfo => ({
72
+ branch: null,
73
+ isMainWorktree: false,
74
+ ...over
75
+ })
76
+
77
+ describe('workspaceTreeFor', () => {
78
+ it('heuristic nests `<repo>-wt-<branch>` under its sibling repo', () => {
79
+ const tree = workspaceTreeFor(
80
+ [makeSession('/www/nastech-agent'), makeSession('/www/nastech-agent-wt-rtl')],
81
+ 'No workspace'
82
+ )
83
+
84
+ expect(tree).toHaveLength(1)
85
+ expect(tree[0].label).toBe('nastech-agent')
86
+ expect(tree[0].groups.map(g => g.label).sort()).toEqual(['nastech-agent', 'rtl'])
87
+ })
88
+
89
+ it('git metadata is authoritative — worktrees group by repoRoot regardless of directory naming', () => {
90
+ const resolver: WorktreeResolver = cwd => {
91
+ if (cwd === '/www/nastech-agent') {
92
+ return info({ repoRoot: '/www/nastech-agent', worktreeRoot: '/www/nastech-agent', isMainWorktree: true, branch: 'main' })
93
+ }
94
+
95
+ if (cwd === '/elsewhere/ha-rtl') {
96
+ return info({ repoRoot: '/www/nastech-agent', worktreeRoot: '/elsewhere/ha-rtl', branch: 'rtl' })
97
+ }
98
+
99
+ return null
100
+ }
101
+
102
+ const tree = workspaceTreeFor(
103
+ [makeSession('/www/nastech-agent'), makeSession('/elsewhere/ha-rtl')],
104
+ 'No workspace',
105
+ resolver
106
+ )
107
+
108
+ expect(tree).toHaveLength(1)
109
+ expect(tree[0].label).toBe('nastech-agent')
110
+ // The main checkout labels by directory (its branch is transient — using it
111
+ // would misattribute old sessions to the currently checked-out branch);
112
+ // linked worktrees label by branch.
113
+ expect(tree[0].groups.map(g => g.label)).toEqual(['nastech-agent', 'rtl'])
114
+ })
115
+
116
+ it('a standalone directory is its own parent (always parent → worktree → sessions)', () => {
117
+ const tree = workspaceTreeFor([makeSession('/www/heval-node')], 'No workspace')
118
+
119
+ expect(tree).toHaveLength(1)
120
+ expect(tree[0].label).toBe('heval-node')
121
+ expect(tree[0].groups).toHaveLength(1)
122
+ expect(tree[0].groups[0].label).toBe('heval-node')
123
+ })
124
+
125
+ it('aggregates session counts across a repo’s worktrees', () => {
126
+ const tree = workspaceTreeFor(
127
+ [makeSession('/www/ha'), makeSession('/www/ha-wt-x'), makeSession('/www/ha-wt-x')],
128
+ 'No workspace'
129
+ )
130
+
131
+ const parent = tree.find(p => p.label === 'ha')
132
+
133
+ expect(parent?.sessionCount).toBe(3)
134
+ })
135
+
136
+ it('no-workspace sessions form their own parent', () => {
137
+ const tree = workspaceTreeFor([makeSession(null)], 'No workspace')
138
+
139
+ expect(tree).toHaveLength(1)
140
+ expect(tree[0].label).toBe('No workspace')
141
+ expect(tree[0].path).toBeNull()
142
+ })
143
+ })
144
+
145
+ describe('uniqueCwds', () => {
146
+ it('dedupes and drops empty/whitespace cwds', () => {
147
+ expect(uniqueCwds([makeSession('/a'), makeSession('/a'), makeSession(null), makeSession(' ')])).toEqual(['/a'])
148
+ })
149
+ })
@@ -0,0 +1,326 @@
1
+ import type { NasTechWorktreeInfo } from '@/global'
2
+ import type { SessionInfo } from '@/nastech'
3
+
4
+ export interface SidebarSessionGroup {
5
+ id: string
6
+ label: string
7
+ path: null | string
8
+ sessions: SessionInfo[]
9
+ // Profile color for the ALL-profiles view; absent for workspace groups.
10
+ color?: null | string
11
+ loadingMore?: boolean
12
+ mode?: 'profile' | 'source' | 'workspace'
13
+ onLoadMore?: () => void
14
+ sourceId?: string
15
+ totalCount?: number
16
+ }
17
+
18
+ const NO_WORKSPACE_ID = '__no_workspace__'
19
+
20
+ /** Path split into segments, ignoring trailing slashes and mixed separators. */
21
+ const segments = (path: string): string[] => path.replace(/[/\\]+$/, '').split(/[/\\]/).filter(Boolean)
22
+
23
+ /** Last path segment. */
24
+ export const baseName = (path: string): string | undefined => segments(path).pop()
25
+
26
+ /** The segments above the basename. */
27
+ const parentSegments = (path: string): string[] => segments(path).slice(0, -1)
28
+
29
+ interface Labelable {
30
+ id: string
31
+ label: string
32
+ path: null | string
33
+ }
34
+
35
+ /**
36
+ * Disambiguate groups whose basename collides (worktrees all end in the same
37
+ * `apps/desktop`, sibling repos share a folder name, etc.) by walking up the
38
+ * path and prepending parent segments until each colliding label is unique —
39
+ * e.g. `nastech-agent/desktop` vs `nastech-agent-wt-rtl/desktop`. Groups with a
40
+ * unique basename keep their short label untouched.
41
+ */
42
+ function disambiguateLabels(groups: Labelable[]): void {
43
+ const byLabel = new Map<string, Labelable[]>()
44
+
45
+ for (const group of groups) {
46
+ const bucket = byLabel.get(group.label)
47
+
48
+ if (bucket) {
49
+ bucket.push(group)
50
+ } else {
51
+ byLabel.set(group.label, [group])
52
+ }
53
+ }
54
+
55
+ for (const bucket of byLabel.values()) {
56
+ if (bucket.length < 2) {
57
+ continue
58
+ }
59
+
60
+ // Only groups backed by a real path can grow a prefix; the synthetic
61
+ // "No workspace" group has no path and stays as-is.
62
+ const pathed = bucket.filter(group => group.path)
63
+
64
+ if (pathed.length < 2) {
65
+ continue
66
+ }
67
+
68
+ const parents = new Map(pathed.map(group => [group.id, parentSegments(group.path!)]))
69
+ let depth = 1
70
+
71
+ // Grow the prefix one parent segment at a time until every label in the
72
+ // bucket is distinct, or we run out of parent segments to add.
73
+ while (depth <= Math.max(...pathed.map(g => parents.get(g.id)!.length))) {
74
+ const labels = new Map<string, number>()
75
+
76
+ for (const group of pathed) {
77
+ const segs = parents.get(group.id)!
78
+ const prefix = segs.slice(-depth).join('/')
79
+ const base = baseName(group.path!) ?? group.path!
80
+ group.label = prefix ? `${prefix}/${base}` : base
81
+ labels.set(group.label, (labels.get(group.label) ?? 0) + 1)
82
+ }
83
+
84
+ if ([...labels.values()].every(count => count === 1)) {
85
+ break
86
+ }
87
+
88
+ depth += 1
89
+ }
90
+ }
91
+ }
92
+
93
+ export function workspaceGroupsFor(
94
+ sessions: SessionInfo[],
95
+ noWorkspaceLabel: string,
96
+ options: { preserveSessionOrder?: boolean } = {}
97
+ ): SidebarSessionGroup[] {
98
+ const groups = new Map<string, SidebarSessionGroup>()
99
+
100
+ for (const session of sessions) {
101
+ const path = session.cwd?.trim() || ''
102
+ const id = path || NO_WORKSPACE_ID
103
+ const label = baseName(path) || path || noWorkspaceLabel
104
+
105
+ const group = groups.get(id) ?? { id, label, path: path || null, sessions: [] }
106
+ group.sessions.push(session)
107
+ groups.set(id, group)
108
+ }
109
+
110
+ if (!options.preserveSessionOrder) {
111
+ // Groups keep recency order (Map insertion = first-seen in the recency-sorted
112
+ // input, so an active project floats up), but rows *within* a group sort by
113
+ // creation time so they don't reshuffle every time a message lands — keeps
114
+ // muscle memory intact.
115
+ for (const group of groups.values()) {
116
+ group.sessions.sort((a, b) => b.started_at - a.started_at)
117
+ }
118
+ }
119
+
120
+ const result = [...groups.values()]
121
+ disambiguateLabels(result)
122
+
123
+ return result
124
+ }
125
+
126
+ /**
127
+ * A worktree's main repo and all its linked worktrees collapse into ONE parent
128
+ * (keyed by the repo root); each worktree is a child group; sessions hang off
129
+ * the worktree they ran in. `parent → worktree → sessions`.
130
+ */
131
+ export interface SidebarWorkspaceTree {
132
+ id: string
133
+ label: string
134
+ path: null | string
135
+ groups: SidebarSessionGroup[]
136
+ sessionCount: number
137
+ }
138
+
139
+ /** Resolves a session cwd to git-worktree identity (from the local fs probe). */
140
+ export type WorktreeResolver = (cwd: string) => NasTechWorktreeInfo | null | undefined
141
+
142
+ interface WorkspacePlacement {
143
+ parentKey: string
144
+ parentLabel: string
145
+ parentPath: string
146
+ worktreeKey: string
147
+ worktreeLabel: string
148
+ worktreePath: string
149
+ }
150
+
151
+ /** Replace a path's final segment, preserving its prefix + separators. */
152
+ const withBaseName = (path: string, name: string): string =>
153
+ path.replace(/[/\\]+$/, '').replace(/[^/\\]+$/, name)
154
+
155
+ /**
156
+ * Path-only fallback for when git metadata is unavailable (remote backends,
157
+ * unreadable paths). Mirrors the git layout: a `<repo>-wt-<branch>` directory
158
+ * nests under its sibling `<repo>`; any other directory is its own repo root.
159
+ */
160
+ function placeByHeuristic(path: string): WorkspacePlacement | null {
161
+ const base = baseName(path)
162
+
163
+ if (!base) {
164
+ return null
165
+ }
166
+
167
+ const worktreeMatch = base.match(/^(.+)-wt-(.+)$/)
168
+
169
+ if (worktreeMatch) {
170
+ const repo = worktreeMatch[1]
171
+ const repoPath = withBaseName(path, repo)
172
+
173
+ return {
174
+ parentKey: repoPath,
175
+ parentLabel: repo,
176
+ parentPath: repoPath,
177
+ worktreeKey: path,
178
+ worktreeLabel: worktreeMatch[2],
179
+ worktreePath: path
180
+ }
181
+ }
182
+
183
+ return {
184
+ parentKey: path,
185
+ parentLabel: base,
186
+ parentPath: path,
187
+ worktreeKey: path,
188
+ worktreeLabel: base,
189
+ worktreePath: path
190
+ }
191
+ }
192
+
193
+ function placeWorkspace(path: string, resolver?: WorktreeResolver): WorkspacePlacement | null {
194
+ const info = resolver?.(path)
195
+
196
+ if (info?.repoRoot && info.worktreeRoot) {
197
+ const dirLabel = baseName(info.worktreeRoot) || info.worktreeRoot
198
+
199
+ return {
200
+ parentKey: info.repoRoot,
201
+ parentLabel: baseName(info.repoRoot) ?? info.repoRoot,
202
+ parentPath: info.repoRoot,
203
+ worktreeKey: info.worktreeRoot,
204
+ // The main checkout's branch is transient — it changes as you work, so a
205
+ // branch label would misattribute every past session to whatever branch
206
+ // is checked out *now*. Label it by directory. Linked worktrees are
207
+ // per-branch by construction, so branch is the clearest label there.
208
+ worktreeLabel: info.isMainWorktree ? dirLabel : info.branch || dirLabel,
209
+ worktreePath: info.worktreeRoot
210
+ }
211
+ }
212
+
213
+ return placeByHeuristic(path)
214
+ }
215
+
216
+ /** Unique, non-empty session cwds — the batch to probe for worktree info. */
217
+ export function uniqueCwds(sessions: SessionInfo[]): string[] {
218
+ const seen = new Set<string>()
219
+
220
+ for (const session of sessions) {
221
+ const path = session.cwd?.trim()
222
+
223
+ if (path) {
224
+ seen.add(path)
225
+ }
226
+ }
227
+
228
+ return [...seen]
229
+ }
230
+
231
+ /**
232
+ * Build the `parent → worktree → sessions` tree. Parents keep recency order
233
+ * (first-seen in the recency-sorted input); worktree groups within a parent do
234
+ * too, while rows inside a worktree sort by creation time (stable muscle memory,
235
+ * matching `workspaceGroupsFor`).
236
+ */
237
+ export function workspaceTreeFor(
238
+ sessions: SessionInfo[],
239
+ noWorkspaceLabel: string,
240
+ resolver?: WorktreeResolver,
241
+ options: { preserveSessionOrder?: boolean } = {}
242
+ ): SidebarWorkspaceTree[] {
243
+ interface WorktreeEntry {
244
+ group: SidebarSessionGroup
245
+ parentKey: string
246
+ parentLabel: string
247
+ parentPath: string
248
+ }
249
+
250
+ const worktrees = new Map<string, WorktreeEntry>()
251
+ const noWorkspace: SessionInfo[] = []
252
+
253
+ for (const session of sessions) {
254
+ const path = session.cwd?.trim() || ''
255
+
256
+ if (!path) {
257
+ noWorkspace.push(session)
258
+
259
+ continue
260
+ }
261
+
262
+ const placement = placeWorkspace(path, resolver)
263
+
264
+ if (!placement) {
265
+ noWorkspace.push(session)
266
+
267
+ continue
268
+ }
269
+
270
+ let entry = worktrees.get(placement.worktreeKey)
271
+
272
+ if (!entry) {
273
+ entry = {
274
+ group: { id: placement.worktreeKey, label: placement.worktreeLabel, path: placement.worktreePath, sessions: [] },
275
+ parentKey: placement.parentKey,
276
+ parentLabel: placement.parentLabel,
277
+ parentPath: placement.parentPath
278
+ }
279
+ worktrees.set(placement.worktreeKey, entry)
280
+ }
281
+
282
+ entry.group.sessions.push(session)
283
+ }
284
+
285
+ if (!options.preserveSessionOrder) {
286
+ for (const entry of worktrees.values()) {
287
+ entry.group.sessions.sort((a, b) => b.started_at - a.started_at)
288
+ }
289
+ }
290
+
291
+ const parents = new Map<string, SidebarWorkspaceTree>()
292
+
293
+ for (const entry of worktrees.values()) {
294
+ let parent = parents.get(entry.parentKey)
295
+
296
+ if (!parent) {
297
+ parent = { id: entry.parentKey, label: entry.parentLabel, path: entry.parentPath, groups: [], sessionCount: 0 }
298
+ parents.set(entry.parentKey, parent)
299
+ }
300
+
301
+ parent.groups.push(entry.group)
302
+ parent.sessionCount += entry.group.sessions.length
303
+ }
304
+
305
+ const result = [...parents.values()]
306
+
307
+ if (noWorkspace.length) {
308
+ result.push({
309
+ id: NO_WORKSPACE_ID,
310
+ label: noWorkspaceLabel,
311
+ path: null,
312
+ groups: [{ id: NO_WORKSPACE_ID, label: noWorkspaceLabel, path: null, sessions: noWorkspace }],
313
+ sessionCount: noWorkspace.length
314
+ })
315
+ }
316
+
317
+ // Parents that collide on basename grow a path prefix; worktree labels that
318
+ // collide inside a parent do the same.
319
+ disambiguateLabels(result)
320
+
321
+ for (const parent of result) {
322
+ disambiguateLabels(parent.groups)
323
+ }
324
+
325
+ return result
326
+ }
@@ -0,0 +1,34 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import type { ChatMessage } from '@/lib/chat-messages'
4
+
5
+ import { lastVisibleMessageIsUser, threadLoadingState } from './thread-loading'
6
+
7
+ function message(id: string, role: ChatMessage['role'], hidden = false): ChatMessage {
8
+ return {
9
+ id,
10
+ role,
11
+ parts: [{ type: 'text', text: `${role}:${id}` }],
12
+ hidden
13
+ }
14
+ }
15
+
16
+ describe('thread loading state', () => {
17
+ it('returns session when routed session is still hydrating', () => {
18
+ expect(threadLoadingState(true, true, true, false)).toBe('session')
19
+ })
20
+
21
+ it('returns response while awaiting an assistant reply to the last visible user message', () => {
22
+ const messages = [message('u1', 'user'), message('a1', 'assistant', true)]
23
+
24
+ expect(lastVisibleMessageIsUser(messages)).toBe(true)
25
+ expect(threadLoadingState(false, true, true, lastVisibleMessageIsUser(messages))).toBe('response')
26
+ })
27
+
28
+ it('does not show response loading when the last visible message is not user-authored', () => {
29
+ const messages = [message('u1', 'user'), message('a1', 'assistant')]
30
+
31
+ expect(lastVisibleMessageIsUser(messages)).toBe(false)
32
+ expect(threadLoadingState(false, true, true, lastVisibleMessageIsUser(messages))).toBeUndefined()
33
+ })
34
+ })
@@ -0,0 +1,26 @@
1
+ import type { ChatMessage } from '@/lib/chat-messages'
2
+
3
+ export type ThreadLoadingState = 'response' | 'session'
4
+
5
+ export function lastVisibleMessageIsUser(messages: ChatMessage[]): boolean {
6
+ const lastVisible = [...messages].reverse().find(message => !message.hidden)
7
+
8
+ return lastVisible?.role === 'user'
9
+ }
10
+
11
+ export function threadLoadingState(
12
+ loadingSession: boolean,
13
+ busy: boolean,
14
+ awaitingResponse: boolean,
15
+ lastVisibleIsUser: boolean
16
+ ): ThreadLoadingState | undefined {
17
+ if (loadingSession) {
18
+ return 'session'
19
+ }
20
+
21
+ if (busy && awaitingResponse && lastVisibleIsUser) {
22
+ return 'response'
23
+ }
24
+
25
+ return undefined
26
+ }