1use ratatui::Frame;
2use ratatui::style::Style;
3use ratatui::widgets::Block;
4
5use crate::layout::MainLayout;
6use crate::state::{AppState, ModalKind};
7use crate::theme;
8use crate::widgets::delegate_bar::DelegateBar;
9use crate::widgets::tool_detail::ToolDetail;
10use crate::widgets::tool_history::{flat_rows, HistoryRow, ToolHistory};
11use crate::widgets::{DialogSelect, Footer, InfoDialog, InputBox, InputDialog, MessageList, PermissionDialog, SidebarInfo, SlashPopover, SummaryBar, ThinkingPanel, ToastStack};
12
13pub struct Renderer;
14
15impl Renderer {
16 pub fn new() -> Self { Self }
17
18 pub fn draw(frame: &mut Frame, state: &mut AppState) {
19 let full = frame.area();
20 frame.render_widget(
21 Block::default().style(Style::default().bg(theme::BACKGROUND())),
22 full,
23 );
24
25 let thinking_lines = if state.is_streaming && !state.live_thinking.trim().is_empty() {
26 state
27 .live_thinking
28 .lines()
29 .filter(|l| !l.trim().is_empty())
30 .count()
31 } else {
32 0
33 };
34
35 let delegate_lines = state.sub_agents.len();
36 let has_summary = state.last_summary.is_some();
37 let summary_items: Option<Vec<String>> = state.last_summary.clone();
38 let layout = MainLayout::split(
39 full,
40 state.input.line_count(),
41 thinking_lines,
42 delegate_lines,
43 has_summary,
44 );
45
46 let tool_area = layout.tool_history;
47 if let Some(area) = tool_area {
48 frame.render_widget(ToolHistory::new(state), area);
49 }
50 if let Some(info_area) = layout.tool_info {
51 frame.render_widget(
52 SidebarInfo {
53 cwd: &state.cwd,
54 model: &state.model_name,
55 mode: &state.permission_mode,
56 cost: state.total_cost,
57 git_branch: state.git_branch.as_deref(),
58 is_streaming: state.is_streaming,
59 is_pending: state.is_pending,
60 is_paused: state.is_paused,
61 spinner_frame: state.spinner_frame,
62 elapsed_secs: state.elapsed_secs,
63 },
64 info_area,
65 );
66 }
67
68 frame.render_widget(
69 MessageList::new(&mut state.conversation, state.spinner_frame)
70 .with_tool_details(state.tool_details)
71 .with_thinking_active(state.is_streaming && !state.live_thinking.trim().is_empty()),
72 layout.messages,
73 );
74 if let Some(thinking_area) = layout.thinking {
75 frame.render_widget(
76 ThinkingPanel::new(&state.live_thinking, state.spinner_frame),
77 thinking_area,
78 );
79 }
80 if let Some(delegate_area) = layout.delegate {
81 frame.render_widget(
82 DelegateBar::new(&state.sub_agents, state.spinner_frame),
83 delegate_area,
84 );
85 }
86 if let (Some(summary_area), Some(items)) = (layout.summary, summary_items) {
87 frame.render_widget(SummaryBar::new(&items), summary_area);
88 }
89 frame.render_widget(InputBox::new(&state.input, !state.is_streaming), layout.input);
90 if !state.input.slash_matches.is_empty() {
91 frame.render_widget(SlashPopover::new(&state.input, layout.input), full);
92 }
93 if let Some(footer_area) = layout.footer {
94 frame.render_widget(
95 Footer {
96 cwd: &state.cwd,
97 model: &state.model_name,
98 mode: &state.permission_mode,
99 cost: state.total_cost,
100 git_branch: state.git_branch.as_deref(),
101 is_streaming: state.is_streaming,
102 is_pending: state.is_pending,
103 is_paused: state.is_paused,
104 spinner_frame: state.spinner_frame,
105 elapsed_secs: state.elapsed_secs,
106 },
107 footer_area,
108 );
109 }
110
111 frame.render_widget(ToastStack::new(&state.toasts), full);
112
113 if state.tool_history.detail_open {
114 if let Some(idx) = state.tool_history.selected {
115 let rows = flat_rows(state);
116 if let Some(row) = rows.get(idx) {
117 let (mi, ti) = match row {
118 HistoryRow::Tool { msg, tool } => (*msg, *tool),
119 HistoryRow::Sub { msg, tool, .. } => (*msg, *tool),
120 };
121 let tool = &state.conversation.messages[mi].tool_uses[ti];
122 frame.render_widget(ToolDetail::new(tool), full);
123 }
124 }
125 }
126
127 match &state.modal.active {
128 Some(ModalKind::Permission { tool_name, description, choice }) => {
129 let chat = layout.messages;
132 let dialog_area = ratatui::layout::Rect {
133 x: chat.x,
134 y: full.y,
135 width: chat.width,
136 height: full.height,
137 };
138 frame.render_widget(
139 PermissionDialog::new(tool_name, description, *choice),
140 dialog_area,
141 );
142 }
143 Some(ModalKind::Select {
144 title,
145 query,
146 options,
147 selected,
148 current_value,
149 footer_hint,
150 ..
151 }) => {
152 frame.render_widget(
153 DialogSelect::new(title, query, options, *selected)
154 .with_current(current_value.as_deref())
155 .with_footer(footer_hint.as_deref()),
156 full,
157 );
158 }
159 Some(ModalKind::Info { title, rows }) => {
160 frame.render_widget(InfoDialog::new(title, rows), full);
161 }
162 Some(ModalKind::Input { title, prompt, buffer, .. }) => {
163 frame.render_widget(InputDialog::new(title, prompt, buffer), full);
164 }
165 Some(ModalKind::QuitConfirm) => {}
166 None => {}
167 }
168 }
169}
170
171impl Default for Renderer {
172 fn default() -> Self { Self }
173}