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.
- package/README.md +44 -31
- package/dist/cli.js +24 -0
- package/docs/.coverage-gaps.json +154 -24
- package/docs/.last-generated +1 -1
- package/docs/features/agent-intelligence.md +12 -2
- package/docs/features/chat.md +40 -5
- package/docs/features/cost-usage.md +1 -1
- package/docs/features/documents.md +5 -2
- package/docs/features/inbox-notifications.md +10 -2
- package/docs/features/keyboard-navigation.md +12 -3
- package/docs/features/provider-runtimes.md +16 -2
- package/docs/features/settings.md +2 -2
- package/docs/features/shared-components.md +7 -3
- package/docs/features/tables.md +3 -1
- package/docs/features/tool-permissions.md +6 -2
- package/docs/features/workflows.md +6 -2
- package/docs/getting-started.md +1 -1
- package/docs/index.md +1 -1
- package/docs/journeys/developer.md +25 -2
- package/docs/journeys/personal-use.md +12 -5
- package/docs/journeys/power-user.md +45 -14
- package/docs/journeys/work-use.md +17 -8
- package/docs/manifest.json +15 -15
- package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +2 -2
- package/docs/superpowers/plans/2026-04-14-chat-command-namespace-refactor.md +1390 -0
- package/docs/superpowers/plans/2026-04-14-chat-environment-integration.md +1561 -0
- package/docs/superpowers/plans/2026-04-14-chat-polish-bundle-v1.md +1219 -0
- package/docs/superpowers/plans/2026-04-14-chat-session-persistence-provider-closeout.md +399 -0
- package/next.config.mjs +1 -0
- package/package.json +3 -3
- package/src/app/api/chat/conversations/[id]/skills/__tests__/activate.test.ts +141 -0
- package/src/app/api/chat/conversations/[id]/skills/activate/route.ts +74 -0
- package/src/app/api/chat/conversations/[id]/skills/deactivate/route.ts +33 -0
- package/src/app/api/chat/export/route.ts +52 -0
- package/src/app/api/chat/files/search/route.ts +50 -0
- package/src/app/api/environment/rescan-if-stale/__tests__/route.test.ts +45 -0
- package/src/app/api/environment/rescan-if-stale/route.ts +23 -0
- package/src/app/api/environment/skills/route.ts +13 -0
- package/src/app/api/schedules/[id]/execute/route.ts +2 -2
- package/src/app/api/settings/chat/pins/route.ts +94 -0
- package/src/app/api/settings/chat/saved-searches/__tests__/route.test.ts +119 -0
- package/src/app/api/settings/chat/saved-searches/route.ts +79 -0
- package/src/app/api/settings/environment/route.ts +26 -0
- package/src/app/api/tasks/[id]/execute/route.ts +52 -12
- package/src/app/api/tasks/[id]/respond/route.ts +31 -15
- package/src/app/api/tasks/[id]/resume/route.ts +24 -3
- package/src/app/documents/page.tsx +4 -1
- package/src/app/settings/page.tsx +2 -0
- package/src/components/book/content-blocks.tsx +1 -1
- package/src/components/chat/__tests__/capability-banner.test.tsx +38 -0
- package/src/components/chat/__tests__/chat-session-provider.test.tsx +166 -1
- package/src/components/chat/__tests__/skill-row.test.tsx +91 -0
- package/src/components/chat/capability-banner.tsx +68 -0
- package/src/components/chat/chat-command-popover.tsx +668 -47
- package/src/components/chat/chat-input.tsx +103 -8
- package/src/components/chat/chat-message.tsx +12 -3
- package/src/components/chat/chat-session-provider.tsx +73 -3
- package/src/components/chat/chat-shell.tsx +62 -3
- package/src/components/chat/command-tab-bar.tsx +68 -0
- package/src/components/chat/conversation-template-picker.tsx +421 -0
- package/src/components/chat/help-dialog.tsx +39 -0
- package/src/components/chat/skill-composition-conflict-dialog.tsx +96 -0
- package/src/components/chat/skill-row.tsx +147 -0
- package/src/components/documents/document-browser.tsx +37 -19
- package/src/components/notifications/__tests__/permission-response-actions.test.tsx +70 -0
- package/src/components/notifications/permission-response-actions.tsx +155 -1
- package/src/components/playbook/playbook-detail-view.tsx +1 -1
- package/src/components/settings/environment-section.tsx +102 -0
- package/src/components/shared/__tests__/filter-hint.test.tsx +40 -0
- package/src/components/shared/__tests__/saved-searches-manager.test.tsx +147 -0
- package/src/components/shared/command-palette.tsx +262 -2
- package/src/components/shared/filter-hint.tsx +70 -0
- package/src/components/shared/filter-input.tsx +59 -0
- package/src/components/shared/saved-searches-manager.tsx +199 -0
- package/src/components/tasks/task-bento-grid.tsx +12 -2
- package/src/components/tasks/task-card.tsx +3 -0
- package/src/components/tasks/task-chip-bar.tsx +30 -1
- package/src/hooks/__tests__/use-chat-autocomplete-tabs.test.ts +47 -0
- package/src/hooks/__tests__/use-saved-searches.test.ts +70 -0
- package/src/hooks/use-active-skills.ts +110 -0
- package/src/hooks/use-chat-autocomplete.ts +120 -7
- package/src/hooks/use-enriched-skills.ts +19 -0
- package/src/hooks/use-pinned-entries.ts +104 -0
- package/src/hooks/use-recent-user-messages.ts +19 -0
- package/src/hooks/use-saved-searches.ts +142 -0
- package/src/lib/agents/__tests__/claude-agent-sdk-options.test.ts +56 -0
- package/src/lib/agents/__tests__/claude-agent.test.ts +17 -4
- package/src/lib/agents/__tests__/task-dispatch.test.ts +166 -0
- package/src/lib/agents/__tests__/tool-permissions.test.ts +60 -0
- package/src/lib/agents/claude-agent.ts +105 -46
- package/src/lib/agents/handoff/bus.ts +2 -2
- package/src/lib/agents/profiles/__tests__/list-fused-profiles.test.ts +110 -0
- package/src/lib/agents/profiles/__tests__/registry.test.ts +47 -0
- package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +30 -3
- package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +6 -2
- package/src/lib/agents/profiles/list-fused-profiles.ts +104 -0
- package/src/lib/agents/profiles/registry.ts +97 -22
- package/src/lib/agents/profiles/types.ts +7 -1
- package/src/lib/agents/router.ts +3 -6
- package/src/lib/agents/runtime/__tests__/catalog.test.ts +130 -0
- package/src/lib/agents/runtime/__tests__/execution-target.test.ts +183 -0
- package/src/lib/agents/runtime/anthropic-direct.ts +8 -0
- package/src/lib/agents/runtime/catalog.ts +121 -0
- package/src/lib/agents/runtime/claude-sdk.ts +32 -0
- package/src/lib/agents/runtime/execution-target.ts +456 -0
- package/src/lib/agents/runtime/index.ts +4 -0
- package/src/lib/agents/runtime/launch-failure.ts +101 -0
- package/src/lib/agents/runtime/openai-codex.ts +35 -0
- package/src/lib/agents/runtime/openai-direct.ts +8 -0
- package/src/lib/agents/task-dispatch.ts +220 -0
- package/src/lib/agents/tool-permissions.ts +16 -1
- package/src/lib/chat/__tests__/active-skill-injection.test.ts +261 -0
- package/src/lib/chat/__tests__/clean-filter-input.test.ts +68 -0
- package/src/lib/chat/__tests__/command-tabs.test.ts +68 -0
- package/src/lib/chat/__tests__/context-builder-files.test.ts +112 -0
- package/src/lib/chat/__tests__/dismissals.test.ts +65 -0
- package/src/lib/chat/__tests__/engine-sdk-options.test.ts +117 -0
- package/src/lib/chat/__tests__/skill-conflict.test.ts +35 -0
- package/src/lib/chat/__tests__/types.test.ts +28 -0
- package/src/lib/chat/active-skills.ts +31 -0
- package/src/lib/chat/clean-filter-input.ts +30 -0
- package/src/lib/chat/codex-engine.ts +30 -7
- package/src/lib/chat/command-tabs.ts +61 -0
- package/src/lib/chat/context-builder.ts +141 -1
- package/src/lib/chat/dismissals.ts +73 -0
- package/src/lib/chat/engine.ts +109 -15
- package/src/lib/chat/files/__tests__/search.test.ts +135 -0
- package/src/lib/chat/files/expand-mention.ts +76 -0
- package/src/lib/chat/files/search.ts +99 -0
- package/src/lib/chat/skill-composition.ts +210 -0
- package/src/lib/chat/skill-conflict.ts +105 -0
- package/src/lib/chat/stagent-tools.ts +6 -19
- package/src/lib/chat/stream-telemetry.ts +9 -4
- package/src/lib/chat/system-prompt.ts +22 -0
- package/src/lib/chat/tool-catalog.ts +33 -3
- package/src/lib/chat/tools/__tests__/profile-tools.test.ts +51 -0
- package/src/lib/chat/tools/__tests__/settings-tools.test.ts +294 -0
- package/src/lib/chat/tools/__tests__/skill-tools.test.ts +474 -0
- package/src/lib/chat/tools/__tests__/task-tools.test.ts +47 -0
- package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +134 -0
- package/src/lib/chat/tools/blueprint-tools.ts +190 -0
- package/src/lib/chat/tools/helpers.ts +2 -0
- package/src/lib/chat/tools/profile-tools.ts +120 -23
- package/src/lib/chat/tools/skill-tools.ts +183 -0
- package/src/lib/chat/tools/task-tools.ts +6 -2
- package/src/lib/chat/tools/workflow-tools.ts +61 -20
- package/src/lib/chat/types.ts +15 -0
- package/src/lib/constants/settings.ts +2 -0
- package/src/lib/data/clear.ts +2 -6
- package/src/lib/db/bootstrap.ts +17 -0
- package/src/lib/db/schema.ts +26 -0
- package/src/lib/environment/__tests__/auto-promote.test.ts +132 -0
- package/src/lib/environment/__tests__/list-skills-enriched.test.ts +55 -0
- package/src/lib/environment/__tests__/skill-enrichment.test.ts +129 -0
- package/src/lib/environment/__tests__/skill-recommendations.test.ts +87 -0
- package/src/lib/environment/data.ts +9 -0
- package/src/lib/environment/list-skills.ts +176 -0
- package/src/lib/environment/parsers/__tests__/skill.test.ts +54 -0
- package/src/lib/environment/parsers/skill.ts +26 -5
- package/src/lib/environment/profile-generator.ts +56 -2
- package/src/lib/environment/skill-enrichment.ts +106 -0
- package/src/lib/environment/skill-recommendations.ts +66 -0
- package/src/lib/filters/__tests__/parse.quoted.test.ts +40 -0
- package/src/lib/filters/__tests__/parse.test.ts +135 -0
- package/src/lib/filters/parse.ts +86 -0
- package/src/lib/instance/__tests__/detect.test.ts +1 -1
- package/src/lib/instance/__tests__/upgrade-poller.test.ts +50 -0
- package/src/lib/instance/fingerprint.ts +8 -10
- package/src/lib/instance/upgrade-poller.ts +53 -1
- package/src/lib/schedules/scheduler.ts +4 -4
- package/src/lib/utils/stagent-paths.ts +4 -0
- package/src/lib/workflows/blueprints/__tests__/render-prompt.test.ts +124 -0
- package/src/lib/workflows/blueprints/render-prompt.ts +71 -0
- package/src/lib/workflows/blueprints/types.ts +6 -0
- package/src/lib/workflows/engine.ts +5 -3
- 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
|
-
|
|
136
|
-
|
|
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
|
-
{
|
|
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(
|
|
171
|
+
value={<span className="text-sm font-semibold">{truncateModel(modelId)}</span>}
|
|
162
172
|
/>
|
|
163
173
|
)}
|
|
164
174
|
|