feat: use session-reported Claude CLI context windows
This commit is contained in:
@@ -37,6 +37,7 @@ import { useServerEvent, useServerResync } from "@/components/server-events";
|
||||
import {
|
||||
isProviderKeyRefusal,
|
||||
firstSourceTextParts,
|
||||
cliContextWindowOf,
|
||||
messageAnchorId,
|
||||
textPartMessageId,
|
||||
kbdEventForAction as kbdEventForChat,
|
||||
@@ -1690,6 +1691,7 @@ export function Chat({
|
||||
phase={phases.at(-1) ?? "probe"}
|
||||
turns={turns}
|
||||
contextTokens={contextTokens}
|
||||
cliContextWindow={cliContextWindowOf(nativeMessages)}
|
||||
pricing={pricing}
|
||||
onReserve={setDockReserve}
|
||||
dictationSlot={<DictationBar dictation={dictation} />}
|
||||
@@ -1916,6 +1918,7 @@ function DockOverlay({
|
||||
phase,
|
||||
turns,
|
||||
contextTokens,
|
||||
cliContextWindow,
|
||||
pricing,
|
||||
composer,
|
||||
onSubmitAnswer,
|
||||
@@ -1952,6 +1955,7 @@ function DockOverlay({
|
||||
/** The session's usage log — the stats bar beneath the composer folds it. */
|
||||
turns: TurnUsage[];
|
||||
contextTokens: number | null;
|
||||
cliContextWindow: ReturnType<typeof cliContextWindowOf>;
|
||||
pricing?: SessionPricing;
|
||||
composer: React.ReactNode;
|
||||
onSubmitAnswer: (output: QuestionAnswerData) => void;
|
||||
@@ -2407,6 +2411,7 @@ function DockOverlay({
|
||||
<StatsBar
|
||||
turns={turns}
|
||||
contextTokens={contextTokens}
|
||||
cliContextWindow={cliContextWindow}
|
||||
pricing={pricing}
|
||||
dictationSlot={dictationSlot}
|
||||
chatId={chatId}
|
||||
|
||||
@@ -13,15 +13,12 @@ import { useModelCatalog } from "@/components/model-catalog";
|
||||
export { useModelCatalog } from "@/components/model-catalog";
|
||||
import { defaultOrderLevel, populateLevel, THINKING_LABEL } from "@/components/thinking-default";
|
||||
|
||||
/** provider:id → contextWindow over the cached catalog — the client-side
|
||||
* twin of the server's catalogContextLimits join (context-meter
|
||||
* amendment, owner 2026-09-23: pi's live catalog is the ONLY limit
|
||||
* source — no hardcoded fallback exists anywhere, so a model missing
|
||||
* from the index reads limit-null and the meter renders usage-only,
|
||||
* the honest-render precedent). Consumed by the stats bar: the same
|
||||
* cached /api/models fetch the picker rides feeds the denominator. */
|
||||
/** provider:id → known contextWindow over the cached catalog, mirroring
|
||||
* the server's catalogContextLimits join. CLI aliases have no global
|
||||
* denominator: the stats bar uses that session's actual CLI report instead.
|
||||
* Missing limits always render usage-only, without a hardcoded fallback. */
|
||||
export function catalogLimits(catalog: Pick<ModelCatalogResponse, "models">): Map<string, number> {
|
||||
return new Map(catalog.models.map((m) => [`${m.provider}:${m.id}`, m.contextWindow]));
|
||||
return new Map(catalog.models.flatMap((m) => m.contextWindow === null ? [] : [[`${m.provider}:${m.id}`, m.contextWindow] as const]));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -79,7 +79,7 @@ export function ModelMenuList({
|
||||
</span>
|
||||
<span className="flex items-center justify-between gap-2 text-muted-foreground">
|
||||
<span className="truncate">{subscriptionLabel(`${m.provider}:${m.id}`) ? `${subscriptionLabel(`${m.provider}:${m.id}`)} (subscription)` : m.provider} · {m.id}</span>
|
||||
<span className="shrink-0 tabular-nums">{formatTokens(m.contextWindow)}</span>
|
||||
<span className="shrink-0 tabular-nums">{m.contextWindow === null ? "—" : formatTokens(m.contextWindow)}</span>
|
||||
</span>
|
||||
</>}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
||||
import { subscriptionLabels, subscriptionLabel, type SessionPricing, type TurnUsage } from "@/src/core";
|
||||
import { CLAUDE_CODE_PROVIDER, subscriptionLabels, subscriptionLabel, type cliContextWindowOf, type SessionPricing, type TurnUsage } from "@/src/core";
|
||||
import { formatTokens, formatUsd, planCostLabel, prettyModel } from "@/components/usage-format";
|
||||
import { ModelControl, catalogLimits, useModelCatalog } from "@/components/model-picker";
|
||||
|
||||
@@ -10,6 +10,15 @@ export function formatContextMeasurement(tokens: number | null, limit: number |
|
||||
return limit === null ? numerator : `${numerator} / ${formatTokens(limit)}`;
|
||||
}
|
||||
|
||||
/** CLI aliases have no global catalog limit: only this session's matching
|
||||
* response can supply a denominator. Other providers keep their catalog join. */
|
||||
export function contextLimitForModel(model: string | undefined, limits: ReadonlyMap<string, number> | undefined,
|
||||
cliWindow: ReturnType<typeof cliContextWindowOf> = null): number | null {
|
||||
if (!model) return null;
|
||||
if (model.startsWith(`${CLAUDE_CODE_PROVIDER}:`)) return cliWindow?.model === model ? cliWindow.tokens : null;
|
||||
return limits?.get(model) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The below-composer stats bar (wave 4 observability): the session's
|
||||
* money/context footprint as one quiet row beneath the composer —
|
||||
@@ -24,12 +33,14 @@ export function formatContextMeasurement(tokens: number | null, limit: number |
|
||||
* Spending arrives already priced by the server through Pi's catalog and
|
||||
* calculator. Missing cost stays "—", never fake zero. Context is a separate
|
||||
* completed-conversation measurement, never a money-log/summarizer fold or
|
||||
* transcript estimate. Its catalog denominator follows the selected model;
|
||||
* model changes preserve the numerator. Successful compaction clears it to ?.
|
||||
* transcript estimate. Its denominator follows the selected model, from the
|
||||
* catalog or the session's CLI report; model changes preserve the numerator.
|
||||
* Successful compaction clears the numerator and any old CLI report.
|
||||
*/
|
||||
export function StatsBar({
|
||||
turns,
|
||||
contextTokens = null,
|
||||
cliContextWindow = null,
|
||||
pricing,
|
||||
dictationSlot,
|
||||
chatId,
|
||||
@@ -38,6 +49,7 @@ export function StatsBar({
|
||||
}: {
|
||||
turns: TurnUsage[];
|
||||
contextTokens?: number | null;
|
||||
cliContextWindow?: ReturnType<typeof cliContextWindowOf>;
|
||||
pricing?: SessionPricing;
|
||||
/** The bound session (per-session models, owner 2026-09-28): present
|
||||
* on the in-session bar — the model control binds THE SESSION'S own
|
||||
@@ -86,11 +98,11 @@ export function StatsBar({
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
}, [open]);
|
||||
// Catalog-only denominator; the picker reports its effective selection.
|
||||
// Missing/pending catalog means usage-only, never a hardcoded maximum.
|
||||
// The picker reports its effective selection. CLI windows are session-
|
||||
// scoped; unknown metadata always means usage-only, never a guessed maximum.
|
||||
const { catalog } = useModelCatalog();
|
||||
const limits = useMemo(() => (catalog ? catalogLimits(catalog) : undefined), [catalog]);
|
||||
const limit = selectedModel ? limits?.get(selectedModel) ?? null : null;
|
||||
const limit = contextLimitForModel(selectedModel, limits, cliContextWindow);
|
||||
const contextLabel = formatContextMeasurement(contextTokens, limit);
|
||||
const { cost = 0, unpriced = turns.length, subscription = 0 } = pricing ?? {};
|
||||
// Honest cost: show dollars only when at least one turn priced;
|
||||
@@ -151,6 +163,9 @@ export function StatsBar({
|
||||
{contextLabel}
|
||||
</span>
|
||||
</div>
|
||||
{selectedModel?.startsWith(`${CLAUDE_CODE_PROVIDER}:`) && (
|
||||
<p>The window is reported by Claude Code’s local summary and may reflect its auto-compaction budget rather than the model’s full maximum. Unavailable windows are omitted.</p>
|
||||
)}
|
||||
<div className="flex justify-between gap-2">
|
||||
<span>{subscription ? "Usage" : "Total cost"}</span>
|
||||
<span className="tabular-nums text-card-foreground">{costLabel}</span>
|
||||
|
||||
Reference in New Issue
Block a user