feat(): handle terminal & ide context#55
Conversation
hiskudin
left a comment
There was a problem hiding this comment.
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 == eventTabwill 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.customNamelookup inEventRowuses one key; the rename written fromSessionStore.renameuses 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:
- Drop it from notify.sh entirely — the panel already enriches via
ITerm2Integration.sessionsByTTY()on the session-discovery side; the event side could just carryTERM_SESSION_IDand let the panel resolve the human-readable name from its own cache (already populated within 15s of any agent-tab activity). - 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.enrichcallsITerm2Integration.ttyMap(forPids:)— cross-class static reuse. MovettyMapontoProcessOutputor a dedicatedTTYenum; right nowTerminalAppIntegrationis coupled toITerm2Integrationvia a name that doesn't advertise the shared utility.TerminalRegistry.integrationsis a mutablestatic varfor tests. Fine for now; if XCTest ever runs in parallel this will surprise someone. Worth a// test-only mutation; do not mutate at runtimecomment, or move behind a function.agent_label()in notify.sh mapsagy|antigravity|antigravity-cli → "Antigravity", but Bootstrap wiresnotify.sh antigravity stop(passesantigravityas agentArg), so theagyandantigravity-clibranches are unreachable from the wired path. Harmless and defensive but worth a one-line comment so a future reader doesn't burn time tracing whereagyarrives from.- New
Session.tabId/tabNamearevarin a struct that's otherwise alllet.SessionStorealready routes mutations throughwith(...); for consistency you could keep themlet.
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>
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 +
agyCLI) and wires automated hook installation for Gemini + Antigravity alongside the existing Claude path.Changes
Terminal integration
TerminalIntegrationprotocol (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 (batchedpscall, one subprocess for N sessions). Tab name flows through from notify.sh via a new wire fielditerm_tab_name.TerminalAppIntegration— mirror of the iTerm one for Terminal.app. tabId is the tty path (no stable UUID exposed); tabName iscustom titlewhen set.VSCodeIntegration— bi-directional. Events teach the cache (ipc_hook → window title); Sessions read VSCODE_IPC_HOOK_CLI from each agent's env viaps ewwand 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
SessiongainstabId+tabName.SessionPersistencekeys widen toagent::path::tabIdwith backward-compat fallback toagent::path(pre-Stage-2 renames still apply across every tab in a project until a per-tab override is written).SessionColormixestabIdinto 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.EventRowusesevent.sessionIDfor iTerm andevent.ipcHookfor VSCode-fork events as its tabId.SessionsView.matchestreats either as the event-side tab id when cross-referencing event-to-session counts.Antigravity +
agynotify.sh+SessionStorerecognise Antigravity helpers as terminals andagyas an agent binary (direct + node/deno/bun-hosted).Agent.canonicalcollapsesantigravity/antigravity-clitoagyso wire and process names stay in sync.EventRownormalisesAntigravity Helper (Renderer)etc. toAntigravity; same forCode Helper→VSCode,Cursor Helper→Cursor,WarpTerminal→Warp,ghostty→Ghostty.Events tab polish
Approve write · iTerm2 · auth · tests).Hook installation
install.shnow 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.shmirrors the additions, cleaning up stale entries.Perf
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.shclean.make reload, fired varied trial events covering all five terminals plus Antigravity + agy. Verified:test-macosjob (swift test) runs the 27 new XCTest cases.