stagent 0.10.0 → 0.11.1

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 (176) hide show
  1. package/README.md +44 -31
  2. package/dist/cli.js +24 -0
  3. package/docs/.coverage-gaps.json +154 -24
  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 +16 -2
  12. package/docs/features/settings.md +2 -2
  13. package/docs/features/shared-components.md +7 -3
  14. package/docs/features/tables.md +3 -1
  15. package/docs/features/tool-permissions.md +6 -2
  16. package/docs/features/workflows.md +6 -2
  17. package/docs/getting-started.md +1 -1
  18. package/docs/index.md +1 -1
  19. package/docs/journeys/developer.md +25 -2
  20. package/docs/journeys/personal-use.md +12 -5
  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 +2 -2
  25. package/docs/superpowers/plans/2026-04-14-chat-command-namespace-refactor.md +1390 -0
  26. package/docs/superpowers/plans/2026-04-14-chat-environment-integration.md +1561 -0
  27. package/docs/superpowers/plans/2026-04-14-chat-polish-bundle-v1.md +1219 -0
  28. package/docs/superpowers/plans/2026-04-14-chat-session-persistence-provider-closeout.md +399 -0
  29. package/next.config.mjs +1 -0
  30. package/package.json +3 -3
  31. package/src/app/api/chat/conversations/[id]/skills/__tests__/activate.test.ts +141 -0
  32. package/src/app/api/chat/conversations/[id]/skills/activate/route.ts +74 -0
  33. package/src/app/api/chat/conversations/[id]/skills/deactivate/route.ts +33 -0
  34. package/src/app/api/chat/export/route.ts +52 -0
  35. package/src/app/api/chat/files/search/route.ts +50 -0
  36. package/src/app/api/environment/rescan-if-stale/__tests__/route.test.ts +45 -0
  37. package/src/app/api/environment/rescan-if-stale/route.ts +23 -0
  38. package/src/app/api/environment/skills/route.ts +13 -0
  39. package/src/app/api/schedules/[id]/execute/route.ts +2 -2
  40. package/src/app/api/settings/chat/pins/route.ts +94 -0
  41. package/src/app/api/settings/chat/saved-searches/__tests__/route.test.ts +119 -0
  42. package/src/app/api/settings/chat/saved-searches/route.ts +79 -0
  43. package/src/app/api/settings/environment/route.ts +26 -0
  44. package/src/app/api/tasks/[id]/execute/route.ts +52 -12
  45. package/src/app/api/tasks/[id]/respond/route.ts +31 -15
  46. package/src/app/api/tasks/[id]/resume/route.ts +24 -3
  47. package/src/app/documents/page.tsx +4 -1
  48. package/src/app/settings/page.tsx +2 -0
  49. package/src/components/book/content-blocks.tsx +1 -1
  50. package/src/components/chat/__tests__/capability-banner.test.tsx +38 -0
  51. package/src/components/chat/__tests__/chat-session-provider.test.tsx +166 -1
  52. package/src/components/chat/__tests__/skill-row.test.tsx +91 -0
  53. package/src/components/chat/capability-banner.tsx +68 -0
  54. package/src/components/chat/chat-command-popover.tsx +668 -47
  55. package/src/components/chat/chat-input.tsx +103 -8
  56. package/src/components/chat/chat-message.tsx +12 -3
  57. package/src/components/chat/chat-session-provider.tsx +73 -3
  58. package/src/components/chat/chat-shell.tsx +62 -3
  59. package/src/components/chat/command-tab-bar.tsx +68 -0
  60. package/src/components/chat/conversation-template-picker.tsx +421 -0
  61. package/src/components/chat/help-dialog.tsx +39 -0
  62. package/src/components/chat/skill-composition-conflict-dialog.tsx +96 -0
  63. package/src/components/chat/skill-row.tsx +147 -0
  64. package/src/components/documents/document-browser.tsx +37 -19
  65. package/src/components/notifications/__tests__/permission-response-actions.test.tsx +70 -0
  66. package/src/components/notifications/permission-response-actions.tsx +155 -1
  67. package/src/components/playbook/playbook-detail-view.tsx +1 -1
  68. package/src/components/settings/environment-section.tsx +102 -0
  69. package/src/components/shared/__tests__/filter-hint.test.tsx +40 -0
  70. package/src/components/shared/__tests__/saved-searches-manager.test.tsx +147 -0
  71. package/src/components/shared/command-palette.tsx +262 -2
  72. package/src/components/shared/filter-hint.tsx +70 -0
  73. package/src/components/shared/filter-input.tsx +59 -0
  74. package/src/components/shared/saved-searches-manager.tsx +199 -0
  75. package/src/components/tasks/task-bento-grid.tsx +12 -2
  76. package/src/components/tasks/task-card.tsx +3 -0
  77. package/src/components/tasks/task-chip-bar.tsx +30 -1
  78. package/src/hooks/__tests__/use-chat-autocomplete-tabs.test.ts +47 -0
  79. package/src/hooks/__tests__/use-saved-searches.test.ts +70 -0
  80. package/src/hooks/use-active-skills.ts +110 -0
  81. package/src/hooks/use-chat-autocomplete.ts +120 -7
  82. package/src/hooks/use-enriched-skills.ts +19 -0
  83. package/src/hooks/use-pinned-entries.ts +104 -0
  84. package/src/hooks/use-recent-user-messages.ts +19 -0
  85. package/src/hooks/use-saved-searches.ts +142 -0
  86. package/src/lib/agents/__tests__/claude-agent-sdk-options.test.ts +56 -0
  87. package/src/lib/agents/__tests__/claude-agent.test.ts +17 -4
  88. package/src/lib/agents/__tests__/task-dispatch.test.ts +166 -0
  89. package/src/lib/agents/__tests__/tool-permissions.test.ts +60 -0
  90. package/src/lib/agents/claude-agent.ts +105 -46
  91. package/src/lib/agents/handoff/bus.ts +2 -2
  92. package/src/lib/agents/profiles/__tests__/list-fused-profiles.test.ts +110 -0
  93. package/src/lib/agents/profiles/__tests__/registry.test.ts +47 -0
  94. package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +30 -3
  95. package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +6 -2
  96. package/src/lib/agents/profiles/list-fused-profiles.ts +104 -0
  97. package/src/lib/agents/profiles/registry.ts +97 -22
  98. package/src/lib/agents/profiles/types.ts +7 -1
  99. package/src/lib/agents/router.ts +3 -6
  100. package/src/lib/agents/runtime/__tests__/catalog.test.ts +130 -0
  101. package/src/lib/agents/runtime/__tests__/execution-target.test.ts +183 -0
  102. package/src/lib/agents/runtime/anthropic-direct.ts +8 -0
  103. package/src/lib/agents/runtime/catalog.ts +121 -0
  104. package/src/lib/agents/runtime/claude-sdk.ts +32 -0
  105. package/src/lib/agents/runtime/execution-target.ts +456 -0
  106. package/src/lib/agents/runtime/index.ts +4 -0
  107. package/src/lib/agents/runtime/launch-failure.ts +101 -0
  108. package/src/lib/agents/runtime/openai-codex.ts +35 -0
  109. package/src/lib/agents/runtime/openai-direct.ts +8 -0
  110. package/src/lib/agents/task-dispatch.ts +220 -0
  111. package/src/lib/agents/tool-permissions.ts +16 -1
  112. package/src/lib/chat/__tests__/active-skill-injection.test.ts +261 -0
  113. package/src/lib/chat/__tests__/clean-filter-input.test.ts +68 -0
  114. package/src/lib/chat/__tests__/command-tabs.test.ts +68 -0
  115. package/src/lib/chat/__tests__/context-builder-files.test.ts +112 -0
  116. package/src/lib/chat/__tests__/dismissals.test.ts +65 -0
  117. package/src/lib/chat/__tests__/engine-sdk-options.test.ts +117 -0
  118. package/src/lib/chat/__tests__/skill-conflict.test.ts +35 -0
  119. package/src/lib/chat/__tests__/types.test.ts +28 -0
  120. package/src/lib/chat/active-skills.ts +31 -0
  121. package/src/lib/chat/clean-filter-input.ts +30 -0
  122. package/src/lib/chat/codex-engine.ts +30 -7
  123. package/src/lib/chat/command-tabs.ts +61 -0
  124. package/src/lib/chat/context-builder.ts +141 -1
  125. package/src/lib/chat/dismissals.ts +73 -0
  126. package/src/lib/chat/engine.ts +109 -15
  127. package/src/lib/chat/files/__tests__/search.test.ts +135 -0
  128. package/src/lib/chat/files/expand-mention.ts +76 -0
  129. package/src/lib/chat/files/search.ts +99 -0
  130. package/src/lib/chat/skill-composition.ts +210 -0
  131. package/src/lib/chat/skill-conflict.ts +105 -0
  132. package/src/lib/chat/stagent-tools.ts +6 -19
  133. package/src/lib/chat/stream-telemetry.ts +9 -4
  134. package/src/lib/chat/system-prompt.ts +22 -0
  135. package/src/lib/chat/tool-catalog.ts +33 -3
  136. package/src/lib/chat/tools/__tests__/profile-tools.test.ts +51 -0
  137. package/src/lib/chat/tools/__tests__/settings-tools.test.ts +294 -0
  138. package/src/lib/chat/tools/__tests__/skill-tools.test.ts +474 -0
  139. package/src/lib/chat/tools/__tests__/task-tools.test.ts +47 -0
  140. package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +134 -0
  141. package/src/lib/chat/tools/blueprint-tools.ts +190 -0
  142. package/src/lib/chat/tools/helpers.ts +2 -0
  143. package/src/lib/chat/tools/profile-tools.ts +120 -23
  144. package/src/lib/chat/tools/skill-tools.ts +183 -0
  145. package/src/lib/chat/tools/task-tools.ts +6 -2
  146. package/src/lib/chat/tools/workflow-tools.ts +61 -20
  147. package/src/lib/chat/types.ts +15 -0
  148. package/src/lib/constants/settings.ts +2 -0
  149. package/src/lib/data/clear.ts +2 -6
  150. package/src/lib/db/bootstrap.ts +17 -0
  151. package/src/lib/db/schema.ts +26 -0
  152. package/src/lib/environment/__tests__/auto-promote.test.ts +132 -0
  153. package/src/lib/environment/__tests__/list-skills-enriched.test.ts +55 -0
  154. package/src/lib/environment/__tests__/skill-enrichment.test.ts +129 -0
  155. package/src/lib/environment/__tests__/skill-recommendations.test.ts +87 -0
  156. package/src/lib/environment/data.ts +9 -0
  157. package/src/lib/environment/list-skills.ts +176 -0
  158. package/src/lib/environment/parsers/__tests__/skill.test.ts +54 -0
  159. package/src/lib/environment/parsers/skill.ts +26 -5
  160. package/src/lib/environment/profile-generator.ts +56 -2
  161. package/src/lib/environment/skill-enrichment.ts +106 -0
  162. package/src/lib/environment/skill-recommendations.ts +66 -0
  163. package/src/lib/filters/__tests__/parse.quoted.test.ts +40 -0
  164. package/src/lib/filters/__tests__/parse.test.ts +135 -0
  165. package/src/lib/filters/parse.ts +86 -0
  166. package/src/lib/instance/__tests__/detect.test.ts +1 -1
  167. package/src/lib/instance/__tests__/upgrade-poller.test.ts +50 -0
  168. package/src/lib/instance/fingerprint.ts +8 -10
  169. package/src/lib/instance/upgrade-poller.ts +53 -1
  170. package/src/lib/schedules/scheduler.ts +4 -4
  171. package/src/lib/utils/stagent-paths.ts +4 -0
  172. package/src/lib/workflows/blueprints/__tests__/render-prompt.test.ts +124 -0
  173. package/src/lib/workflows/blueprints/render-prompt.ts +71 -0
  174. package/src/lib/workflows/blueprints/types.ts +6 -0
  175. package/src/lib/workflows/engine.ts +5 -3
  176. package/src/test/setup.ts +10 -0
@@ -21,9 +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";
26
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
+ };
27
47
 
28
48
  interface RecentProject {
29
49
  id: string;
@@ -64,8 +84,21 @@ export function CommandPalette() {
64
84
  const [recentTasks, setRecentTasks] = useState<RecentTask[]>([]);
65
85
  const [playbookItems, setPlaybookItems] = useState<PlaybookItem[]>([]);
66
86
  const [loadingRecent, setLoadingRecent] = useState(false);
87
+ const [fileQuery, setFileQuery] = useState("");
88
+ const [fileResults, setFileResults] = useState<Array<{ entityId: string; label: string; description?: string }>>([]);
67
89
  const abortRef = useRef<AbortController | null>(null);
90
+ const fileAbortRef = useRef<AbortController | null>(null);
91
+ const fileDebounceRef = useRef<number | null>(null);
68
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);
69
102
 
70
103
  // Defer render until after hydration to avoid Radix ID mismatch
71
104
  useEffect(() => setMounted(true), []);
@@ -86,6 +119,10 @@ export function CommandPalette() {
86
119
  if (!open) {
87
120
  abortRef.current?.abort();
88
121
  abortRef.current = null;
122
+ fileAbortRef.current?.abort();
123
+ if (fileDebounceRef.current) window.clearTimeout(fileDebounceRef.current);
124
+ setFileQuery("");
125
+ setFileResults([]);
89
126
  return;
90
127
  }
91
128
 
@@ -108,6 +145,33 @@ export function CommandPalette() {
108
145
  .finally(() => setLoadingRecent(false));
109
146
  }, [open]);
110
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
+
111
175
  const navigate = useCallback(
112
176
  (href: string) => {
113
177
  setOpen(false);
@@ -121,6 +185,24 @@ export function CommandPalette() {
121
185
  toggleTheme();
122
186
  }
123
187
 
188
+ function handleSelectSkill(id: string, name: string) {
189
+ setOpen(false);
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`);
204
+ }
205
+
124
206
  async function markAllRead() {
125
207
  setOpen(false);
126
208
  await fetch("/api/notifications/mark-all-read", { method: "PATCH" });
@@ -129,11 +211,55 @@ export function CommandPalette() {
129
211
 
130
212
  const hasRecent = recentProjects.length > 0 || recentTasks.length > 0;
131
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
+
132
243
  if (!mounted) return null;
133
244
 
134
245
  return (
135
- <CommandDialog open={open} onOpenChange={setOpen}>
136
- <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
+ />
137
263
  <CommandList>
138
264
  <CommandEmpty>No results found.</CommandEmpty>
139
265
 
@@ -182,6 +308,63 @@ export function CommandPalette() {
182
308
 
183
309
  {hasRecent && <CommandSeparator />}
184
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
+
185
368
  {/* Navigation */}
186
369
  <CommandGroup heading="Navigation">
187
370
  {navigationItems.map((item) => (
@@ -236,6 +419,75 @@ export function CommandPalette() {
236
419
 
237
420
  <CommandSeparator />
238
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
+
239
491
  {/* Utility */}
240
492
  <CommandGroup heading="Utility">
241
493
  <CommandItem onSelect={handleToggleTheme} value="Toggle Theme" keywords={["dark", "light", "mode"]}>
@@ -252,5 +504,13 @@ export function CommandPalette() {
252
504
  </CommandGroup>
253
505
  </CommandList>
254
506
  </CommandDialog>
507
+ <SavedSearchesManager
508
+ open={managerOpen}
509
+ onOpenChange={setManagerOpen}
510
+ searches={savedSearches}
511
+ onRename={renameSavedSearch}
512
+ onRemove={removeSavedSearch}
513
+ />
514
+ </>
255
515
  );
256
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
+ }
@@ -0,0 +1,59 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { Hash } from "lucide-react";
5
+ import { Input } from "@/components/ui/input";
6
+ import { Badge } from "@/components/ui/badge";
7
+ import { parseFilterInput, type FilterClause } from "@/lib/filters/parse";
8
+ import { FilterHint } from "./filter-hint";
9
+
10
+ interface FilterInputProps {
11
+ value: string;
12
+ onChange: (next: { raw: string; clauses: FilterClause[]; rawQuery: string }) => void;
13
+ placeholder?: string;
14
+ }
15
+
16
+ /**
17
+ * FilterInput — free-text input that recognizes `#key:value` filter syntax.
18
+ *
19
+ * Renders parsed clauses as outline badges next to the input. Consumer
20
+ * receives the raw string (for URL serialization) and the parsed breakdown
21
+ * (for list filtering). Keeps the existing free-text search behavior — the
22
+ * `rawQuery` is the text with filter clauses stripped.
23
+ */
24
+ export function FilterInput({ value, onChange, placeholder }: FilterInputProps) {
25
+ const [local, setLocal] = useState(value);
26
+
27
+ useEffect(() => {
28
+ setLocal(value);
29
+ }, [value]);
30
+
31
+ const parsed = parseFilterInput(local);
32
+
33
+ return (
34
+ <div className="flex flex-col gap-1 flex-1 min-w-0">
35
+ <div className="flex flex-wrap items-center gap-2">
36
+ <div className="relative flex-1 min-w-[16rem]">
37
+ <Hash className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground pointer-events-none" />
38
+ <Input
39
+ value={local}
40
+ onChange={(e) => {
41
+ const next = e.target.value;
42
+ setLocal(next);
43
+ const p = parseFilterInput(next);
44
+ onChange({ raw: next, clauses: p.clauses, rawQuery: p.rawQuery });
45
+ }}
46
+ placeholder={placeholder ?? "#status:blocked or search…"}
47
+ className="pl-7 h-8"
48
+ />
49
+ </div>
50
+ {parsed.clauses.map((c, i) => (
51
+ <Badge key={`${c.key}-${i}`} variant="outline" className="text-xs font-mono">
52
+ #{c.key}:{c.value}
53
+ </Badge>
54
+ ))}
55
+ </div>
56
+ <FilterHint inputValue={local} storageKey="stagent.filter-hint.dismissed" />
57
+ </div>
58
+ );
59
+ }
@@ -0,0 +1,199 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { Pencil, Trash2, Check, X } from "lucide-react";
5
+ import {
6
+ Dialog,
7
+ DialogContent,
8
+ DialogDescription,
9
+ DialogHeader,
10
+ DialogTitle,
11
+ } from "@/components/ui/dialog";
12
+ import { Input } from "@/components/ui/input";
13
+ import { Button } from "@/components/ui/button";
14
+ import { Badge } from "@/components/ui/badge";
15
+ import type { SavedSearch } from "@/hooks/use-saved-searches";
16
+
17
+ const LABEL_MAX = 120;
18
+
19
+ interface SavedSearchesManagerProps {
20
+ open: boolean;
21
+ onOpenChange: (open: boolean) => void;
22
+ searches: SavedSearch[];
23
+ onRename: (id: string, label: string) => void;
24
+ onRemove: (id: string) => void;
25
+ }
26
+
27
+ /**
28
+ * SavedSearchesManager — dialog for renaming or deleting saved searches.
29
+ *
30
+ * Distinct from the inline palette delete (which is one-click with a 5s
31
+ * undo toast). This dialog is a deliberate management context, so delete
32
+ * requires an explicit "Confirm" click (no undo).
33
+ */
34
+ export function SavedSearchesManager({
35
+ open,
36
+ onOpenChange,
37
+ searches,
38
+ onRename,
39
+ onRemove,
40
+ }: SavedSearchesManagerProps) {
41
+ const [renamingId, setRenamingId] = useState<string | null>(null);
42
+ const [draft, setDraft] = useState<string>("");
43
+ const [error, setError] = useState<string | null>(null);
44
+ const [pendingDeleteId, setPendingDeleteId] = useState<string | null>(null);
45
+
46
+ function startRename(s: SavedSearch) {
47
+ setRenamingId(s.id);
48
+ setDraft(s.label);
49
+ setError(null);
50
+ }
51
+
52
+ function cancelRename() {
53
+ setRenamingId(null);
54
+ setDraft("");
55
+ setError(null);
56
+ }
57
+
58
+ function commitRename(s: SavedSearch) {
59
+ // If renaming was already cancelled (e.g., via Escape) the renamingId
60
+ // no longer matches — blur fires after cancelRename() has set it to null.
61
+ if (renamingId !== s.id) return;
62
+
63
+ const next = draft.trim();
64
+ if (next.length === 0) {
65
+ setError("Label cannot be empty");
66
+ return;
67
+ }
68
+ if (next.length > LABEL_MAX) {
69
+ setError(`Label too long (max ${LABEL_MAX} chars)`);
70
+ return;
71
+ }
72
+ const dupe = searches.find(
73
+ (other) =>
74
+ other.id !== s.id &&
75
+ other.surface === s.surface &&
76
+ other.label.toLowerCase() === next.toLowerCase()
77
+ );
78
+ if (dupe) {
79
+ setError("A saved search with that label already exists for this surface");
80
+ return;
81
+ }
82
+ if (next !== s.label) onRename(s.id, next);
83
+ cancelRename();
84
+ }
85
+
86
+ return (
87
+ <Dialog open={open} onOpenChange={onOpenChange}>
88
+ <DialogContent className="max-w-lg">
89
+ <DialogHeader>
90
+ <DialogTitle>Manage saved searches</DialogTitle>
91
+ <DialogDescription>Rename or delete your saved filter combinations.</DialogDescription>
92
+ </DialogHeader>
93
+ <div className="px-6 pb-6 space-y-2 overflow-y-auto max-h-[60vh]">
94
+ {searches.length === 0 ? (
95
+ <p className="text-sm text-muted-foreground">No saved searches yet.</p>
96
+ ) : (
97
+ searches.map((s) => {
98
+ const isRenaming = renamingId === s.id;
99
+ const isPendingDelete = pendingDeleteId === s.id;
100
+ return (
101
+ <div
102
+ key={s.id}
103
+ className="flex items-center gap-2 rounded-md border border-border/60 px-3 py-2"
104
+ >
105
+ <div className="flex-1 min-w-0">
106
+ {isRenaming ? (
107
+ <div className="space-y-1">
108
+ <Input
109
+ aria-label="Rename"
110
+ autoFocus
111
+ value={draft}
112
+ onChange={(e) => {
113
+ setDraft(e.target.value);
114
+ setError(null);
115
+ }}
116
+ onKeyDown={(e) => {
117
+ if (e.key === "Escape") {
118
+ e.preventDefault();
119
+ cancelRename();
120
+ } else if (e.key === "Enter") {
121
+ e.preventDefault();
122
+ commitRename(s);
123
+ }
124
+ }}
125
+ onBlur={() => commitRename(s)}
126
+ className="h-7"
127
+ />
128
+ {error && (
129
+ <p className="text-xs text-destructive">{error}</p>
130
+ )}
131
+ </div>
132
+ ) : (
133
+ <div className="flex items-center gap-2">
134
+ <span className="truncate text-sm font-medium">{s.label}</span>
135
+ <Badge variant="outline" className="text-[10px] uppercase">
136
+ {s.surface}
137
+ </Badge>
138
+ </div>
139
+ )}
140
+ <p className="truncate text-xs font-mono text-muted-foreground">
141
+ {s.filterInput}
142
+ </p>
143
+ </div>
144
+ {!isRenaming && !isPendingDelete && (
145
+ <>
146
+ <Button
147
+ variant="ghost"
148
+ size="icon"
149
+ className="h-7 w-7"
150
+ aria-label={`Rename ${s.label}`}
151
+ onClick={() => startRename(s)}
152
+ >
153
+ <Pencil className="h-3.5 w-3.5" />
154
+ </Button>
155
+ <Button
156
+ variant="ghost"
157
+ size="icon"
158
+ className="h-7 w-7 text-destructive hover:text-destructive"
159
+ aria-label={`Delete ${s.label}`}
160
+ onClick={() => setPendingDeleteId(s.id)}
161
+ >
162
+ <Trash2 className="h-3.5 w-3.5" />
163
+ </Button>
164
+ </>
165
+ )}
166
+ {isPendingDelete && (
167
+ <div className="flex items-center gap-1">
168
+ <Button
169
+ variant="destructive"
170
+ size="sm"
171
+ className="h-7"
172
+ aria-label={`Confirm delete ${s.label}`}
173
+ onClick={() => {
174
+ onRemove(s.id);
175
+ setPendingDeleteId(null);
176
+ }}
177
+ >
178
+ <Check className="h-3.5 w-3.5" /> Confirm delete
179
+ </Button>
180
+ <Button
181
+ variant="ghost"
182
+ size="sm"
183
+ className="h-7"
184
+ aria-label="Cancel delete"
185
+ onClick={() => setPendingDeleteId(null)}
186
+ >
187
+ <X className="h-3.5 w-3.5" />
188
+ </Button>
189
+ </div>
190
+ )}
191
+ </div>
192
+ );
193
+ })
194
+ )}
195
+ </div>
196
+ </DialogContent>
197
+ </Dialog>
198
+ );
199
+ }
@@ -18,6 +18,7 @@ import { formatCompactDateTime } from "@/lib/utils/format-timestamp";
18
18
  import { TaskBentoCell } from "./task-bento-cell";
19
19
  import type { TaskItem } from "./task-card";
20
20
  import type { DocumentRow } from "@/lib/db/schema";
21
+ import { getRuntimeCatalogEntry } from "@/lib/agents/runtime/catalog";
21
22
 
22
23
  const priorityConfig: Record<number, { icon: typeof ArrowUp; label: string }> = {
23
24
  0: { icon: ArrowUp, label: "P0 Critical" },
@@ -78,6 +79,7 @@ export function TaskBentoGrid({ task, docs }: TaskBentoGridProps) {
78
79
 
79
80
  const inputDocs = docs.filter((d) => d.direction === "input");
80
81
  const outputDocs = docs.filter((d) => d.direction === "output");
82
+ const modelId = task.effectiveModelId ?? usage?.modelId ?? null;
81
83
  const docSummary =
82
84
  inputDocs.length > 0 && outputDocs.length > 0
83
85
  ? `${inputDocs.length} in / ${outputDocs.length} out`
@@ -154,11 +156,19 @@ export function TaskBentoGrid({ task, docs }: TaskBentoGridProps) {
154
156
  />
155
157
  )}
156
158
 
157
- {usage?.modelId && (
159
+ {task.effectiveRuntimeId && (
160
+ <TaskBentoCell
161
+ icon={Cpu}
162
+ label="Runtime Used"
163
+ value={getRuntimeCatalogEntry(task.effectiveRuntimeId as never).label}
164
+ />
165
+ )}
166
+
167
+ {modelId && (
158
168
  <TaskBentoCell
159
169
  icon={Cpu}
160
170
  label="Model"
161
- value={<span className="text-sm font-semibold">{truncateModel(usage.modelId)}</span>}
171
+ value={<span className="text-sm font-semibold">{truncateModel(modelId)}</span>}
162
172
  />
163
173
  )}
164
174