Skip to content

Commit ff74ea1

Browse files
feat: Unify show and status commands for knowledge (#3052)
- Unifies /knowledge status and /knowledge show - Adds --path and --name args for consistency - Add short flags: -n for --name, -p for --path - Update documentation to reflect show/status unification and new flags Usage examples: /knowledge add -n myproject -p /path/to/project /knowledge add --name docs --path /path/to/docs /knowledge show # Now shows both entries and operations Co-authored-by: Kenneth S. <[email protected]>
1 parent f0df0e3 commit ff74ea1

File tree

3 files changed

+223
-222
lines changed

3 files changed

+223
-222
lines changed

crates/chat-cli/src/cli/chat/cli/knowledge.rs

Lines changed: 113 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,7 @@ use crossterm::style::{
77
Color,
88
};
99
use eyre::Result;
10-
use semantic_search_client::{
11-
OperationStatus,
12-
SystemStatus,
13-
};
10+
use semantic_search_client::SystemStatus;
1411

1512
use crate::cli::chat::tools::sanitize_path_tool_arg;
1613
use 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)]
3027
pub 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

Comments
 (0)