feat(rpc): add validated desktop Pi RPC command palette
This commit is contained in:
@@ -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
@@ -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()];
|
||||
|
||||
@@ -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")?;
|
||||
|
||||
Reference in New Issue
Block a user