feat: Agent Orchestrator — multi-project agent dashboard

Tauri + Svelte 5 + Rust application for orchestrating multiple AI coding agents.
Includes Claude, Aider, Codex, and Ollama provider support, multi-agent
communication (btmsg/bttask), session anchors, plugin sandbox, FTS5 search,
Landlock sandboxing, and 507 vitest + 110 cargo tests.
This commit is contained in:
DexterFromLab 2026-03-15 15:45:27 +01:00
commit 3672e92b7e
272 changed files with 68600 additions and 0 deletions

View file

@ -0,0 +1,58 @@
use tauri::State;
use crate::AppState;
use crate::sidecar::AgentQueryOptions;
use bterminal_core::sandbox::SandboxConfig;
#[tauri::command]
#[tracing::instrument(skip(state, options), fields(session_id = %options.session_id))]
pub fn agent_query(
state: State<'_, AppState>,
options: AgentQueryOptions,
) -> Result<(), String> {
state.sidecar_manager.query(&options)
}
#[tauri::command]
#[tracing::instrument(skip(state))]
pub fn agent_stop(state: State<'_, AppState>, session_id: String) -> Result<(), String> {
state.sidecar_manager.stop_session(&session_id)
}
#[tauri::command]
pub fn agent_ready(state: State<'_, AppState>) -> bool {
state.sidecar_manager.is_ready()
}
#[tauri::command]
#[tracing::instrument(skip(state))]
pub fn agent_restart(state: State<'_, AppState>) -> Result<(), String> {
state.sidecar_manager.restart()
}
/// Update sidecar sandbox configuration and restart to apply.
/// `project_cwds` — directories needing read+write access.
/// `worktree_roots` — optional worktree directories.
/// `enabled` — whether Landlock sandboxing is active.
#[tauri::command]
#[tracing::instrument(skip(state))]
pub fn agent_set_sandbox(
state: State<'_, AppState>,
project_cwds: Vec<String>,
worktree_roots: Vec<String>,
enabled: bool,
) -> Result<(), String> {
let cwd_refs: Vec<&str> = project_cwds.iter().map(|s| s.as_str()).collect();
let wt_refs: Vec<&str> = worktree_roots.iter().map(|s| s.as_str()).collect();
let mut sandbox = SandboxConfig::for_projects(&cwd_refs, &wt_refs);
sandbox.enabled = enabled;
state.sidecar_manager.set_sandbox(sandbox);
// Restart sidecar so Landlock restrictions take effect on the new process
if state.sidecar_manager.is_ready() {
state.sidecar_manager.restart()?;
}
Ok(())
}

View file

@ -0,0 +1,152 @@
use crate::btmsg;
use crate::groups;
#[tauri::command]
pub fn btmsg_get_agents(group_id: String) -> Result<Vec<btmsg::BtmsgAgent>, String> {
btmsg::get_agents(&group_id)
}
#[tauri::command]
pub fn btmsg_unread_count(agent_id: String) -> Result<i32, String> {
btmsg::unread_count(&agent_id)
}
#[tauri::command]
pub fn btmsg_unread_messages(agent_id: String) -> Result<Vec<btmsg::BtmsgMessage>, String> {
btmsg::unread_messages(&agent_id)
}
#[tauri::command]
pub fn btmsg_history(agent_id: String, other_id: String, limit: i32) -> Result<Vec<btmsg::BtmsgMessage>, String> {
btmsg::history(&agent_id, &other_id, limit)
}
#[tauri::command]
pub fn btmsg_send(from_agent: String, to_agent: String, content: String) -> Result<String, String> {
btmsg::send_message(&from_agent, &to_agent, &content)
}
#[tauri::command]
pub fn btmsg_set_status(agent_id: String, status: String) -> Result<(), String> {
btmsg::set_status(&agent_id, &status)
}
#[tauri::command]
pub fn btmsg_ensure_admin(group_id: String) -> Result<(), String> {
btmsg::ensure_admin(&group_id)
}
#[tauri::command]
pub fn btmsg_all_feed(group_id: String, limit: i32) -> Result<Vec<btmsg::BtmsgFeedMessage>, String> {
btmsg::all_feed(&group_id, limit)
}
#[tauri::command]
pub fn btmsg_mark_read(reader_id: String, sender_id: String) -> Result<(), String> {
btmsg::mark_read_conversation(&reader_id, &sender_id)
}
#[tauri::command]
pub fn btmsg_get_channels(group_id: String) -> Result<Vec<btmsg::BtmsgChannel>, String> {
btmsg::get_channels(&group_id)
}
#[tauri::command]
pub fn btmsg_channel_messages(channel_id: String, limit: i32) -> Result<Vec<btmsg::BtmsgChannelMessage>, String> {
btmsg::get_channel_messages(&channel_id, limit)
}
#[tauri::command]
pub fn btmsg_channel_send(channel_id: String, from_agent: String, content: String) -> Result<String, String> {
btmsg::send_channel_message(&channel_id, &from_agent, &content)
}
#[tauri::command]
pub fn btmsg_create_channel(name: String, group_id: String, created_by: String) -> Result<String, String> {
btmsg::create_channel(&name, &group_id, &created_by)
}
#[tauri::command]
pub fn btmsg_add_channel_member(channel_id: String, agent_id: String) -> Result<(), String> {
btmsg::add_channel_member(&channel_id, &agent_id)
}
/// Register all agents from a GroupsFile into the btmsg database.
/// Creates/updates agent records, sets up contact permissions, ensures review channels.
#[tauri::command]
pub fn btmsg_register_agents(config: groups::GroupsFile) -> Result<(), String> {
btmsg::register_agents_from_groups(&config)
}
// ---- Per-message acknowledgment (seen_messages) ----
#[tauri::command]
pub fn btmsg_unseen_messages(agent_id: String, session_id: String) -> Result<Vec<btmsg::BtmsgMessage>, String> {
btmsg::unseen_messages(&agent_id, &session_id)
}
#[tauri::command]
pub fn btmsg_mark_seen(session_id: String, message_ids: Vec<String>) -> Result<(), String> {
btmsg::mark_messages_seen(&session_id, &message_ids)
}
#[tauri::command]
pub fn btmsg_prune_seen() -> Result<u64, String> {
btmsg::prune_seen_messages(7 * 24 * 3600, 200_000)
}
// ---- Heartbeat monitoring ----
#[tauri::command]
pub fn btmsg_record_heartbeat(agent_id: String) -> Result<(), String> {
btmsg::record_heartbeat(&agent_id)
}
#[tauri::command]
pub fn btmsg_get_stale_agents(group_id: String, threshold_secs: i64) -> Result<Vec<String>, String> {
btmsg::get_stale_agents(&group_id, threshold_secs)
}
#[tauri::command]
pub fn btmsg_get_agent_heartbeats(group_id: String) -> Result<Vec<btmsg::AgentHeartbeat>, String> {
btmsg::get_agent_heartbeats(&group_id)
}
// ---- Dead letter queue ----
#[tauri::command]
pub fn btmsg_get_dead_letters(group_id: String, limit: i32) -> Result<Vec<btmsg::DeadLetter>, String> {
btmsg::get_dead_letters(&group_id, limit)
}
#[tauri::command]
pub fn btmsg_clear_dead_letters(group_id: String) -> Result<(), String> {
btmsg::clear_dead_letters(&group_id)
}
#[tauri::command]
pub fn btmsg_queue_dead_letter(
from_agent: String,
to_agent: String,
content: String,
error: String,
) -> Result<(), String> {
btmsg::queue_dead_letter(&from_agent, &to_agent, &content, &error)
}
// ---- Audit log ----
#[tauri::command]
pub fn audit_log_event(agent_id: String, event_type: String, detail: String) -> Result<(), String> {
btmsg::log_audit_event(&agent_id, &event_type, &detail)
}
#[tauri::command]
pub fn audit_log_list(group_id: String, limit: i32, offset: i32) -> Result<Vec<btmsg::AuditEntry>, String> {
btmsg::get_audit_log(&group_id, limit, offset)
}
#[tauri::command]
pub fn audit_log_for_agent(agent_id: String, limit: i32) -> Result<Vec<btmsg::AuditEntry>, String> {
btmsg::get_audit_log_for_agent(&agent_id, limit)
}

View file

@ -0,0 +1,43 @@
use crate::bttask;
#[tauri::command]
pub fn bttask_list(group_id: String) -> Result<Vec<bttask::Task>, String> {
bttask::list_tasks(&group_id)
}
#[tauri::command]
pub fn bttask_comments(task_id: String) -> Result<Vec<bttask::TaskComment>, String> {
bttask::task_comments(&task_id)
}
#[tauri::command]
pub fn bttask_update_status(task_id: String, status: String, version: i64) -> Result<i64, String> {
bttask::update_task_status(&task_id, &status, version)
}
#[tauri::command]
pub fn bttask_add_comment(task_id: String, agent_id: String, content: String) -> Result<String, String> {
bttask::add_comment(&task_id, &agent_id, &content)
}
#[tauri::command]
pub fn bttask_create(
title: String,
description: String,
priority: String,
group_id: String,
created_by: String,
assigned_to: Option<String>,
) -> Result<String, String> {
bttask::create_task(&title, &description, &priority, &group_id, &created_by, assigned_to.as_deref())
}
#[tauri::command]
pub fn bttask_delete(task_id: String) -> Result<(), String> {
bttask::delete_task(&task_id)
}
#[tauri::command]
pub fn bttask_review_queue_count(group_id: String) -> Result<i64, String> {
bttask::review_queue_count(&group_id)
}

View file

@ -0,0 +1,158 @@
// Claude profile and skill discovery commands
#[derive(serde::Serialize)]
pub struct ClaudeProfile {
pub name: String,
pub email: Option<String>,
pub subscription_type: Option<String>,
pub display_name: Option<String>,
pub config_dir: String,
}
#[derive(serde::Serialize)]
pub struct ClaudeSkill {
pub name: String,
pub description: String,
pub source_path: String,
}
#[tauri::command]
pub fn claude_list_profiles() -> Vec<ClaudeProfile> {
let mut profiles = Vec::new();
let config_dir = dirs::config_dir().unwrap_or_default();
let profiles_dir = config_dir.join("switcher").join("profiles");
let alt_dir_root = config_dir.join("switcher-claude");
if let Ok(entries) = std::fs::read_dir(&profiles_dir) {
for entry in entries.flatten() {
if !entry.path().is_dir() { continue; }
let name = entry.file_name().to_string_lossy().to_string();
let toml_path = entry.path().join("profile.toml");
let (email, subscription_type, display_name) = if toml_path.exists() {
let content = std::fs::read_to_string(&toml_path).unwrap_or_else(|e| {
log::warn!("Failed to read {}: {e}", toml_path.display());
String::new()
});
(
extract_toml_value(&content, "email"),
extract_toml_value(&content, "subscription_type"),
extract_toml_value(&content, "display_name"),
)
} else {
(None, None, None)
};
let alt_path = alt_dir_root.join(&name);
let config_dir_str = if alt_path.exists() {
alt_path.to_string_lossy().to_string()
} else {
dirs::home_dir()
.unwrap_or_default()
.join(".claude")
.to_string_lossy()
.to_string()
};
profiles.push(ClaudeProfile {
name,
email,
subscription_type,
display_name,
config_dir: config_dir_str,
});
}
}
if profiles.is_empty() {
let home = dirs::home_dir().unwrap_or_default();
profiles.push(ClaudeProfile {
name: "default".to_string(),
email: None,
subscription_type: None,
display_name: None,
config_dir: home.join(".claude").to_string_lossy().to_string(),
});
}
profiles
}
fn extract_toml_value(content: &str, key: &str) -> Option<String> {
for line in content.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix(key) {
if let Some(rest) = rest.trim().strip_prefix('=') {
let val = rest.trim().trim_matches('"');
if !val.is_empty() {
return Some(val.to_string());
}
}
}
}
None
}
#[tauri::command]
pub fn claude_list_skills() -> Vec<ClaudeSkill> {
let mut skills = Vec::new();
let home = dirs::home_dir().unwrap_or_default();
let skills_dir = home.join(".claude").join("skills");
if let Ok(entries) = std::fs::read_dir(&skills_dir) {
for entry in entries.flatten() {
let path = entry.path();
let (name, skill_file) = if path.is_dir() {
let skill_md = path.join("SKILL.md");
if skill_md.exists() {
(entry.file_name().to_string_lossy().to_string(), skill_md)
} else {
continue;
}
} else if path.extension().map_or(false, |e| e == "md") {
let stem = path.file_stem().unwrap_or_default().to_string_lossy().to_string();
(stem, path.clone())
} else {
continue;
};
let description = if let Ok(content) = std::fs::read_to_string(&skill_file) {
content.lines()
.filter(|l| !l.trim().is_empty() && !l.starts_with('#'))
.next()
.unwrap_or("")
.trim()
.chars()
.take(120)
.collect()
} else {
String::new()
};
skills.push(ClaudeSkill {
name,
description,
source_path: skill_file.to_string_lossy().to_string(),
});
}
}
skills
}
#[tauri::command]
pub fn claude_read_skill(path: String) -> Result<String, String> {
let skills_dir = dirs::home_dir()
.ok_or("Cannot determine home directory")?
.join(".claude")
.join("skills");
let canonical_skills = skills_dir.canonicalize()
.map_err(|_| "Skills directory does not exist".to_string())?;
let canonical_path = std::path::Path::new(&path).canonicalize()
.map_err(|e| format!("Invalid skill path: {e}"))?;
if !canonical_path.starts_with(&canonical_skills) {
return Err("Access denied: path is outside skills directory".to_string());
}
std::fs::read_to_string(&canonical_path).map_err(|e| format!("Failed to read skill: {e}"))
}

View file

