feat(web): edit usage prices across selected environments#9861

merged4
t3code/stream-usage-environment-data
t3code/usage-environment-prices
openedSep 5, 2026byJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 3d ago
6 unresolved threads · Jump to file
0 / 7 viewed7 files changedLine totals unavailable

apps/web/src/components/usage/UsagePriceOverrides.tsx · Earlier revision 0d1c2ef4 · right 150

Original diff
@@ -1,318 +1,558 @@
 import { useAtomValue } from "@effect/atom-react";
-import type { EnvironmentId, UsageModelPriceOverride } from "@t3tools/contracts";
-import { useState } from "react";
+import type { EnvironmentId } from "@t3tools/contracts";
+import { ChevronDownIcon, PlusIcon, RotateCcwIcon, XIcon } from "lucide-react";
+import * as Option from "effect/Option";
+import { AsyncResult, Atom } from "effect/unstable/reactivity";
+import { useRef, useState } from "react";
 
 import { isElectron } from "../../env";
-import {
-  type EnvironmentPresentation,
-  useEnvironments,
-  usePrimaryEnvironmentId,
-} from "../../state/environments";
+import { cn } from "../../lib/utils";
+import { environmentPresentations } from "../../state/presentation";
 import { serverEnvironment } from "../../state/server";
-import { useEnvironmentSessionState } from "../../state/session";
+import { environmentSession } from "../../state/session";
 import { useAtomCommand } from "../../state/use-atom-command";
 import type { EnvironmentUsageStatus } from "../../state/usage";
 import {
   resolvePrimaryOperateAccess,
   resolveRemoteOperateAccess,
 } from "../settings/ProviderSettingsPanel.logic";
-import { Button } from "../ui/button";
+import { Button, buttonVariants } from "../ui/button";
 import {
   Dialog,
   DialogDescription,
+  DialogFooter,
   DialogHeader,
   DialogPanel,
   DialogPopup,
   DialogTitle,
 } from "../ui/dialog";
 import { Input } from "../ui/input";
 import { Label } from "../ui/label";
-import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";
-import { parseUsagePriceForm, USAGE_PRICE_FIELDS, usagePriceForm } from "./usagePriceForm";
+import { Menu, MenuCheckboxItem, MenuPopup, MenuSeparator, MenuTrigger } from "../ui/menu";
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table";
+import { Tooltip, TooltipTrigger, TooltipPopup } from "../ui/tooltip";
+import { USAGE_PRICE_FIELDS } from "./usagePriceForm";
+import {
+  usagePriceCell,
+  usagePriceTableChanges,
+  type UsagePriceDraft,
+  type UsagePriceField,
+} from "./usagePriceTable";
+import {
+  writeUsagePrices,
+  type UsagePriceTarget,
+  type UsagePriceWriteResult,
+} from "./usagePriceTargets";
 
-export function UsagePriceOverrides({
-  usage,
-}: {
-  readonly usage: readonly EnvironmentUsageStatus[];
-}) {
-  const [open, setOpen] = useState(false);
-  return (
-    <>
-      <Button variant="outline" size="sm" onClick={() => setOpen(true)}>
-        Model prices
-      </Button>
-      {open ? <ModelPricesDialog usage={usage} onOpenChange={setOpen} /> : null}
-    </>
-  );
-}
+const priceTargetsAtom = Atom.make((get): readonly UsagePriceTarget[] =>
+  [...get(environmentPresentations.presentationsAtom)].map(([environmentId, environment]) => {
+    const settings = get(serverEnvironment.settingsValueAtom(environmentId));
+    const session = get(environmentSession.sessionStateAtom(environmentId));
+    const sessionAccess = {
+      session: Option.getOrNull(AsyncResult.value(session)),
+      isPending: session.waiting,
+      hasError: session._tag === "Failure",
+    };
+    const isPrimary = environment.entry.target._tag === "PrimaryConnectionTarget";
+    const access = isPrimary
+      ? resolvePrimaryOperateAccess({ ...sessionAccess, isPrimary, hasDesktopBridge: isElectron })
+      : resolveRemoteOperateAccess(sessionAccess);
+    return {
+      environmentId,
+      label: environment.entry.target.label,
+      prices: settings?.usagePriceOverrides ?? null,
+      unavailable:
+        environment.connection.phase !== "connected"
+          ? "Offline"
+          : settings === null
+            ? "Prices not loaded"
+            : environment.serverConfig?.environment.capabilities.usagePriceOverrides !== true
+              ? "Update server to edit prices"
+              : access === "pending"
+                ? "Checking permissions…"
+                : access === "denied"
+                  ? "Read-only access"
+                  : null,
+    };
+  }),
+);
+
+type SaveAttempt = {
+  readonly drafts: readonly UsagePriceDraft[];
+  readonly destinations: readonly {
+    readonly environmentId: EnvironmentId;
+    readonly label: string;
+  }[];
+  readonly results: ReadonlyMap<
+    EnvironmentId,
+    UsagePriceWriteResult | { readonly status: "saving" }
+  >;
+};
 
-function ModelPricesDialog({
+export function UsagePriceOverrides({
   usage,
+  initialSelectedEnvironmentIds,
   onOpenChange,
 }: {
   readonly usage: readonly EnvironmentUsageStatus[];
+  readonly initialSelectedEnvironmentIds: ReadonlySet<EnvironmentId> | null;
   readonly onOpenChange: (open: boolean) => void;
 }) {
-  const { environments } = useEnvironments();
-  const primaryEnvironmentId = usePrimaryEnvironmentId();
-  const [selectedId, setSelectedId] = useState<EnvironmentId | null>(primaryEnvironmentId);
-  const environment =
-    environments.find((entry) => entry.environmentId === selectedId) ?? environments[0];
-  const summary = usage.find(
-    (entry) => entry.environmentId === environment?.environmentId,
-  )?.summary;
-  const models = [...new Set(summary?.buckets.map((bucket) => bucket.model) ?? [])].sort();
+  const environments = useAtomValue(priceTargetsAtom);
+  const [selectedIds, setSelectedIds] = useState(initialSelectedEnvironmentIds);
+  const selected = environments.filter(
+    (environment) => selectedIds === null || selectedIds.has(environment.environmentId),
+  );
+  const [drafts, setDrafts] = useState<readonly UsagePriceDraft[]>([]);
+  const [pending, setPending] = useState(false);
+  const [attempt, setAttempt] = useState<SaveAttempt | null>(null);
+  const focusRowRef = useRef<string | null>(null);
+  const nextRowId = useRef(0);
+  const updateSettings = useAtomCommand(serverEnvironment.updateSettings, { reportFailure: false });
+  const customModels = [
+    ...new Set(selected.flatMap((environment) => Object.keys(environment.prices ?? {}))),
+  ].sort();
+  const models = [
+    ...new Set([
+      ...customModels,
+      ...usage
+        .filter((environment) =>
+          selected.some((target) => target.environmentId === environment.environmentId),
+        )
+        .flatMap((environment) => environment.summary?.buckets.map((bucket) => bucket.model) ?? []),
+    ]),
+  ].sort();
+  // Keep new rows in place while successful environments publish their updated settings.
+  const newModels = new Set(
+    drafts.filter((draft) => draft.isNew).map((draft) => draft.model.trim()),
+  );
+  const rows: readonly UsagePriceDraft[] = [
+    ...customModels
+      .filter((model) => attempt === null || !newModels.has(model))
+      .map(
+        (model) =>
+          drafts.find((draft) => draft.id === `model:${model}`) ?? {
+            id: `model:${model}`,
+            model,
+            isNew: false,
+            values: {},
+          },
+      ),
+    ...drafts.filter((draft) => draft.isNew),
+  ];
+  const plans = new Map(
+    selected.map((target) => [target.environmentId, usagePriceTableChanges(target, drafts)]),
+  );
+  const errors = new Map(
+    selected
+      .filter((target) => target.prices !== null)
+      .flatMap((target) => [...plans.get(target.environmentId)!.errors]),
+  );
C
cursorBotSep 5, 2026

Unwritable envs block valid price saves

Medium Severity

The save-blocking errors map keeps validation failures from every selected environment with loaded prices, including ones already marked unavailable (offline, read-only, old server). A single-field edit of an existing override then fails validation on those destinations because they still need a complete price to create one. Save stays disabled, and Apply to is frozen once drafts exist, so writable environments cannot receive the change without discarding and deselecting first.

Additional Locations (2)

Reviewed by Cursor Bugbot for commit 0d1c2ef4d8475dbadac7a383bbdef20020d425cf. Configure here.

J
Julius MarmingeSep 5, 2026

Fixed in f3f881b61. Only writable destinations contribute save-blocking validation; unavailable destinations still receive their own Not saved result. Regression tests cover offline, read-only, and older servers without an existing override while another environment can accept a single-column edit. All 33 focused tests and web typecheck pass.

apps/web/src/components/usage/UsagePriceOverrides.tsx · Earlier revision 32c1d4cb · right 263

Original diff
@@ -1,318 +1,552 @@
 import { useAtomValue } from "@effect/atom-react";
-import type { EnvironmentId, UsageModelPriceOverride } from "@t3tools/contracts";
-import { useState } from "react";
+import type { EnvironmentId } from "@t3tools/contracts";
+import { ChevronDownIcon, PlusIcon, RotateCcwIcon, XIcon } from "lucide-react";
+import * as Option from "effect/Option";
+import { AsyncResult, Atom } from "effect/unstable/reactivity";
+import { useRef, useState } from "react";
 
 import { isElectron } from "../../env";
-import {
-  type EnvironmentPresentation,
-  useEnvironments,
-  usePrimaryEnvironmentId,
-} from "../../state/environments";
+import { cn } from "../../lib/utils";
+import { environmentPresentations } from "../../state/presentation";
 import { serverEnvironment } from "../../state/server";
-import { useEnvironmentSessionState } from "../../state/session";
+import { environmentSession } from "../../state/session";
 import { useAtomCommand } from "../../state/use-atom-command";
 import type { EnvironmentUsageStatus } from "../../state/usage";
 import {
   resolvePrimaryOperateAccess,
   resolveRemoteOperateAccess,
 } from "../settings/ProviderSettingsPanel.logic";
-import { Button } from "../ui/button";
+import { Button, buttonVariants } from "../ui/button";
 import {
   Dialog,
   DialogDescription,
+  DialogFooter,
   DialogHeader,
   DialogPanel,
   DialogPopup,
   DialogTitle,
 } from "../ui/dialog";
 import { Input } from "../ui/input";
 import { Label } from "../ui/label";
-import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";
-import { parseUsagePriceForm, USAGE_PRICE_FIELDS, usagePriceForm } from "./usagePriceForm";
+import { Menu, MenuCheckboxItem, MenuPopup, MenuSeparator, MenuTrigger } from "../ui/menu";
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table";
+import { Tooltip, TooltipTrigger, TooltipPopup } from "../ui/tooltip";
+import { USAGE_PRICE_FIELDS } from "./usagePriceForm";
+import {
+  usagePriceCell,
+  usagePriceTableChanges,
+  usagePriceTableErrors,
+  type UsagePriceDraft,
+  type UsagePriceField,
+} from "./usagePriceTable";
+import {
+  writeUsagePrices,
+  type UsagePriceTarget,
+  type UsagePriceWriteResult,
+} from "./usagePriceTargets";
 
-export function UsagePriceOverrides({
-  usage,
-}: {
-  readonly usage: readonly EnvironmentUsageStatus[];
-}) {
-  const [open, setOpen] = useState(false);
-  return (
-    <>
-      <Button variant="outline" size="sm" onClick={() => setOpen(true)}>
-        Model prices
-      </Button>
-      {open ? <ModelPricesDialog usage={usage} onOpenChange={setOpen} /> : null}
-    </>
-  );
-}
+const priceTargetsAtom = Atom.make((get): readonly UsagePriceTarget[] =>
+  [...get(environmentPresentations.presentationsAtom)].map(([environmentId, environment]) => {
+    const settings = get(serverEnvironment.settingsValueAtom(environmentId));
+    const session = get(environmentSession.sessionStateAtom(environmentId));
+    const sessionAccess = {
+      session: Option.getOrNull(AsyncResult.value(session)),
+      isPending: session.waiting,
+      hasError: session._tag === "Failure",
+    };
+    const isPrimary = environment.entry.target._tag === "PrimaryConnectionTarget";
+    const access = isPrimary
+      ? resolvePrimaryOperateAccess({ ...sessionAccess, isPrimary, hasDesktopBridge: isElectron })
+      : resolveRemoteOperateAccess(sessionAccess);
+    return {
+      environmentId,
+      label: environment.entry.target.label,
+      prices: settings?.usagePriceOverrides ?? null,
+      unavailable:
+        environment.connection.phase !== "connected"
+          ? "Offline"
+          : settings === null
+            ? "Prices not loaded"
+            : environment.serverConfig?.environment.capabilities.usagePriceOverrides !== true
+              ? "Update server to edit prices"
+              : access === "pending"
+                ? "Checking permissions…"
+                : access === "denied"
+                  ? "Read-only access"
+                  : null,
+    };
+  }),
+);
+
+type SaveAttempt = {
+  readonly drafts: readonly UsagePriceDraft[];
+  readonly destinations: readonly {
+    readonly environmentId: EnvironmentId;
+    readonly label: string;
+  }[];
+  readonly results: ReadonlyMap<
+    EnvironmentId,
+    UsagePriceWriteResult | { readonly status: "saving" }
+  >;
+};
 
-function ModelPricesDialog({
+export function UsagePriceOverrides({
   usage,
+  initialSelectedEnvironmentIds,
   onOpenChange,
 }: {
   readonly usage: readonly EnvironmentUsageStatus[];
+  readonly initialSelectedEnvironmentIds: ReadonlySet<EnvironmentId> | null;
   readonly onOpenChange: (open: boolean) => void;
 }) {
-  const { environments } = useEnvironments();
-  const primaryEnvironmentId = usePrimaryEnvironmentId();
-  const [selectedId, setSelectedId] = useState<EnvironmentId | null>(primaryEnvironmentId);
-  const environment =
-    environments.find((entry) => entry.environmentId === selectedId) ?? environments[0];
-  const summary = usage.find(
-    (entry) => entry.environmentId === environment?.environmentId,
-  )?.summary;
-  const models = [...new Set(summary?.buckets.map((bucket) => bucket.model) ?? [])].sort();
+  const environments = useAtomValue(priceTargetsAtom);
+  const [selectedIds, setSelectedIds] = useState(initialSelectedEnvironmentIds);
+  const selected = environments.filter(
+    (environment) => selectedIds === null || selectedIds.has(environment.environmentId),
+  );
+  const [drafts, setDrafts] = useState<readonly UsagePriceDraft[]>([]);
+  const [pending, setPending] = useState(false);
+  const [attempt, setAttempt] = useState<SaveAttempt | null>(null);
+  const focusRowRef = useRef<string | null>(null);
+  const nextRowId = useRef(0);
+  const updateSettings = useAtomCommand(serverEnvironment.updateSettings, { reportFailure: false });
+  const customModels = [
+    ...new Set(selected.flatMap((environment) => Object.keys(environment.prices ?? {}))),
+  ].sort();
+  const models = [
+    ...new Set([
+      ...customModels,
+      ...usage
+        .filter((environment) =>
+          selected.some((target) => target.environmentId === environment.environmentId),
+        )
+        .flatMap((environment) => environment.summary?.buckets.map((bucket) => bucket.model) ?? []),
+    ]),
+  ].sort();
+  // Keep new rows in place while successful environments publish their updated settings.
+  const newModels = new Set(
+    drafts.filter((draft) => draft.isNew).map((draft) => draft.model.trim()),
+  );
+  const rows: readonly UsagePriceDraft[] = [
+    ...customModels
+      .filter((model) => attempt === null || !newModels.has(model))
+      .map(
+        (model) =>
+          drafts.find((draft) => draft.id === `model:${model}`) ?? {
+            id: `model:${model}`,
+            model,
+            isNew: false,
+            values: {},
+          },
+      ),
+    ...drafts.filter((draft) => draft.isNew),
+  ];
+  const errors = usagePriceTableErrors(selected, drafts);
+  for (const draft of drafts) {
+    if (!draft.isNew) continue;
+    if (draft.model.trim() === "") errors.set(draft.id, "Enter a model ID.");
+    else if (
+      attempt === null &&
+      (customModels.includes(draft.model.trim()) ||
+        drafts.some((other) => other.id !== draft.id && other.model.trim() === draft.model.trim()))
+    )
+      errors.set(draft.id, "This model already has a row. Edit its prices there.");
+  }
+  const failedDestinations =
+    attempt?.destinations.filter(
+      (destination) => attempt.results.get(destination.environmentId)?.status === "failed",
+    ) ?? [];
+  const locked = pending || failedDestinations.length > 0;
+  const hasChanges = drafts.length > 0;
+  const destinationLabel =
+    selected.length === 1 ? selected[0]!.label : `${selected.length} environments`;
+  const selectionLabel = selectedIds === null ? "All environments" : destinationLabel;
+  const discard = () => {
+    setDrafts([]);
+    setAttempt(null);
+  };
+  const selectEnvironments = (ids: ReadonlySet<EnvironmentId> | null) => {
+    setSelectedIds(ids);
+    setAttempt(null);
+  };
+  const updateDraft = (draft: UsagePriceDraft) => {
+    setDrafts((previous) =>
+      previous.some((entry) => entry.id === draft.id)
+        ? previous.map((entry) => (entry.id === draft.id ? draft : entry))
+        : [...previous, draft],
+    );
+    setAttempt(null);
+  };
+  const editCell = (row: UsagePriceDraft, field: UsagePriceField, value: string) => {
+    const values = { ...row.values, [field]: value };
+    const original = usagePriceCell(selected, row.model, field);
+    if (
+      !row.isNew &&
+      original.placeholder !== "Mixed" &&
+      original.placeholder !== "Unavailable" &&
+      value === original.value
+    )
+      delete values[field];
+    if (!row.isNew && Object.keys(values).length === 0)
+      setDrafts((previous) => previous.filter((entry) => entry.id !== row.id));
+    else updateDraft({ ...row, values });
+  };
+  const save = async (retry = false) => {
+    if (pending) return;
+    const destinations = retry ? failedDestinations : selected;
+    const edits = retry && attempt ? attempt.drafts : drafts;
+    if (destinations.length === 0 || edits.length === 0) return;
+    const targets = destinations.map(
+      (destination) =>
+        environments.find(
+          (environment) => environment.environmentId === destination.environmentId,
+        ) ?? { ...destination, prices: null, unavailable: "Environment removed" },
+    );
+    const changes = new Map(
+      targets.map((target) => [target.environmentId, usagePriceTableChanges(target, edits)]),
+    );
+    setPending(true);
+    setAttempt((previous) => ({
+      drafts: edits,
+      destinations: retry && previous ? previous.destinations : targets,
+      results: new Map([
+        ...(retry && previous ? previous.results : []),
+        ...targets.map((target) => [target.environmentId, { status: "saving" } as const] as const),
+      ]),
+    }));
+    let failed = false;
+    await writeUsagePrices({
+      targets: targets.map((target) => ({
+        ...target,
+        unavailable:
+          target.unavailable ?? [...changes.get(target.environmentId)!.errors.values()][0] ?? null,
+      })),
+      changes: new Map([...changes].map(([id, plan]) => [id, plan.changes])),
+      write: updateSettings,
+      onResult: (environmentId, result) => {
+        if (result.status === "failed") failed = true;
+        setAttempt((previous) =>
+          previous === null
+            ? null
+            : { ...previous, results: new Map(previous.results).set(environmentId, result) },
+        );
+      },
+    });
+    setPending(false);
+    if (!failed) setDrafts([]);
+  };
 
   return (
-    <Dialog open onOpenChange={onOpenChange}>
-      <DialogPopup className="max-w-xl">
+    <Dialog
+      open
+      onOpenChange={(open) => {
+        if (!pending) onOpenChange(open);
+      }}
+    >
+      <DialogPopup className="max-w-3xl">
         <DialogHeader>
-          <DialogTitle>Model prices</DialogTitle>
+          <DialogTitle>Custom model prices</DialogTitle>
           <DialogDescription>
-            Override estimated costs using USD per million tokens. Prices apply to all saved history
-            on the selected environment.
+            Prices apply to all past and future usage on the environments you select.
           </DialogDescription>
         </DialogHeader>
-        <DialogPanel className="grid gap-5 pb-6">
-          {environment ? (
-            <>
-              <div className="grid gap-1.5">
-                <Label htmlFor="usage-prices-environment">Environment</Label>
-                <Select
-                  value={environment.environmentId}
-                  onValueChange={(value) => {
-                    const next = environments.find((entry) => entry.environmentId === value);
-                    if (next) setSelectedId(next.environmentId);
-                  }}
+        <DialogPanel className="grid gap-4">
+          <div className="flex flex-wrap items-center justify-between gap-2">
+            <div className="flex min-w-0 items-center gap-2">
+              <Label id="usage-prices-apply-label" className="shrink-0">
+                Apply to
+              </Label>
+              <Menu>
+                <MenuTrigger
+                  aria-labelledby="usage-prices-apply-label usage-prices-selection"
+                  disabled={pending || hasChanges}
+                  className={cn(buttonVariants({ variant: "outline", size: "sm" }), "min-w-0")}
M
macroscopeappBotSep 5, 2026

usage/UsagePriceOverrides.tsx:263

Shared primitives: rather than reapplying buttonVariants as a class string, MenuTrigger should compose Button from ui/ via render, which is the convention at the other menu call sites (PreviewMoreMenu.tsx, CompactComposerControlsMenu.tsx).

diff
1 <MenuTrigger
2+ render={<Button variant="outline" size="sm" className="min-w-0" />}
3 aria-labelledby="usage-prices-apply-label usage-prices-selection"
4 disabled={pending || hasChanges}
5- className={cn(buttonVariants({ variant: "outline", size: "sm" }), "min-w-0")}
6 >

(buttonVariants can then be dropped from the ../ui/button import.)

No longer relevant as of ef877f722c22f41122711c3a2bf1d2675c12160e

J
Julius MarmingeSep 5, 2026

Fixed in ef877f722. The menu trigger now composes Button through render. Verified the environment dropdown still opens and selects normally; web typecheck and scoped lint pass.

apps/web/src/components/usage/UsagePriceOverrides.tsx · Earlier revision 32c1d4cb · right 430

Original diff
@@ -1,318 +1,552 @@
 import { useAtomValue } from "@effect/atom-react";
-import type { EnvironmentId, UsageModelPriceOverride } from "@t3tools/contracts";
-import { useState } from "react";
+import type { EnvironmentId } from "@t3tools/contracts";
+import { ChevronDownIcon, PlusIcon, RotateCcwIcon, XIcon } from "lucide-react";
+import * as Option from "effect/Option";
+import { AsyncResult, Atom } from "effect/unstable/reactivity";
+import { useRef, useState } from "react";
 
 import { isElectron } from "../../env";
-import {
-  type EnvironmentPresentation,
-  useEnvironments,
-  usePrimaryEnvironmentId,
-} from "../../state/environments";
+import { cn } from "../../lib/utils";
+import { environmentPresentations } from "../../state/presentation";
 import { serverEnvironment } from "../../state/server";
-import { useEnvironmentSessionState } from "../../state/session";
+import { environmentSession } from "../../state/session";
 import { useAtomCommand } from "../../state/use-atom-command";
 import type { EnvironmentUsageStatus } from "../../state/usage";
 import {
   resolvePrimaryOperateAccess,
   resolveRemoteOperateAccess,
 } from "../settings/ProviderSettingsPanel.logic";
-import { Button } from "../ui/button";
+import { Button, buttonVariants } from "../ui/button";
 import {
   Dialog,
   DialogDescription,
+  DialogFooter,
   DialogHeader,
   DialogPanel,
   DialogPopup,
   DialogTitle,
 } from "../ui/dialog";
 import { Input } from "../ui/input";
 import { Label } from "../ui/label";
-import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";
-import { parseUsagePriceForm, USAGE_PRICE_FIELDS, usagePriceForm } from "./usagePriceForm";
+import { Menu, MenuCheckboxItem, MenuPopup, MenuSeparator, MenuTrigger } from "../ui/menu";
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table";
+import { Tooltip, TooltipTrigger, TooltipPopup } from "../ui/tooltip";
+import { USAGE_PRICE_FIELDS } from "./usagePriceForm";
+import {
+  usagePriceCell,
+  usagePriceTableChanges,
+  usagePriceTableErrors,
+  type UsagePriceDraft,
+  type UsagePriceField,
+} from "./usagePriceTable";
+import {
+  writeUsagePrices,
+  type UsagePriceTarget,
+  type UsagePriceWriteResult,
+} from "./usagePriceTargets";
 
-export function UsagePriceOverrides({
-  usage,
-}: {
-  readonly usage: readonly EnvironmentUsageStatus[];
-}) {
-  const [open, setOpen] = useState(false);
-  return (
-    <>
-      <Button variant="outline" size="sm" onClick={() => setOpen(true)}>
-        Model prices
-      </Button>
-      {open ? <ModelPricesDialog usage={usage} onOpenChange={setOpen} /> : null}
-    </>
-  );
-}
+const priceTargetsAtom = Atom.make((get): readonly UsagePriceTarget[] =>
+  [...get(environmentPresentations.presentationsAtom)].map(([environmentId, environment]) => {
+    const settings = get(serverEnvironment.settingsValueAtom(environmentId));
+    const session = get(environmentSession.sessionStateAtom(environmentId));
+    const sessionAccess = {
+      session: Option.getOrNull(AsyncResult.value(session)),
+      isPending: session.waiting,
+      hasError: session._tag === "Failure",
+    };
+    const isPrimary = environment.entry.target._tag === "PrimaryConnectionTarget";
+    const access = isPrimary
+      ? resolvePrimaryOperateAccess({ ...sessionAccess, isPrimary, hasDesktopBridge: isElectron })
+      : resolveRemoteOperateAccess(sessionAccess);
+    return {
+      environmentId,
+      label: environment.entry.target.label,
+      prices: settings?.usagePriceOverrides ?? null,
+      unavailable:
+        environment.connection.phase !== "connected"
+          ? "Offline"
+          : settings === null
+            ? "Prices not loaded"
+            : environment.serverConfig?.environment.capabilities.usagePriceOverrides !== true
+              ? "Update server to edit prices"
+              : access === "pending"
+                ? "Checking permissions…"
+                : access === "denied"
+                  ? "Read-only access"
+                  : null,
+    };
+  }),
+);
+
+type SaveAttempt = {
+  readonly drafts: readonly UsagePriceDraft[];
+  readonly destinations: readonly {
+    readonly environmentId: EnvironmentId;
+    readonly label: string;
+  }[];
+  readonly results: ReadonlyMap<
+    EnvironmentId,
+    UsagePriceWriteResult | { readonly status: "saving" }
+  >;
+};
 
-function ModelPricesDialog({
+export function UsagePriceOverrides({
   usage,
+  initialSelectedEnvironmentIds,
   onOpenChange,
 }: {
   readonly usage: readonly EnvironmentUsageStatus[];
+  readonly initialSelectedEnvironmentIds: ReadonlySet<EnvironmentId> | null;
   readonly onOpenChange: (open: boolean) => void;
 }) {
-  const { environments } = useEnvironments();
-  const primaryEnvironmentId = usePrimaryEnvironmentId();
-  const [selectedId, setSelectedId] = useState<EnvironmentId | null>(primaryEnvironmentId);
-  const environment =
-    environments.find((entry) => entry.environmentId === selectedId) ?? environments[0];
-  const summary = usage.find(
-    (entry) => entry.environmentId === environment?.environmentId,
-  )?.summary;
-  const models = [...new Set(summary?.buckets.map((bucket) => bucket.model) ?? [])].sort();
+  const environments = useAtomValue(priceTargetsAtom);
+  const [selectedIds, setSelectedIds] = useState(initialSelectedEnvironmentIds);
+  const selected = environments.filter(
+    (environment) => selectedIds === null || selectedIds.has(environment.environmentId),
+  );
+  const [drafts, setDrafts] = useState<readonly UsagePriceDraft[]>([]);
+  const [pending, setPending] = useState(false);
+  const [attempt, setAttempt] = useState<SaveAttempt | null>(null);
+  const focusRowRef = useRef<string | null>(null);
+  const nextRowId = useRef(0);
+  const updateSettings = useAtomCommand(serverEnvironment.updateSettings, { reportFailure: false });
+  const customModels = [
+    ...new Set(selected.flatMap((environment) => Object.keys(environment.prices ?? {}))),
+  ].sort();
+  const models = [
+    ...new Set([
+      ...customModels,
+      ...usage
+        .filter((environment) =>
+          selected.some((target) => target.environmentId === environment.environmentId),
+        )
+        .flatMap((environment) => environment.summary?.buckets.map((bucket) => bucket.model) ?? []),
+    ]),
+  ].sort();
+  // Keep new rows in place while successful environments publish their updated settings.
+  const newModels = new Set(
+    drafts.filter((draft) => draft.isNew).map((draft) => draft.model.trim()),
+  );
+  const rows: readonly UsagePriceDraft[] = [
+    ...customModels
+      .filter((model) => attempt === null || !newModels.has(model))
+      .map(
+        (model) =>
+          drafts.find((draft) => draft.id === `model:${model}`) ?? {
+            id: `model:${model}`,
+            model,
+            isNew: false,
+            values: {},
+          },
+      ),
+    ...drafts.filter((draft) => draft.isNew),
+  ];
+  const errors = usagePriceTableErrors(selected, drafts);
+  for (const draft of drafts) {
+    if (!draft.isNew) continue;
+    if (draft.model.trim() === "") errors.set(draft.id, "Enter a model ID.");
+    else if (
+      attempt === null &&
+      (customModels.includes(draft.model.trim()) ||
+        drafts.some((other) => other.id !== draft.id && other.model.trim() === draft.model.trim()))
+    )
+      errors.set(draft.id, "This model already has a row. Edit its prices there.");
+  }
+  const failedDestinations =
+    attempt?.destinations.filter(
+      (destination) => attempt.results.get(destination.environmentId)?.status === "failed",
+    ) ?? [];
+  const locked = pending || failedDestinations.length > 0;
+  const hasChanges = drafts.length > 0;
+  const destinationLabel =
+    selected.length === 1 ? selected[0]!.label : `${selected.length} environments`;
+  const selectionLabel = selectedIds === null ? "All environments" : destinationLabel;
+  const discard = () => {
+    setDrafts([]);
+    setAttempt(null);
+  };
+  const selectEnvironments = (ids: ReadonlySet<EnvironmentId> | null) => {
+    setSelectedIds(ids);
+    setAttempt(null);
+  };
+  const updateDraft = (draft: UsagePriceDraft) => {
+    setDrafts((previous) =>
+      previous.some((entry) => entry.id === draft.id)
+        ? previous.map((entry) => (entry.id === draft.id ? draft : entry))
+        : [...previous, draft],
+    );
+    setAttempt(null);
+  };
+  const editCell = (row: UsagePriceDraft, field: UsagePriceField, value: string) => {
+    const values = { ...row.values, [field]: value };
+    const original = usagePriceCell(selected, row.model, field);
+    if (
+      !row.isNew &&
+      original.placeholder !== "Mixed" &&
+      original.placeholder !== "Unavailable" &&
+      value === original.value
+    )
+      delete values[field];
+    if (!row.isNew && Object.keys(values).length === 0)
+      setDrafts((previous) => previous.filter((entry) => entry.id !== row.id));
+    else updateDraft({ ...row, values });
+  };
+  const save = async (retry = false) => {
+    if (pending) return;
+    const destinations = retry ? failedDestinations : selected;
+    const edits = retry && attempt ? attempt.drafts : drafts;
+    if (destinations.length === 0 || edits.length === 0) return;
+    const targets = destinations.map(
+      (destination) =>
+        environments.find(
+          (environment) => environment.environmentId === destination.environmentId,
+        ) ?? { ...destination, prices: null, unavailable: "Environment removed" },
+    );
+    const changes = new Map(
+      targets.map((target) => [target.environmentId, usagePriceTableChanges(target, edits)]),
+    );
+    setPending(true);
+    setAttempt((previous) => ({
+      drafts: edits,
+      destinations: retry && previous ? previous.destinations : targets,
+      results: new Map([
+        ...(retry && previous ? previous.results : []),
+        ...targets.map((target) => [target.environmentId, { status: "saving" } as const] as const),
+      ]),
+    }));
+    let failed = false;
+    await writeUsagePrices({
+      targets: targets.map((target) => ({
+        ...target,
+        unavailable:
+          target.unavailable ?? [...changes.get(target.environmentId)!.errors.values()][0] ?? null,
+      })),
+      changes: new Map([...changes].map(([id, plan]) => [id, plan.changes])),
+      write: updateSettings,
+      onResult: (environmentId, result) => {
+        if (result.status === "failed") failed = true;
+        setAttempt((previous) =>
+          previous === null
+            ? null
+            : { ...previous, results: new Map(previous.results).set(environmentId, result) },
+        );
+      },
+    });
+    setPending(false);
+    if (!failed) setDrafts([]);
+  };
 
   return (
-    <Dialog open onOpenChange={onOpenChange}>
-      <DialogPopup className="max-w-xl">
+    <Dialog
+      open
+      onOpenChange={(open) => {
+        if (!pending) onOpenChange(open);
+      }}
+    >
+      <DialogPopup className="max-w-3xl">
         <DialogHeader>
-          <DialogTitle>Model prices</DialogTitle>
+          <DialogTitle>Custom model prices</DialogTitle>
           <DialogDescription>
-            Override estimated costs using USD per million tokens. Prices apply to all saved history
-            on the selected environment.
+            Prices apply to all past and future usage on the environments you select.
           </DialogDescription>
         </DialogHeader>
-        <DialogPanel className="grid gap-5 pb-6">
-          {environment ? (
-            <>
-              <div className="grid gap-1.5">
-                <Label htmlFor="usage-prices-environment">Environment</Label>
-                <Select
-                  value={environment.environmentId}
-                  onValueChange={(value) => {
-                    const next = environments.find((entry) => entry.environmentId === value);
-                    if (next) setSelectedId(next.environmentId);
-                  }}
+        <DialogPanel className="grid gap-4">
+          <div className="flex flex-wrap items-center justify-between gap-2">
+            <div className="flex min-w-0 items-center gap-2">
+              <Label id="usage-prices-apply-label" className="shrink-0">
+                Apply to
+              </Label>
+              <Menu>
+                <MenuTrigger
+                  aria-labelledby="usage-prices-apply-label usage-prices-selection"
+                  disabled={pending || hasChanges}
+                  className={cn(buttonVariants({ variant: "outline", size: "sm" }), "min-w-0")}
                 >
-                  <SelectTrigger id="usage-prices-environment">
-                    <SelectValue>{environment.label}</SelectValue>
-                  </SelectTrigger>
-                  <SelectPopup>
-                    {environments.map((entry) => (
-                      <SelectItem key={entry.environmentId} value={entry.environmentId}>
-                        {entry.label}
-                      </SelectItem>
+                  <span id="usage-prices-selection" className="truncate">
+                    {selectionLabel}
+                  </span>
+                  <ChevronDownIcon className="size-3.5 shrink-0" aria-hidden />
+                </MenuTrigger>
+                <MenuPopup align="start" className="w-80 max-w-[calc(100vw-2rem)]">
+                  <MenuCheckboxItem
+                    checked={selectedIds === null}
+                    closeOnClick={false}
+                    onCheckedChange={(checked) => selectEnvironments(checked ? null : new Set())}
+                  >
+                    All environments
+                  </MenuCheckboxItem>
+                  <MenuSeparator />
+                  {environments.map((environment) => (
+                    <MenuCheckboxItem
+                      key={environment.environmentId}
+                      checked={selectedIds === null || selectedIds.has(environment.environmentId)}
+                      closeOnClick={false}
+                      onCheckedChange={(checked) => {
+                        const next = new Set(selected.map((target) => target.environmentId));
+                        if (checked) next.add(environment.environmentId);
+                        else next.delete(environment.environmentId);
+                        selectEnvironments(next.size === environments.length ? null : next);
+                      }}
+                    >
+                      <span className="flex min-w-0 items-center gap-3">
+                        <span className="min-w-0 flex-1 truncate">{environment.label}</span>
+                        {environment.unavailable ? (
+                          <span className="text-xs text-muted-foreground">
+                            {environment.unavailable}
+                          </span>
+                        ) : null}
+                      </span>
+                    </MenuCheckboxItem>
+                  ))}
+                </MenuPopup>
+              </Menu>
+            </div>
+            <span className="text-xs text-muted-foreground">USD / million tokens</span>
+          </div>
+          {selected.length === 0 ? (
+            <p className="text-sm text-muted-foreground">
+              {environments.length === 0
+                ? "Connect an environment to set model prices."
+                : "Select an environment to see and change its model prices."}
+            </p>
+          ) : (
+            <>
+              <div className="min-w-0 overflow-hidden rounded-lg border border-border">
+                <Table className="min-w-160 table-fixed" aria-label="Custom model prices">
+                  <colgroup>
+                    <col className="w-[34%]" />
+                    {USAGE_PRICE_FIELDS.map((field) => (
+                      <col key={field.key} />
                     ))}
-                  </SelectPopup>
-                </Select>
+                    <col className="w-10" />
+                  </colgroup>
+                  <TableHeader>
+                    <TableRow className="hover:bg-transparent">
+                      <TableHead className="pl-3">Model ID</TableHead>
+                      {USAGE_PRICE_FIELDS.map((field) => (
+                        <TableHead key={field.key}>{field.label}</TableHead>
+                      ))}
+                      <TableHead className="px-1">
+                        <Button
+                          size="icon-xs"
+                          variant="ghost"
+                          aria-label="Add model price"
+                          disabled={locked}
+                          onClick={() => {
+                            const id = `new:${nextRowId.current++}`;
+                            focusRowRef.current = id;
+                            updateDraft({ id, model: "", isNew: true, values: {} });
+                          }}
+                        >
+                          <PlusIcon aria-hidden />
+                        </Button>
+                      </TableHead>
+                    </TableRow>
+                  </TableHeader>
+                  <TableBody>
+                    {rows.length === 0 ? (
+                      <TableRow className="hover:bg-transparent">
+                        <TableCell
+                          colSpan={6}
+                          className="py-8 text-center whitespace-normal text-muted-foreground"
+                        >
+                          {selected.some((environment) => environment.prices === null)
+                            ? "Some environment prices are unavailable."
+                            : "No custom prices. Add a row to override automatic pricing."}
+                        </TableCell>
+                      </TableRow>
+                    ) : (
+                      rows.map((row) => (
+                        <TableRow
+                          key={row.id}
+                          data-row-id={row.id}
+                          className="hover:bg-transparent"
+                        >
+                          <TableCell className="pl-3 whitespace-normal">
+                            {row.isNew ? (
+                              <Input
+                                size="compact"
+                                value={row.model}
+                                ref={(node) => {
+                                  if (node && focusRowRef.current === row.id) {
+                                    node.focus();
+                                    focusRowRef.current = null;
+                                  }
+                                }}
+                                aria-label="New model ID"
+                                aria-invalid={
+                                  (row.model.trim() !== "" && errors.has(row.id)) || undefined
+                                }
+                                list="usage-price-models"
+                                placeholder="Model ID"
+                                autoComplete="off"
+                                spellCheck={false}
+                                disabled={locked}
+                                onChange={(event) =>
+                                  updateDraft({ ...row, model: event.target.value })
+                                }
+                              />
+                            ) : (
+                              <span
+                                className={cn(
+                                  "block break-all",
+                                  row.removed && "text-muted-foreground line-through",
+                                )}
+                              >
+                                {row.model}
+                              </span>
+                            )}
+                            {errors.has(row.id) &&
+                            (row.model.trim() !== "" ||
+                              Object.values(row.values).some((value) => value !== "")) ? (
+                              <p role="alert" className="mt-1 text-xs text-destructive">
+                                {errors.get(row.id)}
+                              </p>
+                            ) : null}
+                          </TableCell>
+                          {row.removed ? (
+                            <TableCell colSpan={4} className="text-muted-foreground">
+                              Automatic pricing after saving
+                            </TableCell>
+                          ) : (
+                            USAGE_PRICE_FIELDS.map((field) => {
+                              const cell = usagePriceCell(selected, row.model, field.key);
+                              return (
+                                <TableCell key={field.key} className="px-1">
+                                  <Input
+                                    size="compact"
+                                    inputMode="decimal"
+                                    aria-label={`${field.label} price for ${row.model || "new model"}`}
+                                    value={row.values[field.key] ?? (row.isNew ? "" : cell.value)}
+                                    placeholder={
+                                      row.isNew
+                                        ? field.optional
+                                          ? "Input rate"
+                                          : "0.00"
+                                        : cell.placeholder
+                                    }
+                                    autoComplete="off"
+                                    disabled={locked}
+                                    className="bg-transparent shadow-none tabular-nums"
M
macroscopeappBotSep 5, 2026

usage/UsagePriceOverrides.tsx:430

Shared primitives: bg-transparent shadow-none overrides the Input primitive's base chrome from the call site. Input already supports opting out (unstyled), and InputGroup variant="ghost" is the existing borderless treatment — prefer one of those, or add a ghost variant to Input, instead of an ad hoc class override on every cell.

No longer relevant as of ef877f722c22f41122711c3a2bf1d2675c12160e

J
Julius MarmingeSep 5, 2026

Fixed in ef877f722. Removed the background and shadow overrides so table cells use the standard compact Input styling and keep its focus and disabled states.

apps/web/src/components/usage/usagePriceTable.ts · Earlier revision f3f881b6 · right 26–26

Original diff
@@ -0,0 +1,84 @@
+import {
+  parseUsagePriceForm,
+  usagePriceForm,
+  USAGE_PRICE_FIELDS,
+  type UsagePriceForm,
+} from "./usagePriceForm";
+import type { UsagePriceChange, UsagePriceTarget } from "./usagePriceTargets";
+
+export type UsagePriceField = (typeof USAGE_PRICE_FIELDS)[number]["key"];
+
+export interface UsagePriceDraft {
+  readonly id: string;
+  readonly model: string;
+  readonly isNew: boolean;
+  readonly values: Partial<Pick<UsagePriceForm, UsagePriceField>>;
+  readonly removed?: boolean;
+}
+
+export function usagePriceCell(
+  targets: readonly UsagePriceTarget[],
+  model: string,
+  field: UsagePriceField,
+) {
+  const optional = USAGE_PRICE_FIELDS.find((entry) => entry.key === field)!.optional;
+  const values = targets.map((target) =>
+    target.prices?.[model] ? usagePriceForm(model, target.prices[model])[field] : null,

apps/web/src/components/usage/usagePriceTable.ts · right 26–26

M
macroscopeappBotSep 5, 2026

🟠 High usage/usagePriceTable.ts:26

usagePriceCell crashes while rendering a model named toString (or another inherited Object.prototype key): target.prices?.[model] returns the inherited function, and usagePriceForm then dereferences missing price fields. Require an own property before reading the price record.

diff
1- target.prices?.[model] ? usagePriceForm(model, target.prices[model])[field] : null,
2+ target.prices && Object.prototype.hasOwnProperty.call(target.prices, model)
3+ ? usagePriceForm(model, target.prices[model])[field]
4+ : null,
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/web/src/components/usage/usagePriceTable.ts around line 26:
2
3`usagePriceCell` crashes while rendering a model named `toString` (or another inherited `Object.prototype` key): `target.prices?.[model]` returns the inherited function, and `usagePriceForm` then dereferences missing price fields. Require an own property before reading the price record.
J
Julius MarmingeSep 5, 2026

Fixed in e540d908c. All table price lookups now require an own record property, including display, edits, and reset. Regression tests cover constructor, toString, and proto as exact model IDs, both before and after a custom price exists. The focused tests, web typecheck, and scoped lint pass.

apps/web/src/components/usage/UsagePriceOverrides.tsx · Earlier revision ef877f72 · right 238

Original diff
@@ -20,299 +20,535 @@ import { Button } from "../ui/button";
 import {
   Dialog,
   DialogDescription,
+  DialogFooter,
   DialogHeader,
   DialogPanel,
   DialogPopup,
   DialogTitle,
 } from "../ui/dialog";
 import { Input } from "../ui/input";
 import { Label } from "../ui/label";
-import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";
-import { parseUsagePriceForm, USAGE_PRICE_FIELDS, usagePriceForm } from "./usagePriceForm";
+import { Menu, MenuCheckboxItem, MenuPopup, MenuSeparator, MenuTrigger } from "../ui/menu";
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table";
+import { Tooltip, TooltipTrigger, TooltipPopup } from "../ui/tooltip";
+import { USAGE_PRICE_FIELDS } from "./usagePriceForm";
+import {
+  isEmptyUsagePriceDraft,
+  usagePriceCell,
+  usagePriceTableChanges,
+  usagePriceTableErrors,
+  type UsagePriceDraft,
+  type UsagePriceField,
+} from "./usagePriceTable";
+import {
+  writeUsagePrices,
+  type UsagePriceTarget,
+  type UsagePriceWriteResult,
+} from "./usagePriceTargets";
 
-export function UsagePriceOverrides({
-  usage,
-}: {
-  readonly usage: readonly EnvironmentUsageStatus[];
-}) {
-  const [open, setOpen] = useState(false);
-  return (
-    <>
-      <Button variant="outline" size="sm" onClick={() => setOpen(true)}>
-        Model prices
-      </Button>
-      {open ? <ModelPricesDialog usage={usage} onOpenChange={setOpen} /> : null}
-    </>
-  );
-}
+const priceTargetsAtom = Atom.make((get): readonly UsagePriceTarget[] =>
+  [...get(environmentPresentations.presentationsAtom)].map(([environmentId, environment]) => {
+    const settings = get(serverEnvironment.settingsValueAtom(environmentId));
+    const session = get(environmentSession.sessionStateAtom(environmentId));
+    const sessionAccess = {
+      session: Option.getOrNull(AsyncResult.value(session)),
+      isPending: session.waiting,
+      hasError: session._tag === "Failure",
+    };
+    const isPrimary = environment.entry.target._tag === "PrimaryConnectionTarget";
+    const access = isPrimary
+      ? resolvePrimaryOperateAccess({ ...sessionAccess, isPrimary, hasDesktopBridge: isElectron })
+      : resolveRemoteOperateAccess(sessionAccess);
+    return {
+      environmentId,
+      label: environment.entry.target.label,
+      prices: settings?.usagePriceOverrides ?? null,
+      unavailable:
+        environment.connection.phase !== "connected"
+          ? "Offline"
+          : settings === null
+            ? "Prices not loaded"
+            : environment.serverConfig?.environment.capabilities.usagePriceOverrides !== true
+              ? "Update server to edit prices"
+              : access === "pending"
+                ? "Checking permissions…"
+                : access === "denied"
+                  ? "Read-only access"
+                  : null,
+    };
+  }),
+);
+
+type SaveAttempt = {
+  readonly drafts: readonly UsagePriceDraft[];
+  readonly destinations: readonly {
+    readonly environmentId: EnvironmentId;
+    readonly label: string;
+  }[];
+  readonly results: ReadonlyMap<
+    EnvironmentId,
+    UsagePriceWriteResult | { readonly status: "saving" }
+  >;
+};
 
-function ModelPricesDialog({
+export function UsagePriceOverrides({
   usage,
+  initialSelectedEnvironmentIds,
   onOpenChange,
 }: {
   readonly usage: readonly EnvironmentUsageStatus[];
+  readonly initialSelectedEnvironmentIds: ReadonlySet<EnvironmentId> | null;
   readonly onOpenChange: (open: boolean) => void;
 }) {
-  const { environments } = useEnvironments();
-  const primaryEnvironmentId = usePrimaryEnvironmentId();
-  const [selectedId, setSelectedId] = useState<EnvironmentId | null>(primaryEnvironmentId);
-  const environment =
-    environments.find((entry) => entry.environmentId === selectedId) ?? environments[0];
-  const summary = usage.find(
-    (entry) => entry.environmentId === environment?.environmentId,
-  )?.summary;
-  const models = [...new Set(summary?.buckets.map((bucket) => bucket.model) ?? [])].sort();
+  const environments = useAtomValue(priceTargetsAtom);
+  const [selectedIds, setSelectedIds] = useState(initialSelectedEnvironmentIds);
+  const selected = environments.filter(
+    (environment) => selectedIds === null || selectedIds.has(environment.environmentId),
+  );
+  const [drafts, setDrafts] = useState<readonly UsagePriceDraft[]>([]);
+  const [pending, setPending] = useState(false);
+  const [attempt, setAttempt] = useState<SaveAttempt | null>(null);
+  const focusRowRef = useRef<string | null>(null);
+  const nextRowId = useRef(0);
+  const updateSettings = useAtomCommand(serverEnvironment.updateSettings, { reportFailure: false });
+  const customModels = [
+    ...new Set(selected.flatMap((environment) => Object.keys(environment.prices ?? {}))),
+  ].sort();
+  const models = [
+    ...new Set([
+      ...customModels,
+      ...usage
+        .filter((environment) =>
+          selected.some((target) => target.environmentId === environment.environmentId),
+        )
+        .flatMap((environment) => environment.summary?.buckets.map((bucket) => bucket.model) ?? []),
+    ]),
+  ].sort();
+  // Keep new rows in place while successful environments publish their updated settings.
+  const newModels = new Set(
+    drafts.filter((draft) => draft.isNew).map((draft) => draft.model.trim()),
+  );
+  const rows: readonly UsagePriceDraft[] = [
+    ...customModels
+      .filter((model) => attempt === null || !newModels.has(model))
+      .map(
+        (model) =>
+          drafts.find((draft) => draft.id === `model:${model}`) ?? {
+            id: `model:${model}`,
+            model,
+            isNew: false,
+            values: {},
+          },
+      ),
+    ...drafts.filter((draft) => draft.isNew),
+  ];
+  const stagedDrafts = drafts.filter((draft) => !isEmptyUsagePriceDraft(draft));
+  const errors = usagePriceTableErrors(selected, stagedDrafts);
+  for (const draft of stagedDrafts) {
+    if (!draft.isNew) continue;
+    if (draft.model.trim() === "") errors.set(draft.id, "Enter a model ID.");
+    else if (
+      attempt === null &&
+      (customModels.includes(draft.model.trim()) ||
+        drafts.some((other) => other.id !== draft.id && other.model.trim() === draft.model.trim()))
+    )
+      errors.set(draft.id, "This model already has a row. Edit its prices there.");
+  }
+  const failedDestinations =
+    attempt?.destinations.filter(
+      (destination) => attempt.results.get(destination.environmentId)?.status === "failed",
+    ) ?? [];
+  const locked = pending || failedDestinations.length > 0;
+  const hasChanges = stagedDrafts.length > 0;
+  const destinationLabel =
+    selected.length === 1 ? selected[0]!.label : `${selected.length} environments`;
+  const selectionLabel = selectedIds === null ? "All environments" : destinationLabel;
+  const discard = () => {
+    setDrafts([]);
+    setAttempt(null);
+  };
+  const selectEnvironments = (ids: ReadonlySet<EnvironmentId> | null) => {
+    setSelectedIds(ids);
+    setAttempt(null);
+  };
+  const updateDraft = (draft: UsagePriceDraft) => {
+    setDrafts((previous) =>
+      previous.some((entry) => entry.id === draft.id)
+        ? previous.map((entry) => (entry.id === draft.id ? draft : entry))
+        : [...previous, draft],
+    );
+    setAttempt(null);
+  };
+  const editCell = (row: UsagePriceDraft, field: UsagePriceField, value: string) => {
+    const values = { ...row.values, [field]: value };
+    const original = usagePriceCell(selected, row.model, field);
+    if (
+      !row.isNew &&
+      original.placeholder !== "Mixed" &&
+      original.placeholder !== "Unavailable" &&
+      value === original.value
+    )
+      delete values[field];
+    if (!row.isNew && Object.keys(values).length === 0)
+      setDrafts((previous) => previous.filter((entry) => entry.id !== row.id));
+    else updateDraft({ ...row, values });
+  };
+  const save = async (retry = false) => {
+    if (pending) return;
+    const destinations = retry ? failedDestinations : selected;
+    const edits = retry && attempt ? attempt.drafts : stagedDrafts;
+    if (destinations.length === 0 || edits.length === 0) return;
+    const targets = destinations.map(
+      (destination) =>
+        environments.find(
+          (environment) => environment.environmentId === destination.environmentId,
+        ) ?? { ...destination, prices: null, unavailable: "Environment removed" },
+    );
+    const changes = new Map(
+      targets.map((target) => [target.environmentId, usagePriceTableChanges(target, edits)]),
+    );
+    setPending(true);
+    setAttempt((previous) => ({
+      drafts: edits,
+      destinations: retry && previous ? previous.destinations : targets,
+      results: new Map([
+        ...(retry && previous ? previous.results : []),
+        ...targets.map((target) => [target.environmentId, { status: "saving" } as const] as const),
+      ]),
+    }));
+    let failed = false;
+    await writeUsagePrices({
+      targets: targets.map((target) => ({
+        ...target,
+        unavailable:
+          target.unavailable ?? [...changes.get(target.environmentId)!.errors.values()][0] ?? null,
+      })),
+      changes: new Map([...changes].map(([id, plan]) => [id, plan.changes])),
+      write: updateSettings,
+      onResult: (environmentId, result) => {
+        if (result.status === "failed") failed = true;
+        setAttempt((previous) =>
+          previous === null
+            ? null
+            : { ...previous, results: new Map(previous.results).set(environmentId, result) },
+        );
+      },
+    });
+    setPending(false);
+    if (!failed) setDrafts([]);
C
cursorBotSep 5, 2026

Retry lock blocks completing prices

Medium Severity

A single-column edit can save on a writable destination while an unavailable one is skipped. After that destination becomes writable without a full override, retry treats the missing required rate as a failed save and locked keeps the table read-only, so the incomplete fields cannot be filled. Discard is the only way out, and it drops the staged change instead of finishing the documented retry.

Additional Locations (2)

Reviewed by Cursor Bugbot for commit ef877f722c22f41122711c3a2bf1d2675c12160e. Configure here.

J
Julius MarmingeSep 5, 2026

Retry deliberately replays the original batch only on failed destinations. The table stays locked after a partial save so changing its values cannot silently alter that batch or write again to successful destinations. An environment without an existing override needs both required rates; a single-column edit cannot create that override. That case reports the missing rate and requires discarding the pending batch, selecting that environment, and entering a complete price. Successful writes remain persisted. I am keeping this explicit recovery path within the approved flow rather than making Retry edit an already partially applied batch.

apps/web/src/components/usage/UsagePriceOverrides.tsx · Earlier revision 32c1d4cb · right 153

Original diff
@@ -1,318 +1,552 @@
 import { useAtomValue } from "@effect/atom-react";
-import type { EnvironmentId, UsageModelPriceOverride } from "@t3tools/contracts";
-import { useState } from "react";
+import type { EnvironmentId } from "@t3tools/contracts";
+import { ChevronDownIcon, PlusIcon, RotateCcwIcon, XIcon } from "lucide-react";
+import * as Option from "effect/Option";
+import { AsyncResult, Atom } from "effect/unstable/reactivity";
+import { useRef, useState } from "react";
 
 import { isElectron } from "../../env";
-import {
-  type EnvironmentPresentation,
-  useEnvironments,
-  usePrimaryEnvironmentId,
-} from "../../state/environments";
+import { cn } from "../../lib/utils";
+import { environmentPresentations } from "../../state/presentation";
 import { serverEnvironment } from "../../state/server";
-import { useEnvironmentSessionState } from "../../state/session";
+import { environmentSession } from "../../state/session";
 import { useAtomCommand } from "../../state/use-atom-command";
 import type { EnvironmentUsageStatus } from "../../state/usage";
 import {
   resolvePrimaryOperateAccess,
   resolveRemoteOperateAccess,
 } from "../settings/ProviderSettingsPanel.logic";
-import { Button } from "../ui/button";
+import { Button, buttonVariants } from "../ui/button";
 import {
   Dialog,
   DialogDescription,
+  DialogFooter,
   DialogHeader,
   DialogPanel,
   DialogPopup,
   DialogTitle,
 } from "../ui/dialog";
 import { Input } from "../ui/input";
 import { Label } from "../ui/label";
-import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";
-import { parseUsagePriceForm, USAGE_PRICE_FIELDS, usagePriceForm } from "./usagePriceForm";
+import { Menu, MenuCheckboxItem, MenuPopup, MenuSeparator, MenuTrigger } from "../ui/menu";
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table";
+import { Tooltip, TooltipTrigger, TooltipPopup } from "../ui/tooltip";
+import { USAGE_PRICE_FIELDS } from "./usagePriceForm";
+import {
+  usagePriceCell,
+  usagePriceTableChanges,
+  usagePriceTableErrors,
+  type UsagePriceDraft,
+  type UsagePriceField,
+} from "./usagePriceTable";
+import {
+  writeUsagePrices,
+  type UsagePriceTarget,
+  type UsagePriceWriteResult,
+} from "./usagePriceTargets";
 
-export function UsagePriceOverrides({
-  usage,
-}: {
-  readonly usage: readonly EnvironmentUsageStatus[];
-}) {
-  const [open, setOpen] = useState(false);
-  return (
-    <>
-      <Button variant="outline" size="sm" onClick={() => setOpen(true)}>
-        Model prices
-      </Button>
-      {open ? <ModelPricesDialog usage={usage} onOpenChange={setOpen} /> : null}
-    </>
-  );
-}
+const priceTargetsAtom = Atom.make((get): readonly UsagePriceTarget[] =>
+  [...get(environmentPresentations.presentationsAtom)].map(([environmentId, environment]) => {
+    const settings = get(serverEnvironment.settingsValueAtom(environmentId));
+    const session = get(environmentSession.sessionStateAtom(environmentId));
+    const sessionAccess = {
+      session: Option.getOrNull(AsyncResult.value(session)),
+      isPending: session.waiting,
+      hasError: session._tag === "Failure",
+    };
+    const isPrimary = environment.entry.target._tag === "PrimaryConnectionTarget";
+    const access = isPrimary
+      ? resolvePrimaryOperateAccess({ ...sessionAccess, isPrimary, hasDesktopBridge: isElectron })
+      : resolveRemoteOperateAccess(sessionAccess);
+    return {
+      environmentId,
+      label: environment.entry.target.label,
+      prices: settings?.usagePriceOverrides ?? null,
+      unavailable:
+        environment.connection.phase !== "connected"
+          ? "Offline"
+          : settings === null
+            ? "Prices not loaded"
+            : environment.serverConfig?.environment.capabilities.usagePriceOverrides !== true
+              ? "Update server to edit prices"
+              : access === "pending"
+                ? "Checking permissions…"
+                : access === "denied"
+                  ? "Read-only access"
+                  : null,
+    };
+  }),
+);
+
+type SaveAttempt = {
+  readonly drafts: readonly UsagePriceDraft[];
+  readonly destinations: readonly {
+    readonly environmentId: EnvironmentId;
+    readonly label: string;
+  }[];
+  readonly results: ReadonlyMap<
+    EnvironmentId,
+    UsagePriceWriteResult | { readonly status: "saving" }
+  >;
+};
 
-function ModelPricesDialog({
+export function UsagePriceOverrides({
   usage,
+  initialSelectedEnvironmentIds,
   onOpenChange,
 }: {
   readonly usage: readonly EnvironmentUsageStatus[];
+  readonly initialSelectedEnvironmentIds: ReadonlySet<EnvironmentId> | null;
   readonly onOpenChange: (open: boolean) => void;
 }) {
-  const { environments } = useEnvironments();
-  const primaryEnvironmentId = usePrimaryEnvironmentId();
-  const [selectedId, setSelectedId] = useState<EnvironmentId | null>(primaryEnvironmentId);
-  const environment =
-    environments.find((entry) => entry.environmentId === selectedId) ?? environments[0];
-  const summary = usage.find(
-    (entry) => entry.environmentId === environment?.environmentId,
-  )?.summary;
-  const models = [...new Set(summary?.buckets.map((bucket) => bucket.model) ?? [])].sort();
+  const environments = useAtomValue(priceTargetsAtom);
+  const [selectedIds, setSelectedIds] = useState(initialSelectedEnvironmentIds);
+  const selected = environments.filter(
+    (environment) => selectedIds === null || selectedIds.has(environment.environmentId),
+  );
+  const [drafts, setDrafts] = useState<readonly UsagePriceDraft[]>([]);
+  const [pending, setPending] = useState(false);
+  const [attempt, setAttempt] = useState<SaveAttempt | null>(null);
+  const focusRowRef = useRef<string | null>(null);
+  const nextRowId = useRef(0);
+  const updateSettings = useAtomCommand(serverEnvironment.updateSettings, { reportFailure: false });
+  const customModels = [
+    ...new Set(selected.flatMap((environment) => Object.keys(environment.prices ?? {}))),
+  ].sort();
+  const models = [
+    ...new Set([
+      ...customModels,
+      ...usage
+        .filter((environment) =>
+          selected.some((target) => target.environmentId === environment.environmentId),
+        )
+        .flatMap((environment) => environment.summary?.buckets.map((bucket) => bucket.model) ?? []),
+    ]),
+  ].sort();
+  // Keep new rows in place while successful environments publish their updated settings.
+  const newModels = new Set(
+    drafts.filter((draft) => draft.isNew).map((draft) => draft.model.trim()),
+  );
+  const rows: readonly UsagePriceDraft[] = [
+    ...customModels
+      .filter((model) => attempt === null || !newModels.has(model))
+      .map(
+        (model) =>
+          drafts.find((draft) => draft.id === `model:${model}`) ?? {
+            id: `model:${model}`,
+            model,
+            isNew: false,
+            values: {},
+          },
+      ),
+    ...drafts.filter((draft) => draft.isNew),
+  ];
+  const errors = usagePriceTableErrors(selected, drafts);
+  for (const draft of drafts) {
+    if (!draft.isNew) continue;
+    if (draft.model.trim() === "") errors.set(draft.id, "Enter a model ID.");
+    else if (
+      attempt === null &&
+      (customModels.includes(draft.model.trim()) ||
+        drafts.some((other) => other.id !== draft.id && other.model.trim() === draft.model.trim()))
+    )
+      errors.set(draft.id, "This model already has a row. Edit its prices there.");
C
cursorBotSep 5, 2026

Empty rows silently block saving

Medium Severity

Adding a model with the plus button inserts an empty draft that is treated as invalid, so Save changes stays disabled. The Enter a model ID message is hidden until the row has content, and Apply to also locks because hasChanges is already true. Other staged edits cannot be saved until that blank row is filled or removed.

Additional Locations (2)

Reviewed by Cursor Bugbot for commit 32c1d4cb358ac90234d659844bc8ae7eed039f22. Configure here.

J
Julius MarmingeSep 5, 2026

Fixed in ef877f722. Completely empty new rows no longer count as staged changes or save-blocking errors. Partially filled rows still validate. A regression test covers valid edits alongside an empty row and a nameless row with an explicit zero. Browser verification confirms Apply to stays available for an empty row and existing edits remain saveable.