feat(settings): let Codex terminals opt back into Codex's shared server (#23929)

* feat(settings): add a setting to run Codex terminals on Codex's shared server

Off injects ORCA_CODEX_ISOLATE=0 into new terminals on every host (local,
daemon, SSH relay, WSL), which the codex shell wrapper from #23900 reads
to skip --no-daemon. On (the default) injects nothing.

* feat(terminal): announce per-terminal Codex servers once

Shows a one-time toast the first time a Codex terminal starts, with an
Open Settings action that lands on the new Codex server setting. The
seen flag persists in UI state and is set when the toast is shown.

* fix(settings): drop the status warning from the Codex server setting

* fix(terminal): simplify the Codex shared-server notice

* fix(terminal): say agent status in the Codex notice

* fix(settings): hide the Codex server setting from paired web search

Gives its search entry an id and gates it with
includeCodexTerminalServerIsolation, as the other host-only rows are, so
a paired web client cannot find a row it never renders. Its search
keywords now use catalogued agents-search keys instead of missing ones;
CODEX_ISOLATE_ENV is no longer exported; the toast id comment names the
case it guards.

* test(settings): assert the Codex server search gate through the metadata builder

Calling getAgentsPaneSearchEntries directly stayed green with the web
gate deleted; the metadata builder test fails without it.
This commit is contained in:
Jinwoo Hong
2026-09-29 21:44:27 -04:00
committed by GitHub
parent 8a38a7a3e6
commit a0abcb6818
26 changed files with 568 additions and 17 deletions
@@ -0,0 +1,68 @@
import { describe, expect, it, vi } from 'vitest'
import { getDefaultSettings } from '../../../../shared/constants'
import type { GlobalSettings } from '../../../../shared/global-settings-types'
import { buildRuntimePtySpawnOptions } from '../runtime/spawn-options'
import { createRuntimePtySpawnState } from '../runtime/spawn-state'
import type { PtyRuntimeControllerDeps } from '../runtime/controller-deps'
import { buildPtyIpcSpawnOptions } from './spawn-options'
import { createPtyIpcSpawnState } from './spawn-state'
import type { PtySpawnIpcDeps } from './spawn-types'
function settingsWith(isolation: boolean | undefined): GlobalSettings {
const settings = getDefaultSettings('/tmp')
if (isolation === undefined) {
delete settings.codexTerminalServerIsolation
return settings
}
return { ...settings, codexTerminalServerIsolation: isolation }
}
async function ipcSpawnEnv(
isolation: boolean | undefined,
connectionId: string | null
): Promise<Record<string, string> | undefined> {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: buildPtyIpcSpawnOptions only reads the members stubbed here; the rest belong to later spawn stages this test never runs.
const deps = {
transitionSpawnHiddenRendererPtyDeliveryState: vi.fn(),
getSettings: () => settingsWith(isolation),
runtime: { registerPreAllocatedHandleForPty: vi.fn() }
} as unknown as PtySpawnIpcDeps
const ctx = createPtyIpcSpawnState(deps, { cols: 80, rows: 24, connectionId })
ctx.env = { KEEP: '1' }
await buildPtyIpcSpawnOptions(ctx)
return ctx.spawnOptions.env
}
async function runtimeSpawnEnv(
isolation: boolean | undefined,
connectionId: string | null
): Promise<Record<string, string> | undefined> {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: buildRuntimePtySpawnOptions only reads getSettings here; the rest belong to later spawn stages this test never runs.
const deps = { getSettings: () => settingsWith(isolation) } as unknown as PtyRuntimeControllerDeps
const ctx = createRuntimePtySpawnState(deps, { cols: 80, rows: 24, connectionId })
ctx.env = { KEEP: '1' }
await buildRuntimePtySpawnOptions(ctx)
return ctx.spawnOptions.env
}
// Why both hosts: SSH panes reach the relay with this env untouched, local ones reach
// node-pty or the daemon; WSL forwarding is covered by wsl-orca-env.test.ts.
describe.each([
['renderer spawn', ipcSpawnEnv],
['runtime spawn', runtimeSpawnEnv]
])('%s: Codex terminal server isolation', (_name, spawnEnv) => {
it.each([null, 'ssh-1'])(
'injects nothing while the setting is on (connection %s)',
async (id) => {
expect(await spawnEnv(true, id)).toEqual({ KEEP: '1' })
expect(await spawnEnv(undefined, id)).toEqual({ KEEP: '1' })
}
)
it.each([null, 'ssh-1'])(
'opts new terminals out with ORCA_CODEX_ISOLATE=0 when off (connection %s)',
async (id) => {
expect(await spawnEnv(false, id)).toEqual({ KEEP: '1', ORCA_CODEX_ISOLATE: '0' })
}
)
})
+6 -3
View File
@@ -23,6 +23,7 @@ import { ptySizes } from '../delivery/visibility-state'
import { shouldSeedPreAttachPtySize } from '../delivery/attached-pty-size'
import { getStartupTerminalIngressIntent } from '../../terminal-startup-color-query-replies'
import { resolveConfiguredTerminalShellArgs } from '../configured-terminal-shell-args'
import { withCodexTerminalServerIsolationEnv } from '../../../../shared/codex-terminal-server-isolation'
import { planCodexNoDaemonLaunch } from '../../../pty/codex-no-daemon-launch-command'
import type { PtyIpcSpawnState } from './spawn-state'
import { applyAgentWorkspaceTrustToSpawn } from '../../../agent-workspace-trust-spawn'
@@ -32,9 +33,11 @@ export async function buildPtyIpcSpawnOptions(
ctx: PtyIpcSpawnState
): Promise<{ isReattach: true } | null> {
const args = ctx.args
ctx.spawnEnv = ctx.preAllocatedHandle
? { ...ctx.env, ORCA_TERMINAL_HANDLE: ctx.preAllocatedHandle }
: ctx.env
// Why here: every provider (local, daemon, SSH relay, WSL) spawns from this env.
ctx.spawnEnv = withCodexTerminalServerIsolationEnv(
ctx.preAllocatedHandle ? { ...ctx.env, ORCA_TERMINAL_HANDLE: ctx.preAllocatedHandle } : ctx.env,
ctx.deps.getSettings?.()
)
const envToDelete = ctx.claudeAuth?.stripAuthEnv
? [...CLAUDE_AUTH_ENV_VARS, 'ANTHROPIC_CUSTOM_HEADERS']
: undefined
@@ -23,6 +23,7 @@ import { CLAUDE_AUTH_ENV_VARS } from '../../../claude-accounts/environment'
import { LEGACY_TERMINAL_SHIM_REMOTE_ENV_KEYS } from '../../../pty/legacy-terminal-shim-dir'
import { PI_PROCESS_OWNER_ENV_KEYS } from '../../../pty/pi-process-owner-env'
import { resolveConfiguredTerminalShellArgs } from '../configured-terminal-shell-args'
import { withCodexTerminalServerIsolationEnv } from '../../../../shared/codex-terminal-server-isolation'
import { planCodexNoDaemonLaunch } from '../../../pty/codex-no-daemon-launch-command'
import { resolveStablePaneOwner } from '../pane/stable-owner'
import { getStartupTerminalIngressIntent } from '../../terminal-startup-color-query-replies'
@@ -42,6 +43,8 @@ export async function buildRuntimePtySpawnOptions(
> {
const args = ctx.args
// Why here: every provider (local, daemon, SSH relay, WSL) spawns from this env.
ctx.env = withCodexTerminalServerIsolationEnv(ctx.env, ctx.deps.getSettings?.())
const authEnvToDelete = ctx.claudeAuth?.stripAuthEnv
? [...CLAUDE_AUTH_ENV_VARS, 'ANTHROPIC_CUSTOM_HEADERS']
: undefined
+2
View File
@@ -64,6 +64,7 @@ describe('addOrcaWslInteropEnv', () => {
ORCA_CLI_COMMAND: 'orca-ide',
ORCA_WSL_CLI_DIR: 'C:\\Users\\jin\\AppData\\Roaming\\Orca\\wsl-managed-cli\\hash',
ORCA_CODEX_LAUNCH_PREFLIGHT: 'C:\\Program Files\\Orca\\resources\\bin\\orca.exe',
ORCA_CODEX_ISOLATE: '0',
ORCA_OMP_FRESH_CONFIG: 'C:\\Orca\\fresh-session.yml',
ORCA_OMP_STATUS_EXTENSION: 'C:\\Users\\jin\\.omp\\agent\\extensions\\orca-agent-status.ts',
ORCA_PRIME_AGENT_STATUS_EXTENSION: 'C:\\stale\\orca-agent-status.ts',
@@ -90,6 +91,7 @@ describe('addOrcaWslInteropEnv', () => {
expect(env.WSLENV).toContain('ORCA_CLI_COMMAND/u')
expect(env.WSLENV).toContain('ORCA_WSL_CLI_DIR/p')
expect(env.WSLENV).toContain('ORCA_CODEX_LAUNCH_PREFLIGHT/p')
expect(env.WSLENV).toContain('ORCA_CODEX_ISOLATE/u')
expect(env.WSLENV).toContain('ORCA_OMP_STATUS_EXTENSION/p')
expect(env.WSLENV).toContain('ORCA_OMP_FRESH_CONFIG/p')
expect(env.WSLENV).not.toContain('ORCA_PRIME_AGENT_STATUS_EXTENSION')
@@ -363,6 +363,17 @@ describe('PtyHandler', () => {
}
)
it('keeps the Codex server opt-out the client asked for', async () => {
await dispatcher.callRequest('pty.spawn', {
cols: 80,
rows: 24,
env: { ORCA_CODEX_ISOLATE: '0' }
})
const spawnEnv = mockPtySpawn.mock.calls.at(-1)?.[2]?.env as Record<string, string>
expect(spawnEnv.ORCA_CODEX_ISOLATE).toBe('0')
})
it('drops an ORCA_HISTFILE handed over in the client env', async () => {
await dispatcher.callRequest('pty.spawn', {
cols: 80,
@@ -19,6 +19,7 @@ import { useWebSessionTabsSync } from '../runtime/web-session-tabs-sync'
import { useLocalStructuredSessionTabsSync } from '../runtime/local-structured-session-tabs-sync'
import { useRemoteRuntimeRecoveryTriggers } from '../runtime/use-remote-runtime-recovery-triggers'
import { useBrowserIdentityMigrationNotice } from '../components/browser-pane/browser-user-agent-migration-notice'
import { useCodexTerminalServerIsolationNotice } from '../components/terminal-pane/codex-terminal-server-isolation-notice'
/**
* App-level subscriptions that must outlive any individual surface. Each one is here because
@@ -52,4 +53,5 @@ export function useAppShellServices(options: { floatingPanelVisible: boolean }):
usePrimarySelectionPaste(primarySelectionMiddleClickPaste)
useOsc52ClipboardDefaultOnNotice(persistedUIReady)
useBrowserIdentityMigrationNotice()
useCodexTerminalServerIsolationNotice()
}
@@ -13,6 +13,7 @@ import {
getAgentWorkspaceTrustTitle
} from './agent-workspace-trust-copy'
import { getAgentAwakeDescription, getAgentAwakeTitle } from './agent-awake-copy'
import { getCodexTerminalServerIsolationTitle } from './codex-terminal-server-isolation-copy'
import { AgentAwakeSetting } from './AgentAwakeSetting'
import { AgentRuntimeSetting } from './AgentRuntimeSetting'
import type * as AgentRuntimeSettingModule from './AgentRuntimeSetting'
@@ -283,6 +284,17 @@ describe('AgentsPane', () => {
}
})
it('keeps the host-only Codex server row out of paired web clients', () => {
Reflect.set(globalThis, '__ORCA_WEB_CLIENT__', true)
try {
expect(renderPane(getDefaultSettings('/tmp'))).not.toContain(
getCodexTerminalServerIsolationTitle()
)
} finally {
Reflect.deleteProperty(globalThis, '__ORCA_WEB_CLIENT__')
}
})
it('renders the agent runtime control on Windows-class hosts', () => {
const markup = renderPane(
{
@@ -8,6 +8,7 @@ import { useAppStore } from '@/store'
import { AgentAwakeSetting } from './AgentAwakeSetting'
import { AgentCacheTimerSection } from './AgentCacheTimerSection'
import { AgentRuntimeSetting } from './AgentRuntimeSetting'
import { CodexTerminalServerIsolationSetting } from './CodexTerminalServerIsolationSetting'
import { buildCodexSessionSourceHomeControl } from './codex-session-source-home-control'
import {
getAgentGeneratedTabTitlesDescription,
@@ -260,7 +261,13 @@ export function AgentsPane({
/>
<AgentStatusHooksSetting settings={settings} updateSettings={updateSettings} />
{!isPairedWebClientWindow() ? (
<AgentWorkspaceTrustSetting settings={settings} updateSettings={updateSettings} />
<>
<AgentWorkspaceTrustSetting settings={settings} updateSettings={updateSettings} />
<CodexTerminalServerIsolationSetting
settings={settings}
updateSettings={updateSettings}
/>
</>
) : null}
<AgentGeneratedTabTitlesSetting settings={settings} updateSettings={updateSettings} />
{!isPairedWebClientWindow() ? (
@@ -0,0 +1,41 @@
import type { GlobalSettings } from '../../../../shared/global-settings-types'
import { isCodexTerminalServerIsolationEnabled } from '../../../../shared/codex-terminal-server-isolation'
import { CODEX_TERMINAL_SERVER_ISOLATION_SETTINGS_TARGET_ID } from '@/lib/settings-navigation-types'
import {
getCodexTerminalServerIsolationDescription,
getCodexTerminalServerIsolationSearchKeywords,
getCodexTerminalServerIsolationTitle
} from './codex-terminal-server-isolation-copy'
import { SearchableSetting } from './SearchableSetting'
import { SettingsSwitchRow } from './SettingsFormControls'
type CodexTerminalServerIsolationSettingProps = {
settings: GlobalSettings
updateSettings: (updates: Partial<GlobalSettings>) => void | Promise<void>
}
export function CodexTerminalServerIsolationSetting({
settings,
updateSettings
}: CodexTerminalServerIsolationSettingProps): React.JSX.Element {
const title = getCodexTerminalServerIsolationTitle()
const description = getCodexTerminalServerIsolationDescription()
const enabled = isCodexTerminalServerIsolationEnabled(settings)
return (
<section className="space-y-3">
<SearchableSetting
id={CODEX_TERMINAL_SERVER_ISOLATION_SETTINGS_TARGET_ID}
title={title}
description={description}
keywords={getCodexTerminalServerIsolationSearchKeywords()}
>
<SettingsSwitchRow
label={title}
description={description}
checked={enabled}
onChange={() => void updateSettings({ codexTerminalServerIsolation: !enabled })}
/>
</SearchableSetting>
</section>
)
}
@@ -20,6 +20,11 @@ import {
getAgentWorkspaceTrustTitle
} from './agent-workspace-trust-copy'
import { getAgentCacheTimerSearchEntries } from './agent-cache-timer-search'
import {
getCodexTerminalServerIsolationDescription,
getCodexTerminalServerIsolationSearchKeywords,
getCodexTerminalServerIsolationTitle
} from './codex-terminal-server-isolation-copy'
import { translate } from '@/i18n/i18n'
import { searchKeywords, translateSearchKeyword, uniqueKeywords } from './settings-search-keywords'
import { createLocalizedCatalog } from '@/i18n/localized-catalog'
@@ -68,11 +73,13 @@ type AgentsPaneSearchOptions = {
includeAgentAwake?: boolean
includeAgentRuntime?: boolean
includeAgentWorkspaceTrust?: boolean
includeCodexTerminalServerIsolation?: boolean
}
const AGENT_AWAKE_SEARCH_ENTRY_ID = 'agent-awake'
const AGENT_WORKSPACE_TRUST_SEARCH_ENTRY_ID = 'agent-workspace-trust'
const AGENT_RUNTIME_SEARCH_ENTRY_ID = 'agent-runtime'
const CODEX_TERMINAL_SERVER_ISOLATION_SEARCH_ENTRY_ID = 'codex-terminal-server-isolation'
const getAllAgentsPaneSearchEntries = createLocalizedCatalog(() => [
{
@@ -121,6 +128,12 @@ const getAllAgentsPaneSearchEntries = createLocalizedCatalog(() => [
description: getAgentWorkspaceTrustDescription(),
keywords: getAgentWorkspaceTrustSearchKeywords()
},
{
title: getCodexTerminalServerIsolationTitle(),
id: CODEX_TERMINAL_SERVER_ISOLATION_SEARCH_ENTRY_ID,
description: getCodexTerminalServerIsolationDescription(),
keywords: getCodexTerminalServerIsolationSearchKeywords()
},
{
title: getAgentGeneratedTabTitlesTitle(),
description: getAgentGeneratedTabTitlesDescription(),
@@ -159,15 +172,23 @@ const getAllAgentsPaneSearchEntries = createLocalizedCatalog(() => [
export function getAgentsPaneSearchEntries({
includeAgentAwake = true,
includeAgentRuntime = true,
includeAgentWorkspaceTrust = true
includeAgentWorkspaceTrust = true,
includeCodexTerminalServerIsolation = true
}: AgentsPaneSearchOptions = {}) {
const entries = getAllAgentsPaneSearchEntries()
return entries.filter(
(entry) =>
(!('id' in entry) || entry.id !== AGENT_RUNTIME_SEARCH_ENTRY_ID || includeAgentRuntime) &&
(!('id' in entry) || entry.id !== AGENT_AWAKE_SEARCH_ENTRY_ID || includeAgentAwake) &&
(!('id' in entry) ||
entry.id !== AGENT_WORKSPACE_TRUST_SEARCH_ENTRY_ID ||
includeAgentWorkspaceTrust)
const hiddenIds = new Set<string>()
if (!includeAgentAwake) {
hiddenIds.add(AGENT_AWAKE_SEARCH_ENTRY_ID)
}
if (!includeAgentRuntime) {
hiddenIds.add(AGENT_RUNTIME_SEARCH_ENTRY_ID)
}
if (!includeAgentWorkspaceTrust) {
hiddenIds.add(AGENT_WORKSPACE_TRUST_SEARCH_ENTRY_ID)
}
if (!includeCodexTerminalServerIsolation) {
hiddenIds.add(CODEX_TERMINAL_SERVER_ISOLATION_SEARCH_ENTRY_ID)
}
return getAllAgentsPaneSearchEntries().filter(
(entry) => !('id' in entry) || !hiddenIds.has(entry.id)
)
}
@@ -0,0 +1,32 @@
import { translate } from '@/i18n/i18n'
import { searchKeywords } from './settings-search-keywords'
export function getCodexTerminalServerIsolationTitle(): string {
return translate(
'settings.agents.codexTerminalServerIsolation.title',
'Run each Codex terminal on its own server'
)
}
export function getCodexTerminalServerIsolationDescription(): string {
return translate(
'settings.agents.codexTerminalServerIsolation.description',
"Keeps Orca's status and closing tabs working correctly. Turn off to use Codex's shared server and its agents overview. Applies to new terminals."
)
}
export function getCodexTerminalServerIsolationSearchKeywords(): string[] {
return searchKeywords([
{
key: 'auto.components.settings.agents.search.5ded38b843',
fallback: 'codex',
englishOnly: true
},
{ key: 'auto.components.settings.agents.search.7e15b89f6e', fallback: 'server' },
{ key: 'auto.components.settings.agents.search.ff457e1e7b', fallback: 'daemon' },
{ key: 'auto.components.settings.agents.search.9f3becc4e8', fallback: 'shared' },
{ key: 'auto.components.settings.agents.search.34337ed5c7', fallback: 'isolate' },
{ key: 'auto.components.settings.agents.search.9a84e65118', fallback: 'agents overview' },
{ key: 'auto.components.settings.agents.search.6984d4291a', fallback: 'status' }
])
}
@@ -0,0 +1,152 @@
// @vitest-environment happy-dom
import { act, createElement } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { CODEX_TERMINAL_SERVER_ISOLATION_SETTINGS_TARGET_ID } from '@/lib/settings-navigation-types'
import { useCodexTerminalServerIsolationNotice } from './codex-terminal-server-isolation-notice'
// Why a real zustand store double: the hook relies on subscribe/setState semantics.
const { toastInfoMock, harness } = vi.hoisted(() => ({
toastInfoMock: vi.fn(),
harness: { setState: (_patch: Record<string, unknown>, _replace?: true): void => {} }
}))
vi.mock('sonner', () => ({ toast: { info: toastInfoMock } }))
vi.mock('@/store', async () => {
const { createStore } = await import('zustand/vanilla')
const backing = createStore<Record<string, unknown>>()(() => ({}))
harness.setState = (patch, replace) =>
replace ? backing.setState(patch, true) : backing.setState(patch)
const useAppStore = <T>(selector: (state: Record<string, unknown>) => T): T =>
selector(backing.getState())
return { useAppStore: Object.assign(useAppStore, backing) }
})
const store = {
setState: (patch: Record<string, unknown>, replace?: true) => harness.setState(patch, replace)
}
let seen = false
const openSettingsPage = vi.fn()
const openSettingsTarget = vi.fn()
const mountedRoots: Root[] = []
function resetStore(overrides: Record<string, unknown> = {}): void {
seen = false
store.setState(
{
persistedUIReady: true,
codexTerminalServerIsolationNoticeSeen: false,
settings: { codexTerminalServerIsolation: true },
tabsByWorktree: {},
agentStatusByPaneKey: {},
paneForegroundAgentByPaneKey: {},
openSettingsPage,
openSettingsTarget,
markCodexTerminalServerIsolationNoticeSeen: () => {
seen = true
store.setState({ codexTerminalServerIsolationNoticeSeen: true })
},
...overrides
},
true
)
}
function HookProbe(): null {
useCodexTerminalServerIsolationNotice()
return null
}
async function mountProbe(): Promise<void> {
const container = document.createElement('div')
document.body.appendChild(container)
const root = createRoot(container)
mountedRoots.push(root)
await act(async () => {
root.render(createElement(HookProbe))
})
}
const codexTab = { 'wt-1': [{ id: 'tab-1', launchAgent: 'codex' }] }
describe('useCodexTerminalServerIsolationNotice', () => {
beforeEach(() => {
toastInfoMock.mockReset()
openSettingsPage.mockReset()
openSettingsTarget.mockReset()
resetStore()
})
afterEach(() => {
for (const root of mountedRoots.splice(0)) {
act(() => root.unmount())
}
document.body.innerHTML = ''
})
it('shows once when the first Codex terminal starts, and marks it seen', async () => {
await mountProbe()
expect(toastInfoMock).not.toHaveBeenCalled()
act(() => store.setState({ tabsByWorktree: codexTab }))
act(() => store.setState({ agentStatusByPaneKey: { 'tab-2:leaf': { agentType: 'codex' } } }))
expect(toastInfoMock).toHaveBeenCalledTimes(1)
expect(toastInfoMock.mock.calls[0]?.[1]).toMatchObject({ duration: Infinity })
expect(seen).toBe(true)
})
it.each([
[
'a typed codex seen by hooks',
{ agentStatusByPaneKey: { 'tab-1:leaf': { agentType: 'codex' } } }
],
[
'a typed codex in the foreground',
{ paneForegroundAgentByPaneKey: { 'tab-1:leaf': { agent: 'codex' } } }
]
])('also triggers on %s', async (_name, patch) => {
await mountProbe()
act(() => store.setState(patch))
expect(toastInfoMock).toHaveBeenCalledTimes(1)
})
it('never shows for other agents', async () => {
await mountProbe()
act(() =>
store.setState({
tabsByWorktree: { 'wt-1': [{ id: 'tab-1', launchAgent: 'claude' }] },
agentStatusByPaneKey: { 'tab-1:leaf': { agentType: 'claude' } },
paneForegroundAgentByPaneKey: { 'tab-1:leaf': { agent: 'opencode' } }
})
)
expect(toastInfoMock).not.toHaveBeenCalled()
})
it.each([
['it was already seen', { codexTerminalServerIsolationNoticeSeen: true }],
['the user turned the setting off', { settings: { codexTerminalServerIsolation: false } }],
['persisted UI has not hydrated', { persistedUIReady: false }]
])('stays quiet when %s', async (_name, overrides) => {
resetStore({ ...overrides, tabsByWorktree: codexTab })
await mountProbe()
expect(toastInfoMock).not.toHaveBeenCalled()
})
it('opens Settings at the Codex server setting', async () => {
resetStore({ tabsByWorktree: codexTab })
await mountProbe()
toastInfoMock.mock.calls[0]?.[1]?.action.onClick()
expect(openSettingsPage).toHaveBeenCalledTimes(1)
expect(openSettingsTarget).toHaveBeenCalledWith({
pane: 'agents',
repoId: null,
sectionId: CODEX_TERMINAL_SERVER_ISOLATION_SETTINGS_TARGET_ID
})
})
})
@@ -0,0 +1,111 @@
import { useEffect } from 'react'
import { toast } from 'sonner'
import { translate } from '@/i18n/i18n'
import { useAppStore } from '@/store'
import type { AppState } from '@/store/types'
import { isPairedWebClientWindow } from '@/lib/desktop-window-chrome'
import { CODEX_TERMINAL_SERVER_ISOLATION_SETTINGS_TARGET_ID } from '@/lib/settings-navigation-types'
import { isCodexTerminalServerIsolationEnabled } from '../../../../shared/codex-terminal-server-isolation'
type CodexNoticeState = Pick<
AppState,
| 'persistedUIReady'
| 'codexTerminalServerIsolationNoticeSeen'
| 'settings'
| 'tabsByWorktree'
| 'agentStatusByPaneKey'
| 'paneForegroundAgentByPaneKey'
>
// Why three sources: Orca-launched tabs, hook-reported agents (SSH too), and a typed `codex` seen locally.
function hasCodexTerminal(state: CodexNoticeState): boolean {
return (
Object.values(state.tabsByWorktree).some((tabs) =>
tabs.some((tab) => tab.launchAgent === 'codex')
) ||
Object.values(state.agentStatusByPaneKey).some((entry) => entry.agentType === 'codex') ||
Object.values(state.paneForegroundAgentByPaneKey).some((entry) => entry.agent === 'codex')
)
}
export function shouldShowCodexTerminalServerIsolationNotice(state: CodexNoticeState): boolean {
return (
state.persistedUIReady &&
!state.codexTerminalServerIsolationNoticeSeen &&
state.settings !== null &&
// Why: a user who already opted out needs no announcement of the default.
isCodexTerminalServerIsolationEnabled(state.settings) &&
hasCodexTerminal(state)
)
}
function didNoticeInputsChange(state: CodexNoticeState, previous: CodexNoticeState): boolean {
return (
state.persistedUIReady !== previous.persistedUIReady ||
state.settings !== previous.settings ||
state.tabsByWorktree !== previous.tabsByWorktree ||
state.agentStatusByPaneKey !== previous.agentStatusByPaneKey ||
state.paneForegroundAgentByPaneKey !== previous.paneForegroundAgentByPaneKey
)
}
function showCodexTerminalServerIsolationNotice(): void {
// Why mark before showing: seen means shown, so a quit or reload never repeats it.
useAppStore.getState().markCodexTerminalServerIsolationNoticeSeen()
toast.info(
translate(
'terminal.codexTerminalServerIsolationNotice.title',
'Orca now runs Codex without its shared server'
),
{
// Why a stable id: a late sync that resets the flag can't stack a second toast.
id: 'codex-terminal-server-isolation-notice',
description: translate(
'terminal.codexTerminalServerIsolationNotice.description',
'This makes agent status more reliable. You can turn it back on in Settings.'
),
duration: Infinity,
action: {
label: translate(
'terminal.codexTerminalServerIsolationNotice.openSettings',
'Open Settings'
),
onClick: () => {
const store = useAppStore.getState()
store.openSettingsPage()
store.openSettingsTarget({
pane: 'agents',
repoId: null,
sectionId: CODEX_TERMINAL_SERVER_ISOLATION_SETTINGS_TARGET_ID
})
}
}
}
)
}
export function useCodexTerminalServerIsolationNotice(): void {
const seen = useAppStore((s) => s.codexTerminalServerIsolationNoticeSeen)
useEffect(() => {
// Why: a paired web client's terminals follow the host's setting, not this window's.
if (seen || isPairedWebClientWindow()) {
return
}
if (shouldShowCodexTerminalServerIsolationNotice(useAppStore.getState())) {
showCodexTerminalServerIsolationNotice()
return
}
// Why a filtered subscription: a selector would rescan every tab on each store write.
const unsubscribe = useAppStore.subscribe((state, previous) => {
if (
didNoticeInputsChange(state, previous) &&
shouldShowCodexTerminalServerIsolationNotice(state)
) {
unsubscribe()
showCodexTerminalServerIsolationNotice()
}
})
return unsubscribe
}, [seen])
}
@@ -43,7 +43,8 @@ export function buildCapabilitySettingsSections({
searchEntries: getAgentsPaneSearchEntries({
includeAgentAwake: !isWebClient,
includeAgentRuntime: isLocalWindowsHost,
includeAgentWorkspaceTrust: !isWebClient
includeAgentWorkspaceTrust: !isWebClient,
includeCodexTerminalServerIsolation: !isWebClient
}),
group: 'capabilities'
},
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest'
import { buildSettingsNavigationMetadata } from './useSettingsNavigationMetadata'
import type { Repo } from '../../../shared/repo-types'
import { getCodexTerminalServerIsolationTitle } from '../components/settings/codex-terminal-server-isolation-copy'
const repo = {
id: 'repo-1',
@@ -179,6 +180,21 @@ describe('settings navigation metadata', () => {
)
})
it('lists the host-only Codex server setting in desktop Agents search only', () => {
const agentTitles = (isWebClient: boolean): string[] | undefined =>
buildSettingsNavigationMetadata({
isMac: false,
isWindows: false,
isWebClient,
repos: [repo]
})
.find((section) => section.id === 'agents')
?.searchEntries.map((entry) => entry.title)
expect(agentTitles(false)).toContain(getCodexTerminalServerIsolationTitle())
expect(agentTitles(true)).not.toContain(getCodexTerminalServerIsolationTitle())
})
it('keeps the Browser shortcut searchable for a capable web runtime', () => {
const sections = buildSettingsNavigationMetadata({
isMac: false,
+8 -3
View File
@@ -1588,14 +1588,12 @@
"042c551bc5": "config",
"0d1c334987": "lid",
"0d752916f8": "hooks",
"agent-trust-trust": "trust",
"agent-trust-folder": "folder",
"agent-trust-worktree": "worktree",
"13b20636a6": "waiting",
"167daeb5e9": "command",
"2afd3b5858": "enable",
"2e188c771c": "hide",
"32836788b0": "generated title",
"34337ed5c7": "isolate",
"48f84d10f1": "running",
"52115d0d7c": "auto",
"5784ae8c43": "rename",
@@ -1605,16 +1603,22 @@
"66b6b82eb4": "awake",
"6956646a1e": "title",
"6984d4291a": "status",
"7e15b89f6e": "server",
"845ad9128a": "power",
"848dcae8d3": "generated",
"8599603496": "done",
"87fffe6c20": "show",
"8a17fd6026": "stable",
"966890236d": "name",
"9a84e65118": "agents overview",
"9f3becc4e8": "shared",
"a6d594c17d": "install",
"a79d266f71": "session",
"afbf35be68": "stable session",
"affbf130f6": "working",
"agent-trust-folder": "folder",
"agent-trust-trust": "trust",
"agent-trust-worktree": "worktree",
"be59907510": "override",
"be7ea3553b": "tab",
"c1317fe641": "restore",
@@ -1627,6 +1631,7 @@
"ef804b7337": "Agent Location",
"f2932bf22b": "detected",
"f412abbba5": "claude",
"ff457e1e7b": "daemon",
"ff8de8a2ad": "display"
}
},
+16
View File
@@ -208,6 +208,12 @@
"restartRequired": "Restart required",
"unavailable": "Browser identity is unavailable."
}
},
"agents": {
"codexTerminalServerIsolation": {
"title": "Run each Codex terminal on its own server",
"description": "Keeps Orca's status and closing tabs working correctly. Turn off to use Codex's shared server and its agents overview. Applies to new terminals."
}
}
},
"menu": {
@@ -9190,6 +9196,11 @@
"042c551bc5": "config",
"f412abbba5": "claude",
"5ded38b843": "codex",
"7e15b89f6e": "server",
"ff457e1e7b": "daemon",
"9f3becc4e8": "shared",
"34337ed5c7": "isolate",
"9a84e65118": "agents overview",
"be7ea3553b": "tab",
"6956646a1e": "title",
"32836788b0": "generated title",
@@ -18523,6 +18534,11 @@
"title": "Could not save terminal session",
"capacity": "The device saving this terminal reported full storage or an exceeded storage quota. Free space on that device, then try again.",
"unknown": "Orca could not save the terminal state. Try again. If this continues, share the Orca logs with support so we can identify the cause."
},
"codexTerminalServerIsolationNotice": {
"title": "Orca now runs Codex without its shared server",
"description": "This makes agent status more reliable. You can turn it back on in Settings.",
"openSettings": "Open Settings"
}
},
"fileExplorer": {
@@ -66,6 +66,8 @@ export const BROWSER_TERMINAL_LINK_ACTIONS_SETTINGS_TARGET_ID = 'browser-termina
export const BROWSER_CLIENT_HOSTED_REMOTE_SETTINGS_TARGET_ID = 'browser-client-hosted-remote'
export const BROWSER_SSH_WORKSPACE_ROUTING_SETTINGS_TARGET_ID = 'browser-ssh-workspace-routing'
export const BROWSER_USER_AGENT_SETTINGS_TARGET_ID = 'browser-user-agent'
export const CODEX_TERMINAL_SERVER_ISOLATION_SETTINGS_TARGET_ID =
'agents-codex-terminal-server-isolation'
export const GLOBAL_WORKTREE_VISIBILITY_SETTINGS_TARGET_ID = 'general-global-worktree-visibility'
export type SettingsNavigationTarget = {
@@ -114,4 +114,6 @@ export type UISliceContextual = {
dismissUsagePercentageDisplayChangeNotice: () => void
usageEmptyStateDismissed: boolean
dismissUsageEmptyState: () => void
codexTerminalServerIsolationNoticeSeen: boolean
markCodexTerminalServerIsolationNoticeSeen: () => void
}
@@ -242,6 +242,8 @@ export function createUiHydrationActions(set: UISliceSet, _get: UISliceGet): Par
ui.usagePercentageDisplayChangeNoticeDismissed === true,
// Why: default false so existing users still see the CTA; only explicit dismissal persists true.
usageEmptyStateDismissed: ui.usageEmptyStateDismissed === true,
codexTerminalServerIsolationNoticeSeen:
ui.codexTerminalServerIsolationNoticeSeen === true,
...hydrateAgentReadState(ui),
workspaceCleanupDismissals: sanitizeWorkspaceCleanupDismissals(
ui.workspaceCleanup?.dismissals
@@ -138,6 +138,16 @@ export function createUiTrustActions(set: UISliceSet, _get: UISliceGet): Partial
}
window.api.ui.set({ usageEmptyStateDismissed: true }).catch(console.error)
return { usageEmptyStateDismissed: true }
}),
// Why default true: nothing may toast before hydration reads the persisted flag.
codexTerminalServerIsolationNoticeSeen: true,
markCodexTerminalServerIsolationNoticeSeen: () =>
set((s) => {
if (s.codexTerminalServerIsolationNoticeSeen) {
return s
}
window.api.ui.set({ codexTerminalServerIsolationNoticeSeen: true }).catch(console.error)
return { codexTerminalServerIsolationNoticeSeen: true }
})
}
}
@@ -0,0 +1,26 @@
import type { GlobalSettings } from './global-settings-types'
type CodexTerminalServerIsolationSettings =
| Partial<Pick<GlobalSettings, 'codexTerminalServerIsolation'>>
| null
| undefined
// Why this name: the codex shell wrapper (codex-shell-launch-preflight.ts) reads it to skip --no-daemon.
const CODEX_ISOLATE_ENV = 'ORCA_CODEX_ISOLATE'
export function isCodexTerminalServerIsolationEnabled(
settings: CodexTerminalServerIsolationSettings
): boolean {
return settings?.codexTerminalServerIsolation !== false
}
/** Opt-out only: with isolation on nothing is injected, so behaviour matches the pre-setting default. */
export function withCodexTerminalServerIsolationEnv(
env: Record<string, string> | undefined,
settings: CodexTerminalServerIsolationSettings
): Record<string, string> | undefined {
if (isCodexTerminalServerIsolationEnabled(settings)) {
return env
}
return { ...env, [CODEX_ISOLATE_ENV]: '0' }
}
+1
View File
@@ -221,6 +221,7 @@ export function buildDefaultSettings(args: {
agentYoloDefaultsMigrated: true,
agentStatusHooksEnabled: true,
agentWorkspaceTrustEnabled: true,
codexTerminalServerIsolation: true,
tabAutoGenerateTitle: false,
confirmClosePinnedTab: true,
editorPreviewTabsEnabled: true,
+2
View File
@@ -419,6 +419,8 @@ export type GlobalSettings = {
agentStatusHooksEnabled: boolean
/** Pre-trust the worktree or folder Orca starts an agent in, so its "trust this folder?" prompt is skipped. Defaults on. */
agentWorkspaceTrustEnabled: boolean
/** Why: Codex's shared server runs every tab's hooks with the first tab's env; off opts new terminals back into it. Absent reads as on. */
codexTerminalServerIsolation?: boolean
/** Dismissed freshness tuples: no write authority, just suppress re-nudging the same official placement/revision. */
dismissedSkillFreshnessNudges?: string[]
/** Why: generated tab titles are subjective, so they stay opt-in and manual renames win. */
+2
View File
@@ -170,6 +170,8 @@ export type PersistedUIState = {
usagePercentageDisplayChangeNoticeDismissed?: boolean
/** User-hidden empty-state usage CTA; permanently hides the "Connect AI accounts" prompt even if providers are later disconnected. */
usageEmptyStateDismissed?: boolean
/** One-shot toast announcing per-terminal Codex servers; set when shown, so absent means not yet seen. */
codexTerminalServerIsolationNoticeSeen?: boolean
/** URL for new browser tabs; null = blank tab. */
browserDefaultUrl?: string | null
browserDefaultSearchEngine?: 'google' | 'duckduckgo' | 'bing' | 'kagi' | null
@@ -238,6 +238,7 @@ export const UiUpdateFields = z
projectOrderManualDefaultNoticeDismissed: z.boolean().optional(),
usagePercentageDisplayChangeNoticeDismissed: z.boolean().optional(),
usageEmptyStateDismissed: z.boolean().optional(),
codexTerminalServerIsolationNoticeSeen: z.boolean().optional(),
petVisible: z.boolean().optional(),
petId: z.string().optional(),
customPets: UnknownRecordArray.optional(),