stagent 0.9.5 → 0.10.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 (277) hide show
  1. package/README.md +5 -42
  2. package/dist/cli.js +42 -18
  3. package/docs/.coverage-gaps.json +13 -55
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/provider-runtimes.md +4 -0
  6. package/docs/features/schedules.md +32 -4
  7. package/docs/features/settings.md +28 -5
  8. package/docs/features/tables.md +9 -2
  9. package/docs/features/workflows.md +10 -4
  10. package/docs/journeys/developer.md +15 -1
  11. package/docs/journeys/personal-use.md +21 -4
  12. package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +1691 -0
  13. package/docs/superpowers/plans/2026-04-08-schedule-orchestration.md +2983 -0
  14. package/docs/superpowers/plans/2026-04-11-schedule-maxturns-api-control.md +551 -0
  15. package/docs/superpowers/plans/2026-04-11-task-create-profile-validation.md +864 -0
  16. package/docs/superpowers/plans/2026-04-11-task-runtime-stagent-mcp-injection.md +739 -0
  17. package/docs/superpowers/specs/2026-04-08-chat-sse-resilience-hotfix-design.md +201 -0
  18. package/docs/superpowers/specs/2026-04-08-schedule-orchestration-design.md +371 -0
  19. package/docs/superpowers/specs/2026-04-08-swarm-visibility-design.md +213 -0
  20. package/package.json +3 -2
  21. package/src/__tests__/instrumentation-smoke.test.ts +15 -0
  22. package/src/app/analytics/page.tsx +1 -21
  23. package/src/app/api/chat/conversations/[id]/messages/route.ts +22 -1
  24. package/src/app/api/diagnostics/chat-streams/route.ts +65 -0
  25. package/src/app/api/instance/config/route.ts +41 -0
  26. package/src/app/api/instance/init/route.ts +34 -0
  27. package/src/app/api/instance/upgrade/check/route.ts +26 -0
  28. package/src/app/api/instance/upgrade/route.ts +96 -0
  29. package/src/app/api/instance/upgrade/status/route.ts +35 -0
  30. package/src/app/api/memory/route.ts +0 -11
  31. package/src/app/api/notifications/route.ts +4 -2
  32. package/src/app/api/projects/[id]/route.ts +5 -155
  33. package/src/app/api/projects/__tests__/delete-project.test.ts +10 -19
  34. package/src/app/api/schedules/[id]/execute/route.ts +111 -0
  35. package/src/app/api/schedules/[id]/route.ts +9 -1
  36. package/src/app/api/schedules/__tests__/execute-route.test.ts +118 -0
  37. package/src/app/api/schedules/route.ts +3 -12
  38. package/src/app/api/settings/openai/login/route.ts +22 -0
  39. package/src/app/api/settings/openai/logout/route.ts +7 -0
  40. package/src/app/api/settings/openai/route.ts +21 -1
  41. package/src/app/api/settings/providers/route.ts +35 -8
  42. package/src/app/api/tables/[id]/enrich/__tests__/route.test.ts +153 -0
  43. package/src/app/api/tables/[id]/enrich/plan/route.ts +98 -0
  44. package/src/app/api/tables/[id]/enrich/route.ts +147 -0
  45. package/src/app/api/tables/[id]/enrich/runs/route.ts +25 -0
  46. package/src/app/api/tasks/[id]/execute/route.ts +0 -21
  47. package/src/app/api/workflows/[id]/resume/route.ts +59 -0
  48. package/src/app/api/workflows/[id]/status/route.ts +22 -8
  49. package/src/app/api/workspace/context/route.ts +2 -0
  50. package/src/app/api/workspace/fix-data-dir/route.ts +81 -0
  51. package/src/app/chat/page.tsx +11 -0
  52. package/src/app/inbox/page.tsx +12 -5
  53. package/src/app/layout.tsx +42 -21
  54. package/src/app/page.tsx +0 -2
  55. package/src/app/settings/page.tsx +6 -9
  56. package/src/components/chat/__tests__/chat-session-provider.test.tsx +408 -0
  57. package/src/components/chat/chat-command-popover.tsx +2 -2
  58. package/src/components/chat/chat-input.tsx +2 -3
  59. package/src/components/chat/chat-session-provider.tsx +720 -0
  60. package/src/components/chat/chat-shell.tsx +92 -401
  61. package/src/components/instance/__tests__/instance-section.test.tsx +125 -0
  62. package/src/components/instance/instance-section.tsx +382 -0
  63. package/src/components/instance/upgrade-badge.tsx +219 -0
  64. package/src/components/notifications/__tests__/batch-proposal-review.test.tsx +95 -0
  65. package/src/components/notifications/__tests__/notification-item.test.tsx +106 -0
  66. package/src/components/notifications/batch-proposal-review.tsx +20 -5
  67. package/src/components/notifications/inbox-list.tsx +11 -2
  68. package/src/components/notifications/notification-item.tsx +56 -2
  69. package/src/components/notifications/pending-approval-host.tsx +56 -37
  70. package/src/components/schedules/schedule-create-sheet.tsx +19 -1
  71. package/src/components/schedules/schedule-edit-sheet.tsx +20 -1
  72. package/src/components/schedules/schedule-form.tsx +31 -0
  73. package/src/components/settings/__tests__/providers-runtimes-section.test.tsx +149 -0
  74. package/src/components/settings/auth-method-selector.tsx +19 -4
  75. package/src/components/settings/auth-status-badge.tsx +28 -3
  76. package/src/components/settings/openai-chatgpt-auth-control.tsx +278 -0
  77. package/src/components/settings/openai-runtime-section.tsx +7 -1
  78. package/src/components/settings/providers-runtimes-section.tsx +138 -19
  79. package/src/components/shared/app-sidebar.tsx +4 -3
  80. package/src/components/shared/command-palette.tsx +4 -5
  81. package/src/components/shared/theme-toggle.tsx +5 -24
  82. package/src/components/shared/workspace-indicator.tsx +61 -2
  83. package/src/components/tables/__tests__/table-enrichment-sheet.test.tsx +130 -0
  84. package/src/components/tables/table-create-sheet.tsx +4 -0
  85. package/src/components/tables/table-enrichment-runs.tsx +103 -0
  86. package/src/components/tables/table-enrichment-sheet.tsx +538 -0
  87. package/src/components/tables/table-spreadsheet.tsx +29 -5
  88. package/src/components/tables/table-toolbar.tsx +10 -1
  89. package/src/components/tasks/kanban-board.tsx +1 -0
  90. package/src/components/tasks/kanban-column.tsx +53 -14
  91. package/src/components/tasks/task-bento-grid.tsx +19 -0
  92. package/src/components/tasks/task-card.tsx +26 -3
  93. package/src/components/tasks/task-chip-bar.tsx +24 -0
  94. package/src/components/tasks/task-result-renderer.tsx +1 -1
  95. package/src/components/workflows/delay-step-body.tsx +109 -0
  96. package/src/components/workflows/hooks/use-workflow-status.ts +50 -0
  97. package/src/components/workflows/loop-status-view.tsx +1 -1
  98. package/src/components/workflows/shared/step-result.tsx +78 -0
  99. package/src/components/workflows/shared/workflow-header.tsx +141 -0
  100. package/src/components/workflows/shared/workflow-loading-skeleton.tsx +36 -0
  101. package/src/components/workflows/swarm-dashboard.tsx +2 -15
  102. package/src/components/workflows/views/loop-pattern-view.tsx +137 -0
  103. package/src/components/workflows/views/sequence-pattern-view.tsx +511 -0
  104. package/src/components/workflows/workflow-form-view.tsx +133 -16
  105. package/src/components/workflows/workflow-status-view.tsx +30 -740
  106. package/src/instrumentation-node.ts +94 -0
  107. package/src/instrumentation.ts +4 -48
  108. package/src/lib/agents/__tests__/claude-agent.test.ts +199 -0
  109. package/src/lib/agents/__tests__/execution-manager.test.ts +1 -27
  110. package/src/lib/agents/__tests__/failure-reason.test.ts +68 -0
  111. package/src/lib/agents/__tests__/learned-context.test.ts +0 -11
  112. package/src/lib/agents/__tests__/learning-session.test.ts +158 -0
  113. package/src/lib/agents/__tests__/pattern-extractor.test.ts +48 -0
  114. package/src/lib/agents/claude-agent.ts +155 -18
  115. package/src/lib/agents/execution-manager.ts +0 -35
  116. package/src/lib/agents/learned-context.ts +0 -12
  117. package/src/lib/agents/learning-session.ts +18 -5
  118. package/src/lib/agents/profiles/__tests__/registry.test.ts +6 -4
  119. package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +70 -0
  120. package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +32 -0
  121. package/src/lib/agents/runtime/__tests__/openai-codex-auth.test.ts +118 -0
  122. package/src/lib/agents/runtime/codex-app-server-client.ts +11 -5
  123. package/src/lib/agents/runtime/openai-codex-auth.ts +389 -0
  124. package/src/lib/agents/runtime/openai-codex.ts +29 -60
  125. package/src/lib/agents/runtime/types.ts +8 -0
  126. package/src/lib/book/chapter-mapping.ts +11 -0
  127. package/src/lib/book/content.ts +10 -0
  128. package/src/lib/chat/__tests__/active-streams.test.ts +49 -0
  129. package/src/lib/chat/__tests__/finalize-safety-net.test.ts +139 -0
  130. package/src/lib/chat/__tests__/reconcile.test.ts +137 -0
  131. package/src/lib/chat/__tests__/stream-telemetry.test.ts +151 -0
  132. package/src/lib/chat/active-streams.ts +27 -0
  133. package/src/lib/chat/codex-engine.ts +16 -17
  134. package/src/lib/chat/context-builder.ts +5 -3
  135. package/src/lib/chat/engine.ts +50 -3
  136. package/src/lib/chat/reconcile.ts +117 -0
  137. package/src/lib/chat/stagent-tools.ts +1 -0
  138. package/src/lib/chat/stream-telemetry.ts +132 -0
  139. package/src/lib/chat/suggested-prompts.ts +28 -1
  140. package/src/lib/chat/system-prompt.ts +26 -1
  141. package/src/lib/chat/tool-catalog.ts +2 -1
  142. package/src/lib/chat/tools/__tests__/enrich-table-tool.test.ts +127 -0
  143. package/src/lib/chat/tools/__tests__/schedule-tools.test.ts +261 -0
  144. package/src/lib/chat/tools/__tests__/task-tools.test.ts +352 -0
  145. package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +217 -0
  146. package/src/lib/chat/tools/document-tools.ts +29 -13
  147. package/src/lib/chat/tools/helpers.ts +39 -0
  148. package/src/lib/chat/tools/notification-tools.ts +9 -5
  149. package/src/lib/chat/tools/project-tools.ts +33 -0
  150. package/src/lib/chat/tools/schedule-tools.ts +44 -11
  151. package/src/lib/chat/tools/table-tools.ts +71 -0
  152. package/src/lib/chat/tools/task-tools.ts +84 -20
  153. package/src/lib/chat/tools/workflow-tools.ts +234 -32
  154. package/src/lib/constants/settings.ts +8 -18
  155. package/src/lib/data/__tests__/clear.test.ts +56 -2
  156. package/src/lib/data/clear.ts +20 -15
  157. package/src/lib/data/delete-project.ts +171 -0
  158. package/src/lib/db/__tests__/bootstrap.test.ts +1 -1
  159. package/src/lib/db/bootstrap.ts +45 -16
  160. package/src/lib/db/index.ts +5 -0
  161. package/src/lib/db/migrations/0009_add_app_instances.sql +25 -0
  162. package/src/lib/db/migrations/0024_add_workflow_resume_at.sql +10 -0
  163. package/src/lib/db/migrations/0025_drop_app_instances.sql +3 -0
  164. package/src/lib/db/migrations/0026_drop_license.sql +3 -0
  165. package/src/lib/db/migrations/meta/_journal.json +21 -0
  166. package/src/lib/db/schema.ts +68 -23
  167. package/src/lib/environment/workspace-context.ts +13 -1
  168. package/src/lib/import/dedup.ts +4 -54
  169. package/src/lib/instance/__tests__/bootstrap.test.ts +362 -0
  170. package/src/lib/instance/__tests__/detect.test.ts +115 -0
  171. package/src/lib/instance/__tests__/fingerprint.test.ts +48 -0
  172. package/src/lib/instance/__tests__/git-ops.test.ts +95 -0
  173. package/src/lib/instance/__tests__/settings.test.ts +83 -0
  174. package/src/lib/instance/__tests__/upgrade-poller.test.ts +131 -0
  175. package/src/lib/instance/bootstrap.ts +270 -0
  176. package/src/lib/instance/detect.ts +49 -0
  177. package/src/lib/instance/fingerprint.ts +78 -0
  178. package/src/lib/instance/git-ops.ts +95 -0
  179. package/src/lib/instance/settings.ts +61 -0
  180. package/src/lib/instance/types.ts +77 -0
  181. package/src/lib/instance/upgrade-poller.ts +153 -0
  182. package/src/lib/notifications/__tests__/visibility.test.ts +51 -0
  183. package/src/lib/notifications/visibility.ts +33 -0
  184. package/src/lib/schedules/__tests__/collision-check.test.ts +93 -0
  185. package/src/lib/schedules/__tests__/config.test.ts +62 -0
  186. package/src/lib/schedules/__tests__/firing-metrics.test.ts +99 -0
  187. package/src/lib/schedules/__tests__/integration.test.ts +82 -0
  188. package/src/lib/schedules/__tests__/slot-claim.test.ts +242 -0
  189. package/src/lib/schedules/__tests__/tick-scheduler.test.ts +102 -0
  190. package/src/lib/schedules/__tests__/turn-budget.test.ts +228 -0
  191. package/src/lib/schedules/collision-check.ts +105 -0
  192. package/src/lib/schedules/config.ts +53 -0
  193. package/src/lib/schedules/scheduler.ts +232 -13
  194. package/src/lib/schedules/slot-claim.ts +105 -0
  195. package/src/lib/settings/__tests__/openai-auth.test.ts +101 -0
  196. package/src/lib/settings/__tests__/openai-login-manager.test.ts +64 -0
  197. package/src/lib/settings/__tests__/runtime-setup.test.ts +33 -0
  198. package/src/lib/settings/openai-auth.ts +105 -10
  199. package/src/lib/settings/openai-login-manager.ts +260 -0
  200. package/src/lib/settings/runtime-setup.ts +14 -4
  201. package/src/lib/tables/__tests__/enrichment-planner.test.ts +124 -0
  202. package/src/lib/tables/__tests__/enrichment.test.ts +147 -0
  203. package/src/lib/tables/enrichment-planner.ts +454 -0
  204. package/src/lib/tables/enrichment.ts +328 -0
  205. package/src/lib/tables/query-builder.ts +5 -2
  206. package/src/lib/tables/trigger-evaluator.ts +3 -2
  207. package/src/lib/theme.ts +71 -0
  208. package/src/lib/usage/ledger.ts +2 -18
  209. package/src/lib/util/__tests__/similarity.test.ts +106 -0
  210. package/src/lib/util/similarity.ts +77 -0
  211. package/src/lib/utils/format-timestamp.ts +24 -0
  212. package/src/lib/utils/stagent-paths.ts +12 -0
  213. package/src/lib/validators/__tests__/blueprint.test.ts +172 -0
  214. package/src/lib/validators/__tests__/settings.test.ts +10 -0
  215. package/src/lib/validators/blueprint.ts +70 -9
  216. package/src/lib/validators/profile.ts +2 -2
  217. package/src/lib/validators/settings.ts +3 -1
  218. package/src/lib/workflows/__tests__/delay.test.ts +196 -0
  219. package/src/lib/workflows/__tests__/engine.test.ts +8 -0
  220. package/src/lib/workflows/__tests__/loop-executor.test.ts +54 -0
  221. package/src/lib/workflows/__tests__/post-action.test.ts +108 -0
  222. package/src/lib/workflows/blueprints/instantiator.ts +22 -1
  223. package/src/lib/workflows/blueprints/types.ts +10 -2
  224. package/src/lib/workflows/delay.ts +106 -0
  225. package/src/lib/workflows/engine.ts +207 -4
  226. package/src/lib/workflows/loop-executor.ts +349 -24
  227. package/src/lib/workflows/post-action.ts +91 -0
  228. package/src/lib/workflows/types.ts +166 -1
  229. package/src/app/api/license/checkout/route.ts +0 -28
  230. package/src/app/api/license/portal/route.ts +0 -26
  231. package/src/app/api/license/route.ts +0 -89
  232. package/src/app/api/license/usage/route.ts +0 -63
  233. package/src/app/api/marketplace/browse/route.ts +0 -15
  234. package/src/app/api/marketplace/import/route.ts +0 -28
  235. package/src/app/api/marketplace/publish/route.ts +0 -40
  236. package/src/app/api/onboarding/email/route.ts +0 -53
  237. package/src/app/api/settings/telemetry/route.ts +0 -14
  238. package/src/app/api/sync/export/route.ts +0 -54
  239. package/src/app/api/sync/restore/route.ts +0 -37
  240. package/src/app/api/sync/sessions/route.ts +0 -24
  241. package/src/app/auth/callback/route.ts +0 -73
  242. package/src/app/marketplace/page.tsx +0 -19
  243. package/src/components/analytics/analytics-gate-card.tsx +0 -101
  244. package/src/components/marketplace/blueprint-card.tsx +0 -61
  245. package/src/components/marketplace/marketplace-browser.tsx +0 -131
  246. package/src/components/onboarding/email-capture-card.tsx +0 -104
  247. package/src/components/settings/activation-form.tsx +0 -95
  248. package/src/components/settings/cloud-account-section.tsx +0 -147
  249. package/src/components/settings/cloud-sync-section.tsx +0 -155
  250. package/src/components/settings/subscription-section.tsx +0 -410
  251. package/src/components/settings/telemetry-section.tsx +0 -80
  252. package/src/components/shared/premium-gate-overlay.tsx +0 -50
  253. package/src/components/shared/schedule-gate-dialog.tsx +0 -64
  254. package/src/components/shared/upgrade-banner.tsx +0 -112
  255. package/src/hooks/use-supabase-auth.ts +0 -79
  256. package/src/lib/billing/email.ts +0 -54
  257. package/src/lib/billing/products.ts +0 -80
  258. package/src/lib/billing/stripe.ts +0 -101
  259. package/src/lib/cloud/supabase-browser.ts +0 -32
  260. package/src/lib/cloud/supabase-client.ts +0 -56
  261. package/src/lib/license/__tests__/features.test.ts +0 -56
  262. package/src/lib/license/__tests__/key-format.test.ts +0 -88
  263. package/src/lib/license/__tests__/manager.test.ts +0 -64
  264. package/src/lib/license/__tests__/tier-limits.test.ts +0 -79
  265. package/src/lib/license/cloud-validation.ts +0 -60
  266. package/src/lib/license/features.ts +0 -44
  267. package/src/lib/license/key-format.ts +0 -101
  268. package/src/lib/license/limit-check.ts +0 -111
  269. package/src/lib/license/limit-queries.ts +0 -51
  270. package/src/lib/license/manager.ts +0 -345
  271. package/src/lib/license/notifications.ts +0 -59
  272. package/src/lib/license/tier-limits.ts +0 -71
  273. package/src/lib/marketplace/marketplace-client.ts +0 -107
  274. package/src/lib/sync/cloud-sync.ts +0 -235
  275. package/src/lib/telemetry/conversion-events.ts +0 -71
  276. package/src/lib/telemetry/queue.ts +0 -122
  277. package/src/lib/validators/license.ts +0 -33
