@openduo/duoduo 0.3.1 → 0.3.2

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.
@@ -0,0 +1,621 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Duoduo ATC</title>
7
+ <style>
8
+ *{box-sizing:border-box;margin:0;padding:0}
9
+ :root{
10
+ --bg:#050a15;
11
+ --glass:rgba(10,17,35,0.6);
12
+ --border:rgba(0,240,255,0.15);
13
+ --cyan:#00f0ff;
14
+ --emerald:#10b981;
15
+ --amber:#f59e0b;
16
+ --fuchsia:#d946ef;
17
+ --red:#ef4444;
18
+ --blue:#3b82f6;
19
+ --text:#f8fafc;
20
+ --muted:#64748b;
21
+ --dim:#475569;
22
+ --mono:'JetBrains Mono','SF Mono','Cascadia Code','Fira Code',monospace;
23
+ --sans:system-ui,-apple-system,sans-serif;
24
+ }
25
+ body{
26
+ background:var(--bg);color:var(--text);font-family:var(--mono);
27
+ height:100vh;overflow:hidden;
28
+ background-image:
29
+ linear-gradient(rgba(0,240,255,0.03) 1px,transparent 1px),
30
+ linear-gradient(90deg,rgba(0,240,255,0.03) 1px,transparent 1px);
31
+ background-size:30px 30px;
32
+ }
33
+ .layout{display:flex;flex-direction:column;height:100%;padding:12px;gap:8px}
34
+
35
+ /* Header */
36
+ .header{
37
+ display:flex;justify-content:space-between;align-items:center;
38
+ background:var(--glass);border:1px solid var(--border);
39
+ border-radius:10px;padding:0 16px;height:48px;flex-shrink:0;position:relative;
40
+ }
41
+ .header::after{
42
+ content:'';position:absolute;bottom:0;left:0;width:100%;height:1px;
43
+ background:linear-gradient(90deg,transparent,var(--cyan),transparent);
44
+ }
45
+ .brand{display:flex;align-items:center;gap:10px}
46
+ .brand svg{filter:drop-shadow(0 0 4px rgba(0,240,255,0.4))}
47
+ .brand-name{font-size:14px;font-weight:600;letter-spacing:0.1em;text-transform:uppercase}
48
+ .brand-sub{font-size:9px;color:var(--cyan);letter-spacing:0.2em;margin-left:6px}
49
+ .stats{display:flex;align-items:center;gap:12px;font-size:12px;color:var(--muted)}
50
+ .stats .val{color:var(--cyan);font-weight:600}
51
+ .health-dot{
52
+ width:8px;height:8px;border-radius:50%;
53
+ background:var(--emerald);box-shadow:0 0 8px rgba(16,185,129,0.5);
54
+ animation:pulse 2s ease-in-out infinite;
55
+ }
56
+ .health-dot.warn{background:var(--amber);box-shadow:0 0 8px rgba(245,158,11,0.5)}
57
+ .health-dot.err{background:var(--red);box-shadow:0 0 8px rgba(239,68,68,0.5)}
58
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}}
59
+
60
+ /* Signal Bar */
61
+ .signal-bar{
62
+ display:flex;align-items:center;gap:6px;
63
+ background:var(--glass);border:1px solid var(--border);
64
+ border-radius:8px;padding:6px 14px;flex-shrink:0;min-height:30px;font-size:10px;
65
+ }
66
+ .sig-group{display:flex;align-items:center;gap:4px}
67
+ .sig-sep{width:1px;height:16px;background:rgba(255,255,255,0.1);margin:0 6px}
68
+ .sig-label{color:var(--muted);font-size:9px;letter-spacing:0.05em;text-transform:uppercase;margin-right:4px}
69
+
70
+ /* Unified color semantics:
71
+ .running = green (actively executing right now)
72
+ .standby = blue (idle / last success, waiting)
73
+ .alert = red (error / last failure)
74
+ .off = gray (ended / never run) */
75
+ .ind{width:8px;height:8px;transition:all 0.3s ease;cursor:default;flex-shrink:0}
76
+ .ind.running{background:var(--emerald);box-shadow:0 0 6px rgba(16,185,129,0.4)}
77
+ .ind.standby{background:var(--blue);box-shadow:0 0 4px rgba(59,130,246,0.3)}
78
+ .ind.alert{background:var(--red);box-shadow:0 0 6px rgba(239,68,68,0.4)}
79
+ .ind.off{background:#374151}
80
+ /* Shape: circle = cortex session */
81
+ .ind.circle{border-radius:50%}
82
+ /* Shape: square = cron job */
83
+ .ind.square{border-radius:2px}
84
+ /* Shape: diamond = one-shot job */
85
+ .ind.diamond{border-radius:2px;transform:rotate(45deg)}
86
+ .ind.blink{animation:ind-blink 0.6s ease-out}
87
+ @keyframes ind-blink{0%{transform:scale(1.8);opacity:0.6}100%{transform:scale(1);opacity:1}}
88
+ /* Diamond needs separate blink to preserve rotation */
89
+ .ind.diamond.blink{animation:ind-blink-diamond 0.6s ease-out}
90
+ @keyframes ind-blink-diamond{0%{transform:rotate(45deg) scale(1.8);opacity:0.6}100%{transform:rotate(45deg) scale(1);opacity:1}}
91
+ .part-mark{font-size:11px;cursor:default;transition:all 0.2s ease}
92
+ .part-mark.done{color:var(--emerald)}
93
+ .part-mark.pending{color:var(--muted)}
94
+ .part-mark.blink{animation:mark-blink 0.6s ease-out}
95
+ @keyframes mark-blink{0%{transform:scale(1.6);color:var(--cyan)}50%{color:var(--cyan)}100%{transform:scale(1)}}
96
+ .round-label{color:var(--muted);font-size:10px;margin-left:4px}
97
+
98
+ /* Tooltip */
99
+ .tooltip{
100
+ position:fixed;z-index:100;
101
+ background:rgba(0,0,0,0.92);border:1px solid var(--border);
102
+ border-radius:6px;padding:8px 12px;font-size:11px;
103
+ color:var(--text);pointer-events:none;
104
+ max-width:400px;line-height:1.5;
105
+ box-shadow:0 4px 20px rgba(0,0,0,0.6);
106
+ white-space:pre-line;word-break:break-all;
107
+ }
108
+
109
+ /* Event Stream */
110
+ .stream-wrap{
111
+ flex:1;display:flex;flex-direction:column;
112
+ background:var(--glass);border:1px solid var(--border);
113
+ border-radius:10px;overflow:hidden;position:relative;
114
+ }
115
+ .stream-title{
116
+ font-size:11px;color:var(--cyan);letter-spacing:0.12em;
117
+ text-transform:uppercase;padding:10px 14px 6px;flex-shrink:0;
118
+ display:flex;align-items:center;gap:8px;
119
+ }
120
+ .stream-title::before{
121
+ content:'';display:block;width:10px;height:10px;
122
+ border-top:2px solid var(--cyan);border-left:2px solid var(--cyan);
123
+ }
124
+ .stream{flex:1;overflow-y:auto;padding:0 14px 14px;scroll-behavior:auto}
125
+ .stream::-webkit-scrollbar{width:5px}
126
+ .stream::-webkit-scrollbar-track{background:transparent}
127
+ .stream::-webkit-scrollbar-thumb{background:rgba(0,240,255,0.15);border-radius:3px}
128
+ .stream::-webkit-scrollbar-thumb:hover{background:var(--cyan)}
129
+
130
+ /* Event rows */
131
+ .evt{
132
+ display:flex;gap:8px;padding:4px 0;
133
+ border-bottom:1px solid rgba(255,255,255,0.02);
134
+ font-size:12px;line-height:1.5;
135
+ animation:slide-in 0.25s ease-out;
136
+ align-items:baseline;
137
+ }
138
+ @keyframes slide-in{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
139
+ .evt:last-child{border-bottom:none}
140
+ .evt:hover{background:rgba(255,255,255,0.015)}
141
+ .evt-left{display:flex;gap:8px;flex-shrink:0;align-items:baseline}
142
+ .evt-time{color:var(--dim);width:60px;font-size:11px;flex-shrink:0}
143
+ .evt-actor{color:var(--emerald);width:120px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px}
144
+ .evt-body{flex:1;min-width:0}
145
+
146
+ /* Tool use */
147
+ .tool-call{display:flex;align-items:baseline;gap:6px}
148
+ .tool-icon{color:var(--cyan);flex-shrink:0}
149
+ .tool-name{color:var(--cyan);font-weight:600;flex-shrink:0}
150
+ .tool-desc{color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
151
+ .tool-input{color:var(--dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px;margin-top:1px}
152
+
153
+ /* Tool result */
154
+ .tool-result{display:flex;align-items:baseline;gap:6px}
155
+ .tool-result-icon{flex-shrink:0}
156
+ .tool-result-ok{color:var(--emerald)}
157
+ .tool-result-err{color:var(--red)}
158
+ .tool-result-name{color:var(--dim);font-size:11px;flex-shrink:0}
159
+ .tool-result-summary{
160
+ color:var(--muted);font-size:11px;overflow:hidden;
161
+ display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;
162
+ white-space:pre-line;line-height:1.4;
163
+ }
164
+
165
+ /* Agent output (result/error) */
166
+ .agent-output{cursor:pointer}
167
+ .agent-output-badge{font-size:10px;padding:1px 6px;border-radius:3px;margin-right:6px;font-weight:600}
168
+ .agent-output-badge.result{background:rgba(245,158,11,0.15);color:var(--amber);border:1px solid rgba(245,158,11,0.2)}
169
+ .agent-output-badge.error{background:rgba(239,68,68,0.15);color:var(--red);border:1px solid rgba(239,68,68,0.2)}
170
+ .agent-output-badge.deliver{background:rgba(217,70,239,0.15);color:var(--fuchsia);border:1px solid rgba(217,70,239,0.2)}
171
+ .agent-output-preview{color:var(--muted);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
172
+ .agent-output-full{
173
+ margin-top:6px;padding:10px 14px;
174
+ background:rgba(0,0,0,0.4);border:1px solid rgba(255,255,255,0.06);
175
+ border-radius:6px;font-family:var(--sans);font-size:12px;
176
+ color:#cbd5e1;line-height:1.6;max-height:400px;overflow-y:auto;
177
+ }
178
+ .agent-output-full h1,.agent-output-full h2,.agent-output-full h3{color:var(--text);margin:8px 0 4px;font-family:var(--mono)}
179
+ .agent-output-full h1{font-size:14px}.agent-output-full h2{font-size:13px}.agent-output-full h3{font-size:12px}
180
+ .agent-output-full p{margin:4px 0}
181
+ .agent-output-full code{background:rgba(0,0,0,0.5);padding:1px 4px;border-radius:3px;font-family:var(--mono);font-size:11px;color:var(--fuchsia)}
182
+ .agent-output-full pre{background:rgba(0,0,0,0.5);padding:8px;border-radius:4px;overflow-x:auto;margin:6px 0;font-family:var(--mono);font-size:11px;color:var(--muted)}
183
+ .agent-output-full pre code{background:none;padding:0;color:inherit}
184
+ .agent-output-full ul,.agent-output-full ol{padding-left:20px;margin:4px 0}
185
+ .agent-output-full li{margin:2px 0}
186
+ .agent-output-full table{border-collapse:collapse;margin:6px 0;font-size:11px;width:100%}
187
+ .agent-output-full th,.agent-output-full td{border:1px solid rgba(255,255,255,0.08);padding:3px 8px;text-align:left}
188
+ .agent-output-full th{background:rgba(0,240,255,0.05);color:var(--cyan)}
189
+ .agent-output-full strong{color:var(--text)}
190
+ .agent-output-full a{color:var(--cyan)}
191
+
192
+ /* Lifecycle events */
193
+ .lifecycle{display:flex;align-items:baseline;gap:6px}
194
+ .lifecycle-icon{flex-shrink:0}
195
+ .lifecycle-text{color:var(--fuchsia);font-weight:500}
196
+ .lifecycle-detail{color:var(--muted);font-size:11px}
197
+
198
+ /* System events */
199
+ .sys-evt{color:var(--dim);font-size:11px}
200
+
201
+ /* Raw JSON detail (click to expand) */
202
+ .evt-raw{
203
+ padding:8px 12px;margin:4px 0 6px 0;
204
+ background:rgba(0,0,0,0.5);border:1px solid var(--border);
205
+ border-radius:6px;font-size:10px;color:var(--dim);
206
+ overflow-x:auto;white-space:pre-wrap;word-break:break-all;
207
+ max-height:300px;overflow-y:auto;
208
+ }
209
+
210
+ /* Follow indicator */
211
+ .follow-indicator{
212
+ position:absolute;bottom:10px;right:14px;
213
+ font-size:10px;padding:3px 10px;border-radius:12px;
214
+ cursor:pointer;z-index:10;user-select:none;transition:all 0.2s ease;
215
+ }
216
+ .follow-indicator.live{background:rgba(16,185,129,0.15);border:1px solid rgba(16,185,129,0.3);color:var(--emerald)}
217
+ .follow-indicator.paused{background:rgba(100,116,139,0.15);border:1px solid rgba(100,116,139,0.3);color:var(--muted)}
218
+ </style>
219
+ </head>
220
+ <body>
221
+ <div class="layout">
222
+ <div class="header">
223
+ <div class="brand">
224
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" fill="none" width="24" height="24">
225
+ <defs><filter id="g" x="-20%" y="-20%" width="140%" height="140%">
226
+ <feDropShadow dx="0" dy="0" stdDeviation="1" flood-color="#38BDF8" flood-opacity="0.8"/>
227
+ <feDropShadow dx="0" dy="0" stdDeviation="3" flood-color="#38BDF8" flood-opacity="0.3"/>
228
+ </filter></defs>
229
+ <g filter="url(#g)">
230
+ <ellipse cx="60" cy="48" rx="30" ry="28" fill="#18181B"/>
231
+ <ellipse cx="32" cy="65" rx="14" ry="22" fill="#18181B"/>
232
+ <ellipse cx="88" cy="65" rx="14" ry="22" fill="#18181B"/>
233
+ <circle cx="45" cy="68" r="15" fill="#18181B"/>
234
+ <circle cx="75" cy="68" r="15" fill="#18181B"/>
235
+ <ellipse cx="60" cy="74" rx="20" ry="15" fill="#27272A"/>
236
+ </g>
237
+ <ellipse cx="48" cy="54" rx="5" ry="6.5" fill="#FFF" opacity="0.95"/>
238
+ <ellipse cx="48" cy="54" rx="3.5" ry="5" fill="#000"/>
239
+ <circle cx="46.5" cy="51.5" r="1.5" fill="#FFF"/>
240
+ <ellipse cx="72" cy="54" rx="5" ry="6.5" fill="#FFF" opacity="0.95"/>
241
+ <ellipse cx="72" cy="54" rx="3.5" ry="5" fill="#000"/>
242
+ <circle cx="70.5" cy="51.5" r="1.5" fill="#FFF"/>
243
+ <ellipse cx="60" cy="68" rx="5" ry="3.5" fill="#000"/>
244
+ <ellipse cx="60" cy="66.5" rx="2" ry="1" fill="#FFF" opacity="0.6"/>
245
+ <path d="M55 78C55 88,65 88,65 78Z" fill="#F472B6"/>
246
+ <path d="M53 75Q60 80 67 75" fill="none" stroke="#000" stroke-width="1.5" stroke-linecap="round"/>
247
+ </svg>
248
+ <span class="brand-name">Duoduo<span class="brand-sub">ATC</span></span>
249
+ </div>
250
+ <div class="stats">
251
+ <span id="stat-cost" class="val">--</span>
252
+ <span id="stat-tokens" class="val">--</span>
253
+ <span id="stat-tools" class="val">-- tools</span>
254
+ <div id="health-dot" class="health-dot"></div>
255
+ </div>
256
+ </div>
257
+ <div class="signal-bar" id="signal-bar"></div>
258
+ <div class="stream-wrap">
259
+ <div class="stream-title">Event Stream</div>
260
+ <div class="stream" id="stream"></div>
261
+ <div class="follow-indicator live" id="follow-ind">LIVE</div>
262
+ </div>
263
+ </div>
264
+ <div class="tooltip" id="tooltip" style="display:none"></div>
265
+
266
+ <script>
267
+ (function() {
268
+ "use strict";
269
+ var lastEventId = null, autoFollow = true, blinkTimers = new Map();
270
+ // Track last event timestamp per session from the actual event stream
271
+ var lastSeenBySession = new Map(); // session_key -> timestamp (ms)
272
+ var streamEl = document.getElementById("stream");
273
+ var signalEl = document.getElementById("signal-bar");
274
+ var followEl = document.getElementById("follow-ind");
275
+ var tooltipEl = document.getElementById("tooltip");
276
+ var costEl = document.getElementById("stat-cost");
277
+ var tokensEl = document.getElementById("stat-tokens");
278
+ var toolsEl = document.getElementById("stat-tools");
279
+ var healthDot = document.getElementById("health-dot");
280
+
281
+ function rpc(method, params) {
282
+ return fetch("/rpc", {
283
+ method: "POST", headers: {"Content-Type": "application/json"},
284
+ body: JSON.stringify({jsonrpc:"2.0", id:Date.now(), method:method, params:params||{}})
285
+ }).then(function(r){return r.json()}).then(function(d){
286
+ if(d.error){console.warn("RPC error:",method,d.error);return null}
287
+ return d.result;
288
+ }).catch(function(e){console.warn("RPC fetch error:",method,e);return null});
289
+ }
290
+
291
+ function esc(s){return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}
292
+ function fmtCost(n){if(n==null)return"--";return"$"+(n>=1000?(n/1000).toFixed(1)+"k":n.toFixed(2))}
293
+ function fmtTokens(n){if(n==null)return"--";if(n>=1e6)return(n/1e6).toFixed(1)+"M";if(n>=1e3)return(n/1e3).toFixed(0)+"k";return String(n)}
294
+ function fmtTools(n){if(n==null)return"-- tools";if(n>=1e3)return(n/1e3).toFixed(1)+"k tools";return n+" tools"}
295
+ function timeAgo(s){if(!s)return"never";var ms=Date.now()-new Date(s).getTime();if(ms<60000)return Math.floor(ms/1000)+"s ago";if(ms<3600000)return Math.floor(ms/60000)+"m ago";if(ms<86400000)return Math.floor(ms/3600000)+"h ago";return Math.floor(ms/86400000)+"d ago"}
296
+
297
+ function shortActor(k) {
298
+ if(!k)return"system";
299
+ if(k.startsWith("job:")){var n=k.slice(4),d=n.lastIndexOf(".");return d>0?n.slice(0,d):n}
300
+ if(k.startsWith("meta:subconscious:"))return k.slice(18);
301
+ if(k.startsWith("meta:"))return k.slice(5);
302
+ var p=k.split(":");
303
+ if(p.length>=2)return p[0]+":"+p[p.length-1].slice(-8);
304
+ return k.slice(-12);
305
+ }
306
+
307
+ // --- Simple markdown to HTML (no deps, good enough for dashboards) ---
308
+ function md(text) {
309
+ if (!text) return "";
310
+ var s = esc(text);
311
+ // Headers
312
+ s = s.replace(/^### (.+)$/gm, "<h3>$1</h3>");
313
+ s = s.replace(/^## (.+)$/gm, "<h2>$1</h2>");
314
+ s = s.replace(/^# (.+)$/gm, "<h1>$1</h1>");
315
+ // Bold / italic
316
+ s = s.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
317
+ s = s.replace(/\*(.+?)\*/g, "<em>$1</em>");
318
+ // Code blocks
319
+ s = s.replace(/```[\s\S]*?```/g, function(m) {
320
+ var inner = m.slice(3, -3).replace(/^[a-z]*\n/, "");
321
+ return "<pre><code>" + inner + "</code></pre>";
322
+ });
323
+ // Inline code
324
+ s = s.replace(/`([^`]+)`/g, "<code>$1</code>");
325
+ // Tables (simple: | col | col |)
326
+ s = s.replace(/((?:^\|.+\|\s*$\n?)+)/gm, function(block) {
327
+ var rows = block.trim().split("\n").filter(function(r){return r.trim()});
328
+ if (rows.length < 2) return block;
329
+ var html = "<table>";
330
+ rows.forEach(function(row, i) {
331
+ if (row.match(/^\|[\s:-]+\|$/)) return; // separator row
332
+ var cells = row.split("|").filter(function(c,j,a){return j>0&&j<a.length});
333
+ var tag = i === 0 ? "th" : "td";
334
+ html += "<tr>" + cells.map(function(c){return "<"+tag+">"+c.trim()+"</"+tag+">"}).join("") + "</tr>";
335
+ });
336
+ return html + "</table>";
337
+ });
338
+ // Lists
339
+ s = s.replace(/^- (.+)$/gm, "<li>$1</li>");
340
+ s = s.replace(/((?:<li>.+<\/li>\s*)+)/g, "<ul>$1</ul>");
341
+ // Paragraphs (double newline)
342
+ s = s.replace(/\n\n/g, "</p><p>");
343
+ s = "<p>" + s + "</p>";
344
+ s = s.replace(/<p><(h[123]|pre|ul|table)/g, "<$1");
345
+ s = s.replace(/<\/(h[123]|pre|ul|table)><\/p>/g, "</$1>");
346
+ s = s.replace(/<p>\s*<\/p>/g, "");
347
+ return s;
348
+ }
349
+
350
+ // --- Event rendering ---
351
+ function renderEvtBody(evt) {
352
+ var p = evt.payload || {}, t = evt.type || "";
353
+
354
+ if (t === "agent.tool_use") {
355
+ var tn = p.tool_name || "?";
356
+ var desc = "", input = "";
357
+ try {
358
+ var parsed = JSON.parse(p.input_summary || "{}");
359
+ desc = parsed.description || "";
360
+ if (!desc) {
361
+ // Fallback: show command, file_path, or first string value
362
+ if (parsed.command) desc = parsed.command;
363
+ else if (parsed.file_path) desc = parsed.file_path;
364
+ else if (parsed.pattern) desc = parsed.pattern;
365
+ else if (parsed.query) desc = parsed.query;
366
+ else if (parsed.url) desc = parsed.url;
367
+ else {
368
+ var vals = Object.values(parsed);
369
+ for (var i=0;i<vals.length;i++){if(typeof vals[i]==="string"&&vals[i].length>0){desc=vals[i];break}}
370
+ }
371
+ }
372
+ // Secondary detail
373
+ if (parsed.command && desc !== parsed.command) input = parsed.command;
374
+ else if (parsed.file_path && desc !== parsed.file_path) input = parsed.file_path;
375
+ } catch(e) { desc = p.input_summary || ""; }
376
+
377
+ var html = '<div class="tool-call"><span class="tool-icon">\u26A1</span><span class="tool-name">' + esc(tn) + '</span>';
378
+ if (desc) html += '<span class="tool-desc">' + esc(desc.slice(0, 200)) + '</span>';
379
+ html += '</div>';
380
+ if (input) html += '<div class="tool-input">' + esc(input.slice(0, 300)) + '</div>';
381
+ return html;
382
+ }
383
+
384
+ if (t === "agent.tool_result") {
385
+ var isErr = p.is_error;
386
+ var tn2 = p.tool_name || "";
387
+ var summary = (p.summary || "").trim();
388
+ // Truncate very long results
389
+ if (summary.length > 500) summary = summary.slice(0, 500) + "\u2026";
390
+ var iconCls = isErr ? "tool-result-err" : "tool-result-ok";
391
+ var icon = isErr ? "\u274C" : "\u2705";
392
+ return '<div class="tool-result"><span class="tool-result-icon ' + iconCls + '">' + icon + '</span>' +
393
+ (tn2 ? '<span class="tool-result-name">[' + esc(tn2) + ']</span>' : '') +
394
+ '</div><div class="tool-result-summary">' + esc(summary) + '</div>';
395
+ }
396
+
397
+ if (t === "agent.result") {
398
+ var text = p.text || "";
399
+ var preview = text.replace(/\n/g, " ").slice(0, 120);
400
+ var part = p.partition ? " \u00B7 " + p.partition : "";
401
+ return '<div class="agent-output" data-evtid="' + esc(evt.id) + '">' +
402
+ '<span class="agent-output-badge result">\uD83D\uDCAD Output' + esc(part) + '</span>' +
403
+ '<span class="agent-output-preview">' + esc(preview) + '</span>' +
404
+ '</div>';
405
+ }
406
+
407
+ if (t === "agent.error") {
408
+ var errText = (p.text || "error").slice(0, 200);
409
+ return '<div class="agent-output"><span class="agent-output-badge error">\uD83D\uDEA8 Error</span>' +
410
+ '<span class="agent-output-preview">' + esc(errText) + '</span></div>';
411
+ }
412
+
413
+ if (t === "route.deliver") {
414
+ var src = shortActor(p.source_session_key);
415
+ var content = "";
416
+ if (p.payload) content = (p.payload.notify_content || p.payload.text || "").replace(/\n/g, " ").slice(0, 120);
417
+ return '<div class="agent-output" data-evtid="' + esc(evt.id) + '">' +
418
+ '<span class="agent-output-badge deliver">\uD83D\uDCEB ' + esc(src) + '</span>' +
419
+ '<span class="agent-output-preview">' + esc(content) + '</span></div>';
420
+ }
421
+
422
+ if (t === "job.spawn") return '<div class="lifecycle"><span class="lifecycle-icon">\uD83D\uDE80</span><span class="lifecycle-text">Job spawned</span><span class="lifecycle-detail">' + esc(p.job_id || "?") + (p.cron ? " \u00B7 " + esc(p.cron) : "") + '</span></div>';
423
+ if (t === "job.complete") return '<div class="lifecycle"><span class="lifecycle-icon">\u2705</span><span class="lifecycle-text">Job complete</span><span class="lifecycle-detail">' + esc(p.job_id || "?") + '</span></div>';
424
+ if (t === "job.fail") return '<div class="lifecycle"><span class="lifecycle-icon">\u274C</span><span class="lifecycle-text">Job failed</span><span class="lifecycle-detail">' + esc(p.job_id || "?") + (p.error ? " \u2014 " + esc(p.error.slice(0, 100)) : "") + '</span></div>';
425
+ if (t === "system.cadence_tick") return '<div class="sys-evt">\u23F1 Cadence tick</div>';
426
+ if (t === "session.pause") return '<div class="sys-evt">\u23F8 Session paused</div>';
427
+ if (t === "channel.attached") return '<div class="sys-evt">\uD83D\uDD17 Channel attached</div>';
428
+
429
+ return '<div class="sys-evt">' + esc(t) + " \u2014 " + esc(JSON.stringify(p).slice(0, 100)) + '</div>';
430
+ }
431
+
432
+ function appendEvents(events) {
433
+ events.forEach(function(evt) {
434
+ var row = document.createElement("div");
435
+ row.className = "evt";
436
+ row.dataset.id = evt.id;
437
+
438
+ var time = evt.ts ? new Date(evt.ts).toISOString().slice(11, 19) : "??:??:??";
439
+ var actor = shortActor(evt.session_key);
440
+
441
+ row.innerHTML =
442
+ '<span class="evt-time">' + esc(time) + '</span>' +
443
+ '<span class="evt-actor" title="' + esc(evt.session_key || "") + '">' + esc(actor) + '</span>' +
444
+ '<div class="evt-body">' + renderEvtBody(evt) + '</div>';
445
+
446
+ // Click on output badge to expand markdown
447
+ var outputEl = row.querySelector(".agent-output[data-evtid]");
448
+ if (outputEl) {
449
+ outputEl.addEventListener("click", function() { toggleOutput(row, evt); });
450
+ }
451
+
452
+ // Right-click / double-click for raw JSON on any event
453
+ row.addEventListener("dblclick", function() { toggleRaw(row, evt); });
454
+
455
+ streamEl.appendChild(row);
456
+ lastEventId = evt.id;
457
+ // Track last seen event time per session (for running detection)
458
+ if (evt.session_key && evt.ts) {
459
+ lastSeenBySession.set(evt.session_key, new Date(evt.ts).getTime());
460
+ }
461
+ blinkDot(evt.session_key, evt);
462
+ });
463
+
464
+ // Recycle old DOM nodes to prevent unbounded growth
465
+ var MAX_NODES = 2000;
466
+ while (streamEl.children.length > MAX_NODES) streamEl.removeChild(streamEl.firstChild);
467
+
468
+ if (autoFollow && events.length > 0) streamEl.scrollTop = streamEl.scrollHeight;
469
+ }
470
+
471
+ function toggleOutput(row, evt) {
472
+ var existing = row.querySelector(".agent-output-full");
473
+ if (existing) { existing.remove(); return; }
474
+ var div = document.createElement("div");
475
+ div.className = "agent-output-full";
476
+ var text = "";
477
+ if (evt.type === "agent.result") text = (evt.payload || {}).text || "";
478
+ else if (evt.type === "route.deliver" && evt.payload && evt.payload.payload) text = evt.payload.payload.notify_content || evt.payload.payload.text || "";
479
+ div.innerHTML = md(text);
480
+ row.querySelector(".evt-body").appendChild(div);
481
+ }
482
+
483
+ function toggleRaw(row, evt) {
484
+ var existing = row.querySelector(".evt-raw");
485
+ if (existing) { existing.remove(); return; }
486
+ var pre = document.createElement("pre");
487
+ pre.className = "evt-raw";
488
+ pre.textContent = JSON.stringify(evt, null, 2);
489
+ row.querySelector(".evt-body").appendChild(pre);
490
+ }
491
+
492
+ function blinkDot(key, evt) {
493
+ if (!key) return;
494
+ // Try exact match first
495
+ var dot = signalEl.querySelector('[data-key="' + CSS.escape(key) + '"]');
496
+ // For job sessions: "job:bigshu-tracker.d33e214" → try "job:bigshu-tracker"
497
+ if (!dot && key.startsWith("job:")) {
498
+ var name = key.slice(4), d = name.lastIndexOf(".");
499
+ if (d > 0) dot = signalEl.querySelector('[data-key="' + CSS.escape("job:" + name.slice(0, d)) + '"]');
500
+ }
501
+ // For meta/subconscious: use payload.partition to find the partition mark
502
+ if (!dot && key.startsWith("meta:") && evt && evt.payload && evt.payload.partition) {
503
+ dot = signalEl.querySelector('[data-key="' + CSS.escape("sub:" + evt.payload.partition) + '"]');
504
+ }
505
+ if (!dot) return;
506
+ dot.classList.remove("blink");
507
+ void dot.offsetWidth;
508
+ dot.classList.add("blink");
509
+ var blinkKey = dot.dataset.key;
510
+ clearTimeout(blinkTimers.get(blinkKey));
511
+ blinkTimers.set(blinkKey, setTimeout(function(){dot.classList.remove("blink")}, 600));
512
+ }
513
+
514
+ // --- Signal Bar ---
515
+ function renderSignalBar(status, jobs) {
516
+ var groups = [];
517
+
518
+ // Build set of actively-running job IDs from client-side event stream data.
519
+ // A job is "running" if we've seen an event for it within the last 2 minutes.
520
+ // This is more reliable than registry status which may be stale or filtered.
521
+ var STALE_MS = 2 * 60 * 1000;
522
+ var now = Date.now();
523
+ var activeJobIds = new Set();
524
+ lastSeenBySession.forEach(function(ts, key) {
525
+ if (key.startsWith("job:") && now - ts < STALE_MS) {
526
+ var name = key.slice(4);
527
+ var dot = name.lastIndexOf(".");
528
+ activeJobIds.add(dot > 0 ? name.slice(0, dot) : name);
529
+ }
530
+ });
531
+
532
+ // Cortex (foreground sessions) — circles
533
+ var cortex = (status.sessions || []).filter(function(s){return !s.session_key.startsWith("meta:") && !s.session_key.startsWith("job:")});
534
+ if (cortex.length > 0) {
535
+ var h = '<span class="sig-label">cortex</span>';
536
+ cortex.forEach(function(s){
537
+ var color = s.status==="active"?"running":s.status==="error"?"alert":s.status==="ended"?"off":"standby";
538
+ var tip = s.session_key+"\n"+s.status+" \u2022 "+(s.health||"ok")+"\nlast: "+timeAgo(s.last_event_at)+"\ncreated: "+timeAgo(s.created_at);
539
+ h+='<div class="ind circle '+color+'" data-key="'+esc(s.session_key)+'" data-tip="'+esc(tip)+'"></div>';
540
+ });
541
+ groups.push(h);
542
+ }
543
+
544
+ // Jobs — squares (cron) or diamonds (once)
545
+ var jobList = jobs || [];
546
+ if (jobList.length > 0) {
547
+ var h2 = '<span class="sig-label">job</span>';
548
+ jobList.forEach(function(j){
549
+ var isOnce = j.frontmatter && j.frontmatter.cron === "once";
550
+ var shape = isOnce ? "diamond" : "square";
551
+ var isRunning = activeJobIds.has(j.id);
552
+ var color;
553
+ if (isRunning) {
554
+ color = "running";
555
+ } else {
556
+ var r = j.state ? j.state.last_result : "unknown";
557
+ color = r === "failure" ? "alert" : r === "success" ? "standby" : "off";
558
+ }
559
+ var statusText = isRunning ? "running" : (j.state ? j.state.last_result : "unknown");
560
+ var tip = j.id + (isOnce ? " (once)" : "") + "\ncron: "+(j.frontmatter?j.frontmatter.cron:"?") +"\n"+statusText+" \u2022 "+(j.state?j.state.run_count:0)+" runs\nlast: "+timeAgo(j.state?j.state.last_run_at:null);
561
+ h2+='<div class="ind '+shape+' '+color+'" data-key="job:'+esc(j.id)+'" data-tip="'+esc(tip)+'"></div>';
562
+ });
563
+ groups.push(h2);
564
+ }
565
+
566
+ // Subconscious partitions
567
+ var parts = status.subconscious ? status.subconscious.partitions : [];
568
+ if (parts.length > 0) {
569
+ var done = parts.filter(function(p){return p.done}).length;
570
+ var h3 = '<span class="sig-label">sub</span>';
571
+ parts.forEach(function(p){
572
+ h3+='<span class="part-mark '+(p.done?"done":"pending")+'" data-key="sub:'+esc(p.name)+'" data-tip="'+esc(p.name+" \u2014 "+(p.done?"done":"pending"))+'">'+(p.done?"\u2713":"\u00B7")+'</span>';
573
+ });
574
+ h3+='<span class="round-label">'+done+'/'+parts.length+'</span>';
575
+ groups.push(h3);
576
+ }
577
+
578
+ // Cadence tick
579
+ if (status.cadence && status.cadence.last_tick) {
580
+ var tip = "last tick: "+timeAgo(status.cadence.last_tick)+"\ninterval: "+(status.cadence.interval_ms/1000)+"s";
581
+ groups.push('<span class="sig-label" data-tip="'+esc(tip)+'">\u23F1 '+timeAgo(status.cadence.last_tick)+'</span>');
582
+ }
583
+ signalEl.innerHTML = groups.map(function(g){return'<div class="sig-group">'+g+'</div>'}).join('<div class="sig-sep"></div>');
584
+ }
585
+
586
+ // --- Tooltip ---
587
+ document.addEventListener("mouseover",function(e){var t=e.target.dataset?e.target.dataset.tip:null;if(t){tooltipEl.textContent=t;tooltipEl.style.display="block";posTooltip(e)}});
588
+ document.addEventListener("mouseout",function(e){if(e.target.dataset&&e.target.dataset.tip)tooltipEl.style.display="none"});
589
+ document.addEventListener("mousemove",function(e){if(tooltipEl.style.display==="block")posTooltip(e)});
590
+ function posTooltip(e){var x=e.clientX+12,y=e.clientY+12;var tw=tooltipEl.offsetWidth,th=tooltipEl.offsetHeight;if(x+tw>window.innerWidth-8)x=e.clientX-tw-8;if(y+th>window.innerHeight-8)y=e.clientY-th-8;tooltipEl.style.left=x+"px";tooltipEl.style.top=y+"px"}
591
+
592
+ // --- Scroll / Follow ---
593
+ streamEl.addEventListener("scroll",function(){
594
+ var atBottom=streamEl.scrollHeight-streamEl.scrollTop-streamEl.clientHeight<100;
595
+ if(atBottom&&!autoFollow){autoFollow=true;followEl.className="follow-indicator live";followEl.textContent="LIVE"}
596
+ else if(!atBottom&&autoFollow){autoFollow=false;followEl.className="follow-indicator paused";followEl.textContent="PAUSED"}
597
+ });
598
+ followEl.addEventListener("click",function(){autoFollow=true;followEl.className="follow-indicator live";followEl.textContent="LIVE";streamEl.scrollTop=streamEl.scrollHeight});
599
+
600
+ // --- Polling ---
601
+ function pollStatus(){
602
+ return Promise.allSettled([rpc("system.status"),rpc("usage.get"),rpc("job.list")]).then(function(r){
603
+ var st=r[0].status==="fulfilled"?r[0].value:null;
604
+ var us=r[1].status==="fulfilled"?r[1].value:null;
605
+ var jr=r[2].status==="fulfilled"?r[2].value:null;
606
+ if(us&&us.sessions){var cost=0,tokens=0,tools=0;Object.values(us.sessions).forEach(function(s){var m=s.summary;if(m){cost+=m.total_cost_usd||0;tokens+=(m.total_input_tokens||0)+(m.total_output_tokens||0)+(m.total_cache_read_tokens||0);tools+=m.total_tool_calls||0}});costEl.textContent=fmtCost(cost);tokensEl.textContent=fmtTokens(tokens);toolsEl.textContent=fmtTools(tools)}
607
+ if(st){var gw=st.health.gateway,ms=st.health.meta_session;healthDot.className=gw==="ok"&&(ms==="ok"||ms==="starting")?"health-dot":gw==="down"||ms==="down"?"health-dot err":"health-dot warn";renderSignalBar(st,jr?jr.jobs:[])}else{healthDot.className="health-dot err"}
608
+ });
609
+ }
610
+ function pollEvents(){
611
+ var params={limit:200};if(lastEventId)params.after_id=lastEventId;
612
+ return rpc("spine.tail",params).then(function(r){if(r&&r.events&&r.events.length>0)appendEvents(r.events)});
613
+ }
614
+
615
+ Promise.allSettled([pollStatus(),pollEvents()]).then(function(){streamEl.scrollTop=streamEl.scrollHeight});
616
+ setInterval(pollEvents,3000);
617
+ setInterval(pollStatus,5000);
618
+ })();
619
+ </script>
620
+ </body>
621
+ </html>