Skip to content

Stabilize TUI refresh, scrolling, responsive layout, and dashboard data density - #863

Open
ihearttokyo wants to merge 3 commits into
getagentseal:mainfrom
ihearttokyo:codex/fix-tui-refresh-layout
Open

Stabilize TUI refresh, scrolling, responsive layout, and dashboard data density#863
ihearttokyo wants to merge 3 commits into
getagentseal:mainfrom
ihearttokyo:codex/fix-tui-refresh-layout

Conversation

@ihearttokyo

@ihearttokyo ihearttokyo commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Keep the active view and vertical position stable during background work, eliminate refresh blanking, and enforce a one-minute minimum automatic refresh interval.
  • Make the complete dashboard scrollable and render its panels in a stable one-, two-, or three-column order, with immediate breakpoint reflow and a safe width cap.
  • Preserve every metric and its heading, shorten project paths in meaningful stages, and size Daily Activity to the tallest relevant sibling panel without weakening navigation or day mode.

Why the dashboard failed

Three independent behaviors combined into the visible failures. A background result could replace state after the user had entered Optimize. Ink could paint once with the previous terminal width before React received a resize. Because the alternate screen removed terminal scrollback, content taller than the viewport became unreachable. At narrow widths, the shared row renderer also gave labels enough space to displace metric headings or values.

The repair keeps view, width, viewport, and row-density decisions inside the existing dashboard state and rendering path. It adds no dependency or parallel layout system.

User-visible behavior

Stable refresh and navigation

  • Optimize remains mounted when dashboard data refreshes in the background.
  • Automatic data refresh runs no more than once per minute; --refresh 0 remains fully static.
  • Background work keeps the current frame visible instead of showing a loading or blank frame.
  • Refresh and resize rerenders preserve the application scroll offset.
  • Up and down move one application row, Page Up and Page Down move one viewport, and Home and End jump to the bounds.
  • Deliberate navigation to another view, period, provider, or day begins at the top.

Responsive dashboard

  • The eight panels retain source order in every layout: one column through 89 terminal columns, two columns from 90 through 134, and three columns from 135 upward.
  • Three-column rows follow the standard 3/3/2 arrangement and grow symmetrically by one panel character for every three additional terminal characters.
  • The dashboard stops growing at the lesser of 256 terminal columns or the width the current data can usefully render.
  • Resize state is captured before Ink's next paint, so 89/90 and 134/135 transitions do not show a stale intermediate arrangement.
  • Terminals wider than 256 columns retain a populated dashboard rather than clearing the frame.
  • Colored bars remain at the left edge of every data section; Daily Activity places its bar before the date.

Complete, compact data rows

  • Metric widths come from their headings and rendered values. Adjacent metric cells use exactly one column of separation.
  • Tok/s and every other metric column always render. Unavailable values display - instead of removing a column.
  • Costs, including the estimated-cost ~ marker, render in full whenever the panel can hold them.
  • The project heading spells out session.
  • Project labels yield space before any heading or metric does. Shortening removes the folder prefix first, then the year in a date folder, and only then truncates the project title with a macOS-style ellipsis.

Adaptive Daily Activity history

  • One-column layout displays 10 dates.
  • Two-column layout displays MAX(10, visible By Project rows).
  • Three-column layout displays MAX(10, visible By Project rows, visible By Activity rows).
  • Day mode remains one date, and available history remains the upper bound.
  • The same calculated page size controls rendering, j/k, Space paging, g/G, final-page clamping, and the Showing X-Y of Z status.
  • By Activity row counting and rendering share the same aggregation, preventing the calculated Daily Activity height from drifting away from the panel it matches.

TDDRGR and bug-fix rounds

The adaptive-row regression first failed for the intended behavioral reason: a two-column lifetime view with 14 visible projects rendered 10 dates instead of 14. The smallest production change introduced one shared page-size calculation. After the test passed, existing project-row limits and Activity aggregation were reused rather than duplicated, and the focused tests remained green.

Two post-implementation bug-fix rounds then exercised independent real user paths. After each round, the relevant 70-test regression matrix and live Ghostty path were rerun:

  1. Two-column paging showed 1-14, Space advanced to 15-28, and g returned to 1-14. Accessibility bounds confirmed the entire app frame when a native Ghostty layer capture omitted window chrome; the misleading partial captures were discarded.
  2. Live resizing produced 10 rows in one column, 14 in two columns, and 18 in three columns. The 18-row result matched the rendered By Activity data. No new defect was found in either round.

