Files
multica/server/internal/util/text.go
Bohan Jiang 936ccce8fa fix(comments): unescape \n in agent task-completion output (#1850)
PR #1744 fixed literal `\n\n` rendering for the CLI surfaces (`issue
create / update --description`, `issue comment add --content`) but the
agent-completion path bypasses the CLI entirely: the daemon POSTs the
agent's stdout to `/api/daemon/tasks/:id/complete`, and `TaskService.
CompleteTask` writes `payload.Output` straight into `createAgentComment`
and `CreateChatMessage` without decoding. Models (e.g. Codex) routinely
emit Python/JSON-style `\n` literals in their final output, which then
land in the DB as the 4-char escape sequence and render as one wall of
text in the issue/chat panel — exactly the bug report in #1820.

- Move `unescapeFlagText` from `server/cmd/multica/cmd_issue.go` to
  `server/internal/util/text.go` as `UnescapeBackslashEscapes` so the
  CLI and the service layer share one implementation. The full
  contract-boundary test suite moves with it.
- Apply `UnescapeBackslashEscapes` to `payload.Output` before it
  reaches `createAgentComment` and `CreateChatMessage` in
  `TaskService.CompleteTask`. Same `\n / \r / \t / \\` decoding as the
  CLI; other escape sequences (`\d`, `\w`, `\u`, etc.) pass through
  verbatim so regex/format strings in agent output survive.

Closes #1820
2026-04-29 17:05:17 +08:00

49 lines
1.3 KiB
Go

package util
import "strings"
// UnescapeBackslashEscapes decodes the common backslash escape sequences
// (\n, \r, \t, \\) that LLM agents routinely emit as 4-character literals
// because Python/JSON-style string conventions are their default. The same
// helper is used by the CLI to fix bash-double-quote bodies (where the shell
// doesn't expand \n) and by the daemon-task completion path to fix raw agent
// stdout that arrives with literal `\n\n` between paragraphs.
//
// Only \n / \r / \t / \\ are decoded. Other escape sequences (\d, \w, \s,
// \u, \0, \", etc.) pass through verbatim so regex literals and printf
// format strings survive without surprise mutation. Callers that need the
// literal 4-char sequence intact should bypass this helper entirely (the CLI
// exposes --content-stdin / --description-stdin for that case).
func UnescapeBackslashEscapes(s string) string {
if !strings.ContainsRune(s, '\\') {
return s
}
var b strings.Builder
b.Grow(len(s))
for i := 0; i < len(s); i++ {
c := s[i]
if c == '\\' && i+1 < len(s) {
switch s[i+1] {
case 'n':
b.WriteByte('\n')
i++
continue
case 'r':
b.WriteByte('\r')
i++
continue
case 't':
b.WriteByte('\t')
i++
continue
case '\\':
b.WriteByte('\\')
i++
continue
}
}
b.WriteByte(c)
}
return b.String()
}