@songsid/agend 2.3.0-beta.1 → 2.3.0-beta.3

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 (174) hide show
  1. package/dist/agent-cli.js +5 -5
  2. package/dist/agent-cli.js.map +1 -1
  3. package/dist/agent-file-read.d.ts +11 -6
  4. package/dist/agent-file-read.js +32 -17
  5. package/dist/agent-file-read.js.map +1 -1
  6. package/dist/agent-whats-new.js +10 -13
  7. package/dist/agent-whats-new.js.map +1 -1
  8. package/dist/auth-api.js +10 -5
  9. package/dist/auth-api.js.map +1 -1
  10. package/dist/backend/claude-code.d.ts +2 -2
  11. package/dist/backend/claude-code.js +10 -5
  12. package/dist/backend/claude-code.js.map +1 -1
  13. package/dist/backend/types.d.ts +13 -0
  14. package/dist/backend/types.js +10 -0
  15. package/dist/backend/types.js.map +1 -1
  16. package/dist/cache-analysis.d.ts +47 -1
  17. package/dist/cache-analysis.js +68 -8
  18. package/dist/cache-analysis.js.map +1 -1
  19. package/dist/cache-report-tool.d.ts +17 -0
  20. package/dist/cache-report-tool.js +40 -0
  21. package/dist/cache-report-tool.js.map +1 -0
  22. package/dist/cache-service.d.ts +15 -0
  23. package/dist/cache-service.js +26 -1
  24. package/dist/cache-service.js.map +1 -1
  25. package/dist/channel/mcp-tools.js +9 -3
  26. package/dist/channel/mcp-tools.js.map +1 -1
  27. package/dist/classic-channel-manager.d.ts +8 -3
  28. package/dist/classic-channel-manager.js +10 -3
  29. package/dist/classic-channel-manager.js.map +1 -1
  30. package/dist/cli.js +7 -3
  31. package/dist/cli.js.map +1 -1
  32. package/dist/config-validator.js +5 -1
  33. package/dist/config-validator.js.map +1 -1
  34. package/dist/daemon.js +5 -5
  35. package/dist/daemon.js.map +1 -1
  36. package/dist/delivery-outbox.js +5 -0
  37. package/dist/delivery-outbox.js.map +1 -1
  38. package/dist/fleet-manager.d.ts +71 -3
  39. package/dist/fleet-manager.js +256 -45
  40. package/dist/fleet-manager.js.map +1 -1
  41. package/dist/fleet-traffic.d.ts +38 -0
  42. package/dist/fleet-traffic.js +96 -0
  43. package/dist/fleet-traffic.js.map +1 -0
  44. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +20 -5
  45. package/dist/general-knowledge/skills/multi-channel/SKILL.md +1 -1
  46. package/dist/general-knowledge/skills/web-appearance/SKILL.md +149 -5
  47. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +398 -0
  48. package/dist/locale.js +6 -4
  49. package/dist/locale.js.map +1 -1
  50. package/dist/memory-hogs.d.ts +27 -0
  51. package/dist/memory-hogs.js +124 -0
  52. package/dist/memory-hogs.js.map +1 -0
  53. package/dist/outbound-handlers.d.ts +4 -0
  54. package/dist/outbound-handlers.js +74 -1
  55. package/dist/outbound-handlers.js.map +1 -1
  56. package/dist/outbound-schemas.d.ts +98 -0
  57. package/dist/outbound-schemas.js +56 -0
  58. package/dist/outbound-schemas.js.map +1 -1
  59. package/dist/public-web-gateway.js +1 -1
  60. package/dist/public-web-gateway.js.map +1 -1
  61. package/dist/scheduler/scheduler.d.ts +12 -0
  62. package/dist/scheduler/scheduler.js +83 -10
  63. package/dist/scheduler/scheduler.js.map +1 -1
  64. package/dist/service-installer.d.ts +2 -1
  65. package/dist/service-installer.js +18 -3
  66. package/dist/service-installer.js.map +1 -1
  67. package/dist/tmux-manager.d.ts +5 -0
  68. package/dist/tmux-manager.js +16 -0
  69. package/dist/tmux-manager.js.map +1 -1
  70. package/dist/tool-permissions.d.ts +5 -0
  71. package/dist/tool-permissions.js +15 -2
  72. package/dist/tool-permissions.js.map +1 -1
  73. package/dist/types.d.ts +1 -1
  74. package/dist/ui/app-effects.js +33 -9
  75. package/dist/ui/chat-effects.js +62 -4
  76. package/dist/ui/chat-strings.js +40 -4
  77. package/dist/ui/chat-thread.js +13 -5
  78. package/dist/ui/effect-trial-panel.js +19 -16
  79. package/dist/ui/fleet-cache.js +23 -1
  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 +8 -2
  85. package/dist/ui/panel-details.js +2 -1
  86. package/dist/ui/panel-fleet.js +17 -5
  87. package/dist/ui/panel-settings.js +3 -2
  88. package/dist/ui/preview.js +3 -2
  89. package/dist/ui/settings-appearance.js +34 -18
  90. package/dist/ui/settings-dialogs.js +3 -6
  91. package/dist/ui/settings-effects.js +4 -3
  92. package/dist/ui/settings-learn.js +25 -0
  93. package/dist/ui/settings-model.js +6 -2
  94. package/dist/ui/settings-mods.js +4 -3
  95. package/dist/ui/settings-owner-effects.js +8 -8
  96. package/dist/ui/settings-strings.js +34 -38
  97. package/dist/ui/shared/app-i18n.js +2 -2
  98. package/dist/ui/shared/app-prefs.js +1 -1
  99. package/dist/ui/shared/app-shell.js +23 -1
  100. package/dist/ui/shared/app-store.js +12 -1
  101. package/dist/ui/shared/app-stream.js +2 -1
  102. package/dist/ui/shared/app.css +163 -12
  103. package/dist/ui/shared/effect-app-runtime.js +1 -1
  104. package/dist/ui/shared/effect-frame.js +10 -5
  105. package/dist/ui/shared/effect-geometry.js +24 -9
  106. package/dist/ui/shared/effect-policy.js +2 -1
  107. package/dist/ui/shared/effect-registry.js +71 -17
  108. package/dist/ui/shared/effect-trial-shadow.js +62 -0
  109. package/dist/ui/shared/effect-trial-stage.js +15 -9
  110. package/dist/ui/shared/effect-trial.css +40 -0
  111. package/dist/ui/shared/identity-card.js +5 -2
  112. package/dist/ui/shared/identity-color.js +3 -3
  113. package/dist/ui/shared/instance-nav.js +35 -9
  114. package/dist/ui/shared/panel-view.js +2 -2
  115. package/dist/ui/shared/usage-dialog.js +14 -2
  116. package/dist/ui/shared/view-strings.js +22 -2
  117. package/dist/ui/signin.html +1 -1
  118. package/dist/usage/providers.d.ts +3 -0
  119. package/dist/usage/providers.js +4 -3
  120. package/dist/usage/providers.js.map +1 -1
  121. package/dist/view-api.d.ts +21 -0
  122. package/dist/view-api.js +28 -0
  123. package/dist/view-api.js.map +1 -1
  124. package/dist/web-api.d.ts +5 -6
  125. package/dist/web-api.js +60 -11
  126. package/dist/web-api.js.map +1 -1
  127. package/dist/web-appearance-view.d.ts +10 -0
  128. package/dist/web-appearance-view.js +77 -8
  129. package/dist/web-appearance-view.js.map +1 -1
  130. package/dist/web-asset-parts.d.ts +3 -8
  131. package/dist/web-asset-parts.js.map +1 -1
  132. package/dist/web-auth.js +1 -1
  133. package/dist/web-auth.js.map +1 -1
  134. package/dist/web-effect-service.js +3 -1
  135. package/dist/web-effect-service.js.map +1 -1
  136. package/dist/web-effect-tools.js +4 -1
  137. package/dist/web-effect-tools.js.map +1 -1
  138. package/dist/web-effect-trials.d.ts +4 -4
  139. package/dist/web-effect-trials.js +6 -6
  140. package/dist/web-effect-trials.js.map +1 -1
  141. package/dist/web-effect-upload.d.ts +2 -4
  142. package/dist/web-effect-upload.js +3 -8
  143. package/dist/web-effect-upload.js.map +1 -1
  144. package/dist/web-effect.d.ts +14 -0
  145. package/dist/web-effect.js +21 -0
  146. package/dist/web-effect.js.map +1 -1
  147. package/dist/web-guest-access.d.ts +67 -0
  148. package/dist/web-guest-access.js +126 -0
  149. package/dist/web-guest-access.js.map +1 -0
  150. package/dist/web-identity-palette.d.ts +1 -1
  151. package/dist/web-identity-palette.js +2 -2
  152. package/dist/web-identity-palette.js.map +1 -1
  153. package/dist/web-mod-agent.js +5 -3
  154. package/dist/web-mod-agent.js.map +1 -1
  155. package/dist/web-mod-bundle.d.ts +5 -0
  156. package/dist/web-mod-bundle.js +6 -0
  157. package/dist/web-mod-bundle.js.map +1 -1
  158. package/dist/web-mod-store.d.ts +5 -0
  159. package/dist/web-mod-store.js +8 -1
  160. package/dist/web-mod-store.js.map +1 -1
  161. package/dist/web-prefs.d.ts +8 -0
  162. package/dist/web-prefs.js +9 -0
  163. package/dist/web-prefs.js.map +1 -1
  164. package/dist/web-preview.d.ts +6 -2
  165. package/dist/web-preview.js +4 -2
  166. package/dist/web-preview.js.map +1 -1
  167. package/dist/web-session.d.ts +15 -7
  168. package/dist/web-session.js +25 -8
  169. package/dist/web-session.js.map +1 -1
  170. package/dist/web-settings-tools.d.ts +69 -0
  171. package/dist/web-settings-tools.js +247 -0
  172. package/dist/web-settings-tools.js.map +1 -0
  173. package/package.json +1 -1
  174. package/templates/systemd.service.ejs +8 -0
