Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions src/main/pi/titlebar-extension-source.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ const BRAILLE_RE = /[⠀-⣿]/

type TitlebarContext = {
ui: { setTitle: (title: string) => void }
hasUI?: boolean
agent?: { kind: 'main' | 'sub' }
isIdle?: () => boolean
}
type HookHandler = (event?: unknown, context?: TitlebarContext) => Promise<void> | void
Expand All @@ -32,6 +34,8 @@ function createHarness(
paneKey?: string
isIdle?: () => boolean
kind?: PiAgentKind
hasUI?: boolean
agentKind?: 'main' | 'sub'
processTitle?: string
cwdImpl?: () => string
sessionNameImpl?: () => string
Expand All @@ -42,6 +46,8 @@ function createHarness(
): Harness {
const titles: string[] = []
const ctx: TitlebarContext = {
hasUI: options.hasUI,
agent: options.agentKind ? { kind: options.agentKind } : undefined,
ui: {
setTitle: (title: string) => {
options.setTitle?.(title)
Expand Down Expand Up @@ -125,6 +131,43 @@ describe('getPiTitlebarExtensionSource', () => {
expect(createHarness({ paneKey: '' }).handlers).toEqual({})
})

it.each(['omp', 'pi'] as const)(
'publishes a strong idle title at %s main UI startup',
async (kind) => {
const harness = createHarness({
kind,
processTitle: 'omp',
hasUI: true,
agentKind: 'main',
isIdle: () => true
})

await harness.callHook('session_start')

expect(harness.lastTitle()).toBe(IDLE_TITLE)
expect(detectAgentStatusFromTitle(harness.lastTitle() ?? '')).toBe('idle')
expect(vi.getTimerCount()).toBe(0)

await harness.callHook('agent_start')
expect(harness.lastTitle()).toMatch(BRAILLE_RE)
await harness.callHook('agent_end')
expect(harness.lastTitle()).toBe(IDLE_TITLE)
}
)

it.each([
{ name: 'busy main UI', hasUI: true, agentKind: 'main', idle: false },
{ name: 'idle subagent UI', hasUI: true, agentKind: 'sub', idle: true },
{ name: 'idle main RPC', hasUI: false, agentKind: 'main', idle: true }
] as const)('does not publish a title at $name startup', async ({ hasUI, agentKind, idle }) => {
const harness = createHarness({ kind: 'omp', hasUI, agentKind, isIdle: () => idle })

await harness.callHook('session_start')

expect(harness.titles).toEqual([])
expect(vi.getTimerCount()).toBe(0)
})

it('stops the spinner when the agent settles', async () => {
const harness = createHarness()

Expand Down
5 changes: 4 additions & 1 deletion src/main/pi/titlebar-extension-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,9 +250,12 @@ export function getPiTitlebarExtensionSource(kind: PiAgentKind = 'pi'): string {
' // 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 () => {",
" on('session_start', async (_event, ctx) => {",
" if (ctx?.hasUI === false || ctx?.agent?.kind === 'sub') return",
' clearOwnedTimers()',
' resetPromptState()',
' // Why: startup readiness needs an idle title before the first turn can be delivered.',
' if (ctx?.isIdle?.()) stopAnimation(ctx)',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new startup publication lands in the generic explicit-idle lane rather than OMP's guarded π > lane. The readiness engine accepts π - <session> - <cwd> with no quiescence and no screen requirement (readScreenInputVeto returning null does not veto), so a supervised first-run startup can settle on this title before or while the setup wizard opens and type the first task into it.

Technical details
# Startup `π -` title weakens the OMP setup-wizard guard

## Affected sites
- `src/main/pi/titlebar-extension-source.ts:258` — publishes the managed rest title at `session_start` when `ctx.isIdle()`.
- `src/main/runtime/tui-idle-evidence.ts:294` — `hasExplicitIdleTitle` returns READY_STRONG for `π -` with no age check.
- `src/main/runtime/tui-idle-evidence.ts:313-319` — the OMP-only `π >` lane that DOES require `TUI_IDLE_QUIESCENCE_MS` (3000ms) plus a screen read clear of the wizard.
- `src/main/runtime/terminal-wait-detection.ts:24-30` and `src/main/runtime/agent-state-rules/pi.json:5-11` — the `^π - ` anchor classifies the managed title as explicit idle.
- `src/main/runtime/screen-input-veto.ts:16-22` — the veto is `null` with no trustworthy screen, and `evaluateTuiIdle` only blocks on `true`.
- `src/main/runtime/omp-readiness-transcripts.test.ts:195-213` — asserts `π -` needs no screen because "A post-turn title cannot come from setup".

## Required outcome
- A startup `π -` title must not settle `tui-idle` before OMP's setup wizard can be ruled out. Either hold a `π -` title observed at `session_start` to the same age+screen requirement as `π >`, or establish with a captured transcript that the extension's `session_start` always runs after the wizard's alternate-screen paint.

## Suggested approach
- Add a readiness-level regression: a first-run `omp-18-setup` transcript with a startup `π - capture-cwd` and no trustworthy screen (`size: null`) must not settle; then make the fix satisfy it.

## Open questions for the human
- Does OMP 18.5.0 emit `session_start` only after the first-run wizard has painted the alternate screen on macOS? The readiness comments state OMP paints its idle title before the wizard, so the ordering is the crux.

' })',
'',
' // Why: modern Pi emits agent_end mid-run and only settle later, so settlement is the',
Expand Down