--machine mode flag for verbose CLI output
This commit is contained in:
@@ -167,14 +167,12 @@ use tokio_util::sync::CancellationToken;
|
||||
use tracing::{error, info};
|
||||
|
||||
use g3_core::error_handling::{classify_error, ErrorType, RecoverableError};
|
||||
mod retro_tui;
|
||||
mod theme;
|
||||
pub mod tui;
|
||||
mod ui_writer_impl;
|
||||
use retro_tui::RetroTui;
|
||||
use theme::ColorTheme;
|
||||
use tui::SimpleOutput;
|
||||
use ui_writer_impl::{ConsoleUiWriter, RetroTuiWriter};
|
||||
mod simple_output;
|
||||
use simple_output::SimpleOutput;
|
||||
mod machine_ui_writer;
|
||||
use machine_ui_writer::MachineUiWriter;
|
||||
use ui_writer_impl::ConsoleUiWriter;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "g3")]
|
||||
@@ -220,13 +218,9 @@ pub struct Cli {
|
||||
#[arg(long)]
|
||||
pub interactive_requirements: bool,
|
||||
|
||||
/// Use retro terminal UI (inspired by 80s sci-fi)
|
||||
/// Enable machine-friendly output mode with JSON markers and stats
|
||||
#[arg(long)]
|
||||
pub retro: bool,
|
||||
|
||||
/// Color theme for retro mode (default, dracula, or path to theme file)
|
||||
#[arg(long, value_name = "THEME")]
|
||||
pub theme: Option<String>,
|
||||
pub machine: bool,
|
||||
|
||||
/// Override the configured provider (anthropic, databricks, embedded, openai)
|
||||
#[arg(long, value_name = "PROVIDER")]
|
||||
@@ -253,7 +247,7 @@ pub async fn run() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
// Only initialize logging if not in retro mode
|
||||
if !cli.retro {
|
||||
if !cli.machine {
|
||||
// Initialize logging with filtering
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
||||
|
||||
@@ -291,16 +285,16 @@ pub async fn run() -> Result<()> {
|
||||
tracing_subscriber::registry().with(filter).init();
|
||||
}
|
||||
|
||||
if !cli.retro {
|
||||
if !cli.machine {
|
||||
info!("Starting G3 AI Coding Agent");
|
||||
}
|
||||
|
||||
// Set up workspace directory
|
||||
let workspace_dir = if let Some(ws) = cli.workspace {
|
||||
ws
|
||||
let workspace_dir = if let Some(ws) = &cli.workspace {
|
||||
ws.clone()
|
||||
} else if cli.autonomous {
|
||||
// For autonomous mode, use G3_WORKSPACE env var or default
|
||||
setup_workspace_directory()?
|
||||
setup_workspace_directory(cli.machine)?
|
||||
} else {
|
||||
// Default to current directory for interactive/single-shot mode
|
||||
std::env::current_dir()?
|
||||
@@ -421,9 +415,9 @@ Output ONLY the markdown content, no explanations or meta-commentary."#,
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(requirements_text) = cli.requirements {
|
||||
if let Some(requirements_text) = &cli.requirements {
|
||||
// Use requirements text override
|
||||
Project::new_autonomous_with_requirements(workspace_dir.clone(), requirements_text)?
|
||||
Project::new_autonomous_with_requirements(workspace_dir.clone(), requirements_text.clone())?
|
||||
} else {
|
||||
// Use traditional requirements.md file
|
||||
Project::new_autonomous(workspace_dir.clone())?
|
||||
@@ -436,7 +430,7 @@ Output ONLY the markdown content, no explanations or meta-commentary."#,
|
||||
project.ensure_workspace_exists()?;
|
||||
project.enter_workspace()?;
|
||||
|
||||
if !cli.retro {
|
||||
if !cli.machine {
|
||||
info!("Using workspace: {}", project.workspace().display());
|
||||
}
|
||||
|
||||
@@ -450,7 +444,7 @@ Output ONLY the markdown content, no explanations or meta-commentary."#,
|
||||
// Apply macax flag override
|
||||
if cli.macax {
|
||||
config.macax.enabled = true;
|
||||
if !cli.retro {
|
||||
if !cli.machine {
|
||||
info!("macOS Accessibility API tools enabled");
|
||||
}
|
||||
}
|
||||
@@ -473,7 +467,7 @@ Output ONLY the markdown content, no explanations or meta-commentary."#,
|
||||
}
|
||||
|
||||
// Initialize agent
|
||||
let ui_writer = ConsoleUiWriter::new();
|
||||
// ui_writer will be created conditionally based on machine mode
|
||||
|
||||
// Combine AGENTS.md and README content if both exist
|
||||
let combined_content = match (agents_content.clone(), readme_content.clone()) {
|
||||
@@ -485,28 +479,117 @@ Output ONLY the markdown content, no explanations or meta-commentary."#,
|
||||
(None, None) => None,
|
||||
};
|
||||
|
||||
let mut agent = if cli.autonomous {
|
||||
Agent::new_autonomous_with_readme_and_quiet(
|
||||
config.clone(),
|
||||
ui_writer,
|
||||
combined_content.clone(),
|
||||
cli.quiet,
|
||||
)
|
||||
.await?
|
||||
// Execute task, autonomous mode, or start interactive mode based on machine mode
|
||||
if cli.machine {
|
||||
// Machine mode - use MachineUiWriter
|
||||
let ui_writer = MachineUiWriter::new();
|
||||
|
||||
let agent = if cli.autonomous {
|
||||
Agent::new_autonomous_with_readme_and_quiet(
|
||||
config.clone(),
|
||||
ui_writer,
|
||||
combined_content.clone(),
|
||||
cli.quiet,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
Agent::new_with_readme_and_quiet(
|
||||
config.clone(),
|
||||
ui_writer,
|
||||
combined_content.clone(),
|
||||
cli.quiet,
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
run_with_machine_mode(agent, cli, project).await?;
|
||||
} else {
|
||||
Agent::new_with_readme_and_quiet(
|
||||
config.clone(),
|
||||
ui_writer,
|
||||
combined_content.clone(),
|
||||
cli.quiet,
|
||||
)
|
||||
.await?
|
||||
// Normal mode - use ConsoleUiWriter
|
||||
let ui_writer = ConsoleUiWriter::new();
|
||||
|
||||
let agent = if cli.autonomous {
|
||||
Agent::new_autonomous_with_readme_and_quiet(
|
||||
config.clone(),
|
||||
ui_writer,
|
||||
combined_content.clone(),
|
||||
cli.quiet,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
Agent::new_with_readme_and_quiet(
|
||||
config.clone(),
|
||||
ui_writer,
|
||||
combined_content.clone(),
|
||||
cli.quiet,
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
run_with_console_mode(agent, cli, project, combined_content).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Simplified machine mode version of autonomous mode
|
||||
async fn run_autonomous_machine(
|
||||
mut agent: Agent<MachineUiWriter>,
|
||||
project: Project,
|
||||
show_prompt: bool,
|
||||
show_code: bool,
|
||||
max_turns: usize,
|
||||
_quiet: bool,
|
||||
) -> Result<()> {
|
||||
println!("AUTONOMOUS_MODE_STARTED");
|
||||
println!("WORKSPACE: {}", project.workspace().display());
|
||||
println!("MAX_TURNS: {}", max_turns);
|
||||
|
||||
// Check if requirements exist
|
||||
if !project.has_requirements() {
|
||||
println!("ERROR: requirements.md not found in workspace directory");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Read requirements
|
||||
let requirements = match project.read_requirements()? {
|
||||
Some(content) => content,
|
||||
None => {
|
||||
println!("ERROR: Could not read requirements");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
println!("REQUIREMENTS_LOADED");
|
||||
|
||||
// For now, just execute a simple autonomous loop
|
||||
// This is a simplified version - full implementation would need coach-player loop
|
||||
let task = format!(
|
||||
"You are G3 in implementation mode. Read and implement the following requirements:\n\n{}\n\nImplement this step by step, creating all necessary files and code.",
|
||||
requirements
|
||||
);
|
||||
|
||||
println!("TASK_START");
|
||||
let result = agent.execute_task_with_timing(&task, None, false, show_prompt, show_code, true).await?;
|
||||
println!("AGENT_RESPONSE:");
|
||||
println!("{}", result.response);
|
||||
println!("END_AGENT_RESPONSE");
|
||||
println!("TASK_END");
|
||||
|
||||
println!("AUTONOMOUS_MODE_ENDED");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_with_console_mode(
|
||||
mut agent: Agent<ConsoleUiWriter>,
|
||||
cli: Cli,
|
||||
project: Project,
|
||||
combined_content: Option<String>,
|
||||
) -> Result<()> {
|
||||
|
||||
// Execute task, autonomous mode, or start interactive mode
|
||||
if cli.autonomous {
|
||||
// Autonomous mode with coach-player feedback loop
|
||||
if !cli.retro {
|
||||
if !cli.machine {
|
||||
info!("Starting autonomous mode");
|
||||
}
|
||||
run_autonomous(
|
||||
@@ -520,7 +603,7 @@ Output ONLY the markdown content, no explanations or meta-commentary."#,
|
||||
.await?;
|
||||
} else if let Some(task) = cli.task {
|
||||
// Single-shot mode
|
||||
if !cli.retro {
|
||||
if !cli.machine {
|
||||
info!("Executing task: {}", task);
|
||||
}
|
||||
let output = SimpleOutput::new();
|
||||
@@ -530,26 +613,43 @@ Output ONLY the markdown content, no explanations or meta-commentary."#,
|
||||
output.print_smart(&result.response);
|
||||
} else {
|
||||
// Interactive mode (default)
|
||||
if !cli.retro {
|
||||
if !cli.machine {
|
||||
info!("Starting interactive mode");
|
||||
}
|
||||
println!("📁 Workspace: {}", project.workspace().display());
|
||||
run_interactive(agent, cli.show_prompt, cli.show_code, combined_content).await?;
|
||||
}
|
||||
|
||||
if cli.retro {
|
||||
// Use retro terminal UI
|
||||
run_interactive_retro(
|
||||
config, // Already has overrides applied
|
||||
cli.show_prompt,
|
||||
cli.show_code,
|
||||
cli.theme,
|
||||
combined_content,
|
||||
)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_with_machine_mode(
|
||||
mut agent: Agent<MachineUiWriter>,
|
||||
cli: Cli,
|
||||
project: Project,
|
||||
) -> Result<()> {
|
||||
if cli.autonomous {
|
||||
// Autonomous mode with coach-player feedback loop
|
||||
run_autonomous_machine(
|
||||
agent,
|
||||
project,
|
||||
cli.show_prompt,
|
||||
cli.show_code,
|
||||
cli.max_turns,
|
||||
cli.quiet,
|
||||
)
|
||||
.await?;
|
||||
} else if let Some(task) = cli.task {
|
||||
// Single-shot mode
|
||||
let result = agent
|
||||
.execute_task_with_timing(&task, None, false, cli.show_prompt, cli.show_code, true)
|
||||
.await?;
|
||||
} else {
|
||||
// Use standard terminal UI
|
||||
let output = SimpleOutput::new();
|
||||
output.print(&format!("📁 Workspace: {}", project.workspace().display()));
|
||||
run_interactive(agent, cli.show_prompt, cli.show_code, combined_content).await?;
|
||||
}
|
||||
println!("AGENT_RESPONSE:");
|
||||
println!("{}", result.response);
|
||||
println!("END_AGENT_RESPONSE");
|
||||
} else {
|
||||
// Interactive mode
|
||||
run_interactive_machine(agent, cli.show_prompt, cli.show_code).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -691,274 +791,6 @@ fn extract_readme_heading(readme_content: &str) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
async fn run_interactive_retro(
|
||||
config: Config,
|
||||
show_prompt: bool,
|
||||
show_code: bool,
|
||||
theme_name: Option<String>,
|
||||
combined_content: Option<String>,
|
||||
) -> Result<()> {
|
||||
use crossterm::event::{self, Event, KeyCode, KeyModifiers};
|
||||
use std::time::Duration;
|
||||
|
||||
// Set environment variable to suppress println in other crates
|
||||
std::env::set_var("G3_RETRO_MODE", "1");
|
||||
|
||||
// Load the color theme
|
||||
let theme = match ColorTheme::load(theme_name.as_deref()) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to load theme: {}. Using default.", e);
|
||||
ColorTheme::default()
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize the retro terminal UI
|
||||
let tui = RetroTui::start(theme).await?;
|
||||
|
||||
// Create agent with RetroTuiWriter
|
||||
let ui_writer = RetroTuiWriter::new(tui.clone());
|
||||
let mut agent = Agent::new_with_readme_and_quiet(config, ui_writer, combined_content.clone(), false).await?;
|
||||
|
||||
// Display initial system messages
|
||||
tui.output("SYSTEM: AGENT ONLINE\n\n");
|
||||
|
||||
// Display message if AGENTS.md or README was loaded
|
||||
if let Some(ref content) = combined_content {
|
||||
// Check what was loaded
|
||||
let has_agents = content.contains("Agent Configuration");
|
||||
let has_readme = content.contains("Project README");
|
||||
|
||||
if has_agents {
|
||||
tui.output("SYSTEM: AGENT CONFIGURATION LOADED\n\n");
|
||||
}
|
||||
|
||||
if has_readme {
|
||||
// Extract the first heading or title from the README
|
||||
let readme_snippet = extract_readme_heading(content)
|
||||
.unwrap_or_else(|| "PROJECT DOCUMENTATION LOADED".to_string());
|
||||
|
||||
tui.output(&format!(
|
||||
"SYSTEM: PROJECT README LOADED - {}\n\n",
|
||||
readme_snippet
|
||||
));
|
||||
}
|
||||
}
|
||||
tui.output("SYSTEM: READY FOR INPUT\n\n");
|
||||
tui.output("\n\n");
|
||||
|
||||
// Display provider and model information
|
||||
match agent.get_provider_info() {
|
||||
Ok((provider, model)) => {
|
||||
tui.update_provider_info(&provider, &model);
|
||||
}
|
||||
Err(e) => {
|
||||
tui.update_provider_info("ERROR", &e.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Track multiline input
|
||||
let mut multiline_buffer = String::new();
|
||||
let mut in_multiline = false;
|
||||
|
||||
// Main event loop
|
||||
loop {
|
||||
// Update context window display
|
||||
let context = agent.get_context_window();
|
||||
tui.update_context(
|
||||
context.used_tokens,
|
||||
context.total_tokens,
|
||||
context.percentage_used(),
|
||||
);
|
||||
|
||||
// Poll for keyboard events
|
||||
if event::poll(Duration::from_millis(50))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
tui.exit();
|
||||
break;
|
||||
}
|
||||
KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
tui.exit();
|
||||
break;
|
||||
}
|
||||
// Emacs/bash-like shortcuts
|
||||
KeyCode::Char('a') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
tui.cursor_home();
|
||||
}
|
||||
KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
tui.cursor_end();
|
||||
}
|
||||
KeyCode::Char('w') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
tui.delete_word();
|
||||
}
|
||||
KeyCode::Char('k') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
tui.delete_to_end();
|
||||
}
|
||||
KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
// Delete from beginning to cursor (similar to Ctrl-K but opposite direction)
|
||||
let (input_buffer, cursor_pos) = tui.get_input_state();
|
||||
if cursor_pos > 0 {
|
||||
let after = input_buffer.chars().skip(cursor_pos).collect::<String>();
|
||||
tui.update_input(&after);
|
||||
tui.cursor_home();
|
||||
}
|
||||
}
|
||||
KeyCode::Left => {
|
||||
tui.cursor_left();
|
||||
}
|
||||
KeyCode::Right => {
|
||||
tui.cursor_right();
|
||||
}
|
||||
KeyCode::Home if !key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
tui.cursor_home();
|
||||
}
|
||||
KeyCode::End if !key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
tui.cursor_end();
|
||||
}
|
||||
KeyCode::Delete => {
|
||||
tui.delete_char();
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
let (input_buffer, _) = tui.get_input_state();
|
||||
if !input_buffer.is_empty() {
|
||||
// Clear the input for next command
|
||||
tui.update_input("");
|
||||
let trimmed = input_buffer.trim_end();
|
||||
|
||||
// Check if line ends with backslash for continuation
|
||||
if let Some(without_backslash) = trimmed.strip_suffix('\\') {
|
||||
// Remove the backslash and add to buffer
|
||||
multiline_buffer.push_str(without_backslash);
|
||||
multiline_buffer.push('\n');
|
||||
in_multiline = true;
|
||||
tui.status("MULTILINE INPUT");
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we're in multiline mode and no backslash, this is the final line
|
||||
let final_input = if in_multiline {
|
||||
multiline_buffer.push_str(&input_buffer);
|
||||
in_multiline = false;
|
||||
let result = multiline_buffer.clone();
|
||||
multiline_buffer.clear();
|
||||
tui.status("READY");
|
||||
result
|
||||
} else {
|
||||
input_buffer.clone()
|
||||
};
|
||||
|
||||
let input = final_input.trim().to_string();
|
||||
if input.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if input == "exit" || input == "quit" {
|
||||
tui.exit();
|
||||
break;
|
||||
}
|
||||
|
||||
// Execute the task
|
||||
tui.output(&format!("> {}", input));
|
||||
tui.status("PROCESSING");
|
||||
|
||||
const MAX_TIMEOUT_RETRIES: u32 = 3;
|
||||
let mut attempt = 0;
|
||||
|
||||
loop {
|
||||
attempt += 1;
|
||||
|
||||
match agent
|
||||
.execute_task_with_timing(
|
||||
&input,
|
||||
None,
|
||||
false,
|
||||
show_prompt,
|
||||
show_code,
|
||||
true,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
if attempt > 1 {
|
||||
tui.output(&format!(
|
||||
"SYSTEM: REQUEST SUCCEEDED AFTER {} ATTEMPTS",
|
||||
attempt
|
||||
));
|
||||
}
|
||||
tui.output(&result.response);
|
||||
tui.status("READY");
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
// Check if this is a timeout error that we should retry
|
||||
let error_type = classify_error(&e);
|
||||
|
||||
if matches!(
|
||||
error_type,
|
||||
ErrorType::Recoverable(RecoverableError::Timeout)
|
||||
) && attempt < MAX_TIMEOUT_RETRIES
|
||||
{
|
||||
// Calculate retry delay with exponential backoff
|
||||
let delay_ms = 1000 * (2_u64.pow(attempt - 1));
|
||||
let delay = std::time::Duration::from_millis(delay_ms);
|
||||
|
||||
tui.output(&format!("SYSTEM: TIMEOUT ERROR (ATTEMPT {}/{}). RETRYING IN {:?}...",
|
||||
attempt, MAX_TIMEOUT_RETRIES, delay));
|
||||
tui.status("RETRYING");
|
||||
|
||||
// Wait before retrying
|
||||
tokio::time::sleep(delay).await;
|
||||
continue;
|
||||
}
|
||||
|
||||
// For non-timeout errors or after max retries
|
||||
tui.error(&format!("Task execution failed: {}", e));
|
||||
tui.status("ERROR");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
tui.insert_char(c);
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
tui.backspace();
|
||||
}
|
||||
KeyCode::Up => {
|
||||
tui.scroll_up();
|
||||
}
|
||||
KeyCode::Down => {
|
||||
tui.scroll_down();
|
||||
}
|
||||
KeyCode::PageUp => {
|
||||
tui.scroll_page_up();
|
||||
}
|
||||
KeyCode::PageDown => {
|
||||
tui.scroll_page_down();
|
||||
}
|
||||
KeyCode::Home if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
tui.scroll_home(); // Ctrl+Home for scrolling to top
|
||||
}
|
||||
KeyCode::End if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
tui.scroll_end(); // Ctrl+End for scrolling to bottom
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Small delay to prevent CPU spinning
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
|
||||
tui.output("SYSTEM: SHUTDOWN INITIATED");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_interactive<W: UiWriter>(
|
||||
mut agent: Agent<W>,
|
||||
show_prompt: bool,
|
||||
@@ -1109,7 +941,7 @@ async fn run_interactive<W: UiWriter>(
|
||||
}
|
||||
"/thinnify" => {
|
||||
let summary = agent.force_thin();
|
||||
output.print_context_thinning(&summary);
|
||||
println!("{}", summary);
|
||||
continue;
|
||||
}
|
||||
"/readme" => {
|
||||
@@ -1247,6 +1079,178 @@ async fn execute_task<W: UiWriter>(
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_interactive_machine(
|
||||
mut agent: Agent<MachineUiWriter>,
|
||||
show_prompt: bool,
|
||||
show_code: bool,
|
||||
) -> Result<()> {
|
||||
println!("INTERACTIVE_MODE_STARTED");
|
||||
|
||||
// Display provider and model information
|
||||
match agent.get_provider_info() {
|
||||
Ok((provider, model)) => {
|
||||
println!("PROVIDER: {}", provider);
|
||||
println!("MODEL: {}", model);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("ERROR: Failed to get provider info: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize rustyline editor with history
|
||||
let mut rl = DefaultEditor::new()?;
|
||||
|
||||
// Try to load history from a file in the user's home directory
|
||||
let history_file = dirs::home_dir().map(|mut path| {
|
||||
path.push(".g3_history");
|
||||
path
|
||||
});
|
||||
|
||||
if let Some(ref history_path) = history_file {
|
||||
let _ = rl.load_history(history_path);
|
||||
}
|
||||
|
||||
loop {
|
||||
let readline = rl.readline("");
|
||||
match readline {
|
||||
Ok(line) => {
|
||||
let input = line.trim().to_string();
|
||||
|
||||
if input.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if input == "exit" || input == "quit" {
|
||||
break;
|
||||
}
|
||||
|
||||
// Add to history
|
||||
rl.add_history_entry(&input)?;
|
||||
|
||||
// Check for control commands
|
||||
if input.starts_with('/') {
|
||||
match input.as_str() {
|
||||
"/compact" => {
|
||||
println!("COMMAND: compact");
|
||||
match agent.force_summarize().await {
|
||||
Ok(true) => println!("RESULT: Summarization completed"),
|
||||
Ok(false) => println!("RESULT: Summarization failed"),
|
||||
Err(e) => println!("ERROR: {}", e),
|
||||
}
|
||||
continue;
|
||||
}
|
||||
"/thinnify" => {
|
||||
println!("COMMAND: thinnify");
|
||||
let summary = agent.force_thin();
|
||||
println!("{}", summary);
|
||||
continue;
|
||||
}
|
||||
_ => {
|
||||
println!("ERROR: Unknown command: {}", input);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Execute task
|
||||
println!("TASK_START");
|
||||
execute_task_machine(&mut agent, &input, show_prompt, show_code).await;
|
||||
println!("TASK_END");
|
||||
}
|
||||
Err(ReadlineError::Interrupted) => continue,
|
||||
Err(ReadlineError::Eof) => break,
|
||||
Err(err) => {
|
||||
println!("ERROR: {:?}", err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save history before exiting
|
||||
if let Some(ref history_path) = history_file {
|
||||
let _ = rl.save_history(history_path);
|
||||
}
|
||||
|
||||
println!("INTERACTIVE_MODE_ENDED");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn execute_task_machine(
|
||||
agent: &mut Agent<MachineUiWriter>,
|
||||
input: &str,
|
||||
show_prompt: bool,
|
||||
show_code: bool,
|
||||
) {
|
||||
const MAX_TIMEOUT_RETRIES: u32 = 3;
|
||||
let mut attempt = 0;
|
||||
|
||||
// Create cancellation token for this request
|
||||
let cancellation_token = CancellationToken::new();
|
||||
let cancel_token_clone = cancellation_token.clone();
|
||||
|
||||
loop {
|
||||
attempt += 1;
|
||||
|
||||
// Execute task with cancellation support
|
||||
let execution_result = tokio::select! {
|
||||
result = agent.execute_task_with_timing_cancellable(
|
||||
input, None, false, show_prompt, show_code, true, cancellation_token.clone()
|
||||
) => {
|
||||
result
|
||||
}
|
||||
_ = tokio::signal::ctrl_c() => {
|
||||
cancel_token_clone.cancel();
|
||||
println!("CANCELLED");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match execution_result {
|
||||
Ok(result) => {
|
||||
if attempt > 1 {
|
||||
println!("RETRY_SUCCESS: attempt {}", attempt);
|
||||
}
|
||||
println!("AGENT_RESPONSE:");
|
||||
println!("{}", result.response);
|
||||
println!("END_AGENT_RESPONSE");
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
if e.to_string().contains("cancelled") {
|
||||
println!("CANCELLED");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a timeout error that we should retry
|
||||
let error_type = classify_error(&e);
|
||||
|
||||
if matches!(
|
||||
error_type,
|
||||
ErrorType::Recoverable(RecoverableError::Timeout)
|
||||
) && attempt < MAX_TIMEOUT_RETRIES
|
||||
{
|
||||
// Calculate retry delay with exponential backoff
|
||||
let delay_ms = 1000 * (2_u64.pow(attempt - 1));
|
||||
let delay = std::time::Duration::from_millis(delay_ms);
|
||||
|
||||
println!("TIMEOUT: attempt {} of {}, retrying in {:?}", attempt, MAX_TIMEOUT_RETRIES, delay);
|
||||
|
||||
// Wait before retrying
|
||||
tokio::time::sleep(delay).await;
|
||||
continue;
|
||||
}
|
||||
|
||||
// For non-timeout errors or after max retries
|
||||
println!("ERROR: {}", e);
|
||||
if attempt > 1 {
|
||||
println!("FAILED_AFTER_RETRIES: {}", attempt);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_execution_error(e: &anyhow::Error, input: &str, output: &SimpleOutput, attempt: u32) {
|
||||
// Enhanced error logging with detailed information
|
||||
error!("=== TASK EXECUTION ERROR ===");
|
||||
@@ -1280,16 +1284,13 @@ fn handle_execution_error(e: &anyhow::Error, input: &str, output: &SimpleOutput,
|
||||
|
||||
fn display_context_progress<W: UiWriter>(agent: &Agent<W>, output: &SimpleOutput) {
|
||||
let context = agent.get_context_window();
|
||||
output.print_context(
|
||||
context.used_tokens,
|
||||
context.total_tokens,
|
||||
context.percentage_used(),
|
||||
);
|
||||
output.print(&format!("Context: {}/{} tokens ({:.1}%)",
|
||||
context.used_tokens, context.total_tokens, context.percentage_used()));
|
||||
}
|
||||
|
||||
/// Set up the workspace directory for autonomous mode
|
||||
/// Uses G3_WORKSPACE environment variable or defaults to ~/tmp/workspace
|
||||
fn setup_workspace_directory() -> Result<PathBuf> {
|
||||
fn setup_workspace_directory(machine_mode: bool) -> Result<PathBuf> {
|
||||
let workspace_dir = if let Ok(env_workspace) = std::env::var("G3_WORKSPACE") {
|
||||
PathBuf::from(env_workspace)
|
||||
} else {
|
||||
@@ -1302,7 +1303,7 @@ fn setup_workspace_directory() -> Result<PathBuf> {
|
||||
// Create the directory if it doesn't exist
|
||||
if !workspace_dir.exists() {
|
||||
std::fs::create_dir_all(&workspace_dir)?;
|
||||
let output = SimpleOutput::new();
|
||||
let output = SimpleOutput::new_with_mode(machine_mode);
|
||||
output.print(&format!(
|
||||
"📁 Created workspace directory: {}",
|
||||
workspace_dir.display()
|
||||
|
||||
93
crates/g3-cli/src/machine_ui_writer.rs
Normal file
93
crates/g3-cli/src/machine_ui_writer.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
use g3_core::ui_writer::UiWriter;
|
||||
use std::io::{self, Write};
|
||||
|
||||
/// Machine-mode implementation of UiWriter that prints plain, unformatted output
|
||||
/// This is designed for programmatic consumption and outputs everything verbatim
|
||||
pub struct MachineUiWriter;
|
||||
|
||||
impl MachineUiWriter {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl UiWriter for MachineUiWriter {
|
||||
fn print(&self, message: &str) {
|
||||
print!("{}", message);
|
||||
}
|
||||
|
||||
fn println(&self, message: &str) {
|
||||
println!("{}", message);
|
||||
}
|
||||
|
||||
fn print_inline(&self, message: &str) {
|
||||
print!("{}", message);
|
||||
let _ = io::stdout().flush();
|
||||
}
|
||||
|
||||
fn print_system_prompt(&self, prompt: &str) {
|
||||
println!("SYSTEM_PROMPT:");
|
||||
println!("{}", prompt);
|
||||
println!("END_SYSTEM_PROMPT");
|
||||
println!();
|
||||
}
|
||||
|
||||
fn print_context_status(&self, message: &str) {
|
||||
println!("CONTEXT_STATUS: {}", message);
|
||||
}
|
||||
|
||||
fn print_context_thinning(&self, message: &str) {
|
||||
println!("CONTEXT_THINNING: {}", message);
|
||||
}
|
||||
|
||||
fn print_tool_header(&self, tool_name: &str) {
|
||||
println!("TOOL_CALL: {}", tool_name);
|
||||
}
|
||||
|
||||
fn print_tool_arg(&self, key: &str, value: &str) {
|
||||
println!("TOOL_ARG: {} = {}", key, value);
|
||||
}
|
||||
|
||||
fn print_tool_output_header(&self) {
|
||||
println!("TOOL_OUTPUT:");
|
||||
}
|
||||
|
||||
fn update_tool_output_line(&self, line: &str) {
|
||||
println!("{}", line);
|
||||
}
|
||||
|
||||
fn print_tool_output_line(&self, line: &str) {
|
||||
println!("{}", line);
|
||||
}
|
||||
|
||||
fn print_tool_output_summary(&self, count: usize) {
|
||||
println!("TOOL_OUTPUT_LINES: {}", count);
|
||||
}
|
||||
|
||||
fn print_tool_timing(&self, duration_str: &str) {
|
||||
println!("TOOL_DURATION: {}", duration_str);
|
||||
println!("END_TOOL_OUTPUT");
|
||||
println!();
|
||||
}
|
||||
|
||||
fn print_agent_prompt(&self) {
|
||||
let _ = io::stdout().flush();
|
||||
}
|
||||
|
||||
fn print_agent_response(&self, content: &str) {
|
||||
print!("{}", content);
|
||||
let _ = io::stdout().flush();
|
||||
}
|
||||
|
||||
fn notify_sse_received(&self) {
|
||||
// No-op for machine mode
|
||||
}
|
||||
|
||||
fn flush(&self) {
|
||||
let _ = io::stdout().flush();
|
||||
}
|
||||
|
||||
fn wants_full_output(&self) -> bool {
|
||||
true // Machine mode wants complete, untruncated output
|
||||
}
|
||||
}
|
||||
32
crates/g3-cli/src/simple_output.rs
Normal file
32
crates/g3-cli/src/simple_output.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
/// Simple output helper for printing messages
|
||||
pub struct SimpleOutput {
|
||||
machine_mode: bool,
|
||||
}
|
||||
|
||||
impl SimpleOutput {
|
||||
pub fn new() -> Self {
|
||||
SimpleOutput { machine_mode: false }
|
||||
}
|
||||
|
||||
pub fn new_with_mode(machine_mode: bool) -> Self {
|
||||
SimpleOutput { machine_mode }
|
||||
}
|
||||
|
||||
pub fn print(&self, message: &str) {
|
||||
if !self.machine_mode {
|
||||
println!("{}", message);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print_smart(&self, message: &str) {
|
||||
if !self.machine_mode {
|
||||
println!("{}", message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SimpleOutput {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
use crate::retro_tui::RetroTui;
|
||||
use g3_core::ui_writer::UiWriter;
|
||||
use std::io::{self, Write};
|
||||
use std::sync::Mutex;
|
||||
use std::time::Instant;
|
||||
|
||||
/// Console implementation of UiWriter that prints to stdout
|
||||
pub struct ConsoleUiWriter {
|
||||
@@ -347,241 +345,3 @@ impl UiWriter for ConsoleUiWriter {
|
||||
}
|
||||
}
|
||||
|
||||
/// RetroTui implementation of UiWriter that sends output to the TUI
|
||||
pub struct RetroTuiWriter {
|
||||
tui: RetroTui,
|
||||
current_tool_name: Mutex<Option<String>>,
|
||||
current_tool_output: Mutex<Vec<String>>,
|
||||
current_tool_start: Mutex<Option<Instant>>,
|
||||
current_tool_caption: Mutex<String>,
|
||||
}
|
||||
|
||||
impl RetroTuiWriter {
|
||||
pub fn new(tui: RetroTui) -> Self {
|
||||
Self {
|
||||
tui,
|
||||
current_tool_name: Mutex::new(None),
|
||||
current_tool_output: Mutex::new(Vec::new()),
|
||||
current_tool_start: Mutex::new(None),
|
||||
current_tool_caption: Mutex::new(String::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UiWriter for RetroTuiWriter {
|
||||
fn print(&self, message: &str) {
|
||||
self.tui.output(message);
|
||||
}
|
||||
|
||||
fn println(&self, message: &str) {
|
||||
self.tui.output(message);
|
||||
}
|
||||
|
||||
fn print_inline(&self, message: &str) {
|
||||
// For inline printing, we'll just append to the output
|
||||
self.tui.output(message);
|
||||
}
|
||||
|
||||
fn print_system_prompt(&self, prompt: &str) {
|
||||
self.tui.output("🔍 System Prompt:");
|
||||
self.tui.output("================");
|
||||
for line in prompt.lines() {
|
||||
self.tui.output(line);
|
||||
}
|
||||
self.tui.output("================");
|
||||
self.tui.output("");
|
||||
}
|
||||
|
||||
fn print_context_status(&self, message: &str) {
|
||||
self.tui.output(message);
|
||||
}
|
||||
|
||||
fn print_context_thinning(&self, message: &str) {
|
||||
// For TUI, we'll use a highlighted output with special formatting
|
||||
// The TUI will handle the visual presentation
|
||||
|
||||
// Add visual separators and emphasis
|
||||
self.tui.output("");
|
||||
self.tui.output("═══════════════════════════════════════════════════════════");
|
||||
self.tui.output(&format!("✨ {} ✨", message));
|
||||
self.tui.output(" └─ Context optimized successfully");
|
||||
self.tui.output("═══════════════════════════════════════════════════════════");
|
||||
self.tui.output("");
|
||||
}
|
||||
|
||||
fn print_tool_header(&self, tool_name: &str) {
|
||||
// Start collecting tool output
|
||||
*self.current_tool_start.lock().unwrap() = Some(Instant::now());
|
||||
*self.current_tool_name.lock().unwrap() = Some(tool_name.to_string());
|
||||
self.current_tool_output.lock().unwrap().clear();
|
||||
self.current_tool_output
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(format!("Tool: {}", tool_name));
|
||||
|
||||
// Initialize caption
|
||||
*self.current_tool_caption.lock().unwrap() = String::new();
|
||||
}
|
||||
|
||||
fn print_tool_arg(&self, key: &str, value: &str) {
|
||||
// Filter out any keys that look like they might be agent message content
|
||||
// (e.g., keys that are suspiciously long or contain message-like content)
|
||||
let is_valid_arg_key = key.len() < 50
|
||||
&& !key.contains('\n')
|
||||
&& !key.contains("I'll")
|
||||
&& !key.contains("Let me")
|
||||
&& !key.contains("Here's")
|
||||
&& !key.contains("I can");
|
||||
|
||||
if is_valid_arg_key {
|
||||
self.current_tool_output
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(format!("{}: {}", key, value));
|
||||
}
|
||||
|
||||
// Build caption from first argument (usually the most important one)
|
||||
let mut caption = self.current_tool_caption.lock().unwrap();
|
||||
if caption.is_empty() && (key == "file_path" || key == "command" || key == "path") {
|
||||
// Truncate long values for the caption
|
||||
let truncated = if value.len() > 50 {
|
||||
// Use char_indices to safely truncate at character boundary
|
||||
let truncate_at = value.char_indices()
|
||||
.nth(47)
|
||||
.map(|(i, _)| i)
|
||||
.unwrap_or(value.len());
|
||||
format!("{}...", &value[..truncate_at])
|
||||
} else {
|
||||
value.to_string()
|
||||
};
|
||||
|
||||
// Add range information for read_file tool calls
|
||||
let tool_name = self.current_tool_name.lock().unwrap();
|
||||
let range_suffix = if tool_name.as_ref().is_some_and(|name| name == "read_file") {
|
||||
// We need to check if start/end args will be provided - for now just check if this is a partial read
|
||||
// This is a simplified approach since we're building the caption incrementally
|
||||
String::new() // We'll handle this in print_tool_output_header instead
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
*caption = format!("{}{}", truncated, range_suffix);
|
||||
}
|
||||
}
|
||||
|
||||
fn print_tool_output_header(&self) {
|
||||
// This is called right before tool execution starts
|
||||
// Send the initial tool header to the TUI now
|
||||
if let Some(tool_name) = self.current_tool_name.lock().unwrap().as_ref() {
|
||||
let mut caption = self.current_tool_caption.lock().unwrap().clone();
|
||||
|
||||
// Add range information for read_file tool calls
|
||||
if tool_name == "read_file" {
|
||||
// Check the tool output for start/end parameters
|
||||
let output = self.current_tool_output.lock().unwrap();
|
||||
let has_start = output.iter().any(|line| line.starts_with("start:"));
|
||||
let has_end = output.iter().any(|line| line.starts_with("end:"));
|
||||
|
||||
if has_start || has_end {
|
||||
let start_val = output.iter().find(|line| line.starts_with("start:")).map(|line| line.split(':').nth(1).unwrap_or("0").trim()).unwrap_or("0");
|
||||
let end_val = output.iter().find(|line| line.starts_with("end:")).map(|line| line.split(':').nth(1).unwrap_or("end").trim()).unwrap_or("end");
|
||||
caption = format!("{} [{}..{}]", caption, start_val, end_val);
|
||||
}
|
||||
}
|
||||
|
||||
// Send the tool output with initial header
|
||||
self.tui.tool_output(tool_name, &caption, "");
|
||||
}
|
||||
|
||||
self.current_tool_output.lock().unwrap().push(String::new());
|
||||
self.current_tool_output
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push("Output:".to_string());
|
||||
}
|
||||
|
||||
fn update_tool_output_line(&self, line: &str) {
|
||||
// For retro mode, we'll just add to the output buffer
|
||||
self.current_tool_output
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(line.to_string());
|
||||
}
|
||||
|
||||
fn print_tool_output_line(&self, line: &str) {
|
||||
self.current_tool_output
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(line.to_string());
|
||||
}
|
||||
|
||||
fn print_tool_output_summary(&self, hidden_count: usize) {
|
||||
self.current_tool_output.lock().unwrap().push(format!(
|
||||
"... ({} more line{})",
|
||||
hidden_count,
|
||||
if hidden_count == 1 { "" } else { "s" }
|
||||
));
|
||||
}
|
||||
|
||||
fn print_tool_timing(&self, duration_str: &str) {
|
||||
self.current_tool_output
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(format!("⚡️ {}", duration_str));
|
||||
|
||||
// Calculate the actual duration
|
||||
let duration_ms = if let Some(start) = *self.current_tool_start.lock().unwrap() {
|
||||
start.elapsed().as_millis()
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// Get the tool name and caption
|
||||
if let Some(tool_name) = self.current_tool_name.lock().unwrap().as_ref() {
|
||||
let content = self.current_tool_output.lock().unwrap().join("\n");
|
||||
let caption = self.current_tool_caption.lock().unwrap().clone();
|
||||
let caption = if caption.is_empty() {
|
||||
"Completed".to_string()
|
||||
} else {
|
||||
caption
|
||||
};
|
||||
|
||||
// Update the tool detail panel with the complete output without adding a new header
|
||||
// This keeps the original header in place to be updated by tool_complete
|
||||
self.tui.update_tool_detail(tool_name, &content);
|
||||
|
||||
// Determine success based on whether there's an error in the output
|
||||
// This is a simple heuristic - you might want to make this more sophisticated
|
||||
let success = !content.contains("error")
|
||||
&& !content.contains("Error")
|
||||
&& !content.contains("ERROR");
|
||||
|
||||
// Send the completion status to update the header
|
||||
self.tui
|
||||
.tool_complete(tool_name, success, duration_ms, &caption);
|
||||
}
|
||||
|
||||
// Clear the buffers
|
||||
*self.current_tool_name.lock().unwrap() = None;
|
||||
self.current_tool_output.lock().unwrap().clear();
|
||||
*self.current_tool_start.lock().unwrap() = None;
|
||||
*self.current_tool_caption.lock().unwrap() = String::new();
|
||||
}
|
||||
|
||||
fn print_agent_prompt(&self) {
|
||||
self.tui.output("\n💬 ");
|
||||
}
|
||||
|
||||
fn print_agent_response(&self, content: &str) {
|
||||
self.tui.output(content);
|
||||
}
|
||||
|
||||
fn notify_sse_received(&self) {
|
||||
// Notify the TUI that an SSE was received
|
||||
self.tui.sse_received();
|
||||
}
|
||||
|
||||
fn flush(&self) {
|
||||
// No-op for TUI since it handles its own rendering
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2677,12 +2677,19 @@ Template:
|
||||
if tool_call.tool != "final_output" {
|
||||
let output_lines: Vec<&str> = tool_result.lines().collect();
|
||||
|
||||
// Check if UI wants full output (machine mode) or truncated (human mode)
|
||||
let wants_full = self.ui_writer.wants_full_output();
|
||||
|
||||
// Helper function to safely truncate strings at character boundaries
|
||||
let truncate_line = |line: &str, max_width: usize| -> String {
|
||||
let char_count = line.chars().count();
|
||||
if char_count <= max_width {
|
||||
let truncate_line = |line: &str, max_width: usize, truncate: bool| -> String {
|
||||
if !truncate {
|
||||
// Machine mode - return full line
|
||||
line.to_string()
|
||||
} else if line.chars().count() <= max_width {
|
||||
// Human mode - line fits within limit
|
||||
line.to_string()
|
||||
} else {
|
||||
// Human mode - truncate long line
|
||||
let truncated: String = line
|
||||
.chars()
|
||||
.take(max_width.saturating_sub(3))
|
||||
@@ -2697,18 +2704,18 @@ Template:
|
||||
|
||||
// For todo tools, show all lines without truncation
|
||||
let is_todo_tool = tool_call.tool == "todo_read" || tool_call.tool == "todo_write";
|
||||
let max_lines_to_show = if is_todo_tool { output_len } else { MAX_LINES };
|
||||
let max_lines_to_show = if is_todo_tool || wants_full { output_len } else { MAX_LINES };
|
||||
|
||||
for (idx, line) in output_lines.iter().enumerate() {
|
||||
if !is_todo_tool && idx >= max_lines_to_show {
|
||||
if !is_todo_tool && !wants_full && idx >= max_lines_to_show {
|
||||
break;
|
||||
}
|
||||
// Clip line to max width
|
||||
let clipped_line = truncate_line(line, MAX_LINE_WIDTH);
|
||||
let clipped_line = truncate_line(line, MAX_LINE_WIDTH, !wants_full);
|
||||
self.ui_writer.update_tool_output_line(&clipped_line);
|
||||
}
|
||||
|
||||
if !is_todo_tool && output_len > MAX_LINES {
|
||||
if !is_todo_tool && !wants_full && output_len > MAX_LINES {
|
||||
self.ui_writer.print_tool_output_summary(output_len);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,10 @@ pub trait UiWriter: Send + Sync {
|
||||
|
||||
/// Flush any buffered output
|
||||
fn flush(&self);
|
||||
|
||||
/// Returns true if this UI writer wants full, untruncated output
|
||||
/// Default is false (truncate for human readability)
|
||||
fn wants_full_output(&self) -> bool { false }
|
||||
}
|
||||
|
||||
/// A no-op implementation for when UI output is not needed
|
||||
@@ -75,4 +79,5 @@ impl UiWriter for NullUiWriter {
|
||||
fn print_agent_response(&self, _content: &str) {}
|
||||
fn notify_sse_received(&self) {}
|
||||
fn flush(&self) {}
|
||||
fn wants_full_output(&self) -> bool { false }
|
||||
}
|
||||
Reference in New Issue
Block a user