Vectrum
Sign In
JjohannSoMy PI configHistory
327 lines · 11.5 KB · TypeScript
// @orca-managed-pi-extension
function processName(value: unknown): string {
  return String(value || '').split(/[\\/]/).pop()?.toLowerCase() || ''
}

const CONFIGURED_HOOK_PATH = '/hook/pi'
let cachedOmpRuntime: boolean | null = null

function isOmpRuntime(): boolean {
  if (cachedOmpRuntime !== null) return cachedOmpRuntime
  if (CONFIGURED_HOOK_PATH === '/hook/omp') {
    cachedOmpRuntime = true
    return true
  }
  const executableNames = [
    processName(process.title),
    processName(process.env._),
    processName(process.argv[1]),
    processName(process.argv[0])
  ]
  cachedOmpRuntime = executableNames.some((name) =>
    ['omp', 'omp.js', 'omp.sh', 'omp.cmd', 'omp.exe', 'omp.bat'].includes(name)
  )
  return cachedOmpRuntime
}

const BRAILLE_FRAMES = [
  '\u280b',
  '\u2819',
  '\u2839',
  '\u2838',
  '\u283c',
  '\u2834',
  '\u2826',
  '\u2827',
  '\u2807',
  '\u280f'
]

const FRAME_INTERVAL_MS = 80
// Why: pi repaints the title from its own writers (session_info_changed, the win32
// update-check restore) with no event we observe, so the marker has to be re-asserted
// even when no spinner frame is due. Coarse on purpose: it only rewrites one string.
const MARKER_REASSERT_MS = 1000
const AGENT_END_IDLE_RECHECK_MS = 25
const AGENT_END_IDLE_RECHECK_MAX_MS = 250
// Why: a failed idle compaction can end without auto_compaction_end, and no agent turn will
// close a maintenance spinner — cap it so idle maintenance cannot strand a working title.
const IDLE_COMPACTION_MAX_FRAMES = Math.ceil(300000 / FRAME_INTERVAL_MS)

// Why: `-` is the plain separator; `!` is the state marker Orca reads as needs-input
// (src/shared/pi-state-title-marker.ts), so mobile and the CLI see the wait too.
function getMarkedTitle(pi, marker) {
  const cwd = process.cwd().split(/[\\/]/).filter(Boolean).at(-1) || process.cwd()
  const session = pi.getSessionName()
  return session
    ? `\u03c0 ${marker} ${session} - ${cwd}`
    : `\u03c0 ${marker} ${cwd}`
}

function getBaseTitle(pi) {
  return getMarkedTitle(pi, '-')
}

// Why: the ctx.ui pi passes is a getter that calls assertActive() and throws once a
// session-replacing dialog invalidates the runner; optional chaining cannot screen
// that out. Read it behind a try and never mutate state before a paint has succeeded.
function resolvePainter(ctx) {
  try {
    return typeof ctx?.ui?.setTitle === 'function' ? ctx : null
  } catch {
    return null
  }
}

