@songsid/agend 2.3.0 → 2.3.2-beta.1

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 (201) hide show
  1. package/README.md +1 -1
  2. package/README.zh-TW.md +1 -1
  3. package/dist/authority/authority-state.d.ts +4 -0
  4. package/dist/authority/authority-state.js +16 -10
  5. package/dist/authority/authority-state.js.map +1 -1
  6. package/dist/authority/session-authorizations.d.ts +7 -1
  7. package/dist/authority/session-authorizations.js +6 -4
  8. package/dist/authority/session-authorizations.js.map +1 -1
  9. package/dist/authority/web-authority.d.ts +19 -6
  10. package/dist/authority/web-authority.js +22 -8
  11. package/dist/authority/web-authority.js.map +1 -1
  12. package/dist/backend/muse.d.ts +20 -2
  13. package/dist/backend/muse.js +154 -16
  14. package/dist/backend/muse.js.map +1 -1
  15. package/dist/channel/adapters/discord.d.ts +2 -0
  16. package/dist/channel/adapters/discord.js +33 -3
  17. package/dist/channel/adapters/discord.js.map +1 -1
  18. package/dist/channel/adapters/telegram.d.ts +1 -1
  19. package/dist/channel/adapters/telegram.js +3 -35
  20. package/dist/channel/adapters/telegram.js.map +1 -1
  21. package/dist/channel/mcp-tools.js +5 -5
  22. package/dist/channel/mcp-tools.js.map +1 -1
  23. package/dist/channel/types.d.ts +11 -0
  24. package/dist/cli.js +6 -3
  25. package/dist/cli.js.map +1 -1
  26. package/dist/config-validator.js +42 -5
  27. package/dist/config-validator.js.map +1 -1
  28. package/dist/daemon.d.ts +47 -3
  29. package/dist/daemon.js +380 -60
  30. package/dist/daemon.js.map +1 -1
  31. package/dist/delivery-outbox.d.ts +23 -1
  32. package/dist/delivery-outbox.js +114 -17
  33. package/dist/delivery-outbox.js.map +1 -1
  34. package/dist/doctor.d.ts +3 -0
  35. package/dist/doctor.js +9 -0
  36. package/dist/doctor.js.map +1 -1
  37. package/dist/event-log.d.ts +5 -0
  38. package/dist/event-log.js +13 -0
  39. package/dist/event-log.js.map +1 -1
  40. package/dist/fleet-context.d.ts +1 -1
  41. package/dist/fleet-lock.d.ts +6 -0
  42. package/dist/fleet-lock.js +19 -0
  43. package/dist/fleet-lock.js.map +1 -1
  44. package/dist/fleet-manager.d.ts +174 -2
  45. package/dist/fleet-manager.js +678 -68
  46. package/dist/fleet-manager.js.map +1 -1
  47. package/dist/fleet-process-handlers.d.ts +1 -0
  48. package/dist/fleet-process-handlers.js +9 -1
  49. package/dist/fleet-process-handlers.js.map +1 -1
  50. package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +11 -0
  51. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +4 -4
  52. package/dist/general-knowledge/skills/web-appearance/SKILL.md +7 -7
  53. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +109 -57
  54. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +110 -58
  55. package/dist/guest-seen-people.d.ts +38 -0
  56. package/dist/guest-seen-people.js +78 -0
  57. package/dist/guest-seen-people.js.map +1 -0
  58. package/dist/instance-config-impact.js +4 -1
  59. package/dist/instance-config-impact.js.map +1 -1
  60. package/dist/instance-lifecycle.d.ts +2 -0
  61. package/dist/instance-lifecycle.js +27 -1
  62. package/dist/instance-lifecycle.js.map +1 -1
  63. package/dist/locale.js +62 -12
  64. package/dist/locale.js.map +1 -1
  65. package/dist/log-service.js +1 -0
  66. package/dist/log-service.js.map +1 -1
  67. package/dist/mac-auto-restore.d.ts +117 -0
  68. package/dist/mac-auto-restore.js +294 -0
  69. package/dist/mac-auto-restore.js.map +1 -0
  70. package/dist/needs-you.js +1 -0
  71. package/dist/needs-you.js.map +1 -1
  72. package/dist/outbound-handlers.d.ts +2 -2
  73. package/dist/outbound-handlers.js +5 -5
  74. package/dist/outbound-handlers.js.map +1 -1
  75. package/dist/outbound-schemas.d.ts +2 -0
  76. package/dist/outbound-schemas.js +2 -0
  77. package/dist/outbound-schemas.js.map +1 -1
  78. package/dist/public-web-gateway.js +9 -1
  79. package/dist/public-web-gateway.js.map +1 -1
  80. package/dist/search/search-index.d.ts +10 -2
  81. package/dist/search/search-index.js +17 -4
  82. package/dist/search/search-index.js.map +1 -1
  83. package/dist/service-installer.d.ts +23 -1
  84. package/dist/service-installer.js +40 -11
  85. package/dist/service-installer.js.map +1 -1
  86. package/dist/session-oom-stop.d.ts +45 -0
  87. package/dist/session-oom-stop.js +259 -0
  88. package/dist/session-oom-stop.js.map +1 -0
  89. package/dist/session-oom.d.ts +13 -0
  90. package/dist/session-oom.js +119 -7
  91. package/dist/session-oom.js.map +1 -1
  92. package/dist/settings-api.d.ts +27 -1
  93. package/dist/settings-api.js +108 -2
  94. package/dist/settings-api.js.map +1 -1
  95. package/dist/settings-change.js +37 -3
  96. package/dist/settings-change.js.map +1 -1
  97. package/dist/settings-effect.js +5 -2
  98. package/dist/settings-effect.js.map +1 -1
  99. package/dist/spawn-gate.d.ts +2 -0
  100. package/dist/spawn-gate.js +17 -4
  101. package/dist/spawn-gate.js.map +1 -1
  102. package/dist/start-attempt.d.ts +15 -1
  103. package/dist/start-attempt.js +35 -5
  104. package/dist/start-attempt.js.map +1 -1
  105. package/dist/start-failure-chat.d.ts +41 -0
  106. package/dist/start-failure-chat.js +159 -0
  107. package/dist/start-failure-chat.js.map +1 -0
  108. package/dist/start-failure-notice.d.ts +4 -0
  109. package/dist/start-failure-notice.js +8 -0
  110. package/dist/start-failure-notice.js.map +1 -1
  111. package/dist/start-failure.d.ts +49 -0
  112. package/dist/start-failure.js +141 -1
  113. package/dist/start-failure.js.map +1 -1
  114. package/dist/telegram-api-root.d.ts +1 -0
  115. package/dist/telegram-api-root.js +40 -0
  116. package/dist/telegram-api-root.js.map +1 -0
  117. package/dist/turn-reply-guard.d.ts +25 -0
  118. package/dist/turn-reply-guard.js +111 -12
  119. package/dist/turn-reply-guard.js.map +1 -1
  120. package/dist/types.d.ts +21 -3
  121. package/dist/ui/backend-signin.js +5 -2
  122. package/dist/ui/chat-ambient.js +1 -1
  123. package/dist/ui/chat-effects.js +4 -4
  124. package/dist/ui/chat-render.js +5 -2
  125. package/dist/ui/chat-strings.js +2 -2
  126. package/dist/ui/chat-thread.js +2 -2
  127. package/dist/ui/first-run.js +2 -2
  128. package/dist/ui/fleet-canvas-view.js +114 -0
  129. package/dist/ui/fleet-canvas.js +15 -77
  130. package/dist/ui/fleet-office-editor.js +80 -5
  131. package/dist/ui/fleet-office-layout.js +197 -0
  132. package/dist/ui/fleet-office-model.js +6 -3
  133. package/dist/ui/fleet-office-pack.d.ts +39 -0
  134. package/dist/ui/fleet-office-pack.js +240 -29
  135. package/dist/ui/fleet-office-skin.js +130 -21
  136. package/dist/ui/fleet-office-store.js +75 -4
  137. package/dist/ui/fleet-office-view.js +154 -0
  138. package/dist/ui/fleet-office-zones.js +95 -33
  139. package/dist/ui/fleet-office.js +530 -94
  140. package/dist/ui/fleet-usage.js +7 -6
  141. package/dist/ui/panel-chat.js +20 -7
  142. package/dist/ui/panel-details.js +2 -2
  143. package/dist/ui/panel-fleet.js +11 -77
  144. package/dist/ui/panel-log.js +5 -5
  145. package/dist/ui/panel-settings.js +586 -164
  146. package/dist/ui/search-dialog.js +26 -7
  147. package/dist/ui/settings-appearance.js +82 -29
  148. package/dist/ui/settings-apply.js +42 -7
  149. package/dist/ui/settings-dialogs.js +68 -22
  150. package/dist/ui/settings-draft.js +51 -0
  151. package/dist/ui/settings-guests.js +244 -78
  152. package/dist/ui/settings-learn.js +2 -2
  153. package/dist/ui/settings-model.js +2 -0
  154. package/dist/ui/settings-registry.js +142 -0
  155. package/dist/ui/settings-strings.js +108 -44
  156. package/dist/ui/settings-web-authority.js +34 -23
  157. package/dist/ui/shared/app-i18n.js +14 -0
  158. package/dist/ui/shared/app-nav.js +10 -5
  159. package/dist/ui/shared/app-prefs.js +80 -2
  160. package/dist/ui/shared/app-route.js +39 -5
  161. package/dist/ui/shared/app-shell.js +80 -8
  162. package/dist/ui/shared/app-stream.js +2 -1
  163. package/dist/ui/shared/app.css +251 -20
  164. package/dist/ui/shared/app.js +3 -0
  165. package/dist/ui/shared/effect-stock-trust.js +1 -1
  166. package/dist/ui/shared/identity-color.js +4 -4
  167. package/dist/ui/shared/signin.js +2 -2
  168. package/dist/ui/shared/ui-dialog.js +6 -1
  169. package/dist/ui/shared/ui-icons.js +2 -0
  170. package/dist/ui/shared/usage-dialog.js +21 -11
  171. package/dist/ui/shared/view-strings.js +2 -0
  172. package/dist/ui/shared/web-authorize.js +13 -12
  173. package/dist/web-api.d.ts +9 -3
  174. package/dist/web-api.js +161 -6
  175. package/dist/web-api.js.map +1 -1
  176. package/dist/web-auth.js +7 -1
  177. package/dist/web-auth.js.map +1 -1
  178. package/dist/web-experimental.d.ts +1 -1
  179. package/dist/web-experimental.js +1 -1
  180. package/dist/web-experimental.js.map +1 -1
  181. package/dist/web-mod-agent.js +2 -2
  182. package/dist/web-mod-agent.js.map +1 -1
  183. package/dist/web-office-skins.d.ts +108 -0
  184. package/dist/web-office-skins.js +298 -0
  185. package/dist/web-office-skins.js.map +1 -0
  186. package/dist/web-office-store.d.ts +8 -2
  187. package/dist/web-office-store.js +37 -7
  188. package/dist/web-office-store.js.map +1 -1
  189. package/dist/web-settings-patch.d.ts +6 -0
  190. package/dist/web-settings-patch.js +61 -4
  191. package/dist/web-settings-patch.js.map +1 -1
  192. package/dist/web-shell-routes.d.ts +4 -2
  193. package/dist/web-shell-routes.js +17 -7
  194. package/dist/web-shell-routes.js.map +1 -1
  195. package/dist/working-recovery.d.ts +104 -0
  196. package/dist/working-recovery.js +263 -0
  197. package/dist/working-recovery.js.map +1 -0
  198. package/dist/working-state-journal.d.ts +70 -0
  199. package/dist/working-state-journal.js +106 -0
  200. package/dist/working-state-journal.js.map +1 -0
  201. package/package.json +1 -1
