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
350
ui-dioxus/src/state.rs
Normal file
350
ui-dioxus/src/state.rs
Normal 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
|
||||
},
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue