diff --git a/apps/ui/src/lib/agent-context-parser.ts b/apps/ui/src/lib/agent-context-parser.ts index 324f6549..8313e055 100644 --- a/apps/ui/src/lib/agent-context-parser.ts +++ b/apps/ui/src/lib/agent-context-parser.ts @@ -243,40 +243,28 @@ function extractSummary(content: string): string | undefined { // First, clean up any fragmented text from streaming const cleanedContent = cleanFragmentedText(content); - // Look for tags - find ALL occurrences and take the LAST one - const summaryTagMatches = [...cleanedContent.matchAll(/([\s\S]*?)<\/summary>/gi)]; - if (summaryTagMatches.length > 0) { - // Clean up the extracted summary content as well - return cleanFragmentedText(summaryTagMatches[summaryTagMatches.length - 1][1]).trim(); - } - - // Fallback: Look for summary sections - find all and take the last - // Stop at same-level ## sections (but not ###), or tool markers, or end - const summaryMatches = [ - ...cleanedContent.matchAll(/## Summary[^\n]*\n([\s\S]*?)(?=\n## [^#]|\nšŸ”§|$)/gi), + // Define regex patterns to try in order of priority + // Each pattern specifies which capture group contains the summary content + const regexesToTry = [ + { regex: /([\s\S]*?)<\/summary>/gi, group: 1 }, + { regex: /## Summary[^\n]*\n([\s\S]*?)(?=\n## [^#]|\nšŸ”§|$)/gi, group: 1 }, + { + regex: + /āœ“ (?:Feature|Verification|Task) (?:successfully|completed|verified)[^\n]*(?:\n[^\n]{1,200})?/gi, + group: 0, + }, + { + regex: /(?:What was done|Changes made|Implemented)[^\n]*\n([\s\S]*?)(?=\n## [^#]|\nšŸ”§|$)/gi, + group: 1, + }, ]; - if (summaryMatches.length > 0) { - return cleanFragmentedText(summaryMatches[summaryMatches.length - 1][1]).trim(); - } - // Look for completion markers and extract surrounding text - find all and take the last - const completionMatches = [ - ...cleanedContent.matchAll( - /āœ“ (?:Feature|Verification|Task) (?:successfully|completed|verified)[^\n]*(?:\n[^\n]{1,200})?/gi - ), - ]; - if (completionMatches.length > 0) { - return cleanFragmentedText(completionMatches[completionMatches.length - 1][0]).trim(); - } - - // Look for "What was done" type sections - find all and take the last - const whatWasDoneMatches = [ - ...cleanedContent.matchAll( - /(?:What was done|Changes made|Implemented)[^\n]*\n([\s\S]*?)(?=\n## [^#]|\nšŸ”§|$)/gi - ), - ]; - if (whatWasDoneMatches.length > 0) { - return cleanFragmentedText(whatWasDoneMatches[whatWasDoneMatches.length - 1][1]).trim(); + for (const { regex, group } of regexesToTry) { + const matches = [...cleanedContent.matchAll(regex)]; + if (matches.length > 0) { + const lastMatch = matches[matches.length - 1]; + return cleanFragmentedText(lastMatch[group]).trim(); + } } return undefined; diff --git a/apps/ui/src/lib/log-parser.ts b/apps/ui/src/lib/log-parser.ts index 30967694..5228a8fc 100644 --- a/apps/ui/src/lib/log-parser.ts +++ b/apps/ui/src/lib/log-parser.ts @@ -1211,49 +1211,35 @@ export function extractSummary(rawOutput: string): string | null { // that got separated by newlines during accumulation (e.g., "") const cleanedOutput = cleanFragmentedText(rawOutput); - // Try to find tags first (preferred format) - // Use matchAll to find ALL occurrences and take the LAST one - const summaryTagMatches = [...cleanedOutput.matchAll(/([\s\S]*?)<\/summary>/gi)]; - if (summaryTagMatches.length > 0) { - // Clean up the extracted summary content as well - return cleanFragmentedText(summaryTagMatches[summaryTagMatches.length - 1][1]).trim(); - } - - // Try to find markdown ## Summary section - find all and take the last - // Stop at same-level ## sections (but not ###), or tool markers, or end - const summaryHeaderMatches = [ - ...cleanedOutput.matchAll(/^##\s+Summary[^\n]*\n([\s\S]*?)(?=\n##\s+[^#]|\nšŸ”§|$)/gm), + // Define regex patterns to try in order of priority + // Each pattern specifies a processor function to extract the summary from the match + const regexesToTry: Array<{ + regex: RegExp; + processor: (m: RegExpMatchArray) => string; + }> = [ + { regex: /([\s\S]*?)<\/summary>/gi, processor: (m) => m[1] }, + { regex: /^##\s+Summary[^\n]*\n([\s\S]*?)(?=\n##\s+[^#]|\nšŸ”§|$)/gm, processor: (m) => m[1] }, + { + regex: /^##\s+(Feature|Changes|Implementation)[^\n]*\n([\s\S]*?)(?=\n##\s+[^#]|\nšŸ”§|$)/gm, + processor: (m) => `## ${m[1]}\n${m[2]}`, + }, + { + regex: /(^|\n)(All tasks completed[\s\S]*?)(?=\nšŸ”§|\nšŸ“‹|\n⚔|\nāŒ|$)/g, + processor: (m) => m[2], + }, + { + regex: + /(^|\n)((I've|I have) (successfully |now )?(completed|finished|implemented)[\s\S]*?)(?=\nšŸ”§|\nšŸ“‹|\n⚔|\nāŒ|$)/g, + processor: (m) => m[2], + }, ]; - if (summaryHeaderMatches.length > 0) { - return cleanFragmentedText(summaryHeaderMatches[summaryHeaderMatches.length - 1][1]).trim(); - } - // Try other summary formats (Feature, Changes, Implementation) - find all and take the last - const otherHeaderMatches = [ - ...cleanedOutput.matchAll( - /^##\s+(Feature|Changes|Implementation)[^\n]*\n([\s\S]*?)(?=\n##\s+[^#]|\nšŸ”§|$)/gm - ), - ]; - if (otherHeaderMatches.length > 0) { - const lastMatch = otherHeaderMatches[otherHeaderMatches.length - 1]; - return cleanFragmentedText(`## ${lastMatch[1]}\n${lastMatch[2]}`).trim(); - } - - // Try to find summary introduction lines - find all and take the last - const introMatches = [ - ...cleanedOutput.matchAll(/(^|\n)(All tasks completed[\s\S]*?)(?=\nšŸ”§|\nšŸ“‹|\n⚔|\nāŒ|$)/g), - ]; - if (introMatches.length > 0) { - return cleanFragmentedText(introMatches[introMatches.length - 1][2]).trim(); - } - - const completionMatches = [ - ...cleanedOutput.matchAll( - /(^|\n)((I've|I have) (successfully |now )?(completed|finished|implemented)[\s\S]*?)(?=\nšŸ”§|\nšŸ“‹|\n⚔|\nāŒ|$)/g - ), - ]; - if (completionMatches.length > 0) { - return cleanFragmentedText(completionMatches[completionMatches.length - 1][2]).trim(); + for (const { regex, processor } of regexesToTry) { + const matches = [...cleanedOutput.matchAll(regex)]; + if (matches.length > 0) { + const lastMatch = matches[matches.length - 1]; + return cleanFragmentedText(processor(lastMatch)).trim(); + } } return null;