wait --until free on remote and SSH panes (#840)
`pane_is_free` read the pane's local process tree and folded two very different answers into one `false`. On a pane that is only the near end of a connection that tree describes the tunnel: a pane routed to a remote daemon has no local pty at all, so `DaemonPane::procs` returned `Default::default()` and the tree came back empty; a pane whose shell is running `ssh` has a tree whose depth-1 process is the `ssh` itself, busy for exactly as long as you are logged in. Either way `free` was unreachable structurally, `seen_busy` was set on every poll — which suppressed the one hint that would have pointed at the gap — and the wait rode the whole `--timeout` before recommending `--until free`, the flag that had just failed. Freeness is now three-valued: free, busy, or "nothing here can answer", and the last one carries its reason. On a remote pane freeness is the far shell's own OSC 133 prompt marks. That is sound because the near shell cannot be at a prompt while the connection owns its pty, so a prompt mark on such a pane can only have come from the far side. Two things had to change for the daemon to be able to say it. The reader suppresses relayed prompt marks so a foreground program cannot engage the local line editor — right for the editor, and precisely wrong here — so `ShellState` now keeps the mark's own unsuppressed reading beside the editor's. And "not at a prompt" on a remote pane means nothing until the far shell has proved it reports at all, since the newest mark is otherwise the near shell's own "I started `ssh`", which nothing will ever supersede; a latch records the first prompt mark that arrives while the pane is remote, and is cleared on every hop. `PaneProcs` carries all of this to the CLI in a new optional `context` — remote target, whether this machine holds the pty, the mark's reading, the latch — so the answer still costs one request per poll and an older server, which omits the field, keeps today's tree-only behaviour. When the far host has no shell integration the honest answer is that this machine cannot tell an idle remote prompt from a running remote command. `wait` says so — exit 1, `status: unknown`, a `free_unknown` string naming the host — after one poll of grace for a handshake still in flight, and only when `free` was the only state that could still answer, so `--until done,free` keeps waiting on `done`. Without that it would hang forever on a wait with no `--timeout`. The `no-agent` timeout hint now fires only for a caller who did not already pass `--until free`, and the reason freeness never resolved is printed and put in the JSON in its place. Deliberately left alone: on a local pane the process tree still holds the verdict. A prompt mark can only turn a busy tree into free — which is what fixes a plain `ssh` pane on Windows, where the daemon has no way to name the pane as remote — never the other way round, so no pane that reads busy today can start reading free because an integration went quiet. `free` therefore now means "will take input" rather than strictly "back to the bare shell": a pane sitting at a nested shell's prompt is free, and the reference says so. The handoff record is unchanged, so a pane mid-`ssh` that survives an exec comes back reporting "cannot determine" until the far side's next prompt rather than guessing. Claude-Session: https://claude.ai/code/session_01UUyWQXzcBAoBzaSX8pc7nU
tty7
A terminal workbench: persistent sessions, remote work, agents.
Pure Rust · GPU rendering on Zed's gpui · VT core from Alacritty
English · 简体中文
Why
A background server owns your shells and panes — not the window. Everything below follows from that.
- Performance — ~2× the throughput of Alacritty, Ghostty, or Kitty (benchmarks)
- Persistent sessions — quit or reboot; your shells and supported agent sessions keep running, no tmux
- Agent-aware — Claude Code, Codex & co.: status, notifications, and git context for every repo at once
- Scriptable by agents — one agent opens a pane for another, hands off a task, waits, and reads the result, with or without the GUI running
- Editor-grade input — suggestions, completion, highlighting, history search, with no plugin to install
- Remote development — files, repos, panes, and git data stay on the remote machine, over a native SSH stack
- Git beside the terminal — source control, diffs, and worktrees without leaving the window
Install
Native builds for macOS, Windows, and Linux on Releases:
| macOS | …-macos-arm64.dmg · …-x86_64.dmg |
drag into Applications |
| Windows | …-setup.exe · portable ….zip |
|
| Linux | …-x86_64.AppImage |
chmod +x and run — X11/Wayland libraries bundled |
What's inside
| Agent-aware | per-pane detection (20 CLIs) · status dot · notifications · branch + diff · tray icon when input is needed · resume after reboot · tab sidebar grouped by repository |
| CLI + Skills | bundled tty7 CLI · agent skill · run streams a command and exits with its code · split · send · wait --until free · capture |
| Editor-grade input | ghost suggestions from history · explained tab completion · syntax highlighting · multi-line editing · click places the caret · ⌃ R fuzzy history |
| Window | tabs & splits · ⌘ P palette · ⌘ F scrollback search · ⌘ J panel with process tree and listening ports · 13 themes, your own YAML, iTerm2 import · IME |
| Shell integration | injected when a pane starts, nothing to install · prompt marks · working directory · exit codes · command-finished notifications · zsh, bash, fish, PowerShell, WSL, remote panes |
| Remote workspaces | remote files, repos, changes, diffs, worktrees, tabs, and panes · reconnect from any client and continue where you left off |
| SSH | native russh stack: profiles with keychain secrets · SFTP panel · port forwarding · jump hosts · one-time, unprivileged tty7-server install |
| Git | panel follows the focused pane · stage, commit, amend, branch, push, stash · side-by-side or unified diffs · commit graph with cherry-pick, revert, and reset · a new worktree opens its own tab |
Supported agents
Detection is free: brand avatar, branch + diff, tab title.
Status takes one click under Settings → Agents to install that agent's hook,
and brings the status dot, notifications, the tray icon, tty7 wait, and resume
after a reboot. Fork needs both — the agent's own fork command, and the hook
that tells tty7 which session to fork.
The full support matrix, all twenty
| Agent | Detected | Status · resume | Fork |
|---|---|---|---|
| Claude Code | ✓ | ✓ | ✓ |
| Codex | ✓ | ✓ | ✓ |
| TraeCode | ✓ | ✓ | ✓ |
| Grok | ✓ | ✓ | ✓ |
| OpenCode | ✓ | ✓ | ✓ |
| Oh My Pi | ✓ | ✓ | ✓ |
| Droid | ✓ | ✓ | ✓ |
| Qwen Code | ✓ | ✓ | ✓ |
| Goose | ✓ | ✓ | ✓ |
| Gemini | ✓ | ✓ | |
| Copilot | ✓ | ✓ | |
| Kimi Code | ✓ | ✓ | |
| Pi | ✓ | ✓ | |
| Aider | ✓ | ||
| Amp | ✓ | ||
| Cursor | ✓ | ||
| Auggie | ✓ | ||
| Hermes | ✓ | ||
| Vibe | ✓ | ||
| Antigravity | ✓ |
None of them are wrapped or proxied — the agent you start is the agent you get,
in a normal PTY, with its own interface. An agent launched through a wrapper
script can be mapped to one by name with agent_commands in config.json.
Documentation
Full documentation lives in docs/ —
keyboard shortcuts ·
config.json ·
CLI reference. The agent-facing CLI interface is also
documented in skills/tty7/SKILL.md.
Install the skill with:
npx skills add l0ng-ai/tty7 # install
npx skills update tty7 # update later
Benchmarks
Same machine, same day, same 155×40 grid — Apple M1 Pro, macOS 26.3.1, five-run averages (2026-07-04):
| tty7 | Alacritty | Ghostty | Kitty | |
|---|---|---|---|---|
Plaintext I/O — 11 MB cat (lower = better) |
95 ms | 239 ms | 179 ms | 185 ms |
| DOOM-fire frame rate (higher = better) | 888 fps | 485 fps | 552 fps | 617 fps |
| Cold-launch memory | 116 MB¹ | 105 MB | 128 MB | 130 MB |
¹ GUI 105 MB + the persistent server 11 MB.
Methodology and one-command reproduction: scripts/bench/.
Built on gpui and alacritty_terminal · Apache-2.0 · Discord · Changelog