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,942 @@
1
+ import { useStore } from '@nanostores/react'
2
+ import type * as React from 'react'
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
4
+
5
+ import { PageLoader } from '@/components/page-loader'
6
+ import { Button } from '@/components/ui/button'
7
+ import { Codicon } from '@/components/ui/codicon'
8
+ import {
9
+ Dialog,
10
+ DialogContent,
11
+ DialogDescription,
12
+ DialogFooter,
13
+ DialogHeader,
14
+ DialogTitle
15
+ } from '@/components/ui/dialog'
16
+ import { Input } from '@/components/ui/input'
17
+ import { SearchField } from '@/components/ui/search-field'
18
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
19
+ import { Textarea } from '@/components/ui/textarea'
20
+ import {
21
+ createCronJob,
22
+ type CronJob,
23
+ deleteCronJob,
24
+ getCronJobRuns,
25
+ getCronJobs,
26
+ pauseCronJob,
27
+ resumeCronJob,
28
+ type SessionInfo,
29
+ triggerCronJob,
30
+ updateCronJob
31
+ } from '@/nastech'
32
+ import { type Translations, useI18n } from '@/i18n'
33
+ import { AlertTriangle, Clock } from '@/lib/icons'
34
+ import { cn } from '@/lib/utils'
35
+ import { $cronFocusJobId, $cronJobs, setCronFocusJobId, setCronJobs, updateCronJobs } from '@/store/cron'
36
+ import { notify, notifyError } from '@/store/notifications'
37
+
38
+ import { useRefreshHotkey } from '../hooks/use-refresh-hotkey'
39
+ import { OverlayMain, OverlayNewButton, OverlaySidebar, OverlaySplitLayout } from '../overlays/overlay-split-layout'
40
+ import { OverlayView } from '../overlays/overlay-view'
41
+ import type { SetStatusbarItemGroup } from '../shell/statusbar-controls'
42
+
43
+ import { jobState, jobTitle, STATE_DOT } from './job-state'
44
+
45
+ const DEFAULT_DELIVER = 'local'
46
+
47
+ const DELIVERY_VALUES: readonly string[] = ['local', 'telegram', 'discord', 'slack', 'email']
48
+
49
+ const SCHEDULE_OPTIONS: ReadonlyArray<ScheduleOption> = [
50
+ { expr: '0 9 * * *', value: 'daily' },
51
+ { expr: '0 9 * * 1-5', value: 'weekdays' },
52
+ { expr: '0 9 * * 1', value: 'weekly' },
53
+ { expr: '0 9 1 * *', value: 'monthly' },
54
+ { expr: '0 * * * *', value: 'hourly' },
55
+ { expr: '*/15 * * * *', value: 'every-15-minutes' },
56
+ { value: 'custom' }
57
+ ]
58
+
59
+ const STATE_TONE: Record<string, 'good' | 'muted' | 'warn' | 'bad'> = {
60
+ enabled: 'good',
61
+ scheduled: 'good',
62
+ running: 'good',
63
+ paused: 'warn',
64
+ disabled: 'muted',
65
+ error: 'bad',
66
+ completed: 'muted'
67
+ }
68
+
69
+ const PILL_TONE: Record<'good' | 'muted' | 'warn' | 'bad', string> = {
70
+ good: 'bg-primary/10 text-primary',
71
+ muted: 'bg-muted text-muted-foreground',
72
+ warn: 'bg-amber-500/10 text-amber-600 dark:text-amber-300',
73
+ bad: 'bg-destructive/10 text-destructive'
74
+ }
75
+
76
+ const asText = (value: unknown): string => (typeof value === 'string' ? value : '')
77
+
78
+ const truncate = (value: string, max = 80): string => (value.length > max ? `${value.slice(0, max)}…` : value)
79
+
80
+ function jobName(job: CronJob): string {
81
+ return asText(job.name).trim()
82
+ }
83
+
84
+ function jobPrompt(job: CronJob): string {
85
+ return asText(job.prompt)
86
+ }
87
+
88
+ function jobScheduleDisplay(job: CronJob): string {
89
+ return asText(job.schedule_display) || asText(job.schedule?.display) || asText(job.schedule?.expr) || '—'
90
+ }
91
+
92
+ function jobScheduleExpr(job: CronJob): string {
93
+ return asText(job.schedule?.expr) || asText(job.schedule_display) || ''
94
+ }
95
+
96
+ function jobDeliver(job: CronJob): string {
97
+ return asText(job.deliver) || DEFAULT_DELIVER
98
+ }
99
+
100
+ function cronParts(expr: string): null | string[] {
101
+ const parts = expr.trim().replace(/\s+/g, ' ').split(' ')
102
+
103
+ return parts.length === 5 ? parts : null
104
+ }
105
+
106
+ function dayName(value: string, c: Translations['cron']): string {
107
+ return c.days[value] ?? c.dayFallback(value)
108
+ }
109
+
110
+ function formatCronTime(minute: string, hour: string): string {
111
+ const numericHour = Number(hour)
112
+ const numericMinute = Number(minute)
113
+
114
+ if (!Number.isInteger(numericHour) || !Number.isInteger(numericMinute)) {
115
+ return `${hour}:${minute}`
116
+ }
117
+
118
+ return new Date(2000, 0, 1, numericHour, numericMinute).toLocaleTimeString(undefined, {
119
+ hour: 'numeric',
120
+ minute: '2-digit'
121
+ })
122
+ }
123
+
124
+ function isIntegerToken(value: string): boolean {
125
+ return /^\d+$/.test(value)
126
+ }
127
+
128
+ function scheduleOptionForExpr(expr: string): ScheduleOption {
129
+ const normalized = expr.trim().replace(/\s+/g, ' ')
130
+ const exactMatch = SCHEDULE_OPTIONS.find(option => option.expr === normalized)
131
+
132
+ if (exactMatch) {
133
+ return exactMatch
134
+ }
135
+
136
+ const parts = cronParts(normalized)
137
+
138
+ if (!parts) {
139
+ return SCHEDULE_OPTIONS[SCHEDULE_OPTIONS.length - 1]
140
+ }
141
+
142
+ const [minute, hour, dayOfMonth, month, dayOfWeek] = parts
143
+
144
+ if (dayOfMonth === '*' && month === '*' && dayOfWeek === '*' && isIntegerToken(minute) && isIntegerToken(hour)) {
145
+ return SCHEDULE_OPTIONS.find(option => option.value === 'daily') ?? SCHEDULE_OPTIONS[0]
146
+ }
147
+
148
+ if (dayOfMonth === '*' && month === '*' && dayOfWeek === '1-5' && isIntegerToken(minute) && isIntegerToken(hour)) {
149
+ return SCHEDULE_OPTIONS.find(option => option.value === 'weekdays') ?? SCHEDULE_OPTIONS[0]
150
+ }
151
+
152
+ if (
153
+ dayOfMonth === '*' &&
154
+ month === '*' &&
155
+ isIntegerToken(dayOfWeek) &&
156
+ isIntegerToken(minute) &&
157
+ isIntegerToken(hour)
158
+ ) {
159
+ return SCHEDULE_OPTIONS.find(option => option.value === 'weekly') ?? SCHEDULE_OPTIONS[0]
160
+ }
161
+
162
+ if (
163
+ month === '*' &&
164
+ dayOfWeek === '*' &&
165
+ isIntegerToken(dayOfMonth) &&
166
+ isIntegerToken(minute) &&
167
+ isIntegerToken(hour)
168
+ ) {
169
+ return SCHEDULE_OPTIONS.find(option => option.value === 'monthly') ?? SCHEDULE_OPTIONS[0]
170
+ }
171
+
172
+ if (hour === '*' && dayOfMonth === '*' && month === '*' && dayOfWeek === '*' && isIntegerToken(minute)) {
173
+ return SCHEDULE_OPTIONS.find(option => option.value === 'hourly') ?? SCHEDULE_OPTIONS[0]
174
+ }
175
+
176
+ if (normalized === '*/15 * * * *') {
177
+ return SCHEDULE_OPTIONS.find(option => option.value === 'every-15-minutes') ?? SCHEDULE_OPTIONS[0]
178
+ }
179
+
180
+ return SCHEDULE_OPTIONS[SCHEDULE_OPTIONS.length - 1]
181
+ }
182
+
183
+ function scheduleSummary(option: ScheduleOption, expr: string, c: Translations['cron']): string {
184
+ const parts = cronParts(expr)
185
+
186
+ if (!parts) {
187
+ return c.scheduleHints[option.value] ?? ''
188
+ }
189
+
190
+ const [minute, hour, dayOfMonth, , dayOfWeek] = parts
191
+
192
+ if (option.value === 'daily') {
193
+ return c.everyDayAt(formatCronTime(minute, hour))
194
+ }
195
+
196
+ if (option.value === 'weekdays') {
197
+ return c.weekdaysAt(formatCronTime(minute, hour))
198
+ }
199
+
200
+ if (option.value === 'weekly') {
201
+ return c.everyDayOfWeekAt(dayName(dayOfWeek, c), formatCronTime(minute, hour))
202
+ }
203
+
204
+ if (option.value === 'monthly') {
205
+ return c.monthlyOnDayAt(dayOfMonth, formatCronTime(minute, hour))
206
+ }
207
+
208
+ if (option.value === 'hourly') {
209
+ return minute === '0' ? c.topOfHour : c.everyHourAt(minute.padStart(2, '0'))
210
+ }
211
+
212
+ return c.scheduleHints[option.value] ?? ''
213
+ }
214
+
215
+ function formatTime(iso?: null | string): string {
216
+ if (!iso) {
217
+ return '—'
218
+ }
219
+
220
+ const date = new Date(iso)
221
+
222
+ if (Number.isNaN(date.valueOf())) {
223
+ return iso
224
+ }
225
+
226
+ return date.toLocaleString()
227
+ }
228
+
229
+ function matchesQuery(job: CronJob, q: string): boolean {
230
+ if (!q) {
231
+ return true
232
+ }
233
+
234
+ const needle = q.toLowerCase()
235
+
236
+ return [jobTitle(job), jobPrompt(job), jobScheduleDisplay(job), jobScheduleExpr(job), jobDeliver(job)].some(value =>
237
+ value.toLowerCase().includes(needle)
238
+ )
239
+ }
240
+
241
+ interface CronViewProps extends React.ComponentProps<'section'> {
242
+ onClose: () => void
243
+ onOpenSession?: (sessionId: string) => void
244
+ setStatusbarItemGroup?: SetStatusbarItemGroup
245
+ }
246
+
247
+ export function CronView({ onClose, onOpenSession, setStatusbarItemGroup: _setStatusbarItemGroup }: CronViewProps) {
248
+ const { t } = useI18n()
249
+ const c = t.cron
250
+ // Source of truth is the shared atom (also fed by the controller poll), so the
251
+ // sidebar and this overlay never drift — a delete here clears the sidebar row
252
+ // immediately. `loading` only gates the first paint before the atom is filled.
253
+ const jobs = useStore($cronJobs)
254
+ const [loading, setLoading] = useState(jobs.length === 0)
255
+ const [query, setQuery] = useState('')
256
+ const [busyJobId, setBusyJobId] = useState<null | string>(null)
257
+ // Master/detail: the job whose schedule + run history fill the right pane.
258
+ const [selectedJobId, setSelectedJobId] = useState<null | string>(null)
259
+ // Set when a job is opened from the sidebar so we scroll it into view once the
260
+ // row exists. Cleared after the scroll fires.
261
+ const pendingScrollRef = useRef<null | string>(null)
262
+ const focusJobId = useStore($cronFocusJobId)
263
+
264
+ const [editor, setEditor] = useState<EditorState>({ mode: 'closed' })
265
+ const [pendingDelete, setPendingDelete] = useState<CronJob | null>(null)
266
+ const [deleting, setDeleting] = useState(false)
267
+
268
+ const refresh = useCallback(async () => {
269
+ try {
270
+ setCronJobs(await getCronJobs())
271
+ } catch (err) {
272
+ notifyError(err, c.failedLoad)
273
+ } finally {
274
+ setLoading(false)
275
+ }
276
+ }, [c])
277
+
278
+ useRefreshHotkey(refresh)
279
+
280
+ useEffect(() => {
281
+ void refresh()
282
+ }, [refresh])
283
+
284
+ // Sidebar → "open this job": resolve the focus id (or name) to a job, select
285
+ // it, queue a scroll, then clear the one-shot focus so re-opening cron
286
+ // normally doesn't re-trigger it.
287
+ useEffect(() => {
288
+ if (!focusJobId) {return}
289
+
290
+ const match = jobs.find(job => job.id === focusJobId || jobName(job) === focusJobId)
291
+
292
+ if (match) {
293
+ setSelectedJobId(match.id)
294
+ pendingScrollRef.current = match.id
295
+ }
296
+
297
+ setCronFocusJobId(null)
298
+ }, [focusJobId, jobs])
299
+
300
+ const visibleJobs = useMemo(
301
+ () => jobs.filter(job => matchesQuery(job, query.trim())).sort((a, b) => jobTitle(a).localeCompare(jobTitle(b))),
302
+ [jobs, query]
303
+ )
304
+
305
+ // Detail always reflects a concrete job: the explicitly selected one, else the
306
+ // first visible row, so the right pane is never empty while jobs exist.
307
+ const selectedJob = useMemo(
308
+ () => visibleJobs.find(job => job.id === selectedJobId) ?? visibleJobs[0] ?? null,
309
+ [visibleJobs, selectedJobId]
310
+ )
311
+
312
+ // Scroll a sidebar-opened job into view once its list row is mounted.
313
+ useEffect(() => {
314
+ const target = pendingScrollRef.current
315
+
316
+ if (!target || selectedJob?.id !== target) {return}
317
+
318
+ pendingScrollRef.current = null
319
+ requestAnimationFrame(() => {
320
+ document.querySelector(`[data-cron-row="${CSS.escape(target)}"]`)?.scrollIntoView({ block: 'nearest' })
321
+ })
322
+ }, [selectedJob])
323
+
324
+ const totalCount = jobs.length
325
+
326
+ async function handlePauseResume(job: CronJob) {
327
+ setBusyJobId(job.id)
328
+
329
+ try {
330
+ const isPaused = jobState(job) === 'paused'
331
+ const updated = isPaused ? await resumeCronJob(job.id) : await pauseCronJob(job.id)
332
+ updateCronJobs(rows => rows.map(row => (row.id === job.id ? updated : row)))
333
+ notify({
334
+ kind: 'success',
335
+ title: isPaused ? c.resumed : c.paused,
336
+ message: truncate(jobTitle(job), 60)
337
+ })
338
+ } catch (err) {
339
+ notifyError(err, c.failedUpdate)
340
+ } finally {
341
+ setBusyJobId(null)
342
+ }
343
+ }
344
+
345
+ async function handleTrigger(job: CronJob) {
346
+ setBusyJobId(job.id)
347
+
348
+ try {
349
+ const updated = await triggerCronJob(job.id)
350
+ updateCronJobs(rows => rows.map(row => (row.id === job.id ? updated : row)))
351
+ notify({ kind: 'success', title: c.triggered, message: truncate(jobTitle(job), 60) })
352
+ } catch (err) {
353
+ notifyError(err, c.failedTrigger)
354
+ } finally {
355
+ setBusyJobId(null)
356
+ }
357
+ }
358
+
359
+ async function handleConfirmDelete() {
360
+ if (!pendingDelete) {
361
+ return
362
+ }
363
+
364
+ setDeleting(true)
365
+
366
+ try {
367
+ await deleteCronJob(pendingDelete.id)
368
+ updateCronJobs(rows => rows.filter(row => row.id !== pendingDelete.id))
369
+ notify({ kind: 'success', title: c.deleted, message: truncate(jobTitle(pendingDelete), 60) })
370
+ setPendingDelete(null)
371
+ } catch (err) {
372
+ notifyError(err, c.failedDelete)
373
+ } finally {
374
+ setDeleting(false)
375
+ }
376
+ }
377
+
378
+ async function handleEditorSave(values: EditorValues) {
379
+ if (editor.mode === 'create') {
380
+ const created = await createCronJob({
381
+ prompt: values.prompt,
382
+ schedule: values.schedule,
383
+ name: values.name || undefined,
384
+ deliver: values.deliver || DEFAULT_DELIVER
385
+ })
386
+
387
+ updateCronJobs(rows => [...rows, created])
388
+ notify({ kind: 'success', title: c.created, message: truncate(jobTitle(created), 60) })
389
+ } else if (editor.mode === 'edit') {
390
+ const updated = await updateCronJob(editor.job.id, {
391
+ prompt: values.prompt,
392
+ schedule: values.schedule,
393
+ name: values.name,
394
+ deliver: values.deliver
395
+ })
396
+
397
+ updateCronJobs(rows => rows.map(row => (row.id === updated.id ? updated : row)))
398
+ notify({ kind: 'success', title: c.updated, message: truncate(jobTitle(updated), 60) })
399
+ }
400
+
401
+ setEditor({ mode: 'closed' })
402
+ }
403
+
404
+ return (
405
+ <OverlayView closeLabel={c.close} onClose={onClose}>
406
+ {loading && jobs.length === 0 ? (
407
+ <PageLoader label={c.loading} />
408
+ ) : (
409
+ <OverlaySplitLayout>
410
+ <OverlaySidebar>
411
+ <OverlayNewButton label={c.newCron} onClick={() => setEditor({ mode: 'create' })} />
412
+ {totalCount > 0 && (
413
+ <SearchField
414
+ aria-label={c.search}
415
+ containerClassName="mb-1 w-full px-2"
416
+ onChange={setQuery}
417
+ placeholder={c.search}
418
+ value={query}
419
+ />
420
+ )}
421
+ {visibleJobs.map(job => (
422
+ <CronJobListRow
423
+ active={selectedJob?.id === job.id}
424
+ c={c}
425
+ job={job}
426
+ key={job.id}
427
+ onSelect={() => setSelectedJobId(job.id)}
428
+ />
429
+ ))}
430
+ {visibleJobs.length === 0 && (
431
+ <p className="px-2 py-4 text-center text-xs text-muted-foreground">
432
+ {totalCount === 0 ? c.emptyTitleNew : c.emptyTitleSearch}
433
+ </p>
434
+ )}
435
+ </OverlaySidebar>
436
+
437
+ <OverlayMain className="px-0">
438
+ {selectedJob ? (
439
+ <CronJobDetail
440
+ busy={busyJobId === selectedJob.id}
441
+ c={c}
442
+ job={selectedJob}
443
+ onDelete={() => setPendingDelete(selectedJob)}
444
+ onEdit={() => setEditor({ mode: 'edit', job: selectedJob })}
445
+ onOpenSession={onOpenSession}
446
+ onPauseResume={() => void handlePauseResume(selectedJob)}
447
+ onTrigger={() => void handleTrigger(selectedJob)}
448
+ />
449
+ ) : (
450
+ <div className="grid h-full place-items-center px-6 py-12 text-center text-sm text-muted-foreground">
451
+ <div>
452
+ <Clock className="mx-auto size-6 text-muted-foreground/60" />
453
+ <p className="mt-3">{totalCount === 0 ? c.emptyDescNew : c.emptyDescSearch}</p>
454
+ </div>
455
+ </div>
456
+ )}
457
+ </OverlayMain>
458
+ </OverlaySplitLayout>
459
+ )}
460
+
461
+ <CronEditorDialog editor={editor} onClose={() => setEditor({ mode: 'closed' })} onSave={handleEditorSave} />
462
+
463
+ <Dialog onOpenChange={open => !open && !deleting && setPendingDelete(null)} open={pendingDelete !== null}>
464
+ <DialogContent className="max-w-md">
465
+ <DialogHeader>
466
+ <DialogTitle>{c.deleteTitle}</DialogTitle>
467
+ <DialogDescription>
468
+ {pendingDelete ? (
469
+ <>
470
+ {c.deleteDescPrefix}
471
+ <span className="font-medium text-foreground">{truncate(jobTitle(pendingDelete), 60)}</span>
472
+ {c.deleteDescSuffix}
473
+ </>
474
+ ) : null}
475
+ </DialogDescription>
476
+ </DialogHeader>
477
+ <DialogFooter>
478
+ <Button disabled={deleting} onClick={() => setPendingDelete(null)} variant="outline">
479
+ {t.common.cancel}
480
+ </Button>
481
+ <Button disabled={deleting} onClick={() => void handleConfirmDelete()} variant="destructive">
482
+ {deleting ? c.deleting : t.common.delete}
483
+ </Button>
484
+ </DialogFooter>
485
+ </DialogContent>
486
+ </Dialog>
487
+ </OverlayView>
488
+ )
489
+ }
490
+
491
+ function CronJobListRow({
492
+ active,
493
+ c,
494
+ job,
495
+ onSelect
496
+ }: {
497
+ active: boolean
498
+ c: Translations['cron']
499
+ job: CronJob
500
+ onSelect: () => void
501
+ }) {
502
+ const state = jobState(job)
503
+
504
+ return (
505
+ <button
506
+ className={cn(
507
+ 'flex w-full flex-col items-start gap-0.5 rounded-md px-2 py-1.5 text-left transition-colors',
508
+ active ? 'bg-accent text-foreground' : 'text-foreground/85 hover:bg-accent/60'
509
+ )}
510
+ data-cron-row={job.id}
511
+ onClick={onSelect}
512
+ type="button"
513
+ >
514
+ <span className="flex w-full items-center gap-2">
515
+ <span
516
+ aria-hidden="true"
517
+ className={cn('size-1.5 shrink-0 rounded-full', STATE_DOT[state] ?? 'bg-muted-foreground')}
518
+ />
519
+ <span className="min-w-0 flex-1 truncate text-sm font-medium">{jobTitle(job)}</span>
520
+ </span>
521
+ <span className="truncate pl-3.5 text-[0.66rem] text-muted-foreground">{jobScheduleDisplay(job)}</span>
522
+ </button>
523
+ )
524
+ }
525
+
526
+ function CronJobDetail({
527
+ busy,
528
+ c,
529
+ job,
530
+ onDelete,
531
+ onEdit,
532
+ onOpenSession,
533
+ onPauseResume,
534
+ onTrigger
535
+ }: {
536
+ busy: boolean
537
+ c: Translations['cron']
538
+ job: CronJob
539
+ onDelete: () => void
540
+ onEdit: () => void
541
+ onOpenSession?: (sessionId: string) => void
542
+ onPauseResume: () => void
543
+ onTrigger: () => void
544
+ }) {
545
+ const state = jobState(job)
546
+ const isPaused = state === 'paused'
547
+ const deliver = jobDeliver(job)
548
+ const prompt = jobPrompt(job)
549
+
550
+ return (
551
+ <div className="flex h-full min-h-0 flex-col">
552
+ <div className="min-h-0 flex-1 overflow-y-auto">
553
+ <div className="mx-auto max-w-2xl space-y-6 px-6 py-6">
554
+ <header className="space-y-3">
555
+ <div className="flex flex-wrap items-start justify-between gap-3">
556
+ <div className="min-w-0 space-y-1">
557
+ <div className="flex flex-wrap items-center gap-2">
558
+ <h3 className="text-xl font-semibold tracking-tight">{jobTitle(job)}</h3>
559
+ <StatePill tone={STATE_TONE[state] ?? 'muted'}>{c.states[state] ?? state}</StatePill>
560
+ {deliver && deliver !== DEFAULT_DELIVER && (
561
+ <StatePill tone="muted">{c.deliveryLabels[deliver] ?? deliver}</StatePill>
562
+ )}
563
+ </div>
564
+ <div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-[0.7rem] text-muted-foreground">
565
+ <span className="inline-flex items-center gap-1">
566
+ <Clock className="size-3" />
567
+ {jobScheduleDisplay(job)}
568
+ </span>
569
+ <span>
570
+ {c.last} {formatTime(job.last_run_at)}
571
+ </span>
572
+ <span>
573
+ {c.next} {formatTime(job.next_run_at)}
574
+ </span>
575
+ </div>
576
+ </div>
577
+ <div className="flex shrink-0 items-center gap-1">
578
+ <Button disabled={busy} onClick={onPauseResume} size="sm" variant="outline">
579
+ <Codicon name={isPaused ? 'play' : 'debug-pause'} size="0.875rem" />
580
+ {isPaused ? c.resumeTitle : c.pauseTitle}
581
+ </Button>
582
+ <Button disabled={busy} onClick={onTrigger} size="sm" variant="outline">
583
+ <Codicon name="zap" size="0.875rem" />
584
+ {c.triggerNow}
585
+ </Button>
586
+ <Button onClick={onEdit} size="sm" variant="outline">
587
+ <Codicon name="edit" size="0.875rem" />
588
+ {c.edit}
589
+ </Button>
590
+ <Button
591
+ className="text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
592
+ onClick={onDelete}
593
+ size="sm"
594
+ variant="ghost"
595
+ >
596
+ <Codicon name="trash" size="0.875rem" />
597
+ </Button>
598
+ </div>
599
+ </div>
600
+
601
+ {prompt && <p className="line-clamp-3 text-xs text-muted-foreground">{prompt}</p>}
602
+ {job.last_error && (
603
+ <p className="inline-flex items-start gap-1 text-[0.7rem] text-destructive">
604
+ <AlertTriangle className="mt-px size-3 shrink-0" />
605
+ <span className="line-clamp-2">{job.last_error}</span>
606
+ </p>
607
+ )}
608
+ </header>
609
+
610
+ <CronJobRuns c={c} jobId={job.id} onOpenSession={onOpenSession} />
611
+ </div>
612
+ </div>
613
+ </div>
614
+ )
615
+ }
616
+
617
+ function formatRunTime(seconds?: null | number): string {
618
+ if (!seconds) {
619
+ return '—'
620
+ }
621
+
622
+ const date = new Date(seconds * 1000)
623
+
624
+ return Number.isNaN(date.valueOf()) ? '—' : date.toLocaleString()
625
+ }
626
+
627
+ // Runs are produced by the background scheduler tick (no UI signal), so poll
628
+ // while the panel is open + on tab re-focus so a fired run shows up within a few
629
+ // seconds instead of waiting for a reload.
630
+ const RUNS_POLL_INTERVAL_MS = 8000
631
+
632
+ function CronJobRuns({
633
+ c,
634
+ jobId,
635
+ onOpenSession
636
+ }: {
637
+ c: Translations['cron']
638
+ jobId: string
639
+ onOpenSession?: (sessionId: string) => void
640
+ }) {
641
+ const [runs, setRuns] = useState<null | SessionInfo[]>(null)
642
+
643
+ useEffect(() => {
644
+ let cancelled = false
645
+
646
+ const load = () =>
647
+ getCronJobRuns(jobId)
648
+ .then(result => {
649
+ if (!cancelled) {setRuns(result)}
650
+ })
651
+ .catch(() => {
652
+ if (!cancelled) {setRuns(prev => prev ?? [])}
653
+ })
654
+
655
+ void load()
656
+
657
+ const intervalId = window.setInterval(() => {
658
+ if (document.visibilityState === 'visible') {void load()}
659
+ }, RUNS_POLL_INTERVAL_MS)
660
+
661
+ const onVisible = () => {
662
+ if (document.visibilityState === 'visible') {void load()}
663
+ }
664
+
665
+ document.addEventListener('visibilitychange', onVisible)
666
+
667
+ return () => {
668
+ cancelled = true
669
+ window.clearInterval(intervalId)
670
+ document.removeEventListener('visibilitychange', onVisible)
671
+ }
672
+ }, [jobId])
673
+
674
+ return (
675
+ <div>
676
+ <div className="mb-1.5 text-[0.62rem] font-medium uppercase tracking-wide text-muted-foreground">
677
+ {c.runHistory}
678
+ {runs && runs.length > 0 ? ` · ${runs.length}` : ''}
679
+ </div>
680
+ {runs === null ? (
681
+ <div className="flex items-center gap-1.5 py-1 text-xs text-muted-foreground">
682
+ <Codicon name="loading" size="0.75rem" spinning />
683
+ </div>
684
+ ) : runs.length === 0 ? (
685
+ <div className="py-1 text-xs text-muted-foreground">{c.noRuns}</div>
686
+ ) : (
687
+ <div className="flex flex-col gap-px">
688
+ {runs.map(run => (
689
+ <button
690
+ className="flex items-center justify-between gap-3 rounded-md px-2 py-1 text-left text-xs hover:bg-(--chrome-action-hover) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40"
691
+ key={run.id}
692
+ onClick={() => onOpenSession?.(run.id)}
693
+ type="button"
694
+ >
695
+ <span className="truncate text-foreground">{run.title?.trim() || run.preview?.trim() || run.id}</span>
696
+ <span className="shrink-0 text-[0.62rem] text-muted-foreground tabular-nums">
697
+ {formatRunTime(run.last_active || run.started_at)}
698
+ </span>
699
+ </button>
700
+ ))}
701
+ </div>
702
+ )}
703
+ </div>
704
+ )
705
+ }
706
+
707
+ function StatePill({ children, tone }: { children: string; tone: keyof typeof PILL_TONE }) {
708
+ return (
709
+ <span
710
+ className={cn('inline-flex items-center rounded-full px-1.5 py-0.5 text-[0.64rem] capitalize', PILL_TONE[tone])}
711
+ >
712
+ {children}
713
+ </span>
714
+ )
715
+ }
716
+
717
+ function CronEditorDialog({
718
+ editor,
719
+ onClose,
720
+ onSave
721
+ }: {
722
+ editor: EditorState
723
+ onClose: () => void
724
+ onSave: (values: EditorValues) => Promise<void>
725
+ }) {
726
+ const { t } = useI18n()
727
+ const c = t.cron
728
+ const open = editor.mode !== 'closed'
729
+ const isEdit = editor.mode === 'edit'
730
+ const initial = isEdit ? editor.job : null
731
+
732
+ const [name, setName] = useState('')
733
+ const [prompt, setPrompt] = useState('')
734
+ const [schedule, setSchedule] = useState('')
735
+ const [schedulePreset, setSchedulePreset] = useState('daily')
736
+ const [deliver, setDeliver] = useState(DEFAULT_DELIVER)
737
+ const [saving, setSaving] = useState(false)
738
+ const [error, setError] = useState<null | string>(null)
739
+
740
+ useEffect(() => {
741
+ if (!open) {
742
+ return
743
+ }
744
+
745
+ setName(initial ? jobName(initial) : '')
746
+ setPrompt(initial ? jobPrompt(initial) : '')
747
+ setSchedule(initial ? jobScheduleExpr(initial) : (SCHEDULE_OPTIONS[0].expr ?? ''))
748
+ setSchedulePreset(initial ? scheduleOptionForExpr(jobScheduleExpr(initial)).value : 'daily')
749
+ setDeliver(initial ? jobDeliver(initial) : DEFAULT_DELIVER)
750
+ setError(null)
751
+ setSaving(false)
752
+ }, [initial, open])
753
+
754
+ const selectedScheduleOption =
755
+ SCHEDULE_OPTIONS.find(candidate => candidate.value === schedulePreset) ?? SCHEDULE_OPTIONS[0]
756
+
757
+ function handleSchedulePresetChange(nextPreset: string) {
758
+ setSchedulePreset(nextPreset)
759
+ setError(null)
760
+
761
+ const option = SCHEDULE_OPTIONS.find(candidate => candidate.value === nextPreset)
762
+
763
+ if (option?.expr) {
764
+ setSchedule(option.expr)
765
+ } else if (scheduleOptionForExpr(schedule).value !== 'custom') {
766
+ setSchedule('')
767
+ }
768
+ }
769
+
770
+ const scheduleHint = scheduleSummary(selectedScheduleOption, schedule, c)
771
+
772
+ async function handleSubmit(event: React.FormEvent) {
773
+ event.preventDefault()
774
+ const trimmedPrompt = prompt.trim()
775
+ const trimmedSchedule = schedule.trim()
776
+
777
+ if (!trimmedPrompt || !trimmedSchedule) {
778
+ setError(c.promptScheduleRequired)
779
+
780
+ return
781
+ }
782
+
783
+ setSaving(true)
784
+ setError(null)
785
+
786
+ try {
787
+ await onSave({
788
+ deliver,
789
+ name: name.trim(),
790
+ prompt: trimmedPrompt,
791
+ schedule: trimmedSchedule
792
+ })
793
+ } catch (err) {
794
+ setError(err instanceof Error ? err.message : c.failedSave)
795
+ } finally {
796
+ setSaving(false)
797
+ }
798
+ }
799
+
800
+ return (
801
+ <Dialog onOpenChange={value => !value && !saving && onClose()} open={open}>
802
+ <DialogContent className="max-w-lg">
803
+ <DialogHeader>
804
+ <DialogTitle>{isEdit ? c.editTitle : c.createTitle}</DialogTitle>
805
+ <DialogDescription>{isEdit ? c.editDesc : c.createDesc}</DialogDescription>
806
+ </DialogHeader>
807
+
808
+ <form className="grid gap-4" onSubmit={handleSubmit}>
809
+ <Field htmlFor="cron-name" label={c.nameLabel} optional optionalLabel={c.optional}>
810
+ <Input
811
+ autoFocus
812
+ id="cron-name"
813
+ onChange={event => setName(event.target.value)}
814
+ placeholder={c.namePlaceholder}
815
+ value={name}
816
+ />
817
+ </Field>
818
+
819
+ <Field htmlFor="cron-prompt" label={c.promptLabel}>
820
+ <Textarea
821
+ className="min-h-24 font-mono"
822
+ id="cron-prompt"
823
+ onChange={event => setPrompt(event.target.value)}
824
+ placeholder={c.promptPlaceholder}
825
+ value={prompt}
826
+ />
827
+ </Field>
828
+
829
+ <div className="grid items-start gap-4 sm:grid-cols-2">
830
+ <Field htmlFor="cron-frequency" label={c.frequencyLabel}>
831
+ <Select onValueChange={handleSchedulePresetChange} value={schedulePreset}>
832
+ <SelectTrigger className="h-9 rounded-md" id="cron-frequency">
833
+ <SelectValue />
834
+ </SelectTrigger>
835
+ <SelectContent>
836
+ {SCHEDULE_OPTIONS.map(option => (
837
+ <SelectItem key={option.value} value={option.value}>
838
+ {c.scheduleLabels[option.value]}
839
+ </SelectItem>
840
+ ))}
841
+ </SelectContent>
842
+ </Select>
843
+ </Field>
844
+
845
+ <Field htmlFor="cron-deliver" label={c.deliverLabel}>
846
+ <Select onValueChange={setDeliver} value={deliver}>
847
+ <SelectTrigger className="h-9 rounded-md" id="cron-deliver">
848
+ <SelectValue />
849
+ </SelectTrigger>
850
+ <SelectContent>
851
+ {DELIVERY_VALUES.map(value => (
852
+ <SelectItem key={value} value={value}>
853
+ {c.deliveryLabels[value]}
854
+ </SelectItem>
855
+ ))}
856
+ </SelectContent>
857
+ </Select>
858
+ </Field>
859
+ </div>
860
+
861
+ {schedulePreset === 'custom' ? (
862
+ <Field htmlFor="cron-schedule" label={c.customScheduleLabel}>
863
+ <Input
864
+ className="font-mono"
865
+ id="cron-schedule"
866
+ onChange={event => setSchedule(event.target.value)}
867
+ placeholder={c.customPlaceholder}
868
+ value={schedule}
869
+ />
870
+ <FieldHint>{c.customHint}</FieldHint>
871
+ </Field>
872
+ ) : (
873
+ <div className="rounded-md bg-(--ui-bg-quinary) px-3 py-2">
874
+ <div className="flex flex-wrap items-center justify-between gap-2 text-xs">
875
+ <span className="font-medium text-foreground">{scheduleHint}</span>
876
+ <span className="font-mono text-muted-foreground">{schedule}</span>
877
+ </div>
878
+ </div>
879
+ )}
880
+
881
+ {error && (
882
+ <div className="flex items-start gap-2 rounded-md bg-destructive/10 px-3 py-2 text-xs text-destructive">
883
+ <AlertTriangle className="mt-0.5 size-3.5 shrink-0" />
884
+ <span>{error}</span>
885
+ </div>
886
+ )}
887
+
888
+ <DialogFooter>
889
+ <Button disabled={saving} onClick={onClose} type="button" variant="outline">
890
+ {t.common.cancel}
891
+ </Button>
892
+ <Button disabled={saving} type="submit">
893
+ {saving ? t.common.saving : isEdit ? c.saveChanges : c.createAction}
894
+ </Button>
895
+ </DialogFooter>
896
+ </form>
897
+ </DialogContent>
898
+ </Dialog>
899
+ )
900
+ }
901
+
902
+ function Field({
903
+ children,
904
+ htmlFor,
905
+ label,
906
+ optional,
907
+ optionalLabel
908
+ }: {
909
+ children: React.ReactNode
910
+ htmlFor: string
911
+ label: string
912
+ optional?: boolean
913
+ optionalLabel?: string
914
+ }) {
915
+ return (
916
+ <div className="grid gap-1.5">
917
+ <label className="flex items-baseline gap-2 text-xs font-medium text-foreground" htmlFor={htmlFor}>
918
+ {label}
919
+ {optional && <span className="text-[0.65rem] font-normal text-muted-foreground">{optionalLabel}</span>}
920
+ </label>
921
+ {children}
922
+ </div>
923
+ )
924
+ }
925
+
926
+ function FieldHint({ children }: { children: React.ReactNode }) {
927
+ return <p className="text-[0.66rem] leading-4 text-muted-foreground">{children}</p>
928
+ }
929
+
930
+ type EditorState = { mode: 'closed' } | { mode: 'create' } | { job: CronJob; mode: 'edit' }
931
+
932
+ interface EditorValues {
933
+ deliver: string
934
+ name: string
935
+ prompt: string
936
+ schedule: string
937
+ }
938
+
939
+ interface ScheduleOption {
940
+ expr?: string
941
+ value: string
942
+ }