stagent 0.6.2 → 0.6.3

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 (68) hide show
  1. package/dist/cli.js +47 -1
  2. package/package.json +1 -2
  3. package/src/app/api/documents/[id]/route.ts +5 -1
  4. package/src/app/api/documents/[id]/versions/route.ts +53 -0
  5. package/src/app/api/documents/route.ts +5 -1
  6. package/src/app/api/projects/[id]/documents/route.ts +124 -0
  7. package/src/app/api/projects/[id]/route.ts +35 -3
  8. package/src/app/api/projects/__tests__/delete-project.test.ts +1 -0
  9. package/src/app/api/schedules/route.ts +19 -1
  10. package/src/app/api/tasks/[id]/route.ts +37 -2
  11. package/src/app/api/tasks/[id]/siblings/route.ts +48 -0
  12. package/src/app/api/tasks/route.ts +8 -9
  13. package/src/app/api/workflows/[id]/documents/route.ts +209 -0
  14. package/src/app/api/workflows/[id]/execute/route.ts +6 -2
  15. package/src/app/api/workflows/[id]/route.ts +16 -3
  16. package/src/app/api/workflows/[id]/status/route.ts +18 -2
  17. package/src/app/api/workflows/route.ts +13 -2
  18. package/src/app/documents/page.tsx +5 -1
  19. package/src/app/layout.tsx +0 -1
  20. package/src/app/manifest.ts +3 -3
  21. package/src/app/projects/[id]/page.tsx +62 -2
  22. package/src/components/documents/document-chip-bar.tsx +17 -1
  23. package/src/components/documents/document-detail-view.tsx +51 -0
  24. package/src/components/documents/document-grid.tsx +5 -0
  25. package/src/components/documents/document-table.tsx +4 -0
  26. package/src/components/documents/types.ts +3 -0
  27. package/src/components/projects/project-form-sheet.tsx +133 -2
  28. package/src/components/schedules/schedule-form.tsx +113 -1
  29. package/src/components/shared/document-picker-sheet.tsx +283 -0
  30. package/src/components/tasks/task-card.tsx +8 -1
  31. package/src/components/tasks/task-create-panel.tsx +137 -14
  32. package/src/components/tasks/task-detail-view.tsx +47 -0
  33. package/src/components/tasks/task-edit-dialog.tsx +125 -2
  34. package/src/components/workflows/workflow-form-view.tsx +231 -7
  35. package/src/components/workflows/workflow-kanban-card.tsx +8 -1
  36. package/src/components/workflows/workflow-list.tsx +90 -45
  37. package/src/components/workflows/workflow-status-view.tsx +167 -22
  38. package/src/lib/__tests__/npx-process-cwd.test.ts +17 -2
  39. package/src/lib/agents/profiles/registry.ts +6 -3
  40. package/src/lib/book/__tests__/chapter-slugs.test.ts +80 -0
  41. package/src/lib/book/chapter-generator.ts +4 -19
  42. package/src/lib/book/chapter-mapping.ts +17 -0
  43. package/src/lib/book/content.ts +5 -16
  44. package/src/lib/book/update-detector.ts +3 -16
  45. package/src/lib/chat/engine.ts +1 -0
  46. package/src/lib/chat/system-prompt.ts +9 -1
  47. package/src/lib/chat/tool-catalog.ts +1 -0
  48. package/src/lib/chat/tools/settings-tools.ts +109 -0
  49. package/src/lib/chat/tools/workflow-tools.ts +145 -2
  50. package/src/lib/data/clear.ts +12 -0
  51. package/src/lib/db/bootstrap.ts +48 -0
  52. package/src/lib/db/migrations/0016_add_workflow_document_inputs.sql +13 -0
  53. package/src/lib/db/migrations/0017_add_document_picker_tables.sql +25 -0
  54. package/src/lib/db/migrations/0018_add_workflow_run_number.sql +2 -0
  55. package/src/lib/db/schema.ts +77 -0
  56. package/src/lib/docs/reader.ts +2 -3
  57. package/src/lib/documents/context-builder.ts +75 -2
  58. package/src/lib/documents/document-resolver.ts +119 -0
  59. package/src/lib/documents/processors/spreadsheet.ts +2 -1
  60. package/src/lib/schedules/scheduler.ts +31 -1
  61. package/src/lib/utils/app-root.ts +20 -0
  62. package/src/lib/validators/__tests__/task.test.ts +43 -10
  63. package/src/lib/validators/task.ts +7 -1
  64. package/src/lib/workflows/blueprints/registry.ts +3 -3
  65. package/src/lib/workflows/engine.ts +24 -8
  66. package/src/lib/workflows/types.ts +14 -0
  67. package/public/icon.svg +0 -13
  68. package/src/components/tasks/file-upload.tsx +0 -120
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
- import { useState, useEffect } from "react";
4
- import { useRouter } from "next/navigation";
3
+ import { useState, useEffect, useCallback } from "react";
4
+ import { useRouter, useSearchParams } from "next/navigation";
5
5
  import { Button } from "@/components/ui/button";
6
6
  import { Input } from "@/components/ui/input";
7
7
  import { Textarea } from "@/components/ui/textarea";
@@ -27,6 +27,8 @@ import {
27
27
  ArrowDown,
28
28
  Brain,
29
29
  ShieldCheck,
30
+ FileText,
31
+ X,
30
32
  } from "lucide-react";
31
33
  import { toast } from "sonner";
32
34
  import { FormSectionCard } from "@/components/shared/form-section-card";
@@ -47,6 +49,8 @@ import {
47
49
  MAX_PARALLEL_BRANCHES,
48
50
  MIN_PARALLEL_BRANCHES,
49
51
  } from "@/lib/workflows/parallel";
52
+ import { DocumentPickerSheet } from "@/components/shared/document-picker-sheet";
53
+ import { getFileIcon, formatSize } from "@/components/documents/utils";
50
54
  import {
51
55
  DEFAULT_SWARM_CONCURRENCY_LIMIT,
52
56
  MAX_SWARM_WORKERS,
@@ -284,6 +288,15 @@ function normalizeSwarmSteps(
284
288
  );
285
289
  }
286
290
 
291
+ const PATTERN_LABELS: Record<string, string> = {
292
+ sequence: "Sequence",
293
+ "planner-executor": "Planner → Executor",
294
+ checkpoint: "Checkpoint",
295
+ loop: "Autonomous Loop",
296
+ parallel: "Parallel Research",
297
+ swarm: "Multi-Agent Swarm",
298
+ };
299
+
287
300
  const PATTERN_ICONS: Record<string, React.ReactNode> = {
288
301
  sequence: <ArrowDown className="h-3.5 w-3.5 text-muted-foreground" />,
289
302
  "planner-executor": <Brain className="h-3.5 w-3.5 text-muted-foreground" />,
@@ -304,6 +317,7 @@ export function WorkflowFormView({
304
317
  runtimeOptions.map((runtime) => [runtime.id, runtime.label])
305
318
  );
306
319
  const router = useRouter();
320
+ const searchParams = useSearchParams();
307
321
  const mode = workflow ? (clone ? "clone" : "edit") : "create";
308
322
 
309
323
  const [name, setName] = useState("");
@@ -313,6 +327,13 @@ export function WorkflowFormView({
313
327
  const [loading, setLoading] = useState(false);
314
328
  const [error, setError] = useState<string | null>(null);
315
329
 
330
+ // Document pool state
331
+ const [selectedDocIds, setSelectedDocIds] = useState<Set<string>>(new Set());
332
+ const [selectedDocs, setSelectedDocs] = useState<
333
+ Array<{ id: string; originalName: string; mimeType: string; size: number }>
334
+ >([]);
335
+ const [pickerOpen, setPickerOpen] = useState(false);
336
+
316
337
  // Loop-specific state
317
338
  const [loopPrompt, setLoopPrompt] = useState("");
318
339
  const [maxIterations, setMaxIterations] = useState(5);
@@ -325,6 +346,119 @@ export function WorkflowFormView({
325
346
  DEFAULT_SWARM_CONCURRENCY_LIMIT
326
347
  );
327
348
 
349
+ // Pre-populate documents from URL params (e.g., from Output Dock chain)
350
+ useEffect(() => {
351
+ const inputDocsParam = searchParams.get("inputDocs");
352
+ if (inputDocsParam) {
353
+ const docIds = inputDocsParam.split(",").filter(Boolean);
354
+ if (docIds.length > 0) {
355
+ setSelectedDocIds(new Set(docIds));
356
+ // Fetch document metadata for display
357
+ Promise.all(
358
+ docIds.map((id) =>
359
+ fetch(`/api/documents?id=${id}`)
360
+ .then((r) => r.json())
361
+ .then((docs) =>
362
+ Array.isArray(docs) && docs.length > 0 ? docs[0] : null
363
+ )
364
+ .catch(() => null)
365
+ )
366
+ ).then((results) => {
367
+ setSelectedDocs(
368
+ results.filter(Boolean).map((d: Record<string, unknown>) => ({
369
+ id: d.id as string,
370
+ originalName: d.originalName as string,
371
+ mimeType: d.mimeType as string,
372
+ size: d.size as number,
373
+ }))
374
+ );
375
+ });
376
+ }
377
+ }
378
+ }, [searchParams]);
379
+
380
+ // Handle document picker confirmation
381
+ const handleDocPickerConfirm = useCallback(
382
+ (ids: string[]) => {
383
+ setSelectedDocIds(new Set(ids));
384
+ // Fetch metadata for newly selected docs
385
+ const newIds = ids.filter(
386
+ (id) => !selectedDocs.some((d) => d.id === id)
387
+ );
388
+ if (newIds.length > 0) {
389
+ fetch(`/api/documents?projectId=${projectId}&status=ready`)
390
+ .then((r) => r.json())
391
+ .then((allDocs: Array<Record<string, unknown>>) => {
392
+ const idSet = new Set(ids);
393
+ setSelectedDocs(
394
+ allDocs
395
+ .filter((d) => idSet.has(d.id as string))
396
+ .map((d) => ({
397
+ id: d.id as string,
398
+ originalName: d.originalName as string,
399
+ mimeType: d.mimeType as string,
400
+ size: d.size as number,
401
+ }))
402
+ );
403
+ })
404
+ .catch(() => {});
405
+ } else {
406
+ // Remove deselected docs
407
+ setSelectedDocs((prev) =>
408
+ prev.filter((d) => ids.includes(d.id))
409
+ );
410
+ }
411
+ },
412
+ [projectId, selectedDocs]
413
+ );
414
+
415
+ function removeDocument(id: string) {
416
+ setSelectedDocIds((prev) => {
417
+ const next = new Set(prev);
418
+ next.delete(id);
419
+ return next;
420
+ });
421
+ setSelectedDocs((prev) => prev.filter((d) => d.id !== id));
422
+ }
423
+
424
+ // Load existing document bindings when editing
425
+ useEffect(() => {
426
+ if (!workflow || clone) return;
427
+ fetch(`/api/workflows/${workflow.id}/documents`)
428
+ .then((r) => r.json())
429
+ .then((bindings: Array<{ documentId: string; document: { id: string; originalName: string; mimeType: string; size: number } | null }>) => {
430
+ const docs = bindings
431
+ .filter((b) => b.document)
432
+ .map((b) => b.document!);
433
+ if (docs.length > 0) {
434
+ setSelectedDocIds(new Set(docs.map((d) => d.id)));
435
+ setSelectedDocs(docs);
436
+ }
437
+ })
438
+ .catch(() => {});
439
+ }, [workflow, clone]);
440
+
441
+ // Auto-populate project default documents for new workflows
442
+ useEffect(() => {
443
+ if (workflow || !projectId) return; // Only for create mode with a project selected
444
+ fetch(`/api/projects/${projectId}/documents`)
445
+ .then((r) => r.json())
446
+ .then((docs: Array<Record<string, unknown>>) => {
447
+ if (Array.isArray(docs) && docs.length > 0) {
448
+ setSelectedDocIds(new Set(docs.map((d) => d.id as string)));
449
+ setSelectedDocs(
450
+ docs.map((d) => ({
451
+ id: d.id as string,
452
+ originalName: d.originalName as string,
453
+ mimeType: d.mimeType as string,
454
+ size: d.size as number,
455
+ }))
456
+ );
457
+ }
458
+ })
459
+ .catch(() => {});
460
+ }, [projectId, workflow]);
461
+
328
462
  // Pre-populate form for edit/clone
329
463
  useEffect(() => {
330
464
  if (!workflow) return;
@@ -723,6 +857,23 @@ export function WorkflowFormView({
723
857
  });
724
858
 
725
859
  if (res.ok) {
860
+ const data = await res.json().catch(() => null);
861
+ const workflowId = isEdit ? workflow.id : data?.id;
862
+
863
+ // Attach pool documents to the workflow via junction table
864
+ if (workflowId && selectedDocIds.size > 0) {
865
+ await fetch(`/api/workflows/${workflowId}/documents`, {
866
+ method: "POST",
867
+ headers: { "Content-Type": "application/json" },
868
+ body: JSON.stringify({
869
+ documentIds: [...selectedDocIds],
870
+ }),
871
+ }).catch(() => {
872
+ // Non-blocking — workflow was created, docs attachment is best-effort
873
+ console.warn("[workflow-form] Failed to attach pool documents");
874
+ });
875
+ }
876
+
726
877
  toast.success(
727
878
  mode === "edit"
728
879
  ? "Workflow updated"
@@ -734,9 +885,8 @@ export function WorkflowFormView({
734
885
  if (isEdit) {
735
886
  router.push(`/workflows/${workflow.id}`);
736
887
  } else {
737
- const data = await res.json().catch(() => null);
738
- if (data?.id) {
739
- router.push(`/workflows/${data.id}`);
888
+ if (workflowId) {
889
+ router.push(`/workflows/${workflowId}`);
740
890
  } else {
741
891
  router.push("/workflows");
742
892
  }
@@ -963,15 +1113,20 @@ export function WorkflowFormView({
963
1113
  </div>
964
1114
  <div className="space-y-1.5">
965
1115
  <Label>Pattern</Label>
1116
+ {mode === "edit" ? (
1117
+ <div className="flex h-9 w-fit items-center gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm opacity-50 cursor-not-allowed">
1118
+ {PATTERN_ICONS[pattern]}
1119
+ {PATTERN_LABELS[pattern] ?? pattern}
1120
+ </div>
1121
+ ) : (
966
1122
  <Select
967
1123
  value={pattern}
968
1124
  onValueChange={(value) =>
969
1125
  setPattern(value as WorkflowPattern)
970
1126
  }
971
- disabled={mode === "edit"}
972
1127
  >
973
1128
  <SelectTrigger>
974
- <SelectValue />
1129
+ <SelectValue placeholder="Select pattern" />
975
1130
  </SelectTrigger>
976
1131
  <SelectContent>
977
1132
  <SelectItem value="sequence">
@@ -1012,6 +1167,7 @@ export function WorkflowFormView({
1012
1167
  </SelectItem>
1013
1168
  </SelectContent>
1014
1169
  </Select>
1170
+ )}
1015
1171
  <p className="text-xs text-muted-foreground">How steps execute</p>
1016
1172
  </div>
1017
1173
  {projects.length > 0 && (
@@ -1041,6 +1197,74 @@ export function WorkflowFormView({
1041
1197
  </div>
1042
1198
  </FormSectionCard>
1043
1199
 
1200
+ {/* Input Documents — Document Pool */}
1201
+ {projectId && (
1202
+ <FormSectionCard
1203
+ icon={FileText}
1204
+ title="Input Documents"
1205
+ hint="Attach documents from the project pool as context for this workflow"
1206
+ >
1207
+ <div className="space-y-3">
1208
+ {selectedDocs.length > 0 && (
1209
+ <div className="flex flex-wrap gap-2">
1210
+ {selectedDocs.map((doc) => {
1211
+ const Icon = getFileIcon(doc.mimeType);
1212
+ return (
1213
+ <Badge
1214
+ key={doc.id}
1215
+ variant="secondary"
1216
+ className="flex items-center gap-1.5 pl-2 pr-1 py-1"
1217
+ >
1218
+ <Icon className="h-3 w-3" />
1219
+ <span className="text-xs max-w-[180px] truncate">
1220
+ {doc.originalName}
1221
+ </span>
1222
+ <span className="text-[10px] text-muted-foreground">
1223
+ {formatSize(doc.size)}
1224
+ </span>
1225
+ <button
1226
+ type="button"
1227
+ onClick={() => removeDocument(doc.id)}
1228
+ className="ml-0.5 rounded-full p-0.5 hover:bg-muted transition-colors"
1229
+ aria-label={`Remove ${doc.originalName}`}
1230
+ >
1231
+ <X className="h-3 w-3" />
1232
+ </button>
1233
+ </Badge>
1234
+ );
1235
+ })}
1236
+ </div>
1237
+ )}
1238
+ <Button
1239
+ type="button"
1240
+ variant="outline"
1241
+ size="sm"
1242
+ onClick={() => setPickerOpen(true)}
1243
+ className="gap-1.5"
1244
+ >
1245
+ <Plus className="h-3.5 w-3.5" />
1246
+ {selectedDocs.length > 0
1247
+ ? "Add More Documents"
1248
+ : "Attach Documents"}
1249
+ </Button>
1250
+ {selectedDocs.length > 0 && (
1251
+ <p className="text-xs text-muted-foreground">
1252
+ {selectedDocs.length} document{selectedDocs.length !== 1 ? "s" : ""} will be injected as context for all steps
1253
+ </p>
1254
+ )}
1255
+ </div>
1256
+
1257
+ <DocumentPickerSheet
1258
+ open={pickerOpen}
1259
+ onOpenChange={setPickerOpen}
1260
+ projectId={projectId}
1261
+ selectedIds={selectedDocIds}
1262
+ onConfirm={handleDocPickerConfirm}
1263
+ groupBy="workflow"
1264
+ />
1265
+ </FormSectionCard>
1266
+ )}
1267
+
1044
1268
  {isLoop && (
1045
1269
  <FormSectionCard icon={RefreshCw} title="Loop Config">
1046
1270
  <div className="space-y-4">
@@ -3,7 +3,7 @@
3
3
  import Link from "next/link";
4
4
  import { Card } from "@/components/ui/card";
5
5
  import { Badge } from "@/components/ui/badge";
6
- import { Workflow, Loader2 } from "lucide-react";
6
+ import { Workflow, Loader2, FileText } from "lucide-react";
7
7
  import { workflowStatusVariant, patternLabels } from "@/lib/constants/status-colors";
8
8
 
9
9
  export interface WorkflowKanbanItem {
@@ -16,6 +16,7 @@ export interface WorkflowKanbanItem {
16
16
  projectName?: string;
17
17
  stepProgress: { current: number; total: number };
18
18
  currentStepName?: string;
19
+ outputDocCount?: number;
19
20
  createdAt: string;
20
21
  updatedAt?: string;
21
22
  }
@@ -112,6 +113,12 @@ export function WorkflowKanbanCard({ workflow }: WorkflowKanbanCardProps) {
112
113
  >
113
114
  {workflow.status}
114
115
  </Badge>
116
+ {workflow.outputDocCount != null && workflow.outputDocCount > 0 && (
117
+ <span className="text-[11px] text-muted-foreground flex items-center gap-0.5">
118
+ <FileText className="h-3 w-3" />
119
+ {workflow.outputDocCount}
120
+ </span>
121
+ )}
115
122
  <div className="flex-1" />
116
123
  <span className="text-[11px] text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity">
117
124
  View →
@@ -10,6 +10,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
10
10
  import { ConfirmDialog } from "@/components/shared/confirm-dialog";
11
11
  import { EmptyState } from "@/components/shared/empty-state";
12
12
  import { GitBranch, Pencil, Copy, RotateCcw, Trash2, FileCog, Play } from "lucide-react";
13
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
13
14
  import { toast } from "sonner";
14
15
  import { workflowStatusVariant, patternLabels } from "@/lib/constants/status-colors";
15
16
  import { IconCircle, getWorkflowIconFromName } from "@/lib/constants/card-icons";
@@ -23,6 +24,9 @@ interface Workflow {
23
24
  definition: string;
24
25
  createdAt: string;
25
26
  updatedAt: string;
27
+ taskCount?: number;
28
+ outputDocCount?: number;
29
+ runNumber?: number;
26
30
  }
27
31
 
28
32
  interface WorkflowListProps {
@@ -175,6 +179,18 @@ export function WorkflowList({ projects }: WorkflowListProps) {
175
179
  <span>{patternLabels[pattern] ?? pattern}</span>
176
180
  <span>&middot;</span>
177
181
  <span>{stepCount} step{stepCount !== 1 ? "s" : ""}</span>
182
+ {wf.taskCount != null && wf.taskCount > 0 && (
183
+ <>
184
+ <span className="text-muted-foreground">&middot;</span>
185
+ <span>{wf.taskCount} task{wf.taskCount !== 1 ? "s" : ""}</span>
186
+ </>
187
+ )}
188
+ {wf.outputDocCount != null && wf.outputDocCount > 0 && (
189
+ <>
190
+ <span className="text-muted-foreground">&middot;</span>
191
+ <span>{wf.outputDocCount} doc{wf.outputDocCount !== 1 ? "s" : ""}</span>
192
+ </>
193
+ )}
178
194
  </div>
179
195
  {promptPreview && (
180
196
  <p className="text-xs text-muted-foreground line-clamp-2 mt-1.5">
@@ -182,53 +198,82 @@ export function WorkflowList({ projects }: WorkflowListProps) {
182
198
  </p>
183
199
  )}
184
200
  <div className="flex items-center justify-between mt-3">
185
- <Badge variant={workflowStatusVariant[wf.status] ?? "secondary"}>
186
- {wf.status}
187
- </Badge>
188
- <div className="flex items-center gap-1">
189
- {wf.status === "draft" && (
190
- <Button
191
- variant="ghost"
192
- size="icon"
193
- className="h-7 w-7"
194
- aria-label="Edit workflow"
195
- onClick={(e) => { e.stopPropagation(); router.push(`/workflows/${wf.id}/edit`); }}
196
- >
197
- <Pencil className="h-3.5 w-3.5" />
198
- </Button>
199
- )}
200
- <Button
201
- variant="ghost"
202
- size="icon"
203
- className="h-7 w-7"
204
- aria-label="Clone workflow"
205
- onClick={(e) => { e.stopPropagation(); router.push(`/workflows/${wf.id}/edit?clone=true`); }}
206
- >
207
- <Copy className="h-3.5 w-3.5" />
208
- </Button>
209
- {(wf.status === "completed" || wf.status === "failed") && (
210
- <Button
211
- variant="ghost"
212
- size="icon"
213
- className="h-7 w-7"
214
- aria-label="Re-run workflow"
215
- onClick={(e) => { e.stopPropagation(); handleRerun(wf.id); }}
216
- >
217
- <RotateCcw className="h-3.5 w-3.5" />
218
- </Button>
219
- )}
220
- {wf.status !== "active" && (
221
- <Button
222
- variant="ghost"
223
- size="icon"
224
- className="h-7 w-7 text-destructive"
225
- aria-label="Delete workflow"
226
- onClick={(e) => { e.stopPropagation(); setConfirmDeleteId(wf.id); }}
227
- >
228
- <Trash2 className="h-3.5 w-3.5" />
229
- </Button>
201
+ <div className="flex items-center gap-2">
202
+ <Badge variant={workflowStatusVariant[wf.status] ?? "secondary"}>
203
+ {wf.status}
204
+ </Badge>
205
+ {wf.runNumber != null && wf.runNumber > 0 && (
206
+ <Badge variant="outline" className="text-[10px] font-normal">
207
+ Run #{wf.runNumber}
208
+ </Badge>
230
209
  )}
231
210
  </div>
211
+ <TooltipProvider>
212
+ <div className="flex items-center gap-1">
213
+ {(wf.status === "draft" || wf.status === "completed" || wf.status === "failed") && (
214
+ <Tooltip>
215
+ <TooltipTrigger asChild>
216
+ <Button
217
+ variant="ghost"
218
+ size="icon"
219
+ className="h-7 w-7"
220
+ aria-label="Edit workflow"
221
+ onClick={(e) => { e.stopPropagation(); router.push(`/workflows/${wf.id}/edit`); }}
222
+ >
223
+ <Pencil className="h-3.5 w-3.5" />
224
+ </Button>
225
+ </TooltipTrigger>
226
+ <TooltipContent>Edit</TooltipContent>
227
+ </Tooltip>
228
+ )}
229
+ <Tooltip>
230
+ <TooltipTrigger asChild>
231
+ <Button
232
+ variant="ghost"
233
+ size="icon"
234
+ className="h-7 w-7"
235
+ aria-label="Clone workflow"
236
+ onClick={(e) => { e.stopPropagation(); router.push(`/workflows/${wf.id}/edit?clone=true`); }}
237
+ >
238
+ <Copy className="h-3.5 w-3.5" />
239
+ </Button>
240
+ </TooltipTrigger>
241
+ <TooltipContent>Clone</TooltipContent>
242
+ </Tooltip>
243
+ {(wf.status === "completed" || wf.status === "failed") && (
244
+ <Tooltip>
245
+ <TooltipTrigger asChild>
246
+ <Button
247
+ variant="ghost"
248
+ size="icon"
249
+ className="h-7 w-7"
250
+ aria-label="Re-run workflow"
251
+ onClick={(e) => { e.stopPropagation(); handleRerun(wf.id); }}
252
+ >
253
+ <RotateCcw className="h-3.5 w-3.5" />
254
+ </Button>
255
+ </TooltipTrigger>
256
+ <TooltipContent>Re-run</TooltipContent>
257
+ </Tooltip>
258
+ )}
259
+ {wf.status !== "active" && (
260
+ <Tooltip>
261
+ <TooltipTrigger asChild>
262
+ <Button
263
+ variant="ghost"
264
+ size="icon"
265
+ className="h-7 w-7 text-destructive"
266
+ aria-label="Delete workflow"
267
+ onClick={(e) => { e.stopPropagation(); setConfirmDeleteId(wf.id); }}
268
+ >
269
+ <Trash2 className="h-3.5 w-3.5" />
270
+ </Button>
271
+ </TooltipTrigger>
272
+ <TooltipContent>Delete</TooltipContent>
273
+ </Tooltip>
274
+ )}
275
+ </div>
276
+ </TooltipProvider>
232
277
  </div>
233
278
  </CardContent>
234
279
  </Card>