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

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.

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.

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.

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:

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.

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:

Terminal window
luvus integration install claude # or: copilot · codex · antigravity · letta · opencode · kimi · grok · hermes · omp

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.
  • antigravity: adds one Luvus-owned PreInvocation entry to ~/.gemini/config/hooks.json. It reports only the exact conversation id for agy --conversation <id> restore; screen detection continues to own agent state, and every other named Antigravity hook is preserved.
  • letta: adds one quiet SessionStart entry to ~/.letta/settings.json and 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, or OPENCODE_TUI_CONFIG file 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. No luvus process 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.

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:

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.

~/.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"]

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.

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:

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.

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.