@songsid/agend 2.3.2-beta.1 → 2.3.2-beta.10

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 (266) hide show
  1. package/THIRD_PARTY_NOTICES.md +40 -0
  2. package/dist/agent-cli.js +21 -2
  3. package/dist/agent-cli.js.map +1 -1
  4. package/dist/agent-endpoint.js +5 -0
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/antigravity-usage-scan.d.ts +58 -0
  7. package/dist/antigravity-usage-scan.js +257 -0
  8. package/dist/antigravity-usage-scan.js.map +1 -0
  9. package/dist/antigravity-usage-worker.d.ts +1 -0
  10. package/dist/antigravity-usage-worker.js +10 -0
  11. package/dist/antigravity-usage-worker.js.map +1 -0
  12. package/dist/auth-api.js +4 -0
  13. package/dist/auth-api.js.map +1 -1
  14. package/dist/backend/kiro-identity.d.ts +64 -0
  15. package/dist/backend/kiro-identity.js +101 -0
  16. package/dist/backend/kiro-identity.js.map +1 -1
  17. package/dist/backend/kiro-retired-model.d.ts +62 -0
  18. package/dist/backend/kiro-retired-model.js +160 -0
  19. package/dist/backend/kiro-retired-model.js.map +1 -0
  20. package/dist/backend/kiro.d.ts +71 -2
  21. package/dist/backend/kiro.js +352 -11
  22. package/dist/backend/kiro.js.map +1 -1
  23. package/dist/backend/types.d.ts +59 -0
  24. package/dist/backend/types.js.map +1 -1
  25. package/dist/backend-signin-guide.d.ts +1 -0
  26. package/dist/backend-signin-guide.js +9 -0
  27. package/dist/backend-signin-guide.js.map +1 -0
  28. package/dist/backend-signin-pending.d.ts +10 -0
  29. package/dist/backend-signin-pending.js +44 -0
  30. package/dist/backend-signin-pending.js.map +1 -0
  31. package/dist/browser-backend-signins.d.ts +56 -0
  32. package/dist/browser-backend-signins.js +189 -0
  33. package/dist/browser-backend-signins.js.map +1 -0
  34. package/dist/cache-ledger.d.ts +38 -2
  35. package/dist/cache-ledger.js +133 -8
  36. package/dist/cache-ledger.js.map +1 -1
  37. package/dist/cache-prices.d.ts +2 -1
  38. package/dist/cache-prices.js +5 -2
  39. package/dist/cache-prices.js.map +1 -1
  40. package/dist/cache-service.d.ts +68 -0
  41. package/dist/cache-service.js +355 -13
  42. package/dist/cache-service.js.map +1 -1
  43. package/dist/channel/adapters/discord.d.ts +11 -0
  44. package/dist/channel/adapters/discord.js +71 -14
  45. package/dist/channel/adapters/discord.js.map +1 -1
  46. package/dist/channel/mcp-tools.js +29 -2
  47. package/dist/channel/mcp-tools.js.map +1 -1
  48. package/dist/channel/tool-router.js +2 -1
  49. package/dist/channel/tool-router.js.map +1 -1
  50. package/dist/classic-channel-manager.d.ts +11 -0
  51. package/dist/classic-channel-manager.js +39 -1
  52. package/dist/classic-channel-manager.js.map +1 -1
  53. package/dist/cli.js +21 -2
  54. package/dist/cli.js.map +1 -1
  55. package/dist/config-validator.d.ts +7 -1
  56. package/dist/config-validator.js +53 -10
  57. package/dist/config-validator.js.map +1 -1
  58. package/dist/config.d.ts +19 -0
  59. package/dist/config.js +31 -1
  60. package/dist/config.js.map +1 -1
  61. package/dist/cpu-quota.d.ts +23 -0
  62. package/dist/cpu-quota.js +86 -0
  63. package/dist/cpu-quota.js.map +1 -0
  64. package/dist/daemon.d.ts +23 -0
  65. package/dist/daemon.js +106 -7
  66. package/dist/daemon.js.map +1 -1
  67. package/dist/dashboard-permissions.d.ts +20 -6
  68. package/dist/dashboard-permissions.js +58 -9
  69. package/dist/dashboard-permissions.js.map +1 -1
  70. package/dist/doctor.js +2 -1
  71. package/dist/doctor.js.map +1 -1
  72. package/dist/fleet-manager.d.ts +72 -0
  73. package/dist/fleet-manager.js +507 -89
  74. package/dist/fleet-manager.js.map +1 -1
  75. package/dist/forged-envelope-scan.d.ts +9 -0
  76. package/dist/forged-envelope-scan.js +30 -5
  77. package/dist/forged-envelope-scan.js.map +1 -1
  78. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +5 -2
  79. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +14 -1
  80. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +24 -1
  81. package/dist/general-knowledge/skills/web-appearance/SKILL.md +24 -1
  82. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +37 -9
  83. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +44 -11
  84. package/dist/instance-config-impact.js +10 -5
  85. package/dist/instance-config-impact.js.map +1 -1
  86. package/dist/instance-lifecycle.d.ts +19 -0
  87. package/dist/instance-lifecycle.js +55 -12
  88. package/dist/instance-lifecycle.js.map +1 -1
  89. package/dist/kiro-db-reader.d.ts +5 -0
  90. package/dist/kiro-db-reader.js +18 -0
  91. package/dist/kiro-db-reader.js.map +1 -1
  92. package/dist/kiro-engine-status.d.ts +2 -0
  93. package/dist/kiro-engine-status.js +9 -0
  94. package/dist/kiro-engine-status.js.map +1 -1
  95. package/dist/kiro-migrate.d.ts +49 -0
  96. package/dist/kiro-migrate.js +105 -0
  97. package/dist/kiro-migrate.js.map +1 -0
  98. package/dist/kiro-transcript-lane.d.ts +2 -0
  99. package/dist/kiro-transcript-lane.js.map +1 -1
  100. package/dist/kiro-transcript-worker.js +1 -0
  101. package/dist/kiro-transcript-worker.js.map +1 -1
  102. package/dist/kiro-tui-forensic.d.ts +7 -0
  103. package/dist/kiro-tui-forensic.js +110 -0
  104. package/dist/kiro-tui-forensic.js.map +1 -0
  105. package/dist/kiro-usage-run.d.ts +7 -0
  106. package/dist/kiro-usage-run.js +39 -0
  107. package/dist/kiro-usage-run.js.map +1 -0
  108. package/dist/kiro-usage-scan.d.ts +49 -0
  109. package/dist/kiro-usage-scan.js +223 -0
  110. package/dist/kiro-usage-scan.js.map +1 -0
  111. package/dist/kiro-usage-worker.d.ts +1 -0
  112. package/dist/kiro-usage-worker.js +10 -0
  113. package/dist/kiro-usage-worker.js.map +1 -0
  114. package/dist/kiro-usage.d.ts +58 -0
  115. package/dist/kiro-usage.js +310 -0
  116. package/dist/kiro-usage.js.map +1 -0
  117. package/dist/locale.js +88 -8
  118. package/dist/locale.js.map +1 -1
  119. package/dist/log-service.js +1 -1
  120. package/dist/log-service.js.map +1 -1
  121. package/dist/login-url.d.ts +4 -0
  122. package/dist/login-url.js +22 -0
  123. package/dist/login-url.js.map +1 -0
  124. package/dist/needs-you-hub.d.ts +2 -1
  125. package/dist/needs-you-hub.js +1 -1
  126. package/dist/needs-you-hub.js.map +1 -1
  127. package/dist/needs-you.d.ts +8 -2
  128. package/dist/needs-you.js +6 -0
  129. package/dist/needs-you.js.map +1 -1
  130. package/dist/notice-delivery.d.ts +16 -0
  131. package/dist/notice-delivery.js +35 -0
  132. package/dist/notice-delivery.js.map +1 -0
  133. package/dist/office-share-page.d.ts +16 -0
  134. package/dist/office-share-page.js +85 -0
  135. package/dist/office-share-page.js.map +1 -0
  136. package/dist/office-share-service.d.ts +91 -0
  137. package/dist/office-share-service.js +176 -0
  138. package/dist/office-share-service.js.map +1 -0
  139. package/dist/office-share.d.ts +258 -0
  140. package/dist/office-share.js +549 -0
  141. package/dist/office-share.js.map +1 -0
  142. package/dist/opencode-usage-scan.d.ts +28 -0
  143. package/dist/opencode-usage-scan.js +92 -0
  144. package/dist/opencode-usage-scan.js.map +1 -0
  145. package/dist/opencode-usage-worker.d.ts +1 -0
  146. package/dist/opencode-usage-worker.js +10 -0
  147. package/dist/opencode-usage-worker.js.map +1 -0
  148. package/dist/outbound-handlers.js +22 -2
  149. package/dist/outbound-handlers.js.map +1 -1
  150. package/dist/outbound-schemas.d.ts +26 -2
  151. package/dist/outbound-schemas.js +27 -2
  152. package/dist/outbound-schemas.js.map +1 -1
  153. package/dist/public-web-gateway.js +7 -1
  154. package/dist/public-web-gateway.js.map +1 -1
  155. package/dist/quickstart-api.js +13 -2
  156. package/dist/quickstart-api.js.map +1 -1
  157. package/dist/quickstart.js +2 -1
  158. package/dist/quickstart.js.map +1 -1
  159. package/dist/remote-card-answer.d.ts +83 -0
  160. package/dist/remote-card-answer.js +208 -0
  161. package/dist/remote-card-answer.js.map +1 -0
  162. package/dist/reply-triggers.d.ts +2 -0
  163. package/dist/reply-triggers.js +3 -0
  164. package/dist/reply-triggers.js.map +1 -0
  165. package/dist/settings-api.d.ts +2 -0
  166. package/dist/settings-api.js +41 -16
  167. package/dist/settings-api.js.map +1 -1
  168. package/dist/settings-change.js +17 -14
  169. package/dist/settings-change.js.map +1 -1
  170. package/dist/settings-effect.js +7 -1
  171. package/dist/settings-effect.js.map +1 -1
  172. package/dist/settings-instance-patch.js +4 -1
  173. package/dist/settings-instance-patch.js.map +1 -1
  174. package/dist/setup-form.d.ts +1 -1
  175. package/dist/setup-form.js +55 -6
  176. package/dist/setup-form.js.map +1 -1
  177. package/dist/setup-host.d.ts +9 -0
  178. package/dist/setup-host.js +127 -6
  179. package/dist/setup-host.js.map +1 -1
  180. package/dist/setup-signin-assets.d.ts +2 -0
  181. package/dist/setup-signin-assets.js +6 -0
  182. package/dist/setup-signin-assets.js.map +1 -0
  183. package/dist/setup-wizard.js +2 -0
  184. package/dist/setup-wizard.js.map +1 -1
  185. package/dist/structured-signin-transport.d.ts +8 -0
  186. package/dist/structured-signin-transport.js +79 -0
  187. package/dist/structured-signin-transport.js.map +1 -0
  188. package/dist/structured-signin.d.ts +131 -0
  189. package/dist/structured-signin.js +388 -0
  190. package/dist/structured-signin.js.map +1 -0
  191. package/dist/tool-permissions.js +12 -0
  192. package/dist/tool-permissions.js.map +1 -1
  193. package/dist/topic-commands.js +6 -2
  194. package/dist/topic-commands.js.map +1 -1
  195. package/dist/transcript-sources.d.ts +26 -2
  196. package/dist/transcript-sources.js +103 -40
  197. package/dist/transcript-sources.js.map +1 -1
  198. package/dist/turn-reply-guard.d.ts +1 -1
  199. package/dist/turn-reply-guard.js +1 -1
  200. package/dist/turn-reply-guard.js.map +1 -1
  201. package/dist/types.d.ts +28 -8
  202. package/dist/ui/backend-signin.js +63 -10
  203. package/dist/ui/chat-strings.js +2 -2
  204. package/dist/ui/fleet-office-editor.js +14 -9
  205. package/dist/ui/fleet-office-layout.js +70 -32
  206. package/dist/ui/fleet-office-model.js +6 -3
  207. package/dist/ui/fleet-office-overview.js +167 -0
  208. package/dist/ui/fleet-office-pack.d.ts +7 -2
  209. package/dist/ui/fleet-office-pack.js +119 -15
  210. package/dist/ui/fleet-office-skin.js +98 -23
  211. package/dist/ui/fleet-office-store.js +39 -5
  212. package/dist/ui/fleet-office-view.js +5 -4
  213. package/dist/ui/fleet-office-zones.js +172 -12
  214. package/dist/ui/fleet-office.js +547 -186
  215. package/dist/ui/fleet-usage.js +19 -15
  216. package/dist/ui/office-share-dialog.js +145 -0
  217. package/dist/ui/office-share-page.js +164 -0
  218. package/dist/ui/panel-chat.js +8 -1
  219. package/dist/ui/panel-log.js +2 -2
  220. package/dist/ui/panel-needs.js +4 -4
  221. package/dist/ui/panel-settings.js +37 -11
  222. package/dist/ui/settings-dialogs.js +8 -1
  223. package/dist/ui/settings-guests.js +17 -7
  224. package/dist/ui/settings-model.js +14 -6
  225. package/dist/ui/settings-registry.js +4 -0
  226. package/dist/ui/settings-strings.js +16 -2
  227. package/dist/ui/shared/app-i18n.js +4 -2
  228. package/dist/ui/shared/app-shell.js +14 -6
  229. package/dist/ui/shared/app-stream.js +2 -1
  230. package/dist/ui/shared/app.css +67 -2
  231. package/dist/ui/shared/office/cozy.png +0 -0
  232. package/dist/ui/shared/side-panel.js +95 -0
  233. package/dist/ui/structured-signin.js +115 -0
  234. package/dist/usage-report.d.ts +20 -0
  235. package/dist/usage-report.js +54 -17
  236. package/dist/usage-report.js.map +1 -1
  237. package/dist/usage-rollup.d.ts +9 -1
  238. package/dist/usage-rollup.js +20 -4
  239. package/dist/usage-rollup.js.map +1 -1
  240. package/dist/web-api.d.ts +18 -0
  241. package/dist/web-api.js +201 -0
  242. package/dist/web-api.js.map +1 -1
  243. package/dist/web-auth.js +1 -1
  244. package/dist/web-auth.js.map +1 -1
  245. package/dist/web-backend-login.d.ts +1 -7
  246. package/dist/web-backend-login.js +2 -24
  247. package/dist/web-backend-login.js.map +1 -1
  248. package/dist/web-office-acts.d.ts +173 -0
  249. package/dist/web-office-acts.js +285 -0
  250. package/dist/web-office-acts.js.map +1 -0
  251. package/dist/web-office-skins.d.ts +3 -2
  252. package/dist/web-office-skins.js +3 -2
  253. package/dist/web-office-skins.js.map +1 -1
  254. package/dist/web-office-store.js +40 -7
  255. package/dist/web-office-store.js.map +1 -1
  256. package/dist/web-settings-patch.d.ts +2 -1
  257. package/dist/web-settings-patch.js +4 -1
  258. package/dist/web-settings-patch.js.map +1 -1
  259. package/dist/working-recovery.d.ts +2 -2
  260. package/dist/working-recovery.js +43 -8
  261. package/dist/working-recovery.js.map +1 -1
  262. package/dist/working-state-journal.d.ts +2 -0
  263. package/dist/working-state-journal.js +16 -1
  264. package/dist/working-state-journal.js.map +1 -1
  265. package/package.json +3 -2
  266. package/templates/windows-shortcuts.ps1 +155 -140