@@ -2,7 +2,7 @@
2
2
 
3
3
  import Link from "next/link";
4
4
  import { usePathname, useRouter } from "next/navigation";
5
- import { useEffect, useMemo, useRef, useState } from "react";
5
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
6
6
  import {
7
7
  ArrowUpRight,
8
8
  Inbox,
@@ -108,12 +108,14 @@ function PendingApprovalDetail({
108
108
  selected,
109
109
  overflow,
110
110
  onResponded,
111
+ onRequestFailed,
111
112
  onOpenInbox,
112
113
  onSelect,
113
114
  }: {
114
115
  selected: PendingApprovalPayload;
115
116
  overflow: PendingApprovalPayload[];
116
117
  onResponded: () => void;
118
+ onRequestFailed: () => void;
117
119
  onOpenInbox: () => void;
118
120
  onSelect: (notificationId: string) => void;
119
121
  }) {
@@ -172,6 +174,7 @@ function PendingApprovalDetail({
172
174
  profileIds={parsed.profileIds}
173
175
  body={selected.body ?? ""}
174
176
  onResponded={onResponded}
177
+ onRequestFailed={onRequestFailed}
175
178
  />
176
179
  );
177
180
  })()
@@ -264,6 +267,33 @@ export function PendingApprovalHost() {
264
267
  const router = useRouter();
265
268
  const pathname = usePathname();
266
269
 
270
+ const applySnapshot = useCallback((snapshot: PendingApprovalPayload[]) => {
271
+ const nextItems = dedupePendingApprovals(snapshot);
272
+ const previousIds = new Set(knownIdsRef.current);
273
+ const newestNew = nextItems.find(
274
+ (item) => !previousIds.has(item.notificationId)
275
+ );
276
+
277
+ if (newestNew) {
278
+ setAnnouncement(
279
+ `Permission required for ${buildContextLabel(newestNew)}. ${newestNew.compactSummary}`
280
+ );
281
+ }
282
+
283
+ knownIdsRef.current = nextItems.map((item) => item.notificationId);
284
+ setItems(nextItems);
285
+ }, []);
286
+
287
+ const refreshApprovals = useCallback(async () => {
288
+ const res = await fetch("/api/notifications/pending-approvals", {
289
+ cache: "no-store",
290
+ });
291
+ if (!res.ok) return;
292
+
293
+ const snapshot = (await res.json()) as PendingApprovalPayload[];
294
+ applySnapshot(snapshot);
295
+ }, [applySnapshot]);
296
+
267
297
  const primary = items[0] ?? null;
268
298
  const selected = useMemo(() => {
269
299
  if (!items.length) return null;
@@ -287,51 +317,25 @@ export function PendingApprovalHost() {
287
317
  let pollId: ReturnType<typeof setInterval> | null = null;
288
318
  let eventSource: EventSource | null = null;
289
319
 
290
- function applySnapshot(snapshot: PendingApprovalPayload[]) {
291
- if (cancelled) return;
292
-
293
- const nextItems = dedupePendingApprovals(snapshot);
294
- const previousIds = new Set(knownIdsRef.current);
295
- const newestNew = nextItems.find(
296
- (item) => !previousIds.has(item.notificationId)
297
- );
298
-
299
- if (newestNew) {
300
- setAnnouncement(
301
- `Permission required for ${buildContextLabel(newestNew)}. ${newestNew.compactSummary}`
302
- );
303
- }
304
-
305
- knownIdsRef.current = nextItems.map((item) => item.notificationId);
306
- setItems(nextItems);
307
- }
308
-
309
- async function refresh() {
310
- try {
311
- const res = await fetch("/api/notifications/pending-approvals", {
312
- cache: "no-store",
313
- });
314
- if (!res.ok) return;
315
-
316
- const snapshot = (await res.json()) as PendingApprovalPayload[];
317
- applySnapshot(snapshot);
318
- } catch {
319
- // Fallback refresh should fail quietly.
320
- }
321
- }
322
-
323
320
  const startPolling = () => {
324
321
  if (pollId) return;
325
- pollId = setInterval(refresh, 15_000);
322
+ pollId = setInterval(() => {
323
+ refreshApprovals().catch(() => {
324
+ // Fallback refresh should fail quietly.
325
+ });
326
+ }, 15_000);
326
327
  };
327
328
 
328
- refresh();
329
+ refreshApprovals().catch(() => {
330
+ // Initial refresh should fail quietly.
331
+ });
329
332
 
330
333
  try {
331
334
  eventSource = new EventSource("/api/notifications/pending-approvals/stream");
332
335
  eventSource.onmessage = (event) => {
333
336
  try {
334
337
  const snapshot = JSON.parse(event.data) as PendingApprovalPayload[];
338
+ if (cancelled) return;
335
339
  applySnapshot(snapshot);
336
340
  } catch {
337
341
  startPolling();
@@ -351,7 +355,7 @@ export function PendingApprovalHost() {
351
355
  if (pollId) clearInterval(pollId);
352
356
  eventSource?.close();
353
357
  };
354
- }, []);
358
+ }, [applySnapshot, refreshApprovals]);
355
359
 
356
360
  function removeNotification(notificationId: string) {
357
361
  setItems((current) =>
@@ -453,6 +457,11 @@ export function PendingApprovalHost() {
453
457
  profileIds={parsed.profileIds}
454
458
  body={primary.body ?? ""}
455
459
  onResponded={() => removeNotification(primary.notificationId)}
460
+ onRequestFailed={() => {
461
+ refreshApprovals().catch(() => {
462
+ // Refresh failures are surfaced by the batch review toast.
463
+ });
464
+ }}
456
465
  compact
457
466
  />
458
467
  );
@@ -518,6 +527,11 @@ export function PendingApprovalHost() {
518
527
  selected={selected}
519
528
  overflow={overflowItems}
520
529
  onResponded={() => removeNotification(selected.notificationId)}
530
+ onRequestFailed={() => {
531
+ refreshApprovals().catch(() => {
532
+ // Refresh failures are surfaced by the batch review toast.
533
+ });
534
+ }}
521
535
  onOpenInbox={handleOpenInbox}
522
536
  onSelect={setSelectedId}
523
537
  />
@@ -545,6 +559,11 @@ export function PendingApprovalHost() {
545
559
  selected={selected}
546
560
  overflow={overflowItems}
547
561
  onResponded={() => removeNotification(selected.notificationId)}
562
+ onRequestFailed={() => {
563
+ refreshApprovals().catch(() => {
564
+ // Refresh failures are surfaced by the batch review toast.
565
+ });
566
+ }}
548
567
  onOpenInbox={handleOpenInbox}
549
568
  onSelect={setSelectedId}
550
569
  />
@@ -11,6 +11,7 @@ import {
11
11
  import { Calendar } from "lucide-react";
12
12
  import { toast } from "sonner";
13
13
  import { ScheduleForm, type ScheduleFormValues } from "./schedule-form";
14
+ import type { CronCollisionWarning } from "@/lib/schedules/collision-check";
14
15
 
15
16
  interface ScheduleCreateSheetProps {
16
17
  projects: { id: string; name: string }[];
@@ -27,6 +28,7 @@ export function ScheduleCreateSheet({
27
28
  }: ScheduleCreateSheetProps) {
28
29
  const [loading, setLoading] = useState(false);
29
30
  const [error, setError] = useState<string | null>(null);
31
+ const [warnings, setWarnings] = useState<CronCollisionWarning[]>([]);
30
32
 
31
33
  async function handleSubmit(values: ScheduleFormValues) {
32
34
  setLoading(true);
@@ -58,10 +60,15 @@ export function ScheduleCreateSheet({
58
60
  });
59
61
 
60
62
  if (res.ok) {
63
+ const { warnings: newWarnings } = await res.json();
61
64
  setError(null);
62
- onOpenChange(false);
65
+ setWarnings(newWarnings ?? []);
63
66
  toast.success("Schedule created");
64
67
  onCreated();
68
+ if (!newWarnings || newWarnings.length === 0) {
69
+ onOpenChange(false);
70
+ }
71
+ // Keep sheet open if there are warnings so the user sees the banner
65
72
  } else {
66
73
  const data = await res.json().catch(() => null);
67
74
  setError(data?.error ?? `Failed to create schedule (${res.status})`);
@@ -89,6 +96,17 @@ export function ScheduleCreateSheet({
89
96
 
90
97
  {/* Body — px-6 pb-6 per project convention (SheetContent has NO body padding) */}
91
98
  <div className="px-6 pb-6 overflow-y-auto">
99
+ {warnings.length > 0 && (
100
+ <div className="mb-4 rounded-lg border border-amber-500/40 bg-amber-50 p-3 text-sm">
101
+ <p className="font-medium text-amber-900">
102
+ Overlap detected with: {warnings[0].overlappingSchedules.join(", ")}
103
+ </p>
104
+ <p className="text-amber-800">
105
+ Combined load: ~{warnings[0].estimatedConcurrentSteps} agent steps.
106
+ Schedules will take turns; the last to run may be delayed.
107
+ </p>
108
+ </div>
109
+ )}
92
110
  <ScheduleForm
93
111
  projects={projects}
94
112
  onSubmit={handleSubmit}
@@ -16,6 +16,7 @@ import {
16
16
  type ScheduleFormValues,
17
17
  type ScheduleFormInitialValues,
18
18
  } from "./schedule-form";
19
+ import type { CronCollisionWarning } from "@/lib/schedules/collision-check";
19
20
 
20
21
  interface ScheduleEditSheetProps {
21
22
  scheduleId: string | null;
@@ -49,6 +50,7 @@ export function ScheduleEditSheet({
49
50
  const [loaded, setLoaded] = useState(false);
50
51
  const [loading, setLoading] = useState(false);
51
52
  const [error, setError] = useState<string | null>(null);
53
+ const [warnings, setWarnings] = useState<CronCollisionWarning[]>([]);
52
54
 
53
55
  const fetchSchedule = useCallback(async () => {
54
56
  if (!scheduleId) return;
@@ -63,6 +65,7 @@ export function ScheduleEditSheet({
63
65
  setSchedule(null);
64
66
  setLoaded(false);
65
67
  setError(null);
68
+ setWarnings([]);
66
69
  return;
67
70
  }
68
71
  fetchSchedule();
@@ -102,9 +105,14 @@ export function ScheduleEditSheet({
102
105
  });
103
106
 
104
107
  if (res.ok) {
108
+ const { warnings: newWarnings } = await res.json();
109
+ setWarnings(newWarnings ?? []);
105
110
  toast.success("Schedule updated");
106
- onOpenChange(false);
107
111
  onUpdated();
112
+ if (!newWarnings || newWarnings.length === 0) {
113
+ onOpenChange(false);
114
+ }
115
+ // Keep sheet open if there are warnings so the user sees the banner
108
116
  } else {
109
117
  const data = await res.json().catch(() => null);
110
118
  setError(data?.error ?? `Failed to update schedule (${res.status})`);
@@ -131,6 +139,17 @@ export function ScheduleEditSheet({
131
139
 
132
140
  {/* Body — px-6 pb-6 per project convention (SheetContent has NO body padding) */}
133
141
  <div className="px-6 pb-6 overflow-y-auto">
142
+ {warnings.length > 0 && (
143
+ <div className="mb-4 rounded-lg border border-amber-500/40 bg-amber-50 p-3 text-sm">
144
+ <p className="font-medium text-amber-900">
145
+ Overlap detected with: {warnings[0].overlappingSchedules.join(", ")}
146
+ </p>
147
+ <p className="text-amber-800">
148
+ Combined load: ~{warnings[0].estimatedConcurrentSteps} agent steps.
149
+ Schedules will take turns; the last to run may be delayed.
150
+ </p>
151
+ </div>
152
+ )}
134
153
  {!loaded ? (
135
154
  <div className="space-y-4">
136
155
  <Skeleton className="h-8 w-full" />
@@ -63,6 +63,7 @@ export interface ScheduleFormValues {
63
63
  activeTimezone: string;
64
64
  heartbeatBudgetPerDay: number | "";
65
65
  documentIds: string[];
66
+ maxTurns: number | null;
66
67
  }
67
68
 
68
69
  export interface ScheduleFormInitialValues {
@@ -76,6 +77,7 @@ export interface ScheduleFormInitialValues {
76
77
  recurs: boolean;
77
78
  maxFirings: number | null;
78
79
  expiresAt: string | null;
80
+ maxTurns?: number | null;
79
81
  }
80
82
 
81
83
  interface ScheduleFormProps {
@@ -140,6 +142,7 @@ export function ScheduleForm({
140
142
  const [expiresInHours, setExpiresInHours] = useState<number | "">(
141
143
  initialValues ? "" : ""
142
144
  );
145
+ const [maxTurns, setMaxTurns] = useState<number | null>(initialValues?.maxTurns ?? null);
143
146
  const [profiles, setProfiles] = useState<ProfileOption[]>([]);
144
147
 
145
148
  // NL schedule input state
@@ -279,6 +282,7 @@ export function ScheduleForm({
279
282
  activeTimezone,
280
283
  heartbeatBudgetPerDay,
281
284
  documentIds: [...selectedDocIds],
285
+ maxTurns,
282
286
  });
283
287
  }
284
288
 
@@ -507,6 +511,13 @@ export function ScheduleForm({
507
511
  ? "Extra instructions appended to the heartbeat evaluation"
508
512
  : "Instructions for each execution"}
509
513
  </p>
514
+ {scheduleType === "scheduled" && (
515
+ <p className="text-muted-foreground text-xs">
516
+ Note: writing &quot;MAX N turns&quot; in your prompt is a hint to the model,
517
+ not a runtime limit. Use <strong>Max agent steps</strong> below to enforce
518
+ a budget.
519
+ </p>
520
+ )}
510
521
  </div>
511
522
 
512
523
  {/* Natural Language Schedule Input */}
@@ -640,6 +651,26 @@ export function ScheduleForm({
640
651
  )}
641
652
  </div>
642
653
 
654
+ {/* Max agent steps */}
655
+ <div className="space-y-2">
656
+ <Label htmlFor="max-turns">Max agent steps per run</Label>
657
+ <Input
658
+ id="max-turns"
659
+ type="number"
660
+ min={1}
661
+ max={10000}
662
+ placeholder="Inherits global default"
663
+ value={maxTurns ?? ""}
664
+ onChange={(e) =>
665
+ setMaxTurns(e.target.value ? parseInt(e.target.value, 10) : null)
666
+ }
667
+ />
668
+ <p className="text-muted-foreground text-xs">
669
+ One step = one agent action (message, tool call, or sub-response). Most
670
+ schedules use 50–500 steps; heavy research runs 2,000+.
671
+ </p>
672
+ </div>
673
+
643
674
  {/* Project */}
644
675
  {projects.length > 0 && (
645
676
  <div className="space-y-2">
@@ -0,0 +1,149 @@
1
+ import { render, screen, waitFor } from "@testing-library/react";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+
4
+ import { ProvidersAndRuntimesSection } from "@/components/settings/providers-runtimes-section";
5
+
6
+ describe("providers and runtimes section", () => {
7
+ beforeEach(() => {
8
+ vi.clearAllMocks();
9
+ vi.stubGlobal("open", vi.fn());
10
+ vi.stubGlobal(
11
+ "fetch",
12
+ vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
13
+ const url = String(input);
14
+ const method = init?.method ?? "GET";
15
+
16
+ if (url === "/api/settings/providers" && method === "GET") {
17
+ return {
18
+ ok: true,
19
+ json: async () => ({
20
+ providers: {
21
+ anthropic: {
22
+ configured: false,
23
+ authMethod: "api_key",
24
+ hasKey: false,
25
+ apiKeySource: "unknown",
26
+ dualBilling: false,
27
+ runtimes: [
28
+ {
29
+ runtimeId: "claude-code",
30
+ label: "Claude Code",
31
+ providerId: "anthropic",
32
+ configured: false,
33
+ authMethod: "none",
34
+ apiKeySource: "unknown",
35
+ billingMode: "usage",
36
+ },
37
+ {
38
+ runtimeId: "anthropic-direct",
39
+ label: "Anthropic Direct API",
40
+ providerId: "anthropic",
41
+ configured: false,
42
+ authMethod: "none",
43
+ apiKeySource: "unknown",
44
+ billingMode: "usage",
45
+ },
46
+ ],
47
+ },
48
+ openai: {
49
+ configured: true,
50
+ authMethod: "oauth",
51
+ hasKey: true,
52
+ apiKeySource: "env",
53
+ oauthConnected: false,
54
+ account: null,
55
+ rateLimits: null,
56
+ login: {
57
+ phase: "idle",
58
+ loginId: null,
59
+ authUrl: null,
60
+ account: null,
61
+ rateLimits: null,
62
+ error: null,
63
+ startedAt: null,
64
+ updatedAt: new Date("2026-04-10T15:00:00.000Z").toISOString(),
65
+ },
66
+ dualBilling: false,
67
+ runtimes: [
68
+ {
69
+ runtimeId: "openai-codex-app-server",
70
+ label: "OpenAI Codex App Server",
71
+ providerId: "openai",
72
+ configured: false,
73
+ authMethod: "oauth",
74
+ apiKeySource: "oauth",
75
+ billingMode: "usage",
76
+ },
77
+ {
78
+ runtimeId: "openai-direct",
79
+ label: "OpenAI Direct API",
80
+ providerId: "openai",
81
+ configured: true,
82
+ authMethod: "api_key",
83
+ apiKeySource: "env",
84
+ billingMode: "usage",
85
+ },
86
+ ],
87
+ },
88
+ },
89
+ routingPreference: "quality",
90
+ configuredProviderCount: 1,
91
+ }),
92
+ };
93
+ }
94
+
95
+ if (url === "/api/settings/openai/login" && method === "POST") {
96
+ return {
97
+ ok: true,
98
+ json: async () => ({
99
+ phase: "pending",
100
+ loginId: "login-1",
101
+ authUrl: "https://auth.openai.com/log-in",
102
+ account: null,
103
+ rateLimits: null,
104
+ error: null,
105
+ startedAt: new Date("2026-04-10T15:01:00.000Z").toISOString(),
106
+ updatedAt: new Date("2026-04-10T15:01:00.000Z").toISOString(),
107
+ }),
108
+ };
109
+ }
110
+
111
+ throw new Error(`Unexpected fetch: ${url}`);
112
+ })
113
+ );
114
+ });
115
+
116
+ afterEach(() => {
117
+ vi.unstubAllGlobals();
118
+ });
119
+
120
+ it("shows partial OpenAI setup state when ChatGPT auth is selected but not connected", async () => {
121
+ render(<ProvidersAndRuntimesSection />);
122
+
123
+ await waitFor(() => {
124
+ expect(screen.getByText("Direct API only")).toBeInTheDocument();
125
+ });
126
+
127
+ expect(
128
+ screen.getByText("Codex App Server needs ChatGPT sign-in. OpenAI Direct API remains active.")
129
+ ).toBeInTheDocument();
130
+ expect(screen.getAllByText("Sign in with ChatGPT")).toHaveLength(2);
131
+ });
132
+
133
+ it("updates the provider row immediately when ChatGPT sign-in starts", async () => {
134
+ render(<ProvidersAndRuntimesSection />);
135
+
136
+ const signInButton = await screen.findByRole("button", {
137
+ name: "Sign in with ChatGPT",
138
+ });
139
+ signInButton.click();
140
+
141
+ await waitFor(() => {
142
+ expect(
143
+ screen.getByText("Waiting for ChatGPT sign-in. OpenAI Direct API remains active.")
144
+ ).toBeInTheDocument();
145
+ });
146
+
147
+ expect(screen.getAllByText("Waiting for ChatGPT sign-in")).toHaveLength(2);
148
+ });
149
+ });
@@ -4,13 +4,22 @@ import { Key, Shield } from "lucide-react";
4
4
  import { cn } from "@/lib/utils";
5
5
  import type { AuthMethod } from "@/lib/constants/settings";
6
6
 
7
+ interface AuthMethodOption {
8
+ id: AuthMethod;
9
+ icon: typeof Key;
10
+ title: string;
11
+ description: string;
12
+ }
13
+
7
14
  interface AuthMethodSelectorProps {
8
15
  value: AuthMethod;
9
16
  onChange: (method: AuthMethod) => void;
10
17
  recommendedMethod?: AuthMethod | null;
18
+ label?: string;
19
+ options?: AuthMethodOption[];
11
20
  }
12
21
 
13
- const methods = [
22
+ const defaultMethods = [
14
23
  {
15
24
  id: "api_key" as const,
16
25
  icon: Key,
@@ -25,12 +34,18 @@ const methods = [
25
34
  },
26
35
  ];
27
36
 
28
- export function AuthMethodSelector({ value, onChange, recommendedMethod }: AuthMethodSelectorProps) {
37
+ export function AuthMethodSelector({
38
+ value,
39
+ onChange,
40
+ recommendedMethod,
41
+ label = "Authentication Method",
42
+ options = defaultMethods,
43
+ }: AuthMethodSelectorProps) {
29
44
  return (
30
45
  <div className="space-y-2">
31
- <p className="text-sm font-medium">Authentication Method</p>
46
+ <p className="text-sm font-medium">{label}</p>
32
47
  <div className="grid grid-cols-2 gap-3">
33
- {methods.map((method) => {
48
+ {options.map((method) => {
34
49
  const Icon = method.icon;
35
50
  const isSelected = value === method.id;
36
51
  return (
@@ -1,21 +1,30 @@
1
1
  "use client";
2
2
 
3
3
  import { Badge } from "@/components/ui/badge";
4
- import type { ApiKeySource } from "@/lib/constants/settings";
4
+ import type { ApiKeySource, AuthMethod } from "@/lib/constants/settings";
5
5
 
6
6
  interface AuthStatusBadgeProps {
7
7
  connected: boolean;
8
8
  apiKeySource: ApiKeySource;
9
+ authMethod?: AuthMethod | "none";
10
+ oauthLabel?: string;
11
+ oauthConnected?: boolean;
9
12
  }
10
13
 
11
14
  const sourceLabels: Record<ApiKeySource, string> = {
12
15
  db: "Managed API Key",
13
16
  env: "Environment Variable",
14
- oauth: "OAuth (Claude Max/Pro)",
17
+ oauth: "OAuth",
15
18
  unknown: "Unknown",
16
19
  };
17
20
 
18
- export function AuthStatusBadge({ connected, apiKeySource }: AuthStatusBadgeProps) {
21
+ export function AuthStatusBadge({
22
+ connected,
23
+ apiKeySource,
24
+ authMethod,
25
+ oauthLabel = "OAuth",
26
+ oauthConnected,
27
+ }: AuthStatusBadgeProps) {
19
28
  if (!connected && apiKeySource === "unknown") {
20
29
  return (
21
30
  <Badge variant="outline" className="border-warning/50 text-warning">
@@ -32,6 +41,22 @@ export function AuthStatusBadge({ connected, apiKeySource }: AuthStatusBadgeProp
32
41
  );
33
42
  }
34
43
 
44
+ if (connected && authMethod === "oauth" && oauthConnected === false) {
45
+ return (
46
+ <Badge variant="outline" className="border-status-warning/50 text-status-warning">
47
+ Direct API only
48
+ </Badge>
49
+ );
50
+ }
51
+
52
+ if (connected && authMethod === "oauth" && (oauthConnected ?? true)) {
53
+ return (
54
+ <Badge variant="outline" className="border-success/50 text-success">
55
+ Connected via {oauthLabel}
56
+ </Badge>
57
+ );
58
+ }
59
+
35
60
  if (apiKeySource === "unknown") {
36
61
  return (
37
62
  <Badge variant="outline" className="border-success/50 text-success">