feat(desktop): show context usage beside the model controls - #4576
Conversation
07eb5d4 to
a4df755
Compare
bb6730c to
fd09f45
Compare
4768bc1 to
5287d9e
Compare
5287d9e to
e091fce
Compare
e091fce to
3452f8b
Compare
|
Changed after looking at it running: the indicator now shows a percentage whenever a window is available from either source — the user's declaration first, otherwise the window the model reports — with The earlier behaviour reserved the percentage for a declared window, which left the common case (nothing declared) showing a bare token count that means little on its own. The declaration-only rule belongs to the compaction threshold, which a reported window must never arm; it does not belong to a read-only display, and |
M4n5ter
left a comment
There was a problem hiding this comment.
English
Blocking: the indicator is reading request state from the rendered transcript slice, not from the session tail
ChatComposerRegion finds the last token_usage in messages and combines its anchor with the currently selected model's context window. Those values are not guaranteed to describe the same request:
- Manual
/compactappends an anchorless synthetictoken_usagerow. The runtime selector deliberately skips that row and keeps the last real request anchor, but this selector stops on it and shows—. - Switching the model or connection immediately changes
activeModelChoice, while the transcript still contains the previous route's anchor. The UI can therefore divide tokens produced by model A's tokenizer by model B's window (for example, 100k / 16k = 625%). The runtime already treats these anchors as route-specific and rejects them across(connectionId, model)changes. messagesis only the currently loaded transcript range. Restoring or browsing an older range can make this control report historical usage as the session's latest usage.
These are normal supported states, so the displayed number can look precise while being unrelated to the active request. This needs one source-of-truth fix rather than separate UI guards: derive a route-validated latest-request usage projection from the session tail (reusing the invariant enforced by persistedRequestAnchor) and pass that small projection into the composer. If the active route has no valid provider usage, show —.
Please add behavior coverage for at least model/connection switching, an anchorless /compact row, and a loaded range with newer transcript data.
Simplification audit: once this is fixed at the owner, the broad messages prop and the duplicate structural LastRequestAnchor shape can be removed from ChatComposerRegion. I did not find another decision-relevant simplification issue in this patch.
Verification: the focused core test passed (13/13), renderer architecture checks passed (71/71), the AppShell hook check passed, and git diff --check passed. GitHub CI was still running when this review was posted.
中文
阻塞问题:这里拿到的是“当前画面里最后一条 usage”,不是“这个 session 最近一次有效请求的 usage”
ChatComposerRegion 先从 messages 里倒着找 token_usage,再拿当前选中模型的窗口去算比例。这两个数据不一定属于同一次请求:
- 手动
/compact会追加一条没有 anchor 的合成token_usage。runtime 现有逻辑会跳过它,继续使用上一次真实请求;这里却会停在这条记录上,直接显示—。 - 切换模型或连接后,模型窗口马上变了,但 transcript 里还是旧模型的 anchor。于是界面可能拿 A 模型 tokenizer 算出的 token 数,除以 B 模型的窗口,甚至显示出 625% 这种看似准确、实际没意义的数字。runtime 已经明确把 anchor 限定在同一个
(connectionId, model)上,这里绕开了这个约束。 messages只是当前加载出来的 transcript 区间。用户翻到历史区间时,这个控件会把历史 usage 当成 session 的最新 usage。
这些都不是罕见边界,而是正常操作路径。不要在组件里分别打补丁;应该由掌握 session 尾部和当前路由的那一层,产出一份已经校验过的“最近请求 usage”,再把这个小对象传给 composer。当前模型/连接没有有效 provider usage 时就显示 —。
测试至少要覆盖:切换模型/连接、遇到没有 anchor 的 /compact usage,以及当前只加载了历史区间但后面还有新消息。
简化审计的结论也指向同一个修复:状态归属放对以后,ChatComposerRegion 不需要接收整个 messages,也不需要自己再定义一份 LastRequestAnchor 结构。除此之外,这个补丁里没有值得单独拦截的复杂度问题。
本地验证:core 定向测试 13/13、renderer architecture 71/71、AppShell hook 检查和 git diff --check 都通过。发评论时 GitHub CI 仍在运行。
3452f8b to
c2cbbd4
Compare
A read-only indicator in the composer's model controls shows the latest request as the provider counted it: input plus output tokens of the last accepted request, read from the session's newest token_usage record. With a user-declared Maka window it shows the percentage (over 100% is shown as such, never clamped); without one it shows the absolute count and names the model's reported window in a tooltip; without usage it shows a dash and says the provider reported none. Chat model choices carry the reported and the declared window separately so the two are never confused (apache#4559). Refs apache#4559 Generated-by: Claude Code Claude-Session: https://claude.ai/code/session_014ajaRxC4jydavY9nYUFj5J
c2cbbd4 to
c112553
Compare
|
All three failure modes are real, and I agree they are one problem rather than three: the control was reading request state from the rendered slice. Fixed on The record now carries the route. The reason the renderer could not do this correctly is that The projection is resolved at the owner.
Coverage is One note on the ratchet, since it shaped the shape: a new renderer module would have counted as closure growth, so the selector lives in |
|
CI is green on Flagging a scope change for the re-review: this is no longer a renderer-only patch. Fixing the three failure modes at the owner required the record to carry what the owner needs, so The renderer half is the smaller half. The parts worth the attention are |
Summary
Follows #4653, which is merged; this branch is a single commit on
main.A read-only context-usage indicator in the composer's model controls, to the right of the thinking-level control. It shows the latest request as the provider counted it — input plus output tokens of the last accepted request, read from the session's newest
token_usagerecord (lastRequestAnchor, the shape #4653 introduces) — and never a local estimate.used / windowon hover. Over 100% is shown as such, not clamped: a declared window is a target, not a limit. The distinction between the two sources matters for the compaction threshold, which only a declaration arms; it does not matter for reading a number off the screen, and the reported window is already what the rest of the app displays.buildChatModelChoicesnow carries the reported window (contextWindow) and the declared window (declaredContextWindow) as separate fields, resolved by core's single owner of the declaration rule, so the two are never confused downstream.The usage it shows is route-validated at the owner.
token_usageanchors now record the model and connection that produced them, andselectLatestRequestUsagerefuses anything it cannot pair with the request the user is about to make: a loaded range that is not the session tail, a usage row with no anchor (which is what manual/compactwrites, and which the runtime's own reader also scans past), an anchor from another route, and an anchor written before anchors carried their route. The composer receives one number, not the transcript slice. Adding two keys to a record decoded against a closed allowlist moves the compatibility epoch to 107.Refs #4559
Verification
npm --workspace @maka/{core,ui} run build,tsc --noEmitfor core and ui,npm --workspace @maka/desktop run typecheck,npm run check:renderer-architecture -- --base origin/main,npm run check:app-shell-hooks,npm run astryx:surface-inventory,npm run lint,npm run format:check— clean.llm-connections.test.js13/13. Not run locally: the desktop main suite (chat-composer-region-draft-handoff.test.tsgains the new requiredmessagesprop) and Desktop e2e — relying on CI.Review focus
app-shell.tsxcarries two small unrelated rewrites (newChatProviderType, theactiveSessionForViewannotation) that pay for the newmessagesprop under the renderer ratchet: the file's import declarations and specifiers go down by one each, so the debt counts do not increase. Say if you would rather see the ratchet entry adjusted instead.chat-composer-region.tsxscans the message list backwards on each render to find the newesttoken_usage. It is a legacy-ratchet file, so auseMemowould count as a new hook call; the scan stops at the first match, which is normally the last message.AI use
Select exactly one:
Tool(s) and scope: Claude Code — implementation; reviewed and verified by the author.
Checklist
Does this PR entail a change in behavior?
https://claude.ai/code/session_014ajaRxC4jydavY9nYUFj5J