@songsid/agend 2.3.3-beta.4 → 2.3.3-beta.5

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 (234) hide show
  1. package/dist/agent-cli.js +13 -112
  2. package/dist/agent-cli.js.map +1 -1
  3. package/dist/agent-endpoint.d.ts +0 -1
  4. package/dist/agent-endpoint.js +1 -9
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/agent-whats-new.d.ts +3 -6
  7. package/dist/agent-whats-new.js +7 -18
  8. package/dist/agent-whats-new.js.map +1 -1
  9. package/dist/auth-api.d.ts +0 -7
  10. package/dist/auth-api.js +1 -15
  11. package/dist/auth-api.js.map +1 -1
  12. package/dist/backend/codex.d.ts +1 -0
  13. package/dist/backend/codex.js +106 -15
  14. package/dist/backend/codex.js.map +1 -1
  15. package/dist/backend/types.d.ts +17 -0
  16. package/dist/backend/types.js.map +1 -1
  17. package/dist/bundled-skills.d.ts +4 -6
  18. package/dist/bundled-skills.js +10 -14
  19. package/dist/bundled-skills.js.map +1 -1
  20. package/dist/channel/mcp-server.js +5 -5
  21. package/dist/channel/mcp-server.js.map +1 -1
  22. package/dist/channel/mcp-tools.d.ts +1 -3
  23. package/dist/channel/mcp-tools.js +4 -58
  24. package/dist/channel/mcp-tools.js.map +1 -1
  25. package/dist/cli.js +3 -1
  26. package/dist/cli.js.map +1 -1
  27. package/dist/config-validator.js +6 -3
  28. package/dist/config-validator.js.map +1 -1
  29. package/dist/daemon.d.ts +11 -3
  30. package/dist/daemon.js +59 -107
  31. package/dist/daemon.js.map +1 -1
  32. package/dist/dashboard-live.d.ts +1 -1
  33. package/dist/dashboard-live.js +3 -3
  34. package/dist/dashboard-live.js.map +1 -1
  35. package/dist/dashboard-permissions.d.ts +1 -1
  36. package/dist/dashboard-permissions.js +1 -3
  37. package/dist/dashboard-permissions.js.map +1 -1
  38. package/dist/fleet-manager.d.ts +26 -44
  39. package/dist/fleet-manager.js +70 -226
  40. package/dist/fleet-manager.js.map +1 -1
  41. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +11 -1
  42. package/dist/general-knowledge/skills/session-management/SKILL.md +6 -0
  43. package/dist/general-knowledge/skills/web-appearance/SKILL.md +13 -204
  44. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +26 -63
  45. package/dist/instance-lifecycle.js +1 -1
  46. package/dist/instance-lifecycle.js.map +1 -1
  47. package/dist/locale.js +2 -0
  48. package/dist/locale.js.map +1 -1
  49. package/dist/outbound-handlers.js +19 -7
  50. package/dist/outbound-handlers.js.map +1 -1
  51. package/dist/outbound-schemas.d.ts +0 -62
  52. package/dist/outbound-schemas.js +2 -41
  53. package/dist/outbound-schemas.js.map +1 -1
  54. package/dist/public-web-gateway.js +0 -9
  55. package/dist/public-web-gateway.js.map +1 -1
  56. package/dist/public-web-link.d.ts +2 -0
  57. package/dist/public-web-link.js +5 -0
  58. package/dist/public-web-link.js.map +1 -1
  59. package/dist/search/search-docs.d.ts +1 -1
  60. package/dist/search/search-docs.js +2 -2
  61. package/dist/search/search-docs.js.map +1 -1
  62. package/dist/settings-api.d.ts +6 -0
  63. package/dist/settings-api.js +14 -2
  64. package/dist/settings-api.js.map +1 -1
  65. package/dist/settings-change.js +3 -1
  66. package/dist/settings-change.js.map +1 -1
  67. package/dist/tool-permissions.js +3 -10
  68. package/dist/tool-permissions.js.map +1 -1
  69. package/dist/topic-commands.d.ts +6 -2
  70. package/dist/topic-commands.js +2 -0
  71. package/dist/topic-commands.js.map +1 -1
  72. package/dist/types.d.ts +2 -1
  73. package/dist/ui/chat-ambient.js +4 -4
  74. package/dist/ui/chat-commands.js +3 -2
  75. package/dist/ui/chat-store.js +4 -1
  76. package/dist/ui/chat-strings.js +6 -42
  77. package/dist/ui/chat-thread.js +6 -44
  78. package/dist/ui/panel-chat.js +21 -50
  79. package/dist/ui/preview-panel.js +3 -15
  80. package/dist/ui/search-dialog.js +30 -14
  81. package/dist/ui/settings-appearance.js +1 -3
  82. package/dist/ui/settings-guests.js +13 -4
  83. package/dist/ui/settings-learn.js +1 -1
  84. package/dist/ui/settings-mods.js +5 -12
  85. package/dist/ui/settings-registry.js +0 -1
  86. package/dist/ui/settings-strings.js +6 -6
  87. package/dist/ui/shared/app-prefs.js +17 -17
  88. package/dist/ui/shared/app-shell.js +12 -8
  89. package/dist/ui/shared/app-store.js +6 -2
  90. package/dist/ui/shared/app.css +4 -36
  91. package/dist/ui/shared/app.js +5 -4
  92. package/dist/ui/shared/instance-nav.js +9 -9
  93. package/dist/ui/shared/instance-switch.js +8 -2
  94. package/dist/ui/shared/panel-view.js +22 -8
  95. package/dist/ui/shared/view-strings.js +6 -0
  96. package/dist/usage/usage-api.d.ts +3 -0
  97. package/dist/usage/usage-api.js +5 -1
  98. package/dist/usage/usage-api.js.map +1 -1
  99. package/dist/view-api.d.ts +2 -0
  100. package/dist/view-api.js +64 -0
  101. package/dist/view-api.js.map +1 -1
  102. package/dist/web-api.d.ts +9 -17
  103. package/dist/web-api.js +255 -371
  104. package/dist/web-api.js.map +1 -1
  105. package/dist/web-appearance-tools.d.ts +0 -2
  106. package/dist/web-appearance-tools.js +2 -5
  107. package/dist/web-appearance-tools.js.map +1 -1
  108. package/dist/web-appearance-view.d.ts +0 -6
  109. package/dist/web-appearance-view.js +0 -1
  110. package/dist/web-appearance-view.js.map +1 -1
  111. package/dist/web-appearance.d.ts +14 -7
  112. package/dist/web-appearance.js +5 -6
  113. package/dist/web-appearance.js.map +1 -1
  114. package/dist/web-asset-parts.d.ts +1 -17
  115. package/dist/web-asset-parts.js +1 -15
  116. package/dist/web-asset-parts.js.map +1 -1
  117. package/dist/web-auth.js +1 -7
  118. package/dist/web-auth.js.map +1 -1
  119. package/dist/web-chat-history.d.ts +3 -7
  120. package/dist/web-chat-history.js +10 -17
  121. package/dist/web-chat-history.js.map +1 -1
  122. package/dist/web-commands.d.ts +9 -1
  123. package/dist/web-commands.js +13 -0
  124. package/dist/web-commands.js.map +1 -1
  125. package/dist/web-experimental.d.ts +2 -11
  126. package/dist/web-experimental.js +8 -36
  127. package/dist/web-experimental.js.map +1 -1
  128. package/dist/web-guest-access.d.ts +40 -2
  129. package/dist/web-guest-access.js +50 -14
  130. package/dist/web-guest-access.js.map +1 -1
  131. package/dist/web-mod-bundle.d.ts +2 -45
  132. package/dist/web-mod-bundle.js +15 -63
  133. package/dist/web-mod-bundle.js.map +1 -1
  134. package/dist/web-mod-store.d.ts +3 -48
  135. package/dist/web-mod-store.js +30 -97
  136. package/dist/web-mod-store.js.map +1 -1
  137. package/dist/web-prefs.d.ts +13 -0
  138. package/dist/web-prefs.js +38 -0
  139. package/dist/web-prefs.js.map +1 -1
  140. package/dist/web-preview.d.ts +1 -11
  141. package/dist/web-preview.js +6 -20
  142. package/dist/web-preview.js.map +1 -1
  143. package/dist/web-session.d.ts +11 -3
  144. package/dist/web-session.js +22 -11
  145. package/dist/web-session.js.map +1 -1
  146. package/dist/web-settings-patch.d.ts +1 -0
  147. package/dist/web-settings-patch.js +6 -3
  148. package/dist/web-settings-patch.js.map +1 -1
  149. package/dist/web-settings-tools.d.ts +0 -13
  150. package/dist/web-settings-tools.js +12 -84
  151. package/dist/web-settings-tools.js.map +1 -1
  152. package/package.json +1 -1
  153. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +0 -376
  154. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +0 -672
  155. package/dist/ui/app-effects.js +0 -251
  156. package/dist/ui/chat-effect-posters.js +0 -42
  157. package/dist/ui/chat-effects.js +0 -167
  158. package/dist/ui/effect-trial-controller.js +0 -124
  159. package/dist/ui/effect-trial-offer.js +0 -2
  160. package/dist/ui/effect-trial-panel.js +0 -40
  161. package/dist/ui/effects-waker.js +0 -2
  162. package/dist/ui/settings-effects.js +0 -202
  163. package/dist/ui/settings-owner-effects.js +0 -82
  164. package/dist/ui/shared/effect-app-runtime.js +0 -299
  165. package/dist/ui/shared/effect-baseline.js +0 -64
  166. package/dist/ui/shared/effect-chat-runtime.js +0 -162
  167. package/dist/ui/shared/effect-cursor.js +0 -126
  168. package/dist/ui/shared/effect-display-metadata.js +0 -39
  169. package/dist/ui/shared/effect-frame.js +0 -146
  170. package/dist/ui/shared/effect-geometry.js +0 -137
  171. package/dist/ui/shared/effect-host-style.js +0 -10
  172. package/dist/ui/shared/effect-owner-regions.js +0 -74
  173. package/dist/ui/shared/effect-owner-source.js +0 -122
  174. package/dist/ui/shared/effect-policy.js +0 -151
  175. package/dist/ui/shared/effect-poster-reader.js +0 -98
  176. package/dist/ui/shared/effect-posters.js +0 -117
  177. package/dist/ui/shared/effect-profiler.js +0 -184
  178. package/dist/ui/shared/effect-profiles.js +0 -65
  179. package/dist/ui/shared/effect-registry.js +0 -545
  180. package/dist/ui/shared/effect-runtime-loader.js +0 -236
  181. package/dist/ui/shared/effect-safety.js +0 -153
  182. package/dist/ui/shared/effect-stock-snapshot.js +0 -43
  183. package/dist/ui/shared/effect-stock-trust.js +0 -217
  184. package/dist/ui/shared/effect-trial-shadow.js +0 -62
  185. package/dist/ui/shared/effect-trial-source.js +0 -103
  186. package/dist/ui/shared/effect-trial-stage.js +0 -170
  187. package/dist/ui/shared/effect-trial.css +0 -40
  188. package/dist/ui/shared/feature-flags.js +0 -127
  189. package/dist/ui/shared/owner-effect-prefs.js +0 -117
  190. package/dist/web-effect-bootstrap.d.ts +0 -12
  191. package/dist/web-effect-bootstrap.js +0 -49
  192. package/dist/web-effect-bootstrap.js.map +0 -1
  193. package/dist/web-effect-bundle.d.ts +0 -28
  194. package/dist/web-effect-bundle.js +0 -171
  195. package/dist/web-effect-bundle.js.map +0 -1
  196. package/dist/web-effect-canonical.d.ts +0 -8
  197. package/dist/web-effect-canonical.js +0 -27
  198. package/dist/web-effect-canonical.js.map +0 -1
  199. package/dist/web-effect-library.d.ts +0 -114
  200. package/dist/web-effect-library.js +0 -530
  201. package/dist/web-effect-library.js.map +0 -1
  202. package/dist/web-effect-playback.d.ts +0 -30
  203. package/dist/web-effect-playback.js +0 -71
  204. package/dist/web-effect-playback.js.map +0 -1
  205. package/dist/web-effect-poster.d.ts +0 -8
  206. package/dist/web-effect-poster.js +0 -52
  207. package/dist/web-effect-poster.js.map +0 -1
  208. package/dist/web-effect-profiles.d.ts +0 -118
  209. package/dist/web-effect-profiles.js +0 -181
  210. package/dist/web-effect-profiles.js.map +0 -1
  211. package/dist/web-effect-service.d.ts +0 -93
  212. package/dist/web-effect-service.js +0 -335
  213. package/dist/web-effect-service.js.map +0 -1
  214. package/dist/web-effect-tools.d.ts +0 -48
  215. package/dist/web-effect-tools.js +0 -129
  216. package/dist/web-effect-tools.js.map +0 -1
  217. package/dist/web-effect-trials.d.ts +0 -86
  218. package/dist/web-effect-trials.js +0 -204
  219. package/dist/web-effect-trials.js.map +0 -1
  220. package/dist/web-effect-upload.d.ts +0 -27
  221. package/dist/web-effect-upload.js +0 -185
  222. package/dist/web-effect-upload.js.map +0 -1
  223. package/dist/web-effect-worker.d.ts +0 -8
  224. package/dist/web-effect-worker.js +0 -170
  225. package/dist/web-effect-worker.js.map +0 -1
  226. package/dist/web-effect.d.ts +0 -103
  227. package/dist/web-effect.js +0 -153
  228. package/dist/web-effect.js.map +0 -1
  229. package/dist/web-experimental-http.d.ts +0 -2
  230. package/dist/web-experimental-http.js +0 -12
  231. package/dist/web-experimental-http.js.map +0 -1
  232. package/dist/web-mod-agent.d.ts +0 -50
  233. package/dist/web-mod-agent.js +0 -207
  234. package/dist/web-mod-agent.js.map +0 -1
