stagent 0.4.0 → 0.6.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 (276) hide show
  1. package/README.md +67 -31
  2. package/dist/cli.js +151 -2
  3. package/docs/.coverage-gaps.json +21 -0
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/agent-intelligence.md +36 -14
  6. package/docs/features/chat.md +53 -71
  7. package/docs/features/cost-usage.md +14 -10
  8. package/docs/features/dashboard-kanban.md +30 -13
  9. package/docs/features/delivery-channels.md +198 -0
  10. package/docs/features/design-system.md +10 -10
  11. package/docs/features/documents.md +8 -8
  12. package/docs/features/home-workspace.md +20 -15
  13. package/docs/features/inbox-notifications.md +22 -10
  14. package/docs/features/keyboard-navigation.md +11 -11
  15. package/docs/features/monitoring.md +1 -1
  16. package/docs/features/playbook.md +30 -32
  17. package/docs/features/profiles.md +33 -11
  18. package/docs/features/projects.md +2 -2
  19. package/docs/features/provider-runtimes.md +58 -14
  20. package/docs/features/schedules.md +77 -41
  21. package/docs/features/settings.md +134 -51
  22. package/docs/features/shared-components.md +7 -15
  23. package/docs/features/tool-permissions.md +9 -9
  24. package/docs/features/workflows.md +32 -21
  25. package/docs/getting-started.md +33 -9
  26. package/docs/index.md +25 -16
  27. package/docs/journeys/developer.md +124 -207
  28. package/docs/journeys/personal-use.md +70 -79
  29. package/docs/journeys/power-user.md +107 -151
  30. package/docs/journeys/work-use.md +81 -113
  31. package/docs/manifest.json +79 -47
  32. package/docs/superpowers/plans/2026-03-30-finish-in-progress-features.md +547 -0
  33. package/docs/superpowers/specs/2026-03-27-chat-screenshot-display-design.md +303 -0
  34. package/docs/use-cases/agency-operator.md +84 -0
  35. package/docs/use-cases/solo-founder.md +75 -0
  36. package/docs/why-stagent.md +59 -0
  37. package/package.json +12 -3
  38. package/src/app/api/channels/[id]/route.ts +103 -0
  39. package/src/app/api/channels/[id]/test/route.ts +52 -0
  40. package/src/app/api/channels/inbound/slack/route.ts +109 -0
  41. package/src/app/api/channels/inbound/telegram/poll/route.ts +128 -0
  42. package/src/app/api/channels/inbound/telegram/route.ts +76 -0
  43. package/src/app/api/channels/route.ts +71 -0
  44. package/src/app/api/chat/conversations/[id]/messages/route.ts +3 -2
  45. package/src/app/api/chat/conversations/route.ts +15 -0
  46. package/src/app/api/chat/entities/search/route.ts +112 -0
  47. package/src/app/api/documents/[id]/file/route.ts +4 -1
  48. package/src/app/api/environment/profiles/suggest/route.ts +19 -3
  49. package/src/app/api/environment/scan/route.ts +8 -1
  50. package/src/app/api/handoffs/[id]/route.ts +76 -0
  51. package/src/app/api/handoffs/route.ts +89 -0
  52. package/src/app/api/memory/route.ts +181 -0
  53. package/src/app/api/profiles/[id]/route.ts +16 -1
  54. package/src/app/api/profiles/[id]/test/route.ts +4 -0
  55. package/src/app/api/profiles/[id]/test-results/route.ts +22 -0
  56. package/src/app/api/profiles/[id]/test-single/route.ts +64 -0
  57. package/src/app/api/profiles/assist/route.ts +35 -0
  58. package/src/app/api/profiles/import-repo/apply-updates/route.ts +123 -0
  59. package/src/app/api/profiles/import-repo/check-updates/route.ts +163 -0
  60. package/src/app/api/profiles/import-repo/confirm/route.ts +118 -0
  61. package/src/app/api/profiles/import-repo/preview/route.ts +107 -0
  62. package/src/app/api/profiles/import-repo/route.ts +29 -0
  63. package/src/app/api/profiles/import-repo/scan/route.ts +25 -0
  64. package/src/app/api/profiles/route.ts +73 -22
  65. package/src/app/api/projects/[id]/route.ts +119 -9
  66. package/src/app/api/projects/__tests__/delete-project.test.ts +170 -0
  67. package/src/app/api/runtimes/ollama/route.ts +86 -0
  68. package/src/app/api/runtimes/suggest/route.ts +29 -0
  69. package/src/app/api/schedules/[id]/heartbeat-history/route.ts +77 -0
  70. package/src/app/api/schedules/[id]/route.ts +41 -3
  71. package/src/app/api/schedules/parse/route.ts +66 -0
  72. package/src/app/api/schedules/route.ts +71 -12
  73. package/src/app/api/settings/author-default/route.ts +7 -0
  74. package/src/app/api/settings/browser-tools/route.ts +68 -0
  75. package/src/app/api/settings/learning/route.ts +41 -0
  76. package/src/app/api/settings/ollama/route.ts +34 -0
  77. package/src/app/api/settings/providers/route.ts +57 -0
  78. package/src/app/api/settings/routing/route.ts +24 -0
  79. package/src/app/api/settings/web-search/route.ts +28 -0
  80. package/src/app/api/tasks/[id]/execute/route.ts +13 -1
  81. package/src/app/documents/page.tsx +3 -0
  82. package/src/app/environment/page.tsx +8 -1
  83. package/src/app/settings/page.tsx +12 -4
  84. package/src/app/workflows/[id]/edit/page.tsx +2 -0
  85. package/src/app/workflows/new/page.tsx +2 -0
  86. package/src/components/chat/chat-command-popover.tsx +280 -0
  87. package/src/components/chat/chat-input.tsx +90 -10
  88. package/src/components/chat/chat-message.tsx +9 -3
  89. package/src/components/chat/chat-model-selector.tsx +42 -1
  90. package/src/components/chat/chat-shell.tsx +31 -5
  91. package/src/components/chat/screenshot-gallery.tsx +96 -0
  92. package/src/components/dashboard/welcome-landing.tsx +9 -9
  93. package/src/components/environment/artifact-card.tsx +27 -1
  94. package/src/components/environment/environment-dashboard.tsx +50 -2
  95. package/src/components/environment/environment-summary-card.tsx +5 -2
  96. package/src/components/environment/suggested-profiles.tsx +117 -52
  97. package/src/components/handoffs/handoff-approval-card.tsx +159 -0
  98. package/src/components/memory/memory-browser.tsx +315 -0
  99. package/src/components/monitoring/log-entry.tsx +61 -27
  100. package/src/components/profiles/learned-context-panel.tsx +4 -4
  101. package/src/components/profiles/profile-assist-panel.tsx +512 -0
  102. package/src/components/profiles/profile-browser.tsx +109 -8
  103. package/src/components/profiles/profile-card.tsx +29 -1
  104. package/src/components/profiles/profile-detail-view.tsx +200 -28
  105. package/src/components/profiles/profile-form-view.tsx +220 -82
  106. package/src/components/profiles/repo-import-wizard.tsx +648 -0
  107. package/src/components/profiles/smoke-test-editor.tsx +106 -0
  108. package/src/components/projects/project-detail.tsx +15 -2
  109. package/src/components/schedules/schedule-create-sheet.tsx +32 -330
  110. package/src/components/schedules/schedule-detail-sheet.tsx +37 -21
  111. package/src/components/schedules/schedule-edit-sheet.tsx +159 -0
  112. package/src/components/schedules/schedule-form.tsx +749 -0
  113. package/src/components/schedules/schedule-list.tsx +31 -2
  114. package/src/components/settings/auth-method-selector.tsx +7 -1
  115. package/src/components/settings/browser-tools-section.tsx +247 -0
  116. package/src/components/settings/budget-guardrails-section.tsx +111 -48
  117. package/src/components/settings/channels-section.tsx +526 -0
  118. package/src/components/settings/chat-settings-section.tsx +27 -1
  119. package/src/components/settings/data-management-section.tsx +8 -6
  120. package/src/components/settings/learning-context-section.tsx +124 -0
  121. package/src/components/settings/ollama-section.tsx +270 -0
  122. package/src/components/settings/providers-runtimes-section.tsx +499 -0
  123. package/src/components/settings/runtime-timeout-section.tsx +4 -4
  124. package/src/components/settings/web-search-section.tsx +101 -0
  125. package/src/components/shared/command-palette.tsx +1 -30
  126. package/src/components/shared/screenshot-lightbox.tsx +151 -0
  127. package/src/components/shared/tag-input.tsx +156 -0
  128. package/src/components/tasks/kanban-board.tsx +32 -0
  129. package/src/components/tasks/kanban-column.tsx +4 -2
  130. package/src/components/tasks/task-card.tsx +1 -0
  131. package/src/components/tasks/task-chip-bar.tsx +6 -1
  132. package/src/components/tasks/task-create-panel.tsx +55 -5
  133. package/src/components/workflows/workflow-form-view.tsx +38 -3
  134. package/src/hooks/use-caret-position.ts +104 -0
  135. package/src/hooks/use-chat-autocomplete.ts +288 -0
  136. package/src/hooks/use-project-skills.ts +66 -0
  137. package/src/hooks/use-tag-suggestions.ts +31 -0
  138. package/src/instrumentation.ts +4 -1
  139. package/src/lib/agents/__tests__/browser-mcp.test.ts +175 -0
  140. package/src/lib/agents/__tests__/claude-agent.test.ts +6 -0
  141. package/src/lib/agents/__tests__/learned-context.test.ts +10 -0
  142. package/src/lib/agents/agentic-loop.ts +235 -0
  143. package/src/lib/agents/browser-mcp.ts +174 -0
  144. package/src/lib/agents/claude-agent.ts +83 -198
  145. package/src/lib/agents/handoff/bus.ts +164 -0
  146. package/src/lib/agents/handoff/governance.ts +47 -0
  147. package/src/lib/agents/handoff/types.ts +16 -0
  148. package/src/lib/agents/learned-context.ts +27 -7
  149. package/src/lib/agents/memory/decay.ts +61 -0
  150. package/src/lib/agents/memory/extractor.ts +181 -0
  151. package/src/lib/agents/memory/retrieval.ts +96 -0
  152. package/src/lib/agents/memory/types.ts +6 -0
  153. package/src/lib/agents/profiles/__tests__/project-profiles.test.ts +119 -0
  154. package/src/lib/agents/profiles/__tests__/registry.test.ts +11 -3
  155. package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +2 -2
  156. package/src/lib/agents/profiles/builtins/content-creator/SKILL.md +19 -0
  157. package/src/lib/agents/profiles/builtins/content-creator/profile.yaml +27 -0
  158. package/src/lib/agents/profiles/builtins/customer-support-agent/SKILL.md +19 -0
  159. package/src/lib/agents/profiles/builtins/customer-support-agent/profile.yaml +26 -0
  160. package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +2 -2
  161. package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +2 -2
  162. package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +2 -2
  163. package/src/lib/agents/profiles/builtins/financial-analyst/SKILL.md +19 -0
  164. package/src/lib/agents/profiles/builtins/financial-analyst/profile.yaml +24 -0
  165. package/src/lib/agents/profiles/builtins/general/profile.yaml +2 -2
  166. package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +2 -2
  167. package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +2 -2
  168. package/src/lib/agents/profiles/builtins/marketing-strategist/SKILL.md +19 -0
  169. package/src/lib/agents/profiles/builtins/marketing-strategist/profile.yaml +27 -0
  170. package/src/lib/agents/profiles/builtins/operations-coordinator/SKILL.md +19 -0
  171. package/src/lib/agents/profiles/builtins/operations-coordinator/profile.yaml +26 -0
  172. package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +2 -2
  173. package/src/lib/agents/profiles/builtins/researcher/SKILL.md +1 -0
  174. package/src/lib/agents/profiles/builtins/researcher/profile.yaml +2 -2
  175. package/src/lib/agents/profiles/builtins/sales-researcher/SKILL.md +19 -0
  176. package/src/lib/agents/profiles/builtins/sales-researcher/profile.yaml +26 -0
  177. package/src/lib/agents/profiles/builtins/shopping-assistant/SKILL.md +1 -0
  178. package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +2 -2
  179. package/src/lib/agents/profiles/builtins/sweep/profile.yaml +1 -1
  180. package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +2 -2
  181. package/src/lib/agents/profiles/builtins/travel-planner/SKILL.md +2 -0
  182. package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +2 -2
  183. package/src/lib/agents/profiles/builtins/wealth-manager/SKILL.md +2 -0
  184. package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +2 -2
  185. package/src/lib/agents/profiles/project-profiles.ts +193 -0
  186. package/src/lib/agents/profiles/registry.ts +130 -6
  187. package/src/lib/agents/profiles/types.ts +28 -0
  188. package/src/lib/agents/router.ts +174 -2
  189. package/src/lib/agents/runtime/__tests__/catalog.test.ts +15 -4
  190. package/src/lib/agents/runtime/anthropic-direct.ts +644 -0
  191. package/src/lib/agents/runtime/catalog.ts +57 -2
  192. package/src/lib/agents/runtime/claude.ts +205 -1
  193. package/src/lib/agents/runtime/index.ts +22 -0
  194. package/src/lib/agents/runtime/ollama-adapter.ts +409 -0
  195. package/src/lib/agents/runtime/openai-direct.ts +514 -0
  196. package/src/lib/agents/runtime/profile-assist-types.ts +30 -0
  197. package/src/lib/agents/runtime/types.ts +2 -0
  198. package/src/lib/agents/tool-permissions.ts +203 -0
  199. package/src/lib/channels/gateway.ts +321 -0
  200. package/src/lib/channels/poller.ts +268 -0
  201. package/src/lib/channels/registry.ts +90 -0
  202. package/src/lib/channels/slack-adapter.ts +188 -0
  203. package/src/lib/channels/telegram-adapter.ts +218 -0
  204. package/src/lib/channels/types.ts +43 -0
  205. package/src/lib/channels/webhook-adapter.ts +74 -0
  206. package/src/lib/chat/command-data.ts +50 -0
  207. package/src/lib/chat/context-builder.ts +147 -3
  208. package/src/lib/chat/engine.ts +182 -19
  209. package/src/lib/chat/ollama-engine.ts +198 -0
  210. package/src/lib/chat/slash-commands.ts +191 -0
  211. package/src/lib/chat/stagent-tools.ts +106 -20
  212. package/src/lib/chat/tool-catalog.ts +209 -0
  213. package/src/lib/chat/tool-registry.ts +90 -0
  214. package/src/lib/chat/tools/chat-history-tools.ts +4 -4
  215. package/src/lib/chat/tools/document-tools.ts +43 -6
  216. package/src/lib/chat/tools/handoff-tools.ts +70 -0
  217. package/src/lib/chat/tools/notification-tools.ts +4 -4
  218. package/src/lib/chat/tools/profile-tools.ts +3 -3
  219. package/src/lib/chat/tools/project-tools.ts +3 -3
  220. package/src/lib/chat/tools/schedule-tools.ts +29 -13
  221. package/src/lib/chat/tools/settings-tools.ts +2 -2
  222. package/src/lib/chat/tools/task-tools.ts +66 -11
  223. package/src/lib/chat/tools/usage-tools.ts +2 -2
  224. package/src/lib/chat/tools/workflow-tools.ts +8 -8
  225. package/src/lib/chat/types.ts +22 -6
  226. package/src/lib/constants/known-tools.ts +19 -0
  227. package/src/lib/constants/prose-styles.ts +1 -1
  228. package/src/lib/constants/settings.ts +11 -0
  229. package/src/lib/data/channel-bindings.ts +85 -0
  230. package/src/lib/data/clear.ts +38 -4
  231. package/src/lib/data/profile-test-results.ts +48 -0
  232. package/src/lib/data/seed-data/conversations.ts +196 -0
  233. package/src/lib/data/seed-data/learned-context.ts +99 -0
  234. package/src/lib/data/seed-data/notifications.ts +54 -1
  235. package/src/lib/data/seed-data/profile-test-results.ts +96 -0
  236. package/src/lib/data/seed-data/repo-imports.ts +51 -0
  237. package/src/lib/data/seed-data/views.ts +60 -0
  238. package/src/lib/data/seed.ts +51 -0
  239. package/src/lib/db/bootstrap.ts +167 -0
  240. package/src/lib/db/migrations/0012_add_screenshot_columns.sql +5 -0
  241. package/src/lib/db/migrations/0013_add_repo_imports.sql +15 -0
  242. package/src/lib/db/migrations/0014_add_linked_profile_id.sql +3 -0
  243. package/src/lib/db/migrations/0015_add_channel_bindings.sql +23 -0
  244. package/src/lib/db/schema.ts +192 -1
  245. package/src/lib/environment/__tests__/auto-scan.test.ts +86 -0
  246. package/src/lib/environment/__tests__/profile-linker.test.ts +187 -0
  247. package/src/lib/environment/auto-scan.ts +48 -0
  248. package/src/lib/environment/data.ts +25 -0
  249. package/src/lib/environment/profile-generator.ts +40 -10
  250. package/src/lib/environment/profile-linker.ts +143 -0
  251. package/src/lib/environment/profile-rules.ts +96 -0
  252. package/src/lib/import/dedup.ts +149 -0
  253. package/src/lib/import/format-adapter.ts +631 -0
  254. package/src/lib/import/github-api.ts +219 -0
  255. package/src/lib/import/repo-scanner.ts +251 -0
  256. package/src/lib/schedules/__tests__/nlp-parser.test.ts +330 -0
  257. package/src/lib/schedules/active-hours.ts +120 -0
  258. package/src/lib/schedules/heartbeat-parser.ts +224 -0
  259. package/src/lib/schedules/heartbeat-prompt.ts +153 -0
  260. package/src/lib/schedules/nlp-parser.ts +357 -0
  261. package/src/lib/schedules/scheduler.ts +218 -3
  262. package/src/lib/screenshots/__tests__/persist.test.ts +104 -0
  263. package/src/lib/screenshots/persist.ts +114 -0
  264. package/src/lib/settings/__tests__/budget-guardrails.test.ts +39 -1
  265. package/src/lib/settings/helpers.ts +6 -0
  266. package/src/lib/settings/routing.ts +24 -0
  267. package/src/lib/settings/runtime-setup.ts +28 -1
  268. package/src/lib/usage/ledger.ts +2 -1
  269. package/src/lib/utils/stagent-paths.ts +4 -0
  270. package/src/lib/validators/__tests__/settings.test.ts +9 -0
  271. package/src/lib/validators/profile.ts +39 -0
  272. package/src/lib/workflows/blueprints/builtins/business-daily-briefing.yaml +102 -0
  273. package/src/lib/workflows/blueprints/builtins/content-marketing-pipeline.yaml +90 -0
  274. package/src/lib/workflows/blueprints/builtins/customer-support-triage.yaml +107 -0
  275. package/src/lib/workflows/blueprints/builtins/financial-reporting.yaml +104 -0
  276. package/src/lib/workflows/blueprints/builtins/lead-research-pipeline.yaml +82 -0
