Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<session id>` 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`:
Expand Down
2 changes: 2 additions & 0 deletions modules/claude/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{ "schemaVersion": 1, "id": "claude", "name": "Claude Code", "kinds": ["touchbar-module"],
"entryPoints": { "touchbarModule": "touchbar.py" }, "touchbarModule": { "apiVersion": 1, "order": 25 } }
76 changes: 76 additions & 0 deletions modules/claude/touchbar.py
Original file line number Diff line number Diff line change
@@ -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/<session id>` 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
68 changes: 68 additions & 0 deletions tests/test_module_claude.py
Original file line number Diff line number Diff line change
@@ -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)
Loading