@openduo/duoduo 0.3.1 → 0.3.3

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,870 @@
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-meta{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-badge.ingress{background:rgba(56,189,248,0.15);color:var(--cyan);border:1px solid rgba(56,189,248,0.2)}
172
+ .msg-media{font-size:10px;color:var(--dim);margin-right:4px}
173
+ .agent-output-preview{color:var(--muted);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
174
+ .agent-output-full{
175
+ margin-top:6px;padding:10px 14px;
176
+ background:rgba(0,0,0,0.4);border:1px solid rgba(255,255,255,0.06);
177
+ border-radius:6px;font-family:var(--sans);font-size:12px;
178
+ color:#cbd5e1;line-height:1.6;max-height:400px;overflow-y:auto;
179
+ }
180
+ .agent-output-full h1,.agent-output-full h2,.agent-output-full h3{color:var(--text);margin:8px 0 4px;font-family:var(--mono)}
181
+ .agent-output-full h1{font-size:14px}.agent-output-full h2{font-size:13px}.agent-output-full h3{font-size:12px}
182
+ .agent-output-full p{margin:4px 0}
183
+ .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)}
184
+ .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)}
185
+ .agent-output-full pre code{background:none;padding:0;color:inherit}
186
+ .agent-output-full ul,.agent-output-full ol{padding-left:20px;margin:4px 0}
187
+ .agent-output-full li{margin:2px 0}
188
+ .agent-output-full table{border-collapse:collapse;margin:6px 0;font-size:11px;width:100%}
189
+ .agent-output-full th,.agent-output-full td{border:1px solid rgba(255,255,255,0.08);padding:3px 8px;text-align:left}
190
+ .agent-output-full th{background:rgba(0,240,255,0.05);color:var(--cyan)}
191
+ .agent-output-full strong{color:var(--text)}
192
+ .agent-output-full a{color:var(--cyan)}
193
+
194
+ /* Lifecycle events */
195
+ .lifecycle{display:flex;align-items:baseline;gap:6px}
196
+ .lifecycle-icon{flex-shrink:0}
197
+ .lifecycle-text{color:var(--fuchsia);font-weight:500}
198
+ .lifecycle-detail{color:var(--muted);font-size:11px}
199
+
200
+ /* System events */
201
+ .sys-evt{color:var(--dim);font-size:11px}
202
+
203
+ /* Raw JSON detail (click to expand) */
204
+ .evt-raw{
205
+ padding:8px 12px;margin:4px 0 6px 0;
206
+ background:rgba(0,0,0,0.5);border:1px solid var(--border);
207
+ border-radius:6px;font-size:10px;color:var(--dim);
208
+ overflow-x:auto;white-space:pre-wrap;word-break:break-all;
209
+ max-height:300px;overflow-y:auto;
210
+ }
211
+
212
+ /* Follow indicator */
213
+ .follow-indicator{
214
+ position:absolute;bottom:10px;right:14px;
215
+ font-size:10px;padding:3px 10px;border-radius:12px;
216
+ cursor:pointer;z-index:10;user-select:none;transition:all 0.2s ease;
217
+ }
218
+ .follow-indicator.live{background:rgba(16,185,129,0.15);border:1px solid rgba(16,185,129,0.3);color:var(--emerald)}
219
+ .follow-indicator.paused{background:rgba(100,116,139,0.15);border:1px solid rgba(100,116,139,0.3);color:var(--muted)}
220
+
221
+ /* Config button */
222
+ .cfg-btn{
223
+ font-size:10px;padding:2px 8px;border-radius:4px;cursor:pointer;
224
+ background:rgba(0,240,255,0.08);border:1px solid rgba(0,240,255,0.2);
225
+ color:var(--cyan);font-family:var(--mono);letter-spacing:0.05em;
226
+ transition:all 0.15s ease;
227
+ }
228
+ .cfg-btn:hover{background:rgba(0,240,255,0.15);border-color:rgba(0,240,255,0.4)}
229
+
230
+ /* Config overlay */
231
+ .cfg-overlay{
232
+ position:fixed;inset:0;z-index:50;
233
+ background:rgba(0,0,0,0.6);backdrop-filter:blur(4px);
234
+ display:none;justify-content:center;align-items:center;
235
+ }
236
+ .cfg-overlay.open{display:flex}
237
+ .cfg-panel{
238
+ background:var(--glass);border:1px solid var(--border);
239
+ border-radius:12px;padding:20px 24px;
240
+ max-width:640px;width:90%;max-height:80vh;overflow-y:auto;
241
+ box-shadow:0 8px 40px rgba(0,0,0,0.6);
242
+ }
243
+ .cfg-panel::-webkit-scrollbar{width:5px}
244
+ .cfg-panel::-webkit-scrollbar-track{background:transparent}
245
+ .cfg-panel::-webkit-scrollbar-thumb{background:rgba(0,240,255,0.15);border-radius:3px}
246
+ .cfg-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:14px}
247
+ .cfg-title{font-size:13px;color:var(--cyan);letter-spacing:0.1em;text-transform:uppercase;font-weight:600}
248
+ .cfg-close{font-size:16px;cursor:pointer;color:var(--muted);background:none;border:none;padding:4px 8px}
249
+ .cfg-close:hover{color:var(--text)}
250
+ .cfg-group{margin-bottom:14px}
251
+ .cfg-group-title{font-size:10px;color:var(--fuchsia);letter-spacing:0.1em;text-transform:uppercase;margin-bottom:6px;font-weight:600}
252
+ .cfg-row{display:flex;align-items:baseline;gap:8px;padding:3px 0;font-size:12px}
253
+ .cfg-key{color:var(--muted);min-width:200px;flex-shrink:0}
254
+ .cfg-val{color:var(--text);font-weight:500}
255
+ .cfg-val.from-env{color:var(--cyan)}
256
+ .cfg-val.from-settings{color:var(--amber)}
257
+ .cfg-val.unset{color:var(--dim);font-style:italic}
258
+ .cfg-src{font-size:9px;color:var(--dim);margin-left:4px}
259
+ .cfg-copy-all{
260
+ font-size:10px;color:var(--cyan);cursor:pointer;background:rgba(0,240,255,0.06);
261
+ border:1px solid rgba(0,240,255,0.2);border-radius:4px;padding:3px 10px;
262
+ font-family:var(--mono);letter-spacing:0.03em;transition:all 0.15s ease;
263
+ }
264
+ .cfg-copy-all:hover{background:rgba(0,240,255,0.12);border-color:rgba(0,240,255,0.4)}
265
+ .cfg-copy-all.copied{color:var(--emerald);border-color:rgba(16,185,129,0.3);background:rgba(16,185,129,0.08)}
266
+
267
+ /* Mobile: <= 640px */
268
+ @media(max-width:640px){
269
+ .layout{padding:6px;gap:4px}
270
+ .header{padding:0 10px;height:40px;border-radius:8px}
271
+ .brand-name{font-size:12px}
272
+ .brand-sub{display:none}
273
+ .stats{gap:6px;font-size:10px}
274
+ .signal-bar{flex-wrap:wrap;padding:4px 10px;gap:4px;border-radius:6px}
275
+ .sig-sep{height:12px;margin:0 4px}
276
+ .stream{padding:0 8px 8px}
277
+ .stream-title{padding:8px 8px 4px;font-size:10px}
278
+ /* Stack: time+actor as compact label row, body below full-width */
279
+ .evt{flex-direction:column;gap:2px;font-size:11px;padding:6px 0}
280
+ .evt-meta{gap:4px}
281
+ .evt-time{width:auto;font-size:10px}
282
+ .evt-actor{width:auto;font-size:10px}
283
+ .evt-body{width:100%}
284
+ .tool-name{font-size:11px}
285
+ .tool-desc{font-size:10px;white-space:normal;word-break:break-word}
286
+ .tool-input{font-size:10px;white-space:normal;word-break:break-word}
287
+ .tool-call{flex-wrap:wrap}
288
+ .agent-output-badge{font-size:9px;padding:1px 4px}
289
+ .agent-output-preview{font-size:10px;white-space:normal;word-break:break-word}
290
+ .agent-output-full{padding:8px 10px;font-size:11px;max-height:300px}
291
+ .lifecycle{flex-wrap:wrap}
292
+ .lifecycle-detail{word-break:break-word}
293
+ .follow-indicator{bottom:6px;right:8px;font-size:9px;padding:2px 8px}
294
+ .tooltip{max-width:280px;font-size:10px}
295
+ /* Config: full-height drawer from top, no wasted space */
296
+ .cfg-overlay{align-items:flex-start}
297
+ .cfg-panel{padding:12px 14px;border-radius:0 0 10px 10px;width:100%;max-height:90vh}
298
+ .cfg-header{margin-bottom:10px}
299
+ .cfg-title{font-size:12px}
300
+ .cfg-group{margin-bottom:8px}
301
+ .cfg-group-title{font-size:9px;margin-bottom:4px}
302
+ /* Keep key-value inline but compact */
303
+ .cfg-row{gap:4px;padding:2px 0;font-size:11px}
304
+ .cfg-key{min-width:0;font-size:10px}
305
+ .cfg-val{font-size:10px}
306
+ .cfg-src{font-size:8px}
307
+ }
308
+ </style>
309
+ </head>
310
+ <body>
311
+ <div class="layout">
312
+ <div class="header">
313
+ <div class="brand">
314
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" fill="none" width="24" height="24">
315
+ <defs><filter id="g" x="-20%" y="-20%" width="140%" height="140%">
316
+ <feDropShadow dx="0" dy="0" stdDeviation="1" flood-color="#38BDF8" flood-opacity="0.8"/>
317
+ <feDropShadow dx="0" dy="0" stdDeviation="3" flood-color="#38BDF8" flood-opacity="0.3"/>
318
+ </filter></defs>
319
+ <g filter="url(#g)">
320
+ <ellipse cx="60" cy="48" rx="30" ry="28" fill="#18181B"/>
321
+ <ellipse cx="32" cy="65" rx="14" ry="22" fill="#18181B"/>
322
+ <ellipse cx="88" cy="65" rx="14" ry="22" fill="#18181B"/>
323
+ <circle cx="45" cy="68" r="15" fill="#18181B"/>
324
+ <circle cx="75" cy="68" r="15" fill="#18181B"/>
325
+ <ellipse cx="60" cy="74" rx="20" ry="15" fill="#27272A"/>
326
+ </g>
327
+ <ellipse cx="48" cy="54" rx="5" ry="6.5" fill="#FFF" opacity="0.95"/>
328
+ <ellipse cx="48" cy="54" rx="3.5" ry="5" fill="#000"/>
329
+ <circle cx="46.5" cy="51.5" r="1.5" fill="#FFF"/>
330
+ <ellipse cx="72" cy="54" rx="5" ry="6.5" fill="#FFF" opacity="0.95"/>
331
+ <ellipse cx="72" cy="54" rx="3.5" ry="5" fill="#000"/>
332
+ <circle cx="70.5" cy="51.5" r="1.5" fill="#FFF"/>
333
+ <ellipse cx="60" cy="68" rx="5" ry="3.5" fill="#000"/>
334
+ <ellipse cx="60" cy="66.5" rx="2" ry="1" fill="#FFF" opacity="0.6"/>
335
+ <path d="M55 78C55 88,65 88,65 78Z" fill="#F472B6"/>
336
+ <path d="M53 75Q60 80 67 75" fill="none" stroke="#000" stroke-width="1.5" stroke-linecap="round"/>
337
+ </svg>
338
+ <span class="brand-name">Duoduo<span class="brand-sub">ATC</span></span>
339
+ </div>
340
+ <div class="stats">
341
+ <span id="stat-cost" class="val">--</span>
342
+ <span id="stat-tokens" class="val">--</span>
343
+ <span id="stat-tools" class="val">-- tools</span>
344
+ <button class="cfg-btn" id="cfg-btn">CONFIG</button>
345
+ <div id="health-dot" class="health-dot"></div>
346
+ </div>
347
+ </div>
348
+ <div class="signal-bar" id="signal-bar"></div>
349
+ <div class="stream-wrap">
350
+ <div class="stream-title">Event Stream</div>
351
+ <div class="stream" id="stream"></div>
352
+ <div class="follow-indicator live" id="follow-ind">LIVE</div>
353
+ </div>
354
+ </div>
355
+ <div class="cfg-overlay" id="cfg-overlay">
356
+ <div class="cfg-panel" id="cfg-panel">
357
+ <div class="cfg-header">
358
+ <span class="cfg-title">Runtime Configuration</span>
359
+ <button class="cfg-copy-all" id="cfg-copy-all">Copy as .env</button>
360
+ <button class="cfg-close" id="cfg-close">&times;</button>
361
+ </div>
362
+ <div id="cfg-body">Loading...</div>
363
+ </div>
364
+ </div>
365
+ <div class="tooltip" id="tooltip" style="display:none"></div>
366
+
367
+ <script>
368
+ (function() {
369
+ "use strict";
370
+ var lastEventId = null, autoFollow = true, blinkTimers = new Map();
371
+ // Track last event timestamp per session from the actual event stream
372
+ var lastSeenBySession = new Map(); // session_key -> timestamp (ms)
373
+ var streamEl = document.getElementById("stream");
374
+ var signalEl = document.getElementById("signal-bar");
375
+ var followEl = document.getElementById("follow-ind");
376
+ var tooltipEl = document.getElementById("tooltip");
377
+ var costEl = document.getElementById("stat-cost");
378
+ var tokensEl = document.getElementById("stat-tokens");
379
+ var toolsEl = document.getElementById("stat-tools");
380
+ var healthDot = document.getElementById("health-dot");
381
+
382
+ function rpc(method, params) {
383
+ return fetch("/rpc", {
384
+ method: "POST", headers: {"Content-Type": "application/json"},
385
+ body: JSON.stringify({jsonrpc:"2.0", id:Date.now(), method:method, params:params||{}})
386
+ }).then(function(r){return r.json()}).then(function(d){
387
+ if(d.error){console.warn("RPC error:",method,d.error);return null}
388
+ return d.result;
389
+ }).catch(function(e){console.warn("RPC fetch error:",method,e);return null});
390
+ }
391
+
392
+ function esc(s){return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}
393
+ function fmtCost(n){if(n==null)return"--";return"$"+(n>=1000?(n/1000).toFixed(1)+"k":n.toFixed(2))}
394
+ 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)}
395
+ function fmtTools(n){if(n==null)return"-- tools";if(n>=1e3)return(n/1e3).toFixed(1)+"k tools";return n+" tools"}
396
+ 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"}
397
+
398
+ function shortActor(k) {
399
+ if(!k)return"system";
400
+ if(k.startsWith("job:")){var n=k.slice(4),d=n.lastIndexOf(".");return d>0?n.slice(0,d):n}
401
+ if(k.startsWith("meta:subconscious:"))return k.slice(18);
402
+ if(k.startsWith("meta:"))return k.slice(5);
403
+ var p=k.split(":");
404
+ if(p.length>=2)return p[0]+":"+p[p.length-1].slice(-8);
405
+ return k.slice(-12);
406
+ }
407
+
408
+ // --- Simple markdown to HTML (no deps, good enough for dashboards) ---
409
+ function md(text) {
410
+ if (!text) return "";
411
+ var s = esc(text);
412
+ // Headers
413
+ s = s.replace(/^### (.+)$/gm, "<h3>$1</h3>");
414
+ s = s.replace(/^## (.+)$/gm, "<h2>$1</h2>");
415
+ s = s.replace(/^# (.+)$/gm, "<h1>$1</h1>");
416
+ // Bold / italic
417
+ s = s.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
418
+ s = s.replace(/\*(.+?)\*/g, "<em>$1</em>");
419
+ // Code blocks
420
+ s = s.replace(/```[\s\S]*?```/g, function(m) {
421
+ var inner = m.slice(3, -3).replace(/^[a-z]*\n/, "");
422
+ return "<pre><code>" + inner + "</code></pre>";
423
+ });
424
+ // Inline code
425
+ s = s.replace(/`([^`]+)`/g, "<code>$1</code>");
426
+ // Tables (simple: | col | col |)
427
+ s = s.replace(/((?:^\|.+\|\s*$\n?)+)/gm, function(block) {
428
+ var rows = block.trim().split("\n").filter(function(r){return r.trim()});
429
+ if (rows.length < 2) return block;
430
+ var html = "<table>";
431
+ rows.forEach(function(row, i) {
432
+ if (row.match(/^\|[\s:-]+\|$/)) return; // separator row
433
+ var cells = row.split("|").filter(function(c,j,a){return j>0&&j<a.length});
434
+ var tag = i === 0 ? "th" : "td";
435
+ html += "<tr>" + cells.map(function(c){return "<"+tag+">"+c.trim()+"</"+tag+">"}).join("") + "</tr>";
436
+ });
437
+ return html + "</table>";
438
+ });
439
+ // Lists
440
+ s = s.replace(/^- (.+)$/gm, "<li>$1</li>");
441
+ s = s.replace(/((?:<li>.+<\/li>\s*)+)/g, "<ul>$1</ul>");
442
+ // Paragraphs (double newline)
443
+ s = s.replace(/\n\n/g, "</p><p>");
444
+ s = "<p>" + s + "</p>";
445
+ s = s.replace(/<p><(h[123]|pre|ul|table)/g, "<$1");
446
+ s = s.replace(/<\/(h[123]|pre|ul|table)><\/p>/g, "</$1>");
447
+ s = s.replace(/<p>\s*<\/p>/g, "");
448
+ return s;
449
+ }
450
+
451
+ // --- Event rendering ---
452
+ function renderEvtBody(evt) {
453
+ var p = evt.payload || {}, t = evt.type || "";
454
+
455
+ if (t === "agent.tool_use") {
456
+ var tn = p.tool_name || "?";
457
+ var desc = "", input = "";
458
+ try {
459
+ var parsed = JSON.parse(p.input_summary || "{}");
460
+ desc = parsed.description || "";
461
+ if (!desc) {
462
+ // Fallback: show command, file_path, or first string value
463
+ if (parsed.command) desc = parsed.command;
464
+ else if (parsed.file_path) desc = parsed.file_path;
465
+ else if (parsed.pattern) desc = parsed.pattern;
466
+ else if (parsed.query) desc = parsed.query;
467
+ else if (parsed.url) desc = parsed.url;
468
+ else {
469
+ var vals = Object.values(parsed);
470
+ for (var i=0;i<vals.length;i++){if(typeof vals[i]==="string"&&vals[i].length>0){desc=vals[i];break}}
471
+ }
472
+ }
473
+ // Secondary detail
474
+ if (parsed.command && desc !== parsed.command) input = parsed.command;
475
+ else if (parsed.file_path && desc !== parsed.file_path) input = parsed.file_path;
476
+ } catch(e) { desc = p.input_summary || ""; }
477
+
478
+ var html = '<div class="tool-call"><span class="tool-icon">\u26A1</span><span class="tool-name">' + esc(tn) + '</span>';
479
+ if (desc) html += '<span class="tool-desc">' + esc(desc.slice(0, 200)) + '</span>';
480
+ html += '</div>';
481
+ if (input) html += '<div class="tool-input">' + esc(input.slice(0, 300)) + '</div>';
482
+ return html;
483
+ }
484
+
485
+ if (t === "agent.tool_result") {
486
+ var isErr = p.is_error;
487
+ var tn2 = p.tool_name || "";
488
+ var summary = (p.summary || "").trim();
489
+ // Truncate very long results
490
+ if (summary.length > 500) summary = summary.slice(0, 500) + "\u2026";
491
+ var iconCls = isErr ? "tool-result-err" : "tool-result-ok";
492
+ var icon = isErr ? "\u274C" : "\u2705";
493
+ return '<div class="tool-result"><span class="tool-result-icon ' + iconCls + '">' + icon + '</span>' +
494
+ (tn2 ? '<span class="tool-result-name">[' + esc(tn2) + ']</span>' : '') +
495
+ '</div><div class="tool-result-summary">' + esc(summary) + '</div>';
496
+ }
497
+
498
+ if (t === "agent.result") {
499
+ var text = p.text || "";
500
+ var preview = text.replace(/\n/g, " ").slice(0, 120);
501
+ var part = p.partition ? " \u00B7 " + p.partition : "";
502
+ return '<div class="agent-output" data-evtid="' + esc(evt.id) + '">' +
503
+ '<span class="agent-output-badge result">\uD83D\uDCAD Output' + esc(part) + '</span>' +
504
+ '<span class="agent-output-preview">' + esc(preview) + '</span>' +
505
+ '</div>';
506
+ }
507
+
508
+ if (t === "agent.error") {
509
+ var errText = (p.text || "error").slice(0, 200);
510
+ return '<div class="agent-output"><span class="agent-output-badge error">\uD83D\uDEA8 Error</span>' +
511
+ '<span class="agent-output-preview">' + esc(errText) + '</span></div>';
512
+ }
513
+
514
+ if (t === "route.deliver") {
515
+ var src = shortActor(p.source_session_key);
516
+ var content = "";
517
+ if (p.payload) content = (p.payload.notify_content || p.payload.text || "").replace(/\n/g, " ").slice(0, 120);
518
+ return '<div class="agent-output" data-evtid="' + esc(evt.id) + '">' +
519
+ '<span class="agent-output-badge deliver">\uD83D\uDCEB ' + esc(src) + '</span>' +
520
+ '<span class="agent-output-preview">' + esc(content) + '</span></div>';
521
+ }
522
+
523
+ if (t === "channel.message") {
524
+ var msgText = (p.text || "");
525
+ var preview = msgText.replace(/\n/g, " ").slice(0, 120);
526
+ var mediaCount = Array.isArray(p.media) ? p.media.length : 0;
527
+ var mediaBadge = mediaCount > 0 ? ' <span class="msg-media">\uD83D\uDCCE ' + mediaCount + '</span>' : '';
528
+ return '<div class="agent-output" data-evtid="' + esc(evt.id) + '">' +
529
+ '<span class="agent-output-badge ingress">\u2709 Message</span>' +
530
+ mediaBadge +
531
+ '<span class="agent-output-preview">' + esc(preview || "(empty)") + '</span></div>';
532
+ }
533
+
534
+ if (t === "channel.command") {
535
+ var cmdText = p.command || p.text || "";
536
+ return '<div class="agent-output">' +
537
+ '<span class="agent-output-badge ingress">\u2709 Command</span>' +
538
+ '<span class="agent-output-preview">' + esc(cmdText.slice(0, 200)) + '</span></div>';
539
+ }
540
+
541
+ 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>';
542
+ 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>';
543
+ 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>';
544
+ if (t === "system.cadence_tick") return '<div class="sys-evt">\u23F1 Cadence tick</div>';
545
+ if (t === "session.pause") return '<div class="sys-evt">\u23F8 Session paused</div>';
546
+ if (t === "channel.attached") return '<div class="sys-evt">\uD83D\uDD17 Channel attached</div>';
547
+
548
+ return '<div class="sys-evt">' + esc(t) + " \u2014 " + esc(JSON.stringify(p).slice(0, 100)) + '</div>';
549
+ }
550
+
551
+ function appendEvents(events) {
552
+ events.forEach(function(evt) {
553
+ var row = document.createElement("div");
554
+ row.className = "evt";
555
+ row.dataset.id = evt.id;
556
+
557
+ var time = evt.ts ? new Date(evt.ts).toISOString().slice(11, 19) : "??:??:??";
558
+ var actor = shortActor(evt.session_key);
559
+
560
+ row.innerHTML =
561
+ '<div class="evt-meta">' +
562
+ '<span class="evt-time">' + esc(time) + '</span>' +
563
+ '<span class="evt-actor" title="' + esc(evt.session_key || "") + '">' + esc(actor) + '</span>' +
564
+ '</div>' +
565
+ '<div class="evt-body">' + renderEvtBody(evt) + '</div>';
566
+
567
+ // Click on output badge to expand markdown
568
+ var outputEl = row.querySelector(".agent-output[data-evtid]");
569
+ if (outputEl) {
570
+ outputEl.addEventListener("click", function() { toggleOutput(row, evt); });
571
+ }
572
+
573
+ // Right-click / double-click for raw JSON on any event
574
+ row.addEventListener("dblclick", function() { toggleRaw(row, evt); });
575
+
576
+ streamEl.appendChild(row);
577
+ lastEventId = evt.id;
578
+ // Track last seen event time per session (for running detection)
579
+ if (evt.session_key && evt.ts) {
580
+ lastSeenBySession.set(evt.session_key, new Date(evt.ts).getTime());
581
+ }
582
+ blinkDot(evt.session_key, evt);
583
+ });
584
+
585
+ // Recycle old DOM nodes to prevent unbounded growth
586
+ var MAX_NODES = 2000;
587
+ while (streamEl.children.length > MAX_NODES) streamEl.removeChild(streamEl.firstChild);
588
+
589
+ if (autoFollow && events.length > 0) streamEl.scrollTop = streamEl.scrollHeight;
590
+ }
591
+
592
+ function toggleOutput(row, evt) {
593
+ var existing = row.querySelector(".agent-output-full");
594
+ if (existing) { existing.remove(); return; }
595
+ var div = document.createElement("div");
596
+ div.className = "agent-output-full";
597
+ var text = "";
598
+ if (evt.type === "agent.result") text = (evt.payload || {}).text || "";
599
+ else if (evt.type === "route.deliver" && evt.payload && evt.payload.payload) text = evt.payload.payload.notify_content || evt.payload.payload.text || "";
600
+ else if (evt.type === "channel.message") text = (evt.payload || {}).text || "";
601
+ div.innerHTML = md(text);
602
+ row.querySelector(".evt-body").appendChild(div);
603
+ }
604
+
605
+ function toggleRaw(row, evt) {
606
+ var existing = row.querySelector(".evt-raw");
607
+ if (existing) { existing.remove(); return; }
608
+ var pre = document.createElement("pre");
609
+ pre.className = "evt-raw";
610
+ pre.textContent = JSON.stringify(evt, null, 2);
611
+ row.querySelector(".evt-body").appendChild(pre);
612
+ }
613
+
614
+ function blinkDot(key, evt) {
615
+ if (!key) return;
616
+ // Try exact match first
617
+ var dot = signalEl.querySelector('[data-key="' + CSS.escape(key) + '"]');
618
+ // For job sessions: "job:bigshu-tracker.d33e214" → try "job:bigshu-tracker"
619
+ if (!dot && key.startsWith("job:")) {
620
+ var name = key.slice(4), d = name.lastIndexOf(".");
621
+ if (d > 0) dot = signalEl.querySelector('[data-key="' + CSS.escape("job:" + name.slice(0, d)) + '"]');
622
+ }
623
+ // For meta/subconscious: use payload.partition to find the partition mark
624
+ if (!dot && key.startsWith("meta:") && evt && evt.payload && evt.payload.partition) {
625
+ dot = signalEl.querySelector('[data-key="' + CSS.escape("sub:" + evt.payload.partition) + '"]');
626
+ }
627
+ if (!dot) return;
628
+ dot.classList.remove("blink");
629
+ void dot.offsetWidth;
630
+ dot.classList.add("blink");
631
+ var blinkKey = dot.dataset.key;
632
+ clearTimeout(blinkTimers.get(blinkKey));
633
+ blinkTimers.set(blinkKey, setTimeout(function(){dot.classList.remove("blink")}, 600));
634
+ }
635
+
636
+ // --- Signal Bar ---
637
+ function renderSignalBar(status, jobs) {
638
+ var groups = [];
639
+
640
+ // Build set of actively-running job IDs from client-side event stream data.
641
+ // A job is "running" if we've seen an event for it within the last 2 minutes.
642
+ // This is more reliable than registry status which may be stale or filtered.
643
+ var STALE_MS = 2 * 60 * 1000;
644
+ var now = Date.now();
645
+ var activeJobIds = new Set();
646
+ lastSeenBySession.forEach(function(ts, key) {
647
+ if (key.startsWith("job:") && now - ts < STALE_MS) {
648
+ var name = key.slice(4);
649
+ var dot = name.lastIndexOf(".");
650
+ activeJobIds.add(dot > 0 ? name.slice(0, dot) : name);
651
+ }
652
+ });
653
+
654
+ // Cortex (foreground sessions) — circles
655
+ var cortex = (status.sessions || []).filter(function(s){return !s.session_key.startsWith("meta:") && !s.session_key.startsWith("job:")});
656
+ if (cortex.length > 0) {
657
+ var h = '<span class="sig-label">cortex</span>';
658
+ cortex.forEach(function(s){
659
+ var color = s.status==="active"?"running":s.status==="error"?"alert":s.status==="ended"?"off":"standby";
660
+ var tip = s.session_key+"\n"+s.status+" \u2022 "+(s.health||"ok")+"\nlast: "+timeAgo(s.last_event_at)+"\ncreated: "+timeAgo(s.created_at)+(s.cwd?"\ncwd: "+s.cwd:"");
661
+ h+='<div class="ind circle '+color+'" data-key="'+esc(s.session_key)+'" data-tip="'+esc(tip)+'"></div>';
662
+ });
663
+ groups.push(h);
664
+ }
665
+
666
+ // Jobs — squares (cron) or diamonds (once)
667
+ var jobList = jobs || [];
668
+ if (jobList.length > 0) {
669
+ var h2 = '<span class="sig-label">job</span>';
670
+ jobList.forEach(function(j){
671
+ var c = j.frontmatter ? j.frontmatter.cron || "" : "";
672
+ var isOnce = c === "once" || c.startsWith("@in ") || c.startsWith("@once");
673
+ var shape = isOnce ? "diamond" : "square";
674
+ var isRunning = activeJobIds.has(j.id);
675
+ var color;
676
+ if (isRunning) {
677
+ color = "running";
678
+ } else {
679
+ var r = j.state ? j.state.last_result : "unknown";
680
+ color = r === "failure" ? "alert" : r === "success" ? "standby" : "off";
681
+ }
682
+ var statusText = isRunning ? "running" : (j.state ? j.state.last_result : "unknown");
683
+ var cwdRel = j.frontmatter ? j.frontmatter.cwd_rel : null;
684
+ 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)+(cwdRel?"\ncwd: "+cwdRel:"");
685
+ h2+='<div class="ind '+shape+' '+color+'" data-key="job:'+esc(j.id)+'" data-tip="'+esc(tip)+'"></div>';
686
+ });
687
+ groups.push(h2);
688
+ }
689
+
690
+ // Subconscious partitions
691
+ var parts = status.subconscious ? status.subconscious.partitions : [];
692
+ if (parts.length > 0) {
693
+ var done = parts.filter(function(p){return p.done}).length;
694
+ var h3 = '<span class="sig-label">sub</span>';
695
+ parts.forEach(function(p){
696
+ 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>';
697
+ });
698
+ h3+='<span class="round-label">'+done+'/'+parts.length+'</span>';
699
+ groups.push(h3);
700
+ }
701
+
702
+ // Cadence tick
703
+ if (status.cadence && status.cadence.last_tick) {
704
+ var tip = "last tick: "+timeAgo(status.cadence.last_tick)+"\ninterval: "+(status.cadence.interval_ms/1000)+"s";
705
+ groups.push('<span class="sig-label" data-tip="'+esc(tip)+'">\u23F1 '+timeAgo(status.cadence.last_tick)+'</span>');
706
+ }
707
+ signalEl.innerHTML = groups.map(function(g){return'<div class="sig-group">'+g+'</div>'}).join('<div class="sig-sep"></div>');
708
+ }
709
+
710
+ // --- Tooltip ---
711
+ 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)}});
712
+ document.addEventListener("mouseout",function(e){if(e.target.dataset&&e.target.dataset.tip)tooltipEl.style.display="none"});
713
+ document.addEventListener("mousemove",function(e){if(tooltipEl.style.display==="block")posTooltip(e)});
714
+ 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"}
715
+
716
+ // --- Scroll / Follow ---
717
+ streamEl.addEventListener("scroll",function(){
718
+ var atBottom=streamEl.scrollHeight-streamEl.scrollTop-streamEl.clientHeight<100;
719
+ if(atBottom&&!autoFollow){autoFollow=true;followEl.className="follow-indicator live";followEl.textContent="LIVE"}
720
+ else if(!atBottom&&autoFollow){autoFollow=false;followEl.className="follow-indicator paused";followEl.textContent="PAUSED"}
721
+ });
722
+ followEl.addEventListener("click",function(){autoFollow=true;followEl.className="follow-indicator live";followEl.textContent="LIVE";streamEl.scrollTop=streamEl.scrollHeight});
723
+
724
+ // --- Polling ---
725
+ function pollStatus(){
726
+ return Promise.allSettled([rpc("system.status"),rpc("usage.get"),rpc("job.list")]).then(function(r){
727
+ var st=r[0].status==="fulfilled"?r[0].value:null;
728
+ var us=r[1].status==="fulfilled"?r[1].value:null;
729
+ var jr=r[2].status==="fulfilled"?r[2].value:null;
730
+ 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)}
731
+ 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"}
732
+ });
733
+ }
734
+ function pollEvents(){
735
+ var params={limit:200};if(lastEventId)params.after_id=lastEventId;
736
+ return rpc("spine.tail",params).then(function(r){if(r&&r.events&&r.events.length>0)appendEvents(r.events)});
737
+ }
738
+
739
+ Promise.allSettled([pollStatus(),pollEvents()]).then(function(){streamEl.scrollTop=streamEl.scrollHeight});
740
+ setInterval(pollEvents,3000);
741
+ setInterval(pollStatus,5000);
742
+
743
+ // --- Config Panel ---
744
+ var cfgOverlay = document.getElementById("cfg-overlay");
745
+ var cfgBody = document.getElementById("cfg-body");
746
+ var GROUP_LABELS = {
747
+ network:"Network",sessions:"Sessions",cadence:"Cadence",
748
+ transfer:"Transfer",logging:"Logging",sdk:"SDK & Models",paths:"Paths",
749
+ subconscious:"Subconscious"
750
+ };
751
+ // Keys whose numeric value is a duration in milliseconds
752
+ var MS_KEYS = new Set([
753
+ "idle_ms","heartbeat_ms","interval_ms","runtime_lock_heartbeat_ms",
754
+ "pull_wait_ms","max_duration_ms"
755
+ ]);
756
+ function fmtMs(ms) {
757
+ if (typeof ms !== "number" || !isFinite(ms)) return String(ms);
758
+ if (ms < 1000) return ms + "ms";
759
+ var s = ms / 1000;
760
+ if (s < 60) return s + "s (" + ms.toLocaleString() + "ms)";
761
+ var m = s / 60;
762
+ if (m < 60) return (m % 1 === 0 ? m : m.toFixed(1)) + "min (" + ms.toLocaleString() + "ms)";
763
+ var h = m / 60;
764
+ return (h % 1 === 0 ? h : h.toFixed(1)) + "h (" + ms.toLocaleString() + "ms)";
765
+ }
766
+ function fmtEntryValue(key, e) {
767
+ if (e.source === "unset") return "(unset)";
768
+ if (MS_KEYS.has(key) && typeof e.value === "number") return fmtMs(e.value);
769
+ return String(e.value);
770
+ }
771
+ // Map config key → env var name (static groups)
772
+ var ENV_NAMES = {
773
+ port:"ALADUO_PORT",daemon_host:"ALADUO_DAEMON_HOST",
774
+ max_concurrent_channel:"ALADUO_SESSION_MAX_CONCURRENT_CHANNEL",max_concurrent_job:"ALADUO_SESSION_MAX_CONCURRENT_JOB",
775
+ idle_ms:"ALADUO_SESSION_IDLE_MS",heartbeat_ms:"ALADUO_SESSION_HEARTBEAT_MS",
776
+ interval_ms:"ALADUO_CADENCE_INTERVAL_MS",meta_max_quiet_ticks:"ALADUO_META_MAX_QUIET_TICKS",
777
+ runtime_lock_heartbeat_ms:"ALADUO_RUNTIME_LOCK_HEARTBEAT_MS",
778
+ pull_limit:"ALADUO_PULL_LIMIT",pull_wait_ms:"ALADUO_PULL_WAIT_MS",subscribe_replay_limit:"ALADUO_SUBSCRIBE_REPLAY_LIMIT",
779
+ log_level:"ALADUO_LOG_LEVEL",sdk_debug:"ALADUO_SDK_DEBUG",log_session_lifecycle:"ALADUO_LOG_SESSION_LIFECYCLE",
780
+ permission_mode:"ALADUO_PERMISSION_MODE",
781
+ work_dir:"ALADUO_WORK_DIR",bootstrap_dir:"ALADUO_BOOTSTRAP_DIR",meta_prompt_path:"ALADUO_META_PROMPT_PATH"
782
+ };
783
+ // Reverse the shortening for sdk group keys (model_opus → ANTHROPIC_DEFAULT_OPUS_MODEL, etc.)
784
+ function sdkKeyToEnvName(key) {
785
+ if (ENV_NAMES[key]) return ENV_NAMES[key];
786
+ if (key.startsWith("model_")) return "ANTHROPIC_DEFAULT_" + key.slice(6).toUpperCase();
787
+ if (key === "base_url") return "ANTHROPIC_BASE_URL";
788
+ // Generic: CLAUDE_CODE_ or ANTHROPIC_ prefix
789
+ return key.toUpperCase();
790
+ }
791
+ function resolveEnvName(group, key) {
792
+ if (group === "sdk") return sdkKeyToEnvName(key);
793
+ return ENV_NAMES[key] || "";
794
+ }
795
+ var lastCfg = null;
796
+ function buildDotEnv(cfg) {
797
+ var lines = ["# duoduo daemon configuration", "# Generated from system.config at " + new Date().toISOString(), ""];
798
+ var kvGroups = ["network","sessions","cadence","transfer","logging","sdk","paths"];
799
+ kvGroups.forEach(function(group) {
800
+ if (!cfg[group]) return;
801
+ var entries = cfg[group];
802
+ var groupLines = [];
803
+ Object.keys(entries).forEach(function(key) {
804
+ var e = entries[key];
805
+ var envName = resolveEnvName(group, key);
806
+ if (!envName || e.source === "unset") return;
807
+ groupLines.push(envName + "=" + String(e.value));
808
+ });
809
+ if (groupLines.length > 0) {
810
+ lines.push("# " + (GROUP_LABELS[group] || group));
811
+ lines = lines.concat(groupLines);
812
+ lines.push("");
813
+ }
814
+ });
815
+ return lines.join("\n");
816
+ }
817
+ function renderConfigPanel(cfg) {
818
+ lastCfg = cfg;
819
+ var html = "";
820
+ var kvGroups = ["network","sessions","cadence","transfer","logging","sdk","paths"];
821
+ kvGroups.forEach(function(group) {
822
+ if (!cfg[group]) return;
823
+ var label = GROUP_LABELS[group] || group;
824
+ html += '<div class="cfg-group"><div class="cfg-group-title">' + esc(label) + '</div>';
825
+ var entries = cfg[group];
826
+ Object.keys(entries).forEach(function(key) {
827
+ var e = entries[key];
828
+ var valCls = e.source === "env" ? "cfg-val from-env" : e.source === "settings" ? "cfg-val from-settings" : e.source === "unset" ? "cfg-val unset" : "cfg-val";
829
+ var display = fmtEntryValue(key, e);
830
+ var srcLabel = e.source === "unset" ? "" : e.source;
831
+ var srcTag = srcLabel ? '<span class="cfg-src">' + esc(srcLabel) + '</span>' : '';
832
+ html += '<div class="cfg-row"><span class="cfg-key">' + esc(key) + '</span><span class="' + valCls + '">' + esc(display) + '</span>' + srcTag + '</div>';
833
+ });
834
+ html += '</div>';
835
+ });
836
+ if (cfg.subconscious && cfg.subconscious.partitions && cfg.subconscious.partitions.length > 0) {
837
+ html += '<div class="cfg-group"><div class="cfg-group-title">' + esc(GROUP_LABELS.subconscious) + '</div>';
838
+ cfg.subconscious.partitions.forEach(function(p) {
839
+ var status = p.enabled ? "enabled" : "disabled";
840
+ var detail = "cooldown: " + p.cooldown_ticks + " tick" + (p.cooldown_ticks !== 1 ? "s" : "") + " \u00B7 timeout: " + fmtMs(p.max_duration_ms);
841
+ var valCls = p.enabled ? "cfg-val" : "cfg-val unset";
842
+ html += '<div class="cfg-row"><span class="cfg-key">' + esc(p.name) + '</span><span class="' + valCls + '">' + esc(status) + '</span><span class="cfg-src">' + esc(detail) + '</span></div>';
843
+ });
844
+ html += '</div>';
845
+ }
846
+ cfgBody.innerHTML = html;
847
+ }
848
+ document.getElementById("cfg-btn").addEventListener("click", function() {
849
+ cfgOverlay.classList.add("open");
850
+ cfgBody.innerHTML = "Loading...";
851
+ rpc("system.config").then(function(cfg) {
852
+ if (cfg) renderConfigPanel(cfg);
853
+ else cfgBody.innerHTML = '<span style="color:var(--red)">Failed to load config</span>';
854
+ });
855
+ });
856
+ document.getElementById("cfg-close").addEventListener("click", function() { cfgOverlay.classList.remove("open"); });
857
+ document.getElementById("cfg-copy-all").addEventListener("click", function() {
858
+ if (!lastCfg) return;
859
+ var btn = this;
860
+ navigator.clipboard.writeText(buildDotEnv(lastCfg)).then(function() {
861
+ btn.textContent = "\u2713 Copied";
862
+ btn.classList.add("copied");
863
+ setTimeout(function(){ btn.textContent = "Copy as .env"; btn.classList.remove("copied"); }, 1500);
864
+ });
865
+ });
866
+ cfgOverlay.addEventListener("click", function(e) { if (e.target === cfgOverlay) cfgOverlay.classList.remove("open"); });
867
+ })();
868
+ </script>
869
+ </body>
870
+ </html>