@@ -6,11 +6,12 @@ import { Skeleton } from "@/components/ui/skeleton";
6
6
  import { Button } from "@/components/ui/button";
7
7
  import { ScheduleCreateSheet } from "./schedule-create-sheet";
8
8
  import { ScheduleDetailSheet } from "./schedule-detail-sheet";
9
+ import { ScheduleEditSheet } from "./schedule-edit-sheet";
9
10
  import { ScheduleStatusBadge } from "./schedule-status-badge";
10
11
  import { ConfirmDialog } from "@/components/shared/confirm-dialog";
11
12
  import { EmptyState } from "@/components/shared/empty-state";
12
13
  import { describeCron } from "@/lib/schedules/interval-parser";
13
- import { Clock, Pause, Play, Trash2 } from "lucide-react";
14
+ import { Clock, Heart, Pause, Play, Trash2 } from "lucide-react";
14
15
  import { toast } from "sonner";
15
16
 
16
17
  interface Schedule {
@@ -27,6 +28,8 @@ interface Schedule {
27
28
  lastFiredAt: string | null;
28
29
  nextFireAt: string | null;
29
30
  createdAt: string;
31
+ type: "scheduled" | "heartbeat";
32
+ suppressionCount: number;
30
33
  }
31
34
 
32
35
  interface ScheduleListProps {
@@ -42,6 +45,7 @@ export function ScheduleList({ projects, initialSelectedId }: ScheduleListProps)
42
45
  initialSelectedId ?? null
43
46
  );
44
47
  const [createOpen, setCreateOpen] = useState(false);
48
+ const [editingScheduleId, setEditingScheduleId] = useState<string | null>(null);
45
49
 
46
50
  const refresh = useCallback(async () => {
47
51
  const res = await fetch("/api/schedules");
@@ -153,7 +157,10 @@ export function ScheduleList({ projects, initialSelectedId }: ScheduleListProps)
153
157
  >
154
158
  <CardHeader className="pb-2">
155
159
  <div className="flex items-center justify-between gap-2 min-w-0">
156
- <CardTitle className="min-w-0 truncate text-base font-medium">
160
+ <CardTitle className="min-w-0 truncate text-base font-medium flex items-center gap-1.5">
161
+ {sched.type === "heartbeat" && (
162
+ <Heart className="h-3.5 w-3.5 text-rose-500 shrink-0" />
163
+ )}
157
164
  {sched.name}
158
165
  </CardTitle>
159
166
  <ScheduleStatusBadge status={sched.status} />
@@ -167,6 +174,14 @@ export function ScheduleList({ projects, initialSelectedId }: ScheduleListProps)
167
174
  {sched.firingCount} firing
168
175
  {sched.firingCount !== 1 ? "s" : ""}
169
176
  </span>
177
+ {sched.type === "heartbeat" && sched.suppressionCount > 0 && (
178
+ <>
179
+ <span>·</span>
180
+ <span className="text-emerald-600">
181
+ {sched.suppressionCount} suppressed
182
+ </span>
183
+ </>
184
+ )}
170
185
  {!sched.recurs && (
171
186
  <>
172
187
  <span>·</span>
@@ -247,6 +262,20 @@ export function ScheduleList({ projects, initialSelectedId }: ScheduleListProps)
247
262
  }}
248
263
  onDeleted={refresh}
249
264
  onUpdated={refresh}
265
+ onEdit={(id) => {
266
+ setSelectedScheduleId(null);
267
+ setEditingScheduleId(id);
268
+ }}
269
+ />
270
+
271
+ <ScheduleEditSheet
272
+ scheduleId={editingScheduleId}
273
+ projects={projects}
274
+ open={editingScheduleId !== null}
275
+ onOpenChange={(open) => {
276
+ if (!open) setEditingScheduleId(null);
277
+ }}
278
+ onUpdated={refresh}
250
279
  />
251
280
 
252
281
  <ConfirmDialog
@@ -7,6 +7,7 @@ import type { AuthMethod } from "@/lib/constants/settings";
7
7
  interface AuthMethodSelectorProps {
8
8
  value: AuthMethod;
9
9
  onChange: (method: AuthMethod) => void;
10
+ recommendedMethod?: AuthMethod | null;
10
11
  }
11
12
 
12
13
  const methods = [
@@ -24,7 +25,7 @@ const methods = [
24
25
  },
25
26
  ];
26
27
 
27
- export function AuthMethodSelector({ value, onChange }: AuthMethodSelectorProps) {
28
+ export function AuthMethodSelector({ value, onChange, recommendedMethod }: AuthMethodSelectorProps) {
28
29
  return (
29
30
  <div className="space-y-2">
30
31
  <p className="text-sm font-medium">Authentication Method</p>
@@ -58,6 +59,11 @@ export function AuthMethodSelector({ value, onChange }: AuthMethodSelectorProps)
58
59
  <span className="text-xs text-muted-foreground">
59
60
  {method.description}
60
61
  </span>
62
+ {recommendedMethod === method.id && !isSelected && (
63
+ <span className="text-[10px] font-medium uppercase tracking-wider text-primary/70 mt-0.5">
64
+ Recommended
65
+ </span>
66
+ )}
61
67
  </button>
62
68
  );
63
69
  })}
@@ -0,0 +1,247 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState, useCallback } from "react";
4
+ import { Globe, Chrome, Theater, ChevronDown } from "lucide-react";
5
+ import { toast } from "sonner";
6
+ import {
7
+ Card,
8
+ CardContent,
9
+ CardDescription,
10
+ CardHeader,
11
+ CardTitle,
12
+ } from "@/components/ui/card";
13
+ import { Switch } from "@/components/ui/switch";
14
+ import { Input } from "@/components/ui/input";
15
+ import { Label } from "@/components/ui/label";
16
+ import { FormSectionCard } from "@/components/shared/form-section-card";
17
+
18
+ interface BrowserToolsState {
19
+ chromeDevtoolsEnabled: boolean;
20
+ playwrightEnabled: boolean;
21
+ chromeDevtoolsConfig: string;
22
+ playwrightConfig: string;
23
+ }
24
+
25
+ const DEFAULT_STATE: BrowserToolsState = {
26
+ chromeDevtoolsEnabled: false,
27
+ playwrightEnabled: false,
28
+ chromeDevtoolsConfig: "",
29
+ playwrightConfig: "",
30
+ };
31
+
32
+ export function BrowserToolsSection() {
33
+ const [state, setState] = useState<BrowserToolsState>(DEFAULT_STATE);
34
+ const [saving, setSaving] = useState(false);
35
+ const [chromeExpanded, setChromeExpanded] = useState(false);
36
+ const [playwrightExpanded, setPlaywrightExpanded] = useState(false);
37
+
38
+ const fetchSettings = useCallback(async () => {
39
+ try {
40
+ const res = await fetch("/api/settings/browser-tools");
41
+ if (res.ok) {
42
+ const data = await res.json();
43
+ setState(data);
44
+ }
45
+ } catch {
46
+ // Use defaults
47
+ }
48
+ }, []);
49
+
50
+ useEffect(() => {
51
+ fetchSettings();
52
+ }, [fetchSettings]);
53
+
54
+ const handleToggle = async (
55
+ field: "chromeDevtoolsEnabled" | "playwrightEnabled",
56
+ value: boolean
57
+ ) => {
58
+ setState((prev) => ({ ...prev, [field]: value }));
59
+ setSaving(true);
60
+ try {
61
+ const res = await fetch("/api/settings/browser-tools", {
62
+ method: "POST",
63
+ headers: { "Content-Type": "application/json" },
64
+ body: JSON.stringify({ [field]: value }),
65
+ });
66
+ if (res.ok) {
67
+ const data = await res.json();
68
+ setState(data);
69
+ const label =
70
+ field === "chromeDevtoolsEnabled"
71
+ ? "Chrome DevTools MCP"
72
+ : "Playwright MCP";
73
+ toast.success(`${label} ${value ? "enabled" : "disabled"}`);
74
+ }
75
+ } catch {
76
+ toast.error("Failed to save setting");
77
+ setState((prev) => ({ ...prev, [field]: !value }));
78
+ } finally {
79
+ setSaving(false);
80
+ }
81
+ };
82
+
83
+ const handleConfigSave = async (
84
+ field: "chromeDevtoolsConfig" | "playwrightConfig",
85
+ value: string
86
+ ) => {
87
+ setSaving(true);
88
+ try {
89
+ await fetch("/api/settings/browser-tools", {
90
+ method: "POST",
91
+ headers: { "Content-Type": "application/json" },
92
+ body: JSON.stringify({ [field]: value }),
93
+ });
94
+ toast.success("Configuration saved");
95
+ } catch {
96
+ toast.error("Failed to save configuration");
97
+ } finally {
98
+ setSaving(false);
99
+ }
100
+ };
101
+
102
+ return (
103
+ <Card>
104
+ <CardHeader>
105
+ <CardTitle className="flex items-center gap-2">
106
+ <Globe className="h-5 w-5" />
107
+ Browser Tools
108
+ </CardTitle>
109
+ <CardDescription>
110
+ Enable browser automation MCP servers for chat and task agents.
111
+ Read-only tools (screenshots, snapshots) are auto-approved. Mutation
112
+ tools (click, navigate, type) require permission.
113
+ </CardDescription>
114
+ </CardHeader>
115
+ <CardContent className="space-y-4">
116
+ {/* Chrome DevTools MCP */}
117
+ <FormSectionCard
118
+ icon={Chrome}
119
+ title="Chrome DevTools MCP"
120
+ hint="Connect to a running Chrome instance via CDP. Best for debugging live apps, performance profiling, and network inspection."
121
+ >
122
+ <div className="space-y-3">
123
+ <div className="flex items-center justify-between">
124
+ <Label htmlFor="chrome-devtools-toggle" className="text-sm">
125
+ {state.chromeDevtoolsEnabled ? "Enabled" : "Disabled"}
126
+ </Label>
127
+ <Switch
128
+ id="chrome-devtools-toggle"
129
+ checked={state.chromeDevtoolsEnabled}
130
+ disabled={saving}
131
+ onCheckedChange={(v) =>
132
+ handleToggle("chromeDevtoolsEnabled", v)
133
+ }
134
+ />
135
+ </div>
136
+
137
+ {state.chromeDevtoolsEnabled && (
138
+ <>
139
+ <button
140
+ type="button"
141
+ onClick={() => setChromeExpanded((e) => !e)}
142
+ className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
143
+ >
144
+ <ChevronDown
145
+ className={`h-3 w-3 transition-transform ${chromeExpanded ? "rotate-0" : "-rotate-90"}`}
146
+ />
147
+ Advanced configuration
148
+ </button>
149
+ {chromeExpanded && (
150
+ <div className="space-y-2">
151
+ <Label
152
+ htmlFor="chrome-config"
153
+ className="text-xs text-muted-foreground"
154
+ >
155
+ Extra CLI arguments (e.g.{" "}
156
+ <code className="text-[11px]">
157
+ --headless --browser-url http://localhost:9222
158
+ </code>
159
+ )
160
+ </Label>
161
+ <Input
162
+ id="chrome-config"
163
+ placeholder="--headless"
164
+ value={state.chromeDevtoolsConfig}
165
+ disabled={saving}
166
+ onChange={(e) =>
167
+ setState((prev) => ({
168
+ ...prev,
169
+ chromeDevtoolsConfig: e.target.value,
170
+ }))
171
+ }
172
+ onBlur={(e) =>
173
+ handleConfigSave("chromeDevtoolsConfig", e.target.value)
174
+ }
175
+ />
176
+ </div>
177
+ )}
178
+ </>
179
+ )}
180
+ </div>
181
+ </FormSectionCard>
182
+
183
+ {/* Playwright MCP */}
184
+ <FormSectionCard
185
+ icon={Theater}
186
+ title="Playwright MCP"
187
+ hint="Launch a headless browser for autonomous tasks. Best for research, scraping, testing, and structured page analysis."
188
+ >
189
+ <div className="space-y-3">
190
+ <div className="flex items-center justify-between">
191
+ <Label htmlFor="playwright-toggle" className="text-sm">
192
+ {state.playwrightEnabled ? "Enabled" : "Disabled"}
193
+ </Label>
194
+ <Switch
195
+ id="playwright-toggle"
196
+ checked={state.playwrightEnabled}
197
+ disabled={saving}
198
+ onCheckedChange={(v) => handleToggle("playwrightEnabled", v)}
199
+ />
200
+ </div>
201
+
202
+ {state.playwrightEnabled && (
203
+ <>
204
+ <button
205
+ type="button"
206
+ onClick={() => setPlaywrightExpanded((e) => !e)}
207
+ className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
208
+ >
209
+ <ChevronDown
210
+ className={`h-3 w-3 transition-transform ${playwrightExpanded ? "rotate-0" : "-rotate-90"}`}
211
+ />
212
+ Advanced configuration
213
+ </button>
214
+ {playwrightExpanded && (
215
+ <div className="space-y-2">
216
+ <Label
217
+ htmlFor="playwright-config"
218
+ className="text-xs text-muted-foreground"
219
+ >
220
+ Extra CLI arguments (e.g.{" "}
221
+ <code className="text-[11px]">--browser firefox</code>)
222
+ </Label>
223
+ <Input
224
+ id="playwright-config"
225
+ placeholder="--browser chromium"
226
+ value={state.playwrightConfig}
227
+ disabled={saving}
228
+ onChange={(e) =>
229
+ setState((prev) => ({
230
+ ...prev,
231
+ playwrightConfig: e.target.value,
232
+ }))
233
+ }
234
+ onBlur={(e) =>
235
+ handleConfigSave("playwrightConfig", e.target.value)
236
+ }
237
+ />
238
+ </div>
239
+ )}
240
+ </>
241
+ )}
242
+ </div>
243
+ </FormSectionCard>
244
+ </CardContent>
245
+ </Card>
246
+ );
247
+ }
@@ -56,6 +56,19 @@ function buildFormState(policy: BudgetPolicy): BudgetFormState {
56
56
  policy.runtimes["openai-codex-app-server"].monthlySpendCapUsd
57
57
  ),
58
58
  },
