Skip to main content

zeph_tui/app/
draw.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use crate::layout::AppLayout;
5use crate::theme::Theme;
6use crate::widgets;
7
8use super::{App, Panel};
9
10impl App {
11    pub fn draw(&mut self, frame: &mut ratatui::Frame) {
12        let layout = AppLayout::compute(
13            frame.area(),
14            self.show_side_panels,
15            self.desired_input_height(),
16        );
17
18        self.draw_header(frame, layout.header);
19        if self.sessions.current().show_splash {
20            widgets::splash::render(frame, layout.chat);
21        } else {
22            let mut cache = std::mem::take(&mut self.sessions.current_mut().render_cache);
23            let max_scroll = widgets::chat::render(self, frame, layout.chat, &mut cache);
24            self.sessions.current_mut().render_cache = cache;
25            self.sessions.current_mut().scroll_offset =
26                self.sessions.current().scroll_offset.min(max_scroll);
27        }
28        self.draw_side_panel(frame, &layout);
29        let spinner_idx = self.throbber_state().index().cast_unsigned();
30        let busy = self.is_agent_busy();
31        let activity_label = self.status_label().map(str::to_owned);
32        let supervisor_label = self.supervisor_activity_label();
33        let effective_label = activity_label.or(supervisor_label);
34        widgets::input::render(
35            self,
36            frame,
37            layout.input,
38            busy,
39            effective_label.as_deref(),
40            spinner_idx,
41        );
42        widgets::status::render(self, &self.metrics, frame, layout.status);
43
44        if let Some(state) = &self.file_picker_state {
45            widgets::file_picker::render(state, frame, layout.input);
46        }
47
48        if let Some(state) = &self.slash_autocomplete {
49            widgets::slash_autocomplete::render(state, frame, layout.input);
50        }
51
52        if let Some(state) = &self.reverse_search {
53            widgets::reverse_search::render(
54                state,
55                &self.sessions.current().input_history,
56                frame,
57                layout.input,
58            );
59        }
60
61        if let Some(state) = &self.confirm_state {
62            widgets::confirm::render(&state.prompt, frame, frame.area());
63        }
64
65        if let Some(state) = &self.elicitation_state {
66            widgets::elicitation::render(&state.dialog, frame, frame.area());
67        }
68
69        if let Some(palette) = &self.command_palette {
70            widgets::command_palette::render(palette, frame, frame.area());
71        }
72
73        if self.show_help {
74            widgets::help::render(frame, frame.area());
75        }
76    }
77
78    pub(super) fn draw_header(&self, frame: &mut ratatui::Frame, area: ratatui::layout::Rect) {
79        use ratatui::text::{Line, Span};
80        use ratatui::widgets::Paragraph;
81
82        let theme = Theme::default();
83
84        let provider = if self.metrics.provider_name.is_empty() {
85            "---"
86        } else {
87            &self.metrics.provider_name
88        };
89        let model = if self.metrics.model_name.is_empty() {
90            "---"
91        } else {
92            &self.metrics.model_name
93        };
94
95        let ctx_badge = if self.metrics.extended_context {
96            " [1M CTX]"
97        } else {
98            ""
99        };
100        let text = format!(
101            " Zeph v{} | Provider: {provider} | Model: {model}{ctx_badge}",
102            env!("CARGO_PKG_VERSION")
103        );
104
105        let line = Line::from(Span::styled(text, theme.header));
106        let paragraph = Paragraph::new(line).style(theme.header);
107        frame.render_widget(paragraph, area);
108    }
109
110    fn draw_side_panel(&mut self, frame: &mut ratatui::Frame, layout: &AppLayout) {
111        widgets::skills::render(&self.metrics, frame, layout.skills);
112        widgets::memory::render(&self.metrics, frame, layout.memory);
113
114        // Split the resources area into: context gauge (3 rows), compaction badge (3 rows),
115        // and the remaining resources panel. Each gauge/badge needs at least its border rows.
116        {
117            use ratatui::layout::{Constraint, Direction, Layout};
118            let context_split = Layout::default()
119                .direction(Direction::Vertical)
120                .constraints([
121                    Constraint::Length(3),
122                    Constraint::Length(3),
123                    Constraint::Min(0),
124                ])
125                .split(layout.resources);
126            widgets::context_gauge::render(&self.metrics, frame, context_split[0]);
127            widgets::compaction_badge::render(&self.metrics, frame, context_split[1]);
128            widgets::resources::render(&self.metrics, frame, context_split[2]);
129        }
130
131        let tick = self.throbber_state.index().cast_unsigned();
132        let has_graph = self.metrics.orchestration_graph.as_ref().is_some_and(|s| {
133            // Use is_stale() to check if snapshot is too old to show (IC4).
134            !s.is_stale()
135        });
136        let panel_focused = self.active_panel == Panel::SubAgents;
137
138        // When SubAgents panel is focused (`a` key), always show the interactive sidebar.
139        // Otherwise: auto-show plan when graph active, security events, or subagents list.
140        if panel_focused {
141            widgets::subagents::render_interactive(
142                &self.metrics,
143                &mut self.subagent_sidebar,
144                frame,
145                layout.subagents,
146                tick,
147            );
148        } else if has_graph && !self.sessions.current().plan_view_active {
149            widgets::plan_view::render(&self.metrics, frame, layout.subagents, tick);
150        } else if self.has_recent_security_events() {
151            widgets::security::render(&self.metrics, frame, layout.subagents);
152        } else {
153            widgets::subagents::render(&self.metrics, frame, layout.subagents);
154        }
155
156        // Overlay fleet panel over the subagents slot when `f` key is active (#3884).
157        if self.active_panel == Panel::Fleet {
158            widgets::fleet::render(
159                &self.fleet_snapshot,
160                frame,
161                layout.subagents,
162                &mut self.fleet_list_state,
163            );
164        }
165
166        // Overlay task registry over the subagents slot when `/tasks` is toggled.
167        if self.show_task_panel {
168            if self.task_supervisor.is_some() {
169                widgets::task_registry::render(
170                    &self.cached_task_snapshots,
171                    tick,
172                    layout.subagents,
173                    frame,
174                );
175            } else {
176                use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
177                let theme = Theme::default();
178                let block = Block::default()
179                    .borders(Borders::ALL)
180                    .border_style(theme.panel_border)
181                    .title(" Tasks ");
182                let paragraph = Paragraph::new(" Task supervisor not available.")
183                    .block(block)
184                    .wrap(Wrap { trim: true });
185                frame.render_widget(paragraph, layout.subagents);
186            }
187        }
188    }
189}