fix(renderer): keep OpenCode OC| reclaim durable under launch ownership

Native OC| titles reclaim and stay OpenCode after process observation, with
sidebar title-derived rows matching tab identity. Nested Claude/Codex still
fenced while launch is active (#13341).
This commit is contained in:
OrcaWin
2026-08-12 03:00:23 -07:00
parent 00686addd1
commit ef41ba5004
5 changed files with 100 additions and 28 deletions
@@ -239,6 +239,21 @@ describe('buildTitleDerivedAgentRows', () => {
expect(rows.map((row) => [row.agentType, row.state])).toEqual([['claude', 'working']])
})
// Why (#8478): native OC| under a non-OpenCode launch matches tab-agent reclaim (sidebar parity).
it('lets a native OpenCode title own the row under an active Claude launch', () => {
const rows = buildWorktreeAgentRows({
tabs: [makeTab('tab-1', { launchAgent: 'claude' })],
entries: [],
retained: [],
runtimePaneTitlesByTabId: { 'tab-1': { 1: 'OC | Greeting' } },
ptyIdsByTabId: { 'tab-1': ['pty-opencode'] },
terminalLayoutsByTabId: { 'tab-1': makeSingleLayout(LEAF_ID_1) },
now: 2000
})
expect(rows.map((row) => [row.agentType, row.state])).toEqual([['opencode', 'idle']])
})
it('produces no row for a spinner-only title when the tab has no launch identity', () => {
const rows = buildWorktreeAgentRows({
tabs: [makeTab('tab-1')],
@@ -20,6 +20,7 @@ import {
resolveCompatibleAgentTypeForOwner
} from '../../../../shared/agent-title-owner'
import { resolvePaneAgentOwner } from '../../../../shared/pane-agent-owner'
import { isOpenCodeNativeTitle } from '../../../../shared/opencode-terminal-title'
const EMPTY_RUNTIME_TITLES: Record<string, Record<number, string>> = {}
const EMPTY_LIVE_PTY_IDS: Record<string, string[]> = {}
@@ -213,6 +214,10 @@ export function resolveTitleDerivedAgentType(
}
return agentType
}
// Why (#8478): native OC| is provider identity (pane reuse), not a nested child — match tab-agent.
if (agentType === 'opencode' && isOpenCodeNativeTitle(title)) {
return agentType
}
// Why (#13341): nested child titles keep the durable owner until launch clears.
if (owner && agentType !== 'unknown' && agentType !== owner) {
return null
+26 -24
View File
@@ -114,15 +114,6 @@ export function resolveTabAgentFromSignals(args: {
processShellForeground: args.processShellForeground
})
const activeLaunchAgent = launchedAgentExited ? null : launchAgent
// Why: re-own the foreground process within its title-identity group so OMP's nested pi (shell → omp → pi) can't flip an OMP-owned tab's icon.
const processAgentRaw = resolveSignalAgentForLaunchOwner(args.processAgent, owner)
// Why (#13341): Claude↔Codex nested CLIs become the local foreground process while the
// parent session still owns the pane. Do not let that child process rebrand the icon
// until launch ownership has exited (then process may name a genuine reuse).
const processAgent =
activeLaunchAgent && processAgentRaw && processAgentRaw !== activeLaunchAgent
? null
: processAgentRaw
// Title carries identity only as a reuse override (names a DIFFERENT-group agent) or a legacy standalone id when no hook — same-group titles say nothing (OMP wraps Pi), so the record wins.
const explicitTitleAgent = resolveSignalAgentForLaunchOwner(
@@ -131,38 +122,49 @@ export function resolveTabAgentFromSignals(args: {
)
const priorIdentity = idleFocusedIdentity ?? activeLaunchAgent
const nativeOpenCodeTitle = explicitTitleAgent === 'opencode' && isOpenCodeNativeTitle(args.title)
// Why (#8478): native `OC |` is provider identity for pane reuse — durable while the frame
// remains, including after process observation and under a non-OpenCode launch (#13341 still
// fences other cross-group nested titles/processes).
const openCodeNativeReclaim =
nativeOpenCodeTitle &&
(activeLaunchAgent === null || activeLaunchAgent !== 'opencode') &&
(priorIdentity === null || priorIdentity !== 'opencode')
// Why: re-own the foreground process within its title-identity group so OMP's nested pi (shell → omp → pi) can't flip an OMP-owned tab's icon.
const processAgentRaw = resolveSignalAgentForLaunchOwner(args.processAgent, owner)
// Why (#13341): Claude↔Codex nested CLIs become the local foreground process while the
// parent session still owns the pane. Do not let that child process rebrand the icon
// until launch ownership has exited (then process may name a genuine reuse).
// Why (#8478): after OC| reclaim, a launch-matching process must not undo OpenCode.
const processAgent =
openCodeNativeReclaim && processAgentRaw && processAgentRaw !== 'opencode'
? null
: activeLaunchAgent && processAgentRaw && processAgentRaw !== activeLaunchAgent
? null
: processAgentRaw
// Why: a "claude" token in another agent's task text is a mention, not identity, so it must
// not take a pane from its known owner — only a title that PRESENTS Claude may (#8940).
const titleClaimsIdentity =
explicitTitleAgent !== 'claude' || isClaudeIdentityFrameTitle(args.title)
// Why: native OpenCode titles can reclaim stale launch intent before any observed hook signal.
// Why (#13341): once a launch owner is still active, a nested child's title is not pane reuse.
// Why (#13341): nested child titles are not pane reuse while launch ownership is active —
// except native OC| (#8478), which is provider identity, not a nested CLI.
const titleReclaimsReusedPane =
priorIdentity !== null &&
explicitTitleAgent !== null &&
explicitTitleAgent !== priorIdentity &&
titleClaimsIdentity &&
!activeLaunchAgent &&
(!activeLaunchAgent || nativeOpenCodeTitle) &&
(args.hasObservedAgentSignal || hasCompletedHook || nativeOpenCodeTitle)
// Why: OpenCode's native `OC |` frame is provider identity at startup (before hooks), not a nested child.
const openCodeStartupTitle =
nativeOpenCodeTitle &&
launchAgent !== null &&
launchAgent !== 'opencode' &&
!args.hasObservedAgentSignal
// Why: native OpenCode titles lack a provider generation and cannot displace durable ownership.
const titleAgent =
processProvesShell ||
sleepingSessionAgent ||
(nativeOpenCodeTitle && idleFocusedIdentity !== null)
? null
: openCodeStartupTitle
: openCodeNativeReclaim || titleReclaimsReusedPane
? explicitTitleAgent
: titleReclaimsReusedPane
? explicitTitleAgent
: priorIdentity
? null
: explicitTitleAgent
: priorIdentity
? null
: explicitTitleAgent
// Identity-first precedence (see useTabAgent JSDoc): live hook > process > title > completed > sleeping > launch > sibling.
return (
@@ -155,4 +155,55 @@ describe('resolveTabAgentFromSignals — nested child identity (#13341)', () =>
})
).toBe('codex')
})
// Why (#8478): native OC| reclaim must stay OpenCode after observation and under launch.
it('keeps OpenCode OC| reclaim after process observation under a non-OpenCode launch', () => {
expect(
resolveTabAgentFromSignals({
hasObservedAgentSignal: false,
isRemote: false,
title: 'OC | Greeting',
hookAgent: null,
processAgent: null,
launchAgent: 'claude'
})
).toBe('opencode')
expect(
resolveTabAgentFromSignals({
hasObservedAgentSignal: true,
isRemote: false,
title: 'OC | Greeting',
hookAgent: null,
processAgent: 'claude',
launchAgent: 'claude'
})
).toBe('opencode')
})
it('still fences nested Codex title under Claude launch (not OC|)', () => {
expect(
resolveTabAgentFromSignals({
hasObservedAgentSignal: true,
isRemote: false,
title: '✦ Codex',
hookAgent: null,
processAgent: 'codex',
launchAgent: 'claude'
})
).toBe('claude')
})
it('clears OpenCode reclaim on shell return so launch can exit', () => {
expect(
resolveTabAgentFromSignals({
hasObservedAgentSignal: true,
isRemote: false,
title: 'zsh',
hookAgent: null,
processAgent: null,
processShellForeground: true,
launchAgent: 'claude'
})
).toBeNull()
})
})
@@ -241,18 +241,17 @@ describe('OpenCode native title tab identity', () => {
launchAgent: 'claude'
})
).toBe('claude')
// Why (#13341): nested process under active launch is not stronger than parent ownership.
// Why (#8478): native OC| reclaim is durable — process observation must not undo OpenCode.
expect(
resolveTabAgentFromSignals({
hasObservedAgentSignal: false,
hasObservedAgentSignal: true,
isRemote: false,
title: 'OC | Greeting',
hookAgent: null,
processAgent: 'codex',
sleepingSessionAgent: 'claude',
launchAgent: 'claude'
})
).toBe('claude')
).toBe('opencode')
for (const title of [
'OpenCode ready',