feat(cli): shorten file paths in tool output display

Add three-level path shortening hierarchy for cleaner CLI output:
1. Project path -> <project_name>/... (when project loaded via /project)
2. Workspace path -> ./... (relative to current working directory)
3. Home path -> ~/... (fallback for paths under home directory)

Changes:
- Add shorten_path() and shorten_paths_in_command() functions in display.rs
- Add project_path/project_name fields to ConsoleUiWriter
- Add set_workspace_path(), set_project_path(), clear_project() to UiWriter trait
- Add ui_writer() getter to Agent struct
- Wire up project path setting in /project and /unproject commands
- Set workspace path when creating agents in all CLI modes

Before: ● read_file | /Users/dhanji/icloud/butler/projects/appa_estate/status.md
After:  ● read_file | appa_estate/status.md (with project loaded)
        ● read_file | ./src/main.rs (workspace-relative)
        ● read_file | ~/Documents/file.txt (home-relative)
This commit is contained in:
Dhanji R. Prasanna
2026-01-21 21:27:16 +05:30
parent 0f7961d3c6
commit 9325a43ff3
9 changed files with 264 additions and 12 deletions

View File

@@ -1,4 +1,5 @@
use crate::filter_json::{filter_json_tool_calls, reset_json_tool_state, ToolParsingHint};
use crate::display::{shorten_path, shorten_paths_in_command};
use crate::streaming_markdown::StreamingMarkdownFormatter;
use g3_core::ui_writer::UiWriter;
use std::io::{self, Write};
@@ -137,6 +138,12 @@ impl ParsingHintState {
pub struct ConsoleUiWriter {
current_tool_name: std::sync::Mutex<Option<String>>,
current_tool_args: std::sync::Mutex<Vec<(String, String)>>,
/// Workspace path for shortening displayed paths
workspace_path: std::sync::Mutex<Option<std::path::PathBuf>>,
/// Project path for shortening displayed paths (takes priority over workspace)
project_path: std::sync::Mutex<Option<std::path::PathBuf>>,
/// Project name for display (e.g., "appa_estate")
project_name: std::sync::Mutex<Option<String>>,
current_output_line: std::sync::Mutex<Option<String>>,
output_line_printed: std::sync::Mutex<bool>,
/// Track if we're in shell compact mode (for appending timing to output line)
@@ -191,6 +198,9 @@ impl ConsoleUiWriter {
Self {
current_tool_name: std::sync::Mutex::new(None),
current_tool_args: std::sync::Mutex::new(Vec::new()),
workspace_path: std::sync::Mutex::new(None),
project_path: std::sync::Mutex::new(None),
project_name: std::sync::Mutex::new(None),
current_output_line: std::sync::Mutex::new(None),
output_line_printed: std::sync::Mutex::new(false),
is_shell_compact: std::sync::Mutex::new(false),
@@ -201,6 +211,18 @@ impl ConsoleUiWriter {
}
}
impl ConsoleUiWriter {
fn get_workspace_path(&self) -> Option<std::path::PathBuf> {
self.workspace_path.lock().unwrap().clone()
}
fn get_project_info(&self) -> Option<(std::path::PathBuf, String)> {
let path = self.project_path.lock().unwrap().clone()?;
let name = self.project_name.lock().unwrap().clone()?;
Some((path, name))
}
}
impl UiWriter for ConsoleUiWriter {
fn print(&self, message: &str) {
print!("{}", message);
@@ -308,17 +330,28 @@ impl UiWriter for ConsoleUiWriter {
// For multi-line values, only show the first line
let first_line = value.lines().next().unwrap_or("");
// Truncate long values for display
let display_value = if first_line.len() > 80 {
// Get workspace path for shortening
let workspace = self.get_workspace_path();
let workspace_ref = workspace.as_deref();
// Get project info for shortening
let project_info = self.get_project_info();
let project_ref = project_info.as_ref().map(|(p, n)| (p.as_path(), n.as_str()));
// Shorten paths in the value (handles both file paths and shell commands)
let shortened = shorten_paths_in_command(first_line, workspace_ref, project_ref);
// Truncate long values for display (after shortening)
let display_value = if shortened.chars().count() > 80 {
// Use char_indices to safely truncate at character boundary
let truncate_at = first_line
let truncate_at = shortened
.char_indices()
.nth(77)
.map(|(i, _)| i)
.unwrap_or(first_line.len());
format!("{}...", &first_line[..truncate_at])
.unwrap_or(shortened.len());
format!("{}...", &shortened[..truncate_at])
} else {
first_line.to_string()
shortened
};
// Add range information for read_file tool calls
@@ -500,16 +533,21 @@ impl UiWriter for ConsoleUiWriter {
String::new()
}
} else {
// Truncate long paths
if file_path.len() > 60 {
let truncate_at = file_path
// Shorten path (project -> name/, workspace -> ./, home -> ~) then truncate if still long
let workspace = self.get_workspace_path();
let project_info = self.get_project_info();
let project_ref = project_info.as_ref().map(|(p, n)| (p.as_path(), n.as_str()));
let shortened = shorten_path(file_path, workspace.as_deref(), project_ref);
if shortened.chars().count() > 60 {
let truncate_at = shortened
.char_indices()
.nth(57)
.map(|(i, _)| i)
.unwrap_or(file_path.len());
format!("{}", &file_path[..truncate_at])
.unwrap_or(shortened.len());
format!("{}...", &shortened[..truncate_at])
} else {
file_path.to_string()
shortened
}
};
@@ -806,4 +844,18 @@ impl UiWriter for ConsoleUiWriter {
fn set_agent_mode(&self, is_agent_mode: bool) {
self.hint_state.is_agent_mode.store(is_agent_mode, Ordering::Relaxed);
}
fn set_workspace_path(&self, path: std::path::PathBuf) {
*self.workspace_path.lock().unwrap() = Some(path);
}
fn set_project_path(&self, path: std::path::PathBuf, name: String) {
*self.project_path.lock().unwrap() = Some(path);
*self.project_name.lock().unwrap() = Some(name);
}
fn clear_project(&self) {
*self.project_path.lock().unwrap() = None;
*self.project_name.lock().unwrap() = None;
}
}