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:
Hibryda 2026-03-19 06:05:58 +01:00
parent 90c7315336
commit f3d2ca78ba
34 changed files with 17467 additions and 0 deletions

350
ui-dioxus/src/state.rs Normal file
View file

@ -0,0 +1,350 @@
/// Application state management via Dioxus signals.
///
/// Analogous to Svelte 5 runes ($state, $derived) — Dioxus signals provide
/// automatic dependency tracking and targeted re-renders.
use serde::{Deserialize, Serialize};
use uuid::Uuid;
/// Agent activity status — mirrors the Svelte health store's ActivityState.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AgentStatus {
Idle,
Running,
Done,
Stalled,
Error,
}
impl AgentStatus {
pub fn css_class(&self) -> &'static str {
match self {
AgentStatus::Idle => "idle",
AgentStatus::Running => "running",
AgentStatus::Done => "done",
AgentStatus::Stalled => "stalled",
AgentStatus::Error => "error",
}
}
pub fn label(&self) -> &'static str {
match self {
AgentStatus::Idle => "Idle",
AgentStatus::Running => "Running",
AgentStatus::Done => "Done",
AgentStatus::Stalled => "Stalled",
AgentStatus::Error => "Error",
}
}
}
/// A single message in an agent conversation.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AgentMessage {
pub id: String,
pub role: MessageRole,
pub content: String,
/// For tool calls: tool name
pub tool_name: Option<String>,
/// For tool results: output text
pub tool_output: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum MessageRole {
User,
Assistant,
Tool,
}
impl MessageRole {
pub fn css_class(&self) -> &'static str {
match self {
MessageRole::User => "user",
MessageRole::Assistant => "assistant",
MessageRole::Tool => "tool",
}
}
pub fn label(&self) -> &'static str {
match self {
MessageRole::User => "You",
MessageRole::Assistant => "Claude",
MessageRole::Tool => "Tool",
}
}
}
/// Which tab is active in a ProjectBox.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProjectTab {
Model,
Docs,
Files,
}
impl ProjectTab {
pub fn label(&self) -> &'static str {
match self {
ProjectTab::Model => "Model",
ProjectTab::Docs => "Docs",
ProjectTab::Files => "Files",
}
}
pub fn all() -> &'static [ProjectTab] {
&[ProjectTab::Model, ProjectTab::Docs, ProjectTab::Files]
}
}
/// A project configuration — corresponds to GroupsFile ProjectConfig in the Svelte app.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ProjectConfig {
pub id: String,
pub name: String,
pub cwd: String,
pub provider: String,
pub accent: String,
}
impl ProjectConfig {
pub fn new(name: &str, cwd: &str, provider: &str, accent: &str) -> Self {
Self {
id: Uuid::new_v4().to_string(),
name: name.to_string(),
cwd: cwd.to_string(),
provider: provider.to_string(),
accent: accent.to_string(),
}
}
}
/// Per-project agent session state.
#[derive(Debug, Clone)]
pub struct AgentSession {
pub session_id: String,
pub status: AgentStatus,
pub messages: Vec<AgentMessage>,
pub cost_usd: f64,
pub tokens_used: u64,
pub model: String,
}
impl AgentSession {
pub fn new() -> Self {
Self {
session_id: Uuid::new_v4().to_string(),
status: AgentStatus::Idle,
messages: Vec::new(),
cost_usd: 0.0,
tokens_used: 0,
model: "claude-sonnet-4-20250514".to_string(),
}
}
}
/// Global app state — sidebar, command palette visibility, etc.
/// (Used when wiring backend; prototype uses individual signals.)
#[derive(Debug, Clone, PartialEq)]
#[allow(dead_code)]
pub struct AppState {
pub settings_open: bool,
pub palette_open: bool,
}
impl Default for AppState {
fn default() -> Self {
Self {
settings_open: false,
palette_open: false,
}
}
}
/// Terminal output line for the mock terminal display.
#[derive(Debug, Clone, PartialEq)]
pub struct TerminalLine {
pub kind: TerminalLineKind,
pub text: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TerminalLineKind {
Prompt,
Output,
}
/// Create demo projects for the prototype.
pub fn demo_projects() -> Vec<ProjectConfig> {
vec![
ProjectConfig::new(
"agent-orchestrator",
"~/code/ai/agent-orchestrator",
"claude",
"#89b4fa", // blue
),
ProjectConfig::new(
"quanta-discord-bot",
"~/code/bots/quanta-discord-bot",
"claude",
"#a6e3a1", // green
),
]
}
/// Create demo messages for a project agent pane.
pub fn demo_messages() -> Vec<AgentMessage> {
vec![
AgentMessage {
id: "m1".to_string(),
role: MessageRole::User,
content: "Add error handling to the WebSocket reconnection logic. It should use \
exponential backoff with a 30s cap."
.to_string(),
tool_name: None,
tool_output: None,
},
AgentMessage {
id: "m2".to_string(),
role: MessageRole::Assistant,
content: "I'll add exponential backoff to the WebSocket reconnection in \
`remote.rs`. Let me first read the current implementation."
.to_string(),
tool_name: None,
tool_output: None,
},
AgentMessage {
id: "m3".to_string(),
role: MessageRole::Tool,
content: "Read src-tauri/src/remote.rs".to_string(),
tool_name: Some("Read".to_string()),
tool_output: Some(
"pub struct RemoteManager {\n connections: HashMap<String, WebSocketConn>,\n \
reconnect_attempts: HashMap<String, u32>,\n}"
.to_string(),
),
},
AgentMessage {
id: "m4".to_string(),
role: MessageRole::Assistant,
content: "I can see the current implementation tracks reconnect attempts but \
doesn't implement backoff. I'll add the exponential backoff logic \
with a 30-second cap."
.to_string(),
tool_name: None,
tool_output: None,
},
AgentMessage {
id: "m5".to_string(),
role: MessageRole::Tool,
content: "Edit src-tauri/src/remote.rs".to_string(),
tool_name: Some("Edit".to_string()),
tool_output: Some(
"Added reconnect_with_backoff() method:\n\
- Base delay: 1s\n\
- Multiplier: 2x per attempt\n\
- Cap: 30s\n\
- Max retries: 5"
.to_string(),
),
},
AgentMessage {
id: "m6".to_string(),
role: MessageRole::Assistant,
content: "Done. The WebSocket reconnection now uses exponential backoff \
(1s -> 2s -> 4s -> 8s -> 16s -> 30s cap). Added \
`reconnect_with_backoff()` method with 5 max retries before \
giving up and emitting a `remote-machine-failed` event."
.to_string(),
tool_name: None,
tool_output: None,
},
]
}
/// Demo terminal output lines.
pub fn demo_terminal_lines() -> Vec<TerminalLine> {
vec![
TerminalLine {
kind: TerminalLineKind::Prompt,
text: "~/code/ai/agent-orchestrator $ ".to_string(),
},
TerminalLine {
kind: TerminalLineKind::Output,
text: "cargo test --workspace".to_string(),
},
TerminalLine {
kind: TerminalLineKind::Output,
text: " Compiling agor-core v0.1.0".to_string(),
},
TerminalLine {
kind: TerminalLineKind::Output,
text: " Compiling agor-dioxus v0.1.0".to_string(),
},
TerminalLine {
kind: TerminalLineKind::Output,
text: " Running tests/unit.rs".to_string(),
},
TerminalLine {
kind: TerminalLineKind::Output,
text: "test result: ok. 47 passed; 0 failed".to_string(),
},
TerminalLine {
kind: TerminalLineKind::Prompt,
text: "~/code/ai/agent-orchestrator $ ".to_string(),
},
]
}
/// Command palette command entries.
#[derive(Debug, Clone)]
pub struct PaletteCommand {
pub label: String,
pub shortcut: Option<String>,
pub icon: String,
}
pub fn palette_commands() -> Vec<PaletteCommand> {
vec![
PaletteCommand {
label: "New Agent Session".to_string(),
shortcut: Some("Ctrl+N".to_string()),
icon: "\u{25B6}".to_string(), // play
},
PaletteCommand {
label: "Stop Agent".to_string(),
shortcut: Some("Ctrl+C".to_string()),
icon: "\u{25A0}".to_string(), // stop
},
PaletteCommand {
label: "Toggle Settings".to_string(),
shortcut: Some("Ctrl+,".to_string()),
icon: "\u{2699}".to_string(), // gear
},
PaletteCommand {
label: "Switch Project Group".to_string(),
shortcut: Some("Ctrl+G".to_string()),
icon: "\u{25A3}".to_string(), // box
},
PaletteCommand {
label: "Focus Next Project".to_string(),
shortcut: Some("Ctrl+]".to_string()),
icon: "\u{2192}".to_string(), // arrow right
},
PaletteCommand {
label: "Toggle Terminal".to_string(),
shortcut: Some("Ctrl+`".to_string()),
icon: "\u{2588}".to_string(), // terminal
},
PaletteCommand {
label: "Search Everything".to_string(),
shortcut: Some("Ctrl+Shift+F".to_string()),
icon: "\u{1F50D}".to_string(), // magnifying glass (will render as text)
},
PaletteCommand {
label: "Reload Agent Config".to_string(),
shortcut: None,
icon: "\u{21BB}".to_string(), // reload
},
]
}