@songsid/agend 2.3.5-alpha.3 → 2.3.5-alpha.4

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.
Files changed (101) hide show
  1. package/dist/agent-runtime-api.d.ts +4 -0
  2. package/dist/agent-runtime-api.js +59 -0
  3. package/dist/agent-runtime-api.js.map +1 -0
  4. package/dist/agent-runtime-change.d.ts +74 -0
  5. package/dist/agent-runtime-change.js +102 -0
  6. package/dist/agent-runtime-change.js.map +1 -0
  7. package/dist/auth-api.js +4 -0
  8. package/dist/auth-api.js.map +1 -1
  9. package/dist/backend/codex-footer-hints.d.ts +4 -0
  10. package/dist/backend/codex-footer-hints.js +27 -0
  11. package/dist/backend/codex-footer-hints.js.map +1 -0
  12. package/dist/backend/codex.js +8 -10
  13. package/dist/backend/codex.js.map +1 -1
  14. package/dist/fleet-manager.d.ts +3 -0
  15. package/dist/fleet-manager.js +157 -1
  16. package/dist/fleet-manager.js.map +1 -1
  17. package/dist/fleet-windows-runtime.d.ts +6 -1
  18. package/dist/fleet-windows-runtime.js +70 -1
  19. package/dist/fleet-windows-runtime.js.map +1 -1
  20. package/dist/office-share-page.js +1 -1
  21. package/dist/office-share-page.js.map +1 -1
  22. package/dist/public-web-link.d.ts +6 -0
  23. package/dist/public-web-link.js +9 -0
  24. package/dist/public-web-link.js.map +1 -1
  25. package/dist/runtime-api.js +84 -9
  26. package/dist/runtime-api.js.map +1 -1
  27. package/dist/runtime-build-info.d.ts +9 -0
  28. package/dist/runtime-build-info.js +57 -0
  29. package/dist/runtime-build-info.js.map +1 -0
  30. package/dist/runtime-bundle.d.ts +3 -1
  31. package/dist/runtime-bundle.js +14 -8
  32. package/dist/runtime-bundle.js.map +1 -1
  33. package/dist/runtime-cli.d.ts +6 -1
  34. package/dist/runtime-cli.js +46 -11
  35. package/dist/runtime-cli.js.map +1 -1
  36. package/dist/runtime-client-store.d.ts +20 -1
  37. package/dist/runtime-client-store.js +56 -4
  38. package/dist/runtime-client-store.js.map +1 -1
  39. package/dist/runtime-client.d.ts +12 -0
  40. package/dist/runtime-client.js +43 -7
  41. package/dist/runtime-client.js.map +1 -1
  42. package/dist/runtime-connection.d.ts +22 -0
  43. package/dist/runtime-connection.js +93 -0
  44. package/dist/runtime-connection.js.map +1 -0
  45. package/dist/runtime-coordinator.d.ts +9 -0
  46. package/dist/runtime-coordinator.js +81 -34
  47. package/dist/runtime-coordinator.js.map +1 -1
  48. package/dist/runtime-pane-boot.d.ts +7 -0
  49. package/dist/runtime-pane-boot.js +23 -0
  50. package/dist/runtime-pane-boot.js.map +1 -1
  51. package/dist/runtime-protocol.d.ts +13 -0
  52. package/dist/runtime-protocol.js +18 -0
  53. package/dist/runtime-protocol.js.map +1 -1
  54. package/dist/settings-effect.js +1 -1
  55. package/dist/settings-effect.js.map +1 -1
  56. package/dist/settings-http-confirmation.d.ts +3 -0
  57. package/dist/settings-http-confirmation.js +16 -1
  58. package/dist/settings-http-confirmation.js.map +1 -1
  59. package/dist/ui/agent-runtime-settings.js +67 -0
  60. package/dist/ui/agent-runtime.js +20 -2
  61. package/dist/ui/backend-signin.js +36 -21
  62. package/dist/ui/native-instance-settings.js +5 -8
  63. package/dist/ui/panel-chat.js +3 -2
  64. package/dist/ui/panel-fleet.js +4 -2
  65. package/dist/ui/panel-runtimes.js +136 -33
  66. package/dist/ui/panel-settings.js +224 -139
  67. package/dist/ui/panel-windows-cli.js +77 -41
  68. package/dist/ui/settings-agent-filters.js +94 -0
  69. package/dist/ui/settings-appearance.js +122 -23
  70. package/dist/ui/settings-dialogs.js +5 -5
  71. package/dist/ui/settings-guests.js +25 -11
  72. package/dist/ui/settings-layout.js +245 -0
  73. package/dist/ui/settings-model.js +8 -0
  74. package/dist/ui/settings-registry.js +5 -3
  75. package/dist/ui/settings-shortcuts.js +65 -0
  76. package/dist/ui/settings-strings.js +93 -0
  77. package/dist/ui/settings-web-authority.js +14 -11
  78. package/dist/ui/shared/app-shell.js +17 -7
  79. package/dist/ui/shared/app.css +1022 -12
  80. package/dist/ui/shared/instance-nav.js +3 -2
  81. package/dist/ui/shared/machine-name.js +13 -0
  82. package/dist/ui/shared/settings-width.js +31 -0
  83. package/dist/ui/shared/tokens.css +1 -0
  84. package/dist/ui/shared/ui-dialog.js +2 -2
  85. package/dist/ui/shared/ui-menu.js +3 -3
  86. package/dist/ui/windows-cli-catalog.js +41 -12
  87. package/dist/web-api.d.ts +1 -0
  88. package/dist/web-api.js +3 -0
  89. package/dist/web-api.js.map +1 -1
  90. package/dist/windows-agents.d.ts +3 -1
  91. package/dist/windows-agents.js +13 -10
  92. package/dist/windows-agents.js.map +1 -1
  93. package/dist/windows-cli-api.js +2 -1
  94. package/dist/windows-cli-api.js.map +1 -1
  95. package/dist/windows-cli-worker.d.ts +1 -0
  96. package/dist/windows-cli-worker.js +2 -2
  97. package/dist/windows-cli-worker.js.map +1 -1
  98. package/dist/windows-cli.d.ts +1 -0
  99. package/dist/windows-cli.js +7 -2
  100. package/dist/windows-cli.js.map +1 -1
  101. package/package.json +1 -1
@@ -56,6 +56,12 @@ import { WebAuthorityCard } from "./settings-web-authority.js";
56
56
  import { OutsideCard } from "./settings-outside.js";
57
57
  import { NamedLinkTest, setTypedTunnelToken } from "./settings-named-test.js";
58
58
  import { SignInList } from "./backend-signin.js";
59
+ import { ProviderMark } from "./provider-marks.js";
60
+ import { CliProductMark, cliLabel } from "./cli-picker.js";
61
+ import { SettingsPageHead, SettingsTabs, SettingsTabPanel, useSettingsTabs, revealSettingsTarget, layoutText } from "./settings-layout.js";
62
+ import { AgentFilters, agentFilterText, agentRuntime, agentRuntimeOffline, agentStatus, filterAgentGroups } from "./settings-agent-filters.js";
63
+ import { useRuntimeCatalog } from "./runtime-catalog.js";
64
+ import { ShortcutsHelpRow } from "./settings-shortcuts.js";
59
65
  import { GeneralLocation } from "./general-location.js";
60
66
 
61
67
  const tn = (k, ...v) => t(`settings.${k}`, ...v);