@@ -46,6 +46,7 @@ import { startTour } from "./chat-tour.js";
46
46
  import "./chat-strings.js";
47
47
  import { AppearanceSection } from "./settings-appearance.js";
48
48
  import { GuestsCard } from "./settings-guests.js";
49
+ import { OfficeSharesCard } from "./office-share-dialog.js";
49
50
  import { syncStage, useDraft } from "./settings-draft.js";
50
51
  import { WebAuthorityCard } from "./settings-web-authority.js";
51
52
  import { SignInList } from "./backend-signin.js";
@@ -952,8 +953,14 @@ function Classic({ ctx, search, openDialog }) {
952
953
  })}</div>`;
953
954
  }
954
955
 
956
+ // Sign-in notices and daily tips post in the topic or not at all: the DM choice was withdrawn (hmsg-d1809f2a,
957
+ // hmsg-3b8ac2af), and an old "dm" reads as on.
958
+ const noticeMode = (v) => v !== "false" && v !== false;
959
+ const noticeChoices = () => [{ value: "true", label: tn("noticeTopic") }, { value: "false", label: tn("noticeOff") }];
960
+ const noticeLabel = (v) => tn(v ? "noticeTopic" : "noticeOff");
961
+
955
962
  /** General's form, from the saved defaults (`d`) and ClassicBot's (`c`). */
956
- function generalForm(d, c, fleet = {}) {
963
+ function generalForm(d, c, fleet = {}, schema = {}) {
957
964
  const hang = d.hang_detector || {}, startup = d.startup || {};
958
965
  return {
959
966
  backend: d.backend || "claude-code", model: d.model || "", locale: d.locale || "", auto: String(d.auto_pause_after ?? 0),
@@ -966,6 +973,9 @@ function generalForm(d, c, fleet = {}) {
966
973
  // #2347 §10 review: unset startup concurrency is automatic (deriveSpawnConcurrency: 2–10 by free memory and cores),
967
974
  // not 10 — an empty field, and a save that leaves it unset (the defaults merge replaces `startup` whole).
968
975
  concurrency: startup.concurrency == null ? "" : String(startup.concurrency), stagger: String(startup.stagger_delay_ms ?? 500),
976
+ tips: String(noticeMode(d.tips)),
977
+ replyTriggers: Boolean(d.reply_triggers ?? schema.reply_triggers_default),
978
+ classicReplyTriggers: Boolean(c.reply_triggers ?? d.reply_triggers ?? schema.reply_triggers_default), classicReplyTriggersInherit: !hasOwn(c, "reply_triggers"),
969
979
  classicBackend: c.backend || d.backend || "claude-code",
970
980
  classicToolProgress: c.tool_progress ?? d.tool_progress ?? "off", classicToolProgressInherit: !hasOwn(c, "tool_progress"),
971
981
  classicReply: c.reply_completion_guard ?? d.reply_completion_guard ?? true, classicReplyInherit: !hasOwn(c, "reply_completion_guard"),
@@ -985,7 +995,7 @@ function General({ ctx, part = "all" }) {
985
995
  const hang = d.hang_detector || {}, startup = d.startup || {};
986
996
  const primary = channelsOf(ctx.fleet)[0];
987
997
  const original = useMemo(() => ({ c: structuredClone(c) }), []);
988
- const base = useMemo(() => generalForm(d, c, ctx.fleet), []);
998
+ const base = useMemo(() => generalForm(d, c, ctx.fleet, schema), []);
989
999
  const [f, setF] = useDraft(ctx, "general", () => base);
990
1000
  const set = (k) => (v) => setF((x) => ({ ...x, [k]: v }));
991
1001
  /** The class of a field's row: `field`, and `s-dirty` while one of its keys differs from what is saved. */
@@ -1011,27 +1021,31 @@ function General({ ctx, part = "all" }) {
1011
1021
  const defaultsPatch = changedFields({
1012
1022
  backend: f.backend, model: f.model || undefined, locale: f.locale || null, auto_pause_after: num(f.auto),
1013
1023
  agent_mode: f.agent_mode, tool_set: f.tool_set || null, tool_progress: f.tool_progress, cross_instance_visibility: f.visibility,
1014
- reply_completion_guard: f.reply, log_level: f.log_level,
1015
- // The dashboard features every agent may use unless its own setting says otherwise. All three go, because the write
1024
+ tips: noticeMode(f.tips), reply_triggers: f.replyTriggers, reply_completion_guard: f.reply, log_level: f.log_level,
1025
+ // The dashboard features every agent may use unless its own setting says otherwise. All of them go, because the write
1016
1026
  // to the fleet's defaults merges top-level keys only (`dashboard` is replaced whole): the untouched ones as saved.
1017
- dashboard: Object.fromEntries(DASHBOARD_FEATURES.map((k) => [k, f[`dash_${k}`]])),
1027
+ dashboard: Object.fromEntries(DASHBOARD_FEATURES.filter((k) => k !== "office").map((k) => [k, f[`dash_${k}`]])),
1028
+ office_access: f.dash_office,
1018
1029
  hang_detector: { enabled: f.hangOn, timeout_minutes: num(f.hang) },
1019
1030
  startup: { ...(f.concurrency.trim() === "" ? {} : { concurrency: num(f.concurrency) }), stagger_delay_ms: num(f.stagger) },
1020
1031
  }, {
1021
1032
  backend: d.backend || "claude-code", model: d.model || undefined, locale: d.locale || null, auto_pause_after: d.auto_pause_after ?? 0,
1022
1033
  agent_mode: d.agent_mode || "mcp", tool_set: TOOL_SETS.includes(d.tool_set) ? d.tool_set : null, tool_progress: d.tool_progress ?? "off",
1023
- cross_instance_visibility: visibilityDefault(d), reply_completion_guard: d.reply_completion_guard ?? true, log_level: d.log_level || "info",
1024
- dashboard: Object.fromEntries(DASHBOARD_FEATURES.map((k) => [k, dashboardDefault(d, k)])),
1034
+ tips: d.tips ?? true, cross_instance_visibility: visibilityDefault(d), reply_triggers: Boolean(d.reply_triggers ?? schema.reply_triggers_default), reply_completion_guard: d.reply_completion_guard ?? true, log_level: d.log_level || "info",
1035
+ dashboard: Object.fromEntries(DASHBOARD_FEATURES.filter((k) => k !== "office").map((k) => [k, dashboardDefault(d, k)])),
1036
+ office_access: dashboardDefault(d, "office"),
1025
1037
  hang_detector: { enabled: hang.enabled !== false, timeout_minutes: hang.timeout_minutes ?? HANG_TIMEOUT_DEFAULT_MINUTES },
1026
1038
  startup: { ...(startup.concurrency == null ? {} : { concurrency: startup.concurrency }), stagger_delay_ms: startup.stagger_delay_ms ?? 500 },
1027
1039
  });
1028
1040
  const oc = original.c;
1029
1041
  const classicPatch = changedFields({
1042
+ reply_triggers: f.classicReplyTriggersInherit ? null : f.classicReplyTriggers,
1030
1043
  backend: f.classicBackend,
1031
1044
  tool_progress: f.classicToolProgressInherit ? null : f.classicToolProgress,
1032
1045
  reply_completion_guard: f.classicReplyInherit ? null : f.classicReply,
1033
1046
  admin_users: f.admins, allowed_guilds: f.guilds,
1034
1047
  }, {
1048
+ reply_triggers: hasOwn(oc, "reply_triggers") ? oc.reply_triggers : null,
1035
1049
  backend: oc.backend || d.backend || "claude-code",
1036
1050
  tool_progress: hasOwn(oc, "tool_progress") ? oc.tool_progress : null,
1037
1051
  reply_completion_guard: hasOwn(oc, "reply_completion_guard") ? oc.reply_completion_guard : null,
@@ -1054,7 +1068,7 @@ function General({ ctx, part = "all" }) {
1054
1068
  // ClassicBot's inherited ones say what they inherit from Agent defaults.
1055
1069
  const unset = {
1056
1070
  locale: !d.locale, backend: !d.backend, model: !d.model, auto: !hasOwn(d, "auto_pause_after"), tool_progress: !hasOwn(d, "tool_progress"),
1057
- visibility: !VISIBILITY_MODES.includes(d.cross_instance_visibility), reply: !hasOwn(d, "reply_completion_guard"), tool_set: !d.tool_set,
1071
+ tips: !hasOwn(d, "tips"), replyTriggers: !hasOwn(d, "reply_triggers"), visibility: !VISIBILITY_MODES.includes(d.cross_instance_visibility), reply: !hasOwn(d, "reply_completion_guard"), tool_set: !d.tool_set,
1058
1072
  log_level: !d.log_level, agent_mode: !d.agent_mode, hangOn: !hasOwn(hang, "enabled"), hang: !hasOwn(hang, "timeout_minutes"),
1059
1073
  concurrency: !hasOwn(startup, "concurrency"), stagger: !hasOwn(startup, "stagger_delay_ms"),
1060
1074
  };
@@ -1077,6 +1091,8 @@ function General({ ctx, part = "all" }) {
1077
1091
  <div class=${fc("field", "tool_progress")}>${L(tn("toolProgress"), "defaults.tool_progress", "g-tp", "tool_progress")}<${Select} id="g-tp" value=${f.tool_progress} onChange=${set("tool_progress")} options=${TOOL_PROGRESS} /></div>
1078
1092
  <div class=${fc("field", "visibility")}>${L(tn("crossInstanceVisibility"), "defaults.cross_instance_visibility", "g-vis", "visibility")}<${Select} id="g-vis" value=${f.visibility} onChange=${set("visibility")} options=${VISIBILITY_MODES} />
1079
1093
  <p class="note">${tn("crossInstanceVisibilityHint")}</p></div>
1094
+ <div class=${fc("field", "tips")}>${L(tn("dailyTips"), "defaults.tips", "g-tips", "tips")}<${Select} id="g-tips" value=${f.tips} onChange=${set("tips")} options=${noticeChoices()} /><${InEffect} text=${noticeLabel(noticeMode(f.tips))} /><p class="note">${tn("dailyTipsHint")}</p></div>
1095
+ <div class=${fc("field", "replyTriggers")}><label class="check"><input id="g-reply-triggers" type="checkbox" checked=${f.replyTriggers} onChange=${(e) => set("replyTriggers")(e.target.checked)} /><span>${tn("replyTriggers")}</span>${dm("replyTriggers")}<${Impact} schema=${schema} field="defaults.reply_triggers" /></label><p class="note">${tn("replyTriggersHint")}</p></div>
1080
1096
  <div class=${fc("field", "reply")}><label class="check"><input id="g-reply" type="checkbox" checked=${f.reply} onChange=${(e) => set("reply")(e.target.checked)} /><span>${tn("replyCompletionGuard")}</span>${dm("reply")}<${Impact} schema=${schema} field="defaults.reply_completion_guard" /></label>
1081
1097
  <p class="note">${fleetReply}</p></div>
1082
1098
  </div>
@@ -1093,6 +1109,9 @@ function General({ ctx, part = "all" }) {
1093
1109
  <div class=${fc("field", "classicToolProgress", "classicToolProgressInherit")}>${L(tn("toolProgress"), "classic_defaults.tool_progress", "g-ctp")}
1094
1110
  <${Override} inherit=${f.classicToolProgressInherit} onInherit=${set("classicToolProgressInherit")} inherited=${fromDefaults("tool_progress", d.tool_progress ?? "off", "#g-tp")}>
1095
1111
  <${Select} id="g-ctp" disabled=${f.classicToolProgressInherit} value=${f.classicToolProgressInherit ? (d.tool_progress ?? "off") : f.classicToolProgress} onChange=${set("classicToolProgress")} options=${TOOL_PROGRESS} /></${Override}></div>
1112
+ <div class=${fc("field", "classicReplyTriggers", "classicReplyTriggersInherit")}>${L(tn("replyTriggers"), "classic_defaults.reply_triggers")}
1113
+ <${Override} inherit=${f.classicReplyTriggersInherit} onInherit=${set("classicReplyTriggersInherit")} inherited=${fromDefaults("reply_triggers", Boolean(d.reply_triggers ?? schema.reply_triggers_default), "#g-reply-triggers")}>
1114
+ <label class="check"><input id="g-creply-triggers" type="checkbox" disabled=${f.classicReplyTriggersInherit} checked=${f.classicReplyTriggersInherit ? Boolean(d.reply_triggers ?? schema.reply_triggers_default) : f.classicReplyTriggers} onChange=${(e) => set("classicReplyTriggers")(e.target.checked)} /><span>${tn("replyTriggers")}</span></label></${Override}><p class="note">${tn("replyTriggersHint")}</p></div>
1096
1115
  <div class=${fc("field", "classicReply", "classicReplyInherit")}>${L(tn("replyCompletionGuard"), "classic_defaults.reply_completion_guard")}
1097
1116
  <${Override} inherit=${f.classicReplyInherit} onInherit=${set("classicReplyInherit")} inherited=${fromDefaults("reply_completion_guard", d.reply_completion_guard ?? true, "#g-reply")}>
1098
1117
  <label class="check"><input id="g-creply" type="checkbox" disabled=${f.classicReplyInherit} checked=${f.classicReplyInherit ? (d.reply_completion_guard ?? true) : f.classicReply} onChange=${(e) => set("classicReply")(e.target.checked)} /><span>${tn("replyCompletionGuard")}</span></label></${Override}>
@@ -1140,7 +1159,7 @@ function General({ ctx, part = "all" }) {
1140
1159
  * the temporary public link none of it can be changed — the form says so and stays disabled (the server refuses too).
1141
1160
  */
1142
1161
  export const WEB_READ_ONLY = [
1143
- "web.allowed_hosts", "web.view_access", "web.notify_login", "web.preview", "web.preview_port", "web.preview_origin", "web.preview_on_public_link",
1162
+ "web.allowed_hosts", "web.view_access", "web.preview", "web.preview_port", "web.preview_origin", "web.preview_on_public_link",
1144
1163
  "web.usage_panel", "web.search", "web.provider_secrets", "web.upload_max_file_mb", "web.upload_max_total_mb", "web_terminal.bind", "web_terminal.tunnel",
1145
1164
  ];
1146
1165
  /**
@@ -1173,6 +1192,7 @@ function WebSection({ ctx, staged, part = "all" }) {
1173
1192
  // serves their reads there).
1174
1193
  const overPublicLink = globalThis.document?.body?.dataset?.publicLink === "1";
1175
1194
  const original = useMemo(() => ({
1195
+ notify: noticeMode(web.notify_login),
1176
1196
  echo: web.echo_to_channel ?? true,
1177
1197
  log: web.log_tab === true, // #1218: off by default
1178
1198
  usagePub: web.usage_report_on_public_link === true, // #2122: off by default
@@ -1182,7 +1202,7 @@ function WebSection({ ctx, staged, part = "all" }) {
1182
1202
  }), []);
1183
1203
  const text = (v) => (v === null || v === undefined ? "" : String(v));
1184
1204
  /** #2347: a field's row class: `s-dirty` while it differs from what is saved (the draft may come from before). */
1185
- const savedForm = useMemo(() => ({ echo: original.echo, log: original.log, usagePub: original.usagePub, pub: original.pub.allow_public,
1205
+ const savedForm = useMemo(() => ({ notify: String(original.notify), echo: original.echo, log: original.log, usagePub: original.usagePub, pub: original.pub.allow_public,
1186
1206
  ttl: String(original.pub.ttl_minutes), protocol: original.pub.protocol, hostname: original.pub.hostname, originPort: String(original.pub.origin_port),
1187
1207
  abs: text(original.session.absolute_hours), idle: text(original.session.idle_hours), termOn: original.terminal.enabled !== false,
1188
1208
  termTtl: text(original.terminal.ttl_minutes), autoOpen: original.pub.auto_open }), []);
@@ -1194,6 +1214,7 @@ function WebSection({ ctx, staged, part = "all" }) {
1194
1214
  const pub = pending.public_link || {}, session = pending.session?.local || {}, terminal = pending.web_terminal || {};
1195
1215
  const pick = (obj, key, fallback) => Object.prototype.hasOwnProperty.call(obj, key) ? obj[key] : fallback;
1196
1216
  return {
1217
+ notify: String(pick(pending, "notify_login", original.notify)),
1197
1218
  echo: pick(pending, "echo_to_channel", original.echo), log: pick(pending, "log_tab", original.log), usagePub: pick(pending, "usage_report_on_public_link", original.usagePub),
1198
1219
  pub: pick(pub, "allow_public", original.pub.allow_public), ttl: String(pick(pub, "ttl_minutes", original.pub.ttl_minutes)), protocol: pick(pub, "protocol", original.pub.protocol),
1199
1220
  hostname: pick(pub, "hostname", original.pub.hostname), originPort: String(pick(pub, "origin_port", original.pub.origin_port)), autoOpen: pick(pub, "auto_open", original.pub.auto_open),
@@ -1242,6 +1263,7 @@ function WebSection({ ctx, staged, part = "all" }) {
1242
1263
  const terminalPatch = changedFields({ enabled: f.termOn === (original.terminal.enabled !== false) ? original.terminal.enabled : f.termOn ? null : false,
1243
1264
  ttl_minutes: value(f.termTtl) }, original.terminal);
1244
1265
  const body = {
1266
+ ...(noticeMode(f.notify) !== original.notify ? { notify_login: noticeMode(f.notify) } : {}),
1245
1267
  ...(f.echo !== original.echo ? { echo_to_channel: f.echo } : {}),
1246
1268
  ...(f.log !== original.log ? { log_tab: f.log } : {}),
1247
1269
  ...(f.usagePub !== original.usagePub ? { usage_report_on_public_link: f.usagePub } : {}),
@@ -1250,6 +1272,7 @@ function WebSection({ ctx, staged, part = "all" }) {
1250
1272
  ...(Object.keys(terminalPatch).length ? { web_terminal: terminalPatch } : {}),
1251
1273
  };
1252
1274
  const fields = [
1275
+ ...(body.notify_login !== undefined ? ["web.notify_login"] : []),
1253
1276
  ...(body.echo_to_channel !== undefined ? ["web.echo_to_channel"] : []),
1254
1277
  ...(body.log_tab !== undefined ? ["web.log_tab"] : []),
1255
1278
  ...(body.usage_report_on_public_link !== undefined ? ["web.usage_report_on_public_link"] : []),
@@ -1297,6 +1320,7 @@ function WebSection({ ctx, staged, part = "all" }) {
1297
1320
  ${overPublicLink && part !== "readonly" ? html`<p class="note" role="status" id="webPublicRefused">${tn("webPublicRefused")}</p>` : null}
1298
1321
  ${has("access") ? lock(html`<section class="card"><h3>${tn("webSessionTitle")}</h3>
