Rove

Troubleshooting

User-facing symptom → cause → fix, for the questions that keep coming back. One section per symptom; keep entries short and command-exact.

rove exits with "no Bun was found" (or env: bun: No such file or directory)

The Rove CLI runs on the Bun runtime. The published rove and rove bins are node launchers that re-exec through Bun, so an npm install -g or npx on a machine without Bun still works; the first launch offers to install Bun for you. You land on this error when that offer could not be made (no TTY, CI=true, or ROVE_NO_BUN_BOOTSTRAP=1) or was declined.

Fix it with any of:

curl -fsSL https://bun.sh/install | bash        # macOS / Linux / WSL
powershell -c "irm bun.sh/install.ps1 | iex"    # Windows
npm install -g bun                              # any platform

Bun is discovered on PATH, in $BUN_INSTALL/bin, in ~/.bun/bin, and in a bun npm package installed beside Rove. A Bun anywhere else needs ROVE_BUN=/path/to/bun. Export it from your shell profile so daemon restarts see it too. The bare env: bun: No such file or directory message comes from an install made before the launcher shipped, whose bin needed Bun on PATH. rove update replaces it.

rove exits with "this machine's Bun is too old"

Rove's terminals are built on Bun's PTY API, which arrived in Bun 1.3.11 (engines.bun in the published package). An older Bun ignores the option silently, so Rove would start, look healthy, and open every terminal and engine tab empty — Rove refuses to start instead.

Nothing else catches this for you: bun install ignores engines outright and npm only honours it under engine-strict, so the install itself always succeeds. Upgrade Bun with whichever manager owns it:

bun upgrade                 # Bun installed itself (~/.bun)
brew upgrade bun            # Homebrew
npm install -g bun@latest   # npm-managed Bun

A newer Bun elsewhere on the machine is enough — Rove skips a too-old candidate and uses the next one it finds, or you can name it with ROVE_BUN=/path/to/bun. ROVE_SKIP_BUN_CHECK=1 runs on the old Bun anyway; it is unsupported and the terminals stay blank, so use it only to reach rove doctor or rove update.

Windows opens Rove, but engine and terminal tabs never start

Windows needs three separate runtimes:

  • Bun ≥ 1.3.11 runs the Rove CLI and TUI.
  • Node.js runs the Windows Hosted PTY process. A Bun-only global install does not install Node for you.
  • Git for Windows, including its Git Bash, supplies the POSIX shell used by every engine and terminal launch — and by rove update, whose install script is POSIX shell. Rove deliberately does not use the WSL bash.exe, because it cannot address the Windows worktree correctly.

Start with:

rove doctor
where.exe node
where.exe git

Install Node.js if Doctor says the Windows PTY host cannot find it. If the engine launch names C:\Program Files\Git\bin\bash.exe, install Git for Windows or set SHELL to the full, existing Windows path of another compatible Bash. An inherited MSYS value such as /usr/bin/bash is not a spawnable Windows executable path.

Remote-project password auth is not available on Windows; use --key or ssh-agent. Only macOS has the keychain integration used by --password.

Windows: rove update fails with EBUSY on opentui.dll

npm error code EBUSY
npm error syscall copyfile
npm error path ...\node_modules\@sma1lboy\rove\node_modules\@opentui\core-win32-x64\opentui.dll
npm error dest ...\node_modules\@sma1lboy\.rove-xsdjqHxL\node_modules\@opentui\core-win32-x64\opentui.dll

npm moves the old package to a sibling .rove-<hash> directory before it unpacks the new one, and deletes that copy afterwards. Windows refuses to delete a DLL a running process has mapped — opentui.dll in the TUI and the daemon, conpty.node in the PTY host — and a running Rove is the normal state during an update, so the delete fails, npm swallows it, and the directory stays behind with the mapped files inside. The hash is derived from the path, so the next update targets the same directory, finds it occupied, falls back to a file-by-file copy, and dies copying onto the DLL that is still mapped.

