@songsid/agend 0.0.4 → 0.0.6
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/README.md +1 -1
- package/README.zh-TW.md +1 -1
- package/dist/agent-cli.js +0 -0
- package/dist/cli.js +105 -32
- package/dist/cli.js.map +1 -1
- package/dist/fleet-manager.js +4 -1
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills.md +95 -0
- package/dist/logger.d.ts +0 -2
- package/dist/logger.js +2 -16
- package/dist/logger.js.map +1 -1
- package/package.json +3 -5
- package/dist/fleet-dashboard-html.d.ts +0 -6
- package/dist/fleet-dashboard-html.js +0 -443
- package/dist/fleet-dashboard-html.js.map +0 -1
- package/dist/fleet-health-server.d.ts +0 -35
- package/dist/fleet-health-server.js +0 -290
- package/dist/fleet-health-server.js.map +0 -1
- package/dist/fleet-instructions.d.ts +0 -5
- package/dist/fleet-instructions.js +0 -161
- package/dist/fleet-instructions.js.map +0 -1
- package/dist/fleet-rpc-handlers.d.ts +0 -42
- package/dist/fleet-rpc-handlers.js +0 -356
- package/dist/fleet-rpc-handlers.js.map +0 -1
|
@@ -175,3 +175,98 @@ templates: # Reusable fleet deployment templates
|
|
|
175
175
|
- `describe_instance("<name>")` — shows status, last activity, description
|
|
176
176
|
- `tmux capture-pane -t agend:<name> -p | tail -20` — see actual CLI screen
|
|
177
177
|
- Look for `X% !>` prompt = idle, `Thinking...` = busy, `error` = needs attention
|
|
178
|
+
|
|
179
|
+
## 10. Safe Update & Restart
|
|
180
|
+
|
|
181
|
+
**Update AgEnD to latest version:**
|
|
182
|
+
```bash
|
|
183
|
+
agend update # update to latest
|
|
184
|
+
agend update --version 0.0.6 # pin specific version
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The `agend update` command automatically:
|
|
188
|
+
- Detects if sudo is needed (switches to nvm if so)
|
|
189
|
+
- Installs new version
|
|
190
|
+
- Verifies installation succeeded
|
|
191
|
+
- Updates service file (ExecStart path)
|
|
192
|
+
- Restarts fleet
|
|
193
|
+
|
|
194
|
+
**Manual restart (if update isn't needed):**
|
|
195
|
+
```bash
|
|
196
|
+
agend fleet restart # graceful restart (SIGUSR2) — keeps sessions, reloads config
|
|
197
|
+
agend fleet stop && agend fleet start # full restart — new code takes effect
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**NEVER do:**
|
|
201
|
+
- `kill -9` on the fleet process (corrupts state)
|
|
202
|
+
- Edit fleet.yaml while fleet is restarting
|
|
203
|
+
- Run `agend update` while another update is in progress
|
|
204
|
+
|
|
205
|
+
## 11. Model Names by Backend
|
|
206
|
+
|
|
207
|
+
Models are specified in fleet.yaml `defaults.model` or per-instance `model` field.
|
|
208
|
+
|
|
209
|
+
| Backend | Model Names | Default |
|
|
210
|
+
|---------|-------------|---------|
|
|
211
|
+
| **kiro-cli** | `claude-sonnet-4-20250514`, `claude-opus-4-20250514`, `claude-haiku-3-20250307` | auto (latest) |
|
|
212
|
+
| **claude-code** | `sonnet`, `opus`, `haiku`, `opusplan`, `best`, `sonnet[1m]`, `opus[1m]` | sonnet |
|
|
213
|
+
| **gemini-cli** | `gemini-2.5-pro`, `gemini-2.5-flash` | auto |
|
|
214
|
+
| **codex** | `gpt-4o`, `o3`, `o4-mini` | gpt-4o |
|
|
215
|
+
| **opencode** | depends on provider config | — |
|
|
216
|
+
|
|
217
|
+
**Important:** kiro-cli uses FULL model IDs (e.g. `claude-sonnet-4-20250514`), NOT short names like `sonnet`. Claude Code uses short names. Don't mix them up.
|
|
218
|
+
|
|
219
|
+
Example fleet.yaml:
|
|
220
|
+
```yaml
|
|
221
|
+
defaults:
|
|
222
|
+
backend: kiro-cli
|
|
223
|
+
model: claude-sonnet-4-20250514
|
|
224
|
+
|
|
225
|
+
instances:
|
|
226
|
+
heavy-task:
|
|
227
|
+
model: claude-opus-4-20250514
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## 12. Config Validation
|
|
231
|
+
|
|
232
|
+
**Before editing fleet.yaml or classicBot.yaml, always validate after:**
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
# Validate fleet.yaml syntax
|
|
236
|
+
agend fleet start --dry-run 2>&1 | head -5
|
|
237
|
+
# Or simply:
|
|
238
|
+
node -e "const yaml = require('js-yaml'); const fs = require('fs'); yaml.load(fs.readFileSync('$HOME/.agend/fleet.yaml', 'utf-8')); console.log('✓ valid YAML')"
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**Common fleet.yaml mistakes:**
|
|
242
|
+
- Missing `channel.mode` field → error on start
|
|
243
|
+
- Wrong indentation (YAML is indent-sensitive)
|
|
244
|
+
- `topic_id` as string vs number (both work, but be consistent)
|
|
245
|
+
- `backend` typo (valid: `claude-code`, `gemini-cli`, `codex`, `opencode`, `kiro-cli`)
|
|
246
|
+
- `model` using wrong format for the backend
|
|
247
|
+
|
|
248
|
+
**classicBot.yaml validation:**
|
|
249
|
+
```bash
|
|
250
|
+
node -e "const yaml = require('js-yaml'); const fs = require('fs'); yaml.load(fs.readFileSync('$HOME/.agend/classicBot.yaml', 'utf-8')); console.log('✓ valid YAML')"
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**Common classicBot.yaml mistakes:**
|
|
254
|
+
- `allowed_guilds` values must be strings (Discord IDs are too large for YAML integers)
|
|
255
|
+
- Channel IDs as keys must be quoted strings
|
|
256
|
+
- Missing `defaults` section (optional but recommended)
|
|
257
|
+
|
|
258
|
+
**After editing config:**
|
|
259
|
+
```bash
|
|
260
|
+
agend reload # hot-reload (SIGHUP) — adds/removes instances without restart
|
|
261
|
+
agend fleet restart # if channel/defaults changed — needs full restart
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## 13. What NOT to Do (Dangerous Operations)
|
|
265
|
+
|
|
266
|
+
- **Don't delete `~/.agend/fleet.yaml`** while fleet is running
|
|
267
|
+
- **Don't delete `~/.agend/fleet.pid`** manually — use `agend fleet stop`
|
|
268
|
+
- **Don't kill tmux server** (`tmux kill-server`) — kills all agent sessions
|
|
269
|
+
- **Don't edit instance output.log** — it's actively written by the daemon
|
|
270
|
+
- **Don't run two fleet processes** on the same AGEND_HOME — port/socket conflicts
|
|
271
|
+
- **Don't change `channel.group_id`** without re-creating all topics — routing breaks
|
|
272
|
+
- **Don't remove an instance from fleet.yaml** that has active work — stop it first
|
package/dist/logger.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import pino from "pino";
|
|
2
|
-
/** Truncate log to tail when it exceeds MAX_LOG_SIZE (no rotation/backup files) */
|
|
3
|
-
export declare function truncateLogIfNeeded(logPath: string): void;
|
|
4
2
|
/** Rotate a log file: foo.log → foo.log.1 → foo.log.2 → foo.log.3 (deleted) */
|
|
5
3
|
export declare function rotateLogIfNeeded(logPath: string, maxSize?: number, maxFiles?: number): void;
|
|
6
4
|
export declare function createLogger(level?: string): pino.Logger<never, boolean>;
|
package/dist/logger.js
CHANGED
|
@@ -1,25 +1,11 @@
|
|
|
1
1
|
import pino from "pino";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { mkdirSync, statSync,
|
|
3
|
+
import { mkdirSync, statSync, existsSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { getAgendHome } from "./paths.js";
|
|
5
5
|
const DATA_DIR = getAgendHome();
|
|
6
6
|
const LOG_FILE = join(DATA_DIR, "daemon.log");
|
|
7
7
|
const MAX_LOG_SIZE = 10 * 1024 * 1024; // 10 MB
|
|
8
|
-
const TRUNCATE_TO = 5 * 1024 * 1024; // keep last 5 MB
|
|
9
8
|
const ROTATE_MAX_FILES = 3;
|
|
10
|
-
/** Truncate log to tail when it exceeds MAX_LOG_SIZE (no rotation/backup files) */
|
|
11
|
-
export function truncateLogIfNeeded(logPath) {
|
|
12
|
-
try {
|
|
13
|
-
const stat = statSync(logPath);
|
|
14
|
-
if (stat.size < MAX_LOG_SIZE)
|
|
15
|
-
return;
|
|
16
|
-
const buf = readFileSync(logPath);
|
|
17
|
-
const tail = buf.subarray(buf.length - TRUNCATE_TO);
|
|
18
|
-
const nl = tail.indexOf(0x0a);
|
|
19
|
-
writeFileSync(logPath, nl >= 0 ? tail.subarray(nl + 1) : tail);
|
|
20
|
-
}
|
|
21
|
-
catch { /* file may not exist yet */ }
|
|
22
|
-
}
|
|
23
9
|
/** Rotate a log file: foo.log → foo.log.1 → foo.log.2 → foo.log.3 (deleted) */
|
|
24
10
|
export function rotateLogIfNeeded(logPath, maxSize = MAX_LOG_SIZE, maxFiles = ROTATE_MAX_FILES) {
|
|
25
11
|
try {
|
|
@@ -52,7 +38,7 @@ export function rotateLogIfNeeded(logPath, maxSize = MAX_LOG_SIZE, maxFiles = RO
|
|
|
52
38
|
}
|
|
53
39
|
export function createLogger(level = "info") {
|
|
54
40
|
mkdirSync(DATA_DIR, { recursive: true });
|
|
55
|
-
|
|
41
|
+
rotateLogIfNeeded(LOG_FILE);
|
|
56
42
|
return pino({
|
|
57
43
|
level,
|
|
58
44
|
transport: {
|
package/dist/logger.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACjG,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1C,MAAM,QAAQ,GAAG,YAAY,EAAE,CAAC;AAChC,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;AAC9C,MAAM,YAAY,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,QAAQ;AAC/C,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAE3B,+EAA+E;AAC/E,MAAM,UAAU,iBAAiB,CAAC,OAAe,EAAE,OAAO,GAAG,YAAY,EAAE,QAAQ,GAAG,gBAAgB;IACpG,IAAI,CAAC;QACH,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC;YAAE,OAAO;QACjC,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;QAC/B,IAAI,IAAI,CAAC,IAAI,GAAG,OAAO;YAAE,OAAO;QAEhC,+BAA+B;QAC/B,KAAK,IAAI,CAAC,GAAG,QAAQ,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YACnC,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YACtD,MAAM,GAAG,GAAG,GAAG,OAAO,IAAI,CAAC,EAAE,CAAC;YAC9B,IAAI,CAAC,KAAK,QAAQ,EAAE,CAAC;gBAAC,IAAI,CAAC;oBAAC,UAAU,CAAC,GAAG,CAAC,CAAC;gBAAC,CAAC;gBAAC,MAAM,CAAC,CAAA,CAAC;YAAC,CAAC;YACzD,IAAI,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;gBAAC,IAAI,CAAC;oBAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;gBAAC,CAAC;gBAAC,MAAM,CAAC,CAAA,CAAC;YAAC,CAAC;QACjE,CAAC;QACD,uBAAuB;QACvB,aAAa,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC,CAAC,iBAAiB,CAAC,CAAC;AAC/B,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,QAAgB,MAAM;IACjD,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACzC,iBAAiB,CAAC,QAAQ,CAAC,CAAC;IAC5B,OAAO,IAAI,CAAC;QACV,KAAK;QACL,SAAS,EAAE;YACT,OAAO,EAAE;gBACP;oBACE,MAAM,EAAE,aAAa;oBACrB,OAAO,EAAE;wBACP,WAAW,EAAE,CAAC;wBACd,QAAQ,EAAE,IAAI;wBACd,aAAa,EAAE,cAAc;wBAC7B,MAAM,EAAE,cAAc;qBACvB;oBACD,KAAK;iBACN;gBACD;oBACE,MAAM,EAAE,aAAa;oBACrB,OAAO,EAAE;wBACP,WAAW,EAAE,QAAQ;wBACrB,QAAQ,EAAE,KAAK;wBACf,aAAa,EAAE,cAAc;wBAC7B,MAAM,EAAE,cAAc;qBACvB;oBACD,KAAK;iBACN;aACF;SACF;KACF,CAAC,CAAC;AACL,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@songsid/agend",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Multi-agent fleet daemon
|
|
3
|
+
"version": "0.0.6",
|
|
4
|
+
"description": "Multi-agent fleet daemon — run any coding CLI (Claude, Gemini, Codex, OpenCode) from Telegram",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"workspaces": [
|
|
7
|
-
"plugins/*"
|
|
8
|
-
],
|
|
6
|
+
"workspaces": ["plugins/*"],
|
|
9
7
|
"bin": {
|
|
10
8
|
"agend": "./dist/cli.js",
|
|
11
9
|
"agend-agent": "./dist/agent-cli.js"
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Activity / fleet dashboard HTML served by the daemon's health server.
|
|
3
|
-
* Pure constant — extracted from fleet-manager.ts to keep that module under
|
|
4
|
-
* a manageable size (P4.1).
|
|
5
|
-
*/
|
|
6
|
-
export declare const ACTIVITY_VIEWER_HTML = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>AgEnD Activity Viewer</title>\n<script src=\"https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js\"></script>\n<style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body { background: #0d1117; color: #c9d1d9; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', monospace; }\n .header { padding: 16px 24px; border-bottom: 1px solid #21262d; display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }\n .header h1 { font-size: 18px; color: #58a6ff; font-weight: 600; }\n .controls { display: flex; gap: 8px; align-items: center; }\n .controls select, .controls button { background: #21262d; color: #c9d1d9; border: 1px solid #30363d; border-radius: 6px; padding: 4px 10px; font-size: 13px; cursor: pointer; }\n .controls button.active { background: #1f6feb; border-color: #1f6feb; color: #fff; }\n .controls button:hover { border-color: #58a6ff; }\n .speed-group { display: flex; gap: 2px; }\n .speed-group button { border-radius: 0; }\n .speed-group button:first-child { border-radius: 6px 0 0 6px; }\n .speed-group button:last-child { border-radius: 0 6px 6px 0; }\n .status { font-size: 12px; color: #8b949e; margin-left: auto; }\n #diagram { padding: 24px; overflow-x: auto; }\n #diagram .mermaid { background: transparent; }\n #diagram svg { max-width: 100%; }\n .feed { padding: 12px 24px; max-height: 300px; overflow-y: auto; border-top: 1px solid #21262d; font-size: 13px; line-height: 1.8; }\n .feed-line { opacity: 0.6; }\n .feed-line.visible { opacity: 1; }\n .feed-line .time { color: #8b949e; }\n .feed-line .msg { color: #58a6ff; }\n .feed-line .tool { color: #d29922; }\n .feed-line .task { color: #3fb950; }\n /* Agent Board */\n .board { padding: 16px 24px; display: flex; gap: 12px; flex-wrap: wrap; border-bottom: 1px solid #21262d; }\n .card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 12px 14px; min-width: 200px; flex: 1; max-width: 280px; transition: border-color 0.3s; }\n .card.flash { border-color: #58a6ff; }\n .card-header { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; }\n .card-header .dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }\n .card-header .dot.running { background: #3fb950; }\n .card-header .dot.stopped { background: #8b949e; }\n .card-header .dot.crashed { background: #f85149; }\n .card-header .name { font-weight: 600; font-size: 14px; }\n .card-row { font-size: 12px; color: #8b949e; line-height: 1.6; }\n .card-row span { color: #c9d1d9; }\n .card-task { font-size: 12px; color: #d29922; margin-top: 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n .board-empty { font-size: 13px; color: #8b949e; padding: 8px 0; }\n .section-label { font-size: 11px; color: #484f58; text-transform: uppercase; letter-spacing: 1px; padding: 10px 24px 0; }\n .tabs { display: flex; gap: 0; padding: 0 24px; border-bottom: 1px solid #21262d; }\n .tab { padding: 8px 16px; font-size: 13px; color: #8b949e; cursor: pointer; border: none; border-bottom: 2px solid transparent; background: none; }\n .tab.active { color: #58a6ff; border-bottom-color: #58a6ff; }\n .tab:hover { color: #c9d1d9; }\n .view { display: none; }\n .view.active { display: block; }\n #graphCanvas { width: 100%; background: #0d1117; display: block; }\n</style>\n</head>\n<body>\n<div class=\"header\">\n <h1>AgEnD Activity</h1>\n <div class=\"controls\">\n <select id=\"range\">\n <option value=\"1h\">1h</option>\n <option value=\"2h\" selected>2h</option>\n <option value=\"4h\">4h</option>\n <option value=\"8h\">8h</option>\n <option value=\"24h\">24h</option>\n </select>\n <button id=\"btnLoad\">Load</button>\n <button id=\"btnPlay\">\u25B6 Play</button>\n <button id=\"btnPause\" style=\"display:none\">\u23F8 Pause</button>\n <div class=\"speed-group\">\n <button class=\"speed\" data-speed=\"1\">1x</button>\n <button class=\"speed active\" data-speed=\"2\">2x</button>\n <button class=\"speed\" data-speed=\"5\">5x</button>\n <button class=\"speed\" data-speed=\"10\">10x</button>\n </div>\n </div>\n <div class=\"status\" id=\"status\">Ready</div>\n</div>\n<div class=\"section-label\">Agents</div>\n<div class=\"board\" id=\"board\"><div class=\"board-empty\">Loading...</div></div>\n<div class=\"tabs\">\n <button class=\"tab active\" data-view=\"graph\">Network Graph</button>\n <button class=\"tab\" data-view=\"seq\">Sequence Diagram</button>\n</div>\n<div id=\"viewGraph\" class=\"view active\"><canvas id=\"graphCanvas\" height=\"400\"></canvas></div>\n<div id=\"viewSeq\" class=\"view\"><div id=\"diagram\"><div class=\"mermaid\" id=\"mermaidEl\"></div></div></div>\n<div class=\"feed\" id=\"feed\"></div>\n\n<script>\nmermaid.initialize({ startOnLoad: false, theme: 'dark', sequence: { mirrorActors: false, messageAlign: 'left' } });\n\nlet rows = [];\nlet speed = 2;\nlet playing = false;\nlet playTimeout = null;\nlet visibleCount = 0;\n\ndocument.querySelectorAll('.speed').forEach(btn => {\n btn.addEventListener('click', () => {\n document.querySelectorAll('.speed').forEach(b => b.classList.remove('active'));\n btn.classList.add('active');\n speed = parseInt(btn.dataset.speed);\n });\n});\n\ndocument.getElementById('btnLoad').addEventListener('click', load);\ndocument.getElementById('btnPlay').addEventListener('click', startReplay);\ndocument.getElementById('btnPause').addEventListener('click', pauseReplay);\n\nasync function load() {\n const range = document.getElementById('range').value;\n document.getElementById('status').textContent = 'Loading...';\n try {\n const resp = await fetch('/api/activity?since=' + range + '&limit=500');\n rows = await resp.json();\n document.getElementById('status').textContent = rows.length + ' events loaded';\n visibleCount = rows.length;\n renderFull();\n } catch (e) {\n document.getElementById('status').textContent = 'Error: ' + e.message;\n }\n}\n\nfunction buildMermaid(entries) {\n const participants = new Set();\n entries.forEach(r => { participants.add(r.sender); if (r.receiver) participants.add(r.receiver); });\n const aliases = new Map();\n let idx = 0;\n participants.forEach(p => {\n const a = p.length > 12 ? String.fromCharCode(65 + idx++) : p;\n aliases.set(p, a);\n });\n\n let lines = ['sequenceDiagram'];\n aliases.forEach((a, p) => lines.push(' participant ' + a + ' as ' + p));\n\n entries.forEach(r => {\n const s = aliases.get(r.sender) || r.sender;\n const summary = (r.summary || '').replace(/\"/g, \"'\").slice(0, 80);\n if (r.event === 'tool_call') {\n lines.push(' Note over ' + s + ': \uD83D\uDD27 ' + summary);\n } else if (r.receiver) {\n const recv = aliases.get(r.receiver) || r.receiver;\n lines.push(' ' + s + '->>' + recv + ': ' + summary);\n } else {\n lines.push(' Note over ' + s + ': ' + summary);\n }\n });\n return lines.join('\\n');\n}\n\nasync function renderDiagram(entries) {\n const code = buildMermaid(entries);\n const el = document.getElementById('mermaidEl');\n el.removeAttribute('data-processed');\n el.innerHTML = code;\n try { await mermaid.run({ nodes: [el] }); } catch {}\n}\n\nfunction renderFeed(count) {\n const feed = document.getElementById('feed');\n feed.innerHTML = '';\n rows.forEach((r, i) => {\n const vis = i < count;\n const time = (r.timestamp || '').replace('T', ' ').slice(11, 19);\n const icon = r.event === 'message' ? '\uD83D\uDCAC' : r.event === 'tool_call' ? '\uD83D\uDD27' : '\uD83D\uDCCB';\n const cls = r.event === 'tool_call' ? 'tool' : r.event === 'task_update' ? 'task' : 'msg';\n const arrow = r.receiver ? r.sender + ' \u2192 ' + r.receiver : r.sender;\n const line = document.createElement('div');\n line.className = 'feed-line' + (vis ? ' visible' : '');\n line.innerHTML = '<span class=\"time\">' + time + '</span> ' + icon + ' <span class=\"' + cls + '\">' + arrow + ': ' + (r.summary || '') + '</span>';\n feed.appendChild(line);\n });\n if (count > 0) feed.lastElementChild?.scrollIntoView({ behavior: 'smooth' });\n}\n\nfunction renderFull() {\n visibleCount = rows.length;\n renderDiagram(rows);\n renderFeed(rows.length);\n}\n\nfunction startReplay() {\n playing = true;\n visibleCount = 0;\n document.getElementById('btnPlay').style.display = 'none';\n document.getElementById('btnPause').style.display = '';\n stepReplay();\n}\n\nfunction pauseReplay() {\n playing = false;\n if (playTimeout) clearTimeout(playTimeout);\n document.getElementById('btnPlay').style.display = '';\n document.getElementById('btnPause').style.display = 'none';\n}\n\nfunction stepReplay() {\n if (!playing || visibleCount >= rows.length) {\n pauseReplay();\n document.getElementById('status').textContent = 'Replay complete';\n return;\n }\n visibleCount++;\n const visible = rows.slice(0, visibleCount);\n renderDiagram(visible);\n renderFeed(visibleCount);\n document.getElementById('status').textContent = visibleCount + '/' + rows.length;\n\n // Calculate delay from real timestamps\n let delayMs = 500;\n if (visibleCount < rows.length) {\n const curr = new Date(rows[visibleCount - 1].timestamp).getTime();\n const next = new Date(rows[visibleCount].timestamp).getTime();\n delayMs = Math.max(100, Math.min(3000, (next - curr) / speed));\n }\n playTimeout = setTimeout(stepReplay, delayMs);\n}\n\n// \u2500\u2500 Tab switching \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\ndocument.querySelectorAll('.tab').forEach(tab => {\n tab.addEventListener('click', () => {\n document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));\n document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));\n tab.classList.add('active');\n document.getElementById('view' + (tab.dataset.view === 'graph' ? 'Graph' : 'Seq')).classList.add('active');\n if (tab.dataset.view === 'graph') resizeCanvas();\n });\n});\n\n// \u2500\u2500 Network Graph \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst canvas = document.getElementById('graphCanvas');\nconst ctx2d = canvas.getContext('2d');\nlet graphNodes = []; // {name, x, y, color, isGeneral}\nlet graphEdges = new Map(); // \"a->b\" \u2192 {from, to}\nlet pulses = []; // {fromX, fromY, toX, toY, progress, color}\n\nfunction resizeCanvas() {\n canvas.width = canvas.parentElement.offsetWidth;\n canvas.height = 400;\n layoutNodes();\n}\n\nfunction layoutNodes() {\n if (graphNodes.length === 0) return;\n const cx = canvas.width / 2;\n const cy = canvas.height / 2;\n const radius = Math.min(cx, cy) - 60;\n // Find general (center)\n const general = graphNodes.find(n => n.isGeneral);\n const others = graphNodes.filter(n => !n.isGeneral);\n if (general) { general.x = cx; general.y = cy; }\n others.forEach((n, i) => {\n const angle = (2 * Math.PI * i / others.length) - Math.PI / 2;\n n.x = cx + radius * Math.cos(angle);\n n.y = cy + radius * Math.sin(angle);\n });\n}\n\nfunction updateGraphFromFleet(data) {\n const names = new Set();\n data.instances.forEach(inst => names.add(inst.name));\n // Add user node if activity mentions it\n rows.forEach(r => { names.add(r.sender); if (r.receiver) names.add(r.receiver); });\n // Rebuild nodes (preserve positions if same set)\n const oldMap = new Map(graphNodes.map(n => [n.name, n]));\n graphNodes = [...names].map(name => {\n const old = oldMap.get(name);\n const inst = data.instances.find(i => i.name === name);\n const color = !inst ? '#8b949e' : inst.status === 'running' ? '#3fb950' : inst.status === 'crashed' ? '#f85149' : '#484f58';\n return { name, x: old?.x ?? 0, y: old?.y ?? 0, color, isGeneral: inst?.general_topic ?? false };\n });\n layoutNodes();\n // Build edges from activity\n graphEdges.clear();\n rows.forEach(r => {\n if (r.receiver && r.event === 'message') {\n const key = r.sender + '->' + r.receiver;\n graphEdges.set(key, { from: r.sender, to: r.receiver });\n }\n });\n}\n\nfunction spawnPulse(sender, receiver, event) {\n const from = graphNodes.find(n => n.name === sender);\n const to = graphNodes.find(n => n.name === (receiver || sender));\n if (!from || !to) return;\n const colors = { message: '#58a6ff', tool_call: '#d29922', task_update: '#3fb950' };\n pulses.push({ fromX: from.x, fromY: from.y, toX: to.x, toY: to.y, progress: 0, color: colors[event] || '#58a6ff' });\n}\n\nfunction drawGraph() {\n if (!ctx2d) return;\n ctx2d.clearRect(0, 0, canvas.width, canvas.height);\n // Draw edges\n ctx2d.strokeStyle = '#21262d';\n ctx2d.lineWidth = 1;\n graphEdges.forEach(e => {\n const from = graphNodes.find(n => n.name === e.from);\n const to = graphNodes.find(n => n.name === e.to);\n if (from && to) {\n ctx2d.beginPath();\n ctx2d.moveTo(from.x, from.y);\n ctx2d.lineTo(to.x, to.y);\n ctx2d.stroke();\n }\n });\n // Draw pulses\n pulses = pulses.filter(p => p.progress <= 1);\n pulses.forEach(p => {\n p.progress += 0.02;\n const x = p.fromX + (p.toX - p.fromX) * p.progress;\n const y = p.fromY + (p.toY - p.fromY) * p.progress;\n ctx2d.beginPath();\n ctx2d.arc(x, y, 5, 0, Math.PI * 2);\n ctx2d.fillStyle = p.color;\n ctx2d.shadowColor = p.color;\n ctx2d.shadowBlur = 12;\n ctx2d.fill();\n ctx2d.shadowBlur = 0;\n });\n // Draw nodes\n graphNodes.forEach(n => {\n // Glow\n ctx2d.beginPath();\n ctx2d.arc(n.x, n.y, n.isGeneral ? 28 : 22, 0, Math.PI * 2);\n ctx2d.fillStyle = n.color + '22';\n ctx2d.fill();\n // Circle\n ctx2d.beginPath();\n ctx2d.arc(n.x, n.y, n.isGeneral ? 24 : 18, 0, Math.PI * 2);\n ctx2d.fillStyle = '#161b22';\n ctx2d.strokeStyle = n.color;\n ctx2d.lineWidth = 2;\n ctx2d.fill();\n ctx2d.stroke();\n // Label\n ctx2d.fillStyle = '#c9d1d9';\n ctx2d.font = (n.isGeneral ? '12' : '11') + 'px -apple-system, monospace';\n ctx2d.textAlign = 'center';\n ctx2d.fillText(n.name.length > 14 ? n.name.slice(0, 12) + '..' : n.name, n.x, n.y + (n.isGeneral ? 38 : 32));\n });\n requestAnimationFrame(drawGraph);\n}\n\n// Hook into replay: spawn pulses when stepping\nconst origStep = stepReplay;\nstepReplay = function() {\n const prevCount = visibleCount;\n origStep();\n if (visibleCount > prevCount && visibleCount <= rows.length) {\n const r = rows[visibleCount - 1];\n spawnPulse(r.sender, r.receiver, r.event);\n }\n};\n\n// Hook into full load: spawn pulses for all visible events on load\nconst origRenderFull = renderFull;\nrenderFull = function() {\n origRenderFull();\n // Update graph nodes from fleet data (if available)\n fetch('/api/fleet').then(r => r.json()).then(data => {\n updateGraphFromFleet(data);\n }).catch(() => {\n // Fallback: build nodes from activity only\n const names = new Set();\n rows.forEach(r => { names.add(r.sender); if (r.receiver) names.add(r.receiver); });\n graphNodes = [...names].map(n => ({ name: n, x: 0, y: 0, color: '#8b949e', isGeneral: n === 'general' }));\n layoutNodes();\n });\n};\n\nresizeCanvas();\nwindow.addEventListener('resize', resizeCanvas);\nrequestAnimationFrame(drawGraph);\n\n// \u2500\u2500 Agent Board \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nlet prevBoard = '';\n\nasync function loadBoard() {\n try {\n const resp = await fetch('/api/fleet');\n const data = await resp.json();\n renderBoard(data);\n } catch {}\n}\n\nfunction renderBoard(data) {\n const board = document.getElementById('board');\n const cards = data.instances.map(inst => {\n const statusDot = inst.status === 'running' ? 'running' : inst.status === 'crashed' ? 'crashed' : 'stopped';\n const icon = inst.status === 'running' ? '\uD83D\uDFE2' : inst.status === 'crashed' ? '\uD83D\uDD34' : '\u26AA';\n const role = inst.general_topic ? 'coordinator' : inst.description || 'worker';\n const costStr = '$' + (inst.costCents / 100).toFixed(2);\n const lastMs = inst.lastActivity;\n let lastStr = '\u2014';\n if (lastMs) {\n const ago = Math.floor((Date.now() - lastMs) / 1000);\n lastStr = ago < 60 ? ago + 's ago' : ago < 3600 ? Math.floor(ago/60) + 'm ago' : Math.floor(ago/3600) + 'h ago';\n }\n const ipc = inst.ipc ? '\u2713' : '\u2717';\n const rl = inst.rateLimits ? ' \u00B7 5h:' + inst.rateLimits.five_hour_pct + '%' : '';\n const taskLine = inst.currentTask\n ? '<div class=\"card-task\">\uD83D\uDCCC ' + inst.currentTask + '</div>'\n : '<div class=\"card-task\" style=\"color:#484f58\">(idle)</div>';\n return '<div class=\"card\" data-name=\"' + inst.name + '\">' +\n '<div class=\"card-header\"><div class=\"dot ' + statusDot + '\"></div><div class=\"name\">' + inst.name + '</div></div>' +\n '<div class=\"card-row\">' + role.slice(0, 30) + '</div>' +\n '<div class=\"card-row\">Backend: <span>' + inst.backend + '</span> \u00B7 Tools: <span>' + inst.tool_set + '</span></div>' +\n '<div class=\"card-row\">IPC: <span>' + ipc + '</span> \u00B7 Cost: <span>' + costStr + '</span>' + rl + '</div>' +\n '<div class=\"card-row\">Last: <span>' + lastStr + '</span></div>' +\n taskLine +\n '</div>';\n });\n\n const newHtml = cards.join('');\n if (newHtml !== prevBoard) {\n board.innerHTML = newHtml;\n // Flash changed cards\n board.querySelectorAll('.card').forEach(c => {\n c.classList.add('flash');\n setTimeout(() => c.classList.remove('flash'), 1000);\n });\n prevBoard = newHtml;\n }\n}\n\n// Auto-refresh board every 10s\nsetInterval(loadBoard, 10000);\n\n// Auto-load on page open\nloadBoard();\nload();\n</script>\n</body>\n</html>";
|
|
@@ -1,443 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Activity / fleet dashboard HTML served by the daemon's health server.
|
|
3
|
-
* Pure constant — extracted from fleet-manager.ts to keep that module under
|
|
4
|
-
* a manageable size (P4.1).
|
|
5
|
-
*/
|
|
6
|
-
export const ACTIVITY_VIEWER_HTML = `<!DOCTYPE html>
|
|
7
|
-
<html lang="en">
|
|
8
|
-
<head>
|
|
9
|
-
<meta charset="utf-8">
|
|
10
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
11
|
-
<title>AgEnD Activity Viewer</title>
|
|
12
|
-
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
|
|
13
|
-
<style>
|
|
14
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
15
|
-
body { background: #0d1117; color: #c9d1d9; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', monospace; }
|
|
16
|
-
.header { padding: 16px 24px; border-bottom: 1px solid #21262d; display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
|
|
17
|
-
.header h1 { font-size: 18px; color: #58a6ff; font-weight: 600; }
|
|
18
|
-
.controls { display: flex; gap: 8px; align-items: center; }
|
|
19
|
-
.controls select, .controls button { background: #21262d; color: #c9d1d9; border: 1px solid #30363d; border-radius: 6px; padding: 4px 10px; font-size: 13px; cursor: pointer; }
|
|
20
|
-
.controls button.active { background: #1f6feb; border-color: #1f6feb; color: #fff; }
|
|
21
|
-
.controls button:hover { border-color: #58a6ff; }
|
|
22
|
-
.speed-group { display: flex; gap: 2px; }
|
|
23
|
-
.speed-group button { border-radius: 0; }
|
|
24
|
-
.speed-group button:first-child { border-radius: 6px 0 0 6px; }
|
|
25
|
-
.speed-group button:last-child { border-radius: 0 6px 6px 0; }
|
|
26
|
-
.status { font-size: 12px; color: #8b949e; margin-left: auto; }
|
|
27
|
-
#diagram { padding: 24px; overflow-x: auto; }
|
|
28
|
-
#diagram .mermaid { background: transparent; }
|
|
29
|
-
#diagram svg { max-width: 100%; }
|
|
30
|
-
.feed { padding: 12px 24px; max-height: 300px; overflow-y: auto; border-top: 1px solid #21262d; font-size: 13px; line-height: 1.8; }
|
|
31
|
-
.feed-line { opacity: 0.6; }
|
|
32
|
-
.feed-line.visible { opacity: 1; }
|
|
33
|
-
.feed-line .time { color: #8b949e; }
|
|
34
|
-
.feed-line .msg { color: #58a6ff; }
|
|
35
|
-
.feed-line .tool { color: #d29922; }
|
|
36
|
-
.feed-line .task { color: #3fb950; }
|
|
37
|
-
/* Agent Board */
|
|
38
|
-
.board { padding: 16px 24px; display: flex; gap: 12px; flex-wrap: wrap; border-bottom: 1px solid #21262d; }
|
|
39
|
-
.card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 12px 14px; min-width: 200px; flex: 1; max-width: 280px; transition: border-color 0.3s; }
|
|
40
|
-
.card.flash { border-color: #58a6ff; }
|
|
41
|
-
.card-header { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; }
|
|
42
|
-
.card-header .dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
|
43
|
-
.card-header .dot.running { background: #3fb950; }
|
|
44
|
-
.card-header .dot.stopped { background: #8b949e; }
|
|
45
|
-
.card-header .dot.crashed { background: #f85149; }
|
|
46
|
-
.card-header .name { font-weight: 600; font-size: 14px; }
|
|
47
|
-
.card-row { font-size: 12px; color: #8b949e; line-height: 1.6; }
|
|
48
|
-
.card-row span { color: #c9d1d9; }
|
|
49
|
-
.card-task { font-size: 12px; color: #d29922; margin-top: 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
50
|
-
.board-empty { font-size: 13px; color: #8b949e; padding: 8px 0; }
|
|
51
|
-
.section-label { font-size: 11px; color: #484f58; text-transform: uppercase; letter-spacing: 1px; padding: 10px 24px 0; }
|
|
52
|
-
.tabs { display: flex; gap: 0; padding: 0 24px; border-bottom: 1px solid #21262d; }
|
|
53
|
-
.tab { padding: 8px 16px; font-size: 13px; color: #8b949e; cursor: pointer; border: none; border-bottom: 2px solid transparent; background: none; }
|
|
54
|
-
.tab.active { color: #58a6ff; border-bottom-color: #58a6ff; }
|
|
55
|
-
.tab:hover { color: #c9d1d9; }
|
|
56
|
-
.view { display: none; }
|
|
57
|
-
.view.active { display: block; }
|
|
58
|
-
#graphCanvas { width: 100%; background: #0d1117; display: block; }
|
|
59
|
-
</style>
|
|
60
|
-
</head>
|
|
61
|
-
<body>
|
|
62
|
-
<div class="header">
|
|
63
|
-
<h1>AgEnD Activity</h1>
|
|
64
|
-
<div class="controls">
|
|
65
|
-
<select id="range">
|
|
66
|
-
<option value="1h">1h</option>
|
|
67
|
-
<option value="2h" selected>2h</option>
|
|
68
|
-
<option value="4h">4h</option>
|
|
69
|
-
<option value="8h">8h</option>
|
|
70
|
-
<option value="24h">24h</option>
|
|
71
|
-
</select>
|
|
72
|
-
<button id="btnLoad">Load</button>
|
|
73
|
-
<button id="btnPlay">▶ Play</button>
|
|
74
|
-
<button id="btnPause" style="display:none">⏸ Pause</button>
|
|
75
|
-
<div class="speed-group">
|
|
76
|
-
<button class="speed" data-speed="1">1x</button>
|
|
77
|
-
<button class="speed active" data-speed="2">2x</button>
|
|
78
|
-
<button class="speed" data-speed="5">5x</button>
|
|
79
|
-
<button class="speed" data-speed="10">10x</button>
|
|
80
|
-
</div>
|
|
81
|
-
</div>
|
|
82
|
-
<div class="status" id="status">Ready</div>
|
|
83
|
-
</div>
|
|
84
|
-
<div class="section-label">Agents</div>
|
|
85
|
-
<div class="board" id="board"><div class="board-empty">Loading...</div></div>
|
|
86
|
-
<div class="tabs">
|
|
87
|
-
<button class="tab active" data-view="graph">Network Graph</button>
|
|
88
|
-
<button class="tab" data-view="seq">Sequence Diagram</button>
|
|
89
|
-
</div>
|
|
90
|
-
<div id="viewGraph" class="view active"><canvas id="graphCanvas" height="400"></canvas></div>
|
|
91
|
-
<div id="viewSeq" class="view"><div id="diagram"><div class="mermaid" id="mermaidEl"></div></div></div>
|
|
92
|
-
<div class="feed" id="feed"></div>
|
|
93
|
-
|
|
94
|
-
<script>
|
|
95
|
-
mermaid.initialize({ startOnLoad: false, theme: 'dark', sequence: { mirrorActors: false, messageAlign: 'left' } });
|
|
96
|
-
|
|
97
|
-
let rows = [];
|
|
98
|
-
let speed = 2;
|
|
99
|
-
let playing = false;
|
|
100
|
-
let playTimeout = null;
|
|
101
|
-
let visibleCount = 0;
|
|
102
|
-
|
|
103
|
-
document.querySelectorAll('.speed').forEach(btn => {
|
|
104
|
-
btn.addEventListener('click', () => {
|
|
105
|
-
document.querySelectorAll('.speed').forEach(b => b.classList.remove('active'));
|
|
106
|
-
btn.classList.add('active');
|
|
107
|
-
speed = parseInt(btn.dataset.speed);
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
document.getElementById('btnLoad').addEventListener('click', load);
|
|
112
|
-
document.getElementById('btnPlay').addEventListener('click', startReplay);
|
|
113
|
-
document.getElementById('btnPause').addEventListener('click', pauseReplay);
|
|
114
|
-
|
|
115
|
-
async function load() {
|
|
116
|
-
const range = document.getElementById('range').value;
|
|
117
|
-
document.getElementById('status').textContent = 'Loading...';
|
|
118
|
-
try {
|
|
119
|
-
const resp = await fetch('/api/activity?since=' + range + '&limit=500');
|
|
120
|
-
rows = await resp.json();
|
|
121
|
-
document.getElementById('status').textContent = rows.length + ' events loaded';
|
|
122
|
-
visibleCount = rows.length;
|
|
123
|
-
renderFull();
|
|
124
|
-
} catch (e) {
|
|
125
|
-
document.getElementById('status').textContent = 'Error: ' + e.message;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function buildMermaid(entries) {
|
|
130
|
-
const participants = new Set();
|
|
131
|
-
entries.forEach(r => { participants.add(r.sender); if (r.receiver) participants.add(r.receiver); });
|
|
132
|
-
const aliases = new Map();
|
|
133
|
-
let idx = 0;
|
|
134
|
-
participants.forEach(p => {
|
|
135
|
-
const a = p.length > 12 ? String.fromCharCode(65 + idx++) : p;
|
|
136
|
-
aliases.set(p, a);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
let lines = ['sequenceDiagram'];
|
|
140
|
-
aliases.forEach((a, p) => lines.push(' participant ' + a + ' as ' + p));
|
|
141
|
-
|
|
142
|
-
entries.forEach(r => {
|
|
143
|
-
const s = aliases.get(r.sender) || r.sender;
|
|
144
|
-
const summary = (r.summary || '').replace(/"/g, "'").slice(0, 80);
|
|
145
|
-
if (r.event === 'tool_call') {
|
|
146
|
-
lines.push(' Note over ' + s + ': 🔧 ' + summary);
|
|
147
|
-
} else if (r.receiver) {
|
|
148
|
-
const recv = aliases.get(r.receiver) || r.receiver;
|
|
149
|
-
lines.push(' ' + s + '->>' + recv + ': ' + summary);
|
|
150
|
-
} else {
|
|
151
|
-
lines.push(' Note over ' + s + ': ' + summary);
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
return lines.join('\\n');
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
async function renderDiagram(entries) {
|
|
158
|
-
const code = buildMermaid(entries);
|
|
159
|
-
const el = document.getElementById('mermaidEl');
|
|
160
|
-
el.removeAttribute('data-processed');
|
|
161
|
-
el.innerHTML = code;
|
|
162
|
-
try { await mermaid.run({ nodes: [el] }); } catch {}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function renderFeed(count) {
|
|
166
|
-
const feed = document.getElementById('feed');
|
|
167
|
-
feed.innerHTML = '';
|
|
168
|
-
rows.forEach((r, i) => {
|
|
169
|
-
const vis = i < count;
|
|
170
|
-
const time = (r.timestamp || '').replace('T', ' ').slice(11, 19);
|
|
171
|
-
const icon = r.event === 'message' ? '💬' : r.event === 'tool_call' ? '🔧' : '📋';
|
|
172
|
-
const cls = r.event === 'tool_call' ? 'tool' : r.event === 'task_update' ? 'task' : 'msg';
|
|
173
|
-
const arrow = r.receiver ? r.sender + ' → ' + r.receiver : r.sender;
|
|
174
|
-
const line = document.createElement('div');
|
|
175
|
-
line.className = 'feed-line' + (vis ? ' visible' : '');
|
|
176
|
-
line.innerHTML = '<span class="time">' + time + '</span> ' + icon + ' <span class="' + cls + '">' + arrow + ': ' + (r.summary || '') + '</span>';
|
|
177
|
-
feed.appendChild(line);
|
|
178
|
-
});
|
|
179
|
-
if (count > 0) feed.lastElementChild?.scrollIntoView({ behavior: 'smooth' });
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function renderFull() {
|
|
183
|
-
visibleCount = rows.length;
|
|
184
|
-
renderDiagram(rows);
|
|
185
|
-
renderFeed(rows.length);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function startReplay() {
|
|
189
|
-
playing = true;
|
|
190
|
-
visibleCount = 0;
|
|
191
|
-
document.getElementById('btnPlay').style.display = 'none';
|
|
192
|
-
document.getElementById('btnPause').style.display = '';
|
|
193
|
-
stepReplay();
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function pauseReplay() {
|
|
197
|
-
playing = false;
|
|
198
|
-
if (playTimeout) clearTimeout(playTimeout);
|
|
199
|
-
document.getElementById('btnPlay').style.display = '';
|
|
200
|
-
document.getElementById('btnPause').style.display = 'none';
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function stepReplay() {
|
|
204
|
-
if (!playing || visibleCount >= rows.length) {
|
|
205
|
-
pauseReplay();
|
|
206
|
-
document.getElementById('status').textContent = 'Replay complete';
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
visibleCount++;
|
|
210
|
-
const visible = rows.slice(0, visibleCount);
|
|
211
|
-
renderDiagram(visible);
|
|
212
|
-
renderFeed(visibleCount);
|
|
213
|
-
document.getElementById('status').textContent = visibleCount + '/' + rows.length;
|
|
214
|
-
|
|
215
|
-
// Calculate delay from real timestamps
|
|
216
|
-
let delayMs = 500;
|
|
217
|
-
if (visibleCount < rows.length) {
|
|
218
|
-
const curr = new Date(rows[visibleCount - 1].timestamp).getTime();
|
|
219
|
-
const next = new Date(rows[visibleCount].timestamp).getTime();
|
|
220
|
-
delayMs = Math.max(100, Math.min(3000, (next - curr) / speed));
|
|
221
|
-
}
|
|
222
|
-
playTimeout = setTimeout(stepReplay, delayMs);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// ── Tab switching ────────────────────────────────
|
|
226
|
-
document.querySelectorAll('.tab').forEach(tab => {
|
|
227
|
-
tab.addEventListener('click', () => {
|
|
228
|
-
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
229
|
-
document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
|
|
230
|
-
tab.classList.add('active');
|
|
231
|
-
document.getElementById('view' + (tab.dataset.view === 'graph' ? 'Graph' : 'Seq')).classList.add('active');
|
|
232
|
-
if (tab.dataset.view === 'graph') resizeCanvas();
|
|
233
|
-
});
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
// ── Network Graph ────────────────────────────────
|
|
237
|
-
const canvas = document.getElementById('graphCanvas');
|
|
238
|
-
const ctx2d = canvas.getContext('2d');
|
|
239
|
-
let graphNodes = []; // {name, x, y, color, isGeneral}
|
|
240
|
-
let graphEdges = new Map(); // "a->b" → {from, to}
|
|
241
|
-
let pulses = []; // {fromX, fromY, toX, toY, progress, color}
|
|
242
|
-
|
|
243
|
-
function resizeCanvas() {
|
|
244
|
-
canvas.width = canvas.parentElement.offsetWidth;
|
|
245
|
-
canvas.height = 400;
|
|
246
|
-
layoutNodes();
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function layoutNodes() {
|
|
250
|
-
if (graphNodes.length === 0) return;
|
|
251
|
-
const cx = canvas.width / 2;
|
|
252
|
-
const cy = canvas.height / 2;
|
|
253
|
-
const radius = Math.min(cx, cy) - 60;
|
|
254
|
-
// Find general (center)
|
|
255
|
-
const general = graphNodes.find(n => n.isGeneral);
|
|
256
|
-
const others = graphNodes.filter(n => !n.isGeneral);
|
|
257
|
-
if (general) { general.x = cx; general.y = cy; }
|
|
258
|
-
others.forEach((n, i) => {
|
|
259
|
-
const angle = (2 * Math.PI * i / others.length) - Math.PI / 2;
|
|
260
|
-
n.x = cx + radius * Math.cos(angle);
|
|
261
|
-
n.y = cy + radius * Math.sin(angle);
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function updateGraphFromFleet(data) {
|
|
266
|
-
const names = new Set();
|
|
267
|
-
data.instances.forEach(inst => names.add(inst.name));
|
|
268
|
-
// Add user node if activity mentions it
|
|
269
|
-
rows.forEach(r => { names.add(r.sender); if (r.receiver) names.add(r.receiver); });
|
|
270
|
-
// Rebuild nodes (preserve positions if same set)
|
|
271
|
-
const oldMap = new Map(graphNodes.map(n => [n.name, n]));
|
|
272
|
-
graphNodes = [...names].map(name => {
|
|
273
|
-
const old = oldMap.get(name);
|
|
274
|
-
const inst = data.instances.find(i => i.name === name);
|
|
275
|
-
const color = !inst ? '#8b949e' : inst.status === 'running' ? '#3fb950' : inst.status === 'crashed' ? '#f85149' : '#484f58';
|
|
276
|
-
return { name, x: old?.x ?? 0, y: old?.y ?? 0, color, isGeneral: inst?.general_topic ?? false };
|
|
277
|
-
});
|
|
278
|
-
layoutNodes();
|
|
279
|
-
// Build edges from activity
|
|
280
|
-
graphEdges.clear();
|
|
281
|
-
rows.forEach(r => {
|
|
282
|
-
if (r.receiver && r.event === 'message') {
|
|
283
|
-
const key = r.sender + '->' + r.receiver;
|
|
284
|
-
graphEdges.set(key, { from: r.sender, to: r.receiver });
|
|
285
|
-
}
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function spawnPulse(sender, receiver, event) {
|
|
290
|
-
const from = graphNodes.find(n => n.name === sender);
|
|
291
|
-
const to = graphNodes.find(n => n.name === (receiver || sender));
|
|
292
|
-
if (!from || !to) return;
|
|
293
|
-
const colors = { message: '#58a6ff', tool_call: '#d29922', task_update: '#3fb950' };
|
|
294
|
-
pulses.push({ fromX: from.x, fromY: from.y, toX: to.x, toY: to.y, progress: 0, color: colors[event] || '#58a6ff' });
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function drawGraph() {
|
|
298
|
-
if (!ctx2d) return;
|
|
299
|
-
ctx2d.clearRect(0, 0, canvas.width, canvas.height);
|
|
300
|
-
// Draw edges
|
|
301
|
-
ctx2d.strokeStyle = '#21262d';
|
|
302
|
-
ctx2d.lineWidth = 1;
|
|
303
|
-
graphEdges.forEach(e => {
|
|
304
|
-
const from = graphNodes.find(n => n.name === e.from);
|
|
305
|
-
const to = graphNodes.find(n => n.name === e.to);
|
|
306
|
-
if (from && to) {
|
|
307
|
-
ctx2d.beginPath();
|
|
308
|
-
ctx2d.moveTo(from.x, from.y);
|
|
309
|
-
ctx2d.lineTo(to.x, to.y);
|
|
310
|
-
ctx2d.stroke();
|
|
311
|
-
}
|
|
312
|
-
});
|
|
313
|
-
// Draw pulses
|
|
314
|
-
pulses = pulses.filter(p => p.progress <= 1);
|
|
315
|
-
pulses.forEach(p => {
|
|
316
|
-
p.progress += 0.02;
|
|
317
|
-
const x = p.fromX + (p.toX - p.fromX) * p.progress;
|
|
318
|
-
const y = p.fromY + (p.toY - p.fromY) * p.progress;
|
|
319
|
-
ctx2d.beginPath();
|
|
320
|
-
ctx2d.arc(x, y, 5, 0, Math.PI * 2);
|
|
321
|
-
ctx2d.fillStyle = p.color;
|
|
322
|
-
ctx2d.shadowColor = p.color;
|
|
323
|
-
ctx2d.shadowBlur = 12;
|
|
324
|
-
ctx2d.fill();
|
|
325
|
-
ctx2d.shadowBlur = 0;
|
|
326
|
-
});
|
|
327
|
-
// Draw nodes
|
|
328
|
-
graphNodes.forEach(n => {
|
|
329
|
-
// Glow
|
|
330
|
-
ctx2d.beginPath();
|
|
331
|
-
ctx2d.arc(n.x, n.y, n.isGeneral ? 28 : 22, 0, Math.PI * 2);
|
|
332
|
-
ctx2d.fillStyle = n.color + '22';
|
|
333
|
-
ctx2d.fill();
|
|
334
|
-
// Circle
|
|
335
|
-
ctx2d.beginPath();
|
|
336
|
-
ctx2d.arc(n.x, n.y, n.isGeneral ? 24 : 18, 0, Math.PI * 2);
|
|
337
|
-
ctx2d.fillStyle = '#161b22';
|
|
338
|
-
ctx2d.strokeStyle = n.color;
|
|
339
|
-
ctx2d.lineWidth = 2;
|
|
340
|
-
ctx2d.fill();
|
|
341
|
-
ctx2d.stroke();
|
|
342
|
-
// Label
|
|
343
|
-
ctx2d.fillStyle = '#c9d1d9';
|
|
344
|
-
ctx2d.font = (n.isGeneral ? '12' : '11') + 'px -apple-system, monospace';
|
|
345
|
-
ctx2d.textAlign = 'center';
|
|
346
|
-
ctx2d.fillText(n.name.length > 14 ? n.name.slice(0, 12) + '..' : n.name, n.x, n.y + (n.isGeneral ? 38 : 32));
|
|
347
|
-
});
|
|
348
|
-
requestAnimationFrame(drawGraph);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// Hook into replay: spawn pulses when stepping
|
|
352
|
-
const origStep = stepReplay;
|
|
353
|
-
stepReplay = function() {
|
|
354
|
-
const prevCount = visibleCount;
|
|
355
|
-
origStep();
|
|
356
|
-
if (visibleCount > prevCount && visibleCount <= rows.length) {
|
|
357
|
-
const r = rows[visibleCount - 1];
|
|
358
|
-
spawnPulse(r.sender, r.receiver, r.event);
|
|
359
|
-
}
|
|
360
|
-
};
|
|
361
|
-
|
|
362
|
-
// Hook into full load: spawn pulses for all visible events on load
|
|
363
|
-
const origRenderFull = renderFull;
|
|
364
|
-
renderFull = function() {
|
|
365
|
-
origRenderFull();
|
|
366
|
-
// Update graph nodes from fleet data (if available)
|
|
367
|
-
fetch('/api/fleet').then(r => r.json()).then(data => {
|
|
368
|
-
updateGraphFromFleet(data);
|
|
369
|
-
}).catch(() => {
|
|
370
|
-
// Fallback: build nodes from activity only
|
|
371
|
-
const names = new Set();
|
|
372
|
-
rows.forEach(r => { names.add(r.sender); if (r.receiver) names.add(r.receiver); });
|
|
373
|
-
graphNodes = [...names].map(n => ({ name: n, x: 0, y: 0, color: '#8b949e', isGeneral: n === 'general' }));
|
|
374
|
-
layoutNodes();
|
|
375
|
-
});
|
|
376
|
-
};
|
|
377
|
-
|
|
378
|
-
resizeCanvas();
|
|
379
|
-
window.addEventListener('resize', resizeCanvas);
|
|
380
|
-
requestAnimationFrame(drawGraph);
|
|
381
|
-
|
|
382
|
-
// ── Agent Board ──────────────────────────────────
|
|
383
|
-
|
|
384
|
-
let prevBoard = '';
|
|
385
|
-
|
|
386
|
-
async function loadBoard() {
|
|
387
|
-
try {
|
|
388
|
-
const resp = await fetch('/api/fleet');
|
|
389
|
-
const data = await resp.json();
|
|
390
|
-
renderBoard(data);
|
|
391
|
-
} catch {}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function renderBoard(data) {
|
|
395
|
-
const board = document.getElementById('board');
|
|
396
|
-
const cards = data.instances.map(inst => {
|
|
397
|
-
const statusDot = inst.status === 'running' ? 'running' : inst.status === 'crashed' ? 'crashed' : 'stopped';
|
|
398
|
-
const icon = inst.status === 'running' ? '🟢' : inst.status === 'crashed' ? '🔴' : '⚪';
|
|
399
|
-
const role = inst.general_topic ? 'coordinator' : inst.description || 'worker';
|
|
400
|
-
const costStr = '$' + (inst.costCents / 100).toFixed(2);
|
|
401
|
-
const lastMs = inst.lastActivity;
|
|
402
|
-
let lastStr = '—';
|
|
403
|
-
if (lastMs) {
|
|
404
|
-
const ago = Math.floor((Date.now() - lastMs) / 1000);
|
|
405
|
-
lastStr = ago < 60 ? ago + 's ago' : ago < 3600 ? Math.floor(ago/60) + 'm ago' : Math.floor(ago/3600) + 'h ago';
|
|
406
|
-
}
|
|
407
|
-
const ipc = inst.ipc ? '✓' : '✗';
|
|
408
|
-
const rl = inst.rateLimits ? ' · 5h:' + inst.rateLimits.five_hour_pct + '%' : '';
|
|
409
|
-
const taskLine = inst.currentTask
|
|
410
|
-
? '<div class="card-task">📌 ' + inst.currentTask + '</div>'
|
|
411
|
-
: '<div class="card-task" style="color:#484f58">(idle)</div>';
|
|
412
|
-
return '<div class="card" data-name="' + inst.name + '">' +
|
|
413
|
-
'<div class="card-header"><div class="dot ' + statusDot + '"></div><div class="name">' + inst.name + '</div></div>' +
|
|
414
|
-
'<div class="card-row">' + role.slice(0, 30) + '</div>' +
|
|
415
|
-
'<div class="card-row">Backend: <span>' + inst.backend + '</span> · Tools: <span>' + inst.tool_set + '</span></div>' +
|
|
416
|
-
'<div class="card-row">IPC: <span>' + ipc + '</span> · Cost: <span>' + costStr + '</span>' + rl + '</div>' +
|
|
417
|
-
'<div class="card-row">Last: <span>' + lastStr + '</span></div>' +
|
|
418
|
-
taskLine +
|
|
419
|
-
'</div>';
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
const newHtml = cards.join('');
|
|
423
|
-
if (newHtml !== prevBoard) {
|
|
424
|
-
board.innerHTML = newHtml;
|
|
425
|
-
// Flash changed cards
|
|
426
|
-
board.querySelectorAll('.card').forEach(c => {
|
|
427
|
-
c.classList.add('flash');
|
|
428
|
-
setTimeout(() => c.classList.remove('flash'), 1000);
|
|
429
|
-
});
|
|
430
|
-
prevBoard = newHtml;
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// Auto-refresh board every 10s
|
|
435
|
-
setInterval(loadBoard, 10000);
|
|
436
|
-
|
|
437
|
-
// Auto-load on page open
|
|
438
|
-
loadBoard();
|
|
439
|
-
load();
|
|
440
|
-
</script>
|
|
441
|
-
</body>
|
|
442
|
-
</html>`;
|
|
443
|
-
//# sourceMappingURL=fleet-dashboard-html.js.map
|