diff --git a/ui/src/App.tsx b/ui/src/App.tsx
index b8d77f4..d00f809 100644
--- a/ui/src/App.tsx
+++ b/ui/src/App.tsx
@@ -133,6 +133,7 @@ function boardRoutes() {
} />
} />
} />
+ } />
} />
} />
} />
diff --git a/ui/src/components/BudgetPolicyCard.tsx b/ui/src/components/BudgetPolicyCard.tsx
index 4ff7209..7834e8c 100644
--- a/ui/src/components/BudgetPolicyCard.tsx
+++ b/ui/src/components/BudgetPolicyCard.tsx
@@ -33,11 +33,13 @@ export function BudgetPolicyCard({
onSave,
isSaving,
compact = false,
+ variant = "card",
}: {
summary: BudgetPolicySummary;
onSave?: (amountCents: number) => void;
isSaving?: boolean;
compact?: boolean;
+ variant?: "card" | "plain";
}) {
const [draftBudget, setDraftBudget] = useState(centsInputValue(summary.amount));
@@ -49,6 +51,142 @@ export function BudgetPolicyCard({
const canSave = typeof parsedDraft === "number" && parsedDraft !== summary.amount && Boolean(onSave);
const progress = summary.amount > 0 ? Math.min(100, summary.utilizationPercent) : 0;
const StatusIcon = summary.status === "hard_stop" ? ShieldAlert : summary.status === "warning" ? AlertTriangle : Wallet;
+ const isPlain = variant === "plain";
+
+ const observedBudgetGrid = isPlain ? (
+
+
+
Observed
+
{formatCents(summary.observedAmount)}
+
+ {summary.amount > 0 ? `${summary.utilizationPercent}% of limit` : "No cap configured"}
+
+
+
+
Budget
+
+ {summary.amount > 0 ? formatCents(summary.amount) : "Disabled"}
+
+
+ Soft alert at {summary.warnPercent}%{summary.paused && summary.pauseReason ? ` · ${summary.pauseReason} pause` : ""}
+
+
+
+ ) : (
+
+
+
Observed
+
{formatCents(summary.observedAmount)}
+
+ {summary.amount > 0 ? `${summary.utilizationPercent}% of limit` : "No cap configured"}
+
+
+
+
Budget
+
+ {summary.amount > 0 ? formatCents(summary.amount) : "Disabled"}
+
+
+ Soft alert at {summary.warnPercent}%{summary.paused && summary.pauseReason ? ` · ${summary.pauseReason} pause` : ""}
+
+
+
+ );
+
+ const progressSection = (
+
+
+ Remaining
+ {summary.amount > 0 ? formatCents(summary.remainingAmount) : "Unlimited"}
+
+
+
+ );
+
+ const pausedPane = summary.paused ? (
+
+
+
+ {summary.scopeType === "project"
+ ? "Execution is paused for this project until the budget is raised or the incident is dismissed."
+ : "Heartbeats are paused for this scope until the budget is raised or the incident is dismissed."}
+
+
+ ) : null;
+
+ const saveSection = onSave ? (
+
+
+
+ Budget (USD)
+
+ setDraftBudget(event.target.value)}
+ className="mt-2"
+ inputMode="decimal"
+ placeholder="0.00"
+ />
+
+
{
+ if (typeof parsedDraft === "number" && onSave) onSave(parsedDraft);
+ }}
+ disabled={!canSave || isSaving || parsedDraft === null}
+ >
+ {isSaving ? "Saving..." : summary.amount > 0 ? "Update budget" : "Set budget"}
+
+
+ ) : null;
+
+ if (isPlain) {
+ return (
+
+
+
+
+ {summary.scopeType}
+
+
{summary.scopeName}
+
{windowLabel(summary.windowKind)}
+
+
+
+ {summary.paused ? "Paused" : summary.status === "warning" ? "Warning" : summary.status === "hard_stop" ? "Hard stop" : "Healthy"}
+
+
+
+ {observedBudgetGrid}
+ {progressSection}
+ {pausedPane}
+ {saveSection}
+ {parsedDraft === null ? (
+
Enter a valid non-negative dollar amount.
+ ) : null}
+
+ );
+ }
return (
@@ -68,84 +206,12 @@ export function BudgetPolicyCard({
-
-
-
Observed
-
{formatCents(summary.observedAmount)}
-
- {summary.amount > 0 ? `${summary.utilizationPercent}% of limit` : "No cap configured"}
-
-
-
-
Budget
-
- {summary.amount > 0 ? formatCents(summary.amount) : "Disabled"}
-
-
- Soft alert at {summary.warnPercent}%{summary.paused && summary.pauseReason ? ` · ${summary.pauseReason} pause` : ""}
-
-
-
-
-
-
- Remaining
- {summary.amount > 0 ? formatCents(summary.remainingAmount) : "Unlimited"}
-
-
-
-
- {summary.paused ? (
-
-
-
- {summary.scopeType === "project"
- ? "Execution is paused for this project until the budget is raised or the incident is dismissed."
- : "Heartbeats are paused for this scope until the budget is raised or the incident is dismissed."}
-
-
- ) : null}
-
- {onSave ? (
-
-
-
-
- Budget (USD)
-
- setDraftBudget(event.target.value)}
- className="mt-2"
- inputMode="decimal"
- placeholder="0.00"
- />
-
-
{
- if (typeof parsedDraft === "number" && onSave) onSave(parsedDraft);
- }}
- disabled={!canSave || isSaving || parsedDraft === null}
- >
- {isSaving ? "Saving..." : summary.amount > 0 ? "Update budget" : "Set budget"}
-
-
- {parsedDraft === null ? (
-
Enter a valid non-negative dollar amount.
- ) : null}
-
+ {observedBudgetGrid}
+ {progressSection}
+ {pausedPane}
+ {saveSection}
+ {parsedDraft === null ? (
+ Enter a valid non-negative dollar amount.
) : null}
diff --git a/ui/src/components/BudgetSidebarMarker.tsx b/ui/src/components/BudgetSidebarMarker.tsx
new file mode 100644
index 0000000..43f10b9
--- /dev/null
+++ b/ui/src/components/BudgetSidebarMarker.tsx
@@ -0,0 +1,13 @@
+import { DollarSign } from "lucide-react";
+
+export function BudgetSidebarMarker({ title = "Paused by budget" }: { title?: string }) {
+ return (
+
+
+
+ );
+}
diff --git a/ui/src/components/SidebarAgents.tsx b/ui/src/components/SidebarAgents.tsx
index b94ccfe..5b438f0 100644
--- a/ui/src/components/SidebarAgents.tsx
+++ b/ui/src/components/SidebarAgents.tsx
@@ -10,6 +10,7 @@ import { heartbeatsApi } from "../api/heartbeats";
import { queryKeys } from "../lib/queryKeys";
import { cn, agentRouteRef, agentUrl } from "../lib/utils";
import { AgentIcon } from "./AgentIconPicker";
+import { BudgetSidebarMarker } from "./BudgetSidebarMarker";
import {
Collapsible,
CollapsibleContent,
@@ -124,15 +125,22 @@ export function SidebarAgents() {
>
{agent.name}
- {runCount > 0 && (
+ {(agent.pauseReason === "budget" || runCount > 0) && (
-
-
-
-
-
- {runCount} live
-
+ {agent.pauseReason === "budget" ? (
+
+ ) : null}
+ {runCount > 0 ? (
+
+
+
+
+ ) : null}
+ {runCount > 0 ? (
+
+ {runCount} live
+
+ ) : null}
)}
diff --git a/ui/src/components/SidebarProjects.tsx b/ui/src/components/SidebarProjects.tsx
index 72d0a21..cc6f417 100644
--- a/ui/src/components/SidebarProjects.tsx
+++ b/ui/src/components/SidebarProjects.tsx
@@ -20,6 +20,7 @@ import { projectsApi } from "../api/projects";
import { queryKeys } from "../lib/queryKeys";
import { cn, projectRouteRef } from "../lib/utils";
import { useProjectOrder } from "../hooks/useProjectOrder";
+import { BudgetSidebarMarker } from "./BudgetSidebarMarker";
import {
Collapsible,
CollapsibleContent,
@@ -88,6 +89,7 @@ function SortableProjectItem({
style={{ backgroundColor: project.color ?? "#6366f1" }}
/>
{project.name}
+ {project.pauseReason === "budget" ? : null}
{projectSidebarSlots.length > 0 && (
diff --git a/ui/src/pages/AgentDetail.tsx b/ui/src/pages/AgentDetail.tsx
index c1c9ebf..3203935 100644
--- a/ui/src/pages/AgentDetail.tsx
+++ b/ui/src/pages/AgentDetail.tsx
@@ -185,10 +185,11 @@ function scrollToContainerBottom(container: ScrollContainer, behavior: ScrollBeh
container.scrollTo({ top: container.scrollHeight, behavior });
}
-type AgentDetailView = "dashboard" | "configuration" | "runs";
+type AgentDetailView = "dashboard" | "configuration" | "runs" | "budget";
function parseAgentDetailView(value: string | null): AgentDetailView {
if (value === "configure" || value === "configuration") return "configuration";
+ if (value === "budget") return "budget";
if (value === "runs") return value;
return "dashboard";
}
@@ -638,6 +639,7 @@ export function AgentDetail() {
{ value: "dashboard", label: "Dashboard" },
{ value: "configuration", label: "Configuration" },
{ value: "runs", label: "Runs" },
+ { value: "budget", label: "Budget" },
]}
value={activeView}
onValueChange={(value) => navigate(`/agents/${canonicalAgentRef}/${value}`)}
@@ -645,15 +647,6 @@ export function AgentDetail() {
)}
- {!urlRunId && resolvedCompanyId ? (
-
budgetMutation.mutate(amount)}
- />
- ) : null}
-
{actionError && {actionError}
}
{isPendingApproval && (
@@ -752,6 +745,17 @@ export function AgentDetail() {
adapterType={agent.adapterType}
/>
)}
+
+ {activeView === "budget" && resolvedCompanyId ? (
+
+ budgetMutation.mutate(amount)}
+ variant="plain"
+ />
+
+ ) : null}
);
}
diff --git a/ui/src/pages/ProjectDetail.tsx b/ui/src/pages/ProjectDetail.tsx
index b74eb9d..4de4078 100644
--- a/ui/src/pages/ProjectDetail.tsx
+++ b/ui/src/pages/ProjectDetail.tsx
@@ -26,7 +26,7 @@ import { PluginSlotMount, PluginSlotOutlet, usePluginSlots } from "@/plugins/slo
/* ── Top-level tab types ── */
-type ProjectBaseTab = "overview" | "list" | "configuration";
+type ProjectBaseTab = "overview" | "list" | "configuration" | "budget";
type ProjectPluginTab = `plugin:${string}`;
type ProjectTab = ProjectBaseTab | ProjectPluginTab;
@@ -41,6 +41,7 @@ function resolveProjectTab(pathname: string, projectId: string): ProjectTab | nu
const tab = segments[projectsIdx + 2];
if (tab === "overview") return "overview";
if (tab === "configuration") return "configuration";
+ if (tab === "budget") return "budget";
if (tab === "issues") return "list";
return null;
}
@@ -328,6 +329,10 @@ export function ProjectDetail() {
navigate(`/projects/${canonicalProjectRef}/configuration`, { replace: true });
return;
}
+ if (activeTab === "budget") {
+ navigate(`/projects/${canonicalProjectRef}/budget`, { replace: true });
+ return;
+ }
if (activeTab === "list") {
if (filter) {
navigate(`/projects/${canonicalProjectRef}/issues/${filter}`, { replace: true });
@@ -454,6 +459,8 @@ export function ProjectDetail() {
}
if (tab === "overview") {
navigate(`/projects/${canonicalProjectRef}/overview`);
+ } else if (tab === "budget") {
+ navigate(`/projects/${canonicalProjectRef}/budget`);
} else if (tab === "configuration") {
navigate(`/projects/${canonicalProjectRef}/configuration`);
} else {
@@ -470,12 +477,20 @@ export function ProjectDetail() {
onSelect={(color) => updateProject.mutate({ color })}
/>
- updateProject.mutate({ name })}
- as="h2"
- className="text-xl font-bold"
- />
+
+
updateProject.mutate({ name })}
+ as="h2"
+ className="text-xl font-bold"
+ />
+ {project.pauseReason === "budget" ? (
+
+
+ Paused by budget hard stop
+
+ ) : null}
+
({
value: item.value,
label: item.label,
@@ -526,15 +542,6 @@ export function ProjectDetail() {
/>
- {resolvedCompanyId ? (
- budgetMutation.mutate(amount)}
- />
- ) : null}
-
{activeTab === "overview" && (
)}
+ {activeTab === "budget" && resolvedCompanyId ? (
+
+ budgetMutation.mutate(amount)}
+ />
+
+ ) : null}
+
{activePluginTab && (