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 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 & 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 & 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 & 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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user