mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
feat(extensions): ui prompt events (#8355)
* feat(extensions): ui prompt events * fix: handlers should not delay the prompt
This commit is contained in:
parent
e86823096c
commit
ccfe79ed23
6 changed files with 104 additions and 2 deletions
|
|
@ -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)).
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -166,6 +166,9 @@ export type {
|
|||
TreePreparation,
|
||||
TurnEndEvent,
|
||||
TurnStartEvent,
|
||||
UIPromptEndEvent,
|
||||
UIPromptKind,
|
||||
UIPromptStartEvent,
|
||||
// Events - User Bash
|
||||
UserBashEvent,
|
||||
UserBashEventResult,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -144,6 +144,9 @@ export type {
|
|||
ToolResultEvent,
|
||||
TurnEndEvent,
|
||||
TurnStartEvent,
|
||||
UIPromptEndEvent,
|
||||
UIPromptKind,
|
||||
UIPromptStartEvent,
|
||||
UserBashEvent,
|
||||
UserBashEventResult,
|
||||
WidgetPlacement,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue