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,403 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+ import {
5
+ Dialog,
6
+ DialogContent,
7
+ DialogDescription,
8
+ DialogHeader,
9
+ DialogTitle,
10
+ DialogTrigger,
11
+ } from "@/components/ui/dialog";
12
+ import { Button } from "@/components/ui/button";
13
+ import { Input } from "@/components/ui/input";
14
+ import { Textarea } from "@/components/ui/textarea";
15
+ import { Label } from "@/components/ui/label";
16
+ import {
17
+ Select,
18
+ SelectContent,
19
+ SelectItem,
20
+ SelectTrigger,
21
+ SelectValue,
22
+ } from "@/components/ui/select";
23
+ import { Switch } from "@/components/ui/switch";
24
+ import { Plus, Calendar, Clock, Bot } from "lucide-react";
25
+ import { toast } from "sonner";
26
+ import {
27
+ type AgentRuntimeId,
28
+ DEFAULT_AGENT_RUNTIME,
29
+ listRuntimeCatalog,
30
+ } from "@/lib/agents/runtime/catalog";
31
+ import {
32
+ getSupportedRuntimes,
33
+ profileSupportsRuntime,
34
+ } from "@/lib/agents/profiles/compatibility";
35
+ import type { AgentProfile } from "@/lib/agents/profiles/types";
36
+
37
+ type ProfileOption = Pick<AgentProfile, "id" | "name" | "supportedRuntimes">;
38
+
39
+ interface ScheduleCreateDialogProps {
40
+ projects: { id: string; name: string }[];
41
+ onCreated: () => void;
42
+ }
43
+
44
+ const INTERVAL_PRESETS = [
45
+ { label: "Every 5 minutes", value: "5m" },
46
+ { label: "Every 15 minutes", value: "15m" },
47
+ { label: "Every 30 minutes", value: "30m" },
48
+ { label: "Every hour", value: "1h" },
49
+ { label: "Every 2 hours", value: "2h" },
50
+ { label: "Daily at 9 AM", value: "1d" },
51
+ { label: "Custom", value: "custom" },
52
+ ];
53
+
54
+ export function ScheduleCreateDialog({
55
+ projects,
56
+ onCreated,
57
+ }: ScheduleCreateDialogProps) {
58
+ const runtimeOptions = listRuntimeCatalog();
59
+ const runtimeLabelMap = new Map(
60
+ runtimeOptions.map((runtime) => [runtime.id, runtime.label])
61
+ );
62
+ const [open, setOpen] = useState(false);
63
+ const [name, setName] = useState("");
64
+ const [prompt, setPrompt] = useState("");
65
+ const [intervalPreset, setIntervalPreset] = useState("5m");
66
+ const [customInterval, setCustomInterval] = useState("");
67
+ const [projectId, setProjectId] = useState("");
68
+ const [assignedAgent, setAssignedAgent] = useState("");
69
+ const [agentProfile, setAgentProfile] = useState("");
70
+ const [recurs, setRecurs] = useState(true);
71
+ const [maxFirings, setMaxFirings] = useState<number | "">("");
72
+ const [expiresInHours, setExpiresInHours] = useState<number | "">("");
73
+ const [profiles, setProfiles] = useState<ProfileOption[]>([]);
74
+ const [loading, setLoading] = useState(false);
75
+ const [error, setError] = useState<string | null>(null);
76
+
77
+ useEffect(() => {
78
+ fetch("/api/profiles")
79
+ .then((r) => r.json())
80
+ .then((data: ProfileOption[]) => setProfiles(data))
81
+ .catch(() => {});
82
+ }, []);
83
+
84
+ const selectedRuntimeId = (assignedAgent ||
85
+ DEFAULT_AGENT_RUNTIME) as AgentRuntimeId;
86
+ const selectedProfile = profiles.find((profile) => profile.id === agentProfile);
87
+ const profileCompatibilityError =
88
+ selectedProfile && !profileSupportsRuntime(selectedProfile, selectedRuntimeId)
89
+ ? `${selectedProfile.name} does not support ${
90
+ runtimeLabelMap.get(selectedRuntimeId) ?? selectedRuntimeId
91
+ }`
92
+ : null;
93
+
94
+ function resetForm() {
95
+ setName("");
96
+ setPrompt("");
97
+ setIntervalPreset("5m");
98
+ setCustomInterval("");
99
+ setProjectId("");
100
+ setAssignedAgent("");
101
+ setAgentProfile("");
102
+ setRecurs(true);
103
+ setMaxFirings("");
104
+ setExpiresInHours("");
105
+ setError(null);
106
+ }
107
+
108
+ async function handleSubmit(e: React.FormEvent) {
109
+ e.preventDefault();
110
+ if (!name.trim() || !prompt.trim()) return;
111
+
112
+ const interval =
113
+ intervalPreset === "custom" ? customInterval : intervalPreset;
114
+ if (!interval.trim()) {
115
+ setError("Please enter an interval");
116
+ return;
117
+ }
118
+ if (profileCompatibilityError) {
119
+ setError(profileCompatibilityError);
120
+ return;
121
+ }
122
+
123
+ setLoading(true);
124
+ setError(null);
125
+
126
+ try {
127
+ const res = await fetch("/api/schedules", {
128
+ method: "POST",
129
+ headers: { "Content-Type": "application/json" },
130
+ body: JSON.stringify({
131
+ name: name.trim(),
132
+ prompt: prompt.trim(),
133
+ interval,
134
+ projectId: projectId || undefined,
135
+ assignedAgent: assignedAgent || undefined,
136
+ agentProfile: agentProfile || undefined,
137
+ recurs,
138
+ maxFirings: maxFirings || undefined,
139
+ expiresInHours: expiresInHours || undefined,
140
+ }),
141
+ });
142
+
143
+ if (res.ok) {
144
+ resetForm();
145
+ setOpen(false);
146
+ toast.success("Schedule created");
147
+ onCreated();
148
+ } else {
149
+ const data = await res.json().catch(() => null);
150
+ setError(data?.error ?? `Failed to create schedule (${res.status})`);
151
+ }
152
+ } catch {
153
+ setError("Network error — could not reach server");
154
+ } finally {
155
+ setLoading(false);
156
+ }
157
+ }
158
+
159
+ return (
160
+ <Dialog open={open} onOpenChange={setOpen}>
161
+ <DialogTrigger asChild>
162
+ <Button>
163
+ <Plus className="h-4 w-4 mr-2" />
164
+ New Schedule
165
+ </Button>
166
+ </DialogTrigger>
167
+ <DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
168
+ <DialogHeader>
169
+ <DialogTitle className="flex items-center gap-2">
170
+ <Calendar className="h-5 w-5 text-muted-foreground" />
171
+ Create Schedule
172
+ </DialogTitle>
173
+ <DialogDescription>
174
+ Define when the agent should run, what it should do, and which project context it should use.
175
+ </DialogDescription>
176
+ </DialogHeader>
177
+ <form onSubmit={handleSubmit} className="space-y-4">
178
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
179
+ {/* Left column */}
180
+ <div className="space-y-2">
181
+ <Label htmlFor="sched-name">Name</Label>
182
+ <Input
183
+ id="sched-name"
184
+ value={name}
185
+ onChange={(e) => setName(e.target.value)}
186
+ placeholder="e.g., Build status check"
187
+ required
188
+ />
189
+ <p className="text-xs text-muted-foreground">Human-readable schedule name</p>
190
+ </div>
191
+
192
+ {/* Right column */}
193
+ <div className="space-y-2">
194
+ <Label className="flex items-center gap-1.5">
195
+ <Clock className="h-3.5 w-3.5 text-muted-foreground" />
196
+ Interval
197
+ </Label>
198
+ <Select value={intervalPreset} onValueChange={setIntervalPreset}>
199
+ <SelectTrigger>
200
+ <SelectValue />
201
+ </SelectTrigger>
202
+ <SelectContent>
203
+ {INTERVAL_PRESETS.map((p) => (
204
+ <SelectItem key={p.value} value={p.value}>
205
+ {p.label}
206
+ </SelectItem>
207
+ ))}
208
+ </SelectContent>
209
+ </Select>
210
+ {intervalPreset === "custom" && (
211
+ <>
212
+ <Input
213
+ value={customInterval}
214
+ onChange={(e) => setCustomInterval(e.target.value)}
215
+ placeholder="e.g., 10m, 3h, or */5 * * * *"
216
+ className="mt-1"
217
+ />
218
+ <p className="text-xs text-muted-foreground">Duration or cron expression</p>
219
+ </>
220
+ )}
221
+ </div>
222
+
223
+ {/* Prompt — spans full left column */}
224
+ <div className="space-y-2">
225
+ <Label htmlFor="sched-prompt">Prompt</Label>
226
+ <Textarea
227
+ id="sched-prompt"
228
+ value={prompt}
229
+ onChange={(e) => setPrompt(e.target.value)}
230
+ placeholder="What the agent does each firing"
231
+ rows={3}
232
+ required
233
+ />
234
+ <p className="text-xs text-muted-foreground">Instructions for each execution</p>
235
+ </div>
236
+
237
+ {/* Recurring + conditional fields */}
238
+ <div className="space-y-3">
239
+ <div className="flex items-center justify-between">
240
+ <Label htmlFor="sched-recurs">Recurring</Label>
241
+ <Switch
242
+ id="sched-recurs"
243
+ checked={recurs}
244
+ onCheckedChange={setRecurs}
245
+ />
246
+ </div>
247
+ {recurs && (
248
+ <>
249
+ <div className="space-y-1.5">
250
+ <Label htmlFor="sched-max">Max firings</Label>
251
+ <Input
252
+ id="sched-max"
253
+ type="number"
254
+ min={1}
255
+ value={maxFirings}
256
+ onChange={(e) =>
257
+ setMaxFirings(e.target.value ? Number(e.target.value) : "")
258
+ }
259
+ placeholder="Unlimited"
260
+ />
261
+ <p className="text-xs text-muted-foreground">Leave empty = unlimited</p>
262
+ </div>
263
+ <div className="space-y-1.5">
264
+ <Label htmlFor="sched-expires">Expires in (hours)</Label>
265
+ <Input
266
+ id="sched-expires"
267
+ type="number"
268
+ min={1}
269
+ value={expiresInHours}
270
+ onChange={(e) =>
271
+ setExpiresInHours(
272
+ e.target.value ? Number(e.target.value) : ""
273
+ )
274
+ }
275
+ placeholder="Never"
276
+ />
277
+ <p className="text-xs text-muted-foreground">Auto-pause timer</p>
278
+ </div>
279
+ </>
280
+ )}
281
+ </div>
282
+
283
+ {/* Project */}
284
+ {projects.length > 0 && (
285
+ <div className="space-y-2">
286
+ <Label>Project</Label>
287
+ <Select
288
+ value={projectId || "none"}
289
+ onValueChange={(value) =>
290
+ setProjectId(value === "none" ? "" : value)
291
+ }
292
+ >
293
+ <SelectTrigger>
294
+ <SelectValue placeholder="None" />
295
+ </SelectTrigger>
296
+ <SelectContent>
297
+ <SelectItem value="none">None</SelectItem>
298
+ {projects.map((p) => (
299
+ <SelectItem key={p.id} value={p.id}>
300
+ {p.name}
301
+ </SelectItem>
302
+ ))}
303
+ </SelectContent>
304
+ </Select>
305
+ <p className="text-xs text-muted-foreground">Context directory</p>
306
+ </div>
307
+ )}
308
+
309
+ <div className="space-y-2">
310
+ <Label className="flex items-center gap-1.5">
311
+ <Bot className="h-3.5 w-3.5 text-muted-foreground" />
312
+ Runtime
313
+ </Label>
314
+ <Select
315
+ value={assignedAgent || "default"}
316
+ onValueChange={(value) =>
317
+ setAssignedAgent(value === "default" ? "" : value)
318
+ }
319
+ >
320
+ <SelectTrigger>
321
+ <SelectValue placeholder="Default runtime" />
322
+ </SelectTrigger>
323
+ <SelectContent>
324
+ <SelectItem value="default">Default runtime</SelectItem>
325
+ {runtimeOptions.map((runtime) => (
326
+ <SelectItem key={runtime.id} value={runtime.id}>
327
+ {runtime.label}
328
+ </SelectItem>
329
+ ))}
330
+ </SelectContent>
331
+ </Select>
332
+ <p className="text-xs text-muted-foreground">
333
+ Which provider runtime each firing should use
334
+ </p>
335
+ </div>
336
+
337
+ {/* Agent Profile */}
338
+ {profiles.length > 0 && (
339
+ <div className="space-y-2">
340
+ <Label className="flex items-center gap-1.5">
341
+ <Bot className="h-3.5 w-3.5 text-muted-foreground" />
342
+ Agent Profile
343
+ </Label>
344
+ <Select
345
+ value={agentProfile || "auto"}
346
+ onValueChange={(value) =>
347
+ setAgentProfile(value === "auto" ? "" : value)
348
+ }
349
+ >
350
+ <SelectTrigger>
351
+ <SelectValue placeholder="Auto-detect" />
352
+ </SelectTrigger>
353
+ <SelectContent>
354
+ <SelectItem value="auto">Auto-detect</SelectItem>
355
+ {profiles.map((p) => (
356
+ <SelectItem
357
+ key={p.id}
358
+ value={p.id}
359
+ disabled={!profileSupportsRuntime(p, selectedRuntimeId)}
360
+ >
361
+ {p.name}
362
+ </SelectItem>
363
+ ))}
364
+ </SelectContent>
365
+ </Select>
366
+ <p className="text-xs text-muted-foreground">
367
+ Auto-detect only considers profiles compatible with the selected runtime
368
+ </p>
369
+ {selectedProfile && (
370
+ <p
371
+ className={`text-xs ${
372
+ profileCompatibilityError
373
+ ? "text-destructive"
374
+ : "text-muted-foreground"
375
+ }`}
376
+ >
377
+ {profileCompatibilityError ??
378
+ `Supports ${getSupportedRuntimes(selectedProfile)
379
+ .map(
380
+ (runtimeId) =>
381
+ runtimeLabelMap.get(runtimeId) ?? runtimeId
382
+ )
383
+ .join(", ")}`}
384
+ </p>
385
+ )}
386
+ </div>
387
+ )}
388
+ </div>
389
+
390
+ {error && <p className="text-sm text-destructive">{error}</p>}
391
+
392
+ <Button
393
+ type="submit"
394
+ disabled={loading || !name.trim() || !prompt.trim()}
395
+ className="w-full"
396
+ >
397
+ {loading ? "Creating..." : "Create Schedule"}
398
+ </Button>
399
+ </form>
400
+ </DialogContent>
401
+ </Dialog>
402
+ );
403
+ }
@@ -0,0 +1,274 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState, useCallback } from "react";
4
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
5
+ import { Badge } from "@/components/ui/badge";
6
+ import { Button } from "@/components/ui/button";
7
+ import { Skeleton } from "@/components/ui/skeleton";
8
+ import { ScheduleStatusBadge } from "./schedule-status-badge";
9
+ import { ConfirmDialog } from "@/components/shared/confirm-dialog";
10
+ import { describeCron } from "@/lib/schedules/interval-parser";
11
+ import { taskStatusVariant } from "@/lib/constants/status-colors";
12
+ import { Pause, Play, Trash2, Clock, Zap, Hash } from "lucide-react";
13
+ import { toast } from "sonner";
14
+ import { useRouter } from "next/navigation";
15
+
16
+ interface TaskSummary {
17
+ id: string;
18
+ title: string;
19
+ status: string;
20
+ createdAt: string;
21
+ result: string | null;
22
+ }
23
+
24
+ interface ScheduleDetail {
25
+ id: string;
26
+ name: string;
27
+ prompt: string;
28
+ cronExpression: string;
29
+ assignedAgent: string | null;
30
+ agentProfile: string | null;
31
+ recurs: boolean;
32
+ status: string;
33
+ maxFirings: number | null;
34
+ firingCount: number;
35
+ expiresAt: string | null;
36
+ lastFiredAt: string | null;
37
+ nextFireAt: string | null;
38
+ createdAt: string;
39
+ firingHistory: TaskSummary[];
40
+ }
41
+
42
+ interface ScheduleDetailViewProps {
43
+ scheduleId: string;
44
+ initialSchedule?: ScheduleDetail;
45
+ }
46
+
47
+ export function ScheduleDetailView({ scheduleId, initialSchedule }: ScheduleDetailViewProps) {
48
+ const router = useRouter();
49
+ const [schedule, setSchedule] = useState<ScheduleDetail | null>(initialSchedule ?? null);
50
+ const [loaded, setLoaded] = useState(!!initialSchedule);
51
+ const [confirmDelete, setConfirmDelete] = useState(false);
52
+
53
+ const refresh = useCallback(async () => {
54
+ const res = await fetch(`/api/schedules/${scheduleId}`);
55
+ if (res.ok) setSchedule(await res.json());
56
+ setLoaded(true);
57
+ }, [scheduleId]);
58
+
59
+ useEffect(() => {
60
+ // Always refresh to get enriched data (firingHistory), but don't block rendering
61
+ refresh();
62
+ const interval = setInterval(refresh, 10_000);
63
+ return () => clearInterval(interval);
64
+ }, [refresh]);
65
+
66
+ async function handlePauseResume() {
67
+ if (!schedule) return;
68
+ const newStatus = schedule.status === "active" ? "paused" : "active";
69
+ const res = await fetch(`/api/schedules/${scheduleId}`, {
70
+ method: "PATCH",
71
+ headers: { "Content-Type": "application/json" },
72
+ body: JSON.stringify({ status: newStatus }),
73
+ });
74
+ if (res.ok) {
75
+ toast.success(newStatus === "paused" ? "Schedule paused" : "Schedule resumed");
76
+ refresh();
77
+ } else {
78
+ const data = await res.json().catch(() => null);
79
+ toast.error(data?.error ?? "Failed to update schedule");
80
+ }
81
+ }
82
+
83
+ async function handleDelete() {
84
+ setConfirmDelete(false);
85
+ const res = await fetch(`/api/schedules/${scheduleId}`, {
86
+ method: "DELETE",
87
+ });
88
+ if (res.ok) {
89
+ toast.success("Schedule deleted");
90
+ router.push("/schedules");
91
+ } else {
92
+ const data = await res.json().catch(() => null);
93
+ toast.error(data?.error ?? "Failed to delete schedule");
94
+ }
95
+ }
96
+
97
+ if (!loaded) {
98
+ return (
99
+ <div className="space-y-4">
100
+ <Skeleton className="h-8 w-64" />
101
+ <Skeleton className="h-32 w-full" />
102
+ <Skeleton className="h-48 w-full" />
103
+ </div>
104
+ );
105
+ }
106
+
107
+ if (!schedule) {
108
+ return <p className="text-muted-foreground">Schedule not found.</p>;
109
+ }
110
+
111
+ function formatDate(val: string | null): string {
112
+ if (!val) return "—";
113
+ return new Date(val).toLocaleString();
114
+ }
115
+
116
+ return (
117
+ <div className="space-y-6" aria-live="polite">
118
+ {/* Header */}
119
+ <div className="flex items-center justify-between">
120
+ <div>
121
+ <h1 className="text-2xl font-bold">{schedule.name}</h1>
122
+ <p className="text-sm text-muted-foreground mt-1">
123
+ {describeCron(schedule.cronExpression)}
124
+ {schedule.assignedAgent && ` · Runtime: ${schedule.assignedAgent}`}
125
+ {schedule.agentProfile && ` · Profile: ${schedule.agentProfile}`}
126
+ </p>
127
+ </div>
128
+ <div className="flex items-center gap-2">
129
+ <ScheduleStatusBadge status={schedule.status} />
130
+ {(schedule.status === "active" || schedule.status === "paused") && (
131
+ <Button
132
+ variant="outline"
133
+ size="sm"
134
+ onClick={handlePauseResume}
135
+ aria-label={
136
+ schedule.status === "active" ? "Pause schedule" : "Resume schedule"
137
+ }
138
+ >
139
+ {schedule.status === "active" ? (
140
+ <>
141
+ <Pause className="h-3.5 w-3.5 mr-1" />
142
+ Pause
143
+ </>
144
+ ) : (
145
+ <>
146
+ <Play className="h-3.5 w-3.5 mr-1" />
147
+ Resume
148
+ </>
149
+ )}
150
+ </Button>
151
+ )}
152
+ <Button
153
+ variant="outline"
154
+ size="sm"
155
+ className="text-destructive"
156
+ onClick={() => setConfirmDelete(true)}
157
+ aria-label="Delete schedule"
158
+ >
159
+ <Trash2 className="h-3.5 w-3.5 mr-1" />
160
+ Delete
161
+ </Button>
162
+ </div>
163
+ </div>
164
+
165
+ {/* Info cards */}
166
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
167
+ <Card>
168
+ <CardContent className="pt-4">
169
+ <div className="flex items-center gap-2 text-sm text-muted-foreground mb-1">
170
+ <Hash className="h-3.5 w-3.5" />
171
+ Firings
172
+ </div>
173
+ <p className="text-2xl font-bold">
174
+ {schedule.firingCount}
175
+ {schedule.maxFirings && (
176
+ <span className="text-sm font-normal text-muted-foreground">
177
+ {" "}
178
+ / {schedule.maxFirings}
179
+ </span>
180
+ )}
181
+ </p>
182
+ </CardContent>
183
+ </Card>
184
+ <Card>
185
+ <CardContent className="pt-4">
186
+ <div className="flex items-center gap-2 text-sm text-muted-foreground mb-1">
187
+ <Zap className="h-3.5 w-3.5" />
188
+ Last Fired
189
+ </div>
190
+ <p className="text-sm font-medium">
191
+ {formatDate(schedule.lastFiredAt)}
192
+ </p>
193
+ </CardContent>
194
+ </Card>
195
+ <Card>
196
+ <CardContent className="pt-4">
197
+ <div className="flex items-center gap-2 text-sm text-muted-foreground mb-1">
198
+ <Clock className="h-3.5 w-3.5" />
199
+ Next Fire
200
+ </div>
201
+ <p className="text-sm font-medium">
202
+ {schedule.status === "active"
203
+ ? formatDate(schedule.nextFireAt)
204
+ : "—"}
205
+ </p>
206
+ </CardContent>
207
+ </Card>
208
+ </div>
209
+
210
+ {/* Prompt */}
211
+ <Card>
212
+ <CardHeader className="pb-2">
213
+ <CardTitle className="text-sm font-medium">Prompt</CardTitle>
214
+ </CardHeader>
215
+ <CardContent>
216
+ <p className="text-sm whitespace-pre-wrap">{schedule.prompt}</p>
217
+ </CardContent>
218
+ </Card>
219
+
220
+ {/* Firing History */}
221
+ <Card>
222
+ <CardHeader className="pb-2">
223
+ <CardTitle className="text-sm font-medium">
224
+ Firing History ({schedule.firingHistory.length})
225
+ </CardTitle>
226
+ </CardHeader>
227
+ <CardContent>
228
+ {schedule.firingHistory.length === 0 ? (
229
+ <p className="text-sm text-muted-foreground">
230
+ No firings yet. The scheduler checks every 60 seconds.
231
+ </p>
232
+ ) : (
233
+ <div className="space-y-2">
234
+ {schedule.firingHistory.map((task) => (
235
+ <div
236
+ key={task.id}
237
+ className="flex items-center justify-between border rounded-lg p-3 text-sm cursor-pointer hover:bg-accent/50 transition-colors"
238
+ tabIndex={0}
239
+ onClick={() => router.push(`/monitor?taskId=${task.id}`)}
240
+ onKeyDown={(e) => {
241
+ if (e.key === "Enter" || e.key === " ") {
242
+ e.preventDefault();
243
+ router.push(`/monitor?taskId=${task.id}`);
244
+ }
245
+ }}
246
+ >
247
+ <div>
248
+ <span className="font-medium">{task.title}</span>
249
+ <span className="text-muted-foreground ml-2">
250
+ {formatDate(task.createdAt)}
251
+ </span>
252
+ </div>
253
+ <Badge variant={taskStatusVariant[task.status] ?? "secondary"}>
254
+ {task.status}
255
+ </Badge>
256
+ </div>
257
+ ))}
258
+ </div>
259
+ )}
260
+ </CardContent>
261
+ </Card>
262
+
263
+ <ConfirmDialog
264
+ open={confirmDelete}
265
+ onOpenChange={setConfirmDelete}
266
+ title="Delete Schedule"
267
+ description="This will permanently delete this schedule. Child tasks will be kept."
268
+ confirmLabel="Delete"
269
+ onConfirm={handleDelete}
270
+ destructive
271
+ />
272
+ </div>
273
+ );
274
+ }