59
+ "anthropic-direct": {
60
+ monthlySpendCapUsd: toInputValue(
61
+ policy.runtimes["anthropic-direct"].monthlySpendCapUsd
62
+ ),
63
+ },
64
+ "openai-direct": {
65
+ monthlySpendCapUsd: toInputValue(
66
+ policy.runtimes["openai-direct"].monthlySpendCapUsd
67
+ ),
68
+ },
69
+ ollama: {
70
+ monthlySpendCapUsd: "", // Ollama is always $0
71
+ },
59
72
  },
60
73
  };
61
74
  }
@@ -77,6 +90,19 @@ function buildPayload(form: BudgetFormState): BudgetPolicy {
77
90
  form.runtimes["openai-codex-app-server"].monthlySpendCapUsd
78
91
  ),
79
92
  },
93
+ "anthropic-direct": {
94
+ monthlySpendCapUsd: toNullableNumber(
95
+ form.runtimes["anthropic-direct"].monthlySpendCapUsd
96
+ ),
97
+ },
98
+ "openai-direct": {
99
+ monthlySpendCapUsd: toNullableNumber(
100
+ form.runtimes["openai-direct"].monthlySpendCapUsd
101
+ ),
102
+ },
103
+ ollama: {
104
+ monthlySpendCapUsd: null, // Ollama is always $0 — no budget needed
105
+ },
80
106
  },
