@@ -7,10 +7,7 @@ use crossterm::style::{
77 Color ,
88} ;
99use eyre:: Result ;
10- use semantic_search_client:: {
11- OperationStatus ,
12- SystemStatus ,
13- } ;
10+ use semantic_search_client:: SystemStatus ;
1411
1512use crate :: cli:: chat:: tools:: sanitize_path_tool_arg;
1613use crate :: cli:: chat:: {
@@ -28,10 +25,15 @@ use crate::util::knowledge_store::KnowledgeStore;
2825/// Knowledge base management commands
2926#[ derive( Clone , Debug , PartialEq , Eq , Subcommand ) ]
3027pub enum KnowledgeSubcommand {
31- /// Display the knowledge base contents
28+ /// Display the knowledge base contents and background operations
3229 Show ,
3330 /// Add a file or directory to knowledge base
3431 Add {
32+ /// Name for the knowledge base entry
33+ #[ arg( long, short = 'n' ) ]
34+ name : String ,
35+ /// Path to file or directory to add
36+ #[ arg( long, short = 'p' ) ]
3537 path : String ,
3638 /// Include patterns (e.g., `**/*.ts`, `**/*.md`)
3739 #[ arg( long, action = clap:: ArgAction :: Append ) ]
@@ -50,8 +52,6 @@ pub enum KnowledgeSubcommand {
5052 Update { path : String } ,
5153 /// Remove all knowledge base entries
5254 Clear ,
53- /// Show background operation status
54- Status ,
5555 /// Cancel a background operation
5656 Cancel {
5757 /// Operation ID to cancel (optional - cancels most recent if not provided)
@@ -116,15 +116,15 @@ impl KnowledgeSubcommand {
116116 }
117117 } ,
118118 KnowledgeSubcommand :: Add {
119+ name,
119120 path,
120121 include,
121122 exclude,
122123 index_type,
123- } => Self :: handle_add ( os, session, path, include, exclude, index_type) . await ,
124+ } => Self :: handle_add ( os, session, name , path, include, exclude, index_type) . await ,
124125 KnowledgeSubcommand :: Remove { path } => Self :: handle_remove ( os, session, path) . await ,
125126 KnowledgeSubcommand :: Update { path } => Self :: handle_update ( os, session, path) . await ,
126127 KnowledgeSubcommand :: Clear => Self :: handle_clear ( os, session) . await ,
127- KnowledgeSubcommand :: Status => Self :: handle_status ( os, session) . await ,
128128 KnowledgeSubcommand :: Cancel { operation_id } => {
129129 Self :: handle_cancel ( os, session, operation_id. as_deref ( ) ) . await
130130 } ,
@@ -148,16 +148,33 @@ impl KnowledgeSubcommand {
148148 Ok ( store) => {
149149 let store = store. lock ( ) . await ;
150150 let contexts = store. get_all ( ) . await . unwrap_or_default ( ) ;
151+ let status_data = store. get_status_data ( ) . await . ok ( ) ;
152+
153+ // Show contexts if any exist
154+ if !contexts. is_empty ( ) {
155+ Self :: format_knowledge_entries_with_indent ( session, & contexts, " " ) ?;
156+ }
151157
152- if contexts. is_empty ( ) {
158+ // Show operations if any exist
159+ if let Some ( status) = & status_data {
160+ if !status. operations . is_empty ( ) {
161+ let formatted_status = Self :: format_status_display ( status) ;
162+ if !formatted_status. is_empty ( ) {
163+ queue ! ( session. stderr, style:: Print ( format!( "{}\n " , formatted_status) ) ) ?;
164+ }
165+ }
166+ }
167+
168+ // Only show <none> if both contexts and operations are empty
169+ if contexts. is_empty ( )
170+ && ( status_data. is_none ( ) || status_data. as_ref ( ) . unwrap ( ) . operations . is_empty ( ) )
171+ {
153172 queue ! (
154173 session. stderr,
155174 style:: SetForegroundColor ( Color :: DarkGrey ) ,
156175 style:: Print ( " <none>\n \n " ) ,
157176 style:: SetForegroundColor ( Color :: Reset )
158177 ) ?;
159- } else {
160- Self :: format_knowledge_entries_with_indent ( session, & contexts, " " ) ?;
161178 }
162179 } ,
163180 Err ( _) => {
@@ -194,14 +211,16 @@ impl KnowledgeSubcommand {
194211 style:: Print ( "\n " )
195212 ) ?;
196213
197- // Description line with original description
198- queue ! (
199- session. stderr,
200- style:: Print ( format!( "{} " , indent) ) ,
201- style:: SetForegroundColor ( Color :: Grey ) ,
202- style:: Print ( format!( "{}\n " , ctx. description) ) ,
203- style:: SetForegroundColor ( Color :: Reset )
204- ) ?;
214+ // Path line if available (matching operation format)
215+ if let Some ( source_path) = & ctx. source_path {
216+ queue ! (
217+ session. stderr,
218+ style:: Print ( format!( "{} " , indent) ) ,
219+ style:: SetForegroundColor ( Color :: Grey ) ,
220+ style:: Print ( format!( "{}\n " , source_path) ) ,
221+ style:: SetForegroundColor ( Color :: Reset )
222+ ) ?;
223+ }
205224
206225 // Stats line with improved colors
207226 queue ! (
@@ -237,6 +256,7 @@ impl KnowledgeSubcommand {
237256 async fn handle_add (
238257 os : & Os ,
239258 session : & mut ChatSession ,
259+ name : & str ,
240260 path : & str ,
241261 include_patterns : & [ String ] ,
242262 exclude_patterns : & [ String ] ,
@@ -276,7 +296,7 @@ impl KnowledgeSubcommand {
276296 . with_exclude_patterns ( exclude)
277297 . with_embedding_type ( embedding_type_resolved) ;
278298
279- match store. add ( path , & sanitized_path. clone ( ) , options) . await {
299+ match store. add ( name , & sanitized_path. clone ( ) , options) . await {
280300 Ok ( message) => OperationResult :: Info ( message) ,
281301 Err ( e) => {
282302 if e. contains ( "Invalid include pattern" ) || e. contains ( "Invalid exclude pattern" ) {
@@ -396,118 +416,54 @@ impl KnowledgeSubcommand {
396416 }
397417 }
398418
399- /// Handle status operation
400- async fn handle_status ( os : & Os , session : & ChatSession ) -> OperationResult {
401- let agent = Self :: get_agent ( session) ;
402- let async_knowledge_store = match KnowledgeStore :: get_async_instance ( os, agent) . await {
403- Ok ( store) => store,
404- Err ( e) => return OperationResult :: Error ( format ! ( "Error accessing knowledge base directory: {}" , e) ) ,
405- } ;
406- let store = async_knowledge_store. lock ( ) . await ;
407-
408- match store. get_status_data ( ) . await {
409- Ok ( status_data) => {
410- let formatted_status = Self :: format_status_display ( & status_data) ;
411- OperationResult :: Info ( formatted_status)
412- } ,
413- Err ( e) => OperationResult :: Error ( format ! ( "Failed to get status: {}" , e) ) ,
414- }
415- }
416-
417419 /// Format status data for display (UI rendering responsibility)
418420 fn format_status_display ( status : & SystemStatus ) -> String {
419- let mut status_lines = Vec :: new ( ) ;
420-
421- // Show knowledge base summary
422- status_lines. push ( format ! (
423- "📚 Total knowledge base entries: {} ({} persistent, {} volatile)" ,
424- status. total_contexts, status. persistent_contexts, status. volatile_contexts
425- ) ) ;
426-
427421 if status. operations . is_empty ( ) {
428- status_lines. push ( "✅ No active operations" . to_string ( ) ) ;
429- return status_lines. join ( "\n " ) ;
422+ return String :: new ( ) ; // No operations, no output
430423 }
431424
432- status_lines. push ( "📊 Active Operations:" . to_string ( ) ) ;
433- status_lines. push ( format ! (
434- " 📈 Queue Status: {} active, {} waiting (max {} concurrent)" ,
435- status. active_count, status. waiting_count, status. max_concurrent
436- ) ) ;
437-
425+ let mut output = String :: new ( ) ;
438426 for op in & status. operations {
439- let formatted_operation = Self :: format_operation_display ( op) ;
440- status_lines. push ( formatted_operation) ;
441- }
442-
443- status_lines. join ( "\n " )
444- }
445-
446- /// Format a single operation for display
447- fn format_operation_display ( op : & OperationStatus ) -> String {
448- let elapsed = op. started_at . elapsed ( ) . unwrap_or_default ( ) ;
449-
450- let ( status_icon, status_info) = if op. is_cancelled {
451- ( "🛑" , "Cancelled" . to_string ( ) )
452- } else if op. is_failed {
453- ( "❌" , op. message . clone ( ) )
454- } else if op. is_waiting {
455- ( "⏳" , op. message . clone ( ) )
456- } else if Self :: should_show_progress_bar ( op. current , op. total ) {
457- ( "🔄" , Self :: create_progress_bar ( op. current , op. total , & op. message ) )
458- } else {
459- ( "🔄" , op. message . clone ( ) )
460- } ;
461-
462- let operation_desc = op. operation_type . display_name ( ) ;
463-
464- // Format with conditional elapsed time and ETA
465- if op. is_cancelled || op. is_failed {
466- format ! (
467- " {} {} | {}\n {}" ,
468- status_icon, op. short_id, operation_desc, status_info
469- )
470- } else {
471- let mut time_info = format ! ( "Elapsed: {}s" , elapsed. as_secs( ) ) ;
472-
473- if let Some ( eta) = op. eta {
474- time_info. push_str ( & format ! ( " | ETA: {}s" , eta. as_secs( ) ) ) ;
427+ let operation_desc = op. operation_type . display_name ( ) ;
428+
429+ // Main entry line with operation name and ID (like knowledge entries)
430+ output. push_str ( & format ! ( " 🔄 {} ({})\n " , operation_desc, & op. short_id) ) ;
431+
432+ // Description/path line (indented like knowledge entries)
433+ // Use actual path from operation type if available, otherwise use message
434+ let description = match & op. operation_type {
435+ semantic_search_client:: OperationType :: Indexing { path, .. } => path. clone ( ) ,
436+ semantic_search_client:: OperationType :: Clearing => op. message . clone ( ) ,
437+ } ;
438+ output. push_str ( & format ! ( " {}\n " , description) ) ;
439+
440+ // Status/progress line with ETA if available
441+ if op. is_cancelled {
442+ output. push_str ( " Cancelled\n " ) ;
443+ } else if op. is_failed {
444+ output. push_str ( " Failed\n " ) ;
445+ } else if op. is_waiting {
446+ output. push_str ( " Waiting\n " ) ;
447+ } else if Self :: should_show_progress_bar ( op. current , op. total ) {
448+ let percentage = ( op. current as f64 / op. total as f64 * 100.0 ) as u8 ;
449+ if let Some ( eta) = op. eta {
450+ output. push_str ( & format ! ( " {}% • ETA: {}s\n " , percentage, eta. as_secs( ) ) ) ;
451+ } else {
452+ output. push_str ( & format ! ( " {}%\n " , percentage) ) ;
453+ }
454+ } else {
455+ output. push_str ( " In progress\n " ) ;
475456 }
476-
477- format ! (
478- " {} {} | {}\n {} | {}" ,
479- status_icon, op. short_id, operation_desc, status_info, time_info
480- )
481457 }
458+
459+ output. trim_end ( ) . to_string ( ) // Remove trailing newline
482460 }
483461
484462 /// Check if progress bar should be shown
485463 fn should_show_progress_bar ( current : u64 , total : u64 ) -> bool {
486464 total > 0 && current <= total
487465 }
488466
489- /// Create progress bar display
490- fn create_progress_bar ( current : u64 , total : u64 , message : & str ) -> String {
491- if total == 0 {
492- return message. to_string ( ) ;
493- }
494-
495- let percentage = ( current as f64 / total as f64 * 100.0 ) as u8 ;
496- let filled = ( current as f64 / total as f64 * 30.0 ) as usize ;
497- let empty = 30 - filled;
498-
499- let mut bar = String :: new ( ) ;
500- bar. push_str ( & "█" . repeat ( filled) ) ;
501- if filled < 30 && current < total {
502- bar. push ( '▓' ) ;
503- bar. push_str ( & "░" . repeat ( empty. saturating_sub ( 1 ) ) ) ;
504- } else {
505- bar. push_str ( & "░" . repeat ( empty) ) ;
506- }
507-
508- format ! ( "{} {}% ({}/{}) {}" , bar, percentage, current, total, message)
509- }
510-
511467 /// Handle cancel operation
512468 async fn handle_cancel ( os : & Os , session : & ChatSession , operation_id : Option < & str > ) -> OperationResult {
513469 let agent = Self :: get_agent ( session) ;
@@ -583,7 +539,6 @@ impl KnowledgeSubcommand {
583539 KnowledgeSubcommand :: Remove { .. } => "remove" ,
584540 KnowledgeSubcommand :: Update { .. } => "update" ,
585541 KnowledgeSubcommand :: Clear => "clear" ,
586- KnowledgeSubcommand :: Status => "status" ,
587542 KnowledgeSubcommand :: Cancel { .. } => "cancel" ,
588543 }
589544 }
@@ -595,7 +550,7 @@ mod tests {
595550
596551 use super :: * ;
597552
598- #[ derive( Parser ) ]
553+ #[ derive( Parser , Debug ) ]
599554 #[ command( name = "test" ) ]
600555 struct TestCli {
601556 #[ command( subcommand) ]
@@ -608,6 +563,9 @@ mod tests {
608563 let result = TestCli :: try_parse_from ( [
609564 "test" ,
610565 "add" ,
566+ "--name" ,
567+ "my-project" ,
568+ "--path" ,
611569 "/some/path" ,
612570 "--include" ,
613571 "*.rs" ,
@@ -623,9 +581,14 @@ mod tests {
623581 let cli = result. unwrap ( ) ;
624582
625583 if let KnowledgeSubcommand :: Add {
626- path, include, exclude, ..
584+ name,
585+ path,
586+ include,
587+ exclude,
588+ ..
627589 } = cli. knowledge
628590 {
591+ assert_eq ! ( name, "my-project" ) ;
629592 assert_eq ! ( path, "/some/path" ) ;
630593 assert_eq ! ( include, vec![ "*.rs" , "**/*.md" ] ) ;
631594 assert_eq ! ( exclude, vec![ "node_modules/**" , "target/**" ] ) ;
@@ -650,14 +613,19 @@ mod tests {
650613 #[ test]
651614 fn test_empty_patterns_allowed ( ) {
652615 // Test that commands work without any patterns
653- let result = TestCli :: try_parse_from ( [ "test" , "add" , "/some/path" ] ) ;
616+ let result = TestCli :: try_parse_from ( [ "test" , "add" , "--name" , "my-project" , "--path" , " /some/path"] ) ;
654617 assert ! ( result. is_ok( ) ) ;
655618
656619 let cli = result. unwrap ( ) ;
657620 if let KnowledgeSubcommand :: Add {
658- path, include, exclude, ..
621+ name,
622+ path,
623+ include,
624+ exclude,
625+ ..
659626 } = cli. knowledge
660627 {
628+ assert_eq ! ( name, "my-project" ) ;
661629 assert_eq ! ( path, "/some/path" ) ;
662630 assert ! ( include. is_empty( ) ) ;
663631 assert ! ( exclude. is_empty( ) ) ;
@@ -672,6 +640,9 @@ mod tests {
672640 let result = TestCli :: try_parse_from ( [
673641 "test" ,
674642 "add" ,
643+ "--name" ,
644+ "my-project" ,
645+ "--path" ,
675646 "/some/path" ,
676647 "--include" ,
677648 "*.rs" ,
@@ -691,12 +662,31 @@ mod tests {
691662 }
692663 }
693664
665+ #[ test]
666+ fn test_add_command_with_name_and_path ( ) {
667+ // Test that add command accepts both name and path parameters
668+ let result = TestCli :: try_parse_from ( [ "test" , "add" , "--name" , "my-project" , "--path" , "/path/to/project" ] ) ;
669+
670+ assert ! ( result. is_ok( ) ) ;
671+ let cli = result. unwrap ( ) ;
672+
673+ if let KnowledgeSubcommand :: Add { name, path, .. } = cli. knowledge {
674+ assert_eq ! ( name, "my-project" ) ;
675+ assert_eq ! ( path, "/path/to/project" ) ;
676+ } else {
677+ panic ! ( "Expected Add subcommand" ) ;
678+ }
679+ }
680+
694681 #[ test]
695682 fn test_multiple_exclude_patterns ( ) {
696683 // Test multiple exclude patterns
697684 let result = TestCli :: try_parse_from ( [
698685 "test" ,
699686 "add" ,
687+ "--name" ,
688+ "my-project" ,
689+ "--path" ,
700690 "/some/path" ,
701691 "--exclude" ,
702692 "node_modules/**" ,
0 commit comments