@clawlabz/clawskin 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +98 -0
- package/CONTRIBUTING.md +40 -0
- package/LICENSE +21 -0
- package/README.md +137 -0
- package/docs/ARCHITECTURE.md +132 -0
- package/docs/PROGRESS.md +15 -0
- package/docs/ROADMAP.md +130 -0
- package/package.json +27 -0
- package/public/_headers +13 -0
- package/public/_redirects +1 -0
- package/public/app.html +595 -0
- package/public/css/app.css +379 -0
- package/public/css/style.css +415 -0
- package/public/index.html +324 -0
- package/public/js/app/AgentSlot.js +596 -0
- package/public/js/app/AgentStateMapper.js +213 -0
- package/public/js/app/ClawSkinApp.js +551 -0
- package/public/js/app/ConnectionPanel.js +142 -0
- package/public/js/app/DeviceIdentity.js +118 -0
- package/public/js/app/GatewayClient.js +284 -0
- package/public/js/app/SettingsManager.js +47 -0
- package/public/js/character/AnimationManager.js +69 -0
- package/public/js/character/BubbleManager.js +157 -0
- package/public/js/character/CharacterSprite.js +98 -0
- package/public/js/game.js +116 -0
- package/public/js/pets/Pet.js +303 -0
- package/public/js/pets/PetManager.js +102 -0
- package/public/js/scenes/CafeScene.js +594 -0
- package/public/js/scenes/HackerScene.js +527 -0
- package/public/js/scenes/OfficeScene.js +404 -0
- package/public/js/sprites/SpriteGenerator.js +927 -0
- package/public/js/state/AgentStateSync.js +89 -0
- package/public/js/state/DemoMode.js +65 -0
- package/public/js/ui/CharacterEditor.js +145 -0
- package/public/js/ui/ScenePicker.js +29 -0
- package/serve.cjs +132 -0
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClawSkinApp.js — Main application controller for ClawSkin product mode
|
|
3
|
+
* Supports multiple agents displayed simultaneously in one scene
|
|
4
|
+
*/
|
|
5
|
+
class ClawSkinApp {
|
|
6
|
+
constructor(canvasId, options = {}) {
|
|
7
|
+
this.canvas = document.getElementById(canvasId);
|
|
8
|
+
this.ctx = this.canvas.getContext('2d');
|
|
9
|
+
this.width = options.width || 640;
|
|
10
|
+
this.height = options.height || 400;
|
|
11
|
+
this.canvas.width = this.width;
|
|
12
|
+
this.canvas.height = this.height;
|
|
13
|
+
|
|
14
|
+
this.settings = new SettingsManager();
|
|
15
|
+
this.gateway = null;
|
|
16
|
+
this.demoMode = null;
|
|
17
|
+
this.spriteGen = new SpriteGenerator();
|
|
18
|
+
this.scenes = [];
|
|
19
|
+
this.currentScene = null;
|
|
20
|
+
this.running = false;
|
|
21
|
+
this.lastTime = 0;
|
|
22
|
+
this.mode = 'demo'; // 'demo' | 'live'
|
|
23
|
+
|
|
24
|
+
// Multi-agent support
|
|
25
|
+
this.agents = []; // AgentSlot[]
|
|
26
|
+
this.demoCharacter = null; // Single character for demo mode
|
|
27
|
+
this.petManager = null; // Independent pet system
|
|
28
|
+
|
|
29
|
+
// UI elements
|
|
30
|
+
this.statusEl = options.statusEl ? document.getElementById(options.statusEl) : null;
|
|
31
|
+
this.modeEl = options.modeEl ? document.getElementById(options.modeEl) : null;
|
|
32
|
+
this.agentListEl = options.agentListEl ? document.getElementById(options.agentListEl) : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
init() {
|
|
36
|
+
const config = this.settings.load();
|
|
37
|
+
const charConfig = config.character || CharacterEditor.loadSaved() || CharacterSprite.defaultConfig();
|
|
38
|
+
|
|
39
|
+
// Scenes
|
|
40
|
+
this.scenes = [
|
|
41
|
+
new OfficeScene(this.canvas, this.ctx, this.spriteGen),
|
|
42
|
+
new HackerScene(this.canvas, this.ctx, this.spriteGen),
|
|
43
|
+
new CafeScene(this.canvas, this.ctx, this.spriteGen),
|
|
44
|
+
];
|
|
45
|
+
this.scenes.forEach(s => s.init());
|
|
46
|
+
|
|
47
|
+
const sceneIdx = config.scene || 0;
|
|
48
|
+
this.currentScene = this.scenes[sceneIdx] || this.scenes[0];
|
|
49
|
+
|
|
50
|
+
// Demo character (used when not connected)
|
|
51
|
+
const pos = this.currentScene.getCharacterPosition();
|
|
52
|
+
this.demoCharacter = new CharacterSprite(charConfig, pos.x, pos.y);
|
|
53
|
+
this.demoMode = new DemoMode(this.demoCharacter);
|
|
54
|
+
|
|
55
|
+
// Gateway client
|
|
56
|
+
this.gateway = new GatewayClient({
|
|
57
|
+
autoReconnect: true,
|
|
58
|
+
onConnected: (hello) => this._onGatewayConnected(hello),
|
|
59
|
+
onDisconnected: (info) => this._onGatewayDisconnected(info),
|
|
60
|
+
onEvent: (event) => this._onGatewayEvent(event),
|
|
61
|
+
onStateChange: (state, detail) => this._onConnectionStateChange(state, detail),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Independent pet system
|
|
65
|
+
this.petManager = new PetManager(this.spriteGen);
|
|
66
|
+
this.petManager.setSceneBounds(this.width, this.height);
|
|
67
|
+
this.petManager.initDefaults();
|
|
68
|
+
|
|
69
|
+
// Click-to-customize: click an agent to open their character editor
|
|
70
|
+
this.canvas.addEventListener('click', (e) => this._handleCanvasClick(e));
|
|
71
|
+
this.selectedAgent = null;
|
|
72
|
+
|
|
73
|
+
return this;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
_handleCanvasClick(e) {
|
|
77
|
+
if (this.mode !== 'live') return;
|
|
78
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
79
|
+
const scaleX = this.width / rect.width;
|
|
80
|
+
const scaleY = this.height / rect.height;
|
|
81
|
+
const clickX = (e.clientX - rect.left) * scaleX;
|
|
82
|
+
const clickY = (e.clientY - rect.top) * scaleY;
|
|
83
|
+
|
|
84
|
+
for (const slot of this.agents) {
|
|
85
|
+
if (slot.hitTest(clickX, clickY)) {
|
|
86
|
+
this.selectedAgent = slot;
|
|
87
|
+
// Update CharacterEditor to edit this agent
|
|
88
|
+
if (window._charEditor) {
|
|
89
|
+
window._charEditor.character = slot.character;
|
|
90
|
+
window._charEditor.onChange = (cfg) => {
|
|
91
|
+
slot.updateConfig(cfg);
|
|
92
|
+
};
|
|
93
|
+
// Show editor and scroll to it
|
|
94
|
+
const editorEl = document.getElementById('char-editor');
|
|
95
|
+
if (editorEl) {
|
|
96
|
+
editorEl.style.display = 'block';
|
|
97
|
+
window._charEditor.visible = true;
|
|
98
|
+
window._charEditor.render();
|
|
99
|
+
editorEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
100
|
+
}
|
|
101
|
+
// Update header to show which agent is being edited
|
|
102
|
+
const header = document.querySelector('.editor-header span, .editor-header');
|
|
103
|
+
if (header) {
|
|
104
|
+
header.textContent = '🎨 Editing: ' + slot.name;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Visual feedback — show a brief highlight
|
|
108
|
+
slot.character.showBubble('✨', 'speech', 1000);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
start() {
|
|
115
|
+
this.running = true;
|
|
116
|
+
this.lastTime = performance.now();
|
|
117
|
+
|
|
118
|
+
const config = this.settings.load();
|
|
119
|
+
|
|
120
|
+
if (config.autoConnect && config.gatewayUrl) {
|
|
121
|
+
this.connectToGateway(config.gatewayUrl, config.token);
|
|
122
|
+
} else {
|
|
123
|
+
this._startDemoMode();
|
|
124
|
+
this._tryAutoDetect();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
this._loop();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ──── Gateway Connection ────
|
|
131
|
+
|
|
132
|
+
connectToGateway(url, token) {
|
|
133
|
+
this.settings.update({
|
|
134
|
+
gatewayUrl: url,
|
|
135
|
+
token: token || '',
|
|
136
|
+
lastConnected: Date.now(),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
this.gateway.connect(url, token);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
disconnectGateway() {
|
|
143
|
+
this.gateway.disconnect(true);
|
|
144
|
+
this._clearAgents();
|
|
145
|
+
this._startDemoMode();
|
|
146
|
+
this.settings.update({ autoConnect: false });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async _tryAutoDetect() {
|
|
150
|
+
// On remote deployments (not localhost/file), skip local probing entirely.
|
|
151
|
+
// Users on remote hosts should connect via the UI panel.
|
|
152
|
+
const origin = window.location.origin || '';
|
|
153
|
+
const isLocal = !origin || origin.includes('localhost') || origin.includes('127.0.0.1') || origin.startsWith('file:');
|
|
154
|
+
|
|
155
|
+
if (!isLocal) {
|
|
156
|
+
// Remote deployment — show connection panel immediately
|
|
157
|
+
if (window._connPanel) {
|
|
158
|
+
window._connPanel.expanded = true;
|
|
159
|
+
window._connPanel.render();
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Local mode — try auto-detect
|
|
165
|
+
|
|
166
|
+
// Step 1: Try fetching local config from serve.cjs (/api/config)
|
|
167
|
+
try {
|
|
168
|
+
const res = await fetch('/api/config', { signal: AbortSignal.timeout(2000) });
|
|
169
|
+
if (res.ok) {
|
|
170
|
+
const config = await res.json();
|
|
171
|
+
if (config?.gatewayUrl) {
|
|
172
|
+
const saved = this.settings.load();
|
|
173
|
+
this.settings.update({
|
|
174
|
+
gatewayUrl: config.gatewayUrl,
|
|
175
|
+
autoConnect: true,
|
|
176
|
+
});
|
|
177
|
+
if (window._connPanel) {
|
|
178
|
+
window._connPanel.render(this.settings.load());
|
|
179
|
+
}
|
|
180
|
+
// Use previously saved token if available; never read token from /api/config
|
|
181
|
+
this.gateway.connect(config.gatewayUrl, saved.token || '');
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
} catch {}
|
|
186
|
+
|
|
187
|
+
// Step 2: Fallback — probe ws://localhost:18789 directly
|
|
188
|
+
const localUrl = 'ws://localhost:18789';
|
|
189
|
+
try {
|
|
190
|
+
const ws = new WebSocket(localUrl);
|
|
191
|
+
const timeout = setTimeout(() => { ws.close(); }, 3000);
|
|
192
|
+
ws.onopen = () => {
|
|
193
|
+
clearTimeout(timeout);
|
|
194
|
+
ws.close();
|
|
195
|
+
this.settings.update({ gatewayUrl: localUrl });
|
|
196
|
+
this.gateway.connect(localUrl, '');
|
|
197
|
+
};
|
|
198
|
+
ws.onerror = () => {
|
|
199
|
+
clearTimeout(timeout);
|
|
200
|
+
if (window._connPanel) {
|
|
201
|
+
window._connPanel.expanded = true;
|
|
202
|
+
window._connPanel.render();
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
} catch {}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async _onGatewayConnected(hello) {
|
|
209
|
+
this.mode = 'live';
|
|
210
|
+
this.demoMode.stop();
|
|
211
|
+
this._updateModeDisplay();
|
|
212
|
+
|
|
213
|
+
// Discover all active sessions from Gateway
|
|
214
|
+
await this._discoverAgents();
|
|
215
|
+
this._renderAgentList();
|
|
216
|
+
this._updateStatusDisplay();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
_onGatewayDisconnected(info) {
|
|
220
|
+
this.mode = 'demo';
|
|
221
|
+
this._clearAgents();
|
|
222
|
+
this._startDemoMode();
|
|
223
|
+
this._updateModeDisplay();
|
|
224
|
+
this._renderAgentList();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
_onGatewayEvent(event) {
|
|
228
|
+
if (!event || !event.event) return;
|
|
229
|
+
|
|
230
|
+
// Route events to matching agent slots
|
|
231
|
+
const payload = event.payload;
|
|
232
|
+
const sessionKey = payload?.sessionKey;
|
|
233
|
+
|
|
234
|
+
if (sessionKey) {
|
|
235
|
+
// Parse agentId from sessionKey (e.g. "agent:ifig:discord:..." → "ifig")
|
|
236
|
+
const match = sessionKey.match(/^agent:([^:]+):/);
|
|
237
|
+
const agentId = match ? match[1] : 'main';
|
|
238
|
+
|
|
239
|
+
// Route to matching agent
|
|
240
|
+
const slot = this.agents.find(a => a.agentId === agentId);
|
|
241
|
+
if (slot) {
|
|
242
|
+
slot.handleEvent(event);
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
// Broadcast to all agents
|
|
246
|
+
for (const slot of this.agents) {
|
|
247
|
+
slot.handleEvent(event);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
this._updateStatusDisplay();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
_onConnectionStateChange(state, detail) {
|
|
255
|
+
if (window._connPanel) {
|
|
256
|
+
window._connPanel.setState(state, detail);
|
|
257
|
+
if (state === 'error' && detail && (detail.includes('auth') || detail.includes('token') || detail.includes('pairing'))) {
|
|
258
|
+
window._connPanel.expanded = true;
|
|
259
|
+
window._connPanel.render();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
this._updateStatusDisplay();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ──── Multi-Agent Management ────
|
|
266
|
+
|
|
267
|
+
async _discoverAgents() {
|
|
268
|
+
try {
|
|
269
|
+
const result = await this.gateway.getSessionsList({ activeMinutes: 1440 });
|
|
270
|
+
const sessions = result?.sessions || result || [];
|
|
271
|
+
|
|
272
|
+
if (!Array.isArray(sessions) || sessions.length === 0) {
|
|
273
|
+
this._addAgent({ agentId: 'main', label: 'Main Agent', sessionKeys: ['main'] });
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Group sessions by agentId
|
|
278
|
+
// Session keys look like: "agent:main:main", "agent:ifig:discord:channel:123", "agent:xhs:main"
|
|
279
|
+
// The agentId is either session.agentId or extracted from the key pattern "agent:<agentId>:..."
|
|
280
|
+
const agentMap = new Map();
|
|
281
|
+
|
|
282
|
+
for (const session of sessions) {
|
|
283
|
+
const key = session.key || session.sessionKey;
|
|
284
|
+
if (!key) continue;
|
|
285
|
+
|
|
286
|
+
// Determine agentId: prefer session.agentId, else parse from key
|
|
287
|
+
let agentId = session.agentId || null;
|
|
288
|
+
if (!agentId) {
|
|
289
|
+
const match = key.match(/^agent:([^:]+):/);
|
|
290
|
+
agentId = match ? match[1] : 'main';
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (!agentMap.has(agentId)) {
|
|
294
|
+
agentMap.set(agentId, {
|
|
295
|
+
agentId,
|
|
296
|
+
label: session.label || agentId,
|
|
297
|
+
sessionKeys: [],
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
agentMap.get(agentId).sessionKeys.push(key);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Create one AgentSlot per unique agentId
|
|
304
|
+
for (const [agentId, info] of agentMap) {
|
|
305
|
+
this._addAgent({
|
|
306
|
+
agentId,
|
|
307
|
+
label: info.label,
|
|
308
|
+
sessionKeys: info.sessionKeys,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (this.agents.length === 0) {
|
|
313
|
+
this._addAgent({ agentId: 'main', label: 'Main Agent', sessionKeys: ['main'] });
|
|
314
|
+
}
|
|
315
|
+
} catch (e) {
|
|
316
|
+
console.warn('[ClawSkinApp] session discovery failed:', e);
|
|
317
|
+
this._addAgent({ agentId: 'main', label: 'Main Agent', sessionKeys: ['main'] });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Fetch identity (name) for each agent
|
|
321
|
+
for (const slot of this.agents) {
|
|
322
|
+
this._fetchAgentIdentity(slot);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
_addAgent(options) {
|
|
327
|
+
// Don't duplicate by agentId
|
|
328
|
+
if (this.agents.find(a => a.agentId === options.agentId)) return;
|
|
329
|
+
|
|
330
|
+
const index = this.agents.length;
|
|
331
|
+
|
|
332
|
+
const slot = new AgentSlot({
|
|
333
|
+
...options,
|
|
334
|
+
index,
|
|
335
|
+
x: 0,
|
|
336
|
+
y: 0,
|
|
337
|
+
showNameTag: true,
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
this.agents.push(slot);
|
|
341
|
+
|
|
342
|
+
// Give all agents cross-references for social wandering
|
|
343
|
+
for (const a of this.agents) {
|
|
344
|
+
a._otherAgents = this.agents;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
this._repositionAgents();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
_clearAgents() {
|
|
351
|
+
for (const slot of this.agents) slot.destroy();
|
|
352
|
+
this.agents = [];
|
|
353
|
+
this._renderAgentList();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
_repositionAgents() {
|
|
357
|
+
// Positions are now determined by workstations in _render()
|
|
358
|
+
// No manual positioning needed
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async _fetchAgentIdentity(slot) {
|
|
362
|
+
try {
|
|
363
|
+
const identity = await this.gateway.getAgentIdentity(slot.agentId);
|
|
364
|
+
if (identity?.name) {
|
|
365
|
+
slot.name = identity.name;
|
|
366
|
+
slot.agentId = identity.agentId || slot.agentId;
|
|
367
|
+
this._renderAgentList();
|
|
368
|
+
}
|
|
369
|
+
} catch {}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ──── Demo Mode ────
|
|
373
|
+
|
|
374
|
+
_startDemoMode() {
|
|
375
|
+
this.mode = 'demo';
|
|
376
|
+
const pos = this.currentScene.getCharacterPosition();
|
|
377
|
+
this.demoCharacter.x = pos.x;
|
|
378
|
+
this.demoCharacter.y = pos.y;
|
|
379
|
+
this.demoMode.start();
|
|
380
|
+
this._updateModeDisplay();
|
|
381
|
+
this._updateStatusDisplay();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ──── Scene Management ────
|
|
385
|
+
|
|
386
|
+
setScene(index) {
|
|
387
|
+
if (index < 0 || index >= this.scenes.length) return;
|
|
388
|
+
this.currentScene = this.scenes[index];
|
|
389
|
+
this.currentScene.init();
|
|
390
|
+
|
|
391
|
+
if (this.mode === 'demo') {
|
|
392
|
+
const pos = this.currentScene.getCharacterPosition();
|
|
393
|
+
this.demoCharacter.x = pos.x;
|
|
394
|
+
this.demoCharacter.y = pos.y;
|
|
395
|
+
} else {
|
|
396
|
+
this._repositionAgents();
|
|
397
|
+
}
|
|
398
|
+
this.settings.update({ scene: index });
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ──── Render Loop ────
|
|
402
|
+
|
|
403
|
+
_loop() {
|
|
404
|
+
if (!this.running) return;
|
|
405
|
+
const now = performance.now();
|
|
406
|
+
const dt = now - this.lastTime;
|
|
407
|
+
this.lastTime = now;
|
|
408
|
+
|
|
409
|
+
this._update(dt);
|
|
410
|
+
this._render();
|
|
411
|
+
requestAnimationFrame(() => this._loop());
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
_update(dt) {
|
|
415
|
+
if (this.currentScene) this.currentScene.update(dt);
|
|
416
|
+
|
|
417
|
+
if (this.mode === 'demo') {
|
|
418
|
+
this.demoMode.update(dt);
|
|
419
|
+
this.demoCharacter.update(dt);
|
|
420
|
+
} else {
|
|
421
|
+
for (const slot of this.agents) slot.update(dt);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Update independent pets
|
|
425
|
+
if (this.petManager) {
|
|
426
|
+
const obstacles = this.currentScene?.obstacles || [];
|
|
427
|
+
this.petManager.update(dt, this.agents, obstacles);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
_render() {
|
|
432
|
+
this.ctx.clearRect(0, 0, this.width, this.height);
|
|
433
|
+
this.ctx.imageSmoothingEnabled = false;
|
|
434
|
+
|
|
435
|
+
// Layer 0: Background wall + static decorations
|
|
436
|
+
if (this.currentScene) this.currentScene.render(this.ctx);
|
|
437
|
+
|
|
438
|
+
const scene = this.currentScene;
|
|
439
|
+
|
|
440
|
+
if (this.mode === 'demo') {
|
|
441
|
+
const stations = scene?.getWorkstations?.(1) || [];
|
|
442
|
+
const s = stations[0];
|
|
443
|
+
if (s) {
|
|
444
|
+
this.demoCharacter.x = s.charX;
|
|
445
|
+
this.demoCharacter.y = s.charY;
|
|
446
|
+
if (s.charScale) this.demoCharacter.scale = s.charScale;
|
|
447
|
+
|
|
448
|
+
// Back-to-front: chair → character → desk+monitor
|
|
449
|
+
scene.renderChair(this.ctx, s);
|
|
450
|
+
this.demoCharacter.render(this.ctx);
|
|
451
|
+
scene.renderDesk(this.ctx, s, this.demoCharacter.animManager?.currentState || 'idle');
|
|
452
|
+
} else {
|
|
453
|
+
this.demoCharacter.render(this.ctx);
|
|
454
|
+
}
|
|
455
|
+
} else {
|
|
456
|
+
// Live mode — 3-phase rendering with Y-sort depth ordering
|
|
457
|
+
const stations = scene?.getWorkstations?.(this.agents.length) || [];
|
|
458
|
+
|
|
459
|
+
// Assign stations to agents
|
|
460
|
+
for (let i = 0; i < this.agents.length; i++) {
|
|
461
|
+
const s = stations[i];
|
|
462
|
+
if (!s) continue;
|
|
463
|
+
const slot = this.agents[i];
|
|
464
|
+
slot.station = s;
|
|
465
|
+
slot._scene = scene;
|
|
466
|
+
if (s.charScale) slot.character.scale = s.charScale;
|
|
467
|
+
|
|
468
|
+
// If not wandering and not manually positioned, snap to station
|
|
469
|
+
if (!slot.isWandering && !slot._isReturning) {
|
|
470
|
+
slot.character.x = s.charX;
|
|
471
|
+
if (!slot._manualY) slot.character.y = s.charY;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Phase 1: Render ALL chairs (furniture is always visible)
|
|
476
|
+
for (let i = 0; i < stations.length; i++) {
|
|
477
|
+
scene.renderChair(this.ctx, stations[i]);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Phase 2: Render characters sorted by Y (back-to-front depth sort)
|
|
481
|
+
const sortedAgents = [...this.agents].sort((a, b) => a.character.y - b.character.y);
|
|
482
|
+
for (const slot of sortedAgents) {
|
|
483
|
+
slot.render(this.ctx);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Phase 3: Render ALL desks + laptops + cups (always visible, in front of characters)
|
|
487
|
+
for (let i = 0; i < stations.length; i++) {
|
|
488
|
+
const state = this.agents[i]?.stateMapper.currentState || 'idle';
|
|
489
|
+
scene.renderDesk(this.ctx, stations[i], state);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Phase 4: Render pets (independent of agents)
|
|
493
|
+
if (this.petManager) {
|
|
494
|
+
this.petManager.render(this.ctx);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Phase 5: Render ALL name tags on top of everything
|
|
498
|
+
for (const slot of this.agents) {
|
|
499
|
+
slot.renderNameTag(this.ctx);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// ──── UI Updates ────
|
|
505
|
+
|
|
506
|
+
_updateStatusDisplay() {
|
|
507
|
+
if (!this.statusEl) return;
|
|
508
|
+
if (this.mode === 'demo') {
|
|
509
|
+
this.statusEl.textContent = '🎮 Demo Mode';
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
const active = this.agents.filter(a =>
|
|
513
|
+
a.stateMapper.currentState !== 'idle' && a.stateMapper.currentState !== 'sleeping'
|
|
514
|
+
);
|
|
515
|
+
if (active.length > 0) {
|
|
516
|
+
this.statusEl.textContent = `⚡ ${active.length} active / ${this.agents.length} agents`;
|
|
517
|
+
} else {
|
|
518
|
+
this.statusEl.textContent = `💤 ${this.agents.length} agent${this.agents.length > 1 ? 's' : ''} idle`;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
_updateModeDisplay() {
|
|
523
|
+
if (!this.modeEl) return;
|
|
524
|
+
if (this.mode === 'live') {
|
|
525
|
+
this.modeEl.textContent = '🟢 Live';
|
|
526
|
+
this.modeEl.className = 'mode-badge mode-live';
|
|
527
|
+
} else {
|
|
528
|
+
this.modeEl.textContent = '🎮 Demo';
|
|
529
|
+
this.modeEl.className = 'mode-badge mode-demo';
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
_renderAgentList() {
|
|
534
|
+
if (!this.agentListEl) return;
|
|
535
|
+
if (this.agents.length === 0) {
|
|
536
|
+
this.agentListEl.innerHTML = '';
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const stateIcons = {
|
|
540
|
+
idle: '💤', thinking: '🤔', typing: '⌨️', executing: '⚙️',
|
|
541
|
+
browsing: '🔍', error: '❌', sleeping: '😴', waving: '👋'
|
|
542
|
+
};
|
|
543
|
+
const esc = ConnectionPanel.esc;
|
|
544
|
+
this.agentListEl.innerHTML = this.agents.map(a => {
|
|
545
|
+
const icon = stateIcons[a.stateMapper.currentState] || '💤';
|
|
546
|
+
return `<span class="agent-badge" title="${esc(a.sessionKey)}">${icon} ${esc(a.name)}</span>`;
|
|
547
|
+
}).join('');
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (typeof window !== 'undefined') window.ClawSkinApp = ClawSkinApp;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConnectionPanel.js — Gateway connection UI
|
|
3
|
+
* Renders URL/Token input, connect button, status indicator
|
|
4
|
+
*/
|
|
5
|
+
class ConnectionPanel {
|
|
6
|
+
/** Escape HTML to prevent XSS from untrusted Gateway data */
|
|
7
|
+
static esc(str) {
|
|
8
|
+
if (!str) return '';
|
|
9
|
+
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
constructor(containerId, options = {}) {
|
|
13
|
+
this.container = document.getElementById(containerId);
|
|
14
|
+
this.onConnect = options.onConnect || null;
|
|
15
|
+
this.onDisconnect = options.onDisconnect || null;
|
|
16
|
+
this.state = 'disconnected'; // disconnected|connecting|connected|error
|
|
17
|
+
this.errorMessage = null;
|
|
18
|
+
this.agentName = null;
|
|
19
|
+
this.agentId = null;
|
|
20
|
+
this.settings = options.settings || {};
|
|
21
|
+
this.expanded = !this.settings.gatewayUrl;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
render(settings) {
|
|
25
|
+
if (settings) this.settings = settings;
|
|
26
|
+
const s = this.settings;
|
|
27
|
+
const isConnected = this.state === 'connected';
|
|
28
|
+
const isConnecting = this.state === 'connecting';
|
|
29
|
+
const isError = this.state === 'error';
|
|
30
|
+
|
|
31
|
+
// Status dot color
|
|
32
|
+
const dotClass = isConnected ? 'dot-connected' :
|
|
33
|
+
isConnecting ? 'dot-connecting' :
|
|
34
|
+
isError ? 'dot-error' : 'dot-disconnected';
|
|
35
|
+
|
|
36
|
+
const esc = ConnectionPanel.esc;
|
|
37
|
+
const statusText = isConnected ? `Connected${this.agentName ? ' — ' + esc(this.agentName) : ''}` :
|
|
38
|
+
isConnecting ? 'Connecting...' :
|
|
39
|
+
isError ? esc(this.errorMessage || 'Connection failed') :
|
|
40
|
+
'Not connected';
|
|
41
|
+
|
|
42
|
+
this.container.innerHTML = `
|
|
43
|
+
<div class="conn-panel ${this.expanded ? 'expanded' : 'collapsed'}">
|
|
44
|
+
<div class="conn-status-bar" onclick="window._connPanel.toggleExpand()">
|
|
45
|
+
<div class="conn-status-left">
|
|
46
|
+
<div class="conn-dot ${dotClass}"></div>
|
|
47
|
+
<span class="conn-status-text">${statusText}</span>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="conn-toggle">${this.expanded ? '▲' : '▼'}</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
${this.expanded ? `
|
|
53
|
+
<div class="conn-form">
|
|
54
|
+
<div class="conn-field">
|
|
55
|
+
<label>Gateway URL</label>
|
|
56
|
+
<input type="text" id="conn-url" value="${s.gatewayUrl || ''}"
|
|
57
|
+
placeholder="wss://your-gateway.example.com"
|
|
58
|
+
${isConnected ? 'disabled' : ''} />
|
|
59
|
+
<div class="conn-hint">
|
|
60
|
+
Local: <code>ws://localhost:18789</code> · Remote: <code>wss://your-machine.ts.net</code>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="conn-field">
|
|
64
|
+
<label>Token <span class="conn-optional">(if auth enabled)</span></label>
|
|
65
|
+
<input type="password" id="conn-token" value="${s.token || ''}"
|
|
66
|
+
placeholder="Gateway auth token"
|
|
67
|
+
${isConnected ? 'disabled' : ''} />
|
|
68
|
+
</div>
|
|
69
|
+
<div class="conn-field">
|
|
70
|
+
<label>Session Key</label>
|
|
71
|
+
<input type="text" id="conn-session" value="${s.sessionKey || 'main'}"
|
|
72
|
+
placeholder="main"
|
|
73
|
+
${isConnected ? 'disabled' : ''} />
|
|
74
|
+
<div class="conn-hint">Which agent session to visualize</div>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="conn-actions">
|
|
77
|
+
${isConnected ? `
|
|
78
|
+
<button class="conn-btn conn-btn-disconnect" onclick="window._connPanel._disconnect()">
|
|
79
|
+
Disconnect
|
|
80
|
+
</button>
|
|
81
|
+
` : `
|
|
82
|
+
<button class="conn-btn conn-btn-connect" onclick="window._connPanel._connect()"
|
|
83
|
+
${isConnecting ? 'disabled' : ''}>
|
|
84
|
+
${isConnecting ? '⏳ Connecting...' : '🔌 Connect'}
|
|
85
|
+
</button>
|
|
86
|
+
`}
|
|
87
|
+
<label class="conn-auto-label">
|
|
88
|
+
<input type="checkbox" id="conn-auto" ${s.autoConnect ? 'checked' : ''} />
|
|
89
|
+
Auto-connect on load
|
|
90
|
+
</label>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
` : ''}
|
|
94
|
+
</div>
|
|
95
|
+
`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
setState(state, detail) {
|
|
99
|
+
this.state = state;
|
|
100
|
+
if (state === 'error') this.errorMessage = detail;
|
|
101
|
+
else if (state === 'connected') this.errorMessage = null;
|
|
102
|
+
this.render();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
setAgentInfo(name, id) {
|
|
106
|
+
this.agentName = name;
|
|
107
|
+
this.agentId = id;
|
|
108
|
+
if (this.state === 'connected') {
|
|
109
|
+
this.expanded = false;
|
|
110
|
+
}
|
|
111
|
+
this.render();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
toggleExpand() {
|
|
115
|
+
this.expanded = !this.expanded;
|
|
116
|
+
this.render();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
_getFormValues() {
|
|
120
|
+
return {
|
|
121
|
+
gatewayUrl: (document.getElementById('conn-url')?.value || '').trim(),
|
|
122
|
+
token: (document.getElementById('conn-token')?.value || '').trim(),
|
|
123
|
+
sessionKey: (document.getElementById('conn-session')?.value || 'main').trim(),
|
|
124
|
+
autoConnect: document.getElementById('conn-auto')?.checked ?? true,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
_connect() {
|
|
129
|
+
const vals = this._getFormValues();
|
|
130
|
+
if (!vals.gatewayUrl) {
|
|
131
|
+
this.setState('error', 'Please enter a Gateway URL');
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (this.onConnect) this.onConnect(vals);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
_disconnect() {
|
|
138
|
+
if (this.onDisconnect) this.onDisconnect();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (typeof window !== 'undefined') window.ConnectionPanel = ConnectionPanel;
|