stagent 0.9.6 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (396) hide show
  1. package/README.md +20 -44
  2. package/dist/cli.js +66 -18
  3. package/docs/.coverage-gaps.json +144 -56
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/agent-intelligence.md +12 -2
  6. package/docs/features/chat.md +40 -5
  7. package/docs/features/cost-usage.md +1 -1
  8. package/docs/features/documents.md +5 -2
  9. package/docs/features/inbox-notifications.md +10 -2
  10. package/docs/features/keyboard-navigation.md +12 -3
  11. package/docs/features/provider-runtimes.md +20 -2
  12. package/docs/features/schedules.md +32 -4
  13. package/docs/features/settings.md +28 -5
  14. package/docs/features/shared-components.md +7 -3
  15. package/docs/features/tables.md +11 -2
  16. package/docs/features/tool-permissions.md +6 -2
  17. package/docs/features/workflows.md +14 -4
  18. package/docs/index.md +1 -1
  19. package/docs/journeys/developer.md +39 -2
  20. package/docs/journeys/personal-use.md +32 -8
  21. package/docs/journeys/power-user.md +45 -14
  22. package/docs/journeys/work-use.md +17 -8
  23. package/docs/manifest.json +15 -15
  24. package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +1691 -0
  25. package/docs/superpowers/plans/2026-04-08-schedule-orchestration.md +2983 -0
  26. package/docs/superpowers/plans/2026-04-11-schedule-maxturns-api-control.md +551 -0
  27. package/docs/superpowers/plans/2026-04-11-task-create-profile-validation.md +864 -0
  28. package/docs/superpowers/plans/2026-04-11-task-runtime-stagent-mcp-injection.md +739 -0
  29. package/docs/superpowers/plans/2026-04-14-chat-command-namespace-refactor.md +1390 -0
  30. package/docs/superpowers/plans/2026-04-14-chat-environment-integration.md +1561 -0
  31. package/docs/superpowers/plans/2026-04-14-chat-polish-bundle-v1.md +1219 -0
  32. package/docs/superpowers/plans/2026-04-14-chat-session-persistence-provider-closeout.md +399 -0
  33. package/docs/superpowers/specs/2026-04-08-chat-sse-resilience-hotfix-design.md +201 -0
  34. package/docs/superpowers/specs/2026-04-08-schedule-orchestration-design.md +371 -0
  35. package/docs/superpowers/specs/2026-04-08-swarm-visibility-design.md +213 -0
  36. package/next.config.mjs +1 -0
  37. package/package.json +3 -2
  38. package/src/__tests__/instrumentation-smoke.test.ts +15 -0
  39. package/src/app/analytics/page.tsx +1 -21
  40. package/src/app/api/chat/conversations/[id]/messages/route.ts +22 -1
  41. package/src/app/api/chat/conversations/[id]/skills/__tests__/activate.test.ts +141 -0
  42. package/src/app/api/chat/conversations/[id]/skills/activate/route.ts +74 -0
  43. package/src/app/api/chat/conversations/[id]/skills/deactivate/route.ts +33 -0
  44. package/src/app/api/chat/export/route.ts +52 -0
  45. package/src/app/api/chat/files/search/route.ts +50 -0
  46. package/src/app/api/diagnostics/chat-streams/route.ts +65 -0
  47. package/src/app/api/environment/rescan-if-stale/__tests__/route.test.ts +45 -0
  48. package/src/app/api/environment/rescan-if-stale/route.ts +23 -0
  49. package/src/app/api/environment/skills/route.ts +13 -0
  50. package/src/app/api/instance/config/route.ts +41 -0
  51. package/src/app/api/instance/init/route.ts +34 -0
  52. package/src/app/api/instance/upgrade/check/route.ts +26 -0
  53. package/src/app/api/instance/upgrade/route.ts +96 -0
  54. package/src/app/api/instance/upgrade/status/route.ts +35 -0
  55. package/src/app/api/memory/route.ts +0 -11
  56. package/src/app/api/notifications/route.ts +4 -2
  57. package/src/app/api/projects/[id]/route.ts +5 -155
  58. package/src/app/api/projects/__tests__/delete-project.test.ts +10 -19
  59. package/src/app/api/schedules/[id]/execute/route.ts +111 -0
  60. package/src/app/api/schedules/[id]/route.ts +9 -1
  61. package/src/app/api/schedules/__tests__/execute-route.test.ts +118 -0
  62. package/src/app/api/schedules/route.ts +3 -12
  63. package/src/app/api/settings/chat/pins/route.ts +94 -0
  64. package/src/app/api/settings/chat/saved-searches/__tests__/route.test.ts +119 -0
  65. package/src/app/api/settings/chat/saved-searches/route.ts +79 -0
  66. package/src/app/api/settings/environment/route.ts +26 -0
  67. package/src/app/api/settings/openai/login/route.ts +22 -0
  68. package/src/app/api/settings/openai/logout/route.ts +7 -0
  69. package/src/app/api/settings/openai/route.ts +21 -1
  70. package/src/app/api/settings/providers/route.ts +35 -8
  71. package/src/app/api/tables/[id]/enrich/__tests__/route.test.ts +153 -0
  72. package/src/app/api/tables/[id]/enrich/plan/route.ts +98 -0
  73. package/src/app/api/tables/[id]/enrich/route.ts +147 -0
  74. package/src/app/api/tables/[id]/enrich/runs/route.ts +25 -0
  75. package/src/app/api/tasks/[id]/execute/route.ts +52 -33
  76. package/src/app/api/tasks/[id]/respond/route.ts +31 -15
  77. package/src/app/api/tasks/[id]/resume/route.ts +24 -3
  78. package/src/app/api/workflows/[id]/resume/route.ts +59 -0
  79. package/src/app/api/workflows/[id]/status/route.ts +22 -8
  80. package/src/app/api/workspace/context/route.ts +2 -0
  81. package/src/app/api/workspace/fix-data-dir/route.ts +81 -0
  82. package/src/app/chat/page.tsx +11 -0
  83. package/src/app/documents/page.tsx +4 -1
  84. package/src/app/inbox/page.tsx +12 -5
  85. package/src/app/layout.tsx +42 -21
  86. package/src/app/page.tsx +0 -2
  87. package/src/app/settings/page.tsx +8 -9
  88. package/src/components/chat/__tests__/capability-banner.test.tsx +38 -0
  89. package/src/components/chat/__tests__/chat-session-provider.test.tsx +573 -0
  90. package/src/components/chat/__tests__/skill-row.test.tsx +91 -0
  91. package/src/components/chat/capability-banner.tsx +68 -0
  92. package/src/components/chat/chat-command-popover.tsx +670 -49
  93. package/src/components/chat/chat-input.tsx +104 -10
  94. package/src/components/chat/chat-message.tsx +12 -3
  95. package/src/components/chat/chat-session-provider.tsx +790 -0
  96. package/src/components/chat/chat-shell.tsx +151 -401
  97. package/src/components/chat/command-tab-bar.tsx +68 -0
  98. package/src/components/chat/conversation-template-picker.tsx +421 -0
  99. package/src/components/chat/help-dialog.tsx +39 -0
  100. package/src/components/chat/skill-composition-conflict-dialog.tsx +96 -0
  101. package/src/components/chat/skill-row.tsx +147 -0
  102. package/src/components/documents/document-browser.tsx +37 -19
  103. package/src/components/instance/__tests__/instance-section.test.tsx +125 -0
  104. package/src/components/instance/instance-section.tsx +382 -0
  105. package/src/components/instance/upgrade-badge.tsx +219 -0
  106. package/src/components/notifications/__tests__/batch-proposal-review.test.tsx +95 -0
  107. package/src/components/notifications/__tests__/notification-item.test.tsx +106 -0
  108. package/src/components/notifications/__tests__/permission-response-actions.test.tsx +70 -0
  109. package/src/components/notifications/batch-proposal-review.tsx +20 -5
  110. package/src/components/notifications/inbox-list.tsx +11 -2
  111. package/src/components/notifications/notification-item.tsx +56 -2
  112. package/src/components/notifications/pending-approval-host.tsx +56 -37
  113. package/src/components/notifications/permission-response-actions.tsx +155 -1
  114. package/src/components/schedules/schedule-create-sheet.tsx +19 -1
  115. package/src/components/schedules/schedule-edit-sheet.tsx +20 -1
  116. package/src/components/schedules/schedule-form.tsx +31 -0
  117. package/src/components/settings/__tests__/providers-runtimes-section.test.tsx +149 -0
  118. package/src/components/settings/auth-method-selector.tsx +19 -4
  119. package/src/components/settings/auth-status-badge.tsx +28 -3
  120. package/src/components/settings/environment-section.tsx +102 -0
  121. package/src/components/settings/openai-chatgpt-auth-control.tsx +278 -0
  122. package/src/components/settings/openai-runtime-section.tsx +7 -1
  123. package/src/components/settings/providers-runtimes-section.tsx +138 -19
  124. package/src/components/shared/__tests__/filter-hint.test.tsx +40 -0
  125. package/src/components/shared/__tests__/saved-searches-manager.test.tsx +147 -0
  126. package/src/components/shared/app-sidebar.tsx +4 -3
  127. package/src/components/shared/command-palette.tsx +266 -7
  128. package/src/components/shared/filter-hint.tsx +70 -0
  129. package/src/components/shared/filter-input.tsx +59 -0
  130. package/src/components/shared/saved-searches-manager.tsx +199 -0
  131. package/src/components/shared/theme-toggle.tsx +5 -24
  132. package/src/components/shared/workspace-indicator.tsx +61 -2
  133. package/src/components/tables/__tests__/table-enrichment-sheet.test.tsx +130 -0
  134. package/src/components/tables/table-create-sheet.tsx +4 -0
  135. package/src/components/tables/table-enrichment-runs.tsx +103 -0
  136. package/src/components/tables/table-enrichment-sheet.tsx +538 -0
  137. package/src/components/tables/table-spreadsheet.tsx +29 -5
  138. package/src/components/tables/table-toolbar.tsx +10 -1
  139. package/src/components/tasks/kanban-board.tsx +1 -0
  140. package/src/components/tasks/kanban-column.tsx +53 -14
  141. package/src/components/tasks/task-bento-grid.tsx +31 -2
  142. package/src/components/tasks/task-card.tsx +29 -3
  143. package/src/components/tasks/task-chip-bar.tsx +54 -1
  144. package/src/components/tasks/task-result-renderer.tsx +1 -1
  145. package/src/components/workflows/delay-step-body.tsx +109 -0
  146. package/src/components/workflows/hooks/use-workflow-status.ts +50 -0
  147. package/src/components/workflows/loop-status-view.tsx +1 -1
  148. package/src/components/workflows/shared/step-result.tsx +78 -0
  149. package/src/components/workflows/shared/workflow-header.tsx +141 -0
  150. package/src/components/workflows/shared/workflow-loading-skeleton.tsx +36 -0
  151. package/src/components/workflows/swarm-dashboard.tsx +2 -15
  152. package/src/components/workflows/views/loop-pattern-view.tsx +137 -0
  153. package/src/components/workflows/views/sequence-pattern-view.tsx +511 -0
  154. package/src/components/workflows/workflow-form-view.tsx +133 -16
  155. package/src/components/workflows/workflow-status-view.tsx +30 -740
  156. package/src/hooks/__tests__/use-chat-autocomplete-tabs.test.ts +47 -0
  157. package/src/hooks/__tests__/use-saved-searches.test.ts +70 -0
  158. package/src/hooks/use-active-skills.ts +110 -0
  159. package/src/hooks/use-chat-autocomplete.ts +120 -7
  160. package/src/hooks/use-enriched-skills.ts +19 -0
  161. package/src/hooks/use-pinned-entries.ts +104 -0
  162. package/src/hooks/use-recent-user-messages.ts +19 -0
  163. package/src/hooks/use-saved-searches.ts +142 -0
  164. package/src/instrumentation-node.ts +94 -0
  165. package/src/instrumentation.ts +4 -48
  166. package/src/lib/agents/__tests__/claude-agent-sdk-options.test.ts +56 -0
  167. package/src/lib/agents/__tests__/claude-agent.test.ts +212 -0
  168. package/src/lib/agents/__tests__/execution-manager.test.ts +1 -27
  169. package/src/lib/agents/__tests__/failure-reason.test.ts +68 -0
  170. package/src/lib/agents/__tests__/learned-context.test.ts +0 -11
  171. package/src/lib/agents/__tests__/learning-session.test.ts +158 -0
  172. package/src/lib/agents/__tests__/pattern-extractor.test.ts +48 -0
  173. package/src/lib/agents/__tests__/task-dispatch.test.ts +166 -0
  174. package/src/lib/agents/__tests__/tool-permissions.test.ts +60 -0
  175. package/src/lib/agents/claude-agent.ts +217 -21
  176. package/src/lib/agents/execution-manager.ts +0 -35
  177. package/src/lib/agents/handoff/bus.ts +2 -2
  178. package/src/lib/agents/learned-context.ts +0 -12
  179. package/src/lib/agents/learning-session.ts +18 -5
  180. package/src/lib/agents/profiles/__tests__/list-fused-profiles.test.ts +110 -0
  181. package/src/lib/agents/profiles/__tests__/registry.test.ts +53 -4
  182. package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +97 -0
  183. package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +36 -0
  184. package/src/lib/agents/profiles/list-fused-profiles.ts +104 -0
  185. package/src/lib/agents/profiles/registry.ts +18 -0
  186. package/src/lib/agents/profiles/types.ts +7 -1
  187. package/src/lib/agents/router.ts +3 -6
  188. package/src/lib/agents/runtime/__tests__/catalog.test.ts +130 -0
  189. package/src/lib/agents/runtime/__tests__/execution-target.test.ts +183 -0
  190. package/src/lib/agents/runtime/__tests__/openai-codex-auth.test.ts +118 -0
  191. package/src/lib/agents/runtime/anthropic-direct.ts +8 -0
  192. package/src/lib/agents/runtime/catalog.ts +121 -0
  193. package/src/lib/agents/runtime/claude-sdk.ts +32 -0
  194. package/src/lib/agents/runtime/codex-app-server-client.ts +11 -5
  195. package/src/lib/agents/runtime/execution-target.ts +456 -0
  196. package/src/lib/agents/runtime/index.ts +4 -0
  197. package/src/lib/agents/runtime/launch-failure.ts +101 -0
  198. package/src/lib/agents/runtime/openai-codex-auth.ts +389 -0
  199. package/src/lib/agents/runtime/openai-codex.ts +64 -60
  200. package/src/lib/agents/runtime/openai-direct.ts +8 -0
  201. package/src/lib/agents/runtime/types.ts +8 -0
  202. package/src/lib/agents/task-dispatch.ts +220 -0
  203. package/src/lib/agents/tool-permissions.ts +16 -1
  204. package/src/lib/book/chapter-mapping.ts +11 -0
  205. package/src/lib/book/content.ts +10 -0
  206. package/src/lib/chat/__tests__/active-skill-injection.test.ts +261 -0
  207. package/src/lib/chat/__tests__/active-streams.test.ts +49 -0
  208. package/src/lib/chat/__tests__/clean-filter-input.test.ts +68 -0
  209. package/src/lib/chat/__tests__/command-tabs.test.ts +68 -0
  210. package/src/lib/chat/__tests__/context-builder-files.test.ts +112 -0
  211. package/src/lib/chat/__tests__/dismissals.test.ts +65 -0
  212. package/src/lib/chat/__tests__/engine-sdk-options.test.ts +117 -0
  213. package/src/lib/chat/__tests__/finalize-safety-net.test.ts +139 -0
  214. package/src/lib/chat/__tests__/reconcile.test.ts +137 -0
  215. package/src/lib/chat/__tests__/skill-conflict.test.ts +35 -0
  216. package/src/lib/chat/__tests__/stream-telemetry.test.ts +151 -0
  217. package/src/lib/chat/__tests__/types.test.ts +28 -0
  218. package/src/lib/chat/active-skills.ts +31 -0
  219. package/src/lib/chat/active-streams.ts +27 -0
  220. package/src/lib/chat/clean-filter-input.ts +30 -0
  221. package/src/lib/chat/codex-engine.ts +46 -24
  222. package/src/lib/chat/command-tabs.ts +61 -0
  223. package/src/lib/chat/context-builder.ts +146 -4
  224. package/src/lib/chat/dismissals.ts +73 -0
  225. package/src/lib/chat/engine.ts +159 -18
  226. package/src/lib/chat/files/__tests__/search.test.ts +135 -0
  227. package/src/lib/chat/files/expand-mention.ts +76 -0
  228. package/src/lib/chat/files/search.ts +99 -0
  229. package/src/lib/chat/reconcile.ts +117 -0
  230. package/src/lib/chat/skill-composition.ts +210 -0
  231. package/src/lib/chat/skill-conflict.ts +105 -0
  232. package/src/lib/chat/stagent-tools.ts +7 -19
  233. package/src/lib/chat/stream-telemetry.ts +137 -0
  234. package/src/lib/chat/suggested-prompts.ts +28 -1
  235. package/src/lib/chat/system-prompt.ts +48 -1
  236. package/src/lib/chat/tool-catalog.ts +35 -4
  237. package/src/lib/chat/tools/__tests__/enrich-table-tool.test.ts +127 -0
  238. package/src/lib/chat/tools/__tests__/profile-tools.test.ts +51 -0
  239. package/src/lib/chat/tools/__tests__/schedule-tools.test.ts +261 -0
  240. package/src/lib/chat/tools/__tests__/settings-tools.test.ts +294 -0
  241. package/src/lib/chat/tools/__tests__/skill-tools.test.ts +474 -0
  242. package/src/lib/chat/tools/__tests__/task-tools.test.ts +399 -0
  243. package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +351 -0
  244. package/src/lib/chat/tools/blueprint-tools.ts +190 -0
  245. package/src/lib/chat/tools/document-tools.ts +29 -13
  246. package/src/lib/chat/tools/helpers.ts +41 -0
  247. package/src/lib/chat/tools/notification-tools.ts +9 -5
  248. package/src/lib/chat/tools/profile-tools.ts +120 -23
  249. package/src/lib/chat/tools/project-tools.ts +33 -0
  250. package/src/lib/chat/tools/schedule-tools.ts +44 -11
  251. package/src/lib/chat/tools/skill-tools.ts +183 -0
  252. package/src/lib/chat/tools/table-tools.ts +71 -0
  253. package/src/lib/chat/tools/task-tools.ts +89 -21
  254. package/src/lib/chat/tools/workflow-tools.ts +275 -32
  255. package/src/lib/chat/types.ts +15 -0
  256. package/src/lib/constants/settings.ts +10 -18
  257. package/src/lib/data/__tests__/clear.test.ts +56 -2
  258. package/src/lib/data/clear.ts +17 -16
  259. package/src/lib/data/delete-project.ts +171 -0
  260. package/src/lib/db/__tests__/bootstrap.test.ts +1 -1
  261. package/src/lib/db/bootstrap.ts +62 -16
  262. package/src/lib/db/index.ts +5 -0
  263. package/src/lib/db/migrations/0009_add_app_instances.sql +25 -0
  264. package/src/lib/db/migrations/0024_add_workflow_resume_at.sql +10 -0
  265. package/src/lib/db/migrations/0025_drop_app_instances.sql +3 -0
  266. package/src/lib/db/migrations/0026_drop_license.sql +3 -0
  267. package/src/lib/db/migrations/meta/_journal.json +21 -0
  268. package/src/lib/db/schema.ts +94 -23
  269. package/src/lib/environment/__tests__/auto-promote.test.ts +132 -0
  270. package/src/lib/environment/__tests__/list-skills-enriched.test.ts +55 -0
  271. package/src/lib/environment/__tests__/skill-enrichment.test.ts +129 -0
  272. package/src/lib/environment/__tests__/skill-recommendations.test.ts +87 -0
  273. package/src/lib/environment/data.ts +9 -0
  274. package/src/lib/environment/list-skills.ts +176 -0
  275. package/src/lib/environment/parsers/__tests__/skill.test.ts +54 -0
  276. package/src/lib/environment/parsers/skill.ts +26 -5
  277. package/src/lib/environment/profile-generator.ts +54 -0
  278. package/src/lib/environment/skill-enrichment.ts +106 -0
  279. package/src/lib/environment/skill-recommendations.ts +66 -0
  280. package/src/lib/environment/workspace-context.ts +13 -1
  281. package/src/lib/filters/__tests__/parse.quoted.test.ts +40 -0
  282. package/src/lib/filters/__tests__/parse.test.ts +135 -0
  283. package/src/lib/filters/parse.ts +86 -0
  284. package/src/lib/import/dedup.ts +4 -54
  285. package/src/lib/instance/__tests__/bootstrap.test.ts +362 -0
  286. package/src/lib/instance/__tests__/detect.test.ts +115 -0
  287. package/src/lib/instance/__tests__/fingerprint.test.ts +48 -0
  288. package/src/lib/instance/__tests__/git-ops.test.ts +95 -0
  289. package/src/lib/instance/__tests__/settings.test.ts +83 -0
  290. package/src/lib/instance/__tests__/upgrade-poller.test.ts +181 -0
  291. package/src/lib/instance/bootstrap.ts +270 -0
  292. package/src/lib/instance/detect.ts +49 -0
  293. package/src/lib/instance/fingerprint.ts +76 -0
  294. package/src/lib/instance/git-ops.ts +95 -0
  295. package/src/lib/instance/settings.ts +61 -0
  296. package/src/lib/instance/types.ts +77 -0
  297. package/src/lib/instance/upgrade-poller.ts +205 -0
  298. package/src/lib/notifications/__tests__/visibility.test.ts +51 -0
  299. package/src/lib/notifications/visibility.ts +33 -0
  300. package/src/lib/schedules/__tests__/collision-check.test.ts +93 -0
  301. package/src/lib/schedules/__tests__/config.test.ts +62 -0
  302. package/src/lib/schedules/__tests__/firing-metrics.test.ts +99 -0
  303. package/src/lib/schedules/__tests__/integration.test.ts +82 -0
  304. package/src/lib/schedules/__tests__/slot-claim.test.ts +242 -0
  305. package/src/lib/schedules/__tests__/tick-scheduler.test.ts +102 -0
  306. package/src/lib/schedules/__tests__/turn-budget.test.ts +228 -0
  307. package/src/lib/schedules/collision-check.ts +105 -0
  308. package/src/lib/schedules/config.ts +53 -0
  309. package/src/lib/schedules/scheduler.ts +236 -17
  310. package/src/lib/schedules/slot-claim.ts +105 -0
  311. package/src/lib/settings/__tests__/openai-auth.test.ts +101 -0
  312. package/src/lib/settings/__tests__/openai-login-manager.test.ts +64 -0
  313. package/src/lib/settings/__tests__/runtime-setup.test.ts +33 -0
  314. package/src/lib/settings/openai-auth.ts +105 -10
  315. package/src/lib/settings/openai-login-manager.ts +260 -0
  316. package/src/lib/settings/runtime-setup.ts +14 -4
  317. package/src/lib/tables/__tests__/enrichment-planner.test.ts +124 -0
  318. package/src/lib/tables/__tests__/enrichment.test.ts +147 -0
  319. package/src/lib/tables/enrichment-planner.ts +454 -0
  320. package/src/lib/tables/enrichment.ts +328 -0
  321. package/src/lib/tables/query-builder.ts +5 -2
  322. package/src/lib/tables/trigger-evaluator.ts +3 -2
  323. package/src/lib/theme.ts +71 -0
  324. package/src/lib/usage/ledger.ts +2 -18
  325. package/src/lib/util/__tests__/similarity.test.ts +106 -0
  326. package/src/lib/util/similarity.ts +77 -0
  327. package/src/lib/utils/format-timestamp.ts +24 -0
  328. package/src/lib/utils/stagent-paths.ts +12 -0
  329. package/src/lib/validators/__tests__/blueprint.test.ts +172 -0
  330. package/src/lib/validators/__tests__/settings.test.ts +10 -0
  331. package/src/lib/validators/blueprint.ts +70 -9
  332. package/src/lib/validators/profile.ts +2 -2
  333. package/src/lib/validators/settings.ts +3 -1
  334. package/src/lib/workflows/__tests__/delay.test.ts +196 -0
  335. package/src/lib/workflows/__tests__/engine.test.ts +8 -0
  336. package/src/lib/workflows/__tests__/loop-executor.test.ts +54 -0
  337. package/src/lib/workflows/__tests__/post-action.test.ts +108 -0
  338. package/src/lib/workflows/blueprints/__tests__/render-prompt.test.ts +124 -0
  339. package/src/lib/workflows/blueprints/instantiator.ts +22 -1
  340. package/src/lib/workflows/blueprints/render-prompt.ts +71 -0
  341. package/src/lib/workflows/blueprints/types.ts +16 -2
  342. package/src/lib/workflows/delay.ts +106 -0
  343. package/src/lib/workflows/engine.ts +212 -7
  344. package/src/lib/workflows/loop-executor.ts +349 -24
  345. package/src/lib/workflows/post-action.ts +91 -0
  346. package/src/lib/workflows/types.ts +166 -1
  347. package/src/test/setup.ts +10 -0
  348. package/src/app/api/license/checkout/route.ts +0 -28
  349. package/src/app/api/license/portal/route.ts +0 -26
  350. package/src/app/api/license/route.ts +0 -89
  351. package/src/app/api/license/usage/route.ts +0 -63
  352. package/src/app/api/marketplace/browse/route.ts +0 -15
  353. package/src/app/api/marketplace/import/route.ts +0 -28
  354. package/src/app/api/marketplace/publish/route.ts +0 -40
  355. package/src/app/api/onboarding/email/route.ts +0 -53
  356. package/src/app/api/settings/telemetry/route.ts +0 -14
  357. package/src/app/api/sync/export/route.ts +0 -54
  358. package/src/app/api/sync/restore/route.ts +0 -37
  359. package/src/app/api/sync/sessions/route.ts +0 -24
  360. package/src/app/auth/callback/route.ts +0 -73
  361. package/src/app/marketplace/page.tsx +0 -19
  362. package/src/components/analytics/analytics-gate-card.tsx +0 -101
  363. package/src/components/marketplace/blueprint-card.tsx +0 -61
  364. package/src/components/marketplace/marketplace-browser.tsx +0 -131
  365. package/src/components/onboarding/email-capture-card.tsx +0 -104
  366. package/src/components/settings/activation-form.tsx +0 -95
  367. package/src/components/settings/cloud-account-section.tsx +0 -147
  368. package/src/components/settings/cloud-sync-section.tsx +0 -155
  369. package/src/components/settings/subscription-section.tsx +0 -410
  370. package/src/components/settings/telemetry-section.tsx +0 -80
  371. package/src/components/shared/premium-gate-overlay.tsx +0 -50
  372. package/src/components/shared/schedule-gate-dialog.tsx +0 -64
  373. package/src/components/shared/upgrade-banner.tsx +0 -112
  374. package/src/hooks/use-supabase-auth.ts +0 -79
  375. package/src/lib/billing/email.ts +0 -54
  376. package/src/lib/billing/products.ts +0 -80
  377. package/src/lib/billing/stripe.ts +0 -101
  378. package/src/lib/cloud/supabase-browser.ts +0 -32
  379. package/src/lib/cloud/supabase-client.ts +0 -56
  380. package/src/lib/license/__tests__/features.test.ts +0 -56
  381. package/src/lib/license/__tests__/key-format.test.ts +0 -88
  382. package/src/lib/license/__tests__/manager.test.ts +0 -64
  383. package/src/lib/license/__tests__/tier-limits.test.ts +0 -79
  384. package/src/lib/license/cloud-validation.ts +0 -60
  385. package/src/lib/license/features.ts +0 -44
  386. package/src/lib/license/key-format.ts +0 -101
  387. package/src/lib/license/limit-check.ts +0 -111
  388. package/src/lib/license/limit-queries.ts +0 -51
  389. package/src/lib/license/manager.ts +0 -345
  390. package/src/lib/license/notifications.ts +0 -59
  391. package/src/lib/license/tier-limits.ts +0 -71
  392. package/src/lib/marketplace/marketplace-client.ts +0 -107
  393. package/src/lib/sync/cloud-sync.ts +0 -235
  394. package/src/lib/telemetry/conversion-events.ts +0 -71
  395. package/src/lib/telemetry/queue.ts +0 -122
  396. package/src/lib/validators/license.ts +0 -33
