fix(windows): find PowerShell 7 wherever it is installed and say so when it is missing

Search every place PowerShell 7 can be installed (Program Files, per-user,
the terminal PATH, App Paths, the Store package) off the main thread,
cache the result per shell setting, and retry after a launch failure or
a timed-out search.

When the shell setting asks for PowerShell 7 and none is found, the
fallback shell prints a one-line notice. Main translates the notice in
the UI language and passes it with the spawn options, because the
terminal daemon has no UI language; English is used when a client does
not send one. The cmd notice is caret-escaped so translated text is
always printed literally.
This commit is contained in:
Orca Worker
2026-11-12 23:15:18 -08:00
parent d2e2c788a5
commit 596bdd9afe
39 changed files with 1033 additions and 230 deletions
-1
View File
@@ -443,7 +443,6 @@
1 src/main/providers/local-pty/local-pty-shell-ready-wrapper-root.ts
1 src/main/providers/shell-launch/macos/login-session-pty-probe.ts
3 src/main/providers/shell-launch/macos/tcc-login-shell.ts
3 src/main/providers/shell-launch/windows/powershell-executable.ts
2 src/main/providers/working-directory/validation.ts
4 src/main/pty/history/fish-history-session.ts
4 src/main/pty/history/terminal-history-deletion.ts
+2
View File
@@ -5,6 +5,7 @@ import { startDaemon, type DaemonHandle } from './server/daemon-main'
import { createPtySubprocess } from './pty-subprocess/pty-subprocess'
import { warmWindowsConptyOnce } from './pty-subprocess/windows-conpty-warmup'
import { warmPwshAvailabilityCache } from '../pty/shell-startup/pwsh'
import { warmWindowsPwshSearch } from '../providers/shell-launch/windows/powershell-search'
import { createDaemonFileLog, createNoopDaemonFileLog } from './observability/daemon-file-log'
import { PROTOCOL_VERSION } from './protocol/daemon-protocol-version'
import { detectOwnCgroupScopeUnit, ownFreshDaemonScope } from './process-scopes/daemon-cgroup-scope'
@@ -60,6 +61,7 @@ async function main(): Promise<void> {
const processCeiling = initializeTerminalScopes(daemonScope, log)
startDaemonScopeDeathWatch({ daemonScope, log })
void warmPwshAvailabilityCache()
warmWindowsPwshSearch()
// Detached stderr is unavailable; retain TCC preflight failures in diagnostic logs.
const runMacosLoginPreflight = async (): Promise<void> => {
@@ -26,6 +26,7 @@ import type * as HistorySeedProtocol from '../history/seed-transfer/terminal-his
import type * as PtyOwnerQueryProtocol from './daemon-pty-owner-query-protocol'
import type { TerminalSnapshot } from './terminal-snapshot'
import type { SetLaunchNoteRequest } from './daemon-launch-note-protocol'
import type { WindowsPwshNotFoundNotices } from '../../providers/shell-launch/windows/pwsh-not-found-notice'
export type SessionState = 'created' | 'spawning' | 'running' | 'exiting' | 'exited'
@@ -53,6 +54,7 @@ export type CreateOrAttachRequest = {
terminalWindowsWslDistro?: string | null
/** Preserve the selected PowerShell implementation so daemon and local spawns resolve the same executable. */
terminalWindowsPowerShellImplementation?: 'auto' | 'powershell.exe' | 'pwsh.exe'
terminalWindowsPwshNotFoundNotices?: WindowsPwshNotFoundNotices
shellReadySupported?: boolean
shellReadyTimeoutMs?: number
/** Server-side fence that prevents a client timeout from publishing an orphan PTY. */
@@ -133,6 +133,9 @@ export abstract class DaemonPtySpawnRequest extends DaemonPtyRuntimeState {
terminalWindowsPowerShellImplementation: context.attachOnly
? undefined
: opts.terminalWindowsPowerShellImplementation,
terminalWindowsPwshNotFoundNotices: context.attachOnly
? undefined
: opts.terminalWindowsPwshNotFoundNotices,
shellReadySupported: context.attachOnly ? false : context.shellReadySupported,
...(!context.attachOnly && context.shellReadyTimeoutMs !== undefined
? { shellReadyTimeoutMs: context.shellReadyTimeoutMs }
@@ -9,6 +9,7 @@ import {
wrapShellSpawnForMacosTccAttribution
} from '../../providers/shell-launch/macos/tcc-login-shell'
import type { WindowsShellSpawnAttempt } from '../../providers/shell-launch/windows/shell-fallback-chain'
import { forgetWindowsPowerShellAfterLaunchFailure } from '../../providers/shell-launch/windows/powershell-search'
import { assignHostProcessToKillOnCloseJob } from '../../windows/windows-pty-job'
import { wrapPtySpawnInTerminalScope } from '../process-scopes/terminal-scopes-runtime'
import { spawnConptyWithInputReader, type ConptyStartupEvents } from './conpty-startup-input-reader'
@@ -149,6 +150,7 @@ export async function spawnNativeDaemonPty(args: {
if (process.platform !== 'win32') {
throw primaryErr
}
forgetWindowsPowerShellAfterLaunchFailure(args.shellPath)
for (const attempt of args.windowsFallbackAttempts.slice(1)) {
try {
const spawned = await spawnAt(
@@ -15,6 +15,7 @@ import {
import { createDaemonPtySubprocessHandle } from './subprocess-handle'
import type { StartupCommandDelivery } from '../../../shared/agent-launch/startup/codex-startup-delivery'
import type { TuiAgent } from '../../../shared/agent-launch/catalog/tui-agent'
import type { WindowsPwshNotFoundNotices } from '../../providers/shell-launch/windows/pwsh-not-found-notice'
const PTY_SPAWN_HEALTH_RETRY_ATTEMPTS = 2
@@ -33,6 +34,7 @@ export type PtySubprocessOptions = {
terminalShellArgs?: string[]
terminalWindowsWslDistro?: string | null
terminalWindowsPowerShellImplementation?: 'auto' | 'powershell.exe' | 'pwsh.exe'
terminalWindowsPwshNotFoundNotices?: WindowsPwshNotFoundNotices
onCreationDisposition?: (disposition: PtyCreationDisposition) => void
assertCreateAllowed?: () => void
isCanceled?: () => boolean
@@ -69,7 +71,7 @@ export async function createPtySubprocess(opts: PtySubprocessOptions): Promise<S
try {
const size = normalizePtySize(opts.cols, opts.rows)
const env = createDaemonPtyEnvironment(opts)
const launch = createPtyShellLaunchPlan(opts, env)
const launch = await createPtyShellLaunchPlan(opts, env)
await preflightPtySpawn({
validationCwd: launch.validationCwd,
@@ -59,12 +59,13 @@ export type PtyShellLaunchPlan = {
startupAgentRecognition: RecognizedAgentProcess | null
}
export function createPtyShellLaunchPlan(
export async function createPtyShellLaunchPlan(
opts: PtySubprocessOptions,
env: Record<string, string>
): PtyShellLaunchPlan {
): Promise<PtyShellLaunchPlan> {
const resolvedWslContext = resolveWslSessionContext(opts)
let shellPath = resolvedWslContext ? 'wsl.exe' : opts.shellOverride || resolvePtyShellPath(env)
const requestedShell = shellPath
let shellArgs: string[]
let startupCommandDeliveredInShellArgs = false
let windowsFallbackAttempts: WindowsShellSpawnAttempt[] = []
@@ -111,12 +112,17 @@ export function createPtyShellLaunchPlan(
) {
env[ORCA_CODEX_LAUNCH_PREFLIGHT_CMD_QUOTE_ENV] = '"'
}
windowsFallbackAttempts = buildWindowsPowerShellSpawnAttempts({
windowsFallbackAttempts = await buildWindowsPowerShellSpawnAttempts({
shellPath,
cwd: spawnCwd,
defaultCwd: resolveSafePtyDefaultCwd(),
wslContext: resolvedWslContext,
startupCommand: opts.command
startupCommand: opts.command,
// The daemon outlives app restarts; the terminal env carries main's current PATH.
env,
requestedShell,
powerShellImplementation: opts.terminalWindowsPowerShellImplementation,
pwshNotFoundNotices: opts.terminalWindowsPwshNotFoundNotices
})
const primaryAttempt = windowsFallbackAttempts[0]
if (primaryAttempt) {
@@ -108,7 +108,7 @@ describeOnWindows("Git Bash launcher shell proof with Orca's real launch", () =>
env: extraEnv
}
const env = createDaemonPtyEnvironment(opts)
const plan = createPtyShellLaunchPlan(opts, env)
const plan = await createPtyShellLaunchPlan(opts, env)
expect(isGitForWindowsBashLauncherPath(plan.shellPath)).toBe(true)
expect(plan.shellArgs.join(' ')).toContain('exec "$BASH"')
// Launcher, exec stub, interactive bash: the shape that a size-1 or size-2 rule never matches.
@@ -44,8 +44,6 @@ const PWSH7_ABS = 'C:\\Program Files\\PowerShell\\7\\pwsh.exe'
const WINDOWS_POWERSHELL_ABS = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe'
const CMD_ABS = 'C:\\Windows\\System32\\cmd.exe'
vi.mock('../../../providers/shell-launch/windows/powershell-executable', () => ({
resolveWindowsPowerShellExecutablePath: (family: 'pwsh.exe' | 'powershell.exe') =>
family === 'pwsh.exe' ? PWSH7_ABS : WINDOWS_POWERSHELL_ABS,
resolveWindowsPowerShellSpawnChain: (family: 'pwsh.exe' | 'powershell.exe') =>
family === 'pwsh.exe'
? [PWSH7_ABS, WINDOWS_POWERSHELL_ABS, CMD_ABS]
@@ -126,6 +126,7 @@ export class DaemonTerminalAdmission {
terminalShellArgs: payload.terminalShellArgs,
terminalWindowsWslDistro: payload.terminalWindowsWslDistro,
terminalWindowsPowerShellImplementation: payload.terminalWindowsPowerShellImplementation,
terminalWindowsPwshNotFoundNotices: payload.terminalWindowsPwshNotFoundNotices,
shellReadySupported: payload.shellReadySupported,
historySeedChunks,
startupIngress: parsePtyStartupIngressIntent(payload.startupIngress),
@@ -11,6 +11,7 @@ import type {
} from '../../../shared/agent-session/agent-session-host-authority'
import type { PtyIncarnationId } from '../../../shared/terminal/pty/lifecycle/pty-incarnation'
import type { TerminalExitCause } from '../../../shared/terminal/pty/lifecycle/terminal-exit-cause'
import type { WindowsPwshNotFoundNotices } from '../../providers/shell-launch/windows/pwsh-not-found-notice'
export type CreateOrAttachOptions = {
sessionId: string
@@ -28,6 +29,7 @@ export type CreateOrAttachOptions = {
terminalShellArgs?: string[]
terminalWindowsWslDistro?: string | null
terminalWindowsPowerShellImplementation?: 'auto' | 'powershell.exe' | 'pwsh.exe'
terminalWindowsPwshNotFoundNotices?: WindowsPwshNotFoundNotices
shellReadySupported?: boolean
shellReadyTimeoutMs?: number
historySeedChunks?: readonly string[]
@@ -5,6 +5,7 @@ import type { SubprocessHandle } from '../sessions/session-subprocess-handle'
import type { TakePendingOutputResult } from '../protocol/daemon-rpc-contract'
import type { TerminalSnapshot } from '../protocol/terminal-snapshot'
import type { TerminalSpawnGate } from '../admission/terminal-spawn-gate'
import type { WindowsPwshNotFoundNotices } from '../../providers/shell-launch/windows/pwsh-not-found-notice'
export type TerminalHostOptions = {
spawnSubprocess: (opts: {
@@ -21,6 +22,7 @@ export type TerminalHostOptions = {
terminalShellArgs?: string[]
terminalWindowsWslDistro?: string | null
terminalWindowsPowerShellImplementation?: 'auto' | 'powershell.exe' | 'pwsh.exe'
terminalWindowsPwshNotFoundNotices?: WindowsPwshNotFoundNotices
onCreationDisposition?: (disposition: PtyCreationDisposition) => void
assertCreateAllowed?: () => void
isCanceled?: () => boolean
@@ -72,6 +72,7 @@ export async function spawnAndPublishSession(
terminalShellArgs: opts.terminalShellArgs,
terminalWindowsWslDistro: opts.terminalWindowsWslDistro,
terminalWindowsPowerShellImplementation: opts.terminalWindowsPowerShellImplementation,
terminalWindowsPwshNotFoundNotices: opts.terminalWindowsPwshNotFoundNotices,
isCanceled: opts.isCanceled,
assertCreateAllowed: deps.assertCreateAllowed,
...(opts.cancelSignal ? { cancelSignal: opts.cancelSignal } : {})
@@ -12,6 +12,8 @@ import { PI_PROCESS_OWNER_ENV_KEYS } from '../../pty/shell-startup/pi-process-ow
import { planCodexNoDaemonLaunch } from '../../pty/shell-startup/codex-no-daemon-launch-command'
import { applyAgentWorkspaceTrustToSpawn } from '../../agent-launch/trust/agent-workspace-trust-spawn'
import type { PtySpawnOptions } from '../../providers/pty/provider-contract'
import { buildWindowsPwshNotFoundNotices } from '../../providers/shell-launch/windows/pwsh-not-found-notice'
import { translateMain } from '../../i18n/main-i18n'
import type { CodexAccountSelectionTarget } from '../../../shared/codex-selection-lane'
import { getStartupTerminalIngressIntent } from '../terminal-startup-color-query-replies'
import { CODEX_HOME_ENV_KEYS } from './host-env/codex-home'
@@ -249,6 +251,8 @@ export async function assemblePtySpawnLaunchOptions(input: PtySpawnLaunchOptions
spawnOptions.terminalWindowsPowerShellImplementation = input.getSettings
? (input.getSettings()?.terminalWindowsPowerShellImplementation ?? 'auto')
: undefined
spawnOptions.terminalWindowsPwshNotFoundNotices =
buildWindowsPwshNotFoundNotices(translateMain)
}
}
return { spawnOptions, env, launchCommand }
@@ -11,6 +11,7 @@ import {
wrapShellSpawnForMacosTccAttribution
} from '../shell-launch/macos/tcc-login-shell'
import { formatLocalPtyEnvironmentDiag } from '../working-directory/validation'
import { forgetWindowsPowerShellAfterLaunchFailure } from '../shell-launch/windows/powershell-search'
let didEnsureSpawnHelperExecutable = false
@@ -144,6 +145,9 @@ function spawnWindowsFallbackChain(
): ShellSpawnResult | null {
const { termName = 'xterm-256color', cols, rows, env, ptySpawn } = params
const attempts = params.windowsFallbackAttempts ?? []
if (attempts.length > 0) {
forgetWindowsPowerShellAfterLaunchFailure(params.shellPath)
}
// Skip the first entry: it is the primary that already failed above.
for (const attempt of attempts.slice(1)) {
try {
@@ -18,7 +18,10 @@ import {
shouldProbeWindowsPowerShellAvailability,
type WindowsPowerShellShellFamily
} from '../shell-launch/windows/powershell'
import { buildWindowsPowerShellSpawnAttempts } from '../shell-launch/windows/shell-fallback-chain'
import {
buildWindowsPowerShellSpawnAttempts,
type WindowsShellSpawnAttempt
} from '../shell-launch/windows/shell-fallback-chain'
import { resolveWindowsShellLaunchArgs } from '../shell-launch/windows/shell-args'
import { assertSafeAgentStartupCwd } from '../working-directory/default-cwd'
import type { PtySpawnOptions } from '../pty/provider-contract'
@@ -48,7 +51,7 @@ export type LocalPtyLaunchPlan = {
effectiveCwd: string
validationCwd: string
startupCommandDeliveredInShellArgs: boolean
windowsFallbackAttempts: ReturnType<typeof buildWindowsPowerShellSpawnAttempts>
windowsFallbackAttempts: WindowsShellSpawnAttempt[]
shellReadyLaunch: ReturnType<typeof getShellLaunchConfig> | null
getFallbackShellReadyConfig:
| ((shell: string) => ReturnType<typeof getShellLaunchConfig>)
@@ -62,7 +65,7 @@ export type LocalPtyLaunchPlan = {
export class DeferredLocalPtyLaunchPlan {
constructor(
readonly availability: boolean | Promise<boolean>,
readonly finish: (available: boolean) => LocalPtyLaunchPlan
readonly finish: (available: boolean) => Promise<LocalPtyLaunchPlan>
) {}
}
@@ -74,7 +77,7 @@ function finalizeLocalPtyLaunchPlan(
effectiveCwd: string
validationCwd: string
startupCommandDeliveredInShellArgs?: boolean
windowsFallbackAttempts?: ReturnType<typeof buildWindowsPowerShellSpawnAttempts>
windowsFallbackAttempts?: WindowsShellSpawnAttempt[]
}
): LocalPtyLaunchPlan {
ensureNodePtySpawnHelperExecutable()
@@ -133,7 +136,7 @@ function createWindowsLocalPtyLaunchPlan(
})
const shouldResolvePowerShellFamily =
powerShellImplementation !== undefined || pathWin32.basename(shellFamily) === shellFamily
const finish = (pwshAvailable: boolean): LocalPtyLaunchPlan => {
const finish = async (pwshAvailable: boolean): Promise<LocalPtyLaunchPlan> => {
let shellPath: string
if (resolvedGitBashPath) {
shellPath = resolvedGitBashPath
@@ -149,12 +152,17 @@ function createWindowsLocalPtyLaunchPlan(
: shellFamily
}
// Bare pwsh.exe may resolve to a failing Store alias; use absolute executable/fallback attempts.
const windowsFallbackAttempts = buildWindowsPowerShellSpawnAttempts({
const windowsFallbackAttempts = await buildWindowsPowerShellSpawnAttempts({
shellPath,
cwd,
defaultCwd,
wslContext: seed.launchWslContext,
startupCommand: args.command
startupCommand: args.command,
// Main's env plus the spawn's own; PATH here is what the shell will see.
env: args.env ? { ...process.env, ...args.env } : process.env,
requestedShell: shellFamily,
powerShellImplementation,
pwshNotFoundNotices: args.terminalWindowsPwshNotFoundNotices
})
const primaryAttempt = windowsFallbackAttempts[0]
if (primaryAttempt) {
@@ -183,9 +191,11 @@ function createWindowsLocalPtyLaunchPlan(
windowsFallbackAttempts
})
}
return shouldProbePwsh
? new DeferredLocalPtyLaunchPlan(getOptions().pwshAvailable?.() ?? false, finish)
: finish(false)
// Locating PowerShell is async, so every Windows plan defers.
return new DeferredLocalPtyLaunchPlan(
shouldProbePwsh ? (getOptions().pwshAvailable?.() ?? false) : false,
finish
)
}
export function createLocalPtyLaunchPlan(
@@ -86,8 +86,6 @@ const WINDOWS_POWERSHELL_ABS = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\
const PWSH7_ABS = 'C:\\Program Files\\PowerShell\\7\\pwsh.exe'
const CMD_ABS = 'C:\\Windows\\System32\\cmd.exe'
vi.mock('../shell-launch/windows/powershell-executable', () => ({
resolveWindowsPowerShellExecutablePath: (family: 'pwsh.exe' | 'powershell.exe') =>
family === 'pwsh.exe' ? PWSH7_ABS : WINDOWS_POWERSHELL_ABS,
resolveWindowsPowerShellSpawnChain: (family: 'pwsh.exe' | 'powershell.exe') =>
family === 'pwsh.exe'
? [PWSH7_ABS, WINDOWS_POWERSHELL_ABS, CMD_ABS]
@@ -86,8 +86,6 @@ const WINDOWS_POWERSHELL_ABS = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\
const PWSH7_ABS = 'C:\\Program Files\\PowerShell\\7\\pwsh.exe'
const CMD_ABS = 'C:\\Windows\\System32\\cmd.exe'
vi.mock('../shell-launch/windows/powershell-executable', () => ({
resolveWindowsPowerShellExecutablePath: (family: 'pwsh.exe' | 'powershell.exe') =>
family === 'pwsh.exe' ? PWSH7_ABS : WINDOWS_POWERSHELL_ABS,
resolveWindowsPowerShellSpawnChain: (family: 'pwsh.exe' | 'powershell.exe') =>
family === 'pwsh.exe'
? [PWSH7_ABS, WINDOWS_POWERSHELL_ABS, CMD_ABS]
@@ -64,7 +64,7 @@ export async function spawnLocalPty(
if (plan instanceof DeferredLocalPtyLaunchPlan) {
const available = await plan.availability
throwIfCanceled()
plan = plan.finish(available)
plan = await plan.finish(available)
}
throwIfCanceled()
// Daemon-off (the daemon failed to start) a restore spawns here: same E9 hold as the daemon's.
@@ -21,6 +21,7 @@ import type { PtyProcessInfo } from './process-info'
import type { TerminalExitCause } from '../../../shared/terminal/pty/lifecycle/terminal-exit-cause'
import type { TerminalOwner } from '../../../shared/terminal/pty/lifecycle/terminal-owner'
import type { WriteSettlement } from '../../../shared/terminal/pty/lifecycle/pty-write-settlement'
import type { WindowsPwshNotFoundNotices } from '../shell-launch/windows/pwsh-not-found-notice'
export type PtyProviderBufferSnapshot = {
data: string
@@ -86,6 +87,8 @@ export type PtySpawnOptions = {
terminalWindowsWslDistro?: string | null
/** Persisted inbox/pwsh choice keeps local and daemon PowerShell-family launches aligned. */
terminalWindowsPowerShellImplementation?: 'auto' | 'powershell.exe' | 'pwsh.exe'
/** Translated by main so a fallback shell can say PowerShell 7 was not found in the UI language. */
terminalWindowsPwshNotFoundNotices?: WindowsPwshNotFoundNotices
/** Fresh-spawn-only source authority installed before any PTY output is released. */
startupIngress?: PtyStartupIngressIntent
/** Why the caller is starting this terminal; presence lets the host defer an automatic restore. */
@@ -0,0 +1,366 @@
import { win32 as pathWin32 } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { resolveWindowsPowerShellSpawnChain } from './powershell-executable'
import { forgetWindowsPowerShellAfterLaunchFailure } from './powershell-search'
import { buildWindowsPowerShellSpawnAttempts } from './shell-fallback-chain'
// A simulated machine: real installs are never touched.
type SimulatedAppPaths = { machine?: string; user?: string }
const machine = vi.hoisted(() => {
const appPaths: SimulatedAppPaths = {}
return {
files: new Map<string, string>(),
aliases: new Map<string, string>(),
// Paths whose stat never settles, like an unreachable network share.
unresponsive: new Set<string>(),
appPaths
}
})
function key(path: string): string {
return pathWin32.normalize(path).toLowerCase()
}
function enoent(path: string): Error {
return Object.assign(new Error(`ENOENT: ${path}`), { code: 'ENOENT' })
}
vi.mock('node:fs/promises', () => ({
stat: async (path: string) => {
if (machine.unresponsive.has(key(path))) {
return new Promise(() => {})
}
if (!machine.files.has(key(path))) {
throw enoent(path)
}
return { isFile: () => true, size: 4096 }
},
readlink: async (path: string) => {
const target = machine.aliases.get(key(path))
if (!target) {
throw enoent(path)
}
return target
},
readdir: async (dir: string) => {
const prefix = `${key(dir)}\\`
const names = new Set<string>()
for (const original of [...machine.files.values(), ...machine.aliases.keys()]) {
const normalized = pathWin32.normalize(original)
if (normalized.toLowerCase().startsWith(prefix)) {
names.add(normalized.slice(prefix.length).split('\\')[0])
}
}
if (names.size === 0) {
throw enoent(dir)
}
return [...names]
}
}))
vi.mock('../../../windows/windows-native-registry', async (importOriginal) => ({
...(await importOriginal<object>()),
loadWindowsNativeRegistry: () => ({
HK: { CU: 1, LM: 2 },
getRegistryKey: (root: number) => {
const value = root === 2 ? machine.appPaths.machine : machine.appPaths.user
return value === undefined ? null : { '': { name: '', type: 2, value } }
}
})
}))
const ENV: NodeJS.ProcessEnv = {
ProgramW6432: 'C:\\Program Files',
ProgramFiles: 'C:\\Program Files',
'ProgramFiles(x86)': 'C:\\Program Files (x86)',
LOCALAPPDATA: 'C:\\Users\\dev\\AppData\\Local',
USERPROFILE: 'C:\\Users\\dev',
SystemRoot: 'C:\\Windows',
ComSpec: 'C:\\Windows\\System32\\cmd.exe',
Path: 'C:\\Windows\\System32'
}
const WINDOWS_POWERSHELL = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe'
const CMD = 'C:\\Windows\\System32\\cmd.exe'
const WINDOWS_APPS = 'C:\\Users\\dev\\AppData\\Local\\Microsoft\\WindowsApps'
const STORE_EXE =
'C:\\Program Files\\WindowsApps\\Microsoft.PowerShell_7.6.3.0_x64__8wekyb3d8bbwe\\pwsh.exe'
function install(...paths: string[]): void {
for (const path of paths) {
machine.files.set(key(path), path)
}
}
function uninstall(path: string): void {
machine.files.delete(key(path))
}
function alias(path: string, target: string): void {
machine.aliases.set(key(path), target)
}
let searchId = 0
// Each test gets its own setting so one test's cached search never answers another's.
function freshSetting(): string {
searchId += 1
return `pwsh.exe|test-${searchId}`
}
async function findPwsh(
options: { env?: NodeJS.ProcessEnv; settingKey?: string } = {}
): Promise<string> {
const chain = await resolveWindowsPowerShellSpawnChain('pwsh.exe', {
platform: 'win32',
env: options.env ?? ENV,
settingKey: options.settingKey ?? freshSetting()
})
return chain[0]
}
beforeEach(() => {
machine.files.clear()
machine.aliases.clear()
machine.unresponsive.clear()
machine.appPaths = {}
install(WINDOWS_POWERSHELL, CMD)
// This process's own PATH is the stale one; tests put fresh entries on the terminal env.
vi.stubEnv('PATH', 'C:\\Windows\\System32')
vi.stubEnv('Path', 'C:\\Windows\\System32')
})
afterEach(() => {
vi.unstubAllEnvs()
})
describe('PowerShell 7 discovery sources', () => {
it('picks the highest Program Files version across 64-bit and 32-bit roots', async () => {
install(
'C:\\Program Files\\PowerShell\\7\\pwsh.exe',
'C:\\Program Files (x86)\\PowerShell\\6\\pwsh.exe',
'C:\\Program Files (x86)\\PowerShell\\8\\pwsh.exe'
)
expect(await findPwsh()).toBe('C:\\Program Files (x86)\\PowerShell\\8\\pwsh.exe')
})
it('finds a per-user install', async () => {
install('C:\\Users\\dev\\AppData\\Local\\Microsoft\\PowerShell\\7\\pwsh.exe')
expect(await findPwsh()).toBe(
'C:\\Users\\dev\\AppData\\Local\\Microsoft\\PowerShell\\7\\pwsh.exe'
)
})
it('finds pwsh on the terminal PATH when this process PATH is stale', async () => {
install('D:\\Tools\\pwsh\\pwsh.exe')
const env = { ...ENV, Path: 'relative-tools;"D:\\Tools\\pwsh";C:\\Windows\\System32' }
expect(await findPwsh({ env })).toBe('D:\\Tools\\pwsh\\pwsh.exe')
})
it('reads both PATH spellings when a merged env carries PATH and Path', async () => {
install('D:\\Second\\pwsh.exe')
const env = { ...ENV, PATH: 'C:\\Windows\\System32', Path: 'D:\\Second' }
expect(await findPwsh({ env })).toBe('D:\\Second\\pwsh.exe')
})
it('stops waiting on an unresponsive PATH entry and uses the install it found', async () => {
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
try {
install('C:\\Program Files\\PowerShell\\7\\pwsh.exe')
machine.unresponsive.add(key('\\\\slow-share\\tools\\pwsh.exe'))
const found = findPwsh({ env: { ...ENV, Path: '\\\\slow-share\\tools' } })
await vi.advanceTimersByTimeAsync(1_000)
expect(await found).toBe('C:\\Program Files\\PowerShell\\7\\pwsh.exe')
} finally {
vi.useRealTimers()
}
})
it('searches again once a timed-out only install starts responding', async () => {
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
try {
const settingKey = freshSetting()
const env = { ...ENV, Path: '\\\\slow-share\\tools' }
install('\\\\slow-share\\tools\\pwsh.exe')
machine.unresponsive.add(key('\\\\slow-share\\tools\\pwsh.exe'))
const first = findPwsh({ env, settingKey })
await vi.advanceTimersByTimeAsync(1_000)
expect(await first).toBe(WINDOWS_POWERSHELL)
machine.unresponsive.clear()
expect(await findPwsh({ env, settingKey })).toBe('\\\\slow-share\\tools\\pwsh.exe')
} finally {
vi.useRealTimers()
}
})
it('reads the App Paths registration, machine before user', async () => {
install('C:\\Program Files\\Custom PS\\pwsh.exe', 'D:\\User PS\\pwsh.exe')
machine.appPaths = {
machine: '"%ProgramFiles%\\Custom PS\\pwsh.exe"',
user: 'D:\\User PS\\pwsh.exe'
}
expect(await findPwsh()).toBe('C:\\Program Files\\Custom PS\\pwsh.exe')
})
it('resolves the Store install when WindowsApps is not on PATH', async () => {
install(STORE_EXE)
alias(`${WINDOWS_APPS}\\Microsoft.PowerShell_8wekyb3d8bbwe\\pwsh.exe`, STORE_EXE)
expect(await findPwsh()).toBe(STORE_EXE)
})
it.each([
'C:\\Users\\dev\\.dotnet\\tools\\pwsh.exe',
'C:\\Users\\dev\\scoop\\apps\\pwsh\\current\\pwsh.exe'
])('finds the user tool install %s', async (path) => {
install(path)
expect(await findPwsh()).toBe(path)
})
it('never launches the zero-byte Store alias itself', async () => {
install(`${WINDOWS_APPS}\\pwsh.exe`)
expect(await findPwsh()).toBe(WINDOWS_POWERSHELL)
})
it.each(['pwsh.exe', `${WINDOWS_APPS}\\pwsh.exe`, 'C:\\Tools\\powershell.exe'])(
'rejects unsafe Store alias target %s',
async (target) => {
install(target)
alias(`${WINDOWS_APPS}\\pwsh.exe`, target)
expect(await findPwsh()).toBe(WINDOWS_POWERSHELL)
}
)
it('uses a preview build only when no stable build exists', async () => {
const preview = 'C:\\Program Files\\PowerShell\\7-preview\\pwsh.exe'
install(preview)
expect(await findPwsh()).toBe(preview)
install('C:\\Users\\dev\\.dotnet\\tools\\pwsh.exe')
expect(await findPwsh()).toBe('C:\\Users\\dev\\.dotnet\\tools\\pwsh.exe')
})
})
describe('PowerShell 7 search cache', () => {
const programFilesPwsh = 'C:\\Program Files\\PowerShell\\7\\pwsh.exe'
const perUserPwsh = 'C:\\Users\\dev\\AppData\\Local\\Microsoft\\PowerShell\\7\\pwsh.exe'
it('reuses the found path until the shell setting changes', async () => {
const settingKey = freshSetting()
install(programFilesPwsh)
expect(await findPwsh({ settingKey })).toBe(programFilesPwsh)
uninstall(programFilesPwsh)
install(perUserPwsh)
expect(await findPwsh({ settingKey })).toBe(programFilesPwsh)
expect(await findPwsh({ settingKey: freshSetting() })).toBe(perUserPwsh)
})
it('searches again after the cached path fails to launch', async () => {
const settingKey = freshSetting()
install(programFilesPwsh)
expect(await findPwsh({ settingKey })).toBe(programFilesPwsh)
uninstall(programFilesPwsh)
install(perUserPwsh)
forgetWindowsPowerShellAfterLaunchFailure(programFilesPwsh)
expect(await findPwsh({ settingKey })).toBe(perUserPwsh)
})
it('keeps a miss for the same PATH but searches again once the terminal PATH changes', async () => {
const settingKey = freshSetting()
expect(await findPwsh({ settingKey })).toBe(WINDOWS_POWERSHELL)
install('D:\\New\\pwsh.exe')
expect(await findPwsh({ settingKey })).toBe(WINDOWS_POWERSHELL)
const env = { ...ENV, Path: `${ENV.Path};D:\\New` }
expect(await findPwsh({ settingKey, env })).toBe('D:\\New\\pwsh.exe')
})
})
describe('PowerShell 7 search warm-up', () => {
it('lets the first terminal find pwsh on its PATH after a warm-up saw a stale PATH', async () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')!
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
try {
vi.resetModules()
const search = await import('./powershell-search')
install('D:\\Fresh\\pwsh.exe')
search.warmWindowsPwshSearch()
const env = { ...ENV, Path: 'D:\\Fresh;C:\\Windows\\System32' }
expect(await search.findWindowsPwsh({ env, settingKey: 'powershell.exe|pwsh.exe' })).toBe(
'D:\\Fresh\\pwsh.exe'
)
} finally {
Object.defineProperty(process, 'platform', originalPlatform)
}
})
})
describe('PowerShell 7 spawn attempts', () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')!
beforeEach(() => {
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
})
afterEach(() => {
Object.defineProperty(process, 'platform', originalPlatform)
})
function attempts(
requestedShell: string,
implementation: string,
pwshNotFoundNotices?: { windowsPowerShell: string; commandPrompt: string }
) {
return buildWindowsPowerShellSpawnAttempts({
pwshNotFoundNotices,
shellPath: 'pwsh.exe',
cwd: 'C:\\repo',
defaultCwd: 'C:\\repo',
env: ENV,
requestedShell,
powerShellImplementation: implementation
})
}
// The fallback shell prints the notice itself, so it rides ConPTY like any other output.
function startupScript(attempt: { shellPath: string; shellArgs: string[] }): string {
return attempt.shellPath === CMD
? attempt.shellArgs[1]
: Buffer.from(attempt.shellArgs[3], 'base64').toString('utf16le')
}
it('has each fallback shell announce that PowerShell 7 was not found', async () => {
const plan = await attempts('powershell.exe', `missing-${freshSetting()}`)
expect(plan.map((attempt) => attempt.shellPath)).toEqual([WINDOWS_POWERSHELL, CMD])
expect(startupScript(plan[0])).toMatch(/^Write-Host '\[Orca .*\]' -ForegroundColor Yellow\n/)
expect(startupScript(plan[1])).toMatch(/ & echo \[Orca .*\]$/)
})
it('prints the notice main translated, kept literal in cmd', async () => {
const plan = await attempts('powershell.exe', `translated-${freshSetting()}`, {
windowsPowerShell: 'PowerShell 7 fehlt, Windows PowerShell geöffnet.',
commandPrompt: 'PowerShell 7 fehlt & Eingabeaufforderung (cmd) geöffnet.'
})
expect(startupScript(plan[0])).toMatch(
/^Write-Host '\[PowerShell 7 fehlt, Windows PowerShell geöffnet\.\]' -ForegroundColor Yellow\n/
)
expect(startupScript(plan[1])).toMatch(
/ & echo \[PowerShell 7 fehlt \^& Eingabeaufforderung \^\(cmd\^\) geöffnet\.\]$/
)
})
it('opens PowerShell 7 without a notice when it is found', async () => {
install('C:\\Program Files\\PowerShell\\7\\pwsh.exe')
const plan = await attempts('powershell.exe', `found-${freshSetting()}`)
expect(plan[0].shellPath).toBe('C:\\Program Files\\PowerShell\\7\\pwsh.exe')
expect(plan.map(startupScript).join('\n')).not.toContain('[Orca')
})
it('prefers a preview build when the shell setting points at one', async () => {
const preview = 'C:\\Program Files\\PowerShell\\7-preview\\pwsh.exe'
install('C:\\Program Files\\PowerShell\\7\\pwsh.exe', preview)
const plan = await attempts(preview, `preview-${freshSetting()}`)
expect(plan[0].shellPath).toBe(preview)
})
})
@@ -1,196 +1,49 @@
import { existsSync, readlinkSync, statSync } from 'node:fs'
import { win32 as pathWin32 } from 'node:path'
import { readWindowsEnv } from './pwsh-discovery'
import {
findWindowsPowerShell,
findWindowsPwsh,
type WindowsPowerShellResolveOptions
} from './powershell-search'
export type WindowsPowerShellResolveOptions = {
env?: NodeJS.ProcessEnv
/** Returns true when the candidate path is a real, non-empty executable. */
isRealExecutable?: (path: string) => boolean
/** Resolves a Store App Execution Alias to its package executable. */
resolveAppExecutionAlias?: (path: string) => string | null
platform?: NodeJS.Platform
}
/** Store alias reparse stubs may launch through execFile PATH lookup but fail ConPTY absolute CreateProcessW (5). */
function isWindowsAppExecutionAliasPath(candidate: string): boolean {
return /[\\/]Microsoft[\\/]WindowsApps[\\/]/i.test(pathWin32.normalize(candidate))
}
/** Reject zero-byte alias stubs even if future Windows versions move them outside WindowsApps. */
function defaultIsRealExecutable(candidate: string): boolean {
if (isWindowsAppExecutionAliasPath(candidate)) {
return false
}
try {
if (!existsSync(candidate)) {
return false
}
const stat = statSync(candidate)
return stat.isFile() && stat.size > 0
} catch {
return false
}
}
function defaultResolveAppExecutionAlias(candidate: string): string | null {
try {
const target = readlinkSync(candidate)
return pathWin32.resolve(pathWin32.dirname(candidate), target)
} catch {
return null
}
}
function readEnv(env: NodeJS.ProcessEnv, names: string[]): string | undefined {
for (const name of names) {
const value = env[name]
if (value) {
return value
}
}
return undefined
}
function getPathExecutableCandidates(env: NodeJS.ProcessEnv, executable: string): string[] {
const pathValue = readEnv(env, ['PATH', 'Path', 'path'])
if (!pathValue) {
return []
}
const candidates: string[] = []
const seen = new Set<string>()
for (const rawPart of pathValue.split(pathWin32.delimiter)) {
const dir = rawPart.trim().replace(/^"|"$/g, '')
// Relative PATH entries enable repo-local shell hijacking; accept only absolute directories.
if (!pathWin32.isAbsolute(dir)) {
continue
}
pushUniqueCandidate(candidates, seen, pathWin32.join(dir, executable))
}
return candidates
}
function pushUniqueCandidate(
candidates: string[],
seen: Set<string>,
candidate: string | undefined
): void {
if (!candidate) {
return
}
const normalized = pathWin32.normalize(candidate)
const key = normalized.toLowerCase()
if (!seen.has(key)) {
seen.add(key)
candidates.push(normalized)
}
}
function getPwshCandidatePaths(env: NodeJS.ProcessEnv): string[] {
const candidates: string[] = []
const seen = new Set<string>()
const programFilesRoots = [
readEnv(env, ['ProgramW6432', 'PROGRAMW6432']),
readEnv(env, ['ProgramFiles', 'PROGRAMFILES']),
readEnv(env, ['ProgramFiles(x86)', 'PROGRAMFILES(X86)'])
]
for (const root of programFilesRoots) {
if (!root) {
continue
}
// Prefer major 7 while probing adjacent 8/6 install directories for compatibility.
for (const major of ['7', '8', '6']) {
pushUniqueCandidate(candidates, seen, pathWin32.join(root, 'PowerShell', major, 'pwsh.exe'))
}
}
const localAppData = readEnv(env, ['LOCALAPPDATA', 'LocalAppData'])
if (localAppData) {
for (const major of ['7', '8', '6']) {
pushUniqueCandidate(
candidates,
seen,
pathWin32.join(localAppData, 'Microsoft', 'PowerShell', major, 'pwsh.exe')
)
}
}
for (const candidate of getPathExecutableCandidates(env, 'pwsh.exe')) {
pushUniqueCandidate(candidates, seen, candidate)
}
return candidates
}
function getWindowsPowerShellPath(env: NodeJS.ProcessEnv): string {
const systemRoot = readEnv(env, ['SystemRoot', 'WINDIR', 'windir']) || 'C:\\Windows'
return pathWin32.join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe')
}
export type { WindowsPowerShellResolveOptions }
export function getWindowsCmdPath(env: NodeJS.ProcessEnv = process.env): string {
const comspec = readEnv(env, ['ComSpec', 'COMSPEC'])
const comspec = readWindowsEnv([env], ['ComSpec', 'COMSPEC'])
if (comspec) {
return comspec
}
const systemRoot = readEnv(env, ['SystemRoot', 'WINDIR', 'windir']) || 'C:\\Windows'
const systemRoot = readWindowsEnv([env], ['SystemRoot', 'WINDIR', 'windir']) || 'C:\\Windows'
return pathWin32.join(systemRoot, 'System32', 'cmd.exe')
}
/** Null when neither a real executable nor safe Store package target resolves; callers try the next shell. */
export function resolveWindowsPowerShellExecutablePath(
family: 'pwsh.exe' | 'powershell.exe',
options: WindowsPowerShellResolveOptions = {}
): string | null {
const platform = options.platform ?? process.platform
if (platform !== 'win32') {
return null
}
const env = options.env ?? process.env
const isRealExecutable = options.isRealExecutable ?? defaultIsRealExecutable
const resolveAppExecutionAlias =
options.resolveAppExecutionAlias ?? defaultResolveAppExecutionAlias
if (family === 'powershell.exe') {
const windowsPowerShell = getWindowsPowerShellPath(env)
return isRealExecutable(windowsPowerShell) ? windowsPowerShell : null
}
for (const candidate of getPwshCandidatePaths(env)) {
if (isWindowsAppExecutionAliasPath(candidate)) {
const target = resolveAppExecutionAlias(candidate)
// ConPTY cannot launch the alias itself; its real package target preserves the requested pwsh.
if (
target &&
pathWin32.isAbsolute(target) &&
pathWin32.basename(target).toLowerCase() === 'pwsh.exe' &&
!isWindowsAppExecutionAliasPath(target) &&
isRealExecutable(target)
) {
return pathWin32.normalize(target)
}
continue
}
if (isRealExecutable(candidate)) {
return candidate
}
}
return null
}
/** Deduplicated requested PowerShell → inbox PowerShell → cmd.exe fallback paths. */
export function resolveWindowsPowerShellSpawnChain(
export async function resolveWindowsPowerShellSpawnChain(
requestedFamily: 'pwsh.exe' | 'powershell.exe',
options: WindowsPowerShellResolveOptions = {}
): string[] {
): Promise<string[]> {
const env = options.env ?? process.env
const chain: string[] = []
const seen = new Set<string>()
const add = (candidate: string | null): void => {
if (candidate) {
pushUniqueCandidate(chain, seen, candidate)
}
}
add(resolveWindowsPowerShellExecutablePath(requestedFamily, options))
// Inbox PowerShell avoids Store alias launch hazards.
add(resolveWindowsPowerShellExecutablePath('powershell.exe', options))
const isWindows = (options.platform ?? process.platform) === 'win32'
const [pwsh, windowsPowerShell] = isWindows
? await Promise.all([
requestedFamily === 'pwsh.exe' ? findWindowsPwsh(options) : null,
// Inbox PowerShell avoids Store alias launch hazards.
findWindowsPowerShell(env)
])
: [null, null]
// cmd.exe remains the last attempt when policy blocks both PowerShell variants.
pushUniqueCandidate(chain, seen, getWindowsCmdPath(env))
const chain = [pwsh, windowsPowerShell, getWindowsCmdPath(env)].filter(
(candidate): candidate is string => Boolean(candidate)
)
const seen = new Set<string>()
return chain
.map((candidate) => pathWin32.normalize(candidate))
.filter((candidate) => {
const key = candidate.toLowerCase()
if (seen.has(key)) {
return false
}
seen.add(key)
return true
})
}
@@ -0,0 +1,139 @@
import { win32 as pathWin32 } from 'node:path'
import {
discoverWindowsPwshInstalls,
isRealWindowsExecutable,
readWindowsEnv,
readWindowsPathValue,
selectWindowsPwsh,
type WindowsPwshInstall
} from './pwsh-discovery'
export type WindowsPowerShellResolveOptions = {
/** The environment the terminal will get. */
env?: NodeJS.ProcessEnv
/** The requested shell setting; a different value searches again. */
settingKey?: string
preferPreview?: boolean
platform?: NodeJS.Platform
}
type PwshSearch = {
/** Null for a warm-up search, which the first request's setting adopts. */
settingKey: string | null
searchedPath: string
installs: Promise<WindowsPwshInstall[]>
settled?: WindowsPwshInstall[]
/** False when a probe missed its deadline: an empty result is then not a confirmed miss. */
complete?: boolean
missLogged?: boolean
}
// Cached per process (main and the terminal daemon each keep one) so tab opens never search the disk.
let pwshSearch: PwshSearch | null = null
let windowsPowerShellFound: string | null = null
function readPath(env: NodeJS.ProcessEnv): string {
return readWindowsPathValue(env)
}
function startPwshSearch(
settingKey: string | null,
options: WindowsPowerShellResolveOptions
): PwshSearch {
const env = options.env ?? process.env
const search: PwshSearch = {
settingKey,
searchedPath: readPath(env),
installs: discoverWindowsPwshInstalls(env).then(
(discovery) => {
search.settled = discovery.installs
search.complete = discovery.complete
return discovery.installs
},
(error: unknown) => {
// A failed search must not pin the result; the next spawn searches again.
if (pwshSearch === search) {
pwshSearch = null
}
console.warn('[pty] PowerShell 7 (pwsh.exe) search failed:', error)
return []
}
)
}
return search
}
/** Searches once per shell setting; a miss is searched again only when the terminal's PATH differs. */
export async function findWindowsPwsh(
options: WindowsPowerShellResolveOptions
): Promise<string | null> {
const settingKey = options.settingKey ?? ''
const requestPath = readPath(options.env ?? process.env)
if (pwshSearch?.settingKey === null) {
pwshSearch.settingKey = settingKey
}
let search = pwshSearch
if (!search || search.settingKey !== settingKey) {
search = startPwshSearch(settingKey, options)
pwshSearch = search
}
let installs = await search.installs
// A daemon started before an install sees a stale PATH; the terminal's PATH may know better.
if (installs.length === 0 && search.searchedPath !== requestPath) {
if (pwshSearch === search || pwshSearch?.settingKey !== settingKey) {
pwshSearch = startPwshSearch(settingKey, options)
}
search = pwshSearch
installs = await search.installs
}
if (installs.length === 0 && search.complete === false) {
// Only an unanswered probe kept this empty; the next terminal searches again.
if (pwshSearch === search) {
pwshSearch = null
}
} else if (installs.length === 0 && !search.missLogged) {
search.missLogged = true
console.warn(
'[pty] PowerShell 7 (pwsh.exe) was not found in Program Files, the per-user install folder, PATH, App Paths, the Store install, or user tool folders'
)
}
return selectWindowsPwsh(installs, options.preferPreview === true)
}
/** Starts the search before the first terminal asks, so that terminal opens no slower. */
export function warmWindowsPwshSearch(): void {
if (process.platform === 'win32' && !pwshSearch) {
pwshSearch = startPwshSearch(null, {})
}
}
export async function findWindowsPowerShell(env: NodeJS.ProcessEnv): Promise<string | null> {
const systemRoot = readWindowsEnv([env], ['SystemRoot', 'WINDIR', 'windir']) || 'C:\\Windows'
const candidate = pathWin32.join(
systemRoot,
'System32',
'WindowsPowerShell',
'v1.0',
'powershell.exe'
)
if (windowsPowerShellFound === candidate) {
return candidate
}
// Only a hit is cached: a missing inbox shell is rare and must not stick.
if (!(await isRealWindowsExecutable(candidate))) {
return null
}
windowsPowerShellFound = candidate
return candidate
}
/** A launch of a cached shell failed: search again on the next spawn. */
export function forgetWindowsPowerShellAfterLaunchFailure(shellPath: string): void {
const failed = pathWin32.normalize(shellPath).toLowerCase()
if (pwshSearch?.settled?.some((install) => install.path.toLowerCase() === failed)) {
pwshSearch = null
}
if (windowsPowerShellFound?.toLowerCase() === failed) {
windowsPowerShellFound = null
}
}
@@ -0,0 +1,275 @@
import { readdir, readlink, stat } from 'node:fs/promises'
import { homedir } from 'node:os'
import { win32 as pathWin32 } from 'node:path'
import { expandWindowsEnvironmentVariables } from '../../../../shared/windows-environment-expansion'
import {
loadWindowsNativeRegistry,
WINDOWS_REG_EXPAND_SZ,
WINDOWS_REG_SZ
} from '../../../windows/windows-native-registry'
export type WindowsPwshInstall = { path: string; preview: boolean }
/** `complete` is false when a probe missed the deadline, so an empty result is not a confirmed miss. */
export type WindowsPwshDiscovery = { installs: WindowsPwshInstall[]; complete: boolean }
type Candidate = { path: string; alias: boolean }
const APP_PATHS_PWSH_KEY = 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\pwsh.exe'
const VERSION_DIRECTORY = /^(\d+)(-preview)?$/i
const STORE_PACKAGE_DIRECTORY = /^Microsoft\.PowerShell(Preview)?_/i
// Local probes finish in milliseconds; this bounds only unreachable network PATH entries.
const PROBE_DEADLINE_MS = 1_000
/** Store alias reparse stubs may launch through execFile PATH lookup but fail ConPTY absolute CreateProcessW (5). */
export function isWindowsAppExecutionAliasPath(candidate: string): boolean {
return /[\\/]Microsoft[\\/]WindowsApps[\\/]/i.test(pathWin32.normalize(candidate))
}
export function isPreviewPwshPath(candidate: string): boolean {
return /[\\/]\d+-preview[\\/]|[\\/]Microsoft\.PowerShellPreview_/i.test(candidate)
}
export function readWindowsEnv(
envs: readonly NodeJS.ProcessEnv[],
names: readonly string[]
): string | undefined {
for (const env of envs) {
for (const name of names) {
const value = env[name]
if (value) {
return value
}
}
}
return undefined
}
export async function isRealWindowsExecutable(candidate: string): Promise<boolean> {
// Zero-byte alias stubs are never launchable even if a future Windows moves them.
if (isWindowsAppExecutionAliasPath(candidate)) {
return false
}
try {
const info = await stat(candidate)
return info.isFile() && info.size > 0
} catch {
return false
}
}
async function resolveAppExecutionAlias(candidate: string): Promise<string | null> {
try {
const target = await readlink(candidate)
return pathWin32.resolve(pathWin32.dirname(candidate), target)
} catch {
return null
}
}
async function listDirectory(directory: string): Promise<string[]> {
try {
return await readdir(directory)
} catch {
return []
}
}
function readAppPathValues(): string[] {
let registry: ReturnType<typeof loadWindowsNativeRegistry>
try {
registry = loadWindowsNativeRegistry()
} catch {
return []
}
const values: string[] = []
for (const root of [registry.HK.LM, registry.HK.CU]) {
try {
const entry = registry.getRegistryKey(root, APP_PATHS_PWSH_KEY)?.['']
if (
entry &&
(entry.type === WINDOWS_REG_SZ || entry.type === WINDOWS_REG_EXPAND_SZ) &&
typeof entry.value === 'string'
) {
values.push(entry.value)
}
} catch {
// An unreadable hive only removes this source.
}
}
return values
}
function toCandidate(path: string): Candidate {
return { path, alias: isWindowsAppExecutionAliasPath(path) }
}
/** `<root>\<version>\pwsh.exe` for every numbered install, highest major first. */
async function versionedInstallCandidates(roots: (string | undefined)[]): Promise<Candidate[]> {
const uniqueRoots = [...new Set(roots.filter((root): root is string => Boolean(root)))]
const listings = await Promise.all(uniqueRoots.map((root) => listDirectory(root)))
const installs = listings.flatMap((names, rootIndex) =>
names.flatMap((name) => {
const match = VERSION_DIRECTORY.exec(name)
return match
? [
{
major: Number(match[1]),
path: pathWin32.join(uniqueRoots[rootIndex], name, 'pwsh.exe')
}
]
: []
})
)
// Stable sort keeps 64-bit roots ahead of 32-bit ones for the same major.
installs.sort((a, b) => b.major - a.major)
return installs.map((install) => toCandidate(install.path))
}
/** Every PATH spelling in key order: a merged env object can hold both `PATH` and `Path`. */
export function readWindowsPathValue(env: NodeJS.ProcessEnv): string {
return Object.keys(env)
.filter((name) => name.toLowerCase() === 'path')
.map((name) => env[name])
.filter((value): value is string => Boolean(value))
.join(pathWin32.delimiter)
}
function pathDirectoryCandidates(envs: readonly NodeJS.ProcessEnv[]): Candidate[] {
const candidates: Candidate[] = []
for (const env of envs) {
for (const rawPart of readWindowsPathValue(env).split(pathWin32.delimiter)) {
const dir = rawPart.trim().replace(/^"|"$/g, '')
// Relative PATH entries enable repo-local shell hijacking; accept only absolute directories.
if (pathWin32.isAbsolute(dir)) {
candidates.push(toCandidate(pathWin32.join(dir, 'pwsh.exe')))
}
}
}
return candidates
}
function appPathCandidates(terminalEnv: NodeJS.ProcessEnv): Candidate[] {
const expansionEnv = { ...process.env, ...terminalEnv }
return readAppPathValues()
.map((value) => expandWindowsEnvironmentVariables(value.trim(), expansionEnv))
.map((value) => value.replace(/^"|"$/g, ''))
.filter((value) => pathWin32.isAbsolute(value))
.map(toCandidate)
}
async function storeCandidates(windowsApps: string | undefined): Promise<Candidate[]> {
if (!windowsApps) {
return []
}
const packageDirs = (await listDirectory(windowsApps)).filter((name) =>
STORE_PACKAGE_DIRECTORY.test(name)
)
return [
pathWin32.join(windowsApps, 'pwsh.exe'),
...packageDirs.flatMap((name) => [
pathWin32.join(windowsApps, name, 'pwsh.exe'),
pathWin32.join(windowsApps, name, 'pwsh-preview.exe')
])
].map(toCandidate)
}
async function probeCandidate(candidate: Candidate): Promise<string | null> {
if (!candidate.alias) {
return (await isRealWindowsExecutable(candidate.path)) ? candidate.path : null
}
const target = await resolveAppExecutionAlias(candidate.path)
// ConPTY cannot launch the alias itself; its real package target preserves the requested pwsh.
if (
target &&
pathWin32.isAbsolute(target) &&
pathWin32.basename(target).toLowerCase() === 'pwsh.exe' &&
!isWindowsAppExecutionAliasPath(target) &&
(await isRealWindowsExecutable(target))
) {
return pathWin32.normalize(target)
}
return null
}
/** Every launchable pwsh.exe, best first. Reads only; never starts pwsh. */
export async function discoverWindowsPwshInstalls(
/** The environment the terminal will get; its PATH outranks this process's possibly stale one. */
terminalEnv: NodeJS.ProcessEnv
): Promise<WindowsPwshDiscovery> {
const envs = [terminalEnv, process.env]
const programFilesRoots = [
['ProgramW6432', 'PROGRAMW6432'],
['ProgramFiles', 'PROGRAMFILES'],
['ProgramFiles(x86)', 'PROGRAMFILES(X86)']
].map((names) => {
const root = readWindowsEnv(envs, names)
return root ? pathWin32.join(root, 'PowerShell') : undefined
})
const localAppData = readWindowsEnv(envs, ['LOCALAPPDATA', 'LocalAppData'])
const home = readWindowsEnv(envs, ['USERPROFILE', 'UserProfile']) ?? homedir()
const [programFiles, perUser, store] = await Promise.all([
versionedInstallCandidates(programFilesRoots),
versionedInstallCandidates([
localAppData ? pathWin32.join(localAppData, 'Microsoft', 'PowerShell') : undefined
]),
storeCandidates(
localAppData ? pathWin32.join(localAppData, 'Microsoft', 'WindowsApps') : undefined
)
])
const ordered = [
...programFiles,
...perUser,
...pathDirectoryCandidates(envs),
...appPathCandidates(terminalEnv),
...store,
toCandidate(pathWin32.join(home, '.dotnet', 'tools', 'pwsh.exe')),
toCandidate(pathWin32.join(home, 'scoop', 'apps', 'pwsh', 'current', 'pwsh.exe'))
]
const seenCandidates = new Set<string>()
const candidates = ordered.filter((candidate) => {
const key = pathWin32.normalize(candidate.path).toLowerCase()
if (seenCandidates.has(key)) {
return false
}
seenCandidates.add(key)
return true
})
// A PATH entry on an unresponsive share can stall stat; stop waiting rather than hold the tab.
let timer: ReturnType<typeof setTimeout> | undefined
const timedOut = Symbol('timed-out')
const deadline = new Promise<typeof timedOut>((resolve) => {
timer = setTimeout(() => resolve(timedOut), PROBE_DEADLINE_MS)
timer.unref?.()
})
const resolved = await Promise.all(
candidates.map((candidate) => Promise.race([probeCandidate(candidate), deadline]))
).finally(() => clearTimeout(timer))
const installs: WindowsPwshInstall[] = []
const seenInstalls = new Set<string>()
resolved.forEach((path, index) => {
if (path === timedOut) {
return
}
const key = path?.toLowerCase()
if (!path || !key || seenInstalls.has(key)) {
return
}
seenInstalls.add(key)
installs.push({
path,
preview: isPreviewPwshPath(path) || isPreviewPwshPath(candidates[index].path)
})
})
return { installs, complete: !resolved.includes(timedOut) }
}
/** Stable unless only a preview exists; the reverse when the setting asks for a preview build. */
export function selectWindowsPwsh(
installs: readonly WindowsPwshInstall[],
preferPreview: boolean
): string | null {
return (
(installs.find((install) => install.preview === preferPreview) ?? installs[0])?.path ?? null
)
}
@@ -0,0 +1,20 @@
/** Per fallback shell; translated in main because the terminal daemon has no UI language. */
export type WindowsPwshNotFoundNotices = {
windowsPowerShell: string
commandPrompt: string
}
export function buildWindowsPwshNotFoundNotices(
translate: (key: string, fallback: string) => string = (_key, fallback) => fallback
): WindowsPwshNotFoundNotices {
return {
windowsPowerShell: translate(
'terminal.windowsPwshNotFound.windowsPowerShell',
'Orca could not find PowerShell 7 (pwsh.exe), so this terminal opened Windows PowerShell instead.'
),
commandPrompt: translate(
'terminal.windowsPwshNotFound.commandPrompt',
'Orca could not find PowerShell 7 (pwsh.exe), so this terminal opened Command Prompt instead.'
)
}
}
@@ -81,14 +81,26 @@ function getPowerShellRestoreCwdCommand(cwd: string): string {
].join('\n')
}
/** Translated text must stay one literal echo: caret-escape cmd operators and drop quotes and line breaks. */
function escapeCmdEchoText(text: string): string {
return text
.replace(/[\r\n]+/g, ' ')
.replace(/"/g, "'")
.replace(/([\^&|<>()%!])/g, '^$1')
}
function getPowerShellEncodedCommand(
cwd: string,
startupCommand?: string
startupCommand?: string,
startupNotice?: string
): {
encodedCommand: string
startupCommandDeliveredInShellArgs?: boolean
} {
const bootstrap = `${getPowerShellOsc133Bootstrap()}${getPowerShellRestoreCwdCommand(cwd)}`
const notice = startupNotice
? `Write-Host ${quoteStartupArg(startupNotice, 'powershell')} -ForegroundColor Yellow\n`
: ''
const bootstrap = `${notice}${getPowerShellOsc133Bootstrap()}${getPowerShellRestoreCwdCommand(cwd)}`
if (!startupCommand || startupCommand.length > STARTUP_COMMAND_TEXT_MAX_CHARS) {
return { encodedCommand: encodePowerShellCommand(bootstrap) }
}
@@ -126,7 +138,9 @@ export function resolveWindowsShellLaunchArgs(
defaultCwd: string,
wslContext?: WindowsShellWslContext,
startupCommand?: string,
codexLaunchPreflightCommand?: string
codexLaunchPreflightCommand?: string,
/** Printed by the shell itself so ConPTY's screen model stays in sync. */
startupNotice?: string
): WindowsShellLaunchArgs {
const shellBasename = pathWin32.basename(shellPath).toLowerCase()
const nativeCwd = msysDrivePathToWindows(cwd)
@@ -135,6 +149,7 @@ export function resolveWindowsShellLaunchArgs(
const shellArgStartupCommand = getCmdShellArgStartupCommand(startupCommand)
const startupCommands = [
CMD_UTF8_SETUP_COMMAND,
...(startupNotice ? [`echo ${escapeCmdEchoText(startupNotice)}`] : []),
...(codexLaunchPreflightCommand ? [CMD_CODEX_LAUNCH_PREFLIGHT] : []),
...(shellArgStartupCommand ? [shellArgStartupCommand] : [])
]
@@ -147,7 +162,7 @@ export function resolveWindowsShellLaunchArgs(
}
if (shellBasename === 'powershell.exe' || shellBasename === 'pwsh.exe') {
const powerShellCommand = getPowerShellEncodedCommand(nativeCwd, startupCommand)
const powerShellCommand = getPowerShellEncodedCommand(nativeCwd, startupCommand, startupNotice)
// Windows status requires OSC 133 prompt/readline hooks after profiles; encoded delivery follows powershell-osc133-bootstrap.ts MDE review.
return {
shellArgs: ['-NoLogo', '-NoExit', '-EncodedCommand', powerShellCommand.encodedCommand],
@@ -1,10 +1,12 @@
import { win32 as pathWin32 } from 'node:path'
import { resolveWindowsShellLaunchArgs } from './shell-args'
import type { WindowsShellWslContext } from './shell-args'
import { resolveWindowsPowerShellSpawnChain } from './powershell-executable'
import { isPreviewPwshPath } from './pwsh-discovery'
import {
resolveWindowsPowerShellSpawnChain,
type WindowsPowerShellResolveOptions
} from './powershell-executable'
buildWindowsPwshNotFoundNotices,
type WindowsPwshNotFoundNotices
} from './pwsh-not-found-notice'
export type WindowsShellSpawnAttempt = {
shellPath: string
@@ -19,14 +21,17 @@ function toAttempt(
cwd: string,
defaultCwd: string,
wslContext: WindowsShellWslContext | undefined,
startupCommand: string | undefined
startupCommand: string | undefined,
startupNotice?: string
): WindowsShellSpawnAttempt {
const resolved = resolveWindowsShellLaunchArgs(
shellPath,
cwd,
defaultCwd,
wslContext,
startupCommand
startupCommand,
undefined,
startupNotice
)
return {
shellPath,
@@ -37,22 +42,56 @@ function toAttempt(
}
}
function pwshNotFoundNotice(
fallbackShellPath: string,
notices: WindowsPwshNotFoundNotices
): string {
const notice =
pathWin32.basename(fallbackShellPath).toLowerCase() === 'powershell.exe'
? notices.windowsPowerShell
: notices.commandPrompt
return `[${notice}]`
}
/** Requested PowerShell → inbox PowerShell → cmd.exe, with absolute ConPTY-safe paths and per-shell argv.
* Store alias stubs fail CreateProcessW (5); non-PowerShell requests retain their single-shell path. */
export function buildWindowsPowerShellSpawnAttempts(args: {
export async function buildWindowsPowerShellSpawnAttempts(args: {
shellPath: string
cwd: string
defaultCwd: string
wslContext?: WindowsShellWslContext
startupCommand?: string
resolveOptions?: WindowsPowerShellResolveOptions
}): WindowsShellSpawnAttempt[] {
/** The environment the terminal will get, so PATH lookups see what the shell will see. */
env: NodeJS.ProcessEnv
/** The shell setting as requested (name or path) and the PowerShell version setting. */
requestedShell: string
powerShellImplementation: string | undefined
/** Translated by main; English when an older client did not send them. */
pwshNotFoundNotices?: WindowsPwshNotFoundNotices
}): Promise<WindowsShellSpawnAttempt[]> {
const basename = pathWin32.basename(args.shellPath).toLowerCase()
if (basename !== 'pwsh.exe' && basename !== 'powershell.exe') {
return []
}
const chain = resolveWindowsPowerShellSpawnChain(basename, args.resolveOptions)
const chain = await resolveWindowsPowerShellSpawnChain(basename, {
env: args.env,
settingKey: `${args.requestedShell}|${args.powerShellImplementation ?? ''}`,
preferPreview: isPreviewPwshPath(args.requestedShell)
})
// A missing PowerShell 7 is announced by the fallback shell itself.
const pwshMissing =
basename === 'pwsh.exe' && pathWin32.basename(chain[0] ?? '').toLowerCase() !== 'pwsh.exe'
const notices = pwshMissing
? (args.pwshNotFoundNotices ?? buildWindowsPwshNotFoundNotices())
: undefined
return chain.map((candidate) =>
toAttempt(candidate, args.cwd, args.defaultCwd, args.wslContext, args.startupCommand)
toAttempt(
candidate,
args.cwd,
args.defaultCwd,
args.wslContext,
args.startupCommand,
notices ? pwshNotFoundNotice(candidate, notices) : undefined
)
)
}
@@ -22,7 +22,10 @@ type WindowsShellPathHydrationOptions = {
hydrate?: () => Promise<HydrationResult>
merge?: (segments: string[]) => string[]
resolveGitBashPath?: (shell: string) => string | null
resolvePowerShellChain?: (family: 'powershell.exe' | 'pwsh.exe') => string[]
resolvePowerShellChain?: (
family: 'powershell.exe' | 'pwsh.exe',
settingKey: string
) => string[] | Promise<string[]>
warn?: (error: unknown) => void
}
@@ -32,7 +35,9 @@ export function createWindowsShellPathHydration(options: WindowsShellPathHydrati
const merge = options.merge ?? mergePathSegments
const resolveGitBashPath = options.resolveGitBashPath ?? resolveWindowsGitBashShellPath
const resolvePowerShellChain =
options.resolvePowerShellChain ?? resolveWindowsPowerShellSpawnChain
options.resolvePowerShellChain ??
((family: 'powershell.exe' | 'pwsh.exe', settingKey: string) =>
resolveWindowsPowerShellSpawnChain(family, { settingKey }))
const warn =
options.warn ??
((error: unknown) => {
@@ -41,10 +46,12 @@ export function createWindowsShellPathHydration(options: WindowsShellPathHydrati
let generation = 0
let ready = Promise.resolve()
const setConfiguredShell = (
// PowerShell paths come from an async cached search; a newer request wins over a slower one.
const setConfiguredShell = async (
shell: string | null | undefined,
implementation?: WindowsPowerShellImplementation
): void => {
implementation: WindowsPowerShellImplementation | undefined,
requestGeneration: number
): Promise<void> => {
const requestedShell = shell?.trim() || 'powershell.exe'
const basename = pathWin32.basename(requestedShell).toLowerCase()
const shellFamily: WindowsPowerShellShellFamily =
@@ -58,7 +65,13 @@ export function createWindowsShellPathHydration(options: WindowsShellPathHydrati
configure(requestedShell, resolveGitBashPath(requestedShell), null)
return
}
const spawnChain = resolvePowerShellChain(effectivePowerShell)
const spawnChain = await resolvePowerShellChain(
effectivePowerShell,
`${requestedShell}|${implementation ?? ''}`
)
if (requestGeneration !== generation) {
return
}
const profileShell = spawnChain[0] ?? 'cmd.exe'
const fallbackShell = spawnChain.slice(1).find((candidate) => {
const candidateBasename = pathWin32.basename(candidate).toLowerCase()
@@ -70,13 +83,15 @@ export function createWindowsShellPathHydration(options: WindowsShellPathHydrati
return {
configure: (shell, implementation?: WindowsPowerShellImplementation) => {
generation += 1
setConfiguredShell(shell, implementation)
const configured = setConfiguredShell(shell, implementation, generation)
ready = ready.then(() => configured)
},
hydrate: (shell, implementation?: WindowsPowerShellImplementation) => {
generation += 1
const requestGeneration = generation
setConfiguredShell(shell, implementation)
const configured = setConfiguredShell(shell, implementation, requestGeneration)
ready = ready.then(async () => {
await configured
if (requestGeneration !== generation) {
return
}
+5 -1
View File
@@ -21820,7 +21820,11 @@
"count_other": "{{count}} laufende Terminals"
},
"restartedInputNotSent": "Das Terminal wurde während der Eingabe neu gestartet. Daher hat es möglicherweise einen Teil der Eingabe nicht erhalten. Die neue Shell prüfen, bevor die Eingabe wiederholt wird.",
"typingBeforeStartNotSent": "Einige Tasteneingaben während des Terminalstarts wurden nicht gesendet."
"typingBeforeStartNotSent": "Einige Tasteneingaben während des Terminalstarts wurden nicht gesendet.",
"windowsPwshNotFound": {
"windowsPowerShell": "Orca konnte PowerShell 7 (pwsh.exe) nicht finden, daher wurde in diesem Terminal stattdessen Windows PowerShell geöffnet.",
"commandPrompt": "Orca konnte PowerShell 7 (pwsh.exe) nicht finden, daher wurde in diesem Terminal stattdessen die Eingabeaufforderung geöffnet."
}
},
"fileExplorer": {
"root": {
+4
View File
@@ -23324,6 +23324,10 @@
"decline": "No thanks",
"failed": "Could not turn on Claude Code theme sync",
"failedDescription": "Turn it on in Settings."
},
"windowsPwshNotFound": {
"windowsPowerShell": "Orca could not find PowerShell 7 (pwsh.exe), so this terminal opened Windows PowerShell instead.",
"commandPrompt": "Orca could not find PowerShell 7 (pwsh.exe), so this terminal opened Command Prompt instead."
}
},
"fileExplorer": {
+4
View File
@@ -22026,6 +22026,10 @@
"gotIt": "Entendido",
"body": "Para ocultarlo, escriba /diff en Claude Code. Claude recuerda esa elección para sesiones posteriores.",
"title": "Claude Code puede abrir su propio panel de diferencias al lado del chat en terminales anchas. Ese panel es de Claude, no de Orca."
},
"windowsPwshNotFound": {
"windowsPowerShell": "Orca no encontró PowerShell 7 (pwsh.exe), así que este terminal abrió Windows PowerShell en su lugar.",
"commandPrompt": "Orca no encontró PowerShell 7 (pwsh.exe), así que este terminal abrió el Símbolo del sistema en su lugar."
}
},
"quickOpen": {
+4
View File
@@ -22405,6 +22405,10 @@
"gotIt": "J'ai compris",
"title": "Claude Code peut ouvrir son propre panneau de différences à côté du chat dans les terminaux larges. Ce panneau appartient à Claude, pas à Orca.",
"body": "Pour le masquer, tapez /diff dans Claude Code. Claude se souvient de ce choix pour les sessions ultérieures."
},
"windowsPwshNotFound": {
"windowsPowerShell": "Orca n'a pas trouvé PowerShell 7 (pwsh.exe), ce terminal a donc ouvert Windows PowerShell à la place.",
"commandPrompt": "Orca n'a pas trouvé PowerShell 7 (pwsh.exe), ce terminal a donc ouvert l'invite de commandes à la place."
}
},
"fileSearch": {
+4
View File
@@ -21726,6 +21726,10 @@
"gotIt": "了解",
"title": "Claude Code は、ワイドターミナルでチャットの横に独自の差分パネルを開くことができます。そのパネルは Claude のものであり、Orca のものではありません。",
"body": "非表示にするには、Claude Code で /diff と入力してください。Claude はこの選択を今後のセッションでも記憶します。"
},
"windowsPwshNotFound": {
"windowsPowerShell": "Orca は PowerShell 7 (pwsh.exe) を見つけられなかったため、このターミナルでは代わりに Windows PowerShell を開きました。",
"commandPrompt": "Orca は PowerShell 7 (pwsh.exe) を見つけられなかったため、このターミナルでは代わりにコマンド プロンプトを開きました。"
}
},
"fileSearch": {
+4
View File
@@ -21718,6 +21718,10 @@
"gotIt": "확인",
"body": "숨기려면 Claude Code에 /diff를 입력하세요. Claude는 이후 세션을 위해 해당 선택을 기억합니다.",
"title": "Claude Code는 넓은 터미널에서 채팅 옆에 자체 diff 패널을 열 수 있습니다. 이 패널은 Orca가 아닌 Claude의 패널입니다."
},
"windowsPwshNotFound": {
"windowsPowerShell": "Orca에서 PowerShell 7(pwsh.exe)을 찾을 수 없어 이 터미널에서 대신 Windows PowerShell을 열었습니다.",
"commandPrompt": "Orca에서 PowerShell 7(pwsh.exe)을 찾을 수 없어 이 터미널에서 대신 명령 프롬프트를 열었습니다."
}
},
"fileSearch": {
+5 -1
View File
@@ -22797,7 +22797,11 @@
"count_one": "{{count}} terminal em execução"
},
"restartedInputNotSent": "O terminal reiniciou enquanto você digitava, então parte da entrada pode não ter chegado a ele. Verifique o novo shell antes de digitar novamente.",
"typingBeforeStartNotSent": "Algumas teclas digitadas enquanto o terminal iniciava não foram enviadas."
"typingBeforeStartNotSent": "Algumas teclas digitadas enquanto o terminal iniciava não foram enviadas.",
"windowsPwshNotFound": {
"windowsPowerShell": "O Orca não encontrou o PowerShell 7 (pwsh.exe), então este terminal abriu o Windows PowerShell no lugar.",
"commandPrompt": "O Orca não encontrou o PowerShell 7 (pwsh.exe), então este terminal abriu o Prompt de Comando no lugar."
}
},
"fileExplorer": {
"root": {
+5 -1
View File
@@ -22521,7 +22521,11 @@
"count_many": "{{count}} работающих терминалов"
},
"restartedInputNotSent": "Терминал перезапустился во время ввода, поэтому часть текста могла не дойти. Проверьте новую оболочку, прежде чем вводить текст снова.",
"typingBeforeStartNotSent": "Часть клавиш, нажатых во время запуска терминала, не была отправлена."
"typingBeforeStartNotSent": "Часть клавиш, нажатых во время запуска терминала, не была отправлена.",
"windowsPwshNotFound": {
"windowsPowerShell": "Orca не удалось найти PowerShell 7 (pwsh.exe), поэтому в этом терминале вместо него открыт Windows PowerShell.",
"commandPrompt": "Orca не удалось найти PowerShell 7 (pwsh.exe), поэтому в этом терминале вместо него открыта командная строка."
}
},
"fileExplorer": {
"root": {
+5 -1
View File
@@ -21790,7 +21790,11 @@
"count_other": "Çalışan {{count}} terminal"
},
"restartedInputNotSent": "Siz yazarken terminal yeniden başlatıldı, bu nedenle girdilerinizin bir kısmı terminale ulaşmamış olabilir. Yeniden yazmadan önce yeni kabuğu kontrol edin.",
"typingBeforeStartNotSent": "Terminal başlatılırken yazılan bazı tuşlar gönderilmedi."
"typingBeforeStartNotSent": "Terminal başlatılırken yazılan bazı tuşlar gönderilmedi.",
"windowsPwshNotFound": {
"windowsPowerShell": "Orca, PowerShell 7'yi (pwsh.exe) bulamadı, bu nedenle bu terminal bunun yerine Windows PowerShell'i açtı.",
"commandPrompt": "Orca, PowerShell 7'yi (pwsh.exe) bulamadı, bu nedenle bu terminal bunun yerine Komut İstemi'ni açtı."
}
},
"fileExplorer": {
"root": {
+5 -1
View File
@@ -21496,7 +21496,11 @@
"count_other": "{{count}} 個正在執行的終端機"
},
"restartedInputNotSent": "終端機在您輸入時重新啟動,部分輸入可能未送達。請先檢視新的 shell,再重新輸入。",
"typingBeforeStartNotSent": "終端機啟動期間輸入的部分按鍵未送出。"
"typingBeforeStartNotSent": "終端機啟動期間輸入的部分按鍵未送出。",
"windowsPwshNotFound": {
"windowsPowerShell": "Orca 找不到 PowerShell 7 (pwsh.exe),因此此終端機改為開啟 Windows PowerShell。",
"commandPrompt": "Orca 找不到 PowerShell 7 (pwsh.exe),因此此終端機改為開啟命令提示字元。"
}
},
"fileExplorer": {
"root": {
+4
View File
@@ -22066,6 +22066,10 @@
"gotIt": "知道了",
"title": "Claude Code 在较宽的终端中会在聊天旁打开自己的差异面板,那个面板属于 Claude,而不是 Orca",
"body": "要隐藏它,请在 Claude Code 中输入 /diff,Claude 会在之后的会话中记住这一选择"
},
"windowsPwshNotFound": {
"windowsPowerShell": "Orca 未找到 PowerShell 7 (pwsh.exe),因此此终端改为打开了 Windows PowerShell。",
"commandPrompt": "Orca 未找到 PowerShell 7 (pwsh.exe),因此此终端改为打开了命令提示符。"
}
},
"fileSearch": {