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;