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.
- package/README.md +67 -31
- package/dist/cli.js +151 -2
- package/docs/.coverage-gaps.json +21 -0
- package/docs/.last-generated +1 -1
- package/docs/features/agent-intelligence.md +36 -14
- package/docs/features/chat.md +53 -71
- package/docs/features/cost-usage.md +14 -10
- package/docs/features/dashboard-kanban.md +30 -13
- package/docs/features/delivery-channels.md +198 -0
- package/docs/features/design-system.md +10 -10
- package/docs/features/documents.md +8 -8
- package/docs/features/home-workspace.md +20 -15
- package/docs/features/inbox-notifications.md +22 -10
- package/docs/features/keyboard-navigation.md +11 -11
- package/docs/features/monitoring.md +1 -1
- package/docs/features/playbook.md +30 -32
- package/docs/features/profiles.md +33 -11
- package/docs/features/projects.md +2 -2
- package/docs/features/provider-runtimes.md +58 -14
- package/docs/features/schedules.md +77 -41
- package/docs/features/settings.md +134 -51
- package/docs/features/shared-components.md +7 -15
- package/docs/features/tool-permissions.md +9 -9
- package/docs/features/workflows.md +32 -21
- package/docs/getting-started.md +33 -9
- package/docs/index.md +25 -16
- package/docs/journeys/developer.md +124 -207
- package/docs/journeys/personal-use.md +70 -79
- package/docs/journeys/power-user.md +107 -151
- package/docs/journeys/work-use.md +81 -113
- package/docs/manifest.json +79 -47
- package/docs/superpowers/plans/2026-03-30-finish-in-progress-features.md +547 -0
- package/docs/superpowers/specs/2026-03-27-chat-screenshot-display-design.md +303 -0
- package/docs/use-cases/agency-operator.md +84 -0
- package/docs/use-cases/solo-founder.md +75 -0
- package/docs/why-stagent.md +59 -0
- package/package.json +12 -3
- package/src/app/api/channels/[id]/route.ts +103 -0
- package/src/app/api/channels/[id]/test/route.ts +52 -0
- package/src/app/api/channels/inbound/slack/route.ts +109 -0
- package/src/app/api/channels/inbound/telegram/poll/route.ts +128 -0
- package/src/app/api/channels/inbound/telegram/route.ts +76 -0
- package/src/app/api/channels/route.ts +71 -0
- package/src/app/api/chat/conversations/[id]/messages/route.ts +3 -2
- package/src/app/api/chat/conversations/route.ts +15 -0
- package/src/app/api/chat/entities/search/route.ts +112 -0
- package/src/app/api/documents/[id]/file/route.ts +4 -1
- package/src/app/api/environment/profiles/suggest/route.ts +19 -3
- package/src/app/api/environment/scan/route.ts +8 -1
- package/src/app/api/handoffs/[id]/route.ts +76 -0
- package/src/app/api/handoffs/route.ts +89 -0
- package/src/app/api/memory/route.ts +181 -0
- package/src/app/api/profiles/[id]/route.ts +16 -1
- package/src/app/api/profiles/[id]/test/route.ts +4 -0
- package/src/app/api/profiles/[id]/test-results/route.ts +22 -0
- package/src/app/api/profiles/[id]/test-single/route.ts +64 -0
- package/src/app/api/profiles/assist/route.ts +35 -0
- package/src/app/api/profiles/import-repo/apply-updates/route.ts +123 -0
- package/src/app/api/profiles/import-repo/check-updates/route.ts +163 -0
- package/src/app/api/profiles/import-repo/confirm/route.ts +118 -0
- package/src/app/api/profiles/import-repo/preview/route.ts +107 -0
- package/src/app/api/profiles/import-repo/route.ts +29 -0
- package/src/app/api/profiles/import-repo/scan/route.ts +25 -0
- package/src/app/api/profiles/route.ts +73 -22
- package/src/app/api/projects/[id]/route.ts +119 -9
- package/src/app/api/projects/__tests__/delete-project.test.ts +170 -0
- package/src/app/api/runtimes/ollama/route.ts +86 -0
- package/src/app/api/runtimes/suggest/route.ts +29 -0
- package/src/app/api/schedules/[id]/heartbeat-history/route.ts +77 -0
- package/src/app/api/schedules/[id]/route.ts +41 -3
- package/src/app/api/schedules/parse/route.ts +66 -0
- package/src/app/api/schedules/route.ts +71 -12
- package/src/app/api/settings/author-default/route.ts +7 -0
- package/src/app/api/settings/browser-tools/route.ts +68 -0
- package/src/app/api/settings/learning/route.ts +41 -0
- package/src/app/api/settings/ollama/route.ts +34 -0
- package/src/app/api/settings/providers/route.ts +57 -0
- package/src/app/api/settings/routing/route.ts +24 -0
- package/src/app/api/settings/web-search/route.ts +28 -0
- package/src/app/api/tasks/[id]/execute/route.ts +13 -1
- package/src/app/documents/page.tsx +3 -0
- package/src/app/environment/page.tsx +8 -1
- package/src/app/settings/page.tsx +12 -4
- package/src/app/workflows/[id]/edit/page.tsx +2 -0
- package/src/app/workflows/new/page.tsx +2 -0
- package/src/components/chat/chat-command-popover.tsx +280 -0
- package/src/components/chat/chat-input.tsx +90 -10
- package/src/components/chat/chat-message.tsx +9 -3
- package/src/components/chat/chat-model-selector.tsx +42 -1
- package/src/components/chat/chat-shell.tsx +31 -5
- package/src/components/chat/screenshot-gallery.tsx +96 -0
- package/src/components/dashboard/welcome-landing.tsx +9 -9
- package/src/components/environment/artifact-card.tsx +27 -1
- package/src/components/environment/environment-dashboard.tsx +50 -2
- package/src/components/environment/environment-summary-card.tsx +5 -2
- package/src/components/environment/suggested-profiles.tsx +117 -52
- package/src/components/handoffs/handoff-approval-card.tsx +159 -0
- package/src/components/memory/memory-browser.tsx +315 -0
- package/src/components/monitoring/log-entry.tsx +61 -27
- package/src/components/profiles/learned-context-panel.tsx +4 -4
- package/src/components/profiles/profile-assist-panel.tsx +512 -0
- package/src/components/profiles/profile-browser.tsx +109 -8
- package/src/components/profiles/profile-card.tsx +29 -1
- package/src/components/profiles/profile-detail-view.tsx +200 -28
- package/src/components/profiles/profile-form-view.tsx +220 -82
- package/src/components/profiles/repo-import-wizard.tsx +648 -0
- package/src/components/profiles/smoke-test-editor.tsx +106 -0
- package/src/components/projects/project-detail.tsx +15 -2
- package/src/components/schedules/schedule-create-sheet.tsx +32 -330
- package/src/components/schedules/schedule-detail-sheet.tsx +37 -21
- package/src/components/schedules/schedule-edit-sheet.tsx +159 -0
- package/src/components/schedules/schedule-form.tsx +749 -0
- package/src/components/schedules/schedule-list.tsx +31 -2
- package/src/components/settings/auth-method-selector.tsx +7 -1
- package/src/components/settings/browser-tools-section.tsx +247 -0
- package/src/components/settings/budget-guardrails-section.tsx +111 -48
- package/src/components/settings/channels-section.tsx +526 -0
- package/src/components/settings/chat-settings-section.tsx +27 -1
- package/src/components/settings/data-management-section.tsx +8 -6
- package/src/components/settings/learning-context-section.tsx +124 -0
- package/src/components/settings/ollama-section.tsx +270 -0
- package/src/components/settings/providers-runtimes-section.tsx +499 -0
- package/src/components/settings/runtime-timeout-section.tsx +4 -4
- package/src/components/settings/web-search-section.tsx +101 -0
- package/src/components/shared/command-palette.tsx +1 -30
- package/src/components/shared/screenshot-lightbox.tsx +151 -0
- package/src/components/shared/tag-input.tsx +156 -0
- package/src/components/tasks/kanban-board.tsx +32 -0
- package/src/components/tasks/kanban-column.tsx +4 -2
- package/src/components/tasks/task-card.tsx +1 -0
- package/src/components/tasks/task-chip-bar.tsx +6 -1
- package/src/components/tasks/task-create-panel.tsx +55 -5
- package/src/components/workflows/workflow-form-view.tsx +38 -3
- package/src/hooks/use-caret-position.ts +104 -0
- package/src/hooks/use-chat-autocomplete.ts +288 -0
- package/src/hooks/use-project-skills.ts +66 -0
- package/src/hooks/use-tag-suggestions.ts +31 -0
- package/src/instrumentation.ts +4 -1
- package/src/lib/agents/__tests__/browser-mcp.test.ts +175 -0
- package/src/lib/agents/__tests__/claude-agent.test.ts +6 -0
- package/src/lib/agents/__tests__/learned-context.test.ts +10 -0
- package/src/lib/agents/agentic-loop.ts +235 -0
- package/src/lib/agents/browser-mcp.ts +174 -0
- package/src/lib/agents/claude-agent.ts +83 -198
- package/src/lib/agents/handoff/bus.ts +164 -0
- package/src/lib/agents/handoff/governance.ts +47 -0
- package/src/lib/agents/handoff/types.ts +16 -0
- package/src/lib/agents/learned-context.ts +27 -7
- package/src/lib/agents/memory/decay.ts +61 -0
- package/src/lib/agents/memory/extractor.ts +181 -0
- package/src/lib/agents/memory/retrieval.ts +96 -0
- package/src/lib/agents/memory/types.ts +6 -0
- package/src/lib/agents/profiles/__tests__/project-profiles.test.ts +119 -0
- package/src/lib/agents/profiles/__tests__/registry.test.ts +11 -3
- package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/content-creator/SKILL.md +19 -0
- package/src/lib/agents/profiles/builtins/content-creator/profile.yaml +27 -0
- package/src/lib/agents/profiles/builtins/customer-support-agent/SKILL.md +19 -0
- package/src/lib/agents/profiles/builtins/customer-support-agent/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/financial-analyst/SKILL.md +19 -0
- package/src/lib/agents/profiles/builtins/financial-analyst/profile.yaml +24 -0
- package/src/lib/agents/profiles/builtins/general/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/marketing-strategist/SKILL.md +19 -0
- package/src/lib/agents/profiles/builtins/marketing-strategist/profile.yaml +27 -0
- package/src/lib/agents/profiles/builtins/operations-coordinator/SKILL.md +19 -0
- package/src/lib/agents/profiles/builtins/operations-coordinator/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/researcher/SKILL.md +1 -0
- package/src/lib/agents/profiles/builtins/researcher/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/sales-researcher/SKILL.md +19 -0
- package/src/lib/agents/profiles/builtins/sales-researcher/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/shopping-assistant/SKILL.md +1 -0
- package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/sweep/profile.yaml +1 -1
- package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/travel-planner/SKILL.md +2 -0
- package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/wealth-manager/SKILL.md +2 -0
- package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +2 -2
- package/src/lib/agents/profiles/project-profiles.ts +193 -0
- package/src/lib/agents/profiles/registry.ts +130 -6
- package/src/lib/agents/profiles/types.ts +28 -0
- package/src/lib/agents/router.ts +174 -2
- package/src/lib/agents/runtime/__tests__/catalog.test.ts +15 -4
- package/src/lib/agents/runtime/anthropic-direct.ts +644 -0
- package/src/lib/agents/runtime/catalog.ts +57 -2
- package/src/lib/agents/runtime/claude.ts +205 -1
- package/src/lib/agents/runtime/index.ts +22 -0
- package/src/lib/agents/runtime/ollama-adapter.ts +409 -0
- package/src/lib/agents/runtime/openai-direct.ts +514 -0
- package/src/lib/agents/runtime/profile-assist-types.ts +30 -0
- package/src/lib/agents/runtime/types.ts +2 -0
- package/src/lib/agents/tool-permissions.ts +203 -0
- package/src/lib/channels/gateway.ts +321 -0
- package/src/lib/channels/poller.ts +268 -0
- package/src/lib/channels/registry.ts +90 -0
- package/src/lib/channels/slack-adapter.ts +188 -0
- package/src/lib/channels/telegram-adapter.ts +218 -0
- package/src/lib/channels/types.ts +43 -0
- package/src/lib/channels/webhook-adapter.ts +74 -0
- package/src/lib/chat/command-data.ts +50 -0
- package/src/lib/chat/context-builder.ts +147 -3
- package/src/lib/chat/engine.ts +182 -19
- package/src/lib/chat/ollama-engine.ts +198 -0
- package/src/lib/chat/slash-commands.ts +191 -0
- package/src/lib/chat/stagent-tools.ts +106 -20
- package/src/lib/chat/tool-catalog.ts +209 -0
- package/src/lib/chat/tool-registry.ts +90 -0
- package/src/lib/chat/tools/chat-history-tools.ts +4 -4
- package/src/lib/chat/tools/document-tools.ts +43 -6
- package/src/lib/chat/tools/handoff-tools.ts +70 -0
- package/src/lib/chat/tools/notification-tools.ts +4 -4
- package/src/lib/chat/tools/profile-tools.ts +3 -3
- package/src/lib/chat/tools/project-tools.ts +3 -3
- package/src/lib/chat/tools/schedule-tools.ts +29 -13
- package/src/lib/chat/tools/settings-tools.ts +2 -2
- package/src/lib/chat/tools/task-tools.ts +66 -11
- package/src/lib/chat/tools/usage-tools.ts +2 -2
- package/src/lib/chat/tools/workflow-tools.ts +8 -8
- package/src/lib/chat/types.ts +22 -6
- package/src/lib/constants/known-tools.ts +19 -0
- package/src/lib/constants/prose-styles.ts +1 -1
- package/src/lib/constants/settings.ts +11 -0
- package/src/lib/data/channel-bindings.ts +85 -0
- package/src/lib/data/clear.ts +38 -4
- package/src/lib/data/profile-test-results.ts +48 -0
- package/src/lib/data/seed-data/conversations.ts +196 -0
- package/src/lib/data/seed-data/learned-context.ts +99 -0
- package/src/lib/data/seed-data/notifications.ts +54 -1
- package/src/lib/data/seed-data/profile-test-results.ts +96 -0
- package/src/lib/data/seed-data/repo-imports.ts +51 -0
- package/src/lib/data/seed-data/views.ts +60 -0
- package/src/lib/data/seed.ts +51 -0
- package/src/lib/db/bootstrap.ts +167 -0
- package/src/lib/db/migrations/0012_add_screenshot_columns.sql +5 -0
- package/src/lib/db/migrations/0013_add_repo_imports.sql +15 -0
- package/src/lib/db/migrations/0014_add_linked_profile_id.sql +3 -0
- package/src/lib/db/migrations/0015_add_channel_bindings.sql +23 -0
- package/src/lib/db/schema.ts +192 -1
- package/src/lib/environment/__tests__/auto-scan.test.ts +86 -0
- package/src/lib/environment/__tests__/profile-linker.test.ts +187 -0
- package/src/lib/environment/auto-scan.ts +48 -0
- package/src/lib/environment/data.ts +25 -0
- package/src/lib/environment/profile-generator.ts +40 -10
- package/src/lib/environment/profile-linker.ts +143 -0
- package/src/lib/environment/profile-rules.ts +96 -0
- package/src/lib/import/dedup.ts +149 -0
- package/src/lib/import/format-adapter.ts +631 -0
- package/src/lib/import/github-api.ts +219 -0
- package/src/lib/import/repo-scanner.ts +251 -0
- package/src/lib/schedules/__tests__/nlp-parser.test.ts +330 -0
- package/src/lib/schedules/active-hours.ts +120 -0
- package/src/lib/schedules/heartbeat-parser.ts +224 -0
- package/src/lib/schedules/heartbeat-prompt.ts +153 -0
- package/src/lib/schedules/nlp-parser.ts +357 -0
- package/src/lib/schedules/scheduler.ts +218 -3
- package/src/lib/screenshots/__tests__/persist.test.ts +104 -0
- package/src/lib/screenshots/persist.ts +114 -0
- package/src/lib/settings/__tests__/budget-guardrails.test.ts +39 -1
- package/src/lib/settings/helpers.ts +6 -0
- package/src/lib/settings/routing.ts +24 -0
- package/src/lib/settings/runtime-setup.ts +28 -1
- package/src/lib/usage/ledger.ts +2 -1
- package/src/lib/utils/stagent-paths.ts +4 -0
- package/src/lib/validators/__tests__/settings.test.ts +9 -0
- package/src/lib/validators/profile.ts +39 -0
- package/src/lib/workflows/blueprints/builtins/business-daily-briefing.yaml +102 -0
- package/src/lib/workflows/blueprints/builtins/content-marketing-pipeline.yaml +90 -0
- package/src/lib/workflows/blueprints/builtins/customer-support-triage.yaml +107 -0
- package/src/lib/workflows/blueprints/builtins/financial-reporting.yaml +104 -0
- 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
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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
|
|
356
|
+
const anthropicAllocation = deriveClaudeAllocation(form);
|
|
306
357
|
const claudeRuntime = snapshot.runtimeStates["claude-code"];
|
|
307
|
-
|
|
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">
|
|
406
|
-
<span className="text-muted-foreground">{
|
|
463
|
+
<span className="font-medium">Anthropic</span>
|
|
464
|
+
<span className="text-muted-foreground">{anthropicAllocation}%</span>
|
|
407
465
|
</div>
|
|
408
466
|
<Slider
|
|
409
|
-
value={[
|
|
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
|
-
|
|
428
|
-
<
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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 & 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 & OpenAI Direct
|
|
505
|
+
</p>
|
|
506
|
+
</div>
|
|
442
507
|
</div>
|
|
443
508
|
</div>
|
|
444
509
|
</div>
|
|
445
510
|
</div>
|
|
446
511
|
) : (
|
|
447
|
-
<div className="
|
|
448
|
-
{
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|
|