fix(agents): render formatted prompt bubbles

- Render user prompt transcript bubbles through the shared Markdown component so bullets, line breaks, and inline formatting display like normal messages
- Preserve multi-line Buzz event Content fields when parsing ACP prompt payloads, stopping before harness metadata such as Tags and Parsed fields
- Add transcript helper coverage for a multi-line mention prompt to guard against flattening formatted responses back to one line
This commit is contained in:
Taylor Ho
2026-06-15 10:31:26 -07:00
committed by npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
parent 51dfa3a0ad
commit a7d6df9f3c
3 changed files with 65 additions and 4 deletions

View File

@@ -291,7 +291,7 @@ function PromptUserMessage({
/>
<div className="group relative flex max-w-[85%] min-w-0 flex-col items-end gap-1">
<div className="w-full min-w-0 rounded-2xl bg-muted p-2.5 text-sm leading-relaxed text-foreground">
<p className="whitespace-pre-wrap wrap-break-word">{text}</p>
<Markdown content={text || " "} tight />
{contextOpen && context ? (
<PromptContextSections sections={context.sections} setup={setup} />
) : null}
@@ -607,7 +607,7 @@ function MessageItem({
<Markdown compact content={text || " "} />
) : (
<>
<p className="whitespace-pre-wrap wrap-break-word">{text}</p>
<Markdown content={text || " "} tight />
<TranscriptTimestamp timestamp={item.timestamp} />
</>
)}

View File

@@ -65,6 +65,37 @@ test("parsePromptText extracts content, hex pubkey, and a title-cased kind", ()
);
});
test("parsePromptText preserves multiline event content in the user bubble text", () => {
const text = [
"[Buzz event: @mention]",
"Event ID: event-1",
"Channel: agents",
`From: tho (hex: ${HEX})`,
"Time: 2026-06-15T17:15:00Z",
"Content: @Ned",
"",
"- remove that stray cherry pick if it's not adding value here",
"- help me understand what that e2eBridge change does",
"- we'd want the e2e seed path as a separate pull request",
'Tags: [["h","agents"]]',
"Parsed: mentions=[Ned]",
].join("\n");
const result = parsePromptText(text);
assert.equal(
result.userText,
[
"@Ned",
"",
"- remove that stray cherry pick if it's not adding value here",
"- help me understand what that e2eBridge change does",
"- we'd want the e2e seed path as a separate pull request",
].join("\n"),
);
assert.equal(result.userPubkey, HEX);
});
test("parsePromptText lowercases the extracted hex pubkey", () => {
const text = [
"[Buzz event: dm]",

View File

@@ -85,9 +85,39 @@ function parsePromptSections(text: string): PromptSection[] {
return sections;
}
const EVENT_CONTENT_BOUNDARY_RE =
/^(?:Event ID|Channel|Kind|From|Time|Tags|Parsed):\s*/;
const EVENT_BLOCK_BOUNDARY_RE = /^--- Event \d+\b/;
function extractEventContent(body: string): string {
const contentMatch = body.match(/^Content:\s*(.*)$/m);
return contentMatch?.[1]?.trim() ?? "";
const lines = body.split(/\r?\n/);
const chunks: string[] = [];
for (let i = 0; i < lines.length; i++) {
const match = lines[i].match(/^Content:\s?(.*)$/);
if (!match) {
continue;
}
const contentLines = [match[1] ?? ""];
for (let j = i + 1; j < lines.length; j++) {
const line = lines[j];
if (
EVENT_CONTENT_BOUNDARY_RE.test(line) ||
EVENT_BLOCK_BOUNDARY_RE.test(line)
) {
break;
}
contentLines.push(line);
}
const content = contentLines.join("\n").trim();
if (content) {
chunks.push(content);
}
}
return chunks.join("\n\n");
}
function extractEventAuthorPubkey(body: string): string | null {