feat(extensions): ui prompt events (#8355)

* feat(extensions): ui prompt events

* fix: handlers should not delay the prompt
This commit is contained in:
Cristina Poncela Cubeiro 2026-08-27 11:28:05 +02:00 • committed by GitHub
parent e86823096c
commit ccfe79ed23
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 104 additions and 2 deletions

View file

@ -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)).

View file

@ -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).

View file

@ -166,6 +166,9 @@ export type {
TreePreparation,
TurnEndEvent,
TurnStartEvent,
UIPromptEndEvent,
UIPromptKind,
UIPromptStartEvent,
// Events - User Bash
UserBashEvent,
UserBashEventResult,

View file

@ -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<T>(kind: UIPromptKind, title: string | undefined, run: () => Promise<T>): Promise<T> {
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<RunnerEmitEvent, { type: "ui_prompt_start" | "ui_prompt_end" }>): void {
queueMicrotask(() => {
void this.emit(event);
});
}
getUIContext(): ExtensionUIContext {
return this.uiContext;
}

View file

@ -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<AgentStartEvent>): void;
on(event: "agent_end", handler: ExtensionHandler<AgentEndEvent>): void;
on(event: "agent_settled", handler: ExtensionHandler<AgentSettledEvent>): void;
on(event: "ui_prompt_start", handler: ExtensionHandler<UIPromptStartEvent>): void;
on(event: "ui_prompt_end", handler: ExtensionHandler<UIPromptEndEvent>): void;
on(event: "turn_start", handler: ExtensionHandler<TurnStartEvent>): void;
on(event: "turn_end", handler: ExtensionHandler<TurnEndEvent>): void;
on(event: "message_start", handler: ExtensionHandler<MessageStartEvent>): void;

View file

@ -144,6 +144,9 @@ export type {
ToolResultEvent,
TurnEndEvent,
TurnStartEvent,
UIPromptEndEvent,
UIPromptKind,
UIPromptStartEvent,
UserBashEvent,
UserBashEventResult,
WidgetPlacement,