Normalize whitespace in grid get_process_output to save tokens
Grid snapshots pad every row to the full terminal width and leave the bottom of the screen blank, so MCP grid reads carried a lot of dead whitespace. Add normalizeGridText (CRLF/lone-CR to LF, right-trim each line, collapse blank runs to a single blank, drop leading/trailing blanks) and apply it to the grid branch of GetProcessOutput only. Stream output, raw output, and WaitForPattern matching are untouched. Resolves the terminal-read newline/token-waste TODO item.
This commit is contained in:
@@ -10,6 +10,11 @@ loosely follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
- MCP clients can now call `scratchpad_delete` with a scratchpad name
|
||||
to remove a shared project scratchpad.
|
||||
|
||||
### Changed
|
||||
- Grid-mode `get_process_output` now returns whitespace-normalized
|
||||
text to avoid sending padded terminal rows and repeated blank lines
|
||||
over MCP.
|
||||
|
||||
### Fixed
|
||||
- Closing an agent now escalates from SIGTERM to SIGKILL when needed,
|
||||
so agents that ignore SIGTERM disappear from the running tab bar
|
||||
|
||||
1
TODO.md
1
TODO.md
@@ -1,4 +1,3 @@
|
||||
- [ ] We should deduplicate /r/n newlines or /n newlines to save tokens on mcp responses for terminal reads.
|
||||
- [ ] Codex idle detection seems to trigger too soon, see below [CODEX IDLE]
|
||||
- [ ] Issue with mcp timing out [MCP TIMEOUT]
|
||||
- [ ] When opening a codex sub agent, the message gets input to the field, but the message is never submitted.
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/hjbdev/patterm/internal/mcp"
|
||||
"github.com/hjbdev/patterm/internal/preset"
|
||||
@@ -398,7 +399,7 @@ func (h *toolHost) GetProcessOutput(callerID, processID, mode string, sinceOffse
|
||||
if c.Kind == KindAgent {
|
||||
txt = applyChromeTrim(txt, h.chromeHintsFor(c.PresetRef))
|
||||
}
|
||||
out.Content = txt
|
||||
out.Content = normalizeGridText(txt)
|
||||
return out, nil
|
||||
case "stream":
|
||||
b, end := c.StreamRead(sinceOffset)
|
||||
@@ -1018,6 +1019,30 @@ func stripANSI(s string) string {
|
||||
return ansiRegexp.ReplaceAllString(s, "")
|
||||
}
|
||||
|
||||
func normalizeGridText(s string) string {
|
||||
s = strings.ReplaceAll(s, "\r\n", "\n")
|
||||
s = strings.ReplaceAll(s, "\r", "\n")
|
||||
|
||||
lines := strings.Split(s, "\n")
|
||||
out := make([]string, 0, len(lines))
|
||||
pendingBlank := false
|
||||
for _, line := range lines {
|
||||
line = strings.TrimRightFunc(line, unicode.IsSpace)
|
||||
if line == "" {
|
||||
if len(out) > 0 {
|
||||
pendingBlank = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
if pendingBlank {
|
||||
out = append(out, "")
|
||||
pendingBlank = false
|
||||
}
|
||||
out = append(out, line)
|
||||
}
|
||||
return strings.Join(out, "\n")
|
||||
}
|
||||
|
||||
// stripANSIBytes is the byte-slice form of stripANSI. Skips the
|
||||
// string conversion and the regex DFA — useful when the caller will
|
||||
// itself walk the result line-by-line (SearchOutput) or feed it to a
|
||||
|
||||
@@ -104,3 +104,44 @@ func TestStripANSIBytesEquivalence(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeGridText(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "line endings",
|
||||
in: "one\r\ntwo\rthree",
|
||||
want: "one\ntwo\nthree",
|
||||
},
|
||||
{
|
||||
name: "trailing whitespace",
|
||||
in: "one \ntwo\t\t\nthree",
|
||||
want: "one\ntwo\nthree",
|
||||
},
|
||||
{
|
||||
name: "collapse blank runs",
|
||||
in: "one\n\n\n two\n \n\t\nthree",
|
||||
want: "one\n\n two\n\nthree",
|
||||
},
|
||||
{
|
||||
name: "trim leading and trailing blanks",
|
||||
in: "\n \n\t\none\n\n",
|
||||
want: "one",
|
||||
},
|
||||
{
|
||||
name: "already clean",
|
||||
in: "one\n\ntwo\nthree",
|
||||
want: "one\n\ntwo\nthree",
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := normalizeGridText(tc.in); got != tc.want {
|
||||
t.Fatalf("normalizeGridText(%q) = %q, want %q", tc.in, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user