Watch every luvus session, not one of them - #2
johnkattenhorn wants to merge 2 commits into
Conversation
luvus scopes its orchestration ledger, leases and task board to the session, so the working practice it pushes you towards is one session per project. A widget that watches a single session then answers "is anything waiting for me?" for one project and stays silent about the rest, which is worse than not asking: an honest zero reads as nothing waiting. `session` now takes `*` for every running session, or a comma-separated list for the ones you name; empty still means the default session and only that, so an existing installation does not change. The bar keeps one number across the fleet — blocked, then working, then a count — and the panel grows a heading per session, ordered the way the rows are, so a session with a blocked agent comes first. A session with nothing in it still says so rather than disappearing. Service stays single-session and there is now one per session. Its process lifecycle is the hard-won part of this plugin, and teaching one Process to multiplex several subscriptions would put all of it back in play; an Instantiator and a merge in Model cost nothing by comparison. The two factors multiply with monitors, so MAX_SESSIONS caps the first well below luvus's 64-subscriber limit. Two things about luvus itself are now encoded rather than assumed. The default session is addressed by passing no --session flag at all: `--session default` is accepted, answers from somewhere else, and reported zero agents against a default session holding one. And a session name reaches argv, so it is guarded exactly like a pane id, since luvus reads a leading-dash name as one of its own flags. Jumping is per-session on both halves: the pane focus goes through that session's own Service, because pane 2 exists in all of them, and `windowClassTemplate` puts the session name into the window class so one terminal per project can be raised. The name is regex-escaped on the way in, since Hyprland's class: is a pattern. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GBh3oDkHe62bNbDPbDg42R
Hyprland 0.56 takes a Lua expression where hyprctl dispatch used to take
a dispatcher and its arguments, so `focuswindow class:X` is a syntax
error there:
error: [string "return hl.dispatch(focuswindow class:org.o..."]:1:
')' expected near 'class'
The window half of every jump has therefore done nothing on 0.56 —
silently, because the dispatch is fire-and-forget and the pane focus
still works, which makes it look like a compositor ignoring a raise
rather than a command that never parsed. Found while testing something
else; nobody would have reported it as more than "it does not raise the
window".
Both forms are now issued, newest first. No release understands both, so
exactly one lands and the other fails on its own stderr. Omarchy's own
omarchy-launch-or-focus chains the same pair with `||`; this uses two
argv vectors instead, because a shell string carrying a user setting is
the injection sink this plugin already refuses elsewhere.
The class now lands inside a Lua string literal, so it is escaped as one:
backslashes doubled, since a class is a regex and needs them; quotes
escaped; control characters dropped. Tests cover a class that tries to
close the literal and run os.execute.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBh3oDkHe62bNbDPbDg42R
|
Pushed
The window half of every jump therefore does nothing on 0.56 — silently, because the dispatch is fire-and-forget and the pane focus still works. It reads as Hyprland ignoring a raise rather than a command that never parsed. I only found it because I was testing something unrelated on this machine. Omarchy's own hyprctl dispatch "hl.dsp.focus({ window = \"address:$ADDR\" })" || hyprctl dispatch focuswindow "address:$ADDR"The commit issues both forms, newest first, rather than chaining them in a shell — no release understands both, so exactly one lands and the other fails on its own stderr, and Verified on Hyprland 0.56.2: the Lua form parses and a missing class is a harmless Worth cherry-picking on its own if you would rather not take the multi-session change — it is independent of everything else here. |
Closes #1.
Option 2 from the issue: one widget watches every session, the bar keeps one number, and the panel groups its rows by session. Option 1's shape is in here too —
sessiontakes a comma-separated list — but*is the one that makes a per-project session layout usable.An existing installation does not change:
sessionleft empty still means the default session and only that.What it looks like
{ "id": "riclib.luvus", "session": "*", "windowClassTemplate": "org.omarchy.luvus.{session}" }The bar still answers one question across the whole fleet — blocked, then working, then a count. The panel grows a heading per session, ordered the way the rows are, so a session holding a blocked agent comes first. A session with nothing in it keeps its heading and says "no agents", because one that quietly disappears reads as one with nothing waiting in it.
The shape
Servicestays single-session, and there is now one per session. Its process lifecycle —_closing, the backoff, the declarativespawnWatch— is the part of this plugin that took the most work to get right, and teaching oneProcessto multiplex several subscriptions would put all of it back in play.BarSlotowns anInstantiatorover the resolved session list, collects their states on a zero-interval timer, and merges them inModel.mergeStates. A newSessions.qmlreadsluvus session list --jsonon the same 30s cadence as the agent poll, so a session started later is picked up without a restart.The cost multiplies with monitors — sessions × monitors subscriptions — so
Model.MAX_SESSIONSis 16, which keeps a two-monitor machine at half of luvus's 64-subscriber cap. Measured here with five sessions on two monitors: tenluvus eventsprocesses and nothing else at rest.Two things about luvus that are now encoded rather than assumed
The default session is addressed by passing no
--sessionflag at all.luvus session list --jsoncalls itdefaultand marks it"default": true, but that name is a label, not an argument. On 0.14.1, against a default session holding one agent,--session default agent listanswered with zero agents and a revision eighteen hundred behind — a different server.Model.sessionKeymaps that entry to"", and there's a test for it.A session name reaches argv, so it is guarded exactly like a pane id. luvus parses
--session -xas one of its own global flags rather than rejecting it, soModel.sessionNamerefuses anything that is not plainly a name and the session is dropped instead of being watched under a name that means something else.Jumping
Both halves are per-session now. The pane focus goes through that session's own
Service, because pane 2 exists in all of them andluvus pane focus 2addresses whichever server the argv names. For the window,windowClassTemplateputs the session into the class —org.omarchy.luvus.{session}against terminals launched withomarchy-launch-or-focus-tui --app-id=org.omarchy.luvus.<session>. Hyprland'sclass:is a regular expression, so the session name is escaped when it is substituted.focusWindowClassis untouched and still right for a single window.Checks
node --test tests/model.test.js— 53 pass, 26 of them new: session parsing, the default-session key, the argv guard, resolution of""/"*"/ a list, merging and its counts, one dead session among live ones, group ordering, the cap, and the class template.omarchy plugin validate .clean, thetextFormatsweep clean, no secrets in the tree.agent listper session, sampled in the same second.One testing note worth having in
CLAUDE.md, which it now does: a shell running inside a luvus pane exportsLUVUS_SOCKET_PATHandLUVUS_SESSION, and the CLI honours them — so a hand-check typed into that terminal answers for that session and can make the widget look wrong about the default one. Quickshell has none of those variables. Unset them before comparing.Happy to split this, change the setting's spelling, or drop the window template if you'd rather keep that out of the plugin.