From 6200e242b285a970548ead3d04a7f2d84c555f45 Mon Sep 17 00:00:00 2001 From: scoootscooob Date: Mon, 2 Mar 2026 14:19:52 -0800 Subject: [PATCH] fix(auto-reply): preserve newlines in stripInlineStatus and extractInlineSimpleCommand The /\s+/g whitespace normalizer collapsed newlines along with spaces/tabs, destroying paragraph structure in multi-line messages before they reached the LLM. Use /[^\S\n]+/g to only collapse horizontal whitespace while preserving line breaks. Closes #32216 Co-Authored-By: Claude Opus 4.6 --- src/auto-reply/reply/reply-inline.test.ts | 54 +++++++++++++++++++++++ src/auto-reply/reply/reply-inline.ts | 12 ++++- 2 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 src/auto-reply/reply/reply-inline.test.ts diff --git a/src/auto-reply/reply/reply-inline.test.ts b/src/auto-reply/reply/reply-inline.test.ts new file mode 100644 index 00000000000..a35616692c2 --- /dev/null +++ b/src/auto-reply/reply/reply-inline.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from "vitest"; +import { extractInlineSimpleCommand, stripInlineStatus } from "./reply-inline.js"; + +describe("stripInlineStatus", () => { + it("strips /status directive from message", () => { + const result = stripInlineStatus("/status hello world"); + expect(result.cleaned).toBe("hello world"); + expect(result.didStrip).toBe(true); + }); + + it("preserves newlines in multi-line messages", () => { + const result = stripInlineStatus("first line\nsecond line\nthird line"); + expect(result.cleaned).toBe("first line\nsecond line\nthird line"); + expect(result.didStrip).toBe(false); + }); + + it("preserves newlines when stripping /status", () => { + const result = stripInlineStatus("/status\nfirst paragraph\n\nsecond paragraph"); + expect(result.cleaned).toBe("first paragraph\n\nsecond paragraph"); + expect(result.didStrip).toBe(true); + }); + + it("collapses horizontal whitespace but keeps newlines", () => { + const result = stripInlineStatus("hello world\n indented line"); + expect(result.cleaned).toBe("hello world\n indented line"); + // didStrip is true because whitespace normalization changed the string + expect(result.didStrip).toBe(true); + }); + + it("returns empty string for whitespace-only input", () => { + const result = stripInlineStatus(" "); + expect(result.cleaned).toBe(""); + expect(result.didStrip).toBe(false); + }); +}); + +describe("extractInlineSimpleCommand", () => { + it("extracts /help command", () => { + const result = extractInlineSimpleCommand("/help some question"); + expect(result?.command).toBe("/help"); + expect(result?.cleaned).toBe("some question"); + }); + + it("preserves newlines after extracting command", () => { + const result = extractInlineSimpleCommand("/help first line\nsecond line"); + expect(result?.command).toBe("/help"); + expect(result?.cleaned).toBe("first line\nsecond line"); + }); + + it("returns null for empty body", () => { + expect(extractInlineSimpleCommand("")).toBeNull(); + expect(extractInlineSimpleCommand(undefined)).toBeNull(); + }); +}); diff --git a/src/auto-reply/reply/reply-inline.ts b/src/auto-reply/reply/reply-inline.ts index dc3c4e97425..6fe84df394e 100644 --- a/src/auto-reply/reply/reply-inline.ts +++ b/src/auto-reply/reply/reply-inline.ts @@ -24,7 +24,10 @@ export function extractInlineSimpleCommand(body?: string): { if (!command) { return null; } - const cleaned = body.replace(match[0], " ").replace(/\s+/g, " ").trim(); + const cleaned = body + .replace(match[0], " ") + .replace(/[^\S\n]+/g, " ") + .trim(); return { command, cleaned }; } @@ -36,6 +39,11 @@ export function stripInlineStatus(body: string): { if (!trimmed) { return { cleaned: "", didStrip: false }; } - const cleaned = trimmed.replace(INLINE_STATUS_RE, " ").replace(/\s+/g, " ").trim(); + // Use [^\S\n]+ instead of \s+ to only collapse horizontal whitespace, + // preserving newlines so multi-line messages keep their paragraph structure. + const cleaned = trimmed + .replace(INLINE_STATUS_RE, " ") + .replace(/[^\S\n]+/g, " ") + .trim(); return { cleaned, didStrip: cleaned !== trimmed }; }