Terminals
How Daintree's terminal panels work: the PTY, the WebGL and DOM renderers, scrollback, alt-screen mode, search, links, clipboard, drag-and-drop, and the type-anywhere rescue.
A terminal is the only panel kind with a real process behind it, and it is the kind agents run in. Everything on this page applies equally to a plain shell and to an agent panel: an agent is a terminal running an agent CLI, not a separate kind. For the panel model itself, see Terminals & Panels.
The PTY and the Renderer
Daintree runs node-pty for native pseudo-terminal processes and xterm.js for rendering. You get full terminal emulation: colors, cursor positioning, the alternate screen buffer, and the sixel and iTerm2 image protocols. PTYs live in a separate utility process, and output crosses to the renderer over an IPC channel with explicit flow control, which is what the Paused badge reports on.
WebGL and the DOM fallback
Each visible terminal wants a WebGL context, because the DOM renderer falls back to the configured font and mangles block, box-drawing, and Powerline glyphs, exactly the characters agent TUIs are built out of. Chromium caps how many WebGL contexts one renderer can hold, and silently evicts the oldest when you go over, so Daintree manages the pool itself.
The manager runs in one of two modes and flips between them on a count, with hysteresis so opening one panel at the boundary doesn't flap the whole fleet. On a typical machine the upper threshold is around 18 open terminals wanting WebGL and the lower is around 15; both scale with your RAM and the active resource profile. Above the upper threshold the fleet enters DOM fallback. The focused terminal and pinned alternate-screen terminals can retain WebGL for readable glyphs; once demand falls to the lower threshold, other eligible visible terminals re-attach.
A circuit breaker sits behind that: three genuine context-loss events inside 60 seconds disables WebGL for the rest of the session. A single GPU eviction that drops every context at once counts as one event, not many, so one bad frame can't trip it, but a machine with a persistently faulty GPU driver stops strobing between renderers and settles on DOM.
xterm's glyph atlas is shared across every terminal using the same font and theme, and it has no per-entry eviction, so truecolor agent output would otherwise only ever add to it. Daintree caps the atlas texture size at 1024 px, which is what makes eviction reachable at all: once every page has grown to the cap, the next demand clears the atlas instead of doubling it. The practical effect is that a long-running fleet of streaming agents plateaus in memory instead of climbing.
Redraw
Occasionally a TUI and the renderer disagree about what is on screen: a resize landed mid-repaint, or a program wrote cursor-relative output over a stale grid. Redraw Terminal (Command-K, then Command-D Control-K, then Control-D Control-K, then Control-D , or the panel's right-click menu) repairs it. When you trigger it yourself it forces a full geometry resync immediately, rather than waiting for a gap in the output, so it works on a busy agent, which is exactly when you need it. The same repair dispatched automatically by a plugin or an agent keeps the write-quiescence delay, because an unprompted repaint under a live cursor is the thing that delay exists to prevent.
Adaptive refresh rates
Rendering adapts its refresh rate to visibility and focus. With a dozen panels open, that saves real work:
| Tier | Interval | Roughly | When |
|---|---|---|---|
| Burst | 16 ms | 60 fps | Active output streaming |
| Focused | 100 ms | 10 fps | The focused panel |
| Visible | 200 ms | 5 fps | On screen, not focused |
| Background | 1000 ms | 1 fps | Off screen or in an inactive worktree |
Backgrounded Terminals Stay Live
Daintree does not sleep individual terminals. There is no "Sleep Terminal" action, no sweep over idle panels, and no hibernated state a terminal can be in. Scroll a panel out of view, switch worktrees, or leave an agent alone for an hour, and it keeps its process and buffer alive while the view is resident, with background rendering throttled. A project view can still be evicted as described below.
Earlier versions did suspend a background terminal's renderer and rebuild it on reveal, keeping the PTY alive underneath. It reclaimed memory, and it was the root cause of a recurring class of agent-TUI corruption (zebra striping, double-spaced output, dropped lines) reproduced across OpenCode, Codex, Gemini CLI and Claude Code. The resync on reveal could never be made reliable for a program that had redrawn itself while the view was gone. Removing per-terminal hibernation removed the whole failure mode.
Scrollback
Pick a base scrollback size in Settings > Panel Grid > Scrollback: 500, 1,000 (default), 2,500, or 5,000 lines. The settings panel shows an estimated memory cost for the size you pick.
That number is a base, not a limit. Daintree scales it by what the terminal is actually doing, because an agent's transcript is the thing you scroll back through and a shell's is usually not:
| Terminal | Multiplier | Floor | Ceiling |
|---|---|---|---|
| Agent | 10× | 500 lines | 10,000 lines |
| Plain shell | 0.3× | 200 lines | 2,000 lines |
At the default 1,000-line base, an agent panel holds 10,000 lines and a shell holds 300. The efficiency resource profile lowers the agent ceiling to 4,000 lines on constrained hardware; Performance Mode drops every terminal to a flat 100 lines.
Scrollback restore
When a session is restored, Daintree replays its saved buffer into the fresh terminal. If that replay can't finish, an inline banner appears at the top of the panel with a Reset terminal button that clears the buffer and starts fresh. You can also dismiss the banner and keep working. The message names what went wrong:
- Scrollback restore timed out: the replay didn't finish in time.
- Scrollback contents couldn't be replayed: the saved buffer couldn't be parsed.
- Scrollback restore failed: a general fallback carrying the underlying error.
In every case the terminal itself is fine and the live process is untouched. The only thing missing is earlier output.
Alt-Screen Mode
Most agent CLIs can render either inline (writing into the normal buffer, the way a shell command does) or on the alternate screen, the full-screen buffer that programs like vim and htop use. The two behave differently in ways you will notice:
- Inline is smoother in Daintree. Output joins the WebGL-rendered scrollback, so you can scroll back through it and search it, and a resize reflows cleanly.
- Alt screen matches what the CLI looks like in a bare terminal, which is what you want if you rely on its own full-screen layout.
Inline is the default. Turn Use alt-screen mode by default on in Settings > CLI agents to flip it globally, and override it per agent or per preset with the tri-state Alt-screen mode control (Default / Inline / Alt screen). See Agents and Agent Presets.
Runtime State Badges
The panel header carries small badges for states Daintree enters on your behalf. They are automatic and each recovers on its own:
| Badge | What it means |
|---|---|
| Paused | Output is arriving faster than it can be drained. Streaming pauses to prevent data loss, then resumes once the buffer clears. The tooltip shows how long it has been held. |
| Paused (memory) | The app is under memory pressure and this terminal was paused to ease it. Recovers automatically. |
| N queued | Commands are queued for this terminal and will be sent in order. |
| [exit N] | The process has exited with that code. |
If you'd rather not wait out a pause, Force resume (paused) is in the right-click menu and bound to Command-K, then Command-U Control-K, then Control-U Control-K, then Control-U . A watchdog also fires it for you if a pause outlasts what the flow-control cycle should need.
An agent panel's header also carries its state chip, and a waiting agent says why it is waiting: Approval, Question, or Error. See Agent States.
Subagents in the Header
Some agent CLIs delegate. Ask Claude Code to review four files and it can hand each one to a subagent: a child session with its own conversation, its own transcript, and its own model, running underneath the parent and reporting back to it. Codex spawns child threads the same way. Daintree does not create these and cannot create them — the CLI decides to delegate, entirely on its own. What the header adds is a way to see the children that already exist, and read what each one was asked to do.
Two agents can report their children: Claude Code and Codex CLI. For every other agent, nothing is ever queried and nothing appears.
The chip
When a lookup finds children, a small pill appears at the right-hand end of the panel header, past the state chip and the runtime badges: a network glyph and a count. It is self-gating, so a terminal that never delegates never grows an affordance, and the chip's absence is not a diagnostic — an agent that can't report children, a session that hasn't delegated yet, and a lookup that couldn't identify the session all look identical from outside, which is to say they all look like nothing.
Clicking it opens a popover headed Claude subagents or Codex subagents, with a refresh button and a scrolling list of rows, newest activity first, capped at 50. Each row carries:
- A title, taken from the best thing the provider recorded: the handle the parent hung on the child (Claude's description of the delegated task, Codex's nickname), then the worker kind, then the first 48 characters of the delegated prompt, then the first eight characters of the child's id.
- A status word, and how long ago the child last wrote.
- A second line, only when it says something the title didn't already spend: the delegated task, the worker kind, the model, and a depth — but a depth only past the first level, since every child is at least one level down and "Depth 1" would be noise on every row.
Status vocabulary
| Label | What it means |
|---|---|
| Working | The child is producing output. |
| Waiting for approval | Held at a permission prompt. Typing at it won't help; the approval is what it needs. |
| Waiting for input | Held on a question. |
| Idle | The provider reports the child as loaded and doing nothing. |
| Done | The child's last turn ended cleanly. |
| Error | The provider reported a system error. |
| Not loaded | The provider holds the child but never hydrated it, so it reported no state at all. |
| Unknown | The last thing on record was an unfinished step that stopped changing, or a record in a shape this build doesn't recognize. |
Color follows the same three-way split the rest of the app uses: Error is the error tone, Working and both Waiting states are the active tone, and everything else stays muted. A finished child is not a state worth coloring.
No provider reaches every one of those, and that is deliberate rather than incomplete. Codex never reports Done, because a thread it declined to load has no finish to observe — in practice most Codex rows read Not loaded. Claude Code never reports Idle, Waiting, or Error, because the files on disk cannot say a child is sitting at an approval prompt or that it failed. A status a provider can't honestly establish is one it doesn't guess at, which is also why Unknown keeps its reason: a child that was never loaded, one that stopped mid-step, and a record that wouldn't parse are three different admissions, and one word for all three would claim knowledge none of them carries.
Transcripts
Click a row to expand it. Daintree reads that child's transcript on expand, and reads it again if the child has written anything since you last looked, so a row you left open updates rather than going stale. The transcript renders oldest first as alternating Task and Reply blocks: the work the parent delegated, then what the child said back.
What is not there is as deliberate as what is. Thinking and reasoning traces are dropped — they are the child's scratchpad, not its output, and they dwarf the answer. Tool calls and tool results are dropped too: those are mechanism, and rendering raw tool output would put whole file contents into a popover.
A transcript is capped at 24 messages and 4,000 characters per message. When the cap bites, the oldest replies are dropped and the delegated task is kept wherever it sits, because the task is the one message that makes every reply below it legible. A trimmed transcript says "Showing the latest messages" at the top; a child that hasn't recorded anything yet says "No messages recorded yet".
If the read fails, the row says why in a sentence and offers Retry:
| Message | Cause |
|---|---|
| Couldn't match this terminal to a … session | The terminal has no session that can be correlated to a store. |
| More than one … session ran in this folder, so this terminal's can't be identified | Codex only. See below. |
| That subagent isn't one of this terminal's | The named child failed the membership check against this terminal's session. |
| Couldn't read …'s session files | Claude only: the store is there, but the read failed. |
| Couldn't read … sessions | Codex only: the app-server answered with something unusable. |
| … took too long to respond | The lookup passed its budget. |
| … CLI isn't available | The CLI isn't on the PATH Daintree resolves against. |
| This terminal isn't running … | The terminal was relaunched onto another agent since the list was fetched. |
Where the data comes from
Neither provider is scraped from the terminal. Each is read from what that CLI already keeps, and the two are read completely differently — which is worth knowing, because it is where the two behave differently.
Claude Code writes a JSONL transcript per child, plus a small sidecar, into its own per-session folder. Daintree opens those files, reads them, and closes them: nothing writes, renames, deletes, or moves anything in that store. The alternatives were worse citizens. SubagentStart and SubagentStop hooks would mean writing entries into your settings.json, and streaming JSON output would mean running Claude headless. Reading files Claude already wrote is the narrow option.
None of that format is contracted — Anthropic documents neither the record schema nor the folder naming — so every field is parsed as optional and a record that won't parse costs one row rather than the whole list. Status is inferred, because there is no completion record: the file simply stops. A last turn that ended cleanly reads Done. Otherwise a file written within the last two minutes reads Working and anything quieter reads Unknown — never Error. A file that stopped mid-tool-call looks the same whether the child died or is three minutes into a test run, and the disk can't tell them apart.
CLAUDE_CONFIG_DIR the way the CLI does. If a preset or your shell profile exports a different CLAUDE_CONFIG_DIR to Claude Code alone, Daintree reads the wrong directory and the list comes back empty rather than wrong — and an empty list is indistinguishable from a session that simply hasn't delegated, because the chip only appears once a lookup finds children. If you have pointed Claude Code at a custom config directory and no count ever appears, that is the first thing to check.Codex is read over its own app-server protocol instead. Each lookup spawns a short-lived codex app-server, asks it for the parent's child threads and, on expand, for one thread's turns, and lets it exit. The transport enforces a method allowlist before anything reaches the server's stdin: the protocol's thread-creation call is deliberately absent, because it would mark a project trusted in your ~/.codex/config.toml, and writing your agent's own config is out of bounds. At most two of those servers run at once, so restoring a project full of Codex panes queues rather than storming.
Refresh
Nothing polls. Daintree looks once when the panel mounts, again whenever the parent agent settles (idle, waiting, or completed — the points at which anything it spawned has had a chance to reach the store), and again whenever you click refresh in the popover. Automatic lookups are floored at 20 seconds apart, so an agent flickering between states can't turn either provider into a storm; the refresh button bypasses the floor, because you asked.
Each answer is cached against the terminal and the PTY's start time, so a restore that remounts every pane at once shows what it had rather than re-querying per terminal, and a panel whose process was replaced looks up its own children instead of inheriting the dead session's.
Process Icons
A terminal shows an icon for whatever is running inside it. Start node and the generic terminal icon becomes a Node.js icon; run pytest and it becomes pytest. This works everywhere a terminal appears: the panel header, the tab bar, the dock, the worktree sidebar, and the quick switcher.
The detector walks each terminal's child process tree and matches running executables against one registry of about 60 tools: package managers, language runtimes, compiled-language toolchains, Python and JavaScript tooling, build systems, containers, cloud and tunnel CLIs, databases, and terminal tools. Agents are merged on top from the agent registry, and plugins can contribute their own detections through contributes.processTools.
When several processes match at once, precedence runs tool > runtime > package manager. That ordering is deliberate and it is the opposite of what you might expect: a package manager or a language runtime is usually a launcher for the thing you actually care about. node /path/to/vite.js reports Vite rather than Node, and npm run dev reports whatever the script started, with npm as the fallback only when nothing identifiable runs beneath it.
Dedicated agent panels are the exception: they always show their own brand icon whatever is running inside them, so an agent that shells out to npm install doesn't briefly become an npm panel.
Search
Command-F Control-F Control-F opens the search bar at the top of the focused panel with a "Find in terminal" input. Matches highlight as you type, debounced so a fast typist doesn't fire a search per keystroke. The counter reads "3 of 12" once you're stepping through matches, "12 matches" before you are, or "No matches".
Toggles. Three buttons, freely combined: Aa for case sensitivity, .* to treat the query as a regular expression, and an underlined ab for whole-word matching. Case-sensitive and regex persist across searches; whole word resets each time you reopen the bar.
Overview ruler. A thin strip beside the scrollbar marks every match across the whole buffer, with a brighter tick for the active one, so you can see how matches are distributed without scrolling.
Navigation. Enter or the down chevron goes to the next match, Shift+Enter or the up chevron to the previous. Navigation wraps. Escape closes the bar.
History. With the input focused, ArrowUp and ArrowDown cycle through recent searches. Anything you've typed but not yet recalled is held as a draft, so stepping back to it loses nothing.
Scroll Navigation
When you scroll up to read earlier output, Daintree tracks whether new content has arrived since you left the bottom.
"New output below." A floating pill appears at the bottom of the panel when you're scrolled back and new output has actually arrived: scrolling up on its own doesn't trigger it. Click it to jump to the bottom and take keyboard focus. It keeps clear of drag-to-select and right-click menus, and respects reduced-motion preferences.
Scroll to last activity (Option-Command-L Control-Alt-L Control-Alt-L ) jumps to the line where output was most recently written, which is not the same as the bottom. If an agent produced output 500 lines up and a prompt sits at the end of the buffer, this takes you to the output. Press it twice in a row to land at the absolute bottom.
Links in Output
Daintree scans terminal output for things worth clicking, and validates candidates against the filesystem before turning them into links, so a slash-command like /help never becomes one.
| What | Click does |
|---|---|
File paths, with optional :line:col | Opens the file in a File Viewer at that position. |
file:// URLs | Same as a file path. Agent CLIs print generated images this way. |
| Directory paths | Opens the File Browser, revealing that folder. |
[image #N] references | Jumps to that figure; a modified click opens it in a lightbox. |
http / https URLs | Opens in your default browser. |
| A localhost URL, Cmd/Ctrl-clicked | Opens it in an in-app Browser panel in the same worktree, reusing an existing one if there is one. |
A file link pointing outside your project roots won't open in place: the containment rules exist for a reason. Instead the failure toast offers Reveal in File Manager, since you clicked it deliberately and the file is real, just elsewhere. Every other failure offers Copy path. A burst of bad links from a scrolled stack trace collapses into one toast rather than twenty.
Clipboard
Terminal panels support VS Code-style copy and paste from the keyboard, the right-click menu, and the clipboard image path.
| Action | Shortcut |
|---|---|
| Copy selection | Cmd+C |
| Paste | Cmd+V |
Cmd+C copies the current terminal selection even when the hybrid input bar has focus, so you don't have to click into the output first.
| Action | Shortcut |
|---|---|
| Copy selection | Right-click > Copy |
| Paste | Ctrl+Shift+V |
Ctrl+C sends an interrupt to the running process, so copying goes through the context menu. Paste is Ctrl+Shift+V, matching the VS Code terminal convention.
Daintree picks the best available shell: pwsh.exe, then powershell.exe, then cmd.exe. Each launches in UTF-8 (PowerShell with -NoLogo and a UTF-8 bootstrap, cmd.exe with a silenced chcp 65001), so box-drawing characters and emoji render correctly with no startup banner.
| Action | Shortcut |
|---|---|
| Copy selection | Right-click > Copy |
| Paste | Ctrl+Shift+V |
| Copy on select | Select text (writes to PRIMARY) |
| Paste from selection | Middle-click |
Ctrl+C sends an interrupt to the running process, so copying goes through the context menu. Paste is Ctrl+Shift+V.
Daintree follows the X11 and Wayland selection conventions: selecting text writes to PRIMARY on mouse release, and middle-click pastes from PRIMARY into the PTY, bracketed-paste wrapped where the shell supports it. Middle-click paste is disabled while a panel's input is locked. Copy-on-select fails silently on minimal Wayland compositors that don't implement the primary-selection protocol; the keyboard and menu paths still work there.
Pasted text is wrapped for shells that support bracketed paste, so multi-line content doesn't execute before you press Enter.
Clipboard images. Paste a screenshot into a terminal and Daintree writes it to a PNG in a temporary directory, then inserts the shell-escaped absolute path at the cursor. Pasting into the hybrid input bar does the same and adds a thumbnail preview chip inline. Clipboard images are cleaned up after 24 hours.
Right-click menu. Right-clicking a terminal opens a menu with Copy, Paste, and Send to Agent at the top; Copy and Send to Agent are disabled with no selection. Right-clicking a detected link adds Open Link and Copy Link Address. Shift-F10 Shift-F10 Shift-F10 opens the menu from the keyboard. The full reference is on Palettes & Menus.
Drag and Drop
Drag files from Finder, Explorer, the File Browser, or the Review Hub onto a terminal, and what gets inserted depends on what is running in it:
- An agent gets an
@filetoken, spelled relative to the terminal's live working directory, falling back to an absolute path for anything out of tree. That is the same reference the@autocomplete and the input bar produce, so a file means the same thing however you attach it. - A plain shell gets a shell-escaped absolute path, which is what a shell can actually consume.
Delivery follows the terminal's live bracketed-paste mode. Agent CLIs read raw stdin and can't tell an injected byte from a typed one, so an unwrapped @ would drive their own interactive file picker character by character; wrapping the batch marks it as a paste instead. A drop never appends a carriage return: it inserts text, it doesn't submit.
Dragging over a terminal shows drop feedback, and that feedback is suppressed while the panel's input is locked rather than advertising a target that would discard the drop. Paths containing a carriage return, newline, or escape character are skipped: all three are legal in a filename and none of them can be delivered safely. See Unified Input for the rest of the attach surfaces.
Type-Anywhere Rescue
Typing a prompt into a window that has quietly lost focus is a small, constant annoyance. Daintree handles it in two ways, from one listener, and the two are deliberately asymmetric.
Locate. If a terminal does have focus but has scrolled out of view, the keystroke goes where it always would (the event is never touched) and Daintree scrolls that panel back into view and pulses it, so you can see where your typing is landing.
Rescue. If nothing useful has focus at all (the document body, the chrome of a read-only panel), the character is routed into the input draft of the last agent you typed to, and focus moves there. With no typing history yet, it falls back to the sole open agent; with several open and no history, it refuses rather than guessing.
Nothing is ever submitted. The character lands in a draft you can see and edit. Rescue is gated hard: it only fires on a plain printable keystroke, never during IME composition, never while a modal is open, never into a locked or restarting panel, and never into a plain shell: routing a draft into a shell would execute it on Enter.
Line Editing
Word-by-word cursor movement. On macOS, Option+← and Option+→ jump the cursor a word at a time in the shell line editor. Daintree emits the meta sequences ESC b and ESC f rather than xterm's default modifier arrows, because neither zsh's ZLE nor bash's readline binds the modifier arrows to word motion: out of the box, the keys would do nothing. This applies to the normal buffer only: a full-screen TUI decodes the modifier arrows itself and would only be confused by the substitution.
Hybrid input bar. An optional text field above each terminal, giving you ordinary editing (selection, cursor movement, clipboard, multi-line) before the input reaches the PTY. Toggle it in Settings > Panel Grid, with an optional auto-focus. It is also the surface the type-anywhere rescue routes into, so turning it off disables the rescue.
Terminal Info
Every terminal has a diagnostic dialog covering how it was spawned, its PTY internals, and its runtime statistics. Use it to check which flags an agent launched with, confirm a TTY device path, or grab a dump for a bug report. Open it from the panel header's right-click menu (View Terminal Info) or the command palette; there is no dedicated shortcut. It is offered only for terminals: browser, dev preview, review, and the file panels don't expose it.
| Section | Contains |
|---|---|
| Session Metadata | Terminal ID, kind, title and title mode, project and worktree IDs, current directory, location, spawn source, whether it was started via MCP, spawn status, and creation time. |
| Spawn Command | The shell path, the command, and every argument as its own selectable chip. The easiest place to confirm an agent launched with the flags you expected. |
| Agent Launch Context | Agent panels only: launch agent, launch flags, model, preset, preset color, and the original preset if it was changed. |
| Agent Live State | Agent panels only: the currently detected agent and its session ID. |
| Terminal Classification | Whether it carries an agent launch hint, whether a PTY is active, whether activity analysis is on, and the resize strategy. |
| PTY Diagnostics | Dimensions, shell PID, TTY device, foreground process, and exit code once it has exited. |
| Runtime Statistics | Running time, spawn timestamp, and restart count, useful when auto-restart or a crash loop is in play. |
| Activity Metrics | Last input and last output, the agent state, the last state change, and the current refresh tier. |
| Performance & Diagnostics | Output and semantic buffer sizes in lines, plus whether synchronized output (DEC 2026) is active. |
The footer's Copy to Clipboard button produces a structured plain-text dump of every section, ready to paste into a bug report.
Closing and Resuming
Closing a terminal ends its process. Closing an agent session records it in the session journal, so Command-K, then Command-R Control-K, then Control-R Control-K, then Control-R can bring it back with its conversation intact where the agent supports resume. Shift-Command-T Control-Shift-T Control-Shift-T reopens the last closed panel as a fresh session in the same configuration. What survives a close, a restart, and a project switch is covered on Session Management.