@kylecheng3146/agent-ops 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +60 -1
  2. package/dist/packages/cli/src/args.js +19 -2
  3. package/dist/packages/cli/src/bin.js +14 -4
  4. package/dist/packages/cli/src/cli.js +20 -2
  5. package/dist/packages/cli/src/codex-loop-process.js +19 -2
  6. package/dist/packages/cli/src/commands/advance.js +19 -2
  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.js +10 -4
  10. package/dist/packages/cli/src/commands/run.js +6 -2
  11. package/dist/packages/cli/src/commands/update.js +8 -0
  12. package/dist/packages/cli/src/context.js +7 -5
  13. package/dist/packages/cli/src/hook-entry.js +2 -1
  14. package/dist/packages/cli/src/hook-process.js +23 -2
  15. package/dist/packages/cli/src/loop-entry.js +2 -1
  16. package/dist/packages/cli/src/office-entry.js +261 -0
  17. package/dist/packages/cli/src/parallel-deps.js +4 -2
  18. package/dist/packages/cli/src/run-deps.js +9 -2
  19. package/dist/packages/cli/src/run-entry.js +9 -0
  20. package/dist/packages/cli/src/wizard.js +14 -2
  21. package/dist/runtime/src/adapters/agy/config.js +4 -3
  22. package/dist/runtime/src/adapters/agy/events.js +9 -0
  23. package/dist/runtime/src/adapters/claude/config.js +3 -3
  24. package/dist/runtime/src/adapters/claude/events.js +8 -0
  25. package/dist/runtime/src/adapters/codex/config.js +3 -3
  26. package/dist/runtime/src/adapters/codex/events.js +8 -0
  27. package/dist/runtime/src/adapters/opencode/config.js +5 -4
  28. package/dist/runtime/src/adapters/opencode/events.js +8 -0
  29. package/dist/runtime/src/config/explain.js +5 -0
  30. package/dist/runtime/src/fs/transaction.js +2 -1
  31. package/dist/runtime/src/hooks/codex-loop.js +4 -0
  32. package/dist/runtime/src/hooks/completion-gate.js +38 -3
  33. package/dist/runtime/src/install/harness.js +44 -1
  34. package/dist/runtime/src/install/plan.js +16 -11
  35. package/dist/runtime/src/install/preauth.js +44 -18
  36. package/dist/runtime/src/install/profiles.js +8 -2
  37. package/dist/runtime/src/install/update.js +1 -0
  38. package/dist/runtime/src/office/collect.js +268 -0
  39. package/dist/runtime/src/office/page.js +887 -0
  40. package/dist/runtime/src/office/repos.js +43 -0
  41. package/dist/runtime/src/office/scene.js +181 -0
  42. package/dist/runtime/src/office/server.js +143 -0
  43. package/dist/runtime/src/office/sessions.js +130 -0
  44. package/dist/runtime/src/office/snapshot.js +117 -0
  45. package/dist/runtime/src/parallel/finish.js +3 -0
  46. package/dist/runtime/src/parallel/guard.js +7 -1
  47. package/dist/runtime/src/review/slots.js +1 -1
  48. package/dist/runtime/src/run/hosts/codex.js +1 -1
  49. package/dist/runtime/src/run/phase.js +37 -0
  50. package/dist/runtime/src/run/service.js +8 -0
  51. package/dist/runtime/src/schema/validate.js +13 -2
  52. package/dist/runtime/src/security/permissions.js +6 -0
  53. package/dist/runtime/src/verify/source-fingerprint.js +4 -0
  54. package/docs/en/spec/harness-adapters.md +122 -1
  55. package/docs/zh-TW/spec/harness-adapters.md +100 -1
  56. package/package.json +1 -1
  57. package/schemas/config.schema.json +10 -1
