Skip to content

v1.5: running-CI badge, display-tier framework, and GitHub quota gauges - #9

Merged
sumitake merged 6 commits into
mainfrom
dev/claude/ci-running-v1.5
Aug 4, 2026
Merged

v1.5: running-CI badge, display-tier framework, and GitHub quota gauges#9
sumitake merged 6 commits into
mainfrom
dev/claude/ci-running-v1.5

Conversation

@sumitake

@sumitake sumitake commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Adds live CI-run visibility with a reusable display-tier framework:

  • busybar.display framework: documented priority ladder (ambient 20 / overlay 21 / alert 60 / session 90) with contracts and the probe-established firmware facts (equal-priority draws 409 — the device OpenAPI 'override' claim is wrong; occluded elements are evicted, never restored). Future integrations join a tier instead of rediscovering these.
  • Running badge (overlay tier): most-recent in_progress run, REPO #PR WORKFLOW ribbon (+N for concurrent), ETA from median of the last 5 same-workflow durations, progress track; 20s cadence while active.
  • Alternation: calendar ambient redraw tuned to 10s (measured across three on-device rounds), log-noise controls keep INFO volume flat.
  • GitHub quota gauges: GITHUB GRAPHQL / GITHUB REST frames in the overlay rotation (rate_limit endpoint is quota-exempt), headroom-themed with reset countdowns, staleness guard.
  • Unified display-shape tracking across all tiers — fixes the stale-element class at every transition seam, with per-transition tests and on-device before/after ink verification.

Tests: 174 (was 76 at branch base). Rotation measured live: badge→gql→rest at exact 20s cycle; calendar reclaims 3/4 gap windows.

🤖 Generated with Claude Code

sumitake and others added 6 commits August 3, 2026 20:21
New feature: ci_status shows an actively-running CI job, intended to
alternate with the calendar. Two empirical probes (required before
any implementation, per the task brief) found the firmware diverges
from its own OpenAPI documentation in ways that reshape the whole
design:

1. Equal priority from a different application_name is REJECTED
   (409 "low priority"), not an "override" as the docs claim. Only a
   strictly-greater priority succeeds against the currently-showing
   app. The running badge therefore uses priority 21, not the 20
   the brief specified -- otherwise every draw would 409 whenever
   the calendar (also priority 20) happens to be showing.

2. Occluded elements are EVICTED, not restored: probed both
   occluder-timeout-expiry and occluder-explicit-clear, and in
   both cases a lower-priority app's still-live element did not
   reappear -- the panel went black instead. This rules out "clean
   alternation, zero coordination" and forces the documented
   fallback: badge with a fixed ~10s timeout drawn once per poll
   cycle; the calendar only returns if its own independent 60s
   redraw happens to land in the gap.

Feature implementation on top of the corrected model:
- ci_status/github.py: fetch_running_runs (separate ETag slot from
  the existing failure/stuck poll -- different URL/query) and
  fetch_median_eta (cached per workflow_id for the process's
  lifetime; a successful fetch caches even a confirmed-empty
  history, but a network/HTTP error does not, so it retries next
  time rather than permanently locking in "no history").
- ci_status/logic.py: select_running_run (most-recently-started
  across repos, +N for others active), compute_median_duration_minutes,
  ETA text (~4m / ~1h05m reusing calendar_countdown._format_countdown
  verbatim, "soon" once floored under a minute, "3m in" with no
  history), progress-bar track repurposed as elapsed/median, and
  build_ci_payload extended with strict failure > stuck > running >
  quiet-green precedence via a new RunningInfo dataclass so the
  render/precedence logic stays pure and testable without network
  mocking.
- ci_status/main.py: run_once gains an optional running_cache param
  (mirrors state_cache's existing mutate-in-place pattern, gated on
  show_running so old callers/configs stay backward compatible);
  next_poll_seconds is a small pure function deciding
  running_poll_seconds vs poll_seconds, kept separate from run_once's
  return value so it's unit-testable without mocking time.sleep.
- Config: show_running (default true), running_poll_seconds
  (default 20) added to DEFAULTS and config.example.toml.

76 tests on main before this branch (verified by checking main out
directly), 124 after: +48 covering running-run selection, PR/branch
fallback, ETA math (median, no-history, soon threshold), payload
precedence, and the cadence switch.
New spec section leads with the two probe findings (equal-priority
rejection, occlusion eviction) since they invalidated the brief's
assumed "clean alternation" design before any code was written, plus
the feature description, palette/geometry, cadence/precedence, and
config additions. ci_status README gets a full "Running-Job Badge"
section spelling out the honest on-device-observed rhythm (badge
visible ~50% of the time, calendar not observed to recover during
a ~130s/6-cycle test window) rather than describing it as a clean
alternation. calendar_countdown README cross-references the same
caveat so the interaction is documented from both sides.
Generalize the priority/dwell mechanics the v1.5 probes uncovered into a
reusable framework (src/busybar/display.py): a strictly-increasing
priority ladder (AMBIENT=20, OVERLAY=21, ALERT=60, SESSION=90 reference)
plus the two contract helpers each tier actually needs
(ambient_timeout, overlay_gap_elapsed), so future integrations adopt
the same pattern instead of re-deriving it from scratch.

calendar_countdown now imports PRIORITY_AMBIENT/ambient_timeout from the
shared module instead of a local constant, and its default poll_seconds
moves 60 -> 10 after on-device re-measurement (60s: 0/6 dwell cycles
recovered; 15s: 2/6; 10s: 4/6 -- see the spec doc and reports for the
full tables). Also adds should_log_info() so the faster 10s cadence
doesn't multiply calendar.log's line rate for unchanged-state polls
(INFO only on summary change or a 10-minute heartbeat, DEBUG otherwise).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sion)

