Totals over the window, a readable time axis, and a build stamp - #38
Merged
Conversation
A rate sample is the mean over the gap before it, so multiplying each sample by the interval it measures and summing telescopes back to exactly the counter delta. No counter history is kept, no source changes, and nothing is read that the ring buffer does not hold. WindowTotal reports the span it actually covered alongside the value. Ten seconds after launch the buffer holds ten seconds, and putting "2 min" under a number covering a twelfth of that is the quiet kind of wrong. A gap wider than maximumGap is clipped rather than credited in full, so a slept laptop cannot invent traffic. MetricUnit.accumulation says what a rate adds up to, derived from the unit the way ChartMirror and ChartStack read direction and composition. Network totals in bytes and the divide by eight lives there: a link is quoted in bits, a volume in bytes. Claude-Session: https://claude.ai/code/session_01EqTTtmt4fyNtMxjc2ZVBzj
A second number per legend entry: how much moved, beside how fast it is moving. The span sits once beside the title rather than after every entry, since the History picker is global and repeating it down a legend is four copies of one fact. The span reads the window only when the card really has it. Ten seconds after launch it says ten seconds, because "2 min" over a twelfth of that looks exactly like the right answer and nobody would check it. An entry covering materially less than the card's stated span is dimmed — one source failed while its neighbour kept reading. AppModel.totalGap is four ticks of the master clock, so it follows the Sampling tab. A ceiling fixed for the 0.5 s default would clip every interval of a sampler slowed to 2 s and quietly report a quarter of the traffic. Off by default, like mirroring and stacking. Narrower reason: those change what the picture means and this only adds a number, but it adds one to a header already decided by ViewThatFits. Claude-Session: https://claude.ai/code/session_01EqTTtmt4fyNtMxjc2ZVBzj
The argument for matching mirroring and stacking lost on first contact: shipped off, the reaction to the finished feature was "I don't see it". Those two change what the picture means, so a reader who never asked for them deserves the chart they had. A total only adds a number beside one already there, and a number nobody can find is worth less than a header that reflows. Claude-Session: https://claude.ai/code/session_01EqTTtmt4fyNtMxjc2ZVBzj
08:52:3008:53:0008:53:30. Three things were wrong at once. The room was measured off the chart rather than the plot, with a flat 50-point allowance standing in for the y-axis — right for one card, and generous for every card whose numbers are long, which are the cards whose plots are narrowest. chartBackground hands over the real plot rect. Not chartOverlay: an overlay would swallow the mouse-down a tile's drag needs. Every stride was costed at the same label width, though a stride of a minute or more shows no seconds and needs a third less room. Measured, at the new smaller label size, that is 20 points against 32. Budgeting the coarse strides at the wide figure left the axis unable to reach for the one a cramped card wanted. And the labels were wider than they needed to be. They get their own size now, smaller than the y-axis labels beside them, and the hour loses its leading zero. Rule 2 now beats rule 1. Slightly tight labels do beat a bare axis, but these were not slightly tight, they were on top of each other. Two labels are still what the walk reaches for; what is gone is the promise. Turning them sideways is how a narrow card has both: on its side a label costs its line height, 10 points against 32, so it fits four times where upright it fits two. Claude-Session: https://claude.ai/code/session_01EqTTtmt4fyNtMxjc2ZVBzj
A monitor is left running for days, and the copy on screen is very often not the copy just built. "Am I looking at the change I just made?" should not cost two clicks. The two halves are sourced differently on purpose. The commit is stamped at build time by a prebuild plugin running git describe, because a running program has no other way to know it and a checked-in constant is one somebody has to remember to update. The build time is read at runtime from the executable's modification date: stamping it too would rewrite a source file on every build and recompile MonitorCore every time, which is the fast swift run loop gone for a fact the filesystem already has. describe rather than a bare hash, for the -dirty. A build with uncommitted changes is not the commit it names. Drawn in Theme.readout rather than the title bar's secondary style. A stamp nobody can read at a glance is one nobody checks, which is the same failure as not having one. Claude-Session: https://claude.ai/code/session_01EqTTtmt4fyNtMxjc2ZVBzj
Wrapped entries put the totals at four different left edges down the same card. A card with totals now draws a small Grid instead of a FlowLayout: swatch, name, rate, total. Right-justified, because these are magnitudes and a magnitude is read by where its last digit sits — 104 MB over 48 MB aligned on the left puts the hundreds above the tens and hides the difference the column exists to show. Headed, because a second bare number beside a rate does not say what it is. The span beside the title says how long; the heading says of what. Under the title, never beside it. ViewThatFits is the right question for a wrapping row and the wrong one for a column of figures: pushed right of the title on a wide card, the heading floats in the middle of the header with nothing under it that reads as a table. Cards with nothing to total keep the flow — Memory's seven slices and five temperature sensors want wrapping, not a column. Claude-Session: https://claude.ai/code/session_01EqTTtmt4fyNtMxjc2ZVBzj
The stamp borrowed the toolbar's styling and came out as dark text on a light glass pill — the lowest contrast anywhere in the window, on the one element whose whole purpose is to still be legible in a PNG somebody opens next month. It was clipped where the capsule ended, too. macOS 26 wraps every toolbar item in that shared capsule. The stamp is not a control and should not dress as one, so it opts out and paints its own background. The call is gated to 26 and additive: earlier releases add no capsule and need no opt-out. Not the panel's palette and not the cards' monospaced face either. This is the one thing in the window that is not a reading — everything else is a measurement styled to be scanned, and this is a label on the photograph. Claude-Session: https://claude.ai/code/session_01EqTTtmt4fyNtMxjc2ZVBzj
One block — a heading with its build underneath — rather than a chip parked next to a system title repeating it. So the window's own title goes: toolbar(removing: .title), or an empty navigationTitle on macOS 14, where the .title kind does not exist. The Window scene keeps its real name, so the Window menu and the Dock still say what this is. That name now lives beside the version in MonitorVersion. Two spellings of one name is the sort of thing nobody notices until a screenshot. A rounded rectangle rather than a capsule now that it is two lines: a capsule's ends bow away from a left-aligned second line. Claude-Session: https://claude.ai/code/session_01EqTTtmt4fyNtMxjc2ZVBzj
Removing the window title cost the toolbar something that was not obvious until it was gone. The title was taking the slack in the middle, and it was that, not the items' own placement, pushing the History picker and the size and rate controls to the right. Without it, .automatic packed them up against the stamp. .primaryAction says what the layout actually depends on, instead of leaning on something that is no longer there. Claude-Session: https://claude.ai/code/session_01EqTTtmt4fyNtMxjc2ZVBzj
This was referenced Aug 28, 2026
Splitting this session into three left them stacked, and a stacked pull request is not independently reviewable or mergeable — so the split bought none of the review value it looked like it bought. What it cost was a squash-merge and a release each, for one change. The separable history belongs in the commits, where it is free. Claude-Session: https://claude.ai/code/session_01EqTTtmt4fyNtMxjc2ZVBzj
There is one window. A change to a card, the axis under it and the title bar above it are seen together or not at all, and the only useful question — does this read better — can only be asked of the whole screen. Claude-Session: https://claude.ai/code/session_01EqTTtmt4fyNtMxjc2ZVBzj
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.
Closes #35.
Three related pieces of one session, in one pull request because they were only
ever mergeable in order — see the commits for the separable history.
1. Totals over the window
Every legend entry gains a second number: how much moved over the window the
chart draws, beside how fast it is moving now. Network and disk in bytes,
packets and disk operations as counts.
The sum is already in the buffer. A rate sample is the mean over the gap
before it, so
Σ(rate × preceding gap)telescopes back to exactly the counterdelta. No counter history, no source changes, nothing read that the ring buffer
does not already hold — so it stays well inside the rule that v1 writes nothing
to disk. The round-trip test is the premise: differentiate a synthetic counter
with
RateTracker, integrate it back, expect the original delta.Three things it refuses to fake:
ten seconds, and "2 min" over a twelfth of that looks exactly like the right
answer — which is why nobody would check it.
maximumGapis clipped, so a laptop back from sleep cannot credit one sample with an hour
that never crossed the wire.
AppModel.totalGapfollows the Sampling tabrather than assuming 0.5 s.
RateTrackergives on a first read.Network totals in bytes while the rate stays Mbit/s: a link is quoted in
bits, a volume in bytes.
MetricUnit.accumulationholds the ÷8 as its onlydefinition, derived from the unit like
directionandcomposition.A card with totals draws a
Grid— headed, right-justified — rather than thewrapping
FlowLayout. A magnitude is read by where its last digit sits, andwrapped entries put them at four different left edges.
2. A time axis that does not collide
From a screenshot:
08:52:3008:53:0008:53:30. Three things were wrong at once.allowance stood in for the y-axis — right for one card, generous for every
card whose numbers are long, which are the cards whose plots are narrowest.
Now read from the real plot rect via
chartBackground. NotchartOverlay:an overlay would swallow the mouse-down a tile's drag needs.
minute or more shows no seconds. Measured: 20 points against 32.
loses its leading zero.
Rule 2 now beats rule 1. "Never fewer than two labels" used to win on the
grounds that slightly tight beats bare. Right about slightly tight, wrong about
the output: four labels 32 points wide with centres 40 apart. Two labels are
still what the walk reaches for; what is gone is the promise.
Turn the time labels sideways, new in the Charts tab, off by default: on its
side a label costs its line height, so the narrowest card fits four times where
upright it fits two.
3. A build stamp in the title bar
There to identify which build produced which screenshot. White on black in the
system font — deliberately not the panel's palette or the cards' monospaced
face. It is the one thing in the window that is not a reading: everything else
is a measurement styled to be scanned, and this is a label on the photograph.
Two facts, two sources. The commit is stamped at build time by a new
StampCommitprebuild plugin (git describe --tags --always --dirty) — arunning program has no other way to know, and a checked-in constant is one
somebody has to remember to update. The build time is read at runtime from the
executable's modification date: stamping it too would rewrite a source file on
every build and recompile
MonitorCoreevery time, for a fact the filesystemalready has. The plugin rewrites its output only when the hash changes, for the
same reason.
Three things this turned up on the way:
dark-on-light and clipped it. It opts out with
sharedBackgroundVisibility(.hidden), gated to 26 and additive.toolbar(removing:)is macOS 14 but the
.titlekind is 15, so 14 gets an emptynavigationTitle.taking the slack in the middle, not the items' placement. They are
.primaryActionnow, which says what the layout actually depends on.Verification
215 tests in 26 suites,
swift build -c release, andswiftformat Sources Tests Plugins --lintall clean. Lint now coversPlugins;ci.ymlupdated to match.ChartAxisTestsis rewritten against the new contract, keeping every existingclaim and adding the ones this needed — chiefly labels never overlap, swept
across every window, six plot widths, both orientations and forty phases each.
All three pieces were looked at in the running app and revised from what they
actually looked like, which is where most of the above came from.
https://claude.ai/code/session_01EqTTtmt4fyNtMxjc2ZVBzj