terminal: tail pill above floating chrome + autoscroll fix (#375)

Two bugs on the agent terminal page after the #362 overhaul:

## 1. `↓ N new` pill clipped by the composer

The fixed-overlay composer (z-index 30, agent.css) sits in the
root stacking context. The pill — `position: absolute` inside
`.live.terminal` with no z-index — defaults to its document-order
position in the body's stacking order, which the composer covers.

Fix: bump `.agent-main .tail-pill { z-index: 35 }` so the pill
participates in the root stacking context above the composer.
Scoped to the agent-page overlay layout — the shared `.tail-pill`
rule stays untouched (the dashboard's in-page layout doesn't need
the bump).

## 2. Autoscroll-on-new-message not firing when the operator was
   already at the bottom

`afterAppend()` in terminal.js was calling `isNearBottom()` AFTER
appending the new row. The new row's own height is already in
`scrollHeight` at that point, so for any row taller than the
NEAR_BOTTOM_PX threshold (48px — easily passed by a multi-line
message body, a tool-result summary, a markdown block), the check
returns false and the pill shows + scroll stays put.

Fix: capture `wasNearBottom = isNearBottom()` BEFORE the
`log.appendChild(...)` in each of `row` / `details` /
`detailsDiff`, pass it into `afterAppend(wasNearBottom)`. Now the
auto-scroll triggers whenever the operator was visually at the
bottom an instant before the row landed, regardless of the new
row's height.

Same shared `@hive/shared/terminal.js` is used by the dashboard
+ per-agent UI + the upcoming /flow.html page, so both pages
inherit the fix.

## Validation

`npm run build` clean.

Bundle deltas: shared terminal bundle re-inlined into both consumers
unchanged in size (the wasNearBottom variable is a single bool, no
measurable delta). Agent CSS +0.1kb (z-index property).

Browser smoke test isn't possible from inside iris's container —
worth eyeballing post-deploy:
  - With the operator scrolled to bottom, a tall message lands and
    the view scrolls to keep it visible (instead of pinning the
    pill).
  - The pill appears above the composer when the operator is
    scrolled up and new messages land.

Closes #375.
This commit is contained in:
iris 2026-05-24 13:14:01 +02:00 committed by Mara
parent 14b79f43cf
commit 5615da9211
2 changed files with 20 additions and 6 deletions

View file

@ -88,8 +88,16 @@ export function create(opts) {
log.addEventListener('scroll', () => {
if (isNearBottom()) { unseen = 0; updatePill(); }
});
function afterAppend() {
if (currentNoAnim || isNearBottom()) {
// Auto-scroll decision uses the PRE-append scroll position
// (issue #375). Checking after the append underestimates
// "nearness" because the new row's own height has already pushed
// `scrollHeight - scrollTop - clientHeight` past the threshold,
// even when the user was visually at the bottom an instant ago.
// Each row/details/detailsDiff captures `nearBottomBeforeAppend`
// and hands it to afterAppend so the auto-scroll triggers
// whenever the operator was at the bottom when the row landed.
function afterAppend(wasNearBottom) {
if (currentNoAnim || wasNearBottom) {
log.scrollTop = log.scrollHeight;
} else {
unseen += 1;
@ -112,15 +120,17 @@ export function create(opts) {
}
function row(cls, text) {
clearPlaceholder();
const wasNearBottom = isNearBottom();
const e = document.createElement('div');
e.className = 'row ' + (cls || '') + (currentNoAnim ? ' no-anim' : '');
e.appendChild(linkify(text));
log.appendChild(e);
afterAppend();
afterAppend(wasNearBottom);
return e;
}
function details(cls, summary, body) {
clearPlaceholder();
const wasNearBottom = isNearBottom();
const d = document.createElement('details');
d.className = 'row ' + (cls || '') + (currentNoAnim ? ' no-anim' : '');
const s = document.createElement('summary');
@ -131,11 +141,12 @@ export function create(opts) {
pre.appendChild(linkify(body));
d.appendChild(pre);
log.appendChild(d);
afterAppend();
afterAppend(wasNearBottom);
return d;
}
function detailsDiff(cls, summary, body) {
clearPlaceholder();
const wasNearBottom = isNearBottom();
const d = document.createElement('details');
d.className = 'row ' + (cls || '') + (currentNoAnim ? ' no-anim' : '');
const s = document.createElement('summary');
@ -153,7 +164,7 @@ export function create(opts) {
}
d.appendChild(pre);
log.appendChild(d);
afterAppend();
afterAppend(wasNearBottom);
return d;
}