refactor(g3-cli): extract commands module and fix test organization
- Extract handle_command() from interactive.rs to new commands.rs module (320 lines, 15 match arms for /help, /compact, /thinnify, etc.) - Fix orphaned tests in completion.rs that were outside mod tests block - Add #[allow(dead_code)] to with_include_prompt_filename() (used in tests) - interactive.rs reduced from 595 to 290 lines Agent: fowler
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
# Project Memory
|
# Project Memory
|
||||||
> Updated: 2026-01-20T04:10:35Z | Size: 15.8k chars
|
> Updated: 2026-01-20T08:53:25Z | Size: 16.3k chars
|
||||||
|
|
||||||
### Remember Tool Wiring
|
### Remember Tool Wiring
|
||||||
- `crates/g3-core/src/tools/memory.rs` [0..5000] - `execute_remember()`, `get_memory_path()`, `merge_memory()`
|
- `crates/g3-core/src/tools/memory.rs` [0..5000] - `execute_remember()`, `get_memory_path()`, `merge_memory()`
|
||||||
@@ -285,4 +285,14 @@ Semantic data for context thinning operations, replacing pre-formatted strings.
|
|||||||
|
|
||||||
- `crates/g3-cli/src/g3_status.rs`
|
- `crates/g3-cli/src/g3_status.rs`
|
||||||
- `Status::NoChanges` [42] - new status variant for thinning with no changes
|
- `Status::NoChanges` [42] - new status variant for thinning with no changes
|
||||||
- `G3Status::thin_result()` [265..292] - formats ThinResult with proper colors/styling
|
- `G3Status::thin_result()` [265..292] - formats ThinResult with proper colors/styling
|
||||||
|
|
||||||
|
### CLI Display Utilities
|
||||||
|
Shared display functions for interactive and agent modes.
|
||||||
|
|
||||||
|
- `crates/g3-cli/src/display.rs`
|
||||||
|
- `format_workspace_path()` [9..17] - formats path with ~ for home dir
|
||||||
|
- `print_workspace_path()` [20..29] - prints formatted workspace path
|
||||||
|
- `LoadedContent` [32..39] - tracks loaded project files (README, AGENTS.md, Memory, include prompt)
|
||||||
|
- `print_loaded_status()` [87..103] - prints "✓ README ✓ AGENTS.md" status line
|
||||||
|
- `print_project_heading()` [106..114] - prints project name from README
|
||||||
320
crates/g3-cli/src/commands.rs
Normal file
320
crates/g3-cli/src/commands.rs
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
//! Interactive command handlers for G3 CLI.
|
||||||
|
//!
|
||||||
|
//! Handles `/` commands in interactive mode.
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use rustyline::Editor;
|
||||||
|
|
||||||
|
use g3_core::ui_writer::UiWriter;
|
||||||
|
use g3_core::Agent;
|
||||||
|
|
||||||
|
use crate::completion::G3Helper;
|
||||||
|
use crate::g3_status::{G3Status, Status};
|
||||||
|
use crate::simple_output::SimpleOutput;
|
||||||
|
use crate::task_execution::execute_task_with_retry;
|
||||||
|
|
||||||
|
/// Handle a control command. Returns true if the command was handled and the loop should continue.
|
||||||
|
pub async fn handle_command<W: UiWriter>(
|
||||||
|
input: &str,
|
||||||
|
agent: &mut Agent<W>,
|
||||||
|
output: &SimpleOutput,
|
||||||
|
rl: &mut Editor<G3Helper, rustyline::history::DefaultHistory>,
|
||||||
|
show_prompt: bool,
|
||||||
|
show_code: bool,
|
||||||
|
) -> Result<bool> {
|
||||||
|
match input {
|
||||||
|
"/help" => {
|
||||||
|
output.print("");
|
||||||
|
output.print("📖 Control Commands:");
|
||||||
|
output.print(" /compact - Trigger compaction (compacts conversation history)");
|
||||||
|
output.print(" /thinnify - Trigger context thinning (replaces large tool results with file references)");
|
||||||
|
output.print(" /skinnify - Trigger full context thinning (like /thinnify but for entire context, not just first third)");
|
||||||
|
output.print(" /clear - Clear session and start fresh (discards continuation artifacts)");
|
||||||
|
output.print(" /fragments - List dehydrated context fragments (ACD)");
|
||||||
|
output.print(" /rehydrate - Restore a dehydrated fragment by ID");
|
||||||
|
output.print(" /resume - List and switch to a previous session");
|
||||||
|
output.print(" /dump - Dump entire context window to file for debugging");
|
||||||
|
output.print(" /readme - Reload README.md and AGENTS.md from disk");
|
||||||
|
output.print(" /stats - Show detailed context and performance statistics");
|
||||||
|
output.print(" /run <file> - Read file and execute as prompt");
|
||||||
|
output.print(" /help - Show this help message");
|
||||||
|
output.print(" exit/quit - Exit the interactive session");
|
||||||
|
output.print("");
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
"/compact" => {
|
||||||
|
output.print_g3_progress("compacting session");
|
||||||
|
match agent.force_compact().await {
|
||||||
|
Ok(true) => {
|
||||||
|
output.print_g3_status("compacting session", "done");
|
||||||
|
}
|
||||||
|
Ok(false) => {
|
||||||
|
output.print_g3_status("compacting session", "failed");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
output.print_g3_status("compacting session", &format!("error: {}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
"/thinnify" => {
|
||||||
|
let result = agent.force_thin();
|
||||||
|
G3Status::thin_result(&result);
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
"/skinnify" => {
|
||||||
|
let result = agent.force_thin_all();
|
||||||
|
G3Status::thin_result(&result);
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
"/fragments" => {
|
||||||
|
if let Some(session_id) = agent.get_session_id() {
|
||||||
|
match g3_core::acd::list_fragments(session_id) {
|
||||||
|
Ok(fragments) => {
|
||||||
|
if fragments.is_empty() {
|
||||||
|
output.print("No dehydrated fragments found for this session.");
|
||||||
|
} else {
|
||||||
|
output.print(&format!(
|
||||||
|
"📦 {} dehydrated fragment(s):\n",
|
||||||
|
fragments.len()
|
||||||
|
));
|
||||||
|
for fragment in &fragments {
|
||||||
|
output.print(&fragment.generate_stub());
|
||||||
|
output.print("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
output.print(&format!("❌ Error listing fragments: {}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
output.print("No active session - fragments are session-scoped.");
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
cmd if cmd.starts_with("/rehydrate") => {
|
||||||
|
let parts: Vec<&str> = cmd.splitn(2, ' ').collect();
|
||||||
|
if parts.len() < 2 || parts[1].trim().is_empty() {
|
||||||
|
output.print("Usage: /rehydrate <fragment_id>");
|
||||||
|
output.print("Use /fragments to list available fragment IDs.");
|
||||||
|
} else {
|
||||||
|
let fragment_id = parts[1].trim();
|
||||||
|
if let Some(session_id) = agent.get_session_id() {
|
||||||
|
match g3_core::acd::Fragment::load(session_id, fragment_id) {
|
||||||
|
Ok(fragment) => {
|
||||||
|
output.print(&format!(
|
||||||
|
"✅ Fragment '{}' loaded ({} messages, ~{} tokens)",
|
||||||
|
fragment_id, fragment.message_count, fragment.estimated_tokens
|
||||||
|
));
|
||||||
|
output.print("");
|
||||||
|
output.print(&fragment.generate_stub());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
output.print(&format!(
|
||||||
|
"❌ Failed to load fragment '{}': {}",
|
||||||
|
fragment_id, e
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
output.print("No active session - fragments are session-scoped.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
cmd if cmd.starts_with("/run") => {
|
||||||
|
let parts: Vec<&str> = cmd.splitn(2, ' ').collect();
|
||||||
|
if parts.len() < 2 || parts[1].trim().is_empty() {
|
||||||
|
output.print("Usage: /run <file-path>");
|
||||||
|
output.print("Reads the file and executes its content as a prompt.");
|
||||||
|
} else {
|
||||||
|
let file_path = parts[1].trim();
|
||||||
|
// Expand tilde
|
||||||
|
let expanded_path = if file_path.starts_with("~/") {
|
||||||
|
if let Some(home) = dirs::home_dir() {
|
||||||
|
home.join(&file_path[2..])
|
||||||
|
} else {
|
||||||
|
std::path::PathBuf::from(file_path)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
std::path::PathBuf::from(file_path)
|
||||||
|
};
|
||||||
|
match std::fs::read_to_string(&expanded_path) {
|
||||||
|
Ok(content) => {
|
||||||
|
let prompt = content.trim();
|
||||||
|
if prompt.is_empty() {
|
||||||
|
output.print("❌ File is empty.");
|
||||||
|
} else {
|
||||||
|
G3Status::progress(&format!("loading {}", file_path));
|
||||||
|
G3Status::done();
|
||||||
|
execute_task_with_retry(agent, prompt, show_prompt, show_code, output).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
output.print(&format!("❌ Failed to read file '{}': {}", file_path, e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
"/dump" => {
|
||||||
|
// Dump entire context window to a file for debugging
|
||||||
|
let dump_dir = std::path::Path::new("tmp");
|
||||||
|
if !dump_dir.exists() {
|
||||||
|
if let Err(e) = std::fs::create_dir_all(dump_dir) {
|
||||||
|
output.print(&format!("❌ Failed to create tmp directory: {}", e));
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
|
||||||
|
let dump_path = dump_dir.join(format!("context_dump_{}.txt", timestamp));
|
||||||
|
|
||||||
|
let context = agent.get_context_window();
|
||||||
|
let mut dump_content = String::new();
|
||||||
|
dump_content.push_str("# Context Window Dump\n");
|
||||||
|
dump_content.push_str(&format!("# Timestamp: {}\n", chrono::Utc::now()));
|
||||||
|
dump_content.push_str(&format!(
|
||||||
|
"# Messages: {}\n",
|
||||||
|
context.conversation_history.len()
|
||||||
|
));
|
||||||
|
dump_content.push_str(&format!(
|
||||||
|
"# Used tokens: {} / {} ({:.1}%)\n\n",
|
||||||
|
context.used_tokens,
|
||||||
|
context.total_tokens,
|
||||||
|
context.percentage_used()
|
||||||
|
));
|
||||||
|
|
||||||
|
for (i, msg) in context.conversation_history.iter().enumerate() {
|
||||||
|
dump_content.push_str(&format!("=== Message {} ===\n", i));
|
||||||
|
dump_content.push_str(&format!("Role: {:?}\n", msg.role));
|
||||||
|
dump_content.push_str(&format!("Kind: {:?}\n", msg.kind));
|
||||||
|
dump_content.push_str(&format!("Content ({} chars):\n", msg.content.len()));
|
||||||
|
dump_content.push_str(&msg.content);
|
||||||
|
dump_content.push_str("\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
match std::fs::write(&dump_path, &dump_content) {
|
||||||
|
Ok(_) => output.print(&format!("📄 Context dumped to: {}", dump_path.display())),
|
||||||
|
Err(e) => output.print(&format!("❌ Failed to write dump: {}", e)),
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
"/clear" => {
|
||||||
|
output.print("🧹 Clearing session...");
|
||||||
|
agent.clear_session();
|
||||||
|
output.print("✅ Session cleared. Starting fresh.");
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
"/readme" => {
|
||||||
|
output.print("📚 Reloading README.md and AGENTS.md...");
|
||||||
|
match agent.reload_readme() {
|
||||||
|
Ok(true) => {
|
||||||
|
output.print("✅ README content reloaded successfully")
|
||||||
|
}
|
||||||
|
Ok(false) => {
|
||||||
|
output.print("⚠️ No README was loaded at startup, cannot reload")
|
||||||
|
}
|
||||||
|
Err(e) => output.print(&format!("❌ Error reloading README: {}", e)),
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
"/stats" => {
|
||||||
|
let stats = agent.get_stats();
|
||||||
|
output.print(&stats);
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
"/resume" => {
|
||||||
|
output.print("📋 Scanning for available sessions...");
|
||||||
|
|
||||||
|
match g3_core::list_sessions_for_directory() {
|
||||||
|
Ok(sessions) => {
|
||||||
|
if sessions.is_empty() {
|
||||||
|
output.print("No sessions found for this directory.");
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current session ID to mark it
|
||||||
|
let current_session_id = agent.get_session_id().map(|s| s.to_string());
|
||||||
|
|
||||||
|
output.print("");
|
||||||
|
output.print("Available sessions:");
|
||||||
|
for (i, session) in sessions.iter().enumerate() {
|
||||||
|
let time_str = g3_core::format_session_time(&session.created_at);
|
||||||
|
let context_str = format!("{:.0}%", session.context_percentage);
|
||||||
|
let current_marker =
|
||||||
|
if current_session_id.as_deref() == Some(&session.session_id) {
|
||||||
|
" (current)"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
let todo_marker = if session.has_incomplete_todos() {
|
||||||
|
" 📝"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use description if available, otherwise fall back to session ID
|
||||||
|
let display_name = match &session.description {
|
||||||
|
Some(desc) => format!("'{}'", desc),
|
||||||
|
None => {
|
||||||
|
if session.session_id.len() > 40 {
|
||||||
|
format!("{}...", &session.session_id[..40])
|
||||||
|
} else {
|
||||||
|
session.session_id.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
output.print(&format!(
|
||||||
|
" {}. [{}] {} ({}){}{}\n",
|
||||||
|
i + 1,
|
||||||
|
time_str,
|
||||||
|
display_name,
|
||||||
|
context_str,
|
||||||
|
todo_marker,
|
||||||
|
current_marker
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
output.print_inline("\nSession number to resume (Enter to cancel): ");
|
||||||
|
// Read user selection
|
||||||
|
if let Ok(selection) = rl.readline("") {
|
||||||
|
let selection = selection.trim();
|
||||||
|
if selection.is_empty() {
|
||||||
|
output.print("Cancelled.");
|
||||||
|
} else if let Ok(num) = selection.parse::<usize>() {
|
||||||
|
if num >= 1 && num <= sessions.len() {
|
||||||
|
let selected = &sessions[num - 1];
|
||||||
|
match agent.switch_to_session(selected) {
|
||||||
|
Ok(true) => {
|
||||||
|
G3Status::resuming(&selected.session_id, Status::Done);
|
||||||
|
}
|
||||||
|
Ok(false) => {
|
||||||
|
G3Status::resuming_summary(&selected.session_id);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
G3Status::resuming(&selected.session_id, Status::Error(e.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
output.print("Invalid selection.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
output.print("Invalid input. Please enter a number.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => output.print(&format!("❌ Error listing sessions: {}", e)),
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
output.print(&format!(
|
||||||
|
"❌ Unknown command: {}. Type /help for available commands.",
|
||||||
|
input
|
||||||
|
));
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -509,7 +509,6 @@ mod tests {
|
|||||||
println!("Line: '{}', start: {}, completions: {}", line, start, completions.len());
|
println!("Line: '{}', start: {}, completions: {}", line, start, completions.len());
|
||||||
assert_eq!(completions.len(), 0, "Quoted non-path should not trigger completion");
|
assert_eq!(completions.len(), 0, "Quoted non-path should not trigger completion");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_resume_completion_lists_sessions() {
|
fn test_resume_completion_lists_sessions() {
|
||||||
@@ -561,3 +560,4 @@ mod tests {
|
|||||||
// The important thing is it doesn't panic
|
// The important thing is it doesn't panic
|
||||||
println!("list_sessions returned {} sessions", sessions.len());
|
println!("list_sessions returned {} sessions", sessions.len());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ impl LoadedContent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create with explicit include prompt filename.
|
/// Create with explicit include prompt filename.
|
||||||
|
#[allow(dead_code)] // Used in tests, may be useful for future callers
|
||||||
pub fn with_include_prompt_filename(mut self, filename: Option<String>) -> Self {
|
pub fn with_include_prompt_filename(mut self, filename: Option<String>) -> Self {
|
||||||
if self.include_prompt_filename.is_some() {
|
if self.include_prompt_filename.is_some() {
|
||||||
self.include_prompt_filename = filename;
|
self.include_prompt_filename = filename;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use tracing::{debug, error};
|
|||||||
use g3_core::ui_writer::UiWriter;
|
use g3_core::ui_writer::UiWriter;
|
||||||
use g3_core::Agent;
|
use g3_core::Agent;
|
||||||
|
|
||||||
|
use crate::commands::handle_command;
|
||||||
use crate::display::{LoadedContent, print_loaded_status, print_project_heading, print_workspace_path};
|
use crate::display::{LoadedContent, print_loaded_status, print_project_heading, print_workspace_path};
|
||||||
use crate::g3_status::{G3Status, Status};
|
use crate::g3_status::{G3Status, Status};
|
||||||
use crate::project_files::extract_readme_heading;
|
use crate::project_files::extract_readme_heading;
|
||||||
@@ -287,309 +288,3 @@ pub async fn run_interactive<W: UiWriter>(
|
|||||||
output.print("👋 Goodbye!");
|
output.print("👋 Goodbye!");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle a control command. Returns true if the command was handled and the loop should continue.
|
|
||||||
async fn handle_command<W: UiWriter>(
|
|
||||||
input: &str,
|
|
||||||
agent: &mut Agent<W>,
|
|
||||||
output: &SimpleOutput,
|
|
||||||
rl: &mut Editor<G3Helper, rustyline::history::DefaultHistory>,
|
|
||||||
show_prompt: bool,
|
|
||||||
show_code: bool,
|
|
||||||
) -> Result<bool> {
|
|
||||||
match input {
|
|
||||||
"/help" => {
|
|
||||||
output.print("");
|
|
||||||
output.print("📖 Control Commands:");
|
|
||||||
output.print(" /compact - Trigger compaction (compacts conversation history)");
|
|
||||||
output.print(" /thinnify - Trigger context thinning (replaces large tool results with file references)");
|
|
||||||
output.print(" /skinnify - Trigger full context thinning (like /thinnify but for entire context, not just first third)");
|
|
||||||
output.print(" /clear - Clear session and start fresh (discards continuation artifacts)");
|
|
||||||
output.print(" /fragments - List dehydrated context fragments (ACD)");
|
|
||||||
output.print(" /rehydrate - Restore a dehydrated fragment by ID");
|
|
||||||
output.print(" /resume - List and switch to a previous session");
|
|
||||||
output.print(" /dump - Dump entire context window to file for debugging");
|
|
||||||
output.print(" /readme - Reload README.md and AGENTS.md from disk");
|
|
||||||
output.print(" /stats - Show detailed context and performance statistics");
|
|
||||||
output.print(" /run <file> - Read file and execute as prompt");
|
|
||||||
output.print(" /help - Show this help message");
|
|
||||||
output.print(" exit/quit - Exit the interactive session");
|
|
||||||
output.print("");
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
"/compact" => {
|
|
||||||
output.print_g3_progress("compacting session");
|
|
||||||
match agent.force_compact().await {
|
|
||||||
Ok(true) => {
|
|
||||||
output.print_g3_status("compacting session", "done");
|
|
||||||
}
|
|
||||||
Ok(false) => {
|
|
||||||
output.print_g3_status("compacting session", "failed");
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
output.print_g3_status("compacting session", &format!("error: {}", e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
"/thinnify" => {
|
|
||||||
let result = agent.force_thin();
|
|
||||||
G3Status::thin_result(&result);
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
"/skinnify" => {
|
|
||||||
let result = agent.force_thin_all();
|
|
||||||
G3Status::thin_result(&result);
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
"/fragments" => {
|
|
||||||
if let Some(session_id) = agent.get_session_id() {
|
|
||||||
match g3_core::acd::list_fragments(session_id) {
|
|
||||||
Ok(fragments) => {
|
|
||||||
if fragments.is_empty() {
|
|
||||||
output.print("No dehydrated fragments found for this session.");
|
|
||||||
} else {
|
|
||||||
output.print(&format!(
|
|
||||||
"📦 {} dehydrated fragment(s):\n",
|
|
||||||
fragments.len()
|
|
||||||
));
|
|
||||||
for fragment in &fragments {
|
|
||||||
output.print(&fragment.generate_stub());
|
|
||||||
output.print("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
output.print(&format!("❌ Error listing fragments: {}", e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
output.print("No active session - fragments are session-scoped.");
|
|
||||||
}
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
cmd if cmd.starts_with("/rehydrate") => {
|
|
||||||
let parts: Vec<&str> = cmd.splitn(2, ' ').collect();
|
|
||||||
if parts.len() < 2 || parts[1].trim().is_empty() {
|
|
||||||
output.print("Usage: /rehydrate <fragment_id>");
|
|
||||||
output.print("Use /fragments to list available fragment IDs.");
|
|
||||||
} else {
|
|
||||||
let fragment_id = parts[1].trim();
|
|
||||||
if let Some(session_id) = agent.get_session_id() {
|
|
||||||
match g3_core::acd::Fragment::load(session_id, fragment_id) {
|
|
||||||
Ok(fragment) => {
|
|
||||||
output.print(&format!(
|
|
||||||
"✅ Fragment '{}' loaded ({} messages, ~{} tokens)",
|
|
||||||
fragment_id, fragment.message_count, fragment.estimated_tokens
|
|
||||||
));
|
|
||||||
output.print("");
|
|
||||||
output.print(&fragment.generate_stub());
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
output.print(&format!(
|
|
||||||
"❌ Failed to load fragment '{}': {}",
|
|
||||||
fragment_id, e
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
output.print("No active session - fragments are session-scoped.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
cmd if cmd.starts_with("/run") => {
|
|
||||||
let parts: Vec<&str> = cmd.splitn(2, ' ').collect();
|
|
||||||
if parts.len() < 2 || parts[1].trim().is_empty() {
|
|
||||||
output.print("Usage: /run <file-path>");
|
|
||||||
output.print("Reads the file and executes its content as a prompt.");
|
|
||||||
} else {
|
|
||||||
let file_path = parts[1].trim();
|
|
||||||
// Expand tilde
|
|
||||||
let expanded_path = if file_path.starts_with("~/") {
|
|
||||||
if let Some(home) = dirs::home_dir() {
|
|
||||||
home.join(&file_path[2..])
|
|
||||||
} else {
|
|
||||||
std::path::PathBuf::from(file_path)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
std::path::PathBuf::from(file_path)
|
|
||||||
};
|
|
||||||
match std::fs::read_to_string(&expanded_path) {
|
|
||||||
Ok(content) => {
|
|
||||||
let prompt = content.trim();
|
|
||||||
if prompt.is_empty() {
|
|
||||||
output.print("❌ File is empty.");
|
|
||||||
} else {
|
|
||||||
G3Status::progress(&format!("loading {}", file_path));
|
|
||||||
G3Status::done();
|
|
||||||
execute_task_with_retry(agent, prompt, show_prompt, show_code, output).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
output.print(&format!("❌ Failed to read file '{}': {}", file_path, e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
"/dump" => {
|
|
||||||
// Dump entire context window to a file for debugging
|
|
||||||
let dump_dir = std::path::Path::new("tmp");
|
|
||||||
if !dump_dir.exists() {
|
|
||||||
if let Err(e) = std::fs::create_dir_all(dump_dir) {
|
|
||||||
output.print(&format!("❌ Failed to create tmp directory: {}", e));
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
|
|
||||||
let dump_path = dump_dir.join(format!("context_dump_{}.txt", timestamp));
|
|
||||||
|
|
||||||
let context = agent.get_context_window();
|
|
||||||
let mut dump_content = String::new();
|
|
||||||
dump_content.push_str("# Context Window Dump\n");
|
|
||||||
dump_content.push_str(&format!("# Timestamp: {}\n", chrono::Utc::now()));
|
|
||||||
dump_content.push_str(&format!(
|
|
||||||
"# Messages: {}\n",
|
|
||||||
context.conversation_history.len()
|
|
||||||
));
|
|
||||||
dump_content.push_str(&format!(
|
|
||||||
"# Used tokens: {} / {} ({:.1}%)\n\n",
|
|
||||||
context.used_tokens,
|
|
||||||
context.total_tokens,
|
|
||||||
context.percentage_used()
|
|
||||||
));
|
|
||||||
|
|
||||||
for (i, msg) in context.conversation_history.iter().enumerate() {
|
|
||||||
dump_content.push_str(&format!("=== Message {} ===\n", i));
|
|
||||||
dump_content.push_str(&format!("Role: {:?}\n", msg.role));
|
|
||||||
dump_content.push_str(&format!("Kind: {:?}\n", msg.kind));
|
|
||||||
dump_content.push_str(&format!("Content ({} chars):\n", msg.content.len()));
|
|
||||||
dump_content.push_str(&msg.content);
|
|
||||||
dump_content.push_str("\n\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
match std::fs::write(&dump_path, &dump_content) {
|
|
||||||
Ok(_) => output.print(&format!("📄 Context dumped to: {}", dump_path.display())),
|
|
||||||
Err(e) => output.print(&format!("❌ Failed to write dump: {}", e)),
|
|
||||||
}
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
"/clear" => {
|
|
||||||
output.print("🧹 Clearing session...");
|
|
||||||
agent.clear_session();
|
|
||||||
output.print("✅ Session cleared. Starting fresh.");
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
"/readme" => {
|
|
||||||
output.print("📚 Reloading README.md and AGENTS.md...");
|
|
||||||
match agent.reload_readme() {
|
|
||||||
Ok(true) => {
|
|
||||||
output.print("✅ README content reloaded successfully")
|
|
||||||
}
|
|
||||||
Ok(false) => {
|
|
||||||
output.print("⚠️ No README was loaded at startup, cannot reload")
|
|
||||||
}
|
|
||||||
Err(e) => output.print(&format!("❌ Error reloading README: {}", e)),
|
|
||||||
}
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
"/stats" => {
|
|
||||||
let stats = agent.get_stats();
|
|
||||||
output.print(&stats);
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
"/resume" => {
|
|
||||||
output.print("📋 Scanning for available sessions...");
|
|
||||||
|
|
||||||
match g3_core::list_sessions_for_directory() {
|
|
||||||
Ok(sessions) => {
|
|
||||||
if sessions.is_empty() {
|
|
||||||
output.print("No sessions found for this directory.");
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get current session ID to mark it
|
|
||||||
let current_session_id = agent.get_session_id().map(|s| s.to_string());
|
|
||||||
|
|
||||||
output.print("");
|
|
||||||
output.print("Available sessions:");
|
|
||||||
for (i, session) in sessions.iter().enumerate() {
|
|
||||||
let time_str = g3_core::format_session_time(&session.created_at);
|
|
||||||
let context_str = format!("{:.0}%", session.context_percentage);
|
|
||||||
let current_marker =
|
|
||||||
if current_session_id.as_deref() == Some(&session.session_id) {
|
|
||||||
" (current)"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
};
|
|
||||||
let todo_marker = if session.has_incomplete_todos() {
|
|
||||||
" 📝"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
};
|
|
||||||
|
|
||||||
// Use description if available, otherwise fall back to session ID
|
|
||||||
let display_name = match &session.description {
|
|
||||||
Some(desc) => format!("'{}'", desc),
|
|
||||||
None => {
|
|
||||||
if session.session_id.len() > 40 {
|
|
||||||
format!("{}...", &session.session_id[..40])
|
|
||||||
} else {
|
|
||||||
session.session_id.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
output.print(&format!(
|
|
||||||
" {}. [{}] {} ({}){}{}\n",
|
|
||||||
i + 1,
|
|
||||||
time_str,
|
|
||||||
display_name,
|
|
||||||
context_str,
|
|
||||||
todo_marker,
|
|
||||||
current_marker
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
output.print_inline("\nSession number to resume (Enter to cancel): ");
|
|
||||||
// Read user selection
|
|
||||||
if let Ok(selection) = rl.readline("") {
|
|
||||||
let selection = selection.trim();
|
|
||||||
if selection.is_empty() {
|
|
||||||
output.print("Cancelled.");
|
|
||||||
} else if let Ok(num) = selection.parse::<usize>() {
|
|
||||||
if num >= 1 && num <= sessions.len() {
|
|
||||||
let selected = &sessions[num - 1];
|
|
||||||
match agent.switch_to_session(selected) {
|
|
||||||
Ok(true) => {
|
|
||||||
G3Status::resuming(&selected.session_id, Status::Done);
|
|
||||||
}
|
|
||||||
Ok(false) => {
|
|
||||||
G3Status::resuming_summary(&selected.session_id);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
G3Status::resuming(&selected.session_id, Status::Error(e.to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
output.print("Invalid selection.");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
output.print("Invalid input. Please enter a number.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => output.print(&format!("❌ Error listing sessions: {}", e)),
|
|
||||||
}
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
output.print(&format!(
|
|
||||||
"❌ Unknown command: {}. Type /help for available commands.",
|
|
||||||
input
|
|
||||||
));
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ mod agent_mode;
|
|||||||
mod autonomous;
|
mod autonomous;
|
||||||
mod cli_args;
|
mod cli_args;
|
||||||
mod coach_feedback;
|
mod coach_feedback;
|
||||||
|
mod commands;
|
||||||
mod display;
|
mod display;
|
||||||
mod interactive;
|
mod interactive;
|
||||||
mod simple_output;
|
mod simple_output;
|
||||||
|
|||||||
Reference in New Issue
Block a user