feat: Finalizer-Agent – dritter Agent der nach Worker + Review einschreitet
Backend:
- SessionRecord + DB-Migration: finalizer_agent_id auf sessions
- ChatContext.FinalizerContext (Model, SystemPrompt, Tools, Workspace)
- ChatService: FinalizerContext aufbauen (analog zu ReviewContext)
- AnthropicService: RunFinalizer() nach Review-Phase (oder direkt nach Worker)
- RunReviewAgentChat() refactored zu generischem RunAgentChat()
mit agentType-Parameter ('review' | 'finalizer') für SSE-Events und DB-Tags
- SSE-Events: finalizer_start, finalizer_text, finalizer_tool_use, finalizer_done
Frontend:
- Session-Page: Finalizer-Agent-Picker (im 'Weitere Einstellungen' Bereich)
- Chat: SSE-Events für Finalizer-Phase verarbeiten
- UI: addReviewBubble/finalizeReviewBubble unterstützen agentType 'finalizer'
mit eigener Farbe (success-green) und Flag-Checkered-Icon
- renderMarkdown: [Finalizer: AgentName] Tags parsen
- History: Finalizer-Messages als .finalizer-bubble rendern
- Shared-Page: Finalizer-Messages korrekt erkennen und stylen
- Dashboard: Finalizer-Badge (flag-checkered) auf Session-Cards
This commit is contained in:
1 parent
a3f2aa38c1
commit
4bec740765
14 files changed
+228
-46
No files matched your search
@@ -992,6 +992,18 @@
|
||||
margin-right: .3em;
|
||||
}
|
||||
|
||||
.finalizer-inline-header {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.finalizer-bubble .review-header {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.finalizer-bubble {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.review-inline-feedback {
|
||||
color: var(--warning);
|
||||
margin-bottom: .4em;
|
||||
|
||||
@@ -130,6 +130,11 @@
|
||||
<label>Max. Review-Runden</label>
|
||||
<input type="number" id="maxReviewIterations" min="1" max="10" value="3">
|
||||
</div>
|
||||
<label>Finalizer-Agent <small>(optional)</small></label>
|
||||
<div id="finalizerAgentSelectCard" class="agent-select-card">
|
||||
<div class="agent-select-empty"><i class="fa fa-flag-checkered"></i> Kein Finalizer-Agent</div>
|
||||
</div>
|
||||
<input type="hidden" id="finalizerAgentSelect" value="">
|
||||
<label>Benutzerdefinierte Befehle</label>
|
||||
<div class="custom-commands" id="customCommandsList"></div>
|
||||
<button type="button" id="addCommandBtn" class="add-cmd-btn">+ Befehl hinzufügen</button>
|
||||
|
||||
@@ -228,6 +228,23 @@ async function sendMessage() {
|
||||
finalizeReviewBubble('approved', data);
|
||||
break;
|
||||
|
||||
case 'finalizer_start':
|
||||
endStreamBubble();
|
||||
addReviewBubble('start', data, 'finalizer');
|
||||
break;
|
||||
|
||||
case 'finalizer_text':
|
||||
appendToReviewText(data.text);
|
||||
break;
|
||||
|
||||
case 'finalizer_tool_use':
|
||||
addReviewToolUse(data);
|
||||
break;
|
||||
|
||||
case 'finalizer_done':
|
||||
finalizeReviewBubble('done', data, 'finalizer');
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
endStreamBubble();
|
||||
addErrorBubble(data.error, true);
|
||||
|
||||
@@ -157,8 +157,8 @@ function _mergeConsecutiveAssistantTexts(msgs) {
|
||||
for (const msg of msgs) {
|
||||
if (msg.type === 'text' && msg.role === 'assistant') {
|
||||
const prev = result[result.length - 1];
|
||||
const isReview = msg.content && msg.content.startsWith('[Review:');
|
||||
const prevIsReview = prev && prev.content && prev.content.startsWith('[Review:');
|
||||
const isReview = msg.content && (msg.content.startsWith('[Review:') || msg.content.startsWith('[Finalizer:'));
|
||||
const prevIsReview = prev && prev.content && (prev.content.startsWith('[Review:') || prev.content.startsWith('[Finalizer:'));
|
||||
if (prev && prev.type === 'text' && prev.role === 'assistant'
|
||||
&& prev.iteration === msg.iteration && !isReview && !prevIsReview) {
|
||||
// Gleiche Iteration: nahtlos zusammenfügen (Web-Search-Fragmente etc.)
|
||||
@@ -181,7 +181,7 @@ function _renderMessagesIntoFragment(fragment, msgs, imageMessageIds) {
|
||||
const div = document.createElement('div');
|
||||
switch (msg.type) {
|
||||
case 'text': {
|
||||
const isReviewMsg = msg.content && (msg.content.startsWith('[Review:') || msg.content.startsWith('[Review-Feedback'));
|
||||
const isReviewMsg = msg.content && (msg.content.startsWith('[Review:') || msg.content.startsWith('[Review-Feedback') || msg.content.startsWith('[Finalizer:'));
|
||||
if (msg.role === 'user' && !isReviewMsg) {
|
||||
div.className = 'msg user markdown' + (msg.isMarked ? ' marked' : '');
|
||||
if (msg.deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden');
|
||||
@@ -290,7 +290,7 @@ function renderMessages(msgs, addToConversation) {
|
||||
}
|
||||
}
|
||||
} else if (msg.role === 'assistant') {
|
||||
if (msg.content && msg.content.startsWith('[Review:')) {
|
||||
if (msg.content && (msg.content.startsWith('[Review:') || msg.content.startsWith('[Finalizer:'))) {
|
||||
addReviewBubbleStatic(msg.content, msg.createdAt, msg.id, msg.isMarked, msg.deletedOn);
|
||||
} else {
|
||||
addAssistantBubble(msg.content, msg.createdAt, msg.id, msg.isMarked, msg.deletedOn);
|
||||
|
||||
@@ -25,6 +25,8 @@ registerPage('session', {
|
||||
const reviewAgentSelectCard = document.getElementById('reviewAgentSelectCard');
|
||||
const reviewIterationsRow = document.getElementById('reviewIterationsRow');
|
||||
const maxReviewIterationsEl = document.getElementById('maxReviewIterations');
|
||||
const finalizerAgentSelect = document.getElementById('finalizerAgentSelect');
|
||||
const finalizerAgentSelectCard = document.getElementById('finalizerAgentSelectCard');
|
||||
const imagePreview = document.getElementById('sessionImagePreview');
|
||||
const saveBtn = document.getElementById('sessionSaveBtn');
|
||||
const cancelBtn = document.getElementById('sessionCancelBtn');
|
||||
@@ -123,6 +125,14 @@ registerPage('session', {
|
||||
}
|
||||
updateReviewVisibility();
|
||||
|
||||
// Initiale Anzeige – Finalizer-Agent
|
||||
if (session && session.finalizerAgentId) {
|
||||
finalizerAgentSelect.value = session.finalizerAgentId;
|
||||
renderAgentInCard(finalizerAgentSelectCard, agentMap[session.finalizerAgentId] || null, 'fa-flag-checkered', 'Kein Finalizer-Agent');
|
||||
} else {
|
||||
renderAgentInCard(finalizerAgentSelectCard, null, 'fa-flag-checkered', 'Kein Finalizer-Agent');
|
||||
}
|
||||
|
||||
// Klick auf Agent-Card → Modal öffnen
|
||||
agentSelectCard.addEventListener('click', () => {
|
||||
openAgentPickerModal(agents, agentSkillsMap, agentSelect.value, (selectedId) => {
|
||||
@@ -140,6 +150,14 @@ registerPage('session', {
|
||||
});
|
||||
});
|
||||
|
||||
// Klick auf Finalizer-Agent-Card → Modal öffnen
|
||||
finalizerAgentSelectCard.addEventListener('click', () => {
|
||||
openAgentPickerModal(agents, agentSkillsMap, finalizerAgentSelect.value, (selectedId) => {
|
||||
finalizerAgentSelect.value = selectedId || '';
|
||||
renderAgentInCard(finalizerAgentSelectCard, selectedId ? agentMap[selectedId] : null, 'fa-flag-checkered', 'Kein Finalizer-Agent');
|
||||
});
|
||||
});
|
||||
|
||||
more.addEventListener('click', () => {
|
||||
const content = more.querySelector('[hidden]');
|
||||
while (content.firstChild)
|
||||
@@ -189,6 +207,7 @@ registerPage('session', {
|
||||
agentId: agentSelect.value || null,
|
||||
reviewAgentId: reviewAgentSelect.value || null,
|
||||
maxReviewIterations: parseInt(maxReviewIterationsEl.value) || 3,
|
||||
finalizerAgentId: finalizerAgentSelect.value || null,
|
||||
context: _sessionContextMde.value().trim() || null,
|
||||
workspace: workspaceEl.value.trim() || null,
|
||||
customCommands: commands,
|
||||
|
||||
@@ -26,10 +26,10 @@ registerPage('shared', {
|
||||
|
||||
for (const msg of messages) {
|
||||
const c = msg.content || '';
|
||||
const isReview = c.startsWith('[Review:') || c.startsWith('[Review-Feedback');
|
||||
const isReview = c.startsWith('[Review:') || c.startsWith('[Review-Feedback') || c.startsWith('[Finalizer:');
|
||||
const div = document.createElement('div');
|
||||
if (isReview) {
|
||||
div.className = 'msg review-bubble markdown';
|
||||
div.className = 'msg review-bubble markdown' + (c.startsWith('[Finalizer:') ? ' finalizer-bubble' : '');
|
||||
} else {
|
||||
div.className = 'msg ' + (msg.role === 'user' ? 'user' : 'assistant') + ' markdown';
|
||||
}
|
||||
@@ -63,11 +63,12 @@ function _mergeConsecutiveTexts(msgs) {
|
||||
const result = [];
|
||||
for (const msg of msgs) {
|
||||
const c = msg.content || '';
|
||||
const isReview = c.startsWith('[Review:') || c.startsWith('[Review-Feedback');
|
||||
const isReview = c.startsWith('[Review:') || c.startsWith('[Review-Feedback') || c.startsWith('[Finalizer:');
|
||||
if (msg.role === 'assistant' && !isReview) {
|
||||
const prev = result[result.length - 1];
|
||||
if (prev && prev.role === 'assistant'
|
||||
&& !(prev.content || '').startsWith('[Review:')
|
||||
&& !(prev.content || '').startsWith('[Finalizer:')
|
||||
&& prev.iteration === msg.iteration) {
|
||||
prev.content = (prev.content || '') + ' ' + c;
|
||||
if (msg.createdAt) prev.createdAt = msg.createdAt;
|
||||
|
||||
+18
-4
@@ -17,6 +17,7 @@ function buildSessionCard(session, agent) {
|
||||
const badges = [];
|
||||
if (agent) badges.push(`<span class="badge">${esc(agent.name)}</span>`);
|
||||
if (session.reviewAgentId) badges.push(`<span class="badge badge-feature" title="Review-Agent"><i class="fa fa-check-circle-o"></i></span>`);
|
||||
if (session.finalizerAgentId) badges.push(`<span class="badge badge-feature" title="Finalizer-Agent"><i class="fa fa-flag-checkered"></i></span>`);
|
||||
if (session.context) badges.push(`<span class="badge badge-feature" title="${esc(session.context)}"><i class="fa fa-file-text-o"></i></span>`);
|
||||
if (session.telegramConfigured) badges.push(`<span class="badge ${session.telegramLinked ? 'badge-feature' : 'badge-off'}" title="Telegram ${session.telegramLinked ? 'verbunden' : 'konfiguriert'}"><i class="fa fa-paper-plane"></i></span>`);
|
||||
|
||||
@@ -178,6 +179,11 @@ function renderMarkdown(text) {
|
||||
return '<span class="review-inline-header"><i class="fa fa-check-circle-o"></i> <strong>' + esc(agentName.trim()) + '</strong></span> ';
|
||||
});
|
||||
|
||||
// 4a2) [Finalizer: AgentName] prefix → Finalizer-Header
|
||||
html = html.replace(/\[Finalizer:\s*([^\]]+)\]\s*/g, function(match, agentName) {
|
||||
return '<span class="review-inline-header finalizer-inline-header"><i class="fa fa-flag-checkered"></i> <strong>' + esc(agentName.trim()) + '</strong></span> ';
|
||||
});
|
||||
|
||||
// 4b) [Review-Feedback von AgentName (Runde X/Y)]: → Review-Feedback-Header
|
||||
html = html.replace(/\[Review-Feedback von\s+([^\(]+)\(Runde\s+(\d+)\/(\d+)\)\]:\s*/g, function(match, agentName, round, maxRounds) {
|
||||
return '<div class="review-inline-feedback"><i class="fa fa-repeat"></i> <strong>Feedback von ' + esc(agentName.trim()) + '</strong> <span class="review-round">Runde ' + esc(round) + '/' + esc(maxRounds) + '</span></div>';
|
||||
@@ -682,12 +688,17 @@ let _reviewBubble = null;
|
||||
let _reviewTextEl = null;
|
||||
let _reviewText = '';
|
||||
|
||||
function addReviewBubble(type, data) {
|
||||
function addReviewBubble(type, data, agentType) {
|
||||
_reviewBubble = document.createElement('div');
|
||||
_reviewBubble.className = 'msg review-bubble streamed';
|
||||
if (agentType === 'finalizer') _reviewBubble.classList.add('finalizer-bubble');
|
||||
const header = document.createElement('div');
|
||||
header.className = 'review-header';
|
||||
header.innerHTML = '<i class="fa fa-check-circle-o"></i> <strong>' + esc(data.agentName || 'Review-Agent') + '</strong> <span class="review-round">Runde ' + data.round + '/' + data.maxRounds + '</span>';
|
||||
if (agentType === 'finalizer') {
|
||||
header.innerHTML = '<i class="fa fa-flag-checkered"></i> <strong>' + esc(data.agentName || 'Finalizer-Agent') + '</strong>';
|
||||
} else {
|
||||
header.innerHTML = '<i class="fa fa-check-circle-o"></i> <strong>' + esc(data.agentName || 'Review-Agent') + '</strong> <span class="review-round">Runde ' + data.round + '/' + data.maxRounds + '</span>';
|
||||
}
|
||||
_reviewBubble.appendChild(header);
|
||||
_reviewTextEl = document.createElement('div');
|
||||
_reviewTextEl.className = 'review-content markdown';
|
||||
@@ -713,11 +724,14 @@ function addReviewToolUse(data) {
|
||||
scrollDown();
|
||||
}
|
||||
|
||||
function finalizeReviewBubble(type, data) {
|
||||
function finalizeReviewBubble(type, data, agentType) {
|
||||
if (!_reviewBubble) return;
|
||||
const status = document.createElement('div');
|
||||
status.className = 'review-status';
|
||||
if (type === 'approved') {
|
||||
if (agentType === 'finalizer' || type === 'done') {
|
||||
status.className += ' review-approved';
|
||||
status.innerHTML = '<i class="fa fa-flag-checkered"></i> Abgeschlossen';
|
||||
} else if (type === 'approved') {
|
||||
status.className += ' review-approved';
|
||||
status.innerHTML = '<i class="fa fa-check"></i> Approved';
|
||||
} else {
|
||||
|
||||
Reference in new issue
Block a user