The gate question before any day-plan design: can Ghostty be scripted to open a tab with two vertical panes, each in its own directory, with a command running in one? Measured against the installed 1.3.1 rather than reasoned about, because this is undocumented platform behavior. Yes, but not through the documented API. new window / new tab / split return correctly shaped objects that never spawn a shell - nine probe windows, shell count flat at 29 - and every call that needs the process fails with "Terminal surface model is not available". That is issue #12730, a 1.3.1 regression closed as not planned, and 1.3.1 is the newest release. surface configuration is collateral: all four of its useful properties ride that path. What does work is perform action on an ALREADY REALIZED surface. new_tab and new_split produce live shells (29 -> 31), and input text + send key drives them - two panes cd'd to different repos reported distinct directories and distinct PIDs. The working directory becomes a typed cd, so the broken path is not needed at all. Three constraints fell out of the measurement and are recorded because each was found by breaking something: pane targeting must diff tab ids, since last tab of w is not the new tab and a probe that trusted position typed into a tab it did not create; new tabs inherit the anchor's directory; and count of terminals is not a pane count. Recommends splitting the work - the plan here as a third rendering of the board scan, the Ghostty driver in a separate repo. The argument is the measurement: a preview API that breaks in 1.4, whose documented path is already broken, with a blast radius reaching into other repos' live sessions, does not belong behind the mailbox. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GfDGWyyhnM26J4p93GSk2L
145 lines
7.4 KiB
Markdown
145 lines
7.4 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.
|
|
|
|
### 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.
|