@@ -6,8 +6,6 @@ import { register, t } from "/assets/app-i18n.js";
6
6
  import { recordEdit } from "/assets/text-undo.js";
7
7
  import { appStore, useStore } from "/assets/app-store.js";
8
8
  import { ModsCard } from "./settings-mods.js";
9
- import { experimentalComponent } from "/assets/feature-flags.js";
10
- const EffectsCard = experimentalComponent(() => import("./settings-effects.js"), m => m.EffectsCard);
11
9
  import { Icon } from "/assets/ui-icons.js";
12
10
  import { toast } from "/assets/ui-toast.js";
13
11
  import { ownerCssOffHere, ownerCssTrial, safeMode, setOwnerCssActive, setOwnerCssOffHere, startOwnerCssTrial } from "/assets/owner-css.js";
@@ -71,7 +69,7 @@ export function AppearanceSection(props = {}) {
71
69
  return html`${group("groupLook")}<${ThemesCard} ...${props} /><${IdentityColorCard} /><${CardSizeCard} />
72
70
  ${group("groupLayout")}<${SidebarWidgetCard} />
73
71
  ${group("groupCss")}<${OwnerCssCard} />
74
- ${group("groupMods")}<${ModsCard} /><${EffectsCard} />
72
+ ${group("groupMods")}<${ModsCard} />
75
73
  <${RestoreDefaultsCard} ...${props} />`;
76
74
  }
77
75
 
@@ -22,13 +22,16 @@ const PLATFORMS = { discord: "Discord", telegram: "Telegram" };
22
22
  const own = (o, k) => !!o && Object.prototype.hasOwnProperty.call(o, k);
23
23
  const sameList = (a, b) => (a || []).length === (b || []).length && (a || []).every((n) => (b || []).includes(n));
24
24
  const accountKey = (g) => `${g.adapterId}:${g.userId}`;
25
- const copyGrants = (m) => { const out = {}; for (const [k, v] of Object.entries(m || {})) if (v && Array.isArray(v.instances)) out[k] = { ...(v.name ? { name: v.name } : {}), instances: [...v.instances] }; return out; };
25
+ // #2855: a grant's level — "trusted", or chat (the default, so never written).
26
+ const grantOf = (v) => ({ ...(v.name ? { name: v.name } : {}), instances: [...v.instances], ...(v.level === "trusted" ? { level: "trusted" } : {}) });
27
+ const copyGrants = (m) => { const out = {}; for (const [k, v] of Object.entries(m || {})) if (v && Array.isArray(v.instances)) out[k] = grantOf(v); return out; };
26
28
  /** The saved state, from the fleet's config: `grants` null while the fleet still uses #1920's derived access. */
27
29
  export function savedGuests(fleet) {
28
30
  const g = (fleet && fleet.web && fleet.web.guests) || {};
29
31
  return { enabled: g.enabled === true, chat: g.chat !== false, grants: g.grants ? copyGrants(g.grants) : null };
30
32
  }
31
- const sameGrant = (a, b) => !!a && !!b && (a.name || "") === (b.name || "") && sameList(a.instances, b.instances);
33
+ const levelOf = (g) => (g && g.level === "trusted" ? "trusted" : "chat");
34
+ const sameGrant = (a, b) => !!a && !!b && (a.name || "") === (b.name || "") && sameList(a.instances, b.instances) && levelOf(a) === levelOf(b);
32
35
 