@@ -11,9 +11,12 @@ import { navigate } from "/assets/app-nav.js";
11
11
  import { chatPath, fleetPath } from "/assets/app-route.js";
12
12
  import { namesOf, namesStore } from "/assets/instance-nav.js";
13
13
  import { Icon } from "/assets/ui-icons.js";
14
+ import { goToSetting, searchSettings, settingLabel } from "./settings-registry.js";
15
+ import "./settings-strings.js";
14
16
 
15
17
  register("search", {
16
18
  en: {
19
+ settingsGroup: "Settings", settingsWhere: "Settings › {0}",
17
20
  label: "Search", placeholder: "Search chats, messages and tasks…", close: "Close", filters: "Filters", filtersOn: "Filters ({0})",
18
21
  hint: "Find who you asked about something: a word or two from the chat, a message between agents or a task. Short words (1–2 characters) work too.",
19
22
  recent: "Recent searches", clearRecent: "Clear",
@@ -32,6 +35,7 @@ register("search", {
32
35
  notFound: "No longer found — it may have been removed.", from: "{0} → {1}", groupCount: "{0} results",
33
36
  },
34
37
  "zh-TW": {
38
+ settingsGroup: "設定", settingsWhere: "設定 › {0}",
35
39
  label: "搜尋", placeholder: "搜尋聊天、訊息與任務…", close: "關閉", filters: "篩選", filtersOn: "篩選({0})",
36
40
  hint: "找回某件事當初交給誰:輸入聊天、agent 之間訊息或任務裡的一兩個詞。1–2 個字的短詞也可以。",
37
41
  recent: "最近的搜尋", clearRecent: "清除",
@@ -142,6 +146,9 @@ export function jumpToMessage(instance, key, w = window) {
142
146
 
143
147
  const errorText = (status) => (status === 404 ? t("search.unavailable") : status === 503 ? t("search.busy") : status === 401 ? t("search.signedOut") : t("search.failed"));
144
148
 
149
+ /** #2347 §6: how many settings the dialog lists above the messages (the Settings page's own search lists them all). */
150
+ const SETTINGS_SHOWN = 5;
151
+
145
152
  export function SearchDialog({ preset = {}, onClose }) {
146
153
  const ref = useRef(null), input = useRef(null), list = useRef(null);
147
154
  const app = useStore(appStore);
@@ -211,7 +218,9 @@ export function SearchDialog({ preset = {}, onClose }) {
211
218
  const label = (name) => (byName.has(name) ? namesOf(byName.get(name), names.primary).primary : name);
212
219
  // While a new query runs, the last answer stays (dimmed) rather than flashing empty.
213
220
  const groups = useMemo(() => groupHits(res.hits || []), [res]);
214
- const flat = useMemo(() => flatHits(groups), [groups]);
221
+ // #2347 §6: the settings matching the query, from the page's own registry (no request), above the messages.
222
+ const settings = useMemo(() => (q.trim() ? searchSettings(q).slice(0, SETTINGS_SHOWN) : []), [q]);
223
+ const flat = useMemo(() => [...settings.map((entry) => ({ setting: entry })), ...flatHits(groups)], [settings, groups]);
215
224
  const activeFilters = (f.instance ? 1 : 0) + (f.team ? 1 : 0) + (f.since !== "any" ? 1 : 0) + (f.sources.length < SOURCES.length ? 1 : 0) + (f.mine ? 1 : 0);
216
225
 
217
226
  // The active row stays in view.
@@ -222,6 +231,7 @@ export function SearchDialog({ preset = {}, onClose }) {
222
231
 
223
232
  const open = (hit) => {
224
233
  if (!hit) return;
234
+ if (hit.setting) { rememberSearch(q); onClose(); goToSetting(hit.setting); return; }
225
235
  rememberSearch(q); setRecent(readRecent());
226
236
  const key = hit.source === "chat" ? messageKeyOf(hit) : null;
227
237
  if (key && hit.live) { onClose(); jumpToMessage(hit.instance, key); return; }
@@ -286,31 +296,40 @@ export function SearchDialog({ preset = {}, onClose }) {
286
296
  </div>` : null}
287
297
  <div class="search-body" ref=${list}>
288
298
  ${detail ? html`<${Detail} detail=${detail} q=${q} label=${label} onBack=${() => { setDetail(null); input.current && input.current.focus(); }} onClose=${onClose} />`
289
- : html`<${Results} q=${q} res=${res} groups=${groups} active=${active} setActive=${setActive} open=${open} label=${label} filtered=${activeFilters > 0}
299
+ : html`<${Results} q=${q} res=${res} groups=${groups} settings=${settings} active=${active} setActive=${setActive} open=${open} label=${label} filtered=${activeFilters > 0}
290
300
  recent=${recent} pick=${(s) => { recordEdit(input.current); setQ(s); input.current && input.current.focus(); }} clearRecent=${() => { forgetRecent(); setRecent([]); }} />`}
291
301
  </div>
292
302
  <div class="search-foot" aria-hidden="true">${t("search.keys")}</div>
293
303
  </div></dialog>`;
294
304
  }
295
305
 
296
- function Results({ q, res, groups, active, setActive, open, label, filtered, recent, pick, clearRecent }) {
306
+ function Results({ q, res, groups, settings = [], active, setActive, open, label, filtered, recent, pick, clearRecent }) {
297
307
  if (!q.trim()) {
298
308
  return html`<div class="search-empty"><p class="search-hint">${t("search.hint")}</p>
299
309
  ${recent.length ? html`<div class="search-recent"><div class="search-recent-head"><span>${t("search.recent")}</span>
300
310
  <button type="button" class="chip-btn" onClick=${clearRecent}>${t("search.clearRecent")}</button></div>
301
311
  <ul>${recent.map(s => html`<li><button type="button" class="search-recent-item" onClick=${() => pick(s)}><${Icon} name="clock" size=${14} /><span>${s}</span></button></li>`)}</ul></div>` : null}</div>`;
302
312
  }
303
- if (res.status === "error") return html`<p class="search-status bad" role="alert">${res.error}</p>`;
304
- const hits = res.hits || [];
313
+ // #2383 review: the settings are this page's own — shown (and reachable) whatever the message search answered.
314
+ const failed = res.status === "error";
315
+ const hits = failed ? [] : res.hits || [];
305
316
  return html`
306
317
  ${res.building ? html`<p class="search-note" role="status">${t("search.building")}</p>` : null}
307
318
  ${res.limited ? html`<p class="search-note" role="status">${t("search.limited")}</p>` : null}
308
319
  ${res.status === "loading" && !hits.length ? html`<p class="search-status" role="status">${t("search.searching")}</p>` : null}
309
- ${res.status === "ok" && !hits.length ? html`<p class="search-status" role="status">${filtered ? t("search.noneFiltered") : t("search.none")}</p>` : null}
320
+ ${failed ? html`<p class="search-status bad" role="alert">${res.error}</p>` : null}
321
+ ${res.status === "ok" && !hits.length && !settings.length ? html`<p class="search-status" role="status">${filtered ? t("search.noneFiltered") : t("search.none")}</p>` : null}
310
322
  <div id="search-results" role="listbox" aria-label=${t("search.results")} class=${`search-results${res.status === "loading" ? " stale" : ""}`}>
323
+ ${settings.length ? html`<div role="group" class="search-group search-settings" aria-label=${t("search.settingsGroup")}>
324
+ <div class="search-group-head" aria-hidden="true"><span>${t("search.settingsGroup")}</span><span class="search-count">${t("search.groupCount", settings.length)}</span></div>
325
+ ${settings.map((entry, i) => html`<div id=${`search-opt-${i}`} role="option" aria-selected=${i === active ? "true" : "false"} class=${`search-row${i === active ? " on" : ""}`}
326
+ onMouseMove=${i === active ? undefined : () => setActive(i)} onClick=${() => open({ setting: entry })}>
327
+ <div class="search-row-top"><span class="search-badge setting">${t("search.settingsGroup")}</span><span class="search-who">${t("search.settingsWhere", t(`settings.sec_${entry.section}`))}</span></div>
328
+ <div class="search-snippet">${settingLabel(entry)}${entry.key ? html` <code class="s-find-key">${entry.key}</code>` : null}</div></div>`)}
329
+ </div>` : null}
311
330
  ${groups.map(g => html`<div role="group" class="search-group" aria-label=${label(g.instance)}>
312
331
  <div class="search-group-head" aria-hidden="true"><span>${label(g.instance)}</span><span class="search-count">${t("search.groupCount", g.hits.length)}</span></div>
313
- ${g.hits.map(({ hit, index }) => html`<${Row} hit=${hit} index=${index} on=${index === active} label=${label} onHover=${() => setActive(index)} onOpen=${() => open(hit)} />`)}
332
+ ${g.hits.map(({ hit, index }) => html`<${Row} hit=${hit} index=${index + settings.length} on=${index + settings.length === active} label=${label} onHover=${() => setActive(index + settings.length)} onOpen=${() => open(hit)} />`)}
314
333
  </div>`)}
315
334
  </div>`;
316
335
  }
@@ -16,7 +16,8 @@ import { followsReducedMotion, onMotionChange, setFollowsReducedMotion } from "/
16
16
  import { CARD_SIZES, cardSizeDefault, cardSizeStore, setCardSizeDefault } from "/assets/identity-card.js";
17
17
  import { AgentWidget, checkWidget, fleetBinds, setSidebarWidget, sidebarWidgetRaw, sidebarWidgetStore, widgetProblem } from "/assets/agent-widget.js";
18
18
  import { confirmedWrite } from "./settings-confirm.js";
19
- import { savePref } from "/assets/app-prefs.js";
19
+ import { savePref, withoutHold } from "/assets/app-prefs.js";
20
+ import { DEFAULT_SCHEMA, impactOf, ownChoiceImpact } from "./settings-model.js";
20
21
  import { confirmDialog } from "/assets/ui-confirm.js";
21
22
  import { colorsOff, defaultPlaces, identityClasses, identityOf, PLACEMENTS, paintIdentity, setColorsOff, setDefaultPlaces, useIdentityColors } from "/assets/identity-color.js";
22
23
  import { CardTitle } from "./settings-learn.js";
@@ -52,26 +53,67 @@ function refusal(body) {
52
53
  return known.includes(problem) ? tn(`cssError_${problem}`, detail || "") : tn("cssError_other", problem || "?");
53
54
  }
54
55
 
55
- export function AppearanceSection() {
56
+ /**
57
+ * #2347: in Settings, a change here is saved by the bar's Save like every Settings change. The synced preferences are
58
+ * held by app-prefs while Settings is open (the panel stages them); `stage`/`unstage`/`staged` (the panel's) carry the
59
+ * rest: this device's own switches and the fleet's Canvas Worker switch. Without them (shown on its own) each control
60
+ * saves at once, as before.
61
+ */
62
+ export function AppearanceSection(props = {}) {
63
+ const part = props.part || "look";
56
64
  const publicLink = typeof document !== "undefined" && document.body && document.body.dataset.publicLink === "1";
57
- if (publicLink) return html`<section class="card s-appearance"><h3>${tn("cssTitle")}</h3><p class="sub">${tn("cssPublicLink")}</p></section>`;
65
+ // #2347 regrouping: Look (the person's own), this device's switches (in Preferences) and the host-wide cards (in Web
66
+ // dashboard) are drawn where each belongs. None of it is offered over the public link (Look says so).
67
+ if (publicLink) return part === "look" ? html`<section class="card s-appearance"><h3>${tn("cssTitle")}</h3><p class="sub">${tn("cssPublicLink")}</p></section>` : null;
68
+ if (part === "device") return html`<${MotionCard} ...${props} /><${DeviceCard} ...${props} />`;
69
+ if (part === "dashboard") return html`<${AppIconCard} /><${CardLimitsCard} /><${ExperimentalEffectsCard} ...${props} />`;
58
70
  const group = (key) => html`<h2 class="s-ap-group">${tn(key)}</h2>`;
59
- return html`${group("groupLook")}<${ThemesCard} /><${IdentityColorCard} /><${CardSizeCard} /><${AppIconCard} />
71
+ return html`${group("groupLook")}<${ThemesCard} ...${props} /><${IdentityColorCard} /><${CardSizeCard} />
60
72
  ${group("groupLayout")}<${SidebarWidgetCard} />
61
- ${group("groupCss")}<${OwnerCssCard} /><${DeviceCard} />
73
+ ${group("groupCss")}<${OwnerCssCard} />
62
74
  ${group("groupMods")}<${ModsCard} /><${EffectsCard} />
63
- ${group("groupAdvanced")}<${ExperimentalEffectsCard} /><${CardLimitsCard} /><${RestoreDefaultsCard} />`;
75
+ <${RestoreDefaultsCard} ...${props} />`;
76
+ }
77
+
78
+ /** #2347: the staged keys of this device's own switches (Restore defaults sets them, so their staged change goes). */
79
+ const DEVICE_CHOICES = ["device:fx-follow-motion", "device:owner-css-off"];
80
+ /**
81
+ * #2347: a switch kept on this device only — shown at once; in Settings staged under `key` with what it was before, so
82
+ * Discard (or leaving Settings) puts that back. Back to that value: no change.
83
+ */
84
+ function deviceChoice(props, key, label, read, write) {
85
+ return (value) => {
86
+ const prev = props.staged && props.staged.get(key);
87
+ const from = prev ? prev.from : read();
88
+ write(value);
89
+ if (!props.stage) return;
90
+ if (value === from) props.unstage(key);
91
+ else props.stage(key, { label: tn("youDeviceChoice", label), impact: ownChoiceImpact(props.schema), from, local: { commit: () => {}, revert: () => write(from) } }, true);
92
+ };
64
93
  }
65
94
 
66
95
  register("experimentalEffects", {
67
96
  en: { title: "Experimental: Canvas Worker effects", help: "Enable uploaded Canvas Worker effects. Ambient effects are always available. Saved effects stay intact when this is off.", failed: "Could not change this setting. Try again." },
68
97
  "zh-TW": { title: "實驗功能:Canvas Worker 特效", help: "啟用上傳的 Canvas Worker 特效。氛圍特效不受影響;關閉時保留已儲存的特效。", failed: "無法變更此設定,請重試。" },
69
98
  });
70
- function ExperimentalEffectsCard() {
99
+ const CANVAS_WORKER = "fleet:experimental.canvas_worker";
100
+ function ExperimentalEffectsCard(props = {}) {
71
101
  const enabled = useCanvasWorker(), [writable, setWritable] = useState(false), [busy, setBusy] = useState(false), [failed, setFailed] = useState(false);
72
102
  useEffect(() => { let live = true; Promise.resolve(globalThis.AgendAuth?.trialSession?.()).then(value => { if (live) setWritable(value?.localAdmin === true); }, () => {}); return () => { live = false; }; }, []);
73
103
  if (!writable) return null;
104
+ // #2347: in Settings it is a fleet change like any other — staged here, written by the bar's Save (this page's switch
105
+ // follows once it applied).
106
+ const pending = props.staged && props.staged.get(CANVAS_WORKER);
107
+ const shown = pending ? pending.draft : enabled;
108
+ const stageIt = (value) => {
109
+ setFailed(false);
110
+ if (value === enabled) { props.unstage(CANVAS_WORKER); return; }
111
+ props.stage(CANVAS_WORKER, { label: t("experimentalEffects.title"), impact: impactOf(props.schema || DEFAULT_SCHEMA, "web.experimental.canvas_worker"), draft: value,
112
+ request: { method: "PUT", url: "/api/settings/fleet/web", body: { experimental: { canvas_worker: value } } },
113
+ onResult: (outcome) => { if (outcome && outcome.status === "applied") appStore.set({ canvasWorker: value }); else if (outcome && outcome.status === "failed") setFailed(true); } }, true);
114
+ };
74
115
  const change = async value => {
116
+ if (props.stage) { stageIt(value); return; }
75
117
  setBusy(true); setFailed(false);
76
118
  try {
77
119
  const result = await confirmedWrite("/api/settings/fleet/web", { method: "PUT", body: { experimental: { canvas_worker: value } }, label: t("experimentalEffects.title") });
@@ -79,7 +121,7 @@ function ExperimentalEffectsCard() {
79
121
  } catch { setFailed(true); }
80
122
  finally { setBusy(false); }
81
123
  };
82
- return html`<section class="card s-appearance s-experimental-effects"><label class="check"><input type="checkbox" checked=${enabled} disabled=${busy} onChange=${event => change(event.target.checked)} /><span>${t("experimentalEffects.title")}</span></label><p class="sub">${t("experimentalEffects.help")}</p>${failed ? html`<p role="alert">${t("experimentalEffects.failed")}</p>` : null}</section>`;
124
+ return html`<section class="card s-appearance s-experimental-effects"><label class="check"><input type="checkbox" checked=${shown} disabled=${busy} onChange=${event => change(event.target.checked)} /><span>${t("experimentalEffects.title")}</span></label><p class="sub">${t("experimentalEffects.help")}</p>${failed ? html`<p role="alert">${t("experimentalEffects.failed")}</p>` : null}</section>`;
83
125
  }
84
126
 
85
127
  /** #2056 B: a sample sidebar row in the host's default colour places, so the choice is seen rather than described. */
@@ -118,23 +160,27 @@ function IdentityColorCard() {
118
160
  * fleet's card limits.
119
161
  */
120
162
  export async function restoreAppearanceDefaults() {
121
- setThemesShown(true);
122
- setFollowsReducedMotion(false);
123
- setCardSizeDefault("m");
124
- savePref("card.sizeByInstance", null);
125
- setSidebarWidget(null);
126
- setDefaultPlaces(null);
127
- setColorsOff(false);
128
- setOwnerCssOffHere(false);
163
+ // #2347: a confirmed action — it saves at once, Settings open or not (nothing it sets stays held).
164
+ withoutHold(() => {
165
+ setThemesShown(true);
166
+ setFollowsReducedMotion(false);
167
+ setCardSizeDefault("m");
168
+ savePref("card.sizeByInstance", null);
169
+ setSidebarWidget(null);
170
+ setDefaultPlaces(null);
171
+ setColorsOff(false);
172
+ setOwnerCssOffHere(false);
173
+ });
129
174
  const r = await postJson("/ui/owner-css/restore", { sha: null });
130
175
  return r && r.error ? { ok: false, error: r.error } : { ok: true };
131
176
  }
132
- function RestoreDefaultsCard() {
177
+ function RestoreDefaultsCard(props = {}) {
133
178
  const [busy, setBusy] = useState(false);
134
179
  async function go() {
135
180
  if (!(await confirmDialog({ title: tn("restoreTitle"), message: tn("restoreConfirm"), confirmLabel: tn("restoreGo"), danger: true }))) return;
136
181
  setBusy(true);
137
182
  const r = await restoreAppearanceDefaults();
183
+ if (props.unstage) for (const key of DEVICE_CHOICES) props.unstage(key); // saved by the restore itself
138
184
  setBusy(false);
139
185
  if (r.ok) toast(tn("restoreDone")); else toast(tn("restoreCssFailed", r.error), false);
140
186
  }
@@ -148,7 +194,7 @@ function RestoreDefaultsCard() {
148
194
  function CardSizeCard() {
149
195
  useStore(cardSizeStore);
150
196
  const size = cardSizeDefault();
151
- return html`<section class="card s-appearance s-synced"><${CardTitle} title=${tn("cardSizeTitle")} topic="cards" />
197
+ return html`<section class="card s-appearance s-synced s-card-size"><${CardTitle} title=${tn("cardSizeTitle")} topic="cards" />
152
198
  <div class="card-size" role="group" aria-label=${tn("cardSizeTitle")}>
153
199
  ${CARD_SIZES.map((s) => html`<button key=${s} type="button" class=${`seg-item${s === size ? " active" : ""}`} aria-pressed=${s === size ? "true" : "false"}
154
200
  onClick=${() => setCardSizeDefault(s)}>${t(`card.size${s.toUpperCase()}Name`)}</button>`)}
@@ -306,7 +352,7 @@ function CardLimitsCard() {
306
352
  };
307
353
  useEffect(() => { load(); }, []);
308
354
  if (!view) return null;
309
- if (view.error) return html`<section class="card s-appearance"><${CardTitle} title=${tn("cardLimitsTitle")} topic="limits" /><p class="sub">${tn("cardLimitsUnavailable")}</p></section>`;
355
+ if (view.error) return html`<section class="card s-appearance s-card-limits"><${CardTitle} title=${tn("cardLimitsTitle")} topic="limits" /><p class="sub">${tn("cardLimitsUnavailable")}</p></section>`;
310
356
  const configured = (g, k) => (view.configured && view.configured[g] && typeof view.configured[g][k] === "number" ? view.configured[g][k] : null);
311
357
  const save = async () => {
312
358
  const body = {};
@@ -345,16 +391,10 @@ function CardLimitsCard() {
345
391
  /** #1629 r2 (d): whether this person's screens show agents' themes (and, as they come, effects) — synced. */
346
392
  function ThemesCard() {
347
393
  const s = useStore(themesStore);
348
- // #2117: this device's own switch (not synced): effects play unless it follows the system's reduced-motion setting.
349
- const [follow, setFollow] = useState(followsReducedMotion);
350
- useEffect(() => onMotionChange(() => setFollow(followsReducedMotion())), []);
351
- return html`<section class="card s-appearance s-synced"><${CardTitle} title=${tn("themesTitle")} topic="themes" />
352
- <label class="check"><input type="checkbox" checked=${s.shown} disabled=${safeMode()} onChange=${(e) => setThemesShown(e.target.checked)} />
394
+ return html`<section class="card s-appearance s-synced s-themes"><${CardTitle} title=${tn("themesTitle")} topic="themes" />
395
+ <label class="check"><input id="fx-show-themes" type="checkbox" checked=${s.shown} disabled=${safeMode()} onChange=${(e) => setThemesShown(e.target.checked)} />
353
396
  <span>${tn("themesShow")}</span></label>
354
397
  <p class="sub">${safeMode() ? tn("themesSafeMode") : tn("themesShowLead")}</p>
355
- <label class="check"><input type="checkbox" id="fx-follow-reduced-motion" checked=${follow} onChange=${(e) => setFollowsReducedMotion(e.target.checked)} />
356
- <span>${tn("themesFollowMotion")}</span></label>
357
- <p class="sub">${tn("themesFollowMotionLead")}</p>
358
398
  </section>`;
359
399
  }
360
400
 
@@ -499,10 +539,23 @@ function OwnerCssCard() {
499
539
  </section>`;
500
540
  }
501
541
 
502
- function DeviceCard() {
542
+ /** #2117: this device's own switch (not synced): effects play unless it follows the system's reduced-motion setting. */
543
+ function MotionCard(props = {}) {
544
+ const [follow, setFollow] = useState(followsReducedMotion);
545
+ useEffect(() => onMotionChange(() => setFollow(followsReducedMotion())), []);
546
+ const setFollowHere = deviceChoice(props, DEVICE_CHOICES[0], tn("themesFollowMotion"), followsReducedMotion, setFollowsReducedMotion);
547
+ return html`<section class="card s-appearance s-local s-motion"><h3>${tn("motionTitle")}</h3>
548
+ <label class="check"><input type="checkbox" id="fx-follow-reduced-motion" checked=${follow} onChange=${(e) => setFollowHere(e.target.checked)} />
549
+ <span>${tn("themesFollowMotion")}</span></label>
550
+ <p class="sub">${tn("themesFollowMotionLead")}</p>
551
+ </section>`;
552
+ }
553
+
554
+ function DeviceCard(props = {}) {
503
555
  const [off, setOff] = useState(ownerCssOffHere());
556
+ const setOffHere = deviceChoice(props, DEVICE_CHOICES[1], tn("cssDeviceOff"), ownerCssOffHere, (v) => { setOwnerCssOffHere(v); setOff(ownerCssOffHere()); });
504
557
  return html`<section class="card s-appearance s-local"><h3>${tn("cssDeviceTitle")}</h3>
505
- <label class="check"><input type="checkbox" checked=${off} onChange=${(e) => { setOwnerCssOffHere(e.target.checked); setOff(ownerCssOffHere()); }} />
558
+ <label class="check"><input id="css-off-here" type="checkbox" checked=${off} onChange=${(e) => setOffHere(e.target.checked)} />
506
559
  <span>${tn("cssDeviceOff")}</span></label>
507
560
  <p class="sub">${tn("cssSafe")}</p>
508
561
  </section>`;
@@ -13,6 +13,12 @@ import { toast } from "/assets/ui-toast.js";
13
13
  import "./settings-strings.js";
14
14
  import { api, confirmedWrite, newKey, selfConfirmed } from "./settings-confirm.js";
15
15
 
16
+ /**
17
+ * #2347 (leader's default, for the user to confirm): a Save that needs AgEnD restarted offers Restart now (its card, and
18
+ * the app-wide banner) and never restarts on its own. true restarts once such a Save settles cleanly.
19
+ */
20
+ export const AUTO_RESTART_AFTER_SAVE = false;
21
+
16
22
  const WATCH_LIMIT_MS = 10 * 60_000;
17
23
  const POST_RETRIES = [1000, 2000, 4000];
18
24
  const now = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
@@ -26,7 +32,8 @@ function view() {
26
32
  if (!op) return null;
27
33
  return {
28
34
  id: op.id, phase: op.phase, error: op.error, job: op.job, lostJob: op.lostJob, restart: op.restart,
29
- steps: op.steps.map(({ label, impact, status, error, pendingId, note }) => ({ label, impact, status, error: error || null, pendingId: pendingId || null, ...(note ? { note } : {}) })),
35
+ // stageKey: which staged change a step is (#2367 review: the panel tells that change's section how it went).
36
+ steps: op.steps.map(({ label, impact, status, error, pendingId, note, stageKey }) => ({ label, impact, status, error: error || null, pendingId: pendingId || null, ...(note ? { note } : {}), ...(stageKey ? { stageKey } : {}) })),
30
37
  leftover: op.leftover,
31
38
  };
32
39
  }
@@ -199,24 +206,29 @@ async function watch(o, id) {
199
206
  const rows = (current && current.targets) || [];
200
207
  const failed = rows.some((r) => r.status === "failed"), restart = rows.some((r) => r.status === "restart-required");
201
208
  if (!o.lostJob) toast(t(failed ? "settings.applyPartial" : restart ? "settings.applyRestartNeeded" : "settings.changesApplied"), !(failed || restart));
209
+ if (AUTO_RESTART_AFTER_SAVE && restart && !failed && !o.lostJob && o.restart === "idle") restartFleet();
202
210
  }
203
211
 
204
212
  /**
205
213
  * A full AgEnD restart for a job that needs one: its own confirmation (the caller's), its own key, one at a time. The
206
214
  * job's fleet row moves to "running" on the server; that job is watched again.
207
215
  */
216
+ /** POST /api/settings/restart-fleet for `jobId`, with its own key: { ok, status, body } (a lost answer: status 0). */
217
+ async function postRestart(jobId) {
218
+ const key = newKey("restart");
219
+ try {
220
+ return await api("/api/settings/restart-fleet", { method: "POST", headers: { "Idempotency-Key": key },
221
+ body: JSON.stringify({ job_id: jobId, confirm: "restart-agend", idempotency_key: key }) });
222
+ } catch { return { ok: false, status: 0, body: null }; }
223
+ }
224
+
208
225
  export async function restartFleet(forJob) {
209
226
  const o = op;
210
227
  if (!o || !o.job || o.restart === "busy" || ACTIVE.has(o.phase)) return { ok: false };
211
228
  // A confirmation given for one job never restarts for another (the operation was replaced meanwhile).
212
229
  if (forJob && (forJob.opId !== o.id || forJob.jobId !== o.job.id)) return { ok: false, stale: true };
213
230
  o.restart = "busy"; publish();
214
- const key = newKey("restart");
215
- let res;
216
- try {
217
- res = await api("/api/settings/restart-fleet", { method: "POST", headers: { "Idempotency-Key": key },
218
- body: JSON.stringify({ job_id: o.job.id, confirm: "restart-agend", idempotency_key: key }) });
219
- } catch { res = { ok: false, status: 0, body: null }; }
231
+ const res = await postRestart(o.job.id);
220
232
  if (!mine(o)) return res;
221
233
  if (!res.ok) { o.restart = "idle"; publish(); return res; }
222
234
  toast(t("settings.restartFleetStarted")); // when it is accepted; how it ends is said when it settles
@@ -229,6 +241,29 @@ export async function restartFleet(forJob) {
229
241
  return res;
230
242
  }
231
243
 
244
+ /**
245
+ * #2347: Restart now from the app-wide banner, for the newest Save that needs it (`jobId`, the server's pick). The Save
246
+ * in hand when it is that job. Otherwise the restart is asked for first, and only once it is accepted does that job
247
+ * become the operation, so its card and line follow the restart as they follow any other (a refusal leaves the banner
248
+ * as it was). The caller asked first.
249
+ */
250
+ export async function restartPendingChanges(jobId) {
251
+ if (operationActive()) return { ok: false, busy: true };
252
+ if (op && op.job && op.job.id === jobId) return restartFleet({ opId: op.id, jobId });
253
+ const res = await postRestart(jobId);
254
+ if (!res.ok || operationActive()) return res;
255
+ toast(t("settings.restartFleetStarted"));
256
+ let fresh = null;
257
+ try { fresh = await api(`/api/settings/apply/${encodeURIComponent(jobId)}`); } catch { fresh = null; }
258
+ if (operationActive()) return res;
259
+ const job = fresh && fresh.ok && fresh.body && fresh.body.id ? fresh.body
260
+ : { id: jobId, status: "running", targets: [{ target: "fleet", kind: "restart", status: "running" }] };
261
+ const o = op = { id: ++seq, phase: "watching", error: null, job, lostJob: false, restart: "busy", leftover: [], steps: [] };
262
+ publish();
263
+ try { await watch(o, jobId); } finally { if (mine(o)) { o.restart = "idle"; publish(); } }
264
+ return res;
265
+ }
266
+
232
267
  // Leaving while writes or the POST are still to come would lose them: the browser asks first. Once the job exists,
233
268
  // a reload re-attaches nothing but loses nothing either (the job runs on the server).
234
269
  if (typeof window !== "undefined") {
@@ -14,6 +14,9 @@ import { api, confirmedWrite, newKey } from "./settings-confirm.js";
14
14
  import { adminUserIdError, adminUserIdPattern, botHandle, TokenEnvNote, TokenField, verifyBotToken } from "./settings-token.js";
15
15
  import { DiscordServerPicker } from "./settings-discord.js";
16
16
  import { startOperation } from "./settings-apply.js";
17
+ import { goToTarget } from "./settings-registry.js";
18
+ import { sectionPath } from "/assets/app-route.js";
19
+ import { backendLabel } from "/assets/instance-nav.js";
17
20
  import "./settings-strings.js"; // its words, wherever a picker of this file is used (#2072: Fleet's create dialog too)
18
21
  import { backendChoices, modelChoices, OTHER_MODEL, useBackendRows, useModelCatalog } from "./settings-catalog.js";
19
22
  import {
@@ -69,18 +72,45 @@ export function ModelSelect({ value, onChange, disabled, id, backend, instance,
69
72
  value=${value} onInput=${(e) => onChange(e.target.value)} />` : null}</div>`;
70
73
  }
71
74
  export function Impact({ schema, field }) { return html`<span class="impact">${impactText(impactOf(schema, field))}</span>`; }
72
- export function Label({ text, schema, field, htmlFor }) {
73
- return html`<label for=${htmlFor}>${text}${field ? html` <${Impact} schema=${schema} field=${field} />` : null}</label>`;
75
+ export function Label({ text, schema, field, htmlFor, mark }) {
76
+ return html`<label for=${htmlFor}>${text}${mark ? html` ${mark}` : null}${field ? html` <${Impact} schema=${schema} field=${field} />` : null}</label>`;
74
77
  }
78
+ /** #2347 §10: the mark of a value nobody set — what is in effect is the built-in default. */
79
+ export function DefaultMark() {
80
+ return html`<span class="tag s-default">${tn("defaultMark")}</span>`;
81
+ }
82
+ /** #2347 §10: what an empty field means — the value in effect, in plain text, and where it comes from (the mark, or why). */
83
+ export function InEffect({ text, why }) {
84
+ return html`<p class="s-effect">${tn("inEffect", text)} ${why ? html`<span class="tag">${why}</span>` : html`<${DefaultMark} />`}</p>`;
85
+ }
86
+ /**
87
+ * #2347 §10: what an inherited field inherits — its value, marked "default" when nobody set it there either — and a
88
+ * link to where it is set (Agent defaults; ClassicBot defaults for a room). `from`: { section, target, label }.
89
+ */
90
+ export function Inherited({ text, builtin, from }) {
91
+ const go = (e) => { e.preventDefault(); goToTarget(from.section, from.target); };
92
+ return html`<p class="s-inherited">${tn("inheritedValue", text)}${builtin ? html` <${DefaultMark} />` : null}${from
93
+ ? html` · <a href=${`${sectionPath(from.section)}${from.target || ""}`} onClick=${go}>${tn(from.label)}</a>` : null}</p>`;
94
+ }
95
+ /** A value as the inherited line says it: on/off, minutes (0 = off), a backend's name; anything else as it is. */
96
+ export function valueText(v, kind) {
97
+ if (typeof v === "boolean") return tn(v ? "valueOn" : "valueOff");
98
+ if (kind === "minutes") return Number(v) === 0 ? tn("valueOff") : tn("minutesValue", v);
99
+ if (kind === "backend") return backendLabel(v);
100
+ return String(v);
101
+ }
102
+ /** Where an agent's inherited setting is set: Agent defaults, at its field. */
103
+ export const inAgentDefaults = (target) => ({ section: "agent-defaults", target, label: "changeInDefaults" });
75
104
  export function Feedback({ error, warning }) {
76
105
  if (!error && !warning) return null;
77
106
  return html`<p class=${`feedback${error ? " error" : " warning"}`} role=${error ? "alert" : undefined}>${error ? tn(error) : tn(warning)}</p>`;
78
107
  }
79
108
  /** An override of a default: the control, and "Inherit from defaults" beside it (on: the control shows the default). */
80
- export function Override({ inherit, onInherit, schema, field, children }) {
109
+ export function Override({ inherit, onInherit, schema, field, inherited, children }) {
81
110
  return html`<div class="override">${children}
82
111
  <label class="check inherit-row"><input type="checkbox" checked=${inherit} onChange=${(e) => onInherit(e.target.checked)} /><span>${tn("inheritDefault")}</span>
83
- ${field ? html`<${Impact} schema=${schema} field=${field} />` : null}</label></div>`;
112
+ ${field ? html`<${Impact} schema=${schema} field=${field} />` : null}</label>
113
+ ${inherit && inherited ? html`<${Inherited} ...${inherited} />` : null}</div>`;
84
114
  }
85
115
  export function Drawer({ title, children, open = false }) {
86
116
  return html`<details class="drawer" open=${open}><summary>${title}</summary><div class="drawer-body">${children}</div></details>`;
@@ -253,20 +283,25 @@ export function AgentDialog({ name, inst, ctx, onClose }) {
253
283
  };
254
284
  // What an override shows while it inherits: the default.
255
285
  const shown = (k, own, dflt) => (f.inherit[k] ? dflt : own);
286
+ // #2347 §10: …and says so — the value, "default" when Agent defaults do not set it either, and where it is set.
287
+ const dh = defaults.hang_detector || {};
288
+ const inh = (k, value, target, kind, set = hasOwn(defaults, k)) => ({ text: valueText(value, kind), builtin: !set, from: target ? inAgentDefaults(target) : null });
289
+ const inheritedBackend = !inst.backend && f.backend === (defaults.backend || "claude-code");
256
290
  const L = (text, field, id) => html`<${Label} text=${text} schema=${schema} field=${field} htmlFor=${id} />`;
257
291
  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}>
258
292
  <div class="grid2">
259
293
  <div class="field">${L(tn("displayName"), "instance.display_name", "ag-dn")}
260
- <${Override} inherit=${f.inherit.display_name} onInherit=${setInherit("display_name")}>
294
+ <${Override} inherit=${f.inherit.display_name} onInherit=${setInherit("display_name")} inherited=${inh("display_name", defaults.display_name || name, null, "", !!defaults.display_name)}>
261
295
  <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>
262
296
  <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>
263
- <div class="field">${L(tn("backend"), "instance.backend", "ag-be")}<${BackendSelect} id="ag-be" value=${f.backend} onChange=${set("backend")} /></div>
297
+ <div class="field">${L(tn("backend"), "instance.backend", "ag-be")}<${BackendSelect} id="ag-be" value=${f.backend} onChange=${set("backend")} />
298
+ ${inheritedBackend ? html`<${Inherited} ...${inh("backend", f.backend, "#g-be", "backend")} />` : null}</div>
264
299
  <div class="field">${L(tn("model"), "instance.model", "ag-model")}<${ModelSelect} id="ag-model" value=${f.model} onChange=${set("model")}
265
- backend=${f.backend || defaults.backend || "claude-code"} instance=${name} inherit=${tn("inheritPlaceholder")} /></div>
300
+ backend=${f.backend || defaults.backend || "claude-code"} instance=${name} inherit=${tn("inheritValue", defaults.model || tn("modelCliDefault"))} /></div>
266
301
  </div>
267
302
  <div class="field">${L(tn("workingDir"), "instance.working_directory", "ag-wd")}<input id="ag-wd" type="text" value=${f.working_directory} onInput=${(e) => set("working_directory")(e.target.value)} /></div>
268
303
  <div class="field">${L(`${tn("autoPause")} (${tn("minutes")})`, "instance.auto_pause_after", "ag-auto")}
269
- <${Override} inherit=${f.inherit.auto_pause_after} onInherit=${setInherit("auto_pause_after")}>
304
+ <${Override} inherit=${f.inherit.auto_pause_after} onInherit=${setInherit("auto_pause_after")} inherited=${inh("auto_pause_after", defaults.auto_pause_after ?? 0, "#g-auto", "minutes")}>
270
305
  <input id="ag-auto" type="number" min="0" step="1" placeholder=${tn("zeroDisabled")} class=${errors.auto ? "invalid" : ""} disabled=${f.inherit.auto_pause_after}
271
306
  value=${shown("auto_pause_after", f.auto_pause_after, String(defaults.auto_pause_after ?? 0))} onInput=${(e) => set("auto_pause_after")(e.target.value)} /></${Override}>
272
307
  <${Feedback} error=${errors.auto} /></div>
@@ -274,16 +309,18 @@ export function AgentDialog({ name, inst, ctx, onClose }) {
274
309
  <div class="grid2">
275
310
  <div class="field">${L(tn("channelBinding"), "instance.topic_id", "ag-ch")}<${Select} id="ag-ch" value=${f.channel_id} onChange=${set("channel_id")} options=${["", ...ids]} /></div>
276
311
  <div class="field">${L(`${tn("hangTimeout")} (${tn("minutes")})`, "instance.hang_detector", "ag-hang")}
277
- <${Override} inherit=${f.inherit.hang} onInherit=${setInherit("hang")}>
312
+ <${Override} inherit=${f.inherit.hang} onInherit=${setInherit("hang")}
313
+ inherited=${inh("hang", dh.timeout_minutes ?? HANG_TIMEOUT_DEFAULT_MINUTES, "#g-hang", "minutes", hasOwn(dh, "timeout_minutes"))}>
278
314
  <input id="ag-hang" type="number" min="0.1" step="0.5" class=${errors.hang ? "invalid" : ""} disabled=${f.inherit.hang}
279
315
  value=${shown("hang", f.hang, String((defaults.hang_detector && defaults.hang_detector.timeout_minutes) ?? HANG_TIMEOUT_DEFAULT_MINUTES))} onInput=${(e) => set("hang")(e.target.value)} /></${Override}>
280
316
  <${Feedback} error=${errors.hang} /></div>
281
- ${[["agent_mode", "agentMode", AGENT_MODES, defaults.agent_mode || "mcp"], ["tool_set", "toolSet", TOOL_SETS, toolSetDefault(defaults, inst, name)],
282
- ["tool_progress", "toolProgress", TOOL_PROGRESS, defaults.tool_progress ?? "off"],
283
- ["cross_instance_visibility", "crossInstanceVisibility", VISIBILITY_MODES, visibilityDefault(defaults)],
284
- ["log_level", "logLevel", LOG_LEVELS, defaults.log_level || "info"]].map(([k, label, opts, dflt]) => html`<div key=${k} class="field">
317
+ ${[["agent_mode", "agentMode", AGENT_MODES, defaults.agent_mode || "mcp", "#g-am"], ["tool_set", "toolSet", TOOL_SETS, toolSetDefault(defaults, inst, name), "#g-ts"],
318
+ ["tool_progress", "toolProgress", TOOL_PROGRESS, defaults.tool_progress ?? "off", "#g-tp"],
319
+ ["cross_instance_visibility", "crossInstanceVisibility", VISIBILITY_MODES, visibilityDefault(defaults), "#g-vis"],
320
+ ["log_level", "logLevel", LOG_LEVELS, defaults.log_level || "info", "#g-ll"]].map(([k, label, opts, dflt, target]) => html`<div key=${k} class="field">
285
321
  ${L(tn(label), `instance.${k}`, `ag-${k}`)}
286
- <${Override} inherit=${f.inherit[k]} onInherit=${setInherit(k)}>
322
+ <${Override} inherit=${f.inherit[k]} onInherit=${setInherit(k)}
323
+ inherited=${inh(k, dflt, target, "", k === "cross_instance_visibility" ? VISIBILITY_MODES.includes(defaults[k]) : hasOwn(defaults, k))}>
287
324
  <${Select} id=${`ag-${k}`} value=${shown(k, f[k], dflt)} disabled=${f.inherit[k]} onChange=${set(k)}
288
325
  options=${opts.includes(shown(k, f[k], dflt)) || k === "cross_instance_visibility" ? opts : [shown(k, f[k], dflt), ...opts]} /></${Override}>
289
326
  ${k === "cross_instance_visibility" ? html`<p class="note">${tn("crossInstanceVisibilityHint")}</p>` : null}</div>`)}
@@ -297,16 +334,17 @@ export function AgentDialog({ name, inst, ctx, onClose }) {
297
334
  </select></div>`)}
298
335
  </fieldset>
299
336
  <div class="field">${L(tn("replyCompletionGuard"), "instance.reply_completion_guard")}
300
- <${Override} inherit=${f.inherit.reply_completion_guard} onInherit=${setInherit("reply_completion_guard")}>
337
+ <${Override} inherit=${f.inherit.reply_completion_guard} onInherit=${setInherit("reply_completion_guard")} inherited=${inh("reply_completion_guard", defaults.reply_completion_guard ?? true, "#g-reply")}>
301
338
  <label class="check"><input type="checkbox" disabled=${f.inherit.reply_completion_guard} checked=${shown("reply_completion_guard", f.reply_completion_guard, defaults.reply_completion_guard ?? true)}
302
339
  onChange=${(e) => set("reply_completion_guard")(e.target.checked)} /><span>${tn("replyCompletionGuard")}</span></label></${Override}>
303
340
  <p class="note">${replyHint(f.backend, f.inherit.agent_mode ? defaults.agent_mode || "mcp" : f.agent_mode, kiroUi)}</p></div>
304
341
  <div class="field">${L(tn("lightweight"), "instance.lightweight")}
305
- <${Override} inherit=${f.inherit.lightweight} onInherit=${setInherit("lightweight")}>
342
+ <${Override} inherit=${f.inherit.lightweight} onInherit=${setInherit("lightweight")} inherited=${inh("lightweight", defaults.lightweight ?? false, null)}>
306
343
  <label class="check"><input type="checkbox" disabled=${f.inherit.lightweight} checked=${shown("lightweight", f.lightweight, defaults.lightweight ?? false)}
307
344
  onChange=${(e) => set("lightweight")(e.target.checked)} /><span>${tn("lightweight")}</span></label></${Override}></div>
308
345
  <div class="field">${L(tn("modelFailover"), "instance.model_failover", "ag-fo")}
309
- <${Override} inherit=${f.inherit.model_failover} onInherit=${setInherit("model_failover")}>
346
+ <${Override} inherit=${f.inherit.model_failover} onInherit=${setInherit("model_failover")}
347
+ inherited=${inh("model_failover", (defaults.model_failover && defaults.model_failover[0]) || tn("modelNone"), null, "", !!(defaults.model_failover && defaults.model_failover.length))}>
310
348
  <${ModelSelect} id="ag-fo" disabled=${f.inherit.model_failover} backend=${f.backend || defaults.backend || "claude-code"} instance=${name} inherit=${tn("modelNone")}
311
349
  value=${shown("model_failover", f.model_failover, (defaults.model_failover && defaults.model_failover[0]) || "")} onChange=${set("model_failover")} /></${Override}>
312
350
  <${Feedback} warning=${failoverWarn} /></div>
@@ -642,22 +680,30 @@ export function ClassicDialog({ room, ctx, onClose }) {
642
680
  ctx.stageClassic(c, patch);
643
681
  onClose();
644
682
  };
683
+ // #2347 §10: what a room inherits, from where — ClassicBot defaults when they set it, else Agent defaults ("default"
684
+ // when neither does) — with a link there.
685
+ const roomInh = (k, kind, classicTarget, agentTarget) => {
686
+ const own = hasOwn(cd, k);
687
+ return { text: valueText(inherited[k], kind), builtin: !own && !hasOwn(fd, k),
688
+ from: own ? { section: "chat", target: classicTarget, label: "changeInClassic" } : inAgentDefaults(agentTarget) };
689
+ };
645
690
  const L = (text, field, id) => html`<${Label} text=${text} schema=${schema} field=${field} htmlFor=${id} />`;
646
691
  return html`<${EditDialog} title=${c.name || c.instanceName} subtitle=${`${tn("idLine", c.instanceName)} · ${tn(`exec_${runtime.execution}`)}`} onClose=${onClose} onStage=${stage} ready=${ready} impacts=${Object.keys(pending()).map((k) => `classic.${k}`)} schema=${schema}>
647
692
  <div class="grid2">
648
- <div class="field">${L(tn("backend"), "classic.backend", "cl-be")}<${BackendSelect} id="cl-be" value=${f.backend} onChange=${set("backend")} /></div>
693
+ <div class="field">${L(tn("backend"), "classic.backend", "cl-be")}<${BackendSelect} id="cl-be" value=${f.backend} onChange=${set("backend")} />
694
+ ${!c.backend && f.backend === inherited.backend ? html`<${Inherited} ...${roomInh("backend", "backend", "#g-cbe", "#g-be")} />` : null}</div>
649
695
  <div class="field">${L(tn("model"), "classic.model", "cl-model")}<${ModelSelect} id="cl-model" value=${f.model} onChange=${set("model")}
650
- backend=${f.backend} instance=${c.instanceName} inherit=${inherited.model ? tn("inheritValue", inherited.model) : tn("inheritPlaceholder")} /></div>
696
+ backend=${f.backend} instance=${c.instanceName} inherit=${tn("inheritValue", inherited.model || tn("modelCliDefault"))} /></div>
651
697
  <div class="field">${L(`${tn("autoPause")} (${tn("minutes")})`, "classic.auto_pause_after", "cl-auto")}
652
- <${Override} inherit=${f.inherit.auto_pause_after} onInherit=${setInherit("auto_pause_after")}>
698
+ <${Override} inherit=${f.inherit.auto_pause_after} onInherit=${setInherit("auto_pause_after")} inherited=${roomInh("auto_pause_after", "minutes", "#classicbot", "#g-auto")}>
653
699
  <input id="cl-auto" type="number" min="0" step="1" class=${errors.auto ? "invalid" : ""} disabled=${f.inherit.auto_pause_after}
654
700
  value=${f.inherit.auto_pause_after ? String(inherited.auto_pause_after) : f.auto_pause_after} onInput=${(e) => set("auto_pause_after")(e.target.value)} /></${Override}>
655
701
  <${Feedback} error=${errors.auto} /></div>
656
702
  <div class="field">${L(tn("toolProgress"), "classic.tool_progress", "cl-tp")}
657
- <${Override} inherit=${f.inherit.tool_progress} onInherit=${setInherit("tool_progress")}>
703
+ <${Override} inherit=${f.inherit.tool_progress} onInherit=${setInherit("tool_progress")} inherited=${roomInh("tool_progress", "", "#g-ctp", "#g-tp")}>
658
704
  <${Select} id="cl-tp" disabled=${f.inherit.tool_progress} value=${f.inherit.tool_progress ? inherited.tool_progress : f.tool_progress} onChange=${set("tool_progress")} options=${TOOL_PROGRESS} /></${Override}></div>
659
705
  <div class="field">${L(tn("replyCompletionGuard"), "classic.reply_completion_guard")}
660
- <${Override} inherit=${f.inherit.reply_completion_guard} onInherit=${setInherit("reply_completion_guard")}>
706
+ <${Override} inherit=${f.inherit.reply_completion_guard} onInherit=${setInherit("reply_completion_guard")} inherited=${roomInh("reply_completion_guard", "", "#g-creply", "#g-reply")}>
661
707
  <label class="check"><input type="checkbox" disabled=${f.inherit.reply_completion_guard} checked=${f.inherit.reply_completion_guard ? inherited.reply_completion_guard : f.reply_completion_guard}
662
708
  onChange=${(e) => set("reply_completion_guard")(e.target.checked)} /><span>${tn("replyCompletionGuard")}</span></label></${Override}>
663
709
  <p class="note">${replyHint(f.backend, classicMode, fd.kiro_ui)}</p></div>