Skip to content

feat(composer): show where the context window went, in the composer - #625

Merged
0x-copilot-dev merged 1 commit into
devfrom
claude/composer-context-breakdown-ui-e6324e
Aug 13, 2026
Merged

0x-copilot-dev merged 1 commit into
devfrom
claude/composer-context-breakdown-ui-e6324e

Conversation

@0x-copilot-dev

Copy link
Copy Markdown
Owner

Two read endpoints have shipped for a while and nothing rendered either one.
/context answers how full is the window; /context/occupancy answers
who filled it. This adds the control that asks both, next to send.

ContextPill

The design

Three disclosure levels over one pill — the resting meter (22px gauge + the
server's headroom percent), a hover line (in · cached · free), and a 300px
breakdown that matches the model popover's frame.

The breakdown is grouped by lifecycle and coloured by class, and that is the
whole idea. segment_class is a structural taxonomy — it answers what kind of
bytes are these
. lifecycle answers what happens if I do nothing: resident
bytes are rent charged on every call and are fixed by trimming a surface;
per_result bytes are a multiplier on tool-call count and are fixed by shrinking
one note. Read by class, two connectors whose schemas cost 17% of the window
every single turn are an unremarkable "tools 19%".

What it refuses to state

Most of the work is in the negative cases:

  • headroom_pct is rendered verbatim and the unit is named ("60% free"), so
    no 100 - x happens on the client and a bare percent next to a filling gauge
    cannot be read backwards. The gauge draws consumed segments; the track behind
    it is that headroom — two independent server values that agree, rather than
    one derived from the other.
  • Unknown window (model absent from pricing) → absolute tokens, an empty
    track, and no percent anywhere.
  • Nothing measured → no pill. No skeleton, no spinner, no zero.
  • The headline is provider_input_tokens (authoritative, matches billing);
    segments sum to our estimate, and the signed unattributed_delta gets its
    own row instead of being absorbed.
  • undeclared_tokens surfaces in the popover only — it is a first-party contract
    defect, not something the user did.
  • third_party / stable_prefix / counter_source: "proxy" each get a marker:
    "large but cached" and "large and re-billed every turn" are different findings,
    and a proxy count is the fail-open signature.

Not a stream projection, deliberately

context_occupancy is in the event vocabulary and events.py projects a payload
and an activity_kind for it — but nothing emits it. The type is absent from
runtime_worker/streaming_executor.py, where its sibling usage_recorded is
emitted. A selector over session.events would have been correct, tested, green
and permanently blank.

So the cockpit reads the API once per turn boundary through the existing
Transport port and hands the built view to whichever composer the host mounts.
Both hosts share the mapping rather than duplicating a fetch — the model-catalog
failure mode.

Wiring the emitter would make the meter live mid-run. It is a follow-up, not a
blocker: the question the meter answers is asked between turns.

Tokens

Aliases over the existing --surface-hue-* ramp, not new colours — it is already
a perceptually-even categorical set with a light-ground step-down. Cool rungs
only: amber and ember carry pressure on this component, so a warm segment would
read as an alarm when it means nothing but "which class".

Tests

  • 335 passing in packages/chat-surface/src/composer (60 new).
  • The shrink contract is asserted with getComputedStyle against the real
    stylesheet — jsdom performs no layout, and this control must never become the
    row's ellipsis target or push send onto a second line.
  • CB-8 (composer_and_budgets.py) drives it against the packaged app: it
    asks the running app what the occupancy ledger recorded, then checks the pixel
    agrees. That pairing is the point — every unit test here runs on fixtures and
    would pass over a stack that never writes a row.

Two failures in the full chat-surface suite (ChatsArchive, canvasLifecycle)
are pre-existing on the base — verified by stashing.

Known follow-ups

  • onOpenReport is left unwired, so no link renders. The L3 report already
    exists as ContextPanel (by_call + by_subagent) but lives on the
    deprecated web ChatScreen and is a no-op on desktop; porting it into
    chat-surface is the follow-up rather than deep-linking a dead surface.
  • The transient "just compacted" pill state from the mock was dropped — it needs
    a timer in a presentational component, and the same fact is in the popover
    footer.

🤖 Generated with Claude Code

Two read endpoints have shipped for a while and nothing rendered either
one. `/context` answers "how full is the window"; `/context/occupancy`
answers "who filled it". This adds the control that asks both, in the
place the question actually gets asked — next to send.

Three disclosure levels over one pill: the resting meter (a 22px gauge
plus the server's headroom percent), a hover line (in · cached · free),
and a 300px breakdown.

The breakdown is grouped by LIFECYCLE and coloured by CLASS, and that is
the whole design. `segment_class` is a structural taxonomy and answers
"what kind of bytes are these"; `lifecycle` answers "what happens if I do
nothing" — `resident` bytes are rent charged on every call and are fixed
by trimming a surface, `per_result` bytes are a multiplier on tool-call
count and are fixed by shrinking one note. Read by class, two connectors
whose schemas cost 17% of the window every turn are an unremarkable
"tools 19%".

What the meter refuses to state is most of the work:

- `headroom_pct` is rendered VERBATIM and the unit is named ("60% free"),
  so no `100 - x` happens on the client and a bare percent next to a
  filling gauge cannot be read backwards. The gauge draws consumed
  segments and the track behind it is that headroom — two independent
  server values that agree, rather than one derived from the other.
- An unknown window (model absent from pricing) shows absolute tokens,
  an empty track, and NO percent anywhere.
- Nothing measured renders no pill: no skeleton, no spinner, no zero.
- The headline is `provider_input_tokens` (authoritative, matches
  billing); segments sum to our estimate and the signed
  `unattributed_delta` gets its own row instead of being absorbed.
- `undeclared_tokens` surfaces in the popover only — it is a first-party
  contract defect, not something the user did.
- `third_party` / `stable_prefix` / `counter_source: "proxy"` each get a
  marker, because "large but cached" and "large and re-billed every turn"
  are different findings, and a proxy count is the fail-open signature.

NOT a stream projection, deliberately. `context_occupancy` is in the
event vocabulary and `events.py` projects a payload and an activity_kind
for it, but nothing emits it — the type is absent from
`runtime_worker/streaming_executor.py` where its sibling `usage_recorded`
is emitted. A selector over `session.events` would have been correct,
tested, green and permanently blank. The cockpit reads the API once per
turn boundary instead, through the existing Transport port, and hands the
built view to whichever composer the host mounts — so the two hosts share
the mapping rather than duplicating a fetch.

Tokens are ALIASES over the existing `--surface-hue-*` ramp, not new
colours: it is already a perceptually-even categorical set with a
light-ground step-down. Cool rungs only — amber and ember carry pressure
on this component, and a warm segment would read as an alarm when it
means nothing but "which class".

CB-8 drives it against the packaged app: it asks the running app what the
occupancy ledger recorded, then checks the pixel agrees. That pairing is
the point — every unit test here runs on fixtures and would pass over a
stack that never writes a row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@parthpahwa1
parthpahwa1 force-pushed the claude/composer-context-breakdown-ui-e6324e branch from 6b18198 to c9f645c Compare August 13, 2026 19:57
@0x-copilot-dev
0x-copilot-dev merged commit af10323 into dev Aug 13, 2026
13 checks passed
@0x-copilot-dev
0x-copilot-dev deleted the claude/composer-context-breakdown-ui-e6324e branch August 13, 2026 20:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant