@songsid/agend 2.3.5-alpha.2 → 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 (279) hide show
  1. package/dist/agend-home.d.ts +13 -0
  2. package/dist/agend-home.js +42 -0
  3. package/dist/agend-home.js.map +1 -1
  4. package/dist/agent-runtime-api.d.ts +4 -0
  5. package/dist/agent-runtime-api.js +59 -0
  6. package/dist/agent-runtime-api.js.map +1 -0
  7. package/dist/agent-runtime-change.d.ts +74 -0
  8. package/dist/agent-runtime-change.js +102 -0
  9. package/dist/agent-runtime-change.js.map +1 -0
  10. package/dist/auth-api.d.ts +21 -0
  11. package/dist/auth-api.js +95 -4
  12. package/dist/auth-api.js.map +1 -1
  13. package/dist/authority/web-authority.d.ts +12 -2
  14. package/dist/authority/web-authority.js +25 -4
  15. package/dist/authority/web-authority.js.map +1 -1
  16. package/dist/backend/claude-code.js +16 -1
  17. package/dist/backend/claude-code.js.map +1 -1
  18. package/dist/backend/codex-footer-hints.d.ts +4 -0
  19. package/dist/backend/codex-footer-hints.js +27 -0
  20. package/dist/backend/codex-footer-hints.js.map +1 -0
  21. package/dist/backend/codex.js +19 -11
  22. package/dist/backend/codex.js.map +1 -1
  23. package/dist/backend/kiro-identity.d.ts +2 -0
  24. package/dist/backend/kiro-identity.js +13 -6
  25. package/dist/backend/kiro-identity.js.map +1 -1
  26. package/dist/backend/kiro.js +18 -3
  27. package/dist/backend/kiro.js.map +1 -1
  28. package/dist/backend-availability.d.ts +6 -0
  29. package/dist/backend-availability.js +13 -5
  30. package/dist/backend-availability.js.map +1 -1
  31. package/dist/channel/mcp-tools.js +1 -1
  32. package/dist/channel/mcp-tools.js.map +1 -1
  33. package/dist/cli.js +6 -9
  34. package/dist/cli.js.map +1 -1
  35. package/dist/config-validator.js +12 -1
  36. package/dist/config-validator.js.map +1 -1
  37. package/dist/daemon.d.ts +5 -0
  38. package/dist/daemon.js +107 -30
  39. package/dist/daemon.js.map +1 -1
  40. package/dist/fleet-manager.d.ts +146 -8
  41. package/dist/fleet-manager.js +1030 -166
  42. package/dist/fleet-manager.js.map +1 -1
  43. package/dist/fleet-windows-runtime.d.ts +6 -1
  44. package/dist/fleet-windows-runtime.js +77 -1
  45. package/dist/fleet-windows-runtime.js.map +1 -1
  46. package/dist/fleet-yaml-patch.d.ts +3 -0
  47. package/dist/fleet-yaml-patch.js +103 -0
  48. package/dist/fleet-yaml-patch.js.map +1 -0
  49. package/dist/general-binding.d.ts +85 -0
  50. package/dist/general-binding.js +298 -0
  51. package/dist/general-binding.js.map +1 -0
  52. package/dist/general-host-owner.d.ts +8 -0
  53. package/dist/general-host-owner.js +26 -0
  54. package/dist/general-host-owner.js.map +1 -0
  55. package/dist/general-instance.d.ts +7 -0
  56. package/dist/general-instance.js +20 -3
  57. package/dist/general-instance.js.map +1 -1
  58. package/dist/general-knowledge/skills/model-discovery/SKILL.md +2 -0
  59. package/dist/general-role.d.ts +5 -0
  60. package/dist/general-role.js +26 -0
  61. package/dist/general-role.js.map +1 -0
  62. package/dist/instance-lifecycle.d.ts +1 -0
  63. package/dist/instance-lifecycle.js +26 -4
  64. package/dist/instance-lifecycle.js.map +1 -1
  65. package/dist/instructions.d.ts +2 -0
  66. package/dist/instructions.js +6 -0
  67. package/dist/instructions.js.map +1 -1
  68. package/dist/kiro-resume-summary.d.ts +37 -0
  69. package/dist/kiro-resume-summary.js +85 -0
  70. package/dist/kiro-resume-summary.js.map +1 -0
  71. package/dist/locale.js +59 -0
  72. package/dist/locale.js.map +1 -1
  73. package/dist/login-controller.js +9 -1
  74. package/dist/login-controller.js.map +1 -1
  75. package/dist/native-pane-lifecycle.js +4 -1
  76. package/dist/native-pane-lifecycle.js.map +1 -1
  77. package/dist/native-pane-manager.d.ts +1 -0
  78. package/dist/native-pane-manager.js +23 -2
  79. package/dist/native-pane-manager.js.map +1 -1
  80. package/dist/needs-you-hub.js +1 -1
  81. package/dist/needs-you-hub.js.map +1 -1
  82. package/dist/needs-you.d.ts +8 -1
  83. package/dist/needs-you.js +5 -2
  84. package/dist/needs-you.js.map +1 -1
  85. package/dist/office-share-page.js +1 -1
  86. package/dist/office-share-page.js.map +1 -1
  87. package/dist/package-preimage.js +27 -2
  88. package/dist/package-preimage.js.map +1 -1
  89. package/dist/pane-port.d.ts +11 -0
  90. package/dist/public-web-gateway.js +9 -0
  91. package/dist/public-web-gateway.js.map +1 -1
  92. package/dist/public-web-link.d.ts +11 -0
  93. package/dist/public-web-link.js +13 -0
  94. package/dist/public-web-link.js.map +1 -1
  95. package/dist/quickstart-api.js +9 -5
  96. package/dist/quickstart-api.js.map +1 -1
  97. package/dist/quickstart.d.ts +1 -0
  98. package/dist/quickstart.js +4 -1
  99. package/dist/quickstart.js.map +1 -1
  100. package/dist/reply-buttons-controller.js +10 -0
  101. package/dist/reply-buttons-controller.js.map +1 -1
  102. package/dist/runtime-api.js +84 -9
  103. package/dist/runtime-api.js.map +1 -1
  104. package/dist/runtime-build-info.d.ts +9 -0
  105. package/dist/runtime-build-info.js +57 -0
  106. package/dist/runtime-build-info.js.map +1 -0
  107. package/dist/runtime-bundle.d.ts +3 -1
  108. package/dist/runtime-bundle.js +17 -8
  109. package/dist/runtime-bundle.js.map +1 -1
  110. package/dist/runtime-cli.d.ts +6 -1
  111. package/dist/runtime-cli.js +46 -11
  112. package/dist/runtime-cli.js.map +1 -1
  113. package/dist/runtime-client-store.d.ts +20 -1
  114. package/dist/runtime-client-store.js +56 -4
  115. package/dist/runtime-client-store.js.map +1 -1
  116. package/dist/runtime-client.d.ts +12 -0
  117. package/dist/runtime-client.js +43 -7
  118. package/dist/runtime-client.js.map +1 -1
  119. package/dist/runtime-connection.d.ts +22 -0
  120. package/dist/runtime-connection.js +93 -0
  121. package/dist/runtime-connection.js.map +1 -0
  122. package/dist/runtime-coordinator.d.ts +9 -0
  123. package/dist/runtime-coordinator.js +81 -34
  124. package/dist/runtime-coordinator.js.map +1 -1
  125. package/dist/runtime-general-protocol.d.ts +22 -0
  126. package/dist/runtime-general-protocol.js +57 -0
  127. package/dist/runtime-general-protocol.js.map +1 -0
  128. package/dist/runtime-general-workspace.d.ts +17 -0
  129. package/dist/runtime-general-workspace.js +213 -0
  130. package/dist/runtime-general-workspace.js.map +1 -0
  131. package/dist/runtime-pane-assets.d.ts +1 -1
  132. package/dist/runtime-pane-assets.js +1 -1
  133. package/dist/runtime-pane-assets.js.map +1 -1
  134. package/dist/runtime-pane-boot.d.ts +7 -0
  135. package/dist/runtime-pane-boot.js +23 -0
  136. package/dist/runtime-pane-boot.js.map +1 -1
  137. package/dist/runtime-pane-host.d.ts +1 -0
  138. package/dist/runtime-pane-host.js +22 -1
  139. package/dist/runtime-pane-host.js.map +1 -1
  140. package/dist/runtime-pane-mcp.d.ts +8 -0
  141. package/dist/runtime-pane-mcp.js +15 -2
  142. package/dist/runtime-pane-mcp.js.map +1 -1
  143. package/dist/runtime-pane-port.d.ts +6 -1
  144. package/dist/runtime-pane-port.js +21 -0
  145. package/dist/runtime-pane-port.js.map +1 -1
  146. package/dist/runtime-pane-protocol.d.ts +4 -1
  147. package/dist/runtime-pane-protocol.js +7 -1
  148. package/dist/runtime-pane-protocol.js.map +1 -1
  149. package/dist/runtime-protocol.d.ts +13 -0
  150. package/dist/runtime-protocol.js +18 -0
  151. package/dist/runtime-protocol.js.map +1 -1
  152. package/dist/settings-api.d.ts +5 -0
  153. package/dist/settings-api.js +37 -1
  154. package/dist/settings-api.js.map +1 -1
  155. package/dist/settings-baseline.d.ts +1 -0
  156. package/dist/settings-baseline.js +1 -1
  157. package/dist/settings-baseline.js.map +1 -1
  158. package/dist/settings-confirmation.d.ts +1 -0
  159. package/dist/settings-confirmation.js +1 -0
  160. package/dist/settings-confirmation.js.map +1 -1
  161. package/dist/settings-effect.d.ts +4 -0
  162. package/dist/settings-effect.js +36 -1
  163. package/dist/settings-effect.js.map +1 -1
  164. package/dist/settings-http-confirmation.d.ts +4 -0
  165. package/dist/settings-http-confirmation.js +20 -1
  166. package/dist/settings-http-confirmation.js.map +1 -1
  167. package/dist/settings-request-capability.d.ts +2 -0
  168. package/dist/settings-request-capability.js +4 -0
  169. package/dist/settings-request-capability.js.map +1 -1
  170. package/dist/settings-route-lifetime.d.ts +14 -0
  171. package/dist/settings-route-lifetime.js +48 -0
  172. package/dist/settings-route-lifetime.js.map +1 -0
  173. package/dist/settings-transaction.d.ts +4 -0
  174. package/dist/settings-transaction.js +9 -0
  175. package/dist/settings-transaction.js.map +1 -1
  176. package/dist/setup-form.js +6 -3
  177. package/dist/setup-form.js.map +1 -1
  178. package/dist/setup-i18n.js +3 -0
  179. package/dist/setup-i18n.js.map +1 -1
  180. package/dist/stall-reporter.d.ts +21 -0
  181. package/dist/stall-reporter.js +82 -0
  182. package/dist/stall-reporter.js.map +1 -0
  183. package/dist/tmux-control.d.ts +2 -2
  184. package/dist/tmux-control.js +4 -2
  185. package/dist/tmux-control.js.map +1 -1
  186. package/dist/tmux-exec-helper.d.ts +6 -3
  187. package/dist/tmux-exec-helper.js +115 -53
  188. package/dist/tmux-exec-helper.js.map +1 -1
  189. package/dist/tool-permissions.js +2 -1
  190. package/dist/tool-permissions.js.map +1 -1
  191. package/dist/tunnel/cloudflared-install.d.ts +4 -0
  192. package/dist/tunnel/cloudflared-install.js +20 -1
  193. package/dist/tunnel/cloudflared-install.js.map +1 -1
  194. package/dist/tunnel/cloudflared.d.ts +8 -1
  195. package/dist/tunnel/cloudflared.js +34 -6
  196. package/dist/tunnel/cloudflared.js.map +1 -1
  197. package/dist/tunnel/lease.d.ts +3 -3
  198. package/dist/tunnel/lease.js +23 -19
  199. package/dist/tunnel/lease.js.map +1 -1
  200. package/dist/tunnel/manager.js +6 -7
  201. package/dist/tunnel/manager.js.map +1 -1
  202. package/dist/types.d.ts +2 -0
  203. package/dist/ui/agent-proposals.js +2 -2
  204. package/dist/ui/agent-runtime-settings.js +67 -0
  205. package/dist/ui/agent-runtime.js +20 -2
  206. package/dist/ui/backend-signin.js +36 -21
  207. package/dist/ui/general-location.js +71 -0
  208. package/dist/ui/native-instance-settings.js +5 -8
  209. package/dist/ui/native-pane-recovery.js +1 -1
  210. package/dist/ui/panel-chat.js +5 -2
  211. package/dist/ui/panel-fleet.js +4 -2
  212. package/dist/ui/panel-needs.js +2 -0
  213. package/dist/ui/panel-runtimes.js +136 -33
  214. package/dist/ui/panel-settings.js +227 -138
  215. package/dist/ui/panel-windows-cli.js +77 -41
  216. package/dist/ui/settings-agent-filters.js +94 -0
  217. package/dist/ui/settings-appearance.js +122 -23
  218. package/dist/ui/settings-dialogs.js +13 -7
  219. package/dist/ui/settings-guests.js +25 -11
  220. package/dist/ui/settings-layout.js +245 -0
  221. package/dist/ui/settings-model.js +10 -1
  222. package/dist/ui/settings-outside.js +176 -0
  223. package/dist/ui/settings-registry.js +6 -3
  224. package/dist/ui/settings-shortcuts.js +65 -0
  225. package/dist/ui/settings-strings.js +95 -0
  226. package/dist/ui/settings-web-authority.js +14 -11
  227. package/dist/ui/shared/app-i18n.js +2 -2
  228. package/dist/ui/shared/app-shell.js +17 -7
  229. package/dist/ui/shared/app.css +1038 -13
  230. package/dist/ui/shared/instance-nav.js +16 -5
  231. package/dist/ui/shared/machine-name.js +13 -0
  232. package/dist/ui/shared/panel-view.js +266 -17
  233. package/dist/ui/shared/qrcode.module.js +2237 -0
  234. package/dist/ui/shared/settings-width.js +31 -0
  235. package/dist/ui/shared/signin.js +39 -5
  236. package/dist/ui/shared/tokens.css +1 -0
  237. package/dist/ui/shared/ui-dialog.js +2 -2
  238. package/dist/ui/shared/ui-menu.js +3 -3
  239. package/dist/ui/shared/vendor/qrcode-generator.LICENSE +27 -0
  240. package/dist/ui/shared/vendor/vendor.json +19 -2
  241. package/dist/ui/shared/view-strings.js +28 -0
  242. package/dist/ui/windows-cli-catalog.js +41 -12
  243. package/dist/unattended-install.d.ts +1 -1
  244. package/dist/unattended-install.js +19 -5
  245. package/dist/unattended-install.js.map +1 -1
  246. package/dist/update-check.d.ts +2 -8
  247. package/dist/update-check.js +2 -5
  248. package/dist/update-check.js.map +1 -1
  249. package/dist/view-api.d.ts +15 -0
  250. package/dist/view-api.js +94 -33
  251. package/dist/view-api.js.map +1 -1
  252. package/dist/web-api.d.ts +13 -0
  253. package/dist/web-api.js +80 -0
  254. package/dist/web-api.js.map +1 -1
  255. package/dist/web-login.d.ts +4 -3
  256. package/dist/web-login.js +23 -14
  257. package/dist/web-login.js.map +1 -1
  258. package/dist/web-only-fleet.d.ts +20 -0
  259. package/dist/web-only-fleet.js +21 -0
  260. package/dist/web-only-fleet.js.map +1 -1
  261. package/dist/web-remote-access.d.ts +152 -0
  262. package/dist/web-remote-access.js +368 -0
  263. package/dist/web-remote-access.js.map +1 -0
  264. package/dist/web-session.d.ts +12 -0
  265. package/dist/web-session.js +22 -8
  266. package/dist/web-session.js.map +1 -1
  267. package/dist/windows-agents.d.ts +15 -1
  268. package/dist/windows-agents.js +37 -10
  269. package/dist/windows-agents.js.map +1 -1
  270. package/dist/windows-cli-api.js +2 -1
  271. package/dist/windows-cli-api.js.map +1 -1
  272. package/dist/windows-cli-worker.d.ts +1 -0
  273. package/dist/windows-cli-worker.js +2 -2
  274. package/dist/windows-cli-worker.js.map +1 -1
  275. package/dist/windows-cli.d.ts +1 -0
  276. package/dist/windows-cli.js +7 -2
  277. package/dist/windows-cli.js.map +1 -1
  278. package/launcher/agend-install.cjs +16 -2
  279. package/package.json +1 -1
