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 +40 -0
- package/LICENSE +1 -1
- package/README.md +14 -44
- package/cli.js +163 -5
- package/dashboard.html +185 -3
- package/dashboard.js +288 -16
- package/package.json +1 -1
- package/server.js +227 -31
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.
|
|
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 ·
|
|
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
|
-
- **
|
|
88
|
-
- **
|
|
89
|
-
- **
|
|
90
|
-
- **
|
|
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
|
-
- **
|
|
97
|
-
- **
|
|
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
|
|
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
|
|
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),
|
|
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.
|
|
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 --
|
|
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">🔔<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">🔔</button>
|
|
2615
2622
|
<button class="theme-toggle" id="theme-toggle" onclick="toggleTheme()" title="Toggle dark/light theme">🌙</button>
|
|
2616
2623
|
<button class="sound-toggle" id="sound-toggle" onclick="toggleSound()" title="Toggle notification sound">🔈</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.
|
|
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' +
|
|
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' +
|
|
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' ? '🟢' : n.type === 'offline' ? '🔴' : n.type === 'listening' ? '🔊' : '🔕';
|
|
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">→</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 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : '#' + (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();
|