Releases: binlecode/co-awareness
Release list
v2.2.0
Added
- Unified telemetry source list: The top metrics section and the collapsible "Other Sources" list are merged into a single flat source list at the top of the menu (
CPU,Memory,DRAM Bandwidth,GPU,Network,Disk,Fan,Battery,Temperature,ANE). Each available source displays its live readout directly. Clicking any row immediately switches the active telemetry monitor driving the status bar item. - Trace Chart and Live Value at the top of Presets: The
Presets ▸submenu now featuresTrace ChartandLive Valueat the top above the GIF presets. SelectingTrace ChartorLive Valueswitches the status bar representation directly, while selecting any GIF switches to that animated runner. Mutually exclusive selection checkmarks indicate the active representation. - Single status bar display architecture (
--display): The status bar representation is configured via--display <gif|trace|value|chart>orCO_AWARENESS_DISPLAY, unifying all visual forms into one slot with zero jitter.
v2.1.0 — the trace chart menu-bar label
Added
--label chart: the last 60 seconds, on the menu bar. The label slot could show a number or
a fixed tag, but a single reading can't tell you whether a spike is just starting or already over,
and the dropdown's sparkline only showed that after a click. A new label mode, Trace Chart
(--label chart,CO_AWARENESS_LABEL=chart, or Settings ▸ Menu Bar Label), draws the active
source's load history as a 45 pt bar sparkline in the adjacent slot. It uses the same
green/amber/red thresholds as the dropdown (inverted for battery) and redraws on the same 2 s tick.
The slot keeps a fixed width, so nothing beside it moves; a Keep Awake countdown sits next to the
chart, closest to the icon, in the Keep Awake tint. Remembered across relaunches like the other modes.
chartis now a reserved keyword, so a custom label reading literally "chart" can't be expressed.
release: v1.23.1 — the menu-bar countdown pauses when the icon is hidden
Fixed
- A timed Keep Awake kept a 1 Hz redraw running while the icon was hidden. The menu-bar countdown
measured and relaid out its slot every second even with the status item behind the notch, in menu-bar
overflow, on another Space, or with the display asleep — the one case the app's pause-when-hidden
rule exists to cover, and an 8-hour window held it for eight hours. The countdown now reads the same
occlusion verdict the animation does and stops with it, redrawing on the way back so the slot never
shows the second it went dark on. A countdown in an open menu is unaffected: the menu is its own
window, visible whatever the status item is doing.
release: v1.23.0 — Keep Awake counts down on the menu bar itself
Added
- Keep Awake can now wait for a process instead of a clock.
--keep-awake-pid <pid>(or
MENUBAR_LOAD_RUNNER_KEEP_AWAKE_PID) holds the Mac awake until that process exits, then releases
on its own — the shape an unattended terminal job actually has, where picking2hor4his a
guess in both directions and the guess that ends early is the one that costs the job. From the menu,
Keep Awake ▸ Until a process exits…takes a pid or a name, matching the newest process of
that name you are running, so binding to a job already in flight doesn't start with apgrep. The
hold names its subject wherever it is shown (Keep Awake: claude (41293)), obeys the battery band
and the 5% floor like any other window, and is deliberately never resumed after a reboot — pids are
recycled, so a restored one could bind to something unrelated. It survives an in-app restart, which
doesn't reboot the Mac. Release is event-driven (a kqueue exit watch), with a 2s liveness re-check
underneath it as a fallback. - A timed Keep Awake now counts down on the menu bar itself, not just inside the menu. How long
is left was previously a fact you had to open the menu to learn, which is the wrong shape for the
thing it answers — you glance at a menu bar, you navigate a menu. With the label off, the slot
reveals the countdown on its own (29:58) in the Keep Awake tint, wearing the paused tone when a
condition has the hold suspended, and collapses back to nothing when the window ends or you disarm
it. With--label valueor a custom label, the countdown sits alongside the reading rather than
replacing it (CPU 45% 29:58), always on the side nearest the icon. The slot reserves width for
the full88:88/88:88:88template, so a ticking countdown shifts neighboring items by 0 pt —
the same no-jitter promise the telemetry labels already make. An indefinite or process-bound hold
shows nothing, having no clock to show. The 1 Hz ticker behind it runs only while there is a live
countdown to draw or the menu is open, and stops otherwise.
Fixed
- A countdown that ran out while Keep Awake was paused stayed frozen at
00:01on the menu bar.
If the window elapsed while a low battery had the hold suspended, the bar kept showing one second
remaining indefinitely — and disagreed with the menu, which correctly showed the window as over.
The readout was reusing the seconds-remaining value that thecaffeinaterespawn needs floored at
1s; it now reads the deadline directly, like the in-menu row always did. The 1 Hz ticker that the
stuck value kept alive now stops with it. - An externally killed
caffeinateleft a process-bound hold half-armed. Keep Awake correctly
went off, but the process binding survived it — so restarting the app from the menu silently
re-armed a hold the user had not asked for, and re-enabling from a tint row re-attached to the old
process. The binding is now released with the intent, and the restart path forwards a bound pid
only while Keep Awake is actually on.
release: v1.22.0 — freeze animation + honor Reduce Motion (R17)
Added
- The animation can now hold still — and does so on its own when macOS asks. The app has always
throttled itself under pressure and paused when hidden, but the one OS signal saying this user
wants less motion — System Settings → Accessibility → Display → Reduce Motion — did nothing,
and the only way to calm the icon was to quit. Now that setting freezes the animation live (no
relaunch), and a manualSettings ▸ Freeze Animationtoggle reaches the same machinery for
screen-sharing or just a quieter menu bar. Frozen means frozen: the game loop stops entirely (zero
redraws — cheaper than even the slowest animation) and the icon holds its current frame, no jump.
Because the animation's speed is the readout, a frozen icon doesn't go silent: with the menu-bar
label off, the label slot temporarily shows the live value instead (a custom label is respected,
and your saved label choice is untouched — the menu saysoff (value while frozen)so nothing is
invisible). The toggle survives a relaunch; the Reduce Motion half is re-read fresh each launch.
While the OS setting holds, the menu row readsFreeze Animation — on via Reduce Motionand its
checkmark keeps tracking your own toggle, which stays editable underneath. Menu-only, like the
label position — no new CLI flag. MENUBAR_LOAD_RUNNER_LOG_ANIMATION=1(debug/test hook): prints the freeze gate, frame cursor,
speed, and label-handoff state each 2s tick, sotests/qa.sh§3g can assert a frozen launch never
animates — the sibling of theLOG_SLOTS/LOG_ASSERTIONS/LOG_AWAKEhooks, for the same
no-TCC-grant reason.
release: v1.21.0 — build the update before the restart, not during it
Fixed
- Restarting after a self-update left the menu bar empty for minutes. Reported as "after
auto-upgrade, restart no longer works" — and the restart did in fact work, which is the whole
problem: it just took 132 seconds during which nothing was on screen and nothing said why.
Applying an update pulled the source only, leaving the compile to the launcher at restart, i.e.
to the one window in which the app does not exist. The build now runs between the pull and the
Restart offer, while the app is still up and animating, so the restart itself is about a second.
The cost was bimodal, which is why this survived so long: against a warm clang module cache the
deferred build was ~6.5s and looked fine, cold it was 32s, and under load on the day it was
measured, 132s. The dropdown reports the new phase asBuilding vX.Y.Z…rather than sitting
silent, and a build that fails is not fatal — the launcher still compiles at restart, and the
alert says so instead of letting a long gap read as a failure. - A restart that never happened left no trace. The relaunch is handed to a detached shell that
waits for the app's pid to disappear; its output went to/dev/null. When the 30s wait expired
with the app still alive it ran the launcher anyway, the singleton guard declined, and that was
the end of it — silently, in the one code path whose failure the app cannot report, because by
then it has quit. Both the timeout and the launcher's reason now go to the log file. - The launcher compiled before checking the singleton guard. A duplicate launch paid for a full
swiftcrun only to be turned away, and during a long build — when no instance is up yet — it
could start a second compile writing the same output as the one already in flight. The guard runs
first now. - Rebuilding could corrupt a running instance. The build was written over the binary in place,
which is unsafe for a process paging out of it;scripts/install-login-item.shdid exactly that
on every reinstall. Builds now go to a temp and are renamed into place, so the live process keeps
its own inode.
Added
--precompileon the launcher: build the binary if the source is newer, then exit without
launching. Safe to run while an instance is live. This is the single place the build command
lives —install.sh,scripts/install-login-item.sh, and the in-app updater all call it instead
of carrying their ownswiftcline, so the flags cannot drift out of agreement.
Internal
tests/qa.sh§6 covers--precompilebuilding without launching, a live instance surviving a
rebuild, and a rejected launch leaving the binary untouched; §2 now checks the launcher's own
--helplists its own flags, which nothing did before.tests/install-smoke.shasserts that installing never starts the app, and cleans up any
instance it finds under its sandbox rather than letting one outlive the directory it was
installed into.
release: v1.20.1 — an idle die is a reading, not a missing one
Fixed
-
The temperature source could claim it had no reading while it was visibly driving the
animation. Caught by hand in the release's own menu walk: the dashboard showed
Temperature: warming up...on one row andSpeed Multiplier (auto: Temperature): 1.74xtwo rows
below it, after minutes of correct readings. The cause is a distinction 1.20.0 failed to make. A
powered-down core cluster answers the sensor read with0rather than declining it, and those get
filtered out so the menu doesn't print-4 °C; but on a tick where every cluster happens to be
parked, the filter emptied the whole set and the reader reported that as missing data. It
isn't — it is a reading of idle. Only a total read failure (the SMC going away mid-run) is
missing data, and the two are now separate: an all-parked tick reports an idle load and the menu
saysTemperature: ≤30 °C · every core cluster parkedrather than inventing a measurement.Rare — it did not recur in 45 sampled ticks or three targeted attempts to reproduce it — and
cosmetic when it fired, since the animation simply held its last speed. Recorded in
docs/ROADMAP.md§ Verification debt, because the parked branch cannot be reached on demand: the
only hook that would force it is one that changes a decision, which this repo's testing rules bar.
release: v1.20.0 — die temperature as an eighth load source
The eighth load source, and the first one that reads how hot the machine is rather than how busy.
Added
-
An eighth load source: die temperature (
--load-source temperature, or pick Temperature
from Other Sources in the menu). Where fan speed is the lagging thermal signal — fans trail
the work that heated the machine by seconds — this is the leading one: the die responds in
milliseconds. Read at the same unprivileged tier as every other source (nosudo, no
entitlement), straight off the performance-core temperature sensors via the sharedSMCClient
that shipped in 1.19.4. The menu shows the hottest sensor, the spread across all of them, and how
many answered; the menu-bar label showsTMP 72°. Machines where no sensor answers (typically
VMs) disable the row and fall back to CPU, exactly like fan on a fanless Mac.Three things about it are deliberate and worth knowing. The animation follows the hottest
sensor, not the average — throttling responds to the hottest die, and averaging a loaded core
cluster against an idle one hides the event worth watching. The scale is absolute (30 °C idle
→ 100 °C flat out) rather than rescaled to your machine's recent range, so a given animation speed
means the same temperature on every Mac and on every day; the cost is that a Mac which cools well
never quite reaches the top of the range, which is the honest reading rather than a flattering
one. And only ~12 cluster sensors are sampled per tick rather than all 102 the chip publishes —
they carry the same maximum, and the full sweep would have cost about 1% of a core continuously,
which is not a thing an indicator built to not add load gets to spend.
release: v1.19.4 — extract the SMC plumbing into a shared SMCClient
An internal release: the SMC plumbing moved out of the fan reader into a shared client. Nothing you
can see changed, and that is the whole acceptance criterion.
Fixed
- The SMC connection is now shared, so a second sensor source can't open a second one. Reading
fan RPM opens a connection to the System Management Controller, and that connection is never
closed — the app holds it for its whole life and the kernel reclaims it at exit. That is fine for
one. But the plumbing was private to the fan reader, so the next SMC-backed source (die
temperature) would have had to duplicate it and open its own, leaving two permanently-open
connections where one would do. The mechanism now lives in a single sharedSMCClient, and the
struct-layout safety check that disables the SMC path on an incompatible toolchain now gates
every SMC source at once instead of only the reader holding its own copy of the check.
Unchanged, deliberately
- Fan readings, the per-fan menu lines, availability on fanless Macs, and every CLI flag and
environment variable. The fan label and menu rows were captured from the pre- and post-change
builds back-to-back and are identical, at two hardware states (fans warm, and both fans stopped).