@@ -107,7 +113,7 @@ async function loadLive(lease) {
107
113
  if (!lease.current()) return null;
108
114
  const live = {}, classicLive = {};
109
115
  if (f.ok && f.body && Array.isArray(f.body.instances)) for (const i of f.body.instances) {
110
- live[i.name] = { status: i.status, state: i.state, backend: i.backend, model: i.model, model_display: modelText(i, i.model_display), effort: i.effort, effort_supported: i.effort_supported, runtime:i.runtime, capabilities:i.capabilities };
116
+ live[i.name] = { status: i.status, state: i.state, backend: i.backend, model: i.model, model_display: modelText(i, i.model_display), effort: i.effort, effort_supported: i.effort_supported, runtime:i.runtime, capabilities:i.capabilities, machine_id:i.machine_id, machine_name:i.machine_name, machine_status:i.machine_status };
111
117
  if (i.classic) classicLive[i.name] = { status: i.status, state: i.state };
112
118
  }
113
119
  // /api/fleet carries live state for ClassicBot rooms; the profiles are the fallback for one not listed there yet.
@@ -381,20 +387,23 @@ export function SettingsPanel({ route, navKey }) {
381
387
  const formKey = data && !data.error ? `${data.loadedAt}:${draftEpoch}` : "";
382
388
  // #2347 regrouping: each card where the person looks for it (the IA in #2347 §3); a form split across sections (the
383
389
  // defaults, Web) is one form with one draft, shown a part at a time.
384
- const sectionBody = section === "preferences" ? html`<${DeviceSection} key=${you.key} /><${AppearanceSection} ...${you} part="device" />`
390
+ const sectionBody = section === "preferences" ? html`<${DeviceSection} key=${you.key} />`
385
391
  : section === "look" ? html`<${AppearanceSection} ...${you} part="look" />`
386
392
  : section === "runtimes" ? html`<${RuntimesPanel} navKey=${navKey} />`
387
393
  : !data ? html`<${Skeleton} lines=${6} />`
388
394
  : data.error ? html`<${ErrorState} message=${tn("configLoadFailed")} onRetry=${reload} />`
389
- : section === "agents" ? html`<${YamlLock} staged=${staged}><${Agents} ctx=${ctx} search=${search} openDialog=${toDialog} /></${YamlLock}>${ctx.providerOk ? html`<${ProviderKeys} ctx=${ctx} />` : null}`
395
+ : section === "agents" ? html`<${YamlLock} staged=${staged}><${Agents} ctx=${ctx} search=${search} clearSearch=${() => setSearch("")} openDialog=${toDialog} /></${YamlLock}><details class="s-ui-disclosure s-ai-accounts"><summary><span>${layoutText("aiAccounts")}</span><small>${layoutText("aiAccountsSummary")}</small></summary><div class="s-disclosure-body s-auth-body">
396
+ <div class="s-auth-scope"><${Icon} name="view" size=${18} /><div><strong>${layoutText("aiHostScope")}</strong><p>${layoutText("aiAccountsHint")}</p><a href=${settingsPath("runtimes")}>${layoutText("aiWindowsLink")}<span aria-hidden="true">→</span></a></div></div>
397
+ <${SignInList} />${ctx.providerOk ? html`<${ProviderKeys} ctx=${ctx} />` : html`<section class="s-auth-section s-api-unavailable"><header class="s-auth-section-head"><div><h3>${layoutText("apiKeysTitle")}</h3><p>${layoutText("apiKeysHint")}</p></div><span class="s-auth-state">${layoutText("apiKeysUnavailableState")}</span></header><p class="note">${layoutText("apiKeysUnavailable")}</p></section>`}
398
+ </div></details>`
390
399
  : section === "agent-defaults" ? html`<${GeneralLocation} /><${YamlLock} staged=${staged}><${General} key=${formKey} ctx=${ctx} part="fleet" /></${YamlLock}>`
391
400
  : section === "chat" ? html`<${YamlLock} staged=${staged}><${Bots} ctx=${ctx} search=${search} openDialog=${toDialog} /></${YamlLock}>
392
401
  <${WebSection} key=${formKey} ctx=${ctx} staged=${staged} part="chat" />
393
- <div id="classicbot" class="s-anchor"><h2 class="s-ap-group">ClassicBot</h2><${Classic} ctx=${ctx} search=${search} openDialog=${toDialog} />
394
- <${General} key=${formKey} ctx=${ctx} part="classic" /></div>`
402
+ <div id="classicbot" class="s-anchor s-classic-area"><section class="card s-chat-classic"><h2 class="s-ap-group">ClassicBot</h2><${Classic} ctx=${ctx} search=${search} openDialog=${toDialog} /></section>
403
+ <details class="s-ui-disclosure s-classic-defaults"><summary>${layoutText("classicDefaults")}</summary><div class="s-disclosure-body"><${General} key=${formKey} ctx=${ctx} part="classic" /></div></details></div>`
395
404
  : section === "web-dashboard" ? html`<${WebSection} key=${formKey} ctx=${ctx} staged=${staged} part="dashboard" /><${AppearanceSection} ...${you} part="dashboard" />`
396
405
  : section === "access" ? html`<${WebSection} key=${formKey} ctx=${ctx} staged=${staged} part="access" />`
397
- : html`<${Developer} key=${`dev:${draftEpoch}`} ctx=${ctx} /><${WebSection} key=${formKey} ctx=${ctx} staged=${staged} part="readonly" />`;
406
+ : html`<${AdvancedSettings} ctx=${ctx} staged=${staged} formKey=${formKey} draftEpoch=${draftEpoch} />`;
398
407
  // #2347 §6: the setting a search result opened — brought into view once its section is drawn (its data may still be
399
408
  // on its way), focused, and flashed for 2 s.
400
409
  const wanted = useRef(null);
@@ -413,6 +422,7 @@ export function SettingsPanel({ route, navKey }) {
413
422
  if (++tries < 40) timer = setTimeout(find, 50); else wanted.current = null;
414
423
  return;
415
424
  }
425
+ if (revealSettingsTarget(el)) { timer = setTimeout(find, 0); return; }
416
426
  wanted.current = null;
417
427
  // #2347 §10: a setting in a closed drawer (Advanced) is shown — every drawer around it opens first.
418
428
  for (let d = el.closest && el.closest("details"); d; d = d.parentElement && d.parentElement.closest("details")) d.open = true;
@@ -453,7 +463,14 @@ export function SettingsPanel({ route, navKey }) {
453
463
  if (!id) return;
454
464
  let el = null;
455
465
  try { el = document.getElementById(decodeURIComponent(id)); } catch { el = null; }
456
- if (el && typeof el.scrollIntoView === "function") el.scrollIntoView({ block: "start" });
466
+ if (!el) return;
467
+ const show = () => {
468
+ // Hash links, like settings-search targets, must reveal every enclosing disclosure before scrolling.
469
+ for (let d = el.closest && el.closest("details"); d; d = d.parentElement && d.parentElement.closest("details")) d.open = true;
470
+ if (typeof el.scrollIntoView === "function") el.scrollIntoView({ block: "start" });
471
+ };
472
+ if (revealSettingsTarget(el)) { const timer = setTimeout(show, 0); return () => clearTimeout(timer); }
473
+ show();
457
474
  }, [section, navKey, !!data]);
458
475
  // A dialog belongs to the navigation that opened it (its draft names one object, never another).
459
476
  const open = dialog && dialog.key === navKey ? dialog : null;
@@ -472,14 +489,15 @@ export function SettingsPanel({ route, navKey }) {
472
489
  <span class="s-nav-head" id=${`s-grp-${g.id}`}>${tn(`grp_${g.id}`)}</span>
473
490
  ${g.sections.map((k) => html`<a key=${k} href=${sectionPath(k)} class=${`seg-item${k === section ? " active" : ""}`}
474
491
  aria-current=${k === section ? "page" : undefined}><${Icon} name=${ICONS[k]} size=${16} /><span>${tn(`sec_${k}`)}</span></a>`)}</div>`)}</div></nav>
475
- <div data-trust="panel-content" class="panel-body"><div class="col">
492
+ <div data-trust="panel-content" class="panel-body"><div class="col" data-settings-section=${section}>
476
493
  <a class="s-back" href=${settingsPath()}><${Icon} name="back" size=${16} /><span>${tn("title")}</span><span class="s-back-here">${tn(`sec_${section}`)}</span></a>
494
+ ${section !== "runtimes" ? html`<${SettingsPageHead} section=${section}>${section === "agents" && ctx ? html`<button type="button" id="newAgentBtn" class="btn btn-primary" disabled=${yamlStaged(staged)} onClick=${() => toDialog({ kind: "create" })}><${Icon} name="plus" size=${16} />${tn("newAgent")}</button>` : null}</${SettingsPageHead}>` : null}
477
495
  ${searchable ? html`<div class="s-toolbar"><label class="s-search"><${Icon} name="search" size=${14} /><span class="sr-only">${tn("searchAll")}</span>
478
496
  <input type="search" value=${search} placeholder=${tn("searchAll")} onInput=${(e) => setSearch(e.target.value)} /></label></div>` : null}
479
497
  ${op ? html`<${OperationCard} op=${op} schema=${data && data.schema} />` : null}
480
498
  <${PendingChanges} inline=${true} />
481
499
  ${ctx && (section === "agents" || section === "chat") ? html`<${FirstRunCard} onConnect=${openWizard} />` : null}
482
- ${sectionBody}
500
+ <div class="s-section-layout" data-section=${section}>${sectionBody}</div>
483
501
  </div></div>
484
502
  ${staged.size ? html`<${PendingBar} staged=${staged} busy=${opBusy(op)} onApply=${apply} onDiscard=${discard} section=${section} />` : null}
485
503
  ${open && ctx ? renderDialog(open, ctx, close) : null}
