let-them-talk 3.4.4 → 3.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.5.0] - 2026-03-15
4
+
5
+ ### Added — Group Conversation Mode
6
+ - **`set_conversation_mode("group")`** — enables free multi-agent collaboration with auto-broadcast
7
+ - **`listen_group()`** — batch message receiver with random stagger (1-3s) to prevent simultaneous responses
8
+ - Returns ALL unconsumed messages + last 20 messages of context + hints about silent agents
9
+ - Auto-broadcast in group mode: every message is shared with all agents automatically
10
+ - Cooldown enforcement: agents must wait 3s between sends to maintain conversation flow
11
+ - Cascade prevention: broadcast copies don't trigger further broadcasts
12
+ - MCP tools: 27 → 29
13
+
14
+ ### Added — Dashboard Features
15
+ - **Notification panel** — bell icon with badge count, dropdown event feed (agent online/offline, listening status changes)
16
+ - **Agent leaderboard** — performance scoring (0-100) with responsiveness, activity, reliability, collaboration dimensions
17
+ - **Cross-project search** — "All Projects" toggle in search bar, searches across all registered projects
18
+ - **Animated replay export** — Export conversation as self-playing HTML file with typing animations and play/pause controls
19
+ - **Ollama integration** — `npx let-them-talk init --ollama` auto-detects Ollama, creates bridge script for local models
20
+
21
+ ### Fixed — PID & Registration Integrity
22
+ - Registration file locking with try/finally (prevents race conditions when multiple agents register simultaneously)
23
+ - PID stale detection uses `last_activity` with 30s threshold (prevents false "alive" from Windows PID reuse)
24
+ - Lock file cleaned up on process exit
25
+ - Dashboard inject/nudge snapshots project context at click time (prevents wrong-project race)
26
+
27
+ ### Security
28
+ - `toolHandoff` and workflow auto-handoff now check `canSendTo` permissions
29
+ - `lastSentAt` updated in `toolBroadcast` (prevents cooldown bypass)
30
+ - `config.json` added to both server and dashboard reset cleanup
31
+ - Auto-broadcast respects `canSendTo` per recipient
32
+
3
33
  ## [3.4.4] - 2026-03-15
4
34
 
5
35
  ### Fixed
package/LICENSE CHANGED
@@ -6,7 +6,7 @@ License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
6
6
  Parameters
7
7
 
8
8
  Licensor: Dekelelz
9
- Licensed Work: Let Them Talk v3.4.4
9
+ Licensed Work: Let Them Talk v3.5.0
10
10
  The Licensed Work is (c) 2024-2026 Dekelelz.
11
11
  Additional Use Grant: You may make use of the Licensed Work, provided that
12
12
  you may not use the Licensed Work for a Commercial
package/README.md CHANGED
@@ -84,19 +84,20 @@ Each terminal spawns its own MCP server process. All processes share a `.agent-b
84
84
 
85
85
  ## Highlights
86
86
 
87
- - **27 MCP tools** — messaging, tasks, workflows, profiles, workspaces, branching
88
- - **Premium dashboard** — glassmorphism UI, Inter font, gradient accents, SSE real-time (~200ms)
89
- - **Stats & analytics** — per-agent message counts, response times, hourly activity charts, velocity
90
- - **Conversation templates** — 4 built-in multi-agent workflows (Code Review, Debug Squad, Feature Dev, Research & Write)
87
+ - **29 MCP tools** — messaging, tasks, workflows, profiles, workspaces, branching, group chat
88
+ - **Group conversation mode** — free multi-agent collaboration with auto-broadcast, stagger delays, and cooldown
89
+ - **Premium dashboard** — glassmorphism UI, notifications panel, agent leaderboard, cross-project search
90
+ - **Animated replay export** — export conversations as self-playing HTML with typing animations
91
+ - **Ollama integration** — `npx let-them-talk init --ollama` for local AI models
92
+ - **Stats & analytics** — per-agent scores, response times, hourly charts, conversation velocity
93
+ - **Conversation templates** — 4 built-in workflows (Code Review, Debug Squad, Feature Dev, Research & Write)
91
94
  - **Message management** — edit, delete, copy messages with full edit history
92
95
  - **Task management** — drag-and-drop kanban board between agents
93
96
  - **Workflow pipelines** — multi-step automation with auto-handoff
94
97
  - **Agent profiles** — display names, SVG avatars, roles, bios
95
98
  - **Conversation branching** — fork at any point, isolated history per branch
96
- - **Compact view** — dense message toggle for power users, persists to localStorage
97
- - **Multi-format export** — HTML, Markdown, and JSON export
98
- - **CLI tools** — send messages and check status directly from the command line
99
- - **Secure by default** — CSRF protection, LAN auth tokens, Content Security Policy, agent permissions
99
+ - **Multi-format export** — HTML, Markdown, JSON, and animated replay
100
+ - **Secure by default** — CSRF, LAN auth tokens, CSP, permissions, registration locking
100
101
  - **Zero config** — one `npx` command, auto-detects your CLI, works immediately
101
102
 
102
103
  ## Agent Templates
package/cli.js CHANGED
@@ -3,14 +3,15 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const os = require('os');
6
+ const { execSync } = require('child_process');
6
7
 
7
8
  const command = process.argv[2];
8
9
 
9
10
  function printUsage() {
10
11
  console.log(`
