Fix extra newlines before tool calls in JSON filter

The JSON tool call filter was outputting newlines immediately as they
were encountered. When the LLM output contained multiple newlines before
a tool call, each newline was output before the tool call JSON was
detected and suppressed, leaving orphaned blank lines in the output.

Changes:
- Add pending_newlines field to FilterState to buffer newlines at line start
- First newline after content is output immediately, subsequent ones buffered
- When tool call confirmed, pending_newlines cleared (suppressing extra blanks)
- When not a tool call, pending_newlines output with the buffer
- Add flush_json_tool_filter() to flush pending content at end of streaming
- Update tests to reflect new behavior
- Add tests for newline suppression behavior
This commit is contained in:
Dhanji R. Prasanna
2026-01-11 17:04:27 +05:30
parent 9509e51708
commit 2fbdac7aa9
4 changed files with 158 additions and 17 deletions

View File

@@ -3,7 +3,7 @@
//! These tests hammer the filter with malformed JSON, partial tool calls,
//! edge cases, and adversarial inputs to ensure robustness.
use g3_cli::filter_json::{filter_json_tool_calls, reset_json_tool_state};
use g3_cli::filter_json::{filter_json_tool_calls, flush_json_tool_filter, reset_json_tool_state};
// ============================================================================
// Malformed JSON Tests
@@ -478,7 +478,9 @@ fn test_empty_input() {
#[test]
fn test_just_newline() {
reset_json_tool_state();
assert_eq!(filter_json_tool_calls("\n"), "\n");
let result = filter_json_tool_calls("\n");
let flushed = flush_json_tool_filter();
assert_eq!(format!("{}{}", result, flushed), "\n");
}
#[test]

View File

@@ -211,7 +211,8 @@ After"#;
{"tool": "final_output", "args": {"summary": "Task completed successfully"}}"#;
let result = filter_json_tool_calls(input);
let expected = "\n";
// Leading newline before tool call at start of input is suppressed
let expected = "";
assert_eq!(result, expected);
}
@@ -509,13 +510,13 @@ End"#;
let result = filter_json_tool_calls(input);
// The tool call starts on its own line after the read_file output,
// so it should be filtered out. Only the read_file output should remain.
// The tool call starts on its own line after the read_file output.
// The tool call is filtered out, and extra newlines before it are suppressed.
// Only one newline remains (the line ending after "1ms").
let expected = r#"┌─ read_file | ./crates/g3-cli/src/ui_writer_impl.rs [13000..13300]
│ }
│ (11 lines)
└─ ⚡️ 1ms
"#;
assert_eq!(
result, expected,
@@ -538,9 +539,10 @@ End"#;
let result = filter_json_tool_calls(input);
// The shell tool call starts at line beginning, so it should be filtered out
// Only the surrounding text should remain
// Note: The tool call is on its own line, so filtering leaves an empty line
let expected = "Let me create a test case:\n\n\n\nDone.";
// Only the surrounding text should remain.
// Extra newlines before the tool call are suppressed (one blank line before
// becomes just the line ending), but newlines after are preserved.
let expected = "Let me create a test case:\n\n\nDone.";
assert_eq!(
result, expected,