Current update scripts sweep those leftovers before npm runs (deleting what can be deleted, and renaming the rest out of npm's way — Windows allows renaming a mapped file), so simply running rove update again on a current build is the fix. If it still reports EBUSY, something is holding files in the old install in a way the sweep cannot get past — almost always a running Rove. Quit the TUI, run rove daemon stop, and retry; engine sessions live in the PTY host and survive both. A .rove-<hash>.stale-<n> directory next to the package is that leftover, parked; it is removed by a later update once the old build has exited.

The Windows screen looks scrambled, and stays that way

Rows overlapping each other, fragments of a pane you already left, patches of the terminal's background image showing through. Rove's renderer draws each frame by writing only the cells that CHANGED since the last one, and a terminal that reflowed its own grid — on a resize, a font-size change, a window split — has moved cells the renderer still believes it owns. Nothing corrected that, so the leftovers survived every later frame and the only cure was quitting.

Rove now repaints every cell after a resize and whenever the window regains focus, so the ordinary cases clear themselves. For anything that slips past — a terminal that reflowed without changing the cell grid, another program writing over Rove — press ctrl+a r to erase and repaint. Nothing but the screen changes: no task, tab, or engine state moves.

Rove also starts opaque on Windows. Windows Terminal ships acrylic and background images on by default, and in transparent mode Rove paints no opaque cell of its own, so anything a frame does not cover shows the wallpaper rather than the previous frame. Set transparentBackground to true in state.json, or turn it on in Settings → General, if you want it anyway; a value you have already chosen is left alone.

Windows: engineAlive and liveVendor come back as unknown

"Is an engine running in this tab" is answered from the process tree, and on Windows that answer needs two things macOS and Linux do not:

  • Windows PowerShell (%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe) for the process table. The ps on a Git for Windows PATH is a Cygwin build that rejects -A, so Rove reads Get-CimInstance Win32_Process instead.
  • node-pty's conpty_console_list addon, which ships prebuilt with Rove's node-pty dependency. An npm-installed engine launches through a .cmd shim whose cmd.exe exits immediately, so the process table alone cannot link a tab's shell to its engine — the tab's ConPTY console can, and that addon is what reads it.

When either is missing the walk reports unknown rather than guessing. rove api collect and get-task leave engineAlive/liveVendor unset and running untouched; rove api send refuses with ENGINE_PROBE_FAILED (not ENGINE_NOT_RUNNING — that one is a positive "this tab is a bare shell"). Check that both exist, then retry:

where.exe powershell
rove api inspect --task-id <id>

The Windows walk costs roughly half a second per probe, most of it PowerShell startup, where the POSIX ps costs ~20ms. That is why the sidebar's live engine badge can lag a second or so behind an engine you just quit.

The daemon, sidebar, or a terminal session looks wedged

Run the read-only diagnosis first:

rove doctor
rove api inspect --task-id <task-id> --pretty

inspect does not start missing services. It joins daemon activity, Hosted PTY sessions, persisted tab snapshots, and durable abnormal-exit records, so it is the best first attachment for a badge, label, or engine-crash report. rove doctor --report writes a bundle containing the same diagnosis plus recent logs and environment details. It lands at ~/.rove/rove-doctor-report.txt — beside the logs it quotes, and the same path wherever you ran the command from, so it never drops an untracked file into the repo you were debugging.

The raw logs live under the active Rove home (normally your OS home):

PathContains
~/.rove/daemon.logdaemon startup, crashes, RPC failures, task-deletion audit
~/.rove/pty.logHosted PTY startup and session-host failures
~/.rove/client.logTUI/pane connection, disconnect, and reconnect diagnostics; [pane-crash] render errors, tagged with the region (sidebar, workspace, files, page) that showed the error card

Rove is spawning more processes than it should

A terminal tab title that flickers between your shell's name and git, a fan that will not settle, or an editor that feels a beat behind: all three can mean Rove is forking child processes far more often than its polls intend. Two things Rove does on a timer legitimately fork — the engine walk that answers "which engine is live in this tab" runs ps every 2 seconds, and the worktree-changes chip runs git status per worktree, though only when no daemon is connected (a connected daemon polls once, centrally, and pushes the counts). Anything beyond those two is a bug worth reporting.

ps cannot tell you which is which: a git that lives a few milliseconds is caught mid-exec and macOS reports its arguments as (git). git's own trace2 sees every invocation on the machine but records no parent, so on a machine running several agents it cannot say who asked.

Set ROVE_SPAWN_PROFILE to a file path and Rove logs one JSON line per child it spawns, naming the code that wanted it:

ROVE_SPAWN_PROFILE=/tmp/rove-spawns.log rove

Leave it running for 30 seconds of the behaviour you are chasing, then count by site:

$ jq -r .site /tmp/rove-spawns.log | sort | uniq -c | sort -rn
    14 engine.foregroundWalk
     6 sidebar.gitHead
     2 sidebar.worktreeChanges

Divide by your window to get a rate. engine.foregroundWalk at roughly one every 2 seconds is the design; sidebar.worktreeChanges firing steadily while a daemon is connected is not, and neither is any site in the tens per second. Each line also carries cwd, which names the worktree being polled — useful when one repo is responsible for all of it. Include the counts in a bug report.

Unset, the variable costs one boolean test per spawn and touches no disk.

Processes keep running days after their task is gone

Ending a session in Rove ends its whole subtree: the PTY host signals the child's process group, which reaches the shell, the engine, and everything they spawned. That only happens when Rove is the one doing the killing. Kill an engine from outside — kill -9, an OOM reaper, a crashed PTY host — and Rove is never told, so it never signals the group, and whatever the engine had spawned is reparented to init and runs until you reboot. A machine that has been through a few of those accumulates test runners, dev servers, and browsers burning CPU for something you closed last week.

rove doctor lists them under orphans:, with age, memory, and command:

orphans: ⚠ 3 process(es) outlived the PTY session that spawned them (97 MB RSS)
         pid 15752 (group 14297) up 04-21:22:21, 25 MB: bun test test/render

A process is listed only when its environment carries the marker the PTY host sets on every child it spawns, its parent is init, its process group has no leader left, and that group is not one the PTY host still reports as live. A healthy task never matches, and neither does anything you started outside Rove.

Killing needs rove doctor --kill-orphans, and doctor never does it on its own: a database tunnel or dev server you deliberately backgrounded from a Rove terminal, whose tab you then closed, is indistinguishable from a leak. Read the list first.

On macOS the environment of an Apple-signed system binary is unreadable without root, so those are never listed. What actually leaks — bun, node, a browser, a CLI tool — reads fine.

A probe that could not run at all is a different answer, and doctor now prints it as one:

orphans: ✗ could not read process environments — ps eww exited 127

That is the step which turns a candidate into a finding, so when it fails every candidate stays unclassified. ✓ none used to cover that case too, which made a machine nothing had looked at indistinguishable from a clean one. The realistic triggers are Linux hidepid=2 and reading across uids.

Who deleted my task?

Every task deletion is recorded in ~/.rove/daemon.log, whether it succeeds or fails:

grep task-deletion-audit ~/.rove/daemon.log

Each deletion writes a requested line when the RPC arrives, then either removed or failed. The requested line names the task, its branch and worktree path, the --force/--delete-branch flags, and who asked:

  • by=<taskId>::<tabId> — another Rove session ran rove api delete from inside that tab. This is a verified identity, not the inherited $ROVE_TASK_ID env, so an unverifiable caller is simply absent rather than misattributed.
  • spawnedBy=<taskId>::<tabId> — the deleted task's own spawner. Useful context, but it names who CREATED the task, not who deleted it.
  • client=<n> — the daemon connection id, which distinguishes concurrent callers when neither identity above is present (a TUI keypress, rove api).

A salvaged line appears between requested and removed when a forced deletion had uncommitted work to destroy. It names the git ref holding a snapshot of that work and the exact commands to recover it:

salvaged task <id> — uncommitted work saved to refs/rove/salvage/<branch>-<stamp> (<sha>).
Recover with: git -C <repo> show refs/rove/salvage/<branch>-<stamp> | ...

The same line is written for a forced worktree removal from the worktrees page (salvaged worktree <path> — …). No salvaged line means there was nothing uncommitted to save. See WORKTREES.

A removed line that also says "git deregistered the worktree but could NOT delete" means the deletion succeeded and left a directory behind: git dropped its registration but could not unlink the tree (most often an unwritable path inside it). The task is gone and there is nothing to retry — retrying is impossible, because git no longer knows that worktree. The line names the directory; delete it by hand if you want the space. See WORKTREES.

A failed line means the deletion ran only partway: the hosted session was torn down and the Inbox/activity state cleared, but the worktree directory and the task entry remain, and the task is left in deletion.phase === "error" (the sidebar row shows it). Delete it again once you have fixed whatever the reason names — a common one is a worktree directory that is no longer a git worktree, which git -C <path> rev-parse --is-inside-work-tree confirms.

(Installs upgraded from pre-0.8.189 builds may still have a ~/.rove/ directory; runtime files now live under ~/.rove, with legacy paths honoured only while a process started before the move is still alive.)

After an upgrade, both background processes can still be running old code, and rove update says so when it finishes — and so can an already-open TUI, which keeps executing the bundle it launched with. Doctor reports each version separately. A stale daemon is fixed by rove daemon restart, which never touches a live session; an attached Rove shows the amber DAEMON OUT OF DATE banner instead, and ctrl+a r there restarts the daemon and relaunches the TUI on the installed build in one confirmed step. A stale PTY host is not fixed by either — it survives daemon restarts by design, so it keeps serving its boot-time build until rove reset replaces it, which is also the remedy when the host is wedged. Reset stops both runtimes, ends every live terminal and engine session, and clears the frozen-session store, but does not touch git worktrees. Read the confirmation carefully before proceeding.

rove doctor --fix walks these remedies for you, one confirmation per fix: safe ones (a daemon restart, a skill install) run after a per-fix y/N, while anything that would kill live sessions — rove reset included — is printed for you to run yourself, never executed.

What terminal output does Rove persist after a crash?

Hosted PTY output is not always memory-only. Two bounded recovery stores live under ~/.rove/ (or the selected Rove home):

  • pty-exits.json keeps at most the newest 50 death records, with exit metadata and up to the last 40 plain-text output lines each. rove api inspect exposes these as sessionExits, newest first. Two layers: layer: "pty" is the terminal process itself (abnormal exits only), and layer: "engine" is the AI process gone from a terminal that kept running — the case where you return to a shell prompt and want to know what happened. An engine record names the engine's pid, its vendor, and the exit code from the shell's Engine exited (code N) banner; code 143 means it was killed with SIGTERM.

    To find out who killed it: Rove logs every signal it sends a terminal subtree to daemon.log as [pty-signal]. POSIX gives a dying process no way to learn its killer's pid, so attribution works by elimination — no [pty-signal] line for that session means the signal came from outside Rove (the engine's own wrapper, a provider limit, the OS, or your shell).

  • pty-sessions/ freezes each hosted session's launch metadata and bounded scrollback ring so a PTY-host crash, restart, or machine reboot can restore the old screen and respawn the launch command. An explicit tab close or task delete drops that session's frozen record; a reset asks the running host to start fresh.