@@ -487,6 +505,14 @@ export function SettingsPanel({ route, navKey }) {
487
505
  </div>`;
488
506
  }
489
507
 
508
+ function AdvancedSettings({ ctx, staged, formKey, draftEpoch }) {
509
+ const [tab, setTab] = useSettingsTabs("advanced", "editor");
510
+ return html`<div class="s-advanced-layout"><${SettingsTabs} id="advanced" value=${tab} onChange=${setTab} items=${[{ id: "editor", label: layoutText("editor") }, { id: "effective", label: layoutText("effective") }]} />
511
+ <${SettingsTabPanel} group="advanced" id="editor" active=${tab === "editor"}><${Developer} key=${`dev:${draftEpoch}`} ctx=${ctx} /></${SettingsTabPanel}>
512
+ <${SettingsTabPanel} group="advanced" id="effective" active=${tab === "effective"}><${WebSection} key=${formKey} ctx=${ctx} staged=${staged} part="readonly" /></${SettingsTabPanel}>
513
+ </div>`;
514
+ }
515
+
490
516
  /**
491
517
  * #1604, #1665: Preferences. Synced across the person's devices (kept on the host, app-prefs.js): theme (theme.js),
492
518
  * language (app-i18n.js) and HTML previews (preview.js, through the app's confirm). On the public link they are
@@ -608,17 +634,23 @@ export function DeviceSection() {
608
634
  setTimeout(go, 0);
609
635
  };
610
636
  const publicLink = typeof document !== "undefined" && document.body && document.body.dataset.publicLink === "1";
611
- return html`<${ProfileCard} />
612
- <section class="card s-device s-synced"><h3>${tn("syncedTitle")}</h3>
613
- <p class="sub">${publicLink ? tn("syncedPublicLink") : tn("syncedLead")}</p>
614
- <${DevicePrefs} labels=${true} />
615
- ${P() ? html`<label class="check"><input id="pref-previews" type="checkbox" checked=${on} onChange=${(e) => { const want = e.target.checked; setPreviewOptIn(want).then(setOn); }} />
616
- <span>${t("chat.previews")}</span></label>` : null}
617
- </section>
618
- <section class="card s-device s-local"><h3>${tn("deviceTitle")}</h3>
619
- <p class="sub">${tn("deviceLead")}</p>
620
- <div class="s-device-row"><button type="button" id="tourBtn" class="btn" title=${t("chat.tourBtnTitle")} onClick=${replay}><${Icon} name="info" size=${16} />${t("chat.tour")}</button></div>
621
- </section>`;
637
+ return html`<div class="s-pref-stack">
638
+ <section class="card s-device s-pref-personal"><h3>${tn("prefsProfileGroup")}</h3>
639
+ <${ProfileCard} embedded=${true} />
640
+ <div class="s-pref-name-format"><${DevicePrefs} labels=${true} fields=${["names"]} /></div>
641
+ </section>
642
+ <section class="card s-device s-synced s-pref-content"><div class="s-pref-group-head"><h3>${tn("prefsContentGroup")}</h3><span class="s-scope" title=${publicLink ? tn("syncedPublicLink") : tn("syncedLead")}>${tn("prefsAccountScope")}</span></div>
643
+ ${publicLink ? html`<p class="sub">${tn("syncedPublicLink")}</p>` : null}
644
+ <${DevicePrefs} labels=${true} fields=${["language"]} />
645
+ ${P() ? html`<label class="check s-pref-preview"><span>${t("chat.previews")}</span><input id="pref-previews" class="s-ui-toggle" type="checkbox" checked=${on} onChange=${(e) => { const want = e.target.checked; setPreviewOptIn(want).then(setOn); }} /><small>${layoutText("previewsPersonalHint")}</small></label>` : null}
646
+ </section>
647
+ <section class="card s-device s-local s-pref-help"><h3>${tn("prefsHelpGroup")}</h3>
648
+ <div class="s-pref-help-row"><div><strong>${t("chat.tour")}</strong><p class="sub">${tn("prefsTourHint")}</p></div>
649
+ <button type="button" id="tourBtn" class="btn" title=${t("chat.tourBtnTitle")} onClick=${replay}><${Icon} name="info" size=${16} />${tn("prefsReplayTour")}</button></div>
650
+ <${ShortcutsHelpRow} />
651
+ </section>
652
+ <a class="s-pref-look-link" href=${settingsPath("look")}>${tn("prefsLookHint")}<${Icon} name="chevron" size=${16} /></a>
653
+ </div>`;
622
654
  }
623
655
 
624
656
  /**
@@ -628,7 +660,7 @@ export function DeviceSection() {
628
660
  * refuses one; this only shows what it says.
629
661
  */
630
662
  const view = (b) => ({ phase: "ready", name: b.name || "", shown: b.shown || "", id: b.id || "", bound: b.bound === true, platform: b.platform_name || "", error: "" });
631
- export function ProfileCard() {
663
+ export function ProfileCard({ embedded = false } = {}) {
632
664
  const [state, setState] = useState({ phase: "loading", name: "", shown: "", id: "", bound: false, platform: "", error: "" });
633
665
  const [draft, setDraft] = useState("");
634
666
  const lease = useLease();
@@ -655,16 +687,22 @@ export function ProfileCard() {
655
687
  };
656
688
  const busy = state.phase !== "ready";
657
689
  const changed = draft.trim() !== state.name;
658
- return html`<section class="card s-device s-profile"><h3>${tn("profileTitle")}</h3>
659
- <p class="sub">${state.bound ? tn("profileLeadBound") : tn("profileLeadUnbound")}</p>
660
- <div class="field"><label for="profile-name">${tn("profileName")}</label>
661
- <input id="profile-name" type="text" maxlength="200" autocomplete="nickname" value=${draft} disabled=${busy}
690
+ const initial = [...(state.shown || state.name || "web-user")][0]?.toUpperCase() || "W";
691
+ const fields = html`<div class="s-pref-profile-layout">
692
+ ${embedded ? html`<span class="s-pref-avatar" aria-hidden="true">${initial}</span>` : null}
693
+ <div class="s-pref-profile-main"><div class="field"><label for="profile-name">${tn("profileName")}</label>
694
+ <div class="s-pref-name-action"><input id="profile-name" type="text" maxlength="200" autocomplete="nickname" value=${draft} disabled=${busy}
662
695
  placeholder=${state.platform || "web-user"} aria-invalid=${state.error ? "true" : "false"} aria-describedby="profile-help"
663
696
  onInput=${(e) => setDraft(e.target.value)} onKeyDown=${(e) => { if (e.key === "Enter" && changed && draft.trim()) save(false); }} />
664
- <p class="sub" id="profile-help">${state.error ? html`<span class="err" role="alert">${state.error}</span>` : tn("profileHelp", state.shown || "web-user", state.id)}</p></div>
665
- <div class="s-device-row"><button type="button" class="btn primary" disabled=${busy || !changed || !draft.trim()} onClick=${() => save(false)}>${tn("profileSave")}</button>
666
- ${state.name ? html`<button type="button" class="btn" disabled=${busy} onClick=${() => save(true)}>${state.bound ? tn("profileUseAccount", state.platform || "web-user") : tn("profileClear")}</button>` : null}</div>
667
- </section>`;
697
+ <button type="button" class="btn primary" disabled=${busy || !changed || !draft.trim()} onClick=${() => save(false)}>${embedded ? tn("prefsSaveName") : tn("profileSave")}</button></div>
698
+ <p class="sub" id="profile-help">${state.error ? html`<span class="err" role="alert">${state.error}</span>` : tn("profileHelp", state.shown || "web-user", state.id)}</p>
699
+ </div>
700
+ <p class="sub s-pref-immediate">${tn("prefsNameImmediate")}</p>
701
+ ${state.name ? html`<div class="s-device-row"><button type="button" class="btn btn-ghost btn-sm" disabled=${busy} onClick=${() => save(true)}>${state.bound ? tn("profileUseAccount", state.platform || "web-user") : tn("profileClear")}</button></div>` : null}
702
+ <details class="s-pref-identity-details"><summary>${layoutText("profileIdentityDetails")}</summary><p class="sub">${state.bound ? tn("profileLeadBound") : tn("profileLeadUnbound")}</p></details>
703
+ </div></div>`;
704
+ return embedded ? html`<div class="s-profile s-pref-profile-fields">${fields}</div>`
705
+ : html`<section class="card s-device s-profile"><h3>${tn("profileTitle")}</h3>${fields}</section>`;
668
706
  }
669
707
 
670
708
  /** The operation in hand still owns Apply: its writes and job, or a restart of AgEnD from the press to its end. */
@@ -761,60 +799,67 @@ function agentChannelLabel(inst, chs) {
761
799
  return idx >= 0 ? `${chs[idx].type} (${chLabel(idx)})` : inst.channel_id;
762
800
  }
763
801
 
764
- function Agents({ ctx, search, openDialog }) {
802
+ function Agents({ ctx, search, clearSearch, openDialog }) {
765
803
  const insts = ctx.fleet.instances || {};
766
804
  const [collapsed, setCollapsed] = useState(() => new Set());
767
805
  const [busy, setBusy] = useState(() => new Map()); // name → the action under way
806
+ const [runtime, setRuntime] = useState("");
807
+ const [status, setStatus] = useState("");
808
+ // The computers' names come from the same list as the Runtime page (revoked ones included, so an Agent still bound to one can say so).
809
+ const catalog = useRuntimeCatalog(Object.values(insts).some((inst) => inst?.machine_id));
810
+ // A fleet with remote Runtimes off answers { enabled: false, machines: [] }: that is no list, not an empty one.
811
+ const machines = catalog.data && catalog.data.enabled !== false ? new Map(catalog.data.machines.map((machine) => [machine.id, { name: machine.name, status: machine.status }])) : null;
768
812
  const chs = channelsOf(ctx.fleet);
769
813
  // #2172: the sidebar's grouping — an agent in a team under its team, else under its first tag.
770
814
  const teamsOf = (name) => Object.keys(ctx.fleet.teams || {}).filter((t) => Array.isArray(ctx.fleet.teams[t]?.members) && ctx.fleet.teams[t].members.includes(name));
771
- const groups = groupAgents(insts, ctx.live, search, (name) => navGroupOf({ tags: insts[name]?.tags, teams: teamsOf(name) }), groupLabel);
815
+ const groups = filterAgentGroups(groupAgents(insts, ctx.live, search, (name) => navGroupOf({ tags: insts[name]?.tags, teams: teamsOf(name) }), groupLabel), insts, ctx.live, { runtime, status });
816
+ const active = !!(runtime || status || search.trim());
772
817
  // One action per agent at a time: the whole row is off (Settings and ⋯ too), and the pressed button says it is working.
773
818
  const run = async (name, action, work) => {
774
819
  if (busy.has(name)) return;
775
820
  setBusy((b) => new Map(b).set(name, action));
776
821
  try { await work(); } finally { setBusy((b) => { const n = new Map(b); n.delete(name); return n; }); }
777
822
  };
778
- const head = html`<div class="list-head"><span>${Object.keys(insts).length === 1 ? tn("countAgent") : tn("countAgents", Object.keys(insts).length)}</span>
779
- <button type="button" id="newAgentBtn" class="btn btn-primary" onClick=${() => openDialog({ kind: "create" })}><${Icon} name="plus" size=${16} />${tn("newAgent")}</button></div>`;
780
- // #2233: after the agents, each login on this machine with Sign in (backend-signin.js).
781
- const signIns = html`<${SignInList} />`;
782
- if (!Object.keys(insts).length) return html`${head}<${Empty} icon="bot" title=${tn("noAgents")} />${signIns}`;
783
- if (!groups.length) return html`${head}<${Empty} icon="search" title=${tn("noAgentMatch", search.trim())} />${signIns}`;
784
- return html`${head}<div class="s-list">${groups.map(({ group, names }) => {
785
- const folded = collapsed.has(group) && !search.trim(); // searching opens every group
823
+ const head = html`<${AgentFilters} instances=${insts} live=${ctx.live} machines=${machines} runtime=${runtime} status=${status} active=${active}
824
+ matched=${groups.reduce((count, group) => count + group.names.length, 0)} total=${Object.keys(insts).length} onRuntime=${setRuntime} onStatus=${setStatus}
825
+ onClear=${() => { setRuntime(""); setStatus(""); clearSearch(); }} />`;
826
+ if (!Object.keys(insts).length) return html`${head}<${Empty} icon="bot" title=${tn("noAgents")} />`;
827
+ if (!groups.length) return html`${head}<${Empty} icon="search" title=${search.trim() ? tn("noAgentMatch", search.trim()) : agentFilterText("noMatch")} hint=${agentFilterText("noMatchHint")} />`;
828
+ return html`${head}<div class="s-list s-agents-list">
829
+ <div class="s-list-columns s-agent-columns" aria-hidden="true"><span>${tn("uiAgentColumnName")}</span><span>${agentFilterText("runtime")}</span><span>${tn("uiAgentColumnCli")}</span><span>${tn("uiListColumnState")}</span><span>${tn("uiListColumnActions")}</span></div>${groups.map(({ group, names }) => {
830
+ const folded = collapsed.has(group) && !active; // searching/filtering opens every matching group
786
831
  return html`<section key=${group} class="s-group">
787
832
  <button type="button" class="s-group-head" aria-expanded=${folded ? "false" : "true"}
788
833
  onClick=${() => setCollapsed((c) => { const n = new Set(c); n.has(group) ? n.delete(group) : n.add(group); return n; })}>
789
834
  <${Icon} name="chevron" size=${14} cls=${folded ? "caret folded" : "caret"} /><${GroupMark} group=${group} /><span class="grow">${groupLabel(group)}</span><span class="count">${names.length}</span></button>
790
835
  ${folded ? null : names.map((name) => {
791
836
  const inst = insts[name];
792
- const st = (ctx.live[name] && ctx.live[name].status) || "stopped";
837
+ const st = agentStatus(ctx.live[name]);
793
838
  const sum = effectiveSummary(ctx.live[name] || {}, inst, ctx.fleet.defaults || {});
794
839
  const ch = agentChannelLabel(inst, chs);
795
840
  const working = busy.has(name), doing = busy.get(name);
796
841
  const label = (action, text) => (doing === action ? tn("working") : text);
797
- return html`<div key=${name} class="s-row">
798
- <span class=${dotClass(st)} title=${tn(STATUS_KEY[st] || "stStopped")} aria-hidden="true"></span>
799
- <span class="s-name" title=${name}>${shortName(name, inst)}</span>
800
- ${inst.runtime === "windows" ? html`<span class="tag">Windows</span>` : null}
801
- <span class="s-meta" title=${sum.backend}>${sum.model}</span>
802
- <span class="s-br" aria-hidden="true"></span>
803
- ${inst.runtime !== "windows" || ctx.live[name]?.capabilities?.effort === true ? html`<span class="tag">${tn("effortTag", sum.effort)}</span>` : null}
804
- ${ch ? html`<span class=${`tag${ch.includes("persona") ? " persona" : ""}`}>${ch}</span>` : null}
805
- <span class="sr-only">${tn(STATUS_KEY[st] || "stStopped")}</span>
842
+ const runtimeTarget = agentRuntime(inst, ctx.live[name], machines);
843
+ return html`<div key=${name} class="s-row s-agent-row">
844
+ <div class="s-agent-identity"><span class="s-name" title=${name}>${shortName(name, inst)}</span>
845
+ <div class="s-agent-context">${inst.runtime !== "windows" || ctx.live[name]?.capabilities?.effort === true ? html`<span class="tag">${tn("effortTag", sum.effort)}</span>` : null}
846
+ ${ch ? html`<span class=${`tag${ch.includes("persona") ? " persona" : ""}`}>${ch}</span>` : null}</div>
847
+ </div>
848
+ <div class="s-agent-runtime"><span class=${`tag${runtimeTarget.gone ? " warn" : ""}`} title=${runtimeTarget.title || runtimeTarget.label}>${runtimeTarget.label}</span></div>
849
+ <div class="s-agent-cli"><${CliProductMark} id=${sum.backend} size=${20} /><span><strong>${cliLabel(sum.backend)}</strong><span class="s-meta" title=${sum.model}>${sum.model}</span></span></div>
850
+ <span class="s-agent-status"><span class=${dotClass(agentStatus(ctx.live[name]))} aria-hidden="true"></span><span>${agentFilterText(agentStatus(ctx.live[name]))}${agentRuntimeOffline(inst, ctx.live[name]) ? html`<small class="s-agent-runtime-state">${agentFilterText("offline")}</small>` : null}</span></span>
806
851
  <span class="s-actions">
807
852
  ${inst.runtime === "windows" ? html`<a class="btn btn-sm" href=${chatPath(name)}>${t("app.chat")}</a>` : null}
808
853
  <button type="button" class="btn btn-sm" disabled=${working} onClick=${() => openDialog({ kind: "agent", name, inst })}>${tn("settingsButton")}</button>
809
854
  ${st === "running" ? html`<button type="button" class="btn btn-sm" disabled=${working} onClick=${() => run(name, "pause", () => ctx.pauseWake(name, "pause"))}>${label("pause", tn("pause"))}</button>` : null}
810
855
  ${st === "paused" ? html`<button type="button" class="btn btn-sm" disabled=${working} onClick=${() => run(name, "wake", () => ctx.pauseWake(name, "wake"))}>${label("wake", tn("wake"))}</button>` : null}
811
- ${st !== "paused" ? html`<button type="button" class="btn btn-sm" disabled=${working} onClick=${() => run(name, "startStop", () => ctx.startStop(name, st === "running"))}>${label("startStop", st === "running" ? tn("stop") : tn("start"))}</button>` : null}
812
- <${Menu} label=${t("app.more")} items=${[{ key: "delete", label: tn("deleteAgentMenu"), icon: "trash", danger: true, disabled: working, onSelect: () => ctx.deleteAgent(name) }]} />
856
+ ${["running", "stopped", "crashed"].includes(st) ? html`<button type="button" class="btn btn-sm" disabled=${working} onClick=${() => run(name, "startStop", () => ctx.startStop(name, st === "running"))}>${label("startStop", st === "running" ? tn("stop") : tn("start"))}</button>` : null}
857
+ <${Menu} cls="s-agent-menu" fit=${true} label=${t("app.more")} items=${[{ key: "delete", label: tn("deleteAgentMenu"), icon: "trash", danger: true, disabled: working, onSelect: () => ctx.deleteAgent(name) }]} />
813
858
  </span>
814
859
  </div>`;
815
860
  })}
816
861
  </section>`;
817
- })}</div>${signIns}`;
862
+ })}</div>`;
818
863
  }
819
864
 
820
865
  function ConnectionRetry({ id, ctx }) {
@@ -844,10 +889,10 @@ function Bots({ ctx, search, openDialog }) {
844
889
  const all = channelsOf(ctx.fleet);
845
890
  const needle = search.trim().toLowerCase();
846
891
  const rows = all.map((ch, i) => ({ ch, i })).filter(({ ch }) => !needle || [ch.id, ch.type, ch.group_id, ch.bot_token_env].some((v) => String(v ?? "").toLowerCase().includes(needle)));
847
- const head = html`<div class="list-head"><span>${all.length === 1 ? tn("countBot") : tn("countBots", all.length)}</span>
892
+ const head = html`<div class="list-head s-list-toolbar"><span>${all.length === 1 ? tn("countBot") : tn("countBots", all.length)}</span>
848
893
  <button type="button" id="newBotBtn" class="btn btn-primary" onClick=${() => openDialog({ kind: "newBot" })}><${Icon} name="plus" size=${16} />${tn("newBot")}</button></div>`;
849
- return html`${head}
850
- ${!rows.length ? html`<${Empty} icon="plug" title=${needle ? tn("noAgentMatch", search.trim()) : tn("noBots")} />` : html`<div class="s-list">${rows.map(({ ch, i }) => {
894
+ return html`<section class="card s-chat-connections"><div class="s-chat-section-head"><h3>${layoutText("chatConnections")}</h3>${head}</div>
895
+ ${!rows.length ? html`<div class="s-chat-empty"><${Icon} name="plug" size=${22} /><p>${needle ? tn("noAgentMatch", search.trim()) : tn("noBots")}</p></div>` : html`<div class="s-list s-connections-list"><div class="s-list-columns s-connection-columns" aria-hidden="true"><span>${tn("uiConnectionColumnService")}</span><span>${tn("uiConnectionColumnInfo")}</span><span>${tn("uiListColumnActions")}</span></div>${rows.map(({ ch, i }) => {
851
896
  const users = (ch.access && ch.access.allowed_users) || [];
852
897
  const meta = ctx.connections.find((c, j) => channelId(c, j) === channelId(ch, i)) || {};
853
898
  const token = meta.token_present;
@@ -872,7 +917,7 @@ function Bots({ ctx, search, openDialog }) {
872
917
  ${!overPublicLink && ctx.fleetUp && meta.status !== "connected" ? html`<${ConnectionRetry} id=${channelId(ch, i)} ctx=${ctx} />` : null}
873
918
  <button type="button" class="btn btn-sm" onClick=${() => openDialog({ kind: "bot", id: channelId(ch, i) })}>${tn("settingsButton")}</button></span>
874
919
  </div>`;
875
- })}</div>`}`;
920
+ })}</div>`}</section>`;
876
921
  }
