Skip to main content

yapi_core/extensions/
ui.rs

1//! What extensions show, as pi's `ExtensionUIContext`: dialogs,
2//! notifications, footer statuses, widgets, the editor and custom components.
3
4use std::sync::Arc;
5use std::time::Duration;
6
7use futures_util::future::BoxFuture;
8use serde_json::Value;
9use tokio_util::sync::CancellationToken;
10
11/// How a notification is shown.
12#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
13pub enum NotifyKind {
14    /// A status message.
15    #[default]
16    Info,
17    /// A warning.
18    Warning,
19    /// An error.
20    Error,
21}
22
23impl NotifyKind {
24    /// pi's name for the kind.
25    pub fn as_str(self) -> &'static str {
26        match self {
27            NotifyKind::Info => "info",
28            NotifyKind::Warning => "warning",
29            NotifyKind::Error => "error",
30        }
31    }
32}
33
34/// What extensions see of the terminal.
35#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
36pub struct TerminalState {
37    /// Its size in columns and rows.
38    pub size: (usize, usize),
39    /// The prompt editor, or an extension's editor in its place, has the
40    /// keyboard, rather than a selector, a dialog or an extension component,
41    /// as pi-tui's `focusedComponent` tells.
42    pub editor_focused: bool,
43}
44
45/// pi's `ExtensionUIDialogOptions`: when a dialog closes on its own.
46#[derive(Clone, Debug, Default)]
47pub struct DialogOptions {
48    /// Closes the dialog as cancelled after this long. Interactive dialogs
49    /// count down in their title.
50    pub timeout: Option<Duration>,
51    /// Closes the dialog as cancelled once cancelled.
52    pub cancel: Option<CancellationToken>,
53}
54
55/// pi's `WidgetPlacement`: where a widget goes.
56#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
57pub enum Placement {
58    /// Above the editor.
59    #[default]
60    AboveEditor,
61    /// Below the editor.
62    BelowEditor,
63}
64
65/// pi's `WorkingIndicatorOptions`: the animation shown while the agent works.
66#[derive(Clone, Debug, Default, PartialEq)]
67pub struct WorkingIndicator {
68    /// Frames drawn as given, escape sequences included. `None` keeps the
69    /// default spinner frames, and an empty list hides the indicator.
70    pub frames: Option<Vec<String>>,
71    /// Milliseconds per frame, or `None` for the default.
72    pub interval_ms: Option<u64>,
73}
74
75/// What a widget shows.
76#[derive(Clone, Debug)]
77pub enum Widget {
78    /// Lines of text with escape sequences.
79    Lines(Vec<String>),
80    /// A component the extension renders.
81    Component(RemoteComponent),
82}
83
84/// The extension runtime side of components: renders them and delivers
85/// input to them.
86pub trait ComponentHost: Send + Sync {
87    /// The lines of component `handle` at `width` columns: text with escape
88    /// sequences, and pi-tui's cursor marker where a focused component wants
89    /// the cursor. Empty once the component is gone.
90    fn render(&self, handle: u32, width: u16) -> BoxFuture<'static, Vec<String>>;
91
92    /// Delivers raw terminal input to component `handle`.
93    fn input(&self, handle: u32, data: &str);
94
95    /// Delivers pi-tui's `TuiMouseEvent` `event` to component `handle`,
96    /// after the input delivered before it. Resolves to whether the
97    /// component took it, as pi-tui's `dispatchMouseEvent` decides.
98    fn mouse(&self, handle: u32, event: &Value) -> BoxFuture<'static, bool>;
99
100    /// Sends `op` to editor component `handle`, after the input delivered
101    /// before it: `{"op": "setText", "text"}`, `{"op": "addToHistory",
102    /// "text"}`, `{"op": "insertTextAtCursor", "text", "apart"}`, or
103    /// `{"op": "configure", "border", "paddingX", "autocompleteMaxVisible",
104    /// "focused", "rows"}`. `apart` sets the text apart from the words
105    /// around the cursor, and `border` is a thinking level or `bashMode`.
106    fn editor_op(&self, handle: u32, op: &Value);
107
108    /// Runs the runtime's `onTerminalInput` listeners over raw input `keys`,
109    /// in order. Answers the keys left to handle, as the listeners
110    /// transformed them, without those they consumed or emptied.
111    fn terminal_input(&self, keys: Vec<String>) -> BoxFuture<'static, Vec<String>>;
112
113    /// The suggestions of the runtime's autocomplete providers for pi's
114    /// provider arguments `request`, as for [`ExtensionUi::suggestions`],
115    /// with what applying each item gives: `{"prefix", "items", "applied":
116    /// [{"lines", "cursorLine", "cursorCol"}]}`, or `Null`.
117    fn suggestions(&self, request: Value) -> BoxFuture<'static, Value>;
118}
119
120/// A pi-tui component that lives in an extension runtime.
121#[derive(Clone)]
122pub struct RemoteComponent {
123    runtime: u64,
124    handle: u32,
125    host: Arc<dyn ComponentHost>,
126}
127
128impl std::fmt::Debug for RemoteComponent {
129    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130        f.debug_struct("RemoteComponent")
131            .field("runtime", &self.runtime)
132            .field("handle", &self.handle)
133            .finish_non_exhaustive()
134    }
135}
136
137impl RemoteComponent {
138    /// Component `handle` of `host`, the runtime numbered `runtime`.
139    pub fn new(runtime: u64, handle: u32, host: Arc<dyn ComponentHost>) -> RemoteComponent {
140        RemoteComponent {
141            runtime,
142            handle,
143            host,
144        }
145    }
146
147    /// What identifies it across runtimes: the runtime's number and its
148    /// handle there.
149    pub fn key(&self) -> (u64, u32) {
150        (self.runtime, self.handle)
151    }
152
153    /// Its lines at `width` columns (see [`ComponentHost::render`]).
154    pub fn render(&self, width: u16) -> BoxFuture<'static, Vec<String>> {
155        self.host.render(self.handle, width)
156    }
157
158    /// Delivers raw terminal input.
159    pub fn input(&self, data: &str) {
160        self.host.input(self.handle, data);
161    }
162
163    /// Delivers a mouse event (see [`ComponentHost::mouse`]).
164    pub fn mouse(&self, event: &Value) -> BoxFuture<'static, bool> {
165        self.host.mouse(self.handle, event)
166    }
167
168    /// Sends an operation to the editor it is, as
169    /// [`ComponentHost::editor_op`] describes.
170    pub fn editor_op(&self, op: &Value) {
171        self.host.editor_op(self.handle, op);
172    }
173}
174
175/// The options of pi's `custom`: how a custom component is shown.
176#[derive(Clone, Debug, Default)]
177pub struct CustomOptions {
178    /// Over the screen instead of in the editor's place.
179    pub overlay: bool,
180    /// pi's `OverlayOptions`: anchor, size and margins.
181    pub overlay_options: Value,
182}
183
184/// pi's `ExtensionUIContext` as a mode implements it. Dialogs a mode cannot
185/// show resolve as cancelled. Everything else does nothing by default.
186pub trait ExtensionUi: Send + Sync {
187    /// Whether a person can answer dialogs.
188    fn has_ui(&self) -> bool;
189
190    /// pi's `ctx.shutdown()`: the mode exits once the agent is idle.
191    fn shutdown(&self) {}
192
193    /// Shows a message.
194    fn notify(&self, message: &str, kind: NotifyKind);
195
196    /// Shows a message that names no kind. Hosts show it as information,
197    /// and RPC forwards it without a kind, as pi does.
198    fn notify_untyped(&self, message: &str) {
199        self.notify(message, NotifyKind::Info);
200    }
201
202    /// Asks to pick one of `options`, resolving to `None` when cancelled.
203    fn select(
204        &self,
205        _title: &str,
206        _options: Vec<String>,
207        _dialog: DialogOptions,
208    ) -> BoxFuture<'static, Option<String>> {
209        Box::pin(async { None })
210    }
211
212    /// Asks for a line of text, resolving to `None` when cancelled.
213    fn input(
214        &self,
215        _title: &str,
216        _placeholder: Option<&str>,
217        _dialog: DialogOptions,
218    ) -> BoxFuture<'static, Option<String>> {
219        Box::pin(async { None })
220    }
221
222    /// Asks a yes or no question, resolving to `false` when cancelled.
223    fn confirm(
224        &self,
225        _title: &str,
226        _message: &str,
227        _dialog: DialogOptions,
228    ) -> BoxFuture<'static, bool> {
229        Box::pin(async { false })
230    }
231
232    /// Asks for multi-line text starting from `prefill`, resolving to `None`
233    /// when cancelled.
234    fn editor(&self, _title: &str, _prefill: Option<&str>) -> BoxFuture<'static, Option<String>> {
235        Box::pin(async { None })
236    }
237
238    /// Sets the footer status `key`, or clears it.
239    fn set_status(&self, _key: &str, _text: Option<&str>) {}
240
241    /// Sets widget `key`, or removes it. Without a placement it goes above
242    /// the editor.
243    fn set_widget(&self, _key: &str, _widget: Option<Widget>, _placement: Option<Placement>) {}
244
245    /// Replaces the footer, or restores the built-in one.
246    fn set_footer(&self, _component: Option<RemoteComponent>) {}
247
248    /// Replaces the startup header, or restores the built-in one.
249    fn set_header(&self, _component: Option<RemoteComponent>) {}
250
251    /// Sets the terminal title.
252    fn set_title(&self, _title: &str) {}
253
254    /// Completions an extension computed in the background are ready. The
255    /// editor asks for its suggestions again.
256    fn refresh_completions(&self) {}
257
258    /// Sets the message shown while the agent works, or restores the default.
259    fn set_working_message(&self, _message: Option<&str>) {}
260
261    /// Shows or hides the indicator shown while the agent works.
262    fn set_working_visible(&self, _visible: bool) {}
263
264    /// Sets the animation shown while the agent works, or restores the
265    /// default spinner.
266    fn set_working_indicator(&self, _indicator: Option<WorkingIndicator>) {}
267
268    /// Sets the label of hidden thinking blocks, or restores the default.
269    fn set_hidden_thinking_label(&self, _label: Option<&str>) {}
270
271    /// Replaces the editor's text.
272    fn set_editor_text(&self, _text: &str) {}
273
274    /// Pastes into the editor as a terminal paste would.
275    fn paste_to_editor(&self, text: &str) {
276        self.set_editor_text(text);
277    }
278
279    /// The editor's text.
280    fn editor_text(&self) -> String {
281        String::new()
282    }
283
284    /// Whether [`ExtensionUi::custom`] and component widgets are shown.
285    fn shows_components(&self) -> bool {
286        false
287    }
288
289    /// Replaces the editor with `editor`, or restores the built-in one.
290    /// `embeds_status` when the editor shows the working status in its top
291    /// border.
292    fn set_editor(&self, _editor: Option<RemoteComponent>, _embeds_status: bool) {}
293
294    /// The extension's editor now holds `text`, paste markers expanded.
295    fn editor_changed(&self, _text: &str) {}
296
297    /// The extension's editor submitted `text`.
298    fn editor_submit(&self, _text: &str) {}
299
300    /// The extension's editor asks for app action `action`, a key binding id
301    /// such as `app.interrupt`.
302    fn editor_action(&self, _action: &str) {}
303
304    /// Starts passing raw input through the `onTerminalInput` listeners of
305    /// extension runtime `runtime`, which `listeners` runs, or stops with
306    /// `None`. Runtimes' listeners see input in the order they started.
307    fn set_terminal_input(&self, _runtime: u64, _listeners: Option<Arc<dyn ComponentHost>>) {}
308
309    /// Completes through the providers composed with
310    /// `addAutocompleteProvider`, which `providers` runs. `triggers` open
311    /// completion too. They replace the providers of the runtime that set
312    /// them before.
313    fn set_autocomplete(&self, _providers: Arc<dyn ComponentHost>, _triggers: Vec<String>) {}
314
315    /// What the built-in provider suggests for pi's provider arguments
316    /// `request`, `{"lines", "cursorLine", "cursorCol", "force"}` with
317    /// UTF-16 columns: pi's `AutocompleteSuggestions`, or `Null`.
318    fn suggestions(&self, _request: Value) -> BoxFuture<'static, Value> {
319        Box::pin(async { Value::Null })
320    }
321
322    /// The built-in provider's `applyCompletion` for `{"lines",
323    /// "cursorLine", "cursorCol", "item", "prefix"}`: `{"lines",
324    /// "cursorLine", "cursorCol"}`.
325    fn apply_completion(&self, _request: &Value) -> Value {
326        Value::Null
327    }
328
329    /// Whether raw input `data` is an extension shortcut, which then runs.
330    fn editor_shortcut(&self, _data: &str) -> bool {
331        false
332    }
333
334    /// The key bindings components match keys against: `{"kitty",
335    /// "bindings": {id: [key]}, "actions": [id]}`, where `actions` are the
336    /// app actions an extension's editor can ask for. `Null` keeps pi-tui's
337    /// defaults.
338    fn keybindings(&self) -> Value {
339        Value::Null
340    }
341
342    /// Shows `component` with keyboard focus until [`ExtensionUi::close`].
343    fn custom(&self, _component: RemoteComponent, _options: CustomOptions) {}
344
345    /// Removes the custom component `component`.
346    fn close(&self, _component: RemoteComponent) {}
347
348    /// Removes the editor, widgets, footer and header that extension runtime
349    /// `runtime` showed, which it lost when it restarted. The built-in
350    /// editor, footer and header return.
351    fn drop_components(&self, _runtime: u64) {}
352
353    /// Shows a screen of the `/mcp` manager with keyboard focus, opening the
354    /// manager. [`McpScreen::Close`] closes it. Without the TUI the screen
355    /// is dropped, which closes its menu.
356    ///
357    /// [`McpScreen::Close`]: crate::mcp::extension::McpScreen::Close
358    fn mcp_manager(&self, _screen: crate::mcp::extension::McpScreen) {}
359
360    /// Components changed and need rendering.
361    fn request_render(&self) {}
362
363    /// An extension registered transcript renderers or a Markdown
364    /// transformer after its session started. What draws next uses them.
365    fn renderers_changed(&self) {}
366
367    /// Whether tool output is expanded.
368    fn tools_expanded(&self) -> bool {
369        false
370    }
371
372    /// Expands or collapses tool output.
373    fn set_tools_expanded(&self, _expanded: bool) {}
374
375    /// The terminal's state, kept current as it changes, or `None` without a
376    /// terminal, as in print, JSON and RPC modes.
377    fn terminal(&self) -> Option<tokio::sync::watch::Receiver<TerminalState>> {
378        None
379    }
380
381    /// What custom footers read: `{"gitBranch", "statuses": [[key, text]],
382    /// "providers"}`.
383    fn footer_data(&self) -> Value {
384        Value::Null
385    }
386
387    /// The theme as escape sequences, and the colors they come from:
388    /// `{"name", "mode", "fg": {token: sequence}, "bg": {token: sequence},
389    /// "dim": [token], "colors": {token: color}}`. `Null` leaves text
390    /// unstyled.
391    fn theme(&self) -> Value {
392        Value::Null
393    }
394
395    /// The theme called `name`, as [`ExtensionUi::theme`] describes it, or
396    /// `Null` when there is none.
397    fn get_theme(&self, _name: &str) -> Value {
398        Value::Null
399    }
400
401    /// pi's `getAllThemes`: the themes [`ExtensionUi::set_theme`] can switch
402    /// to, as `[{"name", "path"}]` with no `path` for a theme without a file.
403    /// Empty without a UI that switches themes, as in pi's RPC mode.
404    fn all_themes(&self) -> Value {
405        Value::Array(Vec::new())
406    }
407
408    /// Switches to `theme`: a theme name, which is saved as `/settings`
409    /// saves it, or a theme document `{"name", "appearance", "colors"}`,
410    /// which is not saved. The error says why it could not.
411    fn set_theme(&self, _theme: &Value) -> Result<(), String> {
412        Err("UI not available".into())
413    }
414
415    /// A handler of extension `path` failed on `event`, with the error's
416    /// stack when the runtime gave one. By default it reports as print mode
417    /// does.
418    fn extension_error(&self, path: &str, _event: &str, error: &str, _stack: Option<&str>) {
419        use std::io::Write as _;
420        let _ = writeln!(std::io::stderr(), "Extension error ({path}): {error}");
421    }
422}
423
424/// The UI of print and JSON modes: nothing is shown.
425pub struct NoUi;
426
427impl ExtensionUi for NoUi {
428    fn has_ui(&self) -> bool {
429        false
430    }
431
432    fn notify(&self, _message: &str, _kind: NotifyKind) {}
433}