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.
- package/dist/cli.js +47 -1
- package/package.json +1 -2
- package/src/app/api/documents/[id]/route.ts +5 -1
- package/src/app/api/documents/[id]/versions/route.ts +53 -0
- package/src/app/api/documents/route.ts +5 -1
- package/src/app/api/projects/[id]/documents/route.ts +124 -0
- package/src/app/api/projects/[id]/route.ts +35 -3
- package/src/app/api/projects/__tests__/delete-project.test.ts +1 -0
- package/src/app/api/schedules/route.ts +19 -1
- package/src/app/api/tasks/[id]/route.ts +37 -2
- package/src/app/api/tasks/[id]/siblings/route.ts +48 -0
- package/src/app/api/tasks/route.ts +8 -9
- package/src/app/api/workflows/[id]/documents/route.ts +209 -0
- package/src/app/api/workflows/[id]/execute/route.ts +6 -2
- package/src/app/api/workflows/[id]/route.ts +16 -3
- package/src/app/api/workflows/[id]/status/route.ts +18 -2
- package/src/app/api/workflows/route.ts +13 -2
- package/src/app/documents/page.tsx +5 -1
- package/src/app/layout.tsx +0 -1
- package/src/app/manifest.ts +3 -3
- package/src/app/projects/[id]/page.tsx +62 -2
- package/src/components/documents/document-chip-bar.tsx +17 -1
- package/src/components/documents/document-detail-view.tsx +51 -0
- package/src/components/documents/document-grid.tsx +5 -0
- package/src/components/documents/document-table.tsx +4 -0
- package/src/components/documents/types.ts +3 -0
- package/src/components/projects/project-form-sheet.tsx +133 -2
- package/src/components/schedules/schedule-form.tsx +113 -1
- package/src/components/shared/document-picker-sheet.tsx +283 -0
- package/src/components/tasks/task-card.tsx +8 -1
- package/src/components/tasks/task-create-panel.tsx +137 -14
- package/src/components/tasks/task-detail-view.tsx +47 -0
- package/src/components/tasks/task-edit-dialog.tsx +125 -2
- package/src/components/workflows/workflow-form-view.tsx +231 -7
- package/src/components/workflows/workflow-kanban-card.tsx +8 -1
- package/src/components/workflows/workflow-list.tsx +90 -45
- package/src/components/workflows/workflow-status-view.tsx +167 -22
- package/src/lib/__tests__/npx-process-cwd.test.ts +17 -2
- package/src/lib/agents/profiles/registry.ts +6 -3
- package/src/lib/book/__tests__/chapter-slugs.test.ts +80 -0
- package/src/lib/book/chapter-generator.ts +4 -19
- package/src/lib/book/chapter-mapping.ts +17 -0
- package/src/lib/book/content.ts +5 -16
- package/src/lib/book/update-detector.ts +3 -16
- package/src/lib/chat/engine.ts +1 -0
- package/src/lib/chat/system-prompt.ts +9 -1
- package/src/lib/chat/tool-catalog.ts +1 -0
- package/src/lib/chat/tools/settings-tools.ts +109 -0
- package/src/lib/chat/tools/workflow-tools.ts +145 -2
- package/src/lib/data/clear.ts +12 -0
- package/src/lib/db/bootstrap.ts +48 -0
- package/src/lib/db/migrations/0016_add_workflow_document_inputs.sql +13 -0
- package/src/lib/db/migrations/0017_add_document_picker_tables.sql +25 -0
- package/src/lib/db/migrations/0018_add_workflow_run_number.sql +2 -0
- package/src/lib/db/schema.ts +77 -0
- package/src/lib/docs/reader.ts +2 -3
- package/src/lib/documents/context-builder.ts +75 -2
- package/src/lib/documents/document-resolver.ts +119 -0
- package/src/lib/documents/processors/spreadsheet.ts +2 -1
- package/src/lib/schedules/scheduler.ts +31 -1
- package/src/lib/utils/app-root.ts +20 -0
- package/src/lib/validators/__tests__/task.test.ts +43 -10
- package/src/lib/validators/task.ts +7 -1
- package/src/lib/workflows/blueprints/registry.ts +3 -3
- package/src/lib/workflows/engine.ts +24 -8
- package/src/lib/workflows/types.ts +14 -0
- package/public/icon.svg +0 -13
- 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
|
|
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 [
|
|
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
|
-
|
|
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="
|
|
373
|
-
<
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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}
|