877
922
 
878
923
  /**
@@ -922,40 +967,40 @@ function ProviderKeys({ ctx }) {
922
967
  } catch { say(spec.id, { error: tn("failed") }); }
923
968
  finally { inFlight.current.delete(spec.id); if (lease.current()) setBusy(new Set(inFlight.current)); }
924
969
  };
925
- return html`<section class="card s-provider-keys"><h3>${tn("providerKeys")}</h3>
926
- ${!ctx.providerSecrets.length ? html`<p class="note">${tn("noProviderVerifiers")}</p>` : ctx.providerSecrets.map((spec) => html`<div key=${spec.id} class="s-key">
927
- <div class="s-key-row"><strong>${spec.display_name}</strong><span class="tag">${spec.token_present ? tn("keyConfigured") : tn("keyNotConfigured")}</span>
928
- ${spec.verifier === "available" ? html`<input type="password" autocomplete="new-password" aria-label=${tn("providerKeyInput", spec.display_name)}
929
- placeholder=${spec.token_present ? tn("keyConfigured") : tn("keyNotConfigured")} value=${keys[spec.id] || ""} onInput=${(e) => setKeys((k) => ({ ...k, [spec.id]: e.target.value }))} />
930
- <button type="button" class="btn btn-sm btn-primary" disabled=${busy.has(spec.id)} onClick=${() => go(spec)}>${tn("verifyApply")}</button>`
931
- : html`<span class="tag">${tn("unsupportedVerifier")}</span>`}</div>
932
- ${spec.verifier === "available" ? null : html`<p class="note">${tn("unsupportedVerifierHint")}</p>`}
933
- ${notes[spec.id] ? html`<p class=${`feedback${notes[spec.id].error ? " error" : ""}`} role="status">${notes[spec.id].error || notes[spec.id].text}</p>` : null}
934
- </div>`)}
970
+ return html`<section class="card s-provider-keys s-auth-section"><header class="s-auth-section-head"><div><h3>${layoutText("apiKeysTitle")}</h3><p>${layoutText("apiKeysHint")}</p></div></header>
971
+ ${!ctx.providerSecrets.length ? html`<p class="note">${tn("noProviderVerifiers")}</p>` : html`<div class="s-api-key-list">${ctx.providerSecrets.map((spec) => {
972
+ const provider = spec.id.split(".")[0], mark = ["openai", "anthropic"].includes(provider) ? provider : null;
973
+ return html`<div key=${spec.id} class="s-key s-api-key-row">
974
+ <div class="s-api-key-heading"><span class="s-provider-symbol">${mark ? html`<${ProviderMark} provider=${mark} size=${22} />` : html`<${Icon} name="key" size=${22} />`}</span><strong>${spec.display_name}</strong><span class=${`s-auth-state${spec.token_present ? " is-usable" : ""}`}>${spec.token_present ? tn("keyConfigured") : tn("keyNotConfigured")}</span></div>
975
+ ${spec.verifier === "available" ? html`<div class="s-api-key-form"><label><span>${tn("providerKeyInput", spec.display_name)}</span><input type="password" autocomplete="new-password" aria-label=${tn("providerKeyInput", spec.display_name)}
976
+ placeholder=${layoutText(spec.token_present ? "apiReplacePlaceholder" : "apiNewPlaceholder")} value=${keys[spec.id] || ""} onInput=${(e) => setKeys((k) => ({ ...k, [spec.id]: e.target.value }))} /></label>
977
+ <button type="button" class="btn btn-sm btn-primary" disabled=${busy.has(spec.id)} onClick=${() => go(spec)}>${tn("verifyApply")}</button></div>`
978
+ : html`<div class="s-api-key-unsupported"><span class="s-auth-state">${tn("unsupportedVerifier")}</span><p class="note">${tn("unsupportedVerifierHint")}</p></div>`}
979
+ ${notes[spec.id] ? html`<p class=${`feedback${notes[spec.id].error ? " error" : ""}`} role="status">${notes[spec.id].error || notes[spec.id].text}</p>` : null}
980
+ </div>`;
981
+ })}</div>`}
982
+ <p class="note s-api-action-note">${tn("uiProviderKeyActionHint")}</p>
935
983
  </section>`;
936
984
  }
937
985
 
938
986
  function Classic({ ctx, search, openDialog }) {
939
987
  const rooms = Object.entries((ctx.classic && ctx.classic.channels) || {}).map(([key, ch]) => ({ key, ...ch }));
940
- if (!rooms.length) return html`<${Empty} icon="room" title=${tn("noClassic")} />`;
988
+ if (!rooms.length) return html`<div class="s-chat-empty"><${Icon} name="room" size=${22} /><p>${tn("noClassic")}</p></div>`;
941
989
  const needle = search.trim().toLowerCase();
942
990
  const list = rooms.filter((c) => !needle || String(c.name || "").toLowerCase().includes(needle) || String(c.instanceName || "").toLowerCase().includes(needle))
943
991
  .sort((a, b) => ((STATUS_ORDER[ctx.classicRuntime(a.instanceName).status] ?? 9) - (STATUS_ORDER[ctx.classicRuntime(b.instanceName).status] ?? 9))
944
992
  || String(a.name || "").localeCompare(String(b.name || "")));
945
- if (!list.length) return html`<${Empty} icon="search" title=${tn("noAgentMatch", search.trim())} />`;
946
- return html`<div class="list-head"><span>${rooms.length === 1 ? tn("countRoom") : tn("countRooms", rooms.length)}</span></div><div class="s-list">${list.map((c) => {
993
+ if (!list.length) return html`<div class="s-chat-empty"><${Icon} name="search" size=${22} /><p>${tn("noAgentMatch", search.trim())}</p></div>`;
994
+ return html`<div class="list-head s-list-toolbar"><span>${rooms.length === 1 ? tn("countRoom") : tn("countRooms", rooms.length)}</span></div><div class="s-list s-classic-list"><div class="s-list-columns s-classic-columns" aria-hidden="true"><span>${tn("uiClassicColumnRoom")}</span><span>${tn("uiAgentColumnCli")}</span><span>${tn("uiListColumnState")}</span><span>${tn("uiListColumnActions")}</span></div>${list.map((c) => {
947
995
  const rt = ctx.classicRuntime(c.instanceName);
948
996
  const sum = effectiveSummary(ctx.live[c.instanceName] || {}, c, ctx.fleet.defaults || {}, (ctx.classic && ctx.classic.defaults) || {});
949
997
  const adapter = c.adapterId || "discord";
950
- return html`<div key=${c.key} class="s-row">
951
- <span class=${dotClass(rt.execution)} title=${tn(`exec_${rt.execution}`)} aria-hidden="true"></span>
952
- <span class="s-name" title=${c.instanceName || ""}>${shortName(c.name || c.instanceName || "?", { display_name: c.display_name })}</span>
953
- <span class="s-meta" title=${sum.backend}>${sum.model}</span>
954
- <span class="s-br" aria-hidden="true"></span>
955
- <span class="tag">${tn("effortTag", sum.effort)}</span>
956
- <span class=${`tag${adapter !== "discord" ? " persona" : ""}`}>${adapter}</span>
957
- ${c.collab ? html`<span class="tag">collab</span>` : null}
958
- <span class="tag">${tn(`exec_${rt.execution}`)}</span>
998
+ return html`<div key=${c.key} class="s-row s-classic-row">
999
+ <div class="s-classic-identity"><span class="s-name" title=${c.instanceName || ""}>${shortName(c.name || c.instanceName || "?", { display_name: c.display_name })}</span>
1000
+ <div class="s-agent-context"><span class="tag">${tn("effortTag", sum.effort)}</span><span class=${`tag${adapter !== "discord" ? " persona" : ""}`}>${adapter}</span>${c.collab ? html`<span class="tag">collab</span>` : null}</div>
1001
+ </div>
1002
+ <div class="s-classic-cli"><${CliProductMark} id=${sum.backend} size=${20} /><span><strong>${cliLabel(sum.backend)}</strong><span class="s-meta" title=${sum.model}>${sum.model}</span></span></div>
1003
+ <span class="s-classic-status"><span class=${dotClass(rt.execution)} aria-hidden="true"></span><span>${tn(`exec_${rt.execution}`)}</span></span>
959
1004
  <span class="s-actions">
960
1005
  <button type="button" class="btn btn-sm" onClick=${() => openDialog({ kind: "classic", room: c })}>${tn("settingsButton")}</button>
961
1006
  ${rt.status === "running" ? html`<button type="button" class="btn btn-sm" onClick=${() => ctx.pauseWake(c.instanceName, "pause")}>${tn("pause")}</button>` : null}
@@ -1091,30 +1136,43 @@ function General({ ctx, part = "all" }) {
1091
1136
  const classicReply = replyGuardSupported(f.classicBackend, f.agent_mode, d.kiro_ui) ? tn("replyGuardSupported") : tn("replyGuardUnsupported", f.classicBackend, f.agent_mode);
1092
1137
  // #2347: Agent defaults show the fleet's part; Chat & connections ClassicBot's. One form, one draft either way.
1093
1138
  const fleetPart = part !== "classic", classicPart = part !== "fleet";
1094
- return html`<div class=${`form s-general s-part-${part}`}>
1095
- ${fleetPart ? html`<section class="card"><h3>${tn("defaultsTitle")}</h3>
1139
+ return html`<div class=${fleetPart ? "s-defaults-layout" : "s-classic-layout"}><div class=${`form s-general s-part-${part}`}>
1140
+ ${fleetPart ? html`<section class="card s-defaults-model"><h3>${tn("uiDefaultsModelsTitle")}</h3>
1096
1141
  <div class="grid2">
1097
- <div class=${fc("field", "locale")}>${L(tn("language"), "defaults.locale", "g-locale", "locale")}<${Select} id="g-locale" value=${f.locale} onChange=${set("locale")}
1098
- options=${localeOptions(f.locale).map((o) => (o.auto ? { value: "", label: tn("localeAuto") } : o))} /></div>
1099
1142
  <div class=${fc("field", "backend")}>${L(tn("defaultBackend"), "defaults.backend", "g-be", "backend")}<${BackendSelect} id="g-be" value=${f.backend} onChange=${set("backend")} /></div>
1100
1143
  <div class=${fc("field", "model")}>${L(tn("defaultModel"), "defaults.model", "g-model", "model")}<${ModelSelect} id="g-model" value=${f.model} onChange=${set("model")}
1101
1144
  backend=${f.backend || "claude-code"} inherit=${tn("modelCliDefault")} /></div>
1145
+ <div class=${fc("field", "locale")}>${L(tn("language"), "defaults.locale", "g-locale", "locale")}<${Select} id="g-locale" value=${f.locale} onChange=${set("locale")}
1146
+ options=${localeOptions(f.locale).map((o) => (o.auto ? { value: "", label: tn("localeAuto") } : o))} /></div>
1147
+ </div>
1148
+ </section>
1149
+ <section class="card s-defaults-work"><h3>${tn("uiDefaultsWorkTitle")}</h3>
1150
+ <div class="grid2">
1102
1151
  <div class=${fc("field", "auto")}>${L(`${tn("autoPause")} (${tn("minutes")})`, "defaults.auto_pause_after", "g-auto", "auto")}<input id="g-auto" type="number" min="0" step="1" class=${errors.auto ? "invalid" : ""} value=${f.auto} onInput=${(e) => set("auto")(e.target.value)} /><${Feedback} error=${errors.auto} /></div>
1103
1152
  <div class=${fc("field", "tool_progress")}>${L(tn("toolProgress"), "defaults.tool_progress", "g-tp", "tool_progress")}<${Select} id="g-tp" value=${f.tool_progress} onChange=${set("tool_progress")} options=${TOOL_PROGRESS} /></div>
1153
+ <div class=${fc("field", "tips")}>${L(tn("dailyTips"), "defaults.tips", "g-tips", "tips")}<${Select} id="g-tips" value=${f.tips} onChange=${set("tips")} options=${noticeChoices()} /><${InEffect} text=${noticeLabel(noticeMode(f.tips))} /><p class="note">${tn("dailyTipsHint")}</p></div>
1154
+ </div>
1155
+ </section>
1156
+ <section class="card s-defaults-collaboration"><h3>${tn("uiDefaultsCollaborationTitle")}</h3>
1157
+ <div class="grid2">
1104
1158
  <div class=${fc("field", "visibility")}>${L(tn("crossInstanceVisibility"), "defaults.cross_instance_visibility", "g-vis", "visibility")}<${Select} id="g-vis" value=${f.visibility} onChange=${set("visibility")} options=${VISIBILITY_MODES} />
1105
1159
  <p class="note">${tn("crossInstanceVisibilityHint")}</p></div>
1106
- <div class=${fc("field", "tips")}>${L(tn("dailyTips"), "defaults.tips", "g-tips", "tips")}<${Select} id="g-tips" value=${f.tips} onChange=${set("tips")} options=${noticeChoices()} /><${InEffect} text=${noticeLabel(noticeMode(f.tips))} /><p class="note">${tn("dailyTipsHint")}</p></div>
1107
- <div class=${fc("field", "replyTriggers")}><label class="check"><input id="g-reply-triggers" type="checkbox" checked=${f.replyTriggers} onChange=${(e) => set("replyTriggers")(e.target.checked)} /><span>${tn("replyTriggers")}</span>${dm("replyTriggers")}<${Impact} schema=${schema} field="defaults.reply_triggers" /></label><p class="note">${tn("replyTriggersHint")}</p></div>
1108
- <div class=${fc("field", "reply")}><label class="check"><input id="g-reply" type="checkbox" checked=${f.reply} onChange=${(e) => set("reply")(e.target.checked)} /><span>${tn("replyCompletionGuard")}</span>${dm("reply")}<${Impact} schema=${schema} field="defaults.reply_completion_guard" /></label>
1109
- <p class="note">${fleetReply}</p></div>
1160
+ <div class=${fc("field", "replyTriggers")}><label for="g-reply-triggers" class="s-defaults-toggle-label"><span>${tn("replyTriggers")}</span>${dm("replyTriggers")}</label>
1161
+ <input id="g-reply-triggers" class="s-ui-toggle" type="checkbox" checked=${f.replyTriggers} onChange=${(e) => set("replyTriggers")(e.target.checked)} />
1162
+ <div class="s-defaults-toggle-help"><details class="s-defaults-explanation"><summary>${layoutText("replyRulesHelp")}</summary><p class="note">${tn("replyTriggersHint")}</p></details><${Impact} schema=${schema} field="defaults.reply_triggers" /></div></div>
1163
+ <div class=${fc("field", "reply")}><label for="g-reply" class="s-defaults-toggle-label"><span>${tn("replyCompletionGuard")}</span>${dm("reply")}</label>
1164
+ <input id="g-reply" class="s-ui-toggle" type="checkbox" checked=${f.reply} onChange=${(e) => set("reply")(e.target.checked)} />
1165
+ <div class="s-defaults-toggle-help"><details class="s-defaults-explanation"><summary>${layoutText("replySupportHelp")}</summary><p class="note">${fleetReply}</p></details><${Impact} schema=${schema} field="defaults.reply_completion_guard" /></div></div>
1110
1166
  </div>
1167
+ </section>
1168
+ <details class="s-ui-disclosure s-defaults-permissions"><summary>${tn("uiDefaultsCapabilitiesTitle")}</summary><div class="s-ui-disclosure-body">
1111
1169
  <fieldset id="g-dash" class=${fc("field dash-perms", ...DASHBOARD_FEATURES.map((k) => `dash_${k}`))}><legend>${L(tn("dashboardAccess"), "defaults.dashboard")}</legend>
1112
1170
  <p class="note">${tn("dashboardDefaultsHint")}</p>
1113
1171
  ${DASHBOARD_FEATURES.map((k) => html`<label key=${k} class="check"><input type="checkbox" checked=${f[`dash_${k}`]}
1114
1172
  onChange=${(e) => set(`dash_${k}`)(e.target.checked)} /><span>${tn(`dashboard_${k}`)}</span></label>`)}
1115
1173
  </fieldset>
1116
- </section>` : null}
1117
- ${classicPart ? html`<section class="card"><h3>${tn("classicDefaultsTitle")}</h3>
1174
+ </div></details>` : null}
1175
+ ${classicPart ? html`<section class="card s-classic-defaults-card"><h3>${tn("classicDefaultsTitle")}</h3>
1118
1176
  <div class="grid2">
1119
1177
  <div class=${fc("field", "classicBackend")}>${L(tn("classicBackend"), "classic_defaults.backend", "g-cbe")}<${BackendSelect} id="g-cbe" value=${f.classicBackend} onChange=${set("classicBackend")} />
1120
1178
  ${!c.backend && f.classicBackend === base.classicBackend ? html`<${Inherited} text=${valueText(f.classicBackend, "backend")} builtin=${!d.backend} from=${inAgentDefaults("#g-be")} />` : null}</div>
@@ -1135,12 +1193,12 @@ function General({ ctx, part = "all" }) {
1135
1193
  <div id="g-guilds" class=${fc("field", "guilds")}>${L(tn("allowedGuilds"), "classic.allowed_guilds")}<${ChipList} items=${f.guilds} onChange=${set("guilds")} placeholder=${tn("guildIdPlaceholder")} label=${tn("allowedGuilds")} /></div>
1136
1194
  </div>
1137
1195
  </section>` : null}
1138
- ${fleetPart ? html`<section class="card" id="project-roots"><h3>${tn("projectRootsTitle")}</h3>
1196
+ ${fleetPart ? html`<details class="s-ui-disclosure s-defaults-workspace" id="project-roots"><summary>${tn("uiDefaultsWorkspaceTitle")}</summary><div class="s-ui-disclosure-body"><section class="card"><h3>${tn("projectRootsTitle")}</h3>
1139
1197
  <p class="note">${tn("projectRootsHint")}</p>
1140
1198
  <div id="g-roots" class=${fc("field", "roots")}>${L(tn("projectRootsTitle"), "fleet.project_roots")}<${ChipList} items=${f.roots} onChange=${set("roots")} placeholder="/path/to/projects" label=${tn("projectRootsTitle")} />
1141
1199
  ${f.roots.length ? null : html`<${InEffect} text=${tn("projectRootsAnywhere")} />`}</div>
1142
- </section>` : null}
1143
- ${fleetPart ? html`<${Drawer} title=${tn("advancedSection")}>
1200
+ </section></div></details>` : null}
1201
+ ${fleetPart ? html`<${Drawer} title=${tn("uiDefaultsAdvancedTitle")}>
1144
1202
  <section class="card"><h3>${tn("runtimeResources")}</h3><div class="grid2">
1145
1203
  <div class=${fc("field", "tool_set")}>${L(tn("toolSet"), "defaults.tool_set", "g-ts", "tool_set")}<${Select} id="g-ts" value=${f.tool_set} onChange=${set("tool_set")}
1146
1204
  options=${[{ value: "", label: tn("toolSetUnset", toolSetDefault({}, {}), toolSetDefault({}, { general_topic: true })) }, ...TOOL_SETS]} /></div>
@@ -1159,7 +1217,7 @@ function General({ ctx, part = "all" }) {
1159
1217
  <section class="card"><h3>${tn("accessTitle")}</h3>
1160
1218
  <p class="note">${primary ? tn("accessMovedHint", primary.id || chLabel(0)) : tn("noBots")}</p></section>
1161
1219
  </${Drawer}>` : null}
1162
- </div>`;
1220
+ </div></div>`;
1163
1221
  }
1164
1222
 
1165
1223
  /**
@@ -1199,6 +1257,7 @@ export function uploadMbInEffect(web) {
1199
1257
  }
1200
1258
  function WebSection({ ctx, staged, part = "all" }) {
1201
1259
  const { schema } = ctx;
1260
+ const [accessTab, setAccessTab] = useSettingsTabs("access", "login");
1202
1261
  const web = ctx.fleet.web || {}, local = (web.session && web.session.local) || {}, term = ctx.fleet.web_terminal || {};
1203
1262
  // Over the public link the fields read only; the Guests and Web authority cards are not offered (the server never
1204
1263
  // serves their reads there).
@@ -1329,17 +1388,31 @@ function WebSection({ ctx, staged, part = "all" }) {
1329
1388
  if (v === undefined) return html`${roText(webReadOnlyDefault(path, ctx.fleet))} <${DefaultMark} />${capped ? html` <span class="tag">${tn("cappedByTotal")}</span>` : null}`;
1330
1389
  return applies === v ? roText(v) : html`${roText(v)} <span class="tag">${tn("inEffectShort", roText(applies))}</span>`;
1331
1390
  };
1332
- // #2347: one form, one draft, shown where each part belongs — Access & security (sessions, the browser terminal, the
1333
- // public link and its fixed address, guests, web authority), Chat & connections (web echo), Web dashboard (the Log tab,
1334
- // usage over the public link), Advanced (what only fleet.yaml sets).
1391
+ // One Web draft remains mounted across the Access tabs. A tab only changes what is visible;
1392
+ // validation, confirmation and staged writes continue to belong to the existing form.
1335
1393
  const has = (p) => part === "all" || part === p;
1336
1394
  const lock = (body) => html`<fieldset class="s-web-fields" disabled=${overPublicLink}>${body}</fieldset>`;
1395
+ const tabs = [
1396
+ { id: "login", label: layoutText("accessLogin") },
1397
+ { id: "guests", label: layoutText("accessGuests") },
1398
+ { id: "sharing", label: layoutText("accessSharing") },
1399
+ ];
1400
+ const toggleRow = (key, id, label, hint, impact) => html`<div class=${fc("s-setting-row", key)}>
1401
+ <div class="s-setting-copy"><label for=${id}>${part === "dashboard" ? layoutText(`revDashboard_${key}`) : tn(label)}<${Impact} schema=${schema} field=${impact} /></label>
1402
+ <p class="note" id=${`${id}-hint`}>${part === "dashboard" ? layoutText(`revDashboardHint_${key}`) : tn(hint)}</p>
1403
+ ${part === "dashboard" ? html`<details class="s-setting-details"><summary>${layoutText("revDetails")}</summary><p class="note"><strong>${tn(label)}</strong> ${tn(hint)}</p></details>` : null}
1404
+ </div>
1405
+ <input class="s-ui-toggle" id=${id} type="checkbox" checked=${f[key]} aria-describedby=${`${id}-hint`} onChange=${(e) => set(key)(e.target.checked)} />
1406
+ </div>`;
1407
+ const configured = (path) => { const [top, key] = path.split("."); return (top === "web" ? web : term)[key] !== undefined; };
1337
1408
  return html`<div class=${`form s-web s-part-${part}`}>
1338
- ${has("access") ? html`<p class="sub">${tn("webLead")}</p>` : null}
1339
- ${overPublicLink && part !== "readonly" ? html`<p class="note" role="status" id="webPublicRefused">${tn("webPublicRefused")}</p>` : null}
1340
- ${has("access") ? lock(html`<section class="card"><h3>${tn("webSessionTitle")}</h3>
1341
- <p class="note">${tn("webSessionHint")}</p>
1342
- <div class=${fc("field", "notify")}><label for="webNotifyLogin">${tn("webNotifyLogin")}<${Impact} schema=${schema} field="web.notify_login" /></label><${Select} id="webNotifyLogin" value=${f.notify} onChange=${set("notify")} options=${noticeChoices()} /><${InEffect} text=${noticeLabel(noticeMode(f.notify))} />${web.notify_login === undefined ? html`<${DefaultMark} />` : null}<p class="note">${tn("webNotifyLoginHint")}</p></div>
1409
+ ${overPublicLink && part !== "readonly" ? html`<p class="note s-access-notice" role="status" id="webPublicRefused">${tn("webPublicRefused")}</p>` : null}
1410
+ ${has("access") ? html`<${SettingsTabs} id="access" value=${accessTab} onChange=${setAccessTab} items=${tabs} />
1411
+ <div class="s-access-banner"><${Icon} name="info" size=${18} /><p>${layoutText("revAccessPolicy")}</p><details class="s-access-policy"><summary>${layoutText("revDetails")}</summary><p class="note">${tn("webLead")}</p></details></div>
1412
+ <${SettingsTabPanel} group="access" id="login" active=${accessTab === "login"}>
1413
+ <div class="s-access-login-layout"><div class="s-access-stack">
1414
+ ${lock(html`<section class="card s-access-session"><h3>${layoutText("revLoginSessions")}</h3>
1415
+ <div class=${fc("field", "notify")}><label for="webNotifyLogin">${tn("webNotifyLogin")}<${Impact} schema=${schema} field="web.notify_login" /></label><${Select} id="webNotifyLogin" value=${f.notify} onChange=${set("notify")} options=${noticeChoices()} /><${InEffect} text=${noticeLabel(noticeMode(f.notify))} /></div>
1343
1416
  <div class="grid2">
1344
1417
  <div class=${fc("field", "abs")}><label for="webSessionAbsolute">${tn("sessionAbsolute")}</label><input id="webSessionAbsolute" type="number" min="1" max="720" step="any"
1345
1418
  class=${errors.abs ? "invalid" : ""} value=${f.abs} onInput=${(e) => set("abs")(e.target.value)} /><${Feedback} error=${errors.abs} />
@@ -1348,8 +1421,13 @@ function WebSection({ ctx, staged, part = "all" }) {
1348
1421
  class=${errors.idle ? "invalid" : ""} value=${f.idle} onInput=${(e) => set("idle")(e.target.value)} /><${Feedback} error=${errors.idle} />
1349
1422
  ${value(f.idle) === null && !errors.abs ? html`<${InEffect} text=${tn("hoursValue", idle)} why=${idle < 48 ? tn("cappedBySession") : null} />` : null}</div>
1350
1423
  </div>
1351
- </section>
1352
- <section class="card"><h3>${tn("webTerminalTitle")}</h3>
1424
+ <p class="note s-session-ceiling">${layoutText("revPublicSessionLimit")}</p>
1425
+ <details class="s-setting-details"><summary>${layoutText("revSessionDetails")}</summary><p class="note">${tn("webSessionHint")}</p><p class="note">${tn("webNotifyLoginHint")}</p></details>
1426
+ </section>`)}
1427
+ ${!overPublicLink ? html`<${WebAuthorityCard} ctx=${ctx} staged=${staged} />` : null}
1428
+ ${/* #3008: a web-only fleet's connections from outside — changed on this computer only, shown (read-only) elsewhere. */ ""}
1429
+ <${OutsideCard} />
1430
+ <details class="drawer s-terminal-disclosure"><summary>${tn("webTerminalTitle")}</summary><div class="drawer-body">${lock(html`<section class="card s-access-terminal"><h3>${tn("webTerminalTitle")}</h3>
1353
1431
  <p class="note">${tn("webTerminalHint")}</p>
1354
1432
  <label class=${fc("check", "termOn")}><input id="webTerminalEnabled" type="checkbox" checked=${f.termOn} onChange=${(e) => set("termOn")(e.target.checked)} /><span>${tn("webTerminalEnabled")}</span><${Impact} schema=${schema} field="web_terminal.enabled" /></label>
1355
1433
  <div class="grid2">
@@ -1357,8 +1435,18 @@ function WebSection({ ctx, staged, part = "all" }) {
1357
1435
  class=${errors.termTtl ? "invalid" : ""} value=${f.termTtl} onInput=${(e) => set("termTtl")(e.target.value)} /><${Feedback} error=${errors.termTtl} />
1358
1436
  ${value(f.termTtl) === null ? html`<${InEffect} text=${tn("minutesValue", 10)} />` : null}</div>
1359
1437
  </div>
1360
- </section>
1361
- <section class="card"><h3>${tn("publicLinkTitle")}</h3>
1438
+ </section>`)}</div></details>
1439
+ </div><aside class="card s-access-related"><h3>${layoutText("revRelatedAccess")}</h3>
1440
+ <button type="button" onClick=${() => setAccessTab("guests")}><${Icon} name="user" size=${18} /><span><strong>${layoutText("accessGuests")}</strong><small>${layoutText("revRelatedGuests")}</small></span><${Icon} name="chevron" size=${16} /></button>
1441
+ <button type="button" onClick=${() => setAccessTab("sharing")}><${Icon} name="globe" size=${18} /><span><strong>${layoutText("accessSharing")}</strong><small>${layoutText("revRelatedSharing")}</small></span><${Icon} name="chevron" size=${16} /></button>
1442
+ </aside></div>
1443
+ </${SettingsTabPanel}>
1444
+ <${SettingsTabPanel} group="access" id="guests" active=${accessTab === "guests"}>
1445
+ ${!overPublicLink ? html`<${GuestsCard} ctx=${ctx} staged=${staged} />` : html`<p class="note">${tn("webPublicRefused")}</p>`}
1446
+ </${SettingsTabPanel}>
1447
+ <${SettingsTabPanel} group="access" id="sharing" active=${accessTab === "sharing"}>
1448
+ <div class="s-access-stack">
1449
+ ${lock(html`<section class="card s-public-entry"><h3>${layoutText("revPublicEntry")}</h3>
1362
1450
  <label class=${fc("check", "pub")}><input id="publicWebLink" type="checkbox" checked=${f.pub} onChange=${(e) => set("pub")(e.target.checked)} /><span>${tn("publicLink")}</span><${Impact} schema=${schema} field="web.public_link.allow_public" /></label>
1363
1451
  <p class="note">${tn("publicLinkHint")}</p>
1364
1452
  <div class="grid2">
@@ -1374,35 +1462,31 @@ function WebSection({ ctx, staged, part = "all" }) {
1374
1462
  ${named ? html`<p class="note" id="publicTtlNamedHint">${tn("publicTtlNamedHint")}</p>` : null}
1375
1463
  ${named || f.autoOpen ? html`<label class=${fc("check", "autoOpen")}><input id="publicAutoOpen" type="checkbox" checked=${f.autoOpen} onChange=${(e) => set("autoOpen")(e.target.checked)} /><span>${tn("publicAutoOpen")}</span><${Impact} schema=${schema} field="web.public_link.auto_open" /></label>
1376
1464
  <p class="note">${tn("publicAutoOpenHint")}</p><${Feedback} error=${errors.autoOpen} />` : null}
1465
+
1466
+ ${!overPublicLink ? html`<div class="s-public-credentials"><${PublicTunnelToken} /></div>` : null}
1467
+ </section>`)}
1468
+ <div class="s-sharing-list"><h3>${layoutText("revReadOnlySharing")}</h3><p class="note">${layoutText("revSharingOrigin")}</p><${OfficeSharesCard} /><${PageSharesCard} /></div>
1469
+ </div>
1470
+ </${SettingsTabPanel}>` : null}
1471
+ ${has("chat") ? lock(html`<section class="card s-web-chat"><h3>${layoutText("messageBehavior")}</h3>
1472
+ ${toggleRow("echo", "webEchoToChannel", "webEcho", "webEchoHint", "web.echo_to_channel")}
1377
1473
  </section>`) : null}
1378
- ${has("access") && !overPublicLink ? html`<${PublicTunnelToken} />` : null}
1379
- ${has("access") ? html`<${OfficeSharesCard} />` : null}
1380
- ${has("access") ? html`<${PageSharesCard} />` : null}
1381
- ${has("access") && !overPublicLink ? html`<${GuestsCard} ctx=${ctx} staged=${staged} /><${WebAuthorityCard} ctx=${ctx} staged=${staged} />` : null}
1382
- ${/* #3008: a web-only fleet's connections from outside — changed on this computer only, shown (read-only) elsewhere. */ ""}
1383
- ${has("access") ? html`<${OutsideCard} />` : null}
1384
- ${has("chat") ? lock(html`<section class="card"><h3>${tn("webChat")}</h3>
1385
- <label class=${fc("check", "echo")}><input id="webEchoToChannel" type="checkbox" checked=${f.echo} onChange=${(e) => set("echo")(e.target.checked)} /><span>${tn("webEcho")}</span><${Impact} schema=${schema} field="web.echo_to_channel" /></label>
1386
- <p class="note">${tn("webEchoHint")}</p>
1387
- </section>`) : null}
1388
- ${has("dashboard") ? lock(html`<section class="card"><h3>${tn("webDashboardTitle")}</h3>
1389
- <label class=${fc("check", "log")}><input id="webLogTab" type="checkbox" checked=${f.log} onChange=${(e) => set("log")(e.target.checked)} /><span>${tn("webLogTab")}</span><${Impact} schema=${schema} field="web.log_tab" /></label>
1390
- <p class="note">${tn("webLogTabHint")}</p>
1391
- <label class=${fc("check", "usagePub")}><input id="webUsagePublic" type="checkbox" checked=${f.usagePub} onChange=${(e) => set("usagePub")(e.target.checked)} /><span>${tn("webUsagePublic")}</span><${Impact} schema=${schema} field="web.usage_report_on_public_link" /></label>
1392
- <p class="note">${tn("webUsagePublicHint")}</p>
1393
- <label class=${fc("check", "levels")}><input id="webOfficeLevels" type="checkbox" checked=${f.levels} onChange=${(e) => set("levels")(e.target.checked)} /><span>${tn("webOfficeLevels")}</span><${Impact} schema=${schema} field="web.office_levels" /></label>
1394
- <p class="note">${tn("webOfficeLevelsHint")}</p>
1395
- <label class=${fc("check", "cdn")}><input id="webPreviewCdn" type="checkbox" checked=${f.cdn} onChange=${(e) => set("cdn")(e.target.checked)} /><span>${tn("webPreviewCdn")}</span><${Impact} schema=${schema} field="web.preview_cdn" /></label>
1396
- <p class="note">${tn("webPreviewCdnHint")}</p>
1474
+ ${has("dashboard") ? lock(html`<section class="card s-dashboard-features"><h3>${layoutText("dashboardFeatures")}</h3>
1475
+ ${toggleRow("log", "webLogTab", "webLogTab", "webLogTabHint", "web.log_tab")}
1476
+ ${toggleRow("usagePub", "webUsagePublic", "webUsagePublic", "webUsagePublicHint", "web.usage_report_on_public_link")}
1477
+ ${toggleRow("levels", "webOfficeLevels", "webOfficeLevels", "webOfficeLevelsHint", "web.office_levels")}
1478
+ ${toggleRow("cdn", "webPreviewCdn", "webPreviewCdn", "webPreviewCdnHint", "web.preview_cdn")}
1397
1479
  </section>`) : null}
1398
- ${has("readonly") ? html`<section class="card s-web-readonly"><h3>${tn("webReadOnlyTitle")}</h3>
1399
- <p class="note">${tn("webReadOnlyLead")}</p>
1400
- <dl class="s-web-ro">${WEB_READ_ONLY.map((k) => html`<div key=${k}><dt><code>${k}</code></dt><dd>${shown(k)}</dd></div>`)}</dl>
1401
- <p class="note">${tn("webSearchHint")}</p>
1402
- </section>` : null}
1480
+ ${has("readonly") ? html`<div class="s-effective-view"><p class="s-effective-notice"><${Icon} name="info" size=${18} />${layoutText("revEffectiveHint")}</p>
1481
+ <section class="card s-web-readonly"><h3 class="sr-only">${tn("webReadOnlyTitle")}</h3>
1482
+ <div class="s-ro-columns" aria-hidden="true"><span>${layoutText("effectiveKey")}</span><span>${layoutText("effectiveValue")}</span><span>${layoutText("effectiveSource")}</span></div>
1483
+ ${["web", "web_terminal"].map(group => html`<h4 class="s-ro-group">${group === "web" ? tn("sec_web-dashboard") : tn("webTerminalTitle")}</h4>
1484
+ <dl class="s-web-ro">${WEB_READ_ONLY.filter(k => k.startsWith(`${group}.`)).map((k) => html`<div key=${k}><dt><code>${k}</code></dt><dd class="s-ro-value">${shown(k)}</dd><dd class="s-ro-source" aria-label=${layoutText("effectiveSource")}><span class="tag">${layoutText(configured(k) ? "effectiveSourceConfig" : "effectiveSourceDefault")}</span></dd></div>`)}</dl>`)}
1485
+ </section><div class="s-effective-footnotes"><p class="note">${tn("webReadOnlyLead")}</p><p class="note">${tn("webSearchHint")}</p></div>
1486
+ <div class="s-effective-actions"><button type="button" class="btn" onClick=${() => revealSettingsTarget(document.getElementById("advanced-panel-editor"))}><${Icon} name="edit" size=${16} />${layoutText("revGoEditor")}</button></div>
1487
+ </div>` : null}
1403
1488
  </div>`;
1404
1489
  }
1405
-
1406
1490
  /**
1407
1491
  * Developer: the fleet model as YAML or JSON, to read, copy, download — or edit. #2347: an edit's Apply stages its
1408
1492
  * writes in the Save bar like every form's change (Save writes them, in order, then one apply; Discard drops them),
@@ -1456,14 +1540,15 @@ function Developer({ ctx }) {
1456
1540
  return html`<section class="card s-dev">
