Skip to content

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.

Every pane running a recognized agent (Claude Code, Copilot, Codex, opencode, Kimi, Grok, Pi, Cursor, Gemini, Aider, Amp, Droid) appears in the sidebar with a live state:

StateMeaningHow it’s detected
🔴 blockedwaiting on you: approve/deny is on screena permission prompt is visible
🟠 workingthe agent is generatinga spinner or “esc to interrupt” hint on screen
🟢 donefinished while unfocusedsustained quiet after working
idlequiet, nothing pendingno recent activity

A working agent shows a rotating-circle spinner in its status dot, so you can tell at a glance which agents are still busy. When one finishes or blocks, luvus can play a short retro chime. The sound is optional and off by default, turn it on in Settings → General.

Click a row to jump to that agent’s pane from anywhere, across workspaces.

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 live agents only (default) and the full resumable history.

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 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.

luvus can play a short retro chime when an agent finishes a working stretch or blocks on a permission prompt. Both sounds are off by default because sound is a matter of taste. Turn them on in Settings → General, and use the Test sound row to hear the chime before you commit to it. The blocked chime rings at most once until you’ve actually looked at the pane, so a repeating prompt can’t spam you.

luvus reopens each agent’s own conversation after a restart, with zero configuration. It discovers the agent’s most recent session from the agent’s native on-disk store, keyed to the pane’s folder, and runs the right resume command for you:

AgentResumes via
Claude Codeits project transcript store
GitHub Copilot CLIits session-state store
Codexits rollout files
opencodeits session storage
Kimiits session index (~/.kimi-code/session_index.jsonl)
Grokits session directory (~/.grok/sessions)
Piits session store (~/.pi/agent/sessions)
Cursorresume command (when the session id is known)

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.

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 f to fork the focused pane.
  • Run luvus agent fork <target> from the CLI. Add --name <alias> to name the new fork or --no-focus to 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:

AgentForks via
Claude Codeclaude --resume <id> --fork-session
Codexcodex fork <id>
Pipi --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 hook with luvus integration install codex; its SessionStart hook binds both new and resumed Codex panes to their exact rollout.

Screen-based detection needs no setup and works for everything. The optional hook adds precision: the agent itself reports its exact session id and lifecycle events (permission prompt raised, turn ended) into luvus:

Terminal window
luvus integration install claude # or: copilot · codex · opencode · kimi

or 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.
  • opencode: installs a tiny plugin file.
  • kimi: adds a [[hooks]] entry to ~/.kimi-code/config.toml, edited in place so your API keys, comments, and own hooks are left untouched.

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.

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

[identity] lists the names that identify the agent, matched as whole words. The two lists differ in how far each name is trusted:

ListTrusted in
distinctanywhere, including whatever the pane prints
ambiguousonly 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.

~/.luvus/manifests/cursor.toml
agent = "cursor"
[identity]
distinct = ["cursor-agent"] # the CLI binary: unmistakable
ambiguous = ["cursor"] # also a normal English word

Naming an agent luvus does not ship teaches it a new one, no rebuild and no waiting for a release:

~/.luvus/manifests/myagent.toml
agent = "myagent"
[identity]
distinct = ["myagent-cli"]

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.

~/.luvus/manifests/myagent.toml
# 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 | idle
priority = 200
region = "screen" # screen (recent output, default) or title (window title)
any = ["esc to interrupt", "esc to cancel"]
[[rule]]
state = "blocked"
priority = 300
region = "screen"
all = ["do you want to proceed"]
not = ["cancelled"]

Each rule holds when all of the conditions you list are true:

FieldHolds when
anyany listed substring is present (case insensitive)
allevery listed substring is present
notnone of the listed substrings are present
spinnera 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.

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:

Terminal window
luvus pane status # my own status
luvus wait output 7 --match "tests passed" --timeout 300
luvus wait agent-status 7 --status done

See Scripting luvus for the patterns, and Multi-Agent Orchestration for running a whole team.