Skip to content

Terminal Emulator Stack — The Triple Layer Portagenty Has to Deal With

S5 · Instance 🔬 Research 2026-04-18

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.

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.

Terminal Emulator  ·  Hotkeys: Ctrl+Shift+*, Super/Cmd+*, Alt+N
e.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 ↓
Multiplexer  ·  Hotkeys: prefix key (Ctrl+B tmux, Ctrl+P Zellij) + letter
e.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 ↓
Launcher / Session Manager  ·  Hotkeys: none at runtime (spawn-and-exit, not interactive)
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
Shell / App  ·  Hotkeys: Ctrl+letter (readline/emacs), Alt+letter, single-mod combos
e.g. bash, zsh, fish, Claude Code, OpenCode, Neovim

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.

Overlaps burn people when two layers want the same key. Classic examples:

KeyWho wants itTypical fix
Ctrl+Pbash history-prev · Zellij pane-mode · vim fuzzy-findRebind Zellij’s prefix, or the app
Ctrl+Dshell EOF · Zellij close-paneZellij confirm-on-close, or rebind
Ctrl+TabWezTerm tab-switch · some editorsEmulator usually wins; prefer Alt+N
Ctrl+Abash start-of-line · tmux default prefixChange 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.

ToolLinkPlatformNotable for agentic work
WezTermwezterm.orgWin / Mac / LinuxMature ConPTY, strong OSC 52, Lua config
Ghosttyghostty.orgMac / Linux (Win coming)Fast, Kitty keyboard protocol, modern defaults
Alacrittyalacritty.orgWin / Mac / LinuxMinimal, GPU, no tabs/splits (rely on mux)
Kittysw.kovidgoyal.net/kittyMac / LinuxBest image protocol; defines Kitty keyboard protocol
Windows Terminalgithub.com/microsoft/terminalWindowsDefault WSL landing pad; improving but thin config
iTerm2iterm2.comMacRich feature set; inline image protocol
Warpwarp.devWin / Mac / LinuxAI-native; vendor lock-in caveats
ToolLinkNotes
Zellijzellij.devRust, discoverable keybindings (status bar), KDL config — my primary
tmuxgithub.com/tmux/tmuxUniversal, oldest stable, bidirectional control mode (tmux -CC)
GNU screengnu.org/software/screenOldest; pre-installed on many servers
abduco / dvtmbrain-dump.org/projects/abducoMinimal attach-detach only; pair with dvtm for tiling
ToolLinkNotes
Portagentygithub.com/cybersader/portagentyMine. TOML-defined workspaces, multi-backend, pa claim cross-device
seshgithub.com/joshmedeski/seshPopular tmux-first smart session picker
tmuxinatorgithub.com/tmuxinator/tmuxinatorRuby; YAML layouts for tmux
smuggithub.com/ivaaaan/smugGo; tmuxinator-like without Ruby
tmuxifiergithub.com/jimeh/tmuxifierShell-based; layout templates
zoxidegithub.com/ajeetdsouza/zoxideAdjacent: directory jumper, not a session manager, but often chained in
ToolLinkRole
bashgnu.org/software/bashDefault shell almost everywhere
zshzsh.orgFeature-rich alt; default on macOS
fishfishshell.comUser-friendly; non-POSIX defaults
Claude Codeclaude.com/claude-codeAnthropic’s CLI coding agent (lives in the shell)
OpenCodeopencode.aiOpen-source agent CLI with recursive sub-agents
Neovimneovim.ioEditor; 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:

ProtocolWhat it doesWho must supportWho breaks it
OSC 52Copy text from remote shell → local clipboardEmulator (WezTerm ✓, Ghostty ✓, Alacritty ✓, WT ✓); mux must pass-throughtmux disables by default (needs set -g set-clipboard on); some emulators gate it
OSC 8Clickable hyperlinks in terminalEmulator (most modern ✓); mux must pass-throughZellij historically swallowed; tmux 3.x passes through
Sixel / Kitty graphics / iTerm2 imagesInline imagesEmulator-specific; protocol fragmentationMultiplexers strip or mangle; Kitty protocol is cleanest
Synchronized output (OSC 2026)Batch frame updates, no flickerNew-ish; emulator + app must both knowFew muxes relay it; emulators increasingly support
Undercurl / styled underlinesRicher diagnostics in editorsModern emulators ✓Older tmux eats it
True color (24-bit)Non-palette colorsAll modern emulators ✓Some older tmux builds
Bracketed pasteDistinguish user-paste from typingUniversal-ishFlaky in nested mux sessions
Kitty keyboard protocolUnambiguous key reporting (Ctrl+Shift+letters, etc.)Kitty, WezTerm (partial), Ghostty ✓; others noMost mux + many emulators ignore it
ConPTY nuancesWindows’ Conhost pseudoterminalWindows Terminal, WSL shellsAnything 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.

My primary setup: Windows + WSL2. That adds a layer:

Windows emulator (WezTerm / Windows Terminal)
(ConPTY)
WSL2 Linux shell
Multiplexer (Zellij / tmux inside WSL)
Shell + agent (bash + Claude Code)

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.

The reason portagenty started needing to “get into” terminals/muxes:

  1. Session inventory that survives — find running sessions across muxes. Zellij: zellij list-sessions; tmux: tmux ls. Abstraction layer needed.
  2. Attach across machinespa claim over 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.).
  3. Spawn-with-intent — “open a session for project X with these panes.” Zellij has zellij action new-pane ... and layouts (KDL); tmux has new-session -d + send-keys. Fundamentally different APIs, same intent.
  4. 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.
  5. 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.

  • 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.
  • 02-stack/02-terminal/ — the tier-2 stack section already exists; could gain:
    • emulator-comparison.md — the feature matrix table above, trimmed + concrete
    • mux-over-ssh.md — the clipboard / TERM / ConPTY path
    • portagenty-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/portagenty covering “mux backends as plugins” might be the honest home.
  • 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