//! ProjectBox: individual project card with header, tab bar, content area. //! //! Each project gets a card in the grid with: //! - Header: name, status dot, CWD //! - Tab bar: Model / Docs / Files //! - Content area: AgentPane (Model tab), placeholder (Docs/Files) //! - Terminal section in Model tab use gpui::*; use crate::state::{AgentStatus, Project, ProjectTab}; use crate::theme; use crate::components::pulsing_dot::{PulsingDot, DotStatus}; // ── Accent Colors by Index ────────────────────────────────────────── fn accent_color(index: usize) -> Rgba { const ACCENTS: [Rgba; 8] = [ theme::BLUE, theme::MAUVE, theme::GREEN, theme::PEACH, theme::PINK, theme::TEAL, theme::SAPPHIRE, theme::LAVENDER, ]; ACCENTS[index % ACCENTS.len()] } // ── Status Indicator ──────────────────────────────────────────────── fn project_status_dot(status: AgentStatus) -> Div { let color = match status { AgentStatus::Idle => theme::OVERLAY0, AgentStatus::Running => theme::GREEN, AgentStatus::Done => theme::BLUE, AgentStatus::Error => theme::RED, }; div() .w(px(8.0)) .h(px(8.0)) .rounded(px(4.0)) .bg(color) } // ── Tab Button ────────────────────────────────────────────────────── fn tab_button(label: &str, active: bool, accent: Rgba) -> Div { let fg = if active { accent } else { theme::SUBTEXT0 }; let mut tab = div() .px(px(10.0)) .py(px(4.0)) .text_size(px(11.0)) .text_color(fg) .cursor_pointer() .hover(|s| s.text_color(theme::TEXT)); if active { tab = tab.border_b_2().border_color(accent); } tab.child(label.to_string()) } // ── ProjectBox View ───────────────────────────────────────────────── pub struct ProjectBox { pub project: Project, /// Entity handle for the embedded AgentPane (Model tab) pub agent_pane: Option>, /// Entity handle for the embedded terminal (Model tab) pub terminal_view: Option>, pub status_dot: Option>, } impl ProjectBox { pub fn new(project: Project) -> Self { Self { project, agent_pane: None, terminal_view: None, status_dot: None, } } /// Initialize sub-views. Must be called after the ProjectBox entity is created. pub fn init_subviews(&mut self, cx: &mut Context) { eprintln!("[ProjectBox] init_subviews for {}", self.project.name); // Create pulsing status dot let dot_status = match self.project.agent.status { AgentStatus::Running => DotStatus::Running, AgentStatus::Idle => DotStatus::Idle, AgentStatus::Done => DotStatus::Done, AgentStatus::Error => DotStatus::Error, }; let dot = cx.new(|_cx| PulsingDot::new(dot_status, 8.0)); // Start blinking AFTER entity registered (Zed BlinkManager pattern) dot.update(cx, |d, cx| d.start_blinking(cx)); self.status_dot = Some(dot); // Create agent pane with demo messages let agent_pane = cx.new(|_cx| { crate::components::agent_pane::AgentPane::with_demo_messages() }); self.agent_pane = Some(agent_pane); // Create terminal view with demo content let terminal_view = cx.new(|_cx| { let mut tv = crate::terminal::renderer::TerminalView::new(120, 10); tv.feed_demo(); tv }); self.terminal_view = Some(terminal_view); } } impl Render for ProjectBox { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { let accent = accent_color(self.project.accent_index); let name = self.project.name.clone(); let cwd = self.project.cwd.clone(); let status = self.project.agent.status; let active_tab = self.project.active_tab; // Build content area based on active tab let content = match active_tab { ProjectTab::Model => { let mut model_content = div() .id(SharedString::from(format!("model-content-{}", self.project.id))) .flex_1() .w_full() .flex() .flex_col(); // Agent pane (upper portion) if let Some(ref pane) = self.agent_pane { model_content = model_content.child( div() .flex_1() .w_full() .child(pane.clone()), ); } // Resize handle model_content = model_content.child( div() .id(SharedString::from(format!("resize-{}", self.project.id))) .w_full() .h(px(4.0)) .bg(theme::SURFACE0) .cursor_pointer() .hover(|s| s.bg(theme::SURFACE1)), ); // Terminal view if let Some(ref term) = self.terminal_view { model_content = model_content.child( div() .w_full() .h(px(180.0)) .child(term.clone()), ); } model_content } ProjectTab::Docs => { div() .id(SharedString::from(format!("docs-content-{}", self.project.id))) .flex_1() .w_full() .flex() .items_center() .justify_center() .text_size(px(14.0)) .text_color(theme::OVERLAY0) .child("Documentation viewer — renders project markdown files") } ProjectTab::Files => { div() .id(SharedString::from(format!("files-content-{}", self.project.id))) .flex_1() .w_full() .flex() .flex_col() .p(px(12.0)) .gap(px(4.0)) .child( div() .text_size(px(12.0)) .text_color(theme::SUBTEXT0) .child("src/"), ) .child( div() .pl(px(16.0)) .text_size(px(12.0)) .text_color(theme::TEXT) .child("main.rs"), ) .child( div() .pl(px(16.0)) .text_size(px(12.0)) .text_color(theme::TEXT) .child("lib.rs"), ) .child( div() .text_size(px(12.0)) .text_color(theme::SUBTEXT0) .child("Cargo.toml"), ) } }; div() .id(SharedString::from(format!("project-{}", self.project.id))) .flex_1() .min_w(px(400.0)) .min_h(px(300.0)) .flex() .flex_col() .bg(theme::BASE) .rounded(px(8.0)) .border_1() .border_color(theme::SURFACE0) .overflow_hidden() // ── Header ────────────────────────────────────────── .child( div() .w_full() .h(px(36.0)) .flex() .flex_row() .items_center() .px(px(12.0)) .gap(px(8.0)) .bg(theme::MANTLE) .border_b_1() .border_color(theme::SURFACE0) // Accent stripe on left .child( div() .w(px(3.0)) .h(px(20.0)) .rounded(px(2.0)) .bg(accent), ) // Animated status dot (Zed BlinkManager pattern) .children(self.status_dot.clone()) // Project name .child( div() .text_size(px(13.0)) .text_color(theme::TEXT) .child(name), ) .child(div().flex_1()) // CWD (ellipsized) .child( div() .text_size(px(10.0)) .text_color(theme::OVERLAY0) .max_w(px(200.0)) .overflow_hidden() .whitespace_nowrap() .child(cwd), ), ) // ── Tab Bar ───────────────────────────────────────── .child( div() .w_full() .h(px(32.0)) .flex() .flex_row() .items_center() .px(px(8.0)) .gap(px(2.0)) .bg(theme::MANTLE) .border_b_1() .border_color(theme::SURFACE0) .child(tab_button("Model", active_tab == ProjectTab::Model, accent)) .child(tab_button("Docs", active_tab == ProjectTab::Docs, accent)) .child(tab_button("Files", active_tab == ProjectTab::Files, accent)), ) // ── Content Area ──────────────────────────────────── .child(content) } }