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.
350 lines
10 KiB
Rust
350 lines
10 KiB
Rust
/// 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
|
|
},
|
|
]
|
|
}
|