claude-remote-cli 1.8.0 → 1.9.1
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 +177 -73
- package/public/icon.svg +6 -0
- package/public/index.html +3 -1
- package/public/manifest.json +9 -1
- package/public/style.css +50 -3
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
|
|
|
@@ -159,65 +162,59 @@
|
|
|
159
162
|
term.onScroll(updateScrollbar);
|
|
160
163
|
term.onWriteParsed(updateScrollbar);
|
|
161
164
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
+
var isMac = /Mac|iPhone|iPad|iPod/.test(navigator.platform || '');
|
|
166
|
+
term.attachCustomKeyEventHandler(function (e) {
|
|
167
|
+
if (isMobileDevice) {
|
|
168
|
+
return false;
|
|
165
169
|
}
|
|
166
|
-
});
|
|
167
170
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
for (var i = 0; i < clipboardItems.length; i++) {
|
|
184
|
-
var types = clipboardItems[i].types;
|
|
185
|
-
for (var j = 0; j < types.length; j++) {
|
|
186
|
-
if (types[j].indexOf('image/') === 0) {
|
|
187
|
-
imageType = types[j];
|
|
188
|
-
imageBlob = clipboardItems[i];
|
|
189
|
-
break;
|
|
190
|
-
}
|
|
171
|
+
if (!isMac && e.type === 'keydown' && e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey &&
|
|
172
|
+
(e.key === 'v' || e.key === 'V')) {
|
|
173
|
+
if (navigator.clipboard && navigator.clipboard.read) {
|
|
174
|
+
navigator.clipboard.read().then(function (clipboardItems) {
|
|
175
|
+
var imageBlob = null;
|
|
176
|
+
var imageType = null;
|
|
177
|
+
|
|
178
|
+
for (var i = 0; i < clipboardItems.length; i++) {
|
|
179
|
+
var types = clipboardItems[i].types;
|
|
180
|
+
for (var j = 0; j < types.length; j++) {
|
|
181
|
+
if (types[j].indexOf('image/') === 0) {
|
|
182
|
+
imageType = types[j];
|
|
183
|
+
imageBlob = clipboardItems[i];
|
|
184
|
+
break;
|
|
191
185
|
}
|
|
192
|
-
if (imageBlob) break;
|
|
193
186
|
}
|
|
187
|
+
if (imageBlob) break;
|
|
188
|
+
}
|
|
194
189
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}).catch(function () {});
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
return false; // Prevent xterm from handling Ctrl+V
|
|
216
|
-
}
|
|
190
|
+
if (imageBlob) {
|
|
191
|
+
imageBlob.getType(imageType).then(function (blob) {
|
|
192
|
+
uploadImage(blob, imageType);
|
|
193
|
+
});
|
|
194
|
+
} else {
|
|
195
|
+
navigator.clipboard.readText().then(function (text) {
|
|
196
|
+
if (text) term.paste(text);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}).catch(function () {
|
|
200
|
+
if (navigator.clipboard.readText) {
|
|
201
|
+
navigator.clipboard.readText().then(function (text) {
|
|
202
|
+
if (text) term.paste(text);
|
|
203
|
+
}).catch(function () {});
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
return false;
|
|
217
207
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return true;
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
term.onData(function (data) {
|
|
214
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
215
|
+
ws.send(data);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
221
218
|
|
|
222
219
|
var resizeObserver = new ResizeObserver(function () {
|
|
223
220
|
fitAddon.fit();
|
|
@@ -312,38 +309,98 @@
|
|
|
312
309
|
// ── WebSocket / Session Connection ──────────────────────────────────────────
|
|
313
310
|
|
|
314
311
|
function connectToSession(sessionId) {
|
|
312
|
+
if (reconnectTimer) {
|
|
313
|
+
clearTimeout(reconnectTimer);
|
|
314
|
+
reconnectTimer = null;
|
|
315
|
+
}
|
|
316
|
+
reconnectAttempt = 0;
|
|
317
|
+
|
|
315
318
|
if (ws) {
|
|
319
|
+
ws.onclose = null;
|
|
316
320
|
ws.close();
|
|
317
321
|
ws = null;
|
|
318
322
|
}
|
|
319
323
|
|
|
320
324
|
activeSessionId = sessionId;
|
|
325
|
+
delete attentionSessions[sessionId];
|
|
321
326
|
noSessionMsg.hidden = true;
|
|
322
327
|
term.clear();
|
|
323
|
-
|
|
328
|
+
if (isMobileDevice) {
|
|
329
|
+
mobileInput.value = '';
|
|
330
|
+
mobileInput.dispatchEvent(new Event('sessionchange'));
|
|
331
|
+
mobileInput.focus();
|
|
332
|
+
} else {
|
|
333
|
+
term.focus();
|
|
334
|
+
}
|
|
324
335
|
closeSidebar();
|
|
325
336
|
updateSessionTitle();
|
|
337
|
+
highlightActiveSession();
|
|
326
338
|
|
|
339
|
+
openPtyWebSocket(sessionId);
|
|
340
|
+
}
|
|
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
|
+
}
|
|
362
|
+
|
|
363
|
+
if (activeSessionId !== sessionId) return;
|
|
341
364
|
|
|
342
|
-
|
|
343
|
-
|
|
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
|
|
|
@@ -1191,6 +1277,7 @@
|
|
|
1191
1277
|
if (!isMobileDevice) return;
|
|
1192
1278
|
|
|
1193
1279
|
var lastInputValue = '';
|
|
1280
|
+
var isComposing = false;
|
|
1194
1281
|
|
|
1195
1282
|
function focusMobileInput() {
|
|
1196
1283
|
if (document.activeElement !== mobileInput) {
|
|
@@ -1250,6 +1337,31 @@
|
|
|
1250
1337
|
}
|
|
1251
1338
|
}
|
|
1252
1339
|
|
|
1340
|
+
mobileInput.addEventListener('compositionstart', function () {
|
|
1341
|
+
isComposing = true;
|
|
1342
|
+
});
|
|
1343
|
+
|
|
1344
|
+
mobileInput.addEventListener('compositionend', function () {
|
|
1345
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
1346
|
+
var currentValue = mobileInput.value;
|
|
1347
|
+
sendInputDiff(currentValue);
|
|
1348
|
+
lastInputValue = currentValue;
|
|
1349
|
+
}
|
|
1350
|
+
setTimeout(function () { isComposing = false; }, 0);
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
mobileInput.addEventListener('blur', function () {
|
|
1354
|
+
if (isComposing) {
|
|
1355
|
+
isComposing = false;
|
|
1356
|
+
lastInputValue = mobileInput.value;
|
|
1357
|
+
}
|
|
1358
|
+
});
|
|
1359
|
+
|
|
1360
|
+
mobileInput.addEventListener('sessionchange', function () {
|
|
1361
|
+
isComposing = false;
|
|
1362
|
+
lastInputValue = '';
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1253
1365
|
// Handle text input with autocorrect
|
|
1254
1366
|
var clearTimer = null;
|
|
1255
1367
|
mobileInput.addEventListener('input', function () {
|
|
@@ -1262,6 +1374,8 @@
|
|
|
1262
1374
|
|
|
1263
1375
|
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
1264
1376
|
|
|
1377
|
+
if (isComposing) return;
|
|
1378
|
+
|
|
1265
1379
|
var currentValue = mobileInput.value;
|
|
1266
1380
|
sendInputDiff(currentValue);
|
|
1267
1381
|
lastInputValue = currentValue;
|
|
@@ -1293,24 +1407,14 @@
|
|
|
1293
1407
|
lastInputValue = '';
|
|
1294
1408
|
break;
|
|
1295
1409
|
case 'Tab':
|
|
1296
|
-
e.preventDefault();
|
|
1297
1410
|
ws.send('\t');
|
|
1298
1411
|
break;
|
|
1299
1412
|
case 'ArrowUp':
|
|
1300
|
-
e.preventDefault();
|
|
1301
1413
|
ws.send('\x1b[A');
|
|
1302
1414
|
break;
|
|
1303
1415
|
case 'ArrowDown':
|
|
1304
|
-
e.preventDefault();
|
|
1305
1416
|
ws.send('\x1b[B');
|
|
1306
1417
|
break;
|
|
1307
|
-
case 'ArrowLeft':
|
|
1308
|
-
// Let input handle cursor movement for autocorrect
|
|
1309
|
-
handled = false;
|
|
1310
|
-
break;
|
|
1311
|
-
case 'ArrowRight':
|
|
1312
|
-
handled = false;
|
|
1313
|
-
break;
|
|
1314
1418
|
default:
|
|
1315
1419
|
handled = false;
|
|
1316
1420
|
}
|
package/public/icon.svg
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
|
2
|
+
<rect width="512" height="512" rx="64" fill="#1a1a1a"/>
|
|
3
|
+
<text x="256" y="200" text-anchor="middle" font-family="monospace" font-size="72" font-weight="bold" fill="#d97757">>_</text>
|
|
4
|
+
<text x="256" y="320" text-anchor="middle" font-family="-apple-system,sans-serif" font-size="56" font-weight="600" fill="#ececec">Claude</text>
|
|
5
|
+
<text x="256" y="390" text-anchor="middle" font-family="-apple-system,sans-serif" font-size="40" fill="#9b9b9b">Remote CLI</text>
|
|
6
|
+
</svg>
|
package/public/index.html
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
|
6
6
|
<title>Claude Remote CLI</title>
|
|
7
7
|
<link rel="manifest" href="/manifest.json" />
|
|
8
|
+
<link rel="icon" href="/icon.svg" type="image/svg+xml" />
|
|
9
|
+
<link rel="apple-touch-icon" href="/icon.svg" />
|
|
8
10
|
<meta name="mobile-web-app-capable" content="yes" />
|
|
9
11
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
10
12
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
@@ -64,7 +66,7 @@
|
|
|
64
66
|
<button id="menu-btn" class="icon-btn" aria-label="Open sessions menu">☰</button>
|
|
65
67
|
<span id="session-title" class="mobile-title">No session</span>
|
|
66
68
|
</div>
|
|
67
|
-
<input type="text" id="mobile-input" autocomplete="
|
|
69
|
+
<input type="text" id="mobile-input" dir="ltr" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" aria-label="Terminal input" />
|
|
68
70
|
<div id="terminal-container"></div>
|
|
69
71
|
<div id="terminal-scrollbar"><div id="terminal-scrollbar-thumb"></div></div>
|
|
70
72
|
<div id="no-session-msg">No active session. Create or select a session to begin.</div>
|
package/public/manifest.json
CHANGED
|
@@ -4,5 +4,13 @@
|
|
|
4
4
|
"display": "standalone",
|
|
5
5
|
"start_url": "/",
|
|
6
6
|
"background_color": "#1a1a1a",
|
|
7
|
-
"theme_color": "#1a1a1a"
|
|
7
|
+
"theme_color": "#1a1a1a",
|
|
8
|
+
"icons": [
|
|
9
|
+
{
|
|
10
|
+
"src": "/icon.svg",
|
|
11
|
+
"sizes": "any",
|
|
12
|
+
"type": "image/svg+xml",
|
|
13
|
+
"purpose": "any"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
8
16
|
}
|
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;
|
|
@@ -430,13 +470,20 @@ html, body {
|
|
|
430
470
|
|
|
431
471
|
#mobile-input {
|
|
432
472
|
position: absolute;
|
|
433
|
-
|
|
434
|
-
|
|
473
|
+
left: 0;
|
|
474
|
+
top: 0;
|
|
435
475
|
width: 1px;
|
|
436
476
|
height: 1px;
|
|
437
477
|
opacity: 0;
|
|
438
478
|
font-size: 16px; /* prevents iOS zoom on focus */
|
|
439
479
|
z-index: -1;
|
|
480
|
+
border: 0;
|
|
481
|
+
padding: 0;
|
|
482
|
+
margin: 0;
|
|
483
|
+
outline: none;
|
|
484
|
+
color: transparent;
|
|
485
|
+
caret-color: transparent;
|
|
486
|
+
background: transparent;
|
|
440
487
|
}
|
|
441
488
|
|
|
442
489
|
/* ===== Terminal Scrollbar ===== */
|