fix(transcript): reserve a stable slot for tail runtime status - #432
Merged
Merged
Conversation
The working / planning / run-activity row mounts and clears with the turn's phase, and each of those rows is its own box at the tail of `.thread-content`. A row appearing between two tool rows therefore grew the content by 40.125px (46 CSS px on the reporter's Windows / DPR=1 build), and pinned follow re-baselined to the new bottom, pushing the rows the user was already reading up by the same amount. Clearing the status moved them back. Reserve that row for the whole running turn instead: an always-mounted `.transcript-runtime-status` lane, sized from the indicator's own box (8px/4px padding, 2px/8px margins, one line of `--text-sm-plus`), holds the indicators and stays reserved, empty and invisible while a concrete row, a permission card, or the answer owns the tail. An idle transcript renders no lane at all, so a finished transcript keeps its exact layout. Follow, the content ResizeObserver, automatic disclosure and the live-region roles are unchanged. The new React/Chromium scenario mounts the production ChatTranscript in a real scrolling pane and asserts that the content height, scroll extent, scroll offset and the first and last row positions stay within 0.01px while the waiting status appears and after it clears, that the empty lane stays invisible in both themes, that the live region survives, and that an idle transcript has no lane. Removing the reserve fails it by 40.125px of content height and 40px of row movement. Refs #323
…ipt-runtime-status-slot
The English E2E plan gained E2E-CHAT-runtime-status-keeps-row-position with issue #323, but the Chinese mirror was left without the section. Every other scenario added recently (the skill-market, queue and chat ones) carries a zh-CN counterpart, so this restores the convention. Documentation only; no code, behaviour, or assertion changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The transcript's tail status row ("waiting for model", planning, working) was conditionally rendered straight into
.thread-content, so the row's line box, padding and margin entered and left the normal document flow with every phase change. While the user is pinned to the bottom, the contentResizeObserverinuseTranscriptScroll.tsre-baselines follow synchronously, so the height change moved rows the user was already reading. Reported and measured at 46 CSS px of heading displacement on Windows/Electron 43.6.0 at DPR=1, with no manual disclosure and no user scrolling involved (that separate path is #324).Changes
apps/desktop/src/features/chat/transcript/ChatTranscript.tsx.transcript-runtime-statuslane, gated onruntimeStatusLane(==transcriptRunning). All three indicators already requiredtranscriptRunning, so the gate cannot hide one: only the wrapper's lifetime changed.apps/desktop/src/styles/chat-shell.css.transcript-runtime-statusreserves exactly one indicator box:calc(var(--text-sm-plus) * var(--leading-body) + 22px)— the--text-sm-plusline at--leading-bodyplus the indicator's own8px/4pxpadding and2px/8pxmargins, which is whydisplay: flow-rootis needed so an empty lane does not collapse its margins away. Measured reserve: 40.125 px, the indicator's own height.No scroll compensation, no timeout-based re-anchoring, and the
ResizeObserver,overflow-anchorsettings, pinned-follow rule and automatic disclosure rules are untouched.Validation
On the integrated local
main(merge commitb1bd0f2b, after merging this branch):pnpm build:jspnpm --filter @pi-desktop/desktop typecheckpnpm lint:biomepnpm -r --if-present testpnpm test:e2e:transcriptpnpm test:e2e:theme-surfacesfailures: []pnpm test:e2e:layoutpnpm test:e2e:bootThe new scenario
E2E-CHAT-runtime-status-keeps-row-positionmeasures the realChatTranscriptin a real Electron viewport with the app's built stylesheet linked in (the previous fixture bundled components withloader: { ".css": "empty" }, so a CSS-reserved slot was not measurable — the harness now links the built CSS the same waytest:e2e:theme-surfacesalready does, and asserts the linked stylesheet list matches the built one so a stale build cannot pass).Geometry is byte-identical across empty lane → waiting → cleared:
Removing the
min-heightand rebuilding makes the scenario fail with exactly the reported symptom (contentHeight moved by 40.125px), so the test is not vacuous.Specs
docs/spec/04-ux/08-component-spec.md— the tail status lane contract.docs/spec/06-delivery/04-e2e-test-plan.md—E2E-CHAT-runtime-status-keeps-row-position.Known boundary
The lane's reserve starts and ends with the turn (
transcriptRunning), and entering/leaving the reading window removes it. Both are real content changes the transcript already follows; the fix targets the intra-turn flicker the issue reports. A one-line reserve can wrap in an extremely narrow pane; the measured scenario is the issue's own width regime.Fixes #323