diff --git a/Services/AnthropicService.cs b/Services/AnthropicService.cs
index 26c85e9..aca9904 100644
--- a/Services/AnthropicService.cs
+++ b/Services/AnthropicService.cs
@@ -450,30 +450,35 @@ public class AnthropicService
var result = new ReviewResult { ReviewIterationCount = reviewIterations };
- // Prüfe ob [APPROVED]
- if (!string.IsNullOrEmpty(reviewText) && reviewText.Contains("[APPROVED]"))
+ // Default = Approved (analog zum Preparer, wo Default = Forward ist).
+ // Nur bei explizitem [FEEDBACK]-Tag wird Feedback an den Worker zurückgespielt.
+ // [APPROVED] wird weiterhin erkannt, ist aber nicht mehr zwingend nötig.
+ if (!string.IsNullOrEmpty(reviewText) && reviewText.Contains("[FEEDBACK]"))
{
+ // Explizites Feedback – an Worker zurückspielen
+ var feedbackText = reviewText.Replace("[FEEDBACK]", "").Replace("[APPROVED]", "").Trim();
+ result.HasFeedback = true;
+ result.Feedback = $"[Review-Feedback von {review.Agent?.Name ?? "Review-Agent"} (Runde {reviewRound + 1}/{review.MaxReviewIterations})]:\n{feedbackText}";
+
+ await TryWriteSse(response, "review_feedback", new
+ {
+ round = reviewRound + 1,
+ maxRounds = review.MaxReviewIterations,
+ feedback = feedbackText,
+ agentName = review.Agent?.Name ?? "Review-Agent"
+ }, cancellationToken);
+ }
+ else
+ {
+ // Default: Approved (kein [FEEDBACK]-Tag → alles OK)
await TryWriteSse(response, "review_approved", new
{
round = reviewRound + 1,
agentName = review.Agent?.Name ?? "Review-Agent"
}, cancellationToken);
result.HasFeedback = false;
- return result;
}
- // Feedback – an Worker zurückspielen
- result.HasFeedback = true;
- result.Feedback = $"[Review-Feedback von {review.Agent?.Name ?? "Review-Agent"} (Runde {reviewRound + 1}/{review.MaxReviewIterations})]:\n{reviewText}";
-
- await TryWriteSse(response, "review_feedback", new
- {
- round = reviewRound + 1,
- maxRounds = review.MaxReviewIterations,
- feedback = reviewText,
- agentName = review.Agent?.Name ?? "Review-Agent"
- }, cancellationToken);
-
return result;
}
@@ -803,6 +808,16 @@ public class AnthropicService
is_error = isError
});
+ await TryWriteSse(response, $"{agentType}_tool_end", new
+ {
+ toolName,
+ toolId,
+ input = toolInput.ToString(),
+ result = toolResult.Length > 500 ? toolResult[..500] + "..." : toolResult,
+ isError,
+ agentName
+ }, cancellationToken);
+
await _db.SaveMessage(new MessageRecord
{
ChainId = chainId,
diff --git a/wwwroot/css/chat.css b/wwwroot/css/chat.css
index 2167ecb..b2dd785 100644
--- a/wwwroot/css/chat.css
+++ b/wwwroot/css/chat.css
@@ -1124,12 +1124,9 @@
word-break: break-word;
}
-.review-tool-use {
- font-size: var(--font-size);
- opacity: var(--semi-transparent);
- display: flex;
- align-items: center;
- gap: var(--badge-spacing);
+.review-tool-box {
+ margin: var(--badge-spacing) 0;
+ border-radius: var(--border-radius);
}
.review-status {
diff --git a/wwwroot/js/chat.js b/wwwroot/js/chat.js
index 6f69591..0c13ec6 100644
--- a/wwwroot/js/chat.js
+++ b/wwwroot/js/chat.js
@@ -87,6 +87,10 @@ function handleStreamEvent(eventType, data) {
addReviewToolUse(data);
break;
+ case 'preparer_tool_end': case 'review_tool_end': case 'finalizer_tool_end':
+ finishReviewToolBox(data);
+ break;
+
case 'preparer_done':
finalizeReviewBubble(data.forwarded ? 'forwarded' : 'query', data, 'preparer');
break;
diff --git a/wwwroot/js/history.js b/wwwroot/js/history.js
index 0d839de..1970f9f 100644
--- a/wwwroot/js/history.js
+++ b/wwwroot/js/history.js
@@ -320,11 +320,8 @@ function _renderReviewGroup(tagGroup, archived) {
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 = ' ' + esc(m.toolName) + '';
- // Flush accumulated text before tool indicator
+ if (!isToolVisible(m.toolName)) continue;
+ // Flush accumulated text before tool box
if (textParts.length > 0) {
var contentDiv = document.createElement('div');
contentDiv.className = 'review-content markdown';
@@ -333,9 +330,41 @@ function _renderReviewGroup(tagGroup, archived) {
bubble.appendChild(contentDiv);
textParts = [];
}
+ // Full tool-box (same as normal tool boxes)
+ var toolDiv = document.createElement('div');
+ toolDiv.className = 'tool-box review-tool-box' + (archived ? ' archived' : '');
+ // Look ahead for matching tool_result
+ var toolResult = null;
+ var toolIsError = false;
+ for (var j = i + 1; j < tagGroup.length; j++) {
+ if (tagGroup[j].type === 'tool_result' && tagGroup[j].toolCallId === m.toolCallId) {
+ toolResult = tagGroup[j].content;
+ toolIsError = tagGroup[j].isError;
+ break;
+ }
+ }
+ renderToolBoxContent(toolDiv, m.toolName, m.content, toolResult, toolIsError);
bubble.appendChild(toolDiv);
+ } else if (m.type === 'web_search_result') {
+ if (!isToolVisible('web_search')) continue;
+ 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 = [];
+ }
+ var wsSources = [];
+ try { wsSources = JSON.parse(m.content || '[]'); } catch(e) {}
+ if (wsSources.length > 0) {
+ var wsDiv = document.createElement('div');
+ wsDiv.className = 'tool-box review-tool-box' + (archived ? ' archived' : '');
+ renderToolBoxContent(wsDiv, 'web_search', null, null, false, wsSources);
+ bubble.appendChild(wsDiv);
+ }
}
- // tool_result and web_search_result are skipped (same as streaming)
+ // tool_result is consumed by tool_use look-ahead above
}
// Flush remaining text
diff --git a/wwwroot/js/stream-ui.js b/wwwroot/js/stream-ui.js
index c8ba5e0..d904367 100644
--- a/wwwroot/js/stream-ui.js
+++ b/wwwroot/js/stream-ui.js
@@ -87,15 +87,32 @@ function appendToReviewText(text) {
scrollDown();
}
+let _currentReviewToolBox = null;
+
function addReviewToolUse(data) {
if (!_reviewBubble) return;
- const toolDiv = document.createElement('div');
- toolDiv.className = 'review-tool-use';
- toolDiv.innerHTML = ' ' + esc(data.toolName) + '';
+ if (!isToolVisible(data.toolName)) {
+ _currentReviewToolBox = null;
+ return;
+ }
+ var toolDiv = document.createElement('div');
+ toolDiv.className = 'tool-box review-tool-box streamed';
+ toolDiv.dataset.toolName = data.toolName;
+ var displayName = TOOL_DISPLAY_NAMES[data.toolName] || data.toolName;
+ toolDiv.innerHTML = '' + esc(displayName) + ' ';
_reviewBubble.appendChild(toolDiv);
+ _currentReviewToolBox = toolDiv;
scrollDown();
}
+function finishReviewToolBox(data) {
+ if (_currentReviewToolBox) {
+ renderToolBoxContent(_currentReviewToolBox, data.toolName, data.input, data.result, data.isError, data.sources);
+ _currentReviewToolBox = null;
+ scrollDown();
+ }
+}
+
function finalizeReviewBubble(type, data, agentType) {
if (!_reviewBubble) return;
const status = document.createElement('div');
@@ -120,5 +137,6 @@ function finalizeReviewBubble(type, data, agentType) {
_reviewBubble = null;
_reviewTextEl = null;
_reviewText = '';
+ _currentReviewToolBox = null;
scrollDown();
}