Skip to content

feat(tray): render the 24h activity histogram inline at the top of the menu - #950

Merged
github-actions[bot] merged 7 commits into
mainfrom
feat/tray-inline-24h-histogram
Aug 3, 2026
Merged

feat(tray): render the 24h activity histogram inline at the top of the menu#950
github-actions[bot] merged 7 commits into
mainfrom
feat/tray-inline-24h-histogram

Conversation

@Dumbris

@Dumbris Dumbris commented Aug 3, 2026

Copy link
Copy Markdown
Member

What

The 24h activity chart used to hide behind an "Activity (24h)" submenu — the overview was the only glance row that required a second navigation step. It now renders inline, directly under the summary line, so the day's shape is on screen the moment the tray icon is clicked.

Verified live on a dev build (screenshot in the session): summary → chart → Recent → Clients, no submenu.

How

  • GlanceSection builds the row eagerly but caches the chart item by its shaped bars (HistogramBar is Equatable), so the debounced closed-menu rebuilds never re-render a SwiftUI chart nobody is looking at. rebuildMenu reuses one NSMenu via removeAllItems(), so re-adding the cached item is safe.
  • The row's kind (loading placeholder / failure row / 132 pt chart) is structural in updateInPlace: a kind flip resizes the menu, so it defers to menuDidClose like every other structural change (FR-023 discipline). Within a kind, new bars swap the hosted view in place (fixed chartItemSize frame, no resize) and a failure row refreshes its tooltip.
  • The submenu's "Not updating" marker is not ported: the inline row sits directly under the summary line, whose "not updating" segment already says it.
  • HistogramSubmenuDelegate is deleted. The pure loading/failed/chart decision (ActivityHistogram.state) and the injectable chart factory seam are unchanged.
  • Spec 048 invariant intact: building the row reads AppState only, no network request.

Tests

Old submenu-structure tests are replaced by GlanceInlineHistogramTests: the three row kinds, chart-wins-over-stale-failure, timeline-arrives-while-closed, the eager-build cache (factory called once for unchanged bars, same item instance), kind-flip-is-structural both directions, in-place view swap under an open menu, tooltip refresh, and the real-chart size/accessibility checks. Full swift test suite: 725+ tests, 0 failures.

…e menu

The chart used to hide behind an "Activity (24h)" submenu — the overview
was the only glance row that required a second navigation step. It now
renders inline, directly under the summary line, so the day's shape is
on screen the moment the tray icon is clicked.

- GlanceSection builds the row eagerly but caches the chart item by its
  shaped bars (HistogramBar is Equatable), so the debounced closed-menu
  rebuilds do not re-render a SwiftUI chart nobody is looking at.
- The row's KIND (loading placeholder / failure row / 132pt chart) is
  structural in updateInPlace — a kind flip resizes the menu, so it
  defers to menuDidClose like every other structural change. Within a
  kind, bars swap the hosted view in place and a failure refreshes its
  tooltip.
- The submenu's "Not updating" marker is not ported: the inline row sits
  directly under the summary line, whose "not updating" segment already
  says it.
- HistogramSubmenuDelegate is deleted; its loading/failed/chart decision
  (ActivityHistogram.state) and the chart factory seam are unchanged.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 3, 2026

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: b4be59b
Status: ✅  Deploy successful!
Preview URL: https://01c32d5d.mcpproxy-docs.pages.dev
Branch Preview URL: https://feat-tray-inline-24h-histogr.mcpproxy-docs.pages.dev

View logs

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

📦 Build Artifacts

Workflow Run: View Run
Branch: feat/tray-inline-24h-histogram

Available Artifacts

  • archive-darwin-amd64 (28 MB)
  • archive-darwin-arm64 (26 MB)
  • archive-linux-amd64 (17 MB)
  • archive-linux-arm64 (15 MB)
  • archive-windows-amd64 (28 MB)
  • archive-windows-arm64 (25 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (22 MB)
  • installer-dmg-darwin-arm64 (20 MB)

How to Download

Option 1: GitHub Web UI (easiest)

  1. Go to the workflow run page linked above
  2. Scroll to the bottom "Artifacts" section
  3. Click on the artifact you want to download

Option 2: GitHub CLI

gh run download 30830407136 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

…round 1)

The adversarial panel + opencode cross-model review caught a real
regression: making the histogram kind structural meant a timeline that
loads while the menu is open — the common open-just-after-launch case —
returned false from updateInPlace on every tick, freezing the summary,
row identities and client presence for the whole menu session. Main has
a test pinning exactly this (testTheTimelineArrivingWhileTheMenuIsOpen-
KeepsRowsUpdating), and it failed on the branch.

- The histogram never vetoes the in-place update. Its two text kinds
  share one-line geometry and rewrite each other in place (a failing
  fetch replaces "loading…" instead of leaving it up); a text-to-chart
  flip keeps the built row on screen — the height cannot change under
  the cursor — and the next rebuild installs the chart, since
  menuWillOpen runs one before every display.
- The chart cache is also keyed by the current time zone: the bars are
  UTC-keyed and survive a zone change, but the VoiceOver "busiest hour"
  label does not.
- Stale "submenu" wording in the factory seam doc corrected.
@Dumbris

Dumbris commented Aug 3, 2026

Copy link
Copy Markdown
Member Author

Adversarial review round 1 (two independent lenses + opencode/gpt-5.6-sol cross-model; codex quota-blocked) — findings, all addressed in c3cb0ed:

  • Real regression caught: making the histogram row's kind structural froze the ENTIRE glance block (summary, row identities/click payloads, client presence) for the whole menu session when the timeline loaded while the menu was open — the common open-just-after-launch case. Main pins this behavior in testTheTimelineArrivingWhileTheMenuIsOpenKeepsRowsUpdating, which was red on the branch. Fixed: the histogram never vetoes the in-place update; text↔text rewrites in place, text↔chart keeps the built row until the next rebuild (menuWillOpen runs one before every display).
  • A fetch failing while the menu is open now replaces "loading…" in place instead of leaving it up.
  • The chart cache is additionally keyed by the current time zone (bars are UTC-keyed and survive a zone change; the VoiceOver "busiest hour" label does not).

Verified clean by the lifecycle lens on the current tip: cached-item re-add across rebuilds (single NSMenu, removeAllItems() on every path), hidden-block cache staleness, mid-open fixed-frame view swaps, no retain cycles, spec-048 no-fetch invariant, and structural discipline (no half-written menu path).

Deliberate accessibility decision: the old enabled "Activity (24h)" submenu anchor was keyboard-highlightable; the inline chart is a disabled view-backed item, skipped by arrow-key navigation (the chart data itself was equally keyboard-unreachable in the submenu — its only row was disabled). The full series summary stays on the host view for the VoiceOver cursor, and the summary text line above carries the headline numbers.

Note: AppLifecycleTests.testTheSharedJournalNeverWritesToTheRealInstanceRootUnderTests fails identically on main when a real tray run has written ~/.mcpproxy/tray-lifecycle.jsonl — environmental, not this PR.

… prose

The menu was over 600pt wide: failed rows put a 40-char error clause on
the title line next to a 34-char label, and Needs-Attention rows carried
untruncated core errors. The 24h chart also spent 132pt of height on a
mostly-empty plot.

Width discipline: the widest thing in the menu is now the chart block —
the element designed to be looked at.

- Failed rows are one fact per line: title is always `label ×N — age`;
  the error clause takes the muted second line (the failure mark already
  flags the row) and the displaced reason stays in the tooltip and the
  VoiceOver string. Pre-14.4, with no second line, the clause rejoins
  the title under a tighter 28-char budget.
- A row GAINING its error line is structural (defers to close), exactly
  like a run gaining its reason; the identity-turnover and late-failure
  liveness tests pin the one-line fallback where the rewrite happens in
  place.
- Budgets: label 34→30, second line 60→44 — both sized to the chart
  block width at the menu font.
- Chart: 248×84 incl. legend (was 260×116), item 272×96 (was 288×132) —
  a shape to recognise, not a plot to read values off; the Web UI has
  the full-size version.
- Needs-Attention rows truncate to the same 44-char budget, full text in
  the tooltip.
@Dumbris

Dumbris commented Aug 3, 2026

Copy link
Copy Markdown
Member Author

Round 2 — compact revision (owner feedback: menu too wide, chart too big; screenshot-verified live):

  • Width discipline: the widest thing in the menu is now the 24h chart block (272 pt), never text. Failed rows are one fact per line — title label ×N — age, error clause on the muted second line, displaced reason in the tooltip + VoiceOver string. Budgets: label 34→30, second line 60→44, pre-14.4 fallback clause 40→28. Needs-Attention rows truncate to the same budget with the full text in the tooltip.
  • Chart bounded: 248×84 incl. legend (item 272×96, was 288×132) — a shape to recognise at a glance; the Web UI keeps the full-size chart.
  • Structural consistency: a row gaining its error line defers to menu close, exactly like a run gaining its reason (FR-023); the identity-turnover/late-failure liveness tests pin the one-line fallback path where the rewrite lands in place, plus a new test pinning the structural case.

This is a deliberate compact revision of spec 090 FR-011a ("error joins the title") at the owner's request. Full suite: 787 tests, only the known environmental AppLifecycleTests failure (fails on main too).

Dumbris added 2 commits August 3, 2026 17:02
Round-2 verify notes: the 44-char truncation of Needs-Attention rows
also truncated what VoiceOver reads (tooltips are not spoken), so the
row now carries the full text as its accessibility label — the same
FR-025 truncated-on-screen/spoken-in-full discipline as the glance
rows. Three comments still describing the chart as 132 pt updated to
its actual 96 pt.
…e day

Owner feedback round 3:

- Needs Attention is one row with a count and a warning icon; the
  per-server rows live in its submenu, a hover away. N servers no
  longer cost N rows of a menu that opens with a chart. The section
  stays absent entirely when nothing needs attention (as before).
- A loaded-but-idle 24h axis renders as the muted sentence "No calls
  in the last 24h" instead of 24 empty bars — an all-zero chart reads
  as a broken widget, while the words say what the flat axis implied.
  Idle is a third text kind: it swaps with loading/failed in place
  (one-line geometry) and defers a flip to/from the chart to the next
  rebuild like the other text kinds.
@Dumbris

Dumbris commented Aug 3, 2026

Copy link
Copy Markdown
Member Author

Round 3 (owner feedback): Needs Attention collapses into a submenu — one row with count + warning icon, per-server detail on hover, absent entirely when nothing needs attention. An idle 24h axis renders words, not an empty chart: muted "No calls in the last 24h" (new third text kind — swaps in place with loading/failed, text↔chart flips defer to the next rebuild like before).

Both screenshot-verified live on a dev build. Cross-model review (opencode) on the delta: no genuine defects — histogram state/cache transitions consistent, submenu ownership survives rebuilds, collapsed parent stays accessible. Suite: 787 tests, only the known environmental AppLifecycleTests failure.

Dumbris added 2 commits August 3, 2026 18:59
Owner-reported contradiction: the Recent list showed calls from days ago
directly under a chart saying "No calls in the last 24h" — three windows
(hour summary, 24h chart, unbounded recent list) in one block.

The menu now speaks ONE frame, the 24 hours the histogram draws:

- Header: "N calls in the last 24h" via new glanceCallsLast24h — the
  same poll+live-SSE reconciliation as the hour count (GH #934), summed
  over the histogram axis. A zero says nothing the idle row does not,
  so the segment appears only when there is something to count, and a
  summary with nothing to say gets no row (presence flip = structural).
- Recent: rows filtered to the frame (GlancePresence.lookback — already
  24h for clients); a retained record from days ago no longer appears.
  With nothing qualifying, the section is just "Open Activity…" — a
  header over an empty list explains nothing.
- Clients placeholder: "No clients in the last 24h", naming the frame.
- The hour-count machinery stays (callsThisHour + glanceCallsThisHour
  and their reconciliation tests are the tested contract the 24h getter
  shares its live-call state with).

Header-consistency tests now pin the frame semantics: counts SURVIVE an
hour rollover; only sliding off the 24h axis drops them.
…itic)

Codex round 1 on the frame commit: Recent filtered on a raw 86,400 s
cutoff while the header and chart use the hour-aligned 24-bar axis, so
at e.g. 12:15 a call from yesterday 12:30 showed in Recent while the
chart had already dropped its hour — recreating the contradiction one
hour at a time. Recent now keeps a run only while its HOUR is still on
the axis, the same rule ActivityHistogram.bars and callsInLast24Hours
apply. Its other finding — the poll/SSE snapshot watermark race — is
the pre-existing, deliberate GH-#934 trade-off (boundary = issue time;
transient over-count self-corrects at the next 30 s poll), unchanged.
@Dumbris

Dumbris commented Aug 3, 2026

Copy link
Copy Markdown
Member Author

Round 4 (owner feedback): one 24h frame for every number in the menu. The reported contradiction — Recent rows from days ago under "No calls in the last 24h" — is gone:

Codex is back and served as critic: round 1 caught a real edge (Recent used a raw 86,400 s cutoff vs the hour-aligned axis — a call 23 h 40 m old could show in Recent after its hour slid off the chart); fixed in b4be59b with an edge test. Round 2: CLEAN, and it confirmed the poll/SSE snapshot watermark race it flagged is the pre-existing, deliberate GH-#934 trade-off on main, not worsened here.

Screenshot-verified live. Suite: 789 tests, only the known environmental AppLifecycleTests failure.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Approved (Model B): adversarial panel + opencode + codex critic all clean at this head SHA; qa-gate green; CI fully green across 4 owner-feedback rounds. Arming auto-merge.

@github-actions
github-actions Bot merged commit 195f47f into main Aug 3, 2026
37 checks passed
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