feat(bridge): establish local pi status bridge

Deliver the initial local bridge, Noctalia v4/v5 adapters, desktop client, service unit, tests, and implementation documentation for persistent Pi status and control.
This commit is contained in:
2026-07-27 14:51:45 +02:00
commit b7fea83ed6
91 changed files with 15029 additions and 0 deletions
+151
View File
@@ -0,0 +1,151 @@
mod bridge;
use serde_json::Value;
use std::sync::Mutex;
use tauri::{async_runtime::JoinHandle, AppHandle, Emitter, Manager, State};
struct Subscription(Mutex<Option<JoinHandle<()>>>);
fn socket_path() -> Result<String, String> {
bridge::default_socket_path()
}
#[tauri::command]
async fn list_agents() -> Result<Value, String> {
bridge::list_agents(&socket_path()?).await
}
#[tauri::command]
async fn list_directories() -> Result<Value, String> {
bridge::list_directories(&socket_path()?).await
}
#[tauri::command]
async fn select_worktree(worktree_path: String) -> Result<Value, String> {
bridge::select_worktree(&socket_path()?, &worktree_path).await
}
#[tauri::command]
async fn load_agent(agent_id: String) -> Result<Value, String> {
bridge::load_agent(&socket_path()?, &agent_id).await
}
#[tauri::command]
async fn list_sessions(agent_id: String) -> Result<Value, String> {
bridge::list_sessions(&socket_path()?, &agent_id).await
}
#[tauri::command]
async fn switch_session(agent_id: String, session_path: String) -> Result<Value, String> {
bridge::switch_session(&socket_path()?, &agent_id, &session_path).await
}
#[tauri::command]
async fn new_session(agent_id: String) -> Result<Value, String> {
bridge::new_session(&socket_path()?, &agent_id).await
}
#[tauri::command]
async fn submit_prompt(agent_id: String, message: String) -> Result<(), String> {
bridge::submit_prompt(&socket_path()?, &agent_id, &message).await
}
#[tauri::command]
async fn abort(agent_id: String) -> Result<(), String> {
bridge::abort(&socket_path()?, &agent_id).await
}
#[tauri::command]
async fn retry(agent_id: String) -> Result<(), String> {
bridge::command(&socket_path()?, &agent_id, "retry").await
}
#[tauri::command]
async fn restart(agent_id: String) -> Result<(), String> {
bridge::command(&socket_path()?, &agent_id, "restart").await
}
#[tauri::command]
async fn set_model(agent_id: String, provider: String, model_id: String) -> Result<(), String> {
bridge::set_model(&socket_path()?, &agent_id, &provider, &model_id).await
}
#[tauri::command]
async fn set_thinking_level(agent_id: String, level: String) -> Result<(), String> {
bridge::set_thinking_level(&socket_path()?, &agent_id, &level).await
}
#[tauri::command]
async fn respond_to_extension(
agent_id: String,
request_id: String,
response: Value,
) -> Result<(), String> {
bridge::respond_to_extension(&socket_path()?, &agent_id, &request_id, response).await
}
#[tauri::command]
fn subscribe_agent(
app: AppHandle,
subscriptions: State<'_, Subscription>,
agent_id: String,
cursor: u64,
) -> Result<(), String> {
let socket = socket_path()?;
if let Some(subscription) = subscriptions
.0
.lock()
.map_err(|_| "Could not update bridge subscription".to_owned())?
.take()
{
subscription.abort();
}
let event_app = app.clone();
let task = tauri::async_runtime::spawn(async move {
if let Err(error) = bridge::subscribe(socket, agent_id.clone(), cursor, event_app.clone()).await {
let _ = event_app.emit(
"bridge-error",
serde_json::json!({ "agentId": agent_id, "message": error }),
);
}
});
let mut current_subscription = subscriptions
.0
.lock()
.map_err(|_| "Could not retain bridge subscription".to_owned())?;
*current_subscription = Some(task);
Ok(())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let builder = tauri::Builder::default()
.manage(Subscription(Mutex::new(None)))
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}))
.invoke_handler(tauri::generate_handler![
list_agents,
list_directories,
select_worktree,
load_agent,
list_sessions,
switch_session,
new_session,
submit_prompt,
abort,
retry,
restart,
set_model,
set_thinking_level,
respond_to_extension,
subscribe_agent
]);
let context = tauri::generate_context!();
if let Err(error) = builder.run(context) {
eprintln!("Pi Status UI exited: {error}");
}
}