diff --git a/apps/ui/src/lib/agent-context-parser.ts b/apps/ui/src/lib/agent-context-parser.ts index 35e2cceb..324f6549 100644 --- a/apps/ui/src/lib/agent-context-parser.ts +++ b/apps/ui/src/lib/agent-context-parser.ts @@ -237,39 +237,46 @@ function cleanFragmentedText(content: string): string { /** * Extracts a summary from completed feature context * Looks for content between and tags + * Returns the LAST summary found to ensure we get the most recent/updated one */ function extractSummary(content: string): string | undefined { // First, clean up any fragmented text from streaming const cleanedContent = cleanFragmentedText(content); - // Look for tags - capture everything between opening and closing tags - const summaryTagMatch = cleanedContent.match(/([\s\S]*?)<\/summary>/i); - if (summaryTagMatch) { + // 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(summaryTagMatch[1]).trim(); + return cleanFragmentedText(summaryTagMatches[summaryTagMatches.length - 1][1]).trim(); } - // Fallback: Look for summary sections - capture everything including subsections (###) + // Fallback: Look for summary sections - find all and take the last // Stop at same-level ## sections (but not ###), or tool markers, or end - const summaryMatch = cleanedContent.match(/## Summary[^\n]*\n([\s\S]*?)(?=\n## [^#]|\nšŸ”§|$)/i); - if (summaryMatch) { - return cleanFragmentedText(summaryMatch[1]).trim(); + const summaryMatches = [ + ...cleanedContent.matchAll(/## Summary[^\n]*\n([\s\S]*?)(?=\n## [^#]|\nšŸ”§|$)/gi), + ]; + if (summaryMatches.length > 0) { + return cleanFragmentedText(summaryMatches[summaryMatches.length - 1][1]).trim(); } - // Look for completion markers and extract surrounding text - const completionMatch = cleanedContent.match( - /āœ“ (?:Feature|Verification|Task) (?:successfully|completed|verified)[^\n]*(?:\n[^\n]{1,200})?/i - ); - if (completionMatch) { - return cleanFragmentedText(completionMatch[0]).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 - const whatWasDoneMatch = cleanedContent.match( - /(?:What was done|Changes made|Implemented)[^\n]*\n([\s\S]*?)(?=\n## [^#]|\nšŸ”§|$)/i - ); - if (whatWasDoneMatch) { - return cleanFragmentedText(whatWasDoneMatch[1]).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(); } return undefined; diff --git a/apps/ui/src/lib/log-parser.ts b/apps/ui/src/lib/log-parser.ts index 8a873b5f..30967694 100644 --- a/apps/ui/src/lib/log-parser.ts +++ b/apps/ui/src/lib/log-parser.ts @@ -1198,46 +1198,62 @@ function mergeConsecutiveEntries(entries: LogEntry[]): LogEntry[] { /** * Extracts summary content from raw log output - * Returns the summary text if found, or null if no summary exists + * Returns the LAST summary text if found, or null if no summary exists + * This ensures we get the most recent/updated summary when multiple exist */ export function extractSummary(rawOutput: string): string | null { if (!rawOutput || !rawOutput.trim()) { return null; } + // First, clean up any fragmented text from streaming + // This handles cases where streaming providers send partial text chunks + // that got separated by newlines during accumulation (e.g., "") + const cleanedOutput = cleanFragmentedText(rawOutput); + // Try to find tags first (preferred format) - const summaryTagMatch = rawOutput.match(/([\s\S]*?)<\/summary>/); - if (summaryTagMatch) { - return summaryTagMatch[1].trim(); + // 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 - const summaryHeaderMatch = rawOutput.match(/^##\s+Summary\s*\n([\s\S]*?)(?=\n##\s+|$)/m); - if (summaryHeaderMatch) { - return summaryHeaderMatch[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), + ]; + if (summaryHeaderMatches.length > 0) { + return cleanFragmentedText(summaryHeaderMatches[summaryHeaderMatches.length - 1][1]).trim(); } - // Try other summary formats (Feature, Changes, Implementation) - const otherHeaderMatch = rawOutput.match( - /^##\s+(Feature|Changes|Implementation)\s*\n([\s\S]*?)(?=\n##\s+|$)/m - ); - if (otherHeaderMatch) { - return `## ${otherHeaderMatch[1]}\n${otherHeaderMatch[2].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 - const introMatch = rawOutput.match( - /(^|\n)(All tasks completed[\s\S]*?)(?=\nšŸ”§|\nšŸ“‹|\n⚔|\nāŒ|$)/ - ); - if (introMatch) { - return introMatch[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 completionMatch = rawOutput.match( - /(^|\n)((I've|I have) (successfully |now )?(completed|finished|implemented)[\s\S]*?)(?=\nšŸ”§|\nšŸ“‹|\n⚔|\nāŒ|$)/ - ); - if (completionMatch) { - return completionMatch[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(); } return null;