From 26e4ac0d2fb76a5ee377d21a0d146b0b9ffef354 Mon Sep 17 00:00:00 2001 From: Kacper Date: Tue, 30 Dec 2025 15:14:57 +0100 Subject: [PATCH] fix(suggestions): Improve JSON extraction for Cursor responses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cursor responses may include text after the JSON object, causing JSON.parse to fail. Added multi-strategy extraction similar to validate-issue.ts: 1. Try extracting from ```json code block 2. Try extracting from ``` code block 3. Try finding {"suggestions" and matching braces 4. Try finding any JSON object with suggestions array Uses bracket counting to find the correct closing brace. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../suggestions/generate-suggestions.ts | 103 +++++++++++++++++- 1 file changed, 99 insertions(+), 4 deletions(-) diff --git a/apps/server/src/routes/suggestions/generate-suggestions.ts b/apps/server/src/routes/suggestions/generate-suggestions.ts index 908ea724..22855a89 100644 --- a/apps/server/src/routes/suggestions/generate-suggestions.ts +++ b/apps/server/src/routes/suggestions/generate-suggestions.ts @@ -289,11 +289,10 @@ ${JSON.stringify(suggestionsSchema, null, 2)}`; })), }); } else { - // Fallback: try to parse from text (for backwards compatibility) + // Fallback: try to parse from text using multiple strategies logger.warn('No structured output received, attempting to parse from text'); - const jsonMatch = responseText.match(/\{[\s\S]*"suggestions"[\s\S]*\}/); - if (jsonMatch) { - const parsed = JSON.parse(jsonMatch[0]); + const parsed = extractSuggestionsJson(responseText); + if (parsed && parsed.suggestions) { events.emit('suggestions:event', { type: 'suggestions_complete', suggestions: parsed.suggestions.map((s: Record, i: number) => ({ @@ -323,3 +322,99 @@ ${JSON.stringify(suggestionsSchema, null, 2)}`; }); } } + +/** + * Extract suggestions JSON from response text using multiple strategies. + * Handles various formats: markdown code blocks, raw JSON, etc. + */ +function extractSuggestionsJson( + responseText: string +): { suggestions: Array> } | null { + const strategies = [ + // Strategy 1: JSON in ```json code block + () => { + const match = responseText.match(/```json\s*([\s\S]*?)```/); + if (match) { + return JSON.parse(match[1].trim()); + } + return null; + }, + // Strategy 2: JSON in ``` code block (no language specified) + () => { + const match = responseText.match(/```\s*([\s\S]*?)```/); + if (match) { + const content = match[1].trim(); + if (content.startsWith('{') && content.includes('"suggestions"')) { + return JSON.parse(content); + } + } + return null; + }, + // Strategy 3: Find JSON object containing "suggestions" array + () => { + // Find the start of the JSON object + const startIdx = responseText.indexOf('{"suggestions"'); + if (startIdx === -1) return null; + + // Find matching closing brace by counting brackets + let depth = 0; + let endIdx = -1; + for (let i = startIdx; i < responseText.length; i++) { + if (responseText[i] === '{') depth++; + if (responseText[i] === '}') { + depth--; + if (depth === 0) { + endIdx = i + 1; + break; + } + } + } + + if (endIdx > startIdx) { + return JSON.parse(responseText.slice(startIdx, endIdx)); + } + return null; + }, + // Strategy 4: Find any JSON object with suggestions + () => { + const startIdx = responseText.indexOf('{'); + if (startIdx === -1) return null; + + // Find matching closing brace + let depth = 0; + let endIdx = -1; + for (let i = startIdx; i < responseText.length; i++) { + if (responseText[i] === '{') depth++; + if (responseText[i] === '}') { + depth--; + if (depth === 0) { + endIdx = i + 1; + break; + } + } + } + + if (endIdx > startIdx) { + const parsed = JSON.parse(responseText.slice(startIdx, endIdx)); + if (parsed.suggestions && Array.isArray(parsed.suggestions)) { + return parsed; + } + } + return null; + }, + ]; + + for (const strategy of strategies) { + try { + const result = strategy(); + if (result && result.suggestions && Array.isArray(result.suggestions)) { + logger.debug('Successfully extracted suggestions JSON'); + return result; + } + } catch { + // Strategy failed, try next + } + } + + return null; +}