33
36
  /**
34
37
  * What changed against the saved state, as the PUT /api/settings/fleet/web body's `guests` (null: nothing), and how
@@ -44,7 +47,7 @@ export function guestsBody(original, f) {
44
47
  for (const k of new Set([...Object.keys(was), ...Object.keys(f.grants)])) {
45
48
  const a = own(was, k) ? was[k] : undefined, b = own(f.grants, k) ? f.grants[k] : undefined;
46
49
  if (a && b && sameGrant(a, b)) continue;
47
- diff[k] = b ? { ...(b.name ? { name: b.name } : {}), instances: [...b.instances] } : null;
50
+ diff[k] = b ? grantOf(b) : null;
48
51
  count++;
49
52
  }
50
53
  // #2365 review: turning grants on is a change by itself, even with no one in them yet (default deny): `grants: {}`.
@@ -235,6 +238,12 @@ export function GuestsSection({ ctx, staged }) {
235
238
  ${removed ? html`<span class="g-state">${tn("grantsWillBeRemoved")}</span>` : added ? html`<span class="g-state">${tn("grantsNew")}</span>` : pill(person)}</div>
236
239
  <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>
237
240
  ${draft && !draft.instances.length ? html`<p class="note g-error" role="alert">${tn("grantsNoAgents")}</p>` : null}
241
+ ${draft ? html`<div class=${`g-level${levelOf(draft) !== levelOf(saved) && saved ? " s-dirty" : ""}`}>
242
+ <span class="note" id=${`level-${k}`}>${tn("grantsLevel", person.name)}</span>
243
+ <div class="seg-inline" role="group" aria-labelledby=${`level-${k}`}>${["chat", "trusted"].map((l) => html`<button key=${l} type="button"
244
+ class=${`btn btn-sm${levelOf(draft) === l ? " btn-primary" : ""}`} aria-pressed=${levelOf(draft) === l ? "true" : "false"} data-level=${l}
245
+ onClick=${() => setGrant(k, { ...draft, level: l === "trusted" ? "trusted" : undefined })}>${tn(l === "trusted" ? "grantsLevelTrusted" : "grantsLevelChat")}</button>`)}</div>
246
+ ${levelOf(draft) === "trusted" ? html`<p class="note">${tn("grantsLevelTrustedHint")}</p>` : null}</div>` : null}
238
247
  ${editing === k && draft ? html`<div class="g-edit"><p class="note">${tn("grantsMaySee", person.name)}</p>
239
248
  <${Names} id=${`grant-${k}`} all=${instances} rooms=${rooms} value=${draft.instances} wrote=${person.wrote} onChange=${(list) => setGrant(k, { ...draft, instances: list })} /></div>` : null}
240
249
  ${readOnly ? null : html`<div class="guest-actions">
@@ -260,7 +269,7 @@ export function GuestsSection({ ctx, staged }) {
260
269
  ${overview && overview.audit ? html`<h4>${tn("grantsRecent")}</h4>${!overview.audit.length ? html`<p class="note" id="grantsRecentNone">${tn("grantsRecentNone")}</p>`
261
270
  : 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}>
262
271
  <span class="g-when">${new Date(e.at).toLocaleString()}</span> <strong>${e.name}</strong>
263
- ${e.kind === "grant" ? html`${e.added.map((n) => html` <span key=${`+${n}`} class="g-wrote g-add">+${n}</span>`)}${e.removed.map((n) => html` <span key=${`-${n}`} class="g-wrote g-del">−${n}</span>`)}
272
+ ${e.kind === "grant" ? html`${e.added.map((n) => html` <span key=${`+${n}`} class="g-wrote g-add">+${n}</span>`)}${e.removed.map((n) => html` <span key=${`-${n}`} class="g-wrote g-del">−${n}</span>`)}${e.level ? html` <span class="g-wrote g-level-to">${tn("grantsAuditLevel", tn(e.level === "trusted" ? "grantsLevelTrusted" : "grantsLevelChat"))}</span>` : null}
264
273
  <span class="note"> · <span class="g-wrote">${tn(e.via === "confirmed" ? "grantsAuditConfirmed" : "grantsAuditDirect")}</span>${e.endedSessions ? html` · <span class="g-wrote">${tn("grantsAuditEnded", e.endedSessions)}</span>` : null}</span>`
265
274
  : html` <span class="note">${e.outcome === "sent" ? tn("grantsAuditCodeSent") : tn("grantsAuditCodeNot", auditOutcome(e.outcome))}</span>`}</li>`)}</ul>`}` : null}
266
275
  ${status && status.kind === "waiting" ? html`<p class="note" role="status">${tn("grantsWaiting")}</p>` : null}
@@ -12,7 +12,7 @@ const DOCS = { en: "https://github.com/songsid/AgEnD/blob/main/docs/web-dashboar
12
12
  export const LEARN_ANCHORS = {
13
13
  colors: ["identity-colours", "識別色"], css: ["custom-css-settings--look", "自訂-css設定--外觀"], themes: ["agents-themes", "agent-的主題"],
14
14
  cards: ["identity-cards", "身分卡"], limits: ["card-upload-limits", "身分卡上傳限制"], widgets: ["widgets", "小工具"], mods: ["mods", "mod"],
15
- effects: ["effects", "效果"], restore: ["restore-defaults", "還原預設值"],
15
+ restore: ["restore-defaults", "還原預設值"],
16
16
  "app-icon": ["app-icon", "app-圖示"],
17
17
  };
18
18
  export function learnHref(topic, language = lang()) {
@@ -4,7 +4,6 @@
4
4
  // parts the person ticks are applied, all or nothing. Custom CSS from a file is never ticked by default. Installed
5
5
  // mods can be downloaded again or uninstalled (what they applied stays: Reset and the CSS controls work on it).
6
6
  // Export builds a file from what is in force now.
7
- import { canvasWorkerEnabled, useCanvasWorker } from "/assets/feature-flags.js";
8
7
  import { html, useEffect, useLayoutEffect, useRef, useState } from "/assets/app-html.js";
9
8
  import { t } from "/assets/app-i18n.js";
10
9
  import { appStore, useStore } from "/assets/app-store.js";
@@ -48,7 +47,6 @@ export function defaultChoice(plan) {
48
47
  for (const it of plan.items) {
49
48
  if (it.part === "theme" || it.part === "identity" || it.part === "ambient") c[it.part] = true;
50
49
  else if (it.part === "widget") c.widgets[it.slot] = true;
51
- else if (it.part === "effect" && canvasWorkerEnabled()) c.effects = true;
52
50
  // ownerCss stays false: "read it before turning it on" (r4 §4).
53
51
  }
54
52
  return c;
@@ -62,7 +60,7 @@ function Swatch({ color }) {
62
60
  return html`<span class="s-mod-sw"><span class="s-mod-sw-chip" ref=${ref} aria-hidden="true"></span><code>${color}</code></span>`;
63
61
  }
64
62
  /** Every field of the checked theme, as it will be applied — nothing left unseen (#1897 review). */
65
- export function ThemeDetail({ theme, pictures, effects }) {
63
+ export function ThemeDetail({ theme, pictures }) {
66
64
  const th = theme || {};
67
65
  const row = (k, v) => html`<div class="s-mod-kv"><span class="k">${k}</span><span class="v">${v}</span></div>`;
68
66
  const pal = (mode) => {
@@ -83,7 +81,6 @@ export function ThemeDetail({ theme, pictures, effects }) {
83
81
  ${th.background ? pic(th.background.image, tn("modBackground")) : null}
84
82
  ${th.background ? row(tn("modBackgroundFit"), `${th.background.fit} · ${Math.round(th.background.opacity * 100)}%`) : null}
85
83
  ${th.avatar ? pic(th.avatar, tn("modAvatar")) : null}
86
- ${(canvasWorkerEnabled() ? effects || [] : []).map((e, i) => html`<div key=${i}>${row(tn("modPlacesEffect"), html`<span><code>${e.effect}</code>${e.id ? html` <span class="muted">${e.id.slice(0, 8)}</span>` : null}${Object.keys(e.params || {}).length ? html` · <code>${JSON.stringify(e.params)}</code>` : null}</span>`)}</div>`)}
87
84
  <details class="s-mod-raw"><summary>${tn("modAllFields")}</summary><pre class="s-mod-css-text">${JSON.stringify(th, null, 2)}</pre></details>
88
85
  </div>`;
89
86
  }
@@ -92,7 +89,7 @@ function PlanItem({ it, choice, setChoice, binds, fleet }) {
92
89
  const tick = (label, on, set) => html`<label class="check s-mod-tick"><input type="checkbox" checked=${on} onChange=${(e) => set(e.target.checked)} /><span>${label}</span></label>`;
93
90
  if (it.part === "theme") {
94
91
  return html`<li class="s-mod-item">${tick(tn("modPartTheme"), choice.theme, (v) => setChoice({ ...choice, theme: v }))}
95
- <div class="s-mod-detail"><${ThemeDetail} theme=${it.theme} pictures=${it.pictures} effects=${it.effects} /></div></li>`;
92
+ <div class="s-mod-detail"><${ThemeDetail} theme=${it.theme} pictures=${it.pictures} /></div></li>`;
96
93
  }
97
94
  if (it.part === "identity") {
98
95
  return html`<li class="s-mod-item">${tick(tn("modPartIdentity"), choice.identity, (v) => setChoice({ ...choice, identity: v }))}
@@ -116,11 +113,8 @@ function PlanItem({ it, choice, setChoice, binds, fleet }) {
116
113
  <div class="s-mod-detail">${one("loop")}${one("click")}
117
114
  <details class="s-mod-raw"><summary>${tn("modAllFields")}</summary><pre class="s-mod-css-text">${JSON.stringify({ loop: it.loop, click: it.click }, null, 2)}</pre></details></div></li>`;
118
115
  }
119
- if (it.part === "effect") {
120
- if (!canvasWorkerEnabled()) return null;
121
- return html`<li class="s-mod-item">${tick(tn("modPartEffect", it.title), choice.effects, (v) => setChoice({ ...choice, effects: v }))}
122
- <div class="s-mod-detail"><span>${kib(it.bytes)} · ${tn("modRegions", it.regions.join(", "))}</span>${it.wants.length ? html`<span>${tn("modWants", it.wants.join(", "))}</span>` : null}</div></li>`;
123
- }
116
+ // #2799: a Canvas Worker effect entry (the server refuses the file that carries one) is not offered.
117
+ if (it.part === "effect") return null;
124
118
  if (it.part === "ownerCss") {
125
119
  return html`<li class="s-mod-item s-mod-css">${tick(tn("modPartCss"), choice.ownerCss, (v) => setChoice({ ...choice, ownerCss: v }))}
126
120
  <p class="s-mod-warn">${tn("modCssWarn")}</p>
@@ -141,7 +135,7 @@ function Plan({ staged, onDone }) {
141
135
  const names = (app.instances || []).map((i) => i.name);
142
136
  const install = async () => {
143
137
  setBusy(true); setError("");
144
- const r = await postJson("/ui/mods/commit", { stage: staged.stage, plan_hash: staged.plan_hash, include: { ...choice, effects: canvasWorkerEnabled() && choice.effects }, ...(toInstance ? { target } : {}) });
138
+ const r = await postJson("/ui/mods/commit", { stage: staged.stage, plan_hash: staged.plan_hash, include: { ...choice, effects: false }, ...(toInstance ? { target } : {}) });
145
139
  setBusy(false);
146
140
  // The stage this plan shows is gone or no longer this plan (replaced on another device, expired): read again —
147
141
  // the page then shows whatever is staged now, with its own, fresh choices (keyed by stage).
@@ -209,7 +203,6 @@ function ExportForm() {
209
203
  }
210
204
 
211
205
  export function ModsCard() {
212
- useCanvasWorker();
213
206
  const [state, setState] = useState(null);
214
207
  const [busy, setBusy] = useState(false);
215
208
  const [error, setError] = useState("");
@@ -28,7 +28,6 @@ const ROWS = [
28
28
  ["look.widget", "look", ".s-widget", "settings.widgetTitle", "widget.sidebar", ["sidebar widget", "widget", "json"], ["側邊欄小工具", "小工具"]],
29
29
  ["look.css", "look", ".s-owner-css", "settings.cssTitle", null, ["custom css", "stylesheet", "style", "css"], ["自訂 CSS", "樣式表", "樣式"]],
30
30
  ["look.mods", "look", ".s-mods", "settings.modsTitle", null, ["mod", "mods", "import", "add-on", "agendmod"], ["mod", "匯入", "擴充"]],
31
- ["look.effects", "look", ".s-effects", null, null, ["effects", "canvas", "ambient", "petals", "click stars"], ["特效", "效果", "花瓣", "氛圍"], L("Effects", "效果")],
32
31
  ["look.restore", "look", ".s-restore", "settings.restoreTitle", null, ["restore defaults", "reset", "default"], ["還原預設值", "重設", "預設"]],
33
32
  // Fleet · Agents
34
33
  ["agents.new", "agents", "#newAgentBtn", "settings.newAgent", null, ["new agent", "create", "add agent", "instance"], ["新增 agent", "建立", "新 agent"]],
@@ -393,16 +393,14 @@ register("settings", {
393
393
  modPartWidget_chat_header: "Chat header widget",
394
394
  modPartWidget_details: "Details page widget",
395
395
  modPartWidget_sidebar: "Your sidebar widget",
396
- modPartEffect: "Effect: {0}",
397
396
  modPartCss: "Custom CSS (on your screens only)",
398
397
  modCssWarn: "Custom CSS from this file — read it before turning it on.",
399
398
  modCssAssets: "{0} files for it ({1})",
400
399
  modAccent: "Accent", modFont: "Font", modPictures: "{0} pictures ({1})", modPlaces: "places {0} effects",
401
400
  modColorsLight: "Light colours", modColorsDark: "Dark colours", modBubble: "Bubbles", modWorkingLabel: "Working label",
402
401
  modBackground: "Background picture", modBackgroundFit: "Background fit and opacity", modAvatar: "Picture (face)",
403
- modPlacesEffect: "Places effect", modAllFields: "Every field, as it will be applied",
402
+ modAllFields: "Every field, as it will be applied",
404
403
  modStageGone: "That import was replaced or ran out of time. Showing what is staged now.",
405
- modRegions: "draws on: {0}", modWants: "hears: {0}",
406
404
  modTarget: "Apply to",
407
405
  modTargetPick: "Choose an agent…",
408
406
  modTargetNeeded: "Choose the agent this look goes to.",
@@ -541,6 +539,8 @@ register("settings", {
541
539
  grantsChoose: "Choose", grantsChosen: "Chosen", grantsMaySee: "{0} may see:", grantsClassicRooms: "ClassicBot rooms", grantsWroteHere: "wrote here", grantsWroteTo: "Wrote to: {0}",
542
540
  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",
543
541
  grantsEditAgents: "Edit agents", grantsRemove: "Remove access", grantsUndoRemove: "Keep access",
542
+ grantsLevel: "What {0} may do", grantsLevelChat: "Chat", grantsLevelTrusted: "Trusted",
543
+ grantsLevelTrustedHint: "Trusted adds watching these agents' screens (read-only) and searching their chats. The screen shows everything the agent's terminal does: other people's messages to it, commands and their output, which can include secrets. Chat and search stay limited to what this person may see.", grantsAuditLevel: "→ {0}",
544
544
  grantsWillBeRemoved: "Access ends when you save.", grantsNew: "New — granted when you save", grantsNoAgents: "Tick at least one agent, or remove this person.",
545
545
  grantsSend: "Send sign-in code", grantsSendAgain: "Send again in {0}", grantsSendSaveFirst: "Save first: a code goes only to saved access.",
546
546
  grantsSent: "Code sent {0} · expires {1}", grantsSentPublic: "It works on the open public link too.", grantsSending: "Sending…",
@@ -1047,16 +1047,14 @@ register("settings", {
1047
1047
  modPartWidget_chat_header: "聊天標頭小工具",
1048
1048
  modPartWidget_details: "詳細資料頁小工具",
1049
1049
  modPartWidget_sidebar: "你的側邊欄小工具",
1050
- modPartEffect: "效果:{0}",
1051
1050
  modPartCss: "自訂 CSS(只套用在你的畫面)",
1052
1051
  modCssWarn: "這是檔案裡的自訂 CSS——開啟前請先讀過。",
1053
1052
  modCssAssets: "附帶 {0} 個檔案({1})",
1054
1053
  modAccent: "強調色", modFont: "字型", modPictures: "{0} 張圖片({1})", modPlaces: "放置 {0} 個效果",
1055
1054
  modColorsLight: "淺色模式顏色", modColorsDark: "深色模式顏色", modBubble: "對話泡泡", modWorkingLabel: "工作中文字",
1056
1055
  modBackground: "背景圖片", modBackgroundFit: "背景填滿方式與不透明度", modAvatar: "圖片(頭像)",
1057
- modPlacesEffect: "放置效果", modAllFields: "所有欄位(套用時的樣子)",
1056
+ modAllFields: "所有欄位(套用時的樣子)",
1058
1057
  modStageGone: "那次匯入已被取代或已逾時。以下顯示目前暫存的內容。",
1059
- modRegions: "繪製於:{0}", modWants: "接收:{0}",
1060
1058
  modTarget: "套用到",
1061
1059
  modTargetPick: "選擇一個 agent…",
1062
1060
  modTargetNeeded: "請選擇這套外觀要套用到哪個 agent。",
@@ -1194,6 +1192,8 @@ register("settings", {
1194
1192
  grantsChoose: "選擇", grantsChosen: "已選擇", grantsMaySee: "{0} 可以看到:", grantsClassicRooms: "ClassicBot 聊天室", grantsWroteHere: "傳過訊息", grantsWroteTo: "傳過訊息給:{0}",
1195
1193
  grantsLastSeen: "最後出現 {0}", grantsRecent: "最近的變更", grantsRecentNone: "還沒有人的存取權變更過。", grantsAuditDirect: "立即生效", grantsAuditConfirmed: "在 General 確認後生效", grantsAuditEnded: "結束了 {0} 個登入", grantsAuditCodeSent: "已寄出登入碼", grantsAuditCodeNot: "登入碼未寄出({0})", grantsAuditDmFailed: "私訊沒有送達", grantsAuditWait: "一分鐘內重複要求", grantsAuditAdminLimit: "已達管理員的登入碼上限",
1196
1194
  grantsEditAgents: "編輯 agent", grantsRemove: "移除存取權", grantsUndoRemove: "保留存取權",
1195
+ grantsLevel: "{0} 可以做什麼", grantsLevelChat: "聊天", grantsLevelTrusted: "信任",
1196
+ grantsLevelTrustedHint: "信任多了:觀看這些 agent 的畫面(唯讀),以及搜尋它們的聊天。畫面會顯示 agent 終端上的一切:別人傳給它的訊息、指令和它們的輸出,其中可能有機密。聊天和搜尋仍只限這個人看得到的內容。", grantsAuditLevel: "→ {0}",
1197
1197
  grantsWillBeRemoved: "儲存後存取權就會結束。", grantsNew: "新增 —— 儲存後授權", grantsNoAgents: "至少勾選一個 agent,或移除這個人。",
1198
1198
  grantsSend: "傳送登入碼", grantsSendAgain: "{0} 後可再傳", grantsSendSaveFirst: "請先儲存:登入碼只會傳給已儲存的授權。",
1199
1199
  grantsSent: "{0} 已傳送 · {1} 到期", grantsSentPublic: "也可以用在目前開啟的公開連結上。", grantsSending: "傳送中…",
@@ -91,15 +91,6 @@ let holding = 0;
91
91
  const held = new Map(); // key → value chosen in Settings, shown here, not saved yet
92
92
  const heldFrom = new Map(); // key → its cached form before it was held (a choice back to it is no change)
93
93
  const heldListeners = new Set();
94
- // #2193: whether the signed-in person has a personal effect slot chosen (their fx.* keys: in the page's data-prefs and
95
- // in every host read, never in this host cache). The effects runtime is loaded only when there is something to play.
96
- const ownerEffectsIn = (values) => !!values && OWNER_EFFECT_PREF_KEYS.some((k) => own(values, k) && values[k] != null && values[k] !== "");
97
- let ownerFx = initial ? ownerEffectsIn(initial.values) : false;
98
- const ownerFxListeners = new Set();
99
- /** Whether the person has any personal effect (fx.page/header/sidebar/box/cursor) chosen, as this page last read. */
100
- export function ownerEffectsChosen() { return ownerFx; }
101
- /** Called when that changes. Returns the unsubscribe. */
102
- export function onOwnerEffectsChanged(fn) { ownerFxListeners.add(fn); return () => ownerFxListeners.delete(fn); }
103
94
 
104
95
  // The host's values into the cache, now — before the sidebar, the tour or the first-run card read their keys. A key
105
96
  // the host does not have: the default, on a device in step with the host; this device's own (to upload) otherwise.
@@ -111,9 +102,20 @@ if (host) {
111
102
  }
112
103
  }
113
104
 
114
- /** This page may change the host's preferences: the signed-in app, not over the public link, not a guest's (#1920). */
115
- export function prefsWritable() {
116
- return !!initial && !guestPage() && !(typeof document !== "undefined" && document.body && document.body.dataset.publicLink === "1");
105
+ /**
106
+ * #2855: the preferences a guest keeps as its own, on the host under its account (the same list as web-prefs.ts
107
+ * GUEST_OWN_PREF_KEYS); the rest of a guest's page follows the host's look and stays on this browser.
108
+ */
109
+ export const GUEST_OWN_PREF_KEYS = Object.freeze(["theme", "lang", "nav.favorites", "nav.sort", "nav.filter"]);
110
+ const GUEST_OWN = new Set(GUEST_OWN_PREF_KEYS);
111
+ /**
112
+ * This page may change a preference on the host: the signed-in app, not over the public link (#1920). A guest's page
113
+ * (#2855): only its own keys — over the public link too, since only its own bucket is written; no key named: none.
114
+ */
115
+ export function prefsWritable(key) {
116
+ if (!initial) return false;
117
+ if (guestPage()) return key !== undefined && GUEST_OWN.has(key);
118
+ return !(typeof document !== "undefined" && document.body && document.body.dataset.publicLink === "1");
117
119
  }
118
120
  /**
119
121
  * The value in force for a key, in memory — never only in this browser's storage (a full storage must not bring back
@@ -140,8 +142,6 @@ function take(body) {
140
142
  if (!isView(body) || (rev !== null && body.rev < rev)) return false;
141
143
  rev = body.rev;
142
144
  host = hostValues(body.values);
143
- const fx = ownerEffectsIn(body.values);
144
- if (fx !== ownerFx) { ownerFx = fx; for (const fn of [...ownerFxListeners]) { try { fn(fx); } catch { /* the rest still hear */ } } }
145
145
  return true;
146
146
  }
147
147
 
@@ -153,7 +153,7 @@ export function savePref(key, value) {
153
153
  if (!hostKey(key)) return;
154
154
  if (holding && HELD_KEYS.has(key)) { hold(key, value); return; }
155
155
  cache(key, value);
156
- if (!prefsWritable()) return;
156
+ if (!prefsWritable(key)) return;
157
157
  pending.set(key, value);
158
158
  mine.set(key, value);
159
159
  clearTimeout(flushTimer);
@@ -328,9 +328,9 @@ function apply(changed) {
328
328
  * refused session or an unreadable store leaves the device unmarked, so the next load tries again.
329
329
  */
330
330
  export async function migratePrefs() {
331
- if (!prefsWritable() || inStep()) return false;
331
+ if (!initial || inStep() || !HOST_KEYS.some((key) => prefsWritable(key))) return false;
332
332
  for (const key of HOST_KEYS) {
333
- if (own(host, key)) continue;
333
+ if (own(host, key) || !prefsWritable(key)) continue;
334
334
  const local = SYNCED[key].fromLocal(readLocal(SYNCED[key].ls));
335
335
  if (local === undefined) continue;
336
336
  let r;
@@ -7,7 +7,7 @@ import { Toasts } from "./ui-toast.js";
7
7
  import { ConfirmHost, confirmDialog } from "./ui-confirm.js";
8
8
  import { ColorDialogHost } from "./identity-color.js";
9
9
  import { t, lang, setLang, onLang } from "./app-i18n.js";
10
- import { appStore, createStore, guestBadge, guestPage, useStore } from "./app-store.js";
10
+ import { appStore, createStore, guestBadge, guestPage, guestTrusted, useStore } from "./app-store.js";
11
11
  import { navStore } from "./app-nav.js";
12
12
  import { toast } from "./ui-toast.js";
13
13
  import { chatPath, viewPath, settingsPath, routeKey, NEEDS_PATH, fleetEntryPath } from "./app-route.js";
@@ -78,7 +78,8 @@ export const searchShortcut = () => (isMacLike() ? "⌘K" : "Ctrl K");
78
78
  export function searchOffered(admin) {
79
79
  const body = typeof document !== "undefined" ? document.body : null;
80
80
  const offLink = !!body && body.dataset.publicLink === "1" && body.dataset.searchLink === "0";
81
- return !!admin && !guestPage() && !offLink;
81
+ // #2855: a trusted guest searches too (only what it can see: the server's rule).
82
+ return (guestPage() ? guestTrusted() : !!admin) && !offLink;
82
83
  }
83
84
  let searchModule = null;
84
85
  /** Open search; `preset.instance`: narrowed to that agent (Ctrl+F in its chat). */
@@ -107,6 +108,10 @@ export function handleSearchKey(e) {
107
108
  openSearch(key === "f" ? { instance: route.instance } : {});
108
109
  }
109
110
 
111
+ /** The sidebar's Search row (Ctrl+K / ⌘K). */
112
+ const searchRow = () => html`<button type="button" class="side-row side-search" onClick=${() => openSearch()} title=${t("app.searchTitle", searchShortcut())}
113
+ aria-keyshortcuts=${isMacLike() ? "Meta+K" : "Control+K"}><${Icon} name="search" /><span class="grow">${t("app.search")}</span><kbd class="kbd-hint" aria-hidden="true">${searchShortcut()}</kbd></button>`;
114
+
110
115
  function readCollapsed() { try { return localStorage.getItem("agend_sidebar") === "hidden"; } catch { return false; } }
111
116
  const narrow = () => typeof matchMedia === "function" && matchMedia("(max-width: 899px)").matches;
112
117
 
@@ -350,6 +355,7 @@ function Sidebar({ route, onNewInstance, viewOnly }) {
350
355
  <a class="brand" href="/ui" onClick=${closeDrawer}>${t("app.brand")}</a>
351
356
  <button type="button" class="icon-btn side-collapse" onClick=${toggleSidebar} aria-label=${narrow() ? t("app.closeMenu") : t("app.collapse")} title=${narrow() ? t("app.closeMenu") : t("app.collapse")} aria-controls="sidebar"><${Icon} name="sidebar" /></button>
352
357
  </div>
358
+ ${searchOffered(false) ? html`<nav data-trust="navigation" class="side-nav" aria-label=${t("app.menu")}>${searchRow()}</nav>` : null}
353
359
  <${InstanceNav} route=${route} items=${app.instances} loaded=${app.ready !== false} exec=${app.exec} awaiting=${app.awaiting} onPick=${closeDrawer} />
354
360
  <div class="side-foot" data-trust="navigation">
355
361
  <${GuestChip} />
@@ -372,8 +378,7 @@ function Sidebar({ route, onNewInstance, viewOnly }) {
372
378
  <nav data-trust="navigation" class="side-nav" aria-label=${t("app.menu")}>
373
379
  ${overview}
374
380
  ${navLink("needs", NEEDS_PATH, "inbox", t("app.needsNav"), needsCount)}
375
- ${searchOffered(canCreate) ? html`<button type="button" class="side-row side-search" onClick=${() => openSearch()} title=${t("app.searchTitle", searchShortcut())}
376
- aria-keyshortcuts=${isMacLike() ? "Meta+K" : "Control+K"}><${Icon} name="search" /><span class="grow">${t("app.search")}</span><kbd class="kbd-hint" aria-hidden="true">${searchShortcut()}</kbd></button>` : null}
381
+ ${searchOffered(canCreate) ? searchRow() : null}
377
382
  ${canCreate && onNewInstance ? html`<button type="button" class="side-row side-new" onClick=${onNewInstance}><${Icon} name="plus" /><span class="grow">${t("app.newAgent")}</span></button>` : null}
378
383
  </nav>
379
384
  ${shell.side ? html`<${shell.side.Component} />` : html`<${InstanceNav} route=${route} items=${app.instances} loaded=${app.ready !== false}
@@ -393,8 +398,7 @@ function GuestChip() {
393
398
  const { name, platform } = guestBadge();
394
399
  const where = PLATFORM_NAMES[platform] || platform;
395
400
  const label = name ? (where ? t("app.guestAsOn", name, where) : t("app.guestAs", name)) : t("app.guestChip");
396
- // Inside .side-foot (a marked, stock-trusted element): no trust marker of its own — effect-stock-trust's closed inventory
397
- // refuses any element it does not know, which would hide a guest's static effect posters (#2101 review).
401
+ // Inside .side-foot (a marked element): no trust marker of its own (#2101 review).
398
402
  return html`<p class="guest-chip" title=${t("app.guestHint")}><${Icon} name="user" size=${14} /><span class="grow">${label}</span></p>`;
399
403
  }
400
404
 
@@ -781,7 +785,7 @@ export function Shell({ panels, onNewInstance, viewOnly = false }) {
781
785
  document.removeEventListener("compositionstart", start, true); document.removeEventListener("compositionend", end, true);
782
786
  };
783
787
  }, []);
784
- const guest = guestPage(); // #1920: a guest's page — no operations, usage, pending changes or owner CSS
788
+ const guest = guestPage(); // #1920: a guest's page — no operations, pending changes or owner CSS (#2855: usage, yes)
785
789
  const cls = ["shell", shell.collapsed ? "sb-collapsed" : "", shell.drawer ? "sb-open" : "", shell.keyboard ? "kb-open" : "", guest ? "guest" : ""].filter(Boolean).join(" ");
786
790
  return html`<div ref=${root} class=${cls}>
787
791
  <a data-trust="navigation" class="skip" href="#main">${t("app.skip")}</a>
@@ -795,7 +799,7 @@ export function Shell({ panels, onNewInstance, viewOnly = false }) {
795
799
  ${viewOnly || guest || !shell.banner ? null : html`<${shell.banner} />`}
796
800
  <${Outlet} route=${nav.route} seq=${nav.seq} panels=${panels} />
797
801
  </main>
798
- ${guest ? null : html`<${RightRail} route=${nav.route} />`}
802
+ <${RightRail} route=${nav.route} />
799
803
  <${BottomTabs} route=${nav.route} viewOnly=${viewOnly} />
800
804
  ${shell.dialog ? html`<${shell.dialog.Component} ...${shell.dialog.props} onClose=${closeDialog} />` : null}
801
805
  ${viewOnly || guest || (nav.route && nav.route.panel === "settings") ? null : html`<${PendingChanges} />`}
@@ -24,7 +24,7 @@ export function createStore(initial) {
24
24
  * (app-stream.js catchUp) — "catching" / "retrying" / "failed" / "ok"; `retryHydration` asks for it again.
25
25
  * `connections`: how many chat connections the fleet has (#1519 P7: 0 is web-only — the first-run card); null until known. */
26
26
  export const appStore = createStore({ ready: false, instances: [], uptime: 0, exec: {}, awaiting: {}, connection: "connecting", needs: [], connections: null,
27
- hydration: "none", retryHydration: null, canvasWorker: typeof document !== "undefined" && document.body?.dataset.canvasWorker === "1" });
27
+ hydration: "none", retryHydration: null });
28
28
 
29
29
  /**
30
30
  * #1920 phase 3: this page is a guest's — the server says so on <body> (data-tier="guest"). A guest sees only its own
@@ -34,6 +34,10 @@ export const appStore = createStore({ ready: false, instances: [], uptime: 0, ex
34
34
  export function guestPage() {
35
35
  return typeof document !== "undefined" && !!document.body && document.body.dataset.tier === "guest";
36
36
  }
37
+ /** #2855: this guest's access is Trusted — its page offers View (read-only) and search (the server's, on <body>). */
38
+ export function guestTrusted() {
39
+ return guestPage() && document.body.dataset.guestLevel === "trusted";
40
+ }
37
41
  /** Who a guest's page is signed in as: its web name and its platform (the server's, on <body>). */
38
42
  export function guestBadge() {
39
43
  const d = typeof document !== "undefined" && document.body ? document.body.dataset : {};
@@ -49,7 +53,7 @@ export function applyStatus(d) {
49
53
  exec[i.name] = (i.execution_state !== undefined ? i.execution_state : i.state) ?? null;
50
54
  if (i.state === "awaiting_input") awaiting[i.name] = typeof i.interaction_summary === "string" ? i.interaction_summary : "";
51
55
  }
52
- appStore.set({ canvasWorker: d.canvas_worker === true, ready: true, instances: d.instances, uptime: d.uptime ?? 0, exec, awaiting,
56
+ appStore.set({ ready: true, instances: d.instances, uptime: d.uptime ?? 0, exec, awaiting,
53
57
  startup: d.startup?.phase === "starting" ? d.startup : null,
54
58
  connections: typeof d.connections === "number" ? d.connections : null,
55
59
  // #1665: the host preferences' revision (app-prefs.js re-reads them when it moves).
@@ -952,6 +952,8 @@ img.amb-p { object-fit: contain; }
952
952
  .s-guests .g-chips { display: flex; flex-wrap: wrap; gap: var(--s-1); margin: var(--s-2) 0 0 calc(28px + var(--s-2)); }
953
953
  .s-guests .g-chip { padding: 1px var(--s-2); border: 1px solid color-mix(in srgb, var(--text-3) 55%, transparent); border-radius: 0; font-size: var(--fs-sm); color: var(--text-2); white-space: nowrap; }
954
954
  .s-guests .g-edit { margin: var(--s-2) 0 0 calc(28px + var(--s-2)); }
955
+ .s-guests .g-level { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); margin: var(--s-2) 0 0 calc(28px + var(--s-2)); }
956
+ .s-guests .g-level > .note:last-child { flex-basis: 100%; margin: 0; }
955
957
  .s-guests .guest-actions { display: flex; flex-wrap: wrap; gap: var(--s-1); margin: var(--s-2) 0 0 calc(28px + var(--s-2)); }
956
958
  .s-guests .g-error { color: var(--danger); margin-left: calc(28px + var(--s-2)); }
957
959
  .s-guests .g-search { width: 100%; max-width: 360px; margin: var(--s-1) 0 var(--s-2); }
@@ -1265,6 +1267,7 @@ body.side-resizing, body.side-resizing * { cursor: col-resize !important; user-s
1265
1267
  .v-pre .ansi[data-b] { font-weight: var(--fw-bold); }
1266
1268
  /* #1582: faint text (SGR 2) — a CLI's suggestion or placeholder, never input — stays faint here. */
1267
1269
  .v-pre .ansi[data-dim] { opacity: .5; }
1270
+ .v-watching { flex-shrink: 0; margin: 0; padding: var(--s-2) var(--s-4); font-size: var(--fs-sm); color: var(--text-2); background: var(--surface); border-bottom: 1px solid var(--border); }
1268
1271
  .v-term-note { position: absolute; left: var(--s-3); right: var(--s-3); top: var(--s-3); padding: var(--s-2) var(--s-3); border-radius: var(--r-sm); background: var(--warn-bg); border: 1px solid var(--warn-line); color: #f0f0f0; font-size: var(--fs-sm); }
1269
1272
  .v-probe { position: absolute; visibility: hidden; white-space: pre; left: -9999px; top: 0; font-size: 100px; }
1270
1273
  .help-list { list-style: disc; padding-left: var(--s-5); display: flex; flex-direction: column; gap: var(--s-2); font-size: var(--fs-sm); }
@@ -2201,17 +2204,6 @@ html.oo-immersive .p-fleet > .panel-body { z-index: 50; }
2201
2204
  .s-limit-warnings { margin: var(--s-2) 0; padding-left: var(--s-4); }
2202
2205
  @media (max-width: 479px) { .grid3 { grid-template-columns: minmax(0, 1fr); } }
2203
2206
 
2204
- /* #1629 B2: effect library controls. No runtime layer is created by this card. */
2205
- .s-fx-actions { display: flex; flex-wrap: wrap; gap: var(--s-2); align-items: center; }
2206
- .s-fx-item { margin-top: var(--s-3); padding-top: var(--s-3); border-top: 1px solid var(--border); }
2207
- .s-fx-item h4 { margin: 0; }
2208
- .s-fx-poster { display: block; max-width: min(240px, 100%); max-height: 160px; object-fit: contain; border-radius: var(--r-sm); }
2209
-
2210
- .s-fx-chat, .s-fx-params label:not(.check) { display: flex; align-items: center; flex-wrap: wrap; gap: var(--s-2); }
2211
- .s-fx-chat { margin: var(--s-2) 0; }
2212
- .s-fx-params { display: flex; flex-direction: column; gap: var(--s-2); margin: var(--s-2) 0; max-width: 100%; }
2213
- .s-fx-target, .s-fx-params input:not([type=checkbox]), .s-fx-params select { min-height: 36px; min-width: 0; max-width: 100%; padding: 0 var(--s-2); border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--bg); color: var(--text); }
2214
-
2215
2207
  /* #1629: stock paint bands. Structural ancestors remain neutral; only bounded content owns level 1.
2216
2208
  The viewport effect root is a direct body sibling: behind 0 / stock content 1 (header 2) / front 20.
2217
2209
  Nested menus cannot cross a content stacking context: the unmounted trust guard suspends fronts for overlays. */
@@ -2222,38 +2214,14 @@ html.oo-immersive .p-fleet > .panel-body { z-index: 50; }
2222
2214
  /* Header menus must stay above the later scroller stacking context. */
2223
2215
  .panel-head { z-index: 2; }
2224
2216
  @media (min-width: 900px) { .chat-split > .pv-divider, .chat-split > .pv-panel { position: relative; z-index: 1; } }
2225
- /* #1629: owner choices share the existing control font floor. */
2226
- .s-fx-owner{margin-top:var(--s-4);padding-top:var(--s-3);border-top:1px solid var(--border)}
2227
- .s-fx-owner-row{display:flex;align-items:end;flex-wrap:wrap;gap:var(--s-2);margin-top:var(--s-2)}
2228
- .s-fx-owner-row>label{display:flex;flex-direction:column;gap:var(--s-1);min-width:0;flex:1}
2229
-
2230
- /* #2044: the effect frame's document declares no colour scheme (light). An iframe whose own colour scheme differs
2231
- from its document's gets an opaque backdrop, so under the dark theme the frame hid the whole chat: keep them equal. */
2232
- .agend-effect-frame { color-scheme: normal !important; }
2233
-
2234
- /* #1629: host-created static poster IMG; geometry and visibility belong to the trusted CSSOM binding. */
2235
- .agend-effect-poster { object-fit: cover !important; object-position: center !important; user-select: none; }
2236
-
2237
2217
  /* #1629: full-app bounded stock surfaces. Panel roots stay neutral; effects never raise the whole app. */
2238
2218
  .p-fleet > .seg, .p-settings > .seg,
2239
2219
  .p-fleet > .panel-body, .p-needs > .panel-body, .p-settings > .panel-body,
2240
2220
  .p-log > .panel-body, .p-details > .panel-body, .p-view > .panel-body,
2241
- .p-log > .log-bar, .p-view > .v-term, .p-view > .v-id { position: relative; z-index: 1; }
2221
+ .p-log > .log-bar, .p-view > .v-term, .p-view > .v-id, .p-view > .v-watching { position: relative; z-index: 1; }
2242
2222
  /* Keep the existing Apply bar's sticky behavior, in its own bounded stock band. */
2243
2223
  .p-settings > .s-pending { z-index: 1; }
2244
2224
 
2245
- /* #1629: host-owned local Trial panel. Only its canvas frame lives inside the paint-contained stage. */
2246
- .effect-trial-head, .effect-trial-controls, .effect-trial-note { position: relative; z-index: 1; background: var(--bg); padding: var(--s-3); margin: 0; }
2247
- .effect-trial-head { display: flex; align-items: center; gap: var(--s-2); border-bottom: 1px solid var(--border); }
2248
- .effect-trial-head > span { flex: 1; min-width: 0; overflow-wrap: anywhere; }
2249
- .effect-trial-body { flex: 1; min-height: 0; display: flex; padding: var(--s-3); }
2250
- .effect-trial-stage { position: relative; z-index: 0; flex: 1; min-width: 0; min-height: 0; contain: paint; overflow: clip; padding: 0; border: 0; outline: none; box-shadow: none; }
2251
-
2252
- /* #2115: diagnostics remain inside the stock protected panel header, never in an effect plane. */
2253
- .panel-head:has(> .chat-fx-notice) { flex-wrap: wrap; }
2254
- .chat-fx-notice { order: 10; flex: 1 1 100%; min-width: 0; max-width: 100%; font-size: var(--fs-sm); overflow-wrap: anywhere; }
2255
- .chat-fx-notice > span { display: block; }
2256
-
2257
2225
  /* ── #2293: global search — the sidebar row's shortcut, the header's button (collapsed sidebar, phone), the dialog ── */
2258
2226
  /* #2478: a touch screen has no Ctrl/⌘ K — the shortcut's hint is not shown there (the row is). */
2259
2227
  @media (pointer: coarse) { .side-search .kbd-hint { display: none; } }
@@ -13,7 +13,7 @@ import { installSwipe } from "./app-swipe.js";
13
13
  import { installTextUndo } from "./text-undo.js";
14
14
  import { startRouter, navStore, navigate } from "./app-nav.js";
15
15
  import { createStream } from "./app-stream.js";
16
- import { appStore, applyStatus, applyActivity, guestPage } from "./app-store.js";
16
+ import { appStore, applyStatus, applyActivity, guestPage, guestTrusted } from "./app-store.js";
17
17
  import { chatPath } from "./app-route.js";
18
18
  import { startNeedsNotifier } from "./app-needs.js";
19
19
  import { toast } from "./ui-toast.js";
@@ -101,7 +101,8 @@ if (mode === "full") {
101
101
  import("/ui/js/backend-signin.js").then((m) => m.installSignIn(), () => {});
102
102
  }
103
103
  // #1665: this device's first upload of its own choices (once), then another device's changes at each status frame.
104
- // A guest's page only reads them (prefsWritable is false there).
104
+ // A guest's page (#2855) uploads and saves only its own keys (prefsWritable(key)); another device's changes reach it at
105
+ // its next load (its status frames carry no prefs_rev).
105
106
  bootPrefs();
106
107
  // #1629: agents' themes, read when the status frame's themes_rev moves (a guest's frames never carry it).
107
108
  if (!guest) startThemes();
@@ -127,8 +128,8 @@ if (!guest) installOwnerCss();
127
128
  }
128
129
 
129
130
  document.addEventListener("keydown", handleKey);
130
- // #2293: Ctrl+K / ⌘K (and Ctrl+F in a chat) — an admin's full app only; the server refuses a guest's search anyway.
131
- if (mode === "full" && !guest) document.addEventListener("keydown", handleSearchKey);
131
+ // #2293: Ctrl+K / ⌘K (and Ctrl+F in a chat) — an admin's full app, and a trusted guest's (#2855: what it can see).
132
+ if (mode === "full" && (!guest || guestTrusted())) document.addEventListener("keydown", handleSearchKey);
132
133
  // #2342: Ctrl/⌘+Z and Ctrl+Shift+Z / Ctrl+Y / ⌘⇧Z in every text field, one history per field (the browser's is one
133
134
  // for the whole page, and any script write wipes it).
134
135
  installTextUndo({ mac: isMacLike() });
@@ -504,18 +504,18 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
504
504
  const known = new Set(byName.keys());
505
505
  const favs = nav.favorites.filter((n) => known.has(n));
506
506
  const favSet = new Set(favs);
507
- const canStar = !viewOnly && prefsWritable();
508
- // #1920: a guest's list is to pick from — no reordering and no filters (one this device kept would hide rows with no
509
- // way to clear it).
507
+ const canStar = !viewOnly && prefsWritable("nav.favorites");
508
+ // #1920: a guest's list is not reordered (the order is the host's). #2855: it stars, sorts and filters as its own
509
+ // preferences (kept on the host under its account), with reply times that only count replies its channel showed.
510
510
  const guest = guestPage();
511
- // #2179: when each row last replied (a guest's list has no times). "recent" mode (never a guest's): the favorites keep
511
+ // #2179: when each row last replied (a guest's: replies its channel showed). "recent" mode: the favorites keep
512
512
  // their order or are sorted too, and every other row is one list, the last reply first. "custom" (the default) is the
513
513
  // person's own order — a reply never moves a row.
514
514
  const now = Date.now();
515
515
  // The anonymous View reader (viewOnly) has no reply times (/api/profiles carries none) and no way to change the sort:
516
516
  // its list is always Custom, whatever this browser kept from a signed-in page (#2181 review).
517
- const ageOf = (n) => (guest || viewOnly ? null : replyAge(byName.get(n), now));
518
- const recent = !guest && !viewOnly && nav.sort.mode === "recent";
517
+ const ageOf = (n) => (viewOnly ? null : replyAge(byName.get(n), now));
518
+ const recent = !viewOnly && nav.sort.mode === "recent";
519
519
  const favsSorted = recent && nav.sort.favs === "recent";
520
520
  let shownGroups, shownInsts;
521
521
  if (recent) {
@@ -528,7 +528,7 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
528
528
  shownInsts = new Map([...instByGroup].map(([g, names]) => [g, names.filter((n) => !favSet.has(n))]));
529
529
  if (favs.length) shownInsts.set(FAV_GROUP, favs);
530
530
  }
531
- const f = guest ? { q: "", status: [], cli: [] } : nav.filter;
531
+ const f = nav.filter;
532
532
  const shown = filterSidebar(shownGroups, shownInsts, byName, f.q, backendLabel, { status: f.status, cli: f.cli, statusOf: facetOf });
533
533
  // #1936: while a filter is on, the instance you have open stays on top, pinned and marked "Current", whether or not
534
534
  // it matches; a match is not repeated below. "N / M" still counts the filter's matches (the pinned row counts only
@@ -626,7 +626,7 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
626
626
  return html`<div class=${`view-roster inst-nav${editing && canStar ? " fav-editing" : ""}`}>
627
627
  <div data-trust="navigation" class="side-section" id="instanceList" ref=${list}>
628
628
  <div class="inst-head"><h2 class="side-label">${tn("instancesLabel")}</h2>
629
- ${canStar && items.length && !guest ? html`<${SortMenu} sort=${nav.sort} />`
629
+ ${canStar && items.length ? html`<${SortMenu} sort=${nav.sort} />`
630
630
  : recent ? html`<span class="chip-btn nav-sort-btn on nav-sort-static"><${Icon} name="sort" size=${12} /><span>${tn("sortRecent")}</span></span>` : null}
631
631
  ${canStar && items.length ? html`<button type="button" class="chip-btn fav-edit" aria-pressed=${editing ? "true" : "false"} onClick=${() => setEditing(!editing)}>
632
632
  ${editing ? tn("favDone") : html`<${Icon} name="star" size=${12} />${tn("favEdit")}`}</button>` : null}</div>
@@ -653,7 +653,7 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
653
653
  })}
