fix(suggestions): Improve JSON extraction for Cursor responses

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 <noreply@anthropic.com>
This commit is contained in:
Kacper
2025-12-30 15:14:57 +01:00
parent efd9a1b7d9
commit 26e4ac0d2f

View File

@@ -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<string, unknown>, 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<Record<string, unknown>> } | 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;
}