let-them-talk 5.3.0 → 5.4.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 (166) hide show
  1. package/CHANGELOG.md +3 -1
  2. package/README.md +158 -592
  3. package/SECURITY.md +3 -3
  4. package/USAGE.md +151 -0
  5. package/agent-contracts.js +447 -0
  6. package/api-agents.js +760 -0
  7. package/autonomy/decision-v2.js +380 -0
  8. package/autonomy/watchdog-policy.js +572 -0
  9. package/cli.js +454 -298
  10. package/conversation-templates/autonomous-feature.json +83 -22
  11. package/conversation-templates/code-review.json +69 -21
  12. package/conversation-templates/debug-squad.json +69 -21
  13. package/conversation-templates/feature-build.json +69 -21
  14. package/conversation-templates/research-write.json +69 -21
  15. package/dashboard.html +3148 -174
  16. package/dashboard.js +823 -786
  17. package/data-dir.js +58 -0
  18. package/docs/architecture/branch-semantics.md +157 -0
  19. package/docs/architecture/canonical-event-schema.md +88 -0
  20. package/docs/architecture/markdown-workspace.md +183 -0
  21. package/docs/architecture/runtime-contract.md +459 -0
  22. package/docs/architecture/runtime-migration-hardening.md +64 -0
  23. package/events/hooks.js +154 -0
  24. package/events/log.js +457 -0
  25. package/events/replay.js +33 -0
  26. package/events/schema.js +432 -0
  27. package/managed-team-integration.js +261 -0
  28. package/office/agents.js +704 -597
  29. package/office/animation.js +1 -1
  30. package/office/assets/arcade-cabinet.js +141 -0
  31. package/office/assets/archway.js +77 -0
  32. package/office/assets/bar-counter.js +91 -0
  33. package/office/assets/bar-stool.js +71 -0
  34. package/office/assets/beanbag.js +64 -0
  35. package/office/assets/bench.js +99 -0
  36. package/office/assets/bollard.js +87 -0
  37. package/office/assets/cactus.js +100 -0
  38. package/office/assets/carpet-tile.js +46 -0
  39. package/office/assets/chair.js +123 -0
  40. package/office/assets/chandelier.js +107 -0
  41. package/office/assets/coffee-machine.js +95 -0
  42. package/office/assets/coffee-table.js +81 -0
  43. package/office/assets/column.js +95 -0
  44. package/office/assets/desk-lamp.js +102 -0
  45. package/office/assets/desk.js +76 -0
  46. package/office/assets/dining-table.js +105 -0
  47. package/office/assets/door.js +70 -0
  48. package/office/assets/dual-monitor.js +72 -0
  49. package/office/assets/fence.js +76 -0
  50. package/office/assets/filing-cabinet.js +111 -0
  51. package/office/assets/floor-lamp.js +69 -0
  52. package/office/assets/floor-tile.js +54 -0
  53. package/office/assets/flower-pot.js +76 -0
  54. package/office/assets/foosball.js +95 -0
  55. package/office/assets/fridge.js +99 -0
  56. package/office/assets/gaming-chair.js +154 -0
  57. package/office/assets/gaming-desk.js +105 -0
  58. package/office/assets/glass-door.js +72 -0
  59. package/office/assets/glass-wall.js +64 -0
  60. package/office/assets/half-wall.js +49 -0
  61. package/office/assets/hanging-plant.js +112 -0
  62. package/office/assets/index.js +151 -0
  63. package/office/assets/indoor-tree.js +90 -0
  64. package/office/assets/l-sofa.js +153 -0
  65. package/office/assets/marble-floor.js +64 -0
  66. package/office/assets/materials.js +40 -0
  67. package/office/assets/meeting-table.js +88 -0
  68. package/office/assets/microwave.js +94 -0
  69. package/office/assets/monitor.js +67 -0
  70. package/office/assets/neon-strip.js +73 -0
  71. package/office/assets/painting.js +84 -0
  72. package/office/assets/palm-tree.js +108 -0
  73. package/office/assets/pc-tower.js +91 -0
  74. package/office/assets/pendant-light.js +67 -0
  75. package/office/assets/ping-pong.js +114 -0
  76. package/office/assets/plant.js +72 -0
  77. package/office/assets/planter-box.js +95 -0
  78. package/office/assets/pool-table.js +94 -0
  79. package/office/assets/printer.js +113 -0
  80. package/office/assets/reception-desk.js +133 -0
  81. package/office/assets/rug.js +78 -0
  82. package/office/assets/sculpture.js +85 -0
  83. package/office/assets/server-rack.js +98 -0
  84. package/office/assets/sink.js +109 -0
  85. package/office/assets/sofa.js +106 -0
  86. package/office/assets/speaker.js +83 -0
  87. package/office/assets/spotlight.js +83 -0
  88. package/office/assets/street-lamp.js +97 -0
  89. package/office/assets/trash-can.js +83 -0
  90. package/office/assets/treadmill.js +126 -0
  91. package/office/assets/trophy.js +89 -0
  92. package/office/assets/tv-screen.js +79 -0
  93. package/office/assets/vase.js +84 -0
  94. package/office/assets/wall-clock.js +84 -0
  95. package/office/assets/wall.js +53 -0
  96. package/office/assets/water-cooler.js +146 -0
  97. package/office/assets/whiteboard.js +115 -0
  98. package/office/assets.js +3 -431
  99. package/office/builder.js +791 -355
  100. package/office/campus-env.js +1012 -1119
  101. package/office/environment.js +2 -0
  102. package/office/gallery.js +997 -0
  103. package/office/index.js +165 -61
  104. package/office/navigation.js +173 -152
  105. package/office/player.js +178 -68
  106. package/office/robot-character.js +272 -0
  107. package/office/spectator-camera.js +33 -10
  108. package/office/state.js +2 -0
  109. package/office/world-save.js +35 -4
  110. package/package.json +57 -3
  111. package/providers/comfyui.js +383 -0
  112. package/providers/dalle.js +79 -0
  113. package/providers/gemini.js +181 -0
  114. package/providers/ollama.js +184 -0
  115. package/providers/replicate.js +115 -0
  116. package/providers/zai.js +183 -0
  117. package/runtime-descriptor.js +270 -0
  118. package/scripts/check-agent-contract-advisory.js +132 -0
  119. package/scripts/check-api-agent-parity.js +277 -0
  120. package/scripts/check-autonomy-v2-decision.js +207 -0
  121. package/scripts/check-autonomy-v2-execution.js +588 -0
  122. package/scripts/check-autonomy-v2-watchdog.js +224 -0
  123. package/scripts/check-branch-fork-snapshot.js +337 -0
  124. package/scripts/check-branch-isolation.js +787 -0
  125. package/scripts/check-branch-semantics.js +139 -0
  126. package/scripts/check-dashboard-control-plane.js +1304 -0
  127. package/scripts/check-docs-onboarding.js +490 -0
  128. package/scripts/check-event-schema.js +276 -0
  129. package/scripts/check-evidence-completion.js +239 -0
  130. package/scripts/check-invariants.js +992 -0
  131. package/scripts/check-lifecycle-hooks.js +525 -0
  132. package/scripts/check-managed-team-integration.js +166 -0
  133. package/scripts/check-markdown-workspace-export.js +548 -0
  134. package/scripts/check-markdown-workspace-safety.js +347 -0
  135. package/scripts/check-markdown-workspace.js +136 -0
  136. package/scripts/check-message-replay.js +429 -0
  137. package/scripts/check-migration-hardening.js +300 -0
  138. package/scripts/check-performance-indexing.js +272 -0
  139. package/scripts/check-provider-capabilities.js +316 -0
  140. package/scripts/check-runtime-contract.js +109 -0
  141. package/scripts/check-session-aware-context.js +172 -0
  142. package/scripts/check-session-lifecycle.js +210 -0
  143. package/scripts/export-markdown-workspace.js +84 -0
  144. package/scripts/fixtures/message-replay/clean.jsonl +2 -0
  145. package/scripts/fixtures/message-replay/corrupt-correction-payload.jsonl +1 -0
  146. package/scripts/fixtures/message-replay/corrupt-jsonl.jsonl +1 -0
  147. package/scripts/fixtures/message-replay/corrupt-payload.jsonl +1 -0
  148. package/scripts/fixtures/message-replay/out-of-order.jsonl +2 -0
  149. package/scripts/migrate-legacy-to-canonical.js +201 -0
  150. package/scripts/run-verification-suite.js +242 -0
  151. package/scripts/sync-packaged-docs.js +69 -0
  152. package/server.js +9546 -7216
  153. package/state/agents.js +161 -0
  154. package/state/canonical.js +3068 -0
  155. package/state/dashboard-queries.js +441 -0
  156. package/state/evidence.js +56 -0
  157. package/state/io.js +69 -0
  158. package/state/markdown-workspace.js +951 -0
  159. package/state/messages.js +669 -0
  160. package/state/sessions.js +683 -0
  161. package/state/tasks-workflows.js +92 -0
  162. package/templates/debate.json +2 -2
  163. package/templates/managed.json +4 -4
  164. package/templates/pair.json +2 -2
  165. package/templates/review.json +2 -2
  166. package/templates/team.json +3 -3
