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
@@ -1,20 +1,18 @@
1
1
  "use client";
2
2
 
3
- import { useState, useCallback, useEffect, useMemo } from "react";
4
- import { useRouter } from "next/navigation";
5
- import type { ConversationRow, ChatMessageRow } from "@/lib/db/schema";
3
+ import { useState, useCallback, useEffect, useMemo, useRef } from "react";
4
+ import type { ConversationRow } from "@/lib/db/schema";
6
5
  import type { PromptCategory } from "@/lib/chat/types";
7
- import { DEFAULT_CHAT_MODEL, CHAT_MODELS, getRuntimeForModel, type ChatModelOption } from "@/lib/chat/types";
8
- import { usePersistedState } from "@/hooks/use-persisted-state";
6
+ import { useChatSession } from "./chat-session-provider";
9
7
  import { ConversationList } from "./conversation-list";
10
8
  import { ChatMessageList } from "./chat-message-list";
11
9
  import { ChatInput } from "./chat-input";
12
- import type { MentionReference } from "@/hooks/use-chat-autocomplete";
13
10
  import { ChatEmptyState } from "./chat-empty-state";
14
11
  import { ChatActivityIndicator } from "./chat-activity-indicator";
12
+ import { ConversationTemplatePicker } from "./conversation-template-picker";
15
13
  import { Button } from "@/components/ui/button";
16
14
  import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
17
- import { MessageCircle, PanelRightOpen } from "lucide-react";
15
+ import { PanelRightOpen, Sparkles } from "lucide-react";
18
16
 