@ -0,0 +1,130 @@
// File browser commands (Files tab)
#[derive(serde::Serialize)]
pub struct DirEntry {
pub name: String,
pub path: String,
pub is_dir: bool,
pub size: u64,
pub ext: String,
}
/// Content types for file viewer routing
#[derive(serde::Serialize)]
#[serde(tag = "type")]
pub enum FileContent {
Text { content: String, lang: String },
Binary { message: String },
TooLarge { size: u64 },
}
#[tauri::command]
pub fn list_directory_children(path: String) -> Result<Vec<DirEntry>, String> {
let dir = std::path::Path::new(&path);
if !dir.is_dir() {
return Err(format!("Not a directory: {path}"));
}
let mut entries = Vec::new();
let read_dir = std::fs::read_dir(dir).map_err(|e| format!("Failed to read directory: {e}"))?;
for entry in read_dir {
let entry = entry.map_err(|e| format!("Failed to read entry: {e}"))?;
let metadata = entry.metadata().map_err(|e| format!("Failed to read metadata: {e}"))?;
let name = entry.file_name().to_string_lossy().into_owned();
if name.starts_with('.') {
continue;
}
let is_dir = metadata.is_dir();
let ext = if is_dir {
String::new()
} else {
std::path::Path::new(&name)
.extension()
.map(|e| e.to_string_lossy().to_lowercase())
.unwrap_or_default()
};
entries.push(DirEntry {
name,
path: entry.path().to_string_lossy().into_owned(),
is_dir,
size: metadata.len(),
ext,
});
}
entries.sort_by(|a, b| {
b.is_dir.cmp(&a.is_dir).then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase()))
});
Ok(entries)
}
#[tauri::command]
pub fn read_file_content(path: String) -> Result<FileContent, String> {
let file_path = std::path::Path::new(&path);
if !file_path.is_file() {
return Err(format!("Not a file: {path}"));
}
let metadata = std::fs::metadata(&path).map_err(|e| format!("Failed to read metadata: {e}"))?;
let size = metadata.len();
if size > 10 * 1024 * 1024 {
return Ok(FileContent::TooLarge { size });
}
let ext = file_path
.extension()
.map(|e| e.to_string_lossy().to_lowercase())
.unwrap_or_default();
let binary_exts = ["png", "jpg", "jpeg", "gif", "webp", "svg", "ico", "bmp",
"pdf", "zip", "tar", "gz", "7z", "rar",
"mp3", "mp4", "wav", "ogg", "webm", "avi",
"woff", "woff2", "ttf", "otf", "eot",
"exe", "dll", "so", "dylib", "wasm"];
if binary_exts.contains(&ext.as_str()) {
return Ok(FileContent::Binary { message: format!("Binary file ({ext}), {size} bytes") });
}
let content = std::fs::read_to_string(&path)
.map_err(|_| format!("Binary or non-UTF-8 file"))?;
let lang = match ext.as_str() {
"rs" => "rust",
"ts" | "tsx" => "typescript",
"js" | "jsx" | "mjs" | "cjs" => "javascript",
"py" => "python",
"svelte" => "svelte",
"html" | "htm" => "html",
"css" | "scss" | "less" => "css",
"json" => "json",
"toml" => "toml",
"yaml" | "yml" => "yaml",
"md" | "markdown" => "markdown",
"sh" | "bash" | "zsh" => "bash",
"sql" => "sql",
"xml" => "xml",
"csv" => "csv",
"dockerfile" => "dockerfile",
"lock" => "text",
_ => "text",
}.to_string();
Ok(FileContent::Text { content, lang })
}
#[tauri::command]
pub fn write_file_content(path: String, content: String) -> Result<(), String> {
let file_path = std::path::Path::new(&path);
if !file_path.is_file() {
return Err(format!("Not an existing file: {path}"));
}
std::fs::write(&path, content.as_bytes())
.map_err(|e| format!("Failed to write file: {e}"))
}
#[tauri::command]
pub async fn pick_directory(window: tauri::Window) -> Result<Option<String>, String> {
let dialog = rfd::AsyncFileDialog::new()
.set_title("Select Directory")
.set_parent(&window);
let folder = dialog.pick_folder().await;
Ok(folder.map(|f| f.path().to_string_lossy().into_owned()))
}

View file

@ -0,0 +1,16 @@
use crate::groups::{GroupsFile, MdFileEntry};
#[tauri::command]
pub fn groups_load() -> Result<GroupsFile, String> {
crate::groups::load_groups()
}
#[tauri::command]
pub fn groups_save(config: GroupsFile) -> Result<(), String> {
crate::groups::save_groups(&config)
}
#[tauri::command]
pub fn discover_markdown_files(cwd: String) -> Result<Vec<MdFileEntry>, String> {
crate::groups::discover_markdown_files(&cwd)
}

View file

