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;