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
262
ui-gpui/src/state.rs
Normal file
262
ui-gpui/src/state.rs
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
//! Application state — projects, agents, settings.
|
||||
//!
|
||||
//! All mutable state lives in `AppState`, wrapped in `Entity<AppState>` for
|
||||
//! GPUI reactivity. Components read via `entity.read(cx)` and mutate via
|
||||
//! `entity.update(cx, |state, cx| { ... cx.notify(); })`.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
// ── Agent Message Types ─────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum MessageRole {
|
||||
User,
|
||||
Assistant,
|
||||
System,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AgentMessage {
|
||||
pub id: String,
|
||||
pub role: MessageRole,
|
||||
pub content: String,
|
||||
pub timestamp: u64,
|
||||
/// For tool calls: the tool name.
|
||||
pub tool_name: Option<String>,
|
||||
/// For tool results: the result content.
|
||||
pub tool_result: Option<String>,
|
||||
/// Whether this message block is collapsed in the UI.
|
||||
pub collapsed: bool,
|
||||
}
|
||||
|
||||
// ── Agent State ─────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum AgentStatus {
|
||||
Idle,
|
||||
Running,
|
||||
Done,
|
||||
Error,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Project ─────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ProjectTab {
|
||||
Model,
|
||||
Docs,
|
||||
Files,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Project {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub cwd: String,
|
||||
pub active_tab: ProjectTab,
|
||||
pub agent: AgentSession,
|
||||
/// Accent color index (cycles through palette).
|
||||
pub accent_index: usize,
|
||||
}
|
||||
|
||||
impl Project {
|
||||
pub fn new(name: &str, cwd: &str, accent_index: usize) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
name: name.to_string(),
|
||||
cwd: cwd.to_string(),
|
||||
active_tab: ProjectTab::Model,
|
||||
agent: AgentSession::new(),
|
||||
accent_index,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Settings ────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Settings {
|
||||
pub theme: String,
|
||||
pub ui_font_family: String,
|
||||
pub ui_font_size: f32,
|
||||
pub term_font_family: String,
|
||||
pub term_font_size: f32,
|
||||
pub default_shell: String,
|
||||
pub default_cwd: String,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
theme: "Catppuccin Mocha".to_string(),
|
||||
ui_font_family: "Inter".to_string(),
|
||||
ui_font_size: 14.0,
|
||||
term_font_family: "JetBrains Mono".to_string(),
|
||||
term_font_size: 14.0,
|
||||
default_shell: std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_string()),
|
||||
default_cwd: dirs::home_dir()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "/".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Command Palette ─────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PaletteCommand {
|
||||
pub id: &'static str,
|
||||
pub label: &'static str,
|
||||
pub shortcut: Option<&'static str>,
|
||||
}
|
||||
|
||||
pub fn all_commands() -> Vec<PaletteCommand> {
|
||||
vec![
|
||||
PaletteCommand {
|
||||
id: "settings",
|
||||
label: "Open Settings",
|
||||
shortcut: Some("Ctrl+,"),
|
||||
},
|
||||
PaletteCommand {
|
||||
id: "new_project",
|
||||
label: "Add Project",
|
||||
shortcut: Some("Ctrl+N"),
|
||||
},
|
||||
PaletteCommand {
|
||||
id: "toggle_sidebar",
|
||||
label: "Toggle Sidebar",
|
||||
shortcut: Some("Ctrl+B"),
|
||||
},
|
||||
PaletteCommand {
|
||||
id: "focus_next",
|
||||
label: "Focus Next Project",
|
||||
shortcut: Some("Ctrl+]"),
|
||||
},
|
||||
PaletteCommand {
|
||||
id: "focus_prev",
|
||||
label: "Focus Previous Project",
|
||||
shortcut: Some("Ctrl+["),
|
||||
},
|
||||
PaletteCommand {
|
||||
id: "close_project",
|
||||
label: "Close Focused Project",
|
||||
shortcut: None,
|
||||
},
|
||||
PaletteCommand {
|
||||
id: "restart_agent",
|
||||
label: "Restart Agent",
|
||||
shortcut: None,
|
||||
},
|
||||
PaletteCommand {
|
||||
id: "stop_agent",
|
||||
label: "Stop Agent",
|
||||
shortcut: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// ── Root Application State ──────────────────────────────────────────
|
||||
|
||||
pub struct AppState {
|
||||
pub projects: Vec<Project>,
|
||||
pub focused_project_idx: Option<usize>,
|
||||
pub settings: Settings,
|
||||
pub sidebar_open: bool,
|
||||
pub settings_open: bool,
|
||||
pub palette_open: bool,
|
||||
pub palette_query: String,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
/// Create initial state with demo projects.
|
||||
pub fn new_demo() -> Self {
|
||||
Self {
|
||||
projects: vec![
|
||||
Project::new("agent-orchestrator", "~/code/ai/agent-orchestrator", 0),
|
||||
Project::new("quanta-discord-bot", "~/code/quanta-discord-bot", 1),
|
||||
],
|
||||
focused_project_idx: Some(0),
|
||||
settings: Settings::default(),
|
||||
sidebar_open: true,
|
||||
settings_open: false,
|
||||
palette_open: false,
|
||||
palette_query: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn focused_project(&self) -> Option<&Project> {
|
||||
self.focused_project_idx
|
||||
.and_then(|i| self.projects.get(i))
|
||||
}
|
||||
|
||||
pub fn focused_project_mut(&mut self) -> Option<&mut Project> {
|
||||
self.focused_project_idx
|
||||
.and_then(|i| self.projects.get_mut(i))
|
||||
}
|
||||
|
||||
pub fn toggle_sidebar(&mut self) {
|
||||
self.sidebar_open = !self.sidebar_open;
|
||||
}
|
||||
|
||||
pub fn toggle_settings(&mut self) {
|
||||
self.settings_open = !self.settings_open;
|
||||
}
|
||||
|
||||
pub fn toggle_palette(&mut self) {
|
||||
self.palette_open = !self.palette_open;
|
||||
if self.palette_open {
|
||||
self.palette_query.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// Filtered palette commands based on current query.
|
||||
pub fn filtered_commands(&self) -> Vec<PaletteCommand> {
|
||||
let q = self.palette_query.to_lowercase();
|
||||
all_commands()
|
||||
.into_iter()
|
||||
.filter(|cmd| q.is_empty() || cmd.label.to_lowercase().contains(&q))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Total running agents.
|
||||
pub fn running_agent_count(&self) -> usize {
|
||||
self.projects
|
||||
.iter()
|
||||
.filter(|p| p.agent.status == AgentStatus::Running)
|
||||
.count()
|
||||
}
|
||||
|
||||
/// Total cost across all agents.
|
||||
pub fn total_cost(&self) -> f64 {
|
||||
self.projects.iter().map(|p| p.agent.cost_usd).sum()
|
||||
}
|
||||
|
||||
/// Total tokens across all agents.
|
||||
pub fn total_tokens(&self) -> u64 {
|
||||
self.projects.iter().map(|p| p.agent.tokens_used).sum()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue