From 9c1eb755f9954854bfa60b73601f6169f917b944 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Mon, 29 Jun 2026 12:19:28 +0300 Subject: [PATCH] fix: prevent embedded JSON examples from rendering as result cards Only parse full-message generated JSON results Keep markdown prose with JSON examples rendered normally Add regression coverage for embedded JSON examples --- .../message/parts/generatedJsonResult.test.ts | 40 +++++++++++++++++++ .../chat/message/parts/generatedJsonResult.ts | 18 +++------ 2 files changed, 46 insertions(+), 12 deletions(-) create mode 100644 packages/ui/src/components/chat/message/parts/generatedJsonResult.test.ts diff --git a/packages/ui/src/components/chat/message/parts/generatedJsonResult.test.ts b/packages/ui/src/components/chat/message/parts/generatedJsonResult.test.ts new file mode 100644 index 00000000..f1f6cea5 --- /dev/null +++ b/packages/ui/src/components/chat/message/parts/generatedJsonResult.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, test } from 'bun:test'; + +import { parseGeneratedJsonResult } from './generatedJsonResult'; + +describe('parseGeneratedJsonResult', () => { + test('parses a full pull request JSON result', () => { + expect(parseGeneratedJsonResult('{"title":"Side task","body":"Details"}')).toEqual({ + kind: 'pr', + title: 'Side task', + body: 'Details', + raw: JSON.stringify({ title: 'Side task', body: 'Details' }, null, 2), + }); + }); + + test('parses a full fenced JSON result', () => { + expect(parseGeneratedJsonResult('```json\n{"subject":"Fix parser","highlights":["Narrow detection"]}\n```')).toEqual({ + kind: 'commit', + subject: 'Fix parser', + highlights: ['Narrow detection'], + raw: JSON.stringify({ subject: 'Fix parser', highlights: ['Narrow detection'] }, null, 2), + }); + }); + + test('ignores JSON examples embedded in markdown prose', () => { + const markdown = [ + 'Recommended endpoint:', + '', + '```json', + '{', + ' "title": "Side task",', + ' "prompt": "Investigate X"', + '}', + '```', + '', + 'This should stay markdown.', + ].join('\n'); + + expect(parseGeneratedJsonResult(markdown)).toBeNull(); + }); +}); diff --git a/packages/ui/src/components/chat/message/parts/generatedJsonResult.ts b/packages/ui/src/components/chat/message/parts/generatedJsonResult.ts index 783a20ef..2d95cb73 100644 --- a/packages/ui/src/components/chat/message/parts/generatedJsonResult.ts +++ b/packages/ui/src/components/chat/message/parts/generatedJsonResult.ts @@ -16,21 +16,15 @@ export type GeneratedResult = GeneratedCommitResult | GeneratedPrResult; const parseJsonObjects = (value: string): Record[] => { const text = value.trim(); - const candidates = new Set(); + const candidates: string[] = []; - const fencedMatches = text.matchAll(/```(?:json)?\s*([\s\S]*?)```/gi); - for (const match of fencedMatches) { - if (match[1]) candidates.add(match[1].trim()); + const fencedMatch = text.match(/^```(?:json)?\s*([\s\S]*?)```$/i); + if (fencedMatch?.[1]) { + candidates.push(fencedMatch[1].trim()); } - const firstObjectStart = text.indexOf('{'); - if (firstObjectStart >= 0) { - for (let end = text.length; end > firstObjectStart; end -= 1) { - if (text[end - 1] === '}') { - candidates.add(text.slice(firstObjectStart, end)); - break; - } - } + if (text.startsWith('{') && text.endsWith('}')) { + candidates.push(text); } const parsed: Record[] = [];