Files
orca/src/shared/pi-agent-kind.ts
T
NeilandOrca 6154aded34 Quiet-window Pi/OMP intermediate done so resumed work cancels notification (#6361) (#6558)
Pi (pi.dev) and OMP are goal/mission agents whose normalizePiCompatibleEvent
maps milestone agent_end -> hook state 'done' while they are still working.
observeHookStatus gated the quiet window on `workingStatusObserved`, so these
intermediate 'done' events (workingStatusObserved === false) fell through to an
immediate "agent finished" notification while the TUI kept spinning, and a
follow-up working event could not cancel it.

Route Pi/OMP 'done' through the existing quiet window via a new
doneShouldUseQuietWindow() predicate (delegating to a shared
isPiCompatibleAgentType on the canonical PiAgentKind), so resumed work cancels
the premature notification. Codex and other turn-end-only producers keep their
immediate dispatch.

Also harden the process-exit backstop: skip the agent-evidence teardown while a
quiet-window 'done' is pending, otherwise a poll that finds the agent gone would
clear hasAgentRunEvidence and the timer would silently drop the real completion.

Co-authored-by: Orca <help@stably.ai>
2026-06-28 16:52:55 -07:00

74 lines
3.2 KiB
TypeScript

import { TUI_AGENT_CONFIG } from './tui-agent-config'
import { getCommandTokenPathBasename, getFirstCommandToken } from './command-token-scanner'
/**
* 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.
* 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).
*/
export type PiAgentKind = 'pi' | 'omp'
/**
* True when `agentType` names a Pi-compatible (goal/mission) kind. These agents
* emit milestone `agent_end` events between steps while still working, so they
* are treated differently from agents that only signal completion at turn end.
*/
export function isPiCompatibleAgentType(
agentType: string | null | undefined
): agentType is PiAgentKind {
return agentType === 'pi' || agentType === 'omp'
}
const OMP_LAUNCH_CMD = TUI_AGENT_CONFIG.omp.launchCmd
// Why: regex carved to avoid matching `pi` inside `pip`, `mpi`, `api`,
// `python`, or `omp` inside `comp`, `omp.sh` (acceptable - that's literally
// the binary), `omp-foo`, etc. The leading boundary excludes alnum/underscore
// AND `-`/`.`/`/`/`\\` so that `~/bin/pi` or `./omp` still match but
// `mpi`/`pomp` do not. Trailing boundary allows whitespace, end-of-string,
// shell separators, or argv-style flags (`pi -v`, `omp --help`).
const BOUNDARY_BEFORE = `(?:^|[\\s;&|('"\`])`
const BOUNDARY_AFTER = `(?:$|[\\s;&|)'"\`])`
const PATH_PREFIX = `(?:[^\\s;&|('"\`]*[\\\\/])?`
function makeLaunchCmdRegex(launchCmd: string): RegExp {
// Why: launchCmd may be a multi-token string ("hermes --tui"); only the
// first token is the binary name. Use that for matching.
const binary = getCommandTokenPathBasename(getFirstCommandToken(launchCmd))
const escaped = binary.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
return new RegExp(
`${BOUNDARY_BEFORE}${PATH_PREFIX}${escaped}(?:\\.cmd|\\.exe|\\.sh)?${BOUNDARY_AFTER}`,
'i'
)
}
const OMP_REGEX = makeLaunchCmdRegex(OMP_LAUNCH_CMD)
/**
* Identify the Pi-compatible agent kind a launch command targets.
*
* 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 Orca
* prepared Pi integration by default.
*
* 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)) {
return 'omp'
}
// Why: PI launches and the no-command (bare-shell) fallback both resolve to
// 'pi'. A bare shell that later invokes `pi` keeps the historical default;
// if it later invokes `omp`, the status extension re-routes at runtime based
// on the executable name so attribution still lands on OMP.
return 'pi'
}