mirror of
https://github.com/stablyai/orca.git
synced 2026-10-24 00:04:01 +00:00
fix(launch-prompt): the PowerShell a pane is spawned as is a host fact
c729128c3a typed a PowerShell prompt holding `"` or a trailing `\` on the line
because it treated 5.1 and 7 as indistinguishable, so 5.1 broke it as main
does. But this Orca chooses the executable it spawns: the requested shell, the
PowerShell implementation setting, then whether pwsh.exe is installed.
LaunchHost gains windowsPowerShell, derived by spawnedWindowsPowerShell from
those same inputs (main reads its pwsh probe cache, the renderer main's probe
over the existing IPC). On powershell.exe those prompts are measured damaged
and get a launch file (or main's paste on an AI button); on pwsh.exe they ride
the line, which carried them exactly. The PowerShell stays unknown, and main's
delivery decides, for an SSH or paired host and before the pwsh probe answers.
This commit is contained in:
@@ -120,6 +120,15 @@ export function isPwshAvailableAsync(): Promise<boolean> {
|
||||
return pwshProbeInFlight
|
||||
}
|
||||
|
||||
/** The probe's answer when it has one, without blocking; null starts a probe for a later caller. */
|
||||
export function cachedPwshAvailability(): boolean | null {
|
||||
if (pwshAvailableCache && isCacheFresh(pwshAvailableCache)) {
|
||||
return pwshAvailableCache.available
|
||||
}
|
||||
void isPwshAvailableAsync()
|
||||
return null
|
||||
}
|
||||
|
||||
export function warmPwshAvailabilityCache(): Promise<boolean> {
|
||||
if (pwshAvailableCache?.available) {
|
||||
return Promise.resolve(true)
|
||||
|
||||
@@ -186,7 +186,8 @@ export class OrcaRuntimeWithCreateAgentSession extends OrcaRuntimeWithGetAgentSe
|
||||
prompt: request.prompt ?? '',
|
||||
host: thisOrcaLaunchHost({
|
||||
launchPlatform: startupArgs.platform,
|
||||
isRemote: Boolean(workspace.connectionId)
|
||||
isRemote: Boolean(workspace.connectionId),
|
||||
settings
|
||||
}),
|
||||
// Why: this create returns before the agent is ready, so nothing pastes after it.
|
||||
paste: 'never'
|
||||
|
||||
@@ -61,7 +61,7 @@ export class OrcaRuntimeWithResolveMobileSessionTerminalCommand extends OrcaRunt
|
||||
isRemote
|
||||
}),
|
||||
prompt: opts.agentPrompt ?? '',
|
||||
host: thisOrcaLaunchHost({ launchPlatform, isRemote }),
|
||||
host: thisOrcaLaunchHost({ launchPlatform, isRemote, settings }),
|
||||
// Why: a quick command has no paste after ready.
|
||||
paste: 'never'
|
||||
})
|
||||
|
||||
@@ -271,7 +271,12 @@ export class OrcaRuntimeWithResolveWorktreeRemovalTarget extends OrcaRuntimeWith
|
||||
...planInputs,
|
||||
prompt: opts.startupPrompt ?? '',
|
||||
...(opts.launchFile ? { launchFile: opts.launchFile } : {}),
|
||||
host: thisOrcaLaunchHost({ launchPlatform: platform, isRemote }),
|
||||
host: thisOrcaLaunchHost({
|
||||
launchPlatform: platform,
|
||||
isRemote,
|
||||
settings,
|
||||
windowsShellOverride: opts.shellOverride
|
||||
}),
|
||||
paste: opts.onStartupPromptCarry ? 'when-host-proves-agent' : 'never'
|
||||
})
|
||||
if (!planned) {
|
||||
|
||||
@@ -160,7 +160,8 @@ export function buildWorktreeStartupForAgent(
|
||||
prompt: environment.prompt ?? '',
|
||||
host: thisOrcaLaunchHost({
|
||||
isRemote: repoIsRemote(repo),
|
||||
launchPlatform: environment.getLaunchPlatform()
|
||||
launchPlatform: environment.getLaunchPlatform(),
|
||||
settings
|
||||
}),
|
||||
// The caller, or else the host's follow-up, pastes what the line leaves once the agent runs.
|
||||
paste: 'when-host-proves-agent'
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { cachedPwshAvailability } from '../pwsh'
|
||||
import { thisOrcaLaunchHost } from './this-orca-launch-host'
|
||||
|
||||
vi.mock('../pwsh', () => ({ cachedPwshAvailability: vi.fn(() => null) }))
|
||||
|
||||
const realPlatform = process.platform
|
||||
|
||||
function onPlatform(platform: NodeJS.Platform): void {
|
||||
Object.defineProperty(process, 'platform', { value: platform, configurable: true })
|
||||
}
|
||||
|
||||
// Why: this Orca spawns its own Windows panes, so it knows the PowerShell that gets the line.
|
||||
describe('the PowerShell a launch this Orca runs lands in', () => {
|
||||
afterEach(() => {
|
||||
onPlatform(realPlatform)
|
||||
vi.mocked(cachedPwshAvailability).mockReturnValue(null)
|
||||
})
|
||||
|
||||
it('follows the requested shell, then the settings and the pwsh probe, on this Windows machine', () => {
|
||||
onPlatform('win32')
|
||||
const settings = {
|
||||
terminalWindowsShell: 'powershell.exe',
|
||||
terminalWindowsPowerShellImplementation: 'auto' as const
|
||||
}
|
||||
const host = (windowsShellOverride?: string) =>
|
||||
thisOrcaLaunchHost({
|
||||
launchPlatform: 'win32',
|
||||
isRemote: false,
|
||||
settings,
|
||||
windowsShellOverride
|
||||
})
|
||||
expect(host().windowsPowerShell).toBeNull()
|
||||
vi.mocked(cachedPwshAvailability).mockReturnValue(false)
|
||||
expect(host().windowsPowerShell).toBe('powershell.exe')
|
||||
expect(host('pwsh.exe').windowsPowerShell).toBe('pwsh.exe')
|
||||
expect(
|
||||
thisOrcaLaunchHost({ launchPlatform: 'win32', isRemote: true, settings }).windowsPowerShell
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('knows none for a launch on another platform', () => {
|
||||
onPlatform('darwin')
|
||||
vi.mocked(cachedPwshAvailability).mockReturnValue(true)
|
||||
expect(
|
||||
thisOrcaLaunchHost({
|
||||
launchPlatform: 'win32',
|
||||
isRemote: false,
|
||||
settings: { terminalWindowsShell: 'pwsh.exe' }
|
||||
}).windowsPowerShell
|
||||
).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,9 +1,31 @@
|
||||
import { describeLaunchHost, type LaunchHost } from '../../shared/launch-host'
|
||||
import { cachedPwshAvailability } from '../pwsh'
|
||||
import {
|
||||
describeLaunchHost,
|
||||
spawnedWindowsPowerShell,
|
||||
type LaunchHost,
|
||||
type WindowsShellSettings
|
||||
} from '../../shared/launch-host'
|
||||
|
||||
/** The host facts for a launch this Orca runs itself, locally or over its own SSH connection. */
|
||||
export function thisOrcaLaunchHost(args: {
|
||||
launchPlatform: NodeJS.Platform
|
||||
isRemote: boolean
|
||||
settings: WindowsShellSettings
|
||||
/** The shell this launch asked for (`--shell`), which outranks the setting. */
|
||||
windowsShellOverride?: string | null
|
||||
}): LaunchHost {
|
||||
return describeLaunchHost({ ...args, hostPlatform: process.platform, paired: false })
|
||||
const local = !args.isRemote && args.launchPlatform === 'win32' && process.platform === 'win32'
|
||||
return describeLaunchHost({
|
||||
launchPlatform: args.launchPlatform,
|
||||
isRemote: args.isRemote,
|
||||
hostPlatform: process.platform,
|
||||
paired: false,
|
||||
windowsPowerShell: local
|
||||
? spawnedWindowsPowerShell({
|
||||
settings: args.settings,
|
||||
windowsShellOverride: args.windowsShellOverride,
|
||||
pwshAvailable: cachedPwshAvailability()
|
||||
})
|
||||
: null
|
||||
})
|
||||
}
|
||||
|
||||
@@ -16,7 +16,12 @@ describe('the full composer’s renderer-spawned startup', () => {
|
||||
cmdOverrides: {},
|
||||
platform: 'linux',
|
||||
isRemote: true,
|
||||
host: { paired: false, provesAgentInFront: true, takesLaunchFile: true },
|
||||
host: {
|
||||
paired: false,
|
||||
provesAgentInFront: true,
|
||||
takesLaunchFile: true,
|
||||
windowsPowerShell: null
|
||||
},
|
||||
paste: 'when-host-proves-agent'
|
||||
}),
|
||||
prompt
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { launchHostIsPaired } from './launch-file-host'
|
||||
import { useAppStore } from '@/store'
|
||||
import { clientLaunchHost, launchHostIsPaired } from './launch-file-host'
|
||||
import { localPwshAvailability } from './local-pwsh-availability'
|
||||
|
||||
vi.mock('./local-pwsh-availability', () => ({ localPwshAvailability: vi.fn(() => null) }))
|
||||
import { buildQuickComposerStartup } from '@/hooks/composer-state/quick-startup-plan'
|
||||
|
||||
describe('whether a launch lands on a paired host', () => {
|
||||
@@ -26,7 +30,12 @@ describe('the quick composer on a paired host', () => {
|
||||
platform: 'linux',
|
||||
shell: null,
|
||||
isRemote: false,
|
||||
host: { paired: true, provesAgentInFront: true, takesLaunchFile: false },
|
||||
host: {
|
||||
paired: true,
|
||||
provesAgentInFront: true,
|
||||
takesLaunchFile: false,
|
||||
windowsPowerShell: null
|
||||
},
|
||||
telemetrySource: 'sidebar'
|
||||
})
|
||||
expect(startup.backendStartup).toBeUndefined()
|
||||
@@ -34,3 +43,31 @@ describe('the quick composer on a paired host', () => {
|
||||
expect(startup.startupPlan?.pastePromptAfterReady).toBe('fix the build\nthen run the tests')
|
||||
})
|
||||
})
|
||||
|
||||
// Why: this client spawns its own Windows panes, so it knows the PowerShell that gets the line.
|
||||
describe('the PowerShell a local Windows launch from this client lands in', () => {
|
||||
afterEach(() => {
|
||||
vi.mocked(localPwshAvailability).mockReturnValue(null)
|
||||
})
|
||||
|
||||
const host = (isRemote = false) =>
|
||||
clientLaunchHost({ runtimeEnvironmentId: null, launchPlatform: 'win32', isRemote })
|
||||
|
||||
it('follows this client’s shell settings and the pwsh probe', () => {
|
||||
const settings = useAppStore.getState().settings
|
||||
useAppStore.setState({
|
||||
settings: {
|
||||
...settings!,
|
||||
terminalWindowsShell: 'powershell.exe',
|
||||
terminalWindowsPowerShellImplementation: 'auto'
|
||||
}
|
||||
})
|
||||
expect(host().windowsPowerShell).toBeNull()
|
||||
vi.mocked(localPwshAvailability).mockReturnValue(false)
|
||||
expect(host().windowsPowerShell).toBe('powershell.exe')
|
||||
vi.mocked(localPwshAvailability).mockReturnValue(true)
|
||||
expect(host().windowsPowerShell).toBe('pwsh.exe')
|
||||
expect(host(true).windowsPowerShell).toBeNull()
|
||||
useAppStore.setState({ settings })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { isWebRuntimeSessionActive } from '@/runtime/web-runtime-session-environment'
|
||||
import { CLIENT_PLATFORM } from '@/lib/new-workspace'
|
||||
import { describeLaunchHost, type LaunchHost } from '../../../shared/launch-host'
|
||||
import { useAppStore } from '@/store'
|
||||
import {
|
||||
describeLaunchHost,
|
||||
spawnedWindowsPowerShell,
|
||||
type LaunchHost
|
||||
} from '../../../shared/launch-host'
|
||||
import { isWebClientLocation } from './web-client-location'
|
||||
import { localPwshAvailability } from './local-pwsh-availability'
|
||||
|
||||
/**
|
||||
* Whether a launch lands on a paired Orca: another Orca this client drives, which may be an older
|
||||
@@ -18,10 +24,19 @@ export function clientLaunchHost(args: {
|
||||
launchPlatform: NodeJS.Platform
|
||||
isRemote: boolean
|
||||
}): LaunchHost {
|
||||
const paired = launchHostIsPaired(args.runtimeEnvironmentId)
|
||||
const local = !paired && !args.isRemote && args.launchPlatform === 'win32'
|
||||
return describeLaunchHost({
|
||||
launchPlatform: args.launchPlatform,
|
||||
isRemote: args.isRemote,
|
||||
hostPlatform: CLIENT_PLATFORM,
|
||||
paired: launchHostIsPaired(args.runtimeEnvironmentId)
|
||||
paired,
|
||||
// Why this client's settings: a local pane is spawned from them, as its shell is.
|
||||
windowsPowerShell: local
|
||||
? spawnedWindowsPowerShell({
|
||||
settings: useAppStore.getState().settings,
|
||||
pwshAvailable: localPwshAvailability()
|
||||
})
|
||||
: null
|
||||
})
|
||||
}
|
||||
|
||||
@@ -84,7 +84,12 @@ const settings = {
|
||||
describe('buildDirectWorkItemAgentStartupPlan', () => {
|
||||
it('omits native-chat preferences when the new workspace opens in terminal mode', () => {
|
||||
const result = buildDirectWorkItemAgentStartupPlan({
|
||||
host: { paired: false, provesAgentInFront: true, takesLaunchFile: true },
|
||||
host: {
|
||||
paired: false,
|
||||
provesAgentInFront: true,
|
||||
takesLaunchFile: true,
|
||||
windowsPowerShell: null
|
||||
},
|
||||
agent: 'codex',
|
||||
draftContent: 'Review issue 42',
|
||||
promptDelivery: 'draft',
|
||||
@@ -99,7 +104,12 @@ describe('buildDirectWorkItemAgentStartupPlan', () => {
|
||||
|
||||
it('applies native-chat preferences when the new workspace opens in chat', () => {
|
||||
const result = buildDirectWorkItemAgentStartupPlan({
|
||||
host: { paired: false, provesAgentInFront: true, takesLaunchFile: true },
|
||||
host: {
|
||||
paired: false,
|
||||
provesAgentInFront: true,
|
||||
takesLaunchFile: true,
|
||||
windowsPowerShell: null
|
||||
},
|
||||
agent: 'codex',
|
||||
draftContent: 'Review issue 42',
|
||||
promptDelivery: 'draft',
|
||||
@@ -148,7 +158,12 @@ describe('buildDirectWorkItemAgentStartupPlan global arguments fallback', () =>
|
||||
|
||||
it('resolves the global Agents arguments when the launch names none', () => {
|
||||
const result = buildDirectWorkItemAgentStartupPlan({
|
||||
host: { paired: false, provesAgentInFront: true, takesLaunchFile: true },
|
||||
host: {
|
||||
paired: false,
|
||||
provesAgentInFront: true,
|
||||
takesLaunchFile: true,
|
||||
windowsPowerShell: null
|
||||
},
|
||||
agent: 'codex',
|
||||
draftContent: 'Fix the broken checks',
|
||||
promptDelivery: 'draft',
|
||||
@@ -162,7 +177,12 @@ describe('buildDirectWorkItemAgentStartupPlan global arguments fallback', () =>
|
||||
|
||||
it('lets an explicit per-action value win over the global one', () => {
|
||||
const result = buildDirectWorkItemAgentStartupPlan({
|
||||
host: { paired: false, provesAgentInFront: true, takesLaunchFile: true },
|
||||
host: {
|
||||
paired: false,
|
||||
provesAgentInFront: true,
|
||||
takesLaunchFile: true,
|
||||
windowsPowerShell: null
|
||||
},
|
||||
agent: 'codex',
|
||||
agentArgs: '--model gpt-5',
|
||||
draftContent: 'Fix the broken checks',
|
||||
@@ -180,7 +200,7 @@ describe('buildDirectWorkItemAgentStartupPlan global arguments fallback', () =>
|
||||
describe('buildDirectWorkItemAgentStartupPlan submitted prompts', () => {
|
||||
const submit = (agent: 'claude' | 'gemini', draftContent: string, paired = false) =>
|
||||
buildDirectWorkItemAgentStartupPlan({
|
||||
host: { paired, provesAgentInFront: true, takesLaunchFile: !paired },
|
||||
host: { paired, provesAgentInFront: true, takesLaunchFile: !paired, windowsPowerShell: null },
|
||||
agent,
|
||||
draftContent,
|
||||
promptDelivery: 'submit-after-ready',
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
let known: boolean | null = null
|
||||
let reading = false
|
||||
|
||||
/**
|
||||
* Whether pwsh.exe is installed on this machine, from main's shared probe; null until it answers.
|
||||
* The first call starts the read, so a launch before then is planned without it.
|
||||
*/
|
||||
export function localPwshAvailability(): boolean | null {
|
||||
if (known === null && !reading && typeof window !== 'undefined' && window.api?.pwsh) {
|
||||
reading = true
|
||||
void window.api.pwsh
|
||||
.isAvailable()
|
||||
.then((available) => {
|
||||
known = available
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
reading = false
|
||||
})
|
||||
}
|
||||
return known
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { describeLaunchHost } from './launch-host'
|
||||
import { describeLaunchHost, spawnedWindowsPowerShell } from './launch-host'
|
||||
|
||||
describe('what a launch host can do with its prompt', () => {
|
||||
it.each([
|
||||
@@ -19,8 +19,76 @@ describe('what a launch host can do with its prompt', () => {
|
||||
expect(describeLaunchHost({ isRemote, paired, launchPlatform, hostPlatform })).toEqual({
|
||||
paired,
|
||||
provesAgentInFront: proves,
|
||||
takesLaunchFile
|
||||
takesLaunchFile,
|
||||
windowsPowerShell: null
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
// Why: the PowerShell decides how `"` and a trailing `\` reach the agent, and this Orca picks it.
|
||||
describe('the PowerShell a local Windows pane is spawned as', () => {
|
||||
it.each([
|
||||
['the pwsh shell', { terminalWindowsShell: 'pwsh.exe' }, undefined, null, 'pwsh.exe'],
|
||||
[
|
||||
'a requested pwsh over the setting',
|
||||
{ terminalWindowsShell: 'cmd.exe' },
|
||||
'pwsh',
|
||||
null,
|
||||
'pwsh.exe'
|
||||
],
|
||||
[
|
||||
'Windows PowerShell chosen explicitly',
|
||||
{
|
||||
terminalWindowsShell: 'powershell.exe',
|
||||
terminalWindowsPowerShellImplementation: 'powershell.exe'
|
||||
},
|
||||
undefined,
|
||||
true,
|
||||
'powershell.exe'
|
||||
],
|
||||
[
|
||||
'auto with pwsh installed',
|
||||
{ terminalWindowsShell: '', terminalWindowsPowerShellImplementation: 'auto' },
|
||||
undefined,
|
||||
true,
|
||||
'pwsh.exe'
|
||||
],
|
||||
[
|
||||
'auto without pwsh',
|
||||
{ terminalWindowsShell: 'powershell.exe', terminalWindowsPowerShellImplementation: 'auto' },
|
||||
undefined,
|
||||
false,
|
||||
'powershell.exe'
|
||||
],
|
||||
[
|
||||
'auto before the probe answers',
|
||||
{ terminalWindowsShell: 'powershell.exe', terminalWindowsPowerShellImplementation: 'auto' },
|
||||
undefined,
|
||||
null,
|
||||
null
|
||||
],
|
||||
['cmd', { terminalWindowsShell: 'cmd.exe' }, undefined, true, null],
|
||||
['Git Bash', { terminalWindowsShell: 'git-bash' }, undefined, true, null]
|
||||
] as const)('%s', (_label, settings, windowsShellOverride, pwshAvailable, expected) => {
|
||||
expect(spawnedWindowsPowerShell({ settings, windowsShellOverride, pwshAvailable })).toBe(
|
||||
expected
|
||||
)
|
||||
})
|
||||
|
||||
it('is not this machine’s to know for an SSH or paired host', () => {
|
||||
for (const where of [
|
||||
{ isRemote: true, paired: false },
|
||||
{ isRemote: false, paired: true }
|
||||
]) {
|
||||
expect(
|
||||
describeLaunchHost({
|
||||
...where,
|
||||
launchPlatform: 'win32',
|
||||
hostPlatform: 'win32',
|
||||
windowsPowerShell: 'powershell.exe'
|
||||
}).windowsPowerShell
|
||||
).toBeNull()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
import type { GlobalSettings } from './global-settings-types'
|
||||
|
||||
export type WindowsPowerShell = 'powershell.exe' | 'pwsh.exe'
|
||||
|
||||
export type WindowsShellSettings =
|
||||
| Partial<
|
||||
Pick<GlobalSettings, 'terminalWindowsShell' | 'terminalWindowsPowerShellImplementation'>
|
||||
>
|
||||
| null
|
||||
| undefined
|
||||
|
||||
/** What the host a launch runs on can do with its prompt, derived in one place from where it runs. */
|
||||
export type LaunchHost = {
|
||||
/** Another Orca this client drives, possibly an older one: it is sent a command it may neither
|
||||
@@ -10,6 +21,36 @@ export type LaunchHost = {
|
||||
* SSH Windows host's relay may run its panes in WSL (its OpenSSH default shell, which this client
|
||||
* cannot see), where it writes none. Such a host gets the line or the paste instead. */
|
||||
takesLaunchFile: boolean
|
||||
/** The PowerShell a local Windows pane is spawned as, which decides how it hands `"` and a
|
||||
* trailing `\` to the agent. Null where this Orca does not choose it (an SSH or paired host) or
|
||||
* has not yet learned whether pwsh is installed. */
|
||||
windowsPowerShell: WindowsPowerShell | null
|
||||
}
|
||||
|
||||
/**
|
||||
* The PowerShell this host spawns for a pane, resolved as the spawn resolves it: the requested
|
||||
* shell, then the implementation setting, then whether pwsh.exe is installed. Null for a shell that
|
||||
* is not PowerShell, or when that probe has not answered.
|
||||
*/
|
||||
export function spawnedWindowsPowerShell(args: {
|
||||
settings: WindowsShellSettings
|
||||
/** The shell this launch asked for, which outranks the setting. */
|
||||
windowsShellOverride?: string | null
|
||||
pwshAvailable: boolean | null
|
||||
}): WindowsPowerShell | null {
|
||||
const shell = (args.windowsShellOverride ?? args.settings?.terminalWindowsShell ?? '').trim()
|
||||
const name = shell.replaceAll('\\', '/').split('/').pop()?.toLowerCase() ?? ''
|
||||
if (name === 'pwsh.exe' || name === 'pwsh') {
|
||||
return 'pwsh.exe'
|
||||
}
|
||||
if (name !== '' && name !== 'powershell.exe' && name !== 'powershell') {
|
||||
return null
|
||||
}
|
||||
const implementation = args.settings?.terminalWindowsPowerShellImplementation
|
||||
if (implementation === 'powershell.exe' || implementation === 'pwsh.exe') {
|
||||
return implementation
|
||||
}
|
||||
return args.pwshAvailable === null ? null : args.pwshAvailable ? 'pwsh.exe' : 'powershell.exe'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -23,11 +64,15 @@ export function describeLaunchHost(args: {
|
||||
/** The platform of the machine this Orca runs on. */
|
||||
hostPlatform: NodeJS.Platform
|
||||
paired: boolean
|
||||
/** `spawnedWindowsPowerShell` on this machine, which a launch elsewhere does not use. */
|
||||
windowsPowerShell?: WindowsPowerShell | null
|
||||
}): LaunchHost {
|
||||
const runsElsewhere = args.isRemote || args.paired
|
||||
return {
|
||||
paired: args.paired,
|
||||
provesAgentInFront: (runsElsewhere ? args.launchPlatform : args.hostPlatform) !== 'win32',
|
||||
takesLaunchFile: !args.paired && !(args.isRemote && args.launchPlatform === 'win32')
|
||||
takesLaunchFile: !args.paired && !(args.isRemote && args.launchPlatform === 'win32'),
|
||||
windowsPowerShell:
|
||||
runsElsewhere || args.launchPlatform !== 'win32' ? null : (args.windowsPowerShell ?? null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,14 +25,24 @@ describe('the one launch-prompt decision every launch path builds through', () =
|
||||
...base,
|
||||
prompt: 'fix it',
|
||||
platform: 'linux',
|
||||
host: { paired: true, provesAgentInFront: true, takesLaunchFile: false }
|
||||
host: {
|
||||
paired: true,
|
||||
provesAgentInFront: true,
|
||||
takesLaunchFile: false,
|
||||
windowsPowerShell: null
|
||||
}
|
||||
})
|
||||
expect(short?.carry).toBe('on-line')
|
||||
const long = planLaunchForTest({
|
||||
...base,
|
||||
prompt: 'fix it\nthen run the tests',
|
||||
platform: 'linux',
|
||||
host: { paired: true, provesAgentInFront: true, takesLaunchFile: false }
|
||||
host: {
|
||||
paired: true,
|
||||
provesAgentInFront: true,
|
||||
takesLaunchFile: false,
|
||||
windowsPowerShell: null
|
||||
}
|
||||
})
|
||||
expect(long?.carry).toBe('paste-after-ready')
|
||||
expect(long?.pasteAfterReady).toBe('fix it\nthen run the tests')
|
||||
|
||||
@@ -181,7 +181,12 @@ describe('a host that types the line raw', () => {
|
||||
// A local WSL pane: a Linux line, on a Windows host that cannot read what holds it.
|
||||
const extra = {
|
||||
platform: 'linux' as const,
|
||||
host: { paired: false, provesAgentInFront: false, takesLaunchFile: true }
|
||||
host: {
|
||||
paired: false,
|
||||
provesAgentInFront: false,
|
||||
takesLaunchFile: true,
|
||||
windowsPowerShell: null
|
||||
}
|
||||
}
|
||||
const prompt = 'fix the build\nthen run the tests'
|
||||
expect(plan('gemini', prompt, extra)?.carry).toBe('on-line')
|
||||
@@ -216,7 +221,12 @@ describe('a host that types the line raw', () => {
|
||||
it('pastes on a paired host what its line cannot carry typed, even for Claude', () => {
|
||||
const extra = {
|
||||
platform: 'linux' as const,
|
||||
host: { paired: true, provesAgentInFront: true, takesLaunchFile: false }
|
||||
host: {
|
||||
paired: true,
|
||||
provesAgentInFront: true,
|
||||
takesLaunchFile: false,
|
||||
windowsPowerShell: null
|
||||
}
|
||||
}
|
||||
expect(plan('claude', 'fix it', extra)?.carry).toBe('on-line')
|
||||
expect(plan('claude', 'first line\nsecond line', extra)?.carry).toBe('paste-after-ready')
|
||||
|
||||
@@ -170,7 +170,9 @@ export function carryLaunchPrompt<A extends CarriedPlanArgs, P extends { launchC
|
||||
return { carry: 'on-line', plan }
|
||||
}
|
||||
const windowsLine =
|
||||
args.platform === 'win32' ? windowsLaunchLineVerdict(text, plan.launchCommand, shell) : 'exact'
|
||||
args.platform === 'win32'
|
||||
? windowsLaunchLineVerdict(text, plan.launchCommand, shell, args.host.windowsPowerShell)
|
||||
: 'exact'
|
||||
// Why #24257's typed budget: an older paired host may type the line raw, truncated past it.
|
||||
if (
|
||||
windowsLine === 'damaged' ||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { describeLaunchHost } from './launch-host'
|
||||
import { describeLaunchHost, type WindowsPowerShell } from './launch-host'
|
||||
import { planLaunchPrompt } from './tui-agent-startup'
|
||||
import type { AgentStartupShell } from './tui-agent-startup-shell'
|
||||
import type { TuiAgent } from './tui-agent'
|
||||
@@ -31,11 +31,22 @@ const PROMPTS = {
|
||||
|
||||
type Row = keyof typeof PROMPTS
|
||||
|
||||
function windowsHost(windowsPowerShell: WindowsPowerShell | null) {
|
||||
return describeLaunchHost({
|
||||
launchPlatform: 'win32',
|
||||
isRemote: false,
|
||||
hostPlatform: 'win32',
|
||||
paired: false,
|
||||
windowsPowerShell
|
||||
})
|
||||
}
|
||||
|
||||
function carry(
|
||||
agent: TuiAgent,
|
||||
row: Row,
|
||||
shell: AgentStartupShell,
|
||||
command = `node C:/Users/neil/orca-qa/stack-final/win/bin/stub.js --qa-as=${agent}`
|
||||
command = `node C:/Users/neil/orca-qa/stack-final/win/bin/stub.js --qa-as=${agent}`,
|
||||
windowsPowerShell: WindowsPowerShell | null = null
|
||||
) {
|
||||
return planLaunchPrompt({
|
||||
agent,
|
||||
@@ -43,12 +54,7 @@ function carry(
|
||||
cmdOverrides: { [agent]: command },
|
||||
platform: 'win32',
|
||||
shell,
|
||||
host: describeLaunchHost({
|
||||
launchPlatform: 'win32',
|
||||
isRemote: false,
|
||||
hostPlatform: 'win32',
|
||||
paired: false
|
||||
}),
|
||||
host: windowsHost(windowsPowerShell),
|
||||
paste: 'when-host-proves-agent'
|
||||
})?.carry
|
||||
}
|
||||
@@ -72,9 +78,9 @@ describe('an agent.launch prompt on a Windows host, per measured shell', () => {
|
||||
['powershell', 'ml5', 'exact on 5.1 and 7', 'on-line'],
|
||||
['powershell', 'ml9k', 'damaged on 5.1, lines run as commands on 7', 'launch-file'],
|
||||
['powershell', 'e8191', 'exact on 5.1 and 7', 'on-line'],
|
||||
['powershell', 'pq', 'exact on 7, damaged on 5.1 (version unknown here)', 'on-line'],
|
||||
['powershell', 'pq', 'exact on 7, damaged on 5.1 (PowerShell unknown here)', 'on-line'],
|
||||
['powershell', 'ppct', 'exact on 5.1 and 7', 'on-line'],
|
||||
['powershell', 'pbs', 'exact on 7, damaged on 5.1 (version unknown here)', 'on-line'],
|
||||
['powershell', 'pbs', 'exact on 7, damaged on 5.1 (PowerShell unknown here)', 'on-line'],
|
||||
['posix', 'p2k', 'exact in Git Bash', 'on-line'],
|
||||
['posix', 'p20k', 'never started in Git Bash', 'launch-file'],
|
||||
['posix', 'ml5', 'exact in Git Bash', 'on-line'],
|
||||
@@ -88,6 +94,25 @@ describe('an agent.launch prompt on a Windows host, per measured shell', () => {
|
||||
expect(carry('codex', row, shell)).toBe(expected)
|
||||
})
|
||||
|
||||
// Why: this Orca spawns the pane, so it knows which PowerShell gets the line. 5.1's measured
|
||||
// damage gets a launch file, better than main; 7 keeps the line it carried exactly.
|
||||
it.each<[Row, WindowsPowerShell, string, 'on-line' | 'launch-file']>([
|
||||
['pq', 'powershell.exe', 'damaged on 5.1', 'launch-file'],
|
||||
['pbs', 'powershell.exe', 'damaged on 5.1', 'launch-file'],
|
||||
['ppct', 'powershell.exe', 'exact on 5.1', 'on-line'],
|
||||
['ml5', 'powershell.exe', 'exact on 5.1', 'on-line'],
|
||||
['p20k', 'powershell.exe', 'exact on 5.1', 'on-line'],
|
||||
['pq', 'pwsh.exe', 'exact on 7', 'on-line'],
|
||||
['pbs', 'pwsh.exe', 'exact on 7', 'on-line'],
|
||||
['ppct', 'pwsh.exe', 'exact on 7', 'on-line'],
|
||||
['ml9k', 'pwsh.exe', 'lines run as commands on 7', 'launch-file']
|
||||
])('powershell %s spawned as %s (main: %s) rides %s', (row, ps, _main, expected) => {
|
||||
const command = (agent: TuiAgent) =>
|
||||
`node C:/Users/neil/orca-qa/stack-final/win/bin/stub.js --qa-as=${agent}`
|
||||
expect(carry('claude', row, 'powershell', command('claude'), ps)).toBe(expected)
|
||||
expect(carry('codex', row, 'powershell', command('codex'), ps)).toBe(expected)
|
||||
})
|
||||
|
||||
// Why: a shim's cmd.exe re-reads the line, so cmd's cap, its `%NAME%` expansion and a cut at a
|
||||
// line break apply, and PowerShell hands a shim `"` and a trailing `\` the legacy way.
|
||||
it.each<[AgentStartupShell, Row, 'on-line' | 'launch-file']>([
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* `exact` arrived byte for byte, `damaged` was measured lost or changed, and `uncertain` was not
|
||||
* measured, so the caller does there what main does on its path.
|
||||
*/
|
||||
import type { WindowsPowerShell } from './launch-host'
|
||||
import type { AgentStartupShell } from './tui-agent-startup-shell'
|
||||
import { TYPED_STARTUP_LINE_BUDGET_BYTES } from './typed-startup-line'
|
||||
|
||||
@@ -72,7 +73,9 @@ function shellVerdict(line: string, shell: AgentStartupShell): WindowsLineVerdic
|
||||
export function windowsLaunchLineVerdict(
|
||||
prompt: string,
|
||||
line: string,
|
||||
shell: AgentStartupShell
|
||||
shell: AgentStartupShell,
|
||||
/** The PowerShell the pane is spawned as (`LaunchHost.windowsPowerShell`), when known. */
|
||||
windowsPowerShell: WindowsPowerShell | null
|
||||
): WindowsLineVerdict {
|
||||
if (hasKeyByte(line)) {
|
||||
return 'damaged'
|
||||
@@ -80,9 +83,16 @@ export function windowsLaunchLineVerdict(
|
||||
const verdict = shellVerdict(line, shell)
|
||||
const legacyArgsDamage = prompt.includes('"') || prompt.endsWith('\\')
|
||||
if (shell === 'cmd' || !launchesThroughCmdShim(line)) {
|
||||
// Why: 5.1 hands every native command `"` and a trailing `\` the legacy way, 7 only a shim;
|
||||
// this client cannot tell which PowerShell runs.
|
||||
return shell === 'powershell' && verdict === 'exact' && legacyArgsDamage ? 'uncertain' : verdict
|
||||
if (shell !== 'powershell' || verdict !== 'exact' || !legacyArgsDamage) {
|
||||
return verdict
|
||||
}
|
||||
// Measured: 5.1 hands every native command `"` and a trailing `\` the legacy way and breaks
|
||||
// them; 7 carried them exactly. Where the PowerShell is not known, main's delivery decides.
|
||||
return windowsPowerShell === 'powershell.exe'
|
||||
? 'damaged'
|
||||
: windowsPowerShell === 'pwsh.exe'
|
||||
? 'exact'
|
||||
: 'uncertain'
|
||||
}
|
||||
// Measured (#23962 W-1): PowerShell hands a shim `"` and a trailing `\` the legacy way, and the
|
||||
// shim's cmd.exe expands `%NAME%`.
|
||||
|
||||
@@ -95,7 +95,12 @@ describe('a prompt a Windows shell would damage on the launch line', () => {
|
||||
cmdOverrides: {},
|
||||
platform: 'win32',
|
||||
shell: 'powershell',
|
||||
host: { paired: true, provesAgentInFront: false, takesLaunchFile: false }
|
||||
host: {
|
||||
paired: true,
|
||||
provesAgentInFront: false,
|
||||
takesLaunchFile: false,
|
||||
windowsPowerShell: null
|
||||
}
|
||||
})
|
||||
expect(startup?.launchFile).toBeUndefined()
|
||||
expect(startup?.launchCommand).toContain('fix the build')
|
||||
|
||||
Reference in New Issue
Block a user