Fix PowerShell worktree terminal cwd (#7435)

This commit is contained in:
Matteo Musacchio 2026-07-24 04:24:48 -03:00 committed by GitHub
parent cda97cec41
commit c1d2c4be08
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 55 additions and 34 deletions

View File

@ -2,10 +2,6 @@ import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import {
encodePowerShellCommand,
getPowerShellOsc133Bootstrap
} from '../powershell-osc133-bootstrap'
import {
buildWslInteractiveLoginShellCommand,
escapeWslShCommandForWindows
@ -19,6 +15,15 @@ function expectedWslArgs(linuxCwd: string, distro?: string): string[] {
return distro ? ['-d', distro, ...shellArgs] : shellArgs
}
function decodePowerShellCommand(result: ReturnType<typeof resolveWindowsShellLaunchArgs>): string {
expect(result.shellArgs.slice(0, 3)).toEqual(['-NoLogo', '-NoExit', '-EncodedCommand'])
return Buffer.from(result.shellArgs[3] ?? '', 'base64').toString('utf16le')
}
function expectedPowerShellRestoreCwdCommand(cwdLiteral: string): string {
return `try { Set-Location -LiteralPath ${cwdLiteral} -ErrorAction Stop } catch { Write-Warning "Failed to restore working directory: $_" }`
}
describe('resolveWindowsShellLaunchArgs', () => {
let previousUserDataPath: string | undefined
let userDataPath: string
@ -90,14 +95,9 @@ describe('resolveWindowsShellLaunchArgs', () => {
'C:\\Users\\alice',
'C:\\Users\\alice'
)
expect(result.shellArgs).toEqual([
'-NoLogo',
'-NoExit',
'-EncodedCommand',
encodePowerShellCommand(getPowerShellOsc133Bootstrap())
])
expect(result.shellArgs).toEqual(['-NoLogo', '-NoExit', '-EncodedCommand', expect.any(String)])
const command = Buffer.from(result.shellArgs[3] ?? '', 'base64').toString('utf16le')
const command = decodePowerShellCommand(result)
const outputEncodingIndex = command.indexOf('[Console]::OutputEncoding')
const opencodeRestoreIndex = command.indexOf(
'$env:OPENCODE_CONFIG_DIR = $env:ORCA_OPENCODE_CONFIG_DIR'
@ -106,6 +106,9 @@ describe('resolveWindowsShellLaunchArgs', () => {
const ompExtensionIndex = command.indexOf('--extension $env:ORCA_OMP_STATUS_EXTENSION')
const codexRestoreIndex = command.indexOf('$env:CODEX_HOME = $env:ORCA_CODEX_HOME')
const promptIndex = command.indexOf('function Global:prompt')
const cwdRestoreIndex = command.indexOf(
expectedPowerShellRestoreCwdCommand("'C:\\Users\\alice'")
)
expect(command).not.toContain('$PROFILE')
expect(command).not.toContain('ORCA_PI_CODING_AGENT_DIR')
@ -118,6 +121,7 @@ describe('resolveWindowsShellLaunchArgs', () => {
expect(codexRestoreIndex).toBeGreaterThan(outputEncodingIndex)
expect(codexRestoreIndex).toBeGreaterThan(ompWrapperIndex)
expect(promptIndex).toBeGreaterThan(codexRestoreIndex)
expect(cwdRestoreIndex).toBeGreaterThan(promptIndex)
expect(command).toContain('Esc = [char]27')
expect(command).toContain('Bel = [char]7')
expect(command).toContain(')]133;D;$fakeExitCode$(')
@ -134,6 +138,21 @@ describe('resolveWindowsShellLaunchArgs', () => {
expect(result.effectiveCwd).toBe('C:\\Users\\alice\\project')
expect(result.validationCwd).toBe('C:\\Users\\alice\\project')
expect(decodePowerShellCommand(result)).toContain(
expectedPowerShellRestoreCwdCommand("'C:\\Users\\alice\\project'")
)
})
it('quotes the PowerShell cwd restore command literally', () => {
const result = resolveWindowsShellLaunchArgs(
'powershell.exe',
"C:\\Users\\alice\\client's app",
'C:\\Users\\alice'
)
expect(decodePowerShellCommand(result)).toContain(
expectedPowerShellRestoreCwdCommand("'C:\\Users\\alice\\client''s app'")
)
})
it('embeds short PowerShell startup commands after the OSC 133 bootstrap', () => {
@ -146,8 +165,9 @@ describe('resolveWindowsShellLaunchArgs', () => {
)
expect(result.startupCommandDeliveredInShellArgs).toBe(true)
const command = Buffer.from(result.shellArgs[3] ?? '', 'base64').toString('utf16le')
const command = decodePowerShellCommand(result)
expect(command).toContain('function Global:prompt')
expect(command).toContain(expectedPowerShellRestoreCwdCommand("'C:\\Users\\alice'"))
expect(command.trimEnd().endsWith("& 'codex' '--no-alt-screen'")).toBe(true)
})
@ -163,7 +183,7 @@ describe('resolveWindowsShellLaunchArgs', () => {
)
expect(result.startupCommandDeliveredInShellArgs).toBe(true)
const command = Buffer.from(result.shellArgs[3] ?? '', 'base64').toString('utf16le')
const command = decodePowerShellCommand(result)
expect(command).toContain(`\n${startupCommand}`)
expect(command.trimEnd().endsWith(startupCommand)).toBe(true)
})
@ -178,22 +198,16 @@ describe('resolveWindowsShellLaunchArgs', () => {
)
expect(result.startupCommandDeliveredInShellArgs).toBeUndefined()
expect(result.shellArgs).toEqual([
'-NoLogo',
'-NoExit',
'-EncodedCommand',
encodePowerShellCommand(getPowerShellOsc133Bootstrap())
])
expect(result.shellArgs).toEqual(['-NoLogo', '-NoExit', '-EncodedCommand', expect.any(String)])
expect(decodePowerShellCommand(result)).toContain(
expectedPowerShellRestoreCwdCommand("'C:\\Users\\alice'")
)
})
it('handles pwsh.exe (PowerShell Core) the same as Windows PowerShell', () => {
const result = resolveWindowsShellLaunchArgs('pwsh.exe', 'C:\\', 'C:\\Users\\alice')
expect(result.shellArgs).toEqual([
'-NoLogo',
'-NoExit',
'-EncodedCommand',
encodePowerShellCommand(getPowerShellOsc133Bootstrap())
])
expect(result.shellArgs).toEqual(['-NoLogo', '-NoExit', '-EncodedCommand', expect.any(String)])
expect(decodePowerShellCommand(result)).toContain(expectedPowerShellRestoreCwdCommand("'C:\\'"))
})
it('starts Git Bash as an interactive login shell with UTF-8 console setup', () => {
@ -362,12 +376,7 @@ describe('resolveWindowsShellLaunchArgs', () => {
it('is case-insensitive on the shell basename', () => {
const result = resolveWindowsShellLaunchArgs('PowerShell.EXE', 'C:\\', 'C:\\')
expect(result.shellArgs).toEqual([
'-NoLogo',
'-NoExit',
'-EncodedCommand',
encodePowerShellCommand(getPowerShellOsc133Bootstrap())
])
expect(result.shellArgs).toEqual(['-NoLogo', '-NoExit', '-EncodedCommand', expect.any(String)])
})
})

View File

@ -11,6 +11,7 @@ import {
encodePowerShellCommand,
getPowerShellOsc133Bootstrap
} from '../powershell-osc133-bootstrap'
import { quoteStartupArg } from '../../shared/tui-agent-startup-shell'
const CMD_EXE_COMMAND_LINE_MAX_CHARS = 8191
const STARTUP_COMMAND_TEXT_MAX_CHARS = 6000
@ -80,11 +81,22 @@ function getCmdShellArgStartupCommand(command?: string): string | null {
* Short startup commands are appended to the bootstrap and marked as delivered;
* large payloads return the bootstrap alone so stdin delivery remains available.
*/
function getPowerShellEncodedCommand(startupCommand?: string): {
function getPowerShellRestoreCwdCommand(cwd: string): string {
return [
'',
'# Profiles can change location; restore the PTY cwd after profile loading.',
`try { Set-Location -LiteralPath ${quoteStartupArg(cwd, 'powershell')} -ErrorAction Stop } catch { Write-Warning "Failed to restore working directory: $_" }`
].join('\n')
}
function getPowerShellEncodedCommand(
cwd: string,
startupCommand?: string
): {
encodedCommand: string
startupCommandDeliveredInShellArgs?: boolean
} {
const bootstrap = getPowerShellOsc133Bootstrap()
const bootstrap = `${getPowerShellOsc133Bootstrap()}${getPowerShellRestoreCwdCommand(cwd)}`
if (!startupCommand || startupCommand.length > STARTUP_COMMAND_TEXT_MAX_CHARS) {
return { encodedCommand: encodePowerShellCommand(bootstrap) }
}
@ -168,7 +180,7 @@ export function resolveWindowsShellLaunchArgs(
}
if (shellBasename === 'powershell.exe' || shellBasename === 'pwsh.exe') {
const powerShellCommand = getPowerShellEncodedCommand(startupCommand)
const powerShellCommand = getPowerShellEncodedCommand(nativeCwd, startupCommand)
// Why: foreground-process status on Windows depends on OSC 133 C/D, and
// PowerShell needs a prompt/readline bootstrap after profiles finish.
return {