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,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useState, useEffect } from "react";
3
+ import { useState, useEffect, useCallback } from "react";
4
4
  import { useRouter } from "next/navigation";
5
5
  import { Button } from "@/components/ui/button";
6
6
  import { Input } from "@/components/ui/input";
@@ -14,11 +14,13 @@ import {
14
14
  SelectValue,
15
15
  } from "@/components/ui/select";
16
16
  import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
17
- import { Bot, FileText, Settings, Paperclip } from "lucide-react";
17
+ import { Bot, FileText, Settings, Paperclip, Plus, X } from "lucide-react";
18
18
  import { toast } from "sonner";
19
19
  import { AIAssistPanel } from "./ai-assist-panel";
20
- import { FileUpload } from "./file-upload";
21
20
  import type { TaskAssistResponse } from "@/lib/agents/runtime/task-assist-types";
21
+ import { Badge } from "@/components/ui/badge";
22
+ import { DocumentPickerSheet } from "@/components/shared/document-picker-sheet";
23
+ import { getFileIcon, formatSize } from "@/components/documents/utils";
22
24
  import { FormSectionCard } from "@/components/shared/form-section-card";
23
25
  import {
24
26
  saveAssistState,
@@ -45,12 +47,11 @@ type ProfileOption = Pick<
45
47
  isBuiltin?: boolean;
46
48
  };
47
49
 
