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
@@ -1,256 +1,28 @@
1
1
  "use client";
2
2
 
3
- import { useEffect, useState, useCallback } from "react";
3
+ import { useCallback, useState } from "react";
4
4
  import { useRouter } from "next/navigation";
5
- import Link from "next/link";
6
- import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
7
- import { Badge } from "@/components/ui/badge";
8
- import { Button } from "@/components/ui/button";
9
- import { Skeleton } from "@/components/ui/skeleton";
10
- import {
11
- CheckCircle,
12
- Circle,
13
- Loader2,
14
- XCircle,
15
- ShieldQuestion,
16
- Play,
17
- Pencil,
18
- Copy,
19
- RotateCcw,
20
- Trash2,
21
- Clock3,
22
- GitBranch,
23
- MessageSquareMore,
24
- FileText,
25
- Paperclip,
26
- ArrowRight,
27
- FolderKanban,
28
- } from "lucide-react";
29
- import { Checkbox } from "@/components/ui/checkbox";
30
- import ReactMarkdown from "react-markdown";
31
- import remarkGfm from "remark-gfm";
32
5
  import { toast } from "sonner";
33
- import { workflowStatusVariant, patternLabels } from "@/lib/constants/status-colors";
34
- import { IconCircle, getWorkflowIconFromName } from "@/lib/constants/card-icons";
35
- import { LoopStatusView } from "./loop-status-view";
36
6
  import { ConfirmDialog } from "@/components/shared/confirm-dialog";
