Use managed Pi and OMP extensions (#5681)
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
3bd7b36012
commit
84a5def248
|
|
@ -15,8 +15,8 @@ export { parsePtySessionId } from '../../shared/pty-session-id-format'
|
|||
*
|
||||
* Both pty.ts (host-daemon spawn path) and DaemonPtyAdapter.doSpawn
|
||||
* (fallback when opts.sessionId is absent) must use this helper — a
|
||||
* drifted format would break cold-restore mapping and Pi overlay
|
||||
* keying.
|
||||
* drifted format would break cold-restore mapping and legacy Pi overlay
|
||||
* cleanup keying.
|
||||
*/
|
||||
export function mintPtySessionId(worktreeId?: string): string {
|
||||
return worktreeId
|
||||
|
|
@ -25,18 +25,17 @@ export function mintPtySessionId(worktreeId?: string): string {
|
|||
}
|
||||
|
||||
/**
|
||||
* Why: `effectiveSessionId` is used as a filesystem key (Pi overlay
|
||||
* directory under app.getPath('userData')). The security property we
|
||||
* want is containment: the derived overlay path must be strictly
|
||||
* inside the userData root so a crafted IPC payload (args.sessionId
|
||||
* or args.worktreeId forwarded from the renderer) cannot make us
|
||||
* write overlay files outside userData.
|
||||
* Why: `effectiveSessionId` is used as a filesystem key for provider hook
|
||||
* state and legacy Pi overlay cleanup under app.getPath('userData'). The
|
||||
* security property we want is containment: derived paths must be strictly
|
||||
* inside the userData root so a crafted IPC payload (args.sessionId or
|
||||
* args.worktreeId forwarded from the renderer) cannot make us write outside
|
||||
* userData.
|
||||
*
|
||||
* Callers pass `app.getPath('userData')` as `userDataPath`. Any
|
||||
* subpath inside userData is acceptable as a filesystem key since
|
||||
* the Pi overlay path lives deeper inside userData — enforcing
|
||||
* "id cannot escape userData" is a superset of "id cannot escape Pi
|
||||
* overlay root".
|
||||
* subpath inside userData is acceptable as a filesystem key since callers use
|
||||
* deeper roots under userData — enforcing "id cannot escape userData" is a
|
||||
* superset of the per-feature containment checks.
|
||||
*
|
||||
* Note: real worktreeIds are `${repo.id}::${absolutePath}` so minted
|
||||
* session ids contain `/` on POSIX and `\` on Windows. Rejecting
|
||||
|
|
|
|||
|
|
@ -864,7 +864,7 @@ describe('createPtySubprocess', () => {
|
|||
expect(lastCall[2].env.ORCA_SHELL_READY_MARKER).toBe('0')
|
||||
})
|
||||
|
||||
it('uses shell wrapper when Pi config must survive shell startup', () => {
|
||||
it('uses shell wrapper when typed OMP commands need the status extension', () => {
|
||||
const proc = mockPtyProcess()
|
||||
spawnMock.mockReturnValue(proc)
|
||||
const platform = Object.getOwnPropertyDescriptor(process, 'platform')
|
||||
|
|
@ -877,8 +877,7 @@ describe('createPtySubprocess', () => {
|
|||
rows: 24,
|
||||
env: {
|
||||
SHELL: '/bin/zsh',
|
||||
PI_CODING_AGENT_DIR: '/tmp/orca-pi-agent-overlay',
|
||||
ORCA_PI_CODING_AGENT_DIR: '/tmp/orca-pi-agent-overlay'
|
||||
ORCA_OMP_STATUS_EXTENSION: '/tmp/.omp/agent/extensions/orca-agent-status.ts'
|
||||
}
|
||||
})
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -500,8 +500,8 @@ export function createPtySubprocess(opts: PtySubprocessOptions): SubprocessHandl
|
|||
if (opts.env?.TERM) {
|
||||
env.TERM = opts.env.TERM
|
||||
}
|
||||
// Why: any Orca-injected overlay env that user rc files can clobber
|
||||
// needs the wrapper so the post-rc restore line runs.
|
||||
// Why: OpenCode/Codex path restoration and OMP's typed-command status
|
||||
// wrapper need shell-ready code after user startup files run.
|
||||
let shellLaunch: ReturnType<typeof getShellReadyLaunchConfig> | null = null
|
||||
if (opts.command && isCodexStartupCommand) {
|
||||
// Why: Codex needs the env-restoring wrapper, but waiting for a shell
|
||||
|
|
@ -513,8 +513,7 @@ export function createPtySubprocess(opts: PtySubprocessOptions): SubprocessHandl
|
|||
shellLaunch =
|
||||
env.ORCA_ATTRIBUTION_SHIM_DIR ||
|
||||
env.ORCA_OPENCODE_CONFIG_DIR ||
|
||||
env.ORCA_PI_CODING_AGENT_DIR ||
|
||||
env.ORCA_OMP_CODING_AGENT_DIR ||
|
||||
env.ORCA_OMP_STATUS_EXTENSION ||
|
||||
env.ORCA_CODEX_HOME ||
|
||||
env.ORCA_AGENT_TEAMS_SHIM_DIR
|
||||
? getAttributionShellLaunchConfig(shellPath)
|
||||
|
|
|
|||
|
|
@ -461,7 +461,7 @@ describePosix('daemon shell-ready launch config', () => {
|
|||
15_000
|
||||
)
|
||||
|
||||
it('writes wrappers that restore OpenCode and Pi config after user startup files', async () => {
|
||||
it('writes wrappers without restoring Pi/OMP homes after user startup files', async () => {
|
||||
const { getShellReadyLaunchConfig } = await importFreshShellReady()
|
||||
|
||||
getShellReadyLaunchConfig('/bin/zsh')
|
||||
|
|
@ -472,31 +472,25 @@ describePosix('daemon shell-ready launch config', () => {
|
|||
const bashRc = readFileSync(join(userDataPath, 'shell-ready', 'bash', 'rcfile'), 'utf8')
|
||||
const restoreLine =
|
||||
'[[ -n "${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="${ORCA_OPENCODE_CONFIG_DIR}"'
|
||||
const piRestoreLine =
|
||||
'[[ -n "${ORCA_PI_CODING_AGENT_DIR:-}" ]] && export PI_CODING_AGENT_DIR="${ORCA_PI_CODING_AGENT_DIR}"'
|
||||
const codexRestoreLine =
|
||||
'[[ -n "${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="${ORCA_CODEX_HOME}"'
|
||||
const agentTeamsPathRestoreLine = '[[ -n "${ORCA_AGENT_TEAMS_SHIM_DIR:-}" ]] || return 0'
|
||||
const ompRestoreLine =
|
||||
'if [[ -z "${ORCA_PI_CODING_AGENT_DIR:-}" && -n "${ORCA_OMP_CODING_AGENT_DIR:-}" ]]; then'
|
||||
const ompWrapperLine = 'command omp --extension "${ORCA_OMP_STATUS_EXTENSION}" "$@"'
|
||||
expect(zshrc).toContain(restoreLine)
|
||||
expect(zlogin).toContain(restoreLine)
|
||||
expect(bashRc).toContain(restoreLine)
|
||||
expect(zshrc).toContain(piRestoreLine)
|
||||
expect(zlogin).toContain(piRestoreLine)
|
||||
expect(bashRc).toContain(piRestoreLine)
|
||||
expect(zshrc).not.toContain('ORCA_PI_CODING_AGENT_DIR')
|
||||
expect(zlogin).not.toContain('ORCA_PI_CODING_AGENT_DIR')
|
||||
expect(bashRc).not.toContain('ORCA_PI_CODING_AGENT_DIR')
|
||||
expect(zshrc).toContain(codexRestoreLine)
|
||||
expect(zlogin).toContain(codexRestoreLine)
|
||||
expect(zshrc).toContain(agentTeamsPathRestoreLine)
|
||||
expect(zlogin).toContain(agentTeamsPathRestoreLine)
|
||||
expect(bashRc).toContain(agentTeamsPathRestoreLine)
|
||||
expect(bashRc).toContain(codexRestoreLine)
|
||||
// OMP launches use ORCA_OMP_CODING_AGENT_DIR; both restore lines must be
|
||||
// present so a PTY of either kind has its overlay restored after rc files.
|
||||
expect(zshrc).toContain(ompRestoreLine)
|
||||
expect(zlogin).toContain(ompRestoreLine)
|
||||
expect(bashRc).toContain(ompRestoreLine)
|
||||
expect(zshrc).not.toContain('ORCA_OMP_CODING_AGENT_DIR')
|
||||
expect(zlogin).not.toContain('ORCA_OMP_CODING_AGENT_DIR')
|
||||
expect(bashRc).not.toContain('ORCA_OMP_CODING_AGENT_DIR')
|
||||
expect(zshrc).toContain(ompWrapperLine)
|
||||
expect(zlogin).toContain(ompWrapperLine)
|
||||
expect(bashRc).toContain(ompWrapperLine)
|
||||
|
|
|
|||
|
|
@ -114,12 +114,6 @@ __orca_restore_agent_teams_path
|
|||
# Why: user startup files may set the default OpenCode config after Orca's
|
||||
# spawn env; restore the Orca-managed config dir before the first prompt.
|
||||
[[ -n "\${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="\${ORCA_OPENCODE_CONFIG_DIR}"
|
||||
# Why: bare shells carry both Pi and OMP shadows so a later typed OMP can
|
||||
# switch on demand. Keep Pi as the shell default unless this PTY is OMP-only.
|
||||
[[ -n "\${ORCA_PI_CODING_AGENT_DIR:-}" ]] && export PI_CODING_AGENT_DIR="\${ORCA_PI_CODING_AGENT_DIR}"
|
||||
if [[ -z "\${ORCA_PI_CODING_AGENT_DIR:-}" && -n "\${ORCA_OMP_CODING_AGENT_DIR:-}" ]]; then
|
||||
export PI_CODING_AGENT_DIR="\${ORCA_OMP_CODING_AGENT_DIR}"
|
||||
fi
|
||||
${getPosixOmpShellWrapper()}
|
||||
# Why: Codex must keep using Orca's runtime CODEX_HOME after profile scripts.
|
||||
[[ -n "\${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="\${ORCA_CODEX_HOME}"
|
||||
|
|
@ -232,12 +226,6 @@ __orca_restore_agent_teams_path() {
|
|||
if [[ ! -o login ]]; then
|
||||
# Why: ~/.zshrc can export the user's default OpenCode config after spawn.
|
||||
[[ -n "\${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="\${ORCA_OPENCODE_CONFIG_DIR}"
|
||||
# Why: bare shells carry both Pi and OMP shadows; keep Pi as the default and
|
||||
# let the OMP wrapper switch to OMP only for that command.
|
||||
[[ -n "\${ORCA_PI_CODING_AGENT_DIR:-}" ]] && export PI_CODING_AGENT_DIR="\${ORCA_PI_CODING_AGENT_DIR}"
|
||||
if [[ -z "\${ORCA_PI_CODING_AGENT_DIR:-}" && -n "\${ORCA_OMP_CODING_AGENT_DIR:-}" ]]; then
|
||||
export PI_CODING_AGENT_DIR="\${ORCA_OMP_CODING_AGENT_DIR}"
|
||||
fi
|
||||
${getPosixOmpShellWrapper()}
|
||||
[[ -n "\${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="\${ORCA_CODEX_HOME}"
|
||||
fi
|
||||
|
|
@ -300,10 +288,6 @@ __orca_restore_agent_teams_path() {
|
|||
__orca_restore_agent_teams_path
|
||||
# Why: .zlogin is the final login startup file before the prompt is shown.
|
||||
[[ -n "\${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="\${ORCA_OPENCODE_CONFIG_DIR}"
|
||||
[[ -n "\${ORCA_PI_CODING_AGENT_DIR:-}" ]] && export PI_CODING_AGENT_DIR="\${ORCA_PI_CODING_AGENT_DIR}"
|
||||
if [[ -z "\${ORCA_PI_CODING_AGENT_DIR:-}" && -n "\${ORCA_OMP_CODING_AGENT_DIR:-}" ]]; then
|
||||
export PI_CODING_AGENT_DIR="\${ORCA_OMP_CODING_AGENT_DIR}"
|
||||
fi
|
||||
${getPosixOmpShellWrapper()}
|
||||
[[ -n "\${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="\${ORCA_CODEX_HOME}"
|
||||
${getZshShellReadyMarkerRegistrationBlock(SHELL_READY_MARKER)}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { delimiter, join } from 'node:path'
|
|||
const isWindowsHost = process.platform === 'win32'
|
||||
const posixOnlyIt = isWindowsHost ? it.skip : it
|
||||
const expectedOmpStatusExtension = join(
|
||||
'/tmp/orca-pi-agent-overlay',
|
||||
'/tmp/default-omp-agent',
|
||||
'extensions',
|
||||
'orca-agent-status.ts'
|
||||
)
|
||||
|
|
@ -293,11 +293,15 @@ describe('registerPtyHandlers', () => {
|
|||
ORCA_AGENT_HOOK_TOKEN: 'agent-token'
|
||||
})
|
||||
piBuildPtyEnvMock.mockImplementation(
|
||||
(_ptyId: string, existingAgentDir?: string, _kind?: string) => ({
|
||||
PI_CODING_AGENT_DIR: existingAgentDir
|
||||
? '/tmp/orca-pi-agent-overlay'
|
||||
: '/tmp/orca-pi-agent-overlay'
|
||||
})
|
||||
(_ptyId: string, existingAgentDir?: string, kind?: string) =>
|
||||
kind === 'omp'
|
||||
? {
|
||||
ORCA_OMP_SOURCE_AGENT_DIR: existingAgentDir ?? '/tmp/default-omp-agent',
|
||||
ORCA_OMP_STATUS_EXTENSION: `${existingAgentDir ?? '/tmp/default-omp-agent'}/extensions/orca-agent-status.ts`
|
||||
}
|
||||
: {
|
||||
ORCA_PI_SOURCE_AGENT_DIR: existingAgentDir ?? '/tmp/default-pi-agent'
|
||||
}
|
||||
)
|
||||
isPwshAvailableMock.mockReturnValue(false)
|
||||
spawnMock.mockReturnValue({
|
||||
|
|
@ -518,7 +522,7 @@ describe('registerPtyHandlers', () => {
|
|||
// Why: PR #2662 finding 2 — the threading from IPC `args.command` through
|
||||
// buildPtyHostEnv to piTitlebarExtensionService.buildPtyEnv was untested
|
||||
// for the OMP case because this helper never forwarded a command. Accept
|
||||
// an optional `command` so callers can exercise OMP overlay resolution.
|
||||
// an optional `command` so callers can exercise OMP target resolution.
|
||||
command?: string
|
||||
): Promise<Record<string, string>> {
|
||||
const savedEnv: Record<string, string | undefined> = {}
|
||||
|
|
@ -769,18 +773,20 @@ describe('registerPtyHandlers', () => {
|
|||
}
|
||||
)
|
||||
|
||||
it('injects the Pi agent overlay env into Orca terminal PTYs', async () => {
|
||||
it('installs Pi managed extensions without redirecting Orca terminal PTY homes', async () => {
|
||||
const env = await spawnAndGetEnv(undefined, { PI_CODING_AGENT_DIR: '/tmp/user-pi-agent' })
|
||||
expect(piBuildPtyEnvMock).toHaveBeenCalledWith(expect.any(String), '/tmp/user-pi-agent', 'pi')
|
||||
expect(piBuildPtyEnvMock).toHaveBeenCalledWith(expect.any(String), undefined, 'omp')
|
||||
expect(env.PI_CODING_AGENT_DIR).toBe('/tmp/orca-pi-agent-overlay')
|
||||
expect(env.ORCA_PI_CODING_AGENT_DIR).toBe('/tmp/orca-pi-agent-overlay')
|
||||
expect(env.PI_CODING_AGENT_DIR).toBe('/tmp/user-pi-agent')
|
||||
expect(env.ORCA_PI_CODING_AGENT_DIR).toBeUndefined()
|
||||
expect(env.ORCA_PI_SOURCE_AGENT_DIR).toBe('/tmp/user-pi-agent')
|
||||
expect(env.ORCA_OMP_CODING_AGENT_DIR).toBe('/tmp/orca-pi-agent-overlay')
|
||||
expect(env.ORCA_OMP_STATUS_EXTENSION).toBe(expectedOmpStatusExtension)
|
||||
expect(env.ORCA_OMP_CODING_AGENT_DIR).toBeUndefined()
|
||||
expect(env.ORCA_OMP_STATUS_EXTENSION).toBe(
|
||||
'/tmp/default-omp-agent/extensions/orca-agent-status.ts'
|
||||
)
|
||||
})
|
||||
|
||||
it('threads command: "omp" through to piBuildPtyEnv and emits ORCA_OMP_* shadow vars', async () => {
|
||||
it('threads command: "omp" through to piBuildPtyEnv and emits OMP status metadata', async () => {
|
||||
// Why: OMP launches must emit OMP-named Orca shadow vars (ORCA_OMP_*),
|
||||
// not Pi-named ones. The PI_CODING_AGENT_DIR binary var is unavoidable
|
||||
// (OMP's own binary reads it — see C:\tmp\pr-workspace\oh-my-pi
|
||||
|
|
@ -798,9 +804,11 @@ describe('registerPtyHandlers', () => {
|
|||
'/tmp/user-omp-agent',
|
||||
'omp'
|
||||
)
|
||||
expect(env.PI_CODING_AGENT_DIR).toBe('/tmp/orca-pi-agent-overlay')
|
||||
expect(env.ORCA_OMP_CODING_AGENT_DIR).toBe('/tmp/orca-pi-agent-overlay')
|
||||
expect(env.ORCA_OMP_STATUS_EXTENSION).toBe(expectedOmpStatusExtension)
|
||||
expect(env.PI_CODING_AGENT_DIR).toBe('/tmp/user-omp-agent')
|
||||
expect(env.ORCA_OMP_CODING_AGENT_DIR).toBeUndefined()
|
||||
expect(env.ORCA_OMP_STATUS_EXTENSION).toBe(
|
||||
'/tmp/user-omp-agent/extensions/orca-agent-status.ts'
|
||||
)
|
||||
expect(env.ORCA_OMP_SOURCE_AGENT_DIR).toBe('/tmp/user-omp-agent')
|
||||
// CRITICAL: a Pi-named shadow MUST NOT leak into an OMP PTY env.
|
||||
expect(env.ORCA_PI_CODING_AGENT_DIR).toBeUndefined()
|
||||
|
|
@ -813,8 +821,8 @@ describe('registerPtyHandlers', () => {
|
|||
ORCA_PI_SOURCE_AGENT_DIR: '/tmp/user-pi-agent'
|
||||
})
|
||||
expect(piBuildPtyEnvMock).toHaveBeenCalledWith(expect.any(String), '/tmp/user-pi-agent', 'pi')
|
||||
expect(env.PI_CODING_AGENT_DIR).toBe('/tmp/orca-pi-agent-overlay')
|
||||
expect(env.ORCA_PI_CODING_AGENT_DIR).toBe('/tmp/orca-pi-agent-overlay')
|
||||
expect(env.PI_CODING_AGENT_DIR).toBe('/tmp/parent-orca-pi-overlay')
|
||||
expect(env.ORCA_PI_CODING_AGENT_DIR).toBeUndefined()
|
||||
expect(env.ORCA_PI_SOURCE_AGENT_DIR).toBe('/tmp/user-pi-agent')
|
||||
})
|
||||
|
||||
|
|
@ -832,8 +840,8 @@ describe('registerPtyHandlers', () => {
|
|||
)
|
||||
|
||||
expect(piBuildPtyEnvMock).toHaveBeenCalledWith(expect.any(String), undefined, 'omp')
|
||||
expect(env.ORCA_OMP_CODING_AGENT_DIR).toBe('/tmp/orca-pi-agent-overlay')
|
||||
expect(env.ORCA_OMP_SOURCE_AGENT_DIR).toBeUndefined()
|
||||
expect(env.ORCA_OMP_CODING_AGENT_DIR).toBeUndefined()
|
||||
expect(env.ORCA_OMP_SOURCE_AGENT_DIR).toBe('/tmp/default-omp-agent')
|
||||
expect(env.ORCA_PI_CODING_AGENT_DIR).toBeUndefined()
|
||||
expect(env.ORCA_PI_SOURCE_AGENT_DIR).toBeUndefined()
|
||||
})
|
||||
|
|
@ -852,8 +860,8 @@ describe('registerPtyHandlers', () => {
|
|||
)
|
||||
|
||||
expect(piBuildPtyEnvMock).toHaveBeenCalledWith(expect.any(String), undefined, 'pi')
|
||||
expect(env.ORCA_PI_CODING_AGENT_DIR).toBe('/tmp/orca-pi-agent-overlay')
|
||||
expect(env.ORCA_PI_SOURCE_AGENT_DIR).toBeUndefined()
|
||||
expect(env.ORCA_PI_CODING_AGENT_DIR).toBeUndefined()
|
||||
expect(env.ORCA_PI_SOURCE_AGENT_DIR).toBe('/tmp/default-pi-agent')
|
||||
expect(env.ORCA_OMP_CODING_AGENT_DIR).toBeUndefined()
|
||||
expect(env.ORCA_OMP_SOURCE_AGENT_DIR).toBeUndefined()
|
||||
expect(env.ORCA_OMP_STATUS_EXTENSION).toBeUndefined()
|
||||
|
|
@ -877,26 +885,29 @@ describe('registerPtyHandlers', () => {
|
|||
expect(env.ORCA_PI_SOURCE_AGENT_DIR).toBeUndefined()
|
||||
})
|
||||
|
||||
posixOnlyIt('mirrors Pi config exported only by shell startup files', async () => {
|
||||
readFileSyncMock.mockImplementation((path: string) =>
|
||||
path.endsWith('.zshrc') ? 'export PI_CODING_AGENT_DIR="$HOME/.config/pi-agent"\n' : ''
|
||||
)
|
||||
posixOnlyIt(
|
||||
'uses Pi config exported only by shell startup files as the managed extension target',
|
||||
async () => {
|
||||
readFileSyncMock.mockImplementation((path: string) =>
|
||||
path.endsWith('.zshrc') ? 'export PI_CODING_AGENT_DIR="$HOME/.config/pi-agent"\n' : ''
|
||||
)
|
||||
|
||||
const env = await spawnAndGetEnv(undefined, {
|
||||
HOME: '/home/tester',
|
||||
SHELL: '/bin/zsh',
|
||||
PI_CODING_AGENT_DIR: undefined
|
||||
})
|
||||
const env = await spawnAndGetEnv(undefined, {
|
||||
HOME: '/home/tester',
|
||||
SHELL: '/bin/zsh',
|
||||
PI_CODING_AGENT_DIR: undefined
|
||||
})
|
||||
|
||||
expect(piBuildPtyEnvMock).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
'/home/tester/.config/pi-agent',
|
||||
'pi'
|
||||
)
|
||||
expect(env.PI_CODING_AGENT_DIR).toBe('/tmp/orca-pi-agent-overlay')
|
||||
expect(env.ORCA_PI_CODING_AGENT_DIR).toBe('/tmp/orca-pi-agent-overlay')
|
||||
expect(env.ORCA_PI_SOURCE_AGENT_DIR).toBe('/home/tester/.config/pi-agent')
|
||||
})
|
||||
expect(piBuildPtyEnvMock).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
'/home/tester/.config/pi-agent',
|
||||
'pi'
|
||||
)
|
||||
expect(env.PI_CODING_AGENT_DIR).toBeUndefined()
|
||||
expect(env.ORCA_PI_CODING_AGENT_DIR).toBeUndefined()
|
||||
expect(env.ORCA_PI_SOURCE_AGENT_DIR).toBe('/home/tester/.config/pi-agent')
|
||||
}
|
||||
)
|
||||
|
||||
it('injects the agent hook receiver env into Orca terminal PTYs', async () => {
|
||||
const env = await spawnAndGetEnv()
|
||||
|
|
@ -1029,7 +1040,7 @@ describe('registerPtyHandlers', () => {
|
|||
// never invoked, so every host-local env injection must happen inside
|
||||
// the pty:spawn IPC handler instead. Before the refactor, only the
|
||||
// hook server env and attribution shims were injected on this path;
|
||||
// OpenCode plugin dir, Pi overlay, Codex home, and dev-mode CLI
|
||||
// OpenCode plugin dir, Pi managed extension env, Codex home, and dev-mode CLI
|
||||
// overrides were silently missing for daemon users (the common case).
|
||||
|
||||
function setupDaemonAdapter() {
|
||||
|
|
@ -1113,7 +1124,7 @@ describe('registerPtyHandlers', () => {
|
|||
},
|
||||
processEnvOverrides?: Record<string, string | undefined>,
|
||||
// Why: daemon spawn tests need to exercise both WSL launch metadata
|
||||
// from main and PR #2662 command threading for OMP overlay selection.
|
||||
// from main and PR #2662 command threading for OMP target selection.
|
||||
spawnArgs?: {
|
||||
cwd?: string
|
||||
shellOverride?: string
|
||||
|
|
@ -1217,21 +1228,18 @@ describe('registerPtyHandlers', () => {
|
|||
expect(env.ORCA_OPENCODE_SOURCE_CONFIG_DIR).toBe('/user/custom/opencode')
|
||||
})
|
||||
|
||||
it('injects Pi overlay env (PI_CODING_AGENT_DIR) on the daemon path', async () => {
|
||||
it('installs Pi managed extensions without redirecting homes on the daemon path', async () => {
|
||||
const env = await daemonSpawnAndGetEnv({ PI_CODING_AGENT_DIR: '/user/.pi/agent' })
|
||||
// Why: asserts the overlay key was passed through — the id is the
|
||||
// daemon-assigned sessionId minted in pty.ts, and the mock returns
|
||||
// the fixed overlay path from the shared setup.
|
||||
expect(piBuildPtyEnvMock).toHaveBeenCalledWith(expect.any(String), '/user/.pi/agent', 'pi')
|
||||
expect(piBuildPtyEnvMock).toHaveBeenCalledWith(expect.any(String), undefined, 'omp')
|
||||
expect(env.PI_CODING_AGENT_DIR).toBe('/tmp/orca-pi-agent-overlay')
|
||||
expect(env.ORCA_PI_CODING_AGENT_DIR).toBe('/tmp/orca-pi-agent-overlay')
|
||||
expect(env.PI_CODING_AGENT_DIR).toBe('/user/.pi/agent')
|
||||
expect(env.ORCA_PI_CODING_AGENT_DIR).toBeUndefined()
|
||||
expect(env.ORCA_PI_SOURCE_AGENT_DIR).toBe('/user/.pi/agent')
|
||||
expect(env.ORCA_OMP_CODING_AGENT_DIR).toBe('/tmp/orca-pi-agent-overlay')
|
||||
expect(env.ORCA_OMP_CODING_AGENT_DIR).toBeUndefined()
|
||||
expect(env.ORCA_OMP_STATUS_EXTENSION).toBe(expectedOmpStatusExtension)
|
||||
})
|
||||
|
||||
it('threads command: "omp" through to piBuildPtyEnv on the daemon path with OMP shadow vars', async () => {
|
||||
it('threads command: "omp" through to piBuildPtyEnv on the daemon path with OMP status metadata', async () => {
|
||||
// Why: mirror of the local-spawn OMP threading assertion. The
|
||||
// daemon path's `command` forwarding could silently regress and
|
||||
// Pi-only tests would still pass.
|
||||
|
|
@ -1247,9 +1255,11 @@ describe('registerPtyHandlers', () => {
|
|||
'/user/.omp/agent',
|
||||
'omp'
|
||||
)
|
||||
expect(env.PI_CODING_AGENT_DIR).toBe('/tmp/orca-pi-agent-overlay')
|
||||
expect(env.ORCA_OMP_CODING_AGENT_DIR).toBe('/tmp/orca-pi-agent-overlay')
|
||||
expect(env.ORCA_OMP_STATUS_EXTENSION).toBe(expectedOmpStatusExtension)
|
||||
expect(env.PI_CODING_AGENT_DIR).toBe('/user/.omp/agent')
|
||||
expect(env.ORCA_OMP_CODING_AGENT_DIR).toBeUndefined()
|
||||
expect(env.ORCA_OMP_STATUS_EXTENSION).toBe(
|
||||
'/user/.omp/agent/extensions/orca-agent-status.ts'
|
||||
)
|
||||
expect(env.ORCA_OMP_SOURCE_AGENT_DIR).toBe('/user/.omp/agent')
|
||||
expect(env.ORCA_PI_CODING_AGENT_DIR).toBeUndefined()
|
||||
expect(env.ORCA_PI_SOURCE_AGENT_DIR).toBeUndefined()
|
||||
|
|
@ -1686,7 +1696,7 @@ describe('registerPtyHandlers', () => {
|
|||
// existing-agent-dir guard stays consistent whether Pi's env was
|
||||
// carried on the IPC wire or inherited by the daemon via fork. The
|
||||
// fallback must reach piTitlebarExtensionService.buildPtyEnv as the
|
||||
// second arg so the overlay preserves the user's existing root.
|
||||
// second arg so Orca installs managed extensions in the user's root.
|
||||
const env = await daemonSpawnAndGetEnv({}, undefined, undefined, {
|
||||
PI_CODING_AGENT_DIR: '/ambient/pi/agent'
|
||||
})
|
||||
|
|
@ -1695,8 +1705,9 @@ describe('registerPtyHandlers', () => {
|
|||
'/ambient/pi/agent',
|
||||
'pi'
|
||||
)
|
||||
expect(env.PI_CODING_AGENT_DIR).toBe('/tmp/orca-pi-agent-overlay')
|
||||
expect(env.ORCA_PI_CODING_AGENT_DIR).toBe('/tmp/orca-pi-agent-overlay')
|
||||
expect(env.PI_CODING_AGENT_DIR).toBeUndefined()
|
||||
expect(env.ORCA_PI_CODING_AGENT_DIR).toBeUndefined()
|
||||
expect(env.ORCA_PI_SOURCE_AGENT_DIR).toBe('/ambient/pi/agent')
|
||||
})
|
||||
|
||||
it('skips attribution shims on the daemon path when the setting is disabled', async () => {
|
||||
|
|
@ -1749,10 +1760,10 @@ describe('registerPtyHandlers', () => {
|
|||
})
|
||||
|
||||
it('sweeps per-PTY state when provider.spawn fails for a MINTED sessionId', async () => {
|
||||
// Why: buildPtyHostEnv has filesystem side-effects (Pi overlay
|
||||
// materialization). If provider.spawn later fails, the overlay would
|
||||
// leak. The handler should clear per-PTY state for the minted id so
|
||||
// it isn't orphaned.
|
||||
// Why: buildPtyHostEnv has filesystem side-effects (Pi/OMP managed
|
||||
// extension installation and legacy overlay cleanup). If provider.spawn
|
||||
// later fails, per-PTY state for the minted id should be cleared so it
|
||||
// isn't orphaned.
|
||||
const daemonSpawn = vi.fn(async () => {
|
||||
throw new Error('spawn boom')
|
||||
})
|
||||
|
|
@ -1778,8 +1789,9 @@ describe('registerPtyHandlers', () => {
|
|||
|
||||
it('does NOT sweep per-PTY state on provider.spawn failure for CALLER-supplied sessionId', async () => {
|
||||
// Why: a caller-supplied sessionId may refer to an existing PTY whose
|
||||
// state (OpenCode hooks, Pi overlay, agent-hook pane caches) must not
|
||||
// be clobbered on a retry/attach failure. Only minted ids get swept.
|
||||
// state (OpenCode hooks, legacy Pi overlay cleanup, agent-hook pane
|
||||
// caches) must not be clobbered on a retry/attach failure. Only minted
|
||||
// ids get swept.
|
||||
const daemonSpawn = vi.fn(async () => {
|
||||
throw new Error('spawn boom')
|
||||
})
|
||||
|
|
@ -4700,8 +4712,8 @@ describe('registerPtyHandlers', () => {
|
|||
expect(args).toEqual(['-l'])
|
||||
expect(options.env.OPENCODE_CONFIG_DIR).toBeUndefined()
|
||||
expect(options.env.ORCA_OPENCODE_CONFIG_DIR).toBeUndefined()
|
||||
expect(options.env.PI_CODING_AGENT_DIR).toBe('/tmp/orca-pi-agent-overlay')
|
||||
expect(options.env.ORCA_PI_CODING_AGENT_DIR).toBe('/tmp/orca-pi-agent-overlay')
|
||||
expect(options.env.PI_CODING_AGENT_DIR).toBe('/tmp/user-pi-agent')
|
||||
expect(options.env.ORCA_PI_CODING_AGENT_DIR).toBeUndefined()
|
||||
expect(options.env.ORCA_PI_SOURCE_AGENT_DIR).toBe('/tmp/user-pi-agent')
|
||||
expect(options.env.ZDOTDIR).toBe('/tmp/orca-user-data/shell-ready/zsh')
|
||||
expect(options.env.ORCA_SHELL_READY_MARKER).toBe('0')
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import { openCodeHookService } from '../opencode/hook-service'
|
|||
import { agentHookServer } from '../agent-hooks/server'
|
||||
import { isAgentStatusHooksEnabled } from '../agent-hooks/managed-agent-hook-controls'
|
||||
import { piTitlebarExtensionService } from '../pi/titlebar-extension-service'
|
||||
import { ORCA_PI_AGENT_STATUS_EXTENSION_FILE } from '../pi/agent-status-extension-source'
|
||||
import { detectPiAgentKindFromCommand, type PiAgentKind } from '../../shared/pi-agent-kind'
|
||||
import { isPwshAvailable } from '../pwsh'
|
||||
import { LocalPtyProvider } from '../providers/local-pty-provider'
|
||||
|
|
@ -347,11 +346,11 @@ function finishPtyShutdown(
|
|||
// ─── Host PTY env assembly ──────────────────────────────────────────
|
||||
// Why: both the LocalPtyProvider.buildSpawnEnv closure and the daemon-active
|
||||
// fallback in pty:spawn need the same set of host-local env injections
|
||||
// (OpenCode plugin dir, agent-hook server coordinates, Pi overlay, Codex
|
||||
// account home, dev-mode CLI overrides, GitHub attribution shims). They used
|
||||
// to be implemented twice, which silently drifted — daemon-backed PTYs never
|
||||
// got the OpenCode plugin, Pi overlay, Codex home, or dev CLI PATH prepend,
|
||||
// so status dots, Pi state, Codex account switching, and CLI→dev
|
||||
// (OpenCode plugin dir, agent-hook server coordinates, Pi/OMP managed
|
||||
// extensions, Codex account home, dev-mode CLI overrides, GitHub attribution
|
||||
// shims). They used to be implemented twice, which silently drifted —
|
||||
// daemon-backed PTYs never got the OpenCode plugin, Pi integration, Codex
|
||||
// home, or dev CLI PATH prepend, so status dots, Pi state, Codex switching, and CLI→dev
|
||||
// routing were all broken for daemon users (the common case).
|
||||
//
|
||||
// Centralizing the injections here makes future additions fail-safe: a new
|
||||
|
|
@ -364,8 +363,8 @@ export type BuildPtyHostEnvOptions = {
|
|||
skipCodexHomeEnv?: boolean
|
||||
githubAttributionEnabled: boolean
|
||||
/** The launch command the renderer chose for this PTY (e.g. 'pi', 'omp',
|
||||
* 'claude'). Used to resolve the per-agent overlay source dir for Pi /
|
||||
* OMP - both consume `PI_CODING_AGENT_DIR` but default to different
|
||||
* 'claude'). Used to resolve the per-agent managed extension target for
|
||||
* Pi / OMP - both consume `PI_CODING_AGENT_DIR` but default to different
|
||||
* `~/.<kind>/agent` paths. Undefined for bare-shell spawns; defaults
|
||||
* resolve to Pi for back-compat. NEVER infer from disk presence; that's
|
||||
* the bug this option fixes (cross-agent shadowing when both dirs exist). */
|
||||
|
|
@ -504,10 +503,6 @@ function resolveScopedPiAgentSourceDir(
|
|||
return readEnvWithProcessFallback(baseEnv, sourceKey)
|
||||
}
|
||||
|
||||
function getPiAgentStatusExtensionPath(agentDir: string): string {
|
||||
return join(agentDir, 'extensions', ORCA_PI_AGENT_STATUS_EXTENSION_FILE)
|
||||
}
|
||||
|
||||
function clearPiAgentShadowEnv(baseEnv: Record<string, string>, kind: PiAgentKind): void {
|
||||
if (kind === 'omp') {
|
||||
delete baseEnv.ORCA_OMP_CODING_AGENT_DIR
|
||||
|
|
@ -519,29 +514,28 @@ function clearPiAgentShadowEnv(baseEnv: Record<string, string>, kind: PiAgentKin
|
|||
delete baseEnv.ORCA_PI_SOURCE_AGENT_DIR
|
||||
}
|
||||
|
||||
function exposePiAgentOverlayEnv(
|
||||
function exposePiManagedExtensionEnv(
|
||||
baseEnv: Record<string, string>,
|
||||
kind: PiAgentKind,
|
||||
overlayDir: string,
|
||||
sourceDir: string | undefined
|
||||
managedEnv: Record<string, string>
|
||||
): void {
|
||||
if (kind === 'omp') {
|
||||
baseEnv.ORCA_OMP_CODING_AGENT_DIR = overlayDir
|
||||
baseEnv.ORCA_OMP_STATUS_EXTENSION = getPiAgentStatusExtensionPath(overlayDir)
|
||||
if (sourceDir) {
|
||||
// Why: preserve the original OMP root across nested Orca terminals; the
|
||||
// public env var is intentionally restored to the current PTY overlay.
|
||||
baseEnv.ORCA_OMP_SOURCE_AGENT_DIR = sourceDir
|
||||
delete baseEnv.ORCA_OMP_CODING_AGENT_DIR
|
||||
if (managedEnv.ORCA_OMP_SOURCE_AGENT_DIR) {
|
||||
baseEnv.ORCA_OMP_SOURCE_AGENT_DIR = managedEnv.ORCA_OMP_SOURCE_AGENT_DIR
|
||||
} else {
|
||||
delete baseEnv.ORCA_OMP_SOURCE_AGENT_DIR
|
||||
}
|
||||
if (managedEnv.ORCA_OMP_STATUS_EXTENSION) {
|
||||
baseEnv.ORCA_OMP_STATUS_EXTENSION = managedEnv.ORCA_OMP_STATUS_EXTENSION
|
||||
} else {
|
||||
delete baseEnv.ORCA_OMP_STATUS_EXTENSION
|
||||
}
|
||||
return
|
||||
}
|
||||
baseEnv.ORCA_PI_CODING_AGENT_DIR = overlayDir
|
||||
if (sourceDir) {
|
||||
// Why: preserve the original Pi root across nested Orca terminals; the
|
||||
// public env var is intentionally restored to the current PTY overlay.
|
||||
baseEnv.ORCA_PI_SOURCE_AGENT_DIR = sourceDir
|
||||
delete baseEnv.ORCA_PI_CODING_AGENT_DIR
|
||||
if (managedEnv.ORCA_PI_SOURCE_AGENT_DIR) {
|
||||
baseEnv.ORCA_PI_SOURCE_AGENT_DIR = managedEnv.ORCA_PI_SOURCE_AGENT_DIR
|
||||
} else {
|
||||
delete baseEnv.ORCA_PI_SOURCE_AGENT_DIR
|
||||
}
|
||||
|
|
@ -568,8 +562,9 @@ function getInheritedAgentHookEnvKeysToDelete(
|
|||
}
|
||||
|
||||
// Why: when agent status is disabled, a nested Orca terminal can still pass
|
||||
// through a prior PTY's OpenCode/Pi overlay env. Restore the user's original
|
||||
// source dir when Orca recorded one, otherwise strip only values known to be ours.
|
||||
// through prior OpenCode or legacy Pi/OMP overlay env. Restore the user's
|
||||
// original source dir when Orca recorded one, otherwise strip only values
|
||||
// known to be ours.
|
||||
function restoreOrStripOverlayEnv(
|
||||
baseEnv: Record<string, string>,
|
||||
keys: {
|
||||
|
|
@ -660,8 +655,7 @@ export function buildPtyHostEnv(
|
|||
// value cannot coexist with an Orca-only injection. Hand the user's value
|
||||
// (when present) to the hook service and let it materialize a source-scoped
|
||||
// mirror overlay that lets the user's plugins and Orca's status plugin
|
||||
// load together — same pattern Pi uses below for PI_CODING_AGENT_DIR. See
|
||||
// docs/opencode-config-dir-collision.md.
|
||||
// load together. See docs/opencode-config-dir-collision.md.
|
||||
Object.assign(baseEnv, openCodeHookService.buildPtyEnv(id, preexistingOpenCodeConfigDir))
|
||||
if (baseEnv.OPENCODE_CONFIG_DIR) {
|
||||
// Why: ~/.zshrc can re-export the user's default after spawn; shell-ready
|
||||
|
|
@ -698,36 +692,22 @@ export function buildPtyHostEnv(
|
|||
Object.assign(baseEnv, agentHookServer.buildPtyEnv())
|
||||
}
|
||||
|
||||
// Why: PI_CODING_AGENT_DIR owns Pi's / OMP's full config/session root (OMP
|
||||
// inherits the env var name from Pi by design; its CHANGELOG documents the
|
||||
// OMP_CODING_AGENT_DIR -> PI_CODING_AGENT_DIR rename. Build a source-scoped
|
||||
// overlay from the caller's chosen root so Orca extensions load without
|
||||
// making each terminal look like a separate Pi home.
|
||||
// Why: PI_CODING_AGENT_DIR owns Pi's / OMP's full config/session root. Keep
|
||||
// that home as the user's normal source of truth and install only Orca-owned,
|
||||
// env-guarded extension files into the selected agent's extension dir.
|
||||
if (opts.agentStatusHooksEnabled) {
|
||||
clearPiAgentShadowEnv(baseEnv, 'pi')
|
||||
clearPiAgentShadowEnv(baseEnv, 'omp')
|
||||
if (piAgentKind === 'pi') {
|
||||
const piEnv = piTitlebarExtensionService.buildPtyEnv(id, preexistingPiAgentDir, 'pi')
|
||||
Object.assign(baseEnv, piEnv)
|
||||
if (piEnv.PI_CODING_AGENT_DIR) {
|
||||
// Why: ~/.zshrc can re-export the user's default after spawn; shell-ready
|
||||
// wrappers restore this PTY-scoped value after user startup files run.
|
||||
baseEnv.PI_CODING_AGENT_DIR = piEnv.PI_CODING_AGENT_DIR
|
||||
exposePiAgentOverlayEnv(baseEnv, 'pi', piEnv.PI_CODING_AGENT_DIR, preexistingPiAgentDir)
|
||||
}
|
||||
exposePiManagedExtensionEnv(baseEnv, 'pi', piEnv)
|
||||
}
|
||||
|
||||
if (shouldPrepareOmpShadow) {
|
||||
const ompEnv = piTitlebarExtensionService.buildPtyEnv(id, preexistingOmpAgentDir, 'omp')
|
||||
if (ompEnv.PI_CODING_AGENT_DIR) {
|
||||
if (piAgentKind === 'omp') {
|
||||
// Why: an OMP-launched PTY should default the binary-facing env var to
|
||||
// OMP. Bare shells keep the Pi primary and use the `omp` shell wrapper
|
||||
// to switch only while OMP is running.
|
||||
baseEnv.PI_CODING_AGENT_DIR = ompEnv.PI_CODING_AGENT_DIR
|
||||
}
|
||||
exposePiAgentOverlayEnv(baseEnv, 'omp', ompEnv.PI_CODING_AGENT_DIR, preexistingOmpAgentDir)
|
||||
}
|
||||
Object.assign(baseEnv, ompEnv)
|
||||
exposePiManagedExtensionEnv(baseEnv, 'omp', ompEnv)
|
||||
}
|
||||
} else {
|
||||
// Why: when agent status is disabled we must strip BOTH kinds' shadow vars
|
||||
|
|
@ -2181,18 +2161,18 @@ export function registerPtyHandlers(
|
|||
}
|
||||
// Why: the daemon-backed provider replaces LocalPtyProvider and therefore
|
||||
// never runs its buildSpawnEnv closure. We must assemble the same
|
||||
// host-local env (OpenCode plugin, agent-hook server, Pi overlay, Codex
|
||||
// home, dev CLI overrides, GitHub attribution shims) here so both spawn
|
||||
// paths behave identically. buildPtyHostEnv is the shared helper that
|
||||
// encapsulates the full set of injections and their order/guards.
|
||||
// host-local env (OpenCode plugin, agent-hook server, Pi/OMP managed
|
||||
// extensions, Codex home, dev CLI overrides, GitHub attribution shims)
|
||||
// here so both spawn paths behave identically. buildPtyHostEnv is the
|
||||
// shared helper that encapsulates the full set of injections and guards.
|
||||
//
|
||||
// Safety: skip the entire injection when a remote (SSH) connection is in
|
||||
// play. Every injection here is either host-loopback (the agent-hook
|
||||
// server binds 127.0.0.1, so shipping its token to an SSH host would
|
||||
// leak a loopback secret for no functional benefit) or a path on the
|
||||
// local filesystem (OpenCode plugin dir, Pi overlay, Codex home, dev
|
||||
// CLI bin, attribution shim dir) that would resolve to nothing — or
|
||||
// something misleading — on the remote machine.
|
||||
// local filesystem (OpenCode plugin dir, Pi/OMP extension paths, Codex
|
||||
// home, dev CLI bin, attribution shim dir) that would resolve to
|
||||
// nothing — or something misleading — on the remote machine.
|
||||
const isDaemonHostSpawn = !args.connectionId && !(provider instanceof LocalPtyProvider)
|
||||
// Why: daemon host-env setup needs a stable id BEFORE provider.spawn so
|
||||
// provider hooks and legacy Pi overlay cleanup can run in buildPtyHostEnv.
|
||||
|
|
@ -2206,8 +2186,8 @@ export function registerPtyHandlers(
|
|||
// fresh UUID per spawn; that would orphan reconnectable terminal state.
|
||||
// Why: only state for ids we minted in THIS request should be cleared on
|
||||
// spawn failure. If the caller supplied args.sessionId it may refer to
|
||||
// an existing PTY whose state (OpenCode hooks, Pi overlay, agent-hook
|
||||
// pane caches) we must not clobber on a retry/attach failure.
|
||||
// an existing PTY whose state (OpenCode hooks, legacy Pi overlay cleanup,
|
||||
// agent-hook pane caches) we must not clobber on a retry/attach failure.
|
||||
const isMintedSessionId = args.sessionId === undefined && isDaemonHostSpawn
|
||||
const effectiveSessionId =
|
||||
args.sessionId ?? (isDaemonHostSpawn ? mintPtySessionId(args.worktreeId) : undefined)
|
||||
|
|
@ -2326,8 +2306,8 @@ export function registerPtyHandlers(
|
|||
})
|
||||
promoteAgentTeamsShimPath(env, requestedAgentTeamsPath)
|
||||
} catch (err) {
|
||||
// Why: buildPtyHostEnv has filesystem side-effects (Pi overlay
|
||||
// materialization). If it throws before we reach provider.spawn,
|
||||
// Why: buildPtyHostEnv has filesystem side-effects (Pi/OMP managed
|
||||
// extension installation). If it throws before we reach provider.spawn,
|
||||
// clear per-PTY state so the next attempt starts clean.
|
||||
//
|
||||
// Only sweep state for ids we MINTED in this request — caller-
|
||||
|
|
|
|||
|
|
@ -2,13 +2,12 @@
|
|||
// in-process TypeScript extension API (pi.on('agent_start'), 'tool_call',
|
||||
// etc.). To get pi panes into the unified agent-hooks pipeline alongside
|
||||
// Claude/Codex/Gemini/OpenCode/Cursor, we ship a bundled extension into
|
||||
// the Pi overlay (PiTitlebarExtensionService) that POSTs to
|
||||
// the selected Pi/OMP extension dir (PiTitlebarExtensionService) that POSTs to
|
||||
// /hook/<kind> using the same ORCA_AGENT_HOOK_* + ORCA_PANE_KEY env that every
|
||||
// PTY already receives from ipc/pty.ts.
|
||||
//
|
||||
// Each Pi process still gets its own paneKey through env even when multiple
|
||||
// PTYs share one source-scoped overlay. Like the OpenCode plugin, the returned
|
||||
// source is a string (loaded by jiti from disk inside the pi process), so we
|
||||
// Each Pi process gets its own paneKey through env. Like the OpenCode plugin,
|
||||
// the returned source is a string (loaded by jiti from disk inside the pi process), so we
|
||||
// keep the source body in plain JS without TS types and avoid pulling pi or
|
||||
// any Orca dep into the pi runtime.
|
||||
import type { PiAgentKind } from '../../shared/pi-agent-kind'
|
||||
|
|
@ -96,8 +95,8 @@ export function getPiAgentStatusExtensionSource(kind: PiAgentKind = 'pi'): strin
|
|||
' const isOmpExecutable = executableNames.some((name) =>',
|
||||
" ['omp', 'omp.js', 'omp.sh', 'omp.cmd', 'omp.exe', 'omp.bat'].includes(name)",
|
||||
' )',
|
||||
' // Why: a bare shell gets the Pi overlay at spawn time, but may later',
|
||||
' // launch OMP. Runtime executable detection keeps that status labeled',
|
||||
' // Why: a bare shell may launch either Pi or OMP after spawn. Runtime',
|
||||
' // executable detection keeps that status labeled',
|
||||
' // as OMP instead of silently reporting it as Pi.',
|
||||
' if (isOmpExecutable) {',
|
||||
" return '/hook/omp'",
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export function getPiPrefillExtensionSource(kind: PiAgentKind): string {
|
|||
return [
|
||||
'export default function (pi) {',
|
||||
" pi.on('session_start', async (event, ctx) => {",
|
||||
' if (!process.env.ORCA_PANE_KEY) return',
|
||||
" if (event.reason !== 'startup') return",
|
||||
` const prefill = process.env.${envVar}`,
|
||||
' if (!prefill) return',
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { createHash } from 'crypto'
|
||||
import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from 'fs'
|
||||
import { tmpdir } from 'os'
|
||||
import { basename, join, sep } from 'path'
|
||||
import { join, sep } from 'path'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const userDataDir = mkdtempSync(join(tmpdir(), 'orca-pi-overlay-path-userdata-'))
|
||||
|
|
@ -29,36 +28,28 @@ const PATH_SHAPED_PTY_ID = [
|
|||
'feature@@a1b2c3d4'
|
||||
].join(sep)
|
||||
|
||||
function overlayPath(kind: 'pi' | 'omp', sourceAgentDir: string): string {
|
||||
const rootDir = kind === 'pi' ? 'pi-agent-overlays' : 'omp-agent-overlays'
|
||||
const safeName = createHash('sha256')
|
||||
.update(`source:${sourceAgentDir}`)
|
||||
.digest('hex')
|
||||
.slice(0, 32)
|
||||
return join(userDataDir, rootDir, safeName)
|
||||
}
|
||||
|
||||
function legacyOverlayPath(kind: 'pi' | 'omp', ptyId: string): string {
|
||||
const rootDir = kind === 'pi' ? 'pi-agent-overlays' : 'omp-agent-overlays'
|
||||
return join(userDataDir, rootDir, ptyId)
|
||||
}
|
||||
|
||||
describe('PiTitlebarExtensionService overlay paths', () => {
|
||||
describe('PiTitlebarExtensionService legacy overlay paths', () => {
|
||||
afterEach(() => {
|
||||
rmSync(join(userDataDir, 'pi-agent-overlays'), { recursive: true, force: true })
|
||||
rmSync(join(userDataDir, 'omp-agent-overlays'), { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('hashes source agent dirs into bounded shared overlay directory names', () => {
|
||||
it('does not redirect path-shaped PTY ids into active Pi homes', () => {
|
||||
const piHome = mkdtempSync(join(tmpdir(), 'orca-pi-overlay-path-home-'))
|
||||
const svc = new PiTitlebarExtensionService()
|
||||
|
||||
try {
|
||||
const env = svc.buildPtyEnv(PATH_SHAPED_PTY_ID, piHome, 'pi')
|
||||
|
||||
expect(env.PI_CODING_AGENT_DIR).toBe(overlayPath('pi', piHome))
|
||||
expect(basename(env.PI_CODING_AGENT_DIR!)).toMatch(/^[a-f0-9]{32}$/)
|
||||
expect(readdirSync(join(env.PI_CODING_AGENT_DIR!, 'extensions')).sort()).toEqual([
|
||||
expect(env.PI_CODING_AGENT_DIR).toBeUndefined()
|
||||
expect(env.ORCA_PI_SOURCE_AGENT_DIR).toBe(piHome)
|
||||
expect(existsSync(join(userDataDir, 'pi-agent-overlays'))).toBe(false)
|
||||
expect(readdirSync(join(piHome, 'extensions')).sort()).toEqual([
|
||||
'orca-agent-status.ts',
|
||||
'orca-prefill.ts',
|
||||
'orca-titlebar-spinner.ts'
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
existsSync,
|
||||
lstatSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
|
|
@ -13,7 +12,6 @@ import {
|
|||
import { tmpdir } from 'os'
|
||||
import type * as osModule from 'os'
|
||||
import { join } from 'path'
|
||||
import { createHash } from 'crypto'
|
||||
|
||||
// The service calls app.getPath('userData') for its overlay root. Point that
|
||||
// at a real tmp dir so we can exercise the filesystem behavior end-to-end.
|
||||
|
|
@ -47,21 +45,6 @@ vi.mock('electron', () => ({
|
|||
|
||||
import { PiTitlebarExtensionService, isSafeDescendCandidate } from './titlebar-extension-service'
|
||||
|
||||
function overlayPath(kind: 'pi' | 'omp', sourceAgentDir: string): string {
|
||||
const rootDir = kind === 'pi' ? 'pi-agent-overlays' : 'omp-agent-overlays'
|
||||
const safeName = createHash('sha256')
|
||||
.update(`source:${sourceAgentDir}`)
|
||||
.digest('hex')
|
||||
.slice(0, 32)
|
||||
return join(userDataDir, rootDir, safeName)
|
||||
}
|
||||
|
||||
function ptyOverlayPath(kind: 'pi' | 'omp', ptyId: string): string {
|
||||
const rootDir = kind === 'pi' ? 'pi-agent-overlays' : 'omp-agent-overlays'
|
||||
const safeName = createHash('sha256').update(ptyId).digest('hex').slice(0, 32)
|
||||
return join(userDataDir, rootDir, safeName)
|
||||
}
|
||||
|
||||
function legacyOverlayPath(kind: 'pi' | 'omp', ptyId: string): string {
|
||||
const rootDir = kind === 'pi' ? 'pi-agent-overlays' : 'omp-agent-overlays'
|
||||
return join(userDataDir, rootDir, ptyId)
|
||||
|
|
@ -124,90 +107,84 @@ describe('PiTitlebarExtensionService', () => {
|
|||
})
|
||||
}
|
||||
|
||||
it('buildPtyEnv mirrors the user agent dir into an overlay under userData', () => {
|
||||
it('buildPtyEnv installs Orca extensions into the user agent dir without redirecting the home', () => {
|
||||
const svc = new PiTitlebarExtensionService()
|
||||
const env = svc.buildPtyEnv('pty-1', piHome, 'pi')
|
||||
|
||||
expect(env.PI_CODING_AGENT_DIR).toBe(overlayPath('pi', piHome))
|
||||
// Orca's titlebar extension is added alongside user extensions, not replacing them.
|
||||
const overlayExtensions = readdirSync(join(env.PI_CODING_AGENT_DIR!, 'extensions')).sort()
|
||||
expect(overlayExtensions).toEqual([
|
||||
expect(env.PI_CODING_AGENT_DIR).toBeUndefined()
|
||||
expect(env.ORCA_PI_SOURCE_AGENT_DIR).toBe(piHome)
|
||||
const extensions = readdirSync(join(piHome, 'extensions')).sort()
|
||||
expect(extensions).toEqual([
|
||||
'orca-agent-status.ts',
|
||||
'orca-prefill.ts',
|
||||
'orca-titlebar-spinner.ts',
|
||||
'user-ext'
|
||||
])
|
||||
const statusExtensionSource = readFileSync(
|
||||
join(env.PI_CODING_AGENT_DIR!, 'extensions', 'orca-agent-status.ts'),
|
||||
join(piHome, 'extensions', 'orca-agent-status.ts'),
|
||||
'utf-8'
|
||||
)
|
||||
const titlebarExtensionSource = readFileSync(
|
||||
join(piHome, 'extensions', 'orca-titlebar-spinner.ts'),
|
||||
'utf-8'
|
||||
)
|
||||
const prefillExtensionSource = readFileSync(
|
||||
join(piHome, 'extensions', 'orca-prefill.ts'),
|
||||
'utf-8'
|
||||
)
|
||||
expect(statusExtensionSource).toContain('@orca-managed-pi-extension')
|
||||
expect(statusExtensionSource).toContain('/hook/pi')
|
||||
expect(statusExtensionSource).toContain('process.title')
|
||||
expect(statusExtensionSource).toContain("return '/hook/omp'")
|
||||
expect(
|
||||
JSON.parse(readFileSync(join(env.PI_CODING_AGENT_DIR!, 'settings.json'), 'utf-8'))
|
||||
).toEqual({
|
||||
defaultProvider: 'amazon-bedrock',
|
||||
hideThinkingBlock: true,
|
||||
packages: ['npm:pi-web-access'],
|
||||
terminal: {
|
||||
showImages: false,
|
||||
clearOnShrink: true
|
||||
}
|
||||
})
|
||||
// User's top-level resources are reachable via the overlay.
|
||||
expect(existsSync(join(env.PI_CODING_AGENT_DIR!, 'skills', 'my-skill', 'SKILL.md'))).toBe(true)
|
||||
expect(existsSync(join(env.PI_CODING_AGENT_DIR!, 'auth.json'))).toBe(true)
|
||||
expect(titlebarExtensionSource).toContain('@orca-managed-pi-extension')
|
||||
expect(titlebarExtensionSource).toContain('process.env.ORCA_PANE_KEY')
|
||||
expect(prefillExtensionSource).toContain('@orca-managed-pi-extension')
|
||||
expect(prefillExtensionSource).toContain('process.env.ORCA_PANE_KEY')
|
||||
expectPiHomeIntact()
|
||||
})
|
||||
|
||||
it('clearPty leaves the source overlay alive without touching the user Pi dir', () => {
|
||||
it('clearPty leaves the real Pi dir and managed extensions intact', () => {
|
||||
const svc = new PiTitlebarExtensionService()
|
||||
const env = svc.buildPtyEnv('pty-2', piHome, 'pi')
|
||||
svc.buildPtyEnv('pty-2', piHome, 'pi')
|
||||
svc.clearPty('pty-2')
|
||||
|
||||
// Why: source-scoped overlays may be shared by other live Pi terminals;
|
||||
// per-PTY teardown must not remove shared state.
|
||||
expect(existsSync(env.PI_CODING_AGENT_DIR!)).toBe(true)
|
||||
expect(existsSync(join(piHome, 'extensions', 'orca-agent-status.ts'))).toBe(true)
|
||||
expectPiHomeIntact()
|
||||
})
|
||||
|
||||
it('uses one source-scoped overlay for multiple PTYs with the same Pi dir', () => {
|
||||
it('uses the same source dir for multiple PTYs with the same Pi dir', () => {
|
||||
const svc = new PiTitlebarExtensionService()
|
||||
const firstEnv = svc.buildPtyEnv('pty-shared-1', piHome, 'pi')
|
||||
const secondEnv = svc.buildPtyEnv('pty-shared-2', piHome, 'pi')
|
||||
|
||||
expect(secondEnv.PI_CODING_AGENT_DIR).toBe(firstEnv.PI_CODING_AGENT_DIR)
|
||||
expect(secondEnv.PI_CODING_AGENT_DIR).toBe(overlayPath('pi', piHome))
|
||||
expect(
|
||||
readFileSync(
|
||||
join(secondEnv.PI_CODING_AGENT_DIR!, 'extensions', 'user-ext', 'ext.ts'),
|
||||
'utf-8'
|
||||
)
|
||||
).toBe('user extension')
|
||||
expect(firstEnv.PI_CODING_AGENT_DIR).toBeUndefined()
|
||||
expect(secondEnv.PI_CODING_AGENT_DIR).toBeUndefined()
|
||||
expect(secondEnv.ORCA_PI_SOURCE_AGENT_DIR).toBe(firstEnv.ORCA_PI_SOURCE_AGENT_DIR)
|
||||
expect(readFileSync(join(piHome, 'extensions', 'user-ext', 'ext.ts'), 'utf-8')).toBe(
|
||||
'user extension'
|
||||
)
|
||||
expectPiHomeIntact()
|
||||
})
|
||||
|
||||
it('source-backs OMP agent.db before OMP lazily creates it', () => {
|
||||
it('leaves OMP SQLite files in the real home instead of redirecting to an overlay', () => {
|
||||
const svc = new PiTitlebarExtensionService()
|
||||
const env = svc.buildPtyEnv('pty-omp-sqlite', piHome, 'omp')
|
||||
|
||||
const sourcePath = join(piHome, 'agent.db')
|
||||
const overlayPath = join(env.PI_CODING_AGENT_DIR!, 'agent.db')
|
||||
const content = 'agent.db credentials'
|
||||
|
||||
expect(existsSync(sourcePath)).toBe(true)
|
||||
expect(existsSync(overlayPath)).toBe(true)
|
||||
expect(env.PI_CODING_AGENT_DIR).toBeUndefined()
|
||||
expect(env.ORCA_OMP_SOURCE_AGENT_DIR).toBe(piHome)
|
||||
expect(env.ORCA_OMP_STATUS_EXTENSION).toBe(join(piHome, 'extensions', 'orca-agent-status.ts'))
|
||||
expect(existsSync(sourcePath)).toBe(false)
|
||||
expect(existsSync(join(userDataDir, 'omp-agent-overlays'))).toBe(false)
|
||||
expect(existsSync(join(piHome, 'history.db'))).toBe(false)
|
||||
writeFileSync(overlayPath, content)
|
||||
writeFileSync(sourcePath, content)
|
||||
|
||||
expect(readFileSync(sourcePath, 'utf-8')).toBe(content)
|
||||
if (process.platform !== 'win32') {
|
||||
expect(lstatSync(overlayPath).isSymbolicLink()).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('rebuilding an overlay for the same ptyId does not corrupt the user Pi dir', () => {
|
||||
it('rebuilding managed extensions for the same ptyId does not corrupt the user Pi dir', () => {
|
||||
const svc = new PiTitlebarExtensionService()
|
||||
svc.buildPtyEnv('pty-3', piHome, 'pi')
|
||||
svc.buildPtyEnv('pty-3', piHome, 'pi')
|
||||
|
|
@ -215,13 +192,13 @@ describe('PiTitlebarExtensionService', () => {
|
|||
expectPiHomeIntact()
|
||||
})
|
||||
|
||||
it('reconciles mirrored entries while preserving Pi-created shared overlay files', () => {
|
||||
it('rebuilding updates Orca-owned extensions while preserving user files', () => {
|
||||
const svc = new PiTitlebarExtensionService()
|
||||
const firstEnv = svc.buildPtyEnv('pty-refresh-1', piHome, 'pi')
|
||||
const overlayDir = firstEnv.PI_CODING_AGENT_DIR!
|
||||
|
||||
mkdirSync(join(overlayDir, 'runtime-cache'), { recursive: true })
|
||||
writeFileSync(join(overlayDir, 'runtime-cache', 'index.json'), '{}')
|
||||
svc.buildPtyEnv('pty-refresh-1', piHome, 'pi')
|
||||
writeFileSync(
|
||||
join(piHome, 'extensions', 'orca-agent-status.ts'),
|
||||
'// @orca-managed-pi-extension\nstale'
|
||||
)
|
||||
|
||||
rmSync(join(piHome, 'extensions', 'user-ext'), { recursive: true, force: true })
|
||||
mkdirSync(join(piHome, 'extensions', 'new-ext'), { recursive: true })
|
||||
|
|
@ -230,13 +207,10 @@ describe('PiTitlebarExtensionService', () => {
|
|||
|
||||
const secondEnv = svc.buildPtyEnv('pty-refresh-2', piHome, 'pi')
|
||||
|
||||
expect(secondEnv.PI_CODING_AGENT_DIR).toBe(overlayDir)
|
||||
expect(readFileSync(join(overlayDir, 'auth.json'), 'utf-8')).toBe('rotated token')
|
||||
expect(existsSync(join(overlayDir, 'extensions', 'user-ext'))).toBe(false)
|
||||
expect(readFileSync(join(overlayDir, 'extensions', 'new-ext', 'ext.ts'), 'utf-8')).toBe(
|
||||
'new user extension'
|
||||
expect(secondEnv.PI_CODING_AGENT_DIR).toBeUndefined()
|
||||
expect(readFileSync(join(piHome, 'extensions', 'orca-agent-status.ts'), 'utf-8')).toContain(
|
||||
'/hook/pi'
|
||||
)
|
||||
expect(existsSync(join(overlayDir, 'runtime-cache', 'index.json'))).toBe(true)
|
||||
expect(readFileSync(join(piHome, 'auth.json'), 'utf-8')).toBe('rotated token')
|
||||
expect(readFileSync(join(piHome, 'extensions', 'new-ext', 'ext.ts'), 'utf-8')).toBe(
|
||||
'new user extension'
|
||||
|
|
@ -250,17 +224,15 @@ describe('PiTitlebarExtensionService', () => {
|
|||
const svc = new PiTitlebarExtensionService()
|
||||
const env = svc.buildPtyEnv('pty-same-name-extension', piHome, 'pi')
|
||||
|
||||
expect(env.PI_CODING_AGENT_DIR).toBeUndefined()
|
||||
expect(readFileSync(join(piHome, 'extensions', 'orca-agent-status.ts'), 'utf-8')).toBe(
|
||||
userStatusExtension
|
||||
)
|
||||
expect(
|
||||
readFileSync(join(env.PI_CODING_AGENT_DIR!, 'extensions', 'orca-agent-status.ts'), 'utf-8')
|
||||
).toContain('/hook/pi')
|
||||
expectPiHomeIntact()
|
||||
})
|
||||
|
||||
it.skipIf(process.platform === 'win32')(
|
||||
'does not write bundled extensions through a symlinked user extensions dir',
|
||||
'writes bundled extensions through a symlinked user extensions dir',
|
||||
() => {
|
||||
const realExtensionsDir = mkdtempSync(join(tmpdir(), 'orca-real-pi-extensions-'))
|
||||
try {
|
||||
|
|
@ -271,18 +243,13 @@ describe('PiTitlebarExtensionService', () => {
|
|||
const svc = new PiTitlebarExtensionService()
|
||||
const env = svc.buildPtyEnv('pty-symlinked-extensions', piHome, 'pi')
|
||||
|
||||
expect(existsSync(join(realExtensionsDir, 'orca-agent-status.ts'))).toBe(false)
|
||||
expect(existsSync(join(realExtensionsDir, 'orca-prefill.ts'))).toBe(false)
|
||||
expect(existsSync(join(realExtensionsDir, 'orca-titlebar-spinner.ts'))).toBe(false)
|
||||
expect(
|
||||
readFileSync(join(env.PI_CODING_AGENT_DIR!, 'extensions', 'real-user-ext.ts'), 'utf-8')
|
||||
).toBe('real user extension')
|
||||
expect(
|
||||
readFileSync(
|
||||
join(env.PI_CODING_AGENT_DIR!, 'extensions', 'orca-agent-status.ts'),
|
||||
'utf-8'
|
||||
)
|
||||
).toContain('/hook/pi')
|
||||
expect(env.PI_CODING_AGENT_DIR).toBeUndefined()
|
||||
expect(existsSync(join(realExtensionsDir, 'orca-agent-status.ts'))).toBe(true)
|
||||
expect(existsSync(join(realExtensionsDir, 'orca-prefill.ts'))).toBe(true)
|
||||
expect(existsSync(join(realExtensionsDir, 'orca-titlebar-spinner.ts'))).toBe(true)
|
||||
expect(readFileSync(join(realExtensionsDir, 'orca-agent-status.ts'), 'utf-8')).toContain(
|
||||
'/hook/pi'
|
||||
)
|
||||
} finally {
|
||||
rmSync(realExtensionsDir, { recursive: true, force: true })
|
||||
}
|
||||
|
|
@ -306,16 +273,14 @@ describe('PiTitlebarExtensionService', () => {
|
|||
const svc = new PiTitlebarExtensionService()
|
||||
const env = svc.buildPtyEnv('pty-4', piHome, 'pi')
|
||||
|
||||
expect(env.PI_CODING_AGENT_DIR).toBe(overlayPath('pi', piHome))
|
||||
expect(env.PI_CODING_AGENT_DIR).toBeUndefined()
|
||||
expect(env.ORCA_PI_SOURCE_AGENT_DIR).toBe(piHome)
|
||||
expect(existsSync(legacyOverlayDir)).toBe(false)
|
||||
expect(existsSync(join(env.PI_CODING_AGENT_DIR!, 'skills', 'my-skill', 'SKILL.md'))).toBe(
|
||||
true
|
||||
)
|
||||
expectPiHomeIntact()
|
||||
}
|
||||
)
|
||||
|
||||
// Why: per-agent overlay source dir. Orca's user picks Pi or OMP per
|
||||
// Why: per-agent source dir. Orca's user picks Pi or OMP per
|
||||
// launch (the agent kind isn't a global install-time choice), so each
|
||||
// build's source dir MUST be resolved from the agent kind, not from a
|
||||
// disk-presence check that silently shadows the other agent's user
|
||||
|
|
@ -329,7 +294,7 @@ describe('PiTitlebarExtensionService', () => {
|
|||
return agentDir
|
||||
}
|
||||
|
||||
it('launching pi with both ~/.pi/agent and ~/.omp/agent present mirrors ~/.pi/agent', () => {
|
||||
it('launching pi with both ~/.pi/agent and ~/.omp/agent present installs into ~/.pi/agent', () => {
|
||||
const fakeHome = mkdtempSync(join(tmpdir(), 'orca-pi-both-'))
|
||||
seedAgentDir(fakeHome, '.pi', 'pi')
|
||||
seedAgentDir(fakeHome, '.omp', 'omp')
|
||||
|
|
@ -339,22 +304,21 @@ describe('PiTitlebarExtensionService', () => {
|
|||
const svc = new PiTitlebarExtensionService()
|
||||
const env = svc.buildPtyEnv('pty-pi-both', undefined, 'pi')
|
||||
|
||||
expect(env.PI_CODING_AGENT_DIR).toBe(overlayPath('pi', join(fakeHome, '.pi', 'agent')))
|
||||
// The Pi auth file must be the one mirrored (not OMP's).
|
||||
expect(readFileSync(join(env.PI_CODING_AGENT_DIR!, 'auth.json'), 'utf-8')).toBe(
|
||||
'pi secret token'
|
||||
)
|
||||
// The user extension dir must be Pi's, not OMP's.
|
||||
const overlayExtensions = readdirSync(join(env.PI_CODING_AGENT_DIR!, 'extensions')).sort()
|
||||
expect(overlayExtensions).toContain('pi-ext')
|
||||
expect(overlayExtensions).not.toContain('omp-ext')
|
||||
expect(env.PI_CODING_AGENT_DIR).toBeUndefined()
|
||||
expect(env.ORCA_PI_SOURCE_AGENT_DIR).toBe(join(fakeHome, '.pi', 'agent'))
|
||||
expect(
|
||||
existsSync(join(fakeHome, '.pi', 'agent', 'extensions', 'orca-agent-status.ts'))
|
||||
).toBe(true)
|
||||
expect(
|
||||
existsSync(join(fakeHome, '.omp', 'agent', 'extensions', 'orca-agent-status.ts'))
|
||||
).toBe(false)
|
||||
} finally {
|
||||
homedirOverride.current = ''
|
||||
rmSync(fakeHome, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('launching omp with both ~/.pi/agent and ~/.omp/agent present mirrors ~/.omp/agent into omp-agent-overlays', () => {
|
||||
it('launching omp with both ~/.pi/agent and ~/.omp/agent present installs into ~/.omp/agent', () => {
|
||||
const fakeHome = mkdtempSync(join(tmpdir(), 'orca-omp-both-'))
|
||||
seedAgentDir(fakeHome, '.pi', 'pi')
|
||||
seedAgentDir(fakeHome, '.omp', 'omp')
|
||||
|
|
@ -364,28 +328,20 @@ describe('PiTitlebarExtensionService', () => {
|
|||
const svc = new PiTitlebarExtensionService()
|
||||
const env = svc.buildPtyEnv('pty-omp-both', undefined, 'omp')
|
||||
|
||||
// Critical regression guard for "OMP is its own program with its own
|
||||
// paths": OMP overlays live under userData/omp-agent-overlays, NEVER
|
||||
// under userData/pi-agent-overlays. A future refactor that re-shares
|
||||
// the Pi overlay root for OMP would re-introduce cross-agent state
|
||||
// visibility this PR exists to prevent.
|
||||
expect(env.PI_CODING_AGENT_DIR).toBe(overlayPath('omp', join(fakeHome, '.omp', 'agent')))
|
||||
// CRITICAL regression guard: even though ~/.pi/agent exists, the OMP
|
||||
// launch MUST resolve OMP's own source dir, not Pi's.
|
||||
expect(readFileSync(join(env.PI_CODING_AGENT_DIR!, 'auth.json'), 'utf-8')).toBe(
|
||||
'omp secret token'
|
||||
expect(env.PI_CODING_AGENT_DIR).toBeUndefined()
|
||||
expect(env.ORCA_OMP_SOURCE_AGENT_DIR).toBe(join(fakeHome, '.omp', 'agent'))
|
||||
expect(env.ORCA_OMP_STATUS_EXTENSION).toBe(
|
||||
join(fakeHome, '.omp', 'agent', 'extensions', 'orca-agent-status.ts')
|
||||
)
|
||||
const overlayExtensions = readdirSync(join(env.PI_CODING_AGENT_DIR!, 'extensions')).sort()
|
||||
expect(overlayExtensions).toContain('omp-ext')
|
||||
expect(overlayExtensions).not.toContain('pi-ext')
|
||||
expect(
|
||||
readFileSync(
|
||||
join(env.PI_CODING_AGENT_DIR!, 'extensions', 'orca-agent-status.ts'),
|
||||
join(fakeHome, '.omp', 'agent', 'extensions', 'orca-agent-status.ts'),
|
||||
'utf-8'
|
||||
)
|
||||
).toContain('/hook/omp')
|
||||
// Pi's overlay root MUST NOT have been touched by the OMP launch.
|
||||
expect(existsSync(ptyOverlayPath('pi', 'pty-omp-both'))).toBe(false)
|
||||
expect(
|
||||
existsSync(join(fakeHome, '.pi', 'agent', 'extensions', 'orca-agent-status.ts'))
|
||||
).toBe(false)
|
||||
} finally {
|
||||
homedirOverride.current = ''
|
||||
rmSync(fakeHome, { recursive: true, force: true })
|
||||
|
|
@ -405,24 +361,16 @@ describe('PiTitlebarExtensionService', () => {
|
|||
const svc = new PiTitlebarExtensionService()
|
||||
const env = svc.buildPtyEnv('pty-omp-empty', undefined, 'omp')
|
||||
|
||||
expect(env.PI_CODING_AGENT_DIR).toBe(overlayPath('omp', join(fakeHome, '.omp', 'agent')))
|
||||
// The Pi-only home must NOT leak into the OMP overlay; the auth
|
||||
// token from ~/.pi/agent/auth.json must be absent.
|
||||
expect(existsSync(join(env.PI_CODING_AGENT_DIR!, 'auth.json'))).toBe(false)
|
||||
// Only Orca's bundled extensions are present — no user extensions
|
||||
// from the other agent's dir.
|
||||
const overlayExtensions = readdirSync(join(env.PI_CODING_AGENT_DIR!, 'extensions')).sort()
|
||||
expect(overlayExtensions).toEqual([
|
||||
const ompAgentDir = join(fakeHome, '.omp', 'agent')
|
||||
expect(env.PI_CODING_AGENT_DIR).toBeUndefined()
|
||||
expect(env.ORCA_OMP_SOURCE_AGENT_DIR).toBe(ompAgentDir)
|
||||
expect(existsSync(join(ompAgentDir, 'auth.json'))).toBe(false)
|
||||
const extensions = readdirSync(join(ompAgentDir, 'extensions')).sort()
|
||||
expect(extensions).toEqual([
|
||||
'orca-agent-status.ts',
|
||||
'orca-prefill.ts',
|
||||
'orca-titlebar-spinner.ts'
|
||||
])
|
||||
expect(
|
||||
JSON.parse(readFileSync(join(env.PI_CODING_AGENT_DIR!, 'settings.json'), 'utf-8'))
|
||||
).toEqual({
|
||||
hideThinkingBlock: true,
|
||||
terminal: { clearOnShrink: true }
|
||||
})
|
||||
} finally {
|
||||
homedirOverride.current = ''
|
||||
rmSync(fakeHome, { recursive: true, force: true })
|
||||
|
|
|
|||
|
|
@ -1,14 +1,6 @@
|
|||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
realpathSync,
|
||||
statSync,
|
||||
writeFileSync
|
||||
} from 'fs'
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
||||
import { homedir } from 'os'
|
||||
import { basename, join } from 'path'
|
||||
import { join } from 'path'
|
||||
import { app } from 'electron'
|
||||
import { createHash } from 'crypto'
|
||||
import {
|
||||
|
|
@ -23,15 +15,8 @@ export { ORCA_OMP_PREFILL_ENV_VAR, ORCA_PI_PREFILL_ENV_VAR } from './prefill-ext
|
|||
import { ORCA_PI_EXTENSION_FILE, getPiTitlebarExtensionSource } from './titlebar-extension-source'
|
||||
import {
|
||||
isSafeDescendCandidate as sharedIsSafeDescendCandidate,
|
||||
mirrorEntry,
|
||||
safeRemoveOverlay,
|
||||
safeRemoveTree
|
||||
safeRemoveOverlay
|
||||
} from '../pty/overlay-mirror'
|
||||
import {
|
||||
isOmpPersistentSqliteEntry,
|
||||
mirrorOmpPersistentSqliteFiles
|
||||
} from '../pty/omp-sqlite-overlay'
|
||||
import { mergePiOverlayUiSettings } from '../../shared/pi-overlay-ui-settings'
|
||||
import type { PiAgentKind } from '../../shared/pi-agent-kind'
|
||||
|
||||
// Why: the Pi test suite imports `isSafeDescendCandidate` from this module's
|
||||
|
|
@ -41,23 +26,25 @@ import type { PiAgentKind } from '../../shared/pi-agent-kind'
|
|||
export const isSafeDescendCandidate = sharedIsSafeDescendCandidate
|
||||
|
||||
const PI_AGENT_SUBDIR = 'agent'
|
||||
const PI_AGENT_SETTINGS_FILE = 'settings.json'
|
||||
const PI_OVERLAY_MANIFEST_FILE = '.orca-pi-overlay-manifest.json'
|
||||
const ORCA_MANAGED_EXTENSION_MARKER = '@orca-managed-pi-extension'
|
||||
|
||||
type PiOverlayManifest = {
|
||||
topLevelEntries: string[]
|
||||
extensionEntries: string[]
|
||||
type ManagedExtensionWriteResult = 'written' | 'skipped-user-owned' | 'failed'
|
||||
|
||||
type PiManagedExtensionEnv = {
|
||||
extensionDir?: string
|
||||
sourceAgentDir: string
|
||||
statusExtensionPath?: string
|
||||
}
|
||||
|
||||
// Why: each agent owns its own overlay tree so OMP launches never touch
|
||||
// Pi's overlay dir (and vice versa). Shadowing one inside the other would
|
||||
// re-introduce the cross-agent state leak the per-kind PR exists to prevent.
|
||||
// Why: old Orca versions used per-kind overlay roots. Keep the names so
|
||||
// upgrade-time cleanup can remove stale PTY-scoped Pi/OMP overlay dirs without
|
||||
// guessing which agent a terminated pane launched.
|
||||
const OVERLAY_ROOT_DIR_NAME: Record<PiAgentKind, string> = {
|
||||
pi: 'pi-agent-overlays',
|
||||
omp: 'omp-agent-overlays'
|
||||
}
|
||||
|
||||
// Why: the overlay source dir is chosen by which agent is being launched, NOT
|
||||
// Why: the managed extension target is chosen by which agent is being launched, NOT
|
||||
// by which `~/.<agent>/agent` dir happens to exist on disk first. A
|
||||
// cross-agent fallback (Pi -> OMP or vice versa) silently shadows the other
|
||||
// agent's user extensions when both are installed and the user picks the
|
||||
|
|
@ -75,18 +62,17 @@ function toSafeOverlayDirName(ptyId: string): string {
|
|||
return createHash('sha256').update(ptyId).digest('hex').slice(0, 32)
|
||||
}
|
||||
|
||||
function withOrcaManagedExtensionMarker(source: string): string {
|
||||
return source.includes(ORCA_MANAGED_EXTENSION_MARKER)
|
||||
? source
|
||||
: `// ${ORCA_MANAGED_EXTENSION_MARKER}\n${source}`
|
||||
}
|
||||
|
||||
export class PiTitlebarExtensionService {
|
||||
private getOverlayRoot(kind: PiAgentKind): string {
|
||||
return join(app.getPath('userData'), OVERLAY_ROOT_DIR_NAME[kind])
|
||||
}
|
||||
|
||||
private getSourceOverlayDir(sourceAgentDir: string, kind: PiAgentKind): string {
|
||||
// Why: PI_CODING_AGENT_DIR is Pi's whole mutable home. Scope overlays to
|
||||
// the source home, not a PTY, so Orca Pi terminals share config/session
|
||||
// state while still avoiding writes to the user's real agent dir.
|
||||
return join(this.getOverlayRoot(kind), toSafeOverlayDirName(`source:${sourceAgentDir}`))
|
||||
}
|
||||
|
||||
private getPtyOverlayDir(ptyId: string, kind: PiAgentKind): string {
|
||||
// Why: old Orca versions used PTY-scoped hashed overlays. Keep resolving
|
||||
// that path so new spawns/teardowns can clean stale pre-migration dirs.
|
||||
|
|
@ -104,143 +90,57 @@ export class PiTitlebarExtensionService {
|
|||
safeRemoveOverlay(overlayDir, this.getOverlayRoot(kind))
|
||||
}
|
||||
|
||||
private readOverlayManifest(overlayDir: string): PiOverlayManifest {
|
||||
private canOverwriteManagedExtension(path: string): boolean {
|
||||
try {
|
||||
const parsed = JSON.parse(
|
||||
readFileSync(join(overlayDir, PI_OVERLAY_MANIFEST_FILE), 'utf8')
|
||||
) as Partial<PiOverlayManifest>
|
||||
return {
|
||||
topLevelEntries: Array.isArray(parsed.topLevelEntries) ? parsed.topLevelEntries : [],
|
||||
extensionEntries: Array.isArray(parsed.extensionEntries) ? parsed.extensionEntries : []
|
||||
}
|
||||
return readFileSync(path, 'utf8').includes(ORCA_MANAGED_EXTENSION_MARKER)
|
||||
} catch {
|
||||
return { topLevelEntries: [], extensionEntries: [] }
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private writeOverlayManifest(overlayDir: string, manifest: PiOverlayManifest): void {
|
||||
writeFileSync(
|
||||
join(overlayDir, PI_OVERLAY_MANIFEST_FILE),
|
||||
`${JSON.stringify(manifest, null, 2)}\n`
|
||||
)
|
||||
}
|
||||
|
||||
private clearManifestEntries(overlayDir: string, manifest: PiOverlayManifest): void {
|
||||
for (const entryName of manifest.topLevelEntries) {
|
||||
safeRemoveTree(join(overlayDir, entryName))
|
||||
}
|
||||
|
||||
const overlayExtensionsDir = join(overlayDir, 'extensions')
|
||||
for (const entryName of manifest.extensionEntries) {
|
||||
safeRemoveTree(join(overlayExtensionsDir, entryName))
|
||||
}
|
||||
}
|
||||
|
||||
private mirrorAgentDir(sourceAgentDir: string, overlayDir: string, kind: PiAgentKind): void {
|
||||
const previousManifest = this.readOverlayManifest(overlayDir)
|
||||
this.clearManifestEntries(overlayDir, previousManifest)
|
||||
|
||||
const nextManifest: PiOverlayManifest = { topLevelEntries: [], extensionEntries: [] }
|
||||
|
||||
if (!existsSync(sourceAgentDir)) {
|
||||
if (kind === 'omp') {
|
||||
nextManifest.topLevelEntries.push(
|
||||
...mirrorOmpPersistentSqliteFiles(sourceAgentDir, overlayDir)
|
||||
)
|
||||
}
|
||||
this.writeOverlayManifest(overlayDir, nextManifest)
|
||||
return
|
||||
}
|
||||
|
||||
for (const entry of readdirSync(sourceAgentDir, { withFileTypes: true })) {
|
||||
const sourcePath = join(sourceAgentDir, entry.name)
|
||||
|
||||
if (entry.name === PI_AGENT_SETTINGS_FILE) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (kind === 'omp' && isOmpPersistentSqliteEntry(entry.name)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (entry.name === 'extensions') {
|
||||
const isSymlink = entry.isSymbolicLink()
|
||||
let isLinkPointingToDir = false
|
||||
if (isSymlink) {
|
||||
try {
|
||||
isLinkPointingToDir = statSync(sourcePath).isDirectory()
|
||||
} catch {
|
||||
isLinkPointingToDir = false
|
||||
}
|
||||
}
|
||||
|
||||
if (!entry.isDirectory() && !isLinkPointingToDir) {
|
||||
mirrorEntry(sourcePath, join(overlayDir, basename(sourcePath)))
|
||||
nextManifest.topLevelEntries.push(entry.name)
|
||||
continue
|
||||
}
|
||||
|
||||
// Why: `extensions/` must be a real overlay directory so Orca's
|
||||
// bundled files are written only into userData, never through a user
|
||||
// symlink/junction that points at their real extension store.
|
||||
const resolvedSource = isLinkPointingToDir ? realpathSync(sourcePath) : sourcePath
|
||||
const overlayExtensionsDir = join(overlayDir, 'extensions')
|
||||
mkdirSync(overlayExtensionsDir, { recursive: true })
|
||||
for (const extensionEntry of readdirSync(resolvedSource, { withFileTypes: true })) {
|
||||
if (
|
||||
extensionEntry.name === ORCA_PI_EXTENSION_FILE ||
|
||||
extensionEntry.name === ORCA_PI_PREFILL_EXTENSION_FILE ||
|
||||
extensionEntry.name === ORCA_PI_AGENT_STATUS_EXTENSION_FILE
|
||||
) {
|
||||
continue
|
||||
}
|
||||
mirrorEntry(
|
||||
join(resolvedSource, extensionEntry.name),
|
||||
join(overlayExtensionsDir, extensionEntry.name)
|
||||
)
|
||||
nextManifest.extensionEntries.push(extensionEntry.name)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Why: PI_CODING_AGENT_DIR controls Pi's / OMP's entire state tree, not
|
||||
// just extension discovery. Mirror the user's top-level resources into
|
||||
// the overlay so enabling Orca's titlebar extension preserves auth,
|
||||
// sessions, skills, prompts, themes, and any future files stored there.
|
||||
mirrorEntry(sourcePath, join(overlayDir, basename(sourcePath)))
|
||||
nextManifest.topLevelEntries.push(entry.name)
|
||||
}
|
||||
|
||||
if (kind === 'omp') {
|
||||
nextManifest.topLevelEntries.push(
|
||||
...mirrorOmpPersistentSqliteFiles(sourceAgentDir, overlayDir)
|
||||
)
|
||||
}
|
||||
|
||||
this.writeOverlayManifest(overlayDir, nextManifest)
|
||||
}
|
||||
|
||||
private readPiSettings(sourceAgentDir: string): unknown {
|
||||
const settingsPath = join(sourceAgentDir, PI_AGENT_SETTINGS_FILE)
|
||||
if (!existsSync(settingsPath)) {
|
||||
return {}
|
||||
private writeManagedExtension(path: string, source: string): ManagedExtensionWriteResult {
|
||||
if (existsSync(path) && !this.canOverwriteManagedExtension(path)) {
|
||||
return 'skipped-user-owned'
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(readFileSync(settingsPath, 'utf8'))
|
||||
writeFileSync(path, source)
|
||||
return 'written'
|
||||
} catch {
|
||||
return {}
|
||||
return 'failed'
|
||||
}
|
||||
}
|
||||
|
||||
private writeOverlaySettings(sourceAgentDir: string, overlayDir: string): void {
|
||||
// Why: settings.json is a real overlay file, not a mirror, so Orca can
|
||||
// apply UI-only safeguards without modifying the user's Pi / OMP config.
|
||||
const settings = mergePiOverlayUiSettings(this.readPiSettings(sourceAgentDir))
|
||||
writeFileSync(
|
||||
join(overlayDir, PI_AGENT_SETTINGS_FILE),
|
||||
`${JSON.stringify(settings, null, 2)}\n`
|
||||
private installManagedExtensions(
|
||||
sourceAgentDir: string,
|
||||
kind: PiAgentKind
|
||||
): PiManagedExtensionEnv {
|
||||
const extensionsDir = join(sourceAgentDir, 'extensions')
|
||||
try {
|
||||
mkdirSync(extensionsDir, { recursive: true })
|
||||
} catch {
|
||||
return { sourceAgentDir }
|
||||
}
|
||||
|
||||
this.writeManagedExtension(
|
||||
join(extensionsDir, ORCA_PI_EXTENSION_FILE),
|
||||
withOrcaManagedExtensionMarker(getPiTitlebarExtensionSource())
|
||||
)
|
||||
this.writeManagedExtension(
|
||||
join(extensionsDir, ORCA_PI_PREFILL_EXTENSION_FILE),
|
||||
withOrcaManagedExtensionMarker(getPiPrefillExtensionSource(kind))
|
||||
)
|
||||
const statusExtensionPath = join(extensionsDir, ORCA_PI_AGENT_STATUS_EXTENSION_FILE)
|
||||
const statusResult = this.writeManagedExtension(
|
||||
statusExtensionPath,
|
||||
withOrcaManagedExtensionMarker(getPiAgentStatusExtensionSource(kind))
|
||||
)
|
||||
|
||||
return {
|
||||
extensionDir: extensionsDir,
|
||||
sourceAgentDir,
|
||||
statusExtensionPath: statusResult === 'written' ? statusExtensionPath : undefined
|
||||
}
|
||||
}
|
||||
|
||||
buildPtyEnv(
|
||||
|
|
@ -249,70 +149,32 @@ export class PiTitlebarExtensionService {
|
|||
kind: PiAgentKind
|
||||
): Record<string, string> {
|
||||
const sourceAgentDir = existingAgentDir || getDefaultPiAgentDir(kind)
|
||||
const overlayDir = this.getSourceOverlayDir(sourceAgentDir, kind)
|
||||
|
||||
try {
|
||||
this.safeRemoveOverlay(this.getPtyOverlayDir(ptyId, kind), kind)
|
||||
this.safeRemoveOverlay(this.getLegacyOverlayDir(ptyId, kind), kind)
|
||||
} catch {
|
||||
// Why: on Windows the overlay directory can be locked by another process
|
||||
// (e.g. antivirus, indexer, or a previous Orca session that didn't clean up).
|
||||
// If we can't remove the stale overlay, fall back to the user's own
|
||||
// agent dir (Pi or OMP - both consume PI_CODING_AGENT_DIR) so the
|
||||
// terminal still spawns - the titlebar spinner is not worth blocking
|
||||
// the PTY.
|
||||
return existingAgentDir ? { PI_CODING_AGENT_DIR: existingAgentDir } : {}
|
||||
// Why: old per-PTY overlay cleanup is best-effort; a locked stale
|
||||
// directory should not prevent the terminal from starting.
|
||||
}
|
||||
|
||||
try {
|
||||
mkdirSync(overlayDir, { recursive: true })
|
||||
this.mirrorAgentDir(sourceAgentDir, overlayDir, kind)
|
||||
this.writeOverlaySettings(sourceAgentDir, overlayDir)
|
||||
|
||||
const extensionsDir = join(overlayDir, 'extensions')
|
||||
mkdirSync(extensionsDir, { recursive: true })
|
||||
// Why: Pi / OMP both auto-load global extensions from
|
||||
// PI_CODING_AGENT_DIR/extensions. Add Orca's titlebar extension alongside
|
||||
// the user's existing extensions instead of replacing that directory,
|
||||
// otherwise Orca terminals would silently disable the user's
|
||||
// customization inside Orca only.
|
||||
safeRemoveTree(join(extensionsDir, ORCA_PI_EXTENSION_FILE))
|
||||
writeFileSync(join(extensionsDir, ORCA_PI_EXTENSION_FILE), getPiTitlebarExtensionSource())
|
||||
safeRemoveTree(join(extensionsDir, ORCA_PI_PREFILL_EXTENSION_FILE))
|
||||
writeFileSync(
|
||||
join(extensionsDir, ORCA_PI_PREFILL_EXTENSION_FILE),
|
||||
getPiPrefillExtensionSource(kind)
|
||||
)
|
||||
// Why: bundled status extension that bridges the in-process event API
|
||||
// (`pi.on('agent_start', ...)` etc., identical between Pi and OMP) to the
|
||||
// unified /hook/<kind> endpoint. Without this, panes would have no entry in
|
||||
// agentStatusByPaneKey and the dashboard would fall back to terminal-title
|
||||
// heuristics like any uninstrumented CLI.
|
||||
safeRemoveTree(join(extensionsDir, ORCA_PI_AGENT_STATUS_EXTENSION_FILE))
|
||||
writeFileSync(
|
||||
join(extensionsDir, ORCA_PI_AGENT_STATUS_EXTENSION_FILE),
|
||||
getPiAgentStatusExtensionSource(kind)
|
||||
)
|
||||
} catch {
|
||||
// Why: overlay creation is best-effort - permission errors (EPERM/EACCES)
|
||||
// on Windows can occur when the userData directory is restricted or when
|
||||
// symlink/junction creation fails without developer mode. Fall back to
|
||||
// the user's own agent dir (Pi or OMP) so the terminal spawns without
|
||||
// the Orca extension.
|
||||
this.clearPty(ptyId)
|
||||
return existingAgentDir ? { PI_CODING_AGENT_DIR: existingAgentDir } : {}
|
||||
}
|
||||
|
||||
return {
|
||||
PI_CODING_AGENT_DIR: overlayDir
|
||||
const installed = this.installManagedExtensions(sourceAgentDir, kind)
|
||||
const env: Record<string, string> = {}
|
||||
if (kind === 'omp') {
|
||||
env.ORCA_OMP_SOURCE_AGENT_DIR = installed.sourceAgentDir
|
||||
if (installed.statusExtensionPath) {
|
||||
env.ORCA_OMP_STATUS_EXTENSION = installed.statusExtensionPath
|
||||
}
|
||||
} else {
|
||||
env.ORCA_PI_SOURCE_AGENT_DIR = installed.sourceAgentDir
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
clearPty(ptyId: string): void {
|
||||
// Why: PTY teardown doesn't know which kind was launched (the daemon
|
||||
// exit path discards the launch command). Sweep both old PTY-scoped
|
||||
// overlay roots for migration cleanup, but leave source-scoped overlays
|
||||
// alive because another Pi terminal may be using the same source home.
|
||||
// overlay roots for migration cleanup. Source-scoped legacy overlays are
|
||||
// deliberately left in place so upgrades never delete user runtime state.
|
||||
for (const kind of Object.keys(OVERLAY_ROOT_DIR_NAME) as PiAgentKind[]) {
|
||||
try {
|
||||
this.safeRemoveOverlay(this.getPtyOverlayDir(ptyId, kind), kind)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ export function getPiTitlebarExtensionSource(): string {
|
|||
'}',
|
||||
'',
|
||||
'export default function (pi) {',
|
||||
' if (!process.env.ORCA_PANE_KEY) return',
|
||||
' let timer = null',
|
||||
' let frameIndex = 0',
|
||||
'',
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ describe('PowerShell OSC 133 bootstrap', () => {
|
|||
|
||||
expect(script).toContain('[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()')
|
||||
expect(script).toContain('ORCA_OPENCODE_CONFIG_DIR')
|
||||
expect(script).toContain('ORCA_PI_CODING_AGENT_DIR')
|
||||
expect(script).toContain('ORCA_OMP_CODING_AGENT_DIR')
|
||||
expect(script).not.toContain('ORCA_PI_CODING_AGENT_DIR')
|
||||
expect(script).not.toContain('ORCA_OMP_CODING_AGENT_DIR')
|
||||
expect(script).toContain('ORCA_OMP_STATUS_EXTENSION')
|
||||
expect(script).toContain('function Global:omp')
|
||||
expect(script).toContain('--extension $env:ORCA_OMP_STATUS_EXTENSION')
|
||||
|
|
|
|||
|
|
@ -24,10 +24,6 @@ try {
|
|||
|
||||
# Profiles can re-export user defaults after Orca's spawn env is set.
|
||||
if ($env:ORCA_OPENCODE_CONFIG_DIR) { $env:OPENCODE_CONFIG_DIR = $env:ORCA_OPENCODE_CONFIG_DIR }
|
||||
if ($env:ORCA_PI_CODING_AGENT_DIR) { $env:PI_CODING_AGENT_DIR = $env:ORCA_PI_CODING_AGENT_DIR }
|
||||
if (-not $env:ORCA_PI_CODING_AGENT_DIR -and $env:ORCA_OMP_CODING_AGENT_DIR) {
|
||||
$env:PI_CODING_AGENT_DIR = $env:ORCA_OMP_CODING_AGENT_DIR
|
||||
}
|
||||
${getPowerShellOmpShellWrapper()}
|
||||
if ($env:ORCA_CODEX_HOME) { $env:CODEX_HOME = $env:ORCA_CODEX_HOME }
|
||||
|
||||
|
|
|
|||
|
|
@ -464,13 +464,12 @@ export class LocalPtyProvider implements IPtyProvider {
|
|||
}
|
||||
}
|
||||
if (!wslInfo && process.platform !== 'win32') {
|
||||
// Why: any Orca-injected overlay env that user rc files can clobber
|
||||
// needs the wrapper so the post-rc restore line runs.
|
||||
// Why: OpenCode/Codex path restoration and OMP's typed-command status
|
||||
// wrapper need shell-ready code after user startup files run.
|
||||
const needsNoMarkerWrapper =
|
||||
finalEnv.ORCA_ATTRIBUTION_SHIM_DIR ||
|
||||
finalEnv.ORCA_OPENCODE_CONFIG_DIR ||
|
||||
finalEnv.ORCA_PI_CODING_AGENT_DIR ||
|
||||
finalEnv.ORCA_OMP_CODING_AGENT_DIR ||
|
||||
finalEnv.ORCA_OMP_STATUS_EXTENSION ||
|
||||
finalEnv.ORCA_CODEX_HOME ||
|
||||
finalEnv.ORCA_AGENT_TEAMS_SHIM_DIR
|
||||
const isCodexStartupCommand =
|
||||
|
|
|
|||
|
|
@ -330,7 +330,7 @@ describePosix('local PTY shell-ready launch config', () => {
|
|||
expect(zlogin).toContain('== "user:__orca_prompt_mark"')
|
||||
})
|
||||
|
||||
it('writes wrappers that restore agent config homes after user startup files', async () => {
|
||||
it('writes wrappers without restoring Pi/OMP homes after user startup files', async () => {
|
||||
const { getBashShellReadyRcfileContent, getShellReadyLaunchConfig } =
|
||||
await importFreshLocalPtyShellReady()
|
||||
|
||||
|
|
@ -341,29 +341,25 @@ describePosix('local PTY shell-ready launch config', () => {
|
|||
const bashRc = getBashShellReadyRcfileContent()
|
||||
const restoreLine =
|
||||
'[[ -n "${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="${ORCA_OPENCODE_CONFIG_DIR}"'
|
||||
const piRestoreLine =
|
||||
'[[ -n "${ORCA_PI_CODING_AGENT_DIR:-}" ]] && export PI_CODING_AGENT_DIR="${ORCA_PI_CODING_AGENT_DIR}"'
|
||||
const codexRestoreLine =
|
||||
'[[ -n "${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="${ORCA_CODEX_HOME}"'
|
||||
const agentTeamsPathRestoreLine = '[[ -n "${ORCA_AGENT_TEAMS_SHIM_DIR:-}" ]] || return 0'
|
||||
const ompRestoreLine =
|
||||
'if [[ -z "${ORCA_PI_CODING_AGENT_DIR:-}" && -n "${ORCA_OMP_CODING_AGENT_DIR:-}" ]]; then'
|
||||
const ompWrapperLine = 'command omp --extension "${ORCA_OMP_STATUS_EXTENSION}" "$@"'
|
||||
expect(zshrc).toContain(restoreLine)
|
||||
expect(zlogin).toContain(restoreLine)
|
||||
expect(bashRc).toContain(restoreLine)
|
||||
expect(zshrc).toContain(piRestoreLine)
|
||||
expect(zlogin).toContain(piRestoreLine)
|
||||
expect(bashRc).toContain(piRestoreLine)
|
||||
expect(zshrc).not.toContain('ORCA_PI_CODING_AGENT_DIR')
|
||||
expect(zlogin).not.toContain('ORCA_PI_CODING_AGENT_DIR')
|
||||
expect(bashRc).not.toContain('ORCA_PI_CODING_AGENT_DIR')
|
||||
expect(zshrc).toContain(codexRestoreLine)
|
||||
expect(zlogin).toContain(codexRestoreLine)
|
||||
expect(zshrc).toContain(agentTeamsPathRestoreLine)
|
||||
expect(zlogin).toContain(agentTeamsPathRestoreLine)
|
||||
expect(bashRc).toContain(agentTeamsPathRestoreLine)
|
||||
expect(bashRc).toContain(codexRestoreLine)
|
||||
expect(zshrc).toContain(ompRestoreLine)
|
||||
expect(zlogin).toContain(ompRestoreLine)
|
||||
expect(bashRc).toContain(ompRestoreLine)
|
||||
expect(zshrc).not.toContain('ORCA_OMP_CODING_AGENT_DIR')
|
||||
expect(zlogin).not.toContain('ORCA_OMP_CODING_AGENT_DIR')
|
||||
expect(bashRc).not.toContain('ORCA_OMP_CODING_AGENT_DIR')
|
||||
expect(zshrc).toContain(ompWrapperLine)
|
||||
expect(zlogin).toContain(ompWrapperLine)
|
||||
expect(bashRc).toContain(ompWrapperLine)
|
||||
|
|
|
|||
|
|
@ -175,12 +175,6 @@ __orca_restore_agent_teams_path
|
|||
# Why: user startup files may set the default OpenCode config after Orca's
|
||||
# spawn env; restore the Orca-managed config dir before the first prompt.
|
||||
[[ -n "\${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="\${ORCA_OPENCODE_CONFIG_DIR}"
|
||||
# Why: bare shells carry both Pi and OMP shadows so a later typed OMP can
|
||||
# switch on demand. Keep Pi as the shell default unless this PTY is OMP-only.
|
||||
[[ -n "\${ORCA_PI_CODING_AGENT_DIR:-}" ]] && export PI_CODING_AGENT_DIR="\${ORCA_PI_CODING_AGENT_DIR}"
|
||||
if [[ -z "\${ORCA_PI_CODING_AGENT_DIR:-}" && -n "\${ORCA_OMP_CODING_AGENT_DIR:-}" ]]; then
|
||||
export PI_CODING_AGENT_DIR="\${ORCA_OMP_CODING_AGENT_DIR}"
|
||||
fi
|
||||
${getPosixOmpShellWrapper()}
|
||||
# Why: Codex must keep using Orca's runtime CODEX_HOME after profile scripts.
|
||||
[[ -n "\${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="\${ORCA_CODEX_HOME}"
|
||||
|
|
@ -296,12 +290,6 @@ __orca_restore_agent_teams_path() {
|
|||
if [[ ! -o login ]]; then
|
||||
# Why: ~/.zshrc can export the user's default OpenCode config after spawn.
|
||||
[[ -n "\${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="\${ORCA_OPENCODE_CONFIG_DIR}"
|
||||
# Why: bare shells carry both Pi and OMP shadows; keep Pi as the default and
|
||||
# let the OMP wrapper switch to OMP only for that command.
|
||||
[[ -n "\${ORCA_PI_CODING_AGENT_DIR:-}" ]] && export PI_CODING_AGENT_DIR="\${ORCA_PI_CODING_AGENT_DIR}"
|
||||
if [[ -z "\${ORCA_PI_CODING_AGENT_DIR:-}" && -n "\${ORCA_OMP_CODING_AGENT_DIR:-}" ]]; then
|
||||
export PI_CODING_AGENT_DIR="\${ORCA_OMP_CODING_AGENT_DIR}"
|
||||
fi
|
||||
${getPosixOmpShellWrapper()}
|
||||
# Why: Codex must keep using Orca's runtime CODEX_HOME after rc files.
|
||||
[[ -n "\${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="\${ORCA_CODEX_HOME}"
|
||||
|
|
@ -365,10 +353,6 @@ __orca_restore_agent_teams_path() {
|
|||
__orca_restore_agent_teams_path
|
||||
# Why: .zlogin is the final login startup file before the prompt is shown.
|
||||
[[ -n "\${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="\${ORCA_OPENCODE_CONFIG_DIR}"
|
||||
[[ -n "\${ORCA_PI_CODING_AGENT_DIR:-}" ]] && export PI_CODING_AGENT_DIR="\${ORCA_PI_CODING_AGENT_DIR}"
|
||||
if [[ -z "\${ORCA_PI_CODING_AGENT_DIR:-}" && -n "\${ORCA_OMP_CODING_AGENT_DIR:-}" ]]; then
|
||||
export PI_CODING_AGENT_DIR="\${ORCA_OMP_CODING_AGENT_DIR}"
|
||||
fi
|
||||
${getPosixOmpShellWrapper()}
|
||||
[[ -n "\${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="\${ORCA_CODEX_HOME}"
|
||||
${getZshShellReadyMarkerRegistrationBlock(SHELL_READY_MARKER_ESCAPED)}
|
||||
|
|
|
|||
|
|
@ -41,26 +41,21 @@ describe('resolveWindowsShellLaunchArgs', () => {
|
|||
const opencodeRestoreIndex = command.indexOf(
|
||||
'$env:OPENCODE_CONFIG_DIR = $env:ORCA_OPENCODE_CONFIG_DIR'
|
||||
)
|
||||
const piRestoreIndex = command.indexOf(
|
||||
'$env:PI_CODING_AGENT_DIR = $env:ORCA_PI_CODING_AGENT_DIR'
|
||||
)
|
||||
const ompRestoreIndex = command.indexOf(
|
||||
'$env:PI_CODING_AGENT_DIR = $env:ORCA_OMP_CODING_AGENT_DIR'
|
||||
)
|
||||
const ompSourceConfigIndex = command.indexOf(
|
||||
'$env:PI_CODING_AGENT_DIR = $env:ORCA_OMP_SOURCE_AGENT_DIR'
|
||||
)
|
||||
const ompWrapperIndex = command.indexOf('function Global:omp')
|
||||
const ompExtensionIndex = command.indexOf('--extension $env:ORCA_OMP_STATUS_EXTENSION')
|
||||
const codexRestoreIndex = command.indexOf('$env:CODEX_HOME = $env:ORCA_CODEX_HOME')
|
||||
const promptIndex = command.indexOf('function Global:prompt')
|
||||
|
||||
expect(command).not.toContain('$PROFILE')
|
||||
expect(command).not.toContain('ORCA_PI_CODING_AGENT_DIR')
|
||||
expect(command).not.toContain('ORCA_OMP_CODING_AGENT_DIR')
|
||||
expect(command).not.toContain('$env:PI_CODING_AGENT_DIR = $env:ORCA_OMP_SOURCE_AGENT_DIR')
|
||||
expect(outputEncodingIndex).toBeGreaterThanOrEqual(0)
|
||||
expect(opencodeRestoreIndex).toBeGreaterThan(outputEncodingIndex)
|
||||
expect(piRestoreIndex).toBeGreaterThan(outputEncodingIndex)
|
||||
expect(ompRestoreIndex).toBeGreaterThan(piRestoreIndex)
|
||||
expect(ompSourceConfigIndex).toBeGreaterThan(ompRestoreIndex)
|
||||
expect(ompWrapperIndex).toBeGreaterThan(opencodeRestoreIndex)
|
||||
expect(ompExtensionIndex).toBeGreaterThan(ompWrapperIndex)
|
||||
expect(codexRestoreIndex).toBeGreaterThan(outputEncodingIndex)
|
||||
expect(codexRestoreIndex).toBeGreaterThan(ompRestoreIndex)
|
||||
expect(codexRestoreIndex).toBeGreaterThan(ompWrapperIndex)
|
||||
expect(promptIndex).toBeGreaterThan(codexRestoreIndex)
|
||||
expect(command).toContain('Esc = [char]27')
|
||||
expect(command).toContain('Bel = [char]7')
|
||||
|
|
|
|||
|
|
@ -118,10 +118,11 @@ describePosix('OMP shell wrapper node-pty reproduction', () => {
|
|||
...process.env,
|
||||
HOME: tempDir,
|
||||
PATH: `${binDir}:${process.env.PATH ?? ''}`,
|
||||
PI_CODING_AGENT_DIR: piDir,
|
||||
ORCA_PI_CODING_AGENT_DIR: piDir,
|
||||
ORCA_OMP_CODING_AGENT_DIR: ompDir,
|
||||
PI_CODING_AGENT_DIR: '',
|
||||
ORCA_PI_CODING_AGENT_DIR: '',
|
||||
ORCA_OMP_CODING_AGENT_DIR: '',
|
||||
ORCA_OMP_STATUS_EXTENSION: statusExtension,
|
||||
ORCA_FAKE_OMP_DEFAULT_DIR: ompDir,
|
||||
ORCA_CAPTURE_FILE: captureFile,
|
||||
ORCA_AFTER_PI_FILE: afterPiFile,
|
||||
TERM: process.env.TERM || 'xterm-256color'
|
||||
|
|
@ -140,7 +141,8 @@ exit 0
|
|||
})
|
||||
|
||||
const unwrapped = readFileSync(unwrappedCapture, 'utf8')
|
||||
expect(unwrapped).toContain(`PI=${piDir}`)
|
||||
expect(unwrapped).toContain('PI=\n')
|
||||
expect(unwrapped).toContain(`EFFECTIVE=${ompDir}`)
|
||||
expect(unwrapped).toContain('ARG1=ask')
|
||||
expect(unwrapped).not.toContain('ARG1=--extension')
|
||||
|
||||
|
|
@ -148,8 +150,7 @@ exit 0
|
|||
const wrappedAfterPi = join(tempDir, 'wrapped-after-pi')
|
||||
const wrappedOutput = await runInteractiveBashPty({
|
||||
cwd: tempDir,
|
||||
rcfileContent: `[[ -n "\${ORCA_PI_CODING_AGENT_DIR:-}" ]] && export PI_CODING_AGENT_DIR="\${ORCA_PI_CODING_AGENT_DIR}"
|
||||
${getPosixOmpShellWrapper()}`,
|
||||
rcfileContent: getPosixOmpShellWrapper(),
|
||||
env: makeEnv(wrappedCapture, wrappedAfterPi),
|
||||
input: `type omp
|
||||
omp ask
|
||||
|
|
@ -160,19 +161,19 @@ exit 0
|
|||
|
||||
const wrapped = readFileSync(wrappedCapture, 'utf8')
|
||||
expect(wrappedOutput).toContain('omp is a function')
|
||||
expect(wrapped).toContain(`PI=${ompDir}`)
|
||||
expect(wrapped).toContain('PI=\n')
|
||||
expect(wrapped).toContain(`EFFECTIVE=${ompDir}`)
|
||||
expect(wrapped).toContain('ARG1=--extension')
|
||||
expect(wrapped).toContain(`ARG2=${statusExtension}`)
|
||||
expect(wrapped).toContain('ARG3=ask')
|
||||
expect(readFileSync(wrappedAfterPi, 'utf8')).toBe(piDir)
|
||||
expect(readFileSync(wrappedAfterPi, 'utf8')).toBe('')
|
||||
})
|
||||
|
||||
itWithBash('runs OMP config subcommands against the source home, not the overlay', async () => {
|
||||
itWithBash('runs OMP config subcommands without redirecting the home', async () => {
|
||||
const tempDir = makeTempDir()
|
||||
const binDir = join(tempDir, 'bin')
|
||||
const sourceDir = join(tempDir, 'source-omp-agent')
|
||||
const overlayDir = join(tempDir, 'overlay-omp-agent')
|
||||
const extensionDir = join(overlayDir, 'extensions')
|
||||
const extensionDir = join(sourceDir, 'extensions')
|
||||
mkdirSync(binDir)
|
||||
mkdirSync(sourceDir, { recursive: true })
|
||||
mkdirSync(extensionDir, { recursive: true })
|
||||
|
|
@ -183,14 +184,14 @@ exit 0
|
|||
const captureFile = join(tempDir, 'config-capture')
|
||||
await runInteractiveBashPty({
|
||||
cwd: tempDir,
|
||||
rcfileContent: `[[ -n "\${ORCA_OMP_CODING_AGENT_DIR:-}" ]] && export PI_CODING_AGENT_DIR="\${ORCA_OMP_CODING_AGENT_DIR}"
|
||||
${getPosixOmpShellWrapper()}`,
|
||||
rcfileContent: getPosixOmpShellWrapper(),
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: tempDir,
|
||||
PATH: `${binDir}:${process.env.PATH ?? ''}`,
|
||||
PI_CODING_AGENT_DIR: overlayDir,
|
||||
ORCA_OMP_CODING_AGENT_DIR: overlayDir,
|
||||
PI_CODING_AGENT_DIR: '',
|
||||
ORCA_PI_CODING_AGENT_DIR: '',
|
||||
ORCA_OMP_CODING_AGENT_DIR: '',
|
||||
ORCA_OMP_SOURCE_AGENT_DIR: sourceDir,
|
||||
ORCA_OMP_STATUS_EXTENSION: statusExtension,
|
||||
ORCA_FAKE_OMP_DEFAULT_DIR: sourceDir,
|
||||
|
|
@ -203,11 +204,10 @@ exit 0
|
|||
})
|
||||
|
||||
const capture = readFileSync(captureFile, 'utf8')
|
||||
expect(capture).toContain(`PI=${sourceDir}`)
|
||||
expect(capture).toContain('PI=\n')
|
||||
expect(capture).toContain(`EFFECTIVE=${sourceDir}`)
|
||||
expect(capture).toContain('ARG1=config')
|
||||
expect(readFileSync(join(sourceDir, 'config.yml'), 'utf8')).toBe('updated-by-omp-config\n')
|
||||
expect(() => readFileSync(join(overlayDir, 'config.yml'), 'utf8')).toThrow()
|
||||
})
|
||||
|
||||
itWithBash(
|
||||
|
|
@ -216,8 +216,7 @@ exit 0
|
|||
const tempDir = makeTempDir()
|
||||
const binDir = join(tempDir, 'bin')
|
||||
const defaultOmpDir = join(tempDir, '.omp', 'agent')
|
||||
const overlayDir = join(tempDir, 'overlay-omp-agent')
|
||||
const extensionDir = join(overlayDir, 'extensions')
|
||||
const extensionDir = join(defaultOmpDir, 'extensions')
|
||||
mkdirSync(binDir)
|
||||
mkdirSync(defaultOmpDir, { recursive: true })
|
||||
mkdirSync(extensionDir, { recursive: true })
|
||||
|
|
@ -228,14 +227,14 @@ exit 0
|
|||
const captureFile = join(tempDir, 'default-config-capture')
|
||||
await runInteractiveBashPty({
|
||||
cwd: tempDir,
|
||||
rcfileContent: `[[ -n "\${ORCA_OMP_CODING_AGENT_DIR:-}" ]] && export PI_CODING_AGENT_DIR="\${ORCA_OMP_CODING_AGENT_DIR}"
|
||||
${getPosixOmpShellWrapper()}`,
|
||||
rcfileContent: getPosixOmpShellWrapper(),
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: tempDir,
|
||||
PATH: `${binDir}:${process.env.PATH ?? ''}`,
|
||||
PI_CODING_AGENT_DIR: overlayDir,
|
||||
ORCA_OMP_CODING_AGENT_DIR: overlayDir,
|
||||
PI_CODING_AGENT_DIR: '',
|
||||
ORCA_PI_CODING_AGENT_DIR: '',
|
||||
ORCA_OMP_CODING_AGENT_DIR: '',
|
||||
ORCA_OMP_STATUS_EXTENSION: statusExtension,
|
||||
ORCA_FAKE_OMP_DEFAULT_DIR: defaultOmpDir,
|
||||
ORCA_CAPTURE_FILE: captureFile,
|
||||
|
|
@ -252,7 +251,6 @@ exit 0
|
|||
expect(readFileSync(join(defaultOmpDir, 'config.yml'), 'utf8')).toBe(
|
||||
'updated-by-omp-config\n'
|
||||
)
|
||||
expect(() => readFileSync(join(overlayDir, 'config.yml'), 'utf8')).toThrow()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
// Why: OMP 15.x discovers built-in user extensions from ~/.omp/agent, not
|
||||
// PI_CODING_AGENT_DIR/extensions. Orca's status extension must be
|
||||
// passed explicitly when users type `omp` in an existing terminal.
|
||||
// Why: OMP 15.x discovers built-in user extensions from ~/.omp/agent, but a
|
||||
// typed `omp` in an existing terminal still needs Orca's status extension
|
||||
// passed explicitly. Do not redirect PI_CODING_AGENT_DIR here: that variable
|
||||
// is OMP's mutable home, so config/auth/session commands must keep the user's
|
||||
// normal source of truth.
|
||||
|
||||
const OMP_SUBCOMMANDS = [
|
||||
'acp',
|
||||
|
|
@ -26,7 +28,7 @@ const OMP_SUBCOMMANDS = [
|
|||
|
||||
export function getPosixOmpShellWrapper(): string {
|
||||
const subcommands = OMP_SUBCOMMANDS.join('|')
|
||||
return `# Why: OMP does not auto-load Orca's PTY overlay extensions; wrap only
|
||||
return `# Why: OMP does not auto-load Orca's managed status extension; wrap only
|
||||
# interactive launch invocations so subcommands such as \`omp config\` keep
|
||||
# their normal argv shape.
|
||||
__orca_omp_is_subcommand() {
|
||||
|
|
@ -42,25 +44,9 @@ __orca_omp_should_skip_extension() {
|
|||
__orca_omp_is_subcommand "\${1:-}"
|
||||
}
|
||||
__orca_omp() {
|
||||
local __orca_prev_pi="\${PI_CODING_AGENT_DIR-}"
|
||||
local __orca_had_pi=0
|
||||
[[ -n "\${PI_CODING_AGENT_DIR+x}" ]] && __orca_had_pi=1
|
||||
local __orca_use_overlay=1
|
||||
__orca_omp_should_skip_extension "\${1:-}" && __orca_use_overlay=0
|
||||
if [[ $__orca_use_overlay -eq 1 && -n "\${ORCA_OMP_CODING_AGENT_DIR:-}" ]]; then
|
||||
export PI_CODING_AGENT_DIR="\${ORCA_OMP_CODING_AGENT_DIR}"
|
||||
elif [[ $__orca_use_overlay -eq 0 ]]; then
|
||||
# Why: config/editing subcommands mutate OMP's home. Route those to the
|
||||
# user's source home instead of Orca's status-extension runtime overlay.
|
||||
if [[ -n "\${ORCA_OMP_SOURCE_AGENT_DIR:-}" ]]; then
|
||||
export PI_CODING_AGENT_DIR="\${ORCA_OMP_SOURCE_AGENT_DIR}"
|
||||
else
|
||||
unset PI_CODING_AGENT_DIR
|
||||
fi
|
||||
fi
|
||||
|
||||
local __orca_status=0
|
||||
if [[ $__orca_use_overlay -eq 1 && -n "\${ORCA_OMP_STATUS_EXTENSION:-}" && -f "\${ORCA_OMP_STATUS_EXTENSION}" ]]; then
|
||||
local __orca_use_extension=1
|
||||
__orca_omp_should_skip_extension "\${1:-}" && __orca_use_extension=0
|
||||
if [[ $__orca_use_extension -eq 1 && -n "\${ORCA_OMP_STATUS_EXTENSION:-}" && -f "\${ORCA_OMP_STATUS_EXTENSION}" ]]; then
|
||||
if [[ "\${1:-}" == "launch" ]]; then
|
||||
shift
|
||||
command omp launch --extension "\${ORCA_OMP_STATUS_EXTENSION}" "$@"
|
||||
|
|
@ -70,16 +56,8 @@ __orca_omp() {
|
|||
else
|
||||
command omp "$@"
|
||||
fi
|
||||
__orca_status=$?
|
||||
|
||||
if [[ $__orca_had_pi -eq 1 ]]; then
|
||||
export PI_CODING_AGENT_DIR="$__orca_prev_pi"
|
||||
else
|
||||
unset PI_CODING_AGENT_DIR
|
||||
fi
|
||||
return $__orca_status
|
||||
}
|
||||
if [[ -n "\${ORCA_OMP_CODING_AGENT_DIR:-}" || -n "\${ORCA_OMP_STATUS_EXTENSION:-}" ]]; then
|
||||
if [[ -n "\${ORCA_OMP_STATUS_EXTENSION:-}" ]]; then
|
||||
omp() { __orca_omp "$@"; }
|
||||
fi
|
||||
`
|
||||
|
|
@ -87,7 +65,7 @@ fi
|
|||
|
||||
export function getPowerShellOmpShellWrapper(): string {
|
||||
const subcommands = OMP_SUBCOMMANDS.map((value) => `'${value}'`).join(', ')
|
||||
return `# Why: OMP does not auto-load Orca's PTY overlay extensions; wrap only
|
||||
return `# Why: OMP does not auto-load Orca's managed status extension; wrap only
|
||||
# interactive launch invocations so subcommands such as \`omp config\` keep
|
||||
# their normal argv shape.
|
||||
function Global:__OrcaOmpIsSubcommand {
|
||||
|
|
@ -100,29 +78,15 @@ function Global:__OrcaOmpShouldSkipExtension {
|
|||
if (@("help", "--help", "-h", "--version", "-v") -contains $Name) { return $true }
|
||||
return __OrcaOmpIsSubcommand -Name $Name
|
||||
}
|
||||
if ($env:ORCA_OMP_CODING_AGENT_DIR -or $env:ORCA_OMP_STATUS_EXTENSION) {
|
||||
if ($env:ORCA_OMP_STATUS_EXTENSION) {
|
||||
function Global:omp {
|
||||
$orcaPrevPi = $env:PI_CODING_AGENT_DIR
|
||||
$orcaHadPi = Test-Path Env:PI_CODING_AGENT_DIR
|
||||
$orcaUseOverlay = -not (__OrcaOmpShouldSkipExtension -Name ([string]($args[0])))
|
||||
if ($orcaUseOverlay -and $env:ORCA_OMP_CODING_AGENT_DIR) {
|
||||
$env:PI_CODING_AGENT_DIR = $env:ORCA_OMP_CODING_AGENT_DIR
|
||||
} elseif (-not $orcaUseOverlay) {
|
||||
# Why: config/editing subcommands mutate OMP's home. Route those to
|
||||
# the user's source home instead of Orca's runtime overlay.
|
||||
if ($env:ORCA_OMP_SOURCE_AGENT_DIR) {
|
||||
$env:PI_CODING_AGENT_DIR = $env:ORCA_OMP_SOURCE_AGENT_DIR
|
||||
} else {
|
||||
Remove-Item Env:PI_CODING_AGENT_DIR -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
$orcaUseExtension = -not (__OrcaOmpShouldSkipExtension -Name ([string]($args[0])))
|
||||
$orcaStatus = 0
|
||||
$orcaCommand = Get-Command omp -CommandType Application,ExternalScript -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
if (-not $orcaCommand) {
|
||||
Write-Error "omp executable not found"
|
||||
$orcaStatus = 127
|
||||
} elseif ($orcaUseOverlay -and $env:ORCA_OMP_STATUS_EXTENSION -and
|
||||
} elseif ($orcaUseExtension -and $env:ORCA_OMP_STATUS_EXTENSION -and
|
||||
(Test-Path -LiteralPath $env:ORCA_OMP_STATUS_EXTENSION)) {
|
||||
if ($args.Count -gt 0 -and $args[0] -eq "launch") {
|
||||
$orcaLaunchArgs = @($args | Select-Object -Skip 1)
|
||||
|
|
@ -136,11 +100,6 @@ if ($env:ORCA_OMP_CODING_AGENT_DIR -or $env:ORCA_OMP_STATUS_EXTENSION) {
|
|||
$orcaStatus = $LASTEXITCODE
|
||||
}
|
||||
|
||||
if ($orcaHadPi) {
|
||||
$env:PI_CODING_AGENT_DIR = $orcaPrevPi
|
||||
} else {
|
||||
Remove-Item Env:PI_CODING_AGENT_DIR -ErrorAction SilentlyContinue
|
||||
}
|
||||
$global:LASTEXITCODE = $orcaStatus
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,9 +44,9 @@ function prepareShellConfigDirEnv(agentId: string): { ok: true; env?: NodeJS.Pro
|
|||
return null
|
||||
}
|
||||
// Why: each kind owns a distinct ORCA_*_SOURCE_* shadow so a headless commit
|
||||
// run from inside an OMP overlay restores the OMP source dir, never the Pi
|
||||
// one (and vice versa). PI_CODING_AGENT_DIR is the binary-facing var both
|
||||
// kinds emit — see src/main/pi/titlebar-extension-service.ts.
|
||||
// run from inside a legacy OMP overlay restores the OMP source dir, never
|
||||
// the Pi one (and vice versa). PI_CODING_AGENT_DIR is the binary-facing var
|
||||
// both kinds consume — see src/main/pi/titlebar-extension-service.ts.
|
||||
const sourceVar =
|
||||
agentId === 'opencode'
|
||||
? 'ORCA_OPENCODE_SOURCE_CONFIG_DIR'
|
||||
|
|
|
|||
|
|
@ -70,12 +70,24 @@ describe('PluginOverlayManager', () => {
|
|||
expect(manager.materializeOpenCode('tab-missing:0', join(homeDir, 'missing'))).toBeNull()
|
||||
})
|
||||
|
||||
it('materializes Pi extension into <overlay>/extensions/<file>', () => {
|
||||
it('installs Pi extension into the real agent extensions dir', () => {
|
||||
manager.setSources({ piExtensionSource: '// pi extension' })
|
||||
const dir = manager.materializePi('tab-2:0')
|
||||
expect(dir).not.toBeNull()
|
||||
const file = join(dir!, 'extensions', 'orca-agent-status.ts')
|
||||
expect(existsSync(file)).toBe(true)
|
||||
expect(readFileSync(file, 'utf8')).toContain('@orca-managed-pi-extension')
|
||||
})
|
||||
|
||||
it("does not overwrite a user's same-named remote Pi extension file", () => {
|
||||
const piAgentDir = join(homeDir, '.pi', 'agent')
|
||||
const extensionFile = join(piAgentDir, 'extensions', 'orca-agent-status.ts')
|
||||
mkdirSync(join(piAgentDir, 'extensions'), { recursive: true })
|
||||
writeFileSync(extensionFile, 'user-owned remote status extension')
|
||||
|
||||
manager.setSources({ piExtensionSource: '// pi extension' })
|
||||
expect(manager.materializePi('tab-user-owned-pi:0')).toBeNull()
|
||||
expect(readFileSync(extensionFile, 'utf8')).toBe('user-owned remote status extension')
|
||||
})
|
||||
|
||||
it('uses the kind-specific Pi-compatible extension source when available', () => {
|
||||
|
|
@ -89,15 +101,15 @@ describe('PluginOverlayManager', () => {
|
|||
|
||||
expect(piDir).not.toBeNull()
|
||||
expect(ompDir).not.toBeNull()
|
||||
expect(readFileSync(join(piDir!, 'extensions', 'orca-agent-status.ts'), 'utf8')).toBe(
|
||||
expect(readFileSync(join(piDir!, 'extensions', 'orca-agent-status.ts'), 'utf8')).toContain(
|
||||
'// pi extension'
|
||||
)
|
||||
expect(readFileSync(join(ompDir!, 'extensions', 'orca-agent-status.ts'), 'utf8')).toBe(
|
||||
expect(readFileSync(join(ompDir!, 'extensions', 'orca-agent-status.ts'), 'utf8')).toContain(
|
||||
'// omp extension'
|
||||
)
|
||||
})
|
||||
|
||||
it('mirrors the remote default Pi agent dir before adding Orca status extension', () => {
|
||||
it('installs Orca status extension into the remote default Pi agent dir', () => {
|
||||
const piAgentDir = join(homeDir, '.pi', 'agent')
|
||||
mkdirSync(join(piAgentDir, 'skills', 'my-skill'), { recursive: true })
|
||||
mkdirSync(join(piAgentDir, 'extensions', 'user-ext'), { recursive: true })
|
||||
|
|
@ -133,10 +145,10 @@ describe('PluginOverlayManager', () => {
|
|||
])
|
||||
expect(JSON.parse(readFileSync(join(dir!, 'settings.json'), 'utf8'))).toEqual({
|
||||
defaultProvider: 'amazon-bedrock',
|
||||
hideThinkingBlock: true,
|
||||
hideThinkingBlock: false,
|
||||
terminal: {
|
||||
showImages: false,
|
||||
clearOnShrink: true
|
||||
clearOnShrink: false
|
||||
}
|
||||
})
|
||||
expect(JSON.parse(readFileSync(join(piAgentDir, 'settings.json'), 'utf8'))).toEqual({
|
||||
|
|
@ -164,36 +176,36 @@ describe('PluginOverlayManager', () => {
|
|||
expect(dir).not.toBeNull()
|
||||
expect(readFileSync(join(dir!, 'auth.json'), 'utf8')).toBe('custom token')
|
||||
expect(readFileSync(join(dir!, 'extensions', 'custom.ts'), 'utf8')).toBe('custom extension')
|
||||
expect(readFileSync(join(dir!, 'extensions', 'orca-agent-status.ts'), 'utf8')).toBe(
|
||||
expect(readFileSync(join(dir!, 'extensions', 'orca-agent-status.ts'), 'utf8')).toContain(
|
||||
'// pi extension'
|
||||
)
|
||||
})
|
||||
|
||||
it('source-backs lazy OMP agent.db on the relay', () => {
|
||||
it('leaves lazy OMP agent.db in the real remote home on the relay', () => {
|
||||
manager.setSources({ piExtensionSource: '// pi extension' })
|
||||
const sourceDir = join(homeDir, '.omp', 'agent')
|
||||
const firstDir = manager.materializePi('tab-relay-omp-sqlite:0', undefined, 'omp')
|
||||
|
||||
expect(firstDir).not.toBeNull()
|
||||
expect(firstDir).toBe(sourceDir)
|
||||
const sourcePath = join(sourceDir, 'agent.db')
|
||||
const overlayPath = join(firstDir!, 'agent.db')
|
||||
const content = 'agent.db relay credentials'
|
||||
|
||||
expect(existsSync(sourcePath)).toBe(true)
|
||||
expect(existsSync(overlayPath)).toBe(true)
|
||||
expect(existsSync(sourcePath)).toBe(false)
|
||||
expect(existsSync(join(homeDir, '.orca-relay', 'omp-overlays'))).toBe(false)
|
||||
expect(existsSync(join(sourceDir, 'history.db'))).toBe(false)
|
||||
writeFileSync(overlayPath, content)
|
||||
writeFileSync(sourcePath, content)
|
||||
|
||||
expect(readFileSync(sourcePath, 'utf8')).toBe(content)
|
||||
|
||||
const secondDir = manager.materializePi('tab-relay-omp-sqlite:0', undefined, 'omp')
|
||||
|
||||
expect(secondDir).toBe(firstDir)
|
||||
expect(readFileSync(join(secondDir!, 'agent.db'), 'utf8')).toBe('agent.db relay credentials')
|
||||
expect(readFileSync(join(secondDir!, 'agent.db'), 'utf8')).toBe(content)
|
||||
})
|
||||
|
||||
// Why: per-agent overlay source dir. The renderer picks Pi or OMP per
|
||||
// launch, and the relay must mirror the right `~/.<kind>/agent` source —
|
||||
// Why: per-agent source dir. The renderer picks Pi or OMP per
|
||||
// launch, and the relay must use the right `~/.<kind>/agent` source —
|
||||
// disk-presence guessing (always-Pi or first-exists) shadows the other
|
||||
// agent's user extensions when both dirs exist on the remote disk.
|
||||
describe('per-agent default source dir (no cross-agent fallback)', () => {
|
||||
|
|
@ -205,7 +217,7 @@ describe('PluginOverlayManager', () => {
|
|||
return agentDir
|
||||
}
|
||||
|
||||
it('launching pi with both ~/.pi/agent and ~/.omp/agent present mirrors ~/.pi/agent into pi-overlays', () => {
|
||||
it('launching pi with both ~/.pi/agent and ~/.omp/agent present installs into ~/.pi/agent', () => {
|
||||
seedAgentDir('.pi', 'pi')
|
||||
seedAgentDir('.omp', 'omp')
|
||||
|
||||
|
|
@ -213,15 +225,15 @@ describe('PluginOverlayManager', () => {
|
|||
const dir = manager.materializePi('tab-relay-pi-both:0', undefined, 'pi')
|
||||
|
||||
expect(dir).not.toBeNull()
|
||||
// Pi overlays live under .orca-relay/pi-overlays, separate from OMP's tree.
|
||||
expect(dir!).toMatch(/[\\/]\.orca-relay[\\/]pi-overlays[\\/]/)
|
||||
expect(dir).toBe(join(homeDir, '.pi', 'agent'))
|
||||
expect(readFileSync(join(dir!, 'auth.json'), 'utf8')).toBe('pi token')
|
||||
const overlayExtensions = readdirSync(join(dir!, 'extensions')).sort()
|
||||
expect(overlayExtensions).toContain('pi-ext')
|
||||
expect(overlayExtensions).not.toContain('omp-ext')
|
||||
const extensions = readdirSync(join(dir!, 'extensions')).sort()
|
||||
expect(extensions).toContain('pi-ext')
|
||||
expect(extensions).toContain('orca-agent-status.ts')
|
||||
expect(extensions).not.toContain('omp-ext')
|
||||
})
|
||||
|
||||
it('launching omp with both ~/.pi/agent and ~/.omp/agent present mirrors ~/.omp/agent into omp-overlays', () => {
|
||||
it('launching omp with both ~/.pi/agent and ~/.omp/agent present installs into ~/.omp/agent', () => {
|
||||
seedAgentDir('.pi', 'pi')
|
||||
seedAgentDir('.omp', 'omp')
|
||||
|
||||
|
|
@ -229,23 +241,20 @@ describe('PluginOverlayManager', () => {
|
|||
const dir = manager.materializePi('tab-relay-omp-both:0', undefined, 'omp')
|
||||
|
||||
expect(dir).not.toBeNull()
|
||||
// CRITICAL: OMP overlays live in a distinct subtree (.orca-relay/omp-overlays)
|
||||
// so the remote box never mixes Pi and OMP overlay state for the same paneKey.
|
||||
expect(dir!).toMatch(/[\\/]\.orca-relay[\\/]omp-overlays[\\/]/)
|
||||
expect(dir!).not.toMatch(/[\\/]pi-overlays[\\/]/)
|
||||
expect(dir).toBe(join(homeDir, '.omp', 'agent'))
|
||||
// Even though ~/.pi/agent exists, the OMP launch MUST mirror OMP's
|
||||
// source dir. Cross-agent fallback would silently shadow the user's
|
||||
// OMP extensions on the remote.
|
||||
expect(readFileSync(join(dir!, 'auth.json'), 'utf8')).toBe('omp token')
|
||||
const overlayExtensions = readdirSync(join(dir!, 'extensions')).sort()
|
||||
expect(overlayExtensions).toContain('omp-ext')
|
||||
expect(overlayExtensions).not.toContain('pi-ext')
|
||||
const extensions = readdirSync(join(dir!, 'extensions')).sort()
|
||||
expect(extensions).toContain('omp-ext')
|
||||
expect(extensions).toContain('orca-agent-status.ts')
|
||||
expect(extensions).not.toContain('pi-ext')
|
||||
})
|
||||
|
||||
it('launching omp when only ~/.pi/agent exists does NOT mirror Pi state', () => {
|
||||
// Why: missing OMP source dir on the remote must materialize the
|
||||
// overlay from empty — Orca's status extension only, no Pi state
|
||||
// cross-pollinated in.
|
||||
// Why: missing OMP source dir on the remote must create only OMP's
|
||||
// own extension dir. Pi state must never cross-pollinate in.
|
||||
seedAgentDir('.pi', 'pi')
|
||||
expect(existsSync(join(homeDir, '.omp'))).toBe(false)
|
||||
|
||||
|
|
@ -253,15 +262,11 @@ describe('PluginOverlayManager', () => {
|
|||
const dir = manager.materializePi('tab-relay-omp-empty:0', undefined, 'omp')
|
||||
|
||||
expect(dir).not.toBeNull()
|
||||
expect(dir!).toMatch(/[\\/]\.orca-relay[\\/]omp-overlays[\\/]/)
|
||||
// Pi-only home must NOT leak into the OMP overlay.
|
||||
expect(dir).toBe(join(homeDir, '.omp', 'agent'))
|
||||
// Pi-only home must NOT leak into the OMP home.
|
||||
expect(existsSync(join(dir!, 'auth.json'))).toBe(false)
|
||||
const overlayExtensions = readdirSync(join(dir!, 'extensions')).sort()
|
||||
expect(overlayExtensions).toEqual(['orca-agent-status.ts'])
|
||||
expect(JSON.parse(readFileSync(join(dir!, 'settings.json'), 'utf8'))).toEqual({
|
||||
hideThinkingBlock: true,
|
||||
terminal: { clearOnShrink: true }
|
||||
})
|
||||
const extensions = readdirSync(join(dir!, 'extensions')).sort()
|
||||
expect(extensions).toEqual(['orca-agent-status.ts'])
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -271,7 +276,7 @@ describe('PluginOverlayManager', () => {
|
|||
expect(manager.materializePi('tab-missing-pi:0', join(homeDir, 'missing-pi'))).toBeNull()
|
||||
})
|
||||
|
||||
it('clearOverlay removes opencode + every Pi-kind overlay root for an id', () => {
|
||||
it('clearOverlay removes OpenCode overlays without deleting real Pi/OMP homes', () => {
|
||||
manager.setSources({
|
||||
opencodePluginSource: 'opencode',
|
||||
piExtensionSource: 'pi',
|
||||
|
|
@ -280,7 +285,6 @@ describe('PluginOverlayManager', () => {
|
|||
const opencodeDir = manager.materializeOpenCode('tab-3:0')!
|
||||
const piDir = manager.materializePi('tab-3:0', undefined, 'pi')!
|
||||
const ompDir = manager.materializePi('tab-3:0', undefined, 'omp')!
|
||||
// Sanity: each kind got its own subtree, not a shared one.
|
||||
expect(piDir).not.toBe(ompDir)
|
||||
expect(existsSync(opencodeDir)).toBe(true)
|
||||
expect(existsSync(piDir)).toBe(true)
|
||||
|
|
@ -289,8 +293,8 @@ describe('PluginOverlayManager', () => {
|
|||
manager.clearOverlay('tab-3:0')
|
||||
|
||||
expect(existsSync(opencodeDir)).toBe(false)
|
||||
expect(existsSync(piDir)).toBe(false)
|
||||
expect(existsSync(ompDir)).toBe(false)
|
||||
expect(existsSync(piDir)).toBe(true)
|
||||
expect(existsSync(ompDir)).toBe(true)
|
||||
})
|
||||
|
||||
it.skipIf(process.platform === 'win32')(
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
// Why: relay-side equivalent of Orca's userData-backed plugin overlay system.
|
||||
// Orca's local OpenCodeHookService and PiTitlebarExtensionService each
|
||||
// materialize a per-PTY overlay and inject OPENCODE_CONFIG_DIR /
|
||||
// PI_CODING_AGENT_DIR pointing at it. Those paths describe the local
|
||||
// filesystem and would resolve to nothing on a remote box, so when a PTY runs
|
||||
// on the relay, the relay must do the same materialization on its own disk.
|
||||
// Why: relay-side equivalent of Orca's local agent integration installers.
|
||||
// OpenCode still needs a config overlay, while Pi/OMP now get Orca-managed
|
||||
// extension files installed into the remote agent homes. Host paths from the
|
||||
// renderer are meaningless on SSH targets, so the relay performs the remote
|
||||
// filesystem work itself.
|
||||
//
|
||||
// Plugin source strings ship over the JSON-RPC channel at session-ready
|
||||
// (commit #7) — they are NOT bundled with the relay binary because the
|
||||
|
|
@ -14,7 +13,8 @@
|
|||
// We deliberately do not reuse OpenCodeHookService / PiTitlebarExtensionService
|
||||
// directly: those modules import `electron` and ride on Orca's userData
|
||||
// path. The relay's electron-free constraint forces a thin parallel
|
||||
// implementation rooted at $HOME/.orca-relay/.
|
||||
// implementation rooted at $HOME/.orca-relay/ for OpenCode and at the remote
|
||||
// Pi/OMP homes for those agents.
|
||||
|
||||
import { createHash } from 'crypto'
|
||||
import {
|
||||
|
|
@ -28,13 +28,8 @@ import {
|
|||
writeFileSync
|
||||
} from 'fs'
|
||||
import { homedir } from 'os'
|
||||
import { basename, join } from 'path'
|
||||
import { join } from 'path'
|
||||
import { mirrorEntry, safeRemoveOverlay } from '../main/pty/overlay-mirror'
|
||||
import {
|
||||
isOmpPersistentSqliteEntry,
|
||||
mirrorOmpPersistentSqliteFiles
|
||||
} from '../main/pty/omp-sqlite-overlay'
|
||||
import { mergePiOverlayUiSettings } from '../shared/pi-overlay-ui-settings'
|
||||
import type { PiAgentKind } from '../shared/pi-agent-kind'
|
||||
|
||||
const RELAY_HOOKS_DIR = '.orca-relay'
|
||||
|
|
@ -46,7 +41,13 @@ const PI_OVERLAY_SUBDIR_BY_KIND: Record<PiAgentKind, string> = {
|
|||
const OPENCODE_PLUGIN_FILE = 'orca-opencode-status.js'
|
||||
const PI_EXTENSION_FILE = 'orca-agent-status.ts'
|
||||
const PI_AGENT_SUBDIR = 'agent'
|
||||
const PI_AGENT_SETTINGS_FILE = 'settings.json'
|
||||
const ORCA_MANAGED_EXTENSION_MARKER = '@orca-managed-pi-extension'
|
||||
|
||||
function withOrcaManagedPiExtensionMarker(source: string): string {
|
||||
return source.includes(ORCA_MANAGED_EXTENSION_MARKER)
|
||||
? source
|
||||
: `// ${ORCA_MANAGED_EXTENSION_MARKER}\n${source}`
|
||||
}
|
||||
// Why: source-dir resolution is keyed off the launching agent (Pi or OMP).
|
||||
// Both consume `PI_CODING_AGENT_DIR` but default to different `~/.<kind>/agent`
|
||||
// paths on the remote disk. The renderer-chosen launch command flows in via
|
||||
|
|
@ -106,19 +107,18 @@ export class PluginOverlayManager {
|
|||
* `agent_hook.installPlugins`. The first install enables the augmenter
|
||||
* output; subsequent installs (e.g. Orca version upgrade in flight) refresh
|
||||
* the cached source so future spawns see the new strings.
|
||||
* Note: existing per-PTY overlays already on disk keep the previous source
|
||||
* until that PTY exits — a long-running PTY does NOT pick up the new
|
||||
* source, matching the local-Orca behavior where the plugin file is
|
||||
* written once at spawn time. */
|
||||
* Note: existing running agents keep whatever source they loaded at
|
||||
* process start. Future PTYs pick up the refreshed source when the relay
|
||||
* writes plugin/extension files before spawn. */
|
||||
setSources(sources: PluginSources): void {
|
||||
if (typeof sources.opencodePluginSource === 'string') {
|
||||
this.opencodePluginSource = sources.opencodePluginSource
|
||||
}
|
||||
if (typeof sources.piExtensionSource === 'string') {
|
||||
this.piExtensionSources.pi = sources.piExtensionSource
|
||||
this.piExtensionSources.pi = withOrcaManagedPiExtensionMarker(sources.piExtensionSource)
|
||||
}
|
||||
if (typeof sources.ompExtensionSource === 'string') {
|
||||
this.piExtensionSources.omp = sources.ompExtensionSource
|
||||
this.piExtensionSources.omp = withOrcaManagedPiExtensionMarker(sources.ompExtensionSource)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -222,118 +222,38 @@ export class PluginOverlayManager {
|
|||
return join(this.homeDir, PI_AGENT_HOME_DIR_NAME[kind], PI_AGENT_SUBDIR)
|
||||
}
|
||||
|
||||
private mirrorPiAgentDir(sourceAgentDir: string, overlayDir: string, kind: PiAgentKind): void {
|
||||
if (!existsSync(sourceAgentDir)) {
|
||||
if (kind === 'omp') {
|
||||
mirrorOmpPersistentSqliteFiles(sourceAgentDir, overlayDir)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for (const entry of readdirSync(sourceAgentDir, { withFileTypes: true })) {
|
||||
const sourcePath = join(sourceAgentDir, entry.name)
|
||||
|
||||
if (entry.name === PI_AGENT_SETTINGS_FILE) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (kind === 'omp' && isOmpPersistentSqliteEntry(entry.name)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (entry.name === 'extensions') {
|
||||
const isSymlink = entry.isSymbolicLink()
|
||||
let isLinkPointingToDir = false
|
||||
if (isSymlink) {
|
||||
try {
|
||||
isLinkPointingToDir = statSync(sourcePath).isDirectory()
|
||||
} catch {
|
||||
isLinkPointingToDir = false
|
||||
}
|
||||
}
|
||||
|
||||
if ((!isSymlink && entry.isDirectory()) || isLinkPointingToDir) {
|
||||
const resolvedSource = isLinkPointingToDir ? realpathSync(sourcePath) : sourcePath
|
||||
const overlayExtensionsDir = join(overlayDir, 'extensions')
|
||||
mkdirSync(overlayExtensionsDir, { recursive: true })
|
||||
for (const extensionEntry of readdirSync(resolvedSource, { withFileTypes: true })) {
|
||||
if (extensionEntry.name === PI_EXTENSION_FILE) {
|
||||
continue
|
||||
}
|
||||
mirrorEntry(
|
||||
join(resolvedSource, extensionEntry.name),
|
||||
join(overlayExtensionsDir, extensionEntry.name)
|
||||
)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
mirrorEntry(sourcePath, join(overlayDir, basename(sourcePath)))
|
||||
}
|
||||
|
||||
if (kind === 'omp') {
|
||||
mirrorOmpPersistentSqliteFiles(sourceAgentDir, overlayDir)
|
||||
}
|
||||
}
|
||||
|
||||
private readPiSettings(sourceAgentDir: string): unknown {
|
||||
const settingsPath = join(sourceAgentDir, PI_AGENT_SETTINGS_FILE)
|
||||
if (!existsSync(settingsPath)) {
|
||||
return {}
|
||||
}
|
||||
|
||||
private canOverwritePiExtension(path: string): boolean {
|
||||
try {
|
||||
return JSON.parse(readFileSync(settingsPath, 'utf8'))
|
||||
return readFileSync(path, 'utf8').includes(ORCA_MANAGED_EXTENSION_MARKER)
|
||||
} catch {
|
||||
return {}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private writePiOverlaySettings(sourceAgentDir: string, overlayDir: string): void {
|
||||
// Why: relay overlays run on the remote disk, but the same Pi UI guardrails
|
||||
// need to stay overlay-local so SSH sessions do not mutate user config.
|
||||
const settings = mergePiOverlayUiSettings(this.readPiSettings(sourceAgentDir))
|
||||
writeFileSync(
|
||||
join(overlayDir, PI_AGENT_SETTINGS_FILE),
|
||||
`${JSON.stringify(settings, null, 2)}\n`
|
||||
)
|
||||
}
|
||||
|
||||
/** Materialize the Pi extension overlay for `id` and return the directory
|
||||
* path that should be assigned to PI_CODING_AGENT_DIR. `kind` selects which
|
||||
* Pi-compatible agent's source dir to mirror when `existingAgentDir` is
|
||||
* not supplied - defaults to 'pi' for back-compat with pre-OMP callers.
|
||||
* NEVER fall back across kinds: a missing source dir for the chosen kind
|
||||
* materializes the overlay from empty (Orca extensions only) rather than
|
||||
* silently mirroring the other agent's state. */
|
||||
/** Install the Pi/OMP status extension into the remote real agent dir and
|
||||
* return that directory. `kind` selects which Pi-compatible agent's default
|
||||
* dir to use when `existingAgentDir` is not supplied. */
|
||||
materializePi(id: string, existingAgentDir?: string, kind: PiAgentKind = 'pi'): string | null {
|
||||
const extensionSource = this.getPiExtensionSource(kind)
|
||||
if (!extensionSource || !isUsableId(id)) {
|
||||
return null
|
||||
}
|
||||
const root = this.piRoots[kind]
|
||||
const dir = join(root, safeDirName(id))
|
||||
try {
|
||||
const sourceAgentDir = existingAgentDir ?? this.getDefaultPiAgentDir(kind)
|
||||
if (existingAgentDir && !existsSync(existingAgentDir)) {
|
||||
return null
|
||||
}
|
||||
// Why: PI_CODING_AGENT_DIR is the whole state root for both Pi and OMP
|
||||
// (OMP inherits the env-var name from Pi by design). Mirror the remote
|
||||
// user's default agent dir so Orca's status extension does not hide auth,
|
||||
// sessions, skills, prompts, themes, or user extensions inside SSH panes.
|
||||
safeRemoveOverlay(dir, root)
|
||||
mkdirSync(dir, { recursive: true })
|
||||
this.mirrorPiAgentDir(sourceAgentDir, dir, kind)
|
||||
this.writePiOverlaySettings(sourceAgentDir, dir)
|
||||
const extensionsDir = join(dir, 'extensions')
|
||||
const extensionsDir = join(sourceAgentDir, 'extensions')
|
||||
mkdirSync(extensionsDir, { recursive: true })
|
||||
writeFileSync(join(extensionsDir, PI_EXTENSION_FILE), extensionSource)
|
||||
return dir
|
||||
const extensionPath = join(extensionsDir, PI_EXTENSION_FILE)
|
||||
if (!this.canOverwritePiExtension(extensionPath)) {
|
||||
return null
|
||||
}
|
||||
writeFileSync(extensionPath, extensionSource)
|
||||
return sourceAgentDir
|
||||
} catch (err) {
|
||||
process.stderr.write(
|
||||
`[plugin-overlay] failed to materialize ${kind} overlay: ${err instanceof Error ? err.message : String(err)}\n`
|
||||
`[plugin-overlay] failed to install ${kind} extension: ${err instanceof Error ? err.message : String(err)}\n`
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -694,10 +694,12 @@ describe('PtyHandler', () => {
|
|||
async () => {
|
||||
const oldShell = process.env.SHELL
|
||||
const oldHome = process.env.HOME
|
||||
const oldOrcaPi = process.env.ORCA_PI_CODING_AGENT_DIR
|
||||
const homeDir = mkdtempSync(join(tmpdir(), 'relay-pty-shell-launch-'))
|
||||
|
||||
process.env.SHELL = '/bin/bash'
|
||||
process.env.HOME = homeDir
|
||||
delete process.env.ORCA_PI_CODING_AGENT_DIR
|
||||
try {
|
||||
if (!existsSync('/bin/bash')) {
|
||||
return
|
||||
|
|
@ -706,8 +708,7 @@ describe('PtyHandler', () => {
|
|||
handler.addEnvAugmenter(() => ({
|
||||
OPENCODE_CONFIG_DIR: '/remote/overlay/opencode',
|
||||
ORCA_OPENCODE_CONFIG_DIR: '/remote/overlay/opencode',
|
||||
PI_CODING_AGENT_DIR: '/remote/overlay/pi',
|
||||
ORCA_PI_CODING_AGENT_DIR: '/remote/overlay/pi'
|
||||
ORCA_OMP_STATUS_EXTENSION: '/remote/.omp/agent/extensions/orca-agent-status.ts'
|
||||
}))
|
||||
|
||||
await dispatcher.callRequest('pty.spawn', { env: { HOME: homeDir } })
|
||||
|
|
@ -722,6 +723,11 @@ describe('PtyHandler', () => {
|
|||
} else {
|
||||
process.env.HOME = oldHome
|
||||
}
|
||||
if (oldOrcaPi === undefined) {
|
||||
delete process.env.ORCA_PI_CODING_AGENT_DIR
|
||||
} else {
|
||||
process.env.ORCA_PI_CODING_AGENT_DIR = oldOrcaPi
|
||||
}
|
||||
}
|
||||
|
||||
const shellArgs = mockPtySpawn.mock.calls[0][1]
|
||||
|
|
@ -730,13 +736,12 @@ describe('PtyHandler', () => {
|
|||
|
||||
expect(shellArgs).toEqual(['--rcfile', rcfile])
|
||||
expect(spawnOptions.env.ORCA_OPENCODE_CONFIG_DIR).toBe('/remote/overlay/opencode')
|
||||
expect(spawnOptions.env.ORCA_PI_CODING_AGENT_DIR).toBe('/remote/overlay/pi')
|
||||
expect(spawnOptions.env.ORCA_PI_CODING_AGENT_DIR).toBeUndefined()
|
||||
expect(readFileSync(rcfile, 'utf8')).toContain(
|
||||
'export OPENCODE_CONFIG_DIR="${ORCA_OPENCODE_CONFIG_DIR}"'
|
||||
)
|
||||
expect(readFileSync(rcfile, 'utf8')).toContain(
|
||||
'export PI_CODING_AGENT_DIR="${ORCA_PI_CODING_AGENT_DIR}"'
|
||||
)
|
||||
expect(readFileSync(rcfile, 'utf8')).not.toContain('ORCA_PI_CODING_AGENT_DIR')
|
||||
expect(readFileSync(rcfile, 'utf8')).toContain('command omp --extension')
|
||||
|
||||
rmSync(homeDir, { recursive: true, force: true })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -736,14 +736,11 @@ export class PtyHandler {
|
|||
const shell = resolveDefaultShell()
|
||||
// Why: `command` is intentionally absent from this revive path because
|
||||
// SerializedPtyEntry (see line 99) does not persist it — ManagedPty
|
||||
// never stored the renderer-chosen launch command. The Pi/OMP overlay
|
||||
// augmenter in src/relay/relay.ts therefore sees `ctx.command ===
|
||||
// undefined` for revived PTYs and falls back to the Pi-default kind
|
||||
// (see detectPiAgentKindFromCommand in src/shared/pi-agent-kind.ts).
|
||||
// Acceptable pre-OMP fallback: a cold-restart revived OMP shell that
|
||||
// later relaunches `omp` keeps the historical behavior of loading the
|
||||
// Pi overlay. Plumbing `command` through serialization is a separate,
|
||||
// larger change (out of scope for PR #2662).
|
||||
// never stored the renderer-chosen launch command. The Pi/OMP extension
|
||||
// installer in src/relay/relay.ts therefore sees `ctx.command ===
|
||||
// undefined` for revived PTYs and prepares the Pi default plus OMP's
|
||||
// typed-command wrapper. Plumbing `command` through serialization is a
|
||||
// separate, larger change.
|
||||
const spawnEnv = this.buildSpawnEnv(revivedEnv, {
|
||||
id: entry.id,
|
||||
paneKey: entry.paneKey,
|
||||
|
|
|
|||
|
|
@ -38,10 +38,7 @@ function windowsShellArgs(shellName: string): string[] | null {
|
|||
|
||||
function hasOverlayRestoreEnv(env: Record<string, string>): boolean {
|
||||
return Boolean(
|
||||
env.ORCA_OPENCODE_CONFIG_DIR ||
|
||||
env.ORCA_PI_CODING_AGENT_DIR ||
|
||||
env.ORCA_OMP_CODING_AGENT_DIR ||
|
||||
env.ORCA_REMOTE_CLI_BIN_DIR
|
||||
env.ORCA_OPENCODE_CONFIG_DIR || env.ORCA_REMOTE_CLI_BIN_DIR || env.ORCA_OMP_STATUS_EXTENSION
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -101,10 +98,6 @@ ${getZshStartupFileSourceBlock({
|
|||
if [[ ! -o login ]]; then
|
||||
# Why: remote startup files can re-export user defaults after relay spawn.
|
||||
[[ -n "\${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="\${ORCA_OPENCODE_CONFIG_DIR}"
|
||||
[[ -n "\${ORCA_PI_CODING_AGENT_DIR:-}" ]] && export PI_CODING_AGENT_DIR="\${ORCA_PI_CODING_AGENT_DIR}"
|
||||
if [[ -z "\${ORCA_PI_CODING_AGENT_DIR:-}" && -n "\${ORCA_OMP_CODING_AGENT_DIR:-}" ]]; then
|
||||
export PI_CODING_AGENT_DIR="\${ORCA_OMP_CODING_AGENT_DIR}"
|
||||
fi
|
||||
[[ -n "\${ORCA_REMOTE_CLI_BIN_DIR:-}" ]] && case ":$PATH:" in *:"\${ORCA_REMOTE_CLI_BIN_DIR}":*) ;; *) export PATH="\${ORCA_REMOTE_CLI_BIN_DIR}:$PATH" ;; esac
|
||||
${getPosixOmpShellWrapper()}
|
||||
fi
|
||||
|
|
@ -120,10 +113,6 @@ ${getZshStartupFileSourceBlock({
|
|||
})}
|
||||
# Why: .zlogin is the final zsh login startup file before the prompt.
|
||||
[[ -n "\${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="\${ORCA_OPENCODE_CONFIG_DIR}"
|
||||
[[ -n "\${ORCA_PI_CODING_AGENT_DIR:-}" ]] && export PI_CODING_AGENT_DIR="\${ORCA_PI_CODING_AGENT_DIR}"
|
||||
if [[ -z "\${ORCA_PI_CODING_AGENT_DIR:-}" && -n "\${ORCA_OMP_CODING_AGENT_DIR:-}" ]]; then
|
||||
export PI_CODING_AGENT_DIR="\${ORCA_OMP_CODING_AGENT_DIR}"
|
||||
fi
|
||||
[[ -n "\${ORCA_REMOTE_CLI_BIN_DIR:-}" ]] && case ":$PATH:" in *:"\${ORCA_REMOTE_CLI_BIN_DIR}":*) ;; *) export PATH="\${ORCA_REMOTE_CLI_BIN_DIR}:$PATH" ;; esac
|
||||
${getPosixOmpShellWrapper()}
|
||||
${getZshFinalZdotdirRestoreBlock('"${ORCA_USER_ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"')}
|
||||
|
|
@ -139,10 +128,6 @@ elif [[ -f "$HOME/.profile" ]]; then
|
|||
fi
|
||||
# Why: remote startup files can re-export user defaults after relay spawn.
|
||||
[[ -n "\${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="\${ORCA_OPENCODE_CONFIG_DIR}"
|
||||
[[ -n "\${ORCA_PI_CODING_AGENT_DIR:-}" ]] && export PI_CODING_AGENT_DIR="\${ORCA_PI_CODING_AGENT_DIR}"
|
||||
if [[ -z "\${ORCA_PI_CODING_AGENT_DIR:-}" && -n "\${ORCA_OMP_CODING_AGENT_DIR:-}" ]]; then
|
||||
export PI_CODING_AGENT_DIR="\${ORCA_OMP_CODING_AGENT_DIR}"
|
||||
fi
|
||||
[[ -n "\${ORCA_REMOTE_CLI_BIN_DIR:-}" ]] && case ":$PATH:" in *:"\${ORCA_REMOTE_CLI_BIN_DIR}":*) ;; *) export PATH="\${ORCA_REMOTE_CLI_BIN_DIR}:$PATH" ;; esac
|
||||
${getPosixOmpShellWrapper()}
|
||||
# Why: SSH bash sessions need the same command lifecycle markers as local
|
||||
|
|
|
|||
|
|
@ -510,14 +510,9 @@ async function main(): Promise<void> {
|
|||
// restart.
|
||||
ptyHandler.addEnvAugmenter(() => hookServer.buildPtyEnv())
|
||||
|
||||
// Why: per-PTY plugin overlays for OpenCode and Pi. `OPENCODE_CONFIG_DIR`
|
||||
// and `PI_CODING_AGENT_DIR` only make sense on the relay's own filesystem
|
||||
// — paths the renderer would synthesize for the Orca host's userData are
|
||||
// meaningless on the remote. The overlay manager materializes a per-PTY
|
||||
// dir on the remote (rooted at $HOME/.orca-relay/) so the agent CLI inside
|
||||
// the relay-spawned PTY loads the bundled status plugin and posts to the
|
||||
// relay's hook server. Source bodies arrive over JSON-RPC (see
|
||||
// `agent_hook.installPlugins` below) — not bundled with the relay binary.
|
||||
// Why: plugin install paths must be resolved on the relay host. OpenCode
|
||||
// still needs a relay-local config overlay, while Pi/OMP receive guarded
|
||||
// status extensions in their real remote agent dirs.
|
||||
const pluginOverlay = new PluginOverlayManager()
|
||||
ptyHandler.addEnvAugmenter((ctx) => {
|
||||
const env: Record<string, string> = {}
|
||||
|
|
@ -539,9 +534,8 @@ async function main(): Promise<void> {
|
|||
}
|
||||
if (pluginOverlay.hasPiSource()) {
|
||||
// Why: source-dir defaulting is keyed on which Pi-compatible agent is
|
||||
// being launched (Pi vs OMP). The renderer-supplied `command` is the
|
||||
// only signal - disk-presence guessing silently shadows the other
|
||||
// agent's extensions when both `~/.pi/agent` and `~/.omp/agent` exist.
|
||||
// being launched (Pi vs OMP). Install Orca's guarded extension into that
|
||||
// real remote agent dir without redirecting PI_CODING_AGENT_DIR.
|
||||
const kind = detectPiAgentKindFromCommand(ctx.command)
|
||||
const hasLaunchCommand = typeof ctx.command === 'string' && ctx.command.trim().length > 0
|
||||
const shouldPrepareOmpShadow = kind === 'omp' || !hasLaunchCommand
|
||||
|
|
@ -549,33 +543,20 @@ async function main(): Promise<void> {
|
|||
const sourceDir = resolvePiSourceAgentDir(ctx.env, ctx.shell, 'pi')
|
||||
const dir = pluginOverlay.materializePi(overlayId, sourceDir, 'pi')
|
||||
if (dir) {
|
||||
env.PI_CODING_AGENT_DIR = dir
|
||||
// Why: shadow var is agent-scoped so remote shell-ready wrappers can
|
||||
// restore Pi by default while the `omp` wrapper switches on demand.
|
||||
env.ORCA_PI_CODING_AGENT_DIR = dir
|
||||
if (sourceDir) {
|
||||
env.ORCA_PI_SOURCE_AGENT_DIR = sourceDir
|
||||
}
|
||||
env.ORCA_PI_SOURCE_AGENT_DIR = dir
|
||||
}
|
||||
}
|
||||
if (shouldPrepareOmpShadow) {
|
||||
// Why: in a bare shell, PI_CODING_AGENT_DIR is historically Pi's
|
||||
// default. Do not mirror it into OMP; use OMP's own default unless an
|
||||
// OMP-scoped source shadow is already present from a nested Orca shell.
|
||||
// Why: in a bare shell, prepare OMP's status extension so a typed
|
||||
// `omp` gets integration, but do not make OMP the shell's home.
|
||||
const sourceDir =
|
||||
kind === 'omp'
|
||||
? resolvePiSourceAgentDir(ctx.env, ctx.shell, 'omp')
|
||||
: ctx.env.ORCA_OMP_SOURCE_AGENT_DIR
|
||||
const dir = pluginOverlay.materializePi(overlayId, sourceDir, 'omp')
|
||||
if (dir) {
|
||||
if (kind === 'omp') {
|
||||
env.PI_CODING_AGENT_DIR = dir
|
||||
}
|
||||
env.ORCA_OMP_CODING_AGENT_DIR = dir
|
||||
env.ORCA_OMP_STATUS_EXTENSION = getRelayPiStatusExtensionPath(dir)
|
||||
if (sourceDir) {
|
||||
env.ORCA_OMP_SOURCE_AGENT_DIR = sourceDir
|
||||
}
|
||||
env.ORCA_OMP_SOURCE_AGENT_DIR = dir
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -608,8 +589,8 @@ async function main(): Promise<void> {
|
|||
// the wire at session-ready (the renderer's bundled hook-service strings
|
||||
// change as new agent events are added — pinning them to the relay binary
|
||||
// would force a relay redeploy on every Orca update). Cache them so each
|
||||
// subsequent PTY spawn can materialize a per-PTY overlay rooted under
|
||||
// $HOME/.orca-relay/. See docs/design/agent-status-over-ssh.md §4.
|
||||
// subsequent PTY spawn can materialize the remote OpenCode overlay and
|
||||
// install Pi/OMP managed extensions. See docs/design/agent-status-over-ssh.md §4.
|
||||
// Why: bound the per-source size so a buggy/hostile Orca can't OOM the
|
||||
// relay by pushing a giant string. The HTTP path has HOOK_REQUEST_MAX_BYTES
|
||||
// = 1 MB; the JSON-RPC path needs an equivalent ceiling. Real plugin sources
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@ import { TUI_AGENT_CONFIG } from './tui-agent-config'
|
|||
* Pi-compatible agent kinds. Both Pi and OMP (omp.sh) consume the same
|
||||
* `PI_CODING_AGENT_DIR` env contract and the same extension API, but each
|
||||
* defaults its on-disk config dir to a different `~/.<kind>/agent` path.
|
||||
* The Orca overlay needs to know which agent is being launched so it mirrors
|
||||
* the user's actual source dir for THAT agent, with no cross-agent fallback
|
||||
* Orca's managed extension installer needs to know which agent is being
|
||||
* launched so it targets the user's actual source dir for THAT agent, with no
|
||||
* cross-agent fallback
|
||||
* (otherwise switching agents in the same workspace silently shadows the
|
||||
* other agent's user extensions).
|
||||
*/
|
||||
|
|
@ -41,12 +42,12 @@ const OMP_REGEX = makeLaunchCmdRegex(OMP_LAUNCH_CMD)
|
|||
*
|
||||
* Returns 'omp' when the command launches OMP (`omp` / `omp.sh`), otherwise
|
||||
* defaults to 'pi'. Defaulting to 'pi' preserves prior behavior for the
|
||||
* non-launch case (e.g. bare shells that may later invoke `pi`) where the
|
||||
* `~/.pi/agent` overlay was always materialized.
|
||||
* non-launch case (e.g. bare shells that may later invoke `pi`) where Orca
|
||||
* prepared Pi integration by default.
|
||||
*
|
||||
* NEVER cross-fall-back: a missing source dir for the resolved kind is the
|
||||
* overlay's "no source, just Orca extensions" branch - the other agent's
|
||||
* dir MUST NOT be substituted.
|
||||
* NEVER cross-fall-back: a missing source dir for the resolved kind means
|
||||
* "create that kind's extension dir only" - the other agent's dir MUST NOT
|
||||
* be substituted.
|
||||
*/
|
||||
export function detectPiAgentKindFromCommand(command: string | undefined): PiAgentKind {
|
||||
if (typeof command === 'string' && OMP_REGEX.test(command)) {
|
||||
|
|
|
|||
|
|
@ -248,12 +248,11 @@ describe('tui agent startup plans', () => {
|
|||
})
|
||||
|
||||
it('returns an OMP draft plan with ORCA_OMP_PREFILL (OMP-scoped, not Pi-shared)', () => {
|
||||
// Why: OMP owns its own overlay tree, bundled prefill extension, and
|
||||
// prefill env var. The OMP overlay's orca-prefill.ts reads
|
||||
// ORCA_OMP_PREFILL — see src/main/pi/titlebar-extension-service.ts —
|
||||
// so a draft plan for OMP MUST emit that name. A regression here would
|
||||
// either silently drop the draft (Pi var ignored by OMP overlay) or
|
||||
// honor a stale Pi-PTY draft from a previous launch.
|
||||
// Why: OMP owns its own managed prefill extension and env var.
|
||||
// orca-prefill.ts reads ORCA_OMP_PREFILL for OMP launches — see
|
||||
// src/main/pi/titlebar-extension-service.ts — so a draft plan for OMP
|
||||
// MUST emit that name. A regression here would either silently drop the
|
||||
// draft (Pi var ignored by OMP) or honor a stale Pi-PTY draft.
|
||||
const plan = buildAgentDraftLaunchPlan({
|
||||
agent: 'omp',
|
||||
draft: 'fix the omp regression',
|
||||
|
|
|
|||
|
|
@ -307,11 +307,11 @@ test.describe('Localhost SSH', () => {
|
|||
ptyId,
|
||||
[
|
||||
'opencode_status_file="$OPENCODE_CONFIG_DIR/plugins/orca-opencode-status.js"',
|
||||
'pi_status_file="$PI_CODING_AGENT_DIR/extensions/orca-agent-status.ts"',
|
||||
'if [ -n "$OPENCODE_CONFIG_DIR" ] && [ -f "$opencode_status_file" ] && [ -n "$PI_CODING_AGENT_DIR" ] && [ -f "$pi_status_file" ]; then',
|
||||
'pi_status_file="$HOME/.pi/agent/extensions/orca-agent-status.ts"',
|
||||
'if [ -n "$OPENCODE_CONFIG_DIR" ] && [ -f "$opencode_status_file" ] && [ -f "$pi_status_file" ]; then',
|
||||
` ${emitMarkerCommand(pluginOverlayMarker)}`,
|
||||
'else',
|
||||
` printf '%s opencode=%s opencode_file=%s pi=%s pi_file=%s\\n' ${shellQuote(pluginOverlayFailedMarker)} "$OPENCODE_CONFIG_DIR" "$opencode_status_file" "$PI_CODING_AGENT_DIR" "$pi_status_file"`,
|
||||
` printf '%s opencode=%s opencode_file=%s pi_file=%s\\n' ${shellQuote(pluginOverlayFailedMarker)} "$OPENCODE_CONFIG_DIR" "$opencode_status_file" "$pi_status_file"`,
|
||||
'fi'
|
||||
].join('\n')
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in New Issue