claude-code-remote-pilot 0.5.2 → 0.5.4
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 -0
- package/lib/SessionManager.js +5 -4
- package/lib/ui.html +41 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.4 — 2026-05-06
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Web UI respawn completed**: offline session respawn now has loading/error feedback and immediately updates session detail state after success.
|
|
7
|
+
- **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
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
3
12
|
## 0.5.2 — 2026-05-06
|
|
4
13
|
|
|
5
14
|
### Fixed
|
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
|
@@ -574,11 +574,13 @@ function DashboardScreen({ onNavigate, sessions, activity, serverStatus }) {
|
|
|
574
574
|
}
|
|
575
575
|
|
|
576
576
|
/* --- Session Detail --- */
|
|
577
|
-
function SessionDetailScreen({ session, onBack, onKilled }) {
|
|
577
|
+
function SessionDetailScreen({ session, onBack, onKilled, onRespawned }) {
|
|
578
578
|
const [output, setOutput] = useState('');
|
|
579
579
|
const [msg, setMsg] = useState('');
|
|
580
580
|
const [sending, setSending] = useState(false);
|
|
581
581
|
const [killing, setKilling] = useState(false);
|
|
582
|
+
const [respawning, setRespawning] = useState(false);
|
|
583
|
+
const [respawnError, setRespawnError] = useState('');
|
|
582
584
|
const [copyOk, setCopyOk] = useState(false);
|
|
583
585
|
const terminalRef = useRef(null);
|
|
584
586
|
const inputRef = useRef(null);
|
|
@@ -661,14 +663,31 @@ function SessionDetailScreen({ session, onBack, onKilled }) {
|
|
|
661
663
|
};
|
|
662
664
|
|
|
663
665
|
const handleRespawn = async () => {
|
|
666
|
+
if (respawning) return;
|
|
667
|
+
setRespawning(true);
|
|
668
|
+
setRespawnError('');
|
|
669
|
+
const controller = new AbortController();
|
|
670
|
+
const timeoutId = setTimeout(() => controller.abort(), 12000);
|
|
664
671
|
try {
|
|
665
|
-
const res = await apiFetch(`/api/sessions/${encodeURIComponent(session.name)}/respawn`, {
|
|
672
|
+
const res = await apiFetch(`/api/sessions/${encodeURIComponent(session.name)}/respawn`, {
|
|
673
|
+
method: 'POST',
|
|
674
|
+
signal: controller.signal,
|
|
675
|
+
});
|
|
676
|
+
const data = await res.json();
|
|
666
677
|
if (!res.ok) {
|
|
667
|
-
|
|
668
|
-
|
|
678
|
+
setRespawnError(data.error || 'Failed to respawn');
|
|
679
|
+
return;
|
|
669
680
|
}
|
|
681
|
+
onRespawned(data);
|
|
670
682
|
} catch (e) {
|
|
671
|
-
if (e.
|
|
683
|
+
if (e && e.name === 'AbortError') {
|
|
684
|
+
setRespawnError('Respawn timeout. Please try again.');
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
if (e.message !== 'Unauthorized') setRespawnError('Network error');
|
|
688
|
+
} finally {
|
|
689
|
+
clearTimeout(timeoutId);
|
|
690
|
+
setRespawning(false);
|
|
672
691
|
}
|
|
673
692
|
};
|
|
674
693
|
|
|
@@ -700,9 +719,16 @@ function SessionDetailScreen({ session, onBack, onKilled }) {
|
|
|
700
719
|
</div>
|
|
701
720
|
<div className="detail-actions">
|
|
702
721
|
{isOffline && (
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
722
|
+
<>
|
|
723
|
+
<button className="btn btn-sm btn-primary" onClick={handleRespawn} disabled={respawning}>
|
|
724
|
+
{respawning ? 'Respawning…' : '↺ Respawn'}
|
|
725
|
+
</button>
|
|
726
|
+
{respawnError && (
|
|
727
|
+
<span style={{ color: 'var(--error)', fontSize: 12, alignSelf: 'center' }}>
|
|
728
|
+
{respawnError}
|
|
729
|
+
</span>
|
|
730
|
+
)}
|
|
731
|
+
</>
|
|
706
732
|
)}
|
|
707
733
|
{!isOffline && (
|
|
708
734
|
<button className="btn btn-sm" onClick={copyAttachCmd} title={`tmux attach -t ${session.name}`}>
|
|
@@ -1047,7 +1073,13 @@ function App() {
|
|
|
1047
1073
|
case 'dashboard': return <DashboardScreen onNavigate={navigate} sessions={sessions} activity={activity} serverStatus={serverStatus} />;
|
|
1048
1074
|
case 'sessions': return <SessionsScreen sessions={sessions} onNavigate={navigate} />;
|
|
1049
1075
|
case 'create': return <CreateSessionScreen onBack={() => navigate('dashboard')} onCreated={s => navigate('detail', s)} />;
|
|
1050
|
-
case 'detail': return <SessionDetailScreen session={selectedSession} onBack={() => navigate('dashboard')} onKilled={() => navigate('sessions')}
|
|
1076
|
+
case 'detail': return <SessionDetailScreen session={selectedSession} onBack={() => navigate('dashboard')} onKilled={() => navigate('sessions')} onRespawned={(respawned) => {
|
|
1077
|
+
setSessions(prev => {
|
|
1078
|
+
const next = prev.filter(s => s.name !== respawned.name);
|
|
1079
|
+
return [{ ...respawned, id: respawned.name }, ...next];
|
|
1080
|
+
});
|
|
1081
|
+
setSelectedSession({ ...respawned, id: respawned.name });
|
|
1082
|
+
}} />;
|
|
1051
1083
|
default: return <DashboardScreen onNavigate={navigate} sessions={sessions} activity={activity} serverStatus={serverStatus} />;
|
|
1052
1084
|
}
|
|
1053
1085
|
};
|
package/package.json
CHANGED