claude-code-remote-pilot 0.5.3 → 0.5.5
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/CHANGELOG.md +9 -3
- package/lib/SessionManager.js +5 -4
- package/lib/ui.html +57 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.5.
|
|
3
|
+
## 0.5.5 — 2026-05-06
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Respawn loading no longer appears stuck**: web respawn now has a request timeout and surfaces a clear timeout error when the API call hangs, so the button always returns to clickable state.
|
|
7
|
+
- **Terminal output now feels realtime**: session detail terminal polling runs at a faster cadence and triggers immediate output refresh after sending input/keys, improving perceived connect speed and responsiveness.
|
|
8
|
+
- **"Connecting…" no longer gets stuck**: terminal detail view now clears connecting state on poll failures, shows a retrying error hint, and fixes session state initialization order for stable render behavior.
|
|
9
|
+
|
|
10
|
+
## 0.5.4 — 2026-05-06
|
|
4
11
|
|
|
5
12
|
### Fixed
|
|
6
|
-
- **Auto-resume respects clock reset time**: limit recovery now waits until the parsed `resets at HH:MM` timestamp (with next-day rollover) before sending the resume command.
|
|
7
13
|
- **Web UI respawn completed**: offline session respawn now has loading/error feedback and immediately updates session detail state after success.
|
|
14
|
+
- **Web respawn now matches CLI spawn behavior**: respawn starts a fresh session from stored path and default command semantics (same as watch-mode spawn), avoiding failures caused by stale stored command values.
|
|
8
15
|
|
|
9
16
|
---
|
|
10
17
|
|
|
@@ -12,7 +19,6 @@
|
|
|
12
19
|
|
|
13
20
|
### Fixed
|
|
14
21
|
- **Auto-resume now waits for reset time**: when Claude shows an explicit `resets at HH:MM` clock time, `Watcher` now resumes at that exact reset timestamp (including next-day rollover) instead of relying only on relative wait parsing.
|
|
15
|
-
- **Web UI respawn flow completed**: offline session respawn now shows in-button loading, inline errors, and immediately updates the detail view to the newly active session after successful `POST /api/sessions/:name/respawn`.
|
|
16
22
|
|
|
17
23
|
---
|
|
18
24
|
|
package/lib/SessionManager.js
CHANGED
|
@@ -35,9 +35,10 @@ class SessionManager {
|
|
|
35
35
|
if (this.sessions.has(sessionName)) throw new Error(`Session "${sessionName}" already exists.`);
|
|
36
36
|
|
|
37
37
|
// Kill stale tmux session from a previous crashed run
|
|
38
|
-
try {
|
|
38
|
+
try {
|
|
39
|
+
execSync(`tmux has-session -t "${sessionName}"`, { stdio: 'ignore' });
|
|
39
40
|
execSync(`tmux kill-session -t "${sessionName}"`, { stdio: 'ignore' });
|
|
40
|
-
} catch {}
|
|
41
|
+
} catch { }
|
|
41
42
|
|
|
42
43
|
execSync(`tmux new-session -d -s "${sessionName}" -c "${resolved}" "${command}"`, { stdio: 'ignore' });
|
|
43
44
|
|
|
@@ -73,13 +74,13 @@ class SessionManager {
|
|
|
73
74
|
const entry = this.sessions.get(name);
|
|
74
75
|
if (!entry) throw new Error(`Session "${name}" not found.`);
|
|
75
76
|
entry.watcher.stop();
|
|
76
|
-
try { execSync(`tmux kill-session -t "${name}"`, { stdio: 'ignore' }); } catch {}
|
|
77
|
+
try { execSync(`tmux kill-session -t "${name}"`, { stdio: 'ignore' }); } catch { }
|
|
77
78
|
this.sessions.delete(name);
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
killAll() {
|
|
81
82
|
for (const name of [...this.sessions.keys()]) {
|
|
82
|
-
try { this.kill(name); } catch {}
|
|
83
|
+
try { this.kill(name); } catch { }
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
|
package/lib/ui.html
CHANGED
|
@@ -575,6 +575,7 @@ function DashboardScreen({ onNavigate, sessions, activity, serverStatus }) {
|
|
|
575
575
|
|
|
576
576
|
/* --- Session Detail --- */
|
|
577
577
|
function SessionDetailScreen({ session, onBack, onKilled, onRespawned }) {
|
|
578
|
+
const isOffline = session.status === 'offline';
|
|
578
579
|
const [output, setOutput] = useState('');
|
|
579
580
|
const [msg, setMsg] = useState('');
|
|
580
581
|
const [sending, setSending] = useState(false);
|
|
@@ -582,9 +583,11 @@ function SessionDetailScreen({ session, onBack, onKilled, onRespawned }) {
|
|
|
582
583
|
const [respawning, setRespawning] = useState(false);
|
|
583
584
|
const [respawnError, setRespawnError] = useState('');
|
|
584
585
|
const [copyOk, setCopyOk] = useState(false);
|
|
586
|
+
const [connecting, setConnecting] = useState(!isOffline);
|
|
587
|
+
const [pollError, setPollError] = useState(false);
|
|
585
588
|
const terminalRef = useRef(null);
|
|
586
589
|
const inputRef = useRef(null);
|
|
587
|
-
const
|
|
590
|
+
const pollNowRef = useRef(() => {});
|
|
588
591
|
|
|
589
592
|
const copyAttachCmd = () => {
|
|
590
593
|
const cmd = `tmux attach -t ${session.name}`;
|
|
@@ -599,18 +602,47 @@ function SessionDetailScreen({ session, onBack, onKilled, onRespawned }) {
|
|
|
599
602
|
if (!isOffline) setTimeout(() => inputRef.current?.focus(), 50);
|
|
600
603
|
}, [session.name, isOffline]);
|
|
601
604
|
|
|
602
|
-
// Poll terminal output
|
|
605
|
+
// Poll terminal output at near-realtime cadence
|
|
603
606
|
useEffect(() => {
|
|
604
|
-
if (isOffline)
|
|
607
|
+
if (isOffline) {
|
|
608
|
+
setConnecting(false);
|
|
609
|
+
setPollError(false);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
let mounted = true;
|
|
613
|
+
let inFlight = false;
|
|
614
|
+
|
|
605
615
|
const poll = () => {
|
|
616
|
+
if (inFlight) return;
|
|
617
|
+
inFlight = true;
|
|
606
618
|
apiFetch(`/api/sessions/${encodeURIComponent(session.name)}/output`, { cache: 'no-store' })
|
|
607
619
|
.then(r => r.json())
|
|
608
|
-
.then(d =>
|
|
609
|
-
|
|
620
|
+
.then(d => {
|
|
621
|
+
if (!mounted) return;
|
|
622
|
+
setOutput(d.output || '');
|
|
623
|
+
setConnecting(false);
|
|
624
|
+
setPollError(false);
|
|
625
|
+
})
|
|
626
|
+
.catch(e => {
|
|
627
|
+
if (!mounted) return;
|
|
628
|
+
if (e && e.message !== 'Unauthorized') console.error('[ccp] output poll error:', e);
|
|
629
|
+
setConnecting(false);
|
|
630
|
+
setPollError(true);
|
|
631
|
+
})
|
|
632
|
+
.finally(() => {
|
|
633
|
+
inFlight = false;
|
|
634
|
+
});
|
|
610
635
|
};
|
|
636
|
+
|
|
637
|
+
setConnecting(true);
|
|
638
|
+
pollNowRef.current = poll;
|
|
611
639
|
poll();
|
|
612
|
-
const t = setInterval(poll,
|
|
613
|
-
return () =>
|
|
640
|
+
const t = setInterval(poll, 600);
|
|
641
|
+
return () => {
|
|
642
|
+
mounted = false;
|
|
643
|
+
pollNowRef.current = () => {};
|
|
644
|
+
clearInterval(t);
|
|
645
|
+
};
|
|
614
646
|
}, [session.name, isOffline]);
|
|
615
647
|
|
|
616
648
|
// Auto-scroll terminal to bottom
|
|
@@ -628,6 +660,7 @@ function SessionDetailScreen({ session, onBack, onKilled, onRespawned }) {
|
|
|
628
660
|
body: JSON.stringify({ message: msg }),
|
|
629
661
|
});
|
|
630
662
|
setMsg('');
|
|
663
|
+
pollNowRef.current();
|
|
631
664
|
} catch {
|
|
632
665
|
} finally {
|
|
633
666
|
setSending(false);
|
|
@@ -643,6 +676,7 @@ function SessionDetailScreen({ session, onBack, onKilled, onRespawned }) {
|
|
|
643
676
|
headers: { 'Content-Type': 'application/json' },
|
|
644
677
|
body: JSON.stringify({ key }),
|
|
645
678
|
});
|
|
679
|
+
pollNowRef.current();
|
|
646
680
|
} catch {}
|
|
647
681
|
setTimeout(() => inputRef.current?.focus(), 0);
|
|
648
682
|
};
|
|
@@ -666,8 +700,13 @@ function SessionDetailScreen({ session, onBack, onKilled, onRespawned }) {
|
|
|
666
700
|
if (respawning) return;
|
|
667
701
|
setRespawning(true);
|
|
668
702
|
setRespawnError('');
|
|
703
|
+
const controller = new AbortController();
|
|
704
|
+
const timeoutId = setTimeout(() => controller.abort(), 12000);
|
|
669
705
|
try {
|
|
670
|
-
const res = await apiFetch(`/api/sessions/${encodeURIComponent(session.name)}/respawn`, {
|
|
706
|
+
const res = await apiFetch(`/api/sessions/${encodeURIComponent(session.name)}/respawn`, {
|
|
707
|
+
method: 'POST',
|
|
708
|
+
signal: controller.signal,
|
|
709
|
+
});
|
|
671
710
|
const data = await res.json();
|
|
672
711
|
if (!res.ok) {
|
|
673
712
|
setRespawnError(data.error || 'Failed to respawn');
|
|
@@ -675,8 +714,13 @@ function SessionDetailScreen({ session, onBack, onKilled, onRespawned }) {
|
|
|
675
714
|
}
|
|
676
715
|
onRespawned(data);
|
|
677
716
|
} catch (e) {
|
|
717
|
+
if (e && e.name === 'AbortError') {
|
|
718
|
+
setRespawnError('Respawn timeout. Please try again.');
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
678
721
|
if (e.message !== 'Unauthorized') setRespawnError('Network error');
|
|
679
722
|
} finally {
|
|
723
|
+
clearTimeout(timeoutId);
|
|
680
724
|
setRespawning(false);
|
|
681
725
|
}
|
|
682
726
|
};
|
|
@@ -754,7 +798,11 @@ function SessionDetailScreen({ session, onBack, onKilled, onRespawned }) {
|
|
|
754
798
|
? '<span style="color:oklch(50% 0.018 50)">Session is offline — no output available.</span>'
|
|
755
799
|
: _termHtml
|
|
756
800
|
? _termHtml + '<span style="opacity:0.4">▊</span>'
|
|
757
|
-
:
|
|
801
|
+
: (connecting
|
|
802
|
+
? '<span style="color:oklch(50% 0.018 50)">Connecting…</span>'
|
|
803
|
+
: (pollError
|
|
804
|
+
? '<span style="color:oklch(64% 0.20 28)">Can\'t read terminal output. Retrying…</span>'
|
|
805
|
+
: '<span style="color:oklch(50% 0.018 50)">No output yet.</span>'))
|
|
758
806
|
}}
|
|
759
807
|
/>
|
|
760
808
|
|
package/package.json
CHANGED