@songsid/agend 2.3.2-beta.2 → 2.3.2-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 (110) hide show
  1. package/dist/agent-cli.js +21 -2
  2. package/dist/agent-cli.js.map +1 -1
  3. package/dist/agent-endpoint.js +5 -0
  4. package/dist/agent-endpoint.js.map +1 -1
  5. package/dist/auth-api.js +2 -0
  6. package/dist/auth-api.js.map +1 -1
  7. package/dist/backend/kiro.d.ts +21 -1
  8. package/dist/backend/kiro.js +89 -1
  9. package/dist/backend/kiro.js.map +1 -1
  10. package/dist/channel/adapters/discord.js +7 -4
  11. package/dist/channel/adapters/discord.js.map +1 -1
  12. package/dist/channel/mcp-tools.js +4 -0
  13. package/dist/channel/mcp-tools.js.map +1 -1
  14. package/dist/classic-channel-manager.d.ts +5 -0
  15. package/dist/classic-channel-manager.js +14 -1
  16. package/dist/classic-channel-manager.js.map +1 -1
  17. package/dist/config-validator.js +22 -5
  18. package/dist/config-validator.js.map +1 -1
  19. package/dist/config.js +6 -1
  20. package/dist/config.js.map +1 -1
  21. package/dist/daemon.js +16 -4
  22. package/dist/daemon.js.map +1 -1
  23. package/dist/dashboard-permissions.d.ts +20 -6
  24. package/dist/dashboard-permissions.js +58 -9
  25. package/dist/dashboard-permissions.js.map +1 -1
  26. package/dist/fleet-manager.d.ts +16 -1
  27. package/dist/fleet-manager.js +183 -21
  28. package/dist/fleet-manager.js.map +1 -1
  29. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +24 -1
  30. package/dist/general-knowledge/skills/web-appearance/SKILL.md +24 -1
  31. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +8 -3
  32. package/dist/instance-config-impact.js +10 -5
  33. package/dist/instance-config-impact.js.map +1 -1
  34. package/dist/instance-lifecycle.d.ts +8 -0
  35. package/dist/instance-lifecycle.js +5 -3
  36. package/dist/instance-lifecycle.js.map +1 -1
  37. package/dist/locale.js +2 -6
  38. package/dist/locale.js.map +1 -1
  39. package/dist/notice-delivery.d.ts +15 -0
  40. package/dist/notice-delivery.js +37 -0
  41. package/dist/notice-delivery.js.map +1 -0
  42. package/dist/outbound-schemas.d.ts +20 -0
  43. package/dist/outbound-schemas.js +17 -0
  44. package/dist/outbound-schemas.js.map +1 -1
  45. package/dist/public-web-gateway.js +1 -1
  46. package/dist/public-web-gateway.js.map +1 -1
  47. package/dist/quickstart.js +2 -0
  48. package/dist/quickstart.js.map +1 -1
  49. package/dist/reply-triggers.d.ts +2 -0
  50. package/dist/reply-triggers.js +3 -0
  51. package/dist/reply-triggers.js.map +1 -0
  52. package/dist/settings-api.js +27 -4
  53. package/dist/settings-api.js.map +1 -1
  54. package/dist/settings-change.js +17 -14
  55. package/dist/settings-change.js.map +1 -1
  56. package/dist/settings-effect.js +7 -1
  57. package/dist/settings-effect.js.map +1 -1
  58. package/dist/settings-instance-patch.js +4 -1
  59. package/dist/settings-instance-patch.js.map +1 -1
  60. package/dist/setup-host.js +2 -1
  61. package/dist/setup-host.js.map +1 -1
  62. package/dist/setup-wizard.js +2 -0
  63. package/dist/setup-wizard.js.map +1 -1
  64. package/dist/tool-permissions.js +10 -0
  65. package/dist/tool-permissions.js.map +1 -1
  66. package/dist/topic-commands.js +2 -1
  67. package/dist/topic-commands.js.map +1 -1
  68. package/dist/turn-reply-guard.d.ts +1 -1
  69. package/dist/turn-reply-guard.js +1 -1
  70. package/dist/turn-reply-guard.js.map +1 -1
  71. package/dist/types.d.ts +16 -6
  72. package/dist/ui/chat-strings.js +2 -2
  73. package/dist/ui/fleet-office-editor.js +10 -6
  74. package/dist/ui/fleet-office-layout.js +34 -28
  75. package/dist/ui/fleet-office-model.js +6 -3
  76. package/dist/ui/fleet-office-overview.js +2 -2
  77. package/dist/ui/fleet-office-pack.d.ts +3 -1
  78. package/dist/ui/fleet-office-pack.js +58 -7
  79. package/dist/ui/fleet-office-skin.js +13 -4
  80. package/dist/ui/fleet-office-store.js +27 -1
  81. package/dist/ui/fleet-office-view.js +5 -4
  82. package/dist/ui/fleet-office-zones.js +155 -6
  83. package/dist/ui/fleet-office.js +169 -41
  84. package/dist/ui/panel-chat.js +8 -1
  85. package/dist/ui/panel-settings.js +32 -10
  86. package/dist/ui/settings-dialogs.js +8 -1
  87. package/dist/ui/settings-model.js +14 -6
  88. package/dist/ui/settings-registry.js +4 -0
  89. package/dist/ui/settings-strings.js +14 -0
  90. package/dist/ui/shared/app-i18n.js +2 -0
  91. package/dist/ui/shared/app-shell.js +14 -6
  92. package/dist/ui/shared/app-stream.js +2 -1
  93. package/dist/ui/shared/app.css +20 -2
  94. package/dist/ui/shared/side-panel.js +95 -0
  95. package/dist/web-api.d.ts +3 -0
  96. package/dist/web-api.js +11 -0
  97. package/dist/web-api.js.map +1 -1
  98. package/dist/web-auth.js +1 -1
  99. package/dist/web-auth.js.map +1 -1
  100. package/dist/web-office-acts.d.ts +173 -0
  101. package/dist/web-office-acts.js +285 -0
  102. package/dist/web-office-acts.js.map +1 -0
  103. package/dist/web-office-skins.d.ts +1 -1
  104. package/dist/web-office-skins.js +1 -1
  105. package/dist/web-office-skins.js.map +1 -1
  106. package/dist/web-settings-patch.d.ts +2 -1
  107. package/dist/web-settings-patch.js +4 -1
  108. package/dist/web-settings-patch.js.map +1 -1
  109. package/package.json +1 -1
  110. package/templates/windows-shortcuts.ps1 +11 -2
@@ -3,7 +3,8 @@
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
5
  import { LOGICAL, own } from "./fleet-office-pack.js";
6
- import { nameHash, pathLength, along, routeVia } from "./fleet-office-model.js";
6
+ import { FLIGHT, nameHash, pathLength, along, routeVia } from "./fleet-office-model.js";
7
+ import { hue } from "./fleet-traffic.js";
7
8
 
8
9
  /** The area for a state: the pack's own, else its "default", else its first area. */
9
10
  export function zoneOf(pack, state) {
@@ -76,12 +77,16 @@ export function assignSeats(pack, wants, prev = new Map()) {
76
77
  * the desks layout (rooms → desks with a seat), in logical units, plus `mode: "zones"`, the pack's areas and graph, the
77
78
  * seats given (`seats`, the next call's `prev`), `crowd` and `over` (the areas over capacity).
78
79
  */
79
- export function zonesLayout(pack, nodes, prev = new Map()) {
80
- const wants = nodes.filter((n) => n.state !== "missing").map((n) => ({ name: n.name, area: zoneOf(pack, n.state) }));
80
+ export function zonesLayout(pack, nodes, prev = new Map(), { night = false } = {}) {
81
+ // At night (#2568) the idle go to bed: the area asleep people go to while it has a free seat; the rest doze where they are.
82
+ const bed = zoneOf(pack, "paused"), beds = (own(pack.areas, bed)?.seats || []).length, sleepers = new Set(), inBed = new Set();
83
+ let used = nodes.filter((n) => n.state !== "idle" && zoneOf(pack, n.state) === bed).length;
84
+ if (night) for (const n of nodes) if (n.state === "idle") { sleepers.add(n.name); if (used < beds) { used++; inBed.add(n.name); } }
85
+ const wants = nodes.filter((n) => n.state !== "missing").map((n) => ({ name: n.name, area: inBed.has(n.name) ? bed : zoneOf(pack, n.state) }));
81
86
  const { seats, crowd, over } = assignSeats(pack, wants, prev);
82
87
  const desks = nodes.filter((n) => seats.has(n.name)).map((n) => {
83
88
  const s = seats.get(n.name);
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 };
89
+ 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, night: sleepers.has(n.name) };
85
90
  });
86
91
  return { mode: "zones", rooms: [{ key: "\u0000zones", name: null, lobby: false, x: 0, y: 0, w: LOGICAL.w, h: LOGICAL.h, desks }], areas: pack.areas,
87
92
  waypoints: pack.waypoints || [], edges: pack.edges || [], door: pack.door, width: LOGICAL.w, height: LOGICAL.h, seats, crowd, over };
@@ -103,6 +108,145 @@ export function headingAt(pts, t) {
103
108
  }
104
109
  return null;
105
110
  }
111
+ // ── Night (#2568) ──
112
+
113
+ /** Bedtime on the viewer's clock: from FROM o'clock to TO o'clock the idle sleep (anyone with work still gets up for it). */
114
+ export const NIGHT = { FROM: 0, TO: 6, TICK: 60000 };
115
+ export const isNight = (d = new Date()) => { const h = d.getHours(); return NIGHT.FROM <= NIGHT.TO ? h >= NIGHT.FROM && h < NIGHT.TO : h >= NIGHT.FROM || h < NIGHT.TO; };
116
+
117
+ // ── Wandering (#2560) ──
118
+
119
+ /**
120
+ * An idle person now and then strolls to a spot near their seat and back, so a quiet room is not a row of statues:
121
+ * every PERIOD ms (each person's own phase, from their name) they are AWAY ms at a waypoint within REACH of the seat —
122
+ * one no seat of the office is on (taken or free), picked anew each time — then walk back. Pure: the page asks every TICK ms and walks the change.
123
+ */
124
+ export const WANDER = { PERIOD: 40000, AWAY: 8000, REACH: 220, TICK: 1000 };
125
+ /** Where `name`, seated at `seat`, should be at `t` (ms) while idle: a spot near the seat, or the seat. */
126
+ export function wanderTarget(name, seat, graph, t, seats = []) {
127
+ const near = (graph.waypoints || []).filter((w) => Math.hypot(w.x - seat.x, w.y - seat.y) <= WANDER.REACH
128
+ && !seats.some((s) => Math.hypot(w.x - s.x, w.y - s.y) < 4));
129
+ if (!near.length) return seat;
130
+ const h = nameHash(name), k = t + (h % WANDER.PERIOD), turn = Math.floor(k / WANDER.PERIOD);
131
+ if (k - turn * WANDER.PERIOD >= WANDER.AWAY) return seat;
132
+ const w = near[nameHash(`${name}\u0000${turn}`) % near.length];
133
+ return { x: w.x, y: w.y };
134
+ }
135
+ /** An NPC drawn as furniture (#2596: an item with `walk`): its walker's name — never a person's, and its own phase. */
136
+ export const npcKey = (f) => `furniture/${f.id}`;
137
+ /** Everyone's walk target: their seat, or — idle and `wander` on — wanderTarget's spot (one person a spot: whoever
138
+ * would share it stays seated this time). And, `wander` on, each item of `furniture` with a `walk` (#2596): its spot
139
+ * (x, y), or a stroll near it as an idle person's (by npcKey) — none at `night` for one with a night place (asleep there). */
140
+ export function walkTargets(desks, graph, t, wander = true, furniture = [], night = false) {
141
+ // Every seat of the office (anyone's, taken or not: someone may sit down on it any moment), not only the ones in use.
142
+ // And each walking NPC's spot (#2596 review): its home, where it stands outside its stroll and goes back to whenever
143
+ // its stroll would collide, so nobody else's stroll may end there — no two walkers ever share a target.
144
+ const homes = wander ? furniture.filter((f) => f.walk && !(night && f.night)).map((f) => ({ x: f.x, y: f.y })) : [];
145
+ const seats = [...Object.values(graph.areas || {}).flatMap((a) => a.seats || []), ...desks.map((d) => d.seat), ...homes];
146
+ const used = new Set(homes.map((h) => `${h.x},${h.y}`));
147
+ const stroll = (name, home) => {
148
+ const p = wanderTarget(name, home, graph, t, seats), k = `${p.x},${p.y}`;
149
+ if (p === home || used.has(k)) return home;
150
+ used.add(k);
151
+ return p;
152
+ };
153
+ const out = new Map(desks.map((d) => [d.node.name, !wander || d.node.state !== "idle" || d.night ? d.seat : stroll(d.node.name, d.seat)]));
154
+ if (wander) for (const f of furniture) if (f.walk && !(night && f.night)) out.set(npcKey(f), stroll(npcKey(f), { x: f.x, y: f.y }));
155
+ return out;
156
+ }
157
+
158
+ // ── Mail from another office (#2564) ──
159
+
160
+ /**
161
+ * Something sent to a person here by someone who is not (in another office, or in none): it flies in at the door and
162
+ * lands on the floor near the middle of the room; the person walks over, picks it up and walks back to their seat.
163
+ * MAX_WAIT: a letter no one picks up is gone after it; GAP: letters waiting side by side; SEARCH: how far from the
164
+ * middle a landing spot is looked for when no waypoint will do; ASLEEP: who does not get up
165
+ * for it (it waits for them to wake, up to MAX_WAIT).
166
+ */
167
+ export const MAIL = { MAX_WAIT: 60000, GAP: 16, SEARCH: 320, ASLEEP: new Set(["paused", "stopped"]), MAX: 20 };
168
+ /**
169
+ * Where the `n`th waiting letter lands: `n` GAPs along from the waypoint nearest the room's middle — the nearest whose
170
+ * spot (offset included) no seat of the office is on, taken or free (someone may sit there any moment; #2561 likewise);
171
+ * with none, the nearest point round the middle (rings GAP·2 apart, inside the room) that no seat is on; with none of
172
+ * those either, null — the letter is not delivered here (never on a seat).
173
+ */
174
+ export function dropPoint(L, n = 0) {
175
+ const mid = { x: L.width / 2, y: L.height * 0.55 }, d = (p, q) => Math.hypot(p.x - q.x, p.y - q.y);
176
+ const seats = [...Object.values(L.areas || {}).flatMap((a) => a.seats || []), ...desksIn(L).map((q) => q.seat)];
177
+ const onSeat = (p) => seats.some((q) => d(p, q) < 4);
178
+ const spots = (L.waypoints || []).map((w) => ({ x: Math.round(w.x + n * MAIL.GAP), y: Math.round(w.y), from: w }))
179
+ .filter((p) => !onSeat(p.from) && !onSeat(p)).sort((p, q) => d(p.from, mid) - d(q.from, mid));
180
+ if (spots.length) return { x: spots[0].x, y: spots[0].y };
181
+ const inRoom = (p) => p.x >= 0 && p.y >= 0 && p.x <= L.width && p.y <= L.height;
182
+ for (let r = 0; r <= MAIL.SEARCH; r += 2 * MAIL.GAP) {
183
+ for (let k = 0, m = r ? 16 : 1; k < m; k++) {
184
+ const a = (2 * Math.PI * k) / m, p = { x: Math.round(mid.x + r * Math.cos(a) + n * MAIL.GAP), y: Math.round(mid.y + r * Math.sin(a)) };
185
+ if (inRoom(p) && !onSeat(p)) return p;
186
+ }
187
+ }
188
+ return null;
189
+ }
190
+ const desksIn = (L) => (L.rooms || []).flatMap((r) => r.desks || []).filter((d) => d.node);
191
+ /**
192
+ * The letters of one office. `sync(items, now, L, still)`: each new traffic item to someone drawn here from someone
193
+ * who is not becomes a letter (none with reduced motion, nor for a stale delegation; a zones office only).
194
+ * `frame(now, L, walkers)` → what to draw, { id, hue, kind, from, x, y, t } (t: its flight, 0 on the floor), picking
195
+ * up the letters whose person stands on them, and dropping those gone too long or whose person left. `targets(L)`:
196
+ * name → where to walk to pick one up (the first waiting for them; none while they are asleep). `prime(items)`: mark as
197
+ * seen what was in the air before this office was shown (a new office's or style's mail starts empty). `size()`.
198
+ */
199
+ export function createMail({ max = MAIL.MAX } = {}) {
200
+ const seen = new Set();
201
+ let letters = [];
202
+ const here = (L, name) => desksIn(L).find((d) => d.node.name === name) || null;
203
+ return {
204
+ sync(items, now, L, still = false) {
205
+ let started = 0;
206
+ for (const it of items || []) {
207
+ if (seen.has(it.id)) continue;
208
+ seen.add(it.id);
209
+ if (seen.size > 2000) seen.delete(seen.values().next().value);
210
+ if (still || it.phase === "stale" || !L || L.mode !== "zones" || !here(L, it.to) || here(L, it.from)) continue;
211
+ const drop = dropPoint(L, letters.filter((l) => l.to !== it.to).length % 4);
212
+ if (!drop) continue; // nowhere off the seats: not delivered here
213
+ const pts = [{ x: L.door.x, y: L.door.y }, drop];
214
+ letters.push({ id: it.id, from: it.from, to: it.to, kind: it.kind, hue: hue(it.kind), start: now, drop, pts,
215
+ ms: Math.min(FLIGHT.MAX_MS, Math.max(FLIGHT.MIN_MS, pathLength(pts) / FLIGHT.SPEED)) });
216
+ started++;
217
+ }
218
+ if (letters.length > max) letters = letters.slice(letters.length - max);
219
+ return started;
220
+ },
221
+ frame(now, L, walkers) {
222
+ const out = [];
223
+ letters = letters.filter((l) => {
224
+ if (!here(L, l.to)) return false;
225
+ const t = (now - l.start) / l.ms;
226
+ if (t < 1) { if (t >= 0) { const p = along(l.pts, t); out.push({ id: l.id, hue: l.hue, kind: l.kind, from: l.from, x: p.x, y: p.y, t }); } return true; }
227
+ if (now - l.start - l.ms > MAIL.MAX_WAIT) return false;
228
+ const w = walkers && walkers.at(l.to, now);
229
+ if (w && !w.walking && Math.hypot(w.x - l.drop.x, w.y - l.drop.y) < 4) return false; // picked up
230
+ out.push({ id: l.id, hue: l.hue, kind: l.kind, from: l.from, x: l.drop.x, y: l.drop.y, t: 0 });
231
+ return true;
232
+ });
233
+ return out;
234
+ },
235
+ targets(L, now) {
236
+ const out = new Map();
237
+ for (const l of letters) {
238
+ if (out.has(l.to) || now - l.start < l.ms) continue;
239
+ const d = here(L, l.to);
240
+ if (d && !MAIL.ASLEEP.has(d.node.state) && !d.night) out.set(l.to, l.drop); // asleep, or in bed for the night (#2568)
241
+ }
242
+ return out;
243
+ },
244
+ /** What is already in the traffic when this office's mail starts (#2564): seen, so it is not delivered again here. */
245
+ prime(items) { for (const it of items || []) seen.add(it.id); },
246
+ size: () => letters.length,
247
+ };
248
+ }
249
+
106
250
  /**
107
251
  * Where each character is. `sync(targets, now, graph, still)`: targets name → { x, y }; someone new is already there;
108
252
  * someone whose target moved walks from where they are now along the graph (routeVia) — or is there at once when
@@ -232,8 +376,13 @@ export function linesFor(state, lang, pack, defaults) {
232
376
  // ── Furniture ──
233
377
 
234
378
  /** The furniture active now: an item whose `activeWhen` names a state someone shown is in. */
235
- export function activeFurniture(pack, states) {
379
+ export function activeFurniture(pack, states, byName = new Map()) {
236
380
  const on = new Set();
237
- for (const f of (pack && pack.furniture) || []) if (f.activeWhen && f.activeWhen.some((s) => states.has(s))) on.add(f.id);
381
+ for (const f of (pack && pack.furniture) || []) {
382
+ if (!f.activeWhen) continue;
383
+ // activeFor (#2570): only those people's states count; else anyone's shown.
384
+ const hit = f.activeFor ? f.activeFor.some((n) => byName.has(n) && f.activeWhen.includes(byName.get(n))) : f.activeWhen.some((s) => states.has(s));
385
+ if (hit) on.add(f.id);
386
+ }
238
387
  return on;
239
388
  }
@@ -1,6 +1,6 @@
1
1
  // Fleet → Org chart → Office: the fleet as a small pixel office, the third view beside Canvas and List
2
2
  // (fleet-canvas.js). The same data as theirs: the structure from /ui/org (buildOrgView), the live state from the app
3
- // store, the traffic from fleet-traffic.js's store — nothing new is read but the offices people made (/ui/offices).
3
+ // store, the traffic from fleet-traffic.js's store — nothing new is read but the offices people made (/ui/offices) and what agents do in them (/ui/office-acts, #2578).
4
4
  //
5
5
  // - Two layouts, by skin. Desks (the built-in "pixel" skin, the default): each team is a room, each member a desk with a
6
6
  // name tag; General and the instances in no team share the lobby; a grid that wraps at the stage's width
@@ -62,8 +62,9 @@ import { hits, needle } from "./fleet-canvas-layout.js";
62
62
  import { AUTO_ID, LOBBY, autoLayout, backingScale, coatOf, createFlights, customLayout, deskAt, desksOf, filterOk, lampOf, layoutOk, lightOf, membersOf,
63
63
  officePose, officeRooms, signOf, workOn } from "./fleet-office-model.js";
64
64
  import { BUILT_IN_SKINS, CHAR_DEPTH, DEFAULT_PAL, bubbleBox, loadPackSkin, modeOf, skinFor, tagBox } from "./fleet-office-skin.js";
65
- import { CALLING, activeFurniture, bubbleAt, bubbleRank, createWalkers, linesFor, personAt, placeBubbles, thinBubbles, zonesLayout } from "./fleet-office-zones.js";
66
- import { createOfficeStore } from "./fleet-office-store.js";
65
+ import { CALLING, activeFurniture, bubbleAt, bubbleRank, NIGHT, WANDER, createMail, createWalkers, isNight, linesFor, npcKey, personAt, placeBubbles, thinBubbles, walkTargets, zonesLayout } from "./fleet-office-zones.js";
66
+ import { actsOk, createOfficeStore } from "./fleet-office-store.js";
67
+ import { POLL_MS } from "./fleet-traffic.js";
67
68
  import { OfficeEditor, officeForSave } from "./fleet-office-editor.js";
68
69
  import { LayoutEditor, handlesOf } from "./fleet-office-layout.js";
69
70
  import { applyOverrides } from "./fleet-office-pack.js";
@@ -83,7 +84,7 @@ register("orgo", {
83
84
  b_working: "On it.|Typing…|Almost there|In the zone|One more test", b_idle: "Tea time|Ready when you are|Stretching|Anything for me?",
84
85
  b_paused: "Zzz…|Back later", b_stopped: "Off duty|Zzz…", b_needs: "Need you!|A question for you|Over here!",
85
86
  b_crashed: "Ouch, crashed|Help!", b_stuck: "Hmm, stuck?|Still thinking…",
86
- hideAsleep: "Hide paused and stopped", layoutIgnored: "Its saved layout does not fit this style: drawn as the style's own.",
87
+ hideAsleep: "Hide paused and stopped", nightOn: "Bedtime 0–6: the idle sleep", layoutIgnored: "Its saved layout does not fit this style: drawn as the style's own.",
87
88
  fit: "Fit the office", overview: "Overview: click to move there", fullscreen: "Fullscreen", exitFullscreen: "Exit fullscreen",
88
89
  fill: "Fill", fillTitle: "Fill the panel (the scene's overflow is cropped; drag to see it)", zoomLevel: "Zoom {0}",
89
90
  allOffices: "All offices", labelAll: "All offices: each office is a tile, drawn in its own style. Drag to pan; the wheel, a pinch, + and − zoom, 0 fits, F is fullscreen. Double-click a tile to open that office.",
@@ -99,7 +100,7 @@ register("orgo", {
99
100
  b_working: "處理中|打字中…|快好了|專心中|再測一次", b_idle: "喝茶中|隨時可以開工|伸個懶腰|有事找我嗎?",
100
101
  b_paused: "Zzz…|晚點回來", b_stopped: "下班了|Zzz…", b_needs: "需要你!|有個問題|在這裡!",
101
102
  b_crashed: "糟糕,當掉了|救命!", b_stuck: "嗯,卡住了?|還在想…",
102
- hideAsleep: "隱藏暫停與停止的成員", layoutIgnored: "它儲存的配置不適用這個風格:以風格本身的配置顯示。",
103
+ hideAsleep: "隱藏暫停與停止的成員", nightOn: "0–6 點就寢:閒著的人去睡", layoutIgnored: "它儲存的配置不適用這個風格:以風格本身的配置顯示。",
103
104
  fit: "顯示整個辦公室", overview: "總覽:點一下移到那裡", fullscreen: "全螢幕", exitFullscreen: "結束全螢幕",
104
105
  fill: "填滿", fillTitle: "填滿整個面板(超出的部分會被裁切,拖曳即可看到)", zoomLevel: "縮放 {0}",
105
106
  allOffices: "所有辦公室", labelAll: "所有辦公室:每間辦公室是一格,以它自己的風格顯示。拖曳平移;滾輪、雙指、+ 和 − 縮放,0 顯示全部,F 切換全螢幕。雙擊一格即可開啟那間辦公室。",
@@ -180,7 +181,8 @@ function looksOf(L, work, q, mode, zones) {
180
181
  const n = d.node;
181
182
  if (!n) return [d.key, { name: "", label: "", state: "missing", pose: "away", light: null, lamp: false, sign: null, coat: "", work: 0, dim: !!q, match: false, free: zones }];
182
183
  const match = !!q && hits(n, q);
183
- return [d.key, { name: n.name, label: nodeText(n).primary, state: n.state, pose: officePose(n.state), light: lightOf(n.state), lamp: lampOf(n.state),
184
+ // At night (#2568) the idle are drawn asleep (their state stays idle: the tooltip and the list say so).
185
+ return [d.key, { name: n.name, label: nodeText(n).primary, state: n.state, pose: officePose(d.night ? "paused" : n.state), night: !!d.night, light: lightOf(n.state), lamp: lampOf(n.state),
184
186
  sign: zones ? null : signOf(n.state), coat: coatOf(n.i, n.i ? identityOf(n.i) : null, mode), work: work.get(n.name) || 0, dim: !!q && !match, match, free: zones }];
185
187
  }));
186
188
  }
@@ -192,17 +194,78 @@ function withOverrides(baseSkin, zones, office) {
192
194
  return a.ok ? { skin: { ...baseSkin, pack: a.pack }, ignored: false } : { skin: baseSkin, ignored: true };
193
195
  }
194
196
  /** A stored office's scene (or the lobby tile's): seats in its style's areas, its own seats, or one room wrapping at `deskW`. */
195
- function officeScene(office, nodes, skin, zones, deskW, prevSeats) {
196
- if (zones) return zonesLayout(skin.pack, nodes, prevSeats);
197
+ function officeScene(office, nodes, skin, zones, deskW, prevSeats, night = false) {
198
+ if (zones) return zonesLayout(skin.pack, nodes, prevSeats, { night });
197
199
  const lay = office.layout || layoutOk(skin.layout);
198
200
  return lay ? customLayout({ ...office, layout: lay }, nodes) : autoLayout([{ key: office.id, name: office.name, lobby: !!office.lobby, nodes }], { width: deskW });
199
201
  }
200
202
  /** What drawPart needs of a scene; its layers are painted on first draw (and again at another resolution). */
201
203
  function partOf(L, looks, skin, zones, walkers, flights) {
202
204
  const all = L.rooms.flatMap((r) => r.desks);
203
- const shown = new Set([...looks.values()].map((l) => l.state));
205
+ const shown = new Set([...looks.values()].map((l) => l.state)), byName = new Map([...looks.values()].filter((l) => l.name).map((l) => [l.name, l.state]));
204
206
  return { L, looks, skin, zones, walkers, flights, all, desks: all.filter((d) => d.node), layers: null,
205
- active: zones ? activeFurniture(skin.pack, shown) : new Set(), lines: bubbleDefaults(), lang: lang() };
207
+ active: zones ? activeFurniture(skin.pack, shown, byName) : new Set(), lines: bubbleDefaults(), lang: lang() };
208
+ }
209
+ /** At bedtime (#2568): each item with a `night` drawn as it — its strip playing, at its night place (its own when none). */
210
+ export const nightFurniture = (list) => list.map((f) => (f.night ? { ...f, sprite: f.night.sprite, activeSprite: null, x: f.night.x ?? f.x, y: f.night.y ?? f.y, asleep: true } : f));
211
+ /** #2596: each NPC with a `walk` where its walker is now (`at(key)`): moved there — its tag and depth with it — and, while
212
+ * it walks, `walking` and its `heading` (its walk strip: walkStrip); asleep, or with no walker (not strolling), where it is. */
213
+ export const strollFurniture = (list, at) => list.map((f) => {
214
+ const w = f.walk && !f.asleep ? at(npcKey(f)) : null;
215
+ return w ? { ...f, x: w.x, y: w.y, walking: !!w.walking, heading: w.heading || null } : f;
216
+ });
217
+ /** Where a seated person's bubble points: over the head of the picture drawn — a pack's figure can be taller than the
218
+ * built-in one (#2558: a 134 px figure had its face under the bubble); never lower than the built-in spot. */
219
+ export function speakerAt(sk, look, at) {
220
+ const top = sk.charBox ? sk.charBox(look, at).y + 2 : Infinity;
221
+ return { x: at.x + 10, y: Math.min(at.y - 50, top) };
222
+ }
223
+ /** The pose an area's seats are for: the first state (asleep, working, calling, idle) the pack sends there. */
224
+ const GHOST_POSE = [["paused", "paused"], ["stopped", "paused"], ["working", "working"], ["needs", "needs"], ["crashed", "needs"], ["stuck", "needs"], ["idle", "idle"]];
225
+ /** How faint Edit layout's figures are: seen through, yet readable over a busy picture (a dimmed person is 0.35). */
226
+ const GHOST_ALPHA = 0.6;
227
+ /** Edit layout's faint figures: one on each seat no one is on, { x, y, look } (the pack's default set; drawn at GHOST_ALPHA). */
228
+ export function ghostsOf(pack, people = []) {
229
+ const taken = (s) => people.some((q) => Math.abs(q.at.x - s.x) < 2 && Math.abs(q.at.y - s.y) < 2);
230
+ const out = [];
231
+ for (const [name, a] of Object.entries(pack.areas)) {
232
+ const hit = GHOST_POSE.find(([st]) => pack.stateZones && pack.stateZones[st] === name);
233
+ const pose = hit ? hit[1] : "idle";
234
+ for (const s of a.seats) if (!taken(s)) out.push({ x: s.x, y: s.y, look: { key: `ghost:${name}`, name: "", label: "", state: pose, pose, coat: "#8a8f99", free: true } });
235
+ }
236
+ return out;
237
+ }
238
+ /** All offices: the tile `name` is drawn in ({ tile, skin }), or null. */
239
+ const senderTile = (parts, name) => (name ? parts.find((p) => desksOf(p.L).some((d) => d.node.name === name)) || null : null);
240
+ /**
241
+ * Whether an agent's act (office_set, #2578) holds now: not past its end; its person not called (a call wins) nor
242
+ * asleep; and no new work begun since — `seen` (kept by the page) remembers each act whose person was seen not working,
243
+ * so working again after that (or after acting while idle) is new work, and the act gives way. The turn it acted in
244
+ * (working at the time) keeps it.
245
+ */
246
+ export function actLive(a, state, at = Date.now(), seen = null) {
247
+ if (!a || at >= a.until || CALLING.has(state) || ASLEEP.has(state)) return false;
248
+ if (!seen) return true;
249
+ const k = `${a.instance}@${a.since ?? a.until}`;
250
+ if (state !== "working") { seen.add(k); return true; }
251
+ return !seen.has(k) && (!a.state || a.state === "working");
252
+ }
253
+ /** How far beside the person an agent going to them stands. */
254
+ const ACT_BESIDE = 48;
255
+ /** Where an agent's act sends it at `t`: its point, beside the person it goes to (where they are now), or null. */
256
+ export function actTarget(a, L, walkers, t) {
257
+ if (a.x !== undefined) return { x: a.x, y: a.y };
258
+ if (!a.person) return null;
259
+ const d = desksOf(L).find((x) => x.node.name === a.person), there = (walkers && walkers.at(a.person, t)) || (d && d.seat);
260
+ return there ? { x: Math.round(Math.min(L.width, there.x + ACT_BESIDE)), y: Math.round(there.y) } : null;
261
+ }
262
+ /**
263
+ * A look in its agent's own pose (office_set, #2578) while its act holds, else as it is. In bed for the night (#2568,
264
+ * `look.night`) it is asleep, as for mail (#2569): bedtime wins over an act.
265
+ */
266
+ function actPosed(p, look) {
267
+ const act = look && p.acts && p.acts.get(look.name);
268
+ return act && act.pose && actLive(act, look.night ? "paused" : look.state, Date.now(), p.actSeen) ? { ...look, pose: officePose(act.pose) } : look;
206
269
  }
207
270
  /** The back and front layers: pictures of the scene at `ls` layer px per logical unit (`tagsOff`: no name tags but the Find box's matches). */
208
271
  function paintLayers(p, env, ls, tagsOff = false) {
@@ -248,6 +311,8 @@ function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false, defer
248
311
  T();
249
312
  const speakers = [], tags = [];
250
313
  const up = still ? [] : p.flights.frame(ms, p.L);
314
+ // Mail from another office (#2564): flying in at the door, or on the floor until its person picks it up.
315
+ const post = still || !p.mail || !p.zones ? [] : p.mail.frame(ms > 0 ? ms : now(), p.L, p.walkers);
251
316
  let walking = false;
252
317
  if (p.zones) {
253
318
  // Furniture under the people; the people (nearest last) with the furniture whose depth is "y" among them, by base
@@ -259,18 +324,26 @@ function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false, defer
259
324
  if (at.walking) walking = true;
260
325
  return { d, at, look: p.looks.get(d.key) };
261
326
  }).sort((x, y) => x.at.y - y.at.y);
262
- const furniture = sk.pack.furniture;
263
- const band = [...furniture.filter((f) => f.depth === "y").map((f) => ({ y: f.y, k: 0, f })), ...people.map((q) => ({ y: q.at.y, k: 1, p: q }))]
327
+ const furniture = strollFurniture(p.night ? nightFurniture(sk.pack.furniture) : sk.pack.furniture, (k) => p.walkers.at(k, t));
328
+ if (furniture.some((f) => f.walking)) walking = true;
329
+ const band = [...furniture.filter((f) => f.depth === "y").map((f) => ({ y: f.y, k: 0, f })), ...people.map((q) => ({ y: q.at.y, k: 1, p: q })),
330
+ ...(p.ghosts ? ghostsOf(sk.pack, people).map((g) => ({ y: g.y, k: 1, g })) : [])]
264
331
  .sort((x, y) => x.y - y.y || x.k - y.k);
265
- for (const f of furniture) if (f.depth !== "y" && f.depth < CHAR_DEPTH) sk.drawFurniture(ctx, f, p.active.has(f.id), ms, env);
266
- for (const { f, p: q } of band) {
267
- if (f) { sk.drawFurniture(ctx, f, p.active.has(f.id), ms, env); continue; }
332
+ for (const f of furniture) if (f.depth !== "y" && f.depth < CHAR_DEPTH) sk.drawFurniture(ctx, f, f.asleep || p.active.has(f.id), ms, env);
333
+ for (const it of post) if (it.t === 0) sk.drawProp(ctx, it, ms, env); // a letter on the floor: under the people
334
+ for (const { f, p: q, g } of band) {
335
+ if (f) { sk.drawFurniture(ctx, f, f.asleep || p.active.has(f.id), ms, env); continue; }
336
+ if (g) { ctx.save(); ctx.globalAlpha = GHOST_ALPHA; sk.drawCharacter(ctx, { x: g.x - 44, y: g.y - 53, w: 88, h: 53, seat: g }, g.look, 0, env); ctx.restore(); continue; }
268
337
  const box = { ...q.d, x: q.at.x - 44, y: q.at.y - 53, w: 88, h: 53, seat: { x: q.at.x, y: q.at.y } };
269
- sk.drawCharacter(ctx, box, q.at.walking ? { ...q.look, pose: "walk", heading: q.at.heading } : q.look, ms, env);
270
- if (!q.at.walking) speakers.push({ look: q.look, x: q.at.x + 10, y: q.at.y - 50, area: q.d.area });
338
+ // An agent's own pose (office_set, #2578) while its act lasts and nothing calls it; walking is walking.
339
+ const posed = actPosed(p, q.look);
340
+ const look = q.at.walking ? { ...q.look, pose: "walk", heading: q.at.heading } : posed;
341
+ sk.drawCharacter(ctx, box, look, ms, env);
342
+ if (!q.at.walking) speakers.push({ look: q.look, ...speakerAt(sk, look, q.at), area: q.d.area });
271
343
  }
272
- for (const f of furniture) if (f.depth !== "y" && f.depth >= CHAR_DEPTH) sk.drawFurniture(ctx, f, p.active.has(f.id), ms, env);
344
+ for (const f of furniture) if (f.depth !== "y" && f.depth >= CHAR_DEPTH) sk.drawFurniture(ctx, f, f.asleep || p.active.has(f.id), ms, env);
273
345
  for (const it of up) sk.drawProp(ctx, it, ms, env);
346
+ for (const it of post) if (it.t > 0) sk.drawProp(ctx, it, ms, env);
274
347
  if (sk.drawForeground) sk.drawForeground(ctx, p.L, env);
275
348
  // Who found no spot in an area: a count in its corner (never over its name).
276
349
  for (const [ar, n] of p.L.crowd || []) if (p.L.areas[ar] && sk.drawCrowd) sk.drawCrowd(ctx, p.L.areas[ar], n, env);
@@ -281,8 +354,10 @@ function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false, defer
281
354
  sk.drawTag(ctx, q.at.x, q.at.y, q.look, env);
282
355
  tags.push(tagBox(ctx, q.at.x, q.at.y, q.look.label));
283
356
  }
357
+ // A furniture item's own name (#2574: an NPC drawn as furniture), tagged like a person; none on a tile too small to read.
358
+ if (!tagsOff) for (const f of furniture) if (f.label) { sk.drawTag(ctx, f.x, f.y, { label: f.label }, env); tags.push(tagBox(ctx, f.x, f.y, f.label)); }
284
359
  } else {
285
- for (const d of p.desks) { const look = p.looks.get(d.key); sk.drawCharacter(ctx, d, look, ms, env); speakers.push({ look, x: d.x + 58, y: d.y + 12 }); }
360
+ for (const d of p.desks) { const look = p.looks.get(d.key); sk.drawCharacter(ctx, d, actPosed(p, look), ms, env); speakers.push({ look, x: d.x + 58, y: d.y + 12 }); }
286
361
  blit(p.layers.front);
287
362
  // Tags left out of the layer: the hovered desk's front again, with its tag.
288
363
  const hd = tagsOff && hot ? p.desks.find((d) => d.key === hot) : null;
@@ -294,8 +369,11 @@ function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false, defer
294
369
  const bubbles = [];
295
370
  for (const sp of speakers) {
296
371
  if (sp.look.dim || sp.look.pose === "away") continue;
297
- const b = bubbleAt(sp.look.name, sp.look.state, ms, linesFor(sp.look.state, p.lang, sk.pack, p.lines));
298
- if (b) bubbles.push({ key: sp.look.name, area: sp.area, state: sp.look.state, calling: CALLING.has(sp.look.state), rank: bubbleRank(sp.look.state), shown: b.shown,
372
+ const says = sp.look.night ? "paused" : sp.look.state; // asleep at night: its "Zzz…" lines
373
+ // What an agent says itself (office_set, #2578): whole, kept over the area's thinning like a call.
374
+ const act = p.acts && p.acts.get(sp.look.name), own = actLive(act, says, Date.now(), p.actSeen) && act.say ? act.say : null;
375
+ const b = own ? { text: own, shown: [...own].length } : bubbleAt(sp.look.name, says, ms, linesFor(says, p.lang, sk.pack, p.lines));
376
+ if (b) bubbles.push({ key: sp.look.name, area: sp.area, state: sp.look.state, calling: CALLING.has(sp.look.state), rank: own ? 0 : bubbleRank(sp.look.state), shown: b.shown,
299
377
  box: bubbleBox(ctx, sp.x, sp.y, b.text), tail: { x: sp.x, y: sp.y }, sp, b });
300
378
  }
301
379
  // A crowded area speaks less (thinBubbles: calls, then work, a few at most); no bubble covers a name tag. `defer`
@@ -303,7 +381,7 @@ function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false, defer
303
381
  const placed = placeBubbles(thinBubbles(bubbles, p.zones ? p.L.over : null), 22, tags);
304
382
  const say = () => { T(); for (const { sp, b, dy } of placed) sk.drawBubble(ctx, sp.x, sp.y, [...b.text].slice(0, b.shown).join(""), env, -dy); };
305
383
  if (defer) defer.push(say); else say();
306
- return walking || up.length > 0 || p.flights.size() > 0;
384
+ return walking || up.length > 0 || p.flights.size() > 0 || post.some((it) => it.t > 0);
307
385
  }
308
386
  /**
309
387
  * The person at (x, y) in a scene's units, as it draws them: on zones where they walk, in the box the skin draws them
@@ -356,6 +434,11 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
356
434
  const [filter, setFilter] = useState(() => store.filter());
357
435
  const [style, setStyle] = useState(() => store.skin());
358
436
  const [hideAsleep, setHideAsleep] = useState(() => store.hideAsleep());
437
+ // Bedtime (#2568), when it is on here: the idle sleep from NIGHT.FROM to NIGHT.TO on this device's clock (read every NIGHT.TICK).
438
+ const [nightOn, setNightOn] = useState(() => store.nightOn());
439
+ const [nightNow, setNightNow] = useState(() => isNight());
440
+ useEffect(() => { if (!nightOn) return undefined; setNightNow(isNight()); const h = setInterval(() => setNightNow(isNight()), NIGHT.TICK); return () => clearInterval(h); }, [nightOn]);
441
+ const night = nightOn && nightNow;
359
442
  const [lobbyOn, setLobbyOn] = useState(() => store.lobbyTile()); // #2554: All offices' lobby tile
360
443
  const [packs, setPacks] = useState([]); // the skin packs the host keeps: [{ id, name, sha }]
361
444
  const [packSkins, setPackSkins] = useState(() => new Map()); // id@sha → a loaded skin, or null (not a pack)
@@ -406,6 +489,24 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
406
489
  const off = sub ? lease.hold(sub("offices", () => { void read(); })) : null;
407
490
  return () => { if (off) off(); };
408
491
  }, [lease, store]);
492
+ // #2578: what agents do here by their own hand (office_set) — on opening, then as the stream brings it.
493
+ // A read a newer one or the stream overtook is dropped (it is older: review — an opening read answered after the one
494
+ // the stream's return began brought a cleared act back); read again when the stream comes back, and — with no live
495
+ // stream (the public link, or a stream polling) — every POLL_MS while the page is shown.
496
+ const [acts, setActs] = useState([]);
497
+ const actSeen = useRef(new Set());
498
+ useEffect(() => {
499
+ if (!lease) return undefined;
500
+ let heard = 0; // reads begun and events heard: the newest wins
501
+ const read = async () => { const at = ++heard, d = await store.acts(); if (lease.current() && d && at === heard) setActs(d); };
502
+ void read();
503
+ const sub = appStore.get().streamOn;
504
+ const polled = () => globalThis.document?.body?.dataset?.webTransport === "poll" || appStore.get().connection === "polling";
505
+ const offs = sub ? [lease.hold(sub("office_acts", (d) => { heard++; if (d && Array.isArray(d.acts)) setActs(actsOk(d.acts, d.now)); else void read(); })),
506
+ lease.hold(sub("connection", (c) => { if (c === "live") void read(); }))] : [];
507
+ const h = lease.interval(() => { if (polled() && !globalThis.document?.hidden) void read(); }, POLL_MS);
508
+ return () => { for (const off of offs) off(); lease.clear(h); };
509
+ }, [lease, store]);
409
510
  // The skin packs: on opening, and again whenever the stream says one was uploaded or deleted.
410
511
  useEffect(() => {
411
512
  if (!lease) return undefined;
@@ -489,17 +590,20 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
489
590
  return withOverrides(baseSkin, zones, custom);
490
591
  }, [baseSkin, zones, custom, layout, layoutStale]);
491
592
  const skin = placed.skin;
593
+ // While Edit layout is open: a faint figure on every seat, in the pose its area is for (#2558), so a seat is placed by
594
+ // what will stand, sit or lie there, not by a dot.
595
+ const ghosts = zones && !!layout && !layoutStale;
492
596
  const keep = (n) => !(hideAsleep && ASLEEP.has(n.state));
493
597
  const single = useMemo(() => {
494
598
  if (overview) return { rooms: [], width: 1, height: 1 }; // All offices: the tiles below
495
599
  const all = custom ? membersOf(custom, view) : [...new Map(officeRooms(view, filter).flatMap((r) => r.nodes).map((n) => [n.name, n])).values()];
496
600
  const nodes = all.filter(keep);
497
- if (zones) { const z = zonesLayout(skin.pack, nodes, seatsRef.current); seatsRef.current = z.seats; return z; }
601
+ if (zones) { const z = zonesLayout(skin.pack, nodes, seatsRef.current, { night }); seatsRef.current = z.seats; return z; }
498
602
  // The desks wrap at the stage's width at the scale they open on (desksK: a pixel level near one CSS px a unit).
499
603
  const deskW = Math.max(240, Math.floor(width / desksK(ratio)));
500
604
  if (!custom) return autoLayout(officeRooms(view, filter).map((r) => (hideAsleep ? { ...r, nodes: r.nodes.filter(keep) } : r)), { width: deskW });
501
605
  return officeScene(custom, nodes, skin, false, deskW);
502
- }, [overview, view, filter, custom, width, ratio, skin, hideAsleep]);
606
+ }, [overview, view, filter, custom, width, ratio, skin, hideAsleep, night]);
503
607
  // ── #2554: All offices' tiles — each stored office (then the lobby: whoever is in no office) laid out as its own view
504
608
  // lays it out, in its own style (desks wrap at a cell's width), with its own seats, walkers and flights ──
505
609
  const lobbyAll = useMemo(() => (overview ? lobbyNodes(stored, view) : []), [overview, stored, view]);
@@ -516,24 +620,45 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
516
620
  const sk = withOverrides(base, z, sp.lobby ? null : sp.office).skin;
517
621
  const seatKey = `${sp.key}@${pk || base.id}`;
518
622
  // A desks office is as large as its rooms (cropped: no margins, no corridors outside them).
519
- const TL0 = officeScene(sp.office, sp.nodes.filter(keep), sk, z, OV.CELL_W, tileSeats.current.get(seatKey)), TL = z ? TL0 : cropToRooms(TL0);
623
+ const TL0 = officeScene(sp.office, sp.nodes.filter(keep), sk, z, OV.CELL_W, tileSeats.current.get(seatKey), night), TL = z ? TL0 : cropToRooms(TL0);
520
624
  if (z) tileSeats.current.set(seatKey, TL0.seats);
521
625
  return { key: sp.key, name: sp.name, lobby: sp.lobby, count: sp.nodes.length, skin: sk, zones: z, L: TL, prefix: `${sp.key}\u0001`,
522
626
  walkers: mine(tileWalkers, sp.key, createWalkers), flights: mine(tileFlights, sp.key, createFlights) };
523
627
  });
524
- }, [overview, stored, view, packs, packSkins, hideAsleep, lobbyOn, lobbyAll, style]);
628
+ }, [overview, stored, view, packs, packSkins, hideAsleep, lobbyOn, lobbyAll, style, night]);
525
629
  const arranged = useMemo(() => tileLayout(tiles.map((tl) => ({ key: tl.key, w: tl.L.width, h: tl.L.height })), size), [tiles, size.w, size.h]);
526
630
  const tileLooks = useMemo(() => { const work = workOn(traffic); return tiles.map((tl) => looksOf(tl.L, work, q, mode, tl.zones)); }, [tiles, traffic, q, mode, themeRev]);
527
631
  // The scene the view pans and zooms: the office's, or the overview of every tile.
528
632
  const L = useMemo(() => (overview ? { mode: "overview", rooms: [], width: arranged.width, height: arranged.height } : single), [overview, single, arranged]);
529
633
  const looks = useMemo(() => looksOf(L, workOn(traffic), q, mode, zones), [L, traffic, q, mode, themeRev]);
530
- useEffect(() => { flights.sync(traffic || [], now(), still); }, [traffic]);
531
- // Zones: everyone's seat is where they walk to.
634
+ // #2564: what comes in from other offices — one office's and style's own: a letter's door and drop are that layout's,
635
+ // so another office (or style) starts with none (one shown meanwhile is not carried over to a different room).
636
+ // #2578: the acts of the people in this stored office (by name); none elsewhere. On a desks style (review: accepted
637
+ // acts drew nothing there) the line and the pose show; nobody walks (the host refuses go there).
638
+ const actsHere = useMemo(() => new Map(custom && !overview ? acts.filter((a) => a.office === custom.id).map((a) => [a.instance, a]) : []), [acts, custom, overview]);
639
+ const mailKey = `${custom ? custom.id : AUTO_ID}@${baseSkin.id}`;
640
+ const mail = useMemo(() => { const m = createMail(); m.prime(traffic || []); return m; }, [mailKey]);
641
+ useEffect(() => { flights.sync(traffic || [], now(), still); if (zones && !overview && mail.sync(traffic || [], now(), L, still)) loop.current?.wake(); }, [traffic, mail]);
642
+ // Zones: everyone's seat is where they walk to — an idle person now and then strolls near it and back (#2560; not
643
+ // with reduced motion, nor while Edit layout is open: the seats are being placed); someone with mail from another
644
+ // office walks to it first (#2564). An NPC drawn as furniture with a `walk` strolls near its spot alike (#2596).
645
+ const wander = zones && !overview && !still && !layout;
532
646
  useEffect(() => {
533
- if (!zones || overview) return;
534
- walkers.sync(new Map(desksOf(L).map((d) => [d.node.name, d.seat])), now(), L, still);
535
- loop.current?.wake();
536
- }, [L]);
647
+ if (!zones || overview) return undefined;
648
+ const go = () => {
649
+ const t = now(), to = walkTargets(desksOf(L), L, t, wander, skin.pack.furniture, night);
650
+ // An act's walk; not someone in bed for the night (#2568: bedtime wins, as for mail).
651
+ for (const d of desksOf(L)) { const a = actsHere.get(d.node.name), at = !d.night && actLive(a, d.node.state, Date.now(), actSeen.current) ? actTarget(a, L, walkers, t) : null; if (at) to.set(d.node.name, at); }
652
+ for (const [name, at] of mail.targets(L, t)) to.set(name, at);
653
+ walkers.sync(to, t, L, still);
654
+ // Reduced motion has no loop: drawn again here, or an act's walk (set after the scene was drawn) never shows.
655
+ if (still) { if (scene.current && !globalThis.document?.hidden) draw(0); } else loop.current?.wake();
656
+ };
657
+ go();
658
+ if (still) return undefined;
659
+ const h = setInterval(go, WANDER.TICK);
660
+ return () => clearInterval(h);
661
+ }, [L, wander, mail, skin, night, actsHere]);
537
662
  // …and in each tile on a zones style; the traffic within a tile is its own, between tiles the overview's.
538
663
  useEffect(() => {
539
664
  for (const tl of tiles) if (tl.zones) tl.walkers.sync(new Map(desksOf(tl.L).map((d) => [d.node.name, d.seat])), now(), tl.L, still);
@@ -647,7 +772,7 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
647
772
  const fitted0 = zones || overview; // All offices opens whole, as a zones scene
648
773
  return { v: overview ? fitL(fitMode) : openingView(L.width, L.height, size, ratio, fitted0, fitMode), fit: fitted0 };
649
774
  };
650
- // An office (or style) opened: its kept view, else its opening (zones fitted; desks from the top left). A resize fits
775
+ // An office (or style) opened: its kept view, else its opening (zones fitted; desks from the top left, or centred where smaller). A resize fits
651
776
  // a fitted view again (and opens an untouched one again); a view a person moved keeps the same point in the middle.
652
777
  // Either way it is then kept on the scene (clampPan): a kept view from a bigger panel cannot leave it off-screen.
653
778
  const opened = useRef(null), sized = useRef(null);
@@ -776,13 +901,13 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
776
901
  const base = { ctx, ratio, BW, BH, matte, env, dirty: true };
777
902
  if (overview) {
778
903
  // Each tile a scene of its own, where the overview puts it (arranged.tiles, in the same order).
779
- const parts = tiles.map((tl, i) => ({ ...partOf(tl.L, tileLooks[i], tl.skin, tl.zones, tl.walkers, tl.flights), key: tl.key, prefix: tl.prefix, tile: arranged.tiles[i] }));
904
+ const parts = tiles.map((tl, i) => ({ ...partOf(tl.L, tileLooks[i], tl.skin, tl.zones, tl.walkers, tl.flights), night, key: tl.key, prefix: tl.prefix, tile: arranged.tiles[i] }));
780
905
  scene.current = { ...base, overview: true, parts, lay: arranged };
781
906
  } else {
782
- scene.current = { ...base, overview: false, part: partOf(L, looks, skin, zones, walkers, flights) };
907
+ scene.current = { ...base, overview: false, part: { ...partOf(L, looks, skin, zones, walkers, flights), ghosts, night, mail, acts: actsHere, actSeen: actSeen.current } };
783
908
  }
784
909
  if (still) draw(0); else loop.current?.wake();
785
- }, [L, looks, skin, pal, size.w, size.h, ratio, matte, overview, tiles, tileLooks, arranged]);
910
+ }, [L, looks, skin, pal, size.w, size.h, ratio, matte, overview, tiles, tileLooks, arranged, ghosts, night, mail, actsHere]);
786
911
 
787
912
  /** One frame at `ms` (0: still); whether anything is moving (in the air, or someone walking). */
788
913
  function draw(ms) {
@@ -815,10 +940,12 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
815
940
  }
816
941
  for (const say of said) say();
817
942
  const up = still ? [] : cross.frame(ms, s.parts);
818
- if (up.length) {
819
- ctx.setTransform(a, 0, 0, a, ox, oy);
820
- const prop = skinFor("pixel").drawProp;
821
- for (const it of up) prop(ctx, it, ms, env);
943
+ // Each drawn by its sender's tile's skin (#2562: their own prop, else that pack's for its kind), at that tile's scale;
944
+ // a sender in no tile drawn: the built-in item, in overview units.
945
+ for (const it of up) {
946
+ const from = senderTile(s.parts, it.from);
947
+ if (from) { ctx.setTransform(a * from.tile.s, 0, 0, a * from.tile.s, ox, oy); from.skin.drawProp(ctx, { ...it, x: it.x / from.tile.s, y: it.y / from.tile.s }, ms, env); }
948
+ else { ctx.setTransform(a, 0, 0, a, ox, oy); skinFor("pixel").drawProp(ctx, it, ms, env); }
822
949
  }
823
950
  return moving || up.length > 0 || cross.size() > 0;
824
951
  }
@@ -1012,6 +1139,7 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
1012
1139
  </select></label>
1013
1140
  <${People} nodes=${everyone} pinned=${f.pinned} onChange=${(pinned) => setF({ ...f, pinned })} />` : null}
1014
1141
  <label class="check oo-hide-asleep"><input type="checkbox" checked=${hideAsleep} onChange=${() => { const v = !hideAsleep; store.saveHideAsleep(v); setHideAsleep(v); setTip(null); }} /><span>${to("hideAsleep")}</span></label>
1142
+ ${zones || overview ? html`<label class="check oo-night"><input type="checkbox" checked=${nightOn} onChange=${() => { const v = !nightOn; store.saveNightOn(v); setNightOn(v); }} /><span>${to("nightOn")}</span></label>` : null}
1015
1143
  ${overview ? html`<label class="check oo-lobby-tile" title=${lobbyAll.length ? undefined : to("lobbyEveryone")}><input type="checkbox" checked=${lobbyOn && lobbyAll.length > 0}
1016
1144
  disabled=${!lobbyAll.length} onChange=${() => { const v = !lobbyOn; store.saveLobbyTile(v); setLobbyOn(v); setTip(null); }} /><span>${to("lobbyTile")}</span></label>
1017
1145
  ${lobbyAll.length ? null : html`<span class="muted oo-note oo-lobby-none">${to("lobbyEveryone")}</span>`}` : null}
@@ -1035,7 +1163,7 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
1035
1163
  : html`<button key=${tl.key} ref=${at} type="button" class="oo-tile-label" data-office=${tl.key} title=${to("openOffice", tl.name)} aria-label=${to("openOffice", tl.name)}
1036
1164
  onClick=${() => pick(tl.key)}>${text}</button>`;
1037
1165
  }) : null}
1038
- ${layout ? html`<${LayoutEditor} key=${layout.id} base=${layout.base} start=${layout.start} place=${placeOverlay}
1166
+ ${layout ? html`<${LayoutEditor} key=${layout.id} base=${layout.base} start=${layout.start} place=${placeOverlay} dock=${!fs}
1039
1167
  stale=${layoutStale ? t("orgl.stale") : ""} onChange=${onDraft} onSave=${saveLayout} onClose=${closeLayout} />` : null}
1040
1168
  ${tipText ? html`<div ref=${tipRef} class="oo-tip" role="tooltip">
1041
1169
  <strong>${tipText.primary}</strong>${tipText.secondary ? html` <span class="muted mono">${tipText.secondary}</span>` : null}
@@ -44,6 +44,11 @@ const writeKey = (k, v) => { try { if (v) localStorage.setItem(k, v); else local
44
44
  export const chatCardPrefs = createStore({ open: readKey(CHAT_CARD_KEY, "open"), rowHidden: readKey(CHAT_CARD_ROW_KEY, "hidden") });
45
45
  export const setChatCardOpen = (open) => { writeKey(CHAT_CARD_KEY, open ? "open" : null); chatCardPrefs.set((s) => ({ ...s, open })); };
46
46
  export const setChatCardRowHidden = (rowHidden) => { writeKey(CHAT_CARD_ROW_KEY, rowHidden ? "hidden" : null); chatCardPrefs.set((s) => ({ ...s, rowHidden })); };
47
+ // Full width (the header's ⋯ menu): this browser's choice, like code wrap. The thread and the composer take the panel's
48
+ // width; a person's bubble keeps its cap (app.css). A phone's column is the screen already: no item there.
49
+ const CHAT_WIDE_KEY = "agend_chat_full_width";
50
+ export const chatWide = createStore({ on: readKey(CHAT_WIDE_KEY, "on") });
51
+ export const setChatWide = (on) => { writeKey(CHAT_WIDE_KEY, on ? "on" : null); chatWide.set({ on }); };
47
52
  function ChatCard({ name, card }) {
48
53
  const prefs = useStore(chatCardPrefs);
49
54
  if (!card || !prefs.open) return null;
@@ -260,6 +265,7 @@ function ChatView({ name, inst, lease, navKey, exec, awaiting }) {
260
265
  const publishCanvasPlayback = useCallback(value => setCanvasPlayback(previous =>
261
266
  !previous && !value.playing || previous?.name === value.name && previous.playing === value.playing ? previous : value), []);
262
267
  const [wrap, setWrap] = useState(codeWrap);
268
+ const wide = useStore(chatWide).on;
263
269
  const view = useRef(null), split = useRef(null), thread = useRef(null), bg = useRef(null), effectSurface = useRef(null);
264
270
  useLayoutEffect(() => { applyTheme(view.current, theme, mode); }, [themeKey, mode]);
265
271
  useLayoutEffect(() => {
@@ -321,6 +327,7 @@ function ChatView({ name, inst, lease, navKey, exec, awaiting }) {
321
327
  ...themeMenuItems(name),
322
328
  // #2117: its ambient — why it is not showing here (with the way to change that), and a person's Clear.
323
329
  ...ambientMenuItems(name, { publicLink: document.body.dataset.publicLink === "1", clear: clearAmbient }),
330
+ narrowScreen() ? null : { key: "fullWidth", checkbox: true, checked: wide, label: t("chat.fullWidth"), onSelect: () => setChatWide(!wide) },
324
331
  cwEnabled && canvasWorkerMenuAvailable(window) ? { key: "effects", label: t("cwControls.title"), onSelect: () => setDialog("effects") } : null,
325
332
  { key: "delete", label: t("chat.delete"), icon: "trash", danger: true, onSelect: () => setDialog("delete") },
326
333
  ];
@@ -333,7 +340,7 @@ function ChatView({ name, inst, lease, navKey, exec, awaiting }) {
333
340
  const sub = html`<span class="status"><span class=${`dot ${cls}`} aria-hidden="true"></span>${statusLabel(inst, exec, awaiting)}</span>
334
341
  ${inst.model ? html`<button type="button" class="hd-chip" title=${t("chat.chipModel", modelText(inst))} aria-label=${t("chat.chipModel", modelText(inst))} onClick=${() => pick("model")}>${modelText(inst)}</button>` : null}
335
342
  ${inst.effort ? html`<button type="button" class="hd-chip" title=${t("chat.chipEffort", inst.effort)} aria-label=${t("chat.chipEffort", inst.effort)} onClick=${() => pick("effort")}>${inst.effort}</button>` : null}`;
336
- return html`<div class=${`panel p-chat${wrap ? " wrap-code" : ""}`} ref=${view}>
343
+ return html`<div class=${`panel p-chat${wrap ? " wrap-code" : ""}`} data-width=${wide ? "full" : undefined} ref=${view}>
337
344
  <${PanelHeader} title=${headerName(namesOf(inst || { name }, names.primary))} sub=${sub} nav=${guest ? null : html`<${InstanceSwitch} name=${name} current="chat" />`} notice=${html`<${ChatEffects} name=${name} lease=${lease} element=${effectSurface} store=${store} open=${dialog === "effects"} onPlayback=${publishCanvasPlayback} onClose=${() => setDialog(null)} active=${fxHere} />`}>${fxHere ? html`<${ChatEffectPosters} name=${name} lease=${lease} element=${effectSurface} />` : null}<${CardMenu} card=${card} /><${HeaderTools} navKey=${navKey} />${guest ? null : html`<${Menu} items=${items} label=${t("app.more")} />`}</${PanelHeader}>
338
345
  <${ChatCard} key=${name} name=${name} card=${card} />
339
346
  <${AgentWidget} slot="chat_header" widget=${widgetFor(name, "chat_header")} binds=${instanceBinds(inst, exec, awaiting)} />