@@ -0,0 +1,538 @@
1
+ "use client";
2
+
3
+ import { useEffect, useMemo, useState } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Input } from "@/components/ui/input";
7
+ import { Label } from "@/components/ui/label";
8
+ import {
9
+ Select,
10
+ SelectContent,
11
+ SelectItem,
12
+ SelectTrigger,
13
+ SelectValue,
14
+ } from "@/components/ui/select";
15
+ import {
16
+ Sheet,
17
+ SheetContent,
18
+ SheetDescription,
19
+ SheetFooter,
20
+ SheetHeader,
21
+ SheetTitle,
22
+ } from "@/components/ui/sheet";
23
+ import { Textarea } from "@/components/ui/textarea";
24
+ import { Switch } from "@/components/ui/switch";
25
+ import { Badge } from "@/components/ui/badge";
26
+ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
27
+ import { Separator } from "@/components/ui/separator";
28
+ import { toast } from "sonner";
29
+ import type { ColumnDef, FilterOperator } from "@/lib/tables/types";
30
+ import type { EnrichmentPlan } from "@/lib/tables/enrichment-planner";
31
+
32
+ interface ProfileOption {
33
+ id: string;
34
+ name: string;
35
+ }
36
+
37
+ interface TableEnrichmentSheetProps {
38
+ open: boolean;
39
+ onOpenChange: (open: boolean) => void;
40
+ tableId: string;
41
+ columns: ColumnDef[];
42
+ onLaunched?: () => void;
43
+ }
44
+
45
+ const SUPPORTED_TYPES = new Set([
46
+ "text",
47
+ "number",
48
+ "boolean",
49
+ "select",
50
+ "url",
51
+ "email",
52
+ ]);
53
+
54
+ const FILTER_OPERATORS: Array<{ value: FilterOperator; label: string }> = [
55
+ { value: "eq", label: "Equals" },
56
+ { value: "neq", label: "Not equal" },
57
+ { value: "contains", label: "Contains" },
58
+ { value: "starts_with", label: "Starts with" },
59
+ { value: "gt", label: "Greater than" },
60
+ { value: "gte", label: "Greater than or equal" },
61
+ { value: "lt", label: "Less than" },
62
+ { value: "lte", label: "Less than or equal" },
63
+ { value: "is_empty", label: "Is empty" },
64
+ { value: "is_not_empty", label: "Is not empty" },
65
+ ];
66
+
67
+ export function TableEnrichmentSheet({
68
+ open,
69
+ onOpenChange,
70
+ tableId,
71
+ columns,
72
+ onLaunched,
73
+ }: TableEnrichmentSheetProps) {
74
+ const router = useRouter();
75
+ const supportedColumns = useMemo(
76
+ () => columns.filter((column) => SUPPORTED_TYPES.has(column.dataType)),
77
+ [columns]
78
+ );
79
+ const [profiles, setProfiles] = useState<ProfileOption[]>([]);
80
+ const [targetColumn, setTargetColumn] = useState("");
81
+ const [promptMode, setPromptMode] = useState<"auto" | "custom">("auto");
82
+ const [prompt, setPrompt] = useState("");
83
+ const [agentProfileOverride, setAgentProfileOverride] = useState("");
84
+ const [batchSize, setBatchSize] = useState("50");
85
+ const [filterEnabled, setFilterEnabled] = useState(false);
86
+ const [filterColumn, setFilterColumn] = useState("");
87
+ const [filterOperator, setFilterOperator] = useState<FilterOperator>("is_empty");
88
+ const [filterValue, setFilterValue] = useState("");
89
+ const [preview, setPreview] = useState<EnrichmentPlan | null>(null);
90
+ const [previewSignature, setPreviewSignature] = useState("");
91
+ const [previewing, setPreviewing] = useState(false);
92
+ const [launching, setLaunching] = useState(false);
93
+
94
+ const currentSignature = JSON.stringify({
95
+ targetColumn,
96
+ promptMode,
97
+ prompt,
98
+ agentProfileOverride,
99
+ batchSize,
100
+ filterEnabled,
101
+ filterColumn,
102
+ filterOperator,
103
+ filterValue,
104
+ });
105
+ const previewIsStale = preview !== null && previewSignature !== currentSignature;
106
+ const selectedTargetColumn = supportedColumns.find(
107
+ (column) => column.name === targetColumn
108
+ );
109
+
110
+ useEffect(() => {
111
+ if (!open) return;
112
+ fetch("/api/profiles")
113
+ .then((res) => (res.ok ? res.json() : []))
114
+ .then((data: Array<{ id: string; name: string }>) => {
115
+ setProfiles(data.map((profile) => ({ id: profile.id, name: profile.name })));
116
+ })
117
+ .catch(() => setProfiles([]));
118
+ }, [open]);
119
+
120
+ useEffect(() => {
121
+ if (!open || supportedColumns.length === 0) return;
122
+ setTargetColumn((current) => current || supportedColumns[0].name);
123
+ setFilterColumn((current) => current || supportedColumns[0].name);
124
+ }, [open, supportedColumns]);
125
+
126
+ useEffect(() => {
127
+ if (!open) {
128
+ setPreview(null);
129
+ setPreviewSignature("");
130
+ }
131
+ }, [open]);
132
+
133
+ async function handlePreview() {
134
+ if (!targetColumn) {
135
+ toast.error("Choose a target column first");
136
+ return;
137
+ }
138
+
139
+ setPreviewing(true);
140
+ try {
141
+ const res = await fetch(`/api/tables/${tableId}/enrich/plan`, {
142
+ method: "POST",
143
+ headers: { "Content-Type": "application/json" },
144
+ body: JSON.stringify(buildPayload(false)),
145
+ });
146
+ const data = await res.json();
147
+ if (!res.ok) {
148
+ throw new Error(data.error ?? "Failed to build enrichment plan");
149
+ }
150
+ setPreview(data as EnrichmentPlan);
151
+ setPreviewSignature(currentSignature);
152
+ } catch (error) {
153
+ toast.error(error instanceof Error ? error.message : "Failed to build plan");
154
+ } finally {
155
+ setPreviewing(false);
156
+ }
157
+ }
158
+
159
+ async function handleLaunch() {
160
+ if (!preview) {
161
+ toast.error("Preview the plan before launching");
162
+ return;
163
+ }
164
+ if (previewIsStale) {
165
+ toast.error("Preview is stale. Refresh it before launching.");
166
+ return;
167
+ }
168
+
169
+ setLaunching(true);
170
+ try {
171
+ const res = await fetch(`/api/tables/${tableId}/enrich`, {
172
+ method: "POST",
173
+ headers: { "Content-Type": "application/json" },
174
+ body: JSON.stringify(buildPayload(true)),
175
+ });
176
+ const data = await res.json();
177
+ if (!res.ok) {
178
+ throw new Error(data.error ?? "Failed to launch enrichment");
179
+ }
180
+ toast.success(`Started enrichment for ${data.rowCount} row(s)`);
181
+ onOpenChange(false);
182
+ onLaunched?.();
183
+ router.push(`/workflows/${data.workflowId}`);
184
+ } catch (error) {
185
+ toast.error(error instanceof Error ? error.message : "Failed to launch enrichment");
186
+ } finally {
187
+ setLaunching(false);
188
+ }
189
+ }
190
+
191
+ function buildPayload(includePlan: boolean) {
192
+ const payload: Record<string, unknown> = {
193
+ targetColumn,
194
+ promptMode,
195
+ batchSize: Number(batchSize) || 50,
196
+ };
197
+
198
+ if (prompt.trim()) {
199
+ payload.prompt = prompt.trim();
200
+ }
201
+ if (agentProfileOverride.trim()) {
202
+ payload.agentProfileOverride = agentProfileOverride.trim();
203
+ }
204
+
205
+ const filter = buildFilter();
206
+ if (filter) {
207
+ payload.filter = filter;
208
+ }
209
+
210
+ if (includePlan && preview) {
211
+ payload.plan = preview;
212
+ }
213
+
214
+ return payload;
215
+ }
216
+
217
+ function buildFilter(): Record<string, unknown> | undefined {
218
+ if (!filterEnabled || !filterColumn) return undefined;
219
+ const filter: Record<string, unknown> = {
220
+ column: filterColumn,
221
+ operator: filterOperator,
222
+ };
223
+ if (filterOperator === "is_empty" || filterOperator === "is_not_empty") {
224
+ return filter;
225
+ }
226
+ if (filterValue.trim() === "") return undefined;
227
+ filter.value = coerceFilterValue(filterValue.trim());
228
+ return filter;
229
+ }
230
+
231
+ if (supportedColumns.length === 0) {
232
+ return (
233
+ <Sheet open={open} onOpenChange={onOpenChange}>
234
+ <SheetContent side="right" className="w-[520px] sm:max-w-[520px]">
235
+ <SheetHeader>
236
+ <SheetTitle>Enrich Table</SheetTitle>
237
+ <SheetDescription>
238
+ Review which columns support enrichment before configuring a planner run.
239
+ </SheetDescription>
240
+ </SheetHeader>
241
+ <div className="px-6 py-4">
242
+ <p className="text-sm text-muted-foreground">
243
+ This table does not have any enrichment-compatible columns yet.
244
+ </p>
245
+ </div>
246
+ </SheetContent>
247
+ </Sheet>
248
+ );
249
+ }
250
+
251
+ return (
252
+ <Sheet open={open} onOpenChange={onOpenChange}>
253
+ <SheetContent side="right" className="w-[560px] sm:max-w-[560px] flex flex-col">
254
+ <SheetHeader>
255
+ <SheetTitle>Enrich Table</SheetTitle>
256
+ <SheetDescription>
257
+ Choose a target column, preview the row-by-row plan, then launch the enrichment workflow.
258
+ </SheetDescription>
259
+ </SheetHeader>
260
+
261
+ <div className="flex-1 overflow-y-auto px-6 pb-6 space-y-6">
262
+ <section className="surface-card-muted rounded-lg border p-4 space-y-4">
263
+ <div className="space-y-2">
264
+ <Label htmlFor="target-column">Target Column</Label>
265
+ <Select value={targetColumn} onValueChange={setTargetColumn}>
266
+ <SelectTrigger id="target-column">
267
+ <SelectValue placeholder="Choose a target column" />
268
+ </SelectTrigger>
269
+ <SelectContent>
270
+ {supportedColumns.map((column) => (
271
+ <SelectItem key={column.name} value={column.name}>
272
+ {column.displayName} · {column.dataType}
273
+ </SelectItem>
274
+ ))}
275
+ </SelectContent>
276
+ </Select>
277
+ </div>
278
+
279
+ <div className="space-y-3">
280
+ <div className="flex items-center justify-between gap-3">
281
+ <div>
282
+ <Label>Optional Filter</Label>
283
+ <p className="text-xs text-muted-foreground">
284
+ Narrow the rows before idempotent skip removes already-filled cells.
285
+ </p>
286
+ </div>
287
+ <Switch checked={filterEnabled} onCheckedChange={setFilterEnabled} />
288
+ </div>
289
+
290
+ {filterEnabled && (
291
+ <div className="grid gap-3 sm:grid-cols-2">
292
+ <div className="space-y-2 sm:col-span-2">
293
+ <Label htmlFor="filter-column">Filter Column</Label>
294
+ <Select value={filterColumn} onValueChange={setFilterColumn}>
295
+ <SelectTrigger id="filter-column">
296
+ <SelectValue />
297
+ </SelectTrigger>
298
+ <SelectContent>
299
+ {supportedColumns.map((column) => (
300
+ <SelectItem key={column.name} value={column.name}>
301
+ {column.displayName}
302
+ </SelectItem>
303
+ ))}
304
+ </SelectContent>
305
+ </Select>
306
+ </div>
307
+ <div className="space-y-2">
308
+ <Label htmlFor="filter-operator">Operator</Label>
309
+ <Select
310
+ value={filterOperator}
311
+ onValueChange={(value) => setFilterOperator(value as FilterOperator)}
312
+ >
313
+ <SelectTrigger id="filter-operator">
314
+ <SelectValue />
315
+ </SelectTrigger>
316
+ <SelectContent>
317
+ {FILTER_OPERATORS.map((operator) => (
318
+ <SelectItem key={operator.value} value={operator.value}>
319
+ {operator.label}
320
+ </SelectItem>
321
+ ))}
322
+ </SelectContent>
323
+ </Select>
324
+ </div>
325
+ {filterOperator !== "is_empty" &&
326
+ filterOperator !== "is_not_empty" && (
327
+ <div className="space-y-2">
328
+ <Label htmlFor="filter-value">Value</Label>
329
+ <Input
330
+ id="filter-value"
331
+ value={filterValue}
332
+ onChange={(event) => setFilterValue(event.target.value)}
333
+ />
334
+ </div>
335
+ )}
336
+ </div>
337
+ )}
338
+ </div>
339
+ </section>
340
+
341
+ <section className="surface-card-muted rounded-lg border p-4 space-y-4">
342
+ <div className="space-y-2">
343
+ <Label>Planning Mode</Label>
344
+ <RadioGroup
345
+ value={promptMode}
346
+ onValueChange={(value) => setPromptMode(value as "auto" | "custom")}
347
+ className="gap-2"
348
+ >
349
+ <label className="surface-control rounded-lg border p-3 flex items-start gap-3 cursor-pointer">
350
+ <RadioGroupItem value="auto" />
351
+ <div>
352
+ <p className="text-sm font-medium">Auto plan</p>
353
+ <p className="text-xs text-muted-foreground">
354
+ Planner chooses the row strategy, output contract, and step sequence.
355
+ </p>
356
+ </div>
357
+ </label>
358
+ <label className="surface-control rounded-lg border p-3 flex items-start gap-3 cursor-pointer">
359
+ <RadioGroupItem value="custom" />
360
+ <div>
361
+ <p className="text-sm font-medium">Custom prompt</p>
362
+ <p className="text-xs text-muted-foreground">
363
+ Use one explicit prompt and let the system enforce the final typed contract.
364
+ </p>
365
+ </div>
366
+ </label>
367
+ </RadioGroup>
368
+ </div>
369
+
370
+ <div className="space-y-2">
371
+ <Label htmlFor="planner-prompt">
372
+ {promptMode === "auto" ? "Extra Guidance" : "Custom Prompt"}
373
+ </Label>
374
+ <Textarea
375
+ id="planner-prompt"
376
+ rows={6}
377
+ value={prompt}
378
+ onChange={(event) => setPrompt(event.target.value)}
379
+ placeholder={
380
+ promptMode === "auto"
381
+ ? "Optional. Add operator guidance that should influence the planner."
382
+ : `Describe how to determine the "${selectedTargetColumn?.displayName ?? "target"}" value for each row.`
383
+ }
384
+ />
385
+ </div>
386
+
387
+ <div className="grid gap-3 sm:grid-cols-2">
388
+ <div className="space-y-2">
389
+ <Label htmlFor="planner-profile">Agent Profile Override</Label>
390
+ <Select
391
+ value={agentProfileOverride || "recommended"}
392
+ onValueChange={(value) =>
393
+ setAgentProfileOverride(value === "recommended" ? "" : value)
394
+ }
395
+ >
396
+ <SelectTrigger id="planner-profile">
397
+ <SelectValue />
398
+ </SelectTrigger>
399
+ <SelectContent>
400
+ <SelectItem value="recommended">Use planner recommendation</SelectItem>
401
+ {profiles.map((profile) => (
402
+ <SelectItem key={profile.id} value={profile.id}>
403
+ {profile.name}
404
+ </SelectItem>
405
+ ))}
406
+ </SelectContent>
407
+ </Select>
408
+ </div>
409
+
410
+ <div className="space-y-2">
411
+ <Label htmlFor="batch-size">Batch Size</Label>
412
+ <Input
413
+ id="batch-size"
414
+ inputMode="numeric"
415
+ value={batchSize}
416
+ onChange={(event) => setBatchSize(event.target.value)}
417
+ />
418
+ </div>
419
+ </div>
420
+ </section>
421
+
422
+ <section className="surface-card rounded-lg border p-4 space-y-4">
423
+ <div className="flex items-center justify-between gap-3">
424
+ <div>
425
+ <h3 className="text-sm font-medium">Plan Preview</h3>
426
+ <p className="text-xs text-muted-foreground">
427
+ Strategy, contract, and row estimate before launch.
428
+ </p>
429
+ </div>
430
+ <Button onClick={handlePreview} disabled={previewing}>
431
+ {previewing ? "Planning…" : "Preview Plan"}
432
+ </Button>
433
+ </div>
434
+
435
+ {!preview ? (
436
+ <p className="text-sm text-muted-foreground">
437
+ Preview the plan to inspect the strategy, generated prompts, and typed writeback contract.
438
+ </p>
439
+ ) : (
440
+ <div className="space-y-4">
441
+ {previewIsStale && (
442
+ <div className="rounded-lg border border-status-warning bg-status-warning/10 px-3 py-2 text-xs text-foreground">
443
+ Inputs changed after the last preview. Refresh the plan before launching.
444
+ </div>
445
+ )}
446
+
447
+ <div className="flex items-center gap-2 flex-wrap">
448
+ <Badge>{preview.strategy}</Badge>
449
+ <Badge variant="outline">{preview.agentProfile}</Badge>
450
+ <Badge variant="outline">{preview.eligibleRowCount} eligible rows</Badge>
451
+ </div>
452
+
453
+ <div className="space-y-2">
454
+ <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
455
+ Planner reasoning
456
+ </p>
457
+ <p className="text-sm">{preview.reasoning}</p>
458
+ </div>
459
+
460
+ <div className="space-y-2">
461
+ <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
462
+ Output contract
463
+ </p>
464
+ <div className="surface-control rounded-lg border p-3 space-y-1">
465
+ <p className="text-sm font-medium">
466
+ {preview.targetContract.columnLabel} · {preview.targetContract.dataType}
467
+ </p>
468
+ {preview.targetContract.allowedOptions?.length ? (
469
+ <p className="text-xs text-muted-foreground">
470
+ Allowed options: {preview.targetContract.allowedOptions.join(", ")}
471
+ </p>
472
+ ) : (
473
+ <p className="text-xs text-muted-foreground">
474
+ Final step must return a typed value or NOT_FOUND.
475
+ </p>
476
+ )}
477
+ </div>
478
+ </div>
479
+
480
+ <Separator />
481
+
482
+ <div className="space-y-3">
483
+ {preview.steps.map((step) => (
484
+ <div key={step.id} className="surface-control rounded-lg border p-3 space-y-2">
485
+ <div className="flex items-center justify-between gap-3">
486
+ <div>
487
+ <p className="text-sm font-medium">{step.name}</p>
488
+ <p className="text-xs text-muted-foreground">{step.purpose}</p>
489
+ </div>
490
+ {step.agentProfile && (
491
+ <Badge variant="outline">{step.agentProfile}</Badge>
492
+ )}
493
+ </div>
494
+ <pre className="whitespace-pre-wrap text-xs text-muted-foreground font-mono">
495
+ {step.prompt}
496
+ </pre>
497
+ </div>
498
+ ))}
499
+ </div>
500
+
501
+ {preview.sampleBindings.length > 0 && (
502
+ <div className="space-y-2">
503
+ <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
504
+ Sample row bindings
505
+ </p>
506
+ <pre className="surface-control rounded-lg border p-3 text-xs overflow-x-auto">
507
+ {JSON.stringify(preview.sampleBindings, null, 2)}
508
+ </pre>
509
+ </div>
510
+ )}
511
+ </div>
512
+ )}
513
+ </section>
514
+ </div>
515
+
516
+ <SheetFooter className="px-6">
517
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
518
+ Cancel
519
+ </Button>
520
+ <Button onClick={handleLaunch} disabled={!preview || previewIsStale || launching}>
521
+ {launching ? "Launching…" : "Launch Enrichment"}
522
+ </Button>
523
+ </SheetFooter>
524
+ </SheetContent>
525
+ </Sheet>
526
+ );
527
+ }
528
+
529
+ function coerceFilterValue(value: string): string | number | boolean {
530
+ const lower = value.toLowerCase();
531
+ if (lower === "true") return true;
532
+ if (lower === "false") return false;
533
+ const numeric = Number(value);
534
+ if (!Number.isNaN(numeric) && value.trim() !== "") {
535
+ return numeric;
536
+ }
537
+ return value;
538
+ }
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useState, useCallback, useRef } from "react";
3
+ import { useState, useCallback, useEffect, useRef } from "react";
4
4
  import { Checkbox } from "@/components/ui/checkbox";
