pub mod codemode;
pub mod discovery;
mod events;
mod grants;
pub mod tool_search;
mod ui;
pub use events::{EventPolicy, delivers};
pub use grants::{CAPABILITIES, Grants, receives, tool_granted};
pub use ui::{
ComponentHost, CustomOptions, DialogOptions, ExtensionUi, NoUi, NotifyKind, Placement,
RemoteComponent, TerminalState, Widget, WorkingIndicator,
};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use futures_util::future::BoxFuture;
use indexmap::IndexMap;
use serde_json::Value;
use tokio_util::sync::CancellationToken;
use yapi_ai::model_catalog::RefreshOptions;
use yapi_ai::registry::{Availability, ModelRegistry};
use yapi_types::autocomplete::ArgumentCompletions;
use yapi_types::model::Model;
use yapi_types::models::ModelDefinition;
use yapi_types::rpc::SourceInfo;
use crate::agent_session::WeakSession;
use crate::tools::RegisteredTool;
use crate::tools::registry::ToolRegistry;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Mode {
Tui,
Rpc,
#[default]
Print,
Json,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ToolInfo {
pub name: String,
pub description: String,
pub parameters: Value,
pub exposure: crate::tools::Exposure,
pub namespace: Option<crate::tools::Namespace>,
}
#[derive(Clone, Default)]
pub struct Tools(Arc<Mutex<ToolRegistry>>);
impl Tools {
pub fn new(registry: ToolRegistry) -> Tools {
Tools(Arc::new(Mutex::new(registry)))
}
fn lock(&self) -> std::sync::MutexGuard<'_, ToolRegistry> {
yapi_types::sync::lock(&self.0)
}
pub fn with<T>(&self, f: impl FnOnce(&mut ToolRegistry) -> T) -> T {
f(&mut self.lock())
}
pub fn register(&self, tool: RegisteredTool) {
self.lock().register(tool);
}
pub fn active(&self) -> Vec<String> {
self.lock().active()
}
pub fn set_active(&self, names: Vec<String>) {
self.lock().set_active(names);
}
pub fn all(&self) -> Vec<ToolInfo> {
self.lock().all().iter().map(RegisteredTool::info).collect()
}
}
#[derive(Clone)]
pub struct Context {
pub cwd: PathBuf,
pub agent_dir: PathBuf,
pub project_dir: PathBuf,
pub project_trusted: bool,
pub mode: Mode,
pub ui: Arc<dyn ExtensionUi>,
pub tools: Tools,
pub cancel: CancellationToken,
pub session: WeakSession,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Command {
pub name: String,
pub description: String,
}
#[derive(Clone)]
pub struct ResolvedCommand {
pub invocation: String,
pub command: Command,
pub extension: Arc<dyn Extension>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Shortcut {
pub key: String,
pub description: Option<String>,
}
#[derive(Clone)]
pub struct ShortcutBinding {
pub key: String,
pub registered: String,
pub description: Option<String>,
pub path: String,
pub extension: Arc<dyn Extension>,
}
pub const RESERVED_ACTIONS: [&str; 18] = [
"app.interrupt",
"app.clear",
"app.exit",
"app.suspend",
"app.thinking.cycle",
"app.model.cycleForward",
"app.model.cycleBackward",
"app.model.select",
"app.tools.expand",
"app.thinking.toggle",
"app.editor.external",
"app.message.copy",
"app.message.followUp",
"tui.input.submit",
"tui.select.confirm",
"tui.select.cancel",
"tui.input.copy",
"tui.editor.deleteToLineEnd",
];
pub fn resolve_shortcuts(
extensions: &[Arc<dyn Extension>],
builtin: &[(String, Vec<String>)],
) -> (Vec<ShortcutBinding>, Vec<(String, String)>) {
let mut taken: std::collections::HashMap<String, (&str, bool)> =
std::collections::HashMap::new();
for (action, keys) in builtin {
let reserved = RESERVED_ACTIONS.contains(&action.as_str());
for key in keys {
let key = key.to_lowercase();
if taken
.get(&key)
.is_some_and(|(_, existing)| *existing && !reserved)
{
continue;
}
taken.insert(key, (action.as_str(), reserved));
}
}
let mut bindings: Vec<ShortcutBinding> = Vec::new();
let mut warnings = Vec::new();
for extension in extensions {
let path = extension.source().path;
for shortcut in extension.shortcuts() {
let key = shortcut.key.to_lowercase();
match taken.get(&key) {
Some((_, true)) => {
warnings.push((
path.clone(),
format!(
"Extension shortcut '{}' from {path} conflicts with built-in shortcut. Skipping.",
shortcut.key
),
));
continue;
}
Some((action, false)) => warnings.push((
path.clone(),
format!(
"Extension shortcut conflict: '{}' is built-in shortcut for {action} and {path}. Using {path}.",
shortcut.key
),
)),
None => {}
}
let binding = ShortcutBinding {
key: key.clone(),
registered: shortcut.key.clone(),
description: shortcut.description,
path: path.clone(),
extension: extension.clone(),
};
match bindings.iter_mut().find(|existing| existing.key == key) {
Some(existing) => {
warnings.push((
path.clone(),
format!(
"Extension shortcut conflict: '{}' registered by both {} and {path}. Using {path}.",
shortcut.key, existing.path
),
));
*existing = binding;
}
None => bindings.push(binding),
}
}
}
(bindings, warnings)
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ToolRenderers {
pub call: bool,
pub result: bool,
pub own_shell: bool,
}
#[derive(Clone, Default)]
pub struct Loadout {
pub declared: Vec<RegisteredTool>,
pub callable: Vec<RegisteredTool>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct LoadoutChanges {
pub descriptions: std::collections::HashMap<String, String>,
pub hidden: std::collections::HashSet<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Renderers {
pub tools: std::collections::HashMap<String, ToolRenderers>,
pub messages: Vec<String>,
pub entries: Vec<String>,
pub markdown: bool,
pub code: bool,
}
pub trait Extension: Send + Sync {
fn source(&self) -> SourceInfo;
fn grants(&self) -> Grants {
Grants::default()
}
fn warn(&self, _message: &str) {}
fn load(&self, _tools: &Tools) {}
fn commands(&self) -> Vec<Command> {
Vec::new()
}
fn complete(&self, _command: &str, _prefix: &str) -> ArgumentCompletions {
ArgumentCompletions::Ready(None)
}
fn shortcuts(&self) -> Vec<Shortcut> {
Vec::new()
}
fn run_shortcut<'a>(
&'a self,
_key: &'a str,
_ctx: &'a Context,
) -> BoxFuture<'a, Result<(), String>> {
Box::pin(async { Ok(()) })
}
fn run_command<'a>(
&'a self,
_command: &'a str,
_args: &'a str,
_ctx: &'a Context,
) -> BoxFuture<'a, ()> {
Box::pin(async {})
}
fn settle(&self) -> BoxFuture<'_, u64> {
Box::pin(async { 0 })
}
fn session_start<'a>(&'a self, _ctx: &'a Context) -> BoxFuture<'a, ()> {
Box::pin(async {})
}
fn session_shutdown<'a>(&'a self, _ctx: &'a Context) -> BoxFuture<'a, ()> {
Box::pin(async {})
}
fn end_session<'a>(&'a self, _session_id: &'a str) -> BoxFuture<'a, ()> {
Box::pin(async {})
}
fn before_agent_start<'a>(
&'a self,
_ctx: &'a Context,
_sections: &'a mut IndexMap<String, String>,
) -> BoxFuture<'a, ()> {
Box::pin(async {})
}
fn handles(&self, _kind: &str) -> bool {
false
}
fn handle<'a>(&'a self, _ctx: &'a Context, _event: &'a Value) -> BoxFuture<'a, Option<Value>> {
Box::pin(async { None })
}
fn prepare_loadout(&self, _loadout: &Loadout) -> LoadoutChanges {
LoadoutChanges::default()
}
fn refresh_loadout<'a>(&'a self, _loadout: &'a Loadout) -> BoxFuture<'a, ()> {
Box::pin(async {})
}
fn renderers(&self) -> Renderers {
Renderers::default()
}
fn component<'a>(&'a self, _request: &'a Value) -> BoxFuture<'a, Option<RemoteComponent>> {
Box::pin(async { None })
}
fn transform_markdown<'a>(
&'a self,
_request: &'a Value,
) -> BoxFuture<'a, Result<Option<String>, String>> {
Box::pin(async { Ok(None) })
}
fn render_code<'a>(
&'a self,
_request: &'a Value,
) -> BoxFuture<'a, Result<Option<Vec<String>>, String>> {
Box::pin(async { Ok(None) })
}
fn tool_call<'a>(
&'a self,
_ctx: &'a Context,
_tool: &'a str,
_input: &'a Value,
) -> BoxFuture<'a, Option<String>> {
Box::pin(async { None })
}
fn run_bash<'a>(
&'a self,
_handle: &'a Value,
_command: &'a str,
_cwd: &'a std::path::Path,
_output: tokio::sync::mpsc::UnboundedSender<Vec<u8>>,
_cancel: CancellationToken,
) -> BoxFuture<'a, Result<Option<i32>, String>> {
Box::pin(async { Err("This extension has no bash operations".to_owned()) })
}
fn refresh_models<'a>(
&'a self,
_registry: &'a ModelRegistry,
_options: &'a RefreshOptions,
) -> BoxFuture<'a, Vec<(String, ModelList)>> {
Box::pin(async { Vec::new() })
}
}
pub type Setup =
Box<dyn FnOnce(crate::agent_session::AgentSession) -> BoxFuture<'static, ()> + Send>;
pub enum SessionAction {
New {
parent: Option<String>,
setup: Option<Setup>,
},
Fork {
entry_id: String,
at: bool,
},
Tree {
target_id: String,
options: crate::agent_session::TreeNavigation,
},
Switch {
path: String,
},
Reload,
Replaced,
}
pub type SessionActions =
Arc<dyn Fn(SessionAction) -> BoxFuture<'static, Result<bool, String>> + Send + Sync>;
#[derive(Clone)]
pub struct BashOperations {
pub extension: Arc<dyn Extension>,
pub handle: Value,
}
pub type ModelList = Result<ModelUpdate, String>;
#[derive(Debug)]
pub enum ModelUpdate {
Definitions(Vec<ModelDefinition>),
Modified(Option<Vec<Model>>),
Availability(Availability),
}
pub fn builtins() -> Vec<Arc<dyn Extension>> {
vec![
Arc::new(tool_search::ToolSearchExtension),
Arc::new(crate::mcp::extension::McpExtension::new()),
]
}
pub fn builtin_source(name: &str) -> SourceInfo {
SourceInfo {
path: format!("builtin:{name}"),
source: "builtin".into(),
scope: "temporary".into(),
origin: "top-level".into(),
base_dir: None,
}
}