Move fixed_filter_json from g3-core to g3-cli

Properly separates UI display concern from core library:
- fixed_filter_json module now lives in g3-cli (UI layer)
- UiWriter trait gains filter_json_tool_calls() and reset_json_filter() methods
- g3-core delegates filtering to UI layer via trait methods
- Different UiWriter implementations can choose their own filtering behavior
- ConsoleUiWriter filters JSON tool calls for clean terminal display
- MachineUiWriter/NullUiWriter use default pass-through

Benefits:
- Proper separation of concerns
- Core stays clean without display-specific logic
- Testability - filter can be tested independently in g3-cli
This commit is contained in:
Dhanji R. Prasanna
2025-12-22 10:32:21 +11:00
parent fbf31e5f68
commit 01a5284d6d
14 changed files with 297 additions and 183 deletions

View File

@@ -35,50 +35,18 @@ pub struct LogParser;
impl LogParser {
/// Parse logs from a workspace directory
pub fn parse_logs(workspace: &Path) -> Result<Vec<LogEntry>> {
let logs_dir = workspace.join("logs");
if !logs_dir.exists() {
return Ok(Vec::new());
}
let mut entries = Vec::new();
// Read all JSON log files
for entry in fs::read_dir(&logs_dir).context("Failed to read logs directory")? {
let entry = entry?;
let path = entry.path();
// Try new .g3/sessions/ directory first
let g3_sessions_dir = workspace.join(".g3").join("sessions");
if g3_sessions_dir.exists() {
Self::parse_sessions_dir(&g3_sessions_dir, &mut entries)?;
}
if path.extension().and_then(|s| s.to_str()) == Some("json") {
if let Ok(content) = fs::read_to_string(&path) {
if let Ok(json) = serde_json::from_str::<Value>(&content) {
// Try to parse as a log session
if let Some(messages) = json.get("messages").and_then(|m| m.as_array()) {
for msg in messages {
entries.push(LogEntry {
timestamp: msg
.get("timestamp")
.and_then(|t| t.as_str())
.and_then(|s| DateTime::parse_from_rfc3339(s).ok())
.map(|dt| dt.with_timezone(&Utc)),
role: msg
.get("role")
.and_then(|r| r.as_str())
.map(String::from),
content: msg
.get("content")
.and_then(|c| c.as_str())
.map(String::from),
tool_calls: msg
.get("tool_calls")
.and_then(|tc| tc.as_array())
.map(|arr| arr.clone()),
raw: msg.clone(),
});
}
}
}
}
}
// Also check old logs/ directory for backwards compatibility
let logs_dir = workspace.join("logs");
if logs_dir.exists() {
Self::parse_logs_dir(&logs_dir, &mut entries)?;
}
// Sort by timestamp
@@ -92,6 +60,76 @@ impl LogParser {
Ok(entries)
}
/// Parse logs from the new .g3/sessions/ directory structure
fn parse_sessions_dir(sessions_dir: &Path, entries: &mut Vec<LogEntry>) -> Result<()> {
for session_entry in fs::read_dir(sessions_dir).context("Failed to read sessions directory")? {
let session_entry = session_entry?;
let session_path = session_entry.path();
if session_path.is_dir() {
// Look for session.json in each session directory
let session_file = session_path.join("session.json");
if session_file.exists() {
Self::parse_session_file(&session_file, entries)?;
}
}
}
Ok(())
}
/// Parse logs from the old logs/ directory structure
fn parse_logs_dir(logs_dir: &Path, entries: &mut Vec<LogEntry>) -> Result<()> {
// Read all JSON log files
for entry in fs::read_dir(&logs_dir).context("Failed to read logs directory")? {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("json") {
Self::parse_session_file(&path, entries)?;
}
}
Ok(())
}
/// Parse a single session JSON file
fn parse_session_file(path: &Path, entries: &mut Vec<LogEntry>) -> Result<()> {
if let Ok(content) = fs::read_to_string(path) {
if let Ok(json) = serde_json::from_str::<Value>(&content) {
// Try to parse as a log session - check both "messages" and "context_window.conversation_history"
let messages = json.get("messages").and_then(|m| m.as_array())
.or_else(|| json.get("context_window")
.and_then(|cw| cw.get("conversation_history"))
.and_then(|ch| ch.as_array()));
if let Some(messages) = messages {
for msg in messages {
entries.push(LogEntry {
timestamp: msg
.get("timestamp")
.and_then(|t| t.as_str())
.and_then(|s| DateTime::parse_from_rfc3339(s).ok())
.map(|dt| dt.with_timezone(&Utc)),
role: msg
.get("role")
.and_then(|r| r.as_str())
.map(String::from),
content: msg
.get("content")
.and_then(|c| c.as_str())
.map(String::from),
tool_calls: msg
.get("tool_calls")
.and_then(|tc| tc.as_array())
.map(|arr| arr.clone()),
raw: msg.clone(),
});
}
}
}
}
Ok(())
}
/// Extract chat messages from log entries
pub fn extract_chat_messages(entries: &[LogEntry]) -> Vec<ChatMessage> {
entries