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,48 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { db } from "@/lib/db";
3
+ import { tasks } from "@/lib/db/schema";
4
+ import { eq, and, ne, isNull } from "drizzle-orm";
5
+
6
+ export async function GET(
7
+ _req: NextRequest,
8
+ { params }: { params: Promise<{ id: string }> }
9
+ ) {
10
+ const { id } = await params;
11
+
12
+ const [task] = await db
13
+ .select({
14
+ workflowId: tasks.workflowId,
15
+ workflowRunNumber: tasks.workflowRunNumber,
16
+ })
17
+ .from(tasks)
18
+ .where(eq(tasks.id, id));
19
+
20
+ if (!task || !task.workflowId) {
21
+ return NextResponse.json([]);
22
+ }
23
+ // Match siblings by workflowId + workflowRunNumber.
24
+ // For pre-existing tasks (workflowRunNumber is NULL), match all tasks
25
+ // in the same workflow that also have NULL workflowRunNumber.
26
+ const runCondition = task.workflowRunNumber != null
27
+ ? eq(tasks.workflowRunNumber, task.workflowRunNumber)
28
+ : isNull(tasks.workflowRunNumber);
29
+
30
+ const siblings = await db
31
+ .select({
32
+ id: tasks.id,
33
+ title: tasks.title,
34
+ status: tasks.status,
35
+ createdAt: tasks.createdAt,
36
+ })
37
+ .from(tasks)
38
+ .where(
39
+ and(
40
+ eq(tasks.workflowId, task.workflowId),
41
+ runCondition,
42
+ ne(tasks.id, id)
43
+ )
44
+ )
45
+ .orderBy(tasks.createdAt);
46
+
47
+ return NextResponse.json(siblings);
48
+ }
@@ -66,27 +66,26 @@ export async function POST(req: NextRequest) {
66
66
  updatedAt: now,
67
67
  });
68
68
 