Correctness review was clean. Ponytail review found the implementation already lean and did not recommend another abstraction.

Validation

  • Real-data Ghostty validation across one-, two-, and three-column layouts, breakpoint transitions, paging, scrolling, refresh preservation, and widths above 256 columns.
  • Twenty window-bounded Ghostty views covering 73 through 283 terminal columns, followed by dedicated adaptive-row and post-fix captures.
  • Focused Daily Activity tests: 9/9.
  • Complete dashboard suite: 48/48.
  • Relevant layout, model, and overview regression matrix: 70/70.
  • TypeScript compilation, CLI production build, and browser dashboard build. The existing Vite warning for a JavaScript chunk above 500 KB remains unchanged.
  • git diff --check.
  • Installed CLI version/help smoke checks. The installed dist/main.js, dist/cli.js, and dashboard HTML hashes match the repository build.
  • GitHub checks exercised by the pull request: Semgrep, co-author guard, Firstlook, and Windows package build.

In the full repository run, 2,507 tests passed, 2 failed, and 5 were skipped, with 26 missing-jsdom environment errors. Both failures are pre-existing Copilot durable-cache assertions in tests/parser.test.ts; they do not exercise this dashboard work. A durable-total assertion that failed in an earlier run passed in the final run.

Native Shift-Space could not be distinguished from Space in synthesized terminal input because both arrive as the same byte. Reverse page-cursor behavior remains covered deterministically, and g was validated in Ghostty as the reliable first-page return path.

Reviewer focus

The highest-value review is the interaction among the shared row renderer, the calculated Daily Activity page size, and the existing scroll state. The acceptance criteria are that no view or scroll position changes because of background refresh, no metric disappears at supported widths, each resize immediately preserves panel order, and Daily Activity navigation uses the same page size shown on screen.

Keep background refreshes from blanking or replacing the active Optimize view, and enforce a one-minute minimum refresh interval.\n\nRework the dashboard into a stable 3/2/1-column flow with left-aligned bars, justified metric columns, readable project headings, and a ten-row Daily Activity viewport.\n\nSynchronize resize state before Ink paints so breakpoint transitions do not leave stale frames, preserve content beyond 256 terminal columns, and cap the dashboard at the current data's renderable width. Add focused regression coverage and a submission statement documenting live Ghostty validation.
@ihearttokyo

ihearttokyo commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Submission note — rebuilt August 1

Landed status: PR #863 now contains the complete dashboard work through 91b5f91, including refresh and viewport stability, responsive metric layout, staged project-path shortening, and adaptive Daily Activity history.

What this work fixes

CodeBurn's dashboard had four connected failure modes. A background result could replace state after the user entered Optimize; Ink could paint once at the previous terminal width; the alternate screen removed terminal scrollback while the app remained taller than its viewport; and narrow rows let labels displace metric headings or values.

The repair keeps those decisions in the existing dashboard state and rendering path. It adds no dependency or second layout system.

Behavior contract

  • Background refreshes keep the active view, frame, and vertical position. Enabled refreshes run no more than once per minute, while --refresh 0 remains static.
  • The complete application scrolls by line, page, or boundary. Refresh and resize rerenders retain position; deliberate navigation starts at the top.
  • Panels preserve source order through one column at 89 characters or fewer, two columns from 90 through 134, and three columns from 135 upward. Three-column rows use the 3/3/2 arrangement and grow symmetrically.
  • Dashboard width stops at the lesser of 256 terminal characters or the current data's renderable width. Wider windows retain a populated frame, and breakpoint changes reflow before the next Ink paint.
  • Colored bars stay on the left. Metric cells use their full heading and value widths with exactly one separating column; Tok/s and other metrics show - rather than disappearing.
  • Project labels yield space before metrics. Shortening removes the parent folder, then the date year, and only then truncates the project title with an ellipsis.
  • Daily Activity shows 10 rows in one column, MAX(10, By Project rows) in two columns, and MAX(10, By Project rows, By Activity rows) in three columns. The same size governs rendering, cursor movement, paging, bounds, and status text.

