Skip to content

Agent replies clamp from the tail: show the end, Show more at the top - #589

Merged
ZacxDev merged 3 commits into
mainfrom
tailclamp-agent-replies
Nov 11, 2026
Merged

ZacxDev merged 3 commits into
mainfrom
tailclamp-agent-replies

Conversation

@ZacxDev

@ZacxDev ZacxDev commented Nov 11, 2026 •

Copy link
Copy Markdown
Owner

What

Long agent replies are now clamped from the tail. The bubble shows its last ~15 lines, the fade is at the top, and Show more sits at the top of the bubble. Opening it reveals the earlier text above, and the end stays where it was on screen: the log's scroll follows the bubble's growth on every animation frame. Show less is also at the top. The user's own messages and kickoffs still clamp head-first, as before. Relayed plugin mail is still a relay row, not a bubble. Both directions now show about 15 lines (was 12).

The rules that already held still hold:

  • a tap on the bubble only ever opens it;
  • controls inside the bubble keep their own taps;
  • an opened message stays open across a redraw, keyed by data-msg-id;
  • the log sticks to the bottom when it is at its end;
  • a streaming reply is never clamped.

How

  • The server sets the direction from the role, never from the text. chatBubbleOf adds data-clamp-from="tail" for assistant (clampFrom). Every surface draws through it: the agent chat page, the chief panel, and the mirrored Claude Code and opencode views (opencode becomes the same events). The agent chat's live bubble() follows the same rule in JS.
  • bubbleClampScript changes:
    • It pins a closed tail bubble's own box to its end, in the same task that clamps it, so the first paint already shows the end.
    • It puts the control before the bubble, marked data-clamp-toggle="tail".
    • It keeps the end in place while the bubble opens.
    • It re-pins the bubble when a code block (<details>) inside it is toggled.
  • The CSS fade is mirrored (to top) for [data-clamp-from="tail"][data-clamp="closed"].
  • The chief panel body now includes the clamp script, ahead of agentChatScript. Its frame, which every page loads, still does not.
  • Chat logs are bottom-aligned (round 1, operator decision): a transcript shorter than its log sits at its bottom, next to the composer, on the mirrored view, the agent chat page and the chief panel. A zero-height ::before flex item takes the free space, so a full log is laid out exactly as before. Because of this, a reply in a short transcript also opens with its end in place.
  • Trade-off of bottom alignment, worth a look: in a short transcript, a new message now pushes the earlier bubbles up, as a messaging app does. CLS counts this: 0.018 at 390px in bfcache-restore "the Rendered view restored at 390px does not shift". That case guards the restore, so its fixture now fills the log first (with a check that the log scrolls). The bar is unchanged.
  • A code block in the middle of a closed reply opens below its summary, and the summary stays where it was. Only the last block re-pins the bubble to its end, and never past its own summary.
  • Docs: docs/mirroring.md now describes the clamp.

Tests

  • Go: TestAnAgentReplyIsClampedFromItsTailOnEverySurface checks the rendered HTML of the agent chat page, the chief panel body, and the mirrored Claude Code and opencode views. Replies are tail-first; user messages and kickoffs are not; relay rows are not bubbles. It also checks the live bubble(). TestTheChiefPanelBodyCarriesTheBubbleClamp replaces the old "no clamp" test. TestTheChatLogsGapMatchesTheBottomAlignRule pins the bottom-align rule's coupling to the logs' gap-3.

  • e2e tail-clamp.spec.ts (new, leg 2) runs at 390×844 with touch and at 1280×800, both set in the spec. On the mirrored view, the agent chat page and the chief panel:

    • the reply shows its last line and not its first;
    • Show more is above the bubble;
    • after opening, the last line is at the same screen y (within 2px), and the first line is reachable by scrolling up;
    • Show less, at the top, closes it again;
    • a long user message still shows its first line.

    Other cases in the same spec:

    • a reply in the middle of the log, where stick-to-bottom cannot help;
    • a transcript shorter than the log: it sits at the bottom, and the reply's end stays within 2px when it opens (agent chat page), plus bottom alignment on the mirrored view;
    • a code block in the middle of a closed reply: its summary stays on screen and in place after opening;
    • stay-open across a #chat-log refresh;
    • a 40-reply tail-clamped transcript that opens at its end with CLS < 0.01.
  • mirror-chat.spec.ts is updated for 15 lines, for the tail control that now sits next to a short user bubble, and for the code-block fixture (the block moved to the end of the reply).

Red/green

Round 1 (at f10623ee): the new short-transcript, bottom-alignment and mid-reply code-block e2e cases (6, both widths) are red at 431b4f8c and green at the head; so is TestTheChatLogsGapMatchesTheBottomAlignRule. TestTheClampScriptReadsTheServersDirection was removed: it only checked for strings.

Round 0:

At origin/main (2b469c0), with the new tests copied in:

  • all 3 new or changed Go tests fail;
  • all 14 new e2e cases fail;
  • mirror-chat "a long bubble is clamped…" fails (15-line bound).

At the head, all of them pass. The related specs were also run locally and are green: mirror-chat, agent-chat-markdown, chief-fold-in, chief-thread-search and attention-delivery.

Mutations

Each mutant was applied at the head, the tests were run, and the file was restored (checked by sha256).

Mutant Killed by
Direction attribute ignored (everything head-first) e2e "the reply shows its LAST line" (all surfaces, both widths)
Scroll compensation removed e2e "the last line stayed put" (8), "the last line is still on screen" (2)
Compensation replaced by stick-to-bottom only e2e "the last line stayed put"; mid-log case: 405 → 1069 px
Button placed at the bottom (created and found there) e2e "Show more" control-before-the-bubble assertion (data-clamp-toggle="tail" on the preceding sibling), on all surfaces
Re-measure re-pins a bubble the reader moved off its end (round 1) e2e "the tapped summary is still on screen" (both widths)
Chief panel without the script e2e chief panel case (both widths); Go TestTheChiefPanelBodyCarriesTheBubbleClamp

🤖 Generated with Claude Code

A long assistant bubble now shows its LAST ~15 lines with the fade and
"Show more" at its top; opening it grows the earlier text upward while the
log's scroll follows the growth, so the end stays where it was on screen.
The reader's own messages and kickoffs stay head-first; relayed plugin mail
is a relay row (no bubble) as before. Both directions show about 15 lines
(was 12).

- chatBubbleOf declares the direction from the role (clampFrom:
  data-clamp-from="tail" for "assistant"), so the agent chat page, the
  chief panel and the mirrored Claude Code and opencode views all agree;
  the agent chat's live bubble() spells the same rule. Nothing reads the
  text to decide.
- bubbleClampScript pins a closed tail bubble's own box to its end, puts
  its control before it (data-clamp-toggle="tail"), anchors the end while
  opening, and re-pins when a code block inside it is toggled.
- The chief panel body now includes the clamp script (ahead of
  agentChatScript); its frame still does not.
- Boundary, documented: a transcript that does not fill its log has
  nothing to scroll, so there the end moves down into the empty space
  (and stays on screen) before the log can follow.

Tests: Go unit tests on every surface's rendered HTML; a new
tail-clamp.spec.ts at 390x844 (touch) and 1280x800 covering the mirrored
view, the agent chat page and the chief panel, stay-open across a refresh,
the mid-log case, the fits-on-screen boundary and a layout-shift check;
mirror-chat.spec.ts updated for 15 lines and the tail direction.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ZacxDev

ZacxDev commented Nov 11, 2026

Copy link
Copy Markdown
Owner Author

Round 0 (requirements & deletion) — round 0 · requirements: 11 (unattributed: 1) · deletion candidates: 0 · verdict: proceed to the checklist.

