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        widgets::chat::render_activity(self, frame, layout.activity);
30        widgets::input::render(self, frame, layout.input);
31        widgets::status::render(self, &self.metrics, frame, layout.status);
32
33        if let Some(state) = &self.file_picker_state {
34            widgets::file_picker::render(state, frame, layout.input);
35        }
36
37        if let Some(state) = &self.slash_autocomplete {
38            widgets::slash_autocomplete::render(state, frame, layout.input);
39        }
40
41        if let Some(state) = &self.confirm_state {
42            widgets::confirm::render(&state.prompt, frame, frame.area());
43        }
44
45        if let Some(state) = &self.elicitation_state {
46            widgets::elicitation::render(&state.dialog, frame, frame.area());
47        }
48
49        if let Some(palette) = &self.command_palette {
50            widgets::command_palette::render(palette, frame, frame.area());
51        }
52
53        if self.show_help {
54            widgets::help::render(frame, frame.area());
55        }
56    }
57
58    pub(super) fn draw_header(&self, frame: &mut ratatui::Frame, area: ratatui::layout::Rect) {
59        use ratatui::text::{Line, Span};
60        use ratatui::widgets::Paragraph;
61
62        let theme = Theme::default();
63
64        let provider = if self.metrics.provider_name.is_empty() {
65            "---"
66        } else {
67            &self.metrics.provider_name
68        };
69        let model = if self.metrics.model_name.is_empty() {
70            "---"
71        } else {
72            &self.metrics.model_name
73        };
74
75        let ctx_badge = if self.metrics.extended_context {
76            " [1M CTX]"
77        } else {
78            ""
79        };
80        let text = format!(
81            " Zeph v{} | Provider: {provider} | Model: {model}{ctx_badge}",
82            env!("CARGO_PKG_VERSION")
83        );
84
85        let line = Line::from(Span::styled(text, theme.header));
86        let paragraph = Paragraph::new(line).style(theme.header);
87        frame.render_widget(paragraph, area);
88    }
89
90    fn draw_side_panel(&mut self, frame: &mut ratatui::Frame, layout: &AppLayout) {
91        widgets::skills::render(&self.metrics, frame, layout.skills);
92        widgets::memory::render(&self.metrics, frame, layout.memory);
93
94        // Split the resources area into: context gauge (3 rows), compaction badge (3 rows),
95        // and the remaining resources panel. Each gauge/badge needs at least its border rows.
96        {
97            use ratatui::layout::{Constraint, Direction, Layout};
98            let context_split = Layout::default()
99                .direction(Direction::Vertical)
100                .constraints([
101                    Constraint::Length(3),
102                    Constraint::Length(3),
103                    Constraint::Min(0),
104                ])
105                .split(layout.resources);
106            widgets::context_gauge::render(&self.metrics, frame, context_split[0]);
107            widgets::compaction_badge::render(&self.metrics, frame, context_split[1]);
108            widgets::resources::render(&self.metrics, frame, context_split[2]);
109        }
110
111        let tick = self.throbber_state.index().cast_unsigned();
112        let has_graph = self.metrics.orchestration_graph.as_ref().is_some_and(|s| {
113            // Use is_stale() to check if snapshot is too old to show (IC4).
114            !s.is_stale()
115        });
116        let panel_focused = self.active_panel == Panel::SubAgents;
117
118        // When SubAgents panel is focused (`a` key), always show the interactive sidebar.
119        // Otherwise: auto-show plan when graph active, security events, or subagents list.
120        if panel_focused {
121            widgets::subagents::render_interactive(
122                &self.metrics,
123                &mut self.subagent_sidebar,
124                frame,
125                layout.subagents,
126                tick,
127            );
128        } else if has_graph && !self.sessions.current().plan_view_active {
129            widgets::plan_view::render(&self.metrics, frame, layout.subagents, tick);
130        } else if self.has_recent_security_events() {
131            widgets::security::render(&self.metrics, frame, layout.subagents);
132        } else {
133            widgets::subagents::render(&self.metrics, frame, layout.subagents);
134        }
135
136        // Overlay task registry over the subagents slot when `/tasks` is toggled.
137        if self.show_task_panel {
138            if self.task_supervisor.is_some() {
139                widgets::task_registry::render(
140                    &self.cached_task_snapshots,
141                    tick,
142                    layout.subagents,
143                    frame,
144                );
145            } else {
146                use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
147                let theme = Theme::default();
148                let block = Block::default()
149                    .borders(Borders::ALL)
150                    .border_style(theme.panel_border)
151                    .title(" Tasks ");
152                let paragraph = Paragraph::new(" Task supervisor not available.")
153                    .block(block)
154                    .wrap(Wrap { trim: true });
155                frame.render_widget(paragraph, layout.subagents);
156            }
157        }
158    }
159}