diff --git a/src/components/chat/hooks/useChatMessages.ts b/src/components/chat/hooks/useChatMessages.ts index b43cdd6f..90157d8b 100644 --- a/src/components/chat/hooks/useChatMessages.ts +++ b/src/components/chat/hooks/useChatMessages.ts @@ -13,6 +13,48 @@ function formatToolResultContent(content: unknown): string { return toolUseErrorMatch ? toolUseErrorMatch[1] : text; } +type ParsedTaskNotification = { + status: string; + summary: string; + result: string; +}; + +/** + * Parses a background-agent `` block. + * + * The harness injects these as user-role messages when a background task stops. + * Newer notifications carry extra fields (``, ``, ``, + * and a `` markdown payload) that the previous single-shot regex could + * not match, so the whole raw XML block leaked through as plain user text. + * Fields are extracted independently so the block renders as an assistant + * notification plus, when present, the agent's markdown result. + */ +function parseTaskNotification(content: string): ParsedTaskNotification | null { + if (!content.trimStart().startsWith('')) { + return null; + } + + const statusMatch = /([\s\S]*?)<\/status>/.exec(content); + const summaryMatch = /([\s\S]*?)<\/summary>/.exec(content); + + let result = ''; + const resultOpen = content.indexOf(''); + if (resultOpen !== -1) { + const afterOpen = content.slice(resultOpen + ''.length); + const closeIndex = afterOpen.indexOf(''); + result = + closeIndex === -1 + ? afterOpen.replace(/<\/task-notification>\s*$/, '').trim() + : afterOpen.slice(0, closeIndex).trim(); + } + + return { + status: statusMatch?.[1]?.trim() || 'completed', + summary: summaryMatch?.[1]?.trim() || 'Background task finished', + result, + }; +} + /** * Convert NormalizedMessage[] from the session store into ChatMessage[] * that the existing UI components expect. @@ -56,17 +98,26 @@ export function normalizedToChatMessages(messages: NormalizedMessage[]): ChatMes if (msg.role === 'user') { // Parse task notifications - const taskNotifRegex = /\s*[^<]*<\/task-id>\s*[^<]*<\/output-file>\s*([^<]*)<\/status>\s*([^<]*)<\/summary>\s*<\/task-notification>/g; - const taskNotifMatch = taskNotifRegex.exec(content); - if (taskNotifMatch) { + const taskNotif = parseTaskNotification(content); + if (taskNotif) { converted.push({ type: 'assistant', - content: taskNotifMatch[2]?.trim() || 'Background task finished', + content: taskNotif.summary, timestamp: msg.timestamp, isTaskNotification: true, - taskStatus: taskNotifMatch[1]?.trim() || 'completed', + taskStatus: taskNotif.status, ...sharedMetadata, }); + // Render the agent's result as a normal assistant message so its + // markdown displays correctly instead of leaking raw XML. + if (taskNotif.result) { + converted.push({ + type: 'assistant', + content: formatUsageLimitText(unescapeWithMathProtection(decodeHtmlEntities(taskNotif.result))), + timestamp: msg.timestamp, + ...sharedMetadata, + }); + } } else { converted.push({ type: 'user',