Working with Agents
luvus treats your AI agents as first-class citizens of the terminal: it knows which panes run agents, what state each is in, and how to bring a conversation back after a restart, all without wrapping or modifying the agents themselves.
The AGENTS sidebar
Section titled “The AGENTS sidebar”Every pane running a recognized agent (Claude Code, Copilot, Codex, opencode, Kimi, Grok, Hermes CLI, Letta Code, Muse Code, Pi, Oh My Pi (OMP), Cursor, Gemini, Qwen, Kilo Code, Kiro, Aider, Amp, Droid, Devin, or fx) appears in the sidebar with a live state:
| State | Meaning | How it’s detected |
|---|---|---|
| 🔴 blocked | waiting on you: approve/deny is on screen | a permission prompt is visible |
| 🟠 working | the agent is generating | a spinner or “esc to interrupt” hint on screen |
| 🟢 done | finished while unfocused | sustained quiet after working |
| idle | quiet, nothing pending | no recent activity |
A working agent shows a filled amber status dot, so you can tell at a glance which agents are still busy without continuous UI animation. When one finishes or blocks, luvus can play a Retro, Soft, or Pulse cue. Sounds are optional and off by default, turn on the events you want in Settings → General.
Click a row to jump to that agent’s pane from anywhere, across workspaces. For
keyboard navigation, press Ctrl+Space a to focus AGENTS, move with arrows or
j / k, and press Enter to open the selected live or resumable agent. Press
a for the selected row’s right-click actions, f to switch All / Active, s
to switch all workspaces / this workspace, and Esc or q to return input to
the pane. Choose Hide Path in any agent row’s menu for a compact one-row
list, then Show Path to restore the workspace and session detail rows. The
choice persists across restarts.
States are debounced: an agent that pauses mid-turn (thinking, tool calls, API latency) holds at working instead of flickering. You get one clean working → done per turn, not a dozen flaps.
Working needs proof. An agent counts as working only when a real generating indicator is on screen, like a spinner or an interrupt hint. Output alone proves nothing. A CLI printing its whole welcome screen at launch is idle, your keystrokes echoing while you compose a prompt are idle, a scrolling log is idle. That way you never get a false state or a false completion chime.
The All / Active toggle in the header switches between the full resumable history and live agents only. A new configuration starts with All, then luvus remembers whichever view you select across restarts.
When Show agent session title is on, the second line of each agent row
prefers the live OSC title, then a title a module published with
ui.agent_title.push. If neither is set, live rows keep workspace · =pane
and resumable rows keep the project folder. Luvus pane aliases (=name) are
not used as titles. Core Luvus does not parse an agent’s native session store
for this line; a module supplies names such as Pi’s session_info.name.
Usage in Mission Control
Section titled “Usage in Mission Control”Open Mission Control with Ctrl+Space m, from the desktop switcher, or with
luvus mission open [<workspace>]. Automation clients can call the same
workspace-scoped operation through UHP as mission.open. To read Mission
Control data without opening or focusing the tab, use mission.snapshot; use
mission.refresh only when fresh native usage counters are needed.
Mission Control reads the structured usage counters that each agent already stores for its own session. It shows input, output, and cache tokens, context usage when the agent records its context limit, and cost when the agent records one or Luvus has pricing for the model.
Click a session row to select it and update the Selected Agent panel. Use
Enter to open its live pane or resume the selected historical session.
| Native usage reader | Data source |
|---|---|
| Claude Code | project transcript |
| Codex | rollout token counters |
| GitHub Copilot CLI | session shutdown metrics |
| opencode | V1 selected-session integration events; V2 usage is unavailable |
| Kimi | session status records |
| Grok | completed-turn usage records |
| Pi | assistant message usage |
| Oh My Pi (OMP) | assistant message usage |
| Gemini and Qwen | project chat records |
| fx | session usage snapshot |
Aider, OpenCode V2, Kilo Code, Kiro, Cursor, Amp, Droid, Muse, Devin, Letta Code, and
manifest-defined agents still receive live identity and state detection,
but show — for usage until they expose a stable per-session counter or report
one through an integration. Luvus never estimates tokens from transcript text.
Usage parsing runs off the render path only when Mission Control becomes active, its scope changes, or you choose Refresh. A hidden dashboard schedules no usage work. Unchanged sessions are cached by agent, session ID, and source modification time. JSONL readers cap individual records, so an unusually large tool result cannot cause an equally large temporary allocation.
How luvus knows which agent a pane is running
Section titled “How luvus knows which agent a pane is running”An agent is a program, so luvus asks the operating system rather than guessing
from what a pane happens to be showing. It scans the processes running under
each pane and matches the program name. A pane running claude is Claude Code;
a pane that merely prints the word “claude” is a shell.
That distinction matters more than it sounds. Names like amp, cursor,
droid, grok, muse, and pi are ordinary English words, so reading them off the
screen turns “for example” into Amp and “cursor is out of bounds” into Cursor.
Your sidebar fills with agents that were never running.
The scan covers every pane at once, a couple of times a second, off the render path, so it costs nothing you can feel.
Where luvus cannot see processes, it falls back to reading text, ranked by how deliberate that text is: the command the pane was started with, then the window title the agent sets for itself, then its output. Names that double as ordinary words are only ever believed from the first two, never from output. This fallback covers Windows and remote panes, so detection degrades there, it does not break.
Sound alerts
Section titled “Sound alerts”luvus can play a cue when an agent finishes a working stretch or blocks on a prompt. Choose Retro, Soft, or Pulse in Settings → General. Each style has a positive done cue and a distinct blocked cue, with separate test buttons so you can hear both first. Event sounds remain off by default. A blocked cue rings at most once until you’ve actually looked at the pane, so a repeating prompt can’t spam you.
Session resume: the flagship
Section titled “Session resume: the flagship”luvus reopens each agent’s own conversation after a restart. For agents with a stable native store, it discovers the session by pane folder with no setup. Integrations provide exact ownership where directory matching alone is not sufficient, and Luvus runs the agent’s own resume command:
| Agent | Resumes via |
|---|---|
| Claude Code | its project transcript store |
| GitHub Copilot CLI | its session-state store |
| Codex | its rollout files |
| Antigravity CLI | its workspace conversation cache (~/.gemini/antigravity-cli/cache/last_conversations.json) |
| opencode | legacy session storage, plus exact live ownership from the optional integration |
| Kimi | its session index (~/.kimi-code/session_index.jsonl) |
| Grok | its session directory (~/.grok/sessions) |
| Hermes CLI | its exact session report from the optional integration |
| Letta Code | its exact conversation report from the optional integration, resumed with letta --conversation <id> |
| Pi | its session store (~/.pi/agent/sessions) |
| Oh My Pi (OMP) | its profile-, XDG-, and override-aware native session store |
| Muse Code | its XDG data store ($XDG_DATA_HOME/muse/sessions) |
| Gemini | its project chat store (~/.gemini/tmp) |
| Qwen | its project chat store (~/.qwen/tmp) |
| fx | its session store (~/.fx/sessions) |
| Cursor | resume command (when the session id is known) |
| Kilo Code | kilo --session <id> (when the exact session id is known) |
| Devin | devin --resume <id> (when the exact session id is known) |
OpenCode V2 now runs under the canonical opencode executable; the former
opencode2 preview name is accepted as an alias. The optional integration
selects the installed generation and uses V2’s CLI-only plugin contract when
appropriate. It reports and releases the exact root session selected in this
pane, while Luvus leaves the shared service and its live SQLite database alone.
Exact-ID resume uses opencode --session <id>, and scheduled work is offered
only for explicit Full access through opencode run --auto.
You’ll also see recent sessions listed at the bottom of the AGENTS sidebar (toggle All): click one to reopen it into a new pane, even sessions from before you installed luvus. The ✕ hides an entry from the list. It never touches the agent’s stored session.
Fork a session into a new pane
Section titled “Fork a session into a new pane”Sometimes you want to try a different direction without losing where you are. Fork to New Pane branches the agent conversation in the current pane into a brand new pane beside it. The fork inherits the entire context of the original session, but under a new session id, so the two run independently: keep the original going in the left pane and explore the branch on the right, with no crossed wires.
Two ways to trigger it:
- Right-click the pane and choose Fork to New Pane.
- Press
Ctrl+Space fto fork the focused pane. - Run
luvus agent fork <target>from the CLI. Add--name <alias>to name the new fork or--no-focusto leave the current view unchanged.
The new pane opens straight into the agent, already caught up on the whole conversation, exactly as if you had resumed it, except the original session is untouched and the two diverge from here.
Forking uses the agent’s own fork command, so it is available for agents that support it natively:
| Agent | Forks via |
|---|---|
| Claude Code | claude --resume <id> --fork-session |
| Grok Build | grok --resume <id> --fork-session |
| Codex | codex fork <id> |
| Kilo Code | kilo --session <id> --fork |
| Pi | pi --fork <id> |
| Oh My Pi (OMP) | omp --fork <id> |
The TUI action only appears for supported agents and otherwise remains a no-op. The CLI reports a structured error instead, so automation never mistakes an unsupported or unresolved fork for success.
Codex forks require the exact session identity reported by its integration or
recorded when Luvus resumes a session. Luvus never guesses the newest Codex
rollout in a shared folder, because that could fork another pane’s active
conversation. Install or refresh the Codex hook with
luvus integration install codex; its SessionStart and prompt hooks bind new
and resumed Codex panes to their exact rollout.
Kilo Code also requires an exact session ID for resume or fork. Luvus detects
the kilo and kilocode executables natively, but it does not scan Kilo’s
current database or guess which session belongs to a pane.
Devin has the same boundary, without fork. Luvus does not open Devin’s private
session database, so its sessions are never listed as resumable and
luvus agent resume <id> cannot find them. A pane resumes on restore only from
an exact binding reported to Luvus (luvus pane report --agent devin --session <id>) and persisted, which restores as devin --resume <id>.
Muse Code supports native detection and resume, but not Fork to New Pane.
Its /fork command exists only inside the active TUI, and its CLI refuses to
resume the same live session in a sibling pane.
Hermes CLI has the same external boundary. Luvus can resume a session whose
exact identity was reported and persisted by its integration, but Hermes
exposes /branch and /fork only inside the active TUI, so it is not listed as
a native pane-fork target.
Letta Code reports its exact selected conversation through an optional quiet
SessionStart hook. Luvus persists that pane binding and resumes it with
letta --conversation <id>, but does not inspect Letta memory, conversations,
credentials, or cloud state. Letta does not currently expose a reviewed
external conversation-fork command, so Fork to New Pane stays unavailable.
Precise events: the integration hook
Section titled “Precise events: the integration hook”Screen-based detection needs no setup and works for everything. The optional hook adds precision: the agent itself reports its exact session id and, where its hook contract supports them safely, lifecycle events into luvus:
luvus integration install claude # or: copilot · codex · antigravity · letta · opencode · kimi · grok · hermes · ompor toggle it in Settings → Integrations. What it does per agent:
- claude / copilot / codex: registers a small session-start hook script in the agent’s own settings file.
- antigravity: adds one Luvus-owned
PreInvocationentry to~/.gemini/config/hooks.json. It reports only the exact conversation id foragy --conversation <id>restore; screen detection continues to own agent state, and every other named Antigravity hook is preserved. - letta: adds one quiet
SessionStartentry to~/.letta/settings.jsonand one small platform-specific hook script. It reports only the exact conversation ID for restart resume. Detection and state remain native and work without the integration. - opencode: installs a TUI-local plugin and registers it in the effective
tui.json,tui.jsonc, orOPENCODE_TUI_CONFIGfile without removing comments or unrelated plugins. It reports only the root session selected in that pane and sends cumulative structured usage directly to the inherited owner-local Luvus endpoint. Noluvusprocess is spawned per update and no agent database is opened. - kimi: adds a
[[hooks]]entry to~/.kimi-code/config.toml, edited in place so your API keys, comments, and own hooks are left untouched. - grok: installs its own hook file without editing Grok’s authentication configuration.
- hermes: installs an opt-in plugin under the active
HERMES_HOME. It reports an exact session id once per session, which Luvus persists for restart resume. Luvus preserves unrelated YAML settings and removes only its own plugin entry on uninstall. Without the integration, detection still works but historical Hermes sessions are not scanned or guessed. - omp: installs one managed TypeScript extension in OMP’s active agent directory. It reports exact session identity and authoritative idle/working/blocked/done transitions while preserving Luvus’s native process/screen detection as the zero-setup fallback.
Uninstalling (luvus integration uninstall <agent>, or the same Settings
toggle) surgically removes only luvus’s hook entry. Your other settings
and the agent installation itself are never touched.
Custom detection rules
Section titled “Custom detection rules”Detection ships with built-in defaults that already cover the known agents. If
you run an agent luvus does not recognize, or you want to tune how an existing
one reads, add your own TOML files in ~/.luvus/manifests/. Name each file
after its agent (myagent.toml) to keep things findable. Every *.toml in that
folder merges on top of the built-ins, and the first time luvus runs it drops an
annotated example.toml.txt there to copy from.
A manifest controls two separate things:
[identity]decides which agent a pane is running[[rule]]decides what state that agent is in
Teaching luvus a new agent
Section titled “Teaching luvus a new agent”[identity] lists the names that identify the agent, matched as whole words.
The two lists differ in how far each name is trusted:
| List | Trusted in |
|---|---|
distinct |
anywhere, including whatever the pane prints |
ambiguous |
only the command that started the pane, or the agent’s own window title |
Put a name in ambiguous when it is also an ordinary word, so a pane that
happens to print it is not mistaken for the agent. Add replace = true to drop
luvus’s built-in names instead of adding to them, which is how you remove a
default you disagree with.
agent = "cursor"
[identity]distinct = ["cursor-agent"] # the CLI binary: unmistakableambiguous = ["cursor"] # also a normal English wordNaming an agent luvus does not ship teaches it a new one, no rebuild and no waiting for a release:
agent = "myagent"
[identity]distinct = ["myagent-cli"]This creates detection-only support. It does not grant native session-store access, resume, fork, usage, integration, or skill installation. Contributors adding those trusted built-in capabilities should use the modular Adding Agent Support guide.
State rules
Section titled “State rules”A rule matches text on the pane’s screen (or its window title) and, when it holds, sets the agent’s state. Rules carry a priority, so the highest-priority match wins and a rule of yours can override a built-in one for the same agent.
# Which agent this file applies to. "generic" (the default) means every agent,# and is only valid for rules: identity always needs a specific agent.agent = "myagent"
[[rule]]state = "working" # working | blocked | idlepriority = 200region = "screen" # screen (recent output, default) or title (window title)any = ["esc to interrupt", "esc to cancel"]
[[rule]]state = "blocked"priority = 300region = "screen"all = ["do you want to proceed"]not = ["cancelled"]Each rule holds when all of the conditions you list are true:
| Field | Holds when |
|---|---|
any |
any listed substring is present (case insensitive) |
all |
every listed substring is present |
not |
none of the listed substrings are present |
spinner |
a running braille spinner glyph is visible on a line |
Priorities follow the built-ins: blocked prompts sit around 300, working hints around 100 to 200. Give a rule a higher number than the built-in you want it to beat. A file that fails to parse is skipped with a warning, so a typo never takes detection down.
Agents can drive luvus
Section titled “Agents can drive luvus”Inside every pane, luvus injects $LUVUS_PANE_ID and $LUVUS_SOCKET_PATH, so
the agent in the pane can call the same API you do:
luvus pane status # my own statusluvus wait output 7 --match "tests passed" --timeout 300luvus wait agent-status 7 --status doneSee Scripting luvus for the patterns, and Multi-Agent Orchestration for running a whole team.