stagent 0.1.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 (333) hide show
  1. package/LICENSE +191 -0
  2. package/README.md +399 -0
  3. package/components.json +21 -0
  4. package/dist/cli.js +171 -0
  5. package/drizzle.config.ts +12 -0
  6. package/next.config.mjs +15 -0
  7. package/package.json +114 -0
  8. package/postcss.config.mjs +8 -0
  9. package/public/icon-512.png +0 -0
  10. package/public/icon.svg +13 -0
  11. package/public/readme/home-workspace.png +0 -0
  12. package/public/readme/inbox-approvals.png +0 -0
  13. package/public/readme/workflow-blueprints.png +0 -0
  14. package/public/stagent-s-128.png +0 -0
  15. package/public/stagent-s-64.png +0 -0
  16. package/src/app/api/blueprints/[id]/instantiate/route.ts +27 -0
  17. package/src/app/api/blueprints/[id]/route.ts +39 -0
  18. package/src/app/api/blueprints/import/route.ts +68 -0
  19. package/src/app/api/blueprints/route.ts +29 -0
  20. package/src/app/api/command-palette/recent/route.ts +31 -0
  21. package/src/app/api/data/clear/route.ts +22 -0
  22. package/src/app/api/data/seed/route.ts +22 -0
  23. package/src/app/api/documents/[id]/file/route.ts +44 -0
  24. package/src/app/api/documents/[id]/route.ts +123 -0
  25. package/src/app/api/documents/route.ts +59 -0
  26. package/src/app/api/logs/stream/route.ts +101 -0
  27. package/src/app/api/notifications/[id]/route.ts +36 -0
  28. package/src/app/api/notifications/mark-all-read/route.ts +13 -0
  29. package/src/app/api/notifications/pending-approvals/route.ts +10 -0
  30. package/src/app/api/notifications/pending-approvals/stream/route.ts +101 -0
  31. package/src/app/api/notifications/route.ts +34 -0
  32. package/src/app/api/permissions/route.ts +46 -0
  33. package/src/app/api/profiles/[id]/route.ts +79 -0
  34. package/src/app/api/profiles/[id]/test/route.ts +42 -0
  35. package/src/app/api/profiles/import/route.ts +108 -0
  36. package/src/app/api/profiles/route.ts +50 -0
  37. package/src/app/api/projects/[id]/route.ts +72 -0
  38. package/src/app/api/projects/route.ts +53 -0
  39. package/src/app/api/schedules/[id]/route.ts +185 -0
  40. package/src/app/api/schedules/route.ts +117 -0
  41. package/src/app/api/settings/budgets/route.ts +24 -0
  42. package/src/app/api/settings/openai/route.ts +24 -0
  43. package/src/app/api/settings/route.ts +21 -0
  44. package/src/app/api/settings/test/route.ts +26 -0
  45. package/src/app/api/tasks/[id]/cancel/route.ts +21 -0
  46. package/src/app/api/tasks/[id]/execute/route.ts +90 -0
  47. package/src/app/api/tasks/[id]/logs/route.ts +95 -0
  48. package/src/app/api/tasks/[id]/output/route.ts +47 -0
  49. package/src/app/api/tasks/[id]/respond/route.ts +64 -0
  50. package/src/app/api/tasks/[id]/resume/route.ts +76 -0
  51. package/src/app/api/tasks/[id]/route.ts +77 -0
  52. package/src/app/api/tasks/assist/route.ts +35 -0
  53. package/src/app/api/tasks/route.ts +82 -0
  54. package/src/app/api/uploads/[id]/route.ts +81 -0
  55. package/src/app/api/uploads/cleanup/route.ts +7 -0
  56. package/src/app/api/uploads/route.ts +66 -0
  57. package/src/app/api/workflows/[id]/execute/route.ts +82 -0
  58. package/src/app/api/workflows/[id]/route.ts +133 -0
  59. package/src/app/api/workflows/[id]/status/route.ts +54 -0
  60. package/src/app/api/workflows/[id]/steps/[stepId]/retry/route.ts +22 -0
  61. package/src/app/api/workflows/route.ts +61 -0
  62. package/src/app/apple-icon.tsx +31 -0
  63. package/src/app/costs/page.tsx +256 -0
  64. package/src/app/dashboard/page.tsx +44 -0
  65. package/src/app/documents/[id]/page.tsx +46 -0
  66. package/src/app/documents/page.tsx +45 -0
  67. package/src/app/error.tsx +26 -0
  68. package/src/app/global-error.tsx +23 -0
  69. package/src/app/globals.css +733 -0
  70. package/src/app/icon.tsx +30 -0
  71. package/src/app/inbox/loading.tsx +15 -0
  72. package/src/app/inbox/page.tsx +35 -0
  73. package/src/app/layout.tsx +78 -0
  74. package/src/app/manifest.ts +32 -0
  75. package/src/app/monitor/page.tsx +37 -0
  76. package/src/app/page.tsx +162 -0
  77. package/src/app/profiles/[id]/edit/page.tsx +39 -0
  78. package/src/app/profiles/[id]/page.tsx +33 -0
  79. package/src/app/profiles/new/page.tsx +22 -0
  80. package/src/app/profiles/page.tsx +19 -0
  81. package/src/app/projects/[id]/page.tsx +134 -0
  82. package/src/app/projects/loading.tsx +17 -0
  83. package/src/app/projects/page.tsx +32 -0
  84. package/src/app/schedules/[id]/page.tsx +47 -0
  85. package/src/app/schedules/page.tsx +18 -0
  86. package/src/app/settings/loading.tsx +24 -0
  87. package/src/app/settings/page.tsx +27 -0
  88. package/src/app/tasks/[id]/page.tsx +45 -0
  89. package/src/app/tasks/new/page.tsx +27 -0
  90. package/src/app/workflows/[id]/edit/page.tsx +66 -0
  91. package/src/app/workflows/[id]/page.tsx +37 -0
  92. package/src/app/workflows/blueprints/[id]/page.tsx +40 -0
  93. package/src/app/workflows/blueprints/new/page.tsx +20 -0
  94. package/src/app/workflows/blueprints/page.tsx +11 -0
  95. package/src/app/workflows/new/page.tsx +36 -0
  96. package/src/app/workflows/page.tsx +18 -0
  97. package/src/components/charts/donut-ring.tsx +64 -0
  98. package/src/components/charts/mini-bar.tsx +75 -0
  99. package/src/components/charts/sparkline.tsx +107 -0
  100. package/src/components/costs/cost-dashboard.tsx +877 -0
  101. package/src/components/costs/cost-filters.tsx +179 -0
  102. package/src/components/dashboard/activity-feed.tsx +95 -0
  103. package/src/components/dashboard/greeting.tsx +30 -0
  104. package/src/components/dashboard/priority-queue.tsx +79 -0
  105. package/src/components/dashboard/quick-actions.tsx +62 -0
  106. package/src/components/dashboard/recent-projects.tsx +79 -0
  107. package/src/components/dashboard/stats-cards.tsx +114 -0
  108. package/src/components/documents/document-browser.tsx +235 -0
  109. package/src/components/documents/document-detail-view.tsx +367 -0
  110. package/src/components/documents/document-grid.tsx +78 -0
  111. package/src/components/documents/document-preview.tsx +68 -0
  112. package/src/components/documents/document-table.tsx +119 -0
  113. package/src/components/documents/document-upload-dialog.tsx +153 -0
  114. package/src/components/documents/types.ts +6 -0
  115. package/src/components/documents/utils.ts +57 -0
  116. package/src/components/monitoring/connection-indicator.tsx +14 -0
  117. package/src/components/monitoring/log-entry.tsx +79 -0
  118. package/src/components/monitoring/log-filters.tsx +57 -0
  119. package/src/components/monitoring/log-stream.tsx +144 -0
  120. package/src/components/monitoring/monitor-overview-wrapper.tsx +64 -0
  121. package/src/components/monitoring/monitor-overview.tsx +119 -0
  122. package/src/components/notifications/failure-action.tsx +38 -0
  123. package/src/components/notifications/inbox-list.tsx +165 -0
  124. package/src/components/notifications/message-response.tsx +196 -0
  125. package/src/components/notifications/notification-item.tsx +250 -0
  126. package/src/components/notifications/pending-approval-host.tsx +478 -0
  127. package/src/components/notifications/permission-action.tsx +37 -0
  128. package/src/components/notifications/permission-response-actions.tsx +126 -0
  129. package/src/components/notifications/unread-badge.tsx +35 -0
  130. package/src/components/profiles/profile-browser.tsx +117 -0
  131. package/src/components/profiles/profile-card.tsx +78 -0
  132. package/src/components/profiles/profile-detail-view.tsx +564 -0
  133. package/src/components/profiles/profile-form-view.tsx +480 -0
  134. package/src/components/profiles/profile-import-dialog.tsx +113 -0
  135. package/src/components/projects/project-card.tsx +58 -0
  136. package/src/components/projects/project-create-dialog.tsx +140 -0
  137. package/src/components/projects/project-detail.tsx +68 -0
  138. package/src/components/projects/project-edit-dialog.tsx +219 -0
  139. package/src/components/projects/project-list.tsx +108 -0
  140. package/src/components/schedules/schedule-create-dialog.tsx +403 -0
  141. package/src/components/schedules/schedule-detail-view.tsx +274 -0
  142. package/src/components/schedules/schedule-list.tsx +242 -0
  143. package/src/components/schedules/schedule-status-badge.tsx +16 -0
  144. package/src/components/settings/api-key-form.tsx +141 -0
  145. package/src/components/settings/auth-config-section.tsx +141 -0
  146. package/src/components/settings/auth-method-selector.tsx +67 -0
  147. package/src/components/settings/auth-status-badge.tsx +40 -0
  148. package/src/components/settings/auth-status-dot.tsx +59 -0
  149. package/src/components/settings/budget-guardrails-section.tsx +842 -0
  150. package/src/components/settings/data-management-section.tsx +141 -0
  151. package/src/components/settings/openai-runtime-section.tsx +104 -0
  152. package/src/components/settings/permissions-section.tsx +91 -0
  153. package/src/components/shared/app-sidebar.tsx +123 -0
  154. package/src/components/shared/card-skeleton.tsx +42 -0
  155. package/src/components/shared/command-palette.tsx +250 -0
  156. package/src/components/shared/confirm-dialog.tsx +52 -0
  157. package/src/components/shared/empty-state.tsx +24 -0
  158. package/src/components/shared/error-state.tsx +32 -0
  159. package/src/components/shared/form-section-card.tsx +33 -0
  160. package/src/components/shared/section-heading.tsx +14 -0
  161. package/src/components/shared/stagent-logo.tsx +21 -0
  162. package/src/components/shared/theme-toggle.tsx +46 -0
  163. package/src/components/tasks/ai-assist-panel.tsx +210 -0
  164. package/src/components/tasks/content-preview.tsx +89 -0
  165. package/src/components/tasks/empty-board.tsx +12 -0
  166. package/src/components/tasks/file-upload.tsx +120 -0
  167. package/src/components/tasks/kanban-board.tsx +275 -0
  168. package/src/components/tasks/kanban-column.tsx +75 -0
  169. package/src/components/tasks/skeleton-board.tsx +21 -0
  170. package/src/components/tasks/task-attachments.tsx +114 -0
  171. package/src/components/tasks/task-card.tsx +101 -0
  172. package/src/components/tasks/task-create-panel.tsx +360 -0
  173. package/src/components/tasks/task-detail-view.tsx +356 -0
  174. package/src/components/ui/alert-dialog.tsx +196 -0
  175. package/src/components/ui/badge.tsx +50 -0
  176. package/src/components/ui/button.tsx +71 -0
  177. package/src/components/ui/card.tsx +92 -0
  178. package/src/components/ui/checkbox.tsx +32 -0
  179. package/src/components/ui/command.tsx +184 -0
  180. package/src/components/ui/dialog.tsx +158 -0
  181. package/src/components/ui/dropdown-menu.tsx +257 -0
  182. package/src/components/ui/form.tsx +167 -0
  183. package/src/components/ui/input.tsx +21 -0
  184. package/src/components/ui/label.tsx +24 -0
  185. package/src/components/ui/popover.tsx +89 -0
  186. package/src/components/ui/progress.tsx +31 -0
  187. package/src/components/ui/radio-group.tsx +45 -0
  188. package/src/components/ui/scroll-area.tsx +58 -0
  189. package/src/components/ui/select.tsx +190 -0
  190. package/src/components/ui/separator.tsx +28 -0
  191. package/src/components/ui/sheet.tsx +143 -0
  192. package/src/components/ui/sidebar.tsx +726 -0
  193. package/src/components/ui/skeleton.tsx +13 -0
  194. package/src/components/ui/slider.tsx +63 -0
  195. package/src/components/ui/sonner.tsx +36 -0
  196. package/src/components/ui/switch.tsx +35 -0
  197. package/src/components/ui/table.tsx +116 -0
  198. package/src/components/ui/tabs.tsx +91 -0
  199. package/src/components/ui/textarea.tsx +18 -0
  200. package/src/components/ui/tooltip.tsx +57 -0
  201. package/src/components/workflows/blueprint-editor.tsx +109 -0
  202. package/src/components/workflows/blueprint-gallery.tsx +155 -0
  203. package/src/components/workflows/blueprint-preview.tsx +240 -0
  204. package/src/components/workflows/loop-status-view.tsx +272 -0
  205. package/src/components/workflows/swarm-dashboard.tsx +185 -0
  206. package/src/components/workflows/workflow-form-view.tsx +1376 -0
  207. package/src/components/workflows/workflow-list.tsx +230 -0
  208. package/src/components/workflows/workflow-status-view.tsx +477 -0
  209. package/src/hooks/use-mobile.ts +19 -0
  210. package/src/instrumentation.ts +7 -0
  211. package/src/lib/agents/claude-agent.ts +737 -0
  212. package/src/lib/agents/execution-manager.ts +27 -0
  213. package/src/lib/agents/profiles/assignment-validation.ts +75 -0
  214. package/src/lib/agents/profiles/builtins/code-reviewer/SKILL.md +21 -0
  215. package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +28 -0
  216. package/src/lib/agents/profiles/builtins/data-analyst/SKILL.md +25 -0
  217. package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +27 -0
  218. package/src/lib/agents/profiles/builtins/devops-engineer/SKILL.md +34 -0
  219. package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +27 -0
  220. package/src/lib/agents/profiles/builtins/document-writer/SKILL.md +16 -0
  221. package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +27 -0
  222. package/src/lib/agents/profiles/builtins/general/SKILL.md +13 -0
  223. package/src/lib/agents/profiles/builtins/general/profile.yaml +18 -0
  224. package/src/lib/agents/profiles/builtins/health-fitness-coach/SKILL.md +34 -0
  225. package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +26 -0
  226. package/src/lib/agents/profiles/builtins/learning-coach/SKILL.md +35 -0
  227. package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +26 -0
  228. package/src/lib/agents/profiles/builtins/project-manager/SKILL.md +26 -0
  229. package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +26 -0
  230. package/src/lib/agents/profiles/builtins/researcher/SKILL.md +15 -0
  231. package/src/lib/agents/profiles/builtins/researcher/profile.yaml +27 -0
  232. package/src/lib/agents/profiles/builtins/shopping-assistant/SKILL.md +34 -0
  233. package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +26 -0
  234. package/src/lib/agents/profiles/builtins/technical-writer/SKILL.md +31 -0
  235. package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +29 -0
  236. package/src/lib/agents/profiles/builtins/travel-planner/SKILL.md +23 -0
  237. package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +26 -0
  238. package/src/lib/agents/profiles/builtins/wealth-manager/SKILL.md +24 -0
  239. package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +26 -0
  240. package/src/lib/agents/profiles/compatibility.ts +109 -0
  241. package/src/lib/agents/profiles/registry.ts +293 -0
  242. package/src/lib/agents/profiles/test-runner.ts +18 -0
  243. package/src/lib/agents/profiles/test-types.ts +20 -0
  244. package/src/lib/agents/profiles/types.ts +43 -0
  245. package/src/lib/agents/router.ts +56 -0
  246. package/src/lib/agents/runtime/catalog.ts +85 -0
  247. package/src/lib/agents/runtime/claude-sdk.ts +12 -0
  248. package/src/lib/agents/runtime/claude.ts +370 -0
  249. package/src/lib/agents/runtime/codex-app-server-client.ts +289 -0
  250. package/src/lib/agents/runtime/index.ts +167 -0
  251. package/src/lib/agents/runtime/openai-codex.ts +1089 -0
  252. package/src/lib/agents/runtime/task-assist-types.ts +8 -0
  253. package/src/lib/agents/runtime/types.ts +30 -0
  254. package/src/lib/constants/settings.ts +13 -0
  255. package/src/lib/constants/status-colors.ts +44 -0
  256. package/src/lib/constants/task-status.ts +49 -0
  257. package/src/lib/data/clear.ts +63 -0
  258. package/src/lib/data/seed-data/documents.ts +715 -0
  259. package/src/lib/data/seed-data/logs.ts +195 -0
  260. package/src/lib/data/seed-data/notifications.ts +141 -0
  261. package/src/lib/data/seed-data/profiles.ts +175 -0
  262. package/src/lib/data/seed-data/projects.ts +61 -0
  263. package/src/lib/data/seed-data/schedules.ts +108 -0
  264. package/src/lib/data/seed-data/tasks.ts +341 -0
  265. package/src/lib/data/seed-data/usage-ledger.ts +130 -0
  266. package/src/lib/data/seed-data/workflows.ts +213 -0
  267. package/src/lib/data/seed.ts +129 -0
  268. package/src/lib/db/index.ts +221 -0
  269. package/src/lib/db/migrations/0000_aromatic_gargoyle.sql +59 -0
  270. package/src/lib/db/migrations/0001_first_iron_patriot.sql +6 -0
  271. package/src/lib/db/migrations/0002_add_resume_count.sql +1 -0
  272. package/src/lib/db/migrations/0003_add_settings.sql +5 -0
  273. package/src/lib/db/migrations/0004_add_documents.sql +20 -0
  274. package/src/lib/db/migrations/0005_add_document_preprocessing.sql +4 -0
  275. package/src/lib/db/migrations/0006_add_agent_profile.sql +2 -0
  276. package/src/lib/db/migrations/0007_add_usage_metering_ledger.sql +30 -0
  277. package/src/lib/db/migrations/0008_add_document_version.sql +1 -0
  278. package/src/lib/db/migrations/meta/0000_snapshot.json +416 -0
  279. package/src/lib/db/migrations/meta/0001_snapshot.json +461 -0
  280. package/src/lib/db/migrations/meta/0002_snapshot.json +469 -0
  281. package/src/lib/db/migrations/meta/_journal.json +27 -0
  282. package/src/lib/db/schema.ts +227 -0
  283. package/src/lib/documents/cleanup.ts +50 -0
  284. package/src/lib/documents/context-builder.ts +75 -0
  285. package/src/lib/documents/output-scanner.ts +166 -0
  286. package/src/lib/documents/processor.ts +120 -0
  287. package/src/lib/documents/processors/image.ts +21 -0
  288. package/src/lib/documents/processors/office.ts +36 -0
  289. package/src/lib/documents/processors/pdf.ts +12 -0
  290. package/src/lib/documents/processors/spreadsheet.ts +18 -0
  291. package/src/lib/documents/processors/text.ts +8 -0
  292. package/src/lib/documents/registry.ts +25 -0
  293. package/src/lib/notifications/actionable.ts +108 -0
  294. package/src/lib/notifications/permissions.ts +169 -0
  295. package/src/lib/queries/chart-data.ts +184 -0
  296. package/src/lib/schedules/interval-parser.ts +110 -0
  297. package/src/lib/schedules/scheduler.ts +220 -0
  298. package/src/lib/settings/auth.ts +98 -0
  299. package/src/lib/settings/budget-guardrails.ts +590 -0
  300. package/src/lib/settings/helpers.ts +23 -0
  301. package/src/lib/settings/openai-auth.ts +80 -0
  302. package/src/lib/settings/permissions.ts +102 -0
  303. package/src/lib/usage/ledger.ts +489 -0
  304. package/src/lib/usage/pricing.ts +68 -0
  305. package/src/lib/utils/crypto.ts +90 -0
  306. package/src/lib/utils/format-timestamp.ts +46 -0
  307. package/src/lib/utils/session-cleanup.ts +26 -0
  308. package/src/lib/utils/stagent-paths.ts +18 -0
  309. package/src/lib/utils.ts +6 -0
  310. package/src/lib/validators/blueprint.ts +43 -0
  311. package/src/lib/validators/profile.ts +64 -0
  312. package/src/lib/validators/project.ts +17 -0
  313. package/src/lib/validators/settings.ts +57 -0
  314. package/src/lib/validators/task.ts +30 -0
  315. package/src/lib/workflows/blueprints/builtins/code-review-pipeline.yaml +72 -0
  316. package/src/lib/workflows/blueprints/builtins/documentation-generation.yaml +62 -0
  317. package/src/lib/workflows/blueprints/builtins/investment-research.yaml +81 -0
  318. package/src/lib/workflows/blueprints/builtins/meal-planning.yaml +73 -0
  319. package/src/lib/workflows/blueprints/builtins/product-research.yaml +72 -0
  320. package/src/lib/workflows/blueprints/builtins/research-report.yaml +77 -0
  321. package/src/lib/workflows/blueprints/builtins/sprint-planning.yaml +77 -0
  322. package/src/lib/workflows/blueprints/builtins/travel-planning.yaml +80 -0
  323. package/src/lib/workflows/blueprints/instantiator.ts +131 -0
  324. package/src/lib/workflows/blueprints/registry.ts +128 -0
  325. package/src/lib/workflows/blueprints/template.ts +58 -0
  326. package/src/lib/workflows/blueprints/types.ts +38 -0
  327. package/src/lib/workflows/definition-validation.ts +121 -0
  328. package/src/lib/workflows/engine.ts +1113 -0
  329. package/src/lib/workflows/loop-executor.ts +270 -0
  330. package/src/lib/workflows/parallel.ts +55 -0
  331. package/src/lib/workflows/swarm.ts +97 -0
  332. package/src/lib/workflows/types.ts +112 -0
  333. package/tsconfig.json +41 -0
