Merge sessions/interactive/3c2a09df

This commit is contained in:
Dhanji R. Prasanna
2026-01-30 14:20:12 +11:00
5 changed files with 290 additions and 16 deletions

View File

@@ -39,6 +39,8 @@ pub async fn handle_command<W: UiWriter>(
output.print(" /rehydrate - Restore a dehydrated fragment by ID");
output.print(" /resume - List and switch to a previous session");
output.print(" /research - List pending/completed research tasks");
output.print(" /research <id> - View a specific research report");
output.print(" /research latest - View the most recent research report");
output.print(" /project <path> - Load a project from the given absolute path");
output.print(" /unproject - Unload the current project and reset context");
output.print(" /dump - Dump entire context window to file for debugging");
@@ -131,13 +133,19 @@ pub async fn handle_command<W: UiWriter>(
}
Ok(true)
}
"/research" => {
cmd if cmd == "/research" || cmd.starts_with("/research ") => {
let manager = agent.get_pending_research_manager();
let all_tasks = manager.list_all();
if all_tasks.is_empty() {
// Parse argument: /research, /research latest, /research <id>
let arg = cmd.strip_prefix("/research").unwrap_or("").trim();
if arg.is_empty() {
// List all research tasks
let all_tasks = manager.list_all();
if all_tasks.is_empty() {
output.print("📋 No research tasks (pending or completed).");
} else {
} else {
output.print(&format!("📋 Research Tasks ({} total):\n", all_tasks.len()));
for task in all_tasks {
@@ -163,6 +171,54 @@ pub async fn handle_command<W: UiWriter>(
}
));
output.print("");
}
}
} else if arg == "latest" {
// Show the most recent research report
let all_tasks = manager.list_all();
// Find the most recent completed task (smallest elapsed time = most recent)
let latest = all_tasks.iter()
.filter(|t| t.status != g3_core::pending_research::ResearchStatus::Pending)
.min_by_key(|t| t.started_at.elapsed());
match latest {
Some(task) => {
output.print(&format!("📋 Research Report: `{}`\n", task.id));
output.print(&format!("Query: {}\n", task.query));
output.print(&format!("Status: {} | Elapsed: {}\n", task.status, task.elapsed_display()));
output.print(&"".repeat(60));
if let Some(ref result) = task.result {
output.print(result);
} else {
output.print("(No report content available)");
}
}
None => {
output.print("📋 No completed research tasks yet.");
}
}
} else {
// View a specific research report by ID
let task_id = arg.to_string();
match manager.get(&task_id) {
Some(task) => {
output.print(&format!("📋 Research Report: `{}`\n", task.id));
output.print(&format!("Query: {}\n", task.query));
output.print(&format!("Status: {} | Elapsed: {}\n", task.status, task.elapsed_display()));
output.print(&"".repeat(60));
if let Some(ref result) = task.result {
output.print(result);
} else if task.status == g3_core::pending_research::ResearchStatus::Pending {
output.print("(Research still in progress...)");
} else {
output.print("(No report content available)");
}
}
None => {
output.print(&format!("❓ No research task found with id: `{}`", task_id));
}
}
}
Ok(true)

View File

@@ -284,6 +284,24 @@ impl G3Status {
);
Self::done();
}
/// Print research completion notification: "g3: N research report(s) ... [done/failed]"
///
/// Used for real-time notification when background research completes.
pub fn research_complete(count: usize, all_succeeded: bool) {
let report_word = if count == 1 { "report" } else { "reports" };
print!(
"{} {} research {} ...",
Self::format_prefix(),
count,
report_word
);
if all_succeeded {
Self::done();
} else {
Self::failed();
}
}
}
#[cfg(test)]

View File

@@ -6,7 +6,10 @@ use rustyline::error::ReadlineError;
use rustyline::{Config, Editor};
use crate::completion::G3Helper;
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use tracing::{debug, error};
use tokio::sync::broadcast;
use g3_core::ui_writer::UiWriter;
use g3_core::Agent;
@@ -67,6 +70,57 @@ async fn execute_user_input<W: UiWriter>(
}
}
/// Spawn a background task to handle research completion notifications.
///
/// This task listens for research completions and prints status messages in real-time.
/// When g3 is idle (waiting for input), it reprints the prompt after the notification.
/// When g3 is busy (processing), it just prints the notification (interleaving is fine).
///
/// Returns a handle to the spawned task and an `is_busy` flag that should be set
/// to true while the agent is processing and false when waiting for input.
fn spawn_research_notification_handler(
mut rx: broadcast::Receiver<g3_core::ResearchCompletionNotification>,
is_busy: Arc<AtomicBool>,
prompt: Arc<std::sync::RwLock<String>>,
) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move {
loop {
match rx.recv().await {
Ok(notification) => {
use std::io::Write;
let succeeded = notification.status == g3_core::ResearchStatus::Complete;
// Print the completion notification
// If we're idle (at prompt), we need to print on a new line first
let busy = is_busy.load(Ordering::SeqCst);
if !busy {
// Clear the current line (prompt) and move to start
print!("\r\x1b[K");
}
G3Status::research_complete(1, succeeded);
// If we're idle, reprint the prompt
if !busy {
let prompt_str = prompt.read().unwrap().clone();
print!("{}", prompt_str);
let _ = std::io::stdout().flush();
}
}
Err(broadcast::error::RecvError::Closed) => {
// Channel closed, exit the task
break;
}
Err(broadcast::error::RecvError::Lagged(_)) => {
// Missed some messages, continue
continue;
}
}
}
})
}
/// Run interactive mode with console output.
/// If `agent_name` is Some, we're in agent+chat mode: skip session resume/verbose welcome,
/// and use the agent name as the prompt (e.g., "butler>").
@@ -186,6 +240,16 @@ pub async fn run_interactive<W: UiWriter>(
let _ = rl.load_history(history_path);
}
// Enable research completion notifications for real-time updates
let research_rx = agent.enable_research_notifications();
let is_busy = Arc::new(AtomicBool::new(false));
let current_prompt = Arc::new(std::sync::RwLock::new(String::new()));
let _notification_handle = spawn_research_notification_handler(
research_rx,
is_busy.clone(),
current_prompt.clone(),
);
// Track multiline input
let mut multiline_buffer = String::new();
let mut in_multiline = false;
@@ -221,6 +285,10 @@ pub async fn run_interactive<W: UiWriter>(
// Build prompt
let prompt = build_prompt(in_multiline, agent_name, &active_project);
// Update the shared prompt for the notification handler
*current_prompt.write().unwrap() = prompt.clone();
is_busy.store(false, Ordering::SeqCst);
let readline = rl.readline(&prompt);
match readline {
@@ -258,9 +326,11 @@ pub async fn run_interactive<W: UiWriter>(
// Reprint input with formatting
reprint_formatted_input(&input, &prompt);
is_busy.store(true, Ordering::SeqCst);
execute_user_input(
&mut agent, &input, show_prompt, show_code, &output, from_agent_mode
).await;
is_busy.store(false, Ordering::SeqCst);
} else {
// Single line input
let input = line.trim().to_string();
@@ -278,7 +348,11 @@ pub async fn run_interactive<W: UiWriter>(
// Check for control commands
if input.starts_with('/') {
if handle_command(&input, &mut agent, workspace_path, &output, &mut active_project, &mut rl, show_prompt, show_code).await? {
is_busy.store(true, Ordering::SeqCst);
let handled = handle_command(&input, &mut agent, workspace_path, &output, &mut active_project, &mut rl, show_prompt, show_code).await?;
is_busy.store(false, Ordering::SeqCst);
if handled {
continue;
}
}
@@ -286,9 +360,11 @@ pub async fn run_interactive<W: UiWriter>(
// Reprint input with formatting
reprint_formatted_input(&input, &prompt);
is_busy.store(true, Ordering::SeqCst);
execute_user_input(
&mut agent, &input, show_prompt, show_code, &output, from_agent_mode
).await;
is_busy.store(false, Ordering::SeqCst);
}
}
Err(ReadlineError::Interrupted) => {