@ -0,0 +1,67 @@
use tauri::State;
use crate::AppState;
use crate::{ctx, memora};
// --- ctx commands ---
#[tauri::command]
pub fn ctx_init_db(state: State<'_, AppState>) -> Result<(), String> {
state.ctx_db.init_db()
}
#[tauri::command]
pub fn ctx_register_project(state: State<'_, AppState>, name: String, description: String, work_dir: Option<String>) -> Result<(), String> {
state.ctx_db.register_project(&name, &description, work_dir.as_deref())
}
#[tauri::command]
pub fn ctx_get_context(state: State<'_, AppState>, project: String) -> Result<Vec<ctx::CtxEntry>, String> {
state.ctx_db.get_context(&project)
}
#[tauri::command]
pub fn ctx_get_shared(state: State<'_, AppState>) -> Result<Vec<ctx::CtxEntry>, String> {
state.ctx_db.get_shared()
}
#[tauri::command]
pub fn ctx_get_summaries(state: State<'_, AppState>, project: String, limit: i64) -> Result<Vec<ctx::CtxSummary>, String> {
state.ctx_db.get_summaries(&project, limit)
}
#[tauri::command]
pub fn ctx_search(state: State<'_, AppState>, query: String) -> Result<Vec<ctx::CtxEntry>, String> {
state.ctx_db.search(&query)
}
// --- Memora commands (read-only) ---
#[tauri::command]
pub fn memora_available(state: State<'_, AppState>) -> bool {
state.memora_db.is_available()
}
#[tauri::command]
pub fn memora_list(
state: State<'_, AppState>,
tags: Option<Vec<String>>,
limit: Option<i64>,
offset: Option<i64>,
) -> Result<memora::MemoraSearchResult, String> {
state.memora_db.list(tags, limit.unwrap_or(50), offset.unwrap_or(0))
}
#[tauri::command]
pub fn memora_search(
state: State<'_, AppState>,
query: String,
tags: Option<Vec<String>>,
limit: Option<i64>,
) -> Result<memora::MemoraSearchResult, String> {
state.memora_db.search(&query, tags, limit.unwrap_or(50))
}
#[tauri::command]
pub fn memora_get(state: State<'_, AppState>, id: i64) -> Result<Option<memora::MemoraNode>, String> {
state.memora_db.get(id)
}

View file

@ -0,0 +1,46 @@
// Miscellaneous commands — CLI args, URL opening, frontend telemetry
#[tauri::command]
pub fn cli_get_group() -> Option<String> {
let args: Vec<String> = std::env::args().collect();
let mut i = 1;
while i < args.len() {
if args[i] == "--group" {
if i + 1 < args.len() {
return Some(args[i + 1].clone());
}
} else if let Some(val) = args[i].strip_prefix("--group=") {
return Some(val.to_string());
}
i += 1;
}
None
}
#[tauri::command]
pub fn open_url(url: String) -> Result<(), String> {
if !url.starts_with("http://") && !url.starts_with("https://") {
return Err("Only http/https URLs are allowed".into());
}
std::process::Command::new("xdg-open")
.arg(&url)
.spawn()
.map_err(|e| format!("Failed to open URL: {e}"))?;
Ok(())
}
#[tauri::command]
pub fn is_test_mode() -> bool {
std::env::var("BTERMINAL_TEST").map_or(false, |v| v == "1")
}
#[tauri::command]
pub fn frontend_log(level: String, message: String, context: Option<serde_json::Value>) {
match level.as_str() {
"error" => tracing::error!(source = "frontend", ?context, "{message}"),
"warn" => tracing::warn!(source = "frontend", ?context, "{message}"),
"info" => tracing::info!(source = "frontend", ?context, "{message}"),
"debug" => tracing::debug!(source = "frontend", ?context, "{message}"),
_ => tracing::trace!(source = "frontend", ?context, "{message}"),
}
}

View file

@ -0,0 +1,17 @@
pub mod pty;
pub mod agent;
pub mod watcher;
pub mod session;
pub mod persistence;
pub mod knowledge;
pub mod claude;
pub mod groups;
pub mod files;
pub mod remote;
pub mod misc;
pub mod btmsg;
pub mod bttask;
pub mod notifications;
pub mod search;
pub mod plugins;
pub mod secrets;

View file

@ -0,0 +1,8 @@
// Notification commands — desktop notification via notify-rust
use crate::notifications;
#[tauri::command]
pub fn notify_desktop(title: String, body: String, urgency: String) -> Result<(), String> {
notifications::send_desktop_notification(&title, &body, &urgency)
}

View file

@ -0,0 +1,109 @@
use tauri::State;
use crate::AppState;
use crate::session::{AgentMessageRecord, ProjectAgentState, SessionMetric, SessionAnchorRecord};
// --- Agent message persistence ---
#[tauri::command]
pub fn agent_messages_save(
state: State<'_, AppState>,
session_id: String,
project_id: String,
sdk_session_id: Option<String>,
messages: Vec<AgentMessageRecord>,
) -> Result<(), String> {
state.session_db.save_agent_messages(
&session_id,
&project_id,
sdk_session_id.as_deref(),
&messages,
)
}
#[tauri::command]
pub fn agent_messages_load(
state: State<'_, AppState>,
project_id: String,
) -> Result<Vec<AgentMessageRecord>, String> {
state.session_db.load_agent_messages(&project_id)
}
// --- Project agent state ---
#[tauri::command]
pub fn project_agent_state_save(
state: State<'_, AppState>,
agent_state: ProjectAgentState,
) -> Result<(), String> {
state.session_db.save_project_agent_state(&agent_state)
}
#[tauri::command]
pub fn project_agent_state_load(
state: State<'_, AppState>,
project_id: String,
) -> Result<Option<ProjectAgentState>, String> {
state.session_db.load_project_agent_state(&project_id)
}
// --- Session metrics ---
#[tauri::command]
pub fn session_metric_save(
state: State<'_, AppState>,
metric: SessionMetric,
) -> Result<(), String> {
state.session_db.save_session_metric(&metric)
}
#[tauri::command]
pub fn session_metrics_load(
state: State<'_, AppState>,
project_id: String,
limit: i64,
) -> Result<Vec<SessionMetric>, String> {
state.session_db.load_session_metrics(&project_id, limit)
}
// --- Session anchors ---
#[tauri::command]
pub fn session_anchors_save(
state: State<'_, AppState>,
anchors: Vec<SessionAnchorRecord>,
) -> Result<(), String> {
state.session_db.save_session_anchors(&anchors)
}
#[tauri::command]
pub fn session_anchors_load(
state: State<'_, AppState>,
project_id: String,
) -> Result<Vec<SessionAnchorRecord>, String> {
state.session_db.load_session_anchors(&project_id)
}
#[tauri::command]
pub fn session_anchor_delete(
state: State<'_, AppState>,
id: String,
) -> Result<(), String> {
state.session_db.delete_session_anchor(&id)
}
#[tauri::command]
pub fn session_anchors_clear(
state: State<'_, AppState>,
project_id: String,
) -> Result<(), String> {
state.session_db.delete_project_anchors(&project_id)
}
#[tauri::command]
pub fn session_anchor_update_type(
state: State<'_, AppState>,
id: String,
anchor_type: String,
) -> Result<(), String> {
state.session_db.update_anchor_type(&id, &anchor_type)
}

View file

@ -0,0 +1,20 @@
// Plugin discovery and file access commands
use crate::AppState;
use crate::plugins;
#[tauri::command]
pub fn plugins_discover(state: tauri::State<'_, AppState>) -> Vec<plugins::PluginMeta> {
let plugins_dir = state.app_config.plugins_dir();
plugins::discover_plugins(&plugins_dir)
}
#[tauri::command]
pub fn plugin_read_file(
state: tauri::State<'_, AppState>,
plugin_id: String,
filename: String,
) -> Result<String, String> {
let plugins_dir = state.app_config.plugins_dir();
plugins::read_plugin_file(&plugins_dir, &plugin_id, &filename)
}

View file

@ -0,0 +1,33 @@
use tauri::State;
use crate::AppState;
use crate::pty::PtyOptions;
#[tauri::command]
#[tracing::instrument(skip(state), fields(shell = ?options.shell))]
pub fn pty_spawn(
state: State<'_, AppState>,
options: PtyOptions,
) -> Result<String, String> {
state.pty_manager.spawn(options)
}
#[tauri::command]
pub fn pty_write(state: State<'_, AppState>, id: String, data: String) -> Result<(), String> {
state.pty_manager.write(&id, &data)
}
#[tauri::command]
pub fn pty_resize(
state: State<'_, AppState>,
id: String,
cols: u16,
rows: u16,
) -> Result<(), String> {
state.pty_manager.resize(&id, cols, rows)
}
#[tauri::command]
#[tracing::instrument(skip(state))]
pub fn pty_kill(state: State<'_, AppState>, id: String) -> Result<(), String> {
state.pty_manager.kill(&id)
}

View file

