Centralize g3 status message formatting
Extract a new g3_status module in g3-cli that provides consistent formatting for all 'g3:' prefixed system status messages. Key changes: - Add G3Status struct with methods for progress, done, failed, error, etc. - Add Status enum with Done, Failed, Error, Resolved, Insufficient, NoChanges - Add ThinResult struct in g3-core for semantic thinning data - Update UiWriter trait with print_thin_result() method - Refactor context thinning to return ThinResult instead of formatted strings - Update all callers to use the new centralized formatting - Session resume/decline messages now use G3Status - Compaction status messages now use G3Status This maintains clean separation of concerns: g3-core emits semantic data, g3-cli handles all terminal formatting and colors.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# Project Memory
|
||||
> Updated: 2026-01-19T08:32:03Z | Size: 14.0k chars
|
||||
> Updated: 2026-01-20T04:10:35Z | Size: 15.8k chars
|
||||
|
||||
### Remember Tool Wiring
|
||||
- `crates/g3-core/src/tools/memory.rs` [0..5000] - `execute_remember()`, `get_memory_path()`, `merge_memory()`
|
||||
@@ -253,3 +253,36 @@ let mut registry = ProviderRegistry::new();
|
||||
registry.register(provider);
|
||||
let agent = Agent::new_for_test(config, NullUiWriter, registry).await?;
|
||||
```
|
||||
|
||||
### G3 Status Message Formatting
|
||||
Centralized formatting for all "g3:" prefixed system status messages.
|
||||
|
||||
- `crates/g3-cli/src/g3_status.rs`
|
||||
- `G3Status` - static methods for consistent status message formatting
|
||||
- `Status` enum - `Done`, `Failed`, `Error(String)`, `Custom(String)`, `Resolved`, `Insufficient`
|
||||
- `progress()` [64..76] - prints "g3: <message> ..." (no newline, stays on same line)
|
||||
- `progress_ln()` [79..90] - prints "g3: <message> ..." with newline
|
||||
- `done()` [93..101] - prints bold green "[done]"
|
||||
- `failed()` [104..111] - prints red "[failed]"
|
||||
- `error()` [114..122] - prints red "[error: <msg>]"
|
||||
- `status()` [125..152] - dispatches to appropriate status method
|
||||
- `complete()` [155..158] - one-shot progress + status
|
||||
- `info_inline()` [168..178] - ANSI escape to append to previous line
|
||||
- `format_status()` [181..214] - returns formatted status string
|
||||
- `resuming()` [227..236] - session resume message with cyan session ID
|
||||
- `resuming_summary()` [239..248] - resume with "(summary)" note
|
||||
|
||||
### ThinResult Struct
|
||||
Semantic data for context thinning operations, replacing pre-formatted strings.
|
||||
|
||||
- `crates/g3-core/src/context_window.rs`
|
||||
- `ThinResult` [16..36] - struct with scope, before/after percentages, counts, chars_saved, had_changes
|
||||
- `thin_context_with_scope()` [373..450] - returns `ThinResult` instead of `(String, usize)`
|
||||
- `build_thin_result()` [720..740] - constructs `ThinResult` from operation data
|
||||
|
||||
- `crates/g3-core/src/ui_writer.rs`
|
||||
- `print_thin_result(&self, result: &ThinResult)` [31] - trait method for UI formatting
|
||||
|
||||
- `crates/g3-cli/src/g3_status.rs`
|
||||
- `Status::NoChanges` [42] - new status variant for thinning with no changes
|
||||
- `G3Status::thin_result()` [265..292] - formats ThinResult with proper colors/styling
|
||||
328
crates/g3-cli/src/g3_status.rs
Normal file
328
crates/g3-cli/src/g3_status.rs
Normal file
@@ -0,0 +1,328 @@
|
||||
//! Centralized formatting for g3 system status messages.
|
||||
//!
|
||||
//! This module provides consistent formatting for all "g3:" prefixed status messages,
|
||||
//! including progress indicators, completion statuses, and inline updates.
|
||||
//!
|
||||
//! # Usage
|
||||
//!
|
||||
//! ```ignore
|
||||
//! use crate::g3_status::G3Status;
|
||||
//!
|
||||
//! // Start a progress message (stays on same line, no newline)
|
||||
//! G3Status::progress("compacting session");
|
||||
//!
|
||||
//! // Complete with status (adds to same line, then newline)
|
||||
//! G3Status::done();
|
||||
//! // or
|
||||
//! G3Status::failed();
|
||||
//! // or
|
||||
//! G3Status::error("timeout");
|
||||
//!
|
||||
//! // One-shot status message (progress + status on same line)
|
||||
//! G3Status::complete("compacting session", Status::Done);
|
||||
//! ```
|
||||
|
||||
use crossterm::style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor};
|
||||
use std::io::{self, Write};
|
||||
|
||||
/// Status types for g3 system messages
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Status {
|
||||
/// Success - bold green "[done]"
|
||||
Done,
|
||||
/// Failure - red "[failed]"
|
||||
Failed,
|
||||
/// Error with message - red "[error: <msg>]"
|
||||
Error(String),
|
||||
/// Custom status - plain "[<status>]"
|
||||
Custom(String),
|
||||
/// Resolved status - for thinning operations
|
||||
Resolved,
|
||||
/// Insufficient - for thinning operations
|
||||
Insufficient,
|
||||
/// No changes - for thinning operations that didn't modify anything
|
||||
NoChanges,
|
||||
}
|
||||
|
||||
impl Status {
|
||||
/// Parse a status string into a Status enum
|
||||
pub fn from_str(s: &str) -> Self {
|
||||
match s {
|
||||
"done" => Status::Done,
|
||||
"failed" => Status::Failed,
|
||||
"resolved" => Status::Resolved,
|
||||
"insufficient" => Status::Insufficient,
|
||||
s if s.starts_with("error:") => Status::Error(s[6..].trim().to_string()),
|
||||
s if s.starts_with("error") => Status::Error(s[5..].trim().to_string()),
|
||||
other => Status::Custom(other.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Centralized g3 system status message formatting
|
||||
pub struct G3Status;
|
||||
|
||||
impl G3Status {
|
||||
/// Print a progress message that stays on the same line.
|
||||
/// Format: "g3: <message> ..."
|
||||
/// - "g3:" is bold green
|
||||
/// - No trailing newline (use `done()`, `failed()`, etc. to complete)
|
||||
pub fn progress(message: &str) {
|
||||
print!(
|
||||
"{}{}g3:{}{} {} ...",
|
||||
SetAttribute(Attribute::Bold),
|
||||
SetForegroundColor(Color::Green),
|
||||
ResetColor,
|
||||
SetAttribute(Attribute::Reset),
|
||||
message
|
||||
);
|
||||
let _ = io::stdout().flush();
|
||||
}
|
||||
|
||||
/// Print a progress message with a newline at the end.
|
||||
/// Use this when you don't plan to add a status on the same line.
|
||||
pub fn progress_ln(message: &str) {
|
||||
println!(
|
||||
"{}{}g3:{}{} {} ...",
|
||||
SetAttribute(Attribute::Bold),
|
||||
SetForegroundColor(Color::Green),
|
||||
ResetColor,
|
||||
SetAttribute(Attribute::Reset),
|
||||
message
|
||||
);
|
||||
}
|
||||
|
||||
/// Complete a progress message with "[done]" in bold green.
|
||||
pub fn done() {
|
||||
println!(
|
||||
" {}{}[done]{}",
|
||||
SetForegroundColor(Color::Green),
|
||||
SetAttribute(Attribute::Bold),
|
||||
ResetColor
|
||||
);
|
||||
}
|
||||
|
||||
/// Complete a progress message with "[failed]" in red.
|
||||
pub fn failed() {
|
||||
println!(
|
||||
" {}[failed]{}",
|
||||
SetForegroundColor(Color::Red),
|
||||
ResetColor
|
||||
);
|
||||
}
|
||||
|
||||
/// Complete a progress message with "[error: <msg>]" in red.
|
||||
pub fn error(msg: &str) {
|
||||
println!(
|
||||
" {}[error: {}]{}",
|
||||
SetForegroundColor(Color::Red),
|
||||
msg,
|
||||
ResetColor
|
||||
);
|
||||
}
|
||||
|
||||
/// Complete a progress message with a custom status.
|
||||
pub fn status(status: &Status) {
|
||||
match status {
|
||||
Status::Done => Self::done(),
|
||||
Status::Failed => Self::failed(),
|
||||
Status::Error(msg) => Self::error(msg),
|
||||
Status::Resolved => {
|
||||
println!(
|
||||
" {}{}[resolved]{}",
|
||||
SetForegroundColor(Color::Green),
|
||||
SetAttribute(Attribute::Bold),
|
||||
ResetColor
|
||||
);
|
||||
}
|
||||
Status::Insufficient => {
|
||||
println!(
|
||||
" {}[insufficient]{}",
|
||||
SetForegroundColor(Color::Yellow),
|
||||
ResetColor
|
||||
);
|
||||
}
|
||||
Status::Custom(s) => {
|
||||
println!(" [{}]", s);
|
||||
}
|
||||
Status::NoChanges => {
|
||||
println!(
|
||||
" {}[no changes]{}",
|
||||
SetForegroundColor(Color::DarkGrey),
|
||||
ResetColor
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Print a complete status message (progress + status) on one line.
|
||||
/// Format: "g3: <message> ... [status]"
|
||||
pub fn complete(message: &str, status: Status) {
|
||||
Self::progress(message);
|
||||
Self::status(&status);
|
||||
}
|
||||
|
||||
/// Print an info message in dimmed/grey text.
|
||||
/// Format: "... <message>"
|
||||
pub fn info(message: &str) {
|
||||
println!(
|
||||
"{}... {}{}",
|
||||
SetForegroundColor(Color::DarkGrey),
|
||||
message,
|
||||
ResetColor
|
||||
);
|
||||
}
|
||||
|
||||
/// Print an info message inline (no newline, for appending to user input).
|
||||
/// Uses ANSI escape to move cursor up and to end of previous line.
|
||||
pub fn info_inline(message: &str) {
|
||||
// Move cursor up one line, to end of line, then print
|
||||
print!(
|
||||
"\x1b[1A\x1b[999C {}... {}{}\n",
|
||||
SetForegroundColor(Color::DarkGrey),
|
||||
message,
|
||||
ResetColor
|
||||
);
|
||||
let _ = io::stdout().flush();
|
||||
}
|
||||
|
||||
/// Format a status string for inline use (returns the formatted string).
|
||||
/// Useful when building custom messages.
|
||||
pub fn format_status(status: &Status) -> String {
|
||||
match status {
|
||||
Status::Done => format!(
|
||||
"{}{}[done]{}",
|
||||
SetForegroundColor(Color::Green),
|
||||
SetAttribute(Attribute::Bold),
|
||||
ResetColor
|
||||
),
|
||||
Status::Failed => format!(
|
||||
"{}[failed]{}",
|
||||
SetForegroundColor(Color::Red),
|
||||
ResetColor
|
||||
),
|
||||
Status::Error(msg) => format!(
|
||||
"{}[error: {}]{}",
|
||||
SetForegroundColor(Color::Red),
|
||||
msg,
|
||||
ResetColor
|
||||
),
|
||||
Status::Resolved => format!(
|
||||
"{}{}[resolved]{}",
|
||||
SetForegroundColor(Color::Green),
|
||||
SetAttribute(Attribute::Bold),
|
||||
ResetColor
|
||||
),
|
||||
Status::Insufficient => format!(
|
||||
"{}[insufficient]{}",
|
||||
SetForegroundColor(Color::Yellow),
|
||||
ResetColor
|
||||
),
|
||||
Status::Custom(s) => format!("[{}]", s),
|
||||
Status::NoChanges => format!(
|
||||
"{}[no changes]{}",
|
||||
SetForegroundColor(Color::DarkGrey),
|
||||
ResetColor
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Format the "g3:" prefix for inline use.
|
||||
pub fn format_prefix() -> String {
|
||||
format!(
|
||||
"{}{}g3:{}{}",
|
||||
SetAttribute(Attribute::Bold),
|
||||
SetForegroundColor(Color::Green),
|
||||
ResetColor,
|
||||
SetAttribute(Attribute::Reset),
|
||||
)
|
||||
}
|
||||
|
||||
/// Print a resuming session message with session ID highlighted.
|
||||
/// Format: "... resuming <session_id> [done/error]"
|
||||
pub fn resuming(session_id: &str, status: Status) {
|
||||
let status_str = Self::format_status(&status);
|
||||
println!(
|
||||
"... resuming {}{}{} {}",
|
||||
SetForegroundColor(Color::Cyan),
|
||||
session_id,
|
||||
ResetColor,
|
||||
status_str
|
||||
);
|
||||
}
|
||||
|
||||
/// Print a resuming session message with "(summary)" note.
|
||||
pub fn resuming_summary(session_id: &str) {
|
||||
let status_str = Self::format_status(&Status::Done);
|
||||
println!(
|
||||
"... resuming {}{}{} (summary) {}",
|
||||
SetForegroundColor(Color::Cyan),
|
||||
session_id,
|
||||
ResetColor,
|
||||
status_str
|
||||
);
|
||||
}
|
||||
|
||||
/// Print a context thinning result.
|
||||
/// Format: "g3: thinning context ... 70% -> 40% ... [done]"
|
||||
/// or: "g3: thinning context (full) ... 70% ... [no changes]"
|
||||
pub fn thin_result(result: &g3_core::ThinResult) {
|
||||
use g3_core::ThinScope;
|
||||
|
||||
let scope_desc = match result.scope {
|
||||
ThinScope::FirstThird => "thinning context",
|
||||
ThinScope::All => "thinning context (full)",
|
||||
};
|
||||
|
||||
if result.had_changes {
|
||||
// Format: "g3: thinning context ... 70% -> 40% ... [done]"
|
||||
print!(
|
||||
"{} {} ... {}% -> {}% ...",
|
||||
Self::format_prefix(),
|
||||
scope_desc,
|
||||
result.before_percentage,
|
||||
result.after_percentage
|
||||
);
|
||||
Self::done();
|
||||
} else {
|
||||
// Format: "g3: thinning context ... 70% ... [no changes]"
|
||||
Self::complete(&format!("{} ... {}%", scope_desc, result.before_percentage), Status::NoChanges);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_status_from_str() {
|
||||
assert_eq!(Status::from_str("done"), Status::Done);
|
||||
assert_eq!(Status::from_str("failed"), Status::Failed);
|
||||
assert_eq!(Status::from_str("resolved"), Status::Resolved);
|
||||
assert_eq!(Status::from_str("insufficient"), Status::Insufficient);
|
||||
assert_eq!(Status::from_str("error: timeout"), Status::Error("timeout".to_string()));
|
||||
assert_eq!(Status::from_str("error timeout"), Status::Error("timeout".to_string()));
|
||||
assert_eq!(Status::from_str("custom"), Status::Custom("custom".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_status_contains_ansi() {
|
||||
let done = G3Status::format_status(&Status::Done);
|
||||
assert!(done.contains("[done]"));
|
||||
assert!(done.contains("\x1b")); // Contains ANSI escape
|
||||
|
||||
let failed = G3Status::format_status(&Status::Failed);
|
||||
assert!(failed.contains("[failed]"));
|
||||
|
||||
let error = G3Status::format_status(&Status::Error("test".to_string()));
|
||||
assert!(error.contains("[error: test]"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_prefix() {
|
||||
let prefix = G3Status::format_prefix();
|
||||
assert!(prefix.contains("g3:"));
|
||||
assert!(prefix.contains("\x1b")); // Contains ANSI escape
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ use tracing::{debug, error};
|
||||
use g3_core::ui_writer::UiWriter;
|
||||
use g3_core::Agent;
|
||||
|
||||
use crate::g3_status::{G3Status, Status};
|
||||
use crate::project_files::extract_readme_heading;
|
||||
use crate::simple_output::SimpleOutput;
|
||||
use crate::task_execution::execute_task_with_retry;
|
||||
@@ -54,21 +55,20 @@ pub async fn run_interactive<W: UiWriter>(
|
||||
// Resume the session
|
||||
match agent.restore_from_continuation(&continuation) {
|
||||
Ok(true) => {
|
||||
// Print bold green [done]
|
||||
println!("{}... resuming ... [done]{}", SetForegroundColor(Color::Green), ResetColor);
|
||||
G3Status::resuming(&continuation.session_id, Status::Done);
|
||||
}
|
||||
Ok(false) => {
|
||||
println!("{}... resuming ... [done]{}", SetForegroundColor(Color::Green), ResetColor);
|
||||
G3Status::resuming_summary(&continuation.session_id);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}... failed: {}{}", SetForegroundColor(Color::Yellow), e, ResetColor);
|
||||
G3Status::resuming(&continuation.session_id, Status::Error(e.to_string()));
|
||||
// Clear the invalid continuation
|
||||
let _ = g3_core::clear_continuation();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// User declined, clear the continuation
|
||||
println!("{}... starting fresh{}", SetForegroundColor(Color::DarkGrey), ResetColor);
|
||||
G3Status::info_inline("starting fresh");
|
||||
let _ = g3_core::clear_continuation();
|
||||
}
|
||||
}
|
||||
@@ -371,13 +371,13 @@ async fn handle_command<W: UiWriter>(
|
||||
Ok(true)
|
||||
}
|
||||
"/thinnify" => {
|
||||
let summary = agent.force_thin();
|
||||
println!("{}", summary);
|
||||
let result = agent.force_thin();
|
||||
G3Status::thin_result(&result);
|
||||
Ok(true)
|
||||
}
|
||||
"/skinnify" => {
|
||||
let summary = agent.force_thin_all();
|
||||
println!("{}", summary);
|
||||
let result = agent.force_thin_all();
|
||||
G3Status::thin_result(&result);
|
||||
Ok(true)
|
||||
}
|
||||
"/fragments" => {
|
||||
@@ -566,22 +566,13 @@ async fn handle_command<W: UiWriter>(
|
||||
let selected = &sessions[num - 1];
|
||||
match agent.switch_to_session(selected) {
|
||||
Ok(true) => {
|
||||
output.print_inline(&format!(
|
||||
"... resuming \x1b[36m{}\x1b[0m \x1b[1;32m[done]\x1b[0m\n",
|
||||
selected.session_id
|
||||
));
|
||||
G3Status::resuming(&selected.session_id, Status::Done);
|
||||
}
|
||||
Ok(false) => {
|
||||
output.print_inline(&format!(
|
||||
"... resuming \x1b[36m{}\x1b[0m (summary) \x1b[1;32m[done]\x1b[0m\n",
|
||||
selected.session_id
|
||||
));
|
||||
G3Status::resuming_summary(&selected.session_id);
|
||||
}
|
||||
Err(e) => {
|
||||
output.print_inline(&format!(
|
||||
"... resuming \x1b[36m{}\x1b[0m \x1b[1;31m[error: {}]\x1b[0m\n",
|
||||
selected.session_id, e
|
||||
));
|
||||
G3Status::resuming(&selected.session_id, Status::Error(e.to_string()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -17,6 +17,7 @@ mod simple_output;
|
||||
mod task_execution;
|
||||
mod ui_writer_impl;
|
||||
mod utils;
|
||||
mod g3_status;
|
||||
|
||||
use anyhow::Result;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crossterm::style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor};
|
||||
use crate::g3_status::{G3Status, Status};
|
||||
|
||||
/// Simple output helper for printing messages
|
||||
#[derive(Clone)]
|
||||
@@ -25,45 +25,16 @@ impl SimpleOutput {
|
||||
|
||||
/// Print a g3 status message with colored tag and status
|
||||
/// Format: "g3: <message> ... [status]"
|
||||
/// - "g3:" is bold green
|
||||
/// - "done" status is normal
|
||||
/// - "failed" and "error" statuses are red
|
||||
/// Uses centralized G3Status formatting.
|
||||
pub fn print_g3_status(&self, message: &str, status: &str) {
|
||||
let status_colored = match status {
|
||||
s if s.starts_with("error") || s == "failed" => {
|
||||
format!(
|
||||
"{}[{}]{}",
|
||||
SetForegroundColor(Color::Red),
|
||||
status,
|
||||
ResetColor
|
||||
)
|
||||
}
|
||||
_ => format!("[{}]", status),
|
||||
};
|
||||
|
||||
println!(
|
||||
"{}{}g3:{}{} {} ... {}",
|
||||
SetAttribute(Attribute::Bold),
|
||||
SetForegroundColor(Color::Green),
|
||||
ResetColor,
|
||||
SetAttribute(Attribute::Reset),
|
||||
message,
|
||||
status_colored
|
||||
);
|
||||
G3Status::complete(message, Status::from_str(status));
|
||||
}
|
||||
|
||||
/// Print a g3 status message in progress (no status yet)
|
||||
/// Format: "g3: <message> ..."
|
||||
/// - "g3:" is bold green
|
||||
/// Uses centralized G3Status formatting.
|
||||
pub fn print_g3_progress(&self, message: &str) {
|
||||
println!(
|
||||
"{}{}g3:{}{} {} ...",
|
||||
SetAttribute(Attribute::Bold),
|
||||
SetForegroundColor(Color::Green),
|
||||
ResetColor,
|
||||
SetAttribute(Attribute::Reset),
|
||||
message
|
||||
);
|
||||
G3Status::progress_ln(message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -212,38 +212,18 @@ impl UiWriter for ConsoleUiWriter {
|
||||
}
|
||||
|
||||
fn print_g3_progress(&self, message: &str) {
|
||||
use crossterm::style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor};
|
||||
use std::io::Write;
|
||||
print!(
|
||||
"{}{}g3:{}{} {} ...",
|
||||
SetAttribute(Attribute::Bold),
|
||||
SetForegroundColor(Color::Green),
|
||||
ResetColor,
|
||||
SetAttribute(Attribute::Reset),
|
||||
message
|
||||
);
|
||||
let _ = std::io::stdout().flush();
|
||||
crate::g3_status::G3Status::progress(message);
|
||||
}
|
||||
|
||||
fn print_g3_status(&self, message: &str, status: &str) {
|
||||
use crossterm::style::{Color, ResetColor, SetForegroundColor};
|
||||
use crate::g3_status::Status;
|
||||
let _ = message; // unused now - progress already printed the message
|
||||
let status_colored = if status.starts_with("error") || status == "failed" {
|
||||
format!(
|
||||
" {}[{}]{}",
|
||||
SetForegroundColor(Color::Red),
|
||||
status,
|
||||
ResetColor
|
||||
)
|
||||
} else {
|
||||
format!(" [{}]", status)
|
||||
};
|
||||
println!("{}", status_colored);
|
||||
crate::g3_status::G3Status::status(&Status::from_str(status));
|
||||
}
|
||||
|
||||
fn print_context_thinning(&self, message: &str) {
|
||||
// Simple status line output - message already contains ANSI formatting
|
||||
println!("{}", message);
|
||||
fn print_thin_result(&self, result: &g3_core::ThinResult) {
|
||||
// Use centralized G3Status formatting
|
||||
crate::g3_status::G3Status::thin_result(result);
|
||||
}
|
||||
|
||||
fn print_tool_header(&self, tool_name: &str, _tool_args: Option<&serde_json::Value>) {
|
||||
|
||||
@@ -234,9 +234,9 @@ fn apply_summary_fallback_sequence<W: UiWriter>(
|
||||
|
||||
// Step 1: Try thinnify (first third of context)
|
||||
ui_writer.print_context_status("🥒 Step 1: Trying thinnify...\n");
|
||||
let (thin_msg, chars_saved) = context_window.thin_context(None);
|
||||
thinning_events.push(chars_saved);
|
||||
ui_writer.print_context_thinning(&thin_msg);
|
||||
let thin_result = context_window.thin_context(None);
|
||||
thinning_events.push(thin_result.chars_saved);
|
||||
ui_writer.print_thin_result(&thin_result);
|
||||
|
||||
// Recalculate after thinnify
|
||||
let (new_max, still_needs_reduction) = provider_config::calculate_summary_max_tokens(
|
||||
@@ -253,9 +253,9 @@ fn apply_summary_fallback_sequence<W: UiWriter>(
|
||||
|
||||
// Step 2: Try skinnify (entire context)
|
||||
ui_writer.print_context_status("🦴 Step 2: Trying skinnify...\n");
|
||||
let (skinny_msg, chars_saved) = context_window.thin_context_all(None);
|
||||
thinning_events.push(chars_saved);
|
||||
ui_writer.print_context_thinning(&skinny_msg);
|
||||
let skinny_result = context_window.thin_context_all(None);
|
||||
thinning_events.push(skinny_result.chars_saved);
|
||||
ui_writer.print_thin_result(&skinny_result);
|
||||
|
||||
// Recalculate after skinnify
|
||||
let (final_max, final_needs_reduction) = provider_config::calculate_summary_max_tokens(
|
||||
|
||||
@@ -13,6 +13,26 @@ use tracing::{debug, warn};
|
||||
use crate::paths::get_thinned_dir;
|
||||
use crate::ToolCall;
|
||||
|
||||
/// Result of a context thinning operation.
|
||||
/// Contains semantic data for the UI layer to format.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ThinResult {
|
||||
/// Scope of the thinning operation
|
||||
pub scope: ThinScope,
|
||||
/// Context percentage before thinning
|
||||
pub before_percentage: u32,
|
||||
/// Context percentage after thinning
|
||||
pub after_percentage: u32,
|
||||
/// Number of tool result messages that were thinned
|
||||
pub leaned_count: usize,
|
||||
/// Number of tool calls in assistant messages that were thinned
|
||||
pub tool_call_leaned_count: usize,
|
||||
/// Total characters saved
|
||||
pub chars_saved: usize,
|
||||
/// Whether any changes were made
|
||||
pub had_changes: bool,
|
||||
}
|
||||
|
||||
/// Scope for context thinning operations
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ThinScope {
|
||||
@@ -349,12 +369,12 @@ Format this as a detailed but concise summary that can be used to resume the con
|
||||
/// * `scope` - Controls which messages to process (first third or all)
|
||||
///
|
||||
/// # Returns
|
||||
/// A tuple of (summary message, chars saved)
|
||||
/// A `ThinResult` with semantic data about the operation
|
||||
pub fn thin_context_with_scope(
|
||||
&mut self,
|
||||
session_id: Option<&str>,
|
||||
scope: ThinScope,
|
||||
) -> (String, usize) {
|
||||
) -> ThinResult {
|
||||
let current_percentage = self.percentage_used() as u32;
|
||||
|
||||
// Only update last_thinning_percentage for incremental thinning
|
||||
@@ -373,7 +393,17 @@ Format this as a detailed but concise summary that can be used to resume the con
|
||||
// Determine output directory: use session dir if available, otherwise ~/tmp
|
||||
let tmp_dir = match Self::resolve_thinned_dir(session_id, scope) {
|
||||
Ok(dir) => dir,
|
||||
Err(msg) => return (msg, 0),
|
||||
Err(_) => {
|
||||
return ThinResult {
|
||||
scope,
|
||||
before_percentage: current_percentage,
|
||||
after_percentage: current_percentage,
|
||||
leaned_count: 0,
|
||||
tool_call_leaned_count: 0,
|
||||
chars_saved: 0,
|
||||
had_changes: false,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Collect modifications to apply (avoids borrow checker issues)
|
||||
@@ -409,7 +439,7 @@ Format this as a detailed but concise summary that can be used to resume the con
|
||||
let new_percentage = self.percentage_used() as u32;
|
||||
|
||||
// Build result message
|
||||
self.build_thin_result_message(
|
||||
self.build_thin_result(
|
||||
scope,
|
||||
current_percentage,
|
||||
new_percentage,
|
||||
@@ -466,13 +496,13 @@ Format this as a detailed but concise summary that can be used to resume the con
|
||||
modifications
|
||||
}
|
||||
|
||||
/// Backward-compatible wrapper for thin_context (first third only)
|
||||
pub fn thin_context(&mut self, session_id: Option<&str>) -> (String, usize) {
|
||||
/// Thin context (first third only)
|
||||
pub fn thin_context(&mut self, session_id: Option<&str>) -> ThinResult {
|
||||
self.thin_context_with_scope(session_id, ThinScope::FirstThird)
|
||||
}
|
||||
|
||||
/// Backward-compatible wrapper for thin_context_all (entire history)
|
||||
pub fn thin_context_all(&mut self, session_id: Option<&str>) -> (String, usize) {
|
||||
/// Thin entire context (all messages)
|
||||
pub fn thin_context_all(&mut self, session_id: Option<&str>) -> ThinResult {
|
||||
self.thin_context_with_scope(session_id, ThinScope::All)
|
||||
}
|
||||
|
||||
@@ -697,7 +727,7 @@ Format this as a detailed but concise summary that can be used to resume the con
|
||||
}
|
||||
|
||||
/// Build the result message for thinning operations
|
||||
fn build_thin_result_message(
|
||||
fn build_thin_result(
|
||||
&self,
|
||||
scope: ThinScope,
|
||||
current_percentage: u32,
|
||||
@@ -705,27 +735,17 @@ Format this as a detailed but concise summary that can be used to resume the con
|
||||
leaned_count: usize,
|
||||
tool_call_leaned_count: usize,
|
||||
chars_saved: usize,
|
||||
) -> (String, usize) {
|
||||
// Nothing was thinned
|
||||
if leaned_count == 0 && tool_call_leaned_count == 0 {
|
||||
let scope_desc = match scope {
|
||||
ThinScope::FirstThird => "",
|
||||
ThinScope::All => " (full)",
|
||||
};
|
||||
let msg = format!(
|
||||
"\x1b[1;32mg3:\x1b[0m thinning context{} ... {}% ... \x1b[38;5;208m[no changes]\x1b[0m",
|
||||
scope_desc, current_percentage
|
||||
);
|
||||
return (msg, 0);
|
||||
) -> ThinResult {
|
||||
let had_changes = leaned_count > 0 || tool_call_leaned_count > 0;
|
||||
ThinResult {
|
||||
scope,
|
||||
before_percentage: current_percentage,
|
||||
after_percentage: new_percentage,
|
||||
leaned_count,
|
||||
tool_call_leaned_count,
|
||||
chars_saved: if had_changes { chars_saved } else { 0 },
|
||||
had_changes,
|
||||
}
|
||||
|
||||
// Format: "g3: thinning context ... 70% -> 40% ... [done]"
|
||||
// with "g3:" and "[done]" in bold green
|
||||
let msg = format!(
|
||||
"\x1b[1;32mg3:\x1b[0m thinning context ... {}% -> {}% ... \x1b[1;32m[done]\x1b[0m",
|
||||
current_percentage, new_percentage
|
||||
);
|
||||
(msg, chars_saved)
|
||||
}
|
||||
|
||||
/// Recalculate token usage based on current conversation history
|
||||
|
||||
@@ -35,7 +35,7 @@ pub use session_continuation::{
|
||||
pub use task_result::TaskResult;
|
||||
|
||||
// Re-export context window types
|
||||
pub use context_window::{ContextWindow, ThinScope};
|
||||
pub use context_window::{ContextWindow, ThinResult, ThinScope};
|
||||
|
||||
// Export agent prompt generation for CLI use
|
||||
pub use prompts::get_agent_system_prompt;
|
||||
@@ -500,8 +500,8 @@ impl<W: UiWriter> Agent<W> {
|
||||
// Step 1: Try thinnify (first third of context)
|
||||
self.ui_writer
|
||||
.print_context_status("🥒 Step 1: Trying thinnify...\n");
|
||||
let thin_msg = self.do_thin_context();
|
||||
self.ui_writer.print_context_thinning(&thin_msg);
|
||||
let thin_result = self.do_thin_context();
|
||||
self.ui_writer.print_thin_result(&thin_result);
|
||||
|
||||
// Recalculate after thinnify
|
||||
let (new_max, still_needs_reduction) =
|
||||
@@ -516,8 +516,8 @@ impl<W: UiWriter> Agent<W> {
|
||||
// Step 2: Try skinnify (entire context)
|
||||
self.ui_writer
|
||||
.print_context_status("🦴 Step 2: Trying skinnify...\n");
|
||||
let skinny_msg = self.do_thin_context_all();
|
||||
self.ui_writer.print_context_thinning(&skinny_msg);
|
||||
let skinny_result = self.do_thin_context_all();
|
||||
self.ui_writer.print_thin_result(&skinny_result);
|
||||
|
||||
// Recalculate after skinnify
|
||||
let (final_max, final_needs_reduction) =
|
||||
@@ -1084,32 +1084,32 @@ impl<W: UiWriter> Agent<W> {
|
||||
}
|
||||
}
|
||||
/// Manually trigger context thinning regardless of thresholds
|
||||
pub fn force_thin(&mut self) -> String {
|
||||
pub fn force_thin(&mut self) -> ThinResult {
|
||||
debug!("Manual context thinning triggered");
|
||||
self.do_thin_context()
|
||||
}
|
||||
|
||||
/// Manually trigger context thinning for the ENTIRE context window
|
||||
/// Unlike force_thin which only processes the first third, this processes all messages
|
||||
pub fn force_thin_all(&mut self) -> String {
|
||||
pub fn force_thin_all(&mut self) -> ThinResult {
|
||||
debug!("Manual full context skinnifying triggered");
|
||||
self.do_thin_context_all()
|
||||
}
|
||||
|
||||
/// Internal helper: thin context and track the event
|
||||
fn do_thin_context(&mut self) -> String {
|
||||
let (message, chars_saved) = self.context_window.thin_context(self.session_id.as_deref());
|
||||
self.thinning_events.push(chars_saved);
|
||||
message
|
||||
fn do_thin_context(&mut self) -> ThinResult {
|
||||
let result = self.context_window.thin_context(self.session_id.as_deref());
|
||||
self.thinning_events.push(result.chars_saved);
|
||||
result
|
||||
}
|
||||
|
||||
/// Internal helper: thin all context and track the event
|
||||
fn do_thin_context_all(&mut self) -> String {
|
||||
let (message, chars_saved) = self
|
||||
fn do_thin_context_all(&mut self) -> ThinResult {
|
||||
let result = self
|
||||
.context_window
|
||||
.thin_context_all(self.session_id.as_deref());
|
||||
self.thinning_events.push(chars_saved);
|
||||
message
|
||||
self.thinning_events.push(result.chars_saved);
|
||||
result
|
||||
}
|
||||
|
||||
/// Ensure context window has capacity before streaming.
|
||||
@@ -1127,8 +1127,8 @@ impl<W: UiWriter> Agent<W> {
|
||||
self.context_window.percentage_used() as u32
|
||||
));
|
||||
|
||||
let thin_summary = self.do_thin_context();
|
||||
self.ui_writer.print_context_thinning(&thin_summary);
|
||||
let thin_result = self.do_thin_context();
|
||||
self.ui_writer.print_thin_result(&thin_result);
|
||||
|
||||
if !self.context_window.should_compact() {
|
||||
self.ui_writer.print_g3_status("thinning", "resolved");
|
||||
@@ -2096,8 +2096,8 @@ Skip if nothing new. Be brief."#;
|
||||
|
||||
// Thin context if needed before tool execution
|
||||
if self.context_window.should_thin() {
|
||||
let thin_summary = self.do_thin_context();
|
||||
self.ui_writer.print_context_thinning(&thin_summary);
|
||||
let thin_result = self.do_thin_context();
|
||||
self.ui_writer.print_thin_result(&thin_result);
|
||||
}
|
||||
|
||||
// Calculate new content to display (skip already-shown text)
|
||||
|
||||
@@ -27,8 +27,11 @@ pub trait UiWriter: Send + Sync {
|
||||
/// - "g3:" should be bold green, "failed"/"error" status should be red
|
||||
fn print_g3_status(&self, message: &str, status: &str);
|
||||
|
||||
/// Print a context thinning success message with highlight and animation
|
||||
fn print_context_thinning(&self, message: &str);
|
||||
/// Print a context thinning result
|
||||
fn print_thin_result(&self, result: &crate::ThinResult);
|
||||
|
||||
/// Print a context thinning message (legacy - for pre-formatted messages)
|
||||
fn print_context_thinning(&self, _message: &str) {}
|
||||
|
||||
/// Print a tool execution header
|
||||
fn print_tool_header(&self, tool_name: &str, tool_args: Option<&serde_json::Value>);
|
||||
@@ -136,7 +139,7 @@ impl UiWriter for NullUiWriter {
|
||||
fn print_context_status(&self, _message: &str) {}
|
||||
fn print_g3_progress(&self, _message: &str) {}
|
||||
fn print_g3_status(&self, _message: &str, _status: &str) {}
|
||||
fn print_context_thinning(&self, _message: &str) {}
|
||||
fn print_thin_result(&self, _result: &crate::ThinResult) {}
|
||||
fn print_tool_header(&self, _tool_name: &str, _tool_args: Option<&serde_json::Value>) {}
|
||||
fn print_tool_arg(&self, _key: &str, _value: &str) {}
|
||||
fn print_tool_output_header(&self) {}
|
||||
|
||||
@@ -274,7 +274,7 @@ impl UiWriter for TrackingUiWriter {
|
||||
fn print_context_status(&self, _message: &str) {}
|
||||
fn print_g3_progress(&self, _message: &str) {}
|
||||
fn print_g3_status(&self, _message: &str, _status: &str) {}
|
||||
fn print_context_thinning(&self, _message: &str) {}
|
||||
fn print_thin_result(&self, _result: &g3_core::ThinResult) {}
|
||||
|
||||
fn print_tool_header(&self, _tool_name: &str, _tool_args: Option<&serde_json::Value>) {
|
||||
// Count each tool call
|
||||
|
||||
@@ -69,14 +69,13 @@ fn test_thin_context_basic() {
|
||||
|
||||
// Trigger thinning at 50%
|
||||
context.used_tokens = 5000;
|
||||
let (summary, _chars_saved) = context.thin_context(None);
|
||||
let result = context.thin_context(None);
|
||||
|
||||
println!("Thinning summary: {}", summary);
|
||||
println!("Thinning result: {:?}", result);
|
||||
|
||||
// Should show the new format with percentage change
|
||||
assert!(summary.contains("g3:"), "Summary was: {}", summary);
|
||||
assert!(summary.contains("thinning context"));
|
||||
assert!(summary.contains("[done]"));
|
||||
// Should have made changes
|
||||
assert!(result.had_changes, "Expected thinning to make changes");
|
||||
assert!(result.chars_saved > 0, "Expected chars to be saved");
|
||||
|
||||
// Check that the large tool results were replaced
|
||||
let first_third_end = context.conversation_history.len() / 3;
|
||||
@@ -127,14 +126,13 @@ fn test_thin_write_file_tool_calls() {
|
||||
|
||||
// Trigger thinning at 50%
|
||||
context.used_tokens = 5000;
|
||||
let (summary, _chars_saved) = context.thin_context(None);
|
||||
let result = context.thin_context(None);
|
||||
|
||||
println!("Thinning summary: {}", summary);
|
||||
println!("Thinning result: {:?}", result);
|
||||
|
||||
// Should show the new format with percentage change
|
||||
assert!(summary.contains("g3:"));
|
||||
assert!(summary.contains("thinning context"));
|
||||
assert!(summary.contains("[done]"));
|
||||
// Should have made changes
|
||||
assert!(result.had_changes, "Expected thinning to make changes");
|
||||
assert!(result.chars_saved > 0, "Expected chars to be saved");
|
||||
|
||||
// Check that the large content was replaced with a file reference
|
||||
let first_third_end = context.conversation_history.len() / 3;
|
||||
@@ -189,14 +187,13 @@ fn test_thin_str_replace_tool_calls() {
|
||||
|
||||
// Trigger thinning at 50%
|
||||
context.used_tokens = 5000;
|
||||
let (summary, _chars_saved) = context.thin_context(None);
|
||||
let result = context.thin_context(None);
|
||||
|
||||
println!("Thinning summary: {}", summary);
|
||||
println!("Thinning result: {:?}", result);
|
||||
|
||||
// Should show the new format with percentage change
|
||||
assert!(summary.contains("g3:"));
|
||||
assert!(summary.contains("thinning context"));
|
||||
assert!(summary.contains("[done]"));
|
||||
// Should have made changes
|
||||
assert!(result.had_changes, "Expected thinning to make changes");
|
||||
assert!(result.chars_saved > 0, "Expected chars to be saved");
|
||||
|
||||
// Check that the large diff was replaced with a file reference
|
||||
let first_third_end = context.conversation_history.len() / 3;
|
||||
@@ -225,12 +222,12 @@ fn test_thin_context_no_large_results() {
|
||||
}
|
||||
|
||||
context.used_tokens = 5000;
|
||||
let (summary, _chars_saved) = context.thin_context(None);
|
||||
let result = context.thin_context(None);
|
||||
|
||||
// Should report no large results found
|
||||
assert!(summary.contains("g3:"));
|
||||
assert!(summary.contains("thinning context"));
|
||||
assert!(summary.contains("[no changes]"));
|
||||
// Should report no changes (no large results found)
|
||||
assert!(!result.had_changes, "Expected no changes");
|
||||
assert_eq!(result.chars_saved, 0, "Expected no chars saved");
|
||||
assert_eq!(result.leaned_count, 0, "Expected no messages thinned");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -256,11 +253,10 @@ fn test_thin_context_only_affects_first_third() {
|
||||
}
|
||||
|
||||
context.used_tokens = 5000;
|
||||
let (summary, _chars_saved) = context.thin_context(None);
|
||||
let result = context.thin_context(None);
|
||||
|
||||
// Should show the new format with percentage change
|
||||
assert!(summary.contains("g3:"));
|
||||
assert!(summary.contains("[done]"));
|
||||
// Should have made changes
|
||||
assert!(result.had_changes, "Expected thinning to make changes");
|
||||
|
||||
// Check that messages after the first third are NOT thinned
|
||||
let first_third_end = context.conversation_history.len() / 3;
|
||||
|
||||
@@ -30,9 +30,9 @@ fn test_todo_read_results_not_thinned() {
|
||||
|
||||
// Trigger thinning at 50%
|
||||
context.used_tokens = 5000;
|
||||
let (summary, _chars_saved) = context.thin_context(None);
|
||||
let result = context.thin_context(None);
|
||||
|
||||
println!("Thinning summary: {}", summary);
|
||||
println!("Thinning result: {:?}", result);
|
||||
|
||||
// Check that the TODO result was NOT thinned
|
||||
let first_third_end = context.conversation_history.len() / 3;
|
||||
@@ -87,9 +87,9 @@ fn test_todo_write_results_not_thinned() {
|
||||
|
||||
// Trigger thinning at 50%
|
||||
context.used_tokens = 5000;
|
||||
let (summary, _chars_saved) = context.thin_context(None);
|
||||
let result = context.thin_context(None);
|
||||
|
||||
println!("Thinning summary: {}", summary);
|
||||
println!("Thinning result: {:?}", result);
|
||||
|
||||
// Check that the TODO write result was NOT thinned
|
||||
let first_third_end = context.conversation_history.len() / 3;
|
||||
@@ -135,14 +135,13 @@ fn test_non_todo_results_still_thinned() {
|
||||
|
||||
// Trigger thinning at 50%
|
||||
context.used_tokens = 5000;
|
||||
let (summary, _chars_saved) = context.thin_context(None);
|
||||
let result = context.thin_context(None);
|
||||
|
||||
println!("Thinning summary: {}", summary);
|
||||
println!("Thinning result: {:?}", result);
|
||||
|
||||
// Should show the new format with percentage change (indicating thinning happened)
|
||||
assert!(summary.contains("g3:"), "Non-TODO results should be thinned");
|
||||
assert!(summary.contains("thinning context"));
|
||||
assert!(summary.contains("[done]"));
|
||||
// Should have made changes (non-TODO results should be thinned)
|
||||
assert!(result.had_changes, "Non-TODO results should be thinned");
|
||||
assert!(result.chars_saved > 0, "Expected chars to be saved");
|
||||
|
||||
// Check that the result was actually thinned
|
||||
let first_third_end = context.conversation_history.len() / 3;
|
||||
@@ -184,7 +183,7 @@ fn test_todo_read_with_spaces_in_tool_name() {
|
||||
|
||||
// Trigger thinning
|
||||
context.used_tokens = 5000;
|
||||
let (_summary, _chars_saved) = context.thin_context(None);
|
||||
let _result = context.thin_context(None);
|
||||
|
||||
// Verify TODO result was not thinned
|
||||
let first_third_end = context.conversation_history.len() / 3;
|
||||
|
||||
@@ -54,7 +54,7 @@ impl UiWriter for MockUiWriter {
|
||||
}
|
||||
fn print_g3_progress(&self, _message: &str) {}
|
||||
fn print_g3_status(&self, _message: &str, _status: &str) {}
|
||||
fn print_context_thinning(&self, _message: &str) {}
|
||||
fn print_thin_result(&self, _result: &g3_core::ThinResult) {}
|
||||
fn print_tool_header(&self, _tool_name: &str, _tool_args: Option<&serde_json::Value>) {}
|
||||
fn print_tool_arg(&self, _key: &str, _value: &str) {}
|
||||
fn print_tool_output_header(&self) {}
|
||||
|
||||
@@ -242,8 +242,16 @@ impl g3_core::ui_writer::UiWriter for PlannerUiWriter {
|
||||
println!("g3: {} ... [{}]", message, status);
|
||||
}
|
||||
|
||||
fn print_context_thinning(&self, message: &str) {
|
||||
println!("🗜️ {}", message);
|
||||
fn print_thin_result(&self, result: &g3_core::ThinResult) {
|
||||
// Simple text output for planner
|
||||
if result.had_changes {
|
||||
println!(
|
||||
"🗜️ thinning context ... {}% -> {}% ... [done]",
|
||||
result.before_percentage, result.after_percentage
|
||||
);
|
||||
} else {
|
||||
println!("🗜️ thinning context ... {}% ... [no changes]", result.before_percentage);
|
||||
}
|
||||
}
|
||||
|
||||
fn print_tool_header(&self, tool_name: &str, tool_args: Option<&serde_json::Value>) {
|
||||
|
||||
Reference in New Issue
Block a user