@@ -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 */
@@ -157,7 +174,9 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
157
174
  }
158
175
  /* #1604: one row — Settings and Session (the anonymous View reader: Sign in and the theme/language popover). Their
159
176
  pop-ups open over the whole footer's width. */
160
- .side-foot { display: flex; align-items: center; gap: var(--s-1); padding: var(--s-2); position: relative; }
177
+ /* #1922: a divider above the footer, with the filter block's bottom padding and the footer's top padding trimmed to
178
+ --s-1: the gap reads as a section break, not an empty ~20 px band (the two --s-2 paddings used to stack). */
179
+ .side-foot { display: flex; align-items: center; gap: var(--s-1); padding: var(--s-1) var(--s-2) var(--s-2); border-top: 1px solid var(--border); position: relative; }
161
180
  .side-foot > .side-row, .side-foot > .session { flex: 1; min-width: 0; }
162
181
  .side-foot > .session { position: static; }
163
182
  .side-foot .side-row span:not(.dot):not(.meta) { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@@ -220,11 +239,15 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
220
239
  }
221
240
  .panel-head .sb-open { display: none; }
222
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)); }
223
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. */
224
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)); }
225
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; }
226
247
  .panel-sub { color: var(--text-2); font-size: var(--fs-sm); white-space: nowrap; }
