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,30 @@
1
+ import { ImageResponse } from "next/og";
2
+ import { readFileSync } from "fs";
3
+ import { join } from "path";
4
+
5
+ export const size = { width: 32, height: 32 };
6
+ export const contentType = "image/png";
7
+
8
+ export default function Icon() {
9
+ const logoData = readFileSync(join(process.cwd(), "public/stagent-s-64.png"));
10
+ const logoSrc = `data:image/png;base64,${logoData.toString("base64")}`;
11
+
12
+ return new ImageResponse(
13
+ (
14
+ <div
15
+ style={{
16
+ width: "100%",
17
+ height: "100%",
18
+ display: "flex",
19
+ alignItems: "center",
20
+ justifyContent: "center",
21
+ background: "transparent",
22
+ }}
23
+ >
24
+ {/* eslint-disable-next-line @next/next/no-img-element */}
25
+ <img src={logoSrc} width="30" height="30" alt="" />
26
+ </div>
27
+ ),
28
+ { ...size }
29
+ );
30
+ }
@@ -0,0 +1,15 @@
1
+ import { Skeleton } from "@/components/ui/skeleton";
2
+
3
+ export default function InboxLoading() {
4
+ return (
5
+ <div className="p-6">
6
+ <Skeleton className="h-8 w-24 mb-6" />
7
+ <Skeleton className="h-10 w-80 mb-4 rounded-md" />
8
+ <div className="space-y-3">
9
+ {Array.from({ length: 5 }).map((_, i) => (
10
+ <Skeleton key={i} className="h-24 rounded-lg" />
11
+ ))}
12
+ </div>
13
+ </div>
14
+ );
15
+ }
@@ -0,0 +1,35 @@
1
+ import { db } from "@/lib/db";
2
+ import { notifications } from "@/lib/db/schema";
3
+ import { desc } from "drizzle-orm";
4
+ import { InboxList } from "@/components/notifications/inbox-list";
5
+
6
+ export const dynamic = "force-dynamic";
7
+
8
+ export default async function InboxPage() {
9
+ const rows = await db
10
+ .select()
11
+ .from(notifications)
12
+ .orderBy(desc(notifications.createdAt))
13
+ .limit(100);
14
+
15
+ // Serialize Date objects for client component consumption
16
+ const initialNotifications = rows.map((n) => ({
17
+ ...n,
18
+ createdAt: n.createdAt.toISOString(),
19
+ respondedAt: n.respondedAt?.toISOString() ?? null,
20
+ }));
21
+
22
+ return (
23
+ <div className="gradient-sunset-glow min-h-screen p-4 sm:p-6">
24
+ <div className="surface-page surface-page-shell mx-auto min-h-[calc(100dvh-2rem)] max-w-6xl rounded-[30px] p-5 sm:p-6 lg:p-7">
25
+ <div className="mb-5 space-y-1">
26
+ <h1 className="text-2xl font-bold">Inbox</h1>
27
+ <p className="text-sm text-muted-foreground">
28
+ Review approvals, questions, failures, and completions without leaving the supervision flow.
29
+ </p>
30
+ </div>
31
+ <InboxList initialNotifications={initialNotifications} />
32
+ </div>
33
+ </div>
34
+ );
35
+ }
@@ -0,0 +1,78 @@
1
+ import type { Metadata } from "next";
2
+ import { Geist, Geist_Mono } from "next/font/google";
3
+ import { SidebarProvider, SidebarInset } from "@/components/ui/sidebar";
4
+ import { TooltipProvider } from "@/components/ui/tooltip";
5
+ import { AppSidebar } from "@/components/shared/app-sidebar";
6
+ import { CommandPalette } from "@/components/shared/command-palette";
7
+ import { PendingApprovalHost } from "@/components/notifications/pending-approval-host";
8
+ import { Toaster } from "@/components/ui/sonner";
9
+ import "./globals.css";
10
+
11
+ const geistSans = Geist({
12
+ variable: "--font-geist-sans",
13
+ subsets: ["latin"],
14
+ });
15
+
16
+ const geistMono = Geist_Mono({
17
+ variable: "--font-geist-mono",
18
+ subsets: ["latin"],
19
+ });
20
+
21
+ export const metadata: Metadata = {
22
+ title: "Stagent",
23
+ description: "AI agent task management",
24
+ };
25
+
26
+ // Inline theme bootstrap prevents a flash between the server render and local theme preference.
27
+ const CRITICAL_THEME_CSS = `
28
+ :root {
29
+ color-scheme: light;
30
+ --background: oklch(0.98 0.005 260);
31
+ --foreground: oklch(0.15 0.02 260);
32
+ --surface-1: rgba(255, 255, 255, 0.92);
33
+ --surface-2: rgba(255, 255, 255, 0.82);
34
+ --border: oklch(0.82 0.01 260 / 0.2);
35
+ }
36
+ html.dark {
37
+ color-scheme: dark;
38
+ --background: oklch(0.09 0.02 265);
39
+ --foreground: oklch(0.93 0.015 270);
40
+ --surface-1: oklch(0.16 0.02 268 / 0.96);
41
+ --surface-2: oklch(0.14 0.018 272 / 0.9);
42
+ --border: oklch(0.36 0.03 270 / 0.28);
43
+ }
44
+ html { background: var(--background); }
45
+ `.replace(/\s+/g, " ").trim();
46
+
47
+ // Static theme initialization script — no user input, safe from XSS.
48
+ const THEME_INIT_SCRIPT = `(function(){try{var d=document.documentElement;var s=localStorage.getItem('stagent-theme');var t=s==='dark'||s==='light'?s:(window.matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light');d.classList.toggle('dark',t==='dark');d.dataset.theme=t;d.style.colorScheme=t;d.style.backgroundColor=t==='dark'?'oklch(0.09 0.02 265)':'oklch(0.98 0.005 260)';document.cookie='stagent-theme='+t+';path=/;max-age=31536000;SameSite=Lax';}catch(e){}})()`;
49
+
50
+ export default function RootLayout({
51
+ children,
52
+ }: {
53
+ children: React.ReactNode;
54
+ }) {
55
+ return (
56
+ <html lang="en" suppressHydrationWarning>
57
+ <head>
58
+ <style dangerouslySetInnerHTML={{ __html: CRITICAL_THEME_CSS }} />
59
+ <script dangerouslySetInnerHTML={{ __html: THEME_INIT_SCRIPT }} />
60
+ </head>
61
+ <body
62
+ className={`${geistSans.variable} ${geistMono.variable} font-sans antialiased bg-background text-foreground`}
63
+ >
64
+ <TooltipProvider>
65
+ <SidebarProvider>
66
+ <AppSidebar />
67
+ <SidebarInset className="min-w-0">
68
+ {children}
69
+ </SidebarInset>
70
+ </SidebarProvider>
71
+ <PendingApprovalHost />
72
+ <CommandPalette />
73
+ <Toaster />
74
+ </TooltipProvider>
75
+ </body>
76
+ </html>
77
+ );
78
+ }
@@ -0,0 +1,32 @@
1
+ import type { MetadataRoute } from "next";
2
+
3
+ export default function manifest(): MetadataRoute.Manifest {
4
+ return {
5
+ name: "Stagent",
6
+ short_name: "Stagent",
7
+ description: "AI agent task management",
8
+ start_url: "/",
9
+ display: "standalone",
10
+ background_color: "#0f172a",
11
+ theme_color: "#2563eb",
12
+ icons: [
13
+ {
14
+ src: "/icon-512.png",
15
+ sizes: "512x512",
16
+ type: "image/png",
17
+ purpose: "any",
18
+ },
19
+ {
20
+ src: "/icon-512.png",
21
+ sizes: "512x512",
22
+ type: "image/png",
23
+ purpose: "maskable",
24
+ },
25
+ {
26
+ src: "/icon.svg",
27
+ sizes: "any",
28
+ type: "image/svg+xml",
29
+ },
30
+ ],
31
+ };
32
+ }
@@ -0,0 +1,37 @@
1
+ import { Suspense } from "react";
2
+ import { db } from "@/lib/db";
3
+ import { tasks } from "@/lib/db/schema";
4
+ import { MonitorOverview } from "@/components/monitoring/monitor-overview";
5
+ import { MonitorRefreshButton } from "@/components/monitoring/monitor-overview-wrapper";
6
+ import { LogStream } from "@/components/monitoring/log-stream";
7
+ import { Skeleton } from "@/components/ui/skeleton";
8
+
9
+ export const dynamic = "force-dynamic";
10
+
11
+ export default async function MonitorPage() {
12
+ const activeTasks = await db
13
+ .select({ id: tasks.id, title: tasks.title })
14
+ .from(tasks)
15
+ .orderBy(tasks.createdAt);
16
+
17
+ return (
18
+ <div className="gradient-forest-dawn min-h-screen p-6">
19
+ <div className="flex items-center justify-between mb-6">
20
+ <h1 className="text-2xl font-bold">Monitor</h1>
21
+ <MonitorRefreshButton />
22
+ </div>
23
+ <Suspense
24
+ fallback={
25
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
26
+ {Array.from({ length: 4 }).map((_, i) => (
27
+ <Skeleton key={i} className="h-24 rounded-lg" />
28
+ ))}
29
+ </div>
30
+ }
31
+ >
32
+ <MonitorOverview />
33
+ </Suspense>
34
+ <LogStream tasks={activeTasks} />
35
+ </div>
36
+ );
37
+ }
@@ -0,0 +1,162 @@
1
+ import { db } from "@/lib/db";
2
+ import { tasks, projects, agentLogs, notifications } from "@/lib/db/schema";
3
+ import { eq, count, gte, and, desc, sql, inArray } from "drizzle-orm";
4
+ import { Greeting } from "@/components/dashboard/greeting";
5
+ import { StatsCards } from "@/components/dashboard/stats-cards";
6
+ import { PriorityQueue } from "@/components/dashboard/priority-queue";
7
+ import type { PriorityTask } from "@/components/dashboard/priority-queue";
8
+ import { ActivityFeed } from "@/components/dashboard/activity-feed";
9
+ import type { ActivityEntry } from "@/components/dashboard/activity-feed";
10
+ import { QuickActions } from "@/components/dashboard/quick-actions";
11
+ import { RecentProjects } from "@/components/dashboard/recent-projects";
12
+ import type { RecentProject } from "@/components/dashboard/recent-projects";
13
+ import {
14
+ getCompletionsByDay,
15
+ getTaskCreationsByDay,
16
+ getActiveProjectActivityByDay,
17
+ getAgentActivityByHour,
18
+ getNotificationsByDay,
19
+ } from "@/lib/queries/chart-data";
20
+
21
+ export const dynamic = "force-dynamic";
22
+
23
+ export default async function HomePage() {
24
+ const today = new Date();
25
+ today.setHours(0, 0, 0, 0);
26
+
27
+ // Run all DB queries in parallel
28
+ const [
29
+ [runningResult],
30
+ [failedResult],
31
+ [completedTodayResult],
32
+ [completedAllTimeResult],
33
+ [awaitingResult],
34
+ [activeProjectsResult],
35
+ priorityTasks,
36
+ recentLogs,
37
+ allProjects,
38
+ recentActiveProjects,
39
+ completionsByDay,
40
+ taskCreationsByDay,
41
+ projectCreationsByDay,
42
+ agentActivityByHour,
43
+ notificationsByDay,
44
+ ] = await Promise.all([
45
+ db.select({ count: count() }).from(tasks).where(eq(tasks.status, "running")),
46
+ db.select({ count: count() }).from(tasks).where(eq(tasks.status, "failed")),
47
+ db.select({ count: count() }).from(tasks).where(
48
+ and(eq(tasks.status, "completed"), gte(tasks.updatedAt, today))
49
+ ),
50
+ db.select({ count: count() }).from(tasks).where(eq(tasks.status, "completed")),
51
+ db.select({ count: count() }).from(notifications).where(
52
+ and(
53
+ eq(notifications.read, false),
54
+ inArray(notifications.type, [
55
+ "permission_required",
56
+ "agent_message",
57
+ "budget_alert",
58
+ ])
59
+ )
60
+ ),
61
+ db.select({ count: count() }).from(projects).where(eq(projects.status, "active")),
62
+ // Priority queue: failed + running tasks, sorted by priority
63
+ db.select().from(tasks).where(
64
+ inArray(tasks.status, ["failed", "running", "queued"])
65
+ ).orderBy(tasks.priority, desc(tasks.updatedAt)).limit(5),
66
+ // Recent agent logs
67
+ db.select().from(agentLogs).orderBy(desc(agentLogs.timestamp)).limit(6),
68
+ // All projects for quick actions
69
+ db.select({ id: projects.id, name: projects.name }).from(projects).orderBy(projects.name),
70
+ // Recent active projects with task counts
71
+ db.select({
72
+ id: projects.id,
73
+ name: projects.name,
74
+ }).from(projects)
75
+ .where(eq(projects.status, "active"))
76
+ .orderBy(desc(projects.updatedAt))
77
+ .limit(3),
78
+ // Chart data queries
79
+ getCompletionsByDay(7),
80
+ getTaskCreationsByDay(7),
81
+ getActiveProjectActivityByDay(7),
82
+ getAgentActivityByHour(),
83
+ getNotificationsByDay(7),
84
+ ]);
85
+
86
+ // Build project name lookup for priority tasks
87
+ const projectMap = new Map(allProjects.map((p) => [p.id, p.name]));
88
+
89
+ const serializedPriorityTasks: PriorityTask[] = priorityTasks.map((t) => ({
90
+ id: t.id,
91
+ title: t.title,
92
+ status: t.status,
93
+ priority: t.priority,
94
+ projectName: t.projectId ? projectMap.get(t.projectId) ?? undefined : undefined,
95
+ }));
96
+
97
+ // Get task titles for log entries
98
+ const logTaskIds = [...new Set(recentLogs.filter((l) => l.taskId).map((l) => l.taskId!))];
99
+ const logTasks = logTaskIds.length > 0
100
+ ? await db.select({ id: tasks.id, title: tasks.title }).from(tasks).where(inArray(tasks.id, logTaskIds))
101
+ : [];
102
+ const taskTitleMap = new Map(logTasks.map((t) => [t.id, t.title]));
103
+
104
+ const serializedLogs: ActivityEntry[] = recentLogs.map((l) => ({
105
+ id: l.id,
106
+ event: l.event,
107
+ payload: l.payload,
108
+ timestamp: l.timestamp.toISOString(),
109
+ taskTitle: l.taskId ? taskTitleMap.get(l.taskId) ?? undefined : undefined,
110
+ }));
111
+
112
+ // Get task counts per project for recent projects
113
+ const recentProjectData: RecentProject[] = await Promise.all(
114
+ recentActiveProjects.map(async (p) => {
115
+ const [total] = await db.select({ count: count() }).from(tasks).where(eq(tasks.projectId, p.id));
116
+ const [completed] = await db.select({ count: count() }).from(tasks).where(
117
+ and(eq(tasks.projectId, p.id), eq(tasks.status, "completed"))
118
+ );
119
+ return {
120
+ id: p.id,
121
+ name: p.name,
122
+ totalTasks: total.count,
123
+ completedTasks: completed.count,
124
+ };
125
+ })
126
+ );
127
+
128
+ return (
129
+ <div className="gradient-morning-sky min-h-screen p-4 sm:p-6">
130
+ <div className="surface-page surface-page-shell mx-auto min-h-[calc(100dvh-2rem)] max-w-7xl rounded-[30px] p-5 sm:p-6 lg:p-7">
131
+ <Greeting
132
+ runningCount={runningResult.count}
133
+ awaitingCount={awaitingResult.count}
134
+ failedCount={failedResult.count}
135
+ />
136
+ <StatsCards
137
+ runningCount={runningResult.count}
138
+ completedToday={completedTodayResult.count}
139
+ completedAllTime={completedAllTimeResult.count}
140
+ awaitingReview={awaitingResult.count}
141
+ activeProjects={activeProjectsResult.count}
142
+ sparklines={{
143
+ completions: completionsByDay,
144
+ creations: taskCreationsByDay,
145
+ projects: projectCreationsByDay,
146
+ notifications: notificationsByDay,
147
+ }}
148
+ />
149
+ <div className="grid grid-cols-1 gap-6 lg:grid-cols-5 mb-6">
150
+ <div className="lg:col-span-3">
151
+ <PriorityQueue tasks={serializedPriorityTasks} />
152
+ </div>
153
+ <div className="lg:col-span-2">
154
+ <ActivityFeed entries={serializedLogs} hourlyActivity={agentActivityByHour} />
155
+ </div>
156
+ </div>
157
+ <QuickActions />
158
+ <RecentProjects projects={recentProjectData} />
159
+ </div>
160
+ </div>
161
+ );
162
+ }
@@ -0,0 +1,39 @@
1
+ import { notFound } from "next/navigation";
2
+ import Link from "next/link";
3
+ import { getProfile, isBuiltin } from "@/lib/agents/profiles/registry";
4
+ import { Button } from "@/components/ui/button";
5
+ import { ArrowLeft } from "lucide-react";
6
+ import { ProfileFormView } from "@/components/profiles/profile-form-view";
7
+
8
+ export const dynamic = "force-dynamic";
9
+
10
+ export default async function EditProfilePage({
11
+ params,
12
+ searchParams,
13
+ }: {
14
+ params: Promise<{ id: string }>;
15
+ searchParams: Promise<{ duplicate?: string }>;
16
+ }) {
17
+ const { id } = await params;
18
+ const { duplicate } = await searchParams;
19
+
20
+ const profile = getProfile(id);
21
+ if (!profile) notFound();
22
+
23
+ // Builtins can't be edited, but can be duplicated
24
+ if (isBuiltin(id) && duplicate !== "true") notFound();
25
+
26
+ return (
27
+ <div className="gradient-ocean-mist min-h-screen p-6">
28
+ <div className="max-w-6xl mx-auto">
29
+ <Link href={duplicate === "true" ? "/profiles" : `/profiles/${id}`}>
30
+ <Button variant="ghost" size="sm" className="mb-4">
31
+ <ArrowLeft className="h-4 w-4 mr-1" />
32
+ {duplicate === "true" ? "Back to Profiles" : "Back to Profile"}
33
+ </Button>
34
+ </Link>
35
+ <ProfileFormView profileId={id} duplicate={duplicate === "true"} />
36
+ </div>
37
+ </div>
38
+ );
39
+ }
@@ -0,0 +1,33 @@
1
+ import { notFound } from "next/navigation";
2
+ import Link from "next/link";
3
+ import { getProfile, isBuiltin } from "@/lib/agents/profiles/registry";
4
+ import { Button } from "@/components/ui/button";
5
+ import { ArrowLeft } from "lucide-react";
6
+ import { ProfileDetailView } from "@/components/profiles/profile-detail-view";
7
+
8
+ export const dynamic = "force-dynamic";
9
+
10
+ export default async function ProfileDetailPage({
11
+ params,
12
+ }: {
13
+ params: Promise<{ id: string }>;
14
+ }) {
15
+ const { id } = await params;
16
+
17
+ const profile = getProfile(id);
18
+ if (!profile) notFound();
19
+
20
+ return (
21
+ <div className="gradient-ocean-mist min-h-[100dvh] p-4 sm:p-6">
22
+ <div className="surface-page mx-auto max-w-7xl rounded-[28px] border border-border/60 p-6 shadow-[0_18px_48px_oklch(0.12_0.02_260_/_0.08)]">
23
+ <Link href="/profiles">
24
+ <Button variant="ghost" size="sm" className="mb-4">
25
+ <ArrowLeft className="h-4 w-4 mr-1" />
26
+ Back to Profiles
27
+ </Button>
28
+ </Link>
29
+ <ProfileDetailView profileId={id} isBuiltin={isBuiltin(id)} initialProfile={profile} />
30
+ </div>
31
+ </div>
32
+ );
33
+ }
@@ -0,0 +1,22 @@
1
+ import Link from "next/link";
2
+ import { Button } from "@/components/ui/button";
3
+ import { ArrowLeft } from "lucide-react";
4
+ import { ProfileFormView } from "@/components/profiles/profile-form-view";
5
+
6
+ export const dynamic = "force-dynamic";
7
+
8
+ export default async function NewProfilePage() {
9
+ return (
10
+ <div className="gradient-ocean-mist min-h-screen p-6">
11
+ <div className="max-w-6xl mx-auto">
12
+ <Link href="/profiles">
13
+ <Button variant="ghost" size="sm" className="mb-4">
14
+ <ArrowLeft className="h-4 w-4 mr-1" />
15
+ Back to Profiles
16
+ </Button>
17
+ </Link>
18
+ <ProfileFormView />
19
+ </div>
20
+ </div>
21
+ );
22
+ }
@@ -0,0 +1,19 @@
1
+ import { listProfiles, isBuiltin } from "@/lib/agents/profiles/registry";
2
+ import { ProfileBrowser } from "@/components/profiles/profile-browser";
3
+
4
+ export const dynamic = "force-dynamic";
5
+
6
+ export default async function ProfilesPage() {
7
+ const profiles = listProfiles().map((p) => ({
8
+ ...p,
9
+ isBuiltin: isBuiltin(p.id),
10
+ }));
11
+
12
+ return (
13
+ <div className="gradient-ocean-mist min-h-[100dvh] p-4 sm:p-6">
14
+ <div className="surface-page mx-auto max-w-7xl rounded-[28px] border border-border/60 p-6 shadow-[0_18px_48px_oklch(0.12_0.02_260_/_0.08)]">
15
+ <ProfileBrowser initialProfiles={profiles} />
16
+ </div>
17
+ </div>
18
+ );
19
+ }
@@ -0,0 +1,134 @@
1
+ import { notFound } from "next/navigation";
2
+ import Link from "next/link";
3
+ import { db } from "@/lib/db";
4
+ import { projects, tasks } from "@/lib/db/schema";
5
+ import { eq, count } from "drizzle-orm";
6
+ import { Badge } from "@/components/ui/badge";
7
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
8
+ import { Button } from "@/components/ui/button";
9
+ import { ArrowLeft } from "lucide-react";
10
+ import { COLUMN_ORDER } from "@/lib/constants/task-status";
11
+ import { ProjectDetailClient } from "@/components/projects/project-detail";
12
+ import { Sparkline } from "@/components/charts/sparkline";
13
+ import { getProjectCompletionTrend } from "@/lib/queries/chart-data";
14
+
15
+ export const dynamic = "force-dynamic";
16
+
17
+ export default async function ProjectDetailPage({
18
+ params,
19
+ }: {
20
+ params: Promise<{ id: string }>;
21
+ }) {
22
+ const { id } = await params;
23
+
24
+ const [project] = await db
25
+ .select()
26
+ .from(projects)
27
+ .where(eq(projects.id, id));
28
+
29
+ if (!project) notFound();
30
+
31
+ const projectTasks = await db
32
+ .select()
33
+ .from(tasks)
34
+ .where(eq(tasks.projectId, id))
35
+ .orderBy(tasks.priority, tasks.createdAt);
36
+
37
+ // Status breakdown
38
+ const statusCounts: Record<string, number> = {};
39
+ for (const status of COLUMN_ORDER) {
40
+ statusCounts[status] = projectTasks.filter((t) => t.status === status).length;
41
+ }
42
+
43
+ const completionTrend = await getProjectCompletionTrend(id, 14);
44
+ const totalTasks = projectTasks.length;
45
+
46
+ const serializedTasks = projectTasks.map((t) => ({
47
+ ...t,
48
+ createdAt: t.createdAt.toISOString(),
49
+ updatedAt: t.updatedAt.toISOString(),
50
+ }));
51
+
52
+ const statusVariant: Record<string, "default" | "secondary" | "outline"> = {
53
+ active: "default",
54
+ paused: "secondary",
55
+ completed: "outline",
56
+ };
57
+
58
+ return (
59
+ <div className="gradient-ocean-mist min-h-screen p-6">
60
+ <div className="mb-6">
61
+ <Link href="/projects">
62
+ <Button variant="ghost" size="sm" className="mb-2">
63
+ <ArrowLeft className="h-4 w-4 mr-1" />
64
+ Back to Projects
65
+ </Button>
66
+ </Link>
67
+ <div className="flex items-center gap-3">
68
+ <h1 className="text-2xl font-bold">{project.name}</h1>
69
+ <Badge variant={statusVariant[project.status] ?? "secondary"}>
70
+ {project.status}
71
+ </Badge>
72
+ </div>
73
+ {project.description && (
74
+ <p className="text-muted-foreground mt-1">{project.description}</p>
75
+ )}
76
+ </div>
77
+
78
+ {/* Status breakdown */}
79
+ <div className="grid grid-cols-3 sm:grid-cols-6 gap-3 mb-6">
80
+ {COLUMN_ORDER.map((status) => (
81
+ <Card key={status}>
82
+ <CardHeader className="pb-1 pt-3 px-3">
83
+ <CardTitle className="text-xs font-medium text-muted-foreground capitalize">
84
+ {status}
85
+ </CardTitle>
86
+ </CardHeader>
87
+ <CardContent className="px-3 pb-3">
88
+ <div className="text-xl font-bold">{statusCounts[status]}</div>
89
+ </CardContent>
90
+ </Card>
91
+ ))}
92
+ </div>
93
+
94
+ {/* Stacked status bar + completion sparkline */}
95
+ {totalTasks > 0 && (
96
+ <div className="mb-6 space-y-3">
97
+ <div className="flex h-1.5 rounded-full overflow-hidden" role="img" aria-label="Task status distribution">
98
+ {COLUMN_ORDER.map((status) => {
99
+ const pct = (statusCounts[status] / totalTasks) * 100;
100
+ if (pct === 0) return null;
101
+ const statusColors: Record<string, string> = {
102
+ planned: "var(--muted-foreground)",
103
+ queued: "var(--chart-4)",
104
+ running: "var(--chart-1)",
105
+ completed: "var(--chart-2)",
106
+ failed: "var(--destructive)",
107
+ };
108
+ return (
109
+ <div
110
+ key={status}
111
+ style={{ width: `${pct}%`, backgroundColor: statusColors[status] ?? "var(--muted)" }}
112
+ title={`${status}: ${statusCounts[status]}`}
113
+ />
114
+ );
115
+ })}
116
+ </div>
117
+ <div className="flex items-center gap-3">
118
+ <span className="text-xs text-muted-foreground shrink-0">14-day completions</span>
119
+ <Sparkline
120
+ data={completionTrend}
121
+ width={200}
122
+ height={24}
123
+ color="var(--chart-2)"
124
+ label="14-day completion trend"
125
+ className="flex-1"
126
+ />
127
+ </div>
128
+ </div>
129
+ )}
130
+
131
+ <ProjectDetailClient tasks={serializedTasks} projectId={id} />
132
+ </div>
133
+ );
134
+ }
@@ -0,0 +1,17 @@
1
+ import { Skeleton } from "@/components/ui/skeleton";
2
+
3
+ export default function ProjectsLoading() {
4
+ return (
5
+ <div className="p-6">
6
+ <div className="flex items-center justify-between mb-6">
7
+ <Skeleton className="h-8 w-32" />
8
+ <Skeleton className="h-10 w-32 rounded-md" />
9
+ </div>
10
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
11
+ {Array.from({ length: 6 }).map((_, i) => (
12
+ <Skeleton key={i} className="h-32 rounded-lg" />
13
+ ))}
14
+ </div>
15
+ </div>
16
+ );
17
+ }