none — examined: bubble_clamp.go (tail/pin/toggleOf/bubbleOf/animate anchorEnd/toggle re-pin), agents_detail.go (chatBubbleOf + clampFrom, live bubble(), relay-row branch), chief_panel.go (clamp script added, old no-clamp invariant removed per the operator's decision), input.css mask, all 3 chatBubbleOf callers, tail-clamp.spec.ts (7 cases x 2 viewports) vs mirror-chat.spec.ts stay-open (user bubble, distinct path), the 2 new Go tests. Every behaviour answers an operator decision or is a direct consequence of one; nothing to delete.

@ZacxDev

ZacxDev commented Nov 11, 2026

Copy link
Copy Markdown
Owner Author

Round 1 (nine axes) at 431b4f8: 0 🔴, 2 🟡, 2 🟢 — the ladder ends here. Three mutations re-run independently (clampFrom never tail; chief panel without the script; script after agentChatScript): all killed; a fourth (tail() returns false, string kept) survived the Go tests (finding 4). Operator decision on finding 1: bottom-align short chats. All four fixed in a final commit (not re-audited).

1. a short transcript moves the reply's end down on Show more (264px measured); a bottom-aligned log keeps it fixed (0px), so the documented limit is a choice — bubble_clamp.go:20-22
2. opening a code block in the middle of a closed reply re-scrolls the bubble to its end and hides the summary just tapped (548->120, above the bubble's top at 364) — bubble_clamp.go:263-269
3. the comment that Show less keeps a reply's top in place is false near the log's end (+124px at bottom, +300px when the scroll range shrinks) — bubble_clamp.go:33-34
4. TestTheClampScriptReadsTheServersDirection only checks a string; a tail() that returns false but keeps that string passes every Go test — bubble_clamp_tail_test.go:144

ZacxDev and others added 2 commits November 10, 2026 21:58
…comment and test fixes

- Chat logs are bottom-aligned (operator decision): a transcript shorter
  than its log sits at its bottom, by the composer, on #mv-log and on
  #chat-log (agent chat page and chief panel). A zero-height ::before flex
  item takes the free space (margin-bottom:auto) and cancels the log's gap
  with its margin-top, so a full log is laid out exactly as before and the
  rule does not depend on what the first child is. With it a tail reply in a
  short transcript opens with its end in place too: the documented
  "short log" limit is gone, and its test is replaced by one that pins the
  end within 2px there (and the bottom alignment on both surfaces).
  TestTheChatLogsGapMatchesTheBottomAlignRule pins the gap coupling.
- A code block opened in the MIDDLE of a closed tail reply no longer
  re-scrolls the bubble to its end: a tap on its summary marks the bubble
  off its end before it opens (the re-measure its new width triggers can
  run before 'toggle'), and a re-measure leaves a marked bubble's scroll
  alone. Only the LAST block re-pins, never past its own summary. New e2e:
  the tapped summary stays on screen and in place.
- The Show less comment now says what happens: the control stays in place
  away from the log's end, and within stick-to-bottom's reach the end stays
  and the control moves down by what the bubble shrank.
- TestTheClampScriptReadsTheServersDirection removed: it only spelled
  strings; the e2e cases are the guard for the direction.
- mirror-chat's queued-message case now uses a transcript that really
  scrolls (its scrollTop = 200 was a no-op, which passed only because short
  logs were top-aligned).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s its box

With chat logs bottom-aligned, a reply arriving in a log SHORTER than its
box moves every earlier bubble up by its height (a messaging app's
behaviour), which the Layout Instability API counts: CI measured 0.018 at
390px on this case's two-message fixture (e2e leg 2 on f10623e). The case
guards the RESTORE, so it now appends a dozen short pairs first (control:
the log scrolls); in a full log stuck to its end the reply that came
meanwhile scrolls in, which is no shift. Green at 390px and 1280px locally.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ZacxDev
ZacxDev marked this pull request as ready for review November 11, 2026 05:04
@ZacxDev
ZacxDev merged commit 5e3d74d into main Nov 11, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant