264 lines
7.8 KiB
Rust
264 lines
7.8 KiB
Rust
//! 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 {
|
|
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<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()
|
|
}
|
|
}
|