Skip to content

fix(chat): follow your own accepted send back to the end unless you navigate - #1831

Merged
devswha merged 3 commits into
mainfrom
fix/chat-local-send-follow
Oct 24, 2026
Merged

devswha merged 3 commits into
mainfrom
fix/chat-local-send-follow

Conversation

@devswha

@devswha devswha commented Oct 24, 2026

Copy link
Copy Markdown
Owner

What it does for the user

When you scroll up in Chat to read older turns and then send a message from the message box, the chat now takes you back to its end once the PC accepts the message, and follows the reply there. Before this, the view stayed where you were reading, so your own message and the agent's answer appeared off screen behind a "New messages" pill.

It only follows your own message from this browser that the bridge accepted at once (a receipt with no pending item, not a refusal, not an unconfirmed send to an older bridge). These keep the reader's place:

  • a refused or uncertain send
  • a queued or Send now delivery
  • another browser's message, a reconnect, or opening the pane again

Scrolling or paging back, a wheel over the minimap rail, a minimap jump, a restored place, a replaced history, or leaving the pane before the receipt or before the turn shows cancels the follow. Nothing new is drawn: the existing Jump to latest and New messages controls stay as they are.

Why

Agent of Empires re-pins its view on every prompt, local, remote or replayed. Ovis's sends carry a receipt, so only that accepted local receipt counts, and the reader's own navigation after sending always wins. #1734 and #1431 changed how scroll intent is read. I checked this against current main: the regression script below still fails there, at the step where the accepted receipt should bring the view to the end.

How it was verified

  • scripts/chat-local-send-follow-demo-regression.ts (added to the browser lane) runs on the demo app over its fixture transport. The submit receipts and transcript reads are gated, so each ordering is forced rather than timed. It covers:
    • an accepted send following to the end and the reply being followed
    • navigation before the receipt cancelling the follow
    • navigation before the transcript shows the turn cancelling the follow
    • refused, uncertain, remote, queued and Send now sends, a pane switch and a reconnect keeping the reader's place
  • The script passes on this branch. With src/ reset to origin/main it fails at "the accepted receipt brings the view to the end".
  • bun run check fast passes (typecheck, build, 6380 unit tests).
  • Looked at the demo at 390 px (phone) and 1280 px (desktop): after scrolling away and sending, the message and the Working row sit at the end above the composer, no horizontal overflow, no page errors, nothing new drawn.
  • Reviewed by a code-review agent and by Codex. No HIGH findings. One MEDIUM from both (a wheel over the minimap rail scrolled the chat without counting as navigation) is fixed in the second commit, along with paging down no longer cancelling a follow.

Follow-up

The regression script exercises wheel navigation only. PageUp and minimap tick jumps cancelling the follow are covered by code reading, not by a scripted step yet.

Rebased onto current main; the composer send moved to src/lib/useComposerSends.ts since the branch was cut, so the change was ported there.

@coderabbitai

coderabbitai Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 38 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

Learn how review limits work.

Review configuration:

Run configuration
Commits

Reviewing files that changed from the base of the PR and between 4c8ce30 and 9bd37a6.

Files selected for processing: 8
  • DESIGN.md
  • changelog.d/chat-local-send-follow.md
  • scripts/chat-local-send-follow-demo-regression.ts
  • scripts/ci-browser.sh
  • src/components/ChatMinimap.tsx
  • src/components/ChatView.tsx
  • src/components/PaneTerminal.tsx
  • src/lib/useComposerSends.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@devswha

devswha commented Oct 24, 2026

Copy link
Copy Markdown
Owner Author

Merge pass: a rebase is needed. #1828 merged as 6e0f88f, and this branch now conflicts with main in scripts/ci-browser.test.ts. That is the browser lane's script count, because both PRs add a script to scripts/ci-browser.sh. Rebase onto main, keep both scripts in the lane and bump the count to cover both. A rebase needs no new CodeRabbit review. The gate review of ce27dab is still running, and its findings will follow here.

@devswha

devswha commented Oct 24, 2026

Copy link
Copy Markdown
Owner Author

Merge pass review-work for ce27dab: PASSED. Gate APPROVE (medium-high). The only thing left is the rebase noted above, for the scripts/ci-browser.test.ts script count after #1828. Once it's pushed, the merge pass checks that the rebase changed only that count and the lane list, then merges.

