The measurement identified realized surfaces by counting shell processes, which is what was available while probing. morning-driver, building against this document, found a cheaper and stronger test in the sdef: `working directory` is readable on a terminal, and a surface with no shell process reports it EMPTY - exactly the ghost signature the broken creation API produces. That turns "has this surface been realized" from an inference into a machine-checkable question, which is the guard anything driving Ghostty needs before it types into a pane. The same property answers one of the document's own open questions: it goes empty when a shell exits and returns when the prompt is back, so "has this pane returned to a prompt" can be polled rather than guessed at with a delay. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011eKRzhD1RqY2MxvDEkrJAM
160 lines
8.3 KiB
Markdown
160 lines
8.3 KiB
Markdown
# Driving a day plan into Ghostty: what is actually possible on 1.3.1
|
|
|
|
Measured 2026-08-01/02 against the installed terminal, not reasoned about.
|
|
This document exists to answer one gate question before any design: **can
|
|
Ghostty be scripted to open a tab with two vertical panes, each in its own
|
|
directory, with a command running in one?**
|
|
|
|
Short answer: **yes, but not through the documented API** - the documented path
|
|
is broken in the installed version, and the path that works is an undocumented
|
|
composition of primitives that the maintainer has labelled preview.
|
|
|
|
## The environment being measured
|
|
|
|
- Ghostty **1.3.1** (stable, released 2026-03-13). This is the newest release;
|
|
there is no 1.3.2 and no 1.4.
|
|
- Config: `working-directory = inherit`, `window-save-state = always`,
|
|
`shell-integration = zsh`, `macos-titlebar-style = tabs`.
|
|
- Operator's workspace: one window, 13 tabs, 2 vertical panes per tab.
|
|
|
|
## What was measured
|
|
|
|
| Mechanism | Realizes a surface? | Splits? | Can start a command? |
|
|
|---|---|---|---|
|
|
| AppleScript `new window` / `new tab` / `split` | **No** | structure only, panes blank | no |
|
|
| `ghostty +new-window` | not available on macOS | - | - |
|
|
| `open -na Ghostty.app --args ...` | yes (26 shells) | yes, whole layout | **no**, and it duplicates everything |
|
|
| `perform action` on a **realized** surface | **Yes** | yes | **yes** |
|
|
|
|
### 1. The documented API is broken
|
|
|
|
`new window`, `new tab` and `split` return objects with the correct structure -
|
|
`count of tabs` and `count of terminals` are exactly what was asked for - but no
|
|
shell process is ever spawned. Nine probe windows were created across the
|
|
session; the count of `login ... exec -l /bin/zsh` processes stayed at 29
|
|
throughout. The windows are visible and blank, and the tabs carry the
|
|
placeholder ghost emoji as their name.
|
|
|
|
Any subsequent call that needs the process fails:
|
|
|
|
execution error: Ghostty got an error: Terminal surface model is not
|
|
available. (-10000)
|
|
|
|
This is [issue #12730](https://github.com/ghostty-org/ghostty/issues/12730),
|
|
"AppleScript: new tab via 'new tab' creates empty tab without shell process
|
|
(1.3.1 regression)", **closed as not planned with no maintainer comment**. The
|
|
independent measurement here matched the report on every detail including the
|
|
ghost emoji and the full set of failed workarounds (delays up to 4s, explicit
|
|
focus, explicit `activate`, every surface-configuration variant).
|
|
|
|
The likely cause is stated in the issue and is consistent with the 1.3.1 release
|
|
notes: the fix for
|
|
[issue #11457](https://github.com/ghostty-org/ghostty/issues/11457) (AppleScript
|
|
implicitly activating the app) appears to have removed the step that also
|
|
initialized the shell.
|
|
|
|
**Consequence: `surface configuration` is unusable.** Its four useful properties
|
|
- `initial working directory`, `command`, `initial input`,
|
|
`environment variables` - all ride the broken creation path. None of them had
|
|
any measurable effect: no process, no file written, no directory set.
|
|
|
|
**A ghost surface can be detected in one property read, and this is the cheapest
|
|
guard against the whole failure class.** `working directory` is readable on a
|
|
terminal, and a surface with no shell process reports it EMPTY - that is exactly
|
|
the ghost signature. So "has this surface been realized" is a machine-checkable
|
|
question, not something to infer from counting `login ... exec -l /bin/zsh`
|
|
processes as this measurement did. Anything driving Ghostty should wait for a
|
|
new surface to report a working directory before typing into it, and refuse to
|
|
type into one that never does.
|
|
|
|
Contributed by `morning-driver` (2026-08-02) from the sdef plus its own
|
|
measurement, after building against this document. The same property answers a
|
|
question left open below: it goes empty when a shell process exits and returns
|
|
when the prompt is back, so "has this pane finished and returned to a prompt"
|
|
can be polled rather than guessed at with a delay.
|
|
|
|
### 2. `open -na` works and is still the wrong tool
|
|
|
|
`open -na Ghostty.app --args --working-directory=...` produced 26 live shells in
|
|
the correct 13-tab / 2-pane layout. It does this by launching a **second
|
|
Ghostty instance** (pid 80818 alongside the operator's pid 519) which then
|
|
restores the saved session, because `window-save-state = always`.
|
|
|
|
That makes it useless for a day plan on two counts: it duplicates the entire
|
|
existing workspace rather than opening a chosen subset, and session restore
|
|
restores layout and working directories but never running programs - the panes
|
|
came up as bare shells with no Claude Code. It also leaves the operator with two
|
|
instances of their whole environment.
|
|
|
|
### 3. The path that works: actions on realized surfaces
|
|
|
|
The AppleScript *creation commands* are broken. The AppleScript *action*
|
|
mechanism is not, and `new_tab` / `new_split` are both in
|
|
`ghostty +list-actions`. Performing them **on a surface that is already
|
|
realized** produces surfaces that are themselves realized:
|
|
|
|
perform action "new_tab" on <realized terminal> -> +1 live shell
|
|
perform action "new_split:right" on <realized term> -> +1 live shell
|
|
|
|
Measured: 29 -> 31 shells, tab count 13 -> 14, and the operator confirmed a live
|
|
prompt in the new tab.
|
|
|
|
`input text` + `send key "enter"` then works on those panes. Two panes were
|
|
driven to `cd` into different repositories and report `pwd` plus their own PID;
|
|
both wrote distinct directories and distinct PIDs. **This is the full chain, and
|
|
it needs no `surface configuration` at all** - the working directory becomes a
|
|
typed `cd`, and the command becomes typed text.
|
|
|
|
## Three constraints any implementation inherits
|
|
|
|
These came out of the measurement and are not negotiable design details.
|
|
|
|
1. **Never target a pane by position.** `last tab of w` is not the tab that was
|
|
just created; new tabs are inserted relative to the current tab, not
|
|
appended. A probe that trusted position typed into a tab it did not create.
|
|
The new tab must be captured by diffing tab `id`s before and after. This is
|
|
the failure mode with the worst blast radius: the text lands in whatever
|
|
program that pane is running, which may be another repo's live Claude Code
|
|
session.
|
|
2. **A new tab inherits the anchor pane's working directory**
|
|
(`working-directory = inherit`), so every pane must be `cd`-ed explicitly.
|
|
Inheriting silently produces a plan that looks right and points at the wrong
|
|
repo.
|
|
3. **`count of terminals` is not a reliable pane count.** Probe tabs reported 3
|
|
and 4 terminal objects where 2 shells existed. Panes must be addressed by the
|
|
ids captured at creation, never enumerated after the fact.
|
|
|
|
## Recommendation
|
|
|
|
**Both paths, split across two homes - and the split is the recommendation, not
|
|
a hedge.**
|
|
|
|
- **The plan itself - which repos, in what order, with which startup command -
|
|
belongs here**, as a third rendering of the scan `board.sh` already performs,
|
|
built with exactly the argument `--brief` was: it is a lookup over data the
|
|
scan already holds, it costs zero model calls, and `route.sh` already derives
|
|
the per-repo command. Printing that plan as pasteable commands is path (a),
|
|
and it is complete on its own.
|
|
- **Driving Ghostty belongs in a separate repo that consumes the plan.** The
|
|
measurement is the argument: this is a version-pinned, undocumented
|
|
composition on top of an API the maintainer calls a preview and expects to
|
|
break in 1.4, whose documented path is already broken and whose regression was
|
|
closed as not planned. That is a maintenance surface with its own release
|
|
cadence, its own failure modes, and a blast radius that reaches into other
|
|
repos' live sessions. None of that is mailbox transport, and none of it should
|
|
be able to break `coord-inbox` or `board`. That `board.sh` lives here is not
|
|
an argument for Ghostty automation living here.
|
|
|
|
The dependency runs one way: the driver consumes the plan, the plan never knows
|
|
a terminal exists. If 1.4 breaks the driver, the day plan still prints and the
|
|
operator still pastes.
|
|
|
|
## Not measured
|
|
|
|
- Whether `input text` is safe against a pane running a full-screen TUI.
|
|
- Whether 1.3.0 is unaffected. The issue calls this a 1.3.1 regression, which
|
|
implies it, but downgrading a daily driver to gain a preview API that changes
|
|
in 1.4 was not evaluated as a serious option.
|
|
- How "focus" should be expressed, how many tabs a day plan should propose, and
|
|
whether it should remember yesterday. These were deferred until the gate
|
|
question was answered; they are now open.
|