Rebuild the running-CI badge on top of the new busybar.display framework
(PRIORITY_OVERLAY/PRIORITY_ALERT imported, not hardcoded; local geometry
constants renamed RUNNING_* -> OVERLAY_* with back-compat aliases since
they're now shared with the quota frames too) and add a dwell-gate +
round-robin state machine in run_once: the overlay tier now rotates
through up to three frames per active run -- the running badge, then
(if show_quota) a GitHub GraphQL quota frame, then a REST quota frame --
one frame per OVERLAY_DWELL_SECONDS dwell slot, staying silent for a
full gap before redrawing so the ambient-tier calendar gets a fair shot
at reclaiming the screen.

Quota frames (integrations/ci_status/logic.py, github.py):
- RestPoller.fetch_rate_limit(): GET /rate_limit, explicitly exempt from
  GitHub's own rate limiting, fetched fresh once per running-poll cycle
  (no ETag caching attempted -- there's no quota cost to save and the
  values change continuously).
- parse_rate_limit() extracts the core/graphql buckets; a 5-minute
  staleness window in main.py (_refresh_quota) means a single failed
  fetch doesn't blank the frame but sustained failures eventually do --
  build_overlay_payload returns None for unavailable data and the
  caller skips that dwell slot entirely (no draw, no clear, no stale
  numbers).
- Frame layout: title ribbon ("GITHUB GRAPHQL"/"GITHUB REST", scrolls
  if it doesn't fit the ribbon width), a track bar for fraction used
  (clamped [1, PANEL_WIDTH], verified against a real account where
  GitHub's point-based GraphQL "used" exceeded "limit"), and two large
  numerals sharing the bottom row (percent remaining, reset-in).
  Background/track/title/numeral colors all key off remaining-quota
  headroom (high >50%, medium 20-50%, low <20%).
- OVERLAY_FRAME_SHAPE distinguishes the badge's element-id set from the
  quota frames' (same upsert-by-id firmware behavior the v1.3.1
  calendar transition-clear fix addressed); main.py clears only on an
  actual shape change, never between quota_gql <-> quota_rest.
- overlay_state mutations (frame_index, last_dwell_end, last_shape)
  only commit after client.draw() returns DRAWN, never optimistically.

show_quota=true is the new config default (config.py/config.example.toml
land with the framework commit); has no effect when show_running=false.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…vision)

Spec doc: new "Framework generalization, tuned alternation, and quota
frames" subsection under the existing v1.5 section -- the priority-ladder
table and join-recipes for future integrations, the three-round ambient
poll_seconds tuning table (60s/15s/10s, 0/2/4 of 6 dwell cycles
recovered), and the quota-frame design (rate_limit-is-free fact, frame
layout, headroom color table with exact hexes, round-robin/shape-change
mechanics, on-device verification results including the real-account
GraphQL used>limit data quirk).

calendar_countdown README: updated priority language (PRIORITY_AMBIENT,
not a literal "priority 20"), the poll_seconds default and its tuning
rationale, and the outdated "roughly half" alternation claim replaced
with the measured three-round table.

ci_status README: split the old single "Running-Job Badge" section into
"Display Priority Tiers" (shared framework, firmware facts, precedence)
and "Overlay Rotation: Running Badge + Quota Frames" (frame content,
show_quota config, headroom theming table, measured rotation rhythm
including the full 3-frame on-device sequence).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Critical fix: overlay_state["last_shape"] is now a unified tracker of
whatever payload shape (element-id set) was last actually drawn to APP,
checked and committed by every tier that can draw there -- an alert
badge, quiet-green text, or either overlay frame kind -- not just the
two overlay-frame shapes as before. The prior code only compared shapes
inside the overlay branch and reset the whole overlay_state dict
(wiping last_shape along with the rotation bookkeeping) whenever an
alert preempted the overlay or a run ended. That meant an alert
resolving back into an active-run poll saw "no shape on record" and
skipped the clear it actually needed, leaving the alert's `ci` text
element (timeout up to 1.5x poll_seconds, e.g. 180s at the default)
rendered on top of the running badge/quota frames for minutes -- the
v1.3.1 upsert-by-id bug class at a seam OVERLAY_FRAME_SHAPE never
covered. Fix: compute shape generically as
frozenset(e["id"] for e in payload["elements"]) once per draw, gate on
it right before the single client.draw() call regardless of which
precedence tier produced the payload, and split the alert/no-run
branch's state reset so it only pops frame_index/last_dwell_end
(rotation bookkeeping) -- last_shape is left untouched unless a clear
actually happens this same poll (the "nothing to show" branch) or a
draw actually lands (DRAWN-gated commit, same discipline as before).

Important fix: a skipped overlay dwell slot (quota data unavailable)
previously fell through into build_ci_payload(overlay=None), which --
with no alert and show_green off -- returned None and triggered an
unwanted client.clear(APP), evicting the still-valid, still-in-dwell
previous frame. run_once now returns early on a skipped slot before
build_ci_payload is even called, matching build_overlay_payload's own
"no draw, no clear" docstring contract; tests assert both draw and
clear call counts stay unchanged across a skip.

Minors: should_log_info's docstring and test_calendar_loop.py's header
comment still cited the interim 15s/"quadruple" numbers from the first
tuning round instead of the shipped 10s/"sixfold" (matching the
README, which was already correct); added the missing
_quota_used_width regression test for the live-observed used>limit
case.

New tests: test_overlay_then_alert_clears_stale_overlay_shape,
test_alert_then_overlay_clears_stale_alert_shape,
test_quiet_green_then_overlay_clears_stale_green_shape (all assert
clear() fires exactly once at the transition, not zero/twice), updated
skip-slot tests to assert clear.call_count == 0, updated
test_overlay_state_resets_when_run_ends for the corrected last_shape
semantics, added test_quota_used_width_clamped_max_when_used_exceeds_limit.

TZ=UTC uv run pytest -q: 174 passed (170 before this fix).

On-device: real run_once driven against the device (application_name
monkey-patched to "preview") through a synthetic failure -> resolved
-> running-badge transition. Alert frame captured with red bg + white
"CI FAIL o/r:tests" text (956 matching pixels). Post-transition frame:
zero stale red-bg or white-text pixels, 244 badge-bg pixels confirming
the new shape actually drew -- clean transition, no stale banner ink.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@sumitake
sumitake merged commit f32bc95 into main Aug 4, 2026
4 checks passed
@sumitake
sumitake deleted the dev/claude/ci-running-v1.5 branch August 4, 2026 04:37
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.

1 participant