Terminal stack teardown — drop tmux, keep xterm #3991
Replies: 3 comments
Part 1 addendum — the WebGL renderer has an unpatched texture-memory bugConcrete, actionable finding for the "garbled output / cursor jumps" terminal bugs. The bug. AO ships this._deviceMaxTextureSize = this._gl.getParameter(this._gl.MAX_TEXTURE_SIZE)On GPUs/drivers that report The fix (proven in a comparable codebase that patches the newer addon line) is a three-line change:
This is the same class of bug AO's own Actions, cheapest first:
This is the renderer half of the terminal stack — fully independent of the tmux → direct-PTY change above. Both halves can proceed separately. |
|
After a second independent pass over current The runtime/renderer split is right: tmux is replaceable, and xterm can remain the browser painter. A detached PTY host is also a reasonable destination. The part I no longer think is correct is the feature-parity claim for a raw replay ring. The missing abstraction is a terminal authorityThe mode-replay section at the end of the original post actually exposes the gap. AO's current Windows
There is another ownership problem: terminal queries. With direct PTY fan-out, every attached renderer can see DA/DSR queries and potentially answer them, while a deliberately disconnected/headless pane has no renderer to answer. Current AO deliberately avoids forwarding xterm's generic I would introduce a
The browser can still use xterm. Initially the authority can encode a full ANSI repaint plus ordered ANSI updates; later the protocol could carry semantic cells/diffs to a custom painter without changing PTY ownership. This gives us a migration path rather than coupling the runtime rewrite to a renderer rewrite. A detached host plus registry/socket is sufficient for daemon restart. FD handoff is only needed to replace the host itself, and it does not make a host crash recoverable unless another process already holds the master FD and the VT state is checkpointed/transferred. The wire contract can be shared across OSes, but Unix PTY, ConPTY, and process containment will remain platform-specific implementations. Two smaller corrections to the parity table:
Lifecycle: repair ownership, not “idle”Persisting
The wheel proposal is also largely already present: the current handler accumulates pixel deltas and handles line/page modes. The remaining useful refinement is to preserve fractional line-mode deltas too, reset the accumulator when the buffer/mouse-routing context changes, and optionally add a typed diagnostic flag; it is not a missing core behavior. WebGL addendum: good experiment, different evidenceForcing canvas as an A/B test is sound. The more specific texture claim needs qualification:
I would test Safer migration order
So I still agree that tmux can probably be removed. I disagree that deleting it first is the simplification boundary: build the terminal authority first, then tmux becomes just one adapter we can retire safely. |
Part 3 — renderer landscape and canonical AO terminal directionDate: 2026-08-17 This is the canonical continuation checkpoint for discussion #3991. It builds on the original proposal, WebGL addendum, and terminal-authority correction. It independently compares open-source terminal and agent-orchestration projects with AO and focuses on four boundaries that are often conflated: PTY/process ownership, authoritative terminal state, transport/replay, and final painting. Executive answerVerified: xterm.js integration is a real source of bugs and complexity in AO and in mature peer applications. AO, VS Code, Wave, and Tabby all carry renderer fallback, WebGL context-loss recovery, resize/fit coordination, and host-specific input workarounds. AO and Tabby also reach into xterm private internals. These are not hypothetical costs. Correction: xterm.js is not the source of most terminal-system failure modes, and replacing it with a native embedded renderer would not eliminate PTY ownership, process lifetime, reconnect ordering, backpressure, resize authority, or terminal-query routing problems. A native renderer replaces part of the VT/input/rendering stack and introduces native-view, GPU, FFI, packaging, signing, accessibility, and per-platform integration work. Recommendation for AO: do not make a big-bang xterm-to-native rewrite. First separate a durable terminal authority from interchangeable terminal painters: The first renderer bake-off should stay inside AO's existing Electron/web boundary: compare a current xterm baseline with What “embedded terminal” can meanThe phrase covers three materially different products:
This distinction is the key to interpreting the projects below. Revisions and methodAll claims below are based on source or first-party documentation at these exact revisions. “Verified” means directly supported by those sources. “Inference for AO” is architectural analysis, not a claim made by the upstream project. Architecture matrix
AO baseline: renderer and authority are already separateVerifiedOn Unix, AO starts a fresh On Windows, a detached host owns ConPTY, keeps a bounded output ring, broadcasts live bytes, and atomically attaches a client to a ring snapshot before live fan-out (host ownership and sizing, snapshot boundary). This is durable process/transport ownership, but a suffix of raw output is not the same as a canonical screen and mode snapshot. The desktop sends JSON/base64 output frames over a shared WebSocket mux (wire and socket pool, pooled connection). xterm then parses and paints those bytes. AO's xterm component documents and implements concrete integration work:
Inference for AOThe last point is not merely a rendering bug. It is an ownership ambiguity: which emulator is authorized to answer DA/DSR and related terminal queries, and whether a reply came from current output or historical repaint. A native painter would still need that contract. Swapping only xterm leaves tmux/ConPTY lifetime, raw-stream replay, backpressure, and resize arbitration unchanged. AO is also testing an old xterm baselineVerified: AO pins Inference for AO: the fair baseline is not today's 5.5 stack. Before concluding that xterm must go, AO should run the same corpus against stable 6.0 and a pinned 6.1 beta, with a non-WebGL fallback. That upgrade can plausibly fix flicker and corruption cases; it cannot fix AO's transport, PTY, replay, or resize-authority defects. A beta should be shipped only after AO's own trace and lifecycle gates, not because VS Code happens to track beta releases. Full native embedding: cmux and SwiftTermcmuxVerified: cmux is a macOS-only Swift/AppKit application. Its README says it is not Electron and uses libghostty for GPU rendering (implementation summary, scope, platform). It creates a Ghostty surface by passing an AppKit The most relevant capability for AO is Ghostty manual I/O. In that mode Ghostty spawns no child process; input is delivered to a host callback, and externally owned output is injected with cmux does not prove transparent persistence of arbitrary child processes. Its restore contract saves layouts, directories, and best-effort scrollback but says arbitrary live process state is not checkpointed (restore contract). Inference for AO: cmux is the strongest evidence that a libghostty painter could improve a native macOS terminal surface. It is not evidence of a drop-in cross-platform Electron replacement. Hosting an AppKit/Metal surface beside Chromium requires native view and lifecycle integration; Linux and Windows would need another complete surface or a separate AO renderer. cmux's source itself contains substantial native lifecycle, focus, input, resize, and renderer-recovery code, so the complexity moves rather than disappears. electron-libghosttyVerified: Inference for AO: this validates feasibility but also exposes the cost hidden by the phrase “embed Ghostty”: DOM/native z-order, clipping, scroll synchronization, focus, DPI, IPC, N-API ABI, signing, and crash ownership all become product code. It is a useful later macOS prototype, not the cleanest first test of whether xterm's painter is AO's problem. Ghostty API scopeVerified: Ghostty describes Inference for AO: “use libghostty” currently presents two different risk profiles:
The latter can replace terminal-state parsing but does not replace xterm as a complete UI widget. SwiftTermVerified: SwiftTerm offers a headless engine plus AppKit/UIKit terminal views and a convenience local-process view; its macOS PTY helper uses Inference for AO: SwiftTerm is another credible Apple-native prototype candidate. AO should feed it externally owned terminal data instead of using a convenience view that owns the process; otherwise a view's lifetime would become the agent's lifetime. It does not solve AO's Windows/Linux painter requirement. Ghostty-backed web painters: wterm, ghostty-web, and ResttyThese projects matter more to AO's first renderer experiment than a native surface because they keep Electron, browser/mobile delivery, and the deployment boundary constant. wtermVerified: wterm separates a pluggable terminal core from a DOM painter. It offers a lightweight Zig/WASM core or an opt-in It is not renderer-bug-free. Its DOM painter manually handles wide cells and block glyphs; one source-level workaround pixel-snaps CSS gradients specifically because Claude Code's horizontal rule exposed inconsistent browser rounding (block-glyph workaround). Inference for AO: wterm plus ghostty-webVerified: ghostty-web exposes an xterm-compatible API around a Ghostty WASM state engine and a custom Canvas2D renderer, explicitly advertising migration by swapping the import (scope and API, usage). It implements its own selection, scrolling, link detection, input, dirty-row painting, and disposal above the VT core. The repository contains a regression test for stale cells becoming visible after scroll growth under reset-heavy output (viewport-corruption regression). That is useful counter-evidence to “Ghostty core means rendering bugs disappear”: the core can be correct while the viewport/cache/painter around it is not. Inference for AO: this is the lowest-friction parser-and-painter swap because its API resembles xterm. It avoids WebGL context and glyph-atlas failure classes, but Canvas2D still owns DPI, font metrics, selection hit testing, and redraw scheduling. It is a good bake-off candidate, not yet evidence for a production migration. ResttyVerified: Restty combines Inference for AO: Restty is the most ambitious web-native candidate, but it rebuilds the same difficult GPU resource surface—glyph atlases, context/device lifecycle, shaders, fallbacks—that makes xterm WebGL costly. It may ultimately outperform both xterm and DOM painting, but its present maturity and GPU complexity make it a later benchmark rather than AO's first replacement candidate. Browser-widget lineage: VS Code, Wave, and TabbyVS CodeVerified: VS Code uses both node-pty and xterm packages (dependencies). PTYs live in a separate pty-host service with heartbeat/restart and attach/input/resize APIs (host lifecycle, process operations). For persistent terminals, the host feeds output into The client creates xterm and dynamically falls back from WebGL to DOM on context loss; it notes WebGL and DOM renderers can calculate different cell dimensions and therefore requests another resize (client construction, renderer fallback). Inference for AO: VS Code is strong precedent for a durable authority plus disposable xterm painter. It also demonstrates that introducing a headless emulator does not remove mode/query/replay complexity. Using the same emulator family for headless snapshots and client rendering reduces semantic drift, but it couples authority behavior to xterm's parser and serializer coverage. Wave TerminalVerified: Wave's Go shell controller owns the PTY, continuously appends raw bytes to a circular terminal file, and separately handles input/resize (circular output file, PTY pump). The browser owns xterm, its serializer, and WebGL/DOM switching with context-loss fallback (xterm setup, renderer fallback). For restore, the frontend loads a previously serialized xterm cache at its recorded byte offset, then consumes the remaining raw output tail. During idle periods it serializes current xterm state back to storage (cache plus tail, idle serialization). Inference for AO: this is a practical cache design, not a single server-side terminal authority. If no renderer has consumed recent output, server-side semantic state and terminal replies are not inherently current. It illustrates how split ownership can work for an interactive desktop, but it is weaker for multi-viewer, remote, or crash-consistent AO attachments. TabbyVerified: Tabby's Electron main process wraps node-pty and uses an explicit acknowledgement queue to pause and resume PTY flow under renderer backpressure (PTY and acknowledgement queue, PTY ownership and IPC operations). A renderer proxy can reconnect to a surviving PTY by ID (renderer proxy). Its xterm frontend implements flow control, WebGL recovery, input/resize, and private Inference for AO: Tabby directly validates the user's concern: browser-terminal integration can require brittle internals. It also validates a separate point: backpressure and PTY lifetime live outside the renderer and remain necessary after a renderer swap. Server-owned semantic state: Zellij and HerdrZellijVerified: a Zellij terminal pane owns a server-side grid/VTE parser and feeds every PTY byte into it. It pauses or intercepts some host queries and tracks pending input (pane state, PTY-byte handling). Rendering produces semantic character chunks from the grid; the output layer builds per-client changes and encodes them to ANSI (pane rendering, per-client output, ANSI serialization). Its server-to-client render payload is an ANSI string, which the client writes to stdout (IPC message, client output). HerdrVerified: Herdr owns its PTYs, feeds output into vendored Inference for AOThese designs move reconnect, inspection, and slow-client policy onto authoritative visual state. A later frame can safely supersede an unsent earlier frame because it represents a complete current result; arbitrary raw VT chunks cannot be dropped this way. They do not show that AO can eliminate all painters. Zellij and Herdr can rely on an outer terminal because their primary clients are terminal applications. For AO, one of these must still be true:
A custom cell painter is a major product in its own right: Unicode width and shaping, bidi policy, cursor and selection, IME, keyboard protocols, hyperlinks, clipboard, accessibility, images, ligatures, font fallback, and GPU lifetime all become AO's responsibility. Native semantic models: WezTerm and ArborWezTermVerified: WezTerm's native mux does not treat a renderer as the sole copy of terminal state. Its Inference for AO: WezTerm is the strongest architectural reference for a state-authoritative mux with incremental painting. It is not a drop-in embeddable AO widget, but its sequence/change seam is closer to the correct long-term contract than forwarding an unbounded historical byte suffix to every painter. ArborVerified: Arbor, another agent/worktree desktop, keeps daemon-backed terminal sessions, uses an Alacritty-derived emulator by default, offers experimental Inference for AO: Arbor demonstrates that a custom native cell view is viable when the product defines the supported terminal semantics. Its narrow snapshot is also a warning: a screen suitable for common agent output is not automatically full fidelity for arbitrary TUIs, graphics protocols, query replies, and accessibility. AO should state that product choice explicitly if it ever narrows the contract. Agent orchestrators and terminal managersSuperset: durable PTY plus headless xterm authorityVerified: Superset's session object says it owns a PTY subprocess, a headless emulator, attached clients, and disk output. It feeds PTY output through the headless emulator before broadcasting and queues emulator work to prevent blocking the I/O path (session ownership and construction, headless setup and query responses, output pipeline, emulator queue). The authority waits for a consistent emulator boundary, serializes a snapshot, and attaches live output after that boundary (snapshot boundary, attach). Its headless wrapper uses The PTY daemon can hand inherited PTY master descriptors and snapshots to a replacement process (daemon handoff). Inference for AO: this is the closest evidence for the proposed terminal-authority shape, not necessarily a code target. It shows both its value and its cost: consistent snapshots and a sole query responder become possible, but headless write scheduling, mode completeness, handoff, and renderer rehydration are substantial systems work. The extra hand-maintained mode parser also shows a semantic-drift risk when the underlying emulator does not expose every state needed by the product. Vibe Kanban: simple PTY-per-WebSocket baselineVerified: Vibe Kanban creates a Inference for AO: this is an intentionally simpler endpoint, useful as a complexity floor. It does not provide AO's persistent agent terminal, canonical replay, multi-viewer, or daemon-restart requirements. A renderer that reconnects cannot restore a session when the backend creates a new shell per connection. Agent Deck: tmux is the terminal authorityVerified: Agent Deck describes itself as a tmux-backed terminal session manager (README). It creates detached tmux sessions, configures them, and uses Inference for AO: this avoids embedded renderer bugs only because the user's existing terminal paints. It retains tmux as a dependency and gives the application less control over exact visual composition and accessibility. It is a credible optional “Open externally” path, not a full AO desktop replacement. NTM: exact tmux session plus monitoring-grade event streamVerified: NTM explicitly uses tmux as its execution control plane and exposes REST/SSE/WebSocket surfaces (README, capabilities). Its pane streamer prefers a Inference for AO: NTM's events are monitoring-friendly lines, not a faithful interactive terminal frame. That is a useful product separation: AO's overview can use structured agent status/transcripts and avoid continuously painting every hidden terminal, while an exact terminal remains available on demand. Parser/state library without a painter: libvtermVerified: libvterm exposes terminal state properties, input/output and key/mouse APIs, state callbacks, and a screen-cell/callback API (state and input API, screen API). The inspected Neovim fork explicitly says Neovim currently bundles its own vterm rather than using this fork (fork status). Inference for AO: like Which bugs does replacing xterm actually address?
The user's diagnosis is therefore directionally correct at the painter/host-integration layer. It becomes incorrect if “xterm bug” is used as a catch-all for stream corruption, stale replay, terminal replies, process lifetime, or sizing races. Strategy comparisonKeep xterm as both state engine and painterAdvantages:
Costs:
Ghostty VT core plus a web painter (wterm or ghostty-web)Advantages:
Costs:
Native embedded painter (
|
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Terminal stack teardown — drop tmux, keep xterm
A teardown of AO's terminal stack: today's implementation (tmux on macOS/Linux, conpty on Windows, xterm.js renderer) vs. direct PTY ownership, with concrete simplifications and fixes. Every claim is verified against code in this repo (not docs).
What's there today (verified)
backend/internal/adapters/runtime/tmux/— agents run inside tmux sessions; AO drives them viatmux attach, itself wrapped in a raw PTY byptyexec/spawn_unix.go(which already usescreack/pty).backend/internal/adapters/runtime/conpty/— a separate pty-host process (loopback TCP) +ptyregistry(restart recovery) + ring buffer. No tmux.@xterm/xterm5.5.0 + WebGL 0.19.0 (prefer WebGL, fall back to canvas —XtermTerminal.tsx).The two layers (this is the key mental model)
They're orthogonal. Replacing the multiplexer does not replace the renderer, and vice-versa.
Recommendation 1 — replace tmux with direct PTY
creack/pty(v1.1.24) is already a dependency andptyexecalready spawns a raw PTY — it just points it attmux attach. Point it at the agent command instead, and add a ring buffer for scrollback (theconpty/ring.gopattern already in the repo).Feature parity, with the benefit of switching:
creack/pty)ESC[>84;0;0cflood #2094, cursor/repaint desync)pty.Setsize+ SIGWINCH (already inptyexec)enterDelayprocess-tree.go+ setsidkillSessionsByPIDmachineryCross-cutting benefits: drops the
tmuxbinary as an install prerequisite; one runtime code path on all three OSes (today tmux and conpty are two divergent implementations); no persistent tmux server (our owntmux.godocuments a footgun where the first CLI call auto-starts a server pinned to a now-deleted path); deterministic process model; byte fidelity becomes testable (raw bytes in = raw bytes out, impossible to assert through tmux).The one cost, and how to recover it
tmux survives daemon restarts; an in-daemon PTY dies with it. AO already has the recovery shape on Windows (separate pty-host + registry +
AO_RUNTIME_LAUNCH_IDreattach). Port that to macOS/Linux: a small PTY-owning sidecar over a Unix socket (0600) with fd-handoff, so even daemon upgrades don't drop running shells.Renderer: keep xterm, fix the addon
Renderer bugs are separate from the multiplexer. If "garbled output / cursor jumps" is WebGL-related, the lever is the WebGL addon version (0.19.0 stable is the usual suspect —
XtermTerminal.tsxis full of WebGL-atlas workarounds). Mitigations, cheapest first: force the canvas renderer (already the fallback), then patch/upgrade the WebGL addon. Replacing xterm itself is not worth it — it's the category-standard renderer.What I'd do first
loadRendererfallback); if garbled output disappears, backport the WebGL texture-clamp patch (see the addendum comment below).PTY lifecycle cleanup — accumulation is "everything around the kill path"
A comparable codebase hit the macOS PTY-master limit (509/511) after a handful of parallel workspaces and root-caused it to lifecycle gaps around an otherwise-correct per-terminal kill path. The findings map directly onto AO's runtime/reaper:
disposeRequestedAtand have the reaper retry.setsid(MCP servers, build daemons, watchers) reparent to pid 1 and get a fresh pgid, invisible to a ppid-walk +kill(-pgid). AO's own process-tree kill has the same escape hatch; add a kill-by-cwd (workspace dir) backstop, not just the ppid tree.The kill path itself is usually correct; accumulation comes from the lifecycle around it. Cheap wins: idle TTL + cap, kill-by-cwd backstop, and park = disconnect.
Terminal wheel/scroll handling: fractional lines + an escape hatch
A comparable terminal layer replaces xterm's stock wheel handling with a custom one that accumulates fractional lines across events (signed), handles
DOM_DELTA_LINE/PAGEmodes, and keeps a context key so a buffer/mode change resets the accumulator. The notable trick is the escape hatch: alocalStorageflag (STOCK_WHEEL=1) reverts to stock xterm behavior, checked per wheel event rather than at install — because terminal instances are parked and reused across React mounts, an install-time check would need a full window reload. Cheap, targeted terminal-UX improvement with a zero-rebuild rollback switch.Terminal mode replay: reattach must re-assert mode state
A comparable codebase hit a subtle terminal bug worth guarding against in AO's replay path: a kitty-aware TUI (Codex) emits
\x1b[>7uonce at startup to enable the kitty keyboard protocol, but the host's scrollback is a fixed 64 KiB FIFO — long conversations evict that enable byte. On renderer reload, a fresh xterm (kitty flags = 0) reattaches, the replay no longer contains the enable, and the TUI never re-pushes it — so Shift+Enter silently changes meaning mid-conversation. The same class is latent for bracketed paste, focus reporting, mouse tracking, app cursor, and cursor visibility.The fix is VSCode's scaffolding with a corrected policy: run a headless xterm on the host, feed every PTY chunk to it alongside the FIFO, and on replay build a mode-state preamble from the headless state (kitty →
\x1b[=N;1u, bracketed paste →?2004h, focus →?1004h, mouse →?1000h/?1002h/?1003h) and send it before the FIFO bytes. AO's ring-buffer replay should do the same: persist terminal mode state (or re-derive it via a headless parser) and re-assert it on attach, instead of replaying raw bytes and hoping the enable sequence survived.All reactions