@@ -0,0 +1,89 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import ReactMarkdown from "react-markdown";
5
+ import remarkGfm from "remark-gfm";
6
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
7
+ import { Button } from "@/components/ui/button";
8
+ import { Copy, Download, Maximize2, Minimize2 } from "lucide-react";
9
+ import { toast } from "sonner";
10
+
11
+ interface ContentPreviewProps {
12
+ content: string;
13
+ contentType: "text" | "markdown" | "code" | "json" | "unknown";
14
+ }
15
+
16
+ export function ContentPreview({ content, contentType }: ContentPreviewProps) {
17
+ const [expanded, setExpanded] = useState(false);
18
+
19
+ if (!content) return null;
20
+
21
+ function copyToClipboard() {
22
+ navigator.clipboard.writeText(content);
23
+ toast.success("Copied to clipboard");
24
+ }
25
+
26
+ function downloadAsFile() {
27
+ const ext = contentType === "json" ? "json" : contentType === "code" ? "txt" : contentType === "markdown" ? "md" : "txt";
28
+ const blob = new Blob([content], { type: "text/plain" });
29
+ const url = URL.createObjectURL(blob);
30
+ const a = document.createElement("a");
31
+ a.href = url;
32
+ a.download = `output.${ext}`;
33
+ a.click();
34
+ URL.revokeObjectURL(url);
35
+ }
36
+
37
+ // M4: Safe JSON formatting
38
+ function formatJson(raw: string): string {
39
+ try {
40
+ return JSON.stringify(JSON.parse(raw), null, 2);
41
+ } catch {
42
+ return raw;
43
+ }
44
+ }
45
+
46
+ const heightClass = expanded ? "" : "max-h-80";
47
+
48
+ return (
49
+ <Card>
50
+ <CardHeader className="pb-2">
51
+ <div className="flex items-center justify-between">
52
+ <CardTitle className="text-sm font-medium text-muted-foreground">
53
+ Output ({contentType})
54
+ </CardTitle>
55
+ <div className="flex gap-1">
56
+ <Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => setExpanded(!expanded)} aria-label={expanded ? "Collapse output" : "Expand output"}>
57
+ {expanded ? <Minimize2 className="h-3 w-3" /> : <Maximize2 className="h-3 w-3" />}
58
+ </Button>
59
+ <Button variant="ghost" size="icon" className="h-7 w-7" onClick={copyToClipboard} aria-label="Copy to clipboard">
60
+ <Copy className="h-3 w-3" />
61
+ </Button>
62
+ <Button variant="ghost" size="icon" className="h-7 w-7" onClick={downloadAsFile} aria-label="Download as file">
63
+ <Download className="h-3 w-3" />
64
+ </Button>
65
+ </div>
66
+ </div>
67
+ </CardHeader>
68
+ <CardContent>
69
+ {contentType === "json" ? (
70
+ <pre className={`text-xs font-mono bg-muted p-3 rounded overflow-auto ${heightClass}`}>
71
+ {formatJson(content)}
72
+ </pre>
73
+ ) : contentType === "code" ? (
74
+ <pre className={`text-xs font-mono bg-muted p-3 rounded overflow-auto ${heightClass}`}>
75
+ {content}
76
+ </pre>
77
+ ) : contentType === "markdown" ? (
78
+ <div className={`prose prose-sm dark:prose-invert max-w-none overflow-auto ${heightClass}`}>
79
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
80
+ </div>
81
+ ) : (
82
+ <p className={`text-sm whitespace-pre-wrap overflow-auto ${heightClass}`}>
83
+ {content}
84
+ </p>
85
+ )}
86
+ </CardContent>
87
+ </Card>
88
+ );
89
+ }
@@ -0,0 +1,12 @@
1
+ import { ClipboardList } from "lucide-react";
2
+ import { EmptyState } from "@/components/shared/empty-state";
3
+
4
+ export function EmptyBoard() {
5
+ return (
6
+ <EmptyState
7
+ icon={ClipboardList}
8
+ heading="No tasks yet"
9
+ description="Create your first task to get started with the kanban board."
10
+ />
11
+ );
12
+ }
@@ -0,0 +1,120 @@
1
+ "use client";
2
+
3
+ import { useState, useRef } from "react";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Upload, X, FileText, Image, FileCode } from "lucide-react";
6
+
7
+ interface UploadedFile {
8
+ id: string;
9
+ filename: string;
10
+ originalName: string;
11
+ size: number;
12
+ type: string;
13
+ }
14
+
15
+ interface FileUploadProps {
16
+ onUploaded: (file: UploadedFile) => void;
17
+ uploads: UploadedFile[];
18
+ onRemove: (id: string) => void;
19
+ }
20
+
21
+ function formatSize(bytes: number): string {
22
+ if (bytes < 1024) return `${bytes} B`;
23
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
24
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
25
+ }
26
+
27
+ function getFileIcon(type: string) {
28
+ if (type.startsWith("image/")) return Image;
29
+ if (type.includes("pdf") || type.includes("document") || type.includes("text")) return FileText;
30
+ if (type.includes("javascript") || type.includes("typescript") || type.includes("json") || type.includes("xml")) return FileCode;
31
+ return FileText;
32
+ }
33
+
34
+ export function FileUpload({ onUploaded, uploads, onRemove }: FileUploadProps) {
35
+ const [uploading, setUploading] = useState(false);
36
+ const inputRef = useRef<HTMLInputElement>(null);
37
+
38
+ async function handleFile(file: File) {
39
+ setUploading(true);
40
+ try {
41
+ const formData = new FormData();
42
+ formData.append("file", file);
43
+
44
+ const res = await fetch("/api/uploads", {
45
+ method: "POST",
46
+ body: formData,
47
+ });
48
+
49
+ if (res.ok) {
50
+ const data = await res.json();
51
+ onUploaded(data);
52
+ }
53
+ } finally {
54
+ setUploading(false);
55
+ }
56
+ }
57
+
58
+ function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
59
+ const file = e.target.files?.[0];
60
+ if (file) handleFile(file);
61
+ e.target.value = "";
62
+ }
63
+
64
+ function handleDrop(e: React.DragEvent) {
65
+ e.preventDefault();
66
+ const file = e.dataTransfer.files?.[0];
67
+ if (file) handleFile(file);
68
+ }
69
+
70
+ return (
71
+ <div className="space-y-2">
72
+ <div
73
+ role="button"
74
+ tabIndex={0}
75
+ aria-label="Upload a file — click or drag and drop"
76
+ className="border-2 border-dashed rounded-lg p-4 text-center cursor-pointer hover:bg-accent/50 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
77
+ onClick={() => inputRef.current?.click()}
78
+ onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); inputRef.current?.click(); } }}
79
+ onDrop={handleDrop}
80
+ onDragOver={(e) => e.preventDefault()}
81
+ >
82
+ <Upload className="h-5 w-5 mx-auto text-muted-foreground mb-1" />
83
+ <p className="text-xs text-muted-foreground">
84
+ {uploading ? "Uploading..." : "Click or drop a file"}
85
+ </p>
86
+ <p className="text-xs text-muted-foreground mt-0.5">Max 50MB per file</p>
87
+ <input
88
+ ref={inputRef}
89
+ type="file"
90
+ className="hidden"
91
+ onChange={handleChange}
92
+ />
93
+ </div>
94
+ {uploads.length > 0 && (
95
+ <div className="space-y-1">
96
+ {uploads.map((f) => {
97
+ const Icon = getFileIcon(f.type);
98
+ return (
99
+ <div key={f.id} className="flex items-center gap-2 text-sm">
100
+ <Icon className="h-3 w-3 text-muted-foreground" />
101
+ <span className="flex-1 truncate">{f.originalName}</span>
102
+ <span className="text-xs text-muted-foreground">{formatSize(f.size)}</span>
103
+ <Button
104
+ type="button"
105
+ variant="ghost"
106
+ size="icon"
107
+ className="h-5 w-5"
108
+ onClick={() => onRemove(f.id)}
109
+ aria-label={`Remove ${f.originalName}`}
110
+ >
111
+ <X className="h-3 w-3" />
112
+ </Button>
113
+ </div>
114
+ );
115
+ })}
116
+ </div>
117
+ )}
118
+ </div>
119
+ );
120
+ }
@@ -0,0 +1,275 @@
1
+ "use client";
2
+
3
+ import { useId, useState, useCallback, useRef, useEffect } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import Link from "next/link";
6
+ import {
7
+ DndContext,
8
+ DragEndEvent,
9
+ DragOverlay,
10
+ DragStartEvent,
11
+ KeyboardSensor,
12
+ PointerSensor,
13
+ useSensor,
14
+ useSensors,
15
+ } from "@dnd-kit/core";
16
+ import { sortableKeyboardCoordinates } from "@dnd-kit/sortable";
17
+ import {
18
+ Select,
19
+ SelectContent,
20
+ SelectItem,
21
+ SelectTrigger,
22
+ SelectValue,
23
+ } from "@/components/ui/select";
24
+ import { Button } from "@/components/ui/button";
25
+ import { ChevronLeft, ChevronRight, Plus } from "lucide-react";
26
+ import { KanbanColumn } from "./kanban-column";
27
+ import { TaskCard, type TaskItem } from "./task-card";
28
+ import { EmptyBoard } from "./empty-board";
29
+ import { COLUMN_ORDER, isValidDragTransition, type TaskStatus } from "@/lib/constants/task-status";
30
+
31
+ interface KanbanBoardProps {
32
+ initialTasks: TaskItem[];
33
+ projects: { id: string; name: string }[];
34
+ }
35
+
36
+ export function KanbanBoard({ initialTasks, projects }: KanbanBoardProps) {
37
+ const dndId = useId();
38
+ const router = useRouter();
39
+ const [tasks, setTasks] = useState<TaskItem[]>(initialTasks);
40
+ const [activeTask, setActiveTask] = useState<TaskItem | null>(null);
41
+ const [projectFilter, setProjectFilter] = useState("all");
42
+ const [statusFilter, setStatusFilter] = useState("all");
43
+ const [announcement, setAnnouncement] = useState(
44
+ `Showing ${initialTasks.length} task${initialTasks.length === 1 ? "" : "s"} on the kanban board.`
45
+ );
46
+ const hasAnnouncedFilters = useRef(false);
47
+
48
+ // I5: Scroll indicators
49
+ const scrollRef = useRef<HTMLDivElement>(null);
50
+ const [canScrollLeft, setCanScrollLeft] = useState(false);
51
+ const [canScrollRight, setCanScrollRight] = useState(false);
52
+
53
+ const updateScrollIndicators = useCallback(() => {
54
+ const el = scrollRef.current;
55
+ if (!el) return;
56
+ setCanScrollLeft(el.scrollLeft > 0);
57
+ setCanScrollRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 1);
58
+ }, []);
59
+
60
+ useEffect(() => {
61
+ updateScrollIndicators();
62
+ const el = scrollRef.current;
63
+ if (!el) return;
64
+ const observer = new ResizeObserver(updateScrollIndicators);
65
+ observer.observe(el);
66
+ return () => observer.disconnect();
67
+ }, [updateScrollIndicators, tasks]);
68
+
69
+ // Filter tasks by project and status
70
+ const filteredTasks = tasks.filter((t) => {
71
+ if (projectFilter !== "all" && t.projectId !== projectFilter) return false;
72
+ if (statusFilter !== "all" && t.status !== statusFilter) return false;
73
+ return true;
74
+ });
75
+
76
+ useEffect(() => {
77
+ if (!hasAnnouncedFilters.current) {
78
+ hasAnnouncedFilters.current = true;
79
+ return;
80
+ }
81
+
82
+ const projectName =
83
+ projectFilter === "all"
84
+ ? "all projects"
85
+ : projects.find((project) => project.id === projectFilter)?.name ?? "the selected project";
86
+ const statusName = statusFilter === "all" ? "all statuses" : statusFilter;
87
+ setAnnouncement(
88
+ `Showing ${filteredTasks.length} task${filteredTasks.length === 1 ? "" : "s"} for ${projectName} with ${statusName}.`
89
+ );
90
+ }, [filteredTasks.length, projectFilter, projects, statusFilter]);
91
+
92
+ const sensors = useSensors(
93
+ useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
94
+ useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
95
+ );
96
+
97
+ const refresh = useCallback(async () => {
98
+ const res = await fetch("/api/tasks");
99
+ if (res.ok) setTasks(await res.json());
100
+ }, []);
101
+
102
+ function handleDragStart(event: DragStartEvent) {
103
+ const task = tasks.find((t) => t.id === event.active.id);
104
+ setActiveTask(task ?? null);
105
+ }
106
+
107
+ async function handleDragEnd(event: DragEndEvent) {
108
+ setActiveTask(null);
109
+ const { active, over } = event;
110
+ if (!over) return;
111
+
112
+ const task = tasks.find((t) => t.id === active.id);
113
+ if (!task) return;
114
+
115
+ const targetStatus = over.id as TaskStatus;
116
+ if (task.status === targetStatus) return;
117
+
118
+ if (!isValidDragTransition(task.status as TaskStatus, targetStatus)) {
119
+ setAnnouncement(`Cannot move ${task.title} from ${task.status} to ${targetStatus}.`);
120
+ return;
121
+ }
122
+
123
+ // Optimistic update
124
+ const prevTasks = [...tasks];
125
+ setTasks((prev) =>
126
+ prev.map((t) => (t.id === task.id ? { ...t, status: targetStatus } : t))
127
+ );
128
+ setAnnouncement(`Moved ${task.title} to ${targetStatus}.`);
129
+
130
+ try {
131
+ const res = await fetch(`/api/tasks/${task.id}`, {
132
+ method: "PATCH",
133
+ headers: { "Content-Type": "application/json" },
134
+ body: JSON.stringify({ status: targetStatus }),
135
+ });
136
+ if (!res.ok) {
137
+ setTasks(prevTasks);
138
+ setAnnouncement(`Move failed. ${task.title} returned to ${task.status}.`);
139
+ }
140
+ } catch {
141
+ setTasks(prevTasks);
142
+ setAnnouncement(`Move failed. ${task.title} returned to ${task.status}.`);
143
+ }
144
+ }
145
+
146
+ function handleTaskClick(task: TaskItem) {
147
+ router.push(`/tasks/${task.id}`);
148
+ }
149
+
150
+ const groupedTasks = COLUMN_ORDER.reduce(
151
+ (acc, status) => {
152
+ acc[status] = filteredTasks.filter((t) => t.status === status);
153
+ return acc;
154
+ },
155
+ {} as Record<TaskStatus, TaskItem[]>
156
+ );
157
+
158
+ const filterBar = (
159
+ <div className="flex items-center gap-2">
160
+ {projects.length > 0 && (
161
+ <Select value={projectFilter} onValueChange={setProjectFilter}>
162
+ <SelectTrigger className="w-[180px]">
163
+ <SelectValue placeholder="All projects" />
164
+ </SelectTrigger>
165
+ <SelectContent>
166
+ <SelectItem value="all">All projects</SelectItem>
167
+ {projects.map((p) => (
168
+ <SelectItem key={p.id} value={p.id}>{p.name}</SelectItem>
169
+ ))}
170
+ </SelectContent>
171
+ </Select>
172
+ )}
173
+ <Select value={statusFilter} onValueChange={setStatusFilter}>
174
+ <SelectTrigger className="w-[150px]">
175
+ <SelectValue placeholder="All statuses" />
176
+ </SelectTrigger>
177
+ <SelectContent>
178
+ <SelectItem value="all">All statuses</SelectItem>
179
+ {COLUMN_ORDER.map((s) => (
180
+ <SelectItem key={s} value={s} className="capitalize">{s}</SelectItem>
181
+ ))}
182
+ </SelectContent>
183
+ </Select>
184
+ </div>
185
+ );
186
+
187
+ const newTaskButton = (
188
+ <Link href="/tasks/new">
189
+ <Button>
190
+ <Plus className="h-4 w-4 mr-2" />
191
+ New Task
192
+ </Button>
193
+ </Link>
194
+ );
195
+
196
+ if (tasks.length === 0) {
197
+ return (
198
+ <div>
199
+ <div className="flex items-center justify-between mb-6 gap-4 flex-wrap">
200
+ <h1 className="text-2xl font-bold">Dashboard</h1>
201
+ <div className="flex items-center gap-3">
202
+ {filterBar}
203
+ {newTaskButton}
204
+ </div>
205
+ </div>
206
+ <EmptyBoard />
207
+ </div>
208
+ );
209
+ }
210
+
211
+ return (
212
+ <div>
213
+ <div className="flex items-center justify-between mb-6 gap-4 flex-wrap">
214
+ <h1 className="text-2xl font-bold">Dashboard</h1>
215
+ <div className="flex items-center gap-3">
216
+ {filterBar}
217
+ {newTaskButton}
218
+ </div>
219
+ </div>
220
+ <p id={`${dndId}-announcements`} className="sr-only" aria-live="polite" aria-atomic="true">
221
+ {announcement}
222
+ </p>
223
+ <DndContext
224
+ id={dndId}
225
+ sensors={sensors}
226
+ onDragStart={handleDragStart}
227
+ onDragEnd={handleDragEnd}
228
+ >
229
+ <div className="relative">
230
+ <button
231
+ type="button"
232
+ aria-label="Scroll left"
233
+ onClick={() => scrollRef.current?.scrollBy({ left: -280, behavior: "smooth" })}
234
+ className={`surface-control absolute left-1 top-0 z-20 h-8 w-8 rounded-full flex items-center justify-center transition-opacity duration-200 cursor-pointer hover:bg-accent/50 ${canScrollLeft ? "opacity-100" : "opacity-0 pointer-events-none"}`}
235
+ >
236
+ <ChevronLeft className="h-4 w-4 text-foreground" />
237
+ </button>
238
+ <button
239
+ type="button"
240
+ aria-label="Scroll right"
241
+ onClick={() => scrollRef.current?.scrollBy({ left: 280, behavior: "smooth" })}
242
+ className={`surface-control absolute right-1 top-0 z-20 h-8 w-8 rounded-full flex items-center justify-center transition-opacity duration-200 cursor-pointer hover:bg-accent/50 ${canScrollRight ? "opacity-100" : "opacity-0 pointer-events-none"}`}
243
+ >
244
+ <ChevronRight className="h-4 w-4 text-foreground" />
245
+ </button>
246
+ <div
247
+ ref={scrollRef}
248
+ onScroll={updateScrollIndicators}
249
+ className="flex gap-4 overflow-x-auto pb-4"
250
+ role="region"
251
+ aria-label="Kanban board"
252
+ aria-describedby={`${dndId}-announcements`}
253
+ >
254
+ {COLUMN_ORDER.map((status) => (
255
+ <KanbanColumn
256
+ key={status}
257
+ status={status}
258
+ tasks={groupedTasks[status]}
259
+ onTaskClick={handleTaskClick}
260
+ onAddTask={status === "planned" ? () => router.push("/tasks/new") : undefined}
261
+ />
262
+ ))}
263
+ </div>
264
+ </div>
265
+ <DragOverlay>
266
+ {activeTask ? (
267
+ <div className="w-64">
268
+ <TaskCard task={activeTask} onClick={() => {}} />
269
+ </div>
270
+ ) : null}
271
+ </DragOverlay>
272
+ </DndContext>
273
+ </div>
274
+ );
275
+ }
@@ -0,0 +1,75 @@
1
+ "use client";
2
+
3
+ import { useDroppable } from "@dnd-kit/core";
4
+ import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
5
+ import { Badge } from "@/components/ui/badge";
6
+ import { Button } from "@/components/ui/button";
7
+ import { Inbox, Plus } from "lucide-react";
8
+ import { TaskCard, type TaskItem } from "./task-card";
9
+ import type { TaskStatus } from "@/lib/constants/task-status";
10
+
11
+ const columnLabels: Record<string, string> = {
12
+ planned: "Planned",
13
+ queued: "Queued",
14
+ running: "Running",
15
+ completed: "Completed",
16
+ failed: "Failed",
17
+ };
18
+
19
+ export function KanbanColumn({
20
+ status,
21
+ tasks,
22
+ onTaskClick,
23
+ onAddTask,
24
+ }: {
25
+ status: TaskStatus;
26
+ tasks: TaskItem[];
27
+ onTaskClick: (task: TaskItem) => void;
28
+ onAddTask?: () => void;
29
+ }) {
30
+ const { setNodeRef, isOver } = useDroppable({ id: status });
31
+ const label = columnLabels[status] ?? status;
32
+
33
+ return (
34
+ <div className="flex flex-col min-w-60 max-w-72 flex-1 shrink-0" role="group" aria-label={`${label} column, ${tasks.length} tasks`}>
35
+ <div className="flex items-center gap-2 mb-3 px-1">
36
+ <h3 className="text-sm font-medium">{label}</h3>
37
+ <Badge variant="secondary" className="text-xs">
38
+ {tasks.length}
39
+ </Badge>
40
+ </div>
41
+ <div
42
+ ref={setNodeRef}
43
+ aria-label={`Drop zone for ${label}`}
44
+ className={`surface-scroll flex-1 rounded-lg border border-dashed p-2 min-h-[200px] transition-colors ${
45
+ isOver ? "bg-accent/50 border-primary" : "border-border/40"
46
+ }`}
47
+ >
48
+ <SortableContext items={tasks.map((t) => t.id)} strategy={verticalListSortingStrategy}>
49
+ <div className="space-y-2">
50
+ {tasks.length === 0 ? (
51
+ <div className="flex flex-col items-center justify-center h-full min-h-[120px] text-muted-foreground border-2 border-dashed border-border/50 rounded-lg bg-background/35">
52
+ <Inbox className="h-5 w-5 mb-1 opacity-40" />
53
+ <span className="text-xs">No tasks</span>
54
+ </div>
55
+ ) : (
56
+ tasks.map((task) => (
57
+ <TaskCard key={task.id} task={task} onClick={onTaskClick} />
58
+ ))
59
+ )}
60
+ </div>
61
+ </SortableContext>
62
+ {onAddTask && (
63
+ <Button
64
+ variant="ghost"
65
+ className="w-full mt-2 border border-dashed border-border/50 text-muted-foreground hover:text-foreground"
66
+ onClick={onAddTask}
67
+ >
68
+ <Plus className="h-4 w-4 mr-1" />
69
+ Add task
70
+ </Button>
71
+ )}
72
+ </div>
73
+ </div>
74
+ );
75
+ }
@@ -0,0 +1,21 @@
1
+ import { Skeleton } from "@/components/ui/skeleton";
2
+
3
+ export function SkeletonBoard() {
4
+ return (
5
+ <div className="flex gap-4 overflow-x-auto pb-4">
6
+ {Array.from({ length: 5 }).map((_, colIdx) => (
7
+ <div key={colIdx} className="w-64 shrink-0">
8
+ <div className="flex items-center gap-2 mb-3 px-1">
9
+ <Skeleton className="h-4 w-20" />
10
+ <Skeleton className="h-5 w-6 rounded-full" />
11
+ </div>
12
+ <div className="space-y-2 p-2">
13
+ {Array.from({ length: colIdx < 3 ? 3 : 2 }).map((_, cardIdx) => (
14
+ <Skeleton key={cardIdx} className="h-20 w-full rounded-lg" />
15
+ ))}
16
+ </div>
17
+ </div>
18
+ ))}
19
+ </div>
20
+ );
21
+ }