@@ -53,8 +53,16 @@ import { OfficeSharesCard } from "./office-share-dialog.js";
53
53
  import { PageSharesCard } from "./page-share-dialog.js";
54
54
  import { syncStage, useDraft } from "./settings-draft.js";
55
55
  import { WebAuthorityCard } from "./settings-web-authority.js";
56
+ import { OutsideCard } from "./settings-outside.js";
56
57
  import { NamedLinkTest, setTypedTunnelToken } from "./settings-named-test.js";
57
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";
65
+ import { GeneralLocation } from "./general-location.js";
58
66
 
59
67
  const tn = (k, ...v) => t(`settings.${k}`, ...v);
60
68
  /** What a refused change says to a person: the server's code in words (the code itself only for one not listed). */
@@ -105,7 +113,7 @@ async function loadLive(lease) {
105
113
  if (!lease.current()) return null;
106
114
  const live = {}, classicLive = {};
107
115
  if (f.ok && f.body && Array.isArray(f.body.instances)) for (const i of f.body.instances) {
108
- 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 };
109
117
  if (i.classic) classicLive[i.name] = { status: i.status, state: i.state };
110
118
  }
111
119
  // /api/fleet carries live state for ClassicBot rooms; the profiles are the fallback for one not listed there yet.
@@ -379,20 +387,23 @@ export function SettingsPanel({ route, navKey }) {
379
387
  const formKey = data && !data.error ? `${data.loadedAt}:${draftEpoch}` : "";
380
388
  // #2347 regrouping: each card where the person looks for it (the IA in #2347 §3); a form split across sections (the
381
389
  // defaults, Web) is one form with one draft, shown a part at a time.
382
- const sectionBody = section === "preferences" ? html`<${DeviceSection} key=${you.key} /><${AppearanceSection} ...${you} part="device" />`
390
+ const sectionBody = section === "preferences" ? html`<${DeviceSection} key=${you.key} />`
383
391
  : section === "look" ? html`<${AppearanceSection} ...${you} part="look" />`
384
392
  : section === "runtimes" ? html`<${RuntimesPanel} navKey=${navKey} />`
385
393
  : !data ? html`<${Skeleton} lines=${6} />`
386
394
  : data.error ? html`<${ErrorState} message=${tn("configLoadFailed")} onRetry=${reload} />`
387
- : section === "agents" ? html`<${YamlLock} staged=${staged}><${Agents} ctx=${ctx} search=${search} openDialog=${toDialog} /></${YamlLock}>${ctx.providerOk ? html`<${ProviderKeys} ctx=${ctx} />` : null}`
388
- : section === "agent-defaults" ? html`<${YamlLock} staged=${staged}><${General} key=${formKey} ctx=${ctx} part="fleet" /></${YamlLock}>`
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>`
399
+ : section === "agent-defaults" ? html`<${GeneralLocation} /><${YamlLock} staged=${staged}><${General} key=${formKey} ctx=${ctx} part="fleet" /></${YamlLock}>`
389
400
  : section === "chat" ? html`<${YamlLock} staged=${staged}><${Bots} ctx=${ctx} search=${search} openDialog=${toDialog} /></${YamlLock}>
390
401
  <${WebSection} key=${formKey} ctx=${ctx} staged=${staged} part="chat" />
391
- <div id="classicbot" class="s-anchor"><h2 class="s-ap-group">ClassicBot</h2><${Classic} ctx=${ctx} search=${search} openDialog=${toDialog} />
392
- <${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>`
393
404
  : section === "web-dashboard" ? html`<${WebSection} key=${formKey} ctx=${ctx} staged=${staged} part="dashboard" /><${AppearanceSection} ...${you} part="dashboard" />`
394
405
  : section === "access" ? html`<${WebSection} key=${formKey} ctx=${ctx} staged=${staged} part="access" />`
395
- : 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} />`;
396
407
  // #2347 §6: the setting a search result opened — brought into view once its section is drawn (its data may still be
397
408
  // on its way), focused, and flashed for 2 s.
398
409
  const wanted = useRef(null);
@@ -411,6 +422,7 @@ export function SettingsPanel({ route, navKey }) {
411
422
  if (++tries < 40) timer = setTimeout(find, 50); else wanted.current = null;
412
423
  return;
413
424
  }
425
+ if (revealSettingsTarget(el)) { timer = setTimeout(find, 0); return; }
414
426
  wanted.current = null;
415
427
  // #2347 §10: a setting in a closed drawer (Advanced) is shown — every drawer around it opens first.
416
428
  for (let d = el.closest && el.closest("details"); d; d = d.parentElement && d.parentElement.closest("details")) d.open = true;
@@ -451,7 +463,14 @@ export function SettingsPanel({ route, navKey }) {
451
463
  if (!id) return;
452
464
  let el = null;
453
465
  try { el = document.getElementById(decodeURIComponent(id)); } catch { el = null; }
454
- 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();
455
474
  }, [section, navKey, !!data]);
456
475
  // A dialog belongs to the navigation that opened it (its draft names one object, never another).
457
476
  const open = dialog && dialog.key === navKey ? dialog : null;
@@ -470,14 +489,15 @@ export function SettingsPanel({ route, navKey }) {
470
489
  <span class="s-nav-head" id=${`s-grp-${g.id}`}>${tn(`grp_${g.id}`)}</span>
471
490
  ${g.sections.map((k) => html`<a key=${k} href=${sectionPath(k)} class=${`seg-item${k === section ? " active" : ""}`}
472
491
  aria-current=${k === section ? "page" : undefined}><${Icon} name=${ICONS[k]} size=${16} /><span>${tn(`sec_${k}`)}</span></a>`)}</div>`)}</div></nav>
473
- <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}>
474
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}
475
495
  ${searchable ? html`<div class="s-toolbar"><label class="s-search"><${Icon} name="search" size=${14} /><span class="sr-only">${tn("searchAll")}</span>
476
496
  <input type="search" value=${search} placeholder=${tn("searchAll")} onInput=${(e) => setSearch(e.target.value)} /></label></div>` : null}
477
497
  ${op ? html`<${OperationCard} op=${op} schema=${data && data.schema} />` : null}
478
498
  <${PendingChanges} inline=${true} />
479
499
  ${ctx && (section === "agents" || section === "chat") ? html`<${FirstRunCard} onConnect=${openWizard} />` : null}
480
- ${sectionBody}
500
+ <div class="s-section-layout" data-section=${section}>${sectionBody}</div>
481
501
  </div></div>
482
502
  ${staged.size ? html`<${PendingBar} staged=${staged} busy=${opBusy(op)} onApply=${apply} onDiscard=${discard} section=${section} />` : null}
483
503
  ${open && ctx ? renderDialog(open, ctx, close) : null}
@@ -485,6 +505,14 @@ export function SettingsPanel({ route, navKey }) {
485
505
  </div>`;
486
506
  }
487
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
+
488
516
  /**
489
517
  * #1604, #1665: Preferences. Synced across the person's devices (kept on the host, app-prefs.js): theme (theme.js),
490
518
  * language (app-i18n.js) and HTML previews (preview.js, through the app's confirm). On the public link they are
@@ -606,17 +634,23 @@ export function DeviceSection() {
606
634
  setTimeout(go, 0);
607
635
  };
608
636
  const publicLink = typeof document !== "undefined" && document.body && document.body.dataset.publicLink === "1";
609
- return html`<${ProfileCard} />
610
- <section class="card s-device s-synced"><h3>${tn("syncedTitle")}</h3>
611
- <p class="sub">${publicLink ? tn("syncedPublicLink") : tn("syncedLead")}</p>
612
- <${DevicePrefs} labels=${true} />
613
- ${P() ? html`<label class="check"><input id="pref-previews" type="checkbox" checked=${on} onChange=${(e) => { const want = e.target.checked; setPreviewOptIn(want).then(setOn); }} />
614
- <span>${t("chat.previews")}</span></label>` : null}
615
- </section>
616
- <section class="card s-device s-local"><h3>${tn("deviceTitle")}</h3>
617
- <p class="sub">${tn("deviceLead")}</p>
618
- <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>
619
- </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>`;
620
654
  }
621
655
 
622
656
  /**
@@ -626,7 +660,7 @@ export function DeviceSection() {
626
660
  * refuses one; this only shows what it says.
627
661
  */
628
662
  const view = (b) => ({ phase: "ready", name: b.name || "", shown: b.shown || "", id: b.id || "", bound: b.bound === true, platform: b.platform_name || "", error: "" });
629
- export function ProfileCard() {
663
+ export function ProfileCard({ embedded = false } = {}) {
630
664
  const [state, setState] = useState({ phase: "loading", name: "", shown: "", id: "", bound: false, platform: "", error: "" });
631
665
  const [draft, setDraft] = useState("");
632
666
  const lease = useLease();
@@ -653,16 +687,22 @@ export function ProfileCard() {
653
687
  };
654
688
  const busy = state.phase !== "ready";
655
689
  const changed = draft.trim() !== state.name;
656
- return html`<section class="card s-device s-profile"><h3>${tn("profileTitle")}</h3>
657
- <p class="sub">${state.bound ? tn("profileLeadBound") : tn("profileLeadUnbound")}</p>
658
- <div class="field"><label for="profile-name">${tn("profileName")}</label>
659
- <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}
660
695
  placeholder=${state.platform || "web-user"} aria-invalid=${state.error ? "true" : "false"} aria-describedby="profile-help"
661
696
  onInput=${(e) => setDraft(e.target.value)} onKeyDown=${(e) => { if (e.key === "Enter" && changed && draft.trim()) save(false); }} />
662
- <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>
663
- <div class="s-device-row"><button type="button" class="btn primary" disabled=${busy || !changed || !draft.trim()} onClick=${() => save(false)}>${tn("profileSave")}</button>
664
- ${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>
665
- </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>`;
666
706
  }
667
707
 
668
708
  /** The operation in hand still owns Apply: its writes and job, or a restart of AgEnD from the press to its end. */
@@ -759,60 +799,67 @@ function agentChannelLabel(inst, chs) {
759
799
  return idx >= 0 ? `${chs[idx].type} (${chLabel(idx)})` : inst.channel_id;
760
800
  }
761
801
 
762
- function Agents({ ctx, search, openDialog }) {
802
+ function Agents({ ctx, search, clearSearch, openDialog }) {
763
803
  const insts = ctx.fleet.instances || {};
764
804
  const [collapsed, setCollapsed] = useState(() => new Set());
765
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;
766
812
  const chs = channelsOf(ctx.fleet);
767
813
  // #2172: the sidebar's grouping — an agent in a team under its team, else under its first tag.
768
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));
769
- 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());
770
817
  // One action per agent at a time: the whole row is off (Settings and ⋯ too), and the pressed button says it is working.
771
818
  const run = async (name, action, work) => {
772
819
  if (busy.has(name)) return;
773
820
  setBusy((b) => new Map(b).set(name, action));
774
821
  try { await work(); } finally { setBusy((b) => { const n = new Map(b); n.delete(name); return n; }); }
775
822
  };
776
- const head = html`<div class="list-head"><span>${Object.keys(insts).length === 1 ? tn("countAgent") : tn("countAgents", Object.keys(insts).length)}</span>
777
- <button type="button" id="newAgentBtn" class="btn btn-primary" onClick=${() => openDialog({ kind: "create" })}><${Icon} name="plus" size=${16} />${tn("newAgent")}</button></div>`;
778
- // #2233: after the agents, each login on this machine with Sign in (backend-signin.js).
779
- const signIns = html`<${SignInList} />`;
780
- if (!Object.keys(insts).length) return html`${head}<${Empty} icon="bot" title=${tn("noAgents")} />${signIns}`;
781
- if (!groups.length) return html`${head}<${Empty} icon="search" title=${tn("noAgentMatch", search.trim())} />${signIns}`;
782
- return html`${head}<div class="s-list">${groups.map(({ group, names }) => {
783
- 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
784
831
  return html`<section key=${group} class="s-group">
785
832
  <button type="button" class="s-group-head" aria-expanded=${folded ? "false" : "true"}
786
833
  onClick=${() => setCollapsed((c) => { const n = new Set(c); n.has(group) ? n.delete(group) : n.add(group); return n; })}>
787
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>
788
835
  ${folded ? null : names.map((name) => {
789
836
  const inst = insts[name];
790
- const st = (ctx.live[name] && ctx.live[name].status) || "stopped";
837
+ const st = agentStatus(ctx.live[name]);
791
838
  const sum = effectiveSummary(ctx.live[name] || {}, inst, ctx.fleet.defaults || {});
792
839
  const ch = agentChannelLabel(inst, chs);
793
840
  const working = busy.has(name), doing = busy.get(name);
794
841
  const label = (action, text) => (doing === action ? tn("working") : text);
795
- return html`<div key=${name} class="s-row">
796
- <span class=${dotClass(st)} title=${tn(STATUS_KEY[st] || "stStopped")} aria-hidden="true"></span>
797
- <span class="s-name" title=${name}>${shortName(name, inst)}</span>
798
- ${inst.runtime === "windows" ? html`<span class="tag">Windows</span>` : null}
799
- <span class="s-meta" title=${sum.backend}>${sum.model}</span>
800
- <span class="s-br" aria-hidden="true"></span>
801
- ${inst.runtime !== "windows" || ctx.live[name]?.capabilities?.effort === true ? html`<span class="tag">${tn("effortTag", sum.effort)}</span>` : null}
802
- ${ch ? html`<span class=${`tag${ch.includes("persona") ? " persona" : ""}`}>${ch}</span>` : null}
803
- <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>
804
851
  <span class="s-actions">
805
852
  ${inst.runtime === "windows" ? html`<a class="btn btn-sm" href=${chatPath(name)}>${t("app.chat")}</a>` : null}
806
853
  <button type="button" class="btn btn-sm" disabled=${working} onClick=${() => openDialog({ kind: "agent", name, inst })}>${tn("settingsButton")}</button>
807
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}
808
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}
809
- ${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}
810
- <${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) }]} />
811
858
  </span>
812
859
  </div>`;
813
860
  })}
814
861
  </section>`;
815
- })}</div>${signIns}`;
862
+ })}</div>`;
816
863
  }
817
864
 
818
865
  function ConnectionRetry({ id, ctx }) {
@@ -842,10 +889,10 @@ function Bots({ ctx, search, openDialog }) {
842
889
  const all = channelsOf(ctx.fleet);
843
890
  const needle = search.trim().toLowerCase();
844
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)));
845
- 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>
846
893
  <button type="button" id="newBotBtn" class="btn btn-primary" onClick=${() => openDialog({ kind: "newBot" })}><${Icon} name="plus" size=${16} />${tn("newBot")}</button></div>`;
847
- return html`${head}
848
- ${!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 }) => {
849
896
  const users = (ch.access && ch.access.allowed_users) || [];
850
897
  const meta = ctx.connections.find((c, j) => channelId(c, j) === channelId(ch, i)) || {};
851
898
  const token = meta.token_present;
@@ -870,7 +917,7 @@ function Bots({ ctx, search, openDialog }) {
870
917
  ${!overPublicLink && ctx.fleetUp && meta.status !== "connected" ? html`<${ConnectionRetry} id=${channelId(ch, i)} ctx=${ctx} />` : null}
871
918
  <button type="button" class="btn btn-sm" onClick=${() => openDialog({ kind: "bot", id: channelId(ch, i) })}>${tn("settingsButton")}</button></span>
872
919
  </div>`;
873
- })}</div>`}`;
920
+ })}</div>`}</section>`;
874
921
  }
875
922
 
876
923
  /**
@@ -920,40 +967,40 @@ function ProviderKeys({ ctx }) {
920
967
  } catch { say(spec.id, { error: tn("failed") }); }
921
968
  finally { inFlight.current.delete(spec.id); if (lease.current()) setBusy(new Set(inFlight.current)); }
922
969
  };
923
- return html`<section class="card s-provider-keys"><h3>${tn("providerKeys")}</h3>
924
- ${!ctx.providerSecrets.length ? html`<p class="note">${tn("noProviderVerifiers")}</p>` : ctx.providerSecrets.map((spec) => html`<div key=${spec.id} class="s-key">
925
- <div class="s-key-row"><strong>${spec.display_name}</strong><span class="tag">${spec.token_present ? tn("keyConfigured") : tn("keyNotConfigured")}</span>
926
- ${spec.verifier === "available" ? html`<input type="password" autocomplete="new-password" aria-label=${tn("providerKeyInput", spec.display_name)}
927
- placeholder=${spec.token_present ? tn("keyConfigured") : tn("keyNotConfigured")} value=${keys[spec.id] || ""} onInput=${(e) => setKeys((k) => ({ ...k, [spec.id]: e.target.value }))} />
928
- <button type="button" class="btn btn-sm btn-primary" disabled=${busy.has(spec.id)} onClick=${() => go(spec)}>${tn("verifyApply")}</button>`
929
- : html`<span class="tag">${tn("unsupportedVerifier")}</span>`}</div>
930
- ${spec.verifier === "available" ? null : html`<p class="note">${tn("unsupportedVerifierHint")}</p>`}
931
- ${notes[spec.id] ? html`<p class=${`feedback${notes[spec.id].error ? " error" : ""}`} role="status">${notes[spec.id].error || notes[spec.id].text}</p>` : null}
932
- </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>
933
983
  </section>`;
934
984
  }
935
985
 
936
986
  function Classic({ ctx, search, openDialog }) {
937
987
  const rooms = Object.entries((ctx.classic && ctx.classic.channels) || {}).map(([key, ch]) => ({ key, ...ch }));
938
- 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>`;
939
989
  const needle = search.trim().toLowerCase();
940
990
  const list = rooms.filter((c) => !needle || String(c.name || "").toLowerCase().includes(needle) || String(c.instanceName || "").toLowerCase().includes(needle))
941
991
  .sort((a, b) => ((STATUS_ORDER[ctx.classicRuntime(a.instanceName).status] ?? 9) - (STATUS_ORDER[ctx.classicRuntime(b.instanceName).status] ?? 9))
942
992
  || String(a.name || "").localeCompare(String(b.name || "")));
943
- if (!list.length) return html`<${Empty} icon="search" title=${tn("noAgentMatch", search.trim())} />`;
944
- 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) => {
945
995
  const rt = ctx.classicRuntime(c.instanceName);
946
996
  const sum = effectiveSummary(ctx.live[c.instanceName] || {}, c, ctx.fleet.defaults || {}, (ctx.classic && ctx.classic.defaults) || {});
947
997
  const adapter = c.adapterId || "discord";
948
- return html`<div key=${c.key} class="s-row">
949
- <span class=${dotClass(rt.execution)} title=${tn(`exec_${rt.execution}`)} aria-hidden="true"></span>
950
- <span class="s-name" title=${c.instanceName || ""}>${shortName(c.name || c.instanceName || "?", { display_name: c.display_name })}</span>
951
- <span class="s-meta" title=${sum.backend}>${sum.model}</span>
952
- <span class="s-br" aria-hidden="true"></span>
953
- <span class="tag">${tn("effortTag", sum.effort)}</span>
954
- <span class=${`tag${adapter !== "discord" ? " persona" : ""}`}>${adapter}</span>
955
- ${c.collab ? html`<span class="tag">collab</span>` : null}
956
- <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>
957
1004
  <span class="s-actions">
958
1005
  <button type="button" class="btn btn-sm" onClick=${() => openDialog({ kind: "classic", room: c })}>${tn("settingsButton")}</button>
959
1006
  ${rt.status === "running" ? html`<button type="button" class="btn btn-sm" onClick=${() => ctx.pauseWake(c.instanceName, "pause")}>${tn("pause")}</button>` : null}
@@ -1089,30 +1136,43 @@ function General({ ctx, part = "all" }) {
1089
1136
  const classicReply = replyGuardSupported(f.classicBackend, f.agent_mode, d.kiro_ui) ? tn("replyGuardSupported") : tn("replyGuardUnsupported", f.classicBackend, f.agent_mode);
1090
1137
  // #2347: Agent defaults show the fleet's part; Chat & connections ClassicBot's. One form, one draft either way.
1091
1138
  const fleetPart = part !== "classic", classicPart = part !== "fleet";
1092
- return html`<div class=${`form s-general s-part-${part}`}>
1093
- ${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>
1094
1141
  <div class="grid2">
1095
- <div class=${fc("field", "locale")}>${L(tn("language"), "defaults.locale", "g-locale", "locale")}<${Select} id="g-locale" value=${f.locale} onChange=${set("locale")}
1096
- options=${localeOptions(f.locale).map((o) => (o.auto ? { value: "", label: tn("localeAuto") } : o))} /></div>
1097
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>
1098
1143
  <div class=${fc("field", "model")}>${L(tn("defaultModel"), "defaults.model", "g-model", "model")}<${ModelSelect} id="g-model" value=${f.model} onChange=${set("model")}
1099
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">
1100
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>
1101
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">
1102
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} />
1103
1159
  <p class="note">${tn("crossInstanceVisibilityHint")}</p></div>
1104
- <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>
1105
- <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>
1106
- <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>
1107
- <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>
1108
1166
  </div>
1167
+ </section>
1168
+ <details class="s-ui-disclosure s-defaults-permissions"><summary>${tn("uiDefaultsCapabilitiesTitle")}</summary><div class="s-ui-disclosure-body">
1109
1169
  <fieldset id="g-dash" class=${fc("field dash-perms", ...DASHBOARD_FEATURES.map((k) => `dash_${k}`))}><legend>${L(tn("dashboardAccess"), "defaults.dashboard")}</legend>
1110
1170
  <p class="note">${tn("dashboardDefaultsHint")}</p>
1111
1171
  ${DASHBOARD_FEATURES.map((k) => html`<label key=${k} class="check"><input type="checkbox" checked=${f[`dash_${k}`]}
1112
1172
  onChange=${(e) => set(`dash_${k}`)(e.target.checked)} /><span>${tn(`dashboard_${k}`)}</span></label>`)}
1113
1173
  </fieldset>
1114
- </section>` : null}
1115
- ${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>
1116
1176
  <div class="grid2">
1117
1177
  <div class=${fc("field", "classicBackend")}>${L(tn("classicBackend"), "classic_defaults.backend", "g-cbe")}<${BackendSelect} id="g-cbe" value=${f.classicBackend} onChange=${set("classicBackend")} />
1118
1178
  ${!c.backend && f.classicBackend === base.classicBackend ? html`<${Inherited} text=${valueText(f.classicBackend, "backend")} builtin=${!d.backend} from=${inAgentDefaults("#g-be")} />` : null}</div>
@@ -1133,12 +1193,12 @@ function General({ ctx, part = "all" }) {
1133
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>
1134
1194
  </div>
1135
1195
  </section>` : null}
1136
- ${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>
1137
1197
  <p class="note">${tn("projectRootsHint")}</p>
1138
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")} />
1139
1199
  ${f.roots.length ? null : html`<${InEffect} text=${tn("projectRootsAnywhere")} />`}</div>
1140
- </section>` : null}
1141
- ${fleetPart ? html`<${Drawer} title=${tn("advancedSection")}>
1200
+ </section></div></details>` : null}
1201
+ ${fleetPart ? html`<${Drawer} title=${tn("uiDefaultsAdvancedTitle")}>
1142
1202
  <section class="card"><h3>${tn("runtimeResources")}</h3><div class="grid2">
1143
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")}
1144
1204
  options=${[{ value: "", label: tn("toolSetUnset", toolSetDefault({}, {}), toolSetDefault({}, { general_topic: true })) }, ...TOOL_SETS]} /></div>
@@ -1157,7 +1217,7 @@ function General({ ctx, part = "all" }) {
1157
1217
  <section class="card"><h3>${tn("accessTitle")}</h3>
1158
1218
  <p class="note">${primary ? tn("accessMovedHint", primary.id || chLabel(0)) : tn("noBots")}</p></section>
1159
1219
  </${Drawer}>` : null}
1160
- </div>`;
1220
+ </div></div>`;
1161
1221
  }
1162
1222
 
1163
1223
  /**
@@ -1197,6 +1257,7 @@ export function uploadMbInEffect(web) {
1197
1257
  }
1198
1258
  function WebSection({ ctx, staged, part = "all" }) {
1199
1259
  const { schema } = ctx;
1260
+ const [accessTab, setAccessTab] = useSettingsTabs("access", "login");
1200
1261
  const web = ctx.fleet.web || {}, local = (web.session && web.session.local) || {}, term = ctx.fleet.web_terminal || {};
1201
1262
  // Over the public link the fields read only; the Guests and Web authority cards are not offered (the server never
1202
1263
  // serves their reads there).
@@ -1327,17 +1388,31 @@ function WebSection({ ctx, staged, part = "all" }) {
1327
1388
  if (v === undefined) return html`${roText(webReadOnlyDefault(path, ctx.fleet))} <${DefaultMark} />${capped ? html` <span class="tag">${tn("cappedByTotal")}</span>` : null}`;
1328
1389
  return applies === v ? roText(v) : html`${roText(v)} <span class="tag">${tn("inEffectShort", roText(applies))}</span>`;
1329
1390
  };
1330
- // #2347: one form, one draft, shown where each part belongs — Access & security (sessions, the browser terminal, the
1331
- // public link and its fixed address, guests, web authority), Chat & connections (web echo), Web dashboard (the Log tab,
1332
- // 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.
1333
1393
  const has = (p) => part === "all" || part === p;
1334
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; };
1335
1408
  return html`<div class=${`form s-web s-part-${part}`}>
1336
- ${has("access") ? html`<p class="sub">${tn("webLead")}</p>` : null}
1337
- ${overPublicLink && part !== "readonly" ? html`<p class="note" role="status" id="webPublicRefused">${tn("webPublicRefused")}</p>` : null}
1338
- ${has("access") ? lock(html`<section class="card"><h3>${tn("webSessionTitle")}</h3>
1339
- <p class="note">${tn("webSessionHint")}</p>
1340
- <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>
1341
1416
  <div class="grid2">
1342
1417
  <div class=${fc("field", "abs")}><label for="webSessionAbsolute">${tn("sessionAbsolute")}</label><input id="webSessionAbsolute" type="number" min="1" max="720" step="any"
1343
1418
  class=${errors.abs ? "invalid" : ""} value=${f.abs} onInput=${(e) => set("abs")(e.target.value)} /><${Feedback} error=${errors.abs} />
@@ -1346,8 +1421,13 @@ function WebSection({ ctx, staged, part = "all" }) {
1346
1421
  class=${errors.idle ? "invalid" : ""} value=${f.idle} onInput=${(e) => set("idle")(e.target.value)} /><${Feedback} error=${errors.idle} />
1347
1422
  ${value(f.idle) === null && !errors.abs ? html`<${InEffect} text=${tn("hoursValue", idle)} why=${idle < 48 ? tn("cappedBySession") : null} />` : null}</div>
1348
1423
  </div>
1349
- </section>
1350
- <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>
1351
1431
  <p class="note">${tn("webTerminalHint")}</p>
1352
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>
1353
1433
  <div class="grid2">
@@ -1355,8 +1435,18 @@ function WebSection({ ctx, staged, part = "all" }) {
1355
1435
  class=${errors.termTtl ? "invalid" : ""} value=${f.termTtl} onInput=${(e) => set("termTtl")(e.target.value)} /><${Feedback} error=${errors.termTtl} />
1356
1436
  ${value(f.termTtl) === null ? html`<${InEffect} text=${tn("minutesValue", 10)} />` : null}</div>
1357
1437
  </div>
1358
- </section>
1359
- <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>
1360
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>
1361
1451
  <p class="note">${tn("publicLinkHint")}</p>
1362
1452
  <div class="grid2">
@@ -1372,33 +1462,31 @@ function WebSection({ ctx, staged, part = "all" }) {
1372
1462
  ${named ? html`<p class="note" id="publicTtlNamedHint">${tn("publicTtlNamedHint")}</p>` : null}
1373
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>
1374
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")}
1375
1473
  </section>`) : null}
1376
- ${has("access") && !overPublicLink ? html`<${PublicTunnelToken} />` : null}
1377
- ${has("access") ? html`<${OfficeSharesCard} />` : null}
1378
- ${has("access") ? html`<${PageSharesCard} />` : null}
1379
- ${has("access") && !overPublicLink ? html`<${GuestsCard} ctx=${ctx} staged=${staged} /><${WebAuthorityCard} ctx=${ctx} staged=${staged} />` : null}
1380
- ${has("chat") ? lock(html`<section class="card"><h3>${tn("webChat")}</h3>
1381
- <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>
1382
- <p class="note">${tn("webEchoHint")}</p>
1383
- </section>`) : null}
1384
- ${has("dashboard") ? lock(html`<section class="card"><h3>${tn("webDashboardTitle")}</h3>
1385
- <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>
1386
- <p class="note">${tn("webLogTabHint")}</p>
1387
- <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>
1388
- <p class="note">${tn("webUsagePublicHint")}</p>
1389
- <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>
1390
- <p class="note">${tn("webOfficeLevelsHint")}</p>
1391
- <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>
1392
- <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")}
1393
1479
  </section>`) : null}
1394
- ${has("readonly") ? html`<section class="card s-web-readonly"><h3>${tn("webReadOnlyTitle")}</h3>
1395
- <p class="note">${tn("webReadOnlyLead")}</p>
1396
- <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>
1397
- <p class="note">${tn("webSearchHint")}</p>
1398
- </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}
1399
1488
  </div>`;
1400
1489
  }
1401
-
1402
1490
  /**
1403
1491
  * Developer: the fleet model as YAML or JSON, to read, copy, download — or edit. #2347: an edit's Apply stages its
1404
1492
  * writes in the Save bar like every form's change (Save writes them, in order, then one apply; Discard drops them),
@@ -1452,14 +1540,15 @@ function Developer({ ctx }) {
1452
1540
  return html`<section class="card s-dev">
1453
1541
  <div class="s-dev-bar">
1454
1542
  <div class="seg-inline" role="group" aria-label=${tn("format")}>
1455
- ${["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>
1456
- <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>
1457
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>
1458
- <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>
1459
1547
  </div>
1460
1548
  ${editing ? html`<textarea class="s-yaml" aria-label=${tn("devEditor")} spellcheck=${false} wrap="off" value=${draft} onInput=${(e) => setDraft(e.target.value)}></textarea>
1461
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>`
1462
- : 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>
1463
1552
  ${blocked ? html`<p class="note s-dev-blocked" role="status">${tn("devFormsUnsaved")}</p>`
1464
1553
  : held && !msg ? html`<p class="note s-dev-held" role="status">${tn("devStaged", heldCount)}</p>` : null}
1465
1554
  ${msg ? html`<p class=${`feedback${msg.error ? " error" : ""}`} role="status">${msg.error || msg.text}</p>` : null}
@@ -1476,12 +1565,13 @@ function PendingBar({ staged, busy, onApply, onDiscard, section }) {
1476
1565
  const invalid = changes.filter((c) => c.invalid).length;
1477
1566
  // #2382 review: a field to fix in another section is named, and the message goes there (its field focused).
1478
1567
  const elsewhere = changes.find((c) => c.invalid && c.section && c.section !== section);
1568
+ const problem = elsewhere || changes.find((c) => c.invalid && c.target);
1479
1569
  const count = changes.reduce((n, c) => n + (Number.isInteger(c.count) && c.count > 0 ? c.count : 1), 0);
1480
1570
  return html`<div data-trust="panel-controls" class="s-pending" role="region" aria-label=${tn("pendingRegion")}>
1481
1571
  <strong>${tn("changesPending", count)}</strong>
1482
1572
  <span class="note">${kinds.map(impactText).join(" · ")}</span>
1483
1573
  <span class="s-pending-actions">
1484
- ${!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>`
1485
1575
  : html`<span class="feedback error s-fix" role="status">${tn("fixToSave", invalid)}</span>`}
1486
1576
  <button type="button" class="btn" onClick=${onDiscard} disabled=${busy}>${tn("discard")}</button>
1487
1577
  <button type="button" class="btn btn-primary" onClick=${onApply} disabled=${busy || invalid > 0} title=${busy ? tn("applyBusyLocal") : undefined}>${tn("applyChanges")}</button>
@@ -1588,12 +1678,11 @@ function PublicTunnelToken() {
1588
1678
  } catch { if (lease.current()) setMessage(tn("applyFailed")); }
1589
1679
  finally { inFlight.current = false; if (lease.current()) setBusy(false); }
1590
1680
  };
1591
- 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>
1592
1682
  <p class="note">${tn("namedTokenHint")}</p>
1593
- <span class="tag">${present ? tn("keyConfigured") : tn("keyNotConfigured")}</span>
1594
- <input id="publicTunnelToken" type="password" autocomplete="new-password" aria-label=${tn("namedToken")} value=${secret} onInput=${e => { setSecret(e.target.value); setTypedTunnelToken(e.target.value); }} />
1595
- <button type="button" class="btn btn-sm" disabled=${busy || !secret} onClick=${() => go(false)}>${tn("namedTokenSet")}</button>
1596
- <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>
1597
1686
  ${message ? html`<p class="note" role="status">${message}</p>` : null}
1598
1687
  </section>`;
1599
1688
  }