watercooler 0.0.4 → 0.0.5
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/package.json +1 -1
- package/public/app.js +141 -15
- package/public/index.html +22 -2
- package/server.ts +64 -2
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -2,10 +2,11 @@ import * as THREE from 'three';
|
|
|
2
2
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
3
3
|
|
|
4
4
|
// State
|
|
5
|
-
let config = { user: '', mailbox: '' };
|
|
5
|
+
let config = { user: '', mailbox: '', avatar: null };
|
|
6
6
|
let messages = []; // Messages TO user (for main panel)
|
|
7
7
|
let allMessages = []; // All messages involving user (for house dialogs)
|
|
8
8
|
let recipients = [];
|
|
9
|
+
let avatarStates = {}; // Map of name -> {tool_name, timestamp}
|
|
9
10
|
let scene, camera, renderer, controls;
|
|
10
11
|
let agentMeshes = new Map();
|
|
11
12
|
let connectionLines = [];
|
|
@@ -137,7 +138,7 @@ function createTrees() {
|
|
|
137
138
|
}
|
|
138
139
|
}
|
|
139
140
|
|
|
140
|
-
function createAgentHouse(name, position) {
|
|
141
|
+
function createAgentHouse(name, position, toolName = null) {
|
|
141
142
|
const color = getAgentColor(name);
|
|
142
143
|
const group = new THREE.Group();
|
|
143
144
|
group.position.copy(position);
|
|
@@ -183,30 +184,42 @@ function createAgentHouse(name, position) {
|
|
|
183
184
|
window2.position.set(1.8, 2.5, 3.1);
|
|
184
185
|
group.add(window2);
|
|
185
186
|
|
|
186
|
-
// Name label sprite
|
|
187
|
+
// Name label sprite (with optional tool name)
|
|
187
188
|
const canvas = document.createElement('canvas');
|
|
188
189
|
const context = canvas.getContext('2d');
|
|
189
190
|
// High DPI canvas for crisp text
|
|
190
191
|
const scale = 2;
|
|
191
192
|
canvas.width = 512;
|
|
192
|
-
canvas.height = 128;
|
|
193
|
+
canvas.height = toolName ? 160 : 128; // Taller if showing tool
|
|
193
194
|
context.scale(scale, scale);
|
|
195
|
+
|
|
196
|
+
// Background
|
|
194
197
|
context.fillStyle = 'rgba(0, 0, 0, 0.7)';
|
|
195
|
-
context.roundRect(0, 0, 256, 64, 16);
|
|
198
|
+
context.roundRect(0, 0, 256, toolName ? 80 : 64, 16);
|
|
196
199
|
context.fill();
|
|
200
|
+
|
|
201
|
+
// Name text
|
|
197
202
|
context.font = 'bold 24px Arial';
|
|
198
203
|
context.fillStyle = 'white';
|
|
199
204
|
context.textAlign = 'center';
|
|
200
205
|
context.textBaseline = 'middle';
|
|
201
|
-
context.fillText(name, 128,
|
|
206
|
+
context.fillText(name, 128, 24);
|
|
207
|
+
|
|
208
|
+
// Tool name text (if available)
|
|
209
|
+
if (toolName) {
|
|
210
|
+
context.font = 'italic 16px Arial';
|
|
211
|
+
context.fillStyle = '#FFD700'; // Gold color for tool name
|
|
212
|
+
context.fillText(toolName, 128, 56);
|
|
213
|
+
}
|
|
202
214
|
|
|
203
215
|
const texture = new THREE.CanvasTexture(canvas);
|
|
204
216
|
texture.minFilter = THREE.LinearFilter;
|
|
205
217
|
texture.magFilter = THREE.LinearFilter;
|
|
206
218
|
const spriteMat = new THREE.SpriteMaterial({ map: texture });
|
|
207
219
|
const sprite = new THREE.Sprite(spriteMat);
|
|
208
|
-
sprite.position.set(0, 8, 0);
|
|
209
|
-
sprite.scale.set(8, 2, 1);
|
|
220
|
+
sprite.position.set(0, toolName ? 8.5 : 8, 0);
|
|
221
|
+
sprite.scale.set(8, toolName ? 2.5 : 2, 1);
|
|
222
|
+
sprite.name = 'label'; // Tag for easy updates
|
|
210
223
|
group.add(sprite);
|
|
211
224
|
|
|
212
225
|
// Path to house
|
|
@@ -223,6 +236,49 @@ function createAgentHouse(name, position) {
|
|
|
223
236
|
return group;
|
|
224
237
|
}
|
|
225
238
|
|
|
239
|
+
function updateHouseLabel(house, name, toolName = null) {
|
|
240
|
+
const sprite = house.getObjectByName('label');
|
|
241
|
+
if (!sprite) return;
|
|
242
|
+
|
|
243
|
+
// Create new canvas with updated text
|
|
244
|
+
const canvas = document.createElement('canvas');
|
|
245
|
+
const context = canvas.getContext('2d');
|
|
246
|
+
const scale = 2;
|
|
247
|
+
canvas.width = 512;
|
|
248
|
+
canvas.height = toolName ? 160 : 128;
|
|
249
|
+
context.scale(scale, scale);
|
|
250
|
+
|
|
251
|
+
// Background
|
|
252
|
+
context.fillStyle = 'rgba(0, 0, 0, 0.7)';
|
|
253
|
+
context.roundRect(0, 0, 256, toolName ? 80 : 64, 16);
|
|
254
|
+
context.fill();
|
|
255
|
+
|
|
256
|
+
// Name text
|
|
257
|
+
context.font = 'bold 24px Arial';
|
|
258
|
+
context.fillStyle = 'white';
|
|
259
|
+
context.textAlign = 'center';
|
|
260
|
+
context.textBaseline = 'middle';
|
|
261
|
+
context.fillText(name, 128, 24);
|
|
262
|
+
|
|
263
|
+
// Tool name text (if available)
|
|
264
|
+
if (toolName) {
|
|
265
|
+
context.font = 'italic 16px Arial';
|
|
266
|
+
context.fillStyle = '#FFD700';
|
|
267
|
+
context.fillText(toolName, 128, 56);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Update texture
|
|
271
|
+
const texture = new THREE.CanvasTexture(canvas);
|
|
272
|
+
texture.minFilter = THREE.LinearFilter;
|
|
273
|
+
texture.magFilter = THREE.LinearFilter;
|
|
274
|
+
sprite.material.map = texture;
|
|
275
|
+
sprite.material.needsUpdate = true;
|
|
276
|
+
|
|
277
|
+
// Update position and scale
|
|
278
|
+
sprite.position.set(0, toolName ? 8.5 : 8, 0);
|
|
279
|
+
sprite.scale.set(8, toolName ? 2.5 : 2, 1);
|
|
280
|
+
}
|
|
281
|
+
|
|
226
282
|
function createMessageParticle(fromPos, toPos) {
|
|
227
283
|
const particleGeo = new THREE.SphereGeometry(0.3, 8, 8);
|
|
228
284
|
const particleMat = new THREE.MeshStandardMaterial({
|
|
@@ -340,12 +396,44 @@ function updateVillage() {
|
|
|
340
396
|
const z = Math.sin(angle) * radius;
|
|
341
397
|
const position = new THREE.Vector3(x, 0, z);
|
|
342
398
|
|
|
399
|
+
// Get tool name for this agent from avatar states
|
|
400
|
+
const avatarState = avatarStates[agent.toLowerCase()];
|
|
401
|
+
const toolName = avatarState?.tool_name || null;
|
|
402
|
+
|
|
343
403
|
if (!agentMeshes.has(agent)) {
|
|
344
|
-
createAgentHouse(agent, position);
|
|
404
|
+
createAgentHouse(agent, position, toolName);
|
|
345
405
|
} else {
|
|
346
406
|
// Update position if needed
|
|
347
407
|
const house = agentMeshes.get(agent);
|
|
348
408
|
house.position.copy(position);
|
|
409
|
+
|
|
410
|
+
// Update label with current tool name
|
|
411
|
+
updateHouseLabel(house, agent, toolName);
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// Remove houses for coworkers that no longer exist
|
|
416
|
+
agentMeshes.forEach((house, name) => {
|
|
417
|
+
if (!allAgents.has(name)) {
|
|
418
|
+
// Remove from scene
|
|
419
|
+
scene.remove(house);
|
|
420
|
+
|
|
421
|
+
// Dispose of geometries and materials to prevent memory leaks
|
|
422
|
+
house.traverse((child) => {
|
|
423
|
+
if (child.isMesh) {
|
|
424
|
+
child.geometry.dispose();
|
|
425
|
+
if (child.material) {
|
|
426
|
+
if (Array.isArray(child.material)) {
|
|
427
|
+
child.material.forEach(m => m.dispose());
|
|
428
|
+
} else {
|
|
429
|
+
child.material.dispose();
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// Remove from Map
|
|
436
|
+
agentMeshes.delete(name);
|
|
349
437
|
}
|
|
350
438
|
});
|
|
351
439
|
|
|
@@ -410,6 +498,17 @@ async function loadData() {
|
|
|
410
498
|
recipients = Array.isArray(recipientsData) ? recipientsData : [];
|
|
411
499
|
allMessages = Array.isArray(allMessagesData) ? allMessagesData : [];
|
|
412
500
|
|
|
501
|
+
// Load avatar states if avatar DB is configured
|
|
502
|
+
if (config.avatar) {
|
|
503
|
+
try {
|
|
504
|
+
const avatarsRes = await fetch('/api/avatars');
|
|
505
|
+
avatarStates = await avatarsRes.json();
|
|
506
|
+
} catch (err) {
|
|
507
|
+
console.error('Error loading avatar states:', err);
|
|
508
|
+
avatarStates = {};
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
413
512
|
updateUI();
|
|
414
513
|
updateVillage();
|
|
415
514
|
} catch (err) {
|
|
@@ -525,6 +624,18 @@ async function markAsRead(id) {
|
|
|
525
624
|
}
|
|
526
625
|
}
|
|
527
626
|
|
|
627
|
+
window.markAllAsRead = async function() {
|
|
628
|
+
const unreadMessages = messages.filter(m => !m.read && m.recipient.toLowerCase() === config.user.toLowerCase());
|
|
629
|
+
if (unreadMessages.length === 0) return;
|
|
630
|
+
|
|
631
|
+
try {
|
|
632
|
+
await Promise.all(unreadMessages.map(m => fetch(`/api/messages/${m.id}/read`, { method: 'POST' })));
|
|
633
|
+
loadData();
|
|
634
|
+
} catch (err) {
|
|
635
|
+
console.error('Error marking all as read:', err);
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
|
|
528
639
|
async function sendMessage() {
|
|
529
640
|
const to = document.getElementById('recipient-select').value;
|
|
530
641
|
const message = document.getElementById('message-input').value.trim();
|
|
@@ -736,7 +847,7 @@ window.closeHouseDialog = function() {
|
|
|
736
847
|
document.getElementById('house-dialog').classList.remove('active');
|
|
737
848
|
};
|
|
738
849
|
|
|
739
|
-
// Update house labels to show unread indicators
|
|
850
|
+
// Update house labels to show unread indicators and tool names
|
|
740
851
|
function updateHouseLabels() {
|
|
741
852
|
agentMeshes.forEach((group, name) => {
|
|
742
853
|
// Check for unread messages SENT TO this agent (messages they haven't read)
|
|
@@ -753,6 +864,10 @@ function updateHouseLabels() {
|
|
|
753
864
|
!m.read
|
|
754
865
|
).length;
|
|
755
866
|
|
|
867
|
+
// Get tool name for this agent
|
|
868
|
+
const avatarState = avatarStates[name.toLowerCase()];
|
|
869
|
+
const toolName = avatarState?.tool_name || null;
|
|
870
|
+
|
|
756
871
|
// Find the sprite label
|
|
757
872
|
const sprite = group.children.find(c => c.isSprite);
|
|
758
873
|
if (sprite) {
|
|
@@ -761,7 +876,7 @@ function updateHouseLabels() {
|
|
|
761
876
|
const context = canvas.getContext('2d');
|
|
762
877
|
const scale = 2;
|
|
763
878
|
canvas.width = 700;
|
|
764
|
-
canvas.height = 128;
|
|
879
|
+
canvas.height = toolName ? 160 : 128;
|
|
765
880
|
context.scale(scale, scale);
|
|
766
881
|
|
|
767
882
|
// Background - change color if there are unread messages
|
|
@@ -775,7 +890,7 @@ function updateHouseLabels() {
|
|
|
775
890
|
// Default black background
|
|
776
891
|
context.fillStyle = 'rgba(0, 0, 0, 0.7)';
|
|
777
892
|
}
|
|
778
|
-
context.roundRect(0, 0, 350, 64, 16);
|
|
893
|
+
context.roundRect(0, 0, 350, toolName ? 80 : 64, 16);
|
|
779
894
|
context.fill();
|
|
780
895
|
|
|
781
896
|
// Name
|
|
@@ -786,12 +901,19 @@ function updateHouseLabels() {
|
|
|
786
901
|
|
|
787
902
|
if (unreadFromAgent > 0) {
|
|
788
903
|
// Show name with unread indicator from agent
|
|
789
|
-
context.fillText(`${name} 🔴 ${unreadFromAgent}`, 175,
|
|
904
|
+
context.fillText(`${name} 🔴 ${unreadFromAgent}`, 175, 24);
|
|
790
905
|
} else if (unreadCount > 0) {
|
|
791
906
|
// Show name with sent-but-unread count
|
|
792
|
-
context.fillText(`${name} 📤 ${unreadCount}`, 175,
|
|
907
|
+
context.fillText(`${name} 📤 ${unreadCount}`, 175, 24);
|
|
793
908
|
} else {
|
|
794
|
-
context.fillText(name, 175,
|
|
909
|
+
context.fillText(name, 175, 24);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Tool name text (if available)
|
|
913
|
+
if (toolName) {
|
|
914
|
+
context.font = 'italic 16px Arial';
|
|
915
|
+
context.fillStyle = '#FFD700'; // Gold color for tool name
|
|
916
|
+
context.fillText(toolName, 175, 56);
|
|
795
917
|
}
|
|
796
918
|
|
|
797
919
|
const texture = new THREE.CanvasTexture(canvas);
|
|
@@ -799,6 +921,10 @@ function updateHouseLabels() {
|
|
|
799
921
|
texture.magFilter = THREE.LinearFilter;
|
|
800
922
|
sprite.material.map = texture;
|
|
801
923
|
sprite.material.needsUpdate = true;
|
|
924
|
+
|
|
925
|
+
// Update position and scale based on whether tool name is shown
|
|
926
|
+
sprite.position.set(0, toolName ? 8.5 : 8, 0);
|
|
927
|
+
sprite.scale.set(8, toolName ? 2.5 : 2, 1);
|
|
802
928
|
}
|
|
803
929
|
});
|
|
804
930
|
}
|
package/public/index.html
CHANGED
|
@@ -199,6 +199,23 @@
|
|
|
199
199
|
opacity: 1;
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
.mark-all-read-btn {
|
|
203
|
+
background: rgba(255, 255, 255, 0.1);
|
|
204
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
205
|
+
color: rgba(255, 255, 255, 0.8);
|
|
206
|
+
font-size: 0.75rem;
|
|
207
|
+
padding: 4px 10px;
|
|
208
|
+
border-radius: 4px;
|
|
209
|
+
cursor: pointer;
|
|
210
|
+
transition: all 0.2s;
|
|
211
|
+
white-space: nowrap;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.mark-all-read-btn:hover {
|
|
215
|
+
background: rgba(255, 255, 255, 0.2);
|
|
216
|
+
color: white;
|
|
217
|
+
}
|
|
218
|
+
|
|
202
219
|
.messages-container {
|
|
203
220
|
flex: 1;
|
|
204
221
|
overflow-y: auto;
|
|
@@ -303,7 +320,7 @@
|
|
|
303
320
|
}
|
|
304
321
|
|
|
305
322
|
.message-text li {
|
|
306
|
-
margin:
|
|
323
|
+
margin: 2px 0;
|
|
307
324
|
}
|
|
308
325
|
|
|
309
326
|
/* Toggle Messages Button */
|
|
@@ -790,7 +807,10 @@
|
|
|
790
807
|
<div class="messages-panel" id="messages-panel">
|
|
791
808
|
<div class="messages-header">
|
|
792
809
|
<h2>📨 Message History</h2>
|
|
793
|
-
<
|
|
810
|
+
<div style="display: flex; gap: 8px; align-items: center;">
|
|
811
|
+
<button class="mark-all-read-btn" onclick="markAllAsRead()">Mark all as read</button>
|
|
812
|
+
<button class="close-btn" onclick="toggleMessagesPanel()">×</button>
|
|
813
|
+
</div>
|
|
794
814
|
</div>
|
|
795
815
|
<div class="messages-container" id="messages-container">
|
|
796
816
|
<div class="empty-state">
|
package/server.ts
CHANGED
|
@@ -13,6 +13,7 @@ const args = process.argv.slice(2);
|
|
|
13
13
|
let user: string | null = null;
|
|
14
14
|
let mailboxPath: string | null = null;
|
|
15
15
|
let coworkerPath: string | null = null;
|
|
16
|
+
let avatarPath: string | null = null;
|
|
16
17
|
let port: number = parseInt(process.env.PORT || '3000', 10);
|
|
17
18
|
let host: string = process.env.HOST || '0.0.0.0';
|
|
18
19
|
|
|
@@ -23,6 +24,8 @@ for (let i = 0; i < args.length; i++) {
|
|
|
23
24
|
mailboxPath = args[++i];
|
|
24
25
|
} else if (args[i] === '--coworkers' || args[i] === '-c') {
|
|
25
26
|
coworkerPath = args[++i];
|
|
27
|
+
} else if (args[i] === '--avatars' || args[i] === '-a') {
|
|
28
|
+
avatarPath = args[++i];
|
|
26
29
|
} else if (args[i] === '--port' || args[i] === '-p') {
|
|
27
30
|
const p = parseInt(args[++i], 10);
|
|
28
31
|
if (!isNaN(p)) port = p;
|
|
@@ -32,7 +35,7 @@ for (let i = 0; i < args.length; i++) {
|
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
if (!user || !mailboxPath) {
|
|
35
|
-
console.error('Usage: watercooler --user <name> --mailbox <path> [--coworkers <path>] [--port <number>] [--host <address>]');
|
|
38
|
+
console.error('Usage: watercooler --user <name> --mailbox <path> [--coworkers <path>] [--avatars <path>] [--port <number>] [--host <address>]');
|
|
36
39
|
process.exit(1);
|
|
37
40
|
}
|
|
38
41
|
|
|
@@ -41,11 +44,15 @@ console.log(` Mailbox: ${mailboxPath}`);
|
|
|
41
44
|
if (coworkerPath) {
|
|
42
45
|
console.log(` Coworker DB: ${coworkerPath}`);
|
|
43
46
|
}
|
|
47
|
+
if (avatarPath) {
|
|
48
|
+
console.log(` Avatar DB: ${avatarPath}`);
|
|
49
|
+
}
|
|
44
50
|
console.log(` URL: http://${host}:${port}`);
|
|
45
51
|
|
|
46
52
|
// Databases
|
|
47
53
|
let db: Database.Database | null = null;
|
|
48
54
|
let coworkerDb: Database.Database | null = null;
|
|
55
|
+
let avatarDb: Database.Database | null = null;
|
|
49
56
|
|
|
50
57
|
try {
|
|
51
58
|
db = new Database(mailboxPath);
|
|
@@ -64,6 +71,15 @@ if (coworkerPath) {
|
|
|
64
71
|
}
|
|
65
72
|
}
|
|
66
73
|
|
|
74
|
+
if (avatarPath) {
|
|
75
|
+
try {
|
|
76
|
+
avatarDb = new Database(avatarPath);
|
|
77
|
+
console.log(' Avatar DB: connected');
|
|
78
|
+
} catch (err: any) {
|
|
79
|
+
console.warn(' Avatar DB error:', err.message);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
67
83
|
// Helper: Check if table exists
|
|
68
84
|
function tableExists(database: Database.Database | null, tableName: string): boolean {
|
|
69
85
|
if (!database) return false;
|
|
@@ -231,9 +247,55 @@ app.post('/api/messages/:id/read', (req, res) => {
|
|
|
231
247
|
}
|
|
232
248
|
});
|
|
233
249
|
|
|
250
|
+
// API: Get avatar states (latest tool usage per coworker)
|
|
251
|
+
app.get('/api/avatars', (req, res) => {
|
|
252
|
+
try {
|
|
253
|
+
if (!avatarDb) {
|
|
254
|
+
res.json({});
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Check if latest_tool_usage table exists
|
|
259
|
+
const tableCheck = avatarDb.prepare(`
|
|
260
|
+
SELECT name FROM sqlite_master
|
|
261
|
+
WHERE type='table' AND name='latest_tool_usage'
|
|
262
|
+
`).get();
|
|
263
|
+
|
|
264
|
+
if (!tableCheck) {
|
|
265
|
+
res.json({});
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Get latest tool usage per name
|
|
270
|
+
const stmt = avatarDb.prepare(`
|
|
271
|
+
SELECT name, tool_name, timestamp
|
|
272
|
+
FROM latest_tool_usage
|
|
273
|
+
ORDER BY timestamp DESC
|
|
274
|
+
`);
|
|
275
|
+
|
|
276
|
+
const rows = stmt.all() as Array<{name: string; tool_name: string; timestamp: number}>;
|
|
277
|
+
|
|
278
|
+
// Build map of name -> latest tool (first occurrence is latest due to ORDER BY)
|
|
279
|
+
const avatarStates: Record<string, {tool_name: string; timestamp: number}> = {};
|
|
280
|
+
for (const row of rows) {
|
|
281
|
+
if (!avatarStates[row.name]) {
|
|
282
|
+
avatarStates[row.name] = {
|
|
283
|
+
tool_name: row.tool_name,
|
|
284
|
+
timestamp: row.timestamp
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
res.json(avatarStates);
|
|
290
|
+
} catch (err: any) {
|
|
291
|
+
console.error('Error in /api/avatars:', err.message);
|
|
292
|
+
res.status(500).json({ error: err.message });
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
234
296
|
// Config endpoint
|
|
235
297
|
app.get('/api/config', (req, res) => {
|
|
236
|
-
res.json({ user, mailbox: mailboxPath, coworker: coworkerPath });
|
|
298
|
+
res.json({ user, mailbox: mailboxPath, coworker: coworkerPath, avatar: avatarPath });
|
|
237
299
|
});
|
|
238
300
|
|
|
239
301
|
app.listen(port, host, () => {
|