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 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
- return { ...s, id: s.name, queueLength: q ? q.items.length : 0, autoFeed: q ? q.autoFeed : false };
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
- spawnSync('tmux', ['send-keys', '-t', name, message, 'Enter']);
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
- spawnSync('tmux', ['send-keys', '-t', name, item.message, 'Enter']);
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
- spawnSync('tmux', ['send-keys', '-t', session.name, item.message, 'Enter']);
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
- module.exports = { load, saveTelegram, saveSessions, clearSessions, saveResumeCommand, addToHistory, removeFromHistory, getHistory };
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
- <button className="btn btn-sm" onClick={() => onNavigate('create')}>{Icons.plus} New</button>
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
- {sessions.length === 0 ? (
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
- {sessions.map(s => (
538
- <div key={s.id} className={`session-card ${s.status === 'offline' ? 'offline' : ''}`} onClick={() => onNavigate('detail', s)}>
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
- <button className="btn btn-sm" onClick={() => onNavigate('create')}>{Icons.plus} New</button>
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
- {sessions.length === 0 ? (
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
- {sessions.map(s => (
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)' }}>{s.name}</td>
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-remote-pilot",
3
- "version": "0.5.11",
3
+ "version": "0.5.12",
4
4
  "description": "Interactive Claude Code supervisor — spawn and monitor multiple Claude sessions from a single terminal.",
5
5
  "type": "commonjs",
6
6
  "repository": {