19
17
  interface ChatShellProps {
20
18
  initialConversations: ConversationRow[];
@@ -22,60 +20,100 @@ interface ChatShellProps {
22
20
  initialActiveId?: string | null;
23
21
  }
24
22
 
23
+ /**
24
+ * Thin view component for the /chat route. All chat-domain state lives in
25
+ * `ChatSessionProvider` (rendered from `src/app/layout.tsx`), so unmounting
26
+ * this component — e.g., when the user navigates to another sidebar view —
27
+ * does not touch the in-flight SSE reader loop or clear any messages. On
28
+ * remount, we read the provider's current state and render it directly.
29
+ *
30
+ * See `features/chat-session-persistence-provider.md`.
31
+ */
25
32
  export function ChatShell({
26
33
  initialConversations,
27
34
  promptCategories,
28
35
  initialActiveId,
29
36
  }: ChatShellProps) {
30
- const router = useRouter();
31
- const [conversations, setConversations] =
32
- useState<ConversationRow[]>(initialConversations);
33
- const [activeId, setActiveId] = useState<string | null>(null);
34
- const [messages, setMessages] = useState<ChatMessageRow[]>([]);
35
- const [isStreaming, setIsStreaming] = useState(false);
36
- const [abortController, setAbortController] =
37
- useState<AbortController | null>(null);
37
+ const session = useChatSession();
38
+ const {
39
+ conversations,
40
+ activeId,
41
+ messages,
42
+ isStreaming,
43
+ modelId,
44
+ availableModels,
45
+ hydrated,
46
+ hydrate,
47
+ setActiveConversation,
48
+ sendMessage,
49
+ stopStreaming,
50
+ createConversation,
51
+ deleteConversation,
52
+ renameConversation,
53
+ setMessageStatus,
54
+ setModelId,
55
+ } = session;
56
+
57
+ // View-local state only
38
58
  const [mobileListOpen, setMobileListOpen] = useState(false);
39
59
  const [hoverPreview, setHoverPreview] = useState<string | null>(null);
40
- const [modelId, setModelId] = useState(DEFAULT_CHAT_MODEL);
41
- const [availableModels, setAvailableModels] = useState<ChatModelOption[]>(CHAT_MODELS);
60
+ const [templatePickerOpen, setTemplatePickerOpen] = useState(false);
42
61
 
43
- // Persistence via localStorage fallback
44
- const [persistedActiveId, setPersistedActiveId] = usePersistedState<string>("stagent-active-chat", "");
62
+ // Open the template picker from any source (empty-state button, slash
63
+ // command, palette). Central handler keeps the open state authoritative.
64
+ useEffect(() => {
65
+ function onOpen() {
66
+ setTemplatePickerOpen(true);
67
+ }
68
+ window.addEventListener("stagent.chat.openTemplatePicker", onOpen);
69
+ return () =>
70
+ window.removeEventListener("stagent.chat.openTemplatePicker", onOpen);
71
+ }, []);
45
72
 
46
- const activeConversation = conversations.find((c) => c.id === activeId);
73
+ // Track streaming state + activeId in refs so the unmount cleanup sees the
74
+ // values at unmount time, not at effect-setup time (closure-capture bug).
75
+ // If ChatShell unmounts while a stream is in flight (user navigated away),
76
+ // log a telemetry breadcrumb. The stream itself continues inside
77
+ // ChatSessionProvider — this log only exists so diagnostics can confirm
78
+ // the provider-hoisting fix is holding. See `src/lib/chat/stream-telemetry.ts`
79
+ // for the full reason code list.
80
+ const isStreamingRef = useRef(isStreaming);
81
+ const activeIdRef = useRef(activeId);
82
+ useEffect(() => {
83
+ isStreamingRef.current = isStreaming;
84
+ }, [isStreaming]);
85
+ useEffect(() => {
86
+ activeIdRef.current = activeId;
87
+ }, [activeId]);
88
+ useEffect(() => {
89
+ return () => {
90
+ if (isStreamingRef.current) {
91
+ // eslint-disable-next-line no-console
92
+ console.info("[chat-stream] client.stream.view-remount", {
93
+ conversationId: activeIdRef.current,
94
+ });
95
+ }
96
+ };
97
+ // Empty deps: exactly-once cleanup on unmount.
98
+ // eslint-disable-next-line react-hooks/exhaustive-deps
99
+ }, []);
47
100
 
48
- // Restore active conversation on mount
49
- // Read localStorage synchronously to avoid race with usePersistedState's async useEffect
101
+ // Hydrate provider once with the server-rendered conversation list.
102
+ // Subsequent remounts are no-ops the provider preserves its state.
50
103
  useEffect(() => {
51
- let restoredId = initialActiveId || null;
52
- if (!restoredId) {
53
- try {
54
- restoredId = localStorage.getItem("stagent-active-chat") || null;
55
- } catch { /* localStorage unavailable */ }
56
- }
57
- if (restoredId && conversations.some((c) => c.id === restoredId)) {
58
- setActiveId(restoredId);
59
- setPersistedActiveId(restoredId);
60
- // Fetch messages for restored conversation
61
- fetch(`/api/chat/conversations/${restoredId}/messages`)
62
- .then((r) => r.ok ? r.json() : [])
63
- .then((msgs) => setMessages(msgs))
64
- .catch(() => setMessages([]));
65
- }
104
+ hydrate({
105
+ conversations: initialConversations,
106
+ initialActiveId: initialActiveId ?? null,
107
+ });
108
+ // Intentionally run only on mount: initialConversations is the
109
+ // server-rendered snapshot for this specific page visit.
66
110
  // eslint-disable-next-line react-hooks/exhaustive-deps
67
111
  }, []);
68
112
 
69
- // Sync activeId to URL and localStorage
70
- const updateActiveId = useCallback((id: string | null) => {
71
- setActiveId(id);
72
- setPersistedActiveId(id ?? "");
73
- if (id) {
74
- router.replace(`/chat?c=${id}`, { scroll: false });
75
- } else {
76
- router.replace("/chat", { scroll: false });
77
- }
78
- }, [router, setPersistedActiveId]);
113
+ const activeConversation = useMemo(
114
+ () => conversations.find((c) => c.id === activeId),
115
+ [conversations, activeId]
116
+ );
79
117
 
80
118
  // Extract spawned task IDs from messages (execute_task tool results)
81
119
  const spawnedTaskIds = useMemo(() => {
@@ -83,9 +121,14 @@ export function ChatShell({
83
121
  for (const msg of messages) {
84
122
  if (msg.metadata) {
85
123
  try {
86
- const meta = typeof msg.metadata === "string" ? JSON.parse(msg.metadata) : msg.metadata;
87
- // Check for execute_task tool result in metadata
88
- if (meta.type === "permission_request" && meta.toolName === "mcp__stagent__execute_task") {
124
+ const meta =
125
+ typeof msg.metadata === "string"
126
+ ? JSON.parse(msg.metadata)
127
+ : msg.metadata;
128
+ if (
129
+ meta.type === "permission_request" &&
130
+ meta.toolName === "mcp__stagent__execute_task"
131
+ ) {
89
132
  const input = meta.toolInput;
90
133
  if (input?.taskId) taskIds.push(input.taskId);
91
134
  }
@@ -93,379 +136,63 @@ export function ChatShell({
93
136
  // Ignore parse errors
94
137
  }
95
138
  }
96
- // Also scan assistant message content for task execution confirmations
97
139
  if (msg.role === "assistant" && msg.content) {
98
- const taskIdMatch = msg.content.match(/Execution started.*?taskId["\s:]+([a-f0-9-]{36})/i);
140
+ const taskIdMatch = msg.content.match(
141
+ /Execution started.*?taskId["\s:]+([a-f0-9-]{36})/i
142
+ );
99
143
  if (taskIdMatch) taskIds.push(taskIdMatch[1]);
100
144
  }
101
145
  }
102
146
  return [...new Set(taskIds)];
103
147
  }, [messages]);
104
148
 
105
- // Fetch default model and available models on mount
106
- useEffect(() => {
107
- fetch("/api/settings/chat")
108
- .then((r) => r.ok ? r.json() : null)
109
- .then((data) => {
110
- if (data?.defaultModel) setModelId(data.defaultModel);
111
- })
112
- .catch(() => {});
113
-
114
- fetch("/api/chat/models")
115
- .then((r) => r.ok ? r.json() : null)
116
- .then((models) => {
117
- if (models?.length) setAvailableModels(models);
118
- })
119
- .catch(() => {});
120
- }, []);
121
-
122
- // ── Conversation Management ──────────────────────────────────────────
123
-
149
+ // ── Action wrappers ──────────────────────────────────────────────────
124
150
  const handleNewChat = useCallback(async () => {
125
- try {
126
- const res = await fetch("/api/chat/conversations", {
127
- method: "POST",
128
- headers: { "Content-Type": "application/json" },
129
- body: JSON.stringify({ runtimeId: getRuntimeForModel(modelId), modelId }),
130
- });
131
- if (!res.ok) return;
132
- const conversation = await res.json();
133
- setConversations((prev) => [conversation, ...prev]);
134
- updateActiveId(conversation.id);
135
- setMessages([]);
136
- setMobileListOpen(false);
137
- } catch {
138
- // Handle error silently
139
- }
140
- }, [modelId, updateActiveId]);
141
-
142
- const handleSelectConversation = useCallback(async (id: string) => {
143
- updateActiveId(id);
151
+ await createConversation();
144
152
  setMobileListOpen(false);
145
- try {
146
- const [msgRes, convRes] = await Promise.all([
147
- fetch(`/api/chat/conversations/${id}/messages`),
148
- fetch(`/api/chat/conversations/${id}`),
149
- ]);
150
- if (msgRes.ok) {
151
- const msgs = await msgRes.json();
152
- // Clean up stale "streaming" messages from interrupted sessions
153
- setMessages(
154
- msgs.map((m: ChatMessageRow) =>
155
- m.status === "streaming" ? { ...m, status: "complete" as const } : m
156
- )
157
- );
158
- }
159
- if (convRes.ok) {
160
- const conv = await convRes.json();
161
- if (conv.modelId) setModelId(conv.modelId);
162
- }
163
- } catch {
164
- setMessages([]);
165
- }
166
- }, [updateActiveId]);
153
+ }, [createConversation]);
167
154
 
168
- const handleDeleteConversation = useCallback(
169
- async (id: string) => {
170
- try {
171
- await fetch(`/api/chat/conversations/${id}`, {
172
- method: "DELETE",
173
- });
174
- setConversations((prev) => prev.filter((c) => c.id !== id));
175
- if (activeId === id) {
176
- updateActiveId(null);
177
- setMessages([]);
178
- }
179
- } catch {
180
- // Handle error silently
181
- }
155
+ const handleSelectConversation = useCallback(
156
+ (id: string) => {
157
+ setActiveConversation(id);
158
+ setMobileListOpen(false);
182
159
  },
183
- [activeId, updateActiveId]
160
+ [setActiveConversation]
184
161
  );
185
162
 
186
- const handleRenameConversation = useCallback(
187
- async (id: string, title: string) => {
188
- try {
189
- const res = await fetch(`/api/chat/conversations/${id}`, {
190
- method: "PATCH",
191
- headers: { "Content-Type": "application/json" },
192
- body: JSON.stringify({ title }),
193
- });
194
- if (res.ok) {
195
- const updated = await res.json();
196
- setConversations((prev) =>
197
- prev.map((c) => (c.id === id ? updated : c))
198
- );
199
- }
200
- } catch {
201
- // Handle error silently
202
- }
203
- },
204
- []
163
+ const handleDeleteConversation = useCallback(
164
+ (id: string) => deleteConversation(id),
165
+ [deleteConversation]
205
166
  );
206
167
 
207
- // ── Message Sending ──────────────────────────────────────────────────
208
-
209
- const handleSend = useCallback(
210
- async (content: string, mentions?: MentionReference[]) => {
211
- let conversationId = activeId;
212
-
213
- // Create conversation on first message if none active
214
- if (!conversationId) {
215
- try {
216
- const res = await fetch("/api/chat/conversations", {
217
- method: "POST",
218
- headers: { "Content-Type": "application/json" },
219
- body: JSON.stringify({ runtimeId: getRuntimeForModel(modelId), modelId }),
220
- });
221
- if (!res.ok) return;
222
- const conversation = await res.json();
223
- setConversations((prev) => [conversation, ...prev]);
224
- updateActiveId(conversation.id);
225
- conversationId = conversation.id;
226
- } catch {
227
- return;
228
- }
229
- }
230
-
231
- // Add optimistic user message
232
- const userMsg: ChatMessageRow = {
233
- id: crypto.randomUUID(),
234
- conversationId: conversationId!,
235
- role: "user",
236
- content,
237
- metadata: null,
238
- status: "complete",
239
- createdAt: new Date(),
240
- };
241
- setMessages((prev) => [...prev, userMsg]);
242
-
243
- // Add placeholder assistant message
244
- const assistantMsgId = crypto.randomUUID();
245
- const assistantMsg: ChatMessageRow = {
246
- id: assistantMsgId,
247
- conversationId: conversationId!,
248
- role: "assistant",
249
- content: "",
250
- metadata: null,
251
- status: "streaming",
252
- createdAt: new Date(),
253
- };
254
- setMessages((prev) => [...prev, assistantMsg]);
255
-
256
- setIsStreaming(true);
257
- const controller = new AbortController();
258
- setAbortController(controller);
259
-
260
- try {
261
- const res = await fetch(
262
- `/api/chat/conversations/${conversationId}/messages`,
263
- {
264
- method: "POST",
265
- headers: { "Content-Type": "application/json" },
266
- body: JSON.stringify({ content, mentions }),
267
- signal: controller.signal,
268
- }
269
- );
270
-
271
- if (!res.ok || !res.body) {
272
- throw new Error("Failed to send message");
273
- }
274
-
275
- const reader = res.body.getReader();
276
- const decoder = new TextDecoder();
277
- let buffer = "";
278
-
279
- while (true) {
280
- const { done, value } = await reader.read();
281
- if (done) break;
282
-
283
- buffer += decoder.decode(value, { stream: true });
284
- const lines = buffer.split("\n");
285
- buffer = lines.pop() ?? "";
286
-
287
- for (const line of lines) {
288
- if (!line.startsWith("data: ")) continue;
289
- const json = line.slice(6);
290
- try {
291
- const event = JSON.parse(json);
292
- if (event.type === "status") {
293
- setMessages((prev) =>
294
- prev.map((m) =>
295
- m.id === assistantMsgId
296
- ? { ...m, metadata: JSON.stringify({ statusPhase: event.phase, statusMessage: event.message }) }
297
- : m
298
- )
299
- );
300
- } else if (event.type === "delta") {
301
- setMessages((prev) =>
302
- prev.map((m) =>
303
- m.id === assistantMsgId
304
- ? { ...m, content: m.content + event.content }
305
- : m
306
- )
307
- );
308
- } else if (event.type === "done") {
309
- setMessages((prev) =>
310
- prev.map((m) =>
311
- m.id === assistantMsgId
312
- ? {
313
- ...m,
314
- id: event.messageId,
315
- status: "complete",
316
- metadata: (() => {
317
- const existing = m.metadata
318
- ? (() => { try { return JSON.parse(m.metadata!); } catch { return {}; } })()
319
- : {};
320
- if (event.quickAccess?.length) {
321
- existing.quickAccess = event.quickAccess;
322
- }
323
- return JSON.stringify(existing);
324
- })(),
325
- }
326
- : m
327
- )
328
- );
329
- // Refresh conversation from API to get auto-generated title
330
- fetch(`/api/chat/conversations/${conversationId}`)
331
- .then((r) => r.ok ? r.json() : null)
332
- .then((conv) => {
333
- if (conv) {
334
- setConversations((prev) =>
335
- prev.map((c) =>
336
- c.id === conversationId
337
- ? { ...c, title: conv.title, updatedAt: new Date() }
338
- : c
339
- )
340
- );
341
- }
342
- })
343
- .catch(() => {});
344
- } else if (event.type === "permission_request" || event.type === "question") {
345
- // Insert system message for inline permission/question UI
346
- const systemMsg = {
347
- id: event.messageId,
348
- conversationId: conversationId!,
349
- role: "system" as const,
350
- content: event.type === "permission_request"
351
- ? `Permission required: ${event.toolName}`
352
- : "Agent has a question",
353
- metadata: JSON.stringify(event.type === "permission_request"
354
- ? { type: "permission_request", requestId: event.requestId, toolName: event.toolName, toolInput: event.toolInput }
355
- : { type: "question", requestId: event.requestId, questions: event.questions }
356
- ),
357
- status: "pending" as const,
358
- createdAt: new Date(),
359
- };
360
- setMessages((prev) => [...prev, systemMsg]);
361
- } else if (event.type === "screenshot") {
362
- // Append screenshot attachment to assistant message metadata
363
- setMessages((prev) =>
364
- prev.map((m) => {
365
- if (m.id !== assistantMsgId) return m;
366
- const meta = m.metadata ? (() => { try { return JSON.parse(m.metadata!); } catch { return {}; } })() : {};
367
- const attachments = Array.isArray(meta.attachments) ? meta.attachments : [];
368
- attachments.push({
369
- documentId: event.documentId,
370
- thumbnailUrl: event.thumbnailUrl,
371
- originalUrl: event.originalUrl,
372
- width: event.width,
373
- height: event.height,
374
- });
375
- return { ...m, metadata: JSON.stringify({ ...meta, attachments }) };
376
- })
377
- );
378
- } else if (event.type === "error") {
379
- setMessages((prev) =>
380
- prev.map((m) =>
381
- m.id === assistantMsgId
382
- ? {
383
- ...m,
384
- content: m.content || event.message,
385
- status: "error",
386
- }
387
- : m
388
- )
389
- );
390
- }
391
- } catch {
392
- // Ignore malformed SSE data
393
- }
394
- }
395
- }
396
- } catch (error) {
397
- if ((error as Error).name !== "AbortError") {
398
- setMessages((prev) =>
399
- prev.map((m) =>
400
- m.id === assistantMsgId
401
- ? {
402
- ...m,
403
- content:
404
- m.content || "Failed to get response. Please try again.",
405
- status: "error",
406
- }
407
- : m
408
- )
409
- );
410
- }
411
- } finally {
412
- setIsStreaming(false);
413
- setAbortController(null);
414
- }
415
- },
416
- [activeId, modelId, updateActiveId]
168
+ const handleRenameConversation = useCallback(
169
+ (id: string, title: string) => renameConversation(id, title),
170
+ [renameConversation]
417
171
  );
418
172
 
419
- const handleStop = useCallback(() => {
420
- abortController?.abort();
421
- }, [abortController]);
422
-
423
173
  const handleSuggestionClick = useCallback(
424
174
  (prompt: string) => {
425
- handleSend(prompt);
175
+ void sendMessage(prompt);
426
176
  },
427
- [handleSend]
177
+ [sendMessage]
428
178
  );
429
179
 
430
180
  const handleMessageStatusChange = useCallback(
431
181
  (messageId: string, status: string) => {
432
- setMessages((prev) =>
433
- prev.map((m) =>
434
- m.id === messageId
435
- ? { ...m, status: status as "pending" | "streaming" | "complete" | "error" }
436
- : m
437
- )
182
+ setMessageStatus(
183
+ messageId,
184
+ status as "pending" | "streaming" | "complete" | "error"
438
185
  );
439
186
  },
440
- []
187
+ [setMessageStatus]
441
188
  );
442
189
 
443
- const handleModelChange = useCallback(
444
- async (newModelId: string) => {
445
- setModelId(newModelId);
446
- // If there's an active conversation, update both modelId and runtimeId
447
- if (activeId) {
448
- const newRuntimeId = getRuntimeForModel(newModelId);
449
- await fetch(`/api/chat/conversations/${activeId}`, {
450
- method: "PATCH",
451
- headers: { "Content-Type": "application/json" },
452
- body: JSON.stringify({ modelId: newModelId, runtimeId: newRuntimeId }),
453
- }).catch(() => {});
454
- // Update local state so conversation list reflects the change
455
- setConversations((prev) =>
456
- prev.map((c) =>
457
- c.id === activeId
458
- ? { ...c, modelId: newModelId, runtimeId: newRuntimeId }
459
- : c
460
- )
461
- );
462
- }
463
- },
464
- [activeId]
465
- );
190
+ // Suppress unused warnings from props we still accept but no longer own.
191
+ // `hydrated` tells us whether the provider has data — we can use it to
192
+ // skip the empty-state flash on a remount that finds existing state.
193
+ void hydrated;
466
194
 
467
195
  // ── Render ───────────────────────────────────────────────────────────
468
-
469
196
  const conversationListContent = (
470
197
  <ConversationList
471
198
  conversations={conversations}
@@ -498,7 +225,7 @@ export function ChatShell({
498
225
  </Sheet>
499
226
  </div>
500
227
 
501
- {!activeId && messages.length === 0 ? (
228
+ {messages.length === 0 ? (
502
229
  /* Hero mode: vertically centered greeting + input + chips */
503
230
  <div className="flex-1 flex items-center justify-center overflow-hidden">
504
231
  <ChatEmptyState
@@ -507,23 +234,40 @@ export function ChatShell({
507
234
  onHoverPreview={setHoverPreview}
508
235
  >
509
236
  <ChatInput
510
- onSend={handleSend}
511
- onStop={handleStop}
237
+ onSend={sendMessage}
238
+ onStop={stopStreaming}
512
239
  isStreaming={isStreaming}
513
240
  isHeroMode
514
241
  previewText={hoverPreview}
515
242
  modelId={modelId}
516
- onModelChange={handleModelChange}
243
+ onModelChange={setModelId}
517
244
  availableModels={availableModels}
518
245
  projectId={activeConversation?.projectId}
246
+ conversationId={activeId}
519
247
  />
248
+ <div className="mt-3 flex justify-center">
249
+ <Button
250
+ variant="ghost"
251
+ size="sm"
252
+ className="text-xs gap-1.5"
253
+ onClick={() => setTemplatePickerOpen(true)}
254
+ >
255
+ <Sparkles className="h-3.5 w-3.5" />
256
+ Start from template
257
+ </Button>
258
+ </div>
520
259
  </ChatEmptyState>
521
260
  </div>
522
261
  ) : (
523
262
  <>
524
263
  {/* Messages */}
525
264
  <div className="flex-1 overflow-hidden">
526
- <ChatMessageList messages={messages} isStreaming={isStreaming} conversationId={activeId ?? undefined} onMessageStatusChange={handleMessageStatusChange} />
265
+ <ChatMessageList
266
+ messages={messages}
267
+ isStreaming={isStreaming}
268
+ conversationId={activeId ?? undefined}
269
+ onMessageStatusChange={handleMessageStatusChange}
270
+ />
527
271
  </div>
528
272
 
529
273
  {/* Background activity indicator */}
@@ -533,19 +277,25 @@ export function ChatShell({
533
277
 
534
278
  {/* Docked input */}
535
279
  <ChatInput
536
- onSend={handleSend}
537
- onStop={handleStop}
280
+ onSend={sendMessage}
281
+ onStop={stopStreaming}
538
282
  isStreaming={isStreaming}
539
283
  isHeroMode={false}
540
284
  modelId={modelId}
541
- onModelChange={handleModelChange}
285
+ onModelChange={setModelId}
542
286
  availableModels={availableModels}
543
287
  projectId={activeConversation?.projectId}
288
+ conversationId={activeId}
544
289
  />
545
290
  </>
546
291
  )}
547
292
  </div>
548
293
 
294
+ <ConversationTemplatePicker
295
+ open={templatePickerOpen}
296
+ onOpenChange={setTemplatePickerOpen}
297
+ />
298
+
549
299
  {/* Desktop conversation list — right side */}
550
300
  <div className="hidden lg:flex lg:w-[280px] lg:flex-col lg:border-l border-border">
551
301
  {conversationListContent}