stagent 0.1.9 → 0.1.11
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 +144 -62
- package/package.json +1 -2
- package/public/readme/cost-usage-list.png +0 -0
- package/public/readme/dashboard-bulk-select.png +0 -0
- package/public/readme/dashboard-card-edit.png +0 -0
- package/public/readme/dashboard-create-form-ai-applied.png +0 -0
- package/public/readme/dashboard-create-form-ai-assist.png +0 -0
- package/public/readme/dashboard-create-form-empty.png +0 -0
- package/public/readme/dashboard-create-form-filled.png +0 -0
- package/public/readme/dashboard-filtered.png +0 -0
- package/public/readme/dashboard-list.png +0 -0
- package/public/readme/dashboard-sorted.png +0 -0
- package/public/readme/dashboard-workflow-confirm.png +0 -0
- package/public/readme/documents-grid.png +0 -0
- package/public/readme/documents-list.png +0 -0
- package/public/readme/home-below-fold.png +0 -0
- package/public/readme/home-list.png +0 -0
- package/public/readme/inbox-list.png +0 -0
- package/public/readme/monitor-list.png +0 -0
- package/public/readme/profiles-list.png +0 -0
- package/public/readme/projects-detail.png +0 -0
- package/public/readme/projects-list.png +0 -0
- package/public/readme/schedules-list.png +0 -0
- package/public/readme/settings-list.png +0 -0
- package/public/readme/workflows-list.png +0 -0
- package/src/app/api/profiles/route.ts +0 -1
- package/src/app/api/workflows/from-assist/route.ts +143 -0
- package/src/app/dashboard/page.tsx +24 -2
- package/src/app/globals.css +0 -5
- package/src/app/tasks/page.tsx +5 -0
- package/src/app/workflows/from-assist/page.tsx +35 -0
- package/src/components/profiles/profile-detail-view.tsx +1 -16
- package/src/components/profiles/profile-form-view.tsx +0 -22
- package/src/components/projects/project-card.tsx +47 -35
- package/src/components/tasks/ai-assist-panel.tsx +31 -10
- package/src/components/tasks/task-card.tsx +16 -1
- package/src/components/tasks/task-create-panel.tsx +39 -0
- package/src/components/workflows/workflow-confirmation-view.tsx +447 -0
- package/src/lib/agents/__tests__/claude-agent.test.ts +7 -2
- package/src/lib/agents/__tests__/learned-context.test.ts +500 -0
- package/src/lib/agents/__tests__/pattern-extractor.test.ts +243 -0
- package/src/lib/agents/__tests__/sweep.test.ts +202 -0
- package/src/lib/agents/claude-agent.ts +104 -78
- package/src/lib/agents/learned-context.ts +5 -13
- package/src/lib/agents/pattern-extractor.ts +15 -64
- package/src/lib/agents/profiles/__tests__/suggest.test.ts +67 -0
- package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/general/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/researcher/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/sweep/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +0 -1
- package/src/lib/agents/profiles/registry.ts +0 -1
- package/src/lib/agents/profiles/suggest.ts +36 -0
- package/src/lib/agents/profiles/types.ts +0 -1
- package/src/lib/agents/runtime/catalog.ts +1 -1
- package/src/lib/agents/runtime/claude.ts +102 -6
- package/src/lib/agents/runtime/task-assist-types.ts +12 -2
- package/src/lib/constants/task-status.ts +6 -0
- package/src/lib/data/__tests__/clear.test.ts +42 -0
- package/src/lib/data/clear.ts +3 -0
- package/src/lib/data/seed-data/profiles.ts +0 -3
- package/src/lib/notifications/permissions.ts +6 -2
- package/src/lib/usage/__tests__/ledger.test.ts +29 -5
- package/src/lib/usage/ledger.ts +3 -1
- package/src/lib/usage/pricing.ts +61 -7
- package/src/lib/validators/__tests__/profile.test.ts +0 -15
- package/src/lib/validators/profile.ts +0 -1
- package/src/lib/workflows/__tests__/assist-builder.test.ts +255 -0
- package/src/lib/workflows/__tests__/engine.test.ts +2 -0
- package/src/lib/workflows/assist-builder.ts +248 -0
- package/src/lib/workflows/assist-session.ts +78 -0
- package/src/lib/workflows/engine.ts +47 -1
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import { Input } from "@/components/ui/input";
|
|
7
|
+
import { Label } from "@/components/ui/label";
|
|
8
|
+
import { Badge } from "@/components/ui/badge";
|
|
9
|
+
import {
|
|
10
|
+
Select,
|
|
11
|
+
SelectContent,
|
|
12
|
+
SelectItem,
|
|
13
|
+
SelectTrigger,
|
|
14
|
+
SelectValue,
|
|
15
|
+
} from "@/components/ui/select";
|
|
16
|
+
import {
|
|
17
|
+
ArrowUp,
|
|
18
|
+
ArrowDown,
|
|
19
|
+
Star,
|
|
20
|
+
Trash2,
|
|
21
|
+
Loader2,
|
|
22
|
+
GitBranch,
|
|
23
|
+
ListOrdered,
|
|
24
|
+
Settings,
|
|
25
|
+
} from "lucide-react";
|
|
26
|
+
import { toast } from "sonner";
|
|
27
|
+
import { FormSectionCard } from "@/components/shared/form-section-card";
|
|
28
|
+
import type { TaskAssistResponse } from "@/lib/agents/runtime/task-assist-types";
|
|
29
|
+
import type { WorkflowPattern } from "@/lib/workflows/types";
|
|
30
|
+
import { suggestProfileForStep } from "@/lib/agents/profiles/suggest";
|
|
31
|
+
import {
|
|
32
|
+
loadAssistState,
|
|
33
|
+
clearAssistState,
|
|
34
|
+
saveTaskFormState,
|
|
35
|
+
} from "@/lib/workflows/assist-session";
|
|
36
|
+
|
|
37
|
+
const PATTERN_OPTIONS: { value: WorkflowPattern; label: string; description: string }[] = [
|
|
38
|
+
{ value: "sequence", label: "Sequence", description: "Steps run one after another" },
|
|
39
|
+
{ value: "planner-executor", label: "Planner → Executor", description: "First step plans, rest execute" },
|
|
40
|
+
{ value: "checkpoint", label: "Checkpoint", description: "Steps pause for human approval" },
|
|
41
|
+
{ value: "parallel", label: "Parallel", description: "Independent steps run concurrently" },
|
|
42
|
+
{ value: "loop", label: "Loop", description: "Single step repeats iteratively" },
|
|
43
|
+
{ value: "swarm", label: "Swarm", description: "Mayor coordinates workers" },
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
interface StepItem {
|
|
47
|
+
title: string;
|
|
48
|
+
description: string;
|
|
49
|
+
profile: string;
|
|
50
|
+
requiresApproval?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface ProfileOption {
|
|
54
|
+
id: string;
|
|
55
|
+
name: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface WorkflowConfirmationViewProps {
|
|
59
|
+
projects: { id: string; name: string }[];
|
|
60
|
+
profiles: ProfileOption[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function WorkflowConfirmationView({
|
|
64
|
+
projects,
|
|
65
|
+
profiles,
|
|
66
|
+
}: WorkflowConfirmationViewProps) {
|
|
67
|
+
const router = useRouter();
|
|
68
|
+
const [workflowName, setWorkflowName] = useState("");
|
|
69
|
+
const [pattern, setPattern] = useState<WorkflowPattern>("sequence");
|
|
70
|
+
const [selectedProjectId, setSelectedProjectId] = useState("");
|
|
71
|
+
const [steps, setSteps] = useState<StepItem[]>([]);
|
|
72
|
+
const [maxIterations, setMaxIterations] = useState(5);
|
|
73
|
+
const [workerConcurrencyLimit, setWorkerConcurrencyLimit] = useState(2);
|
|
74
|
+
const [submitting, setSubmitting] = useState(false);
|
|
75
|
+
const [priority, setPriority] = useState(2);
|
|
76
|
+
const [assignedAgent, setAssignedAgent] = useState<string | undefined>();
|
|
77
|
+
const [loaded, setLoaded] = useState(false);
|
|
78
|
+
|
|
79
|
+
// Load state from sessionStorage on mount
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
const state = loadAssistState();
|
|
82
|
+
if (!state) {
|
|
83
|
+
router.replace("/tasks/new");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const { assistResult, formState } = state;
|
|
88
|
+
const recommended = assistResult.recommendedPattern;
|
|
89
|
+
const validPattern = recommended === "single" ? "sequence" : (recommended as WorkflowPattern);
|
|
90
|
+
|
|
91
|
+
setPattern(validPattern);
|
|
92
|
+
setWorkflowName(`Workflow: ${formState.title}`);
|
|
93
|
+
setSelectedProjectId(formState.projectId);
|
|
94
|
+
setPriority(parseInt(formState.priority, 10) || 2);
|
|
95
|
+
setAssignedAgent(formState.assignedAgent || undefined);
|
|
96
|
+
setMaxIterations(assistResult.suggestedLoopConfig?.maxIterations ?? 5);
|
|
97
|
+
setWorkerConcurrencyLimit(assistResult.suggestedSwarmConfig?.workerConcurrencyLimit ?? 2);
|
|
98
|
+
|
|
99
|
+
const profileIds = profiles.map((p) => p.id);
|
|
100
|
+
|
|
101
|
+
const newSteps: StepItem[] = [
|
|
102
|
+
{
|
|
103
|
+
title: formState.title,
|
|
104
|
+
description: formState.description,
|
|
105
|
+
profile: formState.agentProfile || "auto",
|
|
106
|
+
},
|
|
107
|
+
...assistResult.breakdown.map((sub) => ({
|
|
108
|
+
title: sub.title,
|
|
109
|
+
description: sub.description,
|
|
110
|
+
profile: sub.suggestedProfile || suggestProfileForStep(sub.title, sub.description, profileIds),
|
|
111
|
+
requiresApproval: sub.requiresApproval,
|
|
112
|
+
})),
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
setSteps(newSteps);
|
|
116
|
+
setLoaded(true);
|
|
117
|
+
}, [router, profiles]);
|
|
118
|
+
|
|
119
|
+
function navigateBackToTask() {
|
|
120
|
+
// Save form state so task form can restore
|
|
121
|
+
const state = loadAssistState();
|
|
122
|
+
if (state) {
|
|
123
|
+
saveTaskFormState(state.formState);
|
|
124
|
+
}
|
|
125
|
+
clearAssistState();
|
|
126
|
+
router.push("/tasks/new?restore=1");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function moveStep(index: number, direction: -1 | 1) {
|
|
130
|
+
if (index === 0) return;
|
|
131
|
+
const newIndex = index + direction;
|
|
132
|
+
if (newIndex < 1 || newIndex >= steps.length) return;
|
|
133
|
+
const newSteps = [...steps];
|
|
134
|
+
[newSteps[index], newSteps[newIndex]] = [newSteps[newIndex], newSteps[index]];
|
|
135
|
+
setSteps(newSteps);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function removeStep(index: number) {
|
|
139
|
+
if (index === 0 || steps.length <= 2) return;
|
|
140
|
+
setSteps(steps.filter((_, i) => i !== index));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function updateStepProfile(index: number, profile: string) {
|
|
144
|
+
const newSteps = [...steps];
|
|
145
|
+
newSteps[index] = { ...newSteps[index], profile };
|
|
146
|
+
setSteps(newSteps);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function handleSubmit(executeImmediately: boolean) {
|
|
150
|
+
if (!workflowName.trim() || steps.length < 2) return;
|
|
151
|
+
setSubmitting(true);
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const definitionSteps = steps.map((step, i) => ({
|
|
155
|
+
id: `step_${i + 1}`,
|
|
156
|
+
name: step.title,
|
|
157
|
+
prompt: step.description,
|
|
158
|
+
agentProfile: step.profile === "auto" ? undefined : step.profile,
|
|
159
|
+
requiresApproval: pattern === "checkpoint" ? step.requiresApproval : undefined,
|
|
160
|
+
...(pattern === "parallel" && i === steps.length - 1
|
|
161
|
+
? { dependsOn: steps.slice(0, -1).map((_, j) => `step_${j + 1}`) }
|
|
162
|
+
: {}),
|
|
163
|
+
}));
|
|
164
|
+
|
|
165
|
+
const definition: Record<string, unknown> = {
|
|
166
|
+
pattern,
|
|
167
|
+
steps: definitionSteps,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
if (pattern === "loop") {
|
|
171
|
+
definition.loopConfig = {
|
|
172
|
+
maxIterations,
|
|
173
|
+
agentProfile: steps[0]?.profile === "auto" ? undefined : steps[0]?.profile,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (pattern === "swarm") {
|
|
178
|
+
definition.swarmConfig = { workerConcurrencyLimit };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const res = await fetch("/api/workflows/from-assist", {
|
|
182
|
+
method: "POST",
|
|
183
|
+
headers: { "Content-Type": "application/json" },
|
|
184
|
+
body: JSON.stringify({
|
|
185
|
+
name: workflowName.trim(),
|
|
186
|
+
projectId: selectedProjectId || undefined,
|
|
187
|
+
definition,
|
|
188
|
+
priority,
|
|
189
|
+
assignedAgent: assignedAgent || undefined,
|
|
190
|
+
executeImmediately,
|
|
191
|
+
parentTask: {
|
|
192
|
+
title: steps[0].title,
|
|
193
|
+
description: steps[0].description,
|
|
194
|
+
agentProfile: steps[0].profile === "auto" ? undefined : steps[0].profile,
|
|
195
|
+
},
|
|
196
|
+
}),
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
if (!res.ok) {
|
|
200
|
+
const data = await res.json().catch(() => null);
|
|
201
|
+
toast.error(data?.error ?? "Failed to create workflow");
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const data = await res.json();
|
|
206
|
+
toast.success(
|
|
207
|
+
executeImmediately
|
|
208
|
+
? `Task created with workflow (${data.taskIds.length} steps)`
|
|
209
|
+
: `Task created with draft workflow (${data.taskIds.length} steps)`,
|
|
210
|
+
{
|
|
211
|
+
action: {
|
|
212
|
+
label: "View workflow",
|
|
213
|
+
onClick: () => window.open(`/workflows/${data.workflow.id}`, "_self"),
|
|
214
|
+
},
|
|
215
|
+
}
|
|
216
|
+
);
|
|
217
|
+
clearAssistState();
|
|
218
|
+
router.push("/dashboard");
|
|
219
|
+
} catch {
|
|
220
|
+
toast.error("Network error — could not create workflow");
|
|
221
|
+
} finally {
|
|
222
|
+
setSubmitting(false);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const isReorderDisabled = pattern === "parallel";
|
|
227
|
+
|
|
228
|
+
if (!loaded) {
|
|
229
|
+
return (
|
|
230
|
+
<div className="flex items-center justify-center py-12 text-muted-foreground">
|
|
231
|
+
<Loader2 className="h-5 w-5 animate-spin mr-2" />
|
|
232
|
+
Loading workflow data...
|
|
233
|
+
</div>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<div className="space-y-6">
|
|
239
|
+
<div>
|
|
240
|
+
<h1 className="text-2xl font-bold">Create Workflow from AI Assist</h1>
|
|
241
|
+
<p className="text-sm text-muted-foreground mt-1">
|
|
242
|
+
Review and customize the AI-suggested workflow before creating it.
|
|
243
|
+
</p>
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
{/* Workflow Identity */}
|
|
247
|
+
<FormSectionCard icon={GitBranch} title="Workflow Identity">
|
|
248
|
+
<div className="space-y-3">
|
|
249
|
+
<div className="space-y-1.5">
|
|
250
|
+
<Label htmlFor="workflow-name">Name</Label>
|
|
251
|
+
<Input
|
|
252
|
+
id="workflow-name"
|
|
253
|
+
value={workflowName}
|
|
254
|
+
onChange={(e) => setWorkflowName(e.target.value)}
|
|
255
|
+
placeholder="Workflow name"
|
|
256
|
+
/>
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
<div className="grid grid-cols-2 gap-3">
|
|
260
|
+
<div className="space-y-1.5">
|
|
261
|
+
<Label>Pattern</Label>
|
|
262
|
+
<Select value={pattern} onValueChange={(v) => setPattern(v as WorkflowPattern)}>
|
|
263
|
+
<SelectTrigger>
|
|
264
|
+
<SelectValue />
|
|
265
|
+
</SelectTrigger>
|
|
266
|
+
<SelectContent>
|
|
267
|
+
{PATTERN_OPTIONS.map((opt) => (
|
|
268
|
+
<SelectItem key={opt.value} value={opt.value}>
|
|
269
|
+
{opt.label}
|
|
270
|
+
</SelectItem>
|
|
271
|
+
))}
|
|
272
|
+
</SelectContent>
|
|
273
|
+
</Select>
|
|
274
|
+
<p className="text-xs text-muted-foreground">
|
|
275
|
+
{PATTERN_OPTIONS.find((o) => o.value === pattern)?.description}
|
|
276
|
+
</p>
|
|
277
|
+
</div>
|
|
278
|
+
<div className="space-y-1.5">
|
|
279
|
+
<Label>Project</Label>
|
|
280
|
+
<Select
|
|
281
|
+
value={selectedProjectId || "none"}
|
|
282
|
+
onValueChange={(v) => setSelectedProjectId(v === "none" ? "" : v)}
|
|
283
|
+
>
|
|
284
|
+
<SelectTrigger>
|
|
285
|
+
<SelectValue placeholder="None" />
|
|
286
|
+
</SelectTrigger>
|
|
287
|
+
<SelectContent>
|
|
288
|
+
<SelectItem value="none">None</SelectItem>
|
|
289
|
+
{projects.map((p) => (
|
|
290
|
+
<SelectItem key={p.id} value={p.id}>{p.name}</SelectItem>
|
|
291
|
+
))}
|
|
292
|
+
</SelectContent>
|
|
293
|
+
</Select>
|
|
294
|
+
</div>
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
</FormSectionCard>
|
|
298
|
+
|
|
299
|
+
{/* Steps */}
|
|
300
|
+
<FormSectionCard icon={ListOrdered} title={`Steps (${steps.length})`}>
|
|
301
|
+
<div className="space-y-2">
|
|
302
|
+
{steps.map((step, i) => (
|
|
303
|
+
<div
|
|
304
|
+
key={i}
|
|
305
|
+
className="flex items-center gap-2 p-2.5 rounded-md border bg-card text-sm"
|
|
306
|
+
>
|
|
307
|
+
<span className="text-xs text-muted-foreground w-5 shrink-0">
|
|
308
|
+
{i + 1}.
|
|
309
|
+
</span>
|
|
310
|
+
{i === 0 && (
|
|
311
|
+
<Badge variant="outline" className="text-xs shrink-0 px-1">
|
|
312
|
+
<Star className="h-2.5 w-2.5" />
|
|
313
|
+
</Badge>
|
|
314
|
+
)}
|
|
315
|
+
<div className="flex-1 min-w-0">
|
|
316
|
+
<span className="font-medium block truncate">{step.title}</span>
|
|
317
|
+
{step.description && (
|
|
318
|
+
<span className="text-xs text-muted-foreground block truncate">
|
|
319
|
+
{step.description}
|
|
320
|
+
</span>
|
|
321
|
+
)}
|
|
322
|
+
</div>
|
|
323
|
+
|
|
324
|
+
{/* Profile selector */}
|
|
325
|
+
<Select
|
|
326
|
+
value={step.profile}
|
|
327
|
+
onValueChange={(v) => updateStepProfile(i, v)}
|
|
328
|
+
>
|
|
329
|
+
<SelectTrigger className="w-32 h-7 text-xs shrink-0">
|
|
330
|
+
<SelectValue />
|
|
331
|
+
</SelectTrigger>
|
|
332
|
+
<SelectContent>
|
|
333
|
+
<SelectItem value="auto">Auto</SelectItem>
|
|
334
|
+
{profiles.map((p) => (
|
|
335
|
+
<SelectItem key={p.id} value={p.id}>
|
|
336
|
+
{p.name}
|
|
337
|
+
</SelectItem>
|
|
338
|
+
))}
|
|
339
|
+
</SelectContent>
|
|
340
|
+
</Select>
|
|
341
|
+
|
|
342
|
+
{/* Reorder buttons */}
|
|
343
|
+
{!isReorderDisabled && (
|
|
344
|
+
<div className="flex flex-col gap-0.5">
|
|
345
|
+
<button
|
|
346
|
+
type="button"
|
|
347
|
+
onClick={() => moveStep(i, -1)}
|
|
348
|
+
disabled={i <= 1}
|
|
349
|
+
className="p-0.5 rounded hover:bg-muted disabled:opacity-30"
|
|
350
|
+
aria-label="Move step up"
|
|
351
|
+
>
|
|
352
|
+
<ArrowUp className="h-3 w-3" />
|
|
353
|
+
</button>
|
|
354
|
+
<button
|
|
355
|
+
type="button"
|
|
356
|
+
onClick={() => moveStep(i, 1)}
|
|
357
|
+
disabled={i === 0 || i === steps.length - 1}
|
|
358
|
+
className="p-0.5 rounded hover:bg-muted disabled:opacity-30"
|
|
359
|
+
aria-label="Move step down"
|
|
360
|
+
>
|
|
361
|
+
<ArrowDown className="h-3 w-3" />
|
|
362
|
+
</button>
|
|
363
|
+
</div>
|
|
364
|
+
)}
|
|
365
|
+
|
|
366
|
+
{/* Remove button */}
|
|
367
|
+
{i > 0 && steps.length > 2 && (
|
|
368
|
+
<button
|
|
369
|
+
type="button"
|
|
370
|
+
onClick={() => removeStep(i)}
|
|
371
|
+
className="p-0.5 rounded hover:bg-destructive/10 text-muted-foreground hover:text-destructive"
|
|
372
|
+
aria-label={`Remove step ${step.title}`}
|
|
373
|
+
>
|
|
374
|
+
<Trash2 className="h-3 w-3" />
|
|
375
|
+
</button>
|
|
376
|
+
)}
|
|
377
|
+
</div>
|
|
378
|
+
))}
|
|
379
|
+
</div>
|
|
380
|
+
</FormSectionCard>
|
|
381
|
+
|
|
382
|
+
{/* Pattern-specific config */}
|
|
383
|
+
{(pattern === "loop" || pattern === "swarm") && (
|
|
384
|
+
<FormSectionCard icon={Settings} title="Configuration">
|
|
385
|
+
{pattern === "loop" && (
|
|
386
|
+
<div className="space-y-1.5">
|
|
387
|
+
<Label htmlFor="max-iterations">Max Iterations</Label>
|
|
388
|
+
<Input
|
|
389
|
+
id="max-iterations"
|
|
390
|
+
type="number"
|
|
391
|
+
min={1}
|
|
392
|
+
max={50}
|
|
393
|
+
value={maxIterations}
|
|
394
|
+
onChange={(e) => setMaxIterations(parseInt(e.target.value, 10) || 5)}
|
|
395
|
+
className="w-32"
|
|
396
|
+
/>
|
|
397
|
+
</div>
|
|
398
|
+
)}
|
|
399
|
+
{pattern === "swarm" && (
|
|
400
|
+
<div className="space-y-1.5">
|
|
401
|
+
<Label htmlFor="concurrency-limit">Worker Concurrency Limit</Label>
|
|
402
|
+
<Input
|
|
403
|
+
id="concurrency-limit"
|
|
404
|
+
type="number"
|
|
405
|
+
min={1}
|
|
406
|
+
max={5}
|
|
407
|
+
value={workerConcurrencyLimit}
|
|
408
|
+
onChange={(e) => setWorkerConcurrencyLimit(parseInt(e.target.value, 10) || 2)}
|
|
409
|
+
className="w-32"
|
|
410
|
+
/>
|
|
411
|
+
</div>
|
|
412
|
+
)}
|
|
413
|
+
<div className="flex items-center gap-2 text-xs text-muted-foreground opacity-50 mt-2">
|
|
414
|
+
<input type="checkbox" disabled />
|
|
415
|
+
<span>Save as Blueprint (coming soon)</span>
|
|
416
|
+
</div>
|
|
417
|
+
</FormSectionCard>
|
|
418
|
+
)}
|
|
419
|
+
|
|
420
|
+
{/* Action bar */}
|
|
421
|
+
<div className="flex justify-end gap-2 pt-2">
|
|
422
|
+
<Button
|
|
423
|
+
variant="ghost"
|
|
424
|
+
onClick={navigateBackToTask}
|
|
425
|
+
disabled={submitting}
|
|
426
|
+
>
|
|
427
|
+
Dismiss
|
|
428
|
+
</Button>
|
|
429
|
+
<Button
|
|
430
|
+
variant="outline"
|
|
431
|
+
onClick={() => handleSubmit(false)}
|
|
432
|
+
disabled={submitting || !workflowName.trim() || steps.length < 2}
|
|
433
|
+
>
|
|
434
|
+
{submitting ? <Loader2 className="h-3 w-3 mr-1 animate-spin" /> : null}
|
|
435
|
+
Accept
|
|
436
|
+
</Button>
|
|
437
|
+
<Button
|
|
438
|
+
onClick={() => handleSubmit(true)}
|
|
439
|
+
disabled={submitting || !workflowName.trim() || steps.length < 2}
|
|
440
|
+
>
|
|
441
|
+
{submitting ? <Loader2 className="h-3 w-3 mr-1 animate-spin" /> : null}
|
|
442
|
+
Accept & Run
|
|
443
|
+
</Button>
|
|
444
|
+
</div>
|
|
445
|
+
</div>
|
|
446
|
+
);
|
|
447
|
+
}
|
|
@@ -315,12 +315,17 @@ describe("executeClaudeTask", () => {
|
|
|
315
315
|
|
|
316
316
|
await executeClaudeTask("task-1");
|
|
317
317
|
|
|
318
|
-
//
|
|
318
|
+
// F1: prompt contains only user task text (title fallback); system instructions in systemPrompt
|
|
319
319
|
expect(mockQuery).toHaveBeenCalledWith(
|
|
320
320
|
expect.objectContaining({
|
|
321
|
-
prompt: "
|
|
321
|
+
prompt: "Test Task",
|
|
322
322
|
})
|
|
323
323
|
);
|
|
324
|
+
// System instructions (including output instructions) are in the systemPrompt option
|
|
325
|
+
const callOptions = mockQuery.mock.calls[0][0].options;
|
|
326
|
+
expect(callOptions.systemPrompt).toBeDefined();
|
|
327
|
+
expect(callOptions.maxTurns).toBeDefined();
|
|
328
|
+
expect(callOptions.maxBudgetUsd).toBeDefined();
|
|
324
329
|
});
|
|
325
330
|
});
|
|
326
331
|
|