From de99ec94084f17706d4da69bc467a81ccee4a294 Mon Sep 17 00:00:00 2001 From: Philippe Matray Date: Sat, 5 Sep 2026 21:02:49 +0200 Subject: [PATCH] feat(claude): a context-window readout for sessions you are not watching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit macarchy-core's Claude Code status line writes each session's context percentage to `$XDG_RUNTIME_DIR/macarchy-claude/` as it renders. This module reads those back every five seconds and puts the fullest one on the bar: green under 60 %, orange, red at 85 % where a compaction is close. The point is the sessions off-screen. Running a fleet of agents, the terminal in front of you says nothing about the three working in branches you cannot see — so the pill carries a badge with how many are alive, and the percentage is the worst of them, not the nearest. Files from sessions that have exited are swept on the same pass, and tmpfs clears whatever is left at logout. With no status line running the pill is a plain icon, so it costs nothing to leave in a layout. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01TN4cPFxUtafhd87w2ZqY7R --- README.md | 22 +++++++++++ modules/claude/manifest.json | 2 + modules/claude/touchbar.py | 76 ++++++++++++++++++++++++++++++++++++ tests/test_module_claude.py | 68 ++++++++++++++++++++++++++++++++ 4 files changed, 168 insertions(+) create mode 100644 modules/claude/manifest.json create mode 100644 modules/claude/touchbar.py create mode 100644 tests/test_module_claude.py diff --git a/README.md b/README.md index 7db8e07..f53b7ea 100644 --- a/README.md +++ b/README.md @@ -184,6 +184,28 @@ left = ["menu", "core.spacer"] right = ["group:system", "core.clock"] ``` +## claude.context — the agent you are not watching + +`claude.context` is a readout of how full the Claude Code context window is. +macarchy-core's status line drops each session's percentage in +`$XDG_RUNTIME_DIR/macarchy-claude/` as it renders; the module reads +them back every five seconds, shows the *fullest* one and sweeps the files of +sessions that have exited. Green under 60 %, orange, then red at 85 %, where a +compaction is close. Running several sessions at once, the pill carries a badge +with how many are alive — the number that matters when three agents are working +in branches you cannot see. + +```toml +[items.claude] +widget = "claude.context" + +[layouts.default] +right = ["claude", "core.clock"] +``` + +With no status line running the pill stays a plain icon, so it costs nothing to +leave in the layout. + ## Writing a module A module lives in its own directory with a `manifest.json`: diff --git a/modules/claude/manifest.json b/modules/claude/manifest.json new file mode 100644 index 0000000..8c9e091 --- /dev/null +++ b/modules/claude/manifest.json @@ -0,0 +1,2 @@ +{ "schemaVersion": 1, "id": "claude", "name": "Claude Code", "kinds": ["touchbar-module"], + "entryPoints": { "touchbarModule": "touchbar.py" }, "touchbarModule": { "apiVersion": 1, "order": 25 } } diff --git a/modules/claude/touchbar.py b/modules/claude/touchbar.py new file mode 100644 index 0000000..db15f38 --- /dev/null +++ b/modules/claude/touchbar.py @@ -0,0 +1,76 @@ +"""claude: how full the Claude Code context window is. + +Each running status line (macarchy-core's `claude-statusline.sh`) drops its +context percentage in `$XDG_RUNTIME_DIR/macarchy-claude/` on every +render. This reads them back, so the sessions you are *not* looking at still +tell you when they are about to compact. +""" +import glob +import os +import time +import weakref + +from macarchy_touchbar.widgets import Button + +# A live status line rewrites its file on every render, so anything older than +# this belongs to a session that has exited. tmpfs clears the rest at logout. +FRESH = 90 + + +class Module: + DIR = None # tests point this somewhere writable + + def setup(self, api): + self.api = api + self.widgets = weakref.WeakSet() + api.widget("context", self.context) + api.every(5, self.refresh) + + def dir(self): + if self.DIR: + return self.DIR + runtime = os.environ.get("XDG_RUNTIME_DIR") + return os.path.join(runtime, "macarchy-claude") if runtime else None + + def state(self): + """(fullest session's percentage, live session count), sweeping the dead.""" + d = self.dir() + if not d: + return None, 0 + now, live = time.time(), [] + for path in glob.glob(os.path.join(d, "*")): + try: + if now - os.path.getmtime(path) > FRESH: + os.unlink(path) + continue + with open(path) as f: + live.append(int(f.read().strip())) + except (OSError, ValueError): + continue + return (max(live) if live else None), len(live) + + def refresh(self): + pct, count = self.state() + theme = self.api.theme + if pct is None: + text = tint = badge = None + else: + text = f"{pct} %" + if pct >= 85: + tint = theme.ACCENT_RED + elif pct >= 60: + tint = theme.ACCENT_ORANGE + else: + tint = theme.ACCENT_GREEN + # Only worth saying how many when it is more than the one in front of you. + badge = str(count) if count > 1 else None + for w in list(self.widgets): + if (w.text, w.tint, w.badge) != (text, tint, badge): + w.text, w.tint, w.badge = text, tint, badge + w.invalidate() + + def context(self, api, **p): + w = Button(api, icon="data_usage", **p) + self.widgets.add(w) + self.refresh() + return w diff --git a/tests/test_module_claude.py b/tests/test_module_claude.py new file mode 100644 index 0000000..84aa997 --- /dev/null +++ b/tests/test_module_claude.py @@ -0,0 +1,68 @@ +import os +import time + +from macarchy_touchbar.draw import Theme +from macarchy_touchbar.loop import EventLoop +from macarchy_touchbar.modules import Registry, ModuleHost, ModuleSpec +from tests.test_modules import Hooks + + +def load(tmp_path, **sessions): + d = tmp_path / "macarchy-claude" + d.mkdir(exist_ok=True) + for name, pct in sessions.items(): + (d / name).write_text(f"{pct}\n") + reg = Registry(); host = ModuleHost(EventLoop(), Hooks(), reg) + host.load(ModuleSpec("claude", "modules/claude/touchbar.py", 25)) + inst = host.modules["claude"]; type(inst).DIR = str(d) + return reg, host, inst, d + + +def button(reg, host, inst): + b = reg.factory("claude.context")(host.apis["claude"]) + inst.refresh() + return b + + +def test_percentage_and_tint_track_the_fullest_session(tmp_path): + reg, host, inst, d = load(tmp_path, s1=42) + b = button(reg, host, inst) + assert b.text == "42 %" and b.tint == Theme.ACCENT_GREEN and b.badge is None + + (d / "s1").write_text("70"); inst.refresh() + assert b.text == "70 %" and b.tint == Theme.ACCENT_ORANGE + + (d / "s1").write_text("91"); inst.refresh() + assert b.text == "91 %" and b.tint == Theme.ACCENT_RED + + +def test_a_fleet_shows_the_worst_one_and_counts_the_rest(tmp_path): + reg, host, inst, d = load(tmp_path, s1=12, s2=88, s3=40) + b = button(reg, host, inst) + assert b.text == "88 %" and b.badge == "3" + + +def test_dead_sessions_are_swept_and_the_button_goes_quiet(tmp_path): + reg, host, inst, d = load(tmp_path, s1=55) + b = button(reg, host, inst) + assert b.text == "55 %" + + stale = time.time() - 120 + os.utime(d / "s1", (stale, stale)) + inst.refresh() + assert not (d / "s1").exists() + assert b.text is None and b.tint is None and b.badge is None + + +def test_unreadable_files_are_ignored_not_fatal(tmp_path): + reg, host, inst, d = load(tmp_path, s1=30) + (d / "s2").write_text("not a number") + b = button(reg, host, inst) + assert b.text == "30 %" and b.badge is None + + +def test_no_runtime_dir_means_no_reading(tmp_path, monkeypatch): + reg, host, inst, d = load(tmp_path, s1=30) + type(inst).DIR = None + monkeypatch.delenv("XDG_RUNTIME_DIR", raising=False) + assert inst.state() == (None, 0)