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 {
5
5
  Sheet,
6
6
  SheetContent,
@@ -19,9 +19,12 @@ import {
19
19
  SelectTrigger,
20
20
  SelectValue,
21
21
  } from "@/components/ui/select";
22
- import { FolderOpen, AlignLeft, FolderCode, Trash2 } from "lucide-react";
22
+ import { FolderOpen, AlignLeft, FolderCode, Trash2, Paperclip, Plus, X } from "lucide-react";
23
23
  import { toast } from "sonner";
24
+ import { Badge } from "@/components/ui/badge";
24
25
  import { ConfirmDialog } from "@/components/shared/confirm-dialog";
26
+ import { DocumentPickerSheet } from "@/components/shared/document-picker-sheet";
27
+ import { getFileIcon, formatSize } from "@/components/documents/utils";
25
28
 
26
29
  interface Project {
27
30
  id: string;
@@ -53,6 +56,9 @@ export function ProjectFormSheet({
53
56
  const [loading, setLoading] = useState(false);
54
57
  const [error, setError] = useState<string | null>(null);
55
58
  const [confirmDelete, setConfirmDelete] = useState(false);
59
+ const [selectedDocIds, setSelectedDocIds] = useState<Set<string>>(new Set());
60
+ const [selectedDocs, setSelectedDocs] = useState<Array<{ id: string; originalName: string; mimeType: string; size: number }>>([]);
61
+ const [pickerOpen, setPickerOpen] = useState(false);
56
62
 
57
63
  // Pre-fill form in edit mode
58
64
  useEffect(() => {
@@ -61,15 +67,68 @@ export function ProjectFormSheet({
61
67
  setDescription(project.description ?? "");
62
68
  setWorkingDirectory(project.workingDirectory ?? "");
63
69
  setStatus(project.status);
70
+ // Load existing default documents
71
+ fetch(`/api/projects/${project.id}/documents`)
72
+ .then((r) => r.json())
73
+ .then((docs: Array<Record<string, unknown>>) => {
74
+ const ids = new Set(docs.map((d) => d.id as string));
75
+ setSelectedDocIds(ids);
76
+ setSelectedDocs(
77
+ docs.map((d) => ({
78
+ id: d.id as string,
79
+ originalName: d.originalName as string,
80
+ mimeType: d.mimeType as string,
81
+ size: d.size as number,
82
+ }))
83
+ );
84
+ })
85
+ .catch(() => {
86
+ setSelectedDocIds(new Set());
87
+ setSelectedDocs([]);
88
+ });
64
89
  } else if (mode === "create") {
65
90
  setName("");
66
91
  setDescription("");
67
92
  setWorkingDirectory("");
68
93
  setStatus("active");
94
+ setSelectedDocIds(new Set());
95
+ setSelectedDocs([]);
69
96
  }
70
97
  setError(null);
71
98
  }, [mode, project, open]);
72
99
 
100
+ const handleDocPickerConfirm = useCallback(
101
+ (ids: string[]) => {
102
+ setSelectedDocIds(new Set(ids));
103
+ const newIds = ids.filter(
104
+ (id) => !selectedDocs.some((d) => d.id === id)
105
+ );
106
+ if (newIds.length > 0) {
107
+ const params = new URLSearchParams({ status: "ready" });
108
+ if (project?.id) params.set("projectId", project.id);
109
+ fetch(`/api/documents?${params}`)
110
+ .then((r) => r.json())
111
+ .then((allDocs: Array<Record<string, unknown>>) => {
112
+ const idSet = new Set(ids);
113
+ setSelectedDocs(
114
+ allDocs
115
+ .filter((d) => idSet.has(d.id as string))
116
+ .map((d) => ({
117
+ id: d.id as string,
118
+ originalName: d.originalName as string,
119
+ mimeType: d.mimeType as string,
120
+ size: d.size as number,
121
+ }))
122
+ );
123
+ })
124
+ .catch(() => {});
125
+ } else {
126
+ setSelectedDocs((prev) => prev.filter((d) => ids.includes(d.id)));
127
+ }
128
+ },
129
+ [project?.id, selectedDocs]
130
+ );
131
+
73
132
  async function handleSubmit(e: React.FormEvent) {
74
133
  e.preventDefault();
75
134
  if (!name.trim()) return;
@@ -85,6 +144,7 @@ export function ProjectFormSheet({
85
144
  name: name.trim(),
86
145
  description: description.trim() || undefined,
87
146
  workingDirectory: workingDirectory.trim() || undefined,
147
+ documentIds: selectedDocIds.size > 0 ? [...selectedDocIds] : undefined,
88
148
  }),
89
149
  });
90
150
  if (res.ok) {
@@ -104,6 +164,7 @@ export function ProjectFormSheet({
104
164
  description: description.trim() || undefined,
105
165
  workingDirectory: workingDirectory.trim() || undefined,
106
166
  status,
167
+ documentIds: [...selectedDocIds],
107
168
  }),
108
169
  });
109
170
  if (res.ok) {
@@ -236,6 +297,76 @@ export function ProjectFormSheet({
236
297
  </div>
237
298
  )}
238
299
 
300
+ {/* Default Documents */}
301
+ {(isEdit || selectedDocs.length > 0) && (
302
+ <div className="space-y-2">
303
+ <Label className="flex items-center gap-1.5">
304
+ <Paperclip className="h-3.5 w-3.5 text-muted-foreground" />
305
+ Default Documents
306
+ </Label>
307
+ <p className="text-xs text-muted-foreground">
308
+ Auto-attached to new tasks and workflows in this project
309
+ </p>
310
+ {selectedDocs.length > 0 && (
311
+ <div className="flex flex-wrap gap-2">
312
+ {selectedDocs.map((doc) => {
313
+ const Icon = getFileIcon(doc.mimeType);
314
+ return (
315
+ <Badge
316
+ key={doc.id}
317
+ variant="secondary"
318
+ className="flex items-center gap-1.5 pl-2 pr-1 py-1"
319
+ >
320
+ <Icon className="h-3 w-3" />
321
+ <span className="text-xs max-w-[140px] truncate">
322
+ {doc.originalName}
323
+ </span>
324
+ <span className="text-[10px] text-muted-foreground">
325
+ {formatSize(doc.size)}
326
+ </span>
327
+ <button
328
+ type="button"
329
+ onClick={() => {
330
+ setSelectedDocIds((prev) => {
331
+ const next = new Set(prev);
332
+ next.delete(doc.id);
333
+ return next;
334
+ });
335
+ setSelectedDocs((prev) => prev.filter((d) => d.id !== doc.id));
336
+ }}
337
+ className="ml-0.5 rounded-full p-0.5 hover:bg-muted transition-colors"
338
+ aria-label={`Remove ${doc.originalName}`}
339
+ >
340
+ <X className="h-3 w-3" />
341
+ </button>
342
+ </Badge>
343
+ );
344
+ })}
345
+ </div>
346
+ )}
347
+ <Button
348
+ type="button"
349
+ variant="outline"
350
+ size="sm"
351
+ onClick={() => setPickerOpen(true)}
352
+ className="gap-1.5"
353
+ >
354
+ <Plus className="h-3.5 w-3.5" />
355
+ {selectedDocs.length > 0 ? "Add More" : "Select Documents"}
356
+ </Button>
357
+ </div>
358
+ )}
359
+
360
+ <DocumentPickerSheet
361
+ open={pickerOpen}
362
+ onOpenChange={setPickerOpen}
363
+ projectId={project?.id ?? null}
364
+ selectedIds={selectedDocIds}
365
+ onConfirm={handleDocPickerConfirm}
366
+ groupBy="source"
367
+ title="Select Default Documents"
368
+ />
369
+
239
370
  {error && (
240
371
  <p className="text-sm text-destructive">{error}</p>
241
372
  )}
@@ -13,7 +13,10 @@ import {
13
13
  SelectValue,
14
14
  } from "@/components/ui/select";
15
15
  import { Switch } from "@/components/ui/switch";
16
- import { Clock, Bot, Heart, Plus, X, GripVertical, Sparkles, CheckCircle2, AlertCircle } from "lucide-react";
16
+ import { Clock, Bot, Heart, Plus, X, GripVertical, Sparkles, CheckCircle2, AlertCircle, Paperclip } from "lucide-react";
17
+ import { Badge } from "@/components/ui/badge";
18
+ import { DocumentPickerSheet } from "@/components/shared/document-picker-sheet";
19
+ import { getFileIcon, formatSize } from "@/components/documents/utils";
17
20
  import {
18
21
  type AgentRuntimeId,
19
22
  DEFAULT_AGENT_RUNTIME,
@@ -59,6 +62,7 @@ export interface ScheduleFormValues {
59
62
  activeHoursEnd: number | "";
60
63
  activeTimezone: string;
61
64
  heartbeatBudgetPerDay: number | "";
65
+ documentIds: string[];
62
66
  }
63
67
 
64
68
  export interface ScheduleFormInitialValues {
@@ -201,6 +205,43 @@ export function ScheduleForm({
201
205
  const [activeTimezone, setActiveTimezone] = useState("UTC");
202
206
  const [heartbeatBudgetPerDay, setHeartbeatBudgetPerDay] = useState<number | "">("");
203
207
 
208
+ // Document picker state
209
+ const [selectedDocIds, setSelectedDocIds] = useState<Set<string>>(new Set());
210
+ const [selectedDocs, setSelectedDocs] = useState<Array<{ id: string; originalName: string; mimeType: string; size: number }>>([]);
211
+ const [pickerOpen, setPickerOpen] = useState(false);
212
+
213
+ const handleDocPickerConfirm = useCallback(
214
+ (ids: string[]) => {
215
+ setSelectedDocIds(new Set(ids));
216
+ const newIds = ids.filter(
217
+ (id) => !selectedDocs.some((d) => d.id === id)
218
+ );
219
+ if (newIds.length > 0) {
220
+ const params = new URLSearchParams({ status: "ready" });
221
+ if (projectId) params.set("projectId", projectId);
222
+ fetch(`/api/documents?${params}`)
223
+ .then((r) => r.json())
224
+ .then((allDocs: Array<Record<string, unknown>>) => {
225
+ const idSet = new Set(ids);
226
+ setSelectedDocs(
227
+ allDocs
228
+ .filter((d) => idSet.has(d.id as string))
229
+ .map((d) => ({
230
+ id: d.id as string,
231
+ originalName: d.originalName as string,
232
+ mimeType: d.mimeType as string,
233
+ size: d.size as number,
234
+ }))
235
+ );
236
+ })
237
+ .catch(() => {});
238
+ } else {
239
+ setSelectedDocs((prev) => prev.filter((d) => ids.includes(d.id)));
240
+ }
241
+ },
242
+ [projectId, selectedDocs]
243
+ );
244
+
204
245
  useEffect(() => {
205
246
  fetch("/api/profiles")
206
247
  .then((r) => r.json())
@@ -261,6 +302,7 @@ export function ScheduleForm({
261
302
  activeHoursEnd,
262
303
  activeTimezone,
263
304
  heartbeatBudgetPerDay,
305
+ documentIds: [...selectedDocIds],
264
306
  });
265
307
  }
266
308
 
@@ -648,6 +690,76 @@ export function ScheduleForm({
648
690
  </div>
649
691
  )}
650
692
 
693
+ {/* Context Documents */}
694
+ <div className="space-y-2">
695
+ <Label className="flex items-center gap-1.5">
696
+ <Paperclip className="h-3.5 w-3.5 text-muted-foreground" />
697
+ Context Documents
698
+ </Label>
699
+ {selectedDocs.length > 0 && (
700
+ <div className="flex flex-wrap gap-2">
701
+ {selectedDocs.map((doc) => {
702
+ const Icon = getFileIcon(doc.mimeType);
703
+ return (
704
+ <Badge
705
+ key={doc.id}
706
+ variant="secondary"
707
+ className="flex items-center gap-1.5 pl-2 pr-1 py-1"
708
+ >
709
+ <Icon className="h-3 w-3" />
710
+ <span className="text-xs max-w-[140px] truncate">
711
+ {doc.originalName}
712
+ </span>
713
+ <span className="text-[10px] text-muted-foreground">
714
+ {formatSize(doc.size)}
715
+ </span>
716
+ <button
717
+ type="button"
718
+ onClick={() => {
719
+ setSelectedDocIds((prev) => {
720
+ const next = new Set(prev);
721
+ next.delete(doc.id);
722
+ return next;
723
+ });
724
+ setSelectedDocs((prev) => prev.filter((d) => d.id !== doc.id));
725
+ }}
726
+ className="ml-0.5 rounded-full p-0.5 hover:bg-muted transition-colors"
727
+ aria-label={`Remove ${doc.originalName}`}
728
+ >
729
+ <X className="h-3 w-3" />
730
+ </button>
731
+ </Badge>
732
+ );
733
+ })}
734
+ </div>
735
+ )}
736
+ <Button
737
+ type="button"
738
+ variant="outline"
739
+ size="sm"
740
+ onClick={() => setPickerOpen(true)}
741
+ className="gap-1.5"
742
+ >
743
+ <Plus className="h-3.5 w-3.5" />
744
+ {selectedDocs.length > 0 ? "Add More" : "Select Documents"}
745
+ </Button>
746
+ {selectedDocs.length > 0 && (
747
+ <p className="text-xs text-muted-foreground">
748
+ {selectedDocs.length} document{selectedDocs.length !== 1 ? "s" : ""} will be provided as context for each firing
749
+ </p>
750
+ )}
751
+ </div>
752
+
753
+ <DocumentPickerSheet
754
+ open={pickerOpen}
755
+ onOpenChange={setPickerOpen}
756
+ projectId={projectId || null}
757
+ selectedIds={selectedDocIds}
758
+ onConfirm={handleDocPickerConfirm}
759
+ groupBy="source"
760
+ title="Select Context Documents"
761
+ />
762
+
651
763
  {/* Runtime */}
652
764
  <div className="space-y-2">
653
765
  <Label className="flex items-center gap-1.5">
@@ -0,0 +1,283 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useMemo } from "react";
4
+ import {
5
+ Sheet,
6
+ SheetContent,
7
+ SheetHeader,
8
+ SheetTitle,
9
+ SheetDescription,
10
+ } from "@/components/ui/sheet";
11
+ import { Button } from "@/components/ui/button";
12
+ import { Input } from "@/components/ui/input";
13
+ import { Checkbox } from "@/components/ui/checkbox";
14
+ import { Badge } from "@/components/ui/badge";
15
+ import { ScrollArea } from "@/components/ui/scroll-area";
16
+ import { FileText, Search, Loader2 } from "lucide-react";
17
+ import { getFileIcon, formatSize, getStatusDotColor } from "@/components/documents/utils";
18
+
19
+ export interface PickerDocument {
20
+ id: string;
21
+ originalName: string;
22
+ mimeType: string;
23
+ size: number;
24
+ direction: string;
25
+ status: string;
26
+ category: string | null;
27
+ taskTitle: string | null;
28
+ projectName: string | null;
29
+ /** Source workflow name (if document was produced by a workflow task) */
30
+ sourceWorkflow?: string;
31
+ /** Parent workflow name (joined from tasks → workflows) */
32
+ workflowName?: string | null;
33
+ createdAt: number;
34
+ }
35
+
36
+ interface DocumentPickerSheetProps {
37
+ open: boolean;
38
+ onOpenChange: (open: boolean) => void;
39
+ /** Scope documents to a project. Null = show all ready documents. */
40
+ projectId: string | null;
41
+ /** Currently selected document IDs (to pre-check) */
42
+ selectedIds: Set<string>;
43
+ /** Called when user confirms selection */
44
+ onConfirm: (selectedIds: string[]) => void;
45
+ /** Optional: scope to a step ID label */
46
+ stepLabel?: string;
47
+ /** Grouping mode: "workflow" groups by source workflow, "project" by project name, "source" by direction */
48
+ groupBy?: "workflow" | "project" | "source";
49
+ /** Override the sheet title */
50
+ title?: string;
51
+ }
52
+
53
+ export function DocumentPickerSheet({
54
+ open,
55
+ onOpenChange,
56
+ projectId,
57
+ selectedIds,
58
+ onConfirm,
59
+ stepLabel,
60
+ groupBy = "source",
61
+ title,
62
+ }: DocumentPickerSheetProps) {
63
+ const [documents, setDocuments] = useState<PickerDocument[]>([]);
64
+ const [loading, setLoading] = useState(false);
65
+ const [search, setSearch] = useState("");
66
+ const [localSelected, setLocalSelected] = useState<Set<string>>(new Set());
67
+
68
+ // Sync initial selection when sheet opens
69
+ useEffect(() => {
70
+ if (open) {
71
+ setLocalSelected(new Set(selectedIds));
72
+ fetchDocuments();
73
+ }
74
+ }, [open, projectId]); // eslint-disable-line react-hooks/exhaustive-deps
75
+
76
+ async function fetchDocuments() {
77
+ setLoading(true);
78
+ try {
79
+ const params = new URLSearchParams({ status: "ready" });
80
+ if (projectId) params.set("projectId", projectId);
81
+ const res = await fetch(`/api/documents?${params}`);
82
+ if (res.ok) {
83
+ const data = await res.json();
84
+ setDocuments(data);
85
+ }
86
+ } catch {
87
+ // Silently fail — empty list shown
88
+ } finally {
89
+ setLoading(false);
90
+ }
91
+ }
92
+
93
+ const filtered = useMemo(() => {
94
+ if (!search.trim()) return documents;
95
+ const q = search.toLowerCase();
96
+ return documents.filter(
97
+ (doc) =>
98
+ doc.originalName.toLowerCase().includes(q) ||
99
+ doc.category?.toLowerCase().includes(q) ||
100
+ doc.taskTitle?.toLowerCase().includes(q)
101
+ );
102
+ }, [documents, search]);
103
+
104
+ // Group documents based on the groupBy mode
105
+ const grouped = useMemo(() => {
106
+ const groups: Record<string, PickerDocument[]> = {};
107
+ for (const doc of filtered) {
108
+ let key: string;
109
+ switch (groupBy) {
110
+ case "workflow":
111
+ key =
112
+ doc.direction === "output"
113
+ ? doc.workflowName
114
+ ? `From: ${doc.workflowName}`
115
+ : doc.taskTitle
116
+ ? `From: ${doc.taskTitle}`
117
+ : "Agent Generated"
118
+ : "Uploaded";
119
+ break;
120
+ case "project":
121
+ key = doc.projectName ?? "No Project";
122
+ break;
123
+ case "source":
124
+ default:
125
+ key =
126
+ doc.direction === "output"
127
+ ? doc.taskTitle
128
+ ? "Task Output"
129
+ : "Agent Generated"
130
+ : "Uploaded";
131
+ break;
132
+ }
133
+ if (!groups[key]) groups[key] = [];
134
+ groups[key].push(doc);
135
+ }
136
+ return groups;
137
+ }, [filtered, groupBy]);
138
+
139
+ function toggleDocument(id: string) {
140
+ setLocalSelected((prev) => {
141
+ const next = new Set(prev);
142
+ if (next.has(id)) {
143
+ next.delete(id);
144
+ } else {
145
+ next.add(id);
146
+ }
147
+ return next;
148
+ });
149
+ }
150
+
151
+ function handleConfirm() {
152
+ onConfirm([...localSelected]);
153
+ onOpenChange(false);
154
+ }
155
+
156
+ const sheetTitle = title
157
+ ? title
158
+ : stepLabel
159
+ ? `Select Documents for "${stepLabel}"`
160
+ : "Select Input Documents";
161
+
162
+ const emptyMessage = search
163
+ ? "No documents match your search."
164
+ : projectId
165
+ ? "No documents available in this project."
166
+ : "No documents available. Upload files in the Documents view.";
167
+
168
+ return (
169
+ <Sheet open={open} onOpenChange={onOpenChange}>
170
+ <SheetContent side="right" className="w-full sm:max-w-lg">
171
+ <SheetHeader className="p-4">
172
+ <SheetTitle className="flex items-center gap-2">
173
+ <FileText className="h-4 w-4" />
174
+ {sheetTitle}
175
+ </SheetTitle>
176
+ <SheetDescription>
177
+ Choose documents to provide as context.
178
+ </SheetDescription>
179
+ </SheetHeader>
180
+
181
+ <div className="px-6 pb-6 flex flex-col gap-4 flex-1 min-h-0">
182
+ {/* Search */}
183
+ <div className="relative">
184
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
185
+ <Input
186
+ placeholder="Search documents..."
187
+ value={search}
188
+ onChange={(e) => setSearch(e.target.value)}
189
+ className="pl-9"
190
+ />
191
+ </div>
192
+
193
+ {/* Document list */}
194
+ <ScrollArea className="flex-1 min-h-0 -mx-2">
195
+ {loading ? (
196
+ <div className="flex items-center justify-center py-12">
197
+ <Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
198
+ </div>
199
+ ) : Object.keys(grouped).length === 0 ? (
200
+ <div className="text-center text-sm text-muted-foreground py-12">
201
+ {emptyMessage}
202
+ </div>
203
+ ) : (
204
+ <div className="space-y-4 px-2">
205
+ {Object.entries(grouped).map(([source, docs]) => (
206
+ <div key={source}>
207
+ <p className="text-xs font-medium text-muted-foreground mb-2">
208
+ {source}
209
+ </p>
210
+ <div className="space-y-1">
211
+ {docs.map((doc) => {
212
+ const Icon = getFileIcon(doc.mimeType);
213
+ const isChecked = localSelected.has(doc.id);
214
+ return (
215
+ <div
216
+ key={doc.id}
217
+ role="button"
218
+ tabIndex={0}
219
+ onClick={() => toggleDocument(doc.id)}
220
+ onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggleDocument(doc.id); } }}
221
+ className={`w-full flex items-center gap-3 px-3 py-2 rounded-lg text-left transition-colors cursor-pointer ${
222
+ isChecked
223
+ ? "bg-accent/50 border border-accent"
224
+ : "hover:bg-muted/50 border border-transparent"
225
+ }`}
226
+ >
227
+ <Checkbox
228
+ checked={isChecked}
229
+ aria-label={`Select ${doc.originalName}`}
230
+ />
231
+ <Icon className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
232
+ <div className="flex-1 min-w-0">
233
+ <p className="text-sm font-medium truncate">
234
+ {doc.originalName}
235
+ </p>
236
+ <p className="text-xs text-muted-foreground">
237
+ {formatSize(doc.size)}
238
+ {doc.category && ` · ${doc.category}`}
239
+ </p>
240
+ </div>
241
+ <div className="flex items-center gap-2">
242
+ <div
243
+ className={`h-2 w-2 rounded-full ${getStatusDotColor(doc.status)}`}
244
+ />
245
+ {doc.direction === "output" && (
246
+ <Badge variant="outline" className="text-[10px] px-1.5">
247
+ output
248
+ </Badge>
249
+ )}
250
+ </div>
251
+ </div>
252
+ );
253
+ })}
254
+ </div>
255
+ </div>
256
+ ))}
257
+ </div>
258
+ )}
259
+ </ScrollArea>
260
+
261
+ {/* Footer */}
262
+ <div className="flex items-center justify-between pt-2 border-t">
263
+ <span className="text-sm text-muted-foreground">
264
+ {localSelected.size} selected
265
+ </span>
266
+ <div className="flex gap-2">
267
+ <Button
268
+ variant="outline"
269
+ size="sm"
270
+ onClick={() => onOpenChange(false)}
271
+ >
272
+ Cancel
273
+ </Button>
274
+ <Button size="sm" onClick={handleConfirm}>
275
+ Confirm
276
+ </Button>
277
+ </div>
278
+ </div>
279
+ </div>
280
+ </SheetContent>
281
+ </Sheet>
282
+ );
283
+ }
@@ -5,7 +5,7 @@ import { useSortable } from "@dnd-kit/sortable";
5
5
  import { CSS } from "@dnd-kit/utilities";
6
6
  import { Card } from "@/components/ui/card";
7
7
  import { Badge } from "@/components/ui/badge";
8
- import { AlertCircle, Bot, ArrowUp, ArrowDown, Minus, Trash2, Check, X, Loader2, Square, CheckSquare, Pencil } from "lucide-react";
8
+ import { AlertCircle, Bot, ArrowUp, ArrowDown, Minus, Trash2, Check, X, Loader2, Square, CheckSquare, Pencil, FileText } from "lucide-react";
9
9
  import type { TaskStatus } from "@/lib/constants/task-status";
10
10
 
11
11
  export interface TaskItem {
@@ -28,6 +28,7 @@ export interface TaskItem {
28
28
  resumeCount: number;
29
29
  createdAt: string;
30
30
  updatedAt: string;
31
+ docCount?: number;
31
32
  usage?: {
32
33
  inputTokens: number | null;
33
34
  outputTokens: number | null;
@@ -165,6 +166,12 @@ export function TaskCard({
165
166
  <span className="truncate">{task.assignedAgent}</span>
166
167
  </Badge>
167
168
  )}
169
+ {task.docCount != null && task.docCount > 0 && (
170
+ <Badge variant="outline" className="text-xs gap-1 h-5">
171
+ <FileText className="h-3 w-3 shrink-0" />
172
+ {task.docCount}
173
+ </Badge>
174
+ )}
168
175
  {isFailed && <AlertCircle className="h-3.5 w-3.5 text-destructive" aria-label="Task failed" />}
169
176
  {isRunning && (
170
177
  <span className="flex h-2 w-2" aria-label="Task running">