@@ -125,8 +125,8 @@ pub struct App {
125125 cloud_response : Option < AtomicNote > ,
126126 synthesis_scroll : u16 ,
127127 coaching_tip : ( String , String ) ,
128- local_tokens_used : u32 , // Token count for current local request
129- cloud_tokens_used : u32 , // Token count for current cloud request
128+ local_tokens_used : u32 , // Token count for current local request
129+ cloud_tokens_used : u32 , // Token count for current cloud request
130130}
131131
132132impl App {
@@ -481,9 +481,9 @@ impl App {
481481
482482 // Create a compact area for the synthesis (60% width, ~12 lines height, centered)
483483 let main_area = app_chunks[ 1 ] ;
484- let synthesis_width = ( main_area. width * 60 / 100 ) . max ( 40 ) . min ( 80 ) ;
484+ let synthesis_width = ( main_area. width * 60 / 100 ) . clamp ( 40 , 80 ) ;
485485 let synthesis_height = 12 . min ( main_area. height - 6 ) ;
486-
486+
487487 let synthesis_area = Rect :: new (
488488 main_area. x + ( main_area. width . saturating_sub ( synthesis_width) ) / 2 ,
489489 main_area. y + ( main_area. height . saturating_sub ( synthesis_height) ) / 2 ,
@@ -954,8 +954,28 @@ impl App {
954954 }
955955 }
956956 KeyCode :: Right => {
957- // Scroll down through synthesis content
958- self . synthesis_scroll += 1 ;
957+ // Scroll down through synthesis content with bounds checking
958+ if let Some ( note) = & self . cloud_response {
959+ // Calculate content height (number of lines when wrapped)
960+ let content_width = 60 ; // Approximate usable width after borders
961+ let lines: Vec < & str > = note. body_text . lines ( ) . collect ( ) ;
962+ let total_wrapped_lines = lines
963+ . iter ( )
964+ . map ( |line| {
965+ ( ( line. len ( ) as f32 / content_width as f32 ) . ceil ( )
966+ as u16 )
967+ . max ( 1 )
968+ } )
969+ . sum :: < u16 > ( ) ;
970+
971+ let display_height = 10 ; // Approximate usable height (12 - 2 for borders)
972+ let max_scroll =
973+ total_wrapped_lines. saturating_sub ( display_height) ;
974+
975+ if self . synthesis_scroll < max_scroll {
976+ self . synthesis_scroll += 1 ;
977+ }
978+ }
959979 }
960980 KeyCode :: Enter | KeyCode :: Esc => {
961981 // Fallback: return to normal without saving
@@ -993,11 +1013,11 @@ impl App {
9931013 // Handle regular chat message
9941014 // Store the original user query for metadata
9951015 self . original_user_query = message. clone ( ) ;
996-
1016+
9971017 // Estimate tokens for local request (rough: chars/4 + prompt overhead)
9981018 self . local_tokens_used = ( message. len ( ) / 4 ) as u32 + 500 ; // ~500 tokens for prompt template
9991019 self . cloud_tokens_used = 0 ; // Reset cloud tokens for new session
1000-
1020+
10011021 self . agent_status = AgentStatus :: Orchestrating ;
10021022 let settings = self . settings . clone ( ) ;
10031023 let tx = self . agent_tx . clone ( ) ;
@@ -1016,17 +1036,17 @@ impl App {
10161036 self . edit_buffer . clear ( ) ;
10171037 }
10181038
1019-
10201039 fn save_synthesis ( & self ) {
10211040 if let Some ( note) = & self . cloud_response {
10221041 // Generate meaningful filename from query and metadata
10231042 let timestamp = chrono:: Utc :: now ( ) ;
10241043 let date_part = timestamp. format ( "%Y-%m-%d" ) . to_string ( ) ;
1025-
1044+
10261045 // Extract keywords from original user query for filename
1027- let keywords = self . extract_filename_keywords ( & self . original_user_query , & note. header_tags ) ;
1046+ let keywords =
1047+ self . extract_filename_keywords ( & self . original_user_query , & note. header_tags ) ;
10281048 let time_suffix = timestamp. format ( "-%H%M" ) . to_string ( ) ; // Add time for uniqueness
1029-
1049+
10301050 let filename = format ! ( "{}-{}{}.md" , date_part, keywords, time_suffix) ;
10311051
10321052 // Get the selected proposal text
@@ -1042,23 +1062,29 @@ impl App {
10421062 let clean_proposal = proposal_text;
10431063
10441064 // Get model names for usage metadata
1045- let local_model = if self . settings . local_model . is_empty ( ) || self . settings . local_model == "[SELECT]" {
1065+ let local_model = if self . settings . local_model . is_empty ( )
1066+ || self . settings . local_model == "[SELECT]"
1067+ {
10461068 "unknown"
10471069 } else {
10481070 & self . settings . local_model
10491071 } ;
10501072
1051- let cloud_model = if self . settings . cloud_model . is_empty ( ) || self . settings . cloud_model == "[SELECT]" {
1073+ let cloud_model = if self . settings . cloud_model . is_empty ( )
1074+ || self . settings . cloud_model == "[SELECT]"
1075+ {
10521076 "anthropic/claude-3.5-sonnet"
10531077 } else {
10541078 & self . settings . cloud_model
10551079 } ;
10561080
10571081 // Estimate token breakdown (rough estimates)
10581082 let local_prompt_tokens = ( self . original_user_query . len ( ) / 4 ) as u32 + 200 ; // Query + template
1059- let local_completion_tokens = self . local_tokens_used . saturating_sub ( local_prompt_tokens) ;
1060- let cloud_prompt_tokens = ( self . final_prompt . len ( ) / 4 ) as u32 + 150 ; // Proposal + synthesis template
1061- let cloud_completion_tokens = self . cloud_tokens_used . saturating_sub ( cloud_prompt_tokens) ;
1083+ let local_completion_tokens =
1084+ self . local_tokens_used . saturating_sub ( local_prompt_tokens) ;
1085+ let cloud_prompt_tokens = ( self . final_prompt . len ( ) / 4 ) as u32 + 150 ; // Proposal + synthesis template
1086+ let cloud_completion_tokens =
1087+ self . cloud_tokens_used . saturating_sub ( cloud_prompt_tokens) ;
10621088
10631089 let markdown_content = format ! (
10641090 "---\n date: {}\n provider: \" OPENROUTER\" \n query: \" {}\" \n proposal: \" {}\" \n tags: [{}]\n \n usage:\n local_model: \" {}\" \n local_prompt_tokens: {}\n local_completion_tokens: {}\n cloud_model: \" {}\" \n cloud_prompt_tokens: {}\n cloud_completion_tokens: {}\n ---\n \n # {}\n \n {}\n " ,
@@ -1093,7 +1119,7 @@ impl App {
10931119 fn handle_cloud_synthesis ( & mut self ) {
10941120 // Set status to searching and trigger cloud API call
10951121 self . agent_status = AgentStatus :: Searching ;
1096-
1122+
10971123 // Estimate tokens for cloud request (prompt + synthesis template)
10981124 self . cloud_tokens_used = ( self . final_prompt . len ( ) / 4 ) as u32 + 300 ; // ~300 tokens for synthesis template
10991125
@@ -1264,9 +1290,9 @@ impl App {
12641290 "the" , "a" , "an" , "and" , "or" , "but" , "in" , "on" , "at" , "to" , "for" , "of" , "with" ,
12651291 "by" , "is" , "are" , "was" , "were" , "be" , "been" , "have" , "has" , "had" , "do" , "does" ,
12661292 "did" , "will" , "would" , "could" , "should" , "can" , "what" , "where" , "when" , "why" ,
1267- "how" , "who" , "which" , "that" , "this" , "these" , "those" , "i" , "you" , "he" , "she" ,
1268- "it" , " we", "they" , "me" , "him" , "her" , "us" , "them" , "my" , "your" , "his" , "her" ,
1269- "its" , " our", "their"
1293+ "how" , "who" , "which" , "that" , "this" , "these" , "those" , "i" , "you" , "he" , "she" , "it" ,
1294+ "we" , "they" , "me" , "him" , "her" , "us" , "them" , "my" , "your" , "his" , "her" , "its ",
1295+ "our" , "their" ,
12701296 ] ;
12711297
12721298 // Extract meaningful words from query
@@ -1276,7 +1302,7 @@ impl App {
12761302 . filter_map ( |word| {
12771303 // Clean up punctuation
12781304 let clean_word = word. trim_matches ( |c : char | !c. is_alphanumeric ( ) ) ;
1279-
1305+
12801306 // Filter out stop words and short words
12811307 if clean_word. len ( ) >= 3 && !stop_words. contains ( & clean_word) {
12821308 Some ( clean_word. to_string ( ) )
@@ -1307,5 +1333,4 @@ impl App {
13071333 }
13081334 }
13091335 }
1310-
13111336}
0 commit comments