From 47cc1f8de145eeeb12ce3a45537cccfc0413390c Mon Sep 17 00:00:00 2001 From: Cristina Poncela Cubeiro <140309543+cristinaponcela@users.noreply.github.com> Date: Wed, 19 Aug 2026 10:04:32 +0200 Subject: [PATCH 1/2] feat(extensions): ui prompt events --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/docs/extensions.md | 18 ++++++++ .../coding-agent/src/core/extensions/index.ts | 3 ++ .../src/core/extensions/runner.ts | 46 ++++++++++++++++++- .../coding-agent/src/core/extensions/types.ts | 22 +++++++++ packages/coding-agent/src/index.ts | 3 ++ 6 files changed, 92 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index e857000f5eb4..acb89972bf44 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### 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. ### Fixed diff --git a/packages/coding-agent/docs/extensions.md b/packages/coding-agent/docs/extensions.md index 2e069acdf400..af4a626b3f9a 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 4ef89cff983b..c0e9898bc414 100644 --- a/packages/coding-agent/src/core/extensions/index.ts +++ b/packages/coding-agent/src/core/extensions/index.ts @@ -164,6 +164,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 fc486e4cd4d1..cba45bf219a1 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,51 @@ 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 }; + void this.emit({ 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; + void this.emit({ + 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; + } + } + 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 9008d6212407..591abfc7cb6c 100644 --- a/packages/coding-agent/src/core/extensions/types.ts +++ b/packages/coding-agent/src/core/extensions/types.ts @@ -740,6 +740,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"; @@ -1059,6 +1077,8 @@ export type ExtensionEvent = | AgentStartEvent | AgentEndEvent | AgentSettledEvent + | UIPromptStartEvent + | UIPromptEndEvent | TurnStartEvent | TurnEndEvent | MessageStartEvent @@ -1245,6 +1265,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 6d64d70c87ce..a4737e7534a6 100644 --- a/packages/coding-agent/src/index.ts +++ b/packages/coding-agent/src/index.ts @@ -143,6 +143,9 @@ export type { ToolResultEvent, TurnEndEvent, TurnStartEvent, + UIPromptEndEvent, + UIPromptKind, + UIPromptStartEvent, UserBashEvent, UserBashEventResult, WidgetPlacement, From ada7ce729ca5f693209442cc0ddf0d807205f94d Mon Sep 17 00:00:00 2001 From: Cristina Poncela Cubeiro <140309543+cristinaponcela@users.noreply.github.com> Date: Wed, 19 Aug 2026 10:16:28 +0200 Subject: [PATCH 2/2] fix: handlers should not delay the prompt --- packages/coding-agent/src/core/extensions/runner.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/src/core/extensions/runner.ts b/packages/coding-agent/src/core/extensions/runner.ts index cba45bf219a1..3346645a1045 100644 --- a/packages/coding-agent/src/core/extensions/runner.ts +++ b/packages/coding-agent/src/core/extensions/runner.ts @@ -454,7 +454,7 @@ export class ExtensionRunner { const outerPrompt = this.uiPromptDepth++ === 0; if (outerPrompt) { this.activeUIPrompt = { kind, title }; - void this.emit({ type: "ui_prompt_start", reason: "ui_prompt", kind, ...(title ? { title } : {}) }); + this.emitUIPromptEvent({ type: "ui_prompt_start", reason: "ui_prompt", kind, ...(title ? { title } : {}) }); } const finish = () => { @@ -463,7 +463,7 @@ export class ExtensionRunner { const prompt = this.activeUIPrompt ?? { kind, title }; this.activeUIPrompt = undefined; - void this.emit({ + this.emitUIPromptEvent({ type: "ui_prompt_end", reason: "ui_prompt", kind: prompt.kind, @@ -479,6 +479,12 @@ export class ExtensionRunner { } } + private emitUIPromptEvent(event: Extract): void { + queueMicrotask(() => { + void this.emit(event); + }); + } + getUIContext(): ExtensionUIContext { return this.uiContext; }