TTYbird can find Claude and Codex processes and return to an existing Ghostty terminal by its AppleScript UUID. Preview is a separate problem: how can TTYbird show output from a terminal whose PTY it does not own?
I first tested Ghostty through macOS Accessibility, or AX. That probe proved that a known AX text area exposes Unicode and scrollback, but it did not provide stable UUID-to-AX identity, a viewport-only value, or output-change notifications. Source review then revealed a different route: Ghostty can run write_screen_file:copy,vt on an exact AppleScript terminal UUID.
Correction, September 16, 2026: The first version overstated the need for a Ghostty change and described the UUID export route as unverified. TTYbird now implements that route as a selection-driven one-shot snapshot. Native checks passed for selected and hidden tabs, new hidden-tab output, an original pane and a new split, and a closed UUID. The implementation is useful, but it is still a scrollback-inclusive output snapshot with clipboard and temporary-file side effects. It is not an exact viewport mirror or an event-driven live feed.
Return, discovery, and preview remain separate. TTYbird discovers processes without AX. An unbound Ghostty session still requires the user to pair the matching terminal, and TTYbird reuses that UUID binding only while process identity matches. Selecting a supported, mapped local Ghostty terminal opens its preview and captures that target once. Moving to another mapped Ghostty terminal captures the new selection once. Leaving the selection unchanged causes no periodic export; r refreshes it, and p toggles the preview. An unbound session shows session information instead of guessing a terminal. Existing tmux and TTYbird-owned terminal previews keep their live behavior.