69
- // Link already-uploaded documents to this task
70
- if (parsed.data.fileIds && parsed.data.fileIds.length > 0) {
69
+ // Link documents to this task (from document picker or legacy fileIds)
70
+ if (parsed.data.documentIds && parsed.data.documentIds.length > 0) {
71
71
  try {
72
- for (const fileId of parsed.data.fileIds) {
73
- // Update existing document record (created by /api/uploads) to link to this task
72
+ for (const docId of parsed.data.documentIds) {
74
73
  await db.update(documents)
75
74
  .set({
76
75
  taskId: id,
77
76
  projectId: parsed.data.projectId ?? null,
78
77
  updatedAt: now,
79
78
  })
80
- .where(eq(documents.id, fileId));
79
+ .where(eq(documents.id, docId));
81
80
 
82
81
  // Trigger processing if not already done (fire-and-forget)
83
- processDocument(fileId).catch((err) => {
84
- console.error(`[tasks] processDocument failed for ${fileId}:`, err);
82
+ processDocument(docId).catch((err) => {
83
+ console.error(`[tasks] processDocument failed for ${docId}:`, err);
85
84
  });
86
85
  }
87
86
  } catch (err) {
88
- // File association is best-effort — don't fail task creation
89
- console.error("[tasks] File association failed:", err);
87
+ // Document association is best-effort — don't fail task creation
88
+ console.error("[tasks] Document association failed:", err);
90
89
  }
91
90
  }
92
91
 
@@ -0,0 +1,209 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { db } from "@/lib/db";
3
+ import {
4
+ workflowDocumentInputs,
5
+ documents,
6
+ workflows,
7
+ } from "@/lib/db/schema";
8
+ import { eq, and, inArray } from "drizzle-orm";
9
+
10
+ type RouteContext = { params: Promise<{ id: string }> };
11
+
12
+ /**
13
+ * GET /api/workflows/[id]/documents
14
+ * List all document bindings for a workflow, with document metadata.
15
+ */
16
+ export async function GET(
17
+ _request: NextRequest,
18
+ context: RouteContext
19
+ ) {
20
+ const { id: workflowId } = await context.params;
21
+
22
+ try {
23
+ const bindings = await db
24
+ .select()
25
+ .from(workflowDocumentInputs)
26
+ .where(eq(workflowDocumentInputs.workflowId, workflowId));
27
+
28
+ if (bindings.length === 0) {
29
+ return NextResponse.json([]);
30
+ }
31
+
32
+ const docIds = bindings.map((b) => b.documentId);
33
+ const docs = await db
34
+ .select()
35
+ .from(documents)
36
+ .where(inArray(documents.id, docIds));
37
+
38
+ const docMap = new Map(docs.map((d) => [d.id, d]));
39
+
40
+ const result = bindings.map((binding) => {
41
+ const doc = docMap.get(binding.documentId);
42
+ return {
43
+ bindingId: binding.id,
44
+ documentId: binding.documentId,
45
+ stepId: binding.stepId,
46
+ createdAt: binding.createdAt,
47
+ document: doc
48
+ ? {
49
+ id: doc.id,
50
+ originalName: doc.originalName,
51
+ filename: doc.filename,
52
+ mimeType: doc.mimeType,
53
+ size: doc.size,
54
+ direction: doc.direction,
55
+ status: doc.status,
56
+ category: doc.category,
57
+ }
58
+ : null,
59
+ };
60
+ });
61
+
62
+ return NextResponse.json(result);
63
+ } catch (error) {
64
+ console.error("[workflow-documents] GET failed:", error);
65
+ return NextResponse.json(
66
+ { error: "Failed to fetch workflow documents" },
67
+ { status: 500 }
68
+ );
69
+ }
70
+ }
71
+
72
+ /**
73
+ * POST /api/workflows/[id]/documents
74
+ * Attach document IDs to a workflow.
75
+ * Body: { documentIds: string[], stepId?: string }
76
+ */
77
+ export async function POST(
78
+ request: NextRequest,
79
+ context: RouteContext
80
+ ) {
81
+ const { id: workflowId } = await context.params;
82
+
83
+ try {
84
+ const body = await request.json();
85
+ const { documentIds, stepId } = body as {
86
+ documentIds: string[];
87
+ stepId?: string;
88
+ };
89
+
90
+ if (!Array.isArray(documentIds) || documentIds.length === 0) {
91
+ return NextResponse.json(
92
+ { error: "documentIds must be a non-empty array" },
93
+ { status: 400 }
94
+ );
95
+ }
96
+
97
+ // Verify workflow exists
98
+ const [workflow] = await db
99
+ .select({ id: workflows.id, projectId: workflows.projectId })
100
+ .from(workflows)
101
+ .where(eq(workflows.id, workflowId));
102
+
103
+ if (!workflow) {
104
+ return NextResponse.json(
105
+ { error: "Workflow not found" },
106
+ { status: 404 }
107
+ );
108
+ }
109
+
110
+ // Verify all documents exist
111
+ const existingDocs = await db
112
+ .select({ id: documents.id })
113
+ .from(documents)
114
+ .where(inArray(documents.id, documentIds));
115
+
116
+ const existingIds = new Set(existingDocs.map((d) => d.id));
117
+ const missing = documentIds.filter((id) => !existingIds.has(id));
118
+ if (missing.length > 0) {
119
+ return NextResponse.json(
120
+ { error: `Documents not found: ${missing.join(", ")}` },
121
+ { status: 404 }
122
+ );
123
+ }
124
+
125
+ // Insert bindings (ignore duplicates via ON CONFLICT)
126
+ const now = new Date();
127
+ const values = documentIds.map((docId) => ({
128
+ id: crypto.randomUUID(),
129
+ workflowId,
130
+ documentId: docId,
131
+ stepId: stepId ?? null,
132
+ createdAt: now,
133
+ }));
134
+
135
+ for (const value of values) {
136
+ try {
137
+ await db.insert(workflowDocumentInputs).values(value);
138
+ } catch (err) {
139
+ // Skip duplicates (unique constraint violation)
140
+ const msg = err instanceof Error ? err.message : "";
141
+ if (!msg.includes("UNIQUE constraint")) throw err;
142
+ }
143
+ }
144
+
145
+ return NextResponse.json(
146
+ { attached: documentIds.length, workflowId, stepId: stepId ?? null },
147
+ { status: 201 }
148
+ );
149
+ } catch (error) {
150
+ console.error("[workflow-documents] POST failed:", error);
151
+ return NextResponse.json(
152
+ { error: "Failed to attach documents" },
153
+ { status: 500 }
154
+ );
155
+ }
156
+ }
157
+
158
+ /**
159
+ * DELETE /api/workflows/[id]/documents
160
+ * Remove document bindings from a workflow.
161
+ * Body: { documentIds: string[], stepId?: string }
162
+ * If no body, removes all bindings.
163
+ */
164
+ export async function DELETE(
165
+ request: NextRequest,
166
+ context: RouteContext
167
+ ) {
168
+ const { id: workflowId } = await context.params;
169
+
170
+ try {
171
+ let body: { documentIds?: string[]; stepId?: string } = {};
172
+ try {
173
+ body = await request.json();
174
+ } catch {
175
+ // Empty body = remove all
176
+ }
177
+
178
+ const { documentIds, stepId } = body;
179
+
180
+ if (documentIds && documentIds.length > 0) {
181
+ // Remove specific bindings
182
+ for (const docId of documentIds) {
183
+ const conditions = [
184
+ eq(workflowDocumentInputs.workflowId, workflowId),
185
+ eq(workflowDocumentInputs.documentId, docId),
186
+ ];
187
+ if (stepId !== undefined) {
188
+ conditions.push(eq(workflowDocumentInputs.stepId, stepId));
189
+ }
190
+ await db
191
+ .delete(workflowDocumentInputs)
192
+ .where(and(...conditions));
193
+ }
194
+ } else {
195
+ // Remove all bindings for this workflow
196
+ await db
197
+ .delete(workflowDocumentInputs)
198
+ .where(eq(workflowDocumentInputs.workflowId, workflowId));
199
+ }
200
+
201
+ return NextResponse.json({ ok: true });
202
+ } catch (error) {
203
+ console.error("[workflow-documents] DELETE failed:", error);
204
+ return NextResponse.json(
205
+ { error: "Failed to remove document bindings" },
206
+ { status: 500 }
207
+ );
208
+ }
209
+ }
@@ -1,7 +1,7 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
2
  import { db } from "@/lib/db";
3
3
  import { workflows } from "@/lib/db/schema";
4
- import { eq, and } from "drizzle-orm";
4
+ import { eq, and, sql } from "drizzle-orm";
5
5
  import { executeWorkflow } from "@/lib/workflows/engine";
6
6
  import type { WorkflowDefinition } from "@/lib/workflows/types";
7
7
 
@@ -57,7 +57,11 @@ export async function POST(
57
57
  // Prevents concurrent double-execution from parallel requests.
58
58
  const claimResult = db
59
59
  .update(workflows)
60
- .set({ status: "active", updatedAt: new Date() })
60
+ .set({
61
+ status: "active",
62
+ runNumber: sql`${workflows.runNumber} + 1`,
63
+ updatedAt: new Date(),
64
+ })
61
65
  .where(
62
66
  and(
63
67
  eq(workflows.id, id),
@@ -35,17 +35,30 @@ export async function PATCH(
35
35
  return NextResponse.json({ error: "Workflow not found" }, { status: 404 });
36
36
  }
37
37
 
38
- // Edit name/definition — draft only
38
+ // Edit name/definition — draft, completed, or failed
39
39
  if (name !== undefined || definition !== undefined) {
40
- if (workflow.status !== "draft") {
40
+ if (!["draft", "completed", "failed"].includes(workflow.status)) {
41
41
  return NextResponse.json(
42
- { error: "Can only edit draft workflows" },
42
+ { error: "Cannot edit active or paused workflows" },
43
43
  { status: 409 }
44
44
  );
45
45
  }
46
46
 
47
47
  const updates: Record<string, unknown> = { updatedAt: new Date() };
48
48
 
49
+ // Reset non-draft workflows to draft and strip execution state
50
+ if (workflow.status !== "draft") {
51
+ updates.status = "draft";
52
+ try {
53
+ const existingDef = JSON.parse(workflow.definition) as Record<string, unknown>;
54
+ delete existingDef._state;
55
+ delete existingDef._loopState;
56
+ updates.definition = JSON.stringify(existingDef);
57
+ } catch {
58
+ // Definition will be overwritten below if provided
59
+ }
60
+ }
61
+
49
62
  if (name !== undefined) {
50
63
  if (!name.trim()) {
51
64
  return NextResponse.json({ error: "Name is required" }, { status: 400 });
@@ -1,7 +1,7 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
2
  import { db } from "@/lib/db";
3
- import { workflows, documents } from "@/lib/db/schema";
4
- import { eq, and, inArray } from "drizzle-orm";
3
+ import { workflows, tasks, documents } from "@/lib/db/schema";
4
+ import { eq, and, inArray, count, desc, sql as drizzleSql } from "drizzle-orm";
5
5
  import { parseWorkflowState } from "@/lib/workflows/engine";
6
6
 
7
7
  /** Collect output documents for workflow step tasks + input documents from parent task */
@@ -81,6 +81,18 @@ export async function GET(
81
81
  return NextResponse.json({ error: "Workflow not found" }, { status: 404 });
82
82
  }
83
83
 
84
+ const runHistory = await db
85
+ .select({
86
+ runNumber: tasks.workflowRunNumber,
87
+ taskCount: count(tasks.id),
88
+ completedCount: drizzleSql<number>`SUM(CASE WHEN ${tasks.status} = 'completed' THEN 1 ELSE 0 END)`,
89
+ failedCount: drizzleSql<number>`SUM(CASE WHEN ${tasks.status} = 'failed' THEN 1 ELSE 0 END)`,
90
+ })
91
+ .from(tasks)
92
+ .where(eq(tasks.workflowId, id))
93
+ .groupBy(tasks.workflowRunNumber)
94
+ .orderBy(desc(tasks.workflowRunNumber));
95
+
84
96
  const { definition, state, loopState } = parseWorkflowState(workflow.definition);
85
97
  const sourceTaskId: string | undefined = definition.sourceTaskId;
86
98
  const { stepDocuments, parentDocuments } = await getWorkflowDocuments(state, sourceTaskId);
@@ -100,6 +112,8 @@ export async function GET(
100
112
  steps: definition.steps,
101
113
  stepDocuments,
102
114
  parentDocuments,
115
+ runNumber: workflow.runNumber,
116
+ runHistory,
103
117
  });
104
118
  }
105
119
 
@@ -118,5 +132,7 @@ export async function GET(
118
132
  workflowState: state,
119
133
  stepDocuments,
120
134
  parentDocuments,
135
+ runNumber: workflow.runNumber,
136
+ runHistory,
121
137
  });
122
138
  }
@@ -1,14 +1,25 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
2
  import { db } from "@/lib/db";
3
3
  import { workflows } from "@/lib/db/schema";
4
- import { desc, eq } from "drizzle-orm";
4
+ import { desc, eq, sql } from "drizzle-orm";
5
5
  import type { WorkflowDefinition } from "@/lib/workflows/types";
6
6
  import { validateWorkflowDefinitionAssignments } from "@/lib/agents/profiles/assignment-validation";
7
7
  import { validateWorkflowDefinition } from "@/lib/workflows/definition-validation";
8
8
 
9
9
  export async function GET() {
10
10
  const result = await db
11
- .select()
11
+ .select({
12
+ id: workflows.id,
13
+ name: workflows.name,
14
+ projectId: workflows.projectId,
15
+ definition: workflows.definition,
16
+ status: workflows.status,
17
+ runNumber: workflows.runNumber,
18
+ createdAt: workflows.createdAt,
19
+ updatedAt: workflows.updatedAt,
20
+ taskCount: sql<number>`(SELECT COUNT(*) FROM tasks t WHERE t.workflow_id = "workflows"."id")`.as("taskCount"),
21
+ outputDocCount: sql<number>`(SELECT COUNT(*) FROM documents d WHERE d.task_id IN (SELECT t2.id FROM tasks t2 WHERE t2.workflow_id = "workflows"."id") AND d.direction = 'output')`.as("outputDocCount"),
22
+ })
12
23
  .from(workflows)
13
24
  .orderBy(desc(workflows.createdAt));
14
25
 
@@ -1,5 +1,5 @@
1
1
  import { db } from "@/lib/db";
2
- import { documents, tasks, projects } from "@/lib/db/schema";
2
+ import { documents, tasks, projects, workflows } from "@/lib/db/schema";
3
3
  import { desc, eq } from "drizzle-orm";
4
4
  import { DocumentBrowser } from "@/components/documents/document-browser";
5
5
  import { PageShell } from "@/components/shared/page-shell";
@@ -31,9 +31,13 @@ export default async function DocumentsPage() {
31
31
  updatedAt: documents.updatedAt,
32
32
  taskTitle: tasks.title,
33
33
  projectName: projects.name,
34
+ workflowId: workflows.id,
35
+ workflowName: workflows.name,
36
+ workflowRunNumber: tasks.workflowRunNumber,
34
37
  })
35
38
  .from(documents)
36
39
  .leftJoin(tasks, eq(documents.taskId, tasks.id))
40
+ .leftJoin(workflows, eq(tasks.workflowId, workflows.id))
37
41
  .leftJoin(projects, eq(documents.projectId, projects.id))
38
42
  .orderBy(desc(documents.createdAt));
39
43
 
@@ -25,7 +25,6 @@ export const metadata: Metadata = {
25
25
  icons: {
26
26
  icon: [
27
27
  { url: "/stagent-s-64.png", sizes: "64x64", type: "image/png" },
28
- { url: "/icon.svg", type: "image/svg+xml" },
29
28
  ],
30
29
  apple: [
31
30
  { url: "/stagent-s-128.png", sizes: "128x128", type: "image/png" },
@@ -23,9 +23,9 @@ export default function manifest(): MetadataRoute.Manifest {
23
23
  purpose: "maskable",
24
24
  },
25
25
  {
26
- src: "/icon.svg",
27
- sizes: "any",
28
- type: "image/svg+xml",
26
+ src: "/stagent-s-64.png",
27
+ sizes: "64x64",
28
+ type: "image/png",
29
29
  },
30
30
  ],
31
31
  };
@@ -1,12 +1,14 @@
1
1
  import { notFound } from "next/navigation";
2
2
  import { db } from "@/lib/db";
3
- import { projects, tasks, workflows } from "@/lib/db/schema";
4
- import { eq, count, getTableColumns } from "drizzle-orm";
3
+ import { projects, tasks, workflows, documents } from "@/lib/db/schema";
4
+ import { eq, count, desc, getTableColumns } from "drizzle-orm";
5
5
  import { Badge } from "@/components/ui/badge";
6
6
  import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
7
7
  import { COLUMN_ORDER } from "@/lib/constants/task-status";
8
8
  import { PageShell } from "@/components/shared/page-shell";
9
9
  import { ProjectDetailClient } from "@/components/projects/project-detail";
10
+ import Link from "next/link";
11
+ import { FileText } from "lucide-react";
10
12
  import { Sparkline } from "@/components/charts/sparkline";
11
13
  import { getProjectCompletionTrend } from "@/lib/queries/chart-data";
12
14
  import { EnvironmentSummaryCard } from "@/components/environment/environment-summary-card";
@@ -38,6 +40,27 @@ export default async function ProjectDetailPage({
38
40
  .where(eq(tasks.projectId, id))
39
41
  .orderBy(tasks.priority, tasks.createdAt);
40
42
 
43
+ // Document count and recent docs
44
+ const [{ docCount }] = await db
45
+ .select({ docCount: count(documents.id) })
46
+ .from(documents)
47
+ .where(eq(documents.projectId, id));
48
+
49
+ const recentDocs = docCount > 0 ? await db
50
+ .select({
51
+ id: documents.id,
52
+ originalName: documents.originalName,
53
+ direction: documents.direction,
54
+ version: documents.version,
55
+ size: documents.size,
56
+ createdAt: documents.createdAt,
57
+ })
58
+ .from(documents)
59
+ .where(eq(documents.projectId, id))
60
+ .orderBy(desc(documents.createdAt))
61
+ .limit(5)
62
+ : [];
63
+
41
64
  // Status breakdown (standalone tasks only for headline metrics)
42
65
  const statusCounts: Record<string, number> = {};
43
66
  const standaloneForCounts = projectTasks.filter((t) => !t.workflowId);
@@ -144,6 +167,40 @@ export default async function ProjectDetailPage({
144
167
  </div>
145
168
  )}
146
169
 
170
+ {/* Recent documents */}
171
+ {recentDocs.length > 0 && (
172
+ <div className="mb-6">
173
+ <Card>
174
+ <CardHeader className="pb-2">
175
+ <div className="flex items-center justify-between">
176
+ <CardTitle className="text-sm">Recent Documents</CardTitle>
177
+ <Link href={`/documents?projectId=${id}`} className="text-xs text-muted-foreground hover:text-foreground transition-colors">
178
+ View all &rarr;
179
+ </Link>
180
+ </div>
181
+ </CardHeader>
182
+ <CardContent className="divide-y divide-border">
183
+ {recentDocs.map((doc) => (
184
+ <Link
185
+ key={doc.id}
186
+ href={`/documents/${doc.id}`}
187
+ className="flex items-center gap-3 py-2 text-xs hover:bg-accent/50 transition-colors -mx-6 px-6"
188
+ >
189
+ <FileText className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
190
+ <span className="truncate flex-1">{doc.originalName}</span>
191
+ <Badge variant="outline" className="text-[10px]">
192
+ {doc.direction}
193
+ </Badge>
194
+ {doc.direction === "output" && (
195
+ <span className="text-muted-foreground">v{doc.version}</span>
196
+ )}
197
+ </Link>
198
+ ))}
199
+ </CardContent>
200
+ </Card>
201
+ </div>
202
+ )}
203
+
147
204
  {/* Task count summary */}
148
205
  {(standaloneCount > 0 || workflowCount > 0) && (
149
206
  <p className="text-xs text-muted-foreground mb-4">
@@ -151,6 +208,9 @@ export default async function ProjectDetailPage({
151
208
  {workflowCount > 0 && (
152
209
  <> &middot; {workflowCount} workflow task{workflowCount !== 1 ? "s" : ""} across {workflowGroupCount} workflow{workflowGroupCount !== 1 ? "s" : ""}</>
153
210
  )}
211
+ {docCount > 0 && (
212
+ <> &middot; {docCount} document{docCount !== 1 ? "s" : ""}</>
213
+ )}
154
214
  </p>
155
215
  )}
156
216
 
@@ -1,6 +1,7 @@
1
1
  import { ProvidersAndRuntimesSection } from "@/components/settings/providers-runtimes-section";
2
2
  import { PermissionsSections } from "@/components/settings/permissions-sections";
3
3
  import { DataManagementSection } from "@/components/settings/data-management-section";
4
+ import { DatabaseSnapshotsSection } from "@/components/settings/database-snapshots-section";
4
5
  import { BudgetGuardrailsSection } from "@/components/settings/budget-guardrails-section";
5
6
  import { ChatSettingsSection } from "@/components/settings/chat-settings-section";
6
7
  import { RuntimeTimeoutSection } from "@/components/settings/runtime-timeout-section";
@@ -27,6 +28,7 @@ export default function SettingsPage() {
27
28
  <ChannelsSection />
28
29
  <BudgetGuardrailsSection />
29
30
  <PermissionsSections />
31
+ <DatabaseSnapshotsSection />
30
32
  <DataManagementSection />
31
33
  </div>
32
34
  </PageShell>
@@ -0,0 +1,67 @@
1
+ import { notFound } from "next/navigation";
2
+ import { db } from "@/lib/db";
3
+ import { projects } from "@/lib/db/schema";
4
+ import { eq } from "drizzle-orm";
5
+ import { getTable, listRows } from "@/lib/data/tables";
6
+ import { PageShell } from "@/components/shared/page-shell";
7
+ import { TableDetailTabs } from "@/components/tables/table-detail-tabs";
8
+ import { evaluateComputedColumns } from "@/lib/tables/computed";
9
+ import type { ColumnDef } from "@/lib/tables/types";
10
+
11
+ export const dynamic = "force-dynamic";
12
+
13
+ interface Props {
14
+ params: Promise<{ id: string }>;
15
+ }
16
+
17
+ export default async function TableDetailPage({ params }: Props) {
18
+ const { id } = await params;
19
+ const table = await getTable(id);
20
+
21
+ if (!table) {
22
+ notFound();
23
+ }
24
+
25
+ let columns: ColumnDef[] = [];
26
+ try {
27
+ columns = JSON.parse(table.columnSchema) as ColumnDef[];
28
+ } catch {
29
+ columns = [];
30
+ }
31
+
32
+ // Fetch project name if table is linked to a project
33
+ let projectName: string | null = null;
34
+ if (table.projectId) {
35
+ const project = db
36
+ .select({ name: projects.name })
37
+ .from(projects)
38
+ .where(eq(projects.id, table.projectId))
39
+ .get();
40
+ projectName = project?.name ?? null;
41
+ }
42
+
43
+ const rawRows = await listRows(id, { limit: 500 });
44
+ const rows = evaluateComputedColumns(columns, rawRows);
45
+
46
+ return (
47
+ <PageShell
48
+ title={table.name}
49
+ description={table.description ?? undefined}
50
+ backHref="/tables"
51
+ backLabel="Tables"
52
+ >
53
+ <TableDetailTabs
54
+ tableId={id}
55
+ columns={columns}
56
+ initialRows={rows}
57
+ tableMeta={{
58
+ source: table.source,
59
+ projectName,
60
+ rowCount: table.rowCount,
61
+ createdAt: table.createdAt ? new Date(table.createdAt).toISOString() : null,
62
+ updatedAt: table.updatedAt ? new Date(table.updatedAt).toISOString() : null,
63
+ }}
64
+ />
65
+ </PageShell>
66
+ );
67
+ }
@@ -0,0 +1,21 @@
1
+ import { listTables } from "@/lib/data/tables";
2
+ import { db } from "@/lib/db";
3
+ import { projects } from "@/lib/db/schema";
4
+ import { TableBrowser } from "@/components/tables/table-browser";
5
+ import { PageShell } from "@/components/shared/page-shell";
6
+
7
+ export const dynamic = "force-dynamic";
8
+
9
+ export default async function TablesPage() {
10
+ const tables = await listTables();
11
+
12
+ const projectList = await db
13
+ .select({ id: projects.id, name: projects.name })
14
+ .from(projects);
15
+
16
+ return (
17
+ <PageShell title="Tables">
18
+ <TableBrowser initialTables={tables} projects={projectList} />
19
+ </PageShell>
20
+ );
21
+ }