The AwaitingCardBody in the dock paints a small preview of each awaiting terminal's last-N lines so the user can see what the agent is asking about without switching to the tile. Today that preview is computed by the card itself: a createEffect per card polls every 250 ms via peekTick, calls tailBuffer(xterm, N), and writes a local tail signal. The card renders from its private signal.
This shape produces a visible UX bug and a subtler design issue.
Symptom
Switching the active terminal while an agent is in the waiting state makes the awaiting card's tail preview flash — the rendered lines collapse to zero for one tick, then refill to N lines on the next.
The trigger is the xterm fit/resize that follows activating a tile:
- The tile's cell grid is briefly reshaped.
buffer.active.length is momentarily small enough that every row reads as alt-screen chrome and tailBuffer returns []. The next peekTick (~250 ms later) finds a populated buffer and refills.
- During the same window,
getTerminalRefs(id) can transiently return undefined as the component reconciles.
Either condition fires setTail([]) in the old effect, the card's <Show when={tail().length > 0}> unmounts the tail div, and you see the flash.
PR #909 (a4c1a90c) papers over the symptom by holding the cached tail across transient empty reads. That eliminates the user-visible flash but is a workaround at the consumer — the underlying coupling is still in place.
Underlying issue
The card both computes the tail and renders it. That couples three concerns:
- Cadence is dictated by the dock's heartbeat (250 ms
peekTick) rather than by when content actually arrives. The walk runs whether or not anything has changed.
- Cache is per-consumer. Each card holds its own
tail signal, so a card that mounts mid-session paints [] for one tick before its first effect run completes. Other surfaces that might want the same data (Cmd+K preview, future sidebar pane, inspector) would have to re-derive independently.
- Lifecycle ties the tail's existence to the card's mount. If the card unmounts (the agent leaves waiting), the snapshot of what it was last waiting on is discarded — even though that snapshot is still semantically meaningful ("the last thing this agent asked you about").
The tail is conceptually semantic state — a snapshot of what the agent was waiting on at the moment it entered the waiting state — not render state derived from xterm's current cell grid. Treating it as render state is what produces the flash, the per-consumer caches, and the lifecycle coupling.
Out of scope
Implementation deliberately not specified — opening this so the problem is captured. Designed in a separate PR.
Related
The
AwaitingCardBodyin the dock paints a small preview of each awaiting terminal's last-N lines so the user can see what the agent is asking about without switching to the tile. Today that preview is computed by the card itself: acreateEffectper card polls every 250 ms viapeekTick, callstailBuffer(xterm, N), and writes a localtailsignal. The card renders from its private signal.This shape produces a visible UX bug and a subtler design issue.
Symptom
Switching the active terminal while an agent is in the waiting state makes the awaiting card's tail preview flash — the rendered lines collapse to zero for one tick, then refill to N lines on the next.
The trigger is the xterm fit/resize that follows activating a tile:
buffer.active.lengthis momentarily small enough that every row reads as alt-screen chrome andtailBufferreturns[]. The nextpeekTick(~250 ms later) finds a populated buffer and refills.getTerminalRefs(id)can transiently returnundefinedas the component reconciles.Either condition fires
setTail([])in the old effect, the card's<Show when={tail().length > 0}>unmounts the tail div, and you see the flash.PR #909 (
a4c1a90c) papers over the symptom by holding the cached tail across transient empty reads. That eliminates the user-visible flash but is a workaround at the consumer — the underlying coupling is still in place.Underlying issue
The card both computes the tail and renders it. That couples three concerns:
peekTick) rather than by when content actually arrives. The walk runs whether or not anything has changed.tailsignal, so a card that mounts mid-session paints[]for one tick before its first effect run completes. Other surfaces that might want the same data (Cmd+K preview, future sidebar pane, inspector) would have to re-derive independently.The tail is conceptually semantic state — a snapshot of what the agent was waiting on at the moment it entered the waiting state — not render state derived from xterm's current cell grid. Treating it as render state is what produces the flash, the per-consumer caches, and the lifecycle coupling.
Out of scope
Implementation deliberately not specified — opening this so the problem is captured. Designed in a separate PR.
Related
a4c1a90cin PR Dock: canonical navigator across desktop & mobile #909.packages/client/src/terminal/bufferTail.ts.AwaitingCardBodyinpackages/client/src/canvas/dock/Dock.tsx.