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,47 @@
1
+ import { describe, it, expect, beforeEach, vi } from "vitest";
2
+ import { renderHook, act } from "@testing-library/react";
3
+ import { useChatAutocomplete } from "../use-chat-autocomplete";
4
+
5
+ const TAB_KEY = "stagent.command-tab";
6
+
7
+ describe("useChatAutocomplete — activeTab persistence", () => {
8
+ beforeEach(() => {
9
+ localStorage.clear();
10
+ });
11
+
12
+ it("defaults to 'actions' when localStorage empty", () => {
13
+ const { result } = renderHook(() => useChatAutocomplete({ projectId: null }));
14
+ expect(result.current.activeTab).toBe("actions");
15
+ });
16
+
17
+ it("reads persisted tab from localStorage on mount", () => {
18
+ localStorage.setItem(TAB_KEY, "skills");
19
+ const { result } = renderHook(() => useChatAutocomplete({ projectId: null }));
20
+ expect(result.current.activeTab).toBe("skills");
21
+ });
22
+
23
+ it("ignores corrupt localStorage values", () => {
24
+ localStorage.setItem(TAB_KEY, "bogus");
25
+ const { result } = renderHook(() => useChatAutocomplete({ projectId: null }));
26
+ expect(result.current.activeTab).toBe("actions");
27
+ });
28
+
29
+ it("persists tab on setActiveTab", () => {
30
+ const { result } = renderHook(() => useChatAutocomplete({ projectId: null }));
31
+ act(() => result.current.setActiveTab("tools"));
32
+ expect(result.current.activeTab).toBe("tools");
33
+ expect(localStorage.getItem(TAB_KEY)).toBe("tools");
34
+ });
35
+
36
+ it("survives localStorage throwing on write", () => {
37
+ const setSpy = vi.spyOn(Storage.prototype, "setItem").mockImplementation(() => {
38
+ throw new Error("QuotaExceeded");
39
+ });
40
+ const { result } = renderHook(() => useChatAutocomplete({ projectId: null }));
41
+ expect(() => {
42
+ act(() => result.current.setActiveTab("tools"));
43
+ }).not.toThrow();
44
+ expect(result.current.activeTab).toBe("tools");
45
+ setSpy.mockRestore();
46
+ });
47
+ });
@@ -0,0 +1,70 @@
1
+ import { renderHook, act, waitFor } from "@testing-library/react";
2
+ import { useSavedSearches } from "../use-saved-searches";
3
+ import { beforeEach, describe, expect, it, vi, afterEach } from "vitest";
4
+
5
+ describe("useSavedSearches — rename", () => {
6
+ const originalFetch = global.fetch;
7
+
8
+ beforeEach(() => {
9
+ global.fetch = vi.fn(async (url: RequestInfo | URL, init?: RequestInit) => {
10
+ const u = String(url);
11
+ if (u.endsWith("/api/settings/chat/saved-searches") && (!init || init.method === undefined || init.method === "GET")) {
12
+ return new Response(
13
+ JSON.stringify({
14
+ searches: [
15
+ {
16
+ id: "s1",
17
+ surface: "task",
18
+ label: "Old label",
19
+ filterInput: "#status:blocked",
20
+ createdAt: "2026-04-14T00:00:00.000Z",
21
+ },
22
+ ],
23
+ }),
24
+ { status: 200, headers: { "Content-Type": "application/json" } }
25
+ );
26
+ }
27
+ if (init?.method === "PUT") {
28
+ return new Response(JSON.stringify({ ok: true }), { status: 200 });
29
+ }
30
+ return new Response("{}", { status: 200 });
31
+ }) as unknown as typeof fetch;
32
+ });
33
+
34
+ afterEach(() => {
35
+ global.fetch = originalFetch;
36
+ vi.restoreAllMocks();
37
+ });
38
+
39
+ it("renames a saved search optimistically and persists via PUT", async () => {
40
+ const { result } = renderHook(() => useSavedSearches());
41
+ await waitFor(() => expect(result.current.loading).toBe(false));
42
+ expect(result.current.searches[0].label).toBe("Old label");
43
+
44
+ await act(async () => {
45
+ result.current.rename("s1", "New label");
46
+ });
47
+
48
+ expect(result.current.searches[0].label).toBe("New label");
49
+
50
+ const putCall = (global.fetch as unknown as ReturnType<typeof vi.fn>).mock.calls.find(
51
+ ([, init]) => init?.method === "PUT"
52
+ );
53
+ expect(putCall).toBeDefined();
54
+ const body = JSON.parse((putCall![1] as RequestInit).body as string);
55
+ expect(body.searches[0].label).toBe("New label");
56
+ expect(body.searches[0].id).toBe("s1");
57
+ });
58
+
59
+ it("no-ops when id is not found", async () => {
60
+ const { result } = renderHook(() => useSavedSearches());
61
+ await waitFor(() => expect(result.current.loading).toBe(false));
62
+ const before = result.current.searches;
63
+
64
+ await act(async () => {
65
+ result.current.rename("does-not-exist", "Whatever");
66
+ });
67
+
68
+ expect(result.current.searches).toEqual(before);
69
+ });
70
+ });
@@ -0,0 +1,110 @@
1
+ "use client";
2
+
3
+ /**
4
+ * `useActiveSkills` — surfaces the current conversation's composition
5
+ * state for UI affordances on the chat popover Skills tab.
6
+ *
7
+ * Returns the merged active skill IDs (legacy + composed), the runtime
8
+ * id, and the runtime's `supportsSkillComposition` + `maxActiveSkills`
9
+ * capability flags. Used by the `+ Add` action and active-count badge
10
+ * in `chat-command-popover.tsx`.
11
+ *
12
+ * See `features/chat-composition-ui-v1.md`.
13
+ */
14
+
15
+ import { useCallback, useEffect, useState } from "react";
16
+ import { mergeActiveSkillIds } from "@/lib/chat/active-skills";
17
+ import { getRuntimeFeatures, type AgentRuntimeId } from "@/lib/agents/runtime/catalog";
18
+
19
+ interface ActiveSkillsState {
20
+ loading: boolean;
21
+ /** Resolved active skill IDs (legacy + composed, deduped, in order). */
22
+ activeIds: string[];
23
+ /** The conversation's runtime id, or null if not yet loaded / no conversation. */
24
+ runtimeId: AgentRuntimeId | null;
25
+ /** True iff the runtime supports composing 2+ skills concurrently. */
26
+ supportsComposition: boolean;
27
+ /** Max simultaneously-active skills for this runtime (1 for Ollama, 3 elsewhere). */
28
+ maxActive: number;
29
+ /** Re-fetch the conversation row. Call after a successful add/remove. */
30
+ refetch: () => Promise<void>;
31
+ }
32
+
33
+ const KNOWN_RUNTIMES = new Set<AgentRuntimeId>([
34
+ "claude-code",
35
+ "openai-codex-app-server",
36
+ "anthropic-direct",
37
+ "openai-direct",
38
+ "ollama",
39
+ ]);
40
+
41
+ export function useActiveSkills(
42
+ conversationId: string | null
43
+ ): ActiveSkillsState {
44
+ const [activeIds, setActiveIds] = useState<string[]>([]);
45
+ const [runtimeId, setRuntimeId] = useState<AgentRuntimeId | null>(null);
46
+ const [loading, setLoading] = useState<boolean>(!!conversationId);
47
+
48
+ const fetchOnce = useCallback(async (): Promise<void> => {
49
+ if (!conversationId) {
50
+ setActiveIds([]);
51
+ setRuntimeId(null);
52
+ setLoading(false);
53
+ return;
54
+ }
55
+ try {
56
+ const r = await fetch(`/api/chat/conversations/${conversationId}`);
57
+ if (!r.ok) {
58
+ setActiveIds([]);
59
+ setRuntimeId(null);
60
+ return;
61
+ }
62
+ const data: {
63
+ activeSkillId?: string | null;
64
+ activeSkillIds?: string[] | null;
65
+ runtimeId?: string | null;
66
+ } = await r.json();
67
+ setActiveIds(
68
+ mergeActiveSkillIds(data.activeSkillId, data.activeSkillIds)
69
+ );
70
+ const rid = data.runtimeId as AgentRuntimeId | null | undefined;
71
+ setRuntimeId(rid && KNOWN_RUNTIMES.has(rid) ? rid : null);
72
+ } catch {
73
+ setActiveIds([]);
74
+ setRuntimeId(null);
75
+ } finally {
76
+ setLoading(false);
77
+ }
78
+ }, [conversationId]);
79
+
80
+ useEffect(() => {
81
+ setLoading(true);
82
+ void fetchOnce();
83
+ }, [fetchOnce]);
84
+
85
+ // Derive capability flags from the catalog. Defaults match Ollama
86
+ // (most conservative) when the runtime is unknown — better to refuse
87
+ // composition than to crash on an unrecognized id.
88
+ const features = runtimeId
89
+ ? safeGetRuntimeFeatures(runtimeId)
90
+ : null;
91
+ const supportsComposition = features?.supportsSkillComposition ?? false;
92
+ const maxActive = features?.maxActiveSkills ?? 1;
93
+
94
+ return {
95
+ loading,
96
+ activeIds,
97
+ runtimeId,
98
+ supportsComposition,
99
+ maxActive,
100
+ refetch: fetchOnce,
101
+ };
102
+ }
103
+
104
+ function safeGetRuntimeFeatures(rid: AgentRuntimeId) {
105
+ try {
106
+ return getRuntimeFeatures(rid);
107
+ } catch {
108
+ return null;
109
+ }
110
+ }
@@ -2,6 +2,11 @@
2
2
 
