feat(rpc): add validated desktop Pi RPC command palette

This commit is contained in:
2026-08-24 20:16:51 +02:00
parent 475f95ba70
commit 99084cc5a5
25 changed files with 2170 additions and 344 deletions
+1 -2
View File
@@ -375,7 +375,7 @@ pub async fn load_agent(socket_path: &str, agent_id: &str) -> Result<Value, Stri
}))
}
pub async fn submit_prompt(socket_path: &str, agent_id: &str, message: &str) -> Result<(), String> {
pub async fn submit_prompt(socket_path: &str, agent_id: &str, message: &str) -> Result<Value, String> {
request(
socket_path,
"submit_prompt",
@@ -383,7 +383,6 @@ pub async fn submit_prompt(socket_path: &str, agent_id: &str, message: &str) ->
Some(json!({ "message": message })),
)
.await
.map(|_| ())
}
pub async fn abort(socket_path: &str, agent_id: &str) -> Result<(), String> {
+80 -3
View File
@@ -1,8 +1,8 @@
mod bridge;
mod ui_state;
use serde_json::Value;
use std::sync::{Arc, Mutex};
use serde_json::{json, Value};
use std::{env, path::Path, sync::{Arc, Mutex}, time::Duration};
use tauri::{async_runtime::JoinHandle, AppHandle, Emitter, Manager, State};
struct LegacySubscription(Mutex<Option<JoinHandle<()>>>);
@@ -45,6 +45,58 @@ fn socket_path() -> Result<String, String> {
bridge::default_socket_path()
}
fn requested_new_worktree_argument(args: &[String]) -> Option<String> {
args.iter()
.position(|arg| arg == "--worktree")
.and_then(|index| args.get(index + 1))
.cloned()
}
fn requested_new_worktree(app: &AppHandle, args: &[String]) -> Option<String> {
if !args.iter().any(|arg| arg == "--new") {
return None;
}
let saved_default = app
.path()
.app_data_dir()
.ok()
.and_then(|directory| ui_state::load_from(&directory).ok())
.and_then(|state| state.default_new_session_worktree);
let worktree = requested_new_worktree_argument(args)
.or(saved_default)
.or_else(|| env::var("PI_STATUS_DEFAULT_WORKTREE").ok())?;
Path::new(&worktree).is_absolute().then_some(worktree)
}
fn launch_new_session(app: AppHandle, args: &[String]) {
let Some(worktree_path) = requested_new_worktree(&app, args) else { return };
tauri::async_runtime::spawn(async move {
// Give a newly-created webview time to register its frontend listeners.
tokio::time::sleep(Duration::from_millis(350)).await;
let _ = app.emit("workspace-new-session", json!({
"phase": "starting",
"detail": "Starting a new Pi session…"
}));
match socket_path().map(|socket| (socket, worktree_path)) {
Ok((socket, worktree_path)) => match bridge::create_session_runtime(&socket, &worktree_path).await {
Ok(result) => { let _ = app.emit("workspace-new-session", json!({
"phase": "ready",
"runtimeId": result.runtime.runtime_id,
"detail": "New session ready"
})); }
Err(error) => { let _ = app.emit("workspace-new-session", json!({
"phase": "error",
"detail": format!("Could not start session: {error}")
})); }
},
Err(error) => { let _ = app.emit("workspace-new-session", json!({
"phase": "error",
"detail": error
})); }
}
});
}
#[tauri::command]
async fn get_workspace() -> Result<bridge::Workspace, String> {
bridge::get_workspace(&socket_path()?).await
@@ -146,7 +198,7 @@ async fn new_session(agent_id: String) -> Result<Value, String> {
}
#[tauri::command]
async fn submit_prompt(agent_id: String, message: String) -> Result<(), String> {
async fn submit_prompt(agent_id: String, message: String) -> Result<Value, String> {
bridge::submit_prompt(&socket_path()?, &agent_id, &message).await
}
@@ -180,6 +232,16 @@ async fn set_session_name(agent_id: String, name: String) -> Result<(), String>
bridge::set_session_name(&socket_path()?, &agent_id, &name).await
}
#[tauri::command]
async fn pi_rpc_command(agent_id: String, command: String, input: Value) -> Result<Value, String> {
bridge::request(
&socket_path()?,
"pi_rpc_command",
Some(&agent_id),
Some(serde_json::json!({ "command": command, "input": input })),
).await
}
#[tauri::command]
async fn compact(agent_id: String, custom_instructions: Option<String>) -> Result<(), String> {
bridge::compact(&socket_path()?, &agent_id, custom_instructions.as_deref()).await
@@ -281,6 +343,7 @@ fn unsubscribe_workspace(subscription: State<'_, WorkspaceSubscription>) -> Resu
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let launch_args = env::args().collect::<Vec<_>>();
let builder = tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.manage(LegacySubscription(Mutex::new(None)))
@@ -289,6 +352,7 @@ pub fn run() {
task: Mutex::new(None),
})
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
launch_new_session(app.clone(), &args);
if let Some(window) = app.get_webview_window("main") {
match window_action(&args, window.is_visible().unwrap_or(false)) {
WindowAction::Hide => {
@@ -301,6 +365,10 @@ pub fn run() {
}
}
}))
.setup(move |app| {
launch_new_session(app.handle().clone(), &launch_args);
Ok(())
})
.invoke_handler(tauri::generate_handler![
get_workspace,
get_workspace_summary,
@@ -329,6 +397,7 @@ pub fn run() {
set_thinking_level,
set_session_name,
compact,
pi_rpc_command,
respond_to_extension,
subscribe_agent
]);
@@ -357,6 +426,14 @@ mod tests {
assert_eq!(*tasks.lock().unwrap(), Some(2));
}
#[test]
fn new_session_uses_an_explicit_absolute_worktree() {
let args = vec!["pi-status-ui".into(), "--new".into(), "--worktree".into(), "/workspace".into()];
assert_eq!(requested_new_worktree_argument(&args), Some("/workspace".into()));
let relative = vec!["pi-status-ui".into(), "--new".into(), "--worktree".into(), "workspace".into()];
assert_eq!(requested_new_worktree_argument(&relative).filter(|path| Path::new(path).is_absolute()), None);
}
#[test]
fn toggle_hides_a_visible_window_and_shows_a_hidden_window() {
let toggle = vec!["--toggle".to_owned()];
+10
View File
@@ -58,6 +58,8 @@ pub struct UiStateV1 {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub interface_scale: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_new_session_worktree: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dismissed_collision_warning: Option<bool>,
}
@@ -73,6 +75,7 @@ impl Default for UiStateV1 {
last_seen: BTreeMap::new(),
workspace_cursor: None,
interface_scale: None,
default_new_session_worktree: None,
dismissed_collision_warning: None,
}
}
@@ -127,6 +130,13 @@ pub fn validate(state: &UiStateV1) -> Result<(), String> {
bounded(runtime_id, "selected runtime id")?;
total_string_bytes = total_string_bytes.saturating_add(runtime_id.len());
}
if let Some(worktree) = &state.default_new_session_worktree {
bounded(worktree, "default new-session worktree")?;
if !Path::new(worktree).is_absolute() {
return Err("Default new-session worktree must be absolute".to_owned());
}
total_string_bytes = total_string_bytes.saturating_add(worktree.len());
}
let mut total_draft_bytes = 0usize;
for (runtime_id, draft) in &state.drafts {
bounded(runtime_id, "draft runtime id")?;