Skip to content

feat(): handle terminal & ide context#55

Merged
hiskudin merged 2 commits into
mainfrom
feat-handle-terminal-ide-context
May 22, 2026
Merged

feat(): handle terminal & ide context#55
hiskudin merged 2 commits into
mainfrom
feat-handle-terminal-ide-context

Conversation

@StuBehan
Copy link
Copy Markdown
Collaborator

@StuBehan StuBehan commented May 21, 2026

Summary

Sessions and events now reflect which terminal/IDE and which tab they came from. Per-tab session identity for iTerm2, Terminal.app, VSCode/Cursor/Antigravity, Warp, and Ghostty — each gets its own renamable name, accent color, and chip in the events row. Adds first-class recognition of Antigravity (IDE + agy CLI) and wires automated hook installation for Gemini + Antigravity alongside the existing Claude path.

Changes

Terminal integration

  • New TerminalIntegration protocol (panel/TerminalIntegration.swift) + TerminalRegistry. SessionStore's poll runs every registered integration on the background queue; main thread no longer sees subprocess spawns.
  • ITerm2Integration — AppleScript snapshot of every iTerm session, cached 15s, joined to live sessions by tty (batched ps call, one subprocess for N sessions). Tab name flows through from notify.sh via a new wire field iterm_tab_name.
  • TerminalAppIntegration — mirror of the iTerm one for Terminal.app. tabId is the tty path (no stable UUID exposed); tabName is custom title when set.
  • VSCodeIntegration — bi-directional. Events teach the cache (ipc_hook → window title); Sessions read VSCODE_IPC_HOOK_CLI from each agent's env via ps eww and join to the cache. Recognises Code, Cursor, and Antigravity helpers (all VSCode forks).
  • EnvVarTerminalIntegration — generic parameterised conformer. Warp and Ghostty both expose TERM_SESSION_ID; one implementation, two registrations.
  • ProcessOutput — shared subprocess helper so the integrations and SessionStore agree on the "swallow stderr, return empty on failure" contract.

Per-tab identity

  • Session gains tabId + tabName. SessionPersistence keys widen to agent::path::tabId with backward-compat fallback to agent::path (pre-Stage-2 renames still apply across every tab in a project until a per-tab override is written).
  • SessionColor mixes tabId into the FNV-1a hash when present — two iTerm tabs in the same cwd get distinct accent colors. Nil tabId reproduces pre-existing colors so existing users see no shuffle.
  • EventRow uses event.sessionID for iTerm and event.ipcHook for VSCode-fork events as its tabId. SessionsView.matches treats either as the event-side tab id when cross-referencing event-to-session counts.

Antigravity + agy

  • notify.sh + SessionStore recognise Antigravity helpers as terminals and agy as an agent binary (direct + node/deno/bun-hosted).
  • Agent.canonical collapses antigravity / antigravity-cli to agy so wire and process names stay in sync.
  • EventRow normalises Antigravity Helper (Renderer) etc. to Antigravity; same for Code HelperVSCode, Cursor HelperCursor, WarpTerminalWarp, ghosttyGhostty.

Events tab polish

  • Title row gains a small monospace chip showing the terminal that fired the event (Approve write · iTerm2 · auth · tests).

Hook installation

  • install.sh now wires Gemini CLI hooks the same way it does Claude (was previously a "manual setup" note). Also wires Antigravity CLI hooks (~/.gemini/antigravity-cli/settings.json).
  • uninstall.sh mirrors the additions, cleaning up stale entries.

Perf

  • iTerm enrichment moved off main thread (was ~150-250ms stall every 3s with ≥10 iTerm sessions). Batched ps -p P1,P2,… cut N subprocess spawns to 1. AppleScript cache bumped 3s → 15s (cuts osascript hits ~5x). NSLock added around the cache since it's now read from the background queue.

Tests

  • EnvVarTerminalIntegrationTests (7), VSCodeIntegrationTests (10) — parser correctness across single/multi/empty/garbage input, env var parameter respected, recognised-helper whitelist, event-fed cache → enrich loop via a testing seam.
  • SessionPersistenceTests (+6), SessionColorTests (+3) — tabId-aware lookups, generic → tabId fallback, override semantics, color stability with/without tabId, palette membership.
  • AgentTests (+1) — antigravity / antigravity-cli / agy three-way canonicalisation.

Testing

  • swift build + ./build.sh clean.
  • make reload, fired varied trial events covering all five terminals plus Antigravity + agy. Verified:
    • Per-tab accent colors (two iTerm tabs in same cwd → different colors; same ipc_hook in VSCode → same color).
    • Terminal chip renders correctly across iTerm2, Terminal, VSCode, Warp, Ghostty, Antigravity.
    • EventRow falls back to ipcHook when sessionID is missing.
    • Sessions tab scroll is smooth with 15 active iTerm sessions (was hitching every 3s pre-perf-fix).
  • Manual rename loop: renamed a running claude session in the Sessions tab → matching event rows updated immediately.
  • CI's test-macos job (swift test) runs the 27 new XCTest cases.

@StuBehan StuBehan requested a review from hiskudin May 21, 2026 00:24
Copy link
Copy Markdown
Collaborator

@hiskudin hiskudin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed end-to-end. Solid piece of work overall — the TerminalIntegration protocol + TerminalRegistry pattern is clean, the perf wins (off-main, batched ps) are real, and the parser test coverage on the new integrations is well-scoped. Comments below — the iTerm tabId mismatch is the one I'd block on; the rest are follow-ups.

🟠 Likely bug: iTerm tabId mismatch between event-side and session-side

