@0xmaxma/claude-gateway 1.3.4 → 1.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/config.template.json +9 -7
- package/dist/agent/runner.d.ts +20 -0
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +94 -1
- package/dist/agent/runner.js.map +1 -1
- package/dist/api/gateway-router.d.ts +14 -0
- package/dist/api/gateway-router.d.ts.map +1 -1
- package/dist/api/gateway-router.js +279 -7
- package/dist/api/gateway-router.js.map +1 -1
- package/dist/discord/receiver.d.ts +1 -0
- package/dist/discord/receiver.d.ts.map +1 -1
- package/dist/discord/receiver.js +7 -1
- package/dist/discord/receiver.js.map +1 -1
- package/dist/session/process.d.ts +8 -0
- package/dist/session/process.d.ts.map +1 -1
- package/dist/session/process.js +34 -0
- package/dist/session/process.js.map +1 -1
- package/dist/shell/claude-pty-shell.js +211 -4
- package/dist/shell/claude-pty-shell.js.map +1 -1
- package/dist/shell/emitter.d.ts +13 -0
- package/dist/shell/emitter.d.ts.map +1 -1
- package/dist/shell/emitter.js +16 -0
- package/dist/shell/emitter.js.map +1 -1
- package/dist/shell/menu-cancel.d.ts +50 -0
- package/dist/shell/menu-cancel.d.ts.map +1 -0
- package/dist/shell/menu-cancel.js +62 -0
- package/dist/shell/menu-cancel.js.map +1 -0
- package/dist/shell/pty-serialize.d.ts +23 -0
- package/dist/shell/pty-serialize.d.ts.map +1 -0
- package/dist/shell/pty-serialize.js +131 -0
- package/dist/shell/pty-serialize.js.map +1 -0
- package/dist/shell/pty-stream-registry.d.ts +59 -0
- package/dist/shell/pty-stream-registry.d.ts.map +1 -0
- package/dist/shell/pty-stream-registry.js +279 -0
- package/dist/shell/pty-stream-registry.js.map +1 -0
- package/dist/shell/screen.d.ts +43 -0
- package/dist/shell/screen.d.ts.map +1 -1
- package/dist/shell/screen.js +91 -1
- package/dist/shell/screen.js.map +1 -1
- package/dist/telegram/receiver.d.ts +1 -0
- package/dist/telegram/receiver.d.ts.map +1 -1
- package/dist/telegram/receiver.js +7 -1
- package/dist/telegram/receiver.js.map +1 -1
- package/dist/ui/web-ui.d.ts +2 -2
- package/dist/ui/web-ui.d.ts.map +1 -1
- package/dist/ui/web-ui.js +682 -114
- package/dist/ui/web-ui.js.map +1 -1
- package/mcp/tools/discord/module.ts +88 -1
- package/mcp/tools/discord/outbound.ts +36 -0
- package/mcp/tools/discord/types.ts +2 -0
- package/mcp/tools/telegram/menu-parser.ts +37 -0
- package/mcp/tools/telegram/receiver-server.ts +83 -0
- package/mcp/tools/telegram/typing.ts +7 -1
- package/package.json +4 -2
package/dist/ui/web-ui.js
CHANGED
|
@@ -3,15 +3,19 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.generateDashboardHtml = generateDashboardHtml;
|
|
4
4
|
/**
|
|
5
5
|
* Generates a self-contained HTML dashboard page for the gateway status UI.
|
|
6
|
-
* No external dependencies
|
|
6
|
+
* No external dependencies except xterm.js CDN for PTY viewer.
|
|
7
7
|
*/
|
|
8
|
-
function generateDashboardHtml() {
|
|
8
|
+
function generateDashboardHtml(dashToken = '') {
|
|
9
|
+
const safeToken = dashToken.replace(/&/g, '&').replace(/"/g, '"');
|
|
9
10
|
return `<!DOCTYPE html>
|
|
10
11
|
<html lang="en">
|
|
11
12
|
<head>
|
|
12
13
|
<meta charset="UTF-8">
|
|
13
14
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
14
|
-
<
|
|
15
|
+
<meta name="dash-token" content="${safeToken}">
|
|
16
|
+
<title>Claude Gateway</title>
|
|
17
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@6.0.0/css/xterm.min.css"
|
|
18
|
+
integrity="sha384-eDYu/eBZQNhtqTaA7Wl3XighXKxm/9VYF+Chh3hQS+UUlKQIJ14hK2imKu4n99aR" crossorigin="anonymous"/>
|
|
15
19
|
<style>
|
|
16
20
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
17
21
|
body {
|
|
@@ -20,8 +24,17 @@ function generateDashboardHtml() {
|
|
|
20
24
|
color: #e2e8f0;
|
|
21
25
|
padding: 24px;
|
|
22
26
|
}
|
|
23
|
-
h1 {
|
|
24
|
-
.
|
|
27
|
+
h1 { font-size: 1.5rem; margin-bottom: 8px; }
|
|
28
|
+
.rainbow {
|
|
29
|
+
background: linear-gradient(90deg, #ff0080, #ff8c00, #ffe600, #00d26a, #00b4ff, #a855f7, #ff0080);
|
|
30
|
+
background-size: 200% auto;
|
|
31
|
+
-webkit-background-clip: text;
|
|
32
|
+
-webkit-text-fill-color: transparent;
|
|
33
|
+
background-clip: text;
|
|
34
|
+
animation: rainbow-shift 3s linear infinite;
|
|
35
|
+
}
|
|
36
|
+
@keyframes rainbow-shift { to { background-position: 200% center; } }
|
|
37
|
+
.meta { color: #718096; font-size: 0.85rem; margin-bottom: 16px; }
|
|
25
38
|
.meta span { color: #a0aec0; }
|
|
26
39
|
h2 { color: #90cdf4; font-size: 1.1rem; margin: 20px 0 10px; }
|
|
27
40
|
table {
|
|
@@ -45,7 +58,8 @@ function generateDashboardHtml() {
|
|
|
45
58
|
padding: 8px 12px;
|
|
46
59
|
border-bottom: 1px solid #1a202c;
|
|
47
60
|
}
|
|
48
|
-
tr
|
|
61
|
+
tr.session-row td { background: #0f1117; color: #cbd5e0; font-size: 0.82rem; }
|
|
62
|
+
tr.session-row:hover td { background: #1a202c; }
|
|
49
63
|
.badge {
|
|
50
64
|
display: inline-block;
|
|
51
65
|
padding: 2px 8px;
|
|
@@ -56,72 +70,216 @@ function generateDashboardHtml() {
|
|
|
56
70
|
.badge-green { background: #22543d; color: #68d391; }
|
|
57
71
|
.badge-red { background: #742a2a; color: #fc8181; }
|
|
58
72
|
.badge-gray { background: #2d3748; color: #a0aec0; }
|
|
73
|
+
.badge-blue { background: #1a365d; color: #63b3ed; }
|
|
74
|
+
.badge-purple { background: #44337a; color: #b794f4; }
|
|
75
|
+
/* Per-model badge colors — each model family gets a distinct hue. */
|
|
76
|
+
.badge-opus { background: #5a3a1a; color: #f6ad55; }
|
|
77
|
+
.badge-sonnet { background: #1a4a52; color: #4fd1c5; }
|
|
78
|
+
.badge-haiku { background: #22543d; color: #68d391; }
|
|
79
|
+
.badge-fable { background: #553052; color: #f687b3; }
|
|
80
|
+
.badge-model { background: #2d3748; color: #cbd5e0; }
|
|
59
81
|
.ts { color: #718096; font-size: 0.8rem; }
|
|
60
|
-
#refresh-indicator {
|
|
61
|
-
|
|
82
|
+
#refresh-indicator { float: right; font-size: 0.75rem; color: #4a5568; }
|
|
83
|
+
.error { color: #fc8181; font-size: 0.85rem; margin-top: 8px; }
|
|
84
|
+
.btn-stream {
|
|
85
|
+
background: #44337a;
|
|
86
|
+
color: #d6bcfa;
|
|
87
|
+
border: 1px solid #6b46c1;
|
|
88
|
+
border-radius: 4px;
|
|
89
|
+
padding: 2px 8px;
|
|
62
90
|
font-size: 0.75rem;
|
|
63
|
-
|
|
91
|
+
cursor: pointer;
|
|
92
|
+
}
|
|
93
|
+
.btn-stream:hover { background: #6b46c1; color: #faf5ff; }
|
|
94
|
+
.pty-viewer {
|
|
95
|
+
display: none;
|
|
96
|
+
margin-top: 24px;
|
|
97
|
+
border: 1px solid #2d3748;
|
|
98
|
+
border-radius: 6px;
|
|
99
|
+
overflow-x: hidden;
|
|
100
|
+
}
|
|
101
|
+
.pty-viewer-header {
|
|
102
|
+
background: #1a202c;
|
|
103
|
+
padding: 8px 12px;
|
|
104
|
+
display: flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
justify-content: space-between;
|
|
107
|
+
font-size: 0.85rem;
|
|
108
|
+
color: #a0aec0;
|
|
109
|
+
}
|
|
110
|
+
.pty-viewer-header .agent-label { color: #63b3ed; font-weight: 600; }
|
|
111
|
+
.pty-viewer-header .session-label { color: #718096; font-family: monospace; font-size: 0.78rem; }
|
|
112
|
+
.pty-close, .pty-refresh {
|
|
113
|
+
background: none;
|
|
114
|
+
border: none;
|
|
115
|
+
color: #718096;
|
|
116
|
+
cursor: pointer;
|
|
117
|
+
font-size: 1rem;
|
|
118
|
+
padding: 0 4px;
|
|
119
|
+
}
|
|
120
|
+
.pty-close:hover { color: #fc8181; }
|
|
121
|
+
.pty-refresh:hover { color: #63b3ed; }
|
|
122
|
+
/* Fixed-size terminal viewport — the server PTY runs at 200x50, so the
|
|
123
|
+
viewer must NOT resize to the panel (that mismatch is what garbles the
|
|
124
|
+
output). We render at the native size and pan horizontally if the 200-col
|
|
125
|
+
width overflows the panel. The Claude TUI uses the alternate screen buffer
|
|
126
|
+
(\x1b[?1049h), which has no scrollback by design — so there is nothing to
|
|
127
|
+
scroll vertically and we hide the (non-functional) vertical scrollbar. */
|
|
128
|
+
#pty-terminal {
|
|
129
|
+
padding: 8px;
|
|
130
|
+
background: #0d1117;
|
|
131
|
+
overflow-x: auto;
|
|
132
|
+
overflow-y: hidden;
|
|
133
|
+
border-radius: 6px;
|
|
134
|
+
}
|
|
135
|
+
/* No scrollback in alt-screen mode → suppress xterm's vertical scrollbar. */
|
|
136
|
+
#pty-terminal .xterm-viewport { overflow-y: hidden !important; }
|
|
137
|
+
.proc-tree {
|
|
138
|
+
font-family: monospace;
|
|
139
|
+
font-size: 0.82rem;
|
|
140
|
+
background: #0d1117;
|
|
141
|
+
border: 1px solid #2d3748;
|
|
142
|
+
border-radius: 6px;
|
|
143
|
+
padding: 12px 16px;
|
|
144
|
+
white-space: pre-wrap;
|
|
145
|
+
word-break: break-word;
|
|
146
|
+
color: #a0aec0;
|
|
147
|
+
}
|
|
148
|
+
.proc-tree .proc-orchestrator { color: #63b3ed; }
|
|
149
|
+
.proc-tree .proc-pty { color: #68d391; }
|
|
150
|
+
.proc-tree .proc-claude { color: #f6e05e; }
|
|
151
|
+
.proc-tree .proc-mcp { color: #b794f4; }
|
|
152
|
+
.proc-tree .proc-receiver { color: #76e4f7; }
|
|
153
|
+
.proc-tree .proc-orphan { color: #fc8181; }
|
|
154
|
+
.proc-tree .proc-label { color: #718096; }
|
|
155
|
+
.proc-tree .proc-summary { color: #f6e05e; font-weight: 600; }
|
|
156
|
+
.session-id {
|
|
157
|
+
font-family: monospace;
|
|
158
|
+
font-size: 0.75rem;
|
|
159
|
+
color: #a0aec0;
|
|
160
|
+
word-break: break-all;
|
|
161
|
+
}
|
|
162
|
+
/* Agent status badges bar */
|
|
163
|
+
.agents-bar {
|
|
164
|
+
display: flex;
|
|
165
|
+
flex-wrap: wrap;
|
|
166
|
+
gap: 8px;
|
|
167
|
+
margin-bottom: 20px;
|
|
168
|
+
}
|
|
169
|
+
.agent-badge {
|
|
170
|
+
display: inline-flex;
|
|
171
|
+
align-items: center;
|
|
172
|
+
gap: 6px;
|
|
173
|
+
background: #1a202c;
|
|
174
|
+
border: 1px solid #2d3748;
|
|
175
|
+
border-radius: 6px;
|
|
176
|
+
padding: 4px 12px;
|
|
177
|
+
font-size: 0.8rem;
|
|
178
|
+
}
|
|
179
|
+
.agent-badge .agent-name { color: #90cdf4; font-weight: 600; }
|
|
180
|
+
.agent-badge .dot-green { color: #68d391; }
|
|
181
|
+
.agent-badge .dot-red { color: #fc8181; }
|
|
182
|
+
|
|
183
|
+
/* Top row: Processes 70% | Agents 30%. Collapses to a single column on
|
|
184
|
+
narrow screens (see media query below). */
|
|
185
|
+
.top-grid {
|
|
186
|
+
display: grid;
|
|
187
|
+
grid-template-columns: 7fr 3fr;
|
|
188
|
+
gap: 24px;
|
|
189
|
+
align-items: start;
|
|
190
|
+
}
|
|
191
|
+
/* The Sessions table has 10 columns — too wide for phones. Wrap it so it
|
|
192
|
+
scrolls horizontally instead of breaking the layout. */
|
|
193
|
+
.table-wrap { width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
|
194
|
+
.table-wrap table { min-width: 820px; }
|
|
195
|
+
|
|
196
|
+
/* ── Responsive breakpoints ──────────────────────────────────────────── */
|
|
197
|
+
@media (max-width: 900px) {
|
|
198
|
+
.top-grid { grid-template-columns: 1fr; gap: 16px; }
|
|
199
|
+
}
|
|
200
|
+
@media (max-width: 640px) {
|
|
201
|
+
body { padding: 14px; }
|
|
202
|
+
h1 { font-size: 1.2rem; }
|
|
203
|
+
h2 { font-size: 1rem; }
|
|
204
|
+
.meta { font-size: 0.78rem; }
|
|
205
|
+
#refresh-indicator { float: none; display: block; margin-top: 4px; }
|
|
206
|
+
.proc-tree { font-size: 0.72rem; padding: 10px 12px; }
|
|
207
|
+
/* On phones allow horizontal pan only — no vertical clipping. */
|
|
64
208
|
}
|
|
65
|
-
.error { color: #fc8181; font-size: 0.85rem; margin-top: 8px; }
|
|
66
209
|
</style>
|
|
67
210
|
</head>
|
|
68
211
|
<body>
|
|
69
|
-
<h1>Claude Gateway <span id="refresh-indicator">refreshing...</span></h1>
|
|
212
|
+
<h1><span class="rainbow">Claude Gateway</span> <span id="gateway-version" style="font-size:0.75rem;color:#718096;"></span> <span id="refresh-indicator">refreshing...</span></h1>
|
|
70
213
|
<div class="meta">
|
|
71
|
-
Uptime: <span id="uptime"
|
|
72
|
-
Started: <span id="started-at"
|
|
73
|
-
Last updated: <span id="last-updated"
|
|
214
|
+
Uptime: <span id="uptime">—</span> |
|
|
215
|
+
Started: <span id="started-at">—</span> |
|
|
216
|
+
Last updated: <span id="last-updated">—</span>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<!-- Row: Processes 70% | Agent badges 30% (collapses on narrow screens) -->
|
|
220
|
+
<div class="top-grid">
|
|
221
|
+
<div>
|
|
222
|
+
<h2>Processes</h2>
|
|
223
|
+
<div class="proc-tree" id="proc-tree">Loading...</div>
|
|
224
|
+
</div>
|
|
225
|
+
<div>
|
|
226
|
+
<h2>Agents</h2>
|
|
227
|
+
<div class="agents-bar" id="agents-bar"></div>
|
|
228
|
+
</div>
|
|
74
229
|
</div>
|
|
75
230
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
<
|
|
84
|
-
<
|
|
85
|
-
</
|
|
86
|
-
</
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
<th>Last Activity</th>
|
|
115
|
-
</tr>
|
|
116
|
-
</thead>
|
|
117
|
-
<tbody id="sessions-tbody">
|
|
118
|
-
<tr><td colspan="4" class="ts">Loading...</td></tr>
|
|
119
|
-
</tbody>
|
|
120
|
-
</table>
|
|
231
|
+
<!-- PTY viewer — full width so the native 200-col terminal has room.
|
|
232
|
+
Placed above the Sessions table so the live mirror is the first thing
|
|
233
|
+
in view when streaming. -->
|
|
234
|
+
<div class="pty-viewer" id="pty-viewer">
|
|
235
|
+
<div class="pty-viewer-header">
|
|
236
|
+
<span>Shell Process Viewer — <span class="agent-label" id="pty-agent-label"></span><span class="session-label" id="pty-session-label"></span></span>
|
|
237
|
+
<span>
|
|
238
|
+
<button class="pty-refresh" id="pty-refresh-btn" title="Refresh (reconnect & redraw)">↺</button>
|
|
239
|
+
<button class="pty-close" id="pty-close-btn" title="Close">✕</button>
|
|
240
|
+
</span>
|
|
241
|
+
</div>
|
|
242
|
+
<div id="pty-terminal"></div>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
<!-- Sessions — full width (session-centric, flat list) -->
|
|
246
|
+
<h2>Sessions</h2>
|
|
247
|
+
<div class="table-wrap">
|
|
248
|
+
<table id="sessions-table">
|
|
249
|
+
<thead>
|
|
250
|
+
<tr>
|
|
251
|
+
<th>Agent</th>
|
|
252
|
+
<th>Session ID</th>
|
|
253
|
+
<th>Chat ID</th>
|
|
254
|
+
<th>Source</th>
|
|
255
|
+
<th>Mode</th>
|
|
256
|
+
<th>Model</th>
|
|
257
|
+
<th>Tokens</th>
|
|
258
|
+
<th>Status</th>
|
|
259
|
+
<th>Uptime</th>
|
|
260
|
+
<th>Spawned</th>
|
|
261
|
+
<th>Shell</th>
|
|
262
|
+
</tr>
|
|
263
|
+
</thead>
|
|
264
|
+
<tbody id="sessions-tbody">
|
|
265
|
+
<tr><td colspan="11" class="ts">Loading...</td></tr>
|
|
266
|
+
</tbody>
|
|
267
|
+
</table>
|
|
268
|
+
</div>
|
|
121
269
|
|
|
122
270
|
<div id="error-msg" class="error" style="display:none;"></div>
|
|
123
271
|
|
|
272
|
+
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@6.0.0/lib/xterm.min.js"
|
|
273
|
+
integrity="sha384-pELe6ZHtFxFcuYBq3gMkqvmnNIqUWnAYjBG5gThqQQCjWp8PJ/65MLK4lMIfEK1e" crossorigin="anonymous"></script>
|
|
124
274
|
<script>
|
|
275
|
+
// Read short-lived dashboard token from meta tag (10 min, server-issued at page load).
|
|
276
|
+
// The raw API key is never embedded in HTML — only this scoped, expiring token is.
|
|
277
|
+
const DASHBOARD_API_KEY = document.querySelector('meta[name="dash-token"]') ? document.querySelector('meta[name="dash-token"]').getAttribute('content') : '';
|
|
278
|
+
|
|
279
|
+
// Must match the server PTY size (src/shell/screen.ts ScreenModel defaults).
|
|
280
|
+
const PTY_COLS = 200;
|
|
281
|
+
const PTY_ROWS = 50;
|
|
282
|
+
|
|
125
283
|
function fmtUptime(seconds) {
|
|
126
284
|
const h = Math.floor(seconds / 3600);
|
|
127
285
|
const m = Math.floor((seconds % 3600) / 60);
|
|
@@ -131,86 +289,348 @@ function generateDashboardHtml() {
|
|
|
131
289
|
return s + 's';
|
|
132
290
|
}
|
|
133
291
|
|
|
292
|
+
// Spawned timestamp, formatted like "6/14/2026, 10:32:31 PM" (en-US, 12h).
|
|
134
293
|
function fmtTs(ts) {
|
|
135
|
-
if (!ts) return '<span class="ts"
|
|
294
|
+
if (!ts) return '<span class="ts">—</span>';
|
|
136
295
|
try {
|
|
137
296
|
const d = new Date(ts);
|
|
138
|
-
return '<span class="ts">' + d.
|
|
297
|
+
return '<span class="ts">' + d.toLocaleString('en-US', {
|
|
298
|
+
year: 'numeric', month: 'numeric', day: 'numeric',
|
|
299
|
+
hour: 'numeric', minute: '2-digit', second: '2-digit', hour12: true
|
|
300
|
+
}) + '</span>';
|
|
139
301
|
} catch(e) { return ts; }
|
|
140
302
|
}
|
|
141
303
|
|
|
142
|
-
function
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
304
|
+
function basePath() {
|
|
305
|
+
const p = window.location.pathname;
|
|
306
|
+
if (p.endsWith('/dashboard')) return p.slice(0, -10);
|
|
307
|
+
if (p.endsWith('/dashboard/')) return p.slice(0, -11);
|
|
308
|
+
return p.endsWith('/') ? p.slice(0, -1) : p;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function apiUrl(path) {
|
|
312
|
+
return basePath() + path;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function wsPtyUrl(agentId, sessionId) {
|
|
316
|
+
// Exchange the API key for a short-lived ticket so the key never appears in
|
|
317
|
+
// the WS URL (which would expose it in server logs and browser history).
|
|
318
|
+
// Streams are per-session, so the session id is always part of the request.
|
|
319
|
+
const base = basePath() + '/api/v1/agents/' + encodeURIComponent(agentId) + '/pty-stream';
|
|
320
|
+
const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
321
|
+
if (!DASHBOARD_API_KEY) {
|
|
322
|
+
return proto + '//' + window.location.host + base + '?session=' + encodeURIComponent(sessionId);
|
|
323
|
+
}
|
|
324
|
+
const r = await fetch(apiUrl('/api/v1/pty-stream-ticket'), {
|
|
325
|
+
method: 'POST',
|
|
326
|
+
headers: { 'Content-Type': 'application/json', 'X-Dash-Token': DASHBOARD_API_KEY },
|
|
327
|
+
body: JSON.stringify({ agentId, sessionId }),
|
|
328
|
+
});
|
|
329
|
+
const { ticket } = await r.json();
|
|
330
|
+
return proto + '//' + window.location.host + base + '?ticket=' + ticket;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ── PTY Viewer ───────────────────────────────────────────────────────────
|
|
334
|
+
let term = null;
|
|
335
|
+
let ptyWs = null;
|
|
336
|
+
let currentPtyAgent = null;
|
|
337
|
+
let currentPtySession = null;
|
|
338
|
+
// Auto-reconnect state for the PTY viewer. The dashboard tab can lose its
|
|
339
|
+
// WebSocket to a gateway restart, an idle timeout, or a transient network
|
|
340
|
+
// blip; rather than make the user click View again, reconnect with backoff.
|
|
341
|
+
let ptyReconnectTimer = null;
|
|
342
|
+
let ptyReconnectAttempts = 0;
|
|
343
|
+
const PTY_RECONNECT_MAX_MS = 10000;
|
|
344
|
+
// Absolute cap so a session that ends without a clean 4404 (e.g. the gateway
|
|
345
|
+
// is gone for good) doesn't retry every 10s forever. After this many failed
|
|
346
|
+
// attempts in a row we give up and tell the user to click View again.
|
|
347
|
+
const PTY_RECONNECT_MAX_ATTEMPTS = 10;
|
|
348
|
+
// Streaming UTF-8 decoder. The PTY stream carries raw UTF-8 bytes (box-drawing
|
|
349
|
+
// chars, spinner braille, emoji). Decoding them as latin1 mangles every
|
|
350
|
+
// multi-byte char into noise — decode as UTF-8 with {stream:true} so sequences
|
|
351
|
+
// split across WebSocket frames are reassembled instead of corrupted.
|
|
352
|
+
let utf8Decoder = null;
|
|
353
|
+
|
|
354
|
+
// The agent's TUI enables mouse tracking (DECSET 1000/1002/1003/1006...).
|
|
355
|
+
// While those modes are active, xterm.js forwards wheel/click events to the
|
|
356
|
+
// app as mouse escapes, which can leave stray report bytes in the view. This
|
|
357
|
+
// is a view-only mirror (disableStdin) so mouse reporting is useless here —
|
|
358
|
+
// strip the set/reset sequences to keep the mirror clean.
|
|
359
|
+
function stripMouseModes(s) {
|
|
360
|
+
return s.replace(/\\x1b\\[\\?(1000|1001|1002|1003|1004|1005|1006|1015|1016)[hl]/g, '');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function openPtyViewer(agentId, sessionId) {
|
|
364
|
+
// Compare the SESSION, not the agent: one agent can have several sessions,
|
|
365
|
+
// each its own stream. Guarding on agent alone made switching between two
|
|
366
|
+
// sessions of the same agent a no-op (the viewer never reconnected).
|
|
367
|
+
if (currentPtySession === sessionId && ptyWs && ptyWs.readyState === WebSocket.OPEN) return;
|
|
368
|
+
closePtyViewer();
|
|
369
|
+
|
|
370
|
+
currentPtyAgent = agentId;
|
|
371
|
+
currentPtySession = sessionId;
|
|
372
|
+
document.getElementById('pty-agent-label').textContent = agentId;
|
|
373
|
+
// Append the session id after the agent name, e.g. "claude-founder · 3c01897c…".
|
|
374
|
+
document.getElementById('pty-session-label').textContent = sessionId ? ' \\u00b7 ' + sessionId : '';
|
|
375
|
+
document.getElementById('pty-viewer').style.display = 'block';
|
|
376
|
+
|
|
377
|
+
if (!term) {
|
|
378
|
+
term = new Terminal({
|
|
379
|
+
theme: { background: '#0d1117', foreground: '#e2e8f0', cursor: '#63b3ed' },
|
|
380
|
+
fontSize: 11,
|
|
381
|
+
lineHeight: 1.0,
|
|
382
|
+
letterSpacing: 0,
|
|
383
|
+
fontFamily: '"JetBrains Mono", "Cascadia Code", Menlo, Monaco, Consolas, "Courier New", monospace',
|
|
384
|
+
fontWeight: 400,
|
|
385
|
+
fontWeightBold: 600,
|
|
386
|
+
// Fixed dimensions matching the server PTY — do NOT auto-fit, the
|
|
387
|
+
// size mismatch is what makes the output unreadable.
|
|
388
|
+
cols: PTY_COLS,
|
|
389
|
+
rows: PTY_ROWS,
|
|
390
|
+
// Alt-screen TUI has no scrollback (the live mirror only shows the
|
|
391
|
+
// current screen), so don't retain any — this also removes the
|
|
392
|
+
// non-functional vertical scrollbar.
|
|
393
|
+
scrollback: 0,
|
|
394
|
+
// View-only mirror of the agent's TUI.
|
|
395
|
+
disableStdin: true,
|
|
396
|
+
cursorBlink: false,
|
|
397
|
+
convertEol: false,
|
|
398
|
+
});
|
|
399
|
+
term.open(document.getElementById('pty-terminal'));
|
|
400
|
+
// After open(), xterm has measured cell height. Pin the container height
|
|
401
|
+
// so all PTY_ROWS are always visible — prevents the alt-screen bottom rows
|
|
402
|
+
// (cost bar, status line) from being clipped by parent overflow or viewport.
|
|
403
|
+
requestAnimationFrame(function() {
|
|
404
|
+
const screen = document.querySelector('#pty-terminal .xterm-screen');
|
|
405
|
+
if (screen) {
|
|
406
|
+
const h = screen.offsetHeight;
|
|
407
|
+
if (h > 0) document.getElementById('pty-terminal').style.minHeight = h + 'px';
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
} else {
|
|
411
|
+
term.reset();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
await connectPtyWs(agentId, sessionId);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// (Re)establish the WebSocket for the session the viewer is currently showing.
|
|
418
|
+
// Split out from openPtyViewer so auto-reconnect can re-run just this part
|
|
419
|
+
// without tearing down the terminal or flickering the panel.
|
|
420
|
+
async function connectPtyWs(agentId, sessionId) {
|
|
421
|
+
// Guard against a stale reconnect firing after the user closed/switched.
|
|
422
|
+
if (currentPtySession !== sessionId) return;
|
|
423
|
+
|
|
424
|
+
// Fresh decoder per (re)connect so a leftover partial byte can't corrupt
|
|
425
|
+
// the first character of the freshly-replayed screen frame.
|
|
426
|
+
utf8Decoder = new TextDecoder('utf-8');
|
|
427
|
+
|
|
428
|
+
let url;
|
|
429
|
+
try {
|
|
430
|
+
url = await wsPtyUrl(agentId, sessionId);
|
|
431
|
+
} catch (e) {
|
|
432
|
+
// Ticket fetch failed (gateway momentarily unreachable) — retry.
|
|
433
|
+
schedulePtyReconnect(agentId, sessionId);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
// The session may have been closed/switched while awaiting the ticket.
|
|
437
|
+
if (currentPtySession !== sessionId) return;
|
|
438
|
+
|
|
439
|
+
ptyWs = new WebSocket(url);
|
|
440
|
+
ptyWs.binaryType = 'arraybuffer';
|
|
441
|
+
|
|
442
|
+
ptyWs.onopen = function() { ptyReconnectAttempts = 0; };
|
|
443
|
+
ptyWs.onmessage = function(ev) {
|
|
444
|
+
const data = ev.data instanceof ArrayBuffer
|
|
445
|
+
? utf8Decoder.decode(ev.data, { stream: true })
|
|
446
|
+
: ev.data;
|
|
447
|
+
term.write(stripMouseModes(data));
|
|
448
|
+
};
|
|
449
|
+
ptyWs.onclose = function(ev) {
|
|
450
|
+
ptyWs = null;
|
|
451
|
+
// Viewer was closed or switched to another session — stop here.
|
|
452
|
+
if (currentPtySession !== sessionId) return;
|
|
453
|
+
// 4404 = the session is no longer running in PTY mode (it ended). No
|
|
454
|
+
// point reconnecting; the server will never accept this stream again.
|
|
455
|
+
if (ev.code === 4404) {
|
|
456
|
+
if (term) term.writeln('\\r\\n\\x1b[33m[session ended]\\x1b[0m');
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
schedulePtyReconnect(agentId, sessionId);
|
|
460
|
+
};
|
|
461
|
+
// onerror is always followed by onclose, which owns the reconnect logic.
|
|
462
|
+
ptyWs.onerror = function() {};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Reconnect with capped exponential backoff (1s -> 10s). A single
|
|
466
|
+
// "reconnecting" notice is shown per disconnect burst; the server replays a
|
|
467
|
+
// clean frame on resubscribe, so the view redraws itself once we are back.
|
|
468
|
+
function schedulePtyReconnect(agentId, sessionId) {
|
|
469
|
+
if (ptyReconnectTimer) return; // already pending
|
|
470
|
+
if (currentPtySession !== sessionId) return; // viewer no longer wants this
|
|
471
|
+
if (ptyReconnectAttempts >= PTY_RECONNECT_MAX_ATTEMPTS) {
|
|
472
|
+
if (term) term.writeln('\\r\\n\\x1b[31m[disconnected — click View to retry]\\x1b[0m');
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (ptyReconnectAttempts === 0 && term) {
|
|
476
|
+
term.writeln('\\r\\n\\x1b[33m[reconnecting\\u2026]\\x1b[0m');
|
|
477
|
+
}
|
|
478
|
+
const delay = Math.min(1000 * Math.pow(2, ptyReconnectAttempts), PTY_RECONNECT_MAX_MS);
|
|
479
|
+
ptyReconnectAttempts++;
|
|
480
|
+
ptyReconnectTimer = setTimeout(function() {
|
|
481
|
+
ptyReconnectTimer = null;
|
|
482
|
+
void connectPtyWs(agentId, sessionId);
|
|
483
|
+
}, delay);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function closePtyViewer() {
|
|
487
|
+
// Cancel any pending reconnect and suppress the handlers on the socket we
|
|
488
|
+
// are about to close intentionally, so it does not schedule a new one.
|
|
489
|
+
if (ptyReconnectTimer) { clearTimeout(ptyReconnectTimer); ptyReconnectTimer = null; }
|
|
490
|
+
ptyReconnectAttempts = 0;
|
|
491
|
+
if (ptyWs) { ptyWs.onclose = null; ptyWs.onerror = null; ptyWs.close(); ptyWs = null; }
|
|
492
|
+
currentPtyAgent = null;
|
|
493
|
+
currentPtySession = null;
|
|
494
|
+
document.getElementById('pty-viewer').style.display = 'none';
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Refresh: force a clean reconnect of the CURRENT session. The server replays
|
|
498
|
+
// a freshly-serialized screen frame on subscribe, so this redraws from a clean
|
|
499
|
+
// xterm state — a manual escape hatch if the live stream ever drifts.
|
|
500
|
+
async function refreshPtyViewer() {
|
|
501
|
+
const agentId = currentPtyAgent;
|
|
502
|
+
const sessionId = currentPtySession;
|
|
503
|
+
if (!agentId) return;
|
|
504
|
+
closePtyViewer();
|
|
505
|
+
await openPtyViewer(agentId, sessionId);
|
|
146
506
|
}
|
|
147
507
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
508
|
+
document.getElementById('pty-close-btn').addEventListener('click', closePtyViewer);
|
|
509
|
+
document.getElementById('pty-refresh-btn').addEventListener('click', function() { void refreshPtyViewer(); });
|
|
510
|
+
|
|
511
|
+
// Event delegation for Live buttons (avoids inline onclick + HTML injection)
|
|
512
|
+
document.getElementById('sessions-tbody').addEventListener('click', function(e) {
|
|
513
|
+
const btn = e.target.closest('.btn-stream');
|
|
514
|
+
if (btn) void openPtyViewer(btn.getAttribute('data-agent-id'), btn.getAttribute('data-session-id'));
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
function escHtml(s) {
|
|
518
|
+
return String(s)
|
|
519
|
+
.replace(/&/g, '&')
|
|
520
|
+
.replace(/</g, '<')
|
|
521
|
+
.replace(/>/g, '>')
|
|
522
|
+
.replace(/"/g, '"');
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function modeBadge(mode) {
|
|
526
|
+
if (mode === 'pty-shell') return '<span class="badge badge-blue">wrap-shell</span>';
|
|
527
|
+
if (mode === 'headless') return '<span class="badge badge-purple">headless</span>';
|
|
528
|
+
return '<span class="ts">' + escHtml(mode || '?') + '</span>';
|
|
152
529
|
}
|
|
153
530
|
|
|
531
|
+
// Source badge with a per-channel color: telegram=blue, discord=purple,
|
|
532
|
+
// api=gray. Unknown sources fall back to gray.
|
|
533
|
+
function sourceBadge(source) {
|
|
534
|
+
const s = String(source || '?').toLowerCase();
|
|
535
|
+
if (s === 'telegram') return '<span class="badge badge-blue">telegram</span>';
|
|
536
|
+
if (s === 'discord') return '<span class="badge badge-purple">discord</span>';
|
|
537
|
+
if (s === 'api') return '<span class="badge badge-gray">api</span>';
|
|
538
|
+
return '<span class="badge badge-gray">' + escHtml(source || '?') + '</span>';
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Prettify a model id for display: drop the "claude-" prefix and any
|
|
542
|
+
// trailing date stamp, e.g. claude-haiku-4-5-20251001 -> haiku-4-5.
|
|
543
|
+
// Each model family gets a distinct badge color; full id kept in the tooltip.
|
|
544
|
+
function fmtModel(m) {
|
|
545
|
+
if (!m) return '<span class="ts">—</span>';
|
|
546
|
+
const id = String(m);
|
|
547
|
+
const label = id.replace(/^claude-/, '').replace(/-\\d{8}$/, '');
|
|
548
|
+
let cls = 'badge-model';
|
|
549
|
+
if (/opus/i.test(id)) cls = 'badge-opus';
|
|
550
|
+
else if (/sonnet/i.test(id)) cls = 'badge-sonnet';
|
|
551
|
+
else if (/haiku/i.test(id)) cls = 'badge-haiku';
|
|
552
|
+
else if (/fable/i.test(id)) cls = 'badge-fable';
|
|
553
|
+
return '<span class="badge ' + cls + '" title="' + escHtml(id) + '">' + escHtml(label) + '</span>';
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Format a context-window token count compactly: 1234 -> "1.2k", 45000 -> "45k".
|
|
557
|
+
// Full value is kept in the tooltip. 0/unknown renders as a dash.
|
|
558
|
+
function fmtTokens(n) {
|
|
559
|
+
const v = Number(n) || 0;
|
|
560
|
+
if (v <= 0) return '<span class="ts">—</span>';
|
|
561
|
+
const label = v >= 1000 ? (v / 1000).toFixed(v >= 10000 ? 0 : 1) + 'k' : String(v);
|
|
562
|
+
return '<span title="' + v.toLocaleString() + ' tokens">' + label + '</span>';
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// ── Status Refresh ────────────────────────────────────────────────────────
|
|
154
566
|
async function refresh() {
|
|
155
567
|
document.getElementById('refresh-indicator').textContent = 'refreshing...';
|
|
156
568
|
try {
|
|
157
|
-
const res = await fetch('/status');
|
|
569
|
+
const res = await fetch(apiUrl('/status'));
|
|
158
570
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
159
571
|
const data = await res.json();
|
|
160
572
|
|
|
161
573
|
document.getElementById('uptime').textContent = fmtUptime(data.uptime || 0);
|
|
162
574
|
document.getElementById('started-at').textContent = data.startedAt
|
|
163
|
-
? new Date(data.startedAt).toLocaleString() : '
|
|
575
|
+
? new Date(data.startedAt).toLocaleString() : '\\u2014';
|
|
164
576
|
document.getElementById('last-updated').textContent = new Date().toLocaleTimeString();
|
|
577
|
+
if (data.version) document.getElementById('gateway-version').textContent = 'v' + data.version;
|
|
165
578
|
document.getElementById('error-msg').style.display = 'none';
|
|
166
579
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
agentRows.length ? agentRows.join('') : '<tr><td colspan="5" class="ts">No agents</td></tr>';
|
|
179
|
-
|
|
180
|
-
// Heartbeat table
|
|
181
|
-
const hbRows = [];
|
|
182
|
-
(data.agents || []).forEach(function(a) {
|
|
183
|
-
const lastResults = (a.heartbeat && a.heartbeat.lastResults) || [];
|
|
184
|
-
lastResults.forEach(function(r) {
|
|
185
|
-
if (!r) return;
|
|
186
|
-
hbRows.push('<tr>' +
|
|
187
|
-
'<td>' + a.id + '</td>' +
|
|
188
|
-
'<td>' + r.taskName + '</td>' +
|
|
189
|
-
'<td>' + fmtTs(r.ts) + '</td>' +
|
|
190
|
-
'<td>' + resultBadge(r.suppressed, r.rateLimited) + '</td>' +
|
|
191
|
-
'</tr>');
|
|
192
|
-
});
|
|
580
|
+
const agents = data.agents || [];
|
|
581
|
+
|
|
582
|
+
// Agent badges bar.
|
|
583
|
+
// Green = available. An agent with a channel receiver (telegram/discord)
|
|
584
|
+
// is green only while its receiver is running; API-only agents have no
|
|
585
|
+
// receiver, so they are always green as long as the gateway loaded them.
|
|
586
|
+
// Red = a channel agent whose receiver is down (genuinely stopped).
|
|
587
|
+
const badges = agents.map(function(a) {
|
|
588
|
+
const ok = a.hasChannel ? a.isRunning : true;
|
|
589
|
+
const dot = ok ? '<span class="dot-green">●</span>' : '<span class="dot-red">●</span>';
|
|
590
|
+
return '<span class="agent-badge">' + dot + ' <span class="agent-name">' + escHtml(a.id) + '</span></span>';
|
|
193
591
|
});
|
|
194
|
-
document.getElementById('
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
//
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
'<
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
'<
|
|
207
|
-
'
|
|
592
|
+
document.getElementById('agents-bar').innerHTML = badges.join('') || '<span class="ts">No agents</span>';
|
|
593
|
+
|
|
594
|
+
// Sessions table — flat, session-centric. One row per real session across
|
|
595
|
+
// all agents; agents with no session do not produce a row.
|
|
596
|
+
const rows = [];
|
|
597
|
+
agents.forEach(function(a) {
|
|
598
|
+
(a.sessions || []).forEach(function(s) {
|
|
599
|
+
const statusBadge = s.isRunning
|
|
600
|
+
? '<span class="badge badge-green">running</span>'
|
|
601
|
+
: '<span class="badge badge-gray">stopped</span>';
|
|
602
|
+
const uptime = s.isRunning ? fmtUptime(s.uptimeSec || 0) : '<span class="ts">—</span>';
|
|
603
|
+
const sessId = s.sessionId
|
|
604
|
+
? '<span class="session-id">' + escHtml(s.sessionId) + '</span>'
|
|
605
|
+
: '<span class="ts">—</span>';
|
|
606
|
+
const chatCell = s.chatId
|
|
607
|
+
? '<span class="session-id">' + escHtml(String(s.chatId)) + '</span>'
|
|
608
|
+
: '<span class="ts">—</span>';
|
|
609
|
+
const liveBtn = (s.hasPtyStream && s.isRunning && s.mode === 'pty-shell')
|
|
610
|
+
? '<button class="btn-stream" data-agent-id="' + escHtml(a.id) + '" data-session-id="' + escHtml(s.sessionId || '') + '">💻 View</button>'
|
|
611
|
+
: '<span class="ts">—</span>';
|
|
612
|
+
rows.push(
|
|
613
|
+
'<tr class="session-row">' +
|
|
614
|
+
'<td><span style="color:#90cdf4;font-weight:600;">' + escHtml(a.id) + '</span></td>' +
|
|
615
|
+
'<td>' + sessId + '</td>' +
|
|
616
|
+
'<td>' + chatCell + '</td>' +
|
|
617
|
+
'<td>' + sourceBadge(s.source) + '</td>' +
|
|
618
|
+
'<td>' + modeBadge(s.mode) + '</td>' +
|
|
619
|
+
'<td>' + fmtModel(s.model) + '</td>' +
|
|
620
|
+
'<td>' + fmtTokens(s.tokens) + '</td>' +
|
|
621
|
+
'<td>' + statusBadge + '</td>' +
|
|
622
|
+
'<td>' + uptime + '</td>' +
|
|
623
|
+
'<td>' + fmtTs(s.spawnedAt ? new Date(s.spawnedAt).toISOString() : null) + '</td>' +
|
|
624
|
+
'<td>' + liveBtn + '</td>' +
|
|
625
|
+
'</tr>'
|
|
626
|
+
);
|
|
208
627
|
});
|
|
209
628
|
});
|
|
629
|
+
|
|
210
630
|
document.getElementById('sessions-tbody').innerHTML =
|
|
211
|
-
|
|
631
|
+
rows.length ? rows.join('') : '<tr><td colspan="11" class="ts">No active sessions</td></tr>';
|
|
212
632
|
|
|
213
|
-
document.getElementById('refresh-indicator').textContent = 'auto-refresh
|
|
633
|
+
document.getElementById('refresh-indicator').textContent = 'auto-refresh 3s';
|
|
214
634
|
} catch(e) {
|
|
215
635
|
document.getElementById('error-msg').textContent = 'Error fetching status: ' + e.message;
|
|
216
636
|
document.getElementById('error-msg').style.display = 'block';
|
|
@@ -218,8 +638,156 @@ function generateDashboardHtml() {
|
|
|
218
638
|
}
|
|
219
639
|
}
|
|
220
640
|
|
|
641
|
+
// ── Process Tree ─────────────────────────────────────────────────────────
|
|
642
|
+
async function refreshProcesses() {
|
|
643
|
+
try {
|
|
644
|
+
const res = await fetch(apiUrl('/processes'));
|
|
645
|
+
if (!res.ok) return;
|
|
646
|
+
const data = await res.json();
|
|
647
|
+
renderProcessTree(data.processes || [], data.numCpus || 1);
|
|
648
|
+
} catch(e) {
|
|
649
|
+
document.getElementById('proc-tree').textContent = 'Error: ' + e.message;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function renderProcessTree(procs, numCpus) {
|
|
654
|
+
if (!procs.length) {
|
|
655
|
+
document.getElementById('proc-tree').textContent = '— no gateway processes found —';
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const pidMap = {};
|
|
660
|
+
procs.forEach(function(p) { pidMap[p.pid] = p; });
|
|
661
|
+
|
|
662
|
+
// Aggregate resource usage across the whole gateway process tree.
|
|
663
|
+
// ps %cpu is per-core (100% = 1 core), so divide by numCpus to get
|
|
664
|
+
// a normalized 0–100% load figure across all available cores.
|
|
665
|
+
// RSS is summed and may slightly over-count shared pages.
|
|
666
|
+
let rawCpuSum = 0, totalRssKb = 0;
|
|
667
|
+
procs.forEach(function(p) {
|
|
668
|
+
rawCpuSum += Number(p.cpu) || 0;
|
|
669
|
+
totalRssKb += Number(p.rssKb) || 0;
|
|
670
|
+
});
|
|
671
|
+
const totalCpu = rawCpuSum / (numCpus || 1);
|
|
672
|
+
const totalMemMb = totalRssKb / 1024;
|
|
673
|
+
const memStr = totalMemMb >= 1024
|
|
674
|
+
? (totalMemMb / 1024).toFixed(2) + ' GB'
|
|
675
|
+
: totalMemMb.toFixed(0) + ' MB';
|
|
676
|
+
|
|
677
|
+
function cat(p) {
|
|
678
|
+
const a = p.args;
|
|
679
|
+
if (a.includes('node') && a.includes('dist/index')) return 'orchestrator';
|
|
680
|
+
if (a.includes('claude-pty-shell')) return 'pty';
|
|
681
|
+
if (a.includes('bun') && a.includes('mcp/server')) return 'mcp';
|
|
682
|
+
if (a.includes('bun') && a.includes('telegram') && a.includes('receiver')) return 'telegram';
|
|
683
|
+
if (a.includes('bun') && a.includes('discord') && a.includes('receiver')) return 'discord';
|
|
684
|
+
if (a.includes('--mcp-config') && (a.includes('--session-id') || a.includes('--print'))) {
|
|
685
|
+
if (a.includes('--session-id')) {
|
|
686
|
+
const parent = pidMap[p.ppid];
|
|
687
|
+
return (parent && cat(parent) === 'pty') ? 'claude-pty' : 'claude-headless';
|
|
688
|
+
}
|
|
689
|
+
return 'claude-headless';
|
|
690
|
+
}
|
|
691
|
+
return 'other';
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// Show full command lines (no truncation) — text wraps inside the box.
|
|
695
|
+
function full(args) {
|
|
696
|
+
return escHtml(args);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function sessionId(args) {
|
|
700
|
+
const m = args.match(/--session-id\\s+(\\S+)/);
|
|
701
|
+
return m ? escHtml(m[1]) : '?';
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function agentName(args) {
|
|
705
|
+
const m = args.match(/agents\\/([^/]+)\\/workspace/);
|
|
706
|
+
return m ? m[1] : '?';
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const lines = [];
|
|
710
|
+
// First line: total resource usage across the whole gateway tree.
|
|
711
|
+
lines.push(
|
|
712
|
+
'<span class="proc-summary">' +
|
|
713
|
+
'\\u2211 ' + procs.length + ' procs' +
|
|
714
|
+
' \\u00b7 CPU ' + totalCpu.toFixed(1) + '%' +
|
|
715
|
+
' \\u00b7 MEM ' + memStr +
|
|
716
|
+
'</span>'
|
|
717
|
+
);
|
|
718
|
+
lines.push('');
|
|
719
|
+
const orchestrator = procs.find(function(p) { return cat(p) === 'orchestrator'; });
|
|
720
|
+
const ptys = procs.filter(function(p) { return cat(p) === 'pty'; });
|
|
721
|
+
const headless = procs.filter(function(p) { return cat(p) === 'claude-headless'; });
|
|
722
|
+
const telegramReceivers = procs.filter(function(p) { return cat(p) === 'telegram'; });
|
|
723
|
+
const discordReceivers = procs.filter(function(p) { return cat(p) === 'discord'; });
|
|
724
|
+
const mcpServers = procs.filter(function(p) { return cat(p) === 'mcp'; });
|
|
725
|
+
|
|
726
|
+
const gatewayPids = new Set(procs.map(function(p) { return p.pid; }));
|
|
727
|
+
const orphans = procs.filter(function(p) {
|
|
728
|
+
const c = cat(p);
|
|
729
|
+
return (c === 'claude-pty' || c === 'claude-headless' || c === 'pty' || c === 'mcp')
|
|
730
|
+
&& !gatewayPids.has(p.ppid)
|
|
731
|
+
&& p.pid !== (orchestrator && orchestrator.pid);
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
if (orchestrator) {
|
|
735
|
+
lines.push('<span class="proc-orchestrator">Orchestrator</span>');
|
|
736
|
+
lines.push(' PID ' + orchestrator.pid + ' <span class="proc-orchestrator">' + full(orchestrator.args) + '</span>');
|
|
737
|
+
lines.push('');
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const sessionCount = ptys.length + headless.length;
|
|
741
|
+
lines.push('<span class="proc-label">Sessions (' + sessionCount + ')</span>');
|
|
742
|
+
|
|
743
|
+
ptys.forEach(function(pty) {
|
|
744
|
+
const agent = agentName(pty.args);
|
|
745
|
+
lines.push(' PID ' + pty.pid + ' <span class="proc-pty">wrap-shell</span> [' + agent + ']');
|
|
746
|
+
const claudeChild = procs.find(function(p) { return p.ppid === pty.pid && cat(p) === 'claude-pty'; });
|
|
747
|
+
if (claudeChild) {
|
|
748
|
+
lines.push(' \\u2514\\u2500 PID ' + claudeChild.pid + ' <span class="proc-claude">claude ' + sessionId(claudeChild.args) + '</span>');
|
|
749
|
+
const mcp = mcpServers.find(function(p) { return p.ppid === claudeChild.pid; });
|
|
750
|
+
if (mcp) {
|
|
751
|
+
lines.push(' \\u2514\\u2500 PID ' + mcp.pid + ' <span class="proc-mcp">mcp</span>');
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
headless.forEach(function(cl) {
|
|
757
|
+
const agent = agentName(cl.args);
|
|
758
|
+
lines.push(' PID ' + cl.pid + ' <span class="proc-claude">claude --print</span>' + (agent !== '?' ? ' [' + agent + ']' : ''));
|
|
759
|
+
const mcp = mcpServers.find(function(p) { return p.ppid === cl.pid; });
|
|
760
|
+
if (mcp) {
|
|
761
|
+
lines.push(' \\u2514\\u2500 PID ' + mcp.pid + ' <span class="proc-mcp">mcp</span>');
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
if (sessionCount === 0) lines.push(' <span class="ts">\\u2014 none \\u2014</span>');
|
|
766
|
+
lines.push('');
|
|
767
|
+
|
|
768
|
+
lines.push('<span class="proc-label">Receivers</span>');
|
|
769
|
+
if (telegramReceivers.length) lines.push(' Telegram \\u00d7' + telegramReceivers.length);
|
|
770
|
+
if (discordReceivers.length) lines.push(' Discord \\u00d7' + discordReceivers.length);
|
|
771
|
+
if (!telegramReceivers.length && !discordReceivers.length) lines.push(' <span class="ts">\\u2014 none \\u2014</span>');
|
|
772
|
+
lines.push('');
|
|
773
|
+
|
|
774
|
+
lines.push('<span class="proc-label">Orphans</span>');
|
|
775
|
+
if (orphans.length) {
|
|
776
|
+
orphans.forEach(function(p) {
|
|
777
|
+
lines.push(' \\u26a0 PID ' + p.pid + ' <span class="proc-orphan">' + full(p.args) + '</span>');
|
|
778
|
+
});
|
|
779
|
+
} else {
|
|
780
|
+
lines.push(' <span class="ts">none \\u2705</span>');
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
document.getElementById('proc-tree').innerHTML = lines.join('\\n');
|
|
784
|
+
}
|
|
785
|
+
|
|
221
786
|
refresh();
|
|
222
|
-
|
|
787
|
+
refreshProcesses();
|
|
788
|
+
setInterval(refresh, 3000);
|
|
789
|
+
// Process tree (with CPU/mem) is heavier (spawns ps) — refresh a bit slower.
|
|
790
|
+
setInterval(refreshProcesses, 6000);
|
|
223
791
|
</script>
|
|
224
792
|
</body>
|
|
225
793
|
</html>`;
|