48
- interface UploadedFile {
50
+ interface SelectedDoc {
49
51
  id: string;
50
- filename: string;
51
52
  originalName: string;
53
+ mimeType: string;
52
54
  size: number;
53
- type: string;
54
55
  }
55
56
 
56
57
  interface TaskCreatePanelProps {
@@ -78,7 +79,9 @@ export function TaskCreatePanel({ projects, defaultProjectId }: TaskCreatePanelP
78
79
  const [priority, setPriority] = useState("2");
79
80
  const [agentProfile, setAgentProfile] = useState<string>("");
80
81
  const [profiles, setProfiles] = useState<ProfileOption[]>([]);
81
- const [uploads, setUploads] = useState<UploadedFile[]>([]);
82
+ const [selectedDocIds, setSelectedDocIds] = useState<Set<string>>(new Set());
83
+ const [selectedDocs, setSelectedDocs] = useState<SelectedDoc[]>([]);
84
+ const [pickerOpen, setPickerOpen] = useState(false);
82
85
  const [loading, setLoading] = useState(false);
83
86
  const [error, setError] = useState<string | null>(null);
84
87
  const [suggestedRuntime, setSuggestedRuntime] = useState<{
@@ -93,6 +96,31 @@ export function TaskCreatePanel({ projects, defaultProjectId }: TaskCreatePanelP
93
96
  .catch(() => {});
94
97
  }, []);
95
98
 
99
+ // Load project default documents when project changes
100
+ useEffect(() => {
101
+ if (!projectId) {
102
+ setSelectedDocIds(new Set());
103
+ setSelectedDocs([]);
104
+ return;
105
+ }
106
+ fetch(`/api/projects/${projectId}/documents`)
107
+ .then((r) => r.json())
108
+ .then((docs: Array<Record<string, unknown>>) => {
109
+ if (Array.isArray(docs) && docs.length > 0) {
110
+ setSelectedDocIds(new Set(docs.map((d) => d.id as string)));
111
+ setSelectedDocs(
112
+ docs.map((d) => ({
113
+ id: d.id as string,
114
+ originalName: d.originalName as string,
115
+ mimeType: d.mimeType as string,
116
+ size: d.size as number,
117
+ }))
118
+ );
119
+ }
120
+ })
121
+ .catch(() => {});
122
+ }, [projectId]);
123
+
96
124
  // Restore form state when returning from workflow confirmation
97
125
  useEffect(() => {
98
126
  const params = new URLSearchParams(window.location.search);
@@ -143,6 +171,42 @@ export function TaskCreatePanel({ projects, defaultProjectId }: TaskCreatePanelP
143
171
  }`
144
172
  : null;
145
173
 
174
+ const handleDocPickerConfirm = useCallback(
175
+ (ids: string[]) => {
176
+ setSelectedDocIds(new Set(ids));
177
+ // Fetch metadata for newly selected docs
178
+ const newIds = ids.filter(
179
+ (id) => !selectedDocs.some((d) => d.id === id)
180
+ );
181
+ if (newIds.length > 0) {
182
+ const params = new URLSearchParams({ status: "ready" });
183
+ if (projectId) params.set("projectId", projectId);
184
+ fetch(`/api/documents?${params}`)
185
+ .then((r) => r.json())
186
+ .then((allDocs: Array<Record<string, unknown>>) => {
187
+ const idSet = new Set(ids);
188
+ setSelectedDocs(
189
+ allDocs
190
+ .filter((d) => idSet.has(d.id as string))
191
+ .map((d) => ({
192
+ id: d.id as string,
193
+ originalName: d.originalName as string,
194
+ mimeType: d.mimeType as string,
195
+ size: d.size as number,
196
+ }))
197
+ );
198
+ })
199
+ .catch(() => {});
200
+ } else {
201
+ // Remove deselected docs
202
+ setSelectedDocs((prev) =>
203
+ prev.filter((d) => ids.includes(d.id))
204
+ );
205
+ }
206
+ },
207
+ [projectId, selectedDocs]
208
+ );
209
+
146
210
  async function handleSubmit(e: React.FormEvent) {
147
211
  e.preventDefault();
148
212
  if (!title.trim()) return;
@@ -163,7 +227,7 @@ export function TaskCreatePanel({ projects, defaultProjectId }: TaskCreatePanelP
163
227
  priority: parseInt(priority, 10),
164
228
  assignedAgent: assignedAgent || undefined,
165
229
  agentProfile: agentProfile || undefined,
166
- fileIds: uploads.length > 0 ? uploads.map((f) => f.id) : undefined,
230
+ documentIds: selectedDocIds.size > 0 ? [...selectedDocIds] : undefined,
167
231
  }),
168
232
  });
169
233
  if (res.ok) {
@@ -240,7 +304,7 @@ export function TaskCreatePanel({ projects, defaultProjectId }: TaskCreatePanelP
240
304
  ))}
241
305
  </SelectContent>
242
306
  </Select>
243
- <p className="text-xs text-muted-foreground">Working directory</p>
307
+ <p className="text-xs text-muted-foreground">Working directory · Select a project to scope document context</p>
244
308
  </div>
245
309
  <div className="space-y-1.5">
246
310
  <Label>Priority</Label>
@@ -369,11 +433,70 @@ export function TaskCreatePanel({ projects, defaultProjectId }: TaskCreatePanelP
369
433
  </div>
370
434
  </FormSectionCard>
371
435
 
372
- <FormSectionCard icon={Paperclip} title="Attachments">
373
- <FileUpload
374
- uploads={uploads}
375
- onUploaded={(f) => setUploads((prev) => [...prev, f])}
376
- onRemove={(id) => setUploads((prev) => prev.filter((f) => f.id !== id))}
436
+ <FormSectionCard icon={Paperclip} title="Context Documents">
437
+ <div className="space-y-3">
438
+ {selectedDocs.length > 0 && (
439
+ <div className="flex flex-wrap gap-2">
440
+ {selectedDocs.map((doc) => {
441
+ const Icon = getFileIcon(doc.mimeType);
442
+ return (
443
+ <Badge
444
+ key={doc.id}
445
+ variant="secondary"
446
+ className="flex items-center gap-1.5 pl-2 pr-1 py-1"
447
+ >
448
+ <Icon className="h-3 w-3" />
449
+ <span className="text-xs max-w-[180px] truncate">
450
+ {doc.originalName}
451
+ </span>
452
+ <span className="text-[10px] text-muted-foreground">
453
+ {formatSize(doc.size)}
454
+ </span>
455
+ <button
456
+ type="button"
457
+ onClick={() => {
458
+ setSelectedDocIds((prev) => {
459
+ const next = new Set(prev);
460
+ next.delete(doc.id);
461
+ return next;
462
+ });
463
+ setSelectedDocs((prev) => prev.filter((d) => d.id !== doc.id));
464
+ }}
465
+ className="ml-0.5 rounded-full p-0.5 hover:bg-muted transition-colors"
466
+ aria-label={`Remove ${doc.originalName}`}
467
+ >
468
+ <X className="h-3 w-3" />
469
+ </button>
470
+ </Badge>
471
+ );
472
+ })}
473
+ </div>
474
+ )}
475
+ <Button
476
+ type="button"
477
+ variant="outline"
478
+ size="sm"
479
+ onClick={() => setPickerOpen(true)}
480
+ className="gap-1.5"
481
+ >
482
+ <Plus className="h-3.5 w-3.5" />
483
+ {selectedDocs.length > 0 ? "Add More Documents" : "Select Documents"}
484
+ </Button>
485
+ {selectedDocs.length > 0 && (
486
+ <p className="text-xs text-muted-foreground">
487
+ {selectedDocs.length} document{selectedDocs.length !== 1 ? "s" : ""} will be provided as context
488
+ </p>
489
+ )}
490
+ </div>
491
+
492
+ <DocumentPickerSheet
493
+ open={pickerOpen}
494
+ onOpenChange={setPickerOpen}
495
+ projectId={projectId || null}
496
+ selectedIds={selectedDocIds}
497
+ onConfirm={handleDocPickerConfirm}
498
+ groupBy="project"
499
+ title="Select Context Documents"
377
500
  />
378
501
  </FormSectionCard>
379
502
 
@@ -1,7 +1,10 @@
1
1
  "use client";
2
2
 
3
+ import { useState, useEffect } from "react";
3
4
  import { useRouter } from "next/navigation";
5
+ import Link from "next/link";
4
6
  import { Skeleton } from "@/components/ui/skeleton";
7
+ import { Badge } from "@/components/ui/badge";
5
8
  import { ConfirmDialog } from "@/components/shared/confirm-dialog";
6
9
  import { TaskAttachments } from "./task-attachments";
7
10
  import { TaskChipBar } from "./task-chip-bar";
@@ -18,6 +21,12 @@ interface TaskDetailViewProps {
18
21
 
19
22
  export function TaskDetailView({ taskId, initialTask }: TaskDetailViewProps) {
20
23
  const router = useRouter();
24
+ const [siblings, setSiblings] = useState<Array<{
25
+ id: string;
26
+ title: string;
27
+ status: string;
28
+ createdAt: string;
29
+ }>>([]);
21
30
 
22
31
  const {
23
32
  task,
@@ -47,6 +56,15 @@ export function TaskDetailView({ taskId, initialTask }: TaskDetailViewProps) {
47
56
  onDeleted: () => router.push("/dashboard"),
48
57
  });
49
58
 
59
+ useEffect(() => {
60
+ if (task?.workflowId) {
61
+ fetch(`/api/tasks/${taskId}/siblings`)
62
+ .then((r) => r.ok ? r.json() : [])
63
+ .then(setSiblings)
64
+ .catch(() => {});
65
+ }
66
+ }, [task?.workflowId, taskId]);
67
+
50
68
  if (!loaded) {
51
69
  return (
52
70
  <div className="space-y-4">
@@ -82,6 +100,35 @@ export function TaskDetailView({ taskId, initialTask }: TaskDetailViewProps) {
82
100
 
83
101
  <TaskBentoGrid task={task} docs={docs} />
84
102
 
103
+ {/* Sibling tasks from same workflow run */}
104
+ {siblings.length > 0 && (
105
+ <div className="space-y-2">
106
+ <h3 className="text-sm font-medium text-muted-foreground">
107
+ Related Tasks ({siblings.length})
108
+ </h3>
109
+ <div className="surface-control rounded-lg divide-y divide-border">
110
+ {siblings.map((s) => (
111
+ <Link
112
+ key={s.id}
113
+ href={`/tasks/${s.id}`}
114
+ className="flex items-center gap-3 px-3 py-2 text-xs hover:bg-accent/50 transition-colors"
115
+ >
116
+ <span className={`w-2 h-2 rounded-full shrink-0 ${
117
+ s.status === "completed" ? "bg-status-completed" :
118
+ s.status === "failed" ? "bg-destructive" :
119
+ s.status === "running" ? "bg-status-running" :
120
+ "bg-muted-foreground"
121
+ }`} />
122
+ <span className="truncate flex-1">{s.title}</span>
123
+ <Badge variant="outline" className="text-[10px] capitalize">
124
+ {s.status}
125
+ </Badge>
126
+ </Link>
127
+ ))}
128
+ </div>
129
+ </div>
130
+ )}
131
+
85
132
  {docs.length > 0 && (
86
133
  <div className="surface-card-muted rounded-lg p-4 space-y-4">
87
134
  {inputDocs.length > 0 && (
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useState, useEffect } from "react";
3
+ import { useState, useEffect, useCallback } from "react";
4
4
  import {
5
5
  Dialog,
6
6
  DialogContent,
@@ -19,7 +19,10 @@ import {
19
19
  SelectTrigger,
20
20
  SelectValue,
21
21
  } from "@/components/ui/select";
22
- import { Bot, FileText, AlignLeft } from "lucide-react";
22
+ import { Bot, FileText, AlignLeft, Paperclip, Plus, X } from "lucide-react";
23
+ import { Badge } from "@/components/ui/badge";
24
+ import { DocumentPickerSheet } from "@/components/shared/document-picker-sheet";
25
+ import { getFileIcon, formatSize } from "@/components/documents/utils";
23
26
  import { toast } from "sonner";
24
27
  import {
25
28
  type AgentRuntimeId,
@@ -69,6 +72,9 @@ export function TaskEditDialog({
69
72
  const [assignedAgent, setAssignedAgent] = useState("");
70
73
  const [agentProfile, setAgentProfile] = useState("");
71
74
  const [profiles, setProfiles] = useState<ProfileOption[]>([]);
75
+ const [selectedDocIds, setSelectedDocIds] = useState<Set<string>>(new Set());
76
+ const [selectedDocs, setSelectedDocs] = useState<Array<{ id: string; originalName: string; mimeType: string; size: number }>>([]);
77
+ const [pickerOpen, setPickerOpen] = useState(false);
72
78
  const [loading, setLoading] = useState(false);
73
79
 
74
80
  useEffect(() => {
@@ -85,6 +91,25 @@ export function TaskEditDialog({
85
91
  setPriority(String(task.priority));
86
92
  setAssignedAgent(task.assignedAgent ?? "");
87
93
  setAgentProfile(task.agentProfile ?? "");
94
+ // Load existing documents linked to this task
95
+ fetch(`/api/documents?taskId=${task.id}&status=ready`)
96
+ .then((r) => r.json())
97
+ .then((docs: Array<Record<string, unknown>>) => {
98
+ const ids = new Set(docs.map((d) => d.id as string));
99
+ setSelectedDocIds(ids);
100
+ setSelectedDocs(
101
+ docs.map((d) => ({
102
+ id: d.id as string,
103
+ originalName: d.originalName as string,
104
+ mimeType: d.mimeType as string,
105
+ size: d.size as number,
106
+ }))
107
+ );
108
+ })
109
+ .catch(() => {
110
+ setSelectedDocIds(new Set());
111
+ setSelectedDocs([]);
112
+ });
88
113
  }
89
114
  }, [task]);
90
115
 
@@ -98,6 +123,38 @@ export function TaskEditDialog({
98
123
  }`
99
124
  : null;
100
125
 
126
+ const handleDocPickerConfirm = useCallback(
127
+ (ids: string[]) => {
128
+ setSelectedDocIds(new Set(ids));
129
+ const newIds = ids.filter(
130
+ (id) => !selectedDocs.some((d) => d.id === id)
131
+ );
132
+ if (newIds.length > 0) {
133
+ const params = new URLSearchParams({ status: "ready" });
134
+ if (task?.projectId) params.set("projectId", task.projectId);
135
+ fetch(`/api/documents?${params}`)
136
+ .then((r) => r.json())
137
+ .then((allDocs: Array<Record<string, unknown>>) => {
138
+ const idSet = new Set(ids);
139
+ setSelectedDocs(
140
+ allDocs
141
+ .filter((d) => idSet.has(d.id as string))
142
+ .map((d) => ({
143
+ id: d.id as string,
144
+ originalName: d.originalName as string,
145
+ mimeType: d.mimeType as string,
146
+ size: d.size as number,
147
+ }))
148
+ );
149
+ })
150
+ .catch(() => {});
151
+ } else {
152
+ setSelectedDocs((prev) => prev.filter((d) => ids.includes(d.id)));
153
+ }
154
+ },
155
+ [task?.projectId, selectedDocs]
156
+ );
157
+
101
158
  async function handleSubmit(e: React.FormEvent) {
102
159
  e.preventDefault();
103
160
  if (!task || !title.trim()) return;
@@ -113,6 +170,7 @@ export function TaskEditDialog({
113
170
  priority: parseInt(priority, 10),
114
171
  assignedAgent: assignedAgent || undefined,
115
172
  agentProfile: agentProfile || undefined,
173
+ documentIds: [...selectedDocIds],
116
174
  }),
117
175
  });
118
176
  if (res.ok) {
@@ -263,6 +321,71 @@ export function TaskEditDialog({
263
321
  )}
264
322
  </div>
265
323
  )}
324
+ {/* Context Documents */}
325
+ <div className="space-y-2">
326
+ <Label className="flex items-center gap-1.5">
327
+ <Paperclip className="h-3.5 w-3.5 text-muted-foreground" />
328
+ Context Documents
329
+ </Label>
330
+ {selectedDocs.length > 0 && (
331
+ <div className="flex flex-wrap gap-2">
332
+ {selectedDocs.map((doc) => {
333
+ const Icon = getFileIcon(doc.mimeType);
334
+ return (
335
+ <Badge
336
+ key={doc.id}
337
+ variant="secondary"
338
+ className="flex items-center gap-1.5 pl-2 pr-1 py-1"
339
+ >
340
+ <Icon className="h-3 w-3" />
341
+ <span className="text-xs max-w-[140px] truncate">
342
+ {doc.originalName}
343
+ </span>
344
+ <span className="text-[10px] text-muted-foreground">
345
+ {formatSize(doc.size)}
346
+ </span>
347
+ <button
348
+ type="button"
349
+ onClick={() => {
350
+ setSelectedDocIds((prev) => {
351
+ const next = new Set(prev);
352
+ next.delete(doc.id);
353
+ return next;
354
+ });
355
+ setSelectedDocs((prev) => prev.filter((d) => d.id !== doc.id));
356
+ }}
357
+ className="ml-0.5 rounded-full p-0.5 hover:bg-muted transition-colors"
358
+ aria-label={`Remove ${doc.originalName}`}
359
+ >
360
+ <X className="h-3 w-3" />
361
+ </button>
362
+ </Badge>
363
+ );
364
+ })}
365
+ </div>
366
+ )}
367
+ <Button
368
+ type="button"
369
+ variant="outline"
370
+ size="sm"
371
+ onClick={() => setPickerOpen(true)}
372
+ className="gap-1.5"
373
+ >
374
+ <Plus className="h-3.5 w-3.5" />
375
+ {selectedDocs.length > 0 ? "Add More" : "Select Documents"}
376
+ </Button>
377
+ </div>
378
+
379
+ <DocumentPickerSheet
380
+ open={pickerOpen}
381
+ onOpenChange={setPickerOpen}
382
+ projectId={task?.projectId ?? null}
383
+ selectedIds={selectedDocIds}
384
+ onConfirm={handleDocPickerConfirm}
385
+ groupBy="project"
386
+ title="Select Context Documents"
387
+ />
388
+
266
389
  <Button
267
390
  type="submit"
268
391
  disabled={loading || !title.trim() || !!profileCompatibilityError}