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,573 @@
1
+ import { act, render, screen, waitFor } from "@testing-library/react";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { useEffect, useRef, useState } from "react";
4
+
5
+ import type { ChatMessageRow } from "@/lib/db/schema";
6
+ import {
7
+ ChatSessionProvider,
8
+ useChatSession,
9
+ } from "@/components/chat/chat-session-provider";
10
+
11
+ // Satisfy the type import linter — we use ChatMessageRow in the Consumer
12
+ // probes below but through inference from session.messages.
13
+ void ({} as ChatMessageRow | undefined);
14
+
15
+ // ── Next.js router mock ──────────────────────────────────────────────
16
+ vi.mock("next/navigation", () => ({
17
+ useRouter: () => ({ replace: vi.fn(), push: vi.fn() }),
18
+ }));
19
+
20
+ // ── Sonner mock ──────────────────────────────────────────────────────
21
+ const toastErrorSpy = vi.fn();
22
+ vi.mock("sonner", () => ({
23
+ toast: {
24
+ error: (...args: unknown[]) => toastErrorSpy(...args),
25
+ },
26
+ }));
27
+
28
+ // ── Test helpers ─────────────────────────────────────────────────────
29
+
30
+ /**
31
+ * Small consumer component that exposes the session value via test ids.
32
+ * Text probes let us assert state without wiring up the full ChatShell.
33
+ */
34
+ function Consumer({ label }: { label?: string }) {
35
+ const session = useChatSession();
36
+ return (
37
+ <div>
38
+ <div data-testid={`${label ?? "c"}-active`}>{session.activeId ?? ""}</div>
39
+ <div data-testid={`${label ?? "c"}-isStreaming`}>
40
+ {String(session.isStreaming)}
41
+ </div>
42
+ <div data-testid={`${label ?? "c"}-messageCount`}>
43
+ {session.messages.length}
44
+ </div>
45
+ <div data-testid={`${label ?? "c"}-assistantContent`}>
46
+ {session.messages
47
+ .filter((m: ChatMessageRow) => m.role === "assistant")
48
+ .map((m: ChatMessageRow) => m.content)
49
+ .join("|")}
50
+ </div>
51
+ <button
52
+ data-testid={`${label ?? "c"}-send`}
53
+ onClick={() => void session.sendMessage("hello")}
54
+ >
55
+ send
56
+ </button>
57
+ <button
58
+ data-testid={`${label ?? "c"}-stop`}
59
+ onClick={() => session.stopStreaming()}
60
+ >
61
+ stop
62
+ </button>
63
+ <button
64
+ data-testid={`${label ?? "c"}-select`}
65
+ onClick={() => session.setActiveConversation("conv-1")}
66
+ >
67
+ select
68
+ </button>
69
+ <button
70
+ data-testid={`${label ?? "c"}-hydrate`}
71
+ onClick={() =>
72
+ session.hydrate({
73
+ conversations: [
74
+ {
75
+ id: "conv-1",
76
+ projectId: null,
77
+ title: "Test conv",
78
+ status: "active",
79
+ runtimeId: "claude-code",
80
+ modelId: "sonnet",
81
+ createdAt: new Date(),
82
+ updatedAt: new Date(),
83
+ archivedAt: null,
84
+ } as unknown as never,
85
+ ],
86
+ initialActiveId: "conv-1",
87
+ })
88
+ }
89
+ >
90
+ hydrate
91
+ </button>
92
+ </div>
93
+ );
94
+ }
95
+
96
+ /**
97
+ * A wrapper that keeps the provider mounted while letting tests mount and
98
+ * unmount a child consumer on demand. This is how we verify that state
99
+ * survives a consumer unmount/remount cycle — the provider is stable, only
100
+ * the child toggles.
101
+ */
102
+ function ProviderWithToggle() {
103
+ const [show, setShow] = useState(true);
104
+ return (
105
+ <ChatSessionProvider>
106
+ <button data-testid="toggle" onClick={() => setShow((v) => !v)}>
107
+ toggle
108
+ </button>
109
+ <div data-testid="consumer-visible">{String(show)}</div>
110
+ {show && <Consumer />}
111
+ </ChatSessionProvider>
112
+ );
113
+ }
114
+
115
+ /**
116
+ * Build a ReadableStream that emits the given SSE chunks as `data: ...` lines.
117
+ * Each chunk is JSON-serialized and prefixed with `data: ` + newline.
118
+ */
119
+ function makeSSEStream(
120
+ chunks: unknown[],
121
+ opts: { closeAfterMs?: number } = {}
122
+ ): ReadableStream<Uint8Array> {
123
+ const encoder = new TextEncoder();
124
+ return new ReadableStream({
125
+ async start(controller) {
126
+ for (const chunk of chunks) {
127
+ const line = `data: ${JSON.stringify(chunk)}\n`;
128
+ controller.enqueue(encoder.encode(line));
129
+ // Tiny yield so React can flush state between chunks.
130
+ await new Promise((r) => setTimeout(r, 0));
131
+ }
132
+ if (opts.closeAfterMs) {
133
+ await new Promise((r) => setTimeout(r, opts.closeAfterMs));
134
+ }
135
+ controller.close();
136
+ },
137
+ });
138
+ }
139
+
140
+ /**
141
+ * Build a ReadableStream that waits indefinitely (useful for testing
142
+ * abort behavior). Signal-aware: closes early if signal aborts.
143
+ */
144
+ function makeHangingStream(signal: AbortSignal): ReadableStream<Uint8Array> {
145
+ return new ReadableStream({
146
+ start(controller) {
147
+ signal.addEventListener("abort", () => {
148
+ controller.error(
149
+ Object.assign(new Error("aborted"), { name: "AbortError" })
150
+ );
151
+ });
152
+ },
153
+ });
154
+ }
155
+
156
+ // ── Suites ───────────────────────────────────────────────────────────
157
+
158
+ describe("ChatSessionProvider", () => {
159
+ beforeEach(() => {
160
+ toastErrorSpy.mockReset();
161
+ vi.stubGlobal("crypto", {
162
+ randomUUID: () => `uuid-${Math.random().toString(36).slice(2, 10)}`,
163
+ });
164
+ });
165
+
166
+ afterEach(() => {
167
+ vi.unstubAllGlobals();
168
+ vi.restoreAllMocks();
169
+ });
170
+
171
+ it("sendMessage accumulates SSE deltas into the assistant message", async () => {
172
+ const fetchMock = vi.fn(async (url: RequestInfo | URL) => {
173
+ const u = url.toString();
174
+ if (u.startsWith("/api/settings/chat")) return new Response(null, { status: 204 });
175
+ if (u.startsWith("/api/chat/models")) return new Response(null, { status: 204 });
176
+ if (u === "/api/chat/conversations" || u.endsWith("/api/chat/conversations")) {
177
+ return new Response(
178
+ JSON.stringify({
179
+ id: "conv-new",
180
+ projectId: null,
181
+ title: "New Chat",
182
+ status: "active",
183
+ runtimeId: "claude-code",
184
+ modelId: "haiku",
185
+ createdAt: new Date().toISOString(),
186
+ updatedAt: new Date().toISOString(),
187
+ }),
188
+ { status: 200 }
189
+ );
190
+ }
191
+ if (u.match(/\/api\/chat\/conversations\/conv-new\/messages$/)) {
192
+ return new Response(
193
+ makeSSEStream([
194
+ { type: "delta", content: "Hello" },
195
+ { type: "delta", content: " world" },
196
+ { type: "done", messageId: "msg-final", quickAccess: [] },
197
+ ]),
198
+ { status: 200 }
199
+ );
200
+ }
201
+ if (u.startsWith("/api/chat/conversations/conv-new")) {
202
+ // GET metadata refresh after "done" event
203
+ return new Response(
204
+ JSON.stringify({ id: "conv-new", title: "Auto Title" }),
205
+ { status: 200 }
206
+ );
207
+ }
208
+ return new Response(null, { status: 404 });
209
+ });
210
+ vi.stubGlobal("fetch", fetchMock);
211
+
212
+ render(
213
+ <ChatSessionProvider>
214
+ <Consumer />
215
+ </ChatSessionProvider>
216
+ );
217
+
218
+ await act(async () => {
219
+ screen.getByTestId("c-send").click();
220
+ });
221
+
222
+ await waitFor(() => {
223
+ expect(screen.getByTestId("c-assistantContent").textContent).toBe(
224
+ "Hello world"
225
+ );
226
+ expect(screen.getByTestId("c-isStreaming").textContent).toBe("false");
227
+ });
228
+ });
229
+
230
+ it("preserves messages across consumer unmount/remount", async () => {
231
+ // Seed state: hydrate with conv-1 (fetch returns empty message list),
232
+ // then send a message and verify it's visible. Then toggle the consumer
233
+ // off and back on and verify the messages are still there.
234
+ const fetchMock = vi.fn(async (url: RequestInfo | URL) => {
235
+ const u = url.toString();
236
+ if (u.startsWith("/api/settings/chat")) return new Response(null, { status: 204 });
237
+ if (u.startsWith("/api/chat/models")) return new Response(null, { status: 204 });
238
+ if (u.match(/\/api\/chat\/conversations\/conv-1\/messages$/)) {
239
+ // Support both GET (select refresh) and POST (send)
240
+ // We can distinguish in a real test but here both return empty/delta
241
+ // If POST, return the SSE stream. Differentiate by checking if there's a body.
242
+ return new Response(
243
+ makeSSEStream([
244
+ { type: "delta", content: "persisted" },
245
+ { type: "done", messageId: "msg-a", quickAccess: [] },
246
+ ]),
247
+ { status: 200 }
248
+ );
249
+ }
250
+ if (u.startsWith("/api/chat/conversations/conv-1")) {
251
+ return new Response(
252
+ JSON.stringify({ id: "conv-1", title: "T" }),
253
+ { status: 200 }
254
+ );
255
+ }
256
+ return new Response(null, { status: 404 });
257
+ });
258
+ vi.stubGlobal("fetch", fetchMock);
259
+
260
+ render(<ProviderWithToggle />);
261
+
262
+ // Hydrate (sets conv-1 as active) and select
263
+ await act(async () => {
264
+ screen.getByTestId("c-hydrate").click();
265
+ });
266
+ await act(async () => {
267
+ screen.getByTestId("c-send").click();
268
+ });
269
+
270
+ await waitFor(() => {
271
+ expect(screen.getByTestId("c-assistantContent").textContent).toBe(
272
+ "persisted"
273
+ );
274
+ });
275
+
276
+ // Unmount the consumer
277
+ await act(async () => {
278
+ screen.getByTestId("toggle").click();
279
+ });
280
+ expect(screen.queryByTestId("c-assistantContent")).toBeNull();
281
+ expect(screen.getByTestId("consumer-visible").textContent).toBe("false");
282
+
283
+ // Remount the consumer — provider state should still be there
284
+ await act(async () => {
285
+ screen.getByTestId("toggle").click();
286
+ });
287
+ await waitFor(() => {
288
+ expect(screen.getByTestId("c-assistantContent").textContent).toBe(
289
+ "persisted"
290
+ );
291
+ });
292
+ });
293
+
294
+ it("selectConversation fetch failure calls toast.error and does not clear state", async () => {
295
+ // The bug this test pins down: `handleSelectConversation`'s old catch
296
+ // block was `setMessages([])`, which wiped all prior turns on any
297
+ // fetch hiccup. The fix: on failure, call toast.error and leave
298
+ // messagesByConversation untouched.
299
+ const fetchMock = vi.fn(async (url: RequestInfo | URL, init?: RequestInit) => {
300
+ const u = url.toString();
301
+ const method = init?.method ?? "GET";
302
+ if (u.startsWith("/api/settings/chat")) return new Response(null, { status: 204 });
303
+ if (u.startsWith("/api/chat/models")) return new Response(null, { status: 204 });
304
+ if (u.match(/\/api\/chat\/conversations\/conv-missing\/messages$/) && method === "GET") {
305
+ return new Response("boom", { status: 500 });
306
+ }
307
+ if (u.startsWith("/api/chat/conversations/conv-missing")) {
308
+ return new Response(JSON.stringify({ id: "conv-missing" }), { status: 200 });
309
+ }
310
+ return new Response(null, { status: 404 });
311
+ });
312
+ vi.stubGlobal("fetch", fetchMock);
313
+
314
+ // Custom consumer that exposes a button to select a specific (failing) conversation
315
+ function FailingSelectConsumer() {
316
+ const session = useChatSession();
317
+ return (
318
+ <div>
319
+ <div data-testid="cache-keys">
320
+ {Object.keys(session.conversations.length ? { placeholder: 1 } : {}).join(",")}
321
+ </div>
322
+ <button
323
+ data-testid="select-failing"
324
+ onClick={() => {
325
+ // Directly call setActiveConversation with an id that has no
326
+ // cache entry — this triggers loadMessagesForConversation,
327
+ // which will hit the failing mock.
328
+ session.setActiveConversation("conv-missing");
329
+ }}
330
+ >
331
+ select failing
332
+ </button>
333
+ </div>
334
+ );
335
+ }
336
+
337
+ render(
338
+ <ChatSessionProvider>
339
+ <FailingSelectConsumer />
340
+ </ChatSessionProvider>
341
+ );
342
+
343
+ await act(async () => {
344
+ screen.getByTestId("select-failing").click();
345
+ });
346
+
347
+ // The fetch fails → toast.error must be called. Prior to the fix,
348
+ // the code would have called `setMessages([])`. Now it calls toast and
349
+ // leaves state alone.
350
+ await waitFor(() => {
351
+ expect(toastErrorSpy).toHaveBeenCalledWith(
352
+ "Failed to load conversation messages"
353
+ );
354
+ });
355
+ });
356
+
357
+ it("stopStreaming aborts an in-flight stream", async () => {
358
+ const fetchMock = vi.fn(async (url: RequestInfo | URL, init?: RequestInit) => {
359
+ const u = url.toString();
360
+ if (u.startsWith("/api/settings/chat")) return new Response(null, { status: 204 });
361
+ if (u.startsWith("/api/chat/models")) return new Response(null, { status: 204 });
362
+ if (u === "/api/chat/conversations" || u.endsWith("/api/chat/conversations")) {
363
+ return new Response(
364
+ JSON.stringify({
365
+ id: "conv-abort",
366
+ projectId: null,
367
+ title: "T",
368
+ status: "active",
369
+ runtimeId: "claude-code",
370
+ modelId: "haiku",
371
+ createdAt: new Date().toISOString(),
372
+ updatedAt: new Date().toISOString(),
373
+ }),
374
+ { status: 200 }
375
+ );
376
+ }
377
+ if (u.match(/\/api\/chat\/conversations\/conv-abort\/messages$/)) {
378
+ const signal = init?.signal as AbortSignal;
379
+ return new Response(makeHangingStream(signal), { status: 200 });
380
+ }
381
+ return new Response(null, { status: 404 });
382
+ });
383
+ vi.stubGlobal("fetch", fetchMock);
384
+
385
+ render(
386
+ <ChatSessionProvider>
387
+ <Consumer />
388
+ </ChatSessionProvider>
389
+ );
390
+
391
+ await act(async () => {
392
+ screen.getByTestId("c-send").click();
393
+ });
394
+
395
+ // Give the fetch a microtask to kick off
396
+ await waitFor(() => {
397
+ expect(screen.getByTestId("c-isStreaming").textContent).toBe("true");
398
+ });
399
+
400
+ await act(async () => {
401
+ screen.getByTestId("c-stop").click();
402
+ });
403
+
404
+ await waitFor(() => {
405
+ expect(screen.getByTestId("c-isStreaming").textContent).toBe("false");
406
+ });
407
+ });
408
+
409
+ it("view-remount telemetry pattern logs on unmount when streaming", async () => {
410
+ // Contract test for the `client.stream.view-remount` telemetry code.
411
+ // Mirrors the pattern ChatShell implements: track isStreaming in a ref,
412
+ // then log on unmount iff the ref was true. The ref is necessary because
413
+ // a stale closure would see isStreaming at effect-setup time, not at
414
+ // unmount time.
415
+ const consoleInfoSpy = vi
416
+ .spyOn(console, "info")
417
+ .mockImplementation(() => {});
418
+
419
+ function ViewRemountConsumer() {
420
+ const { isStreaming, activeId, sendMessage } = useChatSession();
421
+ const isStreamingRef = useRef(isStreaming);
422
+ const activeIdRef = useRef(activeId);
423
+ useEffect(() => {
424
+ isStreamingRef.current = isStreaming;
425
+ }, [isStreaming]);
426
+ useEffect(() => {
427
+ activeIdRef.current = activeId;
428
+ }, [activeId]);
429
+ useEffect(() => {
430
+ return () => {
431
+ if (isStreamingRef.current) {
432
+ // eslint-disable-next-line no-console
433
+ console.info("[chat-stream] client.stream.view-remount", {
434
+ conversationId: activeIdRef.current,
435
+ });
436
+ }
437
+ };
438
+ // Empty deps: run-once cleanup on unmount.
439
+ // eslint-disable-next-line react-hooks/exhaustive-deps
440
+ }, []);
441
+ return (
442
+ <div>
443
+ <div data-testid="vr-isStreaming">{String(isStreaming)}</div>
444
+ <button
445
+ data-testid="vr-send"
446
+ onClick={() => void sendMessage("hello")}
447
+ >
448
+ send
449
+ </button>
450
+ </div>
451
+ );
452
+ }
453
+
454
+ function ViewRemountWrapper() {
455
+ const [mounted, setMounted] = useState(true);
456
+ return (
457
+ <ChatSessionProvider>
458
+ <button data-testid="vr-unmount" onClick={() => setMounted(false)}>
459
+ unmount
460
+ </button>
461
+ {mounted && <ViewRemountConsumer />}
462
+ </ChatSessionProvider>
463
+ );
464
+ }
465
+
466
+ const fetchMock = vi.fn(async (url: RequestInfo | URL, init?: RequestInit) => {
467
+ const u = url.toString();
468
+ if (u.startsWith("/api/settings/chat")) return new Response(null, { status: 204 });
469
+ if (u.startsWith("/api/chat/models")) return new Response(null, { status: 204 });
470
+ if (u === "/api/chat/conversations" || u.endsWith("/api/chat/conversations")) {
471
+ return new Response(
472
+ JSON.stringify({
473
+ id: "conv-vr",
474
+ projectId: null,
475
+ title: "T",
476
+ status: "active",
477
+ runtimeId: "claude-code",
478
+ modelId: "haiku",
479
+ createdAt: new Date().toISOString(),
480
+ updatedAt: new Date().toISOString(),
481
+ }),
482
+ { status: 200 }
483
+ );
484
+ }
485
+ if (u.match(/\/api\/chat\/conversations\/conv-vr\/messages$/)) {
486
+ const signal = init?.signal as AbortSignal;
487
+ return new Response(makeHangingStream(signal), { status: 200 });
488
+ }
489
+ return new Response(null, { status: 404 });
490
+ });
491
+ vi.stubGlobal("fetch", fetchMock);
492
+
493
+ render(<ViewRemountWrapper />);
494
+
495
+ await act(async () => {
496
+ screen.getByTestId("vr-send").click();
497
+ });
498
+
499
+ await waitFor(() => {
500
+ expect(screen.getByTestId("vr-isStreaming").textContent).toBe("true");
501
+ });
502
+
503
+ // Unmount the consumer while streaming is in flight.
504
+ await act(async () => {
505
+ screen.getByTestId("vr-unmount").click();
506
+ });
507
+
508
+ expect(consoleInfoSpy).toHaveBeenCalledWith(
509
+ "[chat-stream] client.stream.view-remount",
510
+ expect.objectContaining({ conversationId: "conv-vr" })
511
+ );
512
+
513
+ consoleInfoSpy.mockRestore();
514
+ });
515
+
516
+ it("view-remount telemetry pattern does NOT log when not streaming", async () => {
517
+ // Guard case: unmounting without an active stream must not emit.
518
+ const consoleInfoSpy = vi
519
+ .spyOn(console, "info")
520
+ .mockImplementation(() => {});
521
+
522
+ function ViewRemountConsumer() {
523
+ const { isStreaming } = useChatSession();
524
+ const isStreamingRef = useRef(isStreaming);
525
+ useEffect(() => {
526
+ isStreamingRef.current = isStreaming;
527
+ }, [isStreaming]);
528
+ useEffect(() => {
529
+ return () => {
530
+ if (isStreamingRef.current) {
531
+ // eslint-disable-next-line no-console
532
+ console.info("[chat-stream] client.stream.view-remount", {
533
+ conversationId: null,
534
+ });
535
+ }
536
+ };
537
+ // eslint-disable-next-line react-hooks/exhaustive-deps
538
+ }, []);
539
+ return <div />;
540
+ }
541
+
542
+ function Wrapper() {
543
+ const [mounted, setMounted] = useState(true);
544
+ return (
545
+ <ChatSessionProvider>
546
+ <button data-testid="toggle" onClick={() => setMounted(false)}>
547
+ toggle
548
+ </button>
549
+ {mounted && <ViewRemountConsumer />}
550
+ </ChatSessionProvider>
551
+ );
552
+ }
553
+
554
+ vi.stubGlobal(
555
+ "fetch",
556
+ vi.fn(async () => new Response(null, { status: 204 }))
557
+ );
558
+
559
+ render(<Wrapper />);
560
+
561
+ await act(async () => {
562
+ screen.getByTestId("toggle").click();
563
+ });
564
+
565
+ const viewRemountCalls = consoleInfoSpy.mock.calls.filter(
566
+ ([msg]) =>
567
+ typeof msg === "string" && msg.includes("client.stream.view-remount")
568
+ );
569
+ expect(viewRemountCalls).toHaveLength(0);
570
+
571
+ consoleInfoSpy.mockRestore();
572
+ });
573
+ });
@@ -0,0 +1,91 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { render, screen, fireEvent } from "@testing-library/react";
3
+ import { Command, CommandList } from "@/components/ui/command";
4
+ import { SkillRow } from "../skill-row";
5
+ import type { EnrichedSkill } from "@/lib/environment/skill-enrichment";
6
+
7
+ const base: EnrichedSkill = {
8
+ id: "code-reviewer",
9
+ name: "code-reviewer",
10
+ tool: "claude-code",
11
+ scope: "user",
12
+ preview: "Review PRs for security",
13
+ sizeBytes: 100,
14
+ absPath: "/p",
15
+ absPaths: ["/p"],
16
+ healthScore: "healthy",
17
+ syncStatus: "synced",
18
+ linkedProfileId: "code-reviewer-profile",
19
+ };
20
+
21
+ // SkillRow uses CommandItem, which must live inside a cmdk Command/CommandList
22
+ function renderRow(skill: EnrichedSkill, recommended = false) {
23
+ return render(
24
+ <Command>
25
+ <CommandList>
26
+ <SkillRow skill={skill} recommended={recommended} onSelect={() => {}} />
27
+ </CommandList>
28
+ </Command>
29
+ );
30
+ }
31
+
32
+ describe("SkillRow", () => {
33
+ it("renders skill name and description", () => {
34
+ renderRow(base);
35
+ expect(screen.getByText("code-reviewer")).toBeInTheDocument();
36
+ expect(screen.getByText(/Review PRs/)).toBeInTheDocument();
37
+ });
38
+
39
+ it("shows synced badge when syncStatus is synced", () => {
40
+ renderRow(base);
41
+ expect(screen.getByText(/synced/i)).toBeInTheDocument();
42
+ });
43
+
44
+ it("shows profile linkage badge", () => {
45
+ renderRow(base);
46
+ expect(screen.getByText(/code-reviewer-profile/)).toBeInTheDocument();
47
+ });
48
+
49
+ it("shows 'stale' badge for stale health", () => {
50
+ renderRow({ ...base, healthScore: "stale" });
51
+ expect(screen.getByText(/stale/i)).toBeInTheDocument();
52
+ });
53
+
54
+ it("shows a recommended indicator when recommended=true", () => {
55
+ renderRow(base, true);
56
+ expect(screen.getByLabelText(/recommended/i)).toBeInTheDocument();
57
+ });
58
+
59
+ it("calls onDismissRecommendation when X is clicked", () => {
60
+ const onDismiss = vi.fn();
61
+ render(
62
+ <Command>
63
+ <CommandList>
64
+ <SkillRow
65
+ skill={base}
66
+ recommended
67
+ onSelect={() => {}}
68
+ onDismissRecommendation={onDismiss}
69
+ />
70
+ </CommandList>
71
+ </Command>
72
+ );
73
+ fireEvent.click(screen.getByLabelText("Dismiss recommendation"));
74
+ expect(onDismiss).toHaveBeenCalledTimes(1);
75
+ });
76
+
77
+ it("does not render dismiss button when not recommended", () => {
78
+ render(
79
+ <Command>
80
+ <CommandList>
81
+ <SkillRow
82
+ skill={base}
83
+ onSelect={() => {}}
84
+ onDismissRecommendation={() => {}}
85
+ />
86
+ </CommandList>
87
+ </Command>
88
+ );
89
+ expect(screen.queryByLabelText("Dismiss recommendation")).toBeNull();
90
+ });
91
+ });