mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
Fix p10k wizard blocking terminal startup
This commit is contained in:
@@ -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<Record<(typeof ORCA_SHELL_WRAPPER_ENV)[number], string>> = {}
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4811,7 +4811,7 @@ describe('registerPtyHandlers', () => {
|
||||
const env = spawnCall[2].env as Record<string, string>
|
||||
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', () => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<typeof vi.fn>
|
||||
@@ -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',
|
||||
|
||||
@@ -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 `~/.<kind>/agent` source dir on the remote disk. The
|
||||
@@ -150,12 +151,17 @@ export class SshPtyProvider implements IPtyProvider {
|
||||
}
|
||||
|
||||
private withRemoteCliBridgeEnv(
|
||||
env: Record<string, string> | undefined
|
||||
): Record<string, string> | undefined {
|
||||
if (!this.remoteCliBridgeEnv) {
|
||||
return env
|
||||
}
|
||||
env: Record<string, string> | undefined,
|
||||
envToDelete?: readonly string[]
|
||||
): Record<string, string> {
|
||||
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) {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
export const POWERLEVEL10K_WIZARD_DISABLE_ENV = 'POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD'
|
||||
|
||||
export function seedPowerlevel10kWizardEnv(
|
||||
env: Record<string, string>,
|
||||
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'
|
||||
}
|
||||
Reference in New Issue
Block a user