@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
@@ -17,6 +17,7 @@ import { startOperation } from "./settings-apply.js";
17
17
  import { goToTarget } from "./settings-registry.js";
18
18
  import { sectionPath } from "/assets/app-route.js";
19
19
  import { backendLabel } from "/assets/instance-nav.js";
20
+ import {AgentRuntimeSettings} from "./agent-runtime-settings.js";
20
21
  import "./settings-strings.js"; // its words, wherever a picker of this file is used (#2072: Fleet's create dialog too)
21
22
  import { backendChoices, modelChoices, OTHER_MODEL, useBackendRows, useModelCatalogState } from "./settings-catalog.js";
22
23
  import {
@@ -146,9 +147,9 @@ export function ChipList({ items, onChange, placeholder, label }) {
146
147
  const replyHint = (backend, mode, kiroUi) => (replyGuardSupported(backend, mode, kiroUi) ? tn("replyGuardSupported") : tn("replyGuardUnsupported", backend, mode));
147
148
 
148
149
  /** The dialog every editor uses: Cancel / Stage change; the footer says what the changes made so far will cost (#2346: only the fields that changed). */
149
- function EditDialog({ title, subtitle, onClose, onStage, impacts, schema, children, ready = true, wide = true }) {
150
+ function EditDialog({ title, subtitle, onClose, onStage, impacts, schema, children, ready = true, wide = true, drawer = false }) {
150
151
  const kinds = [...new Set((impacts || []).map((f) => impactOf(schema, f)))];
151
- return html`<${Dialog} title=${title} label=${subtitle ? `${title} — ${subtitle}` : title} onClose=${onClose} wide=${wide}
152
+ return html`<${Dialog} title=${title} label=${subtitle ? `${title} — ${subtitle}` : title} onClose=${onClose} wide=${wide} drawer=${drawer}
152
153
  actions=${html`${kinds.length ? html`<span class="note dlg-impact">${kinds.map(impactText).join(" · ")}</span>` : null}
153
154
  <button type="button" class="btn" onClick=${onClose}>${tn("cancel")}</button>
154
155
  <button type="button" class="btn btn-primary" disabled=${!ready} onClick=${onStage}>${tn("modalDone")}</button>`}>
@@ -281,8 +282,13 @@ export function AgentDialog({ name, inst, ctx, onClose }) {
281
282
  auto: f.inherit.auto_pause_after ? "" : nonNegative(f.auto_pause_after),
282
283
  hang: f.inherit.hang ? "" : positive(f.hang),
283
284
  };
285
+ // #3008 G1: a fleet with no chat connection has one General at most — ticking a second says which one it is, right
286
+ // by the checkbox (kept ticked, Save waits), by the name the chat list shows (the leader's string 18).
287
+ const otherGeneral = !channelsOf(fleet).length && f.general_topic && !inst.general_topic
288
+ ? Object.entries(fleet.instances || {}).find(([other, c]) => other !== name && c && (c.general_topic === true || (other === "general" && c.general_topic !== false))) : null;
289
+ const generalError = otherGeneral ? tn("oneGeneral", ctx.shortName(otherGeneral[0], otherGeneral[1])) : "";
284
290
  const failoverWarn = !f.inherit.model_failover && !f.model_failover.trim() ? "failoverEmpty" : "";
285
- const ready = !errors.auto && !errors.hang;
291
+ const ready = !errors.auto && !errors.hang && !generalError;
286
292
  const pending = () => agentPatch({ ...f, status_emojis: emojis.value }, inst, defaults, visibilityBaseline, emojis.baseline);
287
293
  const stage = () => {
288
294
  if (!ready) return;
@@ -297,14 +303,13 @@ export function AgentDialog({ name, inst, ctx, onClose }) {
297
303
  const inh = (k, value, target, kind, set = hasOwn(defaults, k)) => ({ text: valueText(value, kind), builtin: !set, from: target ? inAgentDefaults(target) : null });
298
304
  const inheritedBackend = !inst.backend && f.backend === (defaults.backend || "claude-code");
299
305
  const L = (text, field, id) => html`<${Label} text=${text} schema=${schema} field=${field} htmlFor=${id} />`;
300
- return html`<${EditDialog} title=${ctx.shortName(name, inst)} subtitle=${tn("idLine", name)} onClose=${onClose} onStage=${stage} ready=${ready} impacts=${Object.keys(pending()).map((k) => `instance.${k}`)} schema=${schema}>
306
+ return html`<${EditDialog} title=${ctx.shortName(name, inst)} subtitle=${tn("idLine", name)} onClose=${onClose} onStage=${stage} ready=${ready} drawer=${true} impacts=${Object.keys(pending()).map((k) => `instance.${k}`)} schema=${schema}>
307
+ <${AgentRuntimeSettings} name=${name} inst=${{...inst,backend:f.backend}} inherited=${inheritedBackend ? html`<${Inherited} ...${inh("backend", f.backend, "#g-be", "backend")} />` : null} disabled=${Object.keys(pending()).length>0||ctx.staged?.size>0} onChanged=${()=>{ctx.reload();onClose();}}/>
301
308
  <div class="grid2">
302
309
  <div class="field">${L(tn("displayName"), "instance.display_name", "ag-dn")}
303
310
  <${Override} inherit=${f.inherit.display_name} onInherit=${setInherit("display_name")} inherited=${inh("display_name", defaults.display_name || name, null, "", !!defaults.display_name)}>
304
311
  <input id="ag-dn" type="text" placeholder=${name} disabled=${f.inherit.display_name} value=${shown("display_name", f.display_name, defaults.display_name || "")} onInput=${(e) => set("display_name")(e.target.value)} /></${Override}></div>
305
312
  <div class="field">${L(tn("description"), "instance.description", "ag-desc")}<input id="ag-desc" type="text" value=${f.description} onInput=${(e) => set("description")(e.target.value)} /></div>
306
- <div class="field">${L(tn("backend"), "instance.backend", "ag-be")}<${BackendSelect} id="ag-be" value=${f.backend} onChange=${set("backend")} />
307
- ${inheritedBackend ? html`<${Inherited} ...${inh("backend", f.backend, "#g-be", "backend")} />` : null}</div>
308
313
  <div class="field">${L(tn("model"), "instance.model", "ag-model")}<${ModelSelect} id="ag-model" value=${f.model} onChange=${set("model")}
309
314
  backend=${f.backend || defaults.backend || "claude-code"} instance=${name} inherit=${tn("inheritValue", defaults.model || tn("modelCliDefault"))} /></div>
310
315
  </div>
@@ -360,8 +365,9 @@ export function AgentDialog({ name, inst, ctx, onClose }) {
360
365
  </div>
361
366
  <div class="field">${L(tn("systemPrompt"), "instance.systemPrompt", "ag-sys")}<input id="ag-sys" type="text" placeholder="e.g. file:prompts/dev.md" value=${f.systemPrompt} onInput=${(e) => set("systemPrompt")(e.target.value)} /></div>
362
367
  <div class="field">${L(tn("tags"), "instance.tags")}<${ChipList} items=${f.tags} onChange=${set("tags")} placeholder=${tn("tagPlaceholder")} label=${tn("tags")} /></div>
363
- <label class="check"><input type="checkbox" checked=${f.general_topic} onChange=${(e) => set("general_topic")(e.target.checked)} /><span>${tn("generalTopicField")}</span>
368
+ <label class="check"><input id="ag-general" type="checkbox" checked=${f.general_topic} onChange=${(e) => set("general_topic")(e.target.checked)} /><span>${tn("generalTopicField")}</span>
364
369
  <${Impact} schema=${schema} field="instance.general_topic" /></label>
370
+ ${generalError ? html`<p class="feedback error" role="alert" id="ag-general-one">${generalError}</p>` : null}
365
371
  </${Drawer}>
366
372
  <${Drawer} title=${tn("statusEmojis")}>
367
373
  <p class="note">${tn("statusEmojisInstance")} <${Impact} schema=${schema} field="instance.status_emojis" /></p>
@@ -16,6 +16,7 @@ import { confirmDialog } from "/assets/ui-confirm.js";
16
16
  import { api } from "./settings-confirm.js";
17
17
  import { DEFAULT_SCHEMA, impactOf } from "./settings-model.js";
18
18
  import "./settings-strings.js";
19
+ import { layoutText } from "./settings-layout.js";
19
20
 
20
21
  const tn = (k, ...v) => t(`settings.${k}`, ...v);
21
22
  const PLATFORMS = { discord: "Discord", telegram: "Telegram" };
@@ -93,6 +94,7 @@ export function GuestsSection({ ctx, staged }) {
93
94
  const [overview, setOverview] = useState(null), [failed, setFailed] = useState(false);
94
95
  const [people, setPeople] = useState(null), [query, setQuery] = useState(""), [editing, setEditing] = useState(null);
95
96
  const [sendState, setSendState] = useState({}), [now, setNow] = useState(() => Date.now());
97
+ const [viewedGuest, setViewedGuest] = useState(null);
96
98
  const [status, setStatus] = useState(null); // { kind: "waiting" } | { kind: "failed", error }
97
99
  const load = async () => {
98
100
  let r = null;
@@ -211,6 +213,7 @@ export function GuestsSection({ ctx, staged }) {
211
213
  };
212
214
 
213
215
  const rows = f.grants ? [...new Set([...Object.keys(f.grants), ...Object.keys(original.grants || {})])] : [];
216
+ const activeGuest = rows.includes(editing) ? editing : rows.includes(viewedGuest) ? viewedGuest : rows[0];
214
217
  const q = query.trim().toLowerCase();
215
218
  const candidates = (people || []).filter((p) => !(f.grants && own(f.grants, accountKey(p))))
216
219
  .filter((p) => !q || [p.name, p.username, p.userId].some((x) => typeof x === "string" && x.toLowerCase().includes(q))).slice(0, 8);
@@ -218,21 +221,32 @@ export function GuestsSection({ ctx, staged }) {
218
221
  return html`<section class="card s-guests" id="guests"><h3>${tn("guestsTitle")}</h3>
219
222
  <p class="note">${tn("grantsHint")}</p>
220
223
  <fieldset class="s-guest-fields" disabled=${readOnly}>
221
- <label class=${`check${f.enabled !== original.enabled ? " s-dirty" : ""}`}><input id="guestsEnabled" type="checkbox" checked=${f.enabled} onChange=${(e) => set({ enabled: e.target.checked })} /><span>${tn("guestsEnabled")}</span></label>
224
+ <div class="s-guest-policy"><div> <label class=${`check${f.enabled !== original.enabled ? " s-dirty" : ""}`}><input class="s-ui-toggle" id="guestsEnabled" type="checkbox" checked=${f.enabled} onChange=${(e) => set({ enabled: e.target.checked })} /><span>${tn("guestsEnabled")}</span></label>
222
225
  <p class="note">${tn("guestsEnabledHint")}</p>
223
- <label class=${`check${f.chat !== original.chat ? " s-dirty" : ""}`}><input id="guestsChat" type="checkbox" checked=${f.chat} onChange=${(e) => set({ chat: e.target.checked })} /><span>${tn("guestsChat")}</span></label>
226
+ </div><div> <label class=${`check${f.chat !== original.chat ? " s-dirty" : ""}`}><input class="s-ui-toggle" id="guestsChat" type="checkbox" checked=${f.chat} onChange=${(e) => set({ chat: e.target.checked })} /><span>${tn("guestsChat")}</span></label>
224
227
  <p class="note">${tn("guestsChatHint")}</p>
228
+ </div></div>
225
229
  ${failed ? html`<p class="note" role="status">${tn("guestsLoadFailed")} <button type="button" class="btn btn-sm" onClick=${load}>${t("app.retry")}</button></p>`
226
230
  : !overview ? html`<p class="note">${t("app.loading")}</p>` : null}
227
231
  ${!f.grants && overview ? html`<div class="g-legacy"><p class="note">${tn("grantsLegacy")}</p>
228
232
  ${readOnly ? null : html`<button type="button" class="btn btn-sm" id="grantsConvert" onClick=${convert}>${tn("grantsConvert")}</button>`}</div>` : null}
229
- <h4>${tn("grantsWithAccess")}</h4>
230
- ${!rows.length ? html`<p class="note" id="grantsNone">${tn("grantsNoneYet")}</p>` : html`<ul class="guest-list">${rows.map((k) => {
233
+ <div class="s-guest-workbench"><section class="s-guest-list-card"><h4>${layoutText("revGuestManagement")}</h4>
234
+ <div class="s-guest-columns" aria-hidden="true"><span>${tn("guestsTitle")}</span><span>${layoutText("revVisibleAgents")}</span><span>${layoutText("revGuestLevel")}</span><span>${tn("settingsButton")}</span></div>
235
+ ${!rows.length ? html`<p class="note" id="grantsNone">${tn("grantsNoneYet")}</p>` : html`<ul class="guest-list">${rows.map(k => {
236
+ const person=who(k), grant=f.grants?.[k] || original.grants?.[k];
237
+ return html`<li key=${k} class=${`guest-summary${activeGuest === k ? " is-selected" : ""}`}>
238
+ <span class="g-summary-name"><strong>${person.name}</strong><small>${PLATFORMS[person.platform] || person.platform}</small></span>
239
+ <span class="g-summary-agents">${(grant?.instances || []).map(n => html`<span key=${n}>${shownName(rooms,n)}</span>`)}</span>
240
+ <span>${tn(levelOf(grant) === "trusted" ? "grantsLevelTrusted" : "grantsLevelChat")}</span>
241
+ <button type="button" class="btn btn-sm" aria-pressed=${activeGuest === k ? "true" : "false"} aria-controls=${`guest-detail-${encodeURIComponent(k)}`} onClick=${() => {setViewedGuest(k);setEditing(k);}}>${tn("settingsButton")}</button>
242
+ </li>`;
243
+ })}</ul>`}</section>
244
+ <aside class="s-guest-detail-card" aria-label=${layoutText("revGuestDetail")}>${rows.map((k) => {
231
245
  const person = who(k), draft = f.grants && f.grants[k], saved = original.grants && original.grants[k];
232
246
  const removed = !draft, added = !saved, dirty = removed || added || !sameGrant(draft, saved);
233
247
  const applied = !!saved && saved.instances.length > 0 && !dirty;
234
248
  const ss = sendState[k] || {}, cooling = ss.until && ss.until > now;
235
- return html`<li key=${k} class=${`guest-row${dirty ? " s-dirty" : ""}${removed ? " g-removed" : ""}`} data-account=${k}>
249
+ return html`<section key=${k} hidden=${!readOnly && activeGuest !== k} id=${`guest-detail-${encodeURIComponent(k)}`} class=${`guest-row${dirty ? " s-dirty" : ""}${removed ? " g-removed" : ""}`} data-account=${k}>
236
250
  <div class="g-head"><span class="g-av" aria-hidden="true">${(person.name || "?").slice(0, 1).toUpperCase()}</span>
237
251
  <div class="g-id"><strong>${person.name}</strong> <span class="note">${PLATFORMS[person.platform] || person.platform}${person.username ? ` · @${person.username}` : ""}</span></div>
238
252
  ${removed ? html`<span class="g-state">${tn("grantsWillBeRemoved")}</span>` : added ? html`<span class="g-state">${tn("grantsNew")}</span>` : pill(person)}</div>
@@ -255,23 +269,23 @@ export function GuestsSection({ ctx, staged }) {
255
269
  : html`<button type="button" class="btn btn-sm" onClick=${() => setGrant(k, { ...saved })}>${tn("grantsUndoRemove")}</button>`}
256
270
  ${person.guest && (person.guest.sessions || person.guest.devices) ? html`<button type="button" class="btn btn-sm" onClick=${() => signOut(person)}>${tn("guestSignOut")}</button>` : null}
257
271
  </div>`}
258
- </li>`;
259
- })}</ul>`}
260
- ${readOnly ? null : html`<h4>${tn("grantsAdd")}</h4>
272
+ </section>`;
273
+ })}${!rows.length ? html`<p class="note">${layoutText("revSelectGuest")}</p>` : null}</aside></div>
274
+ ${readOnly ? null : html`<details class="s-guest-add"><summary>${tn("grantsAdd")}</summary><div>
261
275
  <input type="search" class="input g-search" id="grantsSearch" value=${query} aria-label=${tn("grantsSearch")} placeholder=${tn("grantsSearch")} onInput=${(e) => setQuery(e.target.value)} />
262
276
  ${people === null ? html`<p class="note">${t("app.loading")}</p>` : !candidates.length ? html`<p class="note" id="grantsNoPeople">${tn("grantsNoPeople")}</p>`
263
277
  : html`<ul class="g-pick">${candidates.map((p) => html`<li key=${accountKey(p)} class="g-cand" data-account=${accountKey(p)}>
264
278
  <span class="g-av" aria-hidden="true">${(p.name || "?").slice(0, 1).toUpperCase()}</span>
265
279
  <div class="g-id"><strong>${p.name}</strong> <span class="note">${PLATFORMS[p.platform] || p.platform}${p.username ? ` · @${p.username}` : ""} · <span class="g-when">${tn("grantsLastSeen", new Date(p.lastSeen).toLocaleString())}</span></span>
266
280
  ${p.instances && p.instances.length ? html`<div class="note">${wroteTo(p.instances)}</div>` : null}</div>
267
- <button type="button" class="btn btn-sm" onClick=${() => { const k = accountKey(p); setGrant(k, { ...(p.name ? { name: p.name } : {}), instances: choosable.filter((n) => (p.instances || []).includes(n)) }); setEditing(k); }}>${tn("grantsChoose")}</button></li>`)}</ul>`}`}
281
+ <button type="button" class="btn btn-sm" onClick=${() => { const k = accountKey(p); setGrant(k, { ...(p.name ? { name: p.name } : {}), instances: choosable.filter((n) => (p.instances || []).includes(n)) }); setEditing(k); }}>${tn("grantsChoose")}</button></li>`)}</ul>`}</div></details>`}
268
282
  </fieldset>
269
- ${overview && overview.audit ? html`<h4>${tn("grantsRecent")}</h4>${!overview.audit.length ? html`<p class="note" id="grantsRecentNone">${tn("grantsRecentNone")}</p>`
283
+ ${overview && overview.audit ? html`<details class="s-guest-audit"><summary>${tn("grantsRecent")}</summary>${!overview.audit.length ? html`<p class="note" id="grantsRecentNone">${tn("grantsRecentNone")}</p>`
270
284
  : html`<ul class="g-audit" id="grantsRecentList">${overview.audit.map((e, i) => html`<li key=${`${e.at}-${i}`} class="g-audit-row" data-kind=${e.kind}>
271
285
  <span class="g-when">${new Date(e.at).toLocaleString()}</span> <strong>${e.name}</strong>
272
286
  ${e.kind === "grant" ? html`${e.added.map((n) => html` <span key=${`+${n}`} class="g-wrote g-add">+${n}</span>`)}${e.removed.map((n) => html` <span key=${`-${n}`} class="g-wrote g-del">−${n}</span>`)}${e.level ? html` <span class="g-wrote g-level-to">${tn("grantsAuditLevel", tn(e.level === "trusted" ? "grantsLevelTrusted" : "grantsLevelChat"))}</span>` : null}
273
287
  <span class="note"> · <span class="g-wrote">${tn(e.via === "confirmed" ? "grantsAuditConfirmed" : "grantsAuditDirect")}</span>${e.endedSessions ? html` · <span class="g-wrote">${tn("grantsAuditEnded", e.endedSessions)}</span>` : null}</span>`
274
- : html` <span class="note">${e.outcome === "sent" ? tn("grantsAuditCodeSent") : tn("grantsAuditCodeNot", auditOutcome(e.outcome))}</span>`}</li>`)}</ul>`}` : null}
288
+ : html` <span class="note">${e.outcome === "sent" ? tn("grantsAuditCodeSent") : tn("grantsAuditCodeNot", auditOutcome(e.outcome))}</span>`}</li>`)}</ul>`}</details>` : null}
275
289
  ${status && status.kind === "waiting" ? html`<p class="note" role="status">${tn("grantsWaiting")}</p>` : null}
276
290
  ${status && status.kind === "failed" ? html`<p class="note g-error" role="alert">${tn("grantsSaveFailed", status.error)}</p>` : null}
277
291
  </section>`;
@@ -0,0 +1,245 @@
1
+ // Shared presentation for Settings. Tab panels stay mounted so switching views never drops a form's draft.
2
+ import { html, useEffect, useRef, useState } from "/assets/app-html.js";
3
+ import { register, t } from "/assets/app-i18n.js";
4
+
5
+ register("settingsLayout", {
6
+ en: {
7
+ account: "Your account", personal: "Your preferences", fleet: "Whole fleet",
8
+ lead_preferences: "Your identity, language and content preferences.",
9
+ lead_look: "Choose how AgEnD looks and keep your workspace comfortable.",
10
+ lead_agents: "Manage agents across runtimes, with their CLI, model and status in one place.",
11
+ "lead_agent-defaults": "Defaults for new agents. Each agent can override the settings it supports.",
12
+ lead_chat: "Connect chat services and choose how messages are received and delivered.",
13
+ "lead_web-dashboard": "Manage fleet-wide dashboard features, branding and resource limits.",
14
+ lead_access: "Manage sign-in, web authority, guests and sharing.",
15
+ lead_advanced: "Inspect and edit fleet configuration, with effective values kept separate.",
16
+ editor: "Configuration editor", effective: "Effective settings (read only)",
17
+ aiAccounts: "AI accounts and keys", aiAccountsHint: "Sign-ins and supported provider keys on the AgEnD host. Windows CLI status is shown under Runtime.",
18
+ classicDefaults: "ClassicBot defaults and access",
19
+ },
20
+ "zh-TW": {
21
+ account: "你的帳號", personal: "你的偏好", fleet: "整個 Fleet",
22
+ lead_preferences: "你的身分、語言與內容偏好。",
23
+ lead_look: "調整顯示方式,保留你的工作習慣。",
24
+ lead_agents: "集中管理各個 Runtime 的 Agent、CLI、模型與狀態。",
25
+ "lead_agent-defaults": "新 Agent 的預設行為;個別 Agent 可覆寫支援的設定。",
26
+ lead_chat: "連接聊天服務,設定訊息如何進入與送出。",
27
+ "lead_web-dashboard": "管理整個 Fleet 的網頁功能、品牌與資源限制。",
28
+ lead_access: "管理登入、網頁授權、訪客與對外分享。",
29
+ lead_advanced: "檢視與編輯 Fleet 設定,並區分目前生效的唯讀內容。",
30
+ editor: "設定編輯器", effective: "生效設定(唯讀)",
31
+ aiAccounts: "AI 帳號與金鑰", aiAccountsHint: "管理 AgEnD 主機的登入與支援的供應商金鑰;Windows CLI 狀態請到 Runtime 查看。",
32
+ classicDefaults: "ClassicBot 預設行為與存取",
33
+ },
34
+ });
35
+
36
+ register("settingsLayout", {
37
+ en: {
38
+ aiAccounts: "AI sign-ins and API keys", aiAccountsSummary: "Host CLI sign-ins and provider API keys, managed separately.",
39
+ aiHostScope: "Credentials on the AgEnD host", aiAccountsHint: "These sign-ins and keys belong to the host running AgEnD.", aiWindowsLink: "Manage Windows computers and their CLIs in Runtime",
40
+ apiKeysTitle: "Provider API keys", apiKeysHint: "API credentials for supported provider integrations.", apiKeysUnavailableState: "Not offered", apiKeysUnavailable: "Provider API key management is not offered in this session.",
41
+ apiNewPlaceholder: "Enter an API key to verify", apiReplacePlaceholder: "Enter a replacement API key",
42
+ },
43
+ "zh-TW": {
44
+ aiAccounts: "AI 登入與 API 金鑰", aiAccountsSummary: "分開管理主機 CLI 登入與供應商 API 金鑰。",
45
+ aiHostScope: "AgEnD 主機上的憑證", aiAccountsHint: "這裡的登入與金鑰屬於執行 AgEnD 的主機。", aiWindowsLink: "前往 Runtime 管理 Windows 電腦與 CLI",
46
+ apiKeysTitle: "供應商 API 金鑰", apiKeysHint: "供支援的供應商整合功能使用的 API 憑證。", apiKeysUnavailableState: "目前未提供", apiKeysUnavailable: "此工作階段未提供供應商 API 金鑰管理。",
47
+ apiNewPlaceholder: "輸入要驗證的 API 金鑰", apiReplacePlaceholder: "輸入替換用的 API 金鑰",
48
+ },
49
+ });
50
+
51
+ export const layoutText = (key, ...values) => t(`settingsLayout.${key}`, ...values);
52
+
53
+ register("settingsLayout", {
54
+ "en": {
55
+ "accessLogin": "Sign-in & authority",
56
+ "accessGuests": "Guests",
57
+ "accessSharing": "Public & sharing",
58
+ "accessSharingList": "Shared links",
59
+ "accessPublicCredentials": "Cloudflare connector credentials",
60
+ "accessPolicyHint": "How access changes take effect",
61
+ "dashboardFeatures": "Features",
62
+ "developerReadOnly": "Read-only mode",
63
+ "developerEditing": "Editing draft",
64
+ "effectiveKey": "Setting key",
65
+ "effectiveValue": "Value in effect",
66
+ "effectiveSource": "Source",
67
+ "effectiveSourceConfig": "fleet.yaml",
68
+ "effectiveSourceDefault": "Default"
69
+ },
70
+ "zh-TW": {
71
+ "accessLogin": "登入與授權",
72
+ "accessGuests": "訪客",
73
+ "accessSharing": "公開與分享",
74
+ "accessSharingList": "分享連結",
75
+ "accessPublicCredentials": "Cloudflare 連接器憑證",
76
+ "accessPolicyHint": "存取變更如何生效",
77
+ "dashboardFeatures": "功能",
78
+ "developerReadOnly": "唯讀模式",
79
+ "developerEditing": "編輯草稿中",
80
+ "effectiveKey": "設定鍵",
81
+ "effectiveValue": "目前生效值",
82
+ "effectiveSource": "來源",
83
+ "effectiveSourceConfig": "fleet.yaml",
84
+ "effectiveSourceDefault": "預設"
85
+ }
86
+ });
87
+
88
+ export function SettingsPageHead({ section, children }) {
89
+ const scope = section === "preferences" ? "account" : section === "look" ? "personal" : "fleet";
90
+ return html`<header class="s-page-head"><div><div class="s-page-title"><h2>${t(`settings.sec_${section}`)}</h2><span class="s-scope">${layoutText(scope)}</span></div><p class="s-page-lead">${layoutText(`lead_${section}`)}</p></div>${children ? html`<div class="s-page-actions">${children}</div>` : null}</header>`;
91
+ }
92
+
93
+ // Settings search, validation links and legacy anchors reveal their owning tab before focusing a control.
94
+ const revealers = new Set();
95
+ export function useSettingsTabs(group, initial) {
96
+ const [value, setValue] = useState(initial);
97
+ const current = useRef(value);
98
+ current.current = value;
99
+ useEffect(() => {
100
+ const reveal = target => {
101
+ const panel = target?.closest?.("[data-settings-tabs]");
102
+ if (!panel || panel.getAttribute("data-settings-tabs") !== group) return false;
103
+ const next = panel.getAttribute("data-settings-tab");
104
+ if (!next || next === current.current) return false;
105
+ current.current = next;
106
+ setValue(next);
107
+ return true;
108
+ };
109
+ revealers.add(reveal);
110
+ return () => revealers.delete(reveal);
111
+ }, [group]);
112
+ return [value, setValue];
113
+ }
114
+
115
+ export function revealSettingsTarget(target) {
116
+ let changed = false;
117
+ for (const reveal of revealers) changed = reveal(target) || changed;
118
+ return changed;
119
+ }
120
+
121
+ export function SettingsTabs({ id, value, onChange, items }) {
122
+ const choose = (key, focus = false) => {
123
+ onChange(key);
124
+ if (focus) document.getElementById(`${id}-tab-${key}`)?.focus();
125
+ };
126
+ const keydown = (event, index) => {
127
+ let next;
128
+ if (event.key === "ArrowRight") next = (index + 1) % items.length;
129
+ else if (event.key === "ArrowLeft") next = (index - 1 + items.length) % items.length;
130
+ else if (event.key === "Home") next = 0;
131
+ else if (event.key === "End") next = items.length - 1;
132
+ else return;
133
+ event.preventDefault();
134
+ choose(items[next].id, true);
135
+ };
136
+ return html`<div class="s-page-tabs" role="tablist" aria-label=${t("settings.title")}>${items.map((item, index) => html`<button key=${item.id} id=${`${id}-tab-${item.id}`} type="button" role="tab" class=${value === item.id ? "on" : ""} aria-selected=${value === item.id ? "true" : "false"} aria-controls=${`${id}-panel-${item.id}`} tabindex=${value === item.id ? 0 : -1} onClick=${() => choose(item.id)} onKeyDown=${event => keydown(event, index)}>${item.label}</button>`)}</div>`;
137
+ }
138
+
139
+ export function SettingsTabPanel({ group, id, active, children }) {
140
+ return html`<div id=${`${group}-panel-${id}`} class="s-tab-panel" role="tabpanel" aria-labelledby=${`${group}-tab-${id}`} data-settings-tabs=${group} data-settings-tab=${id} hidden=${!active}>${children}</div>`;
141
+ }
142
+
143
+ register("settingsLayout", {
144
+ "en": {
145
+ "profileIdentityDetails": "How agents recognise you",
146
+ "previewsPersonalHint": "Only affects your preview preference.",
147
+ "chatConnections": "Chat connections",
148
+ "messageBehavior": "Message behavior",
149
+ "pageContents": "On this page",
150
+ "defaultsAccessWorkspace": "Permissions and workspace",
151
+ "replyRulesHelp": "Reply rules",
152
+ "replySupportHelp": "CLI support"
153
+ },
154
+ "zh-TW": {
155
+ "profileIdentityDetails": "Agent 如何辨識你的身分",
156
+ "previewsPersonalHint": "只影響你的預覽偏好。",
157
+ "chatConnections": "聊天連線",
158
+ "messageBehavior": "訊息行為",
159
+ "pageContents": "本頁內容",
160
+ "defaultsAccessWorkspace": "權限與工作區",
161
+ "replyRulesHelp": "回覆規則說明",
162
+ "replySupportHelp": "CLI 支援情況"
163
+ }
164
+ });
165
+
166
+ register("settingsLayout", {
167
+ "en": {
168
+ "revDashboard_log": "Agent log tab",
169
+ "revDashboardHint_log": "Show prompts, replies, tool steps and fleet events. Secrets are masked; public links can also see it. Enabling needs confirmation.",
170
+ "revDashboard_usagePub": "Usage over the public link",
171
+ "revDashboardHint_usagePub": "Offer the fleet usage report over the temporary public link. Guests never see it.",
172
+ "revDashboard_levels": "Office levels",
173
+ "revDashboardHint_levels": "Show each agent's lifetime work level on its Office name tag and profile.",
174
+ "revDashboard_cdn": "CDN libraries in HTML previews",
175
+ "revDashboardHint_cdn": "Allow cdnjs and jsDelivr. Requests leave this computer, are visible to the CDN and could send page data out.",
176
+ "revDetails": "Details",
177
+ "revAccessPolicy": "Loosening access needs fleet administrator confirmation. Tightening applies at once.",
178
+ "revLoginSessions": "Sign-in sessions",
179
+ "revPublicSessionLimit": "Public-link sessions are capped at 8 hours, end after 30 minutes idle and never outlive the link.",
180
+ "revSessionDetails": "Session rules and notifications",
181
+ "revRelatedAccess": "Other access settings",
182
+ "revRelatedGuests": "Choose who can sign in and which agents they see.",
183
+ "revRelatedSharing": "Manage the public entry and read-only shared links.",
184
+ "revPublicEntry": "Fleet public entry",
185
+ "revReadOnlySharing": "Time-limited read-only sharing",
186
+ "revSharingOrigin": "Create new shares from the Office or an HTML preview. Manage existing links below.",
187
+ "revEffectiveHint": "These are the values in effect. This view is read only; edit supported values in the configuration editor.",
188
+ "revGoEditor": "Open configuration editor",
189
+ "revEditorContext": "Fleet configuration · use the form pages for individual settings",
190
+ "revUpdateCredential": "Update connector credential",
191
+ "revSetCredential": "Set connector credential",
192
+ "revAuthorityBrief": "Only signed-in fleet administrators can confirm their account's authority in Discord or Telegram. Guests cannot. Publishing, destructive actions, spending, secrets and passing authority require it.",
193
+ "revAuthorityDetails": "Authority rules",
194
+ "revAuthorityEnabled": "Allow web authority",
195
+ "revAuthorityEnabledHint": "Turning this off invalidates existing authority at once. Turning it back on does not restore it.",
196
+ "revAuthorityLocal": "Only this computer",
197
+ "revAuthorityLocalHint": "Allow only this computer's browser. Authority already given over a LAN or tailnet becomes invalid and is not restored by switching this off.",
198
+ "revAuthorityPublicWarning": "Public links are reached from the internet and Cloudflare sees their traffic. Enabling their authority needs confirmation; re-enabling temporary-link authority ends all earlier authorizations.",
199
+ "revAuthorityLinks": "Authority over public links",
200
+ "revGuestManagement": "Guest management",
201
+ "revVisibleAgents": "Visible agents",
202
+ "revGuestLevel": "Level",
203
+ "revGuestDetail": "Selected guest",
204
+ "revSelectGuest": "Add or choose a guest to manage their access."
205
+ },
206
+ "zh-TW": {
207
+ "revDashboard_log": "Agent 紀錄分頁",
208
+ "revDashboardHint_log": "顯示提示、回覆、工具步驟與 Fleet 事件;機密值會遮蔽,公開連結也看得到。開啟需確認。",
209
+ "revDashboard_usagePub": "公開連結提供用量報告",
210
+ "revDashboardHint_usagePub": "透過臨時公開連結提供 Fleet 用量報告;訪客一律看不到。",
211
+ "revDashboard_levels": "辦公室等級",
212
+ "revDashboardHint_levels": "在辦公室名牌與名片上顯示 Agent 依終身工作量得到的等級。",
213
+ "revDashboard_cdn": "HTML 預覽載入 CDN 函式庫",
214
+ "revDashboardHint_cdn": "允許 cdnjs 與 jsDelivr。請求會離開本機,CDN 能看到,也可能被頁面用來傳出資料。",
215
+ "revDetails": "詳細說明",
216
+ "revAccessPolicy": "放寬存取需要 Fleet 管理員確認;收緊的變更立即生效。",
217
+ "revLoginSessions": "登入工作階段",
218
+ "revPublicSessionLimit": "公開連結的工作階段最長 8 小時、閒置 30 分鐘登出,也不會超過連結期限。",
219
+ "revSessionDetails": "工作階段與通知規則",
220
+ "revRelatedAccess": "其他存取設定",
221
+ "revRelatedGuests": "管理可登入的訪客與可見 Agent。",
222
+ "revRelatedSharing": "管理公開入口與唯讀分享連結。",
223
+ "revPublicEntry": "Fleet 公開入口",
224
+ "revReadOnlySharing": "限時唯讀分享",
225
+ "revSharingOrigin": "新分享請從辦公室或 HTML 預覽建立;在下方管理既有連結。",
226
+ "revEffectiveHint": "下方為目前生效的設定,僅供檢視;需要修改時,請切換至設定編輯器。",
227
+ "revGoEditor": "前往設定編輯器",
228
+ "revEditorContext": "Fleet 設定檔・個別設定可使用對應表單調整",
229
+ "revUpdateCredential": "更新連接器憑證",
230
+ "revSetCredential": "設定連接器憑證",
231
+ "revAuthorityBrief": "僅限已登入的 Fleet 管理者,並須在 Discord 或 Telegram 確認本人帳號授權;訪客不適用。發布、破壞性操作、花費、密鑰與轉交授權都需要它。",
232
+ "revAuthorityDetails": "授權規則說明",
233
+ "revAuthorityEnabled": "允許網頁授權",
234
+ "revAuthorityEnabledHint": "關閉會立即使現有授權失效;重新開啟不會恢復。",
235
+ "revAuthorityLocal": "僅限本機",
236
+ "revAuthorityLocalHint": "只接受本機瀏覽器;區域網路與 tailnet 上已給出的授權會失效,關掉此限制也不會恢復。",
237
+ "revAuthorityPublicWarning": "公開連結可從網際網路存取,Cloudflare 看得到流量。開啟其授權須經確認;重新開啟臨時連結授權會結束之前給出的全部授權。",
238
+ "revAuthorityLinks": "公開連結的授權範圍",
239
+ "revGuestManagement": "訪客管理",
240
+ "revVisibleAgents": "可見 Agent",
241
+ "revGuestLevel": "等級",
242
+ "revGuestDetail": "選取的訪客",
243
+ "revSelectGuest": "新增或選擇訪客後,管理其存取權限。"
244
+ }
245
+ });
@@ -23,7 +23,8 @@ export const TOOL_SETS = ["worker", "coordinator", "standard", "minimal", "full"
23
23
  export function toolSetDefault(defaults, inst, name = "") {
24
24
  if (TOOL_SETS.includes(defaults && defaults.tool_set)) return defaults.tool_set;
25
25
  const general = hasOwn(inst, "general_topic") ? inst.general_topic === true : !!defaults && defaults.general_topic === true;
26
- return general || name === "general" ? "general" : "worker";
26
+ // The literal-name fallback, unless unticked (general-instance.ts generalByName: an explicit general_topic: false).
27
+ return general || (name === "general" && !(hasOwn(inst, "general_topic") && inst.general_topic === false)) ? "general" : "worker";
27
28
  }
28
29
  export const TOOL_PROGRESS = ["off", "standard", "verbose"];
29
30
  export const AGENT_MODES = ["mcp", "cli"];
@@ -74,6 +75,14 @@ export const visibilityDefault = (defaults) => (VISIBILITY_MODES.includes(defaul
74
75
  export const replyGuardSupported = (backend, mode, kiroUi = "legacy") =>
75
76
  mode === "mcp" && (["claude-code", "codex", "muse"].includes(backend) || (backend === "kiro-cli" && ["legacy", "tui"].includes(kiroUi || "legacy")));
76
77
 
78
+ /**
79
+ * Backends AgEnD no longer runs, with the one to use instead (#1280). The browser cannot import src/backend/removed.ts;
80
+ * tests/settings-removed-backend-1280.test.ts keeps this list equal to it.
81
+ */
82
+ export const REMOVED_BACKEND_REPLACEMENTS = Object.freeze({ "gemini-cli": "antigravity" });
83
+ export const removedBackendReplacement = (backend) =>
84
+ typeof backend === "string" && Object.prototype.hasOwnProperty.call(REMOVED_BACKEND_REPLACEMENTS, backend) ? REMOVED_BACKEND_REPLACEMENTS[backend] : null;
85
+
77
86
  /**
78
87
  * The backend picker's options. A current value it does not offer (the removed gemini-cli, #1280 review) stays first
79
88
  * and selected: otherwise the select falls to its first option and an unrelated edit switches the CLI and account.
@@ -0,0 +1,176 @@
1
+ // #3008 ② (alpha.3; decision 28c2d3a3 rule 3, the user's simplified form hmsg-33c3d609): Settings → Access & security →
2
+ // "Connections from outside", on a fleet with no chat connection. This computer's admin, here, sets up an authenticator
3
+ // app (a QR code and the key, then its first code), and turns on "Allow connections from outside": the public link
4
+ // opens, and signing in through it asks for the app's code. Turning it off closes the link and signs everyone out there.
5
+ // Every change applies at once (an action, not a staged setting) and is this computer's only: elsewhere the card shows
6
+ // the state, read-only. The server decides who may (GET /ui/outside's `here`) and refuses everything else regardless.
7
+ // The words are the leader's (cid-alpha3-design, strings 1–13 and 17).
8
+ import { html, useEffect, useRef, useState } from "/assets/app-html.js";
9
+ import { t, register } from "/assets/app-i18n.js";
10
+ import { toast } from "/assets/ui-toast.js";
11
+ import { confirmDialog } from "/assets/ui-confirm.js";
12
+ import { api } from "./settings-confirm.js";
13
+
14
+ register("outside", {
15
+ en: {
16
+ title: "Connections from outside", switch: "Allow connections from outside",
17
+ needApp: "Set up an authenticator app first. Signing in from outside will ask for its code.",
18
+ setUp: "Set up authenticator app",
19
+ scan: "Scan this with Authy, Google Authenticator or Microsoft Authenticator, or type the key. Then enter the 6-digit code it shows.",
20
+ setUpOn: "Authenticator app set up on {0}.", again: "Set up again",
21
+ againConfirm: "Codes from the old app stop working, and everyone signed in from outside is signed out.",
22
+ remove: "Remove", removeConfirm: "Connections from outside are turned off, and everyone signed in from outside is signed out.",
23
+ openAt: "Open at {0}.", quick: "A quick link gets a new address each time it opens.",
24
+ hereOnly: "Only on the AgEnD computer.", lostPhone: "Lost your phone? On the AgEnD computer, choose “Set up again”.",
25
+ wrong: "That code didn't work. Check that your phone's clock is right and try the newest code.",
26
+ paused: "Too many wrong codes. Try again in {0}.", code: "Authenticator code", key: "Key",
27
+ minutes: "{0} minutes", minute: "1 minute", hours: "{0} hours", hour: "1 hour",
28
+ },
29
+ "zh-TW": {
30
+ title: "從外面連線", switch: "允許從外面連線",
31
+ needApp: "請先設定驗證器 App。從外面登入時會要求輸入它的代碼。",
32
+ setUp: "設定驗證器 App",
33
+ scan: "用 Authy、Google Authenticator 或 Microsoft Authenticator 掃描,或手動輸入金鑰,再輸入 App 顯示的 6 位數代碼。",
34
+ setUpOn: "驗證器 App 已於 {0} 設定。", again: "重新設定",
35
+ againConfirm: "舊 App 的代碼會失效,所有從外面登入的人都會被登出。",
36
+ remove: "移除", removeConfirm: "從外面連線會關閉,所有從外面登入的人都會被登出。",
37
+ openAt: "已開啟:{0}。", quick: "快速連結每次開啟網址都不同。",
38
+ hereOnly: "只能在 AgEnD 電腦上變更。", lostPhone: "手機遺失?請在 AgEnD 電腦上選「重新設定」。",
39
+ wrong: "代碼不對。請確認手機時間正確,並輸入最新的代碼。",
40
+ paused: "錯誤次數太多,請 {0} 後再試。", code: "驗證器代碼", key: "金鑰",
41
+ minutes: "{0} 分鐘", minute: "1 分鐘", hours: "{0} 小時", hour: "1 小時",
42
+ },
43
+ });
44
+ const to = (k, ...v) => t(`outside.${k}`, ...v);
45
+
46
+ /** A wait as words: "15 minutes" / 「15 分鐘」, "2 hours" / 「2 小時」 (rounded up). */
47
+ export function waitWords(ms) {
48
+ const minutes = Math.max(1, Math.ceil(ms / 60000));
49
+ if (minutes < 60) return minutes === 1 ? to("minute") : to("minutes", minutes);
50
+ const hours = Math.ceil(minutes / 60);
51
+ return hours === 1 ? to("hour") : to("hours", hours);
52
+ }
53
+
54
+ /** The key as an app shows it: groups of four. */
55
+ export function keyGroups(secret) {
56
+ return String(secret || "").replace(/(.{4})(?=.)/g, "$1 ");
57
+ }
58
+
59
+ /**
60
+ * The QR code of an otpauth:// URI as one SVG path (dark modules), drawn by the page — no inline style, no innerHTML
61
+ * (the CSP). `qrcode` is the vendored encoder's factory (qrcode.module.js).
62
+ */
63
+ export function qrPath(qrcode, uri) {
64
+ const qr = qrcode(0, "M");
65
+ qr.addData(uri);
66
+ qr.make();
67
+ const n = qr.getModuleCount();
68
+ let d = "";
69
+ for (let r = 0; r < n; r++) for (let c = 0; c < n; c++) if (qr.isDark(r, c)) d += `M${c + 4} ${r + 4}h1v1h-1z`;
70
+ return { size: n + 8, d };
71
+ }
72
+
73
+ function Qr({ uri }) {
74
+ const [shape, setShape] = useState(null);
75
+ useEffect(() => {
76
+ let live = true;
77
+ import("/assets/qrcode.module.js").then((m) => { if (live) setShape(qrPath(m.default || m.qrcode, uri)); }).catch(() => { if (live) setShape(null); });
78
+ return () => { live = false; };
79
+ }, [uri]);
80
+ if (!shape) return null;
81
+ return html`<svg class="s-out-qr" id="outQr" viewBox=${`0 0 ${shape.size} ${shape.size}`} width="200" height="200" role="img" aria-label=${to("setUp")}>
82
+ <rect width=${shape.size} height=${shape.size} fill="#fff" /><path d=${shape.d} fill="#000" /></svg>`;
83
+ }
84
+
85
+ /**
86
+ * #3035 review: how often the card reads the state again while it is shown — quickly while the link is opening (the
87
+ * switch answers before the link is up), slowly otherwise (its lifetime ending, a wait starting or ending elsewhere).
88
+ */
89
+ export const outsidePoll = { quickMs: 2000, slowMs: 15000 };
90
+
91
+ export function OutsideCard() {
92
+ const [s, setS] = useState(null), [enroll, setEnroll] = useState(null), [code, setCode] = useState(""), [msg, setMsg] = useState(""), [busy, setBusy] = useState(false);
93
+ // Fences: an answer that arrives after the card is gone, or after a newer read began, is dropped; nor does a read
94
+ // land while an action is under way (the box shows the click until the action's own read).
95
+ const live = useRef({ mounted: true, seq: 0, busy: false });
96
+ // Every read that ends (answered or not) moves `rev`: the next one is scheduled from there, so a failed read (the
97
+ // network, a 5xx, an unreadable answer) never stops the card reading again (#3035 review r2) — and keeps what it
98
+ // showed meanwhile.
99
+ const [rev, setRev] = useState(0), failed = useRef(false);
100
+ const load = async () => {
101
+ const seq = ++live.current.seq;
102
+ let r = null;
103
+ try { r = await api("/ui/outside"); } catch { r = null; }
104
+ if (!live.current.mounted || seq !== live.current.seq) return;
105
+ const ok = !!(r && r.ok && r.body && typeof r.body === "object");
106
+ failed.current = !ok;
107
+ if (ok) setS(r.body);
108
+ setRev((n) => n + 1);
109
+ };
110
+ useEffect(() => { live.current.mounted = true; load(); return () => { live.current.mounted = false; }; }, []);
111
+ useEffect(() => {
112
+ if (rev === 0) return undefined; // the first read is still out
113
+ if (s && !s.webOnly && !failed.current) return undefined; // a chat fleet: nothing to follow
114
+ const opening = !!s && s.outside && (!s.link || s.link.state !== "open");
115
+ const delay = !s || opening ? outsidePoll.quickMs : outsidePoll.slowMs; // none read yet, or opening: soon
116
+ const timer = setTimeout(() => { if (!live.current.busy) load(); else setRev((n) => n + 1); }, delay);
117
+ return () => clearTimeout(timer);
118
+ }, [rev]);
119
+ if (!s || !s.webOnly) return null;
120
+ const here = s.here === true;
121
+ const act = async (fn) => {
122
+ setBusy(true); setMsg(""); live.current.busy = true; live.current.seq++; // a read under way is outdated now
123
+ try { await fn(); } finally { live.current.busy = false; if (live.current.mounted) { setBusy(false); load(); } }
124
+ };
125
+ const begin = () => act(async () => {
126
+ const r = await api("/ui/outside/enroll", { method: "POST", body: "{}" });
127
+ if (r.ok && r.body && r.body.secret) { setEnroll(r.body); setCode(""); } else toast(t("app.failed"), false);
128
+ });
129
+ const confirm = () => act(async () => {
130
+ const r = await api("/ui/outside/enroll/confirm", { method: "POST", body: JSON.stringify({ code }) });
131
+ if (r.ok) { setEnroll(null); setCode(""); return; }
132
+ const why = r.body && r.body.error;
133
+ if (why === "expired") { setEnroll(null); return; }
134
+ setMsg(why === "wrong" ? to("wrong") : t("app.failed"));
135
+ });
136
+ const again = async () => {
137
+ if (!(await confirmDialog({ title: to("again"), message: to("againConfirm"), confirmLabel: to("again"), danger: true }))) return;
138
+ begin();
139
+ };
140
+ const remove = async () => {
141
+ if (!(await confirmDialog({ title: to("remove"), message: to("removeConfirm"), confirmLabel: to("remove"), danger: true }))) return;
142
+ act(async () => { const r = await api("/ui/outside/remove", { method: "POST", body: "{}" }); if (!r.ok) toast(t("app.failed"), false); });
143
+ };
144
+ // The box follows the click at once (smoke-web-outside-3008: a controlled box that waits for the server reads as a
145
+ // click that did nothing); the server's state, read again after, is what stays.
146
+ const flip = (on) => { setS((x) => ({ ...x, outside: on })); act(async () => {
147
+ const r = await api("/ui/outside/switch", { method: "POST", body: JSON.stringify({ on }) });
148
+ if (!r.ok) toast(t("app.failed"), false);
149
+ }); };
150
+ const link = s.link, open = link && link.state === "open";
151
+ return html`<section class="card s-outside" id="outsideCard"><h3>${to("title")}</h3>
152
+ ${!here ? html`<p class="note" id="outHereOnly">${to("hereOnly")}</p>` : null}
153
+ <label class="check"><input id="outSwitch" type="checkbox" checked=${s.outside} disabled=${!here || busy || (!s.enrolled && !s.outside)}
154
+ onChange=${(e) => flip(e.target.checked)} /><span>${to("switch")}</span></label>
155
+ ${!s.enrolled ? html`<p class="note" id="outNeedApp">${to("needApp")}</p>` : null}
156
+ ${s.outside && open && link.url ? html`<p class="note" id="outOpenAt">${to("openAt", link.url)}${link.quick ? ` ${to("quick")}` : ""}</p>` : null}
157
+ ${s.outside && link && !open ? html`<p class="note" id="outStarting">${t("app.loading")}</p>` : null}
158
+ ${s.pausedForMs > 0 ? html`<p class="note s-va-warn" role="status" id="outPaused">${to("paused", waitWords(s.pausedForMs))}</p>` : null}
159
+ ${enroll ? html`<div class="s-out-enroll" id="outEnroll">
160
+ <p class="note">${to("scan")}</p>
161
+ <${Qr} uri=${enroll.uri} />
162
+ <p class="note">${to("key")}: <code id="outKey">${keyGroups(enroll.secret)}</code></p>
163
+ <div class="field"><label for="outCode">${to("code")}</label>
164
+ <input id="outCode" type="text" inputmode="numeric" autocomplete="one-time-code" maxlength="7" placeholder="123456" value=${code}
165
+ onInput=${(e) => setCode(e.target.value)} /></div>
166
+ ${msg ? html`<p class="feedback error" role="alert" id="outMsg">${msg}</p>` : null}
167
+ <div class="save-row"><button type="button" class="btn" id="outCancel" disabled=${busy} onClick=${() => { setEnroll(null); setMsg(""); }}>${t("app.cancel")}</button>
168
+ <button type="button" class="btn btn-primary" id="outConfirm" disabled=${busy || !/^\s*\d{3}\s?\d{3}\s*$/.test(code)} onClick=${confirm}>${t("app.confirmOk")}</button></div>
169
+ </div>`
170
+ : s.enrolled ? html`<p class="note" id="outSetUpOn">${to("setUpOn", new Date(s.enrolledAt).toLocaleDateString())}</p>
171
+ <p class="note">${to("lostPhone")}</p>
172
+ ${here ? html`<div class="save-row"><button type="button" class="btn" id="outAgain" disabled=${busy} onClick=${again}>${to("again")}</button>
173
+ <button type="button" class="btn btn-danger" id="outRemove" disabled=${busy} onClick=${remove}>${to("remove")}</button></div>` : null}`
174
+ : here ? html`<div class="save-row"><button type="button" class="btn btn-primary" id="outSetUp" disabled=${busy} onClick=${begin}>${to("setUp")}</button></div>` : null}
175
+ </section>`;
176
+ }