refactor(cli): extract display utilities to eliminate code duplication
Created display.rs module with shared display functions: - format_workspace_path() / print_workspace_path() - LoadedContent struct for tracking loaded project files - print_loaded_status() for status line display - print_project_heading() for README heading Updated interactive.rs and agent_mode.rs to use the new module, eliminating duplicated workspace path formatting and loaded items status line logic. Results: - interactive.rs: 641 → 595 lines (-46) - agent_mode.rs: 312 → 288 lines (-24) - New display.rs: 197 lines with 5 unit tests Agent: fowler
This commit is contained in:
@@ -8,6 +8,7 @@ use g3_core::ui_writer::UiWriter;
|
|||||||
use g3_core::Agent;
|
use g3_core::Agent;
|
||||||
|
|
||||||
use crate::project_files::{combine_project_content, read_agents_config, read_include_prompt, read_project_memory, read_project_readme};
|
use crate::project_files::{combine_project_content, read_agents_config, read_include_prompt, read_project_memory, read_project_readme};
|
||||||
|
use crate::display::{LoadedContent, print_loaded_status, print_workspace_path};
|
||||||
use crate::language_prompts::{get_language_prompts_for_workspace, get_agent_language_prompts_for_workspace_with_langs};
|
use crate::language_prompts::{get_language_prompts_for_workspace, get_agent_language_prompts_for_workspace_with_langs};
|
||||||
use crate::simple_output::SimpleOutput;
|
use crate::simple_output::SimpleOutput;
|
||||||
use crate::embedded_agents::load_agent_prompt;
|
use crate::embedded_agents::load_agent_prompt;
|
||||||
@@ -103,18 +104,8 @@ pub async fn run_agent_mode(
|
|||||||
if !chat {
|
if !chat {
|
||||||
output.print(&format!(">> agent mode | {} ({})", agent_name, source));
|
output.print(&format!(">> agent mode | {} ({})", agent_name, source));
|
||||||
}
|
}
|
||||||
let workspace_display = {
|
|
||||||
let path_str = workspace_dir.display().to_string();
|
|
||||||
dirs::home_dir()
|
|
||||||
.and_then(|home| {
|
|
||||||
path_str
|
|
||||||
.strip_prefix(&home.display().to_string())
|
|
||||||
.map(|s| format!("~{}", s))
|
|
||||||
})
|
|
||||||
.unwrap_or(path_str)
|
|
||||||
};
|
|
||||||
// Always print workspace path (it's part of minimal output)
|
// Always print workspace path (it's part of minimal output)
|
||||||
print!("{}-> {}{}\n", crossterm::style::SetForegroundColor(crossterm::style::Color::DarkGrey), workspace_display, crossterm::style::ResetColor);
|
print_workspace_path(&workspace_dir);
|
||||||
|
|
||||||
// Load config
|
// Load config
|
||||||
let mut config = g3_config::Config::load(config_path)?;
|
let mut config = g3_config::Config::load(config_path)?;
|
||||||
@@ -143,33 +134,18 @@ pub async fn run_agent_mode(
|
|||||||
// Read include prompt early so we can show it in the status line
|
// Read include prompt early so we can show it in the status line
|
||||||
let include_prompt = read_include_prompt(include_prompt_path.as_deref());
|
let include_prompt = read_include_prompt(include_prompt_path.as_deref());
|
||||||
|
|
||||||
// Build status line showing only what was loaded (in load order)
|
// Build and print status line showing what was loaded
|
||||||
let mut loaded_items: Vec<String> = Vec::new();
|
let include_filename = include_prompt_path.as_ref()
|
||||||
if readme_content_opt.is_some() {
|
.filter(|_| include_prompt.is_some())
|
||||||
loaded_items.push("README".to_string());
|
.and_then(|p| p.file_name())
|
||||||
}
|
.map(|s| s.to_string_lossy().to_string());
|
||||||
if agents_content_opt.is_some() {
|
let loaded = LoadedContent::new(
|
||||||
loaded_items.push("AGENTS.md".to_string());
|
readme_content_opt.is_some(),
|
||||||
}
|
agents_content_opt.is_some(),
|
||||||
if let Some(path) = &include_prompt_path {
|
memory_content_opt.is_some(),
|
||||||
if include_prompt.is_some() {
|
include_filename,
|
||||||
let filename = path.file_name().map(|s| s.to_string_lossy().to_string()).unwrap_or_else(|| "prompt".to_string());
|
|
||||||
loaded_items.push(filename);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if memory_content_opt.is_some() {
|
|
||||||
loaded_items.push("Memory".to_string());
|
|
||||||
}
|
|
||||||
// Print status line only if something was loaded
|
|
||||||
if !loaded_items.is_empty() {
|
|
||||||
let status_str = loaded_items.iter().map(|s| format!("✓ {}", s)).collect::<Vec<_>>().join(" ");
|
|
||||||
print!(
|
|
||||||
"{} {}{}\n",
|
|
||||||
crossterm::style::SetForegroundColor(crossterm::style::Color::DarkGrey),
|
|
||||||
status_str,
|
|
||||||
crossterm::style::ResetColor
|
|
||||||
);
|
);
|
||||||
}
|
print_loaded_status(&loaded);
|
||||||
|
|
||||||
// Get language-specific prompts (same mechanism as normal mode)
|
// Get language-specific prompts (same mechanism as normal mode)
|
||||||
let language_content = get_language_prompts_for_workspace(&workspace_dir);
|
let language_content = get_language_prompts_for_workspace(&workspace_dir);
|
||||||
|
|||||||
197
crates/g3-cli/src/display.rs
Normal file
197
crates/g3-cli/src/display.rs
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
//! Display utilities for G3 CLI.
|
||||||
|
//!
|
||||||
|
//! Provides shared display functions used by both interactive mode and agent mode.
|
||||||
|
|
||||||
|
use crossterm::style::{Color, ResetColor, SetForegroundColor};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// Format a workspace path for display, replacing home directory with ~.
|
||||||
|
pub fn format_workspace_path(workspace_path: &Path) -> String {
|
||||||
|
let path_str = workspace_path.display().to_string();
|
||||||
|
dirs::home_dir()
|
||||||
|
.and_then(|home| {
|
||||||
|
path_str
|
||||||
|
.strip_prefix(&home.display().to_string())
|
||||||
|
.map(|s| format!("~{}", s))
|
||||||
|
})
|
||||||
|
.unwrap_or(path_str)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print the workspace path in a consistent format.
|
||||||
|
pub fn print_workspace_path(workspace_path: &Path) {
|
||||||
|
let display = format_workspace_path(workspace_path);
|
||||||
|
print!(
|
||||||
|
"{}-> {}{}",
|
||||||
|
SetForegroundColor(Color::DarkGrey),
|
||||||
|
display,
|
||||||
|
ResetColor
|
||||||
|
);
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Information about what project files were loaded.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct LoadedContent {
|
||||||
|
pub has_readme: bool,
|
||||||
|
pub has_agents: bool,
|
||||||
|
pub has_memory: bool,
|
||||||
|
pub include_prompt_filename: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoadedContent {
|
||||||
|
/// Create from explicit boolean flags.
|
||||||
|
pub fn new(has_readme: bool, has_agents: bool, has_memory: bool, include_prompt_filename: Option<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
has_readme,
|
||||||
|
has_agents,
|
||||||
|
has_memory,
|
||||||
|
include_prompt_filename,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create from combined content string by detecting markers.
|
||||||
|
pub fn from_combined_content(content: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
has_readme: content.contains("Project README"),
|
||||||
|
has_agents: content.contains("Agent Configuration"),
|
||||||
|
has_memory: content.contains("=== Project Memory"),
|
||||||
|
include_prompt_filename: if content.contains("Included Prompt") {
|
||||||
|
Some("prompt".to_string()) // Default name when we can't determine the actual filename
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create with explicit include prompt filename.
|
||||||
|
pub fn with_include_prompt_filename(mut self, filename: Option<String>) -> Self {
|
||||||
|
if self.include_prompt_filename.is_some() {
|
||||||
|
self.include_prompt_filename = filename;
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if any content was loaded.
|
||||||
|
pub fn has_any(&self) -> bool {
|
||||||
|
self.has_readme || self.has_agents || self.has_memory || self.include_prompt_filename.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a list of loaded item names in load order.
|
||||||
|
pub fn to_loaded_items(&self) -> Vec<String> {
|
||||||
|
let mut items = Vec::new();
|
||||||
|
if self.has_readme {
|
||||||
|
items.push("README".to_string());
|
||||||
|
}
|
||||||
|
if self.has_agents {
|
||||||
|
items.push("AGENTS.md".to_string());
|
||||||
|
}
|
||||||
|
if let Some(ref filename) = self.include_prompt_filename {
|
||||||
|
items.push(filename.clone());
|
||||||
|
}
|
||||||
|
if self.has_memory {
|
||||||
|
items.push("Memory".to_string());
|
||||||
|
}
|
||||||
|
items
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print a status line showing what project files were loaded.
|
||||||
|
/// Format: " ✓ README ✓ AGENTS.md ✓ Memory"
|
||||||
|
pub fn print_loaded_status(loaded: &LoadedContent) {
|
||||||
|
if !loaded.has_any() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let items = loaded.to_loaded_items();
|
||||||
|
let status_str = items
|
||||||
|
.iter()
|
||||||
|
.map(|s| format!("✓ {}", s))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
print!(
|
||||||
|
"{} {}{}",
|
||||||
|
SetForegroundColor(Color::DarkGrey),
|
||||||
|
status_str,
|
||||||
|
ResetColor
|
||||||
|
);
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print the project name/heading from README content.
|
||||||
|
pub fn print_project_heading(heading: &str) {
|
||||||
|
print!(
|
||||||
|
"{}>> {}{}",
|
||||||
|
SetForegroundColor(Color::DarkGrey),
|
||||||
|
heading,
|
||||||
|
ResetColor
|
||||||
|
);
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_format_workspace_path_with_home() {
|
||||||
|
// This test depends on having a home directory
|
||||||
|
if let Some(home) = dirs::home_dir() {
|
||||||
|
let test_path = home.join("projects").join("myapp");
|
||||||
|
let formatted = format_workspace_path(&test_path);
|
||||||
|
assert!(formatted.starts_with("~/"), "Expected ~/ prefix, got: {}", formatted);
|
||||||
|
assert!(formatted.contains("projects/myapp"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_format_workspace_path_without_home() {
|
||||||
|
let test_path = PathBuf::from("/tmp/workspace");
|
||||||
|
let formatted = format_workspace_path(&test_path);
|
||||||
|
assert_eq!(formatted, "/tmp/workspace");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_loaded_content_from_combined() {
|
||||||
|
let content = "Project README\nAgent Configuration\n=== Project Memory";
|
||||||
|
let loaded = LoadedContent::from_combined_content(content);
|
||||||
|
assert!(loaded.has_readme);
|
||||||
|
assert!(loaded.has_agents);
|
||||||
|
assert!(loaded.has_memory);
|
||||||
|
assert!(loaded.include_prompt_filename.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_loaded_content_with_include_prompt() {
|
||||||
|
let content = "Project README\nIncluded Prompt";
|
||||||
|
let loaded = LoadedContent::from_combined_content(content)
|
||||||
|
.with_include_prompt_filename(Some("custom.md".to_string()));
|
||||||
|
assert!(loaded.has_readme);
|
||||||
|
assert_eq!(loaded.include_prompt_filename, Some("custom.md".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_loaded_content_to_items_order() {
|
||||||
|
let loaded = LoadedContent {
|
||||||
|
has_readme: true,
|
||||||
|
has_agents: true,
|
||||||
|
has_memory: true,
|
||||||
|
include_prompt_filename: Some("prompt.md".to_string()),
|
||||||
|
};
|
||||||
|
let items = loaded.to_loaded_items();
|
||||||
|
assert_eq!(items, vec!["README", "AGENTS.md", "prompt.md", "Memory"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_loaded_content_has_any() {
|
||||||
|
let empty = LoadedContent::default();
|
||||||
|
assert!(!empty.has_any());
|
||||||
|
|
||||||
|
let with_readme = LoadedContent {
|
||||||
|
has_readme: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
assert!(with_readme.has_any());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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::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;
|
||||||
use crate::simple_output::SimpleOutput;
|
use crate::simple_output::SimpleOutput;
|
||||||
@@ -102,67 +103,20 @@ pub async fn run_interactive<W: UiWriter>(
|
|||||||
|
|
||||||
// Display message if AGENTS.md or README was loaded
|
// Display message if AGENTS.md or README was loaded
|
||||||
if let Some(ref content) = combined_content {
|
if let Some(ref content) = combined_content {
|
||||||
// Check what was loaded
|
let loaded = LoadedContent::from_combined_content(content);
|
||||||
let has_agents = content.contains("Agent Configuration");
|
|
||||||
let has_readme = content.contains("Project README");
|
|
||||||
let has_include_prompt = content.contains("Included Prompt");
|
|
||||||
let has_memory = content.contains("=== Project Memory");
|
|
||||||
|
|
||||||
// Extract project name if README is loaded
|
// Extract project name if README is loaded
|
||||||
let project_name = if has_readme {
|
if loaded.has_readme {
|
||||||
// Extract the first heading or title from the README
|
if let Some(name) = extract_readme_heading(content) {
|
||||||
extract_readme_heading(content)
|
print_project_heading(&name);
|
||||||
} else {
|
}
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(name) = project_name {
|
|
||||||
print!("{}>> {}{}\n", SetForegroundColor(Color::DarkGrey), name, ResetColor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build status line showing only what was loaded (in load order)
|
print_loaded_status(&loaded);
|
||||||
let mut loaded_items: Vec<&str> = Vec::new();
|
|
||||||
if has_readme {
|
|
||||||
loaded_items.push("README");
|
|
||||||
}
|
|
||||||
if has_agents {
|
|
||||||
loaded_items.push("AGENTS.md");
|
|
||||||
}
|
|
||||||
if has_include_prompt {
|
|
||||||
loaded_items.push("prompt");
|
|
||||||
}
|
|
||||||
if has_memory {
|
|
||||||
loaded_items.push("Memory");
|
|
||||||
}
|
|
||||||
// Print status line only if something was loaded
|
|
||||||
if !loaded_items.is_empty() {
|
|
||||||
let status_str = loaded_items.iter().map(|s| format!("✓ {}", s)).collect::<Vec<_>>().join(" ");
|
|
||||||
print!(
|
|
||||||
"{} {}{}\n",
|
|
||||||
SetForegroundColor(Color::DarkGrey),
|
|
||||||
status_str,
|
|
||||||
ResetColor
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display workspace path
|
// Display workspace path
|
||||||
let workspace_display = {
|
print_workspace_path(workspace_path);
|
||||||
let path_str = workspace_path.display().to_string();
|
|
||||||
dirs::home_dir()
|
|
||||||
.and_then(|home| {
|
|
||||||
path_str
|
|
||||||
.strip_prefix(&home.display().to_string())
|
|
||||||
.map(|s| format!("~{}", s))
|
|
||||||
})
|
|
||||||
.unwrap_or(path_str)
|
|
||||||
};
|
|
||||||
print!(
|
|
||||||
"{}-> {}{}\n",
|
|
||||||
SetForegroundColor(Color::DarkGrey),
|
|
||||||
workspace_display,
|
|
||||||
ResetColor
|
|
||||||
);
|
|
||||||
output.print("");
|
output.print("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ mod agent_mode;
|
|||||||
mod autonomous;
|
mod autonomous;
|
||||||
mod cli_args;
|
mod cli_args;
|
||||||
mod coach_feedback;
|
mod coach_feedback;
|
||||||
|
mod display;
|
||||||
mod interactive;
|
mod interactive;
|
||||||
mod simple_output;
|
mod simple_output;
|
||||||
mod task_execution;
|
mod task_execution;
|
||||||
|
|||||||
Reference in New Issue
Block a user