export default function (pi) {
  if (!process.env.ORCA_PANE_KEY) return
  // Why: child agents inherit the pane env, and the spinner is harmlessly
  // per-process — but the needs-input marker is status the pane reports, so only
  // one process may assert it. Mirrors ORCA_PI_STATUS_OWNED in the status hook.
  const markerOwnerPid = process.env.ORCA_PI_TITLE_MARKER_OWNED
  const ownsMarker = !markerOwnerPid || markerOwnerPid === String(process.pid)
  if (ownsMarker) process.env.ORCA_PI_TITLE_MARKER_OWNED = String(process.pid)
  // Why: replacement factories share the process realm; retire the old owner before painting.
  const ownersKey = Symbol.for('orca.pi.titlebar.owners')
  const owners = globalThis[ownersKey] ??= new Map()
  const paneKey = process.env.ORCA_PANE_KEY
  owners.get(paneKey)?.()
  let disposed = false
  function clearOwnedTimers() {
    clearPendingAgentEndCheck()
    clearAnimation()
    stopMarkerReassert()
  }

  function dispose() {
    disposed = true
    clearOwnedTimers()
    resetPromptState()
    if (owners.get(paneKey) === dispose) owners.delete(paneKey)
  }
  owners.set(paneKey, dispose)

  function on(name, handler) {
    pi.on(name, (event, ctx) => {
      if (!disposed) return handler(event, ctx)
    })
  }

  let timer = null
  let frameIndex = 0
  // Why: only idle maintenance owns a spinner of its own. A threshold compaction runs
  // inside an agent turn, whose spinner must outlive it, and any newer start clears the
  // marker so a late idle completion cannot stop current work (#16470).
  let idleCompactionOwnsSpinner = false
  // Why: pi already collapses nested prompts into one start/end pair, so this counter
  // guards a close that never arrives, not nesting. A new turn cannot start under a
  // dialog holding input focus, so agent_start doubles as recovery.
  let promptDepth = 0
  let markerPainted = false
  let promptCtx = null
  // Why: a separate handle from `timer`, which clearAnimation() nulls — the marker must
  // survive a turn settling, a shutdown of the spinner, and the idle-maintenance cap.
  let markerTimer = null
  let pendingAgentEndCheck = null
  let pendingAgentEndContext = null
  let agentEndIdleRecheckMs = AGENT_END_IDLE_RECHECK_MS

// Why: buildTitle runs inside the try because it is not safe either — getSessionName()
// calls assertActive() and process.cwd() throws ENOENT once the worktree is deleted.
// Most call sites are timer callbacks, where an escape is an uncaught exception and pi
// exits(1) through its own uncaughtException handler.
  function paintTitle(ctx, buildTitle) {
    if (disposed || !ctx) return false
    try {
      ctx.ui.setTitle(buildTitle())
      return true
    } catch {
      clearOwnedTimers()
      return false
    }
  }

  function resetPromptState() {
    stopMarkerReassert()
    promptDepth = 0
    markerPainted = false
    promptCtx = null
  }

  function clearPendingAgentEndCheck() {
    if (pendingAgentEndCheck !== null) clearTimeout(pendingAgentEndCheck)
    pendingAgentEndCheck = null
    pendingAgentEndContext = null
  }

  function stopMarkerReassert() {
    if (markerTimer) clearInterval(markerTimer)
    markerTimer = null
  }

  function startMarkerReassert(ctx) {
    stopMarkerReassert()
    markerTimer = setInterval(() => paintTitle(ctx, () => getMarkedTitle(pi, '!')), MARKER_REASSERT_MS)
    if (typeof markerTimer.unref === 'function') markerTimer.unref()
  }

  function clearAnimation() {
    if (timer) {
      clearInterval(timer)
      timer = null
    }
    frameIndex = 0
    idleCompactionOwnsSpinner = false
  }

  function stopAnimation(ctx) {
    clearPendingAgentEndCheck()
    clearAnimation()
    // Why: settling under an open dialog still leaves the pane waiting on the user, so
    // the idle title must not retire the marker the dialog is holding.
    paintTitle(ctx, () => (markerPainted ? getMarkedTitle(pi, '!') : getBaseTitle(pi)))
  }

  function renderFrame(ctx) {
    // Why: the maintenance cap runs before the dialog guard so a dialog left open
    // cannot suspend it; stopAnimation keeps the marker while a dialog is open.
    if (idleCompactionOwnsSpinner && frameIndex >= IDLE_COMPACTION_MAX_FRAMES) {
      stopAnimation(ctx)
      return
    }
    // Why: an 80ms working frame would repaint over the needs-input marker within one
    // tick, so a mid-turn dialog would still look busy everywhere the title is the
    // only evidence. Re-assert rather than skip: pi repaints the title on its own
    // (session_info_changed, resetExtensionUI, rebindCurrentSession) and would
    // otherwise wipe the marker with nothing to restore it. The frame still counts,
    // so the cap above keeps accruing in wall-clock.
    if (markerPainted) {
      const painted = paintTitle(ctx, () => getMarkedTitle(pi, '!'))
      frameIndex++
      return painted
    }
      const painted = paintTitle(ctx, () => {
        const frame = BRAILLE_FRAMES[frameIndex % BRAILLE_FRAMES.length]
        const cwd = process.cwd().split(/[\\/]/).filter(Boolean).at(-1) || process.cwd()
        const session = pi.getSessionName()
        return session ? `${frame} \u03c0 - ${session} - ${cwd}` : `${frame} \u03c0 - ${cwd}`
      })
      frameIndex++
      return painted
  }

  function startAnimation(ctx) {
    clearPendingAgentEndCheck()
    clearAnimation()
    if (!renderFrame(ctx)) return
    timer = setInterval(() => renderFrame(ctx), FRAME_INTERVAL_MS)
  }

  function checkPendingAgentEnd() {
    pendingAgentEndCheck = null
    const ctx = pendingAgentEndContext
    if (!ctx) return
    try {
      if (ctx.isIdle()) {
        pendingAgentEndContext = null
        stopAnimation(ctx)
        return
      }
    } catch {
      clearOwnedTimers()
      return
    }
    pendingAgentEndCheck = setTimeout(checkPendingAgentEnd, agentEndIdleRecheckMs)
    if (typeof pendingAgentEndCheck.unref === 'function') pendingAgentEndCheck.unref()
    agentEndIdleRecheckMs = Math.min(agentEndIdleRecheckMs * 2, AGENT_END_IDLE_RECHECK_MAX_MS)
  }

  on('agent_start', async (_event, ctx) => {
    resetPromptState()
    startAnimation(ctx)
  })

  // Why: pi drops an open dialog through resetExtensionUI without resolving its promise,
  // so a replaced or reloaded session never sends the matching close. Both boundaries
  // prove no dialog from the old session is still on screen.
  on('session_start', async () => {
    clearOwnedTimers()
    resetPromptState()
  })

  // Why: modern Pi emits agent_end mid-run and only settle later, so settlement is the
  // authoritative completion boundary. Legacy runtimes never emit it, so agent_end stays.
  on('agent_settled', async (_event, ctx) => {
    stopAnimation(ctx)
  })

  on('agent_end', async (event, ctx) => {
    if (event?.willContinue === true) {
      clearPendingAgentEndCheck()
      return
    }
    if (isOmpRuntime() || !ctx || typeof ctx.isIdle !== 'function') {
      stopAnimation(ctx)
      return
    }
    clearPendingAgentEndCheck()
    agentEndIdleRecheckMs = AGENT_END_IDLE_RECHECK_MS
    pendingAgentEndContext = ctx
    pendingAgentEndCheck = setTimeout(checkPendingAgentEnd, 0)
    if (typeof pendingAgentEndCheck.unref === 'function') pendingAgentEndCheck.unref()
  })

  on('ui_prompt_start', async (_event, ctx) => {
    if (isOmpRuntime() || !ownsMarker) return
    promptDepth++
    // Why: retry on every open rather than only the outermost, so an outer ctx
    // that could not paint cannot decide the whole stack stays unmarked.
    if (markerPainted) return
    const painter = resolvePainter(ctx)
    // Why: only hold the spinner off once the marker is actually up, or a ctx
    // that cannot paint would freeze the title on its last working frame.
    if (!paintTitle(painter, () => getMarkedTitle(pi, '!'))) return
    markerPainted = true
    promptCtx = painter
    startMarkerReassert(painter)
  })

  on('ui_prompt_end', async (_event, ctx) => {
    if (isOmpRuntime() || !ownsMarker || promptDepth === 0) return
    promptDepth--
    if (promptDepth > 0) return
    // Why: the opening ctx already painted once, so a close whose own ctx is stale
    // does not leave the needs-input marker up until the next turn.
    const painter = resolvePainter(ctx) ?? promptCtx
    markerPainted = false
    promptCtx = null
    stopMarkerReassert()
    // Why: a still-live turn resumes its spinner in place; otherwise the pane is idle
    // and must drop the needs-input marker rather than keep asking for attention.
    if (timer) {
      renderFrame(painter)
      return
    }
    paintTitle(painter, () => getBaseTitle(pi))
  })

  on('auto_compaction_start', async (event, ctx) => {
    if (event?.reason !== 'idle') return
    // Why: the idle worker can fire against a turn that just started, and reason alone does
    // not prove the pane is idle. Adopting a live agent spinner would let the matching
    // auto_compaction_end mark the pane idle mid-run — the inverse of #16470.
    if (timer && !idleCompactionOwnsSpinner) return
    startAnimation(ctx)
    idleCompactionOwnsSpinner = true
  })

  on('auto_compaction_end', async (_event, ctx) => {
    if (!idleCompactionOwnsSpinner) return
    stopAnimation(ctx)
  })

  on('session_shutdown', async (_event, ctx) => {
    resetPromptState()
    stopAnimation(ctx)
  })
}