diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index f2107d55..6ea29720 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -5,6 +5,7 @@ ### Added - `--no-extensions` flag to disable extension discovery and loading +- Extension UI dialogs (`ctx.ui.select()`, `ctx.ui.confirm()`, `ctx.ui.input()`) now support a `timeout` option that auto-dismisses the dialog with a live countdown display. Simpler alternative to `AbortSignal` for timed dialogs. ## [0.37.8] - 2026-01-07 diff --git a/packages/coding-agent/docs/extensions.md b/packages/coding-agent/docs/extensions.md index b8bdc60d..a85e8d7f 100644 --- a/packages/coding-agent/docs/extensions.md +++ b/packages/coding-agent/docs/extensions.md @@ -1094,9 +1094,33 @@ ctx.ui.notify("Done!", "info"); // "info" | "warning" | "error" - `ctx.ui.editor()`: [handoff.ts](../examples/extensions/handoff.ts) - `ctx.ui.setEditorText()`: [handoff.ts](../examples/extensions/handoff.ts), [qna.ts](../examples/extensions/qna.ts) -#### Auto-Dismissing Dialogs +#### Timed Dialogs with Countdown -Dialogs can be programmatically dismissed using `AbortSignal`. This is useful for implementing timeouts: +Dialogs support a `timeout` option that auto-dismisses with a live countdown display: + +```typescript +// Dialog shows "Title (5s)" → "Title (4s)" → ... → auto-dismisses at 0 +const confirmed = await ctx.ui.confirm( + "Timed Confirmation", + "This dialog will auto-cancel in 5 seconds. Confirm?", + { timeout: 5000 } +); + +if (confirmed) { + // User confirmed +} else { + // User cancelled or timed out +} +``` + +**Return values on timeout:** +- `select()` returns `undefined` +- `confirm()` returns `false` +- `input()` returns `undefined` + +#### Manual Dismissal with AbortSignal + +For more control (e.g., to distinguish timeout from user cancel), use `AbortSignal`: ```typescript const controller = new AbortController(); @@ -1119,12 +1143,7 @@ if (confirmed) { } ``` -**Return values on abort:** -- `select()` returns `undefined` -- `confirm()` returns `false` -- `input()` returns `undefined` - -See [examples/extensions/timed-confirm.ts](../examples/extensions/timed-confirm.ts) for a complete example. +See [examples/extensions/timed-confirm.ts](../examples/extensions/timed-confirm.ts) for complete examples. ### Widgets, Status, and Footer diff --git a/packages/coding-agent/examples/extensions/timed-confirm.ts b/packages/coding-agent/examples/extensions/timed-confirm.ts index 138996c2..465d6c60 100644 --- a/packages/coding-agent/examples/extensions/timed-confirm.ts +++ b/packages/coding-agent/examples/extensions/timed-confirm.ts @@ -1,16 +1,49 @@ /** - * Example extension demonstrating AbortSignal for auto-dismissing dialogs. + * Example extension demonstrating timed dialogs with live countdown. * * Commands: - * - /timed - Shows confirm dialog that auto-cancels after 5 seconds - * - /timed-select - Shows select dialog that auto-cancels after 10 seconds + * - /timed - Shows confirm dialog that auto-cancels after 5 seconds with countdown + * - /timed-select - Shows select dialog that auto-cancels after 10 seconds with countdown + * - /timed-signal - Shows confirm using AbortSignal (manual approach) */ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; export default function (pi: ExtensionAPI) { + // Simple approach: use timeout option (recommended) pi.registerCommand("timed", { - description: "Show a timed confirmation dialog (auto-cancels in 5s)", + description: "Show a timed confirmation dialog (auto-cancels in 5s with countdown)", + handler: async (_args, ctx) => { + const confirmed = await ctx.ui.confirm( + "Timed Confirmation", + "This dialog will auto-cancel in 5 seconds. Confirm?", + { timeout: 5000 }, + ); + + if (confirmed) { + ctx.ui.notify("Confirmed by user!", "info"); + } else { + ctx.ui.notify("Cancelled or timed out", "info"); + } + }, + }); + + pi.registerCommand("timed-select", { + description: "Show a timed select dialog (auto-cancels in 10s with countdown)", + handler: async (_args, ctx) => { + const choice = await ctx.ui.select("Pick an option", ["Option A", "Option B", "Option C"], { timeout: 10000 }); + + if (choice) { + ctx.ui.notify(`Selected: ${choice}`, "info"); + } else { + ctx.ui.notify("Selection cancelled or timed out", "info"); + } + }, + }); + + // Manual approach: use AbortSignal for more control + pi.registerCommand("timed-signal", { + description: "Show a timed confirm using AbortSignal (manual approach)", handler: async (_args, ctx) => { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); @@ -34,30 +67,4 @@ export default function (pi: ExtensionAPI) { } }, }); - - pi.registerCommand("timed-select", { - description: "Show a timed select dialog (auto-cancels in 10s)", - handler: async (_args, ctx) => { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 10000); - - ctx.ui.notify("Select dialog will auto-cancel in 10 seconds...", "info"); - - const choice = await ctx.ui.select( - "Pick an option (auto-cancels in 10s)", - ["Option A", "Option B", "Option C"], - { signal: controller.signal }, - ); - - clearTimeout(timeoutId); - - if (choice) { - ctx.ui.notify(`Selected: ${choice}`, "info"); - } else if (controller.signal.aborted) { - ctx.ui.notify("Selection timed out", "warning"); - } else { - ctx.ui.notify("Selection cancelled", "info"); - } - }, - }); } diff --git a/packages/coding-agent/src/core/extensions/index.ts b/packages/coding-agent/src/core/extensions/index.ts index 5fd979f5..d62ef0a0 100644 --- a/packages/coding-agent/src/core/extensions/index.ts +++ b/packages/coding-agent/src/core/extensions/index.ts @@ -36,6 +36,7 @@ export type { ExtensionHandler, ExtensionShortcut, ExtensionUIContext, + ExtensionUIDialogOptions, FindToolResultEvent, GetActiveToolsHandler, GetAllToolsHandler, diff --git a/packages/coding-agent/src/core/extensions/types.ts b/packages/coding-agent/src/core/extensions/types.ts index 305e711c..6b1e7596 100644 --- a/packages/coding-agent/src/core/extensions/types.ts +++ b/packages/coding-agent/src/core/extensions/types.ts @@ -46,19 +46,27 @@ export type { AgentToolResult, AgentToolUpdateCallback }; // UI Context // ============================================================================ +/** Options for extension UI dialogs. */ +export interface ExtensionUIDialogOptions { + /** AbortSignal to programmatically dismiss the dialog. */ + signal?: AbortSignal; + /** Timeout in milliseconds. Dialog auto-dismisses with live countdown display. */ + timeout?: number; +} + /** * UI context for extensions to request interactive UI. * Each mode (interactive, RPC, print) provides its own implementation. */ export interface ExtensionUIContext { /** Show a selector and return the user's choice. */ - select(title: string, options: string[], opts?: { signal?: AbortSignal }): Promise; + select(title: string, options: string[], opts?: ExtensionUIDialogOptions): Promise; /** Show a confirmation dialog. */ - confirm(title: string, message: string, opts?: { signal?: AbortSignal }): Promise; + confirm(title: string, message: string, opts?: ExtensionUIDialogOptions): Promise; /** Show a text input dialog. */ - input(title: string, placeholder?: string, opts?: { signal?: AbortSignal }): Promise; + input(title: string, placeholder?: string, opts?: ExtensionUIDialogOptions): Promise; /** Show a notification to the user. */ notify(message: string, type?: "info" | "warning" | "error"): void; diff --git a/packages/coding-agent/src/index.ts b/packages/coding-agent/src/index.ts index daca7825..3fc4ae39 100644 --- a/packages/coding-agent/src/index.ts +++ b/packages/coding-agent/src/index.ts @@ -54,6 +54,7 @@ export type { ExtensionHandler, ExtensionShortcut, ExtensionUIContext, + ExtensionUIDialogOptions, LoadExtensionsResult, LoadedExtension, MessageRenderer, diff --git a/packages/coding-agent/src/modes/interactive/components/countdown-timer.ts b/packages/coding-agent/src/modes/interactive/components/countdown-timer.ts new file mode 100644 index 00000000..aa733886 --- /dev/null +++ b/packages/coding-agent/src/modes/interactive/components/countdown-timer.ts @@ -0,0 +1,38 @@ +/** + * Reusable countdown timer for dialog components. + */ + +import type { TUI } from "@mariozechner/pi-tui"; + +export class CountdownTimer { + private intervalId: ReturnType | undefined; + private remainingSeconds: number; + + constructor( + timeoutMs: number, + private tui: TUI | undefined, + private onTick: (seconds: number) => void, + private onExpire: () => void, + ) { + this.remainingSeconds = Math.ceil(timeoutMs / 1000); + this.onTick(this.remainingSeconds); + + this.intervalId = setInterval(() => { + this.remainingSeconds--; + this.onTick(this.remainingSeconds); + this.tui?.requestRender(); + + if (this.remainingSeconds <= 0) { + this.dispose(); + this.onExpire(); + } + }, 1000); + } + + dispose(): void { + if (this.intervalId) { + clearInterval(this.intervalId); + this.intervalId = undefined; + } + } +} diff --git a/packages/coding-agent/src/modes/interactive/components/extension-input.ts b/packages/coding-agent/src/modes/interactive/components/extension-input.ts index 8771baca..08545c39 100644 --- a/packages/coding-agent/src/modes/interactive/components/extension-input.ts +++ b/packages/coding-agent/src/modes/interactive/components/extension-input.ts @@ -2,64 +2,73 @@ * Simple text input component for extensions. */ -import { Container, getEditorKeybindings, Input, Spacer, Text } from "@mariozechner/pi-tui"; +import { Container, getEditorKeybindings, Input, Spacer, Text, type TUI } from "@mariozechner/pi-tui"; import { theme } from "../theme/theme.js"; +import { CountdownTimer } from "./countdown-timer.js"; import { DynamicBorder } from "./dynamic-border.js"; +export interface ExtensionInputOptions { + tui?: TUI; + timeout?: number; +} + export class ExtensionInputComponent extends Container { private input: Input; private onSubmitCallback: (value: string) => void; private onCancelCallback: () => void; + private titleText: Text; + private baseTitle: string; + private countdown: CountdownTimer | undefined; constructor( title: string, _placeholder: string | undefined, onSubmit: (value: string) => void, onCancel: () => void, + opts?: ExtensionInputOptions, ) { super(); this.onSubmitCallback = onSubmit; this.onCancelCallback = onCancel; + this.baseTitle = title; - // Add top border this.addChild(new DynamicBorder()); this.addChild(new Spacer(1)); - // Add title - this.addChild(new Text(theme.fg("accent", title), 1, 0)); + this.titleText = new Text(theme.fg("accent", title), 1, 0); + this.addChild(this.titleText); this.addChild(new Spacer(1)); - // Create input + if (opts?.timeout && opts.timeout > 0 && opts.tui) { + this.countdown = new CountdownTimer( + opts.timeout, + opts.tui, + (s) => this.titleText.setText(theme.fg("accent", `${this.baseTitle} (${s}s)`)), + () => this.onCancelCallback(), + ); + } + this.input = new Input(); this.addChild(this.input); - this.addChild(new Spacer(1)); - - // Add hint this.addChild(new Text(theme.fg("dim", "enter submit esc cancel"), 1, 0)); - this.addChild(new Spacer(1)); - - // Add bottom border this.addChild(new DynamicBorder()); } handleInput(keyData: string): void { const kb = getEditorKeybindings(); - // Enter if (kb.matches(keyData, "selectConfirm") || keyData === "\n") { this.onSubmitCallback(this.input.getValue()); - return; - } - - // Escape or Ctrl+C to cancel - if (kb.matches(keyData, "selectCancel")) { + } else if (kb.matches(keyData, "selectCancel")) { this.onCancelCallback(); - return; + } else { + this.input.handleInput(keyData); } + } - // Forward to input - this.input.handleInput(keyData); + dispose(): void { + this.countdown?.dispose(); } } diff --git a/packages/coding-agent/src/modes/interactive/components/extension-selector.ts b/packages/coding-agent/src/modes/interactive/components/extension-selector.ts index 96a10581..d60a0427 100644 --- a/packages/coding-agent/src/modes/interactive/components/extension-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/extension-selector.ts @@ -3,90 +3,94 @@ * Displays a list of string options with keyboard navigation. */ -import { Container, getEditorKeybindings, Spacer, Text } from "@mariozechner/pi-tui"; +import { Container, getEditorKeybindings, Spacer, Text, type TUI } from "@mariozechner/pi-tui"; import { theme } from "../theme/theme.js"; +import { CountdownTimer } from "./countdown-timer.js"; import { DynamicBorder } from "./dynamic-border.js"; +export interface ExtensionSelectorOptions { + tui?: TUI; + timeout?: number; +} + export class ExtensionSelectorComponent extends Container { private options: string[]; private selectedIndex = 0; private listContainer: Container; private onSelectCallback: (option: string) => void; private onCancelCallback: () => void; + private titleText: Text; + private baseTitle: string; + private countdown: CountdownTimer | undefined; - constructor(title: string, options: string[], onSelect: (option: string) => void, onCancel: () => void) { + constructor( + title: string, + options: string[], + onSelect: (option: string) => void, + onCancel: () => void, + opts?: ExtensionSelectorOptions, + ) { super(); this.options = options; this.onSelectCallback = onSelect; this.onCancelCallback = onCancel; + this.baseTitle = title; - // Add top border this.addChild(new DynamicBorder()); this.addChild(new Spacer(1)); - // Add title - this.addChild(new Text(theme.fg("accent", title), 1, 0)); + this.titleText = new Text(theme.fg("accent", title), 1, 0); + this.addChild(this.titleText); this.addChild(new Spacer(1)); - // Create list container + if (opts?.timeout && opts.timeout > 0 && opts.tui) { + this.countdown = new CountdownTimer( + opts.timeout, + opts.tui, + (s) => this.titleText.setText(theme.fg("accent", `${this.baseTitle} (${s}s)`)), + () => this.onCancelCallback(), + ); + } + this.listContainer = new Container(); this.addChild(this.listContainer); - this.addChild(new Spacer(1)); - - // Add hint this.addChild(new Text(theme.fg("dim", "↑↓ navigate enter select esc cancel"), 1, 0)); - this.addChild(new Spacer(1)); - - // Add bottom border this.addChild(new DynamicBorder()); - // Initial render this.updateList(); } private updateList(): void { this.listContainer.clear(); - for (let i = 0; i < this.options.length; i++) { - const option = this.options[i]; const isSelected = i === this.selectedIndex; - - let text = ""; - if (isSelected) { - text = theme.fg("accent", "→ ") + theme.fg("accent", option); - } else { - text = ` ${theme.fg("text", option)}`; - } - + const text = isSelected + ? theme.fg("accent", "→ ") + theme.fg("accent", this.options[i]) + : ` ${theme.fg("text", this.options[i])}`; this.listContainer.addChild(new Text(text, 1, 0)); } } handleInput(keyData: string): void { const kb = getEditorKeybindings(); - // Up arrow or k if (kb.matches(keyData, "selectUp") || keyData === "k") { this.selectedIndex = Math.max(0, this.selectedIndex - 1); this.updateList(); - } - // Down arrow or j - else if (kb.matches(keyData, "selectDown") || keyData === "j") { + } else if (kb.matches(keyData, "selectDown") || keyData === "j") { this.selectedIndex = Math.min(this.options.length - 1, this.selectedIndex + 1); this.updateList(); - } - // Enter - else if (kb.matches(keyData, "selectConfirm") || keyData === "\n") { + } else if (kb.matches(keyData, "selectConfirm") || keyData === "\n") { const selected = this.options[this.selectedIndex]; - if (selected) { - this.onSelectCallback(selected); - } - } - // Escape or Ctrl+C - else if (kb.matches(keyData, "selectCancel")) { + if (selected) this.onSelectCallback(selected); + } else if (kb.matches(keyData, "selectCancel")) { this.onCancelCallback(); } } + + dispose(): void { + this.countdown?.dispose(); + } } diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index fb8fedb4..823d1c1c 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -32,6 +32,7 @@ import type { ExtensionContext, ExtensionRunner, ExtensionUIContext, + ExtensionUIDialogOptions, LoadedExtension, } from "../../core/extensions/index.js"; import { KeybindingsManager } from "../../core/keybindings.js"; @@ -764,7 +765,7 @@ export class InteractiveMode { private showExtensionSelector( title: string, options: string[], - opts?: { signal?: AbortSignal }, + opts?: ExtensionUIDialogOptions, ): Promise { return new Promise((resolve) => { if (opts?.signal?.aborted) { @@ -791,6 +792,7 @@ export class InteractiveMode { this.hideExtensionSelector(); resolve(undefined); }, + { tui: this.ui, timeout: opts?.timeout }, ); this.editorContainer.clear(); @@ -804,6 +806,7 @@ export class InteractiveMode { * Hide the extension selector. */ private hideExtensionSelector(): void { + this.extensionSelector?.dispose(); this.editorContainer.clear(); this.editorContainer.addChild(this.editor); this.extensionSelector = undefined; @@ -817,7 +820,7 @@ export class InteractiveMode { private async showExtensionConfirm( title: string, message: string, - opts?: { signal?: AbortSignal }, + opts?: ExtensionUIDialogOptions, ): Promise { const result = await this.showExtensionSelector(`${title}\n${message}`, ["Yes", "No"], opts); return result === "Yes"; @@ -829,7 +832,7 @@ export class InteractiveMode { private showExtensionInput( title: string, placeholder?: string, - opts?: { signal?: AbortSignal }, + opts?: ExtensionUIDialogOptions, ): Promise { return new Promise((resolve) => { if (opts?.signal?.aborted) { @@ -856,6 +859,7 @@ export class InteractiveMode { this.hideExtensionInput(); resolve(undefined); }, + { tui: this.ui, timeout: opts?.timeout }, ); this.editorContainer.clear(); @@ -869,6 +873,7 @@ export class InteractiveMode { * Hide the extension input. */ private hideExtensionInput(): void { + this.extensionInput?.dispose(); this.editorContainer.clear(); this.editorContainer.addChild(this.editor); this.extensionInput = undefined; diff --git a/packages/coding-agent/src/modes/rpc/rpc-mode.ts b/packages/coding-agent/src/modes/rpc/rpc-mode.ts index 7c2668b3..928d18f7 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-mode.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-mode.ts @@ -14,7 +14,7 @@ import * as crypto from "node:crypto"; import * as readline from "readline"; import type { AgentSession } from "../../core/agent-session.js"; -import type { ExtensionUIContext } from "../../core/extensions/index.js"; +import type { ExtensionUIContext, ExtensionUIDialogOptions } from "../../core/extensions/index.js"; import { theme } from "../interactive/theme/theme.js"; import type { RpcCommand, @@ -63,99 +63,67 @@ export async function runRpcMode(session: AgentSession): Promise { { resolve: (value: any) => void; reject: (error: Error) => void } >(); + /** Helper for dialog methods with signal/timeout support */ + function createDialogPromise( + opts: ExtensionUIDialogOptions | undefined, + defaultValue: T, + request: Record, + parseResponse: (response: RpcExtensionUIResponse) => T, + ): Promise { + if (opts?.signal?.aborted) return Promise.resolve(defaultValue); + + const id = crypto.randomUUID(); + return new Promise((resolve, reject) => { + let timeoutId: ReturnType | undefined; + + const cleanup = () => { + if (timeoutId) clearTimeout(timeoutId); + opts?.signal?.removeEventListener("abort", onAbort); + pendingExtensionRequests.delete(id); + }; + + const onAbort = () => { + cleanup(); + resolve(defaultValue); + }; + opts?.signal?.addEventListener("abort", onAbort, { once: true }); + + if (opts?.timeout) { + timeoutId = setTimeout(() => { + cleanup(); + resolve(defaultValue); + }, opts.timeout); + } + + pendingExtensionRequests.set(id, { + resolve: (response: RpcExtensionUIResponse) => { + cleanup(); + resolve(parseResponse(response)); + }, + reject, + }); + output({ type: "extension_ui_request", id, ...request } as RpcExtensionUIRequest); + }); + } + /** * Create an extension UI context that uses the RPC protocol. */ const createExtensionUIContext = (): ExtensionUIContext => ({ - async select(title: string, options: string[], opts?: { signal?: AbortSignal }): Promise { - if (opts?.signal?.aborted) { - return undefined; - } + select: (title, options, opts) => + createDialogPromise(opts, undefined, { method: "select", title, options, timeout: opts?.timeout }, (r) => + "cancelled" in r && r.cancelled ? undefined : "value" in r ? r.value : undefined, + ), - const id = crypto.randomUUID(); - return new Promise((resolve, reject) => { - const onAbort = () => { - pendingExtensionRequests.delete(id); - resolve(undefined); - }; - opts?.signal?.addEventListener("abort", onAbort, { once: true }); + confirm: (title, message, opts) => + createDialogPromise(opts, false, { method: "confirm", title, message, timeout: opts?.timeout }, (r) => + "cancelled" in r && r.cancelled ? false : "confirmed" in r ? r.confirmed : false, + ), - pendingExtensionRequests.set(id, { - resolve: (response: RpcExtensionUIResponse) => { - opts?.signal?.removeEventListener("abort", onAbort); - if ("cancelled" in response && response.cancelled) { - resolve(undefined); - } else if ("value" in response) { - resolve(response.value); - } else { - resolve(undefined); - } - }, - reject, - }); - output({ type: "extension_ui_request", id, method: "select", title, options } as RpcExtensionUIRequest); - }); - }, - - async confirm(title: string, message: string, opts?: { signal?: AbortSignal }): Promise { - if (opts?.signal?.aborted) { - return false; - } - - const id = crypto.randomUUID(); - return new Promise((resolve, reject) => { - const onAbort = () => { - pendingExtensionRequests.delete(id); - resolve(false); - }; - opts?.signal?.addEventListener("abort", onAbort, { once: true }); - - pendingExtensionRequests.set(id, { - resolve: (response: RpcExtensionUIResponse) => { - opts?.signal?.removeEventListener("abort", onAbort); - if ("cancelled" in response && response.cancelled) { - resolve(false); - } else if ("confirmed" in response) { - resolve(response.confirmed); - } else { - resolve(false); - } - }, - reject, - }); - output({ type: "extension_ui_request", id, method: "confirm", title, message } as RpcExtensionUIRequest); - }); - }, - - async input(title: string, placeholder?: string, opts?: { signal?: AbortSignal }): Promise { - if (opts?.signal?.aborted) { - return undefined; - } - - const id = crypto.randomUUID(); - return new Promise((resolve, reject) => { - const onAbort = () => { - pendingExtensionRequests.delete(id); - resolve(undefined); - }; - opts?.signal?.addEventListener("abort", onAbort, { once: true }); - - pendingExtensionRequests.set(id, { - resolve: (response: RpcExtensionUIResponse) => { - opts?.signal?.removeEventListener("abort", onAbort); - if ("cancelled" in response && response.cancelled) { - resolve(undefined); - } else if ("value" in response) { - resolve(response.value); - } else { - resolve(undefined); - } - }, - reject, - }); - output({ type: "extension_ui_request", id, method: "input", title, placeholder } as RpcExtensionUIRequest); - }); - }, + input: (title, placeholder, opts) => + createDialogPromise(opts, undefined, { method: "input", title, placeholder, timeout: opts?.timeout }, (r) => + "cancelled" in r && r.cancelled ? undefined : "value" in r ? r.value : undefined, + ), notify(message: string, type?: "info" | "warning" | "error"): void { // Fire and forget - no response needed diff --git a/packages/coding-agent/src/modes/rpc/rpc-types.ts b/packages/coding-agent/src/modes/rpc/rpc-types.ts index 0ae638ba..f8bc0c59 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-types.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-types.ts @@ -177,9 +177,16 @@ export type RpcResponse = /** Emitted when an extension needs user input */ export type RpcExtensionUIRequest = - | { type: "extension_ui_request"; id: string; method: "select"; title: string; options: string[] } - | { type: "extension_ui_request"; id: string; method: "confirm"; title: string; message: string } - | { type: "extension_ui_request"; id: string; method: "input"; title: string; placeholder?: string } + | { type: "extension_ui_request"; id: string; method: "select"; title: string; options: string[]; timeout?: number } + | { type: "extension_ui_request"; id: string; method: "confirm"; title: string; message: string; timeout?: number } + | { + type: "extension_ui_request"; + id: string; + method: "input"; + title: string; + placeholder?: string; + timeout?: number; + } | { type: "extension_ui_request"; id: string; method: "editor"; title: string; prefill?: string } | { type: "extension_ui_request";