mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 00:03:47 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user