stagent 0.9.6 → 0.11.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 (396) hide show
  1. package/README.md +20 -44
  2. package/dist/cli.js +66 -18
  3. package/docs/.coverage-gaps.json +144 -56
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/agent-intelligence.md +12 -2
  6. package/docs/features/chat.md +40 -5
  7. package/docs/features/cost-usage.md +1 -1
  8. package/docs/features/documents.md +5 -2
  9. package/docs/features/inbox-notifications.md +10 -2
  10. package/docs/features/keyboard-navigation.md +12 -3
  11. package/docs/features/provider-runtimes.md +20 -2
  12. package/docs/features/schedules.md +32 -4
  13. package/docs/features/settings.md +28 -5
  14. package/docs/features/shared-components.md +7 -3
  15. package/docs/features/tables.md +11 -2
  16. package/docs/features/tool-permissions.md +6 -2
  17. package/docs/features/workflows.md +14 -4
  18. package/docs/index.md +1 -1
  19. package/docs/journeys/developer.md +39 -2
  20. package/docs/journeys/personal-use.md +32 -8
  21. package/docs/journeys/power-user.md +45 -14
  22. package/docs/journeys/work-use.md +17 -8
  23. package/docs/manifest.json +15 -15
  24. package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +1691 -0
  25. package/docs/superpowers/plans/2026-04-08-schedule-orchestration.md +2983 -0
  26. package/docs/superpowers/plans/2026-04-11-schedule-maxturns-api-control.md +551 -0
  27. package/docs/superpowers/plans/2026-04-11-task-create-profile-validation.md +864 -0
  28. package/docs/superpowers/plans/2026-04-11-task-runtime-stagent-mcp-injection.md +739 -0
  29. package/docs/superpowers/plans/2026-04-14-chat-command-namespace-refactor.md +1390 -0
  30. package/docs/superpowers/plans/2026-04-14-chat-environment-integration.md +1561 -0
  31. package/docs/superpowers/plans/2026-04-14-chat-polish-bundle-v1.md +1219 -0
  32. package/docs/superpowers/plans/2026-04-14-chat-session-persistence-provider-closeout.md +399 -0
  33. package/docs/superpowers/specs/2026-04-08-chat-sse-resilience-hotfix-design.md +201 -0
  34. package/docs/superpowers/specs/2026-04-08-schedule-orchestration-design.md +371 -0
  35. package/docs/superpowers/specs/2026-04-08-swarm-visibility-design.md +213 -0
  36. package/next.config.mjs +1 -0
  37. package/package.json +3 -2
  38. package/src/__tests__/instrumentation-smoke.test.ts +15 -0
  39. package/src/app/analytics/page.tsx +1 -21
  40. package/src/app/api/chat/conversations/[id]/messages/route.ts +22 -1
  41. package/src/app/api/chat/conversations/[id]/skills/__tests__/activate.test.ts +141 -0
  42. package/src/app/api/chat/conversations/[id]/skills/activate/route.ts +74 -0
  43. package/src/app/api/chat/conversations/[id]/skills/deactivate/route.ts +33 -0
  44. package/src/app/api/chat/export/route.ts +52 -0
  45. package/src/app/api/chat/files/search/route.ts +50 -0
  46. package/src/app/api/diagnostics/chat-streams/route.ts +65 -0
  47. package/src/app/api/environment/rescan-if-stale/__tests__/route.test.ts +45 -0
  48. package/src/app/api/environment/rescan-if-stale/route.ts +23 -0
  49. package/src/app/api/environment/skills/route.ts +13 -0
  50. package/src/app/api/instance/config/route.ts +41 -0
  51. package/src/app/api/instance/init/route.ts +34 -0
  52. package/src/app/api/instance/upgrade/check/route.ts +26 -0
  53. package/src/app/api/instance/upgrade/route.ts +96 -0
  54. package/src/app/api/instance/upgrade/status/route.ts +35 -0
  55. package/src/app/api/memory/route.ts +0 -11
  56. package/src/app/api/notifications/route.ts +4 -2
  57. package/src/app/api/projects/[id]/route.ts +5 -155
  58. package/src/app/api/projects/__tests__/delete-project.test.ts +10 -19
  59. package/src/app/api/schedules/[id]/execute/route.ts +111 -0
  60. package/src/app/api/schedules/[id]/route.ts +9 -1
  61. package/src/app/api/schedules/__tests__/execute-route.test.ts +118 -0
  62. package/src/app/api/schedules/route.ts +3 -12
  63. package/src/app/api/settings/chat/pins/route.ts +94 -0
  64. package/src/app/api/settings/chat/saved-searches/__tests__/route.test.ts +119 -0
  65. package/src/app/api/settings/chat/saved-searches/route.ts +79 -0
  66. package/src/app/api/settings/environment/route.ts +26 -0
  67. package/src/app/api/settings/openai/login/route.ts +22 -0
  68. package/src/app/api/settings/openai/logout/route.ts +7 -0
  69. package/src/app/api/settings/openai/route.ts +21 -1
  70. package/src/app/api/settings/providers/route.ts +35 -8
  71. package/src/app/api/tables/[id]/enrich/__tests__/route.test.ts +153 -0
  72. package/src/app/api/tables/[id]/enrich/plan/route.ts +98 -0
  73. package/src/app/api/tables/[id]/enrich/route.ts +147 -0
  74. package/src/app/api/tables/[id]/enrich/runs/route.ts +25 -0
  75. package/src/app/api/tasks/[id]/execute/route.ts +52 -33
  76. package/src/app/api/tasks/[id]/respond/route.ts +31 -15
  77. package/src/app/api/tasks/[id]/resume/route.ts +24 -3
  78. package/src/app/api/workflows/[id]/resume/route.ts +59 -0
  79. package/src/app/api/workflows/[id]/status/route.ts +22 -8
  80. package/src/app/api/workspace/context/route.ts +2 -0
  81. package/src/app/api/workspace/fix-data-dir/route.ts +81 -0
  82. package/src/app/chat/page.tsx +11 -0
  83. package/src/app/documents/page.tsx +4 -1
  84. package/src/app/inbox/page.tsx +12 -5
  85. package/src/app/layout.tsx +42 -21
  86. package/src/app/page.tsx +0 -2
  87. package/src/app/settings/page.tsx +8 -9
  88. package/src/components/chat/__tests__/capability-banner.test.tsx +38 -0
  89. package/src/components/chat/__tests__/chat-session-provider.test.tsx +573 -0
  90. package/src/components/chat/__tests__/skill-row.test.tsx +91 -0
  91. package/src/components/chat/capability-banner.tsx +68 -0
  92. package/src/components/chat/chat-command-popover.tsx +670 -49
  93. package/src/components/chat/chat-input.tsx +104 -10
  94. package/src/components/chat/chat-message.tsx +12 -3
  95. package/src/components/chat/chat-session-provider.tsx +790 -0
  96. package/src/components/chat/chat-shell.tsx +151 -401
  97. package/src/components/chat/command-tab-bar.tsx +68 -0
  98. package/src/components/chat/conversation-template-picker.tsx +421 -0
  99. package/src/components/chat/help-dialog.tsx +39 -0
  100. package/src/components/chat/skill-composition-conflict-dialog.tsx +96 -0
  101. package/src/components/chat/skill-row.tsx +147 -0
  102. package/src/components/documents/document-browser.tsx +37 -19
  103. package/src/components/instance/__tests__/instance-section.test.tsx +125 -0
  104. package/src/components/instance/instance-section.tsx +382 -0
  105. package/src/components/instance/upgrade-badge.tsx +219 -0
  106. package/src/components/notifications/__tests__/batch-proposal-review.test.tsx +95 -0
  107. package/src/components/notifications/__tests__/notification-item.test.tsx +106 -0
  108. package/src/components/notifications/__tests__/permission-response-actions.test.tsx +70 -0
  109. package/src/components/notifications/batch-proposal-review.tsx +20 -5
  110. package/src/components/notifications/inbox-list.tsx +11 -2
  111. package/src/components/notifications/notification-item.tsx +56 -2
  112. package/src/components/notifications/pending-approval-host.tsx +56 -37
  113. package/src/components/notifications/permission-response-actions.tsx +155 -1
  114. package/src/components/schedules/schedule-create-sheet.tsx +19 -1
  115. package/src/components/schedules/schedule-edit-sheet.tsx +20 -1
  116. package/src/components/schedules/schedule-form.tsx +31 -0
  117. package/src/components/settings/__tests__/providers-runtimes-section.test.tsx +149 -0
  118. package/src/components/settings/auth-method-selector.tsx +19 -4
  119. package/src/components/settings/auth-status-badge.tsx +28 -3
  120. package/src/components/settings/environment-section.tsx +102 -0
  121. package/src/components/settings/openai-chatgpt-auth-control.tsx +278 -0
  122. package/src/components/settings/openai-runtime-section.tsx +7 -1
  123. package/src/components/settings/providers-runtimes-section.tsx +138 -19
  124. package/src/components/shared/__tests__/filter-hint.test.tsx +40 -0
  125. package/src/components/shared/__tests__/saved-searches-manager.test.tsx +147 -0
  126. package/src/components/shared/app-sidebar.tsx +4 -3
  127. package/src/components/shared/command-palette.tsx +266 -7
  128. package/src/components/shared/filter-hint.tsx +70 -0
  129. package/src/components/shared/filter-input.tsx +59 -0
  130. package/src/components/shared/saved-searches-manager.tsx +199 -0
  131. package/src/components/shared/theme-toggle.tsx +5 -24
  132. package/src/components/shared/workspace-indicator.tsx +61 -2
  133. package/src/components/tables/__tests__/table-enrichment-sheet.test.tsx +130 -0
  134. package/src/components/tables/table-create-sheet.tsx +4 -0
  135. package/src/components/tables/table-enrichment-runs.tsx +103 -0
  136. package/src/components/tables/table-enrichment-sheet.tsx +538 -0
  137. package/src/components/tables/table-spreadsheet.tsx +29 -5
  138. package/src/components/tables/table-toolbar.tsx +10 -1
  139. package/src/components/tasks/kanban-board.tsx +1 -0
  140. package/src/components/tasks/kanban-column.tsx +53 -14
  141. package/src/components/tasks/task-bento-grid.tsx +31 -2
  142. package/src/components/tasks/task-card.tsx +29 -3
  143. package/src/components/tasks/task-chip-bar.tsx +54 -1
  144. package/src/components/tasks/task-result-renderer.tsx +1 -1
  145. package/src/components/workflows/delay-step-body.tsx +109 -0
  146. package/src/components/workflows/hooks/use-workflow-status.ts +50 -0
  147. package/src/components/workflows/loop-status-view.tsx +1 -1
  148. package/src/components/workflows/shared/step-result.tsx +78 -0
  149. package/src/components/workflows/shared/workflow-header.tsx +141 -0
  150. package/src/components/workflows/shared/workflow-loading-skeleton.tsx +36 -0
  151. package/src/components/workflows/swarm-dashboard.tsx +2 -15
  152. package/src/components/workflows/views/loop-pattern-view.tsx +137 -0
  153. package/src/components/workflows/views/sequence-pattern-view.tsx +511 -0
  154. package/src/components/workflows/workflow-form-view.tsx +133 -16
  155. package/src/components/workflows/workflow-status-view.tsx +30 -740
  156. package/src/hooks/__tests__/use-chat-autocomplete-tabs.test.ts +47 -0
  157. package/src/hooks/__tests__/use-saved-searches.test.ts +70 -0
  158. package/src/hooks/use-active-skills.ts +110 -0
  159. package/src/hooks/use-chat-autocomplete.ts +120 -7
  160. package/src/hooks/use-enriched-skills.ts +19 -0
  161. package/src/hooks/use-pinned-entries.ts +104 -0
  162. package/src/hooks/use-recent-user-messages.ts +19 -0
  163. package/src/hooks/use-saved-searches.ts +142 -0
  164. package/src/instrumentation-node.ts +94 -0
  165. package/src/instrumentation.ts +4 -48
  166. package/src/lib/agents/__tests__/claude-agent-sdk-options.test.ts +56 -0
  167. package/src/lib/agents/__tests__/claude-agent.test.ts +212 -0
  168. package/src/lib/agents/__tests__/execution-manager.test.ts +1 -27
  169. package/src/lib/agents/__tests__/failure-reason.test.ts +68 -0
  170. package/src/lib/agents/__tests__/learned-context.test.ts +0 -11
  171. package/src/lib/agents/__tests__/learning-session.test.ts +158 -0
  172. package/src/lib/agents/__tests__/pattern-extractor.test.ts +48 -0
  173. package/src/lib/agents/__tests__/task-dispatch.test.ts +166 -0
  174. package/src/lib/agents/__tests__/tool-permissions.test.ts +60 -0
  175. package/src/lib/agents/claude-agent.ts +217 -21
  176. package/src/lib/agents/execution-manager.ts +0 -35
  177. package/src/lib/agents/handoff/bus.ts +2 -2
  178. package/src/lib/agents/learned-context.ts +0 -12
  179. package/src/lib/agents/learning-session.ts +18 -5
  180. package/src/lib/agents/profiles/__tests__/list-fused-profiles.test.ts +110 -0
  181. package/src/lib/agents/profiles/__tests__/registry.test.ts +53 -4
  182. package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +97 -0
  183. package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +36 -0
  184. package/src/lib/agents/profiles/list-fused-profiles.ts +104 -0
  185. package/src/lib/agents/profiles/registry.ts +18 -0
  186. package/src/lib/agents/profiles/types.ts +7 -1
  187. package/src/lib/agents/router.ts +3 -6
  188. package/src/lib/agents/runtime/__tests__/catalog.test.ts +130 -0
  189. package/src/lib/agents/runtime/__tests__/execution-target.test.ts +183 -0
  190. package/src/lib/agents/runtime/__tests__/openai-codex-auth.test.ts +118 -0
  191. package/src/lib/agents/runtime/anthropic-direct.ts +8 -0
  192. package/src/lib/agents/runtime/catalog.ts +121 -0
  193. package/src/lib/agents/runtime/claude-sdk.ts +32 -0
  194. package/src/lib/agents/runtime/codex-app-server-client.ts +11 -5
  195. package/src/lib/agents/runtime/execution-target.ts +456 -0
  196. package/src/lib/agents/runtime/index.ts +4 -0
  197. package/src/lib/agents/runtime/launch-failure.ts +101 -0
  198. package/src/lib/agents/runtime/openai-codex-auth.ts +389 -0
  199. package/src/lib/agents/runtime/openai-codex.ts +64 -60
  200. package/src/lib/agents/runtime/openai-direct.ts +8 -0
  201. package/src/lib/agents/runtime/types.ts +8 -0
  202. package/src/lib/agents/task-dispatch.ts +220 -0
  203. package/src/lib/agents/tool-permissions.ts +16 -1
  204. package/src/lib/book/chapter-mapping.ts +11 -0
  205. package/src/lib/book/content.ts +10 -0
  206. package/src/lib/chat/__tests__/active-skill-injection.test.ts +261 -0
  207. package/src/lib/chat/__tests__/active-streams.test.ts +49 -0
  208. package/src/lib/chat/__tests__/clean-filter-input.test.ts +68 -0
  209. package/src/lib/chat/__tests__/command-tabs.test.ts +68 -0
  210. package/src/lib/chat/__tests__/context-builder-files.test.ts +112 -0
  211. package/src/lib/chat/__tests__/dismissals.test.ts +65 -0
  212. package/src/lib/chat/__tests__/engine-sdk-options.test.ts +117 -0
  213. package/src/lib/chat/__tests__/finalize-safety-net.test.ts +139 -0
  214. package/src/lib/chat/__tests__/reconcile.test.ts +137 -0
  215. package/src/lib/chat/__tests__/skill-conflict.test.ts +35 -0
  216. package/src/lib/chat/__tests__/stream-telemetry.test.ts +151 -0
  217. package/src/lib/chat/__tests__/types.test.ts +28 -0
  218. package/src/lib/chat/active-skills.ts +31 -0
  219. package/src/lib/chat/active-streams.ts +27 -0
  220. package/src/lib/chat/clean-filter-input.ts +30 -0
  221. package/src/lib/chat/codex-engine.ts +46 -24
  222. package/src/lib/chat/command-tabs.ts +61 -0
  223. package/src/lib/chat/context-builder.ts +146 -4
  224. package/src/lib/chat/dismissals.ts +73 -0
  225. package/src/lib/chat/engine.ts +159 -18
  226. package/src/lib/chat/files/__tests__/search.test.ts +135 -0
  227. package/src/lib/chat/files/expand-mention.ts +76 -0
  228. package/src/lib/chat/files/search.ts +99 -0
  229. package/src/lib/chat/reconcile.ts +117 -0
  230. package/src/lib/chat/skill-composition.ts +210 -0
  231. package/src/lib/chat/skill-conflict.ts +105 -0
  232. package/src/lib/chat/stagent-tools.ts +7 -19
  233. package/src/lib/chat/stream-telemetry.ts +137 -0
  234. package/src/lib/chat/suggested-prompts.ts +28 -1
  235. package/src/lib/chat/system-prompt.ts +48 -1
  236. package/src/lib/chat/tool-catalog.ts +35 -4
  237. package/src/lib/chat/tools/__tests__/enrich-table-tool.test.ts +127 -0
  238. package/src/lib/chat/tools/__tests__/profile-tools.test.ts +51 -0
  239. package/src/lib/chat/tools/__tests__/schedule-tools.test.ts +261 -0
  240. package/src/lib/chat/tools/__tests__/settings-tools.test.ts +294 -0
  241. package/src/lib/chat/tools/__tests__/skill-tools.test.ts +474 -0
  242. package/src/lib/chat/tools/__tests__/task-tools.test.ts +399 -0
  243. package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +351 -0
  244. package/src/lib/chat/tools/blueprint-tools.ts +190 -0
  245. package/src/lib/chat/tools/document-tools.ts +29 -13
  246. package/src/lib/chat/tools/helpers.ts +41 -0
  247. package/src/lib/chat/tools/notification-tools.ts +9 -5
  248. package/src/lib/chat/tools/profile-tools.ts +120 -23
  249. package/src/lib/chat/tools/project-tools.ts +33 -0
  250. package/src/lib/chat/tools/schedule-tools.ts +44 -11
  251. package/src/lib/chat/tools/skill-tools.ts +183 -0
  252. package/src/lib/chat/tools/table-tools.ts +71 -0
  253. package/src/lib/chat/tools/task-tools.ts +89 -21
  254. package/src/lib/chat/tools/workflow-tools.ts +275 -32
  255. package/src/lib/chat/types.ts +15 -0
  256. package/src/lib/constants/settings.ts +10 -18
  257. package/src/lib/data/__tests__/clear.test.ts +56 -2
  258. package/src/lib/data/clear.ts +17 -16
  259. package/src/lib/data/delete-project.ts +171 -0
  260. package/src/lib/db/__tests__/bootstrap.test.ts +1 -1
  261. package/src/lib/db/bootstrap.ts +62 -16
  262. package/src/lib/db/index.ts +5 -0
  263. package/src/lib/db/migrations/0009_add_app_instances.sql +25 -0
  264. package/src/lib/db/migrations/0024_add_workflow_resume_at.sql +10 -0
  265. package/src/lib/db/migrations/0025_drop_app_instances.sql +3 -0
  266. package/src/lib/db/migrations/0026_drop_license.sql +3 -0
  267. package/src/lib/db/migrations/meta/_journal.json +21 -0
  268. package/src/lib/db/schema.ts +94 -23
  269. package/src/lib/environment/__tests__/auto-promote.test.ts +132 -0
  270. package/src/lib/environment/__tests__/list-skills-enriched.test.ts +55 -0
  271. package/src/lib/environment/__tests__/skill-enrichment.test.ts +129 -0
  272. package/src/lib/environment/__tests__/skill-recommendations.test.ts +87 -0
  273. package/src/lib/environment/data.ts +9 -0
  274. package/src/lib/environment/list-skills.ts +176 -0
  275. package/src/lib/environment/parsers/__tests__/skill.test.ts +54 -0
  276. package/src/lib/environment/parsers/skill.ts +26 -5
  277. package/src/lib/environment/profile-generator.ts +54 -0
  278. package/src/lib/environment/skill-enrichment.ts +106 -0
  279. package/src/lib/environment/skill-recommendations.ts +66 -0
  280. package/src/lib/environment/workspace-context.ts +13 -1
  281. package/src/lib/filters/__tests__/parse.quoted.test.ts +40 -0
  282. package/src/lib/filters/__tests__/parse.test.ts +135 -0
  283. package/src/lib/filters/parse.ts +86 -0
  284. package/src/lib/import/dedup.ts +4 -54
  285. package/src/lib/instance/__tests__/bootstrap.test.ts +362 -0
  286. package/src/lib/instance/__tests__/detect.test.ts +115 -0
  287. package/src/lib/instance/__tests__/fingerprint.test.ts +48 -0
  288. package/src/lib/instance/__tests__/git-ops.test.ts +95 -0
  289. package/src/lib/instance/__tests__/settings.test.ts +83 -0
  290. package/src/lib/instance/__tests__/upgrade-poller.test.ts +181 -0
  291. package/src/lib/instance/bootstrap.ts +270 -0
  292. package/src/lib/instance/detect.ts +49 -0
  293. package/src/lib/instance/fingerprint.ts +76 -0
  294. package/src/lib/instance/git-ops.ts +95 -0
  295. package/src/lib/instance/settings.ts +61 -0
  296. package/src/lib/instance/types.ts +77 -0
  297. package/src/lib/instance/upgrade-poller.ts +205 -0
  298. package/src/lib/notifications/__tests__/visibility.test.ts +51 -0
  299. package/src/lib/notifications/visibility.ts +33 -0
  300. package/src/lib/schedules/__tests__/collision-check.test.ts +93 -0
  301. package/src/lib/schedules/__tests__/config.test.ts +62 -0
  302. package/src/lib/schedules/__tests__/firing-metrics.test.ts +99 -0
  303. package/src/lib/schedules/__tests__/integration.test.ts +82 -0
  304. package/src/lib/schedules/__tests__/slot-claim.test.ts +242 -0
  305. package/src/lib/schedules/__tests__/tick-scheduler.test.ts +102 -0
  306. package/src/lib/schedules/__tests__/turn-budget.test.ts +228 -0
  307. package/src/lib/schedules/collision-check.ts +105 -0
  308. package/src/lib/schedules/config.ts +53 -0
  309. package/src/lib/schedules/scheduler.ts +236 -17
  310. package/src/lib/schedules/slot-claim.ts +105 -0
  311. package/src/lib/settings/__tests__/openai-auth.test.ts +101 -0
  312. package/src/lib/settings/__tests__/openai-login-manager.test.ts +64 -0
  313. package/src/lib/settings/__tests__/runtime-setup.test.ts +33 -0
  314. package/src/lib/settings/openai-auth.ts +105 -10
  315. package/src/lib/settings/openai-login-manager.ts +260 -0
  316. package/src/lib/settings/runtime-setup.ts +14 -4
  317. package/src/lib/tables/__tests__/enrichment-planner.test.ts +124 -0
  318. package/src/lib/tables/__tests__/enrichment.test.ts +147 -0
  319. package/src/lib/tables/enrichment-planner.ts +454 -0
  320. package/src/lib/tables/enrichment.ts +328 -0
  321. package/src/lib/tables/query-builder.ts +5 -2
  322. package/src/lib/tables/trigger-evaluator.ts +3 -2
  323. package/src/lib/theme.ts +71 -0
  324. package/src/lib/usage/ledger.ts +2 -18
  325. package/src/lib/util/__tests__/similarity.test.ts +106 -0
  326. package/src/lib/util/similarity.ts +77 -0
  327. package/src/lib/utils/format-timestamp.ts +24 -0
  328. package/src/lib/utils/stagent-paths.ts +12 -0
  329. package/src/lib/validators/__tests__/blueprint.test.ts +172 -0
  330. package/src/lib/validators/__tests__/settings.test.ts +10 -0
  331. package/src/lib/validators/blueprint.ts +70 -9
  332. package/src/lib/validators/profile.ts +2 -2
  333. package/src/lib/validators/settings.ts +3 -1
  334. package/src/lib/workflows/__tests__/delay.test.ts +196 -0
  335. package/src/lib/workflows/__tests__/engine.test.ts +8 -0
  336. package/src/lib/workflows/__tests__/loop-executor.test.ts +54 -0
  337. package/src/lib/workflows/__tests__/post-action.test.ts +108 -0
  338. package/src/lib/workflows/blueprints/__tests__/render-prompt.test.ts +124 -0
  339. package/src/lib/workflows/blueprints/instantiator.ts +22 -1
  340. package/src/lib/workflows/blueprints/render-prompt.ts +71 -0
  341. package/src/lib/workflows/blueprints/types.ts +16 -2
  342. package/src/lib/workflows/delay.ts +106 -0
  343. package/src/lib/workflows/engine.ts +212 -7
  344. package/src/lib/workflows/loop-executor.ts +349 -24
  345. package/src/lib/workflows/post-action.ts +91 -0
  346. package/src/lib/workflows/types.ts +166 -1
  347. package/src/test/setup.ts +10 -0
  348. package/src/app/api/license/checkout/route.ts +0 -28
  349. package/src/app/api/license/portal/route.ts +0 -26
  350. package/src/app/api/license/route.ts +0 -89
  351. package/src/app/api/license/usage/route.ts +0 -63
  352. package/src/app/api/marketplace/browse/route.ts +0 -15
  353. package/src/app/api/marketplace/import/route.ts +0 -28
  354. package/src/app/api/marketplace/publish/route.ts +0 -40
  355. package/src/app/api/onboarding/email/route.ts +0 -53
  356. package/src/app/api/settings/telemetry/route.ts +0 -14
  357. package/src/app/api/sync/export/route.ts +0 -54
  358. package/src/app/api/sync/restore/route.ts +0 -37
  359. package/src/app/api/sync/sessions/route.ts +0 -24
  360. package/src/app/auth/callback/route.ts +0 -73
  361. package/src/app/marketplace/page.tsx +0 -19
  362. package/src/components/analytics/analytics-gate-card.tsx +0 -101
  363. package/src/components/marketplace/blueprint-card.tsx +0 -61
  364. package/src/components/marketplace/marketplace-browser.tsx +0 -131
  365. package/src/components/onboarding/email-capture-card.tsx +0 -104
  366. package/src/components/settings/activation-form.tsx +0 -95
  367. package/src/components/settings/cloud-account-section.tsx +0 -147
  368. package/src/components/settings/cloud-sync-section.tsx +0 -155
  369. package/src/components/settings/subscription-section.tsx +0 -410
  370. package/src/components/settings/telemetry-section.tsx +0 -80
  371. package/src/components/shared/premium-gate-overlay.tsx +0 -50
  372. package/src/components/shared/schedule-gate-dialog.tsx +0 -64
  373. package/src/components/shared/upgrade-banner.tsx +0 -112
  374. package/src/hooks/use-supabase-auth.ts +0 -79
  375. package/src/lib/billing/email.ts +0 -54
  376. package/src/lib/billing/products.ts +0 -80
  377. package/src/lib/billing/stripe.ts +0 -101
  378. package/src/lib/cloud/supabase-browser.ts +0 -32
  379. package/src/lib/cloud/supabase-client.ts +0 -56
  380. package/src/lib/license/__tests__/features.test.ts +0 -56
  381. package/src/lib/license/__tests__/key-format.test.ts +0 -88
  382. package/src/lib/license/__tests__/manager.test.ts +0 -64
  383. package/src/lib/license/__tests__/tier-limits.test.ts +0 -79
  384. package/src/lib/license/cloud-validation.ts +0 -60
  385. package/src/lib/license/features.ts +0 -44
  386. package/src/lib/license/key-format.ts +0 -101
  387. package/src/lib/license/limit-check.ts +0 -111
  388. package/src/lib/license/limit-queries.ts +0 -51
  389. package/src/lib/license/manager.ts +0 -345
  390. package/src/lib/license/notifications.ts +0 -59
  391. package/src/lib/license/tier-limits.ts +0 -71
  392. package/src/lib/marketplace/marketplace-client.ts +0 -107
  393. package/src/lib/sync/cloud-sync.ts +0 -235
  394. package/src/lib/telemetry/conversion-events.ts +0 -71
  395. package/src/lib/telemetry/queue.ts +0 -122
  396. package/src/lib/validators/license.ts +0 -33
