agent-office 0.5.0 → 0.6.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 (189) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +259 -228
  3. package/dist/commands/cron-requests.d.ts +7 -0
  4. package/dist/commands/cron-requests.d.ts.map +1 -0
  5. package/dist/commands/cron-requests.js +31 -0
  6. package/dist/commands/cron-requests.js.map +1 -0
  7. package/dist/commands/crons.d.ts +10 -0
  8. package/dist/commands/crons.d.ts.map +1 -0
  9. package/dist/commands/crons.js +45 -0
  10. package/dist/commands/crons.js.map +1 -0
  11. package/dist/commands/hello.d.ts +5 -0
  12. package/dist/commands/hello.d.ts.map +1 -0
  13. package/dist/commands/hello.js +4 -0
  14. package/dist/commands/hello.js.map +1 -0
  15. package/dist/commands/messages.d.ts +5 -0
  16. package/dist/commands/messages.d.ts.map +1 -0
  17. package/dist/commands/messages.js +18 -0
  18. package/dist/commands/messages.js.map +1 -0
  19. package/dist/commands/sessions.d.ts +13 -0
  20. package/dist/commands/sessions.d.ts.map +1 -0
  21. package/dist/commands/sessions.js +58 -0
  22. package/dist/commands/sessions.js.map +1 -0
  23. package/dist/commands/task-columns.d.ts +2 -0
  24. package/dist/commands/task-columns.d.ts.map +1 -0
  25. package/dist/commands/task-columns.js +13 -0
  26. package/dist/commands/task-columns.js.map +1 -0
  27. package/dist/commands/tasks.d.ts +11 -0
  28. package/dist/commands/tasks.d.ts.map +1 -0
  29. package/dist/commands/tasks.js +75 -0
  30. package/dist/commands/tasks.js.map +1 -0
  31. package/dist/config.test.d.ts +2 -0
  32. package/dist/config.test.d.ts.map +1 -0
  33. package/dist/config.test.js +50 -0
  34. package/dist/config.test.js.map +1 -0
  35. package/dist/db/index.d.ts +6 -70
  36. package/dist/db/index.d.ts.map +1 -0
  37. package/dist/db/index.js +4 -11
  38. package/dist/db/index.js.map +1 -0
  39. package/dist/db/mock-storage.d.ts +79 -0
  40. package/dist/db/mock-storage.d.ts.map +1 -0
  41. package/dist/db/mock-storage.js +381 -0
  42. package/dist/db/mock-storage.js.map +1 -0
  43. package/dist/db/mock-storage.test.d.ts +2 -0
  44. package/dist/db/mock-storage.test.d.ts.map +1 -0
  45. package/dist/db/mock-storage.test.js +234 -0
  46. package/dist/db/mock-storage.test.js.map +1 -0
  47. package/dist/db/postgresql-storage.d.ts +10 -8
  48. package/dist/db/postgresql-storage.d.ts.map +1 -0
  49. package/dist/db/postgresql-storage.js +76 -42
  50. package/dist/db/postgresql-storage.js.map +1 -0
  51. package/dist/db/sqlite-storage.d.ts +9 -8
  52. package/dist/db/sqlite-storage.d.ts.map +1 -0
  53. package/dist/db/sqlite-storage.js +75 -41
  54. package/dist/db/sqlite-storage.js.map +1 -0
  55. package/dist/db/storage-base.d.ts +7 -8
  56. package/dist/db/storage-base.d.ts.map +1 -0
  57. package/dist/db/storage-base.js +3 -2
  58. package/dist/db/storage-base.js.map +1 -0
  59. package/dist/db/storage.d.ts +12 -12
  60. package/dist/db/storage.d.ts.map +1 -0
  61. package/dist/db/storage.js +1 -0
  62. package/dist/db/storage.js.map +1 -0
  63. package/dist/db/types.d.ts +67 -0
  64. package/dist/db/types.d.ts.map +1 -0
  65. package/dist/db/types.js +2 -0
  66. package/dist/db/types.js.map +1 -0
  67. package/dist/index.d.ts +2 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +397 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/index.test.d.ts +2 -0
  72. package/dist/index.test.d.ts.map +1 -0
  73. package/dist/index.test.js +49 -0
  74. package/dist/index.test.js.map +1 -0
  75. package/dist/lib/output.d.ts +2 -0
  76. package/dist/lib/output.d.ts.map +1 -0
  77. package/dist/lib/output.js +8 -0
  78. package/dist/lib/output.js.map +1 -0
  79. package/dist/services/cron-service.constraints.test.d.ts +2 -0
  80. package/dist/services/cron-service.constraints.test.d.ts.map +1 -0
  81. package/dist/services/cron-service.constraints.test.js +90 -0
  82. package/dist/services/cron-service.constraints.test.js.map +1 -0
  83. package/dist/services/cron-service.d.ts +45 -0
  84. package/dist/services/cron-service.d.ts.map +1 -0
  85. package/dist/services/cron-service.js +157 -0
  86. package/dist/services/cron-service.js.map +1 -0
  87. package/dist/services/cron-service.test.d.ts +2 -0
  88. package/dist/services/cron-service.test.d.ts.map +1 -0
  89. package/dist/services/cron-service.test.js +280 -0
  90. package/dist/services/cron-service.test.js.map +1 -0
  91. package/dist/services/index.d.ts +5 -0
  92. package/dist/services/index.d.ts.map +1 -0
  93. package/dist/services/index.js +5 -0
  94. package/dist/services/index.js.map +1 -0
  95. package/dist/services/message-service.d.ts +16 -0
  96. package/dist/services/message-service.d.ts.map +1 -0
  97. package/dist/services/message-service.js +39 -0
  98. package/dist/services/message-service.js.map +1 -0
  99. package/dist/services/message-service.test.d.ts +2 -0
  100. package/dist/services/message-service.test.d.ts.map +1 -0
  101. package/dist/services/message-service.test.js +145 -0
  102. package/dist/services/message-service.test.js.map +1 -0
  103. package/dist/services/session-service.constraints.test.d.ts +2 -0
  104. package/dist/services/session-service.constraints.test.d.ts.map +1 -0
  105. package/dist/services/session-service.constraints.test.js +34 -0
  106. package/dist/services/session-service.constraints.test.js.map +1 -0
  107. package/dist/services/session-service.d.ts +27 -0
  108. package/dist/services/session-service.d.ts.map +1 -0
  109. package/dist/services/session-service.js +55 -0
  110. package/dist/services/session-service.js.map +1 -0
  111. package/dist/services/session-service.test.d.ts +2 -0
  112. package/dist/services/session-service.test.d.ts.map +1 -0
  113. package/dist/services/session-service.test.js +87 -0
  114. package/dist/services/session-service.test.js.map +1 -0
  115. package/dist/services/task-service.d.ts +25 -0
  116. package/dist/services/task-service.d.ts.map +1 -0
  117. package/dist/services/task-service.js +87 -0
  118. package/dist/services/task-service.js.map +1 -0
  119. package/dist/services/task-service.test.d.ts +2 -0
  120. package/dist/services/task-service.test.d.ts.map +1 -0
  121. package/dist/services/task-service.test.js +180 -0
  122. package/dist/services/task-service.test.js.map +1 -0
  123. package/package.json +41 -42
  124. package/dist/cli.d.ts +0 -2
  125. package/dist/cli.js +0 -317
  126. package/dist/commands/communicator.d.ts +0 -9
  127. package/dist/commands/communicator.js +0 -2232
  128. package/dist/commands/manage.d.ts +0 -5
  129. package/dist/commands/manage.js +0 -20
  130. package/dist/commands/notifier.d.ts +0 -11
  131. package/dist/commands/notifier.js +0 -100
  132. package/dist/commands/screensaver.d.ts +0 -8
  133. package/dist/commands/screensaver.js +0 -1280
  134. package/dist/commands/serve.d.ts +0 -13
  135. package/dist/commands/serve.js +0 -95
  136. package/dist/commands/task-board.d.ts +0 -29
  137. package/dist/commands/task-board.js +0 -251
  138. package/dist/commands/worker.d.ts +0 -16
  139. package/dist/commands/worker.js +0 -145
  140. package/dist/db/migrate.d.ts +0 -2
  141. package/dist/db/migrate.js +0 -3
  142. package/dist/lib/agentic-coding-server.d.ts +0 -66
  143. package/dist/lib/agentic-coding-server.js +0 -7
  144. package/dist/lib/notifier.d.ts +0 -18
  145. package/dist/lib/notifier.js +0 -15
  146. package/dist/lib/opencode-coding-server.d.ts +0 -11
  147. package/dist/lib/opencode-coding-server.js +0 -66
  148. package/dist/lib/pi-coding-server.d.ts +0 -20
  149. package/dist/lib/pi-coding-server.js +0 -162
  150. package/dist/manage/app.d.ts +0 -6
  151. package/dist/manage/app.js +0 -128
  152. package/dist/manage/components/AgentCode.d.ts +0 -8
  153. package/dist/manage/components/AgentCode.js +0 -73
  154. package/dist/manage/components/CreateSession.d.ts +0 -8
  155. package/dist/manage/components/CreateSession.js +0 -37
  156. package/dist/manage/components/CronList.d.ts +0 -9
  157. package/dist/manage/components/CronList.js +0 -321
  158. package/dist/manage/components/CronRequests.d.ts +0 -8
  159. package/dist/manage/components/CronRequests.js +0 -181
  160. package/dist/manage/components/DeleteSession.d.ts +0 -7
  161. package/dist/manage/components/DeleteSession.js +0 -55
  162. package/dist/manage/components/InjectText.d.ts +0 -8
  163. package/dist/manage/components/InjectText.js +0 -51
  164. package/dist/manage/components/ItemSelector.d.ts +0 -7
  165. package/dist/manage/components/ItemSelector.js +0 -20
  166. package/dist/manage/components/MenuSelect.d.ts +0 -13
  167. package/dist/manage/components/MenuSelect.js +0 -22
  168. package/dist/manage/components/MyMail.d.ts +0 -9
  169. package/dist/manage/components/MyMail.js +0 -143
  170. package/dist/manage/components/Profile.d.ts +0 -8
  171. package/dist/manage/components/Profile.js +0 -60
  172. package/dist/manage/components/ReadMail.d.ts +0 -8
  173. package/dist/manage/components/ReadMail.js +0 -110
  174. package/dist/manage/components/SendMessage.d.ts +0 -9
  175. package/dist/manage/components/SendMessage.js +0 -79
  176. package/dist/manage/components/SessionList.d.ts +0 -9
  177. package/dist/manage/components/SessionList.js +0 -608
  178. package/dist/manage/components/SessionSidebar.d.ts +0 -6
  179. package/dist/manage/components/SessionSidebar.js +0 -24
  180. package/dist/manage/components/TailMessages.d.ts +0 -8
  181. package/dist/manage/components/TailMessages.js +0 -126
  182. package/dist/manage/hooks/useApi.d.ts +0 -147
  183. package/dist/manage/hooks/useApi.js +0 -181
  184. package/dist/server/cron.d.ts +0 -25
  185. package/dist/server/cron.js +0 -107
  186. package/dist/server/index.d.ts +0 -4
  187. package/dist/server/index.js +0 -22
  188. package/dist/server/routes.d.ts +0 -13
  189. package/dist/server/routes.js +0 -1396
