Skip to content

fix: switch Ghostty tabs on banner click#47

Merged
hiskudin merged 2 commits into
mainfrom
feat/tab-switch-on-banner-click
May 20, 2026
Merged

fix: switch Ghostty tabs on banner click#47
hiskudin merged 2 commits into
mainfrom
feat/tab-switch-on-banner-click

Conversation

@hiskudin

Copy link
Copy Markdown
Collaborator

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

  • Run a Claude session in Ghostty tab A (project X), open another Ghostty tab B (project Y), switch to tab B → fire Stop event from tab A → click banner → land on tab A ✓
  • Same flow but click banner from a different app (Zed, Slack) → Ghostty raises + correct tab selected ✓
  • Run a session in Terminal.app or iTerm → existing AX path still works (no regression on the non-Ghostty bundles) ✓

🤖 Generated with Claude Code

hiskudin and others added 2 commits May 20, 2026 11:51
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>
@hiskudin hiskudin changed the title feat: switch Ghostty tabs on banner click fix: switch Ghostty tabs on banner click May 20, 2026
@hiskudin hiskudin merged commit 8df1405 into main May 20, 2026
4 checks passed
@hiskudin hiskudin deleted the feat/tab-switch-on-banner-click branch May 20, 2026 12:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant