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
@@ -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
+ }