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,30 @@
1
+ import { Codicon } from '@/components/ui/codicon'
2
+ import { useI18n } from '@/i18n'
3
+
4
+ interface SidebarLoadMoreRowProps {
5
+ step: number
6
+ onClick: () => void
7
+ loading?: boolean
8
+ }
9
+
10
+ // "Load N more" affordance shared by the recents, messaging, and cron sections.
11
+ // The chevron sits in the same w-3.5 column the rows use for their dot, so it
12
+ // lines up with the list above.
13
+ export function SidebarLoadMoreRow({ step, onClick, loading = false }: SidebarLoadMoreRowProps) {
14
+ const { t } = useI18n()
15
+ const label = loading ? t.sidebar.loading : step > 0 ? t.sidebar.loadCount(step) : t.sidebar.loadMore
16
+
17
+ return (
18
+ <button
19
+ className="flex min-h-5 items-center gap-1.5 self-start bg-transparent pl-2 text-left text-[0.6875rem] text-(--ui-text-tertiary) transition-colors duration-100 ease-out hover:text-foreground hover:transition-none disabled:cursor-default disabled:opacity-60 disabled:hover:text-(--ui-text-tertiary)"
20
+ disabled={loading}
21
+ onClick={onClick}
22
+ type="button"
23
+ >
24
+ <span className="grid w-3.5 shrink-0 place-items-center">
25
+ <Codicon className="opacity-70" name={loading ? 'loading' : 'chevron-down'} size="0.75rem" spinning={loading} />
26
+ </span>
27
+ <span>{label}</span>
28
+ </button>
29
+ )
30
+ }
@@ -0,0 +1,21 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { resolveManualSessionOrderIds } from './order'
4
+
5
+ describe('resolveManualSessionOrderIds', () => {
6
+ it('clears legacy auto-seeded order until the user manually reorders sessions', () => {
7
+ expect(resolveManualSessionOrderIds(['newest', 'older'], ['older', 'newest'], false)).toEqual([])
8
+ })
9
+
10
+ it('keeps a manual order and surfaces newly seen sessions first', () => {
11
+ expect(resolveManualSessionOrderIds(['newest', 'older', 'oldest'], ['oldest', 'older'], true)).toEqual([
12
+ 'newest',
13
+ 'oldest',
14
+ 'older'
15
+ ])
16
+ })
17
+
18
+ it('clears manual order when none of the saved ids still exist', () => {
19
+ expect(resolveManualSessionOrderIds(['newest'], ['gone'], true)).toEqual([])
20
+ })
21
+ })
@@ -0,0 +1,17 @@
1
+ export function resolveManualSessionOrderIds(currentIds: string[], orderIds: string[], manual: boolean): string[] {
2
+ if (!manual || !currentIds.length || !orderIds.length) {
3
+ return []
4
+ }
5
+
6
+ const current = new Set(currentIds)
7
+ const retained = orderIds.filter(id => current.has(id))
8
+
9
+ if (!retained.length) {
10
+ return []
11
+ }
12
+
13
+ const retainedSet = new Set(retained)
14
+ const fresh = currentIds.filter(id => !retainedSet.has(id))
15
+
16
+ return [...fresh, ...retained]
17
+ }
@@ -0,0 +1,516 @@
1
+ import {
2
+ closestCenter,
3
+ DndContext,
4
+ type DragEndEvent,
5
+ type DragOverEvent,
6
+ type DragStartEvent,
7
+ KeyboardSensor,
8
+ type Modifier,
9
+ PointerSensor,
10
+ useSensor,
11
+ useSensors
12
+ } from '@dnd-kit/core'
13
+ import {
14
+ arrayMove,
15
+ horizontalListSortingStrategy,
16
+ SortableContext,
17
+ sortableKeyboardCoordinates,
18
+ useSortable
19
+ } from '@dnd-kit/sortable'
20
+ import { CSS } from '@dnd-kit/utilities'
21
+ import { useStore } from '@nanostores/react'
22
+ import { useEffect, useRef, useState } from 'react'
23
+ import { useNavigate } from 'react-router-dom'
24
+
25
+ import { Button } from '@/components/ui/button'
26
+ import { Codicon } from '@/components/ui/codicon'
27
+ import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@/components/ui/context-menu'
28
+ import { Popover, PopoverAnchor, PopoverContent } from '@/components/ui/popover'
29
+ import { Tip, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
30
+ import { useI18n } from '@/i18n'
31
+ import { triggerHaptic } from '@/lib/haptics'
32
+ import { PROFILE_SWATCHES, profileColorSoft, resolveProfileColor } from '@/lib/profile-color'
33
+ import { cn } from '@/lib/utils'
34
+ import {
35
+ $activeGatewayProfile,
36
+ $profileColors,
37
+ $profileCreateRequest,
38
+ $profileOrder,
39
+ $profiles,
40
+ $profileScope,
41
+ ALL_PROFILES,
42
+ normalizeProfileKey,
43
+ refreshActiveProfile,
44
+ selectProfile,
45
+ setProfileColor,
46
+ setProfileOrder,
47
+ setShowAllProfiles,
48
+ sortByProfileOrder
49
+ } from '@/store/profile'
50
+ import type { ProfileInfo } from '@/types/nastech'
51
+
52
+ import { CreateProfileDialog } from '../../profiles/create-profile-dialog'
53
+ import { DeleteProfileDialog } from '../../profiles/delete-profile-dialog'
54
+ import { RenameProfileDialog } from '../../profiles/rename-profile-dialog'
55
+ import { PROFILES_ROUTE } from '../../routes'
56
+
57
+ const RAIL_GAP = 4 // px — matches gap-1 between squares.
58
+
59
+ // easeOutBack — a little overshoot so squares spring into their new slot rather
60
+ // than sliding in flat. Neighbors reflow on RAIL_TRANSITION; the dragged square
61
+ // glides between snapped cells on the snappier DRAG_TRANSITION.
62
+ const SPRING = 'cubic-bezier(0.34, 1.56, 0.64, 1)'
63
+ const RAIL_TRANSITION = { duration: 300, easing: SPRING }
64
+ const DRAG_TRANSITION = `transform 200ms ${SPRING}`
65
+
66
+ // The rail is a single horizontal strip of fixed cells. Pin drags to the x-axis
67
+ // (no cross-axis scrollbar), snap to whole cells so a square steps slot-to-slot
68
+ // instead of gliding, and clamp to the occupied strip so it can't float past the
69
+ // last profile onto the "+".
70
+ const stepThroughCells: Modifier = ({ containerNodeRect, draggingNodeRect, transform }) => {
71
+ if (!draggingNodeRect || !containerNodeRect) {
72
+ return { ...transform, y: 0 }
73
+ }
74
+
75
+ const pitch = draggingNodeRect.width + RAIL_GAP
76
+ const minX = containerNodeRect.left - draggingNodeRect.left
77
+ const maxX = containerNodeRect.right - draggingNodeRect.right
78
+ const snapped = Math.round(transform.x / pitch) * pitch
79
+
80
+ return { ...transform, x: Math.min(maxX, Math.max(minX, snapped)), y: 0 }
81
+ }
82
+
83
+ // Arc-Spaces-style profile rail at the sidebar foot: a default↔all toggle pinned
84
+ // left, the colored named profiles scrolling between, and Manage pinned right.
85
+ // The active profile pops in its own color — the "where am I" cue. Single-
86
+ // profile users see only the "+" (create their first profile); everything else
87
+ // appears once a second profile exists.
88
+ export function ProfileRail() {
89
+ const { t } = useI18n()
90
+ const p = t.profiles
91
+ const profiles = useStore($profiles)
92
+ const scope = useStore($profileScope)
93
+ const gatewayProfile = useStore($activeGatewayProfile)
94
+ const order = useStore($profileOrder)
95
+ const colors = useStore($profileColors)
96
+ const navigate = useNavigate()
97
+
98
+ const [createOpen, setCreateOpen] = useState(false)
99
+ const [pendingRename, setPendingRename] = useState<null | ProfileInfo>(null)
100
+ const [pendingDelete, setPendingDelete] = useState<null | ProfileInfo>(null)
101
+ const scrollRef = useRef<HTMLDivElement>(null)
102
+
103
+ // A plain mouse wheel only emits deltaY; map it to horizontal scroll so the
104
+ // rail is navigable without a trackpad. Trackpad x-scroll (deltaX) passes
105
+ // through. Native + non-passive so we can preventDefault and not bleed the
106
+ // gesture into the sessions list above.
107
+ useEffect(() => {
108
+ const el = scrollRef.current
109
+
110
+ if (!el) {
111
+ return
112
+ }
113
+
114
+ const onWheel = (event: WheelEvent) => {
115
+ if (el.scrollWidth <= el.clientWidth || Math.abs(event.deltaY) <= Math.abs(event.deltaX)) {
116
+ return
117
+ }
118
+
119
+ el.scrollLeft += event.deltaY
120
+ event.preventDefault()
121
+ }
122
+
123
+ el.addEventListener('wheel', onWheel, { passive: false })
124
+
125
+ return () => el.removeEventListener('wheel', onWheel)
126
+ }, [])
127
+
128
+ const isAll = scope === ALL_PROFILES
129
+ const activeKey = normalizeProfileKey(gatewayProfile)
130
+ const defaultProfile = profiles.find(profile => profile.is_default)
131
+ const onDefault = !isAll && activeKey === 'default'
132
+
133
+ const named = sortByProfileOrder(profiles.filter(profile => !profile.is_default), order)
134
+ const multiProfile = profiles.length > 1
135
+
136
+ // distance constraint: a small drag reorders, a tap still selects the profile.
137
+ const sensors = useSensors(
138
+ useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
139
+ useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
140
+ )
141
+
142
+ // Tick a haptic each time the drag crosses into a new cell, and a satisfying
143
+ // confirm on a committed reorder.
144
+ const lastOverRef = useRef<string | null>(null)
145
+
146
+ const handleDragStart = ({ active }: DragStartEvent) => {
147
+ lastOverRef.current = String(active.id)
148
+ }
149
+
150
+ const handleDragOver = ({ over }: DragOverEvent) => {
151
+ const id = over ? String(over.id) : null
152
+
153
+ if (id && id !== lastOverRef.current) {
154
+ lastOverRef.current = id
155
+ triggerHaptic('selection')
156
+ }
157
+ }
158
+
159
+ const handleDragEnd = ({ active, over }: DragEndEvent) => {
160
+ lastOverRef.current = null
161
+
162
+ if (!over || active.id === over.id) {
163
+ return
164
+ }
165
+
166
+ const ids = named.map(profile => profile.name)
167
+ const from = ids.indexOf(String(active.id))
168
+ const to = ids.indexOf(String(over.id))
169
+
170
+ if (from >= 0 && to >= 0) {
171
+ setProfileOrder(arrayMove(ids, from, to))
172
+ triggerHaptic('success')
173
+ }
174
+ }
175
+
176
+ // Re-pull the running profile + list on mount so a profile created elsewhere
177
+ // shows up; cheap and best-effort.
178
+ useEffect(() => {
179
+ void refreshActiveProfile()
180
+ }, [])
181
+
182
+ // Open the create dialog when the `profile.create` hotkey fires (the dialog
183
+ // state lives here, so the global keybind bumps a request atom we watch).
184
+ const createRequest = useStore($profileCreateRequest)
185
+ const lastCreateRef = useRef(createRequest)
186
+
187
+ useEffect(() => {
188
+ if (createRequest === lastCreateRef.current) {
189
+ return
190
+ }
191
+
192
+ lastCreateRef.current = createRequest
193
+ setCreateOpen(true)
194
+ }, [createRequest])
195
+
196
+ return (
197
+ <div aria-label="Profiles" className="flex items-center gap-0.5" role="tablist">
198
+ {/* One button toggles default ↔ all: home face when scoped to a profile,
199
+ layers face when showing everything. Pinned left like Manage is right.
200
+ Hidden until a second profile exists. */}
201
+ {multiProfile &&
202
+ (defaultProfile ? (
203
+ // On default → toggle to all. Anywhere else (all view or a named
204
+ // profile) → return to default. So leaving a profile never lands on all.
205
+ <ProfilePill
206
+ active={isAll || onDefault}
207
+ glyph={isAll ? 'layers' : 'home'}
208
+ label={onDefault ? p.showAllProfiles : p.switchToProfile(defaultProfile.name)}
209
+ onSelect={() => (onDefault ? setShowAllProfiles(true) : selectProfile(defaultProfile.name))}
210
+ />
211
+ ) : (
212
+ <ProfilePill active={isAll} glyph="layers" label={p.allProfiles} onSelect={() => setShowAllProfiles(true)} />
213
+ ))}
214
+
215
+ {/* Single-profile: the active default's home icon next to the create +. */}
216
+ {!multiProfile && defaultProfile && (
217
+ <ProfilePill
218
+ active
219
+ glyph="home"
220
+ label={defaultProfile.name}
221
+ onSelect={() => selectProfile(defaultProfile.name)}
222
+ />
223
+ )}
224
+
225
+ <div
226
+ className="flex min-w-0 flex-1 items-center gap-1 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
227
+ ref={scrollRef}
228
+ >
229
+ {multiProfile && (
230
+ <DndContext
231
+ collisionDetection={closestCenter}
232
+ modifiers={[stepThroughCells]}
233
+ onDragEnd={handleDragEnd}
234
+ onDragOver={handleDragOver}
235
+ onDragStart={handleDragStart}
236
+ sensors={sensors}
237
+ >
238
+ <SortableContext items={named.map(profile => profile.name)} strategy={horizontalListSortingStrategy}>
239
+ {/* relative → the strip is the dragged square's offsetParent, so the
240
+ clamp modifier bounds drags to the occupied cells (not the +). */}
241
+ <div className="relative flex items-center gap-1">
242
+ {named.map(profile => (
243
+ <ProfileSquare
244
+ active={!isAll && normalizeProfileKey(profile.name) === activeKey}
245
+ color={resolveProfileColor(profile.name, colors)}
246
+ key={profile.name}
247
+ label={profile.name}
248
+ onDelete={() => setPendingDelete(profile)}
249
+ onRecolor={color => setProfileColor(profile.name, color)}
250
+ onRename={() => setPendingRename(profile)}
251
+ onSelect={() => selectProfile(profile.name)}
252
+ />
253
+ ))}
254
+ </div>
255
+ </SortableContext>
256
+ </DndContext>
257
+ )}
258
+
259
+ <Tip label={p.newProfile}>
260
+ <button
261
+ aria-label={p.newProfile}
262
+ className="grid size-5 shrink-0 place-items-center rounded-[3px] text-(--ui-text-tertiary) opacity-55 transition hover:bg-(--ui-control-hover-background) hover:text-foreground hover:opacity-100"
263
+ onClick={() => setCreateOpen(true)}
264
+ type="button"
265
+ >
266
+ <Codicon name="add" size="0.75rem" />
267
+ </button>
268
+ </Tip>
269
+ </div>
270
+
271
+ {multiProfile && (
272
+ <ProfilePill active={false} glyph="ellipsis" label={p.manageProfiles} onSelect={() => navigate(PROFILES_ROUTE)} />
273
+ )}
274
+
275
+ {/* Land in the new profile on a fresh chat (selectProfile triggers the
276
+ new-session reset), not stuck on the session you were just in. */}
277
+ <CreateProfileDialog
278
+ onClose={() => setCreateOpen(false)}
279
+ onCreated={async name => {
280
+ await refreshActiveProfile()
281
+ selectProfile(name)
282
+ }}
283
+ open={createOpen}
284
+ />
285
+
286
+ <RenameProfileDialog
287
+ currentName={pendingRename?.name ?? ''}
288
+ onClose={() => setPendingRename(null)}
289
+ onRenamed={refreshActiveProfile}
290
+ open={pendingRename !== null}
291
+ />
292
+
293
+ <DeleteProfileDialog
294
+ onClose={() => setPendingDelete(null)}
295
+ onDeleted={refreshActiveProfile}
296
+ open={pendingDelete !== null}
297
+ profile={pendingDelete}
298
+ />
299
+ </div>
300
+ )
301
+ }
302
+
303
+ interface ProfilePillProps {
304
+ active: boolean
305
+ // home / All / Manage are glyph action buttons (navigation, not identity).
306
+ glyph: string
307
+ label: string
308
+ onSelect: () => void
309
+ }
310
+
311
+ function ProfilePill({ active, glyph, label, onSelect }: ProfilePillProps) {
312
+ return (
313
+ <Tip label={label}>
314
+ <Button
315
+ aria-label={label}
316
+ aria-pressed={active}
317
+ className={cn(
318
+ 'bg-transparent text-(--ui-text-tertiary) hover:bg-(--ui-control-hover-background) hover:text-foreground',
319
+ active && 'bg-(--ui-control-active-background) text-foreground'
320
+ )}
321
+ onClick={onSelect}
322
+ size="icon-xs"
323
+ type="button"
324
+ variant="ghost"
325
+ >
326
+ <Codicon name={glyph} size="0.875rem" />
327
+ </Button>
328
+ </Tip>
329
+ )
330
+ }
331
+
332
+ interface ProfileSquareProps {
333
+ active: boolean
334
+ color: null | string
335
+ label: string
336
+ onSelect: () => void
337
+ onRecolor: (color: null | string) => void
338
+ onRename: () => void
339
+ onDelete: () => void
340
+ }
341
+
342
+ // Hold this long without moving (a drag would have started first) to open the
343
+ // color picker — the "hard press" gesture, distinct from tap-to-select.
344
+ const LONG_PRESS_MS = 450
345
+
346
+ // A profile *is* its colored square — no icon-button chrome. Soft profile-tint
347
+ // fill + the initial in the full color; the active one pops to full opacity with
348
+ // a color ring. These pack tightly so the rail reads as a strip of profiles,
349
+ // drag-sort to reorder (a tap below the drag threshold still selects), and
350
+ // right-click to rename/delete. The button carries both the tooltip and
351
+ // context-menu triggers via nested asChild Slots, so a single element keeps the
352
+ // dnd listeners, hover tip, and right-click menu.
353
+ function ProfileSquare({ active, color, label, onDelete, onRecolor, onRename, onSelect }: ProfileSquareProps) {
354
+ const { t } = useI18n()
355
+ const p = t.profiles
356
+ const hue = color ?? 'var(--ui-text-quaternary)'
357
+ const [pickerOpen, setPickerOpen] = useState(false)
358
+ const pressTimer = useRef<null | number>(null)
359
+ const suppressClick = useRef(false)
360
+
361
+ const { attributes, isDragging, listeners, setNodeRef, transform, transition } = useSortable({
362
+ id: label,
363
+ transition: RAIL_TRANSITION
364
+ })
365
+
366
+ const clearPress = () => {
367
+ if (pressTimer.current != null) {
368
+ clearTimeout(pressTimer.current)
369
+ pressTimer.current = null
370
+ }
371
+ }
372
+
373
+ // A real drag (movement past the dnd threshold) cancels the pending hold, so a
374
+ // reorder never doubles as a color pick. Also tidy up on unmount.
375
+ useEffect(() => {
376
+ if (isDragging) {
377
+ clearPress()
378
+ }
379
+ }, [isDragging])
380
+ useEffect(() => clearPress, [])
381
+
382
+ const base = CSS.Transform.toString(transform)
383
+ const ring = active ? `inset 0 0 0 1.5px ${hue}` : ''
384
+ const lift = isDragging ? '0 6px 16px -4px rgb(0 0 0 / 0.4)' : ''
385
+
386
+ const pickColor = (next: null | string) => {
387
+ onRecolor(next)
388
+ setPickerOpen(false)
389
+ triggerHaptic('selection')
390
+ }
391
+
392
+ return (
393
+ <Popover onOpenChange={setPickerOpen} open={pickerOpen}>
394
+ <ContextMenu>
395
+ <TooltipProvider delayDuration={0}>
396
+ <Tooltip>
397
+ <PopoverAnchor asChild>
398
+ <ContextMenuTrigger asChild>
399
+ <TooltipTrigger asChild>
400
+ <button
401
+ className={cn(
402
+ 'grid size-5 shrink-0 cursor-grab touch-none select-none place-items-center rounded-[3px] text-[0.5625rem] font-semibold uppercase leading-none transition-opacity hover:opacity-100',
403
+ active ? 'opacity-100' : 'opacity-55',
404
+ isDragging && 'z-10 cursor-grabbing opacity-100'
405
+ )}
406
+ ref={setNodeRef}
407
+ style={{
408
+ backgroundColor: profileColorSoft(hue, active ? 30 : 22),
409
+ boxShadow: [ring, lift].filter(Boolean).join(', ') || undefined,
410
+ color: color ?? undefined,
411
+ // Glide the dragged square between snapped cells with a little
412
+ // overshoot (no scale — the overflow-x strip would clip it).
413
+ transform: base,
414
+ transition: isDragging ? DRAG_TRANSITION : transition
415
+ }}
416
+ type="button"
417
+ {...attributes}
418
+ {...listeners}
419
+ aria-label={label}
420
+ aria-pressed={active}
421
+ // Hold-to-recolor rides alongside the dnd pointer listener (call
422
+ // it first so drag tracking still arms), then a timer opens the
423
+ // picker and flags the trailing click so it doesn't also select.
424
+ onClick={() => {
425
+ if (suppressClick.current) {
426
+ suppressClick.current = false
427
+
428
+ return
429
+ }
430
+
431
+ onSelect()
432
+ }}
433
+ onPointerCancel={clearPress}
434
+ onPointerDown={event => {
435
+ listeners?.onPointerDown?.(event)
436
+
437
+ if (event.button !== 0) {
438
+ return
439
+ }
440
+
441
+ suppressClick.current = false
442
+ clearPress()
443
+ pressTimer.current = window.setTimeout(() => {
444
+ suppressClick.current = true
445
+ triggerHaptic('success')
446
+ setPickerOpen(true)
447
+ }, LONG_PRESS_MS)
448
+ }}
449
+ onPointerLeave={clearPress}
450
+ onPointerUp={clearPress}
451
+ >
452
+ {label.replace(/[^a-z0-9]/gi, '').charAt(0) || '?'}
453
+ </button>
454
+ </TooltipTrigger>
455
+ </ContextMenuTrigger>
456
+ </PopoverAnchor>
457
+ <TooltipContent>{label}</TooltipContent>
458
+ </Tooltip>
459
+ </TooltipProvider>
460
+
461
+ {/* The rail sits at the very bottom, so pad off the chrome (esp. the
462
+ statusbar) — Radix then flips the menu up instead of squishing it. */}
463
+ <ContextMenuContent
464
+ aria-label={p.actionsFor(label)}
465
+ className="w-40"
466
+ collisionPadding={{ bottom: 44, left: 8, right: 8, top: 8 }}
467
+ >
468
+ <ContextMenuItem onSelect={() => setPickerOpen(true)}>
469
+ <Codicon name="symbol-color" size="0.875rem" />
470
+ <span>{p.color}</span>
471
+ </ContextMenuItem>
472
+ <ContextMenuItem onSelect={onRename}>
473
+ <Codicon name="edit" size="0.875rem" />
474
+ <span>{p.rename}</span>
475
+ </ContextMenuItem>
476
+ <ContextMenuItem className="text-destructive focus:text-destructive" onSelect={onDelete} variant="destructive">
477
+ <Codicon name="trash" size="0.875rem" />
478
+ <span>{t.common.delete}</span>
479
+ </ContextMenuItem>
480
+ </ContextMenuContent>
481
+ </ContextMenu>
482
+
483
+ <PopoverContent
484
+ aria-label={p.colorFor(label)}
485
+ className="w-auto p-2"
486
+ collisionPadding={{ bottom: 44, left: 8, right: 8, top: 8 }}
487
+ side="top"
488
+ >
489
+ <div className="grid grid-cols-6 gap-1.5">
490
+ {PROFILE_SWATCHES.map(swatch => (
491
+ <button
492
+ aria-label={p.setColor(swatch)}
493
+ className="size-5 rounded-full transition-transform hover:scale-110"
494
+ key={swatch}
495
+ onClick={() => pickColor(swatch)}
496
+ style={{
497
+ backgroundColor: swatch,
498
+ boxShadow: swatch === color ? '0 0 0 2px var(--ui-bg-elevated), 0 0 0 3.5px currentColor' : undefined,
499
+ color: swatch
500
+ }}
501
+ type="button"
502
+ />
503
+ ))}
504
+ </div>
505
+ <button
506
+ className="mt-2 flex w-full items-center justify-center gap-1.5 rounded-md py-1 text-xs text-(--ui-text-tertiary) transition hover:bg-(--ui-control-hover-background) hover:text-foreground"
507
+ onClick={() => pickColor(null)}
508
+ type="button"
509
+ >
510
+ <Codicon name="sync" size="0.75rem" />
511
+ {p.autoColor}
512
+ </button>
513
+ </PopoverContent>
514
+ </Popover>
515
+ )
516
+ }