This is an architectural diagram of the AX experiment, the implemented export path, and a possible viewport extension. It is not a screenshot.
What I tested
Both test series used Ghostty 1.3.1, build 15212 on macOS and only synthetic windows, tabs, splits, and processes. They did not capture working terminals or model sessions. The AX logs contain measurements and owned identifiers but no terminal text. The export evidence contains booleans, timings, cleanup counts, and source hashes without terminal or clipboard contents.
| Time (JST) | Checkpoint | What changed or was learned |
|---|---|---|
| 14:27 | Scope and baseline | Preserved existing work and limited the AX probe to synthetic terminals. |
| 14:27–14:31 | Environment failure | Compilation initially failed because the Nix SDK and Swift toolchain did not match. Restricting the probe to the Xcode toolchain fixed the environment; this was not a Ghostty API failure. |
| 14:34 | First device run | AX returned ordinary text, Unicode, and scrollback from a Ghostty text area. |
| 14:36 | Lifetime and cleanup failure | A split invalidated the old AX reference. Exiting a fixture process also left a pane behind, so the initial cleanup criterion failed. |
| 14:37–14:38 | Measurement correction | Timing moved to the fixture's output acknowledgement. A retained reference could read a background tab, but the selected tab's AX tree no longer enumerated that tab. |
| 14:38 | Explicit cleanup | Only terminals created by the probe were closed. |
| 14:39 | Corrected rerun | Split invalidation and close invalidation were separated, and cleanup completed. |
| 14:40–14:41 | Final AX run | A source-hashed run added Unicode range edges and API timings: 45 log records, not 45 passing tests; exit code 0; no remaining fixture process. |
| 14:42 | Independent cleanup check | All 19 terminal IDs created across the attempts were absent. |
| 14:47 | AX evidence review | The report, final JSONL, source hashes, and clean scope were cross-checked. |
| 15:29 | First export run | Native UUID export worked, but the harness read an acknowledgement file before it existed. Cleanup passed; the run was not counted as complete. |
| 15:29–15:31 | Corrected export runs | Selected, hidden, updated hidden, original-after-split, exact-split, and closed-UUID cases passed. Review then corrected VT rendering to use parse_vt and strengthened the closed-UUID assertion. |
| 15:35 | Final export evidence | All E01–E07 checkpoints passed, including temporary-file cleanup and byte-for-byte readback of the restored multi-format clipboard fixture. |
The sanitized records are AX evidence.json and Ghostty export evidence.json.
What AX proved
Ghostty 1.3.1 exposes terminal surfaces as AX text areas. Its pinned AppKit implementation returns cachedScreenContents from accessibilityValue(). The cache calls Ghostty's text reader and lasts 500 ms.
The final probe read the oldest marker from 160 lines of history although the terminal initially showed 14 lines. It also read Japanese text, a combining sequence, and emoji. After acknowledged output, a read around 256 ms later was stale; one around 608 ms contained the update. That fits the cache design but establishes no latency bound.
With 306,612 characters, the AX call took 2.38–6.85 ms and the whole probe took 38.14–61.24 ms across three samples. This is not a rendering or continuous-CPU benchmark. The AX reader was not integrated into TTYbird, and minimized-window behavior, resize semantics, and continuous polling cost remain untested.
AXVisibleCharacterRange did not mean the visible viewport. It reported the full cached string, including scrollback. Ghostty's pinned point definition distinguishes the viewport from the screen range, which extends through retained scrollback.
The test also exposed a Unicode boundary hazard. AX reported 2,482 characters while the returned string occupied 2,488 UTF-16 code units. Requesting the reported length omitted the last six UTF-16 units; requesting 2,488 returned the full string. An AX client must treat these as UTF-16 ranges.
AX identity and notifications remain limited. The tested text area had no Ghostty UUID attribute. Titles, directories, order, and focus are unsafe substitutes, and a split can replace an AX element. The probe received kAXErrorInvalidUIElement after a split and after closing a tab; a new tab with the same title and directory did not revive the old reference.
Registration for AXValueChanged succeeded, but the probe observed zero value-change events during three output phases. Two title-change events proved that the observer received events from the expected Ghostty window. Apple's AX observer API registers interest; the application still has to post the notification. This single run does not prove that every version can never emit value changes, but it does not support calling this AX path event-driven.
The UUID export path is now implemented
Ghostty's scripting definition exposes a read-only terminal ID and a targeted perform action. Its AppleScript handler resolves terminal id "..." across live surfaces and dispatches the action to that exact terminal. Focus is a separate command; the export route does not call it.
TTYbird now uses write_screen_file:copy,vt when a supported mapped Ghostty target becomes the selected preview, or when the user presses r to refresh it. Ghostty writes screen.txt in a private random temporary directory and puts its path on the general clipboard. The pinned export implementation supports plain, VT, and HTML, selects the screen range including retained scrollback, and unwraps lines. TTYbird validates the UUID, the private path shape, ownership, regular-file mode, and symlink boundaries before reading at most 1 MiB.
The bytes go directly to libghostty-vt through its complete-VT parser. TTYbird renders at 120 columns and keeps the latest 200 rendered rows. This is not the original viewport: the cursor, original wrapping, scroll position, and exact window geometry are not reproduced. Selection starts one capture; it does not start polling. The same selection is not exported again until the user presses r or moves away and selects it again.
The final native driver used the real Ghostty application and the real Rust adapter. It confirmed:
- the selected tab exported without changing the selected tab;
- a hidden tab returned newly acknowledged output rather than a stale selected-tab snapshot;
- the original terminal and a newly created split were addressed separately by UUID;
- a closed UUID returned the exact expected error;
- every owned temporary export was removed, and the closed UUID left no export behind;
- the seeded plain-text and custom binary clipboard representations were restored and matched on readback.
The native driver does not prove behavior on other Ghostty or macOS versions. It checked selected-tab identity before and after each capture, but it did not independently observe Ghostty's internal split-focus state. The dashboard interaction keeps this capture bounded: selecting a supported mapped Ghostty target requests one snapshot, changing to another requests one for the new target, an unchanged selection does not poll, r refreshes, and p toggles the panel. A transport test can verify that state machine with a fake AppleScript endpoint, but it is not additional native GUI proof.
Clipboard and cleanup boundaries
The adapter keeps up to 4 MiB of clipboard representations in memory, serializes TTYbird exports with a per-user lock, and restores only after the expected pasteboard change and exact Ghostty path remain current. No clipboard backup is written to disk. The temporary terminal export is deleted after successful capture and is never included in collection JSON, logs, or session history. Normal dashboard shutdown waits for bounded capture cleanup.
This is careful rather than atomic. macOS provides no compare-and-restore operation for the general pasteboard. A user or another application can copy in the small gap between the final change-count check and restoration. If the helper is terminated after Ghostty writes the path, a crash or the 10-second subprocess timeout can leave that path on the clipboard and can leave the private export file behind. SIGKILL cannot run cleanup. The feature therefore performs one capture per selection or explicit refresh rather than running a background preview loop.
What still belongs upstream
TTYbird can provide a useful existing-tab snapshot without AX identity mapping, Ghostty IPC, or output notifications. It can also own the permission flow and conservative failure behavior. New sessions launched through TTYbird remain different: TTYbird owns those PTYs and can maintain terminal state directly.
Ghostty cooperation would still improve the feature. The smallest useful extension is a bounded AppleScript getter for a UUID-addressed viewport or a direct bytes/path result that avoids the shared clipboard. That would make exact viewport semantics and side-effect-free capture possible without requiring a new pub/sub system. Revisions or coalesced notifications could later reduce polling cost if a live preview becomes necessary.
The corrected conclusion is narrower than either extreme. AX can read useful text but lacks stable identity and output events in the tested version. The UUID export route now works for bounded snapshots of selected, hidden, and split terminals, with verified cleanup and clipboard restoration in the successful native run. Its remaining races and snapshot semantics are product limits, not proof that existing-tab preview is impossible or that a large Ghostty protocol is required.