1299
1322
  <p class="note">${tn("webSessionHint")}</p>
1323
+ <div class=${fc("field", "notify")}><label for="webNotifyLogin">${tn("webNotifyLogin")}<${Impact} schema=${schema} field="web.notify_login" /></label><${Select} id="webNotifyLogin" value=${f.notify} onChange=${set("notify")} options=${noticeChoices()} /><${InEffect} text=${noticeLabel(noticeMode(f.notify))} />${web.notify_login === undefined ? html`<${DefaultMark} />` : null}<p class="note">${tn("webNotifyLoginHint")}</p></div>
1300
1324
  <div class="grid2">
1301
1325
  <div class=${fc("field", "abs")}><label for="webSessionAbsolute">${tn("sessionAbsolute")}</label><input id="webSessionAbsolute" type="number" min="1" max="720" step="any"
1302
1326
  class=${errors.abs ? "invalid" : ""} value=${f.abs} onInput=${(e) => set("abs")(e.target.value)} /><${Feedback} error=${errors.abs} />
@@ -1331,7 +1355,9 @@ function WebSection({ ctx, staged, part = "all" }) {
1331
1355
  ${named || f.autoOpen ? html`<label class=${fc("check", "autoOpen")}><input id="publicAutoOpen" type="checkbox" checked=${f.autoOpen} onChange=${(e) => set("autoOpen")(e.target.checked)} /><span>${tn("publicAutoOpen")}</span><${Impact} schema=${schema} field="web.public_link.auto_open" /></label>
1332
1356
  <p class="note">${tn("publicAutoOpenHint")}</p><${Feedback} error=${errors.autoOpen} />` : null}
1333
1357
  </section>`) : null}
1334
- ${has("access") && !overPublicLink ? html`<${PublicTunnelToken} /><${GuestsCard} ctx=${ctx} staged=${staged} /><${WebAuthorityCard} ctx=${ctx} staged=${staged} />` : null}
1358
+ ${has("access") && !overPublicLink ? html`<${PublicTunnelToken} />` : null}
1359
+ ${has("access") ? html`<${OfficeSharesCard} />` : null}
1360
+ ${has("access") && !overPublicLink ? html`<${GuestsCard} ctx=${ctx} staged=${staged} /><${WebAuthorityCard} ctx=${ctx} staged=${staged} />` : null}
1335
1361
  ${has("chat") ? lock(html`<section class="card"><h3>${tn("webChat")}</h3>
1336
1362
  <label class=${fc("check", "echo")}><input id="webEchoToChannel" type="checkbox" checked=${f.echo} onChange=${(e) => set("echo")(e.target.checked)} /><span>${tn("webEcho")}</span><${Impact} schema=${schema} field="web.echo_to_channel" /></label>
1337
1363
  <p class="note">${tn("webEchoHint")}</p>
@@ -642,6 +642,7 @@ export function ClassicDialog({ room, ctx, onClose }) {
642
642
  const inherited = {
643
643
  backend: cd.backend || fd.backend || "claude-code", model: cd.model || fd.model || "",
644
644
  auto_pause_after: cd.auto_pause_after ?? fd.auto_pause_after ?? 0, tool_progress: cd.tool_progress ?? fd.tool_progress ?? "off",
645
+ reply_triggers: Boolean(cd.reply_triggers ?? fd.reply_triggers ?? schema.reply_triggers_default),
645
646
  reply_completion_guard: cd.reply_completion_guard ?? fd.reply_completion_guard ?? true,
646
647
  };
647
648
  const c = room;
@@ -649,8 +650,9 @@ export function ClassicDialog({ room, ctx, onClose }) {
649
650
  backend: c.backend || inherited.backend, model: c.model || "",
650
651
  auto_pause_after: String(c.auto_pause_after ?? inherited.auto_pause_after), tool_progress: c.tool_progress ?? inherited.tool_progress,
651
652
  reply_completion_guard: c.reply_completion_guard ?? inherited.reply_completion_guard,
653
+ reply_triggers: c.reply_triggers ?? inherited.reply_triggers,
652
654
  collab: !!c.collab, web_echo: classicWebEchoEffective(c, fleet), context_lines: String(c.context_lines ?? cd.context_lines ?? 5),
653
- inherit: { auto_pause_after: !hasOwn(c, "auto_pause_after"), tool_progress: !hasOwn(c, "tool_progress"), reply_completion_guard: !hasOwn(c, "reply_completion_guard") },
655
+ inherit: { reply_triggers: !hasOwn(c, "reply_triggers"), auto_pause_after: !hasOwn(c, "auto_pause_after"), tool_progress: !hasOwn(c, "tool_progress"), reply_completion_guard: !hasOwn(c, "reply_completion_guard") },
654
656
  }));
