use futures::FutureExt;
use futures::future::BoxFuture;
use oino_ai::JsonValue;
use oino_core::extensions::settings::SettingsLayer;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use crate::backend::{ClientError, SessionBackend};
use crate::protocol::{
AgentInfo, AuthProviderInfo, Checkpoint, CompletionGroup, EditorEvent, ExtensionInfo,
OinoCommand, QueueEdit, QueueKind, RewindResult, RpcResponseData, RunningBash, SettingsScope,
TopicValue,
};
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ShortcutInfo {
pub id: String,
#[serde(default)]
pub default_keys: Vec<String>,
#[serde(default)]
pub description: String,
#[serde(default)]
pub extension_id: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RewindOptions {
pub files: bool,
pub conversation: bool,
}
impl Default for RewindOptions {
fn default() -> Self {
Self {
files: true,
conversation: true,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
pub struct AgentTree {
pub agents: Vec<AgentInfo>,
pub follow: bool,
}
fn whole<T: DeserializeOwned>(data: RpcResponseData) -> Result<T, ClientError> {
serde_json::from_value(data.unwrap_or(JsonValue::Null))
.map_err(|e| ClientError::Protocol(e.to_string()))
}
fn field<T: DeserializeOwned>(data: RpcResponseData, name: &str) -> Result<T, ClientError> {
let value = data
.and_then(|mut d| d.get_mut(name).map(JsonValue::take))
.unwrap_or(JsonValue::Null);
serde_json::from_value(value).map_err(|e| ClientError::Protocol(format!("{name}: {e}")))
}
pub trait FrontendApi {
fn completions(
&self,
text: &str,
cursor: usize,
) -> BoxFuture<'_, Result<Vec<CompletionGroup>, ClientError>>;
fn ui_action(
&self,
panel_id: &str,
action_id: &str,
value: Option<JsonValue>,
) -> BoxFuture<'_, Result<(), ClientError>>;
fn ui_visible(&self, entry_id: &str, visible: bool) -> BoxFuture<'_, Result<(), ClientError>>;
fn ui_resync(&self, rev: u64) -> BoxFuture<'_, Result<JsonValue, ClientError>>;
fn editor_event(&self, event: EditorEvent) -> BoxFuture<'_, Result<bool, ClientError>>;
fn invoke_shortcut(&self, id: &str) -> BoxFuture<'_, Result<(), ClientError>>;
fn shortcuts(&self) -> BoxFuture<'_, Result<Vec<ShortcutInfo>, ClientError>>;
fn settings_schema(&self) -> BoxFuture<'_, Result<Vec<JsonValue>, ClientError>>;
fn extension_settings(
&self,
namespace: &str,
layer: SettingsLayer,
) -> BoxFuture<'_, Result<JsonValue, ClientError>>;
fn set_extension_setting(
&self,
namespace: &str,
key: &str,
value: Option<JsonValue>,
scope: SettingsScope,
) -> BoxFuture<'_, Result<(), ClientError>>;
fn settings(&self) -> BoxFuture<'_, Result<JsonValue, ClientError>>;
fn set_setting(
&self,
key: &str,
value: Option<JsonValue>,
scope: SettingsScope,
) -> BoxFuture<'_, Result<(), ClientError>>;
fn service_call(
&self,
target: Option<&str>,
name: &str,
payload: JsonValue,
) -> BoxFuture<'_, Result<JsonValue, ClientError>>;
fn auth_providers(&self) -> BoxFuture<'_, Result<Vec<AuthProviderInfo>, ClientError>>;
fn login(&self, provider: &str) -> BoxFuture<'_, Result<(), ClientError>>;
fn logout(&self, provider: &str) -> BoxFuture<'_, Result<(), ClientError>>;
fn set_api_key(&self, provider: &str, key: &str) -> BoxFuture<'_, Result<(), ClientError>>;
fn topics(
&self,
subscribe: &[&str],
unsubscribe: &[&str],
) -> BoxFuture<'_, Result<Vec<TopicValue>, ClientError>>;
fn bus_get(&self, topic: &str) -> BoxFuture<'_, Result<Option<JsonValue>, ClientError>>;
fn agents(&self, follow: Option<bool>) -> BoxFuture<'_, Result<AgentTree, ClientError>>;
fn agent_messages(
&self,
agent_id: &str,
) -> BoxFuture<'_, Result<Vec<oino_ai::AgentMessage>, ClientError>>;
fn agent_abort(&self, agent_id: &str) -> BoxFuture<'_, Result<(), ClientError>>;
fn queue_edit(&self, edit: QueueEdit) -> BoxFuture<'_, Result<(), ClientError>>;
fn queue_remove(
&self,
queue: QueueKind,
index: usize,
) -> BoxFuture<'_, Result<(), ClientError>>;
fn queue_send_now(
&self,
queue: QueueKind,
index: usize,
) -> BoxFuture<'_, Result<(), ClientError>>;
fn queue_move(
&self,
queue: QueueKind,
from: usize,
to: usize,
) -> BoxFuture<'_, Result<(), ClientError>>;
fn queue_set_text(
&self,
queue: QueueKind,
index: usize,
text: &str,
) -> BoxFuture<'_, Result<(), ClientError>>;
fn checkpoints(&self) -> BoxFuture<'_, Result<Vec<Checkpoint>, ClientError>>;
fn rewind(
&self,
entry_id: &str,
options: RewindOptions,
) -> BoxFuture<'_, Result<RewindResult, ClientError>>;
fn running_bash(&self) -> BoxFuture<'_, Result<Vec<RunningBash>, ClientError>>;
fn extensions(&self) -> BoxFuture<'_, Result<Vec<ExtensionInfo>, ClientError>>;
fn set_extension_enabled(
&self,
id: &str,
enabled: bool,
) -> BoxFuture<'_, Result<(), ClientError>>;
fn render(&self, request: RenderRow) -> BoxFuture<'_, Result<Option<JsonValue>, ClientError>>;
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RenderRow {
pub target: String,
pub key: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub entry_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub turn_entry_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub turn_index: Option<u64>,
#[serde(default)]
pub running: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub density: Option<String>,
#[serde(default)]
pub expanded: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub width: Option<String>,
#[serde(default)]
pub data: JsonValue,
}
impl<B: SessionBackend + ?Sized> FrontendApi for B {
fn completions(
&self,
text: &str,
cursor: usize,
) -> BoxFuture<'_, Result<Vec<CompletionGroup>, ClientError>> {
let command = OinoCommand::GetCompletions {
text: text.to_owned(),
cursor,
};
self.send(command.into())
.map(|r| field(r?, "groups"))
.boxed()
}
fn ui_action(
&self,
panel_id: &str,
action_id: &str,
value: Option<JsonValue>,
) -> BoxFuture<'_, Result<(), ClientError>> {
let command = OinoCommand::UiAction {
panel_id: panel_id.to_owned(),
action_id: action_id.to_owned(),
value,
values: None,
path: None,
source: None,
};
self.send(command.into()).map(|r| r.map(drop)).boxed()
}
fn ui_visible(&self, entry_id: &str, visible: bool) -> BoxFuture<'_, Result<(), ClientError>> {
let command = OinoCommand::UiVisible {
entry_id: entry_id.to_owned(),
visible,
frontend: None,
};
self.send(command.into()).map(|r| r.map(drop)).boxed()
}
fn ui_resync(&self, rev: u64) -> BoxFuture<'_, Result<JsonValue, ClientError>> {
let command = OinoCommand::UiResync { scope: None, rev };
self.send(command.into()).map(|r| whole(r?)).boxed()
}
fn editor_event(&self, event: EditorEvent) -> BoxFuture<'_, Result<bool, ClientError>> {
self.send(OinoCommand::EditorEvent(event).into())
.map(|r| field(r?, "consumed"))
.boxed()
}
fn invoke_shortcut(&self, id: &str) -> BoxFuture<'_, Result<(), ClientError>> {
let command = OinoCommand::InvokeShortcut {
shortcut_id: id.to_owned(),
};
self.send(command.into()).map(|r| r.map(drop)).boxed()
}
fn shortcuts(&self) -> BoxFuture<'_, Result<Vec<ShortcutInfo>, ClientError>> {
self.send(OinoCommand::GetShortcuts.into())
.map(|r| field(r?, "shortcuts"))
.boxed()
}
fn settings_schema(&self) -> BoxFuture<'_, Result<Vec<JsonValue>, ClientError>> {
self.send(OinoCommand::GetSettingsSchema.into())
.map(|r| field(r?, "namespaces"))
.boxed()
}
fn extension_settings(
&self,
namespace: &str,
layer: SettingsLayer,
) -> BoxFuture<'_, Result<JsonValue, ClientError>> {
let command = OinoCommand::GetExtensionSettings {
namespace: namespace.to_owned(),
scope: layer,
};
self.send(command.into())
.map(|r| field(r?, "values"))
.boxed()
}
fn set_extension_setting(
&self,
namespace: &str,
key: &str,
value: Option<JsonValue>,
scope: SettingsScope,
) -> BoxFuture<'_, Result<(), ClientError>> {
let command = match value {
Some(value) => OinoCommand::SetExtensionSetting {
namespace: namespace.to_owned(),
key: key.to_owned(),
value: Some(value),
scope,
},
None => OinoCommand::UnsetExtensionSetting {
namespace: namespace.to_owned(),
key: key.to_owned(),
scope,
},
};
self.send(command.into()).map(|r| r.map(drop)).boxed()
}
fn settings(&self) -> BoxFuture<'_, Result<JsonValue, ClientError>> {
self.send(OinoCommand::GetSettings { key: None }.into())
.map(|r| field(r?, "settings"))
.boxed()
}
fn set_setting(
&self,
key: &str,
value: Option<JsonValue>,
scope: SettingsScope,
) -> BoxFuture<'_, Result<(), ClientError>> {
let command = OinoCommand::SetSetting {
key: key.to_owned(),
value,
scope,
};
self.send(command.into()).map(|r| r.map(drop)).boxed()
}
fn service_call(
&self,
target: Option<&str>,
name: &str,
payload: JsonValue,
) -> BoxFuture<'_, Result<JsonValue, ClientError>> {
let command = OinoCommand::ServiceCall {
target: target.map(str::to_owned),
name: name.to_owned(),
payload,
};
self.send(command.into())
.map(|r| r.map(Option::unwrap_or_default))
.boxed()
}
fn auth_providers(&self) -> BoxFuture<'_, Result<Vec<AuthProviderInfo>, ClientError>> {
self.send(OinoCommand::AuthProviders.into())
.map(|r| field(r?, "providers"))
.boxed()
}
fn login(&self, provider: &str) -> BoxFuture<'_, Result<(), ClientError>> {
let command = OinoCommand::Login {
provider: provider.to_owned(),
};
self.send(command.into()).map(|r| r.map(drop)).boxed()
}
fn logout(&self, provider: &str) -> BoxFuture<'_, Result<(), ClientError>> {
let command = OinoCommand::Logout {
provider: provider.to_owned(),
};
self.send(command.into()).map(|r| r.map(drop)).boxed()
}
fn set_api_key(&self, provider: &str, key: &str) -> BoxFuture<'_, Result<(), ClientError>> {
let command = OinoCommand::SetApiKey {
provider: provider.to_owned(),
key: key.to_owned(),
};
self.send(command.into()).map(|r| r.map(drop)).boxed()
}
fn topics(
&self,
subscribe: &[&str],
unsubscribe: &[&str],
) -> BoxFuture<'_, Result<Vec<TopicValue>, ClientError>> {
let command = OinoCommand::Topics {
subscribe: subscribe.iter().map(|s| (*s).to_owned()).collect(),
unsubscribe: unsubscribe.iter().map(|s| (*s).to_owned()).collect(),
};
self.send(command.into())
.map(|r| field(r?, "topics"))
.boxed()
}
fn bus_get(&self, topic: &str) -> BoxFuture<'_, Result<Option<JsonValue>, ClientError>> {
let command = OinoCommand::BusGet {
topic: topic.to_owned(),
};
self.send(command.into())
.map(|r| field::<JsonValue>(r?, "data").map(|v| (!v.is_null()).then_some(v)))
.boxed()
}
fn agents(&self, follow: Option<bool>) -> BoxFuture<'_, Result<AgentTree, ClientError>> {
self.send(OinoCommand::Agents { follow }.into())
.map(|r| whole(r?))
.boxed()
}
fn agent_messages(
&self,
agent_id: &str,
) -> BoxFuture<'_, Result<Vec<oino_ai::AgentMessage>, ClientError>> {
let command = OinoCommand::AgentMessages {
agent_id: agent_id.to_owned(),
};
self.send(command.into())
.map(|r| field(r?, "messages"))
.boxed()
}
fn agent_abort(&self, agent_id: &str) -> BoxFuture<'_, Result<(), ClientError>> {
let command = OinoCommand::AgentAbort {
agent_id: agent_id.to_owned(),
};
self.send(command.into()).map(|r| r.map(drop)).boxed()
}
fn queue_edit(&self, edit: QueueEdit) -> BoxFuture<'_, Result<(), ClientError>> {
self.send(OinoCommand::QueueEdit(edit).into())
.map(|r| r.map(drop))
.boxed()
}
fn queue_remove(
&self,
queue: QueueKind,
index: usize,
) -> BoxFuture<'_, Result<(), ClientError>> {
self.queue_edit(QueueEdit::Remove { queue, index })
}
fn queue_send_now(
&self,
queue: QueueKind,
index: usize,
) -> BoxFuture<'_, Result<(), ClientError>> {
self.queue_edit(QueueEdit::SendNow { queue, index })
}
fn queue_move(
&self,
queue: QueueKind,
from: usize,
to: usize,
) -> BoxFuture<'_, Result<(), ClientError>> {
self.queue_edit(QueueEdit::Move { queue, from, to })
}
fn queue_set_text(
&self,
queue: QueueKind,
index: usize,
text: &str,
) -> BoxFuture<'_, Result<(), ClientError>> {
self.queue_edit(QueueEdit::Edit {
queue,
index,
text: text.to_owned(),
})
}
fn checkpoints(&self) -> BoxFuture<'_, Result<Vec<Checkpoint>, ClientError>> {
self.send(OinoCommand::Checkpoints.into())
.map(|r| field(r?, "checkpoints"))
.boxed()
}
fn rewind(
&self,
entry_id: &str,
options: RewindOptions,
) -> BoxFuture<'_, Result<RewindResult, ClientError>> {
let command = OinoCommand::Rewind {
entry_id: entry_id.to_owned(),
files: options.files,
conversation: options.conversation,
};
self.send(command.into()).map(|r| whole(r?)).boxed()
}
fn running_bash(&self) -> BoxFuture<'_, Result<Vec<RunningBash>, ClientError>> {
self.send(OinoCommand::RunningBash.into())
.map(|r| field(r?, "calls"))
.boxed()
}
fn extensions(&self) -> BoxFuture<'_, Result<Vec<ExtensionInfo>, ClientError>> {
self.send(OinoCommand::Extensions.into())
.map(|r| field(r?, "extensions"))
.boxed()
}
fn render(&self, request: RenderRow) -> BoxFuture<'_, Result<Option<JsonValue>, ClientError>> {
let command = OinoCommand::Render {
target: request.target,
key: request.key,
entry_id: request.entry_id,
turn_entry_id: request.turn_entry_id,
turn_index: request.turn_index,
running: request.running,
density: request.density,
expanded: request.expanded,
width: request.width,
data: request.data,
};
self.send(command.into())
.map(|r| field::<JsonValue>(r?, "node").map(|v| (!v.is_null()).then_some(v)))
.boxed()
}
fn set_extension_enabled(
&self,
id: &str,
enabled: bool,
) -> BoxFuture<'_, Result<(), ClientError>> {
let command = OinoCommand::SetExtensionEnabled {
id: id.to_owned(),
enabled,
};
self.send(command.into()).map(|r| r.map(drop)).boxed()
}
}