claude-remote-cli 1.8.0 → 1.9.0
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/dist/server/sessions.js +30 -3
- package/dist/server/ws.js +8 -3
- package/package.json +1 -1
- package/public/app.js +95 -9
- package/public/style.css +41 -1
package/dist/server/sessions.js
CHANGED
|
@@ -6,6 +6,11 @@ import path from 'node:path';
|
|
|
6
6
|
import { readMeta, writeMeta } from './config.js';
|
|
7
7
|
// In-memory registry: id -> Session
|
|
8
8
|
const sessions = new Map();
|
|
9
|
+
const IDLE_TIMEOUT_MS = 5000;
|
|
10
|
+
let idleChangeCallback = null;
|
|
11
|
+
function onIdleChange(cb) {
|
|
12
|
+
idleChangeCallback = cb;
|
|
13
|
+
}
|
|
9
14
|
function create({ repoName, repoPath, cwd, root, worktreeName, displayName, command, args = [], cols = 80, rows = 24, configPath }) {
|
|
10
15
|
const id = crypto.randomBytes(8).toString('hex');
|
|
11
16
|
const createdAt = new Date().toISOString();
|
|
@@ -34,6 +39,7 @@ function create({ repoName, repoPath, cwd, root, worktreeName, displayName, comm
|
|
|
34
39
|
createdAt,
|
|
35
40
|
lastActivity: createdAt,
|
|
36
41
|
scrollback,
|
|
42
|
+
idle: false,
|
|
37
43
|
};
|
|
38
44
|
sessions.set(id, session);
|
|
39
45
|
// Load existing metadata to preserve a previously-set displayName
|
|
@@ -45,8 +51,26 @@ function create({ repoName, repoPath, cwd, root, worktreeName, displayName, comm
|
|
|
45
51
|
writeMeta(configPath, { worktreePath: repoPath, displayName: session.displayName, lastActivity: createdAt });
|
|
46
52
|
}
|
|
47
53
|
let metaFlushTimer = null;
|
|
54
|
+
let idleTimer = null;
|
|
55
|
+
function resetIdleTimer() {
|
|
56
|
+
if (session.idle) {
|
|
57
|
+
session.idle = false;
|
|
58
|
+
if (idleChangeCallback)
|
|
59
|
+
idleChangeCallback(session.id, false);
|
|
60
|
+
}
|
|
61
|
+
if (idleTimer)
|
|
62
|
+
clearTimeout(idleTimer);
|
|
63
|
+
idleTimer = setTimeout(() => {
|
|
64
|
+
if (!session.idle) {
|
|
65
|
+
session.idle = true;
|
|
66
|
+
if (idleChangeCallback)
|
|
67
|
+
idleChangeCallback(session.id, true);
|
|
68
|
+
}
|
|
69
|
+
}, IDLE_TIMEOUT_MS);
|
|
70
|
+
}
|
|
48
71
|
ptyProcess.onData((data) => {
|
|
49
72
|
session.lastActivity = new Date().toISOString();
|
|
73
|
+
resetIdleTimer();
|
|
50
74
|
scrollback.push(data);
|
|
51
75
|
scrollbackBytes += data.length;
|
|
52
76
|
// Trim oldest entries if over limit
|
|
@@ -61,6 +85,8 @@ function create({ repoName, repoPath, cwd, root, worktreeName, displayName, comm
|
|
|
61
85
|
}
|
|
62
86
|
});
|
|
63
87
|
ptyProcess.onExit(() => {
|
|
88
|
+
if (idleTimer)
|
|
89
|
+
clearTimeout(idleTimer);
|
|
64
90
|
if (metaFlushTimer)
|
|
65
91
|
clearTimeout(metaFlushTimer);
|
|
66
92
|
if (configPath && worktreeName) {
|
|
@@ -70,14 +96,14 @@ function create({ repoName, repoPath, cwd, root, worktreeName, displayName, comm
|
|
|
70
96
|
const tmpDir = path.join(os.tmpdir(), 'claude-remote-cli', id);
|
|
71
97
|
fs.rm(tmpDir, { recursive: true, force: true }, () => { });
|
|
72
98
|
});
|
|
73
|
-
return { id, root: session.root, repoName: session.repoName, repoPath, worktreeName: session.worktreeName, displayName: session.displayName, pid: ptyProcess.pid, createdAt, lastActivity: createdAt };
|
|
99
|
+
return { id, root: session.root, repoName: session.repoName, repoPath, worktreeName: session.worktreeName, displayName: session.displayName, pid: ptyProcess.pid, createdAt, lastActivity: createdAt, idle: false };
|
|
74
100
|
}
|
|
75
101
|
function get(id) {
|
|
76
102
|
return sessions.get(id);
|
|
77
103
|
}
|
|
78
104
|
function list() {
|
|
79
105
|
return Array.from(sessions.values())
|
|
80
|
-
.map(({ id, root, repoName, repoPath, worktreeName, displayName, createdAt, lastActivity }) => ({
|
|
106
|
+
.map(({ id, root, repoName, repoPath, worktreeName, displayName, createdAt, lastActivity, idle }) => ({
|
|
81
107
|
id,
|
|
82
108
|
root,
|
|
83
109
|
repoName,
|
|
@@ -86,6 +112,7 @@ function list() {
|
|
|
86
112
|
displayName,
|
|
87
113
|
createdAt,
|
|
88
114
|
lastActivity,
|
|
115
|
+
idle,
|
|
89
116
|
}))
|
|
90
117
|
.sort((a, b) => b.lastActivity.localeCompare(a.lastActivity));
|
|
91
118
|
}
|
|
@@ -118,4 +145,4 @@ function write(id, data) {
|
|
|
118
145
|
}
|
|
119
146
|
session.pty.write(data);
|
|
120
147
|
}
|
|
121
|
-
export { create, get, list, kill, resize, updateDisplayName, write };
|
|
148
|
+
export { create, get, list, kill, resize, updateDisplayName, write, onIdleChange };
|
package/dist/server/ws.js
CHANGED
|
@@ -17,8 +17,8 @@ function parseCookies(cookieHeader) {
|
|
|
17
17
|
function setupWebSocket(server, authenticatedTokens, watcher) {
|
|
18
18
|
const wss = new WebSocketServer({ noServer: true });
|
|
19
19
|
const eventClients = new Set();
|
|
20
|
-
function broadcastEvent(type) {
|
|
21
|
-
const msg = JSON.stringify({ type });
|
|
20
|
+
function broadcastEvent(type, data) {
|
|
21
|
+
const msg = JSON.stringify({ type, ...data });
|
|
22
22
|
for (const client of eventClients) {
|
|
23
23
|
if (client.readyState === client.OPEN) {
|
|
24
24
|
client.send(msg);
|
|
@@ -40,8 +40,10 @@ function setupWebSocket(server, authenticatedTokens, watcher) {
|
|
|
40
40
|
// Event channel: /ws/events
|
|
41
41
|
if (request.url === '/ws/events') {
|
|
42
42
|
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
43
|
+
const cleanup = () => { eventClients.delete(ws); };
|
|
43
44
|
eventClients.add(ws);
|
|
44
|
-
ws.on('close',
|
|
45
|
+
ws.on('close', cleanup);
|
|
46
|
+
ws.on('error', cleanup);
|
|
45
47
|
});
|
|
46
48
|
return;
|
|
47
49
|
}
|
|
@@ -99,6 +101,9 @@ function setupWebSocket(server, authenticatedTokens, watcher) {
|
|
|
99
101
|
}
|
|
100
102
|
});
|
|
101
103
|
});
|
|
104
|
+
sessions.onIdleChange((sessionId, idle) => {
|
|
105
|
+
broadcastEvent('session-idle-changed', { sessionId, idle });
|
|
106
|
+
});
|
|
102
107
|
return { wss, broadcastEvent };
|
|
103
108
|
}
|
|
104
109
|
export { setupWebSocket };
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
var ws = null;
|
|
7
7
|
var term = null;
|
|
8
8
|
var fitAddon = null;
|
|
9
|
+
var reconnectTimer = null;
|
|
10
|
+
var reconnectAttempt = 0;
|
|
9
11
|
|
|
10
12
|
var wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
11
13
|
|
|
@@ -87,6 +89,7 @@
|
|
|
87
89
|
var cachedSessions = [];
|
|
88
90
|
var cachedWorktrees = [];
|
|
89
91
|
var allRepos = [];
|
|
92
|
+
var attentionSessions = {};
|
|
90
93
|
|
|
91
94
|
// ── PIN Auth ────────────────────────────────────────────────────────────────
|
|
92
95
|
|
|
@@ -312,38 +315,92 @@
|
|
|
312
315
|
// ── WebSocket / Session Connection ──────────────────────────────────────────
|
|
313
316
|
|
|
314
317
|
function connectToSession(sessionId) {
|
|
318
|
+
if (reconnectTimer) {
|
|
319
|
+
clearTimeout(reconnectTimer);
|
|
320
|
+
reconnectTimer = null;
|
|
321
|
+
}
|
|
322
|
+
reconnectAttempt = 0;
|
|
323
|
+
|
|
315
324
|
if (ws) {
|
|
325
|
+
ws.onclose = null;
|
|
316
326
|
ws.close();
|
|
317
327
|
ws = null;
|
|
318
328
|
}
|
|
319
329
|
|
|
320
330
|
activeSessionId = sessionId;
|
|
331
|
+
delete attentionSessions[sessionId];
|
|
321
332
|
noSessionMsg.hidden = true;
|
|
322
333
|
term.clear();
|
|
323
334
|
term.focus();
|
|
324
335
|
closeSidebar();
|
|
325
336
|
updateSessionTitle();
|
|
337
|
+
highlightActiveSession();
|
|
338
|
+
|
|
339
|
+
openPtyWebSocket(sessionId);
|
|
340
|
+
}
|
|
326
341
|
|
|
342
|
+
function openPtyWebSocket(sessionId) {
|
|
327
343
|
var url = wsProtocol + '//' + location.host + '/ws/' + sessionId;
|
|
328
|
-
|
|
344
|
+
var socket = new WebSocket(url);
|
|
329
345
|
|
|
330
|
-
|
|
346
|
+
socket.onopen = function () {
|
|
347
|
+
ws = socket;
|
|
348
|
+
reconnectAttempt = 0;
|
|
331
349
|
sendResize();
|
|
332
350
|
};
|
|
333
351
|
|
|
334
|
-
|
|
352
|
+
socket.onmessage = function (event) {
|
|
335
353
|
term.write(event.data);
|
|
336
354
|
};
|
|
337
355
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
356
|
+
socket.onclose = function (event) {
|
|
357
|
+
if (event.code === 1000) {
|
|
358
|
+
term.write('\r\n[Session ended]\r\n');
|
|
359
|
+
ws = null;
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
341
362
|
|
|
342
|
-
|
|
343
|
-
|
|
363
|
+
if (activeSessionId !== sessionId) return;
|
|
364
|
+
|
|
365
|
+
ws = null;
|
|
366
|
+
if (reconnectAttempt === 0) {
|
|
367
|
+
term.write('\r\n[Reconnecting...]\r\n');
|
|
368
|
+
}
|
|
369
|
+
scheduleReconnect(sessionId);
|
|
344
370
|
};
|
|
345
371
|
|
|
346
|
-
|
|
372
|
+
socket.onerror = function () {};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
var MAX_RECONNECT_ATTEMPTS = 30;
|
|
376
|
+
|
|
377
|
+
function scheduleReconnect(sessionId) {
|
|
378
|
+
if (reconnectAttempt >= MAX_RECONNECT_ATTEMPTS) {
|
|
379
|
+
term.write('\r\n[Gave up reconnecting after ' + MAX_RECONNECT_ATTEMPTS + ' attempts]\r\n');
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
var delay = Math.min(1000 * Math.pow(2, reconnectAttempt), 10000);
|
|
383
|
+
reconnectAttempt++;
|
|
384
|
+
|
|
385
|
+
reconnectTimer = setTimeout(function () {
|
|
386
|
+
reconnectTimer = null;
|
|
387
|
+
if (activeSessionId !== sessionId) return;
|
|
388
|
+
fetch('/sessions').then(function (res) {
|
|
389
|
+
return res.json();
|
|
390
|
+
}).then(function (sessions) {
|
|
391
|
+
var exists = sessions.some(function (s) { return s.id === sessionId; });
|
|
392
|
+
if (!exists || activeSessionId !== sessionId) {
|
|
393
|
+
term.write('\r\n[Session ended]\r\n');
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
term.clear();
|
|
397
|
+
openPtyWebSocket(sessionId);
|
|
398
|
+
}).catch(function () {
|
|
399
|
+
if (activeSessionId === sessionId) {
|
|
400
|
+
scheduleReconnect(sessionId);
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
}, delay);
|
|
347
404
|
}
|
|
348
405
|
|
|
349
406
|
// ── Sessions & Worktrees ────────────────────────────────────────────────────
|
|
@@ -365,6 +422,14 @@
|
|
|
365
422
|
if (msg.type === 'worktrees-changed') {
|
|
366
423
|
loadRepos();
|
|
367
424
|
refreshAll();
|
|
425
|
+
} else if (msg.type === 'session-idle-changed') {
|
|
426
|
+
if (msg.idle && msg.sessionId !== activeSessionId) {
|
|
427
|
+
attentionSessions[msg.sessionId] = true;
|
|
428
|
+
}
|
|
429
|
+
if (!msg.idle) {
|
|
430
|
+
delete attentionSessions[msg.sessionId];
|
|
431
|
+
}
|
|
432
|
+
renderUnifiedList();
|
|
368
433
|
}
|
|
369
434
|
} catch (_) {}
|
|
370
435
|
};
|
|
@@ -386,6 +451,14 @@
|
|
|
386
451
|
.then(function (results) {
|
|
387
452
|
cachedSessions = results[0] || [];
|
|
388
453
|
cachedWorktrees = results[1] || [];
|
|
454
|
+
|
|
455
|
+
// Prune attention flags for sessions that no longer exist
|
|
456
|
+
var activeIds = {};
|
|
457
|
+
cachedSessions.forEach(function (s) { activeIds[s.id] = true; });
|
|
458
|
+
Object.keys(attentionSessions).forEach(function (id) {
|
|
459
|
+
if (!activeIds[id]) delete attentionSessions[id];
|
|
460
|
+
});
|
|
461
|
+
|
|
389
462
|
populateSidebarFilters();
|
|
390
463
|
renderUnifiedList();
|
|
391
464
|
})
|
|
@@ -534,6 +607,12 @@
|
|
|
534
607
|
highlightActiveSession();
|
|
535
608
|
}
|
|
536
609
|
|
|
610
|
+
function getSessionStatus(session) {
|
|
611
|
+
if (attentionSessions[session.id]) return 'attention';
|
|
612
|
+
if (session.idle) return 'idle';
|
|
613
|
+
return 'running';
|
|
614
|
+
}
|
|
615
|
+
|
|
537
616
|
function createActiveSessionLi(session) {
|
|
538
617
|
var li = document.createElement('li');
|
|
539
618
|
li.className = 'active-session';
|
|
@@ -552,6 +631,10 @@
|
|
|
552
631
|
subSpan.className = 'session-sub';
|
|
553
632
|
subSpan.textContent = (session.root ? rootShortName(session.root) : '') + ' · ' + (session.repoName || '');
|
|
554
633
|
|
|
634
|
+
var status = getSessionStatus(session);
|
|
635
|
+
var dot = document.createElement('span');
|
|
636
|
+
dot.className = 'status-dot status-dot--' + status;
|
|
637
|
+
infoDiv.appendChild(dot);
|
|
555
638
|
infoDiv.appendChild(nameSpan);
|
|
556
639
|
infoDiv.appendChild(subSpan);
|
|
557
640
|
|
|
@@ -611,6 +694,9 @@
|
|
|
611
694
|
subSpan.className = 'session-sub';
|
|
612
695
|
subSpan.textContent = (wt.root ? rootShortName(wt.root) : '') + ' · ' + (wt.repoName || '');
|
|
613
696
|
|
|
697
|
+
var dot = document.createElement('span');
|
|
698
|
+
dot.className = 'status-dot status-dot--inactive';
|
|
699
|
+
infoDiv.appendChild(dot);
|
|
614
700
|
infoDiv.appendChild(nameSpan);
|
|
615
701
|
infoDiv.appendChild(subSpan);
|
|
616
702
|
|
package/public/style.css
CHANGED
|
@@ -261,10 +261,12 @@ html, body {
|
|
|
261
261
|
|
|
262
262
|
.session-info {
|
|
263
263
|
display: flex;
|
|
264
|
-
flex-direction:
|
|
264
|
+
flex-direction: row;
|
|
265
|
+
flex-wrap: wrap;
|
|
265
266
|
gap: 2px;
|
|
266
267
|
min-width: 0;
|
|
267
268
|
flex: 1;
|
|
269
|
+
align-items: center;
|
|
268
270
|
}
|
|
269
271
|
|
|
270
272
|
.session-name {
|
|
@@ -276,6 +278,39 @@ html, body {
|
|
|
276
278
|
color: var(--text);
|
|
277
279
|
}
|
|
278
280
|
|
|
281
|
+
.status-dot {
|
|
282
|
+
display: inline-block;
|
|
283
|
+
width: 8px;
|
|
284
|
+
height: 8px;
|
|
285
|
+
border-radius: 50%;
|
|
286
|
+
flex-shrink: 0;
|
|
287
|
+
margin-right: 8px;
|
|
288
|
+
margin-top: 2px;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.status-dot--running {
|
|
292
|
+
background: #4ade80;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.status-dot--idle {
|
|
296
|
+
background: #60a5fa;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.status-dot--attention {
|
|
300
|
+
background: #f59e0b;
|
|
301
|
+
box-shadow: 0 0 6px 2px rgba(245, 158, 11, 0.5);
|
|
302
|
+
animation: attention-glow 2s ease-in-out infinite;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
@keyframes attention-glow {
|
|
306
|
+
0%, 100% { box-shadow: 0 0 4px 1px rgba(245, 158, 11, 0.3); }
|
|
307
|
+
50% { box-shadow: 0 0 8px 3px rgba(245, 158, 11, 0.6); }
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.status-dot--inactive {
|
|
311
|
+
background: #6b7280;
|
|
312
|
+
}
|
|
313
|
+
|
|
279
314
|
.session-sub {
|
|
280
315
|
font-size: 0.7rem;
|
|
281
316
|
color: var(--text-muted);
|
|
@@ -293,6 +328,11 @@ html, body {
|
|
|
293
328
|
white-space: nowrap;
|
|
294
329
|
}
|
|
295
330
|
|
|
331
|
+
.session-sub, .session-time {
|
|
332
|
+
width: 100%;
|
|
333
|
+
padding-left: 16px; /* aligns with name text: status-dot 8px + margin-right 8px */
|
|
334
|
+
}
|
|
335
|
+
|
|
296
336
|
.session-actions {
|
|
297
337
|
display: flex;
|
|
298
338
|
align-items: center;
|