Repository navigation
feat(composer): show where the context window went, in the composer - #625
Merged
Merged
Conversation
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
force-pushed
the
claude/composer-context-breakdown-ui-e6324e
branch
from
August 13, 2026 19:57
6b18198 to
c9f645c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two read endpoints have shipped for a while and nothing rendered either one.
/contextanswers how full is the window;/context/occupancyanswerswho filled it. This adds the control that asks both, next to send.
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 300pxbreakdown that matches the model popover's frame.
The breakdown is grouped by lifecycle and coloured by class, and that is the
whole idea.
segment_classis a structural taxonomy — it answers what kind ofbytes are these.
lifecycleanswers what happens if I do nothing:residentbytes are rent charged on every call and are fixed by trimming a surface;
per_resultbytes are a multiplier on tool-call count and are fixed by shrinkingone 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_pctis rendered verbatim and the unit is named ("60% free"), sono
100 - xhappens on the client and a bare percent next to a filling gaugecannot 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.
track, and no percent anywhere.
provider_input_tokens(authoritative, matches billing);segments sum to our estimate, and the signed
unattributed_deltagets itsown row instead of being absorbed.
undeclared_tokenssurfaces in the popover only — it is a first-party contractdefect, 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_occupancyis in the event vocabulary andevents.pyprojects a payloadand an activity_kind for it — but nothing emits it. The type is absent from
runtime_worker/streaming_executor.py, where its siblingusage_recordedisemitted. A selector over
session.eventswould have been correct, tested, greenand permanently blank.
So the cockpit reads the API once per turn boundary through the existing
Transportport 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 alreadya 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
packages/chat-surface/src/composer(60 new).getComputedStyleagainst the realstylesheet — jsdom performs no layout, and this control must never become the
row's ellipsis target or push send onto a second line.
composer_and_budgets.py) drives it against the packaged app: itasks 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-surfacesuite (ChatsArchive,canvasLifecycle)are pre-existing on the base — verified by stashing.
Known follow-ups
onOpenReportis left unwired, so no link renders. The L3 report alreadyexists as
ContextPanel(by_call+by_subagent) but lives on thedeprecated web ChatScreen and is a no-op on desktop; porting it into
chat-surfaceis the follow-up rather than deep-linking a dead surface.a timer in a presentational component, and the same fact is in the popover
footer.
🤖 Generated with Claude Code