@songsid/agend 2.3.2-beta.10 → 2.3.2-beta.12

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 (132) hide show
  1. package/README.md +4 -0
  2. package/README.zh-TW.md +4 -0
  3. package/dist/agent-cli.js +48 -0
  4. package/dist/agent-cli.js.map +1 -1
  5. package/dist/agent-endpoint.js +6 -0
  6. package/dist/agent-endpoint.js.map +1 -1
  7. package/dist/agent-whats-new.js +3 -0
  8. package/dist/agent-whats-new.js.map +1 -1
  9. package/dist/backend/kiro-classic-archive.d.ts +52 -0
  10. package/dist/backend/kiro-classic-archive.js +444 -0
  11. package/dist/backend/kiro-classic-archive.js.map +1 -0
  12. package/dist/backend/kiro-identity.d.ts +19 -0
  13. package/dist/backend/kiro-identity.js +43 -0
  14. package/dist/backend/kiro-identity.js.map +1 -1
  15. package/dist/backend/kiro-migration-verify.d.ts +10 -0
  16. package/dist/backend/kiro-migration-verify.js +52 -0
  17. package/dist/backend/kiro-migration-verify.js.map +1 -0
  18. package/dist/backend/kiro-v2-store-worker.js +21 -1
  19. package/dist/backend/kiro-v2-store-worker.js.map +1 -1
  20. package/dist/backend/kiro-v2-store.d.ts +8 -0
  21. package/dist/backend/kiro-v2-store.js.map +1 -1
  22. package/dist/backend/kiro.d.ts +99 -1
  23. package/dist/backend/kiro.js +352 -18
  24. package/dist/backend/kiro.js.map +1 -1
  25. package/dist/backend/types.d.ts +49 -0
  26. package/dist/backend/types.js +9 -0
  27. package/dist/backend/types.js.map +1 -1
  28. package/dist/channel/ipc-timeouts.js +3 -0
  29. package/dist/channel/ipc-timeouts.js.map +1 -1
  30. package/dist/channel/mcp-tools.js +12 -0
  31. package/dist/channel/mcp-tools.js.map +1 -1
  32. package/dist/cli.js +33 -0
  33. package/dist/cli.js.map +1 -1
  34. package/dist/config-validator.js +2 -0
  35. package/dist/config-validator.js.map +1 -1
  36. package/dist/config.js +2 -0
  37. package/dist/config.js.map +1 -1
  38. package/dist/daemon.d.ts +45 -0
  39. package/dist/daemon.js +342 -19
  40. package/dist/daemon.js.map +1 -1
  41. package/dist/dashboard-permissions.d.ts +15 -4
  42. package/dist/dashboard-permissions.js +49 -23
  43. package/dist/dashboard-permissions.js.map +1 -1
  44. package/dist/fleet-control-audit.d.ts +1 -1
  45. package/dist/fleet-control-audit.js +3 -0
  46. package/dist/fleet-control-audit.js.map +1 -1
  47. package/dist/fleet-manager.d.ts +61 -2
  48. package/dist/fleet-manager.js +421 -28
  49. package/dist/fleet-manager.js.map +1 -1
  50. package/dist/general-knowledge/skills/docker-install/SKILL.md +25 -0
  51. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +10 -0
  52. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +28 -0
  53. package/dist/general-knowledge/skills/web-appearance/SKILL.md +28 -0
  54. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +20 -2
  55. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +20 -2
  56. package/dist/instance-config-impact.js +2 -2
  57. package/dist/instance-config-impact.js.map +1 -1
  58. package/dist/instance-lifecycle.d.ts +4 -0
  59. package/dist/instance-lifecycle.js +7 -0
  60. package/dist/instance-lifecycle.js.map +1 -1
  61. package/dist/kiro-migrate.d.ts +11 -0
  62. package/dist/kiro-migrate.js +86 -31
  63. package/dist/kiro-migrate.js.map +1 -1
  64. package/dist/kiro-session-proof.d.ts +41 -0
  65. package/dist/kiro-session-proof.js +89 -0
  66. package/dist/kiro-session-proof.js.map +1 -0
  67. package/dist/kiro-tui-forensic.d.ts +5 -0
  68. package/dist/kiro-tui-forensic.js +3 -3
  69. package/dist/kiro-tui-forensic.js.map +1 -1
  70. package/dist/locale.js +142 -0
  71. package/dist/locale.js.map +1 -1
  72. package/dist/log-service.js +1 -1
  73. package/dist/log-service.js.map +1 -1
  74. package/dist/needs-you.d.ts +3 -1
  75. package/dist/needs-you.js +4 -2
  76. package/dist/needs-you.js.map +1 -1
  77. package/dist/office-share.d.ts +12 -2
  78. package/dist/office-share.js +27 -2
  79. package/dist/office-share.js.map +1 -1
  80. package/dist/outbound-schemas.d.ts +39 -0
  81. package/dist/outbound-schemas.js +29 -0
  82. package/dist/outbound-schemas.js.map +1 -1
  83. package/dist/remote-card-prompt.d.ts +38 -0
  84. package/dist/remote-card-prompt.js +86 -0
  85. package/dist/remote-card-prompt.js.map +1 -0
  86. package/dist/settings-change.js +8 -5
  87. package/dist/settings-change.js.map +1 -1
  88. package/dist/settings-instance-patch.js +1 -1
  89. package/dist/settings-instance-patch.js.map +1 -1
  90. package/dist/tool-permissions.js +17 -0
  91. package/dist/tool-permissions.js.map +1 -1
  92. package/dist/transcript-sources.d.ts +1 -0
  93. package/dist/transcript-sources.js +2 -1
  94. package/dist/transcript-sources.js.map +1 -1
  95. package/dist/types.d.ts +5 -0
  96. package/dist/ui/chat-store.js +3 -2
  97. package/dist/ui/chat-strings.js +2 -2
  98. package/dist/ui/fleet-office-editor.js +54 -2
  99. package/dist/ui/fleet-office-layout.js +44 -7
  100. package/dist/ui/fleet-office-pack.d.ts +9 -3
  101. package/dist/ui/fleet-office-pack.js +117 -19
  102. package/dist/ui/fleet-office-store.js +43 -0
  103. package/dist/ui/fleet-office-zones.js +92 -17
  104. package/dist/ui/fleet-office.js +102 -16
  105. package/dist/ui/panel-chat.js +14 -2
  106. package/dist/ui/panel-log.js +2 -2
  107. package/dist/ui/panel-needs.js +1 -1
  108. package/dist/ui/panel-settings.js +5 -5
  109. package/dist/ui/settings-model.js +17 -9
  110. package/dist/ui/settings-strings.js +2 -0
  111. package/dist/ui/shared/app-i18n.js +2 -2
  112. package/dist/ui/shared/app-stream.js +1 -1
  113. package/dist/ui/shared/app.css +18 -2
  114. package/dist/web-api.d.ts +5 -1
  115. package/dist/web-api.js +91 -3
  116. package/dist/web-api.js.map +1 -1
  117. package/dist/web-office-acts.js +2 -2
  118. package/dist/web-office-acts.js.map +1 -1
  119. package/dist/web-office-pack-read.d.ts +35 -0
  120. package/dist/web-office-pack-read.js +252 -0
  121. package/dist/web-office-pack-read.js.map +1 -0
  122. package/dist/web-office-pack-tools.d.ts +232 -0
  123. package/dist/web-office-pack-tools.js +754 -0
  124. package/dist/web-office-pack-tools.js.map +1 -0
  125. package/dist/web-office-skins.d.ts +68 -4
  126. package/dist/web-office-skins.js +318 -33
  127. package/dist/web-office-skins.js.map +1 -1
  128. package/dist/web-office-store.d.ts +21 -2
  129. package/dist/web-office-store.js +78 -18
  130. package/dist/web-office-store.js.map +1 -1
  131. package/package.json +1 -1
  132. package/templates/windows-shortcuts.ps1 +16 -15
@@ -42,33 +42,62 @@ export function overflowSpots(area, need = 0) {
42
42
  * slot }). Someone still in the same area keeps their slot; the others take the first free seat, in the area's order,
43
43
  * then an overflow spot (slot -1, -2, …). No two share one. Whoever finds neither is counted in `crowd` (area → n).
44
44
  * Returns { seats: Map name → { area, slot, x, y }, crowd: Map }.
45
+ *
46
+ * #2691: a seat may be reserved (its `for`). Whoever it is for, wanting its area, takes a free seat reserved for them
47
+ * before anything else (the one they sat on last time, else their first in the area's order) — owners are seated
48
+ * before anyone keeps a slot, so a borrower on an owner's seat moves off when the owner comes. Everyone else keeps
49
+ * their slot or takes the first free seat not reserved for someone else: a plain one, else a reserved one that says
50
+ * `lend` (one no owner of it took). A reserved seat not lent, none of whose owners wants the area, is unavailable:
51
+ * the area's overflow spots and `over` count without it. Seats without `for` work as before.
45
52
  */
46
53
  export function assignSeats(pack, wants, prev = new Map()) {
47
- const seats = new Map(), crowd = new Map(), taken = new Map(), spill = new Map(), demand = new Map();
54
+ const seats = new Map(), crowd = new Map(), taken = new Map(), spill = new Map(), demand = new Map(), wanting = new Map();
48
55
  const areaOf = (a) => own(pack.areas, a);
49
- for (const w of wants) demand.set(w.area, (demand.get(w.area) || 0) + 1);
56
+ for (const w of wants) {
57
+ demand.set(w.area, (demand.get(w.area) || 0) + 1);
58
+ if (!wanting.has(w.area)) wanting.set(w.area, new Set());
59
+ wanting.get(w.area).add(w.name);
60
+ }
61
+ const mine = (s, name) => !!s.for && s.for.includes(name);
62
+ const lent = (s) => !!s.for && s.lend === true;
63
+ // The seats someone wanting the area may end up on: plain, lent, or reserved for someone who wants it.
64
+ const usable = (area) => areaOf(area).seats.filter((s) => !s.for || s.lend === true || s.for.some((n) => wanting.get(area).has(n))).length;
50
65
  const spotsOf = (area) => {
51
- if (!spill.has(area)) spill.set(area, overflowSpots(areaOf(area), Math.max(0, (demand.get(area) || 0) - areaOf(area).seats.length)));
66
+ if (!spill.has(area)) spill.set(area, overflowSpots(areaOf(area), Math.max(0, (demand.get(area) || 0) - usable(area))));
52
67
  return spill.get(area);
53
68
  };
54
69
  const pos = (area, slot) => (slot >= 0 ? areaOf(area).seats[slot] : spotsOf(area)[-slot - 1]);
55
70
  const usedIn = (area) => { if (!taken.has(area)) taken.set(area, new Set()); return taken.get(area); };
56
71
  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 }); };
72
+ const free = (w) => !seats.has(w.name) && !!areaOf(w.area);
73
+ // Owners first: their own seat from last time, then their first free one.
74
+ for (const w of wants) {
75
+ const p = prev.get(w.name), s = p && p.area === w.area && p.slot >= 0 && areaOf(w.area) ? areaOf(w.area).seats[p.slot] : null;
76
+ if (s && mine(s, w.name) && !usedIn(w.area).has(p.slot)) give(w.name, w.area, p.slot);
77
+ }
78
+ for (const w of wants) {
79
+ if (!free(w)) continue;
80
+ const used = usedIn(w.area), slot = areaOf(w.area).seats.findIndex((s, i) => mine(s, w.name) && !used.has(i));
81
+ if (slot >= 0) give(w.name, w.area, slot);
82
+ }
57
83
  for (const w of wants) {
58
84
  const p = prev.get(w.name);
59
- if (!p || p.area !== w.area || !areaOf(w.area) || !pos(w.area, p.slot) || usedIn(w.area).has(p.slot)) continue;
85
+ if (!free(w) || !p || p.area !== w.area || !pos(w.area, p.slot) || usedIn(w.area).has(p.slot)) continue;
86
+ const s = p.slot >= 0 ? areaOf(w.area).seats[p.slot] : null;
87
+ if (s && s.for && !lent(s)) continue; // reserved for someone else (theirs would be taken above)
60
88
  give(w.name, w.area, p.slot);
61
89
  }
62
90
  for (const w of wants) {
63
- if (seats.has(w.name) || !areaOf(w.area)) continue;
91
+ if (!free(w)) continue;
64
92
  const used = usedIn(w.area), area = areaOf(w.area);
65
- let slot = area.seats.findIndex((_, i) => !used.has(i));
93
+ let slot = area.seats.findIndex((s, i) => !s.for && !used.has(i));
94
+ if (slot < 0) slot = area.seats.findIndex((s, i) => lent(s) && !used.has(i));
66
95
  if (slot < 0) { const j = spotsOf(w.area).findIndex((_, i) => !used.has(-i - 1)); slot = j < 0 ? null : -j - 1; }
67
96
  if (slot === null) crowd.set(w.area, (crowd.get(w.area) || 0) + 1);
68
97
  else give(w.name, w.area, slot);
69
98
  }
70
- // Over capacity: more people want the area than it has seats (their bubbles and tags are thinned, fleet-office.js).
71
- const over = new Set([...demand].filter(([a, n]) => areaOf(a) && n > areaOf(a).seats.length).map(([a]) => a));
99
+ // Over capacity: more people want the area than it has seats for them (their bubbles and tags are thinned, fleet-office.js).
100
+ const over = new Set([...demand].filter(([a, n]) => areaOf(a) && n > usable(a)).map(([a]) => a));
72
101
  return { seats, crowd, over };
73
102
  }
74
103
 
@@ -79,11 +108,33 @@ export function assignSeats(pack, wants, prev = new Map()) {
79
108
  */
80
109
  export function zonesLayout(pack, nodes, prev = new Map(), { night = false } = {}) {
81
110
  // 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) }));
86
- const { seats, crowd, over } = assignSeats(pack, wants, prev);
111
+ const bed = zoneOf(pack, "paused"), bedSeats = own(pack.areas, bed)?.seats || [], sleepers = new Set(), inBed = new Set();
112
+ const wantsOf = (beds) => nodes.filter((n) => n.state !== "missing").map((n) => ({ name: n.name, area: beds.has(n.name) ? bed : zoneOf(pack, n.state) }));
113
+ let placed = assignSeats(pack, wantsOf(inBed), prev);
114
+ if (night) {
115
+ // #2691 review: who goes to bed is decided by the seat assignment itself (assignSeats' reservation rules), one
116
+ // sleeper at a time — whoever has a bed of their own there first, then the rest in the fleet's order. A sleeper
117
+ // goes to bed only if it gives them a bed (never an overflow spot) without taking one from anyone already in one
118
+ // (the awake first, then the sleepers admitted before), unless it is a bed reserved for them: an owner comes first,
119
+ // as by day. Everyone else dozes where they are.
120
+ const inABed = (r) => new Set([...r.seats].filter(([, s]) => s.area === bed && s.slot >= 0).map(([name]) => name));
121
+ const idle = nodes.filter((n) => n.state === "idle");
122
+ const hasBed = (n) => bedSeats.some((s) => s.for && s.for.includes(n.name));
123
+ for (const n of idle) sleepers.add(n.name);
124
+ for (const n of [...idle.filter(hasBed), ...idle.filter((n) => !hasBed(n))]) {
125
+ const trial = assignSeats(pack, wantsOf(new Set([...inBed, n.name])), prev), s = trial.seats.get(n.name);
126
+ if (!s || s.area !== bed || s.slot < 0) {
127
+ if (!hasBed(n)) break; // no bed free for anyone without one: none for the rest either
128
+ continue;
129
+ }
130
+ const before = inABed(placed), after = inABed(trial);
131
+ const theirs = !!bedSeats[s.slot].for && bedSeats[s.slot].for.includes(n.name);
132
+ if (!theirs && [...before].some((m) => !after.has(m))) continue;
133
+ inBed.add(n.name);
134
+ placed = trial;
135
+ }
136
+ }
137
+ const { seats, crowd, over } = placed;
87
138
  const desks = nodes.filter((n) => seats.has(n.name)).map((n) => {
88
139
  const s = seats.get(n.name);
89
140
  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) };
@@ -386,14 +437,38 @@ export function linesFor(state, lang, pack, defaults) {
386
437
 
387
438
  // ── Furniture ──
388
439
 
389
- /** The furniture active now: an item whose `activeWhen` names a state someone shown is in. */
390
- export function activeFurniture(pack, states, byName = new Map()) {
440
+ /**
441
+ * The people an item with `activeFor` is active for now (#2570): those of them shown in an `activeWhen` state — with
442
+ * "activeOn": "seated" (#2698), only those `posture(name)` says are "seated" (at the seat zones gave them, not walking).
443
+ * `posture` null: no walkers, everyone at their seat.
444
+ */
445
+ function activeForNow(f, byName, posture) {
446
+ const seated = (n) => f.activeOn !== "seated" || !posture || posture(n) === "seated";
447
+ return (f.activeFor || [...byName.keys()]).filter((n) => byName.has(n) && f.activeWhen.includes(byName.get(n)) && seated(n));
448
+ }
449
+ /**
450
+ * The furniture active now: an item whose `activeWhen` names a state someone shown is in. With activeFor (#2570),
451
+ * only those people's states count; with "activeOn": "seated" (#2698), only once one of them is seated (`posture`,
452
+ * name → "walking" | "seated" | "standing"; null: everyone seated) — it turns back as they walk off.
453
+ */
454
+ export function activeFurniture(pack, states, byName = new Map(), posture = null) {
391
455
  const on = new Set();
392
456
  for (const f of (pack && pack.furniture) || []) {
393
457
  if (!f.activeWhen) continue;
394
- // activeFor (#2570): only those people's states count; else anyone's shown.
395
- const hit = f.activeFor ? f.activeFor.some((n) => byName.has(n) && f.activeWhen.includes(byName.get(n))) : f.activeWhen.some((s) => states.has(s));
458
+ const hit = f.activeFor || f.activeOn === "seated" ? activeForNow(f, byName, posture).length > 0 : f.activeWhen.some((s) => states.has(s));
396
459
  if (hit) on.add(f.id);
397
460
  }
398
461
  return on;
399
462
  }
463
+ /**
464
+ * #2698: who is drawn on an item instead of themselves (name → the item's id): for each `active` item with "hides",
465
+ * the people it is active for (activeFor's order) who are not walking — someone walking is always drawn. One item each.
466
+ */
467
+ export function hiddenBy(pack, active, byName = new Map(), posture = null) {
468
+ const out = new Map();
469
+ for (const f of (pack && pack.furniture) || []) {
470
+ if (!f.hides || !f.activeWhen || !active.has(f.id)) continue;
471
+ for (const n of activeForNow(f, byName, posture)) if (!out.has(n) && (!posture || posture(n) !== "walking")) out.set(n, f.id);
472
+ }
473
+ return out;
474
+ }
@@ -15,7 +15,8 @@
15
15
  // goes through the same view transform.
16
16
  // - Speech bubbles: now and then a short line for its state (typed out; at once with reduced motion) — the page's own
17
17
  // lines or the skin's, never anything an agent said. Furniture can react: an item with `activeWhen` animates while
18
- // anyone shown is in that state.
18
+ // anyone shown is in that state — or (#2698, "activeOn": "seated") once its person sits down; one that "hides" its
19
+ // people carries their name tags and bubbles while it is active, and hovering it hits them (furnitureAt).
19
20
  // - Each occupant is a little pixel person in a pose that loops a few frames: working (typing, the lamp on), idle (tea),
20
21
  // paused or stopped (asleep, a closed sign), needs you / crashed / looks stuck (a hand up, a blinking light).
21
22
  // - Traffic: each new message, delegation or report is an item carried from the sender's desk to the receiver's along
@@ -64,13 +65,13 @@ import { hits, needle } from "./fleet-canvas-layout.js";
64
65
  import { AUTO_ID, LOBBY, autoLayout, backingScale, coatOf, createFlights, customLayout, deskAt, desksOf, filterOk, lampOf, layoutOk, lightOf, membersOf,
65
66
  officePose, officeRooms, signOf, workOn } from "./fleet-office-model.js";
66
67
  import { BUILT_IN_SKINS, CHAR_DEPTH, DEFAULT_PAL, bubbleBox, loadCozySkin, loadPackSkin, modeOf, skinFor, tagBox } from "./fleet-office-skin.js";
67
- import { CALLING, activeFurniture, bubbleAt, bubbleRank, NIGHT, WANDER, createMail, createWalkers, isNight, linesFor, npcKey, personAt, placeBubbles, thinBubbles, walkTargets, zonesLayout } from "./fleet-office-zones.js";
68
+ import { CALLING, activeFurniture, bubbleAt, hiddenBy, bubbleRank, NIGHT, WANDER, createMail, createWalkers, isNight, linesFor, npcKey, personAt, placeBubbles, thinBubbles, walkTargets, zonesLayout } from "./fleet-office-zones.js";
68
69
  import { actsOk, createOfficeStore } from "./fleet-office-store.js";
69
70
  import { POLL_MS } from "./fleet-traffic.js";
70
71
  import { OfficeEditor, officeForSave } from "./fleet-office-editor.js";
71
72
  import { LayoutEditor, handlesOf } from "./fleet-office-layout.js";
72
73
  import { ShareButton } from "./office-share-dialog.js";
73
- import { applyOverrides, fitNight } from "./fleet-office-pack.js";
74
+ import { applyOverrides, fitNight, fitSeatFor, stripBox } from "./fleet-office-pack.js";
74
75
  import { ALL_ID, LOBBY_TILE, OV, createCrossFlights, cropToRooms, fromTile, inTile, labelAt, lobbyNodes, overviewOk, tileAt, tileBox, tileLayout, visibleTiles } from "./fleet-office-overview.js";
75
76
  import { centreOn, gestureState, gestures, miniPoint, toScreen, toWorld, wheelFactor, zoomKey, zoomTo } from "./fleet-canvas-view.js";
76
77
  import { OK_MAX, OK_MIN, clampPan, desksK, edgeMatte, edgeStrips, fitScene, layerScale, miniBox, miniFrame, openingView, pinchOffice, savedOf, stepK, viewFromSaved,
@@ -193,7 +194,7 @@ function looksOf(L, work, q, mode, zones) {
193
194
  function withOverrides(baseSkin, zones, office) {
194
195
  const ov = zones && office ? office.overrides : null;
195
196
  if (!ov || ov.pack !== baseSkin.id) return { skin: baseSkin, ignored: false };
196
- const a = applyOverrides(baseSkin.pack, fitNight(baseSkin.pack, ov)); // #2665: a night place the pack lost: left out
197
+ const a = applyOverrides(baseSkin.pack, fitSeatFor(baseSkin.pack, fitNight(baseSkin.pack, ov))); // #2665/#2691: a night place or seat the pack lost: left out
197
198
  return a.ok ? { skin: { ...baseSkin, pack: a.pack }, ignored: false } : { skin: baseSkin, ignored: true };
198
199
  }
199
200
  /** 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`. */
@@ -203,7 +204,7 @@ function officeScene(office, nodes, skin, zones, deskW, prevSeats, night = false
203
204
  return lay ? customLayout({ ...office, layout: lay }, nodes) : autoLayout([{ key: office.id, name: office.name, lobby: !!office.lobby, nodes }], { width: deskW });
204
205
  }
205
206
  /** What drawPart needs of a scene; its layers are painted on first draw (and again at another resolution). */
206
- function partOf(L, looks, skin, zones, walkers, flights) {
207
+ export function partOf(L, looks, skin, zones, walkers, flights) {
207
208
  const all = L.rooms.flatMap((r) => r.desks);
208
209
  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]));
209
210
  return { L, looks, skin, zones, walkers, flights, all, desks: all.filter((d) => d.node), layers: null,
@@ -214,6 +215,43 @@ function partOf(L, looks, skin, zones, walkers, flights) {
214
215
  * (Edit layout, #2665: applyOverrides puts it on `night`), else the pack's night x/y, else its (moved) day place.
215
216
  */
216
217
  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));
218
+ /** #2698: where someone is at `t` against their seat: "walking"; "seated" (still, at the seat zones gave them — or no
219
+ * walker yet: drawn there); "standing" (still elsewhere: a stroll's spot, an act's point, the seat left before a walk). */
220
+ const postureAt = (p, t) => {
221
+ const seats = new Map(p.all.filter((d) => d.node).map((d) => [d.node.name, d.seat]));
222
+ return (name) => {
223
+ const at = p.walkers && p.walkers.at(name, t), s = seats.get(name);
224
+ if (!at) return "seated";
225
+ if (at.walking) return "walking";
226
+ return s && Math.abs(at.x - s.x) < 1 && Math.abs(at.y - s.y) < 1 ? "seated" : "standing";
227
+ };
228
+ };
229
+ /**
230
+ * The furniture active at `t` and who is drawn on an item instead of themselves (`hidden`: name → item id). A pack
231
+ * with no "activeOn": "seated" nor "hides" (#2698): the scene's `active`, as before, and nobody hidden; else worked out
232
+ * from where everyone is at `t` (their walkers). An item asleep at bedtime (a `night`) hides nobody.
233
+ */
234
+ export function furnitureAt(p, t) {
235
+ const pk = p.zones && p.skin && p.skin.pack, list = (pk && pk.furniture) || [];
236
+ if (!list.some((f) => f.activeOn === "seated" || f.hides)) return { active: p.active || new Set(), hidden: new Map() };
237
+ const looks = [...p.looks.values()], byName = new Map(looks.filter((l) => l.name).map((l) => [l.name, l.state]));
238
+ const posture = postureAt(p, t), active = activeFurniture(pk, new Set(looks.map((l) => l.state)), byName, posture);
239
+ const hidden = hiddenBy(pk, active, byName, posture);
240
+ if (p.night) for (const [n, id] of [...hidden]) if (list.some((f) => f.id === id && f.night)) hidden.delete(n);
241
+ return { active, hidden };
242
+ }
243
+ /** #2698: the box an item hiding its people is hit in — its active picture, down to its name tags' row. */
244
+ const hidingBox = (f) => {
245
+ const b = stripBox(f.activeSprite || f.sprite, f.x, f.y);
246
+ return { ...b, h: Math.max(b.h, f.y + 17 - b.y) };
247
+ };
248
+ /** #2698: name tags in one row centred on (x, y) — each { look, x } (its centre), TAG_GAP apart. */
249
+ const TAG_GAP = 4;
250
+ export function tagRow(ctx, x, y, looks) {
251
+ const ws = looks.map((l) => tagBox(ctx, x, y, l.label).w), total = ws.reduce((a, w) => a + w, 0) + TAG_GAP * Math.max(0, ws.length - 1);
252
+ let left = x - total / 2;
253
+ return looks.map((look, i) => { const cx = Math.round(left + ws[i] / 2); left += ws[i] + TAG_GAP; return { look, x: cx }; });
254
+ }
217
255
  /** #2596: each NPC with a `walk` where its walker is now (`at(key)`): moved there — its tag and depth with it — and, while
218
256
  * it walks, `walking` and its `heading` (its walk strip: walkStrip); asleep, or with no walker (not strolling), where it is. */
219
257
  export const strollFurniture = (list, at) => list.map((f) => {
@@ -309,7 +347,7 @@ function paintLayers(p, env, ls, tagsOff = false) {
309
347
  * hovered (its name tag shows even in a crowd); `tagsOff` (#2554: a tile too small to read them): the name tags show
310
348
  * only for the one hovered and the Find box's matches. Whether anything in it is moving (in the air, or someone walking).
311
349
  */
312
- function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false, defer = null) {
350
+ export function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false, defer = null) {
313
351
  const sk = p.skin;
314
352
  const ls = layerScale(a, p.L.width, p.L.height);
315
353
  if (!p.layers || p.layers.ls !== ls || p.layers.tagsOff !== tagsOff) p.layers = paintLayers(p, env, ls, tagsOff);
@@ -326,7 +364,7 @@ function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false, defer
326
364
  // Furniture under the people; the people (nearest last) with the furniture whose depth is "y" among them, by base
327
365
  // y (a tie: the item first, the person in front); furniture over them; things in the air; the pack's foreground
328
366
  // (door frames, pillars, table fronts); then what must stay readable: crowd counts and name tags (bubbles below).
329
- const t = ms > 0 ? ms : now();
367
+ const t = ms > 0 ? ms : now(), live = furnitureAt(p, t);
330
368
  const people = p.desks.map((d) => {
331
369
  const at = p.walkers.at(d.node.name, t) || { ...d.seat, walking: false };
332
370
  if (at.walking) walking = true;
@@ -339,12 +377,13 @@ function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false, defer
339
377
  // #2665: by day, each item's night place too (at night the items are there already).
340
378
  ...(p.ghosts && !p.night ? nightGhostsOf(sk.pack).map((n) => ({ y: n.y, k: 1, n })) : [])]
341
379
  .sort((x, y) => x.y - y.y || x.k - y.k);
342
- 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);
380
+ for (const f of furniture) if (f.depth !== "y" && f.depth < CHAR_DEPTH) sk.drawFurniture(ctx, f, f.asleep || live.active.has(f.id), ms, env);
343
381
  for (const it of post) if (it.t === 0) sk.drawProp(ctx, it, ms, env); // a letter on the floor: under the people
344
382
  for (const { f, p: q, g, n } of band) {
345
- if (f) { sk.drawFurniture(ctx, f, f.asleep || p.active.has(f.id), ms, env); continue; }
383
+ if (f) { sk.drawFurniture(ctx, f, f.asleep || live.active.has(f.id), ms, env); continue; }
346
384
  if (n) { ctx.save(); ctx.globalAlpha = GHOST_ALPHA; sk.drawFurniture(ctx, n, true, 0, env); ctx.restore(); continue; }
347
385
  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; }
386
+ if (live.hidden.has(q.look.name)) continue; // #2698: drawn as the item (never while walking)
348
387
  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 } };
349
388
  // An agent's own pose (office_set, #2578) while its act lasts and nothing calls it; walking is walking.
350
389
  const posed = actPosed(p, q.look);
@@ -352,7 +391,7 @@ function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false, defer
352
391
  sk.drawCharacter(ctx, box, look, ms, env);
353
392
  if (!q.at.walking) speakers.push({ look: q.look, ...speakerAt(sk, look, q.at), area: q.d.area });
354
393
  }
355
- 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);
394
+ for (const f of furniture) if (f.depth !== "y" && f.depth >= CHAR_DEPTH) sk.drawFurniture(ctx, f, f.asleep || live.active.has(f.id), ms, env);
356
395
  for (const it of up) sk.drawProp(ctx, it, ms, env);
357
396
  for (const it of post) if (it.t > 0) sk.drawProp(ctx, it, ms, env);
358
397
  if (sk.drawForeground) sk.drawForeground(ctx, p.L, env);
@@ -360,13 +399,38 @@ function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false, defer
360
399
  for (const [ar, n] of p.L.crowd || []) if (p.L.areas[ar] && sk.drawCrowd) sk.drawCrowd(ctx, p.L.areas[ar], n, env);
361
400
  // In an area over capacity the name tags show on hover or focus only (#2513: a crowd's tags covered each other).
362
401
  const over = p.L.over || new Set();
402
+ const tagged = (q) => !((tagsOff || over.has(q.d.area)) && q.d.key !== hot && !q.look.match);
363
403
  for (const q of people) {
364
- if ((tagsOff || over.has(q.d.area)) && q.d.key !== hot && !q.look.match) continue;
404
+ if (live.hidden.has(q.look.name) || !tagged(q)) continue;
365
405
  sk.drawTag(ctx, q.at.x, q.at.y, q.look, env);
366
406
  tags.push(tagBox(ctx, q.at.x, q.at.y, q.look.label));
367
407
  }
368
408
  // A furniture item's own name (#2574: an NPC drawn as furniture), tagged like a person; none on a tile too small to read.
369
- 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)); }
409
+ // #2698: an item hiding its people carries their tags too, in one row at its tag point (its own first; each keeps
410
+ // its place in the row while another's tag is left out), and their bubbles, over its picture. The tags drawn are
411
+ // kept on the scene (`heldTags`): hitPart hits each person on theirs, past the picture's edge too (#2699 review).
412
+ const held = [];
413
+ for (const f of furniture) {
414
+ const on = people.filter((q) => live.hidden.get(q.look.name) === f.id);
415
+ if (!on.length) {
416
+ if (!tagsOff && f.label) { sk.drawTag(ctx, f.x, f.y, { label: f.label }, env); tags.push(tagBox(ctx, f.x, f.y, f.label)); }
417
+ continue;
418
+ }
419
+ const order = new Map((f.activeFor || []).map((n, i) => [n, i]));
420
+ on.sort((x, y) => (order.get(x.look.name) ?? 0) - (order.get(y.look.name) ?? 0));
421
+ const row = tagRow(ctx, f.x, f.y, [...(f.label ? [{ label: f.label }] : []), ...on.map((q) => q.look)]);
422
+ const top = f.sprite ? Math.min(f.y - 50, stripBox(f.activeSprite || f.sprite, f.x, f.y).y + 2) : f.y - 50;
423
+ for (const [i, r] of row.entries()) {
424
+ const q = f.label ? on[i - 1] : on[i];
425
+ if (q) speakers.push({ look: q.look, x: r.x + 10, y: top, area: q.d.area });
426
+ if (q ? !tagged(q) : tagsOff) continue;
427
+ sk.drawTag(ctx, r.x, f.y, r.look, env);
428
+ const box = tagBox(ctx, r.x, f.y, r.look.label);
429
+ tags.push(box);
430
+ if (q) held.push({ name: q.look.name, id: f.id, box });
431
+ }
432
+ }
433
+ p.heldTags = held;
370
434
  } else {
371
435
  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 }); }
372
436
  blit(p.layers.front);
@@ -398,10 +462,30 @@ function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false, defer
398
462
  * 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
399
463
  * in (an uploaded character's strip, anchor and size: #2513 review), nearest first; on desks, the desk.
400
464
  */
401
- function hitPart(p, x, y, t) {
465
+ export function hitPart(p, x, y, t) {
402
466
  if (!p.zones) return deskAt(p.L, x, y);
403
- return personAt(p.all, x, y, (d) => p.walkers.at(d.node.name, t),
404
- (d, at) => (p.skin.charBox && p.looks.get(d.key) ? p.skin.charBox(p.looks.get(d.key), at) : null));
467
+ // #2698: someone an item hides is hit on it, after everyone drawn as themselves: on their tag as drawPart last drew
468
+ // it (the row can run past the picture: a long label, long names), else on its picture and the tag row under it
469
+ // (several share its width, left to right in the row's order).
470
+ const { hidden } = furnitureAt(p, t);
471
+ const d = personAt(hidden.size ? p.all.filter((dk) => !dk.node || !hidden.has(dk.node.name)) : p.all, x, y, (dk) => p.walkers.at(dk.node.name, t),
472
+ (dk, at) => (p.skin.charBox && p.looks.get(dk.key) ? p.skin.charBox(p.looks.get(dk.key), at) : null));
473
+ if (d || !hidden.size) return d;
474
+ for (const h of [...(p.heldTags || [])].reverse()) {
475
+ const b = h.box;
476
+ if (hidden.get(h.name) !== h.id || x < b.x || x >= b.x + b.w || y < b.y || y >= b.y + b.h) continue;
477
+ return p.all.find((dk) => dk.node && dk.node.name === h.name) || null;
478
+ }
479
+ const sk = p.skin.pack, drawn = strollFurniture(p.night ? nightFurniture(sk.furniture) : sk.furniture, (k) => p.walkers.at(k, t));
480
+ for (const f of [...drawn].reverse()) {
481
+ if (!f.sprite || ![...hidden.values()].includes(f.id)) continue;
482
+ const b = hidingBox(f);
483
+ if (x < b.x || x >= b.x + b.w || y < b.y || y >= b.y + b.h) continue;
484
+ const order = new Map((f.activeFor || []).map((n, i) => [n, i]));
485
+ const on = p.all.filter((dk) => dk.node && hidden.get(dk.node.name) === f.id).sort((m, n) => (order.get(m.node.name) ?? 0) - (order.get(n.node.name) ?? 0));
486
+ return on[Math.min(on.length - 1, Math.floor(((x - b.x) / b.w) * on.length))] || null;
487
+ }
488
+ return null;
405
489
  }
406
490
 
407
491
  function People({ nodes, pinned, onChange }) {
@@ -1113,6 +1197,8 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, assetIm
1113
1197
  setTip(null);
1114
1198
  };
1115
1199
  const closeLayout = () => setLayout(null);
1200
+ // #2691: whom a seat can be reserved for — the office's members (its teams' too).
1201
+ const layoutMembers = useMemo(() => (layout && custom ? membersOf(custom, view).map((n) => n.name) : []), [layout && layout.id, custom, view]);
1116
1202
  const onDraft = (draft) => setLayout((cur) => (cur ? { ...cur, draft } : cur));
1117
1203
  // Save: the session's list, this office's overrides replaced (none: back to the style's own), with the session's rev.
1118
1204
  // A change made meanwhile (409) reads the list again: the session takes it (the view too), the draft is kept, and
@@ -1193,7 +1279,7 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, assetIm
1193
1279
  : 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)}
1194
1280
  onClick=${() => pick(tl.key)}>${text}</button>`;
1195
1281
  }) : null}
1196
- ${layout ? html`<${LayoutEditor} key=${layout.id} base=${layout.base} start=${layout.start} place=${placeOverlay} dock=${!fs}
1282
+ ${layout ? html`<${LayoutEditor} key=${layout.id} base=${layout.base} start=${layout.start} place=${placeOverlay} dock=${!fs} members=${layoutMembers}
1197
1283
  stale=${layoutStale ? t("orgl.stale") : ""} onChange=${onDraft} onSave=${saveLayout} onClose=${closeLayout} />` : null}
1198
1284
  ${tipText ? html`<div ref=${tipRef} class="oo-tip" role="tooltip">
1199
1285
  <strong>${tipText.primary}</strong>${tipText.secondary ? html` <span class="muted mono">${tipText.secondary}</span>` : null}
@@ -618,8 +618,20 @@ function Prompts({ name }) {
618
618
  if (!list.length) return null;
619
619
  return html`<div data-trust="permission" class="prompts" aria-live="polite">${list.map(p => html`<div key=${p.nonce} class=${`prompt${p.resolved ? " done" : ""}`} role="group" aria-label=${p.text}>
620
620
  <div class="txt">${p.resolved ? (p.outcome || t("chat.promptGone")) : textLinkSegments(p.text, p.links).map(sg => sg.href ? html`<a data-trust="link" class="st-link" href=${sg.href}>${sg.text}</a>` : sg.text)}</div>
621
- ${p.resolved ? null : html`<div class="acts">${p.actions.map(a => html`<button key=${a.id} type="button" class="btn" disabled=${!!p.busy} onClick=${() => store.answerPrompt(p, a.id)}>${a.label}</button>`)}
622
- ${p.busy ? html`<span class="wait">${t("chat.promptAnswering")}</span>` : null}</div>`}</div>`)}</div>`;
621
+ ${p.resolved ? null : html`<div class="acts">${p.actions.filter(a => !a.input).map(a => html`<button key=${a.id} type="button" class="btn" disabled=${!!p.busy} onClick=${() => store.answerPrompt(p, a.id)}>${a.label}</button>`)}
622
+ ${p.busy ? html`<span class="wait">${t("chat.promptAnswering")}</span>` : null}</div>`}
623
+ ${p.resolved ? null : p.actions.filter(a => a.input).map(a => html`<${PromptText} key=${a.id} p=${p} action=${a} />`)}</div>`)}</div>`;
624
+ }
625
+
626
+ /** #2675: a prompt's typed answer (a kiro question card's "answer in words"): a box and its send button. */
627
+ function PromptText({ p, action }) {
628
+ const [text, setText] = useState("");
629
+ const send = (e) => { e.preventDefault(); if (text.trim() && !p.busy) store.answerPrompt(p, action.id, text); };
630
+ return html`<form class="prompt-text" onSubmit=${send}>
631
+ <input type="text" maxlength="4000" aria-label=${action.label} placeholder=${t("chat.promptTextPlaceholder")} value=${text} disabled=${!!p.busy}
632
+ onInput=${(e) => setText(e.currentTarget.value)} />
633
+ <button type="submit" class="btn" disabled=${!!p.busy || !text.trim()}>${t("chat.promptTextSend")}</button>
634
+ </form>`;
623
635
  }
624
636
 
625
637
  /** "<name> is working… 0:42", "looks stuck", "Stopping…", or what it waits on at its terminal. Text only. */
@@ -55,7 +55,7 @@ register("log", {
55
55
  cat_read: "Read", cat_edit: "Edit", cat_search: "Search", cat_run: "Run", cat_web: "Web", cat_mcp: "MCP", cat_agent: "Sub-agent",
56
56
  cat_plan: "Plan", cat_message: "Message", cat_other: "Tool", plan_items: "{0} items",
57
57
  ev_crash_respawn: "Crashed and restarted", ev_crash_loop: "Keeps crashing", ev_hang_detected: "Looks stuck", ev_hang_action: "Stuck: acted on",
58
- ev_instance_paused: "Paused", ev_instance_resumed: "Resumed", ev_interactive_prompt: "Waiting at a prompt", ev_interactive_prompt_assist: "Prompt: asked for help",
58
+ ev_instance_paused: "Paused", ev_instance_resumed: "Resumed", ev_interactive_prompt: "Waiting at a prompt", ev_interactive_prompt_assist: "Prompt: asked for help", ev_remote_card: "Asked a question (posted to chat)", ev_remote_card_answer: "Question answered from chat",
59
59
  ev_dialog_parked: "A dialog waits", ev_model_failover: "Switched to another model", ev_model_recovered: "Model back",
60
60
  ev_mcp_died: "Tool server stopped", ev_mcp_recovered: "Tool server back", ev_mcp_auto_restart: "Tool server restarted",
61
61
  ev_message_failed: "A message was not delivered", ev_pty_error: "Terminal error", ev_pty_recovered: "Terminal recovered",
@@ -93,7 +93,7 @@ register("log", {
93
93
  cat_read: "讀取", cat_edit: "編輯", cat_search: "搜尋", cat_run: "執行", cat_web: "網頁", cat_mcp: "MCP", cat_agent: "子 agent",
94
94
  cat_plan: "計畫", cat_message: "訊息", cat_other: "工具", plan_items: "{0} 項",
95
95
  ev_crash_respawn: "當機後重新啟動", ev_crash_loop: "反覆當機", ev_hang_detected: "看起來卡住了", ev_hang_action: "卡住:已處理",
96
- ev_instance_paused: "已暫停", ev_instance_resumed: "已繼續", ev_interactive_prompt: "停在提示上等待", ev_interactive_prompt_assist: "提示:已請求協助",
96
+ ev_instance_paused: "已暫停", ev_instance_resumed: "已繼續", ev_interactive_prompt: "停在提示上等待", ev_interactive_prompt_assist: "提示:已請求協助", ev_remote_card: "提出問題(已貼到聊天)", ev_remote_card_answer: "問題已從聊天回答",
97
97
  ev_dialog_parked: "有對話框在等待", ev_model_failover: "改用另一個模型", ev_model_recovered: "模型恢復",
98
98
  ev_mcp_died: "工具伺服器停止", ev_mcp_recovered: "工具伺服器恢復", ev_mcp_auto_restart: "工具伺服器已重新啟動",
99
99
  ev_message_failed: "有訊息沒有送達", ev_pty_error: "終端機錯誤", ev_pty_recovered: "終端機恢復",
@@ -196,7 +196,7 @@ function NeedsItem({ item, nowMs, owner }) {
196
196
  <div class="n-title"><strong>${reasonText(item)}</strong><span class="n-age">${age(item.since, nowMs)}</span></div>
197
197
  ${detailOf(item) ? html`<p class="n-detail">${detailOf(item)}</p>` : null}
198
198
  <div class="n-actions">
199
- ${item.type === "prompt" && Array.isArray(item.actions) ? item.actions.map((a) => html`<button key=${a.id} type="button" class="btn btn-sm"
199
+ ${item.type === "prompt" && Array.isArray(item.actions) ? item.actions.filter((a) => !a.input).map((a) => html`<button key=${a.id} type="button" class="btn btn-sm"
200
200
  disabled=${promptBusy} onClick=${() => answer(a.id)}>${(p ? p.busy : fallback === "busy") ? tn("answering") : a.label}</button>`) : null}
201
201
  ${item.type === "delivery" ? html`<button type="button" class="btn btn-sm" disabled=${!!ack} onClick=${acknowledge}>
202
202
  <${Icon} name="check" size=${14} />${ack === "busy" ? tn("acknowledging") : tn("acknowledge")}</button>` : null}
@@ -27,7 +27,7 @@ import "./settings-strings.js";
27
27
  import { api, attach, confirmedWrite, newKey } from "./settings-confirm.js";
28
28
  import { dismissOperation, operationActive, restartFleet, startOperation, takeLeftover } from "./settings-apply.js";
29
29
  import {
30
- ACCESS_MODES, AGENT_MODES, DASHBOARD_FEATURES, DEFAULT_SCHEMA, ownChoiceImpact, HANG_TIMEOUT_DEFAULT_MINUTES, LOG_LEVELS, TOOL_PROGRESS, TOOL_SETS, VISIBILITY_MODES, batchImpact,
30
+ ACCESS_MODES, AGENT_MODES, DASHBOARD_FEATURES, SEPARATE_FEATURE_KEYS, DEFAULT_SCHEMA, ownChoiceImpact, HANG_TIMEOUT_DEFAULT_MINUTES, LOG_LEVELS, TOOL_PROGRESS, TOOL_SETS, VISIBILITY_MODES, batchImpact,
31
31
  changedFields, channelId, channelsOf, chLabel, connectionState, dashboardDefault, effectiveSummary, fleetModel, fromYaml, fullModelRequests, groupAgents,
32
32
  hasOwn, impactOf, localeOptions, defaultsImpact, requestImpact, nonNegative, positive, replyGuardSupported, sameValue, shortName, STATUS_ORDER, toYaml,
33
33
  toolSetDefault, visibilityDefault, worstImpact,
@@ -1024,16 +1024,16 @@ function General({ ctx, part = "all" }) {
1024
1024
  tips: noticeMode(f.tips), reply_triggers: f.replyTriggers, reply_completion_guard: f.reply, log_level: f.log_level,
1025
1025
  // The dashboard features every agent may use unless its own setting says otherwise. All of them go, because the write
1026
1026
  // to the fleet's defaults merges top-level keys only (`dashboard` is replaced whole): the untouched ones as saved.
1027
- dashboard: Object.fromEntries(DASHBOARD_FEATURES.filter((k) => k !== "office").map((k) => [k, f[`dash_${k}`]])),
1028
- office_access: f.dash_office,
1027
+ dashboard: Object.fromEntries(DASHBOARD_FEATURES.filter((k) => !Object.hasOwn(SEPARATE_FEATURE_KEYS, k)).map((k) => [k, f[`dash_${k}`]])),
1028
+ ...Object.fromEntries(Object.entries(SEPARATE_FEATURE_KEYS).map(([k, key]) => [key, f[`dash_${k}`]])),
1029
1029
  hang_detector: { enabled: f.hangOn, timeout_minutes: num(f.hang) },
1030
1030
  startup: { ...(f.concurrency.trim() === "" ? {} : { concurrency: num(f.concurrency) }), stagger_delay_ms: num(f.stagger) },
1031
1031
  }, {
1032
1032
  backend: d.backend || "claude-code", model: d.model || undefined, locale: d.locale || null, auto_pause_after: d.auto_pause_after ?? 0,
1033
1033
  agent_mode: d.agent_mode || "mcp", tool_set: TOOL_SETS.includes(d.tool_set) ? d.tool_set : null, tool_progress: d.tool_progress ?? "off",
1034
1034
  tips: d.tips ?? true, cross_instance_visibility: visibilityDefault(d), reply_triggers: Boolean(d.reply_triggers ?? schema.reply_triggers_default), reply_completion_guard: d.reply_completion_guard ?? true, log_level: d.log_level || "info",
1035
- dashboard: Object.fromEntries(DASHBOARD_FEATURES.filter((k) => k !== "office").map((k) => [k, dashboardDefault(d, k)])),
1036
- office_access: dashboardDefault(d, "office"),
1035
+ dashboard: Object.fromEntries(DASHBOARD_FEATURES.filter((k) => !Object.hasOwn(SEPARATE_FEATURE_KEYS, k)).map((k) => [k, dashboardDefault(d, k)])),
1036
+ ...Object.fromEntries(Object.entries(SEPARATE_FEATURE_KEYS).map(([k, key]) => [key, dashboardDefault(d, k)])),
1037
1037
  hang_detector: { enabled: hang.enabled !== false, timeout_minutes: hang.timeout_minutes ?? HANG_TIMEOUT_DEFAULT_MINUTES },
1038
1038
  startup: { ...(startup.concurrency == null ? {} : { concurrency: startup.concurrency }), stagger_delay_ms: startup.stagger_delay_ms ?? 500 },
1039
1039
  });
@@ -40,26 +40,32 @@ export function changedFields(next, baseline) {
40
40
 
41
41
  /**
42
42
  * The dashboard features an agent may use (dashboard-permissions.ts): each "inherit" (the fleet default: `defaults.dashboard`,
43
- * else allowed — off for OFF_UNLESS_ON, `office`), "on" or "off". The patch names only the features that changed: null
43
+ * else allowed — off for OFF_UNLESS_ON, `office` and `office_packs`), "on" or "off". The patch names only the features that changed: null
44
44
  * follows the default again.
45
45
  */
46
- export const DASHBOARD_FEATURES = ["appearance", "task_board", "web_settings", "office"];
47
- /** #2578: the features a person switches on — unset is off (dashboard-permissions.ts OFF_UNLESS_ON). */
48
- export const OFF_UNLESS_ON = ["office"];
46
+ export const DASHBOARD_FEATURES = ["appearance", "task_board", "web_settings", "office", "office_packs"];
47
+ /** #2578, #2692: the features a person switches on — unset is off (dashboard-permissions.ts OFF_UNLESS_ON). */
48
+ export const OFF_UNLESS_ON = ["office", "office_packs"];
49
+ /** #2601, #2692: the features kept beside `dashboard` under their own key (dashboard-permissions.ts SEPARATE_FEATURE_KEYS). */
50
+ export const SEPARATE_FEATURE_KEYS = { office: "office_access", office_packs: "office_pack_access" };
49
51
  export const DASHBOARD_CHOICES = ["inherit", "on", "off"];
50
52
  const dashboardOf = (v) => (v && typeof v === "object" && !Array.isArray(v) ? v : {});
51
53
  /** What a feature is for an agent with no setting of its own: the default's, else allowed (off for OFF_UNLESS_ON). */
52
54
  export const dashboardDefault = (defaults, feature) => {
53
- const v = feature === "office" && defaults && Object.hasOwn(defaults, "office_access") ? defaults.office_access : dashboardOf(defaults && defaults.dashboard)[feature];
55
+ const key = Object.hasOwn(SEPARATE_FEATURE_KEYS, feature) ? SEPARATE_FEATURE_KEYS[feature] : null;
56
+ const v = key && defaults && Object.hasOwn(defaults, key) ? defaults[key] : dashboardOf(defaults && defaults.dashboard)[feature];
54
57
  return OFF_UNLESS_ON.includes(feature) ? v === true : v !== false;
55
58
  };
56
59
  export function dashboardForm(inst) {
57
- const own = { ...dashboardOf(inst.dashboard), ...(Object.hasOwn(inst, "office_access") ? { office: inst.office_access } : {}) };
60
+ const own = { ...dashboardOf(inst.dashboard) };
61
+ for (const [feature, key] of Object.entries(SEPARATE_FEATURE_KEYS)) if (Object.hasOwn(inst, key)) own[feature] = inst[key];
58
62
  return Object.fromEntries(DASHBOARD_FEATURES.map((f) => [f, own[f] === true ? "on" : own[f] === false ? "off" : "inherit"]));
59
63
  }
60
64
  export function dashboardPatchValue(choices) {
61
- return Object.fromEntries(DASHBOARD_FEATURES.filter((f) => f !== "office").map((f) => [f, choices[f] === "on" ? true : choices[f] === "off" ? false : null]));
65
+ return Object.fromEntries(DASHBOARD_FEATURES.filter((f) => !Object.hasOwn(SEPARATE_FEATURE_KEYS, f)).map((f) => [f, choices[f] === "on" ? true : choices[f] === "off" ? false : null]));
62
66
  }
67
+ /** A separately kept feature's own key in a patch (office_access, office_pack_access): true, false or null (inherit). */
68
+ export const separateChoice = (choice) => (choice === "on" ? true : choice === "off" ? false : null);
63
69
 
64
70
  /** #1302: cross-instance notices. Unset, or a value the fleet does not know, means full. */
65
71
  export const visibilityDefault = (defaults) => (VISIBILITY_MODES.includes(defaults && defaults.cross_instance_visibility) ? defaults.cross_instance_visibility : "full");
@@ -181,7 +187,8 @@ export function agentPatch(form, inst, defaults, visibilityBaseline, statusEmoji
181
187
  display_name: form.inherit.display_name ? null : form.display_name.trim(),
182
188
  status_emojis: form.status_emojis,
183
189
  dashboard: dashboardPatchValue(form.dashboard),
184
- office_access: form.dashboard.office === "on" ? true : form.dashboard.office === "off" ? false : null,
190
+ office_access: separateChoice(form.dashboard.office),
191
+ office_pack_access: separateChoice(form.dashboard.office_packs),
185
192
  };
186
193
  return changedFields(next, {
187
194
  backend: inst.backend || defaults.backend || "claude-code",
@@ -205,7 +212,8 @@ export function agentPatch(form, inst, defaults, visibilityBaseline, statusEmoji
205
212
  display_name: hasOwn(inst, "display_name") ? inst.display_name : null,
206
213
  status_emojis: statusEmojisBaseline,
207
214
  dashboard: dashboardPatchValue(dashboardForm(inst)),
208
- office_access: dashboardForm(inst).office === "on" ? true : dashboardForm(inst).office === "off" ? false : null,
215
+ office_access: separateChoice(dashboardForm(inst).office),
216
+ office_pack_access: separateChoice(dashboardForm(inst).office_packs),
209
217
  });
210
218
  }
211
219
 
@@ -113,6 +113,7 @@ register("settings", {
113
113
  dashboard_task_board: "The task board",
114
114
  dashboard_web_settings: "Your web appearance settings",
115
115
  dashboard_office: "Office: walk, speak and act in the Office view",
116
+ dashboard_office_packs: "Office skin packs: check, upload, replace and restore them (everyone sees them)",
116
117
  dashboardInherit: "Fleet default ({0})",
117
118
  dashboardOn: "allowed",
118
119
  dashboardOff: "off",
@@ -762,6 +763,7 @@ register("settings", {
762
763
  dashboard_task_board: "任務看板",
763
764
  dashboard_web_settings: "你的網頁外觀設定",
764
765
  dashboard_office: "辦公室:在辦公室畫面走動、說話、做動作",
766
+ dashboard_office_packs: "辦公室外觀包:檢查、上傳、取代與還原(全 fleet 都看得到)",
765
767
  dashboardInherit: "跟 fleet 預設({0})",
766
768
  dashboardOn: "允許",
767
769
  dashboardOff: "關閉",
@@ -105,7 +105,7 @@ register("app", {
105
105
  confirmTitle: "Please confirm", confirmOk: "Continue", cancel: "Cancel",
106
106
  sidePanelLeave: "Leave {0} without saving? Your changes there will be lost.", sidePanelDiscard: "Discard changes",
107
107
  // The server's own wording (src/locale.ts needs.reason.*), so the web says what Discord says.
108
- needs_reason_hang: "Not responding", needs_reason_exited: "Exited", needs_reason_assist: "Waiting at its terminal", needs_reason_permission: "Permission needed", needs_reason_dangerous_command: "Dangerous command waiting", needs_reason_login: "Sign-in needed", needs_reason_dialog: "Waiting at a dialog", needs_reason_terminal_input: "May be waiting for input", needs_reason_auth_paused: "Paused: sign-in needed", needs_reason_backend_signin: "Backend sign-in needed", needs_reason_crashed: "Crashed", needs_reason_delivery_uncertain: "Could not confirm delivery", needs_reason_delivery_failed: "Delivery failed", needs_reason_task_review: "Waiting for your confirmation", needs_reason_task_blocked: "Blocked: asks you",
108
+ needs_reason_hang: "Not responding", needs_reason_exited: "Exited", needs_reason_assist: "Waiting at its terminal", needs_reason_question: "Asking you a question", needs_reason_permission: "Permission needed", needs_reason_dangerous_command: "Dangerous command waiting", needs_reason_login: "Sign-in needed", needs_reason_dialog: "Waiting at a dialog", needs_reason_terminal_input: "May be waiting for input", needs_reason_auth_paused: "Paused: sign-in needed", needs_reason_backend_signin: "Backend sign-in needed", needs_reason_crashed: "Crashed", needs_reason_delivery_uncertain: "Could not confirm delivery", needs_reason_delivery_failed: "Delivery failed", needs_reason_task_review: "Waiting for your confirmation", needs_reason_task_blocked: "Blocked: asks you",
109
109
  },
110
110
  "zh-TW": {
111
111
  brand: "AgEnD", fleet: "Fleet", fleetOverview: "Fleet 概覽", view: "檢視", settings: "設定", instances: "Instances", newInstance: "新增 instance", newAgent: "新增 Agent",
@@ -145,6 +145,6 @@ register("app", {
145
145
  search: "搜尋", searchTitle: "搜尋聊天、agent 之間的訊息與任務({0})",
146
146
  confirmTitle: "請確認", confirmOk: "繼續", cancel: "取消",
147
147
  sidePanelLeave: "不儲存就離開「{0}」?你在那裡的修改會遺失。", sidePanelDiscard: "捨棄修改",
148
- needs_reason_hang: "沒有回應", needs_reason_exited: "已結束", needs_reason_assist: "在終端機上等待", needs_reason_permission: "需要權限", needs_reason_dangerous_command: "危險指令待確認", needs_reason_login: "需要登入", needs_reason_dialog: "停在對話框", needs_reason_terminal_input: "可能在等輸入", needs_reason_auth_paused: "已暫停:需要登入", needs_reason_backend_signin: "後端需要登入", needs_reason_crashed: "已崩潰", needs_reason_delivery_uncertain: "無法確認是否送達", needs_reason_delivery_failed: "傳送失敗", needs_reason_task_review: "等你確認", needs_reason_task_blocked: "卡住,在問你",
148
+ needs_reason_hang: "沒有回應", needs_reason_exited: "已結束", needs_reason_assist: "在終端機上等待", needs_reason_question: "正在問你問題", needs_reason_permission: "需要權限", needs_reason_dangerous_command: "危險指令待確認", needs_reason_login: "需要登入", needs_reason_dialog: "停在對話框", needs_reason_terminal_input: "可能在等輸入", needs_reason_auth_paused: "已暫停:需要登入", needs_reason_backend_signin: "後端需要登入", needs_reason_crashed: "已崩潰", needs_reason_delivery_uncertain: "無法確認是否送達", needs_reason_delivery_failed: "傳送失敗", needs_reason_task_review: "等你確認", needs_reason_task_blocked: "卡住,在問你",
149
149
  },
150
150
  });
@@ -37,7 +37,7 @@
37
37
  // offices — the stored offices changed ({ rev }; SSE only: the Office view reads /ui/offices again).
38
38
  // office_skins — a skin pack was uploaded or deleted ({ rev }; SSE only: the Office view reads /ui/office-skins again).
39
39
  // office_acts — the agents' live acts in the Office view changed ({ acts }, #2578; a poll page reads /ui/office-acts).
40
- const EVENTS = ["status", "message", "agent_reply_live", "delivery", "deliveries", "prompt", "prompts", "prompt_resolved", "activity", "needs", "reply_buttons", "reactions", "steps_tick", "clock", "traffic", "delegation", "backend_login", "tasks", "offices", "office_skins", "office_acts"];
40
+ const EVENTS = ["status", "message", "agent_reply_live", "delivery", "deliveries", "prompt", "prompts", "prompt_resolved", "activity", "needs", "reply_buttons", "reactions", "steps_tick", "clock", "traffic", "delegation", "backend_login", "tasks", "offices", "office_skins", "office_acts", "office_pack_changes"];
41
41
  const MAX_HELD = 1000; // live events held during one catch-up read; more → read again afterwards
42
42
  const RETRY_DELAYS = [1000, 2000, 4000]; // after the first failed catch-up read; then it waits for Retry
43
43
  const POLL_LIMIT_MS = 10000; // a poll that has not answered by then counts as failed