@@ -100,73 +100,12 @@ impl ChatView {
100100 } ) ;
101101 }
102102
103- fn scroll_to_bottom ( & mut self ) {
104- self . scroll_offset = self
105- . content_height
106- . get ( )
107- . saturating_sub ( self . viewport_height ) ;
108- }
109-
110- fn scroll_up ( & mut self , lines : u16 ) {
111- self . scroll_offset = self . scroll_offset . saturating_sub ( lines) ;
112- self . auto_scroll = false ;
113- }
114-
115- fn scroll_down ( & mut self , lines : u16 ) {
116- let max = self
117- . content_height
118- . get ( )
119- . saturating_sub ( self . viewport_height ) ;
120- self . scroll_offset = self . scroll_offset . saturating_add ( lines) . min ( max) ;
121- if self . scroll_offset >= max {
122- self . auto_scroll = true ;
123- }
124- }
125-
126- /// Build the full text content for rendering.
127- fn build_text ( & self ) -> Text < ' _ > {
128- let mut lines: Vec < Line < ' _ > > = Vec :: new ( ) ;
129-
130- for msg in & self . messages {
131- self . push_message_lines ( & mut lines, msg. role , & msg. content ) ;
132- }
133-
134- // Streaming buffer (not yet committed).
135- if !self . streaming_buffer . is_empty ( ) {
136- self . push_message_lines ( & mut lines, ChatRole :: Assistant , & self . streaming_buffer ) ;
137- }
138-
139- Text :: from ( lines)
140- }
141-
142- /// Append a role label and content lines for a single message.
143- fn push_message_lines < ' a > (
144- & ' a self ,
145- lines : & mut Vec < Line < ' a > > ,
146- role : ChatRole ,
147- content : & ' a str ,
148- ) {
149- // Single blank line between messages.
150- if !lines. is_empty ( ) {
151- lines. push ( Line :: raw ( "" ) ) ;
152- }
153-
154- // Role label.
155- let ( label, style) = match role {
156- ChatRole :: User => ( "❯ You" , self . theme . accent ( ) ) ,
157- ChatRole :: Assistant => ( "⟡ Assistant" , self . theme . secondary ( ) ) ,
158- } ;
159- lines. push ( Line :: from ( vec ! [
160- Span :: raw( " " ) ,
161- Span :: styled( label, style) ,
162- ] ) ) ;
163-
164- // Content lines (immediately after label, no blank line).
165- for line in content. trim ( ) . lines ( ) {
166- lines. push ( Line :: from ( vec ! [
167- Span :: raw( " " ) ,
168- Span :: styled( line, self . theme. text( ) ) ,
169- ] ) ) ;
103+ /// Update cached viewport height and sync scroll position. Called by
104+ /// [`App`](super::super::app::App) after each frame.
105+ pub ( crate ) fn update_layout ( & mut self , area : Rect ) {
106+ self . viewport_height = area. height ;
107+ if self . auto_scroll {
108+ self . scroll_to_bottom ( ) ;
170109 }
171110 }
172111}
@@ -242,7 +181,32 @@ impl Component for ChatView {
242181 }
243182}
244183
184+ // ── Private Helpers ──
185+
245186impl ChatView {
187+ fn scroll_to_bottom ( & mut self ) {
188+ self . scroll_offset = self
189+ . content_height
190+ . get ( )
191+ . saturating_sub ( self . viewport_height ) ;
192+ }
193+
194+ fn scroll_up ( & mut self , lines : u16 ) {
195+ self . scroll_offset = self . scroll_offset . saturating_sub ( lines) ;
196+ self . auto_scroll = false ;
197+ }
198+
199+ fn scroll_down ( & mut self , lines : u16 ) {
200+ let max = self
201+ . content_height
202+ . get ( )
203+ . saturating_sub ( self . viewport_height ) ;
204+ self . scroll_offset = self . scroll_offset . saturating_add ( lines) . min ( max) ;
205+ if self . scroll_offset >= max {
206+ self . auto_scroll = true ;
207+ }
208+ }
209+
246210 fn render_inner ( & self , frame : & mut Frame , area : Rect ) {
247211 let text = self . build_text ( ) ;
248212 #[ expect(
@@ -255,12 +219,48 @@ impl ChatView {
255219 frame. render_widget ( paragraph, area) ;
256220 }
257221
258- /// Update cached viewport height and sync scroll position. Called by
259- /// [`App`](super::super::app::App) after each frame.
260- pub ( crate ) fn update_layout ( & mut self , area : Rect ) {
261- self . viewport_height = area. height ;
262- if self . auto_scroll {
263- self . scroll_to_bottom ( ) ;
222+ fn build_text ( & self ) -> Text < ' _ > {
223+ let mut lines: Vec < Line < ' _ > > = Vec :: new ( ) ;
224+
225+ for msg in & self . messages {
226+ self . push_message_lines ( & mut lines, msg. role , & msg. content ) ;
227+ }
228+
229+ // Streaming buffer (not yet committed).
230+ if !self . streaming_buffer . is_empty ( ) {
231+ self . push_message_lines ( & mut lines, ChatRole :: Assistant , & self . streaming_buffer ) ;
232+ }
233+
234+ Text :: from ( lines)
235+ }
236+
237+ fn push_message_lines < ' a > (
238+ & ' a self ,
239+ lines : & mut Vec < Line < ' a > > ,
240+ role : ChatRole ,
241+ content : & ' a str ,
242+ ) {
243+ // Single blank line between messages.
244+ if !lines. is_empty ( ) {
245+ lines. push ( Line :: raw ( "" ) ) ;
246+ }
247+
248+ // Role label.
249+ let ( label, style) = match role {
250+ ChatRole :: User => ( "❯ You" , self . theme . accent ( ) ) ,
251+ ChatRole :: Assistant => ( "⟡ Assistant" , self . theme . secondary ( ) ) ,
252+ } ;
253+ lines. push ( Line :: from ( vec ! [
254+ Span :: raw( " " ) ,
255+ Span :: styled( label, style) ,
256+ ] ) ) ;
257+
258+ // Content lines (immediately after label, no blank line).
259+ for line in content. trim ( ) . lines ( ) {
260+ lines. push ( Line :: from ( vec ! [
261+ Span :: raw( " " ) ,
262+ Span :: styled( line, self . theme. text( ) ) ,
263+ ] ) ) ;
264264 }
265265 }
266266}
0 commit comments