Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions apps/web/src/components/ComposerUsageIndicator.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import {
EnvironmentId,
ProviderDriverKind,
ProviderInstanceId,
type ServerProvider,
} from "@t3tools/contracts";
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vite-plus/test";

import { deriveComposerUsage, hasComposerUsageContent } from "../providerUsageAccounts";
import { shouldShowComposerContextStrip } from "./BranchToolbar.logic";
import { ComposerUsageIndicator } from "./ComposerUsageIndicator";

const prime: ServerProvider = {
instanceId: ProviderInstanceId.make("primeAgent"),
driver: ProviderDriverKind.make("primeAgent"),
enabled: true,
installed: true,
version: null,
status: "ready",
auth: { status: "authenticated" },
checkedAt: "2026-09-10T12:00:00.000Z",
models: [
{ slug: "default", name: "Prime Agent Default", isCustom: false, capabilities: null },
{
slug: "prime-inference/claude-haiku-4-5",
name: "Claude Haiku 4.5",
subProvider: "prime-inference",
isCustom: false,
capabilities: null,
},
{
slug: "openai/gpt-5.6",
name: "GPT-5.6",
subProvider: "openai",
isCustom: false,
capabilities: null,
},
{
slug: "openai-codex/gpt-5.6",
name: "GPT-5.6",
subProvider: "openai-codex",
isCustom: false,
capabilities: null,
},
],
slashCommands: [],
skills: [],
};
const codex: ServerProvider = {
...prime,
instanceId: ProviderInstanceId.make("codex"),
driver: ProviderDriverKind.make("codex"),
displayName: "Codex Work",
models: [],
auth: { status: "authenticated", accountId: "codex-account" },
usageLimits: {
source: "codex",
checkedAt: "2026-09-10T12:00:00.000Z",
windows: [{ label: "Session", usedPercent: 37, windowDurationMins: 300 }],
},
};

function renderUsage(
selectedModel: string | null,
options: { enabled?: boolean; backends?: ServerProvider["backends"] } = {},
) {
const usage = deriveComposerUsage({
providerStatuses: [
codex,
{ ...prime, ...(options.backends ? { backends: options.backends } : {}) },
],
selectedInstanceId: prime.instanceId,
selectedModel,
enabled: options.enabled ?? true,
});
// Exercise the gate ChatView uses when there are no other reasons to show the strip.
const showStrip = shouldShowComposerContextStrip({
hasActiveProject: true,
isGitRepo: false,
showEnvironmentIndicator: false,
hostsRestingComposerControls: false,
hasCapacityReading: hasComposerUsageContent(usage),
});
const html = renderToStaticMarkup(
<ComposerUsageIndicator
environmentId={EnvironmentId.make("test-environment")}
usage={usage}
timestampFormat="24-hour"
staleAfterMs={300_000}
/>,
);
return { html, showStrip, usage };
}

