micro-models-agent 2.9.0 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/dist/assets/app.js +5 -0
- package/dist/assets/index.html +30 -16
- package/dist/assets/js/dom.js +4 -0
- package/dist/assets/js/settings-controls.js +452 -0
- package/dist/assets/js/settings.js +659 -152
- package/dist/assets/styles.css +103 -9
- package/dist/i18n/en.json +301 -2
- package/dist/i18n/ru.json +301 -2
- package/dist/main.js +2315 -750
- package/package.json +1 -1
|
@@ -11,6 +11,7 @@ import { S, setPath, getPath, THEME_KEY, hooks } from "./store.js";
|
|
|
11
11
|
import { contextLabel } from "./format.js";
|
|
12
12
|
import { closeAllMenus, setSessOpen } from "./tabs.js";
|
|
13
13
|
import { setIndicator, closeConfirm, confirmOverlay, trapTab } from "./dialog.js";
|
|
14
|
+
import { controlFor, PANEL_SECTIONS, keyToPanel } from "./settings-controls.js";
|
|
14
15
|
|
|
15
16
|
function setSelButton(btn, label, value) {
|
|
16
17
|
btn.textContent = label + ":";
|
|
@@ -210,26 +211,9 @@ export function setConnStatus(disconnected) {
|
|
|
210
211
|
connStatus.textContent = disconnected ? "⋯ " + t("web.conn.reconnecting") : "";
|
|
211
212
|
}
|
|
212
213
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
reasoning: ["showReasoning", "reasoning.mode"],
|
|
217
|
-
security: [
|
|
218
|
-
"security.enabled",
|
|
219
|
-
"security.paths.enabled",
|
|
220
|
-
"security.contentScan.enabled",
|
|
221
|
-
"security.sessionEncryption.enabled",
|
|
222
|
-
],
|
|
223
|
-
advanced: ["logLevel", "version"],
|
|
224
|
-
agent: [
|
|
225
|
-
"moe.enabled",
|
|
226
|
-
"moe.maxReplanCycles",
|
|
227
|
-
"critic.enabled",
|
|
228
|
-
"critic.maxCalls",
|
|
229
|
-
"critic.maxRepairCycles",
|
|
230
|
-
"critic.timeoutMs",
|
|
231
|
-
],
|
|
232
|
-
};
|
|
214
|
+
// Panel → named sections lives in `settings-controls.js` (PANEL_SECTIONS).
|
|
215
|
+
// Custom panels (model/ui/browser…) render a bespoke body first and then their
|
|
216
|
+
// sections; purely generic panels (general/context/…) render sections only.
|
|
233
217
|
|
|
234
218
|
// Localized label for a registry key: `config.key.<canonical>`, falling back
|
|
235
219
|
// to the raw key when no translation exists (so a new key is never blank).
|
|
@@ -273,66 +257,65 @@ function descFor(key) {
|
|
|
273
257
|
return d === k ? "" : d;
|
|
274
258
|
}
|
|
275
259
|
|
|
260
|
+
// Generic per-type control (`settings-controls.js`): bool toggle, enum
|
|
261
|
+
// select, int/share/list/json inputs. `post` funnels through `postConfig`.
|
|
276
262
|
function configControl(def, key) {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
(def
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
b.classList.add("on");
|
|
296
|
-
b.setAttribute("aria-pressed", "true");
|
|
297
|
-
postConfig(key, v, false);
|
|
298
|
-
});
|
|
299
|
-
seg.appendChild(b);
|
|
300
|
-
});
|
|
301
|
-
return seg;
|
|
302
|
-
}
|
|
303
|
-
if (def.readOnly) {
|
|
304
|
-
const ro = el("span", "val dim", value == null ? "—" : String(value));
|
|
305
|
-
if (value != null) ro.title = String(value);
|
|
306
|
-
return ro;
|
|
307
|
-
}
|
|
308
|
-
const input = el("input");
|
|
309
|
-
input.setAttribute("aria-label", configLabel(def));
|
|
310
|
-
if (def.valueType === "int") {
|
|
311
|
-
input.type = "number";
|
|
312
|
-
if (def.min != null) input.min = String(def.min);
|
|
313
|
-
if (def.max != null) input.max = String(def.max);
|
|
314
|
-
if (def.step != null) input.step = String(def.step);
|
|
315
|
-
input.value = value == null ? "" : String(value);
|
|
316
|
-
} else if (def.valueType === "share") {
|
|
317
|
-
input.value = value == null ? "" : Math.round(Number(value) * 100) + "%";
|
|
318
|
-
} else {
|
|
319
|
-
input.value = value == null ? "" : String(value);
|
|
320
|
-
}
|
|
321
|
-
input.addEventListener("change", () => {
|
|
322
|
-
postConfig(key, input.value, false);
|
|
263
|
+
// Read-only keys carry their resolved value in the registry entry: the getter
|
|
264
|
+
// is authoritative and may differ from the raw config field (`version`).
|
|
265
|
+
const value =
|
|
266
|
+
def.readOnly && def.value !== undefined
|
|
267
|
+
? def.value
|
|
268
|
+
: S.state.config
|
|
269
|
+
? getPath(S.state.config, key)
|
|
270
|
+
: undefined;
|
|
271
|
+
return controlFor(def, value, {
|
|
272
|
+
label: configLabel(def),
|
|
273
|
+
enumLabel,
|
|
274
|
+
emptyLabel: t("config.enum.none"),
|
|
275
|
+
fieldLabel: (k) => {
|
|
276
|
+
const kk = "config.field." + k;
|
|
277
|
+
const tr = t(kk);
|
|
278
|
+
return tr === kk ? k : tr;
|
|
279
|
+
},
|
|
280
|
+
post: (v) => postConfig(key, v, false),
|
|
323
281
|
});
|
|
324
|
-
return input;
|
|
325
282
|
}
|
|
326
283
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
284
|
+
// One titled section card: uppercase title + optional one-line explanation,
|
|
285
|
+
// then its setting rows. Titles come from PANEL_SECTIONS so a section name can
|
|
286
|
+
// never drift from the keys under it.
|
|
287
|
+
function sectionCard(section) {
|
|
288
|
+
const card = el("div", "settings-section");
|
|
289
|
+
const head = el("div", "sec-head");
|
|
290
|
+
head.appendChild(el("span", "sec-title", t(section.titleKey)));
|
|
291
|
+
card.appendChild(head);
|
|
292
|
+
if (section.descKey) {
|
|
293
|
+
const d = t(section.descKey);
|
|
294
|
+
if (d !== section.descKey) card.appendChild(el("div", "sec-desc", d));
|
|
295
|
+
}
|
|
296
|
+
const body = el("div", "sec-body");
|
|
297
|
+
section.keys.forEach((key) => {
|
|
332
298
|
const def = defFor(key);
|
|
333
299
|
if (!def) return;
|
|
334
|
-
|
|
300
|
+
body.appendChild(row(configLabel(def), configControl(def, key), descFor(def.key)));
|
|
335
301
|
});
|
|
302
|
+
card.appendChild(body);
|
|
303
|
+
return card;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Render the PANEL_SECTIONS table for a panel. Panels with custom bodies call
|
|
307
|
+
// this after their own content so bespoke widgets stay on top.
|
|
308
|
+
function renderSections(panel, sections) {
|
|
309
|
+
const container = document.querySelector('[data-panel="' + panel + '"]');
|
|
310
|
+
if (!container) return;
|
|
311
|
+
sections.forEach((section) => container.appendChild(sectionCard(section)));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function renderPanel(panel) {
|
|
315
|
+
const container = document.querySelector('[data-panel="' + panel + '"]');
|
|
316
|
+
if (!container) return;
|
|
317
|
+
container.textContent = "";
|
|
318
|
+
renderSections(panel, PANEL_SECTIONS[panel] || []);
|
|
336
319
|
}
|
|
337
320
|
|
|
338
321
|
function providerEntries() {
|
|
@@ -389,6 +372,37 @@ function providerInput(value, type, placeholder) {
|
|
|
389
372
|
return input;
|
|
390
373
|
}
|
|
391
374
|
|
|
375
|
+
function providerSelect(options, value) {
|
|
376
|
+
const sel = el("select");
|
|
377
|
+
const none = el("option", "", "—");
|
|
378
|
+
none.value = "";
|
|
379
|
+
sel.appendChild(none);
|
|
380
|
+
options.forEach((v) => {
|
|
381
|
+
const o = el("option", "", v);
|
|
382
|
+
o.value = v;
|
|
383
|
+
if (v === value) o.selected = true;
|
|
384
|
+
sel.appendChild(o);
|
|
385
|
+
});
|
|
386
|
+
if (value == null) sel.value = "";
|
|
387
|
+
return sel;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function providerCheck(value) {
|
|
391
|
+
const input = el("input");
|
|
392
|
+
input.type = "checkbox";
|
|
393
|
+
input.checked = value === true;
|
|
394
|
+
return input;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const CACHE_FLAG_KEYS = [
|
|
398
|
+
"sessionHeader",
|
|
399
|
+
"requestCachePrompt",
|
|
400
|
+
"requestPromptCacheKey",
|
|
401
|
+
"requestSessionId",
|
|
402
|
+
"requestCacheControl",
|
|
403
|
+
"requestStreamUsage",
|
|
404
|
+
];
|
|
405
|
+
|
|
392
406
|
// A titled group inside a settings panel (models / providers).
|
|
393
407
|
function settingsSection(titleKey) {
|
|
394
408
|
const sec = el("div", "settings-section");
|
|
@@ -481,17 +495,49 @@ function providerAdvancedFields(editing) {
|
|
|
481
495
|
const adv = el("details", "pform-adv");
|
|
482
496
|
adv.appendChild(el("summary", "", t("web.settings.provider_advanced")));
|
|
483
497
|
const advGrid = el("div", "pform-grid");
|
|
498
|
+
const cache = editing.cache || {};
|
|
499
|
+
const retry = editing.retry || {};
|
|
484
500
|
const f = {
|
|
485
501
|
ctx: providerInput(editing.contextWindow, "number", "32768"),
|
|
486
502
|
prio: providerInput(editing.priority, "number", "0"),
|
|
487
503
|
rpm: providerInput(editing.rpm, "number", "60"),
|
|
488
504
|
par: providerInput(editing.parallel, "number", "1"),
|
|
505
|
+
maxTokens: providerInput(editing.maxCompletionTokens, "number", "8192"),
|
|
506
|
+
cacheMech: providerSelect(["auto", "explicit", "local", "none"], cache.mechanism),
|
|
507
|
+
cacheReport: providerSelect(["openai", "deepseek", "anthropic", "ollama", "none"], cache.report),
|
|
508
|
+
retryMax: providerInput(retry.maxRetries, "number", "3"),
|
|
509
|
+
retryBase: providerInput(retry.baseDelay, "number", "1000"),
|
|
510
|
+
retryMaxD: providerInput(retry.maxDelay, "number", "30000"),
|
|
489
511
|
};
|
|
512
|
+
f.cacheFlags = CACHE_FLAG_KEYS.map((key) => {
|
|
513
|
+
const box = providerCheck(cache[key]);
|
|
514
|
+
const wrap = el("label", "pform-check");
|
|
515
|
+
wrap.appendChild(box);
|
|
516
|
+
wrap.appendChild(el("span", "", key));
|
|
517
|
+
return { key, box, node: wrap };
|
|
518
|
+
});
|
|
490
519
|
advGrid.appendChild(pfield("web.settings.provider_context", f.ctx));
|
|
491
520
|
advGrid.appendChild(pfield("web.settings.provider_priority", f.prio));
|
|
492
521
|
advGrid.appendChild(pfield("web.settings.provider_rpm", f.rpm));
|
|
493
522
|
advGrid.appendChild(pfield("web.settings.provider_parallel", f.par));
|
|
523
|
+
advGrid.appendChild(pfield("web.settings.provider_max_tokens", f.maxTokens));
|
|
524
|
+
advGrid.appendChild(pfield("web.settings.provider_cache_mechanism", f.cacheMech));
|
|
525
|
+
advGrid.appendChild(pfield("web.settings.provider_cache_report", f.cacheReport));
|
|
494
526
|
adv.appendChild(advGrid);
|
|
527
|
+
const retryGroup = el("div", "pform-checks");
|
|
528
|
+
retryGroup.appendChild(el("span", "pform-checks-title", t("web.settings.provider_retry")));
|
|
529
|
+
[["maxRetries", f.retryMax], ["baseDelay", f.retryBase], ["maxDelay", f.retryMaxD]].forEach(([key, input]) => {
|
|
530
|
+
const wrap = el("label", "pform-check");
|
|
531
|
+
wrap.appendChild(el("span", "", key));
|
|
532
|
+
input.setAttribute("aria-label", key);
|
|
533
|
+
wrap.appendChild(input);
|
|
534
|
+
retryGroup.appendChild(wrap);
|
|
535
|
+
});
|
|
536
|
+
adv.appendChild(retryGroup);
|
|
537
|
+
const flagGroup = el("div", "pform-checks");
|
|
538
|
+
flagGroup.appendChild(el("span", "pform-checks-title", t("web.settings.provider_cache_flags")));
|
|
539
|
+
f.cacheFlags.forEach(({ node }) => flagGroup.appendChild(node));
|
|
540
|
+
adv.appendChild(flagGroup);
|
|
495
541
|
return { node: adv, ...f };
|
|
496
542
|
}
|
|
497
543
|
|
|
@@ -519,23 +565,41 @@ async function submitProviderForm(f, mode, editing) {
|
|
|
519
565
|
["priority", f.prio, 0],
|
|
520
566
|
["rpm", f.rpm, 1],
|
|
521
567
|
["parallel", f.par, 1],
|
|
568
|
+
["maxCompletionTokens", f.maxTokens, 512, 128000],
|
|
569
|
+
["maxRetries", f.retryMax, 0, 10],
|
|
570
|
+
["baseDelay", f.retryBase, 0, 60000],
|
|
571
|
+
["maxDelay", f.retryMaxD, 0, 300000],
|
|
522
572
|
];
|
|
573
|
+
const retry = {};
|
|
523
574
|
for (const entry of nums) {
|
|
524
575
|
const raw = entry[1].value.trim();
|
|
525
576
|
if (!raw) continue;
|
|
526
577
|
const n = Number(raw);
|
|
527
|
-
|
|
578
|
+
const max = entry[3] === undefined ? Number.MAX_SAFE_INTEGER : entry[3];
|
|
579
|
+
if (!Number.isInteger(n) || n < entry[2] || n > max) {
|
|
528
580
|
setIndicator(t("web.settings.provider_invalid"), true);
|
|
529
581
|
return;
|
|
530
582
|
}
|
|
531
|
-
|
|
583
|
+
if (entry[0] === "maxRetries" || entry[0] === "baseDelay" || entry[0] === "maxDelay") {
|
|
584
|
+
retry[entry[0]] = n;
|
|
585
|
+
} else {
|
|
586
|
+
body[entry[0]] = n;
|
|
587
|
+
}
|
|
532
588
|
}
|
|
589
|
+
if (Object.keys(retry).length) body.retry = retry;
|
|
590
|
+
const cache = {};
|
|
591
|
+
if (f.cacheMech.value) cache.mechanism = f.cacheMech.value;
|
|
592
|
+
if (f.cacheReport.value) cache.report = f.cacheReport.value;
|
|
593
|
+
const prevCache = (editing && editing.cache) || {};
|
|
594
|
+
f.cacheFlags.forEach(({ key, box }) => {
|
|
595
|
+
if (box.checked !== (prevCache[key] === true)) cache[key] = box.checked;
|
|
596
|
+
});
|
|
597
|
+
if (Object.keys(cache).length) body.cache = cache;
|
|
533
598
|
const r = await api("/api/providers", { method: "POST", body });
|
|
534
599
|
if (r.ok && r.data && r.data.ok) {
|
|
535
600
|
const persisted = r.data.persisted !== false;
|
|
536
|
-
S.state.providerForm = null;
|
|
537
601
|
setIndicator(persisted ? t("web.settings.saved") : r.data.warning || "not persisted", !persisted);
|
|
538
|
-
|
|
602
|
+
closeSubDialog();
|
|
539
603
|
await loadProviders();
|
|
540
604
|
await loadConfig();
|
|
541
605
|
} else {
|
|
@@ -676,6 +740,7 @@ function renderModelPanel() {
|
|
|
676
740
|
if (!entries.length) provList.appendChild(el("div", "placeholder", t("web.settings.providers_empty")));
|
|
677
741
|
provSec.body.appendChild(provList);
|
|
678
742
|
container.appendChild(provSec.el);
|
|
743
|
+
renderSections("model", PANEL_SECTIONS.model || []);
|
|
679
744
|
}
|
|
680
745
|
|
|
681
746
|
function renderUiPanel() {
|
|
@@ -698,25 +763,40 @@ function renderUiPanel() {
|
|
|
698
763
|
});
|
|
699
764
|
seg.appendChild(b);
|
|
700
765
|
});
|
|
701
|
-
|
|
766
|
+
// Theme is a browser-local preference (localStorage), not a config key, so it
|
|
767
|
+
// gets its own card before the generated sections.
|
|
768
|
+
const card = el("div", "settings-section");
|
|
769
|
+
const head = el("div", "sec-head");
|
|
770
|
+
head.appendChild(el("span", "sec-title", t("web.settings.sec.ui.theme")));
|
|
771
|
+
card.appendChild(head);
|
|
772
|
+
const desc = t("web.settings.sec.ui.theme.desc");
|
|
773
|
+
if (desc !== "web.settings.sec.ui.theme.desc") card.appendChild(el("div", "sec-desc", desc));
|
|
774
|
+
const body = el("div", "sec-body");
|
|
775
|
+
body.appendChild(row(t("web.settings.theme"), seg));
|
|
776
|
+
card.appendChild(body);
|
|
777
|
+
container.appendChild(card);
|
|
778
|
+
renderSections("ui", PANEL_SECTIONS.ui || []);
|
|
702
779
|
}
|
|
703
780
|
|
|
704
781
|
function renderSecurityPanel() {
|
|
705
782
|
const container = document.querySelector('[data-panel="security"]');
|
|
706
783
|
if (!container) return;
|
|
707
784
|
container.textContent = "";
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
785
|
+
renderSections("security", PANEL_SECTIONS.security || []);
|
|
786
|
+
const access = el("div", "settings-section");
|
|
787
|
+
const head = el("div", "sec-head");
|
|
788
|
+
head.appendChild(el("span", "sec-title", t("web.settings.sec.security.access")));
|
|
789
|
+
access.appendChild(head);
|
|
790
|
+
const body = el("div", "sec-body");
|
|
791
|
+
body.appendChild(
|
|
714
792
|
row(t("web.settings.web_token"), el("span", "val dim", S.state.webToken || t("web.settings.web_token_none")), t("config.desc.webToken")),
|
|
715
793
|
);
|
|
716
794
|
const note = el("div", "placeholder");
|
|
717
795
|
note.appendChild(el("span", "tag", t("web.settings.terminal_tag")));
|
|
718
796
|
note.appendChild(document.createTextNode(" " + t("web.settings.security_note")));
|
|
719
|
-
|
|
797
|
+
body.appendChild(note);
|
|
798
|
+
access.appendChild(body);
|
|
799
|
+
container.appendChild(access);
|
|
720
800
|
}
|
|
721
801
|
|
|
722
802
|
function placeholderFor(data, empty) {
|
|
@@ -735,40 +815,66 @@ function groupHeader(label, count) {
|
|
|
735
815
|
|
|
736
816
|
const MEMORY_CATEGORIES = ["conventions", "decisions", "errors", "facts"];
|
|
737
817
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
if (S.state.memory === undefined || S.state.memory === null) {
|
|
743
|
-
container.appendChild(placeholderFor(S.state.memory, t("web.settings.memory_unavailable")));
|
|
744
|
-
return;
|
|
745
|
-
}
|
|
746
|
-
const data = S.state.memory;
|
|
747
|
-
|
|
748
|
-
const addRow = el("div", "mem-add");
|
|
818
|
+
/** Nested dialog: pick a memory category and write the note. */
|
|
819
|
+
function openMemoryForm() {
|
|
820
|
+
const box = el("div", "pform");
|
|
821
|
+
const grid = el("div", "pform-grid");
|
|
749
822
|
const catSel = el("select");
|
|
750
823
|
catSel.setAttribute("aria-label", t("web.settings.memory_category"));
|
|
751
824
|
MEMORY_CATEGORIES.forEach((c) => catSel.appendChild(el("option", "", c)));
|
|
752
825
|
catSel.value = "facts";
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
826
|
+
grid.appendChild(mcpLiteralField(t("web.settings.memory_category"), catSel));
|
|
827
|
+
box.appendChild(grid);
|
|
828
|
+
const area = el("textarea");
|
|
829
|
+
area.rows = 5;
|
|
830
|
+
area.placeholder = t("web.settings.new_memory");
|
|
831
|
+
area.value = "";
|
|
832
|
+
box.appendChild(mcpLiteralField(t("web.settings.new_memory"), area));
|
|
833
|
+
const actions = el("div", "pform-actions");
|
|
834
|
+
actions.appendChild(
|
|
835
|
+
link(t("web.settings.save"), "", async () => {
|
|
836
|
+
const text = area.value.trim();
|
|
837
|
+
if (!text) {
|
|
838
|
+
area.focus();
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
762
841
|
const r = await api("/api/memory", { method: "POST", body: { category: catSel.value, text } });
|
|
763
842
|
if (r.ok && r.data && r.data.ok) {
|
|
764
|
-
|
|
843
|
+
closeSubDialog();
|
|
765
844
|
await loadMemory();
|
|
766
845
|
} else {
|
|
767
846
|
setIndicator(t("web.settings.add_failed"), true);
|
|
768
847
|
}
|
|
769
848
|
}),
|
|
770
849
|
);
|
|
771
|
-
|
|
850
|
+
actions.appendChild(link(t("web.settings.cancel"), "dim", () => closeSubDialog()));
|
|
851
|
+
box.appendChild(actions);
|
|
852
|
+
openSubDialog(t("web.settings.memory_add_title"), box, () => renderMemoryPanel());
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function renderMemoryPanel() {
|
|
856
|
+
const container = document.querySelector('[data-panel="memory"]');
|
|
857
|
+
if (!container) return;
|
|
858
|
+
container.textContent = "";
|
|
859
|
+
if (S.state.memory === undefined || S.state.memory === null) {
|
|
860
|
+
container.appendChild(placeholderFor(S.state.memory, t("web.settings.memory_unavailable")));
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
const data = S.state.memory;
|
|
864
|
+
|
|
865
|
+
// Adding an entry opens the shared nested dialog: a category + a multi-line
|
|
866
|
+
// note with room to write, instead of a cramped inline field.
|
|
867
|
+
const addBtn = el("button", "sec-add");
|
|
868
|
+
addBtn.type = "button";
|
|
869
|
+
addBtn.appendChild(icon("plus"));
|
|
870
|
+
addBtn.appendChild(el("span", "", t("web.settings.add")));
|
|
871
|
+
addBtn.addEventListener("click", () => openMemoryForm());
|
|
872
|
+
const addHead = el("div", "sec-head");
|
|
873
|
+
addHead.appendChild(el("span", "sec-title", t("web.settings.sec.memory.add")));
|
|
874
|
+
addHead.appendChild(addBtn);
|
|
875
|
+
const addSec = el("div", "settings-section");
|
|
876
|
+
addSec.appendChild(addHead);
|
|
877
|
+
container.appendChild(addSec);
|
|
772
878
|
|
|
773
879
|
const search = el("input", "searchbox");
|
|
774
880
|
search.placeholder = t("web.settings.search_memory");
|
|
@@ -907,6 +1013,12 @@ function renderPluginsPanel() {
|
|
|
907
1013
|
list.appendChild(pnote);
|
|
908
1014
|
|
|
909
1015
|
list.appendChild(groupHeader(t("web.settings.mcp_servers"), (data.mcp || []).length));
|
|
1016
|
+
const mcpAdd = el("button", "sec-add");
|
|
1017
|
+
mcpAdd.type = "button";
|
|
1018
|
+
mcpAdd.appendChild(icon("plus"));
|
|
1019
|
+
mcpAdd.appendChild(el("span", "", t("web.settings.mcp_add")));
|
|
1020
|
+
mcpAdd.addEventListener("click", () => openMcpForm("add", null));
|
|
1021
|
+
list.appendChild(mcpAdd);
|
|
910
1022
|
if (!(data.mcp || []).length) {
|
|
911
1023
|
const empty = el("div", "placeholder");
|
|
912
1024
|
empty.textContent = t("web.settings.no_mcp");
|
|
@@ -935,16 +1047,267 @@ function renderPluginsPanel() {
|
|
|
935
1047
|
nameEl.title = m.name;
|
|
936
1048
|
line.appendChild(nameEl);
|
|
937
1049
|
line.appendChild(el("span", "st", m.status || "—"));
|
|
1050
|
+
line.appendChild(chip(t("web.settings.edit"), "", () => openMcpForm("edit", m)));
|
|
1051
|
+
line.appendChild(chip(t("web.settings.delete"), "danger", async () => {
|
|
1052
|
+
const r = await api("/api/mcp/" + encodeURIComponent(m.id || m.name), { method: "DELETE" });
|
|
1053
|
+
if (r.ok && r.data && r.data.ok) {
|
|
1054
|
+
setIndicator(t("web.settings.mcp_saved"), false);
|
|
1055
|
+
await loadPlugins();
|
|
1056
|
+
} else {
|
|
1057
|
+
setIndicator((r.data && r.data.error) || t("web.settings.delete_failed"), true);
|
|
1058
|
+
}
|
|
1059
|
+
}));
|
|
938
1060
|
list.appendChild(line);
|
|
939
1061
|
});
|
|
1062
|
+
if (S.state.mcpForm) list.appendChild(mcpForm());
|
|
940
1063
|
if ((data.mcp || []).length) {
|
|
941
1064
|
const mnote = el("div", "settings-note");
|
|
942
1065
|
mnote.textContent = t("web.settings.mcp_note");
|
|
943
1066
|
list.appendChild(mnote);
|
|
944
1067
|
}
|
|
1068
|
+
renderLspSection(list);
|
|
945
1069
|
container.appendChild(list);
|
|
946
1070
|
}
|
|
947
1071
|
|
|
1072
|
+
// Inline MCP add/edit form. Secrets are never echoed by the API, so env/headers
|
|
1073
|
+
// start blank — leaving them blank on edit keeps the stored values (patch
|
|
1074
|
+
// semantics: absent keys are not touched).
|
|
1075
|
+
function mcpLiteralField(label, input) {
|
|
1076
|
+
const f = el("div", "pform-field");
|
|
1077
|
+
const lab = el("label", "", label);
|
|
1078
|
+
f.appendChild(lab);
|
|
1079
|
+
input.setAttribute("aria-label", label);
|
|
1080
|
+
f.appendChild(input);
|
|
1081
|
+
return f;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function mcpForm() {
|
|
1085
|
+
const mode = (S.state.mcpForm && S.state.mcpForm.mode) || "add";
|
|
1086
|
+
const editing = (S.state.mcpForm && S.state.mcpForm.entry) || {};
|
|
1087
|
+
const box = el("div", "pform");
|
|
1088
|
+
const grid = el("div", "pform-grid");
|
|
1089
|
+
const fName = providerInput(editing.name || "", "text", "my-server");
|
|
1090
|
+
const fTransport = providerSelect(["stdio", "sse", "http"], editing.transport || "stdio");
|
|
1091
|
+
const fCommand = providerInput(editing.command || "", "text", "npx");
|
|
1092
|
+
const fUrl = providerInput(editing.url || "", "text", "https://…");
|
|
1093
|
+
grid.appendChild(mcpLiteralField("name", fName));
|
|
1094
|
+
grid.appendChild(mcpLiteralField("transport", fTransport));
|
|
1095
|
+
grid.appendChild(mcpLiteralField("command", fCommand));
|
|
1096
|
+
grid.appendChild(mcpLiteralField("url", fUrl));
|
|
1097
|
+
box.appendChild(grid);
|
|
1098
|
+
const adv = el("details", "pform-adv");
|
|
1099
|
+
adv.appendChild(el("summary", "", t("web.settings.provider_advanced")));
|
|
1100
|
+
const fArgs = providerInput(editing.args ? JSON.stringify(editing.args) : "", "text", '["--stdio"]');
|
|
1101
|
+
const fTimeout = providerInput(editing.timeout != null ? String(editing.timeout) : "", "number", "30000");
|
|
1102
|
+
const fEnabled = providerCheck(editing.enabled !== false);
|
|
1103
|
+
adv.appendChild(mcpLiteralField("args (JSON)", fArgs));
|
|
1104
|
+
adv.appendChild(mcpLiteralField("timeout (ms)", fTimeout));
|
|
1105
|
+
const envKeys = Array.isArray(editing.envKeys) ? editing.envKeys : [];
|
|
1106
|
+
const headerKeys = Array.isArray(editing.headerKeys) ? editing.headerKeys : [];
|
|
1107
|
+
const fEnv = el("textarea");
|
|
1108
|
+
fEnv.placeholder = envKeys.length ? "stored: " + envKeys.join(", ") + " — blank keeps" : '{"TOKEN": "…"}';
|
|
1109
|
+
const fHeaders = el("textarea");
|
|
1110
|
+
fHeaders.placeholder = headerKeys.length ? "stored: " + headerKeys.join(", ") + " — blank keeps" : '{"Authorization": "…"}';
|
|
1111
|
+
adv.appendChild(mcpLiteralField("env (JSON)", fEnv));
|
|
1112
|
+
adv.appendChild(mcpLiteralField("headers (JSON)", fHeaders));
|
|
1113
|
+
adv.appendChild(mcpLiteralField("enabled", fEnabled));
|
|
1114
|
+
box.appendChild(adv);
|
|
1115
|
+
const f = { name: fName, transport: fTransport, command: fCommand, url: fUrl, args: fArgs, timeout: fTimeout, env: fEnv, headers: fHeaders, enabled: fEnabled };
|
|
1116
|
+
const actions = el("div", "pform-actions");
|
|
1117
|
+
actions.appendChild(link(t("web.settings.save"), "", () => submitMcpForm(f, mode, editing)));
|
|
1118
|
+
actions.appendChild(link(t("web.settings.cancel"), "dim", () => closeSubDialog()));
|
|
1119
|
+
box.appendChild(actions);
|
|
1120
|
+
return box;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/** Open the MCP add/edit nested dialog. */
|
|
1124
|
+
function openMcpForm(mode, entry) {
|
|
1125
|
+
S.state.mcpForm = { mode, entry: entry || null };
|
|
1126
|
+
openSubDialog(
|
|
1127
|
+
t(mode === "edit" ? "web.settings.mcp_edit_title" : "web.settings.mcp_add"),
|
|
1128
|
+
mcpForm(),
|
|
1129
|
+
() => {
|
|
1130
|
+
S.state.mcpForm = null;
|
|
1131
|
+
renderPluginsPanel();
|
|
1132
|
+
void loadPlugins();
|
|
1133
|
+
},
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
function parseJsonMap(raw) {
|
|
1138
|
+
const text = raw.trim();
|
|
1139
|
+
if (!text) return { present: false, value: null };
|
|
1140
|
+
let parsed = null;
|
|
1141
|
+
try {
|
|
1142
|
+
parsed = JSON.parse(text);
|
|
1143
|
+
} catch {
|
|
1144
|
+
return { present: true, value: null };
|
|
1145
|
+
}
|
|
1146
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return { present: true, value: null };
|
|
1147
|
+
if (!Object.values(parsed).every((v) => typeof v === "string")) return { present: true, value: null };
|
|
1148
|
+
return { present: true, value: parsed };
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
async function submitMcpForm(f, mode, editing) {
|
|
1152
|
+
const name = f.name.value.trim();
|
|
1153
|
+
const transport = f.transport.value || "stdio";
|
|
1154
|
+
const command = f.command.value.trim();
|
|
1155
|
+
const url = f.url.value.trim();
|
|
1156
|
+
if (!name || (transport === "stdio" ? !command : !url)) {
|
|
1157
|
+
setIndicator(t("web.settings.mcp_required"), true);
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
const body = { name, transport };
|
|
1161
|
+
if (command) body.command = command;
|
|
1162
|
+
if (url) body.url = url;
|
|
1163
|
+
const argsRaw = f.args.value.trim();
|
|
1164
|
+
if (argsRaw) {
|
|
1165
|
+
let args = null;
|
|
1166
|
+
try {
|
|
1167
|
+
args = JSON.parse(argsRaw);
|
|
1168
|
+
} catch {
|
|
1169
|
+
args = null;
|
|
1170
|
+
}
|
|
1171
|
+
if (!Array.isArray(args) || !args.every((x) => typeof x === "string")) {
|
|
1172
|
+
setIndicator(t("web.settings.mcp_invalid"), true);
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
body.args = args;
|
|
1176
|
+
}
|
|
1177
|
+
for (const key of ["env", "headers"]) {
|
|
1178
|
+
const parsed = parseJsonMap(f[key].value);
|
|
1179
|
+
if (parsed.present && !parsed.value) {
|
|
1180
|
+
setIndicator(t("web.settings.mcp_invalid"), true);
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
if (parsed.present) body[key] = parsed.value;
|
|
1184
|
+
}
|
|
1185
|
+
const timeoutRaw = f.timeout.value.trim();
|
|
1186
|
+
if (timeoutRaw) {
|
|
1187
|
+
const n = Number(timeoutRaw);
|
|
1188
|
+
if (!Number.isInteger(n) || n < 1000 || n > 300000) {
|
|
1189
|
+
setIndicator(t("web.settings.mcp_invalid"), true);
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
body.timeout = n;
|
|
1193
|
+
}
|
|
1194
|
+
body.enabled = f.enabled.checked;
|
|
1195
|
+
const target = mode === "edit" ? "/api/mcp/" + encodeURIComponent(editing.id || editing.name) : "/api/mcp";
|
|
1196
|
+
const r = await api(target, { method: mode === "edit" ? "PUT" : "POST", body });
|
|
1197
|
+
if (r.ok && r.data && r.data.ok) {
|
|
1198
|
+
setIndicator(t("web.settings.mcp_saved"), false);
|
|
1199
|
+
closeSubDialog();
|
|
1200
|
+
} else {
|
|
1201
|
+
setIndicator((r.data && r.data.error) || t("web.settings.save_failed"), true);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
function renderLspSection(list) {
|
|
1206
|
+
const servers = S.state.lsp;
|
|
1207
|
+
list.appendChild(groupHeader(t("web.settings.lsp_servers"), Array.isArray(servers) ? servers.length : null));
|
|
1208
|
+
if (servers === undefined || servers === null) {
|
|
1209
|
+
list.appendChild(placeholderFor(servers, t("web.settings.lsp_unavailable")));
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
if (!servers.length) {
|
|
1213
|
+
const empty = el("div", "placeholder");
|
|
1214
|
+
empty.textContent = t("web.settings.no_lsp");
|
|
1215
|
+
list.appendChild(empty);
|
|
1216
|
+
}
|
|
1217
|
+
servers.forEach((entry) => {
|
|
1218
|
+
const line = el("div", "list-line");
|
|
1219
|
+
line.appendChild(el("span", entry.enabled ? "g" : "dim", entry.enabled ? "✓" : "○"));
|
|
1220
|
+
const nameEl = el("span", "name", entry.lang);
|
|
1221
|
+
nameEl.title = entry.lang;
|
|
1222
|
+
line.appendChild(nameEl);
|
|
1223
|
+
const meta = [entry.command, entry.timeout != null ? entry.timeout + " ms" : null].filter(Boolean).join(" · ");
|
|
1224
|
+
if (meta) line.appendChild(el("span", "src", meta));
|
|
1225
|
+
line.appendChild(el("span", "st", entry.enabled ? t("web.settings.enabled") : t("web.settings.disabled")));
|
|
1226
|
+
line.appendChild(chip(t("web.settings.edit"), "", () => openLspForm(entry)));
|
|
1227
|
+
list.appendChild(line);
|
|
1228
|
+
});
|
|
1229
|
+
if (servers.length) {
|
|
1230
|
+
const note = el("div", "settings-note");
|
|
1231
|
+
note.textContent = t("web.settings.lsp_note");
|
|
1232
|
+
list.appendChild(note);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
/** Open the LSP server edit nested dialog. */
|
|
1237
|
+
function openLspForm(entry) {
|
|
1238
|
+
S.state.lspForm = { lang: entry.lang, entry };
|
|
1239
|
+
openSubDialog(entry.lang, lspForm(), () => {
|
|
1240
|
+
S.state.lspForm = null;
|
|
1241
|
+
renderPluginsPanel();
|
|
1242
|
+
void loadLsp();
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// LSP server edit form (enabled toggle, timeout, command, args JSON).
|
|
1247
|
+
function lspForm() {
|
|
1248
|
+
const editing = (S.state.lspForm && S.state.lspForm.entry) || {};
|
|
1249
|
+
const lang = (S.state.lspForm && S.state.lspForm.lang) || editing.lang || "";
|
|
1250
|
+
const box = el("div", "pform");
|
|
1251
|
+
const grid = el("div", "pform-grid");
|
|
1252
|
+
const fCommand = providerInput(editing.command || "", "text", "npx");
|
|
1253
|
+
const fTimeout = providerInput(editing.timeout != null ? String(editing.timeout) : "", "number", "30000");
|
|
1254
|
+
const fEnabled = providerCheck(editing.enabled !== false);
|
|
1255
|
+
grid.appendChild(mcpLiteralField("command", fCommand));
|
|
1256
|
+
grid.appendChild(mcpLiteralField("timeout (ms)", fTimeout));
|
|
1257
|
+
grid.appendChild(mcpLiteralField("enabled", fEnabled));
|
|
1258
|
+
box.appendChild(grid);
|
|
1259
|
+
const fArgs = el("textarea");
|
|
1260
|
+
fArgs.value = editing.args ? JSON.stringify(editing.args) : "";
|
|
1261
|
+
fArgs.placeholder = '["--stdio"]';
|
|
1262
|
+
box.appendChild(mcpLiteralField("args (JSON)", fArgs));
|
|
1263
|
+
const actions = el("div", "pform-actions");
|
|
1264
|
+
actions.appendChild(link(t("web.settings.save"), "", async () => {
|
|
1265
|
+
const body = {};
|
|
1266
|
+
const command = fCommand.value.trim();
|
|
1267
|
+
if (command) body.command = command;
|
|
1268
|
+
const timeoutRaw = fTimeout.value.trim();
|
|
1269
|
+
if (timeoutRaw) {
|
|
1270
|
+
const n = Number(timeoutRaw);
|
|
1271
|
+
if (!Number.isInteger(n) || n < 1000 || n > 300000) {
|
|
1272
|
+
setIndicator(t("web.settings.provider_invalid"), true);
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
body.timeout = n;
|
|
1276
|
+
}
|
|
1277
|
+
const argsRaw = fArgs.value.trim();
|
|
1278
|
+
if (argsRaw) {
|
|
1279
|
+
let args = null;
|
|
1280
|
+
try {
|
|
1281
|
+
args = JSON.parse(argsRaw);
|
|
1282
|
+
} catch {
|
|
1283
|
+
args = null;
|
|
1284
|
+
}
|
|
1285
|
+
if (!Array.isArray(args) || !args.every((x) => typeof x === "string")) {
|
|
1286
|
+
setIndicator(t("web.settings.provider_invalid"), true);
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
body.args = args;
|
|
1290
|
+
}
|
|
1291
|
+
body.enabled = fEnabled.checked;
|
|
1292
|
+
const r = await api("/api/lsp/" + encodeURIComponent(lang), { method: "PUT", body });
|
|
1293
|
+
if (r.ok && r.data && r.data.ok) {
|
|
1294
|
+
setIndicator(t("web.settings.mcp_saved"), false);
|
|
1295
|
+
closeSubDialog();
|
|
1296
|
+
} else {
|
|
1297
|
+
setIndicator((r.data && r.data.error) || t("web.settings.save_failed"), true);
|
|
1298
|
+
}
|
|
1299
|
+
}));
|
|
1300
|
+
actions.appendChild(link(t("web.settings.cancel"), "dim", () => closeSubDialog()));
|
|
1301
|
+
box.appendChild(actions);
|
|
1302
|
+
return box;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
export async function loadLsp() {
|
|
1306
|
+
const r = await api("/api/lsp");
|
|
1307
|
+
S.state.lsp = r.ok && r.data && Array.isArray(r.data.servers) ? r.data.servers : null;
|
|
1308
|
+
renderPluginsPanel();
|
|
1309
|
+
}
|
|
1310
|
+
|
|
948
1311
|
export async function loadMemory() {
|
|
949
1312
|
const r = await api("/api/memory");
|
|
950
1313
|
S.state.memory = r.ok && r.data ? r.data : null;
|
|
@@ -967,17 +1330,18 @@ export async function loadPlugins() {
|
|
|
967
1330
|
}
|
|
968
1331
|
|
|
969
1332
|
export function renderSettings() {
|
|
970
|
-
|
|
1333
|
+
// Generic panels render straight from PANEL_SECTIONS; custom panels
|
|
1334
|
+
// (model/ui/memory/skills/plugins/security) append their sections themselves
|
|
1335
|
+
// or render bespoke widgets alongside.
|
|
1336
|
+
for (const panel of ["general", "context", "reasoning", "agent", "network", "browser", "sessions", "advanced"]) {
|
|
1337
|
+
renderPanel(panel);
|
|
1338
|
+
}
|
|
971
1339
|
renderModelPanel();
|
|
972
|
-
renderGenericPanel("context", SECTION_KEYS.context);
|
|
973
|
-
renderGenericPanel("reasoning", SECTION_KEYS.reasoning);
|
|
974
|
-
renderGenericPanel("agent", SECTION_KEYS.agent);
|
|
975
1340
|
renderUiPanel();
|
|
976
1341
|
renderMemoryPanel();
|
|
977
1342
|
renderSkillsPanel();
|
|
978
1343
|
renderPluginsPanel();
|
|
979
1344
|
renderSecurityPanel();
|
|
980
|
-
renderGenericPanel("advanced", SECTION_KEYS.advanced);
|
|
981
1345
|
}
|
|
982
1346
|
|
|
983
1347
|
const overlay = document.getElementById("overlay");
|
|
@@ -1025,6 +1389,135 @@ document.querySelectorAll(".content-col .panel").forEach((panel) => {
|
|
|
1025
1389
|
panel.setAttribute("aria-labelledby", "tab-" + panel.id.replace(/^p-/, ""));
|
|
1026
1390
|
});
|
|
1027
1391
|
|
|
1392
|
+
// ---- settings search -------------------------------------------------------
|
|
1393
|
+
// With 100+ keys, scanning a sidebar is slower than typing. The index is the
|
|
1394
|
+
// same PANEL_SECTIONS table that renders the panels (plus each key's i18n
|
|
1395
|
+
// label/description), so search can never disagree with the layout.
|
|
1396
|
+
const setMenuEl = document.getElementById("setMenu");
|
|
1397
|
+
const setSearch = document.getElementById("setSearch");
|
|
1398
|
+
const setSearchClear = document.getElementById("setSearchClear");
|
|
1399
|
+
const setSearchIcon = document.getElementById("setSearchIcon");
|
|
1400
|
+
const setResults = document.getElementById("setResults");
|
|
1401
|
+
if (setSearchIcon) setSearchIcon.appendChild(icon("search", "set-search-svg"));
|
|
1402
|
+
|
|
1403
|
+
function searchIndex() {
|
|
1404
|
+
const owner = keyToPanel();
|
|
1405
|
+
return (S.state.registry || [])
|
|
1406
|
+
.filter((def) => def && typeof def.key === "string")
|
|
1407
|
+
.map((def) => {
|
|
1408
|
+
const label = configLabel(def);
|
|
1409
|
+
return {
|
|
1410
|
+
def,
|
|
1411
|
+
panel: owner.get(def.key) || null,
|
|
1412
|
+
label,
|
|
1413
|
+
desc: descFor(def.key),
|
|
1414
|
+
haystack: `${label} ${def.key} ${descFor(def.key)}`.toLowerCase(),
|
|
1415
|
+
};
|
|
1416
|
+
})
|
|
1417
|
+
.filter((e) => e.panel);
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
let searchRows = [];
|
|
1421
|
+
let searchCursor = -1;
|
|
1422
|
+
|
|
1423
|
+
function closeSearch() {
|
|
1424
|
+
if (setSearch) setSearch.value = "";
|
|
1425
|
+
if (setResults) {
|
|
1426
|
+
setResults.hidden = true;
|
|
1427
|
+
setResults.textContent = "";
|
|
1428
|
+
}
|
|
1429
|
+
if (setMenuEl) setMenuEl.hidden = false;
|
|
1430
|
+
if (setSearchClear) setSearchClear.hidden = true;
|
|
1431
|
+
searchRows = [];
|
|
1432
|
+
searchCursor = -1;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
function flashRow(panel, key) {
|
|
1436
|
+
const panelEl = document.getElementById("p-" + panel);
|
|
1437
|
+
if (!panelEl) return;
|
|
1438
|
+
const rows = Array.from(panelEl.querySelectorAll(".row"));
|
|
1439
|
+
const def = defFor(key);
|
|
1440
|
+
if (!def) return;
|
|
1441
|
+
const label = configLabel(def);
|
|
1442
|
+
const target = rows.find((r) => {
|
|
1443
|
+
const nameEl = r.querySelector(".row-name");
|
|
1444
|
+
return nameEl && nameEl.textContent === label;
|
|
1445
|
+
});
|
|
1446
|
+
if (!target) return;
|
|
1447
|
+
target.scrollIntoView({ block: "center" });
|
|
1448
|
+
target.classList.add("row-flash");
|
|
1449
|
+
setTimeout(() => target.classList.remove("row-flash"), 1200);
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
function pickResult(entry) {
|
|
1453
|
+
if (!entry || !entry.panel) return;
|
|
1454
|
+
closeSearch();
|
|
1455
|
+
selectSettingsPanel(entry.panel);
|
|
1456
|
+
flashRow(entry.panel, entry.def.key);
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
function renderResults(query) {
|
|
1460
|
+
if (!setResults) return;
|
|
1461
|
+
const q = query.trim().toLowerCase();
|
|
1462
|
+
if (!q) {
|
|
1463
|
+
closeSearch();
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
const matches = searchIndex().filter((e) => e.haystack.includes(q));
|
|
1467
|
+
setResults.textContent = "";
|
|
1468
|
+
if (!matches.length) {
|
|
1469
|
+
const empty = el("div", "set-results-empty", t("web.settings.search_empty"));
|
|
1470
|
+
setResults.appendChild(empty);
|
|
1471
|
+
}
|
|
1472
|
+
searchRows = matches.slice(0, 40);
|
|
1473
|
+
searchCursor = searchRows.length ? 0 : -1;
|
|
1474
|
+
searchRows.forEach((entry, i) => {
|
|
1475
|
+
const item = el("div", "set-result" + (i === searchCursor ? " on" : ""));
|
|
1476
|
+
item.setAttribute("role", "option");
|
|
1477
|
+
item.tabIndex = -1;
|
|
1478
|
+
item.appendChild(el("span", "sr-label", entry.label));
|
|
1479
|
+
const panelName = t("web.settings." + entry.panel);
|
|
1480
|
+
item.appendChild(el("span", "sr-panel", panelName === "web.settings." + entry.panel ? entry.panel : panelName));
|
|
1481
|
+
const keyEl = el("span", "sr-key", entry.def.key);
|
|
1482
|
+
keyEl.title = entry.def.key;
|
|
1483
|
+
item.appendChild(keyEl);
|
|
1484
|
+
item.addEventListener("click", () => pickResult(entry));
|
|
1485
|
+
setResults.appendChild(item);
|
|
1486
|
+
});
|
|
1487
|
+
setResults.hidden = false;
|
|
1488
|
+
setMenuEl.hidden = true;
|
|
1489
|
+
setSearchClear.hidden = false;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
function moveSearchCursor(delta) {
|
|
1493
|
+
if (!searchRows.length) return;
|
|
1494
|
+
searchCursor = (searchCursor + delta + searchRows.length) % searchRows.length;
|
|
1495
|
+
Array.from(setResults.children).forEach((node, i) => node.classList.toggle("on", i === searchCursor));
|
|
1496
|
+
const active = setResults.children[searchCursor];
|
|
1497
|
+
if (active) active.scrollIntoView({ block: "nearest" });
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
if (setSearch) {
|
|
1501
|
+
setSearch.addEventListener("input", () => renderResults(setSearch.value));
|
|
1502
|
+
setSearch.addEventListener("keydown", (e) => {
|
|
1503
|
+
if (e.key === "ArrowDown") {
|
|
1504
|
+
e.preventDefault();
|
|
1505
|
+
moveSearchCursor(1);
|
|
1506
|
+
} else if (e.key === "ArrowUp") {
|
|
1507
|
+
e.preventDefault();
|
|
1508
|
+
moveSearchCursor(-1);
|
|
1509
|
+
} else if (e.key === "Enter") {
|
|
1510
|
+
e.preventDefault();
|
|
1511
|
+
pickResult(searchRows[searchCursor]);
|
|
1512
|
+
} else if (e.key === "Escape" && setSearch.value) {
|
|
1513
|
+
// First Esc clears the query; a second one is handled globally (close).
|
|
1514
|
+
e.stopPropagation();
|
|
1515
|
+
closeSearch();
|
|
1516
|
+
}
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
if (setSearchClear) setSearchClear.addEventListener("click", closeSearch);
|
|
1520
|
+
|
|
1028
1521
|
function openSettings() {
|
|
1029
1522
|
settingsReturnFocus = document.activeElement;
|
|
1030
1523
|
overlay.classList.add("open");
|
|
@@ -1032,12 +1525,15 @@ function openSettings() {
|
|
|
1032
1525
|
void loadMemory();
|
|
1033
1526
|
void loadSkills();
|
|
1034
1527
|
void loadPlugins();
|
|
1528
|
+
void loadLsp();
|
|
1035
1529
|
const active = document.querySelector("#setMenu .mi.on") || settingsTabs[0];
|
|
1036
1530
|
if (active) setTimeout(() => active.focus(), 0);
|
|
1037
1531
|
}
|
|
1038
1532
|
function closeSettings() {
|
|
1039
|
-
|
|
1533
|
+
closeSubDialog();
|
|
1040
1534
|
S.state.providerForm = null;
|
|
1535
|
+
S.state.mcpForm = null;
|
|
1536
|
+
S.state.lspForm = null;
|
|
1041
1537
|
overlay.classList.remove("open");
|
|
1042
1538
|
settingsBtn.setAttribute("aria-expanded", "false");
|
|
1043
1539
|
if (settingsReturnFocus && settingsReturnFocus.focus) settingsReturnFocus.focus();
|
|
@@ -1050,60 +1546,71 @@ overlay.addEventListener("click", (e) => {
|
|
|
1050
1546
|
});
|
|
1051
1547
|
// Trap Tab inside the dialog while it is open.
|
|
1052
1548
|
overlay.addEventListener("keydown", (e) => trapTab(overlay, e));
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
const
|
|
1057
|
-
const
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1549
|
+
// One nested dialog for every "edit a single thing" flow (provider, MCP
|
|
1550
|
+
// server, LSP server). Keeps the settings modal visible behind it, traps Tab
|
|
1551
|
+
// and returns focus to the control that opened it.
|
|
1552
|
+
const subOverlay = document.getElementById("subOverlay");
|
|
1553
|
+
const subHost = document.getElementById("subHost");
|
|
1554
|
+
const subTitle = document.getElementById("subTitle");
|
|
1555
|
+
const closeSubBtn = document.getElementById("closeSub");
|
|
1556
|
+
let subReturnFocus = null;
|
|
1557
|
+
let subOnClose = null;
|
|
1558
|
+
|
|
1559
|
+
export function isSubDialogOpen() {
|
|
1560
|
+
return !!subOverlay && subOverlay.classList.contains("open");
|
|
1062
1561
|
}
|
|
1063
1562
|
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
providerOverlay.classList.add("open");
|
|
1563
|
+
/** Open the nested dialog with a built content node. `onClose` runs after. */
|
|
1564
|
+
export function openSubDialog(title, node, onClose) {
|
|
1565
|
+
if (!subOverlay || !subHost) return;
|
|
1566
|
+
subReturnFocus = document.activeElement;
|
|
1567
|
+
subOnClose = onClose || null;
|
|
1568
|
+
subTitle.textContent = title;
|
|
1569
|
+
subHost.textContent = "";
|
|
1570
|
+
subHost.appendChild(node);
|
|
1571
|
+
subOverlay.classList.add("open");
|
|
1074
1572
|
setTimeout(() => {
|
|
1075
|
-
const first =
|
|
1573
|
+
const first = subHost.querySelector("input, select, textarea");
|
|
1076
1574
|
if (first) first.focus();
|
|
1077
1575
|
}, 0);
|
|
1078
1576
|
}
|
|
1079
1577
|
|
|
1080
|
-
function
|
|
1081
|
-
if (!
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
if (!isProviderOpen()) return;
|
|
1089
|
-
S.state.providerForm = null;
|
|
1090
|
-
hideProviderOverlay();
|
|
1091
|
-
renderModelPanel();
|
|
1578
|
+
export function closeSubDialog() {
|
|
1579
|
+
if (!isSubDialogOpen()) return;
|
|
1580
|
+
subOverlay.classList.remove("open");
|
|
1581
|
+
const after = subOnClose;
|
|
1582
|
+
subOnClose = null;
|
|
1583
|
+
if (subReturnFocus && subReturnFocus.focus) subReturnFocus.focus();
|
|
1584
|
+
subReturnFocus = null;
|
|
1585
|
+
if (after) after();
|
|
1092
1586
|
}
|
|
1093
1587
|
|
|
1094
|
-
if (
|
|
1095
|
-
if (
|
|
1096
|
-
|
|
1588
|
+
if (subOverlay) {
|
|
1589
|
+
if (closeSubBtn) closeSubBtn.addEventListener("click", closeSubDialog);
|
|
1590
|
+
subOverlay.addEventListener("click", (e) => {
|
|
1097
1591
|
e.stopPropagation();
|
|
1098
|
-
if (e.target ===
|
|
1592
|
+
if (e.target === subOverlay) closeSubDialog();
|
|
1099
1593
|
});
|
|
1100
|
-
|
|
1101
|
-
|
|
1594
|
+
subOverlay.addEventListener("keydown", (e) => trapTab(subOverlay, e));
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
function openProviderForm(mode, entry) {
|
|
1598
|
+
S.state.providerForm = { mode, entry: entry || null };
|
|
1599
|
+
openSubDialog(
|
|
1600
|
+
t(mode === "edit" ? "web.settings.provider_edit_title" : "web.settings.provider_add_title"),
|
|
1601
|
+
providerForm(),
|
|
1602
|
+
() => renderModelPanel(),
|
|
1603
|
+
);
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
function closeProviderForm() {
|
|
1607
|
+
S.state.providerForm = null;
|
|
1608
|
+
closeSubDialog();
|
|
1102
1609
|
}
|
|
1103
1610
|
|
|
1104
1611
|
document.addEventListener("keydown", (e) => {
|
|
1105
1612
|
if (e.key !== "Escape") return;
|
|
1106
|
-
if (
|
|
1613
|
+
if (isSubDialogOpen()) closeSubDialog();
|
|
1107
1614
|
else if (confirmOverlay && confirmOverlay.classList.contains("open")) closeConfirm();
|
|
1108
1615
|
else if (overlay.classList.contains("open")) closeSettings();
|
|
1109
1616
|
});
|