Replace mode toggle cycle with a dropdown menu

The composite mode control (#planModeBtn) now opens a dropdown menu
instead of cycling, following the emoji-picker pattern: each mode is an
entry with icon + label + description, the active one marked with a
check. Direct jumps between modes are now possible, so selecting
plan/ultracode resets the persisted panel mode to avoid running both the
panel and the CLI worker. Unified the active button states.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MelekiandClaude Opus 4.8 committed 2026-06-20 15:57:34 +02:00
1 parent d5854a3f4b
commit 5b1802efee
4 files changed
+205 -77

No files matched your search

+67 -8
View File
@@ -439,23 +439,82 @@
/* Unified Modus-Toggle: Aus / Plan / Fusion / MoA (ein einziger #planModeBtn) */
#planModeBtn.plan-mode-active {
color: var(--primary);
background: color-mix(in srgb, var(--primary) 15%, transparent);
border-radius: var(--border-radius);
background: color-mix(in srgb, var(--primary) 25%, transparent);
}
#planModeBtn.panel-mode-fusion {
color: var(--primary);
background: color-mix(in srgb, var(--primary) 15%, transparent);
border-radius: var(--border-radius);
background: color-mix(in srgb, var(--primary) 25%, transparent);
}
#planModeBtn.panel-mode-moa {
color: var(--primary);
background: color-mix(in srgb, var(--primary) 28%, transparent);
border-radius: var(--border-radius);
background: color-mix(in srgb, var(--primary) 25%, transparent);
}
#planModeBtn.ultracode-active {
color: var(--info);
background: color-mix(in srgb, var(--info) 18%, transparent);
color: var(--primary);
background: color-mix(in srgb, var(--primary) 25%, transparent);
}
/* Modus-Menü (Dropdown: Aus / Plan / Ultracode / Fusion / MoA) */
.composite-menu {
position: fixed;
z-index: 10000;
background: var(--f2-bg);
border: var(--border-width) solid var(--f2-border);
border-radius: var(--border-radius);
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 4px 16px rgb(0 0 0 / 0.25);
min-width: 16em;
padding-block: calc(var(--badge-spacing) / 2);
}
.composite-menu-item {
display: flex;
align-items: center;
gap: var(--badge-spacing);
padding: var(--menu-item-padding);
background: none;
border: none;
border-radius: unset;
text-align: left;
white-space: nowrap;
cursor: pointer;
color: var(--text);
}
.composite-menu-item:hover {
background: var(--f3-bg);
border: none;
}
.composite-menu-item.active {
color: var(--primary);
}
.composite-menu-icon {
width: 1.3em;
text-align: center;
flex-shrink: 0;
}
.composite-menu-text {
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
}
.composite-menu-desc {
font-size: var(--font-size);
opacity: var(--semi-transparent);
}
.composite-menu-check {
margin-left: auto;
flex-shrink: 0;
color: var(--primary);
}
/* Emoji Picker */
+1 -1
View File
@@ -458,7 +458,7 @@
<input type="file" id="fileInput" accept="image/jpeg,image/png,image/gif,image/webp,application/pdf" multiple hidden>
<button id="fileBtn" class="icon-btn" title="Datei anhängen"><i class="fa fa-paperclip"></i></button>
<button id="explorerBtn" class="icon-btn" title="Workspace durchsuchen"><i class="fa fa-folder"></i></button>
<button id="planModeBtn" class="icon-btn" title="Plan-Modus / Panel umschalten"><i class="fa fa-map"></i></button>
<button id="planModeBtn" class="icon-btn" title="Modus wählen"><i class="fa fa-ban"></i></button>
<textarea id="input" placeholder="Nachricht..." rows="1"></textarea>
<button id="emojiBtn" class="icon-btn" title="Emoji einfügen"><i class="fa fa-smile-o"></i></button>
<button id="send" class="icon-btn"><i class="fa fa-paper-plane"></i></button>
+131 -66
View File
@@ -217,65 +217,70 @@ function handleStreamEvent(eventType, data) {
}
}
// ── Unified Modus-Toggle (Aus / Plan / Fusion / MoA) ──
// ── Unified Modus-Menü (Aus / Plan / Ultracode / Fusion / MoA) ──
//
// Ein einziger Button (#planModeBtn) schaltet zyklisch durch alle Betriebsmodi:
// Aus → Plan-Modus → Fusion (Panel) → MoA (Panel) → Aus
// Der Button (#planModeBtn) öffnet ein Dropdown-Menü (Muster wie der Emoji-Picker), aus dem
// der Betriebsmodus direkt gewählt wird. Der Button zeigt weiterhin das Symbol des aktiven
// Modus; im Menü ist der aktive Eintrag markiert.
//
// Plan-Modus ist transient (wird bei Session-Wechsel zurückgesetzt, nie in der Session
// persistiert). Fusion/MoA werden in der Session persistiert (apiUpdateSession) und
// server-seitig aus ctx.MultiModelMode gelesen – daher reicht ein PATCH.
// 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.
//
// _planModeActive bleibt als Compat-Variable (sendMessage + modals.js lesen sie direkt).
// _planModeActive / _ultracodeActive bleiben als Compat-Variablen (sendMessage + modals.js lesen
// sie direkt).
// 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: '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' },
{ mode: 'moa', icon: 'fa-clone', label: 'MoA', btnClass: 'panel-mode-moa', placeholder: 'Nachricht...', desc: 'Panel: Mixture-of-Agents' }
];
let _compositeMode = 'off'; // 'off' | 'plan' | 'ultracode' | 'fusion' | 'moa'
let _compositeHasAgents = false;
// Compat-Variablen – werden durch cycleCompositeMode / initCompositeMode synchron gehalten
// Compat-Variablen – werden durch setCompositeMode / initCompositeMode synchron gehalten
let _planModeActive = false; // true genau wenn _compositeMode === 'plan'
let _ultracodeActive = false; // true genau wenn _compositeMode === 'ultracode' (transient, pro Request)
const _COMPOSITE_ORDER = ['off', 'plan', 'ultracode', 'fusion', 'moa'];
// Offenes Modus-Dropdown + Outside-Click-Handler (wie beim Emoji-Picker).
let _compositeMenuEl = null;
let _compositeMenuCloseHandler = null;
// Serialisierte Modus-Persistenz. Schnelles Durchklicken (off→plan→fusion→moa) feuert sonst
// ÜBERLAPPENDE PATCHes, deren Commit-Reihenfolge nicht garantiert ist → der zuletzt committete
// Zwischenwert 'fusion' kann 'moa' überschreiben. Und ein direkt danach gesendeter Chat-Request
// lädt die Session, BEVOR 'moa' committed ist → das Backend liest den alten Modus (Fusion → 1 Runde
// statt MoA). Die Kette garantiert (a) Reihenfolge (letzter Klick gewinnt) und (b) dass sendMessage
// das offene PATCH abwarten kann. Die Kette rejected nie (Fehler werden hier geschluckt).
// 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];
}
return _COMPOSITE_MODES[0];
}
function _renderCompositeBtn() {
var btn = $('#planModeBtn');
if (!btn) return;
btn.classList.remove('plan-mode-active', 'ultracode-active', 'panel-mode-fusion', 'panel-mode-moa');
var meta = _compositeMeta(_compositeMode);
var icon = btn.querySelector('i');
if (icon) icon.className = 'fa ' + meta.icon;
if (meta.btnClass) btn.classList.add(meta.btnClass);
btn.title = _compositeMode === 'off'
? 'Modus wählen (aktuell: Aus)'
: 'Modus: ' + meta.label + ' · klicken zum Wechseln';
var input = $('#input');
if (_compositeMode === 'plan') {
btn.classList.add('plan-mode-active');
if (icon) icon.className = 'fa fa-map';
btn.title = 'Plan-Modus aktiv · klicken → Ultracode';
if (input) input.placeholder = 'Planen...';
} else if (_compositeMode === 'ultracode') {
btn.classList.add('ultracode-active');
if (icon) icon.className = 'fa fa-sitemap';
btn.title = 'Ultracode aktiv (dynamischer Workflow) · klicken → Fusion';
if (input) input.placeholder = 'Ultracode – als Workflow...';
} else if (_compositeMode === 'fusion') {
btn.classList.add('panel-mode-fusion');
if (icon) icon.className = 'fa fa-users';
btn.title = 'Fusion aktiv · klicken → MoA';
if (input) input.placeholder = 'Nachricht...';
} else if (_compositeMode === 'moa') {
btn.classList.add('panel-mode-moa');
if (icon) icon.className = 'fa fa-clone';
btn.title = 'MoA aktiv · klicken → Aus';
if (input) input.placeholder = 'Nachricht...';
} else {
if (icon) icon.className = 'fa fa-map';
btn.title = 'Plan-Modus / Panel umschalten';
if (input) input.placeholder = 'Nachricht...';
}
if (input) input.placeholder = meta.placeholder;
}
// Initialisiert den Button aus der geladenen Session (bei jedem Chat-Wechsel aufrufen).
@@ -288,46 +293,106 @@ function initCompositeMode(panelMode, hasAgents) {
_renderCompositeBtn();
}
// Schaltet Aus → Plan → Fusion → MoA → Aus durch.
// Fusion/MoA werden sofort in die Session persistiert; Plan-Modus ist transient (kein Server-Call).
async function cycleCompositeMode() {
var idx = _COMPOSITE_ORDER.indexOf(_compositeMode);
_compositeMode = _COMPOSITE_ORDER[(idx + 1) % _COMPOSITE_ORDER.length];
_planModeActive = (_compositeMode === 'plan');
_ultracodeActive = (_compositeMode === 'ultracode');
// 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) {
if (mode === _compositeMode) return; // schon aktiv → nichts tun
if (_compositeMeta(mode).mode !== mode) return; // unbekannter Modus → ignorieren
_compositeMode = mode;
_planModeActive = (mode === 'plan');
_ultracodeActive = (mode === 'ultracode');
_renderCompositeBtn();
// Nur Fusion/MoA/Off brauchen einen Server-Call (Plan + Ultracode sind transient pro Request).
// Auf die serialisierte Kette legen, damit aufeinanderfolgende Klicks in Reihenfolge committen
// (letzter Klick gewinnt) und sendMessage das offene PATCH abwarten kann.
if (_compositeMode !== 'plan' && _compositeMode !== 'ultracode') {
var sid = getCurrentSessionId();
if (sid) {
var targetMode = _compositeMode;
_modePersist = _modePersist
.then(function () { return apiUpdateSession(sid, { multiModelMode: targetMode }); })
.catch(function (e) {
console.error('Modus speichern fehlgeschlagen:', e);
if (typeof showToast === 'function') showToast('Modus konnte nicht gespeichert werden', 'error');
});
}
// 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 (_compositeMode === 'off') {
if (mode === 'off') {
showToast('Modus: Aus', 'info');
} else if (_compositeMode === 'plan') {
} else if (mode === 'plan') {
showToast('Plan-Modus aktiv', 'info');
} else if (_compositeMode === 'ultracode') {
} else if (mode === 'ultracode') {
showToast('Ultracode aktiv – diese Anfrage läuft als dynamischer Workflow (mehr Tokens/Zeit)', 'info');
} else if (!_compositeHasAgents) {
showToast('Panel: ' + (_compositeMode === 'moa' ? 'MoA' : 'Fusion') + ' aktiv – aber keine Panel-Agenten konfiguriert (im Session-Bearbeiten festlegen)', 'info');
showToast('Panel: ' + (mode === 'moa' ? 'MoA' : 'Fusion') + ' aktiv – aber keine Panel-Agenten konfiguriert (im Session-Bearbeiten festlegen)', 'info');
} else {
showToast('Panel: ' + (_compositeMode === 'moa' ? 'MoA' : 'Fusion') + ' aktiv', 'success');
showToast('Panel: ' + (mode === 'moa' ? 'MoA' : 'Fusion') + ' aktiv', 'success');
}
}
}
// ── Modus-Dropdown-Menü (Muster wie der Emoji-Picker) ──
function _buildCompositeMenu() {
var menu = document.createElement('div');
menu.className = 'composite-menu';
_COMPOSITE_MODES.forEach(function (m) {
var active = (m.mode === _compositeMode);
var item = document.createElement('button');
item.className = 'composite-menu-item' + (active ? ' active' : '');
item.innerHTML =
'<i class="fa ' + m.icon + ' composite-menu-icon"></i>' +
'<span class="composite-menu-text">' +
'<span class="composite-menu-label">' + m.label + '</span>' +
'<span class="composite-menu-desc">' + m.desc + '</span>' +
'</span>' +
(active ? '<i class="fa fa-check composite-menu-check"></i>' : '');
item.addEventListener('click', function (e) {
e.stopPropagation();
closeCompositeMenu();
setCompositeMode(m.mode);
});
menu.appendChild(item);
});
return menu;
}
function toggleCompositeMenu(anchor) {
if (_compositeMenuEl) { closeCompositeMenu(); return; }
var menu = _buildCompositeMenu();
document.body.appendChild(menu);
_compositeMenuEl = menu;
// Positionierung: über dem Button (Input-Leiste ist unten), linksbündig zum Button.
var rect = anchor.getBoundingClientRect();
var h = menu.offsetHeight;
var w = menu.offsetWidth;
var top = rect.top - h - 6;
var left = rect.left;
if (top < 8) top = rect.bottom + 6; // kein Platz oben → unten
if (left + w > window.innerWidth - 8) left = window.innerWidth - w - 8; // rechts abgeschnitten
if (left < 8) left = 8;
menu.style.top = top + 'px';
menu.style.left = left + 'px';
_compositeMenuCloseHandler = function (e) {
if (!menu.contains(e.target) && e.target !== anchor && !anchor.contains(e.target)) closeCompositeMenu();
};
setTimeout(function () { document.addEventListener('click', _compositeMenuCloseHandler); }, 0);
}
function closeCompositeMenu() {
if (_compositeMenuEl) { _compositeMenuEl.remove(); _compositeMenuEl = null; }
if (_compositeMenuCloseHandler) {
document.removeEventListener('click', _compositeMenuCloseHandler);
_compositeMenuCloseHandler = null;
}
}
// ── Chat State ──
let isStreaming = false;
+6 -2
View File
@@ -572,12 +572,16 @@ registerPage('chat', {
});
}
// Unified Modus-Toggle (Aus / Plan / Fusion / MoA) – aus der Session initialisieren
// Unified Modus-Menü (Aus / Plan / Ultracode / Fusion / MoA) – aus der Session initialisieren.
// Klick öffnet ein Dropdown (wie der Emoji-Picker), aus dem der Modus direkt gewählt wird.
const compositeModeBtn = document.getElementById('planModeBtn');
if (compositeModeBtn) {
const hasPanelAgents = !!(session.panelAgentIds && String(session.panelAgentIds).trim());
initCompositeMode(session.multiModelMode, hasPanelAgents);
compositeModeBtn.addEventListener('click', cycleCompositeMode);
compositeModeBtn.addEventListener('click', (e) => {
e.stopPropagation();
toggleCompositeMenu(compositeModeBtn);
});
}
// File Upload