1457
1541
  <div class="s-dev-bar">
1458
1542
  <div class="seg-inline" role="group" aria-label=${tn("format")}>
1459
- ${["yaml", "json"].map((k) => html`<button key=${k} type="button" class=${`btn btn-sm${fmt === k ? " on" : ""}`} aria-pressed=${fmt === k ? "true" : "false"} onClick=${() => setFmt(k)}>${k.toUpperCase()}</button>`)}</div>
1460
- <button type="button" class="btn btn-sm" disabled=${blocked && !editing} title=${blocked && !editing ? tn("devFormsUnsaved") : undefined} onClick=${() => setEditing(!editing)}>${editing ? tn("viewMode") : tn("editMode")}</button>
1543
+ ${["yaml", "json"].map((k) => html`<button key=${k} type="button" class=${`btn btn-sm${fmt === k ? " on" : ""}`} aria-pressed=${fmt === k ? "true" : "false"} onClick=${() => setFmt(k)}>${k.toUpperCase()}</button>`)}</div><p class="s-dev-context"><${Icon} name="info" size=${15} />${layoutText("revEditorContext")}</p>
1544
+ <div class="s-dev-actions"><button type="button" class="btn btn-sm" disabled=${blocked && !editing} title=${blocked && !editing ? tn("devFormsUnsaved") : undefined} onClick=${() => setEditing(!editing)}>${editing ? tn("viewMode") : tn("editMode")}</button>
1461
1545
  <button type="button" class="btn btn-sm" title=${tn("copy")} onClick=${copy}><${Icon} name="copy" size=${14} /><span class="lbl">${tn("copy")}</span></button>
