pixelcrew 0.2.2 → 0.2.6

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 (173) hide show
  1. package/README.md +389 -315
  2. package/bin/pixel-agents.js +306 -149
  3. package/package.json +3 -1
  4. package/skill/SKILL.md +185 -0
  5. package/skill/references/anti-ai-rubric.md +30 -0
  6. package/skill/references/anti-ai-slop-encyclopedia.md +181 -0
  7. package/skill/references/commands-reference.md +135 -0
  8. package/skill/references/testing-and-qa-strategy.md +25 -0
  9. package/skill/references/token-efficiency.md +20 -0
  10. package/src/adapters/adapter.interface.js +77 -0
  11. package/src/adapters/antigravity.js +138 -0
  12. package/src/adapters/claude-code.js +149 -0
  13. package/src/adapters/codex.js +131 -0
  14. package/src/adapters/cursor.js +133 -0
  15. package/src/adapters/generic.js +63 -0
  16. package/src/adapters/index.js +15 -0
  17. package/src/adapters/kiro.js +134 -0
  18. package/src/adapters/registry.js +150 -0
  19. package/src/backend/analyzer/complexity-analyzer.js +104 -0
  20. package/src/backend/analyzer/requirement-analyzer.js +213 -0
  21. package/src/backend/analyzer/threat-analyzer.js +122 -0
  22. package/src/backend/api/api-designer.js +90 -0
  23. package/src/backend/api/error-contract.js +132 -0
  24. package/src/backend/api/route-generator.js +92 -0
  25. package/src/backend/api/schema-generator.js +64 -0
  26. package/src/backend/architecture/adr-generator.js +62 -0
  27. package/src/backend/architecture/architecture-reasoner.js +119 -0
  28. package/src/backend/architecture/architecture-selector.js +57 -0
  29. package/src/backend/architecture/tradeoff-analyzer.js +92 -0
  30. package/src/backend/data/data-modeler.js +114 -0
  31. package/src/backend/data/index-analyzer.js +93 -0
  32. package/src/backend/data/query-analyzer.js +41 -0
  33. package/src/backend/data/schema-generator.js +92 -0
  34. package/src/backend/data/transaction-analyzer.js +49 -0
  35. package/src/backend/index.js +67 -0
  36. package/src/backend/observability/logging-engine.js +80 -0
  37. package/src/backend/observability/metrics-engine.js +31 -0
  38. package/src/backend/observability/tracing-engine.js +16 -0
  39. package/src/backend/patterns/pattern-registry.js +74 -0
  40. package/src/backend/patterns/pattern-selector.js +39 -0
  41. package/src/backend/patterns/tradeoff-engine.js +17 -0
  42. package/src/backend/performance/cache-analyzer.js +31 -0
  43. package/src/backend/performance/concurrency-analyzer.js +34 -0
  44. package/src/backend/performance/performance-engine.js +98 -0
  45. package/src/backend/performance/query-optimizer.js +16 -0
  46. package/src/backend/reliability/failure-analyzer.js +22 -0
  47. package/src/backend/reliability/idempotency-engine.js +56 -0
  48. package/src/backend/reliability/resilience-engine.js +141 -0
  49. package/src/backend/reliability/retry-analyzer.js +15 -0
  50. package/src/backend/security/auth-selector.js +33 -0
  51. package/src/backend/security/authorization-engine.js +90 -0
  52. package/src/backend/security/policy-engine.js +49 -0
  53. package/src/backend/security/secret-analyzer.js +39 -0
  54. package/src/backend/security/security-engine.js +122 -0
  55. package/src/backend/security/threat-model.js +59 -0
  56. package/src/backend/synthesis/backend-synthesizer.js +98 -0
  57. package/src/backend/synthesis/infrastructure-synthesizer.js +108 -0
  58. package/src/backend/synthesis/module-synthesizer.js +262 -0
  59. package/src/backend/validation/architecture-validator.js +70 -0
  60. package/src/backend/validation/backend-quality-validator.js +71 -0
  61. package/src/backend/validation/dependency-validator.js +20 -0
  62. package/src/backend/validation/performance-validator.js +38 -0
  63. package/src/backend/validation/security-validator.js +73 -0
  64. package/src/commands/adapt.js +105 -0
  65. package/src/commands/add.js +78 -0
  66. package/src/commands/animate.js +48 -0
  67. package/src/commands/assemble.js +57 -0
  68. package/src/commands/audit.js +93 -0
  69. package/src/commands/backend.js +125 -0
  70. package/src/commands/bento.js +25 -0
  71. package/src/commands/blueprint.js +65 -0
  72. package/src/commands/bolder.js +48 -0
  73. package/src/commands/boss-fight.js +60 -0
  74. package/src/commands/build.js +39 -0
  75. package/src/commands/calibrate.js +25 -0
  76. package/src/commands/chromatic.js +25 -0
  77. package/src/commands/clarify.js +52 -0
  78. package/src/commands/colorize.js +57 -0
  79. package/src/commands/command.interface.js +28 -0
  80. package/src/commands/commands.js +84 -0
  81. package/src/commands/craft.js +57 -0
  82. package/src/commands/crew.js +66 -0
  83. package/src/commands/critique.js +25 -0
  84. package/src/commands/de-slop.js +25 -0
  85. package/src/commands/delight.js +46 -0
  86. package/src/commands/deploy.js +42 -0
  87. package/src/commands/distill.js +48 -0
  88. package/src/commands/doctor.js +89 -0
  89. package/src/commands/document.js +83 -0
  90. package/src/commands/eight-bit.js +25 -0
  91. package/src/commands/extract.js +75 -0
  92. package/src/commands/fix.js +46 -0
  93. package/src/commands/harden.js +85 -0
  94. package/src/commands/index.js +67 -0
  95. package/src/commands/install.js +75 -0
  96. package/src/commands/layout.js +48 -0
  97. package/src/commands/live.js +44 -0
  98. package/src/commands/manifest.js +25 -0
  99. package/src/commands/office.js +25 -0
  100. package/src/commands/onboard.js +46 -0
  101. package/src/commands/oneshot.js +60 -0
  102. package/src/commands/optimize.js +46 -0
  103. package/src/commands/overdrive.js +46 -0
  104. package/src/commands/parser.js +143 -0
  105. package/src/commands/pixelcrew.js +96 -0
  106. package/src/commands/plan.js +66 -0
  107. package/src/commands/polish.js +45 -0
  108. package/src/commands/quieter.js +48 -0
  109. package/src/commands/recap.js +358 -0
  110. package/src/commands/registry.js +353 -0
  111. package/src/commands/render.js +26 -0
  112. package/src/commands/retrofit.js +25 -0
  113. package/src/commands/review.js +118 -0
  114. package/src/commands/roster.js +25 -0
  115. package/src/commands/sentinel.js +25 -0
  116. package/src/commands/shape.js +66 -0
  117. package/src/commands/skills.js +57 -0
  118. package/src/commands/status.js +48 -0
  119. package/src/commands/stop.js +50 -0
  120. package/src/commands/sync.js +58 -0
  121. package/src/commands/typeset.js +58 -0
  122. package/src/commands/warp.js +25 -0
  123. package/src/core/agent-runtime.js +94 -0
  124. package/src/core/event-bus.js +98 -0
  125. package/src/core/index.js +9 -0
  126. package/src/core/orchestrator.js +234 -0
  127. package/src/core/scheduler.js +124 -0
  128. package/src/core/task-graph.js +185 -0
  129. package/src/dashboard/app.js +1172 -30
  130. package/src/dashboard/index.html +223 -23
  131. package/src/dashboard/styles.css +870 -0
  132. package/src/index.js +16 -0
  133. package/src/orchestrator/artifact-graph.js +104 -0
  134. package/src/orchestrator/ast-normalizer.js +147 -0
  135. package/src/orchestrator/code-generator.js +846 -0
  136. package/src/orchestrator/design-engine.js +203 -0
  137. package/src/orchestrator/engine.js +318 -39
  138. package/src/orchestrator/oneshot.js +351 -1561
  139. package/src/orchestrator/ontology.js +394 -0
  140. package/src/orchestrator/planner.js +302 -0
  141. package/src/orchestrator/repair-planner.js +67 -0
  142. package/src/orchestrator/requirement-contract.js +177 -0
  143. package/src/orchestrator/requirement-validator.js +39 -0
  144. package/src/orchestrator/semantic-engine.js +582 -0
  145. package/src/orchestrator/semantic-reviewer.js +179 -0
  146. package/src/orchestrator/skills-registry.js +194 -0
  147. package/src/orchestrator/task-planner.js +111 -0
  148. package/src/orchestrator/task-queue.js +128 -0
  149. package/src/orchestrator/telemetry.js +209 -0
  150. package/src/orchestrator/ui-synthesizer.js +245 -0
  151. package/src/orchestrator/visual-critic.js +106 -0
  152. package/src/policies/architecture/layering.policy.js +14 -0
  153. package/src/policies/performance/indexing.policy.js +13 -0
  154. package/src/policies/reliability/resilience.policy.js +13 -0
  155. package/src/policies/security/auth.policy.js +31 -0
  156. package/src/protocol/agent.js +124 -0
  157. package/src/protocol/event.js +78 -0
  158. package/src/protocol/index.js +8 -0
  159. package/src/protocol/skill.js +54 -0
  160. package/src/protocol/task.js +100 -0
  161. package/src/scaffold/claude-generator.js +60 -0
  162. package/src/scaffold/commands-catalog.js +321 -0
  163. package/src/scaffold/cursor-generator.js +74 -0
  164. package/src/scaffold/ide-rules.js +133 -0
  165. package/src/scaffold/init.js +415 -91
  166. package/src/scaffold/installer.js +571 -0
  167. package/src/scaffold/kiro-generator.js +86 -0
  168. package/src/scaffold/skills-bundle.js +520 -0
  169. package/src/scaffold/templates.js +57 -4
  170. package/src/scaffold/workstations.js +364 -0
  171. package/src/server/server.js +292 -42
  172. package/src/utils/fs-safe.js +168 -0
  173. package/src/utils/markdown-report.js +175 -0
