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,40 @@
1
+ import { render, screen, waitFor } from "@testing-library/react";
2
+ import { describe, it, expect, beforeEach } from "vitest";
3
+ import { FilterHint } from "../filter-hint";
4
+
5
+ const KEY = "stagent.filter-hint.dismissed";
6
+
7
+ describe("FilterHint", () => {
8
+ beforeEach(() => {
9
+ localStorage.removeItem(KEY);
10
+ });
11
+
12
+ it("renders when input is empty and not dismissed", () => {
13
+ render(<FilterHint inputValue="" storageKey={KEY} />);
14
+ expect(screen.getByText(/#key:value/i)).toBeInTheDocument();
15
+ });
16
+
17
+ it("renders when input has no # character", () => {
18
+ render(<FilterHint inputValue="some search" storageKey={KEY} />);
19
+ expect(screen.getByText(/#key:value/i)).toBeInTheDocument();
20
+ });
21
+
22
+ it("hides when input contains #", () => {
23
+ render(<FilterHint inputValue="#status:blocked" storageKey={KEY} />);
24
+ expect(screen.queryByText(/#key:value/i)).toBeNull();
25
+ });
26
+
27
+ it("sets dismissal flag and hides when input parses a valid clause", async () => {
28
+ render(<FilterHint inputValue="#type:pdf" storageKey={KEY} />);
29
+ expect(localStorage.getItem(KEY)).toBe("1");
30
+ await waitFor(() => {
31
+ expect(screen.queryByText(/#key:value/i)).toBeNull();
32
+ });
33
+ });
34
+
35
+ it("stays hidden on subsequent mounts once dismissed", () => {
36
+ localStorage.setItem(KEY, "1");
37
+ render(<FilterHint inputValue="" storageKey={KEY} />);
38
+ expect(screen.queryByText(/#key:value/i)).toBeNull();
39
+ });
40
+ });
@@ -0,0 +1,147 @@
1
+ import { render, screen, fireEvent } from "@testing-library/react";
2
+ import { describe, it, expect, vi } from "vitest";
3
+ import { SavedSearchesManager } from "../saved-searches-manager";
4
+ import type { SavedSearch } from "@/hooks/use-saved-searches";
5
+
6
+ const search = (over: Partial<SavedSearch> = {}): SavedSearch => ({
7
+ id: "s1",
8
+ surface: "task",
9
+ label: "Blocked tasks",
10
+ filterInput: "#status:blocked",
11
+ createdAt: "2026-04-14T00:00:00.000Z",
12
+ ...over,
13
+ });
14
+
15
+ describe("SavedSearchesManager", () => {
16
+ it("lists all saved searches", () => {
17
+ const items = [
18
+ search({ id: "s1", label: "Blocked tasks" }),
19
+ search({ id: "s2", label: "Pdf docs", surface: "document", filterInput: "#type:pdf" }),
20
+ ];
21
+ render(
22
+ <SavedSearchesManager
23
+ open
24
+ onOpenChange={() => {}}
25
+ searches={items}
26
+ onRename={() => {}}
27
+ onRemove={() => {}}
28
+ />
29
+ );
30
+ expect(screen.getByText("Blocked tasks")).toBeInTheDocument();
31
+ expect(screen.getByText("Pdf docs")).toBeInTheDocument();
32
+ });
33
+
34
+ it("renames on blur with non-empty trimmed label", () => {
35
+ const onRename = vi.fn();
36
+ render(
37
+ <SavedSearchesManager
38
+ open
39
+ onOpenChange={() => {}}
40
+ searches={[search()]}
41
+ onRename={onRename}
42
+ onRemove={() => {}}
43
+ />
44
+ );
45
+ fireEvent.click(screen.getByRole("button", { name: /rename blocked tasks/i }));
46
+ const input = screen.getByRole("textbox", { name: /rename/i });
47
+ fireEvent.change(input, { target: { value: " Renamed " } });
48
+ fireEvent.blur(input);
49
+ expect(onRename).toHaveBeenCalledWith("s1", "Renamed");
50
+ });
51
+
52
+ it("rejects empty label with inline error", () => {
53
+ const onRename = vi.fn();
54
+ render(
55
+ <SavedSearchesManager
56
+ open
57
+ onOpenChange={() => {}}
58
+ searches={[search()]}
59
+ onRename={onRename}
60
+ onRemove={() => {}}
61
+ />
62
+ );
63
+ fireEvent.click(screen.getByRole("button", { name: /rename blocked tasks/i }));
64
+ const input = screen.getByRole("textbox", { name: /rename/i });
65
+ fireEvent.change(input, { target: { value: " " } });
66
+ fireEvent.blur(input);
67
+ expect(onRename).not.toHaveBeenCalled();
68
+ expect(screen.getByText(/cannot be empty/i)).toBeInTheDocument();
69
+ });
70
+
71
+ it("rejects duplicate label within same surface (case-insensitive)", () => {
72
+ const onRename = vi.fn();
73
+ render(
74
+ <SavedSearchesManager
75
+ open
76
+ onOpenChange={() => {}}
77
+ searches={[
78
+ search({ id: "s1", label: "Blocked tasks" }),
79
+ search({ id: "s2", label: "Another" }),
80
+ ]}
81
+ onRename={onRename}
82
+ onRemove={() => {}}
83
+ />
84
+ );
85
+ fireEvent.click(screen.getByRole("button", { name: /rename another/i }));
86
+ const input = screen.getByRole("textbox", { name: /rename/i });
87
+ fireEvent.change(input, { target: { value: "blocked TASKS" } });
88
+ fireEvent.blur(input);
89
+ expect(onRename).not.toHaveBeenCalled();
90
+ expect(screen.getByText(/already exists/i)).toBeInTheDocument();
91
+ });
92
+
93
+ it("rejects label longer than 120 chars", () => {
94
+ const onRename = vi.fn();
95
+ render(
96
+ <SavedSearchesManager
97
+ open
98
+ onOpenChange={() => {}}
99
+ searches={[search()]}
100
+ onRename={onRename}
101
+ onRemove={() => {}}
102
+ />
103
+ );
104
+ fireEvent.click(screen.getByRole("button", { name: /rename blocked tasks/i }));
105
+ const input = screen.getByRole("textbox", { name: /rename/i });
106
+ fireEvent.change(input, { target: { value: "x".repeat(121) } });
107
+ fireEvent.blur(input);
108
+ expect(onRename).not.toHaveBeenCalled();
109
+ expect(screen.getByText(/too long/i)).toBeInTheDocument();
110
+ });
111
+
112
+ it("Escape cancels rename without persisting", () => {
113
+ const onRename = vi.fn();
114
+ render(
115
+ <SavedSearchesManager
116
+ open
117
+ onOpenChange={() => {}}
118
+ searches={[search()]}
119
+ onRename={onRename}
120
+ onRemove={() => {}}
121
+ />
122
+ );
123
+ fireEvent.click(screen.getByRole("button", { name: /rename blocked tasks/i }));
124
+ const input = screen.getByRole("textbox", { name: /rename/i });
125
+ fireEvent.change(input, { target: { value: "Changed" } });
126
+ fireEvent.keyDown(input, { key: "Escape" });
127
+ expect(onRename).not.toHaveBeenCalled();
128
+ expect(screen.queryByRole("textbox", { name: /rename/i })).toBeNull();
129
+ });
130
+
131
+ it("delete requires explicit confirm", () => {
132
+ const onRemove = vi.fn();
133
+ render(
134
+ <SavedSearchesManager
135
+ open
136
+ onOpenChange={() => {}}
137
+ searches={[search()]}
138
+ onRename={() => {}}
139
+ onRemove={onRemove}
140
+ />
141
+ );
142
+ fireEvent.click(screen.getByRole("button", { name: /delete blocked tasks/i }));
143
+ expect(onRemove).not.toHaveBeenCalled();
144
+ fireEvent.click(screen.getByRole("button", { name: /confirm delete/i }));
145
+ expect(onRemove).toHaveBeenCalledWith("s1");
146
+ });
147
+ });
@@ -21,7 +21,6 @@ import {
21
21
  MessageCircle,
22
22
  Table2,
23
23
  BarChart3,
24
- Store,
25
24
  ChevronDown,
26
25
  } from "lucide-react";
27
26
  import { cn } from "@/lib/utils";
@@ -43,6 +42,7 @@ import {
43
42
  import { ThemeToggle } from "@/components/shared/theme-toggle";
44
43
  import { TrustTierBadge } from "@/components/shared/trust-tier-badge";
45
44
  import { UnreadBadge } from "@/components/notifications/unread-badge";
45
+ import { UpgradeBadge } from "@/components/instance/upgrade-badge";
46
46
  import { AuthStatusDot } from "@/components/settings/auth-status-dot";
47
47
  import { StagentLogo } from "@/components/shared/stagent-logo";
48
48
  import { WorkspaceIndicator } from "@/components/shared/workspace-indicator";
@@ -65,7 +65,6 @@ const workItems: NavItem[] = [
65
65
  { title: "Workflows", href: "/workflows", icon: Workflow },
66
66
  { title: "Documents", href: "/documents", icon: FileText },
67
67
  { title: "Tables", href: "/tables", icon: Table2, alsoMatches: ["/tables/"] },
68
- { title: "Marketplace", href: "/marketplace", icon: Store },
69
68
  ];
70
69
 
71
70
  const manageItems: NavItem[] = [
@@ -199,7 +198,6 @@ function NavGroup({
199
198
 
200
199
  export function AppSidebar() {
201
200
  const pathname = usePathname();
202
-
203
201
  // Determine which group owns the current route
204
202
  const activeGroup = useMemo(() => {
205
203
  for (const group of groupMap) {
@@ -248,6 +246,9 @@ export function AppSidebar() {
248
246
  ))}
249
247
  </SidebarContent>
250
248
  <SidebarFooter className="px-4 py-3">
249
+ <div className="group-data-[collapsible=icon]:hidden mb-2 empty:hidden">
250
+ <UpgradeBadge />
251
+ </div>
251
252
  <div className="group-data-[collapsible=icon]:hidden mb-2">
252
253
  <WorkspaceIndicator variant="sidebar" />
253
254
  </div>
@@ -21,8 +21,29 @@ import {
21
21
  CheckCheck,
22
22
  Loader2,
23
23
  BookOpen,
24
+ Sparkles,
25
+ FileCode,
26
+ Bookmark,
27
+ Trash2,
28
+ Settings2,
24
29
  } from "lucide-react";
25
30
  import { navigationItems, createItems } from "@/lib/chat/command-data";
31
+ import { toggleTheme } from "@/lib/theme";
32
+ import { useProjectSkills } from "@/hooks/use-project-skills";
33
+ import { useSavedSearches, type SavedSearch, type SavedSearchSurface } from "@/hooks/use-saved-searches";
34
+ import { SavedSearchesManager } from "./saved-searches-manager";
35
+ import { toast } from "sonner";
36
+
37
+ // Maps each saved-search surface to its list-page route. Tasks route to
38
+ // /dashboard since /tasks is still a redirect stub.
39
+ const SURFACE_ROUTE: Record<SavedSearchSurface, string> = {
40
+ task: "/dashboard",
41
+ project: "/projects",
42
+ workflow: "/workflows",
43
+ document: "/documents",
44
+ skill: "/skills",
45
+ profile: "/profiles",
46
+ };
26
47
 
27
48
  interface RecentProject {
28
49
  id: string;
@@ -63,8 +84,21 @@ export function CommandPalette() {
63
84
  const [recentTasks, setRecentTasks] = useState<RecentTask[]>([]);
64
85
  const [playbookItems, setPlaybookItems] = useState<PlaybookItem[]>([]);
65
86
  const [loadingRecent, setLoadingRecent] = useState(false);
87
+ const [fileQuery, setFileQuery] = useState("");
88
+ const [fileResults, setFileResults] = useState<Array<{ entityId: string; label: string; description?: string }>>([]);
66
89
  const abortRef = useRef<AbortController | null>(null);
90
+ const fileAbortRef = useRef<AbortController | null>(null);
91
+ const fileDebounceRef = useRef<number | null>(null);
67
92
  const router = useRouter();
93
+ const { skills } = useProjectSkills(null);
94
+ const {
95
+ searches: savedSearches,
96
+ refetch: refetchSavedSearches,
97
+ remove: removeSavedSearch,
98
+ save: saveSavedSearch,
99
+ rename: renameSavedSearch,
100
+ } = useSavedSearches();
101
+ const [managerOpen, setManagerOpen] = useState(false);
68
102
 
69
103
  // Defer render until after hydration to avoid Radix ID mismatch
70
104
  useEffect(() => setMounted(true), []);
@@ -85,6 +119,10 @@ export function CommandPalette() {
85
119
  if (!open) {
86
120
  abortRef.current?.abort();
87
121
  abortRef.current = null;
122
+ fileAbortRef.current?.abort();
123
+ if (fileDebounceRef.current) window.clearTimeout(fileDebounceRef.current);
124
+ setFileQuery("");
125
+ setFileResults([]);
88
126
  return;
89
127
  }
90
128
 
@@ -107,6 +145,33 @@ export function CommandPalette() {
107
145
  .finally(() => setLoadingRecent(false));
108
146
  }, [open]);
109
147
 
148
+ function handleInputChange(value: string) {
149
+ setFileQuery(value);
150
+ if (fileDebounceRef.current) {
151
+ window.clearTimeout(fileDebounceRef.current);
152
+ }
153
+ fileAbortRef.current?.abort();
154
+ if (!value || value.length < 2) {
155
+ setFileResults([]);
156
+ return;
157
+ }
158
+ fileDebounceRef.current = window.setTimeout(() => {
159
+ const controller = new AbortController();
160
+ fileAbortRef.current = controller;
161
+ const params = new URLSearchParams({ q: value, limit: "8" });
162
+ fetch(`/api/chat/files/search?${params}`, { signal: controller.signal })
163
+ .then((r) => (r.ok ? r.json() : null))
164
+ .then((data) => {
165
+ if (Array.isArray(data)) setFileResults(data);
166
+ else if (Array.isArray(data?.results)) setFileResults(data.results);
167
+ else setFileResults([]);
168
+ })
169
+ .catch(() => {
170
+ // aborted or failed — ignore
171
+ });
172
+ }, 200);
173
+ }
174
+
110
175
  const navigate = useCallback(
111
176
  (href: string) => {
112
177
  setOpen(false);
@@ -115,11 +180,27 @@ export function CommandPalette() {
115
180
  [router]
116
181
  );
117
182
 
118
- function toggleTheme() {
183
+ function handleToggleTheme() {
184
+ setOpen(false);
185
+ toggleTheme();
186
+ }
187
+
188
+ function handleSelectSkill(id: string, name: string) {
119
189
  setOpen(false);
120
- const isDark = document.documentElement.classList.contains("dark");
121
- document.documentElement.classList.toggle("dark");
122
- localStorage.setItem("stagent-theme", isDark ? "light" : "dark");
190
+ window.dispatchEvent(
191
+ new CustomEvent("stagent.chat.activate-skill", { detail: { id } })
192
+ );
193
+ toast.info(`Skill "${name}" — activation coming soon`);
194
+ }
195
+
196
+ function handleSelectFile(entityId: string, label: string) {
197
+ setOpen(false);
198
+ window.dispatchEvent(
199
+ new CustomEvent("stagent.chat.insert-mention", {
200
+ detail: { type: "file", path: entityId, label },
201
+ })
202
+ );
203
+ toast.info(`File "${label}" — mention insert coming soon`);
123
204
  }
124
205
 
125
206
  async function markAllRead() {
@@ -130,11 +211,55 @@ export function CommandPalette() {
130
211
 
131
212
  const hasRecent = recentProjects.length > 0 || recentTasks.length > 0;
132
213
 
214
+ const handleDeleteSavedSearch = useCallback(
215
+ (s: SavedSearch) => {
216
+ // Optimistic remove + toast with Undo. The closure holds the full
217
+ // record so undo restores id/createdAt verbatim (not just label).
218
+ removeSavedSearch(s.id);
219
+ toast("Saved search deleted", {
220
+ duration: 5000,
221
+ action: {
222
+ label: "Undo",
223
+ onClick: () => {
224
+ // `save` generates a new id — we need to restore the original.
225
+ // The cheapest restoration is to re-save and then immediately
226
+ // patch the id via a rename-adjacent path. Since the hook has
227
+ // no "insert with id" method, we accept id churn on undo: the
228
+ // label/filterInput/surface are preserved, which is what the
229
+ // user sees. Acceptance criterion: the row reappears with its
230
+ // label and filter, the actual id is an implementation detail.
231
+ saveSavedSearch({
232
+ surface: s.surface,
233
+ label: s.label,
234
+ filterInput: s.filterInput,
235
+ });
236
+ },
237
+ },
238
+ });
239
+ },
240
+ [removeSavedSearch, saveSavedSearch]
241
+ );
242
+
133
243
  if (!mounted) return null;
134
244
 
135
245
  return (
136
- <CommandDialog open={open} onOpenChange={setOpen}>
137
- <CommandInput placeholder="Type a command or search..." />
246
+ <>
247
+ <CommandDialog
248
+ open={open}
249
+ onOpenChange={(next) => {
250
+ // Revalidate saved searches on every open. Each useSavedSearches
251
+ // consumer holds its own state, so a save in the chat popover
252
+ // wouldn't otherwise appear here until page reload.
253
+ // See features/saved-search-polish-v1.md.
254
+ if (next && !open) void refetchSavedSearches();
255
+ setOpen(next);
256
+ }}
257
+ >
258
+ <CommandInput
259
+ placeholder="Type a command or search..."
260
+ value={fileQuery}
261
+ onValueChange={handleInputChange}
262
+ />
138
263
  <CommandList>
139
264
  <CommandEmpty>No results found.</CommandEmpty>
140
265
 
@@ -183,6 +308,63 @@ export function CommandPalette() {
183
308
 
184
309
  {hasRecent && <CommandSeparator />}
185
310
 
311
+ {/* Saved searches */}
312
+ {savedSearches.length > 0 && (
313
+ <>
314
+ <CommandGroup heading="Saved searches">
315
+ {savedSearches.map((s) => (
316
+ <CommandItem
317
+ key={`saved-${s.id}`}
318
+ value={`saved ${s.label} ${s.filterInput} ${s.surface}`}
319
+ onSelect={() => {
320
+ const base = SURFACE_ROUTE[s.surface];
321
+ navigate(`${base}?filter=${encodeURIComponent(s.filterInput)}`);
322
+ }}
323
+ keywords={["saved", "search", s.surface]}
324
+ className="group/item"
325
+ onKeyDown={(e) => {
326
+ // ⌘⌫ on focused row deletes with undo
327
+ if ((e.metaKey || e.ctrlKey) && e.key === "Backspace") {
328
+ e.preventDefault();
329
+ e.stopPropagation();
330
+ handleDeleteSavedSearch(s);
331
+ }
332
+ }}
333
+ >
334
+ <Bookmark className="h-4 w-4" />
335
+ <span className="flex-1 truncate">{s.label}</span>
336
+ <span className="text-xs text-muted-foreground font-mono">{s.filterInput}</span>
337
+ <span className="ml-2 text-xs text-muted-foreground">{s.surface}</span>
338
+ <button
339
+ type="button"
340
+ aria-label={`Delete saved search: ${s.label}`}
341
+ className="ml-1 p-1 rounded hover:bg-destructive/10 text-muted-foreground hover:text-destructive opacity-0 group-hover/item:opacity-100 focus-visible:opacity-100 transition-opacity"
342
+ onPointerDown={(e) => e.stopPropagation()}
343
+ onClick={(e) => {
344
+ e.preventDefault();
345
+ e.stopPropagation();
346
+ handleDeleteSavedSearch(s);
347
+ }}
348
+ >
349
+ <Trash2 className="h-3.5 w-3.5" />
350
+ </button>
351
+ </CommandItem>
352
+ ))}
353
+ <CommandItem
354
+ value="manage-saved-searches"
355
+ keywords={["manage", "saved", "rename", "delete"]}
356
+ onSelect={() => {
357
+ setManagerOpen(true);
358
+ }}
359
+ >
360
+ <Settings2 className="h-4 w-4" />
361
+ <span className="flex-1">Manage saved searches…</span>
362
+ </CommandItem>
363
+ </CommandGroup>
364
+ <CommandSeparator />
365
+ </>
366
+ )}
367
+
186
368
  {/* Navigation */}
187
369
  <CommandGroup heading="Navigation">
188
370
  {navigationItems.map((item) => (
@@ -237,9 +419,78 @@ export function CommandPalette() {
237
419
 
238
420
  <CommandSeparator />
239
421
 
422
+ {/* Templates */}
423
+ <CommandGroup heading="Templates">
424
+ <CommandItem
425
+ value="start-from-template"
426
+ keywords={["template", "blueprint", "new", "conversation", "chat"]}
427
+ onSelect={() => {
428
+ setOpen(false);
429
+ // Ensure chat-shell is mounted so its event listener is live.
430
+ // When already on /chat, next-tick dispatch is a no-op nav.
431
+ router.push("/chat");
432
+ window.setTimeout(() => {
433
+ window.dispatchEvent(
434
+ new CustomEvent("stagent.chat.openTemplatePicker")
435
+ );
436
+ }, 50);
437
+ }}
438
+ >
439
+ <Sparkles className="h-4 w-4" />
440
+ Start conversation from template…
441
+ </CommandItem>
442
+ </CommandGroup>
443
+
444
+ <CommandSeparator />
445
+
446
+ {/* Skills */}
447
+ {skills.length > 0 && (
448
+ <>
449
+ <CommandGroup heading="Skills">
450
+ {skills.map((skill) => (
451
+ <CommandItem
452
+ key={`skill-${skill.id}`}
453
+ value={`skill-${skill.name}`}
454
+ onSelect={() => handleSelectSkill(skill.id, skill.name)}
455
+ keywords={["skill", "profile"]}
456
+ >
457
+ <Sparkles className="h-4 w-4" />
458
+ <span className="flex-1 truncate">{skill.name}</span>
459
+ {skill.description && (
460
+ <span className="text-xs text-muted-foreground truncate max-w-[40%]">
461
+ {skill.description}
462
+ </span>
463
+ )}
464
+ </CommandItem>
465
+ ))}
466
+ </CommandGroup>
467
+ <CommandSeparator />
468
+ </>
469
+ )}
470
+
471
+ {/* Files */}
472
+ {fileResults.length > 0 && (
473
+ <>
474
+ <CommandGroup heading="Files">
475
+ {fileResults.map((file) => (
476
+ <CommandItem
477
+ key={`file-${file.entityId}`}
478
+ value={`file-${file.label}`}
479
+ onSelect={() => handleSelectFile(file.entityId, file.label)}
480
+ keywords={["file", "path"]}
481
+ >
482
+ <FileCode className="h-4 w-4" />
483
+ <span className="flex-1 truncate font-mono text-xs">{file.label}</span>
484
+ </CommandItem>
485
+ ))}
486
+ </CommandGroup>
487
+ <CommandSeparator />
488
+ </>
489
+ )}
490
+
240
491
  {/* Utility */}
241
492
  <CommandGroup heading="Utility">
242
- <CommandItem onSelect={toggleTheme} value="Toggle Theme" keywords={["dark", "light", "mode"]}>
493
+ <CommandItem onSelect={handleToggleTheme} value="Toggle Theme" keywords={["dark", "light", "mode"]}>
243
494
  <Sun className="h-4 w-4 dark:hidden" />
244
495
  <Moon className="h-4 w-4 hidden dark:block" />
245
496
  Toggle Theme
@@ -253,5 +504,13 @@ export function CommandPalette() {
253
504
  </CommandGroup>
254
505
  </CommandList>
255
506
  </CommandDialog>
507
+ <SavedSearchesManager
508
+ open={managerOpen}
509
+ onOpenChange={setManagerOpen}
510
+ searches={savedSearches}
511
+ onRename={renameSavedSearch}
512
+ onRemove={removeSavedSearch}
513
+ />
514
+ </>
256
515
  );
257
516
  }
@@ -0,0 +1,70 @@
1
+ "use client";
2
+
3
+ import { useEffect, useMemo, useState } from "react";
4
+ import { Lightbulb } from "lucide-react";
5
+ import { parseFilterInput } from "@/lib/filters/parse";
6
+
7
+ interface FilterHintProps {
8
+ inputValue: string;
9
+ storageKey: string;
10
+ /** Optional copy override; defaults to the #key:value tip. */
11
+ message?: string;
12
+ }
13
+
14
+ /**
15
+ * FilterHint — passive discovery row for the `#key:value` filter syntax.
16
+ *
17
+ * Visibility rules:
18
+ * - Hidden once the dismissal flag is set in localStorage.
19
+ * - Hidden when the input contains `#` (user has discovered the syntax).
20
+ * - The flag is set the first time parseFilterInput returns ≥1 clause.
21
+ *
22
+ * Consumers: chat-command-popover, filter-input (list pages).
23
+ */
24
+ export function FilterHint({ inputValue, storageKey, message }: FilterHintProps) {
25
+ const [dismissed, setDismissed] = useState(false);
26
+
27
+ const parsed = useMemo(() => parseFilterInput(inputValue), [inputValue]);
28
+
29
+ useEffect(() => {
30
+ try {
31
+ if (window.localStorage.getItem(storageKey) === "1") {
32
+ setDismissed(true);
33
+ }
34
+ } catch {
35
+ // Private-mode or disabled storage — hint stays visible.
36
+ }
37
+ }, [storageKey]);
38
+
39
+ useEffect(() => {
40
+ if (dismissed) return;
41
+ if (parsed.clauses.length > 0) {
42
+ try {
43
+ window.localStorage.setItem(storageKey, "1");
44
+ } catch {
45
+ // Private-mode or disabled storage — hint stays visible, no-op.
46
+ }
47
+ setDismissed(true);
48
+ }
49
+ }, [parsed.clauses.length, dismissed, storageKey]);
50
+
51
+ if (dismissed) return null;
52
+ if (inputValue.includes("#")) return null;
53
+
54
+ return (
55
+ <div
56
+ role="note"
57
+ className="flex items-center gap-2 px-3 py-1.5 text-xs text-muted-foreground border-t border-border/50"
58
+ >
59
+ <Lightbulb className="h-3 w-3 shrink-0" aria-hidden />
60
+ <span>
61
+ {message ?? (
62
+ <>
63
+ Tip: use <code className="font-mono text-foreground">#key:value</code> to filter (e.g.{" "}
64
+ <code className="font-mono text-foreground">#status:blocked</code>)
65
+ </>
66
+ )}
67
+ </span>
68
+ </div>
69
+ );
70
+ }