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
@@ -5,21 +5,8 @@ import { Brain, GitBranch, MessageSquareMore, RotateCcw } from "lucide-react";
5
5
  import { toast } from "sonner";
6
6
  import { Badge } from "@/components/ui/badge";
7
7
  import { Button } from "@/components/ui/button";
8
- import { ExpandableResult } from "./workflow-status-view";
9
- import type { SwarmConfig } from "@/lib/workflows/types";
10
-
11
- interface StepWithState {
12
- id: string;
13
- name: string;
14
- prompt: string;
15
- state: {
16
- stepId: string;
17
- status: string;
18
- taskId?: string;
19
- result?: string;
20
- error?: string;
21
- };
22
- }
8
+ import { ExpandableResult } from "./shared/step-result";
9
+ import type { StepWithState, SwarmConfig } from "@/lib/workflows/types";
23
10
 
24
11
  interface SwarmDashboardProps {
25
12
  workflowId: string;
@@ -0,0 +1,137 @@
1
+ "use client";
2
+
3
+ import { useCallback, useMemo, useState } from "react";
4
+ import { Card, CardContent } from "@/components/ui/card";
5
+ import { toast } from "sonner";
6
+ import { LoopStatusView } from "../loop-status-view";
7
+ import { WorkflowFullOutput } from "../workflow-full-output";
8
+ import { WorkflowHeader } from "../shared/workflow-header";
9
+ import type {
10
+ WorkflowStatusResponse,
11
+ } from "@/lib/workflows/types";
12
+
13
+ /**
14
+ * Loop workflow subview. Consumes the `pattern: "loop"` arm of the status
15
+ * API discriminated union. Delegates iteration rendering to the existing
16
+ * LoopStatusView component; adds the loop-prompt display and the Full Output
17
+ * sheet (which reads from `loopState.iterations[].result` rather than from
18
+ * `steps[].state.result` — the latter doesn't exist on the loop arm).
19
+ *
20
+ * Per TDR-031: this is the only place in the view layer that is allowed to
21
+ * read `data.loopState`, because it is the only place narrowed to the loop
22
+ * arm.
23
+ *
24
+ * The Full Output sheet fix is the spec's headline behavior change: before
25
+ * this refactor, `completedStepOutputs` was computed in the god component
26
+ * BEFORE the pattern dispatch, so loop workflows either crashed (pre-PR #6)
27
+ * or got an empty array (post-PR #6). Now the loop subview reads iterations
28
+ * directly, so a completed table enrichment workflow actually shows its
29
+ * per-iteration outputs.
30
+ */
31
+ export function LoopPatternView({
32
+ data,
33
+ onRefresh,
34
+ onRequestDelete,
35
+ }: {
36
+ data: Extract<WorkflowStatusResponse, { pattern: "loop" }>;
37
+ onRefresh: () => Promise<void>;
38
+ onRequestDelete: () => void;
39
+ }) {
40
+ const [executing, setExecuting] = useState(false);
41
+
42
+ const handleExecute = useCallback(async () => {
43
+ setExecuting(true);
44
+ try {
45
+ const res = await fetch(`/api/workflows/${data.id}/execute`, { method: "POST" });
46
+ if (res.ok) {
47
+ toast.success("Workflow started");
48
+ await onRefresh();
49
+ } else {
50
+ const err = await res.json().catch(() => null);
51
+ toast.error(err?.error ?? "Failed to start workflow");
52
+ }
53
+ } finally {
54
+ setExecuting(false);
55
+ }
56
+ }, [data.id, onRefresh]);
57
+
58
+ const handleRerun = useCallback(async () => {
59
+ setExecuting(true);
60
+ try {
61
+ const res = await fetch(`/api/workflows/${data.id}/execute`, { method: "POST" });
62
+ if (res.ok) {
63
+ toast.success("Workflow re-started");
64
+ await onRefresh();
65
+ } else {
66
+ const err = await res.json().catch(() => null);
67
+ toast.error(err?.error ?? "Failed to re-run workflow");
68
+ }
69
+ } finally {
70
+ setExecuting(false);
71
+ }
72
+ }, [data.id, onRefresh]);
73
+
74
+ // Loop workflows expose their completed outputs as `loopState.iterations[]`
75
+ // rather than `steps[].state`. Map each completed iteration to the shape
76
+ // `WorkflowFullOutput` expects. This is the bug fix — previously the Full
77
+ // Output sheet was silently empty for every table enrichment run because
78
+ // the old god component only read from `steps[].state` (which doesn't
79
+ // exist on loop responses).
80
+ const completedIterationOutputs = useMemo(() => {
81
+ const iterations = data.loopState?.iterations ?? [];
82
+ return iterations
83
+ .filter((iter) => iter.status === "completed" && iter.result && iter.result.trim() !== "")
84
+ .map((iter) => ({
85
+ name: `Iteration ${iter.iteration}`,
86
+ result: iter.result!,
87
+ }));
88
+ }, [data.loopState]);
89
+
90
+ const loopPromptText = data.steps[0]?.prompt;
91
+ const showFullOutput =
92
+ data.status === "completed" && completedIterationOutputs.length > 0;
93
+
94
+ return (
95
+ <div className="space-y-6">
96
+ <Card>
97
+ <WorkflowHeader
98
+ data={data}
99
+ executing={executing}
100
+ // Loop workflows have their own start/pause controls inside
101
+ // LoopStatusView — the header's Execute button would be redundant
102
+ // and confusing.
103
+ canExecute={false}
104
+ onExecute={handleExecute}
105
+ onRerun={handleRerun}
106
+ onDelete={onRequestDelete}
107
+ />
108
+ <CardContent>
109
+ {loopPromptText && (
110
+ <div className="mb-4">
111
+ <p className="text-xs font-medium text-muted-foreground mb-1.5">Loop Prompt</p>
112
+ <div className="rounded-md border bg-muted/50 p-3">
113
+ <p className="text-sm whitespace-pre-wrap">{loopPromptText}</p>
114
+ </div>
115
+ </div>
116
+ )}
117
+ {data.loopConfig && (
118
+ <LoopStatusView
119
+ workflowId={data.id}
120
+ workflowStatus={data.status}
121
+ loopConfig={data.loopConfig}
122
+ loopState={data.loopState ?? null}
123
+ onRefresh={() => void onRefresh()}
124
+ />
125
+ )}
126
+ </CardContent>
127
+ </Card>
128
+
129
+ {showFullOutput && (
130
+ <WorkflowFullOutput
131
+ workflowName={data.name}
132
+ steps={completedIterationOutputs}
133
+ />
134
+ )}
135
+ </div>
136
+ );
137
+ }
@@ -0,0 +1,511 @@
1
+ "use client";
2
+
3
+ import { useCallback, useState } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { Badge } from "@/components/ui/badge";
6
+ import { Button } from "@/components/ui/button";
7
+ import { Card, CardContent } from "@/components/ui/card";
8
+ import { Checkbox } from "@/components/ui/checkbox";
9
+ import {
10
+ CheckCircle,
11
+ Circle,
12
+ Loader2,
13
+ XCircle,
14
+ ShieldQuestion,
15
+ Clock3,
16
+ GitBranch,
17
+ MessageSquareMore,
18
+ FileText,
19
+ Paperclip,
20
+ ArrowRight,
21
+ } from "lucide-react";
22
+ import { toast } from "sonner";
23
+ import { SwarmDashboard } from "../swarm-dashboard";
24
+ import { WorkflowFullOutput } from "../workflow-full-output";
25
+ import { DelayStepBody } from "../delay-step-body";
26
+ import { WorkflowHeader } from "../shared/workflow-header";
27
+ import { ExpandableResult, DocumentList } from "../shared/step-result";
28
+ import type {
29
+ WorkflowStatusResponse,
30
+ WorkflowStatusDocument,
31
+ StepWithState,
32
+ } from "@/lib/workflows/types";
33
+
34
+ const stepStatusIcons: Record<string, React.ReactNode> = {
35
+ pending: <Circle className="h-4 w-4 text-muted-foreground" />,
36
+ running: <Loader2 className="h-4 w-4 text-status-running animate-spin" />,
37
+ completed: <CheckCircle className="h-4 w-4 text-status-completed" />,
38
+ failed: <XCircle className="h-4 w-4 text-destructive" />,
39
+ waiting_approval: <ShieldQuestion className="h-4 w-4 text-status-warning" />,
40
+ waiting_dependencies: <Clock3 className="h-4 w-4 text-status-warning" />,
41
+ delayed: <Clock3 className="h-4 w-4 text-status-warning" />,
42
+ };
43
+
44
+ /**
45
+ * Non-loop workflow subview. Consumes the sequence/parallel/swarm/
46
+ * planner-executor/checkpoint arm of the status API discriminated union.
47
+ * Renders either a sequential step list (with delay-step support), a parallel
48
+ * branches + synthesis layout, or delegates to SwarmDashboard — all three
49
+ * share the same `steps: StepWithState[]` shape, so branching stays inside
50
+ * this subview.
51
+ *
52
+ * Optimistic Execute/Rerun state is owned here so the Execute button can flip
53
+ * immediately without a round-trip through the router. The hook's `setData`
54
+ * is the mechanism for pushing optimistic state back into the shared polling
55
+ * cache; subsequent poll ticks will overwrite it with authoritative data.
56
+ *
57
+ * Per TDR-031, this subview never touches `.state` on anything except
58
+ * `StepWithState` (which by the union's type guarantees `.state` is present).
59
+ */
60
+ export function SequencePatternView({
61
+ data,
62
+ setData,
63
+ onRefresh,
64
+ onRequestDelete,
65
+ }: {
66
+ data: Extract<WorkflowStatusResponse, { pattern: Exclude<WorkflowStatusResponse["pattern"], "loop"> }>;
67
+ setData: (updater: (current: WorkflowStatusResponse | null) => WorkflowStatusResponse | null) => void;
68
+ onRefresh: () => Promise<void>;
69
+ onRequestDelete: () => void;
70
+ }) {
71
+ const [executing, setExecuting] = useState(false);
72
+
73
+ const handleExecute = useCallback(async () => {
74
+ setExecuting(true);
75
+ // Optimistic update — immediately show "active" status so the UI doesn't
76
+ // feel laggy while the POST and next poll tick complete.
77
+ setData((current) => {
78
+ if (!current || current.pattern === "loop") return current;
79
+ return {
80
+ ...current,
81
+ status: "active",
82
+ steps: current.steps.map((step, index): StepWithState => {
83
+ if (current.pattern === "swarm") {
84
+ const lastIndex = current.steps.length - 1;
85
+ return {
86
+ ...step,
87
+ state: {
88
+ ...step.state,
89
+ status:
90
+ index === 0
91
+ ? "running"
92
+ : index === lastIndex
93
+ ? "waiting_dependencies"
94
+ : "pending",
95
+ },
96
+ };
97
+ }
98
+ if (current.pattern === "parallel") {
99
+ const isJoin = !!step.dependsOn?.length;
100
+ return {
101
+ ...step,
102
+ state: {
103
+ ...step.state,
104
+ status: isJoin
105
+ ? "waiting_dependencies"
106
+ : index === 0
107
+ ? "running"
108
+ : "pending",
109
+ },
110
+ };
111
+ }
112
+ return index === 0
113
+ ? { ...step, state: { ...step.state, status: "running" } }
114
+ : step;
115
+ }),
116
+ };
117
+ });
118
+ try {
119
+ const res = await fetch(`/api/workflows/${data.id}/execute`, { method: "POST" });
120
+ if (res.ok) {
121
+ toast.success("Workflow started");
122
+ await onRefresh();
123
+ } else {
124
+ const err = await res.json().catch(() => null);
125
+ toast.error(err?.error ?? "Failed to start workflow");
126
+ await onRefresh(); // Revert optimistic update on failure
127
+ }
128
+ } finally {
129
+ setExecuting(false);
130
+ }
131
+ }, [data.id, onRefresh, setData]);
132
+
133
+ const handleRerun = useCallback(async () => {
134
+ setExecuting(true);
135
+ try {
136
+ const res = await fetch(`/api/workflows/${data.id}/execute`, { method: "POST" });
137
+ if (res.ok) {
138
+ toast.success("Workflow re-started");
139
+ await onRefresh();
140
+ } else {
141
+ const err = await res.json().catch(() => null);
142
+ toast.error(err?.error ?? "Failed to re-run workflow");
143
+ }
144
+ } finally {
145
+ setExecuting(false);
146
+ }
147
+ }, [data.id, onRefresh]);
148
+
149
+ // At this point on the non-loop arm, `state` is guaranteed present — no
150
+ // optional chaining needed. This is the AC that PR #6's optional chaining
151
+ // patched; the discriminated union makes the patch unnecessary.
152
+ const completedStepOutputs = data.steps
153
+ .filter((s) => s.state.result && s.state.status === "completed")
154
+ .map((s) => ({ name: s.name, result: s.state.result! }));
155
+
156
+ const hasStepDocs = !!data.stepDocuments && Object.keys(data.stepDocuments).length > 0;
157
+ const hasParentDocs = !!data.parentDocuments && data.parentDocuments.length > 0;
158
+
159
+ const parallelBranches =
160
+ data.pattern === "parallel"
161
+ ? data.steps.filter((step) => !step.dependsOn?.length)
162
+ : [];
163
+ const synthesisStep =
164
+ data.pattern === "parallel"
165
+ ? (data.steps.find((step) => step.dependsOn?.length) ?? null)
166
+ : null;
167
+
168
+ return (
169
+ <div className="space-y-6">
170
+ <Card>
171
+ <WorkflowHeader
172
+ data={data}
173
+ executing={executing}
174
+ canExecute={true}
175
+ onExecute={handleExecute}
176
+ onRerun={handleRerun}
177
+ onDelete={onRequestDelete}
178
+ />
179
+ <CardContent>
180
+ {data.pattern === "swarm" ? (
181
+ <SwarmDashboard
182
+ workflowId={data.id}
183
+ workflowStatus={data.status}
184
+ steps={data.steps}
185
+ swarmConfig={data.swarmConfig}
186
+ onRefresh={onRefresh}
187
+ stepStatusIcons={stepStatusIcons}
188
+ />
189
+ ) : (
190
+ <div className="space-y-4" aria-live="polite">
191
+ {data.pattern === "parallel" && parallelBranches.length > 0 ? (
192
+ <>
193
+ <section className="space-y-3">
194
+ <div className="flex items-center gap-2">
195
+ <GitBranch className="h-4 w-4 text-muted-foreground" />
196
+ <p className="text-sm font-medium">Parallel Branches</p>
197
+ <Badge variant="secondary" className="text-xs">
198
+ {parallelBranches.length}
199
+ </Badge>
200
+ </div>
201
+ <div className="grid gap-3 md:grid-cols-2">
202
+ {parallelBranches.map((step, index) => (
203
+ <div
204
+ key={step.id}
205
+ className="surface-card-muted rounded-lg border border-border/50 p-4"
206
+ >
207
+ <div className="flex items-start gap-3">
208
+ <div className="mt-0.5">
209
+ {stepStatusIcons[step.state.status] ?? stepStatusIcons.pending}
210
+ </div>
211
+ <div className="min-w-0 flex-1">
212
+ <div className="flex items-center gap-2">
213
+ <Badge variant="secondary" className="text-[11px]">
214
+ Branch {index + 1}
215
+ </Badge>
216
+ <span className="text-sm font-medium">{step.name}</span>
217
+ </div>
218
+ <p className="mt-1 text-xs text-muted-foreground line-clamp-2">
219
+ {step.prompt}
220
+ </p>
221
+ {step.state.error && (
222
+ <p className="mt-2 text-xs text-destructive">
223
+ {step.state.error}
224
+ </p>
225
+ )}
226
+ {step.state.result && step.state.status === "completed" && (
227
+ <ExpandableResult result={step.state.result} />
228
+ )}
229
+ {step.state.taskId && data.stepDocuments?.[step.state.taskId] && (
230
+ <DocumentList
231
+ docs={data.stepDocuments[step.state.taskId]}
232
+ label="Generated Files"
233
+ />
234
+ )}
235
+ </div>
236
+ </div>
237
+ </div>
238
+ ))}
239
+ </div>
240
+ </section>
241
+
242
+ {synthesisStep && (
243
+ <section className="space-y-3">
244
+ <div className="flex items-center gap-2">
245
+ <MessageSquareMore className="h-4 w-4 text-muted-foreground" />
246
+ <p className="text-sm font-medium">Synthesis Step</p>
247
+ </div>
248
+ <div className="surface-card-muted rounded-lg border border-border/50 p-4">
249
+ <div className="flex items-start gap-3">
250
+ <div className="mt-0.5">
251
+ {stepStatusIcons[synthesisStep.state.status] ??
252
+ stepStatusIcons.pending}
253
+ </div>
254
+ <div className="min-w-0 flex-1">
255
+ <div className="flex items-center gap-2">
256
+ <Badge variant="outline" className="text-[11px]">
257
+ join
258
+ </Badge>
259
+ <span className="text-sm font-medium">
260
+ {synthesisStep.name}
261
+ </span>
262
+ </div>
263
+ <p className="mt-1 text-xs text-muted-foreground line-clamp-2">
264
+ {synthesisStep.prompt}
265
+ </p>
266
+ <p className="mt-2 text-xs text-muted-foreground">
267
+ Waits for all {parallelBranches.length} branches before running.
268
+ </p>
269
+ {synthesisStep.state.error && (
270
+ <p className="mt-2 text-xs text-destructive">
271
+ {synthesisStep.state.error}
272
+ </p>
273
+ )}
274
+ {synthesisStep.state.result &&
275
+ synthesisStep.state.status === "completed" && (
276
+ <ExpandableResult result={synthesisStep.state.result} />
277
+ )}
278
+ {synthesisStep.state.taskId && data.stepDocuments?.[synthesisStep.state.taskId] && (
279
+ <DocumentList
280
+ docs={data.stepDocuments[synthesisStep.state.taskId]}
281
+ label="Generated Files"
282
+ />
283
+ )}
284
+ </div>
285
+ </div>
286
+ </div>
287
+ </section>
288
+ )}
289
+ </>
290
+ ) : (
291
+ <div className="space-y-3">
292
+ {data.steps.map((step, index) => {
293
+ const isDelayStep = !!step.delayDuration;
294
+ const isActiveDelay = isDelayStep && step.state.status === "delayed";
295
+ return (
296
+ <div key={`${step.id}-${index}`} className="flex items-start gap-3">
297
+ <div className="mt-0.5 flex flex-col items-center">
298
+ {isDelayStep && step.state.status === "pending" ? (
299
+ <Clock3 className="h-4 w-4 text-muted-foreground" />
300
+ ) : (
301
+ stepStatusIcons[step.state.status] ?? stepStatusIcons.pending
302
+ )}
303
+ {index < data.steps.length - 1 && (
304
+ <div className="mt-1 h-6 w-px bg-border" />
305
+ )}
306
+ </div>
307
+ <div className="flex-1 min-w-0">
308
+ <div className="flex items-center gap-2">
309
+ <span className="text-sm font-medium">{step.name}</span>
310
+ {isDelayStep && (
311
+ <Badge variant="secondary" className="text-[10px] uppercase tracking-wide">
312
+ Delay
313
+ </Badge>
314
+ )}
315
+ {step.requiresApproval && !isDelayStep && (
316
+ <Badge variant="outline" className="text-xs">
317
+ checkpoint
318
+ </Badge>
319
+ )}
320
+ </div>
321
+ {isDelayStep ? (
322
+ <DelayStepBody
323
+ workflowId={data.id}
324
+ delayDuration={step.delayDuration!}
325
+ stepStatus={step.state.status}
326
+ resumeAt={isActiveDelay ? data.resumeAt ?? null : null}
327
+ />
328
+ ) : (
329
+ <div className="flex items-center gap-1.5 mt-0.5">
330
+ <p className="text-xs text-muted-foreground truncate">
331
+ {step.prompt.slice(0, 100)}
332
+ {step.prompt.length > 100 ? "..." : ""}
333
+ </p>
334
+ {step.state.taskId && (
335
+ <a
336
+ href={`/tasks/${step.state.taskId}`}
337
+ className="text-[10px] text-primary hover:underline shrink-0"
338
+ >
339
+ view task
340
+ </a>
341
+ )}
342
+ </div>
343
+ )}
344
+ {hasParentDocs && index === 0 && !isDelayStep && (
345
+ <div className="flex items-center gap-1 mt-1">
346
+ <Badge variant="outline" className="text-[10px] px-1.5 py-0">
347
+ {data.parentDocuments!.length} doc{data.parentDocuments!.length !== 1 ? "s" : ""} attached
348
+ </Badge>
349
+ </div>
350
+ )}
351
+ {step.state.error && (
352
+ <p className="text-xs text-destructive mt-1">
353
+ {step.state.error}
354
+ </p>
355
+ )}
356
+ {step.state.result && step.state.status === "completed" && !isDelayStep && (
357
+ <ExpandableResult result={step.state.result} />
358
+ )}
359
+ {step.state.taskId && data.stepDocuments?.[step.state.taskId] && (
360
+ <DocumentList
361
+ docs={data.stepDocuments[step.state.taskId]}
362
+ label="Generated Files"
363
+ />
364
+ )}
365
+ </div>
366
+ </div>
367
+ );
368
+ })}
369
+ </div>
370
+ )}
371
+ </div>
372
+ )}
373
+
374
+ {(hasParentDocs || hasStepDocs) && (
375
+ <div className="mt-6 pt-4 border-t border-border/50">
376
+ <div className="flex items-center gap-2 mb-3">
377
+ <Paperclip className="h-4 w-4 text-muted-foreground" />
378
+ <p className="text-sm font-medium">Documents</p>
379
+ </div>
380
+ {hasParentDocs && (
381
+ <DocumentList docs={data.parentDocuments!} label="Input Files" />
382
+ )}
383
+ {hasStepDocs &&
384
+ Object.entries(data.stepDocuments!).map(([taskId, docs]) => {
385
+ const step = data.steps.find((s) => s.state.taskId === taskId);
386
+ return (
387
+ <DocumentList
388
+ key={taskId}
389
+ docs={docs}
390
+ label={step ? `Output: ${step.name}` : "Output Files"}
391
+ />
392
+ );
393
+ })}
394
+ </div>
395
+ )}
396
+ </CardContent>
397
+ </Card>
398
+
399
+ {data.status === "completed" && completedStepOutputs.length > 0 && (
400
+ <WorkflowFullOutput workflowName={data.name} steps={completedStepOutputs} />
401
+ )}
402
+
403
+ {data.status === "completed" && hasStepDocs && (
404
+ <OutputDock stepDocuments={data.stepDocuments!} steps={data.steps} />
405
+ )}
406
+ </div>
407
+ );
408
+ }
409
+
410
+ /** Output Dock — selectable output documents for chaining into a new workflow */
411
+ function OutputDock({
412
+ stepDocuments,
413
+ steps,
414
+ }: {
415
+ stepDocuments: Record<string, WorkflowStatusDocument[]>;
416
+ steps: StepWithState[];
417
+ }) {
418
+ const router = useRouter();
419
+ const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
420
+
421
+ const allOutputDocs = Object.entries(stepDocuments).flatMap(([taskId, docs]) => {
422
+ const step = steps.find((s) => s.state.taskId === taskId);
423
+ return docs.map((doc) => ({
424
+ ...doc,
425
+ stepName: step?.name ?? "Unknown Step",
426
+ }));
427
+ });
428
+
429
+ if (allOutputDocs.length === 0) return null;
430
+
431
+ function toggleDoc(id: string) {
432
+ setSelectedIds((prev) => {
433
+ const next = new Set(prev);
434
+ if (next.has(id)) next.delete(id);
435
+ else next.add(id);
436
+ return next;
437
+ });
438
+ }
439
+
440
+ function selectAll() {
441
+ setSelectedIds(new Set(allOutputDocs.map((d) => d.id)));
442
+ }
443
+
444
+ function chainIntoNewWorkflow() {
445
+ if (selectedIds.size === 0) return;
446
+ const params = new URLSearchParams({ inputDocs: [...selectedIds].join(",") });
447
+ router.push(`/workflows/new?${params}`);
448
+ }
449
+
450
+ return (
451
+ <Card>
452
+ <div className="px-6 pt-6 pb-3">
453
+ <div className="flex items-center justify-between">
454
+ <div className="flex items-center gap-2 text-sm font-medium">
455
+ <ArrowRight className="h-4 w-4" />
456
+ Chain Output Documents
457
+ </div>
458
+ <Button variant="ghost" size="sm" onClick={selectAll} className="text-xs">
459
+ Select All
460
+ </Button>
461
+ </div>
462
+ <p className="text-xs text-muted-foreground mt-1">
463
+ Select output documents to use as inputs in a new workflow
464
+ </p>
465
+ </div>
466
+ <CardContent>
467
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-2 mb-4">
468
+ {allOutputDocs.map((doc) => {
469
+ const isChecked = selectedIds.has(doc.id);
470
+ return (
471
+ <div
472
+ key={doc.id}
473
+ role="button"
474
+ tabIndex={0}
475
+ onClick={() => toggleDoc(doc.id)}
476
+ onKeyDown={(e) => {
477
+ if (e.key === "Enter" || e.key === " ") {
478
+ e.preventDefault();
479
+ toggleDoc(doc.id);
480
+ }
481
+ }}
482
+ className={`flex items-center gap-3 p-3 rounded-lg text-left transition-colors border cursor-pointer ${
483
+ isChecked
484
+ ? "bg-accent/50 border-accent"
485
+ : "hover:bg-muted/50 border-border/50"
486
+ }`}
487
+ >
488
+ <Checkbox
489
+ checked={isChecked}
490
+ onCheckedChange={() => toggleDoc(doc.id)}
491
+ />
492
+ <FileText className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
493
+ <div className="flex-1 min-w-0">
494
+ <p className="text-sm font-medium truncate">{doc.originalName}</p>
495
+ <p className="text-xs text-muted-foreground truncate">{doc.stepName}</p>
496
+ </div>
497
+ </div>
498
+ );
499
+ })}
500
+ </div>
501
+
502
+ {selectedIds.size > 0 && (
503
+ <Button onClick={chainIntoNewWorkflow} className="w-full gap-2" size="sm">
504
+ <ArrowRight className="h-4 w-4" />
505
+ Chain {selectedIds.size} Document{selectedIds.size !== 1 ? "s" : ""} Into New Workflow
506
+ </Button>
507
+ )}
508
+ </CardContent>
509
+ </Card>
510
+ );
511
+ }