3
3
  import { useState, useCallback, useRef, useEffect } from "react";
4
4
  import { useCaretPosition } from "./use-caret-position";
5
+ import {
6
+ isCommandTabId,
7
+ DEFAULT_COMMAND_TAB,
8
+ type CommandTabId,
9
+ } from "@/lib/chat/command-tabs";
5
10
 
6
11
  export type AutocompleteMode = "slash" | "mention" | null;
7
12
 
@@ -32,6 +37,8 @@ export interface ChatAutocompleteReturn {
32
37
  entityResults: EntitySearchResult[];
33
38
  entityLoading: boolean;
34
39
  mentions: MentionReference[];
40
+ activeTab: CommandTabId;
41
+ setActiveTab: (tab: CommandTabId) => void;
35
42
  handleChange: (value: string, textarea: HTMLTextAreaElement | null) => void;
36
43
  handleKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement>) => boolean;
37
44
  handleSelect: (item: { type: "slash" | "mention"; id: string; label: string; text?: string }) => string;
@@ -39,6 +46,19 @@ export interface ChatAutocompleteReturn {
39
46
  setTextareaRef: (el: HTMLTextAreaElement | null) => void;
40
47
  }
41
48
 
49
+ const TAB_STORAGE_KEY = "stagent.command-tab";
50
+
51
+ function readInitialTab(): CommandTabId {
52
+ if (typeof window === "undefined") return DEFAULT_COMMAND_TAB;
53
+ try {
54
+ const raw = window.localStorage.getItem(TAB_STORAGE_KEY);
55
+ if (raw && isCommandTabId(raw)) return raw;
56
+ } catch {
57
+ // localStorage unavailable — fall through
58
+ }
59
+ return DEFAULT_COMMAND_TAB;
60
+ }
61
+
42
62
  const CLOSED_STATE: AutocompleteState = {
43
63
  open: false,
44
64
  mode: null,
@@ -47,21 +67,45 @@ const CLOSED_STATE: AutocompleteState = {
47
67
  anchorRect: null,
48
68
  };
49
69
 
70
+ /** Compact size label for file popover rows (e.g., "1.4 KB", "23 B"). */
71
+ function humanSize(bytes: number): string {
72
+ if (bytes < 1024) return `${bytes} B`;
73
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
74
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
75
+ }
76
+
50
77
  /**
51
78
  * Detects "/" and "@" triggers in a textarea and manages autocomplete state.
52
79
  *
53
80
  * "/" triggers at position 0 or after a newline.
54
81
  * "@" triggers at position 0 or after whitespace.
55
82
  */
56
- export function useChatAutocomplete(): ChatAutocompleteReturn {
83
+ export function useChatAutocomplete(
84
+ options: { projectId?: string | null } = {}
85
+ ): ChatAutocompleteReturn {
57
86
  const [state, setState] = useState<AutocompleteState>(CLOSED_STATE);
58
87
  const [entityResults, setEntityResults] = useState<EntitySearchResult[]>([]);
88
+ const [fileResults, setFileResults] = useState<EntitySearchResult[]>([]);
59
89
  const [entityLoading, setEntityLoading] = useState(false);
60
90
  const [mentions, setMentions] = useState<MentionReference[]>([]);
91
+ const [activeTab, setActiveTabState] = useState<CommandTabId>(readInitialTab);
92
+
93
+ const setActiveTab = useCallback((tab: CommandTabId) => {
94
+ setActiveTabState(tab);
95
+ try {
96
+ window.localStorage.setItem(TAB_STORAGE_KEY, tab);
97
+ } catch {
98
+ // quota / disabled — silent, in-memory only
99
+ }
100
+ }, []);
61
101
  const textareaRef = useRef<HTMLTextAreaElement | null>(null);
62
102
  const abortRef = useRef<AbortController | null>(null);
103
+ const fileAbortRef = useRef<AbortController | null>(null);
104
+ const fileDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
63
105
  const entityCacheRef = useRef<EntitySearchResult[] | null>(null);
64
106
  const getCaretCoordinates = useCaretPosition();
107
+ const projectIdRef = useRef(options.projectId ?? null);
108
+ projectIdRef.current = options.projectId ?? null;
65
109
 
66
110
  // Ref to let the keyboard handler access current state synchronously
67
111
  const stateRef = useRef(state);
@@ -74,9 +118,51 @@ export function useChatAutocomplete(): ChatAutocompleteReturn {
74
118
  const close = useCallback(() => {
75
119
  setState(CLOSED_STATE);
76
120
  setEntityResults([]);
121
+ setFileResults([]);
77
122
  setEntityLoading(false);
78
123
  entityCacheRef.current = null;
79
124
  if (abortRef.current) abortRef.current.abort();
125
+ if (fileAbortRef.current) fileAbortRef.current.abort();
126
+ if (fileDebounceRef.current) clearTimeout(fileDebounceRef.current);
127
+ }, []);
128
+
129
+ /**
130
+ * Query the file search API with debounce + abort. Results are stored
131
+ * in `fileResults` and merged into the popover stream alongside
132
+ * entity results. Query is bound to the active "@" typeahead text.
133
+ */
134
+ const loadFiles = useCallback((query: string) => {
135
+ // Debounce: wait 150ms after the last keystroke before firing.
136
+ if (fileDebounceRef.current) clearTimeout(fileDebounceRef.current);
137
+ fileDebounceRef.current = setTimeout(() => {
138
+ if (fileAbortRef.current) fileAbortRef.current.abort();
139
+ const controller = new AbortController();
140
+ fileAbortRef.current = controller;
141
+
142
+ const params = new URLSearchParams({ q: query, limit: "20" });
143
+ const projectId = projectIdRef.current;
144
+ if (projectId) params.set("projectId", projectId);
145
+
146
+ fetch(`/api/chat/files/search?${params}`, { signal: controller.signal })
147
+ .then((res) => (res.ok ? res.json() : { results: [] }))
148
+ .then(
149
+ (data: {
150
+ results?: Array<{ path: string; sizeBytes: number }>;
151
+ }) => {
152
+ const hits = data.results ?? [];
153
+ const mapped: EntitySearchResult[] = hits.map((h) => ({
154
+ entityType: "file",
155
+ entityId: h.path,
156
+ label: h.path,
157
+ description: humanSize(h.sizeBytes),
158
+ }));
159
+ setFileResults(mapped);
160
+ }
161
+ )
162
+ .catch(() => {
163
+ // Aborted or failed — leave previous results in place.
164
+ });
165
+ }, 150);
80
166
  }, []);
81
167
 
82
168
  // Fetch all recent entities once on "@" trigger, cache for cmdk client-side filtering
@@ -143,8 +229,15 @@ export function useChatAutocomplete(): ChatAutocompleteReturn {
143
229
  return;
144
230
  }
145
231
 
146
- // Check for "@" trigger — must be at position 0 or after whitespace
147
- const mentionMatch = textBeforeCursor.match(/(?:^|\s)(@[^\s]*)$/);
232
+ // Check for "@" trigger — must be at position 0 or after whitespace.
233
+ // The mention body stops at whitespace or `#` so the filter-namespace
234
+ // (`#key:value`) can chain after a space: `@foo #status:blocked`.
235
+ // Filter tokens are part of the same trigger span so the popover stays
236
+ // open while the user types them. Partial tokens (`@foo #` or
237
+ // `@foo #sta`) are accepted — the parser handles incomplete input.
238
+ const mentionMatch = textBeforeCursor.match(
239
+ /(?:^|\s)(@[^\s#]*(?:\s+#[A-Za-z]?[\w-]*:?[^\s#]*)*)\s*$/
240
+ );
148
241
  if (mentionMatch) {
149
242
  const triggerIndex = cursorPos - mentionMatch[1].length;
150
243
  const query = mentionMatch[1].substring(1); // text after "@"
@@ -157,6 +250,14 @@ export function useChatAutocomplete(): ChatAutocompleteReturn {
157
250
  anchorRect: coords,
158
251
  });
159
252
  loadEntities();
253
+ // File search fires only when the user has typed something after `@`
254
+ // — an empty query would return every tracked file in the repo, which
255
+ // is noisy and defeats the whole "type to narrow" interaction.
256
+ if (query.length > 0) {
257
+ loadFiles(query);
258
+ } else {
259
+ setFileResults([]);
260
+ }
160
261
  return;
161
262
  }
162
263
 
@@ -165,7 +266,7 @@ export function useChatAutocomplete(): ChatAutocompleteReturn {
165
266
  close();
166
267
  }
167
268
  },
168
- [getCaretCoordinates, loadEntities, close]
269
+ [getCaretCoordinates, loadEntities, loadFiles, close]
169
270
  );
170
271
 
171
272
  /**
@@ -245,10 +346,15 @@ export function useChatAutocomplete(): ChatAutocompleteReturn {
245
346
  if (item.type === "slash") {
246
347
  replacement = item.text ?? item.label;
247
348
  } else {
248
- // "@" mention — insert @type:Name
349
+ // "@" mention — format depends on entity type:
350
+ // file: @<path> (CLI-style, matches what users type)
351
+ // other: @<type>:<label> (disambiguates entity types)
249
352
  const eType = item.entityType ?? item.id;
250
353
  const eId = item.entityId ?? item.id;
251
- replacement = `@${eType}:${item.label} `;
354
+ replacement =
355
+ eType === "file"
356
+ ? `@${item.label} `
357
+ : `@${eType}:${item.label} `;
252
358
  // Track the mention
253
359
  setMentions((prev) => {
254
360
  if (prev.some((m) => m.entityId === eId)) return prev;
@@ -271,14 +377,21 @@ export function useChatAutocomplete(): ChatAutocompleteReturn {
271
377
  useEffect(() => {
272
378
  return () => {
273
379
  if (abortRef.current) abortRef.current.abort();
380
+ if (fileAbortRef.current) fileAbortRef.current.abort();
381
+ if (fileDebounceRef.current) clearTimeout(fileDebounceRef.current);
274
382
  };
275
383
  }, []);
276
384
 
277
385
  return {
278
386
  state,
279
- entityResults,
387
+ // Merge entity results with file results so the popover's single
388
+ // group-by-entityType render path covers both — no second props
389
+ // channel needed.
390
+ entityResults: [...entityResults, ...fileResults],
280
391
  entityLoading,
281
392
  mentions,
393
+ activeTab,
394
+ setActiveTab,
282
395
  handleChange,
283
396
  handleKeyDown,
284
397
  handleSelect,
@@ -0,0 +1,19 @@
1
+ "use client";
2
+ import { useEffect, useState } from "react";
3
+ import type { EnrichedSkill } from "@/lib/environment/skill-enrichment";
4
+
5
+ export function useEnrichedSkills(open: boolean): EnrichedSkill[] {
6
+ const [skills, setSkills] = useState<EnrichedSkill[]>([]);
7
+ useEffect(() => {
8
+ if (!open) return;
9
+ const controller = new AbortController();
10
+ fetch("/api/environment/skills", { signal: controller.signal })
11
+ .then((r) => (r.ok ? r.json() : []))
12
+ .then((data) => {
13
+ if (Array.isArray(data)) setSkills(data);
14
+ })
15
+ .catch(() => {});
16
+ return () => controller.abort();
17
+ }, [open]);
18
+ return skills;
19
+ }
@@ -0,0 +1,104 @@
1
+ "use client";
2
+
3
+ /**
4
+ * `usePinnedEntries` — client-side store for chat mention-popover pins.
5
+ *
6
+ * Fetches once on mount, keeps an in-memory list in React state, and writes
7
+ * back via PUT on every mutation (full-list replacement — see
8
+ * `src/app/api/settings/chat/pins/route.ts` for design rationale).
9
+ *
10
+ * Exposes:
11
+ * - `pins`: current pinned entries (stable identity per mount)
12
+ * - `isPinned(id)`: fast membership check
13
+ * - `pin(entry)` / `unpin(id)`: optimistic mutations with background sync
14
+ * - `loading`: true while the initial GET is in flight
15
+ */
16
+
17
+ import { useCallback, useEffect, useMemo, useState } from "react";
18
+
19
+ export interface PinnedEntry {
20
+ id: string;
21
+ type: string;
22
+ label: string;
23
+ description?: string;
24
+ status?: string;
25
+ pinnedAt: string;
26
+ }
27
+
28
+ interface UsePinnedEntriesReturn {
29
+ pins: PinnedEntry[];
30
+ loading: boolean;
31
+ isPinned: (id: string) => boolean;
32
+ pin: (entry: Omit<PinnedEntry, "pinnedAt">) => void;
33
+ unpin: (id: string) => void;
34
+ }
35
+
36
+ export function usePinnedEntries(): UsePinnedEntriesReturn {
37
+ const [pins, setPins] = useState<PinnedEntry[]>([]);
38
+ const [loading, setLoading] = useState(true);
39
+
40
+ useEffect(() => {
41
+ let cancelled = false;
42
+ fetch("/api/settings/chat/pins")
43
+ .then((r) => (r.ok ? r.json() : { pins: [] }))
44
+ .then((data: { pins?: PinnedEntry[] }) => {
45
+ if (!cancelled) setPins(data.pins ?? []);
46
+ })
47
+ .catch(() => {
48
+ // Network / parse failure: start with empty list. Subsequent writes
49
+ // will create the setting on first mutation.
50
+ if (!cancelled) setPins([]);
51
+ })
52
+ .finally(() => {
53
+ if (!cancelled) setLoading(false);
54
+ });
55
+ return () => {
56
+ cancelled = true;
57
+ };
58
+ }, []);
59
+
60
+ const pinnedIdSet = useMemo(() => new Set(pins.map((p) => p.id)), [pins]);
61
+
62
+ const isPinned = useCallback(
63
+ (id: string) => pinnedIdSet.has(id),
64
+ [pinnedIdSet]
65
+ );
66
+
67
+ const persist = useCallback(async (next: PinnedEntry[]) => {
68
+ try {
69
+ await fetch("/api/settings/chat/pins", {
70
+ method: "PUT",
71
+ headers: { "Content-Type": "application/json" },
72
+ body: JSON.stringify({ pins: next }),
73
+ });
74
+ } catch {
75
+ // Optimistic update already applied; server sync failure is silently
76
+ // swallowed. A future follow-up can add a toast on persistent failure.
77
+ }
78
+ }, []);
79
+
80
+ const pin = useCallback(
81
+ (entry: Omit<PinnedEntry, "pinnedAt">) => {
82
+ if (pinnedIdSet.has(entry.id)) return;
83
+ const next: PinnedEntry[] = [
84
+ ...pins,
85
+ { ...entry, pinnedAt: new Date().toISOString() },
86
+ ];
87
+ setPins(next);
88
+ void persist(next);
89
+ },
90
+ [pins, pinnedIdSet, persist]
91
+ );
92
+
93
+ const unpin = useCallback(
94
+ (id: string) => {
95
+ if (!pinnedIdSet.has(id)) return;
96
+ const next = pins.filter((p) => p.id !== id);
97
+ setPins(next);
98
+ void persist(next);
99
+ },
100
+ [pins, pinnedIdSet, persist]
101
+ );
102
+
103
+ return { pins, loading, isPinned, pin, unpin };
104
+ }
@@ -0,0 +1,19 @@
1
+ "use client";
2
+ import { useMemo } from "react";
3
+ import { useChatSession } from "@/components/chat/chat-session-provider";
4
+
5
+ export function useRecentUserMessages(
6
+ conversationId: string | null | undefined,
7
+ limit: number = 20
8
+ ): string[] {
9
+ const { messages } = useChatSession();
10
+ return useMemo(() => {
11
+ if (!conversationId) return [];
12
+ return messages
13
+ .filter((m) => m.role === "user")
14
+ .slice(-limit)
15
+ .map((m) =>
16
+ typeof m.content === "string" ? m.content : JSON.stringify(m.content)
17
+ );
18
+ }, [messages, conversationId, limit]);
19
+ }