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
@@ -9,6 +9,47 @@ import { Inbox, Plus, CheckSquare, Square, ArrowRight, Play, Trash2 } from "luci
9
9
  import { TaskCard, type TaskItem } from "./task-card";
10
10
  import { WorkflowKanbanCard, type WorkflowKanbanItem } from "@/components/workflows/workflow-kanban-card";
11
11
  import type { TaskStatus } from "@/lib/constants/task-status";
12
+ import type { SortOrder } from "./kanban-board";
13
+
14
+ type KanbanItem =
15
+ | { kind: "task"; data: TaskItem }
16
+ | { kind: "workflow"; data: WorkflowKanbanItem };
17
+
18
+ function itemName(item: KanbanItem): string {
19
+ return item.kind === "task" ? item.data.title : item.data.name;
20
+ }
21
+
22
+ function mergedItems(
23
+ tasks: TaskItem[],
24
+ workflows: WorkflowKanbanItem[],
25
+ sortOrder: SortOrder
26
+ ): KanbanItem[] {
27
+ const items: KanbanItem[] = [
28
+ ...workflows.map((w) => ({ kind: "workflow" as const, data: w })),
29
+ ...tasks.map((t) => ({ kind: "task" as const, data: t })),
30
+ ];
31
+
32
+ switch (sortOrder) {
33
+ case "created-desc":
34
+ return items.sort(
35
+ (a, b) => new Date(b.data.createdAt).getTime() - new Date(a.data.createdAt).getTime()
36
+ );
37
+ case "created-asc":
38
+ return items.sort(
39
+ (a, b) => new Date(a.data.createdAt).getTime() - new Date(b.data.createdAt).getTime()
40
+ );
41
+ case "title-asc":
42
+ return items.sort((a, b) => itemName(a).localeCompare(itemName(b)));
43
+ case "priority":
44
+ // Workflows lack priority — keep them at top, then sort tasks by priority
45
+ return items.sort((a, b) => {
46
+ if (a.kind === "workflow" && b.kind === "workflow") return 0;
47
+ if (a.kind === "workflow") return -1;
48
+ if (b.kind === "workflow") return 1;
49
+ return a.data.priority - b.data.priority;
50
+ });
51
+ }
52
+ }
12
53
 
