feat(chat): mark where the prompt's history begins - #395
Merged
Conversation
The chat gave no sign of where the context window starts: the tokenizer sheet counted the messages that did not fit, three taps away, and the chat itself rendered a cut and an uncut message identically. Draw the boundary where it is read. - Add `TokenBreakdown.windowStartMessageId` — the oldest message the trim kept. Taken from `trimmedHistory` rather than from `historyAnchorId`, which only stepped mode fills in, so the same boundary comes out of either trim mode. - Add `contextWindowStartProvider`: the id the chat marks, or null when the chat fits whole, when no prompt has been calculated yet, or when the cached breakdown belongs to a session that is no longer open. - Push it over the bridge as `setContextWindowStart`. The page patches only the two rows involved, through `itemMap`, so the rule lands whether or not the boundary is currently mounted; the renderer keeps the id, so a full re-render draws it again without another call. - Draw the rule inside the first in-prompt message, styled like a date separator and amber like the cutoff notice that counts the same messages. - Seed it from the initializer: the WebView is kept alive across chats, so the renderer would otherwise carry the previous chat's boundary into this one. ## Keeping the numbers honest - Key the drawer's `TokenBreakdownCache` on the connection's context signature as the tokenizer sheet already does, so a breakdown taken under one trim mode is never served under another. - Carry `historyAnchorId` and `visibleMessageIds` through the drawer's vector-lore rebuild of a breakdown instead of dropping them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
The chat gave no sign of where the context window starts: the tokenizer sheet counted the messages that did not fit, three taps away, and the chat itself rendered a cut message and a carried one identically. This draws the boundary where it is read — a CONTEXT LIMIT rule inside the oldest message the next prompt still carries.
The marker
TokenBreakdown.windowStartMessageId— the oldest message the trim kept. Read fromtrimmedHistoryrather than fromhistoryAnchorId, which onlyHistoryTrimMode.steppedfills in, so the same boundary comes out of either trim mode.contextWindowStartProvider(lib/features/chat/state/context_window_marker.dart): the id the chat marks, or null when the chat fits whole (cutoffIndex == 0), when no prompt has been calculated yet, or when the cached breakdown belongs to a session that is no longer open.MessageBridgeCommands.setContextWindowStart. The page patches only the two rows involved, reaching them throughitemMap, so the rule lands whether or not the boundary is currently mounted — and the renderer keeps the id, so a full re-render (a preset switch, a scrollback batch) draws it again without another call from Flutter.ChatWebViewInitializerbefore the first paint. The WebView is kept alive across chats, so the renderer would otherwise carry the previous chat's boundary into this one — the same reason the search state is pushed early.Keeping the numbers honest
TokenBreakdownCacheon the connection'scontextBudgetSignature, astokenizer_sheet.dartalready does. Without it a breakdown taken under one trim mode could be served after a switch to the other, and both the drawer's numbers and the new rule come off that cache.historyAnchorIdandvisibleMessageIdsthroughMagicDrawerStatsService's vector-lore rebuild of a breakdown instead of dropping them.Verification
test/context_window_marker_test.dart(11 cases):windowStartMessageIdunder both trim modes — the sliding cut moves every turn, the stepped boundary holds still while its anchor does — plus the provider end to end against a real in-memory chat, asserting the rule appears in both modes and is withheld for a chat that fits, a stale cross-session breakdown, and a chat with no calculated prompt.test/webview_js/specs/context_limit.spec.js(5 cases): the rule renders inside the right message as its first child, moving the boundary leaves exactly one, an empty id retires it, a re-render redraws it without another push, and an id this chat has no message for draws nothing.flutter analyze: no errors (9 pre-existing warnings/infos on the base branch).npx playwright test: 86 passed.flutter test: 3851 passing; the 3 failures intest/chat_input_bar_test.dart(clipboard attachment thumbnails) also fail onnightlyunchanged and are unrelated to this PR.🤖 Generated with Claude Code