feat: add Dioxus and GPUI UI prototypes for framework comparison
Dioxus (ui-dioxus/): 2,169 lines, WebView mode (same wry as Tauri), Catppuccin theme, 12 components, agor-core integration, compiles clean. Evolution path — keeps xterm.js, gradual migration from Tauri. GPUI (ui-gpui/): 2,490 lines, GPU-accelerated rendering, alacritty_terminal for native terminal, 17 files, Catppuccin palette, demo data. Revolution path — pure Rust UI, 120fps target, no WebView. Both are standalone (not in workspace), share agor-core backend. Created for side-by-side comparison to inform framework decision.
This commit is contained in:
parent
90c7315336
commit
f3d2ca78ba
34 changed files with 17467 additions and 0 deletions
180
ui-dioxus/src/backend.rs
Normal file
180
ui-dioxus/src/backend.rs
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
/// Bridge between agor-core (PtyManager, SidecarManager) and Dioxus signals.
|
||||
///
|
||||
/// In the Tauri app, TauriEventSink implements EventSink by emitting Tauri events
|
||||
/// that the Svelte frontend listens to. Here we implement EventSink to push
|
||||
/// events into Dioxus signals, demonstrating native Rust -> UI reactivity
|
||||
/// without any IPC layer.
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use agor_core::event::EventSink;
|
||||
use agor_core::pty::{PtyManager, PtyOptions};
|
||||
use agor_core::sidecar::{AgentQueryOptions, SidecarConfig, SidecarManager};
|
||||
|
||||
/// Collects events emitted by PtyManager and SidecarManager.
|
||||
/// In a real app, these would drive Dioxus signal updates.
|
||||
///
|
||||
/// Key advantage over Tauri: no serialization/deserialization overhead.
|
||||
/// Events are typed Rust values, not JSON blobs crossing an IPC bridge.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DioxusEventSink {
|
||||
/// Buffered events — a real implementation would use channels or
|
||||
/// direct signal mutation via Dioxus's `schedule_update`.
|
||||
events: Arc<Mutex<Vec<AppEvent>>>,
|
||||
}
|
||||
|
||||
/// Typed event enum — replaces the untyped (event_name, JSON) pattern
|
||||
/// used by the Tauri EventSink.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AppEvent {
|
||||
PtyOutput { id: String, data: String },
|
||||
PtyExit { id: String, code: Option<i32> },
|
||||
AgentMessage { session_id: String, payload: serde_json::Value },
|
||||
AgentReady { provider: String },
|
||||
AgentError { session_id: String, error: String },
|
||||
}
|
||||
|
||||
impl DioxusEventSink {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
events: Arc::new(Mutex::new(Vec::new())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Drain all buffered events. Called from a Dioxus use_effect or timer.
|
||||
pub fn drain_events(&self) -> Vec<AppEvent> {
|
||||
let mut events = self.events.lock().unwrap();
|
||||
std::mem::take(&mut *events)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventSink for DioxusEventSink {
|
||||
fn emit(&self, event: &str, payload: serde_json::Value) {
|
||||
let app_event = match event {
|
||||
"pty-output" => {
|
||||
let id = payload["id"].as_str().unwrap_or("").to_string();
|
||||
let data = payload["data"].as_str().unwrap_or("").to_string();
|
||||
AppEvent::PtyOutput { id, data }
|
||||
}
|
||||
"pty-exit" => {
|
||||
let id = payload["id"].as_str().unwrap_or("").to_string();
|
||||
let code = payload["code"].as_i64().map(|c| c as i32);
|
||||
AppEvent::PtyExit { id, code }
|
||||
}
|
||||
"agent-message" => {
|
||||
let session_id = payload["sessionId"].as_str().unwrap_or("").to_string();
|
||||
AppEvent::AgentMessage {
|
||||
session_id,
|
||||
payload: payload.clone(),
|
||||
}
|
||||
}
|
||||
"sidecar-ready" => {
|
||||
let provider = payload["provider"].as_str().unwrap_or("claude").to_string();
|
||||
AppEvent::AgentReady { provider }
|
||||
}
|
||||
"agent-error" => {
|
||||
let session_id = payload["sessionId"].as_str().unwrap_or("").to_string();
|
||||
let error = payload["error"].as_str().unwrap_or("unknown error").to_string();
|
||||
AppEvent::AgentError { session_id, error }
|
||||
}
|
||||
_ => {
|
||||
// Unknown event — log and ignore
|
||||
log::debug!("Unknown event from backend: {event}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Ok(mut events) = self.events.lock() {
|
||||
events.push(app_event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Backend handle holding initialized managers.
|
||||
///
|
||||
/// In the Tauri app, these live in AppState behind Arc<Mutex<>>
|
||||
/// and are accessed via Tauri commands. Here they're directly
|
||||
/// available to Dioxus components via use_context.
|
||||
pub struct Backend {
|
||||
pub pty_manager: PtyManager,
|
||||
pub sidecar_manager: SidecarManager,
|
||||
pub event_sink: DioxusEventSink,
|
||||
}
|
||||
|
||||
impl Backend {
|
||||
/// Initialize backend managers.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if sidecar search paths cannot be resolved (mirrors Tauri app behavior).
|
||||
pub fn new() -> Self {
|
||||
let event_sink = DioxusEventSink::new();
|
||||
let sink: Arc<dyn EventSink> = Arc::new(event_sink.clone());
|
||||
|
||||
let pty_manager = PtyManager::new(Arc::clone(&sink));
|
||||
|
||||
// Resolve sidecar search paths — same logic as src-tauri/src/lib.rs
|
||||
let mut search_paths = Vec::new();
|
||||
if let Ok(exe_dir) = std::env::current_exe() {
|
||||
if let Some(parent) = exe_dir.parent() {
|
||||
search_paths.push(parent.join("sidecar"));
|
||||
search_paths.push(parent.join("../sidecar/dist"));
|
||||
}
|
||||
}
|
||||
// Also check the repo's sidecar/dist for dev mode
|
||||
search_paths.push(std::path::PathBuf::from("../sidecar/dist"));
|
||||
search_paths.push(std::path::PathBuf::from("./sidecar/dist"));
|
||||
|
||||
let sidecar_config = SidecarConfig {
|
||||
search_paths,
|
||||
env_overrides: std::collections::HashMap::new(),
|
||||
sandbox: agor_core::sandbox::SandboxConfig::default(),
|
||||
};
|
||||
|
||||
let sidecar_manager = SidecarManager::new(Arc::clone(&sink), sidecar_config);
|
||||
|
||||
Self {
|
||||
pty_manager,
|
||||
sidecar_manager,
|
||||
event_sink,
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawn a PTY with default options.
|
||||
pub fn spawn_pty(&self, cwd: Option<&str>) -> Result<String, String> {
|
||||
self.pty_manager.spawn(PtyOptions {
|
||||
shell: None,
|
||||
cwd: cwd.map(|s| s.to_string()),
|
||||
args: None,
|
||||
cols: Some(80),
|
||||
rows: Some(24),
|
||||
})
|
||||
}
|
||||
|
||||
/// Start an agent query.
|
||||
pub fn query_agent(&self, session_id: &str, prompt: &str, cwd: &str) -> Result<(), String> {
|
||||
let options = AgentQueryOptions {
|
||||
provider: "claude".to_string(),
|
||||
session_id: session_id.to_string(),
|
||||
prompt: prompt.to_string(),
|
||||
cwd: Some(cwd.to_string()),
|
||||
max_turns: None,
|
||||
max_budget_usd: None,
|
||||
resume_session_id: None,
|
||||
permission_mode: Some("bypassPermissions".to_string()),
|
||||
setting_sources: Some(vec!["user".to_string(), "project".to_string()]),
|
||||
system_prompt: None,
|
||||
model: None,
|
||||
claude_config_dir: None,
|
||||
additional_directories: None,
|
||||
worktree_name: None,
|
||||
provider_config: serde_json::Value::Null,
|
||||
extra_env: std::collections::HashMap::new(),
|
||||
};
|
||||
self.sidecar_manager.query(&options)
|
||||
}
|
||||
|
||||
/// Stop an agent session.
|
||||
pub fn stop_agent(&self, session_id: &str) -> Result<(), String> {
|
||||
self.sidecar_manager.stop_session(session_id)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue