From ccfe79ed238674f760c986e3a61493aab794000a Mon Sep 17 00:00:00 2001 From: Cristina Poncela Cubeiro <140309543+cristinaponcela@users.noreply.github.com> Date: Thu, 27 Aug 2026 11:28:05 +0200 Subject: [PATCH] feat(extensions): ui prompt events (#8355) * feat(extensions): ui prompt events * fix: handlers should not delay the prompt --- packages/coding-agent/CHANGELOG.md | 8 ++- packages/coding-agent/docs/extensions.md | 18 +++++++ .../coding-agent/src/core/extensions/index.ts | 3 ++ .../src/core/extensions/runner.ts | 52 ++++++++++++++++++- .../coding-agent/src/core/extensions/types.ts | 22 ++++++++ packages/coding-agent/src/index.ts | 3 ++ 6 files changed, 104 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 3cdbf461e..814067a56 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,8 @@ ### Added +- Added `ui_prompt_start` and `ui_prompt_end` extension events so host integrations can distinguish active agent work from waiting on user-facing `ctx.ui` prompts ([#5329](https://github.com/earendil-works/pi/issues/5329)). +- Added transcript usage notices for compaction and branch summaries when cache miss notices are enabled. - Added RPC `clear_queue` to retrieve and remove queued steering and follow-up messages ([#8432](https://github.com/earendil-works/pi/issues/8432)). - Added environment variables and advanced settings for overriding auto-detected terminal hyperlink, image, and truecolor capabilities ([#8665](https://github.com/earendil-works/pi/issues/8665)). @@ -32,7 +34,11 @@ - Added an optional `powershell` tool for Windows, configurable through `defaultTools` and the SDK. See [PowerShell Tool](docs/windows.md#powershell-tool). - Added a `/thinking` selector and searchable default choices to the model and thinking selectors; Ctrl+S saves the selected model as the global default. See [Models and Thinking](docs/keybindings.md#models-and-thinking). - Added optional routing session IDs to exported compaction summary helpers so callers can preserve provider routing without enabling prompt cache writes. -- Added transcript usage notices for compaction and branch summaries when cache miss notices are enabled. +- Added + + + +usage notices for compaction and branch summaries when cache miss notices are enabled. - Added `session_compact_failed` extension events so compaction failures and aborts expose their reason, retry state, source, and error message to handlers ([#8175](https://github.com/earendil-works/pi/issues/8175)). - Added inherited provider-neutral `toolChoice` support to simple stream requests. - Added inherited automatic Anthropic server-side refusal fallback for supported first-party models, including returned-model usage pricing ([#8017](https://github.com/earendil-works/pi/issues/8017)). diff --git a/packages/coding-agent/docs/extensions.md b/packages/coding-agent/docs/extensions.md index 7643856be..03ca0d53b 100644 --- a/packages/coding-agent/docs/extensions.md +++ b/packages/coding-agent/docs/extensions.md @@ -580,6 +580,24 @@ pi.on("agent_settled", async (_event, ctx) => { }); ``` +#### ui_prompt_start / ui_prompt_end + +Notification-only lifecycle events for blocking user-facing extension UI prompts. They fire around `ctx.ui.select()`, `ctx.ui.confirm()`, `ctx.ui.input()`, `ctx.ui.editor()`, and `ctx.ui.custom()` so host/status integrations can report "waiting for user" instead of just "running". + +Nested or overlapping prompts are coalesced into one outer waiting span. Handlers are invoked best-effort and are not awaited before showing or closing the prompt. + +```typescript +pi.on("ui_prompt_start", async (event, ctx) => { + // event.reason === "ui_prompt" + // event.kind: "select" | "confirm" | "input" | "editor" | "custom" + // event.title: prompt title when available +}); + +pi.on("ui_prompt_end", async (event, ctx) => { + // Pi is no longer waiting on that UI prompt span. +}); +``` + #### turn_start / turn_end Fired for each turn (one LLM response + tool calls). diff --git a/packages/coding-agent/src/core/extensions/index.ts b/packages/coding-agent/src/core/extensions/index.ts index 90c786041..f19816a9a 100644 --- a/packages/coding-agent/src/core/extensions/index.ts +++ b/packages/coding-agent/src/core/extensions/index.ts @@ -166,6 +166,9 @@ export type { TreePreparation, TurnEndEvent, TurnStartEvent, + UIPromptEndEvent, + UIPromptKind, + UIPromptStartEvent, // Events - User Bash UserBashEvent, UserBashEventResult, diff --git a/packages/coding-agent/src/core/extensions/runner.ts b/packages/coding-agent/src/core/extensions/runner.ts index fc486e4cd..3346645a1 100644 --- a/packages/coding-agent/src/core/extensions/runner.ts +++ b/packages/coding-agent/src/core/extensions/runner.ts @@ -62,6 +62,7 @@ import type { ToolCallEventResult, ToolResultEvent, ToolResultEventResult, + UIPromptKind, UserBashEvent, UserBashEventResult, } from "./types.ts"; @@ -295,6 +296,8 @@ export class ExtensionRunner { private shortcutDiagnostics: ResourceDiagnostic[] = []; private commandDiagnostics: ResourceDiagnostic[] = []; private staleMessage: string | undefined; + private uiPromptDepth = 0; + private activeUIPrompt: { kind: UIPromptKind; title?: string } | undefined; constructor( extensions: Extension[], @@ -431,10 +434,57 @@ export class ExtensionRunner { } setUIContext(uiContext?: ExtensionUIContext, mode: ExtensionMode = "print"): void { - this.uiContext = uiContext ?? noOpUIContext; + this.uiContext = uiContext ? this.wrapUIPromptContext(uiContext) : noOpUIContext; this.mode = mode; } + private wrapUIPromptContext(ui: ExtensionUIContext): ExtensionUIContext { + return { + ...ui, + select: (title, options, opts) => this.withUIPrompt("select", title, () => ui.select(title, options, opts)), + confirm: (title, message, opts) => this.withUIPrompt("confirm", title, () => ui.confirm(title, message, opts)), + input: (title, placeholder, opts) => + this.withUIPrompt("input", title, () => ui.input(title, placeholder, opts)), + editor: (title, prefill) => this.withUIPrompt("editor", title, () => ui.editor(title, prefill)), + custom: (factory, options) => this.withUIPrompt("custom", undefined, () => ui.custom(factory, options)), + }; + } + + private withUIPrompt(kind: UIPromptKind, title: string | undefined, run: () => Promise): Promise { + const outerPrompt = this.uiPromptDepth++ === 0; + if (outerPrompt) { + this.activeUIPrompt = { kind, title }; + this.emitUIPromptEvent({ type: "ui_prompt_start", reason: "ui_prompt", kind, ...(title ? { title } : {}) }); + } + + const finish = () => { + if (--this.uiPromptDepth > 0) return; + this.uiPromptDepth = 0; + + const prompt = this.activeUIPrompt ?? { kind, title }; + this.activeUIPrompt = undefined; + this.emitUIPromptEvent({ + type: "ui_prompt_end", + reason: "ui_prompt", + kind: prompt.kind, + ...(prompt.title ? { title: prompt.title } : {}), + }); + }; + + try { + return run().finally(finish); + } catch (err) { + finish(); + throw err; + } + } + + private emitUIPromptEvent(event: Extract): void { + queueMicrotask(() => { + void this.emit(event); + }); + } + getUIContext(): ExtensionUIContext { return this.uiContext; } diff --git a/packages/coding-agent/src/core/extensions/types.ts b/packages/coding-agent/src/core/extensions/types.ts index bade46419..0b9474664 100644 --- a/packages/coding-agent/src/core/extensions/types.ts +++ b/packages/coding-agent/src/core/extensions/types.ts @@ -742,6 +742,24 @@ export interface AgentSettledEvent { type: "agent_settled"; } +export type UIPromptKind = "select" | "confirm" | "input" | "editor" | "custom"; + +/** Fired when Pi starts waiting on a blocking user-facing extension UI prompt. */ +export interface UIPromptStartEvent { + type: "ui_prompt_start"; + reason: "ui_prompt"; + kind: UIPromptKind; + title?: string; +} + +/** Fired when Pi is no longer waiting on a blocking user-facing extension UI prompt. */ +export interface UIPromptEndEvent { + type: "ui_prompt_end"; + reason: "ui_prompt"; + kind: UIPromptKind; + title?: string; +} + /** Fired at the start of each turn */ export interface TurnStartEvent { type: "turn_start"; @@ -1077,6 +1095,8 @@ export type ExtensionEvent = | AgentStartEvent | AgentEndEvent | AgentSettledEvent + | UIPromptStartEvent + | UIPromptEndEvent | TurnStartEvent | TurnEndEvent | MessageStartEvent @@ -1263,6 +1283,8 @@ export interface ExtensionAPI { on(event: "agent_start", handler: ExtensionHandler): void; on(event: "agent_end", handler: ExtensionHandler): void; on(event: "agent_settled", handler: ExtensionHandler): void; + on(event: "ui_prompt_start", handler: ExtensionHandler): void; + on(event: "ui_prompt_end", handler: ExtensionHandler): void; on(event: "turn_start", handler: ExtensionHandler): void; on(event: "turn_end", handler: ExtensionHandler): void; on(event: "message_start", handler: ExtensionHandler): void; diff --git a/packages/coding-agent/src/index.ts b/packages/coding-agent/src/index.ts index 52d8f9b06..8e5ce2f3d 100644 --- a/packages/coding-agent/src/index.ts +++ b/packages/coding-agent/src/index.ts @@ -144,6 +144,9 @@ export type { ToolResultEvent, TurnEndEvent, TurnStartEvent, + UIPromptEndEvent, + UIPromptKind, + UIPromptStartEvent, UserBashEvent, UserBashEventResult, WidgetPlacement,