Repository navigation
fix(chat): keep a tool you opened in place while GJC streams, and through its rewrites - #1937
Conversation
…ough its rewrites Opening a tool, a folded block or a whole output is now reading intent, as scrolling up is: following stops until the reader goes back to the end. Before, the next record cleared the click's guard and every follow path pulled the view to the end, dragging an opened running tool 380-460 px up per record. gjc writes its whole session anew and renames it over the old file when it compacts or trims tool outputs. The new inode gave a new history_id, and the chat reset: every open detail folded, a reader's row was lost and a phone-width follower stopped short of the end. A new ID whose newest page still opens with every shown turn (same role and start time), with no older pages held, now keeps the rows and the reader's state. scripts/chat-gjc-streaming-browser-qa.ts feeds GJC-shaped records one at a time through the real transcript parser and ChatView, appended and rewritten, at 390 and 1440 px, and checks every frame. Fixes #1388
…croll measureEnd turns following back on whenever the content fits the view, so a reader who opened a tool early in a session lost the reading intent at once, and the records that later outgrew the screen dragged the opened row again. Opening something now holds through a short chat until the reader goes back to the end. The GJC streaming QA adds that reader (a chat with no earlier turns), and the gamma call carries real narration so the chat outgrows the screen midway.
…flight older reads Review follow-ups for the history continuation: - Only gjc-transcript answers can continue, and every shown part must keep its kind and tool name: a pi branch move can keep a turn's role and start while replacing its tools, and positional disclosures would carry over. - A continuation drops an older-page read still in flight; its answer names the old file and used to reset the chat after the continuation. - The place kept for the pane moves to the new history ID, so a pane switch after a rewrite still restores it.
…d that a gjc rewrite keeps the chat Opening a tool, a folded block or a whole output now joins scrolling up: following stops, and a local send already on its way no longer takes the reader to the end. A gjc rewrite whose newest page still opens with every shown turn keeps the rows, what was opened and the reader's place.
… it into a group Since contiguous todo updates fold into one Plan row (#1846), a todo call the reader had opened became a closed group as soon as the next update arrived, and what they were reading folded away, also across a gjc rewrite the chat continues. The group now opens when the reader had opened any of its updates; a group nobody opened still starts closed.
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the
|
| Layer / File(s) | Summary |
|---|---|
GJC tool identity and history continuity shared/protocol.ts, server/transcript-records.ts, server/conversation.ts, src/lib/chatRows.ts, src/lib/chatRows.test.ts, server/conversation.test.ts |
GJC parsing includes tool-call IDs. continuesHistory checks whether shown GJC turns and tool parts match the next conversation; tests cover accepted and rejected continuations. |
Opened content and reader position src/components/ChatTimeline.tsx, src/components/ChatView.tsx, DESIGN.md, changelog.d/chat-opened-tool-stays.md |
Opening content stops following and holds reader position. Confirmed history continuations preserve rows and position. Todo groups retain their open state across remounts. |
Streaming browser validation scripts/chat-gjc-streaming-browser-qa.ts, scripts/chat-virtual-fixture.tsx, scripts/ci-browser.sh, tsconfig.tools.json |
Browser QA exercises transcript appends and rewrites at desktop and mobile widths. The fixture exposes answer metadata, and browser CI runs the new script. |
Priority:
Estimated code review effort: 3 (Moderate) | ~25 minutes
Change: Bug fix · Severity of issue fixed: Medium
Possibly related issues
- devswha/herdr-web-ui issue 1601: The change preserves an opened todo group when a new update joins the row; the issue’s proposed grouping and status model remain outside this change.
- devswha/herdr-web-ui issue 1053: The change preserves opened todo-group state as updates arrive, but does not implement the full tracking issue.
Suggested reviewers: yoonwoo-ha
Merge Risk: 
Minimal · up to 858a8
-
The chat streaming changes are mergeable after normal checks; no concrete blocking issue was established.
Pre-merge checks: 
4 · 
1
-
Failed checks: 1 warning Passed checks: 4 passed-
Full details: Title check
-
Explanation
The title names the main change: a tool the reader opened stays in place while GJC streams, including through GJC's file rewrites. It matches the raw summary's chat follow and history-continuation changes, and the objectives. It is specific and is not vague. The title is long (86 characters, 15 words) but the scope is clear.
Full details: Description check
-
Explanation
The description follows the template. It has a Change section that describes the problem (
#1388), the cause, and the resulting behavior, and it links the issue. It has a Validation section that lists the browser QA scripts, unit tests, and the check run, and it covers the HTTP contract change with contract-test coverage (server/api.contract.test.ts). The description is long and detailed, but it is complete.
Full details: Linked Issues check
-
Explanation
[Issue
#1388] The PR adds reader-navigation handling for opened tools, folded blocks, and outputs. It preserves expanded row state and viewport position during streaming.continuesHistorypreserves state only for compatible GJC rewrites and rejects dropped, moved, retimed, rewound, or other-source histories. The parser carries GJC tool call IDs for this check. Todo grouping preserves an opened update inside the Plan row. The reported browser QA covers append, rewrite, grouping, pagination, virtualization, desktop, and mobile cases. The reported unit, parser, browser, and fast-check suites pass.
Full details: Out of Scope Changes check
-
Explanation
The changes stay within [Issue
#1388]. Source changes implement chat follow behavior, disclosure-state retention, GJC history continuity, todo grouping behavior, and the required tool-call identity. Tests, fixtures, CI wiring, documentation, and the changelog directly verify or document these behaviors. No unrelated product behavior is demonstrated.
-
Full details: Docstring Coverage
-
Explanation
Docstring coverage is 43.75% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 16 functions across 11 files. (3 skipped: 3 unsupported.)
Resolution
Write docstrings for the functions missing them to satisfy the coverage threshold.
-
Finishing Touches: 💡 1
-
📝 Generate docstrings: 💡
-
- Commit to this branch
- Create a new PR
🧪 Generate unit tests (beta)
-
- Commit to this branch
- Create a new PR
-
- 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.
Comment @coderabbitai help to get the list of available commands.
|
CodeRabbit hit its review limit on this PR ("Review limit reached"), so no CodeRabbit review ran. The reviews that did run, and how each finding was resolved:
The PR touches no auth, takeover, queued input, HTTP/WS contract or workflow. |
|
Merge pass review of 6930a39: Codex (gpt-6.1-sol, xhigh) asks for changes. The code review is still running. 132 focused unit tests pass. MAJOR: a rewind can be mistaken for the same history (
MINOR: Jump to latest leaves the layout guard on (
MINOR: the browser QA wait doesn't prove the response was drawn (
The merge pass will recheck only the new commits once these are fixed. |
|
Merge pass, the second review: the code review (omo-native-code-reviewer) passed.
Codex's MAJOR above still blocks the merge. With the real parser, M1 (
|
… call and its input A rewind that kept a turn's start and its tools' names but replaced a call (read beta.ts became read gamma.ts) passed for the same history, and the kept positional state opened the replacement as if the reader had opened it. Each shown tool's call id and input must now match too. A rewrite that moves where the newest page starts, such as a compaction that drops turns, still starts the chat over, so DESIGN.md and the changelog now claim only rewrites that keep the shown turns (trimmed tool outputs).
Opening a tool held the list's layout guard until the rows changed, so after Jump to latest a whole output or a late image growing below was not followed. Opening already stops following, so it now drops the guard, including one an earlier close left; a click a following reader makes still sets it. The GJC streaming QA gains a jump scenario (open, Jump, close and reopen, Jump, then a row grows), and counts an answer as served only once the chat has shown that very answer (its metadata comes back through onMetadata) and React has committed after it, not when its headers arrive.
|
Fixed in f88fa1b and eea0701, pushed together. MAJOR: a rewind taken for the same history (
M1: compaction (
MINOR: Jump to latest left the layout guard on (
MINOR: the browser QA wait (
Codex Sol on these commits, before the push. Two LOWs were fixed in eea0701: the hold left by a close-then-reopen, and a dropped read advancing the wait. One MEDIUM I left out of this PR:
Verified on eea0701:
Verified on 8e1bdda08 (the same fixes, before the two LOWs): |
|
Merge pass re-review of eea0701 (Codex, changed range 6930a39..eea0701). Changes still requested. Two findings are only partly fixed. The browser QA wait (MINOR) is fixed, and the M1 docs are corrected. The focused tests pass: 11 in chatRows and 97 related. 1. MAJOR, partly fixed: a tool with the same input but a different call (
2. MINOR, partly fixed: close a tool, then Jump (
The merge pass will recheck only the new commits. |
gjc's tool parts carried no call id, so continuesHistory compared undefined with undefined: a rewind that called the same tool with the same input again, under another id and later, in a turn that kept its start, passed for the same history and the replacement inherited the opened state at its place. The parser now gives gjc tool parts their toolCall id (call_id; omp and pi still carry none, as api.contract.test.ts pins). server/conversation.test.ts runs both cases through the real parser: the same calls written again continue, the c2 -> c3 rewind does not.
…atever set it A tool closed while following set the timeline's layout hold; scrolling away and coming back to the end, with Jump to latest or by hand, left it set, so what grew next was not followed until the rows changed. ChatView now owns the hold and drops it whenever following resumes: Jump to latest, New messages, an own send, or the reader scrolling back to the end. The GJC streaming QA's jump scenario adds both: close while following, scroll away, then Jump or scroll back, then a row grows.
|
Both partly-fixed findings are now fixed in e9f5365 and 858a85f, pushed together. 1. MAJOR: same input, different call (e9f5365)
2. MINOR: close a tool, scroll away, then Jump (858a85f)
Stand-in for CodeRabbit. CodeRabbit is rate-limited on this PR, so a code-review agent (omo-native-code-reviewer) reviewed e9f5365 and the Jump commit before it was amended. It approved: no HIGH or MEDIUM. Its two LOWs:
Verified on 32c52fbfb (before the L1 amend):
Verified on 858a85f:
|
…leaves on its own Opening a run is reading there now (#1388), so after the snapshots open the Plan rows the chat offers Jump to latest while already at its end, and takes it back as soon as a layout change scrolls there. The helper had resolved the button, saw it detach before its click, and timed out after 30 s in CI's evidence pass (390 px, before the narration-boundary shot). It now presses the button only if it is there at that moment, and still requires it gone afterwards.
|
Merge pass: pulled from the merge queue, because CI at 858a85f fails a browser regression this change probably caused.
|
|
Fixed in de1105a (test-only, Cause. CI runs this script with
Playwright had already resolved the button, saw it detach before the click, and kept retrying an element that no longer existed until the 30 s timeout. On main, opening kept following, so So the button was not covered, it does detach, and following does come back: it came back on its own before the click. The helper now clicks the button through the DOM only if it is there at that instant (the query and the click happen in the same step, so no resolve-then-detach race remains). Either way it still requires Verified at de1105a, all through
Codex (gpt-6.1-sol, xhigh) on the delta: "No HIGH or MEDIUM findings." It confirmed the pill's appearance and removal match the follow logic. It noted one LOW: an absent pill does not prove exact end geometry (the 48 px follow allowance). That weakness predates this commit and is how the helper worked on main, so I left it unchanged. |
|
CI is green at de1105a: Browser regression (other scripts) now passes, along with ui-regression, Fast checks, Integration and Native Windows install. Ready for the merge pass to recheck the new commit. |
|
Merge pass review-work report. Overall: PASSED. This is an ordinary client change, gated by Codex Sol over three rounds, an omo code reviewer and the merge pass's own checks.
Final head:
de1105a (test only) fixes the CI failure at 858a85f: CodeRabbit did not review this PR (rate-limited or skipped). The reviews above stand in for it. |
## 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.
… each update's own input and output (#1601) (#2161) ## Change Fixes the two gaps #1601 still had after #1846 folded contiguous todo updates into one Plan row. - **Focus survives the fold.** When the agent's second todo update turned the single row the reader was on into a Plan row, the focused button left the page and the keyboard focus fell to `<body>` (expansion already carried over since #1937). A row head now records in its timeline row's memory that it holds the focus (`useRowFocus`, `src/lib/chatRowState.tsx`). The head drawn in its place in the same render takes the focus back: the same update inside the opened run, or the run's own head when it is folded. Focus the reader moved elsewhere is never taken, a head that leaves with no replacement ends its record (a row scrolled back or reopened later never grabs the focus), and a fold while the reader is in another app keeps it too. - **Each attempt shows its own call.** An update opened inside the Plan row showed the parsed list again, which the run already shows above it. It now shows the input the agent sent (JSON indented, under "Input") and the output it got back, including "Show the whole output" for a cut one. Design: the Codex and Claude apps keep one row per action under the turn and keep the reader's place while a turn streams. This follows that, as #1846's DESIGN.md entry does; the entry now records both rules. "Input" is translated into Korean, Japanese and Chinese. A `changelog.d/` fragment is included. Closes #1601. ## Validation - Regressions that fail before and pass after: - `scripts/chat-gjc-streaming-browser-qa.ts` (browser lane): its todo scenario now asserts the focus is on the opened update inside the Plan row after the next update arrives. On main all four variants (1440/390 × append/rewrite) failed with the focus on BODY; on this branch all pass. - `src/components/ChatTurn.test.ts`: an opened two-update run shows each update's raw input and output. - `scripts/todo-snapshots-demo-regression.ts`: 21/21 pass (1440/390, light/dark, Korean), including "open, focused run through append and poll". - `bun run check fast`: green on b4d51fd (unit tests 7886 pass, 0 fail). On the final head 12c0c34 (one line added to `useRowFocus`), typecheck, build and generated files pass; the unit tests had one timeout in an untouched file, `scripts/ci-browser.test.ts` "creates evidence under .ci when CI has no CHECK_DIR" (5029 ms on a 5 s limit at host load ~66). Rerun on its own, it passes in 3.4 s (7/7). - Not covered by an automated test: the background-window case (headless Chromium always reports the page focused) and StrictMode's rehearsed unmount (the browser fixture renders without StrictMode). Both were reviewed at source level and confirmed by Codex's last round. - Reviews: omo-native-code-reviewer PASS (its MEDIUM, a window blur ending the record, fixed; the over-long DESIGN.md line fixed; other LOWs out of scope). Codex Sol 6.1 Fast xhigh: round 1 MEDIUM (a stale record after a head left with no replacement) fixed; round 2 MEDIUMs (StrictMode rehearsal, focus events deferred while the window is in the background) fixed; the confirmation round on the final head PASS with no findings.
## What OMO and GJC transcripts use omp's record format and fold, but were still left out of the live incremental parser: every poll of a growing OMO/GJC chat parsed the whole running task again. This routes `omo-transcript` and `gjc-transcript` through `incrementalLiveTurn`, as Codex, OMP and Claude already are, with the same committed-span SHA-256 check, page/generation invalidation and detached snapshots. Closes #1643. Based on #1634 by @Yoonwoo-Ha (closed while the refactor landed), ported to current main. - `ompReading()` gives the omp fold its options for both the one-shot and the live parse, so they cannot drift: since #1634, GJC tool parts carry their call ids (#1937 matches gjc's rewrites by them), and that option must hold on the live path too. - **Second commit:** OMO/GJC live entries get a cache budget of their own (8 entries / 32 MiB of source bytes, LRU, the same size as the existing one). With one shared budget, OMO/GJC entries pushed OMP, Claude and Codex chats read in turn out of the cache: two 13 MiB OMP chats plus one 11 MiB OMO chat made every OMP poll about 5x slower than main, Claude chats up to 9x. With the split, Codex/OMP/Claude keep exactly main's cache behaviour. - The fixes #1643 asked for: a `JSON.parse` count pins the speed-up (4 parses per appended record instead of 205 after 101 records); the discarded OMO title preview names its own task, so a leak would show; the stale comments are corrected. Plus tests for gaps a mutation run found: omo/gjc name no pictures and only gjc carries call ids, on both the live and the settled path; a growing in-place rewrite drops a title only the old bytes gave; and both budgets evict least-recently-read entries within themselves. ## Measured (copies of real transcripts, median per poll of a growing live task) | Chat | main | this PR | |---|---|---| | OMO, ~2 MiB live task | 7.4 ms | 2.9 ms | | OMO, ~11 MiB | 36 ms | 12 ms | | OMO, ~15 MiB | 52 ms | 16 ms | | GJC, ~2 MiB | 10.8 ms | 2.4 ms | | GJC, ~11 MiB | 64 ms | 12 ms | Quiet-host figures; under load the ratio held at 0.15-0.21 of main. A cold first read and a live task over the 16 MiB window are unchanged. ## Verification - **Differential, warm read vs a cold read of the same bytes** (independent Bun Worker readers): over 85k reads replaying 23 OMO, 22 GJC and 6 OMP real transcripts (copies; cut lines and UTF-8 characters, valid tails without a newline, same-size edits, truncation, gjc-style tmp+rename rewrites, context clears, held pages); about 140k-180k synthetic fuzz reads; and 60k reads under eviction pressure in both budgets. Everything that differed from a cold read is a class that already differs the same way on main. The one exception is listed below. - **Cache behaviour**: over thousands of Codex/OMP/Claude polls (mixes with OMO/GJC chats, either arrival order, chat switching, paused chats), the `JSON.parse` count and cache membership matched main on every poll. `liveTurnCacheUsage()` stayed within budget on every read, and no OMO/GJC read ever evicted a Codex/OMP/Claude entry. - **Contract tests** in an isolated herdr session: omo, gjc, gjc-native-live (with the installed gjc binary), omo-detached, omp, conversation-push and claude-session: 50 passed, 0 failed. - `bun run check fast`: 8345 passed, 0 failed. ## Known limits - Three or more OMO/GJC chats with live tasks of about 11 MiB read at the same time exceed their own 32 MiB budget and evict each other. Each miss costs about 15% more than main's reparse (the hash and the cloned snapshot). This is the thrash #1643 lists as a follow-up, and main has the same for Codex/OMP/Claude. A time-based admission rule was tried and dropped: it brought arrival-order effects, a chat-switching penalty, response-cache hits that never refresh its clock, and a wall-clock dependence. - The live cache's bound becomes 16 entries / 64 MiB of source bytes. An entry keeps normalized turns: 1-2 MiB of heap per 11 MiB tool-heavy task, up to about its source size for tasks of thinking and text. - One cosmetic difference, accepted, and one OMP already has on main: after the settled-page cache evicts a page while the live entry survives, the live OMO/GJC fold can keep a task title (or a sticky context window) that a cold read no longer finds. - pi and Command Code transcripts keep reparsing their live task; unchanged. `server/` is a remote bundle input, so the release that ships this raises `REMOTE_BUNDLE_VERSION`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Yoonwoo-Ha <274636136+Yoonwoo-Ha@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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:
history_idis 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 theirtoolCallid ascall_idfor 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.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:
ChatTimelineremembered a click on a disclosure only until the next rows change (manualLayoutcleared on everyrowsupdate). 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.rewriteEntriesafter compaction eviction and tool-output pruning; I read this from the installed gjc 0.18.12 binary). A rename is a new inode, sotranscriptStreamgives a newhistory_idand 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/conversationnow gives a gjc tool part its owncall_id, the optional fieldshared/protocol.tsalready 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
scripts/chat-gjc-streaming-browser-qa.ts(added to the browser lane). It feeds GJC-shapedmessage/toolCall/toolResultrecords one at a time through the realtranscriptPage("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:aria-expanded="true"aria-expanded="false"after a rewritetodoscenario (added with the rebase) opens a gjctodo_writeupdate 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.tsruns 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.tscoverscontinuesHistory: 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.jumpscenario 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.tspasses 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.tspasses, and so dochat-local-send-follow-demo-regression.ts(fix(chat): follow your own accepted send back to the end unless you navigate #1831),chat-browser-qa.tsandchat-tool-paths-demo-regression.tson the rebased branch.bun run check fastpasses: typecheck, build, generated files, workflow syntax, 6772 unit tests.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