1462
- <button type="button" class="btn btn-sm" title=${tn("devDownload")} onClick=${download}><${Icon} name="download" size=${14} /><span class="lbl">${tn("devDownload")}</span></button>
1546
+ <button type="button" class="btn btn-sm" title=${tn("devDownload")} onClick=${download}><${Icon} name="download" size=${14} /><span class="lbl">${tn("devDownload")}</span></button></div>
1463
1547
  </div>
1464
1548
  ${editing ? html`<textarea class="s-yaml" aria-label=${tn("devEditor")} spellcheck=${false} wrap="off" value=${draft} onInput=${(e) => setDraft(e.target.value)}></textarea>
1465
1549
  <div class="save-row"><button type="button" class="btn btn-primary" disabled=${blocked} onClick=${save}>${tn("devApply")}</button><span class="note">${tn("devApplyHint")}</span></div>`
1466
- : html`<pre class="s-yaml">${text}</pre>`}
1550
+ : html`<pre class="s-yaml" tabindex="0">${text}</pre>`}
1551
+ <div class="s-dev-state"><span class="s-dev-file">fleet.yaml</span><${Icon} name=${editing ? "edit" : "view"} size=${14} /><span>${layoutText(editing ? "developerEditing" : "developerReadOnly")}</span></div>
1467
1552
  ${blocked ? html`<p class="note s-dev-blocked" role="status">${tn("devFormsUnsaved")}</p>`
1468
1553
  : held && !msg ? html`<p class="note s-dev-held" role="status">${tn("devStaged", heldCount)}</p>` : null}
