Skip to content

Dock awaiting-card tail is consumer-derived render state, not semantic state #913

@srid

Description

@srid

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions