use std::sync::Arc;
use std::time::Duration;
use futures_util::future::BoxFuture;
use serde_json::Value;
use tokio_util::sync::CancellationToken;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum NotifyKind {
#[default]
Info,
Warning,
Error,
}
impl NotifyKind {
pub fn as_str(self) -> &'static str {
match self {
NotifyKind::Info => "info",
NotifyKind::Warning => "warning",
NotifyKind::Error => "error",
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct TerminalState {
pub size: (usize, usize),
pub editor_focused: bool,
}
#[derive(Clone, Debug, Default)]
pub struct DialogOptions {
pub timeout: Option<Duration>,
pub cancel: Option<CancellationToken>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Placement {
#[default]
AboveEditor,
BelowEditor,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct WorkingIndicator {
pub frames: Option<Vec<String>>,
pub interval_ms: Option<u64>,
}
#[derive(Clone, Debug)]
pub enum Widget {
Lines(Vec<String>),
Component(RemoteComponent),
}
pub trait ComponentHost: Send + Sync {
fn render(&self, handle: u32, width: u16) -> BoxFuture<'static, Vec<String>>;
fn input(&self, handle: u32, data: &str);
fn mouse(&self, handle: u32, event: &Value) -> BoxFuture<'static, bool>;
fn editor_op(&self, handle: u32, op: &Value);
fn terminal_input(&self, keys: Vec<String>) -> BoxFuture<'static, Vec<String>>;
fn suggestions(&self, request: Value) -> BoxFuture<'static, Value>;
}
#[derive(Clone)]
pub struct RemoteComponent {
runtime: u64,
handle: u32,
host: Arc<dyn ComponentHost>,
}
impl std::fmt::Debug for RemoteComponent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RemoteComponent")
.field("runtime", &self.runtime)
.field("handle", &self.handle)
.finish_non_exhaustive()
}
}
impl RemoteComponent {
pub fn new(runtime: u64, handle: u32, host: Arc<dyn ComponentHost>) -> RemoteComponent {
RemoteComponent {
runtime,
handle,
host,
}
}
pub fn key(&self) -> (u64, u32) {
(self.runtime, self.handle)
}
pub fn render(&self, width: u16) -> BoxFuture<'static, Vec<String>> {
self.host.render(self.handle, width)
}
pub fn input(&self, data: &str) {
self.host.input(self.handle, data);
}
pub fn mouse(&self, event: &Value) -> BoxFuture<'static, bool> {
self.host.mouse(self.handle, event)
}
pub fn editor_op(&self, op: &Value) {
self.host.editor_op(self.handle, op);
}
}
#[derive(Clone, Debug, Default)]
pub struct CustomOptions {
pub overlay: bool,
pub overlay_options: Value,
}
pub trait ExtensionUi: Send + Sync {
fn has_ui(&self) -> bool;
fn shutdown(&self) {}
fn notify(&self, message: &str, kind: NotifyKind);
fn notify_untyped(&self, message: &str) {
self.notify(message, NotifyKind::Info);
}
fn select(
&self,
_title: &str,
_options: Vec<String>,
_dialog: DialogOptions,
) -> BoxFuture<'static, Option<String>> {
Box::pin(async { None })
}
fn input(
&self,
_title: &str,
_placeholder: Option<&str>,
_dialog: DialogOptions,
) -> BoxFuture<'static, Option<String>> {
Box::pin(async { None })
}
fn confirm(
&self,
_title: &str,
_message: &str,
_dialog: DialogOptions,
) -> BoxFuture<'static, bool> {
Box::pin(async { false })
}
fn editor(&self, _title: &str, _prefill: Option<&str>) -> BoxFuture<'static, Option<String>> {
Box::pin(async { None })
}
fn set_status(&self, _key: &str, _text: Option<&str>) {}
fn set_widget(&self, _key: &str, _widget: Option<Widget>, _placement: Option<Placement>) {}
fn set_footer(&self, _component: Option<RemoteComponent>) {}
fn set_header(&self, _component: Option<RemoteComponent>) {}
fn set_title(&self, _title: &str) {}
fn refresh_completions(&self) {}
fn set_working_message(&self, _message: Option<&str>) {}
fn set_working_visible(&self, _visible: bool) {}
fn set_working_indicator(&self, _indicator: Option<WorkingIndicator>) {}
fn set_hidden_thinking_label(&self, _label: Option<&str>) {}
fn set_editor_text(&self, _text: &str) {}
fn paste_to_editor(&self, text: &str) {
self.set_editor_text(text);
}
fn editor_text(&self) -> String {
String::new()
}
fn shows_components(&self) -> bool {
false
}
fn set_editor(&self, _editor: Option<RemoteComponent>, _embeds_status: bool) {}
fn editor_changed(&self, _text: &str) {}
fn editor_submit(&self, _text: &str) {}
fn editor_action(&self, _action: &str) {}
fn set_terminal_input(&self, _runtime: u64, _listeners: Option<Arc<dyn ComponentHost>>) {}
fn set_autocomplete(&self, _providers: Arc<dyn ComponentHost>, _triggers: Vec<String>) {}
fn suggestions(&self, _request: Value) -> BoxFuture<'static, Value> {
Box::pin(async { Value::Null })
}
fn apply_completion(&self, _request: &Value) -> Value {
Value::Null
}
fn editor_shortcut(&self, _data: &str) -> bool {
false
}
fn keybindings(&self) -> Value {
Value::Null
}
fn custom(&self, _component: RemoteComponent, _options: CustomOptions) {}
fn close(&self, _component: RemoteComponent) {}
fn drop_components(&self, _runtime: u64) {}
fn mcp_manager(&self, _screen: crate::mcp::extension::McpScreen) {}
fn request_render(&self) {}
fn renderers_changed(&self) {}
fn tools_expanded(&self) -> bool {
false
}
fn set_tools_expanded(&self, _expanded: bool) {}
fn terminal(&self) -> Option<tokio::sync::watch::Receiver<TerminalState>> {
None
}
fn footer_data(&self) -> Value {
Value::Null
}
fn theme(&self) -> Value {
Value::Null
}
fn get_theme(&self, _name: &str) -> Value {
Value::Null
}
fn all_themes(&self) -> Value {
Value::Array(Vec::new())
}
fn set_theme(&self, _theme: &Value) -> Result<(), String> {
Err("UI not available".into())
}
fn extension_error(&self, path: &str, _event: &str, error: &str, _stack: Option<&str>) {
use std::io::Write as _;
let _ = writeln!(std::io::stderr(), "Extension error ({path}): {error}");
}
}
pub struct NoUi;
impl ExtensionUi for NoUi {
fn has_ui(&self) -> bool {
false
}
fn notify(&self, _message: &str, _kind: NotifyKind) {}
}