@songsid/agend 2.3.0-beta.2 → 2.3.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/dist/agent-whats-new.js +2 -0
  2. package/dist/agent-whats-new.js.map +1 -1
  3. package/dist/auth-api.d.ts +30 -1
  4. package/dist/auth-api.js +42 -12
  5. package/dist/auth-api.js.map +1 -1
  6. package/dist/backend/claude-code.d.ts +2 -2
  7. package/dist/backend/claude-code.js +5 -4
  8. package/dist/backend/claude-code.js.map +1 -1
  9. package/dist/backend/types.d.ts +6 -0
  10. package/dist/backend/types.js +15 -0
  11. package/dist/backend/types.js.map +1 -1
  12. package/dist/backend-availability.d.ts +120 -0
  13. package/dist/backend-availability.js +396 -0
  14. package/dist/backend-availability.js.map +1 -0
  15. package/dist/backend-command.d.ts +21 -3
  16. package/dist/backend-command.js +23 -4
  17. package/dist/backend-command.js.map +1 -1
  18. package/dist/channel/adapters/discord.d.ts +7 -0
  19. package/dist/channel/adapters/discord.js +35 -0
  20. package/dist/channel/adapters/discord.js.map +1 -1
  21. package/dist/channel/mcp-tools.js +8 -4
  22. package/dist/channel/mcp-tools.js.map +1 -1
  23. package/dist/channel/types.d.ts +6 -0
  24. package/dist/classic-channel-manager.d.ts +8 -3
  25. package/dist/classic-channel-manager.js +10 -3
  26. package/dist/classic-channel-manager.js.map +1 -1
  27. package/dist/config-validator.js +73 -2
  28. package/dist/config-validator.js.map +1 -1
  29. package/dist/delivery-outbox.js +5 -0
  30. package/dist/delivery-outbox.js.map +1 -1
  31. package/dist/fleet-context.d.ts +10 -0
  32. package/dist/fleet-manager.d.ts +238 -5
  33. package/dist/fleet-manager.js +878 -71
  34. package/dist/fleet-manager.js.map +1 -1
  35. package/dist/fleet-traffic.d.ts +38 -0
  36. package/dist/fleet-traffic.js +96 -0
  37. package/dist/fleet-traffic.js.map +1 -0
  38. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +13 -5
  39. package/dist/general-knowledge/skills/multi-channel/SKILL.md +1 -1
  40. package/dist/general-knowledge/skills/web-appearance/SKILL.md +56 -11
  41. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +398 -0
  42. package/dist/human-delivery-receipts.js +2 -1
  43. package/dist/human-delivery-receipts.js.map +1 -1
  44. package/dist/locale.js +48 -8
  45. package/dist/locale.js.map +1 -1
  46. package/dist/memory-hogs.d.ts +27 -0
  47. package/dist/memory-hogs.js +124 -0
  48. package/dist/memory-hogs.js.map +1 -0
  49. package/dist/outbound-handlers.d.ts +2 -0
  50. package/dist/outbound-handlers.js +40 -1
  51. package/dist/outbound-handlers.js.map +1 -1
  52. package/dist/outbound-schemas.d.ts +92 -0
  53. package/dist/outbound-schemas.js +55 -1
  54. package/dist/outbound-schemas.js.map +1 -1
  55. package/dist/public-web-gateway.js +1 -1
  56. package/dist/public-web-gateway.js.map +1 -1
  57. package/dist/public-web-link.d.ts +9 -0
  58. package/dist/public-web-link.js +8 -0
  59. package/dist/public-web-link.js.map +1 -1
  60. package/dist/quickstart-api.d.ts +10 -1
  61. package/dist/quickstart-api.js +12 -4
  62. package/dist/quickstart-api.js.map +1 -1
  63. package/dist/reply-buttons-controller.d.ts +2 -0
  64. package/dist/reply-buttons-controller.js +1 -1
  65. package/dist/reply-buttons-controller.js.map +1 -1
  66. package/dist/tmux-manager.d.ts +5 -0
  67. package/dist/tmux-manager.js +16 -0
  68. package/dist/tmux-manager.js.map +1 -1
  69. package/dist/tool-permissions.js +4 -0
  70. package/dist/tool-permissions.js.map +1 -1
  71. package/dist/topic-commands.d.ts +5 -0
  72. package/dist/topic-commands.js +16 -0
  73. package/dist/topic-commands.js.map +1 -1
  74. package/dist/types.d.ts +25 -1
  75. package/dist/ui/app-effects.js +33 -9
  76. package/dist/ui/chat-effects.js +62 -4
  77. package/dist/ui/chat-strings.js +40 -4
  78. package/dist/ui/chat-thread.js +17 -6
  79. package/dist/ui/effect-trial-panel.js +19 -16
  80. package/dist/ui/fleet-canvas-layout.js +111 -0
  81. package/dist/ui/fleet-canvas.js +489 -0
  82. package/dist/ui/fleet-org.js +27 -12
  83. package/dist/ui/fleet-traffic.js +224 -0
  84. package/dist/ui/panel-chat.js +12 -5
  85. package/dist/ui/panel-details.js +2 -1
  86. package/dist/ui/panel-fleet.js +32 -11
  87. package/dist/ui/panel-settings.js +6 -4
  88. package/dist/ui/preview.js +3 -2
  89. package/dist/ui/settings-appearance.js +34 -18
  90. package/dist/ui/settings-catalog.js +89 -0
  91. package/dist/ui/settings-dialogs.js +40 -12
  92. package/dist/ui/settings-effects.js +4 -3
  93. package/dist/ui/settings-learn.js +25 -0
  94. package/dist/ui/settings-model.js +6 -2
  95. package/dist/ui/settings-mods.js +4 -3
  96. package/dist/ui/settings-owner-effects.js +8 -8
  97. package/dist/ui/settings-strings.js +40 -38
  98. package/dist/ui/shared/app-i18n.js +2 -2
  99. package/dist/ui/shared/app-prefs.js +1 -1
  100. package/dist/ui/shared/app-shell.js +23 -1
  101. package/dist/ui/shared/app-store.js +12 -1
  102. package/dist/ui/shared/app-stream.js +2 -1
  103. package/dist/ui/shared/app.css +186 -11
  104. package/dist/ui/shared/card-strings.js +4 -0
  105. package/dist/ui/shared/effect-app-runtime.js +1 -1
  106. package/dist/ui/shared/effect-frame.js +10 -5
  107. package/dist/ui/shared/effect-geometry.js +24 -9
  108. package/dist/ui/shared/effect-policy.js +2 -1
  109. package/dist/ui/shared/effect-registry.js +71 -17
  110. package/dist/ui/shared/effect-stock-trust.js +23 -2
  111. package/dist/ui/shared/effect-trial-shadow.js +62 -0
  112. package/dist/ui/shared/effect-trial-stage.js +15 -9
  113. package/dist/ui/shared/effect-trial.css +40 -0
  114. package/dist/ui/shared/identity-card.js +75 -4
  115. package/dist/ui/shared/identity-color.js +3 -3
  116. package/dist/ui/shared/instance-nav.js +20 -4
  117. package/dist/ui/shared/panel-view.js +24 -4
  118. package/dist/ui/shared/view-strings.js +22 -0
  119. package/dist/view-api.d.ts +32 -0
  120. package/dist/view-api.js +55 -4
  121. package/dist/view-api.js.map +1 -1
  122. package/dist/web-api.d.ts +19 -4
  123. package/dist/web-api.js +178 -20
  124. package/dist/web-api.js.map +1 -1
  125. package/dist/web-appearance-policy.d.ts +4 -6
  126. package/dist/web-appearance-policy.js +5 -12
  127. package/dist/web-appearance-policy.js.map +1 -1
  128. package/dist/web-appearance-tools.js +5 -1
  129. package/dist/web-appearance-tools.js.map +1 -1
  130. package/dist/web-appearance-view.d.ts +14 -1
  131. package/dist/web-appearance-view.js +98 -10
  132. package/dist/web-appearance-view.js.map +1 -1
  133. package/dist/web-appearance.d.ts +2 -2
  134. package/dist/web-appearance.js +5 -6
  135. package/dist/web-appearance.js.map +1 -1
  136. package/dist/web-auth.js +1 -1
  137. package/dist/web-auth.js.map +1 -1
  138. package/dist/web-card.d.ts +14 -0
  139. package/dist/web-card.js +40 -6
  140. package/dist/web-card.js.map +1 -1
  141. package/dist/web-channel-echo.d.ts +7 -1
  142. package/dist/web-channel-echo.js +6 -2
  143. package/dist/web-channel-echo.js.map +1 -1
  144. package/dist/web-chat-history.d.ts +40 -0
  145. package/dist/web-chat-history.js +66 -3
  146. package/dist/web-chat-history.js.map +1 -1
  147. package/dist/web-commands.d.ts +1 -0
  148. package/dist/web-effect-tools.js +2 -3
  149. package/dist/web-effect-tools.js.map +1 -1
  150. package/dist/web-effect-trials.d.ts +2 -2
  151. package/dist/web-effect-trials.js +3 -3
  152. package/dist/web-effect-trials.js.map +1 -1
  153. package/dist/web-guest-access.d.ts +125 -0
  154. package/dist/web-guest-access.js +232 -0
  155. package/dist/web-guest-access.js.map +1 -0
  156. package/dist/web-guest-rooms.d.ts +82 -0
  157. package/dist/web-guest-rooms.js +135 -0
  158. package/dist/web-guest-rooms.js.map +1 -0
  159. package/dist/web-identity-palette.d.ts +1 -1
  160. package/dist/web-identity-palette.js +2 -2
  161. package/dist/web-identity-palette.js.map +1 -1
  162. package/dist/web-login.d.ts +47 -0
  163. package/dist/web-login.js +115 -0
  164. package/dist/web-login.js.map +1 -1
  165. package/dist/web-mod-agent.d.ts +1 -1
  166. package/dist/web-mod-agent.js +2 -4
  167. package/dist/web-mod-agent.js.map +1 -1
  168. package/dist/web-mod-store.d.ts +5 -0
  169. package/dist/web-mod-store.js +6 -0
  170. package/dist/web-mod-store.js.map +1 -1
  171. package/dist/web-prefs.d.ts +8 -0
  172. package/dist/web-prefs.js +9 -0
  173. package/dist/web-prefs.js.map +1 -1
  174. package/dist/web-preview.d.ts +6 -2
  175. package/dist/web-preview.js +4 -2
  176. package/dist/web-preview.js.map +1 -1
  177. package/dist/web-returning-device.d.ts +39 -0
  178. package/dist/web-returning-device.js +46 -5
  179. package/dist/web-returning-device.js.map +1 -1
  180. package/dist/web-session.d.ts +58 -10
  181. package/dist/web-session.js +104 -22
  182. package/dist/web-session.js.map +1 -1
  183. package/dist/web-settings-tools.d.ts +81 -0
  184. package/dist/web-settings-tools.js +257 -0
  185. package/dist/web-settings-tools.js.map +1 -0
  186. package/dist/web-theme.d.ts +2 -0
  187. package/dist/web-theme.js +10 -1
  188. package/dist/web-theme.js.map +1 -1
  189. package/package.json +1 -1
@@ -119,7 +119,10 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
119
119
  @media (prefers-color-scheme: light) { :root:not([data-theme="dark"]) .idc { --id: var(--id-light); --id-name: var(--id-name-light); } }
120
120
  :root[data-theme="light"] .idc { --id: var(--id-light); --id-name: var(--id-name-light); }
121
121
  .idc.id-ring .av { box-shadow: 0 0 0 2px var(--id); }
122
- .idc.id-bar { box-shadow: inset 3px 0 0 var(--id); }
122
+ /* #2056: the bar is a plain straight line inside the row: no caps following the row's rounded corners (an inset shadow
123
+ did), and short of the corners, so the hover and selected highlight's radius is never crossed. */
124
+ .idc.id-bar { position: relative; }
125
+ .idc.id-bar::before { content: ""; position: absolute; left: 0; top: var(--s-2); bottom: var(--s-2); width: 3px; background: var(--id); pointer-events: none; }
123
126
  .idc.id-name .inst-name { color: var(--id-name); }
124
127
  .idc-swatches, .idc-places { border: 0; padding: 0; margin: 0 0 var(--s-3); display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
125
128
  .idc-swatches legend, .idc-places legend { font-weight: var(--fw-semibold); margin-bottom: 6px; width: 100%; }
@@ -132,6 +135,20 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
132
135
  .idc-hex { width: 9ch; font-family: var(--font-mono, monospace); }
133
136
  .dlg-actions .dlg-spacer { flex: 1 1 auto; }
134
137
  .idc-preview { padding: 6px; border-radius: 10px; background: var(--bg-sidebar); max-width: 260px; }
138
+ /* #2056 B: Settings → Web appearance — plain group headings, a "?" per card to its docs section, and the colour card's sample row. */
139
+ .s-ap-group { font-size: var(--fs-sm); font-weight: var(--fw-semibold); color: var(--text); margin: var(--s-4) 0 var(--s-2); }
140
+ .s-ap-group:first-child { margin-top: 0; }
141
+ .s-ap-title { display: flex; align-items: center; gap: var(--s-2); }
142
+ /* The fields' placeholders show the defaults: readable, not the browser's faint grey (dark mode). */
143
+ .s-appearance input::placeholder { color: var(--text-3); opacity: 1; }
144
+ /* A card title in Chinese is never upper-cased ("AGENT 的主題"): the small caps are a Latin convention. */
145
+ :lang(zh-Hant) .card h3 { text-transform: none; letter-spacing: 0; }
146
+ .s-learn { display: inline-grid; place-items: center; width: 18px; height: 18px; border-radius: var(--r-full); border: 1px solid var(--border);
147
+ color: var(--text-3); font-size: 11px; text-decoration: none; text-transform: none; letter-spacing: 0; line-height: 1; }
148
+ .s-learn:hover, .s-learn:focus-visible { color: var(--text); border-color: var(--text-3); }
149
+ .s-colors-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-3); margin-bottom: var(--s-2); }
150
+ .s-colors-row .idc-places { margin-bottom: 0; flex: 1 1 220px; }
151
+ .s-colors-sample { flex: 0 1 220px; min-width: 0; }
135
152
  .av-text { overflow: hidden; }
136
153
  .av .av-dot { position: absolute; right: -2px; bottom: -2px; margin: 0; box-shadow: 0 0 0 2px var(--bg-sidebar); }
137
154
  /* #1722: the CLI as a letter on the avatar's top-left corner — monochrome (the #1630 ring keeps the colour), cut out like the dot */
@@ -222,11 +239,15 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
222
239
  }
223
240
  .panel-head .sb-open { display: none; }
224
241
  .sb-collapsed .panel-head .sb-open { display: inline-flex; }
242
+ /* #2074: a collapsed sidebar's "New agent", beside its ☰ (a phone opens the drawer, which has the row). */
243
+ .panel-head .sb-new { margin-left: calc(-1 * var(--s-1)); }
225
244
  /* One row of --header-h on every page (a phone's instance tabs wrap below it): the title, its status and badges on one centre line. */
226
245
  .panel-title { flex: 1; min-width: 0; display: flex; align-items: center; gap: var(--s-3); min-height: calc(var(--header-h) - 2 * var(--s-1)); }
227
246
  .panel-title h1 { font-size: var(--fs-lg); line-height: var(--lh-lg); font-weight: var(--fw-semibold); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; outline: none; }
228
247
  .panel-sub { color: var(--text-2); font-size: var(--fs-sm); white-space: nowrap; }
229
248
  .status { display: inline-flex; align-items: center; gap: 6px; }
249
+ /* #2015: the largest processes, after the host memory chip's text. */
250
+ .host-mem-top { margin-left: var(--s-2); opacity: 0.8; overflow-wrap: anywhere; }
230
251
  .panel-actions { display: flex; align-items: center; gap: var(--s-1); }
231
252
  /* #1523 N2: the instance view switch — beside the title on a wide screen; on a phone, a row of tabs under the header. */
232
253
  .inst-switch.seg { padding: 0; margin: 0; width: auto; max-width: none; flex-shrink: 0; }
@@ -345,6 +366,9 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
345
366
  .field input:focus, .field select:focus, .field textarea:focus, .field-row input:focus { border-color: var(--focus); box-shadow: 0 0 0 3px var(--accent-dim); }
346
367
  .field input:disabled { opacity: 0.6; }
347
368
  .field-row { display: flex; gap: var(--s-1); align-items: center; }
369
+ /* #2072: a model field — the list, and the text box "Other…" opens under it. */
370
+ .model-pick { display: flex; flex-direction: column; gap: var(--s-1); min-width: 0; }
371
+ .model-pick > select, .model-pick > input { width: 100%; }
348
372
  .note { font-size: var(--fs-xs); color: var(--text-3); }
349
373
  .checks { display: flex; flex-direction: column; gap: var(--s-1); max-height: 180px; overflow-y: auto; }
350
374
  .check { display: flex; gap: var(--s-2); align-items: center; font-size: var(--fs-sm); }
@@ -593,8 +617,12 @@ html.lb-open .scroller { overflow: hidden; }
593
617
  .pv-menu[open] { z-index: 6; background: var(--surface); border-radius: var(--r-sm); padding: var(--s-1); box-shadow: var(--shadow-2); }
594
618
  .pv-menu[open] > .chip-btn { display: block; text-align: left; width: max-content; max-width: 260px; white-space: normal; }
595
619
 
620
+ /* #2087: the pill takes no room in the thread — it was a sticky box in the flow, 38 px at the end of the scroll, so
621
+ hiding it 48 px from the bottom took that room away under a reader still scrolling — the wheel's last step was cut
622
+ short, or the view clamped back. Its dock is sticky and 0 px tall; the pill floats above it. */
623
+ .jump-dock { position: sticky; bottom: 0; height: 0; z-index: 5; }
596
624
  .jump-latest {
597
- position: sticky; bottom: var(--s-3); left: 50%; transform: translateX(-50%); z-index: 5; display: inline-flex; align-items: center; gap: var(--s-1);
625
+ position: absolute; bottom: var(--s-3); left: 50%; transform: translateX(-50%); white-space: nowrap; display: inline-flex; align-items: center; gap: var(--s-1);
598
626
  background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: var(--r-full); box-shadow: var(--shadow-2);
599
627
  padding: 6px var(--s-4); font-size: var(--fs-xs); font-weight: var(--fw-semibold); cursor: pointer;
600
628
  }
@@ -725,6 +753,7 @@ html.lb-open .scroller { overflow: hidden; }
725
753
  .sb-open .sidebar { transform: none; visibility: visible; }
726
754
  .sb-open .scrim { display: block; position: fixed; inset: 0; background: var(--scrim); z-index: 39; }
727
755
  .panel-head .sb-open { display: inline-flex; }
756
+ .panel-head .sb-new { display: none; }
728
757
  .only-narrow { display: inline-flex; }
729
758
  .thread { padding: var(--s-3) var(--s-3) var(--s-2); gap: var(--s-5); }
730
759
  .col { padding: var(--s-3) var(--s-3) var(--s-8); }
@@ -1122,6 +1151,114 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
1122
1151
  content: ""; position: absolute; top: var(--s-5); left: calc(-1 * var(--s-4) - 2px); width: var(--s-4); height: 2px; background: var(--border-strong); }
1123
1152
  .org-link { min-height: var(--tap); }
1124
1153
  }
1154
+ /* ── #1918: the org chart as a canvas (fleet-canvas.js). SVG with classes only: per-card values are SVG attributes, the
1155
+ view's transform one attribute on .oc-world (#1300). The stage fills the panel's height; touch gestures are the
1156
+ canvas's own (touch-action: none on it only), the page around it still scrolls. ── */
1157
+ .oc-bar { flex-wrap: wrap; row-gap: var(--s-2); }
1158
+ .oc-tools { margin-left: auto; display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); }
1159
+ .oc-find { flex: 0 1 260px; min-width: 150px; position: relative; }
1160
+ .oc-find input { height: 32px; padding-right: 72px; }
1161
+ .oc-found { position: absolute; right: var(--s-3); top: 50%; transform: translateY(-50%); font-size: var(--fs-xs); color: var(--text-3); white-space: nowrap; pointer-events: none; }
1162
+ .oc-stage { position: relative; height: clamp(420px, calc(100dvh - 240px), 960px); border: 1px solid var(--border); border-radius: var(--r-lg); overflow: hidden; background: var(--bg); }
1163
+ .oc-svg { display: block; width: 100%; height: 100%; touch-action: none; cursor: grab; user-select: none; -webkit-user-select: none; }
1164
+ .oc-svg:active { cursor: grabbing; }
1165
+ .oc-svg:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
1166
+ .oc-tree path { fill: none; stroke: var(--border-strong); stroke-width: 1.25; }
1167
+ .oc-box { fill: var(--surface); stroke: var(--border); stroke-width: 1; }
1168
+ .oc-team.loose .oc-box { stroke-dasharray: 5 4; }
1169
+ .oc-box.match { stroke: var(--focus); stroke-width: 2.5; }
1170
+ .oc-head { fill: var(--surface-2); }
1171
+ .oc-hit { fill: transparent; }
1172
+ .oc-toggle:not(.empty) { cursor: pointer; }
1173
+ .oc-toggle:focus { outline: none; }
1174
+ .oc-toggle:focus-visible .oc-hit { stroke: var(--focus); stroke-width: 2; }
1175
+ .oc-caret { fill: var(--text-2); font-size: 12px; }
1176
+ .oc-team-name { fill: var(--text); font-size: 13px; font-weight: 600; }
1177
+ .oc-team-meta { fill: var(--text-3); font-size: 11px; }
1178
+ .oc-team-sum { fill: var(--text-2); font-size: 11.5px; }
1179
+ .oc-team.dim { opacity: 0.55; }
1180
+ .oc-node.dim { opacity: 0.4; }
1181
+ .oc-node { cursor: pointer; }
1182
+ .oc-node[role="img"] { cursor: default; }
1183
+ .oc-node:focus { outline: none; }
1184
+ .oc-card { fill: var(--bg); stroke: var(--border); stroke-width: 1; }
1185
+ .oc-node.lead .oc-card { fill: var(--surface); stroke: var(--accent); stroke-width: 1.5; }
1186
+ .oc-node:hover .oc-card { stroke: var(--border-strong); }
1187
+ .oc-node:focus-visible .oc-card, .oc-node.match .oc-card, .oc-node.match:hover .oc-card { stroke: var(--focus); stroke-width: 2.5; }
1188
+ .oc-node.st-missing { opacity: 0.7; }
1189
+ .oc-av { fill: var(--surface-2); stroke: var(--surface); stroke-width: 2; }
1190
+ .oc-node.idc.id-ring .oc-av { stroke: var(--id); stroke-width: 2.5; }
1191
+ .oc-idbar { display: none; }
1192
+ .oc-node.idc.id-bar .oc-idbar { display: inline; fill: var(--id); }
1193
+ .oc-face.emoji .oc-av { fill: transparent; }
1194
+ .oc-av-t { fill: var(--text-2); font-size: 12px; font-weight: 600; }
1195
+ .oc-face.emoji .oc-av-t { font-size: 18px; font-weight: normal; }
1196
+ .oc-name { fill: var(--text); font-size: 13.5px; font-weight: 600; }
1197
+ .oc-node.lead .oc-name { font-size: 15px; }
1198
+ .oc-node.idc.id-name .oc-name { fill: var(--id-name); }
1199
+ .oc-sub { font-size: 12px; }
1200
+ .oc-st { fill: var(--text-2); }
1201
+ .oc-st.st-working { fill: var(--ok); } .oc-st.st-needs, .oc-st.st-stuck { fill: var(--warn); } .oc-st.st-crashed { fill: var(--danger); }
1202
+ .oc-dot { fill: var(--text-3); stroke: var(--bg); stroke-width: 2; }
1203
+ .oc-dot.st-working, .oc-dot.st-idle { fill: var(--ok); } .oc-dot.st-needs, .oc-dot.st-stuck { fill: var(--warn); } .oc-dot.st-crashed { fill: var(--danger); }
1204
+ .oc-dot.st-paused { fill: var(--bg); stroke: var(--text-3); stroke-width: 1.5; }
1205
+ .oc-zoom { position: absolute; right: var(--s-3); top: var(--s-3); display: flex; flex-direction: column; border: 1px solid var(--border); border-radius: var(--r-md); overflow: hidden; background: var(--surface); }
1206
+ .oc-zoom .icon-btn { width: 36px; height: 34px; border-radius: 0; font-size: 18px; }
1207
+ .oc-zoom .icon-btn + .icon-btn { border-top: 1px solid var(--border); }
1208
+ .oc-mini { position: absolute; right: var(--s-3); bottom: var(--s-3); width: 180px; height: 120px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md); cursor: pointer; }
1209
+ .oc-mini-team { fill: var(--surface-2); stroke: var(--border-strong); stroke-width: 6; }
1210
+ .oc-mini-team.needs { fill: var(--warn-bg); stroke: var(--warn); }
1211
+ .oc-mini-top { fill: var(--accent-dim); stroke: var(--accent); stroke-width: 6; }
1212
+ .oc-mini-team.match { stroke: var(--focus); stroke-width: 14; }
1213
+ .oc-mini-hit { fill: var(--focus); }
1214
+ .oc-mini-view { fill: none; stroke: var(--focus); stroke-width: 10; }
1215
+ /* #1918 phase 3: live traffic. Kind hues kept apart from the state colours (ok = working/idle, warn = needs you, danger). */
1216
+ .oc { --fx-task: #b794f6; --fx-send: #2dd4bf; --fx-report: #f472b6; --fx-broadcast: #fb923c; --fx-team: #cbd5e1; }
1217
+ @media (prefers-color-scheme: light) { :root:not([data-theme="dark"]) .oc { --fx-task: #7c3aed; --fx-send: #0f766e; --fx-report: #be185d; --fx-broadcast: #c2410c; --fx-team: #475569; } }
1218
+ :root[data-theme="light"] .oc { --fx-task: #7c3aed; --fx-send: #0f766e; --fx-report: #be185d; --fx-broadcast: #c2410c; --fx-team: #475569; }
1219
+ .oc-flow { fill: none; stroke-width: 2.5; stroke-linecap: round; }
1220
+ .oc-flow-halo { fill: none; stroke: var(--bg); stroke-width: 6; stroke-linecap: round; opacity: 0.85; }
1221
+ .oc-flow.h-task { stroke: var(--fx-task); } .oc-flow.h-send { stroke: var(--fx-send); } .oc-flow.h-report { stroke: var(--fx-report); }
1222
+ .oc-flow.h-broadcast { stroke: var(--fx-broadcast); } .oc-flow.h-team { stroke: var(--fx-team); stroke-width: 3.5; }
1223
+ .oc-flow.ph-live { stroke-dasharray: 7 6; animation: oc-dash 0.9s linear infinite; }
1224
+ .oc-flow.ph-queued { stroke-dasharray: 2 5; opacity: 0.8; }
1225
+ .oc-flow.ph-fading { stroke: var(--text-3); animation: oc-fade 3s ease-out forwards; }
1226
+ .oc-ah.h-task { fill: var(--fx-task); } .oc-ah.h-send { fill: var(--fx-send); } .oc-ah.h-report { fill: var(--fx-report); }
1227
+ .oc-ah.h-broadcast { fill: var(--fx-broadcast); } .oc-ah.h-team { fill: var(--fx-team); } .oc-ah.h-fading { fill: var(--text-3); }
1228
+ .oc-pulse.h-task { fill: var(--fx-task); } .oc-pulse.h-send { fill: var(--fx-send); } .oc-pulse.h-report { fill: var(--fx-report); }
1229
+ .oc-pulse.h-broadcast { fill: var(--fx-broadcast); } .oc-pulse.h-team { fill: var(--fx-team); }
1230
+ .oc-fl.dim { opacity: 0.18; }
1231
+ .oc-badge rect { fill: var(--surface-2); stroke: var(--border); } .oc-badge text { fill: var(--text-2); font-size: 10.5px; }
1232
+ @keyframes oc-dash { to { stroke-dashoffset: -26; } }
1233
+ @keyframes oc-fade { from { opacity: 0.8; } to { opacity: 0.15; } }
1234
+ .oc-legend { position: absolute; left: var(--s-3); bottom: var(--s-3); max-width: calc(100% - 220px);
1235
+ font-size: var(--fs-xs); color: var(--text-2); background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md); padding: 6px 10px; }
1236
+ .oc-legend summary { cursor: pointer; font-weight: var(--fw-semibold); color: var(--text); }
1237
+ .oc-legend[open] summary { margin-bottom: 4px; }
1238
+ .oc-lg-rows { display: flex; flex-wrap: wrap; gap: var(--s-1) var(--s-3); }
1239
+ .oc-lg { display: inline-flex; align-items: center; gap: 6px; }
1240
+ .oc-sw { width: 18px; height: 3px; border-radius: 2px; display: inline-block; background: var(--text-3); }
1241
+ .oc-sw.h-task { background: var(--fx-task); } .oc-sw.h-send { background: var(--fx-send); } .oc-sw.h-report { background: var(--fx-report); } .oc-sw.h-broadcast { background: var(--fx-broadcast); }
1242
+ .oc-sw.ph-queued { background: repeating-linear-gradient(90deg, var(--text-2) 0 3px, transparent 3px 7px); }
1243
+ .oc-sw.ph-fading { opacity: 0.5; }
1244
+ .oc-live { gap: 6px; }
1245
+ .oc-live-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text-3); }
1246
+ .oc-live.on .oc-live-dot { background: var(--ok); }
1247
+ .org-sending { font-size: var(--fs-xs); color: var(--text-2); overflow-wrap: anywhere; }
1248
+ .org-send.k-task { color: var(--fx-task); } .org-send.k-report { color: var(--fx-report); } .org-send.k-broadcast { color: var(--fx-broadcast); }
1249
+ .org-send.k-send, .org-send.k-query, .org-send.k-update { color: var(--fx-send); }
1250
+ @media (prefers-reduced-motion: reduce) {
1251
+ .oc-flow.ph-live { animation: none; stroke-dasharray: none; }
1252
+ .oc-flow.ph-fading { animation: none; opacity: 0.4; }
1253
+ .oc-pulse { display: none; }
1254
+ }
1255
+ @media (max-width: 640px) {
1256
+ .oc-stage { height: calc(100dvh - 248px); min-height: 340px; }
1257
+ .oc-find { flex: 1 1 150px; }
1258
+ .oc-tools { margin-left: 0; width: 100%; flex-wrap: nowrap; }
1259
+ .oc-mini { display: none; }
1260
+ .oc-legend { max-width: calc(100% - 24px); gap: 2px var(--s-2); }
1261
+ }
1125
1262
  /* A phone: Fleet's five tabs fit when only the open one shows its label (the others keep theirs for screen readers). */
1126
1263
  @media (max-width: 479px) {
1127
1264
  .p-fleet .seg-item:not(.active) > span, .p-settings .seg-item:not(.active) > span { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
@@ -1200,12 +1337,43 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
1200
1337
  font-weight: var(--fw-medium); white-space: nowrap; }
1201
1338
 
1202
1339
  /* #1629 r4 §1 / r4.1 Q3 (a): the identity card (identity-card.js). The size is a class token; its heights are here
1203
- (S 160 / M 240 / L 360 px, on a phone 120 / 180 / 240); the banner scales with the portrait. Never a style attribute. */
1204
- .id-card { --id-h: 240px; margin: 0 0 var(--s-4); border: 1px solid var(--border); border-radius: var(--r-md); background: var(--surface); overflow: hidden; }
1205
- .id-card.card-s { --id-h: 160px; }
1206
- .id-card.card-l { --id-h: 360px; }
1207
- .id-card.card-c { --id-h: 96px; margin: 0; border: 0; border-radius: 0; background: none; }
1208
- .id-banner { display: block; width: 100%; height: calc(var(--id-h) / 2); object-fit: cover; }
1340
+ (S 160 / M 240 / L 360 px, on a phone 120 / 180 / 240). Never a style attribute.
1341
+ #2038: the banner's height follows the card's width — S 4:1, M 3:1, L 2.25:1 — between a floor and a ceiling per size,
1342
+ so it is never a strip across a wide chat and never a wall on a phone; the picture fills it, cropped about its middle.
1343
+ The chat header's card (card-c) follows the same size choice (card-c bs-<size>). */
1344
+ .id-card { --id-feather: linear-gradient(to right, transparent 0, #000 7%, #000 93%, transparent 100%); --id-h: 240px; --id-ar: 3 / 1; --id-bmin: 140px; --id-bmax: 240px; margin: 0 0 var(--s-4); border: 1px solid var(--border); border-radius: var(--r-md); background: var(--surface); overflow: hidden; }
1345
+ .id-card.card-s { --id-h: 160px; --id-ar: 4 / 1; --id-bmin: 100px; --id-bmax: 160px; }
1346
+ .id-card.card-l { --id-h: 360px; --id-ar: 9 / 4; --id-bmin: 200px; --id-bmax: 360px; }
1347
+ .id-card.card-c { --id-h: 96px; --id-bmin: 120px; --id-bmax: 200px; margin: 0; border: 0; border-radius: 0; background: none; }
1348
+ .id-card.card-c.bs-s { --id-ar: 4 / 1; --id-bmin: 96px; --id-bmax: 150px; }
1349
+ .id-card.card-c.bs-l { --id-ar: 9 / 4; --id-bmin: 150px; --id-bmax: 220px; }
1350
+ /* #2086: the banner at its focus (--id-focus, set per card), never wider than 5 : 1 — its width is capped at five times its
1351
+ size's tallest height, so the band shown everywhere is (picture aspect ÷ 5) of its height. Beyond that it is centred
1352
+ over a blurred copy of itself. */
1353
+ .id-banner-box { position: relative; overflow: hidden; container: idbanner / inline-size; }
1354
+ /* #2089 review: and never narrower than 9 : 4 — its height never more than its box's width ÷ 2.25 (a size's floor yields
1355
+ to it on a narrow column: a wide sidebar, the usage rail), so the promised safe area holds on every layout. */
1356
+ .id-banner { position: relative; display: block; width: 100%; max-width: calc(var(--id-bmax) * 5); margin-inline: auto; height: auto; aspect-ratio: var(--id-ar);
1357
+ min-height: min(var(--id-bmin), calc(100cqw / 2.25)); max-height: min(var(--id-bmax), calc(100cqw / 2.25)); object-fit: cover; object-position: 50% var(--id-focus, 50%); }
1358
+ /* #2086: Edit profile's banner focus — the picture whole, the band every screen shows (dashed, outside it shaded) and a phone's. */
1359
+ .bf-wrap { position: relative; max-width: 440px; overflow: hidden; border-radius: var(--r-sm); }
1360
+ .bf-pic { display: block; width: 100%; height: auto; }
1361
+ .bf-band { position: absolute; pointer-events: none; box-sizing: border-box; }
1362
+ .bf-band span { position: absolute; right: 4px; top: 2px; font-size: var(--fs-xs); font-weight: var(--fw-semibold); color: #fff; text-shadow: 0 1px 2px #000, 0 0 4px #000; }
1363
+ .bf-safe { border: 2px dashed #fff; box-shadow: 0 0 0 999px rgba(0, 0, 0, .45); }
1364
+ /* The phone's band over the shade, in its own colour (its label at its own top-left, never in the shaded part). */
1365
+ .bf-phone { z-index: 1; border: 2px solid #ffd54a; }
1366
+ .bf-phone span { right: auto; left: 8px; top: 5px; color: #ffd54a; }
1367
+ .bf-field input[type=range] { width: 100%; max-width: 440px; }
1368
+ .id-banner-fill { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: 50% var(--id-focus, 50%); filter: blur(24px) saturate(0.75) brightness(0.7); transform: scale(1.4); opacity: .9; pointer-events: none; }
1369
+ /* Wider than the banner's cap (5 × the size's tallest height): its sides feather into the fill, so the sharp centre
1370
+ leads without a seam. One threshold per size (a container query cannot read a custom property). */
1371
+ @container idbanner (min-width: 750px) { .id-card.card-c.bs-s .id-banner { -webkit-mask-image: var(--id-feather); mask-image: var(--id-feather); } }
1372
+ @container idbanner (min-width: 800px) { .id-card.card-s .id-banner { -webkit-mask-image: var(--id-feather); mask-image: var(--id-feather); } }
1373
+ @container idbanner (min-width: 1000px) { .id-card.card-c:not(.bs-s):not(.bs-l) .id-banner { -webkit-mask-image: var(--id-feather); mask-image: var(--id-feather); } }
1374
+ @container idbanner (min-width: 1100px) { .id-card.card-c.bs-l .id-banner { -webkit-mask-image: var(--id-feather); mask-image: var(--id-feather); } }
1375
+ @container idbanner (min-width: 1200px) { .id-card:not(.card-s):not(.card-l):not(.card-c) .id-banner { -webkit-mask-image: var(--id-feather); mask-image: var(--id-feather); } }
1376
+ @container idbanner (min-width: 1800px) { .id-card.card-l .id-banner { -webkit-mask-image: var(--id-feather); mask-image: var(--id-feather); } }
1209
1377
  .id-row { display: flex; align-items: flex-start; gap: var(--s-3); padding: var(--s-3) var(--s-4); }
1210
1378
  .id-portrait { display: block; flex-shrink: 0; height: var(--id-h); width: auto; max-width: min(calc(var(--id-h) * 0.75), 100%); object-fit: cover; border-radius: var(--r-sm); }
1211
1379
  .id-text { flex: 1; min-width: 0; }
@@ -1263,9 +1431,12 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
1263
1431
  .chat-card-toggle { display: flex; align-items: center; gap: var(--s-2); width: 100%; min-height: 32px; padding: 0 var(--s-4); border: 0; background: none; color: var(--text-2); font: inherit; font-size: var(--fs-xs); text-align: left; cursor: pointer; }
1264
1432
  .chat-card-peek { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-3); }
1265
1433
  @media (max-width: 479px) {
1266
- .id-card.card-s { --id-h: 120px; }
1267
- .id-card.card-m { --id-h: 180px; }
1268
- .id-card.card-l { --id-h: 240px; }
1434
+ .id-card.card-s { --id-h: 120px; --id-bmin: 80px; --id-bmax: 120px; }
1435
+ .id-card.card-m { --id-h: 180px; --id-bmin: 100px; --id-bmax: 150px; }
1436
+ .id-card.card-l { --id-h: 240px; --id-bmin: 120px; --id-bmax: 200px; }
1437
+ .id-card.card-c { --id-bmin: 100px; --id-bmax: 140px; }
1438
+ .id-card.card-c.bs-s { --id-bmin: 80px; --id-bmax: 110px; }
1439
+ .id-card.card-c.bs-l { --id-bmin: 110px; --id-bmax: 150px; }
1269
1440
  .id-row { flex-wrap: wrap; }
1270
1441
  .id-text { flex-basis: 100%; }
1271
1442
  }
@@ -1304,6 +1475,10 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
1304
1475
  .s-fx-owner-row{display:flex;align-items:end;flex-wrap:wrap;gap:var(--s-2);margin-top:var(--s-2)}
1305
1476
  .s-fx-owner-row>label{display:flex;flex-direction:column;gap:var(--s-1);min-width:0;flex:1}
1306
1477
 
1478
+ /* #2044: the effect frame's document declares no colour scheme (light). An iframe whose own colour scheme differs
1479
+ from its document's gets an opaque backdrop, so under the dark theme the frame hid the whole chat: keep them equal. */
1480
+ .agend-effect-frame { color-scheme: normal !important; }
1481
+
1307
1482
  /* #1629: host-created static poster IMG; geometry and visibility belong to the trusted CSSOM binding. */
1308
1483
  .agend-effect-poster { object-fit: cover !important; object-position: center !important; user-select: none; }
1309
1484
 
@@ -13,6 +13,8 @@ register("card", {
13
13
  byAgent: "set by the agent",
14
14
  show: "Show card",
15
15
  hide: "Hide card",
16
+ bannerSafe: "Every screen",
17
+ bannerPhone: "Phone · M card",
16
18
  },
17
19
  "zh-TW": {
18
20
  label: "{0} 的身分卡",
@@ -24,5 +26,7 @@ register("card", {
24
26
  byAgent: "由 agent 設定",
25
27
  show: "顯示卡片",
26
28
  hide: "隱藏卡片",
29
+ bannerSafe: "所有螢幕",
30
+ bannerPhone: "手機 · 中卡片",
27
31
  },
28
32
  });
@@ -98,7 +98,7 @@ export function createAppEffectRuntime({ window: host, current, observe, onChang
98
98
  if (!registry.ok) throw Error("runtime_unavailable");
99
99
  trial = make("trialSource", createEffectTrialSource)({ window: host, current: () => {
100
100
  if (!alive()) return false; const v = observe(), c = v.context;
101
- return alive() && v.ownerCss === "off" && c.enabled === true && c.show === true && c.previewReady === true && c.optIn === true
101
+ return alive() && c.enabled === true && c.show === true && c.trialReady === true
102
102
  && c.safe === false && c.publicLink === false && c.reducedMotion === false && c.saveData === false && c.hidden === false;
103
103
  }, ownerCurrent: (s, name) => sourceAlive(s) && s.name === name,
104
104
  stock: () => registry.frameRoot ? make("stock", stockEffectTrust)(host, { frameRoot: registry.frameRoot, ownerCss: observe().ownerCss }) : null,
@@ -30,16 +30,21 @@ function snapshotRender(raw) {
30
30
  return { code: raw.code, params: { ...params }, instance: raw.instance, width: raw.width, height: raw.height, dpr: raw.dpr, assets, wants: [...raw.wants] };
31
31
  }
32
32
 
33
+ /** Listener transport only; this does not grant HTML-preview or ordinary-effect consent. */
34
+ export function effectTransportReady(host, origin, boot) {
35
+ try {
36
+ const target = new URL(origin), page = new URL(host.location.origin);
37
+ return origin === target.origin && ["http:", "https:"].includes(target.protocol) && ["http:", "https:"].includes(page.protocol)
38
+ && target.origin !== page.origin && !(page.protocol === "https:" && target.protocol !== "https:") && typeof boot === "string" && /^[0-9a-f]{32}$/.test(boot);
39
+ } catch { return false; }
40
+ }
41
+
33
42
  /**
34
43
  * All options come from the host, never from an agent event or manifest origin. The registry supplies an admitted
35
44
  * root and geometry in a later integration. This primitive alone does not implement caps, clipping or profiling.
36
45
  */
37
46
  export function createEffectFrame({ window: host, root, origin, boot, render, clock = () => performance.now(), onReady = () => {}, onRemove = () => {} } = {}) {
38
- let url;
39
- try { url = new URL(origin); } catch { return { ok: false, reason: "preview_unknown" }; }
40
- if (origin !== url.origin || !["http:", "https:"].includes(url.protocol) || typeof boot !== "string" || !/^[0-9a-f]{32}$/.test(boot)) return { ok: false, reason: "preview_unknown" };
41
- let hostOrigin; try { hostOrigin = new URL(host.location.origin); } catch { return { ok: false, reason: "preview_unknown" }; }
42
- if (!["http:", "https:"].includes(hostOrigin.protocol) || origin === hostOrigin.origin || hostOrigin.protocol === "https:" && url.protocol !== "https:") return { ok: false, reason: "preview_unknown" };
47
+ if (!effectTransportReady(host, origin, boot)) return { ok: false, reason: "preview_unknown" };
43
48
  const payload = snapshotRender(render);
44
49
  if (!payload || !host || !root || root.ownerDocument !== host.document || typeof root.append !== "function" || typeof host.MessageChannel !== "function" || typeof clock !== "function" || typeof onReady !== "function" || typeof onRemove !== "function") return { ok: false, reason: "unsupported" };
45
50
  let mounted;
@@ -70,36 +70,51 @@ export function createEffectGeometry({ window: host } = {}) {
70
70
  frame.remove();
71
71
  return { ok: false, reason: "geometry_unknown" };
72
72
  }
73
- let dead = false;
73
+ let dead = false, shown = false;
74
74
  const hide = () => {
75
+ shown = false;
75
76
  try {
76
77
  rule.style.setProperty("visibility", "hidden", "important");
77
- if (frame.isConnected === true && host.getComputedStyle(frame).visibility !== "hidden") frame.remove();
78
- } catch { frame.remove(); }
78
+ if (frame.isConnected === true && host.getComputedStyle(frame).visibility !== "hidden") { frame.remove(); return false; }
79
+ return true;
80
+ } catch { frame.remove(); return false; }
79
81
  };
80
82
  const handle = { ok: true, hide,
81
83
  /** Same-turn host paint snapshot is mandatory, including behind/human placements. No DOM fallback. */
82
84
  sync(rect, stock) {
83
- hide();
85
+ const refuse = () => { hide(); return false; };
84
86
  const proof = effectStockSnapshot(stock);
85
- if (!effectStockAllows(proof, { layer, by })) return false;
86
- if (dead || disposed || !hostEffectSpace(host, frame) || !rect || ![rect.x, rect.y, rect.width, rect.height].every(number) || ![rect.x, rect.y, rect.width, rect.height].every(Number.isInteger) || rect.width < 1 || rect.height < 1 || rect.width > 4096 || rect.height > 4096 || rect.width * rect.height > 8388608) { hide(); return false; }
87
+ if (!effectStockAllows(proof, { layer, by })) return refuse();
88
+ if (dead || disposed || !hostEffectSpace(host, frame) || !rect || ![rect.x, rect.y, rect.width, rect.height].every(number) || ![rect.x, rect.y, rect.width, rect.height].every(Number.isInteger) || rect.width < 1 || rect.height < 1 || rect.width > 4096 || rect.height > 4096 || rect.width * rect.height > 8388608) return refuse();
87
89
  let path = "none";
88
90
  if (layer === "front" && by !== "human") {
89
91
  const clipped = effectTrustClip(rect, proof.rects);
90
- if (!clipped.ok) return false;
92
+ if (!clipped.ok) return refuse();
91
93
  path = clipped.path;
92
94
  }
93
95
 
94
96
  try {
97
+ const matches = visibility => {
98
+ const style = host.getComputedStyle(frame), actual = frame.getBoundingClientRect();
99
+ return style.position === "fixed" && style.pointerEvents === "none" && style.visibility === visibility
100
+ && style.opacity === (layer === "front" && by !== "human" ? "0.85" : "1") && style.zIndex === (layer === "front" ? "20" : "0")
101
+ && sameClip(path, rule.style.getPropertyValue("clip-path")) && sameClip(path, style.clipPath)
102
+ && ["x", "y", "width", "height"].every(key => number(actual[key]) && Math.abs(actual[key] - rect[key]) < 0.02)
103
+ && frame.isConnected === true && frame.ownerDocument === host.document;
104
+ };
105
+ // A fresh proof and actual viewport/clip are still checked every frame. Rewriting an
106
+ // already verified presentation makes WebKit repeatedly invalidate the iframe's paint.
107
+ if (shown && matches("visible")) return true;
108
+ // Changed or unproven geometry is hidden before writes and verified before reveal.
109
+ if (!hide()) return false;
95
110
  for (const key of ["x", "y", "width", "height"]) rule.style.setProperty(key === "x" ? "left" : key === "y" ? "top" : key, `${rect[key]}px`, "important");
96
111
  rule.style.setProperty("clip-path", path, "important");
97
112
  if (!sameClip(path, rule.style.getPropertyValue("clip-path"))) return false;
98
113
  // The stylesheet can lose the cascade to another host rule. Verify the actual viewport and clip
99
114
  // before revealing: holes expressed in planned coordinates cannot protect a transformed frame.
100
- const style = host.getComputedStyle(frame), actual = frame.getBoundingClientRect();
101
- if (style.position !== "fixed" || style.pointerEvents !== "none" || style.visibility !== "hidden" || style.opacity !== (layer === "front" && by !== "human" ? "0.85" : "1") || style.zIndex !== (layer === "front" ? "20" : "0") || !sameClip(path, style.clipPath) || !["x", "y", "width", "height"].every(key => number(actual[key]) && Math.abs(actual[key] - rect[key]) < 0.02)) return false;
115
+ if (!matches("hidden")) return false;
102
116
  rule.style.setProperty("visibility", "visible", "important");
117
+ shown = true;
103
118
  return true;
104
119
  } catch { hide(); return false; }
105
120
  },
@@ -74,7 +74,7 @@ export function allocateEffectFrames(descriptors, context) {
74
74
  else if (context.saveData === true) global = "save_data";
75
75
  else if (context.hidden === true) global = "hidden";
76
76
  else if (context.enabled !== true || context.show !== true) global = "disabled";
77
- else if (context.previewReady !== true || context.optIn !== true) global = "preview_unavailable";
77
+
78
78
  const candidates = [];
79
79
  for (let i = 0; i < descriptors.length; i++) {
80
80
  const d = descriptors[i], r = result[i];
@@ -87,6 +87,7 @@ export function allocateEffectFrames(descriptors, context) {
87
87
  if (!owner && d.region !== "chat") { r.reason = "scope"; continue; }
88
88
  if (d.region === "chat" && (!instanceName(d.instance) || d.instance !== context.openInstance)) { r.reason = "closed_chat"; continue; }
89
89
  if (global) { r.reason = global; continue; }
90
+ if (trial ? context.trialReady !== true : context.previewReady !== true || context.optIn !== true) { r.reason = "preview_unavailable"; continue; }
90
91
  if (d.paused === true) { r.reason = "paused"; continue; }
91
92
  if (d.visible !== true) { r.reason = "offscreen"; continue; }
92
93
  if (d.layer === "front" && context.modal === true && !trial) { r.reason = "modal"; continue; }
@@ -65,7 +65,7 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
65
65
  finally { readingStages.delete(d.trialStage); }
66
66
  return !dead && !blocked && revision === before && stage?.root === d.element ? stage : null;
67
67
  };
68
- const permits = (d, proof) => d.trialStage ? !!localStage(d, proof) : effectStockAllows(proof, d);
68
+ const permits = (d, proof) => eligible(d) && (d.trialStage ? !!localStage(d, proof) : effectStockAllows(proof, d));
69
69
  const coverage = (d = null) => {
70
70
  if (dead || blocked || readingStock) return null;
71
71
  const before = revision; let proof = null; readingStock = true;
@@ -91,6 +91,26 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
91
91
  const current = s => !dead && !blocked && slots.get(s.d.key) === s && s.live;
92
92
  const tell = () => { if (dead) return; const state = view(), stamp = JSON.stringify(state); if (stamp === lastView) return; lastView = stamp; try { onChange(state); } catch { /* a display callback cannot admit code */ } };
93
93
  const receipt = (fn, ...args) => { try { return Reflect.apply(fn, safety, args); } catch { return null; } };
94
+ // #2055: the history's own read names a denial; any other answer stays unknown (and denied).
95
+ const SAFETY_REASON = Object.freeze({ __proto__: null, off: "safety_off", paused: "paused", interrupted: "interrupted" });
96
+ // Display only: the sticky history state per descriptor, which outranks a waiting reason (a queue, a dialog, typing)
97
+ // because it is what Resume must answer. Admission, caps, latches and safety policy never read it.
98
+ const named = new Map();
99
+ // The read can re-enter (an update from a callback): only a pass still current, for the same device, may write.
100
+ const nameSafety = (d, dev = device()) => {
101
+ const proposed = revision;
102
+ const reason = d.trialStage ? null : SAFETY_REASON[receipt(safety.status, d.id, dev)?.state];
103
+ if (dead || proposed !== revision || dev !== device()) return false;
104
+ if (reason) named.set(d.key, { identity: signature(d), reason }); else named.delete(d.key);
105
+ return true;
106
+ };
107
+ // Display only: the closed global gate a row is waiting on, when the host knows it (else the caller's unknown).
108
+ const gateReason = () => {
109
+ if (!gatesKnown()) return null;
110
+ const c = context;
111
+ return c.safe ? "safe_mode" : c.publicLink ? "public_link" : c.reducedMotion ? "reduced_motion" : c.saveData ? "save_data"
112
+ : !(c.enabled && c.show) ? "disabled" : !(c.previewReady && c.optIn) ? "preview_unavailable" : null;
113
+ };
94
114
  const clearMarker = (s, why) => {
95
115
  if (!s.token) return true;
96
116
  const token = s.token; s.token = null;
@@ -110,6 +130,7 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
110
130
  paused.set(s.d.key, signature(s.d));
111
131
  if (receipt(safety.pause, { id: s.d.id, device: s.device, reason: why, metrics: s.budget?.snapshot().metrics ?? null }) !== true) blocked = "safety_unknown";
112
132
  }
133
+ if (count || why === "safety_revoked") nameSafety(s.d, s.device); // #2055: the third pause, or another tab's, says off now
113
134
  clearMarker(s, why);
114
135
  if (s.owner) { try { onRetire(s.owner, why); } catch { /* an observation cannot prevent physical cleanup */ } }
115
136
  if (blocked) for (const other of [...slots.values()]) retire(other, blocked);
@@ -124,14 +145,25 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
124
145
  return true;
125
146
  }
126
147
  function gatesKnown() { return context && FLAGS.every(k => typeof context[k] === "boolean") && (context.openInstance === null || typeof context.openInstance === "string" && context.openInstance.length >= 1 && context.openInstance.length <= 128); }
127
- function eligible() { return gatesKnown() && !context.safe && !context.publicLink && !context.reducedMotion && !context.saveData && context.enabled && context.show && context.previewReady && context.optIn; }
128
- const globalAllowed = () => eligible() && host.document.visibilityState === "visible";
148
+ function consent(d) {
149
+ const trial = d?.trialStage && effectTrialStageIdentity(d.trialStage);
150
+ return trial ? context.trialReady === true : context.previewReady === true && context.optIn === true;
151
+ }
152
+ function eligible(d = null) {
153
+ return gatesKnown() && !context.safe && !context.publicLink && !context.reducedMotion && !context.saveData && context.enabled && context.show
154
+ && (d ? consent(d) : context.previewReady && context.optIn || descriptors.some(x => x.trialStage && consent(x)));
155
+ }
156
+ const globalAllowed = (d = null) => eligible(d) && host.document.visibilityState === "visible";
129
157
  const signature = d => d.identity;
158
+ // A frame stopped because its gates closed is recorded as gates_unknown; when the host knows which gate, say it.
159
+ const shown = why => (why === "gates_unknown" && !eligible() && gateReason()) || why;
130
160
  function view() {
131
161
  return descriptors.map(d => {
132
- const r = results.get(d.key) ?? { verdict: "off", reason: blocked ?? "unknown", queuePos: null }, s = slots.get(d.key);
133
- return { key: d.key, id: d.id, ...r, phase: s?.frame ? s.ready ? "running" : "starting" : s ? "loading" : null,
134
- quality: s?.budget?.snapshot().quality ?? null, reason: blocked ?? ((paused.get(d.key) === signature(d)) ? "paused" : failures.get(d.key)?.identity === signature(d) ? failures.get(d.key).reason : s && context?.hidden ? "hidden" : r.reason) };
162
+ const r = results.get(d.key) ?? { verdict: "off", reason: blocked ?? (!eligible() && gateReason()) ?? "unknown", queuePos: null }, s = slots.get(d.key);
163
+ // #2055 one display precedence: a global latch, then the sticky safety history, then this page's own pause/failure.
164
+ const n = named.get(d.key), sticky = n && n.identity === signature(d) ? n.reason : null;
165
+ return { key: d.key, id: d.id, ...r, ...(sticky && r.verdict === "queued" ? { verdict: "off", queuePos: null } : {}), phase: s?.frame ? s.ready ? "running" : "starting" : s ? "loading" : null,
166
+ quality: s?.budget?.snapshot().quality ?? null, reason: blocked ?? sticky ?? ((paused.get(d.key) === signature(d)) ? "paused" : failures.get(d.key)?.identity === signature(d) ? shown(failures.get(d.key).reason) : s && context?.hidden ? "hidden" : r.reason) };
135
167
  });
136
168
  }
137
169
  function schedule() {
@@ -165,7 +197,9 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
165
197
  const stock = eligible() ? coverage() : null;
166
198
  if (blocked || !eligible() || !stock) {
167
199
  if (!stopAll(blocked ?? "gates_unknown")) return;
168
- results.clear(); tell(); return;
200
+ results.clear();
201
+ if (!blocked) for (const d of descriptors) if (!nameSafety(d) || dead || proposal !== revision) return;
202
+ tell(); return;
169
203
  }
170
204
  // Hidden frames already owned may survive for <=10 s, but no new frame or load is admitted while hidden.
171
205
  const ctx = { ...context, hidden: false };
@@ -176,6 +210,7 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
176
210
  if (dead || proposal !== revision) return;
177
211
  ds.push({ ...d, paused: !allowed || d.paused === true || paused.get(d.key) === signature(d) || failures.get(d.key)?.identity === signature(d) });
178
212
  }
213
+ for (const d of descriptors) if (!nameSafety(d) || dead || proposal !== revision) return;
179
214
  const allocation = allocateEffectFrames(ds, ctx); results.clear();
180
215
  for (const r of allocation) {
181
216
  const d = descriptors.find(d => d.key === r.key);
@@ -230,8 +265,14 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
230
265
  if (slots.size >= max || loads.size >= max) break;
231
266
  const permitted = receipt(safety.allowed, d.id, device());
232
267
  if (proposal !== revision) return;
233
- if (permitted !== true) { results.set(d.key, { verdict: "off", reason: "safety_unknown", queuePos: null }); continue; }
234
- if (slots.has(d.key) || slots.size >= max || loads.size >= max || !globalAllowed() || context.hidden || !coverage(d) || proposal !== revision || !descriptors.some(x => x.key === d.key && signature(x) === signature(d) && x.element === d.element)) break;
268
+ if (permitted !== true) {
269
+ // #2055: say which denial it is. Only the safety history's own read names it; anything else stays unknown (still denied).
270
+ const st = receipt(safety.status, d.id, device());
271
+ if (proposal !== revision) return;
272
+ const why = SAFETY_REASON[st?.state] ?? "safety_unknown";
273
+ results.set(d.key, { verdict: "off", reason: why, queuePos: null }); continue;
274
+ }
275
+ if (slots.has(d.key) || slots.size >= max || loads.size >= max || !globalAllowed(d) || context.hidden || !coverage(d) || proposal !== revision || !descriptors.some(x => x.key === d.key && signature(x) === signature(d) && x.element === d.element)) break;
235
276
  const rect = position(d, coverage(d));
236
277
  if (dead || proposal !== revision) return;
237
278
  if (!rect || d.element.ownerDocument !== host.document) { results.set(d.key, { verdict: "off", reason: "geometry_unknown", queuePos: null }); continue; }
@@ -254,7 +295,7 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
254
295
  const acceptCompletion = () => {
255
296
  if (!current(s)) { clearMarker(s, "superseded"); return false; }
256
297
  if (!withinDeadline()) return false;
257
- if (request.controller.signal.aborted || !globalAllowed() || context.hidden || !coverage(d)) {
298
+ if (request.controller.signal.aborted || !globalAllowed(d) || context.hidden || !coverage(d)) {
258
299
  if (current(s)) retire(s, "admission_revoked"); else clearMarker(s, "superseded");
259
300
  return false;
260
301
  }
@@ -290,7 +331,7 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
290
331
  s.budget = new EffectBudget({ device: s.device, baselineFps: baseline, clock: nativeClock, longTasksSupported });
291
332
  const frame = createEffectFrame({ window: host, root: local?.root ?? root, origin, boot, clock: nativeClock,
292
333
  render: { code: resource.code, params: resource.params, assets: resource.assets, wants: resource.wants, instance: d.instance, width: fresh.width, height: fresh.height, dpr: dpr(s.device) },
293
- onReady() { if (!current(s)) return; if (!globalAllowed() || context.hidden || !coverage(d)) { retire(s, "hidden"); tell(); return; } const n = time(); if (n === null) { blocked = "clock_unavailable"; stopAll(blocked); tell(); return; } s.ready = true; s.readyAt = n; s.healthySince = n; publishEnv(s); schedule(); tell(); },
334
+ onReady() { if (!current(s)) return; if (!globalAllowed(d) || context.hidden || !coverage(d)) { retire(s, "hidden"); tell(); return; } const n = time(); if (n === null) { blocked = "clock_unavailable"; stopAll(blocked); tell(); return; } s.ready = true; s.readyAt = n; s.healthySince = n; publishEnv(s); schedule(); tell(); },
294
335
  onRemove(why) { if (current(s)) { retire(s, why); tell(); } },
295
336
  });
296
337
  s.frame = frame;
@@ -316,7 +357,7 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
316
357
  schedule(); tell();
317
358
  }
318
359
  function publishEnv(s, proof = coverage(s.d)) {
319
- if (!s.ready || !current(s) || context.hidden || !globalAllowed() || !permits(s.d, proof)) return;
360
+ if (!s.ready || !current(s) || context.hidden || !globalAllowed(s.d) || !permits(s.d, proof)) return;
320
361
  const e = effectEvent("env", { width: s.rect.width, height: s.rect.height, dpr: dpr(s.device), quality: s.budget.snapshot().quality }, null, time());
321
362
  if (e) s.frame.send(e);
322
363
  }
@@ -339,12 +380,17 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
339
380
  if (changed) publishEnv(s, paint);
340
381
  if (s.ready) s.budget.frame();
341
382
  if (s.pointer) {
342
- const packet = s.pointer; s.pointer = null;
383
+ const packet = s.pointer;
343
384
  const n = time(), b = s.budget.snapshot();
344
- if (n !== null && n - packet.at <= 100 && permits(s.d, paint) && pointerGate(paint) && current(s) && b.forward && (s.lastPointer === null || n - s.lastPointer >= 1000 / b.pointerHz)) {
385
+ if (n === null || n - packet.at > 100 || !permits(s.d, paint) || !pointerGate(paint) || !current(s) || !b.forward) {
386
+ if (s.pointer === packet) s.pointer = null;
387
+ } else if (s.lastPointer === null || n - s.lastPointer >= 1000 / b.pointerHz) {
388
+ if (s.pointer === packet) s.pointer = null;
345
389
  const event = effectEvent("pointer", packet.fields, s.d.instance, packet.at);
346
390
  if (event && s.frame.send(event)) s.lastPointer = n;
347
391
  }
392
+ // Keep the latest fresh sample until the next eligible frame. An early rAF must
393
+ // not consume a click just because the preceding move used this rate interval.
348
394
  }
349
395
  }
350
396
  if (dead || proposal !== revision) return;
@@ -401,7 +447,11 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
401
447
  if (!globalAllowed() || context.hidden || context.modal || !proof || proof.front !== true) return false;
402
448
  try {
403
449
  const focused = host.document.activeElement;
404
- return host.document.designMode !== "on" && !!focused && ["BODY", "BUTTON", "A"].includes(focused.tagName) && focused.isContentEditable === false && typeof focused.matches === "function" && !focused.matches("input,textarea,select,[contenteditable]:not([contenteditable='false'])");
450
+ // Native clicks on ordinary stock content focus its tabindex=-1 main ancestor.
451
+ // Recognize only that unique host root; unknown/editor focus remains a refusal.
452
+ const mains = focused?.tagName === "MAIN" ? host.document.querySelectorAll("#app > .shell > main#main.main[tabindex='-1']") : [];
453
+ const stockMain = focused?.isConnected === true && focused.ownerDocument === host.document && mains.length === 1 && mains[0] === focused;
454
+ return host.document.designMode !== "on" && !!focused && (["BODY", "BUTTON", "A"].includes(focused.tagName) || stockMain) && focused.isContentEditable === false && typeof focused.matches === "function" && !focused.matches("input,textarea,select,[contenteditable]:not([contenteditable='false'])");
405
455
  } catch { return false; }
406
456
  }
407
457
  function pointer(event) {
@@ -416,7 +466,11 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
416
466
  const fields = { kind, x: event.clientX - r.x, y: event.clientY - r.y, buttons: event.buttons };
417
467
  if (fields.x < 0 || fields.y < 0 || fields.x > r.width || fields.y > r.height) continue;
418
468
  if (kind === "wheel") { fields.dx = event.deltaX; fields.dy = event.deltaY; }
419
- if (effectEvent("pointer", fields, s.d.instance, at)) s.pointer = { fields, at };
469
+ if (effectEvent("pointer", fields, s.d.instance, at)) {
470
+ // One slot, with discrete input ahead of motion. A later move must not erase
471
+ // a waiting click; down/up/click may still coalesce to the latest discrete event.
472
+ if (kind !== "move" || !s.pointer || s.pointer.fields.kind === "move" || at - s.pointer.at > 100) s.pointer = { fields, at };
473
+ }
420
474
  }
421
475
  schedule();
422
476
  }
@@ -456,7 +510,7 @@ export function createEffectRegistry({ window: host, origin, boot, load, safety,
456
510
  descriptors = ds.map(snapshotDescriptor).filter(Boolean);
457
511
  for (const [key, identity] of paused) if (!descriptors.some(d => d.key === key && signature(d) === identity)) paused.delete(key);
458
512
  for (const [key, f] of failures) if (!descriptors.some(d => d.key === key && signature(d) === f.identity)) failures.delete(key);
459
- context = Object.fromEntries([...FLAGS, "openInstance"].map(k => [k, ctx[k]]));
513
+ context = Object.fromEntries([...FLAGS, "trialReady", "openInstance"].map(k => [k, ctx[k]]));
460
514
  reconcile(); return view();
461
515
  },
462
516
  snapshot: view,