feat(extension): make the side panel usable

The settings were 18 rows on one level: the broker URL, the pace in
milliseconds and the model all shouted equally loud, although one is set
once and the other is changed daily.

- Model picker moved into the header. It is the switch you touch every day
  and it sat at the end of the settings list. Source depends on driveMode:
  the verified gateway models in direct mode, the broker's list otherwise;
  hidden when there is nothing to choose.
- Settings grouped into five collapsible sections (Verbindung & Antrieb,
  Verhalten, Anzeige & Sicherheit, Modell & Zugang, Memory). Only "Modell &
  Zugang" starts open — that is what first-time setup needs.
- Existing rows were wrapped in place, not moved, so every element id and
  every binding stays intact.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 18:21:10 +02:00
co-authored by Claude Opus 5
parent f943261c3c
commit 72d6a393fb
2 changed files with 124 additions and 2 deletions
+59
View File
@@ -73,6 +73,7 @@ const usageBarLabel = need<HTMLSpanElement>('usageBarLabel');
const mcpStatus = need<HTMLSpanElement>('mcpStatus'); const mcpStatus = need<HTMLSpanElement>('mcpStatus');
const targetTabEl = need<HTMLSpanElement>('targetTab'); const targetTabEl = need<HTMLSpanElement>('targetTab');
const modeBadge = need<HTMLSpanElement>('modeBadge'); const modeBadge = need<HTMLSpanElement>('modeBadge');
const headerModelSel = need<HTMLSelectElement>('headerModel');
const modeOffBtn = need<HTMLButtonElement>('modeOff'); const modeOffBtn = need<HTMLButtonElement>('modeOff');
const modeConfirmBtn = need<HTMLButtonElement>('modeConfirm'); const modeConfirmBtn = need<HTMLButtonElement>('modeConfirm');
const settingsPanel = need<HTMLDetailsElement>('settingsPanel'); const settingsPanel = need<HTMLDetailsElement>('settingsPanel');
@@ -386,6 +387,7 @@ function applyConfigToFields(cfg: ExtensionConfig): void {
setFieldValue(systemPromptInput, cfg.systemPrompt); setFieldValue(systemPromptInput, cfg.systemPrompt);
setFieldValue(nexusApiKeyInput, cfg.nexusApiKey); setFieldValue(nexusApiKeyInput, cfg.nexusApiKey);
applyNexusUi(cfg); applyNexusUi(cfg);
refreshHeaderModel(cfg);
if (document.activeElement !== memoryEnabledInput) memoryEnabledInput.checked = cfg.memoryEnabled; if (document.activeElement !== memoryEnabledInput) memoryEnabledInput.checked = cfg.memoryEnabled;
setFieldValue(memoryUrlInput, cfg.memoryUrl); setFieldValue(memoryUrlInput, cfg.memoryUrl);
@@ -453,6 +455,49 @@ function applyNexusUi(cfg: ExtensionConfig): void {
: 'GPT läuft über den Azure-OpenAI-Pfad und streamt.'; : 'GPT läuft über den Azure-OpenAI-Pfad und streamt.';
} }
/**
* Modellwahl in der Kopfzeile.
*
* Das Modell ist der Schalter, den man täglich anfasst — er lag bisher am Ende
* einer 18-zeiligen Einstellungsliste. Die Liste hängt vom Antrieb ab: im
* direct-Modus mit Anbieter 'nexus' die geprüften Gateway-Modelle, im
* Broker-Modus die vom Broker gemeldeten, sonst nur der eingetragene Freitext.
*/
function refreshHeaderModel(cfg: ExtensionConfig): void {
const options: { id: string; label: string }[] = [];
if (cfg.driveMode === 'direct') {
if (cfg.provider === 'nexus') {
for (const m of NEXUS_MODELS) options.push({ id: m.id, label: m.label });
}
if (cfg.model && !options.some(o => o.id === cfg.model)) {
options.unshift({ id: cfg.model, label: cfg.model });
}
} else {
for (const m of nexusModels) options.push({ id: m.id, label: m.label ?? m.id });
}
// Nichts zu wählen: den Schalter ausblenden statt eine leere Liste zeigen.
headerModelSel.hidden = options.length < 1;
if (headerModelSel.hidden) return;
const current = cfg.driveMode === 'direct' ? cfg.model : (nexusModelSel.value || cfg.model);
const same = headerModelSel.options.length === options.length
&& options.every((o, i) => headerModelSel.options[i]?.value === o.id);
if (!same) {
headerModelSel.textContent = '';
for (const o of options) {
const el = document.createElement('option');
el.value = o.id;
el.textContent = o.label;
headerModelSel.append(el);
}
}
if (document.activeElement !== headerModelSel && headerModelSel.value !== current) {
headerModelSel.value = current;
}
}
function applyRiskMode(mode: RiskMode): void { function applyRiskMode(mode: RiskMode): void {
const asking = mode === 'confirm'; const asking = mode === 'confirm';
modeOffBtn.setAttribute('aria-pressed', String(!asking)); modeOffBtn.setAttribute('aria-pressed', String(!asking));
@@ -783,6 +828,7 @@ async function loadNexusModels(silent = true): Promise<void> {
const data = await res.json(); const data = await res.json();
nexusModels = Array.isArray(data.models) ? data.models : []; nexusModels = Array.isArray(data.models) ? data.models : [];
renderNexusModels(); renderNexusModels();
refreshHeaderModel(config);
} catch (e: unknown) { } catch (e: unknown) {
nexusModels = []; nexusModels = [];
renderNexusModels(); renderNexusModels();
@@ -869,6 +915,19 @@ function syncNexusModelGroup(): void {
function wireNexusModels(): void { function wireNexusModels(): void {
nexusModelSel.addEventListener('change', () => setNexusModel(nexusModelSel.value)); nexusModelSel.addEventListener('change', () => setNexusModel(nexusModelSel.value));
headerModelSel.addEventListener('change', () => {
const id = headerModelSel.value;
if (!id) return;
if (config.driveMode === 'direct') {
patchConfigValue({ model: id });
setFieldValue(modelInput, id);
applyNexusUi(config);
} else {
// Im Broker-Modus entscheidet der Broker — dasselbe wie im Einstellungs-Dropdown.
setNexusModel(id);
}
});
nexusModelRefresh.addEventListener('click', () => loadNexusModels(false)); nexusModelRefresh.addEventListener('click', () => loadNexusModels(false));
nexusModelProbe.addEventListener('click', probeNexusModels); nexusModelProbe.addEventListener('click', probeNexusModels);
} }
+65 -2
View File
@@ -234,6 +234,54 @@
.settings[open] > summary::before { content: '▾ '; } .settings[open] > summary::before { content: '▾ '; }
.settings > summary:hover { color: var(--text); } .settings > summary:hover { color: var(--text); }
.settings-body { padding: 2px 10px 10px; } .settings-body { padding: 2px 10px 10px; }
/* Klappbare Abschnitte: 18 Zeilen auf einer Ebene sind keine Bedienung,
sondern eine Liste. Was man taeglich anfasst, steht offen; der Rest
liegt zugeklappt darunter. */
.sgroup { border-top: 1px solid var(--border); margin-top: 6px; }
.sgroup > summary {
padding: 6px 0 5px;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.5px;
text-transform: uppercase;
color: var(--text-mute);
cursor: pointer;
user-select: none;
list-style: none;
outline: none;
display: flex;
align-items: center;
gap: 5px;
}
.sgroup > summary::-webkit-details-marker { display: none; }
.sgroup > summary::before {
content: '';
width: 0; height: 0;
border-left: 4px solid currentColor;
border-top: 3px solid transparent;
border-bottom: 3px solid transparent;
transition: transform 0.15s;
flex: 0 0 auto;
}
.sgroup[open] > summary::before { transform: rotate(90deg); }
.sgroup > summary:hover { color: var(--text); }
.sgroup-body { padding: 1px 0 7px; }
/* Modellwahl in der Kopfzeile — der am haeufigsten benutzte Schalter
gehoert nicht ans Ende einer Einstellungsliste. */
#headerModel {
font-size: 10px;
max-width: 116px;
padding: 2px 4px;
background: var(--surface2);
color: var(--text-dim);
border: 1px solid var(--border-lt);
border-radius: 4px;
font-family: inherit;
cursor: pointer;
}
#headerModel:hover { color: var(--text); border-color: var(--accent); }
.row { .row {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -896,6 +944,7 @@
<span id="usageBarLabel"></span> <span id="usageBarLabel"></span>
</div> </div>
<span class="mcp-status" id="mcpStatus"></span> <span class="mcp-status" id="mcpStatus"></span>
<select id="headerModel" title="Aktives Modell"></select>
<span class="status-text" id="statusText">getrennt</span> <span class="status-text" id="statusText">getrennt</span>
<button class="icon-btn" id="settingsToggle" type="button" title="Einstellungen ein-/ausklappen" aria-label="Einstellungen"></button> <button class="icon-btn" id="settingsToggle" type="button" title="Einstellungen ein-/ausklappen" aria-label="Einstellungen"></button>
<button class="icon-btn" id="diagToggle" type="button" title="Diagnose / Tests" aria-label="Diagnose">🔬</button> <button class="icon-btn" id="diagToggle" type="button" title="Diagnose / Tests" aria-label="Diagnose">🔬</button>
@@ -980,6 +1029,8 @@
<details class="settings" id="settingsPanel"> <details class="settings" id="settingsPanel">
<summary>Einstellungen</summary> <summary>Einstellungen</summary>
<div class="settings-body"> <div class="settings-body">
<details class="sgroup"><summary>Verbindung &amp; Antrieb</summary><div class="sgroup-body">
<div class="row"> <div class="row">
<label for="brokerUrl">Broker</label> <label for="brokerUrl">Broker</label>
<input type="text" id="brokerUrl" spellcheck="false" autocomplete="off" placeholder="ws://127.0.0.1:8765/ext/ws"> <input type="text" id="brokerUrl" spellcheck="false" autocomplete="off" placeholder="ws://127.0.0.1:8765/ext/ws">
@@ -1013,6 +1064,9 @@
<div class="hint" id="nexusModelHint"></div> <div class="hint" id="nexusModelHint"></div>
</div> </div>
</div> </div>
</div></details>
<details class="sgroup"><summary>Verhalten</summary><div class="sgroup-body">
<div class="row"> <div class="row">
<label for="inputMode">Eingabe</label> <label for="inputMode">Eingabe</label>
<select id="inputMode"> <select id="inputMode">
@@ -1029,6 +1083,9 @@
<label for="paceMs">Tempo (ms)</label> <label for="paceMs">Tempo (ms)</label>
<input type="number" id="paceMs" min="0" max="5000" step="100" title="Pause zwischen Aktionen in Millisekunden. 0 = keine Pause."> <input type="number" id="paceMs" min="0" max="5000" step="100" title="Pause zwischen Aktionen in Millisekunden. 0 = keine Pause.">
</div> </div>
</div></details>
<details class="sgroup"><summary>Anzeige &amp; Sicherheit</summary><div class="sgroup-body">
<div class="row"> <div class="row">
<label for="pageOverlay">Anzeige</label> <label for="pageOverlay">Anzeige</label>
<select id="pageOverlay" title="Sichtbare Rückmeldung auf der Seite, an der der Agent arbeitet."> <select id="pageOverlay" title="Sichtbare Rückmeldung auf der Seite, an der der Agent arbeitet.">
@@ -1047,8 +1104,10 @@
<label for="blockMailSend" title="Klicks auf Senden-Schaltflächen in Mail-Oberflächen werden abgelehnt. Der Agent legt nur Entwürfe an.">Senden in Mails blockieren (nur Entwürfe)</label> <label for="blockMailSend" title="Klicks auf Senden-Schaltflächen in Mail-Oberflächen werden abgelehnt. Der Agent legt nur Entwürfe an.">Senden in Mails blockieren (nur Entwürfe)</label>
</div> </div>
</div></details>
<details class="sgroup" open id="directSection"><summary>Modell &amp; Zugang</summary><div class="sgroup-body">
<div id="directGroup" hidden> <div id="directGroup" hidden>
<div class="group-title">Direkter Modell-Zugriff</div>
<div class="row"> <div class="row">
<label for="provider">Anbieter</label> <label for="provider">Anbieter</label>
<select id="provider"> <select id="provider">
@@ -1085,7 +1144,9 @@
</div> </div>
</div> </div>
<div class="group-title">Memory (memory.cnull.net)</div> </div></details>
<details class="sgroup"><summary>Memory (memory.cnull.net)</summary><div class="sgroup-body">
<div class="row check"> <div class="row check">
<input type="checkbox" id="memoryEnabled"> <input type="checkbox" id="memoryEnabled">
<label for="memoryEnabled">Memory-Server anbinden</label> <label for="memoryEnabled">Memory-Server anbinden</label>
@@ -1128,6 +1189,8 @@
</div> </div>
<div class="hint">Änderungen werden sofort gespeichert.</div> <div class="hint">Änderungen werden sofort gespeichert.</div>
</div></details>
</div> </div>
</details> </details>