Files
routstrd/docs/plans/tui-history-charts.md
redshift b6caa2afb3 feat(tui): replace Today tab with window-driven History charts
Rename the second usage-monitor tab from Today to History and draw usage
over time as a bar chart instead of the text lists and horizontal hourly
bars.

- daemon: add a zero-filled `timeline` series to the windowed summary,
  bucketed per window (hours for 24h, days for 7d/30d, months for all).
  Months are folded from daily aggregates since the SDK has no month
  grouping.
- tui: renderHistory draws a vertical spend chart with a y-axis, x labels
  and an independently scaled token sparkline plus legend; the [W]
  selector now drives the tab (it was hidden on it before).
- renderBox truncates overlong lines so a large number can't push the
  right border out.
- docs: SKILL.md wording + docs/plans/tui-history-charts.md.

Tests: daemon timeline buckets; chart axis/labels/sparkline/legend,
empty state, and full-width alignment.
2026-10-13 14:02:00 +08:00

2.8 KiB

TUI "History" tab with adaptive bar charts

Status: implemented. Branch feat/tui-history-charts (worktree .worktrees/tui-history-charts, based on main @ 5069b2d).

Goal

Replace the old Today tab (a totals box + a text "Recent Days" list + a horizontal hourly bar list) with a History tab that draws usage over time as bar charts, with the bucketing driven by the existing [W] time-window selector.

[W] window Bucket Bars
24h hour 24
7d day 7
30d day 30
All month one per calendar month since first usage

Y-axis is total spend; a token sparkline shares the x-axis (scaled independently to peak tokens, with both totals in the legend). This also makes the tab window-aware: the selector was previously hidden on it.

Backend — src/daemon/http/usage-summary.ts

  • New TimelineBucket (hour | day | month), TimelinePoint and Timeline types; WindowedStats gains a required timeline: Timeline.
  • buildTimeline(driver, baseFilter, window, now, tzOffsetMinutes):
    • today (24h) → aggregate({ groupBy: "hour" }) over the last 24 local-aligned hours. The SDK's hour key is HH with no date, so the window is aligned to whole hours (each hour-of-day appears at most once).
    • 7d / 30d → aggregate({ groupBy: "day" }) over the last N days.
    • all → aggregate({ groupBy: "day" }) with no after, folded into YYYY-MM months. The SDK has no month grouping, hence the fold.
    • Every series is zero-filled so the x-axis is evenly spaced.
  • buildTimeline is called once per summary build (after buildAggregates) and attached to both the all-time and windowed blocks. The zero-data short-circuit returns an empty timeline.

TUI

  • types.ts — TabId "today" → "history"; re-export the timeline types.
  • constants.ts — tab renamed to History; tabSupportsWindow now excludes only Overview and Recent.
  • render.ts — renderToday replaced by renderHistory + renderTimelineChart (vertical bars, y-axis gutter, x labels, token sparkline, legend). Long series are merged to fit narrow terminals.
  • data.ts — the old-daemon fallback windowFromAllTime supplies an empty timeline.

Tests

  • src/daemon/http/usage-summary.test.ts — hourly / 7-day / 30-day / month-folded timelines, including zero-fill and window totals.
  • src/tui/usage/render.test.ts — axis, labels, sparkline, legend, empty state, and full-width alignment of every rendered line.

Notes / limits

  • Daemons too old to return timeline still render: the History tab falls back to an empty chart rather than erroring.
  • The 24h window is hour-aligned rather than a rolling now - 24h, so each hour-of-day appears once and the HH keys stay unambiguous.