EventRow.tabIdentifier uses event.sessionID, which comes from TERM_SESSION_ID (format w0t0p0:UUID). ITerm2Integration.query() writes unique id of s as tabId, which is just the bare UUID. They aren't string-equal, so:

  • Sessions.matches(): sessionTab == eventTab will be false for iTerm events, so the per-tab event counter gating won't work — falls through to the "either side nil" branch, masking the mismatch silently.
  • SessionColor.color(...): same tab renders with two different accent colors in the Events row vs. the Sessions row.
  • persistence.customName lookup in EventRow uses one key; the rename written from SessionStore.rename uses the other → renames don't surface on event rows.

Fix: normalise on one side. Either strip the w0t0p0: prefix from event.sessionID before using it as a tab identifier, or have iTerm's AppleScript return TERM_SESSION_ID's full value (needs verification — it's not the same surface as unique id of session). Worth a manual test: rename a session from the Sessions tab, fire a Stop event from that same tab, check the event row picks up the rename and uses the same accent color.

🟠 install.sh / uninstall.sh: ~100 lines of pure copy-paste

The Gemini and Antigravity Python blocks in install.sh are byte-identical except for one path arg. Same in uninstall.sh. Extract a shared shell function (wire_claude_shaped_hooks <settings_path> <agent>) that owns the heredoc once — same logic Bootstrap.swift already centralised internally with wireClaudeShapedHooks. As-is, the next "fix the stale-entry regex" change has to touch four places and stay in sync.

🟡 VSCode IPC-hook parser: substring-match risk pinned by test, not fixed

VSCodeIntegrationTests.test_parseIpcHooks_ignoresPrefixCollision documents that NOT_VSCODE_IPC_HOOK_CLI=/wrong.sock matches and returns /wrong.sock. The test pins the behaviour rather than guarding against it. It's hypothetical today but cheap to tighten — anchor on a leading space or start-of-rest. Same fix applies to EnvVarTerminalIntegration.parseEnvValues.

🟡 detect_iterm_tab_name adds 30–80ms to every notify.sh event

On a chatty agent (lots of PermissionRequests in a row), this is per-event osascript latency in the hook critical path. Two paths:

  1. Drop it from notify.sh entirely — the panel already enriches via ITerm2Integration.sessionsByTTY() on the session-discovery side; the event side could just carry TERM_SESSION_ID and let the panel resolve the human-readable name from its own cache (already populated within 15s of any agent-tab activity).
  2. Background-detach the osascript with a deadline so the hook returns before it completes.

Option 1 is cleaner — you've already paid for the cache.

🟡 tabNameFallback is dead code

VSCodeIntegration.tabNameFallback always returns nil with a comment explaining why. Either delete it and inline nil at the callsite, or actually use it. Current shape suggests it was once intended to do something — future contributors will wonder if it's broken.

🟡 No parser tests for iTerm2 / Terminal.app pipe-delimited output

EnvVarTerminalIntegration and VSCodeIntegration have nicely isolated parser tests. The iTerm2 and Terminal.app integrations parse pipe-delimited osascript output inline in query() — no equivalent test. Worth pulling that loop out as static func parseSessions(_ raw: String) -> [String: ITerm2Session] and adding the same edge-case coverage (multi-line, embedded | in tab name, trailing whitespace, empty trailing field).

🟢 Smaller things

  • TerminalAppIntegration.enrich calls ITerm2Integration.ttyMap(forPids:) — cross-class static reuse. Move ttyMap onto ProcessOutput or a dedicated TTY enum; right now TerminalAppIntegration is coupled to ITerm2Integration via a name that doesn't advertise the shared utility.
  • TerminalRegistry.integrations is a mutable static var for tests. Fine for now; if XCTest ever runs in parallel this will surprise someone. Worth a // test-only mutation; do not mutate at runtime comment, or move behind a function.
  • agent_label() in notify.sh maps agy|antigravity|antigravity-cli → "Antigravity", but Bootstrap wires notify.sh antigravity stop (passes antigravity as agentArg), so the agy and antigravity-cli branches are unreachable from the wired path. Harmless and defensive but worth a one-line comment so a future reader doesn't burn time tracing where agy arrives from.
  • New Session.tabId/tabName are var in a struct that's otherwise all let. SessionStore already routes mutations through with(...); for consistency you could keep them let.

Recommend

Land after fixing the iTerm tabId mismatch (real bug) and dedup'ing the install/uninstall heredocs (10-min cleanup, large maintenance win). Other items are fine as follow-ups.

Pre-public-launch prep. With GITHUB_TOKEN (current state), the tag-push
release-please-action makes when the Release PR merges does NOT fire
the tag-keyed Build and Release workflow — GitHub's anti-loop guard
suppresses downstream triggers from default-token pushes. Today we work
around it by manually dispatching the build against each new tag from
Actions → Run workflow.

A fine-grained PAT scoped to this repo (Contents + Pull requests +
Workflows all read+write) makes the action's tag push look like a
normal user push, so release.yml fires automatically. End-to-end every
release becomes "merge the Release PR" → wait → users see Update
available in Settings.

Adds the `token: ${{ secrets.RELEASE_PAT }}` input. The fallback
workflow_dispatch trigger on release.yml stays in place — if the
secret is unset the action degrades to GITHUB_TOKEN and the manual
dispatch path still works (no regression).

Before merging: create the PAT (StackOneHQ org may require approval
from an owner) and add it as repo secret RELEASE_PAT. Setup steps in
the inline comment above the token line.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@hiskudin hiskudin merged commit 46d1661 into main May 22, 2026
4 checks passed
@hiskudin hiskudin deleted the feat-handle-terminal-ide-context branch May 22, 2026 08:31
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.

2 participants