655
657
  const set = (k) => (v) => setF((x) => ({ ...x, [k]: v }));
656
658
  const setInherit = (k) => (on) => setF((x) => ({ ...x, inherit: { ...x.inherit, [k]: on } }));
@@ -664,12 +666,14 @@ export function ClassicDialog({ room, ctx, onClose }) {
664
666
  auto_pause_after: f.inherit.auto_pause_after ? null : Number(f.auto_pause_after),
665
667
  tool_progress: f.inherit.tool_progress ? null : f.tool_progress,
666
668
  reply_completion_guard: f.inherit.reply_completion_guard ? null : f.reply_completion_guard,
669
+ reply_triggers: f.inherit.reply_triggers ? null : f.reply_triggers,
667
670
  collab: f.collab, context_lines: Number(f.context_lines), web_echo: f.web_echo,
668
671
  }, {
669
672
  backend: c.backend || inherited.backend, model: (c.model && c.model.trim()) || null,
670
673
  auto_pause_after: hasOwn(c, "auto_pause_after") ? c.auto_pause_after : null,
671
674
  tool_progress: hasOwn(c, "tool_progress") ? c.tool_progress : null,
672
675
  reply_completion_guard: hasOwn(c, "reply_completion_guard") ? c.reply_completion_guard : null,
676
+ reply_triggers: hasOwn(c, "reply_triggers") ? c.reply_triggers : null,
673
677
  collab: !!c.collab, context_lines: c.context_lines ?? cd.context_lines ?? 5, web_echo: classicWebEchoEffective(c, fleet),
674
678
  });
