stagent 0.6.2 → 0.7.0

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 (176) hide show
  1. package/README.md +21 -2
  2. package/dist/cli.js +272 -1
  3. package/docs/.coverage-gaps.json +66 -16
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/dashboard-kanban.md +13 -7
  6. package/docs/features/settings.md +15 -3
  7. package/docs/features/tables.md +122 -0
  8. package/docs/index.md +3 -2
  9. package/docs/journeys/developer.md +26 -16
  10. package/docs/journeys/personal-use.md +23 -9
  11. package/docs/journeys/power-user.md +40 -14
  12. package/docs/journeys/work-use.md +43 -15
  13. package/docs/manifest.json +27 -17
  14. package/package.json +3 -2
  15. package/src/app/api/chat/entities/search/route.ts +12 -3
  16. package/src/app/api/documents/[id]/route.ts +5 -1
  17. package/src/app/api/documents/[id]/versions/route.ts +53 -0
  18. package/src/app/api/documents/route.ts +5 -1
  19. package/src/app/api/projects/[id]/documents/route.ts +124 -0
  20. package/src/app/api/projects/[id]/route.ts +72 -3
  21. package/src/app/api/projects/__tests__/delete-project.test.ts +13 -0
  22. package/src/app/api/schedules/route.ts +19 -1
  23. package/src/app/api/snapshots/[id]/restore/route.ts +62 -0
  24. package/src/app/api/snapshots/[id]/route.ts +44 -0
  25. package/src/app/api/snapshots/route.ts +54 -0
  26. package/src/app/api/snapshots/settings/route.ts +67 -0
  27. package/src/app/api/tables/[id]/charts/[chartId]/route.ts +89 -0
  28. package/src/app/api/tables/[id]/charts/route.ts +72 -0
  29. package/src/app/api/tables/[id]/columns/route.ts +70 -0
  30. package/src/app/api/tables/[id]/export/route.ts +94 -0
  31. package/src/app/api/tables/[id]/history/route.ts +15 -0
  32. package/src/app/api/tables/[id]/import/route.ts +111 -0
  33. package/src/app/api/tables/[id]/route.ts +86 -0
  34. package/src/app/api/tables/[id]/rows/[rowId]/history/route.ts +32 -0
  35. package/src/app/api/tables/[id]/rows/[rowId]/route.ts +51 -0
  36. package/src/app/api/tables/[id]/rows/route.ts +101 -0
  37. package/src/app/api/tables/[id]/triggers/[triggerId]/route.ts +65 -0
  38. package/src/app/api/tables/[id]/triggers/route.ts +122 -0
  39. package/src/app/api/tables/route.ts +65 -0
  40. package/src/app/api/tables/templates/route.ts +92 -0
  41. package/src/app/api/tasks/[id]/route.ts +37 -2
  42. package/src/app/api/tasks/[id]/siblings/route.ts +48 -0
  43. package/src/app/api/tasks/route.ts +8 -9
  44. package/src/app/api/workflows/[id]/documents/route.ts +209 -0
  45. package/src/app/api/workflows/[id]/execute/route.ts +6 -2
  46. package/src/app/api/workflows/[id]/route.ts +16 -3
  47. package/src/app/api/workflows/[id]/status/route.ts +18 -2
  48. package/src/app/api/workflows/route.ts +13 -2
  49. package/src/app/documents/page.tsx +5 -1
  50. package/src/app/layout.tsx +0 -1
  51. package/src/app/manifest.ts +3 -3
  52. package/src/app/projects/[id]/page.tsx +62 -2
  53. package/src/app/settings/page.tsx +2 -0
  54. package/src/app/tables/[id]/page.tsx +67 -0
  55. package/src/app/tables/page.tsx +21 -0
  56. package/src/app/tables/templates/page.tsx +19 -0
  57. package/src/components/chat/chat-table-result.tsx +139 -0
  58. package/src/components/documents/document-browser.tsx +1 -1
  59. package/src/components/documents/document-chip-bar.tsx +17 -1
  60. package/src/components/documents/document-detail-view.tsx +51 -0
  61. package/src/components/documents/document-grid.tsx +5 -0
  62. package/src/components/documents/document-table.tsx +4 -0
  63. package/src/components/documents/types.ts +3 -0
  64. package/src/components/projects/project-form-sheet.tsx +109 -2
  65. package/src/components/schedules/schedule-form.tsx +91 -1
  66. package/src/components/settings/data-management-section.tsx +17 -12
  67. package/src/components/settings/database-snapshots-section.tsx +469 -0
  68. package/src/components/shared/app-sidebar.tsx +2 -0
  69. package/src/components/shared/document-picker-sheet.tsx +486 -0
  70. package/src/components/tables/table-browser.tsx +234 -0
  71. package/src/components/tables/table-cell-editor.tsx +226 -0
  72. package/src/components/tables/table-chart-builder.tsx +288 -0
  73. package/src/components/tables/table-chart-view.tsx +146 -0
  74. package/src/components/tables/table-column-header.tsx +103 -0
  75. package/src/components/tables/table-column-sheet.tsx +331 -0
  76. package/src/components/tables/table-create-sheet.tsx +240 -0
  77. package/src/components/tables/table-detail-sheet.tsx +144 -0
  78. package/src/components/tables/table-detail-tabs.tsx +278 -0
  79. package/src/components/tables/table-grid.tsx +61 -0
  80. package/src/components/tables/table-history-tab.tsx +148 -0
  81. package/src/components/tables/table-import-wizard.tsx +542 -0
  82. package/src/components/tables/table-list-table.tsx +95 -0
  83. package/src/components/tables/table-relation-combobox.tsx +217 -0
  84. package/src/components/tables/table-spreadsheet.tsx +499 -0
  85. package/src/components/tables/table-template-gallery.tsx +162 -0
  86. package/src/components/tables/table-template-preview.tsx +219 -0
  87. package/src/components/tables/table-toolbar.tsx +79 -0
  88. package/src/components/tables/table-triggers-tab.tsx +446 -0
  89. package/src/components/tables/types.ts +6 -0
  90. package/src/components/tables/use-spreadsheet-keys.ts +171 -0
  91. package/src/components/tables/utils.ts +29 -0
  92. package/src/components/tasks/task-card.tsx +8 -1
  93. package/src/components/tasks/task-create-panel.tsx +111 -14
  94. package/src/components/tasks/task-detail-view.tsx +47 -0
  95. package/src/components/tasks/task-edit-dialog.tsx +103 -2
  96. package/src/components/workflows/workflow-form-view.tsx +207 -7
  97. package/src/components/workflows/workflow-kanban-card.tsx +8 -1
  98. package/src/components/workflows/workflow-list.tsx +90 -45
  99. package/src/components/workflows/workflow-status-view.tsx +168 -23
  100. package/src/instrumentation.ts +3 -0
  101. package/src/lib/__tests__/npx-process-cwd.test.ts +17 -2
  102. package/src/lib/agents/__tests__/claude-agent.test.ts +5 -1
  103. package/src/lib/agents/claude-agent.ts +3 -1
  104. package/src/lib/agents/profiles/registry.ts +6 -3
  105. package/src/lib/agents/runtime/anthropic-direct.ts +29 -0
  106. package/src/lib/agents/runtime/openai-direct.ts +29 -0
  107. package/src/lib/book/__tests__/chapter-slugs.test.ts +80 -0
  108. package/src/lib/book/chapter-generator.ts +4 -19
  109. package/src/lib/book/chapter-mapping.ts +17 -0
  110. package/src/lib/book/content.ts +5 -16
  111. package/src/lib/book/update-detector.ts +3 -16
  112. package/src/lib/chat/engine.ts +1 -0
  113. package/src/lib/chat/stagent-tools.ts +2 -0
  114. package/src/lib/chat/system-prompt.ts +9 -1
  115. package/src/lib/chat/tool-catalog.ts +35 -0
  116. package/src/lib/chat/tools/settings-tools.ts +109 -0
  117. package/src/lib/chat/tools/table-tools.ts +955 -0
  118. package/src/lib/chat/tools/workflow-tools.ts +145 -2
  119. package/src/lib/constants/table-status.ts +68 -0
  120. package/src/lib/data/__tests__/clear.test.ts +1 -1
  121. package/src/lib/data/clear.ts +57 -0
  122. package/src/lib/data/seed-data/__tests__/profiles.test.ts +28 -23
  123. package/src/lib/data/seed-data/conversations.ts +350 -42
  124. package/src/lib/data/seed-data/documents.ts +564 -591
  125. package/src/lib/data/seed-data/learned-context.ts +101 -22
  126. package/src/lib/data/seed-data/notifications.ts +344 -70
  127. package/src/lib/data/seed-data/profile-test-results.ts +92 -11
  128. package/src/lib/data/seed-data/profiles.ts +144 -46
  129. package/src/lib/data/seed-data/projects.ts +50 -18
  130. package/src/lib/data/seed-data/repo-imports.ts +28 -13
  131. package/src/lib/data/seed-data/schedules.ts +208 -41
  132. package/src/lib/data/seed-data/table-templates.ts +234 -0
  133. package/src/lib/data/seed-data/tasks.ts +614 -116
  134. package/src/lib/data/seed-data/usage-ledger.ts +182 -103
  135. package/src/lib/data/seed-data/user-tables.ts +203 -0
  136. package/src/lib/data/seed-data/views.ts +52 -7
  137. package/src/lib/data/seed-data/workflows.ts +231 -84
  138. package/src/lib/data/seed.ts +55 -14
  139. package/src/lib/data/tables.ts +417 -0
  140. package/src/lib/db/bootstrap.ts +275 -0
  141. package/src/lib/db/index.ts +9 -0
  142. package/src/lib/db/migrations/0016_add_workflow_document_inputs.sql +13 -0
  143. package/src/lib/db/migrations/0017_add_document_picker_tables.sql +25 -0
  144. package/src/lib/db/migrations/0018_add_workflow_run_number.sql +2 -0
  145. package/src/lib/db/migrations/0019_add_tables_feature.sql +160 -0
  146. package/src/lib/db/migrations/0020_add_table_triggers.sql +19 -0
  147. package/src/lib/db/migrations/0021_add_row_history.sql +15 -0
  148. package/src/lib/db/schema.ts +445 -0
  149. package/src/lib/docs/reader.ts +2 -3
  150. package/src/lib/documents/context-builder.ts +75 -2
  151. package/src/lib/documents/document-resolver.ts +119 -0
  152. package/src/lib/documents/processors/spreadsheet.ts +2 -1
  153. package/src/lib/schedules/scheduler.ts +31 -1
  154. package/src/lib/snapshots/auto-backup.ts +132 -0
  155. package/src/lib/snapshots/retention.ts +64 -0
  156. package/src/lib/snapshots/snapshot-manager.ts +429 -0
  157. package/src/lib/tables/computed.ts +61 -0
  158. package/src/lib/tables/context-builder.ts +139 -0
  159. package/src/lib/tables/formula-engine.ts +415 -0
  160. package/src/lib/tables/history.ts +115 -0
  161. package/src/lib/tables/import.ts +343 -0
  162. package/src/lib/tables/query-builder.ts +152 -0
  163. package/src/lib/tables/trigger-evaluator.ts +146 -0
  164. package/src/lib/tables/types.ts +141 -0
  165. package/src/lib/tables/validation.ts +119 -0
  166. package/src/lib/utils/app-root.ts +20 -0
  167. package/src/lib/utils/stagent-paths.ts +20 -0
  168. package/src/lib/validators/__tests__/task.test.ts +43 -10
  169. package/src/lib/validators/task.ts +7 -1
  170. package/src/lib/workflows/blueprints/registry.ts +3 -3
  171. package/src/lib/workflows/engine.ts +24 -8
  172. package/src/lib/workflows/types.ts +14 -0
  173. package/tsconfig.json +3 -1
  174. package/public/icon.svg +0 -13
  175. package/src/components/tasks/file-upload.tsx +0 -120
  176. /package/docs/features/{playbook.md → user-guide.md} +0 -0