654
654
  ${loaded && shown.active && !shown.groups.length ? html`<p class="side-empty">${tn("filterNone")}</p>` : null}
655
655
  </div>
656
- ${guest ? null : html`<div class="v-filter">
656
+ ${html`<div class="v-filter">
657
657
  <label class="v-filter-box"><${Icon} name="search" size=${14} /><span class="sr-only">${tn("filterPh")}</span>
658
658
  <input ref=${filterRef} id="filterInput" type="text" autocomplete="off" spellcheck=${false} placeholder=${tn("filterPh")} value=${f.q}
659
659
  onInput=${(e) => setNavFilter({ q: e.target.value })}
@@ -1,8 +1,10 @@
1
1
  // #1523 N2 (§3.2): the instance view switch — Chat | View | Details of ONE instance. Each segment is a link to that
2
2
  // view's URL for the same instance, so switching keeps the instance (and the sidebar, which is never remounted).
3
3
  // Desktop: links in the header; a phone: the same four as top tabs under it (app.css). The anonymous View reader
4
- // gets none (Chat and Details need a session): the caller does not render it there.
4
+ // gets none (Chat and Details need a session): the caller does not render it there. #2855: a trusted guest's is Chat |
5
+ // View | Search (search opens on this agent); a chat-level guest gets none.
5
6
  import { html } from "./app-html.js";
7
+ import { openSearch } from "./app-shell.js";
6
8
  import { t } from "./app-i18n.js";
7
9
  import { chatPath, detailsPath, logPath, viewPath } from "./app-route.js";
8
10
 
@@ -10,9 +12,13 @@ import { chatPath, detailsPath, logPath, viewPath } from "./app-route.js";
10
12
  * current: "chat" | "view" | "log" | "details". #1218: Log sits between View and Details. `data-label`: each tab is as
11
13
  * wide as its label in the current tab's weight (app.css), so the bar keeps its size whichever tab is current.
12
14
  */
13
- export function InstanceSwitch({ name, current }) {
15
+ export function InstanceSwitch({ name, current, guest = false }) {
14
16
  const seg = (key, href, label) => html`<a key=${key} class=${`seg-item${current === key ? " active" : ""}`} href=${href}
15
17
  aria-current=${current === key ? "page" : undefined} data-label=${label}>${label}</a>`;
18
+ if (guest) return html`<nav class="inst-switch seg" aria-label=${t("app.instanceViews", name)}>
19
+ ${seg("chat", chatPath(name), t("app.chat"))}${seg("view", viewPath(name), t("app.view"))}<button key="search" type="button" class="seg-item"
20
+ data-label=${t("app.search")} onClick=${() => openSearch({ instance: name })}>${t("app.search")}</button>
21
+ </nav>`;
16
22
  return html`<nav class="inst-switch seg" aria-label=${t("app.instanceViews", name)}>
17
23
  ${seg("chat", chatPath(name), t("app.chat"))}${seg("view", viewPath(name), t("app.view"))}${seg("log", logPath(name), t("app.log"))}${seg("details", detailsPath(name), t("app.details"))}
18
24
  </nav>`;