@ -0,0 +1,85 @@
use tauri::State;
use crate::AppState;
use crate::remote::{self, RemoteMachineConfig, RemoteMachineInfo};
use crate::pty::PtyOptions;
use crate::sidecar::AgentQueryOptions;
#[tauri::command]
pub async fn remote_list(state: State<'_, AppState>) -> Result<Vec<RemoteMachineInfo>, String> {
Ok(state.remote_manager.list_machines().await)
}
#[tauri::command]
pub async fn remote_add(state: State<'_, AppState>, config: RemoteMachineConfig) -> Result<String, String> {
Ok(state.remote_manager.add_machine(config).await)
}
#[tauri::command]
pub async fn remote_remove(state: State<'_, AppState>, machine_id: String) -> Result<(), String> {
state.remote_manager.remove_machine(&machine_id).await
}
#[tauri::command]
#[tracing::instrument(skip(app, state))]
pub async fn remote_connect(app: tauri::AppHandle, state: State<'_, AppState>, machine_id: String) -> Result<(), String> {
state.remote_manager.connect(&app, &machine_id).await
}
#[tauri::command]
#[tracing::instrument(skip(state))]
pub async fn remote_disconnect(state: State<'_, AppState>, machine_id: String) -> Result<(), String> {
state.remote_manager.disconnect(&machine_id).await
}
#[tauri::command]
#[tracing::instrument(skip(state, options), fields(session_id = %options.session_id))]
pub async fn remote_agent_query(state: State<'_, AppState>, machine_id: String, options: AgentQueryOptions) -> Result<(), String> {
state.remote_manager.agent_query(&machine_id, &options).await
}
#[tauri::command]
#[tracing::instrument(skip(state))]
pub async fn remote_agent_stop(state: State<'_, AppState>, machine_id: String, session_id: String) -> Result<(), String> {
state.remote_manager.agent_stop(&machine_id, &session_id).await
}
#[tauri::command]
#[tracing::instrument(skip(state), fields(shell = ?options.shell))]
pub async fn remote_pty_spawn(state: State<'_, AppState>, machine_id: String, options: PtyOptions) -> Result<String, String> {
state.remote_manager.pty_spawn(&machine_id, &options).await
}
#[tauri::command]
pub async fn remote_pty_write(state: State<'_, AppState>, machine_id: String, id: String, data: String) -> Result<(), String> {
state.remote_manager.pty_write(&machine_id, &id, &data).await
}
#[tauri::command]
pub async fn remote_pty_resize(state: State<'_, AppState>, machine_id: String, id: String, cols: u16, rows: u16) -> Result<(), String> {
state.remote_manager.pty_resize(&machine_id, &id, cols, rows).await
}
#[tauri::command]
pub async fn remote_pty_kill(state: State<'_, AppState>, machine_id: String, id: String) -> Result<(), String> {
state.remote_manager.pty_kill(&machine_id, &id).await
}
// --- SPKI certificate pinning ---
#[tauri::command]
#[tracing::instrument]
pub async fn remote_probe_spki(url: String) -> Result<String, String> {
remote::probe_spki_hash(&url).await
}
#[tauri::command]
#[tracing::instrument(skip(state))]
pub async fn remote_add_pin(state: State<'_, AppState>, machine_id: String, pin: String) -> Result<(), String> {
state.remote_manager.add_spki_pin(&machine_id, pin).await
}
#[tauri::command]
#[tracing::instrument(skip(state))]
pub async fn remote_remove_pin(state: State<'_, AppState>, machine_id: String, pin: String) -> Result<(), String> {
state.remote_manager.remove_spki_pin(&machine_id, &pin).await
}

View file

@ -0,0 +1,59 @@
use crate::AppState;
use crate::search::SearchResult;
use tauri::State;
#[tauri::command]
pub fn search_init(state: State<'_, AppState>) -> Result<(), String> {
// SearchDb is already initialized during app setup; this is a no-op
// but allows the frontend to confirm readiness.
let _db = &state.search_db;
Ok(())
}
#[tauri::command]
pub fn search_query(
state: State<'_, AppState>,
query: String,
limit: Option<i32>,
) -> Result<Vec<SearchResult>, String> {
state.search_db.search_all(&query, limit.unwrap_or(20))
}
#[tauri::command]
pub fn search_rebuild(state: State<'_, AppState>) -> Result<(), String> {
state.search_db.rebuild_index()
}
#[tauri::command]
pub fn search_index_message(
state: State<'_, AppState>,
session_id: String,
role: String,
content: String,
) -> Result<(), String> {
state.search_db.index_message(&session_id, &role, &content)
}
#[tauri::command]
pub fn search_index_task(
state: State<'_, AppState>,
task_id: String,
title: String,
description: String,
status: String,
assigned_to: String,
) -> Result<(), String> {
state.search_db.index_task(&task_id, &title, &description, &status, &assigned_to)
}
#[tauri::command]
pub fn search_index_btmsg(
state: State<'_, AppState>,
msg_id: String,
from_agent: String,
to_agent: String,
content: String,
channel: String,
) -> Result<(), String> {
state.search_db.index_btmsg(&msg_id, &from_agent, &to_agent, &content, &channel)
}

View file

