wip: snapshot das alterações não commitadas (2026-10-20)

Origem: D:/Documents/Github/PiAgentUI/PiAgentUi
Arquivos omitidos (segredos, bancos de dados, logs, >50 MB): 4
This commit is contained in:
Evandro committed 2026-10-20 00:48:28 -03:00
1 parent b3489e1ede
commit 98b5db029f
70 files changed
+9466 -9191

No files matched your search

+1 -1
View File
@@ -335,7 +335,7 @@ async fn connect_ws(
tokio::select! {
outbound = rx.recv() => match outbound {
Some(BridgeCommand::Send(data)) => {
if let Err(error) = write.send(Message::Text(data.into())).await {
if let Err(error) = write.send(Message::Text(data)).await {
let msg = format!("WebSocket write failed: {}", error);
emit(&on_event, BridgeEvent::Error { message: msg.clone() });
state.remove_if_current(&key, conn_id);
+240 -31
View File
@@ -4,16 +4,19 @@
// ============================================
use crate::app::service::ServiceState;
use serde::{Deserialize, Serialize};
use std::{
fs,
path::PathBuf,
process::{Command, Stdio},
sync::atomic::Ordering,
time::Duration,
};
use tauri::State;
use tauri::{Manager, State};
/// 检查 pi 服务是否在运行(通过 health endpoint)
pub async fn is_service_running(url: &str) -> bool {
let health_url = format!("{}/api/health", url.trim_end_matches('/'));
let health_url = format!("{}/global/health", url.trim_end_matches('/'));
match reqwest::Client::builder()
.connect_timeout(Duration::from_secs(3))
.build()
@@ -29,23 +32,194 @@ pub async fn is_service_running(url: &str) -> bool {
}
}
/// 启动 pi serve 进程
fn spawn_pi_serve(
binary_path: &str,
env_vars: &std::collections::HashMap<String, String>,
) -> Result<std::process::Child, String> {
log::info!("Starting pi serve with binary: {}", binary_path);
if !env_vars.is_empty() {
log::info!("Injecting {} environment variable(s)", env_vars.len());
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct PiAgentConnection {
pub port: u16,
pub token: String,
}
#[derive(Serialize, Clone, Debug)]
pub struct PiAgentServiceStart {
pub started: bool,
pub connection: Option<PiAgentConnection>,
pub agent_dir: String,
pub session_dir: String,
}
#[cfg(debug_assertions)]
fn e2e_config_value(key: &str) -> Option<String> {
let config_path = std::env::current_exe()
.ok()?
.parent()?
.join("piagentui-e2e.json");
let config = fs::read_to_string(config_path).ok()?;
let value: serde_json::Value = serde_json::from_str(&config).ok()?;
value.get(key)?.as_str().map(ToOwned::to_owned)
}
pub fn resolve_app_agent_dir(app: &tauri::AppHandle) -> Result<PathBuf, String> {
#[cfg(debug_assertions)]
if let Ok(agent_dir) = std::env::var("PI_CODING_AGENT_DIR") {
return Ok(PathBuf::from(agent_dir));
}
#[cfg(debug_assertions)]
if let Some(agent_dir) = e2e_config_value("agentDir") {
return Ok(PathBuf::from(agent_dir));
}
app.path()
.app_data_dir()
.map(|dir| dir.join("pi-runtime").join("agent"))
.map_err(|e| format!("failed to resolve app data dir: {}", e))
}
fn resolve_app_session_dir(app: &tauri::AppHandle) -> Result<PathBuf, String> {
Ok(resolve_app_agent_dir(app)?.join("sessions"))
}
pub fn read_app_connection(app: &tauri::AppHandle) -> Option<PiAgentConnection> {
let conn_file = resolve_app_agent_dir(app).ok()?.join("piagentui-port.json");
if !conn_file.exists() {
return None;
}
let file = fs::File::open(conn_file).ok()?;
serde_json::from_reader(file).ok()
}
fn connection_url(connection: &PiAgentConnection) -> String {
format!("http://127.0.0.1:{}", connection.port)
}
fn resolve_app_extension_path(app: &tauri::AppHandle) -> Result<PathBuf, String> {
#[cfg(debug_assertions)]
{
let source_extension = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.map(|root| root.join("extensions").join("piagentui-server.ts"));
if let Some(path) = source_extension {
if path.exists() {
return Ok(path);
}
}
}
let mut cmd = Command::new(binary_path);
cmd.arg("rpc").stdout(Stdio::null()).stderr(Stdio::null());
let resource_extension = app
.path()
.resource_dir()
.map_err(|e| format!("failed to resolve app resource dir: {}", e))?
.join("extensions")
.join("piagentui-server.ts");
// 注入用户配置的环境变量
if resource_extension.exists() {
return Ok(resource_extension);
}
Err(format!(
"PiAgentUI bundled extension was not found at {}. The desktop app cannot expose provider onboarding APIs without it.",
resource_extension.display()
))
}
#[derive(Debug)]
struct PiCommandSpec {
program: PathBuf,
prefix_args: Vec<String>,
display: String,
}
fn is_default_pi_binary(binary_path: &str) -> bool {
let trimmed = binary_path.trim();
trimmed.is_empty() || trimmed == "pi"
}
fn resolve_pi_command(binary_path: &str) -> PiCommandSpec {
let trimmed = binary_path.trim();
#[cfg(debug_assertions)]
if is_default_pi_binary(trimmed) {
if let Some(local_cli) = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|root| root.parent().map(|workspace| workspace.join("pi").join("packages").join("coding-agent").join("dist").join("cli.js")))
{
if local_cli.exists() {
let node = std::env::var("PIAGENTUI_NODE_BINARY").unwrap_or_else(|_| "node".to_string());
return PiCommandSpec {
program: PathBuf::from(node.clone()),
prefix_args: vec![local_cli.display().to_string()],
display: format!("{} {}", node, local_cli.display()),
};
}
}
}
let program = if trimmed.is_empty() { "pi" } else { trimmed };
PiCommandSpec {
program: PathBuf::from(program),
prefix_args: Vec::new(),
display: program.to_string(),
}
}
/// 启动 pi RPC 进程 with an app-private Pi runtime.
fn spawn_pi_rpc(
app: &tauri::AppHandle,
binary_path: &str,
env_vars: &std::collections::HashMap<String, String>,
) -> Result<(std::process::Child, PathBuf, PathBuf), String> {
let agent_dir = resolve_app_agent_dir(app)?;
let session_dir = resolve_app_session_dir(app)?;
let extension_path = resolve_app_extension_path(app)?;
let pi_command = resolve_pi_command(binary_path);
fs::create_dir_all(&session_dir).map_err(|e| format!("failed to create private Pi runtime dir: {}", e))?;
log::info!("Starting isolated pi RPC backend with command: {}", pi_command.display);
log::info!("Using private Pi agent dir: {}", agent_dir.display());
log::info!("Loading PiAgentUI extension: {}", extension_path.display());
if !env_vars.is_empty() {
log::info!("Injecting {} user environment variable(s)", env_vars.len());
}
#[cfg(debug_assertions)]
let mut cmd = if let (Some(node_path), Some(script_path)) =
(e2e_config_value("node"), e2e_config_value("script"))
{
log::info!("Using E2E Pi backend shim: {} {}", node_path, script_path);
let mut cmd = Command::new(node_path);
cmd.arg(script_path);
if let Some(marker_dir) = e2e_config_value("markerDir") {
cmd.env("PIAGENTUI_E2E_MARKER_DIR", marker_dir);
}
cmd
} else if let Ok(script_path) = std::env::var("PIAGENTUI_E2E_PI_SCRIPT") {
let node_path = std::env::var("PIAGENTUI_E2E_NODE").unwrap_or_else(|_| binary_path.to_string());
log::info!("Using E2E Pi backend shim: {} {}", node_path, script_path);
let mut cmd = Command::new(node_path);
cmd.arg(script_path);
cmd
} else {
let mut cmd = Command::new(&pi_command.program);
cmd.args(&pi_command.prefix_args);
cmd
};
#[cfg(not(debug_assertions))]
let mut cmd = {
let mut cmd = Command::new(&pi_command.program);
cmd.args(&pi_command.prefix_args);
cmd
};
// Inject user-configured environment first, then enforce private Pi runtime vars.
for (key, value) in env_vars {
cmd.env(key, value);
}
cmd.arg("--mode")
.arg("rpc")
.arg("--extension")
.arg(&extension_path)
.env("PI_CODING_AGENT_DIR", &agent_dir)
.env("PI_CODING_AGENT_SESSION_DIR", &session_dir)
.stdout(Stdio::null())
.stderr(Stdio::null());
#[cfg(target_os = "windows")]
{
@@ -54,12 +228,19 @@ fn spawn_pi_serve(
cmd.creation_flags(CREATE_NO_WINDOW);
}
cmd.spawn().map_err(|e| {
let child = cmd.spawn().map_err(|e| {
let local_hint = if is_default_pi_binary(binary_path) {
" The app also tried its development fallback. If you are running from source, build local Pi with `pnpm --dir pi/packages/coding-agent build`."
} else {
""
};
format!(
"Failed to start '{}': {}. Check that the path is correct.",
binary_path, e
"Failed to start '{}': {}. Check that the path points to the pi executable.{}",
pi_command.display, e, local_hint
)
})
})?;
Ok((child, agent_dir, session_dir))
}
/// 跨平台杀进程
@@ -92,36 +273,64 @@ pub async fn check_pi_service(url: String) -> Result<bool, String> {
Ok(is_service_running(&url).await)
}
/// 启动 pi serve
/// Start an isolated pi RPC backend for the desktop app.
#[tauri::command]
pub async fn start_pi_service(
app: tauri::AppHandle,
state: State<'_, ServiceState>,
url: String,
binary_path: String,
env_vars: std::collections::HashMap<String, String>,
) -> Result<bool, String> {
if is_service_running(&url).await {
log::info!("pi service already running at {}", url);
return Ok(false);
) -> Result<PiAgentServiceStart, String> {
if let Some(connection) = read_app_connection(&app) {
let connection_url = connection_url(&connection);
if is_service_running(&connection_url).await {
log::info!("isolated pi backend already running at {}", connection_url);
let agent_dir = resolve_app_agent_dir(&app)?;
let session_dir = resolve_app_session_dir(&app)?;
return Ok(PiAgentServiceStart {
started: false,
connection: Some(connection),
agent_dir: agent_dir.display().to_string(),
session_dir: session_dir.display().to_string(),
});
}
}
let child = spawn_pi_serve(&binary_path, &env_vars)?;
if is_service_running(&url).await {
log::info!("legacy pi service already running at {}", url);
}
let (child, agent_dir, session_dir) = spawn_pi_rpc(&app, &binary_path, &env_vars)?;
let pid = child.id();
log::info!("Started pi serve, PID: {}", pid);
log::info!("Started isolated pi RPC backend, PID: {}", pid);
state.child_pid.store(pid, Ordering::SeqCst);
state.we_started.store(true, Ordering::SeqCst);
for _ in 0..30 {
tokio::time::sleep(Duration::from_millis(500)).await;
if is_service_running(&url).await {
log::info!("pi service is ready at {}", url);
return Ok(true);
for _ in 0..60 {
tokio::time::sleep(Duration::from_millis(250)).await;
if let Some(connection) = read_app_connection(&app) {
let connection_url = connection_url(&connection);
if is_service_running(&connection_url).await {
log::info!("isolated pi backend is ready at {}", connection_url);
return Ok(PiAgentServiceStart {
started: true,
connection: Some(connection),
agent_dir: agent_dir.display().to_string(),
session_dir: session_dir.display().to_string(),
});
}
}
}
log::warn!("pi service started but health check not passing yet");
Ok(true)
log::warn!("isolated pi backend started but discovery/health is not ready yet");
Ok(PiAgentServiceStart {
started: true,
connection: read_app_connection(&app),
agent_dir: agent_dir.display().to_string(),
session_dir: session_dir.display().to_string(),
})
}
/// 停止 pi serve
+3 -16
View File
@@ -1,4 +1,5 @@
use crate::app::dir_state::OpenDirectoryState;
use crate::app::commands::pi_agent::{read_app_connection, PiAgentConnection};
use std::sync::Arc;
use tauri::State;
@@ -25,21 +26,7 @@ pub fn desktop_window_ready(window: tauri::Window) -> Result<(), String> {
crate::app::mark_window_ready(&window).map_err(|err| err.to_string())
}
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
pub struct PiAgentConnection {
pub port: u16,
pub token: String,
}
#[tauri::command]
pub fn get_pi_agent_connection() -> Option<PiAgentConnection> {
let home = std::env::var_os("USERPROFILE")
.or_else(|| std::env::var_os("HOME"))
.map(std::path::PathBuf::from)?;
let conn_file = home.join(".pi").join("agent").join("piagentui-port.json");
if !conn_file.exists() {
return None;
}
let file = std::fs::File::open(conn_file).ok()?;
serde_json::from_reader(file).ok()
pub fn get_pi_agent_connection(app: tauri::AppHandle) -> Option<PiAgentConnection> {
read_app_connection(&app)
}
+24 -13
View File
@@ -23,6 +23,13 @@ use std::sync::Arc;
#[cfg(not(target_os = "android"))]
use tauri::Emitter;
#[cfg(debug_assertions)]
fn is_e2e_run() -> bool {
std::env::current_exe()
.ok()
.and_then(|exe| exe.parent().map(|dir| dir.join("piagentui-e2e.json")))
.is_some_and(|path| path.exists())
}
/// 从命令行参数中提取目录路径
#[cfg(not(target_os = "android"))]
fn extract_directory_from_args(args: &[String]) -> Option<String> {
@@ -43,18 +50,20 @@ fn create_main_window(app: &tauri::AppHandle) -> Result<tauri::WebviewWindow, ta
return Ok(window);
}
let config = app
.config()
.app
.windows
.iter()
.find(|window| window.label == "main")
.cloned()
.expect("main window config missing");
let builder = configure_desktop_window_builder(tauri::WebviewWindowBuilder::new(
app,
"main",
tauri::WebviewUrl::App("index.html".into()),
))
.title("Pi Agent")
.inner_size(800.0, 600.0)
.resizable(true)
.fullscreen(false);
configure_desktop_window_builder(tauri::WebviewWindowBuilder::from_config(app, &config)?)
.visible(false)
.build()
#[cfg(windows)]
let builder = builder.disable_drag_drop_handler();
builder.visible(false).build()
}
#[cfg(target_os = "android")]
@@ -188,11 +197,13 @@ pub fn run() {
#[cfg(not(target_os = "android"))]
{
let main_window = create_main_window(&app.handle())?;
let main_window = create_main_window(app.handle())?;
finish_desktop_window_setup(&main_window);
#[cfg(debug_assertions)]
main_window.open_devtools();
if !is_e2e_run() {
main_window.open_devtools();
}
}
#[cfg(target_os = "android")]
+11 -3
View File
@@ -6,16 +6,17 @@
"build": {
"frontendDist": "../dist",
"devUrl": "http://localhost:5173",
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build"
"beforeDevCommand": "pnpm dev",
"beforeBuildCommand": "pnpm build"
},
"app": {
"withGlobalTauri": true,
"windows": [
{
"label": "main",
"create": false,
"create": true,
"title": "Pi Agent",
"visible": true,
"width": 800,
"height": 600,
"titleBarStyle": "Overlay",
@@ -40,6 +41,13 @@
"active": true,
"targets": ["nsis", "dmg", "deb"],
"icon": ["icons/32x32.png", "icons/128x128.png", "icons/[email protected]", "icons/icon.icns", "icons/icon.ico"],
"resources": {
"../extensions/piagentui-server.ts": "extensions/piagentui-server.ts",
"../extensions/piagentui-server-core.ts": "extensions/piagentui-server-core.ts",
"../extensions/piagentui-settings-store.ts": "extensions/piagentui-settings-store.ts",
"../extensions/piagentui-extension-manager.ts": "extensions/piagentui-extension-manager.ts",
"../extensions/piagentui-git-manager.ts": "extensions/piagentui-git-manager.ts"
},
"windows": {
"nsis": {
"installerHooks": "./windows/hooks.nsi"