@kylecheng3146/agent-ops 0.5.3 → 0.7.0

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 (124) hide show
  1. package/README.md +103 -1
  2. package/dist/packages/cli/src/args.js +183 -3
  3. package/dist/packages/cli/src/bin.js +76 -19
  4. package/dist/packages/cli/src/cli.js +53 -8
  5. package/dist/packages/cli/src/codex-loop-process.js +19 -2
  6. package/dist/packages/cli/src/commands/advance.js +42 -5
  7. package/dist/packages/cli/src/commands/init.js +2 -0
  8. package/dist/packages/cli/src/commands/office.js +20 -0
  9. package/dist/packages/cli/src/commands/review-show.js +1 -1
  10. package/dist/packages/cli/src/commands/review.js +100 -9
  11. package/dist/packages/cli/src/commands/run.js +115 -0
  12. package/dist/packages/cli/src/commands/task.js +61 -17
  13. package/dist/packages/cli/src/commands/update.js +8 -0
  14. package/dist/packages/cli/src/commands/verify.js +1 -0
  15. package/dist/packages/cli/src/context.js +64 -1
  16. package/dist/packages/cli/src/hook-entry.js +2 -1
  17. package/dist/packages/cli/src/hook-process.js +54 -3
  18. package/dist/packages/cli/src/loop-entry.js +2 -1
  19. package/dist/packages/cli/src/office-entry.js +230 -0
  20. package/dist/packages/cli/src/owned-run-step.js +65 -0
  21. package/dist/packages/cli/src/parallel-deps.js +18 -5
  22. package/dist/packages/cli/src/run-deps.js +321 -0
  23. package/dist/packages/cli/src/run-entry.js +952 -0
  24. package/dist/packages/cli/src/run-setup-entry.js +32 -0
  25. package/dist/packages/cli/src/run-step-entry.js +58 -0
  26. package/dist/packages/cli/src/wizard.js +14 -2
  27. package/dist/runtime/src/adapters/agy/config.js +4 -3
  28. package/dist/runtime/src/adapters/agy/events.js +9 -0
  29. package/dist/runtime/src/adapters/claude/config.js +3 -3
  30. package/dist/runtime/src/adapters/claude/events.js +8 -0
  31. package/dist/runtime/src/adapters/codex/config.js +3 -3
  32. package/dist/runtime/src/adapters/codex/events.js +8 -0
  33. package/dist/runtime/src/adapters/opencode/config.js +5 -4
  34. package/dist/runtime/src/adapters/opencode/events.js +8 -0
  35. package/dist/runtime/src/config/explain.js +11 -0
  36. package/dist/runtime/src/config/hash.js +1 -1
  37. package/dist/runtime/src/config/merge.js +9 -1
  38. package/dist/runtime/src/contracts.js +2 -2
  39. package/dist/runtime/src/hooks/codex-loop.js +4 -0
  40. package/dist/runtime/src/hooks/completion-gate.js +8 -2
  41. package/dist/runtime/src/install/doctor.js +2 -0
  42. package/dist/runtime/src/install/harness.js +44 -1
  43. package/dist/runtime/src/install/plan.js +16 -11
  44. package/dist/runtime/src/install/preauth.js +44 -18
  45. package/dist/runtime/src/install/profiles.js +8 -2
  46. package/dist/runtime/src/install/update.js +1 -0
  47. package/dist/runtime/src/office/collect.js +233 -0
  48. package/dist/runtime/src/office/page.js +873 -0
  49. package/dist/runtime/src/office/scene.js +168 -0
  50. package/dist/runtime/src/office/server.js +139 -0
  51. package/dist/runtime/src/office/sessions.js +115 -0
  52. package/dist/runtime/src/office/snapshot.js +107 -0
  53. package/dist/runtime/src/parallel/finish.js +85 -22
  54. package/dist/runtime/src/parallel/guard.js +12 -2
  55. package/dist/runtime/src/parallel/integrate.js +288 -10
  56. package/dist/runtime/src/parallel/manage.js +4 -1
  57. package/dist/runtime/src/parallel/receipt.js +71 -5
  58. package/dist/runtime/src/parallel/service.js +41 -1
  59. package/dist/runtime/src/review/attestation.js +36 -11
  60. package/dist/runtime/src/review/execute.js +24 -1
  61. package/dist/runtime/src/review/packet.js +35 -1
  62. package/dist/runtime/src/review/runner.js +12 -1
  63. package/dist/runtime/src/review/scope.js +18 -1
  64. package/dist/runtime/src/review/slots.js +35 -10
  65. package/dist/runtime/src/run/authorization.js +60 -0
  66. package/dist/runtime/src/run/budget.js +30 -0
  67. package/dist/runtime/src/run/controls.js +139 -0
  68. package/dist/runtime/src/run/convergence.js +177 -0
  69. package/dist/runtime/src/run/hosts/claude.js +180 -0
  70. package/dist/runtime/src/run/hosts/codex.js +279 -0
  71. package/dist/runtime/src/run/hosts/index.js +4 -0
  72. package/dist/runtime/src/run/hosts/protocol.js +254 -0
  73. package/dist/runtime/src/run/hosts/types.js +1 -0
  74. package/dist/runtime/src/run/hosts/util.js +136 -0
  75. package/dist/runtime/src/run/integration.js +204 -0
  76. package/dist/runtime/src/run/macOS.js +189 -0
  77. package/dist/runtime/src/run/ownership.js +22 -0
  78. package/dist/runtime/src/run/phase.js +37 -0
  79. package/dist/runtime/src/run/planning.js +25 -0
  80. package/dist/runtime/src/run/policy-transition.js +1076 -0
  81. package/dist/runtime/src/run/policy.js +176 -0
  82. package/dist/runtime/src/run/ratchet.js +71 -0
  83. package/dist/runtime/src/run/scheduler.js +260 -0
  84. package/dist/runtime/src/run/service.js +433 -0
  85. package/dist/runtime/src/run/supervisor.js +635 -0
  86. package/dist/runtime/src/run/transport.js +159 -0
  87. package/dist/runtime/src/run/types.js +1 -0
  88. package/dist/runtime/src/run/usage.js +81 -0
  89. package/dist/runtime/src/run/verification.js +64 -0
  90. package/dist/runtime/src/run/worker-gate.js +48 -0
  91. package/dist/runtime/src/schema/acceptance.js +91 -0
  92. package/dist/runtime/src/schema/validate.js +80 -9
  93. package/dist/runtime/src/task/completion.js +14 -2
  94. package/dist/runtime/src/task/contract.js +17 -0
  95. package/dist/runtime/src/task/pin-finding.js +81 -0
  96. package/dist/runtime/src/task/service.js +316 -13
  97. package/dist/runtime/src/task/store.js +57 -7
  98. package/dist/runtime/src/verify/acceptance-coverage.js +85 -0
  99. package/dist/runtime/src/verify/acceptance-protocol.js +252 -0
  100. package/dist/runtime/src/verify/acceptance-replay.js +239 -0
  101. package/dist/runtime/src/verify/adapters/index.js +5 -0
  102. package/dist/runtime/src/verify/adapters/jest.js +142 -0
  103. package/dist/runtime/src/verify/adapters/node-reporter.js +20 -0
  104. package/dist/runtime/src/verify/adapters/node.js +254 -0
  105. package/dist/runtime/src/verify/adapters/pytest.js +188 -0
  106. package/dist/runtime/src/verify/adapters/rust.js +84 -0
  107. package/dist/runtime/src/verify/adapters/shared.js +82 -0
  108. package/dist/runtime/src/verify/adapters/vitest.js +218 -0
  109. package/dist/runtime/src/verify/evidence.js +5 -2
  110. package/dist/runtime/src/verify/service.js +24 -8
  111. package/dist/runtime/src/verify/source-fingerprint.js +1 -1
  112. package/dist/runtime/src/verify/spawn.js +13 -2
  113. package/docs/en/spec/acceptance-and-evidence.md +48 -0
  114. package/docs/en/spec/harness-adapters.md +113 -1
  115. package/docs/en/spec/loop-engineering.md +49 -0
  116. package/docs/zh-TW/spec/acceptance-and-evidence.md +21 -1
  117. package/docs/zh-TW/spec/harness-adapters.md +93 -1
  118. package/docs/zh-TW/spec/loop-engineering.md +22 -0
  119. package/package.json +1 -1
  120. package/schemas/config.schema.json +230 -19
  121. package/schemas/evidence.schema.json +129 -6
  122. package/schemas/task.schema.json +265 -6
  123. package/templates/acceptance/agent_ops_acceptance.rs +36 -0
  124. package/templates/acceptance/agent_ops_pytest.py +46 -0
@@ -0,0 +1,873 @@
1
+ import { sceneModel } from "./scene.js";
2
+ /** Warm cream, oak and sage colors, kept to a single 16-color pixel palette. */
3
+ export const PALETTE = ["#2b241f", "#355a4b", "#6d9275", "#b7c7a3", "#8a5033", "#729ead", "#d9aa72", "#ead8b8",
4
+ "#8e7561", "#6b5d50", "#adc39a", "#f2e5c9", "#e8c99c", "#c86f4a", "#f2c95c", "#fff7e6"];
5
+ /** Each person has a fixed 16-color palette, independent of the room colors. */
6
+ export function avatarPalette(identity, viewer = false) {
7
+ let hash = 0;
8
+ for (const character of identity)
9
+ hash = (Math.imul(hash, 31) + character.charCodeAt(0)) >>> 0;
10
+ const hair = viewer ? ["#302c3b", "#514757", "#756575"] : [
11
+ ["#302c3b", "#514757", "#756575"], ["#51372f", "#80533d", "#b47c51"], ["#745338", "#a37c4f", "#d1ad72"]
12
+ ][hash % 3];
13
+ const skin = viewer ? ["#bf8264", "#e6ae87", "#f5cda4"] : [
14
+ ["#bf8264", "#e6ae87", "#f5cda4"], ["#9b624b", "#c08762", "#e0ae82"], ["#654039", "#96624e", "#bf8c68"]
15
+ ][Math.floor(hash / 9) % 3];
16
+ const clothes = viewer ? ["#bb893b", "#e3b64f", "#f7d878"] : [
17
+ ["#37596d", "#5687a1", "#87b3c7"], ["#934d49", "#c57060", "#e6a087"],
18
+ ["#3b6658", "#61947b", "#92bea1"], ["#625776", "#8c7da4", "#b7a9ca"]
19
+ ][Math.floor(hash / 27) % 4];
20
+ return ["#292633", ...hair, ...skin, ...clothes, "#404958", "#707a8b", "#2e3443", "#626e7f", "#fff1db", "#d58c76"];
21
+ }
22
+ /** Original 32×48 pixel people. Self-contained so the inline client uses this same art. */
23
+ export function avatarSprite(identity, direction = "down", pose = 0, viewer = false) {
24
+ let hash = 0;
25
+ for (const character of identity)
26
+ hash = (Math.imul(hash, 31) + character.charCodeAt(0)) >>> 0;
27
+ const hair = "2", hairLight = "3";
28
+ const style = viewer ? 0 : Math.floor(hash / 3) % 3;
29
+ const skin = "5", skinLight = "6", skinShade = "4", outfit = ["8", "7", "9"];
30
+ const grid = Array.from({ length: 48 }, () => Array(32).fill("."));
31
+ const rect = (x, y, width, height, color) => {
32
+ for (let row = y; row < y + height; row++)
33
+ for (let column = x; column < x + width; column++) {
34
+ if (row >= 0 && row < 48 && column >= 0 && column < 32)
35
+ grid[row][column] = color;
36
+ }
37
+ };
38
+ const stride = pose === 1 ? -1 : pose === 3 ? 1 : 0;
39
+ const side = direction === "left" || direction === "right";
40
+ // Slim trousers, cuffs and shaded shoes; opposite feet alternate during a walk.
41
+ for (let leg = 0; leg < 2; leg++) {
42
+ const x = side ? 12 + leg * 4 : 10 + leg * 7, lift = leg ? -stride : stride;
43
+ rect(x, 35 + lift, 5, 10, "0");
44
+ rect(x + 1, 36 + lift, 3, 7, "a");
45
+ rect(x + 1, 37 + lift, 1, 5, "b");
46
+ rect(x + 1, 43 + lift, 3, 1, "e");
47
+ rect(x - 1, 44 + lift, 6, 3, "c");
48
+ rect(x, 44 + lift, 4, 1, "d");
49
+ rect(x, 46 + lift, 4, 1, "e");
50
+ }
51
+ const bodyX = side ? 11 : 8, bodyW = side ? 11 : 16;
52
+ rect(bodyX + 2, 23, bodyW - 4, 1, "0");
53
+ rect(bodyX, 24, bodyW, 12, "0");
54
+ rect(bodyX + 1, 25, bodyW - 2, 10, outfit[0]);
55
+ rect(bodyX + 1, 33, bodyW - 2, 2, outfit[1]);
56
+ rect(bodyX + bodyW - 3, 27, 2, 6, outfit[1]);
57
+ rect(bodyX + 2, 27, 2, 4, outfit[2]);
58
+ rect(bodyX + 3, 26, bodyW - 6, 1, outfit[2]);
59
+ if (direction !== "up") {
60
+ rect(side ? 18 : 13, 22, side ? 3 : 6, 3, skinShade);
61
+ rect(side ? 18 : 13, 22, side ? 2 : 5, 2, skin);
62
+ rect(side ? 18 : 12, 25, side ? 2 : 8, 1, outfit[1]);
63
+ rect(side ? 18 : 13, 25, side ? 1 : 6, 1, outfit[2]);
64
+ if (!side) {
65
+ rect(18, 29, 3, 3, outfit[1]);
66
+ rect(18, 29, 3, 1, outfit[2]);
67
+ }
68
+ }
69
+ const arms = side ? [12] : [5, 24];
70
+ arms.forEach((x, index) => {
71
+ const swing = stride * (index ? -1 : 1);
72
+ rect(x, 25 + swing, 3, 10, "0");
73
+ rect(x + 1, 26 + swing, 2, 4, outfit[0]);
74
+ rect(x + 1, 29 + swing, 2, 1, outfit[1]);
75
+ rect(x + 1, 30 + swing, 2, 4, skinShade);
76
+ rect(x + 1, 30 + swing, 1, 3, skinLight);
77
+ });
78
+ // Rounded jaw and small dark eyes, with warm skin shading instead of white face blocks.
79
+ rect(9, 9, 14, 1, "0");
80
+ rect(7, 10, 18, 9, "0");
81
+ rect(8, 19, 16, 2, "0");
82
+ rect(10, 21, 12, 1, "0");
83
+ rect(9, 10, 14, 11, skin);
84
+ rect(8, 12, 16, 7, skin);
85
+ rect(22, 13, 2, 6, skinShade);
86
+ rect(19, 20, 3, 1, skinShade);
87
+ rect(10, 13, 3, 1, skinLight);
88
+ rect(6, 15, 2, 4, "0");
89
+ rect(24, 15, 2, 4, "0");
90
+ rect(7, 15, 1, 3, skin);
91
+ rect(24, 15, 1, 3, skin);
92
+ if (side) {
93
+ rect(23, 14, 3, 4, "0");
94
+ rect(23, 15, 2, 2, skin);
95
+ rect(21, 15, 2, 2, "0");
96
+ rect(21, 15, 1, 1, "e");
97
+ rect(23, 19, 1, 1, skinShade);
98
+ }
99
+ else if (direction === "down") {
100
+ rect(10, 14, 3, 1, hairLight);
101
+ rect(19, 14, 3, 1, hairLight);
102
+ rect(11, 15, 2, 2, "0");
103
+ rect(19, 15, 2, 2, "0");
104
+ rect(11, 15, 1, 1, "e");
105
+ rect(19, 15, 1, 1, "e");
106
+ rect(9, 18, 2, 1, skinShade);
107
+ rect(21, 18, 2, 1, skinShade);
108
+ rect(15, 19, 2, 1, skinShade);
109
+ }
110
+ const crown = ["00000000", "00HHHHHHHH00", "0HHHHHHHHHHHH0", "0HHHHHHHHHHHHHH0", "0HHHHhhHHHHHHHHHH0",
111
+ "0HHHhhhhhHHHHHHHHH0", "0HHHHHHhhhhhHHHHHHH0", "0HHHHHHHHHHHHHHHHHH0", "0HHHHHHHHHHHHHHHHHH0"];
112
+ crown.forEach((row, y) => [...row].forEach((pixel, x) => rect(Math.floor((32 - row.length) / 2) + x, y + 2, 1, 1, pixel === "H" ? hair : pixel === "h" ? hairLight : pixel)));
113
+ rect(5, 11, side ? 10 : 4, 5, hair);
114
+ rect(7, 11, side ? 9 : 7, 2, hair);
115
+ rect(11, 12, 2, 2, hair);
116
+ rect(14, 11, 2, 1, hair);
117
+ rect(6, 11, 1, 3, hairLight);
118
+ if (direction === "up") {
119
+ rect(6, 10, 20, 8, hair);
120
+ rect(8, 18, 16, 2, hair);
121
+ rect(10, 20, 12, 1, "0");
122
+ rect(7, 12, 1, 4, hairLight);
123
+ rect(22, 16, 2, 1, hairLight);
124
+ rect(11, 19, 10, 1, hairLight);
125
+ }
126
+ else if (!side) {
127
+ rect(23, 11, 3, 5, hair);
128
+ rect(21, 11, 4, 2, hair);
129
+ rect(20, 12, 2, 1, hair);
130
+ }
131
+ if (style === 1) { // Bob: longer sides and a low back, distinct from the short cut.
132
+ rect(5, 14, side ? 9 : 4, 8, hair);
133
+ if (!side)
134
+ rect(23, 14, 3, 8, hair);
135
+ if (direction === "up")
136
+ rect(8, 18, 16, 5, hair);
137
+ rect(6, 16, 1, 4, hairLight);
138
+ }
139
+ else if (style === 2) { // Curly outline with a raised crown and uneven fringe.
140
+ rect(8, 1, 3, 3, hair);
141
+ rect(14, 1, 3, 3, hair);
142
+ rect(21, 2, 3, 3, hair);
143
+ rect(5, 6, 2, 2, hairLight);
144
+ rect(14, 11, 2, 2, hair);
145
+ rect(19, 10, 2, 2, hair);
146
+ }
147
+ return grid.map(row => (direction === "left" ? row.reverse() : row).join(""));
148
+ }
149
+ /** Every sprite is a character matrix: one hex digit per palette index. */
150
+ export const SPRITES = {
151
+ person: [
152
+ "..................", "....8888888888....", "...888888888888...", "..888cccccc8888...",
153
+ "..88c0cccc0c88....", "..88cccccccc88....", "...88cccccc88.....", "....cccccccc......",
154
+ "...cSSSSSSSSSc....", "..cSSSSSSSSSSSc...", "..cSSSSSSSSSSSc...", "...SSSSSSSSSSSS...",
155
+ "....SSSSSSSSSS....", "....ccSSSScc......", ".....999999.......", "....99999999......",
156
+ "....99....99......", "...999....999.....", "...999....999.....", "..9999....9999...."
157
+ ],
158
+ step: [
159
+ "..................", "....8888888888....", "...888888888888...", "..888cccccc8888...",
160
+ "..88c0cccc0c88....", "..88cccccccc88....", "...88cccccc88.....", "....cccccccc......",
161
+ "...cSSSSSSSSSc....", "..cSSSSSSSSSSSc...", "..cSSSSSSSSSSSc...", "...SSSSSSSSSSSS...",
162
+ "....SSSSSSSSSS....", "....ccSSSScc......", ".....999999.......", "....99999999......",
163
+ ".....99999........", "....999999........", "...999....999.....", "..9999....9999...."
164
+ ],
165
+ desk: [
166
+ "..........88888888..............", "..........81111118..............", "..........81bb1b18..............",
167
+ "..........81111118..............", "..........88888888..............", ".............8888...............",
168
+ "00000000000000000000000000000000", "0eeeeeeeeeeeeeeeeeeeeeeeeeeeeee0", "06666666666666666666666666666660",
169
+ "06666666666666666666666666666660", "00000000000000000000000000000000", "060..........................060",
170
+ "060..........................060", "060..........................060", "060..........................060", "000..........................000"
171
+ ],
172
+ whiteboard: [
173
+ "00000000000000000000000000000000", "0ffffffffffffffffffffffffffffff0", "0ff11f1111fffff44f4fffff22fffff0",
174
+ "0ffffffffffffffffffffffffffffff0", "0ff1111f11ff44444fffff2222fffff0", "0ffffffffffffffffffffffffffffff0",
175
+ "0ff11111ffff444fffff22fffffffff0", "0ffffffffffffffffffffffffffffff0", "0ffffffffffffffffffffffffffffff0",
176
+ "00000000000000000000000000000000", "0777777777777777777777777777777.", "..............0880..............",
177
+ "..............0880..............", "..............0880..............", "..............0880..............", "............00000000............"
178
+ ],
179
+ bench: [
180
+ "......b.........a.........d.....", ".....0b0.......0a0.......0d0....", ".....0b0.......0a0.......0d0....",
181
+ "....0bbb0.....0aaa0.....0ddd0...", "...0bbbbb0...0aaaaa0...0ddddd0..", "...0000000...0000000...0000000..",
182
+ "00000000000000000000000000000000", "0ffffffffffffffffffffffffffffff0", "07777777777777777777777777777770",
183
+ "00000000000000000000000000000000", "070888888888888888888888888880.0", "070..........................070",
184
+ "070..........................070", "070..........................070", "070..........................070", "000..........................000"
185
+ ],
186
+ table: [
187
+ "................................", "....000000000000000000000000....", "..0066666666666666666666666600..",
188
+ ".06666666666666666666666666666..", "0666666ffff666666666ffff6666660.", "0666666ffff666666666ffff6666660.",
189
+ "0666666666666666666666666666660.", ".066666666666666666666666666660.", "..0066666666666666666666666600..",
190
+ "....000000000000000000000000....", "......060..............060......", "......060..............060......",
191
+ "......060..............060......", "......060..............060......", "......000..............000......", "................................"
192
+ ],
193
+ rug: [
194
+ "000000000000000000000000", "0aaaaaaaaaaaaaaaaaaaaaa0", "0a22222222222222222222a0", "0a22222222222222222222a0",
195
+ "0a22222222222222222222a0", "0a22222222222222222222a0", "0a22222222222222222222a0", "0a22222222222222222222a0",
196
+ "0a22222222222222222222a0", "0a22222222222222222222a0", "0a22222222222222222222a0", "0a22222222222222222222a0",
197
+ "0a22222222222222222222a0", "0a22222222222222222222a0", "0aaaaaaaaaaaaaaaaaaaaaa0", "000000000000000000000000"
198
+ ],
199
+ chair: [
200
+ "....000000....", "...06666660...", "...06666660...", "...06666660...", "....000000....", "....066660....",
201
+ "....066660....", "....066660....", "....000000....", "...000..000...", "...060..060...", "...060..060...",
202
+ "..0000..0000..", ".............."
203
+ ],
204
+ window: [
205
+ "000000000000000000000000", "0bbbbbbbbbbbbbbbbbbbbbb0", "0bffffffffffffffffffffb0", "0bffffffff0000ffffffffb0",
206
+ "0bffffffff0000ffffffffb0", "0bffffffffffffffffffffb0", "0bffffffffffffffffffffb0", "000000000000000000000000"
207
+ ],
208
+ door: [
209
+ "0000000000000000", "0666666666666660", "0644444444444460", "0646666664666460", "0646666664666460",
210
+ "0646666664666460", "0644444444444460", "0646666664666460", "0646666664666460", "06466666646ee460",
211
+ "06466666646ee460", "0646666664666460", "0644444444444460", "0646666664666460", "0646666664666460",
212
+ "0646666664666460", "0646666664666460", "0644444444444460", "0646666664666460", "0646666664666460",
213
+ "0646666664666460", "0644444444444460", "0666666666666660", "0000000000000000", "0aaaaaaaaaaaaaa0",
214
+ "0a2a2a2a2a2a2aa0", "0aaaaaaaaaaaaaa0", "0000000000000000"
215
+ ],
216
+ shelf: [
217
+ "00000000000000000000000000000000", "06666666666666666666666666666660", "06............................60",
218
+ "06............................60", "06............................60", "06............................60", "06............................60",
219
+ "06............................60", "06............................60", "06666666666666666666666666666660", "06............................60",
220
+ "06............................60", "06............................60", "06............................60", "06............................60",
221
+ "06............................60", "06............................60", "06666666666666666666666666666660", "00000000000000000000000000000000"
222
+ ],
223
+ book: ["SSS", "SfS", "SSS", "SSS", "SSS", "SSS", "SSS"],
224
+ clock: [
225
+ ".....000000.....", "...00ffffff00...", "..0ffff0ffff0...", ".0fffffffffff0..", ".0fffffffffff0..",
226
+ "0ffffffffffffff0", "0f0ffffffffff0f0", "0ffffffffffffff0", "0ffffffffffffff0", ".0fffffffffff0..",
227
+ ".0fffffffffff0..", "..0ffff0ffff0...", "...00ffffff00...", ".....000000.....", "................", "................"
228
+ ],
229
+ plant: [
230
+ "......a..a......", "...a..aa.a..a...", "....aa2aa2aa....", "..a.2aa22aa2.a..", "...aa2a22a2aa...",
231
+ "....2aa22aa2....", ".....2a22a2.....", "......2222......", ".....000000.....", ".....066660.....",
232
+ ".....064460.....", "......0660......", "......0660......", "......0000......", "................", "................"
233
+ ],
234
+ paper: ["000000", "0ffff0", "000000"],
235
+ alert: [".00.", "0ee0", "0ee0", "0ee0", "0ee0", ".00.", "0ee0", ".00."],
236
+ floor: [
237
+ "7777777777777777", "6666666666666666", "666666b666666666", "66666666666b6666", "7777777777777777",
238
+ "6666666666666666", "6666b66666666666", "6666666666666666", "7777777777777777", "6666666666666666",
239
+ "6666666666666b66", "6666666666666666", "7777777777777777", "666666b666666666", "6666666666666666",
240
+ "6666666666666666"
241
+ ],
242
+ wall: [
243
+ "7777777777777777", "7777777777777777", "7777777777777777", "7777777777777777", "7777777777777777",
244
+ "7777777777777777", "7777777777777777", "7777777777777777", "7777777777777777", "7777777777777777",
245
+ "7777777777777777", "7777777777777777", "7777777777777777", "7777777777777777", "7777777777777777",
246
+ "7777777777777777"
247
+ ],
248
+ baseboard: ["bbbbbbbbbbbbbbbb", "0000000000000000"]
249
+ };
250
+ const STYLE = String.raw `
251
+ :root{color-scheme:light}
252
+ *{box-sizing:border-box}
253
+ html,body{width:100%;height:100%;overflow:hidden}
254
+ body{margin:0;background:#2b241f;color:#2b241f;font:clamp(12px,1.25vw,18px)/1.25 "Courier New",monospace}
255
+ button{font:inherit;color:inherit}
256
+ #app{height:100dvh;display:flex;flex-direction:column;overflow:hidden;background:#ead8b8}
257
+ #header{min-height:54px;display:flex;align-items:center;gap:12px;padding:8px 18px;background:#2b241f;color:#fff7e6;border-bottom:4px solid #8a5033;white-space:nowrap}
258
+ #brand{font-weight:bold;font-size:clamp(16px,2vw,26px);letter-spacing:.02em}
259
+ #crumb{flex:1;overflow:hidden;text-overflow:ellipsis;color:#f2e5c9}
260
+ #header button{border:2px solid #f2e5c9;background:#6d9275;color:#fff7e6;padding:5px 9px;cursor:pointer;box-shadow:2px 2px 0 #000}
261
+ #header button:hover,#header button:focus-visible{background:#adc39a;color:#2b241f;outline:3px solid #f2c95c;outline-offset:2px}
262
+ #header button[hidden]{display:none}
263
+ #live{color:#adc39a}
264
+ #wrap{min-height:0;flex:1;display:flex;flex-direction:column;align-items:center;justify-content:space-between;gap:4px;padding:4px 8px;overflow:hidden;background:#ead8b8}
265
+ #office{display:block;max-width:100%;max-height:none;width:auto;height:auto;image-rendering:pixelated;image-rendering:crisp-edges;border:4px solid #6b5d50;box-shadow:6px 6px 0 #8a5033;outline:none;cursor:pointer}
266
+ #office:focus-visible{outline:4px solid #f2c95c;outline-offset:4px}
267
+ #dialogue{width:100%;min-height:2.25em;padding:6px 12px;background:#fff7e6;border:2px solid #8a5033;color:#2b241f;text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
268
+ #room-nav{width:100%;display:flex;justify-content:center;align-items:center;gap:5px;min-height:28px;overflow:hidden}
269
+ #room-nav button{max-width:18ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border:2px solid #8a5033;background:#f2e5c9;padding:3px 7px;cursor:pointer}
270
+ #room-nav button[aria-current=true]{background:#6d9275;color:#fff7e6;border-color:#2b241f}
271
+ #room-nav button:focus-visible{outline:3px solid #f2c95c;outline-offset:2px}
272
+ #status{position:fixed;z-index:4;inset:6dvh 50% auto auto;transform:translateX(50%);width:min(92vw,680px);max-height:88dvh;overflow:hidden;display:flex;flex-direction:column;gap:4px;padding:16px;background:#fff7e6;border:4px solid #6b5d50;box-shadow:8px 8px 0 #2b241f;color:#2b241f}
273
+ #status[hidden]{display:none}
274
+ #status h2{margin:0 0 8px;color:#355a4b;font-size:1.25em}
275
+ #status .dialog-body{min-height:0;overflow:hidden}
276
+ #status p{margin:5px 0;overflow-wrap:anywhere;white-space:pre-wrap}
277
+ #status ul{margin:5px 0;padding-left:22px}
278
+ #status li{overflow-wrap:anywhere}
279
+ #status .actions{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:8px;margin-top:12px}
280
+ #status>.actions{flex:0 0 auto}
281
+ #status code{display:inline-block;max-width:calc(100% - 74px);overflow-wrap:anywhere;white-space:pre-wrap;background:#f2e5c9;padding:2px 4px}
282
+ #status .dialog-pager{border-top:2px solid #d9aa72;padding-top:6px}
283
+ #status button{border:2px solid #2b241f;background:#d9aa72;padding:5px 10px;cursor:pointer}
284
+ #status button:focus-visible{outline:3px solid #f2c95c;outline-offset:2px}
285
+ .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
286
+ @media(max-width:760px){#header{flex-wrap:wrap;gap:6px;padding:6px 8px}#brand{font-size:16px}#header button{order:1;font-size:12px;padding:3px 5px}#crumb{min-width:70px}#live{font-size:12px}}
287
+ @media (prefers-reduced-motion:reduce){*{scroll-behavior:auto!important}}
288
+ `;
289
+ const CLIENT = String.raw `
290
+ var PALETTE = __PALETTE__, SPRITES = __SPRITES__, T = 8, S = 2, U = 1, dpr = 1;
291
+ var sceneModel = __SCENE__;
292
+ var avatarSprite = __AVATAR__, avatarPalette = __AVATAR_COLORS__, avatarImages = {};
293
+ var canvas = document.getElementById("office"), ctx = canvas.getContext("2d"), app = document.getElementById("app");
294
+ ctx.imageSmoothingEnabled = false;
295
+ var dialogueBox = document.getElementById("dialogue"), statusBox = document.getElementById("status"), roomNav = document.getElementById("room-nav"), wrap = document.getElementById("wrap");
296
+ var crumb = document.getElementById("crumb"), backButton = document.getElementById("back"), recentButton = document.getElementById("recent");
297
+ var languageButton = document.getElementById("language"), live = document.getElementById("live");
298
+ var snapshot = null, model = null, positions = {}, roomCanvases = {}, frame = 0, lineIndex = 0, typed = 0, offline = false;
299
+ var mode = "overview", selectedKey = null, showRecent = false, detailPage = 0, dialogClose = null, detailPreviousFocus = null, detailTarget = null;
300
+ var detailSignature = "";
301
+ var navPage = 0, navSignature = "", NAV_PAGE_SIZE = 8;
302
+ var supervisor = {x:43, y:19, targetX:43, targetY:19}, supervisorReady = false, hallwayReturn = null;
303
+ var roomSupervisor = {x:32, y:34, targetX:32, targetY:34};
304
+ var reducedMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
305
+ var STRINGS = {
306
+ en: {viewer:"You", overview:"Office overview", rooms:"rooms", people:"people", recent:"Recently completed", recentOn:"Hide completed", back:"Back to overview", connected:"● Connected", offline:"○ Reconnecting", quiet:"The office is quiet. No agent is at work.", waiting:"waiting for your answer", enter:"Enter room", close:"Close", previous:"Previous", next:"Next", page:"Page", details:"Details", progress:"Progress", verify:"Verify", review:"Review", pending:"Awaiting answer", task:"Task", status:"Status", phase:"Phase", host:"Host", now:"Now", owner:"Owner", currentWork:"Current work", question:"Pending question", questions:"Questions", workList:"Work list", clickWork:"Click for work", keyboardWork:"Press L for work list", keyboardPhase:"Press 1-5 for phase work", keyboardQuestions:"Press Q for questions", noPeople:"No known work is in this phase", noQuestions:"No pending questions", files:"Changed files", commands:"Commands", copy:"Copy", copied:"Copied", unknown:"unknown", unassigned:"unassigned", phases:{planning:"Planning", implementing:"Implementing", verifying:"Verifying", reviewing:"Reviewing", integrating:"Integrating", unknown:"Unassigned"}, statuses:{active:"active", idle:"idle", running:"running", reviewing:"reviewing", verifying:"verifying", blocked:"blocked", delivered:"delivered", complete:"complete", unknown:"unknown", unassigned:"unassigned", pending:"pending"}},
307
+ zh: {viewer:"你", overview:"辦公室總覽", rooms:"個房間", people:"位成員", recent:"最近完成", recentOn:"隱藏已完成", back:"返回總覽", connected:"● 已連線", offline:"○ 重新連線中", quiet:"辦公室很安靜,目前沒有成員工作。", waiting:"等待你的回覆", enter:"進入房間", close:"關閉", previous:"上一頁", next:"下一頁", page:"頁", details:"詳細資料", progress:"進度", verify:"驗證", review:"審查", pending:"待回覆", task:"任務", status:"狀態", phase:"階段", host:"主機", now:"目前", owner:"負責人", currentWork:"目前工作", question:"待回覆問題", questions:"問題", workList:"工作清單", clickWork:"點擊查看工作", keyboardWork:"按 L 開啟工作清單", keyboardPhase:"按 1-5 查看階段工作", keyboardQuestions:"按 Q 查看問題", noPeople:"此階段目前沒有已知工作", noQuestions:"目前沒有待回覆問題", files:"變更檔案", commands:"指令", copy:"複製", copied:"已複製", unknown:"未知", unassigned:"未分配", phases:{planning:"規劃", implementing:"開發", verifying:"驗證", reviewing:"審查", integrating:"整合", unknown:"未分配"}, statuses:{active:"進行中", idle:"閒置", running:"工作中", reviewing:"審查中", verifying:"驗證中", blocked:"受阻", delivered:"已交付", complete:"已完成", unknown:"未知", unassigned:"未分配", pending:"待回覆"}}
308
+ };
309
+ function getLanguage(){
310
+ try { var cookie = document.cookie.split(";").map(function(part){ return part.trim().split("="); }).find(function(pair){ return pair[0] === "agent-office-language"; }); if (cookie && (cookie[1] === "en" || cookie[1] === "zh")) return cookie[1]; } catch (_) {}
311
+ try { var stored = localStorage.getItem("agent-office-language"); if (stored === "en" || stored === "zh") return stored; } catch (_) {}
312
+ return (navigator.language || "en").toLowerCase().indexOf("zh") === 0 ? "zh" : "en";
313
+ }
314
+ var lang = getLanguage();
315
+ function t(key){ return STRINGS[lang][key] || key; }
316
+ function phaseLabel(phase){ return STRINGS[lang].phases[phase] || phase; }
317
+ function statusText(status){ return STRINGS[lang].statuses[status] || safeText(status); }
318
+ function roleText(kind){return (lang==='zh'?{coordinator:'統籌',worker:'成員',reviewer:'審查',visitor:'訪客'}:{coordinator:'Lead',worker:'Agent',reviewer:'Review',visitor:'Guest'})[kind]||kind;}
319
+ function outcomeText(value){ return value === "pending" ? (lang === "zh" ? "尚未完成" : "pending") : safeText(value); }
320
+ function localizeText(value){
321
+ var text = safeText(value);
322
+ if (text === "The office is quiet. No agent is at work.") return t("quiet");
323
+ if (text === "waiting for your answer") return t("waiting");
324
+ if (text === "reviewing") return phaseLabel("reviewing");
325
+ if (text === "planning" || text === "implementing" || text === "verifying" || text === "integrating") return phaseLabel(text);
326
+ if (STRINGS.en.statuses[text]) return statusText(text);
327
+ if(lang==='zh')text=text.replace(/^writing tests$/u,'撰寫測試').replace(/^writing docs$/u,'撰寫文件').replace(/^editing /u,'編輯 ');
328
+ return text.replace(/waiting for your answer/gu, t("waiting"));
329
+ }
330
+ function saveLanguage(){ try { localStorage.setItem("agent-office-language", lang); } catch (_) {} try { document.cookie = "agent-office-language=" + lang + "; Max-Age=31536000; Path=/; SameSite=Strict"; } catch (_) {} }
331
+ function safeText(value, fallback){ return typeof value === "string" && value.length ? value : (fallback || t("unknown")); }
332
+ function short(value, limit){ value = safeText(value); limit = limit || 22; return value.length > limit ? value.slice(0, limit - 1) + "…" : value; }
333
+ function draw(name, x, y, shirt){
334
+ var rows = SPRITES[name]; if (!rows) return;
335
+ var scale = name === "floor" || name === "wall" || name === "baseboard" ? 1 : S;
336
+ for (var r = 0; r < rows.length; r++) for (var c = 0; c < rows[r].length; c++) {
337
+ var ch = rows[r][c]; if (ch === ".") continue;
338
+ ctx.fillStyle = ch === "S" ? shirt : PALETTE[parseInt(ch, 16)]; ctx.fillRect(x + c * scale, y + r * scale, scale, scale);
339
+ }
340
+ }
341
+ function avatarScale(surface){return Math.max(1,Math.round(Math.min(surface.width/(model.roomCols*T),surface.height/(model.roomRows*T))*T/9));}
342
+ function advanceAvatar(state, tx, ty, speed){
343
+ var dx=tx-state.x,dy=ty-state.y;
344
+ if(Math.abs(dx)>.05||Math.abs(dy)>.05)state.direction=Math.abs(dx)>Math.abs(dy)?(dx<0?'left':'right'):(dy<0?'up':'down');
345
+ if(reducedMotion){state.x=tx;state.y=ty;}else{state.x+=Math.max(-speed,Math.min(speed,dx));state.y+=Math.max(-speed,Math.min(speed,dy));}
346
+ state.walking=!reducedMotion&&(Math.abs(state.x-tx)>.05||Math.abs(state.y-ty)>.05);
347
+ }
348
+ function drawAvatar(identity, state, x, y, scale, viewer){
349
+ var pose=state.walking&&!reducedMotion?Math.floor(frame/7)%4:0,direction=state.direction||'down',key=JSON.stringify([identity,direction,pose,!!viewer]);
350
+ var image=avatarImages[key];
351
+ if(!image){
352
+ image=document.createElement('canvas');image.width=32;image.height=48;var paint=image.getContext('2d'),rows=avatarSprite(identity,direction,pose,!!viewer),colors=avatarPalette(identity,!!viewer);
353
+ for(var row=0;row<rows.length;row++)for(var col=0;col<rows[row].length;col++){var pixel=rows[row][col];if(pixel!=='.'){paint.fillStyle=colors[parseInt(pixel,16)];paint.fillRect(col,row,1,1);}}
354
+ avatarImages[key]=image;
355
+ }
356
+ x=Math.round(x);y=Math.round(y);
357
+ ctx.fillStyle='rgba(43,36,31,.18)';ctx.fillRect(x+8*scale,y+46*scale,17*scale,2*scale);
358
+ ctx.drawImage(image,x,y,32*scale,48*scale);
359
+ }
360
+ function propSize(kind){ var s = SPRITES[kind] || [""]; return {w:s[0].length * S, h:s.length * S}; }
361
+ function roomMetrics(width,height){var rx=width/model.roomCols,ry=height/model.roomRows;return {rx:rx,ry:ry,unit:Math.max(.5,Math.min(rx,ry)/8)};}
362
+ function propGeometry(prop,width,height){
363
+ var metrics=roomMetrics(width,height),rawX=prop.x*metrics.rx,rawY=prop.y*metrics.ry;
364
+ // layout sizes keep the existing scale decisions; visual sizes follow the
365
+ // actual rounded pixel art drawn by furniture() so clicks never drift away.
366
+ var layoutWidths={window:58,plant:32,shelf:28,door:24},visualWidths={desk:75,bench:75,table:65,window:58,plant:32,shelf:28,door:24,whiteboard:65,chair:21},heights={desk:45,bench:45,table:54,window:31,whiteboard:49,shelf:61,plant:31,chair:32,door:46};
367
+ var visualHeights={desk:45,bench:49,table:54,window:34,plant:34,shelf:61,door:46,whiteboard:49,chair:32},visualXOffsets={window:-3},visualYOffsets={bench:-4,window:-3,plant:-3};
368
+ var layoutWidth=layoutWidths[prop.kind]||75,visualWidth=visualWidths[prop.kind]||layoutWidth,artHeight=heights[prop.kind]||45;
369
+ var rawScale=Math.max(.3,Math.min(metrics.unit*1.85,(height-rawY-2*metrics.unit)/artHeight,(width-rawX-2*metrics.unit)/layoutWidth)),scale=Math.max(1,Math.round(rawScale)),visualHeight=visualHeights[prop.kind]||artHeight,visualY=Math.round(rawY+(visualYOffsets[prop.kind]||0)*scale);
370
+ return {x:Math.round(rawX+(visualXOffsets[prop.kind]||0)*scale),y:visualY,rawX:rawX,rawY:rawY,width:visualWidth*scale,height:visualHeight*scale,scale:scale,rawScale:rawScale,unit:metrics.unit};
371
+ }
372
+ function phaseAreaGeometry(area,width,height){var metrics=roomMetrics(width,height);return {x:area.x*metrics.rx,y:area.y*metrics.ry,width:area.width*metrics.rx,height:area.height*metrics.ry,rx:metrics.rx,ry:metrics.ry,unit:metrics.unit};}
373
+ function progressBoardGeometry(width,height){var metrics=roomMetrics(width,height);return {x:24*metrics.rx,y:metrics.ry,width:27*metrics.rx,height:Math.max(7*metrics.ry,66*metrics.unit),unit:metrics.unit};}
374
+ function pendingBoardGeometry(width,height){var board=progressBoardGeometry(width,height),inset=4*board.unit;return {x:board.x+inset,y:board.y+50*board.unit,width:Math.max(0,board.width-inset*2),height:Math.max(0,Math.min(15*board.unit,board.height-50*board.unit)),unit:board.unit};}
375
+ function surfaceForFloor(floor){if(mode==='room')return {x:0,y:0,width:canvas.width,height:canvas.height};var box=roomBox(floor);return {x:Math.round(box.x*T),y:Math.round(box.y*T),width:Math.round(box.width*T),height:Math.round(box.height*T)};}
376
+ function canvasPoint(event){var rect=canvas.getBoundingClientRect();return {x:(event.clientX-rect.left)*canvas.width/rect.width,y:(event.clientY-rect.top)*canvas.height/rect.height};}
377
+ function inside(point,rect){return point.x>=rect.x&&point.x<rect.x+rect.width&&point.y>=rect.y&&point.y<rect.y+rect.height;}
378
+ function fillText(value, x, y, size, color){ ctx.fillStyle = color || PALETTE[0]; ctx.font = "bold " + Math.max(5, Math.round((size || 10) * U)) + "px monospace"; ctx.textBaseline = "top"; ctx.fillText(short(value, 36), x, y); }
379
+ function roomByKey(key){ if (!model) return null; for (var i = 0; i < model.floors.length; i++) if (model.floors[i].key === key) return model.floors[i]; return null; }
380
+ function visibleRooms(){ if (!model) return []; return model.floors.filter(function(f){ return showRecent || !f.completedAt; }); }
381
+ function activeRoom(){ return selectedKey ? roomByKey(selectedKey) : null; }
382
+ function hasRecent(){ return !!(model && model.floors.some(function(f){ return !!f.completedAt; })); }
383
+ function setCanvasSize(width, height){
384
+ width = Math.max(1, Math.round(width)); height = Math.max(1, Math.round(height));
385
+ if (canvas.width !== width) canvas.width = width;
386
+ if (canvas.height !== height) canvas.height = height;
387
+ ctx.imageSmoothingEnabled = false;
388
+ }
389
+ function drawBackdrop(cols, rows, room){
390
+ if (!room) setCanvasSize(cols * T, rows * T);
391
+ var width = canvas.width, height = canvas.height, unit = Math.max(1, Math.min(width / 576, height / 320)), wallHeight = Math.round(height * .18);
392
+ ctx.fillStyle = PALETTE[15]; ctx.fillRect(0, 0, width, wallHeight);
393
+ ctx.fillStyle = PALETTE[12]; ctx.fillRect(0, wallHeight, width, height - wallHeight);
394
+ var plankH = 12 * unit, plankW = 64 * unit;
395
+ for (var row = 0, y = wallHeight; y < height; row++, y += plankH) {
396
+ ctx.fillStyle = row % 3 === 0 ? PALETTE[7] : PALETTE[12]; ctx.fillRect(0, y, width, plankH - unit);
397
+ for (var x = -(row % 2) * plankW / 2; x < width; x += plankW) {
398
+ ctx.fillStyle = PALETTE[6]; ctx.fillRect(x, y, unit, plankH);
399
+ ctx.fillStyle = PALETTE[7]; ctx.fillRect(x + 8 * unit, y + 4 * unit, 24 * unit, unit);
400
+ }
401
+ }
402
+ ctx.fillStyle = PALETTE[6]; ctx.fillRect(0, wallHeight - 3 * unit, width, 3 * unit);
403
+ ctx.fillStyle = 'rgba(43,36,31,.12)'; ctx.fillRect(0, wallHeight, width, 4 * unit);
404
+ ctx.fillStyle = PALETTE[9]; ctx.fillRect(0, height - 2 * unit, width, 2 * unit);
405
+ }
406
+ // Furniture stays square-pixeled while its placement follows the room size.
407
+ function furniture(kind, x, y, unit){
408
+ ctx.save(); ctx.translate(Math.round(x), Math.round(y)); unit=Math.max(1,Math.round(unit));ctx.scale(unit, unit);
409
+ function r(x,y,w,h,c){ctx.fillStyle=PALETTE[c];ctx.fillRect(x,y,w,h);}
410
+ function monitor(x,y){r(x,y,22,16,9);r(x+1,y+1,20,13,0);r(x+3,y+3,16,9,5);for(var i=0;i<3;i++){r(x+4,y+4+i*2,4+i*3,1,i%2?14:3);}r(x+10,y+16,2,3,9);r(x+6,y+19,10,1,9);}
411
+ function cup(x,y){r(x,y,5,6,15);r(x+1,y,3,1,4);r(x+5,y+1,2,3,8);}
412
+ function drawer(x,y){r(x,y,17,19,9);r(x+1,y+1,15,7,8);r(x+1,y+10,15,7,8);r(x+6,y+3,5,1,15);r(x+6,y+12,5,1,15);}
413
+ if(kind==='desk'||kind==='bench'){
414
+ r(2,31,72,5,8);drawer(4,28);drawer(52,28);r(1,17,74,13,4);r(2,17,72,9,6);r(2,17,72,2,7);r(25,30,3,15,9);r(70,30,3,15,9);
415
+ monitor(8,0);monitor(34,1);r(15,23,23,3,9);for(var k=0;k<6;k++)r(17+k*3,24,2,1,15);cup(61,19);
416
+ r(43,20,11,5,15);r(45,21,7,1,8);r(45,23,6,1,8);r(66,7,6,9,9);r(67,8,4,6,3);
417
+ if(kind==='bench'){r(2,-4,72,3,9);for(var j=0;j<7;j++){r(5+j*9,-1,2,8,8);r(4+j*9,1,4,3,j%2?14:5);}r(59,0,10,16,9);r(61,2,6,5,3);r(61,9,6,4,5);}
418
+ } else if(kind==='table'){
419
+ r(6,0,47,12,1);r(8,1,43,9,2);r(2,12,63,30,4);r(3,12,61,26,6);r(3,12,61,2,7);r(7,42,3,12,9);r(55,42,3,12,9);
420
+ r(10,19,14,12,15);r(30,22,13,12,15);for(var l=0;l<4;l++){r(12,21+l*2,10,1,8);r(32,24+l*2,9,1,8);}cup(51,19);monitor(41,0);
421
+ } else if(kind==='window'){
422
+ r(0,0,52,28,8);r(2,2,48,23,5);r(4,4,44,19,3);r(4,4,44,13,5);r(8,6,15,2,15);r(30,9,10,2,15);r(3,24,48,4,6);r(24,2,3,23,15);r(2,2,48,2,15);r(2,12,48,2,15);
423
+ r(-2,-3,56,4,9);r(0,-2,52,2,7);r(-3,27,58,4,4);r(-2,27,56,2,6);r(36,20,7,7,4);r(35,17,9,5,2);r(39,14,4,6,1);
424
+ } else if(kind==='whiteboard'){
425
+ r(0,0,65,38,9);r(2,2,61,33,15);r(4,4,57,1,7);r(5,38,3,9,9);r(57,38,3,9,9);r(2,47,9,2,9);r(54,47,9,2,9);
426
+ for(var n=0;n<6;n++){var bx=7+n%3*18,by=9+Math.floor(n/3)*16;r(bx,by,12,9,n%2?3:14);r(bx+2,by+2,8,1,8);r(bx+2,by+5,6,1,8);if(n<5){r(bx+13,by+5,4,1,8);}}
427
+ } else if(kind==='shelf'){
428
+ r(0,0,28,58,4);r(2,2,24,54,9);for(var shelf=0;shelf<3;shelf++){r(2,17+shelf*18,24,2,6);for(var book=0;book<5;book++){r(4+book*4,4+shelf*18+(book%2)*2,3,12-(book%2)*2,[2,5,13,7,3][book]);r(5+book*4,6+shelf*18,1,6,15);}}
429
+ r(0,58,28,3,8);r(3,44,9,11,6);r(14,44,10,11,6);r(6,46,4,1,15);r(17,46,4,1,15);
430
+ } else if(kind==='plant'){
431
+ r(9,18,16,13,8);r(10,18,14,3,6);r(11,21,12,9,7);r(14,4,3,15,1);r(5,7,11,5,2);r(1,5,9,4,3);r(17,3,12,5,2);r(23,0,8,5,3);r(14,-3,5,10,2);r(6,-2,9,5,1);r(20,10,10,5,1);r(3,13,10,4,2);r(11,11,11,5,3);
432
+ } else if(kind==='chair'){
433
+ r(2,0,18,13,1);r(3,1,16,10,2);r(2,13,18,9,2);r(3,14,16,5,3);r(10,22,2,8,9);r(3,29,18,2,9);r(2,28,3,4,9);r(18,28,3,4,9);
434
+ } else if(kind==='door'){
435
+ r(0,0,24,46,8);r(2,2,20,42,4);r(4,4,16,38,6);r(5,5,14,13,7);r(5,22,14,18,7);r(15,21,3,2,14);r(5,28,14,8,1);r(8,31,8,2,15);r(13,29,2,6,15);
436
+ }
437
+ ctx.restore();
438
+ }
439
+ function drawRoomBase(floor){
440
+ drawBackdrop(model.roomCols, model.roomRows, floor);
441
+ var metrics=roomMetrics(canvas.width,canvas.height),rx=metrics.rx,ry=metrics.ry,unit=metrics.unit,labelUnit = Math.min(U, Math.max(1, unit * 1.25));
442
+ var savedU = U; U = labelUnit;
443
+ // Sage rugs and oak furniture give each phase a real place in the room.
444
+ floor.phaseAreas.forEach(function(a){
445
+ var area=phaseAreaGeometry(a,canvas.width,canvas.height);
446
+ if(a.phase==='implementing'||a.phase==='reviewing'){
447
+ var x=area.x,y=area.y+area.ry,w=area.width,h=area.height-area.ry;
448
+ ctx.fillStyle=PALETTE[3];ctx.fillRect(x,y,w,h);ctx.strokeStyle=PALETTE[2];ctx.lineWidth=2*unit;ctx.strokeRect(x+3*unit,y+3*unit,w-6*unit,h-6*unit);
449
+ }
450
+ });
451
+ floor.props.filter(function(p){return p.kind!=='rug'&&p.kind!=='clock';}).forEach(function(p){
452
+ var geometry=propGeometry(p,canvas.width,canvas.height),px=geometry.rawX,py=geometry.rawY,artUnit=geometry.rawScale;
453
+ if(p.kind!=='window'){ctx.fillStyle='rgba(43,36,31,.14)';ctx.fillRect(px+4*unit,py+28*unit,45*unit,6*unit);}
454
+ furniture(p.kind,px,py,artUnit);
455
+ if(p.kind==='shelf')for(var book=0;book<Math.min(5,floor.books.lit);book++){ctx.fillStyle=PALETTE[14];ctx.fillRect(px+(4+book*4)*Math.round(artUnit),py+4*Math.round(artUnit),3*Math.round(artUnit),2*Math.round(artUnit));}
456
+ if(p.kind==='desk')for(var paper=0;paper<Math.min(4,floor.papers[p.key]||0);paper++){ctx.fillStyle=PALETTE[15];ctx.fillRect(px+44*artUnit,py+(20-paper)*artUnit,9*artUnit,2*artUnit);}
457
+ });
458
+ floor.phaseAreas.forEach(function(a){var area=phaseAreaGeometry(a,canvas.width,canvas.height),ax=area.x,ay=area.y;ctx.fillStyle=PALETTE[15];ctx.fillRect(ax,ay,Math.min(area.width,72*labelUnit),13*labelUnit);fillText(phaseLabel(a.phase),ax+4*labelUnit,ay+2*labelUnit,9,PALETTE[1]);});
459
+ var titleX=2*rx,titleY=ry,w=19*rx;
460
+ ctx.fillStyle=PALETTE[4];ctx.fillRect(titleX,titleY,w,4*ry);ctx.fillStyle=PALETTE[6];ctx.fillRect(titleX+2*unit,titleY+2*unit,w-4*unit,4*ry-4*unit);
461
+ fillText(short(floor.title,20),titleX+6*unit,titleY+6*unit,11);fillText(statusText(floor.status),titleX+6*unit,titleY+22*unit,8,PALETTE[4]);
462
+ var b=floor.board,board=progressBoardGeometry(canvas.width,canvas.height),bx=board.x,by=board.y,bw=board.width,bh=board.height;
463
+ ctx.fillStyle=PALETTE[9];ctx.fillRect(bx,by,bw,bh);ctx.fillStyle=PALETTE[15];ctx.fillRect(bx+2*unit,by+2*unit,bw-4*unit,bh-4*unit);
464
+ fillText('▣ '+t('clickWork'),bx+6*unit,by+5*unit,8,PALETTE[1]);
465
+ fillText(t('progress')+' '+b.passed+'/'+(b.total||'?'),bx+6*unit,by+16*unit,10,PALETTE[1]);
466
+ ctx.fillStyle=PALETTE[3];ctx.fillRect(bx+6*unit,by+29*unit,bw-12*unit,5*unit);ctx.fillStyle=PALETTE[2];ctx.fillRect(bx+6*unit,by+29*unit,(bw-12*unit)*Math.min(1,b.total?b.passed/b.total:0),5*unit);
467
+ fillText(t('verify')+' '+outcomeText(b.verify)+' '+t('review')+' '+outcomeText(b.review),bx+6*unit,by+40*unit,8,PALETTE[4]);
468
+ var pendingRow=pendingBoardGeometry(canvas.width,canvas.height);
469
+ if(b.pending)fillText('! '+t('pending')+' '+b.pending,pendingRow.x+2*unit,pendingRow.y+3*unit,8,PALETTE[13]);
470
+ var oldS=S;S=Math.max(1,Math.round(unit));var cx=52*rx,cy=ry;draw('clock',cx,cy);
471
+ ctx.strokeStyle=PALETTE[13];ctx.lineWidth=2*S;ctx.beginPath();ctx.moveTo(cx+8*S,cy+7*S);var angle=-Math.PI/2+2*Math.PI*floor.clock;ctx.lineTo(cx+8*S+Math.cos(angle)*5*S,cy+7*S+Math.sin(angle)*5*S);ctx.stroke();S=oldS;
472
+ U=savedU;
473
+ ctx.strokeStyle=PALETTE[9];ctx.lineWidth=3*unit;ctx.strokeRect(unit,unit,canvas.width-2*unit,canvas.height-2*unit);
474
+ }
475
+ function drawActor(floor, actor, surface){
476
+ var tx=actor.x*T,ty=actor.y*T,pos=positions[actor.key]||{x:tx,y:ty};
477
+ advanceAvatar(pos,tx,ty,T/5);positions[actor.key]=pos;
478
+ surface=surface||{x:0,y:0,width:canvas.width,height:canvas.height};
479
+ var box=actorBox(actor,surface),oldS=S,oldU=U;S=box.scale;U=box.unit;
480
+ var px=box.x,py=box.y;
481
+ drawAvatar(actor.id,pos,px,py,S,false);
482
+ if(!pos.walking&&!reducedMotion&&Math.floor(frame/30)%2===0){ctx.fillStyle=PALETTE[15];ctx.fillRect(px+12*S,py+30*S,8*S,4*S);}
483
+ var text=short(actor.label,10)+' · '+roleText(actor.kind)+' · '+short(statusText(actor.status),8),font=9*U;
484
+ ctx.font='bold '+font+'px monospace';var labelWidth=Math.min(surface.width-8*U,(ctx.measureText?ctx.measureText(text).width:text.length*font*.62)+8*U),labelX=Math.max(surface.x+4*U,Math.min(surface.x+surface.width-labelWidth-4*U,px-3*U)),labelY=Math.min(surface.y+surface.height-16*U,py+49*S);
485
+ ctx.fillStyle=PALETTE[15];ctx.fillRect(labelX,labelY,labelWidth,14*U);ctx.strokeStyle=PALETTE[8];ctx.lineWidth=U;ctx.strokeRect(labelX,labelY,labelWidth,14*U);fillText(text,labelX+3*U,labelY+2*U,9,actor.alert?PALETTE[13]:PALETTE[0]);
486
+ var alert=alertGeometry(actor,surface);
487
+ if(actor.alert)draw('alert',alert.x,alert.y);
488
+ S=oldS;U=oldU;
489
+ }
490
+ function actorBox(actor,surface){
491
+ var pos=positions[actor.key]||{x:actor.x*T,y:actor.y*T},scale=avatarScale(surface),unit=Math.max(dpr,Math.min(U,scale));
492
+ return {scale:scale,unit:unit,x:Math.round(Math.max(surface.x,Math.min(surface.x+surface.width-32*scale,surface.x+pos.x/(model.roomCols*T)*surface.width))),y:Math.round(Math.max(surface.y,Math.min(surface.y+surface.height-48*scale-16*unit,surface.y+pos.y/(model.roomRows*T)*surface.height)))};
493
+ }
494
+ function alertGeometry(actor,surface){var box=actorBox(actor,surface);return {x:box.x+14*box.scale,y:box.y-9*box.scale,width:4*box.scale,height:8*box.scale,scale:box.scale};}
495
+ function fitCanvas(){
496
+ var wrapW = wrap && wrap.clientWidth ? wrap.clientWidth : window.innerWidth;
497
+ var wrapH = wrap && wrap.clientHeight ? wrap.clientHeight : window.innerHeight - 54;
498
+ var reserved = (dialogueBox && dialogueBox.offsetHeight || 36) + (roomNav && roomNav.offsetHeight || 30) + 24;
499
+ var maxW = Math.max(220, wrapW - 18), maxH = Math.max(160, wrapH - reserved);
500
+ var ratio = Math.min(maxW / canvas.width, maxH / canvas.height);
501
+ canvas.style.width = Math.max(1, Math.floor(canvas.width * ratio)) + "px"; canvas.style.height = Math.max(1, Math.floor(canvas.height * ratio)) + "px";
502
+ }
503
+ function chooseRenderScale(){
504
+ if(!model)return;
505
+ var wrapW=wrap&&wrap.clientWidth?wrap.clientWidth:window.innerWidth,wrapH=wrap&&wrap.clientHeight?wrap.clientHeight:window.innerHeight-54;
506
+ var reserved=(dialogueBox&&dialogueBox.offsetHeight||36)+(roomNav&&roomNav.offsetHeight||30)+24;
507
+ var maxW=Math.max(220,wrapW-18),maxH=Math.max(160,wrapH-reserved),nextDpr=Math.max(1,Math.min(3,window.devicePixelRatio||1)),oldT=T;
508
+ model.cols=model.rows*maxW/maxH;
509
+ T=Math.max(4,maxH/model.rows*nextDpr);S=T/4;U=T/8;dpr=nextDpr;
510
+ if(oldT!==T){Object.keys(positions).forEach(function(key){positions[key].x*=T/oldT;positions[key].y*=T/oldT;});roomCanvases={};}
511
+ }
512
+ function overviewLayout(rooms){
513
+ var count=rooms.length,gap=.6,result=[];if(!count)return result;
514
+ if(count===1)return [{x:gap,y:gap,width:model.cols-6-gap,height:model.rows-gap*2}];
515
+ var hallW=6,left=(model.cols-hallW)/2,right=left,teamIndex=rooms.findIndex(function(f){return f.kind==='run';});
516
+ if(teamIndex>=0&&count<=6){
517
+ left=(model.cols-hallW)*.58;right=model.cols-hallW-left;
518
+ result[teamIndex]={x:gap,y:gap,width:left-gap,height:model.rows-gap*2};
519
+ var others=rooms.filter(function(_,i){return i!==teamIndex;}),height=(model.rows-gap*(others.length+1))/Math.max(1,others.length);
520
+ others.forEach(function(f,i){result[rooms.indexOf(f)]={x:left+hallW,y:gap+i*(height+gap),width:right-gap,height:height};});
521
+ return result;
522
+ }
523
+ var leftCount=Math.ceil(count/2),rightCount=Math.floor(count/2);
524
+ rooms.forEach(function(f,i){var side=i%2,n=side?rightCount:leftCount,row=Math.floor(i/2),height=(model.rows-gap*(n+1))/n;result[i]={x:side?left+hallW:gap,y:gap+row*(height+gap),width:(side?right:left)-gap,height:height};});
525
+ return result;
526
+ }
527
+ function roomBox(floor){var rooms=visibleRooms(),index=rooms.indexOf(floor);return overviewLayout(rooms)[index]||floor.overview;}
528
+ function hallwayBounds(){
529
+ var rooms=visibleRooms(),layout=overviewLayout(rooms),left=layout.find(function(box){return box.x<model.cols/2-3;});
530
+ return {x:left?left.x+left.width:model.cols/2-3,width:6,y:.6,height:model.rows-1.2};
531
+ }
532
+ function ensureSupervisor(){
533
+ if (supervisorReady || !model) return;
534
+ var hall = hallwayBounds(); supervisor.x = supervisor.targetX = hall.x + hall.width / 2; supervisor.y = supervisor.targetY = model.rows - 5; supervisorReady = true;
535
+ }
536
+ function roomEntrances(){
537
+ var hall=hallwayBounds();return visibleRooms().map(function(floor){var box=roomBox(floor),left=box.x<hall.x;return {floor:floor,x:left?hall.x+.35:hall.x+hall.width-.35,y:box.y+box.height/2,height:box.height};});
538
+ }
539
+ function supervisorBounds(){var hall=hallwayBounds();return {minX:hall.x+.2,maxX:hall.x+hall.width-.2,minY:1,maxY:model.rows-3};}
540
+ function drawHallway(){
541
+ var hall = hallwayBounds(), x = hall.x * T, y = hall.y * T, w = hall.width * T, h = hall.height * T;
542
+ ctx.fillStyle = "rgba(183,199,163,.55)"; ctx.fillRect(x, y, w, h);
543
+ ctx.strokeStyle = PALETTE[3]; ctx.lineWidth = Math.max(1, 2 * U); ctx.strokeRect(x, y, w, h);
544
+ for (var line = y + 14 * U; line < y + h; line += 28 * U) { ctx.strokeStyle = "rgba(138,80,51,.28)"; ctx.beginPath(); ctx.moveTo(x + 3 * U, line); ctx.lineTo(x + w - 3 * U, line); ctx.stroke(); }
545
+ var oldS=S;S=T/8;draw('plant',x+T,y+T,PALETTE[2]);draw('plant',x+T,y+h-4*T,PALETTE[2]);S=oldS;
546
+ roomEntrances().forEach(function(entry){ var ex = entry.x * T, ey = entry.y * T; ctx.fillStyle = PALETTE[6]; ctx.fillRect(ex - 5 * U, ey - 14 * U, 10 * U, 28 * U); ctx.strokeStyle = PALETTE[4]; ctx.strokeRect(ex - 5 * U, ey - 14 * U, 10 * U, 28 * U); });
547
+ }
548
+ function drawSupervisor(){
549
+ ensureSupervisor(); if (!model) return;
550
+ advanceAvatar(supervisor,supervisor.targetX,supervisor.targetY,.7);
551
+ var moving=supervisor.walking,scale=Math.max(1,...visibleRooms().map(function(floor){var b=roomBox(floor);return avatarScale({width:b.width*T,height:b.height*T});}));
552
+ var px=supervisor.x*T-16*scale,py=Math.min(canvas.height-48*scale,supervisor.y*T);drawAvatar('viewer',supervisor,px,py,scale,true);
553
+ drawViewerBadge(supervisor.x*T,py,7);
554
+ var entry=roomEntrances().sort(function(a,b){return Math.hypot(a.x-supervisor.x,a.y-supervisor.y)-Math.hypot(b.x-supervisor.x,b.y-supervisor.y);})[0]; if(entry&&(Math.abs(entry.x-supervisor.x)>1.25||Math.abs(entry.y-supervisor.y)>Math.min(1.6,entry.height*.35)))entry=null;
555
+ if (!moving && mode === "overview" && entry) { selectedKey = entry.floor.key; hallwayReturn = {x:supervisor.x, y:supervisor.y}; enterRoomFromHallway(entry.floor); }
556
+ }
557
+ function enterRoomFromHallway(floor){
558
+ if (!floor) return;
559
+ detailTarget=null;detailPage=0;
560
+ roomSupervisor.x = roomSupervisor.targetX = floor.kind === "run" ? model.roomCols - 7 : 6;
561
+ roomSupervisor.y = roomSupervisor.targetY = Math.max(8, Math.min(model.roomRows - 6, Math.round(model.roomRows / 2)));
562
+ mode = "room"; lineIndex = 0; typed = 0; updateHeader(); canvas.focus();
563
+ }
564
+ function drawRoomSupervisor(){
565
+ if (!model) return;
566
+ advanceAvatar(roomSupervisor,roomSupervisor.targetX,roomSupervisor.targetY,.7);
567
+ var scale=avatarScale({width:canvas.width,height:canvas.height});
568
+ var px=roomSupervisor.x/model.roomCols*canvas.width-16*scale,py=Math.min(canvas.height-48*scale,roomSupervisor.y/model.roomRows*canvas.height);drawAvatar('viewer',roomSupervisor,px,py,scale,true);
569
+ drawViewerBadge(px+16*scale,py,8);
570
+ }
571
+ function drawViewerBadge(center,top,size){
572
+ var text=t("viewer"),font=Math.max(5,Math.round(size*U)),pad=Math.max(2,3*U);ctx.font="bold "+font+"px monospace";
573
+ var width=ctx.measureText(text).width+pad*2,height=Math.max(13*U,font+pad*2),x=Math.max(pad,Math.min(canvas.width-width-pad,center-width/2)),y=Math.max(pad,top-height-3*U);
574
+ ctx.fillStyle=PALETTE[15];ctx.fillRect(x,y,width,height);ctx.strokeStyle=PALETTE[1];ctx.strokeRect(x,y,width,height);fillText(text,x+pad,y+pad,size,PALETTE[1]);
575
+ }
576
+ function moveSupervisor(dx, dy){
577
+ if (!model || !statusBox.hidden) return;
578
+ var state=mode==='room'?roomSupervisor:supervisor;state.direction=dx?(dx<0?'left':'right'):(dy<0?'up':'down');
579
+ var step = mode === "room" ? 1 : Math.min(1.25,(model.rows-2)/Math.max(1,Math.ceil(visibleRooms().length/2))/3);
580
+ if (mode === "room") { roomSupervisor.targetX = Math.max(2, Math.min(model.roomCols - 5, roomSupervisor.targetX + dx * step)); roomSupervisor.targetY = Math.max(7, Math.min(model.roomRows - 7, roomSupervisor.targetY + dy * step)); if (reducedMotion) { roomSupervisor.x = roomSupervisor.targetX; roomSupervisor.y = roomSupervisor.targetY; } render(); return; }
581
+ ensureSupervisor(); var bounds = supervisorBounds(); supervisor.targetX = Math.max(bounds.minX, Math.min(bounds.maxX, supervisor.targetX + dx * step)); supervisor.targetY = Math.max(bounds.minY, Math.min(bounds.maxY, supervisor.targetY + dy * step)); if (reducedMotion) { supervisor.x = supervisor.targetX; supervisor.y = supervisor.targetY; } render();
582
+ }
583
+ function renderRoomImage(floor, width, height){
584
+ width=Math.round(width);height=Math.round(height);
585
+ var signature=lang+'|'+T+'|'+width+'x'+height+'|'+JSON.stringify([floor.title,floor.status,floor.board,floor.props,floor.papers,floor.books,floor.clock]);
586
+ var cached=roomCanvases[floor.key];if(cached&&cached.signature===signature)return cached.canvas;
587
+ var roomCanvas=cached?cached.canvas:document.createElement('canvas');
588
+ if(typeof roomCanvas.getContext!=='function')return null;
589
+ roomCanvas.width=width;roomCanvas.height=height;var roomCtx=roomCanvas.getContext('2d');if(!roomCtx)return null;
590
+ var oldCanvas=canvas,oldCtx=ctx;canvas=roomCanvas;ctx=roomCtx;ctx.imageSmoothingEnabled=false;drawRoomBase(floor);canvas=oldCanvas;ctx=oldCtx;
591
+ roomCanvases[floor.key]={canvas:roomCanvas,signature:signature};return roomCanvas;
592
+ }
593
+ function drawOverviewRoom(floor){
594
+ var surface=surfaceForFloor(floor);
595
+ ctx.save();ctx.beginPath();ctx.rect(surface.x,surface.y,surface.width,surface.height);ctx.clip();
596
+ var image=renderRoomImage(floor,surface.width,surface.height);if(image)ctx.drawImage(image,surface.x,surface.y);
597
+ floor.actors.forEach(function(actor){drawActor(floor,actor,surface);});
598
+ if(floor.completedAt){ctx.fillStyle='rgba(53,90,75,.18)';ctx.fillRect(surface.x,surface.y,surface.width,surface.height);}ctx.restore();
599
+ }
600
+ function render(){
601
+ if(!model)return;chooseRenderScale();var floor=activeRoom();
602
+ if(mode==='room'&&floor){
603
+ setCanvasSize(model.cols*T,model.rows*T);var image=renderRoomImage(floor,canvas.width,canvas.height);if(image)ctx.drawImage(image,0,0);else drawRoomBase(floor);
604
+ floor.actors.forEach(function(actor){drawActor(floor,actor);});drawRoomSupervisor();
605
+ }else{drawBackdrop(model.cols,model.rows,null);drawHallway();visibleRooms().forEach(drawOverviewRoom);drawSupervisor();if(!visibleRooms().length)fillText(t('quiet'),4*T,18*T,12);}
606
+ fitCanvas();
607
+ }
608
+ function renderRoomNav(rooms){
609
+ NAV_PAGE_SIZE=Math.max(1,Math.min(8,Math.floor((window.innerWidth-160)/120)));
610
+ if (mode !== "overview") { if (navSignature !== lang + "|room") { roomNav.textContent = ""; navSignature = lang + "|room"; } return; }
611
+ var pages = Math.max(1, Math.ceil(rooms.length / NAV_PAGE_SIZE));
612
+ navPage = Math.max(0, Math.min(navPage, pages - 1));
613
+ var signature = lang + "|overview|" + (showRecent ? "recent" : "active") + "|" + selectedKey + "|" + navPage + "|" + rooms.map(function(f){ return f.key; }).join(",");
614
+ if (signature === navSignature) return;
615
+ navSignature = signature; roomNav.textContent = "";
616
+ if (pages > 1) { var previous = document.createElement("button"); previous.type = "button"; previous.textContent = "‹ " + t("previous"); previous.disabled = navPage === 0; previous.addEventListener("click", function(){ navPage--; updateHeader(); }); roomNav.appendChild(previous); }
617
+ rooms.slice(navPage * NAV_PAGE_SIZE, navPage * NAV_PAGE_SIZE + NAV_PAGE_SIZE).forEach(function(floor, offset){ var button = document.createElement("button"); button.type = "button"; button.textContent = (navPage * NAV_PAGE_SIZE + offset + 1) + ". " + short(floor.title, 18); button.title = t("enter") + ": " + floor.title; button.setAttribute("aria-current", floor.key === selectedKey ? "true" : "false"); button.addEventListener("click", function(){ selectedKey = floor.key; enterRoom(); }); roomNav.appendChild(button); });
618
+ if (pages > 1) { var next = document.createElement("button"); next.type = "button"; next.textContent = t("next") + " ›"; next.disabled = navPage === pages - 1; next.addEventListener("click", function(){ navPage++; updateHeader(); }); roomNav.appendChild(next); }
619
+ }
620
+ function updateHeader(){
621
+ if(document.documentElement)document.documentElement.lang=lang==='zh'?'zh-Hant':'en';
622
+ canvas.setAttribute('aria-label',(lang==='zh'?'方向鍵或 WASD 移動 Supervisor;Enter 進入房間或查看細節;Esc 返回。':'Arrow keys or WASD move Supervisor; Enter opens rooms or details; Escape returns.')+' '+t('keyboardWork')+';'+t('keyboardPhase')+';'+t('keyboardQuestions'));
623
+ var rooms = visibleRooms(), people = rooms.reduce(function(n, f){ return n + f.actors.length; }, 0);
624
+ crumb.textContent = mode === "room" && activeRoom() ? activeRoom().title : t("overview") + " · " + rooms.length + " " + t("rooms") + " · " + people + " " + t("people");
625
+ backButton.hidden = mode !== "room"; backButton.textContent = "← " + t("back"); recentButton.textContent = (showRecent ? "✓ " + t("recentOn") : "▣ " + t("recent")) + (hasRecent() ? " (" + model.floors.filter(function(f){ return !!f.completedAt; }).length + ")" : "");
626
+ languageButton.textContent = lang === "zh" ? "繁中 / EN" : "EN / 繁中"; live.textContent = offline ? t("offline") : t("connected");
627
+ renderRoomNav(rooms);
628
+ }
629
+ function tickDialogue(){
630
+ if (!model) return;
631
+ var floors = mode === "room" && activeRoom() ? [activeRoom()] : visibleRooms(), lines = [];
632
+ floors.forEach(function(floor){ floor.actors.forEach(function(actor){ lines.push(floor.title + ": " + actor.label + " · " + localizeText(actor.narration)); }); if (floor.questions.length) lines.push(floor.title + ": " + t("waiting")); });
633
+ if (!lines.length) lines = [t("quiet")];
634
+ var line = offline ? t("offline") : localizeText(lines[lineIndex % Math.max(1, lines.length)]);
635
+ typed = Math.min(line.length, typed + 2); dialogueBox.textContent = line.slice(0, typed);
636
+ if (typed === line.length && frame % 180 === 0) { lineIndex++; typed = 0; }
637
+ }
638
+ function enterRoom(){ if (!selectedKey && visibleRooms()[0]) selectedKey = visibleRooms()[0].key; var floor = activeRoom(); if (!floor) return; enterRoomFromHallway(floor); render(); }
639
+ function goOverview(){ mode = "overview"; var hall = hallwayBounds(); supervisor.x = supervisor.targetX = hall.x + hall.width / 2; supervisor.y = supervisor.targetY = hall.y + hall.height - 5; hallwayReturn = null; updateHeader(); render(); canvas.focus(); }
640
+ function changeLanguage(){ lang = lang === "zh" ? "en" : "zh"; saveLanguage(); updateHeader(); render(); }
641
+ function showDialog(title, fill){
642
+ if (statusBox.hidden) detailPreviousFocus = document.activeElement || canvas;
643
+ statusBox.textContent = ""; var heading = document.createElement("h2"); heading.id = "status-title"; heading.textContent = title; statusBox.appendChild(heading);
644
+ var body = document.createElement("div"); body.className = "dialog-body"; statusBox.appendChild(body); fill(body);
645
+ var actions = document.createElement("div"); actions.className = "actions";
646
+ var close = document.createElement("button"); close.type = "button"; close.textContent = t("close"); close.addEventListener("click", closeDialog); actions.appendChild(close);
647
+ statusBox.appendChild(actions); statusBox.hidden = false; dialogClose = close;
648
+ if (app) { app.inert = true; app.setAttribute("aria-hidden", "true"); }
649
+ close.focus();
650
+ }
651
+ function closeDialog(){ statusBox.hidden = true; if (app) { app.inert = false; app.removeAttribute("aria-hidden"); } var previous = detailPreviousFocus || canvas; if (!previous || previous.isConnected === false || !previous.focus) previous = canvas; dialogClose = null; detailPreviousFocus = null; if (previous && previous.focus) previous.focus(); }
652
+ function addLine(box, label, value){ var p = document.createElement("p"); p.textContent = label + ": " + safeText(value); box.appendChild(p); }
653
+ function addCommand(box, value, copyValue){
654
+ var row = document.createElement("p"), code = document.createElement("code"), button = document.createElement("button");
655
+ code.textContent = value; button.type = "button"; button.textContent = t("copy"); button.addEventListener("click", function(){
656
+ if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(copyValue || value).then(function(){ button.textContent = t("copied"); }, function(){});
657
+ }); row.appendChild(code); row.appendChild(button); box.appendChild(row);
658
+ }
659
+ function pageItems(items, box, renderItem){
660
+ var small=window.innerWidth<480, pageSize=Math.max(1,Math.min(small?3:5,Math.floor((window.innerHeight*.88-145)/(small?75:56)))), pages;
661
+ items = items.flatMap(function(item){var value=item.value||'', limit=window.innerWidth<480?45:90, chunks=[];for(var offset=0;offset<value.length;offset+=limit)chunks.push(Object.assign({},item,{value:value.slice(offset,offset+limit),copyValue:item.kind==='command'?value:undefined}));return chunks.length?chunks:[item];});
662
+ pages = Math.max(1,Math.ceil(items.length/pageSize));detailPage=Math.max(0,Math.min(detailPage,pages-1));
663
+ items.slice(detailPage * pageSize, detailPage * pageSize + pageSize).forEach(function(item){ renderItem(item, box); });
664
+ var paging = document.createElement("div"); paging.className = "actions dialog-pager"; var page = document.createElement("span"); page.textContent = t("page") + " " + (detailPage + 1) + "/" + pages; paging.appendChild(page);
665
+ if (pages > 1) { var previous = document.createElement("button"); previous.type = "button"; previous.textContent = t("previous"); previous.disabled = detailPage === 0; previous.addEventListener("click", function(){ detailPage--; openSelectedDetail(); }); var next = document.createElement("button"); next.type = "button"; next.textContent = t("next"); next.disabled = detailPage === pages - 1; next.addEventListener("click", function(){ detailPage++; openSelectedDetail(); }); paging.appendChild(previous); paging.appendChild(next); }
666
+ box.appendChild(paging);
667
+ }
668
+ function sourceFor(floor){
669
+ if (!snapshot) return null;
670
+ if (floor.kind === "run") return snapshot.runs.find(function(r){ return r.runId === floor.key; }) || null;
671
+ if (floor.kind === "desk") return floor.sourceIndex === null ? null : snapshot.lobby[floor.sourceIndex] || null;
672
+ if (floor.kind === "review") return floor.sourceIndex === null ? null : snapshot.reviews[floor.sourceIndex] || null;
673
+ return null;
674
+ }
675
+ function sourceSignature(floor){return JSON.stringify(sourceFor(floor),function(key,value){return key==='budget'?undefined:value;});}
676
+ function renderDetailItem(item, box){
677
+ if (item.kind === "command") return addCommand(box, item.value, item.copyValue);
678
+ if (item.kind === "file") { var file = document.createElement("p"); file.textContent = "• " + item.value; box.appendChild(file); return; }
679
+ if (item.kind === "heading") { var heading = document.createElement("p"); heading.textContent = item.value; box.appendChild(heading); return; }
680
+ addLine(box, item.label, item.value);
681
+ }
682
+ function progressText(progress){return progress ? progress.passed+'/'+(progress.total||'?')+' · '+t('verify')+' '+outcomeText(progress.verify||'pending')+' · '+t('review')+' '+outcomeText(progress.review||'pending') : t('unknown');}
683
+ function workItemForActor(floor,actor){return {kind:'work',floorKey:floor.key,actorKey:actor.key,label:t('owner')+': '+actor.label+' · '+roleText(actor.kind),value:t('currentWork')+': '+localizeText(actor.narration)+' · '+t('phase')+': '+phaseLabel(actor.phase)+' · '+t('status')+': '+statusText(actor.status)+' · '+t('progress')+': '+progressText(actor.progress)+' · '+t('pending')+': '+actor.questionCount};}
684
+ function workItemsFor(floor){
685
+ var items=[
686
+ {kind:'line',label:t('task'),value:floor.board.taskId==='unassigned'?t('unassigned'):floor.board.taskId},
687
+ {kind:'line',label:t('status'),value:statusText(floor.status)},
688
+ {kind:'line',label:t('progress'),value:progressText({passed:floor.board.passed,total:floor.board.total,verify:floor.board.verify,review:floor.board.review})},
689
+ {kind:'line',label:t('pending'),value:String(floor.board.pending)}
690
+ ];
691
+ floor.actors.forEach(function(actor){items.push(workItemForActor(floor,actor));});
692
+ floor.questions.forEach(function(question){items.push({kind:'question',label:t('question')+' · ! '+question.questionId,value:question.prompt});});
693
+ if (!floor.actors.length && !floor.questions.length) items.push({kind:'line',label:t('people'),value:'0'});
694
+ return items;
695
+ }
696
+ function phaseWorkItemsFor(floor,phase){
697
+ var items=[{kind:'line',label:t('phase'),value:phaseLabel(phase)}],actors=floor.actors.filter(function(actor){return actor.phase===phase;});
698
+ if(!actors.length)items.push({kind:'line',label:t('currentWork'),value:t('noPeople')});
699
+ actors.forEach(function(actor){items.push(workItemForActor(floor,actor));});
700
+ return items;
701
+ }
702
+ function questionItemsFor(floor){
703
+ var items=[];
704
+ if(!floor.questions.length)items.push({kind:'line',label:t('questions'),value:t('noQuestions')});
705
+ floor.questions.forEach(function(question){items.push({kind:'line',label:t('question')+' · ! '+question.questionId,value:question.prompt});});
706
+ var source=sourceFor(floor);
707
+ if(source&&source.commands&&source.commands.length){items.push({kind:'heading',value:t('commands')+':' });source.commands.forEach(function(command){items.push({kind:'command',value:command});});}
708
+ return items;
709
+ }
710
+ function renderWorkItem(item,box){
711
+ if(item.kind==='work'){
712
+ var row=document.createElement('p'),button=document.createElement('button'),detail=document.createElement('span');
713
+ button.type='button';button.textContent=item.label;button.title=t('details');button.addEventListener('click',function(){var floor=roomByKey(item.floorKey),actor=floor&&floor.actors.find(function(candidate){return candidate.key===item.actorKey;});if(floor&&actor)openActorDetail(floor,actor);});
714
+ detail.textContent=' · '+safeText(item.value);row.appendChild(button);row.appendChild(detail);box.appendChild(row);return;
715
+ }
716
+ if(item.kind==='question'){addLine(box,item.label,localizeText(item.value));return;}
717
+ renderDetailItem(item,box);
718
+ }
719
+ function openWorkList(floor){
720
+ if(!detailTarget||detailTarget.floorKey!==floor.key||detailTarget.view!=='work')detailPage=0;
721
+ detailSignature=sourceSignature(floor);detailTarget={floorKey:floor.key,actorKey:null,view:'work'};
722
+ showDialog(floor.title+' · '+t('workList'),function(box){pageItems(workItemsFor(floor),box,renderWorkItem);});
723
+ }
724
+ function openPhaseWorkList(floor,phase){
725
+ if(!detailTarget||detailTarget.floorKey!==floor.key||detailTarget.view!=='phase'||detailTarget.phase!==phase)detailPage=0;
726
+ detailSignature=sourceSignature(floor);detailTarget={floorKey:floor.key,actorKey:null,view:'phase',phase:phase};
727
+ showDialog(floor.title+' · '+phaseLabel(phase),function(box){pageItems(phaseWorkItemsFor(floor,phase),box,renderWorkItem);});
728
+ }
729
+ function openQuestionList(floor){
730
+ if(!detailTarget||detailTarget.floorKey!==floor.key||detailTarget.view!=='questions')detailPage=0;
731
+ detailSignature=sourceSignature(floor);detailTarget={floorKey:floor.key,actorKey:null,view:'questions'};
732
+ showDialog(floor.title+' · '+t('questions'),function(box){pageItems(questionItemsFor(floor),box,renderDetailItem);});
733
+ }
734
+ function openActorDetail(floor, actor){
735
+ var source = sourceFor(floor), items = [], entries = [];
736
+ if (source && actor.kind !== "reviewer" && floor.kind === "run") { var found = source.agents.find(function(a){ return a.id === actor.id; }); if (found && found.diff) items = found.diff.paths || []; }
737
+ if (source && floor.kind === "desk" && source.diff) items = source.diff.paths || [];
738
+ items.forEach(function(item){ entries.push({kind:"file", value:item}); });
739
+ if (source && source.commands) source.commands.forEach(function(item){ entries.push({kind:"command", value:item}); });
740
+ if (!detailTarget || detailTarget.floorKey !== floor.key || detailTarget.actorKey !== actor.key) detailPage = 0;
741
+ var detailItems = [
742
+ {kind:"line", label:lang==='zh'?'職責':'Role', value:roleText(actor.kind)},
743
+ {kind:"line", label:t("status"), value:statusText(actor.status)}, {kind:"line", label:t("phase"), value:phaseLabel(actor.phase)},
744
+ {kind:"line", label:t("task"), value:actor.taskId === "unassigned" ? t("unassigned") : actor.taskId},
745
+ {kind:"line", label:t("host"), value:localizeText(actor.host)}, {kind:"line", label:t("now"), value:localizeText(actor.narration)}
746
+ ];
747
+ if (actor.questionCount) detailItems.push({kind:"line", label:t("pending"), value:String(actor.questionCount)});
748
+ if (actor.progress) detailItems.push({kind:"line", label:t("progress"), value:actor.progress.passed + "/" + actor.progress.total + " " + t("verify") + " " + outcomeText(actor.progress.verify || "-") + " " + t("review") + " " + outcomeText(actor.progress.review || "-")});
749
+ if (actor.questionCount && floor.questions.length) floor.questions.forEach(function(question){ detailItems.push({kind:"line", label:"! " + question.questionId, value:question.prompt}); });
750
+ if (entries.length) { detailItems.push({kind:"heading", value:t("files") + " / " + t("commands") + ":"}); entries.forEach(function(entry){ detailItems.push(entry); }); }
751
+ detailSignature=sourceSignature(floor); detailTarget = {floorKey: floor.key, actorKey: actor.key, view:'actor'}; showDialog(actor.label, function(box){ pageItems(detailItems, box, renderDetailItem); });
752
+ }
753
+ function openRoomDetail(floor){
754
+ if (!detailTarget || detailTarget.floorKey !== floor.key || detailTarget.actorKey !== null) detailPage = 0;
755
+ detailSignature=sourceSignature(floor); detailTarget = {floorKey: floor.key, actorKey: null, view:'room'}; var source = sourceFor(floor), items = source && source.diff ? source.diff.paths || [] : [], entries = [];
756
+ items.forEach(function(item){ entries.push({kind:"file", value:item}); });
757
+ if (source && source.commands) source.commands.forEach(function(item){ entries.push({kind:"command", value:item}); });
758
+ var detailItems = [
759
+ {kind:"line", label:t("status"), value:statusText(floor.status)}, {kind:"line", label:t("phase"), value:phaseLabel(floor.phase)},
760
+ {kind:"line", label:t("task"), value:floor.board.taskId === "unassigned" ? t("unassigned") : floor.board.taskId},
761
+ {kind:"line", label:t("progress"), value:floor.board.passed + "/" + (floor.board.total || "?")},
762
+ {kind:"line", label:t("verify"), value:outcomeText(floor.board.verify)}, {kind:"line", label:t("review"), value:outcomeText(floor.board.review)},
763
+ {kind:"line", label:t("pending"), value:String(floor.board.pending)}
764
+ ];
765
+ floor.questions.forEach(function(question){ detailItems.push({kind:"line", label:"! " + question.questionId, value:question.prompt}); });
766
+ if (entries.length) { detailItems.push({kind:"heading", value:t("files") + " / " + t("commands") + ":"}); entries.forEach(function(entry){ detailItems.push(entry); }); }
767
+ showDialog(floor.title, function(box){ pageItems(detailItems, box, renderDetailItem); });
768
+ }
769
+ function openSelectedDetail(){
770
+ var target = detailTarget, floor = target ? roomByKey(target.floorKey) : activeRoom(); if (!floor) return;
771
+ if (target && target.view === 'work') return openWorkList(floor);
772
+ if (target && target.view === 'phase') return openPhaseWorkList(floor,target.phase);
773
+ if (target && target.view === 'questions') return openQuestionList(floor);
774
+ if (target && target.actorKey) { var actor = floor.actors.find(function(a){ return a.key === target.actorKey; }); if (actor) return openActorDetail(floor, actor); }
775
+ return openRoomDetail(floor);
776
+ }
777
+ function hitOverview(event){
778
+ var point=canvasPoint(event),x=point.x/T,y=point.y/T;
779
+ return visibleRooms().find(function(f){ var b = roomBox(f); return x >= b.x && x < b.x + b.width && y >= b.y && y < b.y + b.height; }) || null;
780
+ }
781
+ function hitActor(event, floor, surface){
782
+ var point=canvasPoint(event);surface=surface||{x:0,y:0,width:canvas.width,height:canvas.height};
783
+ for (var i = floor.actors.length - 1; i >= 0; i--) { var a=floor.actors[i],box=actorBox(a,surface);if(inside(point,{x:box.x,y:box.y,width:32*box.scale,height:48*box.scale}))return a; }
784
+ return null;
785
+ }
786
+ function surfaceRect(rect,surface){return {x:rect.x+surface.x,y:rect.y+surface.y,width:rect.width,height:rect.height};}
787
+ function hitQuestionBadge(event,floor,surface){
788
+ var point=canvasPoint(event);surface=surface||{x:0,y:0,width:canvas.width,height:canvas.height};
789
+ for(var i=floor.actors.length-1;i>=0;i--){var actor=floor.actors[i];if(actor.questionCount>0&&inside(point,alertGeometry(actor,surface)))return actor;}
790
+ return null;
791
+ }
792
+ function actorForProp(floor,prop){
793
+ var prefix=floor.key+':desk:';
794
+ if(prop.kind!=='desk'||prop.key.indexOf(prefix)!==0)return null;
795
+ var id=prop.key.slice(prefix.length);
796
+ return floor.actors.find(function(actor){return actor.id===id;})||null;
797
+ }
798
+ function hitWorkBoard(event,floor,surface){
799
+ var point=canvasPoint(event),board=progressBoardGeometry(surface.width,surface.height);
800
+ board.x+=surface.x;board.y+=surface.y;
801
+ if(floor.board.pending>0){var pending=pendingBoardGeometry(surface.width,surface.height);pending.x+=surface.x;pending.y+=surface.y;if(inside(point,pending))return 'questions';}
802
+ if(inside(point,board))return 'progress';
803
+ var whiteboard=floor.props.find(function(prop){return prop.kind==='whiteboard';});
804
+ if(whiteboard){var rect=surfaceRect(propGeometry(whiteboard,surface.width,surface.height),surface);if(inside(point,rect))return 'whiteboard';}
805
+ return null;
806
+ }
807
+ function hitWorkTarget(event,floor,surface){
808
+ var point=canvasPoint(event),props=floor.props;
809
+ for(var i=props.length-1;i>=0;i--){var prop=props[i];if(prop.kind!=='desk'&&prop.kind!=='bench'&&prop.kind!=='table')continue;var rect=surfaceRect(propGeometry(prop,surface.width,surface.height),surface);if(inside(point,rect))return {kind:'prop',phase:prop.phase,prop:prop,actor:actorForProp(floor,prop)};}
810
+ for(var j=floor.phaseAreas.length-1;j>=0;j--){var area=phaseAreaGeometry(floor.phaseAreas[j],surface.width,surface.height);if(inside(point,surfaceRect(area,surface)))return {kind:'phase',phase:floor.phaseAreas[j].phase,prop:null,actor:null};}
811
+ return null;
812
+ }
813
+ canvas.addEventListener("click", function(event){
814
+ if (!model) return;
815
+ if (mode === "overview") {
816
+ var floor = hitOverview(event); if (!floor) return; selectedKey=floor.key;var surface=surfaceForFloor(floor),actor=hitActor(event,floor,surface);
817
+ if(actor){enterRoom();openActorDetail(activeRoom()||floor,actor);return;}
818
+ var badge=hitQuestionBadge(event,floor,surface);if(badge){openQuestionList(floor);return;}
819
+ var boardHit=hitWorkBoard(event,floor,surface);if(boardHit==='questions'){openQuestionList(floor);return;}if(boardHit){openWorkList(floor);return;}
820
+ var work=hitWorkTarget(event,floor,surface);if(work){if(work.actor)openActorDetail(floor,work.actor);else if(work.phase)openPhaseWorkList(floor,work.phase);return;}
821
+ enterRoom();return;
822
+ }
823
+ var room = activeRoom(); if (!room) return; var roomSurface=surfaceForFloor(room), actor = hitActor(event, room, roomSurface); if (actor) openActorDetail(room, actor); else {
824
+ var roomBadge=hitQuestionBadge(event,room,roomSurface);if(roomBadge){openQuestionList(room);return;}
825
+ var roomBoard=hitWorkBoard(event,room,roomSurface);if(roomBoard==='questions'){openQuestionList(room);return;}if(roomBoard){openWorkList(room);return;}
826
+ var roomWork=hitWorkTarget(event,room,roomSurface);if(roomWork){if(roomWork.actor)openActorDetail(room,roomWork.actor);else if(roomWork.phase)openPhaseWorkList(room,roomWork.phase);else openRoomDetail(room);return;}
827
+ openRoomDetail(room);
828
+ }
829
+ });
830
+ canvas.addEventListener("keydown", function(event){
831
+ if (event.ctrlKey || event.metaKey || event.altKey) return;
832
+ if (event.key === "Escape" || event.key === "Backspace") { event.preventDefault(); if (!statusBox.hidden) closeDialog(); else if (mode === "room") goOverview(); return; }
833
+ var keyboardFloor=activeRoom()||roomByKey(selectedKey)||visibleRooms()[0];
834
+ if (event.key === "l" || event.key === "L") { if (keyboardFloor) { event.preventDefault(); openWorkList(keyboardFloor); return; } }
835
+ if (event.key === "q" || event.key === "Q") { if (keyboardFloor) { event.preventDefault(); openQuestionList(keyboardFloor); return; } }
836
+ var phaseKeys={"1":"planning","2":"implementing","3":"verifying","4":"reviewing","5":"integrating"};
837
+ if (phaseKeys[event.key] && keyboardFloor) { event.preventDefault(); openPhaseWorkList(keyboardFloor,phaseKeys[event.key]); return; }
838
+ if (mode === "overview" || mode === "room") {
839
+ var move = {ArrowLeft:[-1,0], ArrowRight:[1,0], ArrowUp:[0,-1], ArrowDown:[0,1], a:[-1,0], d:[1,0], w:[0,-1], s:[0,1], A:[-1,0], D:[1,0], W:[0,-1], S:[0,1]}[event.key];
840
+ if (move) { event.preventDefault(); moveSupervisor(move[0], move[1]); return; }
841
+ }
842
+ var rooms = visibleRooms(); if (!rooms.length) return;
843
+ var index = Math.max(0, rooms.findIndex(function(f){ return f.key === selectedKey; }));
844
+ if (event.key === "ArrowRight" || event.key === "ArrowDown") { event.preventDefault(); selectedKey = rooms[(index + 1) % rooms.length].key; updateHeader(); render(); }
845
+ if (event.key === "ArrowLeft" || event.key === "ArrowUp") { event.preventDefault(); selectedKey = rooms[(index - 1 + rooms.length) % rooms.length].key; updateHeader(); render(); }
846
+ if (event.key === "Enter" && mode === "overview") { event.preventDefault(); enterRoom(); }
847
+ if (event.key === "Enter" && mode === "room") { event.preventDefault(); openSelectedDetail(); }
848
+ });
849
+ backButton.addEventListener("click", goOverview); recentButton.addEventListener("click", function(){ showRecent = !showRecent; if (!showRecent && activeRoom() && activeRoom().completedAt) mode = "overview"; updateHeader(); render(); });
850
+ languageButton.addEventListener("click", changeLanguage); statusBox.addEventListener("keydown", function(event){
851
+ if (event.key === "Escape") { event.preventDefault(); closeDialog(); return; }
852
+ if (event.key !== "Tab") return;
853
+ var focusables = statusBox.querySelectorAll ? Array.prototype.slice.call(statusBox.querySelectorAll("button:not([disabled])")) : (dialogClose ? [dialogClose] : []);
854
+ if (!focusables.length) return;
855
+ var first = focusables[0], last = focusables[focusables.length - 1];
856
+ if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
857
+ else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
858
+ });
859
+ function poll(){ fetch("snapshot.json" + location.search, {cache:"no-store"}).then(function(r){ if (!r.ok) throw new Error(String(r.status)); return r.json(); }).then(function(s){ snapshot = s; model = sceneModel(s); offline = false; var selectedMissing=mode==='room'&&!!selectedKey&&!roomByKey(selectedKey),dialogMissing=!statusBox.hidden&&!!detailTarget&&!roomByKey(detailTarget.floorKey); if(selectedMissing||dialogMissing){if(dialogMissing)detailPreviousFocus=canvas;if(!statusBox.hidden)closeDialog();detailTarget=null;detailPage=0;detailSignature="";mode='overview';} if (!selectedKey || !roomByKey(selectedKey)) selectedKey = visibleRooms()[0] && visibleRooms()[0].key; updateHeader(); render(); if (!statusBox.hidden && detailTarget && roomByKey(detailTarget.floorKey)) { var signature=sourceSignature(roomByKey(detailTarget.floorKey)); if(signature!==detailSignature){detailSignature=signature;openSelectedDetail();} } }, function(){ offline = true; updateHeader(); }); }
860
+ function loop(){ frame++; render(); tickDialogue(); requestAnimationFrame(loop); }
861
+ updateHeader(); poll(); setInterval(poll, 2000); window.addEventListener("resize", render); requestAnimationFrame(loop);
862
+ `;
863
+ /** One inline page; the nonce binds its only script and style under the server's CSP. */
864
+ export function officePage(nonce) {
865
+ const script = CLIENT.replace("__PALETTE__", () => JSON.stringify(PALETTE)).replace("__SPRITES__", () => JSON.stringify(SPRITES))
866
+ .replace("__SCENE__", () => sceneModel.toString()).replace("__AVATAR__", () => avatarSprite.toString()).replace("__AVATAR_COLORS__", () => avatarPalette.toString());
867
+ return `<!doctype html>
868
+ <html lang="zh-Hant"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
869
+ <meta name="referrer" content="no-referrer"><title>agent-ops Office · Preview</title><style nonce="${nonce}">${STYLE}</style></head>
870
+ <body><div id="app"><header id="header"><div id="brand">agent-ops Office · Preview</div><div id="crumb">Office overview</div><button id="back" type="button" hidden>← Back to overview</button><button id="recent" type="button">▣ Recently completed</button><button id="language" type="button">繁中 / EN</button><span id="live" aria-live="polite">● Connected</span></header>
871
+ <main id="wrap"><canvas id="office" tabindex="0" width="576" height="304" aria-label="Office rooms; use arrow keys and Enter to explore"></canvas><div id="dialogue" role="status" aria-live="polite"></div><nav id="room-nav" aria-label="Office rooms"></nav></main></div>
872
+ <section id="status" role="dialog" aria-modal="true" aria-labelledby="status-title" tabindex="-1" hidden></section><script nonce="${nonce}">${script}</script></body></html>`;
873
+ }