Tempest Terminal System
Tempest’s terminal system is a sophisticated real PTY architecture that bridges the Tauri desktop application with xterm.js, enabling full shell capability and agent CLI integration. This guide documents the architecture, lifecycle, and key subsystems.Architecture Overview
The terminal system is split into two domains:- Rust Backend (PTY Management):
src-tauri/src/lib.rsmanages actual pseudo-terminal sessions usingportable-pty, spawning real shell processes and bidirectional I/O. - React Frontend (Terminal Rendering):
src/components/TerminalPane.tsxrenders terminal output via xterm.js and handles user input, with session state managed bysrc/store/sessionManager.ts.
PTY Architecture and Shell Selection
Shell Detection
Tempest probes for the preferred interactive shell once at startup and caches it:pwsh) first, falls back to built-in powershell.exe. The probe (pwsh -?) costs 100-200ms, so the result is cached via std::sync::OnceLock to avoid repeated probes per PTY spawn.
Unix/Linux/macOS: Uses the $SHELL environment variable if set, otherwise defaults to /bin/bash.
PTY Session Lifecycle
Thecreate_pty_session Tauri command is the entry point. It accepts:
PTY Session Storage
Sessions are stored in a thread-safe concurrent map (DashMap) indexed bysession_id:
Output Streaming
After PTY spawn, a background thread reads from the PTY master and sends chunks to the frontend:PID Persistence
The child process OS PID is written to a sidecar file.tempest-pid in the worktree directory. This enables cleanup after app restart if the PTY State map is lost:
kill_pid_tree() uses taskkill /F /T (Windows) or kill -9 (Unix) to recursively kill the shell and all descendants.
Agent Command Injection
When spawning an agent session (e.g. Claude Code, Gemini CLI), the frontend pre-assembles the full command line (with session IDs, resume flags, prompt hints) and passes it to the backend ascommand + args vectors.
The backend shell-quotes arguments containing spaces or single quotes and joins them into an agent invocation string:
NoExit flags:
Windows:
-NoExit / exec $SHELL -i ensures the shell stays open after the agent command completes, allowing user interaction. On Unix, exec $SHELL -i drops into an interactive shell after the agent finishes.
I/O Commands: write_to_pty, resize_pty, close_pty_session
write_to_pty
resize_pty
close_pty_session
taskkill /F /T on Windows), waits for exit (up to ~1 second with 40 * 25ms checks), and deletes the PID sidecar file.
On Windows, portable-pty uses a job object to track the shell and all descendants, so killing the child terminates the entire process tree in one operation. This is critical: if the agent CLI spawned subprocesses, they all die together and the worktree directory is released immediately.
Terminal Lifecycle: Frontend (React)
TerminalPane Component
TheTerminalPane React component (in src/components/TerminalPane.tsx) is a memoized renderer. One instance exists per session. Its key responsibilities:
- Create xterm.js terminal on mount
- Attach to the SessionManager to receive buffered output
- Handle keyboard input and send to PTY
- Manage WebGL contexts for rendering
- Handle terminal resizing
- Clean up on unmount
Initialization
FitAddon calculates terminal dimensions (rows/cols) from the container element using font metrics. The SearchAddon enables Ctrl+F terminal search. The WebLinksAddon makes URLs clickable.
Input Handling and Keyboard Shortcuts
xterm.js emitsonData events for every keystroke. The frontend captures these and sends them to the PTY:
attachCustomKeyEventHandler:
- Ctrl/Cmd+F: Toggle search bar (return false to prevent xterm default)
- Ctrl+C: Smart copy-or-SIGINT (if text is selected, copy; otherwise send SIGINT to PTY)
- Ctrl+V: Paste from clipboard
- Ctrl+Shift+C/V: Explicit copy/paste
- App shortcuts (Ctrl+Shift+T, Ctrl+B, etc.): Let pass through to WorkspaceView’s capture-phase listener
Resizing
On mount, a ResizeObserver watches the container element and callsfitAddon.fit() to recompute dimensions, then sends resize_pty to the backend:
WebGL Context Pool
xterm.js can render with WebGL acceleration (faster) or canvas fallback (slower). Tempest pools up to 6 WebGL contexts and assigns them to visible panes. When a pane is hidden, its context is released back to the pool.Cleanup
On unmount, the component detaches from SessionManager, releases WebGL context, and disposes xterm:xterm.js Configuration and Theming
xterm.js Addons
Tempest loads the following xterm.js addons:- FitAddon: Calculates rows/cols from container element
- Unicode11Addon: Enables Unicode 11 support for wide characters, emoji, etc.
- SearchAddon: Provides Ctrl+F search bar
- WebLinksAddon: Makes URLs clickable, delegates to Tauri’s
openUrl()plugin
Terminal Theme
xterm.js colors are mapped from Tempest CSS custom properties:Terminal Display Settings
Font size, font family, cursor style, cursor blink, and scrollback line count are all configurable via app settings and applied to xterm without PTY recreation:- Font size: 13px
- Font family: Geist Mono
- Cursor style: block
- Cursor blink: enabled
- Scrollback: 1000 lines
Session Management and ID Mapping
SessionManager Architecture
TheSessionManager (in src/store/sessionManager.ts) is a singleton that owns all Channel subscriptions and session state. Each Tauri command invocation creates a Channel that the frontend attaches to. SessionManager registers sessions and listens on their channels:
Buffer and Replay
Each session maintains a ring buffer of recent PTY output (capped at 2MB). When a TerminalPane component attaches, it receives all buffered chunks for replay:Multi-Listener Distribution
All listeners (TerminalPane instances) for a session receive each chunk. This allows multiple panes to render the same PTY simultaneously (though this is not yet a UI feature):Work-Done Detection System
For agent sessions, Tempest automatically detects when an agent CLI finishes and marks the session as “done”. This is complex because agents emit streaming output and keepalive bytes, making simple EOF detection unreliable.Detection Mechanisms
The system uses a multi-layered approach, checked in this order:1. Explicit Terminal Signals (Highest Priority)
These fire immediately and bypass the dead zone:- OSC 9 (ConEmu/iTerm2 notification): Agent sends
OSC 9orOSC 9;4;0(state 0 = complete) when it finishes - OSC 9;4;3 (Windows Terminal progress bar - indeterminate): Still working
- OSC 9;4;1/2/4 (progress normal/error/paused): Mark as working, not done
- OSC 133 (FinalTerm shell-integration):
- Code D = command finished (done)
- Code B = prompt ready (done)
- Code C = command started (working)
- OSC 777 (rxvt-unicode notification): Same as OSC 9
- DEC 2004h (Bracketed paste enable): Line editor ready = done
- DEC 2004l (Bracketed paste disable): About to execute = working
- DEC 1049l (Leave alternate screen buffer): TUI app exited = done
2. Byte-Quiet Heuristic (Fallback)
If no explicit signal fires, the system uses byte counting and timers. After the user presses Enter (markUserInput called):
- Starts a “quiet timer” that fires after 5 seconds with no PTY output
- Suppressed if
senderBusy(title says agent is thinking) or if fewer than 200 bytes have been received since the user input (gates false-positives during pre-output thinking) - When quiet timer fires and conditions are met, marks session as done
3. Hard Ceiling (Safety Net)
If the session stays in “working” state for longer than 12 seconds, force done (even if bytes keep trickling in). This catches keepalive-emitting agents that never reach a true quiet window. The ceiling is also suppressed bysenderBusy and the byte gate.
Title-Based senderBusy State
For Claude Code, the title contains a Unicode glyph that changes:✳(white diamond) = idle at prompt- Unicode spinner frames (◆, ◇, etc.) = thinking/generating
onTitleChange), the frontend calls sessionManager.updateTitle(), which updates the senderBusy flag. This blocks heuristic timers while the agent is actively thinking.
Dead Zone
After the user presses Enter, there’s a 300ms “dead zone” where all done signals are suppressed. This prevents the echo of the Enter key itself from triggering false-positive done (the shell echoes the input before processing it).Integration with Work State
The work state (idle/working/done) is persisted inworkState.ts:
useWorkState(sessionId) to display a spinner while “working” and a dot when “done”.
Multiple Terminal Sessions
Concurrent Sessions
Tempest supports multiple PTY sessions running concurrently. Each has a uniquesessionId (UUID). The frontend maintains an array of Session objects, each mapping to a PTY session on the backend:
Session Persistence
Sessions are saved to localStorage keyed by project/worktree. When the app restarts, WorkspaceView’suseEffect re-hydrates stored sessions and calls openSession for each, which calls create_pty_session on the backend.
Multiple Panes (Split View)
Multiple sessions can be visible simultaneously via split panes (horizontal or vertical). Each pane is a separate TerminalPane component rendering its own xterm instance. The split pane layout is stored as a binary tree:Copy and Paste Behavior
Copy
Two methods:- Ctrl+C with selection: Copies selected text to clipboard (xterm.js
getSelection()), returns false to prevent SIGINT - Ctrl+Shift+C: Explicit copy of any selection to clipboard (no SIGINT)
Paste
Three methods:- Ctrl+V: Paste clipboard to PTY
- Ctrl+Shift+V: Explicit paste
- Cmd+V (macOS): Paste
term.paste(), which sends the text to xterm as if the user typed it:
Frontend Session Registration
When a new session is opened, the frontend callscreate_pty_session with a Channel for output events. The backend sends PtyOutputPayload chunks over this channel:
channel.onmessage.
Display Settings and Defaults
Terminal display settings are stored in localStorage viaappSettings.ts:
useSettings() hook:
Styling and Layout
CSS Custom Properties
The terminal pane uses Tempest theme CSS variables:--tempest-bg-editor: Terminal background--tempest-terminal-fg: Foreground text--tempest-terminal-selection: Selection highlight color--tempest-terminal-{black,red,green,yellow,blue,purple,cyan,white}: ANSI colors--tempest-terminal-bright{Black,Red,Green,...}: Bright ANSI colors--tempest-bg-elevated: Search bar background--tempest-border-subtle: Border colors--tempest-accent-blue: Focus indicator (search input)
Container Sizing
The terminal pane is positioned absolutely to fill its parent container, with 8px padding and overflow hidden:Error Handling and Edge Cases
PTY Spawn Failures
Ifcreate_pty_session fails (e.g. shell not found, permission denied), the error is returned as a Tauri error and displayed to the user. Common causes:
- Shell binary not found
- Working directory does not exist
- Insufficient permissions to create PTY