@ -0,0 +1,34 @@
use crate::secrets::SecretsManager;
#[tauri::command]
pub fn secrets_store(key: String, value: String) -> Result<(), String> {
SecretsManager::store_secret(&key, &value)
}
#[tauri::command]
pub fn secrets_get(key: String) -> Result<Option<String>, String> {
SecretsManager::get_secret(&key)
}
#[tauri::command]
pub fn secrets_delete(key: String) -> Result<(), String> {
SecretsManager::delete_secret(&key)
}
#[tauri::command]
pub fn secrets_list() -> Result<Vec<String>, String> {
SecretsManager::list_keys()
}
#[tauri::command]
pub fn secrets_has_keyring() -> bool {
SecretsManager::has_keyring()
}
#[tauri::command]
pub fn secrets_known_keys() -> Vec<String> {
crate::secrets::KNOWN_KEYS
.iter()
.map(|s| s.to_string())
.collect()
}

View file

@ -0,0 +1,81 @@
use tauri::State;
use crate::AppState;
use crate::session::{Session, LayoutState, SshSession};
// --- Session persistence ---
#[tauri::command]
pub fn session_list(state: State<'_, AppState>) -> Result<Vec<Session>, String> {
state.session_db.list_sessions()
}
#[tauri::command]
pub fn session_save(state: State<'_, AppState>, session: Session) -> Result<(), String> {
state.session_db.save_session(&session)
}
#[tauri::command]
pub fn session_delete(state: State<'_, AppState>, id: String) -> Result<(), String> {
state.session_db.delete_session(&id)
}
#[tauri::command]
pub fn session_update_title(state: State<'_, AppState>, id: String, title: String) -> Result<(), String> {
state.session_db.update_title(&id, &title)
}
#[tauri::command]
pub fn session_touch(state: State<'_, AppState>, id: String) -> Result<(), String> {
state.session_db.touch_session(&id)
}
#[tauri::command]
pub fn session_update_group(state: State<'_, AppState>, id: String, group_name: String) -> Result<(), String> {
state.session_db.update_group(&id, &group_name)
}
// --- Layout ---
#[tauri::command]
pub fn layout_save(state: State<'_, AppState>, layout: LayoutState) -> Result<(), String> {
state.session_db.save_layout(&layout)
}
#[tauri::command]
pub fn layout_load(state: State<'_, AppState>) -> Result<LayoutState, String> {
state.session_db.load_layout()
}
// --- Settings ---
#[tauri::command]
pub fn settings_get(state: State<'_, AppState>, key: String) -> Result<Option<String>, String> {
state.session_db.get_setting(&key)
}
#[tauri::command]
pub fn settings_set(state: State<'_, AppState>, key: String, value: String) -> Result<(), String> {
state.session_db.set_setting(&key, &value)
}
#[tauri::command]
pub fn settings_list(state: State<'_, AppState>) -> Result<Vec<(String, String)>, String> {
state.session_db.get_all_settings()
}
// --- SSH sessions ---
#[tauri::command]
pub fn ssh_session_list(state: State<'_, AppState>) -> Result<Vec<SshSession>, String> {
state.session_db.list_ssh_sessions()
}
#[tauri::command]
pub fn ssh_session_save(state: State<'_, AppState>, session: SshSession) -> Result<(), String> {
state.session_db.save_ssh_session(&session)
}
#[tauri::command]
pub fn ssh_session_delete(state: State<'_, AppState>, id: String) -> Result<(), String> {
state.session_db.delete_ssh_session(&id)
}

View file

@ -0,0 +1,43 @@
use tauri::State;
use crate::AppState;
use crate::fs_watcher::FsWatcherStatus;
#[tauri::command]
pub fn file_watch(
app: tauri::AppHandle,
state: State<'_, AppState>,
pane_id: String,
path: String,
) -> Result<String, String> {
state.file_watcher.watch(&app, &pane_id, &path)
}
#[tauri::command]
pub fn file_unwatch(state: State<'_, AppState>, pane_id: String) {
state.file_watcher.unwatch(&pane_id);
}
#[tauri::command]
pub fn file_read(state: State<'_, AppState>, path: String) -> Result<String, String> {
state.file_watcher.read_file(&path)
}
#[tauri::command]
pub fn fs_watch_project(
app: tauri::AppHandle,
state: State<'_, AppState>,
project_id: String,
cwd: String,
) -> Result<(), String> {
state.fs_watcher.watch_project(&app, &project_id, &cwd)
}
#[tauri::command]
pub fn fs_unwatch_project(state: State<'_, AppState>, project_id: String) {
state.fs_watcher.unwatch_project(&project_id);
}
#[tauri::command]
pub fn fs_watcher_status(state: State<'_, AppState>) -> FsWatcherStatus {
state.fs_watcher.status()
}