227
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; }
228
251
  .panel-actions { display: flex; align-items: center; gap: var(--s-1); }
229
252
  /* #1523 N2: the instance view switch — beside the title on a wide screen; on a phone, a row of tabs under the header. */
230
253
  .inst-switch.seg { padding: 0; margin: 0; width: auto; max-width: none; flex-shrink: 0; }
@@ -723,6 +746,7 @@ html.lb-open .scroller { overflow: hidden; }
723
746
  .sb-open .sidebar { transform: none; visibility: visible; }
724
747
  .sb-open .scrim { display: block; position: fixed; inset: 0; background: var(--scrim); z-index: 39; }
725
748
  .panel-head .sb-open { display: inline-flex; }
749
+ .panel-head .sb-new { display: none; }
726
750
  .only-narrow { display: inline-flex; }
727
751
  .thread { padding: var(--s-3) var(--s-3) var(--s-2); gap: var(--s-5); }
728
752
  .col { padding: var(--s-3) var(--s-3) var(--s-8); }
@@ -781,6 +805,9 @@ html.lb-open .scroller { overflow: hidden; }
781
805
  .inst-star.on { color: var(--warn); }
782
806
  .inst-star.on svg, .fav-mark { fill: currentColor; }
783
807
  .fav-head { cursor: default; }
808
+ /* #1936: the open instance, pinned on top while a filter is on — its header reads "Current", a line under it. */
809
+ .pinned-head { cursor: default; }
810
+ .v-pinned { border-bottom: 1px solid var(--border); padding-bottom: var(--s-2); margin-bottom: var(--s-1); }
784
811
  .fav-mark { color: var(--warn); }
785
812
  .inst-nav:not(.fav-editing) .inst-star:not(.on) { visibility: hidden; }
786
813
  /* As specific as the rule that hides it (and later), so hover and focus win. */
@@ -800,7 +827,7 @@ body.side-resizing, body.side-resizing * { cursor: col-resize !important; user-s
800
827
  .dragging { opacity: 0.4; }
801
828
  .drop-before { box-shadow: inset 0 2px 0 var(--accent); }
802
829
  .drop-after { box-shadow: inset 0 -2px 0 var(--accent); }
803
- .v-filter { flex: none; padding: var(--s-2) var(--s-3) var(--s-2); border-top: 1px solid var(--border); }
830
+ .v-filter { flex: none; padding: var(--s-2) var(--s-3) var(--s-1); border-top: 1px solid var(--border); }
804
831
  .v-filter-box { display: flex; align-items: center; gap: var(--s-2); padding: 0 var(--s-1) 0 var(--s-3); border-radius: var(--r-sm); background: var(--surface); color: var(--text-3); }
805
832
  .v-filter-box:focus-within { box-shadow: 0 0 0 2px var(--focus); }
806
833
  .v-filter-box input { flex: 1; min-width: 0; height: 34px; border: 0; background: transparent; color: var(--text); font-size: var(--fs-sm); outline: none; }
@@ -863,6 +890,10 @@ body.side-resizing, body.side-resizing * { cursor: col-resize !important; user-s
863
890
  /* #1749: one line beside ↑↓ — the name gives way first, then the plan badge (each with an ellipsis); ↑↓ never wrap. */
864
891
  .u-head strong { min-width: 0; flex: 0 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
865
892
  .u-plan { min-width: 0; flex: none; max-width: 55%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
893
+ /* #2014: a credential profile's row — the provider name beside the plan badge, and the profile on its own full-width
894
+ muted line under the head (one line in the rail, whole in the dialog). */
895
+ .u-profile { margin-top: calc(-1 * var(--s-2) + 2px); color: var(--text-3); font-size: var(--fs-xs); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
896
+ .dlg .u-profile { white-space: normal; overflow-wrap: anywhere; }
866
897
  .u-order { margin-left: auto; display: flex; gap: 2px; flex: none; }
867
898
  .u-order .icon-btn { width: 28px; height: 28px; }
868
899
  .u-order .icon-btn:disabled { opacity: 0.3; }
@@ -1113,6 +1144,114 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
1113
1144
  content: ""; position: absolute; top: var(--s-5); left: calc(-1 * var(--s-4) - 2px); width: var(--s-4); height: 2px; background: var(--border-strong); }
1114
1145
  .org-link { min-height: var(--tap); }
1115
1146
  }
1147
+ /* ── #1918: the org chart as a canvas (fleet-canvas.js). SVG with classes only: per-card values are SVG attributes, the
1148
+ view's transform one attribute on .oc-world (#1300). The stage fills the panel's height; touch gestures are the
1149
+ canvas's own (touch-action: none on it only), the page around it still scrolls. ── */
1150
+ .oc-bar { flex-wrap: wrap; row-gap: var(--s-2); }
1151
+ .oc-tools { margin-left: auto; display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); }
1152
+ .oc-find { flex: 0 1 260px; min-width: 150px; position: relative; }
1153
+ .oc-find input { height: 32px; padding-right: 72px; }
1154
+ .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; }
1155
+ .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); }
1156
+ .oc-svg { display: block; width: 100%; height: 100%; touch-action: none; cursor: grab; user-select: none; -webkit-user-select: none; }
1157
+ .oc-svg:active { cursor: grabbing; }
1158
+ .oc-svg:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
1159
+ .oc-tree path { fill: none; stroke: var(--border-strong); stroke-width: 1.25; }
1160
+ .oc-box { fill: var(--surface); stroke: var(--border); stroke-width: 1; }
1161
+ .oc-team.loose .oc-box { stroke-dasharray: 5 4; }
1162
+ .oc-box.match { stroke: var(--focus); stroke-width: 2.5; }
1163
+ .oc-head { fill: var(--surface-2); }
1164
+ .oc-hit { fill: transparent; }
1165
+ .oc-toggle:not(.empty) { cursor: pointer; }
1166
+ .oc-toggle:focus { outline: none; }
1167
+ .oc-toggle:focus-visible .oc-hit { stroke: var(--focus); stroke-width: 2; }
1168
+ .oc-caret { fill: var(--text-2); font-size: 12px; }
1169
+ .oc-team-name { fill: var(--text); font-size: 13px; font-weight: 600; }
1170
+ .oc-team-meta { fill: var(--text-3); font-size: 11px; }
1171
+ .oc-team-sum { fill: var(--text-2); font-size: 11.5px; }
1172
+ .oc-team.dim { opacity: 0.55; }
1173
+ .oc-node.dim { opacity: 0.4; }
1174
+ .oc-node { cursor: pointer; }
1175
+ .oc-node[role="img"] { cursor: default; }
1176
+ .oc-node:focus { outline: none; }
1177
+ .oc-card { fill: var(--bg); stroke: var(--border); stroke-width: 1; }
1178
+ .oc-node.lead .oc-card { fill: var(--surface); stroke: var(--accent); stroke-width: 1.5; }
1179
+ .oc-node:hover .oc-card { stroke: var(--border-strong); }
1180
+ .oc-node:focus-visible .oc-card, .oc-node.match .oc-card, .oc-node.match:hover .oc-card { stroke: var(--focus); stroke-width: 2.5; }
1181
+ .oc-node.st-missing { opacity: 0.7; }
1182
+ .oc-av { fill: var(--surface-2); stroke: var(--surface); stroke-width: 2; }
1183
+ .oc-node.idc.id-ring .oc-av { stroke: var(--id); stroke-width: 2.5; }
1184
+ .oc-idbar { display: none; }
1185
+ .oc-node.idc.id-bar .oc-idbar { display: inline; fill: var(--id); }
1186
+ .oc-face.emoji .oc-av { fill: transparent; }
1187
+ .oc-av-t { fill: var(--text-2); font-size: 12px; font-weight: 600; }
1188
+ .oc-face.emoji .oc-av-t { font-size: 18px; font-weight: normal; }
1189
+ .oc-name { fill: var(--text); font-size: 13.5px; font-weight: 600; }
1190
+ .oc-node.lead .oc-name { font-size: 15px; }
1191
+ .oc-node.idc.id-name .oc-name { fill: var(--id-name); }
1192
+ .oc-sub { font-size: 12px; }
1193
+ .oc-st { fill: var(--text-2); }
1194
+ .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); }
1195
+ .oc-dot { fill: var(--text-3); stroke: var(--bg); stroke-width: 2; }
1196
+ .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); }
1197
+ .oc-dot.st-paused { fill: var(--bg); stroke: var(--text-3); stroke-width: 1.5; }
1198
+ .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); }
1199
+ .oc-zoom .icon-btn { width: 36px; height: 34px; border-radius: 0; font-size: 18px; }
1200
+ .oc-zoom .icon-btn + .icon-btn { border-top: 1px solid var(--border); }
1201
+ .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; }
1202
+ .oc-mini-team { fill: var(--surface-2); stroke: var(--border-strong); stroke-width: 6; }
1203
+ .oc-mini-team.needs { fill: var(--warn-bg); stroke: var(--warn); }
1204
+ .oc-mini-top { fill: var(--accent-dim); stroke: var(--accent); stroke-width: 6; }
1205
+ .oc-mini-team.match { stroke: var(--focus); stroke-width: 14; }
1206
+ .oc-mini-hit { fill: var(--focus); }
1207
+ .oc-mini-view { fill: none; stroke: var(--focus); stroke-width: 10; }
1208
+ /* #1918 phase 3: live traffic. Kind hues kept apart from the state colours (ok = working/idle, warn = needs you, danger). */
1209
+ .oc { --fx-task: #b794f6; --fx-send: #2dd4bf; --fx-report: #f472b6; --fx-broadcast: #fb923c; --fx-team: #cbd5e1; }
1210
+ @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; } }
1211
+ :root[data-theme="light"] .oc { --fx-task: #7c3aed; --fx-send: #0f766e; --fx-report: #be185d; --fx-broadcast: #c2410c; --fx-team: #475569; }
1212
+ .oc-flow { fill: none; stroke-width: 2.5; stroke-linecap: round; }
1213
+ .oc-flow-halo { fill: none; stroke: var(--bg); stroke-width: 6; stroke-linecap: round; opacity: 0.85; }
1214
+ .oc-flow.h-task { stroke: var(--fx-task); } .oc-flow.h-send { stroke: var(--fx-send); } .oc-flow.h-report { stroke: var(--fx-report); }
1215
+ .oc-flow.h-broadcast { stroke: var(--fx-broadcast); } .oc-flow.h-team { stroke: var(--fx-team); stroke-width: 3.5; }
1216
+ .oc-flow.ph-live { stroke-dasharray: 7 6; animation: oc-dash 0.9s linear infinite; }
1217
+ .oc-flow.ph-queued { stroke-dasharray: 2 5; opacity: 0.8; }
1218
+ .oc-flow.ph-fading { stroke: var(--text-3); animation: oc-fade 3s ease-out forwards; }
1219
+ .oc-ah.h-task { fill: var(--fx-task); } .oc-ah.h-send { fill: var(--fx-send); } .oc-ah.h-report { fill: var(--fx-report); }
1220
+ .oc-ah.h-broadcast { fill: var(--fx-broadcast); } .oc-ah.h-team { fill: var(--fx-team); } .oc-ah.h-fading { fill: var(--text-3); }
1221
+ .oc-pulse.h-task { fill: var(--fx-task); } .oc-pulse.h-send { fill: var(--fx-send); } .oc-pulse.h-report { fill: var(--fx-report); }
1222
+ .oc-pulse.h-broadcast { fill: var(--fx-broadcast); } .oc-pulse.h-team { fill: var(--fx-team); }
1223
+ .oc-fl.dim { opacity: 0.18; }
1224
+ .oc-badge rect { fill: var(--surface-2); stroke: var(--border); } .oc-badge text { fill: var(--text-2); font-size: 10.5px; }
1225
+ @keyframes oc-dash { to { stroke-dashoffset: -26; } }
1226
+ @keyframes oc-fade { from { opacity: 0.8; } to { opacity: 0.15; } }
1227
+ .oc-legend { position: absolute; left: var(--s-3); bottom: var(--s-3); max-width: calc(100% - 220px);
1228
+ 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; }
1229
+ .oc-legend summary { cursor: pointer; font-weight: var(--fw-semibold); color: var(--text); }
1230
+ .oc-legend[open] summary { margin-bottom: 4px; }
1231
+ .oc-lg-rows { display: flex; flex-wrap: wrap; gap: var(--s-1) var(--s-3); }
1232
+ .oc-lg { display: inline-flex; align-items: center; gap: 6px; }
1233
+ .oc-sw { width: 18px; height: 3px; border-radius: 2px; display: inline-block; background: var(--text-3); }
1234
+ .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); }
1235
+ .oc-sw.ph-queued { background: repeating-linear-gradient(90deg, var(--text-2) 0 3px, transparent 3px 7px); }
1236
+ .oc-sw.ph-fading { opacity: 0.5; }
1237
+ .oc-live { gap: 6px; }
1238
+ .oc-live-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text-3); }
1239
+ .oc-live.on .oc-live-dot { background: var(--ok); }
1240
+ .org-sending { font-size: var(--fs-xs); color: var(--text-2); overflow-wrap: anywhere; }
1241
+ .org-send.k-task { color: var(--fx-task); } .org-send.k-report { color: var(--fx-report); } .org-send.k-broadcast { color: var(--fx-broadcast); }
1242
+ .org-send.k-send, .org-send.k-query, .org-send.k-update { color: var(--fx-send); }
1243
+ @media (prefers-reduced-motion: reduce) {
1244
+ .oc-flow.ph-live { animation: none; stroke-dasharray: none; }
1245
+ .oc-flow.ph-fading { animation: none; opacity: 0.4; }
1246
+ .oc-pulse { display: none; }
1247
+ }
1248
+ @media (max-width: 640px) {
1249
+ .oc-stage { height: calc(100dvh - 248px); min-height: 340px; }
1250
+ .oc-find { flex: 1 1 150px; }
1251
+ .oc-tools { margin-left: 0; width: 100%; flex-wrap: nowrap; }
1252
+ .oc-mini { display: none; }
1253
+ .oc-legend { max-width: calc(100% - 24px); gap: 2px var(--s-2); }
1254
+ }
1116
1255
  /* A phone: Fleet's five tabs fit when only the open one shows its label (the others keep theirs for screen readers). */