675
679
  const stage = async () => {
@@ -707,6 +711,9 @@ export function ClassicDialog({ room, ctx, onClose }) {
707
711
  <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}
708
712
  onChange=${(e) => set("reply_completion_guard")(e.target.checked)} /><span>${tn("replyCompletionGuard")}</span></label></${Override}>
709
713
  <p class="note">${replyHint(f.backend, classicMode, fd.kiro_ui)}</p></div>
714
+ <div class="field">${L(tn("replyTriggers"), "classic.reply_triggers")}
715
+ <${Override} inherit=${f.inherit.reply_triggers} onInherit=${setInherit("reply_triggers")} inherited=${roomInh("reply_triggers", "", "#g-creply-triggers", "#g-reply-triggers")}>
716
+ <label class="check"><input type="checkbox" disabled=${f.inherit.reply_triggers} checked=${f.inherit.reply_triggers ? inherited.reply_triggers : f.reply_triggers} onChange=${(e) => set("reply_triggers")(e.target.checked)} /><span>${tn("replyTriggers")}</span></label></${Override}><p class="note">${tn("replyTriggersHint")}</p></div>
710
717
  <label class="check"><input type="checkbox" checked=${f.collab} onChange=${(e) => set("collab")(e.target.checked)} /><span>${tn("collabMode")}</span><${Impact} schema=${schema} field="classic.collab" /></label>
711
718
  <label class="check"><input type="checkbox" checked=${f.web_echo} onChange=${(e) => set("web_echo")(e.target.checked)} /><span>${tn("webEcho")}</span><${Impact} schema=${schema} field="classic.web_echo" /></label>
712
719
  </div>
@@ -60,11 +60,19 @@ function wroteTo(names) {
60
60
  return html`${before}${names.map((n, i) => html`${i ? ", " : ""}<span key=${n} class="g-wrote">${n}</span>`)}${after}`;
61
61
  }
62
62
 
63
- function Names({ id, all, value, wrote = [], onChange }) {
63
+ /** #2667: a granted name as a person reads it — a ClassicBot room by its display name, when it has one. */
64
+ const shownName = (rooms, n) => { const r = (rooms || []).find((x) => x.name === n); return (r && r.display_name) || n; };
65
+
66
+ /** The agents to choose from: fleet.yaml's instances, then (#2667) the ClassicBot rooms, under their own heading. */
67
+ function Names({ id, all, rooms = [], value, wrote = [], onChange }) {
64
68
  const chosen = new Set(value || []);
65
- return html`<div class="guest-names" id=${id} role="group">${all.map((n) => html`<label key=${n} class="check guest-name">
66
- <input type="checkbox" checked=${chosen.has(n)} onChange=${(e) => { const s = new Set(chosen); if (e.target.checked) s.add(n); else s.delete(n); onChange(all.filter((x) => s.has(x))); }} />
67
- <span>${n}${wrote.includes(n) ? html` <span class="note">${tn("grantsWroteHere")}</span>` : null}</span></label>`)}</div>`;
69
+ const order = [...all, ...rooms.map((r) => r.name)];
70
+ const box = (n, label, note) => html`<label key=${n} class="check guest-name" title=${note ? n : undefined}>
71
+ <input type="checkbox" checked=${chosen.has(n)} onChange=${(e) => { const s = new Set(chosen); if (e.target.checked) s.add(n); else s.delete(n); onChange(order.filter((x) => s.has(x))); }} />
72
+ <span>${label}${note ? html` <span class="note">${n}</span>` : null}${wrote.includes(n) ? html` <span class="note">${tn("grantsWroteHere")}</span>` : null}</span></label>`;
73
+ return html`<div class="guest-names" id=${id} role="group">${all.map((n) => box(n, n, false))}
74
+ ${rooms.length ? html`<span class="guest-names-group note" key="--rooms">${tn("grantsClassicRooms")}</span>` : null}
75
+ ${rooms.map((r) => box(r.name, r.display_name || r.name, !!r.display_name))}</div>`;
68
76
  }
69
77
 
70
78
  const clock = (ms) => new Date(ms).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
@@ -147,6 +155,8 @@ export function GuestsSection({ ctx, staged }) {
147
155
  const setGrant = (k, grant) => { setStatus(null); setF((x) => { const g = { ...(x.grants || {}) }; if (grant) g[k] = grant; else delete g[k]; return { ...x, grants: g }; }); };
148
156
  const guests = (overview && overview.guests) || [];
149
157
  const instances = (overview && overview.instances) || [];
158
+ const rooms = (overview && Array.isArray(overview.rooms) && overview.rooms) || [];
159
+ const choosable = [...instances, ...rooms.map((r) => r.name)];
150
160
  const who = (k) => {
151
161
  const g = guests.find((x) => accountKey(x) === k), p = (people || []).find((x) => accountKey(x) === k);
152
162
  const grant = (f.grants && f.grants[k]) || (original.grants && original.grants[k]);
@@ -223,10 +233,10 @@ export function GuestsSection({ ctx, staged }) {
223
233
  <div class="g-head"><span class="g-av" aria-hidden="true">${(person.name || "?").slice(0, 1).toUpperCase()}</span>
224
234
  <div class="g-id"><strong>${person.name}</strong> <span class="note">${PLATFORMS[person.platform] || person.platform}${person.username ? ` · @${person.username}` : ""}</span></div>
225
235
  ${removed ? html`<span class="g-state">${tn("grantsWillBeRemoved")}</span>` : added ? html`<span class="g-state">${tn("grantsNew")}</span>` : pill(person)}</div>
226
- <div class="g-chips">${((draft || saved).instances || []).map((n) => html`<span key=${n} class="g-chip">${n}</span>`)}</div>
236
+ <div class="g-chips">${((draft || saved).instances || []).map((n) => html`<span key=${n} class="g-chip" title=${shownName(rooms, n) !== n ? n : undefined}>${shownName(rooms, n)}</span>`)}</div>
227
237
  ${draft && !draft.instances.length ? html`<p class="note g-error" role="alert">${tn("grantsNoAgents")}</p>` : null}
228
238
  ${editing === k && draft ? html`<div class="g-edit"><p class="note">${tn("grantsMaySee", person.name)}</p>
229
- <${Names} id=${`grant-${k}`} all=${instances} value=${draft.instances} wrote=${person.wrote} onChange=${(list) => setGrant(k, { ...draft, instances: list })} /></div>` : null}
239
+ <${Names} id=${`grant-${k}`} all=${instances} rooms=${rooms} value=${draft.instances} wrote=${person.wrote} onChange=${(list) => setGrant(k, { ...draft, instances: list })} /></div>` : null}
230
240
  ${readOnly ? null : html`<div class="guest-actions">
231
241
  ${draft ? html`<button type="button" class="btn btn-sm" aria-expanded=${editing === k ? "true" : "false"} onClick=${() => setEditing(editing === k ? null : k)}>${tn("grantsEditAgents")}</button>` : null}
232
242
  ${draft ? html`<button type="button" class="btn btn-sm g-send" disabled=${!applied || ss.sending || cooling || !f.enabled || !original.enabled}
@@ -245,7 +255,7 @@ export function GuestsSection({ ctx, staged }) {
245
255
  <span class="g-av" aria-hidden="true">${(p.name || "?").slice(0, 1).toUpperCase()}</span>
246
256
  <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>
247
257
  ${p.instances && p.instances.length ? html`<div class="note">${wroteTo(p.instances)}</div>` : null}</div>
248
- <button type="button" class="btn btn-sm" onClick=${() => { const k = accountKey(p); setGrant(k, { ...(p.name ? { name: p.name } : {}), instances: instances.filter((n) => (p.instances || []).includes(n)) }); setEditing(k); }}>${tn("grantsChoose")}</button></li>`)}</ul>`}`}
258
+ <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>`}`}
249
259
  </fieldset>
250
260
  ${overview && overview.audit ? html`<h4>${tn("grantsRecent")}</h4>${!overview.audit.length ? html`<p class="note" id="grantsRecentNone">${tn("grantsRecentNone")}</p>`
251
261
  : 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}>
@@ -40,19 +40,25 @@ export function changedFields(next, baseline) {
40
40
 
41
41
  /**
42
42
  * The dashboard features an agent may use (dashboard-permissions.ts): each "inherit" (the fleet default: `defaults.dashboard`,
43
- * else allowed), "on" or "off". The patch names only the features that changed: null follows the default again.
43
+ * else allowed — off for OFF_UNLESS_ON, `office`), "on" or "off". The patch names only the features that changed: null
44
+ * follows the default again.
44
45
  */
45
- export const DASHBOARD_FEATURES = ["appearance", "task_board", "web_settings"];
46
+ export const DASHBOARD_FEATURES = ["appearance", "task_board", "web_settings", "office"];
47
+ /** #2578: the features a person switches on — unset is off (dashboard-permissions.ts OFF_UNLESS_ON). */
48
+ export const OFF_UNLESS_ON = ["office"];
46
49
  export const DASHBOARD_CHOICES = ["inherit", "on", "off"];
47
50
  const dashboardOf = (v) => (v && typeof v === "object" && !Array.isArray(v) ? v : {});
48
- /** What a feature is for an agent with no setting of its own: the default's, else allowed. */
49
- export const dashboardDefault = (defaults, feature) => dashboardOf(defaults && defaults.dashboard)[feature] !== false;
51
+ /** What a feature is for an agent with no setting of its own: the default's, else allowed (off for OFF_UNLESS_ON). */
52
+ export const dashboardDefault = (defaults, feature) => {
53
+ const v = feature === "office" && defaults && Object.hasOwn(defaults, "office_access") ? defaults.office_access : dashboardOf(defaults && defaults.dashboard)[feature];
54
+ return OFF_UNLESS_ON.includes(feature) ? v === true : v !== false;
55
+ };
50
56
  export function dashboardForm(inst) {
51
- const own = dashboardOf(inst.dashboard);
57
+ const own = { ...dashboardOf(inst.dashboard), ...(Object.hasOwn(inst, "office_access") ? { office: inst.office_access } : {}) };
52
58
  return Object.fromEntries(DASHBOARD_FEATURES.map((f) => [f, own[f] === true ? "on" : own[f] === false ? "off" : "inherit"]));
53
59
  }
54
60
  export function dashboardPatchValue(choices) {
55
- return Object.fromEntries(DASHBOARD_FEATURES.map((f) => [f, choices[f] === "on" ? true : choices[f] === "off" ? false : null]));
61
+ return Object.fromEntries(DASHBOARD_FEATURES.filter((f) => f !== "office").map((f) => [f, choices[f] === "on" ? true : choices[f] === "off" ? false : null]));
56
62
  }
57
63
 
58
64
  /** #1302: cross-instance notices. Unset, or a value the fleet does not know, means full. */
@@ -175,6 +181,7 @@ export function agentPatch(form, inst, defaults, visibilityBaseline, statusEmoji
175
181
  display_name: form.inherit.display_name ? null : form.display_name.trim(),
176
182
  status_emojis: form.status_emojis,
177
183
  dashboard: dashboardPatchValue(form.dashboard),
184
+ office_access: form.dashboard.office === "on" ? true : form.dashboard.office === "off" ? false : null,
178
185
  };
179
186
  return changedFields(next, {
180
187
  backend: inst.backend || defaults.backend || "claude-code",
@@ -198,6 +205,7 @@ export function agentPatch(form, inst, defaults, visibilityBaseline, statusEmoji
198
205
  display_name: hasOwn(inst, "display_name") ? inst.display_name : null,
199
206
  status_emojis: statusEmojisBaseline,
200
207
  dashboard: dashboardPatchValue(dashboardForm(inst)),
208
+ office_access: dashboardForm(inst).office === "on" ? true : dashboardForm(inst).office === "off" ? false : null,
201
209
  });
202
210
  }
203
211
 
@@ -33,6 +33,8 @@ const ROWS = [
33
33
  // Fleet · Agents
34
34
  ["agents.new", "agents", "#newAgentBtn", "settings.newAgent", null, ["new agent", "create", "add agent", "instance"], ["新增 agent", "建立", "新 agent"]],
35
35
  ["agents.provider-keys", "agents", ".s-provider-keys", "settings.providerKeys", "web.provider_secrets", ["api key", "provider", "openai", "anthropic", "token", "secret"], ["API 金鑰", "金鑰", "供應商", "token"]],
36
+ ["defaults.tips", "agent-defaults", "#g-tips", "settings.dailyTips", "defaults.tips", ["daily tips", "notices"], ["每日提示"]],
37
+ ["web.notify-login", "access", "#webNotifyLogin", "settings.webNotifyLogin", "web.notify_login", ["sign in", "login", "notices"], ["登入通知"]],
36
38
  // Fleet · Agent defaults
37
39
  ["defaults.locale", "agent-defaults", "#g-locale", "settings.language", "defaults.locale", ["language", "bot language", "reply language", "locale"], ["語言", "回覆語言", "bot 語言"]],
38
40
  ["defaults.backend", "agent-defaults", "#g-be", "settings.defaultBackend", "defaults.backend", ["backend", "cli", "claude", "codex", "kiro"], ["後端", "CLI"]],
@@ -40,6 +42,8 @@ const ROWS = [
40
42
  ["defaults.auto-pause", "agent-defaults", "#g-auto", "settings.autoPause", "defaults.auto_pause_after", ["auto pause", "idle", "sleep", "minutes"], ["自動暫停", "閒置", "休眠"]],
41
43
  ["defaults.tool-progress", "agent-defaults", "#g-tp", "settings.toolProgress", "defaults.tool_progress", ["tool progress", "verbose", "steps"], ["工具進度", "詳細"]],
42
44
  ["defaults.visibility", "agent-defaults", "#g-vis", "settings.crossInstanceVisibility", "defaults.cross_instance_visibility", ["visibility", "bot to bot", "cross instance", "messages"], ["可見性", "bot 之間", "跨 instance"]],
45
+ ["defaults.reply-triggers", "agent-defaults", "#g-reply-triggers", "settings.replyTriggers", "defaults.reply_triggers", ["classic native reply", "reply trigger"], ["原生回覆觸發"]],
46
+ ["chat.classic-reply-triggers", "chat", "#g-creply-triggers", "settings.replyTriggers", "classic_defaults.reply_triggers", ["classic native reply"], ["ClassicBot 原生回覆觸發"]],
43
47
  ["defaults.reply-guard", "agent-defaults", "#g-reply", "settings.replyCompletionGuard", "defaults.reply_completion_guard", ["delivered reply", "reply guard", "require reply"], ["送達回覆", "回覆保護"]],
44
48
  ["defaults.tool-set", "agent-defaults", "#g-ts", "settings.toolSet", "defaults.tool_set", ["tool set", "tools", "mcp"], ["工具組", "工具"]],
45
49
  ["defaults.dashboard", "agent-defaults", "#g-dash", "settings.dashboardAccess", "defaults.dashboard", ["dashboard access", "dashboard features", "permissions", "appearance tools", "task board", "web settings"], ["Dashboard 權限", "權限", "外觀", "任務看板", "網頁設定"]],
@@ -4,6 +4,10 @@ import { register } from "/assets/app-i18n.js";
4
4
 
5
5
  register("settings", {
6
6
  en: {
7
+ noticeTopic: "Topic", noticeOff: "Off",
8
+ webNotifyLogin: "Web sign-in notices", dailyTips: "Daily tips",
9
+ webNotifyLoginHint: "Posted in the topic. Off suppresses operator notices only; guest and public-link security notices still post. Default: Topic.",
10
+ dailyTipsHint: "Posted in the General topic. Default: Topic. /tips still posts where invoked.",
7
11
  title: "Settings",
8
12
  myAgents: "My Agents",
9
13
  bots: "Bots",
@@ -108,6 +112,7 @@ register("settings", {
108
112
  dashboard_appearance: "Its look (theme, pictures, widgets, effects, mod proposals)",
109
113
  dashboard_task_board: "The task board",
110
114
  dashboard_web_settings: "Your web appearance settings",
115
+ dashboard_office: "Office: walk, speak and act in the Office view",
111
116
  dashboardInherit: "Fleet default ({0})",
112
117
  dashboardOn: "allowed",
113
118
  dashboardOff: "off",
@@ -527,7 +532,7 @@ register("settings", {
527
532
  grantsLegacy: "Guests now reach what their chat access gives them. Saving grants switches to explicit access: only the people below, only their ticked agents.",
528
533
  grantsConvert: "Start from today's guests", grantsWithAccess: "People with access", grantsNoneYet: "No one has guest access yet.",
529
534
  grantsAdd: "Add a person", grantsSearch: "Search people who have written to your agents", grantsNoPeople: "No one matches. A person must have written to an agent first.",
530
- grantsChoose: "Choose", grantsChosen: "Chosen", grantsMaySee: "{0} may see:", grantsWroteHere: "wrote here", grantsWroteTo: "Wrote to: {0}",
535
+ grantsChoose: "Choose", grantsChosen: "Chosen", grantsMaySee: "{0} may see:", grantsClassicRooms: "ClassicBot rooms", grantsWroteHere: "wrote here", grantsWroteTo: "Wrote to: {0}",
531
536
  grantsLastSeen: "last seen {0}", grantsRecent: "Recent changes", grantsRecentNone: "No change to anyone's access yet.", grantsAuditDirect: "applied at once", grantsAuditConfirmed: "confirmed in General", grantsAuditEnded: "{0} session(s) ended", grantsAuditCodeSent: "sign-in code sent", grantsAuditCodeNot: "sign-in code not sent ({0})", grantsAuditDmFailed: "the DM did not go through", grantsAuditWait: "asked again within a minute", grantsAuditAdminLimit: "the admin's limit of codes was reached",
532
537
  grantsEditAgents: "Edit agents", grantsRemove: "Remove access", grantsUndoRemove: "Keep access",
533
538
  grantsWillBeRemoved: "Access ends when you save.", grantsNew: "New — granted when you save", grantsNoAgents: "Tick at least one agent, or remove this person.",
@@ -570,6 +575,8 @@ register("settings", {
570
575
  removeItem: "Remove {0}",
571
576
  zeroDisabled: "0 = off",
572
577
  optional: "optional",
578
+ replyTriggers: "Native replies trigger ClassicBot",
579
+ replyTriggersHint: "Off by default: only an exact @bot mention triggers a collab room. On also accepts one-hop replies to that bot. Forwarded content never counts; fleet-topic traffic is unchanged.",
573
580
  collabMode: "Collaboration mode",
574
581
  contextLines: "Context lines",
575
582
  refresh: "Refresh",
@@ -672,6 +679,10 @@ register("settings", {
672
679
  se_externalEmojiHint: "使用其他伺服器的 emoji,bot 在要加反應的頻道需有「使用外部表情符號」權限。",
673
680
  se_unavailable: "無法使用",
674
681
  se_hint: "留空即使用預設值。預覽由 AgEnD 依 bot 實際 react 的方式解析。進度前綴不是反應,而是進度訊息開頭的 emoji。",
682
+ noticeTopic: "頻道", noticeOff: "關閉",
683
+ webNotifyLogin: "網頁登入通知", dailyTips: "每日提示",
684
+ webNotifyLoginHint: "發在頻道。關閉只停用管理者登入通知;訪客與公開連結安全通知仍會發出。預設:頻道。",
685
+ dailyTipsHint: "發在 General 頻道。預設:頻道。/tips 仍送至呼叫的聊天室。",
675
686
  title: "設定",
676
687
  myAgents: "我的 Agent",
677
688
  bots: "機器人",
@@ -750,6 +761,7 @@ register("settings", {
750
761
  dashboard_appearance: "它的外觀(主題、圖片、小工具、特效、mod 提案)",
751
762
  dashboard_task_board: "任務看板",
752
763
  dashboard_web_settings: "你的網頁外觀設定",
764
+ dashboard_office: "辦公室:在辦公室畫面走動、說話、做動作",
753
765
  dashboardInherit: "跟 fleet 預設({0})",
754
766
  dashboardOn: "允許",
755
767
  dashboardOff: "關閉",
@@ -1167,7 +1179,7 @@ register("settings", {
1167
1179
  grantsLegacy: "目前訪客能看到的,取決於他們在聊天中的權限。儲存授權後改為明確授權:只有下列的人,只看得到勾選的 agent。",
1168
1180
  grantsConvert: "以目前的訪客為起點", grantsWithAccess: "有存取權的人", grantsNoneYet: "還沒有人有訪客存取權。",
1169
1181
  grantsAdd: "新增人員", grantsSearch: "搜尋曾傳訊息給你的 agent 的人", grantsNoPeople: "沒有符合的人。對方必須先傳過訊息給某個 agent。",
1170
- grantsChoose: "選擇", grantsChosen: "已選擇", grantsMaySee: "{0} 可以看到:", grantsWroteHere: "傳過訊息", grantsWroteTo: "傳過訊息給:{0}",
1182
+ grantsChoose: "選擇", grantsChosen: "已選擇", grantsMaySee: "{0} 可以看到:", grantsClassicRooms: "ClassicBot 聊天室", grantsWroteHere: "傳過訊息", grantsWroteTo: "傳過訊息給:{0}",
1171
1183
  grantsLastSeen: "最後出現 {0}", grantsRecent: "最近的變更", grantsRecentNone: "還沒有人的存取權變更過。", grantsAuditDirect: "立即生效", grantsAuditConfirmed: "在 General 確認後生效", grantsAuditEnded: "結束了 {0} 個登入", grantsAuditCodeSent: "已寄出登入碼", grantsAuditCodeNot: "登入碼未寄出({0})", grantsAuditDmFailed: "私訊沒有送達", grantsAuditWait: "一分鐘內重複要求", grantsAuditAdminLimit: "已達管理員的登入碼上限",
1172
1184
  grantsEditAgents: "編輯 agent", grantsRemove: "移除存取權", grantsUndoRemove: "保留存取權",
1173
1185
  grantsWillBeRemoved: "儲存後存取權就會結束。", grantsNew: "新增 —— 儲存後授權", grantsNoAgents: "至少勾選一個 agent,或移除這個人。",
@@ -1210,6 +1222,8 @@ register("settings", {
1210
1222
  removeItem: "移除 {0}",
1211
1223
  zeroDisabled: "0 = 關閉",
1212
1224
  optional: "選填",
1225
+ replyTriggers: "原生回覆觸發 ClassicBot",
1226
+ replyTriggersHint: "預設關閉:協作房間只有精確 @bot 提及才觸發。開啟後也接受對該機器人的單層原生回覆;轉發內容不算。Fleet 話題的訊息接收方式不變。",
1213
1227
  collabMode: "協作模式",
1214
1228
  contextLines: "上下文行數",
1215
1229
  refresh: "重新整理",
@@ -103,8 +103,9 @@ register("app", {
103
103
  // #2293: global search — chats, cross-instance messages and tasks (not the sidebar's filter of agents).
104
104
  search: "Search", searchTitle: "Search chats, messages between agents and tasks ({0})",
105
105
  confirmTitle: "Please confirm", confirmOk: "Continue", cancel: "Cancel",
106
+ sidePanelLeave: "Leave {0} without saving? Your changes there will be lost.", sidePanelDiscard: "Discard changes",
106
107
  // The server's own wording (src/locale.ts needs.reason.*), so the web says what Discord says.
107
- needs_reason_hang: "Not responding", needs_reason_exited: "Exited", needs_reason_assist: "Waiting at its terminal", needs_reason_permission: "Permission needed", needs_reason_dangerous_command: "Dangerous command waiting", needs_reason_login: "Sign-in needed", needs_reason_dialog: "Waiting at a dialog", needs_reason_terminal_input: "May be waiting for input", needs_reason_auth_paused: "Paused: sign-in needed", needs_reason_crashed: "Crashed", needs_reason_delivery_uncertain: "Could not confirm delivery", needs_reason_delivery_failed: "Delivery failed", needs_reason_task_review: "Waiting for your confirmation", needs_reason_task_blocked: "Blocked: asks you",
108
+ needs_reason_hang: "Not responding", needs_reason_exited: "Exited", needs_reason_assist: "Waiting at its terminal", needs_reason_permission: "Permission needed", needs_reason_dangerous_command: "Dangerous command waiting", needs_reason_login: "Sign-in needed", needs_reason_dialog: "Waiting at a dialog", needs_reason_terminal_input: "May be waiting for input", needs_reason_auth_paused: "Paused: sign-in needed", needs_reason_backend_signin: "Backend sign-in needed", needs_reason_crashed: "Crashed", needs_reason_delivery_uncertain: "Could not confirm delivery", needs_reason_delivery_failed: "Delivery failed", needs_reason_task_review: "Waiting for your confirmation", needs_reason_task_blocked: "Blocked: asks you",
108
109
  },
109
110
  "zh-TW": {
110
111
  brand: "AgEnD", fleet: "Fleet", fleetOverview: "Fleet 概覽", view: "檢視", settings: "設定", instances: "Instances", newInstance: "新增 instance", newAgent: "新增 Agent",
@@ -143,6 +144,7 @@ register("app", {
143
144
  needsNav: "等你處理", needsTab: "待處理", needsCount: "{0} 件待處理",
144
145
  search: "搜尋", searchTitle: "搜尋聊天、agent 之間的訊息與任務({0})",
145
146
  confirmTitle: "請確認", confirmOk: "繼續", cancel: "取消",
146
- needs_reason_hang: "沒有回應", needs_reason_exited: "已結束", needs_reason_assist: "在終端機上等待", needs_reason_permission: "需要權限", needs_reason_dangerous_command: "危險指令待確認", needs_reason_login: "需要登入", needs_reason_dialog: "停在對話框", needs_reason_terminal_input: "可能在等輸入", needs_reason_auth_paused: "已暫停:需要登入", needs_reason_crashed: "已崩潰", needs_reason_delivery_uncertain: "無法確認是否送達", needs_reason_delivery_failed: "傳送失敗", needs_reason_task_review: "等你確認", needs_reason_task_blocked: "卡住,在問你",
147
+ sidePanelLeave: "不儲存就離開「{0}」?你在那裡的修改會遺失。", sidePanelDiscard: "捨棄修改",
148
+ needs_reason_hang: "沒有回應", needs_reason_exited: "已結束", needs_reason_assist: "在終端機上等待", needs_reason_permission: "需要權限", needs_reason_dangerous_command: "危險指令待確認", needs_reason_login: "需要登入", needs_reason_dialog: "停在對話框", needs_reason_terminal_input: "可能在等輸入", needs_reason_auth_paused: "已暫停:需要登入", needs_reason_backend_signin: "後端需要登入", needs_reason_crashed: "已崩潰", needs_reason_delivery_uncertain: "無法確認是否送達", needs_reason_delivery_failed: "傳送失敗", needs_reason_task_review: "等你確認", needs_reason_task_blocked: "卡住,在問你",
147
149
  },
148
150
  });
@@ -16,7 +16,8 @@ import { onPrefsChanged, savePref } from "./app-prefs.js";
16
16
  import { applyPageTheme, pageNow, resolvedMode, themesStore } from "./app-themes.js";
17
17
  import { ErrorState, Skeleton } from "./ui-states.js";
18
18
  import { InstanceNav, RosterNav, namesStore, setNamesPrimary } from "./instance-nav.js";
19
- import { UsageRail, usageRailStore } from "./header-tools.js";
19
+ import { usageRailStore } from "./header-tools.js";
20
+ import { RightRail, sidePanelStore } from "./side-panel.js";
20
21
  import { OwnerCssKeepBar } from "./owner-css.js";
21
22
  import { AgentWidget, fleetBinds, sidebarWidget, sidebarWidgetStore } from "./agent-widget.js";
22
23
 
@@ -89,14 +90,17 @@ export function openSearch(preset = {}) {
89
90
  }
90
91
  /**
91
92
  * Ctrl+K (⌘K) anywhere opens search; Ctrl+F (⌘F) in an agent's chat opens it narrowed to that agent — elsewhere the
92
- * browser's own find stays. Not while another dialog is open (search, open already, has its input focused).
93
+ * browser's own find stays. Not while another dialog is open (search, open already, has its input focused); a docked
94
+ * side panel (#2585) is not one.
93
95
  */
94
96
  export function handleSearchKey(e) {
95
97
  if (!searchOffered(true)) return;
96
98
  if (e.isComposing || e.altKey || e.shiftKey || !(isMacLike() ? e.metaKey : e.ctrlKey)) return;
97
99
  const key = (e.key || "").toLowerCase();
98
100
  if (key !== "k" && key !== "f") return;
99
- if (typeof document !== "undefined" && document.querySelector("dialog[open]")) return;
101
+ // #2585: a docked side panel is not modal — the page beside it is in use, so search is too (a result that leaves
102
+ // the page asks first when the panel holds changes: side-panel.js).
103
+ if (typeof document !== "undefined" && document.querySelector("dialog[open]:not(.side-panel)")) return;
100
104
  const route = navStore.get().route;
101
105
  if (key === "f" && !(route && route.panel === "chat" && route.instance)) return;
102
106
  e.preventDefault();
@@ -127,7 +131,7 @@ export const sidebarWidthStore = createStore({ width: null });
127
131
  function mainBudget() {
128
132
  const vw = viewportWidth();
129
133
  if (vw < 1200 || typeof document === "undefined") return vw;
130
- const rail = document.querySelector(".u-rail");
134
+ const rail = document.querySelector(".shell > .u-rail, .shell > .side-panel"); // #2585: a docked panel in its place
131
135
  if (!rail) return vw;
132
136
  const w = Math.round(rail.getBoundingClientRect ? rail.getBoundingClientRect().width : 0) || RAIL_W;
133
137
  return vw - w;
@@ -181,11 +185,15 @@ function SidebarResize() {
181
185
  // once the page has drawn its change.
182
186
  const again = () => afterPaint(applyKeptSidebarWidth);
183
187
  const offRail = usageRailStore.subscribe(again);
188
+ // #2585: a docked panel in the rail's place (it is published on every render of its editor: only its coming and
189
+ // going changes the room).
190
+ let docked = !!sidePanelStore.get().panel;
191
+ const offPanel = sidePanelStore.subscribe(() => { const now = !!sidePanelStore.get().panel; if (now !== docked) { docked = now; again(); } });
184
192
  const offNav = navStore.subscribe(again);
185
193
  const w = win();
186
194
  if (w && typeof w.addEventListener === "function") w.addEventListener("resize", applyKeptSidebarWidth);
187
195
  return () => {
188
- offRail(); offNav();
196
+ offRail(); offPanel(); offNav();
189
197
  if (w && typeof w.removeEventListener === "function") w.removeEventListener("resize", applyKeptSidebarWidth);
190
198
  if (activeDrag) activeDrag.end(false); // unmounted mid-drag: nothing kept, nothing left behind
191
199
  };
@@ -787,7 +795,7 @@ export function Shell({ panels, onNewInstance, viewOnly = false }) {
787
795
  ${viewOnly || guest || !shell.banner ? null : html`<${shell.banner} />`}
788
796
  <${Outlet} route=${nav.route} seq=${nav.seq} panels=${panels} />
789
797
  </main>
790
- ${guest ? null : html`<${UsageRail} route=${nav.route} />`}
798
+ ${guest ? null : html`<${RightRail} route=${nav.route} />`}
791
799
  <${BottomTabs} route=${nav.route} viewOnly=${viewOnly} />
792
800
  ${shell.dialog ? html`<${shell.dialog.Component} ...${shell.dialog.props} onClose=${closeDialog} />` : null}
793
801
  ${viewOnly || guest || (nav.route && nav.route.panel === "settings") ? null : html`<${PendingChanges} />`}
@@ -36,7 +36,8 @@
36
36
  // tasks — the task board changed: the card numbers a write touched (SSE only: the board reads again when it is shown).
37
37
  // offices — the stored offices changed ({ rev }; SSE only: the Office view reads /ui/offices again).
38
38
  // office_skins — a skin pack was uploaded or deleted ({ rev }; SSE only: the Office view reads /ui/office-skins again).
39
- const EVENTS = ["status", "message", "agent_reply_live", "delivery", "deliveries", "prompt", "prompts", "prompt_resolved", "activity", "needs", "reply_buttons", "reactions", "steps_tick", "clock", "traffic", "delegation", "backend_login", "tasks", "offices", "office_skins"];
39
+ // office_acts — the agents' live acts in the Office view changed ({ acts }, #2578; a poll page reads /ui/office-acts).
40
+ const EVENTS = ["status", "message", "agent_reply_live", "delivery", "deliveries", "prompt", "prompts", "prompt_resolved", "activity", "needs", "reply_buttons", "reactions", "steps_tick", "clock", "traffic", "delegation", "backend_login", "tasks", "offices", "office_skins", "office_acts"];
40
41
  const MAX_HELD = 1000; // live events held during one catch-up read; more → read again afterwards
41
42
  const RETRY_DELAYS = [1000, 2000, 4000]; // after the first failed catch-up read; then it waits for Retry
42
43
  const POLL_LIMIT_MS = 10000; // a poll that has not answered by then counts as failed