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,446 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useCallback } from "react";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Input } from "@/components/ui/input";
6
+ import { Label } from "@/components/ui/label";
7
+ import { Badge } from "@/components/ui/badge";
8
+ import { Switch } from "@/components/ui/switch";
9
+ import {
10
+ Select,
11
+ SelectContent,
12
+ SelectItem,
13
+ SelectTrigger,
14
+ SelectValue,
15
+ } from "@/components/ui/select";
16
+ import {
17
+ Sheet,
18
+ SheetContent,
19
+ SheetHeader,
20
+ SheetTitle,
21
+ SheetFooter,
22
+ } from "@/components/ui/sheet";
23
+ import { Plus, Zap, Pencil } from "lucide-react";
24
+ import { toast } from "sonner";
25
+
26
+ interface Trigger {
27
+ id: string;
28
+ name: string;
29
+ eventType: "row_added" | "row_updated" | "row_deleted";
30
+ condition: {
31
+ column: string;
32
+ operator: string;
33
+ value: string;
34
+ } | null;
35
+ actionType: "run_workflow" | "create_task";
36
+ actionConfig: Record<string, string>;
37
+ status: "active" | "paused";
38
+ fireCount: number;
39
+ }
40
+
41
+ interface TableTriggersTabProps {
42
+ tableId: string;
43
+ }
44
+
45
+ const EVENT_TYPES = [
46
+ { value: "row_added", label: "Row Added" },
47
+ { value: "row_updated", label: "Row Updated" },
48
+ { value: "row_deleted", label: "Row Deleted" },
49
+ ] as const;
50
+
51
+ const OPERATORS = [
52
+ { value: "equals", label: "equals" },
53
+ { value: "not_equals", label: "not equals" },
54
+ { value: "contains", label: "contains" },
55
+ { value: "greater_than", label: "greater than" },
56
+ { value: "less_than", label: "less than" },
57
+ { value: "is_empty", label: "is empty" },
58
+ { value: "is_not_empty", label: "is not empty" },
59
+ ] as const;
60
+
61
+ const ACTION_TYPES = [
62
+ { value: "run_workflow", label: "Run Workflow" },
63
+ { value: "create_task", label: "Create Task" },
64
+ ] as const;
65
+
66
+ function eventBadgeVariant(eventType: string) {
67
+ switch (eventType) {
68
+ case "row_added":
69
+ return "default" as const;
70
+ case "row_updated":
71
+ return "secondary" as const;
72
+ case "row_deleted":
73
+ return "destructive" as const;
74
+ default:
75
+ return "outline" as const;
76
+ }
77
+ }
78
+
79
+ export function TableTriggersTab({ tableId }: TableTriggersTabProps) {
80
+ const [triggers, setTriggers] = useState<Trigger[]>([]);
81
+ const [loading, setLoading] = useState(true);
82
+ const [sheetOpen, setSheetOpen] = useState(false);
83
+ const [editingTrigger, setEditingTrigger] = useState<Trigger | null>(null);
84
+
85
+ // Form state
86
+ const [name, setName] = useState("");
87
+ const [eventType, setEventType] = useState<Trigger["eventType"]>("row_added");
88
+ const [conditionColumn, setConditionColumn] = useState("");
89
+ const [conditionOperator, setConditionOperator] = useState("equals");
90
+ const [conditionValue, setConditionValue] = useState("");
91
+ const [hasCondition, setHasCondition] = useState(false);
92
+ const [actionType, setActionType] = useState<Trigger["actionType"]>("run_workflow");
93
+ const [actionTargetId, setActionTargetId] = useState("");
94
+ const [saving, setSaving] = useState(false);
95
+
96
+ const fetchTriggers = useCallback(async () => {
97
+ try {
98
+ const res = await fetch(`/api/tables/${tableId}/triggers`);
99
+ if (res.ok) {
100
+ const raw = await res.json();
101
+ const list = raw.triggers ?? raw ?? [];
102
+ // Map API shape (triggerEvent, JSON strings) to component shape (eventType, parsed objects)
103
+ setTriggers(
104
+ list.map((t: Record<string, unknown>) => ({
105
+ ...t,
106
+ eventType: t.triggerEvent ?? t.eventType,
107
+ condition: typeof t.condition === "string" ? JSON.parse(t.condition) : t.condition ?? null,
108
+ actionConfig: typeof t.actionConfig === "string" ? JSON.parse(t.actionConfig) : t.actionConfig ?? {},
109
+ }))
110
+ );
111
+ }
112
+ } catch {
113
+ // silent
114
+ } finally {
115
+ setLoading(false);
116
+ }
117
+ }, [tableId]);
118
+
119
+ useEffect(() => {
120
+ fetchTriggers();
121
+ }, [fetchTriggers]);
122
+
123
+ function resetForm() {
124
+ setName("");
125
+ setEventType("row_added");
126
+ setConditionColumn("");
127
+ setConditionOperator("equals");
128
+ setConditionValue("");
129
+ setHasCondition(false);
130
+ setActionType("run_workflow");
131
+ setActionTargetId("");
132
+ setEditingTrigger(null);
133
+ }
134
+
135
+ function openAdd() {
136
+ resetForm();
137
+ setSheetOpen(true);
138
+ }
139
+
140
+ function openEdit(trigger: Trigger) {
141
+ setEditingTrigger(trigger);
142
+ setName(trigger.name);
143
+ setEventType(trigger.eventType);
144
+ if (trigger.condition) {
145
+ setHasCondition(true);
146
+ setConditionColumn(trigger.condition.column);
147
+ setConditionOperator(trigger.condition.operator);
148
+ setConditionValue(trigger.condition.value);
149
+ } else {
150
+ setHasCondition(false);
151
+ setConditionColumn("");
152
+ setConditionOperator("equals");
153
+ setConditionValue("");
154
+ }
155
+ setActionType(trigger.actionType);
156
+ setActionTargetId(trigger.actionConfig?.targetId ?? "");
157
+ setSheetOpen(true);
158
+ }
159
+
160
+ async function handleToggle(trigger: Trigger) {
161
+ const newStatus = trigger.status === "active" ? "paused" : "active";
162
+ try {
163
+ const res = await fetch(`/api/tables/${tableId}/triggers/${trigger.id}`, {
164
+ method: "PATCH",
165
+ headers: { "Content-Type": "application/json" },
166
+ body: JSON.stringify({ status: newStatus }),
167
+ });
168
+ if (res.ok) {
169
+ setTriggers((prev) =>
170
+ prev.map((t) => (t.id === trigger.id ? { ...t, status: newStatus } : t))
171
+ );
172
+ toast.success(`Trigger ${newStatus === "active" ? "activated" : "paused"}`);
173
+ } else {
174
+ toast.error("Failed to update trigger status");
175
+ }
176
+ } catch {
177
+ toast.error("Failed to update trigger status");
178
+ }
179
+ }
180
+
181
+ async function handleSubmit() {
182
+ if (!name.trim()) {
183
+ toast.error("Trigger name is required");
184
+ return;
185
+ }
186
+
187
+ setSaving(true);
188
+ try {
189
+ const payload = {
190
+ name: name.trim(),
191
+ triggerEvent: eventType,
192
+ condition: hasCondition
193
+ ? { column: conditionColumn, operator: conditionOperator, value: conditionValue }
194
+ : null,
195
+ actionType,
196
+ actionConfig: { targetId: actionTargetId },
197
+ };
198
+
199
+ const isEdit = !!editingTrigger;
200
+ const url = isEdit
201
+ ? `/api/tables/${tableId}/triggers/${editingTrigger.id}`
202
+ : `/api/tables/${tableId}/triggers`;
203
+ const method = isEdit ? "PATCH" : "POST";
204
+
205
+ const res = await fetch(url, {
206
+ method,
207
+ headers: { "Content-Type": "application/json" },
208
+ body: JSON.stringify(payload),
209
+ });
210
+
211
+ if (!res.ok) {
212
+ const err = await res.json().catch(() => ({}));
213
+ toast.error(err.error || `Failed to ${isEdit ? "update" : "create"} trigger`);
214
+ return;
215
+ }
216
+
217
+ toast.success(`Trigger "${name}" ${isEdit ? "updated" : "created"}`);
218
+ setSheetOpen(false);
219
+ resetForm();
220
+ fetchTriggers();
221
+ } catch {
222
+ toast.error("Failed to save trigger");
223
+ } finally {
224
+ setSaving(false);
225
+ }
226
+ }
227
+
228
+ if (loading) {
229
+ return (
230
+ <div className="flex items-center justify-center py-12 text-sm text-muted-foreground">
231
+ Loading triggers...
232
+ </div>
233
+ );
234
+ }
235
+
236
+ return (
237
+ <div className="space-y-4">
238
+ <div className="flex items-center justify-between">
239
+ <h3 className="text-sm font-medium">
240
+ Triggers ({triggers.length})
241
+ </h3>
242
+ <Button variant="outline" size="sm" onClick={openAdd}>
243
+ <Plus className="h-4 w-4 mr-1" />
244
+ Add Trigger
245
+ </Button>
246
+ </div>
247
+
248
+ {triggers.length === 0 ? (
249
+ <div className="rounded-lg border border-dashed p-8 text-center">
250
+ <Zap className="mx-auto h-8 w-8 text-muted-foreground/50" />
251
+ <p className="mt-2 text-sm text-muted-foreground">
252
+ No triggers configured. Add a trigger to automate actions when rows change.
253
+ </p>
254
+ </div>
255
+ ) : (
256
+ <div className="space-y-2">
257
+ {triggers.map((trigger) => (
258
+ <div
259
+ key={trigger.id}
260
+ className="flex items-center gap-3 rounded-lg border p-3"
261
+ >
262
+ <div className="flex-1 min-w-0">
263
+ <div className="flex items-center gap-2">
264
+ <span className="text-sm font-medium truncate">
265
+ {trigger.name}
266
+ </span>
267
+ <Badge variant={eventBadgeVariant(trigger.eventType)}>
268
+ {trigger.eventType.replace(/_/g, " ")}
269
+ </Badge>
270
+ </div>
271
+ {trigger.condition && (
272
+ <p className="text-xs text-muted-foreground mt-0.5">
273
+ When {trigger.condition.column} {trigger.condition.operator}{" "}
274
+ {trigger.condition.value}
275
+ </p>
276
+ )}
277
+ </div>
278
+
279
+ <span className="text-xs text-muted-foreground whitespace-nowrap">
280
+ {trigger.fireCount} fires
281
+ </span>
282
+
283
+ <Switch
284
+ checked={trigger.status === "active"}
285
+ onCheckedChange={() => handleToggle(trigger)}
286
+ aria-label={`Toggle ${trigger.name}`}
287
+ />
288
+
289
+ <Button
290
+ variant="ghost"
291
+ size="icon"
292
+ className="h-8 w-8 shrink-0"
293
+ onClick={() => openEdit(trigger)}
294
+ >
295
+ <Pencil className="h-3.5 w-3.5" />
296
+ </Button>
297
+ </div>
298
+ ))}
299
+ </div>
300
+ )}
301
+
302
+ {/* Trigger config sheet */}
303
+ <Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
304
+ <SheetContent side="right" className="w-[420px] sm:max-w-[420px]">
305
+ <SheetHeader>
306
+ <SheetTitle>
307
+ {editingTrigger ? "Edit Trigger" : "Add Trigger"}
308
+ </SheetTitle>
309
+ </SheetHeader>
310
+
311
+ <div className="px-6 pb-6 space-y-4 overflow-y-auto">
312
+ <div className="space-y-2">
313
+ <Label htmlFor="trigger-name">Name</Label>
314
+ <Input
315
+ id="trigger-name"
316
+ placeholder="e.g. Notify on new row"
317
+ value={name}
318
+ onChange={(e) => setName(e.target.value)}
319
+ />
320
+ </div>
321
+
322
+ <div className="space-y-2">
323
+ <Label>Event Type</Label>
324
+ <Select
325
+ value={eventType}
326
+ onValueChange={(v) => setEventType(v as Trigger["eventType"])}
327
+ >
328
+ <SelectTrigger>
329
+ <SelectValue />
330
+ </SelectTrigger>
331
+ <SelectContent>
332
+ {EVENT_TYPES.map((et) => (
333
+ <SelectItem key={et.value} value={et.value}>
334
+ {et.label}
335
+ </SelectItem>
336
+ ))}
337
+ </SelectContent>
338
+ </Select>
339
+ </div>
340
+
341
+ {/* Condition builder */}
342
+ <div className="space-y-2">
343
+ <div className="flex items-center justify-between">
344
+ <Label htmlFor="trigger-condition">Condition</Label>
345
+ <Switch
346
+ id="trigger-condition"
347
+ checked={hasCondition}
348
+ onCheckedChange={setHasCondition}
349
+ />
350
+ </div>
351
+ {hasCondition && (
352
+ <div className="space-y-2 rounded-md border p-3">
353
+ <div className="space-y-1">
354
+ <Label className="text-xs">Column</Label>
355
+ <Input
356
+ placeholder="Column name"
357
+ value={conditionColumn}
358
+ onChange={(e) => setConditionColumn(e.target.value)}
359
+ className="h-8"
360
+ />
361
+ </div>
362
+ <div className="space-y-1">
363
+ <Label className="text-xs">Operator</Label>
364
+ <Select
365
+ value={conditionOperator}
366
+ onValueChange={setConditionOperator}
367
+ >
368
+ <SelectTrigger className="h-8">
369
+ <SelectValue />
370
+ </SelectTrigger>
371
+ <SelectContent>
372
+ {OPERATORS.map((op) => (
373
+ <SelectItem key={op.value} value={op.value}>
374
+ {op.label}
375
+ </SelectItem>
376
+ ))}
377
+ </SelectContent>
378
+ </Select>
379
+ </div>
380
+ <div className="space-y-1">
381
+ <Label className="text-xs">Value</Label>
382
+ <Input
383
+ placeholder="Comparison value"
384
+ value={conditionValue}
385
+ onChange={(e) => setConditionValue(e.target.value)}
386
+ className="h-8"
387
+ />
388
+ </div>
389
+ </div>
390
+ )}
391
+ </div>
392
+
393
+ {/* Action config */}
394
+ <div className="space-y-2">
395
+ <Label>Action Type</Label>
396
+ <Select
397
+ value={actionType}
398
+ onValueChange={(v) => setActionType(v as Trigger["actionType"])}
399
+ >
400
+ <SelectTrigger>
401
+ <SelectValue />
402
+ </SelectTrigger>
403
+ <SelectContent>
404
+ {ACTION_TYPES.map((at) => (
405
+ <SelectItem key={at.value} value={at.value}>
406
+ {at.label}
407
+ </SelectItem>
408
+ ))}
409
+ </SelectContent>
410
+ </Select>
411
+ </div>
412
+
413
+ <div className="space-y-2">
414
+ <Label htmlFor="trigger-target">
415
+ {actionType === "run_workflow" ? "Workflow ID" : "Task Template"}
416
+ </Label>
417
+ <Input
418
+ id="trigger-target"
419
+ placeholder={
420
+ actionType === "run_workflow"
421
+ ? "Select or enter workflow ID"
422
+ : "Select or enter task template"
423
+ }
424
+ value={actionTargetId}
425
+ onChange={(e) => setActionTargetId(e.target.value)}
426
+ />
427
+ </div>
428
+ </div>
429
+
430
+ <SheetFooter className="px-6">
431
+ <Button variant="outline" onClick={() => setSheetOpen(false)}>
432
+ Cancel
433
+ </Button>
434
+ <Button onClick={handleSubmit} disabled={saving}>
435
+ {saving
436
+ ? "Saving..."
437
+ : editingTrigger
438
+ ? "Update Trigger"
439
+ : "Add Trigger"}
440
+ </Button>
441
+ </SheetFooter>
442
+ </SheetContent>
443
+ </Sheet>
444
+ </div>
445
+ );
446
+ }
@@ -0,0 +1,6 @@
1
+ import type { UserTableRow } from "@/lib/db/schema";
2
+
3
+ export type TableWithRelations = UserTableRow & {
4
+ projectName: string | null;
5
+ columnCount: number;
6
+ };
@@ -0,0 +1,171 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+
3
+ export interface CellPosition {
4
+ rowIndex: number;
5
+ colIndex: number;
6
+ }
7
+
8
+ export type SpreadsheetMode = "idle" | "navigating" | "editing";
9
+
10
+ interface UseSpreadsheetKeysOptions {
11
+ rowCount: number;
12
+ colCount: number;
13
+ onStartEdit: (pos: CellPosition) => void;
14
+ onConfirmEdit: () => void;
15
+ onCancelEdit: () => void;
16
+ onClearCell: (pos: CellPosition) => void;
17
+ onAddRow: () => void;
18
+ }
19
+
20
+ interface UseSpreadsheetKeysReturn {
21
+ activeCell: CellPosition | null;
22
+ mode: SpreadsheetMode;
23
+ setActiveCell: (pos: CellPosition | null) => void;
24
+ setMode: (mode: SpreadsheetMode) => void;
25
+ handleCellClick: (pos: CellPosition) => void;
26
+ handleCellDoubleClick: (pos: CellPosition) => void;
27
+ }
28
+
29
+ export function useSpreadsheetKeys(
30
+ opts: UseSpreadsheetKeysOptions
31
+ ): UseSpreadsheetKeysReturn {
32
+ const [activeCell, setActiveCell] = useState<CellPosition | null>(null);
33
+ const [mode, setMode] = useState<SpreadsheetMode>("idle");
34
+
35
+ const { rowCount, colCount, onStartEdit, onConfirmEdit, onCancelEdit, onClearCell, onAddRow } = opts;
36
+
37
+ const moveTo = useCallback(
38
+ (pos: CellPosition) => {
39
+ const clamped: CellPosition = {
40
+ rowIndex: Math.max(0, Math.min(pos.rowIndex, rowCount - 1)),
41
+ colIndex: Math.max(0, Math.min(pos.colIndex, colCount - 1)),
42
+ };
43
+ setActiveCell(clamped);
44
+ },
45
+ [rowCount, colCount]
46
+ );
47
+
48
+ const handleCellClick = useCallback(
49
+ (pos: CellPosition) => {
50
+ setActiveCell(pos);
51
+ if (mode === "editing") {
52
+ onConfirmEdit();
53
+ }
54
+ setMode("navigating");
55
+ },
56
+ [mode, onConfirmEdit]
57
+ );
58
+
59
+ const handleCellDoubleClick = useCallback(
60
+ (pos: CellPosition) => {
61
+ setActiveCell(pos);
62
+ setMode("editing");
63
+ onStartEdit(pos);
64
+ },
65
+ [onStartEdit]
66
+ );
67
+
68
+ useEffect(() => {
69
+ if (!activeCell) return;
70
+
71
+ function handleKeyDown(e: KeyboardEvent) {
72
+ // Don't interfere with inputs outside the spreadsheet
73
+ const target = e.target as HTMLElement;
74
+ const isInSheet = target.closest("[data-spreadsheet]");
75
+ const isInInput = target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT";
76
+
77
+ if (mode === "editing") {
78
+ if (e.key === "Escape") {
79
+ e.preventDefault();
80
+ onCancelEdit();
81
+ setMode("navigating");
82
+ } else if (e.key === "Enter" && !e.shiftKey) {
83
+ e.preventDefault();
84
+ onConfirmEdit();
85
+ setMode("navigating");
86
+ // Move down after confirm
87
+ if (activeCell) {
88
+ if (activeCell.rowIndex < rowCount - 1) {
89
+ moveTo({ rowIndex: activeCell.rowIndex + 1, colIndex: activeCell.colIndex });
90
+ }
91
+ }
92
+ } else if (e.key === "Tab") {
93
+ e.preventDefault();
94
+ onConfirmEdit();
95
+ setMode("navigating");
96
+ // Move right (or wrap to next row)
97
+ if (activeCell) {
98
+ if (activeCell.colIndex < colCount - 1) {
99
+ moveTo({ rowIndex: activeCell.rowIndex, colIndex: activeCell.colIndex + 1 });
100
+ } else if (activeCell.rowIndex < rowCount - 1) {
101
+ moveTo({ rowIndex: activeCell.rowIndex + 1, colIndex: 0 });
102
+ }
103
+ }
104
+ }
105
+ return;
106
+ }
107
+
108
+ // Navigating mode — only handle if we're in the spreadsheet area
109
+ if (!isInSheet && isInInput) return;
110
+
111
+ if (mode === "navigating" && activeCell) {
112
+ switch (e.key) {
113
+ case "ArrowUp":
114
+ e.preventDefault();
115
+ moveTo({ rowIndex: activeCell.rowIndex - 1, colIndex: activeCell.colIndex });
116
+ break;
117
+ case "ArrowDown":
118
+ e.preventDefault();
119
+ moveTo({ rowIndex: activeCell.rowIndex + 1, colIndex: activeCell.colIndex });
120
+ break;
121
+ case "ArrowLeft":
122
+ e.preventDefault();
123
+ moveTo({ rowIndex: activeCell.rowIndex, colIndex: activeCell.colIndex - 1 });
124
+ break;
125
+ case "ArrowRight":
126
+ e.preventDefault();
127
+ moveTo({ rowIndex: activeCell.rowIndex, colIndex: activeCell.colIndex + 1 });
128
+ break;
129
+ case "Enter":
130
+ case "F2":
131
+ e.preventDefault();
132
+ setMode("editing");
133
+ onStartEdit(activeCell);
134
+ break;
135
+ case "Delete":
136
+ case "Backspace":
137
+ e.preventDefault();
138
+ onClearCell(activeCell);
139
+ break;
140
+ case "Tab":
141
+ e.preventDefault();
142
+ if (activeCell.colIndex < colCount - 1) {
143
+ moveTo({ rowIndex: activeCell.rowIndex, colIndex: activeCell.colIndex + 1 });
144
+ } else if (activeCell.rowIndex < rowCount - 1) {
145
+ moveTo({ rowIndex: activeCell.rowIndex + 1, colIndex: 0 });
146
+ }
147
+ break;
148
+ default:
149
+ // Start editing on printable character
150
+ if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
151
+ setMode("editing");
152
+ onStartEdit(activeCell);
153
+ }
154
+ break;
155
+ }
156
+ }
157
+ }
158
+
159
+ document.addEventListener("keydown", handleKeyDown);
160
+ return () => document.removeEventListener("keydown", handleKeyDown);
161
+ }, [activeCell, mode, rowCount, colCount, moveTo, onStartEdit, onConfirmEdit, onCancelEdit, onClearCell, onAddRow]);
162
+
163
+ return {
164
+ activeCell,
165
+ mode,
166
+ setActiveCell,
167
+ setMode,
168
+ handleCellClick,
169
+ handleCellDoubleClick,
170
+ };
171
+ }
@@ -0,0 +1,29 @@
1
+ import type { ColumnDataType } from "@/lib/constants/table-status";
2
+
3
+ /** Icons for column data types (lucide icon names) */
4
+ export const columnTypeIcons: Record<ColumnDataType, string> = {
5
+ text: "Type",
6
+ number: "Hash",
7
+ date: "Calendar",
8
+ boolean: "CheckSquare",
9
+ select: "List",
10
+ url: "Link",
11
+ email: "Mail",
12
+ relation: "GitBranch",
13
+ computed: "Sparkles",
14
+ };
15
+
16
+ /** Format row count for display */
17
+ export function formatRowCount(count: number): string {
18
+ if (count === 0) return "Empty";
19
+ if (count === 1) return "1 row";
20
+ if (count >= 1000) return `${(count / 1000).toFixed(1)}k rows`;
21
+ return `${count} rows`;
22
+ }
23
+
24
+ /** Format column count for display */
25
+ export function formatColumnCount(count: number): string {
26
+ if (count === 0) return "No columns";
27
+ if (count === 1) return "1 column";
28
+ return `${count} columns`;
29
+ }
@@ -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">