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:
@@ -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'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 = '';
|
||||||
|
|||||||
Reference in New Issue
Block a user