These files can contain text that was visible in the embedded terminal. Treat the Rove home with the same permissions and backup policy as shell history and engine transcripts; do not describe terminal output as "never written to disk."

Rove says the daemon serves a different home

Rove refuses a daemon whose handshake reports a different state home before accepting any tasks from it. This normally means a dev:sandbox or custom-home process inherited the production socket override. The error names both homes and prevents the foreign task list from blanking or replacing the real one.

Check the overrides in the shell that started the unexpected daemon:

env | grep -E '^(ROVE|ROVE)_(HOME_DIR|DAEMON_SOCKET_PATH)='
rove doctor

Stop that daemon from the same environment, then clear the inherited overrides before starting the intended instance:

rove daemon stop
unset ROVE_DAEMON_SOCKET_PATH ROVE_DAEMON_SOCKET_PATH
unset ROVE_HOME_DIR ROVE_HOME_DIR
rove daemon restart

If you intentionally use a custom home, re-export its ROVE_HOME_DIR before the restart instead of unsetting it — and, when you are deliberately running a second instance beside your usual one, re-export the whole group of socket and pidfile overrides with it (see Runtime path overrides). Do not point two homes at one daemon socket: the server refuses a live takeover, and clients reject the wrong owner.

Rove refuses to start a second daemon on one home