1469
1554
  ${msg ? html`<p class=${`feedback${msg.error ? " error" : ""}`} role="status">${msg.error || msg.text}</p>` : null}
@@ -1480,12 +1565,13 @@ function PendingBar({ staged, busy, onApply, onDiscard, section }) {
1480
1565
  const invalid = changes.filter((c) => c.invalid).length;
1481
1566
  // #2382 review: a field to fix in another section is named, and the message goes there (its field focused).
1482
1567
  const elsewhere = changes.find((c) => c.invalid && c.section && c.section !== section);
1568
+ const problem = elsewhere || changes.find((c) => c.invalid && c.target);
1483
1569
  const count = changes.reduce((n, c) => n + (Number.isInteger(c.count) && c.count > 0 ? c.count : 1), 0);
1484
1570
  return html`<div data-trust="panel-controls" class="s-pending" role="region" aria-label=${tn("pendingRegion")}>
1485
1571
  <strong>${tn("changesPending", count)}</strong>
1486
1572
  <span class="note">${kinds.map(impactText).join(" · ")}</span>
1487
1573
  <span class="s-pending-actions">
1488
- ${!invalid ? null : elsewhere ? html`<button type="button" class="link-btn feedback error s-fix" role="status" onClick=${() => goToTarget(elsewhere.section, elsewhere.target)}>${tn("fixToSaveIn", invalid, tn(`sec_${elsewhere.section}`))}</button>`
1574
+ ${!invalid ? null : problem ? html`<button type="button" class="link-btn feedback error s-fix" role="status" onClick=${() => goToTarget(problem.section || section, problem.target)}>${elsewhere ? tn("fixToSaveIn", invalid, tn(`sec_${elsewhere.section}`)) : tn("fixToSave", invalid)}</button>`
1489
1575
  : html`<span class="feedback error s-fix" role="status">${tn("fixToSave", invalid)}</span>`}
1490
1576
  <button type="button" class="btn" onClick=${onDiscard} disabled=${busy}>${tn("discard")}</button>
1491
1577
  <button type="button" class="btn btn-primary" onClick=${onApply} disabled=${busy || invalid > 0} title=${busy ? tn("applyBusyLocal") : undefined}>${tn("applyChanges")}</button>
@@ -1592,12 +1678,11 @@ function PublicTunnelToken() {
1592
1678
  } catch { if (lease.current()) setMessage(tn("applyFailed")); }
1593
1679
  finally { inFlight.current = false; if (lease.current()) setBusy(false); }
1594
1680
  };
1595
- return html`<section class="card"><h3>${tn("namedToken")}</h3>
1681
+ return html`<section class="card s-tunnel-token"><div class="s-token-head"><h3>${tn("namedToken")}</h3><span class="tag">${present ? tn("keyConfigured") : tn("keyNotConfigured")}</span></div>
1596
1682
  <p class="note">${tn("namedTokenHint")}</p>
1597
- <span class="tag">${present ? tn("keyConfigured") : tn("keyNotConfigured")}</span>
1598
- <input id="publicTunnelToken" type="password" autocomplete="new-password" aria-label=${tn("namedToken")} value=${secret} onInput=${e => { setSecret(e.target.value); setTypedTunnelToken(e.target.value); }} />
1599
- <button type="button" class="btn btn-sm" disabled=${busy || !secret} onClick=${() => go(false)}>${tn("namedTokenSet")}</button>
1600
- <button type="button" class="btn btn-sm" disabled=${busy || !present} onClick=${() => go(true)}>${tn("namedTokenClear")}</button>
1683
+ <details class="s-token-editor"><summary>${layoutText(present ? "revUpdateCredential" : "revSetCredential")}</summary><div class="s-token-editor-body"><input class="s-token-input" id="publicTunnelToken" type="password" autocomplete="new-password" aria-label=${tn("namedToken")} value=${secret} onInput=${e => { setSecret(e.target.value); setTypedTunnelToken(e.target.value); }} />
1684
+ <div class="s-token-actions"><button type="button" class="btn btn-sm" disabled=${busy || !secret} onClick=${() => go(false)}>${tn("namedTokenSet")}</button>
1685
+ <button type="button" class="btn btn-sm" disabled=${busy || !present} onClick=${() => go(true)}>${tn("namedTokenClear")}</button></div></div></details>
1601
1686
  ${message ? html`<p class="note" role="status">${message}</p>` : null}
1602
1687
  </section>`;
1603
1688
  }