diff --git a/src/renderer/src/lib/use-tab-agent-pi-identity.test.ts b/src/renderer/src/lib/use-tab-agent-pi-identity.test.ts index abe494ee1..2dea89781 100644 --- a/src/renderer/src/lib/use-tab-agent-pi-identity.test.ts +++ b/src/renderer/src/lib/use-tab-agent-pi-identity.test.ts @@ -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. diff --git a/src/renderer/src/lib/use-tab-agent.ts b/src/renderer/src/lib/use-tab-agent.ts index 93051d41d..b75273bef 100644 --- a/src/renderer/src/lib/use-tab-agent.ts +++ b/src/renderer/src/lib/use-tab-agent.ts @@ -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