The mirror of the case above: two socket paths, one state home. Rove refuses the second daemon and names the socket that already owns the home.

rove daemon: /Users/me is already served by the daemon on
/Users/me/.rove/daemon.sock (pid 61439) — refusing to start a second daemon on
one home.

This happens when a shell overrides ROVE_DAEMON_SOCKET_PATH but leaves ROVE_HOME_DIR pointing at a home another daemon is already serving. Letting both run is worse than the refusal: neither can see the other, so their task lists diverge permanently, the project-main row gets written twice, and automations.json and .config/rove/state.json are raced as well.

Either stop the incumbent, or give the new daemon its own home:

rove daemon stop                       # from the incumbent's environment
# …or, to run both:
export ROVE_HOME_DIR=/path/to/other-home

A crashed daemon's claim never blocks a restart: the check asks the recorded socket whether anything still answers there, so a dead one is just replaced.

Claude or Codex activity badges do not update

Rove installs its own merge-safe, global activity hooks when the TUI launches:

  • Claude Code definitions live in ~/.claude/settings.json.
  • Codex definitions live in ~/.codex/hooks.json. Codex requires the user to trust non-managed hooks once through /hooks; Rove writes the definition but never bypasses that approval.

Fully relaunch Rove to re-run hook installation, then approve the Rove command inside Codex's /hooks page if needed. Use rove api inspect --task-id <id> to compare hook activity with the PTY/process observation. Codex does not currently expose clean signals for every state (failure, session end, and permission waiting), so Rove's polling/PTY fallback remains part of the normal result.

