claude-code-remote-pilot 0.5.4 → 0.5.6
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 +8 -1
- package/lib/ui.html +63 -21
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
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
|
+
- **Respawn/End web actions stabilized**: removed false-positive client respawn timeout and added explicit inline error handling for End action responses, so both controls fail visibly instead of appearing stuck.
|
|
10
|
+
|
|
3
11
|
## 0.5.4 — 2026-05-06
|
|
4
12
|
|
|
5
13
|
### Fixed
|
|
6
14
|
- **Web UI respawn completed**: offline session respawn now has loading/error feedback and immediately updates session detail state after success.
|
|
7
15
|
- **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
|
-
- **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.
|
|
9
16
|
|
|
10
17
|
---
|
|
11
18
|
|
package/lib/ui.html
CHANGED
|
@@ -575,16 +575,20 @@ 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);
|
|
581
582
|
const [killing, setKilling] = useState(false);
|
|
583
|
+
const [killError, setKillError] = useState('');
|
|
582
584
|
const [respawning, setRespawning] = useState(false);
|
|
583
585
|
const [respawnError, setRespawnError] = useState('');
|
|
584
586
|
const [copyOk, setCopyOk] = useState(false);
|
|
587
|
+
const [connecting, setConnecting] = useState(!isOffline);
|
|
588
|
+
const [pollError, setPollError] = useState(false);
|
|
585
589
|
const terminalRef = useRef(null);
|
|
586
590
|
const inputRef = useRef(null);
|
|
587
|
-
const
|
|
591
|
+
const pollNowRef = useRef(() => {});
|
|
588
592
|
|
|
589
593
|
const copyAttachCmd = () => {
|
|
590
594
|
const cmd = `tmux attach -t ${session.name}`;
|
|
@@ -599,18 +603,47 @@ function SessionDetailScreen({ session, onBack, onKilled, onRespawned }) {
|
|
|
599
603
|
if (!isOffline) setTimeout(() => inputRef.current?.focus(), 50);
|
|
600
604
|
}, [session.name, isOffline]);
|
|
601
605
|
|
|
602
|
-
// Poll terminal output
|
|
606
|
+
// Poll terminal output at near-realtime cadence
|
|
603
607
|
useEffect(() => {
|
|
604
|
-
if (isOffline)
|
|
608
|
+
if (isOffline) {
|
|
609
|
+
setConnecting(false);
|
|
610
|
+
setPollError(false);
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
let mounted = true;
|
|
614
|
+
let inFlight = false;
|
|
615
|
+
|
|
605
616
|
const poll = () => {
|
|
617
|
+
if (inFlight) return;
|
|
618
|
+
inFlight = true;
|
|
606
619
|
apiFetch(`/api/sessions/${encodeURIComponent(session.name)}/output`, { cache: 'no-store' })
|
|
607
620
|
.then(r => r.json())
|
|
608
|
-
.then(d =>
|
|
609
|
-
|
|
621
|
+
.then(d => {
|
|
622
|
+
if (!mounted) return;
|
|
623
|
+
setOutput(d.output || '');
|
|
624
|
+
setConnecting(false);
|
|
625
|
+
setPollError(false);
|
|
626
|
+
})
|
|
627
|
+
.catch(e => {
|
|
628
|
+
if (!mounted) return;
|
|
629
|
+
if (e && e.message !== 'Unauthorized') console.error('[ccp] output poll error:', e);
|
|
630
|
+
setConnecting(false);
|
|
631
|
+
setPollError(true);
|
|
632
|
+
})
|
|
633
|
+
.finally(() => {
|
|
634
|
+
inFlight = false;
|
|
635
|
+
});
|
|
610
636
|
};
|
|
637
|
+
|
|
638
|
+
setConnecting(true);
|
|
639
|
+
pollNowRef.current = poll;
|
|
611
640
|
poll();
|
|
612
|
-
const t = setInterval(poll,
|
|
613
|
-
return () =>
|
|
641
|
+
const t = setInterval(poll, 600);
|
|
642
|
+
return () => {
|
|
643
|
+
mounted = false;
|
|
644
|
+
pollNowRef.current = () => {};
|
|
645
|
+
clearInterval(t);
|
|
646
|
+
};
|
|
614
647
|
}, [session.name, isOffline]);
|
|
615
648
|
|
|
616
649
|
// Auto-scroll terminal to bottom
|
|
@@ -628,6 +661,7 @@ function SessionDetailScreen({ session, onBack, onKilled, onRespawned }) {
|
|
|
628
661
|
body: JSON.stringify({ message: msg }),
|
|
629
662
|
});
|
|
630
663
|
setMsg('');
|
|
664
|
+
pollNowRef.current();
|
|
631
665
|
} catch {
|
|
632
666
|
} finally {
|
|
633
667
|
setSending(false);
|
|
@@ -643,6 +677,7 @@ function SessionDetailScreen({ session, onBack, onKilled, onRespawned }) {
|
|
|
643
677
|
headers: { 'Content-Type': 'application/json' },
|
|
644
678
|
body: JSON.stringify({ key }),
|
|
645
679
|
});
|
|
680
|
+
pollNowRef.current();
|
|
646
681
|
} catch {}
|
|
647
682
|
setTimeout(() => inputRef.current?.focus(), 0);
|
|
648
683
|
};
|
|
@@ -654,10 +689,18 @@ function SessionDetailScreen({ session, onBack, onKilled, onRespawned }) {
|
|
|
654
689
|
const handleKill = async () => {
|
|
655
690
|
if (!confirm(`End session "${session.name}"?`)) return;
|
|
656
691
|
setKilling(true);
|
|
692
|
+
setKillError('');
|
|
657
693
|
try {
|
|
658
|
-
await apiFetch(`/api/sessions/${encodeURIComponent(session.name)}`, { method: 'DELETE' });
|
|
694
|
+
const res = await apiFetch(`/api/sessions/${encodeURIComponent(session.name)}`, { method: 'DELETE' });
|
|
695
|
+
if (!res.ok) {
|
|
696
|
+
const d = await res.json().catch(() => ({}));
|
|
697
|
+
setKillError(d.error || 'Failed to end session');
|
|
698
|
+
setKilling(false);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
659
701
|
onKilled();
|
|
660
|
-
} catch {
|
|
702
|
+
} catch (e) {
|
|
703
|
+
if (e && e.message !== 'Unauthorized') setKillError('Network error');
|
|
661
704
|
setKilling(false);
|
|
662
705
|
}
|
|
663
706
|
};
|
|
@@ -666,13 +709,8 @@ function SessionDetailScreen({ session, onBack, onKilled, onRespawned }) {
|
|
|
666
709
|
if (respawning) return;
|
|
667
710
|
setRespawning(true);
|
|
668
711
|
setRespawnError('');
|
|
669
|
-
const controller = new AbortController();
|
|
670
|
-
const timeoutId = setTimeout(() => controller.abort(), 12000);
|
|
671
712
|
try {
|
|
672
|
-
const res = await apiFetch(`/api/sessions/${encodeURIComponent(session.name)}/respawn`, {
|
|
673
|
-
method: 'POST',
|
|
674
|
-
signal: controller.signal,
|
|
675
|
-
});
|
|
713
|
+
const res = await apiFetch(`/api/sessions/${encodeURIComponent(session.name)}/respawn`, { method: 'POST' });
|
|
676
714
|
const data = await res.json();
|
|
677
715
|
if (!res.ok) {
|
|
678
716
|
setRespawnError(data.error || 'Failed to respawn');
|
|
@@ -680,13 +718,8 @@ function SessionDetailScreen({ session, onBack, onKilled, onRespawned }) {
|
|
|
680
718
|
}
|
|
681
719
|
onRespawned(data);
|
|
682
720
|
} catch (e) {
|
|
683
|
-
if (e && e.name === 'AbortError') {
|
|
684
|
-
setRespawnError('Respawn timeout. Please try again.');
|
|
685
|
-
return;
|
|
686
|
-
}
|
|
687
721
|
if (e.message !== 'Unauthorized') setRespawnError('Network error');
|
|
688
722
|
} finally {
|
|
689
|
-
clearTimeout(timeoutId);
|
|
690
723
|
setRespawning(false);
|
|
691
724
|
}
|
|
692
725
|
};
|
|
@@ -740,6 +773,11 @@ function SessionDetailScreen({ session, onBack, onKilled, onRespawned }) {
|
|
|
740
773
|
{Icons.trash} {killing ? 'Ending…' : 'End'}
|
|
741
774
|
</button>
|
|
742
775
|
)}
|
|
776
|
+
{!isOffline && killError && (
|
|
777
|
+
<span style={{ color: 'var(--error)', fontSize: 12, alignSelf: 'center' }}>
|
|
778
|
+
{killError}
|
|
779
|
+
</span>
|
|
780
|
+
)}
|
|
743
781
|
</div>
|
|
744
782
|
</div>
|
|
745
783
|
|
|
@@ -764,7 +802,11 @@ function SessionDetailScreen({ session, onBack, onKilled, onRespawned }) {
|
|
|
764
802
|
? '<span style="color:oklch(50% 0.018 50)">Session is offline — no output available.</span>'
|
|
765
803
|
: _termHtml
|
|
766
804
|
? _termHtml + '<span style="opacity:0.4">▊</span>'
|
|
767
|
-
:
|
|
805
|
+
: (connecting
|
|
806
|
+
? '<span style="color:oklch(50% 0.018 50)">Connecting…</span>'
|
|
807
|
+
: (pollError
|
|
808
|
+
? '<span style="color:oklch(64% 0.20 28)">Can\'t read terminal output. Retrying…</span>'
|
|
809
|
+
: '<span style="color:oklch(50% 0.018 50)">No output yet.</span>'))
|
|
768
810
|
}}
|
|
769
811
|
/>
|
|
770
812
|
|
package/package.json
CHANGED