Reader: come back to where you were reading - #59
Merged
Conversation
Reported on a phone against #58: coming back to a session did not put the reader where it was left. #55 has since changed what "wrong place" means — it opens on the END now rather than the top — but it still is not the place you had scrolled to, which is what this adds. The anchor is a turn timestamp, and the windowed reader forces that choice. Exchanges regroup as older windows arrive: the one at the top of the list is a fragment whose prompt was in the window not yet read, and the two become one when it lands. So no React key, list index or pixel offset identifies a place for longer than one fetch. A turn's `ts` comes from the transcript and never moves. If the remembered turn is not in the window the reader opened with, it pages backwards to find it — through the same public `loadOlder()` a scroll would use, so nothing in lib/traceWindows.ts had to change, and live tailing is untouched. Bounded to six windows; past that it stays on the end. Nothing is remembered until you have moved the view, and the evidence is a gesture, never a `scroll` event — a scroll fires when the reader re-anchors under a prepend, and gating on it filed positions the reader never chose. If you were at the end you return to the new end. A turn that cannot be reached degrades to the end. Bounded to 100 sessions, no expiry, quota failures swallowed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
thomwolf
force-pushed
the
reader/remember-reading-position
branch
from
August 12, 2026 17:50
cbb00de to
f44b25c
Compare
This was referenced Aug 12, 2026
thomwolf
added a commit
that referenced
this pull request
Aug 13, 2026
The bottom bar puts the terminal/reader switch next to the zoom because they are the same kind of setting — how you are looking at everything. But the zoom only moved the terminal: `conversation.css` was written in absolute px, so a reader pane ignored it entirely, and the mode switch quietly changed how big your session was. So the grammar gets ONE base size — `--cx-base`, 13px by default — and every type size in it becomes an em of that. The terminal spends the zoom on its font size, the reader on `--cx-base` (set on `.pane-reader`), and 13px at 100% either way means switching modes no longer resizes the conversation. The reader scales as one surface: bar, turns, tool output, reply line, footer. A 10.5px toolbar left behind at 150% would be the part you could not read in the first place — the remote pane already treats its log, composer and status line as one size for the same reason. Four things the px-to-em pass had to be careful about: - **The Overview card shares this grammar and must not move.** That is why the base is a `var(--cx-base, 13px)` default rather than something inherited: the card resolves to exactly the sizes it had. - **The answer's markdown needed its own heading sizes.** The shared `.markdown` rules are absolute px, so an answer's `h1` would have stayed 22px while the prose around it grew. Same proportions, in em, and this file loads after styles.css. - **Spacing does not scale, but glyph columns must.** The prompt's `❯` gutter, the step rail's ▸/✓ column and the body inset under it, and the tool-field label column (was `4.5rem`) are all em now. At 200% the chevron was running into the first word of the prompt. - **`.cs-pre`'s scroll cap is really "about twenty lines".** In rem it showed ten at 200%; in em it stays nineteen at every zoom. Two deliberate visible changes beyond that. The phone keyboard field keeps its fixed 16px — that guard is what stops iOS zoom-on-focus, and the new rule is later in the cascade, so styles.css's version is restated here. And `.cxv-top` (#59's "earlier messages" line) says the size it means: `font: inherit` sheds the button's UA font but also resets the size the element gets from `.cxv-msg`, so it had been landing on the app's inherited 16px in a column of 11px messages. Verified in Chromium against a fixture built from the real ConversationView / Exchange / ToolCall DOM, comparing computed sizes with the pre-change stylesheet: nothing moves at 100% except `.cxv-top`, every element in the reader scales exactly 0.5x/2x at 50%/200%, the Overview card is untouched at all three, and nothing overflows the pane sideways at 200%. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
lvwerra
pushed a commit
that referenced
this pull request
Aug 13, 2026
Rebased onto main after #59 and #61, which cover ground this branch also touched. Theirs wins in both places: #61 made zoom ONE number for both ways of reading a session (`--cx-base` on `.pane-reader`), so the branch's second path — a `zoom` prop and `zoom: var(--cx-zoom)` on `.cxv-body` — is gone rather than reintroduced; and #59 comes back to where you had got to, over a tail window that already opens on the end, so the branch's own open-on-the-newest-turn landing (reader and trace viewer both) is gone too. TracePane keeps main's version entirely. What is left is what main does not have: - **The prompt band spans the pane.** Reaching into the left gutter but stopping at the text column on the right made it read as a card floating over the answer rather than as the head of it. Full bleed both sides; the meta row sits tight under the band it belongs to, and one exchange ends well before the next begins. - **Nothing of the terminal's may paint over the reader.** Its covers — `restoring last view…`, `starting claude…`, `stopped · output preserved` — sit at `z-index: 4` and were drawn straight over the conversation, so a reconnect turned the reader into a terminal screen with a reader toolbar on top. They are gated off while reading, and the overlay now outranks anything the terminal can raise. - **One `Composer` for the card and the reader.** A composer accretes features — paste-to-attach, history recall, a slash-command menu — and duplicated markup is how one surface quietly gets them and the other does not. `onPasteFiles` and `above` are the seam an attachment strip plugs into. The echo is now optimistic in both: the prompt appears before the POST returns, and a failed send withdraws it and puts the text back in the box. - **The reader switch is the same height as the zoom keys** (22px, like the layout picker beside it). Verified against a running instance with a fixture transcript: the reader renders, the band bleeds to the right edge, an injected `.term-boot` cover paints behind the overlay, the bottom bar is one height, a sent prompt echoes before the POST returns, and main's zoom still scales the reader through `--cx-base` with no second path in play. Server, web and UI suites green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lvwerra
added a commit
that referenced
this pull request
Aug 13, 2026
Rebased onto main after #59 and #61, which cover ground this branch also touched. Theirs wins in both places: #61 made zoom ONE number for both ways of reading a session (`--cx-base` on `.pane-reader`), so the branch's second path — a `zoom` prop and `zoom: var(--cx-zoom)` on `.cxv-body` — is gone rather than reintroduced; and #59 comes back to where you had got to, over a tail window that already opens on the end, so the branch's own open-on-the-newest-turn landing (reader and trace viewer both) is gone too. TracePane keeps main's version entirely. What is left is what main does not have: - **The prompt band spans the pane.** Reaching into the left gutter but stopping at the text column on the right made it read as a card floating over the answer rather than as the head of it. Full bleed both sides; the meta row sits tight under the band it belongs to, and one exchange ends well before the next begins. - **Nothing of the terminal's may paint over the reader.** Its covers — `restoring last view…`, `starting claude…`, `stopped · output preserved` — sit at `z-index: 4` and were drawn straight over the conversation, so a reconnect turned the reader into a terminal screen with a reader toolbar on top. They are gated off while reading, and the overlay now outranks anything the terminal can raise. - **One `Composer` for the card and the reader.** A composer accretes features — paste-to-attach, history recall, a slash-command menu — and duplicated markup is how one surface quietly gets them and the other does not. `onPasteFiles` and `above` are the seam an attachment strip plugs into. The echo is now optimistic in both: the prompt appears before the POST returns, and a failed send withdraws it and puts the text back in the box. - **The reader switch is the same height as the zoom keys** (22px, like the layout picker beside it). Verified against a running instance with a fixture transcript: the reader renders, the band bleeds to the right edge, an injected `.term-boot` cover paints behind the overlay, the bottom bar is one height, a sent prompt echoes before the POST returns, and main's zoom still scales the reader through `--cx-base` with no second path in play. Server, web and UI suites green. Co-authored-by: Agent Manager <agents@agent-manager.local> 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.
Reported on a phone against #58: coming back to a session did not put the reader
where you had left it.
Rewritten on top of #55, which landed while this was open and changed the
premise. The original version of this PR anchored on
page.offset + exchange.atagainst the old read-the-last-400-turns reader; #55 replaced that with byte
windows that page backwards, so that anchor could not survive and the PR was
rebuilt rather than rebased.
What main does now, measured
So #55 already fixed the reported symptom. What is left is the actual promise:
opening on the end is right for a conversation you have not read, and wrong for
one you were half-way up. In-app navigation already survived either way — the pane
stays mounted and the browser preserves the scroll box across the
display: nonethat hides a warm tile — so this is entirely about the cold mount: a reload, an
evicted tab, the Hub rebuilding the Space's iframe.
Why the anchor is a turn timestamp
The windowed reader forces it. Exchanges regroup as older windows arrive: the
one at the top of the list is a fragment whose prompt was in the window not yet
read, and the two become one exchange when it lands. So no React key, no list
index and no pixel offset identifies a place for longer than one fetch. A turn's
tscomes from the transcript and never moves;offthen only carries whereinside that turn you were.
If the remembered turn is not in the window the reader opened with, it pages
backwards to find it — through the same public
loadOlder()a scroll would use.Nothing in
lib/traceWindows.tschanged, so #55's cursors, its one-request-at-a-time guard and its live tailing are all untouched. Bounded to six windows: a
remembered position is not worth walking a 19 MB transcript for, and past that it
stays on the end, which is where the reader would have been anyway.
Three rules that decide whether it feels right
gesture — wheel, touch, pointer, keys, the turn nav, the load-earlier button —
never a
scrollevent. A scroll fires when the reader re-anchors under aprepend, and gating on it filed positions the reader never chose. This is also
what keeps Trace reader: open on the last exchange, page backwards by byte window #55's open-on-the-end landing intact for a session you merely glanced
at.
to be last. Six turns may have arrived while you were away, and they are what you
came back for.
Bounded to 100 sessions, oldest first. No expiry, deliberately: unlike a draft
this is not text you typed, so it carries nothing worth forgetting for its own
sake. A v1 blob from the earlier anchor scheme is dropped rather than misread.
Quota failures and denied storage both degrade in silence.
Verified by running it
A local server at a 390×844 phone viewport, with unmodified
mainas acontrol. 20 checks; the control fails exactly the seven that matter:
The multi-window check uses a 580 KB fixture — several of the reader's 384 KB
windows — because that is the only way to exercise "the turn you were on is not in
the window we opened with". The "worked while I was away" check appends real turns
to the transcript the dev server is reading rather than mocking a response.
Also re-ran #58's 25 draft checks and the joint journey on this build (no
regression), plus
tsc --noEmit,npm testand a production build.Two bugs found by running rather than reading, both worth recording:
and applied in a layout effect — and layout effects run first, so nothing
tried until the next render, which a quiet trace does not have. It now attempts
the seek the moment it is armed, and again on every window that arrives.
scrollwas not enough (see above). Found on the deployed Spaceand not locally, where the pane is 41px taller and had less slack to re-anchor
into. The harness now scrolls with
mouse.wheelrather than by assigningscrollTop, which is a truer test of the same path.Not covered
pagehide{persisted:false}plusa fresh document in the same browsing context, which is what the browser does on
revisiting a discarded tab.
readingPosition.tsrepeats the monotonic stamp and quota-safe write fromdrafts.ts. A conscious choice: the policies differ (drafts expire and aresize-capped, a position is count-bounded and permanent), and a shared abstraction
wanted four parameters to say so. Worth unifying if a third case appears.
🤖 Generated with Claude Code