The hook command itself is intentionally harmless: rove hook … always exits 0 and never starts the daemon; sessions outside tracked Rove worktrees quickly no-op. rove hook setup is deprecated and now performs cleanup only. Older Rove versions installed a Claude WorktreeCreate provider hook that could break claude --worktree; current launches remove Rove's old entry while preserving user hooks and use an observer instead.

One task's badge never moves, and its title never auto-fills

Every other task updates; one worktree stays silent — no activity badge, no auto-title, and an interrupted prompt is never offered back. That is the narrow version of the symptom above: when every task goes quiet, the hooks are the cause; when a single worktree does, its path is.

Rove reads Claude's own transcript directory, ~/.claude/projects/<encoded-worktree-path>. Claude folds every non-alphanumeric character of the path into -; before 0.8.198 Rove folded only / and .. The two names diverged for any worktree path containing a character outside /, ., -, and alphanumerics — an underscore, a space, or anything non-ASCII, whether it came from the repo name, the task slug, or your home directory. Rove then watched a directory Claude never wrote, and every signal derived from that transcript went quiet with no error: activity badge, turn detection, auto-title, and prompt rescue.

Check the version, then confirm the directory exists for the stuck worktree:

rove --version                                          # 0.8.198+ has the fix
cd <the worktree>
ls -d ~/.claude/projects/"$(pwd | sed 's/[^a-zA-Z0-9]/-/g')"

rove update, then rove daemon restart (or ctrl+a r inside a running Rove, which also relaunches the TUI). No history is lost by the upgrade: those directories are written by Claude with the correct encoding, so the corrected name finds the transcripts that were there all along, including for the sessions that ran while the badge sat still.

rove api send refuses with NO_ENGINE_TAB or ENGINE_NOT_RUNNING

Both errors are deliberate refusals, not delivery failures. Silently spawning a fresh engine here would make both sender and receiver believe the prompt was delivered while it actually landed in a duplicate session nobody is watching — so send fails loud instead.

  • NO_ENGINE_TAB: the task has live tabs, but none of them resolves as its engine tab (the engine tab died, or the tabs run something else). List what is actually alive, then address a tab explicitly:

    rove api pty-list
    rove api send --task-id <id> --tab tab-N --prompt "..."   # deliver to a live tab
    rove api send --task-id <id> --tab new --prompt "..."     # or spawn a fresh engine tab
  • ENGINE_NOT_RUNNING: the engine tab exists but its engine process has exited into a plain shell — pasting there would execute the prompt as shell commands. Spawn a fresh engine tab with --tab new as above.

A bare send (no --task-id) targets the dispatcher's tab when run from a task another Rove session spawned, and otherwise the active task — it never silently spawns an engine on a guess.

rove api update --branch fails, but the branch was renamed anyway

The call exits non-zero with git's own complaint, stamped with the path of the main checkout rather than the worktree:

git branch -m <old> <new> (cwd=/path/to/repo) exited with code 128:
fatal: no branch named '<old>'

Meanwhile the worktree is already sitting on <new>. The rename worked; only the task record's idea of the old name was stale — from a retried call whose first response was lost, a concurrent rename, or an out-of-band git branch -m in the worktree. Branch refs are shared across all worktrees of a repo, so the main-checkout path in the message is where git was run, not a branch that is missing from one place and present in another.

0.8.198 resolves the ambiguity: when git branch -m fails, the rename probes the end state, and old-name-gone plus new-name-present is treated as the requested outcome — the call succeeds and the record converges. A genuine collision (both names present) or a missing pair still fails.

On an older version, read the end state before you retry, and do not rename by hand — the branch is already correct:

git -C <worktree> branch --show-current   # already <new>? then it succeeded
rove api get-task --task-id <id>          # what the task record still believes

Upgrading and re-running update --branch converges the record.

Two daemons, or engine tabs split across hosts, after an upgrade

Rove 0.8.189 moved runtime files (sockets, pidfiles, logs) from ~/.rove to ~/.rove. A binary predating the move looks only at the legacy paths; if it cannot see the new daemon it starts a second one on the same task index, or a second PTY host that splits your engine tabs. Current versions leave symlinks at the legacy paths after binding, so mixed-version installs find the same daemon — you only hit the split if an old global install (rove from npm, an old Homebrew bin) is still being launched somewhere.

rove --version        # every entry point should report the same version
rove doctor           # reports version mismatches between the CLI and the daemon
rove daemon restart   # rebinds on the canonical ~/.rove paths

Then update or remove the stale install so both rove and rove resolve to the same current binary.

A plugin installs cleanly, but Rove never loads it

rove plugin list shows it as enabled, while its panes and actions never appear in the TUI. The two views disagree because they read different things: the CLI reads the registry file directly, so it reports what was written; the daemon is the process that actually loads plugins, and it missed the write.

Before 0.8.198 the daemon watched plugins.json with fs.watch. On macOS the FSEvents stream behind fs.watch arms asynchronously, so a registry write landing after the watcher was created but before its stream went live was dropped permanently, with no error on either side. A rove plugin install, link, or enable that raced daemon startup was therefore ignored until the next registry mutation or the next daemon restart.

rove --version         # 0.8.198+ stat-polls the registry instead
rove daemon restart    # makes the daemon re-read a write it dropped
rove plugin list       # then confirm the TUI agrees

0.8.198 takes a synchronous baseline stat before the first registry load and polls every 200ms, so no write can fall between the watcher and the load. Enabling a plugin on a current version no longer depends on when you ran it.

Copy from the embedded terminal doesn't reach my clipboard (especially over SSH)

How copy works. Rove's embedded terminal is a full-mouse TUI: it enables the terminal's mouse reporting (clicks focus panes, tabs are clickable, the wheel routes to the app). Mouse reporting hands drag-selection to Rove, so your terminal emulator's native selection no longer participates. Every mouse-enabled TUI (tmux, vim with mouse=a) makes the same trade. Rove implements its own grid selection instead: drag to select (pane-aware, works inside splits), release to copy. Delivery is dual-channel:

  1. a pipe into the platform clipboard command on the machine Rove runs on (pbcopy / wl-copy / xclip / xsel), and
  2. an OSC52 escape sequence written to the tty.

The SSH case. When you SSH into the machine running Rove, channel 1 lands on the remote machine's clipboard, not yours. The only channel that can reach the clipboard of the machine you are physically at is OSC52: it travels back through the SSH tty and is executed by your local terminal emulator.

So if copy "works locally but not over SSH", the break is almost always at the receiving terminal app (the one drawing pixels in front of you):

Terminal (the one you're physically using)OSC52 clipboard write
iTerm2Off by default: Settings → General → Selection → check "Applications in terminal may access clipboard"
GhosttyAllowed (clipboard-write = allow is the default)
kitty / WezTermAllowed or ask, configurable
Terminal.app (macOS)Unsupported: no fix; use another terminal or the escape hatch below

tmux in the path? If Rove itself runs inside a remote tmux session, tmux swallows OSC52 unless told to forward it:

set -g set-clipboard on

Escape hatch that always works: hold Option (macOS) / Shift (most Linux terminals) while dragging. That bypasses mouse reporting entirely and uses your terminal's native local selection + copy, which always lands on your local clipboard, at the cost of selecting across the whole Rove window (no pane awareness), exactly like tmux.

Right-click opens my terminal's menu instead of Rove's

Why. The outer terminal's context menu lives in the app layer, ahead of the TTY: it decides what to do with a right-click before mouse reporting ever sees it. iTerm2 (and several other emulators) keep right-click for their own menu by default, so Rove's row menu never gets the event. No TUI can take that back from inside the terminal. The fix is a terminal setting, not a Rove one.

iTerm2 ships an official escape hatch for exactly this (Pointer preferences):

  • Settings → Pointer → check "Ctrl-click reported to apps, does not open menu". Ctrl+left-click is then reported to Rove as a right-click and the row menu opens; plain right-click keeps iTerm2's menu, so you lose nothing.
  • Alternatively, Settings → Pointer → Mouse Button Actions can rebind the right-button gesture itself away from iTerm2's menu.

Terminal.app has no reporting toggle for this; use the keyboard fallback below.

Fallback that works everywhere: every row-menu entry is also a direct chord on the row itself (r rename, d delete, and so on); see KEYBINDINGS.md. The one right-click-only surface today is the project header's menu.

Holding ctrl does not open the direct-shortcut guide

The guide needs kitty keyboard protocol modifier press and release events. It works in iTerm2 3.5+, kitty, Ghostty, and WezTerm. Terminal.app and xterm.js do not provide these events. The guide also does not work when Rove runs inside tmux, because tmux does not pass the required modifier events through.

Run rove doctor to check whether the current terminal answers the kitty keyboard protocol probe. Unsupported terminals silently keep the legacy input path. Typing and existing shortcuts continue to work. Only the hold-to-reveal guide is unavailable.

Mouse wheel in the embedded terminal

The wheel follows real terminal-emulator semantics, in order:

  1. the embedded app enabled mouse tracking (claude's transcript, vim, less --mouse) → the wheel is forwarded; the app scrolls itself;
  2. fullscreen app without mouse tracking → 3 arrow keys per tick;
  3. plain shell → Rove's local scrollback (same channel as ctrl+pgup / ctrl+pgdn; scroll to the bottom to resume following).

If scrolling "does nothing" inside an app, that app received the events and chose not to scroll. Check its own mouse setting (e.g. :set mouse=a).

An app may turn mouse tracking on once at startup and never send it again. When you reopen a tab, the Hosted PTY host replays only the last 512 KB of output, so it first re-sends the modes that earlier output had set: mouse tracking, bracketed paste, cursor keys, alternate screen. Before 0.9.238 such an app lost the wheel after a reopen. A running host only picks this up after it restarts: rove reset, or closing every session so the host exits when idle.

Memory stays high after upgrading from a pre-0.8 build

rove 0.8 replaced the old tmux runtime with the PureTUI + Hosted PTY backend, but upgrading the package does not stop sessions that a pre-0.8 build already left running. Those old tmux -L rove sessions keep their bun / engine process groups resident, so memory can look unchanged after the upgrade.

rove doctor now reports them:

legacy tmux: ⚠ tmux 3.5a — 2 pre-v0.8 session(s) on `rove`
             20 process(es) across 8 pane(s), 1008.5 MB RSS total
             → run `rove reset` to stop this retired runtime safely

Fix: rove reset. It stops the daemon and Hosted PTY host, then SIGTERMs each legacy pane process group before killing the retired tmux server (a bare tmux kill-server would leak engines that ignore SIGHUP). Worktrees and the task index are untouched; add --hard only if you also want to wipe task/UI state.

On this page