fix(pi): stop OMP tab icon flashing to Pi while working (#9077)

OMP runs as a `shell → omp → pi` process tree and Orca recognizes both
`omp` and `pi` as distinct agents, so the foreground-process reader
alternates between reporting the two across command boundaries. In
`resolveTabAgentFromSignals` every identity signal is re-owned onto the
pane's durable owner within its title-identity group (pi and omp share
one) — except the process signal, which was consumed raw. Because it
ranks above launchAgent, each `pi` read repainted an OMP-owned tab's icon
to the Pi glyph and the next read flipped it back.

Re-own the process signal the same way the hook and title signals already
are, so a same-group read collapses onto the owner while a genuine
cross-group process (e.g. Codex) still stands and can reclaim a reused
pane. The tab title text was unaffected — its owner resolution already
excludes the process signal — so this is icon-only.

Adds regression coverage for the oscillation, the mirrored/restored pane
(launchAgent gone, durable hook record), and the cross-group scope guard.
This commit is contained in:
Brennan Benson 2026-07-16 15:18:58 -07:00 committed by GitHub
parent de9e9f8f1b
commit dd0f4c39c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 67 additions and 2 deletions

View File

@ -132,6 +132,64 @@ describe('resolveTabAgentFromSignals — Pi/OMP identity', () => {
expect(afterHookCleared).toBe('omp')
})
it('does not flap between OMP and Pi as the foreground process oscillates', () => {
// The reported flicker: OMP wraps Pi (`shell → omp → pi`), so the foreground
// reader alternates between reporting `omp` and `pi` at command boundaries.
// The process signal outranks launchAgent, so without owner-normalization the
// OMP-owned tab's icon flips to Pi on every `pi` read. Both reads must land on
// the launched owner.
const readsPi = resolveTabAgentFromSignals({
hasObservedAgentSignal: true,
isRemote: false,
title: '⠋ OMP',
hookAgent: null,
processAgent: 'pi',
launchAgent: 'omp'
})
const readsOmp = resolveTabAgentFromSignals({
hasObservedAgentSignal: true,
isRemote: false,
title: '⠋ OMP',
hookAgent: null,
processAgent: 'omp',
launchAgent: 'omp'
})
expect(readsPi).toBe('omp')
expect(readsOmp).toBe('omp')
})
it('re-owns a Pi foreground read to a durable OMP identity when launchAgent is gone', () => {
// A mirrored/restored OMP pane keeps only its completed-hook identity; a `pi`
// foreground read (OMP's nested child) must not repaint it to Pi.
expect(
resolveTabAgentFromSignals({
hasObservedAgentSignal: true,
isRemote: false,
title: '⠋ OMP',
hookAgent: null,
focusedCompletedHookAgent: 'omp',
processAgent: 'pi',
launchAgent: undefined
})
).toBe('omp')
})
it('still lets a genuine cross-group foreground process reclaim a reused OMP pane', () => {
// Scope guard: a different-group process (Codex is not Pi-compatible) is
// real-time proof the pane was reused, so it overrides the OMP launch owner
// instead of collapsing onto it.
expect(
resolveTabAgentFromSignals({
hasObservedAgentSignal: true,
isRemote: false,
title: 'zsh',
hookAgent: null,
processAgent: 'codex',
launchAgent: 'omp'
})
).toBe('codex')
})
it('keeps a launchAgent-less Pi pane on Pi and rejects a stale OMP session record', () => {
// The fallback must not over-reach: a genuine Pi pane (recent Pi hook) stays
// Pi even if a stale hibernated OMP record is present.

View File

@ -168,7 +168,13 @@ export function resolveTabAgentFromSignals(args: {
processShellForeground: args.processShellForeground
})
const activeLaunchAgent = launchedAgentExited ? null : launchAgent
const processAgent = args.processAgent ?? null
// Why: the foreground process, like the hook and title signals, must be
// re-owned within its title-identity group. OMP wraps Pi as `shell → omp → pi`,
// so the foreground reader oscillates between reporting `omp` and `pi` across
// command boundaries; taken raw it outranks launchAgent and flips an OMP-owned
// tab's icon between the two glyphs. Re-owning collapses the same-group read
// onto the durable owner while a genuine cross-group process still stands.
const processAgent = resolveSignalAgentForLaunchOwner(args.processAgent, owner)
const sleepingSessionAgent = args.sleepingSessionAgent ?? null
// Identity-first precedence. The live focused hook is ground truth while the
// agent works; process identity covers agents with neither hook nor title; the
@ -199,7 +205,8 @@ export function resolveTabAgentFromSignals(args: {
* 2. Process identity the recognized foreground process, read at OSC 133
* command boundaries (local panes only); covers agents that emit neither
* hooks nor titles, and its shell-foreground mark is title-independent exit
* evidence.
* evidence. Re-owned within its title-identity group, so OMP's nested `pi`
* child (the `shell → omp → pi` tree) cannot flip the icon back to Pi.
* 3. Title only as a reuse override (it names a DIFFERENT-group agent than the
* pane's known identity, proving reuse) or as a legacy standalone identity
* when the pane has no hook. Within the same title-identity group it carries