//! Application state — projects, agents, settings. //! //! All mutable state lives in `AppState`, wrapped in `Entity` 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, /// For tool results: the result content. pub tool_result: Option, /// 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, 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 { 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, pub focused_project_idx: Option, 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 { let mut p1 = Project::new("agent-orchestrator", "~/code/ai/agent-orchestrator", 0); p1.agent.status = AgentStatus::Running; // Triggers pulsing dot animation Self { projects: vec![ p1, 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 { 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() } }