@@ -1,5 +1,5 @@
1
1
  /**
2
- * PIXEL CORPS HQ — Interactive Startup Office Engine & Pixel Art Canvas Renderer
2
+ * PIXEL CREW HQ — Autonomous Multi-Agent Office & Realtime Telemetry Engine
3
3
  */
4
4
 
5
5
  // Global State
@@ -8,9 +8,15 @@ const appState = {
8
8
  state: null,
9
9
  events: [],
10
10
  activeFilter: 'all',
11
+ streamMode: 'sidechat', // 'sidechat' | 'slack'
12
+ autoScrollEnabled: true,
11
13
  audioEnabled: true,
12
14
  crtEnabled: true,
13
15
  nightMode: false,
16
+ activeProvider: 'antigravity',
17
+ activeProviderName: 'Google Antigravity',
18
+ activeProviderIcon: '🪐',
19
+ providersData: null,
14
20
  missionStartTime: null,
15
21
  missionTimerInterval: null,
16
22
  hoveredDesk: null
@@ -104,7 +110,303 @@ const WORKSTATIONS = {
104
110
  }
105
111
  };
106
112
 
107
- // Retro Web Audio Synthesizer
113
+ // ============================================================================
114
+ // PROCEDURAL LO-FI MUSIC SYNTHESIZER & AMBIENT RADIO
115
+ // ============================================================================
116
+ class LofiMusicEngine {
117
+ constructor() {
118
+ this.ctx = null;
119
+ this.isPlaying = false;
120
+ this.currentTrack = 'floor42';
121
+ this.volume = 0.7;
122
+ this.loopTimer = null;
123
+ this.noiseNode = null;
124
+ this.noiseGain = null;
125
+ this.chordStep = 0;
126
+ this.bpm = 75;
127
+ }
128
+
129
+ initContext() {
130
+ if (!this.ctx) {
131
+ const AudioCtx = window.AudioContext || window.webkitAudioContext;
132
+ if (AudioCtx) {
133
+ this.ctx = new AudioCtx();
134
+ }
135
+ }
136
+ if (this.ctx && this.ctx.state === 'suspended') {
137
+ this.ctx.resume();
138
+ }
139
+ }
140
+
141
+ setVolume(val) {
142
+ this.volume = Math.max(0, Math.min(1, val));
143
+ if (this.masterGain && this.ctx) {
144
+ this.masterGain.gain.setValueAtTime(this.volume * 0.8, this.ctx.currentTime);
145
+ }
146
+ }
147
+
148
+ setTrack(trackId) {
149
+ this.currentTrack = trackId;
150
+ this.chordStep = 0;
151
+ if (this.isPlaying) {
152
+ this.stop();
153
+ this.start();
154
+ }
155
+ }
156
+
157
+ start() {
158
+ this.initContext();
159
+ if (!this.ctx) return;
160
+ if (this.isPlaying) return;
161
+ this.isPlaying = true;
162
+
163
+ this.masterGain = this.ctx.createGain();
164
+ this.masterGain.gain.setValueAtTime(this.volume * 0.8, this.ctx.currentTime);
165
+ this.masterGain.connect(this.ctx.destination);
166
+
167
+ this.startVinylNoise();
168
+ this.scheduleNextBeat();
169
+
170
+ const btn = document.getElementById('btnLofiToggle');
171
+ const visualizer = document.getElementById('lofiVisualizer');
172
+ const playIcon = document.getElementById('lofiPlayIcon');
173
+ if (btn) btn.classList.add('playing');
174
+ if (visualizer) visualizer.classList.add('playing');
175
+ if (playIcon) playIcon.textContent = '⏸';
176
+ }
177
+
178
+ stop() {
179
+ this.isPlaying = false;
180
+ if (this.loopTimer) {
181
+ clearTimeout(this.loopTimer);
182
+ this.loopTimer = null;
183
+ }
184
+ this.stopVinylNoise();
185
+
186
+ const btn = document.getElementById('btnLofiToggle');
187
+ const visualizer = document.getElementById('lofiVisualizer');
188
+ const playIcon = document.getElementById('lofiPlayIcon');
189
+ if (btn) btn.classList.remove('playing');
190
+ if (visualizer) visualizer.classList.remove('playing');
191
+ if (playIcon) playIcon.textContent = '▶';
192
+ }
193
+
194
+ toggle() {
195
+ if (this.isPlaying) this.stop();
196
+ else this.start();
197
+ }
198
+
199
+ startVinylNoise() {
200
+ if (!this.ctx || this.noiseNode) return;
201
+ try {
202
+ const bufferSize = this.ctx.sampleRate * 2;
203
+ const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
204
+ const data = buffer.getChannelData(0);
205
+ for (let i = 0; i < bufferSize; i++) {
206
+ const white = Math.random() * 2 - 1;
207
+ const isPop = Math.random() < 0.0008;
208
+ data[i] = (white * 0.035) + (isPop ? (Math.random() * 0.35 - 0.17) : 0);
209
+ }
210
+
211
+ this.noiseNode = this.ctx.createBufferSource();
212
+ this.noiseNode.buffer = buffer;
213
+ this.noiseNode.loop = true;
214
+
215
+ const filter = this.ctx.createBiquadFilter();
216
+ filter.type = 'lowpass';
217
+ filter.frequency.value = 1600;
218
+
219
+ this.noiseGain = this.ctx.createGain();
220
+ this.noiseGain.gain.setValueAtTime(0.06, this.ctx.currentTime);
221
+
222
+ this.noiseNode.connect(filter);
223
+ filter.connect(this.noiseGain);
224
+ this.noiseGain.connect(this.masterGain);
225
+
226
+ this.noiseNode.start();
227
+ } catch {}
228
+ }
229
+
230
+ stopVinylNoise() {
231
+ if (this.noiseNode) {
232
+ try { this.noiseNode.stop(); } catch {}
233
+ this.noiseNode = null;
234
+ }
235
+ }
236
+
237
+ playWarmChord(freqs, duration = 2.4, gainLevel = 0.07) {
238
+ if (!this.ctx || !this.isPlaying) return;
239
+ const now = this.ctx.currentTime;
240
+
241
+ freqs.forEach((freq, i) => {
242
+ try {
243
+ const osc = this.ctx.createOscillator();
244
+ const subOsc = this.ctx.createOscillator();
245
+ const filter = this.ctx.createBiquadFilter();
246
+ const gain = this.ctx.createGain();
247
+
248
+ osc.type = 'triangle';
249
+ osc.frequency.setValueAtTime(freq, now);
250
+
251
+ subOsc.type = 'sine';
252
+ subOsc.frequency.setValueAtTime(freq * 0.5, now);
253
+
254
+ filter.type = 'lowpass';
255
+ filter.frequency.setValueAtTime(1400, now);
256
+ filter.frequency.exponentialRampToValueAtTime(500, now + duration);
257
+
258
+ const stagger = i * 0.022;
259
+ gain.gain.setValueAtTime(0.0001, now + stagger);
260
+ gain.gain.exponentialRampToValueAtTime(gainLevel / freqs.length, now + stagger + 0.08);
261
+ gain.gain.exponentialRampToValueAtTime(0.0001, now + duration);
262
+
263
+ osc.connect(filter);
264
+ subOsc.connect(filter);
265
+ filter.connect(gain);
266
+ gain.connect(this.masterGain);
267
+
268
+ osc.start(now + stagger);
269
+ subOsc.start(now + stagger);
270
+ osc.stop(now + duration);
271
+ subOsc.stop(now + duration);
272
+ } catch {}
273
+ });
274
+ }
275
+
276
+ playLofiKick() {
277
+ if (!this.ctx || !this.isPlaying) return;
278
+ try {
279
+ const now = this.ctx.currentTime;
280
+ const osc = this.ctx.createOscillator();
281
+ const gain = this.ctx.createGain();
282
+
283
+ osc.frequency.setValueAtTime(110, now);
284
+ osc.frequency.exponentialRampToValueAtTime(42, now + 0.16);
285
+
286
+ gain.gain.setValueAtTime(0.1, now);
287
+ gain.gain.exponentialRampToValueAtTime(0.001, now + 0.18);
288
+
289
+ osc.connect(gain);
290
+ gain.connect(this.masterGain);
291
+
292
+ osc.start(now);
293
+ osc.stop(now + 0.18);
294
+ } catch {}
295
+ }
296
+
297
+ playLofiSnare() {
298
+ if (!this.ctx || !this.isPlaying) return;
299
+ try {
300
+ const now = this.ctx.currentTime;
301
+ const buffer = this.ctx.createBuffer(1, this.ctx.sampleRate * 0.14, this.ctx.sampleRate);
302
+ const data = buffer.getChannelData(0);
303
+ for (let i = 0; i < data.length; i++) data[i] = Math.random() * 2 - 1;
304
+
305
+ const noise = this.ctx.createBufferSource();
306
+ noise.buffer = buffer;
307
+
308
+ const filter = this.ctx.createBiquadFilter();
309
+ filter.type = 'bandpass';
310
+ filter.frequency.setValueAtTime(1100, now);
311
+
312
+ const gain = this.ctx.createGain();
313
+ gain.gain.setValueAtTime(0.05, now);
314
+ gain.gain.exponentialRampToValueAtTime(0.001, now + 0.14);
315
+
316
+ noise.connect(filter);
317
+ filter.connect(gain);
318
+ gain.connect(this.masterGain);
319
+
320
+ noise.start(now);
321
+ noise.stop(now + 0.14);
322
+ } catch {}
323
+ }
324
+
325
+ playLofiHat() {
326
+ if (!this.ctx || !this.isPlaying) return;
327
+ try {
328
+ const now = this.ctx.currentTime;
329
+ const buffer = this.ctx.createBuffer(1, this.ctx.sampleRate * 0.04, this.ctx.sampleRate);
330
+ const data = buffer.getChannelData(0);
331
+ for (let i = 0; i < data.length; i++) data[i] = Math.random() * 2 - 1;
332
+
333
+ const noise = this.ctx.createBufferSource();
334
+ noise.buffer = buffer;
335
+
336
+ const filter = this.ctx.createBiquadFilter();
337
+ filter.type = 'highpass';
338
+ filter.frequency.setValueAtTime(6500, now);
339
+
340
+ const gain = this.ctx.createGain();
341
+ gain.gain.setValueAtTime(0.018, now);
342
+ gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.04);
343
+
344
+ noise.connect(filter);
345
+ filter.connect(gain);
346
+ gain.connect(this.masterGain);
347
+
348
+ noise.start(now);
349
+ noise.stop(now + 0.04);
350
+ } catch {}
351
+ }
352
+
353
+ scheduleNextBeat() {
354
+ if (!this.isPlaying) return;
355
+
356
+ const CHORD_PROGRESSIONS = {
357
+ floor42: [
358
+ [293.66, 349.23, 440.00, 523.25, 659.25], // Dm9
359
+ [392.00, 493.88, 587.33, 698.46, 880.00], // G13
360
+ [261.63, 329.63, 392.00, 493.88, 587.33], // Cmaj9
361
+ [220.00, 277.18, 329.63, 440.00, 554.37] // A7b13
362
+ ],
363
+ rainy: [
364
+ [349.23, 440.00, 523.25, 659.25, 783.99], // Fmaj9
365
+ [329.63, 392.00, 493.88, 587.33], // Em7
366
+ [293.66, 349.23, 440.00, 523.25], // Dm7
367
+ [220.00, 261.63, 329.63, 392.00, 493.88] // Am9
368
+ ],
369
+ antigravity: [
370
+ [261.63, 329.63, 392.00, 523.25, 659.25], // Cmaj7/9
371
+ [220.00, 261.63, 329.63, 440.00, 587.33], // Am9
372
+ [349.23, 440.00, 523.25, 698.46], // Fmaj7
373
+ [196.00, 246.94, 293.66, 392.00, 493.88] // G6
374
+ ],
375
+ retro8bit: [
376
+ [261.63, 329.63, 392.00, 523.25], // C
377
+ [220.00, 261.63, 329.63, 440.00], // Am
378
+ [174.61, 220.00, 261.63, 349.23], // F
379
+ [196.00, 246.94, 293.66, 392.00] // G
380
+ ]
381
+ };
382
+
383
+ const chords = CHORD_PROGRESSIONS[this.currentTrack] || CHORD_PROGRESSIONS.floor42;
384
+ const currentChord = chords[this.chordStep % chords.length];
385
+
386
+ this.playWarmChord(currentChord, 2.4, 0.08);
387
+
388
+ const beatInterval = (60 / this.bpm) * 1000;
389
+
390
+ this.playLofiKick();
391
+ this.playLofiHat();
392
+
393
+ setTimeout(() => { if (this.isPlaying) { this.playLofiSnare(); this.playLofiHat(); } }, beatInterval);
394
+ setTimeout(() => { if (this.isPlaying) { this.playLofiKick(); this.playLofiHat(); } }, beatInterval * 2);
395
+ setTimeout(() => { if (this.isPlaying) { this.playLofiHat(); } }, beatInterval * 2.5);
396
+ setTimeout(() => { if (this.isPlaying) { this.playLofiSnare(); this.playLofiHat(); } }, beatInterval * 3);
397
+
398
+ this.chordStep++;
399
+ this.loopTimer = setTimeout(() => {
400
+ this.scheduleNextBeat();
401
+ }, beatInterval * 4);
402
+ }
403
+ }
404
+
405
+ const lofiEngine = new LofiMusicEngine();
406
+
407
+ // ============================================================================
408
+ // RETRO SFX AUDIO SYNTHESIZER
409
+ // ============================================================================
108
410
  class RetroAudioSynth {
109
411
  constructor() {
110
412
  this.ctx = null;
@@ -122,7 +424,7 @@ class RetroAudioSynth {
122
424
  }
123
425
  }
124
426
 
125
- playTone(freq, type = 'square', duration = 0.08, gainVal = 0.05) {
427
+ playTone(freq, type = 'square', duration = 0.08, gainVal = 0.04) {
126
428
  if (!appState.audioEnabled) return;
127
429
  try {
128
430
  this.initContext();
@@ -140,36 +442,40 @@ class RetroAudioSynth {
140
442
 
141
443
  osc.start();
142
444
  osc.stop(this.ctx.currentTime + duration);
143
- } catch (e) {
144
- // Audio autoplay policy
145
- }
445
+ } catch {}
146
446
  }
147
447
 
148
448
  playSpawn() {
149
449
  if (!appState.audioEnabled) return;
150
450
  this.playTone(330, 'triangle', 0.06);
151
- setTimeout(() => this.playTone(440, 'triangle', 0.06), 60);
152
- setTimeout(() => this.playTone(660, 'square', 0.1), 120);
451
+ setTimeout(() => this.playTone(440, 'triangle', 0.06), 55);
452
+ setTimeout(() => this.playTone(660, 'square', 0.09), 110);
453
+ }
454
+
455
+ playTool() {
456
+ if (!appState.audioEnabled) return;
457
+ this.playTone(520, 'triangle', 0.04, 0.03);
458
+ setTimeout(() => this.playTone(780, 'square', 0.04, 0.02), 35);
153
459
  }
154
460
 
155
461
  playSkill() {
156
462
  if (!appState.audioEnabled) return;
157
- this.playTone(659.25, 'square', 0.05, 0.04);
158
- setTimeout(() => this.playTone(987.77, 'square', 0.1, 0.04), 50);
463
+ this.playTone(659.25, 'square', 0.05, 0.03);
464
+ setTimeout(() => this.playTone(987.77, 'square', 0.09, 0.03), 45);
159
465
  }
160
466
 
161
467
  playComplete() {
162
468
  if (!appState.audioEnabled) return;
163
469
  const notes = [523.25, 659.25, 783.99, 1046.50];
164
470
  notes.forEach((note, i) => {
165
- setTimeout(() => this.playTone(note, 'square', 0.12, 0.05), i * 80);
471
+ setTimeout(() => this.playTone(note, 'square', 0.1, 0.04), i * 75);
166
472
  });
167
473
  }
168
474
 
169
475
  playError() {
170
476
  if (!appState.audioEnabled) return;
171
- this.playTone(180, 'sawtooth', 0.15, 0.08);
172
- setTimeout(() => this.playTone(140, 'sawtooth', 0.25, 0.08), 100);
477
+ this.playTone(180, 'sawtooth', 0.12, 0.06);
478
+ setTimeout(() => this.playTone(140, 'sawtooth', 0.2, 0.06), 90);
173
479
  }
174
480
 
175
481
  playClick() {
@@ -179,7 +485,9 @@ class RetroAudioSynth {
179
485
 
180
486
  const synth = new RetroAudioSynth();
181
487
 
182
- // Pixel Office Canvas Renderer
488
+ // ============================================================================
489
+ // PIXEL OFFICE CANVAS RENDERER (HIGH-DPI RETINA SUPPORT)
490
+ // ============================================================================
183
491
  let officeCanvas, officeCtx;
184
492
  let animFrame = 0;
185
493
 
@@ -188,11 +496,20 @@ function initOfficeCanvas() {
188
496
  if (!officeCanvas) return;
189
497
  officeCtx = officeCanvas.getContext('2d');
190
498
 
499
+ // Handle Retina / High-DPI Display Scaling
500
+ const dpr = window.devicePixelRatio || 1;
501
+ const baseW = 960;
502
+ const baseH = 420;
503
+
504
+ officeCanvas.width = baseW * dpr;
505
+ officeCanvas.height = baseH * dpr;
506
+ officeCtx.scale(dpr, dpr);
507
+
191
508
  // Mouse interaction for tooltips and clicking workstations
192
509
  officeCanvas.addEventListener('mousemove', (e) => {
193
510
  const rect = officeCanvas.getBoundingClientRect();
194
- const scaleX = officeCanvas.width / rect.width;
195
- const scaleY = officeCanvas.height / rect.height;
511
+ const scaleX = baseW / rect.width;
512
+ const scaleY = baseH / rect.height;
196
513
  const mouseX = (e.clientX - rect.left) * scaleX;
197
514
  const mouseY = (e.clientY - rect.top) * scaleY;
198
515
 
@@ -433,6 +750,12 @@ function renderOfficeWorker(ctx, agentKey, state, x, y, frame) {
433
750
 
434
751
  const suitColor = colors[agentKey] || '#00f0ff';
435
752
 
753
+ // Monitor Glow Reflection on Desk
754
+ if (state === 'WORKING' || state === 'ANALYZING') {
755
+ ctx.fillStyle = suitColor + '18';
756
+ ctx.fillRect(x - 14, y + 14, 40, 16);
757
+ }
758
+
436
759
  // Hair / Head
437
760
  ctx.fillStyle = '#ffd1a4'; // skin
438
761
  ctx.fillRect(x, y + offsetY, 10, 10);
@@ -464,17 +787,56 @@ function renderOfficeWorker(ctx, agentKey, state, x, y, frame) {
464
787
  ctx.fillStyle = '#ffffff';
465
788
  ctx.fillRect(x + ((frame * 3) % 12) - 2, y + 8, 2, 2);
466
789
  } else if (state === 'ANALYZING') {
467
- // Thinking bubble
790
+ // Small thought icon
468
791
  ctx.fillStyle = '#ffffff';
469
- ctx.fillRect(x + 12, y - 6, 8, 6);
792
+ ctx.fillRect(x + 12, y - 6 + offsetY, 8, 6);
470
793
  ctx.fillStyle = '#000000';
471
- ctx.fillRect(x + 14, y - 4, 4, 2);
794
+ ctx.fillRect(x + 14, y - 4 + offsetY, 4, 2);
472
795
  } else if (state === 'COMPLETED') {
473
796
  // Victory hands up
474
797
  ctx.fillStyle = '#ffd1a4';
475
798
  ctx.fillRect(x - 5, y + 6 + offsetY, 3, 5);
476
799
  ctx.fillRect(x + 12, y + 6 + offsetY, 3, 5);
477
800
  }
801
+
802
+ // Floating Live Task / Speech Bubble above Worker's Head
803
+ if (state === 'WORKING' || state === 'ANALYZING') {
804
+ const aData = (appState.state && appState.state.agents) ? appState.state.agents[agentKey] : null;
805
+ let label = state === 'ANALYZING' ? '🧠 Thinking...' : '⚡ Building...';
806
+ if (aData && aData.currentTask && aData.currentTask !== 'Idle') {
807
+ const t = aData.currentTask;
808
+ if (t.includes('view_file') || t.includes('reading') || t.includes('Read')) label = '📄 ' + (t.split('/').pop().slice(0, 12) || 'file');
809
+ else if (t.includes('replace_file') || t.includes('writing') || t.includes('Edit')) label = '📝 ' + (t.split('/').pop().slice(0, 12) || 'code');
810
+ else if (t.includes('run_command') || t.includes('exec') || t.includes('npm')) label = '⚡ ' + (t.split(' ')[0].slice(0, 10) || 'exec');
811
+ else if (t.includes('grep') || t.includes('search')) label = '🔍 Search';
812
+ else label = t.length > 14 ? t.slice(0, 12) + '..' : t;
813
+ }
814
+
815
+ ctx.font = '7px "Press Start 2P", monospace';
816
+ const textWidth = ctx.measureText(label).width;
817
+ const bubbleW = textWidth + 12;
818
+ const bubbleH = 15;
819
+ const bubbleX = x - bubbleW / 2 + 5;
820
+ const bubbleY = y - 22 + offsetY;
821
+
822
+ ctx.fillStyle = '#000000';
823
+ ctx.fillRect(bubbleX + 2, bubbleY + 2, bubbleW, bubbleH);
824
+ ctx.fillStyle = '#0a0e1c';
825
+ ctx.fillRect(bubbleX, bubbleY, bubbleW, bubbleH);
826
+ ctx.strokeStyle = suitColor;
827
+ ctx.lineWidth = 1;
828
+ ctx.strokeRect(bubbleX, bubbleY, bubbleW, bubbleH);
829
+
830
+ ctx.fillStyle = '#0a0e1c';
831
+ ctx.beginPath();
832
+ ctx.moveTo(x + 3, bubbleY + bubbleH);
833
+ ctx.lineTo(x + 7, bubbleY + bubbleH);
834
+ ctx.lineTo(x + 5, bubbleY + bubbleH + 3);
835
+ ctx.fill();
836
+
837
+ ctx.fillStyle = '#ffffff';
838
+ ctx.fillText(label, bubbleX + 6, bubbleY + 10);
839
+ }
478
840
  }
479
841
 
480
842
  // Break Lounge Area (Espresso machine, water cooler, beanbag)
@@ -493,7 +855,6 @@ function renderBreakLounge(ctx, x, y, w, h) {
493
855
  // Espresso Bar
494
856
  ctx.fillStyle = '#2c223b';
495
857
  ctx.fillRect(x + 14, y + 30, 65, 45);
496
- ctx.fillStyle = '#silver';
497
858
  ctx.fillStyle = '#a0aab8';
498
859
  ctx.fillRect(x + 20, y + 34, 24, 20); // coffee maker
499
860
  ctx.fillStyle = '#00f0ff';
@@ -552,7 +913,284 @@ function startOfficeAnimationLoop() {
552
913
  requestAnimationFrame(loop);
553
914
  }
554
915
 
555
- // Initialize Application
916
+ // ============================================================================
917
+ // REALTIME ANTIGRAVITY SIDECHAT VIEW ENGINE
918
+ // ============================================================================
919
+ function updateActiveTaskHud(taskText, toolName = null) {
920
+ const hudTaskText = document.getElementById('hudActiveTaskText');
921
+ const hudToolBadge = document.getElementById('hudToolBadge');
922
+ const hudToolName = document.getElementById('hudToolName');
923
+
924
+ if (hudTaskText && taskText) {
925
+ hudTaskText.textContent = taskText;
926
+ hudTaskText.title = taskText;
927
+ }
928
+
929
+ if (hudToolBadge && hudToolName) {
930
+ if (toolName) {
931
+ hudToolBadge.style.display = 'inline-flex';
932
+ hudToolName.textContent = toolName.toUpperCase();
933
+ } else {
934
+ hudToolBadge.style.display = 'none';
935
+ }
936
+ }
937
+ }
938
+
939
+ function parseToolAction(msg, metadata = {}) {
940
+ if (!msg) return { action: 'EXEC', file: '', snippet: '' };
941
+ const lower = msg.toLowerCase();
942
+
943
+ let action = metadata.action || 'EXEC';
944
+ let file = metadata.file || '';
945
+ let snippet = msg;
946
+
947
+ if (!metadata.action) {
948
+ if (lower.includes('view_file') || lower.includes('reading') || lower.includes('read ')) {
949
+ action = 'READ';
950
+ } else if (lower.includes('replace_file') || lower.includes('writing') || lower.includes('modify') || lower.includes('edit')) {
951
+ action = 'EDIT';
952
+ } else if (lower.includes('run_command') || lower.includes('npm') || lower.includes('executing')) {
953
+ action = 'EXEC';
954
+ } else if (lower.includes('grep') || lower.includes('search') || lower.includes('finding')) {
955
+ action = 'SEARCH';
956
+ } else if (lower.includes('browser') || lower.includes('open_url')) {
957
+ action = 'BROWSER';
958
+ }
959
+ }
960
+
961
+ // Extract file path if not already provided
962
+ if (!file) {
963
+ const fileMatch = msg.match(/(?:file|path|to|in|edited|reading)\s*[:=]?\s*[`"']?([a-zA-Z0-9_\-./\\]+\.[a-zA-Z0-9]+)[`"']?/i);
964
+ if (fileMatch) {
965
+ file = fileMatch[1];
966
+ }
967
+ }
968
+
969
+ return { action, file, snippet };
970
+ }
971
+
972
+ function appendSidechatCard(event) {
973
+ const container = document.getElementById('sidechatStream');
974
+ if (!container) return;
975
+
976
+ const timeStr = new Date(event.timestamp || Date.now()).toTimeString().split(' ')[0];
977
+ const agentKey = event.agent || 'orchestrator';
978
+ const ws = WORKSTATIONS[agentKey] || { name: 'Tech Lead', color: '#ffd700', icon: '👔' };
979
+ const agentColor = ws.color || '#00f0ff';
980
+ const agentIcon = ws.icon || '🤖';
981
+ const agentUpper = agentKey.toUpperCase();
982
+
983
+ const card = document.createElement('div');
984
+ card.className = `sidechat-card card-${agentKey}`;
985
+
986
+ if (event.type === 'thinking' || (agentKey === 'orchestrator' && event.type === 'progress')) {
987
+ card.innerHTML = `
988
+ <div class="sidechat-card-header">
989
+ <span class="sidechat-agent-badge" style="color: ${agentColor}; border-color: ${agentColor};">
990
+ ${agentIcon} #${agentUpper}
991
+ </span>
992
+ <span class="sidechat-type-badge">THINKING</span>
993
+ <span class="sidechat-timestamp">${timeStr}</span>
994
+ </div>
995
+ <div class="sidechat-thought-box">
996
+ <span class="thought-brain-icon">🧠</span>
997
+ <div class="thought-content">
998
+ ${escapeHtml(event.message)}
999
+ <span class="thought-typing-dots"></span>
1000
+ </div>
1001
+ </div>
1002
+ `;
1003
+ updateActiveTaskHud(event.message, 'REASONING');
1004
+ } else if (event.type === 'tool' || event.type === 'tool_call' || event.skill || event.message.includes('file') || event.message.includes('npm') || event.message.includes('run') || event.message.includes('Edit')) {
1005
+ const toolInfo = parseToolAction(event.message, event.metadata || {});
1006
+ const actionClass = toolInfo.action.toLowerCase();
1007
+
1008
+ card.innerHTML = `
1009
+ <div class="sidechat-card-header">
1010
+ <span class="sidechat-agent-badge" style="color: ${agentColor}; border-color: ${agentColor};">
1011
+ ${agentIcon} #${agentUpper}
1012
+ </span>
1013
+ <span class="sidechat-type-badge">${toolInfo.action}</span>
1014
+ <span class="sidechat-timestamp">${timeStr}</span>
1015
+ </div>
1016
+ <div class="sidechat-tool-box">
1017
+ <div class="tool-box-header">
1018
+ <span class="tool-action-pill ${actionClass}">[${toolInfo.action}]</span>
1019
+ ${toolInfo.file ? `<span class="tool-target-path">${escapeHtml(toolInfo.file)}</span>` : ''}
1020
+ </div>
1021
+ <div class="tool-snippet-block">${escapeHtml(event.message)}</div>
1022
+ </div>
1023
+ `;
1024
+ updateActiveTaskHud(event.message, toolInfo.action);
1025
+ synth.playTool();
1026
+ } else if (event.type === 'complete') {
1027
+ card.innerHTML = `
1028
+ <div class="sidechat-card-header">
1029
+ <span class="sidechat-agent-badge" style="color: #39ff14; border-color: #39ff14;">
1030
+ ${agentIcon} #${agentUpper}
1031
+ </span>
1032
+ <span class="sidechat-type-badge" style="background: #0e2016; color: #39ff14;">COMPLETED</span>
1033
+ <span class="sidechat-timestamp">${timeStr}</span>
1034
+ </div>
1035
+ <div style="background: #0c1a14; border: 1px solid #165b38; padding: 8px 10px; color: #39ff14; font-size: 12px;">
1036
+ ✓ ${escapeHtml(event.message)}
1037
+ </div>
1038
+ `;
1039
+ updateActiveTaskHud(event.message, 'SUCCESS');
1040
+ } else if (event.type === 'error') {
1041
+ card.innerHTML = `
1042
+ <div class="sidechat-card-header">
1043
+ <span class="sidechat-agent-badge" style="color: #ff3344; border-color: #ff3344;">
1044
+ ${agentIcon} #${agentUpper}
1045
+ </span>
1046
+ <span class="sidechat-type-badge" style="background: #2a0c0e; color: #ff3344;">ERROR</span>
1047
+ <span class="sidechat-timestamp">${timeStr}</span>
1048
+ </div>
1049
+ <div style="background: #1c0606; border: 1px solid #551111; padding: 8px 10px; color: #ff3344; font-size: 12px;">
1050
+ ⚠ ${escapeHtml(event.message)}
1051
+ </div>
1052
+ `;
1053
+ } else {
1054
+ card.innerHTML = `
1055
+ <div class="sidechat-card-header">
1056
+ <span class="sidechat-agent-badge" style="color: ${agentColor}; border-color: ${agentColor};">
1057
+ ${agentIcon} #${agentUpper}
1058
+ </span>
1059
+ <span class="sidechat-type-badge">LOG</span>
1060
+ <span class="sidechat-timestamp">${timeStr}</span>
1061
+ </div>
1062
+ <div style="color: #cbd5e1; font-size: 12px; line-height: 1.4;">
1063
+ ${escapeHtml(event.message)}
1064
+ </div>
1065
+ `;
1066
+ }
1067
+
1068
+ container.appendChild(card);
1069
+ if (appState.autoScrollEnabled) {
1070
+ container.scrollTop = container.scrollHeight;
1071
+ }
1072
+ }
1073
+
1074
+ function renderSidechat(events) {
1075
+ const container = document.getElementById('sidechatStream');
1076
+ if (!container) return;
1077
+ container.innerHTML = '';
1078
+ for (const evt of events) {
1079
+ appendSidechatCard(evt);
1080
+ }
1081
+ }
1082
+
1083
+ // ============================================================================
1084
+ // ACCURATE PROVIDER SWITCHER & DIAGNOSTICS MODAL
1085
+ // ============================================================================
1086
+ function updateProviderUI(provData) {
1087
+ if (!provData) return;
1088
+ appState.providersData = provData;
1089
+
1090
+ const activeId = provData.activeProvider || 'antigravity';
1091
+ const activeName = provData.activeProviderName || 'Google Antigravity';
1092
+ const activeIcon = provData.activeProviderIcon || '🪐';
1093
+ const activeDesc = provData.activeProviderDescription || 'Integrated Antigravity IDE Autonomous Pair Programming Engine';
1094
+
1095
+ appState.activeProvider = activeId;
1096
+ appState.activeProviderName = activeName;
1097
+ appState.activeProviderIcon = activeIcon;
1098
+
1099
+ // Header Badge
1100
+ const badgeNameEl = document.getElementById('providerName');
1101
+ const badgeIconEl = document.getElementById('providerIcon');
1102
+ if (badgeNameEl) badgeNameEl.textContent = activeId.toUpperCase();
1103
+ if (badgeIconEl) badgeIconEl.textContent = activeIcon;
1104
+
1105
+ // Modal Header
1106
+ const modalNameEl = document.getElementById('modalActiveProvName');
1107
+ const modalIconEl = document.getElementById('modalActiveProvIcon');
1108
+ const modalDescEl = document.getElementById('modalActiveProvDesc');
1109
+ if (modalNameEl) modalNameEl.textContent = `${activeIcon} ${activeName} (${activeId.toUpperCase()})`;
1110
+ if (modalIconEl) modalIconEl.textContent = activeIcon;
1111
+ if (modalDescEl) modalDescEl.textContent = activeDesc;
1112
+
1113
+ // Render Grid
1114
+ renderProvidersGrid(provData);
1115
+ }
1116
+
1117
+ function renderProvidersGrid(provData) {
1118
+ const grid = document.getElementById('providerGrid');
1119
+ if (!grid || !provData) return;
1120
+
1121
+ grid.innerHTML = '';
1122
+
1123
+ const allProviders = [
1124
+ ...(provData.available || []),
1125
+ ...(provData.missing || [])
1126
+ ];
1127
+
1128
+ allProviders.forEach(p => {
1129
+ const isActive = p.id === provData.activeProvider;
1130
+ const isAvailable = (provData.available || []).some(av => av.id === p.id);
1131
+ const card = document.createElement('div');
1132
+ card.className = `provider-card ${isActive ? 'active' : (isAvailable ? 'available' : 'missing')}`;
1133
+
1134
+ card.innerHTML = `
1135
+ <div class="provider-card-header">
1136
+ <div class="provider-card-title">
1137
+ <span>${p.icon || '💻'}</span>
1138
+ <span>${p.name}</span>
1139
+ </div>
1140
+ <span class="provider-card-status">
1141
+ ${isActive ? 'ACTIVE' : (isAvailable ? 'DETECTED' : 'UNAVAILABLE')}
1142
+ </span>
1143
+ </div>
1144
+ <div class="provider-card-desc">${p.description || ''}</div>
1145
+ `;
1146
+
1147
+ card.addEventListener('click', async () => {
1148
+ synth.playClick();
1149
+ await selectProvider(p.id);
1150
+ });
1151
+
1152
+ grid.appendChild(card);
1153
+ });
1154
+ }
1155
+
1156
+ async function selectProvider(providerId) {
1157
+ try {
1158
+ const res = await fetch('/api/providers/select', {
1159
+ method: 'POST',
1160
+ headers: { 'Content-Type': 'application/json' },
1161
+ body: JSON.stringify({ provider: providerId })
1162
+ });
1163
+ if (res.ok) {
1164
+ const data = await res.json();
1165
+ if (data.providers) {
1166
+ updateProviderUI(data.providers);
1167
+ }
1168
+ synth.playSkill();
1169
+ }
1170
+ } catch (err) {
1171
+ console.error('Failed to select provider:', err);
1172
+ }
1173
+ }
1174
+
1175
+ function openProviderModal() {
1176
+ const modal = document.getElementById('providerModal');
1177
+ if (modal) {
1178
+ if (appState.providersData) {
1179
+ updateProviderUI(appState.providersData);
1180
+ }
1181
+ modal.classList.add('open');
1182
+ synth.playClick();
1183
+ }
1184
+ }
1185
+
1186
+ function closeProviderModal() {
1187
+ const modal = document.getElementById('providerModal');
1188
+ if (modal) modal.classList.remove('open');
1189
+ }
1190
+
1191
+ // ============================================================================
1192
+ // INITIALIZATION & DOM BINDINGS
1193
+ // ============================================================================
556
1194
  async function initApp() {
557
1195
  setupEventListeners();
558
1196
  initOfficeCanvas();
@@ -560,9 +1198,11 @@ async function initApp() {
560
1198
 
561
1199
  // Instant initial fetch before SSE stream connects
562
1200
  try {
563
- const [cfgRes, stRes] = await Promise.all([
1201
+ const [cfgRes, stRes, provRes, tokRes] = await Promise.all([
564
1202
  fetch('/api/config').then(r => r.ok ? r.json() : null),
565
- fetch('/api/state').then(r => r.ok ? r.json() : null)
1203
+ fetch('/api/state').then(r => r.ok ? r.json() : null),
1204
+ fetch('/api/providers').then(r => r.ok ? r.json() : null),
1205
+ fetch('/api/token-telemetry').then(r => r.ok ? r.json() : null)
566
1206
  ]);
567
1207
  if (cfgRes) {
568
1208
  appState.config = cfgRes;
@@ -574,6 +1214,12 @@ async function initApp() {
574
1214
  appState.state = stRes;
575
1215
  renderState();
576
1216
  }
1217
+ if (provRes) {
1218
+ updateProviderUI(provRes);
1219
+ }
1220
+ if (tokRes) {
1221
+ renderTokenTelemetry(tokRes);
1222
+ }
577
1223
  await fetchReports();
578
1224
  } catch (e) {
579
1225
  // Handled by SSE
@@ -582,16 +1228,200 @@ async function initApp() {
582
1228
  connectSSE();
583
1229
  }
584
1230
 
1231
+ let selectedAutocompleteIndex = -1;
1232
+ let currentSuggestions = [];
1233
+
585
1234
  // Setup DOM Event Listeners
586
1235
  function setupEventListeners() {
1236
+ initTokenTelemetryModal();
1237
+
1238
+ // Provider Badge & Switcher Modal
1239
+ const providerBadge = document.getElementById('providerBadge');
1240
+ if (providerBadge) {
1241
+ providerBadge.addEventListener('click', () => {
1242
+ openProviderModal();
1243
+ });
1244
+ }
1245
+
1246
+ const btnCloseProviderModal = document.getElementById('btnCloseProviderModal');
1247
+ if (btnCloseProviderModal) {
1248
+ btnCloseProviderModal.addEventListener('click', () => {
1249
+ closeProviderModal();
1250
+ });
1251
+ }
1252
+
1253
+ const providerModal = document.getElementById('providerModal');
1254
+ if (providerModal) {
1255
+ providerModal.addEventListener('click', (e) => {
1256
+ if (e.target === providerModal) closeProviderModal();
1257
+ });
1258
+ }
1259
+
1260
+ // Lo-Fi Music Synthesizer Controls
1261
+ const btnLofiToggle = document.getElementById('btnLofiToggle');
1262
+ if (btnLofiToggle) {
1263
+ btnLofiToggle.addEventListener('click', () => {
1264
+ synth.playClick();
1265
+ lofiEngine.toggle();
1266
+ });
1267
+ }
1268
+
1269
+ const lofiTrackSelect = document.getElementById('lofiTrackSelect');
1270
+ if (lofiTrackSelect) {
1271
+ lofiTrackSelect.addEventListener('change', (e) => {
1272
+ lofiEngine.setTrack(e.target.value);
1273
+ });
1274
+ }
1275
+
1276
+ const lofiVolSlider = document.getElementById('lofiVolSlider');
1277
+ if (lofiVolSlider) {
1278
+ lofiVolSlider.addEventListener('input', (e) => {
1279
+ lofiEngine.setVolume(parseFloat(e.target.value));
1280
+ });
1281
+ }
1282
+
1283
+ // Stream Tab Switcher (#AI-SIDECHAT vs #TEAM-SLACK)
1284
+ const tabSidechat = document.getElementById('tabSidechat');
1285
+ const tabSlack = document.getElementById('tabSlack');
1286
+ const sidechatStream = document.getElementById('sidechatStream');
1287
+ const terminalLogs = document.getElementById('terminalLogs');
1288
+
1289
+ if (tabSidechat && tabSlack) {
1290
+ tabSidechat.addEventListener('click', () => {
1291
+ synth.playClick();
1292
+ tabSidechat.classList.add('active');
1293
+ tabSlack.classList.remove('active');
1294
+ if (sidechatStream) sidechatStream.style.display = 'flex';
1295
+ if (terminalLogs) terminalLogs.style.display = 'none';
1296
+ appState.streamMode = 'sidechat';
1297
+ });
1298
+
1299
+ tabSlack.addEventListener('click', () => {
1300
+ synth.playClick();
1301
+ tabSlack.classList.add('active');
1302
+ tabSidechat.classList.remove('active');
1303
+ if (sidechatStream) sidechatStream.style.display = 'none';
1304
+ if (terminalLogs) terminalLogs.style.display = 'flex';
1305
+ appState.streamMode = 'slack';
1306
+ });
1307
+ }
1308
+
1309
+ // Auto Scroll Toggle Button
1310
+ const btnAutoScroll = document.getElementById('btnAutoScroll');
1311
+ if (btnAutoScroll) {
1312
+ btnAutoScroll.addEventListener('click', () => {
1313
+ synth.playClick();
1314
+ appState.autoScrollEnabled = !appState.autoScrollEnabled;
1315
+ btnAutoScroll.classList.toggle('active', appState.autoScrollEnabled);
1316
+ btnAutoScroll.textContent = appState.autoScrollEnabled ? 'AUTO-SCROLL ON' : 'AUTO-SCROLL OFF';
1317
+ });
1318
+ }
1319
+
1320
+ // Clear Sidechat / Clear Logs
1321
+ const btnClearSidechat = document.getElementById('btnClearSidechat');
1322
+ if (btnClearSidechat) {
1323
+ btnClearSidechat.addEventListener('click', () => {
1324
+ synth.playClick();
1325
+ if (sidechatStream) sidechatStream.innerHTML = '';
1326
+ if (terminalLogs) terminalLogs.innerHTML = '';
1327
+ });
1328
+ }
1329
+
587
1330
  // Task form submission
588
1331
  const taskForm = document.getElementById('taskForm');
589
1332
  const taskInput = document.getElementById('taskInput');
1333
+ const autocompletePopup = document.getElementById('commandAutocomplete');
1334
+ const autocompleteList = document.getElementById('autocompleteList');
1335
+
1336
+ // Handle Autocomplete popup
1337
+ async function updateAutocomplete() {
1338
+ const val = taskInput.value.trim();
1339
+ if (!val.startsWith('/')) {
1340
+ autocompletePopup.style.display = 'none';
1341
+ selectedAutocompleteIndex = -1;
1342
+ return;
1343
+ }
1344
+
1345
+ try {
1346
+ const res = await fetch(`/api/commands/autocomplete?q=${encodeURIComponent(val)}`);
1347
+ if (res.ok) {
1348
+ const data = await res.json();
1349
+ currentSuggestions = data.suggestions || [];
1350
+ if (currentSuggestions.length > 0) {
1351
+ renderAutocomplete(currentSuggestions);
1352
+ autocompletePopup.style.display = 'block';
1353
+ } else {
1354
+ autocompletePopup.style.display = 'none';
1355
+ }
1356
+ }
1357
+ } catch {
1358
+ autocompletePopup.style.display = 'none';
1359
+ }
1360
+ }
1361
+
1362
+ function renderAutocomplete(suggestions) {
1363
+ autocompleteList.innerHTML = suggestions.map((s, idx) => `
1364
+ <div class="autocomplete-item ${idx === selectedAutocompleteIndex ? 'selected' : ''}" data-index="${idx}">
1365
+ <span class="autocomplete-name">${escapeHtml(s.name)}</span>
1366
+ <span class="autocomplete-desc">${escapeHtml(s.description)}</span>
1367
+ <span class="autocomplete-usage">${escapeHtml(s.usage)}</span>
1368
+ </div>
1369
+ `).join('');
1370
+
1371
+ autocompleteList.querySelectorAll('.autocomplete-item').forEach(item => {
1372
+ item.addEventListener('click', () => {
1373
+ const idx = parseInt(item.getAttribute('data-index'), 10);
1374
+ selectSuggestion(idx);
1375
+ });
1376
+ });
1377
+ }
1378
+
1379
+ function selectSuggestion(idx) {
1380
+ if (idx >= 0 && idx < currentSuggestions.length) {
1381
+ const s = currentSuggestions[idx];
1382
+ taskInput.value = `${s.name} `;
1383
+ autocompletePopup.style.display = 'none';
1384
+ taskInput.focus();
1385
+ }
1386
+ }
1387
+
1388
+ taskInput.addEventListener('input', () => {
1389
+ selectedAutocompleteIndex = 0;
1390
+ updateAutocomplete();
1391
+ });
1392
+
1393
+ taskInput.addEventListener('keydown', (e) => {
1394
+ if (autocompletePopup.style.display === 'block' && currentSuggestions.length > 0) {
1395
+ if (e.key === 'ArrowDown') {
1396
+ e.preventDefault();
1397
+ selectedAutocompleteIndex = (selectedAutocompleteIndex + 1) % currentSuggestions.length;
1398
+ renderAutocomplete(currentSuggestions);
1399
+ } else if (e.key === 'ArrowUp') {
1400
+ e.preventDefault();
1401
+ selectedAutocompleteIndex = (selectedAutocompleteIndex - 1 + currentSuggestions.length) % currentSuggestions.length;
1402
+ renderAutocomplete(currentSuggestions);
1403
+ } else if (e.key === 'Tab' || (e.key === 'Enter' && selectedAutocompleteIndex >= 0 && taskInput.value.trim().split(' ').length === 1)) {
1404
+ e.preventDefault();
1405
+ selectSuggestion(selectedAutocompleteIndex);
1406
+ } else if (e.key === 'Escape') {
1407
+ autocompletePopup.style.display = 'none';
1408
+ }
1409
+ }
1410
+ });
1411
+
1412
+ document.addEventListener('click', (e) => {
1413
+ if (!taskInput.contains(e.target) && !autocompletePopup.contains(e.target)) {
1414
+ autocompletePopup.style.display = 'none';
1415
+ }
1416
+ });
1417
+
590
1418
  taskForm.addEventListener('submit', async (e) => {
591
1419
  e.preventDefault();
1420
+ autocompletePopup.style.display = 'none';
592
1421
  const prompt = taskInput.value.trim();
593
1422
  if (!prompt) return;
594
1423
  synth.playClick();
1424
+ updateActiveTaskHud(prompt, 'RUNNING');
595
1425
  await submitTask(prompt);
596
1426
  taskInput.value = '';
597
1427
  });
@@ -601,6 +1431,7 @@ function setupEventListeners() {
601
1431
  btn.addEventListener('click', async () => {
602
1432
  synth.playClick();
603
1433
  const prompt = btn.getAttribute('data-prompt');
1434
+ updateActiveTaskHud(prompt, 'RUNNING');
604
1435
  await submitTask(prompt);
605
1436
  });
606
1437
  });
@@ -608,11 +1439,13 @@ function setupEventListeners() {
608
1439
  // Header buttons
609
1440
  document.getElementById('btnDemo').addEventListener('click', async () => {
610
1441
  synth.playClick();
1442
+ updateActiveTaskHud('Running Full PixelCrew Multi-Agent Sprint Demo', 'SPRINT');
611
1443
  await fetch('/api/demo', { method: 'POST' });
612
1444
  });
613
1445
 
614
1446
  document.getElementById('btnReset').addEventListener('click', async () => {
615
1447
  synth.playClick();
1448
+ updateActiveTaskHud('Standing by for sprint assignment', null);
616
1449
  await fetch('/api/reset', { method: 'POST' });
617
1450
  });
618
1451
 
@@ -638,6 +1471,8 @@ function setupEventListeners() {
638
1471
  document.getElementById('btnClearLogs').addEventListener('click', () => {
639
1472
  synth.playClick();
640
1473
  document.getElementById('terminalLogs').innerHTML = '';
1474
+ const sidechat = document.getElementById('sidechatStream');
1475
+ if (sidechat) sidechat.innerHTML = '';
641
1476
  });
642
1477
 
643
1478
  // Log filter chips
@@ -1113,16 +1948,24 @@ function escapeHtml(str) {
1113
1948
  .replace(/'/g, '&#039;');
1114
1949
  }
1115
1950
 
1116
- // Submit Task to Server
1951
+ // Submit Task or Slash Command to Server
1117
1952
  async function submitTask(prompt) {
1118
1953
  try {
1119
- await fetch('/api/task', {
1120
- method: 'POST',
1121
- headers: { 'Content-Type': 'application/json' },
1122
- body: JSON.stringify({ prompt })
1123
- });
1954
+ if (prompt.startsWith('/')) {
1955
+ await fetch('/api/command', {
1956
+ method: 'POST',
1957
+ headers: { 'Content-Type': 'application/json' },
1958
+ body: JSON.stringify({ input: prompt })
1959
+ });
1960
+ } else {
1961
+ await fetch('/api/task', {
1962
+ method: 'POST',
1963
+ headers: { 'Content-Type': 'application/json' },
1964
+ body: JSON.stringify({ prompt })
1965
+ });
1966
+ }
1124
1967
  } catch (err) {
1125
- console.error('Failed to submit task:', err);
1968
+ console.error('Failed to submit task or command:', err);
1126
1969
  }
1127
1970
  }
1128
1971
 
@@ -1253,17 +2096,30 @@ function connectSSE() {
1253
2096
  appState.state = data.state;
1254
2097
  appState.events = data.history || [];
1255
2098
 
2099
+ if (data.providers) {
2100
+ updateProviderUI(data.providers);
2101
+ } else if (data.state?.activeProvider) {
2102
+ updateProviderUI({
2103
+ activeProvider: data.state.activeProvider,
2104
+ activeProviderName: data.state.activeProviderName,
2105
+ activeProviderIcon: data.state.activeProviderIcon,
2106
+ available: [{ id: data.state.activeProvider, name: data.state.activeProviderName, icon: data.state.activeProviderIcon }]
2107
+ });
2108
+ }
2109
+
1256
2110
  renderProjectInfo();
1257
2111
  renderAgentGrid();
1258
2112
  renderSkillsList();
1259
2113
  renderState();
1260
2114
  renderLogs(appState.events);
2115
+ renderSidechat(appState.events);
1261
2116
  });
1262
2117
 
1263
2118
  eventSource.addEventListener('agent_event', (e) => {
1264
2119
  const event = JSON.parse(e.data);
1265
2120
  appState.events.push(event);
1266
2121
  appendLog(event);
2122
+ appendSidechatCard(event);
1267
2123
 
1268
2124
  // Real-time visual state & sprite animation update
1269
2125
  if (appState.state && event.agent) {
@@ -1349,6 +2205,11 @@ function connectSSE() {
1349
2205
  renderOneShotResults(event.metadata);
1350
2206
  }
1351
2207
 
2208
+ // Real-time Token Telemetry Update
2209
+ if (event.type === 'token_telemetry' || event.tokenStats || event.metadata?.tokenStats) {
2210
+ renderTokenTelemetry(event.tokenStats || event.metadata?.tokenStats);
2211
+ }
2212
+
1352
2213
  // Audio cues & Report Refresh
1353
2214
  if (event.type === 'spawn') {
1354
2215
  synth.playSpawn();
@@ -1644,5 +2505,286 @@ function openAgentModal(agentKey) {
1644
2505
  synth.playClick();
1645
2506
  }
1646
2507
 
2508
+ // ============================================================================
2509
+ // REAL-TIME TOKEN TELEMETRY & VISUALIZER ENGINE
2510
+ // ============================================================================
2511
+ const tokenState = {
2512
+ rawTokensEstimated: 0,
2513
+ actualTokensUsed: 0,
2514
+ tokensSaved: 0,
2515
+ promptTokens: 0,
2516
+ completionTokens: 0,
2517
+ cachedTokens: 0,
2518
+ efficiencyRatio: 72,
2519
+ costUsd: 0,
2520
+ perAgent: {
2521
+ creativeDirector: 0,
2522
+ uxPlanner: 0,
2523
+ designSystem: 0,
2524
+ frontend: 0,
2525
+ backend: 0,
2526
+ database: 0,
2527
+ performance: 0,
2528
+ security: 0,
2529
+ qa: 0
2530
+ },
2531
+ timeline: []
2532
+ };
2533
+
2534
+ function initTokenTelemetryModal() {
2535
+ const btnOpen = document.getElementById('btnTokenAnalyticsModal');
2536
+ const btnClose = document.getElementById('btnCloseTokenModal');
2537
+ const modal = document.getElementById('tokenAnalyticsModal');
2538
+
2539
+ if (btnOpen && modal) {
2540
+ btnOpen.addEventListener('click', () => {
2541
+ modal.classList.add('open');
2542
+ renderTokenTelemetry(tokenState);
2543
+ synth.playClick();
2544
+ });
2545
+ }
2546
+
2547
+ if (btnClose && modal) {
2548
+ btnClose.addEventListener('click', () => {
2549
+ modal.classList.remove('open');
2550
+ synth.playClick();
2551
+ });
2552
+ }
2553
+
2554
+ if (modal) {
2555
+ modal.addEventListener('click', (e) => {
2556
+ if (e.target === modal) {
2557
+ modal.classList.remove('open');
2558
+ }
2559
+ });
2560
+ }
2561
+ }
2562
+
2563
+ async function fetchTokenTelemetry() {
2564
+ try {
2565
+ const res = await fetch('/api/token-telemetry');
2566
+ if (res.ok) {
2567
+ const data = await res.json();
2568
+ renderTokenTelemetry(data);
2569
+ }
2570
+ } catch (err) {
2571
+ console.warn('Failed to fetch initial token telemetry:', err);
2572
+ }
2573
+ }
2574
+
2575
+ function renderTokenTelemetry(stats) {
2576
+ if (!stats) return;
2577
+ Object.assign(tokenState, stats);
2578
+
2579
+ const used = tokenState.actualTokensUsed || 0;
2580
+ const raw = tokenState.rawTokensEstimated || 0;
2581
+ const saved = tokenState.tokensSaved || 0;
2582
+ const ratio = tokenState.efficiencyRatio || 72;
2583
+ const cost = tokenState.costUsd || ((used / 1000000) * 2.4);
2584
+ const costSaved = ((saved / 1000000) * 2.4);
2585
+
2586
+ // 1. Header & HUD Badges
2587
+ const headerVal = document.getElementById('headerTokenVal');
2588
+ if (headerVal) {
2589
+ headerVal.textContent = used >= 1000 ? `${(used / 1000).toFixed(1)}k tok` : `${used} tok`;
2590
+ }
2591
+ const headerPill = document.getElementById('headerSavedPill');
2592
+ if (headerPill) {
2593
+ headerPill.textContent = `${ratio}% SAVED`;
2594
+ }
2595
+ const hudTokenVal = document.getElementById('hudTokenVal');
2596
+ if (hudTokenVal) {
2597
+ hudTokenVal.textContent = `${used.toLocaleString()} tokens`;
2598
+ }
2599
+ const hudCostVal = document.getElementById('hudCostVal');
2600
+ if (hudCostVal) {
2601
+ hudCostVal.textContent = `($${cost.toFixed(4)})`;
2602
+ }
2603
+
2604
+ // 2. OneShot Modal Token Card
2605
+ const rawEl = document.getElementById('rawTokenVal');
2606
+ const actualEl = document.getElementById('actualTokenVal');
2607
+ const savedEl = document.getElementById('savedTokenVal');
2608
+ if (rawEl && raw > 0) rawEl.textContent = raw.toLocaleString();
2609
+ if (actualEl && used > 0) actualEl.textContent = used.toLocaleString();
2610
+ if (savedEl && saved > 0) savedEl.textContent = `${saved.toLocaleString()} (${ratio}% Saved)`;
2611
+
2612
+ // 3. Analytics Modal KPI Cards
2613
+ const modalActual = document.getElementById('modalActualTokens');
2614
+ if (modalActual) modalActual.textContent = used.toLocaleString();
2615
+
2616
+ const modalSaved = document.getElementById('modalSavedTokens');
2617
+ if (modalSaved) modalSaved.textContent = `${saved.toLocaleString()} (${ratio}%)`;
2618
+
2619
+ const modalRaw = document.getElementById('modalRawTokens');
2620
+ if (modalRaw) modalRaw.textContent = `Raw Est: ${raw.toLocaleString()}`;
2621
+
2622
+ const modalCost = document.getElementById('modalEstCost');
2623
+ if (modalCost) modalCost.textContent = `$${cost.toFixed(4)}`;
2624
+
2625
+ const modalCostSaved = document.getElementById('modalCostSaved');
2626
+ if (modalCostSaved) modalCostSaved.textContent = `Saved: $${costSaved.toFixed(4)}`;
2627
+
2628
+ const modalRatio = document.getElementById('modalPromptOutputRatio');
2629
+ if (modalRatio) {
2630
+ const p = tokenState.promptTokens || 0;
2631
+ const c = tokenState.completionTokens || 0;
2632
+ modalRatio.textContent = `${p.toLocaleString()} / ${c.toLocaleString()}`;
2633
+ }
2634
+
2635
+ const modalCached = document.getElementById('modalCachedTokens');
2636
+ if (modalCached) {
2637
+ modalCached.textContent = `Cached: ${(tokenState.cachedTokens || 0).toLocaleString()}`;
2638
+ }
2639
+
2640
+ // 4. Per-Agent Token Distribution Bars
2641
+ const agentBarsContainer = document.getElementById('agentTokenBars');
2642
+ if (agentBarsContainer && tokenState.perAgent) {
2643
+ const agents = [
2644
+ { key: 'frontend', name: 'Frontend Eng', icon: '💻', class: 'fe' },
2645
+ { key: 'backend', name: 'Backend Eng', icon: '⚙️', class: 'be' },
2646
+ { key: 'qa', name: 'QA Critic', icon: '🔍', class: 'qa' },
2647
+ { key: 'creativeDirector', name: 'Creative Dir', icon: '🎨', class: 'cd' },
2648
+ { key: 'uxPlanner', name: 'UX Planner', icon: '📐', class: 'ux' },
2649
+ { key: 'designSystem', name: 'Design System', icon: '🌿', class: 'ds' },
2650
+ { key: 'database', name: 'DB Architect', icon: '🗄️', class: 'db' },
2651
+ { key: 'performance', name: 'Perf SRE', icon: '⚡', class: 'perf' },
2652
+ { key: 'security', name: 'Security Sentinel', icon: '🛡️', class: 'sec' }
2653
+ ];
2654
+
2655
+ const maxAgentTokens = Math.max(...Object.values(tokenState.perAgent), 1);
2656
+
2657
+ agentBarsContainer.innerHTML = agents.map(ag => {
2658
+ const agTokens = tokenState.perAgent[ag.key] || 0;
2659
+ const pct = Math.round((agTokens / maxAgentTokens) * 100);
2660
+ const share = used > 0 ? Math.round((agTokens / used) * 100) : 0;
2661
+ return `
2662
+ <div class="agent-token-bar-row">
2663
+ <div class="agent-bar-meta">
2664
+ <span>${ag.icon}</span>
2665
+ <span>${ag.name}</span>
2666
+ </div>
2667
+ <div class="agent-bar-track">
2668
+ <div class="agent-bar-fill ${ag.class}" style="width: ${pct}%;"></div>
2669
+ </div>
2670
+ <div class="agent-bar-stats">
2671
+ <strong>${agTokens.toLocaleString()}</strong> <span style="color: var(--color-cyan);">(${share}%)</span>
2672
+ </div>
2673
+ </div>
2674
+ `;
2675
+ }).join('');
2676
+ }
2677
+
2678
+ // 5. Render Real-Time Sparkline Canvas
2679
+ drawTokenSparkline(tokenState.timeline || []);
2680
+ }
2681
+
2682
+ function drawTokenSparkline(timeline) {
2683
+ const canvas = document.getElementById('tokenSparklineCanvas');
2684
+ if (!canvas) return;
2685
+ const ctx = canvas.getContext('2d');
2686
+ if (!ctx) return;
2687
+
2688
+ const w = canvas.width;
2689
+ const h = canvas.height;
2690
+
2691
+ // Clear background
2692
+ ctx.fillStyle = '#04070f';
2693
+ ctx.fillRect(0, 0, w, h);
2694
+
2695
+ // Draw retro gridlines
2696
+ ctx.strokeStyle = '#0e1626';
2697
+ ctx.lineWidth = 1;
2698
+ for (let x = 0; x < w; x += 40) {
2699
+ ctx.beginPath();
2700
+ ctx.moveTo(x, 0);
2701
+ ctx.lineTo(x, h);
2702
+ ctx.stroke();
2703
+ }
2704
+ for (let y = 0; y < h; y += 25) {
2705
+ ctx.beginPath();
2706
+ ctx.moveTo(0, y);
2707
+ ctx.lineTo(w, y);
2708
+ ctx.stroke();
2709
+ }
2710
+
2711
+ if (!timeline || timeline.length === 0) {
2712
+ // Render idle pulse wave
2713
+ ctx.strokeStyle = '#00f0ff33';
2714
+ ctx.setLineDash([4, 4]);
2715
+ ctx.beginPath();
2716
+ ctx.moveTo(0, h / 2);
2717
+ ctx.lineTo(w, h / 2);
2718
+ ctx.stroke();
2719
+ ctx.setLineDash([]);
2720
+ ctx.fillStyle = '#64748b';
2721
+ ctx.font = '9px "Press Start 2P", monospace';
2722
+ ctx.textAlign = 'center';
2723
+ ctx.fillText('AWAITING ACTIVE SPRINT TOKEN EMISSIONS...', w / 2, h / 2 - 10);
2724
+ return;
2725
+ }
2726
+
2727
+ const padding = 20;
2728
+ const maxVal = Math.max(...timeline.map(t => t.totalTokens || 0), 1000);
2729
+ const minVal = 0;
2730
+
2731
+ const getX = (idx) => padding + (idx / Math.max(timeline.length - 1, 1)) * (w - padding * 2);
2732
+ const getY = (val) => (h - padding) - ((val - minVal) / (maxVal - minVal)) * (h - padding * 2);
2733
+
2734
+ // Gradient fill under curve
2735
+ const gradient = ctx.createLinearGradient(0, 0, 0, h);
2736
+ gradient.addColorStop(0, 'rgba(0, 240, 255, 0.3)');
2737
+ gradient.addColorStop(1, 'rgba(0, 255, 136, 0.02)');
2738
+
2739
+ ctx.beginPath();
2740
+ ctx.moveTo(getX(0), h - padding);
2741
+ for (let i = 0; i < timeline.length; i++) {
2742
+ ctx.lineTo(getX(i), getY(timeline[i].totalTokens || 0));
2743
+ }
2744
+ ctx.lineTo(getX(timeline.length - 1), h - padding);
2745
+ ctx.closePath();
2746
+ ctx.fillStyle = gradient;
2747
+ ctx.fill();
2748
+
2749
+ // Glow line
2750
+ ctx.shadowColor = '#00f0ff';
2751
+ ctx.shadowBlur = 8;
2752
+ ctx.strokeStyle = '#00f0ff';
2753
+ ctx.lineWidth = 2;
2754
+
2755
+ ctx.beginPath();
2756
+ for (let i = 0; i < timeline.length; i++) {
2757
+ const x = getX(i);
2758
+ const y = getY(timeline[i].totalTokens || 0);
2759
+ if (i === 0) ctx.moveTo(x, y);
2760
+ else ctx.lineTo(x, y);
2761
+ }
2762
+ ctx.stroke();
2763
+ ctx.shadowBlur = 0;
2764
+
2765
+ // Draw node points & step labels
2766
+ for (let i = 0; i < timeline.length; i++) {
2767
+ const pt = timeline[i];
2768
+ const x = getX(i);
2769
+ const y = getY(pt.totalTokens || 0);
2770
+
2771
+ ctx.fillStyle = '#00ff88';
2772
+ ctx.beginPath();
2773
+ ctx.arc(x, y, 4, 0, Math.PI * 2);
2774
+ ctx.fill();
2775
+ ctx.strokeStyle = '#000000';
2776
+ ctx.lineWidth = 1.5;
2777
+ ctx.stroke();
2778
+
2779
+ // Node label
2780
+ if (i === timeline.length - 1 || i % 2 === 0) {
2781
+ ctx.fillStyle = '#ffffff';
2782
+ ctx.font = '8px "Press Start 2P", monospace';
2783
+ ctx.textAlign = 'center';
2784
+ ctx.fillText(`${(pt.totalTokens || 0).toLocaleString()}`, x, y - 8);
2785
+ }
2786
+ }
2787
+ }
2788
+
1647
2789
  // Bootstrap
1648
2790
  window.addEventListener('DOMContentLoaded', initApp);