Add viewport planning and census tracking for terminal run display - #866
Merged
Conversation
The run footer reported on the graph's top level, which for anything that owns a workflow, regenerates a subgraph or maps over a worklist is the least interesting part of the run: "1 / 3 tasks" while several hundred rows were in flight. `ui/model/runCensus` walks the tree the rows actually draw — owned subgraphs, live Map clones — into a ledger that only ever grows, so a retiring iteration cannot walk the total backwards. A node still PENDING while its own children run is an ownership wrapper, not a task that can never land: it is counted as scaffolding, and `ownershipWrapperStatus` stops it drawing a hollow circle beside a subtree that has plainly finished. The footer gains a wall-clock, hard right on its own so a number that changes every second stops shoving the counts and the spend sideways, and loses the duration that used to ride inside the token line. The live region no longer tracks its content's height in both directions. It grows as rows arrive and then holds, because a block that shrinks drags the footer up the screen mid-run. `ui/model/runViewport` prices the tree against the window and shrinks the deepest list first, so a Map's own row survives to explain the detail beneath it; what still overflows is tail-pinned behind a one-column gutter, which costs no rows at exactly the moment rows ran out. Ink's window size drives the budget, so a resize re-prices on the next frame. The web console takes the same footer model, over the rows it already tracks at every depth. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015vyFdNcj3fSn3tmWpLgDLa
…indow The run's own progress row spaced its three children evenly instead of growing the label, so on a wide terminal its bar sat in the middle of the window rather than at the head of the column every task bar below it lines up in. Both aggregate rows — the run's and a subgraph's — now share one `AggregateProgressRow` laid out in the same three columns `TaskStatusProgressRow` uses, so they cannot drift apart again. That bar reports on the whole run, so it is drawn from a second grey: `strong`, mixed halfway between the terminal's own foreground and background against `medium`'s quarter. Stated as a distance from the page, it comes out brighter on a dark terminal and darker on a light one with neither case special-cased, and `cliPaletteFromRgb` is now where both are derived and tested. Narrowing the window stranded a copy of the frame above the run, once per resize. A live region is erased by walking the cursor up as many lines as were written, and the terminal has already reflowed each of those lines into two rows by the time the walk runs, so it covers half the frame. `useRepaintOnResize` repaints from an erased screen on a width change, and leaves scrollback — the operator's record of the run — alone. Height changes reflow nothing and are left as they were. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015vyFdNcj3fSn3tmWpLgDLa
A task that reports no progress of its own drew a determinate bar at zero for the whole of its run, above a subtree visibly moving. It was not propagation: `Task.progress` initialises to `0` and `TaskRunner` re-stamps `0` the moment a task starts, and the terminal's poll of the live instance took that stamp as a reading. Neither stamp is announced and neither is a measurement — the graph needs a number in the denominator of its average and zero is the honest one to put there, which is why the field cannot simply become `undefined`. `adoptPolledProgress` draws the line at the row instead: a polled zero counts only once the task has actually reported something, and until then the row draws no bar and the spinner carries "working, extent unknown", which is the claim that column already exists to make. A status event into PROCESSING clears the row's figure, so a reused instance does not carry the last job's percentage into the next one. `graph_progress` averages those same unreported zeroes, so the run's own bar inherited the lie and sat at a measured-looking 0% until something completed. `runAggregateProgress` holds it indeterminate until a task has reported or landed. The web console takes the same rule; its rows are event-sourced and never saw the stamp, but its Workflow line did. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015vyFdNcj3fSn3tmWpLgDLa
…file Crediting an evicted path turned on it having been seen *running*. The walk is a 250ms poll and a short task inside a Map clone is routinely caught once as PENDING and then never again, so one key per such iteration was never credited and `done` stopped short of `total` by however many the poll happened to miss. It now turns on *not settled*. `countHidden` and the shrink loop's victim search walked every list in the tree, including those hanging off nodes the plan had already capped away. Those lists are not on screen: their siblings are not rows anyone is missing, and shrinking one frees nothing. The footer reported 78 hidden for a plan whose drawn rows hid 42, and the search spent its step budget grinding a list nobody draws from six rows down to one. Both now walk the lists the plan actually draws. The census ledger outlived its subject. A second run in one process started with the first run's paths in it, credited them COMPLETED as they failed to reappear, and seeded the footer with a total the new run could never meet. `probe2.tsx` was a scratch script `git add -A` swept into the previous commit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015vyFdNcj3fSn3tmWpLgDLa
…ng one Ink decides whether to draw frames as it goes from `!isInCi && stdout.isTTY`, so under a CI runner it resolved to non-interactive: erase sequences and resize handling off, and only the final frame written, at unmount. Every assertion in the viewport tests reads a frame mid-run, so they found an empty screen on CI and passed on a developer's machine — green locally, red on the PR, five failures pointing at product code that was fine. The behaviour under test is what a terminal shows, so the tests now say so with `interactive: true` instead of leaving it to the environment. Reproduced the failure with `CI=true` first and confirmed the whole suite passes both ways after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015vyFdNcj3fSn3tmWpLgDLa
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements a viewport planning system and task census tracking to intelligently fit run task trees into terminal windows. The run footer now reports accurate task counts across the entire tree (including owned subgraphs and Map/Reduce iterations), and the display adapts dynamically to terminal size constraints by selectively hiding the deepest tree levels when space is limited.
Key Changes
Census tracking (
useRunCensus.ts,runCensus.ts,runCensus.test.ts): Walks the live task tree to count all tasks (not just top-level), maintains a ledger of every task ever seen (so totals don't fall as work completes), and distinguishes between countable tasks and structural grouping nodes (Map iterations).Viewport planning (
runViewport.ts,runViewport.test.ts): Computes how many siblings each list can show to fit the terminal budget. Uses a depth-first shrinking strategy—innermost lists lose rows first—so ancestor context is preserved when space runs out. Lists never shrink below a minimum, and the live region itself never shrinks on its own (only when the terminal resizes).Scroll region (
ScrollRegion.tsx): A live region that grows to fit content, holds its height as rows leave (preventing footer movement), and shows a one-column gutter when overflowing. Measures content periodically and respects a budget from the caller.Run status bar updates (
RunStatusBar.tsx,runRowModel.ts): Now displays accurate task counts from the census, reports hidden row count when viewport planning truncates the tree, and shows elapsed time. AddsrunAggregateProgress()to handle indeterminate progress bars correctly (zero is only a measurement once something has been measured or a task has settled).Integration (
WorkflowRunApp.tsx,TaskRunApp.tsx): Both now use the census and viewport planning. The run computes a plan once per frame and publishes it via context so all lists read their own cap. Window resize triggers a full repaint from a clean screen.Supporting utilities:
useRunClock.ts(wall-clock for the run),useRepaintOnResize.ts(screen clear on resize),RunViewport.tsx(context provider for the plan),AggregateProgressRow.tsx(progress bar for non-task aggregates).Test coverage: Comprehensive tests for viewport planning (fitting trees to budgets, depth-first shrinking), census ledger (counting owned subgraphs, handling Map iterations), and scroll region behavior (sticky height, gutter rendering, window resize).
Notable Implementation Details
MAX_LEDGER_KEYS = 20,000) to prevent unbounded memory growth on massive Maps.https://claude.ai/code/session_015vyFdNcj3fSn3tmWpLgDLa