claude-code-remote-pilot 0.5.11 → 0.5.12
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/lib/WebServer.js +33 -7
- package/lib/config.js +12 -1
- package/lib/ui.html +96 -14
- package/package.json +1 -1
package/lib/WebServer.js
CHANGED
|
@@ -22,6 +22,7 @@ class WebServer {
|
|
|
22
22
|
this._tunnelProcess = null;
|
|
23
23
|
this._tunnelUrl = null;
|
|
24
24
|
this._queues = new Map(); // name → { items: [{id,message}], autoFeed, pendingSend }
|
|
25
|
+
this._sessionMeta = config.getAllSessionMeta(); // name → { emoji, color }
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
_buildAllSessions() {
|
|
@@ -33,7 +34,12 @@ class WebServer {
|
|
|
33
34
|
.map(h => ({ name: h.name, path: h.path, status: 'offline', startedAt: h.lastSeen, resumeAt: null }));
|
|
34
35
|
return [...active, ...offline].map(s => {
|
|
35
36
|
const q = this._queues.get(s.name);
|
|
36
|
-
|
|
37
|
+
const m = this._sessionMeta[s.name] || {};
|
|
38
|
+
return {
|
|
39
|
+
...s, id: s.name,
|
|
40
|
+
queueLength: q ? q.items.length : 0, autoFeed: q ? q.autoFeed : false,
|
|
41
|
+
emoji: m.emoji || '', color: m.color || '',
|
|
42
|
+
};
|
|
37
43
|
});
|
|
38
44
|
}
|
|
39
45
|
|
|
@@ -44,6 +50,13 @@ class WebServer {
|
|
|
44
50
|
return this._queues.get(name);
|
|
45
51
|
}
|
|
46
52
|
|
|
53
|
+
// Send a text message followed by Enter. Uses -l (literal) so message content
|
|
54
|
+
// is never misinterpreted as tmux key sequences, then sends Enter separately.
|
|
55
|
+
_tmuxSend(target, message) {
|
|
56
|
+
spawnSync('tmux', ['send-keys', '-t', target, '-l', message]);
|
|
57
|
+
spawnSync('tmux', ['send-keys', '-t', target, 'Enter']);
|
|
58
|
+
}
|
|
59
|
+
|
|
47
60
|
_getOutput(name) {
|
|
48
61
|
const result = spawnSync('tmux', ['capture-pane', '-pt', name, '-e', '-S', '-500'], { encoding: 'utf8' });
|
|
49
62
|
if (!result.stdout) return '';
|
|
@@ -111,9 +124,7 @@ class WebServer {
|
|
|
111
124
|
try {
|
|
112
125
|
const session = this.manager.spawn(dirPath, name, command || 'claude');
|
|
113
126
|
if (initialPrompt) {
|
|
114
|
-
setTimeout(() =>
|
|
115
|
-
spawnSync('tmux', ['send-keys', '-t', session.name, initialPrompt, 'Enter']);
|
|
116
|
-
}, 2000);
|
|
127
|
+
setTimeout(() => this._tmuxSend(session.name, initialPrompt), 2000);
|
|
117
128
|
}
|
|
118
129
|
this._json(res, 201, { ...session, id: session.name });
|
|
119
130
|
} catch (e) {
|
|
@@ -153,7 +164,7 @@ class WebServer {
|
|
|
153
164
|
return this._json(res, 200, { ok: true });
|
|
154
165
|
}
|
|
155
166
|
if (!message) return this._json(res, 400, { error: 'message or key required' });
|
|
156
|
-
|
|
167
|
+
this._tmuxSend(name, message);
|
|
157
168
|
this._json(res, 200, { ok: true });
|
|
158
169
|
});
|
|
159
170
|
}
|
|
@@ -196,10 +207,25 @@ class WebServer {
|
|
|
196
207
|
const q = this._queues.get(name);
|
|
197
208
|
if (!q || !q.items.length) return this._json(res, 400, { error: 'Queue is empty' });
|
|
198
209
|
const item = q.items.shift();
|
|
199
|
-
|
|
210
|
+
this._tmuxSend(name, item.message);
|
|
200
211
|
return this._json(res, 200, { ok: true, item });
|
|
201
212
|
}
|
|
202
213
|
|
|
214
|
+
// PATCH /api/sessions/:name/meta — save emoji / color label
|
|
215
|
+
const metaMatch = pathname.match(/^\/api\/sessions\/([^/]+)\/meta$/);
|
|
216
|
+
if (req.method === 'PATCH' && metaMatch) {
|
|
217
|
+
const name = decodeURIComponent(metaMatch[1]);
|
|
218
|
+
return this._readBody(req, (err, body) => {
|
|
219
|
+
if (err) return this._json(res, 400, { error: err.message });
|
|
220
|
+
const patch = {};
|
|
221
|
+
if (body.emoji !== undefined) patch.emoji = String(body.emoji).slice(0, 8);
|
|
222
|
+
if (body.color !== undefined) patch.color = String(body.color).slice(0, 20);
|
|
223
|
+
this._sessionMeta[name] = { ...(this._sessionMeta[name] || {}), ...patch };
|
|
224
|
+
config.saveSessionMeta(name, patch);
|
|
225
|
+
return this._json(res, 200, { ok: true });
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
203
229
|
// DELETE /api/sessions/:name/queue/:id — remove a specific queued item
|
|
204
230
|
const queueItemMatch = pathname.match(/^\/api\/sessions\/([^/]+)\/queue\/([^/]+)$/);
|
|
205
231
|
if (req.method === 'DELETE' && queueItemMatch) {
|
|
@@ -251,7 +277,7 @@ class WebServer {
|
|
|
251
277
|
if (isWaiting && !q.pendingSend) {
|
|
252
278
|
q.pendingSend = true;
|
|
253
279
|
const item = q.items.shift();
|
|
254
|
-
|
|
280
|
+
this._tmuxSend(session.name, item.message);
|
|
255
281
|
} else if (!isWaiting) {
|
|
256
282
|
q.pendingSend = false;
|
|
257
283
|
}
|
package/lib/config.js
CHANGED
|
@@ -50,4 +50,15 @@ function getHistory() {
|
|
|
50
50
|
return load().sessionHistory || [];
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
function getAllSessionMeta() {
|
|
54
|
+
return load().sessionMeta || {};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function saveSessionMeta(name, meta) {
|
|
58
|
+
const cfg = load();
|
|
59
|
+
const metas = cfg.sessionMeta || {};
|
|
60
|
+
metas[name] = { ...(metas[name] || {}), ...meta };
|
|
61
|
+
save({ sessionMeta: metas });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = { load, saveTelegram, saveSessions, clearSessions, saveResumeCommand, addToHistory, removeFromHistory, getHistory, getAllSessionMeta, saveSessionMeta };
|
package/lib/ui.html
CHANGED
|
@@ -247,6 +247,18 @@ const storage = {
|
|
|
247
247
|
setItem(key, val) { try { localStorage.setItem(key, val); } catch { memStore.set(key, val); } },
|
|
248
248
|
};
|
|
249
249
|
|
|
250
|
+
/* --- Session customization --- */
|
|
251
|
+
const CARD_COLORS = ['', '#ef4444', '#f97316', '#eab308', '#22c55e', '#06b6d4', '#3b82f6', '#a855f7', '#ec4899'];
|
|
252
|
+
|
|
253
|
+
const STATUS_SORT_ORDER = { running: 0, 'needs-response': 1, idle: 2, limit: 3, offline: 4, ended: 5 };
|
|
254
|
+
function sortSessions(sessions, by) {
|
|
255
|
+
if (by === 'name') return [...sessions].sort((a, b) => a.name.localeCompare(b.name));
|
|
256
|
+
return [...sessions].sort((a, b) => {
|
|
257
|
+
const ao = STATUS_SORT_ORDER[a.status] ?? 9, bo = STATUS_SORT_ORDER[b.status] ?? 9;
|
|
258
|
+
return ao !== bo ? ao - bo : a.name.localeCompare(b.name);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
250
262
|
/* --- Auth module-level helpers --- */
|
|
251
263
|
let _authToken = storage.getItem('ccp-token') || '';
|
|
252
264
|
let _onUnauth = () => {};
|
|
@@ -505,8 +517,27 @@ function Sidebar({ currentScreen, onNavigate, sessionCount, open, onClose, conne
|
|
|
505
517
|
);
|
|
506
518
|
}
|
|
507
519
|
|
|
520
|
+
/* --- Sort control --- */
|
|
521
|
+
function SortControl({ sortBy, onSortChange }) {
|
|
522
|
+
return (
|
|
523
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
|
524
|
+
<span style={{ fontSize: 11, color: 'var(--muted)', marginRight: 2 }}>Sort:</span>
|
|
525
|
+
{['status', 'name'].map(opt => (
|
|
526
|
+
<button key={opt} onClick={() => onSortChange(opt)} style={{
|
|
527
|
+
fontSize: 11, padding: '3px 7px', borderRadius: 4, cursor: 'pointer',
|
|
528
|
+
border: '1px solid var(--border)',
|
|
529
|
+
background: sortBy === opt ? 'var(--accent-soft)' : 'var(--bg)',
|
|
530
|
+
color: sortBy === opt ? 'var(--accent)' : 'var(--muted)',
|
|
531
|
+
fontWeight: sortBy === opt ? 600 : 400,
|
|
532
|
+
}}>{opt.charAt(0).toUpperCase() + opt.slice(1)}</button>
|
|
533
|
+
))}
|
|
534
|
+
</div>
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
|
|
508
538
|
/* --- Dashboard --- */
|
|
509
|
-
function DashboardScreen({ onNavigate, sessions, activity, serverStatus }) {
|
|
539
|
+
function DashboardScreen({ onNavigate, sessions, activity, serverStatus, sortBy, onSortChange }) {
|
|
540
|
+
const sorted = sortSessions(sessions, sortBy);
|
|
510
541
|
const running = sessions.filter(s => s.status === 'running');
|
|
511
542
|
const active = sessions.filter(s => s.status !== 'offline');
|
|
512
543
|
const uptime = serverStatus ? formatUptime(serverStatus.startedAt) : '—';
|
|
@@ -523,10 +554,13 @@ function DashboardScreen({ onNavigate, sessions, activity, serverStatus }) {
|
|
|
523
554
|
|
|
524
555
|
<div className="section-header">
|
|
525
556
|
<h2 className="section-title">Sessions</h2>
|
|
526
|
-
<
|
|
557
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
558
|
+
<SortControl sortBy={sortBy} onSortChange={onSortChange} />
|
|
559
|
+
<button className="btn btn-sm" onClick={() => onNavigate('create')}>{Icons.plus} New</button>
|
|
560
|
+
</div>
|
|
527
561
|
</div>
|
|
528
562
|
|
|
529
|
-
{
|
|
563
|
+
{sorted.length === 0 ? (
|
|
530
564
|
<div className="empty-state">
|
|
531
565
|
<div className="empty-state-icon">⌘</div>
|
|
532
566
|
<div className="empty-state-title">No sessions yet</div>
|
|
@@ -534,11 +568,13 @@ function DashboardScreen({ onNavigate, sessions, activity, serverStatus }) {
|
|
|
534
568
|
</div>
|
|
535
569
|
) : (
|
|
536
570
|
<div className="session-cards">
|
|
537
|
-
{
|
|
538
|
-
<div key={s.id} className={`session-card ${s.status === 'offline' ? 'offline' : ''}`}
|
|
571
|
+
{sorted.map(s => (
|
|
572
|
+
<div key={s.id} className={`session-card ${s.status === 'offline' ? 'offline' : ''}`}
|
|
573
|
+
style={s.color ? { borderLeft: `4px solid ${s.color}` } : {}}
|
|
574
|
+
onClick={() => onNavigate('detail', s)}>
|
|
539
575
|
<div className="session-card-header">
|
|
540
576
|
<div>
|
|
541
|
-
<div className="session-card-name">{s.name}</div>
|
|
577
|
+
<div className="session-card-name">{s.emoji ? <span style={{ marginRight: 5 }}>{s.emoji}</span> : null}{s.name}</div>
|
|
542
578
|
<div className="session-card-meta" style={{ maxWidth: 220, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{s.path}</div>
|
|
543
579
|
</div>
|
|
544
580
|
<StatusPill status={s.status} />
|
|
@@ -584,6 +620,8 @@ function SessionDetailScreen({ session, onBack, onKilled, onRespawned }) {
|
|
|
584
620
|
const [output, setOutput] = useState('');
|
|
585
621
|
const [msg, setMsg] = useState('');
|
|
586
622
|
const [sending, setSending] = useState(false);
|
|
623
|
+
const [emoji, setEmoji] = useState(session.emoji || '');
|
|
624
|
+
const [color, setColor] = useState(session.color || '');
|
|
587
625
|
const [killing, setKilling] = useState(false);
|
|
588
626
|
const [killError, setKillError] = useState('');
|
|
589
627
|
const [respawning, setRespawning] = useState(false);
|
|
@@ -603,6 +641,20 @@ function SessionDetailScreen({ session, onBack, onKilled, onRespawned }) {
|
|
|
603
641
|
}).catch(() => {});
|
|
604
642
|
};
|
|
605
643
|
|
|
644
|
+
// Sync label fields when navigating between sessions
|
|
645
|
+
useEffect(() => {
|
|
646
|
+
setEmoji(session.emoji || '');
|
|
647
|
+
setColor(session.color || '');
|
|
648
|
+
}, [session.name]);
|
|
649
|
+
|
|
650
|
+
const saveMeta = useCallback((e, c) => {
|
|
651
|
+
apiFetch(`/api/sessions/${encodeURIComponent(session.name)}/meta`, {
|
|
652
|
+
method: 'PATCH',
|
|
653
|
+
headers: { 'Content-Type': 'application/json' },
|
|
654
|
+
body: JSON.stringify({ emoji: e, color: c }),
|
|
655
|
+
}).catch(() => {});
|
|
656
|
+
}, [session.name]);
|
|
657
|
+
|
|
606
658
|
// Auto-focus input on mount and when session changes
|
|
607
659
|
useEffect(() => {
|
|
608
660
|
if (!isOffline) setTimeout(() => inputRef.current?.focus(), 50);
|
|
@@ -752,7 +804,7 @@ function SessionDetailScreen({ session, onBack, onKilled, onRespawned }) {
|
|
|
752
804
|
|
|
753
805
|
<div className="detail-header">
|
|
754
806
|
<div>
|
|
755
|
-
<h2 className="detail-title">{session.name}</h2>
|
|
807
|
+
<h2 className="detail-title">{emoji ? <span style={{ marginRight: 6 }}>{emoji}</span> : null}{session.name}</h2>
|
|
756
808
|
<div className="detail-meta" style={{ maxWidth: 480, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{session.path}</div>
|
|
757
809
|
</div>
|
|
758
810
|
<div className="detail-actions">
|
|
@@ -849,6 +901,28 @@ function SessionDetailScreen({ session, onBack, onKilled, onRespawned }) {
|
|
|
849
901
|
<div className="info-row"><span className="info-label">Resumes</span><span className="info-value" style={{ fontSize: 11, color: 'var(--warning)' }}>{relativeTime(session.resumeAt)}</span></div>
|
|
850
902
|
)}
|
|
851
903
|
<div className="info-row"><span className="info-label">tmux</span><span className="info-value" style={{ fontSize: 11 }}>{session.name}</span></div>
|
|
904
|
+
<div className="info-row" style={{ alignItems: 'flex-start', paddingTop: 10, marginTop: 4, borderTop: '1px solid var(--border)' }}>
|
|
905
|
+
<span className="info-label" style={{ paddingTop: 4 }}>Label</span>
|
|
906
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
907
|
+
<input
|
|
908
|
+
value={emoji}
|
|
909
|
+
onChange={e => setEmoji(e.target.value)}
|
|
910
|
+
onBlur={() => saveMeta(emoji, color)}
|
|
911
|
+
maxLength={8}
|
|
912
|
+
placeholder="😀"
|
|
913
|
+
style={{ width: 44, textAlign: 'center', fontSize: 18, padding: '2px 4px', border: '1px solid var(--border)', borderRadius: 4, background: 'var(--bg)', color: 'var(--fg)', cursor: 'text' }}
|
|
914
|
+
/>
|
|
915
|
+
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
|
916
|
+
{CARD_COLORS.map(c => (
|
|
917
|
+
<button key={c || 'none'} onClick={() => { setColor(c); saveMeta(emoji, c); }} title={c || 'No color'} style={{
|
|
918
|
+
width: 16, height: 16, borderRadius: '50%', padding: 0, cursor: 'pointer', flexShrink: 0,
|
|
919
|
+
background: c || 'transparent',
|
|
920
|
+
border: color === c ? '2px solid var(--fg)' : c ? '1px solid oklch(0% 0 0 / 0.2)' : '1px dashed var(--muted)',
|
|
921
|
+
}} />
|
|
922
|
+
))}
|
|
923
|
+
</div>
|
|
924
|
+
</div>
|
|
925
|
+
</div>
|
|
852
926
|
</div>
|
|
853
927
|
</div>
|
|
854
928
|
</div>
|
|
@@ -934,14 +1008,18 @@ function CreateSessionScreen({ onBack, onCreated }) {
|
|
|
934
1008
|
}
|
|
935
1009
|
|
|
936
1010
|
/* --- Sessions List --- */
|
|
937
|
-
function SessionsScreen({ sessions, onNavigate }) {
|
|
1011
|
+
function SessionsScreen({ sessions, onNavigate, sortBy, onSortChange }) {
|
|
1012
|
+
const sorted = sortSessions(sessions, sortBy);
|
|
938
1013
|
return (
|
|
939
1014
|
<div>
|
|
940
1015
|
<div className="section-header">
|
|
941
1016
|
<h2 className="section-title">All Sessions</h2>
|
|
942
|
-
<
|
|
1017
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
1018
|
+
<SortControl sortBy={sortBy} onSortChange={onSortChange} />
|
|
1019
|
+
<button className="btn btn-sm" onClick={() => onNavigate('create')}>{Icons.plus} New</button>
|
|
1020
|
+
</div>
|
|
943
1021
|
</div>
|
|
944
|
-
{
|
|
1022
|
+
{sorted.length === 0 ? (
|
|
945
1023
|
<div className="empty-state"><div className="empty-state-icon">⌘</div><div className="empty-state-title">No sessions</div></div>
|
|
946
1024
|
) : (
|
|
947
1025
|
<div style={{ background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 'var(--radius-md)' }}>
|
|
@@ -952,12 +1030,14 @@ function SessionsScreen({ sessions, onNavigate }) {
|
|
|
952
1030
|
))}</tr>
|
|
953
1031
|
</thead>
|
|
954
1032
|
<tbody>
|
|
955
|
-
{
|
|
1033
|
+
{sorted.map(s => (
|
|
956
1034
|
<tr key={s.id} style={{ cursor:'pointer',opacity:s.status==='offline'?0.6:1,transition:'background 0.12s' }}
|
|
957
1035
|
onClick={() => onNavigate('detail', s)}
|
|
958
1036
|
onMouseEnter={e => e.currentTarget.style.background='var(--surface-hover)'}
|
|
959
1037
|
onMouseLeave={e => e.currentTarget.style.background=''}>
|
|
960
|
-
<td style={{ padding:'12px 14px',fontWeight:600,fontSize:13,borderBottom:'1px solid var(--border)'
|
|
1038
|
+
<td style={{ padding:'12px 14px',fontWeight:600,fontSize:13,borderBottom:'1px solid var(--border)',borderLeft: s.color ? `4px solid ${s.color}` : undefined }}>
|
|
1039
|
+
{s.emoji ? <span style={{ marginRight: 5 }}>{s.emoji}</span> : null}{s.name}
|
|
1040
|
+
</td>
|
|
961
1041
|
<td style={{ padding:'12px 14px',fontSize:12,color:'var(--muted)',borderBottom:'1px solid var(--border)',maxWidth:200,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap' }}>{s.path}</td>
|
|
962
1042
|
<td style={{ padding:'12px 14px',borderBottom:'1px solid var(--border)' }}><StatusPill status={s.status} /></td>
|
|
963
1043
|
<td style={{ padding:'12px 14px',fontSize:12,color:'var(--muted)',fontFamily:'var(--font-mono)',borderBottom:'1px solid var(--border)' }}>{relativeTime(s.startedAt)}</td>
|
|
@@ -1171,6 +1251,8 @@ function QueuePanel({ session }) {
|
|
|
1171
1251
|
function App() {
|
|
1172
1252
|
const savedTheme = storage.getItem('ccp-theme');
|
|
1173
1253
|
const [dark, setDark] = useState(savedTheme === 'dark');
|
|
1254
|
+
const [sortBy, setSortByState] = useState(storage.getItem('ccp-sort') || 'status');
|
|
1255
|
+
const setSortBy = (v) => { setSortByState(v); storage.setItem('ccp-sort', v); };
|
|
1174
1256
|
const [screen, setScreen] = useState('dashboard');
|
|
1175
1257
|
const [selectedSession, setSelectedSession] = useState(null);
|
|
1176
1258
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
|
@@ -1279,8 +1361,8 @@ function App() {
|
|
|
1279
1361
|
|
|
1280
1362
|
const renderScreen = () => {
|
|
1281
1363
|
switch (screen) {
|
|
1282
|
-
case 'dashboard': return <DashboardScreen onNavigate={navigate} sessions={sessions} activity={activity} serverStatus={serverStatus} />;
|
|
1283
|
-
case 'sessions': return <SessionsScreen sessions={sessions} onNavigate={navigate} />;
|
|
1364
|
+
case 'dashboard': return <DashboardScreen onNavigate={navigate} sessions={sessions} activity={activity} serverStatus={serverStatus} sortBy={sortBy} onSortChange={setSortBy} />;
|
|
1365
|
+
case 'sessions': return <SessionsScreen sessions={sessions} onNavigate={navigate} sortBy={sortBy} onSortChange={setSortBy} />;
|
|
1284
1366
|
case 'create': return <CreateSessionScreen onBack={() => navigate('dashboard')} onCreated={s => navigate('detail', s)} />;
|
|
1285
1367
|
case 'detail': return <SessionDetailScreen session={selectedSession} onBack={() => navigate('dashboard')} onKilled={() => navigate('sessions')} onRespawned={(respawned) => {
|
|
1286
1368
|
setSessions(prev => {
|
package/package.json
CHANGED