fix(missions-ui): the results area could not scroll at all

The canvas host is a position:relative BLOCK, so flex:1 on MissionCanvas's root
was inert and its height collapsed to its content. That starved the scroller
beneath it — scrollHeight === clientHeight — so it never scrolled, and the
overflow spilled past the page and was clipped by the host's overflow:hidden.
Long results were rendered and then thrown away. Every sibling canvas already
used position:absolute; inset:0; missions was the only one that did not.

Measured after, on a brief 5x the viewport: one scroller, clientH 736 vs
scrollH 3244, scrolling 0 -> 2508 (exactly scrollH - clientH, i.e. the true
bottom), zero page overflow, tab strip pinned throughout.

Also removed five nested scrollers (70vh on live events; maxHeight caps on run
streams, phase summaries, artifact bodies and error traces). Those existed only
to work around the missing height and would have become portholes onto the very
content the operator is trying to read. The xterm pane keeps its bounded box —
FitAddon needs one, and a terminal owning its scrollback is correct.

Deleting the header's description peek reclaims 104px for results (header
256 -> 152px); the same text renders in full in Setup -> Overview, as the code's
own comment noted.

Streaming now follows only when already at the bottom, via a shared
useStickToBottom hook replacing two byte-identical copies, plus a "jump to
latest" pill neither had. Defaults collapse by mission state, and a remount key
fixes scrollTop leaking between tabs — a bug that only appears once scrolling
works.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-13 10:47:00 -07:00
co-authored by Claude Opus 5
parent 4dec77ae6d
commit 5db695460f
9 changed files with 236 additions and 96 deletions
@@ -13,7 +13,16 @@ const mono =
"ui-monospace, SFMono-Regular, SF Mono, Menlo, Monaco, Consolas, monospace";
export function PhaseRunsList({ runs }: { runs: MissionRunSummary[] }) {
const [expanded, setExpanded] = useState<Set<string>>(new Set());
// Open exactly ONE run by default, and only when it is worth looking at:
// still running (there is live output) or failed (it needs attention).
// Everything else stays closed — N stacked open runs was a large part of
// "too many open sections".
const [expanded, setExpanded] = useState<Set<string>>(() => {
const newest = runs[runs.length - 1];
return newest && (newest.status === "running" || newest.status === "failed")
? new Set([newest.id])
: new Set<string>();
});
if (runs.length === 0) return null;
return (
<div
@@ -124,10 +133,11 @@ export function PhaseRunsList({ runs }: { runs: MissionRunSummary[] }) {
background: "rgba(0,0,0,.35)",
color: "#e0d0cf",
fontSize: 10.5,
// pre-wrap + break-word already handle long lines; a
// maxHeight would truncate the stack trace the operator
// opened this <details> specifically to read.
whiteSpace: "pre-wrap",
wordBreak: "break-word",
maxHeight: 260,
overflow: "auto",
}}
>
{r.error}