Skip to content

Run Panel

The Run tab in the right sidebar is the project's command surface: it launches dev servers (e.g. npm run dev, cargo run, a Makefile target) without opening a terminal and typing the command by hand, and it also keeps the commands you submit in terminals so repeated ones stay one click away. It reads the project's manifests, lists the runnable commands next to the recorded ones, runs the chosen one as a hidden background process, detects the port it binds to, and surfaces clickable URLs - localhost for the machine that runs it, plus the owning node's tailnet (Tailscale) and LAN addresses so you can open it from another device.

The panel used to be split into a Run tab and a Cmds tab. They are one tab now; a command that is both a manifest runner and a recorded command shows up as a single row.

What it does

  • Manifest detection. On opening a project, the panel walks the project tree (up to 3 directories deep, skipping node_modules, target, dist, build, out, vendor, virtualenvs, and dotfolders) for supported manifests and lists every runnable command, grouped by ecosystem. Manifests nested in monorepo members - e.g. a Cargo workspace's crates/<app>/package.json - are found and labelled by their relative directory; running one launches it in that subdirectory. Supported manifests:
    • package.json - one entry per scripts key; the package manager is inferred from the nearest lockfile (pnpm-lock.yaml → pnpm, yarn.lock → yarn, bun.lockb/bun.lock → bun, otherwise npm), falling back to the root's manager when a nested package has no lockfile of its own.
    • Cargo.toml - cargo run, plus cargo run --bin <name> per [[bin]].
    • Makefile / makefile / GNUmakefile - make <target> per target.
    • justfile - just <recipe> per recipe.
    • pyproject.toml - [tool.poetry.scripts] / [tool.pdm.scripts] entries.
    • A free-form Run a command… input runs any ad-hoc command line. A bookmark button beside it saves the typed command as a pinned custom runner (see Save a command below) without running it.
  • Run. Starts the command as a PTY session on the project's owning node, marked hidden and tagged dev-server. Hidden sessions are kept out of the normal terminals list - they appear only under Running in this panel. The command runs inside the node's login shell (bash -lc on Unix, powershell -Command on Windows), so it sees the same PATH an interactive shell does. That is what makes toolchain-managed binaries (rustup, fnm, nvm, pyenv, Homebrew) resolve - the daemon's own environment under systemd is minimal - and it means shell syntax works: &&, pipes, globs, and env prefixes such as PORT=3000 npm run dev.
  • Pin / Unpin. Any row - detected runner, custom runner, or recorded command
    • can be pinned. Pinned rows float to a Pinned section at the top of the list. Pins are remembered per project. A merged row pins in both stores at once, so it cannot end up half-pinned.
  • Save a command. The bookmark button next to Run a command… persists the typed command as a pinned custom runner instead of running it. Custom runners appear under a Custom group (or the Pinned section when pinned) and carry a trash button to remove them. Detected runners cannot be removed; they are re-derived on each manifest scan.
  • Type into terminal. Clicking a row's text types the command into the focused terminal without pressing Enter. This needs a terminal you can write to: if another client holds the session claim, or the claim round trip is still in flight, the tap is refused with a toast rather than silently dropped.
  • Recorded commands. Commands submitted in a terminal (or a non-PTY prompt) are tracked per project with a usage count and listed under Recent, sorted by count. Each has Reset count and Remove, and the header has a trash button that clears every recorded command for the project. Detected runners cannot be removed; they are re-derived on each manifest scan.
  • URL detection. The daemon watches the process output for the host:port it prints (e.g. Vite's http://localhost:5173/) and computes the reachable URLs on the owning node, so the tailnet/LAN addresses are correct even when you are connected to a different cluster node. Clicking a URL chip opens it in your system default browser.
  • Open. Reveals the hidden process's terminal in the focused pane so you can watch logs or interact with it. It stays hidden from the normal session list.
  • Stop. Kills the session. The daemon tears down the entire process tree (SIGTERM, then SIGKILL after a grace period), so the port is released and no orphaned node/child process is left holding it.

What gets recorded

A command is recorded when its final trimmed line is a single non-empty command (ls, claude, rm dd, /help, /review src-tauri, /commit). Both input paths are tracked:

  • PTY terminals, by parsing submitted terminal input rather than counting every keystroke
  • PromptInput-based non-PTY sessions, by tracking successful send_input submits

History-recalled commands

Commands brought back without typing them character by character - arrow-up history recall, Ctrl+R reverse search, Tab completion, readline kills (Ctrl+W / Ctrl+U), and in-place line edits - are also counted. Those keystrokes only send escape or control bytes over the input stream; the resulting line is written by the shell, not by you, so the panel reads the rendered prompt line from the terminal and strips the shell prompt prefix. (Applying them as literal edits recorded nonsense: a tab-completed cd Repo<Tab> was stored as cd Repo plus a tab byte.)

Prompt detection is a heuristic: the prefix is taken up to the first prompt symbol ($, #, %, >, , ) followed by a space. If no such delimiter is found (for example, an unusual custom prompt), the command is not recorded rather than recording a polluted line. Right-side prompts (RPROMPT) may be appended to the captured command, since they share the same rendered line.

Long commands are truncated to one line. The full text is available by hovering on desktop, or long-pressing on touch devices.

Remote reachability

URLs are computed from the owning node's addresses:

  • Local (http://localhost:PORT) - only reachable from the machine running the server.
  • Tailnet - the node's MagicDNS name (e.g. host.tailnet.ts.net) or its 100.x Tailscale IP.
  • LAN - the node's LAN/DHCP address.

For the tailnet/LAN URLs to actually load, the dev server must listen on a non-loopback interface (0.0.0.0). When the server only printed localhost / 127.0.0.1, the panel still shows the remote URLs but adds a hint that remote access requires binding 0.0.0.0 (e.g. Vite --host, Next -H 0.0.0.0). Hive does not inject host flags automatically, since the flag differs per tool.

Implementation notes

Dev servers reuse the normal session machinery - there is no separate process registry:

  • Protocol. ClientMessage::NewSession carries tags; a session tagged dev-server ([DEV_SERVER_TAG]) is created hidden. The detected URLs live on SessionInfo.dev_server (DevServerInfo { port, bound_host, urls }), so they flow through the existing session list / SessionUpdated / cluster replication paths - no new push or list message.
  • Daemon. SessionManager::spawn_dev_server_watcher subscribes to the session's output broadcast; session/dev_urls.rs scans each line (ANSI stripped) for a host:port and computes the URLs from local_addrs::detect_local_addresses + the Tailscale Self.DNSName. The detected info is stored on the session and announced via session_resized_tx (live push to local clients) plus a SessionUpdated cluster mutation.
  • Frontend. lib/projectRunners.ts holds the pure manifest parsers plus the depth-bounded tree walk (detectRunners) that finds nested manifests; stores/devServers.ts orchestrates detection (via the files store), start (hidden + tagged session, wrapped in the target node's login shell), stop (KillSession), open (switchTab), opening URLs (lib/openUrl.ts, which uses the Tauri shell plugin on desktop and window.open in the browser), and the saved runners (togglePin, addCustom, removeSaved). stores/commandUsage.ts plus lib/commandUsage.ts hold the recorded-command tracking and its parser. The UI is components/sidebar-right/RunPanel.vue, which merges both lists by command string.

Persistence

Custom runners and pins are stored per project; plain detected runners are not, they are re-derived on each manifest scan. Only entries worth remembering (custom or pinned) are saved.

  • In the Tauri desktop/mobile app, they live under a runners map in the same project-ui-state.json the recorded commands use, via the load_project_runners / save_project_runners commands (local app-state only, not the daemon protocol).
  • In browser-shim mode, the same shape is persisted in localStorage["hive:project-runners"].

Recorded commands are stored the same way, per project_id: in project-ui-state.json via load_project_command_usage / save_project_command_usage, or localStorage["hive:project-command-usage"] in browser-shim mode. Each entry keeps command, count, and pinned.

Detected URLs are not persisted: after a daemon restart the process is gone, and the port is re-detected from output when a server is started again.

Hive - remote AI coding agents over WebSocket.