package/office/agents.js CHANGED
@@ -1,597 +1,704 @@
1
- import { S } from './state.js';
2
- import { DESK_POSITIONS, SPAWN_POS, REST_AREA_POS, REST_AREA_ENTRANCE } from './constants.js';
3
- import { createCharacter } from './character.js';
4
- import { resolveAppearance } from './appearance.js';
5
- import { buildHair } from './hair.js';
6
- import { buildFaceSprite, setEmotion } from './face.js';
7
- import { buildOutfit, removeOutfit } from './outfits.js';
8
- import { getNavigationPath } from './navigation.js';
9
-
10
- // Navigate agent using waypoint pathfinding (campus) or direct walk (other envs)
11
- export function navigateTo(agent, tx, tz, callback) {
12
- var path = getNavigationPath(agent.pos.x, agent.pos.z, tx, tz);
13
- if (!path || path.length === 0) {
14
- walkTo(agent, tx, tz, callback);
15
- return;
16
- }
17
- // Queue all waypoints, put callback on the last one
18
- agent.walkQueue = [];
19
- for (var i = 1; i < path.length; i++) {
20
- agent.walkQueue.push({ x: path[i].x, z: path[i].z, cb: null, triggerDoor: path[i].triggerDoor });
21
- }
22
- // Attach callback to last queued point (or first walk if only 1 point)
23
- if (agent.walkQueue.length > 0) {
24
- agent.walkQueue[agent.walkQueue.length - 1].cb = callback;
25
- }
26
- // Start walking to first point
27
- var first = path[0];
28
- walkTo(agent, first.x, first.z, first.triggerDoor ? function() { triggerManagerDoor(true); } : (path.length === 1 ? callback : null));
29
- }
30
-
31
- function triggerManagerDoor(open) {
32
- if (S._managerDoor) {
33
- S._managerDoorOpen = open ? 1 : 0;
34
- }
35
- }
36
-
37
- export function walkTo(agent, tx, tz, callback) {
38
- var dx = tx - agent.pos.x;
39
- var dz = tz - agent.pos.z;
40
- var dist = Math.sqrt(dx * dx + dz * dz);
41
- agent.walkStart = { x: agent.pos.x, z: agent.pos.z };
42
- agent.target = { x: tx, z: tz, cb: callback || null };
43
- agent.walkProgress = 0;
44
- agent.walkDuration = Math.max(dist * 0.4, 0.3);
45
- }
46
-
47
- export function showBubble(agent, text) {
48
- var display = text.length > 80 ? text.substring(0, 77) + '...' : text;
49
- agent.parts.bubbleDiv.textContent = display;
50
- agent.parts.bubbleDiv.style.display = 'block';
51
- agent.parts.bubbleDiv.style.opacity = '1';
52
- agent.bubbleTimer = 4;
53
- agent.bubbleText = display;
54
- }
55
-
56
- function getDeskPositions() {
57
- return S._campusDeskPositions || DESK_POSITIONS;
58
- }
59
-
60
- function assignDesk(agentName) {
61
- var desks = getDeskPositions();
62
- var used = {};
63
- for (var n in S.agents3d) used[S.agents3d[n].deskIdx] = true;
64
-
65
- // If campus mode, check if agent has "Manager" role or name — assign last desk (manager office)
66
- if (S.currentEnv === 'campus' && desks.length > 0) {
67
- var info = (window.cachedAgents || {})[agentName] || {};
68
- var role = (info.role || '').toLowerCase();
69
- var dname = (info.display_name || agentName).toLowerCase();
70
- var regName = agentName.toLowerCase();
71
- var isManager = role === 'manager' || role === 'project lead' || role === 'ceo' || role === 'director' ||
72
- role.indexOf('project manager') >= 0 || role.indexOf('team lead') >= 0 ||
73
- dname === 'manager' || regName === 'manager';
74
- var managerIdx = desks.length - 1; // last desk is manager office
75
- if (isManager && !used[managerIdx]) {
76
- return managerIdx;
77
- }
78
- // Non-manager agents skip the manager desk
79
- for (var i = 0; i < desks.length - 1; i++) {
80
- if (!used[i]) return i;
81
- }
82
- return Object.keys(S.agents3d).length % (desks.length - 1);
83
- }
84
-
85
- for (var j = 0; j < desks.length; j++) {
86
- if (!used[j]) return j;
87
- }
88
- return Object.keys(S.agents3d).length % desks.length;
89
- }
90
-
91
- function fetchTasks() {
92
- var base = window.currentProjectPath ? '/api/tasks?project=' + encodeURIComponent(window.currentProjectPath) : '/api/tasks';
93
- fetch(base).then(function(r) { return r.json(); }).then(function(data) {
94
- S.cachedTasks = Array.isArray(data) ? data : (data.tasks || []);
95
- }).catch(function() {});
96
- }
97
-
98
- function getAgentTask(agentName) {
99
- for (var i = 0; i < S.cachedTasks.length; i++) {
100
- var t = S.cachedTasks[i];
101
- if (t.assignee === agentName || t.assigned_to === agentName) return t;
102
- }
103
- return null;
104
- }
105
-
106
- function updateConversationVelocity() {
107
- var history = window.cachedHistory;
108
- if (!history || history.length === 0) { S.conversationVelocity = 0; return; }
109
- var now = Date.now();
110
- var cutoff30s = now - 30000;
111
- var cutoff2m = now - 120000;
112
- var recent30 = 0, recent2m = 0;
113
- for (var i = history.length - 1; i >= 0; i--) {
114
- var ts = new Date(history[i].timestamp).getTime();
115
- if (ts > cutoff30s) recent30++;
116
- if (ts > cutoff2m) recent2m++;
117
- if (ts <= cutoff2m) break;
118
- }
119
- S.conversationVelocity = recent30 >= 3 ? 1 : (recent2m === 0 ? -1 : 0);
120
- }
121
-
122
- function updateLabel(agent) {
123
- var nameEl = agent.parts.labelDiv.querySelector('.office3d-label-name');
124
- var dotEl = agent.parts.labelDiv.querySelector('.office3d-label-dot');
125
- if (nameEl) nameEl.textContent = agent.displayName;
126
- if (dotEl) {
127
- var colors = { active: '#4ade80', sleeping: '#facc15', dead: '#f87171' };
128
- dotEl.style.background = colors[agent.state] || '#f87171';
129
- }
130
- }
131
-
132
- function updateDeskScreen(deskIdx, status, isListening) {
133
- var desk = S.deskMeshes[deskIdx];
134
- if (!desk) return;
135
- if (status === 'active' && isListening) {
136
- // Listening green screen
137
- desk.screenMat.emissive.setHex(0x22c55e);
138
- desk.screenMat.emissiveIntensity = 0.5;
139
- desk.screenMat.color.setHex(0x22c55e);
140
- } else if (status === 'active' && !isListening) {
141
- // Active but NOT listening — red screen
142
- desk.screenMat.emissive.setHex(0xef4444);
143
- desk.screenMat.emissiveIntensity = 0.6;
144
- desk.screenMat.color.setHex(0xef4444);
145
- } else if (status === 'sleeping') {
146
- desk.screenMat.emissive.setHex(0x1a2744);
147
- desk.screenMat.emissiveIntensity = 0.15;
148
- desk.screenMat.color.setHex(0x1a2744);
149
- } else {
150
- desk.screenMat.emissive.setHex(0x333333);
151
- desk.screenMat.emissiveIntensity = 0.1;
152
- desk.screenMat.color.setHex(0x333333);
153
- }
154
- }
155
-
156
- function flashDeskScreen(deskIdx) {
157
- var desk = S.deskMeshes[deskIdx];
158
- if (!desk) return;
159
- // Flash white briefly — the next syncAgents call (every 2s) will set the correct persistent color via updateDeskScreen
160
- desk.screenMat.emissive.setHex(0xffffff);
161
- desk.screenMat.emissiveIntensity = 1.5;
162
- setTimeout(function() {
163
- // Force immediate red until next sync corrects it
164
- desk.screenMat.emissive.setHex(0xef4444);
165
- desk.screenMat.emissiveIntensity = 0.6;
166
- desk.screenMat.color.setHex(0xef4444);
167
- }, 300);
168
- }
169
-
170
- function rebuildCharacterAppearance(agent) {
171
- var a = resolveAppearance(agent.displayName, agent.appearance);
172
- agent.parts.bodyMat.color.setHex(a.shirt_hex);
173
- agent.parts.armMat.color.setHex(a.shirt_hex);
174
- agent.parts.legMat.color.setHex(a.pants_hex);
175
- agent.parts.headMat.color.setHex(a.head_hex);
176
- agent.parts.handMat.color.setHex(a.head_hex);
177
- agent.parts.shoeMat.color.setHex(a.shoe_hex);
178
-
179
- // Rebuild hair
180
- var oldHair = agent.parts.hairGroup;
181
- agent.parts.group.remove(oldHair);
182
- oldHair.traverse(function(c) { if (c.geometry) c.geometry.dispose(); if (c.material) c.material.dispose(); });
183
- var newHair = buildHair(a.hair_style, a.hair_hex);
184
- newHair.position.y = 1.05;
185
- agent.parts.group.add(newHair);
186
- agent.parts.hairGroup = newHair;
187
-
188
- // Rebuild face
189
- var oldFace = agent.parts.faceSprite;
190
- agent.parts.head.remove(oldFace);
191
- if (oldFace.material.map) oldFace.material.map.dispose();
192
- oldFace.material.dispose();
193
- var newFace = buildFaceSprite(a.eye_style, a.mouth_style, agent.state === 'sleeping');
194
- newFace.position.set(0, 0, 0.251);
195
- agent.parts.head.add(newFace);
196
- agent.parts.faceSprite = newFace;
197
-
198
- // Rebuild outfit
199
- removeOutfit(agent.parts.group);
200
- if (a.outfit) {
201
- agent.parts.outfitGroup = buildOutfit(a.outfit, { shirt_color: a.shirt_color, pants_color: a.pants_color }, agent.parts.group);
202
- } else {
203
- agent.parts.outfitGroup = null;
204
- }
205
- }
206
-
207
- export function syncAgents() {
208
- if (!window.cachedAgents) return;
209
-
210
- fetchTasks();
211
- updateConversationVelocity();
212
-
213
- for (var name in window.cachedAgents) {
214
- var info = window.cachedAgents[name];
215
- if (!S.agents3d[name]) {
216
- var deskIdx = assignDesk(name);
217
- var allDesks = getDeskPositions();
218
- var deskPos = allDesks[deskIdx] || allDesks[0];
219
- var parts = createCharacter(info.display_name || name, info.appearance || {});
220
- var agent = {
221
- name: name,
222
- displayName: info.display_name || name,
223
- appearance: info.appearance || {},
224
- parts: parts,
225
- deskIdx: deskIdx,
226
- deskPos: { x: deskPos.x, z: deskPos.z },
227
- pos: { x: SPAWN_POS.x, z: SPAWN_POS.z },
228
- target: null,
229
- walkQueue: [],
230
- walkProgress: 0,
231
- walkDuration: 0,
232
- walkStart: null,
233
- state: info.status || 'active',
234
- prevState: null,
235
- registered: false,
236
- bubbleTimer: 0,
237
- bubbleText: '',
238
- isSitting: false,
239
- sittingLerp: 0,
240
- facingTarget: 0,
241
- zzzActive: false,
242
- sleepTransition: 0,
243
- spawnOpacity: 1,
244
- deathOpacity: 1,
245
- dying: false,
246
- currentTask: null,
247
- taskCelebration: 0,
248
- isListening: !!(info.is_listening),
249
- handRaiseTimer: 0,
250
- waveTimer: 0,
251
- thinkTimer: 0,
252
- pointTimer: 0,
253
- celebrateTimer: 0,
254
- stretchTimer: 0,
255
- idleGestureTimer: 5 + Math.random() * 10,
256
- listenLostTimer: 0,
257
- lastMessageTime: 0,
258
- monitorTimer: 0,
259
- location: 'desk', // 'desk', 'dressing_room', 'rest', 'walking'
260
- };
261
-
262
- parts.group.position.set(SPAWN_POS.x, 0, SPAWN_POS.z);
263
- S.scene.add(parts.group);
264
- updateLabel(agent);
265
- S.agents3d[name] = agent;
266
-
267
- // Registration animation
268
- showBubble(agent, 'Checking in...');
269
- (function(a) {
270
- setTimeout(function() {
271
- navigateTo(a, a.deskPos.x, a.deskPos.z + 0.7, function() {
272
- a.registered = true;
273
- showBubble(a, 'Ready to work!');
274
- updateDeskScreen(a.deskIdx, a.state, a.isListening);
275
- });
276
- }, 800);
277
- })(agent);
278
- } else {
279
- var existing = S.agents3d[name];
280
- var newState = info.status || 'active';
281
- var oldState = existing.state;
282
-
283
- // Don't override local state changes (rest area sleeping, dressing room)
284
- var isLocalOverride = existing.location === 'rest' || existing.location === 'dressing_room' || existing.location === 'walking';
285
- if (newState !== oldState && !isLocalOverride) {
286
- existing.prevState = oldState;
287
- existing.state = newState;
288
- if (newState === 'dead' && !existing.dying) {
289
- existing.dying = true;
290
- existing.deathOpacity = 1;
291
- }
292
- }
293
-
294
- // --- Autonomous behaviors: sleeping rest area, waking → back to desk ---
295
- if (newState === 'sleeping' && oldState === 'active' && existing.location === 'desk' && existing.registered && !existing.dying) {
296
- // Agent fell asleep walk to rest area after a short delay
297
- existing.location = 'walking';
298
- (function(a) {
299
- setTimeout(function() {
300
- showBubble(a, 'Need a break...');
301
- a.isSitting = false;
302
- navigateTo(a, REST_AREA_ENTRANCE.x, REST_AREA_ENTRANCE.z, function() {
303
- navigateTo(a, REST_AREA_POS.x, REST_AREA_POS.z, function() {
304
- a.location = 'rest';
305
- a.state = 'sleeping';
306
- showBubble(a, 'zzz...');
307
- });
308
- });
309
- }, 1000 + Math.random() * 2000);
310
- })(existing);
311
- }
312
- if (newState === 'active' && (oldState === 'sleeping' || existing.location === 'rest') && existing.location !== 'desk' && existing.registered && !existing.dying) {
313
- // Agent woke up — walk back to desk
314
- existing.location = 'walking';
315
- existing.state = 'active';
316
- (function(a) {
317
- showBubble(a, 'Back to work!');
318
- navigateTo(a, a.deskPos.x, a.deskPos.z + 0.7, function() {
319
- a.location = 'desk';
320
- });
321
- })(existing);
322
- }
323
-
324
- existing.displayName = info.display_name || name;
325
- var wasListening = existing.isListening;
326
- existing.isListening = !!(info.is_listening);
327
-
328
- // Detect listen mode change — update screen color persistently
329
- if (wasListening && !existing.isListening) {
330
- // Left listen mode flash then stay red until next sync sets updateDeskScreen
331
- existing.listenLostTimer = 3;
332
- flashDeskScreen(existing.deskIdx);
333
- }
334
- if (!wasListening && existing.isListening) {
335
- // Entered listen mode — next updateDeskScreen will set green
336
- existing.listenLostTimer = 0;
337
- }
338
-
339
- var task = getAgentTask(name);
340
- if (task) {
341
- var prevTask = existing.currentTask;
342
- existing.currentTask = task;
343
- if (prevTask && prevTask.status !== 'done' && task.status === 'done') {
344
- existing.taskCelebration = 2;
345
- existing.celebrateTimer = 1.5;
346
- setEmotion(existing, 'happy', 6);
347
- }
348
- // Blocked task → frustrated face
349
- if (task.status === 'blocked' && (!prevTask || prevTask.status !== 'blocked')) {
350
- setEmotion(existing, 'frustrated', 8);
351
- }
352
- } else {
353
- existing.currentTask = null;
354
- }
355
-
356
- // Listening agents look focused
357
- if (existing.isListening && !wasListening) {
358
- setEmotion(existing, 'focused', 10);
359
- }
360
-
361
- var newApp = info.appearance || {};
362
- if (JSON.stringify(newApp) !== JSON.stringify(existing.appearance)) {
363
- existing.appearance = newApp;
364
- rebuildCharacterAppearance(existing);
365
- }
366
-
367
- updateLabel(existing);
368
- if (existing.registered) updateDeskScreen(existing.deskIdx, existing.state, existing.isListening);
369
- }
370
- }
371
-
372
- // --- Random social behavior: idle agents occasionally stretch or look around ---
373
- // Limit concurrent social walks to prevent traffic jams (max 2 walking at once)
374
- var walkingCount = 0;
375
- for (var wn in S.agents3d) { if (S.agents3d[wn].location === 'walking') walkingCount++; }
376
-
377
- for (var sn in S.agents3d) {
378
- var sa = S.agents3d[sn];
379
- if (!sa.registered || sa.state !== 'active' || sa.location !== 'desk' || sa.target) continue;
380
- if (!sa._socialTimer) sa._socialTimer = 30 + Math.random() * 60;
381
- sa._socialTimer -= 2; // syncAgents runs every ~2s
382
- if (sa._socialTimer <= 0) {
383
- sa._socialTimer = 40 + Math.random() * 80; // next social event in 40-120s
384
- // Pick a random behavior: stretch, look around, or visit another agent
385
- var roll = Math.random();
386
- if (roll < 0.4) {
387
- // Stretch at desk
388
- sa.stretchTimer = 2;
389
- } else if (roll < 0.7) {
390
- // Look around curiously
391
- sa.thinkTimer = 1.5;
392
- } else if (walkingCount < 2) {
393
- // Walk to a random nearby agent's desk to "chat" then return (max 2 concurrent)
394
- var others = [];
395
- for (var on in S.agents3d) {
396
- if (on !== sn && S.agents3d[on].registered && S.agents3d[on].state === 'active' && S.agents3d[on].location === 'desk') {
397
- others.push(S.agents3d[on]);
398
- }
399
- }
400
- if (others.length > 0) {
401
- var buddy = others[Math.floor(Math.random() * others.length)];
402
- (function(a, b) {
403
- a.location = 'walking';
404
- a.isSitting = false;
405
- showBubble(a, 'Hey ' + b.displayName + '!');
406
- setEmotion(a, 'playful', 6);
407
- var stopX = b.deskPos.x + 1.5;
408
- var stopZ = b.deskPos.z + 0.7;
409
- navigateTo(a, stopX, stopZ, function() {
410
- // Face buddy
411
- var dx = b.pos.x - a.pos.x;
412
- var dz = b.pos.z - a.pos.z;
413
- a.facingTarget = Math.atan2(dx, dz);
414
- a.waveTimer = 0.8;
415
- // Buddy turns toward visitor
416
- b.facingTarget = Math.atan2(-dx, -dz);
417
- setTimeout(function() {
418
- showBubble(a, 'Back to it!');
419
- navigateTo(a, a.deskPos.x, a.deskPos.z + 0.7, function() {
420
- a.location = 'desk';
421
- });
422
- // Buddy turns back to desk
423
- setTimeout(function() { b.facingTarget = Math.PI; }, 1500);
424
- }, 3000 + Math.random() * 2000);
425
- });
426
- })(sa, buddy);
427
- }
428
- }
429
- }
430
- }
431
-
432
- for (var n in S.agents3d) {
433
- if (!window.cachedAgents[n]) {
434
- var deadAgent = S.agents3d[n];
435
- if (!deadAgent.dying) {
436
- deadAgent.dying = true;
437
- deadAgent.deathOpacity = 1;
438
- deadAgent.state = 'dead';
439
- }
440
- }
441
- }
442
- }
443
-
444
- export function processMessages() {
445
- var history = window.cachedHistory;
446
- if (!history || history.length === 0) return;
447
-
448
- // Use window-level counter so it persists across 3D stop/start cycles (tab switches)
449
- // This prevents message replay when user switches from Messages tab back to 3D Hub
450
- if (typeof window._lastProcessedMsg === 'undefined') window._lastProcessedMsg = 0;
451
- var newMsgs = history.slice(window._lastProcessedMsg);
452
- window._lastProcessedMsg = history.length;
453
-
454
- for (var i = 0; i < newMsgs.length; i++) {
455
- var msg = newMsgs[i];
456
- var from = S.agents3d[msg.from];
457
- if (!from || !from.registered) continue;
458
- var text = msg.content || msg.message || '';
459
-
460
- from.lastMessageTime = Date.now();
461
- flashDeskScreen(from.deskIdx);
462
-
463
- // Instant preview bubble — show short text immediately before walk animation
464
- // Gives users instant visual feedback that the agent is about to speak
465
- var preview = text.length > 30 ? text.substring(0, 27) + '...' : text;
466
- showBubble(from, preview);
467
-
468
- // Auto-celebrate on task completion events
469
- if (text.indexOf('[EVENT] Task') >= 0 && text.indexOf('completed') >= 0) {
470
- from.celebrateTimer = 1.5;
471
- from.taskCelebration = 2;
472
- }
473
-
474
- // Emotion detection from message content
475
- var textLower = text.toLowerCase();
476
- if (textLower.indexOf('done') >= 0 || textLower.indexOf('pass') >= 0 || textLower.indexOf('success') >= 0 || textLower.indexOf('great') >= 0 || textLower.indexOf('shipped') >= 0) {
477
- setEmotion(from, 'happy', 5);
478
- } else if (textLower.indexOf('error') >= 0 || textLower.indexOf('fail') >= 0 || textLower.indexOf('bug') >= 0 || textLower.indexOf('broken') >= 0) {
479
- setEmotion(from, 'frustrated', 5);
480
- } else if (textLower.indexOf('?') >= 0 && (textLower.indexOf('how') >= 0 || textLower.indexOf('why') >= 0 || textLower.indexOf('what if') >= 0)) {
481
- setEmotion(from, 'thinking', 4);
482
- } else if (textLower.indexOf('!') >= 0 && (textLower.indexOf('wow') >= 0 || textLower.indexOf('amazing') >= 0 || textLower.indexOf('awesome') >= 0)) {
483
- setEmotion(from, 'excited', 4);
484
- }
485
-
486
- // Target agent gets surprised when directly addressed
487
- if (msg.to && msg.to !== 'all' && S.agents3d[msg.to]) {
488
- var targetAgent = S.agents3d[msg.to];
489
- if (targetAgent.registered && targetAgent.isSitting) {
490
- setEmotion(targetAgent, 'surprised', 2);
491
- }
492
- }
493
-
494
- // Contextual gesture based on message type
495
- var isBC = !msg.to || msg.to === 'all';
496
- if (isBC) {
497
- from.waveTimer = 0.8;
498
- } else {
499
- from.pointTimer = 0.6;
500
- }
501
-
502
- // Glance reaction — nearby sitting agents glance toward the speaker
503
- for (var gn in S.agents3d) {
504
- var ga = S.agents3d[gn];
505
- if (gn === msg.from || gn === msg.to || !ga.registered || ga.state !== 'active' || !ga.isSitting) continue;
506
- var gdx = from.pos.x - ga.pos.x;
507
- ga._glanceTarget = from.name;
508
- ga._glanceDirection = gdx > 0 ? 1 : -1; // left or right glance
509
- ga._glanceTimer = 0;
510
- }
511
-
512
- if (msg.to && msg.to !== 'all' && S.agents3d[msg.to]) {
513
- var target = S.agents3d[msg.to];
514
- (function(f, t, txt) {
515
- setTimeout(function() {
516
- f.walkQueue = [];
517
- // Calculate a stop point ~1.8m away from the target, facing them
518
- var tx = t.pos.x, tz = t.pos.z;
519
- var fx = f.pos.x, fz = f.pos.z;
520
- var adx = tx - fx, adz = tz - fz;
521
- var dist = Math.sqrt(adx * adx + adz * adz);
522
- var stopDist = 1.8;
523
- var stopX, stopZ;
524
- if (dist > stopDist + 0.5) {
525
- // Approach from sender's direction, stop 1.8m away
526
- stopX = tx - (adx / dist) * stopDist;
527
- stopZ = tz - (adz / dist) * stopDist;
528
- } else {
529
- // Already close — just step to the side of target's desk
530
- stopX = tx + 1.5;
531
- stopZ = tz;
532
- }
533
- navigateTo(f, stopX, stopZ, function() {
534
- // Sender faces target
535
- var dx2 = t.pos.x - f.pos.x;
536
- var dz2 = t.pos.z - f.pos.z;
537
- f.facingTarget = Math.atan2(dx2, dz2);
538
- showBubble(f, txt);
539
-
540
- // Target turns toward sender (listener reaction)
541
- var rdx = f.pos.x - t.pos.x;
542
- var rdz = f.pos.z - t.pos.z;
543
- t.facingTarget = Math.atan2(rdx, rdz);
544
- t.isListening = true;
545
- t._listeningTo = f.name;
546
-
547
- setTimeout(function() {
548
- // Sender walks back to desk
549
- navigateTo(f, f.deskPos.x, f.deskPos.z + 0.7);
550
- // Target turns back to desk after a short delay
551
- setTimeout(function() {
552
- if (t._listeningTo === f.name) {
553
- t.isListening = false;
554
- t._listeningTo = null;
555
- t.facingTarget = Math.PI; // face desk
556
- }
557
- }, 1500);
558
- }, 4200);
559
- });
560
- }, 400);
561
- })(from, target, text);
562
- } else {
563
- (function(f, txt) {
564
- setTimeout(function() {
565
- f.walkQueue = [];
566
- navigateTo(f, 0, 0, function() {
567
- showBubble(f, txt);
568
- // All nearby agents turn toward the broadcaster
569
- for (var an in S.agents3d) {
570
- var a = S.agents3d[an];
571
- if (a.name === f.name || !a.registered || a.state !== 'active') continue;
572
- var bdx = f.pos.x - a.pos.x;
573
- var bdz = f.pos.z - a.pos.z;
574
- a.facingTarget = Math.atan2(bdx, bdz);
575
- a.isListening = true;
576
- a._listeningTo = f.name;
577
- }
578
- setTimeout(function() {
579
- navigateTo(f, f.deskPos.x, f.deskPos.z + 0.7);
580
- // All listeners turn back
581
- setTimeout(function() {
582
- for (var an2 in S.agents3d) {
583
- var a2 = S.agents3d[an2];
584
- if (a2._listeningTo === f.name) {
585
- a2.isListening = false;
586
- a2._listeningTo = null;
587
- a2.facingTarget = Math.PI;
588
- }
589
- }
590
- }, 1500);
591
- }, 4200);
592
- });
593
- }, 400);
594
- })(from, text);
595
- }
596
- }
597
- }
1
+ import { S } from './state.js';
2
+ import { DESK_POSITIONS, SPAWN_POS, REST_AREA_POS, REST_AREA_ENTRANCE } from './constants.js';
3
+ import { createCharacter } from './character.js';
4
+ import { createRobotCharacter, updateRobotAnimation } from './robot-character.js';
5
+ import { resolveAppearance } from './appearance.js';
6
+ import { buildHair } from './hair.js';
7
+ import { buildFaceSprite, setEmotion } from './face.js';
8
+ import { buildOutfit, removeOutfit } from './outfits.js';
9
+ import { getNavigationPath } from './navigation.js';
10
+ import { GALLERY_SEATS } from './gallery.js';
11
+
12
+ // Track which gallery seats are occupied (prevents overlapping bots)
13
+ var _gallerySeatsOccupied = { image: null, video: null, texture: null };
14
+
15
+ // Map bot capability to gallery seat
16
+ // image_gen → image seat (left monitor), video_gen → video seat (center), texture_gen → texture seat (right)
17
+ // vision/chat bots null (go to regular desk)
18
+ function _getGallerySeat(capability) {
19
+ var seatMap = { image_gen: 'image', video_gen: 'video', texture_gen: 'texture' };
20
+ var seatKey = seatMap[capability];
21
+ if (!seatKey || !GALLERY_SEATS || !GALLERY_SEATS[seatKey]) return null;
22
+ if (_gallerySeatsOccupied[seatKey]) return null; // seat taken
23
+ return GALLERY_SEATS[seatKey];
24
+ }
25
+
26
+ function _claimGallerySeat(capability, agentName) {
27
+ var seatMap = { image_gen: 'image', video_gen: 'video', texture_gen: 'texture' };
28
+ var seatKey = seatMap[capability];
29
+ if (seatKey) _gallerySeatsOccupied[seatKey] = agentName;
30
+ }
31
+
32
+ function _releaseGallerySeat(agentName) {
33
+ for (var key in _gallerySeatsOccupied) {
34
+ if (_gallerySeatsOccupied[key] === agentName) _gallerySeatsOccupied[key] = null;
35
+ }
36
+ }
37
+
38
+ // Get the Z position an agent should walk to when going to their desk
39
+ function _agentDeskZ(agent) {
40
+ return agent.isGallerySeat ? agent.deskPos.z : agent.deskPos.z + 0.7;
41
+ }
42
+
43
+ // Navigate agent using waypoint pathfinding (campus) or direct walk (other envs)
44
+ export function navigateTo(agent, tx, tz, callback) {
45
+ var path = getNavigationPath(agent.pos.x, agent.pos.z, tx, tz);
46
+ if (!path || path.length === 0) {
47
+ walkTo(agent, tx, tz, callback);
48
+ return;
49
+ }
50
+ // Queue all waypoints, put callback on the last one
51
+ agent.walkQueue = [];
52
+ for (var i = 1; i < path.length; i++) {
53
+ agent.walkQueue.push({ x: path[i].x, z: path[i].z, cb: null, triggerDoor: path[i].triggerDoor });
54
+ }
55
+ // Attach callback to last queued point (or first walk if only 1 point)
56
+ if (agent.walkQueue.length > 0) {
57
+ agent.walkQueue[agent.walkQueue.length - 1].cb = callback;
58
+ }
59
+ // Start walking to first point
60
+ var first = path[0];
61
+ walkTo(agent, first.x, first.z, first.triggerDoor ? function() { triggerManagerDoor(true); } : (path.length === 1 ? callback : null));
62
+ }
63
+
64
+ function triggerManagerDoor(open) {
65
+ if (S._managerDoor) {
66
+ S._managerDoorOpen = open ? 1 : 0;
67
+ }
68
+ }
69
+
70
+ export function walkTo(agent, tx, tz, callback) {
71
+ var dx = tx - agent.pos.x;
72
+ var dz = tz - agent.pos.z;
73
+ var dist = Math.sqrt(dx * dx + dz * dz);
74
+ agent.walkStart = { x: agent.pos.x, z: agent.pos.z };
75
+ agent.target = { x: tx, z: tz, cb: callback || null };
76
+ agent.walkProgress = 0;
77
+ agent.walkDuration = Math.max(dist * 0.4, 0.3);
78
+ }
79
+
80
+ export function showBubble(agent, text) {
81
+ var display = text.length > 80 ? text.substring(0, 77) + '...' : text;
82
+ agent.parts.bubbleDiv.textContent = display;
83
+ agent.parts.bubbleDiv.style.display = 'block';
84
+ agent.parts.bubbleDiv.style.opacity = '1';
85
+ agent.bubbleTimer = 4;
86
+ agent.bubbleText = display;
87
+ }
88
+
89
+ function getDeskPositions() {
90
+ return S._campusDeskPositions || DESK_POSITIONS;
91
+ }
92
+
93
+ function assignDesk(agentName) {
94
+ var desks = getDeskPositions();
95
+ var used = {};
96
+ for (var n in S.agents3d) used[S.agents3d[n].deskIdx] = true;
97
+
98
+ // If campus mode, check if agent has "Manager" role or name — assign last desk (manager office)
99
+ if (S.currentEnv === 'campus' && desks.length > 0) {
100
+ var info = (window.cachedAgents || {})[agentName] || {};
101
+ var role = (info.role || '').toLowerCase();
102
+ var dname = (info.display_name || agentName).toLowerCase();
103
+ var regName = agentName.toLowerCase();
104
+ var isManager = role === 'manager' || role === 'project lead' || role === 'ceo' || role === 'director' ||
105
+ role.indexOf('project manager') >= 0 || role.indexOf('team lead') >= 0 ||
106
+ dname === 'manager' || regName === 'manager';
107
+ var managerIdx = desks.length - 1; // last desk is manager office
108
+ if (isManager && !used[managerIdx]) {
109
+ return managerIdx;
110
+ }
111
+ // Non-manager agents skip the manager desk
112
+ for (var i = 0; i < desks.length - 1; i++) {
113
+ if (!used[i]) return i;
114
+ }
115
+ return Object.keys(S.agents3d).length % (desks.length - 1);
116
+ }
117
+
118
+ for (var j = 0; j < desks.length; j++) {
119
+ if (!used[j]) return j;
120
+ }
121
+ return Object.keys(S.agents3d).length % desks.length;
122
+ }
123
+
124
+ function fetchTasks() {
125
+ var base = typeof window.scopedApiUrl === 'function'
126
+ ? window.scopedApiUrl('/api/tasks')
127
+ : (window.currentProjectPath ? '/api/tasks?project=' + encodeURIComponent(window.currentProjectPath) : '/api/tasks');
128
+ fetch(base).then(function(r) { return r.json(); }).then(function(data) {
129
+ S.cachedTasks = Array.isArray(data) ? data : (data.tasks || []);
130
+ }).catch(function() {});
131
+ }
132
+
133
+ function getAgentTask(agentName) {
134
+ for (var i = 0; i < S.cachedTasks.length; i++) {
135
+ var t = S.cachedTasks[i];
136
+ if (t.assignee === agentName || t.assigned_to === agentName) return t;
137
+ }
138
+ return null;
139
+ }
140
+
141
+ function updateConversationVelocity() {
142
+ var history = window.cachedHistory;
143
+ if (!history || history.length === 0) { S.conversationVelocity = 0; return; }
144
+ var now = Date.now();
145
+ var cutoff30s = now - 30000;
146
+ var cutoff2m = now - 120000;
147
+ var recent30 = 0, recent2m = 0;
148
+ for (var i = history.length - 1; i >= 0; i--) {
149
+ var ts = new Date(history[i].timestamp).getTime();
150
+ if (ts > cutoff30s) recent30++;
151
+ if (ts > cutoff2m) recent2m++;
152
+ if (ts <= cutoff2m) break;
153
+ }
154
+ S.conversationVelocity = recent30 >= 3 ? 1 : (recent2m === 0 ? -1 : 0);
155
+ }
156
+
157
+ function updateLabel(agent) {
158
+ var nameEl = agent.parts.labelDiv.querySelector('.office3d-label-name');
159
+ var dotEl = agent.parts.labelDiv.querySelector('.office3d-label-dot');
160
+ if (nameEl) nameEl.textContent = agent.displayName;
161
+ if (dotEl) {
162
+ var colors = { active: '#4ade80', sleeping: '#facc15', dead: '#f87171' };
163
+ dotEl.style.background = colors[agent.state] || '#f87171';
164
+ }
165
+ }
166
+
167
+ function updateDeskScreen(deskIdx, status, isListening) {
168
+ var desk = S.deskMeshes[deskIdx];
169
+ if (!desk) return;
170
+ if (status === 'active' && isListening) {
171
+ // Listening green screen
172
+ desk.screenMat.emissive.setHex(0x22c55e);
173
+ desk.screenMat.emissiveIntensity = 0.5;
174
+ desk.screenMat.color.setHex(0x22c55e);
175
+ } else if (status === 'active' && !isListening) {
176
+ // Active but NOT listening — red screen
177
+ desk.screenMat.emissive.setHex(0xef4444);
178
+ desk.screenMat.emissiveIntensity = 0.6;
179
+ desk.screenMat.color.setHex(0xef4444);
180
+ } else if (status === 'sleeping') {
181
+ desk.screenMat.emissive.setHex(0x1a2744);
182
+ desk.screenMat.emissiveIntensity = 0.15;
183
+ desk.screenMat.color.setHex(0x1a2744);
184
+ } else {
185
+ desk.screenMat.emissive.setHex(0x333333);
186
+ desk.screenMat.emissiveIntensity = 0.1;
187
+ desk.screenMat.color.setHex(0x333333);
188
+ }
189
+ }
190
+
191
+ function shouldRenderAgent(info) {
192
+ var isApiAgent = !!(info && (info.is_api_agent || (info.role && info.role === 'api-agent')));
193
+ return !(isApiAgent && info.status !== 'active');
194
+ }
195
+
196
+ function retireAgent(agentName, agent, status) {
197
+ if (!agent) return;
198
+ agent.prevState = agent.state;
199
+ agent.state = status || 'dead';
200
+ agent.isListening = false;
201
+ agent.target = null;
202
+ agent.walkQueue = [];
203
+ agent.walkProgress = 0;
204
+ agent.walkDuration = 0;
205
+ agent.isSitting = false;
206
+ agent.location = 'desk';
207
+ if (agent.deskIdx >= 0) updateDeskScreen(agent.deskIdx, agent.state, false);
208
+ updateLabel(agent);
209
+ agent.registered = false;
210
+ if (!agent.dying) {
211
+ agent.dying = true;
212
+ agent.deathOpacity = 1;
213
+ }
214
+ _releaseGallerySeat(agentName);
215
+ }
216
+
217
+ function projectBotCapability(capabilities, legacyBotCapability) {
218
+ if (Array.isArray(capabilities)) {
219
+ if (capabilities.indexOf('video_generation') !== -1) return 'video_gen';
220
+ if (capabilities.indexOf('texture_generation') !== -1) return 'texture_gen';
221
+ if (capabilities.indexOf('image_generation') !== -1) return 'image_gen';
222
+ if (capabilities.indexOf('vision') !== -1) return 'vision';
223
+ if (capabilities.indexOf('chat') !== -1) return 'chat';
224
+ }
225
+ return legacyBotCapability || '';
226
+ }
227
+
228
+ function flashDeskScreen(deskIdx) {
229
+ var desk = S.deskMeshes[deskIdx];
230
+ if (!desk) return;
231
+ // Flash white briefly — the next syncAgents call (every 2s) will set the correct persistent color via updateDeskScreen
232
+ desk.screenMat.emissive.setHex(0xffffff);
233
+ desk.screenMat.emissiveIntensity = 1.5;
234
+ setTimeout(function() {
235
+ // Force immediate red until next sync corrects it
236
+ desk.screenMat.emissive.setHex(0xef4444);
237
+ desk.screenMat.emissiveIntensity = 0.6;
238
+ desk.screenMat.color.setHex(0xef4444);
239
+ }, 300);
240
+ }
241
+
242
+ function rebuildCharacterAppearance(agent) {
243
+ var a = resolveAppearance(agent.displayName, agent.appearance);
244
+ agent.parts.bodyMat.color.setHex(a.shirt_hex);
245
+ agent.parts.armMat.color.setHex(a.shirt_hex);
246
+ agent.parts.legMat.color.setHex(a.pants_hex);
247
+ agent.parts.headMat.color.setHex(a.head_hex);
248
+ agent.parts.handMat.color.setHex(a.head_hex);
249
+ agent.parts.shoeMat.color.setHex(a.shoe_hex);
250
+
251
+ // Rebuild hair
252
+ var oldHair = agent.parts.hairGroup;
253
+ agent.parts.group.remove(oldHair);
254
+ oldHair.traverse(function(c) { if (c.geometry) c.geometry.dispose(); if (c.material) c.material.dispose(); });
255
+ var newHair = buildHair(a.hair_style, a.hair_hex);
256
+ newHair.position.y = 1.05;
257
+ agent.parts.group.add(newHair);
258
+ agent.parts.hairGroup = newHair;
259
+
260
+ // Rebuild face
261
+ var oldFace = agent.parts.faceSprite;
262
+ agent.parts.head.remove(oldFace);
263
+ if (oldFace.material.map) oldFace.material.map.dispose();
264
+ oldFace.material.dispose();
265
+ var newFace = buildFaceSprite(a.eye_style, a.mouth_style, agent.state === 'sleeping');
266
+ newFace.position.set(0, 0, 0.251);
267
+ agent.parts.head.add(newFace);
268
+ agent.parts.faceSprite = newFace;
269
+
270
+ // Rebuild outfit
271
+ removeOutfit(agent.parts.group);
272
+ if (a.outfit) {
273
+ agent.parts.outfitGroup = buildOutfit(a.outfit, { shirt_color: a.shirt_color, pants_color: a.pants_color }, agent.parts.group);
274
+ } else {
275
+ agent.parts.outfitGroup = null;
276
+ }
277
+ }
278
+
279
+ export function syncAgents() {
280
+ if (!window.cachedAgents) return;
281
+
282
+ // Reset gallery seats if environment was switched
283
+ if (window._gallerySeatsReset) {
284
+ _gallerySeatsOccupied = { image: null, video: null, texture: null };
285
+ window._gallerySeatsReset = false;
286
+ }
287
+
288
+ fetchTasks();
289
+ updateConversationVelocity();
290
+
291
+ for (var name in window.cachedAgents) {
292
+ var info = window.cachedAgents[name];
293
+ // Skip API agents that aren't actively running (user hasn't clicked Start)
294
+ var isApiAgent = !!(info.is_api_agent || (info.role && info.role === 'api-agent'));
295
+ if (!shouldRenderAgent(info)) {
296
+ if (S.agents3d[name]) retireAgent(name, S.agents3d[name], info.status || 'dead');
297
+ continue;
298
+ }
299
+
300
+ if (!S.agents3d[name]) {
301
+ var providerColor = info.provider_color || '#0ea5e9';
302
+ var botCap = projectBotCapability(info.capabilities, info.bot_capability);
303
+ var deskIdx, deskPos;
304
+
305
+ if (isApiAgent && _getGallerySeat(botCap)) {
306
+ // Creative bots (image/video/texture generators) sit at gallery desk
307
+ deskIdx = -1;
308
+ deskPos = _getGallerySeat(botCap);
309
+ } else if (isApiAgent && !_getGallerySeat(botCap)) {
310
+ // Non-creative bots (vision, chat) get a regular workspace desk
311
+ deskIdx = assignDesk(name);
312
+ var allDesks2 = getDeskPositions();
313
+ deskPos = allDesks2[deskIdx] || allDesks2[0];
314
+ } else {
315
+ deskIdx = assignDesk(name);
316
+ var allDesks = getDeskPositions();
317
+ deskPos = allDesks[deskIdx] || allDesks[0];
318
+ }
319
+ // Gallery bots (image/video/texture) get robot character, chat/vision bots get chibi
320
+ var isGalleryBot = isApiAgent && deskIdx === -1;
321
+ var parts = isGalleryBot
322
+ ? createRobotCharacter(info.display_name || name, providerColor)
323
+ : createCharacter(info.display_name || name, info.appearance || {});
324
+ var agent = {
325
+ name: name,
326
+ displayName: info.display_name || name,
327
+ appearance: info.appearance || {},
328
+ parts: parts,
329
+ deskIdx: deskIdx,
330
+ deskPos: { x: deskPos.x, z: deskPos.z },
331
+ pos: { x: SPAWN_POS.x, z: SPAWN_POS.z },
332
+ target: null,
333
+ walkQueue: [],
334
+ walkProgress: 0,
335
+ walkDuration: 0,
336
+ walkStart: null,
337
+ state: info.status || 'active',
338
+ prevState: null,
339
+ registered: false,
340
+ bubbleTimer: 0,
341
+ bubbleText: '',
342
+ isSitting: false,
343
+ sittingLerp: 0,
344
+ facingTarget: 0,
345
+ zzzActive: false,
346
+ sleepTransition: 0,
347
+ spawnOpacity: 1,
348
+ deathOpacity: 1,
349
+ dying: false,
350
+ currentTask: null,
351
+ taskCelebration: 0,
352
+ isListening: !!(info.is_listening),
353
+ handRaiseTimer: 0,
354
+ waveTimer: 0,
355
+ thinkTimer: 0,
356
+ pointTimer: 0,
357
+ celebrateTimer: 0,
358
+ stretchTimer: 0,
359
+ idleGestureTimer: 5 + Math.random() * 10,
360
+ listenLostTimer: 0,
361
+ lastMessageTime: 0,
362
+ monitorTimer: 0,
363
+ location: 'desk', // 'desk', 'dressing_room', 'rest', 'walking'
364
+ isApiAgent: isApiAgent,
365
+ botCapability: botCap,
366
+ isGallerySeat: !!(isApiAgent && deskIdx === -1),
367
+ _processing: false,
368
+ };
369
+
370
+ // Claim gallery seat if applicable
371
+ if (isApiAgent && deskIdx === -1) _claimGallerySeat(botCap, name);
372
+
373
+ parts.group.position.set(SPAWN_POS.x, 0, SPAWN_POS.z);
374
+ S.scene.add(parts.group);
375
+ updateLabel(agent);
376
+ S.agents3d[name] = agent;
377
+
378
+ // Gallery bots walk directly to seat pos, others walk to desk + 0.7 (chair offset)
379
+ var walkTargetZ = agent.isGallerySeat ? deskPos.z : deskPos.z + 0.7;
380
+ showBubble(agent, 'Checking in...');
381
+ (function(a, wz) {
382
+ setTimeout(function() {
383
+ navigateTo(a, a.deskPos.x, wz, function() {
384
+ a.registered = true;
385
+ showBubble(a, a.isApiAgent ? 'Systems online.' : 'Ready to work!');
386
+ if (a.deskIdx >= 0) updateDeskScreen(a.deskIdx, a.state, a.isListening);
387
+ });
388
+ }, 800);
389
+ })(agent, walkTargetZ);
390
+ } else {
391
+ var existing = S.agents3d[name];
392
+ var newState = info.status || 'active';
393
+ var oldState = existing.state;
394
+
395
+ // Don't override local state changes (rest area sleeping, dressing room)
396
+ var isLocalOverride = existing.location === 'rest' || existing.location === 'dressing_room' || existing.location === 'walking';
397
+ if (newState !== oldState && !isLocalOverride) {
398
+ existing.prevState = oldState;
399
+ existing.state = newState;
400
+ if (newState === 'dead' && !existing.dying) {
401
+ existing.dying = true;
402
+ existing.deathOpacity = 1;
403
+ }
404
+ }
405
+
406
+ // --- Autonomous behaviors: sleeping → rest area, waking → back to desk ---
407
+ if (newState === 'sleeping' && oldState === 'active' && existing.location === 'desk' && existing.registered && !existing.dying) {
408
+ // Agent fell asleep walk to rest area after a short delay
409
+ existing.location = 'walking';
410
+ (function(a) {
411
+ setTimeout(function() {
412
+ showBubble(a, 'Need a break...');
413
+ a.isSitting = false;
414
+ navigateTo(a, REST_AREA_ENTRANCE.x, REST_AREA_ENTRANCE.z, function() {
415
+ navigateTo(a, REST_AREA_POS.x, REST_AREA_POS.z, function() {
416
+ a.location = 'rest';
417
+ a.state = 'sleeping';
418
+ showBubble(a, 'zzz...');
419
+ });
420
+ });
421
+ }, 1000 + Math.random() * 2000);
422
+ })(existing);
423
+ }
424
+ if (newState === 'active' && (oldState === 'sleeping' || existing.location === 'rest') && existing.location !== 'desk' && existing.registered && !existing.dying) {
425
+ // Agent woke up — walk back to desk
426
+ existing.location = 'walking';
427
+ existing.state = 'active';
428
+ (function(a) {
429
+ showBubble(a, 'Back to work!');
430
+ navigateTo(a, a.deskPos.x, _agentDeskZ(a), function() {
431
+ a.location = 'desk';
432
+ });
433
+ })(existing);
434
+ }
435
+
436
+ existing.displayName = info.display_name || name;
437
+ var wasListening = existing.isListening;
438
+ existing.isListening = !!(info.is_listening);
439
+
440
+ // Detect listen mode change — update screen color persistently
441
+ if (wasListening && !existing.isListening) {
442
+ // Left listen mode — flash then stay red until next sync sets updateDeskScreen
443
+ existing.listenLostTimer = 3;
444
+ flashDeskScreen(existing.deskIdx);
445
+ }
446
+ if (!wasListening && existing.isListening) {
447
+ // Entered listen mode — next updateDeskScreen will set green
448
+ existing.listenLostTimer = 0;
449
+ }
450
+
451
+ var task = getAgentTask(name);
452
+ if (task) {
453
+ var prevTask = existing.currentTask;
454
+ existing.currentTask = task;
455
+ if (prevTask && prevTask.status !== 'done' && task.status === 'done') {
456
+ existing.taskCelebration = 2;
457
+ existing.celebrateTimer = 1.5;
458
+ setEmotion(existing, 'happy', 6);
459
+ }
460
+ // Blocked task → frustrated face
461
+ if (task.status === 'blocked' && (!prevTask || prevTask.status !== 'blocked')) {
462
+ setEmotion(existing, 'frustrated', 8);
463
+ }
464
+ } else {
465
+ existing.currentTask = null;
466
+ }
467
+
468
+ // Listening agents look focused
469
+ if (existing.isListening && !wasListening) {
470
+ setEmotion(existing, 'focused', 10);
471
+ }
472
+
473
+ var newApp = info.appearance || {};
474
+ if (JSON.stringify(newApp) !== JSON.stringify(existing.appearance)) {
475
+ existing.appearance = newApp;
476
+ rebuildCharacterAppearance(existing);
477
+ }
478
+
479
+ updateLabel(existing);
480
+ if (existing.registered && existing.deskIdx >= 0) updateDeskScreen(existing.deskIdx, existing.state, existing.isListening);
481
+ }
482
+ }
483
+
484
+ // --- Random social behavior: idle agents occasionally stretch or look around ---
485
+ // Limit concurrent social walks to prevent traffic jams (max 2 walking at once)
486
+ var walkingCount = 0;
487
+ for (var wn in S.agents3d) { if (S.agents3d[wn].location === 'walking') walkingCount++; }
488
+
489
+ for (var sn in S.agents3d) {
490
+ var sa = S.agents3d[sn];
491
+ if (!sa.registered || sa.state !== 'active' || sa.location !== 'desk' || sa.target) continue;
492
+ if (!sa._socialTimer) sa._socialTimer = 30 + Math.random() * 60;
493
+ sa._socialTimer -= 2; // syncAgents runs every ~2s
494
+ if (sa._socialTimer <= 0) {
495
+ sa._socialTimer = 40 + Math.random() * 80; // next social event in 40-120s
496
+ // Pick a random behavior: stretch, look around, or visit another agent
497
+ var roll = Math.random();
498
+ if (roll < 0.4) {
499
+ // Stretch at desk
500
+ sa.stretchTimer = 2;
501
+ } else if (roll < 0.7) {
502
+ // Look around curiously
503
+ sa.thinkTimer = 1.5;
504
+ } else if (walkingCount < 2) {
505
+ // Walk to a random nearby agent's desk to "chat" then return (max 2 concurrent)
506
+ var others = [];
507
+ for (var on in S.agents3d) {
508
+ if (on !== sn && S.agents3d[on].registered && S.agents3d[on].state === 'active' && S.agents3d[on].location === 'desk') {
509
+ others.push(S.agents3d[on]);
510
+ }
511
+ }
512
+ if (others.length > 0) {
513
+ var buddy = others[Math.floor(Math.random() * others.length)];
514
+ (function(a, b) {
515
+ a.location = 'walking';
516
+ a.isSitting = false;
517
+ showBubble(a, 'Hey ' + b.displayName + '!');
518
+ setEmotion(a, 'playful', 6);
519
+ var stopX = b.deskPos.x + 1.5;
520
+ var stopZ = _agentDeskZ(b);
521
+ navigateTo(a, stopX, stopZ, function() {
522
+ // Face buddy
523
+ var dx = b.pos.x - a.pos.x;
524
+ var dz = b.pos.z - a.pos.z;
525
+ a.facingTarget = Math.atan2(dx, dz);
526
+ a.waveTimer = 0.8;
527
+ // Buddy turns toward visitor
528
+ b.facingTarget = Math.atan2(-dx, -dz);
529
+ setTimeout(function() {
530
+ showBubble(a, 'Back to it!');
531
+ navigateTo(a, a.deskPos.x, _agentDeskZ(a), function() {
532
+ a.location = 'desk';
533
+ });
534
+ // Buddy turns back to desk
535
+ setTimeout(function() { b.facingTarget = Math.PI; }, 1500);
536
+ }, 3000 + Math.random() * 2000);
537
+ });
538
+ })(sa, buddy);
539
+ }
540
+ }
541
+ }
542
+ }
543
+
544
+ for (var n in S.agents3d) {
545
+ if (!window.cachedAgents[n]) {
546
+ retireAgent(n, S.agents3d[n], 'dead');
547
+ }
548
+ }
549
+ }
550
+
551
+ export function processMessages() {
552
+ var history = window.cachedHistory;
553
+ if (!history || history.length === 0) return;
554
+
555
+ // Use window-level counter so it persists across 3D stop/start cycles (tab switches)
556
+ // This prevents message replay when user switches from Messages tab back to 3D Hub
557
+ if (typeof window._lastProcessedMsg === 'undefined') window._lastProcessedMsg = 0;
558
+ var newMsgs = history.slice(window._lastProcessedMsg);
559
+ window._lastProcessedMsg = history.length;
560
+
561
+ for (var i = 0; i < newMsgs.length; i++) {
562
+ var msg = newMsgs[i];
563
+ var from = S.agents3d[msg.from];
564
+ if (!from || !from.registered) continue;
565
+ var text = msg.content || msg.message || '';
566
+
567
+ from.lastMessageTime = Date.now();
568
+ flashDeskScreen(from.deskIdx);
569
+
570
+ // Instant preview bubble — show short text immediately before walk animation
571
+ // Gives users instant visual feedback that the agent is about to speak
572
+ var preview = text.length > 30 ? text.substring(0, 27) + '...' : text;
573
+ showBubble(from, preview);
574
+
575
+ // Auto-celebrate on task completion events
576
+ if (text.indexOf('[EVENT] Task') >= 0 && text.indexOf('completed') >= 0) {
577
+ from.celebrateTimer = 1.5;
578
+ from.taskCelebration = 2;
579
+ }
580
+
581
+ // Emotion detection from message content
582
+ var textLower = text.toLowerCase();
583
+ if (textLower.indexOf('done') >= 0 || textLower.indexOf('pass') >= 0 || textLower.indexOf('success') >= 0 || textLower.indexOf('great') >= 0 || textLower.indexOf('shipped') >= 0) {
584
+ setEmotion(from, 'happy', 5);
585
+ } else if (textLower.indexOf('error') >= 0 || textLower.indexOf('fail') >= 0 || textLower.indexOf('bug') >= 0 || textLower.indexOf('broken') >= 0) {
586
+ setEmotion(from, 'frustrated', 5);
587
+ } else if (textLower.indexOf('?') >= 0 && (textLower.indexOf('how') >= 0 || textLower.indexOf('why') >= 0 || textLower.indexOf('what if') >= 0)) {
588
+ setEmotion(from, 'thinking', 4);
589
+ } else if (textLower.indexOf('!') >= 0 && (textLower.indexOf('wow') >= 0 || textLower.indexOf('amazing') >= 0 || textLower.indexOf('awesome') >= 0)) {
590
+ setEmotion(from, 'excited', 4);
591
+ }
592
+
593
+ // Target agent gets surprised when directly addressed
594
+ if (msg.to && msg.to !== 'all' && S.agents3d[msg.to]) {
595
+ var targetAgent = S.agents3d[msg.to];
596
+ if (targetAgent.registered && targetAgent.isSitting) {
597
+ setEmotion(targetAgent, 'surprised', 2);
598
+ }
599
+ }
600
+
601
+ // Contextual gesture based on message type
602
+ var isBC = !msg.to || msg.to === 'all';
603
+ if (isBC) {
604
+ from.waveTimer = 0.8;
605
+ } else {
606
+ from.pointTimer = 0.6;
607
+ }
608
+
609
+ // Glance reaction — nearby sitting agents glance toward the speaker
610
+ for (var gn in S.agents3d) {
611
+ var ga = S.agents3d[gn];
612
+ if (gn === msg.from || gn === msg.to || !ga.registered || ga.state !== 'active' || !ga.isSitting) continue;
613
+ var gdx = from.pos.x - ga.pos.x;
614
+ ga._glanceTarget = from.name;
615
+ ga._glanceDirection = gdx > 0 ? 1 : -1; // left or right glance
616
+ ga._glanceTimer = 0;
617
+ }
618
+
619
+ if (msg.to && msg.to !== 'all' && S.agents3d[msg.to]) {
620
+ var target = S.agents3d[msg.to];
621
+ (function(f, t, txt) {
622
+ setTimeout(function() {
623
+ f.walkQueue = [];
624
+ // Calculate a stop point ~1.8m away from the target, facing them
625
+ var tx = t.pos.x, tz = t.pos.z;
626
+ var fx = f.pos.x, fz = f.pos.z;
627
+ var adx = tx - fx, adz = tz - fz;
628
+ var dist = Math.sqrt(adx * adx + adz * adz);
629
+ var stopDist = 1.8;
630
+ var stopX, stopZ;
631
+ if (dist > stopDist + 0.5) {
632
+ // Approach from sender's direction, stop 1.8m away
633
+ stopX = tx - (adx / dist) * stopDist;
634
+ stopZ = tz - (adz / dist) * stopDist;
635
+ } else {
636
+ // Already close — just step to the side of target's desk
637
+ stopX = tx + 1.5;
638
+ stopZ = tz;
639
+ }
640
+ navigateTo(f, stopX, stopZ, function() {
641
+ // Sender faces target
642
+ var dx2 = t.pos.x - f.pos.x;
643
+ var dz2 = t.pos.z - f.pos.z;
644
+ f.facingTarget = Math.atan2(dx2, dz2);
645
+ showBubble(f, txt);
646
+
647
+ // Target turns toward sender (listener reaction)
648
+ var rdx = f.pos.x - t.pos.x;
649
+ var rdz = f.pos.z - t.pos.z;
650
+ t.facingTarget = Math.atan2(rdx, rdz);
651
+ t.isListening = true;
652
+ t._listeningTo = f.name;
653
+
654
+ setTimeout(function() {
655
+ // Sender walks back to desk
656
+ navigateTo(f, f.deskPos.x, _agentDeskZ(f));
657
+ // Target turns back to desk after a short delay
658
+ setTimeout(function() {
659
+ if (t._listeningTo === f.name) {
660
+ t.isListening = false;
661
+ t._listeningTo = null;
662
+ t.facingTarget = Math.PI; // face desk
663
+ }
664
+ }, 1500);
665
+ }, 4200);
666
+ });
667
+ }, 400);
668
+ })(from, target, text);
669
+ } else {
670
+ (function(f, txt) {
671
+ setTimeout(function() {
672
+ f.walkQueue = [];
673
+ navigateTo(f, 0, 0, function() {
674
+ showBubble(f, txt);
675
+ // All nearby agents turn toward the broadcaster
676
+ for (var an in S.agents3d) {
677
+ var a = S.agents3d[an];
678
+ if (a.name === f.name || !a.registered || a.state !== 'active') continue;
679
+ var bdx = f.pos.x - a.pos.x;
680
+ var bdz = f.pos.z - a.pos.z;
681
+ a.facingTarget = Math.atan2(bdx, bdz);
682
+ a.isListening = true;
683
+ a._listeningTo = f.name;
684
+ }
685
+ setTimeout(function() {
686
+ navigateTo(f, f.deskPos.x, _agentDeskZ(f));
687
+ // All listeners turn back
688
+ setTimeout(function() {
689
+ for (var an2 in S.agents3d) {
690
+ var a2 = S.agents3d[an2];
691
+ if (a2._listeningTo === f.name) {
692
+ a2.isListening = false;
693
+ a2._listeningTo = null;
694
+ a2.facingTarget = Math.PI;
695
+ }
696
+ }
697
+ }, 1500);
698
+ }, 4200);
699
+ });
700
+ }, 400);
701
+ })(from, text);
702
+ }
703
+ }
704
+ }