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
4 changes: 2 additions & 2 deletions apps/server/src/provider/Layers/ClaudeAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -803,8 +803,8 @@ function isClaudeDeferredCategory(
return category.isDeferred === true || category.name.trim().toLowerCase().endsWith("(deferred)");
}

/** Keeps the SDK's category order: legend colors are assigned by index, so
* reordering would make them flicker between updates. */
/** Keeps the SDK's category order so snapshot equality stays a cheap
* element-wise compare; the client sorts for display and colors by name. */
function normalizeClaudeContextCategories(
value: SDKControlGetContextUsageResponse["categories"] | undefined,
): ThreadTokenUsageSnapshot["contextCategories"] {
Expand Down
6 changes: 6 additions & 0 deletions apps/web/src/components/chat/ContextWindowMeter.browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,12 @@ describe("ContextWindowMeter", () => {

await expect.element(page.getByText(/1.2m of 1.3m input tokens/)).toBeVisible();
expect(contextSegmentNames()).toEqual(["Messages", "System tools"]);
// Colors follow the category name, not its position in the provider's list.
expect(
document
.querySelector('[data-context-segment="Messages"]')
?.classList.contains("bg-primary"),
).toBe(true);

const breakdownToggle = page.getByRole("button", { name: "Show context breakdown" });
await expect.element(page.getByText("System tools")).not.toBeInTheDocument();
Expand Down
41 changes: 24 additions & 17 deletions apps/web/src/components/chat/ContextWindowMeter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,27 @@ function AccountUsageBar(props: {
);
}

/** Categorical swatches assigned by index (cycled), so a category keeps its
* color as long as the provider keeps its order. */
const CONTEXT_CATEGORY_COLOR_CLASS_NAMES = [
"bg-context-cat-1",
"bg-context-cat-2",
"bg-context-cat-3",
"bg-context-cat-4",
"bg-context-cat-5",
"bg-context-cat-6",
"bg-context-cat-7",
"bg-context-cat-8",
] as const;
/** Swatches keyed by the provider's category name so a category looks the same
* in every thread: conversation content is the primary color, the things the
* session was given (prompt, tools, memory, skills) each get a fixed hue, and
* reserved-but-empty space is gray. Unknown names share one fallback. */
const CONTEXT_CATEGORY_COLOR_CLASS_NAMES: Readonly<Record<string, string>> = {
messages: "bg-primary",
"system prompt": "bg-context-system-prompt",
"system tools": "bg-context-system-tools",
"mcp tools": "bg-context-mcp-tools",
"memory files": "bg-context-memory-files",
skills: "bg-context-skills",
"autocompact buffer": "bg-muted-foreground/45",
};
const CONTEXT_CATEGORY_FALLBACK_COLOR_CLASS_NAME = "bg-context-other";

function contextCategoryColorClassName(name: string): string {
return (
CONTEXT_CATEGORY_COLOR_CLASS_NAMES[name.trim().toLowerCase()] ??
CONTEXT_CATEGORY_FALLBACK_COLOR_CLASS_NAME
);
}

type ContextBreakdownSegment = {
readonly key: string;
Expand All @@ -89,17 +98,15 @@ function buildContextBreakdownSegments(
const toPercentage = (tokens: number) => Math.max(0, Math.min(100, (tokens / maxTokens) * 100));
const categories = usage.contextCategories ?? null;
if (categories && categories.length > 0) {
// Colors are keyed to the provider's order (stable across updates), then
// the bar and legend display largest-first.
// The bar and legend display largest-first; colors come from the name, so
// the sort never reshuffles them.
return categories
.map((category, index) => ({
key: `${index}-${category.name}`,
name: category.name,
tokens: category.tokens,
percentage: toPercentage(category.tokens),
colorClassName:
CONTEXT_CATEGORY_COLOR_CLASS_NAMES[index % CONTEXT_CATEGORY_COLOR_CLASS_NAMES.length] ??
CONTEXT_CATEGORY_COLOR_CLASS_NAMES[0],
colorClassName: contextCategoryColorClassName(category.name),
}))
.sort((left, right) => right.tokens - left.tokens);
}
Expand Down
47 changes: 21 additions & 26 deletions apps/web/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,12 @@
--color-secondary: var(--secondary);
--color-provider-claude: var(--provider-claude);
--color-provider-codex: var(--provider-codex);
--color-context-cat-1: var(--context-cat-1);
--color-context-cat-2: var(--context-cat-2);
--color-context-cat-3: var(--context-cat-3);
--color-context-cat-4: var(--context-cat-4);
--color-context-cat-5: var(--context-cat-5);
--color-context-cat-6: var(--context-cat-6);
--color-context-cat-7: var(--context-cat-7);
--color-context-cat-8: var(--context-cat-8);
--color-context-system-prompt: var(--context-system-prompt);
--color-context-system-tools: var(--context-system-tools);
--color-context-mcp-tools: var(--context-mcp-tools);
--color-context-memory-files: var(--context-memory-files);
--color-context-skills: var(--context-skills);
--color-context-other: var(--context-other);
--color-primary-graph: var(--primary-graph);
--color-primary-readable: var(--primary-readable);
--color-primary-foreground: var(--primary-foreground);
Expand Down Expand Up @@ -256,16 +254,15 @@
--success-foreground: var(--color-emerald-700);
--warning: var(--color-amber-500);
--warning-foreground: var(--color-amber-700);
/* Categorical swatches for breakdown bars (context window usage). Assigned by
index, so the hues stay far enough apart to stay distinguishable in order. */
--context-cat-1: oklch(0.55 0.14 255);
--context-cat-2: oklch(0.55 0.14 55);
--context-cat-3: oklch(0.55 0.12 155);
--context-cat-4: oklch(0.55 0.12 95);
--context-cat-5: oklch(0.55 0.13 5);
--context-cat-6: oklch(0.55 0.12 300);
--context-cat-7: oklch(0.55 0.1 200);
--context-cat-8: oklch(0.55 0.09 30);
/* Context window breakdown swatches, keyed by category name. Messages use
--primary; these cover what the session was given. Shared lightness and
chroma so no category shouts. */
--context-system-prompt: oklch(0.55 0.12 300);
--context-system-tools: oklch(0.55 0.14 55);
--context-mcp-tools: oklch(0.55 0.1 200);
--context-memory-files: oklch(0.55 0.12 155);
--context-skills: oklch(0.55 0.13 5);
--context-other: oklch(0.55 0.12 95);
/* Matches the macOS default text-selection blue so ::selection and the kept
transcript-note highlight paint identically. */
--text-selection: oklch(0.85 0.07 258);
Expand Down Expand Up @@ -323,14 +320,12 @@
--success-foreground: var(--color-emerald-400);
--warning: var(--color-amber-500);
--warning-foreground: var(--color-amber-400);
--context-cat-1: oklch(0.62 0.14 255);
--context-cat-2: oklch(0.68 0.14 55);
--context-cat-3: oklch(0.65 0.12 155);
--context-cat-4: oklch(0.72 0.12 95);
--context-cat-5: oklch(0.65 0.13 5);
--context-cat-6: oklch(0.62 0.12 300);
--context-cat-7: oklch(0.68 0.1 200);
--context-cat-8: oklch(0.66 0.09 30);
--context-system-prompt: oklch(0.62 0.12 300);
--context-system-tools: oklch(0.68 0.14 55);
--context-mcp-tools: oklch(0.68 0.1 200);
--context-memory-files: oklch(0.65 0.12 155);
--context-skills: oklch(0.65 0.13 5);
--context-other: oklch(0.72 0.12 95);
--text-selection: oklch(0.49 0.07 258);
}
}
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/lib/contextWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ function asBoolean(value: unknown): boolean | null {

type ContextWindowCategories = NonNullable<ThreadTokenUsageSnapshot["contextCategories"]>;

/** Keeps the provider's order (legend colors are assigned by index) and drops
* entries the provider sent malformed rather than failing the whole snapshot. */
/** Keeps the provider's order and drops entries the provider sent malformed
* rather than failing the whole snapshot. */
function asContextCategories(value: unknown): ContextWindowCategories | null {
if (!Array.isArray(value)) {
return null;
Expand Down
Loading