@@ -0,0 +1,382 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState, type ReactNode } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Badge } from "@/components/ui/badge";
7
+
8
+ interface InstanceConfig {
9
+ instanceId: string;
10
+ branchName: string;
11
+ isPrivateInstance: boolean;
12
+ createdAt: number;
13
+ }
14
+
15
+ interface Guardrails {
16
+ prePushHookInstalled: boolean;
17
+ prePushHookVersion: string;
18
+ pushRemoteBlocked: string[];
19
+ consentStatus: "not_yet" | "enabled" | "declined_permanently";
20
+ firstBootCompletedAt: number | null;
21
+ }
22
+
23
+ interface UpgradeState {
24
+ lastPolledAt: number | null;
25
+ upgradeAvailable: boolean;
26
+ commitsBehind: number;
27
+ lastSuccessfulUpgradeAt: number | null;
28
+ pollFailureCount: number;
29
+ lastPollError: string | null;
30
+ }
31
+
32
+ interface ConfigResponse {
33
+ devMode: boolean;
34
+ config: InstanceConfig | null;
35
+ guardrails: Guardrails | null;
36
+ upgrade: UpgradeState | null;
37
+ }
38
+
39
+ /**
40
+ * Settings → Instance section. Compact horizontal strip with title + actions
41
+ * in a top bar and metadata in a 4-column grid below. On the canonical dev
42
+ * repo (devMode=true) collapses to a single-row notice to avoid pretending
43
+ * the main branch is an instance.
44
+ */
45
+ const STALE_THRESHOLD_MS = 5 * 60 * 1000;
46
+
47
+ export function InstanceSection() {
48
+ const router = useRouter();
49
+ const [state, setState] = useState<ConfigResponse | null>(null);
50
+ const [loading, setLoading] = useState(true);
51
+ const [busy, setBusy] = useState<"check" | "init" | "upgrade" | null>(null);
52
+ const [message, setMessage] = useState<string | null>(null);
53
+
54
+ async function loadConfig() {
55
+ setLoading(true);
56
+ try {
57
+ const res = await fetch("/api/instance/config", { cache: "no-store" });
58
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
59
+ const data = (await res.json()) as ConfigResponse;
60
+ setState(data);
61
+ return data;
62
+ } catch (err) {
63
+ setMessage(err instanceof Error ? err.message : String(err));
64
+ return null;
65
+ } finally {
66
+ setLoading(false);
67
+ }
68
+ }
69
+
70
+ // Silent background refresh — used after auto-check on mount so we don't
71
+ // flicker the whole card back to its loading state.
72
+ async function refreshConfigSilent() {
73
+ try {
74
+ const res = await fetch("/api/instance/config", { cache: "no-store" });
75
+ if (!res.ok) return;
76
+ const data = (await res.json()) as ConfigResponse;
77
+ setState(data);
78
+ } catch {
79
+ // Swallow — this is a best-effort refresh after auto-check.
80
+ }
81
+ }
82
+
83
+ useEffect(() => {
84
+ let cancelled = false;
85
+ (async () => {
86
+ const data = await loadConfig();
87
+ if (cancelled || !data || data.devMode || !data.config) return;
88
+ // If the cached upgrade state is older than 5 minutes, silently force
89
+ // a fresh check. This self-heals after manual `git pull` + merge in
90
+ // the terminal, so users don't see a stale "N updates pending" count.
91
+ const lastPolled = data.upgrade?.lastPolledAt ?? 0;
92
+ const ageMs = Date.now() - lastPolled * 1000;
93
+ if (ageMs > STALE_THRESHOLD_MS) {
94
+ try {
95
+ const res = await fetch("/api/instance/upgrade/check", {
96
+ method: "POST",
97
+ });
98
+ if (res.ok && !cancelled) {
99
+ await refreshConfigSilent();
100
+ }
101
+ } catch {
102
+ // Silent — manual "Check for upgrades" button remains as fallback.
103
+ }
104
+ }
105
+ })();
106
+ return () => {
107
+ cancelled = true;
108
+ };
109
+ }, []);
110
+
111
+ async function checkNow() {
112
+ setBusy("check");
113
+ setMessage(null);
114
+ try {
115
+ const res = await fetch("/api/instance/upgrade/check", { method: "POST" });
116
+ if (res.status === 202) {
117
+ const body = await res.json();
118
+ setMessage(`Check skipped: ${body.skipped ?? body.error ?? "unknown"}`);
119
+ } else if (res.ok) {
120
+ setMessage("Check complete");
121
+ await loadConfig();
122
+ } else {
123
+ throw new Error(`HTTP ${res.status}`);
124
+ }
125
+ } catch (err) {
126
+ setMessage(err instanceof Error ? err.message : String(err));
127
+ } finally {
128
+ setBusy(null);
129
+ }
130
+ }
131
+
132
+ async function startUpgrade() {
133
+ setBusy("upgrade");
134
+ setMessage(null);
135
+ try {
136
+ const res = await fetch("/api/instance/upgrade", { method: "POST" });
137
+ if (!res.ok) {
138
+ const body = await res.json().catch(() => ({}));
139
+ throw new Error(body.error ?? `HTTP ${res.status}`);
140
+ }
141
+ const data = (await res.json()) as { taskId: string };
142
+ router.push(`/tasks/${data.taskId}`);
143
+ } catch (err) {
144
+ setMessage(err instanceof Error ? err.message : String(err));
145
+ setBusy(null);
146
+ }
147
+ }
148
+
149
+ async function reinit() {
150
+ setBusy("init");
151
+ setMessage(null);
152
+ try {
153
+ const res = await fetch("/api/instance/init", { method: "POST" });
154
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
155
+ await loadConfig();
156
+ setMessage("Instance setup re-run complete");
157
+ } catch (err) {
158
+ setMessage(err instanceof Error ? err.message : String(err));
159
+ } finally {
160
+ setBusy(null);
161
+ }
162
+ }
163
+
164
+ if (loading) {
165
+ return (
166
+ <section className="rounded-xl border bg-card px-5 py-4">
167
+ <h2 className="text-base font-semibold">Instance</h2>
168
+ <p className="mt-1 text-sm text-muted-foreground">Loading…</p>
169
+ </section>
170
+ );
171
+ }
172
+
173
+ // Dev mode: main dev repo. Instance bootstrap is gated off. Show a slim
174
+ // single-row notice so the Settings page layout stays stable without
175
+ // misrepresenting the dev repo as an instance.
176
+ if (state?.devMode) {
177
+ return (
178
+ <section className="rounded-xl border bg-card px-5 py-3 flex items-center justify-between gap-4 flex-wrap">
179
+ <div className="flex items-center gap-3">
180
+ <h2 className="text-base font-semibold">Instance</h2>
181
+ <Badge variant="outline" className="text-xs font-normal">
182
+ Dev mode
183
+ </Badge>
184
+ </div>
185
+ <p className="text-xs text-muted-foreground">
186
+ Running on the main dev repo. Instance upgrade features are disabled.
187
+ Set{" "}
188
+ <code className="font-mono text-[11px] px-1 py-0.5 rounded bg-muted">
189
+ STAGENT_INSTANCE_MODE=true
190
+ </code>{" "}
191
+ to test.
192
+ </p>
193
+ </section>
194
+ );
195
+ }
196
+
197
+ const config = state?.config ?? null;
198
+ const guardrails = state?.guardrails ?? null;
199
+ const upgrade = state?.upgrade ?? null;
200
+ const hasConfig = config !== null;
201
+
202
+ // Not-initialized state
203
+ if (!hasConfig) {
204
+ return (
205
+ <section className="rounded-xl border bg-card px-5 py-4 space-y-3">
206
+ <div className="flex items-center justify-between gap-4 flex-wrap">
207
+ <h2 className="text-base font-semibold">Instance</h2>
208
+ <Button
209
+ variant="default"
210
+ size="sm"
211
+ onClick={reinit}
212
+ disabled={busy !== null}
213
+ >
214
+ {busy === "init" ? "Running…" : "Run setup"}
215
+ </Button>
216
+ </div>
217
+ <div className="rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-xs">
218
+ Instance setup incomplete. Run setup to initialize this workspace.
219
+ </div>
220
+ {message && (
221
+ <div className="text-xs text-muted-foreground">{message}</div>
222
+ )}
223
+ </section>
224
+ );
225
+ }
226
+
227
+ const shortId = config!.instanceId.slice(0, 8) + "…";
228
+ const consentLabel = guardrails?.consentStatus ?? "unknown";
229
+ const hookLabel = guardrails?.prePushHookInstalled
230
+ ? `v${guardrails.prePushHookVersion}`
231
+ : "not installed";
232
+ const blockedLabel = guardrails?.pushRemoteBlocked.length
233
+ ? guardrails.pushRemoteBlocked.join(", ")
234
+ : "none";
235
+ const lastCheck = upgrade?.lastPolledAt
236
+ ? new Date(upgrade.lastPolledAt * 1000).toLocaleString()
237
+ : "never";
238
+ const lastUpgrade = upgrade?.lastSuccessfulUpgradeAt
239
+ ? new Date(upgrade.lastSuccessfulUpgradeAt * 1000).toLocaleString()
240
+ : "never";
241
+ const pollFailing = (upgrade?.pollFailureCount ?? 0) > 0;
242
+
243
+ const upgradeAvailable = upgrade?.upgradeAvailable ?? false;
244
+ const upgradeCount = upgrade?.commitsBehind ?? 0;
245
+ const startUpgradeDisabled = busy !== null || !upgradeAvailable;
246
+ const startUpgradeTitle = upgradeAvailable
247
+ ? `Merge ${upgradeCount} upstream commit${upgradeCount === 1 ? "" : "s"} into ${config!.branchName}`
248
+ : "No upgrades available — click 'Check for upgrades' to refresh";
249
+ const statusMessage = pollFailing && upgrade?.lastPollError
250
+ ? upgrade.lastPollError
251
+ : message;
252
+ const statusToneClass = pollFailing
253
+ ? "text-amber-700 dark:text-amber-400"
254
+ : "text-muted-foreground";
255
+
256
+ return (
257
+ <section className="rounded-xl border bg-card">
258
+ <header className="flex items-start justify-between gap-4 px-5 py-3 border-b flex-wrap">
259
+ <div className="min-w-0 space-y-2">
260
+ <div className="flex items-center gap-3 min-w-0 flex-wrap">
261
+ <h2 className="text-base font-semibold">Instance</h2>
262
+ {upgradeAvailable && (
263
+ <Badge
264
+ variant="outline"
265
+ className="text-xs font-normal border-blue-500/40 bg-blue-500/10 text-blue-700 dark:text-blue-400"
266
+ >
267
+ {upgradeCount} update{upgradeCount === 1 ? "" : "s"} available
268
+ </Badge>
269
+ )}
270
+ {pollFailing && (
271
+ <Badge variant="destructive" className="text-xs font-normal">
272
+ Poll failing ({upgrade?.pollFailureCount})
273
+ </Badge>
274
+ )}
275
+ </div>
276
+ <p className="text-xs text-muted-foreground leading-relaxed max-w-prose">
277
+ Pull latest changes from{" "}
278
+ <code className="font-mono text-[11px] px-1 py-0.5 rounded bg-muted">
279
+ main
280
+ </code>{" "}
281
+ into{" "}
282
+ <code className="font-mono text-[11px] px-1 py-0.5 rounded bg-muted">
283
+ {config!.branchName}
284
+ </code>
285
+ . Nothing is pushed automatically.
286
+ </p>
287
+ </div>
288
+ <div className="flex items-center gap-2 shrink-0 flex-wrap">
289
+ <Button
290
+ variant="outline"
291
+ size="sm"
292
+ onClick={checkNow}
293
+ disabled={busy !== null}
294
+ >
295
+ {busy === "check" ? "Checking…" : "Check"}
296
+ </Button>
297
+ <Button
298
+ variant="default"
299
+ size="sm"
300
+ onClick={startUpgrade}
301
+ disabled={startUpgradeDisabled}
302
+ title={startUpgradeTitle}
303
+ >
304
+ {busy === "upgrade"
305
+ ? "Starting…"
306
+ : upgradeAvailable
307
+ ? `Upgrade (${upgradeCount})`
308
+ : "Upgrade"}
309
+ </Button>
310
+ <Button
311
+ variant="ghost"
312
+ size="sm"
313
+ onClick={reinit}
314
+ disabled={busy !== null}
315
+ >
316
+ {busy === "init" ? "Running…" : "Repair setup"}
317
+ </Button>
318
+ </div>
319
+ </header>
320
+
321
+ <dl className="grid grid-cols-2 md:grid-cols-4 gap-x-6 gap-y-2 px-5 py-3 text-sm">
322
+ <Field label="Branch" mono>
323
+ {config!.branchName}
324
+ </Field>
325
+ <Field
326
+ label="Instance ID"
327
+ mono
328
+ title={config!.instanceId}
329
+ >
330
+ {shortId}
331
+ </Field>
332
+ <Field label="Last check">{lastCheck}</Field>
333
+ <Field label="Last upgrade">{lastUpgrade}</Field>
334
+ </dl>
335
+
336
+ <div className="flex items-start justify-between gap-3 border-t px-5 py-2.5 text-[11px]">
337
+ <p className={`leading-relaxed ${statusToneClass}`}>
338
+ {statusMessage ?? (
339
+ upgradeAvailable
340
+ ? `Ready to merge ${upgradeCount} upstream update${upgradeCount === 1 ? "" : "s"}.`
341
+ : `Up to date. Last checked: ${lastCheck}.`
342
+ )}
343
+ </p>
344
+ <p className="shrink-0 text-muted-foreground">
345
+ Repairs local setup without changing data or commits.
346
+ </p>
347
+ </div>
348
+ </section>
349
+ );
350
+ }
351
+
352
+ function Field({
353
+ label,
354
+ children,
355
+ mono,
356
+ truncate,
357
+ title,
358
+ }: {
359
+ label: string;
360
+ children: ReactNode;
361
+ mono?: boolean;
362
+ truncate?: boolean;
363
+ title?: string;
364
+ }) {
365
+ return (
366
+ <div className="min-w-0">
367
+ <dt className="text-[11px] uppercase tracking-wide text-muted-foreground">
368
+ {label}
369
+ </dt>
370
+ <dd
371
+ title={title}
372
+ className={
373
+ "mt-0.5 " +
374
+ (mono ? "font-mono text-xs " : "") +
375
+ (truncate ? "truncate" : "")
376
+ }
377
+ >
378
+ {children}
379
+ </dd>
380
+ </div>
381
+ );
382
+ }
@@ -0,0 +1,219 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { ArrowUpCircle } from "lucide-react";
6
+ import {
7
+ Dialog,
8
+ DialogContent,
9
+ DialogHeader,
10
+ DialogTitle,
11
+ DialogDescription,
12
+ DialogFooter,
13
+ DialogTrigger,
14
+ } from "@/components/ui/dialog";
15
+ import { Button } from "@/components/ui/button";
16
+ import type { UpgradeState } from "@/lib/instance/types";
17
+
18
+ interface InstanceConfig {
19
+ instanceId: string;
20
+ branchName: string;
21
+ isPrivateInstance: boolean;
22
+ createdAt: number;
23
+ }
24
+
25
+ interface ConfigResponse {
26
+ devMode?: boolean;
27
+ config: InstanceConfig | null;
28
+ upgrade: UpgradeState | null;
29
+ }
30
+
31
+ type StatusResponse = UpgradeState & { devMode?: boolean };
32
+
33
+ /**
34
+ * Sidebar upgrade badge + pre-flight modal combined into a single Client
35
+ * Component. Fetches status on mount and every 5 minutes; renders nothing
36
+ * when no upgrade is available. When clicked, opens the pre-flight modal
37
+ * and loads the full config for the fact panel.
38
+ *
39
+ * Combined into one component because Next.js 16's stricter client/server
40
+ * boundary rules reject passing callback props between separately-imported
41
+ * client components unless they're Server Actions. Bundling the two here
42
+ * preserves the spec's separation of concerns at the design level while
43
+ * satisfying the framework.
44
+ */
45
+ export function UpgradeBadge() {
46
+ const router = useRouter();
47
+ const [state, setState] = useState<StatusResponse | null>(null);
48
+ const [open, setOpen] = useState(false);
49
+ const [config, setConfig] = useState<ConfigResponse | null>(null);
50
+ const [loading, setLoading] = useState(false);
51
+ const [starting, setStarting] = useState(false);
52
+ const [error, setError] = useState<string | null>(null);
53
+
54
+ useEffect(() => {
55
+ let cancelled = false;
56
+
57
+ async function fetchStatus() {
58
+ try {
59
+ const res = await fetch("/api/instance/upgrade/status", {
60
+ cache: "no-store",
61
+ });
62
+ if (!res.ok) return;
63
+ const data = (await res.json()) as StatusResponse;
64
+ if (!cancelled) setState(data);
65
+ } catch {
66
+ // Silent — the badge is ambient; status fetch failures should not
67
+ // produce UI noise. Persistent poll failures surface as a warning
68
+ // variant via state.pollFailureCount >= 3.
69
+ }
70
+ }
71
+
72
+ fetchStatus();
73
+ const interval = setInterval(fetchStatus, 5 * 60 * 1000);
74
+ // Refetch when the tab regains focus — picks up DB changes made by the
75
+ // hourly poller or by a manual "Check for upgrades" click while the user
76
+ // was running git commands in the terminal.
77
+ window.addEventListener("focus", fetchStatus);
78
+ return () => {
79
+ cancelled = true;
80
+ clearInterval(interval);
81
+ window.removeEventListener("focus", fetchStatus);
82
+ };
83
+ }, []);
84
+
85
+ useEffect(() => {
86
+ if (!open) return;
87
+ let cancelled = false;
88
+ setLoading(true);
89
+ setError(null);
90
+ (async () => {
91
+ try {
92
+ const res = await fetch("/api/instance/config", { cache: "no-store" });
93
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
94
+ const data = (await res.json()) as ConfigResponse;
95
+ if (!cancelled) setConfig(data);
96
+ } catch (err) {
97
+ if (!cancelled) setError(err instanceof Error ? err.message : String(err));
98
+ } finally {
99
+ if (!cancelled) setLoading(false);
100
+ }
101
+ })();
102
+ return () => {
103
+ cancelled = true;
104
+ };
105
+ }, [open]);
106
+
107
+ async function startUpgrade() {
108
+ setStarting(true);
109
+ setError(null);
110
+ try {
111
+ const res = await fetch("/api/instance/upgrade", { method: "POST" });
112
+ if (!res.ok) {
113
+ const body = await res.json().catch(() => ({}));
114
+ throw new Error(body.error ?? `HTTP ${res.status}`);
115
+ }
116
+ const data = (await res.json()) as { taskId: string };
117
+ setOpen(false);
118
+ router.push(`/tasks/${data.taskId}`);
119
+ } catch (err) {
120
+ setError(err instanceof Error ? err.message : String(err));
121
+ } finally {
122
+ setStarting(false);
123
+ }
124
+ }
125
+
126
+ if (!state || state.devMode || !state.upgradeAvailable) return null;
127
+
128
+ const failing = state.pollFailureCount >= 3;
129
+ const count = state.commitsBehind;
130
+ const label = failing
131
+ ? "Check failing"
132
+ : `${count} update${count === 1 ? "" : "s"}`;
133
+ const tooltip = failing
134
+ ? "Upgrade check failing — click to retry"
135
+ : `${count} upstream update${count === 1 ? "" : "s"} ready to merge`;
136
+ const buttonClass = failing
137
+ ? "h-7 px-2 rounded-md border border-amber-500/40 bg-amber-500/10 text-[11px] font-medium text-amber-700 dark:text-amber-400 hover:bg-amber-500/20 transition-colors cursor-pointer inline-flex items-center gap-1.5 group-data-[collapsible=icon]:hidden"
138
+ : "h-7 px-2 rounded-md border border-blue-500/40 bg-blue-500/10 text-[11px] font-medium text-blue-700 dark:text-blue-400 hover:bg-blue-500/20 transition-colors cursor-pointer inline-flex items-center gap-1.5 group-data-[collapsible=icon]:hidden";
139
+
140
+ const modalUpgrade = config?.upgrade ?? null;
141
+ const modalCount = modalUpgrade?.commitsBehind ?? count;
142
+ const lastUpgradeText = modalUpgrade?.lastSuccessfulUpgradeAt
143
+ ? new Date(modalUpgrade.lastSuccessfulUpgradeAt * 1000).toLocaleString()
144
+ : "never";
145
+
146
+ return (
147
+ <Dialog open={open} onOpenChange={setOpen}>
148
+ <DialogTrigger asChild>
149
+ <button
150
+ type="button"
151
+ aria-label={tooltip}
152
+ title={tooltip}
153
+ className={buttonClass}
154
+ onClick={(e) => {
155
+ e.preventDefault();
156
+ e.stopPropagation();
157
+ setOpen(true);
158
+ }}
159
+ >
160
+ <ArrowUpCircle className="h-3 w-3" aria-hidden />
161
+ <span>{label}</span>
162
+ </button>
163
+ </DialogTrigger>
164
+ <DialogContent className="sm:max-w-lg">
165
+ <DialogHeader>
166
+ <DialogTitle>Upgrade available</DialogTitle>
167
+ <DialogDescription>
168
+ {modalCount} commit{modalCount === 1 ? "" : "s"} ready to merge into{" "}
169
+ <code className="font-mono text-xs">
170
+ {config?.config?.branchName ?? "…"}
171
+ </code>
172
+ </DialogDescription>
173
+ </DialogHeader>
174
+
175
+ {loading && (
176
+ <div className="py-4 text-sm text-muted-foreground">Loading instance state…</div>
177
+ )}
178
+
179
+ {error && (
180
+ <div className="rounded-md border border-destructive/30 bg-destructive/10 p-3 text-sm text-destructive">
181
+ {error}
182
+ </div>
183
+ )}
184
+
185
+ {config && !loading && (
186
+ <div className="space-y-3 py-2">
187
+ <div className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 text-sm">
188
+ <span className="text-muted-foreground">Branch</span>
189
+ <code className="font-mono text-xs">{config.config?.branchName ?? "—"}</code>
190
+ <span className="text-muted-foreground">Data directory</span>
191
+ <code className="font-mono text-xs break-all">
192
+ {config.config?.isPrivateInstance ? "custom" : "default"}
193
+ </code>
194
+ <span className="text-muted-foreground">Commits behind</span>
195
+ <span>{modalCount}</span>
196
+ <span className="text-muted-foreground">Last successful upgrade</span>
197
+ <span>{lastUpgradeText}</span>
198
+ </div>
199
+
200
+ <p className="text-sm text-muted-foreground leading-relaxed">
201
+ Stagent will stash any uncommitted work, merge <code className="font-mono">main</code> into{" "}
202
+ <code className="font-mono">{config.config?.branchName ?? "your branch"}</code>, install any new
203
+ dependencies, and ask you to resolve conflicts if any appear.
204
+ </p>
205
+ </div>
206
+ )}
207
+
208
+ <DialogFooter>
209
+ <Button variant="ghost" onClick={() => setOpen(false)} disabled={starting}>
210
+ Cancel
211
+ </Button>
212
+ <Button onClick={startUpgrade} disabled={loading || starting || !config?.config}>
213
+ {starting ? "Starting…" : "Start upgrade"}
214
+ </Button>
215
+ </DialogFooter>
216
+ </DialogContent>
217
+ </Dialog>
218
+ );
219
+ }
@@ -0,0 +1,95 @@
1
+ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
2
+ import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
3
+
4
+ import { BatchProposalReview } from "@/components/notifications/batch-proposal-review";
5
+
6
+ const { toastError } = vi.hoisted(() => ({
7
+ toastError: vi.fn(),
8
+ }));
9
+
10
+ vi.mock("sonner", () => ({
11
+ toast: {
12
+ error: toastError,
13
+ },
14
+ }));
15
+
16
+ describe("batch proposal review", () => {
17
+ beforeEach(() => {
18
+ vi.clearAllMocks();
19
+ });
20
+
21
+ afterEach(() => {
22
+ vi.unstubAllGlobals();
23
+ });
24
+
25
+ it("optimistically resolves the batch before the request finishes", async () => {
26
+ const onResponded = vi.fn();
27
+ let resolveFetch: ((value: Response) => void) | null = null;
28
+
29
+ vi.stubGlobal(
30
+ "fetch",
31
+ vi.fn().mockImplementation(
32
+ () =>
33
+ new Promise<Response>((resolve) => {
34
+ resolveFetch = resolve;
35
+ })
36
+ )
37
+ );
38
+
39
+ render(
40
+ <BatchProposalReview
41
+ proposalIds={["p1", "p2"]}
42
+ profileIds={["general"]}
43
+ body="Batch summary"
44
+ onResponded={onResponded}
45
+ />
46
+ );
47
+
48
+ fireEvent.click(screen.getByRole("button", { name: /approve all/i }));
49
+
50
+ expect(onResponded).toHaveBeenCalledTimes(1);
51
+
52
+ resolveFetch?.(
53
+ new Response(JSON.stringify({ action: "approve", count: 2 }), {
54
+ status: 200,
55
+ headers: { "Content-Type": "application/json" },
56
+ })
57
+ );
58
+
59
+ await waitFor(() => {
60
+ expect(screen.getByText("2 proposals approved")).toBeInTheDocument();
61
+ });
62
+ });
63
+
64
+ it("restores server truth when the batch request fails", async () => {
65
+ const onResponded = vi.fn();
66
+ const onRequestFailed = vi.fn();
67
+
68
+ vi.stubGlobal(
69
+ "fetch",
70
+ vi.fn().mockRejectedValue(new Error("Batch approval failed"))
71
+ );
72
+
73
+ render(
74
+ <BatchProposalReview
75
+ proposalIds={["p1", "p2"]}
76
+ profileIds={["general"]}
77
+ body="Batch summary"
78
+ onResponded={onResponded}
79
+ onRequestFailed={onRequestFailed}
80
+ />
81
+ );
82
+
83
+ fireEvent.click(screen.getByRole("button", { name: /approve all/i }));
84
+
85
+ expect(onResponded).toHaveBeenCalledTimes(1);
86
+
87
+ await waitFor(() => {
88
+ expect(onRequestFailed).toHaveBeenCalledTimes(1);
89
+ });
90
+ expect(toastError).toHaveBeenCalledWith("Batch approval failed");
91
+ expect(
92
+ screen.getByRole("button", { name: /approve all \(2\)/i })
93
+ ).toBeInTheDocument();
94
+ });
95
+ });