@@ -1,1280 +0,0 @@
1
- import express from "express";
2
- export async function appScreensaver(options) {
3
- const { url: agentUrl, password, host, port: portStr } = options;
4
- const port = parseInt(portStr, 10);
5
- if (isNaN(port) || port < 1 || port > 65535) {
6
- console.error(`Error: invalid port "${portStr}"`);
7
- process.exit(1);
8
- }
9
- try {
10
- new URL(agentUrl);
11
- }
12
- catch {
13
- console.error(`Error: invalid --url "${agentUrl}"`);
14
- process.exit(1);
15
- }
16
- console.log(`Screensaver: visualizing mail activity from ${agentUrl}/watch`);
17
- const app = express();
18
- app.use(express.json());
19
- // ── GET /coworkers — proxies coworker list from agent-office ───────────────
20
- app.get("/coworkers", async (_req, res) => {
21
- res.setHeader("Content-Type", "application/json");
22
- try {
23
- const response = await fetch(`${agentUrl}/coworkers`, {
24
- headers: {
25
- "Authorization": `Bearer ${password}`,
26
- },
27
- });
28
- if (!response.ok) {
29
- res.json([]);
30
- return;
31
- }
32
- const data = await response.json();
33
- res.json(data);
34
- }
35
- catch {
36
- res.json([]);
37
- }
38
- });
39
- // ── GET / — Three.js ring visualization ─────────────────────────────────────
40
- app.get("/", (_req, res) => {
41
- res.setHeader("Content-Type", "text/html; charset=utf-8");
42
- res.send(renderScreensaverPage());
43
- });
44
- // ── GET /watch-stream — proxies SSE from agent-office /watch ───────────────
45
- app.get("/watch-stream", async (req, res) => {
46
- // Set up SSE headers
47
- res.setHeader("Content-Type", "text/event-stream");
48
- res.setHeader("Cache-Control", "no-cache");
49
- res.setHeader("Connection", "keep-alive");
50
- const abortController = new AbortController();
51
- // Handle client disconnect
52
- req.on("close", () => {
53
- abortController.abort();
54
- res.end();
55
- });
56
- req.on("error", () => {
57
- abortController.abort();
58
- });
59
- try {
60
- // Connect to the agent-office /watch endpoint
61
- const response = await fetch(`${agentUrl}/watch`, {
62
- headers: {
63
- "Authorization": `Bearer ${password}`,
64
- },
65
- signal: abortController.signal,
66
- });
67
- if (!response.ok) {
68
- res.write(`event: error\n`);
69
- res.write(`data: ${JSON.stringify({ error: `HTTP ${response.status}` })}\n\n`);
70
- res.end();
71
- return;
72
- }
73
- // Stream data from agent-office to client
74
- const reader = response.body?.getReader();
75
- if (!reader) {
76
- res.write(`event: error\n`);
77
- res.write(`data: ${JSON.stringify({ error: "No response body" })}\n\n`);
78
- res.end();
79
- return;
80
- }
81
- // Read and forward chunks
82
- while (!abortController.signal.aborted) {
83
- const { done, value } = await reader.read();
84
- if (done)
85
- break;
86
- // Forward the raw SSE data
87
- const chunk = new TextDecoder().decode(value);
88
- res.write(chunk);
89
- }
90
- }
91
- catch (err) {
92
- if (!abortController.signal.aborted) {
93
- res.write(`event: error\n`);
94
- res.write(`data: ${JSON.stringify({ error: String(err) })}\n\n`);
95
- }
96
- }
97
- finally {
98
- res.end();
99
- }
100
- });
101
- const server = app.listen(port, host, () => {
102
- console.log(`Screensaver running at http://${host}:${port}`);
103
- console.log(`Press Ctrl+C to stop.`);
104
- });
105
- server.on('error', (err) => {
106
- if (err.code === 'EADDRINUSE') {
107
- console.error(`Error: Port ${port} is already in use. Is another instance running?`);
108
- }
109
- else {
110
- console.error(`Error: ${err.message}`);
111
- }
112
- process.exit(1);
113
- });
114
- // Keep process alive
115
- await new Promise(() => { });
116
- }
117
- // ── HTML page with Three.js ring visualization ───────────────────────────────
118
- function renderScreensaverPage() {
119
- return `<!DOCTYPE html>
120
- <html lang="en">
121
- <head>
122
- <meta charset="UTF-8">
123
- <meta name="viewport" content="width=device-width, initial-scale=1">
124
- <title>Agent Office Screensaver</title>
125
- <link rel="preconnect" href="https://fonts.googleapis.com">
126
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
127
- <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@200;300&display=swap" rel="stylesheet">
128
- <style>
129
- * { box-sizing: border-box; margin: 0; padding: 0; }
130
- body {
131
- background: #000;
132
- height: 100vh;
133
- overflow: hidden;
134
- }
135
- canvas { display: block; }
136
- </style>
137
- </head>
138
- <body>
139
-
140
- <script type="importmap">
141
- {
142
- "imports": {
143
- "three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js",
144
- "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"
145
- }
146
- }
147
- </script>
148
-
149
- <script type="module">
150
- import * as THREE from 'three'
151
- import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
152
- import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'
153
- import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'
154
- import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'
155
- import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js'
156
- import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'
157
- import { Reflector } from 'three/addons/objects/Reflector.js'
158
-
159
- // ── Constants ──────────────────────────────────────────────────────────
160
- const RING_RADIUS = 8
161
- const NODE_SIZE = 0.35
162
- const ARROW_DECAY_SECONDS = 5 // arrows fully fade after this many seconds
163
- const ARROW_COLOR = 0xff3333
164
- const ARROW_CURVE_HEIGHT = 2.5
165
- const ARROW_HEAD_SIZE = 0.25
166
- const LABEL_SCALE = 0.012
167
-
168
- // ── State ──────────────────────────────────────────────────────────────
169
- let agentNames = [] // sorted list of agent names
170
- let agentNodes = new Map() // name -> { mesh, label, angle }
171
- let arrowObjects = [] // { from, to, mesh, arrowHead, createdAt, opacity }
172
- let watchState = {} // latest state from SSE
173
- let coworkersSet = new Set() // valid coworker names from /coworkers endpoint
174
-
175
- // Track all messages we've seen: key = "from->to", value = lastSent ISO string
176
- let messageEdges = new Map() // "from->to" -> { lastSent: Date }
177
-
178
- // Wait for IBM Plex Sans to load before rendering text sprites
179
- let fontReady = false
180
- document.fonts.load('200 48px "IBM Plex Sans"').then(() => {
181
- fontReady = true
182
- // Re-layout agents so labels render with the loaded font
183
- if (agentNames.length > 0) layoutAgents(agentNames)
184
- })
185
-
186
- // ── Three.js setup ─────────────────────────────────────────────────────
187
- const scene = new THREE.Scene()
188
-
189
- const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 2000)
190
- camera.position.set(0, 14, 14)
191
- camera.lookAt(0, 0, 0)
192
-
193
- const renderer = new THREE.WebGLRenderer({ antialias: true })
194
- renderer.setSize(window.innerWidth, window.innerHeight)
195
- renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
196
- renderer.toneMapping = THREE.ACESFilmicToneMapping
197
- renderer.toneMappingExposure = 1.0
198
- document.body.appendChild(renderer.domElement)
199
-
200
- const controls = new OrbitControls(camera, renderer.domElement)
201
- controls.enableDamping = true
202
- controls.dampingFactor = 0.05
203
- controls.minDistance = 5
204
- controls.maxDistance = 40
205
- controls.maxPolarAngle = Math.PI / 2.1
206
-
207
- // ── Bloom post-processing ──────────────────────────────────────────────
208
- const composer = new EffectComposer(renderer)
209
-
210
- // First pass: render the starfield background into the composer buffer
211
- // (we create bgScene/bgCamera above the composer, referenced here)
212
- let bgRenderPass // assigned after bgScene is created below
213
-
214
- const renderPass = new RenderPass(scene, camera)
215
- renderPass.clear = false // don't clear - stars are already in the buffer
216
- renderPass.clearDepth = true // but do clear depth so scene renders on top
217
- composer.addPass(renderPass)
218
-
219
- const bloomPass = new UnrealBloomPass(
220
- new THREE.Vector2(window.innerWidth, window.innerHeight),
221
- 1.0, // strength
222
- 0.5, // radius
223
- 0.2 // threshold - low so the red arrows and emissive objects bloom
224
- )
225
- composer.addPass(bloomPass)
226
- composer.addPass(new OutputPass())
227
-
228
- // ── Starfield background (fullscreen shader) ───────────────────────────
229
- // Procedural stars with varying brightness, color temperature, and twinkling
230
- const starVertexShader = \`
231
- varying vec2 vUv;
232
- void main() {
233
- vUv = uv;
234
- gl_Position = vec4(position, 1.0);
235
- }
236
- \`
237
- const starFragmentShader = \`
238
- precision highp float;
239
- varying vec2 vUv;
240
- uniform float uTime;
241
- uniform vec2 uResolution;
242
-
243
- // Hash functions for pseudo-random values
244
- float hash(vec2 p) {
245
- p = fract(p * vec2(443.897, 441.423));
246
- p += dot(p, p.yx + 19.19);
247
- return fract((p.x + p.y) * p.x);
248
- }
249
-
250
- float hash3(vec3 p) {
251
- p = fract(p * vec3(443.897, 441.423, 437.195));
252
- p += dot(p, p.yzx + 19.19);
253
- return fract((p.x + p.y + p.z) * p.x);
254
- }
255
-
256
- // Smooth noise
257
- float noise(vec2 p) {
258
- vec2 i = floor(p);
259
- vec2 f = fract(p);
260
- f = f * f * (3.0 - 2.0 * f);
261
- float a = hash(i);
262
- float b = hash(i + vec2(1.0, 0.0));
263
- float c = hash(i + vec2(0.0, 1.0));
264
- float d = hash(i + vec2(1.0, 1.0));
265
- return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
266
- }
267
-
268
- // Star layer - each cell may contain a star
269
- float starLayer(vec2 uv, float scale, float seed) {
270
- vec2 gv = fract(uv * scale) - 0.5;
271
- vec2 id = floor(uv * scale);
272
-
273
- float d = length(gv);
274
- float r = hash(id + seed);
275
-
276
- // Only rare cells have stars (realistic sparse density)
277
- if (r > 0.08) return 0.0;
278
-
279
- // Star brightness varies
280
- float brightness = pow(hash(id + seed + 100.0), 3.0) * 2.0;
281
-
282
- // Twinkling: each star twinkles at its own frequency and phase
283
- float twinkleFreq = 0.5 + hash(id + seed + 200.0) * 3.0;
284
- float twinklePhase = hash(id + seed + 300.0) * 6.28;
285
- float twinkle = 0.7 + 0.3 * sin(uTime * twinkleFreq + twinklePhase);
286
- brightness *= twinkle;
287
-
288
- // Star size varies
289
- float starSize = 0.005 + hash(id + seed + 400.0) * 0.015;
290
-
291
- // Soft circular falloff
292
- float star = smoothstep(starSize, starSize * 0.1, d) * brightness;
293
-
294
- // Add subtle diffraction spikes on brighter stars
295
- if (brightness > 0.8) {
296
- float spike = max(
297
- exp(-abs(gv.x) * 80.0) * exp(-abs(gv.y) * 8.0),
298
- exp(-abs(gv.y) * 80.0) * exp(-abs(gv.x) * 8.0)
299
- ) * brightness * 0.15;
300
- star += spike;
301
- }
302
-
303
- return star;
304
- }
305
-
306
- void main() {
307
- vec2 uv = (gl_FragCoord.xy - 0.5 * uResolution) / min(uResolution.x, uResolution.y);
308
-
309
- // Very dark blue-black base
310
- vec3 bg = vec3(0.005, 0.007, 0.015);
311
-
312
- // Extremely subtle nebula-like color variation
313
- float n1 = noise(uv * 2.0 + uTime * 0.01);
314
- float n2 = noise(uv * 3.0 - uTime * 0.015 + 50.0);
315
- bg += vec3(0.008, 0.004, 0.015) * n1;
316
- bg += vec3(0.003, 0.008, 0.012) * n2;
317
-
318
- // Accumulate multiple star layers at different scales (depth)
319
- float stars = 0.0;
320
- vec3 starColor = vec3(0.0);
321
-
322
- for (float i = 0.0; i < 2.0; i++) {
323
- float scale = 40.0 + i * 50.0;
324
- float layerStars = starLayer(uv, scale, i * 73.156);
325
-
326
- // Color temperature varies per star (white, blue-white, warm yellow)
327
- vec2 id = floor(uv * scale);
328
- float temp = hash(id + i * 73.156 + 500.0);
329
- vec3 col;
330
- if (temp < 0.25) {
331
- col = vec3(0.8, 0.85, 1.0); // blue-white (hot)
332
- } else if (temp < 0.45) {
333
- col = vec3(0.7, 0.8, 1.0); // blue
334
- } else if (temp < 0.75) {
335
- col = vec3(1.0, 0.97, 0.92); // white-warm
336
- } else if (temp < 0.9) {
337
- col = vec3(1.0, 0.9, 0.7); // warm yellow
338
- } else {
339
- col = vec3(1.0, 0.75, 0.6); // orange-ish (cool star)
340
- }
341
-
342
- starColor += layerStars * col;
343
- stars += layerStars;
344
- }
345
-
346
- vec3 finalColor = bg + starColor;
347
-
348
- // Very faint milky way band across the diagonal
349
- float band = exp(-pow((uv.x + uv.y) * 1.5, 2.0) * 3.0);
350
- float bandNoise = noise(uv * 8.0 + 100.0) * noise(uv * 16.0 + 200.0);
351
- finalColor += vec3(0.012, 0.015, 0.025) * band * bandNoise * 3.0;
352
-
353
- gl_FragColor = vec4(finalColor, 1.0);
354
- }
355
- \`
356
-
357
- const starUniforms = {
358
- uTime: { value: 0.0 },
359
- uResolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) },
360
- }
361
-
362
- // Fullscreen quad rendered behind everything
363
- const starMaterial = new THREE.ShaderMaterial({
364
- vertexShader: starVertexShader,
365
- fragmentShader: starFragmentShader,
366
- uniforms: starUniforms,
367
- depthWrite: false,
368
- depthTest: false,
369
- })
370
- const starQuad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), starMaterial)
371
- starQuad.frustumCulled = false
372
-
373
- // Render stars as a separate background scene so they're always behind
374
- const bgScene = new THREE.Scene()
375
- const bgCamera = new THREE.Camera()
376
- bgScene.add(starQuad)
377
-
378
- // Insert the bg render pass at the start of the composer chain
379
- bgRenderPass = new RenderPass(bgScene, bgCamera)
380
- bgRenderPass.clear = true
381
- composer.insertPass(bgRenderPass, 0)
382
-
383
- // ── Lighting ───────────────────────────────────────────────────────────
384
- const ambientLight = new THREE.AmbientLight(0x334466, 0.6)
385
- scene.add(ambientLight)
386
-
387
- const pointLight = new THREE.PointLight(0x6c8eff, 1.5, 50)
388
- pointLight.position.set(0, 8, 0)
389
- scene.add(pointLight)
390
-
391
- // Subtle ground glow
392
- const groundGlow = new THREE.PointLight(0x1a3a5a, 0.5, 30)
393
- groundGlow.position.set(0, -1, 0)
394
- scene.add(groundGlow)
395
-
396
- // ── Procedural environment cubemap for metal reflections ─────────────
397
- // Without an envMap, MeshStandardMaterial with metalness looks flat black.
398
- // Generate a simple dark gradient cubemap so the metal has something to reflect.
399
- function generateEnvMap() {
400
- const size = 128
401
- const faces = []
402
- for (let f = 0; f < 6; f++) {
403
- const canvas = document.createElement('canvas')
404
- canvas.width = size
405
- canvas.height = size
406
- const ctx = canvas.getContext('2d')
407
- // Dark gradient: bluish at top, near-black at bottom
408
- const grad = ctx.createLinearGradient(0, 0, 0, size)
409
- if (f === 2) { // +Y (top)
410
- grad.addColorStop(0, '#0a1828')
411
- grad.addColorStop(1, '#162540')
412
- } else if (f === 3) { // -Y (bottom)
413
- grad.addColorStop(0, '#020408')
414
- grad.addColorStop(1, '#050a10')
415
- } else {
416
- grad.addColorStop(0, '#0c1a2a')
417
- grad.addColorStop(0.5, '#081018')
418
- grad.addColorStop(1, '#040810')
419
- }
420
- ctx.fillStyle = grad
421
- ctx.fillRect(0, 0, size, size)
422
-
423
- // Scatter a few dim specks to give the reflections some texture
424
- for (let i = 0; i < 40; i++) {
425
- const x = Math.random() * size
426
- const y = Math.random() * size
427
- const brightness = Math.floor(20 + Math.random() * 30)
428
- ctx.fillStyle = \`rgb(\${brightness}, \${brightness + 5}, \${brightness + 15})\`
429
- ctx.beginPath()
430
- ctx.arc(x, y, 0.5 + Math.random() * 1.0, 0, Math.PI * 2)
431
- ctx.fill()
432
- }
433
-
434
- faces.push(canvas)
435
- }
436
-
437
- const cubeTexture = new THREE.CubeTexture(faces)
438
- cubeTexture.needsUpdate = true
439
- return cubeTexture
440
- }
441
-
442
- const envMap = generateEnvMap()
443
- scene.environment = envMap
444
-
445
- // ── Ground ring (the platform) — dark brushed metal ────────────────────
446
- const ringGeo = new THREE.RingGeometry(RING_RADIUS - 1.8, RING_RADIUS + 1.8, 128, 4)
447
- const ringMat = new THREE.MeshStandardMaterial({
448
- color: 0x1a1e28,
449
- roughness: 0.55,
450
- metalness: 0.92,
451
- envMap: envMap,
452
- envMapIntensity: 1.5,
453
- side: THREE.DoubleSide,
454
- })
455
- const ringMesh = new THREE.Mesh(ringGeo, ringMat)
456
- ringMesh.rotation.x = -Math.PI / 2
457
- ringMesh.position.y = -0.05
458
- scene.add(ringMesh)
459
-
460
- // ── Glass fence on outer edge ─────────────────────────────────────────
461
- const fenceRadius = RING_RADIUS + 1.75
462
- const fenceHeight = 1.8
463
-
464
- // Single thin cylinder for the glass wall
465
- const glassGeo = new THREE.CylinderGeometry(fenceRadius, fenceRadius, fenceHeight, 128, 1, true)
466
- const glassMat = new THREE.MeshPhysicalMaterial({
467
- color: 0x88aabb,
468
- transparent: true,
469
- opacity: 0.1,
470
- roughness: 0.05,
471
- metalness: 0.2,
472
- transmission: 0.9,
473
- thickness: 0.03,
474
- ior: 1.5,
475
- envMap: envMap,
476
- envMapIntensity: 3.0,
477
- side: THREE.DoubleSide,
478
- depthWrite: false,
479
- })
480
- const glassWall = new THREE.Mesh(glassGeo, glassMat)
481
- glassWall.position.y = fenceHeight / 2
482
- scene.add(glassWall)
483
-
484
- // Metal rail around the top
485
- const railMat = new THREE.MeshStandardMaterial({
486
- color: 0x3a3e48,
487
- roughness: 0.3,
488
- metalness: 0.95,
489
- envMap: envMap,
490
- envMapIntensity: 2.0,
491
- })
492
- const rail = new THREE.Mesh(
493
- new THREE.TorusGeometry(fenceRadius, 0.02, 6, 128),
494
- railMat
495
- )
496
- rail.rotation.x = -Math.PI / 2
497
- rail.position.y = fenceHeight
498
- scene.add(rail)
499
-
500
- // Thin metal poles evenly spaced
501
- const poleMat = new THREE.MeshStandardMaterial({
502
- color: 0x3a3e48,
503
- roughness: 0.35,
504
- metalness: 0.95,
505
- envMap: envMap,
506
- envMapIntensity: 2.0,
507
- })
508
- const poleCount = 12
509
- for (let i = 0; i < poleCount; i++) {
510
- const angle = (i / poleCount) * Math.PI * 2
511
- const pole = new THREE.Mesh(
512
- new THREE.CylinderGeometry(0.02, 0.02, fenceHeight, 6),
513
- poleMat
514
- )
515
- pole.position.set(
516
- Math.cos(angle) * fenceRadius,
517
- fenceHeight / 2,
518
- Math.sin(angle) * fenceRadius
519
- )
520
- scene.add(pole)
521
- }
522
-
523
- // ── Reflective water pool in the center ───────────────────────────────
524
- const poolGeo = new THREE.CircleGeometry(RING_RADIUS - 1.85, 64)
525
- const pool = new Reflector(poolGeo, {
526
- color: 0x445566,
527
- textureWidth: 1024,
528
- textureHeight: 1024,
529
- clipBias: 0.003,
530
- })
531
- pool.rotation.x = -Math.PI / 2
532
- pool.position.y = -0.04
533
- scene.add(pool)
534
-
535
- // Dark water surface shader overlay
536
- const waterUniforms = {
537
- uTime: { value: 0.0 },
538
- uReflectTex: { value: pool.getRenderTarget().texture },
539
- }
540
- const waterMat = new THREE.ShaderMaterial({
541
- uniforms: waterUniforms,
542
- transparent: true,
543
- depthWrite: false,
544
- vertexShader: \`
545
- varying vec2 vUv;
546
- varying vec3 vWorldPos;
547
- varying vec3 vNormal;
548
- void main() {
549
- vUv = uv;
550
- vNormal = normalize(normalMatrix * normal);
551
- vec4 wp = modelMatrix * vec4(position, 1.0);
552
- vWorldPos = wp.xyz;
553
- gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
554
- }
555
- \`,
556
- fragmentShader: \`
557
- precision highp float;
558
- varying vec2 vUv;
559
- varying vec3 vWorldPos;
560
- varying vec3 vNormal;
561
- uniform float uTime;
562
- uniform sampler2D uReflectTex;
563
-
564
- // --- Noise functions ---
565
- vec2 hash2(vec2 p) {
566
- p = vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3)));
567
- return -1.0 + 2.0 * fract(sin(p) * 43758.5453123);
568
- }
569
-
570
- float noise(vec2 p) {
571
- vec2 i = floor(p);
572
- vec2 f = fract(p);
573
- vec2 u = f * f * (3.0 - 2.0 * f);
574
- return mix(mix(dot(hash2(i + vec2(0,0)), f - vec2(0,0)),
575
- dot(hash2(i + vec2(1,0)), f - vec2(1,0)), u.x),
576
- mix(dot(hash2(i + vec2(0,1)), f - vec2(0,1)),
577
- dot(hash2(i + vec2(1,1)), f - vec2(1,1)), u.x), u.y);
578
- }
579
-
580
- float fbm(vec2 p) {
581
- float v = 0.0;
582
- float a = 0.5;
583
- vec2 shift = vec2(100.0);
584
- mat2 rot = mat2(cos(0.5), sin(0.5), -sin(0.5), cos(0.5));
585
- for (int i = 0; i < 5; i++) {
586
- v += a * noise(p);
587
- p = rot * p * 2.0 + shift;
588
- a *= 0.5;
589
- }
590
- return v;
591
- }
592
-
593
- void main() {
594
- vec2 p = (vUv - 0.5) * 2.0;
595
- float r = length(p);
596
-
597
- // Edge fade so water doesn't touch the ring hard
598
- float edgeFade = smoothstep(1.0, 0.85, r);
599
- if (edgeFade < 0.001) discard;
600
-
601
- // Animated FBM water surface
602
- float t = uTime * 0.15;
603
- vec2 q = vec2(fbm(p * 3.0 + vec2(t, 0.0)),
604
- fbm(p * 3.0 + vec2(0.0, t)));
605
- vec2 rr = vec2(fbm(p * 3.0 + 4.0 * q + vec2(1.7, 9.2) + vec2(t * 0.3)),
606
- fbm(p * 3.0 + 4.0 * q + vec2(8.3, 2.8) + vec2(0.0, t * 0.2)));
607
- float f = fbm(p * 3.0 + 4.0 * rr);
608
-
609
- // Perturbed normal for subtle light interaction
610
- float dx = fbm((p + vec2(0.01, 0.0)) * 3.0 + 4.0 * rr) - f;
611
- float dy = fbm((p + vec2(0.0, 0.01)) * 3.0 + 4.0 * rr) - f;
612
- vec3 perturbedNormal = normalize(vec3(-dx * 8.0, 1.0, -dy * 8.0));
613
-
614
- // Fresnel - more reflective at glancing angles
615
- vec3 viewDir = normalize(cameraPosition - vWorldPos);
616
- float fresnel = pow(1.0 - max(dot(viewDir, perturbedNormal), 0.0), 3.0);
617
- fresnel = 0.15 + 0.85 * fresnel;
618
-
619
- // Deep water color with subtle variation
620
- vec3 deepColor = vec3(0.01, 0.025, 0.04);
621
- vec3 midColor = vec3(0.02, 0.05, 0.08);
622
- vec3 waterCol = mix(deepColor, midColor, f * 0.5 + 0.5);
623
-
624
- // Subtle caustic-like bright spots from the FBM pattern
625
- float caustic = pow(max(f * 0.5 + 0.5, 0.0), 6.0) * 0.3;
626
- waterCol += vec3(0.05, 0.12, 0.15) * caustic;
627
-
628
- // Reflection lookup (screen-space UV from the reflector)
629
- vec2 reflUv = gl_FragCoord.xy / vec2(\${Math.round(1024)}.0);
630
- reflUv += perturbedNormal.xz * 0.02; // distort reflection
631
- vec3 reflColor = texture2D(uReflectTex, clamp(reflUv, 0.0, 1.0)).rgb;
632
-
633
- // Blend water color with reflection via fresnel
634
- vec3 color = mix(waterCol, waterCol + reflColor * 0.3, fresnel);
635
-
636
- // Very subtle surface highlights (specular)
637
- vec3 lightDir = normalize(vec3(0.0, 1.0, 0.0));
638
- vec3 halfDir = normalize(lightDir + viewDir);
639
- float spec = pow(max(dot(perturbedNormal, halfDir), 0.0), 120.0);
640
- color += vec3(0.15, 0.25, 0.3) * spec * 0.5;
641
-
642
- gl_FragColor = vec4(color, edgeFade * 0.85);
643
- }
644
- \`,
645
- })
646
- const waterOverlay = new THREE.Mesh(
647
- new THREE.CircleGeometry(RING_RADIUS - 1.84, 64),
648
- waterMat
649
- )
650
- waterOverlay.rotation.x = -Math.PI / 2
651
- waterOverlay.position.y = -0.025
652
- scene.add(waterOverlay)
653
-
654
- // ── Center wireframe icosahedron ───────────────────────────────────────
655
- const icoGeo = new THREE.IcosahedronGeometry(1.8, 1)
656
- const icoWire = new THREE.WireframeGeometry(icoGeo)
657
- const icoMat = new THREE.LineBasicMaterial({ color: 0x5ae0c0, transparent: true, opacity: 0.7 })
658
- const icoLines = new THREE.LineSegments(icoWire, icoMat)
659
- icoLines.position.y = 2.5
660
- scene.add(icoLines)
661
-
662
- // Inner glow sphere
663
- const glowSphereGeo = new THREE.SphereGeometry(1.5, 32, 32)
664
- const glowSphereMat = new THREE.MeshBasicMaterial({
665
- color: 0x2a8a7a,
666
- transparent: true,
667
- opacity: 0.12,
668
- })
669
- const glowSphere = new THREE.Mesh(glowSphereGeo, glowSphereMat)
670
- glowSphere.position.y = 2.5
671
- scene.add(glowSphere)
672
-
673
- // Core point light inside the icosahedron to cast glow on nearby objects
674
- const icoLight = new THREE.PointLight(0x4ae0b0, 1.2, 12)
675
- icoLight.position.y = 2.5
676
- scene.add(icoLight)
677
-
678
- // ── Text label helper (Canvas-based sprites) ───────────────────────────
679
- const FONT_FAMILY = '"IBM Plex Sans", sans-serif'
680
-
681
- function createTextSprite(text, fontSize = 48, color = '#b0c4de') {
682
- const canvas = document.createElement('canvas')
683
- const ctx = canvas.getContext('2d')
684
- ctx.font = \`200 \${fontSize}px \${FONT_FAMILY}\`
685
- const metrics = ctx.measureText(text)
686
- const width = metrics.width + 20
687
- const height = fontSize * 1.4
688
-
689
- canvas.width = width
690
- canvas.height = height
691
-
692
- ctx.font = \`200 \${fontSize}px \${FONT_FAMILY}\`
693
- ctx.textAlign = 'center'
694
- ctx.textBaseline = 'middle'
695
- ctx.fillStyle = color
696
- ctx.fillText(text, width / 2, height / 2)
697
-
698
- const texture = new THREE.CanvasTexture(canvas)
699
- texture.minFilter = THREE.LinearFilter
700
- const spriteMat = new THREE.SpriteMaterial({ map: texture, transparent: true, depthTest: false })
701
- const sprite = new THREE.Sprite(spriteMat)
702
- sprite.scale.set(width * LABEL_SCALE, height * LABEL_SCALE, 1)
703
- return sprite
704
- }
705
-
706
- // ── Agent node helper ──────────────────────────────────────────────────
707
- // Shared materials for all agent figures
708
- const figureMat = new THREE.MeshStandardMaterial({
709
- color: 0x8090a0,
710
- roughness: 0.6,
711
- metalness: 0.3,
712
- envMap: envMap,
713
- })
714
- const deskMat = new THREE.MeshStandardMaterial({
715
- color: 0x3a2a1a,
716
- roughness: 0.7,
717
- metalness: 0.1,
718
- envMap: envMap,
719
- })
720
- const chairMat = new THREE.MeshStandardMaterial({
721
- color: 0x1a1a22,
722
- roughness: 0.8,
723
- metalness: 0.2,
724
- envMap: envMap,
725
- })
726
- // Monitor screen material -- emissive so it glows and responds to pulse
727
- const screenMat = new THREE.MeshStandardMaterial({
728
- color: 0x4488aa,
729
- emissive: 0x2a4a7a,
730
- emissiveIntensity: 0.5,
731
- roughness: 0.1,
732
- metalness: 0.5,
733
- })
734
- const monitorFrameMat = new THREE.MeshStandardMaterial({
735
- color: 0x151518,
736
- roughness: 0.4,
737
- metalness: 0.8,
738
- envMap: envMap,
739
- })
740
-
741
- function createAgentNode(name, angle) {
742
- const group = new THREE.Group()
743
-
744
- // Everything is built in local space facing +Z (inward),
745
- // then the group is rotated to face the center.
746
- const figureGroup = new THREE.Group()
747
-
748
- // ── Desk ──
749
- // Tabletop
750
- const tabletop = new THREE.Mesh(
751
- new THREE.BoxGeometry(0.9, 0.04, 0.45),
752
- deskMat
753
- )
754
- tabletop.position.set(0, 0.38, 0.25)
755
- figureGroup.add(tabletop)
756
-
757
- // Desk legs (4)
758
- const legGeo = new THREE.CylinderGeometry(0.02, 0.02, 0.38, 6)
759
- const legPositions = [[-0.4, 0.19, 0.05], [0.4, 0.19, 0.05], [-0.4, 0.19, 0.45], [0.4, 0.19, 0.45]]
760
- for (const [lx, ly, lz] of legPositions) {
761
- const leg = new THREE.Mesh(legGeo, monitorFrameMat)
762
- leg.position.set(lx, ly, lz)
763
- figureGroup.add(leg)
764
- }
765
-
766
- // ── Monitor ──
767
- // Monitor frame (thin box)
768
- const monitorFrame = new THREE.Mesh(
769
- new THREE.BoxGeometry(0.5, 0.35, 0.025),
770
- monitorFrameMat
771
- )
772
- monitorFrame.position.set(0, 0.60, 0.35)
773
- figureGroup.add(monitorFrame)
774
-
775
- // Monitor screen (slightly inset, emissive) — unique per agent for pulse
776
- const agentScreenMat = screenMat.clone()
777
- const screen = new THREE.Mesh(
778
- new THREE.BoxGeometry(0.44, 0.29, 0.005),
779
- agentScreenMat
780
- )
781
- screen.position.set(0, 0.60, 0.336)
782
- figureGroup.add(screen)
783
-
784
- // Monitor stand (thin cylinder)
785
- const stand = new THREE.Mesh(
786
- new THREE.CylinderGeometry(0.02, 0.03, 0.18, 6),
787
- monitorFrameMat
788
- )
789
- stand.position.set(0, 0.42, 0.35)
790
- figureGroup.add(stand)
791
-
792
- // ── Chair ──
793
- // Seat
794
- const seat = new THREE.Mesh(
795
- new THREE.BoxGeometry(0.4, 0.04, 0.38),
796
- chairMat
797
- )
798
- seat.position.set(0, 0.30, -0.2)
799
- figureGroup.add(seat)
800
-
801
- // Chair back
802
- const chairBack = new THREE.Mesh(
803
- new THREE.BoxGeometry(0.4, 0.4, 0.04),
804
- chairMat
805
- )
806
- chairBack.position.set(0, 0.50, -0.37)
807
- figureGroup.add(chairBack)
808
-
809
- // Chair pedestal
810
- const pedestal = new THREE.Mesh(
811
- new THREE.CylinderGeometry(0.03, 0.03, 0.28, 6),
812
- monitorFrameMat
813
- )
814
- pedestal.position.set(0, 0.16, -0.2)
815
- figureGroup.add(pedestal)
816
-
817
- // Chair base (5 legs radiating out)
818
- for (let i = 0; i < 5; i++) {
819
- const a = (i / 5) * Math.PI * 2
820
- const cLeg = new THREE.Mesh(
821
- new THREE.CylinderGeometry(0.012, 0.012, 0.18, 4),
822
- monitorFrameMat
823
- )
824
- cLeg.position.set(
825
- Math.sin(a) * 0.09,
826
- 0.02,
827
- -0.2 + Math.cos(a) * 0.09
828
- )
829
- cLeg.rotation.z = Math.sin(a) * 0.8
830
- cLeg.rotation.x = -Math.cos(a) * 0.8
831
- figureGroup.add(cLeg)
832
- }
833
-
834
- // ── Person (seated figure) ──
835
- // Torso
836
- const torso = new THREE.Mesh(
837
- new THREE.CylinderGeometry(0.12, 0.10, 0.28, 8),
838
- figureMat
839
- )
840
- torso.position.set(0, 0.50, -0.2)
841
- figureGroup.add(torso)
842
-
843
- // Head
844
- const head = new THREE.Mesh(
845
- new THREE.SphereGeometry(0.09, 12, 10),
846
- figureMat
847
- )
848
- head.position.set(0, 0.73, -0.2)
849
- figureGroup.add(head)
850
-
851
- // Upper arms (angled forward toward desk)
852
- const armGeo = new THREE.CylinderGeometry(0.035, 0.03, 0.22, 6)
853
- for (const side of [-1, 1]) {
854
- const upperArm = new THREE.Mesh(armGeo, figureMat)
855
- upperArm.position.set(side * 0.16, 0.48, -0.05)
856
- upperArm.rotation.x = -0.9
857
- upperArm.rotation.z = side * 0.15
858
- figureGroup.add(upperArm)
859
-
860
- // Forearm resting on desk
861
- const forearm = new THREE.Mesh(
862
- new THREE.CylinderGeometry(0.03, 0.025, 0.2, 6),
863
- figureMat
864
- )
865
- forearm.position.set(side * 0.2, 0.40, 0.15)
866
- forearm.rotation.x = -1.5
867
- figureGroup.add(forearm)
868
- }
869
-
870
- // Upper legs (seated, going forward)
871
- const legGeoFig = new THREE.CylinderGeometry(0.05, 0.04, 0.3, 6)
872
- for (const side of [-1, 1]) {
873
- const thigh = new THREE.Mesh(legGeoFig, figureMat)
874
- thigh.position.set(side * 0.08, 0.30, -0.05)
875
- thigh.rotation.x = -1.45
876
- figureGroup.add(thigh)
877
-
878
- // Lower legs (hanging down)
879
- const shin = new THREE.Mesh(
880
- new THREE.CylinderGeometry(0.04, 0.035, 0.28, 6),
881
- figureMat
882
- )
883
- shin.position.set(side * 0.08, 0.14, 0.08)
884
- figureGroup.add(shin)
885
- }
886
-
887
- // ── Subtle screen glow light ──
888
- const nodeLight = new THREE.PointLight(0x6c8eff, 0.4, 3)
889
- nodeLight.position.set(0, 0.6, 0.3)
890
- figureGroup.add(nodeLight)
891
-
892
- // Rotate the whole figure to face inward (toward center)
893
- figureGroup.rotation.y = angle + Math.PI
894
- group.add(figureGroup)
895
-
896
- // Name label above
897
- const label = createTextSprite(name, 36, '#8ab4f8')
898
- label.position.y = 1.05
899
- group.add(label)
900
-
901
- // Position on ring
902
- const x = Math.cos(angle) * RING_RADIUS
903
- const z = Math.sin(angle) * RING_RADIUS
904
- group.position.set(x, 0, z)
905
-
906
- return { group, sphereMat: agentScreenMat, label, angle }
907
- }
908
-
909
- // ── Standing scientists along the inner ring edge ─────────────────────
910
- {
911
- const innerEdgeRadius = RING_RADIUS - 1.4
912
- const scientistCount = 2
913
- // Spread them at irregular angles so they look casually placed
914
- const baseAngles = [0.8, 3.6]
915
-
916
- for (let i = 0; i < scientistCount; i++) {
917
- const angle = baseAngles[i]
918
- const group = new THREE.Group()
919
-
920
- // Slight random rotation so they're not all identical
921
- const facingJitter = (Math.random() - 0.5) * 0.3
922
-
923
- // ── Standing body ──
924
- // Legs
925
- const legGeo = new THREE.CylinderGeometry(0.035, 0.03, 0.45, 6)
926
- for (const side of [-1, 1]) {
927
- const leg = new THREE.Mesh(legGeo, figureMat)
928
- leg.position.set(side * 0.06, 0.225, 0)
929
- group.add(leg)
930
- }
931
-
932
- // Torso
933
- const torso = new THREE.Mesh(
934
- new THREE.CylinderGeometry(0.11, 0.09, 0.35, 8),
935
- figureMat
936
- )
937
- torso.position.y = 0.625
938
- group.add(torso)
939
-
940
- // Head - tilted slightly downward (looking at water)
941
- const headGroup = new THREE.Group()
942
- headGroup.position.y = 0.88
943
- const head = new THREE.Mesh(
944
- new THREE.SphereGeometry(0.08, 10, 8),
945
- figureMat
946
- )
947
- headGroup.add(head)
948
- headGroup.rotation.x = 0.25 // tilted down, looking at water
949
- group.add(headGroup)
950
-
951
- // Arms - one arm down, one arm up near chin (thoughtful pose)
952
- // Left arm: hanging at side
953
- const armDown = new THREE.Mesh(
954
- new THREE.CylinderGeometry(0.03, 0.025, 0.32, 6),
955
- figureMat
956
- )
957
- armDown.position.set(-0.14, 0.50, 0)
958
- armDown.rotation.z = 0.08
959
- group.add(armDown)
960
-
961
- // Right arm: bent up toward chin
962
- const upperArmR = new THREE.Mesh(
963
- new THREE.CylinderGeometry(0.03, 0.025, 0.2, 6),
964
- figureMat
965
- )
966
- upperArmR.position.set(0.14, 0.58, 0.02)
967
- upperArmR.rotation.z = -0.3
968
- upperArmR.rotation.x = -0.5
969
- group.add(upperArmR)
970
-
971
- const forearmR = new THREE.Mesh(
972
- new THREE.CylinderGeometry(0.025, 0.022, 0.18, 6),
973
- figureMat
974
- )
975
- forearmR.position.set(0.10, 0.74, -0.06)
976
- forearmR.rotation.x = -1.2
977
- group.add(forearmR)
978
-
979
- // Position on the inner edge, facing inward (toward center/water)
980
- const x = Math.cos(angle) * innerEdgeRadius
981
- const z = Math.sin(angle) * innerEdgeRadius
982
- group.position.set(x, 0, z)
983
- group.rotation.y = angle + Math.PI + facingJitter // face center
984
-
985
- scene.add(group)
986
- }
987
- }
988
-
989
- // ── Curved arrow helper ────────────────────────────────────────────────
990
- function createCurvedArrow(fromPos, toPos, opacity) {
991
- const mid = new THREE.Vector3().addVectors(fromPos, toPos).multiplyScalar(0.5)
992
- mid.y += ARROW_CURVE_HEIGHT
993
-
994
- // Push the midpoint outward from center slightly for visual separation
995
- const centerToMid = new THREE.Vector3(mid.x, 0, mid.z)
996
- const dist = centerToMid.length()
997
- if (dist > 0.01) {
998
- centerToMid.normalize().multiplyScalar(dist * 0.3)
999
- mid.x += centerToMid.x
1000
- mid.z += centerToMid.z
1001
- }
1002
-
1003
- const curve = new THREE.QuadraticBezierCurve3(fromPos.clone(), mid, toPos.clone())
1004
- const points = curve.getPoints(40)
1005
- const geometry = new THREE.BufferGeometry().setFromPoints(points)
1006
-
1007
- const group = new THREE.Group()
1008
-
1009
- // Core line
1010
- const material = new THREE.LineBasicMaterial({
1011
- color: ARROW_COLOR,
1012
- transparent: true,
1013
- opacity: opacity,
1014
- })
1015
- const line = new THREE.Line(geometry, material)
1016
- group.add(line)
1017
-
1018
- // Directional cones along the curve (at 25%, 50%, 75%)
1019
- const coneMaterials = []
1020
- const conePositions = [0.25, 0.5, 0.75]
1021
- const up = new THREE.Vector3(0, 1, 0)
1022
-
1023
- for (const t of conePositions) {
1024
- const pos = curve.getPointAt(t)
1025
- const tangent = curve.getTangentAt(t).normalize()
1026
-
1027
- const coneGeo = new THREE.ConeGeometry(ARROW_HEAD_SIZE * 0.35, ARROW_HEAD_SIZE * 2.5, 8)
1028
- const coneMat = new THREE.MeshBasicMaterial({
1029
- color: ARROW_COLOR,
1030
- transparent: true,
1031
- opacity: opacity,
1032
- })
1033
- const cone = new THREE.Mesh(coneGeo, coneMat)
1034
- cone.position.copy(pos)
1035
-
1036
- const quat = new THREE.Quaternion().setFromUnitVectors(up, tangent)
1037
- cone.setRotationFromQuaternion(quat)
1038
-
1039
- group.add(cone)
1040
- coneMaterials.push(coneMat)
1041
- }
1042
-
1043
- return { group, lineMaterial: material, coneMaterials }
1044
- }
1045
-
1046
- // ── Layout agents in a ring ────────────────────────────────────────────
1047
- function layoutAgents(names) {
1048
- // Remove old nodes
1049
- for (const [, node] of agentNodes) {
1050
- scene.remove(node.group)
1051
- }
1052
- agentNodes.clear()
1053
-
1054
- const count = names.length
1055
- if (count === 0) return
1056
-
1057
- for (let i = 0; i < count; i++) {
1058
- const angle = (i / count) * Math.PI * 2 - Math.PI / 2
1059
- const name = names[i]
1060
- const node = createAgentNode(name, angle)
1061
- scene.add(node.group)
1062
- agentNodes.set(name, node)
1063
- }
1064
- }
1065
-
1066
- // ── Update arrows based on message state ───────────────────────────────
1067
- function updateArrows() {
1068
- // Remove old arrows
1069
- for (const arrow of arrowObjects) {
1070
- scene.remove(arrow.group)
1071
- arrow.group.traverse(child => {
1072
- if (child.geometry) child.geometry.dispose()
1073
- if (child.material) child.material.dispose()
1074
- })
1075
- }
1076
- arrowObjects = []
1077
-
1078
- const now = Date.now()
1079
-
1080
- for (const [key, edge] of messageEdges) {
1081
- const [fromName, toName] = key.split('->')
1082
- const fromNode = agentNodes.get(fromName)
1083
- const toNode = agentNodes.get(toName)
1084
- if (!fromNode || !toNode) continue
1085
-
1086
- const ageSec = (now - edge.lastSent.getTime()) / 1000
1087
- if (ageSec > ARROW_DECAY_SECONDS) continue
1088
-
1089
- // Opacity: 1.0 at time 0, fading to 0.0 at ARROW_DECAY_SECONDS
1090
- const opacity = Math.max(0, 1.0 - (ageSec / ARROW_DECAY_SECONDS))
1091
- if (opacity <= 0.01) continue
1092
-
1093
- const fromPos = fromNode.group.position.clone()
1094
- fromPos.y += 0.3
1095
- const toPos = toNode.group.position.clone()
1096
- toPos.y += 0.3
1097
-
1098
- const arrow = createCurvedArrow(fromPos, toPos, opacity)
1099
- scene.add(arrow.group)
1100
- arrowObjects.push({
1101
- group: arrow.group,
1102
- lineMaterial: arrow.lineMaterial,
1103
- coneMaterials: arrow.coneMaterials,
1104
- fromName,
1105
- toName,
1106
- lastSent: edge.lastSent,
1107
- })
1108
- }
1109
- }
1110
-
1111
- // ── Process incoming watch state ───────────────────────────────────────
1112
- function processWatchState(state) {
1113
- watchState = state
1114
- const nameSet = new Set()
1115
-
1116
- // Collect all agent names and message edges (only for valid coworkers)
1117
- for (const agentName in state) {
1118
- // Only include if this is a known coworker
1119
- if (!coworkersSet.has(agentName)) continue
1120
- nameSet.add(agentName)
1121
- const senders = state[agentName] || {}
1122
- for (const senderName in senders) {
1123
- // Only include sender if they're also a known coworker
1124
- if (!coworkersSet.has(senderName)) continue
1125
- nameSet.add(senderName)
1126
- const edgeKey = senderName + '->' + agentName
1127
- const lastSent = new Date(senders[senderName].lastSent)
1128
- messageEdges.set(edgeKey, { lastSent })
1129
- }
1130
- }
1131
-
1132
- const newNames = Array.from(nameSet).sort()
1133
-
1134
- // Re-layout only if agents changed
1135
- if (JSON.stringify(newNames) !== JSON.stringify(agentNames)) {
1136
- agentNames = newNames
1137
- layoutAgents(agentNames)
1138
- }
1139
-
1140
- // Pulse nodes that just received messages
1141
- const now = Date.now()
1142
- for (const [, node] of agentNodes) {
1143
- // Reset emissive
1144
- node.sphereMat.emissive.setHex(0x2a4a7a)
1145
- node.sphereMat.emissiveIntensity = 0.5
1146
- }
1147
- for (const [key, edge] of messageEdges) {
1148
- const ageSec = (now - edge.lastSent.getTime()) / 1000
1149
- if (ageSec < 5) {
1150
- // Recently active - pulse the receiver
1151
- const toName = key.split('->')[1]
1152
- const node = agentNodes.get(toName)
1153
- if (node) {
1154
- node.sphereMat.emissive.setHex(0x6c8eff)
1155
- node.sphereMat.emissiveIntensity = 1.5
1156
- }
1157
- }
1158
- }
1159
-
1160
- updateArrows()
1161
- }
1162
-
1163
- // ── Fetch coworkers list on startup ──────────────────────────────────────
1164
- async function fetchCoworkers() {
1165
- try {
1166
- const response = await fetch('/coworkers')
1167
- const coworkers = await response.json()
1168
- coworkersSet = new Set(coworkers.map(c => c.name))
1169
- console.log('Loaded coworkers:', Array.from(coworkersSet))
1170
- // Re-process watch state if we already have data
1171
- if (Object.keys(watchState).length > 0) {
1172
- processWatchState(watchState)
1173
- }
1174
- } catch (err) {
1175
- console.error('Failed to fetch coworkers:', err)
1176
- }
1177
- }
1178
-
1179
- // Fetch coworkers immediately
1180
- fetchCoworkers()
1181
-
1182
- // ── SSE connection ─────────────────────────────────────────────────────
1183
- const es = new EventSource('/watch-stream')
1184
-
1185
- es.addEventListener('open', () => {
1186
- console.log('SSE connected')
1187
- })
1188
-
1189
- es.addEventListener('state', (event) => {
1190
- try {
1191
- const state = JSON.parse(event.data)
1192
- processWatchState(state)
1193
- } catch (err) {
1194
- console.error('Failed to parse state:', err)
1195
- }
1196
- })
1197
-
1198
- es.addEventListener('error', () => {
1199
- console.warn('SSE disconnected, retrying...')
1200
- })
1201
-
1202
- // ── Animation loop ─────────────────────────────────────────────────────
1203
- const clock = new THREE.Clock()
1204
- let arrowUpdateTimer = 0
1205
-
1206
- function animate() {
1207
- requestAnimationFrame(animate)
1208
- const delta = clock.getDelta()
1209
- const elapsed = clock.getElapsedTime()
1210
-
1211
- // Update shader uniforms
1212
- starUniforms.uTime.value = elapsed
1213
- waterUniforms.uTime.value = elapsed
1214
-
1215
- // Slowly rotate icosahedron
1216
- icoLines.rotation.y = elapsed * 0.15
1217
- icoLines.rotation.x = Math.sin(elapsed * 0.1) * 0.2
1218
- glowSphere.rotation.y = elapsed * 0.15
1219
-
1220
- // Pulse the center glow subtly
1221
- const icoPulse = 0.10 + Math.sin(elapsed * 0.8) * 0.04
1222
- glowSphereMat.opacity = icoPulse
1223
- icoLight.intensity = 1.0 + Math.sin(elapsed * 0.8) * 0.4
1224
-
1225
- // Update arrow opacities periodically (every 0.5s to avoid thrash)
1226
- arrowUpdateTimer += delta
1227
- if (arrowUpdateTimer > 0.5) {
1228
- arrowUpdateTimer = 0
1229
- updateArrowOpacities()
1230
- }
1231
-
1232
- // Pulse recently-active nodes
1233
- const now = Date.now()
1234
- for (const [key, edge] of messageEdges) {
1235
- const ageSec = (now - edge.lastSent.getTime()) / 1000
1236
- if (ageSec < 5) {
1237
- const toName = key.split('->')[1]
1238
- const node = agentNodes.get(toName)
1239
- if (node) {
1240
- const pulse = 0.8 + Math.sin(elapsed * 6) * 0.7
1241
- node.sphereMat.emissiveIntensity = pulse
1242
- }
1243
- }
1244
- }
1245
-
1246
- controls.update()
1247
-
1248
- // Render: bg pass -> scene pass -> bloom -> output (all via composer)
1249
- composer.render()
1250
- }
1251
-
1252
- function updateArrowOpacities() {
1253
- const now = Date.now()
1254
- for (const arrow of arrowObjects) {
1255
- const edge = messageEdges.get(arrow.fromName + '->' + arrow.toName)
1256
- if (!edge) continue
1257
-
1258
- const ageSec = (now - edge.lastSent.getTime()) / 1000
1259
- const opacity = Math.max(0, 1.0 - (ageSec / ARROW_DECAY_SECONDS))
1260
- arrow.lineMaterial.opacity = opacity
1261
- for (const mat of arrow.coneMaterials) mat.opacity = opacity
1262
- }
1263
- }
1264
-
1265
- // ── Window resize ──────────────────────────────────────────────────────
1266
- window.addEventListener('resize', () => {
1267
- camera.aspect = window.innerWidth / window.innerHeight
1268
- camera.updateProjectionMatrix()
1269
- renderer.setSize(window.innerWidth, window.innerHeight)
1270
- composer.setSize(window.innerWidth, window.innerHeight)
1271
- bloomPass.resolution.set(window.innerWidth, window.innerHeight)
1272
- starUniforms.uResolution.value.set(window.innerWidth, window.innerHeight)
1273
- })
1274
-
1275
- // ── Start ──────────────────────────────────────────────────────────────
1276
- animate()
1277
- </script>
1278
- </body>
1279
- </html>`;
1280
- }