1117
1256
  @media (max-width: 479px) {
1118
1257
  .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; }
@@ -1191,12 +1330,17 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
1191
1330
  font-weight: var(--fw-medium); white-space: nowrap; }
1192
1331
 
1193
1332
  /* #1629 r4 §1 / r4.1 Q3 (a): the identity card (identity-card.js). The size is a class token; its heights are here
1194
- (S 160 / M 240 / L 360 px, on a phone 120 / 180 / 240); the banner scales with the portrait. Never a style attribute. */
1195
- .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; }
1196
- .id-card.card-s { --id-h: 160px; }
1197
- .id-card.card-l { --id-h: 360px; }
1198
- .id-card.card-c { --id-h: 96px; margin: 0; border: 0; border-radius: 0; background: none; }
1199
- .id-banner { display: block; width: 100%; height: calc(var(--id-h) / 2); object-fit: cover; }
1333
+ (S 160 / M 240 / L 360 px, on a phone 120 / 180 / 240). Never a style attribute.
1334
+ #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,
1335
+ 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.
1336
+ The chat header's card (card-c) follows the same size choice (card-c bs-<size>). */
1337
+ .id-card { --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; }
1338
+ .id-card.card-s { --id-h: 160px; --id-ar: 4 / 1; --id-bmin: 100px; --id-bmax: 160px; }
1339
+ .id-card.card-l { --id-h: 360px; --id-ar: 9 / 4; --id-bmin: 200px; --id-bmax: 360px; }
1340
+ .id-card.card-c { --id-h: 96px; --id-bmin: 120px; --id-bmax: 200px; margin: 0; border: 0; border-radius: 0; background: none; }
1341
+ .id-card.card-c.bs-s { --id-ar: 4 / 1; --id-bmin: 96px; --id-bmax: 150px; }
1342
+ .id-card.card-c.bs-l { --id-ar: 9 / 4; --id-bmin: 150px; --id-bmax: 220px; }
1343
+ .id-banner { display: block; width: 100%; height: auto; aspect-ratio: var(--id-ar); min-height: var(--id-bmin); max-height: var(--id-bmax); object-fit: cover; object-position: 50% 50%; }
1200
1344
  .id-row { display: flex; align-items: flex-start; gap: var(--s-3); padding: var(--s-3) var(--s-4); }
