let-them-talk 3.4.3 → 3.5.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 CHANGED
@@ -1,5 +1,45 @@
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
+
33
+ ## [3.4.4] - 2026-03-15
34
+
35
+ ### Fixed
36
+ - Add project now accepts any existing directory (removed requirement for package.json or .git)
37
+ - Init safely backs up corrupted .mcp.json and settings.json before overwriting
38
+
39
+ ### Changed
40
+ - Removed plugin references from website and docs
41
+ - Website updated with security features (LAN auth token, CSRF, CSP)
42
+
3
43
  ## [3.4.3] - 2026-03-15
4
44
 
5
45
  ### Removed — Plugin System
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.3
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
@@ -72,7 +72,7 @@ Run `npx let-them-talk init --all` to configure all three at once.
72
72
  | | |
73
73
  +----------- .agent-bridge/ directory ----------+
74
74
  messages · agents · tasks
75
- profiles · workflows · plugins
75
+ profiles · workflows · permissions
76
76
  |
77
77
  v
78
78
  Web Dashboard :3000
@@ -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, plugins
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
- - **Plugin system** — extend with custom tools, 30s sandboxed execution
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
@@ -147,7 +148,7 @@ Launch with `npx let-them-talk dashboard` — opens at `http://localhost:3000`.
147
148
  - Browser notifications and sound alerts
148
149
  - LAN mode for phone access
149
150
 
150
- ## MCP Tools (27 + plugins)
151
+ ## MCP Tools (27)
151
152
 
152
153
  <details>
153
154
  <summary><strong>Messaging (13 tools)</strong></summary>
@@ -207,38 +208,6 @@ Launch with `npx let-them-talk dashboard` — opens at `http://localhost:3000`.
207
208
 
208
209
  </details>
209
210
 
210
- ## Plugins
211
-
212
- Extend Let Them Talk with custom tools. Drop a `.js` file in `.agent-bridge/plugins/`.
213
-
214
- ```javascript
215
- module.exports = {
216
- name: 'my-tool',
217
- description: 'What this tool does',
218
- inputSchema: {
219
- type: 'object',
220
- properties: {
221
- query: { type: 'string', description: 'Input text' }
222
- },
223
- required: ['query']
224
- },
225
- handler(args, ctx) {
226
- // ctx: sendMessage, getAgents, getHistory, readFile, registeredName, dataDir
227
- return { result: 'done', query: args.query };
228
- }
229
- };
230
- ```
231
-
232
- ```bash
233
- npx let-them-talk plugin add my-tool.js # install
234
- npx let-them-talk plugin list # list installed
235
- npx let-them-talk plugin remove my-tool # remove
236
- npx let-them-talk plugin enable my-tool # enable
237
- npx let-them-talk plugin disable my-tool # disable
238
- ```
239
-
240
- Plugins run sandboxed with a 30-second timeout. Manage via CLI or dashboard.
241
-
242
211
  ## CLI Reference
243
212
 
244
213
  ```bash
@@ -248,7 +217,8 @@ npx let-them-talk init --template <name> # use a team template
248
217
  npx let-them-talk templates # list templates
249
218
  npx let-them-talk dashboard # launch web dashboard
250
219
  npx let-them-talk reset # clear conversation data
251
- npx let-them-talk plugin <subcommand> # manage plugins
220
+ npx let-them-talk msg <agent> <text> # send a message from CLI
221
+ npx let-them-talk status # show active agents
252
222
  npx let-them-talk help # show help
253
223
  ```
254
224
 
@@ -268,7 +238,7 @@ Let Them Talk is a **local message broker**. It passes text messages between CLI
268
238
 
269
239
  **Does not:** access the internet, store API keys, run cloud services, or grant new filesystem access.
270
240
 
271
- **Built-in protections:** CORS restriction, XSS prevention, path traversal protection, symlink validation, origin enforcement, SSE connection limits, input validation, message size limits (1MB), plugin sandboxing (30s timeout).
241
+ **Built-in protections:** CSRF custom header, LAN auth tokens, Content Security Policy, CORS restriction, XSS prevention, path traversal protection, symlink validation, origin enforcement, SSE connection limits, input validation, message size limits (1MB), agent permissions.
272
242
 
273
243
  **LAN mode:** Optional phone access exposes the dashboard to your local WiFi only. Requires explicit activation.
274
244
 
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.3
12
+ Let Them Talk — Agent Bridge v3.5.0
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, '/');
@@ -65,7 +77,12 @@ function setupClaude(serverPath, cwd) {
65
77
  try {
66
78
  mcpConfig = JSON.parse(fs.readFileSync(mcpConfigPath, 'utf8'));
67
79
  if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
68
- } catch {}
80
+ } catch {
81
+ // Backup corrupted file before overwriting
82
+ const backup = mcpConfigPath + '.backup';
83
+ fs.copyFileSync(mcpConfigPath, backup);
84
+ console.log(' [warn] Existing .mcp.json was invalid — backed up to .mcp.json.backup');
85
+ }
69
86
  }
70
87
 
71
88
  mcpConfig.mcpServers['agent-bridge'] = {
@@ -93,7 +110,11 @@ function setupGemini(serverPath, cwd) {
93
110
  try {
94
111
  settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
95
112
  if (!settings.mcpServers) settings.mcpServers = {};
96
- } catch {}
113
+ } catch {
114
+ const backup = settingsPath + '.backup';
115
+ fs.copyFileSync(settingsPath, backup);
116
+ console.log(' [warn] Existing settings.json was invalid — backed up to settings.json.backup');
117
+ }
97
118
  }
98
119
 
99
120
  settings.mcpServers['agent-bridge'] = {
@@ -138,6 +159,131 @@ AGENT_BRIDGE_DATA_DIR = ${JSON.stringify(dataDir(cwd))}
138
159
  console.log(' [ok] Codex CLI: .codex/config.toml updated');
139
160
  }
140
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
+
141
287
  function init() {
142
288
  const cwd = process.cwd();
143
289
  const serverPath = path.join(__dirname, 'server.js').replace(/\\/g, '/');
@@ -159,6 +305,18 @@ function init() {
159
305
  targets = ['codex'];
160
306
  } else if (flag === '--all') {
161
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'];
162
320
  } else {
163
321
  // Auto-detect
164
322
  targets = detectCLIs();
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.3</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();