fix: switch Ghostty tabs on banner click#47
Merged
Conversation
Click-to-focus on a Ghostty event banner now activates the .app AND
switches to the source project's tab — not just brings Ghostty
forward. Same UX as iTerm/Terminal but via Ghostty's scripting
dictionary instead of the AX walk (which doesn't work — Ghostty is
GPU-rendered and exposes no AXWindows to other processes; this was
the path I tried first and abandoned).
How it works:
- bundleID == com.mitchellh.ghostty branches off the standard AX
path in AppActivator.activate to focusGhosttyTab(projectPath:).
- That runs an NSAppleScript that iterates `windows → tabs →
terminal` and matches each terminal's `working directory` against
the event's projectPath. First match wins; `tell w to select tab t`
is the click-equivalent that switches the tab.
- String comparison uses `as text` coercion on both sides because
`working directory` returns a URL/alias type that `is` would
otherwise compare unequal to a plain string even when the visible
text matches. `contains` is a defensive fallback for
Unicode-normalisation drift (NFC vs NFD).
- Requires Ghostty 1.3.1+ for the scripting dictionary and the
activation-on-select fix.
Entitlement:
Added com.apple.security.automation.apple-events to entitlements.plist.
Without it, hardened-runtime apps cannot send Apple events at all —
NSAppleScript bails with errAEEventNotPermitted (-1743) before macOS
even surfaces its consent prompt. Now the user gets the standard
"StackNudge would like to control Ghostty" dialog on first banner
click; subsequent clicks use the persisted grant.
Known limit:
When multiple tabs share the same project cwd, AppleScript matches
the first one in the enumeration — which may not be the tab the
event originated from. Tiebreaker via captured tab id on the wire
is queued as a follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a TODO-style comment next to the bundle-ID case explaining why we don't capture a Ghostty tab id in the wire payload yet, and what needs to change upstream before we can. Attempted a tab-id capture via `id of terminal of selected tab of front window` and decided against it: `selected tab` reflects the user's current focus, not the agent's tab, so it produced wrong results in the very case the tiebreaker is meant to solve (user switched away before the hook fires). Real fix needs Ghostty PR #11922 (pid on terminals, targeted at 1.4.0); then we match agent_pid in AppActivator directly. Note in the source so future-us doesn't re-attempt the same dead end. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Click-to-focus on a Ghostty event banner now activates Ghostty AND switches to the source project's tab — same UX as iTerm and Terminal.app — using Ghostty 1.3+'s AppleScript dictionary.
Why
Ghostty's a GPU-rendered terminal that doesn't implement NSAccessibility on its windows. The standard AX walk we use for iTerm/Terminal/Warp gets back zero AXWindows from Ghostty, so the existing click-to-focus would only
NSRunningApplication.activate— bring the app forward without touching the tab. Same-window multi-tab users landed on whatever tab they'd last viewed, not the source.PRs in the Ghostty repo confirm this is by design today; PRs #7601 / #10992 / #11196 add only read-only AX text for screen readers. The maintainer-blessed external-control surface is the AppleScript dictionary landed in 1.3.0 + activation fix in 1.3.1.
What's in the branch
0cc97b9 — Branch on `bundleID == com.mitchellh.ghostty` to a Ghostty-specific path in `AppActivator`. `focusGhosttyTab` runs an NSAppleScript that enumerates `windows → tabs → terminal`, matches each terminal's `working directory` against the event's `projectPath` (exact then `contains` fallback), and `tell w to select tab t` on the first hit.
Adds `com.apple.security.automation.apple-events` to the entitlements. Hardened-runtime apps without this entitlement get `errAEEventNotPermitted (-1743)` before macOS even surfaces the consent prompt — so the user has no way to grant access. With it, the standard "StackNudge would like to control Ghostty" dialog appears on first banner click.
a8d79a7 — TODO comment documenting why the same-cwd disambiguation isn't implemented. We tried capturing `id of terminal of selected tab of front window` at hook-fire time but `selected tab` is the user's current focus, not the agent's tab — so when the user switched away before the hook fired (common), we recorded the wrong id. Ghostty PR #11922 adds `pid` to terminals (target 1.4.0); once it ships we'll match `agent_pid` directly.
Known limit
When two Ghostty tabs are open in the same project cwd, the cwd match returns whichever tab is first in the enumeration. Falls under the "wait for Ghostty 1.4 + match by PID" path documented inline.
Required first-launch consent
First time a user clicks a banner whose event came from Ghostty, macOS pops a "StackNudge would like to control Ghostty" dialog. User accepts once; subsequent clicks use the persisted grant. Same pattern as our existing AX prompt for terminal apps.
Test plan
🤖 Generated with Claude Code