Repository navigation
fix(native-chat): sending brings the latest into view and follows the reply - #24514
Conversation
7b51aa8 to
19144c4
Compare
|
There was a problem hiding this comment.
ℹ️ No critical issues — one rough edge inline.
Reviewed changes
This run reviewed the full PR (19144c4236, 66 files): a renderer-only rework of the native-chat scrolling model, with no wire, host, path or shortcut changes.
- Send reveals the latest —
NativeChatMessageListexposesrevealLatestthrough an imperative handle, and the composer plus every non-composer submit (retry, prompt answers, queue steer/resume, host commands) report through a new optionalonSubmitted. Mobile/host-delivered messages and Stop deliberately do not move the reader. - Follow-state machine —
nextFollowStatereplaces the singlefollowingboolean withfollowing | detached(scroll|navigation|open), so opening a row detaches, and closing the last opened row resumes following wherever the end then is. - Jump controls — new
NativeChatJumpControlswith a solid "Jump to latest" and an up-arrow "Jump to top" that pages older history via the newHISTORY_STARTtarget inuseNativeChatRailHistoryJump. - Accessibility & motion — transcript is a focusable named region, focus hands back to it when a focused jump button hides, and reduced-motion disables smooth align and the spinner.
- Tests — new unit/component suites for the state table, reader navigation and jump controls, plus a source-scan ratchet over send sites.
ℹ️ Nitpicks
native-chat-send-reveal-ratchet.test.tsonly asserts the literal tokenonSubmitted?.()exists somewhere in a file that emitsemitNativeChatMessageSent(, so a future send path could add the token in a comment and still pass. The anti-vacuoussenders.length > 0check is good; matching the call site (or asserting the token appears after the emit within the same function) would make the guard actually pin behavior.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (2)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 4 remain after this review. 📝 WalkthroughWalkthroughThe changes add an event-based transcript follow model that accounts for reader scrolling, navigation, and opened rows. The message list gains accessible region and focus behavior, and history jumps stop when the pane is hidden. Submission and session-action paths now invoke latest-message reveal callbacks under specified acceptance conditions. Reduced-motion preferences control message alignment scrolling. Tests cover these behaviors, including follow-state transitions, submission callbacks, reveal actions, and accessibility. Priority: ➖ Normal Merge Risk: 🔵 Low · up to A focused test run may fail, and an unsuccessful queued action may move the reader unexpectedly. These limited issues do not affect persisted data or host behavior; the PR is mergeable with owner awareness. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The inspected changes affect conversation navigation without adding command permissions or data access. No security regression was identified in those paths, but some failure and session-switch behavior remains unverified. Retained concerns Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
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 |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
This run re-reviewed the two commits since the prior pullfrog review (19144c4236): the held answer reveal and the empty-answer guard. Both address the prior inline concern cleanly.
- Held answer reveal —
useStructuredNativeChatSubmitReveal.revealingRespondnow captures a reveal at click time and only fires it ifrespondreturns non-null and no reader act intervened. Backed by a newreaderActsRefcounter anduntilReaderActsinuse-native-chat-transcript-scroll, which count every non-programmatic-scrollfollowevent. - Empty-answer guard —
NativeChatResolvedViewgatessendAnswerontargetPtyId && hasAskAnswer(prompt, selections)andsendRawontargetPtyId, exactly matching the delegate's early-return conditions, so a no-op answer or a vanished PTY no longer moves the reader. - Ratchet tightened — line comments are stripped before matching
onSubmitted?.(), closing the comment-only loophole from the prior nitpick. Tests cover the empty answer and the missing terminal.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
…umps to latest and top Sending while scrolled up left the reader parked in old history, and the way back was a faint button. A send now resumes following, opening a row to read it stops following until it is closed, and Jump to latest and Jump to top sit above the composer. Refs stablyai#23797.
…o scrolled away An answer's reveal is now held from the click and dropped if the reader acts before the host accepts. In the terminal lane, an answer with no terminal to write to reveals nothing.
The terminal lane's reveal now uses the same check the send does, and the send-site guard test ignores comments.
cbedfa9 to
f351fc5
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
d67decd3-7ec4-4f35-8365-afb19a458eb5
📥 Commits
Reviewing files that changed from the base of the PR and between cbedfa91c75f0e83b0e7bfecdac00fc9e2dcc57d and f351fc5.
📒 Files selected for processing (11)
src/renderer/src/components/native-chat/NativeChatResolvedView.reveal-latest.test.tsxsrc/renderer/src/components/native-chat/NativeChatResolvedView.tsxsrc/renderer/src/components/native-chat/NativeChatStructuredSession.test-harness.tsxsrc/renderer/src/components/native-chat/NativeChatStructuredSession.test.tsxsrc/renderer/src/components/native-chat/NativeChatStructuredSession.tsxsrc/renderer/src/i18n/locales/en.jsonsrc/renderer/src/i18n/locales/es.jsonsrc/renderer/src/i18n/locales/fr.jsonsrc/renderer/src/i18n/locales/ja.jsonsrc/renderer/src/i18n/locales/ko.jsonsrc/renderer/src/i18n/locales/zh.json
🚧 Files skipped from review as they are similar to previous changes (6)
- src/renderer/src/i18n/locales/es.json
- src/renderer/src/i18n/locales/ko.json
- src/renderer/src/i18n/locales/en.json
- src/renderer/src/i18n/locales/ja.json
- src/renderer/src/i18n/locales/zh.json
- src/renderer/src/i18n/locales/fr.json
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 1 remain after this review.
| expect(stubs.revealLatest).toHaveBeenCalledTimes(3) | ||
| }) | ||
|
|
||
| it('leaves the reader where they are when the terminal is gone and nothing is written', () => { |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Give this test its own session fixture.
When this test runs alone, stubs.session is still null. NativeChatResolvedView then reads session.messages and throws before the assertions run. Initialize the session for each test, and reset it during cleanup.
| {/* Host-held drafts, never transcript rows. Above the status area, so running shells and agents sit next to the composer. */} | ||
| <NativeChatQueuedMessageList | ||
| controller={controller.queuedMessages} | ||
| controller={submits.queuedMessages} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
git diff --unified=50 f873aaac5b2544c02f17d98a188b75dc843a3de7 f351fc5e0ce9ce77007295b05bd3ae9369dd0ac0 -- src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx
printf '\n--- queue references ---\n'
rg -n --glob '*.{ts,tsx}' 'queuedMessages|revealLatest|useStructuredNativeChatSubmitReveal' src/renderer/src
printf '\n--- candidate controller declarations ---\n'
rg -n --glob '*.{ts,tsx}' '(^|[[:space:]])(async[[:space:]]+)?(steer|resume)[[:space:]]*[:=(]' src/renderer/srcRepository: stablyai/orca
Length of output: 45117
🏁 Script executed:
printf '%s\n' '--- reveal wrapper ---'
nl -ba src/renderer/src/components/native-chat/use-structured-native-chat-submit-reveal.ts | sed -n '1,115p'
printf '%s\n' '--- queue controller types and methods ---'
nl -ba src/renderer/src/components/native-chat/use-structured-agent-session-queued-messages.ts | sed -n '1,205p'
printf '%s\n' '--- direct queue transport mutation ---'
rg -n --glob '*.ts' --glob '*.tsx' 'const mutate|mutate\s*=|queuedMessagesSteer|queuedMessagesResume|agentSession\.queuedMessages' src/renderer/src/components/native-chat src/renderer/src | head -100Repository: stablyai/orca
Length of output: 14894
🏁 Script executed:
printf '%s\n' '--- mutation implementation ---'
nl -ba src/renderer/src/components/native-chat/use-structured-agent-session-mutate.ts | sed -n '1,280p'
printf '%s\n' '--- queue RPC handlers and contracts ---'
rg -n --glob '*.{ts,tsx}' 'agentSession\.queuedMessageSend|agentSession\.queuedMessagesResume|queuedMessageSend|queuedMessagesResume' src | head -160Repository: stablyai/orca
Length of output: 17082
🏁 Script executed:
printf '%s\n' '--- queued result contracts ---'
rg -n -C 8 'AgentSessionSendResult|AgentSessionQueuedMessagesResumeResult|QueuedMessagesResumeResult|QueuedMessageAction' src/shared/agent-session-wire.ts src/shared/agent-session-queued-message-wire.ts src/shared/agent-session-wire*
printf '%s\n' '--- host queue action methods ---'
nl -ba src/main/native-chat/agent-session-wire/structured-agent-session-queued-mutations.ts | sed -n '155,225p'
nl -ba src/main/native-chat/agent-session-wire/structured-agent-session-queued-mutations.ts | sed -n '300,365p'
printf '%s\n' '--- service dispatch wrappers ---'
nl -ba src/main/runtime/rpc/methods/structured-agent-session-queued-methods.ts | sed -n '1,45p'Repository: stablyai/orca
Length of output: 13326
🏁 Script executed:
nl -ba src/renderer/src/components/native-chat/use-structured-agent-session.ts | sed -n '175,260p'
printf '%s\n' '--- reveal hold behavior ---'
nl -ba src/renderer/src/components/native-chat/use-native-chat-reveal-latest.ts | sed -n '1,70p'Repository: stablyai/orca
Length of output: 6341
🏁 Script executed:
nl -ba src/renderer/src/components/native-chat/use-structured-agent-session.ts | sed -n '155,183p'Repository: stablyai/orca
Length of output: 1405
Reveal only after queued actions succeed.
submits.queuedMessages reveals before host acceptance. A stale or withdrawn card can make steer() fail, and resume() can return resumed: false when no pause remains. Both methods discard their results. steerNewest() also reveals as soon as a local card is available, before its send completes. Propagate each action’s outcome and use a held reveal only after Send-now is accepted or Resume lifts the pause. This keeps the reader in place if the action fails or they scroll first.
nwparker
left a comment
There was a problem hiding this comment.
Reviewed local-send intent, follow/detach state, virtualization and stale-window guards. Repaired rebase omissions in Retry reveal and cancelAsk forwarding; retry regression verified red/green. 2633 native-chat tests plus focused retry/cancellation tests, web typecheck and quality gate pass. Explicit jump-to-top loads older pages and can take time/memory on very long or remote sessions.
Required PR Checks verify passed on this exact commit: f351fc5.
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
This run re-reviewed the one commit since the prior pullfrog review (cbedfa91c7): a rebase-repair commit that restores two pieces of upstream behavior the branch had dropped while it was based on an older main.
- Question cancellation restored —
NativeChatResolvedViewdestructures and forwardscancelAskfromuseNativeChatInteractiveSendinto itsNativeChatInteractiveSendobject and its dependency list, so rejecting an active question regains the upstream cancel path (the type gainedcancelAskonmain, so the omission also broke typecheck). - Retry reveal restored —
NativeChatStructuredSessionpassesretryDelivery(wrapped byuseStructuredNativeChatSubmitReveal) instead of the rawcontroller.retry, so retrying a delivery notice brings the latest into view again. - Coverage — a new
NativeChatStructuredSessiontest pins reveal-on-retry end-to-end (it fails against the rawcontroller.retry), and the resolved-view mock gains thecancelAskstub.
I traced the sibling submits wrappers (messageListRef, retryLaunch, queuedMessages, respond, revealLatest) and confirmed they were all still threaded through; retry was the only stale one. Both changed sites are correct and the added test can fail.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
Jump to top moves to its own PR so this one lands the send reveal and follow behaviour on their own.
|
I split "Jump to top" out of this PR so the send and follow behaviour can land on its own. The up-arrow button and its "start of conversation" history jump now live in #25702 (credited to Kelvin), stacked on this one. Pushed two commits here, no force:
The description now says "Part of #23797"; #25702 closes it. Web typecheck and the 67 native-chat test files touched pass. |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
This run re-reviewed the commits since the prior pullfrog review (f351fc5e0c, #5410923145): a merge of main and a follow-up commit that removes the "Jump to top" feature from this PR.
- Jump to top removed —
NativeChatJumpControlsnow renders only the "Jump to latest" button (the Tooltip, spinner andArrowUp/Loader2are gone);useNativeChatRailHistoryJumpdropsHISTORY_START,startFromBeginningandstartPending;useNativeChatTranscriptScrolldropsscrollToTopandawayFromTop;jumpToStartis unthreaded fromNativeChatMessageList; and thecomponents.native-chat.jumpToTopkeys are removed from all six locales. Jump to top moves to #25702. - Tooltip test scaffolding retired — the
native-chat-app-root-test-render.tsxrender wrapper (added only to supply theTooltipProviderthe removed tooltip needed) is deleted, and the affected suites revert to importingrenderfrom@testing-library/reactdirectly. - Rail-history-jump coverage restored — the
useLaneRailHistoryJumptests revert to the id-targeted API, with the hidden-pane abort folded back in as its own case alongside list unmount. mainmerged in — the branch now carries upstream'sanswerCommandLocally,onSlashCommand(command, output?), subagent roster disclosure,promptsUnanswerableandagentprop work; the reveal/follow feature is threaded through it unchanged.
I verified no reference to the removed symbols (jumpToTop, startFromBeginning, startPending, HISTORY_START, jumpToStart, native-chat-app-root-test-render) survives in the renderer, and that showOlderHistory and restoreScrollOffset still have live callers. The removal is complete and self-contained, and the merge preserved the feature.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
…ange The solid, fading Jump to latest button moves to the UI PR (stablyai#25702) so this one carries only the scroll behaviour.
There was a problem hiding this comment.
ℹ️ No critical issues — one minor note inline.
Reviewed changes
This run re-reviewed the one commit since the prior pullfrog review (b5974c11a8, #5421800817): a scope-reduction that returns "Jump to latest" to its pre-PR form.
- Jump-to-latest control reverted —
NativeChatJumpControlsand its test are deleted, and the button returns to the inline, conditionally rendered form inNativeChatMessageListwith the previous translucentbg-card/90 backdrop-blurstyling. - Message-list plumbing tidied —
NativeChatMessageListnow destructures{ scrollToBottom, scrollMessageToTop, ...follow }fromuseNativeChatTranscriptScrolland derives itsreftype from the handle hook (Parameters<typeof useNativeChatMessageListHandle>[0]); the reader-navigation suite drops the focus-handback case and itsinertcheck.
Note: 1 inline comment(s) dropped because they did not anchor to lines inside the PR diff:
src/renderer/src/components/native-chat/NativeChatMessageList.tsx:425-435(RIGHT) — line 435 (RIGHT) is not inside a diff hunk
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
|
Follow-up to the incremental review, whose inline note was dropped by the anchor pre-flight (the button body is unchanged from Minor: reverting to the inline button also drops the focus handling No critical or blocking issues in the delta.
|
|
Split this one more step: this PR is now the scrolling behaviour only (send brings the latest into view and follows the reply, opening something stops following, keyboard access to the transcript). The visual changes moved to #25702: the solid, fading "Jump to latest" (with its focus hand-off when it hides) joins "Jump to top" there. This PR renders "Jump to latest" exactly as To stay under the file's line limit with the button back inline, |
# Conflicts: # src/renderer/src/components/native-chat/NativeChatStructuredSession.test.tsx # src/renderer/src/components/native-chat/use-native-chat-composer-submit.test.tsx
|
Split the button work one more step, so there are three PRs, each reviewable on its own:
I also merged current |
…layout Replace open-row lifetime tracking with bounded position preservation, and tie delayed reveals to the originating reader and session. Keep queued drafts and picker actions from navigating the transcript. Co-authored-by: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com>
A refused queue Resume moved readers before the host answered, and hiding during disclosure settlement could leave a reopened view at the tail without following new output. Resume now reveals its originating reader only after success, and a restored pane derives following from its actual distance to the end. Merge current main while preserving its chat layout and the existing reader ownership and submission mechanisms. Co-authored-by: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com>
The rail test rejected the retained-position takeover before a message jump. Check its offset and the selected row target, then verify the jump stays spent and a later diff can reveal. Merge main to resolve the transcript styling conflict, preserve incoming typography and remove obsolete test props. Co-authored-by: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com>
Pending reader takeover could keep an earlier end target once a duplicate 250 ms gate expired. Let the existing pending-frame check keep the reader's actual offset until that frame ends. Move unchanged interactive reveal and approval projection into their existing modules so both view files meet the line limit. Co-authored-by: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com>
Integrate the current main branch so the corrected protocol lint can let required checks run. Preserve the chat scrolling change and both locale updates, and format two existing test calls. Co-authored-by: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com>
|
Sending in a long native chat could leave the new message below the viewport. The original patch could also move the reader after they had scrolled away or changed conversations, and could leave following paused after an opened tool row disappeared. Reviewed and pushed head: The opened-row decision follows the common pattern: preserve the clicked position while expansion or collapse settles, then derive following from the reader's position. Leaving a row open does not pause following for its lifetime. The existing virtualizer owns cancellation and the pending reader-scroll frame, so there is no second deadline that can stop reconciling a still-pending frame. The review covered the whole PR adversarially, including an independent architecture proposal drawn from the issue, separate submission and scroll-geometry reviews, focused fixes, fresh reviewers after changes, and a final readiness review without counsel. All 14 scored findings are fixed: nine P2 and five P3. No P0/P1 candidate or open proven finding remains. The table gives source lines at discovery, with evidence and the correction; filenames are under
Fixes landed in 5441267 (R1), ac137ff (R2), efefff8 (R3), and 04a1286 (R4). Final head 47a1fd9 normally merges pinned main Deferred P3 R1-D1: the solid Jump to latest control is #25759, and Jump to top is #25702. This PR does not complete all of #23797; semantic history restoration after an actual transcript unmount remains a wider limitation. Final-head local verification passes: 409 targeted tests across 29 explicit files, one queued renderer typecheck, one serial queued Node typecheck, and scoped lint/format/quality checks across all 59 changed source files. Only two unchanged React effect warnings remain. The promoted pending-frame regression fails before its correction and passes afterward. Counts are from the final committed run, not added across rounds. Hook/DOM geometry tests do not certify Chromium rendering or live-provider delivery. Final CI is complete: 32 checks, 16 successful, 16 skipped, zero failures or cancellations; all four workflows succeeded. The successful PR Checks run includes static analysis/typecheck, all five Node unit shards, relay integration, both package jobs (including Windows), and aggregate verification. Headless persistence, Mobile verification and test line-count checks also passed. The static/typecheck and unit shard 2 checkout logs confirm merge commit The 16 skipped checks remain unverified: end-to-end, cross-version wire, Git compatibility, shell contracts, managed hooks on Node 18, browser provider, Codex index repair, xterm synchronization, real IME, real WSL terminal, mobile bundle/tests-on-main, desktop template, and three Linux runtime compatibility jobs. A successful Windows package is not Windows runtime QA. Rendered/Chromium, SSH and Windows runtime QA were not completed, and there are no final rendered screenshots. M4Air was overloaded and serialized behind another live rig; earlier builds did not reach a usable renderer. Windows Codex startup was blocked by a confirmed update prompt and launcher-reported sign-in requirements, with sign-in disallowed; the low-spec attachment attempt also failed before execution. Openclaw was unreachable. No final current-main/PR rendered comparison ran. Brennan explicitly instructed us to finish without waiting for an M4Air slot. Actual provider/SSH delivery, physical input, reduced-motion rendering and mobile emulator behavior remain unverified. Earlier M4Air drivers were released and their owned processes and disposable profiles cleaned up. The launcher closed the high-spec Windows prompt terminal; the low-spec attachment was abandoned before an observed app launch. Remote evidence checkouts are retained. The PR has not been merged, and Brennan retains that decision. |
# Conflicts: # src/renderer/src/components/native-chat/NativeChatJumpToLatest.tsx # src/renderer/src/components/native-chat/NativeChatMessageList.tsx # src/renderer/src/components/native-chat/NativeChatStructuredSession.test-harness.tsx # src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx # src/renderer/src/components/native-chat/use-structured-agent-session.ts
… at the press Opening or closing a row used to stop the transcript from following the newest output. A reader at the live end who expanded a tool run then lost the stream. Sends that wait on the host (answers, commands, goals, option changes) only brought the latest into view after the host replied, so the reader watched nothing happen at the press. Now following stops only on a reader gesture: an upward wheel or scroll key when there is content above, a scrollbar press, a touch drag once it has carried the view off the end, or a press on content while already away from the end. Opening, closing, layout changes and the app's own scrolls never stop it, and returning to the end resumes it. Every local send reveals the latest at the press. A message that waits as a queued card, a message from another device, and Stop leave a reader who scrolled up where they are. Queue Resume still reveals only after the host lifts the pause, and only in the pane that pressed it while that pane is shown. Removes the disclosure position hold, the reader-opens wiring, the held reveal and its reader generation tracking. The queued-card decision moves into the outbox send so the session hook stays under the line limit. Also restores the transcript label import and the approval card's verified send that the merge with main dropped.
# Conflicts: # src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx # src/renderer/src/components/native-chat/use-structured-agent-session-outbox.ts # src/renderer/src/components/native-chat/use-structured-agent-session.ts
|
Reworked the scrolling rules to the common pattern for chat transcripts, and merged current What's different from the previous head:
The production diff shrank from about +927 to +608 lines, and two files are gone ( Also from the All 386 native-chat and structured-session test files pass, plus the web typecheck and lint. For each of the 15 rules, removing it makes its test fail. The running app hasn't been re-tested since the rework; a new recording will follow. The PR description is updated. This supersedes the earlier review summary (#24514 (comment)). That one describes the pre-rework design: the opened-row hold and held reveals are gone now. |
…r-stop Main's #24514 reveals the transcript on a send and on a Resume that lifts the pause. This branch's composer Resume goes through the same reveal; the queue's Resume answers whether it lifted the pause on both surfaces. The composer's awaitable structured send keeps main's command reveal at the press, and main's new navigation test fires that send without awaiting it. Line limits after the merge: the composer's working/Stop rule moves into the structured stop controls; the queue controller takes the transport's fields by spread; the option picker request type is named; test harness resets combined.
…brennanb2025/nc-host-send-queue - Queue UI (#24586) taken as main has it: paused header, Resume, Steer, clear-queue question, `queueSendsNext` working state, composer Resume and queue hold. - The chat's send answers 'queued' when the transcript draws it as a card (main's rule, read through the in-memory sends' own projection), so the composer moves the reader only for a bubble. - The failed-start send wrapper passes that answer through; the submit reveal hook drops the per-message Retry this branch removed (launch Retry still reveals). - The pending-send projection takes any send shape, so the node project's queued-card test does not reach the renderer store. - Deleted outbox files stay deleted; #24514's queued-card admission test is ported to the sends hook.

ELI5
In the desktop chat, if you scrolled up to read something and then sent a message, you stayed parked in old history and had to find your way back: your message and the reply arrived off-screen.
Now sending takes you to your message and follows the reply. Only you scrolling away stops that. This PR is the scrolling behaviour only; the solid "Jump to latest" landed in #25759, and "Jump to top" is in #25702.
What Changed
Before
After
Mechanism
onSubmittedcallback when you press send, and each pane wraps its other sends (retry, answers, queue actions) with the same command. Each pane has its own list, so a reveal never moves another chat. A test fails if a future send path forgets to report.native-chat-reader-scroll-input.ts. Scroll events alone never stop following, so layout and size changes can't knock you off the end.Why
The issue asks for one predictable scrolling model, and this is the common pattern for chat transcripts: your own scrolling is the only thing that leaves the end, and sending is the only thing that brings you back. Revealing at the press, rather than after the host accepts, needs no tracking of which send is still in flight, and an answer the host refuses has still shown you the latest, which is harmless.
Alternatives considered:
maindid. It can't tell your scroll from the content changing under you, so a tool's output collapsing could knock you off the end.Not in this PR: restoring your position when you reopen a session, screen-reader announcements for new messages, and performance work on long transcripts.
Linked Issue
Part of #23797. Jump to top, also asked for there, is in #25702; the solid Jump to latest landed in #25759.
Visual Proof
Recorded on an earlier version of this PR (before the button changes were split out and before the scrolling rework below), so it shows the old button looks and the old "opening a row stops following" rule. A new recording is to follow.
CleanShot.2026-10-02.at.04.33.13.mp4
Testing
All 386 native-chat and structured-session test files (4,146 tests) pass, plus the web typecheck, lint and formatting. Each rule above has a test that was checked to fail with that rule removed (15 rules).
Not yet re-tested in the running app after the rework; macOS only before it. Not tested on Windows, Linux or over SSH; the change is renderer-only and touches no paths, shortcuts or host calls.
I manually tested these changes locally
Automated tests added/updated, or explained why not below
Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.Notes
Renderer-only. No change to what a paired client and host exchange, no new shortcuts, no file paths. Per scroll event the list sets two booleans that skip re-rendering when unchanged; per streamed chunk the path is the same as before.
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)