@songsid/agend 2.3.0 → 2.3.2-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/README.md +1 -1
  2. package/README.zh-TW.md +1 -1
  3. package/dist/authority/authority-state.d.ts +4 -0
  4. package/dist/authority/authority-state.js +16 -10
  5. package/dist/authority/authority-state.js.map +1 -1
  6. package/dist/authority/session-authorizations.d.ts +7 -1
  7. package/dist/authority/session-authorizations.js +6 -4
  8. package/dist/authority/session-authorizations.js.map +1 -1
  9. package/dist/authority/web-authority.d.ts +19 -6
  10. package/dist/authority/web-authority.js +22 -8
  11. package/dist/authority/web-authority.js.map +1 -1
  12. package/dist/backend/muse.d.ts +20 -2
  13. package/dist/backend/muse.js +154 -16
  14. package/dist/backend/muse.js.map +1 -1
  15. package/dist/channel/adapters/discord.d.ts +2 -0
  16. package/dist/channel/adapters/discord.js +33 -3
  17. package/dist/channel/adapters/discord.js.map +1 -1
  18. package/dist/channel/adapters/telegram.d.ts +1 -1
  19. package/dist/channel/adapters/telegram.js +3 -35
  20. package/dist/channel/adapters/telegram.js.map +1 -1
  21. package/dist/channel/mcp-tools.js +5 -5
  22. package/dist/channel/mcp-tools.js.map +1 -1
  23. package/dist/channel/types.d.ts +11 -0
  24. package/dist/cli.js +6 -3
  25. package/dist/cli.js.map +1 -1
  26. package/dist/config-validator.js +42 -5
  27. package/dist/config-validator.js.map +1 -1
  28. package/dist/daemon.d.ts +47 -3
  29. package/dist/daemon.js +380 -60
  30. package/dist/daemon.js.map +1 -1
  31. package/dist/delivery-outbox.d.ts +23 -1
  32. package/dist/delivery-outbox.js +114 -17
  33. package/dist/delivery-outbox.js.map +1 -1
  34. package/dist/doctor.d.ts +3 -0
  35. package/dist/doctor.js +9 -0
  36. package/dist/doctor.js.map +1 -1
  37. package/dist/event-log.d.ts +5 -0
  38. package/dist/event-log.js +13 -0
  39. package/dist/event-log.js.map +1 -1
  40. package/dist/fleet-context.d.ts +1 -1
  41. package/dist/fleet-lock.d.ts +6 -0
  42. package/dist/fleet-lock.js +19 -0
  43. package/dist/fleet-lock.js.map +1 -1
  44. package/dist/fleet-manager.d.ts +174 -2
  45. package/dist/fleet-manager.js +678 -68
  46. package/dist/fleet-manager.js.map +1 -1
  47. package/dist/fleet-process-handlers.d.ts +1 -0
  48. package/dist/fleet-process-handlers.js +9 -1
  49. package/dist/fleet-process-handlers.js.map +1 -1
  50. package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +11 -0
  51. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +4 -4
  52. package/dist/general-knowledge/skills/web-appearance/SKILL.md +7 -7
  53. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +109 -57
  54. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +110 -58
  55. package/dist/guest-seen-people.d.ts +38 -0
  56. package/dist/guest-seen-people.js +78 -0
  57. package/dist/guest-seen-people.js.map +1 -0
  58. package/dist/instance-config-impact.js +4 -1
  59. package/dist/instance-config-impact.js.map +1 -1
  60. package/dist/instance-lifecycle.d.ts +2 -0
  61. package/dist/instance-lifecycle.js +27 -1
  62. package/dist/instance-lifecycle.js.map +1 -1
  63. package/dist/locale.js +62 -12
  64. package/dist/locale.js.map +1 -1
  65. package/dist/log-service.js +1 -0
  66. package/dist/log-service.js.map +1 -1
  67. package/dist/mac-auto-restore.d.ts +117 -0
  68. package/dist/mac-auto-restore.js +294 -0
  69. package/dist/mac-auto-restore.js.map +1 -0
  70. package/dist/needs-you.js +1 -0
  71. package/dist/needs-you.js.map +1 -1
  72. package/dist/outbound-handlers.d.ts +2 -2
  73. package/dist/outbound-handlers.js +5 -5
  74. package/dist/outbound-handlers.js.map +1 -1
  75. package/dist/outbound-schemas.d.ts +2 -0
  76. package/dist/outbound-schemas.js +2 -0
  77. package/dist/outbound-schemas.js.map +1 -1
  78. package/dist/public-web-gateway.js +9 -1
  79. package/dist/public-web-gateway.js.map +1 -1
  80. package/dist/search/search-index.d.ts +10 -2
  81. package/dist/search/search-index.js +17 -4
  82. package/dist/search/search-index.js.map +1 -1
  83. package/dist/service-installer.d.ts +23 -1
  84. package/dist/service-installer.js +40 -11
  85. package/dist/service-installer.js.map +1 -1
  86. package/dist/session-oom-stop.d.ts +45 -0
  87. package/dist/session-oom-stop.js +259 -0
  88. package/dist/session-oom-stop.js.map +1 -0
  89. package/dist/session-oom.d.ts +13 -0
  90. package/dist/session-oom.js +119 -7
  91. package/dist/session-oom.js.map +1 -1
  92. package/dist/settings-api.d.ts +27 -1
  93. package/dist/settings-api.js +108 -2
  94. package/dist/settings-api.js.map +1 -1
  95. package/dist/settings-change.js +37 -3
  96. package/dist/settings-change.js.map +1 -1
  97. package/dist/settings-effect.js +5 -2
  98. package/dist/settings-effect.js.map +1 -1
  99. package/dist/spawn-gate.d.ts +2 -0
  100. package/dist/spawn-gate.js +17 -4
  101. package/dist/spawn-gate.js.map +1 -1
  102. package/dist/start-attempt.d.ts +15 -1
  103. package/dist/start-attempt.js +35 -5
  104. package/dist/start-attempt.js.map +1 -1
  105. package/dist/start-failure-chat.d.ts +41 -0
  106. package/dist/start-failure-chat.js +159 -0
  107. package/dist/start-failure-chat.js.map +1 -0
  108. package/dist/start-failure-notice.d.ts +4 -0
  109. package/dist/start-failure-notice.js +8 -0
  110. package/dist/start-failure-notice.js.map +1 -1
  111. package/dist/start-failure.d.ts +49 -0
  112. package/dist/start-failure.js +141 -1
  113. package/dist/start-failure.js.map +1 -1
  114. package/dist/telegram-api-root.d.ts +1 -0
  115. package/dist/telegram-api-root.js +40 -0
  116. package/dist/telegram-api-root.js.map +1 -0
  117. package/dist/turn-reply-guard.d.ts +25 -0
  118. package/dist/turn-reply-guard.js +111 -12
  119. package/dist/turn-reply-guard.js.map +1 -1
  120. package/dist/types.d.ts +21 -3
  121. package/dist/ui/backend-signin.js +5 -2
  122. package/dist/ui/chat-ambient.js +1 -1
  123. package/dist/ui/chat-effects.js +4 -4
  124. package/dist/ui/chat-render.js +5 -2
  125. package/dist/ui/chat-strings.js +2 -2
  126. package/dist/ui/chat-thread.js +2 -2
  127. package/dist/ui/first-run.js +2 -2
  128. package/dist/ui/fleet-canvas-view.js +114 -0
  129. package/dist/ui/fleet-canvas.js +15 -77
  130. package/dist/ui/fleet-office-editor.js +80 -5
  131. package/dist/ui/fleet-office-layout.js +197 -0
  132. package/dist/ui/fleet-office-model.js +6 -3
  133. package/dist/ui/fleet-office-pack.d.ts +39 -0
  134. package/dist/ui/fleet-office-pack.js +240 -29
  135. package/dist/ui/fleet-office-skin.js +130 -21
  136. package/dist/ui/fleet-office-store.js +75 -4
  137. package/dist/ui/fleet-office-view.js +154 -0
  138. package/dist/ui/fleet-office-zones.js +95 -33
  139. package/dist/ui/fleet-office.js +530 -94
  140. package/dist/ui/fleet-usage.js +7 -6
  141. package/dist/ui/panel-chat.js +20 -7
  142. package/dist/ui/panel-details.js +2 -2
  143. package/dist/ui/panel-fleet.js +11 -77
  144. package/dist/ui/panel-log.js +5 -5
  145. package/dist/ui/panel-settings.js +586 -164
  146. package/dist/ui/search-dialog.js +26 -7
  147. package/dist/ui/settings-appearance.js +82 -29
  148. package/dist/ui/settings-apply.js +42 -7
  149. package/dist/ui/settings-dialogs.js +68 -22
  150. package/dist/ui/settings-draft.js +51 -0
  151. package/dist/ui/settings-guests.js +244 -78
  152. package/dist/ui/settings-learn.js +2 -2
  153. package/dist/ui/settings-model.js +2 -0
  154. package/dist/ui/settings-registry.js +142 -0
  155. package/dist/ui/settings-strings.js +108 -44
  156. package/dist/ui/settings-web-authority.js +34 -23
  157. package/dist/ui/shared/app-i18n.js +14 -0
  158. package/dist/ui/shared/app-nav.js +10 -5
  159. package/dist/ui/shared/app-prefs.js +80 -2
  160. package/dist/ui/shared/app-route.js +39 -5
  161. package/dist/ui/shared/app-shell.js +80 -8
  162. package/dist/ui/shared/app-stream.js +2 -1
  163. package/dist/ui/shared/app.css +251 -20
  164. package/dist/ui/shared/app.js +3 -0
  165. package/dist/ui/shared/effect-stock-trust.js +1 -1
  166. package/dist/ui/shared/identity-color.js +4 -4
  167. package/dist/ui/shared/signin.js +2 -2
  168. package/dist/ui/shared/ui-dialog.js +6 -1
  169. package/dist/ui/shared/ui-icons.js +2 -0
  170. package/dist/ui/shared/usage-dialog.js +21 -11
  171. package/dist/ui/shared/view-strings.js +2 -0
  172. package/dist/ui/shared/web-authorize.js +13 -12
  173. package/dist/web-api.d.ts +9 -3
  174. package/dist/web-api.js +161 -6
  175. package/dist/web-api.js.map +1 -1
  176. package/dist/web-auth.js +7 -1
  177. package/dist/web-auth.js.map +1 -1
  178. package/dist/web-experimental.d.ts +1 -1
  179. package/dist/web-experimental.js +1 -1
  180. package/dist/web-experimental.js.map +1 -1
  181. package/dist/web-mod-agent.js +2 -2
  182. package/dist/web-mod-agent.js.map +1 -1
  183. package/dist/web-office-skins.d.ts +108 -0
  184. package/dist/web-office-skins.js +298 -0
  185. package/dist/web-office-skins.js.map +1 -0
  186. package/dist/web-office-store.d.ts +8 -2
  187. package/dist/web-office-store.js +37 -7
  188. package/dist/web-office-store.js.map +1 -1
  189. package/dist/web-settings-patch.d.ts +6 -0
  190. package/dist/web-settings-patch.js +61 -4
  191. package/dist/web-settings-patch.js.map +1 -1
  192. package/dist/web-shell-routes.d.ts +4 -2
  193. package/dist/web-shell-routes.js +17 -7
  194. package/dist/web-shell-routes.js.map +1 -1
  195. package/dist/working-recovery.d.ts +104 -0
  196. package/dist/working-recovery.js +263 -0
  197. package/dist/working-recovery.js.map +1 -0
  198. package/dist/working-state-journal.d.ts +70 -0
  199. package/dist/working-state-journal.js +106 -0
  200. package/dist/working-state-journal.js.map +1 -0
  201. package/package.json +1 -1
