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,171 @@
1
+ import { useStore } from '@nanostores/react'
2
+ import { useEffect, useMemo } from 'react'
3
+
4
+ import type { SetTitlebarToolGroup } from '@/app/shell/titlebar-controls'
5
+ import { Codicon } from '@/components/ui/codicon'
6
+ import { Tip } from '@/components/ui/tooltip'
7
+ import { translateNow, useI18n } from '@/i18n'
8
+ import { cn } from '@/lib/utils'
9
+ import {
10
+ $rightRailActiveTabId,
11
+ RIGHT_RAIL_PREVIEW_TAB_ID,
12
+ type RightRailTabId,
13
+ selectRightRailTab
14
+ } from '@/store/layout'
15
+ import {
16
+ $filePreviewTabs,
17
+ $previewReloadRequest,
18
+ $previewTarget,
19
+ closeRightRail,
20
+ closeRightRailTab,
21
+ type PreviewTarget
22
+ } from '@/store/preview'
23
+
24
+ import { PreviewPane } from './preview-pane'
25
+
26
+ export const PREVIEW_RAIL_MIN_WIDTH = '18rem'
27
+ export const PREVIEW_RAIL_MAX_WIDTH = '38rem'
28
+
29
+ const INTRINSIC = `clamp(${PREVIEW_RAIL_MIN_WIDTH}, 36vw, 32rem)`
30
+
31
+ // Track for <Pane id="preview">. Folds the intrinsic clamp with a min-floor
32
+ // against --chat-min-width so the chat surface never gets squeezed below it.
33
+ // Subtracts the project browser width so preview yields rather than crushing
34
+ // the chat when both right-side panes are open.
35
+ export const PREVIEW_RAIL_PANE_WIDTH = `min(${INTRINSIC}, max(0rem, calc(100vw - var(--pane-chat-sidebar-width) - var(--pane-file-browser-width, 0rem) - var(--chat-min-width))))`
36
+
37
+ interface ChatPreviewRailProps {
38
+ onRestartServer?: (url: string, context?: string) => Promise<string>
39
+ setTitlebarToolGroup?: SetTitlebarToolGroup
40
+ }
41
+
42
+ interface RailTab {
43
+ id: RightRailTabId
44
+ label: string
45
+ target: PreviewTarget
46
+ }
47
+
48
+ function tabLabelFor(target: PreviewTarget): string {
49
+ const value = target.label || target.path || target.source || target.url
50
+ const tail = value.split(/[\\/]/).filter(Boolean).at(-1)
51
+
52
+ return tail || value || translateNow('preview.tab')
53
+ }
54
+
55
+ export function ChatPreviewRail({ onRestartServer, setTitlebarToolGroup }: ChatPreviewRailProps) {
56
+ const { t } = useI18n()
57
+ const previewReloadRequest = useStore($previewReloadRequest)
58
+ const activeTabId = useStore($rightRailActiveTabId)
59
+ const filePreviewTabs = useStore($filePreviewTabs)
60
+ const previewTarget = useStore($previewTarget)
61
+
62
+ const tabs = useMemo<readonly RailTab[]>(
63
+ () => [
64
+ ...(previewTarget ? [{ id: RIGHT_RAIL_PREVIEW_TAB_ID, label: t.preview.tab, target: previewTarget } as RailTab] : []),
65
+ ...filePreviewTabs.map(({ id, target }) => ({ id, label: tabLabelFor(target), target }) as RailTab)
66
+ ],
67
+ [filePreviewTabs, previewTarget, t.preview.tab]
68
+ )
69
+
70
+ const activeTab = tabs.find(tab => tab.id === activeTabId) ?? tabs[0]
71
+
72
+ useEffect(() => {
73
+ if (activeTab && activeTab.id !== activeTabId) {
74
+ selectRightRailTab(activeTab.id)
75
+ }
76
+ }, [activeTab, activeTabId])
77
+
78
+ if (!activeTab) {
79
+ return null
80
+ }
81
+
82
+ const isPreview = activeTab.id === RIGHT_RAIL_PREVIEW_TAB_ID
83
+
84
+ return (
85
+ <aside className="relative flex h-full w-full min-w-0 flex-col overflow-hidden border-l border-(--ui-stroke-tertiary) bg-(--ui-editor-surface-background) text-(--ui-text-tertiary)">
86
+ <div className="group/rail-tabs flex h-(--titlebar-height) shrink-0 border-b border-(--ui-stroke-tertiary) bg-(--ui-sidebar-surface-background)">
87
+ <div
88
+ className="flex min-w-0 flex-1 overflow-x-auto overflow-y-hidden overscroll-x-contain [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
89
+ role="tablist"
90
+ >
91
+ {tabs.map(tab => {
92
+ const active = tab.id === activeTab.id
93
+
94
+ return (
95
+ <div
96
+ className={cn(
97
+ 'group/tab relative flex h-full min-w-0 max-w-48 shrink-0 items-center text-[0.6875rem] font-medium [-webkit-app-region:no-drag] last:border-r last:border-(--ui-stroke-quaternary)',
98
+ active
99
+ ? 'bg-(--ui-editor-surface-background) text-foreground [--tab-bg:var(--ui-editor-surface-background)]'
100
+ : 'border-r border-(--ui-stroke-quaternary) text-(--ui-text-tertiary) [--tab-bg:var(--ui-sidebar-surface-background)] hover:bg-(--chrome-action-hover) hover:text-foreground'
101
+ )}
102
+ key={tab.id}
103
+ // Middle-click closes the tab, matching browser/IDE muscle
104
+ // memory. `onMouseDown` swallows the middle-button press so
105
+ // Chromium doesn't switch into autoscroll mode.
106
+ onAuxClick={event => {
107
+ if (event.button !== 1) {
108
+ return
109
+ }
110
+
111
+ event.preventDefault()
112
+ closeRightRailTab(tab.id)
113
+ }}
114
+ onMouseDown={event => {
115
+ if (event.button === 1) {
116
+ event.preventDefault()
117
+ }
118
+ }}
119
+ >
120
+ {active && (
121
+ <span aria-hidden="true" className="absolute inset-x-0 top-0 h-px bg-(--ui-stroke-primary)" />
122
+ )}
123
+ <Tip label={tab.label}>
124
+ <button
125
+ aria-selected={active}
126
+ className="flex h-full min-w-0 max-w-full items-center overflow-hidden pl-3 pr-2 text-left outline-none"
127
+ onClick={() => selectRightRailTab(tab.id)}
128
+ role="tab"
129
+ type="button"
130
+ >
131
+ <span className="block min-w-0 truncate">{tab.label}</span>
132
+ </button>
133
+ </Tip>
134
+ <span
135
+ aria-hidden="true"
136
+ className="pointer-events-none absolute inset-y-0 right-0 w-9 bg-[linear-gradient(to_right,transparent,var(--tab-bg)_55%)] opacity-0 transition-opacity group-hover/tab:opacity-100 group-focus-within/tab:opacity-100"
137
+ />
138
+ <button
139
+ aria-label={t.preview.closeTab(tab.label)}
140
+ className="pointer-events-none absolute right-1.5 top-1/2 grid size-4 -translate-y-1/2 place-items-center rounded-sm text-(--ui-text-tertiary) opacity-0 transition-[background-color,color,opacity] hover:bg-(--ui-bg-secondary) hover:text-foreground focus-visible:pointer-events-auto focus-visible:opacity-100 group-hover/tab:pointer-events-auto group-hover/tab:opacity-100 group-focus-within/tab:pointer-events-auto group-focus-within/tab:opacity-100"
141
+ onClick={() => closeRightRailTab(tab.id)}
142
+ type="button"
143
+ >
144
+ <Codicon name="close" size="0.75rem" />
145
+ </button>
146
+ </div>
147
+ )
148
+ })}
149
+ </div>
150
+ <button
151
+ aria-label={t.preview.closePane}
152
+ className="mr-1.5 grid size-6 shrink-0 self-center place-items-center rounded-md text-(--ui-text-tertiary) opacity-0 transition-opacity hover:bg-(--ui-control-hover-background) hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring group-hover/rail-tabs:opacity-100 [-webkit-app-region:no-drag]"
153
+ onClick={closeRightRail}
154
+ type="button"
155
+ >
156
+ <Codicon name="close" size="0.75rem" />
157
+ </button>
158
+ </div>
159
+
160
+ <div className="min-h-0 flex-1 overflow-hidden">
161
+ <PreviewPane
162
+ embedded
163
+ onRestartServer={isPreview ? onRestartServer : undefined}
164
+ reloadRequest={previewReloadRequest}
165
+ setTitlebarToolGroup={setTitlebarToolGroup}
166
+ target={activeTab.target}
167
+ />
168
+ </div>
169
+ </aside>
170
+ )
171
+ }
@@ -0,0 +1,67 @@
1
+ import { cleanup, fireEvent, render, screen } from '@testing-library/react'
2
+ import { afterEach, describe, expect, it, vi } from 'vitest'
3
+
4
+ import { clearAllPrompts, setApprovalRequest } from '@/store/prompts'
5
+ import { $activeSessionId } from '@/store/session'
6
+ import { onScrollToBottomRequest, resetThreadScroll, setThreadAtBottom } from '@/store/thread-scroll'
7
+
8
+ import { ScrollToBottomButton } from './scroll-to-bottom-button'
9
+
10
+ function pendingApproval() {
11
+ $activeSessionId.set('sess-1')
12
+ setApprovalRequest({ command: 'rm -rf /tmp/x', description: 'dangerous command', sessionId: 'sess-1' })
13
+ }
14
+
15
+ afterEach(() => {
16
+ cleanup()
17
+ clearAllPrompts()
18
+ resetThreadScroll()
19
+ $activeSessionId.set(null)
20
+ })
21
+
22
+ // `getByRole('button')` excludes aria-hidden nodes, so "queryByRole null" is the
23
+ // control's hidden (parked-at-bottom) state.
24
+ describe('ScrollToBottomButton', () => {
25
+ it('stays hidden while parked at the bottom', () => {
26
+ render(<ScrollToBottomButton />)
27
+
28
+ expect(screen.queryByRole('button')).toBeNull()
29
+ })
30
+
31
+ it('is a plain jump-to-bottom control when scrolled up with no approval', () => {
32
+ setThreadAtBottom(false)
33
+ render(<ScrollToBottomButton />)
34
+
35
+ expect(screen.getByRole('button', { name: 'Scroll to bottom' })).toBeTruthy()
36
+ expect(screen.queryByText('Approval needed')).toBeNull()
37
+ })
38
+
39
+ it('morphs into the approval pill when scrolled up with a pending approval', () => {
40
+ pendingApproval()
41
+ setThreadAtBottom(false)
42
+ render(<ScrollToBottomButton />)
43
+
44
+ expect(screen.getByRole('button', { name: 'Approval needed' })).toBeTruthy()
45
+ expect(screen.getByText('Approval needed')).toBeTruthy()
46
+ })
47
+
48
+ it('does not morph while a pending approval is still in view (at bottom)', () => {
49
+ pendingApproval()
50
+ render(<ScrollToBottomButton />)
51
+
52
+ // Parked at bottom → control hidden, so it can't claim "approval needed".
53
+ expect(screen.queryByRole('button')).toBeNull()
54
+ })
55
+
56
+ it('re-arms sticky-bottom on click', () => {
57
+ const handler = vi.fn()
58
+ const stop = onScrollToBottomRequest(handler)
59
+ setThreadAtBottom(false)
60
+ render(<ScrollToBottomButton />)
61
+
62
+ fireEvent.click(screen.getByRole('button'))
63
+
64
+ expect(handler).toHaveBeenCalledTimes(1)
65
+ stop()
66
+ })
67
+ })
@@ -0,0 +1,74 @@
1
+ import { useStore } from '@nanostores/react'
2
+ import { useRef } from 'react'
3
+
4
+ import { Codicon } from '@/components/ui/codicon'
5
+ import { useI18n } from '@/i18n'
6
+ import { triggerHaptic } from '@/lib/haptics'
7
+ import { cn } from '@/lib/utils'
8
+ import { $approvalRequest } from '@/store/prompts'
9
+ import { $threadJumpButtonVisible, requestScrollToBottom } from '@/store/thread-scroll'
10
+
11
+ /**
12
+ * Floating "jump to bottom" control. Sits centered just above the composer,
13
+ * clearing the out-of-flow status stack via the same measured-height CSS vars
14
+ * the thread's bottom clearance uses (`--composer-measured-height` +
15
+ * `--status-stack-measured-height`), so it never overlaps the queue / subagent
16
+ * / background cards. Visible only while the user has scrolled meaningfully
17
+ * away from the bottom; clicking re-arms sticky-bottom and pins the viewport.
18
+ *
19
+ * When the turn is BLOCKED on an approval, this same control morphs into an
20
+ * "Approval needed" pill — the only response surface is the inline Run/Reject
21
+ * bar on the parked tool row, which is always the bottom-most content, so the
22
+ * existing scroll-to-bottom action lands the user right on it. One control, no
23
+ * collision, no second scroll path (native scrollIntoView would scroll
24
+ * overflow:hidden ancestors that can't scroll back and wreck the layout).
25
+ *
26
+ * Enter/exit motion lives in styles.css under `.thread-jump-button` — a
27
+ * directional scale (contract in from 1.1, contract out to 0.9) keyed off
28
+ * `data-state`. `idle` (never-shown) stays silent so it can't flash on mount;
29
+ * `in`/`out` only swap once it has actually appeared.
30
+ */
31
+ export function ScrollToBottomButton() {
32
+ const { t } = useI18n()
33
+ const visible = useStore($threadJumpButtonVisible)
34
+ const request = useStore($approvalRequest)
35
+ // Scrolled away while an approval is pending → the inline Run/Reject bar is
36
+ // below the fold. Relabel so the user knows the session needs them, not just
37
+ // that there's more to read.
38
+ const approval = visible && Boolean(request)
39
+ const hasShownRef = useRef(false)
40
+
41
+ if (visible) {
42
+ hasShownRef.current = true
43
+ }
44
+
45
+ const state = visible ? 'in' : hasShownRef.current ? 'out' : 'idle'
46
+ const label = approval ? t.assistant.approval.jumpToApproval : t.assistant.thread.scrollToBottom
47
+
48
+ return (
49
+ <button
50
+ aria-hidden={!visible}
51
+ aria-label={label}
52
+ className={cn(
53
+ 'thread-jump-button absolute left-1/2 z-20 grid place-items-center backdrop-blur-[0.75rem] [-webkit-backdrop-filter:blur(0.75rem)]',
54
+ approval
55
+ ? 'h-8 grid-flow-col gap-1.5 rounded-full border border-primary/40 bg-(--composer-fill) px-3 text-primary hover:bg-primary/10'
56
+ : 'size-8 rounded-full border border-border/65 bg-(--composer-fill) text-muted-foreground hover:text-foreground',
57
+ !visible && 'pointer-events-none'
58
+ )}
59
+ data-state={state}
60
+ onClick={() => {
61
+ triggerHaptic('selection')
62
+ requestScrollToBottom()
63
+ }}
64
+ style={{
65
+ bottom: 'calc(var(--composer-measured-height) + var(--status-stack-measured-height) + 0.625rem)'
66
+ }}
67
+ tabIndex={visible ? 0 : -1}
68
+ type="button"
69
+ >
70
+ <Codicon name="arrow-down" size={approval ? '0.875rem' : '1rem'} />
71
+ {approval && <span className="text-xs font-medium">{label}</span>}
72
+ </button>
73
+ )
74
+ }
@@ -0,0 +1,325 @@
1
+ import { useStore } from '@nanostores/react'
2
+ import { useEffect, useMemo, useState } from 'react'
3
+
4
+ import { Codicon } from '@/components/ui/codicon'
5
+ import { DisclosureCaret } from '@/components/ui/disclosure-caret'
6
+ import { SidebarGroup, SidebarGroupContent } from '@/components/ui/sidebar'
7
+ import { Tip } from '@/components/ui/tooltip'
8
+ import { getCronJobRuns, type SessionInfo } from '@/nastech'
9
+ import { useI18n } from '@/i18n'
10
+ import { cn } from '@/lib/utils'
11
+ import { $selectedStoredSessionId } from '@/store/session'
12
+ import type { CronJob } from '@/types/nastech'
13
+
14
+ import { jobState, jobTitle, STATE_DOT } from '../../cron/job-state'
15
+ import { SidebarPanelLabel } from '../../shell/sidebar-label'
16
+
17
+ const INACTIVE_STATES = new Set(['completed', 'disabled', 'error', 'paused'])
18
+
19
+ // Recent runs shown in the inline quick-peek — enough to glance at history
20
+ // without turning the sidebar into the full Cron page.
21
+ const PEEK_RUN_LIMIT = 5
22
+
23
+ // Runs are written by the background scheduler tick (no UI signal), so poll the
24
+ // open peek so a freshly-fired run shows up within a few seconds.
25
+ const PEEK_POLL_INTERVAL_MS = 8000
26
+
27
+ const relativeFmt = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto', style: 'short' })
28
+
29
+ // Localized "in 5 min" / "2 hr ago" without hand-rolled strings — picks the
30
+ // coarsest sensible unit so a daily job reads "in 14 hr", not "in 840 min".
31
+ function relativeTime(targetMs: number, nowMs: number): string {
32
+ const diff = targetMs - nowMs
33
+ const abs = Math.abs(diff)
34
+ const sign = diff < 0 ? -1 : 1
35
+
36
+ if (abs < 60_000) {return relativeFmt.format(sign * Math.round(abs / 1000), 'second')}
37
+
38
+ if (abs < 3_600_000) {return relativeFmt.format(sign * Math.round(abs / 60_000), 'minute')}
39
+
40
+ if (abs < 86_400_000) {return relativeFmt.format(sign * Math.round(abs / 3_600_000), 'hour')}
41
+
42
+ return relativeFmt.format(sign * Math.round(abs / 86_400_000), 'day')
43
+ }
44
+
45
+ function nextRunMs(job: CronJob): null | number {
46
+ if (!job.next_run_at) {return null}
47
+
48
+ const ms = Date.parse(job.next_run_at)
49
+
50
+ return Number.isNaN(ms) ? null : ms
51
+ }
52
+
53
+ // Runs all belong to the same job, so the run name just repeats the job name —
54
+ // the timestamp is what tells them apart. Compact (no year, no seconds) for the
55
+ // narrow sidebar.
56
+ function formatRunTime(seconds?: null | number): string {
57
+ if (!seconds) {return '—'}
58
+
59
+ const date = new Date(seconds * 1000)
60
+
61
+ return Number.isNaN(date.valueOf())
62
+ ? '—'
63
+ : date.toLocaleString(undefined, { day: 'numeric', hour: 'numeric', minute: '2-digit', month: 'short' })
64
+ }
65
+
66
+ interface SidebarCronJobsSectionProps {
67
+ jobs: CronJob[]
68
+ label: string
69
+ max?: number
70
+ // Open a run session's chat (1 click to output).
71
+ onOpenRun: (sessionId: string) => void
72
+ // Open the full Cron page focused on this job (manage / full history).
73
+ onManageJob: (jobId: string) => void
74
+ // Fire the job now.
75
+ onTriggerJob: (jobId: string) => void
76
+ onToggle: () => void
77
+ open: boolean
78
+ }
79
+
80
+ export function SidebarCronJobsSection({
81
+ jobs,
82
+ label,
83
+ max = 50,
84
+ onManageJob,
85
+ onOpenRun,
86
+ onTriggerJob,
87
+ onToggle,
88
+ open
89
+ }: SidebarCronJobsSectionProps) {
90
+ const [nowMs, setNowMs] = useState(() => Date.now())
91
+ // Single-open inline peek so the section stays scannable.
92
+ const [peekJobId, setPeekJobId] = useState<null | string>(null)
93
+
94
+ // One clock for the whole section (rows are pure) so the countdowns tick
95
+ // without re-rendering the rest of the sidebar. Only runs while expanded.
96
+ useEffect(() => {
97
+ if (!open) {return}
98
+
99
+ const id = window.setInterval(() => setNowMs(Date.now()), 1000)
100
+
101
+ return () => window.clearInterval(id)
102
+ }, [open])
103
+
104
+ // Upcoming first (soonest next run), jobs with no next run sink to the bottom,
105
+ // then alphabetical for stability.
106
+ const sorted = useMemo(() => {
107
+ return [...jobs].sort((a, b) => {
108
+ const an = nextRunMs(a)
109
+ const bn = nextRunMs(b)
110
+
111
+ if (an !== null && bn !== null && an !== bn) {return an - bn}
112
+
113
+ if (an === null && bn !== null) {return 1}
114
+
115
+ if (an !== null && bn === null) {return -1}
116
+
117
+ return jobTitle(a).localeCompare(jobTitle(b))
118
+ })
119
+ }, [jobs])
120
+
121
+ const shown = sorted.slice(0, max)
122
+ // When capped, signal "50+" rather than implying the list is complete.
123
+ const countLabel = jobs.length > max ? `${max}+` : String(jobs.length)
124
+
125
+ return (
126
+ <SidebarGroup className="shrink-0 p-0 pb-1">
127
+ <div className="group/section flex shrink-0 items-center justify-between pb-1 pt-1.5">
128
+ <button
129
+ className="group/section-label flex w-fit items-center gap-1 bg-transparent text-left leading-none"
130
+ onClick={onToggle}
131
+ type="button"
132
+ >
133
+ <SidebarPanelLabel>{label}</SidebarPanelLabel>
134
+ <span className="text-[0.6875rem] font-medium text-(--ui-text-quaternary)">{countLabel}</span>
135
+ <DisclosureCaret
136
+ className="text-(--ui-text-tertiary) opacity-0 transition group-hover/section-label:opacity-100"
137
+ open={open}
138
+ />
139
+ </button>
140
+ </div>
141
+ {open && (
142
+ <SidebarGroupContent className="flex max-h-72 shrink-0 flex-col gap-px overflow-y-auto overscroll-contain pb-1.75">
143
+ {shown.map(job => (
144
+ <CronJobSidebarRow
145
+ expanded={peekJobId === job.id}
146
+ job={job}
147
+ key={job.id}
148
+ nowMs={nowMs}
149
+ onManage={() => onManageJob(job.id)}
150
+ onOpenRun={onOpenRun}
151
+ onTogglePeek={() => setPeekJobId(prev => (prev === job.id ? null : job.id))}
152
+ onTrigger={() => onTriggerJob(job.id)}
153
+ />
154
+ ))}
155
+ </SidebarGroupContent>
156
+ )}
157
+ </SidebarGroup>
158
+ )
159
+ }
160
+
161
+ function CronJobSidebarRow({
162
+ expanded,
163
+ job,
164
+ nowMs,
165
+ onManage,
166
+ onOpenRun,
167
+ onTogglePeek,
168
+ onTrigger
169
+ }: {
170
+ expanded: boolean
171
+ job: CronJob
172
+ nowMs: number
173
+ onManage: () => void
174
+ onOpenRun: (sessionId: string) => void
175
+ onTogglePeek: () => void
176
+ onTrigger: () => void
177
+ }) {
178
+ const { t } = useI18n()
179
+ const c = t.cron
180
+ const state = jobState(job)
181
+ const next = nextRunMs(job)
182
+ const label = jobTitle(job)
183
+
184
+ const meta = INACTIVE_STATES.has(state)
185
+ ? (c.states[state] ?? state)
186
+ : next !== null
187
+ ? relativeTime(next, nowMs)
188
+ : '—'
189
+
190
+ return (
191
+ <div>
192
+ <div className="group/cron relative grid min-h-[1.625rem] grid-cols-[minmax(0,1fr)_auto] items-center rounded-md hover:bg-(--chrome-action-hover)">
193
+ {/* Lead with the dot in the same w-3.5 cell + pl-2 the session rows use
194
+ so the cron dots line up with the sessions above; the caret sits next
195
+ to the label (matching the other sidebar disclosures) and the whole
196
+ label area toggles the run peek. */}
197
+ <button
198
+ aria-expanded={expanded}
199
+ aria-label={expanded ? c.hideRuns : c.showRuns}
200
+ className="flex min-w-0 items-center gap-1.5 bg-transparent py-0.5 pl-2 pr-1 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40"
201
+ onClick={onTogglePeek}
202
+ title={label}
203
+ type="button"
204
+ >
205
+ <span className="grid w-3.5 shrink-0 place-items-center">
206
+ <span
207
+ aria-hidden="true"
208
+ className={cn(
209
+ 'size-1 rounded-full',
210
+ STATE_DOT[state] ?? 'bg-(--ui-text-quaternary)',
211
+ state === 'running' && 'size-1.5 animate-pulse'
212
+ )}
213
+ />
214
+ </span>
215
+ <span className="min-w-0 truncate text-[0.8125rem] text-(--ui-text-secondary) group-hover/cron:text-foreground">
216
+ {label}
217
+ </span>
218
+ <DisclosureCaret
219
+ className={cn(
220
+ 'shrink-0 text-(--ui-text-tertiary) transition',
221
+ expanded ? 'opacity-100' : 'opacity-0 group-hover/cron:opacity-100'
222
+ )}
223
+ open={expanded}
224
+ />
225
+ </button>
226
+ {/* Trailing cluster: countdown by default, quick actions on hover. */}
227
+ <div className="flex items-center gap-0.5 justify-self-end pr-1">
228
+ <span className="text-[0.6875rem] text-(--ui-text-tertiary) tabular-nums group-hover/cron:hidden">
229
+ {meta}
230
+ </span>
231
+ <div className="hidden items-center gap-0.5 group-hover/cron:flex">
232
+ <Tip label={c.triggerNow}>
233
+ <button
234
+ aria-label={c.triggerNow}
235
+ className="grid size-5 place-items-center rounded-sm text-(--ui-text-tertiary) hover:bg-(--ui-control-hover-background) hover:text-foreground"
236
+ onClick={onTrigger}
237
+ type="button"
238
+ >
239
+ <Codicon name="zap" size="0.75rem" />
240
+ </button>
241
+ </Tip>
242
+ <Tip label={c.manage}>
243
+ <button
244
+ aria-label={c.manage}
245
+ className="grid size-5 place-items-center rounded-sm text-(--ui-text-tertiary) hover:bg-(--ui-control-hover-background) hover:text-foreground"
246
+ onClick={onManage}
247
+ type="button"
248
+ >
249
+ <Codicon name="watch" size="0.75rem" />
250
+ </button>
251
+ </Tip>
252
+ </div>
253
+ </div>
254
+ </div>
255
+ {expanded && <CronJobSidebarRuns jobId={job.id} onOpenRun={onOpenRun} />}
256
+ </div>
257
+ )
258
+ }
259
+
260
+ function CronJobSidebarRuns({
261
+ jobId,
262
+ onOpenRun
263
+ }: {
264
+ jobId: string
265
+ onOpenRun: (sessionId: string) => void
266
+ }) {
267
+ const { t } = useI18n()
268
+ const c = t.cron
269
+ const selectedSessionId = useStore($selectedStoredSessionId)
270
+ const [runs, setRuns] = useState<null | SessionInfo[]>(null)
271
+
272
+ useEffect(() => {
273
+ let cancelled = false
274
+
275
+ const load = () =>
276
+ getCronJobRuns(jobId, PEEK_RUN_LIMIT)
277
+ .then(result => {
278
+ if (!cancelled) {setRuns(result)}
279
+ })
280
+ .catch(() => {
281
+ if (!cancelled) {setRuns(prev => prev ?? [])}
282
+ })
283
+
284
+ void load()
285
+
286
+ const intervalId = window.setInterval(() => {
287
+ if (document.visibilityState === 'visible') {void load()}
288
+ }, PEEK_POLL_INTERVAL_MS)
289
+
290
+ return () => {
291
+ cancelled = true
292
+ window.clearInterval(intervalId)
293
+ }
294
+ }, [jobId])
295
+
296
+ return (
297
+ <div className="mb-1 ml-[1.375rem] flex flex-col gap-px">
298
+ {runs === null ? (
299
+ <div className="flex items-center gap-1.5 py-1 pl-1 text-[0.6875rem] text-(--ui-text-tertiary)">
300
+ <Codicon name="loading" size="0.75rem" spinning />
301
+ </div>
302
+ ) : runs.length === 0 ? (
303
+ <div className="py-1 pl-1 text-[0.6875rem] text-(--ui-text-tertiary)">{c.noRuns}</div>
304
+ ) : (
305
+ <>
306
+ {runs.map(run => (
307
+ <button
308
+ className={cn(
309
+ 'truncate rounded-md px-1.5 py-0.5 text-left text-[0.6875rem] tabular-nums focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40',
310
+ run.id === selectedSessionId
311
+ ? 'bg-(--ui-row-active-background) text-foreground'
312
+ : 'text-(--ui-text-secondary) hover:bg-(--chrome-action-hover) hover:text-foreground'
313
+ )}
314
+ key={run.id}
315
+ onClick={() => onOpenRun(run.id)}
316
+ type="button"
317
+ >
318
+ {formatRunTime(run.last_active || run.started_at)}
319
+ </button>
320
+ ))}
321
+ </>
322
+ )}
323
+ </div>
324
+ )
325
+ }