37
- import { PROSE_NOTIFICATION } from "@/lib/constants/prose-styles";
38
- import { SwarmDashboard } from "./swarm-dashboard";
39
- import { WorkflowFullOutput } from "./workflow-full-output";
40
- import type { LoopState, LoopConfig, SwarmConfig } from "@/lib/workflows/types";
41
-
42
- interface StepWithState {
43
- id: string;
44
- name: string;
45
- prompt: string;
46
- requiresApproval?: boolean;
47
- dependsOn?: string[];
48
- state: {
49
- stepId: string;
50
- status: string;
51
- taskId?: string;
52
- result?: string;
53
- error?: string;
54
- };
55
- }
56
-
57
- interface DocumentInfo {
58
- id: string;
59
- originalName: string;
60
- mimeType: string;
61
- storagePath: string;
62
- direction: string;
63
- }
64
-
65
- interface WorkflowStatusData {
66
- id: string;
67
- name: string;
68
- status: string;
69
- pattern: string;
70
- projectId?: string | null;
71
- definition?: string;
72
- steps: StepWithState[];
73
- loopConfig?: LoopConfig;
74
- loopState?: LoopState;
75
- swarmConfig?: SwarmConfig;
76
- stepDocuments?: Record<string, DocumentInfo[]>;
77
- parentDocuments?: DocumentInfo[];
78
- runNumber?: number;
79
- runHistory?: Array<{
80
- runNumber: number | null;
81
- taskCount: number;
82
- completedCount: number;
83
- failedCount: number;
84
- }>;
85
- }
86
-
87
- interface WorkflowStatusViewProps {
88
- workflowId: string;
89
- }
90
-
91
- const stepStatusIcons: Record<string, React.ReactNode> = {
92
- pending: <Circle className="h-4 w-4 text-muted-foreground" />,
93
- running: <Loader2 className="h-4 w-4 text-status-running animate-spin" />,
94
- completed: <CheckCircle className="h-4 w-4 text-status-completed" />,
95
- failed: <XCircle className="h-4 w-4 text-destructive" />,
96
- waiting_approval: <ShieldQuestion className="h-4 w-4 text-status-warning" />,
97
- waiting_dependencies: <Clock3 className="h-4 w-4 text-status-warning" />,
98
- };
99
-
100
- /** Expandable step result with gradient fade progressive disclosure */
101
- export function ExpandableResult({ result }: { result: string }) {
102
- const [expanded, setExpanded] = useState(false);
103
-
104
- if (!result) return null;
105
-
106
- return (
107
- <div className="mt-2">
108
- <div
109
- className={`${PROSE_NOTIFICATION} ${
110
- expanded
111
- ? "max-h-96 overflow-auto"
112
- : result.length > 200
113
- ? "max-h-20 overflow-hidden mask-fade-bottom"
114
- : ""
115
- }`}
116
- >
117
- <ReactMarkdown remarkPlugins={[remarkGfm]}>{result}</ReactMarkdown>
118
- </div>
119
- {result.length > 200 && (
120
- <button
121
- type="button"
122
- className="mt-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
123
- onClick={() => setExpanded(!expanded)}
124
- >
125
- {expanded ? "Show less" : "Show more"}
126
- </button>
127
- )}
128
- </div>
129
- );
130
- }
131
-
132
- /** Document list for a single step or parent task */
133
- function DocumentList({ docs, label }: { docs: DocumentInfo[]; label: string }) {
134
- if (docs.length === 0) return null;
135
-
136
- return (
137
- <div className="mt-3">
138
- <p className="text-xs font-medium text-muted-foreground mb-1.5">{label}</p>
139
- <div className="space-y-1">
140
- {docs.map((doc) => (
141
- <Link
142
- key={doc.id}
143
- href={`/documents/${doc.id}`}
144
- className="flex items-center gap-2 text-xs text-brand-blue hover:underline"
145
- >
146
- <FileText className="h-3 w-3 shrink-0" />
147
- {doc.originalName}
148
- </Link>
149
- ))}
150
- </div>
151
- </div>
152
- );
153
- }
154
-
155
-
156
- export function WorkflowStatusView({ workflowId }: WorkflowStatusViewProps) {
7
+ import { useWorkflowStatus } from "./hooks/use-workflow-status";
8
+ import { SequencePatternView } from "./views/sequence-pattern-view";
9
+ import { LoopPatternView } from "./views/loop-pattern-view";
10
+ import { WorkflowLoadingSkeleton } from "./shared/workflow-loading-skeleton";
11
+
12
+ /**
13
+ * Thin router for the workflow detail page (TDR-031). Owns polling + the
14
+ * delete confirm dialog and dispatches to a pattern-specific subview. The
15
+ * if/else on `data.pattern` uses TypeScript's discriminated-union narrowing
16
+ * so each subview receives its own arm, with `.state` guaranteed present on
17
+ * the non-loop arm — which is why PR #6's `s.state?.result` optional chain
18
+ * is no longer needed and has been removed.
19
+ */
20
+ export function WorkflowStatusView({ workflowId }: { workflowId: string }) {
157
21
  const router = useRouter();
158
- const [data, setData] = useState<WorkflowStatusData | null>(null);
159
- const [executing, setExecuting] = useState(false);
22
+ const { data, setData, refetch } = useWorkflowStatus(workflowId);
160
23
  const [confirmDelete, setConfirmDelete] = useState(false);
161
24
 
162
- const fetchStatus = useCallback(async () => {
163
- const res = await fetch(`/api/workflows/${workflowId}/status`);
164
- if (res.ok) setData(await res.json());
165
- }, [workflowId]);
166
-
167
- useEffect(() => {
168
- fetchStatus();
169
- const interval = setInterval(fetchStatus, 3000);
170
- return () => clearInterval(interval);
171
- }, [fetchStatus]);
172
-
173
- async function startExecution() {
174
- setExecuting(true);
175
- // Optimistic update — immediately show "active" status
176
- if (data) {
177
- setData({
178
- ...data,
179
- status: "active",
180
- steps: data.steps.map((step, index) => {
181
- if (data.pattern === "swarm") {
182
- const lastIndex = data.steps.length - 1;
183
- return {
184
- ...step,
185
- state: {
186
- ...step.state,
187
- status:
188
- index === 0
189
- ? "running"
190
- : index === lastIndex
191
- ? "waiting_dependencies"
192
- : "pending",
193
- },
194
- };
195
- }
196
-
197
- if (data.pattern === "parallel") {
198
- const isJoin = !!step.dependsOn?.length;
199
- return {
200
- ...step,
201
- state: {
202
- ...step.state,
203
- status: isJoin
204
- ? "waiting_dependencies"
205
- : index === 0
206
- ? "running"
207
- : "pending",
208
- },
209
- };
210
- }
211
-
212
- return index === 0
213
- ? { ...step, state: { ...step.state, status: "running" } }
214
- : step;
215
- }),
216
- });
217
- }
218
- try {
219
- const res = await fetch(`/api/workflows/${workflowId}/execute`, {
220
- method: "POST",
221
- });
222
- if (res.ok) {
223
- toast.success("Workflow started");
224
- fetchStatus();
225
- } else {
226
- const err = await res.json().catch(() => null);
227
- toast.error(err?.error ?? "Failed to start workflow");
228
- fetchStatus(); // Revert optimistic update on failure
229
- }
230
- } finally {
231
- setExecuting(false);
232
- }
233
- }
234
-
235
- async function handleRerun() {
236
- setExecuting(true);
237
- try {
238
- const res = await fetch(`/api/workflows/${workflowId}/execute`, {
239
- method: "POST",
240
- });
241
- if (res.ok) {
242
- toast.success("Workflow re-started");
243
- fetchStatus();
244
- } else {
245
- const err = await res.json().catch(() => null);
246
- toast.error(err?.error ?? "Failed to re-run workflow");
247
- }
248
- } finally {
249
- setExecuting(false);
250
- }
251
- }
252
-
253
- async function handleDelete() {
25
+ const handleDelete = useCallback(async () => {
254
26
  setConfirmDelete(false);
255
27
  const res = await fetch(`/api/workflows/${workflowId}`, { method: "DELETE" });
256
28
  if (res.ok) {
@@ -260,390 +32,24 @@ export function WorkflowStatusView({ workflowId }: WorkflowStatusViewProps) {
260
32
  const err = await res.json().catch(() => null);
261
33
  toast.error(err?.error ?? "Failed to delete workflow");
262
34
  }
263
- }
264
-
265
- if (!data) {
266
- return (
267
- <Card>
268
- <CardHeader>
269
- <div className="flex items-center justify-between">
270
- <div>
271
- <Skeleton className="h-6 w-48 mb-2" />
272
- <Skeleton className="h-4 w-24" />
273
- </div>
274
- <Skeleton className="h-6 w-16 rounded-full" />
275
- </div>
276
- </CardHeader>
277
- <CardContent>
278
- <div className="space-y-3">
279
- {[1, 2, 3].map((i) => (
280
- <div key={i} className="flex items-start gap-3">
281
- <Skeleton className="h-4 w-4 rounded-full mt-0.5" />
282
- <div className="flex-1">
283
- <Skeleton className="h-4 w-32 mb-1" />
284
- <Skeleton className="h-3 w-full" />
285
- </div>
286
- </div>
287
- ))}
288
- </div>
289
- </CardContent>
290
- </Card>
291
- );
292
- }
293
-
294
- const hasDefinition = !!data.definition;
295
- const parallelBranches =
296
- data.pattern === "parallel"
297
- ? data.steps.filter((step) => !step.dependsOn?.length)
298
- : [];
299
- const synthesisStep =
300
- data.pattern === "parallel"
301
- ? data.steps.find((step) => step.dependsOn?.length) ?? null
302
- : null;
35
+ }, [router, workflowId]);
303
36
 
304
- // Collect all completed step outputs for full output sheet
305
- const completedStepOutputs = data.steps
306
- .filter((s) => s.state.result && s.state.status === "completed")
307
- .map((s) => ({ name: s.name, result: s.state.result! }));
37
+ const onRequestDelete = useCallback(() => setConfirmDelete(true), []);
308
38
 
309
- // Check for any documents
310
- const hasStepDocs = data.stepDocuments && Object.keys(data.stepDocuments).length > 0;
311
- const hasParentDocs = data.parentDocuments && data.parentDocuments.length > 0;
39
+ if (!data) return <WorkflowLoadingSkeleton />;
312
40
 
313
41
  return (
314
- <div className="space-y-6">
315
- <Card>
316
- <CardHeader>
317
- <div className="flex items-center justify-between">
318
- <div className="flex items-center gap-3">
319
- <IconCircle
320
- icon={getWorkflowIconFromName(data.name, data.pattern).icon}
321
- colors={getWorkflowIconFromName(data.name, data.pattern).colors}
322
- />
323
- <div>
324
- <CardTitle>{data.name}</CardTitle>
325
- <p className="text-sm text-muted-foreground mt-1">
326
- {patternLabels[data.pattern] ?? data.pattern}
327
- </p>
328
- <div className="flex items-center gap-2 mt-1">
329
- {data.projectId && (
330
- <Badge
331
- variant="outline"
332
- className="text-xs cursor-pointer hover:bg-accent gap-1"
333
- onClick={() => router.push(`/projects/${data.projectId}`)}
334
- >
335
- <FolderKanban className="h-3 w-3" />
336
- Project
337
- </Badge>
338
- )}
339
- {data.runNumber != null && data.runNumber > 0 && (
340
- <Badge variant="outline" className="text-xs font-normal">
341
- Run #{data.runNumber}
342
- </Badge>
343
- )}
344
- </div>
345
- </div>
346
- </div>
347
- <div className="flex items-center gap-2">
348
- <Badge variant={workflowStatusVariant[data.status] ?? "secondary"}>
349
- {data.status}
350
- </Badge>
351
-
352
- {/* Execute button for draft/paused non-loop workflows */}
353
- {data.pattern !== "loop" && (data.status === "draft" || data.status === "paused") && (
354
- <Button size="sm" onClick={startExecution} disabled={executing}>
355
- <Play className="h-3 w-3 mr-1" />
356
- {executing ? "Starting..." : "Execute"}
357
- </Button>
358
- )}
359
-
360
- {/* Edit — draft, completed, or failed */}
361
- {["draft", "completed", "failed"].includes(data.status) && hasDefinition && (
362
- <Button
363
- variant="outline"
364
- size="sm"
365
- onClick={() => router.push(`/workflows/${workflowId}/edit`)}
366
- >
367
- <Pencil className="h-3.5 w-3.5 mr-1.5" />
368
- Edit
369
- </Button>
370
- )}
371
-
372
- {/* Clone — always available */}
373
- {hasDefinition && (
374
- <Button
375
- variant="outline"
376
- size="sm"
377
- onClick={() => router.push(`/workflows/${workflowId}/edit?clone=true`)}
378
- >
379
- <Copy className="h-3.5 w-3.5 mr-1.5" />
380
- Clone
381
- </Button>
382
- )}
383
-
384
- {/* Re-run — completed/failed only */}
385
- {(data.status === "completed" || data.status === "failed") && (
386
- <Button
387
- variant="outline"
388
- size="sm"
389
- onClick={handleRerun}
390
- disabled={executing}
391
- >
392
- <RotateCcw className="h-3.5 w-3.5 mr-1.5" />
393
- Re-run
394
- </Button>
395
- )}
396
-
397
- {/* Delete — not active */}
398
- {data.status !== "active" && (
399
- <Button
400
- variant="outline"
401
- size="sm"
402
- className="text-destructive hover:text-destructive"
403
- onClick={() => setConfirmDelete(true)}
404
- >
405
- <Trash2 className="h-3.5 w-3.5 mr-1.5" />
406
- Delete
407
- </Button>
408
- )}
409
- </div>
410
- </div>
411
- </CardHeader>
412
- <CardContent>
413
- {/* Loop prompt display */}
414
- {data.pattern === "loop" && data.steps[0]?.prompt && (
415
- <div className="mb-4">
416
- <p className="text-xs font-medium text-muted-foreground mb-1.5">Loop Prompt</p>
417
- <div className="rounded-md border bg-muted/50 p-3">
418
- <p className="text-sm whitespace-pre-wrap">{data.steps[0].prompt}</p>
419
- </div>
420
- </div>
421
- )}
422
-
423
- {data.pattern === "loop" && data.loopConfig ? (
424
- <LoopStatusView
425
- workflowId={workflowId}
426
- workflowStatus={data.status}
427
- loopConfig={data.loopConfig}
428
- loopState={data.loopState ?? null}
429
- onRefresh={fetchStatus}
430
- />
431
- ) : data.pattern === "swarm" ? (
432
- <SwarmDashboard
433
- workflowId={workflowId}
434
- workflowStatus={data.status}
435
- steps={data.steps}
436
- swarmConfig={data.swarmConfig}
437
- onRefresh={fetchStatus}
438
- stepStatusIcons={stepStatusIcons}
439
- />
440
- ) : (
441
- <div className="space-y-4" aria-live="polite">
442
- {data.pattern === "parallel" && parallelBranches.length > 0 ? (
443
- <>
444
- <section className="space-y-3">
445
- <div className="flex items-center gap-2">
446
- <GitBranch className="h-4 w-4 text-muted-foreground" />
447
- <p className="text-sm font-medium">Parallel Branches</p>
448
- <Badge variant="secondary" className="text-xs">
449
- {parallelBranches.length}
450
- </Badge>
451
- </div>
452
- <div className="grid gap-3 md:grid-cols-2">
453
- {parallelBranches.map((step, index) => (
454
- <div
455
- key={step.id}
456
- className="surface-card-muted rounded-lg border border-border/50 p-4"
457
- >
458
- <div className="flex items-start gap-3">
459
- <div className="mt-0.5">
460
- {stepStatusIcons[step.state.status] ?? stepStatusIcons.pending}
461
- </div>
462
- <div className="min-w-0 flex-1">
463
- <div className="flex items-center gap-2">
464
- <Badge variant="secondary" className="text-[11px]">
465
- Branch {index + 1}
466
- </Badge>
467
- <span className="text-sm font-medium">{step.name}</span>
468
- </div>
469
- <p className="mt-1 text-xs text-muted-foreground line-clamp-2">
470
- {step.prompt}
471
- </p>
472
- {step.state.error && (
473
- <p className="mt-2 text-xs text-destructive">
474
- {step.state.error}
475
- </p>
476
- )}
477
- {step.state.result && step.state.status === "completed" && (
478
- <ExpandableResult result={step.state.result} />
479
- )}
480
- {/* Step output documents */}
481
- {step.state.taskId && data.stepDocuments?.[step.state.taskId] && (
482
- <DocumentList
483
- docs={data.stepDocuments[step.state.taskId]}
484
- label="Generated Files"
485
- />
486
- )}
487
- </div>
488
- </div>
489
- </div>
490
- ))}
491
- </div>
492
- </section>
493
-
494
- {synthesisStep && (
495
- <section className="space-y-3">
496
- <div className="flex items-center gap-2">
497
- <MessageSquareMore className="h-4 w-4 text-muted-foreground" />
498
- <p className="text-sm font-medium">Synthesis Step</p>
499
- </div>
500
- <div className="surface-card-muted rounded-lg border border-border/50 p-4">
501
- <div className="flex items-start gap-3">
502
- <div className="mt-0.5">
503
- {stepStatusIcons[synthesisStep.state.status] ??
504
- stepStatusIcons.pending}
505
- </div>
506
- <div className="min-w-0 flex-1">
507
- <div className="flex items-center gap-2">
508
- <Badge variant="outline" className="text-[11px]">
509
- join
510
- </Badge>
511
- <span className="text-sm font-medium">
512
- {synthesisStep.name}
513
- </span>
514
- </div>
515
- <p className="mt-1 text-xs text-muted-foreground line-clamp-2">
516
- {synthesisStep.prompt}
517
- </p>
518
- <p className="mt-2 text-xs text-muted-foreground">
519
- Waits for all {parallelBranches.length} branches before
520
- running.
521
- </p>
522
- {synthesisStep.state.error && (
523
- <p className="mt-2 text-xs text-destructive">
524
- {synthesisStep.state.error}
525
- </p>
526
- )}
527
- {synthesisStep.state.result &&
528
- synthesisStep.state.status === "completed" && (
529
- <ExpandableResult result={synthesisStep.state.result} />
530
- )}
531
- {/* Synthesis step documents */}
532
- {synthesisStep.state.taskId && data.stepDocuments?.[synthesisStep.state.taskId] && (
533
- <DocumentList
534
- docs={data.stepDocuments[synthesisStep.state.taskId]}
535
- label="Generated Files"
536
- />
537
- )}
538
- </div>
539
- </div>
540
- </div>
541
- </section>
542
- )}
543
- </>
544
- ) : (
545
- <div className="space-y-3">
546
- {data.steps.map((step, index) => (
547
- <div key={`${step.id}-${index}`} className="flex items-start gap-3">
548
- <div className="mt-0.5 flex flex-col items-center">
549
- {stepStatusIcons[step.state.status] ?? stepStatusIcons.pending}
550
- {index < data.steps.length - 1 && (
551
- <div className="mt-1 h-6 w-px bg-border" />
552
- )}
553
- </div>
554
- <div className="flex-1 min-w-0">
555
- <div className="flex items-center gap-2">
556
- <span className="text-sm font-medium">{step.name}</span>
557
- {step.requiresApproval && (
558
- <Badge variant="outline" className="text-xs">
559
- checkpoint
560
- </Badge>
561
- )}
562
- </div>
563
- <div className="flex items-center gap-1.5 mt-0.5">
564
- <p className="text-xs text-muted-foreground truncate">
565
- {step.prompt.slice(0, 100)}
566
- {step.prompt.length > 100 ? "..." : ""}
567
- </p>
568
- {step.state.taskId && (
569
- <a
570
- href={`/tasks/${step.state.taskId}`}
571
- className="text-[10px] text-primary hover:underline shrink-0"
572
- >
573
- view task
574
- </a>
575
- )}
576
- </div>
577
- {/* Parent document context indicator */}
578
- {data.parentDocuments && data.parentDocuments.length > 0 && index === 0 && (
579
- <div className="flex items-center gap-1 mt-1">
580
- <Badge variant="outline" className="text-[10px] px-1.5 py-0">
581
- {data.parentDocuments.length} doc{data.parentDocuments.length !== 1 ? "s" : ""} attached
582
- </Badge>
583
- </div>
584
- )}
585
- {step.state.error && (
586
- <p className="text-xs text-destructive mt-1">
587
- {step.state.error}
588
- </p>
589
- )}
590
- {step.state.result && step.state.status === "completed" && (
591
- <ExpandableResult result={step.state.result} />
592
- )}
593
- {/* Step output documents */}
594
- {step.state.taskId && data.stepDocuments?.[step.state.taskId] && (
595
- <DocumentList
596
- docs={data.stepDocuments[step.state.taskId]}
597
- label="Generated Files"
598
- />
599
- )}
600
- </div>
601
- </div>
602
- ))}
603
- </div>
604
- )}
605
- </div>
606
- )}
607
-
608
- {/* Documents section — parent inputs and step outputs */}
609
- {(hasParentDocs || hasStepDocs) && (
610
- <div className="mt-6 pt-4 border-t border-border/50">
611
- <div className="flex items-center gap-2 mb-3">
612
- <Paperclip className="h-4 w-4 text-muted-foreground" />
613
- <p className="text-sm font-medium">Documents</p>
614
- </div>
615
- {hasParentDocs && (
616
- <DocumentList docs={data.parentDocuments!} label="Input Files" />
617
- )}
618
- {hasStepDocs && Object.entries(data.stepDocuments!).map(([taskId, docs]) => {
619
- const step = data.steps.find((s) => s.state.taskId === taskId);
620
- return (
621
- <DocumentList
622
- key={taskId}
623
- docs={docs}
624
- label={step ? `Output: ${step.name}` : "Output Files"}
625
- />
626
- );
627
- })}
628
- </div>
629
- )}
630
- </CardContent>
631
- </Card>
632
-
633
- {/* Full output — inline below workflow card when completed */}
634
- {data.status === "completed" && completedStepOutputs.length > 0 && (
635
- <WorkflowFullOutput
636
- workflowName={data.name}
637
- steps={completedStepOutputs}
42
+ <>
43
+ {data.pattern === "loop" ? (
44
+ <LoopPatternView data={data} onRefresh={refetch} onRequestDelete={onRequestDelete} />
45
+ ) : (
46
+ <SequencePatternView
47
+ data={data}
48
+ setData={setData}
49
+ onRefresh={refetch}
50
+ onRequestDelete={onRequestDelete}
638
51
  />
639
52
  )}
640
-
641
- {/* Output Dock — chain into new workflow */}
642
- {data.status === "completed" && hasStepDocs && (
643
- <OutputDock stepDocuments={data.stepDocuments!} steps={data.steps} />
644
- )}
645
-
646
- {/* Delete confirmation */}
647
53
  <ConfirmDialog
648
54
  open={confirmDelete}
649
55
  onOpenChange={setConfirmDelete}
@@ -653,122 +59,6 @@ export function WorkflowStatusView({ workflowId }: WorkflowStatusViewProps) {
653
59
  onConfirm={handleDelete}
654
60
  destructive
655
61
  />
656
- </div>
657
- );
658
- }
659
-
660
- /** Output Dock — selectable output documents for chaining into a new workflow */
661
- function OutputDock({
662
- stepDocuments,
663
- steps,
664
- }: {
665
- stepDocuments: Record<string, DocumentInfo[]>;
666
- steps: StepWithState[];
667
- }) {
668
- const router = useRouter();
669
- const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
670
-
671
- // Flatten all output documents
672
- const allOutputDocs = Object.entries(stepDocuments).flatMap(
673
- ([taskId, docs]) => {
674
- const step = steps.find((s) => s.state.taskId === taskId);
675
- return docs.map((doc) => ({
676
- ...doc,
677
- stepName: step?.name ?? "Unknown Step",
678
- }));
679
- }
680
- );
681
-
682
- if (allOutputDocs.length === 0) return null;
683
-
684
- function toggleDoc(id: string) {
685
- setSelectedIds((prev) => {
686
- const next = new Set(prev);
687
- if (next.has(id)) next.delete(id);
688
- else next.add(id);
689
- return next;
690
- });
691
- }
692
-
693
- function selectAll() {
694
- setSelectedIds(new Set(allOutputDocs.map((d) => d.id)));
695
- }
696
-
697
- function chainIntoNewWorkflow() {
698
- if (selectedIds.size === 0) return;
699
- const params = new URLSearchParams({
700
- inputDocs: [...selectedIds].join(","),
701
- });
702
- router.push(`/workflows/new?${params}`);
703
- }
704
-
705
- return (
706
- <Card>
707
- <CardHeader className="pb-3">
708
- <div className="flex items-center justify-between">
709
- <CardTitle className="text-sm font-medium flex items-center gap-2">
710
- <ArrowRight className="h-4 w-4" />
711
- Chain Output Documents
712
- </CardTitle>
713
- <Button
714
- variant="ghost"
715
- size="sm"
716
- onClick={selectAll}
717
- className="text-xs"
718
- >
719
- Select All
720
- </Button>
721
- </div>
722
- <p className="text-xs text-muted-foreground">
723
- Select output documents to use as inputs in a new workflow
724
- </p>
725
- </CardHeader>
726
- <CardContent>
727
- <div className="grid grid-cols-1 sm:grid-cols-2 gap-2 mb-4">
728
- {allOutputDocs.map((doc) => {
729
- const isChecked = selectedIds.has(doc.id);
730
- return (
731
- <div
732
- key={doc.id}
733
- role="button"
734
- tabIndex={0}
735
- onClick={() => toggleDoc(doc.id)}
736
- onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggleDoc(doc.id); } }}
737
- className={`flex items-center gap-3 p-3 rounded-lg text-left transition-colors border cursor-pointer ${
738
- isChecked
739
- ? "bg-accent/50 border-accent"
740
- : "hover:bg-muted/50 border-border/50"
741
- }`}
742
- >
743
- <Checkbox
744
- checked={isChecked}
745
- onCheckedChange={() => toggleDoc(doc.id)}
746
- />
747
- <FileText className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
748
- <div className="flex-1 min-w-0">
749
- <p className="text-sm font-medium truncate">
750
- {doc.originalName}
751
- </p>
752
- <p className="text-xs text-muted-foreground truncate">
753
- {doc.stepName}
754
- </p>
755
- </div>
756
- </div>
757
- );
758
- })}
759
- </div>
760
-
761
- {selectedIds.size > 0 && (
762
- <Button
763
- onClick={chainIntoNewWorkflow}
764
- className="w-full gap-2"
765
- size="sm"
766
- >
767
- <ArrowRight className="h-4 w-4" />
768
- Chain {selectedIds.size} Document{selectedIds.size !== 1 ? "s" : ""} Into New Workflow
769
- </Button>
770
- )}
771
- </CardContent>
772
- </Card>
62
+ </>
773
63
  );
774
64
  }