fix: Review/Preparer/Finalizer-Bubbles im History-Rendering korrekt gruppiert

Nach dem Streaming werden alle .streamed Elemente entfernt und pollHistory()
rendert die Messages neu aus der DB. Bisher wurden Review-Agent-Messages dabei
als einzelne Bubbles dargestellt (jeder Text-Block + jeder Tool-Call separat),
statt als eine zusammenhängende Review-Bubble wie beim Streaming.

Lösung: _renderMessagesIntoFragment() erkennt jetzt Messages mit tag-Feld
(review/preparer/finalizer) und gruppiert aufeinanderfolgende Messages mit
demselben Tag. _renderReviewGroup() erzeugt daraus eine einzige Review-Bubble
mit Header (Icon + Agent-Name), Tool-Use-Indikatoren und zusammengefasstem
Text-Content - identisch zur Streaming-Darstellung.

Fallback für alte Messages ohne tag-Feld bleibt bestehen (_isReviewMessage).
This commit is contained in:
Meleki committed 2026-03-08 00:04:58 +01:00
1 parent 9abb5fd217
commit 87bde0cbaf
1 file changed
+84
+84
View File
@@ -161,6 +161,7 @@ function _mergeConsecutiveAssistantTexts(msgs) {
const prev = result.length > 0 ? result[result.length - 1] : null;
if (prev && prev.type === 'text' && prev.role === 'assistant'
&& prev.iteration === msg.iteration
&& !msg.tag && !prev.tag
&& !_isReviewMessage(msg.content) && !_isReviewMessage(prev.content)) {
prev.content = (prev.content || '') + ' ' + (msg.content || '');
if (msg.id) prev.id = msg.id;
@@ -187,6 +188,18 @@ function _renderMessagesIntoFragment(fragment, msgs, imageMessageIds, opts) {
const archived = opts && opts.archived;
for (var _msgIdx = 0; _msgIdx < msgs.length; _msgIdx++) {
const msg = msgs[_msgIdx];
// ── Grouped Review/Preparer/Finalizer Bubble (tag-based) ──
if (msg.tag && (msg.tag === 'review' || msg.tag === 'preparer' || msg.tag === 'finalizer')) {
// Collect all consecutive messages with the same tag
var tagGroup = [msg];
while (_msgIdx + 1 < msgs.length && msgs[_msgIdx + 1].tag === msg.tag) {
_msgIdx++;
tagGroup.push(msgs[_msgIdx]);
}
fragment.appendChild(_renderReviewGroup(tagGroup, archived));
continue;
}
switch (msg.type) {
case 'text': {
const isReviewMsg = _isReviewMessage(msg.content);
@@ -266,6 +279,77 @@ function _renderMessagesIntoFragment(fragment, msgs, imageMessageIds, opts) {
}
}
/**
* Renders a group of consecutive messages with the same tag (review/preparer/finalizer)
* into a single Review-Bubble matching the streaming UI structure.
*/
function _renderReviewGroup(tagGroup, archived) {
var tag = tagGroup[0].tag;
var bubble = document.createElement('div');
bubble.className = 'msg review-bubble' + (archived ? ' archived' : '');
if (tag === 'finalizer') bubble.classList.add('finalizer-bubble');
if (tag === 'preparer') bubble.classList.add('preparer-bubble');
// Extract agent name from first text message: "[Review: AgentName] text..."
var agentName = tag === 'preparer' ? 'Vorbereiter-Agent' : tag === 'finalizer' ? 'Finalizer-Agent' : 'Review-Agent';
var firstText = tagGroup.find(function(m) { return m.type === 'text' && m.role === 'assistant'; });
if (firstText) {
var nameMatch = firstText.content.match(/^\[(Review|Preparer|Finalizer): ([^\]]+)\]/);
if (nameMatch) agentName = nameMatch[2];
}
// Header
var header = document.createElement('div');
header.className = 'review-header';
if (tag === 'preparer') {
header.innerHTML = '<i class="fa fa-filter"></i> <strong>' + esc(agentName) + '</strong>';
} else if (tag === 'finalizer') {
header.innerHTML = '<i class="fa fa-flag-checkered"></i> <strong>' + esc(agentName) + '</strong>';
} else {
header.innerHTML = '<i class="fa fa-check-circle-o"></i> <strong>' + esc(agentName) + '</strong>';
}
bubble.appendChild(header);
// Content: collect text (strip prefix) and tool-use indicators
var textParts = [];
for (var i = 0; i < tagGroup.length; i++) {
var m = tagGroup[i];
if (m.type === 'text' && m.role === 'assistant') {
var text = m.content;
// Strip [Tag: AgentName] prefix
text = text.replace(/^\[(Review|Preparer|Finalizer): [^\]]+\]\s*/, '');
if (text) textParts.push(text);
} else if (m.type === 'tool_use') {
// Insert tool-use indicator
var toolDiv = document.createElement('div');
toolDiv.className = 'review-tool-use';
toolDiv.innerHTML = '<i class="fa fa-wrench"></i> <span class="tool-name">' + esc(m.toolName) + '</span>';
// Flush accumulated text before tool indicator
if (textParts.length > 0) {
var contentDiv = document.createElement('div');
contentDiv.className = 'review-content markdown';
contentDiv.innerHTML = renderMarkdown(textParts.join('\n\n'));
parseFluentEmojis(contentDiv);
bubble.appendChild(contentDiv);
textParts = [];
}
bubble.appendChild(toolDiv);
}
// tool_result and web_search_result are skipped (same as streaming)
}
// Flush remaining text
if (textParts.length > 0) {
var contentDiv = document.createElement('div');
contentDiv.className = 'review-content markdown';
contentDiv.innerHTML = renderMarkdown(textParts.join('\n\n'));
parseFluentEmojis(contentDiv);
bubble.appendChild(contentDiv);
}
return bubble;
}
function _isReviewMessage(content) {
return content && (content.startsWith('[Review:') || content.startsWith('[Review-Feedback') || content.startsWith('[Finalizer:') || content.startsWith('[Preparer:'));
}