@@ -0,0 +1,887 @@
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],#header select[hidden]{display:none}
263
+ #header select{border:2px solid #f2e5c9;background:#355a4b;color:#fff7e6;padding:4px 6px;font:inherit;box-shadow:2px 2px 0 #000}
264
+ #header select:focus-visible{outline:3px solid #f2c95c;outline-offset:2px}
265
+ #live{color:#adc39a}
266
+ #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}
267
+ #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}
268
+ #office:focus-visible{outline:4px solid #f2c95c;outline-offset:4px}
269
+ #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}
270
+ #room-nav{width:100%;display:flex;justify-content:center;align-items:center;gap:5px;min-height:28px;overflow:hidden}
271
+ #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}
272
+ #room-nav button[aria-current=true]{background:#6d9275;color:#fff7e6;border-color:#2b241f}
273
+ #room-nav button:focus-visible{outline:3px solid #f2c95c;outline-offset:2px}
274
+ #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}
275
+ #status[hidden]{display:none}
276
+ #status h2{margin:0 0 8px;color:#355a4b;font-size:1.25em}
277
+ #status .dialog-body{min-height:0;overflow:hidden}
278
+ #status p{margin:5px 0;overflow-wrap:anywhere;white-space:pre-wrap}
279
+ #status ul{margin:5px 0;padding-left:22px}
280
+ #status li{overflow-wrap:anywhere}
281
+ #status .actions{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:8px;margin-top:12px}
282
+ #status>.actions{flex:0 0 auto}
283
+ #status code{display:inline-block;max-width:calc(100% - 74px);overflow-wrap:anywhere;white-space:pre-wrap;background:#f2e5c9;padding:2px 4px}
284
+ #status .dialog-pager{border-top:2px solid #d9aa72;padding-top:6px}
285
+ #status button{border:2px solid #2b241f;background:#d9aa72;padding:5px 10px;cursor:pointer}
286
+ #status button:focus-visible{outline:3px solid #f2c95c;outline-offset:2px}
287
+ .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}
288
+ @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}}
289
+ @media (prefers-reduced-motion:reduce){*{scroll-behavior:auto!important}}
290
+ `;
291
+ const CLIENT = String.raw `
292
+ var PALETTE = __PALETTE__, SPRITES = __SPRITES__, T = 8, S = 2, U = 1, dpr = 1;
293
+ var sceneModel = __SCENE__;
294
+ var avatarSprite = __AVATAR__, avatarPalette = __AVATAR_COLORS__, avatarImages = {};
295
+ var canvas = document.getElementById("office"), ctx = canvas.getContext("2d"), app = document.getElementById("app");
296
+ ctx.imageSmoothingEnabled = false;
297
+ var dialogueBox = document.getElementById("dialogue"), statusBox = document.getElementById("status"), roomNav = document.getElementById("room-nav"), wrap = document.getElementById("wrap");
298
+ var crumb = document.getElementById("crumb"), backButton = document.getElementById("back"), recentButton = document.getElementById("recent");
299
+ var languageButton = document.getElementById("language"), live = document.getElementById("live"), repoSelect = document.getElementById("repo-filter"), repoFilter = "", repoSignature = "";
300
+ var snapshot = null, model = null, positions = {}, roomCanvases = {}, frame = 0, lineIndex = 0, typed = 0, offline = false;
301
+ var mode = "overview", selectedKey = null, showRecent = false, detailPage = 0, dialogClose = null, detailPreviousFocus = null, detailTarget = null;
302
+ var detailSignature = "";
303
+ var navPage = 0, navSignature = "", NAV_PAGE_SIZE = 8;
304
+ var supervisor = {x:43, y:19, targetX:43, targetY:19}, supervisorReady = false, hallwayReturn = null;
305
+ var roomSupervisor = {x:32, y:34, targetX:32, targetY:34};
306
+ var reducedMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
307
+ var STRINGS = {
308
+ en: {viewer:"You", overview:"Office overview", rooms:"rooms", people:"people", recent:"Recently completed", recentOn:"Hide completed", repos:"Repository", allRepos:"All repositories", 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"}},
309
+ zh: {viewer:"你", overview:"辦公室總覽", rooms:"個房間", people:"位成員", recent:"最近完成", recentOn:"隱藏已完成", repos:"專案", allRepos:"所有專案", 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:"待回覆"}}
310
+ };
311
+ function getLanguage(){
312
+ 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 (_) {}
313
+ try { var stored = localStorage.getItem("agent-office-language"); if (stored === "en" || stored === "zh") return stored; } catch (_) {}
314
+ return (navigator.language || "en").toLowerCase().indexOf("zh") === 0 ? "zh" : "en";
315
+ }
316
+ var lang = getLanguage();
317
+ function t(key){ return STRINGS[lang][key] || key; }
318
+ function phaseLabel(phase){ return STRINGS[lang].phases[phase] || phase; }
319
+ function statusText(status){ return STRINGS[lang].statuses[status] || safeText(status); }
320
+ function roleText(kind){return (lang==='zh'?{coordinator:'統籌',worker:'成員',reviewer:'審查',visitor:'訪客'}:{coordinator:'Lead',worker:'Agent',reviewer:'Review',visitor:'Guest'})[kind]||kind;}
321
+ function outcomeText(value){ return value === "pending" ? (lang === "zh" ? "尚未完成" : "pending") : safeText(value); }
322
+ function localizeText(value){
323
+ var text = safeText(value);
324
+ if (text === "The office is quiet. No agent is at work.") return t("quiet");
325
+ if (text === "waiting for your answer") return t("waiting");
326
+ if (text === "reviewing") return phaseLabel("reviewing");
327
+ if (text === "planning" || text === "implementing" || text === "verifying" || text === "integrating") return phaseLabel(text);
328
+ if (STRINGS.en.statuses[text]) return statusText(text);
329
+ if(lang==='zh')text=text.replace(/^writing tests$/u,'撰寫測試').replace(/^writing docs$/u,'撰寫文件').replace(/^editing /u,'編輯 ');
330
+ return text.replace(/waiting for your answer/gu, t("waiting"));
331
+ }
332
+ function saveLanguage(){ try { localStorage.setItem("agent-office-language", lang); } catch (_) {} try { document.cookie = "agent-office-language=" + lang + "; Max-Age=31536000; Path=/; SameSite=Strict"; } catch (_) {} }
333
+ function safeText(value, fallback){ return typeof value === "string" && value.length ? value : (fallback || t("unknown")); }
334
+ function short(value, limit){ value = safeText(value); limit = limit || 22; return value.length > limit ? value.slice(0, limit - 1) + "…" : value; }
335
+ function draw(name, x, y, shirt){
336
+ var rows = SPRITES[name]; if (!rows) return;
337
+ var scale = name === "floor" || name === "wall" || name === "baseboard" ? 1 : S;
338
+ for (var r = 0; r < rows.length; r++) for (var c = 0; c < rows[r].length; c++) {
339
+ var ch = rows[r][c]; if (ch === ".") continue;
340
+ ctx.fillStyle = ch === "S" ? shirt : PALETTE[parseInt(ch, 16)]; ctx.fillRect(x + c * scale, y + r * scale, scale, scale);
341
+ }
342
+ }
343
+ function avatarScale(surface){return Math.max(1,Math.round(Math.min(surface.width/(model.roomCols*T),surface.height/(model.roomRows*T))*T/9));}
344
+ function advanceAvatar(state, tx, ty, speed){
345
+ var dx=tx-state.x,dy=ty-state.y;
346
+ 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');
347
+ 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));}
348
+ state.walking=!reducedMotion&&(Math.abs(state.x-tx)>.05||Math.abs(state.y-ty)>.05);
349
+ }
350
+ function drawAvatar(identity, state, x, y, scale, viewer){
351
+ var pose=state.walking&&!reducedMotion?Math.floor(frame/7)%4:0,direction=state.direction||'down',key=JSON.stringify([identity,direction,pose,!!viewer]);
352
+ var image=avatarImages[key];
353
+ if(!image){
354
+ 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);
355
+ 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);}}
356
+ avatarImages[key]=image;
357
+ }
358
+ x=Math.round(x);y=Math.round(y);
359
+ ctx.fillStyle='rgba(43,36,31,.18)';ctx.fillRect(x+8*scale,y+46*scale,17*scale,2*scale);
360
+ ctx.drawImage(image,x,y,32*scale,48*scale);
361
+ }
362
+ function propSize(kind){ var s = SPRITES[kind] || [""]; return {w:s[0].length * S, h:s.length * S}; }
363
+ 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)};}
364
+ function propGeometry(prop,width,height){
365
+ var metrics=roomMetrics(width,height),rawX=prop.x*metrics.rx,rawY=prop.y*metrics.ry;
366
+ // layout sizes keep the existing scale decisions; visual sizes follow the
367
+ // actual rounded pixel art drawn by furniture() so clicks never drift away.
368
+ 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};
369
+ 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};
370
+ var layoutWidth=layoutWidths[prop.kind]||75,visualWidth=visualWidths[prop.kind]||layoutWidth,artHeight=heights[prop.kind]||45;
371
+ 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);
372
+ 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};
373
+ }
374
+ 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};}
375
+ 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};}
376
+ 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};}
377
+ 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)};}
378
+ 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};}
379
+ 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;}
380
+ 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); }
381
+ 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; }
382
+ function visibleRooms(){ if (!model) return []; return model.floors.filter(function(f){ return (showRecent || !f.completedAt) && (!repoFilter || f.repo === repoFilter); }); }
383
+ function repoNames(){ var names = []; if (model) model.floors.forEach(function(f){ if (f.repo && names.indexOf(f.repo) < 0) names.push(f.repo); }); return names; }
384
+ function repoColor(name){ var hash = 0; for (var i = 0; i < name.length; i++) hash = (hash * 31 + name.charCodeAt(i)) >>> 0; return PALETTE[[1, 4, 9, 13, 0, 8][hash % 6]] || PALETTE[1]; }
385
+ function renderRepoFilter(){
386
+ var names = repoNames(); if (repoFilter && names.indexOf(repoFilter) < 0) repoFilter = "";
387
+ repoSelect.hidden = names.length < 2; repoSelect.setAttribute("aria-label", t("repos"));
388
+ var signature = lang + "|" + repoFilter + "|" + names.join("\n"); if (signature === repoSignature) return; repoSignature = signature;
389
+ repoSelect.textContent = ""; [""].concat(names).forEach(function(name){ var option = document.createElement("option"); option.value = name; option.textContent = name || t("allRepos"); option.selected = name === repoFilter; repoSelect.appendChild(option); });
390
+ }
391
+ function activeRoom(){ return selectedKey ? roomByKey(selectedKey) : null; }
392
+ function hasRecent(){ return !!(model && model.floors.some(function(f){ return !!f.completedAt; })); }
393
+ function setCanvasSize(width, height){
394
+ width = Math.max(1, Math.round(width)); height = Math.max(1, Math.round(height));
395
+ if (canvas.width !== width) canvas.width = width;
396
+ if (canvas.height !== height) canvas.height = height;
397
+ ctx.imageSmoothingEnabled = false;
398
+ }
399
+ function drawBackdrop(cols, rows, room){
400
+ if (!room) setCanvasSize(cols * T, rows * T);
401
+ var width = canvas.width, height = canvas.height, unit = Math.max(1, Math.min(width / 576, height / 320)), wallHeight = Math.round(height * .18);
402
+ ctx.fillStyle = PALETTE[15]; ctx.fillRect(0, 0, width, wallHeight);
403
+ ctx.fillStyle = PALETTE[12]; ctx.fillRect(0, wallHeight, width, height - wallHeight);
404
+ var plankH = 12 * unit, plankW = 64 * unit;
405
+ for (var row = 0, y = wallHeight; y < height; row++, y += plankH) {
406
+ ctx.fillStyle = row % 3 === 0 ? PALETTE[7] : PALETTE[12]; ctx.fillRect(0, y, width, plankH - unit);
407
+ for (var x = -(row % 2) * plankW / 2; x < width; x += plankW) {
408
+ ctx.fillStyle = PALETTE[6]; ctx.fillRect(x, y, unit, plankH);
409
+ ctx.fillStyle = PALETTE[7]; ctx.fillRect(x + 8 * unit, y + 4 * unit, 24 * unit, unit);
410
+ }
411
+ }
412
+ ctx.fillStyle = PALETTE[6]; ctx.fillRect(0, wallHeight - 3 * unit, width, 3 * unit);
413
+ ctx.fillStyle = 'rgba(43,36,31,.12)'; ctx.fillRect(0, wallHeight, width, 4 * unit);
414
+ ctx.fillStyle = PALETTE[9]; ctx.fillRect(0, height - 2 * unit, width, 2 * unit);
415
+ }
416
+ // Furniture stays square-pixeled while its placement follows the room size.
417
+ function furniture(kind, x, y, unit){
418
+ ctx.save(); ctx.translate(Math.round(x), Math.round(y)); unit=Math.max(1,Math.round(unit));ctx.scale(unit, unit);
419
+ function r(x,y,w,h,c){ctx.fillStyle=PALETTE[c];ctx.fillRect(x,y,w,h);}
420
+ 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);}
421
+ 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);}
422
+ 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);}
423
+ if(kind==='desk'||kind==='bench'){
424
+ 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);
425
+ 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);
426
+ 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);
427
+ 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);}
428
+ } else if(kind==='table'){
429
+ 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);
430
+ 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);
431
+ } else if(kind==='window'){
432
+ 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);
433
+ 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);
434
+ } else if(kind==='whiteboard'){
435
+ 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);
436
+ 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);}}
437
+ } else if(kind==='shelf'){
438
+ 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);}}
439
+ 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);
440
+ } else if(kind==='plant'){
441
+ 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);
442
+ } else if(kind==='chair'){
443
+ 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);
444
+ } else if(kind==='door'){
445
+ 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);
446
+ }
447
+ ctx.restore();
448
+ }
449
+ function drawRoomBase(floor){
450
+ drawBackdrop(model.roomCols, model.roomRows, floor);
451
+ 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));
452
+ var savedU = U; U = labelUnit;
453
+ // Sage rugs and oak furniture give each phase a real place in the room.
454
+ floor.phaseAreas.forEach(function(a){
455
+ var area=phaseAreaGeometry(a,canvas.width,canvas.height);
456
+ if(a.phase==='implementing'||a.phase==='reviewing'){
457
+ var x=area.x,y=area.y+area.ry,w=area.width,h=area.height-area.ry;
458
+ 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);
459
+ }
460
+ });
461
+ floor.props.filter(function(p){return p.kind!=='rug'&&p.kind!=='clock';}).forEach(function(p){
462
+ var geometry=propGeometry(p,canvas.width,canvas.height),px=geometry.rawX,py=geometry.rawY,artUnit=geometry.rawScale;
463
+ if(p.kind!=='window'){ctx.fillStyle='rgba(43,36,31,.14)';ctx.fillRect(px+4*unit,py+28*unit,45*unit,6*unit);}
464
+ furniture(p.kind,px,py,artUnit);
465
+ 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));}
466
+ 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);}
467
+ });
468
+ 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]);});
469
+ var titleX=2*rx,titleY=ry,w=19*rx;
470
+ 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);
471
+ 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]);
472
+ var b=floor.board,board=progressBoardGeometry(canvas.width,canvas.height),bx=board.x,by=board.y,bw=board.width,bh=board.height;
473
+ 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);
474
+ fillText('▣ '+t('clickWork'),bx+6*unit,by+5*unit,8,PALETTE[1]);
475
+ fillText(t('progress')+' '+b.passed+'/'+(b.total||'?'),bx+6*unit,by+16*unit,10,PALETTE[1]);
476
+ 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);
477
+ fillText(t('verify')+' '+outcomeText(b.verify)+' '+t('review')+' '+outcomeText(b.review),bx+6*unit,by+40*unit,8,PALETTE[4]);
478
+ var pendingRow=pendingBoardGeometry(canvas.width,canvas.height);
479
+ if(b.pending)fillText('! '+t('pending')+' '+b.pending,pendingRow.x+2*unit,pendingRow.y+3*unit,8,PALETTE[13]);
480
+ var oldS=S;S=Math.max(1,Math.round(unit));var cx=52*rx,cy=ry;draw('clock',cx,cy);
481
+ 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;
482
+ U=savedU;
483
+ ctx.strokeStyle=PALETTE[9];ctx.lineWidth=3*unit;ctx.strokeRect(unit,unit,canvas.width-2*unit,canvas.height-2*unit);
484
+ }
485
+ function drawActor(floor, actor, surface){
486
+ var tx=actor.x*T,ty=actor.y*T,pos=positions[actor.key]||{x:tx,y:ty};
487
+ advanceAvatar(pos,tx,ty,T/5);positions[actor.key]=pos;
488
+ surface=surface||{x:0,y:0,width:canvas.width,height:canvas.height};
489
+ var box=actorBox(actor,surface),oldS=S,oldU=U;S=box.scale;U=box.unit;
490
+ var px=box.x,py=box.y;
491
+ drawAvatar(actor.id,pos,px,py,S,false);
492
+ 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);}
493
+ var text=short(actor.label,10)+' · '+roleText(actor.kind)+' · '+short(statusText(actor.status),8),font=9*U;
494
+ 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);
495
+ 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]);
496
+ var alert=alertGeometry(actor,surface);
497
+ if(actor.alert)draw('alert',alert.x,alert.y);
498
+ S=oldS;U=oldU;
499
+ }
500
+ function actorBox(actor,surface){
501
+ var pos=positions[actor.key]||{x:actor.x*T,y:actor.y*T},scale=avatarScale(surface),unit=Math.max(dpr,Math.min(U,scale));
502
+ 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)))};
503
+ }
504
+ 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};}
505
+ function fitCanvas(){
506
+ var wrapW = wrap && wrap.clientWidth ? wrap.clientWidth : window.innerWidth;
507
+ var wrapH = wrap && wrap.clientHeight ? wrap.clientHeight : window.innerHeight - 54;
508
+ var reserved = (dialogueBox && dialogueBox.offsetHeight || 36) + (roomNav && roomNav.offsetHeight || 30) + 24;
509
+ var maxW = Math.max(220, wrapW - 18), maxH = Math.max(160, wrapH - reserved);
510
+ var ratio = Math.min(maxW / canvas.width, maxH / canvas.height);
511
+ canvas.style.width = Math.max(1, Math.floor(canvas.width * ratio)) + "px"; canvas.style.height = Math.max(1, Math.floor(canvas.height * ratio)) + "px";
512
+ }
513
+ function chooseRenderScale(){
514
+ if(!model)return;
515
+ var wrapW=wrap&&wrap.clientWidth?wrap.clientWidth:window.innerWidth,wrapH=wrap&&wrap.clientHeight?wrap.clientHeight:window.innerHeight-54;
516
+ var reserved=(dialogueBox&&dialogueBox.offsetHeight||36)+(roomNav&&roomNav.offsetHeight||30)+24;
517
+ 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;
518
+ model.cols=model.rows*maxW/maxH;
519
+ T=Math.max(4,maxH/model.rows*nextDpr);S=T/4;U=T/8;dpr=nextDpr;
520
+ if(oldT!==T){Object.keys(positions).forEach(function(key){positions[key].x*=T/oldT;positions[key].y*=T/oldT;});roomCanvases={};}
521
+ }
522
+ function overviewLayout(rooms){
523
+ var count=rooms.length,gap=.6,result=[];if(!count)return result;
524
+ if(count===1)return [{x:gap,y:gap,width:model.cols-6-gap,height:model.rows-gap*2}];
525
+ var hallW=6,left=(model.cols-hallW)/2,right=left,teamIndex=rooms.findIndex(function(f){return f.kind==='run';});
526
+ if(teamIndex>=0&&count<=6){
527
+ left=(model.cols-hallW)*.58;right=model.cols-hallW-left;
528
+ result[teamIndex]={x:gap,y:gap,width:left-gap,height:model.rows-gap*2};
529
+ var others=rooms.filter(function(_,i){return i!==teamIndex;}),height=(model.rows-gap*(others.length+1))/Math.max(1,others.length);
530
+ others.forEach(function(f,i){result[rooms.indexOf(f)]={x:left+hallW,y:gap+i*(height+gap),width:right-gap,height:height};});
531
+ return result;
532
+ }
533
+ var leftCount=Math.ceil(count/2),rightCount=Math.floor(count/2);
534
+ 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};});
535
+ return result;
536
+ }
537
+ function roomBox(floor){var rooms=visibleRooms(),index=rooms.indexOf(floor);return overviewLayout(rooms)[index]||floor.overview;}
538
+ function hallwayBounds(){
539
+ var rooms=visibleRooms(),layout=overviewLayout(rooms),left=layout.find(function(box){return box.x<model.cols/2-3;});
540
+ return {x:left?left.x+left.width:model.cols/2-3,width:6,y:.6,height:model.rows-1.2};
541
+ }
542
+ function ensureSupervisor(){
543
+ if (supervisorReady || !model) return;
544
+ var hall = hallwayBounds(); supervisor.x = supervisor.targetX = hall.x + hall.width / 2; supervisor.y = supervisor.targetY = model.rows - 5; supervisorReady = true;
545
+ }
546
+ function roomEntrances(){
547
+ 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};});
548
+ }
549
+ function supervisorBounds(){var hall=hallwayBounds();return {minX:hall.x+.2,maxX:hall.x+hall.width-.2,minY:1,maxY:model.rows-3};}
550
+ function drawHallway(){
551
+ var hall = hallwayBounds(), x = hall.x * T, y = hall.y * T, w = hall.width * T, h = hall.height * T;
552
+ ctx.fillStyle = "rgba(183,199,163,.55)"; ctx.fillRect(x, y, w, h);
553
+ ctx.strokeStyle = PALETTE[3]; ctx.lineWidth = Math.max(1, 2 * U); ctx.strokeRect(x, y, w, h);
554
+ 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(); }
555
+ 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;
556
+ 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); });
557
+ }
558
+ function drawSupervisor(){
559
+ ensureSupervisor(); if (!model) return;
560
+ advanceAvatar(supervisor,supervisor.targetX,supervisor.targetY,.7);
561
+ 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});}));
562
+ var px=supervisor.x*T-16*scale,py=Math.min(canvas.height-48*scale,supervisor.y*T);drawAvatar('viewer',supervisor,px,py,scale,true);
563
+ drawViewerBadge(supervisor.x*T,py,7);
564
+ 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;
565
+ if (!moving && mode === "overview" && entry) { selectedKey = entry.floor.key; hallwayReturn = {x:supervisor.x, y:supervisor.y}; enterRoomFromHallway(entry.floor); }
566
+ }
567
+ function enterRoomFromHallway(floor){
568
+ if (!floor) return;
569
+ detailTarget=null;detailPage=0;
570
+ roomSupervisor.x = roomSupervisor.targetX = floor.kind === "run" ? model.roomCols - 7 : 6;
571
+ roomSupervisor.y = roomSupervisor.targetY = Math.max(8, Math.min(model.roomRows - 6, Math.round(model.roomRows / 2)));
572
+ mode = "room"; lineIndex = 0; typed = 0; updateHeader(); canvas.focus();
573
+ }
574
+ function drawRoomSupervisor(){
575
+ if (!model) return;
576
+ advanceAvatar(roomSupervisor,roomSupervisor.targetX,roomSupervisor.targetY,.7);
577
+ var scale=avatarScale({width:canvas.width,height:canvas.height});
578
+ 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);
579
+ drawViewerBadge(px+16*scale,py,8);
580
+ }
581
+ function drawViewerBadge(center,top,size){
582
+ var text=t("viewer"),font=Math.max(5,Math.round(size*U)),pad=Math.max(2,3*U);ctx.font="bold "+font+"px monospace";
583
+ 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);
584
+ 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]);
585
+ }
586
+ function moveSupervisor(dx, dy){
587
+ if (!model || !statusBox.hidden) return;
588
+ var state=mode==='room'?roomSupervisor:supervisor;state.direction=dx?(dx<0?'left':'right'):(dy<0?'up':'down');
589
+ var step = mode === "room" ? 1 : Math.min(1.25,(model.rows-2)/Math.max(1,Math.ceil(visibleRooms().length/2))/3);
590
+ 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; }
591
+ 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();
592
+ }
593
+ function renderRoomImage(floor, width, height){
594
+ width=Math.round(width);height=Math.round(height);
595
+ var signature=lang+'|'+T+'|'+width+'x'+height+'|'+JSON.stringify([floor.title,floor.status,floor.board,floor.props,floor.papers,floor.books,floor.clock]);
596
+ var cached=roomCanvases[floor.key];if(cached&&cached.signature===signature)return cached.canvas;
597
+ var roomCanvas=cached?cached.canvas:document.createElement('canvas');
598
+ if(typeof roomCanvas.getContext!=='function')return null;
599
+ roomCanvas.width=width;roomCanvas.height=height;var roomCtx=roomCanvas.getContext('2d');if(!roomCtx)return null;
600
+ var oldCanvas=canvas,oldCtx=ctx;canvas=roomCanvas;ctx=roomCtx;ctx.imageSmoothingEnabled=false;drawRoomBase(floor);canvas=oldCanvas;ctx=oldCtx;
601
+ roomCanvases[floor.key]={canvas:roomCanvas,signature:signature};return roomCanvas;
602
+ }
603
+ function drawOverviewRoom(floor){
604
+ var surface=surfaceForFloor(floor);
605
+ ctx.save();ctx.beginPath();ctx.rect(surface.x,surface.y,surface.width,surface.height);ctx.clip();
606
+ var image=renderRoomImage(floor,surface.width,surface.height);if(image)ctx.drawImage(image,surface.x,surface.y);
607
+ floor.actors.forEach(function(actor){drawActor(floor,actor,surface);});
608
+ if(floor.completedAt){ctx.fillStyle='rgba(53,90,75,.18)';ctx.fillRect(surface.x,surface.y,surface.width,surface.height);}
609
+ if(floor.repo){var plate=short(floor.repo,16),plateWidth=(plate.length*6+8)*U;ctx.fillStyle=repoColor(floor.repo);ctx.fillRect(surface.x+2*U,surface.y+2*U,plateWidth,13*U);ctx.strokeStyle=PALETTE[0];ctx.lineWidth=U;ctx.strokeRect(surface.x+2*U,surface.y+2*U,plateWidth,13*U);fillText(plate,surface.x+6*U,surface.y+4*U,8,PALETTE[15]);}
610
+ ctx.restore();
611
+ }
612
+ function render(){
613
+ if(!model)return;chooseRenderScale();var floor=activeRoom();
614
+ if(mode==='room'&&floor){
615
+ 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);
616
+ floor.actors.forEach(function(actor){drawActor(floor,actor);});drawRoomSupervisor();
617
+ }else{drawBackdrop(model.cols,model.rows,null);drawHallway();visibleRooms().forEach(drawOverviewRoom);drawSupervisor();if(!visibleRooms().length)fillText(t('quiet'),4*T,18*T,12);}
618
+ fitCanvas();
619
+ }
620
+ function renderRoomNav(rooms){
621
+ NAV_PAGE_SIZE=Math.max(1,Math.min(8,Math.floor((window.innerWidth-160)/120)));
622
+ if (mode !== "overview") { if (navSignature !== lang + "|room") { roomNav.textContent = ""; navSignature = lang + "|room"; } return; }
623
+ var pages = Math.max(1, Math.ceil(rooms.length / NAV_PAGE_SIZE));
624
+ navPage = Math.max(0, Math.min(navPage, pages - 1));
625
+ var signature = lang + "|overview|" + (showRecent ? "recent" : "active") + "|" + selectedKey + "|" + navPage + "|" + rooms.map(function(f){ return f.key; }).join(",");
626
+ if (signature === navSignature) return;
627
+ navSignature = signature; roomNav.textContent = "";
628
+ 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); }
629
+ 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); });
630
+ 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); }
631
+ }
632
+ function updateHeader(){
633
+ if(document.documentElement)document.documentElement.lang=lang==='zh'?'zh-Hant':'en';
634
+ 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'));
635
+ renderRepoFilter();
636
+ var rooms = visibleRooms(), people = rooms.reduce(function(n, f){ return n + f.actors.length; }, 0);
637
+ crumb.textContent = mode === "room" && activeRoom() ? activeRoom().title : t("overview") + " · " + rooms.length + " " + t("rooms") + " · " + people + " " + t("people");
638
+ 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 + ")" : "");
639
+ languageButton.textContent = lang === "zh" ? "繁中 / EN" : "EN / 繁中"; live.textContent = offline ? t("offline") : t("connected");
640
+ renderRoomNav(rooms);
641
+ }
642
+ function tickDialogue(){
643
+ if (!model) return;
644
+ var floors = mode === "room" && activeRoom() ? [activeRoom()] : visibleRooms(), lines = [];
645
+ 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")); });
646
+ if (!lines.length) lines = [t("quiet")];
647
+ var line = offline ? t("offline") : localizeText(lines[lineIndex % Math.max(1, lines.length)]);
648
+ typed = Math.min(line.length, typed + 2); dialogueBox.textContent = line.slice(0, typed);
649
+ if (typed === line.length && frame % 180 === 0) { lineIndex++; typed = 0; }
650
+ }
651
+ function enterRoom(){ if (!selectedKey && visibleRooms()[0]) selectedKey = visibleRooms()[0].key; var floor = activeRoom(); if (!floor) return; enterRoomFromHallway(floor); render(); }
652
+ 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(); }
653
+ function changeLanguage(){ lang = lang === "zh" ? "en" : "zh"; saveLanguage(); updateHeader(); render(); }
654
+ function showDialog(title, fill){
655
+ if (statusBox.hidden) detailPreviousFocus = document.activeElement || canvas;
656
+ statusBox.textContent = ""; var heading = document.createElement("h2"); heading.id = "status-title"; heading.textContent = title; statusBox.appendChild(heading);
657
+ var body = document.createElement("div"); body.className = "dialog-body"; statusBox.appendChild(body); fill(body);
658
+ var actions = document.createElement("div"); actions.className = "actions";
659
+ var close = document.createElement("button"); close.type = "button"; close.textContent = t("close"); close.addEventListener("click", closeDialog); actions.appendChild(close);
660
+ statusBox.appendChild(actions); statusBox.hidden = false; dialogClose = close;
661
+ if (app) { app.inert = true; app.setAttribute("aria-hidden", "true"); }
662
+ close.focus();
663
+ }
664
+ 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(); }
665
+ function addLine(box, label, value){ var p = document.createElement("p"); p.textContent = label + ": " + safeText(value); box.appendChild(p); }
666
+ function addCommand(box, value, copyValue){
667
+ var row = document.createElement("p"), code = document.createElement("code"), button = document.createElement("button");
668
+ code.textContent = value; button.type = "button"; button.textContent = t("copy"); button.addEventListener("click", function(){
669
+ if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(copyValue || value).then(function(){ button.textContent = t("copied"); }, function(){});
670
+ }); row.appendChild(code); row.appendChild(button); box.appendChild(row);
671
+ }
672
+ function pageItems(items, box, renderItem){
673
+ var small=window.innerWidth<480, pageSize=Math.max(1,Math.min(small?3:5,Math.floor((window.innerHeight*.88-145)/(small?75:56)))), pages;
674
+ 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];});
675
+ pages = Math.max(1,Math.ceil(items.length/pageSize));detailPage=Math.max(0,Math.min(detailPage,pages-1));
676
+ items.slice(detailPage * pageSize, detailPage * pageSize + pageSize).forEach(function(item){ renderItem(item, box); });
677
+ 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);
678
+ 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); }
679
+ box.appendChild(paging);
680
+ }
681
+ function sourceFor(floor){
682
+ if (!snapshot) return null;
683
+ if (floor.kind === "run") return snapshot.runs.find(function(r){ return r.runId === floor.key; }) || null;
684
+ if (floor.kind === "desk") return floor.sourceIndex === null ? null : snapshot.lobby[floor.sourceIndex] || null;
685
+ if (floor.kind === "review") return floor.sourceIndex === null ? null : snapshot.reviews[floor.sourceIndex] || null;
686
+ return null;
687
+ }
688
+ function sourceSignature(floor){return JSON.stringify(sourceFor(floor),function(key,value){return key==='budget'?undefined:value;});}
689
+ function renderDetailItem(item, box){
690
+ if (item.kind === "command") return addCommand(box, item.value, item.copyValue);
691
+ if (item.kind === "file") { var file = document.createElement("p"); file.textContent = "• " + item.value; box.appendChild(file); return; }
692
+ if (item.kind === "heading") { var heading = document.createElement("p"); heading.textContent = item.value; box.appendChild(heading); return; }
693
+ addLine(box, item.label, item.value);
694
+ }
695
+ function progressText(progress){return progress ? progress.passed+'/'+(progress.total||'?')+' · '+t('verify')+' '+outcomeText(progress.verify||'pending')+' · '+t('review')+' '+outcomeText(progress.review||'pending') : t('unknown');}
696
+ 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};}
697
+ function workItemsFor(floor){
698
+ var items=[
699
+ {kind:'line',label:t('task'),value:floor.board.taskId==='unassigned'?t('unassigned'):floor.board.taskId},
700
+ {kind:'line',label:t('status'),value:statusText(floor.status)},
701
+ {kind:'line',label:t('progress'),value:progressText({passed:floor.board.passed,total:floor.board.total,verify:floor.board.verify,review:floor.board.review})},
702
+ {kind:'line',label:t('pending'),value:String(floor.board.pending)}
703
+ ];
704
+ floor.actors.forEach(function(actor){items.push(workItemForActor(floor,actor));});
705
+ floor.questions.forEach(function(question){items.push({kind:'question',label:t('question')+' · ! '+question.questionId,value:question.prompt});});
706
+ if (!floor.actors.length && !floor.questions.length) items.push({kind:'line',label:t('people'),value:'0'});
707
+ return items;
708
+ }
709
+ function phaseWorkItemsFor(floor,phase){
710
+ var items=[{kind:'line',label:t('phase'),value:phaseLabel(phase)}],actors=floor.actors.filter(function(actor){return actor.phase===phase;});
711
+ if(!actors.length)items.push({kind:'line',label:t('currentWork'),value:t('noPeople')});
712
+ actors.forEach(function(actor){items.push(workItemForActor(floor,actor));});
713
+ return items;
714
+ }
715
+ function questionItemsFor(floor){
716
+ var items=[];
717
+ if(!floor.questions.length)items.push({kind:'line',label:t('questions'),value:t('noQuestions')});
718
+ floor.questions.forEach(function(question){items.push({kind:'line',label:t('question')+' · ! '+question.questionId,value:question.prompt});});
719
+ var source=sourceFor(floor);
720
+ 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});});}
721
+ return items;
722
+ }
723
+ function renderWorkItem(item,box){
724
+ if(item.kind==='work'){
725
+ var row=document.createElement('p'),button=document.createElement('button'),detail=document.createElement('span');
726
+ 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);});
727
+ detail.textContent=' · '+safeText(item.value);row.appendChild(button);row.appendChild(detail);box.appendChild(row);return;
728
+ }
729
+ if(item.kind==='question'){addLine(box,item.label,localizeText(item.value));return;}
730
+ renderDetailItem(item,box);
731
+ }
732
+ function openWorkList(floor){
733
+ if(!detailTarget||detailTarget.floorKey!==floor.key||detailTarget.view!=='work')detailPage=0;
734
+ detailSignature=sourceSignature(floor);detailTarget={floorKey:floor.key,actorKey:null,view:'work'};
735
+ showDialog(floor.title+' · '+t('workList'),function(box){pageItems(workItemsFor(floor),box,renderWorkItem);});
736
+ }
737
+ function openPhaseWorkList(floor,phase){
738
+ if(!detailTarget||detailTarget.floorKey!==floor.key||detailTarget.view!=='phase'||detailTarget.phase!==phase)detailPage=0;
739
+ detailSignature=sourceSignature(floor);detailTarget={floorKey:floor.key,actorKey:null,view:'phase',phase:phase};
740
+ showDialog(floor.title+' · '+phaseLabel(phase),function(box){pageItems(phaseWorkItemsFor(floor,phase),box,renderWorkItem);});
741
+ }
742
+ function openQuestionList(floor){
743
+ if(!detailTarget||detailTarget.floorKey!==floor.key||detailTarget.view!=='questions')detailPage=0;
744
+ detailSignature=sourceSignature(floor);detailTarget={floorKey:floor.key,actorKey:null,view:'questions'};
745
+ showDialog(floor.title+' · '+t('questions'),function(box){pageItems(questionItemsFor(floor),box,renderDetailItem);});
746
+ }
747
+ function openActorDetail(floor, actor){
748
+ var source = sourceFor(floor), items = [], entries = [];
749
+ 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 || []; }
750
+ if (source && floor.kind === "desk" && source.diff) items = source.diff.paths || [];
751
+ items.forEach(function(item){ entries.push({kind:"file", value:item}); });
752
+ if (source && source.commands) source.commands.forEach(function(item){ entries.push({kind:"command", value:item}); });
753
+ if (!detailTarget || detailTarget.floorKey !== floor.key || detailTarget.actorKey !== actor.key) detailPage = 0;
754
+ var detailItems = [
755
+ {kind:"line", label:lang==='zh'?'職責':'Role', value:roleText(actor.kind)},
756
+ {kind:"line", label:t("status"), value:statusText(actor.status)}, {kind:"line", label:t("phase"), value:phaseLabel(actor.phase)},
757
+ {kind:"line", label:t("task"), value:actor.taskId === "unassigned" ? t("unassigned") : actor.taskId},
758
+ {kind:"line", label:t("host"), value:localizeText(actor.host)}, {kind:"line", label:t("now"), value:localizeText(actor.narration)}
759
+ ];
760
+ if (actor.questionCount) detailItems.push({kind:"line", label:t("pending"), value:String(actor.questionCount)});
761
+ 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 || "-")});
762
+ if (actor.questionCount && floor.questions.length) floor.questions.forEach(function(question){ detailItems.push({kind:"line", label:"! " + question.questionId, value:question.prompt}); });
763
+ if (entries.length) { detailItems.push({kind:"heading", value:t("files") + " / " + t("commands") + ":"}); entries.forEach(function(entry){ detailItems.push(entry); }); }
764
+ detailSignature=sourceSignature(floor); detailTarget = {floorKey: floor.key, actorKey: actor.key, view:'actor'}; showDialog(actor.label, function(box){ pageItems(detailItems, box, renderDetailItem); });
765
+ }
766
+ function openRoomDetail(floor){
767
+ if (!detailTarget || detailTarget.floorKey !== floor.key || detailTarget.actorKey !== null) detailPage = 0;
768
+ detailSignature=sourceSignature(floor); detailTarget = {floorKey: floor.key, actorKey: null, view:'room'}; var source = sourceFor(floor), items = source && source.diff ? source.diff.paths || [] : [], entries = [];
769
+ items.forEach(function(item){ entries.push({kind:"file", value:item}); });
770
+ if (source && source.commands) source.commands.forEach(function(item){ entries.push({kind:"command", value:item}); });
771
+ var detailItems = [
772
+ {kind:"line", label:t("status"), value:statusText(floor.status)}, {kind:"line", label:t("phase"), value:phaseLabel(floor.phase)},
773
+ {kind:"line", label:t("task"), value:floor.board.taskId === "unassigned" ? t("unassigned") : floor.board.taskId},
774
+ {kind:"line", label:t("progress"), value:floor.board.passed + "/" + (floor.board.total || "?")},
775
+ {kind:"line", label:t("verify"), value:outcomeText(floor.board.verify)}, {kind:"line", label:t("review"), value:outcomeText(floor.board.review)},
776
+ {kind:"line", label:t("pending"), value:String(floor.board.pending)}
777
+ ];
778
+ floor.questions.forEach(function(question){ detailItems.push({kind:"line", label:"! " + question.questionId, value:question.prompt}); });
779
+ if (entries.length) { detailItems.push({kind:"heading", value:t("files") + " / " + t("commands") + ":"}); entries.forEach(function(entry){ detailItems.push(entry); }); }
780
+ showDialog(floor.title, function(box){ pageItems(detailItems, box, renderDetailItem); });
781
+ }
782
+ function openSelectedDetail(){
783
+ var target = detailTarget, floor = target ? roomByKey(target.floorKey) : activeRoom(); if (!floor) return;
784
+ if (target && target.view === 'work') return openWorkList(floor);
785
+ if (target && target.view === 'phase') return openPhaseWorkList(floor,target.phase);
786
+ if (target && target.view === 'questions') return openQuestionList(floor);
787
+ if (target && target.actorKey) { var actor = floor.actors.find(function(a){ return a.key === target.actorKey; }); if (actor) return openActorDetail(floor, actor); }
788
+ return openRoomDetail(floor);
789
+ }
790
+ function hitOverview(event){
791
+ var point=canvasPoint(event),x=point.x/T,y=point.y/T;
792
+ 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;
793
+ }
794
+ function hitActor(event, floor, surface){
795
+ var point=canvasPoint(event);surface=surface||{x:0,y:0,width:canvas.width,height:canvas.height};
796
+ 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; }
797
+ return null;
798
+ }
799
+ function surfaceRect(rect,surface){return {x:rect.x+surface.x,y:rect.y+surface.y,width:rect.width,height:rect.height};}
800
+ function hitQuestionBadge(event,floor,surface){
801
+ var point=canvasPoint(event);surface=surface||{x:0,y:0,width:canvas.width,height:canvas.height};
802
+ 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;}
803
+ return null;
804
+ }
805
+ function actorForProp(floor,prop){
806
+ var prefix=floor.key+':desk:';
807
+ if(prop.kind!=='desk'||prop.key.indexOf(prefix)!==0)return null;
808
+ var id=prop.key.slice(prefix.length);
809
+ return floor.actors.find(function(actor){return actor.id===id;})||null;
810
+ }
811
+ function hitWorkBoard(event,floor,surface){
812
+ var point=canvasPoint(event),board=progressBoardGeometry(surface.width,surface.height);
813
+ board.x+=surface.x;board.y+=surface.y;
814
+ 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';}
815
+ if(inside(point,board))return 'progress';
816
+ var whiteboard=floor.props.find(function(prop){return prop.kind==='whiteboard';});
817
+ if(whiteboard){var rect=surfaceRect(propGeometry(whiteboard,surface.width,surface.height),surface);if(inside(point,rect))return 'whiteboard';}
818
+ return null;
819
+ }
820
+ function hitWorkTarget(event,floor,surface){
821
+ var point=canvasPoint(event),props=floor.props;
822
+ 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)};}
823
+ 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};}
824
+ return null;
825
+ }
826
+ canvas.addEventListener("click", function(event){
827
+ if (!model) return;
828
+ if (mode === "overview") {
829
+ var floor = hitOverview(event); if (!floor) return; selectedKey=floor.key;var surface=surfaceForFloor(floor),actor=hitActor(event,floor,surface);
830
+ if(actor){enterRoom();openActorDetail(activeRoom()||floor,actor);return;}
831
+ var badge=hitQuestionBadge(event,floor,surface);if(badge){openQuestionList(floor);return;}
832
+ var boardHit=hitWorkBoard(event,floor,surface);if(boardHit==='questions'){openQuestionList(floor);return;}if(boardHit){openWorkList(floor);return;}
833
+ var work=hitWorkTarget(event,floor,surface);if(work){if(work.actor)openActorDetail(floor,work.actor);else if(work.phase)openPhaseWorkList(floor,work.phase);return;}
834
+ enterRoom();return;
835
+ }
836
+ var room = activeRoom(); if (!room) return; var roomSurface=surfaceForFloor(room), actor = hitActor(event, room, roomSurface); if (actor) openActorDetail(room, actor); else {
837
+ var roomBadge=hitQuestionBadge(event,room,roomSurface);if(roomBadge){openQuestionList(room);return;}
838
+ var roomBoard=hitWorkBoard(event,room,roomSurface);if(roomBoard==='questions'){openQuestionList(room);return;}if(roomBoard){openWorkList(room);return;}
839
+ 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;}
840
+ openRoomDetail(room);
841
+ }
842
+ });
843
+ canvas.addEventListener("keydown", function(event){
844
+ if (event.ctrlKey || event.metaKey || event.altKey) return;
845
+ if (event.key === "Escape" || event.key === "Backspace") { event.preventDefault(); if (!statusBox.hidden) closeDialog(); else if (mode === "room") goOverview(); return; }
846
+ var keyboardFloor=activeRoom()||roomByKey(selectedKey)||visibleRooms()[0];
847
+ if (event.key === "l" || event.key === "L") { if (keyboardFloor) { event.preventDefault(); openWorkList(keyboardFloor); return; } }
848
+ if (event.key === "q" || event.key === "Q") { if (keyboardFloor) { event.preventDefault(); openQuestionList(keyboardFloor); return; } }
849
+ var phaseKeys={"1":"planning","2":"implementing","3":"verifying","4":"reviewing","5":"integrating"};
850
+ if (phaseKeys[event.key] && keyboardFloor) { event.preventDefault(); openPhaseWorkList(keyboardFloor,phaseKeys[event.key]); return; }
851
+ if (mode === "overview" || mode === "room") {
852
+ 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];
853
+ if (move) { event.preventDefault(); moveSupervisor(move[0], move[1]); return; }
854
+ }
855
+ var rooms = visibleRooms(); if (!rooms.length) return;
856
+ var index = Math.max(0, rooms.findIndex(function(f){ return f.key === selectedKey; }));
857
+ if (event.key === "ArrowRight" || event.key === "ArrowDown") { event.preventDefault(); selectedKey = rooms[(index + 1) % rooms.length].key; updateHeader(); render(); }
858
+ if (event.key === "ArrowLeft" || event.key === "ArrowUp") { event.preventDefault(); selectedKey = rooms[(index - 1 + rooms.length) % rooms.length].key; updateHeader(); render(); }
859
+ if (event.key === "Enter" && mode === "overview") { event.preventDefault(); enterRoom(); }
860
+ if (event.key === "Enter" && mode === "room") { event.preventDefault(); openSelectedDetail(); }
861
+ });
862
+ repoSelect.addEventListener("change", function(){ repoFilter = repoSelect.value; navPage = 0; if (activeRoom() && repoFilter && activeRoom().repo !== repoFilter) mode = "overview"; if (!selectedKey || visibleRooms().every(function(f){ return f.key !== selectedKey; })) selectedKey = visibleRooms()[0] && visibleRooms()[0].key; updateHeader(); render(); });
863
+ backButton.addEventListener("click", goOverview); recentButton.addEventListener("click", function(){ showRecent = !showRecent; if (!showRecent && activeRoom() && activeRoom().completedAt) mode = "overview"; updateHeader(); render(); });
864
+ languageButton.addEventListener("click", changeLanguage); statusBox.addEventListener("keydown", function(event){
865
+ if (event.key === "Escape") { event.preventDefault(); closeDialog(); return; }
866
+ if (event.key !== "Tab") return;
867
+ var focusables = statusBox.querySelectorAll ? Array.prototype.slice.call(statusBox.querySelectorAll("button:not([disabled])")) : (dialogClose ? [dialogClose] : []);
868
+ if (!focusables.length) return;
869
+ var first = focusables[0], last = focusables[focusables.length - 1];
870
+ if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
871
+ else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
872
+ });
873
+ 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(); }); }
874
+ function loop(){ frame++; render(); tickDialogue(); requestAnimationFrame(loop); }
875
+ updateHeader(); poll(); setInterval(poll, 2000); window.addEventListener("resize", render); requestAnimationFrame(loop);
876
+ `;
877
+ /** One inline page; the nonce binds its only script and style under the server's CSP. */
878
+ export function officePage(nonce) {
879
+ const script = CLIENT.replace("__PALETTE__", () => JSON.stringify(PALETTE)).replace("__SPRITES__", () => JSON.stringify(SPRITES))
880
+ .replace("__SCENE__", () => sceneModel.toString()).replace("__AVATAR__", () => avatarSprite.toString()).replace("__AVATAR_COLORS__", () => avatarPalette.toString());
881
+ return `<!doctype html>
882
+ <html lang="zh-Hant"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
883
+ <meta name="referrer" content="no-referrer"><title>agent-ops Office · Preview</title><style nonce="${nonce}">${STYLE}</style></head>
884
+ <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><select id="repo-filter" hidden></select><button id="language" type="button">繁中 / EN</button><span id="live" aria-live="polite">● Connected</span></header>
885
+ <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>
886
+ <section id="status" role="dialog" aria-modal="true" aria-labelledby="status-title" tabindex="-1" hidden></section><script nonce="${nonce}">${script}</script></body></html>`;
887
+ }