Verification

  • Focused Daily Activity tests: 9/9.
  • Complete dashboard suite: 48/48.
  • Relevant layout, model, and overview regression matrix: 70/70.
  • TypeScript, CLI build, browser dashboard build, and patch hygiene: passed.
  • Real-data Ghostty validation: 20 window-bounded views spanning 73–283 columns, plus dedicated adaptive-row and post-fix rounds.
  • Current pushed PR checks: Semgrep, co-author guard, Firstlook, and Windows package build are green.

The full repository run produced 2,507 passing, 2 failing, and 5 skipped tests, plus 26 missing-jsdom environment errors. Both failures are pre-existing Copilot durable-cache assertions in tests/parser.test.ts and do not exercise the dashboard. The earlier durable-total failure passed in the final run.

Reviewer focus

The highest-value review is the interaction among shared metric sizing, calculated Daily Activity page size, and existing scroll state. No refresh should change view or position, no supported width should lose a metric, each resize should preserve panel order on its first frame, and Daily Activity navigation should use the page size visible on screen.

Pin the interactive dashboard to a terminal-sized viewport and add line,
page, home, and end navigation without sacrificing the alternate-screen
resize protections. Preserve the viewport offset across background refreshes
and ordinary rerenders while resetting cleanly for a new view or period.

Give daily-history paging its own Space binding, render full model costs
whenever the panel can hold them, and spell out the project session heading.
Extend the responsive dashboard regressions across one-, two-, and
three-column viewports and update the submission evidence.
@ihearttokyo ihearttokyo changed the title Stabilize TUI refresh and responsive layout Stabilize TUI refresh, scrolling, and responsive layout Jul 31, 2026
@ihearttokyo

ihearttokyo commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up implementation and validation update — August 1

Landed: The validated follow-up is now reviewable in PR #863 at 91b5f91.

Latest implementation

  • Every data metric now remains visible. Tok/s is always present, unavailable values render as -, and adjacent metric cells use exactly one column of padding.
  • Costs render in full when space exists, session is spelled out, and labels surrender width before headings or values.
  • Long project paths shorten in deliberate stages: parent folder first, year in the date folder second, project title last.
  • Daily Activity now matches the tallest relevant sibling: 10 rows in one column, 14 in the tested two-column layout, and 18 in the tested three-column layout. Day mode remains one row.
  • Rendering, j/k, Space paging, g/G, final-page clamping, and Showing X–Y of Z all use the same calculated page size.

TDDRGR and post-implementation rounds

The new two-column regression first failed with 10 dates where 14 were required. One shared page-size calculation made it green; the refactor then reused the existing project limit and Activity aggregation so counting cannot drift from rendering.

Two dedicated bug-fix rounds followed. Each ended with the relevant 70/70 regression matrix and a real Ghostty user path:

  1. Two-column history showed 1–14, Space advanced to 15–28, and g returned to 1–14.
  2. Live resizing showed 10, 14, and 18 dates in the one-, two-, and three-column layouts; 18 matched the rendered By Activity rows.

No additional defect appeared in either round. Correctness review was clean, and Ponytail review found no smaller useful design.

Evidence and limits

  • Dashboard tests: 48/48; focused Daily Activity tests: 9/9.
  • TypeScript, CLI build, browser dashboard build, and git diff --check: passed.
  • The installed CodeBurn 0.9.19 package matches the repository build for dist/main.js, dist/cli.js, and dashboard HTML.
  • Native Ghostty inspection covered the paging path, all three responsive layouts, and the wide capped layout without invoking Computer Use.
  • Synthesized Shift-Space is indistinguishable from Space at the terminal byte level. Reverse page-cursor behavior is covered deterministically; g was validated live as the first-page return path.

This follow-up is now part of PR #863. The push retriggered GitHub checks; the local 70-test matrix and production builds were rerun immediately before commit.

Keep every dashboard metric visible with intrinsic column widths and one
cell of separation, allowing bars and project labels to yield space before
headings or values disappear. Shorten project paths in meaningful stages so
the project title remains recognizable for as long as possible.

Derive Daily Activity's page size from the sibling panels in the active
responsive row: ten dates in one column, the visible project count in two,
and the greater project or activity count in three. Reuse that calculation
for rendering, cursor bounds, paging, and status text so the viewport cannot
drift from its navigation contract.

