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
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Conversation Template Picker — sliding sheet that lists workflow blueprints
|
|
5
|
+
* and, on selection, creates a new conversation pre-filled with the blueprint's
|
|
6
|
+
* rendered `chatPrompt` (falling back to step 1's `promptTemplate`).
|
|
7
|
+
*
|
|
8
|
+
* Transport: writes the rendered prompt into `sessionStorage` under
|
|
9
|
+
* `chat:prefill:<conversationId>` before navigating. The chat composer reads
|
|
10
|
+
* and removes it on mount. This keeps the conversation schema unchanged (no
|
|
11
|
+
* `drafts` column) and survives the client-side navigation without flicker.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useEffect, useMemo, useState } from "react";
|
|
15
|
+
import {
|
|
16
|
+
Sheet,
|
|
17
|
+
SheetContent,
|
|
18
|
+
SheetHeader,
|
|
19
|
+
SheetTitle,
|
|
20
|
+
SheetDescription,
|
|
21
|
+
} from "@/components/ui/sheet";
|
|
22
|
+
import { Button } from "@/components/ui/button";
|
|
23
|
+
import { Badge } from "@/components/ui/badge";
|
|
24
|
+
import { Input as TextInput } from "@/components/ui/input";
|
|
25
|
+
import { Textarea } from "@/components/ui/textarea";
|
|
26
|
+
import { Label } from "@/components/ui/label";
|
|
27
|
+
import { Switch } from "@/components/ui/switch";
|
|
28
|
+
import {
|
|
29
|
+
Select,
|
|
30
|
+
SelectContent,
|
|
31
|
+
SelectItem,
|
|
32
|
+
SelectTrigger,
|
|
33
|
+
SelectValue,
|
|
34
|
+
} from "@/components/ui/select";
|
|
35
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
36
|
+
import { ArrowLeft, Sparkles } from "lucide-react";
|
|
37
|
+
import type {
|
|
38
|
+
WorkflowBlueprint,
|
|
39
|
+
BlueprintVariable,
|
|
40
|
+
} from "@/lib/workflows/blueprints/types";
|
|
41
|
+
import { renderBlueprintPrompt } from "@/lib/workflows/blueprints/render-prompt";
|
|
42
|
+
import { useChatSession } from "./chat-session-provider";
|
|
43
|
+
|
|
44
|
+
export const PREFILL_STORAGE_PREFIX = "chat:prefill:";
|
|
45
|
+
export const PREFILL_PENDING_KEY = "chat:prefill:pending";
|
|
46
|
+
|
|
47
|
+
export function prefillKey(conversationId: string): string {
|
|
48
|
+
return `${PREFILL_STORAGE_PREFIX}${conversationId}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface ConversationTemplatePickerProps {
|
|
52
|
+
open: boolean;
|
|
53
|
+
onOpenChange: (open: boolean) => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function ConversationTemplatePicker({
|
|
57
|
+
open,
|
|
58
|
+
onOpenChange,
|
|
59
|
+
}: ConversationTemplatePickerProps) {
|
|
60
|
+
const { createConversation } = useChatSession();
|
|
61
|
+
const [blueprints, setBlueprints] = useState<WorkflowBlueprint[]>([]);
|
|
62
|
+
const [loaded, setLoaded] = useState(false);
|
|
63
|
+
const [selected, setSelected] = useState<WorkflowBlueprint | null>(null);
|
|
64
|
+
const [paramValues, setParamValues] = useState<Record<string, unknown>>({});
|
|
65
|
+
const [creating, setCreating] = useState(false);
|
|
66
|
+
const [error, setError] = useState<string | null>(null);
|
|
67
|
+
|
|
68
|
+
// Fetch blueprints once the sheet is opened for the first time.
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (!open || loaded) return;
|
|
71
|
+
let cancelled = false;
|
|
72
|
+
fetch("/api/blueprints")
|
|
73
|
+
.then((r) => (r.ok ? r.json() : []))
|
|
74
|
+
.then((data: WorkflowBlueprint[]) => {
|
|
75
|
+
if (!cancelled) setBlueprints(data);
|
|
76
|
+
})
|
|
77
|
+
.catch(() => {
|
|
78
|
+
if (!cancelled) setBlueprints([]);
|
|
79
|
+
})
|
|
80
|
+
.finally(() => {
|
|
81
|
+
if (!cancelled) setLoaded(true);
|
|
82
|
+
});
|
|
83
|
+
return () => {
|
|
84
|
+
cancelled = true;
|
|
85
|
+
};
|
|
86
|
+
}, [open, loaded]);
|
|
87
|
+
|
|
88
|
+
// Reset transient state whenever the sheet closes.
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (!open) {
|
|
91
|
+
setSelected(null);
|
|
92
|
+
setParamValues({});
|
|
93
|
+
setError(null);
|
|
94
|
+
setCreating(false);
|
|
95
|
+
}
|
|
96
|
+
}, [open]);
|
|
97
|
+
|
|
98
|
+
const canSubmit = useMemo(() => {
|
|
99
|
+
if (!selected) return false;
|
|
100
|
+
return selected.variables
|
|
101
|
+
.filter((v) => v.required)
|
|
102
|
+
.every((v) => {
|
|
103
|
+
const raw = paramValues[v.id];
|
|
104
|
+
if (v.type === "boolean") return raw !== undefined;
|
|
105
|
+
if (v.type === "number") return typeof raw === "number" && !isNaN(raw);
|
|
106
|
+
return typeof raw === "string" && raw.trim().length > 0;
|
|
107
|
+
});
|
|
108
|
+
}, [selected, paramValues]);
|
|
109
|
+
|
|
110
|
+
const handleSelect = (bp: WorkflowBlueprint) => {
|
|
111
|
+
// Initialize param values with blueprint defaults.
|
|
112
|
+
const initial: Record<string, unknown> = {};
|
|
113
|
+
for (const v of bp.variables) {
|
|
114
|
+
if (v.default !== undefined) initial[v.id] = v.default;
|
|
115
|
+
}
|
|
116
|
+
setParamValues(initial);
|
|
117
|
+
setSelected(bp);
|
|
118
|
+
setError(null);
|
|
119
|
+
|
|
120
|
+
// Zero-parameter blueprint: start immediately.
|
|
121
|
+
if (bp.variables.length === 0) {
|
|
122
|
+
void startConversation(bp, {});
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
async function startConversation(
|
|
127
|
+
bp: WorkflowBlueprint,
|
|
128
|
+
params: Record<string, unknown>
|
|
129
|
+
) {
|
|
130
|
+
setCreating(true);
|
|
131
|
+
setError(null);
|
|
132
|
+
try {
|
|
133
|
+
const rendered = renderBlueprintPrompt(bp, params);
|
|
134
|
+
|
|
135
|
+
// Race-order: the provider's `createConversation` both POSTs AND
|
|
136
|
+
// activates the new conversation internally. That means by the time
|
|
137
|
+
// `createConversation` resolves, the chat composer has already mounted
|
|
138
|
+
// with the new `conversationId` and its hydration effect has already
|
|
139
|
+
// fired. So we MUST write the prefill *before* awaiting, using a
|
|
140
|
+
// non-id-keyed "pending" slot that the composer consumes on next
|
|
141
|
+
// activation. The input clears it after reading — one-shot semantics.
|
|
142
|
+
const hasPrefill = rendered.firstMessage.trim().length > 0;
|
|
143
|
+
if (hasPrefill) {
|
|
144
|
+
try {
|
|
145
|
+
window.sessionStorage.setItem(PREFILL_PENDING_KEY, rendered.firstMessage);
|
|
146
|
+
} catch {
|
|
147
|
+
// sessionStorage can throw in private modes. User still lands in
|
|
148
|
+
// a valid empty conversation — graceful degradation.
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const id = await createConversation({
|
|
153
|
+
title: rendered.title || bp.name,
|
|
154
|
+
});
|
|
155
|
+
if (!id) {
|
|
156
|
+
// Clean up pending slot on failure so a later unrelated conversation
|
|
157
|
+
// doesn't accidentally consume it.
|
|
158
|
+
try {
|
|
159
|
+
window.sessionStorage.removeItem(PREFILL_PENDING_KEY);
|
|
160
|
+
} catch {}
|
|
161
|
+
throw new Error("Failed to create conversation");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
onOpenChange(false);
|
|
165
|
+
} catch (err) {
|
|
166
|
+
setError(err instanceof Error ? err.message : "Failed to start conversation");
|
|
167
|
+
setCreating(false);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
173
|
+
<SheetContent side="right" className="w-full sm:max-w-xl flex flex-col p-0">
|
|
174
|
+
<SheetHeader className="p-4 border-b border-border">
|
|
175
|
+
<SheetTitle className="flex items-center gap-2">
|
|
176
|
+
{selected && (
|
|
177
|
+
<Button
|
|
178
|
+
variant="ghost"
|
|
179
|
+
size="icon"
|
|
180
|
+
className="h-6 w-6"
|
|
181
|
+
onClick={() => setSelected(null)}
|
|
182
|
+
aria-label="Back to blueprint list"
|
|
183
|
+
>
|
|
184
|
+
<ArrowLeft className="h-4 w-4" />
|
|
185
|
+
</Button>
|
|
186
|
+
)}
|
|
187
|
+
<Sparkles className="h-4 w-4 text-primary" />
|
|
188
|
+
{selected ? selected.name : "Start from template"}
|
|
189
|
+
</SheetTitle>
|
|
190
|
+
<SheetDescription>
|
|
191
|
+
{selected
|
|
192
|
+
? selected.description
|
|
193
|
+
: "Pick a workflow blueprint. Its prompt will pre-fill the chat composer so you can edit before sending."}
|
|
194
|
+
</SheetDescription>
|
|
195
|
+
</SheetHeader>
|
|
196
|
+
|
|
197
|
+
<div className="flex-1 overflow-y-auto px-6 pb-6 pt-4">
|
|
198
|
+
{!selected && (
|
|
199
|
+
<BlueprintList
|
|
200
|
+
blueprints={blueprints}
|
|
201
|
+
loaded={loaded}
|
|
202
|
+
onSelect={handleSelect}
|
|
203
|
+
/>
|
|
204
|
+
)}
|
|
205
|
+
{selected && (
|
|
206
|
+
<ParameterForm
|
|
207
|
+
blueprint={selected}
|
|
208
|
+
values={paramValues}
|
|
209
|
+
onChange={setParamValues}
|
|
210
|
+
/>
|
|
211
|
+
)}
|
|
212
|
+
{error && (
|
|
213
|
+
<p className="mt-4 text-sm text-destructive" role="alert">
|
|
214
|
+
{error}
|
|
215
|
+
</p>
|
|
216
|
+
)}
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
{selected && (
|
|
220
|
+
<div className="flex items-center justify-end gap-2 border-t border-border px-6 py-3">
|
|
221
|
+
<Button variant="ghost" onClick={() => setSelected(null)} disabled={creating}>
|
|
222
|
+
Back
|
|
223
|
+
</Button>
|
|
224
|
+
<Button
|
|
225
|
+
onClick={() => startConversation(selected, paramValues)}
|
|
226
|
+
disabled={!canSubmit || creating}
|
|
227
|
+
>
|
|
228
|
+
{creating ? "Starting…" : "Start conversation"}
|
|
229
|
+
</Button>
|
|
230
|
+
</div>
|
|
231
|
+
)}
|
|
232
|
+
</SheetContent>
|
|
233
|
+
</Sheet>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── Blueprint list ───────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
function BlueprintList({
|
|
240
|
+
blueprints,
|
|
241
|
+
loaded,
|
|
242
|
+
onSelect,
|
|
243
|
+
}: {
|
|
244
|
+
blueprints: WorkflowBlueprint[];
|
|
245
|
+
loaded: boolean;
|
|
246
|
+
onSelect: (bp: WorkflowBlueprint) => void;
|
|
247
|
+
}) {
|
|
248
|
+
if (!loaded) {
|
|
249
|
+
return (
|
|
250
|
+
<div className="space-y-3">
|
|
251
|
+
{Array.from({ length: 4 }).map((_, i) => (
|
|
252
|
+
<Skeleton key={i} className="h-20 w-full" />
|
|
253
|
+
))}
|
|
254
|
+
</div>
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (blueprints.length === 0) {
|
|
259
|
+
return (
|
|
260
|
+
<p className="text-sm text-muted-foreground py-8 text-center">
|
|
261
|
+
No blueprints available.
|
|
262
|
+
</p>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return (
|
|
267
|
+
<div className="space-y-2">
|
|
268
|
+
{blueprints.map((bp) => {
|
|
269
|
+
const previewSource =
|
|
270
|
+
bp.chatPrompt ?? bp.steps[0]?.promptTemplate ?? "";
|
|
271
|
+
const preview =
|
|
272
|
+
previewSource.length > 140
|
|
273
|
+
? previewSource.slice(0, 140).trim() + "…"
|
|
274
|
+
: previewSource;
|
|
275
|
+
return (
|
|
276
|
+
<button
|
|
277
|
+
key={bp.id}
|
|
278
|
+
className="w-full text-left rounded-lg border border-border p-3 hover:bg-muted/50 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
279
|
+
onClick={() => onSelect(bp)}
|
|
280
|
+
>
|
|
281
|
+
<div className="flex items-start justify-between gap-3">
|
|
282
|
+
<div className="flex-1 min-w-0">
|
|
283
|
+
<div className="font-medium text-sm">{bp.name}</div>
|
|
284
|
+
<div className="text-xs text-muted-foreground mt-0.5">
|
|
285
|
+
{bp.description}
|
|
286
|
+
</div>
|
|
287
|
+
{preview && (
|
|
288
|
+
<div className="text-xs text-muted-foreground/80 mt-2 font-mono line-clamp-2">
|
|
289
|
+
{preview}
|
|
290
|
+
</div>
|
|
291
|
+
)}
|
|
292
|
+
</div>
|
|
293
|
+
<div className="flex flex-col items-end gap-1 shrink-0">
|
|
294
|
+
{bp.variables.length > 0 && (
|
|
295
|
+
<Badge variant="secondary" className="text-xs">
|
|
296
|
+
{bp.variables.length} param
|
|
297
|
+
{bp.variables.length > 1 ? "s" : ""}
|
|
298
|
+
</Badge>
|
|
299
|
+
)}
|
|
300
|
+
<Badge variant="outline" className="text-xs capitalize">
|
|
301
|
+
{bp.domain}
|
|
302
|
+
</Badge>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
</button>
|
|
306
|
+
);
|
|
307
|
+
})}
|
|
308
|
+
</div>
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ── Parameter form ───────────────────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
function ParameterForm({
|
|
315
|
+
blueprint,
|
|
316
|
+
values,
|
|
317
|
+
onChange,
|
|
318
|
+
}: {
|
|
319
|
+
blueprint: WorkflowBlueprint;
|
|
320
|
+
values: Record<string, unknown>;
|
|
321
|
+
onChange: (next: Record<string, unknown>) => void;
|
|
322
|
+
}) {
|
|
323
|
+
const setValue = (id: string, v: unknown) => onChange({ ...values, [id]: v });
|
|
324
|
+
|
|
325
|
+
if (blueprint.variables.length === 0) {
|
|
326
|
+
return (
|
|
327
|
+
<p className="text-sm text-muted-foreground">
|
|
328
|
+
This blueprint has no parameters — starting a conversation now.
|
|
329
|
+
</p>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return (
|
|
334
|
+
<div className="space-y-5">
|
|
335
|
+
{blueprint.variables.map((variable) => (
|
|
336
|
+
<VariableInput
|
|
337
|
+
key={variable.id}
|
|
338
|
+
variable={variable}
|
|
339
|
+
value={values[variable.id]}
|
|
340
|
+
onChange={(v) => setValue(variable.id, v)}
|
|
341
|
+
/>
|
|
342
|
+
))}
|
|
343
|
+
</div>
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function VariableInput({
|
|
348
|
+
variable,
|
|
349
|
+
value,
|
|
350
|
+
onChange,
|
|
351
|
+
}: {
|
|
352
|
+
variable: BlueprintVariable;
|
|
353
|
+
value: unknown;
|
|
354
|
+
onChange: (value: unknown) => void;
|
|
355
|
+
}) {
|
|
356
|
+
return (
|
|
357
|
+
<div className="space-y-1.5">
|
|
358
|
+
<Label>
|
|
359
|
+
{variable.label}
|
|
360
|
+
{variable.required && <span className="text-destructive ml-1">*</span>}
|
|
361
|
+
</Label>
|
|
362
|
+
{variable.description && (
|
|
363
|
+
<p className="text-xs text-muted-foreground">{variable.description}</p>
|
|
364
|
+
)}
|
|
365
|
+
|
|
366
|
+
{variable.type === "text" && (
|
|
367
|
+
<TextInput
|
|
368
|
+
value={String(value ?? "")}
|
|
369
|
+
onChange={(e) => onChange(e.target.value)}
|
|
370
|
+
placeholder={variable.placeholder}
|
|
371
|
+
/>
|
|
372
|
+
)}
|
|
373
|
+
|
|
374
|
+
{variable.type === "textarea" && (
|
|
375
|
+
<Textarea
|
|
376
|
+
value={String(value ?? "")}
|
|
377
|
+
onChange={(e) => onChange(e.target.value)}
|
|
378
|
+
placeholder={variable.placeholder}
|
|
379
|
+
rows={3}
|
|
380
|
+
/>
|
|
381
|
+
)}
|
|
382
|
+
|
|
383
|
+
{variable.type === "number" && (
|
|
384
|
+
<TextInput
|
|
385
|
+
type="number"
|
|
386
|
+
value={value !== undefined && value !== null ? Number(value) : ""}
|
|
387
|
+
onChange={(e) =>
|
|
388
|
+
onChange(e.target.value ? Number(e.target.value) : undefined)
|
|
389
|
+
}
|
|
390
|
+
min={variable.min}
|
|
391
|
+
max={variable.max}
|
|
392
|
+
/>
|
|
393
|
+
)}
|
|
394
|
+
|
|
395
|
+
{variable.type === "boolean" && (
|
|
396
|
+
<Switch
|
|
397
|
+
checked={Boolean(value)}
|
|
398
|
+
onCheckedChange={(checked) => onChange(checked)}
|
|
399
|
+
/>
|
|
400
|
+
)}
|
|
401
|
+
|
|
402
|
+
{variable.type === "select" && variable.options && (
|
|
403
|
+
<Select
|
|
404
|
+
value={String(value ?? "")}
|
|
405
|
+
onValueChange={(v) => onChange(v)}
|
|
406
|
+
>
|
|
407
|
+
<SelectTrigger>
|
|
408
|
+
<SelectValue placeholder="Select…" />
|
|
409
|
+
</SelectTrigger>
|
|
410
|
+
<SelectContent>
|
|
411
|
+
{variable.options.map((opt) => (
|
|
412
|
+
<SelectItem key={opt.value} value={opt.value}>
|
|
413
|
+
{opt.label}
|
|
414
|
+
</SelectItem>
|
|
415
|
+
))}
|
|
416
|
+
</SelectContent>
|
|
417
|
+
</Select>
|
|
418
|
+
)}
|
|
419
|
+
</div>
|
|
420
|
+
);
|
|
421
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
|
3
|
+
|
|
4
|
+
interface HelpDialogProps {
|
|
5
|
+
open: boolean;
|
|
6
|
+
onOpenChange: (open: boolean) => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function HelpDialog({ open, onOpenChange }: HelpDialogProps) {
|
|
10
|
+
return (
|
|
11
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
12
|
+
<DialogContent>
|
|
13
|
+
<DialogHeader>
|
|
14
|
+
<DialogTitle>Chat shortcuts</DialogTitle>
|
|
15
|
+
</DialogHeader>
|
|
16
|
+
<div className="space-y-2 text-sm">
|
|
17
|
+
<Row k="/" v="Open actions / skills / tools menu" />
|
|
18
|
+
<Row k="@" v="Reference a project, task, document, or file" />
|
|
19
|
+
<Row k="⌘K" v="Open global command palette" />
|
|
20
|
+
<Row k="⌘/" v="Focus chat input and open slash menu" />
|
|
21
|
+
<Row k="⌘L" v="Clear conversation (new session)" />
|
|
22
|
+
<Row k="⌘⇧L" v="Clear conversation (browser fallback)" />
|
|
23
|
+
<Row k="⌘⏎" v="Send message" />
|
|
24
|
+
<Row k="↑ ↓" v="Navigate popover items" />
|
|
25
|
+
<Row k="Esc" v="Close popover" />
|
|
26
|
+
</div>
|
|
27
|
+
</DialogContent>
|
|
28
|
+
</Dialog>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function Row({ k, v }: { k: string; v: string }) {
|
|
33
|
+
return (
|
|
34
|
+
<div className="flex items-start gap-3">
|
|
35
|
+
<kbd className="shrink-0 rounded border border-border bg-muted px-1.5 py-0.5 font-mono text-xs">{k}</kbd>
|
|
36
|
+
<span className="text-muted-foreground">{v}</span>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SkillCompositionConflictDialog — surfaces conflict excerpts when the
|
|
5
|
+
* user tries to compose two skills whose directives diverge.
|
|
6
|
+
*
|
|
7
|
+
* Triggered by `chat-command-popover.tsx` when `activate_skill mode:"add"`
|
|
8
|
+
* returns `{ requiresConfirmation: true, conflicts: [...] }`. Confirming
|
|
9
|
+
* re-issues the same call with `force: true`.
|
|
10
|
+
*
|
|
11
|
+
* See `features/chat-composition-ui-v1.md`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { AlertTriangle } from "lucide-react";
|
|
15
|
+
import {
|
|
16
|
+
Dialog,
|
|
17
|
+
DialogContent,
|
|
18
|
+
DialogDescription,
|
|
19
|
+
DialogFooter,
|
|
20
|
+
DialogHeader,
|
|
21
|
+
DialogTitle,
|
|
22
|
+
} from "@/components/ui/dialog";
|
|
23
|
+
import { Button } from "@/components/ui/button";
|
|
24
|
+
import type { SkillConflict } from "@/lib/chat/skill-conflict";
|
|
25
|
+
|
|
26
|
+
interface SkillCompositionConflictDialogProps {
|
|
27
|
+
open: boolean;
|
|
28
|
+
onOpenChange: (open: boolean) => void;
|
|
29
|
+
/** Skill the user is trying to add (display label). */
|
|
30
|
+
newSkillName: string;
|
|
31
|
+
conflicts: SkillConflict[];
|
|
32
|
+
/** Called when user clicks "Add anyway" — issues the force:true retry. */
|
|
33
|
+
onConfirm: () => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function SkillCompositionConflictDialog({
|
|
37
|
+
open,
|
|
38
|
+
onOpenChange,
|
|
39
|
+
newSkillName,
|
|
40
|
+
conflicts,
|
|
41
|
+
onConfirm,
|
|
42
|
+
}: SkillCompositionConflictDialogProps) {
|
|
43
|
+
return (
|
|
44
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
45
|
+
<DialogContent className="max-w-lg">
|
|
46
|
+
<DialogHeader>
|
|
47
|
+
<DialogTitle className="flex items-center gap-2">
|
|
48
|
+
<AlertTriangle className="h-4 w-4 text-amber-500" />
|
|
49
|
+
Conflict detected
|
|
50
|
+
</DialogTitle>
|
|
51
|
+
<DialogDescription>
|
|
52
|
+
Adding{" "}
|
|
53
|
+
<span className="font-mono text-foreground">{newSkillName}</span>{" "}
|
|
54
|
+
may conflict with {conflicts.length === 1 ? "an active skill" : "active skills"}.
|
|
55
|
+
Review the directives below and decide whether to add anyway.
|
|
56
|
+
</DialogDescription>
|
|
57
|
+
</DialogHeader>
|
|
58
|
+
|
|
59
|
+
<div className="max-h-72 overflow-y-auto space-y-3 px-1">
|
|
60
|
+
{conflicts.map((c, idx) => (
|
|
61
|
+
<div
|
|
62
|
+
key={`${c.skillA}-${c.skillB}-${idx}`}
|
|
63
|
+
className="rounded-md border bg-muted/30 p-3 text-xs space-y-2"
|
|
64
|
+
>
|
|
65
|
+
<div className="text-muted-foreground">
|
|
66
|
+
Topic: <span className="font-mono text-foreground">{c.sharedTopic}</span>
|
|
67
|
+
</div>
|
|
68
|
+
<div>
|
|
69
|
+
<span className="font-semibold">{c.skillA}:</span>{" "}
|
|
70
|
+
<span className="italic">"{c.excerptA}"</span>
|
|
71
|
+
</div>
|
|
72
|
+
<div>
|
|
73
|
+
<span className="font-semibold">{c.skillB}:</span>{" "}
|
|
74
|
+
<span className="italic">"{c.excerptB}"</span>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
))}
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<DialogFooter className="gap-2">
|
|
81
|
+
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
82
|
+
Cancel
|
|
83
|
+
</Button>
|
|
84
|
+
<Button
|
|
85
|
+
onClick={() => {
|
|
86
|
+
onConfirm();
|
|
87
|
+
onOpenChange(false);
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
Add anyway
|
|
91
|
+
</Button>
|
|
92
|
+
</DialogFooter>
|
|
93
|
+
</DialogContent>
|
|
94
|
+
</Dialog>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Sparkles, Star, X } from "lucide-react";
|
|
3
|
+
import { Badge } from "@/components/ui/badge";
|
|
4
|
+
import { CommandItem } from "@/components/ui/command";
|
|
5
|
+
import type { EnrichedSkill } from "@/lib/environment/skill-enrichment";
|
|
6
|
+
import type { ReactNode } from "react";
|
|
7
|
+
|
|
8
|
+
interface SkillRowProps {
|
|
9
|
+
skill: EnrichedSkill;
|
|
10
|
+
recommended?: boolean;
|
|
11
|
+
onSelect: () => void;
|
|
12
|
+
onDismissRecommendation?: () => void;
|
|
13
|
+
/** Whether this skill is currently active on the conversation. */
|
|
14
|
+
isActive?: boolean;
|
|
15
|
+
/** Optional "+ Add" / disabled "+ Add" button rendered to the right. */
|
|
16
|
+
addButton?: ReactNode;
|
|
17
|
+
/** Called when the user wants to deactivate this (active) skill. */
|
|
18
|
+
onDeactivate?: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function healthVariant(
|
|
22
|
+
h: EnrichedSkill["healthScore"]
|
|
23
|
+
): "default" | "secondary" | "destructive" | "outline" {
|
|
24
|
+
if (h === "healthy") return "default";
|
|
25
|
+
if (h === "stale") return "outline";
|
|
26
|
+
if (h === "aging" || h === "broken") return "destructive";
|
|
27
|
+
return "secondary";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function syncLabel(s: EnrichedSkill["syncStatus"]): string {
|
|
31
|
+
switch (s) {
|
|
32
|
+
case "synced":
|
|
33
|
+
return "synced";
|
|
34
|
+
case "claude-only":
|
|
35
|
+
return "claude-only";
|
|
36
|
+
case "codex-only":
|
|
37
|
+
return "codex-only";
|
|
38
|
+
case "shared":
|
|
39
|
+
return "shared";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function SkillRow({
|
|
44
|
+
skill,
|
|
45
|
+
recommended,
|
|
46
|
+
onSelect,
|
|
47
|
+
onDismissRecommendation,
|
|
48
|
+
isActive,
|
|
49
|
+
addButton,
|
|
50
|
+
onDeactivate,
|
|
51
|
+
}: SkillRowProps) {
|
|
52
|
+
const syncHref =
|
|
53
|
+
skill.syncStatus !== "synced"
|
|
54
|
+
? `/environment?skill=${encodeURIComponent(skill.name)}`
|
|
55
|
+
: null;
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<CommandItem
|
|
59
|
+
key={skill.id}
|
|
60
|
+
value={`${skill.name} ${skill.preview} ${skill.tool}`}
|
|
61
|
+
onSelect={onSelect}
|
|
62
|
+
>
|
|
63
|
+
<Sparkles className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
64
|
+
<div className="flex flex-col min-w-0 gap-0.5">
|
|
65
|
+
<div className="flex items-center gap-1.5">
|
|
66
|
+
<span className="truncate text-sm font-medium">{skill.name}</span>
|
|
67
|
+
{isActive && (
|
|
68
|
+
<Badge variant="default" className="text-[10px] shrink-0">
|
|
69
|
+
active
|
|
70
|
+
</Badge>
|
|
71
|
+
)}
|
|
72
|
+
{recommended && (
|
|
73
|
+
<Star
|
|
74
|
+
className="h-3 w-3 shrink-0 fill-amber-500 text-amber-500"
|
|
75
|
+
aria-label="Recommended for this conversation"
|
|
76
|
+
/>
|
|
77
|
+
)}
|
|
78
|
+
{recommended && onDismissRecommendation && (
|
|
79
|
+
<button
|
|
80
|
+
type="button"
|
|
81
|
+
aria-label="Dismiss recommendation"
|
|
82
|
+
className="rounded p-0.5 text-muted-foreground hover:text-foreground hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
83
|
+
onClick={(e) => {
|
|
84
|
+
e.stopPropagation();
|
|
85
|
+
onDismissRecommendation();
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
<X className="h-3 w-3" />
|
|
89
|
+
</button>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
<span className="truncate text-xs text-muted-foreground">
|
|
93
|
+
{skill.preview}
|
|
94
|
+
</span>
|
|
95
|
+
<div className="flex flex-wrap items-center gap-1 mt-0.5">
|
|
96
|
+
<Badge
|
|
97
|
+
variant={healthVariant(skill.healthScore)}
|
|
98
|
+
className="text-[10px]"
|
|
99
|
+
>
|
|
100
|
+
{skill.healthScore}
|
|
101
|
+
</Badge>
|
|
102
|
+
<Badge variant="outline" className="text-[10px]">
|
|
103
|
+
{syncLabel(skill.syncStatus)}
|
|
104
|
+
</Badge>
|
|
105
|
+
{skill.linkedProfileId && (
|
|
106
|
+
<Badge variant="secondary" className="text-[10px]">
|
|
107
|
+
{skill.linkedProfileId}
|
|
108
|
+
</Badge>
|
|
109
|
+
)}
|
|
110
|
+
<Badge variant="outline" className="text-[10px]">
|
|
111
|
+
{skill.scope}
|
|
112
|
+
</Badge>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
{/* Right-side slot: either the env link or the Add/Deactivate button. */}
|
|
116
|
+
{isActive && onDeactivate ? (
|
|
117
|
+
<button
|
|
118
|
+
type="button"
|
|
119
|
+
aria-label={`Deactivate ${skill.name}`}
|
|
120
|
+
title="Deactivate skill"
|
|
121
|
+
className="ml-auto shrink-0 text-[10px] text-muted-foreground hover:text-foreground rounded px-1 py-0.5 hover:bg-muted focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
122
|
+
onMouseDown={(e) => {
|
|
123
|
+
e.preventDefault();
|
|
124
|
+
e.stopPropagation();
|
|
125
|
+
}}
|
|
126
|
+
onClick={(e) => {
|
|
127
|
+
e.stopPropagation();
|
|
128
|
+
onDeactivate();
|
|
129
|
+
}}
|
|
130
|
+
>
|
|
131
|
+
<X className="h-3 w-3" />
|
|
132
|
+
</button>
|
|
133
|
+
) : addButton ? (
|
|
134
|
+
addButton
|
|
135
|
+
) : syncHref ? (
|
|
136
|
+
<a
|
|
137
|
+
href={syncHref}
|
|
138
|
+
aria-label={`Open ${skill.name} in environment dashboard`}
|
|
139
|
+
className="ml-auto shrink-0 text-muted-foreground hover:text-foreground"
|
|
140
|
+
onClick={(e) => e.stopPropagation()}
|
|
141
|
+
>
|
|
142
|
+
↗
|
|
143
|
+
</a>
|
|
144
|
+
) : null}
|
|
145
|
+
</CommandItem>
|
|
146
|
+
);
|
|
147
|
+
}
|