1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
//! App Frame Rendering
//!
//! Contains frame rendering logic for the unified TUI architecture.
//! Studio, Command, Control (3-view architecture)
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::Style;
use ratatui::widgets::{Clear, Paragraph};
use crate::error::{NikaError, Result};
use super::super::theme::TaskStatus;
use super::super::views::{TuiView, View};
use super::super::widgets::{
check_terminal_size, ConnectionStatus, Header, NikaIntro, StatusBar, StatusMessageWidget,
StatusMetrics,
};
use super::App;
impl App {
/// Render the current frame based on active view
///
/// Uses the View trait's render method for each view type.
/// Each view handles its own layout and widget rendering.
///
/// Layout: Header (1 line) + Content (dynamic) + StatusBar (1 line)
pub(crate) fn render_unified_frame(&mut self) -> Result<()> {
let current_view = self.current_view;
if let Some(ref mut terminal) = self.terminal {
// Ensure timeline cache is up-to-date before rendering Monitor view
if current_view == TuiView::Command {
self.state.ensure_timeline_cache();
}
// All views use unified layout with Header + Content + StatusBar
// Extract read-only values BEFORE taking mutable references
// This allows render() to take &mut self for scroll state updates
let total_tokens = self.command_view.chat.total_tokens();
let provider = self.command_view.chat.provider();
let chat_status = self.command_view.status_line(&self.state);
let _home_status = self
.home_view
.as_ref()
.map(|hv| hv.status_line(&self.state))
.unwrap_or_default();
let studio_status = self.studio_view.status_line(&self.state);
let _monitor_status = {
let task_count = self.state.tasks.len();
let completed = self
.state
.tasks
.values()
.filter(|t| t.status == TaskStatus::Success)
.count();
format!("Tasks: {}/{}", completed, task_count)
};
// Extract references to avoid borrow issues with the closure
let theme = &self.theme;
let state = &self.state;
let command_view = &mut self.command_view;
let _home_view = &mut self.home_view;
let studio_view = &mut self.studio_view;
let control_view = &mut self.control_view;
let workflow_path = &self.state.workflow.path;
let intro_state = &self.intro_state;
// P0 Fix: Use is_paused() accessor for unified pause state
let paused = self.state.is_paused();
let input_mode = self.input_mode;
// Extract data for StatusBar metrics from the centralized pool
let mcp_total = self.mcp_pool.config_count();
let mcp_connected = self.mcp_pool.connected_count();
// Get custom status text from current view (using pre-computed values)
let status_text = match current_view {
TuiView::Studio => studio_status,
TuiView::Command => chat_status,
TuiView::Control => control_view.status_line(state),
};
terminal
.draw(|frame| {
let size = frame.area();
let layout_mode = check_terminal_size(size);
// If terminal is too small, show overlay and return early
if !layout_mode.is_usable() {
use super::super::widgets::TerminalTooSmallOverlay;
frame.render_widget(Clear, size);
let overlay = TerminalTooSmallOverlay::new(size.width, size.height);
frame.render_widget(overlay, size);
return;
}
if let Some(intro) = intro_state {
if !intro.is_done() {
let intro_widget = NikaIntro::new(intro);
frame.render_widget(intro_widget, size);
return;
}
}
// Only clear on full redraw (view switch, resize, first frame)
// Ratatui's differential rendering handles incremental updates
if state.dirty.all {
frame.render_widget(Clear, size);
let bg = Paragraph::new("").style(Style::default().bg(theme.background));
frame.render_widget(bg, size);
}
// Layout: Header (1) + Content (dynamic) + StatusBar (1)
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1), // Header
Constraint::Min(0), // Content
Constraint::Length(1), // StatusBar
])
.split(size);
// Render header
let workflow_name = std::path::Path::new(workflow_path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("No workflow");
let header = Header::new(current_view, theme)
.context(workflow_name)
.status(if paused { "PAUSED" } else { "" });
frame.render_widget(header, chunks[0]);
// Render view content based on current view (3-view architecture)
match current_view {
TuiView::Studio => {
studio_view.render(frame, chunks[1], state, theme);
}
TuiView::Command => {
command_view.render(frame, chunks[1], state, theme);
}
TuiView::Control => {
control_view.render(frame, chunks[1], state, theme);
}
}
// Render status message if active (just above status bar)
// Skip when overlays are visible to prevent overlap
let overlay_visible = matches!(current_view, TuiView::Command)
&& (command_view.chat.provider_modal.visible
|| command_view.chat.command_palette.visible
|| command_view.chat.help_overlay.visible);
if !overlay_visible {
if let Some(msg) = state.status_messages.current() {
// Position status message at bottom of content area
let msg_area = Rect {
x: chunks[1].x,
y: chunks[1].bottom().saturating_sub(1),
width: chunks[1].width,
height: 1,
};
let status_widget = StatusMessageWidget::new(Some(msg));
frame.render_widget(status_widget, msg_area);
}
}
// Render status bar with metrics and custom status text
let metrics = StatusMetrics::new()
.provider(provider)
.tokens(total_tokens)
.mcp(mcp_connected, mcp_total)
.connection(if mcp_total > 0 {
ConnectionStatus::Connected
} else {
ConnectionStatus::Disconnected
});
let status_bar = StatusBar::new(current_view, theme)
.mode(input_mode)
.metrics(metrics)
.custom_text(status_text);
frame.render_widget(status_bar, chunks[2]);
})
.map_err(|e| NikaError::TuiError {
reason: format!("Failed to draw frame: {}", e),
})?;
// Clear dirty flags after successful render
// This prepares for skip-rendering of unchanged panels
self.state.clear_dirty();
}
Ok(())
}
}