feat(desktop): embed BoosAPI Desktop client as a subproject

Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as
a monorepo subdirectory under desktop/. Source only — node_modules,
dist, Cargo target and the upstream .git history are excluded.

desktop/ keeps its own independent Cargo workspace (desktop/src-tauri
with crates/*); it is not part of the backend boosapi crate, so a
`cargo check` at the metapi root still compiles only the backend. Build
the desktop app from desktop/src-tauri per desktop/README.md.

Note: app icons are still placeholder assets pending the final BoosAPI
logo. The updater signing keys live outside the repo and are not
included.
This commit is contained in:
boos
2026-06-22 11:32:57 +08:00
parent d9c78ecc73
commit e0a5f57793
2305 changed files with 449355 additions and 0 deletions
@@ -0,0 +1,408 @@
use crate::api::ApiClient;
use crate::costtracker::CostTracker;
use crate::hooks::HookConfig;
use crate::mcp::{self, McpClient};
use crate::tools::ToolRegistry;
use crate::types::*;
use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
/// Configuration options for creating an agent.
pub struct AgentOptions {
/// LLM model ID (default: claude-sonnet-4-6-20250514).
pub model: Option<String>,
/// API key for authentication.
pub api_key: Option<String>,
/// Custom API base URL.
pub base_url: Option<String>,
/// Custom LLM provider (bypasses built-in ApiClient creation).
pub provider: Option<Arc<dyn crate::LLMProvider>>,
/// Working directory for tool execution.
pub cwd: Option<String>,
/// Custom system prompt (replaces default).
pub system_prompt: Option<String>,
/// Additional text appended to the default system prompt.
pub append_system_prompt: Option<String>,
/// Skills context summary for system prompt injection.
pub skills_summary: Option<String>,
/// Maximum number of agentic turns (default: 100).
pub max_turns: Option<u32>,
/// Maximum budget in USD.
pub max_budget_usd: Option<f64>,
/// Permission mode for tool access control.
pub permission_mode: Option<PermissionMode>,
/// Tool names to allow (whitelist).
pub allowed_tools: Option<Vec<String>>,
/// Tool names to deny (blacklist).
pub disallowed_tools: Option<Vec<String>>,
/// Custom permission callback.
pub can_use_tool: Option<CanUseToolFn>,
/// Callback for user interaction (AskUserQuestion tool).
pub ask_fn: Option<crate::tools::askuser::AskUserFn>,
/// Custom tools to register.
pub custom_tools: Vec<Arc<dyn Tool>>,
/// MCP server configurations.
pub mcp_servers: HashMap<String, McpServerConfig>,
/// Subagent definitions.
pub agents: HashMap<String, SubagentDefinition>,
/// Extended thinking configuration.
pub thinking: Option<ThinkingConfig>,
/// Maximum output tokens per response.
pub max_tokens: Option<u64>,
/// Structured output JSON schema.
pub json_schema: Option<Value>,
/// Hook configuration.
pub hooks: Option<HookConfig>,
/// Cancellation token used to stop the running agent loop and tools.
pub abort_signal: Option<CancellationToken>,
/// Custom HTTP headers for API requests.
pub custom_headers: HashMap<String, String>,
/// Resume the most recent session in the current working directory.
pub continue_session: Option<bool>,
/// Resume a specific session by ID.
pub resume: Option<String>,
/// Fork instead of continuing a session.
pub fork_session: Option<bool>,
/// Control whether the session is persisted to disk.
pub persist_session: Option<bool>,
/// Explicit session ID to use (instead of auto-generating one).
pub session_id: Option<String>,
/// Effort level for reasoning: "low", "medium", "high", "max".
pub effort: Option<String>,
/// Fallback model if primary is unavailable.
pub fallback_model: Option<String>,
/// Sandbox configuration for restricting tool execution.
pub sandbox: Option<SandboxSettings>,
/// Enable debug mode.
pub debug: Option<bool>,
/// Additional working directories.
pub additional_directories: Option<Vec<String>>,
}
impl Default for AgentOptions {
fn default() -> Self {
Self {
model: None,
api_key: None,
base_url: None,
provider: None,
cwd: None,
system_prompt: None,
append_system_prompt: None,
skills_summary: None,
max_turns: None,
max_budget_usd: None,
permission_mode: None,
allowed_tools: None,
disallowed_tools: None,
can_use_tool: None,
ask_fn: None,
custom_tools: Vec::new(),
mcp_servers: HashMap::new(),
agents: HashMap::new(),
thinking: None,
max_tokens: None,
json_schema: None,
hooks: None,
abort_signal: None,
custom_headers: HashMap::new(),
continue_session: None,
resume: None,
fork_session: None,
persist_session: None,
session_id: None,
effort: None,
fallback_model: None,
sandbox: None,
debug: None,
additional_directories: None,
}
}
}
/// Subagent definition.
#[derive(Debug, Clone)]
pub struct SubagentDefinition {
pub description: String,
pub instructions: Option<String>,
pub tools: Option<Vec<String>>,
pub model: Option<String>,
}
/// The main Agent struct that orchestrates the agentic loop.
pub struct Agent {
pub(crate) api_client: ApiClient,
pub(crate) registry: ToolRegistry,
/// Conversation messages.
pub messages: Vec<Message>,
/// Working directory.
pub cwd: String,
/// Custom system prompt.
pub system_prompt: Option<String>,
/// Additional system prompt text.
pub append_system_prompt: Option<String>,
/// Skills context summary.
pub skills_summary: Option<String>,
/// Maximum agentic turns.
pub max_turns: u32,
/// Maximum budget in USD.
pub max_budget_usd: Option<f64>,
pub(crate) cost_tracker: CostTracker,
pub(crate) mcp_client: Arc<McpClient>,
/// Subagent definitions.
pub agents: HashMap<String, SubagentDefinition>,
/// Extended thinking configuration.
pub thinking: Option<ThinkingConfig>,
/// Maximum output tokens.
pub max_tokens: Option<u64>,
/// Structured output JSON schema.
pub json_schema: Option<Value>,
/// Hook configuration.
pub hooks: HookConfig,
pub(crate) can_use_tool: Option<CanUseToolFn>,
pub(crate) session_id: String,
pub(crate) abort_signal: CancellationToken,
}
impl Agent {
/// Create a new agent with the given options.
pub async fn new(options: AgentOptions) -> Result<Self, String> {
let cwd = options.cwd.unwrap_or_else(|| {
std::env::current_dir()
.unwrap()
.to_string_lossy()
.to_string()
});
let api_client = if let Some(provider) = options.provider {
ApiClient::with_provider(provider, options.model)
} else {
ApiClient::new(options.api_key, options.base_url, options.model)
};
let mut registry = ToolRegistry::default_registry();
// Replace default AskUserTool with configured one if ask_fn is provided
if let Some(ask_fn) = options.ask_fn {
registry.register_replace(Arc::new(crate::tools::askuser::AskUserTool::new(ask_fn)));
}
// Register custom tools
for tool in options.custom_tools {
registry.register(tool);
}
// Apply allowed/disallowed tool filters
if let Some(allowed) = &options.allowed_tools {
let allowed_refs: Vec<&str> = allowed.iter().map(|s| s.as_str()).collect();
registry.retain(&allowed_refs);
}
if let Some(disallowed) = &options.disallowed_tools {
let disallowed_refs: Vec<&str> = disallowed.iter().map(|s| s.as_str()).collect();
registry.remove(&disallowed_refs);
}
// Connect MCP servers
let mcp_client = Arc::new(McpClient::new());
for (name, config) in &options.mcp_servers {
match mcp_client.connect(name, config.clone()).await {
Ok(tools) => {
let mcp_tools = mcp::create_mcp_tools(name, &tools, mcp_client.clone());
for tool in mcp_tools {
registry.register(tool);
}
}
Err(e) => {
tracing::warn!("Failed to connect MCP server '{}': {}", name, e);
}
}
}
let session_id = uuid::Uuid::new_v4().to_string();
Ok(Self {
api_client,
registry,
messages: Vec::new(),
cwd,
system_prompt: options.system_prompt,
append_system_prompt: options.append_system_prompt,
skills_summary: options.skills_summary,
max_turns: options.max_turns.unwrap_or(100),
max_budget_usd: options.max_budget_usd,
cost_tracker: CostTracker::new(),
mcp_client,
agents: options.agents,
thinking: options.thinking,
max_tokens: options.max_tokens,
json_schema: options.json_schema,
hooks: options.hooks.unwrap_or_default(),
can_use_tool: options.can_use_tool,
session_id,
abort_signal: options.abort_signal.unwrap_or_default(),
})
}
/// Execute a query and return a channel of streaming SDK messages.
pub async fn query(
&mut self,
prompt: &str,
) -> (mpsc::Receiver<SDKMessage>, tokio::task::JoinHandle<()>) {
let (tx, rx) = mpsc::channel(100);
// Add user message
let user_msg = crate::utils::messages::create_user_message(prompt);
self.messages.push(user_msg);
// Clone what we need for the async loop
let messages = self.messages.clone();
let api_client = self.api_client.clone();
let cwd = self.cwd.clone();
let system_prompt = self.system_prompt.clone();
let append_system_prompt = self.append_system_prompt.clone();
let skills_summary = self.skills_summary.clone();
let max_turns = self.max_turns;
let max_budget_usd = self.max_budget_usd;
let cost_tracker = self.cost_tracker.clone();
let thinking = self.thinking.clone();
let max_tokens = self.max_tokens;
let registry = std::sync::Arc::new(self.registry_snapshot());
let can_use_tool = self.can_use_tool.clone();
let abort_signal = self.abort_signal.clone();
let handle = tokio::spawn(async move {
let result: Result<Vec<Message>, String> = super::r#loop::run_loop(
api_client,
messages,
registry,
&cwd,
system_prompt.as_deref(),
append_system_prompt.as_deref(),
skills_summary.as_deref(),
max_turns,
max_budget_usd,
&cost_tracker,
thinking,
max_tokens,
can_use_tool.as_ref(),
abort_signal,
tx.clone(),
)
.await;
if let Err(e) = result {
let _ = tx
.send(SDKMessage::Error {
message: e.to_string(),
})
.await;
}
});
(rx, handle)
}
/// Blocking query that returns a complete QueryResult.
pub async fn prompt(&mut self, text: &str) -> Result<QueryResult, String> {
let start = std::time::Instant::now();
let (mut rx, handle) = self.query(text).await;
let mut result_text = String::new();
let mut usage = Usage::default();
let mut num_turns = 0;
let mut cost_usd = 0.0;
let mut all_messages = Vec::new();
while let Some(msg) = rx.recv().await {
match msg {
SDKMessage::Result {
text,
usage: u,
num_turns: t,
cost_usd: c,
messages,
..
} => {
result_text = text;
usage = u;
num_turns = t;
cost_usd = c;
all_messages = messages;
}
SDKMessage::Assistant { message, .. } => {
let text = crate::types::extract_text(&message);
if !text.is_empty() {
result_text = text;
}
all_messages.push(message);
}
SDKMessage::Error { message } => {
return Err(message);
}
_ => {}
}
}
let _ = handle.await;
let duration_ms = start.elapsed().as_millis() as u64;
// Update agent's message history
self.messages = all_messages.clone();
Ok(QueryResult {
text: result_text,
usage,
num_turns,
cost_usd,
duration_ms,
messages: all_messages,
})
}
/// Get current conversation messages.
pub fn get_messages(&self) -> &[Message] {
&self.messages
}
/// Clear conversation history.
pub fn clear(&mut self) {
self.messages.clear();
}
/// Set the model.
pub fn set_model(&mut self, model: &str) {
self.api_client.set_model(model.to_string());
}
/// Get the current model.
pub fn model(&self) -> &str {
self.api_client.model()
}
/// Get the cost tracker.
pub fn cost_tracker(&self) -> &CostTracker {
&self.cost_tracker
}
/// Get session ID.
pub fn session_id(&self) -> &str {
&self.session_id
}
/// Close the agent and clean up resources.
pub async fn close(&self) {
self.mcp_client.close_all().await;
}
/// Create a snapshot of the current registry for use in async context.
fn registry_snapshot(&self) -> ToolRegistry {
let mut snapshot = ToolRegistry::new();
for tool in self.registry.all() {
snapshot.register(tool);
}
snapshot
}
}