Add workflow-status telemetry view and make panel modes on-demand
Workflow-status: a new WorkflowProgressTracker singleton taps the
suppressed dynamic-workflow subagent events (those carrying
parent_tool_use_id) into per-session telemetry without altering the
existing guard; the snapshot is serialized into the new chains.workflow_info
column on FinalizeChain (ChainId-matched so a panel member of the same
session cannot clear it) and exposed via GET /api/sessions/{id}/workflow-status
(live -> persisted -> none). The header menu gains a 'Workflow-Status' entry
with a modal that live-polls every 2s while running and self-stops on close.
Panel on-demand: Fusion/MoA is now a transient per-request multiModelMode
flag (like plan/ultracode) instead of a persisted session column. Session
settings keep only the panel members + moa_layers; the mode <select> was
removed and the composer dropdown drives the mode per request.
Docs: ARCHITECTURE.md, ARCHITECTURE_QUICK.md and README.md updated for both
features plus the already-shipped z.ai/GLM provider, GLM 5.2+ effort
passthrough, web-search-prime MCP and the ultracode toggle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
5b1802efee
commit
b6e546f618
22 files changed
+591
-99
No files matched your search
+7
-1
@@ -60,10 +60,12 @@ function apiMe() { return _get('/api/auth/me', null); }
|
||||
|
||||
// ── Chat API ──
|
||||
|
||||
function apiChat(content, sessionId, signal, planMode, ultracode) {
|
||||
function apiChat(content, sessionId, signal, planMode, ultracode, multiModelMode) {
|
||||
var payload = { content: content, sessionId: sessionId };
|
||||
if (planMode) payload.planMode = true;
|
||||
if (ultracode) payload.ultracode = true;
|
||||
// Panel-Modus ist on-demand pro Anfrage (transient): nur fusion/moa durchreichen.
|
||||
if (multiModelMode === 'fusion' || multiModelMode === 'moa') payload.multiModelMode = multiModelMode;
|
||||
return fetch('/api/chat', { method: 'POST', headers: _jsonHeaders, body: JSON.stringify(payload), signal: signal });
|
||||
}
|
||||
|
||||
@@ -124,6 +126,10 @@ function apiGetPayloadPreview(sessionId, planMode) {
|
||||
return _get(url);
|
||||
}
|
||||
|
||||
function apiGetWorkflowStatus(sessionId) {
|
||||
return _get('/api/sessions/' + sessionId + '/workflow-status');
|
||||
}
|
||||
|
||||
function apiIsStreaming(sessionId) { return _get('/api/sessions/' + sessionId + '/streaming'); }
|
||||
function apiGitStatus(sessionId) { return _get('/api/sessions/' + sessionId + '/git-status', null); }
|
||||
|
||||
|
||||
+19
-46
@@ -223,19 +223,18 @@ function handleStreamEvent(eventType, data) {
|
||||
// der Betriebsmodus direkt gewählt wird. Der Button zeigt weiterhin das Symbol des aktiven
|
||||
// Modus; im Menü ist der aktive Eintrag markiert.
|
||||
//
|
||||
// Plan-Modus und Ultracode sind transient (pro Request, nicht in der Session persistiert) und
|
||||
// laufen ohne Panel → beim Wählen wird multiModelMode der Session auf 'off' gesetzt, damit kein
|
||||
// zuvor gewähltes Fusion/MoA-Panel nebenherläuft (das Menü erlaubt direkte Sprünge, anders als
|
||||
// der frühere Zyklus, der immer durch 'off' lief). Fusion/MoA werden in der Session persistiert
|
||||
// und server-seitig aus ctx.MultiModelMode gelesen.
|
||||
// ALLE Modi (Plan / Ultracode / Fusion / MoA) sind transient: sie gelten nur für die nächste
|
||||
// Anfrage, werden NICHT in der Session persistiert und beim Chat-Wechsel auf 'off' zurückgesetzt.
|
||||
// Der gewählte Modus wird pro Request als Flag mitgeschickt (planMode / ultracode / multiModelMode).
|
||||
// Die Panel-MITGLIEDER (PanelAgentIds) bleiben dagegen in den Session-Einstellungen persistent.
|
||||
//
|
||||
// _planModeActive / _ultracodeActive bleiben als Compat-Variablen (sendMessage + modals.js lesen
|
||||
// sie direkt).
|
||||
// sie direkt); Fusion/MoA werden direkt aus _compositeMode abgeleitet.
|
||||
|
||||
// Metadaten je Modus – eine Quelle für Button-Render UND Menü-Aufbau: Reihenfolge im Menü,
|
||||
// Icon (Button + Menü), Label, Button-Aktiv-Klasse, Input-Placeholder, kurze Beschreibung.
|
||||
const _COMPOSITE_MODES = [
|
||||
{ mode: 'off', icon: 'fa-ban', label: 'Aus', btnClass: null, placeholder: 'Nachricht...', desc: 'Normaler Einzel-Agent' },
|
||||
{ mode: 'off', icon: 'fa-user', label: 'Normal', btnClass: null, placeholder: 'Nachricht...', desc: 'Normaler Einzel-Agent' },
|
||||
{ mode: 'plan', icon: 'fa-map', label: 'Plan-Modus', btnClass: 'plan-mode-active', placeholder: 'Planen...', desc: 'Nur analysieren & planen (read-only)' },
|
||||
{ mode: 'ultracode', icon: 'fa-sitemap', label: 'Ultracode', btnClass: 'ultracode-active', placeholder: 'Ultracode – als Workflow...', desc: 'Dynamischer Workflow (mehrere Subagenten)' },
|
||||
{ mode: 'fusion', icon: 'fa-users', label: 'Fusion', btnClass: 'panel-mode-fusion', placeholder: 'Nachricht...', desc: 'Panel: Antworten fusionieren' },
|
||||
@@ -253,14 +252,6 @@ let _ultracodeActive = false; // true genau wenn _compositeMode === 'ultracode
|
||||
let _compositeMenuEl = null;
|
||||
let _compositeMenuCloseHandler = null;
|
||||
|
||||
// Serialisierte Modus-Persistenz. Schnell hintereinander gewählte Modi feuern sonst ÜBERLAPPENDE
|
||||
// PATCHes, deren Commit-Reihenfolge nicht garantiert ist → ein Zwischenwert kann den letzten
|
||||
// überschreiben. Und ein direkt danach gesendeter Chat-Request lädt die Session, BEVOR der Modus
|
||||
// committed ist → das Backend liest den alten Modus. Die Kette garantiert (a) Reihenfolge (letzte
|
||||
// Wahl gewinnt) und (b) dass sendMessage das offene PATCH abwarten kann. Sie rejected nie (Fehler
|
||||
// werden hier geschluckt).
|
||||
let _modePersist = Promise.resolve();
|
||||
|
||||
function _compositeMeta(mode) {
|
||||
for (var i = 0; i < _COMPOSITE_MODES.length; i++) {
|
||||
if (_COMPOSITE_MODES[i].mode === mode) return _COMPOSITE_MODES[i];
|
||||
@@ -283,20 +274,22 @@ function _renderCompositeBtn() {
|
||||
if (input) input.placeholder = meta.placeholder;
|
||||
}
|
||||
|
||||
// Initialisiert den Button aus der geladenen Session (bei jedem Chat-Wechsel aufrufen).
|
||||
// Plan-Modus wird immer auf 'off' zurückgesetzt (transient).
|
||||
function initCompositeMode(panelMode, hasAgents) {
|
||||
// Initialisiert den Button bei jedem Chat-Wechsel. ALLE Modi (Plan/Ultracode/Fusion/MoA) sind
|
||||
// transient (pro Anfrage) → der Button startet immer auf 'off'. `hasAgents` steuert nur den Hinweis,
|
||||
// ob für Fusion/MoA überhaupt Panel-Agenten konfiguriert sind.
|
||||
function initCompositeMode(hasAgents) {
|
||||
_compositeHasAgents = !!hasAgents;
|
||||
_compositeMode = (panelMode === 'fusion' || panelMode === 'moa') ? panelMode : 'off';
|
||||
_compositeMode = 'off';
|
||||
_planModeActive = false;
|
||||
_ultracodeActive = false;
|
||||
_renderCompositeBtn();
|
||||
}
|
||||
|
||||
// Setzt den Modus direkt (aus dem Menü gewählt).
|
||||
// Persistiert das EFFEKTIVE Panel der Session (off/fusion/moa): für plan/ultracode also 'off',
|
||||
// damit kein zuvor gewähltes Panel nebenherläuft. Plan/Ultracode selbst sind transient (Request-Flags).
|
||||
async function setCompositeMode(mode) {
|
||||
// Setzt den Modus direkt (aus dem Menü gewählt). ALLE Modi sind transient (Request-Flags) –
|
||||
// nichts wird mehr persistiert. Plan/Ultracode/Fusion/MoA wirken nur für die nächste Anfrage
|
||||
// und werden beim Chat-Wechsel auf 'off' zurückgesetzt. Die Panel-Mitglieder bleiben dagegen
|
||||
// persistent (in den Session-Einstellungen festgelegt).
|
||||
function setCompositeMode(mode) {
|
||||
if (mode === _compositeMode) return; // schon aktiv → nichts tun
|
||||
if (_compositeMeta(mode).mode !== mode) return; // unbekannter Modus → ignorieren
|
||||
|
||||
@@ -305,23 +298,9 @@ async function setCompositeMode(mode) {
|
||||
_ultracodeActive = (mode === 'ultracode');
|
||||
_renderCompositeBtn();
|
||||
|
||||
// Effektiver Panel-Modus: nur Fusion/MoA aktivieren ein Panel, alles andere 'off'.
|
||||
// Auf die serialisierte Kette legen, damit aufeinanderfolgende Wahlen in Reihenfolge committen
|
||||
// (letzte gewinnt) und sendMessage das offene PATCH abwarten kann.
|
||||
var panelMode = (mode === 'fusion' || mode === 'moa') ? mode : 'off';
|
||||
var sid = getCurrentSessionId();
|
||||
if (sid) {
|
||||
_modePersist = _modePersist
|
||||
.then(function () { return apiUpdateSession(sid, { multiModelMode: panelMode }); })
|
||||
.catch(function (e) {
|
||||
console.error('Modus speichern fehlgeschlagen:', e);
|
||||
if (typeof showToast === 'function') showToast('Modus konnte nicht gespeichert werden', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof showToast === 'function') {
|
||||
if (mode === 'off') {
|
||||
showToast('Modus: Aus', 'info');
|
||||
showToast('Modus: Normal', 'info');
|
||||
} else if (mode === 'plan') {
|
||||
showToast('Plan-Modus aktiv', 'info');
|
||||
} else if (mode === 'ultracode') {
|
||||
@@ -669,12 +648,7 @@ async function sendMessage() {
|
||||
input.value = '';
|
||||
input.dispatchEvent(new Event('input'));
|
||||
|
||||
// Ein evtl. noch offenes Modus-PATCH (Fusion/MoA/Aus aus dem Toggle) MUSS committed sein,
|
||||
// bevor der Chat-Request die Session lädt – sonst läuft die Chain mit dem alten Modus
|
||||
// (z.B. MoA → nur 1 Runde, weil das Backend noch 'fusion' liest).
|
||||
await _modePersist;
|
||||
|
||||
await _runStream((signal) => apiChat(content, getCurrentSessionId(), signal, _planModeActive, _ultracodeActive));
|
||||
await _runStream((signal) => apiChat(content, getCurrentSessionId(), signal, _planModeActive, _ultracodeActive, _compositeMode));
|
||||
}
|
||||
|
||||
// ── Steering (Einwurf in die laufende Chain) ──
|
||||
@@ -710,8 +684,7 @@ async function sendMessageDirect(text) {
|
||||
if (isStreaming || !text) return;
|
||||
addUserBubble(text, true);
|
||||
scrollDownForce();
|
||||
await _modePersist; // offenes Modus-PATCH abwarten (s. sendMessage)
|
||||
await _runStream((signal) => apiChat(text, getCurrentSessionId(), signal, _planModeActive));
|
||||
await _runStream((signal) => apiChat(text, getCurrentSessionId(), signal, _planModeActive, _ultracodeActive, _compositeMode));
|
||||
}
|
||||
|
||||
// ── Choice Click Handler ──
|
||||
|
||||
@@ -96,6 +96,109 @@ async function showPayloadPreviewModal(sessionId) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Workflow-Status Modal (Ultracode) ──
|
||||
|
||||
function _wfNum(n) { return Number(n || 0).toLocaleString('de-DE'); }
|
||||
function _wfTime(t) { try { return new Date(t).toLocaleTimeString('de-DE'); } catch (e) { return ''; } }
|
||||
|
||||
function _renderWorkflowStatus(body, data) {
|
||||
var status = (data && data.status) || 'none';
|
||||
var wf = data && data.workflow;
|
||||
|
||||
var meta = {
|
||||
running: { label: 'Workflow läuft', cls: 'wf-running', icon: 'fa-spinner fa-spin' },
|
||||
completed: { label: 'Workflow abgeschlossen', cls: 'wf-done', icon: 'fa-check-circle' },
|
||||
none: { label: 'Kein Workflow für diese Session', cls: 'wf-none', icon: 'fa-ban' }
|
||||
};
|
||||
var sm = meta[status] || meta.none;
|
||||
|
||||
body.innerHTML = '';
|
||||
|
||||
var head = document.createElement('div');
|
||||
head.className = 'wf-status-head ' + sm.cls;
|
||||
head.innerHTML = '<i class="fa ' + sm.icon + '"></i> ' + esc(sm.label);
|
||||
body.appendChild(head);
|
||||
|
||||
if (!wf) {
|
||||
var empty = document.createElement('div');
|
||||
empty.className = 'wf-empty';
|
||||
empty.textContent = 'Für diese Session wurde noch kein dynamischer Workflow (Ultracode) ausgeführt.';
|
||||
body.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
var stats = [
|
||||
{ label: 'Subagenten', value: _wfNum(wf.subagentCount) },
|
||||
{ label: 'Tool-Aufrufe', value: _wfNum(wf.subagentToolCalls) },
|
||||
{ label: 'Input-Tokens', value: _wfNum(wf.subagentInputTokens) },
|
||||
{ label: 'Output-Tokens', value: _wfNum(wf.subagentOutputTokens) }
|
||||
];
|
||||
var grid = document.createElement('div');
|
||||
grid.className = 'wf-stat-grid';
|
||||
stats.forEach(function (s) {
|
||||
var cell = document.createElement('div');
|
||||
cell.className = 'wf-stat';
|
||||
cell.innerHTML = '<div class="wf-stat-val">' + esc(String(s.value)) +
|
||||
'</div><div class="wf-stat-lbl">' + esc(s.label) + '</div>';
|
||||
grid.appendChild(cell);
|
||||
});
|
||||
body.appendChild(grid);
|
||||
|
||||
var tb = wf.toolBreakdown || {};
|
||||
var keys = Object.keys(tb).sort(function (a, b) { return tb[b] - tb[a]; });
|
||||
if (keys.length) {
|
||||
var sec = document.createElement('div');
|
||||
sec.className = 'wf-section';
|
||||
sec.innerHTML = '<div class="wf-section-lbl">Tool-Aufschlüsselung</div>';
|
||||
var list = document.createElement('div');
|
||||
list.className = 'wf-tool-list';
|
||||
keys.forEach(function (k) {
|
||||
var row = document.createElement('div');
|
||||
row.className = 'wf-tool-row';
|
||||
row.innerHTML = '<span class="wf-tool-name">' + esc(k) +
|
||||
'</span><span class="wf-tool-count">' + esc(String(tb[k])) + '</span>';
|
||||
list.appendChild(row);
|
||||
});
|
||||
sec.appendChild(list);
|
||||
body.appendChild(sec);
|
||||
}
|
||||
|
||||
if (wf.startedAt) {
|
||||
var ts = document.createElement('div');
|
||||
ts.className = 'wf-meta';
|
||||
ts.textContent = 'Start: ' + _wfTime(wf.startedAt) +
|
||||
(wf.lastActivityAt ? ' · letzte Aktivität: ' + _wfTime(wf.lastActivityAt) : '');
|
||||
body.appendChild(ts);
|
||||
}
|
||||
}
|
||||
|
||||
async function showWorkflowStatusModal(sessionId) {
|
||||
const { body, overlay } = _createModal('Workflow-Status', { modal: 'workflow-status-modal' });
|
||||
body.innerHTML = '<div class="sp-loading">Laden...</div>';
|
||||
|
||||
var poll = async function () {
|
||||
try {
|
||||
var data = await apiGetWorkflowStatus(sessionId);
|
||||
_renderWorkflowStatus(body, data);
|
||||
return data && data.status;
|
||||
} catch (e) {
|
||||
body.innerHTML = '<div class="sp-loading text-danger">Fehler beim Laden des Workflow-Status</div>';
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
var status = await poll();
|
||||
|
||||
// Live-Refresh, solange ein Workflow läuft – endet automatisch beim Schließen des Modals.
|
||||
if (status === 'running') {
|
||||
var timer = setInterval(async function () {
|
||||
if (!document.body.contains(overlay)) { clearInterval(timer); return; }
|
||||
var s = await poll();
|
||||
if (s !== 'running') clearInterval(timer);
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Command Output Modal ──
|
||||
|
||||
function showCommandOutputModal(name, command, result) {
|
||||
|
||||
@@ -435,6 +435,13 @@ registerPage('chat', {
|
||||
showPayloadPreviewModal(sessionId);
|
||||
});
|
||||
|
||||
// Workflow-Status anzeigen
|
||||
const workflowStatusBtn = document.getElementById('workflowStatusBtn');
|
||||
if (workflowStatusBtn) workflowStatusBtn.addEventListener('click', () => {
|
||||
if (chatMenuDropdown) chatMenuDropdown.hidden = true;
|
||||
showWorkflowStatusModal(sessionId);
|
||||
});
|
||||
|
||||
// Archivieren
|
||||
const archiveBtn = document.getElementById('archiveBtn');
|
||||
if (archiveBtn) archiveBtn.addEventListener('click', () => {
|
||||
@@ -577,7 +584,7 @@ registerPage('chat', {
|
||||
const compositeModeBtn = document.getElementById('planModeBtn');
|
||||
if (compositeModeBtn) {
|
||||
const hasPanelAgents = !!(session.panelAgentIds && String(session.panelAgentIds).trim());
|
||||
initCompositeMode(session.multiModelMode, hasPanelAgents);
|
||||
initCompositeMode(hasPanelAgents);
|
||||
compositeModeBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
toggleCompositeMenu(compositeModeBtn);
|
||||
|
||||
@@ -29,11 +29,8 @@ registerPage('session', {
|
||||
const maxReviewIterationsEl = document.getElementById('maxReviewIterations');
|
||||
const finalizerAgentSelect = document.getElementById('finalizerAgentSelect');
|
||||
const finalizerAgentSelectCard = document.getElementById('finalizerAgentSelectCard');
|
||||
const panelModeEl = document.getElementById('multiModelMode');
|
||||
const panelAgentsRow = document.getElementById('panelAgentsRow');
|
||||
const panelAgentsCard = document.getElementById('panelAgentsCard');
|
||||
const panelAgentIdsEl = document.getElementById('panelAgentIds');
|
||||
const moaLayersRow = document.getElementById('moaLayersRow');
|
||||
const moaLayersEl = document.getElementById('moaLayers');
|
||||
const imagePreview = document.getElementById('sessionImagePreview');
|
||||
const saveBtn = document.getElementById('sessionSaveBtn');
|
||||
@@ -148,16 +145,11 @@ registerPage('session', {
|
||||
panelAgentsCard.innerHTML = '<div>' + chips + '</div>'
|
||||
+ '<span class="agent-select-change"><i class="fa fa-pencil"></i></span>';
|
||||
}
|
||||
function updatePanelVisibility() {
|
||||
panelAgentsRow.hidden = (panelModeEl.value === 'off');
|
||||
if (moaLayersRow) moaLayersRow.hidden = (panelModeEl.value !== 'moa');
|
||||
}
|
||||
panelModeEl.value = (session && session.multiModelMode) || 'off';
|
||||
// Panel-Modus (Fusion/MoA) wird on-demand im Chat gewählt (nicht mehr hier persistiert).
|
||||
// In den Settings legt man nur noch die Panel-Mitglieder + MoA-Runden fest (immer sichtbar).
|
||||
panelAgentIdsEl.value = (session && session.panelAgentIds) || '';
|
||||
if (moaLayersEl) moaLayersEl.value = (session && session.moaLayers) || 2;
|
||||
renderPanelInCard();
|
||||
updatePanelVisibility();
|
||||
panelModeEl.addEventListener('change', updatePanelVisibility);
|
||||
panelAgentsCard.addEventListener('click', function() {
|
||||
var currentIds = (panelAgentIdsEl.value || '').split(',').map(function(s){ return s.trim(); }).filter(Boolean);
|
||||
openMultiAgentPickerModal(agents, agentSkillsMap, currentIds, function(newIds) {
|
||||
@@ -217,7 +209,6 @@ registerPage('session', {
|
||||
maxReviewIterations: parseInt(maxReviewIterationsEl.value) || 3,
|
||||
finalizerAgentId: finalizerAgentSelect.value || null,
|
||||
preparerAgentId: preparerAgentSelect.value || null,
|
||||
multiModelMode: panelModeEl.value || 'off',
|
||||
panelAgentIds: panelAgentIdsEl.value || null,
|
||||
moaLayers: moaLayersEl ? (parseInt(moaLayersEl.value, 10) || 2) : 2,
|
||||
context: _sessionContextMde.value().trim() || null,
|
||||
|
||||
Reference in new issue
Block a user