13
54
  const columnLabels: Record<string, string> = {
14
55
  planned: "Planned",
@@ -22,6 +63,7 @@ export function KanbanColumn({
22
63
  status,
23
64
  tasks,
24
65
  workflows = [],
66
+ sortOrder = "priority",
25
67
  exitingIds,
26
68
  onTaskClick,
27
69
  onAddTask,
@@ -34,6 +76,7 @@ export function KanbanColumn({
34
76
  status: TaskStatus;
35
77
  tasks: TaskItem[];
36
78
  workflows?: WorkflowKanbanItem[];
79
+ sortOrder?: SortOrder;
37
80
  exitingIds?: Set<string>;
38
81
  onTaskClick: (task: TaskItem) => void;
39
82
  onAddTask?: () => void;
@@ -195,30 +238,26 @@ export function KanbanColumn({
195
238
  </div>
196
239
  ) : (
197
240
  <>
198
- {/* Workflow cards first (not draggable) */}
199
- {workflows.map((workflow) => (
200
- <WorkflowKanbanCard key={workflow.id} workflow={workflow} />
201
- ))}
202
- {/* Task cards (draggable) */}
203
- {tasks.map((task) => {
204
- const isExiting = exitingIds?.has(task.id);
205
- return (
241
+ {mergedItems(tasks, workflows, sortOrder).map((item) =>
242
+ item.kind === "workflow" ? (
243
+ <WorkflowKanbanCard key={item.data.id} workflow={item.data} />
244
+ ) : (
206
245
  <div
207
- key={task.id}
208
- className={isExiting ? "animate-card-exit pointer-events-none" : ""}
246
+ key={item.data.id}
247
+ className={exitingIds?.has(item.data.id) ? "animate-card-exit pointer-events-none" : ""}
209
248
  >
210
249
  <TaskCard
211
- task={task}
250
+ task={item.data}
212
251
  onClick={onTaskClick}
213
252
  selectionMode={selectMode}
214
- selected={selectedIds.has(task.id)}
253
+ selected={selectedIds.has(item.data.id)}
215
254
  onSelect={handleSelect}
216
255
  onDelete={onDeleteTask}
217
256
  onEdit={onEditTask}
218
257
  />
219
258
  </div>
220
- );
221
- })}
259
+ )
260
+ )}
222
261
  </>
223
262
  )}
224
263
  </div>
@@ -10,8 +10,11 @@ import {
10
10
  Timer,
11
11
  Cpu,
12
12
  Paperclip,
13
+ CalendarClock,
14
+ CalendarCheck,
13
15
  } from "lucide-react";
14
16
  import { taskStatusVariant } from "@/lib/constants/status-colors";
17
+ import { formatCompactDateTime } from "@/lib/utils/format-timestamp";
15
18
  import { TaskBentoCell } from "./task-bento-cell";
16
19
  import type { TaskItem } from "./task-card";
17
20
  import type { DocumentRow } from "@/lib/db/schema";
@@ -127,6 +130,22 @@ export function TaskBentoGrid({ task, docs }: TaskBentoGridProps) {
127
130
  />
128
131
  )}
129
132
 
133
+ {usage?.startedAt && (
134
+ <TaskBentoCell
135
+ icon={CalendarClock}
136
+ label="Started At"
137
+ value={formatCompactDateTime(usage.startedAt)}
138
+ />
139
+ )}
140
+
141
+ {usage?.finishedAt && (
142
+ <TaskBentoCell
143
+ icon={CalendarCheck}
144
+ label="Finished At"
145
+ value={formatCompactDateTime(usage.finishedAt)}
146
+ />
147
+ )}
148
+
130
149
  {usage?.startedAt && usage?.finishedAt && (
131
150
  <TaskBentoCell
132
151
  icon={Timer}
@@ -5,7 +5,8 @@ import { useSortable } from "@dnd-kit/sortable";
5
5
  import { CSS } from "@dnd-kit/utilities";
6
6
  import { Card } from "@/components/ui/card";
7
7
  import { Badge } from "@/components/ui/badge";
8
- import { AlertCircle, Bot, ArrowUp, ArrowDown, Minus, Trash2, Check, X, Loader2, Square, CheckSquare, Pencil, FileText } from "lucide-react";
8
+ import { AlertCircle, Bot, ArrowUp, ArrowDown, Minus, Trash2, Check, X, Loader2, Square, CheckSquare, Pencil, FileText, Clock } from "lucide-react";
9
+ import { formatCompactDateTime } from "@/lib/utils/format-timestamp";
9
10
  import type { TaskStatus } from "@/lib/constants/task-status";
10
11
 
11
12
  export interface TaskItem {
@@ -122,6 +123,14 @@ export function TaskCard({
122
123
  const showDeleteButton = onDelete && !isRunning;
123
124
  const isEditable = (task.status === "planned" || task.status === "queued") && !!onEdit;
124
125
 
126
+ // Status-contextual date: planned→createdAt, running→startedAt, completed/failed→finishedAt
127
+ const relevantDate =
128
+ task.status === "planned" || task.status === "queued"
129
+ ? task.createdAt
130
+ : task.status === "running"
131
+ ? task.usage?.startedAt ?? task.updatedAt
132
+ : task.usage?.finishedAt ?? task.updatedAt;
133
+
125
134
  return (
126
135
  <Card
127
136
  ref={setNodeRef}
@@ -244,7 +253,13 @@ export function TaskCard({
244
253
  <Pencil className="h-3.5 w-3.5" />
245
254
  </button>
246
255
  )}
247
- <div className="flex-1" />
256
+ <span
257
+ className="flex items-center gap-1 text-xs text-muted-foreground tabular-nums truncate min-w-0 flex-1"
258
+ title={new Date(relevantDate).toLocaleString()}
259
+ >
260
+ <Clock className="h-3 w-3 shrink-0" />
261
+ {formatCompactDateTime(relevantDate)}
262
+ </span>
248
263
  {showDeleteButton && (
249
264
  <button
250
265
  type="button"
@@ -257,7 +272,15 @@ export function TaskCard({
257
272
  )}
258
273
  </>
259
274
  )
260
- ) : null}
275
+ ) : (
276
+ <span
277
+ className="flex items-center gap-1 text-xs text-muted-foreground tabular-nums truncate min-w-0 flex-1"
278
+ title={new Date(relevantDate).toLocaleString()}
279
+ >
280
+ <Clock className="h-3 w-3 shrink-0" />
281
+ {formatCompactDateTime(relevantDate)}
282
+ </span>
283
+ )}
261
284
  </div>
262
285
  </Card>
263
286
  );
@@ -19,6 +19,8 @@ import {
19
19
  ArrowUp,
20
20
  ArrowDown,
21
21
  Minus,
22
+ CalendarClock,
23
+ CalendarCheck,
22
24
  } from "lucide-react";
23
25
  import { taskStatusVariant } from "@/lib/constants/status-colors";
24
26
  import { MAX_RESUME_COUNT } from "@/lib/constants/task-status";
@@ -172,6 +174,28 @@ export function TaskChipBar({
172
174
  >
173
175
  Updated {formatTimestamp(task.updatedAt)}
174
176
  </Badge>
177
+
178
+ {task.usage?.startedAt && (
179
+ <Badge
180
+ variant="outline"
181
+ className="text-xs font-normal gap-1"
182
+ title={new Date(task.usage.startedAt).toLocaleString()}
183
+ >
184
+ <CalendarClock className="h-3 w-3" />
185
+ Started {formatTimestamp(task.usage.startedAt)}
186
+ </Badge>
187
+ )}
188
+
189
+ {task.usage?.finishedAt && (
190
+ <Badge
191
+ variant="outline"
192
+ className="text-xs font-normal gap-1"
193
+ title={new Date(task.usage.finishedAt).toLocaleString()}
194
+ >
195
+ <CalendarCheck className="h-3 w-3" />
196
+ Finished {formatTimestamp(task.usage.finishedAt)}
197
+ </Badge>
198
+ )}
175
199
  </div>
176
200
 
177
201
  {/* Row 3: Relationship Links (only if any FK exists) */}
@@ -1,6 +1,6 @@
1
1
  import { Separator } from "@/components/ui/separator";
2
2
  import { LightMarkdown } from "@/components/shared/light-markdown";
3
- import { ExpandableResult } from "@/components/workflows/workflow-status-view";
3
+ import { ExpandableResult } from "@/components/workflows/shared/step-result";
4
4
 
5
5
  interface TaskResultRendererProps {
6
6
  description: string | null;
@@ -0,0 +1,109 @@
1
+ "use client";
2
+
3
+ import { useCallback, useState } from "react";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Play } from "lucide-react";
6
+ import { toast } from "sonner";
7
+ import { formatDuration } from "@/lib/workflows/delay";
8
+
9
+ /**
10
+ * Body content for a delay step row in the workflow status view. Renders
11
+ * three visual states keyed off the step's status:
12
+ *
13
+ * - Pending (workflow hasn't reached this step yet): "Will wait 3d"
14
+ * - Active delay (workflow paused, waiting): absolute resume time +
15
+ * remaining duration + Resume Now button
16
+ * - Completed (workflow has already passed this step): "Delayed 3d — completed"
17
+ *
18
+ * Countdown is static on mount/focus — no per-second ticking, because live
19
+ * aria-live updates would flood assistive tech users. Users needing a refresh
20
+ * can reload or refocus the page.
21
+ *
22
+ * Extracted from workflow-status-view.tsx during the TDR-031 router refactor
23
+ * so the non-loop subview can import it without a circular dependency on the
24
+ * thin router file.
25
+ */
26
+ export function DelayStepBody({
27
+ workflowId,
28
+ delayDuration,
29
+ stepStatus,
30
+ resumeAt,
31
+ }: {
32
+ workflowId: string;
33
+ delayDuration: string;
34
+ stepStatus: string;
35
+ resumeAt: number | null;
36
+ }) {
37
+ const [resuming, setResuming] = useState(false);
38
+
39
+ const handleResumeNow = useCallback(async () => {
40
+ setResuming(true);
41
+ try {
42
+ const res = await fetch(`/api/workflows/${workflowId}/resume`, { method: "POST" });
43
+ if (res.status === 202) {
44
+ toast.success("Resume dispatched");
45
+ } else if (res.status === 409) {
46
+ toast.info("Workflow already resumed by scheduler");
47
+ } else {
48
+ const body = await res.json().catch(() => ({}));
49
+ toast.error(body.error ?? "Failed to resume workflow");
50
+ }
51
+ } catch (err) {
52
+ toast.error(err instanceof Error ? err.message : "Failed to resume workflow");
53
+ } finally {
54
+ setResuming(false);
55
+ }
56
+ }, [workflowId]);
57
+
58
+ if (stepStatus === "completed") {
59
+ return (
60
+ <p className="text-xs text-muted-foreground mt-0.5">
61
+ Delayed {delayDuration} — completed
62
+ </p>
63
+ );
64
+ }
65
+
66
+ if (stepStatus === "delayed" && resumeAt) {
67
+ const resumeDate = new Date(resumeAt);
68
+ const remainingMs = Math.max(0, resumeAt - Date.now());
69
+ const remainingLabel =
70
+ remainingMs < 60_000
71
+ ? "less than a minute"
72
+ : formatDuration(Math.round(remainingMs / 60_000) * 60_000);
73
+ return (
74
+ <div className="mt-1 space-y-2">
75
+ <p className="text-xs text-status-warning">
76
+ Resumes{" "}
77
+ <time dateTime={resumeDate.toISOString()}>
78
+ {resumeDate.toLocaleString(undefined, {
79
+ weekday: "short",
80
+ month: "short",
81
+ day: "numeric",
82
+ hour: "numeric",
83
+ minute: "2-digit",
84
+ timeZoneName: "short",
85
+ })}
86
+ </time>{" "}
87
+ <span className="text-muted-foreground">({remainingLabel} remaining)</span>
88
+ </p>
89
+ <Button
90
+ size="sm"
91
+ variant="outline"
92
+ onClick={handleResumeNow}
93
+ disabled={resuming}
94
+ aria-label="Resume workflow now"
95
+ >
96
+ <Play className="h-3 w-3 mr-1" />
97
+ {resuming ? "Resuming..." : "Resume Now"}
98
+ </Button>
99
+ </div>
100
+ );
101
+ }
102
+
103
+ // Pending / upcoming delay — workflow hasn't reached this step yet
104
+ return (
105
+ <p className="text-xs text-muted-foreground mt-0.5">
106
+ Will wait {delayDuration}
107
+ </p>
108
+ );
109
+ }
@@ -0,0 +1,50 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useState } from "react";
4
+ import type { WorkflowStatusResponse } from "@/lib/workflows/types";
5
+
6
+ /**
7
+ * Polling hook for `GET /api/workflows/[id]/status`.
8
+ *
9
+ * Owns the fetch, the 3-second interval, cancellation on unmount, and
10
+ * re-subscription when `workflowId` changes. Exposes `setData` so callers can
11
+ * apply optimistic updates (e.g. flipping a step to "running" immediately when
12
+ * Execute is clicked) without racing the next poll tick.
13
+ *
14
+ * Returns `WorkflowStatusResponse | null` — narrowing on `data.pattern` is the
15
+ * caller's job per TDR-031. This hook intentionally does not narrow for
16
+ * consumers; each pattern-specific subview handles its own arm.
17
+ */
18
+ export function useWorkflowStatus(workflowId: string): {
19
+ data: WorkflowStatusResponse | null;
20
+ setData: (updater: (current: WorkflowStatusResponse | null) => WorkflowStatusResponse | null) => void;
21
+ refetch: () => Promise<void>;
22
+ } {
23
+ const [data, setDataInternal] = useState<WorkflowStatusResponse | null>(null);
24
+
25
+ const refetch = useCallback(async () => {
26
+ const res = await fetch(`/api/workflows/${workflowId}/status`);
27
+ if (res.ok) {
28
+ const json = (await res.json()) as WorkflowStatusResponse;
29
+ setDataInternal(json);
30
+ }
31
+ }, [workflowId]);
32
+
33
+ useEffect(() => {
34
+ // Reset data when the workflowId changes so the old workflow's state
35
+ // never briefly shows while the new one is fetching.
36
+ setDataInternal(null);
37
+ refetch();
38
+ const interval = setInterval(refetch, 3000);
39
+ return () => clearInterval(interval);
40
+ }, [refetch]);
41
+
42
+ const setData = useCallback(
43
+ (updater: (current: WorkflowStatusResponse | null) => WorkflowStatusResponse | null) => {
44
+ setDataInternal((prev) => updater(prev));
45
+ },
46
+ []
47
+ );
48
+
49
+ return { data, setData, refetch };
50
+ }
@@ -3,7 +3,7 @@
3
3
  import { useState } from "react";
4
4
  import { Badge } from "@/components/ui/badge";
5
5
  import { Button } from "@/components/ui/button";
6
- import { ExpandableResult } from "./workflow-status-view";
6
+ import { ExpandableResult } from "./shared/step-result";
7
7
  import {
8
8
  CheckCircle,
9
9
  Circle,
@@ -0,0 +1,78 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import Link from "next/link";
5
+ import ReactMarkdown from "react-markdown";
6
+ import remarkGfm from "remark-gfm";
7
+ import { FileText } from "lucide-react";
8
+ import { PROSE_NOTIFICATION } from "@/lib/constants/prose-styles";
9
+ import type { WorkflowStatusDocument } from "@/lib/workflows/types";
10
+
11
+ /**
12
+ * Expandable step result with gradient fade progressive disclosure.
13
+ * Extracted from the legacy workflow-status-view god component so it can be
14
+ * reused by pattern-specific subviews and LoopStatusView without a circular
15
+ * import.
16
+ */
17
+ export function ExpandableResult({ result }: { result: string }) {
18
+ const [expanded, setExpanded] = useState(false);
19
+
20
+ if (!result) return null;
21
+
22
+ return (
23
+ <div className="mt-2">
24
+ <div
25
+ className={`${PROSE_NOTIFICATION} ${
26
+ expanded
27
+ ? "max-h-96 overflow-auto"
28
+ : result.length > 200
29
+ ? "max-h-20 overflow-hidden mask-fade-bottom"
30
+ : ""
31
+ }`}
32
+ >
33
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>{result}</ReactMarkdown>
34
+ </div>
35
+ {result.length > 200 && (
36
+ <button
37
+ type="button"
38
+ className="mt-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
39
+ onClick={() => setExpanded(!expanded)}
40
+ >
41
+ {expanded ? "Show less" : "Show more"}
42
+ </button>
43
+ )}
44
+ </div>
45
+ );
46
+ }
47
+
48
+ /**
49
+ * Document list for a single step or parent task — renders a labeled cluster
50
+ * of links to document detail pages.
51
+ */
52
+ export function DocumentList({
53
+ docs,
54
+ label,
55
+ }: {
56
+ docs: WorkflowStatusDocument[];
57
+ label: string;
58
+ }) {
59
+ if (docs.length === 0) return null;
60
+
61
+ return (
62
+ <div className="mt-3">
63
+ <p className="text-xs font-medium text-muted-foreground mb-1.5">{label}</p>
64
+ <div className="space-y-1">
65
+ {docs.map((doc) => (
66
+ <Link
67
+ key={doc.id}
68
+ href={`/documents/${doc.id}`}
69
+ className="flex items-center gap-2 text-xs text-brand-blue hover:underline"
70
+ >
71
+ <FileText className="h-3 w-3 shrink-0" />
72
+ {doc.originalName}
73
+ </Link>
74
+ ))}
75
+ </div>
76
+ </div>
77
+ );
78
+ }
@@ -0,0 +1,141 @@
1
+ "use client";
2
+
3
+ import { useRouter } from "next/navigation";
4
+ import { Badge } from "@/components/ui/badge";
5
+ import { Button } from "@/components/ui/button";
6
+ import { CardHeader, CardTitle } from "@/components/ui/card";
7
+ import {
8
+ Play,
9
+ Pencil,
10
+ Copy,
11
+ RotateCcw,
12
+ Trash2,
13
+ FolderKanban,
14
+ } from "lucide-react";
15
+ import { workflowStatusVariant, patternLabels } from "@/lib/constants/status-colors";
16
+ import { IconCircle, getWorkflowIconFromName } from "@/lib/constants/card-icons";
17
+ import type { WorkflowStatusResponse } from "@/lib/workflows/types";
18
+
19
+ /**
20
+ * Pattern-agnostic header card for the workflow detail page. Renders the
21
+ * workflow name, pattern label, project/run badges, status badge, and the
22
+ * action buttons (Execute, Edit, Clone, Re-run, Delete). Each subview passes
23
+ * callbacks and the narrowed-arm `data` object.
24
+ *
25
+ * This component is deliberately read-only with respect to polling state —
26
+ * subviews own their own `executing` state so the Execute button can show
27
+ * the "Starting..." label without a round-trip through the router.
28
+ */
29
+ export function WorkflowHeader({
30
+ data,
31
+ executing,
32
+ canExecute,
33
+ onExecute,
34
+ onRerun,
35
+ onDelete,
36
+ }: {
37
+ data: WorkflowStatusResponse;
38
+ executing: boolean;
39
+ /** Subviews decide when Execute makes sense (e.g. loop workflows hide it in favour of the loop's own start/pause controls). */
40
+ canExecute: boolean;
41
+ onExecute: () => void;
42
+ onRerun: () => void;
43
+ onDelete: () => void;
44
+ }) {
45
+ const router = useRouter();
46
+ const hasDefinition = !!data.definition;
47
+
48
+ return (
49
+ <CardHeader>
50
+ <div className="flex items-center justify-between">
51
+ <div className="flex items-center gap-3">
52
+ <IconCircle
53
+ icon={getWorkflowIconFromName(data.name, data.pattern).icon}
54
+ colors={getWorkflowIconFromName(data.name, data.pattern).colors}
55
+ />
56
+ <div>
57
+ <CardTitle>{data.name}</CardTitle>
58
+ <p className="text-sm text-muted-foreground mt-1">
59
+ {patternLabels[data.pattern] ?? data.pattern}
60
+ </p>
61
+ <div className="flex items-center gap-2 mt-1">
62
+ {data.projectId && (
63
+ <Badge
64
+ variant="outline"
65
+ className="text-xs cursor-pointer hover:bg-accent gap-1"
66
+ onClick={() => router.push(`/projects/${data.projectId}`)}
67
+ >
68
+ <FolderKanban className="h-3 w-3" />
69
+ Project
70
+ </Badge>
71
+ )}
72
+ {data.runNumber != null && data.runNumber > 0 && (
73
+ <Badge variant="outline" className="text-xs font-normal">
74
+ Run #{data.runNumber}
75
+ </Badge>
76
+ )}
77
+ </div>
78
+ </div>
79
+ </div>
80
+ <div className="flex items-center gap-2">
81
+ <Badge variant={workflowStatusVariant[data.status] ?? "secondary"}>
82
+ {data.status}
83
+ </Badge>
84
+
85
+ {canExecute && (data.status === "draft" || data.status === "paused") && (
86
+ <Button size="sm" onClick={onExecute} disabled={executing}>
87
+ <Play className="h-3 w-3 mr-1" />
88
+ {executing ? "Starting..." : "Execute"}
89
+ </Button>
90
+ )}
91
+
92
+ {["draft", "completed", "failed"].includes(data.status) && hasDefinition && (
93
+ <Button
94
+ variant="outline"
95
+ size="sm"
96
+ onClick={() => router.push(`/workflows/${data.id}/edit`)}
97
+ >
98
+ <Pencil className="h-3.5 w-3.5 mr-1.5" />
99
+ Edit
100
+ </Button>
101
+ )}
102
+
103
+ {hasDefinition && (
104
+ <Button
105
+ variant="outline"
106
+ size="sm"
107
+ onClick={() => router.push(`/workflows/${data.id}/edit?clone=true`)}
108
+ >
109
+ <Copy className="h-3.5 w-3.5 mr-1.5" />
110
+ Clone
111
+ </Button>
112
+ )}
113
+
114
+ {(data.status === "completed" || data.status === "failed") && (
115
+ <Button
116
+ variant="outline"
117
+ size="sm"
118
+ onClick={onRerun}
119
+ disabled={executing}
120
+ >
121
+ <RotateCcw className="h-3.5 w-3.5 mr-1.5" />
122
+ Re-run
123
+ </Button>
124
+ )}
125
+
126
+ {data.status !== "active" && (
127
+ <Button
128
+ variant="outline"
129
+ size="sm"
130
+ className="text-destructive hover:text-destructive"
131
+ onClick={onDelete}
132
+ >
133
+ <Trash2 className="h-3.5 w-3.5 mr-1.5" />
134
+ Delete
135
+ </Button>
136
+ )}
137
+ </div>
138
+ </div>
139
+ </CardHeader>
140
+ );
141
+ }
@@ -0,0 +1,36 @@
1
+ import { Card, CardContent, CardHeader } from "@/components/ui/card";
2
+ import { Skeleton } from "@/components/ui/skeleton";
3
+
4
+ /**
5
+ * Loading state shown by the workflow detail router while the first poll
6
+ * tick is in flight. Extracted from the router so the router itself stays
7
+ * within its ≤80-line budget per TDR-031.
8
+ */
9
+ export function WorkflowLoadingSkeleton() {
10
+ return (
11
+ <Card>
12
+ <CardHeader>
13
+ <div className="flex items-center justify-between">
14
+ <div>
15
+ <Skeleton className="h-6 w-48 mb-2" />
16
+ <Skeleton className="h-4 w-24" />
17
+ </div>
18
+ <Skeleton className="h-6 w-16 rounded-full" />
19
+ </div>
20
+ </CardHeader>
21
+ <CardContent>
22
+ <div className="space-y-3">
23
+ {[1, 2, 3].map((i) => (
24
+ <div key={i} className="flex items-start gap-3">
25
+ <Skeleton className="h-4 w-4 rounded-full mt-0.5" />
26
+ <div className="flex-1">
27
+ <Skeleton className="h-4 w-32 mb-1" />
28
+ <Skeleton className="h-3 w-full" />
29
+ </div>
30
+ </div>
31
+ ))}
32
+ </div>
33
+ </CardContent>
34
+ </Card>
35
+ );
36
+ }