diff --git a/apps/ui/src/lib/agent-context-parser.ts b/apps/ui/src/lib/agent-context-parser.ts index 35e2cceb..8313e055 100644 --- a/apps/ui/src/lib/agent-context-parser.ts +++ b/apps/ui/src/lib/agent-context-parser.ts @@ -237,39 +237,34 @@ 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) { - // Clean up the extracted summary content as well - return cleanFragmentedText(summaryTagMatch[1]).trim(); - } + // 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, + }, + ]; - // Fallback: Look for summary sections - capture everything including subsections (###) - // 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(); - } - - // 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 "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(); + 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 8a873b5f..5228a8fc 100644 --- a/apps/ui/src/lib/log-parser.ts +++ b/apps/ui/src/lib/log-parser.ts @@ -1198,46 +1198,48 @@ 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; } - // Try to find tags first (preferred format) - const summaryTagMatch = rawOutput.match(/([\s\S]*?)<\/summary>/); - if (summaryTagMatch) { - return summaryTagMatch[1].trim(); - } + // 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 markdown ## Summary section - const summaryHeaderMatch = rawOutput.match(/^##\s+Summary\s*\n([\s\S]*?)(?=\n##\s+|$)/m); - if (summaryHeaderMatch) { - return summaryHeaderMatch[1].trim(); - } + // 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], + }, + ]; - // 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 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(); - } - - 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(); + 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;