describe("ComposerUsageIndicator", () => {
it.each(["prime-inference/claude-haiku-4-5", "openai/gpt-5.6"])(
"keeps the strip visible and explains unreported capacity for %s",
(selectedModel) => {
const { html, showStrip } = renderUsage(selectedModel);

expect(showStrip).toBe(true);
expect(html).toContain(">Capacity not reported for this backend</span>");
expect(html).not.toContain("Codex Work");
expect(html).not.toContain("37%");
expect(html).not.toContain("<button");
},
);

it.each(["default", "prime-inference/unknown", null])(
"renders nothing and leaves the empty strip hidden for %s",
(selectedModel) => {
const { html, showStrip } = renderUsage(selectedModel);
expect(html).toBe("");
expect(showStrip).toBe(false);
},
);

it("hides unreported capacity when provider usage is disabled", () => {
const { html, showStrip } = renderUsage("prime-inference/claude-haiku-4-5", { enabled: false });
expect(html).toBe("");
expect(showStrip).toBe(false);
});

it.each([
{ verification: "assumed", backends: [] },
{
verification: "matched",
backends: [{ backend: "openai-codex", accountId: "codex-account" }],
},
{
verification: "own",
backends: [{ backend: "openai-codex", usageLimits: codex.usageLimits }],
},
])("preserves $verification capacity for a mapped backend", ({ verification, backends }) => {
const { html, showStrip, usage } = renderUsage("openai-codex/gpt-5.6", { backends });
expect(showStrip).toBe(true);
expect(usage.backend?.verification).toBe(verification);
expect(html).toContain(">37%</span>");
expect(html).toContain('aria-label="Subscription capacity for ');
expect(html).not.toContain("Capacity not reported for this backend");
});

it("preserves the distinct account mismatch message without borrowing capacity", () => {
const { html, showStrip, usage } = renderUsage("openai-codex/gpt-5.6", {
backends: [{ backend: "openai-codex", accountId: "different-account" }],
});
expect(showStrip).toBe(true);
expect(usage.backend?.verification).toBe("mismatch");
expect(html).toContain(">Capacity unavailable</span>");
expect(html).toContain('aria-label="Subscription capacity unavailable"');
expect(html).not.toContain("37%");
expect(html).not.toContain("Capacity not reported for this backend");
});
});
10 changes: 10 additions & 0 deletions apps/web/src/components/ComposerUsageIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { getComposerUsageView } from "./ComposerUsageIndicator.logic";
function describeBackend(backend: ComposerUsageBackend, accountCount: number): string {
const runs = `Prime Agent runs ${backend.model} on ${backend.label}.`;
switch (backend.verification) {
case "unreported":
return "Capacity not reported for this backend";
case "own":
return `${runs} This is Prime Agent's own ${backend.label} capacity, read from its sign-in.`;
case "matched":
Expand Down Expand Up @@ -96,6 +98,14 @@ export const ComposerUsageIndicator = memo(function ComposerUsageIndicator({
})();
}, [accounts, environmentId, isRefreshing, refreshProviders]);

if (usage.backend?.verification === "unreported") {
return (
<span className={cn("px-1 py-0.5 text-xs text-muted-foreground/50", className)}>
{describeBackend(usage.backend, accounts.length)}
</span>
);
}

// Prime is signed in to an account that is not configured here: there is
// no number to show, but silence would read as the gauge being broken.
if (!view && usage.backend?.verification !== "mismatch") return null;
Expand Down
46 changes: 46 additions & 0 deletions apps/web/src/components/chat/ModelListRow.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { ProviderDriverKind, ProviderInstanceId } from "@t3tools/contracts";
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vite-plus/test";

import { Combobox } from "../ui/combobox";
import { ModelListRow } from "./ModelListRow";

function renderRow(disabledReason?: string) {
return renderToStaticMarkup(
<Combobox open>
<ModelListRow
index={0}
model={{ slug: "default", name: "Prime Agent Default" }}
instanceId={ProviderInstanceId.make("primeAgent")}
driverKind={ProviderDriverKind.make("primeAgent")}
providerDisplayName="Prime Agent"
isFavorite={false}
isSelected={false}
showProvider
disabledReason={disabledReason ?? null}
onToggleFavorite={() => undefined}
/>
</Combobox>,
);
}

describe("ModelListRow", () => {
it("puts the disabled reason on the actual option and preserves its disabled semantics", () => {
const reason = "Start a new thread to use Prime Agent Default.";
const html = renderRow(reason);
const option = html.match(/<[^>]+role="option"[^>]*>/)?.[0];

expect(option).toBeDefined();
expect(option).toContain('aria-disabled="true"');
expect(option).toContain(`title="${reason}"`);
});

it("leaves an available option enabled without a disabled title", () => {
const html = renderRow();
const option = html.match(/<[^>]+role="option"[^>]*>/)?.[0];

expect(option).toBeDefined();
expect(option).not.toContain('aria-disabled="true"');
expect(option).not.toContain("title=");
});
});
1 change: 1 addition & 0 deletions apps/web/src/components/chat/ModelListRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const ModelListRow = memo(function ModelListRow(props: {
hideIndicator
index={props.index}
value={modelPickerModelKey(props.instanceId, props.model.slug)}
title={props.disabledReason ?? undefined}
disabled={Boolean(props.disabledReason)}
contentClassName="flex w-full items-center gap-3"
className={cn(
Expand Down
37 changes: 36 additions & 1 deletion apps/web/src/providerUsageAccounts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ const PRIME_MODELS: ServerProvider["models"] = [
isCustom: false,
capabilities: null,
},
{
slug: "openai/gpt-5.6",
name: "GPT-5.6",
subProvider: "openai",
isCustom: false,
capabilities: null,
},
{
slug: "prime-inference/qwen",
name: "Qwen",
Expand Down Expand Up @@ -232,9 +239,37 @@ describe("deriveComposerUsage", () => {
expect(usage.backend?.verification).toBe("assumed");
});

it.each(["prime-inference/qwen", "openai/gpt-5.6"])(
"explains unreported capacity for the known backend of %s",
(selectedModel) => {
const usage = deriveComposerUsage({
providerStatuses: ALL,
selectedInstanceId: "primeAgent",
selectedModel,
enabled: true,
});

expect(usage.accounts).toEqual([]);
expect(usage.primary).toBeNull();
expect(usage.backend?.driver).toBeNull();
expect(usage.backend?.verification).toBe("unreported");
expect(hasComposerUsageContent(usage)).toBe(true);
},
);

it("respects disabled usage for a known unmapped backend", () => {
expect(
deriveComposerUsage({
providerStatuses: ALL,
selectedInstanceId: "primeAgent",
selectedModel: "prime-inference/qwen",
enabled: false,
}),
).toBe(EMPTY_COMPOSER_USAGE);
});

it.each([
["Prime's own default", "default"],
["a backend Pylon has no driver for", "prime-inference/qwen"],
["an unknown slug", "anthropic/not-listed"],
["no model", null],
])("shows nothing for %s", (_label, selectedModel) => {
Expand Down
52 changes: 38 additions & 14 deletions apps/web/src/providerUsageAccounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const PRIME_AGENT_DRIVER = ProviderDriverKind.make("primeAgent");
* Prime's backend names, as its model discovery reports them, mapped to the
* Pylon driver whose accounts hold that backend's subscription. Backends
* without a Pylon driver (Prime Inference, plain OpenAI keys) have no
* capacity to show and are left out.
* capacity to show; the composer explains that reporting is unavailable.
*/
const PRIME_AGENT_BACKEND_DRIVERS: Readonly<Record<string, ProviderDriverKind>> = {
anthropic: ProviderDriverKind.make("claudeAgent"),
Expand All @@ -48,21 +48,22 @@ export type ComposerUsageVerification =
/** Prime is signed in to a different account than any configured here. */
| "mismatch";

export interface ComposerUsageBackend {
readonly driver: ProviderDriverKind;
/** Brand label for the driver, e.g. "Claude". */
export type ComposerUsageBackend = {
/** Brand label for the driver, or the unmapped backend name. */
readonly label: string;
/** Display name of the Prime model the capacity is being shown for. */
readonly model: string;
readonly verification: ComposerUsageVerification;
}
} & (
| { readonly driver: ProviderDriverKind; readonly verification: ComposerUsageVerification }
| { readonly driver: null; readonly verification: "unreported" }
);

export interface ComposerUsage {
/** Accounts the popover compares. Empty when nothing reports capacity. */
readonly accounts: ReadonlyArray<ProviderUsageAccount>;
/** The account the strip shows, or null when there is nothing to show. */
readonly primary: ProviderUsageAccount | null;
/** Set when the composer targets Prime Agent and capacity comes from its backend. */
/** The selected Prime backend and whether its capacity can be attributed or reported. */
readonly backend: ComposerUsageBackend | null;
}

Expand All @@ -77,9 +78,11 @@ export const EMPTY_COMPOSER_USAGE: ComposerUsage = { accounts: [], primary: null
* there is one, so any instant answers this question.
*/
export function hasComposerUsageContent(usage: ComposerUsage): boolean {
// Prime signed in elsewhere still reports that, and saying so is content.
// Explanations of unavailable capacity keep the strip visible too.
return (
usage.backend?.verification === "mismatch" || getComposerUsageView(usage.primary, 0) !== null
usage.backend?.verification === "mismatch" ||
usage.backend?.verification === "unreported" ||
getComposerUsageView(usage.primary, 0) !== null
);
}

Expand All @@ -106,15 +109,21 @@ function accountsForDriver(
});
}

/** The Prime model the composer has selected, when its backend has a Pylon driver. */
/** The selected Prime model with a known backend, including unmapped backends. */
function primeAgentBackend(
prime: ServerProvider,
selectedModel: string | null | undefined,
): { readonly driver: ProviderDriverKind; readonly model: ServerProviderModel } | null {
): {
readonly driver: ProviderDriverKind | null;
readonly model: ServerProviderModel & { readonly subProvider: string };
} | null {
if (!selectedModel) return null;
const model = prime.models.find((candidate) => candidate.slug === selectedModel);
const driver = model?.subProvider ? PRIME_AGENT_BACKEND_DRIVERS[model.subProvider] : undefined;
return model && driver ? { driver, model } : null;
if (!model?.subProvider) return null;
return {
driver: PRIME_AGENT_BACKEND_DRIVERS[model.subProvider] ?? null,
model: { ...model, subProvider: model.subProvider },
};
}

/**
Expand Down Expand Up @@ -194,7 +203,22 @@ export function deriveComposerUsage(input: {
if (selected.driver === PRIME_AGENT_DRIVER) {
const backend = primeAgentBackend(selected, input.selectedModel);
if (!backend) return EMPTY_COMPOSER_USAGE;
return primeAgentUsage(input.providerStatuses, selected, backend);
if (!backend.driver) {
return {
accounts: [],
primary: null,
backend: {
driver: null,
label: backend.model.subProvider,
model: backend.model.shortName ?? backend.model.name,
verification: "unreported",
},
};
}
return primeAgentUsage(input.providerStatuses, selected, {
driver: backend.driver,
model: backend.model,
});
}

const accounts = accountsForDriver(input.providerStatuses, selected.driver, selected.instanceId);
Expand Down
Loading