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,478 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { usePathname, useRouter } from "next/navigation";
5
+ import { useEffect, useMemo, useRef, useState } from "react";
6
+ import {
7
+ ArrowUpRight,
8
+ Inbox,
9
+ Layers3,
10
+ ShieldAlert,
11
+ Workflow,
12
+ } from "lucide-react";
13
+
14
+ import { PermissionResponseActions } from "@/components/notifications/permission-response-actions";
15
+ import { Badge } from "@/components/ui/badge";
16
+ import {
17
+ Dialog,
18
+ DialogContent,
19
+ DialogDescription,
20
+ DialogHeader,
21
+ DialogTitle,
22
+ } from "@/components/ui/dialog";
23
+ import { Button } from "@/components/ui/button";
24
+ import {
25
+ Sheet,
26
+ SheetContent,
27
+ SheetDescription,
28
+ SheetHeader,
29
+ SheetTitle,
30
+ } from "@/components/ui/sheet";
31
+ import { useIsMobile } from "@/hooks/use-mobile";
32
+ import {
33
+ getPermissionDetailEntries,
34
+ type PermissionToolInput,
35
+ } from "@/lib/notifications/permissions";
36
+ import { formatTimestamp } from "@/lib/utils/format-timestamp";
37
+ import { cn } from "@/lib/utils";
38
+ import type { PendingApprovalPayload } from "@/lib/notifications/actionable";
39
+
40
+ function dedupePendingApprovals(items: PendingApprovalPayload[]) {
41
+ return Array.from(
42
+ new Map(items.map((item) => [item.notificationId, item])).values()
43
+ ).sort(
44
+ (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
45
+ );
46
+ }
47
+
48
+ function buildContextLabel(payload: PendingApprovalPayload) {
49
+ if (payload.workflowName && payload.taskTitle) {
50
+ return `${payload.workflowName} · ${payload.taskTitle}`;
51
+ }
52
+
53
+ return payload.taskTitle ?? payload.workflowName ?? "Approval request";
54
+ }
55
+
56
+ function PermissionDetailFields({
57
+ toolName,
58
+ toolInput,
59
+ }: {
60
+ toolName: string | null;
61
+ toolInput: PermissionToolInput | null;
62
+ }) {
63
+ const entries = getPermissionDetailEntries(toolName, toolInput);
64
+
65
+ if (entries.length === 0) return null;
66
+
67
+ return (
68
+ <dl className="space-y-2 text-sm">
69
+ {entries.map((entry) => (
70
+ <div
71
+ key={`${entry.label}-${entry.value}`}
72
+ className="rounded-xl border border-border/60 bg-background/50 px-3 py-2"
73
+ >
74
+ <dt className="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">
75
+ {entry.label}
76
+ </dt>
77
+ <dd className="mt-1 break-all font-mono text-xs text-foreground sm:text-sm">
78
+ {entry.value}
79
+ </dd>
80
+ </div>
81
+ ))}
82
+ </dl>
83
+ );
84
+ }
85
+
86
+ function PendingApprovalDetail({
87
+ selected,
88
+ overflow,
89
+ onResponded,
90
+ onOpenInbox,
91
+ onSelect,
92
+ }: {
93
+ selected: PendingApprovalPayload;
94
+ overflow: PendingApprovalPayload[];
95
+ onResponded: () => void;
96
+ onOpenInbox: () => void;
97
+ onSelect: (notificationId: string) => void;
98
+ }) {
99
+ return (
100
+ <div className="space-y-4">
101
+ <div className="flex flex-wrap items-center gap-2">
102
+ <Badge variant="outline" className="font-mono text-xs">
103
+ {selected.permissionLabel}
104
+ </Badge>
105
+ {selected.workflowName && (
106
+ <Badge variant="secondary" className="text-xs">
107
+ <Workflow className="h-3.5 w-3.5" />
108
+ Workflow
109
+ </Badge>
110
+ )}
111
+ {overflow.length > 0 && (
112
+ <Badge variant="outline" className="text-xs">
113
+ <Layers3 className="h-3.5 w-3.5" />
114
+ {overflow.length} more pending
115
+ </Badge>
116
+ )}
117
+ </div>
118
+
119
+ <div className="glass-card-light rounded-2xl p-4">
120
+ <p className="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">
121
+ Context
122
+ </p>
123
+ <p className="mt-2 text-sm font-medium text-foreground">
124
+ {buildContextLabel(selected)}
125
+ </p>
126
+ <p className="mt-2 text-sm text-muted-foreground">
127
+ {selected.compactSummary}
128
+ </p>
129
+ {selected.body && (
130
+ <p className="mt-3 text-sm leading-6 text-muted-foreground">
131
+ {selected.body}
132
+ </p>
133
+ )}
134
+ <p className="mt-3 text-xs text-muted-foreground">
135
+ Requested {formatTimestamp(selected.createdAt)}
136
+ </p>
137
+ </div>
138
+
139
+ <PermissionDetailFields
140
+ toolName={selected.toolName}
141
+ toolInput={selected.toolInput}
142
+ />
143
+
144
+ {selected.taskId && selected.toolName && selected.toolInput && (
145
+ <PermissionResponseActions
146
+ taskId={selected.taskId}
147
+ notificationId={selected.notificationId}
148
+ toolName={selected.toolName}
149
+ toolInput={selected.toolInput}
150
+ responded={false}
151
+ response={null}
152
+ onResponded={onResponded}
153
+ buttonSize="default"
154
+ layout="stacked"
155
+ />
156
+ )}
157
+
158
+ <div className="flex flex-wrap items-center gap-2">
159
+ <Button variant="outline" onClick={onOpenInbox}>
160
+ <Inbox className="h-4 w-4" />
161
+ Open Inbox
162
+ </Button>
163
+ {selected.deepLink !== "/inbox" && (
164
+ <Button variant="ghost" asChild>
165
+ <Link href={selected.deepLink}>
166
+ <ArrowUpRight className="h-4 w-4" />
167
+ View Context
168
+ </Link>
169
+ </Button>
170
+ )}
171
+ </div>
172
+
173
+ {overflow.length > 0 && (
174
+ <div className="space-y-2">
175
+ <p className="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">
176
+ Also pending
177
+ </p>
178
+ <div className="space-y-2">
179
+ {overflow.map((item) => (
180
+ <button
181
+ key={item.notificationId}
182
+ type="button"
183
+ onClick={() => onSelect(item.notificationId)}
184
+ className="glass-card-light flex w-full items-start justify-between rounded-2xl p-3 text-left transition-colors hover:bg-accent/40 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none"
185
+ >
186
+ <div className="min-w-0">
187
+ <p className="text-sm font-medium text-foreground">
188
+ {buildContextLabel(item)}
189
+ </p>
190
+ <p className="mt-1 truncate text-sm text-muted-foreground">
191
+ {item.compactSummary}
192
+ </p>
193
+ </div>
194
+ <span className="ml-3 shrink-0 text-xs text-muted-foreground">
195
+ {formatTimestamp(item.createdAt)}
196
+ </span>
197
+ </button>
198
+ ))}
199
+ </div>
200
+ </div>
201
+ )}
202
+ </div>
203
+ );
204
+ }
205
+
206
+ export function PendingApprovalHost() {
207
+ const [items, setItems] = useState<PendingApprovalPayload[]>([]);
208
+ const [detailOpen, setDetailOpen] = useState(false);
209
+ const [selectedId, setSelectedId] = useState<string | null>(null);
210
+ const [announcement, setAnnouncement] = useState("");
211
+ const triggerRef = useRef<HTMLButtonElement>(null);
212
+ const knownIdsRef = useRef<string[]>([]);
213
+ const isMobile = useIsMobile();
214
+ const router = useRouter();
215
+ const pathname = usePathname();
216
+
217
+ const primary = items[0] ?? null;
218
+ const selected = useMemo(() => {
219
+ if (!items.length) return null;
220
+ return items.find((item) => item.notificationId === selectedId) ?? items[0];
221
+ }, [items, selectedId]);
222
+
223
+ useEffect(() => {
224
+ if (!items.length) {
225
+ setDetailOpen(false);
226
+ setSelectedId(null);
227
+ return;
228
+ }
229
+
230
+ if (!selectedId || !items.some((item) => item.notificationId === selectedId)) {
231
+ setSelectedId(items[0].notificationId);
232
+ }
233
+ }, [items, selectedId]);
234
+
235
+ useEffect(() => {
236
+ let cancelled = false;
237
+ let pollId: ReturnType<typeof setInterval> | null = null;
238
+ let eventSource: EventSource | null = null;
239
+
240
+ function applySnapshot(snapshot: PendingApprovalPayload[]) {
241
+ if (cancelled) return;
242
+
243
+ const nextItems = dedupePendingApprovals(snapshot);
244
+ const previousIds = new Set(knownIdsRef.current);
245
+ const newestNew = nextItems.find(
246
+ (item) => !previousIds.has(item.notificationId)
247
+ );
248
+
249
+ if (newestNew) {
250
+ setAnnouncement(
251
+ `Permission required for ${buildContextLabel(newestNew)}. ${newestNew.compactSummary}`
252
+ );
253
+ }
254
+
255
+ knownIdsRef.current = nextItems.map((item) => item.notificationId);
256
+ setItems(nextItems);
257
+ }
258
+
259
+ async function refresh() {
260
+ try {
261
+ const res = await fetch("/api/notifications/pending-approvals", {
262
+ cache: "no-store",
263
+ });
264
+ if (!res.ok) return;
265
+
266
+ const snapshot = (await res.json()) as PendingApprovalPayload[];
267
+ applySnapshot(snapshot);
268
+ } catch {
269
+ // Fallback refresh should fail quietly.
270
+ }
271
+ }
272
+
273
+ const startPolling = () => {
274
+ if (pollId) return;
275
+ pollId = setInterval(refresh, 15_000);
276
+ };
277
+
278
+ refresh();
279
+
280
+ try {
281
+ eventSource = new EventSource("/api/notifications/pending-approvals/stream");
282
+ eventSource.onmessage = (event) => {
283
+ try {
284
+ const snapshot = JSON.parse(event.data) as PendingApprovalPayload[];
285
+ applySnapshot(snapshot);
286
+ } catch {
287
+ startPolling();
288
+ }
289
+ };
290
+ eventSource.onerror = () => {
291
+ eventSource?.close();
292
+ eventSource = null;
293
+ startPolling();
294
+ };
295
+ } catch {
296
+ startPolling();
297
+ }
298
+
299
+ return () => {
300
+ cancelled = true;
301
+ if (pollId) clearInterval(pollId);
302
+ eventSource?.close();
303
+ };
304
+ }, []);
305
+
306
+ function removeNotification(notificationId: string) {
307
+ setItems((current) =>
308
+ current.filter((item) => item.notificationId !== notificationId)
309
+ );
310
+ }
311
+
312
+ function openDetail(notificationId: string) {
313
+ setSelectedId(notificationId);
314
+ setDetailOpen(true);
315
+ }
316
+
317
+ function handleOpenInbox() {
318
+ setDetailOpen(false);
319
+ if (pathname !== "/inbox") {
320
+ router.push("/inbox");
321
+ }
322
+ }
323
+
324
+ if (!primary) {
325
+ return <div className="sr-only" aria-live="polite">{announcement}</div>;
326
+ }
327
+
328
+ const overflowCount = Math.max(items.length - 1, 0);
329
+ const overflowItems =
330
+ selected == null
331
+ ? []
332
+ : items.filter((item) => item.notificationId !== selected.notificationId);
333
+
334
+ const compactClassName = isMobile
335
+ ? "inset-x-3 bottom-3"
336
+ : "bottom-6 right-6 w-[min(26rem,calc(100vw-2rem))]";
337
+
338
+ return (
339
+ <>
340
+ <div className="sr-only" aria-live="polite">
341
+ {announcement}
342
+ </div>
343
+
344
+ <section
345
+ className={cn(
346
+ "fixed z-50 animate-in fade-in-0 duration-200",
347
+ isMobile ? "slide-in-from-bottom-3" : "slide-in-from-right-3",
348
+ compactClassName
349
+ )}
350
+ aria-label="Pending approval request"
351
+ >
352
+ <div className="glass-card-heavy rounded-[24px] border border-status-warning/30 p-3 sm:p-4">
353
+ <button
354
+ ref={triggerRef}
355
+ type="button"
356
+ onClick={() => openDetail(primary.notificationId)}
357
+ className="w-full rounded-[20px] p-3 text-left transition-colors hover:bg-accent/35 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none"
358
+ >
359
+ <div className="flex items-start gap-3">
360
+ <div className="mt-0.5 flex size-10 shrink-0 items-center justify-center rounded-2xl bg-status-warning/15 text-status-warning">
361
+ <ShieldAlert className="h-5 w-5" />
362
+ </div>
363
+ <div className="min-w-0 flex-1">
364
+ <div className="flex items-center justify-between gap-3">
365
+ <div>
366
+ <p className="text-xs font-semibold uppercase tracking-[0.14em] text-status-warning">
367
+ Permission Required
368
+ </p>
369
+ <p className="mt-1 text-sm font-medium text-foreground">
370
+ {buildContextLabel(primary)}
371
+ </p>
372
+ </div>
373
+ {overflowCount > 0 && (
374
+ <Badge variant="outline" className="shrink-0 text-xs">
375
+ +{overflowCount} more
376
+ </Badge>
377
+ )}
378
+ </div>
379
+ <div className="mt-3 flex flex-wrap items-center gap-2">
380
+ <Badge variant="outline" className="font-mono text-xs">
381
+ {primary.permissionLabel}
382
+ </Badge>
383
+ {primary.workflowName && (
384
+ <Badge variant="secondary" className="text-xs">
385
+ Workflow
386
+ </Badge>
387
+ )}
388
+ </div>
389
+ <p className="mt-3 line-clamp-2 text-sm text-muted-foreground">
390
+ {primary.compactSummary}
391
+ </p>
392
+ </div>
393
+ </div>
394
+ </button>
395
+
396
+ {primary.taskId && primary.toolName && primary.toolInput && (
397
+ <PermissionResponseActions
398
+ taskId={primary.taskId}
399
+ notificationId={primary.notificationId}
400
+ toolName={primary.toolName}
401
+ toolInput={primary.toolInput}
402
+ responded={false}
403
+ response={null}
404
+ onResponded={() => removeNotification(primary.notificationId)}
405
+ className="mt-3"
406
+ />
407
+ )}
408
+
409
+ <div className="mt-3 flex items-center justify-between gap-3">
410
+ <Button variant="ghost" size="sm" onClick={handleOpenInbox}>
411
+ <Inbox className="h-4 w-4" />
412
+ Open Inbox
413
+ </Button>
414
+ <span className="text-xs text-muted-foreground">
415
+ {formatTimestamp(primary.createdAt)}
416
+ </span>
417
+ </div>
418
+ </div>
419
+ </section>
420
+
421
+ {selected &&
422
+ (isMobile ? (
423
+ <Sheet open={detailOpen} onOpenChange={setDetailOpen}>
424
+ <SheetContent
425
+ side="bottom"
426
+ className="max-h-[85dvh] rounded-t-[28px] px-4 pb-6"
427
+ onCloseAutoFocus={(event) => {
428
+ event.preventDefault();
429
+ triggerRef.current?.focus();
430
+ }}
431
+ >
432
+ <SheetHeader className="px-0 pt-6">
433
+ <SheetTitle>Permission required</SheetTitle>
434
+ <SheetDescription>
435
+ Review and resolve this approval request without leaving the
436
+ current route.
437
+ </SheetDescription>
438
+ </SheetHeader>
439
+ <div className="overflow-y-auto px-0 pb-2">
440
+ <PendingApprovalDetail
441
+ selected={selected}
442
+ overflow={overflowItems}
443
+ onResponded={() => removeNotification(selected.notificationId)}
444
+ onOpenInbox={handleOpenInbox}
445
+ onSelect={setSelectedId}
446
+ />
447
+ </div>
448
+ </SheetContent>
449
+ </Sheet>
450
+ ) : (
451
+ <Dialog open={detailOpen} onOpenChange={setDetailOpen}>
452
+ <DialogContent
453
+ className="max-w-2xl"
454
+ onCloseAutoFocus={(event) => {
455
+ event.preventDefault();
456
+ triggerRef.current?.focus();
457
+ }}
458
+ >
459
+ <DialogHeader>
460
+ <DialogTitle>Permission required</DialogTitle>
461
+ <DialogDescription>
462
+ Review and resolve this approval request without switching to
463
+ the Inbox first.
464
+ </DialogDescription>
465
+ </DialogHeader>
466
+ <PendingApprovalDetail
467
+ selected={selected}
468
+ overflow={overflowItems}
469
+ onResponded={() => removeNotification(selected.notificationId)}
470
+ onOpenInbox={handleOpenInbox}
471
+ onSelect={setSelectedId}
472
+ />
473
+ </DialogContent>
474
+ </Dialog>
475
+ ))}
476
+ </>
477
+ );
478
+ }
@@ -0,0 +1,37 @@
1
+ "use client";
2
+
3
+ import { PermissionResponseActions } from "@/components/notifications/permission-response-actions";
4
+ import type { PermissionToolInput } from "@/lib/notifications/permissions";
5
+
6
+ interface PermissionActionProps {
7
+ taskId: string;
8
+ notificationId: string;
9
+ toolName: string;
10
+ toolInput: PermissionToolInput;
11
+ responded: boolean;
12
+ response: string | null;
13
+ onResponded: () => void;
14
+ }
15
+
16
+ export function PermissionAction({
17
+ taskId,
18
+ notificationId,
19
+ toolName,
20
+ toolInput,
21
+ responded,
22
+ response,
23
+ onResponded,
24
+ }: PermissionActionProps) {
25
+ return (
26
+ <PermissionResponseActions
27
+ taskId={taskId}
28
+ notificationId={notificationId}
29
+ toolName={toolName}
30
+ toolInput={toolInput}
31
+ responded={responded}
32
+ response={response}
33
+ onResponded={onResponded}
34
+ className="mt-2"
35
+ />
36
+ );
37
+ }
@@ -0,0 +1,126 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { Check, ShieldCheck, X } from "lucide-react";
5
+ import { toast } from "sonner";
6
+
7
+ import { Button } from "@/components/ui/button";
8
+ import { cn } from "@/lib/utils";
9
+ import {
10
+ buildPermissionPattern,
11
+ getPermissionResponseLabel,
12
+ type PermissionToolInput,
13
+ } from "@/lib/notifications/permissions";
14
+
15
+ interface PermissionResponseActionsProps {
16
+ taskId: string;
17
+ notificationId: string;
18
+ toolName: string;
19
+ toolInput: PermissionToolInput;
20
+ responded: boolean;
21
+ response: string | null;
22
+ onResponded?: () => void;
23
+ className?: string;
24
+ buttonSize?: "sm" | "default";
25
+ layout?: "inline" | "stacked";
26
+ }
27
+
28
+ export function PermissionResponseActions({
29
+ taskId,
30
+ notificationId,
31
+ toolName,
32
+ toolInput,
33
+ responded,
34
+ response,
35
+ onResponded,
36
+ className,
37
+ buttonSize = "sm",
38
+ layout = "inline",
39
+ }: PermissionResponseActionsProps) {
40
+ const [loading, setLoading] = useState(false);
41
+ const responseLabel = responded ? getPermissionResponseLabel(response) : null;
42
+
43
+ if (responseLabel) {
44
+ return <span className="text-xs text-muted-foreground">{responseLabel}</span>;
45
+ }
46
+
47
+ async function handleAction(
48
+ behavior: "allow" | "deny",
49
+ alwaysAllow = false
50
+ ) {
51
+ setLoading(true);
52
+
53
+ try {
54
+ const permissionPattern = alwaysAllow
55
+ ? buildPermissionPattern(toolName, toolInput)
56
+ : undefined;
57
+
58
+ const res = await fetch(`/api/tasks/${taskId}/respond`, {
59
+ method: "POST",
60
+ headers: { "Content-Type": "application/json" },
61
+ body: JSON.stringify({
62
+ notificationId,
63
+ behavior,
64
+ updatedInput: behavior === "allow" ? toolInput : undefined,
65
+ message: behavior === "deny" ? "User denied this action" : undefined,
66
+ alwaysAllow: alwaysAllow || undefined,
67
+ permissionPattern,
68
+ }),
69
+ });
70
+
71
+ if (!res.ok) {
72
+ const data = (await res.json().catch(() => null)) as
73
+ | { error?: string }
74
+ | null;
75
+ throw new Error(data?.error ?? "Failed to respond to permission request");
76
+ }
77
+
78
+ onResponded?.();
79
+ } catch (error) {
80
+ toast.error(
81
+ error instanceof Error
82
+ ? error.message
83
+ : "Failed to respond to permission request"
84
+ );
85
+ } finally {
86
+ setLoading(false);
87
+ }
88
+ }
89
+
90
+ return (
91
+ <div
92
+ className={cn(
93
+ "flex gap-2",
94
+ layout === "inline" ? "flex-wrap items-center" : "flex-col",
95
+ className
96
+ )}
97
+ >
98
+ <Button
99
+ size={buttonSize}
100
+ variant="outline"
101
+ onClick={() => handleAction("allow")}
102
+ disabled={loading}
103
+ >
104
+ <Check className="h-3.5 w-3.5" />
105
+ Allow Once
106
+ </Button>
107
+ <Button
108
+ size={buttonSize}
109
+ onClick={() => handleAction("allow", true)}
110
+ disabled={loading}
111
+ >
112
+ <ShieldCheck className="h-3.5 w-3.5" />
113
+ Always Allow
114
+ </Button>
115
+ <Button
116
+ size={buttonSize}
117
+ variant="outline"
118
+ onClick={() => handleAction("deny")}
119
+ disabled={loading}
120
+ >
121
+ <X className="h-3.5 w-3.5" />
122
+ Deny
123
+ </Button>
124
+ </div>
125
+ );
126
+ }
@@ -0,0 +1,35 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { Badge } from "@/components/ui/badge";
5
+
6
+ export function UnreadBadge() {
7
+ const [count, setCount] = useState(0);
8
+
9
+ useEffect(() => {
10
+ async function fetchCount() {
11
+ try {
12
+ const res = await fetch("/api/notifications?countOnly=true&unread=true");
13
+ if (res.ok) {
14
+ const data = await res.json();
15
+ setCount(data.count ?? 0);
16
+ }
17
+ } catch {
18
+ // Silently fail
19
+ }
20
+ }
21
+
22
+ fetchCount();
23
+ // Aligned with InboxList polling at 10s to reduce duplicate requests
24
+ const interval = setInterval(fetchCount, 10_000);
25
+ return () => clearInterval(interval);
26
+ }, []);
27
+
28
+ if (count === 0) return null;
29
+
30
+ return (
31
+ <Badge variant="destructive" className="ml-auto text-xs h-5 min-w-5 px-1.5" aria-label={`${count} unread notifications`}>
32
+ {count > 99 ? "99+" : count}
33
+ </Badge>
34
+ );
35
+ }