5
5
  import {
6
6
  Table,
@@ -20,6 +20,8 @@ import { TableColumnSheet } from "./table-column-sheet";
20
20
  import { TableToolbar } from "./table-toolbar";
21
21
  import { TableImportWizard } from "./table-import-wizard";
22
22
  import { TableRowSheet } from "./table-row-sheet";
23
+ import { TableEnrichmentSheet } from "./table-enrichment-sheet";
24
+ import { TableEnrichmentRuns } from "./table-enrichment-runs";
23
25
  import { EmptyState } from "@/components/shared/empty-state";
24
26
  import { Table2 } from "lucide-react";
25
27
  import { evaluateComputedColumns } from "@/lib/tables/computed";
@@ -61,6 +63,8 @@ export function TableSpreadsheet({
61
63
  const [importOpen, setImportOpen] = useState(false);
62
64
  const [rowSheetOpen, setRowSheetOpen] = useState(false);
63
65
  const [rowSheetRow, setRowSheetRow] = useState<ParsedRow | null>(null);
66
+ const [enrichmentOpen, setEnrichmentOpen] = useState(false);
67
+ const [enrichmentRefreshKey, setEnrichmentRefreshKey] = useState(0);
64
68
 
65
69
  // ── Refresh helpers ─────────────────────────────────────────────────
66
70
 
@@ -178,12 +182,15 @@ export function TableSpreadsheet({
178
182
  []
179
183
  );
180
184
 
181
- // Trigger row refresh when sorts change
185
+ // Trigger row refresh when sorts change without mutating state during render.
182
186
  const prevSortsRef = useRef(sorts);
183
- if (prevSortsRef.current !== sorts) {
187
+ useEffect(() => {
188
+ if (prevSortsRef.current === sorts) {
189
+ return;
190
+ }
184
191
  prevSortsRef.current = sorts;
185
- refreshRows();
186
- }
192
+ void refreshRows();
193
+ }, [sorts, refreshRows]);
187
194
 
188
195
  // ── Selection helpers ───────────────────────────────────────────────
189
196
 
@@ -249,6 +256,12 @@ export function TableSpreadsheet({
249
256
  onAddColumn={() => setColumnSheetOpen(true)}
250
257
  onBulkDelete={handleBulkDelete}
251
258
  onImport={() => setImportOpen(true)}
259
+ onEnrich={() => setEnrichmentOpen(true)}
260
+ />
261
+
262
+ <TableEnrichmentRuns
263
+ tableId={tableId}
264
+ refreshKey={enrichmentRefreshKey}
252
265
  />
253
266
 
254
267
  <div className="rounded-lg border overflow-auto">
@@ -379,6 +392,17 @@ export function TableSpreadsheet({
379
392
  onImported={() => { refreshTable(); refreshRows(); }}
380
393
  />
381
394
 
395
+ <TableEnrichmentSheet
396
+ open={enrichmentOpen}
397
+ onOpenChange={setEnrichmentOpen}
398
+ tableId={tableId}
399
+ columns={columns}
400
+ onLaunched={() => {
401
+ setEnrichmentRefreshKey((current) => current + 1);
402
+ refreshRows();
403
+ }}
404
+ />
405
+
382
406
  {rowSheetRow && (
383
407
  <TableRowSheet
384
408
  tableId={tableId}
@@ -7,7 +7,7 @@ import {
7
7
  DropdownMenuItem,
8
8
  DropdownMenuTrigger,
9
9
  } from "@/components/ui/dropdown-menu";
10
- import { Plus, Columns3, Trash2, Upload, Download } from "lucide-react";
10
+ import { Plus, Columns3, Trash2, Upload, Download, Sparkles } from "lucide-react";
11
11
 
12
12
  interface TableToolbarProps {
13
13
  tableId: string;
@@ -17,6 +17,7 @@ interface TableToolbarProps {
17
17
  onAddColumn: () => void;
18
18
  onBulkDelete: () => void;
19
19
  onImport?: () => void;
20
+ onEnrich?: () => void;
20
21
  }
21
22
 
22
23
  export function TableToolbar({
@@ -27,6 +28,7 @@ export function TableToolbar({
27
28
  onAddColumn,
28
29
  onBulkDelete,
29
30
  onImport,
31
+ onEnrich,
30
32
  }: TableToolbarProps) {
31
33
  function handleExport(format: string) {
32
34
  window.open(`/api/tables/${tableId}/export?format=${format}`, "_blank");
@@ -50,6 +52,13 @@ export function TableToolbar({
50
52
  </Button>
51
53
  )}
52
54
 
55
+ {onEnrich && (
56
+ <Button size="sm" onClick={onEnrich}>
57
+ <Sparkles className="h-4 w-4 mr-1" />
58
+ Enrich
59
+ </Button>
60
+ )}
61
+
53
62
  <DropdownMenu>
54
63
  <DropdownMenuTrigger asChild>
55
64
  <Button variant="outline" size="sm">
@@ -427,6 +427,7 @@ export function KanbanBoard({
427
427
  status={status}
428
428
  tasks={groupedTasks[status]}
429
429
  workflows={groupedWorkflows[status]}
430
+ sortOrder={sortOrder}
430
431
  exitingIds={exitingIds}
431
432
  onTaskClick={handleTaskClick}
432
433
  onAddTask={status === "planned" ? () => router.push("/tasks/new") : undefined}