@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.
@@ -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, readFileSync, writeFileSync, renameSync, unlinkSync, existsSync } from "node:fs";
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
- truncateLogIfNeeded(LOG_FILE);
41
+ rotateLogIfNeeded(LOG_FILE);
56
42
  return pino({
57
43
  level,
58
44
  transport: {
@@ -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,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC/G,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,WAAW,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,iBAAiB;AACtD,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAE3B,mFAAmF;AACnF,MAAM,UAAU,mBAAmB,CAAC,OAAe;IACjD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;QAC/B,IAAI,IAAI,CAAC,IAAI,GAAG,YAAY;YAAE,OAAO;QAErC,MAAM,GAAG,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;QAClC,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,GAAG,WAAW,CAAC,CAAC;QACpD,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC9B,aAAa,CAAC,OAAO,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACjE,CAAC;IAAC,MAAM,CAAC,CAAC,4BAA4B,CAAC,CAAC;AAC1C,CAAC;AAED,+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,mBAAmB,CAAC,QAAQ,CAAC,CAAC;IAC9B,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"}
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",
4
- "description": "Multi-agent fleet daemon \u2014 run any coding CLI (Claude, Gemini, Codex, OpenCode) from Telegram",
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