mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 00:03:45 +00:00
fix(codex): tell the user when a resumed pane is pinned to another account (#10804)
When Orca auto-resumes a Codex session belonging to a different account than the one selected, it deliberately pins CODEX_HOME to the ORIGINATING account so it never continues a conversation under someone else's credentials. That pin was silent: the pane came back on the old account while the status bar showed the new one, which is one of the mechanisms behind #10757's "stuck on the previous account". The pin stays — it is correct under concurrent multi-account. This records the pane's real launch account instead of forgetting it, so the existing chip says "Codex is still signed in as A. Restart this session to use B." Review (2 rounds) found three things: the branch was rebased off a stale fork point where a sibling's notice still muted an innocent split-tab pane — and since this PR strictly increases panes carrying notices it was amplifying that keyboard-death path; the runtime-controller spawn site (pty.ts:3705) had ZERO coverage, with a full behavioural revert leaving all 392 tests green despite being reachable via the CLI/relay/orchestration resume path; and a comment claimed the shared runtime mirror "hot-swaps to whichever account is selected", which is untrue under the shipped config. Live-validated in a real Orca dev build: all five cases pass, with every CODEX_HOME claim read off the SPAWNED PROCESS via a shim binary (SIP blocks ps eww). Restart relaunches under the newly selected account with input restored and no prompt loop; a same-account resume produces no chip; in a split tab exactly one pane is noticed while the sibling types normally; and the runtime-controller path records the launch account when driven over the RPC socket. Not verified: WSL is not run and not claimed — prepareCodexSessionResumeForLaunch returns null for WSL targets, so this mechanism is HOST-LANE ONLY. Known follow-ups, deliberately not fixed here: a pane resumed into the legacy mirror is genuinely stale but unnameable in the current record shape, and a dismissal does not survive a cold restore for a resume-pinned pane (correct as-is — the new shell really is on the old account).
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { CodexManagedAccount, GlobalSettings } from '../../shared/types'
|
||||
import { resolveCodexPaneLaunchAccount } from './codex-pane-launch-account'
|
||||
|
||||
const SYSTEM_HOME = '/Users/example/.codex'
|
||||
|
||||
function managedAccount(overrides: Partial<CodexManagedAccount>): CodexManagedAccount {
|
||||
return {
|
||||
id: 'account-a',
|
||||
email: 'a@example.com',
|
||||
managedHomePath: '/data/codex-accounts/account-a/home',
|
||||
createdAt: 0,
|
||||
updatedAt: 0,
|
||||
lastAuthenticatedAt: 0,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
function settings(args: {
|
||||
host?: string | null
|
||||
wsl?: Record<string, string | null>
|
||||
accounts?: CodexManagedAccount[]
|
||||
}): GlobalSettings {
|
||||
return {
|
||||
activeCodexManagedAccountId: args.host ?? null,
|
||||
activeCodexManagedAccountIdsByRuntime: { host: args.host ?? null, wsl: args.wsl ?? {} },
|
||||
codexManagedAccounts: args.accounts ?? []
|
||||
} as GlobalSettings
|
||||
}
|
||||
|
||||
describe('resolveCodexPaneLaunchAccount', () => {
|
||||
it('records the selected account for an ordinary spawn', () => {
|
||||
expect(
|
||||
resolveCodexPaneLaunchAccount({
|
||||
pinnedByResume: false,
|
||||
launchCodexHomePath: '/data/codex-accounts/account-b/home',
|
||||
systemCodexHomePath: SYSTEM_HOME,
|
||||
settings: settings({ host: 'account-b' }),
|
||||
target: { runtime: 'host' }
|
||||
})
|
||||
).toEqual({ selectionKey: 'host', accountId: 'account-b' })
|
||||
})
|
||||
|
||||
it('records the origin account a resume pinned the pane to, not the selection', () => {
|
||||
const accounts = [
|
||||
managedAccount({ id: 'account-a' }),
|
||||
managedAccount({ id: 'account-b', managedHomePath: '/data/codex-accounts/account-b/home' })
|
||||
]
|
||||
|
||||
expect(
|
||||
resolveCodexPaneLaunchAccount({
|
||||
pinnedByResume: true,
|
||||
launchCodexHomePath: '/data/codex-accounts/account-a/home',
|
||||
systemCodexHomePath: SYSTEM_HOME,
|
||||
settings: settings({ host: 'account-b', accounts }),
|
||||
target: { runtime: 'host' }
|
||||
})
|
||||
).toEqual({ selectionKey: 'host', accountId: 'account-a' })
|
||||
})
|
||||
|
||||
it('records the same account a resume pinned to when it is already selected', () => {
|
||||
const accounts = [managedAccount({ id: 'account-a' })]
|
||||
|
||||
// Why: the sweep compares this against the live selection, so an equal
|
||||
// account must still be recorded — it is simply not reported stale.
|
||||
expect(
|
||||
resolveCodexPaneLaunchAccount({
|
||||
pinnedByResume: true,
|
||||
launchCodexHomePath: '/data/codex-accounts/account-a/home',
|
||||
systemCodexHomePath: SYSTEM_HOME,
|
||||
settings: settings({ host: 'account-a', accounts }),
|
||||
target: { runtime: 'host' }
|
||||
})
|
||||
).toEqual({ selectionKey: 'host', accountId: 'account-a' })
|
||||
})
|
||||
|
||||
it('maps a resume redirected to the real system home to the system-default account', () => {
|
||||
expect(
|
||||
resolveCodexPaneLaunchAccount({
|
||||
pinnedByResume: true,
|
||||
launchCodexHomePath: SYSTEM_HOME,
|
||||
systemCodexHomePath: SYSTEM_HOME,
|
||||
settings: settings({
|
||||
host: 'account-a',
|
||||
accounts: [managedAccount({ id: 'account-a' })]
|
||||
}),
|
||||
target: { runtime: 'host' }
|
||||
})
|
||||
).toEqual({ selectionKey: 'host', accountId: null })
|
||||
})
|
||||
|
||||
it('maps a resume that injects no CODEX_HOME to the system-default account', () => {
|
||||
expect(
|
||||
resolveCodexPaneLaunchAccount({
|
||||
pinnedByResume: true,
|
||||
launchCodexHomePath: null,
|
||||
systemCodexHomePath: SYSTEM_HOME,
|
||||
settings: settings({ host: 'account-a', accounts: [managedAccount({ id: 'account-a' })] }),
|
||||
target: { runtime: 'host' }
|
||||
})
|
||||
).toEqual({ selectionKey: 'host', accountId: null })
|
||||
})
|
||||
|
||||
it('refuses to attribute a resume home no account owns', () => {
|
||||
expect(
|
||||
resolveCodexPaneLaunchAccount({
|
||||
pinnedByResume: true,
|
||||
launchCodexHomePath: '/data/codex-runtime-home/home',
|
||||
systemCodexHomePath: SYSTEM_HOME,
|
||||
settings: settings({ host: 'account-a', accounts: [managedAccount({ id: 'account-a' })] }),
|
||||
target: { runtime: 'host' }
|
||||
})
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('does not let a host account answer for a WSL pane on the same path', () => {
|
||||
const accounts = [
|
||||
managedAccount({
|
||||
id: 'host-account',
|
||||
managedHomePath: '//wsl.localhost/Ubuntu/home/u/.codex'
|
||||
})
|
||||
]
|
||||
|
||||
expect(
|
||||
resolveCodexPaneLaunchAccount({
|
||||
pinnedByResume: true,
|
||||
launchCodexHomePath: '//wsl.localhost/Ubuntu/home/u/.codex',
|
||||
systemCodexHomePath: SYSTEM_HOME,
|
||||
settings: settings({ wsl: { Ubuntu: 'wsl-account' }, accounts }),
|
||||
target: { runtime: 'wsl', wslDistro: 'Ubuntu' }
|
||||
})
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('attributes a WSL resume home to the account on that distro lane', () => {
|
||||
const accounts = [
|
||||
managedAccount({
|
||||
id: 'wsl-account',
|
||||
managedHomeRuntime: 'wsl',
|
||||
wslDistro: 'Ubuntu',
|
||||
managedHomePath: '\\\\wsl$\\Ubuntu\\home\\u\\.codex-a'
|
||||
})
|
||||
]
|
||||
|
||||
expect(
|
||||
resolveCodexPaneLaunchAccount({
|
||||
pinnedByResume: true,
|
||||
launchCodexHomePath: '//wsl.localhost/Ubuntu/home/u/.codex-a',
|
||||
systemCodexHomePath: SYSTEM_HOME,
|
||||
settings: settings({ wsl: { Ubuntu: 'other-wsl-account' }, accounts }),
|
||||
target: { runtime: 'wsl', wslDistro: 'Ubuntu' }
|
||||
})
|
||||
).toEqual({ selectionKey: 'wsl:Ubuntu', accountId: 'wsl-account' })
|
||||
})
|
||||
|
||||
it('tolerates settings that carry no managed account roster', () => {
|
||||
expect(
|
||||
resolveCodexPaneLaunchAccount({
|
||||
pinnedByResume: true,
|
||||
launchCodexHomePath: '/data/codex-accounts/account-a/home',
|
||||
systemCodexHomePath: SYSTEM_HOME,
|
||||
settings: { activeCodexManagedAccountId: null } as GlobalSettings,
|
||||
target: { runtime: 'host' }
|
||||
})
|
||||
).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { GlobalSettings } from '../../shared/types'
|
||||
import { normalizeRuntimePathForComparison } from '../../shared/cross-platform-path'
|
||||
import {
|
||||
getCodexSelectionLaneKey,
|
||||
getCodexSelectionTargetForAccount,
|
||||
getSelectedCodexAccountIdForTarget,
|
||||
type CodexAccountSelectionTarget
|
||||
} from '../codex-accounts/runtime-selection'
|
||||
import type { CodexPaneAccountRecord } from './codex-pane-account-registry'
|
||||
|
||||
type CodexPaneLaunchAccountSettings = Pick<
|
||||
GlobalSettings,
|
||||
'activeCodexManagedAccountId' | 'activeCodexManagedAccountIdsByRuntime' | 'codexManagedAccounts'
|
||||
>
|
||||
|
||||
/**
|
||||
* Resolves which Codex account a PTY is actually launching under.
|
||||
*
|
||||
* Why: an automatic session resume deliberately pins CODEX_HOME to the home
|
||||
* that owns the session rather than to the current selection, so a cold-restored
|
||||
* pane can come back on an account the user already switched away from. Naming
|
||||
* that real account — instead of the selection Orca ignored — is what lets the
|
||||
* restart prompt tell the user which account the pane is stuck on.
|
||||
*
|
||||
* Returns null when the launch cannot be attributed, which keeps the pane out of
|
||||
* the stale-pane report entirely.
|
||||
*/
|
||||
export function resolveCodexPaneLaunchAccount(args: {
|
||||
pinnedByResume: boolean
|
||||
launchCodexHomePath: string | null
|
||||
systemCodexHomePath: string
|
||||
settings: CodexPaneLaunchAccountSettings
|
||||
target: CodexAccountSelectionTarget
|
||||
}): CodexPaneAccountRecord | null {
|
||||
const selectionKey = getCodexSelectionLaneKey(args.target)
|
||||
if (!args.pinnedByResume) {
|
||||
return {
|
||||
selectionKey,
|
||||
accountId: getSelectedCodexAccountIdForTarget(args.settings, args.target)
|
||||
}
|
||||
}
|
||||
const accountId = resolveCodexHomeOwnerAccountId(args)
|
||||
return accountId === undefined ? null : { selectionKey, accountId }
|
||||
}
|
||||
|
||||
/** undefined when no account owns the home; null means the system-default account. */
|
||||
function resolveCodexHomeOwnerAccountId(args: {
|
||||
launchCodexHomePath: string | null
|
||||
systemCodexHomePath: string
|
||||
settings: CodexPaneLaunchAccountSettings
|
||||
target: CodexAccountSelectionTarget
|
||||
}): string | null | undefined {
|
||||
// Why: no injected CODEX_HOME means Codex reads the user's own home.
|
||||
if (!args.launchCodexHomePath) {
|
||||
return null
|
||||
}
|
||||
const launchHome = normalizeRuntimePathForComparison(args.launchCodexHomePath)
|
||||
if (launchHome === normalizeRuntimePathForComparison(args.systemCodexHomePath)) {
|
||||
return null
|
||||
}
|
||||
const laneKey = getCodexSelectionLaneKey(args.target)
|
||||
const owner = args.settings.codexManagedAccounts?.find(
|
||||
(account) =>
|
||||
// Why: a WSL pane resolves its account from its own per-distro lane, so a
|
||||
// host account's home must never answer for it (and vice versa).
|
||||
getCodexSelectionLaneKey(getCodexSelectionTargetForAccount(account)) === laneKey &&
|
||||
normalizeRuntimePathForComparison(account.managedHomePath) === launchHome
|
||||
)
|
||||
// Why: an unowned home cannot be named, and naming the account a pane is stuck
|
||||
// on is the prompt's whole job — so decline rather than guess. A wrong notice
|
||||
// silently drops every keystroke in that terminal. Note the shared runtime
|
||||
// mirror only hot-swaps to the current selection on the legacy flag-OFF lane;
|
||||
// a pane resumed into it after the per-account rollout is genuinely stale but
|
||||
// still unnameable, so that cohort stays unreported.
|
||||
return owner ? owner.id : undefined
|
||||
}
|
||||
@@ -13545,6 +13545,219 @@ describe('registerPtyHandlers', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('records the origin account a resumed Codex pane is pinned to', async () => {
|
||||
setLocalPtyProvider({
|
||||
spawn: vi.fn(async () => ({ id: 'pty-resumed' })),
|
||||
write: vi.fn(),
|
||||
resize: vi.fn(),
|
||||
kill: vi.fn(),
|
||||
shutdown: vi.fn(),
|
||||
onData: vi.fn(() => vi.fn()),
|
||||
onExit: vi.fn(() => vi.fn()),
|
||||
listProcesses: vi.fn(async () => []),
|
||||
getForegroundProcess: vi.fn(async () => null)
|
||||
} as never)
|
||||
const getSettings = vi.fn().mockReturnValue({
|
||||
activeCodexManagedAccountId: 'account-b',
|
||||
codexManagedAccounts: [
|
||||
{ id: 'account-a', managedHomePath: '/managed/origin/home' },
|
||||
{ id: 'account-b', managedHomePath: '/managed/current/home' }
|
||||
]
|
||||
})
|
||||
registerPtyHandlers(
|
||||
mainWindow as never,
|
||||
undefined,
|
||||
vi.fn(() => '/managed/current/home'),
|
||||
getSettings as never,
|
||||
undefined,
|
||||
undefined,
|
||||
{ prepareCodexSessionResume: async () => ({ codexHomePath: '/managed/origin/home' }) }
|
||||
)
|
||||
|
||||
await handlers.get('pty:spawn')!(null, {
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
command: 'codex resume session-a',
|
||||
launchAgent: 'codex',
|
||||
resumeProviderSession: {
|
||||
key: 'session_id',
|
||||
id: 'session-a',
|
||||
transcriptPath: '/managed/origin/home/sessions/2026/07/20/rollout-a.jsonl'
|
||||
}
|
||||
})
|
||||
|
||||
// Why: the resume deliberately overrides the selection, so the pane really
|
||||
// is on account-a. Recording that is what makes the restart prompt appear.
|
||||
expect(recordCodexPaneAccountMock.mock.calls).toEqual([
|
||||
['pty-resumed', { selectionKey: 'host', accountId: 'account-a' }]
|
||||
])
|
||||
expect(forgetCodexPaneAccountMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('leaves a resumed Codex pane unattributed when no account owns its home', async () => {
|
||||
setLocalPtyProvider({
|
||||
spawn: vi.fn(async () => ({ id: 'pty-resumed' })),
|
||||
write: vi.fn(),
|
||||
resize: vi.fn(),
|
||||
kill: vi.fn(),
|
||||
shutdown: vi.fn(),
|
||||
onData: vi.fn(() => vi.fn()),
|
||||
onExit: vi.fn(() => vi.fn()),
|
||||
listProcesses: vi.fn(async () => []),
|
||||
getForegroundProcess: vi.fn(async () => null)
|
||||
} as never)
|
||||
const getSettings = vi.fn().mockReturnValue({
|
||||
activeCodexManagedAccountId: 'account-b',
|
||||
codexManagedAccounts: [{ id: 'account-b', managedHomePath: '/managed/current/home' }]
|
||||
})
|
||||
registerPtyHandlers(
|
||||
mainWindow as never,
|
||||
undefined,
|
||||
vi.fn(() => '/managed/current/home'),
|
||||
getSettings as never,
|
||||
undefined,
|
||||
undefined,
|
||||
{ prepareCodexSessionResume: async () => ({ codexHomePath: '/managed/shared-mirror/home' }) }
|
||||
)
|
||||
|
||||
await handlers.get('pty:spawn')!(null, {
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
command: 'codex resume session-a',
|
||||
launchAgent: 'codex',
|
||||
resumeProviderSession: {
|
||||
key: 'session_id',
|
||||
id: 'session-a',
|
||||
transcriptPath: '/managed/shared-mirror/home/sessions/2026/07/20/rollout-a.jsonl'
|
||||
}
|
||||
})
|
||||
|
||||
// Why: an unowned home cannot be named, so guessing here would raise a
|
||||
// restart notice that blocks a correctly-signed-in pane's input.
|
||||
expect(recordCodexPaneAccountMock).not.toHaveBeenCalled()
|
||||
expect(forgetCodexPaneAccountMock).toHaveBeenCalledWith('pty-resumed')
|
||||
})
|
||||
|
||||
// Why: the runtime controller is the CLI/relay resume path, and it repeats the
|
||||
// same recording call the ipc handler makes. Without its own coverage a revert
|
||||
// there is invisible.
|
||||
it('records the origin account for a resumed Codex pane spawned by the runtime controller', async () => {
|
||||
type RuntimeSpawnController = {
|
||||
spawn(args: Record<string, unknown>): Promise<{ id: string }>
|
||||
}
|
||||
setLocalPtyProvider({
|
||||
spawn: vi.fn(async () => ({ id: 'pty-runtime-resumed' })),
|
||||
write: vi.fn(),
|
||||
resize: vi.fn(),
|
||||
kill: vi.fn(),
|
||||
shutdown: vi.fn(),
|
||||
onData: vi.fn(() => vi.fn()),
|
||||
onExit: vi.fn(() => vi.fn()),
|
||||
listProcesses: vi.fn(async () => []),
|
||||
getForegroundProcess: vi.fn(async () => null)
|
||||
} as never)
|
||||
const runtime = {
|
||||
setPtyController: vi.fn(),
|
||||
registerPty: vi.fn(),
|
||||
noteTerminalSpawnCommand: vi.fn(),
|
||||
onPtySpawned: vi.fn(),
|
||||
onPtyExit: vi.fn(),
|
||||
onPtyData: vi.fn()
|
||||
}
|
||||
const getSettings = vi.fn().mockReturnValue({
|
||||
activeCodexManagedAccountId: 'account-b',
|
||||
codexManagedAccounts: [
|
||||
{ id: 'account-a', managedHomePath: '/managed/origin/home' },
|
||||
{ id: 'account-b', managedHomePath: '/managed/current/home' }
|
||||
]
|
||||
})
|
||||
handlers.clear()
|
||||
registerPtyHandlers(
|
||||
mainWindow as never,
|
||||
runtime as never,
|
||||
vi.fn(() => '/managed/current/home'),
|
||||
getSettings as never,
|
||||
undefined,
|
||||
undefined,
|
||||
{ prepareCodexSessionResume: async () => ({ codexHomePath: '/managed/origin/home' }) }
|
||||
)
|
||||
const controller = runtime.setPtyController.mock.calls[0]?.[0] as RuntimeSpawnController
|
||||
|
||||
await controller.spawn({
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
worktreeId: 'wt-runtime',
|
||||
command: 'codex resume session-a',
|
||||
launchAgent: 'codex',
|
||||
resumeProviderSession: {
|
||||
key: 'session_id',
|
||||
id: 'session-a',
|
||||
transcriptPath: '/managed/origin/home/sessions/2026/07/20/rollout-a.jsonl'
|
||||
}
|
||||
})
|
||||
|
||||
expect(recordCodexPaneAccountMock.mock.calls).toEqual([
|
||||
['pty-runtime-resumed', { selectionKey: 'host', accountId: 'account-a' }]
|
||||
])
|
||||
expect(forgetCodexPaneAccountMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('leaves a runtime-controller resumed Codex pane unattributed when no account owns its home', async () => {
|
||||
type RuntimeSpawnController = {
|
||||
spawn(args: Record<string, unknown>): Promise<{ id: string }>
|
||||
}
|
||||
setLocalPtyProvider({
|
||||
spawn: vi.fn(async () => ({ id: 'pty-runtime-resumed' })),
|
||||
write: vi.fn(),
|
||||
resize: vi.fn(),
|
||||
kill: vi.fn(),
|
||||
shutdown: vi.fn(),
|
||||
onData: vi.fn(() => vi.fn()),
|
||||
onExit: vi.fn(() => vi.fn()),
|
||||
listProcesses: vi.fn(async () => []),
|
||||
getForegroundProcess: vi.fn(async () => null)
|
||||
} as never)
|
||||
const runtime = {
|
||||
setPtyController: vi.fn(),
|
||||
registerPty: vi.fn(),
|
||||
noteTerminalSpawnCommand: vi.fn(),
|
||||
onPtySpawned: vi.fn(),
|
||||
onPtyExit: vi.fn(),
|
||||
onPtyData: vi.fn()
|
||||
}
|
||||
const getSettings = vi.fn().mockReturnValue({
|
||||
activeCodexManagedAccountId: 'account-b',
|
||||
codexManagedAccounts: [{ id: 'account-b', managedHomePath: '/managed/current/home' }]
|
||||
})
|
||||
handlers.clear()
|
||||
registerPtyHandlers(
|
||||
mainWindow as never,
|
||||
runtime as never,
|
||||
vi.fn(() => '/managed/current/home'),
|
||||
getSettings as never,
|
||||
undefined,
|
||||
undefined,
|
||||
{ prepareCodexSessionResume: async () => ({ codexHomePath: '/managed/shared-mirror/home' }) }
|
||||
)
|
||||
const controller = runtime.setPtyController.mock.calls[0]?.[0] as RuntimeSpawnController
|
||||
|
||||
await controller.spawn({
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
worktreeId: 'wt-runtime',
|
||||
command: 'codex resume session-a',
|
||||
launchAgent: 'codex',
|
||||
resumeProviderSession: {
|
||||
key: 'session_id',
|
||||
id: 'session-a',
|
||||
transcriptPath: '/managed/shared-mirror/home/sessions/2026/07/20/rollout-a.jsonl'
|
||||
}
|
||||
})
|
||||
|
||||
expect(recordCodexPaneAccountMock).not.toHaveBeenCalled()
|
||||
expect(forgetCodexPaneAccountMock).toHaveBeenCalledWith('pty-runtime-resumed')
|
||||
})
|
||||
|
||||
it('seeds cold restore at recovered dimensions with a legacy dimensionless fallback', async () => {
|
||||
const oscLinks = [{ row: 0, startCol: 0, endCol: 8, uri: 'https://example.com/restored' }]
|
||||
const coldRestore = {
|
||||
|
||||
+18
-12
@@ -160,15 +160,13 @@ import {
|
||||
import { setTerminalViewAttributes } from '../runtime/terminal-view-attribute-store'
|
||||
import { validateTerminalViewAttributes } from '../../shared/terminal-view-attributes'
|
||||
import type { PtyModelRestoreReason } from '../../shared/pty-model-restore-marker'
|
||||
import {
|
||||
getCodexSelectionLaneKey,
|
||||
getSelectedCodexAccountIdForTarget,
|
||||
type CodexAccountSelectionTarget
|
||||
} from '../codex-accounts/runtime-selection'
|
||||
import type { CodexAccountSelectionTarget } from '../codex-accounts/runtime-selection'
|
||||
import {
|
||||
forgetCodexPaneAccount,
|
||||
recordCodexPaneAccount
|
||||
} from '../codex/codex-pane-account-registry'
|
||||
import { resolveCodexPaneLaunchAccount } from '../codex/codex-pane-launch-account'
|
||||
import { getSystemCodexHomePath } from '../codex/codex-home-paths'
|
||||
import { isCodexSystemDefaultRealHomeEnabled } from '../codex/codex-real-home-flag'
|
||||
import { isHostCodexHomeForWsl, isWslCodexHomeForHost } from '../pty/codex-home-wsl-env'
|
||||
import { buildConfiguredProxyEnv, type NetworkProxySettings } from '../../shared/network-proxy'
|
||||
@@ -832,27 +830,33 @@ function getCompatibleSelectedCodexHomePath(
|
||||
// way to tell later that a pane still runs Codex as the previously selected
|
||||
// account. A reattach inherits that baked environment rather than choosing one,
|
||||
// so re-recording it under the current selection would erase the very evidence
|
||||
// that the pane is stale. A resume-pinned pane is deliberately on its session's
|
||||
// own account, so it is forgotten rather than recorded as stale.
|
||||
// that the pane is stale.
|
||||
function recordCodexPaneAccountForSpawn(args: {
|
||||
ptyId: string | undefined
|
||||
isDaemonHostSpawn: boolean
|
||||
isReattach: boolean
|
||||
pinnedByResume: boolean
|
||||
launchCodexHomePath: string | null
|
||||
target: CodexAccountSelectionTarget
|
||||
settings: GlobalSettings | undefined
|
||||
}): void {
|
||||
if (!args.ptyId || !args.isDaemonHostSpawn || args.isReattach) {
|
||||
return
|
||||
}
|
||||
if (args.pinnedByResume || !args.settings) {
|
||||
const record = args.settings
|
||||
? resolveCodexPaneLaunchAccount({
|
||||
pinnedByResume: args.pinnedByResume,
|
||||
launchCodexHomePath: args.launchCodexHomePath,
|
||||
systemCodexHomePath: getSystemCodexHomePath(),
|
||||
settings: args.settings,
|
||||
target: args.target
|
||||
})
|
||||
: null
|
||||
if (!record) {
|
||||
forgetCodexPaneAccount(args.ptyId)
|
||||
return
|
||||
}
|
||||
recordCodexPaneAccount(args.ptyId, {
|
||||
selectionKey: getCodexSelectionLaneKey(args.target),
|
||||
accountId: getSelectedCodexAccountIdForTarget(args.settings, args.target)
|
||||
})
|
||||
recordCodexPaneAccount(args.ptyId, record)
|
||||
}
|
||||
|
||||
function readEnvWithProcessFallback(
|
||||
@@ -3699,6 +3703,7 @@ export function registerPtyHandlers(
|
||||
isDaemonHostSpawn,
|
||||
isReattach: result.isReattach === true,
|
||||
pinnedByResume: Boolean(codexResumeHome),
|
||||
launchCodexHomePath: selectedCodexHomePath,
|
||||
target: codexSelectionTarget,
|
||||
settings: getSettings?.()
|
||||
})
|
||||
@@ -4741,6 +4746,7 @@ export function registerPtyHandlers(
|
||||
isDaemonHostSpawn,
|
||||
isReattach: result.isReattach === true,
|
||||
pinnedByResume: Boolean(codexResumeHome),
|
||||
launchCodexHomePath: selectedCodexHomePath,
|
||||
target: codexSelectionTarget,
|
||||
settings: getSettings?.()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user