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,7 +1,8 @@
1
1
  "use client";
2
2
 
3
- import { useEffect, useRef } from "react";
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
4
  import { createPortal } from "react-dom";
5
+ import { toast } from "sonner";
5
6
  import {
6
7
  Command,
7
8
  CommandEmpty,
@@ -15,19 +16,40 @@ import {
15
16
  ListTodo,
16
17
  GitBranch,
17
18
  FileText,
19
+ FileCode,
18
20
  Bot,
19
21
  Clock,
20
22
  Loader2,
23
+ Pin,
24
+ PinOff,
25
+ Bookmark,
21
26
  } from "lucide-react";
27
+ import { Button } from "@/components/ui/button";
22
28
  import type { LucideIcon } from "lucide-react";
23
29
  import {
24
30
  getToolCatalogWithSkills,
25
31
  groupToolCatalog,
26
32
  TOOL_GROUP_ICONS,
27
- TOOL_GROUP_ORDER,
28
- type ToolCatalogEntry,
29
33
  } from "@/lib/chat/tool-catalog";
30
34
  import type { AutocompleteMode, EntitySearchResult } from "@/hooks/use-chat-autocomplete";
35
+ import { CommandTabBar } from "./command-tab-bar";
36
+ import { partitionCatalogByTab, type CommandTabId } from "@/lib/chat/command-tabs";
37
+ import { useEnrichedSkills } from "@/hooks/use-enriched-skills";
38
+ import { useRecentUserMessages } from "@/hooks/use-recent-user-messages";
39
+ import { SkillRow } from "./skill-row";
40
+ import { computeRecommendation } from "@/lib/environment/skill-recommendations";
41
+ import { browserLocalStore, activeDismissedIds, saveDismissal } from "@/lib/chat/dismissals";
42
+ import { useChatSession } from "@/components/chat/chat-session-provider";
43
+ import type { EnrichedSkill } from "@/lib/environment/skill-enrichment";
44
+ import { parseFilterInput, matchesClauses } from "@/lib/filters/parse";
45
+ import type { FilterClause } from "@/lib/filters/parse";
46
+ import { FilterHint } from "@/components/shared/filter-hint";
47
+ import { cleanFilterInput } from "@/lib/chat/clean-filter-input";
48
+ import { usePinnedEntries, type PinnedEntry } from "@/hooks/use-pinned-entries";
49
+ import { useSavedSearches, type SavedSearch, type SavedSearchSurface } from "@/hooks/use-saved-searches";
50
+ import { useActiveSkills } from "@/hooks/use-active-skills";
51
+ import { SkillCompositionConflictDialog } from "./skill-composition-conflict-dialog";
52
+ import type { SkillConflict } from "@/lib/chat/skill-conflict";
31
53
 
32
54
  interface ChatCommandPopoverProps {
33
55
  open: boolean;
@@ -37,6 +59,8 @@ interface ChatCommandPopoverProps {
37
59
  entityResults: EntitySearchResult[];
38
60
  entityLoading: boolean;
39
61
  projectProfiles?: Array<{ id: string; name: string; description: string }>;
62
+ activeTab: CommandTabId;
63
+ onTabChange: (tab: CommandTabId) => void;
40
64
  onSelect: (item: {
41
65
  type: "slash" | "mention";
42
66
  id: string;
@@ -46,6 +70,9 @@ interface ChatCommandPopoverProps {
46
70
  entityId?: string;
47
71
  }) => void;
48
72
  onClose: () => void;
73
+ onApplySavedSearch?: (filterInput: string) => void;
74
+ /** Active conversation id — used for skill composition HTTP calls. */
75
+ conversationId?: string | null;
49
76
  }
50
77
 
51
78
  const ENTITY_ICONS: Record<string, LucideIcon> = {
@@ -55,6 +82,7 @@ const ENTITY_ICONS: Record<string, LucideIcon> = {
55
82
  document: FileText,
56
83
  profile: Bot,
57
84
  schedule: Clock,
85
+ file: FileCode,
58
86
  };
59
87
 
60
88
  const ENTITY_LABELS: Record<string, string> = {
@@ -64,6 +92,7 @@ const ENTITY_LABELS: Record<string, string> = {
64
92
  document: "Documents",
65
93
  profile: "Profiles",
66
94
  schedule: "Schedules",
95
+ file: "Files",
67
96
  };
68
97
 
69
98
  function groupByType(results: EntitySearchResult[]): Record<string, EntitySearchResult[]> {
@@ -83,8 +112,12 @@ export function ChatCommandPopover({
83
112
  entityResults,
84
113
  entityLoading,
85
114
  projectProfiles,
115
+ activeTab,
116
+ onTabChange,
86
117
  onSelect,
87
118
  onClose,
119
+ onApplySavedSearch,
120
+ conversationId,
88
121
  }: ChatCommandPopoverProps) {
89
122
  const containerRef = useRef<HTMLDivElement>(null);
90
123
 
@@ -100,6 +133,156 @@ export function ChatCommandPopover({
100
133
  return () => document.removeEventListener("mousedown", handleClick);
101
134
  }, [open, onClose]);
102
135
 
136
+ // -----------------------------------------------------------------------
137
+ // Skill composition state
138
+ // -----------------------------------------------------------------------
139
+ const { activeIds, supportsComposition, maxActive, refetch: refetchActive } =
140
+ useActiveSkills(conversationId ?? null);
141
+
142
+ // When activate returns requiresConfirmation, we stash the pending request
143
+ // and open the conflict dialog for the user to decide.
144
+ const [pendingAdd, setPendingAdd] = useState<{
145
+ skillId: string;
146
+ skillName: string;
147
+ conflicts: SkillConflict[];
148
+ } | null>(null);
149
+
150
+ const callActivate = useCallback(
151
+ async (skillId: string, skillName: string, mode: "replace" | "add", force = false) => {
152
+ if (!conversationId) return;
153
+ const r = await fetch(
154
+ `/api/chat/conversations/${conversationId}/skills/activate`,
155
+ {
156
+ method: "POST",
157
+ headers: { "Content-Type": "application/json" },
158
+ body: JSON.stringify({ skillId, mode, force }),
159
+ }
160
+ );
161
+ if (!r.ok) {
162
+ const body = await r.json().catch(() => ({})) as Record<string, unknown>;
163
+ toast.error(typeof body.error === "string" ? body.error : "Failed to add skill");
164
+ return;
165
+ }
166
+ const body = await r.json() as Record<string, unknown>;
167
+ if (body.requiresConfirmation) {
168
+ setPendingAdd({
169
+ skillId,
170
+ skillName,
171
+ conflicts: (body.conflicts as SkillConflict[]) ?? [],
172
+ });
173
+ return;
174
+ }
175
+ await refetchActive();
176
+ const activeCount = Array.isArray(body.activeSkillIds)
177
+ ? (body.activeSkillIds as string[]).length
178
+ : 1;
179
+ toast.success(`Added ${skillName} — ${activeCount} skill${activeCount !== 1 ? "s" : ""} active`);
180
+ },
181
+ [conversationId, refetchActive]
182
+ );
183
+
184
+ const callDeactivate = useCallback(async () => {
185
+ if (!conversationId) return;
186
+ await fetch(`/api/chat/conversations/${conversationId}/skills/deactivate`, {
187
+ method: "POST",
188
+ });
189
+ await refetchActive();
190
+ }, [conversationId, refetchActive]);
191
+
192
+ // Enriched skills — only fetch when popover is open in slash mode
193
+ const enrichedSkills = useEnrichedSkills(open && mode === "slash");
194
+
195
+ // Session context for recommendation
196
+ const { activeId } = useChatSession();
197
+ const recentMessages = useRecentUserMessages(activeId, 20);
198
+
199
+ const dismissStore = useMemo(
200
+ () => browserLocalStore("stagent.chat.dismissed-suggestions"),
201
+ []
202
+ );
203
+
204
+ const [dismissTick, setDismissTick] = useState(0);
205
+
206
+ const dismissedIds = useMemo(
207
+ () =>
208
+ activeId
209
+ ? activeDismissedIds(dismissStore, activeId)
210
+ : new Set<string>(),
211
+ [dismissStore, activeId, dismissTick]
212
+ );
213
+
214
+ const recommended = useMemo(
215
+ () =>
216
+ computeRecommendation(enrichedSkills, recentMessages, {
217
+ dismissedIds,
218
+ }),
219
+ [enrichedSkills, recentMessages, dismissedIds]
220
+ );
221
+
222
+ // Pinned entries persist under settings.chat.pinnedEntries. Hook self-
223
+ // fetches on mount and sends optimistic PUTs on mutation.
224
+ const { pins, isPinned, pin, unpin } = usePinnedEntries();
225
+
226
+ // Parse `#key:value` filter clauses from the query. Relevant for mention
227
+ // mode — slash mode does its own tab-based grouping and doesn't currently
228
+ // consume free-text filters.
229
+ const parsed = useMemo(() => parseFilterInput(query), [query]);
230
+
231
+ // Pre-filter entity results by known filter keys. Unknown keys pass through
232
+ // per the parser contract (silently skipped). cmdk still runs its own
233
+ // fuzzy match on top using `parsed.rawQuery`.
234
+ const filteredEntityResults = useMemo(() => {
235
+ if (parsed.clauses.length === 0) return entityResults;
236
+ return entityResults.filter((r) =>
237
+ matchesClauses(r, parsed.clauses, {
238
+ // `#status:blocked` — case-insensitive substring match so partial
239
+ // values like `#status:block` also hit (helps while typing).
240
+ status: (item, value) =>
241
+ typeof item.status === "string" &&
242
+ item.status.toLowerCase().includes(value.toLowerCase()),
243
+ // `#type:task` — exact match on the entity-type discriminator.
244
+ type: (item, value) =>
245
+ item.entityType.toLowerCase() === value.toLowerCase(),
246
+ })
247
+ );
248
+ }, [entityResults, parsed.clauses]);
249
+
250
+ const { forSurface, save } = useSavedSearches();
251
+
252
+ // Surface inference for saved-search scoping. In mention mode, look at
253
+ // the first filtered entity result's type; fall back to "task" when the
254
+ // list is empty so the "Save this view" button still has a valid target.
255
+ // Slash-mode surface inference is deferred — Saved group renders in
256
+ // mention mode only in v2.
257
+ const currentSurface: SavedSearchSurface = useMemo(() => {
258
+ if (mode !== "mention") return "task";
259
+ const firstType = filteredEntityResults[0]?.entityType as SavedSearchSurface | undefined;
260
+ if (firstType && ["task", "project", "workflow", "document", "skill", "profile"].includes(firstType)) {
261
+ return firstType;
262
+ }
263
+ return "task";
264
+ }, [mode, filteredEntityResults]);
265
+
266
+ const savedForSurface = useMemo(
267
+ () => forSurface(currentSurface),
268
+ [forSurface, currentSurface]
269
+ );
270
+
271
+ // Filter enriched skills by `#scope:` and `#type:` clauses so users can
272
+ // narrow the skills tab with e.g. `/skills #scope:project` or
273
+ // `/skills #type:claude-agent-sdk`. Unknown clauses pass through silently.
274
+ const filteredEnrichedSkills = useMemo(() => {
275
+ if (parsed.clauses.length === 0) return enrichedSkills;
276
+ return enrichedSkills.filter((skill) =>
277
+ matchesClauses(skill, parsed.clauses, {
278
+ // `#scope:project` / `#scope:user` — exact case-insensitive match.
279
+ scope: (s, v) => s.scope.toLowerCase() === v.toLowerCase(),
280
+ // `#type:skill-name` — substring match on the tool name.
281
+ type: (s, v) => (s.tool ?? "").toLowerCase().includes(v.toLowerCase()),
282
+ })
283
+ );
284
+ }, [enrichedSkills, parsed.clauses]);
285
+
103
286
  if (!open || !anchorRect || !mode) return null;
104
287
 
105
288
  // Position above the caret
@@ -116,60 +299,281 @@ export function ChatCommandPopover({
116
299
  ref={containerRef}
117
300
  style={style}
118
301
  data-chat-autocomplete=""
119
- className="rounded-lg border bg-popover text-popover-foreground shadow-lg animate-in fade-in-0 zoom-in-95 slide-in-from-bottom-2"
302
+ className="rounded-lg border bg-popover text-popover-foreground shadow-lg animate-in fade-in-0"
120
303
  >
121
304
  <Command shouldFilter loop>
122
- {/* Hidden input for cmdk filtering synced to query */}
305
+ {/* Hidden input for cmdk filtering. In mention mode we pass the
306
+ filter-stripped `rawQuery` so cmdk's fuzzy match doesn't see
307
+ `#key:value` tokens and score every entity to zero. */}
123
308
  <div className="sr-only">
124
- <CommandInput value={query} />
309
+ <CommandInput value={mode === "mention" ? parsed.rawQuery : query} />
125
310
  </div>
126
311
 
127
- <CommandList className="max-h-[320px]">
128
- <CommandEmpty>
129
- {mode === "slash" ? "No matching tools" : "No matching entities"}
130
- </CommandEmpty>
131
-
132
- {mode === "slash" && (
133
- <ToolCatalogItems
134
- onSelect={onSelect}
135
- projectProfiles={projectProfiles}
136
- />
137
- )}
138
-
139
- {mode === "mention" && (
312
+ {mode === "slash" ? (
313
+ <>
314
+ <CommandTabBar activeTab={activeTab} onChange={onTabChange} />
315
+ {/* Active skill count indicator — shown only on the skills tab. */}
316
+ {activeTab === "skills" && conversationId && (
317
+ <div className="px-3 py-1.5 text-xs text-muted-foreground border-b">
318
+ {activeIds.length} of {maxActive} active
319
+ </div>
320
+ )}
321
+ <CommandList className="max-h-[320px]">
322
+ <FilterHint inputValue={query} storageKey="stagent.filter-hint.dismissed" />
323
+ {activeTab !== "entities" && (
324
+ <CommandEmpty>No matching tools</CommandEmpty>
325
+ )}
326
+ <div
327
+ role="tabpanel"
328
+ id={`command-tabpanel-${activeTab}`}
329
+ aria-labelledby={`command-tab-${activeTab}`}
330
+ >
331
+ <ToolCatalogItems
332
+ onSelect={onSelect}
333
+ projectProfiles={projectProfiles}
334
+ activeTab={activeTab}
335
+ enrichedSkills={filteredEnrichedSkills}
336
+ totalSkillCount={enrichedSkills.length}
337
+ recommendedId={recommended?.id ?? null}
338
+ onDismissRecommendation={
339
+ activeId
340
+ ? (skillId) => {
341
+ saveDismissal(dismissStore, activeId, skillId);
342
+ setDismissTick((t) => t + 1);
343
+ }
344
+ : undefined
345
+ }
346
+ activeSkillIds={activeIds}
347
+ supportsComposition={supportsComposition}
348
+ maxActive={maxActive}
349
+ onAddSkill={
350
+ conversationId
351
+ ? (skillId, skillName) =>
352
+ callActivate(skillId, skillName, "add")
353
+ : undefined
354
+ }
355
+ onDeactivate={conversationId ? callDeactivate : undefined}
356
+ />
357
+ </div>
358
+ </CommandList>
359
+ </>
360
+ ) : (
361
+ <CommandList className="max-h-[320px]">
362
+ <CommandEmpty>No matching entities</CommandEmpty>
140
363
  <MentionItems
141
- results={entityResults}
364
+ results={filteredEntityResults}
142
365
  loading={entityLoading}
143
366
  onSelect={onSelect}
367
+ pins={pins}
368
+ isPinned={isPinned}
369
+ onPin={pin}
370
+ onUnpin={unpin}
371
+ rawQuery={parsed.rawQuery}
372
+ savedSearches={savedForSurface}
373
+ onApplySavedSearch={(filterInput) => onApplySavedSearch?.(filterInput)}
374
+ clauses={parsed.clauses}
144
375
  />
145
- )}
146
- </CommandList>
376
+ {parsed.clauses.length > 0 && (() => {
377
+ // Persist the cleaned filterInput so saved searches don't
378
+ // carry the mention-trigger residue (e.g. `task: `) into
379
+ // their stored value. See features/saved-search-polish-v1.md.
380
+ const persistedFilterInput = cleanFilterInput(
381
+ parsed.clauses,
382
+ parsed.rawQuery
383
+ );
384
+ return (
385
+ <SaveViewFooter
386
+ surface={currentSurface}
387
+ clauses={parsed.clauses}
388
+ filterInput={persistedFilterInput}
389
+ onSave={(label) =>
390
+ save({
391
+ surface: currentSurface,
392
+ label:
393
+ label ||
394
+ parsed.clauses
395
+ .map((c) => `#${c.key}:${c.value}`)
396
+ .join(" "),
397
+ filterInput: persistedFilterInput,
398
+ })
399
+ }
400
+ />
401
+ );
402
+ })()}
403
+ </CommandList>
404
+ )}
147
405
  </Command>
148
406
  </div>
149
407
  );
150
408
 
151
- return createPortal(content, document.body);
409
+ return (
410
+ <>
411
+ {createPortal(content, document.body)}
412
+ {pendingAdd && (
413
+ <SkillCompositionConflictDialog
414
+ open={!!pendingAdd}
415
+ onOpenChange={(o) => { if (!o) setPendingAdd(null); }}
416
+ newSkillName={pendingAdd.skillName}
417
+ conflicts={pendingAdd.conflicts}
418
+ onConfirm={() => {
419
+ void callActivate(pendingAdd.skillId, pendingAdd.skillName, "add", true);
420
+ }}
421
+ />
422
+ )}
423
+ </>
424
+ );
152
425
  }
153
426
 
154
427
  function ToolCatalogItems({
155
428
  onSelect,
156
429
  projectProfiles,
430
+ activeTab,
431
+ enrichedSkills,
432
+ totalSkillCount,
433
+ recommendedId,
434
+ onDismissRecommendation,
435
+ activeSkillIds,
436
+ supportsComposition,
437
+ maxActive,
438
+ onAddSkill,
439
+ onDeactivate,
157
440
  }: {
158
441
  onSelect: ChatCommandPopoverProps["onSelect"];
159
442
  projectProfiles?: ChatCommandPopoverProps["projectProfiles"];
443
+ activeTab: CommandTabId;
444
+ /** Filtered list of skills to render (may be a subset of all skills). */
445
+ enrichedSkills: EnrichedSkill[];
446
+ /** Total number of skills before any filter is applied — used for empty-state copy. */
447
+ totalSkillCount: number;
448
+ recommendedId?: string | null;
449
+ onDismissRecommendation?: (skillId: string) => void;
450
+ /** Currently active skill IDs for this conversation. */
451
+ activeSkillIds?: string[];
452
+ /** Whether the current runtime supports composing 2+ skills. */
453
+ supportsComposition?: boolean;
454
+ /** Max simultaneously-active skills for this runtime. */
455
+ maxActive?: number;
456
+ /** Called when the user clicks "+ Add" on an inactive skill. */
457
+ onAddSkill?: (skillId: string, skillName: string) => void;
458
+ /** Called when the user deactivates the current skill. */
459
+ onDeactivate?: () => void;
160
460
  }) {
161
461
  const catalog = getToolCatalogWithSkills({
162
462
  includeBrowser: true,
163
463
  projectProfiles,
164
464
  });
165
- const groups = groupToolCatalog(catalog);
465
+ const parts = partitionCatalogByTab(catalog);
466
+ const entries = parts[activeTab];
467
+
468
+ if (activeTab === "entities") {
469
+ return (
470
+ <div className="px-4 py-6 text-sm text-muted-foreground text-center">
471
+ Type <span className="font-mono text-foreground">@</span> to reference projects, tasks, documents, or files.
472
+ </div>
473
+ );
474
+ }
475
+
476
+ // When the skills tab has enriched data, render the enriched list
477
+ if (activeTab === "skills" && enrichedSkills.length > 0) {
478
+ const activeSet = new Set(activeSkillIds ?? []);
479
+ const resolvedMax = maxActive ?? 1;
480
+ const atCapacity = (activeSkillIds?.length ?? 0) >= resolvedMax;
481
+
482
+ return (
483
+ <CommandGroup heading="Skills">
484
+ {enrichedSkills.map((skill) => {
485
+ const isActive = activeSet.has(skill.id);
486
+ // Show "+ Add" only when composition is available, slot is free, and
487
+ // we have a conversationId to POST to.
488
+ const canAdd =
489
+ !isActive &&
490
+ supportsComposition &&
491
+ !atCapacity &&
492
+ !!onAddSkill;
493
+ // Show disabled "+" when at capacity or runtime doesn't support composition.
494
+ const showDisabled = !isActive && !canAdd && !!onAddSkill;
495
+ const disabledReason = atCapacity
496
+ ? `Max ${resolvedMax} skills active`
497
+ : "Single skill only on this runtime — switch runtime to compose";
498
+
499
+ return (
500
+ <SkillRow
501
+ key={skill.id}
502
+ skill={skill}
503
+ recommended={recommendedId === skill.id}
504
+ onDismissRecommendation={
505
+ recommendedId === skill.id
506
+ ? () => onDismissRecommendation?.(skill.id)
507
+ : undefined
508
+ }
509
+ onSelect={() =>
510
+ onSelect({
511
+ type: "slash",
512
+ id: skill.name,
513
+ label: skill.name,
514
+ text: `Use the ${skill.name} profile: `,
515
+ })
516
+ }
517
+ isActive={isActive}
518
+ addButton={
519
+ canAdd ? (
520
+ <Button
521
+ variant="ghost"
522
+ size="sm"
523
+ className="ml-auto h-6 px-2 text-[10px] shrink-0"
524
+ onMouseDown={(e) => {
525
+ e.preventDefault();
526
+ e.stopPropagation();
527
+ }}
528
+ onClick={(e) => {
529
+ e.stopPropagation();
530
+ onAddSkill(skill.id, skill.name);
531
+ }}
532
+ >
533
+ + Add
534
+ </Button>
535
+ ) : showDisabled ? (
536
+ <Button
537
+ variant="ghost"
538
+ size="sm"
539
+ disabled
540
+ aria-label={disabledReason}
541
+ title={disabledReason}
542
+ className="ml-auto h-6 px-2 text-[10px] shrink-0"
543
+ >
544
+ + Add
545
+ </Button>
546
+ ) : undefined
547
+ }
548
+ onDeactivate={isActive && onDeactivate ? onDeactivate : undefined}
549
+ />
550
+ );
551
+ })}
552
+ </CommandGroup>
553
+ );
554
+ }
555
+
556
+ if (entries.length === 0) {
557
+ return (
558
+ <div className="px-4 py-6 text-sm text-muted-foreground text-center">
559
+ {activeTab === "skills"
560
+ ? totalSkillCount > 0
561
+ ? "No skills match these filters."
562
+ : "No skills available yet."
563
+ : "Nothing here."}
564
+ </div>
565
+ );
566
+ }
567
+
568
+ const groups = groupToolCatalog(entries);
569
+ const groupNames = Object.keys(groups);
166
570
 
167
571
  return (
168
572
  <>
169
- {TOOL_GROUP_ORDER.map((groupName) => {
573
+ {groupNames.map((groupName) => {
170
574
  const items = groups[groupName];
171
575
  if (!items?.length) return null;
172
- const GroupIcon = TOOL_GROUP_ICONS[groupName];
576
+ const GroupIcon = TOOL_GROUP_ICONS[groupName as keyof typeof TOOL_GROUP_ICONS] ?? FileText;
173
577
  return (
174
578
  <CommandGroup key={groupName} heading={groupName}>
175
579
  {items.map((entry) => (
@@ -184,8 +588,8 @@ function ToolCatalogItems({
184
588
  text: entry.behavior === "execute_immediately"
185
589
  ? entry.name
186
590
  : entry.group === "Skills"
187
- ? `Use the ${entry.name} profile: `
188
- : `Use ${entry.name} to `,
591
+ ? `Use the ${entry.name} profile: `
592
+ : `Use ${entry.name} to `,
189
593
  })
190
594
  }
191
595
  >
@@ -214,10 +618,26 @@ function MentionItems({
214
618
  results,
215
619
  loading,
216
620
  onSelect,
621
+ pins,
622
+ isPinned,
623
+ onPin,
624
+ onUnpin,
625
+ rawQuery,
626
+ savedSearches,
627
+ onApplySavedSearch,
628
+ clauses,
217
629
  }: {
218
630
  results: EntitySearchResult[];
219
631
  loading: boolean;
220
632
  onSelect: ChatCommandPopoverProps["onSelect"];
633
+ pins: PinnedEntry[];
634
+ isPinned: (id: string) => boolean;
635
+ onPin: (entry: Omit<PinnedEntry, "pinnedAt">) => void;
636
+ onUnpin: (id: string) => void;
637
+ rawQuery: string;
638
+ savedSearches: SavedSearch[];
639
+ onApplySavedSearch?: (filterInput: string) => void;
640
+ clauses: FilterClause[];
221
641
  }) {
222
642
  if (loading && results.length === 0) {
223
643
  return (
@@ -228,53 +648,254 @@ function MentionItems({
228
648
  );
229
649
  }
230
650
 
231
- const grouped = groupByType(results);
651
+ // Pins render from the standalone pin records (denormalized label/status),
652
+ // so they surface even when outside the current entities/search window.
653
+ // Filter pins by `rawQuery` so typing a query narrows pins too.
654
+ const q = rawQuery.toLowerCase();
655
+ const visiblePins =
656
+ q.length === 0
657
+ ? pins
658
+ : pins.filter(
659
+ (p) =>
660
+ p.label.toLowerCase().includes(q) ||
661
+ p.description?.toLowerCase().includes(q)
662
+ );
663
+
664
+ // Hide pinned items from their regular type group so they don't render
665
+ // twice on the same popover open.
666
+ const unpinnedResults = results.filter((r) => !isPinned(r.entityId));
667
+ const grouped = groupByType(unpinnedResults);
232
668
  const entityTypes = Object.keys(grouped);
233
669
 
234
- if (entityTypes.length === 0) {
235
- return null; // CommandEmpty will show
670
+ if (savedSearches.length === 0 && visiblePins.length === 0 && entityTypes.length === 0) {
671
+ if (clauses.length > 0) {
672
+ return (
673
+ <CommandEmpty>
674
+ No matches for{" "}
675
+ <span className="font-mono">
676
+ {clauses.map((c) => `#${c.key}:${c.value}`).join(" ")}
677
+ </span>
678
+ </CommandEmpty>
679
+ );
680
+ }
681
+ return null; // Generic "No results" handled by parent CommandList
236
682
  }
237
683
 
238
684
  return (
239
685
  <>
240
- {entityTypes.map((type) => {
241
- const Icon = ENTITY_ICONS[type] ?? FileText;
242
- const groupLabel = ENTITY_LABELS[type] ?? type;
243
- return (
244
- <CommandGroup key={type} heading={groupLabel}>
245
- {grouped[type].map((entity) => (
686
+ {savedSearches.length > 0 && (
687
+ <CommandGroup heading="Saved">
688
+ {savedSearches.map((s) => (
689
+ <CommandItem
690
+ key={`saved-${s.id}`}
691
+ value={`saved ${s.label} ${s.filterInput}`}
692
+ onSelect={() => onApplySavedSearch?.(s.filterInput)}
693
+ >
694
+ <Bookmark className="h-4 w-4 shrink-0" />
695
+ <span className="flex-1 truncate">{s.label}</span>
696
+ <span className="ml-auto shrink-0 text-xs font-mono text-muted-foreground">
697
+ {s.filterInput}
698
+ </span>
699
+ </CommandItem>
700
+ ))}
701
+ </CommandGroup>
702
+ )}
703
+ {visiblePins.length > 0 && (
704
+ <CommandGroup heading="Pinned">
705
+ {visiblePins.map((p) => {
706
+ const Icon = ENTITY_ICONS[p.type] ?? FileText;
707
+ return (
246
708
  <CommandItem
247
- key={`${entity.entityType}-${entity.entityId}`}
248
- value={`${entity.entityType} ${entity.label} ${entity.description ?? ""} ${entity.status ?? ""}`}
709
+ key={`pin-${p.id}`}
710
+ value={`pinned ${p.type} ${p.label} ${p.description ?? ""} ${p.status ?? ""}`}
249
711
  onSelect={() =>
250
712
  onSelect({
251
713
  type: "mention",
252
- id: entity.entityType,
253
- label: entity.label,
254
- entityType: entity.entityType,
255
- entityId: entity.entityId,
714
+ id: p.type,
715
+ label: p.label,
716
+ entityType: p.type,
717
+ entityId: p.id,
256
718
  })
257
719
  }
258
720
  >
259
721
  <Icon className="h-4 w-4 shrink-0" />
260
722
  <div className="flex flex-col min-w-0">
261
- <span className="flex-1 truncate">{entity.label}</span>
262
- {entity.description && (
723
+ <span className="flex-1 truncate">{p.label}</span>
724
+ {p.description && (
263
725
  <span className="truncate text-xs text-muted-foreground">
264
- {entity.description}
726
+ {p.description}
265
727
  </span>
266
728
  )}
267
729
  </div>
268
- {entity.status && (
730
+ {p.status && (
269
731
  <span className="ml-auto shrink-0 text-xs text-muted-foreground">
270
- {entity.status}
732
+ {p.status}
271
733
  </span>
272
734
  )}
735
+ <button
736
+ type="button"
737
+ aria-label={`Unpin ${p.label}`}
738
+ className="ml-2 shrink-0 rounded p-0.5 text-muted-foreground hover:text-foreground hover:bg-muted focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
739
+ // Stop cmdk's parent row selection on pin-button click —
740
+ // otherwise the unpin fires AND the item is inserted.
741
+ onMouseDown={(e) => {
742
+ e.preventDefault();
743
+ e.stopPropagation();
744
+ }}
745
+ onClick={(e) => {
746
+ e.stopPropagation();
747
+ onUnpin(p.id);
748
+ }}
749
+ >
750
+ <PinOff className="h-3.5 w-3.5" />
751
+ </button>
273
752
  </CommandItem>
274
- ))}
753
+ );
754
+ })}
755
+ </CommandGroup>
756
+ )}
757
+ {entityTypes.map((type) => {
758
+ const Icon = ENTITY_ICONS[type] ?? FileText;
759
+ const groupLabel = ENTITY_LABELS[type] ?? type;
760
+ const isFile = type === "file";
761
+ return (
762
+ <CommandGroup key={type} heading={groupLabel}>
763
+ {grouped[type].map((entity) => {
764
+ const pinnable = entity.entityType !== "file";
765
+ return (
766
+ <CommandItem
767
+ key={`${entity.entityType}-${entity.entityId}`}
768
+ value={`${entity.entityType} ${entity.label} ${entity.description ?? ""} ${entity.status ?? ""}`}
769
+ onSelect={() =>
770
+ onSelect({
771
+ type: "mention",
772
+ id: entity.entityType,
773
+ label: entity.label,
774
+ entityType: entity.entityType,
775
+ entityId: entity.entityId,
776
+ })
777
+ }
778
+ >
779
+ <Icon className="h-4 w-4 shrink-0" />
780
+ <div className="flex flex-col min-w-0">
781
+ <span
782
+ className={
783
+ isFile
784
+ ? "flex-1 truncate font-mono text-xs"
785
+ : "flex-1 truncate"
786
+ }
787
+ >
788
+ {entity.label}
789
+ </span>
790
+ {entity.description && (
791
+ <span className="truncate text-xs text-muted-foreground">
792
+ {entity.description}
793
+ </span>
794
+ )}
795
+ </div>
796
+ {entity.status && (
797
+ <span className="ml-auto shrink-0 text-xs text-muted-foreground">
798
+ {entity.status}
799
+ </span>
800
+ )}
801
+ {pinnable && (
802
+ <button
803
+ type="button"
804
+ aria-label={`Pin ${entity.label}`}
805
+ className="ml-2 shrink-0 rounded p-0.5 text-muted-foreground opacity-0 group-hover:opacity-100 data-[selected=true]:opacity-100 hover:text-foreground hover:bg-muted focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring transition-opacity"
806
+ onMouseDown={(e) => {
807
+ e.preventDefault();
808
+ e.stopPropagation();
809
+ }}
810
+ onClick={(e) => {
811
+ e.stopPropagation();
812
+ onPin({
813
+ id: entity.entityId,
814
+ type: entity.entityType,
815
+ label: entity.label,
816
+ description: entity.description,
817
+ status: entity.status,
818
+ });
819
+ }}
820
+ >
821
+ <Pin className="h-3.5 w-3.5" />
822
+ </button>
823
+ )}
824
+ </CommandItem>
825
+ );
826
+ })}
275
827
  </CommandGroup>
276
828
  );
277
829
  })}
278
830
  </>
279
831
  );
280
832
  }
833
+
834
+ function SaveViewFooter({
835
+ surface,
836
+ clauses,
837
+ filterInput,
838
+ onSave,
839
+ }: {
840
+ surface: SavedSearchSurface;
841
+ clauses: FilterClause[];
842
+ filterInput: string;
843
+ onSave: (label: string) => void;
844
+ }) {
845
+ const [renaming, setRenaming] = useState(false);
846
+ const [draft, setDraft] = useState("");
847
+
848
+ const defaultLabel = clauses.map((c) => `#${c.key}:${c.value}`).join(" ");
849
+
850
+ if (!renaming) {
851
+ return (
852
+ <div className="border-t px-2 py-1.5">
853
+ <button
854
+ type="button"
855
+ className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground px-1 py-0.5 rounded transition-colors"
856
+ onClick={() => setRenaming(true)}
857
+ >
858
+ <Bookmark className="h-3.5 w-3.5" />
859
+ Save this view ({surface})
860
+ </button>
861
+ </div>
862
+ );
863
+ }
864
+
865
+ return (
866
+ <form
867
+ className="border-t px-2 py-1.5 flex items-center gap-2"
868
+ onSubmit={(e) => {
869
+ e.preventDefault();
870
+ onSave(draft.trim());
871
+ setRenaming(false);
872
+ setDraft("");
873
+ }}
874
+ >
875
+ <input
876
+ autoFocus
877
+ type="text"
878
+ value={draft}
879
+ onChange={(e) => setDraft(e.target.value)}
880
+ placeholder={defaultLabel}
881
+ className="flex-1 h-7 px-2 text-xs rounded border bg-background focus:outline-none focus:ring-1 focus:ring-ring"
882
+ />
883
+ <button
884
+ type="submit"
885
+ className="h-7 px-2 text-xs rounded bg-primary text-primary-foreground hover:bg-primary/90"
886
+ >
887
+ Save
888
+ </button>
889
+ <button
890
+ type="button"
891
+ className="h-7 px-2 text-xs text-muted-foreground hover:text-foreground"
892
+ onClick={() => {
893
+ setRenaming(false);
894
+ setDraft("");
895
+ }}
896
+ >
897
+ Cancel
898
+ </button>
899
+ </form>
900
+ );
901
+ }