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:
@@ -73,6 +73,7 @@ const usageBarLabel = need<HTMLSpanElement>('usageBarLabel');
|
||||
const mcpStatus = need<HTMLSpanElement>('mcpStatus');
|
||||
const targetTabEl = need<HTMLSpanElement>('targetTab');
|
||||
const modeBadge = need<HTMLSpanElement>('modeBadge');
|
||||
const headerModelSel = need<HTMLSelectElement>('headerModel');
|
||||
const modeOffBtn = need<HTMLButtonElement>('modeOff');
|
||||
const modeConfirmBtn = need<HTMLButtonElement>('modeConfirm');
|
||||
const settingsPanel = need<HTMLDetailsElement>('settingsPanel');
|
||||
@@ -386,6 +387,7 @@ function applyConfigToFields(cfg: ExtensionConfig): void {
|
||||
setFieldValue(systemPromptInput, cfg.systemPrompt);
|
||||
setFieldValue(nexusApiKeyInput, cfg.nexusApiKey);
|
||||
applyNexusUi(cfg);
|
||||
refreshHeaderModel(cfg);
|
||||
|
||||
if (document.activeElement !== memoryEnabledInput) memoryEnabledInput.checked = cfg.memoryEnabled;
|
||||
setFieldValue(memoryUrlInput, cfg.memoryUrl);
|
||||
@@ -453,6 +455,49 @@ function applyNexusUi(cfg: ExtensionConfig): void {
|
||||
: '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 {
|
||||
const asking = mode === 'confirm';
|
||||
modeOffBtn.setAttribute('aria-pressed', String(!asking));
|
||||
@@ -783,6 +828,7 @@ async function loadNexusModels(silent = true): Promise<void> {
|
||||
const data = await res.json();
|
||||
nexusModels = Array.isArray(data.models) ? data.models : [];
|
||||
renderNexusModels();
|
||||
refreshHeaderModel(config);
|
||||
} catch (e: unknown) {
|
||||
nexusModels = [];
|
||||
renderNexusModels();
|
||||
@@ -869,6 +915,19 @@ function syncNexusModelGroup(): void {
|
||||
|
||||
function wireNexusModels(): void {
|
||||
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));
|
||||
nexusModelProbe.addEventListener('click', probeNexusModels);
|
||||
}
|
||||
|
||||
@@ -234,6 +234,54 @@
|
||||
.settings[open] > summary::before { content: '▾ '; }
|
||||
.settings > summary:hover { color: var(--text); }
|
||||
.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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -896,6 +944,7 @@
|
||||
<span id="usageBarLabel"></span>
|
||||
</div>
|
||||
<span class="mcp-status" id="mcpStatus"></span>
|
||||
<select id="headerModel" title="Aktives Modell"></select>
|
||||
<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="diagToggle" type="button" title="Diagnose / Tests" aria-label="Diagnose">🔬</button>
|
||||
@@ -980,6 +1029,8 @@
|
||||
<details class="settings" id="settingsPanel">
|
||||
<summary>Einstellungen</summary>
|
||||
<div class="settings-body">
|
||||
|
||||
<details class="sgroup"><summary>Verbindung & Antrieb</summary><div class="sgroup-body">
|
||||
<div class="row">
|
||||
<label for="brokerUrl">Broker</label>
|
||||
<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>
|
||||
</div>
|
||||
</div></details>
|
||||
|
||||
<details class="sgroup"><summary>Verhalten</summary><div class="sgroup-body">
|
||||
<div class="row">
|
||||
<label for="inputMode">Eingabe</label>
|
||||
<select id="inputMode">
|
||||
@@ -1029,6 +1083,9 @@
|
||||
<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.">
|
||||
</div>
|
||||
</div></details>
|
||||
|
||||
<details class="sgroup"><summary>Anzeige & Sicherheit</summary><div class="sgroup-body">
|
||||
<div class="row">
|
||||
<label for="pageOverlay">Anzeige</label>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
</div></details>
|
||||
|
||||
<details class="sgroup" open id="directSection"><summary>Modell & Zugang</summary><div class="sgroup-body">
|
||||
<div id="directGroup" hidden>
|
||||
<div class="group-title">Direkter Modell-Zugriff</div>
|
||||
<div class="row">
|
||||
<label for="provider">Anbieter</label>
|
||||
<select id="provider">
|
||||
@@ -1085,7 +1144,9 @@
|
||||
</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">
|
||||
<input type="checkbox" id="memoryEnabled">
|
||||
<label for="memoryEnabled">Memory-Server anbinden</label>
|
||||
@@ -1128,6 +1189,8 @@
|
||||
</div>
|
||||
|
||||
<div class="hint">Änderungen werden sofort gespeichert.</div>
|
||||
</div></details>
|
||||
|
||||
</div>
|
||||
</details>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user