Checks

  • Browser tests: the new follow regression passes 4/4, and ui-regression passes 72/72. ui-contexts passed on a fresh rerun. Its one earlier failure was chat-size-regression.ts:72, where terminal response text corrupted the shell's stty size probe. That is the existing terminal-mode flake, not the chat follow.
  • Code review (programming + remove-ai-slops): PASS. With src/ reset to the merge base, the script fails at "the accepted receipt brings the view to the end". Removing the upward-scroll cancel or the accepted-at-once condition also fails it.
  • Gate: checked in the browser:
    • accepted send: follows to the end;
    • refused, uncertain, remote, queued and Send now sends, a pane switch and a reconnect: the view stays where the reader was;
    • navigation before the receipt or before the turn, including a PageUp variant of the script: cancels the follow.
      No automatic replay, focus RPC, new UI or fixed sleep was added, and the design entry names Agent of Empires.

MINOR follow-ups

  • PageUp at scrollTop: 0 moves nothing, so it doesn't cancel a pending follow (ChatView.tsx:1654). Counting the keypress itself would be a reasonable improvement.
  • No script step yet covers the minimap, restore and history-replace cancel paths.
  • The queued step depends on the demo's 2.4 s turn timer.

@devswha
devswha force-pushed the fix/chat-local-send-follow branch from ce27dab to 3434288 Compare October 24, 2026 11:59
@devswha

devswha commented Oct 24, 2026

Copy link
Copy Markdown
Owner Author

