Improve Codex startup prompt reliability (#5686)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Brennan Benson 2026-06-18 11:39:40 -07:00 committed by GitHub
parent 3ce1a269ab
commit bc0b1e28bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
64 changed files with 1462 additions and 88 deletions

View File

@ -14,6 +14,8 @@ const { getMacDaemonSystemResolverHealthMock } = vi.hoisted(() => ({
getMacDaemonSystemResolverHealthMock: vi.fn(async () => 'unknown')
}))
const itOnPosix = process.platform === 'win32' ? it.skip : it
vi.mock('./daemon-health', async (importOriginal) => {
const actual = await importOriginal<typeof DaemonHealthModule>()
return {
@ -123,6 +125,37 @@ describe('DaemonPtyAdapter (IPtyProvider)', () => {
const result = await adapter.spawn({ cols: 80, rows: 24, worktreeId: 'wt-1' })
expect(result.id).toContain('wt-1')
})
itOnPosix('keeps plain Codex startup on the short daemon shell-ready timeout', async () => {
await adapter.spawn({
cols: 80,
rows: 24,
command: 'codex',
env: { SHELL: '/bin/zsh' }
})
await waitFor(() => vi.mocked(lastSubprocess.write).mock.calls.length > 0)
expect(lastSubprocess.write).toHaveBeenCalledWith('codex\n')
})
itOnPosix('waits for shell-ready for delivery-hinted Codex startup', async () => {
await adapter.spawn({
cols: 80,
rows: 24,
command: "codex 'linked issue context'",
startupCommandDelivery: 'shell-ready',
env: { SHELL: '/bin/zsh' }
})
await new Promise((resolve) => setTimeout(resolve, 350))
expect(lastSubprocess.write).not.toHaveBeenCalled()
lastSubprocess._simulateData('\x1b]777;orca-shell-ready\x07')
lastSubprocess._simulateData('\r\nuser@host $ ')
await waitFor(() => vi.mocked(lastSubprocess.write).mock.calls.length > 0)
expect(lastSubprocess.write).toHaveBeenCalledWith("codex 'linked issue context'\n")
})
})
describe('write', () => {

View File

@ -22,6 +22,7 @@ import {
import type { IPtyProvider, PtySpawnOptions, PtySpawnResult } from '../providers/types'
import { isShellProcess } from '../../shared/agent-detection'
import { recognizeAgentProcessFromCommandLine } from '../../shared/agent-process-recognition'
import { shouldUseShellReadyStartupDelivery } from '../../shared/codex-startup-delivery'
export type DaemonPtyAdapterOptions = {
socketPath: string
@ -136,8 +137,16 @@ export class DaemonPtyAdapter implements IPtyProvider {
await this.ensureConnected()
const shellReadySupported = opts.command ? supportsPtyStartupBarrier(opts.env ?? {}) : false
const isCodexStartupCommand =
recognizeAgentProcessFromCommandLine(opts.command)?.agent === 'codex'
const shouldWaitForShellReady =
isCodexStartupCommand &&
shouldUseShellReadyStartupDelivery({
command: opts.command,
startupCommandDelivery: opts.startupCommandDelivery
})
const shellReadyTimeoutMs =
shellReadySupported && recognizeAgentProcessFromCommandLine(opts.command)?.agent === 'codex'
shellReadySupported && isCodexStartupCommand && !shouldWaitForShellReady
? CODEX_SHELL_READY_TIMEOUT_MS
: undefined
@ -149,6 +158,7 @@ export class DaemonPtyAdapter implements IPtyProvider {
env: opts.env,
envToDelete: opts.envToDelete,
command: opts.command,
startupCommandDelivery: opts.startupCommandDelivery,
// Why: without this, the daemon always spawns cmd.exe (COMSPEC) or
// PowerShell as a fallback — regardless of which shell the renderer
// asked for in the "+" menu or persisted as the default. Forwarding

View File

@ -274,6 +274,7 @@ export class DaemonServer {
env: p.env,
envToDelete: p.envToDelete,
command: p.command,
startupCommandDelivery: p.startupCommandDelivery,
shellOverride: p.shellOverride,
terminalWindowsWslDistro: p.terminalWindowsWslDistro,
terminalWindowsPowerShellImplementation: p.terminalWindowsPowerShellImplementation,

View File

@ -951,6 +951,85 @@ describe('createPtySubprocess', () => {
expect(lastCall[2].env.ORCA_SHELL_READY_MARKER).toBe('0')
})
it('keeps plain Codex startup commands on the no-marker wrapper', () => {
const proc = mockPtyProcess()
spawnMock.mockReturnValue(proc)
const platform = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { value: 'linux' })
try {
createPtySubprocess({
sessionId: 'test',
cols: 80,
rows: 24,
command: 'codex',
env: { SHELL: '/bin/zsh' }
})
} finally {
if (platform) {
Object.defineProperty(process, 'platform', platform)
}
}
const lastCall = spawnMock.mock.calls.at(-1)!
expect(lastCall[1]).toEqual(['-l'])
expect(lastCall[2].env.ZDOTDIR).toMatch(ZSH_SHELL_READY_DIR)
expect(lastCall[2].env.ORCA_SHELL_READY_MARKER).toBe('0')
})
it('uses shell-ready wrapper for delivery-hinted Codex startup commands', () => {
const proc = mockPtyProcess()
spawnMock.mockReturnValue(proc)
const platform = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { value: 'linux' })
try {
createPtySubprocess({
sessionId: 'test',
cols: 80,
rows: 24,
command: "codex 'linked issue context'",
startupCommandDelivery: 'shell-ready',
env: { SHELL: '/bin/zsh' }
})
} finally {
if (platform) {
Object.defineProperty(process, 'platform', platform)
}
}
const lastCall = spawnMock.mock.calls.at(-1)!
expect(lastCall[1]).toEqual(['-l'])
expect(lastCall[2].env.ZDOTDIR).toMatch(ZSH_SHELL_READY_DIR)
expect(lastCall[2].env.ORCA_SHELL_READY_MARKER).toBe('1')
})
it('uses shell-ready wrapper for Codex native prefill flags', () => {
const proc = mockPtyProcess()
spawnMock.mockReturnValue(proc)
const platform = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { value: 'linux' })
try {
createPtySubprocess({
sessionId: 'test',
cols: 80,
rows: 24,
command: "codex --prefill 'linked issue context'",
env: { SHELL: '/bin/zsh' }
})
} finally {
if (platform) {
Object.defineProperty(process, 'platform', platform)
}
}
const lastCall = spawnMock.mock.calls.at(-1)!
expect(lastCall[1]).toEqual(['-l'])
expect(lastCall[2].env.ZDOTDIR).toMatch(ZSH_SHELL_READY_DIR)
expect(lastCall[2].env.ORCA_SHELL_READY_MARKER).toBe('1')
})
it('deletes requested env keys after merging daemon process env', () => {
const proc = mockPtyProcess()
spawnMock.mockReturnValue(proc)

View File

@ -33,6 +33,10 @@ import {
recognizeAgentProcess,
recognizeAgentProcessFromCommandLine
} from '../../shared/agent-process-recognition'
import {
shouldUseShellReadyStartupDelivery,
type StartupCommandDelivery
} from '../../shared/codex-startup-delivery'
import { isShellProcess } from '../../shared/shell-process-detection'
import { parsePtySessionId } from './pty-session-id'
import { getAgentForegroundContextPaths } from '../providers/agent-foreground-context-paths'
@ -51,6 +55,7 @@ export type PtySubprocessOptions = {
env?: Record<string, string>
envToDelete?: string[]
command?: string
startupCommandDelivery?: StartupCommandDelivery
/** Explicit shell executable path/basename the renderer asked for.
* Overrides env.COMSPEC / env.SHELL resolution inside the daemon so a user
* who picks "New WSL terminal" from the "+" menu actually gets WSL. */
@ -504,9 +509,15 @@ export function createPtySubprocess(opts: PtySubprocessOptions): SubprocessHandl
// wrapper need shell-ready code after user startup files run.
let shellLaunch: ReturnType<typeof getShellReadyLaunchConfig> | null = null
if (opts.command && isCodexStartupCommand) {
// Why: Codex needs the env-restoring wrapper, but waiting for a shell
// marker delays the first useful TUI frame.
shellLaunch = getAttributionShellLaunchConfig(shellPath)
const shouldWaitForShellReady = shouldUseShellReadyStartupDelivery({
command: opts.command,
startupCommandDelivery: opts.startupCommandDelivery
})
// Why: payload-bearing Codex startup text can be dropped by rc-file noise;
// plain Codex stays markerless to preserve the startup-speed path.
shellLaunch = shouldWaitForShellReady
? getShellReadyLaunchConfig(shellPath)
: getAttributionShellLaunchConfig(shellPath)
} else if (opts.command) {
shellLaunch = getShellReadyLaunchConfig(shellPath)
} else {

View File

@ -1,6 +1,7 @@
import { Session, type SubprocessHandle } from './session'
import { normalizePtySize } from './daemon-pty-size'
import { resolveProcessCwd } from '../providers/process-cwd'
import type { StartupCommandDelivery } from '../../shared/codex-startup-delivery'
import type {
SessionInfo,
TakePendingOutputResult,
@ -19,6 +20,7 @@ export type CreateOrAttachOptions = {
env?: Record<string, string>
envToDelete?: string[]
command?: string
startupCommandDelivery?: StartupCommandDelivery
/** Explicit shell the renderer asked for (e.g. 'wsl.exe' for "New WSL
* terminal" from the "+" menu). Forwarded to the subprocess spawner so the
* daemon path honors per-tab shell selection the same way LocalPtyProvider
@ -48,6 +50,7 @@ export type TerminalHostOptions = {
env?: Record<string, string>
envToDelete?: string[]
command?: string
startupCommandDelivery?: StartupCommandDelivery
shellOverride?: string
terminalWindowsWslDistro?: string | null
terminalWindowsPowerShellImplementation?: 'auto' | 'powershell.exe' | 'pwsh.exe'
@ -113,6 +116,7 @@ export class TerminalHost {
env: opts.env,
envToDelete: opts.envToDelete,
command: opts.command,
startupCommandDelivery: opts.startupCommandDelivery,
shellOverride: opts.shellOverride,
terminalWindowsWslDistro: opts.terminalWindowsWslDistro,
terminalWindowsPowerShellImplementation: opts.terminalWindowsPowerShellImplementation

View File

@ -1,11 +1,13 @@
// ─── Protocol Version ────────────────────────────────────────────────
import type { StartupCommandDelivery } from '../../shared/codex-startup-delivery'
// Why: daemons can survive app updates. Bump for IPC wire-shape changes, or
// when daemon-baked behavior cannot be delivered by on-disk wrapper refresh.
// Why: bump when adding daemon wire behavior so same-version old daemons do
// not silently accept the handshake and then reject new RPCs.
export const PROTOCOL_VERSION = 15
export const PROTOCOL_VERSION = 16
export const PREVIOUS_DAEMON_PROTOCOL_VERSIONS = [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15
] as const
// ─── Session State Machine ──────────────────────────────────────────
@ -87,6 +89,7 @@ export type CreateOrAttachRequest = {
env?: Record<string, string>
envToDelete?: string[]
command?: string
startupCommandDelivery?: StartupCommandDelivery
/** Explicit Windows shell override selected by the user (e.g. 'wsl.exe').
* The daemon forwards this to its subprocess spawner so each tab honors
* the shell picked in the "+" menu or the persisted default-shell setting,

View File

@ -4826,6 +4826,72 @@ describe('registerPtyHandlers', () => {
}
)
posixOnlyIt('waits for shell-ready before writing delivery-hinted Codex startup', async () => {
vi.useFakeTimers()
const mockProc = createMockProc()
spawnMock.mockReturnValue(mockProc.proc)
try {
registerPtyHandlers(mainWindow as never)
await handlers.get('pty:spawn')!(null, {
cols: 80,
rows: 24,
cwd: '/tmp',
command: "codex 'linked issue context'",
startupCommandDelivery: 'shell-ready'
})
const [, , options] = spawnMock.mock.calls[0]!
expect(options.env.ORCA_SHELL_READY_MARKER).toBe('1')
expect(mockProc.proc.write).not.toHaveBeenCalled()
mockProc.emitData('last login: today\r\n')
vi.advanceTimersByTime(1499)
await Promise.resolve()
expect(mockProc.proc.write).not.toHaveBeenCalled()
mockProc.emitData('\x1b]777;orca-shell-ready\x07')
await Promise.resolve()
vi.advanceTimersByTime(49)
await Promise.resolve()
expect(mockProc.proc.write).not.toHaveBeenCalled()
vi.advanceTimersByTime(1)
await Promise.resolve()
expect(mockProc.proc.write).toHaveBeenCalledWith("codex 'linked issue context'\n")
} finally {
vi.useRealTimers()
}
})
posixOnlyIt('waits for shell-ready when Codex uses the native prefill flag', async () => {
vi.useFakeTimers()
const mockProc = createMockProc()
spawnMock.mockReturnValue(mockProc.proc)
try {
registerPtyHandlers(mainWindow as never)
await handlers.get('pty:spawn')!(null, {
cols: 80,
rows: 24,
cwd: '/tmp',
command: "codex --prefill 'linked issue context'"
})
const [, , options] = spawnMock.mock.calls[0]!
expect(options.env.ORCA_SHELL_READY_MARKER).toBe('1')
expect(mockProc.proc.write).not.toHaveBeenCalled()
mockProc.emitData('\x1b]777;orca-shell-ready\x07')
await Promise.resolve()
vi.runAllTimers()
await Promise.resolve()
expect(mockProc.proc.write).toHaveBeenCalledWith("codex --prefill 'linked issue context'\n")
} finally {
vi.useRealTimers()
}
})
posixOnlyIt('keeps the conservative max wait for non-agent startup commands', async () => {
vi.useFakeTimers()
const mockProc = createMockProc()

View File

@ -23,6 +23,7 @@ import { detectPiAgentKindFromCommand, type PiAgentKind } from '../../shared/pi-
import { isPwshAvailable } from '../pwsh'
import { LocalPtyProvider } from '../providers/local-pty-provider'
import type { IPtyProvider, PtySpawnOptions, PtySpawnResult } from '../providers/types'
import type { StartupCommandDelivery } from '../../shared/codex-startup-delivery'
import { SSH_SESSION_EXPIRED_ERROR, isSshPtyNotFoundError } from '../providers/ssh-pty-provider'
import { parseAppSshPtyId, toAppSshPtyId, toRelaySshPtyId } from '../providers/ssh-pty-id'
import { mintPtySessionId, isSafePtySessionId } from '../daemon/pty-session-id'
@ -1750,6 +1751,9 @@ export function registerPtyHandlers(
if (args.command !== undefined) {
spawnOptions.command = args.command
}
if (args.startupCommandDelivery !== undefined) {
spawnOptions.startupCommandDelivery = args.startupCommandDelivery
}
if (args.worktreeId !== undefined) {
spawnOptions.worktreeId = args.worktreeId
}
@ -2098,6 +2102,7 @@ export function registerPtyHandlers(
env?: Record<string, string>
envToDelete?: string[]
command?: string
startupCommandDelivery?: StartupCommandDelivery
connectionId?: string | null
worktreeId?: string
sessionId?: string
@ -2348,6 +2353,9 @@ export function registerPtyHandlers(
if (args.command !== undefined) {
spawnOptions.command = args.command
}
if (args.startupCommandDelivery !== undefined) {
spawnOptions.startupCommandDelivery = args.startupCommandDelivery
}
if (args.worktreeId !== undefined) {
spawnOptions.worktreeId = args.worktreeId
}

View File

@ -245,6 +245,7 @@ async function spawnLocalStartupAndSetupTerminals(args: {
const terminal = await runtime.createTerminal(`id:${worktree.id}`, {
command: startup.command,
env: startup.env,
startupCommandDelivery: startup.startupCommandDelivery,
telemetry: startup.telemetry,
activate: true
})

View File

@ -191,6 +191,35 @@ describe('LocalPtyProvider', () => {
expect(spawnCall[2].env.CUSTOM_VAR).toBe('custom-value')
})
it('uses fallback shell readiness when startup-command shell spawn falls back', async () => {
vi.useFakeTimers()
try {
process.env.SHELL = '/usr/bin/fish'
spawnMock.mockImplementationOnce(() => {
throw new Error('fish failed')
})
spawnMock.mockReturnValue(mockProc)
await provider.spawn({ cols: 80, rows: 24, command: "printf 'linked issue context'" })
expect(spawnMock.mock.calls[0]?.[0]).toBe('/bin/zsh')
await Promise.resolve()
vi.advanceTimersByTime(50)
await Promise.resolve()
expect(mockProc.write).not.toHaveBeenCalled()
const dataCallback = mockProc.onData.mock.calls[0]?.[0] as (data: string) => void
dataCallback('\x1b]777;orca-shell-ready\x07user@host % ')
await Promise.resolve()
vi.advanceTimersByTime(50)
await Promise.resolve()
expect(mockProc.write).toHaveBeenCalledWith("printf 'linked issue context'\n")
} finally {
vi.useRealTimers()
}
})
it('honors explicit terminal env overrides after deleting requested defaults', async () => {
provider.configure({
buildSpawnEnv: (_id, env) => {

View File

@ -42,6 +42,7 @@ import { WINDOWS_GIT_BASH_SHELL } from '../../shared/windows-terminal-shell'
import { resolveAgentForegroundProcess } from './agent-foreground-process'
import { getAgentForegroundContextPaths } from './agent-foreground-context-paths'
import { recognizeAgentProcessFromCommandLine } from '../../shared/agent-process-recognition'
import { shouldUseShellReadyStartupDelivery } from '../../shared/codex-startup-delivery'
const PANE_IDENTITY_ENV_KEYS = ['ORCA_PANE_KEY', 'ORCA_TAB_ID', 'ORCA_WORKTREE_ID'] as const
@ -476,10 +477,19 @@ export class LocalPtyProvider implements IPtyProvider {
recognizeAgentProcessFromCommandLine(args.command)?.agent === 'codex'
let shellLaunch: ReturnType<typeof getShellReadyLaunchConfig> | null = null
if (args.command && isCodexStartupCommand) {
// Why: Codex needs the env-restoring wrapper, but waiting for a shell
// marker delays the first useful TUI frame.
getFallbackShellReadyConfig = (shell) => getAttributionShellLaunchConfig(shell)
shellLaunch = getAttributionShellLaunchConfig(shellPath)
const shouldWaitForShellReady = shouldUseShellReadyStartupDelivery({
command: args.command,
startupCommandDelivery: args.startupCommandDelivery
})
// Why: payload-bearing Codex startup text can be dropped by rc-file noise;
// plain Codex stays markerless to preserve the startup-speed path.
getFallbackShellReadyConfig = (shell) =>
shouldWaitForShellReady
? getShellReadyLaunchConfig(shell)
: getAttributionShellLaunchConfig(shell)
shellLaunch = shouldWaitForShellReady
? getShellReadyLaunchConfig(shellPath)
: getAttributionShellLaunchConfig(shellPath)
} else if (args.command) {
getFallbackShellReadyConfig = (shell) => getShellReadyLaunchConfig(shell)
shellLaunch = getShellReadyLaunchConfig(shellPath)
@ -534,6 +544,9 @@ export class LocalPtyProvider implements IPtyProvider {
: undefined
})
shellPath = spawnResult.shellPath
if (args.command && getFallbackShellReadyConfig) {
shellReadyLaunch = getFallbackShellReadyConfig(shellPath)
}
if (process.platform !== 'win32') {
finalEnv.SHELL = shellPath

View File

@ -8,7 +8,11 @@ import { join, dirname } from 'path'
import { mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync } from 'fs'
import type * as pty from 'node-pty'
import type * as LocalPtyShellReadyModule from './local-pty-shell-ready'
import { writeStartupCommandWhenShellReady } from './local-pty-shell-ready'
import {
createShellReadyScanState,
scanForShellReady,
writeStartupCommandWhenShellReady
} from './local-pty-shell-ready'
const { getUserDataPathMock } = vi.hoisted(() => ({
getUserDataPathMock: vi.fn<() => string>()
@ -128,6 +132,21 @@ describe('writeStartupCommandWhenShellReady', () => {
})
})
describe('scanForShellReady', () => {
it('flushes marker-like output when the full marker is not BEL-terminated', () => {
const state = createShellReadyScanState()
expect(scanForShellReady(state, 'before \x1b]777;orca-shell-readyx')).toEqual({
output: 'before \x1b]777;orca-shell-readyx',
matched: false
})
expect(scanForShellReady(state, ' after')).toEqual({
output: ' after',
matched: false
})
})
})
const describePosix = process.platform === 'win32' ? describe.skip : describe
const hasBash = process.platform !== 'win32' && spawnSync('bash', ['--version']).status === 0
const itWithBash = hasBash ? it : it.skip

View File

@ -74,7 +74,15 @@ export function scanForShellReady(
state.matchPos = 0
return { output: output + remaining, matched: true }
} else {
state.heldBytes += ch
output += state.heldBytes
state.heldBytes = ''
state.matchPos = 0
if (ch === SHELL_READY_MARKER[0]) {
state.heldBytes = ch
state.matchPos = 1
} else {
output += ch
}
}
}

View File

@ -137,7 +137,10 @@ export class SshPtyProvider implements IPtyProvider {
// relay does not execute `command` itself — the user types it into
// the shell — but receiving it as a hint lets overlay resolution be
// per-launch instead of always-Pi.
...(opts.command ? { command: opts.command } : {})
...(opts.command ? { command: opts.command } : {}),
...(opts.startupCommandDelivery
? { startupCommandDelivery: opts.startupCommandDelivery }
: {})
})
return {
...(result as PtySpawnResult),

View File

@ -18,6 +18,7 @@ import type {
import type { GitHistoryOptions, GitHistoryResult } from '../../shared/git-history'
import type { CommitMessageDraftContext } from '../../shared/commit-message-generation'
import type { WorkspaceSpaceDirectoryScanResult } from '../../shared/workspace-space-types'
import type { StartupCommandDelivery } from '../../shared/codex-startup-delivery'
// ─── PTY Provider ───────────────────────────────────────────────────
@ -28,6 +29,7 @@ export type PtySpawnOptions = {
env?: Record<string, string>
envToDelete?: string[]
command?: string
startupCommandDelivery?: StartupCommandDelivery
/** Orca worktree identity. When present, the local provider scopes shell
* history to this worktree so ArrowUp only surfaces local commands. */
worktreeId?: string

View File

@ -902,6 +902,7 @@ type RuntimePtyController = {
rows: number
cwd?: string
command?: string
startupCommandDelivery?: WorktreeStartupLaunch['startupCommandDelivery']
env?: Record<string, string>
envToDelete?: string[]
telemetry?: WorktreeStartupLaunch['telemetry']
@ -3161,6 +3162,7 @@ export class OrcaRuntimeService {
true,
undefined,
undefined,
undefined,
{
tabId: tab.parentTabId,
leafId: tab.leafId,
@ -9923,6 +9925,9 @@ export class OrcaRuntimeService {
agent,
startup: {
command: draftLaunchPlan.launchCommand,
...(draftLaunchPlan.startupCommandDelivery
? { startupCommandDelivery: draftLaunchPlan.startupCommandDelivery }
: {}),
...(draftLaunchPlan.env ? { env: draftLaunchPlan.env } : {})
}
}
@ -9944,6 +9949,9 @@ export class OrcaRuntimeService {
agent,
startup: {
command: startupPlan.launchCommand,
...(startupPlan.startupCommandDelivery
? { startupCommandDelivery: startupPlan.startupCommandDelivery }
: {}),
...(startupPlan.env ? { env: startupPlan.env } : {})
},
draftPaste: { agent, content }
@ -9981,6 +9989,9 @@ export class OrcaRuntimeService {
agent,
startup: {
command: startupPlan.launchCommand,
...(startupPlan.startupCommandDelivery
? { startupCommandDelivery: startupPlan.startupCommandDelivery }
: {}),
...(startupPlan.env ? { env: startupPlan.env } : {})
},
...(startupPlan.followupPrompt
@ -10400,6 +10411,7 @@ export class OrcaRuntimeService {
const terminal = await this.createTerminal(`id:${worktree.id}`, {
command: effectiveStartup.command,
env: effectiveStartup.env,
startupCommandDelivery: effectiveStartup.startupCommandDelivery,
telemetry: effectiveStartup.telemetry
})
if (effectiveDraftPaste) {
@ -10981,6 +10993,7 @@ export class OrcaRuntimeService {
const terminal = await this.createTerminal(`id:${worktree.id}`, {
command: effectiveStartup.command,
env: effectiveStartup.env,
startupCommandDelivery: effectiveStartup.startupCommandDelivery,
telemetry: effectiveStartup.telemetry
})
if (effectiveDraftPaste) {
@ -11237,6 +11250,7 @@ export class OrcaRuntimeService {
const terminal = await this.createTerminal(`path:${result.worktree.path}`, {
command: args.startup.command,
env: args.startup.env,
startupCommandDelivery: args.startup.startupCommandDelivery,
telemetry: args.startup.telemetry
})
if (args.startupDraftPaste) {
@ -12765,6 +12779,7 @@ export class OrcaRuntimeService {
opts: {
command?: string
env?: Record<string, string>
startupCommandDelivery?: WorktreeStartupLaunch['startupCommandDelivery']
telemetry?: WorktreeStartupLaunch['telemetry']
title?: string
focus?: boolean
@ -12842,6 +12857,7 @@ export class OrcaRuntimeService {
rows: 40,
cwd: workspace.path,
command: agentTeamsPlan?.command ?? opts.command,
startupCommandDelivery: opts.startupCommandDelivery,
env,
envToDelete: agentTeamsPlan?.envToDelete,
telemetry: opts.telemetry,
@ -12938,6 +12954,7 @@ export class OrcaRuntimeService {
requestId,
worktreeId,
command: opts.command,
startupCommandDelivery: opts.startupCommandDelivery,
title: opts.title,
activate: opts.focus === true || opts.activate === true
})
@ -12974,6 +12991,7 @@ export class OrcaRuntimeService {
return await this.createTerminal(`id:${worktree.id}`, {
command: startup.startup.command,
env: startup.startup.env,
startupCommandDelivery: startup.startup.startupCommandDelivery,
telemetry: startup.startup.telemetry,
title: opts.title
})
@ -12985,6 +13003,7 @@ export class OrcaRuntimeService {
afterTabId?: string
targetGroupId?: string
command?: string
startupCommandDelivery?: WorktreeStartupLaunch['startupCommandDelivery']
agent?: TuiAgent
activate?: boolean
} = {}
@ -13011,6 +13030,7 @@ export class OrcaRuntimeService {
opts.activate !== false,
opts.afterTabId,
command,
opts.startupCommandDelivery,
undefined,
opts.agent
)
@ -13044,6 +13064,7 @@ export class OrcaRuntimeService {
afterTabId: afterDesktopTabId,
targetGroupId: opts.targetGroupId,
command,
startupCommandDelivery: opts.startupCommandDelivery,
activate: opts.activate
})
})
@ -13100,6 +13121,7 @@ export class OrcaRuntimeService {
activate: boolean,
afterTabId?: string,
command?: string,
startupCommandDelivery?: WorktreeStartupLaunch['startupCommandDelivery'],
identity?: { tabId: string; leafId: string; sessionId?: string },
launchAgent?: TuiAgent
): Promise<RuntimeMobileSessionCreateTerminalResult> {
@ -13111,6 +13133,7 @@ export class OrcaRuntimeService {
const terminal = await this.createTerminal(`id:${worktreeId}`, {
focus: false,
command,
startupCommandDelivery,
...(identity
? {
tabId: identity.tabId,

View File

@ -119,6 +119,7 @@ describe('session tab RPC methods', () => {
afterTabId: undefined,
targetGroupId: 'group-left',
command: 'zsh',
startupCommandDelivery: undefined,
agent: undefined,
activate: true
})
@ -156,11 +157,51 @@ describe('session tab RPC methods', () => {
afterTabId: undefined,
targetGroupId: undefined,
command: undefined,
startupCommandDelivery: undefined,
agent: 'codex',
activate: undefined
})
})
it('dispatches terminal creation with startup command delivery metadata', async () => {
const runtime = {
getRuntimeId: () => 'test-runtime',
createMobileSessionTerminal: vi.fn().mockResolvedValue({
tab: {
type: 'terminal',
id: 'tab-1::leaf-1',
parentTabId: 'tab-1',
leafId: 'leaf-1',
title: 'Terminal',
status: 'ready',
terminal: 'pty-1',
isActive: true
},
publicationEpoch: 'epoch-1',
snapshotVersion: 1
})
} as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: SESSION_TAB_METHODS })
const response = await dispatcher.dispatch(
makeRequest('session.tabs.createTerminal', {
worktree: 'id:wt-1',
command: "codex 'linked issue context'",
startupCommandDelivery: 'shell-ready'
})
)
expect(response.ok).toBe(true)
expect(runtime.createMobileSessionTerminal).toHaveBeenCalledWith('id:wt-1', {
afterTabId: undefined,
targetGroupId: undefined,
command: "codex 'linked issue context'",
startupCommandDelivery: 'shell-ready',
agent: undefined,
activate: undefined
})
})
it('rejects unknown agent presets without creating a terminal', async () => {
const runtime = {
getRuntimeId: () => 'test-runtime',

View File

@ -26,6 +26,7 @@ const CreateTerminalTab = WorktreeTabSelector.extend({
afterTabId: z.string().optional(),
targetGroupId: z.string().optional(),
command: z.string().optional(),
startupCommandDelivery: z.enum(['fast', 'shell-ready']).optional(),
agent: z
.custom<TuiAgent>(isTuiAgent, {
message: 'Unknown agent preset'
@ -111,6 +112,7 @@ export const SESSION_TAB_METHODS: RpcAnyMethod[] = [
afterTabId: params.afterTabId,
targetGroupId: params.targetGroupId,
command: params.command,
startupCommandDelivery: params.startupCommandDelivery,
agent: params.agent,
activate: params.activate
})

View File

@ -507,6 +507,7 @@ const TerminalWait = TerminalHandle.extend({
const TerminalCreateParams = z.object({
worktree: OptionalString,
command: OptionalString,
startupCommandDelivery: z.enum(['fast', 'shell-ready']).optional(),
env: z.record(z.string(), z.string()).optional(),
title: OptionalString,
focus: z.unknown().optional(),
@ -774,6 +775,7 @@ export const TERMINAL_METHODS: RpcAnyMethod[] = [
handler: async (params, { runtime }) => ({
terminal: await runtime.createTerminal(params.worktree, {
command: params.command,
startupCommandDelivery: params.startupCommandDelivery,
env: params.env,
title: params.title,
focus: params.focus === true,

View File

@ -118,6 +118,7 @@ export const WorktreeCreate = z
// terminal pane launches the selected agent instead of an idle shell.
startupCommand: OptionalString,
startupEnv: z.record(z.string(), z.string()).optional(),
startupCommandDelivery: z.enum(['fast', 'shell-ready']).optional(),
// Why: CLI clients should not hardcode agent launch quoting because SSH
// workspaces execute in a different shell than the client process.
startupAgent: OptionalTuiAgent,

View File

@ -83,6 +83,7 @@ describe('worktree RPC methods', () => {
repo: 'repo-1',
name: 'agent-startup',
startupCommand: "codex 'summarize repo'",
startupCommandDelivery: 'shell-ready',
startupEnv: { ORCA_AGENT_MODE: 'direct' },
activate: true
})
@ -95,6 +96,7 @@ describe('worktree RPC methods', () => {
activate: true,
startup: {
command: "codex 'summarize repo'",
startupCommandDelivery: 'shell-ready',
env: { ORCA_AGENT_MODE: 'direct' }
}
})

View File

@ -89,7 +89,10 @@ export const WORKTREE_METHODS: RpcMethod[] = [
startup: params.startupCommand
? {
command: params.startupCommand,
...(params.startupEnv ? { env: params.startupEnv } : {})
...(params.startupEnv ? { env: params.startupEnv } : {}),
...(params.startupCommandDelivery
? { startupCommandDelivery: params.startupCommandDelivery }
: {})
}
: undefined,
...(params.startupAgent ? { startupAgent: params.startupAgent } : {}),

View File

@ -13,6 +13,7 @@ import type { AppIdentity } from '../shared/app-identity'
import type { TerminalPaneSplitSource } from '../shared/feature-education-telemetry'
import type { TaskSourceContext } from '../shared/task-source-context'
import type { ProjectExecutionRuntimeResolution } from '../shared/project-execution-runtime'
import type { StartupCommandDelivery } from '../shared/codex-startup-delivery'
import type {
FolderWorkspacePathStatus,
FolderWorkspacePathStatusRequest
@ -1014,6 +1015,7 @@ export type PreloadApi = {
cwd?: string
env?: Record<string, string>
command?: string
startupCommandDelivery?: StartupCommandDelivery
connectionId?: string | null
worktreeId?: string
sessionId?: string
@ -2385,6 +2387,7 @@ export type PreloadApi = {
afterTabId?: string
targetGroupId?: string
command?: string
startupCommandDelivery?: StartupCommandDelivery
title?: string
activate?: boolean
}) => void

View File

@ -10,6 +10,7 @@ import type { CliInstallStatus } from '../shared/cli-install-types'
import type { AgentHookInstallStatus } from '../shared/agent-hook-types'
import type { TerminalPaneSplitSource } from '../shared/feature-education-telemetry'
import type { ProjectExecutionRuntimeResolution } from '../shared/project-execution-runtime'
import type { StartupCommandDelivery } from '../shared/codex-startup-delivery'
import type {
BaseRefSearchResult,
BaseRefDefaultResult,
@ -694,6 +695,7 @@ const api = {
cwd?: string
env?: Record<string, string>
command?: string
startupCommandDelivery?: StartupCommandDelivery
connectionId?: string | null
worktreeId?: string
sessionId?: string
@ -1697,7 +1699,7 @@ const api = {
copilotStatus: (): Promise<AgentHookInstallStatus> =>
ipcRenderer.invoke('agentHooks:copilotStatus'),
hermesStatus: (): Promise<AgentHookInstallStatus> =>
ipcRenderer.invoke('agentHooks:hermesStatus'),
ipcRenderer.invoke('agentHooks:hermesStatus')
},
agentTrust: {
@ -3002,6 +3004,7 @@ const api = {
afterTabId?: string
targetGroupId?: string
command?: string
startupCommandDelivery?: StartupCommandDelivery
title?: string
activate?: boolean
}) => void
@ -3014,6 +3017,7 @@ const api = {
afterTabId?: string
targetGroupId?: string
command?: string
startupCommandDelivery?: StartupCommandDelivery
title?: string
activate?: boolean
}

View File

@ -165,6 +165,76 @@ describe('PtyHandler', () => {
expect(handler.activePtyCount).toBe(1)
})
it.skipIf(process.platform === 'win32')(
'enables shell-ready marker env for delivery-hinted startup commands',
async () => {
const oldShell = process.env.SHELL
const oldHome = process.env.HOME
const homeDir = mkdtempSync(join(tmpdir(), 'relay-shell-ready-spawn-'))
process.env.SHELL = '/bin/bash'
process.env.HOME = homeDir
try {
await dispatcher.callRequest('pty.spawn', {
env: { HOME: homeDir },
startupCommandDelivery: 'shell-ready'
})
} finally {
if (oldShell === undefined) {
delete process.env.SHELL
} else {
process.env.SHELL = oldShell
}
if (oldHome === undefined) {
delete process.env.HOME
} else {
process.env.HOME = oldHome
}
rmSync(homeDir, { recursive: true, force: true })
}
const spawnOptions = mockPtySpawn.mock.calls[0]?.[2] as
| { env?: Record<string, string> }
| undefined
expect(spawnOptions?.env?.ORCA_SHELL_READY_MARKER).toBe('1')
}
)
it.skipIf(process.platform === 'win32')(
'enables shell-ready marker env for Codex native prefill commands',
async () => {
const oldShell = process.env.SHELL
const oldHome = process.env.HOME
const homeDir = mkdtempSync(join(tmpdir(), 'relay-codex-prefill-spawn-'))
process.env.SHELL = '/bin/bash'
process.env.HOME = homeDir
try {
await dispatcher.callRequest('pty.spawn', {
env: { HOME: homeDir },
command: "codex --prefill 'linked issue context'"
})
} finally {
if (oldShell === undefined) {
delete process.env.SHELL
} else {
process.env.SHELL = oldShell
}
if (oldHome === undefined) {
delete process.env.HOME
} else {
process.env.HOME = oldHome
}
rmSync(homeDir, { recursive: true, force: true })
}
const spawnOptions = mockPtySpawn.mock.calls[0]?.[2] as
| { env?: Record<string, string> }
| undefined
expect(spawnOptions?.env?.ORCA_SHELL_READY_MARKER).toBe('1')
}
)
it('terminates spawned PTY when request becomes stale before response', async () => {
const killSpy = vi.fn()
const term = { ...mockPtyInstance, kill: killSpy, onData: vi.fn(), onExit: vi.fn() }

View File

@ -12,6 +12,7 @@ import {
} from './pty-shell-utils'
import { getRelayShellLaunchConfig } from './pty-shell-launch'
import { DEFAULT_SSH_RELAY_GRACE_PERIOD_SECONDS } from '../shared/ssh-types'
import { shouldUseShellReadyStartupDelivery } from '../shared/codex-startup-delivery'
// Why: node-pty is a native addon that may not be installed on the remote.
// Dynamic import keeps the require() lazy so loadPty() returns null gracefully
@ -433,7 +434,15 @@ export class PtyHandler {
const paneKey = typeof env?.ORCA_PANE_KEY === 'string' ? env.ORCA_PANE_KEY : undefined
const command = typeof params.command === 'string' ? params.command : undefined
const spawnEnv = this.buildSpawnEnv(env, { id, paneKey, shell, command })
const shellLaunch = getRelayShellLaunchConfig(shell, spawnEnv)
// Why: only explicit shell-ready hints are trusted here; native Codex
// prefill detection still auto-enables readiness through the predicate.
const shellLaunch = getRelayShellLaunchConfig(shell, spawnEnv, process.platform, {
emitReadyMarker: shouldUseShellReadyStartupDelivery({
command,
startupCommandDelivery:
params.startupCommandDelivery === 'shell-ready' ? 'shell-ready' : undefined
})
})
// Why: SSH exec channels give the relay a minimal environment without
// .zprofile/.bash_profile sourced. Spawning a login shell ensures PATH

View File

@ -151,6 +151,37 @@ describe('getRelayShellLaunchConfig', () => {
}
)
it.skipIf(process.platform === 'win32')(
'enables the shell-ready marker for requested zsh startup delivery',
() => {
const config = getRelayShellLaunchConfig('/bin/zsh', { HOME: homeDir }, 'linux', {
emitReadyMarker: true
})
const zshRoot = join(homeDir, '.orca-relay', 'shell-ready', 'zsh')
const zlogin = readFileSync(join(zshRoot, '.zlogin'), 'utf8')
expect(config.args).toEqual(['-l'])
expect(config.env.ZDOTDIR).toBe(zshRoot)
expect(config.env.ORCA_SHELL_READY_MARKER).toBe('1')
expect(zlogin).toContain('zle -N zle-line-init __orca_prompt_mark')
expect(zlogin).toContain('printf "\\033]777;orca-shell-ready\\007"')
}
)
it.skipIf(process.platform === 'win32')(
'enables the shell-ready marker for requested bash startup delivery',
() => {
const config = getRelayShellLaunchConfig('/bin/bash', { HOME: homeDir }, 'linux', {
emitReadyMarker: true
})
const bashRc = readFileSync(config.args[1] as string, 'utf8')
expect(config.env.ORCA_SHELL_READY_MARKER).toBe('1')
expect(bashRc).toContain('__orca_append_prompt_command "__orca_prompt_mark"')
expect(bashRc).toContain('printf "\\033]777;orca-shell-ready\\007"')
}
)
itWithBash('runs the relay bash wrapper without fake C/D markers before the first prompt', () => {
const config = getRelayShellLaunchConfig('/bin/bash', { HOME: homeDir })
const output = runInteractiveBashRcfile(config.args[1] as string, homeDir)

View File

@ -4,11 +4,13 @@ import { dirname, join } from 'path'
import { getPosixOmpShellWrapper } from '../main/pty/omp-shell-wrapper'
import {
getZshFinalZdotdirRestoreBlock,
getZshShellReadyMarkerRegistrationBlock,
getZshStartupFileSourceBlock
} from '../main/shell-templates'
const RELAY_SHELL_READY_DIR = '.orca-relay/shell-ready'
const POSIX_LOGIN_ARGS = ['-l']
const SHELL_READY_MARKER_ESCAPED = '\\033]777;orca-shell-ready\\007'
export type RelayShellLaunchConfig = {
args: string[]
@ -116,6 +118,7 @@ ${getZshStartupFileSourceBlock({
[[ -n "\${ORCA_REMOTE_CLI_BIN_DIR:-}" ]] && case ":$PATH:" in *:"\${ORCA_REMOTE_CLI_BIN_DIR}":*) ;; *) export PATH="\${ORCA_REMOTE_CLI_BIN_DIR}:$PATH" ;; esac
${getPosixOmpShellWrapper()}
${getZshFinalZdotdirRestoreBlock('"${ORCA_USER_ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"')}
${getZshShellReadyMarkerRegistrationBlock(SHELL_READY_MARKER_ESCAPED)}
`
const bashRc = `# Orca relay bash overlay wrapper
[[ -f /etc/profile ]] && source /etc/profile
@ -186,6 +189,14 @@ __orca_append_prompt_command() {
fi
}
__orca_prepend_prompt_command
# Why: SSH startup commands are renderer-delivered; emit the same internal
# readiness marker as local shells only when that delivery mode asks for it.
if [[ "\${ORCA_SHELL_READY_MARKER:-0}" == "1" ]]; then
__orca_prompt_mark() {
printf "${SHELL_READY_MARKER_ESCAPED}"
}
__orca_append_prompt_command "__orca_prompt_mark"
fi
__orca_append_prompt_command "__orca_osc133_prompt_done"
__orca_debug_trap_spec="$(trap -p DEBUG)"
if [[ -n "$__orca_debug_trap_spec" ]]; then
@ -229,9 +240,11 @@ trap '__orca_osc133_preexec' DEBUG
export function getRelayShellLaunchConfig(
shellPath: string,
env: Record<string, string>,
platform: NodeJS.Platform = process.platform
platform: NodeJS.Platform = process.platform,
options: { emitReadyMarker?: boolean } = {}
): RelayShellLaunchConfig {
const shellName = shellBasename(shellPath)
const emitReadyMarker = options.emitReadyMarker === true
if (platform === 'win32') {
// Why: pwsh also exists on POSIX remotes; Windows-specific shell args must
// only apply when the relay itself is running on native Windows.
@ -241,7 +254,9 @@ export function getRelayShellLaunchConfig(
if (shellName !== 'zsh' && shellName !== 'bash') {
return { args: POSIX_LOGIN_ARGS, env: {} }
}
if (shellName === 'zsh' && !hasOverlayRestoreEnv(env)) {
// Why: preserve plain zsh startup fast path; only force wrappers when
// shell-ready or overlay env restoration is requested.
if (shellName === 'zsh' && !hasOverlayRestoreEnv(env) && !emitReadyMarker) {
return { args: POSIX_LOGIN_ARGS, env: {} }
}
@ -253,13 +268,14 @@ export function getRelayShellLaunchConfig(
args: POSIX_LOGIN_ARGS,
env: {
ORCA_ORIG_ZDOTDIR: resolveOriginalZdotdir(env),
ZDOTDIR: join(root, 'zsh')
ZDOTDIR: join(root, 'zsh'),
...(emitReadyMarker ? { ORCA_SHELL_READY_MARKER: '1' } : {})
}
}
}
return {
args: ['--rcfile', join(root, 'bash', 'rcfile')],
env: {}
env: emitReadyMarker ? { ORCA_SHELL_READY_MARKER: '1' } : {}
}
}

View File

@ -78,6 +78,9 @@ export function FloatingTerminalWindowControls({
state.queueTabStartupCommand(tab.id, {
command: startupPlan.launchCommand,
...(startupPlan.env ? { env: startupPlan.env } : {}),
...(startupPlan.startupCommandDelivery
? { startupCommandDelivery: startupPlan.startupCommandDelivery }
: {}),
telemetry: {
agent_kind: tuiAgentToAgentKind(defaultAgent),
launch_source: 'shortcut',

View File

@ -131,6 +131,9 @@ export async function submitFolderWorkspaceCreate({
? {
command: startupPlan.launchCommand,
...(startupPlan.env ? { env: startupPlan.env } : {}),
...(startupPlan.startupCommandDelivery
? { startupCommandDelivery: startupPlan.startupCommandDelivery }
: {}),
telemetry: {
agent_kind: tuiAgentToAgentKind(quickAgent),
launch_source: launchSource,

View File

@ -3,6 +3,7 @@ import type { ReplayingPanesRef } from './replay-guard'
import type { AgentCompletionStatusSnapshot } from './agent-completion-coordinator-types'
import type { EventProps } from '../../../../shared/telemetry-events'
import type { TerminalColorSchemeMode } from '../../../../shared/terminal-color-scheme-protocol'
import type { StartupCommandDelivery } from '../../../../shared/codex-startup-delivery'
import type { TuiAgent } from '../../../../shared/types'
export type PtyConnectionDeps = {
@ -14,6 +15,7 @@ export type PtyConnectionDeps = {
/** Renderer-delivered startup input for callers that need xterm paste
* semantics before the submit Enter. */
delivery?: 'terminal-paste'
startupCommandDelivery?: StartupCommandDelivery
env?: Record<string, string>
/** Telemetry payload for `agent_started`. Forwarded to `pty:spawn`
* so main fires the event only after the spawn succeeds. */

View File

@ -2432,7 +2432,7 @@ describe('connectPanePty', () => {
expect(transport.sendInput).not.toHaveBeenCalled()
})
it('sends startup command via sendInput for SSH connections (relay has no shell-ready mechanism)', async () => {
it('sends fast startup commands via sendInput for SSH connections', async () => {
// Capture the setTimeout callback directly so we can fire it without
// vi.useFakeTimers() (which would also replace the rAF mock from beforeEach).
const pendingTimeouts: (() => void)[] = []
@ -2457,7 +2457,8 @@ describe('connectPanePty', () => {
)
transportFactoryQueue.push(transport)
// SSH connection: connectionId is set, relay ignores the command field
// SSH connection: connectionId is set, relay receives command metadata
// for spawn context but the renderer owns fast command delivery.
mockStoreState = {
...mockStoreState,
tabsByWorktree: { 'wt-1': [{ id: 'tab-1', ptyId: null }] },
@ -2485,6 +2486,227 @@ describe('connectPanePty', () => {
}
})
it('waits for the SSH shell-ready marker before sending hinted startup commands', async () => {
const pendingTimeouts: (() => void)[] = []
const originalSetTimeout = globalThis.setTimeout
globalThis.setTimeout = vi.fn((fn: () => void) => {
pendingTimeouts.push(fn)
return 999 as unknown as ReturnType<typeof setTimeout>
}) as unknown as typeof setTimeout
try {
const { connectPanePty } = await import('./pty-connection')
const capturedDataCallback: { current: ((data: string) => void) | null } = {
current: null
}
const transport = createMockTransport('pty-id')
transport.connect.mockImplementation(
async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
capturedDataCallback.current = callbacks.onData ?? null
return 'pty-ssh-1'
}
)
transportFactoryQueue.push(transport)
mockStoreState = {
...mockStoreState,
tabsByWorktree: { 'wt-1': [{ id: 'tab-1', ptyId: null }] },
repos: [{ id: 'repo1', connectionId: 'ssh-conn-1' }]
}
const pane = createPane(1)
const manager = createManager(1)
const deps = createDeps({
startup: {
command: "codex 'linked issue context'",
startupCommandDelivery: 'shell-ready'
}
})
connectPanePty(pane as never, manager as never, deps as never)
expect(capturedDataCallback.current).not.toBeNull()
capturedDataCallback.current?.('user@remote $ ')
for (const fn of pendingTimeouts.splice(0)) {
fn()
}
expect(transport.sendInput).not.toHaveBeenCalled()
capturedDataCallback.current?.('\x1b]777;orca-shell-ready\x07user@remote $ ')
for (const fn of pendingTimeouts.splice(0)) {
fn()
}
expect(transport.sendInput).toHaveBeenCalledWith("codex 'linked issue context'\r")
} finally {
globalThis.setTimeout = originalSetTimeout
}
})
it('falls back for SSH shell-ready startup commands when no marker arrives', async () => {
const pendingTimeouts: (() => void)[] = []
const originalSetTimeout = globalThis.setTimeout
globalThis.setTimeout = vi.fn((fn: () => void) => {
pendingTimeouts.push(fn)
return 999 as unknown as ReturnType<typeof setTimeout>
}) as unknown as typeof setTimeout
try {
const { connectPanePty } = await import('./pty-connection')
const capturedDataCallback: { current: ((data: string) => void) | null } = {
current: null
}
const transport = createMockTransport('pty-id')
transport.connect.mockImplementation(
async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
capturedDataCallback.current = callbacks.onData ?? null
return 'pty-ssh-1'
}
)
transportFactoryQueue.push(transport)
mockStoreState = {
...mockStoreState,
tabsByWorktree: { 'wt-1': [{ id: 'tab-1', ptyId: null }] },
repos: [{ id: 'repo1', connectionId: 'ssh-conn-1' }]
}
const pane = createPane(1)
const manager = createManager(1)
const deps = createDeps({
startup: {
command: "codex 'linked issue context'",
startupCommandDelivery: 'shell-ready'
}
})
connectPanePty(pane as never, manager as never, deps as never)
capturedDataCallback.current?.('fish prompt> ')
expect(transport.sendInput).not.toHaveBeenCalled()
for (const fn of pendingTimeouts.splice(0)) {
fn()
}
for (const fn of pendingTimeouts.splice(0)) {
fn()
}
expect(transport.sendInput).toHaveBeenCalledWith("codex 'linked issue context'\r")
} finally {
globalThis.setTimeout = originalSetTimeout
}
})
it('falls back for quiet SSH shell-ready startup commands with no output', async () => {
const pendingTimeouts: (() => void)[] = []
const originalSetTimeout = globalThis.setTimeout
globalThis.setTimeout = vi.fn((fn: () => void) => {
pendingTimeouts.push(fn)
return 999 as unknown as ReturnType<typeof setTimeout>
}) as unknown as typeof setTimeout
try {
const { connectPanePty } = await import('./pty-connection')
const capturedDataCallback: { current: ((data: string) => void) | null } = {
current: null
}
const transport = createMockTransport('pty-id')
transport.connect.mockImplementation(
async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
capturedDataCallback.current = callbacks.onData ?? null
return 'pty-ssh-1'
}
)
transportFactoryQueue.push(transport)
mockStoreState = {
...mockStoreState,
tabsByWorktree: { 'wt-1': [{ id: 'tab-1', ptyId: null }] },
repos: [{ id: 'repo1', connectionId: 'ssh-conn-1' }]
}
const pane = createPane(1)
const manager = createManager(1)
const deps = createDeps({
startup: {
command: "codex 'linked issue context'",
startupCommandDelivery: 'shell-ready'
}
})
connectPanePty(pane as never, manager as never, deps as never)
await flushAsyncTicks()
expect(capturedDataCallback.current).not.toBeNull()
expect(transport.sendInput).not.toHaveBeenCalled()
for (const fn of pendingTimeouts.splice(0)) {
fn()
}
for (const fn of pendingTimeouts.splice(0)) {
fn()
}
expect(transport.sendInput).toHaveBeenCalledWith("codex 'linked issue context'\r")
} finally {
globalThis.setTimeout = originalSetTimeout
}
})
it('waits for shell-ready for SSH Codex native prefill commands without an explicit hint', async () => {
const pendingTimeouts: (() => void)[] = []
const originalSetTimeout = globalThis.setTimeout
globalThis.setTimeout = vi.fn((fn: () => void) => {
pendingTimeouts.push(fn)
return 999 as unknown as ReturnType<typeof setTimeout>
}) as unknown as typeof setTimeout
try {
const { connectPanePty } = await import('./pty-connection')
const capturedDataCallback: { current: ((data: string) => void) | null } = {
current: null
}
const transport = createMockTransport('pty-id')
transport.connect.mockImplementation(
async ({ callbacks }: { callbacks: ConnectCallbacks }) => {
capturedDataCallback.current = callbacks.onData ?? null
return 'pty-ssh-1'
}
)
transportFactoryQueue.push(transport)
mockStoreState = {
...mockStoreState,
tabsByWorktree: { 'wt-1': [{ id: 'tab-1', ptyId: null }] },
repos: [{ id: 'repo1', connectionId: 'ssh-conn-1' }]
}
const pane = createPane(1)
const manager = createManager(1)
const deps = createDeps({
startup: { command: "codex --prefill 'linked issue context'" }
})
connectPanePty(pane as never, manager as never, deps as never)
capturedDataCallback.current?.('user@remote $ ')
for (const fn of pendingTimeouts.splice(0)) {
fn()
}
expect(transport.sendInput).not.toHaveBeenCalled()
capturedDataCallback.current?.('\x1b]777;orca-shell-ready\x07user@remote $ ')
for (const fn of pendingTimeouts.splice(0)) {
fn()
}
expect(transport.sendInput).toHaveBeenCalledWith("codex --prefill 'linked issue context'\r")
} finally {
globalThis.setTimeout = originalSetTimeout
}
})
it('drops agent status without retaining when OSC 133 reports the command finished', async () => {
const { connectPanePty } = await import('./pty-connection')

View File

@ -37,6 +37,8 @@ import {
POST_REPLAY_REATTACH_RESET,
RESET_TERMINAL_CURSOR_STYLE
} from './layout-serialization'
import { createShellReadyMarkerScanState, scanForShellReadyMarker } from './shell-ready-marker-scan'
import { shouldUseShellReadyStartupDelivery } from '../../../../shared/codex-startup-delivery'
import { getSystemPrefersDark } from '@/lib/terminal-theme'
import {
mode2031SequenceFor,
@ -114,6 +116,7 @@ const AGENT_TASK_COMPLETE_NOTIFICATION_GRACE_MS = 250
const AGENT_TASK_COMPLETE_NOTIFICATION_MAX_WAIT_MS = 1500
const AGENT_TASK_COMPLETE_NOTIFICATION_DETAIL_MAX_AGE_MS = 10_000
const COMMAND_CODE_OUTPUT_DONE_SETTLE_MS = 1500
const SSH_SHELL_READY_STARTUP_FALLBACK_MS = 1500
const HIDDEN_OUTPUT_RESTORE_SCROLLBACK_ROWS = 5000
const HIDDEN_OUTPUT_RESTORE_PENDING_CHARS = 512 * 1024
const HIDDEN_OUTPUT_RESTORE_DEFERRED_RETRY_MS = 50
@ -793,6 +796,7 @@ export function connectPanePty(
let cleanupHiddenOutputRestoreDeferredRetry = (): void => {}
let unregisterE2ePtyDataInjection = (): void => {}
let startupInjectTimer: ReturnType<typeof setTimeout> | null = null
let sshShellReadyFallbackTimer: ReturnType<typeof setTimeout> | null = null
let agentTaskCompleteNotificationGraceTimer: ReturnType<typeof setTimeout> | null = null
let agentTaskCompleteNotificationMaxTimer: ReturnType<typeof setTimeout> | null = null
let agentTaskCompleteStatusUnsubscribe: (() => void) | null = null
@ -1666,6 +1670,9 @@ export function connectPanePty(
cwd: deps.cwd,
env: paneEnv,
command: shouldDeliverStartupViaTerminalPaste ? undefined : paneStartup?.command,
startupCommandDelivery: shouldDeliverStartupViaTerminalPaste
? undefined
: paneStartup?.startupCommandDelivery,
connectionId,
worktreeId: deps.worktreeId,
// Why: closes the SIGKILL race documented in INVESTIGATION.md by letting
@ -1989,10 +1996,32 @@ export function connectPanePty(
}
// Why: for ordinary local startup commands, the local PTY provider already
// writes via the shell-ready barrier. terminal-paste startup commands must
// stay renderer-delivered so xterm can apply bracketed-paste semantics.
// writes via the shell-ready barrier. terminal-paste and SSH startup
// commands stay renderer-delivered so xterm/relay can apply their handling.
let pendingStartupCommand =
shouldDeliverStartupViaTerminalPaste || connectionId ? (paneStartup?.command ?? null) : null
const shouldWaitForSshShellReady =
Boolean(connectionId) &&
shouldUseShellReadyStartupDelivery({
command: paneStartup?.command,
startupCommandDelivery: paneStartup?.startupCommandDelivery
}) &&
!shouldDeliverStartupViaTerminalPaste
const sshShellReadyMarkerScan = shouldWaitForSshShellReady
? createShellReadyMarkerScanState()
: null
let sshStartupShellReady = !shouldWaitForSshShellReady
const markSshStartupShellReady = (): void => {
if (sshStartupShellReady) {
return
}
sshStartupShellReady = true
if (sshShellReadyFallbackTimer !== null) {
clearTimeout(sshShellReadyFallbackTimer)
sshShellReadyFallbackTimer = null
}
schedulePendingStartupCommandDelivery()
}
let sessionRestoredBannerShown = false
const showSessionRestoredBanner = (): void => {
if (sessionRestoredBannerShown) {
@ -2057,6 +2086,18 @@ export function connectPanePty(
if (!pendingStartupCommand) {
return
}
if (!sshStartupShellReady) {
if (sshShellReadyFallbackTimer === null) {
// Why: some SSH shells cannot emit Orca's ready marker. Prefer the
// marker when available, but fall back to the old renderer delivery
// behavior instead of dropping the startup command forever.
sshShellReadyFallbackTimer = setTimeout(() => {
sshShellReadyFallbackTimer = null
markSshStartupShellReady()
}, SSH_SHELL_READY_STARTUP_FALLBACK_MS)
}
return
}
if (startupInjectTimer !== null) {
clearTimeout(startupInjectTimer)
}
@ -2124,6 +2165,9 @@ export function connectPanePty(
} else if (typeof gen === 'number') {
void window.api.pty.clearPendingPaneSerializer(cacheKey, gen).catch(() => {})
}
if (resolvedPtyId && connectionId) {
schedulePendingStartupCommandDelivery()
}
return resolvedPtyId
})
.catch(async () => {
@ -2983,6 +3027,13 @@ export function connectPanePty(
hasReceivedPtyOutput = true
recordAgentHibernationPaneOutput(cacheKey)
}
if (sshShellReadyMarkerScan) {
const scanned = scanForShellReadyMarker(sshShellReadyMarkerScan, data)
if (scanned.matched) {
markSshStartupShellReady()
}
data = scanned.output
}
resetHiddenOutputRestoreIfPtyChanged()
respondToTerminalPixelSizeQueries(data)
observeTerminalBracketedPasteModeOutput(pane.terminal, data)
@ -3706,6 +3757,10 @@ export function connectPanePty(
clearTimeout(startupInjectTimer)
startupInjectTimer = null
}
if (sshShellReadyFallbackTimer !== null) {
clearTimeout(sshShellReadyFallbackTimer)
sshShellReadyFallbackTimer = null
}
clearPendingAgentTaskCompleteNotification()
pendingTerminalBellNotification = false
clearTerminalBellNotificationTimer()

View File

@ -8,6 +8,7 @@
import type { ParsedAgentStatusPayload } from '../../../../shared/agent-status-types'
import type { EventProps } from '../../../../shared/telemetry-events'
import type { ProjectExecutionRuntimeResolution } from '../../../../shared/project-execution-runtime'
import type { StartupCommandDelivery } from '../../../../shared/codex-startup-delivery'
import { ackPtyData, exposeE2eTerminalPtyAckGate } from './terminal-pty-ack-gate'
// ── Singleton PTY event dispatcher ───────────────────────────────────
@ -353,6 +354,7 @@ export type IpcPtyTransportOptions = {
cwd?: string
env?: Record<string, string>
command?: string
startupCommandDelivery?: StartupCommandDelivery
connectionId?: string | null
/** Orca worktree identity for scoped shell history. */
worktreeId?: string

View File

@ -421,6 +421,7 @@ export function createIpcPtyTransport(opts: IpcPtyTransportOptions = {}): PtyTra
cwd,
env,
command,
startupCommandDelivery,
connectionId,
worktreeId,
tabId,
@ -543,6 +544,7 @@ export function createIpcPtyTransport(opts: IpcPtyTransportOptions = {}): PtyTra
cwd,
env,
command,
...(startupCommandDelivery ? { startupCommandDelivery } : {}),
...(connectionId ? { connectionId } : {}),
...(options.sessionId ? { sessionId: options.sessionId } : {}),
worktreeId,

View File

@ -342,6 +342,30 @@ describe('createRemoteRuntimePtyTransport', () => {
)
})
it('passes startup command delivery when creating the remote runtime terminal', async () => {
const { createRemoteRuntimePtyTransport } = await import('./remote-runtime-pty-transport')
const transport = createRemoteRuntimePtyTransport('env-1', {
worktreeId: 'wt-1',
tabId: 'tab-1',
leafId: 'pane:1',
command: "codex 'linked issue context'",
startupCommandDelivery: 'shell-ready'
})
await transport.connect({ url: '', callbacks: {} })
expect(runtimeCall).toHaveBeenCalledWith(
expect.objectContaining({
selector: 'env-1',
method: 'terminal.create',
params: expect.objectContaining({
command: "codex 'linked issue context'",
startupCommandDelivery: 'shell-ready'
})
})
)
})
it('activates pending host session mirrors instead of creating duplicate terminals', async () => {
runtimeCall.mockImplementation((args) => {
if (args.method === 'session.tabs.activate') {

View File

@ -47,6 +47,7 @@ export function createRemoteRuntimePtyTransport(
): PtyTransport {
const {
command,
startupCommandDelivery,
env,
worktreeId,
tabId,
@ -375,6 +376,7 @@ export function createRemoteRuntimePtyTransport(
const created = await callRuntime<{ terminal: RuntimeTerminalCreate }>('terminal.create', {
worktree: toRuntimeWorktreeSelector(worktreeId),
command,
startupCommandDelivery,
env,
tabId,
leafId,

View File

@ -0,0 +1,39 @@
import { describe, expect, it } from 'vitest'
import { createShellReadyMarkerScanState, scanForShellReadyMarker } from './shell-ready-marker-scan'
describe('scanForShellReadyMarker', () => {
it('strips the marker and preserves surrounding output', () => {
const state = createShellReadyMarkerScanState()
expect(scanForShellReadyMarker(state, 'before \x1b]777;orca-shell-ready\x07 after')).toEqual({
output: 'before after',
matched: true
})
})
it('matches markers split across chunks', () => {
const state = createShellReadyMarkerScanState()
expect(scanForShellReadyMarker(state, 'before \x1b]777;orca')).toEqual({
output: 'before ',
matched: false
})
expect(scanForShellReadyMarker(state, '-shell-ready\x07 after')).toEqual({
output: ' after',
matched: true
})
})
it('flushes marker-like output when the full marker is not BEL-terminated', () => {
const state = createShellReadyMarkerScanState()
expect(scanForShellReadyMarker(state, 'before \x1b]777;orca-shell-readyx')).toEqual({
output: 'before \x1b]777;orca-shell-readyx',
matched: false
})
expect(scanForShellReadyMarker(state, ' after')).toEqual({
output: ' after',
matched: false
})
})
})

View File

@ -0,0 +1,54 @@
const SHELL_READY_MARKER = '\x1b]777;orca-shell-ready'
export type ShellReadyMarkerScanState = {
matchPos: number
heldBytes: string
}
export function createShellReadyMarkerScanState(): ShellReadyMarkerScanState {
return { matchPos: 0, heldBytes: '' }
}
export function scanForShellReadyMarker(
state: ShellReadyMarkerScanState,
data: string
): { output: string; matched: boolean } {
let output = ''
for (let i = 0; i < data.length; i += 1) {
const ch = data[i] as string
if (state.matchPos < SHELL_READY_MARKER.length) {
if (ch === SHELL_READY_MARKER[state.matchPos]) {
state.heldBytes += ch
state.matchPos += 1
} else {
output += state.heldBytes
state.heldBytes = ''
state.matchPos = 0
if (ch === SHELL_READY_MARKER[0]) {
state.heldBytes = ch
state.matchPos = 1
} else {
output += ch
}
}
} else if (ch === '\x07') {
const remaining = data.slice(i + 1)
state.heldBytes = ''
state.matchPos = 0
return { output: output + remaining, matched: true }
} else {
output += state.heldBytes
state.heldBytes = ''
state.matchPos = 0
if (ch === SHELL_READY_MARKER[0]) {
state.heldBytes = ch
state.matchPos = 1
} else {
output += ch
}
}
}
return { output, matched: false }
}

View File

@ -28,6 +28,7 @@ import type {
} from '../../../../shared/types'
import type { TerminalPaneSplitSource } from '../../../../shared/feature-education-telemetry'
import type { EventProps } from '../../../../shared/telemetry-events'
import type { StartupCommandDelivery } from '../../../../shared/codex-startup-delivery'
import { resolveTerminalFontWeights } from '../../../../shared/terminal-fonts'
import {
buildFontFamily,
@ -120,6 +121,7 @@ type UseTerminalPaneLifecycleDeps = {
/** Renderer-delivered startup input for callers that need xterm paste
* semantics before the submit Enter. */
delivery?: 'terminal-paste'
startupCommandDelivery?: StartupCommandDelivery
env?: Record<string, string>
/** Telemetry payload for `agent_started`. Forwarded to `pty:spawn`
* so main fires the event only after the spawn succeeds. */

View File

@ -2892,6 +2892,9 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
? {
command: startupPlan.launchCommand,
...(startupPlan.env ? { env: startupPlan.env } : {}),
...(startupPlan.startupCommandDelivery
? { startupCommandDelivery: startupPlan.startupCommandDelivery }
: {}),
telemetry: composerTelemetry
}
: undefined
@ -2954,6 +2957,9 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
startup: {
command: startupPlan.launchCommand,
...(startupPlan.env ? { env: startupPlan.env } : {}),
...(startupPlan.startupCommandDelivery
? { startupCommandDelivery: startupPlan.startupCommandDelivery }
: {}),
...(tuiAgent === 'command-code' && submitStartupPrompt.trim().length > 0
? {
initialAgentStatus: {
@ -3195,6 +3201,9 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
launchCommand: draftLaunchPlan.launchCommand,
expectedProcess: draftLaunchPlan.expectedProcess,
followupPrompt: null,
...(draftLaunchPlan.startupCommandDelivery
? { startupCommandDelivery: draftLaunchPlan.startupCommandDelivery }
: {}),
...(draftLaunchPlan.env ? { env: draftLaunchPlan.env } : {})
}
} else if (agent !== null) {
@ -3226,6 +3235,9 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
? {
command: startupPlan.launchCommand,
...(startupPlan.env ? { env: startupPlan.env } : {}),
...(startupPlan.startupCommandDelivery
? { startupCommandDelivery: startupPlan.startupCommandDelivery }
: {}),
...(quickTelemetry ? { telemetry: quickTelemetry } : {})
}
: undefined

View File

@ -1491,7 +1491,12 @@ export function useIpcEvents(): void {
store.setTabCustomTitle(tab.id, data.title, { recordInteraction: false })
}
if (data.command) {
store.queueTabStartupCommand(tab.id, { command: data.command })
store.queueTabStartupCommand(tab.id, {
command: data.command,
...(data.startupCommandDelivery
? { startupCommandDelivery: data.startupCommandDelivery }
: {})
})
}
window.api.ui.replyTerminalCreate({
requestId: data.requestId,

View File

@ -355,7 +355,7 @@ describe('launchAgentBackgroundSession', () => {
)
})
it('injects startup commands into SSH background sessions after shell output arrives', async () => {
it('injects fast startup commands into SSH background sessions after shell output arrives', async () => {
vi.useFakeTimers()
try {
state.repos = [{ id: 'repo-1', connectionId: 'ssh-1', path: '/repo' }]
@ -368,7 +368,10 @@ describe('launchAgentBackgroundSession', () => {
title: 'Nightly audit'
})
expect(mockSpawn.mock.calls[0]?.[0]?.command).toBeUndefined()
expect(mockSpawn.mock.calls[0]?.[0]?.command).toBe(
"claude '--dangerously-skip-permissions' 'run the automation'"
)
expect(mockSpawn.mock.calls[0]?.[0]?.startupCommandDelivery).toBeUndefined()
const dataSidecar = mockSubscribeToPtyData.mock.calls[0]?.[1] as (data: string) => void
dataSidecar('user@remote repo % ')
vi.advanceTimersByTime(50)
@ -382,6 +385,108 @@ describe('launchAgentBackgroundSession', () => {
}
})
it('waits for shell-ready before injecting payload-bearing SSH background commands', async () => {
vi.useFakeTimers()
try {
state.repos = [{ id: 'repo-1', connectionId: 'ssh-1', path: '/repo' }]
const { launchAgentBackgroundSession } = await import('./launch-agent-background-session')
await launchAgentBackgroundSession({
agent: 'codex',
worktreeId: 'wt-1',
prompt: 'run the automation',
title: 'Nightly audit'
})
expect(mockSpawn.mock.calls[0]?.[0]).toEqual(
expect.objectContaining({
command: "codex '--dangerously-bypass-approvals-and-sandbox' 'run the automation'",
startupCommandDelivery: 'shell-ready'
})
)
const dataSidecar = mockSubscribeToPtyData.mock.calls[0]?.[1] as (data: string) => void
dataSidecar('user@remote repo % ')
vi.advanceTimersByTime(50)
expect(mockWrite).not.toHaveBeenCalled()
dataSidecar('\x1b]777;orca-shell-ready\x07user@remote repo % ')
vi.advanceTimersByTime(50)
expect(mockWrite).toHaveBeenCalledWith(
'pty-1',
"codex '--dangerously-bypass-approvals-and-sandbox' 'run the automation'\r"
)
} finally {
vi.useRealTimers()
}
})
it('waits for shell-ready for SSH background Codex native prefill commands without a hint', async () => {
vi.useFakeTimers()
try {
state.repos = [{ id: 'repo-1', connectionId: 'ssh-1', path: '/repo' }]
state.settings = {
agentCmdOverrides: { codex: "codex --prefill 'draft from override'" },
activeRuntimeEnvironmentId: null
}
const { launchAgentBackgroundSession } = await import('./launch-agent-background-session')
await launchAgentBackgroundSession({
agent: 'codex',
worktreeId: 'wt-1',
title: 'Nightly audit'
})
expect(mockSpawn.mock.calls[0]?.[0]).toEqual(
expect.objectContaining({
command:
"codex --prefill 'draft from override' '--dangerously-bypass-approvals-and-sandbox'"
})
)
expect(mockSpawn.mock.calls[0]?.[0]).not.toHaveProperty('startupCommandDelivery')
const dataSidecar = mockSubscribeToPtyData.mock.calls[0]?.[1] as (data: string) => void
dataSidecar('user@remote repo % ')
vi.advanceTimersByTime(50)
expect(mockWrite).not.toHaveBeenCalled()
dataSidecar('\x1b]777;orca-shell-ready\x07user@remote repo % ')
vi.advanceTimersByTime(50)
expect(mockWrite).toHaveBeenCalledWith(
'pty-1',
"codex --prefill 'draft from override' '--dangerously-bypass-approvals-and-sandbox'\r"
)
} finally {
vi.useRealTimers()
}
})
it('does not rearm SSH background startup delivery after exit cleanup', async () => {
vi.useFakeTimers()
try {
state.repos = [{ id: 'repo-1', connectionId: 'ssh-1', path: '/repo' }]
const { launchAgentBackgroundSession } = await import('./launch-agent-background-session')
await launchAgentBackgroundSession({
agent: 'codex',
worktreeId: 'wt-1',
prompt: 'run the automation',
title: 'Nightly audit'
})
const dataSidecar = mockSubscribeToPtyData.mock.calls[0]?.[1] as (data: string) => void
const exitSidecar = mockSubscribeToPtyExit.mock.calls[0]?.[1] as (code: number) => void
exitSidecar(0)
dataSidecar('\x1b]777;orca-shell-ready\x07user@remote repo % ')
vi.advanceTimersByTime(50)
expect(mockWrite).not.toHaveBeenCalled()
} finally {
vi.useRealTimers()
}
})
it('creates background sessions on the active runtime environment', async () => {
state.settings = { agentCmdOverrides: {}, activeRuntimeEnvironmentId: 'env-1' }
const { launchAgentBackgroundSession } = await import('./launch-agent-background-session')

View File

@ -33,6 +33,8 @@ import { createAgentStatusOscProcessor } from '../../../shared/agent-status-osc'
import type { ParsedAgentStatusPayload } from '../../../shared/agent-status-types'
import type { RuntimeTerminalCreate } from '../../../shared/runtime-types'
import { translate } from '@/i18n/i18n'
import { createSshBackgroundStartupDelivery } from '@/lib/ssh-background-startup-delivery'
import { shouldUseShellReadyStartupDelivery } from '../../../shared/codex-startup-delivery'
export type LaunchAgentBackgroundSessionArgs = {
agent: TuiAgent
@ -138,39 +140,22 @@ export async function launchAgentBackgroundSession(
ORCA_WORKTREE_ID: worktreeId
}
const sshConnectionId = repo?.connectionId ?? null
let pendingSshStartupCommand = sshConnectionId ? startupPlan.launchCommand : null
let sshStartupInjectTimer: ReturnType<typeof setTimeout> | null = null
const clearSshStartupInjectTimer = (): void => {
if (sshStartupInjectTimer !== null) {
clearTimeout(sshStartupInjectTimer)
sshStartupInjectTimer = null
}
}
const scheduleSshStartupInjection = (ptyId: string): void => {
if (!pendingSshStartupCommand) {
return
}
clearSshStartupInjectTimer()
sshStartupInjectTimer = setTimeout(() => {
sshStartupInjectTimer = null
const command = pendingSshStartupCommand
if (!command) {
return
}
pendingSshStartupCommand = null
// Why: the SSH relay ignores spawn.command for interactive PTYs; hidden
// automation tabs must type the startup command themselves after shell output.
const submittedCommand =
command.endsWith('\r') || command.endsWith('\n') ? command : `${command}\r`
window.api.pty.write(ptyId, submittedCommand)
}, 50)
}
const sshStartupDelivery = createSshBackgroundStartupDelivery({
command: sshConnectionId ? startupPlan.launchCommand : null,
waitForShellReady:
Boolean(sshConnectionId) &&
shouldUseShellReadyStartupDelivery({
command: startupPlan.launchCommand,
startupCommandDelivery: startupPlan.startupCommandDelivery
}),
write: (ptyId, data) => window.api.pty.write(ptyId, data)
})
// Route by the worktree's owner host: the agent terminal must spawn on the host
// that owns this worktree, not on the focused runtime.
const runtimeTarget = getActiveRuntimeTarget(
getSettingsForWorktreeRuntimeOwner(store, worktreeId)
)
let ptyId: string
let ptyId = ''
try {
if (runtimeTarget.kind === 'environment') {
// Why: runtime environments execute on the server; using local pty.spawn
@ -181,6 +166,9 @@ export async function launchAgentBackgroundSession(
{
worktree: toRuntimeWorktreeSelector(worktreeId),
command: startupPlan.launchCommand,
...(startupPlan.startupCommandDelivery
? { startupCommandDelivery: startupPlan.startupCommandDelivery }
: {}),
env: paneEnv,
title,
tabId: tab.id,
@ -195,7 +183,10 @@ export async function launchAgentBackgroundSession(
cols: 120,
rows: 40,
cwd: worktree.path,
...(sshConnectionId ? {} : { command: startupPlan.launchCommand }),
command: startupPlan.launchCommand,
...(!startupPlan.startupCommandDelivery
? {}
: { startupCommandDelivery: startupPlan.startupCommandDelivery }),
env: paneEnv,
connectionId: sshConnectionId,
worktreeId,
@ -234,14 +225,15 @@ export async function launchAgentBackgroundSession(
exitHandled = true
unsubscribeExit()
unsubscribeData()
clearSshStartupInjectTimer()
sshStartupDelivery.clear()
useAppStore.getState().clearTabPtyId(tab.id, ptyId)
onExit?.(ptyId, code)
}
const processAgentStatus = createAgentStatusOscProcessor()
const handleData = (data: string): void => {
data = sshStartupDelivery.handleData(data)
onData?.(data)
scheduleSshStartupInjection(ptyId)
sshStartupDelivery.schedule(ptyId)
const processed = processAgentStatus(data)
for (const payload of processed.payloads) {
useAppStore.getState().setAgentStatus(paneKey, payload, undefined)

View File

@ -3,7 +3,6 @@ import { useAppStore } from '@/store'
import {
buildAgentDraftLaunchPlan,
buildAgentStartupPlan,
type AgentDraftLaunchPlan,
type AgentStartupPlan
} from '@/lib/tui-agent-startup'
import { CLIENT_PLATFORM } from '@/lib/new-workspace'
@ -28,8 +27,6 @@ import type { TuiAgent } from '../../../shared/types'
import type { LaunchSource } from '../../../shared/telemetry-events'
import { translate } from '@/i18n/i18n'
const WIN32_INLINE_DRAFT_LIMIT_CHARS = 24_000
export type LaunchAgentInNewTabArgs = {
agent: TuiAgent
worktreeId: string
@ -88,23 +85,6 @@ function seedCommandCodeSubmittedPromptStatus(tabId: string, prompt: string): vo
}
}
function canUseInlineDraftLaunchPlan(
plan: AgentDraftLaunchPlan,
platform: NodeJS.Platform
): boolean {
if (platform !== 'win32') {
return true
}
const envChars = Object.entries(plan.env ?? {}).reduce(
(total, [key, value]) => total + key.length + value.length,
0
)
// Why: Windows CreateProcess/env blocks have tight length ceilings. Large
// generated drafts should use the existing post-ready paste path instead of
// failing the PTY spawn before the agent starts.
return plan.launchCommand.length + envChars <= WIN32_INLINE_DRAFT_LIMIT_CHARS
}
/**
* Create a new terminal tab and queue the agent's launch command, optionally
* with an initial prompt.
@ -195,12 +175,15 @@ export function launchAgentInNewTab(args: LaunchAgentInNewTabArgs): LaunchAgentI
agentArgs: effectiveAgentArgs,
agentEnv
})
if (draftLaunchPlan && canUseInlineDraftLaunchPlan(draftLaunchPlan, resolvedLaunchPlatform)) {
if (draftLaunchPlan) {
startupPlan = {
agent: draftLaunchPlan.agent,
launchCommand: draftLaunchPlan.launchCommand,
expectedProcess: draftLaunchPlan.expectedProcess,
followupPrompt: null,
...(draftLaunchPlan.startupCommandDelivery
? { startupCommandDelivery: draftLaunchPlan.startupCommandDelivery }
: {}),
...(draftLaunchPlan.env ? { env: draftLaunchPlan.env } : {})
}
} else {
@ -253,7 +236,14 @@ export function launchAgentInNewTab(args: LaunchAgentInNewTabArgs): LaunchAgentI
environmentId: runtimeEnvironmentId,
targetGroupId: groupId,
activate: true,
...(hasPrompt ? { command: startupPlan.launchCommand } : { agent })
...(hasPrompt
? {
command: startupPlan.launchCommand,
...(startupPlan.startupCommandDelivery
? { startupCommandDelivery: startupPlan.startupCommandDelivery }
: {})
}
: { agent })
}).then((created) => {
// Why: created means the host accepted the launch, not that a local tab
// exists; keep pruning stale local rows until the snapshot mirrors.
@ -296,6 +286,9 @@ export function launchAgentInNewTab(args: LaunchAgentInNewTabArgs): LaunchAgentI
store.queueTabStartupCommand(tab.id, {
command: startupPlan.launchCommand,
...(startupPlan.env ? { env: startupPlan.env } : {}),
...(startupPlan.startupCommandDelivery
? { startupCommandDelivery: startupPlan.startupCommandDelivery }
: {}),
...(agent === 'command-code' && hasPrompt && promptDelivery === 'auto-submit'
? { initialAgentStatus: { agent, prompt: trimmedPrompt } }
: {}),

View File

@ -0,0 +1,36 @@
import { describe, expect, it, vi } from 'vitest'
vi.mock('sonner', () => ({ toast: { message: vi.fn() } }))
vi.mock('@/lib/agent-paste-draft', () => ({ pasteDraftWhenAgentReady: vi.fn() }))
vi.mock('@/lib/telemetry', () => ({
track: vi.fn(),
tuiAgentToAgentKind: (agent: string) => agent
}))
vi.mock('@/i18n/i18n', () => ({ translate: (_key: string, value: string) => value }))
import { buildDirectWorkItemStartupOpts } from './launch-work-item-direct-agent'
import type { AgentStartupPlan } from './tui-agent-startup'
describe('buildDirectWorkItemStartupOpts', () => {
it('preserves Codex startup command delivery for linked work-item launches', () => {
const plan: AgentStartupPlan = {
agent: 'codex',
launchCommand: "codex 'review linked issue'",
expectedProcess: 'codex',
followupPrompt: null,
startupCommandDelivery: 'shell-ready'
}
expect(buildDirectWorkItemStartupOpts('codex', plan, 'task_page')).toEqual({
startup: {
command: "codex 'review linked issue'",
startupCommandDelivery: 'shell-ready',
telemetry: {
agent_kind: 'codex',
launch_source: 'task_page',
request_kind: 'new'
}
}
})
})
})

View File

@ -4,6 +4,7 @@ import { track, tuiAgentToAgentKind } from '@/lib/telemetry'
import type { AgentStartupPlan } from '@/lib/tui-agent-startup'
import type { AgentStartedTelemetry } from '@/lib/worktree-activation'
import type { LaunchSource } from '../../../shared/telemetry-events'
import type { StartupCommandDelivery } from '../../../shared/codex-startup-delivery'
import type { TuiAgent } from '../../../shared/types'
import { translate } from '@/i18n/i18n'
@ -12,7 +13,12 @@ export function buildDirectWorkItemStartupOpts(
plan: AgentStartupPlan | null,
launchSource: LaunchSource
): {
startup?: { command: string; env?: Record<string, string>; telemetry?: AgentStartedTelemetry }
startup?: {
command: string
env?: Record<string, string>
startupCommandDelivery?: StartupCommandDelivery
telemetry?: AgentStartedTelemetry
}
} {
if (!plan) {
return {}
@ -25,6 +31,9 @@ export function buildDirectWorkItemStartupOpts(
startup: {
command: plan.launchCommand,
...(plan.env ? { env: plan.env } : {}),
...(plan.startupCommandDelivery
? { startupCommandDelivery: plan.startupCommandDelivery }
: {}),
...(telemetry ? { telemetry } : {})
}
}

View File

@ -297,6 +297,9 @@ export async function launchWorkItemDirect(args: LaunchWorkItemDirectArgs): Prom
launchCommand: draftLaunchPlan.launchCommand,
expectedProcess: draftLaunchPlan.expectedProcess,
followupPrompt: null,
...(draftLaunchPlan.startupCommandDelivery
? { startupCommandDelivery: draftLaunchPlan.startupCommandDelivery }
: {}),
...(draftLaunchPlan.env ? { env: draftLaunchPlan.env } : {})
}
draftLaunchedNatively = true

View File

@ -6,11 +6,13 @@ import {
resolveTuiAgentLaunchEnv
} from '../../../shared/tui-agent-launch-defaults'
import type { AgentStartedTelemetry } from '@/lib/worktree-activation'
import type { StartupCommandDelivery } from '../../../shared/codex-startup-delivery'
import type { GlobalSettings, OnboardingState } from '../../../shared/types'
export type OnboardingFolderAgentStartup = {
command: string
env?: Record<string, string>
startupCommandDelivery?: StartupCommandDelivery
telemetry: AgentStartedTelemetry
}
@ -50,6 +52,9 @@ export function buildOnboardingFolderAgentStartup(
return {
command: startupPlan.launchCommand,
...(startupPlan.env ? { env: startupPlan.env } : {}),
...(startupPlan.startupCommandDelivery
? { startupCommandDelivery: startupPlan.startupCommandDelivery }
: {}),
telemetry: {
agent_kind: tuiAgentToAgentKind(agent),
launch_source: 'onboarding',

View File

@ -73,6 +73,9 @@ function launchSleepingAgentSession(record: SleepingAgentSessionRecord): boolean
})
state.queueTabStartupCommand(tab.id, {
command: startupPlan.launchCommand,
...(startupPlan.startupCommandDelivery
? { startupCommandDelivery: startupPlan.startupCommandDelivery }
: {}),
showSessionRestoredBanner: true,
telemetry: {
agent_kind: tuiAgentToAgentKind(record.agent),

View File

@ -111,6 +111,9 @@ export function planSourceControlAgentActionLaunch(args: {
launchCommand: draftLaunchPlan.launchCommand,
expectedProcess: draftLaunchPlan.expectedProcess,
followupPrompt: null,
...(draftLaunchPlan.startupCommandDelivery
? { startupCommandDelivery: draftLaunchPlan.startupCommandDelivery }
: {}),
...(draftLaunchPlan.env ? { env: draftLaunchPlan.env } : {})
}
delivery = 'draft-native'

View File

@ -0,0 +1,104 @@
import {
createShellReadyMarkerScanState,
scanForShellReadyMarker
} from '@/components/terminal-pane/shell-ready-marker-scan'
const SSH_SHELL_READY_STARTUP_FALLBACK_MS = 1500
type SshBackgroundStartupDeliveryOptions = {
command: string | null
waitForShellReady: boolean
write: (ptyId: string, data: string) => void
}
export type SshBackgroundStartupDelivery = {
handleData(data: string): string
schedule(ptyId: string): void
clear(): void
}
export function createSshBackgroundStartupDelivery(
options: SshBackgroundStartupDeliveryOptions
): SshBackgroundStartupDelivery {
let pendingCommand = options.command
let lastPtyId: string | null = null
let startupShellReady = !options.waitForShellReady
const markerScan = options.waitForShellReady ? createShellReadyMarkerScanState() : null
let injectTimer: ReturnType<typeof setTimeout> | null = null
let fallbackTimer: ReturnType<typeof setTimeout> | null = null
const clearInjectTimer = (): void => {
if (injectTimer !== null) {
clearTimeout(injectTimer)
injectTimer = null
}
}
const clearFallbackTimer = (): void => {
if (fallbackTimer !== null) {
clearTimeout(fallbackTimer)
fallbackTimer = null
}
}
function markShellReady(): void {
if (startupShellReady) {
return
}
startupShellReady = true
clearFallbackTimer()
if (pendingCommand && lastPtyId) {
schedule(lastPtyId)
}
}
const schedule = (ptyId: string): void => {
lastPtyId = ptyId
if (!pendingCommand) {
return
}
if (!startupShellReady) {
if (fallbackTimer === null) {
// Why: hidden SSH sessions can use shells that cannot emit Orca's
// marker. Prefer readiness, but never drop the startup command forever.
fallbackTimer = setTimeout(() => {
fallbackTimer = null
markShellReady()
}, SSH_SHELL_READY_STARTUP_FALLBACK_MS)
}
return
}
clearInjectTimer()
injectTimer = setTimeout(() => {
injectTimer = null
const command = pendingCommand
if (!command) {
return
}
pendingCommand = null
// Why: the SSH relay treats spawn.command as metadata for interactive
// PTYs; hidden automation tabs still submit the command themselves.
const submittedCommand =
command.endsWith('\r') || command.endsWith('\n') ? command : `${command}\r`
options.write(ptyId, submittedCommand)
}, 50)
}
return {
handleData(data) {
if (!markerScan) {
return data
}
const scanned = scanForShellReadyMarker(markerScan, data)
if (scanned.matched) {
markShellReady()
}
return scanned.output
},
schedule,
clear() {
clearInjectTimer()
clearFallbackTimer()
pendingCommand = null
lastPtyId = null
}
}
}

View File

@ -8,6 +8,7 @@ import type {
WorktreeSetupLaunch
} from '../../../shared/types'
import type { EventProps } from '../../../shared/telemetry-events'
import type { StartupCommandDelivery } from '../../../shared/codex-startup-delivery'
import { shouldAutoCreateInitialTerminal } from '@/components/terminal/initial-terminal'
import { buildSetupRunnerCommand } from './setup-runner'
import { buildAgentStartupPlan } from './tui-agent-startup'
@ -61,6 +62,7 @@ export type AgentStartedTelemetry = EventProps<'agent_started'>
export type WorktreeStartupPayload = {
command: string
env?: Record<string, string>
startupCommandDelivery?: StartupCommandDelivery
initialAgentStatus?: { agent: TuiAgent; prompt: string }
telemetry?: AgentStartedTelemetry
}
@ -240,6 +242,9 @@ function buildCreatedAgentReopenStartup(worktree: Worktree): WorktreeStartupPayl
return {
command: startupPlan.launchCommand,
...(startupPlan.env ? { env: startupPlan.env } : {}),
...(startupPlan.startupCommandDelivery
? { startupCommandDelivery: startupPlan.startupCommandDelivery }
: {}),
telemetry: {
agent_kind: tuiAgentToAgentKind(agent),
launch_source: 'sidebar',

View File

@ -31,6 +31,7 @@ function buildStartupOpt(
return {
command: plan.launchCommand,
...(plan.env ? { env: plan.env } : {}),
...(plan.startupCommandDelivery ? { startupCommandDelivery: plan.startupCommandDelivery } : {}),
// Why: command-code shows its prompt in the tab status before the first
// hook fires, so the prompt is threaded through here.
...(request.agent === 'command-code' && request.quickPrompt.trim().length > 0

View File

@ -420,7 +420,8 @@ describe('createWebRuntimeSessionTerminal', () => {
worktreeId: WORKTREE_ID,
afterTabId: 'web-terminal-host-tab-1%3A%3Aleaf-1',
targetGroupId: 'group-left',
command: 'zsh',
command: "codex 'linked issue context'",
startupCommandDelivery: 'shell-ready',
activate: true
})
).resolves.toBe(true)
@ -432,7 +433,8 @@ describe('createWebRuntimeSessionTerminal', () => {
worktree: `id:${WORKTREE_ID}`,
afterTabId: 'host-tab-1::leaf-1',
targetGroupId: 'group-left',
command: 'zsh',
command: "codex 'linked issue context'",
startupCommandDelivery: 'shell-ready',
activate: true
},
timeoutMs: 15_000

View File

@ -10,6 +10,7 @@ import type {
RuntimeTerminalSplit
} from '../../../shared/runtime-types'
import type { TerminalPaneSplitSource } from '../../../shared/feature-education-telemetry'
import type { StartupCommandDelivery } from '../../../shared/codex-startup-delivery'
import type { TuiAgent } from '../../../shared/types'
import type { AppState } from '../store/types'
import { useAppStore } from '../store'
@ -44,6 +45,7 @@ export async function createWebRuntimeSessionTerminal(args: {
afterTabId?: string
targetGroupId?: string
command?: string
startupCommandDelivery?: StartupCommandDelivery
agent?: TuiAgent
activate?: boolean
selectWorktree?: boolean
@ -68,6 +70,7 @@ export async function createWebRuntimeSessionTerminal(args: {
afterTabId: args.afterTabId ? toHostSessionTabId(args.afterTabId) : undefined,
targetGroupId: args.targetGroupId,
command: args.command,
startupCommandDelivery: args.startupCommandDelivery,
agent: args.agent,
activate: args.activate !== false
},

View File

@ -23,6 +23,7 @@ import { isValidHostTerminalTabId, isValidTerminalTabId } from '../../../../shar
import { getRepoIdFromWorktreeId, splitWorktreeId } from '../../../../shared/worktree-id'
import { isWslUncPath } from '../../../../shared/wsl-paths'
import type { ProjectExecutionRuntimeResolution } from '../../../../shared/project-execution-runtime'
import type { StartupCommandDelivery } from '../../../../shared/codex-startup-delivery'
import { resolveLocalWindowsTerminalShellOverrideForTab } from '../../../../shared/local-windows-terminal-runtime'
import type { AgentStartedTelemetry } from '../../lib/worktree-activation'
import { scheduleRuntimeGraphSync } from '@/runtime/sync-runtime-graph'
@ -308,6 +309,7 @@ export type TerminalSlice = {
/** Renderer-delivered startup input for callers that need xterm paste
* semantics before the submit Enter. */
delivery?: 'terminal-paste'
startupCommandDelivery?: StartupCommandDelivery
env?: Record<string, string>
/** Initial prompt-start status for agents that lack native prompt hooks. */
initialAgentStatus?: { agent: TuiAgent; prompt: string }
@ -449,6 +451,7 @@ export type TerminalSlice = {
startup: {
command: string
delivery?: 'terminal-paste'
startupCommandDelivery?: StartupCommandDelivery
env?: Record<string, string>
initialAgentStatus?: { agent: TuiAgent; prompt: string }
showSessionRestoredBanner?: boolean
@ -458,6 +461,7 @@ export type TerminalSlice = {
consumeTabStartupCommand: (tabId: string) => {
command: string
delivery?: 'terminal-paste'
startupCommandDelivery?: StartupCommandDelivery
env?: Record<string, string>
initialAgentStatus?: { agent: TuiAgent; prompt: string }
showSessionRestoredBanner?: boolean

View File

@ -1691,6 +1691,9 @@ export const createWorktreeSlice: StateCreator<AppState, [], [], WorktreeSlice>
? {
startupCommand: startup.command,
...(startup.env ? { startupEnv: startup.env } : {}),
...(startup.startupCommandDelivery
? { startupCommandDelivery: startup.startupCommandDelivery }
: {}),
activate: true
}
: {})

View File

@ -0,0 +1,28 @@
import { describe, expect, it } from 'vitest'
import { hasCodexNativeDraftFlag } from './codex-startup-delivery'
describe('hasCodexNativeDraftFlag', () => {
it('matches Codex --prefill option tokens', () => {
expect(hasCodexNativeDraftFlag("codex --prefill 'linked issue context'")).toBe(true)
expect(hasCodexNativeDraftFlag("codex --model gpt-5 --prefill 'draft'")).toBe(true)
})
it('matches Codex --prefill=value option tokens', () => {
expect(hasCodexNativeDraftFlag('codex --prefill=review')).toBe(true)
expect(hasCodexNativeDraftFlag("codex --prefill='linked issue context'")).toBe(true)
})
it('does not match quoted prompt text mentioning prefill', () => {
expect(hasCodexNativeDraftFlag("codex 'please compare --prefill behavior'")).toBe(false)
expect(hasCodexNativeDraftFlag("codex '--prefill=not-an-option'")).toBe(false)
})
it('does not match non-Codex commands', () => {
expect(hasCodexNativeDraftFlag("claude --prefill 'review this'")).toBe(false)
})
it('leaves plain Codex and normal Codex arguments on the fast path', () => {
expect(hasCodexNativeDraftFlag('codex')).toBe(false)
expect(hasCodexNativeDraftFlag('codex --model gpt-5')).toBe(false)
})
})

View File

@ -0,0 +1,82 @@
import { recognizeAgentProcessFromCommandLine } from './agent-process-recognition'
export type StartupCommandDelivery = 'fast' | 'shell-ready'
type CommandToken = {
value: string
startsQuoted: boolean
}
function tokenizeCommandWithQuoteMetadata(command: string): CommandToken[] {
const tokens: CommandToken[] = []
let current = ''
let inToken = false
let startsQuoted = false
let quote: '"' | "'" | null = null
let escaped = false
for (let index = 0; index < command.length; index += 1) {
const char = command[index]
if (escaped) {
current += char
escaped = false
continue
}
if (char === '\\' && quote !== "'") {
const next = command[index + 1]
if (next && (/\s/.test(next) || next === '"' || next === "'" || next === '\\')) {
escaped = true
inToken = true
continue
}
}
if ((char === '"' || char === "'") && quote === null) {
if (!inToken) {
startsQuoted = true
}
quote = char
inToken = true
continue
}
if (quote === char) {
quote = null
continue
}
if (/\s/.test(char) && quote === null) {
if (inToken) {
tokens.push({ value: current, startsQuoted })
current = ''
inToken = false
startsQuoted = false
}
continue
}
current += char
inToken = true
}
if (inToken) {
tokens.push({ value: current, startsQuoted })
}
return tokens
}
export function hasCodexNativeDraftFlag(command: string | null | undefined): boolean {
if (recognizeAgentProcessFromCommandLine(command)?.agent !== 'codex' || !command) {
return false
}
const tokens = tokenizeCommandWithQuoteMetadata(command)
return tokens.some(
(token, index) =>
index > 0 &&
!token.startsQuoted &&
(token.value === '--prefill' || token.value.startsWith('--prefill='))
)
}
export function shouldUseShellReadyStartupDelivery(args: {
command: string | null | undefined
startupCommandDelivery?: StartupCommandDelivery
}): boolean {
return args.startupCommandDelivery === 'shell-ready' || hasCodexNativeDraftFlag(args.command)
}

View File

@ -57,6 +57,24 @@ describe('tui agent startup plans', () => {
})
expect(plan?.launchCommand).toBe("codex 'fix it'")
expect(plan?.startupCommandDelivery).toBe('shell-ready')
})
it('keeps plain empty Codex startup on the fast delivery path', () => {
const plan = buildAgentStartupPlan({
agent: 'codex',
prompt: '',
cmdOverrides: {},
platform: 'linux',
allowEmptyPromptLaunch: true
})
expect(plan).toEqual({
agent: 'codex',
launchCommand: 'codex',
expectedProcess: 'codex',
followupPrompt: null
})
})
it('launches Claude without Orca settings injection', () => {
@ -266,6 +284,28 @@ describe('tui agent startup plans', () => {
expect(plan?.launchCommand).toBe('omp; unset ORCA_OMP_PREFILL')
})
it('returns null for oversized Windows flag drafts so callers paste after ready', () => {
expect(
buildAgentDraftLaunchPlan({
agent: 'claude',
draft: 'x'.repeat(25_000),
cmdOverrides: {},
platform: 'win32'
})
).toBeNull()
})
it('returns null for oversized Windows env-var drafts so callers paste after ready', () => {
expect(
buildAgentDraftLaunchPlan({
agent: 'pi',
draft: 'x'.repeat(25_000),
cmdOverrides: {},
platform: 'win32'
})
).toBeNull()
})
it('launches Devin with stdin-after-start prompt delivery', () => {
const plan = buildAgentStartupPlan({
agent: 'devin',

View File

@ -6,8 +6,11 @@ import {
} from './agent-session-resume'
import { tokenizeCustomCommandTemplate } from './commit-message-prompt'
import { TUI_AGENT_CONFIG } from './tui-agent-config'
import type { StartupCommandDelivery } from './codex-startup-delivery'
import type { TuiAgent } from './types'
const WIN32_INLINE_DRAFT_LIMIT_CHARS = 24_000
export type AgentStartupPlan = {
agent: TuiAgent
launchCommand: string
@ -15,6 +18,7 @@ export type AgentStartupPlan = {
followupPrompt: string | null
draftPrompt?: string | null
env?: Record<string, string>
startupCommandDelivery?: StartupCommandDelivery
}
export type AgentStartupShell = 'posix' | 'powershell' | 'cmd'
@ -143,6 +147,7 @@ export function buildAgentStartupPlan(args: {
launchCommand: `${baseCommand.command} ${quotedPrompt}`,
expectedProcess: config.expectedProcess,
followupPrompt: null,
...(agent === 'codex' ? { startupCommandDelivery: 'shell-ready' as const } : {}),
...(args.agentEnv ? { env: { ...args.agentEnv } } : {})
}
}
@ -229,6 +234,23 @@ export type AgentDraftLaunchPlan = {
launchCommand: string
expectedProcess: string
env?: Record<string, string>
startupCommandDelivery?: StartupCommandDelivery
}
function inlineDraftPlanFitsPlatform(
plan: AgentDraftLaunchPlan,
platform: NodeJS.Platform
): boolean {
if (platform !== 'win32') {
return true
}
const envChars = Object.entries(plan.env ?? {}).reduce(
(total, [key, value]) => total + key.length + value.length,
0
)
// Why: Windows CreateProcess/env blocks have tight length ceilings. Large
// generated drafts should use the existing post-ready paste fallback.
return plan.launchCommand.length + envChars <= WIN32_INLINE_DRAFT_LIMIT_CHARS
}
export function buildAgentDraftLaunchPlan(args: {
@ -256,25 +278,30 @@ export function buildAgentDraftLaunchPlan(args: {
if (!baseCommand.ok) {
return null
}
let plan: AgentDraftLaunchPlan | null = null
if (config.draftPromptFlag) {
const quoted = quoteStartupArg(trimmed, shell)
return {
plan = {
agent,
launchCommand: `${baseCommand.command} ${config.draftPromptFlag} ${quoted}`,
expectedProcess: config.expectedProcess,
// Why: native draft flags carry user text on argv and must survive rc-file startup.
...(agent === 'codex' ? { startupCommandDelivery: 'shell-ready' as const } : {}),
...(args.agentEnv ? { env: { ...args.agentEnv } } : {})
}
}
if (config.draftPromptEnvVar) {
} else if (config.draftPromptEnvVar) {
const clearVar = clearEnvCommand(config.draftPromptEnvVar, shell)
return {
plan = {
agent,
launchCommand: `${baseCommand.command}${commandSeparator(shell)}${clearVar}`,
expectedProcess: config.expectedProcess,
env: { ...args.agentEnv, [config.draftPromptEnvVar]: trimmed }
}
}
return null
if (!plan || !inlineDraftPlanFitsPlatform(plan, platform)) {
return null
}
return plan
}
export { isShellProcess }

View File

@ -28,6 +28,7 @@ import type {
RepoSourceControlAiOverrides,
SourceControlAiSettings
} from './source-control-ai-types'
import type { StartupCommandDelivery } from './codex-startup-delivery'
import type { AgentKind, LaunchSource, RequestKind } from './telemetry-events'
import type { SleepingAgentSessionRecord } from './agent-session-resume'
import type { ClaudeAgentTeamsMode } from './claude-agent-teams-tmux-compat'
@ -1865,6 +1866,7 @@ export type WorktreeSetupLaunch = {
export type WorktreeStartupLaunch = {
command: string
env?: Record<string, string>
startupCommandDelivery?: StartupCommandDelivery
telemetry?: { agent_kind: AgentKind; launch_source: LaunchSource; request_kind: RequestKind }
}