@@ -18,10 +18,12 @@ use serde::{
1818} ;
1919use tracing:: debug;
2020
21+ use super :: consts:: CONTEXT_FILES_MAX_SIZE ;
2122use super :: hooks:: {
2223 Hook ,
2324 HookExecutor ,
2425} ;
26+ use super :: util:: drop_matched_context_files;
2527use crate :: platform:: Context ;
2628use crate :: util:: directories;
2729
@@ -44,6 +46,8 @@ pub struct ContextConfig {
4446pub struct ContextManager {
4547 ctx : Arc < Context > ,
4648
49+ max_context_files_size : usize ,
50+
4751 /// Global context configuration that applies to all profiles.
4852 pub global_config : ContextConfig ,
4953
@@ -65,9 +69,16 @@ impl ContextManager {
6569 /// 2. Load the global configuration
6670 /// 3. Load the default profile configuration
6771 ///
72+ /// # Arguments
73+ /// * `ctx` - The context to use
74+ /// * `max_context_files_size` - Optional maximum token size for context files. If not provided,
75+ /// defaults to `CONTEXT_FILES_MAX_SIZE`.
76+ ///
6877 /// # Returns
6978 /// A Result containing the new ContextManager or an error
70- pub async fn new ( ctx : Arc < Context > ) -> Result < Self > {
79+ pub async fn new ( ctx : Arc < Context > , max_context_files_size : Option < usize > ) -> Result < Self > {
80+ let max_context_files_size = max_context_files_size. unwrap_or ( CONTEXT_FILES_MAX_SIZE ) ;
81+
7182 let profiles_dir = directories:: chat_profiles_dir ( & ctx) ?;
7283
7384 ctx. fs ( ) . create_dir_all ( & profiles_dir) . await ?;
@@ -78,6 +89,7 @@ impl ContextManager {
7889
7990 Ok ( Self {
8091 ctx,
92+ max_context_files_size,
8193 global_config,
8294 current_profile,
8395 profile_config,
@@ -136,7 +148,7 @@ impl ContextManager {
136148 for path in & paths {
137149 // We're using a temporary context_files vector just for validation
138150 // Pass is_validation=true to ensure we error if glob patterns don't match any files
139- match process_path ( & self . ctx , path, & mut context_files, false , true ) . await {
151+ match process_path ( & self . ctx , path, & mut context_files, true ) . await {
140152 Ok ( _) => { } , // Path is valid
141153 Err ( e) => return Err ( eyre ! ( "Invalid path '{}': {}. Use --force to add anyway." , path, e) ) ,
142154 }
@@ -425,17 +437,15 @@ impl ContextManager {
425437 /// 3. Reads the content of each file
426438 /// 4. Returns a vector of (filename, content) pairs
427439 ///
428- /// # Arguments
429- /// * `force` - If true, include paths that don't exist yet
430440 ///
431441 /// # Returns
432442 /// A Result containing a vector of (filename, content) pairs or an error
433- pub async fn get_context_files ( & self , force : bool ) -> Result < Vec < ( String , String ) > > {
443+ pub async fn get_context_files ( & self ) -> Result < Vec < ( String , String ) > > {
434444 let mut context_files = Vec :: new ( ) ;
435445
436- self . collect_context_files ( & self . global_config . paths , & mut context_files, force )
446+ self . collect_context_files ( & self . global_config . paths , & mut context_files)
437447 . await ?;
438- self . collect_context_files ( & self . profile_config . paths , & mut context_files, force )
448+ self . collect_context_files ( & self . profile_config . paths , & mut context_files)
439449 . await ?;
440450
441451 context_files. sort_by ( |a, b| a. 0 . cmp ( & b. 0 ) ) ;
@@ -444,41 +454,49 @@ impl ContextManager {
444454 Ok ( context_files)
445455 }
446456
447- pub async fn get_context_files_by_path ( & self , force : bool , path : & str ) -> Result < Vec < ( String , String ) > > {
457+ pub async fn get_context_files_by_path ( & self , path : & str ) -> Result < Vec < ( String , String ) > > {
448458 let mut context_files = Vec :: new ( ) ;
449- process_path ( & self . ctx , path, & mut context_files, force , true ) . await ?;
459+ process_path ( & self . ctx , path, & mut context_files, true ) . await ?;
450460 Ok ( context_files)
451461 }
452462
453463 /// Get all context files from the global configuration.
454- pub async fn get_global_context_files ( & self , force : bool ) -> Result < Vec < ( String , String ) > > {
464+ pub async fn get_global_context_files ( & self ) -> Result < Vec < ( String , String ) > > {
455465 let mut context_files = Vec :: new ( ) ;
456466
457- self . collect_context_files ( & self . global_config . paths , & mut context_files, force )
467+ self . collect_context_files ( & self . global_config . paths , & mut context_files)
458468 . await ?;
459469
460470 Ok ( context_files)
461471 }
462472
463473 /// Get all context files from the current profile configuration.
464- pub async fn get_current_profile_context_files ( & self , force : bool ) -> Result < Vec < ( String , String ) > > {
474+ pub async fn get_current_profile_context_files ( & self ) -> Result < Vec < ( String , String ) > > {
465475 let mut context_files = Vec :: new ( ) ;
466476
467- self . collect_context_files ( & self . profile_config . paths , & mut context_files, force )
477+ self . collect_context_files ( & self . profile_config . paths , & mut context_files)
468478 . await ?;
469479
470480 Ok ( context_files)
471481 }
472482
473- async fn collect_context_files (
474- & self ,
475- paths : & [ String ] ,
476- context_files : & mut Vec < ( String , String ) > ,
477- force : bool ,
478- ) -> Result < ( ) > {
483+ /// Collects context files and optionally drops files if the total size exceeds the limit.
484+ /// Returns (files_to_use, dropped_files)
485+ pub async fn collect_context_files_with_limit ( & self ) -> Result < ( Vec < ( String , String ) > , Vec < ( String , String ) > ) > {
486+ let mut files = self . get_context_files ( ) . await ?;
487+
488+ let dropped_files = drop_matched_context_files ( & mut files, self . max_context_files_size ) . unwrap_or_default ( ) ;
489+
490+ // remove dropped files from files
491+ files. retain ( |file| !dropped_files. iter ( ) . any ( |dropped| dropped. 0 == file. 0 ) ) ;
492+
493+ Ok ( ( files, dropped_files) )
494+ }
495+
496+ async fn collect_context_files ( & self , paths : & [ String ] , context_files : & mut Vec < ( String , String ) > ) -> Result < ( ) > {
479497 for path in paths {
480498 // Use is_validation=false to handle non-matching globs gracefully
481- process_path ( & self . ctx , path, context_files, force , false ) . await ?;
499+ process_path ( & self . ctx , path, context_files, false ) . await ?;
482500 }
483501 Ok ( ( ) )
484502 }
@@ -647,7 +665,6 @@ async fn load_profile_config(ctx: &Context, profile_name: &str) -> Result<Contex
647665/// # Arguments
648666/// * `path` - The path to process
649667/// * `context_files` - The collection to add files to
650- /// * `force` - If true, include paths that don't exist yet
651668/// * `is_validation` - If true, error when glob patterns don't match; if false, silently skip
652669///
653670/// # Returns
@@ -656,7 +673,6 @@ async fn process_path(
656673 ctx : & Context ,
657674 path : & str ,
658675 context_files : & mut Vec < ( String , String ) > ,
659- force : bool ,
660676 is_validation : bool ,
661677) -> Result < ( ) > {
662678 // Expand ~ to home directory
@@ -703,7 +719,7 @@ async fn process_path(
703719 }
704720 }
705721
706- if !found_any && !force && is_validation {
722+ if !found_any && is_validation {
707723 // When validating paths (e.g., for /context add), error if no files match
708724 return Err ( eyre ! ( "No files found matching glob pattern '{}'" , full_path) ) ;
709725 }
@@ -728,16 +744,10 @@ async fn process_path(
728744 }
729745 }
730746 }
731- } else if !force && is_validation {
747+ } else if is_validation {
732748 // When validating paths (e.g., for /context add), error if the path doesn't exist
733749 return Err ( eyre ! ( "Path '{}' does not exist" , full_path) ) ;
734- } else if force {
735- // When using --force, we'll add the path even though it doesn't exist
736- // This allows users to add paths that will exist in the future
737- context_files. push ( ( full_path. clone ( ) , format ! ( "(Path '{}' does not exist yet)" , full_path) ) ) ;
738750 }
739- // When just showing expanded files (e.g., for /context show --expand),
740- // silently skip non-existent paths if is_validation is false
741751 }
742752
743753 Ok ( ( ) )
@@ -796,9 +806,10 @@ mod tests {
796806 use super :: * ;
797807
798808 // Helper function to create a test ContextManager with Context
799- pub async fn create_test_context_manager ( ) -> Result < ContextManager > {
809+ pub async fn create_test_context_manager ( context_file_size : Option < usize > ) -> Result < ContextManager > {
810+ let context_file_size = context_file_size. unwrap_or ( CONTEXT_FILES_MAX_SIZE ) ;
800811 let ctx = Context :: builder ( ) . with_test_home ( ) . await . unwrap ( ) . build_fake ( ) ;
801- let manager = ContextManager :: new ( ctx) . await ?;
812+ let manager = ContextManager :: new ( ctx, Some ( context_file_size ) ) . await ?;
802813 Ok ( manager)
803814 }
804815
@@ -823,7 +834,7 @@ mod tests {
823834
824835 #[ tokio:: test]
825836 async fn test_profile_ops ( ) -> Result < ( ) > {
826- let mut manager = create_test_context_manager ( ) . await ?;
837+ let mut manager = create_test_context_manager ( None ) . await ?;
827838 let ctx = Arc :: clone ( & manager. ctx ) ;
828839
829840 assert_eq ! ( manager. current_profile, "default" ) ;
@@ -860,28 +871,43 @@ mod tests {
860871 Ok ( ( ) )
861872 }
862873
874+ #[ tokio:: test]
875+ async fn test_collect_exceeds_limit ( ) -> Result < ( ) > {
876+ let mut manager = create_test_context_manager ( Some ( 2 ) ) . await ?;
877+ let ctx: Arc < Context > = Arc :: clone ( & manager. ctx ) ;
878+
879+ ctx. fs ( ) . create_dir_all ( "test" ) . await ?;
880+ ctx. fs ( ) . write ( "test/to-include.md" , "ha" ) . await ?;
881+ ctx. fs ( )
882+ . write ( "test/to-drop.md" , "long content that exceed limit" )
883+ . await ?;
884+ manager. add_paths ( vec ! [ "test/*.md" . to_string( ) ] , false , false ) . await ?;
885+
886+ let ( used, dropped) = manager. collect_context_files_with_limit ( ) . await . unwrap ( ) ;
887+
888+ assert ! ( used. len( ) + dropped. len( ) == 2 ) ;
889+ assert ! ( used. len( ) == 1 ) ;
890+ assert ! ( dropped. len( ) == 1 ) ;
891+ Ok ( ( ) )
892+ }
893+
863894 #[ tokio:: test]
864895 async fn test_path_ops ( ) -> Result < ( ) > {
865- let mut manager = create_test_context_manager ( ) . await ?;
866- let ctx = Arc :: clone ( & manager. ctx ) ;
896+ let mut manager = create_test_context_manager ( None ) . await ?;
897+ let ctx: Arc < Context > = Arc :: clone ( & manager. ctx ) ;
867898
868899 // Create some test files for matching.
869900 ctx. fs ( ) . create_dir_all ( "test" ) . await ?;
870901 ctx. fs ( ) . write ( "test/p1.md" , "p1" ) . await ?;
871902 ctx. fs ( ) . write ( "test/p2.md" , "p2" ) . await ?;
872903
873904 assert ! (
874- manager. get_context_files( false ) . await ?. is_empty( ) ,
905+ manager. get_context_files( ) . await ?. is_empty( ) ,
875906 "no files should be returned for an empty profile when force is false"
876907 ) ;
877- assert_eq ! (
878- manager. get_context_files( true ) . await ?. len( ) ,
879- 2 ,
880- "default non-glob global files should be included when force is true"
881- ) ;
882908
883909 manager. add_paths ( vec ! [ "test/*.md" . to_string( ) ] , false , false ) . await ?;
884- let files = manager. get_context_files ( false ) . await ?;
910+ let files = manager. get_context_files ( ) . await ?;
885911 assert ! ( files[ 0 ] . 0 . ends_with( "p1.md" ) ) ;
886912 assert_eq ! ( files[ 0 ] . 1 , "p1" ) ;
887913 assert ! ( files[ 1 ] . 0 . ends_with( "p2.md" ) ) ;
@@ -900,7 +926,7 @@ mod tests {
900926
901927 #[ tokio:: test]
902928 async fn test_add_hook ( ) -> Result < ( ) > {
903- let mut manager = create_test_context_manager ( ) . await ?;
929+ let mut manager = create_test_context_manager ( None ) . await ?;
904930 let hook = Hook :: new_inline_hook ( HookTrigger :: ConversationStart , "echo test" . to_string ( ) ) ;
905931
906932 // Test adding hook to profile config
@@ -919,7 +945,7 @@ mod tests {
919945
920946 #[ tokio:: test]
921947 async fn test_remove_hook ( ) -> Result < ( ) > {
922- let mut manager = create_test_context_manager ( ) . await ?;
948+ let mut manager = create_test_context_manager ( None ) . await ?;
923949 let hook = Hook :: new_inline_hook ( HookTrigger :: ConversationStart , "echo test" . to_string ( ) ) ;
924950
925951 manager. add_hook ( "test_hook" . to_string ( ) , hook, false ) . await ?;
@@ -936,7 +962,7 @@ mod tests {
936962
937963 #[ tokio:: test]
938964 async fn test_set_hook_disabled ( ) -> Result < ( ) > {
939- let mut manager = create_test_context_manager ( ) . await ?;
965+ let mut manager = create_test_context_manager ( None ) . await ?;
940966 let hook = Hook :: new_inline_hook ( HookTrigger :: ConversationStart , "echo test" . to_string ( ) ) ;
941967
942968 manager. add_hook ( "test_hook" . to_string ( ) , hook, false ) . await ?;
@@ -957,7 +983,7 @@ mod tests {
957983
958984 #[ tokio:: test]
959985 async fn test_set_all_hooks_disabled ( ) -> Result < ( ) > {
960- let mut manager = create_test_context_manager ( ) . await ?;
986+ let mut manager = create_test_context_manager ( None ) . await ?;
961987 let hook1 = Hook :: new_inline_hook ( HookTrigger :: ConversationStart , "echo test" . to_string ( ) ) ;
962988 let hook2 = Hook :: new_inline_hook ( HookTrigger :: ConversationStart , "echo test" . to_string ( ) ) ;
963989
@@ -977,7 +1003,7 @@ mod tests {
9771003
9781004 #[ tokio:: test]
9791005 async fn test_run_hooks ( ) -> Result < ( ) > {
980- let mut manager = create_test_context_manager ( ) . await ?;
1006+ let mut manager = create_test_context_manager ( None ) . await ?;
9811007 let hook1 = Hook :: new_inline_hook ( HookTrigger :: ConversationStart , "echo test" . to_string ( ) ) ;
9821008 let hook2 = Hook :: new_inline_hook ( HookTrigger :: ConversationStart , "echo test" . to_string ( ) ) ;
9831009
@@ -993,7 +1019,7 @@ mod tests {
9931019
9941020 #[ tokio:: test]
9951021 async fn test_hooks_across_profiles ( ) -> Result < ( ) > {
996- let mut manager = create_test_context_manager ( ) . await ?;
1022+ let mut manager = create_test_context_manager ( None ) . await ?;
9971023 let hook1 = Hook :: new_inline_hook ( HookTrigger :: ConversationStart , "echo test" . to_string ( ) ) ;
9981024 let hook2 = Hook :: new_inline_hook ( HookTrigger :: ConversationStart , "echo test" . to_string ( ) ) ;
9991025
0 commit comments