@@ -0,0 +1,19 @@
1
+ import { listTemplates } from "@/lib/data/tables";
2
+ import { PageShell } from "@/components/shared/page-shell";
3
+ import { TableTemplateGallery } from "@/components/tables/table-template-gallery";
4
+
5
+ export const dynamic = "force-dynamic";
6
+
7
+ export default async function TableTemplatesPage() {
8
+ const templates = await listTemplates();
9
+
10
+ return (
11
+ <PageShell
12
+ title="Table Templates"
13
+ backHref="/tables"
14
+ backLabel="Tables"
15
+ >
16
+ <TableTemplateGallery templates={templates} />
17
+ </PageShell>
18
+ );
19
+ }
@@ -0,0 +1,139 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Badge } from "@/components/ui/badge";
6
+ import { Table2, ChevronDown, ChevronUp, ExternalLink } from "lucide-react";
7
+ import Link from "next/link";
8
+
9
+ interface ChatTableResultProps {
10
+ tableId: string;
11
+ tableName: string;
12
+ columns: string[];
13
+ rows: Array<Record<string, unknown>>;
14
+ totalRows?: number;
15
+ aggregation?: {
16
+ operation: string;
17
+ column: string;
18
+ result: number;
19
+ count: number;
20
+ };
21
+ }
22
+
23
+ const MAX_VISIBLE_ROWS = 10;
24
+
25
+ export function ChatTableResult({
26
+ tableId,
27
+ tableName,
28
+ columns,
29
+ rows,
30
+ totalRows,
31
+ aggregation,
32
+ }: ChatTableResultProps) {
33
+ const [expanded, setExpanded] = useState(false);
34
+ const visibleRows = expanded ? rows : rows.slice(0, MAX_VISIBLE_ROWS);
35
+ const hasMore = rows.length > MAX_VISIBLE_ROWS;
36
+
37
+ if (aggregation) {
38
+ return (
39
+ <div className="rounded-lg border p-3 my-2 space-y-2">
40
+ <div className="flex items-center gap-2">
41
+ <Table2 className="h-4 w-4 text-muted-foreground" />
42
+ <span className="text-sm font-medium">{tableName}</span>
43
+ <Badge variant="secondary" className="text-xs">
44
+ {aggregation.operation}
45
+ </Badge>
46
+ </div>
47
+ <div className="flex items-baseline gap-2">
48
+ <span className="text-2xl font-semibold tabular-nums">
49
+ {typeof aggregation.result === "number"
50
+ ? aggregation.result.toLocaleString()
51
+ : String(aggregation.result)}
52
+ </span>
53
+ <span className="text-xs text-muted-foreground">
54
+ {aggregation.column} · {aggregation.count} rows
55
+ </span>
56
+ </div>
57
+ </div>
58
+ );
59
+ }
60
+
61
+ return (
62
+ <div className="rounded-lg border my-2 overflow-hidden">
63
+ <div className="flex items-center justify-between px-3 py-2 bg-muted/50">
64
+ <div className="flex items-center gap-2">
65
+ <Table2 className="h-4 w-4 text-muted-foreground" />
66
+ <span className="text-sm font-medium">{tableName}</span>
67
+ <Badge variant="secondary" className="text-xs">
68
+ {totalRows ?? rows.length} row{(totalRows ?? rows.length) !== 1 ? "s" : ""}
69
+ </Badge>
70
+ </div>
71
+ <Link
72
+ href={`/tables/${tableId}`}
73
+ className="text-xs text-muted-foreground hover:text-foreground flex items-center gap-1"
74
+ >
75
+ Open <ExternalLink className="h-3 w-3" />
76
+ </Link>
77
+ </div>
78
+
79
+ {columns.length > 0 && visibleRows.length > 0 && (
80
+ <div className="overflow-x-auto">
81
+ <table className="w-full text-xs">
82
+ <thead>
83
+ <tr className="border-b bg-muted/30">
84
+ {columns.slice(0, 8).map((col) => (
85
+ <th key={col} className="px-3 py-1.5 text-left font-medium text-muted-foreground">
86
+ {col}
87
+ </th>
88
+ ))}
89
+ {columns.length > 8 && (
90
+ <th className="px-3 py-1.5 text-left font-medium text-muted-foreground">
91
+ +{columns.length - 8} more
92
+ </th>
93
+ )}
94
+ </tr>
95
+ </thead>
96
+ <tbody>
97
+ {visibleRows.map((row, i) => (
98
+ <tr key={i} className="border-b last:border-0">
99
+ {columns.slice(0, 8).map((col) => (
100
+ <td key={col} className="px-3 py-1.5 max-w-[200px] truncate">
101
+ {row[col] == null ? (
102
+ <span className="text-muted-foreground/40">—</span>
103
+ ) : (
104
+ String(row[col])
105
+ )}
106
+ </td>
107
+ ))}
108
+ </tr>
109
+ ))}
110
+ </tbody>
111
+ </table>
112
+ </div>
113
+ )}
114
+
115
+ {hasMore && (
116
+ <div className="px-3 py-2 border-t">
117
+ <Button
118
+ variant="ghost"
119
+ size="sm"
120
+ className="w-full text-xs"
121
+ onClick={() => setExpanded(!expanded)}
122
+ >
123
+ {expanded ? (
124
+ <>
125
+ <ChevronUp className="h-3 w-3 mr-1" />
126
+ Show less
127
+ </>
128
+ ) : (
129
+ <>
130
+ <ChevronDown className="h-3 w-3 mr-1" />
131
+ Show all {rows.length} rows
132
+ </>
133
+ )}
134
+ </Button>
135
+ </div>
136
+ )}
137
+ </div>
138
+ );
139
+ }
@@ -89,7 +89,7 @@ export function DocumentBrowser({
89
89
  let deleted = 0;
90
90
  for (const id of selected) {
91
91
  try {
92
- const res = await fetch(`/api/documents/${id}`, { method: "DELETE" });
92
+ const res = await fetch(`/api/documents/${id}?cascadeDelete=true`, { method: "DELETE" });
93
93
  if (res.ok) deleted++;
94
94
  } catch {
95
95
  // Continue with remaining
@@ -18,6 +18,7 @@ import {
18
18
  ArrowDownLeft,
19
19
  Link2,
20
20
  FolderKanban,
21
+ GitBranch,
21
22
  } from "lucide-react";
22
23
  import {
23
24
  getFileIcon,
@@ -127,8 +128,23 @@ export function DocumentChipBar({
127
128
  </Badge>
128
129
  </div>
129
130
 
130
- {/* Row 3: Links — task, workflow, project */}
131
+ {/* Row 3: Links — workflow, task, project */}
131
132
  <div className="flex flex-wrap items-center gap-2">
133
+ {/* Workflow source */}
134
+ {doc.workflowId && doc.workflowName && (
135
+ <Badge
136
+ variant="secondary"
137
+ className="text-xs cursor-pointer hover:bg-accent gap-1"
138
+ onClick={() => router.push(`/workflows/${doc.workflowId}`)}
139
+ >
140
+ <GitBranch className="h-3 w-3" />
141
+ {doc.workflowName}
142
+ {doc.workflowRunNumber != null && doc.workflowRunNumber > 0 && (
143
+ <span className="text-muted-foreground ml-1">Run #{doc.workflowRunNumber}</span>
144
+ )}
145
+ </Badge>
146
+ )}
147
+
132
148
  {/* Task link */}
133
149
  {doc.taskTitle ? (
134
150
  <Badge
@@ -3,9 +3,12 @@
3
3
  import { useState, useEffect, useCallback } from "react";
4
4
  import { useRouter } from "next/navigation";
5
5
  import { Skeleton } from "@/components/ui/skeleton";
6
+ import { Badge } from "@/components/ui/badge";
7
+ import { FileText } from "lucide-react";
6
8
  import { toast } from "sonner";
7
9
  import { DocumentChipBar } from "./document-chip-bar";
8
10
  import { DocumentContentRenderer } from "./document-content-renderer";
11
+ import { formatSize, formatRelativeTime } from "./utils";
9
12
  import type { DocumentWithRelations } from "./types";
10
13
 
11
14
  /** Serialized version of DocumentWithRelations (Date fields become strings from server) */
@@ -28,6 +31,14 @@ export function DocumentDetailView({ documentId, initialDocument }: DocumentDeta
28
31
  const [projects, setProjects] = useState<{ id: string; name: string }[]>([]);
29
32
  const [deleting, setDeleting] = useState(false);
30
33
  const [linking, setLinking] = useState(false);
34
+ const [versions, setVersions] = useState<Array<{
35
+ id: string;
36
+ version: number;
37
+ size: number;
38
+ status: string;
39
+ createdAt: string;
40
+ workflowRunNumber: number | null;
41
+ }>>([]);
31
42
 
32
43
  const refresh = useCallback(async () => {
33
44
  try {
@@ -56,6 +67,16 @@ export function DocumentDetailView({ documentId, initialDocument }: DocumentDeta
56
67
  .catch(() => {});
57
68
  }, [refresh, initialDocument]);
58
69
 
70
+ // Fetch version history for output documents
71
+ useEffect(() => {
72
+ if (doc?.direction === "output") {
73
+ fetch(`/api/documents/${documentId}/versions`)
74
+ .then((r) => r.ok ? r.json() : [])
75
+ .then(setVersions)
76
+ .catch(() => {});
77
+ }
78
+ }, [doc?.direction, documentId]);
79
+
59
80
  async function handleDelete() {
60
81
  if (!doc) return;
61
82
  setDeleting(true);
@@ -136,6 +157,36 @@ export function DocumentDetailView({ documentId, initialDocument }: DocumentDeta
136
157
  deleting={deleting}
137
158
  linking={linking}
138
159
  />
160
+ {/* Version History */}
161
+ {doc.direction === "output" && versions.length > 1 && (
162
+ <div className="space-y-2">
163
+ <h3 className="text-sm font-medium text-muted-foreground">Version History</h3>
164
+ <div className="surface-control rounded-lg divide-y divide-border">
165
+ {versions.map((v) => (
166
+ <button
167
+ key={v.id}
168
+ className={`w-full flex items-center gap-3 px-3 py-2 text-xs hover:bg-accent/50 transition-colors ${v.id === doc.id ? "bg-accent/30" : ""}`}
169
+ onClick={() => v.id !== doc.id && router.push(`/documents/${v.id}`)}
170
+ disabled={v.id === doc.id}
171
+ >
172
+ <FileText className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
173
+ <span className="font-medium">v{v.version}</span>
174
+ {v.id === doc.id && (
175
+ <Badge variant="outline" className="text-[10px] py-0 px-1.5">current</Badge>
176
+ )}
177
+ <span className="text-muted-foreground">{formatSize(v.size)}</span>
178
+ {v.workflowRunNumber != null && v.workflowRunNumber > 0 && (
179
+ <span className="text-muted-foreground">Run #{v.workflowRunNumber}</span>
180
+ )}
181
+ <span className="text-muted-foreground ml-auto">
182
+ {formatRelativeTime(typeof v.createdAt === "number" ? v.createdAt : new Date(v.createdAt).getTime())}
183
+ </span>
184
+ </button>
185
+ ))}
186
+ </div>
187
+ </div>
188
+ )}
189
+
139
190
  <div className="prose-reader-surface">
140
191
  <DocumentContentRenderer doc={doc} />
141
192
  </div>
@@ -70,6 +70,11 @@ export function DocumentGrid({
70
70
  <span className="capitalize">{doc.direction}</span>
71
71
  {doc.direction === "output" && <span>v{doc.version}</span>}
72
72
  </div>
73
+ {doc.workflowName && (
74
+ <p className="text-[10px] text-muted-foreground truncate mt-0.5">
75
+ {doc.workflowName}
76
+ </p>
77
+ )}
73
78
  </Card>
74
79
  );
75
80
  })}
@@ -46,6 +46,7 @@ export function DocumentTable({
46
46
  <TableHead>Name</TableHead>
47
47
  <TableHead className="hidden md:table-cell">Size</TableHead>
48
48
  <TableHead className="hidden md:table-cell">Direction</TableHead>
49
+ <TableHead className="hidden lg:table-cell">Workflow</TableHead>
49
50
  <TableHead className="hidden lg:table-cell">Task</TableHead>
50
51
  <TableHead className="hidden lg:table-cell">Project</TableHead>
51
52
  <TableHead>Status</TableHead>
@@ -87,6 +88,9 @@ export function DocumentTable({
87
88
  )}
88
89
  </div>
89
90
  </TableCell>
91
+ <TableCell className="hidden lg:table-cell text-muted-foreground text-sm truncate max-w-[140px]">
92
+ {doc.workflowName ?? "—"}
93
+ </TableCell>
90
94
  <TableCell className="hidden lg:table-cell text-sm">
91
95
  {doc.taskTitle ? (
92
96
  <span className="truncate max-w-[150px] block">{doc.taskTitle}</span>
@@ -3,4 +3,7 @@ import type { DocumentRow } from "@/lib/db/schema";
3
3
  export type DocumentWithRelations = DocumentRow & {
4
4
  taskTitle: string | null;
5
5
  projectName: string | null;
6
+ workflowId?: string | null;
7
+ workflowName?: string | null;
8
+ workflowRunNumber?: number | null;
6
9
  };
@@ -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,44 @@ 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[], meta: Array<{ id: string; originalName: string; mimeType: string; size: number }>) => {
102
+ setSelectedDocIds(new Set(ids));
103
+ setSelectedDocs(meta);
104
+ },
105
+ []
106
+ );
107
+
73
108
  async function handleSubmit(e: React.FormEvent) {
74
109
  e.preventDefault();
75
110
  if (!name.trim()) return;
@@ -85,6 +120,7 @@ export function ProjectFormSheet({
85
120
  name: name.trim(),
86
121
  description: description.trim() || undefined,
87
122
  workingDirectory: workingDirectory.trim() || undefined,
123
+ documentIds: selectedDocIds.size > 0 ? [...selectedDocIds] : undefined,
88
124
  }),
89
125
  });
90
126
  if (res.ok) {
@@ -104,6 +140,7 @@ export function ProjectFormSheet({
104
140
  description: description.trim() || undefined,
105
141
  workingDirectory: workingDirectory.trim() || undefined,
106
142
  status,
143
+ documentIds: [...selectedDocIds],
107
144
  }),
108
145
  });
109
146
  if (res.ok) {
@@ -236,6 +273,76 @@ export function ProjectFormSheet({
236
273
  </div>
237
274
  )}
238
275
 
276
+ {/* Default Documents */}
277
+ {(isEdit || selectedDocs.length > 0) && (
278
+ <div className="space-y-2">
279
+ <Label className="flex items-center gap-1.5">
280
+ <Paperclip className="h-3.5 w-3.5 text-muted-foreground" />
281
+ Default Documents
282
+ </Label>
283
+ <p className="text-xs text-muted-foreground">
284
+ Auto-attached to new tasks and workflows in this project
285
+ </p>
286
+ {selectedDocs.length > 0 && (
287
+ <div className="flex flex-wrap gap-2">
288
+ {selectedDocs.map((doc) => {
289
+ const Icon = getFileIcon(doc.mimeType);
290
+ return (
291
+ <Badge
292
+ key={doc.id}
293
+ variant="secondary"
294
+ className="flex items-center gap-1.5 pl-2 pr-1 py-1"
295
+ >
296
+ <Icon className="h-3 w-3" />
297
+ <span className="text-xs max-w-[140px] truncate">
298
+ {doc.originalName}
299
+ </span>
300
+ <span className="text-[10px] text-muted-foreground">
301
+ {formatSize(doc.size)}
302
+ </span>
303
+ <button
304
+ type="button"
305
+ onClick={() => {
306
+ setSelectedDocIds((prev) => {
307
+ const next = new Set(prev);
308
+ next.delete(doc.id);
309
+ return next;
310
+ });
311
+ setSelectedDocs((prev) => prev.filter((d) => d.id !== doc.id));
312
+ }}
313
+ className="ml-0.5 rounded-full p-0.5 hover:bg-muted transition-colors"
314
+ aria-label={`Remove ${doc.originalName}`}
315
+ >
316
+ <X className="h-3 w-3" />
317
+ </button>
318
+ </Badge>
319
+ );
320
+ })}
321
+ </div>
322
+ )}
323
+ <Button
324
+ type="button"
325
+ variant="outline"
326
+ size="sm"
327
+ onClick={() => setPickerOpen(true)}
328
+ className="gap-1.5"
329
+ >
330
+ <Plus className="h-3.5 w-3.5" />
331
+ {selectedDocs.length > 0 ? "Add More" : "Select Documents"}
332
+ </Button>
333
+ </div>
334
+ )}
335
+
336
+ <DocumentPickerSheet
337
+ open={pickerOpen}
338
+ onOpenChange={setPickerOpen}
339
+ projectId={project?.id ?? null}
340
+ selectedIds={selectedDocIds}
341
+ onConfirm={handleDocPickerConfirm}
342
+ groupBy="source"
343
+ title="Select Default Documents"
344
+ />
345
+
239
346
  {error && (
240
347
  <p className="text-sm text-destructive">{error}</p>
241
348
  )}
@@ -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,19 @@ 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[], meta: Array<{ id: string; originalName: string; mimeType: string; size: number }>) => {
215
+ setSelectedDocIds(new Set(ids));
216
+ setSelectedDocs(meta);
217
+ },
218
+ []
219
+ );
220
+
204
221
  useEffect(() => {
205
222
  fetch("/api/profiles")
206
223
  .then((r) => r.json())
@@ -261,6 +278,7 @@ export function ScheduleForm({
261
278
  activeHoursEnd,
262
279
  activeTimezone,
263
280
  heartbeatBudgetPerDay,
281
+ documentIds: [...selectedDocIds],
264
282
  });
265
283
  }
266
284
 
@@ -648,6 +666,78 @@ export function ScheduleForm({
648
666
  </div>
649
667
  )}
650
668
 
669
+ {/* Context Documents */}
670
+ <div className="space-y-2">
671
+ <Label className="flex items-center gap-1.5">
672
+ <Paperclip className="h-3.5 w-3.5 text-muted-foreground" />
673
+ Context Documents
674
+ </Label>
675
+ {selectedDocs.length > 0 && (
676
+ <div className="flex flex-wrap gap-2">
677
+ {selectedDocs.map((doc) => {
678
+ const Icon = getFileIcon(doc.mimeType);
679
+ return (
680
+ <Badge
681
+ key={doc.id}
682
+ variant="secondary"
683
+ className="flex items-center gap-1.5 pl-2 pr-1 py-1"
684
+ >
685
+ <Icon className="h-3 w-3" />
686
+ <span className="text-xs max-w-[140px] truncate">
687
+ {doc.originalName}
688
+ </span>
689
+ <span className="text-[10px] text-muted-foreground">
690
+ {formatSize(doc.size)}
691
+ </span>
692
+ <button
693
+ type="button"
694
+ onClick={() => {
695
+ setSelectedDocIds((prev) => {
696
+ const next = new Set(prev);
697
+ next.delete(doc.id);
698
+ return next;
699
+ });
700
+ setSelectedDocs((prev) => prev.filter((d) => d.id !== doc.id));
701
+ }}
702
+ className="ml-0.5 rounded-full p-0.5 hover:bg-muted transition-colors"
703
+ aria-label={`Remove ${doc.originalName}`}
704
+ >
705
+ <X className="h-3 w-3" />
706
+ </button>
707
+ </Badge>
708
+ );
709
+ })}
710
+ </div>
711
+ )}
712
+ <Button
713
+ type="button"
714
+ variant="outline"
715
+ size="sm"
716
+ onClick={() => setPickerOpen(true)}
717
+ className="gap-1.5"
718
+ >
719
+ <Plus className="h-3.5 w-3.5" />
720
+ {selectedDocs.length > 0 ? "Add More" : "Select Documents"}
721
+ </Button>
722
+ {selectedDocs.length > 0 && (
723
+ <p className="text-xs text-muted-foreground">
724
+ {selectedDocs.length} document{selectedDocs.length !== 1 ? "s" : ""} will be provided as context for each firing
725
+ </p>
726
+ )}
727
+ </div>
728
+
729
+ <DocumentPickerSheet
730
+ open={pickerOpen}
731
+ onOpenChange={setPickerOpen}
732
+ projectId={projectId || null}
733
+ selectedIds={selectedDocIds}
734
+ onConfirm={handleDocPickerConfirm}
735
+ groupBy="source"
736
+ title="Select Context Documents"
737
+ allowCrossProject
738
+ selectedDocumentMeta={selectedDocs}
739
+ />
740
+
651
741
  {/* Runtime */}
652
742
  <div className="space-y-2">
653
743
  <Label className="flex items-center gap-1.5">