81
107
  };
82
108
  }
@@ -162,7 +188,7 @@ function applyBudgetSplit(
162
188
  current: BudgetFormState,
163
189
  overallMonthlySpendCapUsd: string,
164
190
  activeRuntimeIds: AgentRuntimeId[],
165
- claudePercent = deriveClaudeAllocation(current)
191
+ anthropicPercent = deriveClaudeAllocation(current)
166
192
  ): BudgetFormState {
167
193
  const overall = toNullableNumber(overallMonthlySpendCapUsd);
168
194
  const next: BudgetFormState = {
@@ -173,31 +199,56 @@ function applyBudgetSplit(
173
199
  "openai-codex-app-server": {
174
200
  ...current.runtimes["openai-codex-app-server"],
175
201
  },
202
+ "anthropic-direct": { ...current.runtimes["anthropic-direct"] },
203
+ "openai-direct": { ...current.runtimes["openai-direct"] },
176
204
  },
177
205
  };
178
206
 
179
207
  if (overall == null || activeRuntimeIds.length === 0) {
180
208
  next.runtimes["claude-code"].monthlySpendCapUsd = "";
181
209
  next.runtimes["openai-codex-app-server"].monthlySpendCapUsd = "";
210
+ next.runtimes["anthropic-direct"].monthlySpendCapUsd = "";
211
+ next.runtimes["openai-direct"].monthlySpendCapUsd = "";
182
212
  return next;
183
213
  }
184
214
 
185
- if (activeRuntimeIds.length === 1) {
186
- const runtimeId = activeRuntimeIds[0];
187
- next.runtimes[runtimeId].monthlySpendCapUsd = String(overall);
188
- for (const candidate of ["claude-code", "openai-codex-app-server"] as const) {
189
- if (candidate !== runtimeId) {
190
- next.runtimes[candidate].monthlySpendCapUsd = "";
191
- }
192
- }
215
+ // Determine which providers have active runtimes
216
+ const hasAnthropic = activeRuntimeIds.some(
217
+ (id) => id === "claude-code" || id === "anthropic-direct"
218
+ );
219
+ const hasOpenAI = activeRuntimeIds.some(
220
+ (id) => id === "openai-codex-app-server" || id === "openai-direct"
221
+ );
222
+
223
+ if (hasAnthropic && !hasOpenAI) {
224
+ // Single provider: Anthropic gets 100%
225
+ const cap = String(overall);
226
+ next.runtimes["claude-code"].monthlySpendCapUsd = cap;
227
+ next.runtimes["anthropic-direct"].monthlySpendCapUsd = cap;
228
+ next.runtimes["openai-codex-app-server"].monthlySpendCapUsd = "";
229
+ next.runtimes["openai-direct"].monthlySpendCapUsd = "";
193
230
  return next;
194
231
  }
195
232
 
196
- const claudeCap = roundUsd(overall * (claudePercent / 100));
197
- const openAICap = roundUsd(Math.max(overall - claudeCap, 0));
233
+ if (hasOpenAI && !hasAnthropic) {
234
+ // Single provider: OpenAI gets 100%
235
+ const cap = String(overall);
236
+ next.runtimes["openai-codex-app-server"].monthlySpendCapUsd = cap;
237
+ next.runtimes["openai-direct"].monthlySpendCapUsd = cap;
238
+ next.runtimes["claude-code"].monthlySpendCapUsd = "";
239
+ next.runtimes["anthropic-direct"].monthlySpendCapUsd = "";
240
+ return next;
241
+ }
198
242
 
199
- next.runtimes["claude-code"].monthlySpendCapUsd = String(claudeCap);
243
+ // Both providers: split by anthropicPercent
244
+ const anthropicCap = roundUsd(overall * (anthropicPercent / 100));
245
+ const openAICap = roundUsd(Math.max(overall - anthropicCap, 0));
246
+
247
+ // Both runtimes under a provider share the provider's allocation
248
+ next.runtimes["claude-code"].monthlySpendCapUsd = String(anthropicCap);
249
+ next.runtimes["anthropic-direct"].monthlySpendCapUsd = String(anthropicCap);
200
250
  next.runtimes["openai-codex-app-server"].monthlySpendCapUsd = String(openAICap);
251
+ next.runtimes["openai-direct"].monthlySpendCapUsd = String(openAICap);
201
252
  return next;
202
253
  }
203
254
 
@@ -302,9 +353,16 @@ export function BudgetGuardrailsSection() {
302
353
  const overallMonthly = getStatus(snapshot.statuses, "overall", "monthly");
303
354
  const blocked = snapshot.statuses.filter((status) => status.health === "blocked");
304
355
  const warnings = snapshot.statuses.filter((status) => status.health === "warning");
305
- const claudeAllocation = deriveClaudeAllocation(form);
356
+ const anthropicAllocation = deriveClaudeAllocation(form);
306
357
  const claudeRuntime = snapshot.runtimeStates["claude-code"];
307
- const showSplitSlider = activeRuntimes.length === 2;
358
+ // Show split slider when both providers have active runtimes
359
+ const hasAnthropicRuntimes = activeRuntimes.some(
360
+ (r) => r.providerId === "anthropic"
361
+ );
362
+ const hasOpenAIRuntimes = activeRuntimes.some(
363
+ (r) => r.providerId === "openai"
364
+ );
365
+ const showSplitSlider = hasAnthropicRuntimes && hasOpenAIRuntimes;
308
366
 
309
367
  return (
310
368
  <Card className="surface-card">
@@ -402,11 +460,11 @@ export function BudgetGuardrailsSection() {
402
460
 
403
461
  <div className="space-y-3">
404
462
  <div className="flex items-center justify-between gap-3 text-sm">
405
- <span className="font-medium">Claude</span>
406
- <span className="text-muted-foreground">{claudeAllocation}%</span>
463
+ <span className="font-medium">Anthropic</span>
464
+ <span className="text-muted-foreground">{anthropicAllocation}%</span>
407
465
  </div>
408
466
  <Slider
409
- value={[claudeAllocation]}
467
+ value={[anthropicAllocation]}
410
468
  min={0}
411
469
  max={100}
412
470
  step={1}
@@ -424,42 +482,47 @@ export function BudgetGuardrailsSection() {
424
482
  }
425
483
  />
426
484
  <div className="grid gap-3 md:grid-cols-2">
427
- {activeRuntimes.map((runtime) => (
428
- <div key={runtime.runtimeId} className="rounded-xl border border-border/60 bg-background/40 p-3">
429
- <p className="text-sm font-medium">{runtime.label}</p>
430
- <p className="mt-1 text-lg font-semibold">
431
- {formatCurrencyUsd(
432
- toNullableNumber(form.runtimes[runtime.runtimeId].monthlySpendCapUsd)
433
- )}
434
- </p>
435
- <p className="text-xs text-muted-foreground">
436
- {runtime.runtimeId === "claude-code"
437
- ? `${claudeAllocation}% of the monthly cap`
438
- : `${100 - claudeAllocation}% of the monthly cap`}
439
- </p>
440
- </div>
441
- ))}
485
+ <div className="rounded-xl border border-border/60 bg-background/40 p-3">
486
+ <p className="text-sm font-medium">Anthropic</p>
487
+ <p className="mt-1 text-lg font-semibold">
488
+ {formatCurrencyUsd(
489
+ toNullableNumber(form.runtimes["claude-code"].monthlySpendCapUsd)
490
+ )}
491
+ </p>
492
+ <p className="text-xs text-muted-foreground">
493
+ {anthropicAllocation}% shared by Claude Code &amp; Anthropic Direct
494
+ </p>
495
+ </div>
496
+ <div className="rounded-xl border border-border/60 bg-background/40 p-3">
497
+ <p className="text-sm font-medium">OpenAI</p>
498
+ <p className="mt-1 text-lg font-semibold">
499
+ {formatCurrencyUsd(
500
+ toNullableNumber(form.runtimes["openai-codex-app-server"].monthlySpendCapUsd)
501
+ )}
502
+ </p>
503
+ <p className="text-xs text-muted-foreground">
504
+ {100 - anthropicAllocation}% — shared by Codex &amp; OpenAI Direct
505
+ </p>
506
+ </div>
442
507
  </div>
443
508
  </div>
444
509
  </div>
445
510
  </div>
446
511
  ) : (
447
- <div className="grid gap-3 md:grid-cols-2">
448
- {activeRuntimes.map((runtime) => (
449
- <div key={runtime.runtimeId} className="surface-panel rounded-2xl p-4">
450
- <SectionEyebrow icon={Wallet} label="Provider Cap" />
451
- <h3 className="mt-1 text-sm font-semibold">{runtime.label}</h3>
452
- <p className="mt-2 text-lg font-semibold">
453
- {formatCurrencyUsd(
454
- toNullableNumber(form.runtimes[runtime.runtimeId].monthlySpendCapUsd)
455
- )}
456
- </p>
457
- <p className="text-xs text-muted-foreground">
458
- {runtime.label} receives the full monthly cap because it is the only
459
- configured paid provider.
460
- </p>
461
- </div>
462
- ))}
512
+ <div className="surface-panel rounded-2xl p-4">
513
+ <SectionEyebrow icon={Wallet} label="Provider Cap" />
514
+ <h3 className="mt-1 text-sm font-semibold">
515
+ {hasAnthropicRuntimes ? "Anthropic" : "OpenAI"}
516
+ </h3>
517
+ <p className="mt-2 text-lg font-semibold">
518
+ {formatCurrencyUsd(
519
+ toNullableNumber(form.overallMonthlySpendCapUsd)
520
+ )}
521
+ </p>
522
+ <p className="text-xs text-muted-foreground">
523
+ Full monthly cap single provider with{" "}
524
+ {activeRuntimes.length} runtime{activeRuntimes.length > 1 ? "s" : ""}.
525
+ </p>
463
526
  </div>
464
527
  )}
465
528