Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions src/web-ui/src/flow_chat/components/modern/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Also follow the repository and Web UI instructions in the parent guides.

| Changing | Read |
|---|---|
| the tail spacer, the follow target, pinning, holding, resizing, the footer, the reveal | `FLOWCHAT_SCROLL_STABILITY.md` |
| the tail spacer, the follow target, new-Turn revealing, holding, resizing, the footer | `FLOWCHAT_SCROLL_STABILITY.md` |
| history paging, the prepend, the viewport anchor, history presentation | `FLOWCHAT_HISTORY_PAGING.md` |
| anything that writes `scrollTop`, one-shot navigation, the diagnostic trail | `FLOWCHAT_VIEWPORT_REGISTER.md` |
| the virtualizer, item measurement, item keys, anything a row renders | `FLOWCHAT_VIRTUALIZATION.md` |
Expand All @@ -21,8 +21,8 @@ before reporting a defect as new.

## Reservation and Follow

- FlowChat reserves a resident tail spacer of about one viewport, sized from
`scroller.clientHeight` and nothing else.
- FlowChat caps the input footer plus resident tail spacer at three quarters of
`scroller.clientHeight`; the spacer also depends on the current footer inset.
- Static reservation is allowed; reactive compensation is not. Do not derive any
reserved height from a measured content height, a collapse delta, an animation
duration, or a streaming rate.
Expand All @@ -31,7 +31,11 @@ before reporting a defect as new.
- The follow target lives in `flowChatTailFollow.ts` as pure functions over
geometry. Keep it free of timers and mutation observers.
- `scheduleFollowToLatest` must not force the content end — the hold rule is
what keeps a collapse from moving the viewport.
what keeps a collapse from moving the viewport. During `revealing-tail` it
samples the blank crossing and performs no viewport write.
- A new Turn gets one physical-bottom placement after it enters the live-tail
projection. Streaming consumes the exposed spacer at fixed `scrollTop`, then
hands off to `hold-tail` when the blank closes.
- `useFlowChatFollowOutput` is the only continuous outer viewport writer.
- The follow's **write** may be eased; its **target** may not. Everything that
reads the follow — the settle budget and the at-tail band — reads
Expand Down Expand Up @@ -161,10 +165,10 @@ before reporting a defect as new.
Virtualizer-specific compensation stays in `VirtualMessageList`.
- "A new Turn" is `activeSession.dialogTurns.at(-1)`, never the end of the
projection. Do not qualify that identity by whether the Turn is on screen —
that belongs to the response, which defers until the Turn can be aligned.
that belongs to the response, which defers until the Turn can be revealed.
- Detecting one means the ledger **grew**, not that the identity changed. A
rollback truncates `dialogTurns` and moves that identity backwards onto a Turn
that was always there; read as an arrival it pins the survivor to the top.
that was always there; read as an arrival it reveals the survivor as new.
- An action that rewrites `dialogTurns` and wants the viewport moved announces
it — `FLOWCHAT_MESSAGE_SUBMITTED_EVENT` for giving up a navigated history
window, `FLOWCHAT_TURNS_ROLLED_BACK_EVENT` for settling on a new tail. The
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -526,8 +526,9 @@ mistake this separates.
**A tail-anchored window must grow with the session.** It stops at the newest
Turn that existed when it was cut, and nothing moves its end afterwards, so an
appended Turn is simply not rendered. That is worse than it sounds: `latestTurnId`
is read off the rendered items, so follow-output never learns the Turn exists —
no pin, no follow, and nothing to scroll to. `resolveTailWindowGrowth` is
comes from the ledger, so follow-output learns the Turn exists but cannot reveal
it from a projection that has no matching item; the arrival remains pending with
nothing to scroll to. `resolveTailWindowGrowth` is
level-triggered for that reason. An edge — "it reached the tail last render and
does not now" — is consumed whether or not the extension succeeded, stranding
the window permanently on one failure; the current state stays `'extend'` until
Expand All @@ -544,7 +545,7 @@ fallback, but it is the only branch that always shows the message just sent.
*ends*, and a history window re-cut moves that to a Turn which has existed for
hours: measured, navigating to Turn 2 landed correctly and was then overwritten
twice, because each window loaded on the way ended somewhere new and each of
those read as a submission, pinning the window's last Turn to the top.
those read as a submission, moving the window's last Turn as though it were new.

**Whether the Turn can be acted on is a second question, and it does not belong
in the identity.** Qualifying `latestTurnId` by "and it is on screen" makes a
Expand All @@ -555,7 +556,7 @@ and it is how navigating to Turn 29 ended on Turn 38.
to *detect* one, and the detector asked whether `latestTurnId` differed from
last render. A rollback truncates `dialogTurns`, which moves that identity
backwards onto a Turn that has been there all along — so undoing a message
pinned the Turn *before* it to the viewport top. `dialogTurnCount` separates the
moved the Turn *before* it as though it had just arrived. `dialogTurnCount` separates the
two: an arrival grows the ledger, and nothing else that rewrites `dialogTurns`
— a history page merging in above, a window re-cut, a hydration — moves the
last Turn at all, so requiring growth costs nothing and excludes every
Expand All @@ -566,8 +567,7 @@ truncation.
two dozen call sites write that array; inferring an action from its size is the
same mistake as inferring intent from `scrollTop`. So the rollback announces
itself through `FLOWCHAT_TURNS_ROLLED_BACK_EVENT`, exactly as a submission does,
and the transcript settles on the new tail — the Turn it was pinning is one of
the ones that stopped existing.
and the transcript settles on the new tail.

It takes the viewport whether or not follow owned it. A rollback at Turn N
removes N *and everything after
Expand All @@ -588,13 +588,13 @@ already been committed to. Edit-and-rerun does not announce: its truncation is
followed by a rerun whose Turn really is new, and announcing would spend a
visible movement on the way to it.

So the response carries it instead. A new Turn is answered by pinning it to the
viewport top; until it is in the transcript on screen there is nothing to align,
and the fallback — the end of real content is not a stand-in, because it
would leave the Turn unpinned or pull a reader out of a history window. The
answer is therefore **deferred, not dropped**: held in `pendingNewTurnIdRef` and
retried when the transcript next changes, which is exactly when the presentation
is restored to the live tail.
So the response carries it instead. A new Turn is answered by revealing the
resident tail blank with one physical-bottom placement; until the Turn is in the
live-tail projection there is nothing to reveal, and the old content end is not
a stand-in because it can pull a reader out of a history window before the new
Turn exists there. The answer is therefore **deferred, not dropped**: held in
`pendingNewTurnIdRef` and retried when the transcript next changes, which is
exactly when the presentation is restored to the live tail.

**Submitting is what gives up a navigated window.** `resolveTailWindowGrowth`
leaves such a window alone as the session grows, and that is right — a Turn
Expand Down
Loading
Loading