fix(update-tasks): Improve AI response parsing for 'update' command

Refactors the JSON array parsing logic within
in .

The previous logic primarily relied on extracting content from markdown
code blocks (json or javascript), which proved brittle when the AI
response included comments or non-JSON text within the block, leading to
parsing errors for the  command.

This change modifies the parsing strategy to first attempt extracting
content directly between the outermost '[' and ']' brackets. This is
more robust as it targets the expected array structure directly. If
bracket extraction fails, it falls back to looking for a strict json
code block, then prefix stripping, before attempting a raw parse.

This approach aligns with the successful parsing strategy used for
single-object responses in  and resolves the
parsing errors previously observed with the  command.
This commit is contained in:
Eyal Toledano
2025-05-02 00:37:41 -04:00
parent 16297058bb
commit c9e4558a19
2 changed files with 85 additions and 67 deletions

View File

@@ -380,7 +380,7 @@ The changes described in the prompt should be thoughtfully applied to make the t
let loadingIndicator = null; let loadingIndicator = null;
if (outputFormat === 'text') { if (outputFormat === 'text') {
loadingIndicator = startLoadingIndicator( loadingIndicator = startLoadingIndicator(
useResearch ? 'Updating task with research...' : 'Updating task...' useResearch ? 'Updating task with research...\n' : 'Updating task...\n'
); );
} }

View File

@@ -68,76 +68,98 @@ function parseUpdatedTasksFromText(text, expectedCount, logFn, isMCP) {
let cleanedResponse = text.trim(); let cleanedResponse = text.trim();
const originalResponseForDebug = cleanedResponse; const originalResponseForDebug = cleanedResponse;
let parseMethodUsed = 'raw'; // Track which method worked
// Step 1: Attempt to extract from Markdown code block first // --- NEW Step 1: Try extracting between [] first ---
const codeBlockMatch = cleanedResponse.match( const firstBracketIndex = cleanedResponse.indexOf('[');
/```(?:json|javascript)?\s*([\s\S]*?)\s*```/i // Made case-insensitive, allow js const lastBracketIndex = cleanedResponse.lastIndexOf(']');
); let potentialJsonFromArray = null;
if (codeBlockMatch) {
cleanedResponse = codeBlockMatch[1].trim();
report('info', 'Extracted content from Markdown code block.');
} else {
// Step 2 (if no code block): Attempt to strip common language identifiers/intro text
// List common prefixes AI might add before JSON
const commonPrefixes = [
'json\n',
'javascript\n',
'python\n', // Language identifiers
'here are the updated tasks:',
'here is the updated json:', // Common intro phrases
'updated tasks:',
'updated json:',
'response:',
'output:'
];
let prefixFound = false;
for (const prefix of commonPrefixes) {
if (cleanedResponse.toLowerCase().startsWith(prefix)) {
cleanedResponse = cleanedResponse.substring(prefix.length).trim();
report('info', `Stripped prefix: "${prefix.trim()}"`);
prefixFound = true;
break; // Stop after finding the first matching prefix
}
}
// Step 3 (if no code block and no prefix stripped, or after stripping): Find first '[' and last ']' if (firstBracketIndex !== -1 && lastBracketIndex > firstBracketIndex) {
// This helps if there's still leading/trailing text around the array potentialJsonFromArray = cleanedResponse.substring(
const firstBracket = cleanedResponse.indexOf('['); firstBracketIndex,
const lastBracket = cleanedResponse.lastIndexOf(']'); lastBracketIndex + 1
if (firstBracket !== -1 && lastBracket > firstBracket) { );
const extractedArray = cleanedResponse.substring( // Basic check to ensure it's not just "[]" or malformed
firstBracket, if (potentialJsonFromArray.length <= 2) {
lastBracket + 1 potentialJsonFromArray = null; // Ignore empty array
);
// Basic check to see if the extraction looks like JSON
if (extractedArray.length > 2) {
// More than just '[]'
cleanedResponse = extractedArray; // Use the extracted array content
if (!codeBlockMatch && !prefixFound) {
// Only log if we didn't already log extraction/stripping
report('info', 'Extracted content between first [ and last ].');
}
} else if (!codeBlockMatch && !prefixFound) {
report(
'warn',
'Found brackets "[]" but content seems empty or invalid. Proceeding with original cleaned response.'
);
}
} else if (!codeBlockMatch && !prefixFound) {
// Only warn if no other extraction method worked
report(
'warn',
'Response does not appear to contain a JSON code block, known prefix, or clear array structure ([...]). Attempting to parse raw response.'
);
} }
} }
// Step 4: Attempt to parse the (hopefully) cleaned JSON array // If [] extraction yielded something, try parsing it immediately
if (potentialJsonFromArray) {
try {
const testParse = JSON.parse(potentialJsonFromArray);
// It worked! Use this as the primary cleaned response.
cleanedResponse = potentialJsonFromArray;
parseMethodUsed = 'brackets';
report(
'info',
'Successfully parsed JSON content extracted between first [ and last ].'
);
} catch (e) {
report(
'info',
'Content between [] looked promising but failed initial parse. Proceeding to other methods.'
);
// Reset cleanedResponse to original if bracket parsing failed
cleanedResponse = originalResponseForDebug;
}
}
// --- Step 2: If bracket parsing didn't work or wasn't applicable, try code block extraction ---
if (parseMethodUsed === 'raw') {
// Only look for ```json blocks now
const codeBlockMatch = cleanedResponse.match(
/```json\s*([\s\S]*?)\s*```/i // Only match ```json
);
if (codeBlockMatch) {
cleanedResponse = codeBlockMatch[1].trim();
parseMethodUsed = 'codeblock';
report('info', 'Extracted JSON content from JSON Markdown code block.');
} else {
report('info', 'No JSON code block found.');
// --- Step 3: If code block failed, try stripping prefixes ---
const commonPrefixes = [
'json\n',
'javascript\n', // Keep checking common prefixes just in case
'python\n',
'here are the updated tasks:',
'here is the updated json:',
'updated tasks:',
'updated json:',
'response:',
'output:'
];
let prefixFound = false;
for (const prefix of commonPrefixes) {
if (cleanedResponse.toLowerCase().startsWith(prefix)) {
cleanedResponse = cleanedResponse.substring(prefix.length).trim();
parseMethodUsed = 'prefix';
report('info', `Stripped prefix: "${prefix.trim()}"`);
prefixFound = true;
break;
}
}
if (!prefixFound) {
report(
'warn',
'Response does not appear to contain [], JSON code block, or known prefix. Attempting raw parse.'
);
}
}
}
// --- Step 4: Attempt final parse ---
let parsedTasks; let parsedTasks;
try { try {
parsedTasks = JSON.parse(cleanedResponse); parsedTasks = JSON.parse(cleanedResponse);
} catch (parseError) { } catch (parseError) {
report('error', `Failed to parse JSON array: ${parseError.message}`); report('error', `Failed to parse JSON array: ${parseError.message}`);
report(
'error',
`Extraction method used: ${parseMethodUsed}` // Log which method failed
);
report( report(
'error', 'error',
`Problematic JSON string (first 500 chars): ${cleanedResponse.substring(0, 500)}` `Problematic JSON string (first 500 chars): ${cleanedResponse.substring(0, 500)}`
@@ -151,7 +173,7 @@ function parseUpdatedTasksFromText(text, expectedCount, logFn, isMCP) {
); );
} }
// Step 5: Validate Array structure // --- Step 5 & 6: Validate Array structure and Zod schema ---
if (!Array.isArray(parsedTasks)) { if (!Array.isArray(parsedTasks)) {
report( report(
'error', 'error',
@@ -172,7 +194,6 @@ function parseUpdatedTasksFromText(text, expectedCount, logFn, isMCP) {
); );
} }
// Step 6: Validate each task object using Zod
const validationResult = updatedTaskArraySchema.safeParse(parsedTasks); const validationResult = updatedTaskArraySchema.safeParse(parsedTasks);
if (!validationResult.success) { if (!validationResult.success) {
report('error', 'Parsed task array failed Zod validation.'); report('error', 'Parsed task array failed Zod validation.');
@@ -185,7 +206,6 @@ function parseUpdatedTasksFromText(text, expectedCount, logFn, isMCP) {
} }
report('info', 'Successfully validated task structure.'); report('info', 'Successfully validated task structure.');
// Return the validated data, potentially filtering/adjusting length if needed
return validationResult.data.slice( return validationResult.data.slice(
0, 0,
expectedCount || validationResult.data.length expectedCount || validationResult.data.length
@@ -332,9 +352,7 @@ The changes described in the prompt should be applied to ALL tasks in the list.`
let loadingIndicator = null; let loadingIndicator = null;
if (outputFormat === 'text') { if (outputFormat === 'text') {
loadingIndicator = startLoadingIndicator( loadingIndicator = startLoadingIndicator('Updating tasks...\n');
'Calling AI service to update tasks...'
);
} }
let responseText = ''; let responseText = '';