The STREAM_DONE keep-open exception (#4970 round 6/7) was scoped to PINNED
followers only, on the assumption that unpinned readers 'preserve their viewport
normally'. That holds on desktop (overflow-anchor:none + JS snapshot restore) but
NOT on mobile.
Repro (isolated debug instance + CDP touch emulation, faithful to a real phone):
a reader who scrolls UP to read inside the just-settled turn is UNPINNED. At
STREAM_DONE the live worklog/thinking cards collapse into a compact summary,
shrinking the transcript by hundreds of px ABOVE their viewport. On mobile the
CSS resting value is overflow-anchor:auto, but _fixMobileScrollJank() flips an
inline overflow-anchor:none over the settle render — so native scroll-anchoring
is suppressed during exactly the frame the unpinned reader needs it to absorb the
above-viewport shrink. The content then leaps to the top of the latest turn (the
'往回大跳' report). Single-variable measurement: collapse-with-auto shifts the
reader 0px; collapse-with-none shifts -248px.
Fix: keep the just-settled worklog open for BOTH pin states (still one-shot
scoped to the turn that just settled, so historical worklogs still collapse
compact). This removes the shrink entirely, fixing the jump for every
device/anchor-mode instead of fighting the anchor engine. The #4856 guard and the
mobile overflow-anchor:auto CSS are untouched, so Boev's Android DOM-wipe fix and
akrhin's #MOBILESCROLL fix both stay intact.
- Rename _shouldKeepSettledWorklogOpenForPinnedFollow ->
_shouldKeepSettledWorklogOpenForStreamSettle (drops the _scrollPinned /
_messageUserUnpinned gate; keeps the one-shot stream-id token).
- Update the #4970 behavioral regression test to assert keep-open is TRUE for the
armed turn under BOTH pin states, FALSE for historical turns and after disarm.
Extract _turn_transcript_lacks_final_assistant_answer() so settlement
logic can inspect an already-merged transcript. The merge wrapper now
delegates to the pure evaluator without changing behavior.
Fixes#5141
#5172 (render server-initiated turns in a hidden tab, shipped v0.51.743 via
#5218) has a non-blocking multi-pane edge flagged in review: when a
server-initiated turn arrives for a session that is NOT the active pane, the
hidden-tab status poll called _attachServerInitiatedStream() and could not tell
whether the attach actually happened. _attachServerInitiatedStream returned
undefined in all paths, so the poll had no signal — a stop-on-call would cancel
polling while the turn was never rendered, leaving it invisible until the next
user interaction.
Fix: _attachServerInitiatedStream now returns a bool — true when this tab is
responsible for rendering the stream (attached, or already attached to the same
sid/streamId), false when the attach was not consummated (sid not the current
pane, invalid input, or a thrown error). _startHiddenActiveStreamPoll captures
the bool and only calls _stopHiddenActiveStreamPoll() when true; on false it
keeps polling and re-tries on a later tick (e.g. once the user switches the pane
back to sid, or the turn finishes and status returns active_stream_id=null).
Tests in tests/test_hidden_tab_server_initiated_turn.py: assert all return paths
are explicit bool (no bare return), the non-current-pane path returns false, and
the poll gates _stopHiddenActiveStreamPoll on the attach result.
Gate hardening for #5198: the new stream-owner map leaked entries on three
paths that bypass the teardown finally — worker early-return when the stream
was cancelled before startup (streaming.py, gateway_chat.py) and the direct
ACTIVE_RUNS stale-zombie prune (routes.py). Each now calls unregister_stream_owner.
Adds a regression test proving the early-return path no longer leaks.
Co-authored-by: starship-s <starship-s@users.noreply.github.com>
Add a user setting 'large_text_paste_as_attachment' (default: on) that lets
users paste large text inline instead of having it auto-converted to .md file
attachments. The toggle lives in Control Center → Chat tab and persists via
settings.json, following the established upstream pattern for boolean settings.