Terminal Emulator Stack — The Triple Layer Portagenty Has to Deal With
Why this is captured
Section titled “Why this is captured”Portagenty (my launcher — github.com/cybersader/portagenty) started out as “define sessions in TOML, launch over tmux or Zellij.” That was simple. What’s made it less simple: to reliably control sessions across multiplexers and survive over SSH + cross-device, I’ve had to actually understand the triple layer — terminal emulator, multiplexer, launcher — and how they talk to each other.
Stamping questions + partial answers so the thinking is retrievable when I next iterate on portagenty or when the stack docs get filled out.
The three layers
Section titled “The three layers”Keystrokes flow top → bottom. First layer that matches a binding eats the key; lower layers never see it. That’s why hotkey conventions per layer matter.
Ctrl+Shift+*, Super/Cmd+*, Alt+Ne.g. WezTerm, Ghostty, Alacritty, Kitty, Windows Terminal, iTerm2
- Owns the window, font rendering, GPU, input handling
- Speaks: ANSI/VT, OSC sequences, Sixel/Kitty graphics, OSC 52 clipboard, OSC 8 hyperlinks
- Local process on the box in front of your face
- Claims keys first. Unhandled keys get encoded as escape sequences and forwarded ↓
Ctrl+B tmux, Ctrl+P Zellij) + lettere.g. Zellij, tmux, GNU screen
- Owns sessions, panes, tabs, scrollback, detach
- Speaks: ANSI subset + control protocol (tmux control mode, Zellij actions)
- Runs on the remote (or local) box where the work lives
- Sees what the emulator forwards. Matches a prefix combo? Handles it. Else forwards to the focused pane ↓
e.g. Portagenty, sesh, tmuxinator, smug, zoxide (adjacent)
- Owns session inventory + session intent
- Speaks: shells out to multiplexer CLI OR uses its control protocol
- "take me to project X" as one verb
Each layer has its own escape-sequence vocabulary and its own trust/permission model. An agent running inside the shell may emit an escape that only the emulator (top) can act on — e.g., OSC 52 to copy to the host clipboard — and that escape has to traverse the multiplexer cleanly without being swallowed or rewritten.
Hotkey conflict zones
Section titled “Hotkey conflict zones”Overlaps burn people when two layers want the same key. Classic examples:
| Key | Who wants it | Typical fix |
|---|---|---|
Ctrl+P | bash history-prev · Zellij pane-mode · vim fuzzy-find | Rebind Zellij’s prefix, or the app |
Ctrl+D | shell EOF · Zellij close-pane | Zellij confirm-on-close, or rebind |
Ctrl+Tab | WezTerm tab-switch · some editors | Emulator usually wins; prefer Alt+N |
Ctrl+A | bash start-of-line · tmux default prefix | Change tmux prefix to Ctrl+B or Ctrl+Space |
Rule of thumb: each layer picks a modifier pattern the layers below don’t touch. Emulator = Ctrl+Shift+* / Alt+N. Multiplexer = dedicated prefix key. Shell/app = everything else.
Tools in each layer
Section titled “Tools in each layer”Terminal emulators (layer 1)
Section titled “Terminal emulators (layer 1)”| Tool | Link | Platform | Notable for agentic work |
|---|---|---|---|
| WezTerm | wezterm.org | Win / Mac / Linux | Mature ConPTY, strong OSC 52, Lua config |
| Ghostty | ghostty.org | Mac / Linux (Win coming) | Fast, Kitty keyboard protocol, modern defaults |
| Alacritty | alacritty.org | Win / Mac / Linux | Minimal, GPU, no tabs/splits (rely on mux) |
| Kitty | sw.kovidgoyal.net/kitty | Mac / Linux | Best image protocol; defines Kitty keyboard protocol |
| Windows Terminal | github.com/microsoft/terminal | Windows | Default WSL landing pad; improving but thin config |
| iTerm2 | iterm2.com | Mac | Rich feature set; inline image protocol |
| Warp | warp.dev | Win / Mac / Linux | AI-native; vendor lock-in caveats |
Multiplexers (layer 2)
Section titled “Multiplexers (layer 2)”| Tool | Link | Notes |
|---|---|---|
| Zellij | zellij.dev | Rust, discoverable keybindings (status bar), KDL config — my primary |
| tmux | github.com/tmux/tmux | Universal, oldest stable, bidirectional control mode (tmux -CC) |
| GNU screen | gnu.org/software/screen | Oldest; pre-installed on many servers |
| abduco / dvtm | brain-dump.org/projects/abduco | Minimal attach-detach only; pair with dvtm for tiling |
Launchers / session managers (layer 3)
Section titled “Launchers / session managers (layer 3)”| Tool | Link | Notes |
|---|---|---|
| Portagenty | github.com/cybersader/portagenty | Mine. TOML-defined workspaces, multi-backend, pa claim cross-device |
| sesh | github.com/joshmedeski/sesh | Popular tmux-first smart session picker |
| tmuxinator | github.com/tmuxinator/tmuxinator | Ruby; YAML layouts for tmux |
| smug | github.com/ivaaaan/smug | Go; tmuxinator-like without Ruby |
| tmuxifier | github.com/jimeh/tmuxifier | Shell-based; layout templates |
| zoxide | github.com/ajeetdsouza/zoxide | Adjacent: directory jumper, not a session manager, but often chained in |
Shells and apps (layer 4)
Section titled “Shells and apps (layer 4)”| Tool | Link | Role |
|---|---|---|
| bash | gnu.org/software/bash | Default shell almost everywhere |
| zsh | zsh.org | Feature-rich alt; default on macOS |
| fish | fishshell.com | User-friendly; non-POSIX defaults |
| Claude Code | claude.com/claude-code | Anthropic’s CLI coding agent (lives in the shell) |
| OpenCode | opencode.ai | Open-source agent CLI with recursive sub-agents |
| Neovim | neovim.io | Editor; typical target of mux pane arrangements |
The escape-sequence surface area that actually matters
Section titled “The escape-sequence surface area that actually matters”For an agentic setup, the escapes/protocols that carry load:
| Protocol | What it does | Who must support | Who breaks it |
|---|---|---|---|
| OSC 52 | Copy text from remote shell → local clipboard | Emulator (WezTerm ✓, Ghostty ✓, Alacritty ✓, WT ✓); mux must pass-through | tmux disables by default (needs set -g set-clipboard on); some emulators gate it |
| OSC 8 | Clickable hyperlinks in terminal | Emulator (most modern ✓); mux must pass-through | Zellij historically swallowed; tmux 3.x passes through |
| Sixel / Kitty graphics / iTerm2 images | Inline images | Emulator-specific; protocol fragmentation | Multiplexers strip or mangle; Kitty protocol is cleanest |
| Synchronized output (OSC 2026) | Batch frame updates, no flicker | New-ish; emulator + app must both know | Few muxes relay it; emulators increasingly support |
| Undercurl / styled underlines | Richer diagnostics in editors | Modern emulators ✓ | Older tmux eats it |
| True color (24-bit) | Non-palette colors | All modern emulators ✓ | Some older tmux builds |
| Bracketed paste | Distinguish user-paste from typing | Universal-ish | Flaky in nested mux sessions |
| Kitty keyboard protocol | Unambiguous key reporting (Ctrl+Shift+letters, etc.) | Kitty, WezTerm (partial), Ghostty ✓; others no | Most mux + many emulators ignore it |
| ConPTY nuances | Windows’ Conhost pseudoterminal | Windows Terminal, WSL shells | Anything assuming pure Unix PTY |
Takeaway: the “terminal emulator” choice is NOT cosmetic. It determines which agent-relevant features (image paste, clickable URLs, clipboard integration) actually work end-to-end.
The Windows / WSL wrinkle
Section titled “The Windows / WSL wrinkle”My primary setup: Windows + WSL2. That adds a layer:
ConPTY has improved a lot but still has edges around:
- VT sequence translation (some emulator-native escapes get rewritten)
- Clipboard integration (OSC 52 from WSL → Windows clipboard is emulator-dependent)
- Title/tab updates (who owns the window title string?)
- Image protocols (Sixel via ConPTY is finicky)
Portagenty lives in the WSL side (Linux binary), but the user’s keystroke first hits the Windows emulator. That’s why WezTerm pairs well — mature ConPTY, good OSC 52, Lua config that lets me tune edge cases.
What portagenty has to care about
Section titled “What portagenty has to care about”The reason portagenty started needing to “get into” terminals/muxes:
- Session inventory that survives — find running sessions across muxes. Zellij:
zellij list-sessions; tmux:tmux ls. Abstraction layer needed. - Attach across machines —
pa claimover SSH. Requires the mux to be running on the remote and the SSH client to forward what’s needed (agent socket for ssh-forwarded clipboard, TERM value sanity, etc.). - Spawn-with-intent — “open a session for project X with these panes.” Zellij has
zellij action new-pane ...and layouts (KDL); tmux hasnew-session -d+send-keys. Fundamentally different APIs, same intent. - Control protocol vs CLI — tmux control mode (
tmux -CC) is a real bidirectional protocol; Zellij’s action CLI is fire-and-forget. Cross-compat requires choosing the lowest common denominator (CLI) OR specializing per backend. - Clipboard bridging — if portagenty spawns a session over SSH, ensuring OSC 52 round-trips requires coordinated emulator + mux config.
Each of these leaks abstraction: portagenty can’t just “launch a session” — it has to know which mux, which version, which emulator’s on the other end.
Open questions
Section titled “Open questions”- Should portagenty own a session manifest that describes the whole stack? Not just “zellij session
agentic-workflow” but “zellij ≥ 0.40 inside WezTerm on Windows, over SSH to WSL host X, with clipboard-bridge expected.” Too verbose? Or the honest description? - Is terminal multiplexing on its way out for agent workflows? OpenCode’s (former) background agents + Claude Code’s task tool + git worktrees do some of what “many panes in a mux” does. Parallel agents may not need visible panes at all — just filesystem results.
- Ghostty is rapidly maturing. When does it eclipse WezTerm on Windows (if ever — currently WezTerm wins there)?
- What’s the right pane layout for agentic work? One agent + editor + logs? Two agents side-by-side? Persistent vs ephemeral panes? Probably a pattern worth capturing once I’ve iterated.
Candidate destinations
Section titled “Candidate destinations”02-stack/02-terminal/— the tier-2 stack section already exists; could gain:emulator-comparison.md— the feature matrix table above, trimmed + concretemux-over-ssh.md— the clipboard / TERM / ConPTY pathportagenty-on-this-stack.md— how portagenty composes on top
02-stack/patterns/— cross-device SSH pattern already exists; could extend with “terminal protocol pass-through checklist.”- Portagenty repo itself — some of this is really portagenty’s concern (the “what does this launcher abstract over” discussion). An ADR in
cybersader/portagentycovering “mux backends as plugins” might be the honest home.
See also
Section titled “See also”02-stack/02-terminal/index.md— the current stack-level terminal page (Zellij / tmux / Portagenty picks + tradeoffs table)02-stack/patterns/cross-device-ssh.md— adjacent pattern- Portagenty repo:
github.com/cybersader/portagenty - WezTerm docs on OSC 52 + ConPTY behavior
- Kitty keyboard protocol spec (sw.kovidgoyal.net/kitty/keyboard-protocol/)
- Sixel vs Kitty graphics vs iTerm2 inline images — fragmentation worth a post of its own