Work through TODO fixes #8

Merged
harry merged 8 commits from todo-fixes into main 2026-05-25 13:13:25 +01:00
4 changed files with 72 additions and 2 deletions
Showing only changes of commit 53f06b604f - Show all commits

View File

@@ -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 - MCP clients can now call `scratchpad_delete` with a scratchpad name
to remove a shared project scratchpad. 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 ### Fixed
- Closing an agent now escalates from SIGTERM to SIGKILL when needed, - Closing an agent now escalates from SIGTERM to SIGKILL when needed,
so agents that ignore SIGTERM disappear from the running tab bar so agents that ignore SIGTERM disappear from the running tab bar

View File

@@ -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] - [ ] Codex idle detection seems to trigger too soon, see below [CODEX IDLE]
- [ ] Issue with mcp timing out [MCP TIMEOUT] - [ ] 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. - [ ] When opening a codex sub agent, the message gets input to the field, but the message is never submitted.

View File

@@ -7,6 +7,7 @@ import (
"sync" "sync"
"syscall" "syscall"
"time" "time"
"unicode"
"github.com/hjbdev/patterm/internal/mcp" "github.com/hjbdev/patterm/internal/mcp"
"github.com/hjbdev/patterm/internal/preset" "github.com/hjbdev/patterm/internal/preset"
@@ -398,7 +399,7 @@ func (h *toolHost) GetProcessOutput(callerID, processID, mode string, sinceOffse
if c.Kind == KindAgent { if c.Kind == KindAgent {
txt = applyChromeTrim(txt, h.chromeHintsFor(c.PresetRef)) txt = applyChromeTrim(txt, h.chromeHintsFor(c.PresetRef))
} }
out.Content = txt out.Content = normalizeGridText(txt)
return out, nil return out, nil
case "stream": case "stream":
b, end := c.StreamRead(sinceOffset) b, end := c.StreamRead(sinceOffset)
@@ -1018,6 +1019,30 @@ func stripANSI(s string) string {
return ansiRegexp.ReplaceAllString(s, "") 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 // stripANSIBytes is the byte-slice form of stripANSI. Skips the
// string conversion and the regex DFA — useful when the caller will // string conversion and the regex DFA — useful when the caller will
// itself walk the result line-by-line (SearchOutput) or feed it to a // itself walk the result line-by-line (SearchOutput) or feed it to a

View File

@@ -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)
}
})
}
}