@@ -2,27 +2,39 @@
2
2
  // fleet-office-pack.js). Each instance goes to the area for its state now and takes a seat slot there; when its state
3
3
  // changes it walks (along the pack's waypoints) to the new area. Pure — fleet-office.js draws it. Also here: what a
4
4
  // character says in its speech bubble, and which furniture is active.
5
- import { LOGICAL } from "./fleet-office-pack.js";
5
+ import { LOGICAL, own } from "./fleet-office-pack.js";
6
6
  import { nameHash, pathLength, along, routeVia } from "./fleet-office-model.js";
7
7
 
8
8
  /** The area for a state: the pack's own, else its "default", else its first area. */
9
9
  export function zoneOf(pack, state) {
10
10
  const z = pack.stateZones || {};
11
- return z[state] || z.default || Object.keys(pack.areas)[0];
11
+ return own(z, state) || own(z, "default") || Object.keys(pack.areas)[0];
12
12
  }
13
13
 
14
- /** How far apart overflow spots are, and how far they stay from the area's seats (logical units). */
15
- export const OVERFLOW = { STEP: 56, CLEAR: 40, EDGE: 24 };
16
- /** An area's overflow spots, for when its seats are taken: a grid inside its rect, clear of its seats, in reading order. */
17
- export function overflowSpots(area) {
18
- const out = [], { x, y, w, h } = area.rect;
19
- for (let sy = y + OVERFLOW.EDGE + 30; sy <= y + h - OVERFLOW.EDGE; sy += OVERFLOW.STEP) {
20
- for (let sx = x + OVERFLOW.EDGE; sx <= x + w - OVERFLOW.EDGE; sx += OVERFLOW.STEP) {
21
- if (area.seats.every((s) => Math.hypot(s.x - sx, s.y - sy) >= OVERFLOW.CLEAR)) out.push({ x: sx, y: sy });
14
+ /**
15
+ * How far apart overflow spots are, and how far they stay from the area's seats (logical units); TITLE: the band at
16
+ * the top of an area its name is written in, kept clear. A crowded area packs tighter (STEPS, #2513: 70 instances, 42
17
+ * idle, piled up in the lounge) before anyone is left to the "+N" count.
18
+ */
19
+ export const OVERFLOW = { STEP: 56, CLEAR: 40, EDGE: 24, TITLE: 30, STEPS: [56, 46, 38, 32] };
20
+ function spotsAt(area, step) {
21
+ const out = [], { x, y, w, h } = area.rect, clear = step === OVERFLOW.STEP ? OVERFLOW.CLEAR : Math.round(step * 0.72);
22
+ for (let sy = y + OVERFLOW.EDGE + OVERFLOW.TITLE; sy <= y + h - OVERFLOW.EDGE; sy += step) {
23
+ for (let sx = x + OVERFLOW.EDGE; sx <= x + w - OVERFLOW.EDGE; sx += step) {
24
+ if (area.seats.every((s) => Math.hypot(s.x - sx, s.y - sy) >= clear)) out.push({ x: sx, y: sy });
22
25
  }
23
26
  }
24
27
  return out;
25
28
  }
29
+ /**
30
+ * An area's overflow spots, for when its seats are taken: a tidy grid inside its rect, clear of its seats and its
31
+ * title, in reading order. `need`: how many are wanted — the widest spacing that fits them all, else the tightest.
32
+ */
33
+ export function overflowSpots(area, need = 0) {
34
+ let spots = [];
35
+ for (const step of OVERFLOW.STEPS) { spots = spotsAt(area, step); if (spots.length >= need) return spots; }
36
+ return spots;
37
+ }
26
38
 
27
39
  /**
28
40
  * Seats for everyone: `wants` [{ name, area }] in the fleet's order; `prev` what the last call gave (name → { area,
@@ -31,51 +43,71 @@ export function overflowSpots(area) {
31
43
  * Returns { seats: Map name → { area, slot, x, y }, crowd: Map }.
32
44
  */
33
45
  export function assignSeats(pack, wants, prev = new Map()) {
34
- const seats = new Map(), crowd = new Map(), taken = new Map(), spill = new Map();
35
- const spotsOf = (area) => { if (!spill.has(area)) spill.set(area, overflowSpots(pack.areas[area])); return spill.get(area); };
36
- const pos = (area, slot) => (slot >= 0 ? pack.areas[area].seats[slot] : spotsOf(area)[-slot - 1]);
46
+ const seats = new Map(), crowd = new Map(), taken = new Map(), spill = new Map(), demand = new Map();
47
+ const areaOf = (a) => own(pack.areas, a);
48
+ for (const w of wants) demand.set(w.area, (demand.get(w.area) || 0) + 1);
49
+ const spotsOf = (area) => {
50
+ if (!spill.has(area)) spill.set(area, overflowSpots(areaOf(area), Math.max(0, (demand.get(area) || 0) - areaOf(area).seats.length)));
51
+ return spill.get(area);
52
+ };
53
+ const pos = (area, slot) => (slot >= 0 ? areaOf(area).seats[slot] : spotsOf(area)[-slot - 1]);
37
54
  const usedIn = (area) => { if (!taken.has(area)) taken.set(area, new Set()); return taken.get(area); };
38
55
  const give = (name, area, slot) => { const p = pos(area, slot); usedIn(area).add(slot); seats.set(name, { area, slot, x: p.x, y: p.y }); };
39
56
  for (const w of wants) {
40
57
  const p = prev.get(w.name);
41
- if (!p || p.area !== w.area || !pack.areas[w.area] || !pos(w.area, p.slot) || usedIn(w.area).has(p.slot)) continue;
58
+ if (!p || p.area !== w.area || !areaOf(w.area) || !pos(w.area, p.slot) || usedIn(w.area).has(p.slot)) continue;
42
59
  give(w.name, w.area, p.slot);
43
60
  }
44
61
  for (const w of wants) {
45
- if (seats.has(w.name) || !pack.areas[w.area]) continue;
46
- const used = usedIn(w.area), area = pack.areas[w.area];
62
+ if (seats.has(w.name) || !areaOf(w.area)) continue;
63
+ const used = usedIn(w.area), area = areaOf(w.area);
47
64
  let slot = area.seats.findIndex((_, i) => !used.has(i));
48
65
  if (slot < 0) { const j = spotsOf(w.area).findIndex((_, i) => !used.has(-i - 1)); slot = j < 0 ? null : -j - 1; }
49
66
  if (slot === null) crowd.set(w.area, (crowd.get(w.area) || 0) + 1);
50
67
  else give(w.name, w.area, slot);
51
68
  }
52
- return { seats, crowd };
69
+ // Over capacity: more people want the area than it has seats (their bubbles and tags are thinned, fleet-office.js).
70
+ const over = new Set([...demand].filter(([a, n]) => areaOf(a) && n > areaOf(a).seats.length).map(([a]) => a));
71
+ return { seats, crowd, over };
53
72
  }
54
73
 
55
74
  /**
56
75
  * The zones layout for `nodes` (buildOrgView's) on a pack: everyone but the missing at their seat. The same shape as
57
76
  * the desks layout (rooms → desks with a seat), in logical units, plus `mode: "zones"`, the pack's areas and graph, the
58
- * seats given (`seats`, the next call's `prev`) and `crowd`.
77
+ * seats given (`seats`, the next call's `prev`), `crowd` and `over` (the areas over capacity).
59
78
  */
60
79
  export function zonesLayout(pack, nodes, prev = new Map()) {
61
80
  const wants = nodes.filter((n) => n.state !== "missing").map((n) => ({ name: n.name, area: zoneOf(pack, n.state) }));
62
- const { seats, crowd } = assignSeats(pack, wants, prev);
81
+ const { seats, crowd, over } = assignSeats(pack, wants, prev);
63
82
  const desks = nodes.filter((n) => seats.has(n.name)).map((n) => {
64
83
  const s = seats.get(n.name);
65
84
  return { key: `zone/${n.name}`, node: n, x: s.x - 24, y: s.y - 52, w: 48, h: 66, seat: { x: s.x, y: s.y }, wp: null, area: s.area, slot: s.slot };
66
85
  });
67
86
  return { mode: "zones", rooms: [{ key: "\u0000zones", name: null, lobby: false, x: 0, y: 0, w: LOGICAL.w, h: LOGICAL.h, desks }], areas: pack.areas,
68
- waypoints: pack.waypoints || [], edges: pack.edges || [], door: pack.door, width: LOGICAL.w, height: LOGICAL.h, seats, crowd };
87
+ waypoints: pack.waypoints || [], edges: pack.edges || [], door: pack.door, width: LOGICAL.w, height: LOGICAL.h, seats, crowd, over };
69
88
  }
70
89
 
71
90
  // ── Walking ──
72
91
 
73
92
  /** Logical units per ms (crossing the canvas takes about six seconds). */
74
93
  export const WALK = { SPEED: 0.2 };
94
+ /** The step of a polyline a fraction `t` (0–1) of the way along it: { dx, dy } (its direction), or null (no length). */
95
+ export function headingAt(pts, t) {
96
+ const total = pathLength(pts);
97
+ if (!total) return null;
98
+ let left = Math.max(0, Math.min(1, t)) * total;
99
+ for (let i = 1; i < pts.length; i++) {
100
+ const dx = pts[i].x - pts[i - 1].x, dy = pts[i].y - pts[i - 1].y, l = Math.hypot(dx, dy);
101
+ if (l > 0 && (left <= l || i === pts.length - 1)) return { dx, dy };
102
+ left -= l;
103
+ }
104
+ return null;
105
+ }
75
106
  /**
76
107
  * Where each character is. `sync(targets, now, graph, still)`: targets name → { x, y }; someone new is already there;
77
108
  * someone whose target moved walks from where they are now along the graph (routeVia) — or is there at once when
78
- * `still` (reduced motion); someone gone is forgotten. `at(name, now)` → { x, y, walking }; `moving(now)`.
109
+ * `still` (reduced motion); someone gone is forgotten. `at(name, now)` → { x, y, walking } and, while walking, `heading`: the
110
+ * current step's { dx, dy }, which picks a pack's walkDown / walkUp / walkSide strip; `moving(now)`.
79
111
  */
80
112
  export function createWalkers() {
81
113
  const by = new Map();
@@ -84,7 +116,7 @@ export function createWalkers() {
84
116
  const t = (now - w.start) / w.ms;
85
117
  if (t >= 1) return { x: w.to.x, y: w.to.y, walking: false };
86
118
  const p = along(w.pts, Math.max(0, t));
87
- return { x: p.x, y: p.y, walking: true };
119
+ return { x: p.x, y: p.y, walking: true, heading: headingAt(w.pts, Math.max(0, t)) };
88
120
  };
89
121
  return {
90
122
  sync(targets, now, graph, still = false) {
@@ -104,14 +136,17 @@ export function createWalkers() {
104
136
 
105
137
  /**
106
138
  * The person at (x, y) in layout px as the scene draws them (#2431 review): each desk's box moved to where its person is
107
- * now (`posOf(desk)`: a walker's position; null: at the seat), the one drawn last (the nearest) tried first.
139
+ * now (`posOf(desk)`: a walker's position; null: at the seat), the one drawn last (the nearest) tried first — the same
140
+ * order the scene draws them in. `boxOf(desk, at)` (#2513 review): the box the skin draws that person in there (an
141
+ * uploaded character's strip, anchor and size, in its current pose and walking direction); null: the desk's own box.
108
142
  */
109
- export function personAt(desks, x, y, posOf) {
110
- const people = desks.filter((d) => d.node).map((d) => { const at = posOf(d) || d.seat; return { d, at, dx: at.x - d.seat.x, dy: at.y - d.seat.y }; })
143
+ export function personAt(desks, x, y, posOf, boxOf = null) {
144
+ const people = desks.filter((d) => d.node).map((d) => ({ d, at: posOf(d) || { ...d.seat, walking: false, heading: null } }))
111
145
  .sort((a, b) => a.at.y - b.at.y);
112
146
  for (let i = people.length - 1; i >= 0; i--) {
113
- const { d, dx, dy } = people[i];
114
- if (x >= d.x + dx && x < d.x + dx + d.w && y >= d.y + dy && y < d.y + dy + d.h) return d;
147
+ const { d, at } = people[i];
148
+ const b = (boxOf && boxOf(d, at)) || { x: d.x + at.x - d.seat.x, y: d.y + at.y - d.seat.y, w: d.w, h: d.h };
149
+ if (x >= b.x && x < b.x + b.w && y >= b.y && y < b.y + b.h) return d;
115
150
  }
116
151
  return null;
117
152
  }
@@ -143,11 +178,13 @@ export function bubbleAt(name, state, ms, lines) {
143
178
  * { key, calling, shown, box, tail? } with `box` the bubble's whole box ({ x, y, w, h }, for its whole line, so it never
144
179
  * moves while typing out) and `tail` the point its tail reaches: the strip from the box down to it is kept clear too. The ones calling for the person go first (they always show), then the one that started earliest
145
180
  * (the most typed out) — one already showing keeps its place. Each takes its spot, or one row up (`lift`) when that is
146
- * free; else it waits for another round. Returns the placed ones, each with its `dy` (0 or -lift).
181
+ * free; else it waits for another round. Returns the placed ones, each with its `dy` (0 or -lift). `rank` (optional,
182
+ * bubbleRank): a lower one goes first (else: calling 0, the rest 2); `blocked`: boxes no bubble may cover (name tags).
147
183
  */
148
- export function placeBubbles(items, lift = 22) {
149
- const order = [...items].sort((a, b) => (Number(b.calling) - Number(a.calling)) || (b.shown - a.shown) || (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
150
- const taken = [], out = [];
184
+ export function placeBubbles(items, lift = 22, blocked = []) {
185
+ const rank = (it) => (it.rank ?? (it.calling ? 0 : 2));
186
+ const order = [...items].sort((a, b) => (rank(a) - rank(b)) || (b.shown - a.shown) || (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
187
+ const taken = [...blocked], out = [];
151
188
  const hits = (r) => taken.some((t) => r.x < t.x + t.w && t.x < r.x + r.w && r.y < t.y + t.h && t.y < r.y + r.h);
152
189
  for (const it of order) {
153
190
  for (const dy of [0, -lift]) {
@@ -161,10 +198,35 @@ export function placeBubbles(items, lift = 22) {
161
198
  return out;
162
199
  }
163
200
 
201
+ /** Who speaks first when bubbles compete: calling for the person, then working, then everyone else. */
202
+ export const bubbleRank = (state) => (CALLING.has(state) ? 0 : state === "working" ? 1 : 2);
203
+ /** At most this many bubbles in an area over capacity (#2513). */
204
+ export const CROWD_BUBBLES = 3;
205
+ /** States whose bubbles are left out in an area over capacity (the chatter; a call or work still speaks). */
206
+ const QUIET_IN_CROWD = new Set(["idle", "paused", "stopped", "starting", "queued"]);
207
+ /**
208
+ * The bubbles a frame may show (#2513): in an area over capacity (`over`), none for idle or asleep people and at most
209
+ * CROWD_BUBBLES, the highest ranked first (bubbleRank); elsewhere all of them. `items`: { area, state, shown, key, … }.
210
+ */
211
+ export function thinBubbles(items, over) {
212
+ const byArea = new Map(), out = [];
213
+ for (const it of items) {
214
+ if (!over || !over.has(it.area)) { out.push(it); continue; }
215
+ if (QUIET_IN_CROWD.has(it.state)) continue;
216
+ if (!byArea.has(it.area)) byArea.set(it.area, []);
217
+ byArea.get(it.area).push(it);
218
+ }
219
+ for (const list of byArea.values()) {
220
+ list.sort((a, b) => (bubbleRank(a.state) - bubbleRank(b.state)) || (b.shown - a.shown) || (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
221
+ out.push(...list.slice(0, CROWD_BUBBLES));
222
+ }
223
+ return out;
224
+ }
225
+
164
226
  /** The lines for a state: the skin's (in the page's language), else the page's own. */
165
227
  export function linesFor(state, lang, pack, defaults) {
166
- const own = pack && pack.bubbles && pack.bubbles[lang] && pack.bubbles[lang][state];
167
- return own && own.length ? own : defaults[state] || [];
228
+ const mine = pack && own(own(pack.bubbles, lang), state);
229
+ return Array.isArray(mine) && mine.length ? mine : own(defaults, state) || [];
168
230
  }
169
231
 
170
232
  // ── Furniture ──