11
- Let Them Talk — Agent Bridge v3.4.4
12
+ Let Them Talk — Agent Bridge v3.5.1
12
13
  MCP message broker for inter-agent communication
13
- Supports: Claude Code, Gemini CLI, Codex CLI
14
+ Supports: Claude Code, Gemini CLI, Codex CLI, Ollama
14
15
 
15
16
  Usage:
16
17
  npx let-them-talk init Auto-detect CLI and configure MCP
@@ -18,7 +19,8 @@ function printUsage() {
18
19
  npx let-them-talk init --gemini Configure for Gemini CLI
19
20
  npx let-them-talk init --codex Configure for Codex CLI
20
21
  npx let-them-talk init --all Configure for all supported CLIs
21
- npx let-them-talk init --template T Initialize with a team template (pair, team, review, debate)
22
+ npx let-them-talk init --ollama Setup Ollama agent bridge (local LLM)
23
+ npx let-them-talk init --template T Initialize with a team template (pair, team, review, debate, ollama)
22
24
  npx let-them-talk templates List available agent templates
23
25
  npx let-them-talk dashboard Launch the web dashboard (http://localhost:3000)
24
26
  npx let-them-talk dashboard --lan Launch dashboard accessible on LAN (phone/tablet)
@@ -52,6 +54,16 @@ function detectCLIs() {
52
54
  return detected;
53
55
  }
54
56
 
57
+ // Detect Ollama installation
58
+ function detectOllama() {
59
+ try {
60
+ const version = execSync('ollama --version', { encoding: 'utf8', timeout: 5000 }).trim();
61
+ return { installed: true, version };
62
+ } catch {
63
+ return { installed: false };
64
+ }
65
+ }
66
+
55
67
  // The data directory where all agents read/write — must be the same for server + dashboard
56
68
  function dataDir(cwd) {
57
69
  return path.join(cwd, '.agent-bridge').replace(/\\/g, '/');
@@ -147,6 +159,131 @@ AGENT_BRIDGE_DATA_DIR = ${JSON.stringify(dataDir(cwd))}
147
159
  console.log(' [ok] Codex CLI: .codex/config.toml updated');
148
160
  }
149
161
 
162
+ // Setup Ollama agent bridge script
163
+ function setupOllama(serverPath, cwd) {
164
+ const dir = dataDir(cwd);
165
+ const scriptPath = path.join(cwd, '.agent-bridge', 'ollama-agent.js');
166
+
167
+ if (!fs.existsSync(path.join(cwd, '.agent-bridge'))) {
168
+ fs.mkdirSync(path.join(cwd, '.agent-bridge'), { recursive: true });
169
+ }
170
+
171
+ const script = `#!/usr/bin/env node
172
+ // ollama-agent.js - bridges Ollama to Let Them Talk
173
+ // Usage: node .agent-bridge/ollama-agent.js [agent-name] [model]
174
+ const fs = require('fs'), path = require('path'), http = require('http');
175
+ const DATA_DIR = path.join(__dirname);
176
+ const name = process.argv[2] || 'Ollama';
177
+ const model = process.argv[3] || 'llama3';
178
+ const OLLAMA_URL = process.env.OLLAMA_URL || 'http://localhost:11434';
179
+
180
+ function readJson(f) { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return {}; } }
181
+ function readJsonl(f) { if (!fs.existsSync(f)) return []; return fs.readFileSync(f, 'utf8').split('\\n').filter(l => l.trim()).map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean); }
182
+
183
+ // Register agent
184
+ function register() {
185
+ const agentsFile = path.join(DATA_DIR, 'agents.json');
186
+ const agents = readJson(agentsFile);
187
+ agents[name] = { pid: process.pid, timestamp: new Date().toISOString(), last_activity: new Date().toISOString(), provider: 'Ollama (' + model + ')' };
188
+ fs.writeFileSync(agentsFile, JSON.stringify(agents, null, 2));
189
+ console.log('[' + name + '] Registered (PID ' + process.pid + ', model: ' + model + ')');
190
+ }
191
+
192
+ // Update heartbeat
193
+ function heartbeat() {
194
+ const agentsFile = path.join(DATA_DIR, 'agents.json');
195
+ const agents = readJson(agentsFile);
196
+ if (agents[name]) {
197
+ agents[name].last_activity = new Date().toISOString();
198
+ agents[name].pid = process.pid;
199
+ fs.writeFileSync(agentsFile, JSON.stringify(agents, null, 2));
200
+ }
201
+ }
202
+
203
+ // Call Ollama API
204
+ function callOllama(prompt) {
205
+ return new Promise(function(resolve, reject) {
206
+ const url = new URL(OLLAMA_URL + '/api/chat');
207
+ const body = JSON.stringify({ model: model, messages: [{ role: 'user', content: prompt }], stream: false });
208
+ const req = http.request(url, { method: 'POST', headers: { 'Content-Type': 'application/json' } }, function(res) {
209
+ let data = '';
210
+ res.on('data', function(c) { data += c; });
211
+ res.on('end', function() {
212
+ try { const j = JSON.parse(data); resolve(j.message ? j.message.content : data); }
213
+ catch { resolve(data); }
214
+ });
215
+ });
216
+ req.on('error', reject);
217
+ req.write(body);
218
+ req.end();
219
+ });
220
+ }
221
+
222
+ // Send a message
223
+ function sendMessage(to, content) {
224
+ const msgId = 'm' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
225
+ const msg = { id: msgId, from: name, to: to, content: content, timestamp: new Date().toISOString() };
226
+ fs.appendFileSync(path.join(DATA_DIR, 'messages.jsonl'), JSON.stringify(msg) + '\\n');
227
+ fs.appendFileSync(path.join(DATA_DIR, 'history.jsonl'), JSON.stringify(msg) + '\\n');
228
+ console.log('[' + name + '] -> ' + to + ': ' + content.substring(0, 80) + (content.length > 80 ? '...' : ''));
229
+ }
230
+
231
+ // Listen for messages
232
+ let lastOffset = 0;
233
+ function checkMessages() {
234
+ const consumedFile = path.join(DATA_DIR, 'consumed-' + name + '.json');
235
+ const consumed = readJson(consumedFile);
236
+ lastOffset = consumed.offset || 0;
237
+
238
+ const messages = readJsonl(path.join(DATA_DIR, 'messages.jsonl'));
239
+ const newMsgs = messages.slice(lastOffset).filter(function(m) {
240
+ return m.to === name || (m.to === 'all' && m.from !== name);
241
+ });
242
+
243
+ if (newMsgs.length > 0) {
244
+ consumed.offset = messages.length;
245
+ fs.writeFileSync(consumedFile, JSON.stringify(consumed));
246
+ }
247
+
248
+ return newMsgs;
249
+ }
250
+
251
+ async function processMessages() {
252
+ const msgs = checkMessages();
253
+ for (const m of msgs) {
254
+ console.log('[' + name + '] <- ' + m.from + ': ' + m.content.substring(0, 80));
255
+ try {
256
+ const response = await callOllama(m.content);
257
+ sendMessage(m.from, response);
258
+ } catch (e) {
259
+ sendMessage(m.from, 'Error calling Ollama: ' + e.message);
260
+ }
261
+ }
262
+ }
263
+
264
+ // Main loop
265
+ register();
266
+ const hb = setInterval(heartbeat, 10000);
267
+ hb.unref();
268
+ console.log('[' + name + '] Listening for messages... (Ctrl+C to stop)');
269
+ setInterval(processMessages, 2000);
270
+
271
+ // Cleanup on exit
272
+ process.on('SIGINT', function() { console.log('\\n[' + name + '] Shutting down.'); process.exit(0); });
273
+ `;
274
+
275
+ fs.writeFileSync(scriptPath, script);
276
+ console.log(' [ok] Ollama agent script created: .agent-bridge/ollama-agent.js');
277
+ console.log('');
278
+ console.log(' Launch an Ollama agent with:');
279
+ console.log(' node .agent-bridge/ollama-agent.js <name> <model>');
280
+ console.log('');
281
+ console.log(' Examples:');
282
+ console.log(' node .agent-bridge/ollama-agent.js Ollama llama3');
283
+ console.log(' node .agent-bridge/ollama-agent.js Coder codellama');
284
+ console.log(' node .agent-bridge/ollama-agent.js Writer mistral');
285
+ }
286
+
150
287
  function init() {
151
288
  const cwd = process.cwd();
152
289
  const serverPath = path.join(__dirname, 'server.js').replace(/\\/g, '/');
@@ -168,6 +305,18 @@ function init() {
168
305
  targets = ['codex'];
169
306
  } else if (flag === '--all') {
170
307
  targets = ['claude', 'gemini', 'codex'];
308
+ } else if (flag === '--ollama') {
309
+ const ollama = detectOllama();
310
+ if (!ollama.installed) {
311
+ console.log(' Ollama not found. Install it from: https://ollama.com/download');
312
+ console.log(' After installing, run: ollama pull llama3');
313
+ console.log('');
314
+ } else {
315
+ console.log(' Ollama detected: ' + ollama.version);
316
+ setupOllama(serverPath, cwd);
317
+ }
318
+ targets = detectCLIs();
319
+ if (targets.length === 0) targets = ['claude'];
171
320
  } else {
172
321
  // Auto-detect
173
322
  targets = detectCLIs();
@@ -0,0 +1,11 @@
1
+ {
2
+ "id": "code-review",
3
+ "name": "Code Review Pipeline",
4
+ "description": "Developer writes code, Reviewer checks it, Tester validates",
5
+ "agents": [
6
+ { "name": "Developer", "role": "Developer", "prompt": "You are a developer. Write code as instructed. After completing, send your code to Reviewer for review." },
7
+ { "name": "Reviewer", "role": "Code Reviewer", "prompt": "You are a code reviewer. Wait for code from Developer. Review it for bugs, style, and best practices. Send feedback back to Developer or approve and forward to Tester." },
8
+ { "name": "Tester", "role": "QA Tester", "prompt": "You are a QA tester. Wait for approved code from Reviewer. Write and run tests. Report results back to the team." }
9
+ ],
10
+ "workflow": { "name": "Code Review", "steps": ["Write Code", "Review", "Test", "Approve"] }
11
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "id": "debug-squad",
3
+ "name": "Debug Squad",
4
+ "description": "Investigator finds the bug, Fixer patches it, Verifier confirms the fix",
5
+ "agents": [
6
+ { "name": "Investigator", "role": "Bug Investigator", "prompt": "You investigate bugs. Analyze error logs, trace code paths, and identify root causes. Send findings to Fixer." },
7
+ { "name": "Fixer", "role": "Bug Fixer", "prompt": "You fix bugs. Wait for findings from Investigator. Implement fixes and send to Verifier for confirmation." },
8
+ { "name": "Verifier", "role": "Fix Verifier", "prompt": "You verify bug fixes. Wait for patches from Fixer. Test the fix and confirm resolution or send back for more work." }
9
+ ],
10
+ "workflow": { "name": "Bug Fix", "steps": ["Investigate", "Fix", "Verify", "Close"] }
11
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "id": "feature-build",
3
+ "name": "Feature Development",
4
+ "description": "Architect designs, Builder implements, Reviewer approves",
5
+ "agents": [
6
+ { "name": "Architect", "role": "Software Architect", "prompt": "You are a software architect. Design the feature architecture, define interfaces, and create the implementation plan. Send the plan to Builder." },
7
+ { "name": "Builder", "role": "Developer", "prompt": "You are a developer. Wait for architecture plans from Architect. Implement the feature following the design. Send completed code to Reviewer." },
8
+ { "name": "Reviewer", "role": "Senior Reviewer", "prompt": "You are a senior reviewer. Review implementations from Builder against the architecture from Architect. Approve or request changes." }
9
+ ],
10
+ "workflow": { "name": "Feature Dev", "steps": ["Design", "Implement", "Review", "Ship"] }
11
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "id": "research-write",
3
+ "name": "Research & Write",
4
+ "description": "Researcher gathers info, Writer creates content, Editor polishes",
5
+ "agents": [
6
+ { "name": "Researcher", "role": "Researcher", "prompt": "You are a researcher. Gather information on the given topic. Organize findings and send a research brief to Writer." },
7
+ { "name": "Writer", "role": "Writer", "prompt": "You are a writer. Wait for research from Researcher. Write clear, well-structured content based on the findings. Send to Editor." },
8
+ { "name": "Editor", "role": "Editor", "prompt": "You are an editor. Review and polish content from Writer. Check for clarity, accuracy, and style. Send back final version or request revisions." }
9
+ ],
10
+ "workflow": { "name": "Content Pipeline", "steps": ["Research", "Draft", "Edit", "Publish"] }
11
+ }
package/dashboard.html CHANGED
@@ -2611,6 +2611,13 @@
2611
2611
  </div>
2612
2612
  </div>
2613
2613
  <div class="header-actions">
2614
+ <div style="position:relative;display:inline-block">
2615
+ <button class="notif-toggle" id="notif-bell" onclick="toggleNotifPanel()" title="Notifications" style="position:relative">&#x1f514;<span id="notif-badge" style="display:none;position:absolute;top:-4px;right:-4px;background:var(--red);color:#fff;font-size:8px;font-weight:700;min-width:14px;height:14px;border-radius:7px;text-align:center;line-height:14px;padding:0 3px">0</span></button>
2616
+ <div id="notif-panel" style="display:none;position:absolute;right:0;top:100%;margin-top:8px;background:var(--surface);border:1px solid var(--border-light);border-radius:12px;width:320px;max-height:400px;overflow-y:auto;z-index:300;box-shadow:var(--shadow-lg)">
2617
+ <div style="padding:12px 16px;border-bottom:1px solid var(--border);font-weight:600;font-size:13px;display:flex;justify-content:space-between;align-items:center">Notifications <button onclick="clearNotifications()" style="background:none;border:none;color:var(--text-muted);font-size:11px;cursor:pointer">Clear</button></div>
2618
+ <div id="notif-list" style="padding:4px 0"><div style="padding:16px;text-align:center;color:var(--text-muted);font-size:12px">No notifications yet</div></div>
2619
+ </div>
2620
+ </div>
2614
2621
  <button class="notif-toggle" id="notif-toggle" onclick="toggleNotifications()" title="Browser notifications">&#x1f514;</button>
2615
2622
  <button class="theme-toggle" id="theme-toggle" onclick="toggleTheme()" title="Toggle dark/light theme">&#x1f319;</button>
2616
2623
  <button class="sound-toggle" id="sound-toggle" onclick="toggleSound()" title="Toggle notification sound">&#x1f508;</button>
@@ -2623,6 +2630,7 @@
2623
2630
  <div style="padding:7px 12px;font-size:12px;cursor:pointer;transition:background 0.1s" onmouseover="this.style.background='var(--surface-3)'" onmouseout="this.style.background=''" onclick="exportShareableHTML();toggleExportMenu()">HTML (shareable)</div>
2624
2631
  <div style="padding:7px 12px;font-size:12px;cursor:pointer;transition:background 0.1s" onmouseover="this.style.background='var(--surface-3)'" onmouseout="this.style.background=''" onclick="exportConversation();toggleExportMenu()">Markdown (.md)</div>
2625
2632
  <div style="padding:7px 12px;font-size:12px;cursor:pointer;transition:background 0.1s" onmouseover="this.style.background='var(--surface-3)'" onmouseout="this.style.background=''" onclick="exportJSON();toggleExportMenu()">JSON (.json)</div>
2633
+ <div style="padding:7px 12px;font-size:12px;cursor:pointer;transition:background 0.1s;border-top:1px solid var(--border)" onmouseover="this.style.background='var(--surface-3)'" onmouseout="this.style.background=''" onclick="exportReplay();toggleExportMenu()">Animated Replay (.html)</div>
2626
2634
  </div>
2627
2635
  </div>
2628
2636
  <button class="btn btn-danger" onclick="doReset()">Reset</button>
@@ -2744,6 +2752,7 @@
2744
2752
  <div class="branch-tabs" id="branch-tabs"></div>
2745
2753
  <div class="search-bar" id="search-bar">
2746
2754
  <input class="search-input" id="search-input" placeholder="Search messages... ( / )" oninput="onSearch()">
2755
+ <button id="search-all-btn" onclick="toggleSearchAll()" title="Search across all projects" style="background:var(--surface-2);border:1px solid var(--border);border-radius:6px;padding:4px 8px;font-size:10px;cursor:pointer;color:var(--text-muted);white-space:nowrap;transition:all 0.2s">All Projects</button>
2747
2756
  <span class="search-count" id="search-count"></span>
2748
2757
  <button class="compact-toggle" id="compact-toggle" onclick="toggleCompactMode()" title="Toggle compact view">Compact</button>
2749
2758
  </div>
@@ -2778,7 +2787,7 @@
2778
2787
  </div>
2779
2788
  </div>
2780
2789
  <div class="app-footer">
2781
- <span>Let Them Talk v3.4.4</span>
2790
+ <span>Let Them Talk v3.5.0</span>
2782
2791
  </div>
2783
2792
  <div class="profile-popup" id="profile-popup" onclick="event.stopPropagation()">
2784
2793
  <div class="profile-popup-header">
@@ -3182,8 +3191,10 @@ function renderAgents(agents) {
3182
3191
  }
3183
3192
 
3184
3193
  function sendNudge(agentName) {
3194
+ var lockedProject = activeProject;
3195
+ var pq = lockedProject ? '?project=' + encodeURIComponent(lockedProject) : '';
3185
3196
  var body = JSON.stringify({ to: agentName, content: 'Hey ' + agentName + ', the user is waiting for you. Please check for new messages and continue your work.' });
3186
- lttFetch('/api/inject' + projectParam(), {
3197
+ lttFetch('/api/inject' + pq, {
3187
3198
  method: 'POST',
3188
3199
  headers: { 'Content-Type': 'application/json' },
3189
3200
  body: body
@@ -3240,8 +3251,11 @@ function doInject() {
3240
3251
  var content = document.getElementById('inject-content').value.trim();
3241
3252
  if (!target || !content) return;
3242
3253
 
3254
+ // Lock project context at send time — prevents race if user switches project mid-type
3255
+ var lockedProject = activeProject;
3256
+ var pq = lockedProject ? '?project=' + encodeURIComponent(lockedProject) : '';
3243
3257
  var body = JSON.stringify({ to: target, content: content });
3244
- lttFetch('/api/inject' + projectParam(), {
3258
+ lttFetch('/api/inject' + pq, {
3245
3259
  method: 'POST',
3246
3260
  headers: { 'Content-Type': 'application/json' },
3247
3261
  body: body
@@ -3558,6 +3572,10 @@ var searchQuery = '';
3558
3572
 
3559
3573
  function onSearch() {
3560
3574
  searchQuery = document.getElementById('search-input').value.toLowerCase().trim();
3575
+ if (searchAllMode && searchQuery.length >= 2) {
3576
+ searchAllProjects(searchQuery);
3577
+ return;
3578
+ }
3561
3579
  lastMessageCount = 0;
3562
3580
  renderMessages(cachedHistory);
3563
3581
  }
@@ -4281,6 +4299,7 @@ function fetchStats() {
4281
4299
  lttFetch('/api/stats' + pq).then(function(r) { return r.json(); }).then(function(data) {
4282
4300
  renderStats(data);
4283
4301
  }).catch(function(e) { console.error('Stats fetch failed:', e); });
4302
+ fetchScores();
4284
4303
  }
4285
4304
 
4286
4305
  function renderStats(data) {
@@ -4356,6 +4375,11 @@ function renderStats(data) {
4356
4375
  }
4357
4376
  html += '</div></div>';
4358
4377
 
4378
+ html += '<div style="background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:16px;margin-top:16px">' +
4379
+ '<h3 style="font-size:14px;font-weight:700;margin-bottom:12px;color:var(--accent)">Agent Leaderboard</h3>' +
4380
+ '<div id="scores-area"><div style="color:var(--text-muted);font-size:12px">Loading scores...</div></div>' +
4381
+ '</div>';
4382
+
4359
4383
  el.innerHTML = html;
4360
4384
  }
4361
4385
 
@@ -4686,6 +4710,163 @@ function switchBranch(name) {
4686
4710
  poll();
4687
4711
  }
4688
4712
 
4713
+ // ==================== v3.5: NOTIFICATIONS PANEL ====================
4714
+
4715
+ var notifData = [];
4716
+ var notifSeen = 0;
4717
+
4718
+ function toggleNotifPanel() {
4719
+ var panel = document.getElementById('notif-panel');
4720
+ var isOpen = panel.style.display !== 'none';
4721
+ panel.style.display = isOpen ? 'none' : 'block';
4722
+ if (!isOpen) {
4723
+ notifSeen = notifData.length;
4724
+ updateNotifBadge();
4725
+ fetchNotifications();
4726
+ }
4727
+ }
4728
+
4729
+ function fetchNotifications() {
4730
+ var pq = projectParam();
4731
+ lttFetch('/api/notifications' + pq).then(function(r) { return r.json(); }).then(function(data) {
4732
+ notifData = Array.isArray(data) ? data : [];
4733
+ renderNotifList();
4734
+ updateNotifBadge();
4735
+ }).catch(function() {});
4736
+ }
4737
+
4738
+ function renderNotifList() {
4739
+ var el = document.getElementById('notif-list');
4740
+ if (!notifData.length) { el.innerHTML = '<div style="padding:16px;text-align:center;color:var(--text-muted);font-size:12px">No notifications yet</div>'; return; }
4741
+ var html = '';
4742
+ for (var i = notifData.length - 1; i >= 0; i--) {
4743
+ var n = notifData[i];
4744
+ var icon = n.type === 'online' ? '&#x1f7e2;' : n.type === 'offline' ? '&#x1f534;' : n.type === 'listening' ? '&#x1f50a;' : '&#x1f515;';
4745
+ var time = new Date(n.timestamp).toLocaleTimeString();
4746
+ html += '<div style="padding:8px 16px;border-bottom:1px solid var(--border);font-size:12px;display:flex;gap:8px;align-items:center">' +
4747
+ '<span>' + icon + '</span>' +
4748
+ '<div style="flex:1"><div style="color:var(--text)">' + escapeHtml(n.message) + '</div><div style="color:var(--text-muted);font-size:10px">' + time + '</div></div>' +
4749
+ '</div>';
4750
+ }
4751
+ el.innerHTML = html;
4752
+ }
4753
+
4754
+ function updateNotifBadge() {
4755
+ var badge = document.getElementById('notif-badge');
4756
+ var unseen = notifData.length - notifSeen;
4757
+ if (unseen > 0) { badge.textContent = unseen; badge.style.display = 'block'; }
4758
+ else { badge.style.display = 'none'; }
4759
+ }
4760
+
4761
+ function clearNotifications() {
4762
+ notifData = [];
4763
+ notifSeen = 0;
4764
+ renderNotifList();
4765
+ updateNotifBadge();
4766
+ }
4767
+
4768
+ // Close notif panel on outside click
4769
+ document.addEventListener('click', function(e) {
4770
+ var panel = document.getElementById('notif-panel');
4771
+ var bell = document.getElementById('notif-bell');
4772
+ if (panel && panel.style.display !== 'none' && !panel.contains(e.target) && e.target !== bell && !bell.contains(e.target)) {
4773
+ panel.style.display = 'none';
4774
+ }
4775
+ });
4776
+
4777
+ // ==================== v3.5: CROSS-PROJECT SEARCH ====================
4778
+
4779
+ var searchAllMode = false;
4780
+
4781
+ function toggleSearchAll() {
4782
+ searchAllMode = !searchAllMode;
4783
+ var btn = document.getElementById('search-all-btn');
4784
+ btn.style.background = searchAllMode ? 'var(--accent-dim)' : 'var(--surface-2)';
4785
+ btn.style.color = searchAllMode ? 'var(--accent)' : 'var(--text-muted)';
4786
+ btn.style.borderColor = searchAllMode ? 'var(--accent)' : 'var(--border)';
4787
+ if (searchAllMode) {
4788
+ document.getElementById('search-input').placeholder = 'Search ALL projects...';
4789
+ } else {
4790
+ document.getElementById('search-input').placeholder = 'Search messages... ( / )';
4791
+ }
4792
+ onSearch();
4793
+ }
4794
+
4795
+ function searchAllProjects(query) {
4796
+ if (!query || query.length < 2) return;
4797
+ var countEl = document.getElementById('search-count');
4798
+ countEl.textContent = 'Searching...';
4799
+ lttFetch('/api/search-all?q=' + encodeURIComponent(query) + '&limit=30').then(function(r) { return r.json(); }).then(function(data) {
4800
+ if (data.error) { countEl.textContent = data.error; return; }
4801
+ countEl.textContent = data.total + ' results across ' + data.results.length + ' projects';
4802
+ // Render results in the messages area
4803
+ var area = document.getElementById('messages');
4804
+ var html = '';
4805
+ for (var i = 0; i < data.results.length; i++) {
4806
+ var proj = data.results[i];
4807
+ html += '<div class="date-sep">' + escapeHtml(proj.project) + ' (' + proj.messages.length + ')</div>';
4808
+ for (var j = 0; j < proj.messages.length; j++) {
4809
+ var m = proj.messages[j];
4810
+ var time = new Date(m.timestamp).toLocaleString();
4811
+ html += '<div class="message" style="opacity:0.85">' +
4812
+ '<div class="msg-body"><div class="msg-header"><span class="msg-from" style="color:var(--accent)">' + escapeHtml(m.from) + '</span>' +
4813
+ '<span class="msg-arrow">&rarr;</span><span class="msg-to">' + escapeHtml(m.to || 'all') + '</span>' +
4814
+ '<span class="msg-time">' + time + '</span></div>' +
4815
+ '<div class="msg-content">' + escapeHtml(m.content.substring(0, 300)) + (m.content.length > 300 ? '...' : '') + '</div></div></div>';
4816
+ }
4817
+ }
4818
+ if (!html) html = '<div class="empty-state"><div class="empty-text">No results found</div></div>';
4819
+ area.innerHTML = html;
4820
+ }).catch(function() { countEl.textContent = 'Search failed'; });
4821
+ }
4822
+
4823
+ // ==================== v3.5: REPLAY EXPORT ====================
4824
+
4825
+ function exportReplay() {
4826
+ var pq = projectParam();
4827
+ window.location.href = '/api/export-replay' + (pq || '?') + (_lttToken ? '&token=' + encodeURIComponent(_lttToken) : '');
4828
+ }
4829
+
4830
+ // ==================== v3.5: PERFORMANCE SCORES ====================
4831
+
4832
+ function fetchScores() {
4833
+ var pq = projectParam();
4834
+ lttFetch('/api/scores' + pq).then(function(r) { return r.json(); }).then(function(data) {
4835
+ renderScores(data);
4836
+ }).catch(function() {});
4837
+ }
4838
+
4839
+ function renderScores(data) {
4840
+ if (!data || !data.agents) return;
4841
+ var el = document.getElementById('scores-area');
4842
+ if (!el) return;
4843
+ var agents = data.agents;
4844
+ var names = Object.keys(agents).sort(function(a, b) { return agents[b].score - agents[a].score; });
4845
+ if (!names.length) { el.innerHTML = '<div style="color:var(--text-muted);font-size:12px;padding:16px;text-align:center">No agent data yet</div>'; return; }
4846
+
4847
+ var html = '<div style="display:grid;gap:8px">';
4848
+ for (var i = 0; i < names.length; i++) {
4849
+ var n = names[i];
4850
+ var a = agents[n];
4851
+ var medal = i === 0 ? '&#x1f947;' : i === 1 ? '&#x1f948;' : i === 2 ? '&#x1f949;' : '#' + (i + 1);
4852
+ var scoreColor = a.score >= 80 ? 'var(--green)' : a.score >= 60 ? 'var(--accent)' : a.score >= 40 ? 'var(--orange)' : 'var(--red)';
4853
+ html += '<div style="background:var(--surface-2);border:1px solid var(--border);border-radius:10px;padding:12px 16px;display:flex;align-items:center;gap:12px">' +
4854
+ '<div style="font-size:18px;width:28px;text-align:center">' + medal + '</div>' +
4855
+ '<div style="flex:1;min-width:0"><div style="font-weight:600;font-size:13px">' + escapeHtml(n) + '</div>' +
4856
+ '<div style="display:flex;gap:12px;margin-top:4px;font-size:10px;color:var(--text-muted)">' +
4857
+ '<span>Resp: ' + a.responsiveness + '</span>' +
4858
+ '<span>Act: ' + a.activity + '</span>' +
4859
+ '<span>Rel: ' + a.reliability + '</span>' +
4860
+ '<span>Collab: ' + a.collaboration + '</span>' +
4861
+ '</div>' +
4862
+ '</div>' +
4863
+ '<div style="font-size:22px;font-weight:700;color:' + scoreColor + '">' + a.score + '</div>' +
4864
+ '</div>';
4865
+ }
4866
+ html += '</div>';
4867
+ el.innerHTML = html;
4868
+ }
4869
+
4689
4870
  // ==================== POLLING ====================
4690
4871
 
4691
4872
  function poll() {
@@ -4737,6 +4918,7 @@ function poll() {
4737
4918
  renderBookmarksSidebar();
4738
4919
  fetchActivity();
4739
4920
  fetchBranches();
4921
+ fetchNotifications();
4740
4922
  updateTypingIndicator(cachedAgents);
4741
4923
  if (activeView === 'tasks') fetchTasks();
4742
4924
  if (activeView === 'workspaces') fetchWorkspaces();