Rebased onto main (after #1828, 6e0f88f) and pushed 3434288. The only conflict was the browser lane's script count in scripts/ci-browser.test.ts. Both scripts stay in scripts/ci-browser.sh (plan-chip-demo-regression.ts and chat-local-send-follow-demo-regression.ts), and the count is now 84, which matches the 84 run_script lines. Nothing else changed in the rebase. bun run check fast passed before the push (6384 unit tests pass). This is a rebase, so it doesn't need another CodeRabbit review.

On the earlier Native Windows install failure: the rerun of run 40088346948 passed. It was the intermittent windows-plugin.test.ts stop/restart test, now tracked in #1833.

devswha added a commit that referenced this pull request Oct 24, 2026
… a pinned count (#1874)

## What
`scripts/ci-browser.test.ts` pinned the number of browser scripts
(`toHaveLength(83)`), a list of `toContain` lines and the last script.
So every PR that adds a browser script edited the same lines, and
parallel PRs conflicted on them just for that (#1831, #1837, #1849 and
#1852 needed rebases for it). This PR removes those pins and checks the
same things against the files on disk:

- The serial lane's summary must equal `scripts/demo-build.ts` followed
by the list parsed from `scripts/ci-browser.sh`
(`serialBrowserScripts()`), in order. This checks more than the old
count did.
- The lane must not repeat a script or name one that does not exist.
- Every `scripts/*-regression.ts` and `scripts/*-browser-qa.ts` must be
run by the lane in one of three ways: it has its own `run_script` line;
it is imported by a script the lane runs (transitively, e.g. through
`ui-regression.ts` or `ui-contexts-checks.ts`); or it is spawned by one
(`join(import.meta.dir, "x.ts")`). Spawns are followed only from files
that run as scripts, not from imported helpers, because those spawns sit
under `import.meta.main`. A script CI does not run has to be named in a
new `MANUAL_BROWSER_SCRIPTS` list. Today that list holds 13 scripts that
are run by hand (Android emulator, SSH fixture, native Windows, update
QA, ...).
- The manual list must only name scripts that exist and that CI does not
reach, so it cannot go stale.

A PR that adds a browser script now adds only its `run_script` line in
`scripts/ci-browser.sh` (or a line in the manual list), so two such PRs
no longer touch the same line. `docs/development.md` describes the
check.

Test-only change: CI behaviour is unchanged.

## Verification
- `bun test scripts/ci-browser.test.ts`: 7 pass.
- Mutation checks, each restored afterwards:
- Deleting the `run_script scripts/workspace-rows-regression.ts` line
fails "runs every browser script on disk ..." and lists that script as
unassigned.
- Adding `run_script scripts/does-not-exist-regression.ts` fails "runs
each script once, and only scripts that exist".
- Adding an untracked `scripts/brand-new-regression.ts` fails the
assignment check.
  - The restored tree passes.
- `bun run check fast`: green (typecheck, build, 6396 unit tests).
- Reviewed by a code-review agent. Its one HIGH finding (a spawn inside
an imported helper's `import.meta.main` block was being counted as run
by CI, hiding `keyboard-viewport-demo-regression.ts`) is fixed in this
commit.

---------

Co-authored-by: mp <mp@local>
@devswha

devswha commented Oct 24, 2026

Copy link
Copy Markdown
Owner Author

Merge pass: one rebase needed. #1874 is merged on main. scripts/ci-browser.test.ts no longer pins the browser lane's count; it checks the scripts on disk. This branch still edits the old toHaveLength(…) line, so it now conflicts with main. Rebase onto main and drop this PR's change to scripts/ci-browser.test.ts, keeping its run_script line in scripts/ci-browser.sh. The new test checks that line automatically. This is the last rebase a lane count will cause. The merge pass checks that the rebase changed only that file, then re-queues the PR. A rebase needs no new CodeRabbit review.

A reader scrolled back to older turns who sent a message from the box stayed
where they were, so their own message and the reply landed out of view.

PaneTerminal now keeps a per-pane local send receipt, { key,
navigationGeneration }, separate from sentKey. It advances only for this
browser's own immediate composer submission whose receipt is ok with no
pending item, and only while the pane, socket and session that sent it are
still current. Refusals, uncertain answers, pending/queued receipts, Send now,
another browser's message, restore and mount never advance it. The chat's
navigation generation is captured when the message goes out, before the
receipt is awaited.

ChatView raises that generation on deliberate upward wheel, touch or keyboard
scrolling, page navigation, a minimap jump, a restored place, a replaced
history and leaving the pane (a retained pane hidden off-tab, or unmount). A
fresh receipt whose generation still matches re-engages the existing follow
through the Jump to latest path; an upward scroll before the turn is published
ends it as before. Each key is consumed once and a newly mounted chat starts
from the current key. No wire, endpoint or demo transport change.

Checked with the new scripts/chat-local-send-follow-demo-regression.ts on the
demo build (gated submit receipts and transcript reads; it fails on main and
passes here): accepted immediate local send resumes follow, navigation before
receipt cancels follow, navigation before transcript publication cancels
follow, and remote pending refused and hydrated sends preserve place (refused,
uncertain, remote browser, queued and Send now, pane switch before the
receipt, reconnect before the receipt). It is enrolled in the browser lane.
scripts/chat-virtual-browser-qa.ts and
scripts/restored-draft-demo-regression.ts still pass; bun run check fast
passes apart from server opencode, conversation and session-history timeouts
that fail the same way on main on this loaded PC.
@devswha
devswha force-pushed the fix/chat-local-send-follow branch from 3434288 to 9bd37a6 Compare October 24, 2026 15:15
@devswha

devswha commented Oct 24, 2026

Copy link
Copy Markdown
Owner Author

Rebased onto main after #1874 and pushed 9bd37a6 (was 3434288). I dropped this PR's change to scripts/ci-browser.test.ts and kept its run_script line in scripts/ci-browser.sh. Apart from that file, the branch's patch is identical to before (git patch-id matches). scripts/ci-browser.test.ts passes 7/7, and bun run check fast passed before the push. This is a rebase only, so it doesn't need another CodeRabbit review.

@devswha

devswha commented Oct 24, 2026

Copy link
Copy Markdown
Owner Author

Merge pass review-work report. Overall: PASSED.

Lane Verdict Confidence
Manual QA (4 rows, artifacts kept) PASS -
Code review (omo-native-code-reviewer, programming + remove-ai-slops) PASS (WATCH) -
Gate review (another model) at ce27dab APPROVE MEDIUM-HIGH
Rebase onto main (3434288) git range-diff: the only change is the browser lane's script count in scripts/ci-browser.test.ts, 83 to 84; the other two commits are identical -

Manual QA

  • The new follow regression passes 4/4, and ui-regression passes 72/72.
  • ui-contexts passed on a fresh rerun. The one earlier failure was chat-size-regression.ts:72, where terminal response text corrupted the stty size probe. That is the known terminal-mode flake.

Code review and gate

  • With src/ reset, the regression fails at "the accepted receipt brings the view to the end". It also fails when the upward-scroll cancel is removed, and when the accepted-at-once condition is removed.
  • Checked in the browser:
    • An accepted send follows to the end.
    • The reader keeps their place for refused, uncertain, remote, queued and Send now sends, after a pane switch and after a reconnect.
    • Navigating before the receipt or before the turn cancels the follow. A PageUp variant of this passed too.
  • Constraints: no automatic replay, no focus RPC, no new UI and no fixed sleep. The design entry names Agent of Empires.

MINOR follow-ups

  • PageUp at scrollTop: 0 moves nothing, so it doesn't cancel the follow (ChatView.tsx:1654).
  • No script step yet covers the minimap, restore or history-replace cancel paths.
  • The queued step depends on the demo's 2.4 s turn timer.

All required checks are green, and no review thread is open.

CodeRabbit did not review this PR (rate-limited or skipped). The reviews above stand in for it.

@devswha
devswha merged commit 8bf031c into main Oct 24, 2026
10 checks passed
@devswha
devswha deleted the fix/chat-local-send-follow branch October 24, 2026 20:18
devswha added a commit that referenced this pull request Oct 24, 2026
… follow allowances (#1902)

## What it does for the user

Holding the chat's scrollbar still for a moment and then dragging it up
a little now stops Chat from following new output, as any other upward
scroll does. Before this, only a drag that began within half a second of
the press counted. A thumb that rested first and then moved up less than
48 px kept following, so the next update pulled the reader back to the
end.

Fixes #1434. #1734 already fixed items 1, 2, 4 and 5. This PR fixes what
was left of item 3:

- **A held scrollbar press** (3a). In Chromium a thumb drag sends
`pointerdown` on the scroller at the press, no `pointermove` while the
thumb moves, then scroll events, then `pointerup` at release (measured
in Playwright's Chromium with scrollbars shown). `ChatView` now
remembers that a primary-button press on the transcript's scrollbar is
held. While it is held, an upward move counts as the reader's own,
however long the thumb rested first. The hold ends on
`pointerup`/`pointercancel`, on a new press, on any pointer event with
no button down (a release this page did not see), and when the chat's
input listeners are removed (the pane hidden or left).
- **The 1 px and 48 px values** (3c) are named: `READER_ALLOWANCE_PX`
(endpoint rounding only, after the reader's own input) and
`LAYOUT_ALLOWANCE_PX` (a composer growing a few lines before the
correction to the end lands).
- **Screen-reader scrolling** (3b) is not fixed and is documented beside
`LAYOUT_ALLOWANCE_PX`. A scroll that sends no input this page can see,
from a screen reader or from Firefox's scrollbar (which may send no
press), leaves the end only past 48 px. The page has no signal to tell
such a scroll from the composer's own growth, and guessing would bring
back the composer-growth bug #1431 fixed.

No visible change otherwise. Design precedent: none of the coding-agent
apps exposes this. It follows the rule #1734 set: only input that
scrolls the transcript up is the reader's intent.

**#1831:** that open PR edits the same `allowance` line in `onScroll`
(it adds `byReader` and `navigated()`). Whichever lands second keeps
both: #1831's `byReader` with the named constants. A held-thumb drag
then also counts as navigation for #1831's follow of the reader's own
send.

## How it was verified

- `scripts/chat-virtual-browser-qa.ts` (already in CI's browser lane)
runs the real `ChatView` in Chromium at 1440 px and 390 px, light and
dark. Its scroll-intent loop gains four cases. Each fires the scrollbar
press on the scroller right of its content box, then moves the
transcript up 20 px, inside the layout allowance but past endpoint
rounding. `performance.now` is skewed by 600 ms around that move, so the
move comes after the input window as a resting thumb's would, with no
sleep.
- `scrollbar-held`: the press is still held when the move lands.
Following stops and **New messages** shows.
- `scrollbar-held-released-first`: the thumb is released before the
scroll event is delivered (the order Codex's review found). Following
stops.
- `scrollbar-released`, `scrollbar-unseen-release` (no `pointerup`, the
next pointer event has no button down): the press is over, so the same
20 px correction keeps following.
- On origin/main the script fails at `scrollbar-held: only upward input
stops following` (`0 !== 1`). On this branch it passes, 52 PASS,
together with the existing cases: a downward wheel, a click, typing and
composing keys keep following; an upward wheel, navigation keys and a
drag stop it; a composer growing a line paints no frame off the end.
- `bun run check fast` passes on this commit (typecheck, build, unit
tests).
- Reviewed by a code-review agent (omo, approved) and by Codex
(gpt-6.1-sol). Codex's one MEDIUM (a thumb released before the browser
delivered its scroll lost the intent) is fixed and covered by
`scrollbar-held-released-first`. omo's LOWs:
- **Fixed:** the hold now needs the primary button, and it is cleared
when the chat's input listeners go away (the pane hidden or left
mid-press).
- **Left:** a new press clearing the hold has no case of its own; it
duplicates the release paths.

Not reproduced on a real Firefox or with a screen reader; see the
screen-reader note above.

---------

Co-authored-by: mp <mp@local>
devswha added a commit that referenced this pull request Oct 25, 2026
…ough its rewrites (#1937)

## What it does for the user

While a GJC turn streams tool calls into Chat, a tool you opened now
stays open and stays where it is on screen:

- **Opening is reading.** Opening a tool row, a folded work block or a
tool's whole output stops following the end, just as scrolling up does.
New records arrive below without moving what you are reading. Jump to
latest (or New messages) brings you back to the end, and following
resumes from there. A reader who opens nothing keeps following exactly
as before.
- **GJC rewrites that keep the turns no longer reset the chat.** gjc
writes its whole session file again and renames it over the old one: on
its first save, and when it trims old tool outputs (it does the same
when it compacts). The server reads the new file as another history
(`history_id` is the file's identity), so the chat started over: every
opened detail folded, a reader's place was lost, and at phone width the
end was no longer followed. Now a new history ID whose newest page still
opens with every turn already shown, in the same places, keeps the rows,
what was opened and where you read. Each shown turn's role and start
time, each part's kind, and each tool's call id and input must be
unchanged (gjc's tool parts now carry their `toolCall` id as `call_id`
for this); what a tool answered may be trimmed. Anything else still
starts over as before: a compaction that dropped turns or moved where
the newest page starts, a new session, a cleared context, a rewind (even
one that keeps a turn's tools' names), a moved branch, or a reader
holding older pages.
- **A todo update you opened stays open.** Since #1846, contiguous todo
updates fold into one Plan row. An update you had opened turned into a
closed Plan row as soon as the next update arrived, so what you were
reading folded away. The Plan row now opens when you had opened one of
its updates, with that update still open inside it. A Plan row nobody
opened still starts closed.

## Why

#1388: "the viewport alternates between the user message and the bottom"
and "tool details that the reader expanded collapse when new records
arrive". Two causes, both reproduced on current main with GJC-shaped
records:

1. `ChatTimeline` remembered a click on a disclosure only until the next
rows change (`manualLayout` cleared on every `rows` update). Then
ChatView's layout effect and the list's size callback followed the end
again, dragging an opened running tool's header up 383–455 px on every
record.
2. gjc 0.18's session manager rewrites its file through a temp file +
rename (`rewriteEntries` after compaction eviction and tool-output
pruning; I read this from the installed gjc 0.18.12 binary). A rename is
a new inode, so `transcriptStream` gives a new `history_id` and
ChatView's read dropped the row slots, the row memories and the reader's
intent.

Since #1831, a message sent from this browser's box follows the chat
back to the end unless the reader navigated after it went out. Opening a
tool now counts as that navigation too, as scrolling up does, so a send
on its way no longer pulls the reader off a tool they opened. A gjc
rewrite that continues the chat is not navigation (the reader's place is
kept); any other new history still is.

Design: T3 Code took the other side for a reader at the very bottom
(pingdotgg/t3code#17954 keeps a reader who toggles a tool there pinned
to the bottom). Here the issue asks for the opened row to stay put while
a turn streams, and Ovis already treats scrolling up as "stop following
until I come back". Opening a detail joins that rule instead of adding a
new one, so nothing new is drawn: the existing Jump to latest and New
messages controls carry it. DESIGN.md's chat follow paragraph says so.

The server's history identity is unchanged. **HTTP contract:** `GET
/api/pane/conversation` now gives a gjc tool part its own `call_id`, the
optional field `shared/protocol.ts` already declares for Claude and
Codex; omp and pi still carry none (`server/api.contract.test.ts`).
CodeRabbit is rate-limited here, so a code-review agent's review stands
in, and this needs a real review before merging. A byte-prefix check on
the server would miss gjc's rewrites, because pruning changes earlier
records. The continuity check runs only in the chat that already holds
the turns.

## How it was verified

- New `scripts/chat-gjc-streaming-browser-qa.ts` (added to the browser
lane). It feeds GJC-shaped `message`/`toolCall`/`toolResult` records one
at a time through the real `transcriptPage("gjc-transcript")` parser and
the real ChatView → ChatTimeline → LegendList path in Chromium, at 1440
and 390 px. Three readers are covered: one who opened a running tool,
one following the end, and one scrolled up. Each runs with plain appends
and with a gjc-style temp+rename rewrite. It samples every frame from
each refresh until the view settles:
- the opened tool's header and the scrolled reader's row stay within 1
px in every frame
  - the opened tool stays `aria-expanded="true"`
  - a follower ends at the end after every record
  - Jump to latest resumes following
- On main (d3eefef, before this branch was rebased) it fails 62
assertions:
  - header displacement −383 to −455 px per record
  - `aria-expanded="false"` after a rewrite
  - the scrolled reader's row lost after a rewrite
  - the 390 px follower 179–205 px short of the end
- in a chat that still fit the screen when the tool was opened, the
opened tool dragged up to 1502 px once later records overflow it
- On this branch every scenario holds at 0 px (20 scenarios, rebased on
ddee8be). Current main (ddee8be) still fails 62 assertions.
- Its `todo` scenario (added with the rebase) opens a gjc `todo_write`
update and appends the next one, plainly and through a rewrite: the Plan
row's head stays where the opened update's head was in every frame, it
is open, and the update read stays open in it. Run against the commit
before that fix, the script fails only those 16 todo assertions.
- `server/conversation.test.ts` runs a gjc rewrite through the real
parser: the same calls written again continue, and a rewind that reads
the same file again under another call id, later, does not.
- `src/lib/chatRows.test.ts` covers `continuesHistory`: continuation, a
dropped/moved/retimed turn, a page that starts later (dropped first
turns), a rewind that puts another call of the same tool in a shown
place (other input or call id), another source, scrollback, empty and
untimed pages.
- Its `jump` scenario opens a tool and takes Jump to latest at once;
closes and reopens it and jumps again; closes it while following,
scrolls away and jumps back; and does that again but scrolls back to the
end by hand. After each Jump the last row grows with no record between,
and the end is followed. The script counts an answer as served only once
the chat has read its body and React has committed after it.
- `scripts/chat-virtual-browser-qa.ts` passes at 1440 and 390 px, dark
and light (paging, pagination, virtualization, selection and disclosure
restoration). The other chat browser scripts run in this PR's browser
lane.
- `scripts/todo-snapshots-demo-regression.ts` passes, and so do
`chat-local-send-follow-demo-regression.ts` (#1831),
`chat-browser-qa.ts` and `chat-tool-paths-demo-regression.ts` on the
rebased branch.
- `bun run check fast` passes: typecheck, build, generated files,
workflow syntax, 6772 unit tests.
- Reviewed by a code-review agent (no HIGH; its one real gap, a chat too
short to scroll re-arming follow, is fixed in the second commit) and by
Codex Sol 6.1 (no HIGH; three MEDIUM findings on the history
continuation are fixed in the third commit: it applies to gjc only and
needs each shown part's kind and tool to match, an older-page read in
flight is dropped, and the kept reading place moves to the new history
ID). A Codex re-review of that commit found no remaining HIGH or MEDIUM
issue.
- After the rebase onto current main, Codex Sol reviewed the rebase
delta. The two conflict resolutions with #1831 were clean. It found one
MEDIUM, the #1846 todo row folding an update the reader had opened,
which the last commit fixes. Its re-review found one more MEDIUM (a
remounted Plan row lost the openness it had inherited), fixed in the
same commit; its 13-case repro passes.

Not covered: native iOS/Safari, a gjc compaction that drops turns (it
starts over, as before), real gjc runs (the records are synthetic and
the rewrite is simulated as gjc does it), and a reader who has loaded
older pages when gjc rewrites. That reader still starts over, because
those pages were read from the old file.

Fixes #1388

---------

Co-authored-by: mp <mp@local>
devswha added a commit that referenced this pull request Oct 28, 2026
## What it does for the user

Chat now keeps following new output to the end when part of the
transcript folds on its own. Before
this, a turn settling at the end (its work folding together with a block
above it), or a block above
the view folding while the view was still gliding to the end, could stop
following mid-transcript and
show **New messages**, with no scroll by the reader.

Fixes #2042. Reported by @odfalik.

## Cause

`onScroll` in `src/components/ChatView.tsx` treats an upward move with
no reader input that lands past
the 48 px layout allowance as the reader leaving the end: it sets
`pagingAway` and drops the list's
pending scroll to the end. LegendList (`maintainVisibleContentPosition`)
and the browser make such
moves themselves when the transcript shrinks. The list holds the rows on
screen where they were, and
the browser keeps the view inside a shorter transcript. For one shrink
both can move the view up, so
it lands hundreds of pixels from the end, or past the end of a follow
glide that was still on its way.

## Change

- Without reader input, an upward move during which the transcript got
shorter (the last row moved
up in it, or its height dropped since the previous scroll event) now
counts as the layout's: following
goes on, and a view left more than 48 px short of the end is put back
there.
- That pin-back is skipped while `pagingAway` is set and while a
following reader's own click is laid
out where they are (ChatTimeline `manualLayout`), as the timeline's own
follow paths already do.
- Over a transcript that did not shrink, nothing changes: a no-input
scroll (a screen reader's, a
Firefox scrollbar's) leaves the end once the view is past 48 px from it,
however small its steps
(#1902). Reader input (wheel, keys, touch, a scrollbar drag) is
unchanged.
- The new scroll baselines reset with the chat's other scroll state on a
pane change or a history
  replacement.

Known trade-off: a no-input reader scroll that lands in the same scroll
event as a fold is put back at
the end once; the next move leaves as before. During a turn that keeps
folding this can happen more
than once. The page has no signal that tells such a scroll from the
layout's own move in that event.

Design precedent: none of the coding-agent apps exposes how it tells its
own re-layout from a reader's
scroll; this follows the rule #1734 set and #1902 kept, that only input
that scrolls the transcript up
is the reader's intent.

## How it was verified

- `scripts/chat-virtual-browser-qa.ts` (real `ChatView` in Chromium,
1440 and 390 px, light and dark,
  already in CI's browser lane) gains:
- `fold-together`: at the end, the last row and a row above fold in one
layout (a turn settling).
Following goes on to the end and the next output is followed. **Fails on
main**
    ("fold-together: the next output is followed"), passes here.
- `fold-above` / `grow-above`: a row above folds or grows while the view
glides to the end after the
last row grew. These pass here; on main they fail only when the fold
lands in the same frame as the
    glide, so they do not fail on every main run.
- `wheel-up`: a real upward wheel (`page.mouse.wheel`) still stops
following ("Jump to latest", then
    **New messages** with the reader left away from the end).
- Two 30 px upward steps without input from the end stop following past
the allowance. This catches
    the first commit's regression found in review.
  - Full script: 60 PASS over all four width/theme combinations, exit 0.
- `scripts/chat-local-send-follow-demo-regression.ts` (#1831),
`scripts/chat-gjc-streaming-browser-qa.ts`
(#1388/#1937) and `scripts/composer-fit-demo-regression.ts` (#1431):
pass on the final commit.
- `bun run check fast`: green on the final commit.
- Reviews: Codex (gpt-6.1-sol, fast, xhigh) and the omo code reviewer.
Both blocked the first commit on
one finding: the 48 px allowance applied per scroll event, so small
no-input steps were pinned back
(a #1902 regression). The second commit measures it from the end again.
The omo reviewer's other
findings were fixed: the `manualLayout` guard and the baseline resets.
Codex re-reviewed the fix:
  APPROVE.

Not reproduced on a real Firefox or with a screen reader. Once, during
QA, a real wheel landing early in
a glide did not stop following; it did not reproduce in six later runs
(three on main, three here) and
this change does not touch moves made with reader input.
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