From 35d2d41821b2e090a6e7d52d6ffcf836270d3906 Mon Sep 17 00:00:00 2001 From: Shirone Date: Sun, 25 Jan 2026 12:15:05 +0100 Subject: [PATCH 1/2] feat: Update summary extraction logic to return the most recent summary from multiple occurrences - Enhanced `extractSummary` functions in `agent-context-parser.ts` and `log-parser.ts` to utilize `matchAll` for capturing all summary instances. - Modified logic to return the last found summary, ensuring the most recent content is extracted. - Improved handling of fragmented text and various summary formats for consistency. --- apps/ui/src/lib/agent-context-parser.ts | 47 ++++++++++-------- apps/ui/src/lib/log-parser.ts | 66 +++++++++++++++---------- 2 files changed, 68 insertions(+), 45 deletions(-) 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; From 3399d488236f4e2e7598145fb3b29c2e9b19370b Mon Sep 17 00:00:00 2001 From: Shirone Date: Sun, 25 Jan 2026 12:24:57 +0100 Subject: [PATCH 2/2] refactor: Extract regex patterns into configurable arrays in extractSummary functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address code review feedback from Gemini Code Assist on PR #692. Refactored the repeated matchAll() logic in extractSummary() functions to use a loop over a configuration array, reducing code duplication and improving maintainability. - agent-context-parser.ts: Use regexesToTry array with group index - log-parser.ts: Use regexesToTry array with processor functions for special handling šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/ui/src/lib/agent-context-parser.ts | 52 ++++++++----------- apps/ui/src/lib/log-parser.ts | 68 ++++++++++--------------- 2 files changed, 47 insertions(+), 73 deletions(-) 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;