Cover the behavior with live Ink regressions, path-shortening contracts, the
70-test dashboard/model/overview matrix, and the rebuilt submission record.
@ihearttokyo ihearttokyo changed the title Stabilize TUI refresh, scrolling, and responsive layout Stabilize TUI refresh, scrolling, responsive layout, and dashboard data density Aug 2, 2026

@ozymandiashh ozymandiashh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this. The scrolling, refresh-race and adaptive-layout work is careful, and I traced a fair bit of it by hand: the generation-counter and pending-ref coordination in reloadData, the viewRef stale-closure avoidance, the shared getDailyActivityPageSize / getProjectBreakdownRowLimit calculation that fixes the drift you describe, and the shortProject truncation staging. All of it holds up, and the test coverage for the stated behaviour is genuinely strong.

One thing should block, though, because it is invisible to CI and to this PR's own tests by construction.

The resize handler reintroduces the Windows/ConPTY hang from #195

src/dashboard.tsx:1504

process.stdout.write('\u001B[?2026h\u001B[2J\u001B[H')

That writes Begin-Synchronized-Update directly to stdout. The repo already has a guard for exactly this sequence, added by c511627 to close #195, whose commit message states that ConPTY "does not implement this protocol and buffers indefinitely, causing the dashboard to hang with no output":

// src/ink-win.ts:11
if (chunk === BSU || chunk === ESU) return true

The filter is an exact string comparison. The write above is BSU concatenated with clear-screen and cursor-home, so it is a different string, chunk === BSU is false, and the raw BSU passes straight through to the terminal on Windows. Every resize would then trigger the failure mode that guard exists to prevent.

There is also no matching End-Synchronized-Update anywhere in the diff. I grepped the branch: \u001B[?2026l appears zero times. The region is left open and only closes because Ink's next internal frame happens to emit its own correctly paired bsu/esu.

Neither the Windows package-build check nor tests/dashboard.test.ts can catch this. The tests drive PassThrough streams and assert on stripAnsied substrings, so they cannot observe ConPTY buffering or cursor desync.

Ink 7 already listens for resize on stdout and performs a layout recalc plus a full unthrottled re-render (ink.js:261-272), wrapped in its own correct bsu/esu pairing, so the manual write may simply be unnecessary. If a hard clear really is needed, gating it behind process.platform !== 'win32' would at least match the reasoning already encoded in ink-win.ts.

Smaller things, none blocking

  1. The isHeavyPeriod auto-refresh gate is gone. The previous code did if (!dayDate && isHeavyPeriod(period)) return, skipping auto-refresh on 30days/month/all/lifetime. src/dashboard.tsx:1270-1276 no longer has it, so background reparses now fire on heavy tabs. getDashboardScanRange bounds the scan when scrollableHistory is set, so this may well be fine, but it is a behaviour change the description does not mention and it would be good to hear it was deliberate.

  2. Aggregation runs inside render. dailyHistoryPageSize, dailyHistoryRowCount and modelCount are computed in the InteractiveDashboard body (src/dashboard.tsx:1132-1144) rather than in useMemo, so they recompute on every paging keystroke. Separately aggregateModelTotals(projects) runs twice per data-changing render, once inside the memoized getDashboardMaxWidth and again unmemoized in ModelBreakdown.

  3. ScrollableViewport's useLayoutEffect has no dependency array (src/dashboard.tsx:1062-1067), so measureElement runs after every render including every scroll tick, even though content height does not change while scrolling.

  4. StatusBar scrolls away. It is the last child of content, which is entirely inside ScrollableViewport (:1431-1439), so the keybinding hints disappear when you scroll down on a tall dashboard. Pinning it outside the scroll region would keep them visible.

  5. --refresh help text does not mention the new 60s floor. src/main.ts:767,1197,1215 still say "Auto-refresh interval in seconds (0 to disable)", so --refresh 10 silently becomes 60 with only the README explaining why.

  6. src/compare.tsx still caps at 2 columns (:214-215,257), so pressing c on a 135+ column terminal visibly narrows from the new 3-column layout. Pre-existing duplication rather than something this PR introduced, but the gap is wider now.

Everything else checked out on my side: tsc --noEmit is clean, and tests/dashboard.test.ts passes in full. The other failures I saw in full-suite runs (cli-emitters, cli-status-menubar, cache-refresh-lock) reproduce on unmodified main in my environment and are not attributable to this branch.

Happy to re-review once the resize write is sorted.

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.

2 participants