1201
1345
  .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); }
1202
1346
  .id-text { flex: 1; min-width: 0; }
@@ -1254,9 +1398,12 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
1254
1398
  .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; }
1255
1399
  .chat-card-peek { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-3); }
1256
1400
  @media (max-width: 479px) {
1257
- .id-card.card-s { --id-h: 120px; }
1258
- .id-card.card-m { --id-h: 180px; }
1259
- .id-card.card-l { --id-h: 240px; }
1401
+ .id-card.card-s { --id-h: 120px; --id-bmin: 80px; --id-bmax: 120px; }
1402
+ .id-card.card-m { --id-h: 180px; --id-bmin: 100px; --id-bmax: 150px; }
1403
+ .id-card.card-l { --id-h: 240px; --id-bmin: 120px; --id-bmax: 200px; }
1404
+ .id-card.card-c { --id-bmin: 100px; --id-bmax: 140px; }
1405
+ .id-card.card-c.bs-s { --id-bmin: 80px; --id-bmax: 110px; }
1406
+ .id-card.card-c.bs-l { --id-bmin: 110px; --id-bmax: 150px; }
1260
1407
  .id-row { flex-wrap: wrap; }
1261
1408
  .id-text { flex-basis: 100%; }
1262
1409
  }
@@ -1295,6 +1442,10 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
1295
1442
  .s-fx-owner-row{display:flex;align-items:end;flex-wrap:wrap;gap:var(--s-2);margin-top:var(--s-2)}
1296
1443
  .s-fx-owner-row>label{display:flex;flex-direction:column;gap:var(--s-1);min-width:0;flex:1}
1297
1444
 
1445
+ /* #2044: the effect frame's document declares no colour scheme (light). An iframe whose own colour scheme differs
1446
+ from its document's gets an opaque backdrop, so under the dark theme the frame hid the whole chat: keep them equal. */
1447
+ .agend-effect-frame { color-scheme: normal !important; }
1448
+
1298
1449
  /* #1629: host-created static poster IMG; geometry and visibility belong to the trusted CSSOM binding. */
1299
1450
  .agend-effect-poster { object-fit: cover !important; object-position: center !important; user-select: none; }
1300
1451
 
@@ -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,