instar 0.7.52 → 0.7.53
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/.vercel/README.txt +11 -0
- package/.vercel/project.json +1 -0
- package/dashboard/index.html +843 -0
- package/dist/commands/server.js +27 -1
- package/dist/core/AutoUpdater.js +48 -16
- package/dist/core/SessionManager.d.ts +6 -0
- package/dist/core/SessionManager.js +31 -27
- package/dist/core/UpdateChecker.d.ts +2 -1
- package/dist/core/UpdateChecker.js +37 -10
- package/dist/core/types.d.ts +8 -0
- package/dist/monitoring/HealthChecker.d.ts +3 -1
- package/dist/monitoring/HealthChecker.js +15 -2
- package/dist/monitoring/SessionWatchdog.d.ts +83 -0
- package/dist/monitoring/SessionWatchdog.js +326 -0
- package/dist/server/AgentServer.d.ts +2 -0
- package/dist/server/AgentServer.js +1 -0
- package/dist/server/WebSocketManager.d.ts +62 -0
- package/dist/server/WebSocketManager.js +288 -0
- package/dist/server/routes.d.ts +2 -0
- package/dist/server/routes.js +21 -0
- package/package.json +4 -2
|
@@ -0,0 +1,843 @@
|
|
|
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>Instar Dashboard</title>
|
|
7
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css">
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
--bg: #0a0a0a;
|
|
11
|
+
--bg-panel: #111;
|
|
12
|
+
--bg-hover: #1a1a1a;
|
|
13
|
+
--bg-active: #1e2d1e;
|
|
14
|
+
--border: #222;
|
|
15
|
+
--text: #ccc;
|
|
16
|
+
--text-dim: #666;
|
|
17
|
+
--text-bright: #eee;
|
|
18
|
+
--accent: #4ade80;
|
|
19
|
+
--accent-dim: #22c55e;
|
|
20
|
+
--orange: #f59e0b;
|
|
21
|
+
--red: #ef4444;
|
|
22
|
+
--blue: #3b82f6;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
26
|
+
|
|
27
|
+
body {
|
|
28
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
29
|
+
background: var(--bg);
|
|
30
|
+
color: var(--text);
|
|
31
|
+
height: 100vh;
|
|
32
|
+
overflow: hidden;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* Layout */
|
|
36
|
+
.app {
|
|
37
|
+
display: grid;
|
|
38
|
+
grid-template-rows: auto 1fr;
|
|
39
|
+
grid-template-columns: 280px 1fr;
|
|
40
|
+
height: 100vh;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* Header */
|
|
44
|
+
.header {
|
|
45
|
+
grid-column: 1 / -1;
|
|
46
|
+
display: flex;
|
|
47
|
+
align-items: center;
|
|
48
|
+
justify-content: space-between;
|
|
49
|
+
padding: 12px 20px;
|
|
50
|
+
border-bottom: 1px solid var(--border);
|
|
51
|
+
background: var(--bg-panel);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.header-left {
|
|
55
|
+
display: flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
gap: 12px;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.header h1 {
|
|
61
|
+
font-size: 16px;
|
|
62
|
+
font-weight: 600;
|
|
63
|
+
color: var(--text-bright);
|
|
64
|
+
letter-spacing: -0.02em;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.header .logo {
|
|
68
|
+
font-size: 20px;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.status-badge {
|
|
72
|
+
display: inline-flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
gap: 6px;
|
|
75
|
+
font-size: 12px;
|
|
76
|
+
padding: 3px 10px;
|
|
77
|
+
border-radius: 12px;
|
|
78
|
+
background: #0f2f0f;
|
|
79
|
+
color: var(--accent);
|
|
80
|
+
border: 1px solid #1a3a1a;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.status-badge .dot {
|
|
84
|
+
width: 6px;
|
|
85
|
+
height: 6px;
|
|
86
|
+
border-radius: 50%;
|
|
87
|
+
background: var(--accent);
|
|
88
|
+
animation: pulse 2s infinite;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.status-badge.disconnected {
|
|
92
|
+
background: #2f0f0f;
|
|
93
|
+
color: var(--red);
|
|
94
|
+
border-color: #3a1a1a;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.status-badge.disconnected .dot {
|
|
98
|
+
background: var(--red);
|
|
99
|
+
animation: none;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@keyframes pulse {
|
|
103
|
+
0%, 100% { opacity: 1; }
|
|
104
|
+
50% { opacity: 0.4; }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* Sidebar */
|
|
108
|
+
.sidebar {
|
|
109
|
+
border-right: 1px solid var(--border);
|
|
110
|
+
background: var(--bg-panel);
|
|
111
|
+
overflow-y: auto;
|
|
112
|
+
display: flex;
|
|
113
|
+
flex-direction: column;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.sidebar-header {
|
|
117
|
+
padding: 16px;
|
|
118
|
+
border-bottom: 1px solid var(--border);
|
|
119
|
+
display: flex;
|
|
120
|
+
align-items: center;
|
|
121
|
+
justify-content: space-between;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.sidebar-header h2 {
|
|
125
|
+
font-size: 13px;
|
|
126
|
+
font-weight: 600;
|
|
127
|
+
color: var(--text-dim);
|
|
128
|
+
text-transform: uppercase;
|
|
129
|
+
letter-spacing: 0.05em;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.session-count {
|
|
133
|
+
font-size: 11px;
|
|
134
|
+
padding: 2px 8px;
|
|
135
|
+
border-radius: 10px;
|
|
136
|
+
background: var(--border);
|
|
137
|
+
color: var(--text-dim);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.session-list {
|
|
141
|
+
flex: 1;
|
|
142
|
+
overflow-y: auto;
|
|
143
|
+
padding: 8px;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.session-item {
|
|
147
|
+
padding: 10px 12px;
|
|
148
|
+
border-radius: 8px;
|
|
149
|
+
cursor: pointer;
|
|
150
|
+
margin-bottom: 2px;
|
|
151
|
+
transition: background 0.15s;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.session-item:hover {
|
|
155
|
+
background: var(--bg-hover);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.session-item.active {
|
|
159
|
+
background: var(--bg-active);
|
|
160
|
+
border: 1px solid #2a4a2a;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.session-name {
|
|
164
|
+
font-size: 13px;
|
|
165
|
+
font-weight: 500;
|
|
166
|
+
color: var(--text-bright);
|
|
167
|
+
margin-bottom: 4px;
|
|
168
|
+
white-space: nowrap;
|
|
169
|
+
overflow: hidden;
|
|
170
|
+
text-overflow: ellipsis;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.session-meta {
|
|
174
|
+
display: flex;
|
|
175
|
+
align-items: center;
|
|
176
|
+
gap: 8px;
|
|
177
|
+
font-size: 11px;
|
|
178
|
+
color: var(--text-dim);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.session-meta .model-badge {
|
|
182
|
+
padding: 1px 6px;
|
|
183
|
+
border-radius: 4px;
|
|
184
|
+
font-size: 10px;
|
|
185
|
+
font-weight: 600;
|
|
186
|
+
text-transform: uppercase;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.model-badge.opus { background: #2d1b4e; color: #c084fc; }
|
|
190
|
+
.model-badge.sonnet { background: #1b2e4e; color: #60a5fa; }
|
|
191
|
+
.model-badge.haiku { background: #1b3e3e; color: #5eead4; }
|
|
192
|
+
|
|
193
|
+
.empty-state {
|
|
194
|
+
display: flex;
|
|
195
|
+
flex-direction: column;
|
|
196
|
+
align-items: center;
|
|
197
|
+
justify-content: center;
|
|
198
|
+
padding: 40px 20px;
|
|
199
|
+
text-align: center;
|
|
200
|
+
color: var(--text-dim);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.empty-state .icon { font-size: 32px; margin-bottom: 12px; opacity: 0.5; }
|
|
204
|
+
.empty-state p { font-size: 13px; line-height: 1.5; }
|
|
205
|
+
|
|
206
|
+
/* Main panel */
|
|
207
|
+
.main {
|
|
208
|
+
display: flex;
|
|
209
|
+
flex-direction: column;
|
|
210
|
+
overflow: hidden;
|
|
211
|
+
background: var(--bg);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.terminal-header {
|
|
215
|
+
display: flex;
|
|
216
|
+
align-items: center;
|
|
217
|
+
justify-content: space-between;
|
|
218
|
+
padding: 10px 16px;
|
|
219
|
+
border-bottom: 1px solid var(--border);
|
|
220
|
+
background: var(--bg-panel);
|
|
221
|
+
min-height: 44px;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.terminal-header .session-info {
|
|
225
|
+
display: flex;
|
|
226
|
+
align-items: center;
|
|
227
|
+
gap: 10px;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.terminal-header .session-info h3 {
|
|
231
|
+
font-size: 14px;
|
|
232
|
+
font-weight: 500;
|
|
233
|
+
color: var(--text-bright);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.terminal-actions {
|
|
237
|
+
display: flex;
|
|
238
|
+
gap: 6px;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.action-btn {
|
|
242
|
+
padding: 4px 10px;
|
|
243
|
+
border-radius: 6px;
|
|
244
|
+
border: 1px solid var(--border);
|
|
245
|
+
background: var(--bg);
|
|
246
|
+
color: var(--text);
|
|
247
|
+
font-size: 12px;
|
|
248
|
+
cursor: pointer;
|
|
249
|
+
transition: all 0.15s;
|
|
250
|
+
font-family: monospace;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.action-btn:hover {
|
|
254
|
+
background: var(--bg-hover);
|
|
255
|
+
border-color: #333;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.action-btn.danger { border-color: #3a1a1a; color: var(--red); }
|
|
259
|
+
.action-btn.danger:hover { background: #1a0a0a; }
|
|
260
|
+
|
|
261
|
+
.terminal-container {
|
|
262
|
+
flex: 1;
|
|
263
|
+
padding: 4px;
|
|
264
|
+
overflow: hidden;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.terminal-container .xterm {
|
|
268
|
+
height: 100%;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/* Input bar */
|
|
272
|
+
.input-bar {
|
|
273
|
+
display: flex;
|
|
274
|
+
align-items: center;
|
|
275
|
+
padding: 8px 12px;
|
|
276
|
+
border-top: 1px solid var(--border);
|
|
277
|
+
background: var(--bg-panel);
|
|
278
|
+
gap: 8px;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.input-bar input {
|
|
282
|
+
flex: 1;
|
|
283
|
+
padding: 8px 12px;
|
|
284
|
+
border-radius: 6px;
|
|
285
|
+
border: 1px solid var(--border);
|
|
286
|
+
background: var(--bg);
|
|
287
|
+
color: var(--text-bright);
|
|
288
|
+
font-family: 'SF Mono', 'Fira Code', 'JetBrains Mono', monospace;
|
|
289
|
+
font-size: 13px;
|
|
290
|
+
outline: none;
|
|
291
|
+
transition: border-color 0.15s;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.input-bar input:focus {
|
|
295
|
+
border-color: var(--accent-dim);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.input-bar input::placeholder {
|
|
299
|
+
color: var(--text-dim);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.input-bar button {
|
|
303
|
+
padding: 8px 16px;
|
|
304
|
+
border-radius: 6px;
|
|
305
|
+
border: none;
|
|
306
|
+
background: var(--accent-dim);
|
|
307
|
+
color: #000;
|
|
308
|
+
font-size: 13px;
|
|
309
|
+
font-weight: 600;
|
|
310
|
+
cursor: pointer;
|
|
311
|
+
transition: background 0.15s;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.input-bar button:hover {
|
|
315
|
+
background: var(--accent);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.input-bar button:disabled {
|
|
319
|
+
opacity: 0.4;
|
|
320
|
+
cursor: not-allowed;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/* No selection state */
|
|
324
|
+
.no-session {
|
|
325
|
+
flex: 1;
|
|
326
|
+
display: flex;
|
|
327
|
+
flex-direction: column;
|
|
328
|
+
align-items: center;
|
|
329
|
+
justify-content: center;
|
|
330
|
+
color: var(--text-dim);
|
|
331
|
+
gap: 12px;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.no-session .icon { font-size: 48px; opacity: 0.3; }
|
|
335
|
+
.no-session p { font-size: 14px; }
|
|
336
|
+
|
|
337
|
+
/* Auth overlay */
|
|
338
|
+
.auth-overlay {
|
|
339
|
+
position: fixed;
|
|
340
|
+
inset: 0;
|
|
341
|
+
background: var(--bg);
|
|
342
|
+
display: flex;
|
|
343
|
+
align-items: center;
|
|
344
|
+
justify-content: center;
|
|
345
|
+
z-index: 100;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.auth-box {
|
|
349
|
+
background: var(--bg-panel);
|
|
350
|
+
border: 1px solid var(--border);
|
|
351
|
+
border-radius: 12px;
|
|
352
|
+
padding: 32px;
|
|
353
|
+
width: 360px;
|
|
354
|
+
text-align: center;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.auth-box h2 {
|
|
358
|
+
font-size: 18px;
|
|
359
|
+
margin-bottom: 8px;
|
|
360
|
+
color: var(--text-bright);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.auth-box p {
|
|
364
|
+
font-size: 13px;
|
|
365
|
+
color: var(--text-dim);
|
|
366
|
+
margin-bottom: 20px;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.auth-box input {
|
|
370
|
+
width: 100%;
|
|
371
|
+
padding: 10px 14px;
|
|
372
|
+
border-radius: 8px;
|
|
373
|
+
border: 1px solid var(--border);
|
|
374
|
+
background: var(--bg);
|
|
375
|
+
color: var(--text-bright);
|
|
376
|
+
font-family: monospace;
|
|
377
|
+
font-size: 14px;
|
|
378
|
+
outline: none;
|
|
379
|
+
margin-bottom: 12px;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.auth-box input:focus { border-color: var(--accent-dim); }
|
|
383
|
+
|
|
384
|
+
.auth-box button {
|
|
385
|
+
width: 100%;
|
|
386
|
+
padding: 10px;
|
|
387
|
+
border-radius: 8px;
|
|
388
|
+
border: none;
|
|
389
|
+
background: var(--accent-dim);
|
|
390
|
+
color: #000;
|
|
391
|
+
font-size: 14px;
|
|
392
|
+
font-weight: 600;
|
|
393
|
+
cursor: pointer;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.auth-box button:hover { background: var(--accent); }
|
|
397
|
+
|
|
398
|
+
.auth-error {
|
|
399
|
+
color: var(--red);
|
|
400
|
+
font-size: 12px;
|
|
401
|
+
margin-top: 8px;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/* Scrollbar */
|
|
405
|
+
::-webkit-scrollbar { width: 6px; }
|
|
406
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
407
|
+
::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
|
|
408
|
+
::-webkit-scrollbar-thumb:hover { background: #444; }
|
|
409
|
+
</style>
|
|
410
|
+
</head>
|
|
411
|
+
<body>
|
|
412
|
+
<!-- Auth overlay -->
|
|
413
|
+
<div class="auth-overlay" id="authOverlay">
|
|
414
|
+
<div class="auth-box">
|
|
415
|
+
<h2>Instar Dashboard</h2>
|
|
416
|
+
<p>Enter your auth token to connect</p>
|
|
417
|
+
<input type="password" id="tokenInput" placeholder="Bearer token..." autofocus>
|
|
418
|
+
<button onclick="authenticate()">Connect</button>
|
|
419
|
+
<div class="auth-error" id="authError" style="display:none"></div>
|
|
420
|
+
</div>
|
|
421
|
+
</div>
|
|
422
|
+
|
|
423
|
+
<!-- Main app -->
|
|
424
|
+
<div class="app" id="app" style="display:none">
|
|
425
|
+
<header class="header">
|
|
426
|
+
<div class="header-left">
|
|
427
|
+
<span class="logo">🦐</span>
|
|
428
|
+
<h1>Instar Dashboard</h1>
|
|
429
|
+
</div>
|
|
430
|
+
<div class="status-badge" id="connStatus">
|
|
431
|
+
<span class="dot"></span>
|
|
432
|
+
<span id="connText">Connected</span>
|
|
433
|
+
</div>
|
|
434
|
+
</header>
|
|
435
|
+
|
|
436
|
+
<aside class="sidebar">
|
|
437
|
+
<div class="sidebar-header">
|
|
438
|
+
<h2>Sessions</h2>
|
|
439
|
+
<span class="session-count" id="sessionCount">0</span>
|
|
440
|
+
</div>
|
|
441
|
+
<div class="session-list" id="sessionList">
|
|
442
|
+
<div class="empty-state" id="emptyState">
|
|
443
|
+
<div class="icon">💭</div>
|
|
444
|
+
<p>No running sessions<br>Sessions will appear here when spawned</p>
|
|
445
|
+
</div>
|
|
446
|
+
</div>
|
|
447
|
+
</aside>
|
|
448
|
+
|
|
449
|
+
<main class="main" id="mainPanel">
|
|
450
|
+
<div class="no-session" id="noSession">
|
|
451
|
+
<div class="icon">🖥</div>
|
|
452
|
+
<p>Select a session to view its terminal</p>
|
|
453
|
+
</div>
|
|
454
|
+
|
|
455
|
+
<div id="terminalView" style="display:none">
|
|
456
|
+
<div class="terminal-header">
|
|
457
|
+
<div class="session-info">
|
|
458
|
+
<h3 id="termSessionName">—</h3>
|
|
459
|
+
<span class="model-badge" id="termModelBadge" style="display:none"></span>
|
|
460
|
+
</div>
|
|
461
|
+
<div class="terminal-actions">
|
|
462
|
+
<button class="action-btn" onclick="sendKey('C-c')" title="Send Ctrl+C">^C</button>
|
|
463
|
+
<button class="action-btn" onclick="sendKey('Escape')" title="Send Escape">Esc</button>
|
|
464
|
+
<button class="action-btn" onclick="sendSpecial('y')" title="Send 'y'">y</button>
|
|
465
|
+
<button class="action-btn" onclick="sendSpecial('n')" title="Send 'n'">n</button>
|
|
466
|
+
</div>
|
|
467
|
+
</div>
|
|
468
|
+
<div class="terminal-container" id="terminalContainer"></div>
|
|
469
|
+
<div class="input-bar">
|
|
470
|
+
<input type="text" id="termInput" placeholder="Type a message and press Enter..."
|
|
471
|
+
onkeydown="if(event.key==='Enter')sendInput()">
|
|
472
|
+
<button onclick="sendInput()" id="sendBtn">Send</button>
|
|
473
|
+
</div>
|
|
474
|
+
</div>
|
|
475
|
+
</main>
|
|
476
|
+
</div>
|
|
477
|
+
|
|
478
|
+
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
|
|
479
|
+
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
|
|
480
|
+
<script>
|
|
481
|
+
// ── State ────────────────────────────────────────────────
|
|
482
|
+
let ws = null;
|
|
483
|
+
let token = '';
|
|
484
|
+
let sessions = [];
|
|
485
|
+
let activeSession = null;
|
|
486
|
+
let term = null;
|
|
487
|
+
let fitAddon = null;
|
|
488
|
+
|
|
489
|
+
// ── Auth ─────────────────────────────────────────────────
|
|
490
|
+
function authenticate() {
|
|
491
|
+
token = document.getElementById('tokenInput').value.trim();
|
|
492
|
+
if (!token) {
|
|
493
|
+
showAuthError('Please enter a token');
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Test token by hitting /health with auth
|
|
498
|
+
const port = window.location.port || location.port;
|
|
499
|
+
fetch(`/health`, {
|
|
500
|
+
headers: { 'Authorization': `Bearer ${token}` }
|
|
501
|
+
})
|
|
502
|
+
.then(r => {
|
|
503
|
+
if (r.ok) {
|
|
504
|
+
localStorage.setItem('instar_token', token);
|
|
505
|
+
document.getElementById('authOverlay').style.display = 'none';
|
|
506
|
+
document.getElementById('app').style.display = 'grid';
|
|
507
|
+
connectWebSocket();
|
|
508
|
+
} else {
|
|
509
|
+
showAuthError('Invalid token');
|
|
510
|
+
}
|
|
511
|
+
})
|
|
512
|
+
.catch(() => showAuthError('Cannot connect to server'));
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function showAuthError(msg) {
|
|
516
|
+
const el = document.getElementById('authError');
|
|
517
|
+
el.textContent = msg;
|
|
518
|
+
el.style.display = 'block';
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Auto-login with stored token
|
|
522
|
+
const stored = localStorage.getItem('instar_token');
|
|
523
|
+
if (stored) {
|
|
524
|
+
token = stored;
|
|
525
|
+
fetch(`/health`, { headers: { 'Authorization': `Bearer ${token}` } })
|
|
526
|
+
.then(r => {
|
|
527
|
+
if (r.ok) {
|
|
528
|
+
document.getElementById('authOverlay').style.display = 'none';
|
|
529
|
+
document.getElementById('app').style.display = 'grid';
|
|
530
|
+
connectWebSocket();
|
|
531
|
+
}
|
|
532
|
+
})
|
|
533
|
+
.catch(() => {});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Enter on token input
|
|
537
|
+
document.getElementById('tokenInput').addEventListener('keydown', e => {
|
|
538
|
+
if (e.key === 'Enter') authenticate();
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
// ── WebSocket ────────────────────────────────────────────
|
|
542
|
+
let reconnectAttempts = 0;
|
|
543
|
+
const MAX_RECONNECT = 10;
|
|
544
|
+
|
|
545
|
+
function connectWebSocket() {
|
|
546
|
+
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
547
|
+
const url = `${proto}//${location.host}/ws?token=${encodeURIComponent(token)}`;
|
|
548
|
+
|
|
549
|
+
ws = new WebSocket(url);
|
|
550
|
+
|
|
551
|
+
ws.onopen = () => {
|
|
552
|
+
reconnectAttempts = 0;
|
|
553
|
+
updateConnectionStatus(true);
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
ws.onclose = () => {
|
|
557
|
+
updateConnectionStatus(false);
|
|
558
|
+
// Reconnect with backoff
|
|
559
|
+
if (reconnectAttempts < MAX_RECONNECT) {
|
|
560
|
+
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
|
|
561
|
+
reconnectAttempts++;
|
|
562
|
+
setTimeout(connectWebSocket, delay);
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
ws.onerror = () => {
|
|
567
|
+
// onclose will fire after this
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
ws.onmessage = (event) => {
|
|
571
|
+
try {
|
|
572
|
+
const msg = JSON.parse(event.data);
|
|
573
|
+
handleMessage(msg);
|
|
574
|
+
} catch { /* ignore parse errors */ }
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function handleMessage(msg) {
|
|
579
|
+
switch (msg.type) {
|
|
580
|
+
case 'sessions':
|
|
581
|
+
sessions = msg.sessions;
|
|
582
|
+
renderSessionList();
|
|
583
|
+
break;
|
|
584
|
+
|
|
585
|
+
case 'output':
|
|
586
|
+
if (msg.session === activeSession) {
|
|
587
|
+
renderTerminalOutput(msg.data);
|
|
588
|
+
}
|
|
589
|
+
break;
|
|
590
|
+
|
|
591
|
+
case 'session_ended':
|
|
592
|
+
if (msg.session === activeSession) {
|
|
593
|
+
// Session ended — show in terminal
|
|
594
|
+
if (term) {
|
|
595
|
+
term.writeln('\r\n\x1b[33m--- Session ended ---\x1b[0m');
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
break;
|
|
599
|
+
|
|
600
|
+
case 'subscribed':
|
|
601
|
+
// Subscribed confirmation
|
|
602
|
+
break;
|
|
603
|
+
|
|
604
|
+
case 'input_ack':
|
|
605
|
+
// Input acknowledged
|
|
606
|
+
break;
|
|
607
|
+
|
|
608
|
+
case 'pong':
|
|
609
|
+
break;
|
|
610
|
+
|
|
611
|
+
case 'error':
|
|
612
|
+
console.error('[ws]', msg.message);
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function wsSend(msg) {
|
|
618
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
619
|
+
ws.send(JSON.stringify(msg));
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function updateConnectionStatus(connected) {
|
|
624
|
+
const badge = document.getElementById('connStatus');
|
|
625
|
+
const text = document.getElementById('connText');
|
|
626
|
+
if (connected) {
|
|
627
|
+
badge.className = 'status-badge';
|
|
628
|
+
text.textContent = 'Connected';
|
|
629
|
+
} else {
|
|
630
|
+
badge.className = 'status-badge disconnected';
|
|
631
|
+
text.textContent = 'Disconnected';
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// ── Session List ─────────────────────────────────────────
|
|
636
|
+
function renderSessionList() {
|
|
637
|
+
const list = document.getElementById('sessionList');
|
|
638
|
+
const empty = document.getElementById('emptyState');
|
|
639
|
+
const count = document.getElementById('sessionCount');
|
|
640
|
+
|
|
641
|
+
count.textContent = sessions.length;
|
|
642
|
+
|
|
643
|
+
if (sessions.length === 0) {
|
|
644
|
+
empty.style.display = 'flex';
|
|
645
|
+
// Clear any existing session items
|
|
646
|
+
list.querySelectorAll('.session-item').forEach(el => el.remove());
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
empty.style.display = 'none';
|
|
651
|
+
|
|
652
|
+
// Build session items
|
|
653
|
+
const existing = new Map();
|
|
654
|
+
list.querySelectorAll('.session-item').forEach(el => {
|
|
655
|
+
existing.set(el.dataset.session, el);
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
// Remove sessions no longer running
|
|
659
|
+
for (const [name, el] of existing) {
|
|
660
|
+
if (!sessions.find(s => s.tmuxSession === name)) {
|
|
661
|
+
el.remove();
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Add/update sessions
|
|
666
|
+
for (const session of sessions) {
|
|
667
|
+
let el = existing.get(session.tmuxSession);
|
|
668
|
+
if (!el) {
|
|
669
|
+
el = document.createElement('div');
|
|
670
|
+
el.className = 'session-item';
|
|
671
|
+
el.dataset.session = session.tmuxSession;
|
|
672
|
+
el.onclick = () => selectSession(session.tmuxSession, session);
|
|
673
|
+
list.appendChild(el);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const elapsed = formatElapsed(session.startedAt);
|
|
677
|
+
const model = session.model || 'sonnet';
|
|
678
|
+
const isActive = session.tmuxSession === activeSession;
|
|
679
|
+
|
|
680
|
+
el.className = `session-item${isActive ? ' active' : ''}`;
|
|
681
|
+
el.innerHTML = `
|
|
682
|
+
<div class="session-name">${escapeHtml(session.name)}</div>
|
|
683
|
+
<div class="session-meta">
|
|
684
|
+
<span class="model-badge ${model}">${model}</span>
|
|
685
|
+
<span>${elapsed}</span>
|
|
686
|
+
${session.jobSlug ? `<span>${escapeHtml(session.jobSlug)}</span>` : ''}
|
|
687
|
+
</div>
|
|
688
|
+
`;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// ── Terminal ──────────────────────────────────────────────
|
|
693
|
+
function selectSession(tmuxSession, session) {
|
|
694
|
+
// Unsubscribe from previous
|
|
695
|
+
if (activeSession) {
|
|
696
|
+
wsSend({ type: 'unsubscribe', session: activeSession });
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
activeSession = tmuxSession;
|
|
700
|
+
|
|
701
|
+
// Update UI
|
|
702
|
+
document.getElementById('noSession').style.display = 'none';
|
|
703
|
+
document.getElementById('terminalView').style.display = 'flex';
|
|
704
|
+
document.getElementById('terminalView').style.flexDirection = 'column';
|
|
705
|
+
document.getElementById('terminalView').style.height = '100%';
|
|
706
|
+
document.getElementById('termSessionName').textContent = session.name;
|
|
707
|
+
|
|
708
|
+
const badge = document.getElementById('termModelBadge');
|
|
709
|
+
if (session.model) {
|
|
710
|
+
badge.textContent = session.model;
|
|
711
|
+
badge.className = `model-badge ${session.model}`;
|
|
712
|
+
badge.style.display = 'inline-block';
|
|
713
|
+
} else {
|
|
714
|
+
badge.style.display = 'none';
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Initialize terminal if needed
|
|
718
|
+
if (!term) {
|
|
719
|
+
initTerminal();
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Clear and subscribe
|
|
723
|
+
term.clear();
|
|
724
|
+
wsSend({ type: 'subscribe', session: tmuxSession });
|
|
725
|
+
|
|
726
|
+
// Update active state in sidebar
|
|
727
|
+
renderSessionList();
|
|
728
|
+
|
|
729
|
+
// Focus input
|
|
730
|
+
document.getElementById('termInput').focus();
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function initTerminal() {
|
|
734
|
+
const container = document.getElementById('terminalContainer');
|
|
735
|
+
container.innerHTML = '';
|
|
736
|
+
|
|
737
|
+
term = new window.Terminal({
|
|
738
|
+
theme: {
|
|
739
|
+
background: '#0a0a0a',
|
|
740
|
+
foreground: '#ccc',
|
|
741
|
+
cursor: '#4ade80',
|
|
742
|
+
selectionBackground: '#264f78',
|
|
743
|
+
black: '#1e1e1e',
|
|
744
|
+
red: '#f14c4c',
|
|
745
|
+
green: '#4ade80',
|
|
746
|
+
yellow: '#f59e0b',
|
|
747
|
+
blue: '#3b82f6',
|
|
748
|
+
magenta: '#c084fc',
|
|
749
|
+
cyan: '#5eead4',
|
|
750
|
+
white: '#ccc',
|
|
751
|
+
brightBlack: '#666',
|
|
752
|
+
brightRed: '#f87171',
|
|
753
|
+
brightGreen: '#86efac',
|
|
754
|
+
brightYellow: '#fde047',
|
|
755
|
+
brightBlue: '#60a5fa',
|
|
756
|
+
brightMagenta: '#d8b4fe',
|
|
757
|
+
brightCyan: '#99f6e4',
|
|
758
|
+
brightWhite: '#eee',
|
|
759
|
+
},
|
|
760
|
+
fontSize: 13,
|
|
761
|
+
fontFamily: "'SF Mono', 'Fira Code', 'JetBrains Mono', 'Cascadia Code', monospace",
|
|
762
|
+
cursorBlink: false,
|
|
763
|
+
cursorStyle: 'underline',
|
|
764
|
+
scrollback: 10000,
|
|
765
|
+
convertEol: true,
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
fitAddon = new window.FitAddon.FitAddon();
|
|
769
|
+
term.loadAddon(fitAddon);
|
|
770
|
+
term.open(container);
|
|
771
|
+
|
|
772
|
+
// Fit on resize
|
|
773
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
774
|
+
try { fitAddon.fit(); } catch {}
|
|
775
|
+
});
|
|
776
|
+
resizeObserver.observe(container);
|
|
777
|
+
|
|
778
|
+
// Initial fit after a tick (container needs to be visible)
|
|
779
|
+
requestAnimationFrame(() => {
|
|
780
|
+
try { fitAddon.fit(); } catch {}
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function renderTerminalOutput(data) {
|
|
785
|
+
if (!term) return;
|
|
786
|
+
// Replace full terminal content with latest capture
|
|
787
|
+
term.clear();
|
|
788
|
+
term.write(data);
|
|
789
|
+
// Auto-scroll to bottom
|
|
790
|
+
term.scrollToBottom();
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// ── Input ────────────────────────────────────────────────
|
|
794
|
+
function sendInput() {
|
|
795
|
+
const input = document.getElementById('termInput');
|
|
796
|
+
const text = input.value;
|
|
797
|
+
if (!text || !activeSession) return;
|
|
798
|
+
|
|
799
|
+
wsSend({ type: 'input', session: activeSession, text });
|
|
800
|
+
input.value = '';
|
|
801
|
+
input.focus();
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function sendKey(key) {
|
|
805
|
+
if (!activeSession) return;
|
|
806
|
+
wsSend({ type: 'key', session: activeSession, key });
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function sendSpecial(text) {
|
|
810
|
+
if (!activeSession) return;
|
|
811
|
+
// Send literal text without Enter
|
|
812
|
+
wsSend({ type: 'key', session: activeSession, key: text });
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// ── Helpers ───────────────────────────────────────────────
|
|
816
|
+
function formatElapsed(isoStr) {
|
|
817
|
+
const ms = Date.now() - new Date(isoStr).getTime();
|
|
818
|
+
const mins = Math.floor(ms / 60000);
|
|
819
|
+
if (mins < 60) return `${mins}m`;
|
|
820
|
+
const hrs = Math.floor(mins / 60);
|
|
821
|
+
const remMins = mins % 60;
|
|
822
|
+
if (hrs < 24) return `${hrs}h ${remMins}m`;
|
|
823
|
+
const days = Math.floor(hrs / 24);
|
|
824
|
+
return `${days}d ${hrs % 24}h`;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function escapeHtml(s) {
|
|
828
|
+
const div = document.createElement('div');
|
|
829
|
+
div.textContent = s;
|
|
830
|
+
return div.innerHTML;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// ── Keyboard shortcuts ───────────────────────────────────
|
|
834
|
+
document.addEventListener('keydown', (e) => {
|
|
835
|
+
// Ctrl+C when terminal focused but not input
|
|
836
|
+
if (e.key === 'c' && e.ctrlKey && document.activeElement?.id !== 'termInput') {
|
|
837
|
+
e.preventDefault();
|
|
838
|
+
sendKey('C-c');
|
|
839
|
+
}
|
|
840
|
+
});
|
|
841
|
+
</script>
|
|
842
|
+
</body>
|
|
843
|
+
</html>
|