From 0f26a1bb5ed414afa50253caff562e648e30a80c Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:22:42 -0700 Subject: [PATCH] Fix p10k wizard blocking terminal startup --- src/main/daemon/pty-subprocess.test.ts | 61 ++++++++++++++++++- src/main/daemon/pty-subprocess.ts | 12 ++++ src/main/ipc/pty.test.ts | 2 +- src/main/providers/local-pty-provider.test.ts | 58 +++++++++++++++++- src/main/providers/local-pty-provider.ts | 12 ++++ src/main/providers/ssh-pty-provider.test.ts | 43 ++++++++++++- src/main/providers/ssh-pty-provider.ts | 18 ++++-- src/main/pty/powerlevel10k-wizard-env.ts | 13 ++++ 8 files changed, 208 insertions(+), 11 deletions(-) create mode 100644 src/main/pty/powerlevel10k-wizard-env.ts diff --git a/src/main/daemon/pty-subprocess.test.ts b/src/main/daemon/pty-subprocess.test.ts index c0ac8d3a7ab..0ee5216428b 100644 --- a/src/main/daemon/pty-subprocess.test.ts +++ b/src/main/daemon/pty-subprocess.test.ts @@ -55,6 +55,7 @@ const ORCA_SHELL_WRAPPER_ENV = [ ] as const const POWERSHELL_OSC133_COMMAND_ARGS = ['-NoLogo', '-NoExit', '-EncodedCommand', expect.any(String)] const ZSH_SHELL_READY_DIR = /shell-ready[\\/]zsh/ +const POWERLEVEL10K_WIZARD_DISABLE_ENV = 'POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD' const itOnMacHost = process.platform === 'darwin' ? it : it.skip function mockPtyProcess(pid = 12345) { @@ -82,6 +83,7 @@ function mockPtyProcess(pid = 12345) { describe('createPtySubprocess', () => { const savedWrapperEnv: Partial> = {} let previousUserDataPath: string | undefined + let previousPowerlevelWizardDisable: string | undefined let userDataPath: string beforeEach(() => { @@ -94,8 +96,10 @@ describe('createPtySubprocess', () => { validateWorkingDirectoryMock.mockClear() isPwshAvailableMock.mockReturnValue(false) previousUserDataPath = process.env.ORCA_USER_DATA_PATH + previousPowerlevelWizardDisable = process.env[POWERLEVEL10K_WIZARD_DISABLE_ENV] userDataPath = mkdtempSync(join(tmpdir(), 'daemon-pty-subprocess-test-')) process.env.ORCA_USER_DATA_PATH = userDataPath + delete process.env[POWERLEVEL10K_WIZARD_DISABLE_ENV] for (const key of ORCA_SHELL_WRAPPER_ENV) { savedWrapperEnv[key] = process.env[key] delete process.env[key] @@ -108,6 +112,11 @@ describe('createPtySubprocess', () => { } else { process.env.ORCA_USER_DATA_PATH = previousUserDataPath } + if (previousPowerlevelWizardDisable === undefined) { + delete process.env[POWERLEVEL10K_WIZARD_DISABLE_ENV] + } else { + process.env[POWERLEVEL10K_WIZARD_DISABLE_ENV] = previousPowerlevelWizardDisable + } rmSync(userDataPath, { recursive: true, force: true }) for (const key of ORCA_SHELL_WRAPPER_ENV) { if (savedWrapperEnv[key] === undefined) { @@ -150,6 +159,29 @@ describe('createPtySubprocess', () => { ) }) + it('suppresses the first-run Powerlevel10k wizard for daemon terminals', () => { + const proc = mockPtyProcess() + spawnMock.mockReturnValue(proc) + const platform = Object.getOwnPropertyDescriptor(process, 'platform') + Object.defineProperty(process, 'platform', { value: 'linux' }) + + try { + createPtySubprocess({ + sessionId: 'test', + cols: 80, + rows: 24, + env: { SHELL: '/bin/bash' } + }) + } finally { + if (platform) { + Object.defineProperty(process, 'platform', platform) + } + } + + const spawnCall = spawnMock.mock.calls.at(-1)! + expect(spawnCall[2].env[POWERLEVEL10K_WIZARD_DISABLE_ENV]).toBe('true') + }) + itOnMacHost('repairs a deleted macOS daemon cwd before spawning node-pty', () => { const proc = mockPtyProcess() spawnMock.mockReturnValue(proc) @@ -1828,12 +1860,39 @@ describe('createPtySubprocess', () => { expect.objectContaining({ env: expect.objectContaining({ ORCA_TERMINAL_HANDLE: 'term_wsl', - WSLENV: 'FOO/u:ORCA_TERMINAL_HANDLE/u' + WSLENV: 'FOO/u:ORCA_TERMINAL_HANDLE/u:POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD' }) }) ) }) + it('does not mark deleted Powerlevel10k wizard env for daemon WSL import', () => { + const proc = mockPtyProcess() + spawnMock.mockReturnValue(proc) + const platform = Object.getOwnPropertyDescriptor(process, 'platform') + + Object.defineProperty(process, 'platform', { value: 'win32' }) + + try { + createPtySubprocess({ + sessionId: 'test', + cols: 80, + rows: 24, + cwd: '\\\\wsl.localhost\\Ubuntu\\home\\jin\\repo', + envToDelete: [POWERLEVEL10K_WIZARD_DISABLE_ENV] + }) + } finally { + if (platform) { + Object.defineProperty(process, 'platform', platform) + } + } + + const spawnCall = spawnMock.mock.calls.at(-1)! + expect(spawnCall[0]).toBe('wsl.exe') + expect(spawnCall[2].env[POWERLEVEL10K_WIZARD_DISABLE_ENV]).toBeUndefined() + expect(spawnCall[2].env.WSLENV ?? '').not.toContain(POWERLEVEL10K_WIZARD_DISABLE_ENV) + }) + it('keeps daemon WSL split panes in their distro when cwd is already POSIX', () => { const proc = mockPtyProcess() spawnMock.mockReturnValue(proc) diff --git a/src/main/daemon/pty-subprocess.ts b/src/main/daemon/pty-subprocess.ts index 0e6e817dc33..d7ccda8b459 100644 --- a/src/main/daemon/pty-subprocess.ts +++ b/src/main/daemon/pty-subprocess.ts @@ -25,6 +25,10 @@ import { parseWslPath } from '../wsl' import { addWslEnvKeys } from '../wsl-env' import { getWslContextFromSessionId } from './wsl-session-context' import { addOrcaWslInteropEnv } from '../pty/wsl-orca-env' +import { + POWERLEVEL10K_WIZARD_DISABLE_ENV, + seedPowerlevel10kWizardEnv +} from '../pty/powerlevel10k-wizard-env' import { isWindowsGitBashShellPath, resolveWindowsGitBashShellPath } from '../git-bash' import { WINDOWS_GIT_BASH_SHELL } from '../../shared/windows-terminal-shell' import { resolveAgentForegroundProcess } from '../providers/agent-foreground-process' @@ -605,6 +609,14 @@ export function createPtySubprocess(opts: PtySubprocessOptions): SubprocessHandl } shellArgs = shellLaunch?.args ?? ['-l'] } + seedPowerlevel10kWizardEnv(env, { envToDelete: opts.envToDelete }) + if ( + env[POWERLEVEL10K_WIZARD_DISABLE_ENV] !== undefined && + process.platform === 'win32' && + pathWin32.basename(shellPath).toLowerCase() === 'wsl.exe' + ) { + addWslEnvKeys(env, [POWERLEVEL10K_WIZARD_DISABLE_ENV]) + } promoteAgentTeamsShimPath(env, opts.env?.PATH) // Why: asar packaging can strip the +x bit from node-pty's spawn-helper diff --git a/src/main/ipc/pty.test.ts b/src/main/ipc/pty.test.ts index 856ecd2d81a..57f0931fc99 100644 --- a/src/main/ipc/pty.test.ts +++ b/src/main/ipc/pty.test.ts @@ -4811,7 +4811,7 @@ describe('registerPtyHandlers', () => { const env = spawnCall[2].env as Record expect(spawnCall[0]).toBe('wsl.exe') expect(env.ORCA_TERMINAL_HANDLE).toBe('term_wsl') - expect(env.WSLENV).toBe('ORCA_TERMINAL_HANDLE/u') + expect(env.WSLENV).toBe('ORCA_TERMINAL_HANDLE/u:POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD') }) describe('Windows UTF-8 code page', () => { diff --git a/src/main/providers/local-pty-provider.test.ts b/src/main/providers/local-pty-provider.test.ts index 54a7838c87a..06f05fb3386 100644 --- a/src/main/providers/local-pty-provider.test.ts +++ b/src/main/providers/local-pty-provider.test.ts @@ -61,6 +61,7 @@ vi.mock('../wsl', () => ({ })) import { LocalPtyProvider } from './local-pty-provider' +import { POWERLEVEL10K_WIZARD_DISABLE_ENV } from '../pty/powerlevel10k-wizard-env' describe('LocalPtyProvider', () => { let provider: LocalPtyProvider @@ -75,13 +76,16 @@ describe('LocalPtyProvider', () => { } let exitCb: ((info: { exitCode: number }) => void) | undefined let origShell: string | undefined + let origPowerlevelWizardDisable: string | undefined let origPlatform: PropertyDescriptor | undefined beforeEach(() => { origPlatform = Object.getOwnPropertyDescriptor(process, 'platform') Object.defineProperty(process, 'platform', { configurable: true, value: 'linux' }) origShell = process.env.SHELL + origPowerlevelWizardDisable = process.env.POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD process.env.SHELL = '/bin/zsh' + delete process.env.POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD existsSyncMock.mockReturnValue(true) statSyncMock.mockReturnValue({ isDirectory: () => true, mode: 0o755 }) @@ -128,6 +132,11 @@ describe('LocalPtyProvider', () => { } else { process.env.SHELL = origShell } + if (origPowerlevelWizardDisable === undefined) { + delete process.env.POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD + } else { + process.env.POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD = origPowerlevelWizardDisable + } }) describe('spawn', () => { @@ -198,6 +207,35 @@ describe('LocalPtyProvider', () => { expect(spawnCall[2].env.CUSTOM_VAR).toBe('custom-value') }) + it('suppresses the first-run Powerlevel10k wizard for spawned terminals', async () => { + await provider.spawn({ cols: 80, rows: 24 }) + + const spawnCall = spawnMock.mock.calls.at(-1)! + expect(spawnCall[2].env.POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD).toBe('true') + }) + + it('preserves an explicit Powerlevel10k wizard env value', async () => { + await provider.spawn({ + cols: 80, + rows: 24, + env: { POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD: 'already-set' } + }) + + const spawnCall = spawnMock.mock.calls.at(-1)! + expect(spawnCall[2].env.POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD).toBe('already-set') + }) + + it('honors requests to delete the Powerlevel10k wizard env value', async () => { + await provider.spawn({ + cols: 80, + rows: 24, + envToDelete: ['POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD'] + }) + + const spawnCall = spawnMock.mock.calls.at(-1)! + expect(spawnCall[2].env.POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD).toBeUndefined() + }) + it('uses fallback shell readiness when startup-command shell spawn falls back', async () => { vi.useFakeTimers() try { @@ -506,7 +544,25 @@ describe('LocalPtyProvider', () => { const spawnCall = spawnMock.mock.calls.at(-1)! expect(spawnCall[0]).toBe('wsl.exe') expect(spawnCall[2].env.ORCA_TERMINAL_HANDLE).toBe('term_wsl') - expect(spawnCall[2].env.WSLENV).toBe('ORCA_TERMINAL_HANDLE/u') + expect(spawnCall[2].env.WSLENV).toBe( + 'ORCA_TERMINAL_HANDLE/u:POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD' + ) + }) + + it('does not mark deleted Powerlevel10k wizard env for WSL import', async () => { + Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) + + await provider.spawn({ + cols: 80, + rows: 24, + cwd: '\\\\wsl.localhost\\Ubuntu\\home\\jin\\repo', + envToDelete: [POWERLEVEL10K_WIZARD_DISABLE_ENV] + }) + + const spawnCall = spawnMock.mock.calls.at(-1)! + expect(spawnCall[0]).toBe('wsl.exe') + expect(spawnCall[2].env[POWERLEVEL10K_WIZARD_DISABLE_ENV]).toBeUndefined() + expect(spawnCall[2].env.WSLENV ?? '').not.toContain(POWERLEVEL10K_WIZARD_DISABLE_ENV) }) it('does not inherit parent Orca pane identity when caller omits pane env', async () => { diff --git a/src/main/providers/local-pty-provider.ts b/src/main/providers/local-pty-provider.ts index 5b61c20e5f3..7898285c2ac 100644 --- a/src/main/providers/local-pty-provider.ts +++ b/src/main/providers/local-pty-provider.ts @@ -35,6 +35,10 @@ import type { ShellReadySignal } from './local-pty-shell-ready' import { removeInheritedNoColor } from '../pty/terminal-color-env' import { isHostCodexHomeForWsl, isWslCodexHomeForHost } from '../pty/codex-home-wsl-env' import { addWslEnvKeys } from '../wsl-env' +import { + POWERLEVEL10K_WIZARD_DISABLE_ENV, + seedPowerlevel10kWizardEnv +} from '../pty/powerlevel10k-wizard-env' import { isWindowsGitBashShellPath, resolveGitBashPath, @@ -533,6 +537,14 @@ export class LocalPtyProvider implements IPtyProvider { delete finalEnv.ORCA_CODEX_HOME } } + seedPowerlevel10kWizardEnv(finalEnv, { envToDelete: args.envToDelete }) + if ( + finalEnv[POWERLEVEL10K_WIZARD_DISABLE_ENV] !== undefined && + process.platform === 'win32' && + pathWin32.basename(shellPath).toLowerCase() === 'wsl.exe' + ) { + addWslEnvKeys(finalEnv, [POWERLEVEL10K_WIZARD_DISABLE_ENV]) + } if (!wslInfo && process.platform !== 'win32') { // Why: OpenCode/Codex path restoration and OMP's typed-command status // wrapper need shell-ready code after user startup files run. diff --git a/src/main/providers/ssh-pty-provider.test.ts b/src/main/providers/ssh-pty-provider.test.ts index 9de9aa20b17..5adfdecf23b 100644 --- a/src/main/providers/ssh-pty-provider.test.ts +++ b/src/main/providers/ssh-pty-provider.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, vi, beforeEach } from 'vitest' import { SshPtyProvider } from './ssh-pty-provider' +import { POWERLEVEL10K_WIZARD_DISABLE_ENV } from '../pty/powerlevel10k-wizard-env' type MockMultiplexer = { request: ReturnType @@ -43,7 +44,7 @@ describe('SshPtyProvider', () => { cols: 80, rows: 24, cwd: undefined, - env: undefined + env: { [POWERLEVEL10K_WIZARD_DISABLE_ENV]: 'true' } }) expect(result).toEqual({ id: scopedPty1 }) }) @@ -62,7 +63,42 @@ describe('SshPtyProvider', () => { cols: 120, rows: 40, cwd: '/home/user', - env: { FOO: 'bar' } + env: { FOO: 'bar', [POWERLEVEL10K_WIZARD_DISABLE_ENV]: 'true' } + }) + }) + + it('preserves an explicit remote Powerlevel10k wizard env value', async () => { + mux.request.mockResolvedValue({ id: 'pty-2' }) + + await provider.spawn({ + cols: 120, + rows: 40, + env: { [POWERLEVEL10K_WIZARD_DISABLE_ENV]: 'already-set' } + }) + + expect(mux.request).toHaveBeenCalledWith('pty.spawn', { + cols: 120, + rows: 40, + cwd: undefined, + env: { [POWERLEVEL10K_WIZARD_DISABLE_ENV]: 'already-set' } + }) + }) + + it('honors requests to delete the remote Powerlevel10k wizard env value', async () => { + mux.request.mockResolvedValue({ id: 'pty-2' }) + + await provider.spawn({ + cols: 120, + rows: 40, + env: { [POWERLEVEL10K_WIZARD_DISABLE_ENV]: 'already-set' }, + envToDelete: [POWERLEVEL10K_WIZARD_DISABLE_ENV] + }) + + expect(mux.request).toHaveBeenCalledWith('pty.spawn', { + cols: 120, + rows: 40, + cwd: undefined, + env: {} }) }) @@ -88,6 +124,7 @@ describe('SshPtyProvider', () => { env: { PATH: '/home/user/.orca-relay/bin:/usr/bin', ORCA_TERMINAL_HANDLE: 'term_ssh', + [POWERLEVEL10K_WIZARD_DISABLE_ENV]: 'true', ORCA_REMOTE_CLI_BIN_DIR: '/home/user/.orca-relay/bin', ORCA_RELAY_DIR: '/home/user/.orca-relay/relay-v1', ORCA_RELAY_NODE_PATH: '/usr/bin/node', @@ -117,6 +154,7 @@ describe('SshPtyProvider', () => { cwd: undefined, env: { ORCA_TERMINAL_HANDLE: 'term_ssh', + [POWERLEVEL10K_WIZARD_DISABLE_ENV]: 'true', ORCA_REMOTE_CLI_BIN_DIR: '/home/user/.orca-relay/bin', ORCA_RELAY_DIR: '/home/user/.orca-relay/relay-v1', ORCA_RELAY_NODE_PATH: '/usr/bin/node', @@ -147,6 +185,7 @@ describe('SshPtyProvider', () => { cwd: undefined, env: { Path: 'C:/Users/me/.orca-relay/bin;C:/Windows/System32;C:/Tools', + [POWERLEVEL10K_WIZARD_DISABLE_ENV]: 'true', ORCA_REMOTE_CLI_BIN_DIR: 'C:/Users/me/.orca-relay/bin', ORCA_RELAY_DIR: 'C:/Users/me/.orca-remote/relay-v1', ORCA_RELAY_NODE_PATH: 'C:/Program Files/nodejs/node.exe', diff --git a/src/main/providers/ssh-pty-provider.ts b/src/main/providers/ssh-pty-provider.ts index 56c4b94c737..54c2ce5824f 100644 --- a/src/main/providers/ssh-pty-provider.ts +++ b/src/main/providers/ssh-pty-provider.ts @@ -1,6 +1,7 @@ import type { SshChannelMultiplexer } from '../ssh/ssh-channel-multiplexer' import type { IPtyProvider, PtySpawnOptions, PtySpawnResult } from './types' import { toAppSshPtyId, toRelaySshPtyId } from './ssh-pty-id' +import { seedPowerlevel10kWizardEnv } from '../pty/powerlevel10k-wizard-env' type DataCallback = (payload: { id: string; data: string }) => void type ReplayCallback = (payload: { id: string; data: string }) => void @@ -130,7 +131,7 @@ export class SshPtyProvider implements IPtyProvider { cols: opts.cols, rows: opts.rows, cwd: opts.cwd, - env: this.withRemoteCliBridgeEnv(opts.env), + env: this.withRemoteCliBridgeEnv(opts.env, opts.envToDelete), // Why: the relay's plugin-overlay env augmenter needs to know which // Pi-compatible agent is being launched (`pi` vs `omp`) so it mirrors // the right `~/./agent` source dir on the remote disk. The @@ -150,12 +151,17 @@ export class SshPtyProvider implements IPtyProvider { } private withRemoteCliBridgeEnv( - env: Record | undefined - ): Record | undefined { - if (!this.remoteCliBridgeEnv) { - return env - } + env: Record | undefined, + envToDelete?: readonly string[] + ): Record { const merged = { ...env } + for (const key of envToDelete ?? []) { + delete merged[key] + } + seedPowerlevel10kWizardEnv(merged, { envToDelete }) + if (!this.remoteCliBridgeEnv) { + return merged + } const pathDelimiter = this.remoteCliBridgeEnv.pathDelimiter ?? ':' const pathKey = merged.PATH !== undefined ? 'PATH' : merged.Path !== undefined ? 'Path' : null if (pathKey) { diff --git a/src/main/pty/powerlevel10k-wizard-env.ts b/src/main/pty/powerlevel10k-wizard-env.ts new file mode 100644 index 00000000000..baf47dfacc6 --- /dev/null +++ b/src/main/pty/powerlevel10k-wizard-env.ts @@ -0,0 +1,13 @@ +export const POWERLEVEL10K_WIZARD_DISABLE_ENV = 'POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD' + +export function seedPowerlevel10kWizardEnv( + env: Record, + options: { envToDelete?: readonly string[] } = {} +): void { + if (options.envToDelete?.includes(POWERLEVEL10K_WIZARD_DISABLE_ENV)) { + return + } + // Why: p10k's first-run wizard blocks shell startup and queued commands. + // Users can still run `p10k configure` manually inside an Orca terminal. + env[POWERLEVEL10K_WIZARD_DISABLE_ENV] ??= 'true' +}